Skip to content

Instantly share code, notes, and snippets.

@hugo4715
Forked from DarkSeraphim/.LICENSE.TXT
Last active February 21, 2016 12:00
Show Gist options
  • Save hugo4715/808542f9cca7f2942d29 to your computer and use it in GitHub Desktop.
Save hugo4715/808542f9cca7f2942d29 to your computer and use it in GitHub Desktop.
NPC library written for MC 1.8 - spawns and maintains human NPCs
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
/**
* Copyright (C) 2015 Mark Hendriks
*
* This file is part of DarkSeraphim's NPC library.
*
* DarkSeraphim's NPC library is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* DarkSeraphim's NPC library 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DarkSeraphim's NPC library. If not, see <http://www.gnu.org/licenses/>.
*/
package net.darkseraphim.npc;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.java.JavaPlugin;
/**
*
* @author DarkSeraphim.
*/
public class Example extends JavaPlugin implements Listener
{
private NPC npc;
@Override
public void onEnable()
{
this.npc = NPC.createNPC(this, "NPC", new Location(Bukkit.getWorlds().get(0), 0, 0, 0));
Bukkit.getPluginManager().registerEvents(this, this);
}
@EventHandler
public void onJoin(PlayerJoinEvent event)
{
Player player = event.getPlayer();
Location loc = player.getLocation();
this.npc.setPositionRotation(loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch());
Bukkit.getScheduler().runTaskLater(this, () -> this.npc.spawn(player), 20L);
}
@EventHandler
public void onQuit(PlayerQuitEvent event)
{
this.npc.despawn(event.getPlayer(), true);
}
}
/**
* Copyright (C) 2015 Mark Hendriks
*
* This file is part of DarkSeraphim's NPC library.
*
* DarkSeraphim's NPC library is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* DarkSeraphim's NPC library 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DarkSeraphim's NPC library. If not, see <http://www.gnu.org/licenses/>.
*/
package net.darkseraphim.npc;
import com.google.common.base.Charsets;
import com.mojang.authlib.GameProfile;
import net.minecraft.server.v1_8_R1.EntityPlayer;
import net.minecraft.server.v1_8_R1.EnumPlayerInfoAction;
import net.minecraft.server.v1_8_R1.IChatBaseComponent;
import net.minecraft.server.v1_8_R1.MinecraftServer;
import net.minecraft.server.v1_8_R1.Packet;
import net.minecraft.server.v1_8_R1.PacketPlayOutEntityDestroy;
import net.minecraft.server.v1_8_R1.PacketPlayOutNamedEntitySpawn;
import net.minecraft.server.v1_8_R1.PacketPlayOutPlayerInfo;
import net.minecraft.server.v1_8_R1.PlayerConnection;
import net.minecraft.server.v1_8_R1.PlayerInteractManager;
import net.minecraft.server.v1_8_R1.WorldServer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_8_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_8_R1.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* @author DarkSeraphim.
*/
public class NPC extends EntityPlayer
{
// Returns the Packets required to spawn a NPC in form of a List
private static final Function<NPC, List<Packet>> getSpawnPackets = (npc) ->
{
if(npc.spawnPackets == null)
{
Packet[] packets = new Packet[]
{
new PacketPlayOutPlayerInfo(EnumPlayerInfoAction.ADD_PLAYER, npc),
new PacketPlayOutNamedEntitySpawn(npc),
new PacketPlayOutPlayerInfo(EnumPlayerInfoAction.REMOVE_PLAYER, npc)
};
npc.spawnPackets = Arrays.asList(packets);
}
return npc.spawnPackets;
};
// Returns the Packet required to despawn a NPC
private static final Function<NPC, Consumer<PlayerConnection>> getDespawnPacket = (npc) ->
{
if(npc.despawnPacket == null)
{
npc.despawnPacket = new PacketPlayOutEntityDestroy(npc.getId());
}
return (connection) -> connection.sendPacket(npc.despawnPacket);
};
private final Plugin plugin;
private List<Packet> spawnPackets;
private Packet despawnPacket;
private Set<UUID> tracked = new HashSet<>();
private BukkitTask task;
private NPC(Plugin plugin, WorldServer world, GameProfile profile, PlayerInteractManager manager)
{
super(MinecraftServer.getServer(), world, profile, manager);
this.plugin = plugin;
this.ping = 100;
}
/**
* @return the name shown in the tab list
*/
@Override
public IChatBaseComponent getPlayerListName()
{
// Override to set a name
// Not like we would need it, we don't even want a tab list entry
return null;
}
/**
* Creates an NPC, owned by {@code plugin}, with name {@code name} at {@code location}.
*
* @param plugin Plugin which owns the NPC.
* @param name Name of the NPC.
* @param location Location where the NPC should spawn.
* @return NPC which was created.
*/
public static NPC createNPC(Plugin plugin, String name, Location location)
{
GameProfile profile = new GameProfile(UUID.nameUUIDFromBytes(("NPC:" + name).getBytes(Charsets.UTF_8)), name);
WorldServer world = getWorld(location.getWorld());
NPC npc = new NPC(plugin, world, profile, new PlayerInteractManager(world));
npc.setLocation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
return npc;
}
// Returns the NMS World from the Bukkit World
private static WorldServer getWorld(World world)
{
return ((CraftWorld) world).getHandle();
}
// Returns the PlayerConnection of the Player
private PlayerConnection getConnection(Player player)
{
return ((CraftPlayer) player).getHandle().playerConnection;
}
/**
* Spawns (shows) this NPC for Player.
*
* @param player player to spawn NPC for.
*/
public void spawn(Player player)
{
Bukkit.getScheduler().runTaskLater(this.plugin, () ->
{
PlayerConnection connection = getConnection(player);
NPC.getSpawnPackets.apply(this).stream().forEach(connection::sendPacket);
this.tracked.add(player.getUniqueId());
if (this.task == null)
{
this.task = new NPCTrackerTask(this).runTaskTimer(this.plugin, 0L, 5L);
}
}, 1L);
}
/**
* Returns a Stream of all Players which track this NPC.
*
* @return a {@code Stream<Player>} of tracked Players
*/
protected Stream<Player> getTrackedPlayers()
{
return this.tracked.stream().map(Bukkit::getPlayer).filter(player -> player != null);
}
/**
* Despawns (hides) NPC for the given Player.
*
* @param player player to despawn NPC for.
*/
public void despawn(Player player)
{
despawn(player, false);
}
/**
* Despawns (hides) NPC for the given Player.
*
* <i>Note: always invoke this method onQuit. It's good memory management!</i>
*
* @param player player to despawn NPC for.
* @param logout whether it was triggered by a logout.
*/
public void despawn(Player player, boolean logout)
{
if(this.tracked.remove(player.getUniqueId()) && this.tracked.isEmpty())
{
this.task.cancel();
this.task = null;
}
if(!logout)
{
cleanup(player);
}
}
/**
* Sends a PacketPlayOutDestroyEntity to the Player.
*
* @param player Player which will receive the packet
*/
protected void cleanup(Player player)
{
PlayerConnection connection = getConnection(player);
NPC.getDespawnPacket.apply(this).accept(connection);
}
/**
* Auxiliary method which returns the distance^2 between
* the Player and this NPC
* @param player Player with which the distance should be computed for.
* @return the distance^2 between the Player and this NPC
*/
protected double getDistanceSquared(Player player)
{
Location loc = player.getLocation();
double dx = loc.getX() - this.locX;
double dy = loc.getY() - this.locY;
double dz = loc.getZ() - this.locZ;
return dx * dx + dy * dy + dz * dz;
}
}
/**
* Copyright (C) 2015 Mark Hendriks
*
* This file is part of DarkSeraphim's NPC library.
*
* DarkSeraphim's NPC library is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* DarkSeraphim's NPC library 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DarkSeraphim's NPC library. If not, see <http://www.gnu.org/licenses/>.
*/
package net.darkseraphim.npc;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* @author DarkSeraphim.
*/
public class NPCTrackerTask extends BukkitRunnable
{
/**
* Composition of multiple Consumers, allowing you to chain Consumers
* in one lambda call.
*
* @param <T> Generic type of composed consumers
*/
private static class CompositeConsumer<T> implements Consumer<T>
{
private final List<Consumer<T>> consumers;
private CompositeConsumer(List<Consumer<T>> consumers)
{
this.consumers = consumers;
}
/**
* Accepts t for all contained Consumers.
*
* @param t parameter to accept
*/
@Override
public void accept(T t)
{
this.consumers.stream().forEach(consumer -> consumer.accept(t));
}
/**
* Creates a CompositeConsumer of zero or more consumers.
*
* @param consumers Consumers to compose.
* @param <T> Generic type of the Consumers.
* @return A composition of the given consumers.
*/
@SafeVarargs
private static <T> Consumer<T> of(Consumer<T>... consumers)
{
return new CompositeConsumer<>(Arrays.asList(consumers));
}
}
/**
* Predicate which only returns true once, and will return only return true once
* there has been at least one validation which resulted to false.
* @param <T> Predicate type
*/
private static class CachedPredicate<T extends Map.Entry<Player, Boolean>> implements Predicate<T>
{
private final Predicate<T> original;
private final Set<UUID> cache = new HashSet<>();
private CachedPredicate(Predicate<T> original)
{
this.original = original;
}
/**
* Tests if the predicate holds with the given variable, if and only if it previously returned false
*
* @param t variable to test
* @return {@code result && !this.cache.contains(t.getKey().getUniqueId())}
* @modifies adds {@code t.getKey().getUniqueId()} to the cache if \result is true,
* or removes it if \result is false
*/
@Override
public boolean test(T t)
{
boolean result = this.original.test(t);
UUID uuid = t.getKey().getUniqueId();
if (result)
{
return this.cache.add(uuid);
}
else
{
this.cache.remove(uuid);
}
return false;
}
}
// Distance to respawn, customise to your liking
private static final double DISTANCE = 50;
private static final double DISTANCE_SQUARED = DISTANCE * DISTANCE;
private final Function<Player, Map.Entry<Player, Boolean>> inRange;
private final NPC npc;
private final Consumer<Player> update;
private final Predicate<Map.Entry<Player, Boolean>> filter;
protected NPCTrackerTask(NPC npc)
{
this.npc = npc;
this.inRange = player -> new AbstractMap.SimpleEntry<>(player, this.npc.getDistanceSquared(player) < DISTANCE_SQUARED);
Consumer<Player> cleanup = this.npc::cleanup;
Consumer<Player> spawn = this.npc::spawn;
this.update = CompositeConsumer.<Player>of(cleanup, spawn);
this.filter = new CachedPredicate<>(Map.Entry::getValue);
}
/**
* Respawns the NPC for any players which moved within {@code DISTANCE} of the NPC.
*/
@Override
public void run()
{
this.npc.getTrackedPlayers().map(this.inRange)
.filter(this.filter)
.map(Map.Entry::getKey)
.forEach(this.update);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment