diff --git a/src/main/java/fr/themode/demo/Main.java b/src/main/java/fr/themode/demo/Main.java index 5ca0c0f0f..9d7c95e87 100644 --- a/src/main/java/fr/themode/demo/Main.java +++ b/src/main/java/fr/themode/demo/Main.java @@ -35,6 +35,7 @@ public class Main { commandManager.register(new GamemodeCommand()); commandManager.register(new DimensionCommand()); commandManager.register(new ShutdownCommand()); + commandManager.register(new TestItemFrame()); MapAnimationDemo.init(); diff --git a/src/main/java/fr/themode/demo/commands/TestItemFrame.java b/src/main/java/fr/themode/demo/commands/TestItemFrame.java new file mode 100644 index 000000000..956f6db48 --- /dev/null +++ b/src/main/java/fr/themode/demo/commands/TestItemFrame.java @@ -0,0 +1,38 @@ +package fr.themode.demo.commands; + +import fr.themode.demo.map.MapAnimationDemo; +import net.minestom.server.command.CommandProcessor; +import net.minestom.server.command.CommandSender; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.type.EntityItemFrame; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.item.metadata.MapMeta; + +public class TestItemFrame implements CommandProcessor { + @Override + public String getCommandName() { + return "itemframe"; + } + + @Override + public String[] getAliases() { + return new String[0]; + } + + @Override + public boolean process(CommandSender sender, String command, String[] args) { + Player player = (Player)sender; + EntityItemFrame frame = new EntityItemFrame(player.getPosition(), EntityItemFrame.ItemFrameOrientation.SOUTH); + ItemStack map = new ItemStack(Material.FILLED_MAP, (byte) 1); + map.setItemMeta(new MapMeta(MapAnimationDemo.MAP_ID)); + frame.setItemStack(map); + frame.setInstance(player.getInstance()); + return true; + } + + @Override + public boolean hasAccess(Player player) { + return true; + } +} diff --git a/src/main/java/fr/themode/demo/map/MapAnimationDemo.java b/src/main/java/fr/themode/demo/map/MapAnimationDemo.java index 2d738fe78..ebad876a2 100644 --- a/src/main/java/fr/themode/demo/map/MapAnimationDemo.java +++ b/src/main/java/fr/themode/demo/map/MapAnimationDemo.java @@ -6,10 +6,17 @@ import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.item.metadata.MapMeta; import net.minestom.server.map.MapColors; +import net.minestom.server.map.framebuffers.Graphics2DFramebuffer; import net.minestom.server.network.packet.server.play.MapDataPacket; import net.minestom.server.timer.SchedulerManager; import net.minestom.server.utils.time.TimeUnit; +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + public class MapAnimationDemo { public static final int MAP_ID = 1; @@ -27,30 +34,48 @@ public class MapAnimationDemo { }); } + private static final Graphics2DFramebuffer framebuffer = new Graphics2DFramebuffer(); + + private static float time = 0f; + private static long lastTime = System.currentTimeMillis(); + public static void tick() { + Graphics2D renderer = framebuffer.getRenderer(); + renderer.setColor(Color.BLACK); + renderer.clearRect(0, 0, 128, 128); + renderer.setColor(Color.WHITE); + renderer.drawString("Hello from", 0, 10); + renderer.drawString("Graphics2D!", 0, 20); + + long currentTime = System.currentTimeMillis(); + long l = currentTime / 60; + if(l % 2 == 0) { + renderer.setColor(Color.RED); + } + renderer.fillRect(128-10, 0, 10, 10); + + renderer.setColor(Color.GREEN); + float dt = (currentTime-lastTime)/1000.0f; + lastTime = currentTime; + time += dt; + float speed = 10f; + int x = (int) (Math.cos(time*speed) * 10 + 64) - 25; + int y = (int) (Math.sin(time*speed) * 10 + 64) - 10; + renderer.fillRoundRect(x, y, 50, 20, 10, 10); + MapDataPacket mapDataPacket = new MapDataPacket(); mapDataPacket.mapId = MAP_ID; - mapDataPacket.columns = 127; - mapDataPacket.rows = 127; + mapDataPacket.columns = 128; + mapDataPacket.rows = 128; mapDataPacket.icons = new MapDataPacket.Icon[0]; mapDataPacket.x = 0; mapDataPacket.z = 0; mapDataPacket.scale = 0; mapDataPacket.locked = true; mapDataPacket.trackingPosition = true; - byte[] colors = new byte[128*128]; - for (int x = 0; x < 128; x++) { - for (int z = 0; z < 128; z++) { - int r = (int) (Math.random() * MapColors.values().length); - MapColors baseColor = MapColors.values()[r]; - int m = (int) (Math.random() * 4); - byte colorID = (byte) ((baseColor.ordinal() << 2) + m); - colors[x+z*128] = colorID; - } - } - mapDataPacket.data = colors; + mapDataPacket.data = framebuffer.toMapColors(); MinecraftServer.getConnectionManager().getOnlinePlayers().forEach(p -> { - p.sendPacketToViewersAndSelf(mapDataPacket); + p.getPlayerConnection().sendPacket(mapDataPacket); }); } } diff --git a/src/main/java/net/minestom/server/map/Framebuffer.java b/src/main/java/net/minestom/server/map/Framebuffer.java new file mode 100644 index 000000000..7b1bdd390 --- /dev/null +++ b/src/main/java/net/minestom/server/map/Framebuffer.java @@ -0,0 +1,21 @@ +package net.minestom.server.map; + +/** + * Framebuffer to render to a map + */ +public interface Framebuffer { + + int WIDTH = 128; + int HEIGHT = 128; + + byte[] toMapColors(); + + static int index(int x, int z) { + return index(x, z, WIDTH); + } + + static int index(int x, int z, int stride) { + return z*stride + x; + } + +} diff --git a/src/main/java/net/minestom/server/map/MapColors.java b/src/main/java/net/minestom/server/map/MapColors.java index 772c73b23..f3450ec14 100644 --- a/src/main/java/net/minestom/server/map/MapColors.java +++ b/src/main/java/net/minestom/server/map/MapColors.java @@ -1,5 +1,9 @@ package net.minestom.server.map; +import net.minestom.server.utils.thread.MinestomThread; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.function.Function; public enum MapColors { @@ -68,6 +72,50 @@ public enum MapColors { private final int green; private final int blue; + private static final ConcurrentHashMap rgbMap = new ConcurrentHashMap<>(); + // only used if mappingStrategy == ColorMappingStrategy.PRECISE + private static PreciseMapColor[] rgbArray = null; + + private static final ColorMappingStrategy mappingStrategy; + private static final String MAPPING_ARGUMENT = "minestom.map.rgbmapping"; + // only used if MAPPING_ARGUMENT is "approximate" + private static final String REDUCTION_ARGUMENT = "minestom.map.rgbreduction"; + private static final int colorReduction; + + static { + ColorMappingStrategy strategy; + String strategyStr = System.getProperty(MAPPING_ARGUMENT); + if(strategyStr == null) { + strategy = ColorMappingStrategy.LAZY; + } else { + try { + strategy = ColorMappingStrategy.valueOf(strategyStr.toUpperCase()); + } catch (IllegalArgumentException e) { + System.err.println("Unknown color mapping strategy: "+strategyStr); + System.err.println("Defaulting to LAZY."); + strategy = ColorMappingStrategy.LAZY; + } + } + mappingStrategy = strategy; + + int reduction = 10; + String reductionStr = System.getProperty(REDUCTION_ARGUMENT); + if(reductionStr != null) { + try { + reduction = Integer.parseInt(reductionStr); + } catch (NumberFormatException e) { + System.err.println("Invalid integer in reduction argument: "+reductionStr); + e.printStackTrace(); + } + + if(reduction < 0 || reduction >= 255) { + System.err.println("Reduction was found to be invalid: "+reduction+". Must in 0-255, defaulting to 10."); + reduction = 10; + } + } + colorReduction = reduction; + } + MapColors(int red, int green, int blue) { this.red = red; this.green = green; @@ -121,8 +169,101 @@ public enum MapColors { return blue; } - public PreciseMapColor closestColor(int rgb) { - throw new UnsupportedOperationException("TODO"); + private static void fillRGBMap() { + for(MapColors base : values()) { + if(base == NONE) + continue; + for(Multiplier m : Multiplier.values()) { + PreciseMapColor preciseMapColor = new PreciseMapColor(base, m); + int rgb = preciseMapColor.toRGB(); + + if(mappingStrategy == ColorMappingStrategy.APPROXIMATE) { + rgb = reduceColor(rgb); + } + rgbMap.put(rgb, preciseMapColor); + } + } + } + + private static void fillRGBArray() { + rgbArray = new PreciseMapColor[0xFFFFFF+1]; + MinestomThread threads = new MinestomThread(Runtime.getRuntime().availableProcessors(), "RGBMapping", true); + for (int rgb = 0; rgb <= 0xFFFFFF; rgb++) { + int finalRgb = rgb; + threads.execute(() -> rgbArray[finalRgb] = mapColor(finalRgb)); + } + try { + threads.shutdown(); + threads.awaitTermination(100, TimeUnit.MINUTES); + } catch (Throwable t) { + t.printStackTrace(); + } + System.out.println("done mapping."); // todo: remove, debug only + } + + public static PreciseMapColor closestColor(int argb) { + int noAlpha = argb & 0xFFFFFF; + if (mappingStrategy == ColorMappingStrategy.PRECISE) { + if(rgbArray == null) { + synchronized (MapColors.class) { + if(rgbArray == null) { + fillRGBArray(); + } + } + } + return rgbArray[noAlpha]; + } + if(rgbMap.isEmpty()) { + synchronized (rgbMap) { + if(rgbMap.isEmpty()) { + fillRGBMap(); + } + } + } + if(mappingStrategy == ColorMappingStrategy.APPROXIMATE) { + noAlpha = reduceColor(noAlpha); + } + return rgbMap.computeIfAbsent(noAlpha, MapColors::mapColor); + } + + private static int reduceColor(int rgb) { + int red = (rgb >> 16) & 0xFF; + int green = (rgb >> 8) & 0xFF; + int blue = rgb & 0xFF; + + red = red/colorReduction; + green = green/colorReduction; + blue = blue/colorReduction; + return (red << 16) | (green << 8) | blue; + } + + private static PreciseMapColor mapColor(int rgb) { + PreciseMapColor closest = null; + int closestDistance = Integer.MAX_VALUE; + for(MapColors base : values()) { + if (base == NONE) + continue; + for (Multiplier m : Multiplier.values()) { + int rgbKey = PreciseMapColor.toRGB(base, m); + int redKey = (rgbKey >> 16) & 0xFF; + int greenKey = (rgbKey >> 8) & 0xFF; + int blueKey = rgbKey & 0xFF; + + int red = (rgb >> 16) & 0xFF; + int green = (rgb >> 8) & 0xFF; + int blue = rgb & 0xFF; + + int dr = redKey - red; + int dg = greenKey - green; + int db = blueKey - blue; + int dist = (dr * dr + dg * dg + db * db); + if (dist < closestDistance) { + closest = new PreciseMapColor(base, m); + closestDistance = dist; + } + } + } + return closest; } public static class PreciseMapColor { @@ -145,23 +286,69 @@ public enum MapColors { public byte getIndex() { return multiplier.apply(baseColor); } + + public int toRGB() { + return toRGB(baseColor, multiplier); + } + + public static int toRGB(MapColors baseColor, Multiplier multiplier) { + double r = baseColor.red(); + double g = baseColor.green(); + double b = baseColor.blue(); + + r *= multiplier.multiplier(); + g *= multiplier.multiplier(); + b *= multiplier.multiplier(); + + int red = (int) r; + int green = (int) g; + int blue = (int) b; + return (red << 16) | (green << 8) | blue; + } } enum Multiplier { - x1_00(MapColors::baseColor), - x0_53(MapColors::multiply53), - x0_71(MapColors::multiply71), - x0_86(MapColors::multiply86), + x1_00(MapColors::baseColor, 1.00), + x0_53(MapColors::multiply53, 0.53), + x0_71(MapColors::multiply71, 0.71), + x0_86(MapColors::multiply86, 0.86), ; private final Function indexGetter; + private final double multiplier; - Multiplier(Function indexGetter) { + Multiplier(Function indexGetter, double multiplier) { this.indexGetter = indexGetter; + this.multiplier = multiplier; + } + + public double multiplier() { + return multiplier; } public byte apply(MapColors baseColor) { return indexGetter.apply(baseColor); } } + + /** + * How does Minestom compute RGB->MapColor transitions? + */ + public enum ColorMappingStrategy { + /** + * If already computed, send the result. Otherwise, compute the closest color in a RGB Map, and add it to the map + */ + LAZY, + + /** + * All colors are already in the map after the first call. Heavy hit on the memory: + * (2^24) * 4 bytes at the min (~64MB) + */ + PRECISE, + + /** + * RGB components are divided by 10 before issuing a lookup (as with the PRECISE strategy), but saves on memory usage + */ + APPROXIMATE; + } } diff --git a/src/main/java/net/minestom/server/map/framebuffers/DirectFramebuffer.java b/src/main/java/net/minestom/server/map/framebuffers/DirectFramebuffer.java new file mode 100644 index 000000000..fd30f4e5e --- /dev/null +++ b/src/main/java/net/minestom/server/map/framebuffers/DirectFramebuffer.java @@ -0,0 +1,33 @@ +package net.minestom.server.map.framebuffers; + +import net.minestom.server.map.Framebuffer; + +/** + * Framebuffer with direct access to the colors array + */ +public class DirectFramebuffer implements Framebuffer { + + private final byte[] colors = new byte[WIDTH*HEIGHT]; + + /** + * Mutable colors array + * @return + */ + public byte[] getColors() { + return colors; + } + + public byte get(int x, int z) { + return colors[Framebuffer.index(x, z)]; + } + + public DirectFramebuffer set(int x, int z, byte color) { + colors[Framebuffer.index(x, z)] = color; + return this; + } + + @Override + public byte[] toMapColors() { + return colors; + } +} diff --git a/src/main/java/net/minestom/server/map/framebuffers/Graphics2DFramebuffer.java b/src/main/java/net/minestom/server/map/framebuffers/Graphics2DFramebuffer.java new file mode 100644 index 000000000..8de49e3ac --- /dev/null +++ b/src/main/java/net/minestom/server/map/framebuffers/Graphics2DFramebuffer.java @@ -0,0 +1,52 @@ +package net.minestom.server.map.framebuffers; + +import net.minestom.server.map.Framebuffer; +import net.minestom.server.map.MapColors; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; + +/** + * Framebuffer that embeds a BufferedImage, allowing for rendering directly via Graphics2D or its pixel array + */ +public class Graphics2DFramebuffer implements Framebuffer { + + private final byte[] colors = new byte[WIDTH*HEIGHT]; + private final BufferedImage backingImage = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); + private final Graphics2D renderer; + private final int[] pixels; + + public Graphics2DFramebuffer() { + renderer = backingImage.createGraphics(); + pixels = ((DataBufferInt)backingImage.getRaster().getDataBuffer()).getData(); + } + + public Graphics2D getRenderer() { + return renderer; + } + + public BufferedImage getBackingImage() { + return backingImage; + } + + public int get(int x, int z) { + return pixels[x+z*WIDTH]; // stride is always the width of the image + } + + public Graphics2DFramebuffer set(int x, int z, int rgb) { + pixels[x+z*WIDTH] = rgb; + return this; + } + + @Override + public byte[] toMapColors() { + // TODO: update subparts only + for (int x = 0; x < 128; x++) { + for (int z = 0; z < 128; z++) { + colors[Framebuffer.index(x, z)] = MapColors.closestColor(get(x, z)).getIndex(); + } + } + return colors; + } +} diff --git a/src/main/java/net/minestom/server/network/packet/server/play/MapDataPacket.java b/src/main/java/net/minestom/server/network/packet/server/play/MapDataPacket.java index aa6f1cde3..e3b87ab81 100644 --- a/src/main/java/net/minestom/server/network/packet/server/play/MapDataPacket.java +++ b/src/main/java/net/minestom/server/network/packet/server/play/MapDataPacket.java @@ -14,8 +14,8 @@ public class MapDataPacket implements ServerPacket { public Icon[] icons; - public byte columns; - public byte rows; + public short columns; + public short rows; public byte x; public byte z; public byte[] data; @@ -36,12 +36,12 @@ public class MapDataPacket implements ServerPacket { writer.writeVarInt(0); } - writer.writeByte(columns); + writer.writeByte((byte)columns); if (columns <= 0) { return; } - writer.writeByte(rows); + writer.writeByte((byte)rows); writer.writeByte(x); writer.writeByte(z); if (data != null && data.length > 0) {