supportedChars = charsToCheck.stream()
+ /**
+ * Convert the specified font into a MapFont. Since we can't get a list of supported codepoints from
+ * the font object you have to specify each individual character that you want us to convert.
+ *
+ * Unsupported codepoints will be ignored.
+ *
+ * @param font The Java font
+ * @param textToCheck The characters that you want us to convert
+ *
+ * @return The converted font
+ */
+ public static MapFont convert(final Font font, final String textToCheck) {
+ final List supportedCodepoints = textToCheck.codePoints()
.filter(font::canDisplay)
+ .boxed()
.toList();
final MapFont mapFont = new MapFont();
- for (final char c : supportedChars) {
- final BufferedImage img = toImage(font, c);
+ for (final int cp : supportedCodepoints) {
+ final BufferedImage img = toImage(font, cp);
if (img == null) {
continue;
}
- final MapFont.CharacterSprite sprite = makeSprite(img);
- mapFont.setChar(c, sprite);
+ final Sprite sprite = makeSprite(img);
+ mapFont.set(cp, sprite);
}
return mapFont;
}
- private static BufferedImage toImage(final Font font, final char c) {
+ /**
+ * Draw a single codepoint on an image
+ *
+ * @param font The parent font
+ * @param cp The codepoint
+ *
+ * @return The image
+ */
+ private static BufferedImage toImage(final Font font, final int cp) {
+ // Get bounds of the codepoint (why does this have to be so complicated)
BufferedImage image = newImg(1, 1);
Graphics2D graphics = image.createGraphics();
- final Rectangle2D bounds = font.getStringBounds(String.valueOf(c), graphics.getFontMetrics().getFontRenderContext());
+ final Rectangle2D bounds = font.getStringBounds(new String(Character.toChars(cp)), graphics.getFontMetrics().getFontRenderContext());
graphics.dispose();
if (bounds.getWidth() <= 0 || bounds.getHeight() <= 0) {
return null;
}
+ // Create image with correct size
image = newImg((int) Math.ceil(bounds.getWidth()), (int) Math.ceil(bounds.getHeight()));
graphics = image.createGraphics();
- graphics.setColor(new Color(0, 0, 0, 0));
- graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
graphics.setColor(Color.BLACK);
graphics.setFont(font);
- graphics.drawString(String.valueOf(c), 0, graphics.getFontMetrics().getAscent());
+ graphics.drawString(new String(Character.toChars(cp)), 0, graphics.getFontMetrics().getAscent());
graphics.dispose();
return image;
}
+ /**
+ * Create a new image with the specified bounds
+ *
+ * @param w The width of the image
+ * @param h The height of the image
+ *
+ * @return A new image
+ */
private static BufferedImage newImg(final int w, final int h) {
return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
}
- private static MapFont.CharacterSprite makeSprite(final BufferedImage image) {
+ /**
+ * Convert an image into a MapFont sprite
+ *
+ * @param image The image
+ *
+ * @return The sprite
+ */
+ private static Sprite makeSprite(final BufferedImage image) {
final boolean[] data = new boolean[image.getWidth() * image.getHeight()];
- final MapFont.CharacterSprite sprite = new MapFont.CharacterSprite(image.getWidth(), image.getHeight(), data);
+ final Sprite sprite = new Sprite(image.getWidth(), image.getHeight(), data);
for (int x = 0; x < image.getWidth(); x++) {
for (int y = 0; y < image.getHeight(); y++) {
diff --git a/common/src/main/java/dev/cerus/maps/api/font/MapFont.java b/common/src/main/java/dev/cerus/maps/api/font/MapFont.java
new file mode 100644
index 0000000..6a17d7b
--- /dev/null
+++ b/common/src/main/java/dev/cerus/maps/api/font/MapFont.java
@@ -0,0 +1,163 @@
+package dev.cerus.maps.api.font;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.bukkit.map.MinecraftFont;
+
+/**
+ * Represents a font
+ */
+public class MapFont {
+
+ public static final MapFont MINECRAFT_FONT = fromBukkit(MinecraftFont.Font);
+
+ private final Map codePointMap = new HashMap<>();
+ private int maxHeight;
+
+ /**
+ * Convert Bukkit font to maps font
+ *
+ * @param bukkitFont The Bukkit font
+ *
+ * @return The converted font
+ */
+ public static MapFont fromBukkit(final org.bukkit.map.MapFont bukkitFont) {
+ final MapFont mapFont = new MapFont();
+ try {
+ final Field charsField = org.bukkit.map.MapFont.class.getDeclaredField("chars");
+ charsField.setAccessible(true);
+ final Map spriteMap
+ = (Map) charsField.get(bukkitFont);
+ spriteMap.forEach((character, characterSprite) ->
+ mapFont.set(String.valueOf(character).codePointAt(0), Sprite.fromBukkit(characterSprite)));
+ } catch (final NoSuchFieldException | IllegalAccessException e) {
+ throw new RuntimeException("Failed to convert Bukkit font into maps font", e);
+ }
+ return mapFont;
+ }
+
+ /**
+ * Register a codepoint
+ *
+ * @param codePoint The codepoint
+ * @param sprite The sprite
+ */
+ public void set(final int codePoint, final Sprite sprite) {
+ this.codePointMap.put(codePoint, sprite);
+ if (sprite.getHeight() > this.maxHeight) {
+ this.maxHeight = sprite.getHeight();
+ }
+ }
+
+ /**
+ * Get a registered sprite
+ *
+ * @param codePoint The codepoint registered to the sprite
+ *
+ * @return The sprite
+ *
+ * @throws IllegalArgumentException if the codepoint was not registered
+ */
+ public Sprite get(final int codePoint) {
+ if (!this.isValid(codePoint)) {
+ throw new IllegalArgumentException("Invalid code point");
+ }
+ return this.codePointMap.get(codePoint);
+ }
+
+ /**
+ * Get all sprites for the specified text
+ *
+ * @param text The text
+ *
+ * @return A list of sprites
+ *
+ * @throws IllegalArgumentException if the text is invalid
+ */
+ public List getSprites(final String text) {
+ if (!this.isValid(text)) {
+ throw new IllegalArgumentException("Unsupported chars");
+ }
+ return text.codePoints().mapToObj(this::get).toList();
+ }
+
+ /**
+ * Get the width of a string
+ *
+ * @param text The string
+ *
+ * @return The width of the string
+ *
+ * @throws IllegalArgumentException if the text is invalid
+ */
+ public int getWidth(final String text) {
+ if (text == null || text.length() == 0 || !this.isValid(text)) {
+ throw new IllegalArgumentException("Invalid text");
+ }
+ return text.codePoints().map(cp -> this.codePointMap.get(cp).getWidth()).sum() + text.length() - 1;
+ }
+
+ /**
+ * Get the maximum height of this font
+ *
+ * @return The max height
+ */
+ public int getHeight() {
+ return this.getHeight(null);
+ }
+
+ /**
+ * Get the height for a string
+ *
+ * @param text The string
+ *
+ * @return The height of the string
+ *
+ * @throws IllegalArgumentException if the text is invalid
+ */
+ public int getHeight(final String text) {
+ if (text == null) {
+ return this.maxHeight;
+ }
+ if (!this.isValid(text)) {
+ throw new IllegalArgumentException("Unsupported characters");
+ }
+ return text.codePoints().map(cp -> this.codePointMap.get(cp).getHeight()).max().orElse(0);
+ }
+
+ /**
+ * Checks if a codepoint was registered
+ *
+ * @param codePoint The codepoint
+ *
+ * @return True if valid
+ */
+ public boolean isValid(final int codePoint) {
+ return this.codePointMap.containsKey(codePoint);
+ }
+
+ /**
+ * Checks if a character was registered
+ *
+ * @param c The character
+ *
+ * @return True if valid
+ */
+ public boolean isValid(final char c) {
+ return this.isValid(String.valueOf(c).codePointAt(0));
+ }
+
+ /**
+ * Checks if a string is valid
+ *
+ * @param text The string
+ *
+ * @return True if valid
+ */
+ public boolean isValid(final String text) {
+ return text.codePoints().allMatch(this::isValid);
+ }
+
+}
diff --git a/common/src/main/java/dev/cerus/maps/api/font/Sprite.java b/common/src/main/java/dev/cerus/maps/api/font/Sprite.java
new file mode 100644
index 0000000..899eaee
--- /dev/null
+++ b/common/src/main/java/dev/cerus/maps/api/font/Sprite.java
@@ -0,0 +1,71 @@
+package dev.cerus.maps.api.font;
+
+import org.bukkit.map.MapFont;
+
+/**
+ * Represents a single character / codepoint
+ */
+public class Sprite {
+
+ private final int width;
+ private final int height;
+ private final boolean[] grid;
+
+ public Sprite(final int width, final int height) {
+ this(width, height, new boolean[width * height]);
+ }
+
+ public Sprite(final int width, final int height, final boolean[] grid) {
+ if (grid.length != width * height) {
+ throw new IllegalArgumentException();
+ }
+ this.width = width;
+ this.height = height;
+ this.grid = grid;
+ }
+
+ /**
+ * Convert Bukkit sprite into maps sprite
+ *
+ * @param bukkitSprite The Bukkit sprite
+ *
+ * @return The converted sprite
+ */
+ public static Sprite fromBukkit(final MapFont.CharacterSprite bukkitSprite) {
+ final boolean[] data = new boolean[bukkitSprite.getWidth() * bukkitSprite.getHeight()];
+ for (int x = 0; x < bukkitSprite.getWidth(); x++) {
+ for (int y = 0; y < bukkitSprite.getHeight(); y++) {
+ data[y * bukkitSprite.getWidth() + x] = bukkitSprite.get(y, x);
+ }
+ }
+ return new Sprite(bukkitSprite.getWidth(), bukkitSprite.getHeight(), data);
+ }
+
+ /**
+ * Get a pixel
+ *
+ * @param row The row
+ * @param col The column
+ *
+ * @return The pixel (true = visible, false = invisible)
+ */
+ public boolean get(final int row, final int col) {
+ if (row < 0 || col < 0 || row >= this.height || col >= this.width) {
+ return false;
+ }
+ return this.grid[row * this.width + col];
+ }
+
+ public int getWidth() {
+ return this.width;
+ }
+
+ public int getHeight() {
+ return this.height;
+ }
+
+ public boolean[] getGrid() {
+ return this.grid;
+ }
+
+}
diff --git a/common/src/main/java/dev/cerus/maps/api/graphics/MapGraphics.java b/common/src/main/java/dev/cerus/maps/api/graphics/MapGraphics.java
index bcbdf72..7339819 100644
--- a/common/src/main/java/dev/cerus/maps/api/graphics/MapGraphics.java
+++ b/common/src/main/java/dev/cerus/maps/api/graphics/MapGraphics.java
@@ -1,6 +1,8 @@
package dev.cerus.maps.api.graphics;
import dev.cerus.maps.api.colormap.ColorMaps;
+import dev.cerus.maps.api.font.MapFont;
+import dev.cerus.maps.api.font.Sprite;
import dev.cerus.maps.api.graphics.filter.BoxBlurFilter;
import dev.cerus.maps.api.graphics.filter.Filter;
import dev.cerus.maps.api.graphics.filter.GrayscaleFilter;
@@ -10,8 +12,6 @@
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
-import org.bukkit.map.MapFont;
-import org.bukkit.map.MinecraftFont;
/**
* The 2D rendering engine for maps. Implementations only need to take care of pixel setting and retrieving.
@@ -622,79 +622,75 @@ public void drawImage(final BufferedImage img, final int x, final int y) {
/**
* Draws text
- *
- * Stolen from Bukkit, sorry
*
- * @param x X coordinate
- * @param y Y coordinate
- * @param text The text
- * @param startColor The color
- * @param size The size multiplier (1 = normal)
+ * @param x X coordinate
+ * @param y Y coordinate
+ * @param text The text
+ * @param color The color
+ * @param size The size multiplier (1 = normal)
*/
- public void drawText(final int x, final int y, final String text, final byte startColor, final int size) {
- this.drawText(x, y, text, MinecraftFont.Font, startColor, size);
+ public void drawText(final int x, final int y, final String text, final byte color, final int size) {
+ this.drawText(x, y, text, MapFont.MINECRAFT_FONT, color, size);
}
- public void drawText(int x, int y, final String text, final MapFont font, final byte startColor, final int size) {
+ /**
+ * Draws text
+ *
+ * This method was originally copied from Bukkit. It has been heavily modified since then to fit our needs.
+ *
+ * @param x X coordinate
+ * @param y Y coordinate
+ * @param text The text
+ * @param font The font
+ * @param color The color
+ * @param size The size multiplier (1 = normal)
+ */
+ public void drawText(int x, int y, final String text, final MapFont font, final byte color, final int size) {
if (size <= 0) {
throw new IllegalArgumentException("size <= 0");
}
+ final String textWithoutLinefeed = text == null ? "" : text.replace("\n", "");
+ if (textWithoutLinefeed.length() == 0) {
+ return;
+ }
+ if (!font.isValid(textWithoutLinefeed)) {
+ throw new IllegalArgumentException("Invalid text");
+ }
final int xStart = x;
- byte color = startColor;
- if (!font.isValid(text)) {
- throw new IllegalArgumentException("text contains invalid characters");
- } else {
- int currentIndex = 0;
-
- while (true) {
- if (currentIndex >= text.length()) {
- return;
- }
+ final int[] codePoints = text.codePoints().toArray();
+ int currentIndex = 0;
- final char ch = text.charAt(currentIndex);
- if (ch == '\n') {
- // Increment z if the char is a line separator
- x = xStart;
- y += font.getHeight() + 1;
- } else if (ch == '\u00A7' /*-> §*/) {
- // Get distance from current char to end char (';')
- final int end = text.indexOf(';', currentIndex);
- if (end < 0) {
- break;
- }
+ while (true) {
+ if (currentIndex >= codePoints.length) {
+ return;
+ }
- // Parse color
- try {
- color = Byte.parseByte(text.substring(currentIndex + 1, end));
- currentIndex = end;
- } catch (final NumberFormatException var12) {
- break;
- }
- } else {
- // Draw text if the character is not a special character
- final MapFont.CharacterSprite sprite = font.getChar(text.charAt(currentIndex));
-
- for (int row = 0; row < font.getHeight(); ++row) {
- for (int col = 0; col < sprite.getWidth(); ++col) {
- if (sprite.get(row, col)) {
- for (int eX = 0; eX < size; eX++) {
- for (int eY = 0; eY < size; eY++) {
- this.setPixel(x + (size * col) + (eX), y + (size * row) + (eY), color);
- }
+ final int cp = codePoints[currentIndex];
+ if (cp == '\n') {
+ // Increment y if the char is a line separator
+ x = xStart;
+ y += font.getHeight() + 1;
+ } else {
+ // Draw text if the character is not a special character
+ final Sprite sprite = font.get(cp);
+ for (int row = 0; row < font.getHeight(); ++row) {
+ for (int col = 0; col < sprite.getWidth(); ++col) {
+ if (sprite.get(row, col)) {
+ for (int eX = 0; eX < size; eX++) {
+ for (int eY = 0; eY < size; eY++) {
+ this.setPixel(x + (size * col) + (eX), y + (size * row) + (eY), color);
}
}
}
}
-
- // Increment x
- x += (sprite.getWidth() + 1) * size;
}
- ++currentIndex;
+ // Increment x
+ x += (sprite.getWidth() + 1) * size;
}
- throw new IllegalArgumentException("Text contains unterminated color string");
+ ++currentIndex;
}
}
diff --git a/plugin/pom.xml b/plugin/pom.xml
index e60757d..e41ac61 100644
--- a/plugin/pom.xml
+++ b/plugin/pom.xml
@@ -5,7 +5,7 @@
parent
dev.cerus.maps
- 3.3.0
+ 3.4.0
4.0.0
diff --git a/plugin/src/main/java/dev/cerus/maps/plugin/MapsPlugin.java b/plugin/src/main/java/dev/cerus/maps/plugin/MapsPlugin.java
index 6776a17..3dfc20d 100644
--- a/plugin/src/main/java/dev/cerus/maps/plugin/MapsPlugin.java
+++ b/plugin/src/main/java/dev/cerus/maps/plugin/MapsPlugin.java
@@ -1,6 +1,8 @@
package dev.cerus.maps.plugin;
import co.aikar.commands.BukkitCommandManager;
+import dev.cerus.maps.api.colormap.ColorMaps;
+import dev.cerus.maps.api.font.MapFont;
import dev.cerus.maps.api.version.VersionAdapter;
import dev.cerus.maps.plugin.command.MapsCommand;
import dev.cerus.maps.plugin.dev.DevContext;
@@ -67,6 +69,9 @@ public void onEnable() {
commandManager.registerCommand(new MapsDevCommand());
this.getServer().getPluginManager().registerEvents(new DevListener(this), this);
}
+
+ // Force classes to initialize now
+ this.doNothing(ColorMaps.class, MapFont.class);
}
@Override
@@ -77,4 +82,9 @@ public void onDisable() {
this.saveConfig();
}
+ private void doNothing(final Class>... unused) {
+ // This method does nothing. Its only purpose is to trick the JVM into
+ // initializing the passed classes.
+ }
+
}
diff --git a/plugin/src/main/java/dev/cerus/maps/plugin/dev/MapsDevCommand.java b/plugin/src/main/java/dev/cerus/maps/plugin/dev/MapsDevCommand.java
index 00d1e51..89cde07 100644
--- a/plugin/src/main/java/dev/cerus/maps/plugin/dev/MapsDevCommand.java
+++ b/plugin/src/main/java/dev/cerus/maps/plugin/dev/MapsDevCommand.java
@@ -6,11 +6,20 @@
import co.aikar.commands.annotation.Conditions;
import co.aikar.commands.annotation.Dependency;
import co.aikar.commands.annotation.Subcommand;
+import dev.cerus.maps.api.ClientsideMap;
import dev.cerus.maps.api.MapScreen;
import dev.cerus.maps.api.Marker;
+import dev.cerus.maps.api.font.FontConverter;
+import dev.cerus.maps.api.font.MapFont;
+import dev.cerus.maps.api.graphics.ColorCache;
+import dev.cerus.maps.api.graphics.MapGraphics;
import dev.cerus.maps.api.version.VersionAdapter;
+import dev.cerus.maps.plugin.MapsPlugin;
import dev.cerus.maps.plugin.map.MapScreenRegistry;
import dev.cerus.maps.raycast.RayCastUtil;
+import java.awt.Font;
+import java.io.File;
+import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executors;
@@ -19,6 +28,7 @@
import org.bukkit.Particle;
import org.bukkit.entity.Player;
import org.bukkit.map.MapCursor;
+import org.bukkit.plugin.java.JavaPlugin;
/**
* Special command for development operations, do not use in production
@@ -30,6 +40,43 @@ public class MapsDevCommand extends BaseCommand {
@Dependency
private VersionAdapter versionAdapter;
+ @Subcommand("fonttest")
+ public void handleFontTest(final Player player, final int screenId, final String fontName, final int fontSize, String text) {
+ final MapScreen screen = MapScreenRegistry.getScreen(screenId);
+ if (screen == null || !fontName.matches("[A-Za-z0-9]+")) {
+ return;
+ }
+
+ final MapFont mapFont;
+ try {
+ final Font font = Font.createFont(Font.TRUETYPE_FONT, new File(JavaPlugin.getPlugin(MapsPlugin.class).getDataFolder(), fontName + ".ttf"))
+ .deriveFont((float) fontSize);
+ mapFont = FontConverter.convert(font, FontConverter.ASCII + FontConverter.UMLAUTS + FontConverter.SHARP_S + " ");
+ } catch (final Throwable t) {
+ t.printStackTrace();
+ player.sendMessage("Error " + t.getMessage());
+ return;
+ }
+
+ final MapGraphics graphics = screen.getGraphics();
+ graphics.fillComplete((byte) 0);
+ text = text.replace("\\n", "\n");
+ final int width = Arrays.stream(text.split("\n")).mapToInt(mapFont::getWidth).max().orElse(0);
+ final int height = Arrays.stream(text.split("\n")).mapToInt(mapFont::getHeight).sum();
+ graphics.drawText(
+ (graphics.getWidth() / 2) - (width / 2),
+ (graphics.getHeight() / 2) - (height / 2),
+ text,
+ mapFont,
+ ColorCache.rgbToMap(255, 255, 255),
+ 1
+ );
+ screen.sendMaps(true);
+
+ player.sendMessage("W: " + width);
+ player.sendMessage("H: " + height);
+ }
+
@Subcommand("raytesttask")
public void handleRayTestTask(final Player player) {
final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
diff --git a/pom.xml b/pom.xml
index 0b14ba9..7b877c2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
dev.cerus.maps
parent
pom
- 3.3.0
+ 3.4.0
common
bukkit-16_R3