mirror of
https://github.com/Minestom/Minestom.git
synced 2025-03-02 11:21:15 +01:00
Merge Map API & LWJGL code
This commit is contained in:
commit
214fa4c808
59
build.gradle
59
build.gradle
@ -1,9 +1,30 @@
|
||||
import org.gradle.internal.os.OperatingSystem
|
||||
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'java'
|
||||
id 'maven-publish'
|
||||
id 'net.ltgt.apt' version '0.10'
|
||||
id 'org.jetbrains.kotlin.jvm' version '1.3.72'
|
||||
}
|
||||
|
||||
project.ext.lwjglVersion = "3.2.3"
|
||||
|
||||
switch (OperatingSystem.current()) {
|
||||
case OperatingSystem.LINUX:
|
||||
def osArch = System.getProperty("os.arch")
|
||||
project.ext.lwjglNatives = osArch.startsWith("arm") || osArch.startsWith("aarch64")
|
||||
? "natives-linux-${osArch.contains("64") || osArch.startsWith("armv8") ? "arm64" : "arm32"}"
|
||||
: "natives-linux"
|
||||
break
|
||||
case OperatingSystem.MAC_OS:
|
||||
project.ext.lwjglNatives = "natives-macos"
|
||||
break
|
||||
case OperatingSystem.WINDOWS:
|
||||
project.ext.lwjglNatives = System.getProperty("os.arch").contains("64") ? "natives-windows" : "natives-windows-x86"
|
||||
break
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@ -32,6 +53,23 @@ sourceSets {
|
||||
compileClasspath += sourceSets.main.runtimeClasspath
|
||||
runtimeClasspath += sourceSets.main.runtimeClasspath
|
||||
}
|
||||
lwjgl {
|
||||
java {
|
||||
srcDir 'src/lwjgl/java'
|
||||
}
|
||||
|
||||
compileClasspath += sourceSets.main.runtimeClasspath
|
||||
runtimeClasspath += sourceSets.main.runtimeClasspath
|
||||
}
|
||||
}
|
||||
|
||||
// Minestom uses LWJGL libs as optional dependency if interfacing with a GPU is asked
|
||||
java {
|
||||
registerFeature("lwjgl") {
|
||||
usingSourceSet(sourceSets.lwjgl)
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -74,4 +112,25 @@ dependencies {
|
||||
|
||||
api "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||
api 'com.github.jglrxavpok:Hephaistos:v1.0.5'
|
||||
|
||||
// LWJGL, for map rendering
|
||||
lwjglApi platform("org.lwjgl:lwjgl-bom:$lwjglVersion")
|
||||
|
||||
lwjglApi "org.lwjgl:lwjgl"
|
||||
lwjglApi "org.lwjgl:lwjgl-egl"
|
||||
lwjglApi "org.lwjgl:lwjgl-opengl"
|
||||
lwjglApi "org.lwjgl:lwjgl-opengles"
|
||||
lwjglApi "org.lwjgl:lwjgl-glfw"
|
||||
lwjglRuntimeOnly "org.lwjgl:lwjgl::$lwjglNatives"
|
||||
lwjglRuntimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives"
|
||||
lwjglRuntimeOnly "org.lwjgl:lwjgl-opengles::$lwjglNatives"
|
||||
lwjglRuntimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives"
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
from components.java
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
Subproject commit 7979a3350d61d89c0d101ff12f2b8864beeff250
|
||||
Subproject commit f81048bc208feab0db9bbb759debb7e7fe426b0c
|
4
src/lwjgl/README.md
Normal file
4
src/lwjgl/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Minestom LWJGL code
|
||||
|
||||
Here is all LWJGL-related code in Minestom.
|
||||
Accessible when using "lwjgl" as an optional dependency in Gradle when declaring Minestom as a dependency
|
143
src/lwjgl/java/fr/themode/demo/map/MapAnimationDemo.java
Normal file
143
src/lwjgl/java/fr/themode/demo/map/MapAnimationDemo.java
Normal file
@ -0,0 +1,143 @@
|
||||
package fr.themode.demo.map;
|
||||
|
||||
import fr.themode.demo.Main;
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.event.player.PlayerSpawnEvent;
|
||||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.item.Material;
|
||||
import net.minestom.server.item.metadata.MapMeta;
|
||||
import net.minestom.server.map.framebuffers.GLFWFramebuffer;
|
||||
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 java.awt.*;
|
||||
|
||||
import static org.lwjgl.opengl.GL11.*;
|
||||
|
||||
public class MapAnimationDemo {
|
||||
|
||||
public static final int MAP_ID = 1;
|
||||
public static final int OPENGL_MAP_ID = 2;
|
||||
public static final int OPENGL2_MAP_ID = 3;
|
||||
|
||||
private static final Graphics2DFramebuffer framebuffer = new Graphics2DFramebuffer();
|
||||
private static final GLFWFramebuffer glfwFramebuffer = new GLFWFramebuffer();
|
||||
private static final GLFWFramebuffer glfwFramebuffer2 = new GLFWFramebuffer();
|
||||
|
||||
public static void init() {
|
||||
SchedulerManager scheduler = MinecraftServer.getSchedulerManager();
|
||||
scheduler.buildTask(MapAnimationDemo::tick).repeat(16, TimeUnit.MILLISECOND).schedule();
|
||||
|
||||
MinecraftServer.getConnectionManager().addPlayerInitialization(player -> {
|
||||
player.addEventCallback(PlayerSpawnEvent.class, event -> {
|
||||
ItemStack map = new ItemStack(Material.FILLED_MAP, (byte) 1);
|
||||
map.setItemMeta(new MapMeta(MAP_ID));
|
||||
player.getInventory().addItemStack(map);
|
||||
|
||||
ItemStack map2 = new ItemStack(Material.FILLED_MAP, (byte) 1);
|
||||
map2.setItemMeta(new MapMeta(OPENGL_MAP_ID));
|
||||
player.getInventory().addItemStack(map2);
|
||||
|
||||
ItemStack map3 = new ItemStack(Material.FILLED_MAP, (byte) 1);
|
||||
map3.setItemMeta(new MapMeta(OPENGL2_MAP_ID));
|
||||
player.getInventory().addItemStack(map3);
|
||||
});
|
||||
});
|
||||
|
||||
glfwFramebuffer.setupRenderLoop(16, TimeUnit.MILLISECOND, () -> {
|
||||
glClearColor(0f, 0f, 0f, 1f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
glBegin(GL_TRIANGLES);
|
||||
|
||||
glVertex2f(0, -0.75f);
|
||||
glColor3f(1f, 0f, 0f);
|
||||
|
||||
glVertex2f(0.75f, 0.75f);
|
||||
glColor3f(0f, 1f, 0f);
|
||||
|
||||
glVertex2f(-0.75f, 0.75f);
|
||||
glColor3f(0f, 0f, 1f);
|
||||
|
||||
glEnd();
|
||||
});
|
||||
|
||||
glfwFramebuffer2.setupRenderLoop(16, TimeUnit.MILLISECOND, () -> {
|
||||
glClearColor(0f, 1f, 0f, 1f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
glBegin(GL_TRIANGLES);
|
||||
|
||||
glVertex2f(0, -0.75f);
|
||||
glColor3f(1f, 0f, 0f);
|
||||
|
||||
glVertex2f(0.75f, 0.75f);
|
||||
glColor3f(0f, 1f, 0f);
|
||||
|
||||
glVertex2f(-0.75f, 0.75f);
|
||||
glColor3f(0f, 0f, 1f);
|
||||
|
||||
glEnd();
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
renderer.setColor(Color.ORANGE);
|
||||
renderer.drawString("Hi :-)", x+16, y+15);
|
||||
|
||||
MapDataPacket mapDataPacket = new MapDataPacket();
|
||||
mapDataPacket.mapId = MAP_ID;
|
||||
framebuffer.preparePacket(mapDataPacket);
|
||||
MinecraftServer.getConnectionManager().getOnlinePlayers().forEach(p -> {
|
||||
p.getPlayerConnection().sendPacket(mapDataPacket);
|
||||
});
|
||||
|
||||
mapDataPacket.mapId = OPENGL_MAP_ID;
|
||||
glfwFramebuffer.preparePacket(mapDataPacket);
|
||||
MinecraftServer.getConnectionManager().getOnlinePlayers().forEach(p -> {
|
||||
p.getPlayerConnection().sendPacket(mapDataPacket);
|
||||
});
|
||||
|
||||
mapDataPacket.mapId = OPENGL2_MAP_ID;
|
||||
glfwFramebuffer2.preparePacket(mapDataPacket);
|
||||
MinecraftServer.getConnectionManager().getOnlinePlayers().forEach(p -> {
|
||||
p.getPlayerConnection().sendPacket(mapDataPacket);
|
||||
});
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
MinecraftServer.init();
|
||||
|
||||
MinecraftServer.getCommandManager().register(new TestItemFrame());
|
||||
init();
|
||||
Main.main(args);
|
||||
}
|
||||
}
|
37
src/lwjgl/java/fr/themode/demo/map/TestItemFrame.java
Normal file
37
src/lwjgl/java/fr/themode/demo/map/TestItemFrame.java
Normal file
@ -0,0 +1,37 @@
|
||||
package fr.themode.demo.map;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
package net.minestom.server.map.framebuffers;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.map.Framebuffer;
|
||||
import net.minestom.server.map.MapColors;
|
||||
import net.minestom.server.timer.Task;
|
||||
import net.minestom.server.utils.time.TimeUnit;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.PointerBuffer;
|
||||
import org.lwjgl.glfw.GLFWErrorCallback;
|
||||
import org.lwjgl.opengl.GL;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static org.lwjgl.glfw.GLFW.*;
|
||||
import static org.lwjgl.opengl.GL11.*;
|
||||
|
||||
/**
|
||||
* GLFW-based framebuffer.
|
||||
*
|
||||
* Due to its interfacing with OpenGL(-ES), extra care needs to be applied when using this framebuffer.
|
||||
* Rendering to this framebuffer should only be done via the thread on which the context is present.
|
||||
* To perform map conversion at the end of a frame, it is advised to use {@link #render(Runnable)} to render to the map.
|
||||
*
|
||||
* Use {@link #changeRenderingThreadToCurrent} in a thread to switch the thread on which to render.
|
||||
*
|
||||
* Use {@link #setupRenderLoop} with a callback to setup a task in the {@link net.minestom.server.timer.SchedulerManager}
|
||||
* to automatically render to the offscreen buffer on a specialized thread.
|
||||
*
|
||||
* GLFWFramebuffer does not provide guarantee that the result of {@link #toMapColors()} is synchronized with rendering, but
|
||||
* it will be updated after each frame rendered through {@link #render(Runnable)} or {@link #setupRenderLoop(long, TimeUnit, Runnable)}.
|
||||
*
|
||||
* This framebuffer is meant to render to a single map (ie it is only compatible with 128x128 rendering)
|
||||
*/
|
||||
public class GLFWFramebuffer implements Framebuffer {
|
||||
|
||||
private final byte[] colors = new byte[WIDTH*HEIGHT];
|
||||
private final ByteBuffer pixels = BufferUtils.createByteBuffer(WIDTH*HEIGHT*4);
|
||||
private final long glfwWindow;
|
||||
|
||||
public GLFWFramebuffer() {
|
||||
this(GLFW_NATIVE_CONTEXT_API, GLFW_OPENGL_API);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the framebuffer and initializes a new EGL context
|
||||
*/
|
||||
public GLFWFramebuffer(int apiContext, int clientAPI) {
|
||||
if(!glfwInit()) {
|
||||
throw new RuntimeException("Failed to init GLFW");
|
||||
}
|
||||
|
||||
GLFWErrorCallback.createPrint().set();
|
||||
glfwDefaultWindowHints();
|
||||
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
|
||||
|
||||
glfwWindowHint(GLFW_CONTEXT_CREATION_API, apiContext);
|
||||
glfwWindowHint(GLFW_CLIENT_API, clientAPI);
|
||||
|
||||
this.glfwWindow = glfwCreateWindow(WIDTH, HEIGHT, "", 0L, 0L);
|
||||
if(glfwWindow == 0L) {
|
||||
try(var stack = MemoryStack.stackPush()) {
|
||||
PointerBuffer desc = stack.mallocPointer(1);
|
||||
int errcode = glfwGetError(desc);
|
||||
throw new RuntimeException("("+errcode+") Failed to create GLFW Window.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public GLFWFramebuffer unbindContextFromThread() {
|
||||
glfwMakeContextCurrent(0L);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void changeRenderingThreadToCurrent() {
|
||||
glfwMakeContextCurrent(glfwWindow);
|
||||
GL.createCapabilities();
|
||||
}
|
||||
|
||||
public Task setupRenderLoop(long period, TimeUnit unit, Runnable rendering) {
|
||||
return MinecraftServer.getSchedulerManager()
|
||||
.buildTask(new Runnable() {
|
||||
private boolean first = true;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if(first) {
|
||||
changeRenderingThreadToCurrent();
|
||||
first = false;
|
||||
}
|
||||
render(rendering);
|
||||
}
|
||||
})
|
||||
.repeat(period, unit)
|
||||
.schedule();
|
||||
}
|
||||
|
||||
public void render(Runnable rendering) {
|
||||
rendering.run();
|
||||
glfwSwapBuffers(glfwWindow);
|
||||
prepareMapColors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in render after glFlush to read the pixel buffer contents and convert it to map colors.
|
||||
* Only call if you do not use {@link #render(Runnable)} nor {@link #setupRenderLoop(long, TimeUnit, Runnable)}
|
||||
*/
|
||||
public void prepareMapColors() {
|
||||
glReadPixels(0, 0, WIDTH, HEIGHT, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
for (int y = 0; y < HEIGHT; y++) {
|
||||
for (int x = 0; x < WIDTH; x++) {
|
||||
int i = Framebuffer.index(x, y)*4;
|
||||
int red = pixels.get(i) & 0xFF;
|
||||
int green = pixels.get(i+1) & 0xFF;
|
||||
int blue = pixels.get(i+2) & 0xFF;
|
||||
int alpha = pixels.get(i+3) & 0xFF;
|
||||
int argb = (alpha << 24) | (red << 16) | (green << 8) | blue;
|
||||
colors[Framebuffer.index(x, y)] = MapColors.closestColor(argb).getIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
glfwTerminate();
|
||||
}
|
||||
|
||||
public long getGLFWWindow() {
|
||||
return glfwWindow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toMapColors() {
|
||||
return colors;
|
||||
}
|
||||
}
|
@ -132,6 +132,8 @@ public class MinecraftServer {
|
||||
private static MinecraftSessionService sessionService = authService.createMinecraftSessionService();
|
||||
|
||||
public static MinecraftServer init() {
|
||||
if(minecraftServer != null) // don't init twice
|
||||
return minecraftServer;
|
||||
// warmup/force-init registries
|
||||
// without this line, registry types that are not loaded explicitly will have an internal empty registry in Registries
|
||||
// That can happen with PotionType for instance, if no code tries to access a PotionType field
|
||||
|
@ -15,6 +15,12 @@ public class MapMeta implements ItemMeta {
|
||||
private List<MapDecoration> decorations = new ArrayList<>();
|
||||
private ChatColor mapColor = ChatColor.NO_COLOR;
|
||||
|
||||
public MapMeta() {}
|
||||
|
||||
public MapMeta(int id) {
|
||||
this.mapId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the map id
|
||||
*
|
||||
|
50
src/main/java/net/minestom/server/map/Framebuffer.java
Normal file
50
src/main/java/net/minestom/server/map/Framebuffer.java
Normal file
@ -0,0 +1,50 @@
|
||||
package net.minestom.server.map;
|
||||
|
||||
import net.minestom.server.network.packet.server.play.MapDataPacket;
|
||||
|
||||
/**
|
||||
* Framebuffer to render to a map
|
||||
*/
|
||||
public interface Framebuffer {
|
||||
|
||||
int WIDTH = 128;
|
||||
int HEIGHT = 128;
|
||||
|
||||
byte[] toMapColors();
|
||||
|
||||
default void preparePacket(MapDataPacket packet) {
|
||||
preparePacket(packet, 0, 0, WIDTH, HEIGHT);
|
||||
}
|
||||
|
||||
default void preparePacket(MapDataPacket packet, int minX, int minY, int width, int height) {
|
||||
byte[] colors;
|
||||
if(minX == 0 && minY == 0 && width == WIDTH && height == HEIGHT) {
|
||||
colors = toMapColors();
|
||||
} else {
|
||||
colors = new byte[width*height];
|
||||
byte[] mapColors = toMapColors();
|
||||
for (int y = minY; y < Math.min(HEIGHT, minY+height); y++) {
|
||||
for (int x = minX; x < Math.min(WIDTH, minX+width); x++) {
|
||||
byte color = mapColors[index(x, y, WIDTH)];
|
||||
colors[index(x-minX, y-minY, width)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packet.columns = (short) width;
|
||||
packet.rows = (short) height;
|
||||
packet.icons = new MapDataPacket.Icon[0];
|
||||
packet.x = (byte) minX;
|
||||
packet.z = (byte) minY;
|
||||
packet.data = colors;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
354
src/main/java/net/minestom/server/map/MapColors.java
Normal file
354
src/main/java/net/minestom/server/map/MapColors.java
Normal file
@ -0,0 +1,354 @@
|
||||
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 {
|
||||
NONE(0,0,0),
|
||||
GRASS(127, 178, 56),
|
||||
SAND(247, 233, 163),
|
||||
WOOL(199, 199, 199),
|
||||
FIRE(255, 0, 0),
|
||||
ICE(160, 160, 255),
|
||||
METAL(167, 167, 167),
|
||||
PLANT(0, 124, 0),
|
||||
SNOW(255, 255, 255),
|
||||
CLAY(164, 168, 184),
|
||||
DIRT(151, 109, 77),
|
||||
STONE(112, 112, 112),
|
||||
WATER(64, 64, 255),
|
||||
WOOD(143, 119, 72),
|
||||
QUARTZ(255, 252, 245),
|
||||
COLOR_ORANGE(216, 127, 51),
|
||||
COLOR_MAGENTA(178, 76, 216),
|
||||
COLOR_LIGHT_BLUE(102, 153, 216),
|
||||
COLOR_YELLOW(229, 229, 51),
|
||||
COLOR_LIGHT_GREEN(127, 204, 25),
|
||||
COLOR_PINK(242, 127, 165),
|
||||
COLOR_GRAY(76, 76, 76),
|
||||
COLOR_LIGHT_GRAY(153, 153, 153),
|
||||
COLOR_CYAN(76, 127, 153),
|
||||
COLOR_PURPLE(127, 63, 178),
|
||||
COLOR_BLUE(51, 76, 178),
|
||||
COLOR_BROWN(102, 76, 51),
|
||||
COLOR_GREEN(102, 127, 51),
|
||||
COLOR_RED(153, 51, 51),
|
||||
COLOR_BLACK(25, 25, 25),
|
||||
GOLD(250, 238, 77),
|
||||
DIAMOND(92, 219, 213),
|
||||
LAPIS(74, 128, 255),
|
||||
EMERALD(0, 217, 58),
|
||||
PODZOL(129, 86, 49),
|
||||
NETHER(112, 2, 0),
|
||||
TERRACOTTA_WHITE(209, 177, 161),
|
||||
TERRACOTTA_ORANGE(159, 82, 36),
|
||||
TERRACOTTA_MAGENTA(149, 87, 108),
|
||||
TERRACOTTA_LIGHT_BLUE(112, 108, 138),
|
||||
TERRACOTTA_YELLOW(186, 133, 36),
|
||||
TERRACOTTA_LIGHT_GREEN(103, 117, 53),
|
||||
TERRACOTTA_PINK(160, 77, 78),
|
||||
TERRACOTTA_GRAY(57, 41, 35),
|
||||
TERRACOTTA_LIGHT_GRAY(135, 107, 98),
|
||||
TERRACOTTA_CYAN(87, 92, 92),
|
||||
TERRACOTTA_PURPLE(122, 73, 88),
|
||||
TERRACOTTA_BLUE(76, 62, 92),
|
||||
TERRACOTTA_BROWN(76, 50, 35),
|
||||
TERRACOTTA_GREEN(76, 82, 42),
|
||||
TERRACOTTA_RED(142, 60, 46),
|
||||
TERRACOTTA_BLACK(37, 22, 16),
|
||||
CRIMSON_NYLIUM(189, 48, 49),
|
||||
CRIMSON_STEM(148, 63, 97),
|
||||
CRIMSON_HYPHAE(92, 25, 29),
|
||||
WARPED_NYLIUM(22, 126, 134),
|
||||
WARPED_STEM(58, 142, 140),
|
||||
WARPED_HYPHAE(86, 44, 62),
|
||||
WARPED_WART_BLOCK(20, 180, 133),
|
||||
;
|
||||
|
||||
private final int red;
|
||||
private final int green;
|
||||
private final int blue;
|
||||
|
||||
private static final ConcurrentHashMap<Integer, PreciseMapColor> 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;
|
||||
this.blue = blue;
|
||||
}
|
||||
|
||||
// From the wiki: https://minecraft.gamepedia.com/Map_item_format
|
||||
// Map Color ID Multiply R,G,B By = Multiplier
|
||||
//Base Color ID×4 + 0 180 0.71
|
||||
//Base Color ID×4 + 1 220 0.86
|
||||
//Base Color ID×4 + 2 255 (same color) 1
|
||||
//Base Color ID×4 + 3 135 0.53
|
||||
|
||||
/**
|
||||
* Returns the color index with RGB multiplied by 0.53, to use on a map
|
||||
*/
|
||||
public byte multiply53() {
|
||||
return (byte) ((ordinal() << 2) +3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the color index with RGB multiplied by 0.86, to use on a map
|
||||
*/
|
||||
public byte multiply86() {
|
||||
return (byte) ((ordinal() << 2) +1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the color index with RGB multiplied by 0.71, to use on a map
|
||||
*/
|
||||
public byte multiply71() {
|
||||
return (byte) (ordinal() << 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the color index to use on a map
|
||||
*/
|
||||
public byte baseColor() {
|
||||
return (byte) ((ordinal() << 2) +2);
|
||||
}
|
||||
|
||||
public int red() {
|
||||
return red;
|
||||
}
|
||||
|
||||
public int green() {
|
||||
return green;
|
||||
}
|
||||
|
||||
public int blue() {
|
||||
return blue;
|
||||
}
|
||||
|
||||
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 {
|
||||
private MapColors baseColor;
|
||||
private Multiplier multiplier;
|
||||
|
||||
PreciseMapColor(MapColors base, Multiplier multiplier) {
|
||||
this.baseColor = base;
|
||||
this.multiplier = multiplier;
|
||||
}
|
||||
|
||||
public MapColors getBaseColor() {
|
||||
return baseColor;
|
||||
}
|
||||
|
||||
public Multiplier getMultiplier() {
|
||||
return multiplier;
|
||||
}
|
||||
|
||||
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, 1.00),
|
||||
x0_53(MapColors::multiply53, 0.53),
|
||||
x0_71(MapColors::multiply71, 0.71),
|
||||
x0_86(MapColors::multiply86, 0.86),
|
||||
;
|
||||
|
||||
private final Function<MapColors, Byte> indexGetter;
|
||||
private final double multiplier;
|
||||
|
||||
Multiplier(Function<MapColors, Byte> 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,11 +36,12 @@ public class MapDataPacket implements ServerPacket {
|
||||
writer.writeVarInt(0);
|
||||
}
|
||||
|
||||
writer.writeByte(columns);
|
||||
if (columns <= 0)
|
||||
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) {
|
||||
|
Loading…
Reference in New Issue
Block a user