Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
plugins {
`java-library`
id("xyz.jpenilla.run-paper") version "3.0.2"
id("io.papermc.paperweight.userdev") version "2.0.0-beta.21" //Access to Minecraft NMS + Paper API packages
}

group = "xyz.holocons.mc"
Expand All @@ -16,8 +18,8 @@ repositories {
}

dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT")
compileOnly("com.comphenix.protocol:ProtocolLib:5.3.0")
paperweight.paperDevBundle("1.21.11-R0.1-SNAPSHOT")
compileOnly("net.dmulloy2:ProtocolLib:5.4.0")
}

tasks {
Expand Down Expand Up @@ -50,4 +52,10 @@ tasks {
expand(pluginProperties)
}
}
runServer {
// Configure the Minecraft version for our task.
// This is the only required configuration besides applying the plugin.
// Your plugin's jar (or shadowJar if present) will be used automatically.
minecraftVersion("1.21.11")
}
}
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
6 changes: 3 additions & 3 deletions gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions gradlew.bat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven("https://repo.papermc.io/repository/maven-public/")
}
}

Expand Down
122 changes: 76 additions & 46 deletions src/main/java/xyz/holocons/mc/waypoints/Hologram.java
Original file line number Diff line number Diff line change
@@ -1,81 +1,111 @@
package xyz.holocons.mc.waypoints;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;

import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedDataValue;
import com.comphenix.protocol.wrappers.WrappedDataWatcher.Registry;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;

import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.objects.ObjectList;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.ComponentSerialization;
import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket;
import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.decoration.ArmorStand;
import net.minecraft.world.phys.Vec3;

public record Hologram(long chunkKey, UUID uniqueId) {

// These fields are private in NMS
private static final EntityDataAccessor<Byte> DATA_SHARED_FLAGS_ID = getAccessor(Entity.class, "DATA_SHARED_FLAGS_ID");
private static final EntityDataAccessor<Optional<Component>> DATA_CUSTOM_NAME = getAccessor(Entity.class, "DATA_CUSTOM_NAME");
private static final EntityDataAccessor<Boolean> DATA_CUSTOM_NAME_VISIBLE = getAccessor(Entity.class, "DATA_CUSTOM_NAME_VISIBLE");
private static final EntityDataAccessor<Byte> DATA_CLIENT_FLAGS = getAccessor(ArmorStand.class, "DATA_CLIENT_FLAGS");

@SuppressWarnings("unchecked")
private static <T> EntityDataAccessor<T> getAccessor(Class<?> clazz, String name) {
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return (EntityDataAccessor<T>) field.get(null);
} catch (Exception e) {
throw new RuntimeException("Failed to access " + name, e);
}
}

public Hologram(Waypoint waypoint, Player player) {
this(waypoint.getChunkKey(), player.getUniqueId());
}

private static final Vector HOLOGRAM_POSITION_OFFSET = new Vector(0.5, 1.6, 0.5);

// https://nms.screamingsandals.org/1.19.3/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.html
// https://wiki.vg/Protocol#Spawn_Entity
// https://nms.screamingsandals.org/1.21.11/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.html
// https://minecraft.wiki/w/Java_Edition_protocol/Packets#Spawn_Entity
public static PacketContainer getSpawnPacket(int entityId, UUID uniqueId, Waypoint waypoint) {
var location = waypoint.getLocation().add(HOLOGRAM_POSITION_OFFSET);
var packet = new PacketContainer(PacketType.Play.Server.SPAWN_ENTITY);
packet.getIntegers()
.write(0, entityId) // id
.write(1, 0) // xa
.write(2, 0) // ya
.write(3, 0) // za
.write(4, 0); // data
packet.getUUIDs()
.write(0, uniqueId); // uuid
packet.getEntityTypeModifier()
.write(0, EntityType.ARMOR_STAND); // type
packet.getDoubles()
.write(0, location.getX()) // x
.write(1, location.getY()) // y
.write(2, location.getZ()); // z
packet.getBytes()
.write(0, (byte) 0) // xRot
.write(1, (byte) 0) // yRot
.write(2, (byte) 0); // yHeadRot

// NMS packet
var vanillaPacket = new ClientboundAddEntityPacket(
entityId,
uniqueId,
location.getX(), location.getY(), location.getZ(),
0F, 0F,
EntityType.ARMOR_STAND,
0,
Vec3.ZERO,
0D
);

var packet = new PacketContainer(PacketType.Play.Server.SPAWN_ENTITY, vanillaPacket);
return packet;
}

// https://nms.screamingsandals.org/1.19.3/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.html
// https://wiki.vg/Protocol#Set_Entity_Metadata
// https://wiki.vg/Entity_metadata#Entity_Metadata_Format
// https://nms.screamingsandals.org/1.21.11/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.html
// https://minecraft.wiki/w/Java_Edition_protocol/Packets#Set_Entity_Metadata
// https://minecraft.wiki/w/Java_Edition_protocol/Entity_metadata#Entity_Metadata_Format
public static PacketContainer getMetadataPacket(int entityId, Waypoint waypoint) {
var name = WrappedChatComponent.fromJson(GsonComponentSerializer.gson().serialize(waypoint.getDisplayName()));
var metadata = ObjectList.of(
new WrappedDataValue(0, Registry.get(Byte.class), (byte) 0x20),
new WrappedDataValue(2, Registry.getChatComponentSerializer(true), Optional.of(name.getHandle())),
new WrappedDataValue(3, Registry.get(Boolean.class), true),
new WrappedDataValue(15, Registry.get(Byte.class), (byte) (0x08 | 0x10)));
var packet = new PacketContainer(PacketType.Play.Server.ENTITY_METADATA);
packet.getIntegers()
.write(0, entityId); // id
packet.getDataValueCollectionModifier()
.write(0, metadata); // packedItems

// Convert Adventure Component to Minecraft Component
var json = GsonComponentSerializer.gson().serialize(waypoint.getDisplayName());
var result = ComponentSerialization.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(json));
Optional<Component> optComponent = result.result();

// NMS metadata
List<SynchedEntityData.DataValue<?>> metadata = List.of(
SynchedEntityData.DataValue.<Byte>create(DATA_SHARED_FLAGS_ID, (byte) 0x20),
SynchedEntityData.DataValue.<Optional<Component>>create(DATA_CUSTOM_NAME, optComponent),
SynchedEntityData.DataValue.<Boolean>create(DATA_CUSTOM_NAME_VISIBLE, true),
SynchedEntityData.DataValue.<Byte>create(DATA_CLIENT_FLAGS, (byte) (0x08 | 0x10))
);

// NMS packet
var vanillaPacket = new ClientboundSetEntityDataPacket(entityId, metadata);

var packet = new PacketContainer(PacketType.Play.Server.ENTITY_METADATA, vanillaPacket);
return packet;
}

// https://nms.screamingsandals.org/1.19.3/net/minecraft/network/protocol/game/ClientboundRemoveEntitiesPacket.html
// https://wiki.vg/Protocol#Remove_Entities
// https://nms.screamingsandals.org/1.21.11/net/minecraft/network/protocol/game/ClientboundRemoveEntitiesPacket.html
// https://minecraft.wiki/w/Java_Edition_protocol/Packets#Remove_Entities
public static PacketContainer getDestroyPacket(int... entityId) {
var entityIds = IntList.of(entityId);
var packet = new PacketContainer(PacketType.Play.Server.ENTITY_DESTROY);
packet.getIntLists()
.write(0, entityIds); // entityIds

// NMS packet
var vanillaPacket = new ClientboundRemoveEntitiesPacket(IntList.of(entityId));

var packet = new PacketContainer(PacketType.Play.Server.ENTITY_DESTROY, vanillaPacket);
return packet;
}
}
Loading