diff --git a/api/build.gradle b/api/build.gradle index e478258..ba05724 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -1,4 +1,6 @@ dependencies { + api project(":resource-compiler") + api 'net.minestom:minestom-snapshots:bb7acc2e77' api 'org.slf4j:slf4j-api:2.0.16' @@ -6,12 +8,6 @@ dependencies { api 'org.apache.logging.log4j:log4j-api:2.24.0' api 'org.apache.logging.log4j:log4j-core:2.19.0' - api 'net.kyori:adventure-api:4.17.0' - api 'net.kyori:adventure-text-minimessage:4.17.0' - api 'team.unnamed:creative-api:1.7.3' - api 'team.unnamed:creative-serializer-minecraft:1.7.3' - api 'team.unnamed:creative-server:1.7.3' - api 'org.sql2o:sql2o:1.8.0' api 'com.clickhouse:clickhouse-jdbc:0.7.1' api 'org.lz4:lz4-java:1.8.0' diff --git a/api/src/main/java/ru/dragonestia/msb3/api/glyph/glyph/image/ImageGlyph.java b/api/src/main/java/ru/dragonestia/msb3/api/glyph/glyph/image/ImageGlyph.java index 6f071f4..d224887 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/glyph/glyph/image/ImageGlyph.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/glyph/glyph/image/ImageGlyph.java @@ -17,7 +17,6 @@ public interface ImageGlyph extends AppendableGlyph, ResourceProducer { return new ImageGlyphImpl(key, texture, properties); } - @Deprecated(forRemoval = true) static @NotNull ImageGlyph of(@NotNull Texture texture, @NotNull TextureProperties properties) { return of(Glyph.DEFAULT_FONT_KEY, texture, properties); diff --git a/resource-compiler/HELP.md b/resource-compiler/HELP.md new file mode 100644 index 0000000..52601cc --- /dev/null +++ b/resource-compiler/HELP.md @@ -0,0 +1,18 @@ +# Resource Compiler + +Этот модуль отвечает за пакеты ресурсов игровых материалов. + +## Возможности + +Данный модуль поддерживает следующие возможности добавления своих материалов: + +- Глифы - возможность заменить какой-нибудь символ на свою текстуру и использовать +ее в тексте. + +- Предметы со своей моделькой (WIP) + +- Звуки (WIP) + + +## Гайды + diff --git a/resource-compiler/build.gradle b/resource-compiler/build.gradle new file mode 100644 index 0000000..febea5b --- /dev/null +++ b/resource-compiler/build.gradle @@ -0,0 +1,7 @@ +dependencies { + api 'net.kyori:adventure-api:4.17.0' + api 'net.kyori:adventure-text-minimessage:4.17.0' + api 'team.unnamed:creative-api:1.7.3' + api 'team.unnamed:creative-serializer-minecraft:1.7.3' + api 'team.unnamed:creative-server:1.7.3' +} diff --git a/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/Resources.java b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/Resources.java new file mode 100644 index 0000000..67ca76a --- /dev/null +++ b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/Resources.java @@ -0,0 +1,91 @@ +package ru.dragonestia.msb3.resource; + +import lombok.experimental.UtilityClass; +import net.kyori.adventure.key.Key; +import ru.dragonestia.msb3.resource.glyph.GlyphImage; +import ru.dragonestia.msb3.resource.glyph.GlyphRegistry; +import ru.dragonestia.msb3.resource.glyph.MinecraftFont; +import team.unnamed.creative.ResourcePack; +import team.unnamed.creative.atlas.Atlas; +import team.unnamed.creative.base.Writable; +import team.unnamed.creative.font.Font; + +import java.util.HashMap; + +/** + * Модуль ресурс-пака. Через него регистрируются, а также получаются все материалы + */ +@UtilityClass +public final class Resources { + + private final ResourcePack resourcePack = init(); + private final Font.Builder defaultFont = Font.font(); + private final GlyphRegistry glyphs = new GlyphRegistry(resourcePack, defaultFont); + + private boolean compiled = false; + + private ResourcePack init() { + var resourcePack = ResourcePack.resourcePack(); + resourcePack.packMeta(34, "Dragonestia MSB3 - Resource pack"); + resourcePack.icon(Writable.resource(Resources.class.getClassLoader(), "logo.png")); + resourcePack.unknownFile("credits.txt", Writable.stringUtf8("dragonestia.ru")); + return resourcePack; + } + + private void generateAtlases() { + var atlases = new HashMap(); + + glyphs.compileAtlases(atlases); + + for (var atlas: atlases.values()) { + resourcePack.atlas(atlas.build()); + } + } + + /** + * Компилирует все материалы в ресурс-пак + * @throws IllegalStateException Выбрасывается, если ресурс-пак уже скомпилирован + */ + public ResourcePack compile() throws IllegalStateException { + if (compiled) { + throw new IllegalStateException("Already compiled"); + } + + compiled = true; + + for (int line = 0; line < 4; line++) { // precompile 4 lines + MinecraftFont.compileFontProvider(8, 8 - line * 9); + } + + glyphs.compile(); + resourcePack.font(defaultFont.build()); + generateAtlases(); + return resourcePack; + } + + /** + * Регистрация глифа + * @param key Идентификатор глифа + * @param writableTexture Объект с текстурой, предоставляемый ресурс-паку + * @param height Высота глифа + * @param ascent Смещение глифа по высоте + * @throws IllegalStateException Глиф был создан после компиляции ресурс-пака + */ + public void createGlyph(Key key, Writable writableTexture, int height, int ascent) throws IllegalStateException { + if (compiled) { + throw new IllegalStateException("Resources already compiled"); + } + + glyphs.register(key, writableTexture, height, ascent); + } + + /** + * Получение глифа по идентификатору + * @param key Идентификатор глифа + * @return Объект глифа + * @throws NullPointerException Глиф с данным идентификатором не найден + */ + public GlyphImage glyph(Key key) throws NullPointerException { + return glyphs.getGlyph(key); + } +} diff --git a/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphCharacterFactory.java b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphCharacterFactory.java new file mode 100644 index 0000000..fcca276 --- /dev/null +++ b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphCharacterFactory.java @@ -0,0 +1,78 @@ +package ru.dragonestia.msb3.resource.glyph; + +import lombok.experimental.UtilityClass; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Фабрика свободных символов для глифов и шрифтов + */ +@UtilityClass +public final class GlyphCharacterFactory { + + private final char start = 0xA201; + private final Set reservedChars = reservedChars(); + private final int countCharRange = Character.MAX_VALUE + 1 - reservedChars.size(); + + private char currentChar = start; + private int usedChars = 0; + + /** + * Получить первый попавшийся доступный символ + * @return Свободный символ, который можно использовать в качестве глифа + * @throws IllegalStateException Выбрасывается, если будет исчерпан максимальный лимит по символам + */ + public char takeFreeCharacter() throws IllegalStateException { + do { + if (currentChar == Character.MAX_VALUE) { + throw new IllegalStateException("Characters range exceeded (used: %s/%s)".formatted(countUsedChars(), countCharRange())); + } + + currentChar++; + } while (reservedChars.contains(currentChar)); + + usedChars++; + return currentChar; + } + + /** + * Получить количество символов, которое было использовано грифами + * @return Количество глифов зарегистрировано + */ + public int countUsedChars() { + return usedChars; + } + + /** + * Получить допустимое возможное количество глифов, которое в принципе можно зарегистрировать + * @return Лимит глифов + */ + public int countCharRange() { + return countCharRange; + } + + private Set reservedChars() { + var set = new HashSet(); + for (char c = 'a'; c <= 'z'; c++) set.add(c); + for (char c = 'A'; c <= 'Z'; c++) set.add(c); + for (char c = 'а'; c <= 'я'; c++) set.add(c); + for (char c = 'А'; c <= 'Я'; c++) set.add(c); + for (char c = '0'; c <= '9'; c++) set.add(c); + + set.addAll(Arrays.asList( + '!', '?', ':', '$', + ';', '#', '@', '%', + '^', '&', '*', '(', + ')', '_', '-', '+', + '/', '\\', '№', '"', + '\'', '{', '}', '[', + ']', '~', '`', '<', '>', + ',', '.', '|', '\n', '\r', + '\b', '\f', '\t', ' ', + 'ё', 'Ё', '=') + ); + return set; + } +} diff --git a/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphComponent.java b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphComponent.java new file mode 100644 index 0000000..930447c --- /dev/null +++ b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphComponent.java @@ -0,0 +1,12 @@ +package ru.dragonestia.msb3.resource.glyph; + +import net.kyori.adventure.text.Component; + +public interface GlyphComponent { + + String content(); + + int width(); + + Component component(); +} diff --git a/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphComponentBuilder.java b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphComponentBuilder.java new file mode 100644 index 0000000..3b487b8 --- /dev/null +++ b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphComponentBuilder.java @@ -0,0 +1,137 @@ +package ru.dragonestia.msb3.resource.glyph; + +import net.kyori.adventure.text.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * Компонент компоновки глифов. Создан для более простого позиционирования элементов, состоящих из глифов. + * Если необходимо размещать глифы с отступами, относительно друг-друга, то этот класс очень + * хорошо подойдет под данную задачу. + * Например, можно использовать это как компоновщик элементов для UI. + */ +public class GlyphComponentBuilder { + + private static final Entry NEW_LINE = new Entry(Position.ABSOLUTE, 0, new Text("\n", 0)); + + private final List entries = new ArrayList<>(); + + /** + * Добавить компонент после предыдущего компонента + * @param component Компонент с глифом + * @return Текущий объект билдера + */ + public GlyphComponentBuilder append(GlyphComponent component) { + return append(Position.RELATIVE, 0, component); + } + + /** + * Добавить компонент с абсолютным позиционированием + * @param absolutePosition Смещение по горизонтали. Отрицательные числа - влево. Положительные - вправо. + * @param component Компонент с глифом + * @return Текущий объект билдера + */ + public GlyphComponentBuilder append(int absolutePosition, GlyphComponent component) { + return append(Position.ABSOLUTE, absolutePosition, component); + } + + /** + * Добавить компонент с выборочным позиционированием + * @param position Тип позиционирования + * @param offset Смещение по горизонтали. Отрицательные числа - влево. Положительные - вправо. + * @param component Компонент с глифом + * @return Текущий объект билдера + */ + public GlyphComponentBuilder append(Position position, int offset, GlyphComponent component) { + entries.add(new Entry(position, offset, component)); + return this; + } + + /** + * Добавить новую строку. + * + *

ВНИМАНИЕ: стоит учесть то, что не для всех типов вывода текста внутри игры поддерживается несколько строк + * в выводимом тексте + * @return Текущий объект билдера + */ + public GlyphComponentBuilder appendNewLine() { + entries.add(NEW_LINE); + return this; + } + + /** + * Собрать все компоненты глифов в один текстовый компонент + * @return Текстовый компонент + */ + public Component build() { + return build(false); + } + + /** + * Собрать все компоненты глифов в один текстовый компонент + * @param resetPosition Возвращать ли смещение на начало? + * @return Текстовый компонент + */ + public Component build(boolean resetPosition) { + var builder = Component.text(); + + int offset = 0; + + for (var entry: entries) { + Component component = entry.glyphComponent().component(); + var val = entry.offset(); + switch (entry.position()) { + case ABSOLUTE -> { + builder.append(Spacing.get(val - offset)); + offset = val; + } + + case RELATIVE -> { + if (val == 0) { + builder.append(Spacing.get(val)); + } + offset += val; + } + } + offset += entry.glyphComponent().width(); + builder.append(component); + } + + if (resetPosition) { + builder.append(Spacing.get(-offset)); + } + + return builder.build(); + } + + private record Entry(Position position, int offset, GlyphComponent glyphComponent) {} + + private static class Text implements GlyphComponent { + + private final String content; + private final int width; + private final Component component; + + private Text(String content, int wight) { + this.content = content; + this.width = wight; + component = Component.text(content); + } + + @Override + public String content() { + return content; + } + + @Override + public int width() { + return width; + } + + @Override + public Component component() { + return component; + } + } +} diff --git a/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphImage.java b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphImage.java new file mode 100644 index 0000000..746e258 --- /dev/null +++ b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphImage.java @@ -0,0 +1,70 @@ +package ru.dragonestia.msb3.resource.glyph; + +import net.kyori.adventure.text.Component; + +/** + * Глиф - какой-то юникодовский символ. В игре может иметь кастомную текстуру + */ +public final class GlyphImage implements GlyphComponent { + + private final String content; + private final int height; + private final int width; + private final int ascent; + private final Component component; + + public GlyphImage(String content, int height, int width, int ascent) { + if (ascent > height) { + throw new IllegalArgumentException("ascent(%s) > height(%s)".formatted(ascent, height)); + } + + this.content = content; + this.height = height; + this.width = width; + this.ascent = ascent; + this.component = Component.text(content); + } + + /** + * Один или несколько символов, которые связаны с глифом + * @return Строка с символами + */ + @Override + public String content() { + return content; + } + + /** + * Высота текстуры символа. Текстура растягивается и подгоняется под ее высоту. + * @return Высота символа + */ + public int height() { + return height; + } + + /** + * Ширина текстуры символа. Определяется автоматически при компиляции ресурс-пака + * @return Ширина символа + */ + @Override + public int width() { + return width; + } + + /** + * Вертикальное смещение символа. Всегда меньше, либо равен высоте символа. + * @return Позиция смещения + */ + public int ascent() { + return ascent; + } + + /** + * Получение текстового компонента с глифом + * @return Текстовый компонент + */ + @Override + public Component component() { + return component; + } +} diff --git a/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphRegistry.java b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphRegistry.java new file mode 100644 index 0000000..07580d7 --- /dev/null +++ b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphRegistry.java @@ -0,0 +1,100 @@ +package ru.dragonestia.msb3.resource.glyph; + +import net.kyori.adventure.key.Key; +import ru.dragonestia.msb3.resource.utils.ImageUtils; +import team.unnamed.creative.ResourcePack; +import team.unnamed.creative.atlas.Atlas; +import team.unnamed.creative.atlas.AtlasSource; +import team.unnamed.creative.base.Writable; +import team.unnamed.creative.font.Font; +import team.unnamed.creative.font.FontProvider; +import team.unnamed.creative.texture.Texture; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class GlyphRegistry { + + private final ResourcePack resourcePack; + private final Font.Builder defaultFont; + + private final Map glyphs = new HashMap<>(); + private final Map glyphFonts = new HashMap<>(); + + public GlyphRegistry(ResourcePack resourcePack, Font.Builder defaultFont) { + this.resourcePack = resourcePack; + this.defaultFont = defaultFont; + } + + public void compile() { + defaultFont.key(Font.MINECRAFT_DEFAULT); + + defaultFont.addProvider(FontProvider.reference(Spacing.compile(resourcePack))); + + for (var fontBuilder: glyphFonts.values()) { + var font = fontBuilder.build(); + + resourcePack.font(font); + defaultFont.addProvider(FontProvider.reference(font)); + } + + defaultFont.addProvider(FontProvider.reference(MinecraftFont.compile(resourcePack))); + } + + public void compileAtlases(Map atlases) { + for (var holder: glyphs.values()) { + var key = holder.texture().key(); + + var atlas = atlases.computeIfAbsent(key.namespace(), identifier -> { + var obj = Atlas.atlas(); + obj.key(Key.key(identifier, "gui")); + return obj; + }); + + atlas.addSource(AtlasSource.single(Key.key(key.namespace(), key.value().replace(".png", "")))); + } + } + + public void register(Key key, Writable writableTexture, int height, int ascent) { + var font = glyphFonts.computeIfAbsent(key.namespace(), namespace -> { + var obj = Font.font(); + obj.key(Key.key(namespace, "glyphs_font")); + return obj; + }); + + var character = GlyphCharacterFactory.takeFreeCharacter(); + var image = ImageUtils.imageFromWritable(writableTexture); + // TODO: split image when resolution biggest than 256x256 on any axis + + var texture = Texture.texture() + .key(Key.key(key.namespace(), "glyphs/" + key.value() + ".png")) + .data(writableTexture) + .build(); + + resourcePack.texture(texture); + + var provider = FontProvider.bitMap() + .file(Key.key(texture.key().namespace(), texture.key().value())) + .height(height) + .ascent(ascent) + .characters(Character.toString(character)) + .build(); + + font.addProvider(provider); + + int wight = image.getWidth(); + if (height != image.getHeight()) { + wight = (int) ((image.getHeight() / (double) height) * wight); + } + + var glyph = new GlyphImage(Character.toString(character), height, wight, ascent); + var holder = new GlyphResourceHolder(key, glyph, texture); + + glyphs.put(key, holder); + } + + public GlyphImage getGlyph(Key key) { + return Objects.requireNonNull(glyphs.get(key), "Unknown glyph identifier: %s".formatted(key.asString())).glyph(); + } +} diff --git a/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphResourceHolder.java b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphResourceHolder.java new file mode 100644 index 0000000..5e143c6 --- /dev/null +++ b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/GlyphResourceHolder.java @@ -0,0 +1,6 @@ +package ru.dragonestia.msb3.resource.glyph; + +import net.kyori.adventure.key.Key; +import team.unnamed.creative.texture.Texture; + +public record GlyphResourceHolder(Key key, GlyphImage glyph, Texture texture) {} diff --git a/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/MinecraftFont.java b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/MinecraftFont.java new file mode 100644 index 0000000..0030878 --- /dev/null +++ b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/MinecraftFont.java @@ -0,0 +1,221 @@ +package ru.dragonestia.msb3.resource.glyph; + +import lombok.experimental.UtilityClass; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import ru.dragonestia.msb3.resource.utils.ImageUtils; +import team.unnamed.creative.ResourcePack; +import team.unnamed.creative.base.Writable; +import team.unnamed.creative.font.Font; +import team.unnamed.creative.font.FontProvider; +import team.unnamed.creative.texture.Texture; + +import java.security.InvalidParameterException; +import java.util.*; + +@UtilityClass +public final class MinecraftFont { + + private final Texture fontTexture = Texture.texture() + .key(Key.key("msb3", "default_font.png")) + .data(Writable.resource(MinecraftFont.class.getClassLoader(), "glyphs/defaults/minecraft_font.png")) + .build(); + private final List alphabet = List.of( + " АБВГДЕЖЗИК", + "ЛМНОПРСТУФХЦЧШЩЪ", + "ЫЬЭЮЯабвгдежзикл", + "мнопрстуфхцчшщъы", + "ьэюяйЙёЁ ", + "₽!\"#$%&'()*+,-./", + "0123456789: <=>?", + "@ABCDEFGHIJKLMNO", + "PQRSTUVWXYZ[\\]^_", + "`abcdefghijklmno", + "pqrstuvwxyz{|} "); + private final Map characterIndexes = characterIndexes(); // original character => index + private final int[] fontWidths = fontWidths(); // index => character width + private final Map fontMap = new HashMap<>(); + private final Font.Builder font = Font.font().key(Key.key("msb3", "default_font")); + + private Map characterIndexes() { + var map = new HashMap(); + int index = 0; + for (var line: alphabet) { + for (var character: line.toCharArray()) { + map.put(character, index++); + } + } + return map; + } + + private int[] fontWidths() { + var result = new int[String.join("", alphabet).length()]; + var image = ImageUtils.imageFromWritable(fontTexture.data()); + int charWidth = image.getWidth() / alphabet.getFirst().length(); + int charHeight = image.getHeight() / alphabet.size(); + + for (int y = 0; y < alphabet.size(); y++) { + var line = alphabet.get(y); + + for (int x = 0; x < line.length(); x++) { + var character = line.charAt(x); + var index = characterIndexes.get(character); + + int width = 0; + + int startX = x * charWidth; + int startY = y * charHeight; + for (int dx = 0; dx < charWidth; dx++) { + for (int dy = 0; dy < charHeight; dy++) { + var color = image.getRGB(startX + dx, startY + dy); + var alpha = color >> 24; + + if (alpha != 0) { + width = dx + 1; + } + } + } + + result[index] = width; + } + } + + return result; + } + + Font compile(ResourcePack resourcePack) { + resourcePack.texture(fontTexture); + + var compiledFont = font.build(); + resourcePack.font(compiledFont); + return compiledFont; + } + + /** + * Компиляция шрифта под определенный размер шрифта и его смещение по вертикали + * + *

Смещение по вертикали можно использовать в качестве реализации многострочного текста там, где + * это не поддерживается ванильным клиентом игры. + * @param height Высота символа + * @param ascent Смещение по вертикали. Не может быть больше высоты символа + * @throws InvalidParameterException Параметр ascent > height + */ + public void compileFontProvider(int height, int ascent) throws InvalidParameterException { + if (ascent > height) { + throw new InvalidParameterException("ascent(%s) > height(%s)".formatted(ascent, height)); + } + + var entryKey = new EntryKey(height, ascent); + if (fontMap.containsKey(entryKey)) return; + + var newAlphabet = new ArrayList(); + for (var line: alphabet) { + var sb = new StringBuilder(); + + for (int i = 0; i < line.length(); i++) { + var glyph = GlyphCharacterFactory.takeFreeCharacter(); + sb.append(glyph); + } + + newAlphabet.add(sb.toString()); + } + fontMap.put(entryKey, String.join("", newAlphabet).toCharArray()); + + + var provider = FontProvider.bitMap(); + provider.height(height); + provider.ascent(ascent); + provider.file(fontTexture.key()); + provider.characters(newAlphabet); + + font.addProvider(provider.build()); + } + + /** + * Перевести текст в глифы. Высота символа у шрифта равна 8, междустрочный отступ 1. + * + *

Использовать как многострочный текст в тех местах, где это не поддерживается ванильным клиентом игры + * @param lineNumber Номер строки + * @param input Вводимый текст, который необходимо преобразовать + * @return Преобразованный текст в виде компонента глифа + * @throws NullPointerException Маппинги для данного height и ascent не скомпилированы + */ + public Text translateByLineNumber(int lineNumber, String input) throws NullPointerException { + return translate(8 - lineNumber * (8 /* font size */ + 1 /* line indent */), input); + } + + /** + * Перевести текст в глифы. Высота шрифта = 8 + * @param ascent Смещение по вертикали. Можно использовать как многострочный текст. Всегда меньше, либо равно 8 + * @param input Вводимый текст, который необходимо преобразовать + * @return Преобразованный текст в виде компонента глифа + * @throws NullPointerException Маппинги для данного height и ascent не скомпилированы + */ + public Text translate(int ascent, String input) throws NullPointerException { + return translate(8, ascent, input); + } + + /** + * Перевести текст в глифы + * @param height Высота символа + * @param ascent Смещение по вертикали. Можно использовать как многострочный текст. Всегда меньше, либо равен height + * @param input Вводимый текст, который необходимо преобразовать + * @return Преобразованный текст в виде компонента глифа + * @throws NullPointerException Маппинги для данного height и ascent не скомпилированы + */ + public Text translate(int height, int ascent, String input) throws NullPointerException { + var entryKey = new EntryKey(height, ascent); + var mapping = Objects.requireNonNull( + fontMap.get(entryKey), + "Not registered mapping for height=%s ascent=%s".formatted(height, ascent)); + + int textWidth = 0; + var sb = new StringBuilder(); + for (var character: input.toCharArray()) { + if (character == ' ') { + textWidth += height; + sb.append(Spacing.getAsString(height)); + continue; + } + + var index = characterIndexes.getOrDefault(character, characterIndexes.get('?')); + var glyphChar = mapping[index]; + var charWidth = (int) ((fontWidths[index] / 8f) * height) + 1; + + textWidth += charWidth; + sb.append(glyphChar); + } + + return new Text(sb.toString(), textWidth); + } + + private record EntryKey(int height, int ascent) {} + + public static class Text implements GlyphComponent { + + private final String content; + private final int length; + private Component component = null; + + private Text(String content, int length) { + this.content = content; + this.length = length; + } + + @Override + public String content() { + return content; + } + + @Override + public int width() { + return length; + } + + @Override + public Component component() { + if (component == null) component = Component.text(content); + return component; + } + } +} diff --git a/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/Position.java b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/Position.java new file mode 100644 index 0000000..e45ee18 --- /dev/null +++ b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/Position.java @@ -0,0 +1,6 @@ +package ru.dragonestia.msb3.resource.glyph; + +public enum Position { + RELATIVE, + ABSOLUTE, +} diff --git a/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/Spacing.java b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/Spacing.java new file mode 100644 index 0000000..99c840b --- /dev/null +++ b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/glyph/Spacing.java @@ -0,0 +1,87 @@ +package ru.dragonestia.msb3.resource.glyph; + +import lombok.experimental.UtilityClass; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import team.unnamed.creative.ResourcePack; +import team.unnamed.creative.font.Font; +import team.unnamed.creative.font.FontProvider; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@UtilityClass +public class Spacing { + + private final int depth = 13; + + private final Map map = new HashMap<>(); + + Font compile(ResourcePack resourcePack) { + var builder = Font.font(); + builder.key(Key.key("msb3", "font_spacing")); + + var provider = FontProvider.space(); + + for (int i = 0; i <= depth; i++) { + for (var m: List.of(-1, 1)) { + var offset = m * (1 << i); + var character = GlyphCharacterFactory.takeFreeCharacter(); + + map.put(offset, character); + provider.advance(character, offset); + } + } + + builder.addProvider(provider.build()); + + var font = builder.build(); + resourcePack.font(font); + return font; + } + + /** + * Получить строку со смещенным курсором текста по горизонтали. Если смещение положительное - смещение идет вправо. + * Если же наоборот, отрицательное - в левую сторону. + * + *

В основном используется, чтобы смещать глифы по горизонтали. + * @param offset Смещение курсора текста + * @return Строка со смещением + * @throws IllegalStateException Взят слишком большой отступ + */ + public String getAsString(int offset) throws IllegalArgumentException { + int currentDepth = 0; + int val = Math.abs(offset); + var sb = new StringBuilder(); + var sign = offset > 0 ? 1 : -1; + while (val > 0) { + if ((val & 1) != 0) { + var index = (1 << currentDepth) * sign; + var spacingChar = map.get(index); + + if (spacingChar == null) { + throw new IllegalArgumentException("Unsupported spacing depth: %s".formatted(currentDepth)); + } + + sb.append(spacingChar); + } + val = val >> 1; + currentDepth++; + } + return sb.toString(); + } + + /** + * Получить компонент со смещенным курсором текста по горизонтали. Если смещение положительное - смещение идет вправо. + * Если же наоборот, отрицательное - в левую сторону. + * + *

В основном используется, чтобы смещать глифы по горизонтали. + * @param offset Смещение курсора текста + * @return Компонент со смещением + * @throws IllegalStateException Взят слишком большой отступ + */ + public Component get(int offset) throws IllegalArgumentException { + return Component.text(getAsString(offset)); + } +} diff --git a/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/utils/ImageUtils.java b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/utils/ImageUtils.java new file mode 100644 index 0000000..bd8a799 --- /dev/null +++ b/resource-compiler/src/main/java/ru/dragonestia/msb3/resource/utils/ImageUtils.java @@ -0,0 +1,21 @@ +package ru.dragonestia.msb3.resource.utils; + +import lombok.experimental.UtilityClass; +import team.unnamed.creative.base.Writable; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +@UtilityClass +public class ImageUtils { + + public BufferedImage imageFromWritable(Writable writable) { + try (var steam = new ByteArrayInputStream(writable.toByteArray())) { + return ImageIO.read(steam); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/api/src/main/resources/glyphs/defaults/minecraft_font.png b/resource-compiler/src/main/resources/glyphs/defaults/minecraft_font.png similarity index 100% rename from api/src/main/resources/glyphs/defaults/minecraft_font.png rename to resource-compiler/src/main/resources/glyphs/defaults/minecraft_font.png diff --git a/settings.gradle b/settings.gradle index 0a70c40..795b036 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,4 @@ rootProject.name = 'msb3' -include 'api', 'editor' +include 'resource-compiler', 'api', 'editor' +