refactor: implemented new glyphs library

This commit is contained in:
Andrey Terentev 2025-01-25 01:20:14 +07:00
parent f09bb8376d
commit 4299b8efa6
17 changed files with 858 additions and 8 deletions

View File

@ -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'

View File

@ -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
View File

@ -0,0 +1,18 @@
# Resource Compiler
Этот модуль отвечает за пакеты ресурсов игровых материалов.
## Возможности
Данный модуль поддерживает следующие возможности добавления своих материалов:
- Глифы - возможность заменить какой-нибудь символ на свою текстуру и использовать
ее в тексте.
- Предметы со своей моделькой (WIP)
- Звуки (WIP)
## Гайды

View 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'
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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) {}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,6 @@
package ru.dragonestia.msb3.resource.glyph;
public enum Position {
RELATIVE,
ABSOLUTE,
}

View File

@ -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));
}
}

View File

@ -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);
}
}
}

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,3 +1,4 @@
rootProject.name = 'msb3'
include 'api', 'editor'
include 'resource-compiler', 'api', 'editor'