Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rapid packet-based entity data changes #2540

Merged
merged 25 commits into from Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -3004,16 +3004,7 @@ else if (object.getBukkitEntity() instanceof Hanging) {
// (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).
// -->
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,106 @@
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 {

public FakeInternalDataCommand() {
mcmonkey4eva marked this conversation as resolved.
Show resolved Hide resolved
setName("fakeinternaldata");
setSyntax("fakeinternaldata [entity:<entity>] [data:<map>|...] (for:<player>|...) (speed:<duration>)");
setRequiredArguments(2, 4);
autoCompile();
}

// <--[command]
mcmonkey4eva marked this conversation as resolved.
Show resolved Hide resolved
// @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, with each map being formatted like <@link mechanism EntityTag.internal_data>'s input.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A command should probably be considered "more primary" than a mech (ie put the details in command and have the mech link to it) - or add a separate language defining the data that both link to

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed an initial attempt at a language doc for this, let me know if it looks good

//
// 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
mcmonkey4eva marked this conversation as resolved.
Show resolved Hide resolved
//
// @Usage
// Changes an item display's item, then it's scale a second later, then it's item again another second later.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

// - 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) {
mcmonkey4eva marked this conversation as resolved.
Show resolved Hide resolved
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));
}
}
Expand Up @@ -389,18 +389,36 @@ public void sendRelativePositionPacket(Player player, double x, double y, double
}
}
ClientboundPlayerPositionPacket packet = new ClientboundPlayerPositionPacket(x, y, z, yaw, pitch, relativeMovements, 0);
DenizenNetworkManagerImpl.getNetworkManager(player).oldManager.channel.writeAndFlush(packet);
sendAsyncSafe(player, packet);
}

@Override
public void sendRelativeLookPacket(Player player, float yaw, float pitch) {
sendRelativePositionPacket(player, 0, 0, 0, yaw, pitch, null);
}

@Override
public void sendEntityDataPacket(List<Player> players, Entity entity, List<Object> data) {
ClientboundSetEntityDataPacket setEntityDataPacket = new ClientboundSetEntityDataPacket(entity.getEntityId(), (List<SynchedEntityData.DataValue<?>>) (Object) data);
Iterator<Player> playerIterator = players.iterator();
while (playerIterator.hasNext()) {
Player player = playerIterator.next();
if (!DenizenNetworkManagerImpl.getConnection(player).isConnected()) {
playerIterator.remove();
continue;
}
sendAsyncSafe(player, setEntityDataPacket);
}
}

public static void send(Player player, Packet<?> packet) {
((CraftPlayer) player).getHandle().connection.send(packet);
}

public static void sendAsyncSafe(Player player, Packet<?> packet) {
DenizenNetworkManagerImpl.getConnection(player).channel.writeAndFlush(packet);
}

public static <T> SynchedEntityData.DataValue<T> createEntityData(EntityDataAccessor<T> accessor, T value) {
return new SynchedEntityData.DataValue<>(accessor.getId(), accessor.getSerializer(), value);
}
Expand Down
Expand Up @@ -111,6 +111,10 @@ public static Connection getConnection(ServerPlayer player) {
}
}

public static Connection getConnection(Player player) {
return getConnection(((CraftPlayer) player).getHandle());
}

public static DenizenNetworkManagerImpl getNetworkManager(ServerPlayer player) {
return (DenizenNetworkManagerImpl) getConnection(player);
}
Expand Down