diff --git a/src/de/steamwar/lobby/LobbySystem.java b/src/de/steamwar/lobby/LobbySystem.java index 96cb5f5..69e7cd7 100644 --- a/src/de/steamwar/lobby/LobbySystem.java +++ b/src/de/steamwar/lobby/LobbySystem.java @@ -24,6 +24,7 @@ import de.steamwar.lobby.command.FlyCommand; import de.steamwar.lobby.command.HologramCommand; import de.steamwar.lobby.command.PortalCommand; import de.steamwar.lobby.listener.*; +import de.steamwar.lobby.team.TeamPlayer; import de.steamwar.message.Message; import org.bukkit.plugin.java.JavaPlugin; @@ -58,6 +59,7 @@ public class LobbySystem extends JavaPlugin { new WorldInteraction(); new PlayerSeatListener(); new MapsRotateListener(); + new TeamPlayer(); new AlphaWall(l -> l.getX() > 1199, AlphaWall.REFLECT_X); new AlphaWall(l -> l.getX() < 2977, AlphaWall.REFLECT_X); @@ -66,6 +68,10 @@ public class LobbySystem extends JavaPlugin { } + @Override + public void onDisable() { + TeamPlayer.cleanup(); + } public static LobbySystem getPlugin() { return plugin; diff --git a/src/de/steamwar/lobby/LobbySystem.properties b/src/de/steamwar/lobby/LobbySystem.properties index 2c54623..a5459db 100644 --- a/src/de/steamwar/lobby/LobbySystem.properties +++ b/src/de/steamwar/lobby/LobbySystem.properties @@ -3,6 +3,10 @@ TIME = HH:mm:ss DATE=........ COMMAND_HELP_HEAD=§7---=== (§e{0}§7) ===--- +# ServerTeamNPC's +NPC_CHAT_1 = §fHallo, ich bin {0} und bin ein {1}. +NPC_CHAT_2 = §fWillkommen auf §eSteam§8War§f, viel Spaß dir. + # Portal Command PORTAL_COMMAND_LIST_HELP = §8/§7portal §elist §8- §7Listet alle Portale auf PORTAL_COMMAND_ADD_HELP = §8/§7portal §ecreate §8[§7PortalType§8] §8[§7PortalName§8] §8- §7Fügt ein Portal hinzu diff --git a/src/de/steamwar/lobby/display/Displayable.java b/src/de/steamwar/lobby/display/Displayable.java index 0d7b272..327b83d 100644 --- a/src/de/steamwar/lobby/display/Displayable.java +++ b/src/de/steamwar/lobby/display/Displayable.java @@ -38,24 +38,34 @@ public class Displayable extends BasicListener { private final Set visible = new HashSet<>(); - private final int chunkX; - private final int chunkZ; + private int chunkX; + private int chunkZ; private final Consumer show; private final Consumer hide; + private final Consumer move; private final Function playerFilter; - public Displayable(Location location, Consumer show, Consumer hide) { - this(location, show, hide, player -> true); + public Displayable(Location location, Consumer show, Consumer hide, Consumer move) { + this(location, show, hide, move, player -> true); } public Displayable(Location location, Consumer show, Consumer hide, Function playerFilter) { - this.chunkX = posToChunk(location.getX()); - this.chunkZ = posToChunk(location.getZ()); + this(location, show, hide, player -> {}, playerFilter); + } + + public Displayable(Location location, Consumer show, Consumer hide, Consumer move, Function playerFilter) { this.show = show; this.hide = hide; + this.move = move; this.playerFilter = playerFilter; + setLocation(location); + } + public void setLocation(Location location) { + chunkX = posToChunk(location.getX()); + chunkZ = posToChunk(location.getZ()); Bukkit.getOnlinePlayers().forEach(this::checkLocation); + visible.forEach(move); } public Set getVisitors() { diff --git a/src/de/steamwar/lobby/display/NPC.java b/src/de/steamwar/lobby/display/NPC.java index 4a56d02..3672e3a 100644 --- a/src/de/steamwar/lobby/display/NPC.java +++ b/src/de/steamwar/lobby/display/NPC.java @@ -22,9 +22,12 @@ package de.steamwar.lobby.display; import com.comphenix.tinyprotocol.Reflection; import com.comphenix.tinyprotocol.TinyProtocol; import com.mojang.authlib.GameProfile; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; import java.util.Collections; import java.util.List; import java.util.UUID; @@ -62,46 +65,137 @@ public class NPC { private static final Reflection.ConstructorInvoker headRotationConstructor = Reflection.getConstructor(headRotationPacket); 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 static final Class movePacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutEntityTeleport"); + private static final Reflection.ConstructorInvoker movePacketConstructor = Reflection.getConstructor(movePacket); + private static final Reflection.FieldAccessor movePacketEntity = Reflection.getField(movePacket, int.class, 0); + private static final Reflection.FieldAccessor movePacketX = Reflection.getField(movePacket, double.class, 0); + private static final Reflection.FieldAccessor movePacketY = Reflection.getField(movePacket, double.class, 1); + private static final Reflection.FieldAccessor movePacketZ = Reflection.getField(movePacket, double.class, 2); + private static final Reflection.FieldAccessor movePacketYaw = Reflection.getField(movePacket, byte.class, 0); + private static final Reflection.FieldAccessor movePacketPitch = Reflection.getField(movePacket, byte.class, 1); + private static final Reflection.FieldAccessor movePacketOnGround = Reflection.getField(movePacket, boolean.class, 0); + + 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); + private static Object getDataWatcherObject(int index, Class type) { + for(Field field : dataWatcherRegistry.getFields()) { + if(dataWatcherSerializer.isAssignableFrom(field.getType()) && type.equals(((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0])) { + try { + return dataWatcherObjectConstructor.invoke(index, field.get(null)); + } catch (IllegalAccessException e) { + throw new SecurityException("Could not get field", e); + } + } + } + throw new SecurityException("Could not find Serializer for " + type.getName()); + } + + private static final Class item = Reflection.getClass("{nms.network.syncher}.DataWatcher$Item"); + private static final Reflection.ConstructorInvoker itemConstructor = Reflection.getConstructor(item, dataWatcherObject, Object.class); + private static Object getDataWatcherItem(Object dwo, Object value) { + return itemConstructor.invoke(dwo, value); + } + + 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 dataWatcherObject, Object value) { + Object packet = Reflection.newInstance(metadataPacket); + metadataEntity.set(packet, entityId); + metadataMetadata.set(packet, Collections.singletonList(getDataWatcherItem(dataWatcherObject, value))); + return packet; + } + + private static Object skinPartsDataWatcherObject = getDataWatcherObject(16, Byte.class); private final Displayable display; private final int entityId; private final UUID uuid; private final String name; - private final Location location; + private Location location; private final Object addPlayerInfo; private final Object namedSpawn; + private final Object skinParts; private final Object headRotation; private final Object removePlayerInfo; private final Object destroy; + private Object move; public NPC(Location location, UUID uuid, String name) { this.entityId = Hologram.createEntityId(); this.uuid = uuid; this.name = name; this.location = location; - byte yaw = (byte)(int)(location.getYaw() * 256.0 / 360.0); GameProfile profile = new GameProfile(uuid, name); addPlayerInfo = playerInfoPacket(addPlayer, profile); removePlayerInfo = playerInfoPacket(removePlayer, profile); destroy = Hologram.destroyPacket(entityId); + skinParts = getDataWatcherPacket(skinPartsDataWatcherObject, (byte) 0x7F); + namedSpawn = namedSpawnConstructor.invoke(); namedSpawnEntity.set(namedSpawn, entityId); namedSpawnUUID.set(namedSpawn, uuid); + + headRotation = headRotationConstructor.invoke(); + headRotationEntity.set(headRotation, entityId); + + move = movePacketConstructor.invoke(); + movePacketEntity.set(move, entityId); + + setPackets(location); + + display = new Displayable(location, this::show, this::hide, this::move); + } + + public void setLocation(Location location) { + if (isSimilarLocation(location)) { + return; + } + this.location = location; + setPackets(location); + display.setLocation(location); + } + + private void setPackets(Location location) { + byte yaw = (byte)(int)(location.getYaw() * 256.0 / 360.0); + byte pitch = (byte)(int)(location.getPitch() * 256.0 / 360.0); + headRotationYaw.set(headRotation, yaw); + movePacketX.set(move, location.getX()); + movePacketY.set(move, location.getY()); + movePacketZ.set(move, location.getZ()); + movePacketYaw.set(move, yaw); + movePacketPitch.set(move, pitch); + movePacketOnGround.set(move, true); namedSpawnX.set(namedSpawn, location.getX()); namedSpawnY.set(namedSpawn, location.getY()); namedSpawnZ.set(namedSpawn, location.getZ()); namedSpawnYaw.set(namedSpawn, yaw); - namedSpawnPitch.set(namedSpawn, (byte)(int)(location.getPitch() * 256.0 / 360.0)); + namedSpawnPitch.set(namedSpawn, pitch); + } - headRotation = headRotationConstructor.invoke(); - headRotationEntity.set(headRotation, entityId); - headRotationYaw.set(headRotation, yaw); - - display = new Displayable(location, this::show, this::hide); + private boolean isSimilarLocation(Location location) { + if (this.location == null) { + return false; + } + if (Location.normalizeYaw(this.location.getYaw()) != Location.normalizeYaw(location.getYaw())) { + return false; + } + if (Location.normalizePitch(this.location.getPitch()) != Location.normalizePitch(location.getPitch())) { + return false; + } + if (Math.abs(this.location.getX() - location.getX()) > 0.1) { + return false; + } + if (Math.abs(this.location.getY() - location.getY()) > 0.1) { + return false; + } + return !(Math.abs(this.location.getZ() - location.getZ()) > 0.1); } public Location getLocation() { @@ -116,13 +210,21 @@ public class NPC { TinyProtocol.instance.sendPacket(player, addPlayerInfo); TinyProtocol.instance.sendPacket(player, namedSpawn); TinyProtocol.instance.sendPacket(player, headRotation); + TinyProtocol.instance.sendPacket(player, skinParts); } private void hide(Player player) { - TinyProtocol.instance.sendPacket(player, removePlayerInfo); + if (Bukkit.getOnlinePlayers().stream().noneMatch(p -> p.getUniqueId().equals(uuid))) { + TinyProtocol.instance.sendPacket(player, removePlayerInfo); + } TinyProtocol.instance.sendPacket(player, destroy); } + private void move(Player player) { + TinyProtocol.instance.sendPacket(player, headRotation); + TinyProtocol.instance.sendPacket(player, move); + } + public void delete() { display.delete(); } diff --git a/src/de/steamwar/lobby/team/TeamPlayer.java b/src/de/steamwar/lobby/team/TeamPlayer.java new file mode 100644 index 0000000..6c79212 --- /dev/null +++ b/src/de/steamwar/lobby/team/TeamPlayer.java @@ -0,0 +1,204 @@ +/* + * 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.lobby.team; + +import de.steamwar.lobby.LobbySystem; +import de.steamwar.lobby.display.NPC; +import de.steamwar.lobby.listener.BasicListener; +import de.steamwar.sql.SteamwarUser; +import lombok.AllArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.Villager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityInteractEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.stream.Collectors; + +public class TeamPlayer extends BasicListener { + + private static final List cuboids = new ArrayList<>(); + + static { + cuboids.add(new Cuboid(1509, 52, 1464, 1510, 58, 1469)); + cuboids.add(new Cuboid(1538, 52, 1464, 1539, 58, 1469)); + cuboids.add(new Cuboid(1518, 55, 1433, 1530, 60, 1434)); + cuboids.add(new Cuboid(1587, 52, 1471, 1588, 56, 1475)); + cuboids.add(new Cuboid(1478, 52, 1461, 1479, 56, 1463)); + } + + private static final World world = Bukkit.getWorlds().get(0); + private static final Map entities = new HashMap<>(); + + private Set players = new HashSet<>(); + + private Random random = new Random(); + private List strings = new ArrayList<>(); + { + strings.add("NPC_CHAT_1"); + strings.add("NPC_CHAT_2"); + } + + public static void spawnTeamPlayer(World world, SteamwarUser steamwarUser) { + Location location = new Location(world, 1524.5, 52, 1484.5); + String name = steamwarUser.getUserName(); + NPC npc = new NPC(location, steamwarUser.getUUID(), name); + Villager villager = (Villager) world.spawnEntity(location, EntityType.VILLAGER); + villager.setSilent(true); + villager.setInvulnerable(true); + villager.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 1, false, false, false)); + villager.setCustomName(name); + villager.setProfession(Villager.Profession.NONE); + entities.put(name, npc); + } + + public static void cleanup() { + forceLoad(world, true); + world.getEntitiesByClasses(Villager.class).forEach(Entity::remove); + forceLoad(world, false); + } + + private static void forceLoad(World world, boolean setTo) { + for (int x = -10; x < 10; x++) { + for (int z = -10; z < 10; z++) { + world.setChunkForceLoaded(95 + x, 93 + z, setTo); + } + } + } + + { + forceLoad(world, true); + world.getEntitiesByClasses(Villager.class).forEach(Entity::remove); + forceLoad(world, false); + + SteamwarUser.getServerTeam().forEach(user -> { + spawnTeamPlayer(world, user); + }); + + AtomicInteger count = new AtomicInteger(); + LobbySystem.getPlugin().getLogger().log(Level.INFO, "Loaded " + entities.size() + " team players"); + Bukkit.getScheduler().runTaskTimer(LobbySystem.getPlugin(), () -> { + count.incrementAndGet(); + if (count.get() % (20 * 60 * 60) == 0) { + count.set(0); + List steamwarUsers = SteamwarUser.getServerTeam(); + AtomicInteger added = new AtomicInteger(); + steamwarUsers.forEach(user -> { + if (!entities.containsKey(user.getUserName())) { + spawnTeamPlayer(world, user); + added.incrementAndGet(); + } + }); + AtomicInteger removed = new AtomicInteger(); + List names = steamwarUsers.stream().map(SteamwarUser::getUserName).collect(Collectors.toList()); + new HashSet<>(entities.keySet()).stream().filter(name -> !names.contains(name)).forEach(name -> { + world.getEntitiesByClasses(Villager.class).forEach(entity -> { + if (entity.getName().equals(name)) { + entity.remove(); + } + }); + entities.remove(name).delete(); + removed.incrementAndGet(); + }); + if (added.get() > 0 || removed.get() > 0) { + LobbySystem.getPlugin().getLogger().log(Level.INFO, "Loaded " + added.get() + " team players, removed " + removed.get() + " team players"); + } + } + world.getEntitiesByClasses(Villager.class).forEach(entity -> { + NPC npc = entities.get(entity.getName()); + if (npc != null) { + if (illegalLocation(entity.getLocation())) { + entity.teleport(npc.getLocation()); + return; + } + npc.setLocation(entity.getLocation()); + } + }); + }, 1L, 1L); + } + + private boolean illegalLocation(Location location) { + for (Cuboid cuboid : cuboids) { + if (cuboid.contains(location)) { + return true; + } + } + return false; + } + + @EventHandler + public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { + if (!(event.getRightClicked() instanceof Villager)) { + return; + } + if (!players.add(event.getPlayer())) { + players.remove(event.getPlayer()); + return; + } + SteamwarUser user = SteamwarUser.get(event.getRightClicked().getName()); + String message = strings.get(random.nextInt(strings.size())); + LobbySystem.getMessage().send(message, event.getPlayer(), event.getRightClicked().getName(), user.getUserGroup().getColorCode() + user.getUserGroup().name()); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + players.remove(event.getPlayer()); + } + + @EventHandler + public void onEntityInteract(EntityInteractEvent event) { + if (event.getEntityType() == EntityType.VILLAGER) { + event.setCancelled(true); + } + } + + @EventHandler + public void onEntityDamage(EntityDamageEvent event) { + if (event.getEntityType() == EntityType.VILLAGER) { + event.setCancelled(true); + } + } + + @AllArgsConstructor + private static class Cuboid { + private double x1; + private double y1; + private double z1; + private double x2; + private double y2; + private double z2; + + public boolean contains(Location location) { + return location.getX() >= x1 && location.getX() <= x2 && location.getY() >= y1 && location.getY() <= y2 && location.getZ() >= z1 && location.getZ() <= z2; + } + } +}