Skip to content

Commit

Permalink
Rapid packet-based entity data changes (#2540)
Browse files Browse the repository at this point in the history
* WIP incomplete early work in progress

* Store `DataValue`'s instead of constructing async

* Move command registration

* Initial Mergufication, rename command

* Cleanup handling by directly passing `MapTag`

* Default to linked player

* Initial meta

* Imports

* Less spammy reflection

* Fix arg requirements

* Update entity data Ids

* `sendAsyncSafe` - work without interception

* Fix example

* More accurate delay

* Remove invalid players

* Fix required args min

* Fix display entity ids

* Better document how frames behave

* `@Tags`

* Initial attempt at a language doc

* WHY ENGLISH
  • Loading branch information
tal5 committed Oct 26, 2023
1 parent 366353b commit 0f42e74
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 54 deletions.
Expand Up @@ -3,7 +3,6 @@
import com.denizenscript.denizen.nms.util.jnbt.CompoundTag;
import com.denizenscript.denizen.objects.EntityTag;
import com.denizenscript.denizen.objects.LocationTag;
import com.denizenscript.denizencore.objects.ObjectTag;
import com.denizenscript.denizencore.objects.core.ElementTag;
import com.denizenscript.denizencore.objects.core.MapTag;
import org.bukkit.Bukkit;
Expand All @@ -23,7 +22,6 @@
import org.bukkit.util.Vector;

import java.util.List;
import java.util.Map;
import java.util.UUID;

public abstract class EntityHelper {
Expand Down Expand Up @@ -452,11 +450,11 @@ public void setStepHeight(Entity entity, float stepHeight) {
throw new UnsupportedOperationException();
}

public int mapInternalEntityDataName(Entity entity, String name) {
public List<Object> convertInternalEntityDataValues(Entity entity, MapTag internalData) {
throw new UnsupportedOperationException();
}

public void modifyInternalEntityData(Entity entity, Map<Integer, ObjectTag> internalData) {
public void modifyInternalEntityData(Entity entity, MapTag internalData) {
throw new UnsupportedOperationException();
}
}
Expand Up @@ -159,4 +159,8 @@ default void sendRelativePositionPacket(Player player, double x, double y, doubl
default void sendRelativeLookPacket(Player player, float yaw, float pitch) {
throw new UnsupportedOperationException();
}

default void sendEntityDataPacket(List<Player> players, Entity entity, List<Object> data) {
throw new UnsupportedOperationException();
}
}
Expand Up @@ -3013,24 +3013,11 @@ else if (object.getBukkitEntity() instanceof Hanging) {
// @input MapTag
// @description
// Modifies an entity's internal entity data as a map of data name to value.
// The values can be Denizen objects, and will be automatically converted to the relevant internal value.
// This is an advanced mechanism that directly controls an entity's data, with no verification/limitations on what's being set (other than basic type checking).
// You should almost always prefer using the appropriate mechanism/property instead of this, other than very specific special cases.
// See <@link url https://github.com/DenizenScript/Denizen/blob/dev/v1_20/src/main/java/com/denizenscript/denizen/nms/v1_20/helpers/EntityDataNameMapper.java#L50> for all the available names (and their respective ids),
// And <@link url https://wiki.vg/Entity_metadata> for a documentation of what each id is.
// (note that it documents the values that eventually get sent to the client, so the input this expects might be slightly different in some cases).
// See <@link language Internal Entity Data> for more information on the input.
// -->
tagProcessor.registerMechanism("internal_data", false, MapTag.class, (object, mechanism, input) -> {
Map<Integer, ObjectTag> internalData = new HashMap<>(input.size());
for (Map.Entry<StringHolder, ObjectTag> entry : input.entrySet()) {
int id = NMSHandler.entityHelper.mapInternalEntityDataName(object.getBukkitEntity(), entry.getKey().low);
if (id == -1) {
mechanism.echoError("Invalid internal data key: " + entry.getKey());
continue;
}
internalData.put(id, entry.getValue());
}
NMSHandler.entityHelper.modifyInternalEntityData(object.getBukkitEntity(), internalData);
NMSHandler.entityHelper.modifyInternalEntityData(object.getBukkitEntity(), input);
});
}
}
Expand Down
Expand Up @@ -92,6 +92,7 @@ public static void registerCommands() {
registerCommand(CastCommand.class);
registerCommand(EquipCommand.class);
registerCommand(FakeEquipCommand.class);
registerCommand(FakeInternalDataCommand.class);
registerCommand(FeedCommand.class);
registerCommand(FlyCommand.class);
registerCommand(FollowCommand.class);
Expand Down
@@ -0,0 +1,120 @@
package com.denizenscript.denizen.scripts.commands.entity;

import com.denizenscript.denizen.nms.NMSHandler;
import com.denizenscript.denizen.objects.EntityTag;
import com.denizenscript.denizen.objects.PlayerTag;
import com.denizenscript.denizen.utilities.Utilities;
import com.denizenscript.denizencore.DenizenCore;
import com.denizenscript.denizencore.exceptions.InvalidArgumentsRuntimeException;
import com.denizenscript.denizencore.objects.core.DurationTag;
import com.denizenscript.denizencore.objects.core.MapTag;
import com.denizenscript.denizencore.scripts.ScriptEntry;
import com.denizenscript.denizencore.scripts.commands.AbstractCommand;
import com.denizenscript.denizencore.scripts.commands.generator.*;
import com.denizenscript.denizencore.utilities.debugging.Debug;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class FakeInternalDataCommand extends AbstractCommand {

// <--[language]
// @name Internal Entity Data
// @group Minecraft Logic
// @description
// Each entity in Minecraft has a set of data values that get sent to the client, with each data value being a number id -> value pair.
// Denizen allows direct control over that data, as it can be useful for things like setting values that would usually be blocked.
// Because this is such a direct control that's meant to impose less restrictions, there's no limitations/verification on the values being set other than basic type checking.
// For all possible internal entity data values and their respective ids, see <@link url https://github.com/DenizenScript/Denizen/blob/dev/v1_20/src/main/java/com/denizenscript/denizen/nms/v1_20/helpers/EntityDataNameMapper.java#L50>.
// Alternatively, you can use the number id directly instead of the names listed there.
// For a list of all entity data ids and their values, see <@link url https://wiki.vg/Entity_metadata>
// (note that it documents the values that eventually get sent to the client, so the input this expects might be slightly different in some cases).
// You can input the equivalent denizen objects to have them be auto-converted to the internal types.
// -->

public FakeInternalDataCommand() {
setName("fakeinternaldata");
setSyntax("fakeinternaldata [entity:<entity>] [data:<map>|...] (for:<player>|...) (speed:<duration>)");
setRequiredArguments(2, 4);
autoCompile();
}

// <--[command]
// @Name FakeInternalData
// @Syntax fakeinternaldata [entity:<entity>] [data:<map>|...] (for:<player>|...) (speed:<duration>)
// @Required 2
// @Maximum 4
// @Short Sends fake entity data updates, optionally animating them with sub-tick precision.
// @Group entity
//
// @Description
// Sends fake internal entity data updates, optionally sending multiple over time.
// This supports sub-tick precision, allowing smooth/high FPS animations.
//
// The input to 'data:' is a list of <@link object MapTag>s, with each map being a frame to send; see <@link language Internal Entity Data> for more information.
//
// Optionally specify a list of players to fake the data for, defaults to the linked player.
//
// 'speed:' is the amount of time between each frame getting sent, supporting sub-tick delays.
// Note that this is the delay between each frame, regardless of their content (see examples).
//
// @Tags
// None
//
// @Usage
// Animates an item display entity's item for the linked player, and slowly scales it up.
// - fakeinternaldata entity:<[item_display]> data:[item=iron_ingot;scale=0.6,0.6,0.6]|[item=gold_ingot;scale=0.8,0.8,0.8]|[item=netherite_ingot;scale=1,1,1] speed:0.5s
//
// @Usage
// Changes an item display's item, then its scale a second later, then its item again another second later.
// - fakeinternaldata entity:<[item_display]> data:[item=stone]|[scale=2,2,2]|[item=waxed_weathered_cut_copper_slab] speed:1s
//
// @Usage
// Animates a rainbow glow on a display entity for all online players.
// - define color <color[red]>
// - repeat 256 from:0 as:hue:
// - define frames:->:[glow_color=<[color].with_hue[<[hue]>].argb_integer>]
// - fakeinternaldata entity:<[display]> data:<[frames]> for:<server.online_players> speed:0.01s
// -->

public static void autoExecute(ScriptEntry scriptEntry,
@ArgName("entity") @ArgPrefixed EntityTag inputEntity,
@ArgName("data") @ArgPrefixed @ArgSubType(MapTag.class) List<MapTag> data,
@ArgName("for") @ArgPrefixed @ArgDefaultNull @ArgSubType(PlayerTag.class) List<PlayerTag> forPlayers,
@ArgName("speed") @ArgPrefixed @ArgDefaultText("0s") DurationTag speed) {
List<Player> sendTo;
if (forPlayers != null) {
sendTo = new ArrayList<>(forPlayers.size());
for (PlayerTag player : forPlayers) {
sendTo.add(player.getPlayerEntity());
}
}
else if (Utilities.entryHasPlayer(scriptEntry)) {
sendTo = List.of(Utilities.getEntryPlayer(scriptEntry).getPlayerEntity());
}
else {
throw new InvalidArgumentsRuntimeException("Must specify players to fake the internal data for.");
}
Entity entity = inputEntity.getBukkitEntity();
List<List<Object>> frames = new ArrayList<>(data.size());
for (MapTag frame : data) {
frames.add(NMSHandler.entityHelper.convertInternalEntityDataValues(entity, frame));
}
long delayNanos = TimeUnit.MILLISECONDS.toNanos(speed.getMillis());
DenizenCore.runAsync(() -> {
long expectedTime = System.nanoTime();
for (List<Object> frame : frames) {
if (sendTo.isEmpty()) {
break;
}
NMSHandler.packetHelper.sendEntityDataPacket(sendTo, entity, frame);
LockSupport.parkNanos(delayNanos + (expectedTime - System.nanoTime()));
expectedTime += delayNanos;
}
});
}
}
Expand Up @@ -65,34 +65,35 @@ public static void registerDataName(Class<? extends Entity> entityClass, int id,
registerDataName(Interaction.class, 10, "responsive");

// Display
registerDataName(Display.class, 8, "interpolation_delay");
registerDataName(Display.class, 9, "interpolation_duration");
registerDataName(Display.class, 10, "translation");
registerDataName(Display.class, 11, "scale");
registerDataName(Display.class, 12, "left_rotation");
registerDataName(Display.class, 13, "right_rotation");
registerDataName(Display.class, 14, "billboard");
registerDataName(Display.class, 15, "brightness");
registerDataName(Display.class, 16, "view_range");
registerDataName(Display.class, 17, "shadow_radius");
registerDataName(Display.class, 18, "shadow_strength");
registerDataName(Display.class, 19, "width");
registerDataName(Display.class, 20, "height");
registerDataName(Display.class, 21, "glow_color");
registerDataName(Display.class, 8, "transform_interpolation_start");
registerDataName(Display.class, 9, "transform_interpolation_duration");
registerDataName(Display.class, 10, "movement_interpolation_duration");
registerDataName(Display.class, 11, "translation");
registerDataName(Display.class, 12, "scale");
registerDataName(Display.class, 13, "left_rotation");
registerDataName(Display.class, 14, "right_rotation");
registerDataName(Display.class, 15, "billboard");
registerDataName(Display.class, 16, "brightness");
registerDataName(Display.class, 17, "view_range");
registerDataName(Display.class, 18, "shadow_radius");
registerDataName(Display.class, 19, "shadow_strength");
registerDataName(Display.class, 20, "width");
registerDataName(Display.class, 21, "height");
registerDataName(Display.class, 22, "glow_color");

// Block display
registerDataName(Display.BlockDisplay.class, 22, "material");
registerDataName(Display.BlockDisplay.class, 23, "material");

// Item display
registerDataName(Display.ItemDisplay.class, 22, "item");
registerDataName(Display.ItemDisplay.class, 23, "model_transform");
registerDataName(Display.ItemDisplay.class, 23, "item");
registerDataName(Display.ItemDisplay.class, 24, "model_transform");

// Text display
registerDataName(Display.TextDisplay.class, 22, "text");
registerDataName(Display.TextDisplay.class, 23, "line_width");
registerDataName(Display.TextDisplay.class, 24, "background_color");
registerDataName(Display.TextDisplay.class, 25, "text_opacity");
registerDataName(Display.TextDisplay.class, 26, "text_display_flags");
registerDataName(Display.TextDisplay.class, 23, "text");
registerDataName(Display.TextDisplay.class, 24, "line_width");
registerDataName(Display.TextDisplay.class, 25, "background_color");
registerDataName(Display.TextDisplay.class, 26, "text_opacity");
registerDataName(Display.TextDisplay.class, 27, "text_display_flags");

// Thrown item projectile
registerDataName(ThrowableProjectile.class, 8, "item");
Expand Down
Expand Up @@ -11,9 +11,11 @@
import com.denizenscript.denizen.utilities.Utilities;
import com.denizenscript.denizen.utilities.packets.NetworkInterceptHelper;
import com.denizenscript.denizencore.objects.ObjectTag;
import com.denizenscript.denizencore.objects.core.MapTag;
import com.denizenscript.denizencore.scripts.commands.core.ReflectionSetCommand;
import com.denizenscript.denizencore.utilities.ReflectionHelper;
import com.denizenscript.denizencore.utilities.debugging.Debug;
import com.denizenscript.denizencore.utilities.text.StringHolder;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import net.minecraft.commands.arguments.EntityAnchorArgument;
Expand Down Expand Up @@ -80,6 +82,7 @@
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.BiConsumer;

public class EntityHelperImpl extends EntityHelper {

Expand Down Expand Up @@ -799,25 +802,47 @@ public void setStepHeight(Entity entity, float stepHeight) {
((CraftEntity) entity).getHandle().setMaxUpStep(stepHeight);
}

@Override
public int mapInternalEntityDataName(Entity entity, String name) {
return EntityDataNameMapper.getIdForName(((CraftEntity) entity).getHandle().getClass(), name);
public static final Field SynchedEntityData_itemsById = ReflectionHelper.getFields(SynchedEntityData.class).get(ReflectionMappingsInfo.SynchedEntityData_itemsById);

public static Int2ObjectMap<SynchedEntityData.DataItem<Object>> getDataItems(Entity entity) {
try {
return (Int2ObjectMap<SynchedEntityData.DataItem<Object>>) SynchedEntityData_itemsById.get(((CraftEntity) entity).getHandle().getEntityData());
}
catch (IllegalAccessException e) {
throw new RuntimeException(e); // Stop the code here to avoid NPEs down the road
}
}

@Override
public void modifyInternalEntityData(Entity entity, Map<Integer, ObjectTag> internalData) {
SynchedEntityData nmsEntityData = ((CraftEntity) entity).getHandle().getEntityData();
Int2ObjectMap<SynchedEntityData.DataItem<Object>> dataItemsById = ReflectionHelper.getFieldValue(SynchedEntityData.class, ReflectionMappingsInfo.SynchedEntityData_itemsById, nmsEntityData);
for (Map.Entry<Integer, ObjectTag> entry : internalData.entrySet()) {
SynchedEntityData.DataItem<Object> dataItem = dataItemsById.get(entry.getKey().intValue());
public static void convertToInternalData(Entity entity, MapTag internalData, BiConsumer<SynchedEntityData.DataItem<Object>, Object> processConverted) {
Int2ObjectMap<SynchedEntityData.DataItem<Object>> dataItemsById = getDataItems(entity);
for (Map.Entry<StringHolder, ObjectTag> entry : internalData.entrySet()) {
int id = EntityDataNameMapper.getIdForName(((CraftEntity) entity).getHandle().getClass(), entry.getKey().low);
if (id == -1) {
Debug.echoError("Invalid internal data key: " + entry.getKey());
return;
}
SynchedEntityData.DataItem<Object> dataItem = dataItemsById.get(id);
if (dataItem == null) {
Debug.echoError("Invalid internal data id '" + entry.getKey() + "': couldn't be matched to any internal data for entity of type '" + entity.getType() + "'.");
continue;
Debug.echoError("Invalid internal data id '" + id + "': couldn't be matched to any internal data for entity of type '" + entity.getType() + "'.");
return;
}
Object converted = ReflectionSetCommand.convertObjectTypeFor(dataItem.getValue().getClass(), entry.getValue());
if (converted != null) {
nmsEntityData.set(dataItem.getAccessor(), converted);
processConverted.accept(dataItem, converted);
}
}
}

@Override
public List<Object> convertInternalEntityDataValues(Entity entity, MapTag internalData) {
List<Object> dataValues = new ArrayList<>(internalData.size());
convertToInternalData(entity, internalData, (dataItem, converted) -> dataValues.add(PacketHelperImpl.createEntityData(dataItem.getAccessor(), converted)));
return dataValues;
}

@Override
public void modifyInternalEntityData(Entity entity, MapTag internalData) {
SynchedEntityData nmsEntityData = ((CraftEntity) entity).getHandle().getEntityData();
convertToInternalData(entity, internalData, (dataItem, converted) -> nmsEntityData.set(dataItem.getAccessor(), converted));
}
}

0 comments on commit 0f42e74

Please sign in to comment.