import java.awt.Color; import java.awt.image.BufferedImage; import java.util.Arrays; import java.util.Collections; import java.util.function.Supplier; import lombok.Setter; import net.minecraft.core.Direction; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket; import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; import net.minecraft.world.entity.EntityType; 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.MapItemSavedData.MapPatch; import net.minecraft.world.phys.Vec3; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.craftbukkit.v1_18_R2.CraftWorld; import org.bukkit.craftbukkit.v1_18_R2.entity.CraftPlayer; import org.bukkit.entity.Player; import org.bukkit.map.MapView; /* Map packet process: - Create an ItemFrame entity - Set it to have a map item, with the map ID attached to the map item To spawn: - Send an ADD ENTITY (Spawn) packet, containing the item frame entity - Send a SET ENTITY DATA (Metadata) packet, containing the metadata which indicates the map ID - Update (see below) To update: - Send a MAP ITEM DATA packet, containing the map ID, scale, and color patch */ public class ImageFrame { private final ImageBoard board; private final Point position; private final ItemFrame nmsFrame; private final MapView view; private byte[] lastFrame; // Sure this will double the memory usage, but saves A LOT of processing time and bandwidth @Setter private Supplier imageSupplier; public ImageFrame(ImageBoard board, Point position) { this.board = board; this.position = position; Location frameLocation = board.getFrameLocation(this); CraftWorld world = (CraftWorld) frameLocation.getWorld(); this.view = Bukkit.createMap(world); this.nmsFrame = new ItemFrame(EntityType.ITEM_FRAME, world.getHandle()); this.nmsFrame.setDirection(Direction.byName(board.getDirection().name())); this.nmsFrame.setPos(new Vec3(frameLocation.getX(), frameLocation.getY(), frameLocation.getZ())); this.nmsFrame.setInvisible(true); this.nmsFrame.setItem(createMapItem()); } private ItemStack createMapItem() { ItemStack item = new ItemStack(Items.FILLED_MAP); item.getOrCreateTag().putInt("map", view.getId()); // We can do this with Bukkit too, but this is easier return item; } private void sendPacket(Packet packet) { Player player = board.getPlayer(); ((CraftPlayer) player).getHandle().connection.send(packet); } public void draw() { if(imageSupplier == null) { return; } BufferedImage image = imageSupplier.get(); if(image == null) { return; } byte[] colors = getColors(image); if(Arrays.equals(colors, lastFrame)) { return; } if(lastFrame == null) { // Spawn the frame ClientboundAddEntityPacket packet = new ClientboundAddEntityPacket(nmsFrame); ClientboundSetEntityDataPacket metadataPacket = new ClientboundSetEntityDataPacket(nmsFrame.getId(), nmsFrame.getEntityData(), true); sendPacket(packet); sendPacket(metadataPacket); } lastFrame = colors; ClientboundMapItemDataPacket mapPacket = new ClientboundMapItemDataPacket( view.getId(), // Map view ID (byte) 3, // SCALE false, // Show icons Collections.emptyList(), // Icons new MapPatch(0, 0, 128, 128, colors) // Colors ); sendPacket(mapPacket); } private byte[] getColors(BufferedImage image) { byte[] buffer = new byte[128 * 128]; Arrays.fill(buffer, (byte) 0); if (image == null) { return buffer; } for (int y = 0; y < image.getHeight(); y++) { for (int x = 0; x < image.getWidth(); x++) { int index = (y * 128) + x; buffer[index] = ColorMatcher.getColor(new Color(image.getRGB(x, y))); } } // System.out.println("Render time: " + (System.currentTimeMillis() - time) + "ms + (sync = " + Bukkit.isPrimaryThread() + ")"); return buffer; } public Point getPosition() { return position; } }