diff --git a/README.md b/README.md index 484faac..3f1b9c3 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ See [FAQ](#FAQ) dev.cerus.maps common - 3.3.0 + 3.4.0 provided @@ -59,7 +59,7 @@ See [FAQ](#FAQ) dev.cerus.maps plugin - 3.3.0 + 3.4.0 provided diff --git a/bukkit-16_R3/pom.xml b/bukkit-16_R3/pom.xml index 320aefb..4209b5a 100644 --- a/bukkit-16_R3/pom.xml +++ b/bukkit-16_R3/pom.xml @@ -5,7 +5,7 @@ parent dev.cerus.maps - 3.3.0 + 3.4.0 4.0.0 diff --git a/bukkit-17_R1/pom.xml b/bukkit-17_R1/pom.xml index e315653..0a8bcbe 100644 --- a/bukkit-17_R1/pom.xml +++ b/bukkit-17_R1/pom.xml @@ -5,7 +5,7 @@ parent dev.cerus.maps - 3.3.0 + 3.4.0 4.0.0 diff --git a/bukkit-18_R1/pom.xml b/bukkit-18_R1/pom.xml index d9925b1..7d1d421 100644 --- a/bukkit-18_R1/pom.xml +++ b/bukkit-18_R1/pom.xml @@ -5,7 +5,7 @@ parent dev.cerus.maps - 3.3.0 + 3.4.0 4.0.0 diff --git a/bukkit-18_R2/pom.xml b/bukkit-18_R2/pom.xml index 6ab699d..42a29e4 100644 --- a/bukkit-18_R2/pom.xml +++ b/bukkit-18_R2/pom.xml @@ -5,7 +5,7 @@ parent dev.cerus.maps - 3.3.0 + 3.4.0 4.0.0 diff --git a/bukkit-19_R1/pom.xml b/bukkit-19_R1/pom.xml index 68f887c..fbbad6e 100644 --- a/bukkit-19_R1/pom.xml +++ b/bukkit-19_R1/pom.xml @@ -5,7 +5,7 @@ parent dev.cerus.maps - 3.3.0 + 3.4.0 4.0.0 diff --git a/common/pom.xml b/common/pom.xml index 635dd86..220d78c 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -5,7 +5,7 @@ parent dev.cerus.maps - 3.3.0 + 3.4.0 4.0.0 diff --git a/common/src/main/java/dev/cerus/maps/api/font/FontConverter.java b/common/src/main/java/dev/cerus/maps/api/font/FontConverter.java index 6a0e843..f9700e2 100644 --- a/common/src/main/java/dev/cerus/maps/api/font/FontConverter.java +++ b/common/src/main/java/dev/cerus/maps/api/font/FontConverter.java @@ -7,66 +7,113 @@ import java.awt.image.BufferedImage; import java.util.List; import java.util.stream.IntStream; -import org.bukkit.map.MapFont; +/** + * Converts regular Java fonts into MapFonts + */ public class FontConverter { - public static final List ASCII = List.copyOf(IntStream.range(26, 127) + /** + * All Ascii chars + */ + public static final String ASCII = IntStream.range(26, 127) .mapToObj(v -> (char) v) - .toList()); - public static final List UNICODE = List.copyOf(IntStream.range(0, Character.MAX_VALUE) - .filter(value -> Character.isDefined((char) value)) - .mapToObj(v -> (char) v) - .toList()); + .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append) + .toString(); + + /** + * All German umlauts + */ + public static final String UMLAUTS = "ÄäÖöÜü"; + + /** + * Sharp s ("Eszett", "scharfes S") + */ + public static final String SHARP_S = "ẞß"; private FontConverter() { throw new UnsupportedOperationException(); } - public static MapFont convert(final Font font, final List charsToCheck) { - final List 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