feat: implemented MonologueRenderer

This commit is contained in:
Andrey Terentev 2024-10-24 22:14:28 +07:00
parent e8c588b2b6
commit 1b231463f3
8 changed files with 407 additions and 1 deletions

View File

@ -25,10 +25,14 @@ public class ServerBootstrap {
"==============================================", "==============================================",
}; };
@Getter private static ServerBootstrap instance;
private final MinecraftServer server; private final MinecraftServer server;
@Getter private final ResourcePackManager resourcePackManager; @Getter private final ResourcePackManager resourcePackManager;
public ServerBootstrap() { public ServerBootstrap() {
instance = this;
for (var line: LOGO) log.info(line); for (var line: LOGO) log.info(line);
server = MinecraftServer.init(); server = MinecraftServer.init();

View File

@ -52,7 +52,7 @@ public class DefaultGlyphResourcePack implements GlyphResourcePack {
@Override @Override
public <T extends ResourceProducer> @NotNull T get(@NotNull ResourceIdentifier<@NotNull T> id) throws IllegalArgumentException { public <T extends ResourceProducer> @NotNull T get(@NotNull ResourceIdentifier<@NotNull T> id) throws IllegalArgumentException {
if (!compiled.containsKey(id.key())) { if (!compiled.containsKey(id.key())) {
throw new IllegalArgumentException("Producer with that identifier is not compiled"); throw new IllegalArgumentException("Producer with that identifier is not compiled. Provided key: " + id.key());
} }
ResourceProducer producer = compiled.get(id.key()); ResourceProducer producer = compiled.get(id.key());
if (!id.getType().isAssignableFrom(producer.getClass())) { if (!id.getType().isAssignableFrom(producer.getClass())) {

View File

@ -7,6 +7,7 @@ import ru.dragonestia.msb3.api.glyph.compile.GlyphCompiler;
import ru.dragonestia.msb3.api.glyph.font.GlyphFont; import ru.dragonestia.msb3.api.glyph.font.GlyphFont;
import ru.dragonestia.msb3.api.glyph.pack.GlyphResourcePack; import ru.dragonestia.msb3.api.glyph.pack.GlyphResourcePack;
import ru.dragonestia.msb3.api.item.BlankSlotItem; import ru.dragonestia.msb3.api.item.BlankSlotItem;
import ru.dragonestia.msb3.api.talk.MonologueRenderer;
import ru.dragonestia.msb3.api.title.BlackScreen; import ru.dragonestia.msb3.api.title.BlackScreen;
import team.unnamed.creative.ResourcePack; import team.unnamed.creative.ResourcePack;
import team.unnamed.creative.atlas.Atlas; import team.unnamed.creative.atlas.Atlas;
@ -37,9 +38,11 @@ public class ResourcePackManager {
} }
private void initDefaultGlyphs() { private void initDefaultGlyphs() {
glyphResourcePack.withMojangSpaces();
glyphResourcePack.with(BlankSlotItem.compile()); glyphResourcePack.with(BlankSlotItem.compile());
glyphResourcePack.with(GlyphCompiler.instance().compile(BlackScreen.GLYPH)); glyphResourcePack.with(GlyphCompiler.instance().compile(BlackScreen.GLYPH));
glyphResourcePack.with(GlyphFont.compile()); glyphResourcePack.with(GlyphFont.compile());
MonologueRenderer.compile(glyphResourcePack);
} }
public void compile() { public void compile() {

View File

@ -0,0 +1,128 @@
package ru.dragonestia.msb3.api.talk;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextDecoration;
import net.minestom.server.entity.Player;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.ServerBootstrap;
import ru.dragonestia.msb3.api.glyph.glyph.GlyphComponentBuilder;
import ru.dragonestia.msb3.api.glyph.glyph.image.ImageGlyph;
import ru.dragonestia.msb3.api.glyph.glyph.image.TextureProperties;
import ru.dragonestia.msb3.api.glyph.pack.GlyphResourcePack;
import ru.dragonestia.msb3.api.glyph.pack.ResourceIdentifier;
import ru.dragonestia.msb3.api.glyph.pack.StringIdentifier;
import ru.dragonestia.msb3.api.util.ResourceFromJar;
import ru.dragonestia.msb3.api.util.StringUtil;
import team.unnamed.creative.base.Writable;
import team.unnamed.creative.texture.Texture;
public final class MonologueRenderer {
private static final Key MONOLOG_FONT_KEY = Key.key("msb3", "monolog");
private static final Writable DEFAULT_COMPANION_AVATAR_WRITABLE = ResourceFromJar.of(ServerBootstrap.CLASS_LOADER, "glyphs/monologue/default_avatar.png");
private static final Writable COMPANION_AVATAR_FRAME_WRITABLE = ResourceFromJar.of(ServerBootstrap.CLASS_LOADER, "glyphs/monologue/avatar_frame.png");
private static final Writable SPEECH_INDICATOR_WRITABLE = ResourceFromJar.of(ServerBootstrap.CLASS_LOADER, "glyphs/monologue/speech_indicator.png");
private static final Component INDENT_COMPONENT = Component.text(" ".repeat(14));
private static final ImageGlyph AVATAR_FRAME;
private static final ImageGlyph DEFAULT_AVATAR;
private static final ImageGlyph SPEECH_INDICATOR;
private static final ResourceIdentifier<@NotNull ImageGlyph> AVATAR_FRAME_RESOURCE_IDENTIFIER;
private static final ResourceIdentifier<@NotNull ImageGlyph> SPEECH_INDICATOR_RESOURCE_IDENTIFIER;
private static final ResourceIdentifier<@NotNull ImageGlyph> DEFAULT_AVATAR_RESOURCE_IDENTIFIER;
static {
AVATAR_FRAME = ImageGlyph.of(MONOLOG_FONT_KEY,
Texture.texture().key(Key.key("msb3", "monolog/avatar_frame.png")).data(COMPANION_AVATAR_FRAME_WRITABLE).build(),
new TextureProperties(50, 6));
DEFAULT_AVATAR = ImageGlyph.of(MONOLOG_FONT_KEY,
Texture.texture().key(Key.key("msb3", "monolog/default_avatar.png")).data(DEFAULT_COMPANION_AVATAR_WRITABLE).build(),
new TextureProperties(42, 2));
SPEECH_INDICATOR = ImageGlyph.of(MONOLOG_FONT_KEY,
Texture.texture().key(Key.key("msb3", "monolog/speech_indicator.png")).data(SPEECH_INDICATOR_WRITABLE).build(),
new TextureProperties(8, 6));
AVATAR_FRAME_RESOURCE_IDENTIFIER = StringIdentifier.image("monolog_avatar_frame");
SPEECH_INDICATOR_RESOURCE_IDENTIFIER = StringIdentifier.image("monolog_speech_indicator");
DEFAULT_AVATAR_RESOURCE_IDENTIFIER = StringIdentifier.image("monolog_default_avatar");
}
private final Player player;
private final String title;
private final String message;
private MonologueRenderer(Player player, String title, String message) {
this.player = player;
this.title = title;
this.message = message;
}
public void show() {
player.sendMessage(toComponent(ServerBootstrap.getInstance().getResourcePackManager().getGlyphResourcePack()));
}
public static MonologueRenderer create(Player player, String title, String message) {
return new MonologueRenderer(player, title, message);
}
private Component toComponent(@NotNull GlyphResourcePack resourcePack) {
Component text = Component.text(message);
Component[] parts = StringUtil.splitIntoParts(text, 180, string -> string.length() * 4);
if (parts != null && parts.length != 0) {
int avatarLineStart = Math.max(parts.length - 4, 0) / 2;
if (parts.length > 3) ++avatarLineStart;
var avatarComponent = GlyphComponentBuilder.universal(resourcePack.spaces())
.append(resourcePack.get(AVATAR_FRAME_RESOURCE_IDENTIFIER))
.append(4, resourcePack.get(DEFAULT_AVATAR_RESOURCE_IDENTIFIER))
.build();
var titleComponent = Component.empty()
.append(resourcePack.get(SPEECH_INDICATOR_RESOURCE_IDENTIFIER).toAdventure())
.append(Component.space()).append(Component.text(title, NamedTextColor.WHITE, TextDecoration.BOLD));
var monolog = Component.text();
int lineIdx;
for(lineIdx = 0; lineIdx < parts.length + 2; ++lineIdx) {
if (lineIdx == avatarLineStart) {
monolog.append(avatarComponent);
}
monolog.append(INDENT_COMPONENT);
if (lineIdx == 0) {
monolog.append(Component.newline());
}
if (lineIdx == 1) {
monolog.append(titleComponent).append(Component.newline());
}
if (lineIdx > 1) {
monolog.append(parts[lineIdx - 2].applyFallbackStyle(Style.style(NamedTextColor.GRAY)))
.append(Component.newline());
}
}
if (parts.length < 4) {
for(lineIdx = parts.length + 2; lineIdx < 5; ++lineIdx) {
monolog.append(Component.newline());
}
}
return monolog.asComponent();
} else {
throw new IllegalArgumentException("Cannot fit speech text");
}
}
public static void compile(GlyphResourcePack resourcePack) {
resourcePack.with(DEFAULT_AVATAR_RESOURCE_IDENTIFIER, DEFAULT_AVATAR)
.with(SPEECH_INDICATOR_RESOURCE_IDENTIFIER, SPEECH_INDICATOR)
.with(AVATAR_FRAME_RESOURCE_IDENTIFIER, AVATAR_FRAME);
}
}

View File

@ -0,0 +1,271 @@
package ru.dragonestia.msb3.api.util;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;
import lombok.Getter;
import lombok.Setter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.flattener.FlattenerListener;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.jetbrains.annotations.NotNull;
public final class StringUtil {
public static String[] splitIntoParts(String stringToSplit, int maxPartWidth, Function<String, Integer> stringWidthCalculateFunction) {
var partsMap = new TreeMap<Integer, String>();
int beginIndex = 0;
int endIndex = 0;
String lastLineWithTail;
for(int currentIndex = calcMinIndex(stringToSplit.indexOf(10), stringToSplit.indexOf(32)); currentIndex < stringToSplit.length() && currentIndex != -1; currentIndex = calcMinIndex(stringToSplit.indexOf(32, currentIndex + 1), stringToSplit.indexOf(10, currentIndex + 1))) {
lastLineWithTail = stringToSplit.substring(beginIndex, currentIndex);
if (!lastLineWithTail.contains("\n") && stringWidthCalculateFunction.apply(lastLineWithTail) <= maxPartWidth) {
endIndex = currentIndex + 1;
partsMap.put(beginIndex, lastLineWithTail);
} else {
if (beginIndex == endIndex) {
return null;
}
beginIndex = endIndex;
--currentIndex;
}
}
String tail = stringToSplit.substring(endIndex);
String var10000 = partsMap.containsKey(beginIndex) ? partsMap.get(beginIndex) + " " : "";
lastLineWithTail = var10000 + tail;
if (stringWidthCalculateFunction.apply(lastLineWithTail) <= maxPartWidth) {
partsMap.put(beginIndex, lastLineWithTail);
} else {
if (stringWidthCalculateFunction.apply(tail) > maxPartWidth) {
return null;
}
partsMap.put(endIndex, tail);
}
return partsMap.values().toArray(String[]::new);
}
public static Component[] splitIntoParts(Component componentToSplit, int maxPartWidth, Function<String, Integer> stringWidthCalculateFunction) {
var flattenerListener = new StringSplitterFlattenerListener(maxPartWidth, stringWidthCalculateFunction);
ComponentFlattener.basic().flatten(componentToSplit, flattenerListener);
var components = flattenerListener.retrieveResult();
return components.isEmpty()? null : components.toArray(Component[]::new);
}
public static Component cutEnding(Component component, String endingSuffix) {
CutEndingFlattenerListener flattenerListener = new CutEndingFlattenerListener(endingSuffix);
ComponentFlattener.basic().flatten(component, flattenerListener);
return flattenerListener.retrieveResult();
}
private static int calcMinIndex(int... indexes) {
int min = Integer.MAX_VALUE;
boolean containNotMinusOne = false;
for (int index : indexes) {
if (index != -1) {
containNotMinusOne = true;
if (index < min) {
min = index;
}
}
}
return containNotMinusOne ? min : -1;
}
private StringUtil() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
private static class StringSplitterFlattenerListener implements FlattenerListener {
private final Deque<TextColor> colors = new LinkedList<>();
private final List<Component> lines = new LinkedList<>();
private final int maxPartWidth;
private final Function<String, Integer> stringWidthCalculateFunction;
private Component currentComponent = Component.text("");
private boolean retrieve = false;
private boolean broken = false;
public StringSplitterFlattenerListener(int maxPartWidth, Function<String, Integer> stringWidthCalculateFunction) {
this.maxPartWidth = maxPartWidth;
this.stringWidthCalculateFunction = stringWidthCalculateFunction;
}
public void pushStyle(Style style) {
var color = style.color();
if (color != null) colors.add(color);
}
public void component(@NotNull String text) {
String currentColoredPart = PlainTextComponentSerializer.plainText().serialize(currentComponent);
String resultText = currentColoredPart + text;
String[] targetLines = StringUtil.splitIntoParts(resultText, maxPartWidth, stringWidthCalculateFunction);
if (targetLines == null) broken = true;
else {
TextColor currentColor = colors.peekLast();
if (!resultText.isEmpty()) {
int startIndex = 1;
String firstLine = null;
if (targetLines[0].length() > currentColoredPart.length()) {
firstLine = targetLines[0].substring(currentColoredPart.length());
startIndex = 0;
} else {
lines.add(currentComponent);
currentComponent = Component.text("");
}
for (int i = startIndex; i < targetLines.length - 1; ++i) {
String targetLine = targetLines[i];
if (i == 0) {
targetLine = firstLine;
}
var targetLineComponent = currentColor == null ? Component.text(targetLine) : Component.text(targetLine, currentColor);
lines.add(i == 0 ? currentComponent.append(targetLineComponent) : targetLineComponent.applyFallbackStyle(Style.empty()));
}
if (targetLines.length == 1) {
if (startIndex != 1) {
currentComponent = currentComponent.append(currentColor == null ? Component.text(firstLine).applyFallbackStyle(Style.empty()) : Component.text(firstLine, currentColor));
}
} else {
currentComponent = Component.text("")
.append(currentColor == null ? Component.text(targetLines[targetLines.length - 1]) : Component.text(targetLines[targetLines.length - 1], currentColor));
}
}
}
}
public void popStyle(Style style) {
var color = style.color();
if (color != null) colors.removeLast();
}
public List<Component> retrieveResult() {
if (retrieve) {
throw new IllegalStateException("Result had been already retrieved");
} else {
retrieve = true;
if (broken) return List.of();
else {
if (!PlainTextComponentSerializer.plainText().serialize(currentComponent).isEmpty()) {
lines.add(currentComponent);
}
lines.replaceAll(Component::compact);
return lines;
}
}
}
}
private static class CutEndingFlattenerListener implements FlattenerListener {
private final Deque<TextColor> colors = new LinkedList<>();
private final List<TextColorPair> pairs = new ArrayList<>();
private String endingSuffix;
private boolean retrieve = false;
public CutEndingFlattenerListener(String endingSuffix) {
this.endingSuffix = endingSuffix;
}
public void pushStyle(@NotNull Style style) {
TextColor color = style.color();
if (color != null) colors.add(color);
}
public void component(@NotNull String text) {
pairs.add(new TextColorPair(text, colors.peekLast()));
}
public void popStyle(@NotNull Style style) {
TextColor color = style.color();
if (color != null) colors.removeLast();
}
public Component retrieveResult() {
if (retrieve) {
throw new IllegalStateException("Result had been already retrieved");
} else {
retrieve = true;
int removed = 0;
for (int i = pairs.size() - 1; i >= 0; --i) {
TextColorPair pair = pairs.get(i);
if (pair.getColor() != null) {
pairs.remove(i);
removed += pair.getText().length();
} else {
int suffixLength = endingSuffix.length();
String var10001;
if (removed >= suffixLength) {
var10001 = pair.getText().replaceFirst(" $", "");
pair.setText(var10001 + endingSuffix);
endingSuffix = "";
break;
}
int partLength = pair.getText().length();
if (partLength >= suffixLength) {
var10001 = pair.getText().substring(0, partLength - suffixLength).replaceFirst(" $", "");
pair.setText(var10001 + endingSuffix);
endingSuffix = "";
break;
}
pair.setText(endingSuffix.substring(endingSuffix.length() - partLength));
endingSuffix = endingSuffix.substring(0, endingSuffix.length() - partLength);
}
}
if (!endingSuffix.isEmpty()) {
pairs.clear();
pairs.add(new TextColorPair(endingSuffix.trim(), null));
}
Component result = null;
for (TextColorPair pair: pairs) {
Component part = Component.text(pair.getText(), pair.getColor());
if (result == null) {
result = part;
} else {
result = result.append(part);
}
}
return result;
}
}
@Setter
@Getter
private static final class TextColorPair {
private String text;
private TextColor color;
public TextColorPair(String text, TextColor color) {
this.text = text;
this.color = color;
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B