Skip to content

Commit

Permalink
Invisible command for: argument (#2356)
Browse files Browse the repository at this point in the history
* Invisible command update + `for:` argument

* Meta fix

* Invisibility potion effect check in `isInvisible`

* Fixes from review

* `reset` `for:` specific players support
  • Loading branch information
tal5 committed Aug 22, 2022
1 parent 61fb8b9 commit 1d377d0
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 84 deletions.
Expand Up @@ -31,6 +31,10 @@ public void setInvisible(Entity entity, boolean invisible) {
// Do nothing on older versions
}

public boolean isInvisible(Entity entity) {
throw new UnsupportedOperationException();
}

public abstract double getAbsorption(LivingEntity entity);

public abstract void setAbsorption(LivingEntity entity, double value);
Expand Down
@@ -1,60 +1,80 @@
package com.denizenscript.denizen.scripts.commands.entity;

import com.denizenscript.denizen.nms.NMSHandler;
import com.denizenscript.denizen.utilities.Utilities;
import com.denizenscript.denizen.utilities.debugging.Debug;
import com.denizenscript.denizen.npc.traits.InvisibleTrait;
import com.denizenscript.denizen.objects.EntityTag;
import com.denizenscript.denizen.objects.NPCTag;
import com.denizenscript.denizen.objects.PlayerTag;
import com.denizenscript.denizen.utilities.Utilities;
import com.denizenscript.denizen.utilities.debugging.Debug;
import com.denizenscript.denizen.utilities.packets.NetworkInterceptHelper;
import com.denizenscript.denizencore.exceptions.InvalidArgumentsException;
import com.denizenscript.denizencore.objects.Argument;
import com.denizenscript.denizencore.objects.core.ElementTag;
import com.denizenscript.denizencore.scripts.ScriptEntry;
import com.denizenscript.denizencore.scripts.commands.AbstractCommand;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.ItemFrame;
import org.bukkit.potion.PotionEffect;
import org.bukkit.entity.*;
import org.bukkit.potion.PotionEffectType;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;

public class InvisibleCommand extends AbstractCommand {

public InvisibleCommand() {
setName("invisible");
setSyntax("invisible [<entity>] (state:true/false/toggle)");
setRequiredArguments(1, 2);
setSyntax("invisible (<entity>) (state:true/false/toggle/reset) (for:<player>|...)");
setPrefixesHandled("for");
setRequiredArguments(0, 3);
isProcedural = false;
}

// <--[command]
// @Name Invisible
// @Syntax invisible [<entity>] (state:true/false/toggle)
// @Required 1
// @Maximum 2
// @Short Makes an NPC or entity go invisible.
// @Syntax invisible (<entity>) (state:true/false/toggle/reset) (for:<player>|...)
// @Required 0
// @Maximum 3
// @Short Sets whether an NPC or entity are invisible.
// @Group entity
//
// @Description
// For living entities (other than armor_stand), applies a maximum duration invisibility potion.
// For armor_stands or item_frame, toggles them invisible.
// Applies the 'invisible' trait to NPCs.
// Makes the specified entity invisible, defaults to the linked player/NPC if one wasn't specified.
// If an NPC was specified, the 'invisible' trait is applied.
//
// NPCs can't be made invisible if not added to the playerlist.
// (The invisible trait adds the NPC to the playerlist when set)
// See <@link language invisible trait>)
// Optionally specify 'for:' with a list of players to fake the entity's visibility state for these players.
// When using the 'toggle' state with the 'for:' argument, the visibility state will be toggled for each player separately.
// Note that using the 'for:' argument won't apply the 'invisible' trait to NPCs.
// If unspecified, will be set globally.
//
// To reset an entity's fake visibility (for all players) use the 'reset' state.
//
// NPCs can't be made invisible if not added to the playerlist (the invisible trait adds the NPC to the playerlist when set).
// See <@link language invisible trait>
//
// @Tags
// None
//
// @Usage
// Use to makes the player invisible.
// - invisible <player> state:true
// Use to make the linked player (or NPC, if there isn't one) invisible.
// - invisible
//
// @Usage
// Use to make the attached NPC visible if previously invisible, and invisible if not
// Use to make the linked NPC visible if previously invisible, and invisible if not.
// - invisible <npc> state:toggle
//
// @Usage
// Use to make an entity visible for specific players, without changing the way other players see it.
// - invisible <[entity]> state:false for:<[player1]>|<[player2]>
// -->

enum Action {TRUE, FALSE, TOGGLE}
@Override
public void addCustomTabCompletions(TabCompletionsBuilder tab) {
tab.addWithPrefix("state:", Action.values());
}

enum Action {TRUE, FALSE, TOGGLE, RESET}

@Override
public void parseArgs(ScriptEntry scriptEntry) throws InvalidArgumentsException {
Expand All @@ -81,79 +101,143 @@ else if (!scriptEntry.hasObject("target")
arg.reportUnhandled();
}
}
if (!scriptEntry.hasObject("state")) {
scriptEntry.addObject("state", new ElementTag("TRUE"));
scriptEntry.defaultObject("state", new ElementTag("true"));
scriptEntry.defaultObject("target", Utilities.entryDefaultEntity(scriptEntry, true));
}

public static HashMap<UUID, HashMap<UUID, Boolean>> invisibleEntities = new HashMap<>();

public static void setInvisibleForPlayer(EntityTag target, PlayerTag player, boolean invisible) {
if (target == null || target.getUUID() == null || player == null) {
return;
}
if (!scriptEntry.hasObject("target") || !((EntityTag) scriptEntry.getObjectTag("target")).isValid()) {
throw new InvalidArgumentsException("Must specify a valid target!");
NetworkInterceptHelper.enable();
boolean wasModified = !invisibleEntities.containsKey(target.getUUID());
HashMap<UUID, Boolean> playerMap = invisibleEntities.computeIfAbsent(target.getUUID(), k -> new HashMap<>());
wasModified = !playerMap.containsKey(player.getUUID()) || playerMap.get(player.getUUID()) != invisible || wasModified;
playerMap.put(player.getUUID(), invisible);
if (wasModified) {
NMSHandler.packetHelper.sendEntityMetadataFlagsUpdate(player.getPlayerEntity(), target.getBukkitEntity());
}
}

public void setInvisible(EntityTag entity, boolean visible) {
if (entity.getBukkitEntity() instanceof ArmorStand) {
((ArmorStand) entity.getBukkitEntity()).setVisible(visible);
public void setInvisible(EntityTag entity, boolean invisible) {
if (entity.isCitizensNPC()) {
entity.getDenizenNPC().getCitizen().getOrAddTrait(InvisibleTrait.class).setInvisible(invisible);
}
else if (entity.getBukkitEntity() instanceof ArmorStand) {
((ArmorStand) entity.getBukkitEntity()).setVisible(!invisible);
}
else if (entity.getBukkitEntity() instanceof ItemFrame) {
((ItemFrame) entity.getBukkitEntity()).setVisible(visible);
((ItemFrame) entity.getBukkitEntity()).setVisible(!invisible);
}
else if (entity.isLivingEntity() && !entity.isFake) {
if (visible) {
entity.getLivingEntity().setInvisible(invisible);
if (!invisible) {
// Remove the invisibility potion effect for compact with old uses (the command used to add it)
entity.getLivingEntity().removePotionEffect(PotionEffectType.INVISIBILITY);
}
else {
new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 1).apply(entity.getLivingEntity());
}
}
else {
NMSHandler.entityHelper.setInvisible(entity.getBukkitEntity(), !visible);
NMSHandler.entityHelper.setInvisible(entity.getBukkitEntity(), invisible);
}
}

@Override
public void execute(ScriptEntry scriptEntry) {
ElementTag state = scriptEntry.getElement("state");
EntityTag target = scriptEntry.getObjectTag("target");
if (scriptEntry.dbCallShouldDebug()) {
Debug.report(scriptEntry, getName(), state, target);
if (target == null) {
Debug.echoError(scriptEntry, "Must specify a valid target.");
return;
}
if (target.isCitizensNPC()) {
NPC npc = target.getDenizenNPC().getCitizen();
if (!npc.hasTrait(InvisibleTrait.class)) {
npc.addTrait(InvisibleTrait.class);
}
InvisibleTrait trait = npc.getOrAddTrait(InvisibleTrait.class);
switch (Action.valueOf(state.asString().toUpperCase())) {
case FALSE:
trait.setInvisible(false);
break;
case TRUE:
trait.setInvisible(true);
break;
case TOGGLE:
trait.toggle();
break;
}
ElementTag state = scriptEntry.getElement("state");
List<PlayerTag> forPlayers = scriptEntry.argForPrefixList("for", PlayerTag.class, true);
if (scriptEntry.dbCallShouldDebug()) {
Debug.report(scriptEntry, getName(), target, state, db("for", forPlayers));
}
else {
switch (Action.valueOf(state.asString().toUpperCase())) {
case FALSE:
setInvisible(target, true);
break;
case TRUE:
setInvisible(target, false);
break;
case TOGGLE:
if (target.getBukkitEntity() instanceof ArmorStand) {
setInvisible(target, !((ArmorStand) target.getBukkitEntity()).isVisible());
switch (state.asEnum(Action.class)) {
case TRUE:
case FALSE:
if (forPlayers == null) {
setInvisible(target, state.asBoolean());
}
else {
boolean invisible = state.asBoolean();
for (PlayerTag player : forPlayers) {
setInvisibleForPlayer(target, player, invisible);
}
else if (target.getBukkitEntity() instanceof ItemFrame) {
setInvisible(target, !((ItemFrame) target.getBukkitEntity()).isVisible());
}
break;
case TOGGLE:
if (forPlayers == null) {
setInvisible(target, !isInvisible(target.getBukkitEntity(), null, false));
}
else {
for (PlayerTag player : forPlayers) {
setInvisibleForPlayer(target, player, !isInvisible(target.getBukkitEntity(), player.getUUID(), false));
}
else {
setInvisible(target, target.getLivingEntity().hasPotionEffect(PotionEffectType.INVISIBILITY));
}
break;
case RESET:
HashMap<UUID, Boolean> playerMap = invisibleEntities.get(target.getUUID());
if (playerMap == null) {
return;
}
HashSet<UUID> playersToUpdate = new HashSet<>();
if (forPlayers == null) {
playersToUpdate.addAll(playerMap.keySet());
invisibleEntities.remove(target.getUUID());
}
else {
for (PlayerTag player : forPlayers) {
playerMap.remove(player.getUUID());
playersToUpdate.add(player.getUUID());
}
break;
if (playerMap.isEmpty()) {
invisibleEntities.remove(target.getUUID());
}
}
if (!playersToUpdate.isEmpty()) {
for (Player player : NMSHandler.entityHelper.getPlayersThatSee(target.getBukkitEntity())) {
if (playersToUpdate.contains(player.getUniqueId())) {
NMSHandler.packetHelper.sendEntityMetadataFlagsUpdate(player, target.getBukkitEntity());
}
}
}
break;
}
}

public static Boolean isInvisible(Entity entity, UUID player, boolean fakeOnly) {
if (entity == null) {
return null;
}
if (player != null) {
HashMap<UUID, Boolean> playerMap = invisibleEntities.get(entity.getUniqueId());
if (playerMap != null && playerMap.containsKey(player)) {
return playerMap.get(player);
}
}
if (fakeOnly) {
return null;
}
if (EntityTag.isCitizensNPC(entity)) {
InvisibleTrait invisibleTrait = NPCTag.fromEntity(entity).getCitizen().getTraitNullable(InvisibleTrait.class);
return invisibleTrait != null && invisibleTrait.isInvisible();
}
else if (entity instanceof ArmorStand) {
return !((ArmorStand) entity).isVisible();
}
else if (entity instanceof ItemFrame) {
return !((ItemFrame) entity).isVisible();
}
else if (entity instanceof LivingEntity) {
// Check for the invisibility potion effect for compact with old uses (the command used to add it)
return ((LivingEntity) entity).isInvisible() || ((LivingEntity) entity).hasPotionEffect(PotionEffectType.INVISIBILITY);
}
else {
return NMSHandler.entityHelper.isInvisible(entity);
}
}

}
Expand Up @@ -46,6 +46,11 @@ public void setInvisible(Entity entity, boolean invisible) {
((CraftEntity) entity).getHandle().setInvisible(invisible);
}

@Override
public boolean isInvisible(Entity entity) {
return ((CraftEntity) entity).getHandle().isInvisible();
}

@Override
public double getAbsorption(LivingEntity entity) {
return entity.getAbsorptionAmount();
Expand Down
Expand Up @@ -83,6 +83,11 @@ public void setInvisible(Entity entity, boolean invisible) {
((CraftEntity) entity).getHandle().setInvisible(invisible);
}

@Override
public boolean isInvisible(Entity entity) {
return ((CraftEntity) entity).getHandle().isInvisible();
}

@Override
public double getAbsorption(LivingEntity entity) {
return entity.getAbsorptionAmount();
Expand Down
Expand Up @@ -90,6 +90,11 @@ public void setInvisible(Entity entity, boolean invisible) {
((CraftEntity) entity).getHandle().setInvisible(invisible);
}

@Override
public boolean isInvisible(Entity entity) {
return ((CraftEntity) entity).getHandle().isInvisible();
}

@Override
public double getAbsorption(LivingEntity entity) {
return entity.getAbsorptionAmount();
Expand Down
Expand Up @@ -14,6 +14,7 @@
import com.denizenscript.denizen.objects.LocationTag;
import com.denizenscript.denizen.objects.PlayerTag;
import com.denizenscript.denizen.scripts.commands.entity.FakeEquipCommand;
import com.denizenscript.denizen.scripts.commands.entity.InvisibleCommand;
import com.denizenscript.denizen.scripts.commands.entity.RenameCommand;
import com.denizenscript.denizen.scripts.commands.entity.SneakCommand;
import com.denizenscript.denizen.scripts.commands.player.DisguiseCommand;
Expand Down Expand Up @@ -708,7 +709,7 @@ else if (packet instanceof ClientboundAddMobPacket) {
}

public ClientboundSetEntityDataPacket getModifiedMetadataFor(ClientboundSetEntityDataPacket metadataPacket) {
if (!RenameCommand.hasAnyDynamicRenames() && SneakCommand.forceSetSneak.isEmpty()) {
if (!RenameCommand.hasAnyDynamicRenames() && SneakCommand.forceSetSneak.isEmpty() && InvisibleCommand.invisibleEntities.isEmpty()) {
return null;
}
try {
Expand All @@ -719,6 +720,7 @@ public ClientboundSetEntityDataPacket getModifiedMetadataFor(ClientboundSetEntit
}
String nameToApply = RenameCommand.getCustomNameFor(ent.getUUID(), player.getBukkitEntity(), false);
Boolean forceSneak = SneakCommand.shouldSneak(ent.getUUID(), player.getUUID());
Boolean isInvisible = InvisibleCommand.isInvisible(ent.getBukkitEntity(), player.getUUID(), true);
if (nameToApply == null && forceSneak == null) {
return null;
}
Expand All @@ -728,13 +730,23 @@ public ClientboundSetEntityDataPacket getModifiedMetadataFor(ClientboundSetEntit
SynchedEntityData.DataItem<?> item = data.get(i);
EntityDataAccessor<?> watcherObject = item.getAccessor();
int watcherId = watcherObject.getId();
if (watcherId == 0 && forceSneak != null) { // 0: Entity flags
if (watcherId == 0 && (forceSneak != null || isInvisible != null)) { // 0: Entity flags
byte val = (Byte) item.getValue();
if (forceSneak) {
val |= 0x02; // 8: Crouching
if (forceSneak != null) {
if (forceSneak) {
val |= 0x02; // 8: Crouching
}
else {
val &= ~0x02;
}
}
else {
val &= ~0x02;
if (isInvisible != null) {
if (isInvisible) {
val |= 0x20;
}
else {
val &= ~0x20;
}
}
data.set(i, new SynchedEntityData.DataItem(watcherObject, val));
any = true;
Expand Down

0 comments on commit 1d377d0

Please sign in to comment.