Rework hologram rendering (WIP: text displays not yet functional)

This commit is contained in:
fullwall 2024-04-15 01:42:09 +08:00
parent 5f13340dea
commit cd7f3366ab
10 changed files with 258 additions and 370 deletions

View File

@ -521,7 +521,7 @@ public class Citizens extends JavaPlugin implements CitizensPlugin {
}
Translator.setInstance(new File(getDataFolder(), "lang"), locale);
if (!locale.getLanguage().equals("en")) {
Messaging.logTr(Messages.CONTRIBUTE_TO_TRANSLATION_PROMPT);
Messaging.logTr(Messages.CONTRIBUTE_TO_TRANSLATION_PROMPT, locale.getLanguage());
}
}

View File

@ -112,7 +112,7 @@ import net.citizensnpcs.trait.ClickRedirectTrait;
import net.citizensnpcs.trait.CommandTrait;
import net.citizensnpcs.trait.Controllable;
import net.citizensnpcs.trait.CurrentLocation;
import net.citizensnpcs.trait.HologramTrait;
import net.citizensnpcs.trait.HologramTrait.HologramRenderer;
import net.citizensnpcs.trait.ShopTrait;
import net.citizensnpcs.trait.versioned.SnowmanTrait;
import net.citizensnpcs.util.ChunkCoord;
@ -469,17 +469,12 @@ public class EventListen implements Listener {
if (skinnable.getSkinTracker().getSkin() != null) {
skinnable.getSkinTracker().getSkin().apply(skinnable);
}
}
if (npc.isSpawned() && npc.getEntity().getType() == EntityType.PLAYER) {
onNPCPlayerLinkToPlayer(event);
}
ClickRedirectTrait crt = npc.getTraitNullable(ClickRedirectTrait.class);
if (crt != null) {
HologramTrait ht = crt.getRedirectNPC().getTraitNullable(HologramTrait.class);
if (ht != null) {
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(),
() -> ht.onHologramSeenByPlayer(npc, event.getPlayer()), 2);
}
if (npc.data().has(NPC.Metadata.HOLOGRAM_RENDERER)) {
HologramRenderer hr = npc.data().get(NPC.Metadata.HOLOGRAM_RENDERER);
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(),
() -> hr.onSeenByPlayer(event.getPlayer()), 2);
}
}

View File

@ -6,11 +6,9 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import org.bukkit.Bukkit;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDeathEvent;
@ -44,8 +42,7 @@ import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.trait.trait.MobType;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.npc.ai.NPCHolder;
import net.citizensnpcs.trait.ClickRedirectTrait;
import net.citizensnpcs.trait.HologramTrait;
import net.citizensnpcs.trait.HologramTrait.HologramRenderer;
import net.citizensnpcs.trait.MirrorTrait;
import net.citizensnpcs.trait.RotationTrait;
import net.citizensnpcs.trait.RotationTrait.PacketRotationSession;
@ -71,16 +68,13 @@ public class ProtocolLibListener implements Listener {
PacketContainer packet = event.getPacket();
int version = manager.getProtocolVersion(event.getPlayer());
if (npc.data().has(NPC.Metadata.HOLOGRAM_FOR) || npc.data().has(NPC.Metadata.HOLOGRAM_LINE_SUPPLIER)) {
Function<Player, String> hvs = npc.data().get(NPC.Metadata.HOLOGRAM_LINE_SUPPLIER);
if (npc.data().has(NPC.Metadata.HOLOGRAM_RENDERER)) {
HologramRenderer hr = npc.data().get(NPC.Metadata.HOLOGRAM_RENDERER);
Object fakeName = null;
if (hvs != null) {
String suppliedName = hvs.apply(event.getPlayer());
fakeName = version <= 340 ? suppliedName
: Optional.of(Messaging.minecraftComponentFromRawMessage(suppliedName));
}
boolean sneaking = npc.getOrAddTrait(ClickRedirectTrait.class).getRedirectNPC()
.getOrAddTrait(HologramTrait.class).isHologramSneaking(npc, event.getPlayer());
String suppliedName = hr.getPerPlayerText(npc, event.getPlayer());
fakeName = version <= 340 ? suppliedName
: Optional.of(Messaging.minecraftComponentFromRawMessage(suppliedName));
boolean sneaking = hr.isSneaking(npc, event.getPlayer());
boolean delta = false;
if (version < 761) {

View File

@ -108,6 +108,9 @@ public class Settings {
DEFAULT_DISTANCE_MARGIN(
"The default MOVEMENT distance in blocks where the NPC will move to before considering a path finished<br>Note: this is different from the PATHFINDING distance which is specified by path-distance-margin",
"npc.pathfinding.default-distance-margin", 1),
DEFAULT_HOLOGRAM_RENDERER(
"The default renderer for holograms, must be one of the following:<br>interaction - matches inbuilt nametags most closely<br>display - allows for different colored backgrounds<br>armorstand - creates an armorstand and teleports it to the player",
"npc.hologram.default-renderer", "interaction"),
DEFAULT_LOOK_CLOSE("Enable look close by default", "npc.default.look-close.enabled", false),
DEFAULT_LOOK_CLOSE_RANGE("Default look close range in blocks", "npc.default.look-close.range", 10),
DEFAULT_NPC_HOLOGRAM_LINE_HEIGHT("Default distance between hologram lines", "npc.hologram.default-line-height",
@ -166,9 +169,6 @@ public class Settings {
"Minecraft will pick a 'close-enough' location when pathfinding to a block if it can't find a direct path<br>Disabled by default",
"npc.pathfinding.disable-mc-fallback-navigation", true),
DISABLE_TABLIST("Whether to remove NPCs from the tablist", "npc.tablist.disable", true),
DISPLAY_ENTITY_HOLOGRAMS(
"Whether to use display entities for holograms by default (in theory more performant than armor stands)<br>Requires 1.19.4 or above, defaults to false",
"npc.hologram.use-display-entities", false),
ENTITY_SPAWN_WAIT_DURATION(
"Entities are no longer spawned until the chunks are loaded from disk<br>Wait for chunk loading for one second by default, increase if your disk is slow",
"general.entity-spawn-wait-ticks", "general.wait-for-entity-spawn", "1s"),
@ -284,11 +284,11 @@ public class Settings {
this.value = value;
}
Setting(String migrate, String path, Object value) {
if (migrate.contains(".")) {
this.migrate = migrate;
Setting(String migrateOrComments, String path, Object value) {
if (migrateOrComments.contains(".") && !migrateOrComments.contains(" ")) {
migrate = migrateOrComments;
} else {
comments = migrate;
comments = migrateOrComments;
}
this.path = path;
this.value = value;

View File

@ -735,7 +735,7 @@ public class NPCCommands {
throw new CommandException(Messaging.tr(Messages.NPC_CREATE_MISSING_MOBTYPE, args.getFlag("type")));
}
int nameLength = SpigotUtil.getMaxNameLength(type);
if (Placeholders.replace(Messaging.parseComponents(name), sender, npc).length() > nameLength) {
if (Placeholders.replace(Messaging.stripColor(name), sender, npc).length() > nameLength) {
Messaging.sendErrorTr(sender, Messages.NPC_NAME_TOO_LONG, nameLength);
name = name.substring(0, nameLength);
}
@ -1042,7 +1042,7 @@ public class NPCCommands {
if (!(sender instanceof ConsoleCommandSender)
&& !followingNPC.getOrAddTrait(Owner.class).isOwnedBy(sender))
throw new CommandException(CommandMessages.MUST_BE_OWNER);
boolean following = !trait.isEnabled();
boolean following = explicit == null ? !trait.isEnabled() : explicit;
trait.follow(following ? followingNPC.getEntity() : null);
Messaging.sendTr(sender, following ? Messages.FOLLOW_SET : Messages.FOLLOW_UNSET, npc.getName(),
followingNPC.getName());
@ -2532,9 +2532,9 @@ public class NPCCommands {
permission = "citizens.npc.rename")
public void rename(CommandContext args, CommandSender sender, NPC npc) {
String oldName = npc.getName();
String newName = Messaging.parseComponents(args.getJoinedStrings(1));
String newName = args.getJoinedStrings(1);
int nameLength = SpigotUtil.getMaxNameLength(npc.getOrAddTrait(MobType.class).getType());
if (newName.length() > nameLength) {
if (Placeholders.replace(Messaging.stripColor(newName), sender, npc).length() > nameLength) {
Messaging.sendErrorTr(sender, Messages.NPC_NAME_TOO_LONG, nameLength);
newName = newName.substring(0, nameLength);
}

View File

@ -192,7 +192,7 @@ public class CitizensNPC extends AbstractNPC {
@Override
public boolean requiresNameHologram() {
return !data().has(NPC.Metadata.HOLOGRAM_FOR)
return !data().has(NPC.Metadata.HOLOGRAM_RENDERER)
&& (super.requiresNameHologram() || Setting.ALWAYS_USE_NAME_HOLOGRAM.asBoolean());
}

View File

@ -38,8 +38,8 @@ public class FollowTrait extends Trait {
}
private void cancelNavigationIfActive() {
if (npc.getNavigator().isNavigating() && this.entity != null && npc.getNavigator().getEntityTarget() != null
&& this.entity == npc.getNavigator().getEntityTarget().getTarget()) {
if (npc.getNavigator().isNavigating() && entity != null && npc.getNavigator().getEntityTarget() != null
&& entity == npc.getNavigator().getEntityTarget().getTarget()) {
npc.getNavigator().cancelNavigation();
}
}
@ -47,10 +47,10 @@ public class FollowTrait extends Trait {
/**
* Sets the {@link Entity} to follow
*/
public void follow(Entity entity) {
followingUUID = entity == null ? null : entity.getUniqueId();
public void follow(Entity follow) {
cancelNavigationIfActive();
this.entity = null;
followingUUID = follow == null ? null : follow.getUniqueId();
entity = null;
}
public Entity getFollowing() {

View File

@ -3,8 +3,7 @@ package net.citizensnpcs.trait;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -15,14 +14,17 @@ import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Display.Billboard;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.TextDisplay;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Transformation;
import org.joml.Vector3d;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import net.citizensnpcs.Settings.Setting;
@ -36,6 +38,7 @@ import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.api.util.Placeholders;
import net.citizensnpcs.api.util.SpigotUtil;
import net.citizensnpcs.util.NMS;
@ -45,11 +48,10 @@ import net.citizensnpcs.util.Util;
* Manages a set of <em>holograms</em> attached to the NPC. Holograms are lines of text or items that follow the NPC at
* some offset (typically vertically offset).
*/
// TODO: refactor this class
// TODO: cleanup
@TraitName("hologramtrait")
public class HologramTrait extends Trait {
private Location currentLoc;
private BiFunction<String, Player, String> customHologramSupplier;
private double lastEntityBbHeight = 0;
private boolean lastNameplateVisible;
@Persist
@ -58,7 +60,6 @@ public class HologramTrait extends Trait {
private HologramLine nameLine;
private final NPCRegistry registry = CitizensAPI.createCitizensBackedNPCRegistry(new MemoryNPCDataStore());
private int t;
private boolean useDisplayEntities = Setting.DISPLAY_ENTITY_HOLOGRAMS.asBoolean();
@Persist
private int viewRange = -1;
@ -77,6 +78,11 @@ public class HologramTrait extends Trait {
reloadLineHolograms();
}
public void addLine(String text, HologramRenderer hr) {
lines.add(new HologramLine(text, hr));
reloadLineHolograms();
}
/**
* Adds a new hologram line which will displayed over an NPC's head. It will not persist to disk and will last for
* the specified amount of ticks.
@ -101,64 +107,6 @@ public class HologramTrait extends Trait {
lines.clear();
}
@SuppressWarnings("deprecation")
private NPC createHologram(String line, double heightOffset) {
NPC hologramNPC = null;
if (useDisplayEntities) {
hologramNPC = registry.createNPC(EntityType.INTERACTION, line);
hologramNPC.addTrait(new ClickRedirectTrait(npc));
} else {
hologramNPC = registry.createNPC(EntityType.ARMOR_STAND, line);
hologramNPC.getOrAddTrait(ArmorStandTrait.class).setAsHelperEntityWithName(npc);
}
hologramNPC.data().set(NPC.Metadata.HOLOGRAM_FOR, npc.getUniqueId().toString());
if (Setting.PACKET_HOLOGRAMS.asBoolean()) {
hologramNPC.addTrait(PacketNPC.class);
}
if (viewRange != -1) {
hologramNPC.data().set(NPC.Metadata.TRACKING_RANGE, viewRange);
} else if (npc.data().has(NPC.Metadata.TRACKING_RANGE)) {
hologramNPC.data().set(NPC.Metadata.TRACKING_RANGE, npc.data().get(NPC.Metadata.TRACKING_RANGE));
}
hologramNPC.spawn(currentLoc.clone().add(0, getEntityBbHeight() + heightOffset, 0));
Matcher itemMatcher = ITEM_MATCHER.matcher(line);
if (itemMatcher.matches()) {
Material item = SpigotUtil.isUsing1_13API() ? Material.matchMaterial(itemMatcher.group(1), false)
: Material.matchMaterial(itemMatcher.group(1));
if (item == null) {
hologramNPC.destroy();
throw new IllegalStateException("Unknown material " + line);
}
ItemStack itemStack = new ItemStack(item, 1);
NPC itemNPC = registry.createNPCUsingItem(EntityType.DROPPED_ITEM, "", itemStack);
itemNPC.data().setPersistent(NPC.Metadata.NAMEPLATE_VISIBLE, false);
if (itemMatcher.group(2) != null) {
if (itemMatcher.group(2).charAt(1) == '{') {
Bukkit.getUnsafe().modifyItemStack(itemStack, itemMatcher.group(2).substring(1));
itemNPC.setItemProvider(() -> itemStack);
} else {
itemNPC.getOrAddTrait(ScoreboardTrait.class)
.setColor(Util.matchEnum(ChatColor.values(), itemMatcher.group(2).substring(1)));
}
}
if (hologramNPC.data().has(NPC.Metadata.TRACKING_RANGE)) {
itemNPC.data().setPersistent(NPC.Metadata.TRACKING_RANGE,
hologramNPC.data().get(NPC.Metadata.TRACKING_RANGE));
}
itemNPC.getOrAddTrait(MountTrait.class).setMountedOn(hologramNPC.getUniqueId());
itemNPC.spawn(currentLoc);
NPC hn = hologramNPC;
itemNPC.addRunnable(() -> {
if (!itemNPC.isSpawned() || !hn.isSpawned()) {
itemNPC.destroy();
}
});
}
lastEntityBbHeight = getEntityBbHeight();
return hologramNPC;
}
private double getEntityBbHeight() {
return NMS.getBoundingBoxHeight(npc.getEntity());
}
@ -175,14 +123,6 @@ public class HologramTrait extends Trait {
return base;
}
/**
* Note: this is implementation-specific and may be removed at a later date.
*/
public Collection<Entity> getHologramEntities() {
return lines.stream().filter(l -> l.hologram != null && l.hologram.getEntity() != null)
.map(l -> l.hologram.getEntity()).collect(Collectors.toList());
}
/**
* @return The line height between each hologram line, in blocks
*/
@ -197,24 +137,10 @@ public class HologramTrait extends Trait {
return Lists.transform(lines, l -> l.text);
}
/**
* Note: this is implementation-specific and may be removed at a later date.
*/
public Entity getNameEntity() {
return nameLine != null && nameLine.hologram.isSpawned() ? nameLine.hologram.getEntity() : null;
}
public double getViewRange() {
public int getViewRange() {
return viewRange;
}
public boolean isHologramSneaking(NPC hologram, Player player) {
if (nameLine != null && hologram == nameLine.hologram && npc.getEntity() instanceof Player
&& ((Player) npc.getEntity()).isSneaking())
return true;
return false;
}
@Override
public void load(DataKey root) {
clear();
@ -229,33 +155,7 @@ public class HologramTrait extends Trait {
@Override
public void onDespawn() {
if (nameLine != null) {
nameLine.removeNPC();
nameLine = null;
}
for (HologramLine line : lines) {
line.removeNPC();
}
}
public void onHologramSeenByPlayer(NPC hologram, Player player) {
if (useDisplayEntities && npc.isSpawned()) {
double height = -1;
if (nameLine != null && hologram.equals(nameLine.hologram)) {
height = 0;
} else {
for (int i = 0; i < lines.size(); i++) {
if (hologram.equals(lines.get(i).hologram)) {
height = getHeight(i);
break;
}
}
}
if (height == -1)
return;
NMS.linkTextInteraction(player, hologram.getEntity(), npc.getEntity(), height);
}
reloadLineHolograms();
}
@Override
@ -270,32 +170,17 @@ public class HologramTrait extends Trait {
lastNameplateVisible = Boolean
.parseBoolean(npc.data().<Object> get(NPC.Metadata.NAMEPLATE_VISIBLE, true).toString());
currentLoc = npc.getStoredLocation();
if (npc.requiresNameHologram() && lastNameplateVisible) {
nameLine = new HologramLine(npc.getRawName(), false);
nameLine.spawnNPC(0);
}
for (int i = 0; i < lines.size(); i++) {
lines.get(i).spawnNPC(getHeight(i));
}
}
private void reloadLineHolograms() {
for (HologramLine line : lines) {
line.removeNPC();
}
if (!npc.isSpawned())
return;
if (npc.requiresNameHologram() && lastNameplateVisible) {
if (nameLine != null) {
nameLine.removeNPC();
}
nameLine = new HologramLine(npc.getRawName(), false);
nameLine.spawnNPC(0);
}
for (int i = 0; i < lines.size(); i++) {
lines.get(i).spawnNPC(getHeight(i));
if (nameLine != null) {
nameLine.removeNPC();
nameLine = null;
}
currentLoc = null;
}
/**
@ -318,9 +203,6 @@ public class HologramTrait extends Trait {
onDespawn();
return;
}
if (currentLoc == null) {
currentLoc = npc.getStoredLocation().clone();
}
boolean nameplateVisible = Boolean
.parseBoolean(npc.data().<Object> get(NPC.Metadata.NAMEPLATE_VISIBLE, true).toString());
if (npc.requiresNameHologram()) {
@ -328,12 +210,13 @@ public class HologramTrait extends Trait {
nameLine.removeNPC();
nameLine = null;
} else if (nameLine == null && nameplateVisible) {
nameLine = new HologramLine(npc.getRawName(), false);
nameLine.spawnNPC(0);
nameLine = new HologramLine(npc.getRawName(),
SUPPORTS_DISPLAY ? new InteractionVehicleRenderer() : new ArmorstandVehicleRenderer());
}
}
Location npcLoc = npc.getStoredLocation();
boolean updatePosition = Setting.HOLOGRAM_ALWAYS_UPDATE_POSITION.asBoolean()
Vector3d offset = new Vector3d();
boolean updatePosition = Setting.HOLOGRAM_ALWAYS_UPDATE_POSITION.asBoolean() || currentLoc == null
|| currentLoc.getWorld() != npcLoc.getWorld() || currentLoc.distance(npcLoc) >= 0.001
|| lastNameplateVisible != nameplateVisible
|| Math.abs(lastEntityBbHeight - getEntityBbHeight()) >= 0.05;
@ -349,45 +232,28 @@ public class HologramTrait extends Trait {
currentLoc = npcLoc.clone();
lastEntityBbHeight = getEntityBbHeight();
}
if (nameLine != null && nameLine.hologram.isSpawned()) {
if (updatePosition && !useDisplayEntities) {
nameLine.hologram.teleport(npcLoc.clone().add(0, getEntityBbHeight(), 0), TeleportCause.PLUGIN);
if (nameLine != null) {
if (updatePosition) {
nameLine.render(offset);
}
if (updateName) {
nameLine.setText(npc.getRawName());
}
if (useDisplayEntities && nameLine.hologram.getEntity().getVehicle() == null) {
npc.getEntity().addPassenger(nameLine.hologram.getEntity());
}
}
for (int i = 0; i < lines.size(); i++) {
HologramLine line = lines.get(i);
NPC hologramNPC = line.hologram;
if (hologramNPC == null || !hologramNPC.isSpawned())
continue;
if (line.ticks > 0 && --line.ticks == 0) {
lines.remove(i--).removeNPC();
;
continue;
}
if (updatePosition && !useDisplayEntities) {
Location tp = npcLoc.clone().add(0, lastEntityBbHeight + getHeight(i), 0);
hologramNPC.teleport(tp, TeleportCause.PLUGIN);
if (updatePosition) {
offset.y = getHeight(i);
line.render(offset);
}
if (useDisplayEntities && hologramNPC.getEntity().getVehicle() == null) {
npc.getEntity().addPassenger(hologramNPC.getEntity());
if (updateName) {
line.setText(line.text);
}
String text = line.text;
if (ITEM_MATCHER.matcher(text).matches()) {
hologramNPC.data().set(NPC.Metadata.NAMEPLATE_VISIBLE, false);
continue;
}
if (!updateName) {
continue;
}
line.setText(text);
}
}
@ -421,9 +287,7 @@ public class HologramTrait extends Trait {
}
HologramLine line = lines.get(idx);
line.setText(text);
if (line.hologram == null) {
reloadLineHolograms();
}
reloadLineHolograms();
}
/**
@ -442,7 +306,7 @@ public class HologramTrait extends Trait {
* Sets the margin of a line at a specific index
*
* @param idx
* The index
* The line index
* @param type
* The margin type, top or bottom
* @param margin
@ -457,63 +321,43 @@ public class HologramTrait extends Trait {
reloadLineHolograms();
}
/**
* Implementation-specific method: {@see NPC.Metadata#HOLOGRAM_LINE_SUPPLIER}
*/
public void setPerPlayerTextSupplier(BiFunction<String, Player, String> nameSupplier) {
customHologramSupplier = nameSupplier;
}
public void setUseDisplayEntities(boolean use) {
useDisplayEntities = use;
reloadLineHolograms();
}
public void setViewRange(int range) {
this.viewRange = range;
reloadLineHolograms();
}
public abstract class AbstractRenderer implements HologramRenderer {
protected NPC npc;
public class ArmorstandRenderer extends SingleEntityHologramRenderer {
@Override
public void destroy() {
if (npc != null) {
npc.destroy();
npc = null;
}
protected NPC createNPC(Entity base, String name, Vector3d offset) {
NPC npc = registry.createNPC(EntityType.ARMOR_STAND, name);
npc.getOrAddTrait(ArmorStandTrait.class).setAsHelperEntityWithName(npc);
return npc;
}
@Override
public void render(Entity base, Vector3d offset) {
if (npc == null)
return;
npc.getEntity().teleport(base.getLocation().clone().add(offset.x, offset.y, offset.z),
protected void render0(NPC npc, Vector3d offset) {
hologram.getEntity().teleport(npc.getStoredLocation().clone().add(offset.x, offset.y, offset.z),
TeleportCause.PLUGIN);
}
}
protected abstract NPC spawnNPC(Entity base, String text, Vector3d offset);
public class ArmorstandVehicleRenderer extends ArmorstandRenderer {
@Override
protected NPC createNPC(Entity base, String name, Vector3d offset) {
NPC npc = registry.createNPC(EntityType.ARMOR_STAND, name);
npc.getOrAddTrait(ArmorStandTrait.class).setAsHelperEntityWithName(npc);
return npc;
}
@Override
public void updateText(Entity base, String text) {
if (npc == null)
return;
NMS.setCustomName(base, text, text);
public void render0(NPC base, Vector3d offset) {
if (hologram.getEntity().getVehicle() == null) {
base.getEntity().addPassenger(hologram.getEntity());
}
}
}
public class ArmorstandRenderer extends AbstractRenderer {
@Override
protected NPC spawnNPC(Entity base, String name, Vector3d offset) {
NPC hologramNPC = registry.createNPC(EntityType.ARMOR_STAND, name);
hologramNPC.getOrAddTrait(ArmorStandTrait.class).setAsHelperEntityWithName(npc);
return hologramNPC;
}
}
private class HologramLine implements Function<Player, String> {
NPC hologram;
private class HologramLine {
double mb, mt;
boolean persist;
HologramRenderer renderer;
@ -521,55 +365,166 @@ public class HologramTrait extends Trait {
int ticks;
public HologramLine(String text, boolean persist) {
this(text, persist, -1);
this(text, persist, -1,
SUPPORTS_DISPLAY && Setting.DEFAULT_HOLOGRAM_RENDERER.asString().equalsIgnoreCase("interaction")
? new InteractionVehicleRenderer()
: SUPPORTS_DISPLAY
&& Setting.DEFAULT_HOLOGRAM_RENDERER.asString().equalsIgnoreCase("display")
? new TextDisplayVehicleRenderer()
: new ArmorstandRenderer());
}
public HologramLine(String text, boolean persist, int ticks) {
setText(text);
this.persist = persist;
this.ticks = ticks;
this(text, persist, ticks,
SUPPORTS_DISPLAY && Setting.DEFAULT_HOLOGRAM_RENDERER.asString().equalsIgnoreCase("interaction")
? new InteractionVehicleRenderer()
: SUPPORTS_DISPLAY
&& Setting.DEFAULT_HOLOGRAM_RENDERER.asString().equalsIgnoreCase("display")
? new TextDisplayVehicleRenderer()
: new ArmorstandRenderer());
}
public HologramLine(String text, boolean persist, int ticks, HologramRenderer hr) {
if (ITEM_MATCHER.matcher(text).matches()) {
mb = 0.21;
mt = 0.07;
hr = new ItemRenderer();
}
this.persist = persist;
this.ticks = ticks;
this.renderer = hr;
setText(text);
}
@Override
public String apply(Player viewer) {
return Placeholders.replace(text, viewer, npc);
public HologramLine(String text, HologramRenderer renderer) {
this(text, false, -1, renderer);
}
public void removeNPC() {
if (hologram == null)
return;
renderer.destroy();
}
hologram.destroy();
renderer = null;
hologram = null;
public void render(Vector3d vector3d) {
renderer.render(npc, vector3d);
}
public void setText(String text) {
this.text = text == null ? "" : text;
if (ITEM_MATCHER.matcher(text).matches()) {
renderer.destroy();
mb = 0.21;
mt = 0.07;
renderer = new ItemRenderer();
}
renderer.updateText(npc, text);
}
}
if (hologram != null) {
// renderer.updateText(hologram, text);
String name = Placeholders.replace(text, null, npc);
hologram.setName(name);
if (Placeholders.containsPlaceholders(text)) {
hologram.data().set(NPC.Metadata.HOLOGRAM_LINE_SUPPLIER, this);
public static interface HologramRenderer {
void destroy();
String getPerPlayerText(NPC npc, Player viewer);
default boolean isSneaking(NPC npc, Player player) {
return NMS.isSneaking(player);
}
default void onSeenByPlayer(Player player) {
}
void render(NPC npc, Vector3d offset);
void updateText(NPC npc, String text);
}
public class InteractionVehicleRenderer extends SingleEntityHologramRenderer {
private Vector3d lastOffset;
@Override
protected NPC createNPC(Entity base, String name, Vector3d offset) {
lastOffset = new Vector3d(offset);
return registry.createNPC(EntityType.INTERACTION, name);
}
@Override
public void onSeenByPlayer(Player player) {
if (lastOffset == null)
return;
NMS.linkTextInteraction(player, hologram.getEntity(), npc.getEntity(), lastOffset.y);
}
@Override
public void render0(NPC npc, Vector3d offset) {
lastOffset = new Vector3d(offset);
if (hologram.getEntity().getVehicle() == null) {
npc.getEntity().addPassenger(hologram.getEntity());
}
}
}
public class ItemRenderer extends SingleEntityHologramRenderer {
@Override
protected NPC createNPC(Entity base, String name, Vector3d offset) {
Matcher itemMatcher = ITEM_MATCHER.matcher(name);
Material item = SpigotUtil.isUsing1_13API() ? Material.matchMaterial(itemMatcher.group(1), false)
: Material.matchMaterial(itemMatcher.group(1));
ItemStack itemStack = new ItemStack(item, 1);
NPC npc = registry.createNPCUsingItem(EntityType.DROPPED_ITEM, "", itemStack);
npc.data().setPersistent(NPC.Metadata.NAMEPLATE_VISIBLE, false);
if (itemMatcher.group(2) != null) {
if (itemMatcher.group(2).charAt(1) == '{') {
Bukkit.getUnsafe().modifyItemStack(itemStack, itemMatcher.group(2).substring(1));
npc.setItemProvider(() -> itemStack);
} else {
hologram.data().set(NPC.Metadata.NAMEPLATE_VISIBLE, ChatColor.stripColor(name).length() > 0);
hologram.data().remove(NPC.Metadata.HOLOGRAM_LINE_SUPPLIER);
npc.getOrAddTrait(ScoreboardTrait.class)
.setColor(Util.matchEnum(ChatColor.values(), itemMatcher.group(2).substring(1)));
}
}
return npc;
}
@Override
protected void render0(NPC npc, Vector3d offset) {
hologram.getEntity().teleport(npc.getStoredLocation().clone().add(offset.x, offset.y, offset.z),
TeleportCause.PLUGIN);
}
}
public abstract class SingleEntityHologramRenderer implements HologramRenderer {
protected NPC hologram;
protected String text;
protected abstract NPC createNPC(Entity base, String text, Vector3d offset);
@Override
public void destroy() {
if (hologram != null) {
hologram.destroy();
hologram = null;
}
}
private void spawn(NPC npc, String text, Vector3d offset) {
// hologram = renderer.spawn(npc, text, offset);
@Override
public String getPerPlayerText(NPC npc, Player viewer) {
return Placeholders.replace(text, viewer, npc);
}
@Override
public void render(NPC npc, Vector3d offset) {
if (hologram == null) {
spawnHologram(npc, offset);
}
render0(npc, offset);
}
protected abstract void render0(NPC npc, Vector3d offset);
protected void spawnHologram(NPC npc, Vector3d offset) {
hologram = createNPC(npc.getEntity(), text, offset);
if (!hologram.hasTrait(ClickRedirectTrait.class)) {
hologram.addTrait(new ClickRedirectTrait(npc));
}
hologram.data().set(NPC.Metadata.HOLOGRAM_FOR, npc.getUniqueId().toString());
hologram.data().set(NPC.Metadata.HOLOGRAM_RENDERER, this);
if (Setting.PACKET_HOLOGRAMS.asBoolean()) {
hologram.addTrait(PacketNPC.class);
}
@ -579,66 +534,17 @@ public class HologramTrait extends Trait {
hologram.data().set(NPC.Metadata.TRACKING_RANGE, npc.data().get(NPC.Metadata.TRACKING_RANGE));
}
hologram.spawn(npc.getEntity().getLocation().add(offset.x, offset.y, offset.z));
if (customHologramSupplier != null) {
hologram.data().set(NPC.Metadata.HOLOGRAM_LINE_SUPPLIER,
(Function<Player, String>) p -> customHologramSupplier.apply(text, p));
} else if (Placeholders.containsPlaceholders(text)) {
hologram.data().set(NPC.Metadata.HOLOGRAM_LINE_SUPPLIER, this);
}
}
public void spawnNPC(double height) {
String name = Placeholders.replace(text, null, npc);
hologram = createHologram(name, height);
if (customHologramSupplier != null) {
hologram.data().set(NPC.Metadata.HOLOGRAM_LINE_SUPPLIER,
(Function<Player, String>) p -> customHologramSupplier.apply(text, p));
} else if (Placeholders.containsPlaceholders(text)) {
hologram.data().set(NPC.Metadata.HOLOGRAM_LINE_SUPPLIER, this);
}
}
}
public static interface HologramRenderer {
void destroy();
void render(Entity base, Vector3d offset);
void updateText(Entity base, String text);
}
public class InteractionRenderer extends AbstractRenderer {
public void render(Entity base, String name, Vector3d offset) {
if (this.npc.getEntity().getVehicle() == null) {
HologramTrait.this.npc.getEntity().addPassenger(this.npc.getEntity());
}
}
@Override
protected NPC spawnNPC(Entity base, String name, Vector3d offset) {
return registry.createNPC(EntityType.INTERACTION, name);
}
}
public class ItemRenderer extends AbstractRenderer {
@Override
protected NPC spawnNPC(Entity base, String name, Vector3d offset) {
Matcher itemMatcher = ITEM_MATCHER.matcher(name);
Material item = SpigotUtil.isUsing1_13API() ? Material.matchMaterial(itemMatcher.group(1), false)
: Material.matchMaterial(itemMatcher.group(1));
ItemStack itemStack = new ItemStack(item, 1);
NPC itemNPC = registry.createNPCUsingItem(EntityType.DROPPED_ITEM, "", itemStack);
itemNPC.data().setPersistent(NPC.Metadata.NAMEPLATE_VISIBLE, false);
if (itemMatcher.group(2) != null) {
if (itemMatcher.group(2).charAt(1) == '{') {
Bukkit.getUnsafe().modifyItemStack(itemStack, itemMatcher.group(2).substring(1));
itemNPC.setItemProvider(() -> itemStack);
} else {
itemNPC.getOrAddTrait(ScoreboardTrait.class)
.setColor(Util.matchEnum(ChatColor.values(), itemMatcher.group(2).substring(1)));
}
public void updateText(NPC npc, String text) {
this.text = Placeholders.replace(text, null, npc);
if (hologram == null)
return;
hologram.setName(text);
if (!Placeholders.containsPlaceholders(text)) {
hologram.data().set(NPC.Metadata.NAMEPLATE_VISIBLE, Messaging.stripColor(text).length() > 0);
}
return itemNPC;
}
}
@ -653,21 +559,35 @@ public class HologramTrait extends Trait {
return Collections.emptyList();
}
private static List<String> LINE_ARGS = ImmutableList.of("set", "remove", "margintop", "marginbottom");
private static Set<String> LINE_ARGS = ImmutableSet.of("set", "remove", "margintop", "marginbottom");
}
public class TextDisplayRenderer extends AbstractRenderer {
public void render(Entity base, String name, Vector3d offset) {
if (this.npc.getEntity().getVehicle() == null) {
HologramTrait.this.npc.getEntity().addPassenger(this.npc.getEntity());
}
public class TextDisplayVehicleRenderer extends SingleEntityHologramRenderer {
@Override
protected NPC createNPC(Entity base, String name, Vector3d offset) {
NPC npc = registry.createNPC(EntityType.TEXT_DISPLAY, name);
return npc;
}
@Override
protected NPC spawnNPC(Entity base, String name, Vector3d offset) {
return registry.createNPC(EntityType.TEXT_DISPLAY, name);
public void render0(NPC base, Vector3d offset) {
TextDisplay disp = (TextDisplay) hologram.getEntity();
disp.setBillboard(Billboard.CENTER);
Transformation tf = disp.getTransformation();
tf.getTranslation().y = (float) offset.y + 0.1f;
disp.setTransformation(tf);
if (hologram.getEntity().getVehicle() == null) {
base.getEntity().addPassenger(hologram.getEntity());
}
}
}
private static Pattern ITEM_MATCHER = Pattern.compile("<item:(.*?)([:].*?)?>");
private static final Pattern ITEM_MATCHER = Pattern.compile("<item:(.*?)([:].*?)?>");
private static boolean SUPPORTS_DISPLAY = false;
static {
try {
SUPPORTS_DISPLAY = Class.forName("org.bukkit.entity.Display") != null;
} catch (Throwable e) {
}
}
}

View File

@ -12,7 +12,6 @@ import org.bukkit.conversations.ConversationFactory;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
import com.google.common.collect.Maps;
@ -20,11 +19,9 @@ import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.ai.speech.SpeechContext;
import net.citizensnpcs.api.event.NPCRightClickEvent;
import net.citizensnpcs.api.exception.NPCLoadException;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.api.util.Paginator;
import net.citizensnpcs.api.util.Placeholders;
@ -44,9 +41,9 @@ public class Text extends Trait implements Runnable, Listener {
private int delay = -1;
@Persist(value = "talkitem")
private String itemInHandPattern = "default";
private final Plugin plugin;
@Persist(value = "random-talker")
private boolean randomTalker = Setting.DEFAULT_RANDOM_TALKER.asBoolean();
@Persist
private double range = Setting.DEFAULT_TALK_CLOSE_RANGE.asDouble();
@Persist(value = "realistic-looking")
private boolean realisticLooker = Setting.DEFAULT_REALISTIC_LOOKING.asBoolean();
@ -54,11 +51,11 @@ public class Text extends Trait implements Runnable, Listener {
private boolean speechBubbles;
@Persist(value = "talk-close")
private boolean talkClose = Setting.DEFAULT_TALK_CLOSE.asBoolean();
private final List<String> text = new ArrayList<>();
@Persist
private volatile List<String> text = new ArrayList<>();
public Text() {
super("text");
plugin = CitizensAPI.getPlugin();
}
/**
@ -87,9 +84,9 @@ public class Text extends Trait implements Runnable, Listener {
* Builds a text editor in game for the supplied {@link Player}.
*/
public Editor getEditor(Player player) {
Conversation conversation = new ConversationFactory(plugin).withLocalEcho(false).withEscapeSequence("/npc text")
.withEscapeSequence("exit").withModality(false).withFirstPrompt(new TextBasePrompt(this))
.buildConversation(player);
Conversation conversation = new ConversationFactory(CitizensAPI.getPlugin()).withLocalEcho(false)
.withEscapeSequence("/npc text").withEscapeSequence("exit").withModality(false)
.withFirstPrompt(new TextBasePrompt(this)).buildConversation(player);
return new Editor() {
@Override
public void begin() {
@ -135,18 +132,6 @@ public class Text extends Trait implements Runnable, Listener {
return randomTalker;
}
@Override
public void load(DataKey key) throws NPCLoadException {
text.clear();
for (DataKey sub : key.getRelative("text").getIntegerSubKeys()) {
text.add(sub.getString(""));
}
if (text.isEmpty()) {
populateDefaultText();
}
range = key.getDouble("range");
}
@EventHandler
private void onRightClick(NPCRightClickEvent event) {
if (!event.getNPC().equals(npc) || text.size() == 0)
@ -158,8 +143,11 @@ public class Text extends Trait implements Runnable, Listener {
}
}
private void populateDefaultText() {
text.addAll(Setting.DEFAULT_TEXT.asList());
@Override
public void onSpawn() {
if (text.isEmpty()) {
text.addAll(Setting.DEFAULT_TEXT.asList());
}
}
/**
@ -179,15 +167,6 @@ public class Text extends Trait implements Runnable, Listener {
}
}
@Override
public void save(DataKey key) {
key.setDouble("range", range);
key.removeKey("text");
for (int i = 0; i < text.size(); i++) {
key.setString("text." + String.valueOf(i), text.get(i));
}
}
boolean sendPage(CommandSender player, int page) {
Paginator paginator = new Paginator().header("Current Texts").enablePageSwitcher("/npc text page $page");
for (int i = 0; i < text.size(); i++) {

View File

@ -232,7 +232,7 @@ public class RabbitController extends MobEntityController {
initPathfinder(); // make sure the evil goals include the default AI goals
}
super.setRabbitType(type);
NMSImpl.clearGoals(npc, goalSelector, targetSelector);
NMSImpl.clearGoals(goalSelector, targetSelector);
} else if (NMSImpl.getRabbitTypeField() != null) {
datawatcher.set(NMSImpl.getRabbitTypeField(), type);
}