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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ Icon
Network Trash Folder
Temporary Items
.apdisk
.server/

### macOS Patch ###
# iCloud generated files
Expand Down Expand Up @@ -454,3 +455,4 @@ run/
.idea/
next-env.d.ts
*.iml

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ A hosted version of the javadoc can be found [here](https://repo.minceraft.dev/j

| Server Version | Supported |
|----------------|---------------------|
| Paper 26.2.x | ✔️ |
| Paper 26.1.x | ✔️ |
| Paper 1.21.x | ✔️ |
| Paper 1.20.x | ✔️ |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ private static int srgb2linearByte(float srgb) {
* @param newColor the new color of the pixel
*/
public void pixel(int x, int y, int newColor) {
if (x < 0 || x >= width() || y < 0 || y >= height()) {
return;
}

int newAlpha = ((newColor >> 24) & 0xFF);
if (newAlpha == 255) {
// completely opaque pixel, overwrite
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;

import java.util.concurrent.atomic.AtomicInteger;

public interface IPlatform<T> {

AtomicInteger ENTITY_ID_ALLOCATOR = new AtomicInteger(Integer.MIN_VALUE);

default int nextEntityId() {
return ENTITY_ID_ALLOCATOR.getAndIncrement();
}

String getDisplayedName();

void sendPacket(Player player, PacketContainer<T> packet);
Expand Down
2 changes: 2 additions & 0 deletions platform-paper-1.21.6/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ dependencies {
}

java {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))

sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
Expand Down
30 changes: 30 additions & 0 deletions platform-paper-26.2/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import io.papermc.paperweight.userdev.ReobfArtifactConfiguration

plugins {
id("io.papermc.paperweight.userdev")
}

dependencies {
api(project(":platform-common"))
paperweight.paperDevBundle("26.2.build.+")
}

java {
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}

// hack to allow depending on this platform in java 17 projects
configurations.runtimeElements.configure {
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
}

paperweight {
reobfArtifactConfiguration = ReobfArtifactConfiguration.MOJANG_PRODUCTION
}

tasks.reobfJar {
// hacky workaround to make the shadow plugin shadow our mojang-mapped jar
outputJar.set(tasks.jar.map { it.outputs.files.singleFile }.get())
enabled = false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package de.pianoman911.mapengine.common;

import de.pianoman911.mapengine.api.util.PassthroughMode;
import de.pianoman911.mapengine.common.platform.IListenerBridge;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundAnimatePacket;
import net.minecraft.network.protocol.game.ServerboundAttackPacket;
import net.minecraft.network.protocol.game.ServerboundInteractPacket;
import net.minecraft.network.protocol.game.ServerboundSwingPacket;
import net.minecraft.world.phys.Vec3;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.entity.Player;

import java.util.List;

public final class Paper262Listener extends MessageToMessageDecoder<Packet<?>> {

private final Player player;
private final IListenerBridge bridge;

public Paper262Listener(Player player, IListenerBridge bridge) {
this.player = player;
this.bridge = bridge;
}

@Override
public boolean acceptInboundMessage(Object msg) {
return msg instanceof ServerboundInteractPacket || msg instanceof ServerboundSwingPacket || msg instanceof ServerboundAttackPacket;
}

@Override
protected void decode(ChannelHandlerContext ctx, Packet<?> msg, List<Object> out) {
PassthroughMode passthroughMode = null;
if (msg instanceof ServerboundInteractPacket interact) {
int entityId = interact.entityId();
Vec3 pos = interact.location();

passthroughMode = this.bridge.handleInteract(this.player, entityId, pos.x, pos.y, pos.z);
} else if (msg instanceof ServerboundSwingPacket) {
passthroughMode = this.bridge.handleSwing(this.player);
} else if (msg instanceof ServerboundAttackPacket(int entityId)) {
passthroughMode = this.bridge.handleAttack(this.player, entityId);
}
if (passthroughMode == PassthroughMode.ONLY_ANIMATION){
this.handleAnimation();
}
if (passthroughMode != null && passthroughMode != PassthroughMode.ALL) {
return;
}

out.add(msg);
}

private void handleAnimation() {
ClientboundAnimatePacket animatePacket = new ClientboundAnimatePacket(((CraftPlayer) this.player).getHandle(),
ClientboundAnimatePacket.SWING_MAIN_HAND);
this.player.getTrackedBy().forEach(player -> ((CraftPlayer) player).getHandle().connection.send(animatePacket));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package de.pianoman911.mapengine.common;

import de.pianoman911.mapengine.common.data.MapUpdateData;
import de.pianoman911.mapengine.common.platform.IListenerBridge;
import de.pianoman911.mapengine.common.platform.IPlatform;
import de.pianoman911.mapengine.common.platform.PacketContainer;
import io.netty.channel.Channel;
import io.papermc.paper.adventure.PaperAdventure;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.SharedConstants;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
import net.minecraft.network.protocol.game.ClientboundBundlePacket;
import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket;
import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket;
import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.EntityDataSerializers;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.entity.PositionMoveRotation;
import net.minecraft.world.entity.decoration.ItemFrame;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.saveddata.maps.MapDecoration;
import net.minecraft.world.level.saveddata.maps.MapId;
import net.minecraft.world.level.saveddata.maps.MapItemSavedData.MapPatch;
import net.minecraft.world.phys.Vec3;
import org.bukkit.Bukkit;
import org.bukkit.block.BlockFace;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapCursorCollection;
import org.bukkit.plugin.Plugin;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.lang.reflect.Field;

public class Paper262Platform implements IPlatform<Packet<ClientGamePacketListener>>, Listener {

private static final EntityDataAccessor<Byte> DATA_SHARED_FLAGS_ID = EntityDataSerializers.BYTE.createAccessor(0);
private static final EntityDataAccessor<ItemStack> DATA_ITEM = getItemFrameDataAccessor("DATA_ITEM");
private static final EntityDataAccessor<Integer> DATA_ROTATION = getItemFrameDataAccessor("DATA_ROTATION");
private static final EntityDataAccessor<Float> DATA_INTERACTION_BOX_WIDTH_ID = EntityDataSerializers.FLOAT.createAccessor(8);
private static final EntityDataAccessor<Float> DATA_INTERACTION_BOX_HEIGHT_ID = EntityDataSerializers.FLOAT.createAccessor(9);
private static final EntityDataAccessor<Boolean> DATA_INTERACTION_BOX_RESPONSIVE_ID = EntityDataSerializers.BOOLEAN.createAccessor(10);

private static final Paper262SynchedDataBuilder ITEM_FRAME_DATA = Paper262SynchedDataBuilder.builder()
.setDataItem(DATA_SHARED_FLAGS_ID, (byte) 0x00)
.setDataItem(DATA_ITEM, ItemStack.EMPTY)
.setDataItem(DATA_ROTATION, 0);
private static final Paper262SynchedDataBuilder INTERACTION_DATA = Paper262SynchedDataBuilder.builder()
.setDataItem(DATA_SHARED_FLAGS_ID, (byte) 0x00)
.setDataItem(DATA_INTERACTION_BOX_WIDTH_ID, 0f)
.setDataItem(DATA_INTERACTION_BOX_HEIGHT_ID, 0f)
.setDataItem(DATA_INTERACTION_BOX_RESPONSIVE_ID, false);

private final IListenerBridge bridge;

public Paper262Platform(Plugin plugin, IListenerBridge bridge) {
this.bridge = bridge;
Bukkit.getPluginManager().registerEvents(this, plugin);
}

@EventHandler
public void onJoin(PlayerJoinEvent event) {
Paper262Listener listener = new Paper262Listener(event.getPlayer(), this.bridge);
((CraftPlayer) event.getPlayer()).getHandle().connection.connection.channel
.pipeline().addAfter("decoder", "mapengine", listener);
}

@EventHandler
public void onQuit(PlayerQuitEvent event) {
// remove mapengine pipeline handler if the player is still connected
// while this event is being fired; this prevents errors if the player goes
// through a second configuration phase
Channel ch = ((CraftPlayer) event.getPlayer()).getHandle().connection.connection.channel;
ch.eventLoop().execute(() -> {
if (ch.isActive()) {
ch.pipeline().remove("mapengine");
}
});
}

@Override
public String getDisplayedName() {
return MinecraftServer.getServer().getServerModName()
+ " " + SharedConstants.getCurrentVersion().name();
}

@Override
public void sendPacket(Player player, PacketContainer<Packet<ClientGamePacketListener>> packet) {
((CraftPlayer) player).getHandle().connection.connection.channel.write(packet.getPacket());
}

@Override
public void flush(Player player) {
((CraftPlayer) player).getHandle().connection.connection.channel.flush();
}

@Override
public PacketContainer<Packet<ClientGamePacketListener>> createMapDataPacket(MapUpdateData data, int mapId, MapCursorCollection cursors) {
MapId id = new MapId(mapId);
MapPatch updateData = new MapPatch(data.offsetX(), data.offsetY(), data.width(), data.height(), data.buffer());

List<MapDecoration> decorations;
if (cursors != null && cursors.size() > 0) {
decorations = new ArrayList<>(cursors.size());
for (int i = 0; i < cursors.size(); i++) {
MapCursor cursor = cursors.getCursor(i);
if (!cursor.isVisible()) {
continue;
}

decorations.add(new MapDecoration(BuiltInRegistries.MAP_DECORATION_TYPE.get(cursor.getRawType()).orElseThrow(),
cursor.getX(), cursor.getY(), cursor.getDirection(),
cursor.caption() == null ? Optional.empty() : Optional.of(PaperAdventure.asVanilla(cursor.caption()))));
}
} else {
decorations = null;
}

return PacketContainer.wrap(this, new ClientboundMapItemDataPacket(id,
(byte) 0, decorations != null, decorations, updateData));
}

@Override
public PacketContainer<Packet<ClientGamePacketListener>> createMapEntitySpawnPacket(int entityId, BlockVector pos, BlockFace facing, boolean glowing) {
int facingIndex = switch (facing) {
case UP ->
1;
case NORTH ->
2;
case SOUTH ->
3;
case WEST ->
4;
case EAST ->
5;
default ->
0;
};

return PacketContainer.wrap(this, new ClientboundAddEntityPacket(entityId, UUID.randomUUID(),
pos.getX(), pos.getY(), pos.getZ(), 0, 0,
glowing ? EntityTypes.GLOW_ITEM_FRAME : EntityTypes.ITEM_FRAME, facingIndex, Vec3.ZERO, 0));
}

@Override
public PacketContainer<Packet<ClientGamePacketListener>> createMapSetIdPacket(int entityId, int mapId, boolean invisible) {
SynchedEntityData entityData = ITEM_FRAME_DATA.build();

ItemStack mapItem = Items.FILLED_MAP.getDefaultInstance();

mapItem.set(DataComponents.MAP_ID, new MapId(mapId));

entityData.set(DATA_ITEM, mapItem); // map item

if (invisible) {
entityData.set(DATA_SHARED_FLAGS_ID, (byte) 0x20); // invisible
}

return PacketContainer.wrap(this, new ClientboundSetEntityDataPacket(entityId, Objects.requireNonNull(entityData.packDirty())));
}

@Override
public PacketContainer<Packet<ClientGamePacketListener>> createRemoveEntitiesPacket(IntList entityIds) {
return PacketContainer.wrap(this, new ClientboundRemoveEntitiesPacket(entityIds));
}

@Override
public PacketContainer<?> createInteractionEntitySpawnPacket(int interactionId, Vector pos, BlockFace direction) {
return PacketContainer.wrap(this, new ClientboundAddEntityPacket(interactionId, UUID.randomUUID(),
pos.getX(), pos.getY(), pos.getZ(), 0, 0, EntityTypes.INTERACTION, 0, Vec3.ZERO, 0));
}

@Override
public PacketContainer<?> createInteractionEntityBlockSizePacket(int interactionId) {
SynchedEntityData entityData = INTERACTION_DATA.build();

entityData.set(DATA_INTERACTION_BOX_WIDTH_ID, 1f);
entityData.set(DATA_INTERACTION_BOX_HEIGHT_ID, 1f);
entityData.set(DATA_INTERACTION_BOX_RESPONSIVE_ID, true);
entityData.set(DATA_SHARED_FLAGS_ID, (byte) 0x20); // invisible

return PacketContainer.wrap(this, new ClientboundSetEntityDataPacket(interactionId, Objects.requireNonNull(entityData.packDirty())));
}

@Override
public PacketContainer<?> createTeleportPacket(int entityId, Vector pos, float yaw, float pitch, boolean onGround) {
PositionMoveRotation posData = new PositionMoveRotation(
new Vec3(pos.getX(), pos.getY(), pos.getZ()), Vec3.ZERO, yaw, pitch);
ClientboundTeleportEntityPacket packet = new ClientboundTeleportEntityPacket(
entityId, posData, Set.of(), onGround);
return PacketContainer.wrap(this, packet);
}

@Override
public PacketContainer<?> createItemRotationPacket(int entityId, int rotation) {
SynchedEntityData entityData = ITEM_FRAME_DATA.build();

entityData.set(DATA_ROTATION, rotation, true); // item rotation (0-7)

return PacketContainer.wrap(this, new ClientboundSetEntityDataPacket(entityId, Objects.requireNonNull(entityData.packDirty())));
}

@SuppressWarnings("unchecked")
private static <T> EntityDataAccessor<T> getItemFrameDataAccessor(String fieldName) {
try {
Field field = ItemFrame.class.getDeclaredField(fieldName);
field.setAccessible(true);
return (EntityDataAccessor<T>) field.get(null);
} catch (ReflectiveOperationException exception) {
throw new ExceptionInInitializerError(exception);
}
}

@SuppressWarnings("unchecked")
@Override
public void sendBundled(Player player, PacketContainer<?>... packets) {
List<Packet<? super ClientGamePacketListener>> mcPackets = new ArrayList<>(packets.length);
for (PacketContainer<?> packetContainer : packets) {
mcPackets.add((Packet<ClientGamePacketListener>) packetContainer.getPacket());
}
ClientboundBundlePacket packet = new ClientboundBundlePacket(mcPackets);
((CraftPlayer) player).getHandle().connection.send(packet);
}
}
Loading