refactor: implemented new glyphs library
This commit is contained in:
parent
f09bb8376d
commit
4299b8efa6
@ -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'
|
||||
|
||||
@ -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);
|
||||
|
||||
18
resource-compiler/HELP.md
Normal file
18
resource-compiler/HELP.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Resource Compiler
|
||||
|
||||
Этот модуль отвечает за пакеты ресурсов игровых материалов.
|
||||
|
||||
## Возможности
|
||||
|
||||
Данный модуль поддерживает следующие возможности добавления своих материалов:
|
||||
|
||||
- Глифы - возможность заменить какой-нибудь символ на свою текстуру и использовать
|
||||
ее в тексте.
|
||||
|
||||
- Предметы со своей моделькой (WIP)
|
||||
|
||||
- Звуки (WIP)
|
||||
|
||||
|
||||
## Гайды
|
||||
|
||||
7
resource-compiler/build.gradle
Normal file
7
resource-compiler/build.gradle
Normal file
@ -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'
|
||||
}
|
||||
@ -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<String, Atlas.Builder>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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<Character> 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<Character> reservedChars() {
|
||||
var set = new HashSet<Character>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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<Entry> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить новую строку.
|
||||
*
|
||||
* <p> ВНИМАНИЕ: стоит учесть то, что не для всех типов вывода текста внутри игры поддерживается несколько строк
|
||||
* в выводимом тексте
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<Key, GlyphResourceHolder> glyphs = new HashMap<>();
|
||||
private final Map<String, Font.Builder> 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<String, Atlas.Builder> 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();
|
||||
}
|
||||
}
|
||||
@ -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) {}
|
||||
@ -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<String> alphabet = List.of(
|
||||
" АБВГДЕЖЗИК",
|
||||
"ЛМНОПРСТУФХЦЧШЩЪ",
|
||||
"ЫЬЭЮЯабвгдежзикл",
|
||||
"мнопрстуфхцчшщъы",
|
||||
"ьэюяйЙёЁ ",
|
||||
"₽!\"#$%&'()*+,-./",
|
||||
"0123456789: <=>?",
|
||||
"@ABCDEFGHIJKLMNO",
|
||||
"PQRSTUVWXYZ[\\]^_",
|
||||
"`abcdefghijklmno",
|
||||
"pqrstuvwxyz{|} ");
|
||||
private final Map<Character, Integer> characterIndexes = characterIndexes(); // original character => index
|
||||
private final int[] fontWidths = fontWidths(); // index => character width
|
||||
private final Map<EntryKey, char[]> fontMap = new HashMap<>();
|
||||
private final Font.Builder font = Font.font().key(Key.key("msb3", "default_font"));
|
||||
|
||||
private Map<Character, Integer> characterIndexes() {
|
||||
var map = new HashMap<Character, Integer>();
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Компиляция шрифта под определенный размер шрифта и его смещение по вертикали
|
||||
*
|
||||
* <p> Смещение по вертикали можно использовать в качестве реализации многострочного текста там, где
|
||||
* это не поддерживается ванильным клиентом игры.
|
||||
* @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<String>();
|
||||
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.
|
||||
*
|
||||
* <p> Использовать как многострочный текст в тех местах, где это не поддерживается ванильным клиентом игры
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package ru.dragonestia.msb3.resource.glyph;
|
||||
|
||||
public enum Position {
|
||||
RELATIVE,
|
||||
ABSOLUTE,
|
||||
}
|
||||
@ -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<Integer, Character> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить строку со смещенным курсором текста по горизонтали. Если смещение положительное - смещение идет вправо.
|
||||
* Если же наоборот, отрицательное - в левую сторону.
|
||||
*
|
||||
* <p> В основном используется, чтобы смещать глифы по горизонтали.
|
||||
* @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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить компонент со смещенным курсором текста по горизонтали. Если смещение положительное - смещение идет вправо.
|
||||
* Если же наоборот, отрицательное - в левую сторону.
|
||||
*
|
||||
* <p> В основном используется, чтобы смещать глифы по горизонтали.
|
||||
* @param offset Смещение курсора текста
|
||||
* @return Компонент со смещением
|
||||
* @throws IllegalStateException Взят слишком большой отступ
|
||||
*/
|
||||
public Component get(int offset) throws IllegalArgumentException {
|
||||
return Component.text(getAsString(offset));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@ -1,3 +1,4 @@
|
||||
rootProject.name = 'msb3'
|
||||
|
||||
include 'api', 'editor'
|
||||
include 'resource-compiler', 'api', 'editor'
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user