Merge branch 'feat/dialogues' into 'master'

Implemented dialogues

See merge request Dragonestia/Minestom/msb!1
This commit is contained in:
Andrey Terentev 2025-03-29 19:33:32 +00:00
commit bdc0aeb31e
186 changed files with 5953 additions and 1886 deletions

4
.gitignore vendored
View File

@ -24,4 +24,6 @@ bin/
.vscode/
.DS_Store
run/
run/
tarkov/

View File

@ -1,16 +1,42 @@
plugins {
id 'com.github.johnrengelman.shadow' version '7.0.0'
}
tasks {
shadowJar {
manifest {
attributes (
"Multi-Release": true,
"Add-Opens": "java.base/java.lang " +
"java.base/java.lang.reflect"
)
}
mergeServiceFiles()
}
java {
targetCompatibility = JavaVersion.VERSION_21
}
assemble {
dependsOn shadowJar, processResources
}
}
dependencies {
api 'net.minestom:minestom-snapshots:bb7acc2e77'
api project(":resource-compiler")
api 'net.minestom:minestom-snapshots:1_21_4-6490538291'
api 'org.slf4j:slf4j-api:2.0.16'
api 'org.apache.logging.log4j:log4j-slf4j2-impl:2.24.0'
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.spongepowered:configurate-hocon:4.2.0'
api 'org.spongepowered:configurate-gson:4.2.0'
api 'org.spongepowered:configurate-yaml:4.2.0'
api 'org.sql2o:sql2o:1.8.0'
api 'com.clickhouse:clickhouse-jdbc:0.7.1'
@ -19,4 +45,6 @@ dependencies {
api 'io.prometheus:prometheus-metrics-core:1.3.1'
api 'io.prometheus:prometheus-metrics-instrumentation-jvm:1.3.1'
api 'io.prometheus:prometheus-metrics-exporter-httpserver:1.3.1'
implementation 'org.apache.commons:commons-text:1.10.0'
}

View File

@ -1,22 +0,0 @@
package ru.dragonestia.msb3.api;
import lombok.extern.log4j.Log4j2;
import net.minestom.server.entity.GameMode;
import ru.dragonestia.msb3.api.module.FlatWorldModule;
import ru.dragonestia.msb3.api.module.MotdModule;
import ru.dragonestia.msb3.api.module.ResourcePackRepositoryModule;
@Log4j2
public class Bootstrap {
public static void main(String[] args) {
var boot = new ServerBootstrap();
MotdModule.init("logo.png", "<gradient:#ff0059:#e06806><bold>msb3 server</bold></gradient>");
FlatWorldModule.init(GameMode.ADVENTURE);
ResourcePackRepositoryModule.init(boot, "0.0.0.0", 7270);
boot.start("0.0.0.0", 25565);
}
}

View File

@ -1,60 +0,0 @@
package ru.dragonestia.msb3.api;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import net.minestom.server.MinecraftServer;
import ru.dragonestia.msb3.api.item.ItemPrefabManager;
import ru.dragonestia.msb3.api.item.ItemUtil;
import ru.dragonestia.msb3.api.resource.ResourcePackManager;
import java.net.InetSocketAddress;
@Log4j2
public class ServerBootstrap {
public static final ClassLoader CLASS_LOADER = ServerBootstrap.class.getClassLoader();
private static final String[] LOGO = {
"==============================================",
"Minestom Server Base v3 - by ScarletRedMan",
"",
" 8b d8 .dP\"Y8 88\"\"Yb 88888 ",
" 88b d88 `Ybo.\" 88__dP .dP ",
" 88YbdP88 o.`Y8b 88\"\"Yb o `Yb ",
" 88 YY 88 8bodP' 88oodP YbodP ",
"",
"Minecraft version: " + MinecraftServer.VERSION_NAME + " (protocol: " + MinecraftServer.PROTOCOL_VERSION + ")",
"==============================================",
};
@Getter private static ServerBootstrap instance;
@Getter private static boolean started = false;
private final MinecraftServer server;
@Getter private final ResourcePackManager resourcePackManager;
@Getter private final ItemPrefabManager itemPrefabManager;
public ServerBootstrap() {
instance = this;
for (var line: LOGO) log.info(line);
server = MinecraftServer.init();
MinecraftServer.setBrandName("Dragonestia");
resourcePackManager = new ResourcePackManager();
itemPrefabManager = new ItemPrefabManager();
init();
}
public void start(String address, int port) {
started = true;
server.start(new InetSocketAddress(address, port));
}
private void init() {
ItemUtil.init();
}
}

View File

@ -0,0 +1,36 @@
package ru.dragonestia.msb3.api.boot;
import lombok.extern.log4j.Log4j2;
import net.minestom.server.entity.GameMode;
import ru.dragonestia.msb3.api.module.FlatWorldModule;
import ru.dragonestia.msb3.api.module.MotdModule;
import ru.dragonestia.msb3.api.module.PrometheusMetricsModule;
import ru.dragonestia.msb3.api.module.ResourcePackRepositoryModule;
import team.unnamed.creative.ResourcePack;
import java.net.InetSocketAddress;
@Log4j2
public class DefaultBootstrap extends ServerInitializer {
public static void main(String[] args) {
ServerBootstrap.start("0.0.0.0", 25565, new DefaultBootstrap());
}
@Override
public void onDefaultModulesLoaded() {
MotdModule.init("logo.png", "<gradient:#ff0059:#e06806><bold>msb3 server</bold></gradient>");
FlatWorldModule.init(GameMode.ADVENTURE);
PrometheusMetricsModule.init(new InetSocketAddress("0.0.0.0", 7500));
}
@Override
public void onInitializeResources(ResourcePack resourcePack) {
}
@Override
public void onResourcePackCompiled(ResourcePack resourcePack) {
ResourcePackRepositoryModule.init("0.0.0.0", 7270);
}
}

View File

@ -0,0 +1,207 @@
package ru.dragonestia.msb3.api.boot;
import lombok.Getter;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import net.kyori.adventure.key.Key;
import net.minestom.server.MinecraftServer;
import net.minestom.server.event.player.AsyncPlayerConfigurationEvent;
import net.minestom.server.event.player.PlayerDisconnectEvent;
import net.minestom.server.event.player.PlayerSpawnEvent;
import ru.dragonestia.msb3.api.command.DebugCommands;
import ru.dragonestia.msb3.api.dialog.DialogRegistry;
import ru.dragonestia.msb3.api.dialog.action.CloseDialogActionHandler;
import ru.dragonestia.msb3.api.dialog.action.DialogDialogActionHandler;
import ru.dragonestia.msb3.api.dialog.condition.AlwaysDialogConditionHandler;
import ru.dragonestia.msb3.api.dialog.condition.NeverDialogConditionHandler;
import ru.dragonestia.msb3.api.entity.PickableItem;
import ru.dragonestia.msb3.api.event.NPCClickEvent;
import ru.dragonestia.msb3.api.item.ItemUtil;
import ru.dragonestia.msb3.api.player.MsbPlayer;
import ru.dragonestia.msb3.api.player.PlayerContextManager;
import ru.dragonestia.msb3.api.player.defaults.KeyedBossBarContext;
import ru.dragonestia.msb3.api.player.defaults.NavigatorContext;
import ru.dragonestia.msb3.api.player.defaults.TalksContext;
import ru.dragonestia.msb3.api.resource.DialogueResources;
import ru.dragonestia.msb3.api.resource.MonologueResources;
import ru.dragonestia.msb3.api.skin.SkinStorage;
import ru.dragonestia.msb3.api.ui.BlackScreen;
import ru.dragonestia.msb3.api.ui.bossbar.KeyedBossBars;
import ru.dragonestia.msb3.api.ui.navigator.Navigators;
import ru.dragonestia.msb3.api.util.ResourceFromJar;
import ru.dragonestia.msb3.resource.Resources;
import ru.dragonestia.msb3.resource.glyph.GlyphCharacterFactory;
import ru.dragonestia.msb3.resource.glyph.MinecraftFont;
import ru.dragonestia.msb3.resource.utils.ClassPreLoader;
import team.unnamed.creative.lang.Language;
import team.unnamed.creative.serialize.minecraft.MinecraftResourcePackWriter;
import java.io.File;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.List;
@Log4j2
@UtilityClass
public final class ServerBootstrap {
public static final ClassLoader CLASS_LOADER = ServerBootstrap.class.getClassLoader();
private static final String[] LOGO = {
"==============================================",
"Minestom Server Base v3 - by ScarletRedMan",
"",
" 8b d8 .dP\"Y8 88\"\"Yb 88888 ",
" 88b d88 `Ybo.\" 88__dP .dP ",
" 88YbdP88 o.`Y8b 88\"\"Yb o `Yb ",
" 88 YY 88 8bodP' 88oodP YbodP ",
"",
"Minecraft version: " + MinecraftServer.VERSION_NAME + " (protocol: " + MinecraftServer.PROTOCOL_VERSION + ")",
"==============================================",
};
@Getter private boolean started = false;
private final MinecraftServer server = preInitialize();
private MinecraftServer preInitialize() {
for (var line: LOGO) log.info(line);
var server = MinecraftServer.init();
MinecraftServer.setBrandName("Dragonestia");
return server;
}
public synchronized void start(String address, int port, ServerInitializer initializer) {
if (started) return;
started = true;
init(initializer);
server.start(new InetSocketAddress(address, port));
initializer.onServerStarted();
}
private void init(ServerInitializer initializer) {
initializer.onLoad();
initDefaultModules();
initializer.onDefaultModulesLoaded();
initDefaultGlyphs();
initializer.onInitializeResources(Resources.getResourcePack());
compileResourcePack();
initializer.onResourcePackCompiled(Resources.getResourcePack());
}
private void initDefaultModules() {
ItemUtil.init();
PickableItem.registerEvent();
initPlayerContextManager();
initDefaultSkins();
initDefaultDialogActionsAndConditions();
NPCClickEvent.init();
DebugCommands.init();
}
private void compileResourcePack() {
var resourcePack = Resources.compile();
log.info("Used {}/{} glyphs", GlyphCharacterFactory.countUsedChars(), GlyphCharacterFactory.countCharRange());
var file = new File("./resource-pack.zip");
MinecraftResourcePackWriter.minecraft().writeToZipFile(file, resourcePack);
log.info("Compiled resource pack. File: ./resource-pack.zip");
}
private void initDefaultGlyphs() {
ClassPreLoader.preload(BlackScreen.class);
ClassPreLoader.preload(Navigators.class);
ClassPreLoader.preload(KeyedBossBars.class);
MonologueResources.registerAvatar(MonologueResources.DEFAULT, ResourceFromJar.of(ServerBootstrap.CLASS_LOADER, "glyphs/monologue/default_avatar.png"));
MonologueResources.registerFrame(MonologueResources.DEFAULT, ResourceFromJar.of(ServerBootstrap.CLASS_LOADER, "glyphs/monologue/avatar_frame.png"));
DialogueResources.registerAvatar(DialogueResources.DEFAULT, ResourceFromJar.of("glyphs/dialogue/default_avatar.png"));
DialogueResources.registerAvatarFrame(DialogueResources.DEFAULT, ResourceFromJar.of("glyphs/dialogue/avatar_frame.png"));
DialogueResources.registerScrollUp(DialogueResources.DEFAULT, ResourceFromJar.of("glyphs/dialogue/scroll_phrase_up_button.png"));
DialogueResources.registerScrollDown(DialogueResources.DEFAULT, ResourceFromJar.of("glyphs/dialogue/scroll_phrase_down_button.png"));
DialogueResources.registerBackground(DialogueResources.DEFAULT, ResourceFromJar.of("glyphs/dialogue/background.png"));
DialogueResources.registerSubstrate(DialogueResources.DEFAULT, ResourceFromJar.of("glyphs/dialogue/phrase_substrate.png"));
DialogueResources.registerActiveTextField(DialogueResources.DEFAULT, ResourceFromJar.of("glyphs/dialogue/answer_active_text_field.png"));
DialogueResources.registerNotActiveTextField(DialogueResources.DEFAULT, ResourceFromJar.of("glyphs/dialogue/answer_not_active_text_field.png"));
DialogueResources.registerButton(DialogueResources.DEFAULT + 1, ResourceFromJar.of("glyphs/dialogue/answer_button_active_1.png"));
DialogueResources.registerButton(DialogueResources.DEFAULT + 2, ResourceFromJar.of("glyphs/dialogue/answer_button_active_2.png"));
DialogueResources.registerButton(DialogueResources.DEFAULT + 3, ResourceFromJar.of("glyphs/dialogue/answer_button_active_3.png"));
DialogueResources.registerButton(DialogueResources.DEFAULT + 4, ResourceFromJar.of("glyphs/dialogue/answer_button_active_4.png"));
initDialogueFont();
initLocales();
}
private void initDialogueFont() {
MinecraftFont.compileFontProvider(8, -28);
MinecraftFont.compileFontProvider(8, -37);
MinecraftFont.compileFontProvider(8, -46);
MinecraftFont.compileFontProvider(8, -55);
MinecraftFont.compileFontProvider(8, -64);
MinecraftFont.compileFontProvider(8, -73);
MinecraftFont.compileFontProvider(8, -82);
MinecraftFont.compileFontProvider(8, -122);
MinecraftFont.compileFontProvider(8, -131);
MinecraftFont.compileFontProvider(8, -140);
MinecraftFont.compileFontProvider(8, -149);
MinecraftFont.compileFontProvider(8, -170);
MinecraftFont.compileFontProvider(8, -179);
MinecraftFont.compileFontProvider(8, -188);
MinecraftFont.compileFontProvider(8, -197);
}
private void initLocales() {
var locales = new HashMap<String, String>();
locales.put("container.inventory", " ");
for (var lang: List.of("en_us", "ru_ru")) {
Resources.getResourcePack().language(Language.language(Key.key("minecraft", lang), locales));
}
}
private void initPlayerContextManager() {
MinecraftServer.getConnectionManager().setPlayerProvider(PlayerContextManager::create);
MinecraftServer.getGlobalEventHandler().addListener(AsyncPlayerConfigurationEvent.class, event -> {
var player = (MsbPlayer) event.getPlayer();
log.info("Connected player '{}' (uuid: {}, address: {})", player.getUsername(), player.getUuid(), player.getPlayerConnection().getRemoteAddress());
player.initContexts();
}).addListener(PlayerSpawnEvent.class, event -> {
var player = (MsbPlayer) event.getPlayer();
if (event.isFirstSpawn()) player.emitSpawnSignalForContexts();
}).addListener(PlayerDisconnectEvent.class, event -> {
var player = (MsbPlayer) event.getPlayer();
log.info("Disconnected player '{}' (uuid: {}, address: {})", player.getUsername(), player.getUuid(), player.getPlayerConnection().getRemoteAddress());
player.disposeContexts();
});
// Default contexts
PlayerContextManager.registerContext(KeyedBossBarContext.class, KeyedBossBarContext::new);
PlayerContextManager.registerContext(NavigatorContext.class, NavigatorContext::new);
PlayerContextManager.registerContext(TalksContext.class, TalksContext::new);
}
private void initDefaultSkins() {
SkinStorage.loadSkin(SkinStorage.DEFAULT, ResourceFromJar.of("skins/default.msb3skin"));
}
private void initDefaultDialogActionsAndConditions() {
DialogRegistry.registerActionHandler("close", new CloseDialogActionHandler());
DialogRegistry.registerActionHandler("dialog", new DialogDialogActionHandler());
DialogRegistry.registerConditionHandler("always", new AlwaysDialogConditionHandler());
DialogRegistry.registerConditionHandler("never", new NeverDialogConditionHandler());
}
}

View File

@ -0,0 +1,42 @@
package ru.dragonestia.msb3.api.boot;
import team.unnamed.creative.ResourcePack;
/**
* Класс инициализатора сервера. Здесь имеются все точки состояний сервера.
* Создан для того чтобы было проще прописывать логику инициализации сервера
*/
public abstract class ServerInitializer {
/**
* Запускается при самом старте приложения, когда никакая конфигурация сервера еще не подгружена
*/
public void onLoad() {}
/**
* Запускается после инициализации всех стандартных модулей сервера
*/
public abstract void onDefaultModulesLoaded();
/**
* Запускается на стадии подготовки всех ресурсов, которые необходимо скомпилировать и добавить в ресурс-пак.
*
* <p>Именно на данном этапе необходимо регистрировать глифы.
* @param resourcePack Ресурс-пак
*/
public void onInitializeResources(ResourcePack resourcePack) {}
/**
* Запускается после того как все пакеты ресурсов были успешно скомпилированы и готовы к последующей упаковке
* и отправке клиенту игрока.
*
* <p> Сервер в данном состоянии еще не полностью загружен
* @param resourcePack Скомпилированный ресурс-пак
*/
public void onResourcePackCompiled(ResourcePack resourcePack) {}
/**
* Сервер полностью запущен и доступен для игры
*/
public void onServerStarted() {}
}

View File

@ -0,0 +1,106 @@
package ru.dragonestia.msb3.api.command;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.ArgumentEnum;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.utils.location.RelativeVec;
import ru.dragonestia.msb3.api.util.Env;
import java.text.DecimalFormat;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
@Log4j2
@UtilityClass
public class DebugCommands {
public void init() {
if (!Env.bool("MSB3_DEBUG").orElse(false)) return;
registerPosCommand();
registerTeleportCommand();
registerGameModeCommand();
log.info("Registered debug commands");
}
private void registerPosCommand() {
var command = new Command("pos");
var counter = new AtomicInteger(0);
command.setDefaultExecutor((sender, ctx) -> {
var player = (Player) sender;
var pos = player.getPosition();
var decimal = new DecimalFormat("#.00");
var result = "new Pos(%s, %s, %s, %sf, %sf)".formatted(
decimal.format(pos.x()).replace(",", "."),
decimal.format(pos.y()).replace(",", "."),
decimal.format(pos.z()).replace(",", "."),
decimal.format(pos.yaw()).replace(",", "."),
decimal.format(pos.pitch()).replace(",", ".")
);
var color = (counter.getAndIncrement() & 1) == 1 ? NamedTextColor.YELLOW : NamedTextColor.GOLD;
var component = Component.text(result, color, TextDecoration.UNDERLINED)
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, result));
player.sendMessage(Component.text(counter.get() + ") Pos: ").append(component));
log.info("Pos: {}", result);
});
MinecraftServer.getCommandManager().register(command);
}
private void registerTeleportCommand() {
var command = new Command("tp");
command.setDefaultExecutor((source, context) -> source.sendMessage(Component.text("Использование: /tp x y z")));
var posArg = ArgumentType.RelativeBlockPosition("pos");
command.addSyntax((sender, ctx) -> {
var player = (Player) sender;
RelativeVec relativeVec = ctx.get("pos");
var position = player.getPosition().withCoord(relativeVec.fromSender(player));
player.teleport(position);
player.sendMessage(Component.text("Выбили телепортирован на " + position));
}, posArg);
MinecraftServer.getCommandManager().register(command);
}
private void registerGameModeCommand() {
var command = new Command("gm", "gamemode");
ArgumentEnum<GameMode> gamemode = ArgumentType.Enum("gamemode", GameMode.class).setFormat(ArgumentEnum.Format.LOWER_CASED);
gamemode.setCallback((sender, exception) -> {
sender.sendMessage(Component.text("Invalid gamemode ", NamedTextColor.RED)
.append(Component.text(exception.getInput(), NamedTextColor.WHITE))
.append(Component.text("!")));
});
command.setDefaultExecutor((sender, context) -> {
var commandName = context.getCommandName();
sender.sendMessage(Component.text("Использование: /" + commandName + " <gamemode>", NamedTextColor.RED));
});
command.addSyntax((sender, context) -> {
if (!(sender instanceof Player p)) {
sender.sendMessage(Component.text("Please run this command in-game.", NamedTextColor.RED));
return;
}
var mode = context.get(gamemode);
p.setGameMode(mode);
String gamemodeString = "gameMode." + mode.name().toLowerCase(Locale.ROOT);
Component gamemodeComponent = Component.translatable(gamemodeString);
sender.sendMessage(Component.translatable("commands.gamemode.success.self", gamemodeComponent));
}, gamemode);
MinecraftServer.getCommandManager().register(command);
}
}

View File

@ -0,0 +1,77 @@
package ru.dragonestia.msb3.api.dialog;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
import net.kyori.adventure.key.Key;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.dialog.interlocutor.Interlocutor;
import ru.dragonestia.msb3.api.player.PlayerContext;
import ru.dragonestia.msb3.api.player.defaults.TalksContext;
import ru.dragonestia.msb3.api.resource.dialog.ButtonNumber;
import ru.dragonestia.msb3.api.ui.TalksThemes;
import ru.dragonestia.msb3.api.ui.dialogue.DialogueRenderer;
import ru.dragonestia.msb3.api.ui.dialogue.DialogueTheme;
import ru.dragonestia.msb3.api.util.DebugMessage;
import java.util.ArrayList;
import java.util.List;
@Log4j2
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Dialog {
private Key id;
private String text;
private boolean rememberId;
private String themeId;
private List<DialogButton> buttons;
public void switchDialog(Player player, DialogueRenderer renderer, Interlocutor interlocutor) {
var ctx = PlayerContext.of(player, TalksContext.class);
if (rememberId) {
ctx.getOpenedDialogues().add(id.asString());
DebugMessage.send(player, "Идентификатор диалога %s был сохранен в список просмотренных диалогов".formatted(id.asString()));
}
DialogueTheme theme = DialogueTheme.builder().build();
if (themeId != null) {
var themeOpt = TalksThemes.getDialogueTheme(themeId);
if (themeOpt.isPresent()) {
theme = themeOpt.get();
} else {
DebugMessage.sendError(player, "Не найдена тема для диалога с идентификатором %s".formatted(themeId));
log.error("Unknown theme: {}", themeId);
}
}
renderer.setTheme(theme.toBuilder()
.setAvatar(interlocutor.getDialogAvatar())
.build());
renderer.setText(text);
for (var buttonNumber: ButtonNumber.values()) renderer.removeButton(buttonNumber);
var buttons = new ArrayList<>(getButtons());
buttons.removeIf(button -> !button.checkConditions(player, this, button, renderer));
for (var buttonNumber: ButtonNumber.values()) {
if (buttons.isEmpty()) break;
var button = buttons.removeFirst();
renderer.setButton(buttonNumber, button.getText(), buttonCtx -> {
var click = new DialogButtonClick(buttonCtx.player(), interlocutor, this, button, buttonCtx.buttonNumber(), buttonCtx.renderer());
button.onClick(click);
});
}
renderer.rerender();
}
public synchronized void open(Player player, Interlocutor interlocutor) {
var renderer = DialogueRenderer.getRenderer(player).orElseGet(() -> new DialogueRenderer(player, DialogueTheme.builder().build()));
switchDialog(player, renderer, interlocutor);
renderer.show();
}
}

View File

@ -0,0 +1,93 @@
package ru.dragonestia.msb3.api.dialog;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
import net.kyori.adventure.key.Key;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.player.PlayerContext;
import ru.dragonestia.msb3.api.player.defaults.TalksContext;
import ru.dragonestia.msb3.api.ui.dialogue.DialogueRenderer;
import ru.dragonestia.msb3.api.util.DebugMessage;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Log4j2
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class DialogButton {
private final UUID uuid = UUID.randomUUID();
private Key id;
private String text;
private String actionId;
private Map<String, String> params;
private List<DialogCondition> conditions;
public boolean checkConditions(Player player, Dialog dialog, DialogButton button, DialogueRenderer renderer) {
if (PlayerContext.of(player, TalksContext.class).isIgnoreDialogConditions()) {
return true;
}
for (var condition: conditions) {
if (!condition.check(player, dialog, button, renderer)) return false;
}
return true;
}
public void onClick(DialogButtonClick click) {
var player = click.player();
if (actionId == null) {
DebugMessage.sendError(player, "actionId для кнопки имеет значение null");
return;
}
var action = DialogRegistry.findActionHandler(actionId);
if (action.isEmpty()) {
DebugMessage.sendError(player, "Действие кнопки с идентификатором '%s' не зарегистрировано".formatted(actionId));
return;
}
boolean success;
try {
DebugMessage.send(player, "Выполнение действия диалога actionId=%s params=%s".formatted(actionId, params));
action.get().handle(click, params);
success = true;
} catch (Exception ex) {
log.error(ex.getMessage(), ex);
DebugMessage.sendError(player, "Произошла ошибка при выполнении действия клика кнопки. actionId=%s params=%s\n%s: %s".formatted(action, params, ex.getClass().getSimpleName(), ex.getMessage()));
success = false;
}
if (!success) return;
if (id != null) {
var ctx = PlayerContext.of(player, TalksContext.class);
ctx.getClickedButtons().add(id.asString());
DebugMessage.send(player, "Идентификатор кнопки диалога %s был сохранен в список нажатых кнопок".formatted(id.asString()));
}
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (obj == this) return true;
if (obj instanceof DialogButton button) {
return uuid.equals(button.uuid);
}
return false;
}
@Override
public int hashCode() {
return uuid.hashCode();
}
}

View File

@ -0,0 +1,15 @@
package ru.dragonestia.msb3.api.dialog;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.dialog.interlocutor.Interlocutor;
import ru.dragonestia.msb3.api.resource.dialog.ButtonNumber;
import ru.dragonestia.msb3.api.ui.dialogue.DialogueRenderer;
public record DialogButtonClick(
Player player,
Interlocutor interlocutor,
Dialog dialog,
DialogButton dialogButton,
ButtonNumber buttonNumber,
DialogueRenderer renderer
) {}

View File

@ -0,0 +1,43 @@
package ru.dragonestia.msb3.api.dialog;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.ui.dialogue.DialogueRenderer;
import ru.dragonestia.msb3.api.util.DebugMessage;
import java.util.Map;
@Log4j2
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class DialogCondition {
private String conditionId;
private Map<String, String> params;
public boolean check(Player player, Dialog dialog, DialogButton button, DialogueRenderer renderer) {
if (conditionId == null) {
DebugMessage.sendError(player, "Идентификатор условия для кнопки имеет значение null");
return false;
}
var condition = DialogRegistry.findConditionHandler(conditionId);
if (condition.isEmpty()) {
return false;
}
try {
return condition.get().check(player, dialog, button, renderer, params);
} catch (Exception ex) {
log.error(ex.getMessage(), ex);
DebugMessage.send(player, "Во время проверки условия для кнопки произошла ошибка conditionId=%s params=%s".formatted(conditionId, params));
return false;
}
}
}

View File

@ -0,0 +1,60 @@
package ru.dragonestia.msb3.api.dialog;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import net.kyori.adventure.key.Key;
import ru.dragonestia.msb3.api.dialog.action.DialogActionHandler;
import ru.dragonestia.msb3.api.dialog.condition.DialogConditionHandler;
import ru.dragonestia.msb3.api.dialog.data.VoidPlayerDataProvider;
import ru.dragonestia.msb3.api.dialog.data.PlayerDataProvider;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@Log4j2
@UtilityClass
public class DialogRegistry {
private final Map<Key, Dialog> dialogs = new ConcurrentHashMap<>();
private final Map<String, DialogActionHandler> actionHandlers = new HashMap<>();
private final Map<String, DialogConditionHandler> conditionHandlers = new HashMap<>();
@Getter @Setter private PlayerDataProvider playerDataProvider = new VoidPlayerDataProvider();
public void storeDialog(Dialog dialog) {
if (dialog.getId() == null) {
throw new NullPointerException("Dialog id is null");
}
dialogs.put(dialog.getId(), dialog);
}
public Optional<Dialog> findDialog(Key key) {
return Optional.ofNullable(dialogs.get(key));
}
public void registerActionHandler(String id, DialogActionHandler handler) {
var prev = actionHandlers.put(id, handler);
if (prev != null) {
log.warn("Duplicate action handler for id '{}'. Removing prev", id);
}
}
public Optional<DialogActionHandler> findActionHandler(String id) {
return Optional.ofNullable(actionHandlers.get(id));
}
public void registerConditionHandler(String id, DialogConditionHandler handler) {
var prev = conditionHandlers.put(id, handler);
if (prev != null) {
log.warn("Duplicate condition handler for id '{}'. Removing prev", id);
}
}
public Optional<DialogConditionHandler> findConditionHandler(String id) {
return Optional.ofNullable(conditionHandlers.get(id));
}
}

View File

@ -0,0 +1,13 @@
package ru.dragonestia.msb3.api.dialog.action;
import ru.dragonestia.msb3.api.dialog.DialogButtonClick;
import java.util.Map;
public class CloseDialogActionHandler implements DialogActionHandler {
@Override
public void handle(DialogButtonClick click, Map<String, String> params) {
click.renderer().close(false);
}
}

View File

@ -0,0 +1,10 @@
package ru.dragonestia.msb3.api.dialog.action;
import ru.dragonestia.msb3.api.dialog.DialogButtonClick;
import java.util.Map;
public interface DialogActionHandler {
void handle(DialogButtonClick click, Map<String, String> params);
}

View File

@ -0,0 +1,32 @@
package ru.dragonestia.msb3.api.dialog.action;
import lombok.extern.log4j.Log4j2;
import net.kyori.adventure.key.Key;
import ru.dragonestia.msb3.api.dialog.DialogButtonClick;
import ru.dragonestia.msb3.api.dialog.DialogRegistry;
import ru.dragonestia.msb3.api.util.DebugMessage;
import java.util.Map;
@Log4j2
public class DialogDialogActionHandler implements DialogActionHandler {
@Override
public void handle(DialogButtonClick click, Map<String, String> params) {
var player = click.player();
var dialogId = params.get("dialogId");
if (dialogId == null) {
DebugMessage.sendError(player, "Отсутствует обязательный параметр dialogId для команды dialog");
return;
}
var dialog = DialogRegistry.findDialog(Key.key(dialogId));
if (dialog.isEmpty()) {
log.error("Dialog {} not found", dialogId);
DebugMessage.sendError(player, "Диалог с идентификатором %s не найден".formatted(dialogId));
return;
}
dialog.get().switchDialog(player, click.renderer(), click.interlocutor());
}
}

View File

@ -0,0 +1,16 @@
package ru.dragonestia.msb3.api.dialog.condition;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.dialog.Dialog;
import ru.dragonestia.msb3.api.dialog.DialogButton;
import ru.dragonestia.msb3.api.ui.dialogue.DialogueRenderer;
import java.util.Map;
public class AlwaysDialogConditionHandler implements DialogConditionHandler {
@Override
public boolean check(Player player, Dialog dialog, DialogButton button, DialogueRenderer renderer, Map<String, String> params) {
return true;
}
}

View File

@ -0,0 +1,13 @@
package ru.dragonestia.msb3.api.dialog.condition;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.dialog.Dialog;
import ru.dragonestia.msb3.api.dialog.DialogButton;
import ru.dragonestia.msb3.api.ui.dialogue.DialogueRenderer;
import java.util.Map;
public interface DialogConditionHandler {
boolean check(Player player, Dialog dialog, DialogButton button, DialogueRenderer renderer, Map<String, String> params);
}

View File

@ -0,0 +1,16 @@
package ru.dragonestia.msb3.api.dialog.condition;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.dialog.Dialog;
import ru.dragonestia.msb3.api.dialog.DialogButton;
import ru.dragonestia.msb3.api.ui.dialogue.DialogueRenderer;
import java.util.Map;
public class NeverDialogConditionHandler implements DialogConditionHandler {
@Override
public boolean check(Player player, Dialog dialog, DialogButton button, DialogueRenderer renderer, Map<String, String> params) {
return false;
}
}

View File

@ -0,0 +1,10 @@
package ru.dragonestia.msb3.api.dialog.data;
import ru.dragonestia.msb3.api.player.MsbPlayer;
public interface PlayerDataProvider {
TalksPlayerData load(MsbPlayer player);
void save(MsbPlayer player, TalksPlayerData data);
}

View File

@ -0,0 +1,8 @@
package ru.dragonestia.msb3.api.dialog.data;
import java.util.Set;
public record TalksPlayerData(
Set<String> openedDialogs,
Set<String> clickedButtons
) {}

View File

@ -0,0 +1,19 @@
package ru.dragonestia.msb3.api.dialog.data;
import ru.dragonestia.msb3.api.player.MsbPlayer;
import java.util.HashSet;
public class VoidPlayerDataProvider implements PlayerDataProvider {
@Override
public TalksPlayerData load(MsbPlayer player) {
return new TalksPlayerData(
new HashSet<>(),
new HashSet<>()
);
}
@Override
public void save(MsbPlayer player, TalksPlayerData data) {}
}

View File

@ -0,0 +1,15 @@
package ru.dragonestia.msb3.api.dialog.interlocutor;
import ru.dragonestia.msb3.api.resource.DialogueResources;
import ru.dragonestia.msb3.api.resource.MonologueResources;
public class AnonymousInterlocutor extends SimpleInterlocutor {
public AnonymousInterlocutor(String name) {
super(
name,
DialogueResources.getAvatar(DialogueResources.DEFAULT).orElseThrow(),
MonologueResources.getAvatar(MonologueResources.DEFAULT).orElseThrow()
);
}
}

View File

@ -0,0 +1,43 @@
package ru.dragonestia.msb3.api.dialog.interlocutor;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.dialog.Dialog;
import ru.dragonestia.msb3.api.ui.dialogue.DialogueRenderer;
import ru.dragonestia.msb3.api.ui.monologue.Monologue;
import ru.dragonestia.msb3.api.ui.monologue.MonologueTheme;
import ru.dragonestia.msb3.resource.glyph.GlyphImage;
public interface Interlocutor {
String getInterlocutorName();
GlyphImage getDialogAvatar();
GlyphImage getMonologAvatar();
default MonologueTheme getMonologueTheme() {
return MonologueTheme.builder().build();
}
default void sendMonolog(Player receiver, String message) {
sendMonolog(receiver, message, getMonologueTheme());
}
default void sendMonolog(Player receiver, String message, MonologueTheme theme) {
Monologue.create(receiver, getInterlocutorName(), message).show(theme);
}
default void sendDialog(Player receiver, Dialog dialog) {
sendDialog(receiver, dialog, false);
}
default void sendDialog(Player receiver, Dialog dialog, boolean inDialogueWindowNow) {
if (inDialogueWindowNow) {
var renderer = DialogueRenderer.getRenderer(receiver).orElseThrow();
dialog.switchDialog(receiver, renderer, this);
renderer.rerender();
} else {
dialog.open(receiver, this);
}
}
}

View File

@ -0,0 +1,29 @@
package ru.dragonestia.msb3.api.dialog.interlocutor;
import lombok.AllArgsConstructor;
import lombok.Setter;
import ru.dragonestia.msb3.resource.glyph.GlyphImage;
@Setter
@AllArgsConstructor
public class SimpleInterlocutor implements Interlocutor {
private String name;
private GlyphImage dialogAvatar;
private GlyphImage monologAvatar;
@Override
public String getInterlocutorName() {
return name;
}
@Override
public GlyphImage getDialogAvatar() {
return dialogAvatar;
}
@Override
public GlyphImage getMonologAvatar() {
return monologAvatar;
}
}

View File

@ -0,0 +1,134 @@
package ru.dragonestia.msb3.api.dialog.provider;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import net.kyori.adventure.key.Key;
import org.spongepowered.configurate.gson.GsonConfigurationLoader;
import org.spongepowered.configurate.hocon.HoconConfigurationLoader;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import ru.dragonestia.msb3.api.dialog.Dialog;
import ru.dragonestia.msb3.api.dialog.DialogButton;
import ru.dragonestia.msb3.api.dialog.DialogCondition;
import ru.dragonestia.msb3.api.dialog.DialogRegistry;
import team.unnamed.creative.base.Writable;
import java.util.*;
@Log4j2
@UtilityClass
public class DialogFileProvider {
@SneakyThrows
public void readFromHocon(Writable writable) {
var root = HoconConfigurationLoader.builder()
.buildAndLoadString(writable.toUTF8String());
loadDialogs(Objects.requireNonNull(root.get(DialogsList.class)));
}
@SneakyThrows
public void readFromJson(Writable writable) {
var root = GsonConfigurationLoader.builder()
.buildAndLoadString(writable.toUTF8String());
loadDialogs(Objects.requireNonNull(root.get(DialogsList.class)));
}
@SneakyThrows
public void readFromYaml(Writable writable) {
var root = YamlConfigurationLoader.builder()
.buildAndLoadString(writable.toUTF8String());
loadDialogs(Objects.requireNonNull(root.get(DialogsList.class)));
}
private void loadDialogs(DialogsList source) {
for (var entry: source.dialogs.entrySet()) {
var dialogId = entry.getKey();
var dialogEntry = entry.getValue();
var dialog = new Dialog();
dialog.setId(Key.key(dialogId));
dialog.setText(Objects.requireNonNull(dialogEntry.text, "Text is null inside dialog " + dialogId));
dialog.setRememberId(dialogEntry.remember);
dialog.setThemeId(dialogEntry.theme);
var buttons = new ArrayList<DialogButton>();
for (var buttonEntry: dialogEntry.buttons) {
var button = new DialogButton();
if (buttonEntry.id != null) button.setId(Key.key(buttonEntry.id));
button.setText(Objects.requireNonNull(buttonEntry.text));
button.setActionId(Objects.requireNonNull(buttonEntry.action));
button.setParams(Objects.requireNonNull(buttonEntry.params));
var conditions = new ArrayList<DialogCondition>();
for (var conditionEntry: buttonEntry.conditions) {
var condition = new DialogCondition();
condition.setConditionId(Objects.requireNonNull(conditionEntry.id));
condition.setParams(Objects.requireNonNull(conditionEntry.params));
}
button.setConditions(conditions);
buttons.add(button);
}
dialog.setButtons(buttons);
DialogRegistry.storeDialog(dialog);
}
}
@Getter
@Setter
@ConfigSerializable
public static class DialogsList {
private Map<String, DialogEntry> dialogs;
}
@Setter
@Getter
@ConfigSerializable
public static class DialogEntry {
private String text;
private boolean remember = false;
private String theme = null;
private List<ButtonEntry> buttons;
@Override
public String toString() {
return "{text=" + text + ", remember=" + remember + ", theme=" + theme + ", buttons=" + buttons + "}";
}
}
@Setter
@Getter
@ConfigSerializable
public static class ButtonEntry {
private String id = null;
private String text;
private String action;
private Map<String, String> params = new HashMap<>();
private List<ConditionEntry> conditions = new ArrayList<>();
@Override
public String toString() {
return "{id=" + id + ", text=" + text + ", action=" + action + ", params=" + params + ", conditions=" + conditions + "}";
}
}
@Setter
@Getter
@ConfigSerializable
public static class ConditionEntry {
private String id;
private Map<String, String> params = new HashMap<>();
@Override
public String toString() {
return "{id=" + id + ", params=" + params + "}";
}
}
}

View File

@ -0,0 +1,153 @@
package ru.dragonestia.msb3.api.entity;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.*;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.entity.metadata.display.AbstractDisplayMeta;
import net.minestom.server.entity.metadata.display.TextDisplayMeta;
import net.minestom.server.instance.Instance;
import net.minestom.server.network.packet.server.play.PlayerInfoRemovePacket;
import net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket;
import net.minestom.server.network.packet.server.play.TeamsPacket;
import net.minestom.server.scoreboard.Team;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
public class Human extends EntityCreature {
private static final AtomicInteger freeId = new AtomicInteger(0);
private static final Map<String, Team> npcTeams = new ConcurrentHashMap<>();
private static final Point displayNamePositionOffset = new Vec(0, 2, 0);
@Getter private Component name;
private final String username;
@Getter private final PlayerSkin skin;
private final NamedTextColor teamColor;
private final PlayerInfoUpdatePacket createPacket;
private final PlayerInfoRemovePacket removePacket;
private final Team team;
private Entity displayNameEntity;
private Predicate<Player> lastViewableRule;
public Human(String name, PlayerSkin skin) {
this(Component.text(name, NamedTextColor.YELLOW), skin);
}
public Human(Component name, PlayerSkin skin) {
this(name, skin, NamedTextColor.YELLOW);
}
public Human(Component name, PlayerSkin skin, NamedTextColor teamColor) {
super(EntityType.PLAYER);
this.name = name;
this.username = "Human_" + freeId.incrementAndGet();
this.skin = skin;
this.teamColor = teamColor;
this.createPacket = getCreatePacket();
this.removePacket = new PlayerInfoRemovePacket(getUuid());
this.team = pickTeam();
getEntityMeta().setCustomNameVisible(false);
setBoundingBox(0.6, 1.8, 0.6);
getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.2F);
}
private PlayerInfoUpdatePacket getCreatePacket() {
var properties = List.of(new PlayerInfoUpdatePacket.Property("textures", skin.textures(), skin.signature()));
return new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.ADD_PLAYER,
new PlayerInfoUpdatePacket.Entry(getUuid(), username, properties, false, 0, GameMode.CREATIVE, name, null, 0));
}
@Override
public CompletableFuture<Void> setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) {
return super.setInstance(instance, spawnPosition)
.thenRun(this::getDisplayNameEntity);
}
@Override
public @NotNull CompletableFuture<Void> teleport(@NotNull Pos position, @NotNull Vec velocity, long @Nullable [] chunks, int flags, boolean shouldConfirm) {
return super.teleport(position, velocity, chunks, flags, shouldConfirm)
.thenRun(() -> getDisplayNameEntity().teleport(position.add(displayNamePositionOffset), chunks, flags, shouldConfirm));
}
@Override
public void refreshPosition(@NotNull Pos newPosition, boolean ignoreView, boolean sendPackets) {
super.refreshPosition(newPosition, ignoreView, sendPackets);
getDisplayNameEntity().refreshPosition(newPosition.add(displayNamePositionOffset), ignoreView, sendPackets);
}
@Override
protected void remove(boolean permanent) {
super.remove(permanent);
if (displayNameEntity != null) displayNameEntity.remove();
}
@Override
public void updateNewViewer(@NotNull Player player) {
player.sendPacket(createPacket);
super.updateNewViewer(player);
player.sendPacket(new TeamsPacket(team.getTeamName(), new TeamsPacket.AddEntitiesToTeamAction(List.of(username))));
}
@Override
public void updateOldViewer(@NotNull Player player) {
player.sendPacket(removePacket);
super.updateOldViewer(player);
}
@Override
public void updateViewableRule() {
super.updateViewableRule();
if (displayNameEntity != null) displayNameEntity.updateViewableRule();
}
@Override
public void updateViewableRule(@Nullable Predicate<Player> predicate) {
super.updateViewableRule(lastViewableRule = predicate);
if (displayNameEntity != null) displayNameEntity.updateViewableRule(predicate);
}
private Team pickTeam() {
return npcTeams.computeIfAbsent(teamColor.asHexString(), $ -> MinecraftServer.getTeamManager()
.createBuilder("NPC_r%s_g%s_b%s".formatted(teamColor.red(), teamColor.green(), teamColor.blue()))
.collisionRule(TeamsPacket.CollisionRule.ALWAYS)
.teamColor(teamColor)
.nameTagVisibility(TeamsPacket.NameTagVisibility.NEVER)
.build());
}
private synchronized Entity getDisplayNameEntity() {
if (displayNameEntity == null) {
displayNameEntity = new Entity(EntityType.TEXT_DISPLAY);
var meta = (TextDisplayMeta) displayNameEntity.getEntityMeta();
meta.setText(name);
meta.setHasNoGravity(true);
meta.setBillboardRenderConstraints(AbstractDisplayMeta.BillboardConstraints.VERTICAL);
if (lastViewableRule != null) displayNameEntity.updateViewableRule(lastViewableRule);
displayNameEntity.setInstance(instance, getPosition().add(displayNamePositionOffset));
}
return displayNameEntity;
}
public void setName(Component name) {
this.name = name;
if (displayNameEntity != null) {
var meta = (TextDisplayMeta) displayNameEntity.getEntityMeta();
meta.setText(name);
}
}
}

View File

@ -0,0 +1,13 @@
package ru.dragonestia.msb3.api.entity;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.event.NPCClickEvent;
public interface NPC {
default boolean supportedClickType(NPCClickEvent.ClickType clickType) {
return true;
}
void onClickByPlayer(Player player, NPCClickEvent.ClickType clickType);
}

View File

@ -0,0 +1,36 @@
package ru.dragonestia.msb3.api.entity;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.ItemEntity;
import net.minestom.server.entity.Player;
import net.minestom.server.event.item.PickupItemEvent;
import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.time.temporal.ChronoUnit;
public class PickableItem extends ItemEntity {
public PickableItem(@NotNull ItemStack itemStack) {
super(itemStack);
setPickable(true);
setMergeable(false);
setPickupDelay(500, ChronoUnit.MILLIS);
}
public static void registerEvent() {
MinecraftServer.getGlobalEventHandler().addListener(PickupItemEvent.class, event -> {
if (!(event.getItemEntity() instanceof PickableItem)) return;
if (!(event.getEntity() instanceof Player player)) return;
var itemEntity = event.getItemEntity();
var inv = player.getInventory();
var item = itemEntity.getItemStack();
if (!inv.addItemStack(item)) {
event.setCancelled(true);
}
});
}
}

View File

@ -0,0 +1,19 @@
package ru.dragonestia.msb3.api.entity;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import org.jetbrains.annotations.NotNull;
import java.util.function.Supplier;
public class StaticEntity extends Entity {
public StaticEntity(@NotNull EntityType entityType) {
super(entityType);
setNoGravity(true);
}
public record Spot(Pos pos, Supplier<StaticEntity> supplier) {}
}

View File

@ -0,0 +1,83 @@
package ru.dragonestia.msb3.api.entity.goal;
import lombok.Getter;
import lombok.Setter;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.ai.GoalSelector;
import org.jetbrains.annotations.NotNull;
public class LookCloseGoal extends GoalSelector {
private final static int MAX_DIST = 7 * 7;
@Setter @Getter private volatile boolean enabled = false;
private boolean lastSeePlayer = false;
@Getter private float defaultYaw;
@Getter private float defaultPitch;
public LookCloseGoal(@NotNull EntityCreature entityCreature) {
super(entityCreature);
defaultYaw = entityCreature.getPosition().yaw();
defaultPitch = entityCreature.getPosition().pitch();
}
@Override
public boolean shouldStart() {
return true;
}
@Override
public void start() {}
@Override
public void tick(long l) {
if ((l & 0b11) == 0) return;
if (!enabled) {
if (lastSeePlayer) {
entityCreature.setView(defaultYaw, defaultPitch);
lastSeePlayer = false;
}
return;
}
Player closestPlayer = null;
double closestDistance = Double.MAX_VALUE;
for (var player: entityCreature.getViewers()) {
var distance = player.getPosition().distanceSquared(entityCreature.getPosition());
if (distance > MAX_DIST) continue;
if (distance < closestDistance) {
closestDistance = distance;
closestPlayer = player;
}
}
if (closestPlayer != null) {
entityCreature.lookAt(closestPlayer);
lastSeePlayer = true;
return;
}
if (lastSeePlayer) {
entityCreature.setView(defaultYaw, defaultPitch);
lastSeePlayer = false;
}
}
@Override
public boolean shouldEnd() {
return false;
}
@Override
public void end() {}
public void setDefaultRotation(float yaw, float pitch) {
defaultYaw = yaw;
defaultPitch = pitch;
}
}

View File

@ -0,0 +1,55 @@
package ru.dragonestia.msb3.api.event;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerHand;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.event.player.PlayerEntityInteractEvent;
import net.minestom.server.event.trait.EntityEvent;
import net.minestom.server.event.trait.InstanceEvent;
import net.minestom.server.instance.Instance;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.entity.NPC;
import java.util.Objects;
@RequiredArgsConstructor
public class NPCClickEvent implements EntityEvent, InstanceEvent {
@Getter private final Player player;
private final NPC npcEntity;
@Getter private final ClickType clickType;
@Override
public @NotNull Entity getEntity() {
return (Entity) npcEntity;
}
@Override
public @NotNull Instance getInstance() {
return Objects.requireNonNull(player.getInstance());
}
public enum ClickType {
ATTACK,
INTERACT
}
public static void init() {
MinecraftServer.getGlobalEventHandler().addListener(PlayerEntityInteractEvent.class, event -> {
if (event.getHand() != PlayerHand.MAIN) return;
if (event.getEntity() instanceof NPC npc) {
if (!npc.supportedClickType(ClickType.INTERACT)) return;
MinecraftServer.getGlobalEventHandler().call(new NPCClickEvent(event.getPlayer(), npc, ClickType.INTERACT));
}
}).addListener(EntityAttackEvent.class, event -> {
if (event.getTarget() instanceof NPC npc && event.getEntity() instanceof Player player) {
if (!npc.supportedClickType(ClickType.ATTACK)) return;
MinecraftServer.getGlobalEventHandler().call(new NPCClickEvent(player, npc, ClickType.ATTACK));
}
});
}
}

View File

@ -1,7 +0,0 @@
package ru.dragonestia.msb3.api.glyph;
public final class GlyphConstants {
public static final int CHEST_GUI_WIDTH = 176;
public static final int DEFAULT_CHAT_WIDTH = 320;
}

View File

@ -1,8 +0,0 @@
package ru.dragonestia.msb3.api.glyph.compile;
import org.jetbrains.annotations.NotNull;
public interface ArbitraryCharacterFactory {
@NotNull Character nextCharacter() throws IllegalStateException;
}

View File

@ -1,51 +0,0 @@
package ru.dragonestia.msb3.api.glyph.compile;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class DefaultArbitraryCharacterFactory implements ArbitraryCharacterFactory {
private final static Set<Character> reservedCharacters = new HashSet<>();
static {
for (char c = 'a'; c <= 'z'; c++) reservedCharacters.add(c);
for (char c = 'A'; c <= 'Z'; c++) reservedCharacters.add(c);
for (char c = 'а'; c <= 'я'; c++) reservedCharacters.add(c);
for (char c = 'А'; c <= 'Я'; c++) reservedCharacters.add(c);
for (char c = '0'; c <= '9'; c++) reservedCharacters.add(c);
reservedCharacters.addAll(Arrays.asList(
'!', '?', ':', '$',
';', '#', '@', '%',
'^', '&', '*', '(',
')', '_', '-', '+',
'/', '\\', '№', '"',
'\'', '{', '}', '[',
']', '~', '`', '<', '>',
',', '.', '|', '\n', '\r',
'\b', '\f', '\t', ' ',
'ё', 'Ё', '=')
);
}
private char currentChar = '\uA201';
@Override
public @NotNull Character nextCharacter() throws IllegalStateException {
if (currentChar == Character.MAX_VALUE) {
throw new IllegalStateException("Characters range exceeded");
}
do {
currentChar++;
} while (!isCharacterAllowed(currentChar));
return currentChar;
}
private boolean isCharacterAllowed(char c) {
return !reservedCharacters.contains(c);
}
}

View File

@ -1,45 +0,0 @@
package ru.dragonestia.msb3.api.glyph.compile;
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.NotNull;
import team.unnamed.creative.font.Font;
import team.unnamed.creative.font.FontProvider;
import team.unnamed.creative.part.ResourcePackPart;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class DefaultGlyphCompiler implements GlyphCompiler {
@Override
public @NotNull Collection<@NotNull ResourcePackPart> compile(@NotNull Collection<@NotNull ResourceProducer> producers) {
Set<ResourcePackPart> fileResources = new HashSet<>();
Set<Key> fontKeys = producers
.stream()
.map(ResourceProducer::fontKey)
.collect(Collectors.toUnmodifiableSet());
for (Key key : fontKeys) {
List<FontProvider> fontProviders = new ArrayList<>();
ArbitraryCharacterFactory characterFactory = new DefaultArbitraryCharacterFactory();
for (ResourceProducer producer : producers) {
if (producer.fontKey().equals(key)) {
producer.produceResources(characterFactory);
// Add font providers for current font key
fontProviders.addAll(producer.fontProviders());
// Add textures to common set with resources
fileResources.addAll(producer.textures());
}
}
fileResources.add(Font.font(key, fontProviders));
}
return fileResources;
}
<T> Iterable<T> toIterable(final Stream<T> stream) {
return stream::iterator;
}
}

View File

@ -1,20 +0,0 @@
package ru.dragonestia.msb3.api.glyph.compile;
import org.jetbrains.annotations.NotNull;
import team.unnamed.creative.part.ResourcePackPart;
import java.util.Arrays;
import java.util.Collection;
public interface GlyphCompiler {
static GlyphCompiler instance() {
return new DefaultGlyphCompiler();
}
@NotNull Collection<@NotNull ResourcePackPart> compile(@NotNull Collection<@NotNull ResourceProducer> resourceProducerCollection);
default @NotNull Collection<@NotNull ResourcePackPart> compile(@NotNull ResourceProducer... resourceProducer) {
return compile(Arrays.asList(resourceProducer));
}
}

View File

@ -1,26 +0,0 @@
package ru.dragonestia.msb3.api.glyph.compile;
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceAlreadyProducedException;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceNotProducedException;
import team.unnamed.creative.font.FontProvider;
import team.unnamed.creative.texture.Texture;
import java.util.Collection;
import java.util.Collections;
public interface ResourceProducer {
@NotNull Key fontKey();
boolean produced();
void produceResources(ArbitraryCharacterFactory characterFactory) throws ResourceAlreadyProducedException;
@NotNull Collection<@NotNull FontProvider> fontProviders() throws ResourceNotProducedException;
default @NotNull Collection<@NotNull Texture> textures() throws ResourceNotProducedException {
return Collections.emptyList();
}
}

View File

@ -1,55 +0,0 @@
package ru.dragonestia.msb3.api.glyph.font;
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.ServerBootstrap;
import ru.dragonestia.msb3.api.glyph.compile.GlyphCompiler;
import ru.dragonestia.msb3.api.glyph.glyph.Glyph;
import ru.dragonestia.msb3.api.glyph.glyph.image.TextureProperties;
import ru.dragonestia.msb3.api.glyph.glyph.image.multicharacter.LanguageGlyphCollection;
import ru.dragonestia.msb3.api.glyph.glyph.space.SpacesGlyphResourceProducer;
import ru.dragonestia.msb3.api.glyph.glyph.space.mojang.MojangSpacesGlyph;
import ru.dragonestia.msb3.api.util.ResourceFromJar;
import team.unnamed.creative.base.Writable;
import team.unnamed.creative.part.ResourcePackPart;
import team.unnamed.creative.texture.Texture;
import java.util.Collection;
import java.util.List;
public class GlyphFont {
public static final SpacesGlyphResourceProducer SPACES = MojangSpacesGlyph.create();
private static final Key MINECRAFT_FONT_KEY = Key.key(Glyph.DEFAULT_NAMESPACE, "minecraft_font");
private static final Writable MINECRAFT_FONT_IMAGE_WRITABLE = ResourceFromJar.of(ServerBootstrap.CLASS_LOADER, "glyphs/defaults/minecraft_font.png");
public static final LanguageGlyphCollection LANGUAGE_GLYPH = minecraftFontGlyphCollection(
MINECRAFT_FONT_KEY,
Key.key(MINECRAFT_FONT_KEY.asString().concat(".png")),
List.of(new TextureProperties(12,-6),
new TextureProperties(8,-24),
new TextureProperties(8,-36)));
private GlyphFont() {}
public static Collection<ResourcePackPart> compile() {
return GlyphCompiler.instance().compile(SPACES, LANGUAGE_GLYPH);
}
private static @NotNull LanguageGlyphCollection minecraftFontGlyphCollection(@NotNull Key fontKey, @NotNull Key textureKey, @NotNull List<@NotNull TextureProperties> propertiesList) {
return LanguageGlyphCollection.of(fontKey,
Texture.texture(textureKey, MINECRAFT_FONT_IMAGE_WRITABLE),
propertiesList,
List.of(" АБВГДЕЖЗИК",
"ЛМНОПРСТУФХЦЧШЩЪ",
"ЫЬЭЮЯабвгдежзикл",
"мнопрстуфхцчшщъы",
"ьэюяйЙёЁ ",
"₽!\"#$%&'()*+,-./",
"0123456789: <=>?",
"@ABCDEFGHIJKLMNO",
"PQRSTUVWXYZ[\\]^_",
"`abcdefghijklmno",
"pqrstuvwxyz{|} ")
);
}
}

View File

@ -1,3 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph;
public interface AppendableGlyph extends Glyph {}

View File

@ -1,24 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceNotProducedException;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class EmptyGlyph implements AppendableGlyph {
public static final EmptyGlyph INSTANCE = new EmptyGlyph();
@Override
public @NotNull Component toAdventure() throws ResourceNotProducedException {
return Component.text("");
}
@Override
public int width() {
return 0;
}
}

View File

@ -1,21 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceNotProducedException;
public interface Glyph {
String DEFAULT_NAMESPACE = "msb3";
Key DEFAULT_FONT_KEY = Key.key(DEFAULT_NAMESPACE, "default");
Key DEFAULT_SPACES_FONT_KEY = Key.key(DEFAULT_NAMESPACE, "spaces");
int SEPARATOR_WIDTH = 1;
@NotNull Component toAdventure() throws ResourceNotProducedException;
int width();
}

View File

@ -1,60 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.glyph.space.SpacesGlyphResourceProducer;
import java.util.List;
public interface GlyphComponentBuilder {
static @NotNull GlyphComponentBuilder universal(SpacesGlyphResourceProducer spacesProducer) {
return new GlyphComponentBuilderImpl(spacesProducer, 0, Component.text(""));
}
static @NotNull GlyphComponentBuilder gui(SpacesGlyphResourceProducer spacesProducer) {
return new GlyphComponentBuilderImpl(spacesProducer, -8, Component.text("", NamedTextColor.WHITE));
}
@NotNull GlyphComponentBuilder append(PositionType positionType, int position, @NotNull AppendableGlyph glyph);
default @NotNull GlyphComponentBuilder append(PositionType positionType, @NotNull AppendableGlyph glyph) {
return append(positionType, 0, glyph);
}
@NotNull GlyphComponentBuilder append(PositionType positionType, int position, @NotNull List<? extends @NotNull AppendableGlyph> glyphList);
default @NotNull GlyphComponentBuilder append(PositionType positionType, @NotNull List<? extends @NotNull AppendableGlyph> glyphList) {
return append(positionType, 0, glyphList);
}
// Append with default position type
default @NotNull GlyphComponentBuilder append(int position, @NotNull AppendableGlyph glyph) {
return append(PositionType.ABSOLUTE, position, glyph);
}
default @NotNull GlyphComponentBuilder append(@NotNull AppendableGlyph glyph) {
return append(PositionType.ABSOLUTE, glyph);
}
default @NotNull GlyphComponentBuilder append(int position, @NotNull List<? extends @NotNull AppendableGlyph> glyphList) {
return append(PositionType.ABSOLUTE, position, glyphList);
}
default @NotNull GlyphComponentBuilder append(@NotNull List<? extends @NotNull AppendableGlyph> glyphList) {
return append(PositionType.ABSOLUTE, glyphList);
}
@NotNull Component build(boolean keepInitialPosition);
default @NotNull Component build() {
return build(true);
}
enum PositionType {
ABSOLUTE,
RELATIVE
}
}

View File

@ -1,86 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import ru.dragonestia.msb3.api.glyph.glyph.space.SpacesGlyphResourceProducer;
import java.util.ArrayList;
import java.util.List;
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class GlyphComponentBuilderImpl implements GlyphComponentBuilder {
private final SpacesGlyphResourceProducer spacesProducer;
private final int initialPosition;
private final Component baseComponent;
private final List<Glyph> glyphs = new ArrayList<>();
private int previousElementsWidth;
@Override
public @NotNull GlyphComponentBuilder append(PositionType positionType, int position, @NotNull AppendableGlyph glyph) {
if (positionType == PositionType.ABSOLUTE && previousElementsWidth != 0) {
glyphs.add(spacesProducer.translate((-1) * previousElementsWidth));
previousElementsWidth = 0;
}
if (position != 0) {
glyphs.add(spacesProducer.translate(position));
}
glyphs.add(glyph);
this.previousElementsWidth += position + glyph.width();
return this;
}
@Override
public @NotNull GlyphComponentBuilder append(PositionType positionType, int position, @NotNull List<? extends @NotNull AppendableGlyph> glyphList) {
if (positionType == PositionType.ABSOLUTE && previousElementsWidth != 0) {
glyphs.add(spacesProducer.translate((-1) * previousElementsWidth));
previousElementsWidth = 0;
}
if (position != 0) {
glyphs.add(spacesProducer.translate(position));
}
int width = 0;
for (AppendableGlyph glyph : glyphList) {
glyphs.add(glyph);
width += glyph.width();
}
this.previousElementsWidth += position + width;
return this;
}
@Override
public @NotNull Component build(boolean keepInitialPosition) {
if (keepInitialPosition) {
previousElementsWidth += initialPosition;
// Component should have zero width finally
if (previousElementsWidth != 0) {
glyphs.add(spacesProducer.translate((-1) * previousElementsWidth));
}
}
var component = baseComponent;
if (initialPosition != 0) {
component = component.append(spacesProducer.translate(initialPosition).toAdventure());
}
for (Glyph glyph : glyphs) {
component = component.append(glyph.toAdventure());
}
return component;
}
}

View File

@ -1,3 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.exception;
public class ResourceAlreadyProducedException extends RuntimeException {}

View File

@ -1,3 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.exception;
public class ResourceNotProducedException extends RuntimeException {}

View File

@ -1,33 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.image;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.compile.ResourceProducer;
import ru.dragonestia.msb3.api.glyph.glyph.AppendableGlyph;
import ru.dragonestia.msb3.api.glyph.glyph.Glyph;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceNotProducedException;
import team.unnamed.creative.texture.Texture;
public interface ImageGlyph extends AppendableGlyph, ResourceProducer {
static @NotNull ImageGlyph of(@NotNull Key key,
@NotNull Texture texture,
@NotNull TextureProperties properties) {
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);
}
@NotNull Character character() throws ResourceNotProducedException;
@NotNull Texture texture();
default @NotNull Component toAdventure() throws ResourceNotProducedException {
return Component.text(character()).font(fontKey());
}
}

View File

@ -1,105 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.image;
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.compile.ArbitraryCharacterFactory;
import ru.dragonestia.msb3.api.glyph.glyph.Glyph;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceAlreadyProducedException;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceNotProducedException;
import ru.dragonestia.msb3.api.glyph.util.ImageUtil;
import team.unnamed.creative.font.FontProvider;
import team.unnamed.creative.texture.Texture;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class ImageGlyphImpl implements ImageGlyph {
private final Key fontKey;
private final Texture texture;
private final TextureProperties properties;
private Character character;
private Set<FontProvider> fontProviders;
private int width = -1;
@Override
public @NotNull Key fontKey() {
return fontKey;
}
@Override
public boolean produced() {
return fontProviders != null;
}
@Override
public void produceResources(ArbitraryCharacterFactory characterFactory) throws ResourceAlreadyProducedException {
if (fontProviders != null) {
throw new ResourceAlreadyProducedException();
}
var fontProviderBuilder = FontProvider.bitMap();
this.character = characterFactory.nextCharacter();
fontProviderBuilder.characters(String.valueOf(character));
fontProviderBuilder.file(texture.key());
fontProviderBuilder.ascent(properties.ascent());
fontProviderBuilder.height(properties.height());
this.fontProviders = Collections.singleton(fontProviderBuilder.build());
}
@Override
public @NotNull Collection<@NotNull FontProvider> fontProviders() throws ResourceNotProducedException {
if (fontProviders == null) {
throw new ResourceNotProducedException();
}
return fontProviders;
}
@Override
public @NotNull Collection<@NotNull Texture> textures() throws ResourceNotProducedException {
return Collections.singleton(texture);
}
@Override
public int width() {
if (width == -1) {
try {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(texture.data().toByteArray()));
int fileHeight = image.getHeight();
width = (int) Math.ceil(
((double) properties.height() / (double) fileHeight)
* ImageUtil.calculateWidth(image)) + Glyph.SEPARATOR_WIDTH;
} catch (IOException e) {
e.printStackTrace();
}
}
return width;
}
@Override
public @NotNull Character character() throws ResourceNotProducedException {
if (fontProviders == null) {
throw new ResourceNotProducedException();
}
return character;
}
@Override
public @NotNull Texture texture() {
return texture;
}
}

View File

@ -1,11 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.image.multicharacter;
import net.kyori.adventure.text.format.TextColor;
import org.jetbrains.annotations.Nullable;
public interface ColorableGlyph {
@Nullable TextColor color();
void updateColor(@Nullable TextColor color);
}

View File

@ -1,42 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.image.multicharacter;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.format.TextColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.dragonestia.msb3.api.glyph.compile.ResourceProducer;
import ru.dragonestia.msb3.api.glyph.glyph.AppendableGlyph;
import ru.dragonestia.msb3.api.glyph.glyph.Glyph;
import ru.dragonestia.msb3.api.glyph.glyph.image.TextureProperties;
import team.unnamed.creative.texture.Texture;
import java.util.List;
public interface LanguageGlyphCollection extends ResourceProducer {
static @NotNull LanguageGlyphCollection of(@NotNull Key fontKey,
@NotNull Texture texture,
@NotNull List<@NotNull TextureProperties> propertiesList,
@NotNull List<@NotNull String> charactersMapping) {
return new LanguageGlyphCollectionImpl(fontKey, texture, propertiesList, charactersMapping);
}
@Deprecated(forRemoval = true)
static @NotNull LanguageGlyphCollection of(@NotNull Texture texture,
@NotNull List<@NotNull TextureProperties> propertiesList,
@NotNull List<@NotNull String> charactersMapping) {
return of(Glyph.DEFAULT_FONT_KEY, texture, propertiesList, charactersMapping);
}
@NotNull AppendableGlyph translate(int height, int ascent, @NotNull Character character, @Nullable TextColor color) throws IllegalArgumentException;
@NotNull List<@NotNull AppendableGlyph> translate(int height, int ascent, @NotNull String text, @Nullable TextColor color) throws IllegalArgumentException;
default @NotNull AppendableGlyph translate(int height, int ascent, @NotNull Character character) throws IllegalArgumentException {
return translate(height, ascent, character, null);
}
default @NotNull List<@NotNull AppendableGlyph> translate(int height, int ascent, @NotNull String text) throws IllegalArgumentException {
return translate(height, ascent, text, null);
}
}

View File

@ -1,92 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.image.multicharacter;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.format.TextColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.dragonestia.msb3.api.glyph.compile.ArbitraryCharacterFactory;
import ru.dragonestia.msb3.api.glyph.glyph.AppendableGlyph;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceAlreadyProducedException;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceNotProducedException;
import ru.dragonestia.msb3.api.glyph.glyph.image.TextureProperties;
import team.unnamed.creative.font.FontProvider;
import team.unnamed.creative.texture.Texture;
import java.util.*;
public class LanguageGlyphCollectionImpl implements LanguageGlyphCollection {
private final Key fontKey;
private final Texture texture;
private final Map<TextureProperties, MulticharacterImageGlyphCollection> propertiesToMulticharacterMap = new HashMap<>();
private Set<FontProvider> fontProviders;
LanguageGlyphCollectionImpl(
Key fontKey,
Texture texture,
List<TextureProperties> propertiesList,
List<String> charactersMapping
) {
this.fontKey = fontKey;
this.texture = texture;
for (TextureProperties properties : propertiesList) {
propertiesToMulticharacterMap.put(properties, MulticharacterImageGlyphCollection.of(fontKey, texture, properties, charactersMapping));
}
}
@Override
public @NotNull Key fontKey() {
return fontKey;
}
@Override
public boolean produced() {
return fontProviders != null;
}
@Override
public void produceResources(ArbitraryCharacterFactory characterFactory) throws ResourceAlreadyProducedException {
if (fontProviders != null) {
throw new ResourceAlreadyProducedException();
}
fontProviders = new HashSet<>();
for (var entry : propertiesToMulticharacterMap.entrySet()) {
entry.getValue().produceResources(characterFactory);
fontProviders.addAll(entry.getValue().fontProviders());
}
}
@Override
public @NotNull Collection<@NotNull FontProvider> fontProviders() throws ResourceNotProducedException {
return fontProviders;
}
@Override
public @NotNull Collection<@NotNull Texture> textures() throws ResourceNotProducedException {
return Collections.singleton(texture);
}
@Override
public @NotNull AppendableGlyph translate(int height, int ascent, @NotNull Character character, @Nullable TextColor color) throws IllegalArgumentException {
TextureProperties properties = new TextureProperties(height, ascent);
if (!propertiesToMulticharacterMap.containsKey(properties)) {
throw new IllegalArgumentException();
}
return propertiesToMulticharacterMap.get(properties).translate(character, color);
}
@Override
public @NotNull List<@NotNull AppendableGlyph> translate(int height, int ascent, @NotNull String text, @Nullable TextColor color) throws IllegalArgumentException {
TextureProperties properties = new TextureProperties(height, ascent);
if (!propertiesToMulticharacterMap.containsKey(properties)) {
throw new IllegalArgumentException();
}
return propertiesToMulticharacterMap.get(properties).translate(text, color);
}
}

View File

@ -1,54 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.image.multicharacter;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.format.TextColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.dragonestia.msb3.api.glyph.compile.ResourceProducer;
import ru.dragonestia.msb3.api.glyph.glyph.AppendableGlyph;
import ru.dragonestia.msb3.api.glyph.glyph.Glyph;
import ru.dragonestia.msb3.api.glyph.glyph.image.TextureProperties;
import ru.dragonestia.msb3.api.glyph.glyph.space.SpacesGlyph;
import team.unnamed.creative.texture.Texture;
import java.util.ArrayList;
import java.util.List;
public interface MulticharacterImageGlyphCollection extends ResourceProducer {
static @NotNull MulticharacterImageGlyphCollection of(@NotNull Key fontKey,
@NotNull Texture texture,
@NotNull TextureProperties properties,
@NotNull List<@NotNull String> charactersMapping) {
return new MulticharacterImageGlyphCollectionImpl(fontKey, texture, properties, charactersMapping);
}
@Deprecated(forRemoval = true)
static @NotNull MulticharacterImageGlyphCollection of(@NotNull Texture texture,
@NotNull TextureProperties properties,
@NotNull List<@NotNull String> charactersMapping) {
return of(Glyph.DEFAULT_FONT_KEY, texture, properties, charactersMapping);
}
@NotNull AppendableGlyph translate(@NotNull Character character, @Nullable TextColor color) throws IllegalArgumentException;
default @NotNull List<@NotNull AppendableGlyph> translate(@NotNull String text, @Nullable TextColor color) throws IllegalArgumentException {
List<AppendableGlyph> glyphs = new ArrayList<>();
for (char character : text.toCharArray()) {
if (character == ' ') {
glyphs.add(SpacesGlyph.DEFAULT_SPACE_GLYPH);
} else {
glyphs.add(translate(character, color));
}
}
return glyphs;
}
default @NotNull AppendableGlyph translate(@NotNull Character character) throws IllegalArgumentException {
return translate(character, null);
}
default @NotNull List<@NotNull AppendableGlyph> translate(@NotNull String text) throws IllegalArgumentException {
return translate(text, null);
}
}

View File

@ -1,131 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.image.multicharacter;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.format.TextColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.dragonestia.msb3.api.glyph.compile.ArbitraryCharacterFactory;
import ru.dragonestia.msb3.api.glyph.glyph.Glyph;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceAlreadyProducedException;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceNotProducedException;
import ru.dragonestia.msb3.api.glyph.glyph.image.TextureProperties;
import ru.dragonestia.msb3.api.glyph.util.ImageUtil;
import team.unnamed.creative.font.FontProvider;
import team.unnamed.creative.texture.Texture;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.*;
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class MulticharacterImageGlyphCollectionImpl implements MulticharacterImageGlyphCollection {
private final Key fontKey;
private final Texture texture;
private final TextureProperties properties;
private final List<String> charactersMapping;
private final Map<Character, Character> originToArbitraryCharacterMap = new HashMap<>();
private Set<FontProvider> fontProviders;
private BufferedImage image;
@Override
public @NotNull Key fontKey() {
return fontKey;
}
@Override
public boolean produced() {
return fontProviders != null;
}
@Override
public void produceResources(ArbitraryCharacterFactory characterFactory) throws ResourceAlreadyProducedException {
if (fontProviders != null) {
throw new ResourceAlreadyProducedException();
}
var fontProviderBuilder = FontProvider.bitMap();
fontProviderBuilder.file(texture.key());
fontProviderBuilder.ascent(properties.ascent());
fontProviderBuilder.height(properties.height());
List<String> mappingLines = new ArrayList<>();
for (String mappingLine : charactersMapping) {
StringBuilder builder = new StringBuilder();
for (char character : mappingLine.toCharArray()) {
char arbitraryCharacter = characterFactory.nextCharacter();
originToArbitraryCharacterMap.put(character, arbitraryCharacter);
builder.append(arbitraryCharacter);
}
mappingLines.add(builder.toString());
}
fontProviderBuilder.characters(mappingLines);
this.fontProviders = Collections.singleton(fontProviderBuilder.build());
}
@Override
public @NotNull Collection<@NotNull FontProvider> fontProviders() throws ResourceNotProducedException {
return fontProviders;
}
@Override
public @NotNull Collection<@NotNull Texture> textures() throws ResourceNotProducedException {
return Collections.singleton(texture);
}
@Override
public @NotNull PreparedImageGlyph translate(@NotNull Character character, @Nullable TextColor color) throws IllegalArgumentException {
if (!originToArbitraryCharacterMap.containsKey(character)) {
throw new IllegalArgumentException();
}
int width = 0;
for (int lineIndex = 0; lineIndex < charactersMapping.size(); lineIndex++) {
String line = charactersMapping.get(lineIndex);
for (int characterIndex = 0; characterIndex < line.toCharArray().length; characterIndex++) {
if (line.charAt(characterIndex) == character) {
if (image == null) {
cacheImage();
}
if (image == null) {
throw new IllegalArgumentException();
}
int filePartWidth = image.getWidth() / charactersMapping.get(0).length();
int filePartHeight = image.getHeight() / charactersMapping.size();
width = (int) Math.ceil(
((double) properties.height() / (double) filePartHeight)
* ImageUtil.calculateWidth(
image, filePartWidth * characterIndex, filePartHeight * lineIndex,
filePartWidth * (characterIndex + 1), filePartHeight * (lineIndex + 1)
)) + Glyph.SEPARATOR_WIDTH;
break;
}
}
}
return new PreparedImageGlyph(fontKey, originToArbitraryCharacterMap.get(character), width, color);
}
private void cacheImage() {
try {
image = ImageIO.read(new ByteArrayInputStream(texture.data().toByteArray()));
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@ -1,43 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.image.multicharacter;
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.TextColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import ru.dragonestia.msb3.api.glyph.glyph.AppendableGlyph;
@AllArgsConstructor(access = AccessLevel.PACKAGE)
public class PreparedImageGlyph implements AppendableGlyph, ColorableGlyph {
private final Key key;
private final Character character;
private final int width;
private @Nullable TextColor color;
@Override
public @NotNull Component toAdventure() {
return Component.text(character)
.font(key)
.color(color == null ? NamedTextColor.BLACK : color);
}
@Override
public int width() {
return width;
}
@Override
public @Nullable TextColor color() {
return color;
}
@Override
public void updateColor(@Nullable TextColor color) {
this.color = color;
}
}

View File

@ -1,64 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.space;
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.NotNull;
import lombok.RequiredArgsConstructor;
import ru.dragonestia.msb3.api.glyph.glyph.EmptyGlyph;
import ru.dragonestia.msb3.api.glyph.glyph.Glyph;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceNotProducedException;
import ru.dragonestia.msb3.api.glyph.util.ArrayUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@RequiredArgsConstructor
public abstract class AbstractSpacesGlyphResourceProducer implements SpacesGlyphResourceProducer {
private final Key fontKey;
protected Map<Integer, Character> mapping;
@Override
public @NotNull Key fontKey() {
return fontKey;
}
@Override
public boolean produced() {
return mapping != null;
}
@Override
public Glyph translate(int length) throws ResourceNotProducedException {
if (mapping == null) {
throw new ResourceNotProducedException();
}
if (length == 0) {
return EmptyGlyph.INSTANCE;
}
int sign = length > 0 ? 1 : -1;
String binaryString = Integer.toBinaryString(Math.abs(length));
List<Character> characters = new ArrayList<>();
int currentRankLength = 1;
for (int index = 0; index < binaryString.length(); index++) {
char digit = binaryString.charAt(binaryString.length() - index - 1);
if (digit == '1') {
int partLength = currentRankLength * sign;
if (!mapping.containsKey(partLength)) {
throw new IllegalArgumentException("Too much length");
}
characters.add(mapping.get(partLength));
}
currentRankLength *= 2;
}
return new SpacesGlyph(fontKey(), ArrayUtil.toCharArray(characters), length);
}
}

View File

@ -1,24 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.space;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.glyph.AppendableGlyph;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceNotProducedException;
import team.unnamed.creative.font.Font;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PACKAGE)
public class DefaultSpaceGlyph implements AppendableGlyph {
@Override
public @NotNull Component toAdventure() throws ResourceNotProducedException {
return Component.text(" ").font(Font.MINECRAFT_DEFAULT);
}
@Override
public int width() {
return 4;
}
}

View File

@ -1,91 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.space;
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.compile.ArbitraryCharacterFactory;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceAlreadyProducedException;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceNotProducedException;
import team.unnamed.creative.base.Writable;
import team.unnamed.creative.font.BitMapFontProvider;
import team.unnamed.creative.font.FontProvider;
import team.unnamed.creative.texture.Texture;
import java.util.*;
public class DefaultSpacesGlyphResourceProducer extends AbstractSpacesGlyphResourceProducer {
private final Key textureKey;
private final Writable writable;
private Set<Texture> textures;
private Set<FontProvider> fontProviders;
public DefaultSpacesGlyphResourceProducer(Key fontKey, Key textureKey, Writable writable) {
super(fontKey);
this.textureKey = textureKey;
this.writable = writable;
}
@Override
public boolean produced() {
return textures != null;
}
@Override
public void produceResources(ArbitraryCharacterFactory characterFactory) {
if (textures != null) {
throw new ResourceAlreadyProducedException();
}
this.mapping = new HashMap<>();
Set<FontProvider> fontProviders = new HashSet<>();
for (int length = 1; length <= 2048; length *= 2) {
fontProviders.add(prepareBuilder(characterFactory, length).build());
fontProviders.add(prepareBuilder(characterFactory, length * (-1)).build());
}
this.textures = Collections.singleton(Texture.texture(textureKey, writable));
this.fontProviders = fontProviders;
}
@Override
public @NotNull Collection<@NotNull FontProvider> fontProviders() throws ResourceNotProducedException {
if (fontProviders == null) {
throw new ResourceNotProducedException();
}
return fontProviders;
}
@Override
public @NotNull Collection<@NotNull Texture> textures() throws ResourceNotProducedException {
if (textures == null) {
throw new ResourceNotProducedException();
}
return textures;
}
@NotNull
private BitMapFontProvider.Builder prepareBuilder(ArbitraryCharacterFactory characterFactory, int length) {
var fontProviderBuilder = FontProvider.bitMap();
char character = characterFactory.nextCharacter();
fontProviderBuilder.characters(String.valueOf(character));
fontProviderBuilder.file(textureKey);
if (length > 0) {
fontProviderBuilder.height(length - 1);
fontProviderBuilder.ascent(0);
} else {
fontProviderBuilder.height(length - 2);
fontProviderBuilder.ascent(-32768);
}
mapping.put(length, character);
return fontProviderBuilder;
}
}

View File

@ -1,40 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.space;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.glyph.Glyph;
import team.unnamed.creative.base.Writable;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class SpacesGlyph implements Glyph {
private static final Key DEFAULT_SPACE_TEXTURE_KEY = Key.key(Glyph.DEFAULT_NAMESPACE, "space");
public static final @NotNull DefaultSpaceGlyph DEFAULT_SPACE_GLYPH = new DefaultSpaceGlyph();
private final Key key;
private final char[] characters;
private final int length;
public static @NotNull SpacesGlyphResourceProducer create(@NotNull Key fontKey,
@NotNull Key textureKey,
@NotNull Writable spacesWritable) {
return new DefaultSpacesGlyphResourceProducer(fontKey, textureKey, spacesWritable);
}
public static SpacesGlyphResourceProducer create(@NotNull Writable spacesWritable) {
return create(Glyph.DEFAULT_SPACES_FONT_KEY, DEFAULT_SPACE_TEXTURE_KEY, spacesWritable);
}
@Override
public @NotNull Component toAdventure() {
return Component.text(new String(characters)).font(key);
}
@Override
public int width() {
return length;
}
}

View File

@ -1,10 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.space;
import ru.dragonestia.msb3.api.glyph.compile.ResourceProducer;
import ru.dragonestia.msb3.api.glyph.glyph.Glyph;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceNotProducedException;
public interface SpacesGlyphResourceProducer extends ResourceProducer {
Glyph translate(int length) throws ResourceNotProducedException;
}

View File

@ -1,18 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.space.mojang;
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.compile.ResourceProducer;
import ru.dragonestia.msb3.api.glyph.glyph.Glyph;
import ru.dragonestia.msb3.api.glyph.glyph.space.SpacesGlyphResourceProducer;
public interface MojangSpacesGlyph extends Glyph, ResourceProducer {
static @NotNull SpacesGlyphResourceProducer create(@NotNull Key key) {
return new MojangSpacesGlyphResourceProducer(key);
}
static SpacesGlyphResourceProducer create() {
return create(Glyph.DEFAULT_SPACES_FONT_KEY);
}
}

View File

@ -1,60 +0,0 @@
package ru.dragonestia.msb3.api.glyph.glyph.space.mojang;
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.compile.ArbitraryCharacterFactory;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceAlreadyProducedException;
import ru.dragonestia.msb3.api.glyph.glyph.exception.ResourceNotProducedException;
import ru.dragonestia.msb3.api.glyph.glyph.space.AbstractSpacesGlyphResourceProducer;
import team.unnamed.creative.font.FontProvider;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Set;
public class MojangSpacesGlyphResourceProducer extends AbstractSpacesGlyphResourceProducer {
private Set<FontProvider> fontProviders;
public MojangSpacesGlyphResourceProducer(Key key) {
super(key);
}
@Override
public boolean produced() {
return fontProviders != null;
}
@Override
public void produceResources(ArbitraryCharacterFactory characterFactory) {
if (fontProviders != null) {
throw new ResourceAlreadyProducedException();
}
mapping = new HashMap<>();
var fontProviderBuilder = FontProvider.space();
for (int length = 1; length <= 2048; length *= 2) {
fontProviderBuilder.advance(retrieveCharacter(characterFactory, length), length);
fontProviderBuilder.advance(retrieveCharacter(characterFactory, length * (-1)), length * (-1));
}
this.fontProviders = Collections.singleton(fontProviderBuilder.build());
}
@Override
public @NotNull Collection<@NotNull FontProvider> fontProviders() throws ResourceNotProducedException {
if (fontProviders == null) {
throw new ResourceNotProducedException();
}
return fontProviders;
}
private Character retrieveCharacter(ArbitraryCharacterFactory characterFactory, int length) {
char character = characterFactory.nextCharacter();
mapping.put(length, character);
return character;
}
}

View File

@ -1,63 +0,0 @@
package ru.dragonestia.msb3.api.glyph.pack;
import org.jetbrains.annotations.NotNull;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import ru.dragonestia.msb3.api.glyph.compile.GlyphCompiler;
import ru.dragonestia.msb3.api.glyph.compile.ResourceProducer;
import team.unnamed.creative.part.ResourcePackPart;
import java.util.*;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class DefaultGlyphResourcePack implements GlyphResourcePack {
private final Map<String, ResourceProducer> raw = new HashMap<>();
private final Map<String, ResourceProducer> compiled = new HashMap<>();
private final Set<ResourcePackPart> resources = new HashSet<>();
@Override
public @NotNull Collection<@NotNull ResourcePackPart> all() {
if (!raw.isEmpty()) {
compileAll();
}
return resources;
}
@Override
public void compileAll() {
var resources = GlyphCompiler.instance().compile(raw.values());
this.resources.addAll(resources);
compiled.putAll(raw);
raw.clear();
}
@Override
public @NotNull <T extends ResourceProducer> GlyphResourcePack with(@NotNull ResourceIdentifier<@NotNull T> id, @NotNull T producer) {
if (raw.containsKey(id.key()) || compiled.containsKey(id.key())) {
throw new IllegalArgumentException("Producer with this identifier already registered");
}
raw.put(id.key(), producer);
return this;
}
@Override
public @NotNull GlyphResourcePack with(@NotNull ResourcePackPart resource) {
resources.add(resource);
return this;
}
@Override
public <T extends ResourceProducer> @NotNull T get(@NotNull ResourceIdentifier<@NotNull T> id) throws IllegalArgumentException {
if (!compiled.containsKey(id.key())) {
throw new IllegalArgumentException("Producer with that identifier is not compiled. Provided key: " + id.key());
}
ResourceProducer producer = compiled.get(id.key());
if (!id.getType().isAssignableFrom(producer.getClass())) {
throw new IllegalArgumentException("Wrong producer type");
}
return (T) producer;
}
}

View File

@ -1,58 +0,0 @@
package ru.dragonestia.msb3.api.glyph.pack;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.compile.ResourceProducer;
import ru.dragonestia.msb3.api.glyph.glyph.space.SpacesGlyphResourceProducer;
import ru.dragonestia.msb3.api.glyph.glyph.space.mojang.MojangSpacesGlyph;
import team.unnamed.creative.ResourcePack;
import team.unnamed.creative.part.ResourcePackPart;
import java.util.Collection;
public interface GlyphResourcePack {
@NotNull Collection<@NotNull ResourcePackPart> all();
void compileAll();
@Contract("_, _ -> this")
<T extends ResourceProducer> @NotNull GlyphResourcePack with(@NotNull ResourceIdentifier<@NotNull T> id, @NotNull T producer);
@Contract("_ -> this")
@NotNull GlyphResourcePack with(@NotNull ResourcePackPart resource);
@Contract("_ -> this")
default @NotNull GlyphResourcePack with(@NotNull ResourcePackPart... resources) {
for (ResourcePackPart resource : resources) {
with(resource);
}
return this;
}
@Contract("_ -> this")
default @NotNull GlyphResourcePack with(@NotNull Collection<@NotNull ResourcePackPart> resources) {
resources.forEach(this::with);
return this;
}
@Contract("-> this")
default @NotNull GlyphResourcePack withMojangSpaces() {
with(ResourceIdentifier.SPACES, MojangSpacesGlyph.create());
return this;
}
<T extends ResourceProducer> @NotNull T get(@NotNull ResourceIdentifier<@NotNull T> id) throws IllegalArgumentException;
default @NotNull SpacesGlyphResourceProducer spaces() {
return get(ResourceIdentifier.SPACES);
}
default void writeAll(@NotNull ResourcePack resourcePack) {
all().forEach(resourcePack::part);
}
static @NotNull GlyphResourcePack create() {
return new DefaultGlyphResourcePack();
}
}

View File

@ -1,12 +0,0 @@
package ru.dragonestia.msb3.api.glyph.pack;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.glyph.image.ImageGlyph;
public interface ImageResourceIdentifier extends ResourceIdentifier<@NotNull ImageGlyph> {
@Override
default @NotNull Class<@NotNull ImageGlyph> getType() {
return ImageGlyph.class;
}
}

View File

@ -1,19 +0,0 @@
package ru.dragonestia.msb3.api.glyph.pack;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.compile.ResourceProducer;
import ru.dragonestia.msb3.api.glyph.glyph.image.multicharacter.LanguageGlyphCollection;
import ru.dragonestia.msb3.api.glyph.glyph.space.SpacesGlyphResourceProducer;
public interface ResourceIdentifier<T extends @NotNull ResourceProducer> {
@NotNull StringIdentifier<@NotNull SpacesGlyphResourceProducer> SPACES = StringIdentifier.of("spaces", SpacesGlyphResourceProducer.class);
@NotNull StringIdentifier<@NotNull LanguageGlyphCollection> MINECRAFT_FONT = StringIdentifier.of("minecraft_font", LanguageGlyphCollection.class);
String STRING_IDENTIFIER_NAMESPACE = "glyphs";
@NotNull String key();
@NotNull Class<T> getType();
}

View File

@ -1,37 +0,0 @@
package ru.dragonestia.msb3.api.glyph.pack;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.glyph.compile.ResourceProducer;
import ru.dragonestia.msb3.api.glyph.glyph.image.ImageGlyph;
public class StringIdentifier<T extends @NotNull ResourceProducer> implements ResourceIdentifier<T> {
private final @NotNull String id;
private final @NotNull Class<T> type;
protected StringIdentifier(@NotNull String id, @NotNull Class<T> type) {
this.id = id;
this.type = type;
}
@Override
public @NotNull Class<T> getType() {
return type;
}
@Override
public @NotNull String key() {
return id;
}
public static <T extends @NotNull ResourceProducer> @NotNull StringIdentifier<@NotNull T> of(
@NotNull String id,
@NotNull Class<T> type) {
return new StringIdentifier<>(id, type);
}
public static @NotNull StringIdentifier<@NotNull ImageGlyph> image(@NotNull String id) {
return new StringIdentifier<>(id, ImageGlyph.class);
}
}

View File

@ -1,19 +0,0 @@
package ru.dragonestia.msb3.api.glyph.util;
import org.jetbrains.annotations.NotNull;
import lombok.experimental.UtilityClass;
import java.util.List;
@UtilityClass
public class ArrayUtil {
public char[] toCharArray(@NotNull List<Character> list) {
char[] arr = new char[list.size()];
for (int i = 0; i < list.size(); i++) {
arr[i] = list.get(i);
}
return arr;
}
}

View File

@ -1,28 +0,0 @@
package ru.dragonestia.msb3.api.glyph.util;
import lombok.experimental.UtilityClass;
import java.awt.*;
import java.awt.image.BufferedImage;
@UtilityClass
public class ImageUtil {
public int calculateWidth(BufferedImage image, int xFrom, int yFrom, int xTo, int yTo) {
int width;
for (width = xTo - 1; width > xFrom; width--) {
for (int height = yFrom; height < yTo; height++) {
if (new Color(image.getRGB(width, height), true)
.getAlpha() == 255) {
return width - xFrom + 1;
}
}
}
return width - xFrom + 1;
}
public int calculateWidth(BufferedImage image) {
return calculateWidth(image, 0, 0, image.getWidth(), image.getHeight());
}
}

View File

@ -3,8 +3,7 @@ package ru.dragonestia.msb3.api.item;
import net.kyori.adventure.key.Key;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import ru.dragonestia.msb3.api.ServerBootstrap;
import ru.dragonestia.msb3.api.glyph.glyph.Glyph;
import ru.dragonestia.msb3.api.boot.ServerBootstrap;
import ru.dragonestia.msb3.api.util.ResourceFromJar;
import team.unnamed.creative.base.Writable;
import team.unnamed.creative.model.*;
@ -24,7 +23,7 @@ public class BlankSlotItem {
public synchronized static ItemStack getItem() {
if (stack == null) {
stack = ItemStack.builder(Material.PAPER)
.customModelData(1)
.itemModel("minecraft:air")
.build();
}
@ -32,7 +31,7 @@ public class BlankSlotItem {
}
public static Collection<ResourcePackPart> compile() {
var modelKey = Key.key(Glyph.DEFAULT_NAMESPACE, "blank_slot");
var modelKey = Key.key("msb3", "blank_slot");
var itemKey = Key.key("item/paper");
Model blankSlotModel = Model.model()
@ -52,7 +51,7 @@ public class BlankSlotItem {
.overrides(ItemOverride.of(modelKey, ItemPredicate.customModelData(1)))
.build();
Texture texture = Texture.texture(Key.key(Glyph.DEFAULT_NAMESPACE, "blank_slot.png"), BLANK_SLOT_IMAGE_WRITABLE);
Texture texture = Texture.texture(Key.key("msb3", "blank_slot.png"), BLANK_SLOT_IMAGE_WRITABLE);
return Arrays.asList(blankSlotModel, paperItemModel, texture);
}
}

View File

@ -1,7 +1,8 @@
package ru.dragonestia.msb3.api.item;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import ru.dragonestia.msb3.api.ServerBootstrap;
import ru.dragonestia.msb3.api.boot.ServerBootstrap;
import ru.dragonestia.msb3.api.item.prefab.ItemPrefab;
import java.util.HashMap;
@ -9,12 +10,11 @@ import java.util.Map;
import java.util.Optional;
@Log4j2
@UtilityClass
public final class ItemPrefabManager {
private final Map<String, ItemPrefab> prefabs = new HashMap<>();
public ItemPrefabManager() {}
public synchronized void register(ItemPrefab prefab) {
if (ServerBootstrap.isStarted()) {
throw new IllegalStateException("Server is already started");

View File

@ -3,7 +3,7 @@ package ru.dragonestia.msb3.api.item.prefab;
import net.kyori.adventure.nbt.BinaryTag;
import net.minestom.server.item.ItemStack;
import net.minestom.server.tag.Tag;
import ru.dragonestia.msb3.api.ServerBootstrap;
import ru.dragonestia.msb3.api.item.ItemPrefabManager;
import java.util.Optional;
import java.util.function.Supplier;
@ -22,12 +22,12 @@ public class ItemPrefab {
}
public static Optional<ItemPrefab> of(String identifier) {
return ServerBootstrap.getInstance().getItemPrefabManager().getPrefab(identifier);
return ItemPrefabManager.getPrefab(identifier);
}
public static Optional<ItemPrefab> of(ItemStack item) {
if (item == null || item.hasTag(TAG_IDENTIFIER)) return Optional.empty();
return ServerBootstrap.getInstance().getItemPrefabManager().getPrefab(item.getTag(TAG_IDENTIFIER));
return ItemPrefabManager.getPrefab(item.getTag(TAG_IDENTIFIER));
}
public final String getIdentifier() {

View File

@ -1,17 +1,16 @@
package ru.dragonestia.msb3.api.module;
import lombok.Getter;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.GameMode;
import net.minestom.server.event.player.AsyncPlayerConfigurationEvent;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.DynamicChunk;
import net.minestom.server.instance.IChunkLoader;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.*;
import net.minestom.server.instance.block.Block;
import net.minestom.server.world.DimensionType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.dragonestia.msb3.api.world.World;
import ru.dragonestia.msb3.api.world.WorldFactory;
import java.util.concurrent.CompletableFuture;
@ -19,51 +18,53 @@ import java.util.concurrent.CompletableFuture;
public class FlatWorldModule {
private static boolean used = false;
@Getter private static World world;
private FlatWorldModule() {}
public static synchronized void init(GameMode gameMode) {
if (used) return;
used = true;
public static void init(GameMode gameMode) {
var dimension = MinecraftServer.getDimensionTypeRegistry().register("msb3:full_bright", DimensionType.builder()
.ambientLight(2f)
.build());
var factory = WorldFactory.custom(dimension, new IChunkLoader() {
@Override
public @NotNull CompletableFuture<@Nullable Chunk> loadChunk(@NotNull Instance instance, int chunkX, int chunkZ) {
return CompletableFuture.supplyAsync(() -> {
var chunk = new DynamicChunk(instance, chunkX, chunkZ);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y <= 10; y++) {
var block = switch (y) {
case 10 -> Block.GRASS_BLOCK;
case 9, 8, 7 -> Block.DIRT;
case 0 -> Block.BEDROCK;
default -> Block.STONE;
};
chunk.setBlock(x, y, z, block);
}
public @NotNull Chunk loadChunk(@NotNull Instance instance, int chunkX, int chunkZ) {
var chunk = new DynamicChunk(instance, chunkX, chunkZ);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y <= 10; y++) {
var block = switch (y) {
case 10 -> Block.GRASS_BLOCK;
case 9, 8, 7 -> Block.DIRT;
case 0 -> Block.BEDROCK;
default -> Block.STONE;
};
chunk.setBlock(x, y, z, block);
}
}
return chunk;
});
}
return chunk;
}
@Override
public @NotNull CompletableFuture<Void> saveChunk(@NotNull Chunk chunk) {
return new CompletableFuture<>();
}
public void saveChunk(@NotNull Chunk chunk) {}
});
var world = factory.createWorldSync();
init(gameMode, factory.createWorldSync(), new Pos(0, 11, 0));
}
public static synchronized void init(GameMode gameMode, World world, Pos spawnPos) {
if (used) return;
used = true;
FlatWorldModule.world = world;
MinecraftServer.getGlobalEventHandler().addListener(AsyncPlayerConfigurationEvent.class, event -> {
var player = event.getPlayer();
player.setRespawnPoint(new Pos(0, 11, 0));
player.setRespawnPoint(spawnPos);
player.setGameMode(gameMode);
event.setSpawningInstance(world.getInstance());

View File

@ -1,13 +1,13 @@
package ru.dragonestia.msb3.api.module;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.resource.ResourcePackCallback;
import net.kyori.adventure.resource.ResourcePackInfo;
import net.kyori.adventure.resource.ResourcePackRequest;
import net.kyori.adventure.text.Component;
import net.minestom.server.MinecraftServer;
import net.minestom.server.event.player.PlayerSpawnEvent;
import ru.dragonestia.msb3.api.ServerBootstrap;
import team.unnamed.creative.BuiltResourcePack;
import team.unnamed.creative.base.Writable;
import team.unnamed.creative.server.ResourcePackServer;
@ -22,19 +22,16 @@ import java.util.UUID;
import static net.kyori.adventure.text.format.NamedTextColor.RED;
@UtilityClass
public class ResourcePackRepositoryModule {
private static boolean used = false;
private ResourcePackRepositoryModule() {}
private boolean used = false;
@SneakyThrows
public static synchronized void init(ServerBootstrap bootstrap, String address, int port) {
public synchronized void init(String address, int port) {
if (used) return;
used = true;
bootstrap.getResourcePackManager().compile();
var file = new File("./resource-pack.zip");
var hash = calculateHash(file);
var uuid = UUID.randomUUID();
@ -46,6 +43,8 @@ public class ResourcePackRepositoryModule {
server.start();
MinecraftServer.getGlobalEventHandler().addListener(PlayerSpawnEvent.class, event -> {
if (!event.isFirstSpawn()) return;
var player = event.getPlayer();
player.sendResourcePacks(ResourcePackRequest.resourcePackRequest()
@ -59,7 +58,7 @@ public class ResourcePackRepositoryModule {
}
@SneakyThrows
private static String calculateHash(File file) {
private String calculateHash(File file) {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
try (InputStream fis = new FileInputStream(file)) {
@ -68,7 +67,7 @@ public class ResourcePackRepositoryModule {
}
}
private static String byteArray2Hex(final byte[] hash) {
private String byteArray2Hex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b: hash) formatter.format("%02x", b);
return formatter.toString();

View File

@ -0,0 +1,58 @@
package ru.dragonestia.msb3.api.player;
import lombok.Getter;
import net.minestom.server.entity.Player;
import net.minestom.server.network.player.GameProfile;
import net.minestom.server.network.player.PlayerConnection;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.util.UncheckedRunnable;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
public class MsbPlayer extends Player {
private final Map<String, PlayerContext> contexts = new LinkedHashMap<>();
@Getter private final Instant startSessionTime = Instant.now();
public MsbPlayer(@NotNull PlayerConnection playerConnection, @NotNull GameProfile gameProfile) {
super(playerConnection, gameProfile);
}
void putContext(PlayerContext ctx) {
contexts.put(ctx.getClass().getName(), ctx);
}
@SuppressWarnings("unchecked")
public <T extends PlayerContext> T getContext(Class<T> clazz) {
return (T) contexts.get(clazz.getName());
}
public void initContexts() {
for (var entry: contexts.entrySet()) {
var ctxKey = entry.getKey();
var ctx = entry.getValue();
UncheckedRunnable.runIgnoreException(ctx::init);
}
}
public void emitSpawnSignalForContexts() {
for (var entry: contexts.entrySet()) {
var ctxKey = entry.getKey();
var ctx = entry.getValue();
UncheckedRunnable.runIgnoreException(ctx::firstSpawn);
}
}
public void disposeContexts() {
for (var entry: contexts.entrySet()) {
var ctxKey = entry.getKey();
var ctx = entry.getValue();
UncheckedRunnable.runIgnoreException(ctx::dispose);
}
}
}

View File

@ -0,0 +1,24 @@
package ru.dragonestia.msb3.api.player;
import lombok.Getter;
import net.minestom.server.entity.Player;
@Getter
public abstract class PlayerContext {
private final MsbPlayer player;
public PlayerContext(MsbPlayer player) {
this.player = player;
}
public abstract void init();
public void firstSpawn() {}
public abstract void dispose();
public static <T extends PlayerContext> T of(Player player, Class<T> clazz) {
return ((MsbPlayer) player).getContext(clazz);
}
}

View File

@ -0,0 +1,45 @@
package ru.dragonestia.msb3.api.player;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import net.minestom.server.entity.Player;
import net.minestom.server.network.player.GameProfile;
import net.minestom.server.network.player.PlayerConnection;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
@Log4j2
@UtilityClass
public class PlayerContextManager {
private final Map<String, Function<MsbPlayer, ? extends PlayerContext>> contexts = new LinkedHashMap<>();
public MsbPlayer create(@NotNull PlayerConnection playerConnection, @NotNull GameProfile gameProfile) {
var player = new MsbPlayer(playerConnection, gameProfile);
for (var entry: contexts.entrySet()) {
var ctxKey = entry.getKey();
var ctx = entry.getValue().apply(player);
player.putContext(ctx);
}
return player;
}
public void registerContext(Class<? extends PlayerContext> clazz, Function<MsbPlayer, ? extends PlayerContext> ctxProvider) {
var key = clazz.getName();
var prev = contexts.put(key, ctxProvider);
if (prev != null) {
log.warn("Duplicate context registration for {}", key);
}
}
public <T extends PlayerContext> T getContext(Player player, Class<T> clazz) {
return ((MsbPlayer) player).getContext(clazz);
}
}

View File

@ -0,0 +1,45 @@
package ru.dragonestia.msb3.api.player.defaults;
import net.kyori.adventure.bossbar.BossBar;
import ru.dragonestia.msb3.api.player.MsbPlayer;
import ru.dragonestia.msb3.api.player.PlayerContext;
import java.util.HashMap;
import java.util.Map;
public class KeyedBossBarContext extends PlayerContext {
private final Map<String, BossBar> bossBars = new HashMap<>();
public KeyedBossBarContext(MsbPlayer player) {
super(player);
}
@Override
public void init() {
}
@Override
public void dispose() {
hideAll();
}
public void hideAll() {
bossBars.forEach((barId, bossBar) -> getPlayer().hideBossBar(bossBar));
}
public void hide(String bossBarId) {
var bossBar = bossBars.remove(bossBarId);
if (bossBar == null) return;
getPlayer().hideBossBar(bossBar);
}
public BossBar show(String bossBarId, BossBar bossBar) {
var prev = bossBars.put(bossBarId, bossBar);
if (prev != null) getPlayer().hideBossBar(prev);
getPlayer().showBossBar(bossBar);
return bossBar;
}
}

View File

@ -0,0 +1,144 @@
package ru.dragonestia.msb3.api.player.defaults;
import lombok.Setter;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.event.player.PlayerMoveEvent;
import ru.dragonestia.msb3.api.player.MsbPlayer;
import ru.dragonestia.msb3.api.player.PlayerContext;
import ru.dragonestia.msb3.api.ui.bossbar.KeyedBossBars;
import ru.dragonestia.msb3.api.ui.navigator.Navigator;
import ru.dragonestia.msb3.api.ui.navigator.Navigators;
import ru.dragonestia.msb3.resource.glyph.GlyphComponentBuilder;
import ru.dragonestia.msb3.resource.glyph.GlyphImage;
import ru.dragonestia.msb3.resource.glyph.MinecraftFont;
public class NavigatorContext extends PlayerContext implements Navigator {
private final static Component EMPTY = Component.text(" ");
private BossBar bossBar;
private Point target;
private String text;
private MinecraftFont.Text cachedText;
private boolean prevTargetEmpty = true;
private boolean enabled = true;
@Setter private String format = "[ %s ]";
@Setter private TextColor textColor = TextColor.color(0xF8FF91);
@Setter private GlyphImage arrowBackground = Navigators.GLYPH_BACKGROUND;
public NavigatorContext(MsbPlayer player) {
super(player);
}
@Override
public void init() {}
@Override
public void firstSpawn() {
getPlayer().eventNode().addListener(PlayerMoveEvent.class, event -> update());
bossBar = KeyedBossBars.showText(getPlayer(), "msb3_navigator", EMPTY);
}
@Override
public void dispose() {
target = null;
text = null;
}
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled;
update();
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public void setTarget(Point point) {
setTarget(point, null);
}
@Override
public synchronized void setTarget(Point target, String text) {
prevTargetEmpty = this.target == null;
this.target = target;
this.text = text;
update();
}
@Override
public void removeTarget() {
setTarget(null);
}
private void update() {
if (!enabled || (prevTargetEmpty && target == null)) return;
if (bossBar != null) bossBar.name(render());
}
private Component render() {
if (target == null) {
return EMPTY;
}
var builder = new GlyphComponentBuilder();
builder.append((int) (arrowBackground.width() / -2.15) + 1, arrowBackground);
// Text
if (text != null) {
builder.setColorTo(textColor);
if (cachedText == null) {
cachedText = MinecraftFont.translateByLineNumber(0, format.formatted(text));
}
builder.append((int) (cachedText.width() / -2.15), cachedText);
builder.resetColor();
}
// Arrow
builder.append(0, getArrowGlyph());
return builder.build();
}
private GlyphImage getArrowGlyph() {
var playerPos = getPlayer().getPosition();
var angle = computeAngle(playerPos.asVec(), playerPos.direction(), Vec.fromPoint(target));
if (!(angle < -2.749) && !(angle >= 2.749)) {
if (angle < -1.963) return Navigators.GLYPH_DOWN_RIGHT;
if (angle < -1.178) return Navigators.GLYPH_RIGHT;
if (angle < -0.393) return Navigators.GLYPH_UP_RIGHT;
if (angle < 0.393) return Navigators.GLYPH_UP;
if (angle < 1.178) return Navigators.GLYPH_UP_LEFT;
if (angle < 1.963) return Navigators.GLYPH_LEFT;
if (angle < 2.749) return Navigators.GLYPH_DOWN_LEFT;
}
return Navigators.GLYPH_DOWN;
}
private double computeAngle(Vec start, Vec direction, Vec destination) {
Vec destinationDirection = destination.sub(start);
double playerAngle = Math.atan2(direction.z(), direction.x());
double locAngle = Math.atan2(destinationDirection.z(), destinationDirection.x());
double angle;
for (angle = playerAngle - locAngle; angle > Math.PI; angle -= (Math.PI * 2D)) {}
while (angle < -Math.PI) {
angle += (Math.PI * 2D);
}
return angle;
}
}

View File

@ -0,0 +1,40 @@
package ru.dragonestia.msb3.api.player.defaults;
import lombok.Getter;
import lombok.Setter;
import ru.dragonestia.msb3.api.dialog.DialogRegistry;
import ru.dragonestia.msb3.api.dialog.data.TalksPlayerData;
import ru.dragonestia.msb3.api.player.MsbPlayer;
import ru.dragonestia.msb3.api.player.PlayerContext;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@Getter
public class TalksContext extends PlayerContext {
@Setter private boolean ignoreDialogConditions = false;
private final Set<String> openedDialogues = Collections.synchronizedSet(new HashSet<>());
private final Set<String> clickedButtons = Collections.synchronizedSet(new HashSet<>());
public TalksContext(MsbPlayer player) {
super(player);
}
@Override
public void init() {
var data = DialogRegistry.getPlayerDataProvider().load(getPlayer());
openedDialogues.addAll(data.openedDialogs());
clickedButtons.addAll(data.clickedButtons());
}
@Override
public void dispose() {
DialogRegistry.getPlayerDataProvider().save(getPlayer(), new TalksPlayerData(
openedDialogues,
clickedButtons
));
}
}

View File

@ -0,0 +1,272 @@
package ru.dragonestia.msb3.api.resource;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.key.Key;
import ru.dragonestia.msb3.api.resource.dialog.*;
import ru.dragonestia.msb3.resource.Resources;
import ru.dragonestia.msb3.resource.glyph.GlyphImage;
import ru.dragonestia.msb3.resource.utils.ImageUtils;
import team.unnamed.creative.base.Writable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@UtilityClass
public class DialogueResources {
public static final String DEFAULT = "default";
private final Map<String, GlyphImage> avatars = new HashMap<>();
private final Map<String, GlyphImage> avatarFrames = new HashMap<>();
private final Map<String, GlyphImage> scrollUp = new HashMap<>();
private final Map<String, GlyphImage> scrollDown = new HashMap<>();
private final Map<String, Background> backgrounds = new HashMap<>();
private final Map<String, Substrate> substrates = new HashMap<>();
private final Map<String, Button> buttons = new HashMap<>();
private final Map<String, TextField> activeFields = new HashMap<>();
private final Map<String, TextField> notActiveFields = new HashMap<>();
public void registerAvatar(String identifier, Writable writable) {
avatars.put(identifier, Resources.createGlyph(
Key.key("msb3", "dialogue/avatar/" + identifier),
writable,
GlyphPositions.avatar.height(),
GlyphPositions.avatar.y()
));
}
public void registerAvatarFrame(String identifier, Writable writable) {
avatarFrames.put(identifier, Resources.createGlyph(
Key.key("msb3", "dialogue/avatar_frame/" + identifier),
writable,
GlyphPositions.avatar.frameHeight(),
GlyphPositions.avatar.y() + GlyphPositions.avatar.frameBorderSize()
));
}
public void registerScrollUp(String identifier, Writable writable) {
scrollUp.put(identifier, Resources.createGlyph(
Key.key("msb3", "dialogue/scroll_up/" + identifier),
writable,
GlyphPositions.scrollPhraseButton.height(),
GlyphPositions.scrollPhraseButton.buttonY()
));
}
public void registerScrollDown(String identifier, Writable writable) {
scrollDown.put(identifier, Resources.createGlyph(
Key.key("msb3", "dialogue/scroll_down/" + identifier),
writable,
GlyphPositions.scrollPhraseButton.height(),
GlyphPositions.scrollPhraseButton.buttonY()
));
}
public void registerBackground(String identifier, Writable writable) {
BufferedImage image;
Writable part1;
Writable part2;
Writable part3;
Writable part4;
try (var steam = new ByteArrayInputStream(writable.toByteArray())) {
image = ImageIO.read(steam);
var w = image.getWidth();
var h = image.getHeight();
part1 = ImageUtils.imageToWritable(image.getSubimage(0, 0, w / 2, h / 2));
part2 = ImageUtils.imageToWritable(image.getSubimage(w / 2, 0, w / 2, h / 2));
part3 = ImageUtils.imageToWritable(image.getSubimage(0, h / 2, w / 2, h / 2));
part4 = ImageUtils.imageToWritable(image.getSubimage(w / 2, h / 2, w / 2, h / 2));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
var glyph1 = Resources.createGlyph(
Key.key("msb3", "dialogue/background/" + identifier + "__1"),
part1,
GlyphPositions.guiBackground.height() / 2,
GlyphPositions.guiBackground.topPartsY()
);
var glyph2 = Resources.createGlyph(
Key.key("msb3", "dialogue/background/" + identifier + "__2"),
part2,
GlyphPositions.guiBackground.height() / 2,
GlyphPositions.guiBackground.topPartsY()
);
var glyph3 = Resources.createGlyph(
Key.key("msb3", "dialogue/background/" + identifier + "__3"),
part3,
GlyphPositions.guiBackground.height() / 2,
GlyphPositions.guiBackground.bottomPartsY()
);
var glyph4 = Resources.createGlyph(
Key.key("msb3", "dialogue/background/" + identifier + "__4"),
part4,
GlyphPositions.guiBackground.height() / 2,
GlyphPositions.guiBackground.bottomPartsY()
);
backgrounds.put(identifier, new Background(glyph1, glyph2, glyph3, glyph4));
}
public void registerSubstrate(String identifier, Writable writable) {
BufferedImage image;
Writable part1;
Writable part2;
try (var steam = new ByteArrayInputStream(writable.toByteArray())) {
image = ImageIO.read(steam);
var w = image.getWidth();
var h = image.getHeight();
part1 = ImageUtils.imageToWritable(image.getSubimage(0, 0, w / 2, h));
part2 = ImageUtils.imageToWritable(image.getSubimage(w / 2, 0, w / 2, h));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
var glyph1 = Resources.createGlyph(
Key.key("msb3", "dialogue/substrate/" + identifier + "__1"),
part1,
GlyphPositions.phraseSubstrate.height(),
GlyphPositions.phraseSubstrate.y()
);
var glyph2 = Resources.createGlyph(
Key.key("msb3", "dialogue/substrate/" + identifier + "__2"),
part2,
GlyphPositions.phraseSubstrate.height(),
GlyphPositions.phraseSubstrate.y()
);
substrates.put(identifier, new Substrate(glyph1, glyph2));
}
public void registerButton(String identifier, Writable writable) {
var glyph1 = Resources.createGlyph(
Key.key("msb3", "dialogue/button/" + identifier),
writable,
GlyphPositions.answerButton.height(),
GlyphPositions.answerButton.topButtonY()
);
var glyph2 = Resources.createGlyph(
Key.key("msb3", "dialogue/button/" + identifier),
writable,
GlyphPositions.answerButton.height(),
GlyphPositions.answerButton.topButtonY()
);
var glyph3 = Resources.createGlyph(
Key.key("msb3", "dialogue/button/" + identifier),
writable,
GlyphPositions.answerButton.height(),
GlyphPositions.answerButton.bottomButtonY()
);
var glyph4 = Resources.createGlyph(
Key.key("msb3", "dialogue/button/" + identifier),
writable,
GlyphPositions.answerButton.height(),
GlyphPositions.answerButton.bottomButtonY()
);
buttons.put(identifier, new Button(glyph1, glyph2, glyph3, glyph4));
}
public void registerActiveTextField(String identifier, Writable writable) {
var glyph1 = Resources.createGlyph(
Key.key("msb3", "dialogue/text_field/" + identifier),
writable,
GlyphPositions.answerField.height(),
GlyphPositions.answerField.topFieldY()
);
var glyph2 = Resources.createGlyph(
Key.key("msb3", "dialogue/text_field/" + identifier),
writable,
GlyphPositions.answerField.height(),
GlyphPositions.answerField.topFieldY()
);
var glyph3 = Resources.createGlyph(
Key.key("msb3", "dialogue/text_field/" + identifier),
writable,
GlyphPositions.answerField.height(),
GlyphPositions.answerField.bottomFieldY()
);
var glyph4 = Resources.createGlyph(
Key.key("msb3", "dialogue/text_field/" + identifier),
writable,
GlyphPositions.answerField.height(),
GlyphPositions.answerField.bottomFieldY()
);
activeFields.put(identifier, new TextField(glyph1, glyph2, glyph3, glyph4));
}
public void registerNotActiveTextField(String identifier, Writable writable) {
var glyph1 = Resources.createGlyph(
Key.key("msb3", "dialogue/inactive_text_field/" + identifier),
writable,
GlyphPositions.answerField.height(),
GlyphPositions.answerField.topFieldY()
);
var glyph2 = Resources.createGlyph(
Key.key("msb3", "dialogue/inactive_text_field/" + identifier),
writable,
GlyphPositions.answerField.height(),
GlyphPositions.answerField.topFieldY()
);
var glyph3 = Resources.createGlyph(
Key.key("msb3", "dialogue/inactive_text_field/" + identifier),
writable,
GlyphPositions.answerField.height(),
GlyphPositions.answerField.bottomFieldY()
);
var glyph4 = Resources.createGlyph(
Key.key("msb3", "dialogue/inactive_text_field/" + identifier),
writable,
GlyphPositions.answerField.height(),
GlyphPositions.answerField.bottomFieldY()
);
notActiveFields.put(identifier, new TextField(glyph1, glyph2, glyph3, glyph4));
}
public Optional<GlyphImage> getAvatar(String identifier) {
return Optional.ofNullable(avatars.get(identifier));
}
public Optional<GlyphImage> getAvatarFrame(String identifier) {
return Optional.ofNullable(avatarFrames.get(identifier));
}
public Optional<GlyphImage> getScrollUp(String identifier) {
return Optional.ofNullable(scrollUp.get(identifier));
}
public Optional<GlyphImage> getScrollDown(String identifier) {
return Optional.ofNullable(scrollDown.get(identifier));
}
public Optional<Background> getBackground(String identifier) {
return Optional.ofNullable(backgrounds.get(identifier));
}
public Optional<Substrate> getSubstrate(String identifier) {
return Optional.ofNullable(substrates.get(identifier));
}
public Optional<Button> getButton(String identifier) {
return Optional.ofNullable(buttons.get(identifier));
}
public Optional<TextField> getActiveTextField(String identifier) {
return Optional.ofNullable(activeFields.get(identifier));
}
public Optional<TextField> getNotActiveTextField(String identifier) {
return Optional.ofNullable(notActiveFields.get(identifier));
}
}

View File

@ -1,101 +1,58 @@
package ru.dragonestia.msb3.api.resource;
import lombok.Getter;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.msb3.api.ServerBootstrap;
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.resource.Resources;
import ru.dragonestia.msb3.resource.glyph.GlyphImage;
import team.unnamed.creative.base.Writable;
import team.unnamed.creative.texture.Texture;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@UtilityClass
public class MonologueResources {
private static final Key MONOLOG_FONT_KEY = Key.key("msb3", "monolog");
public static final String DEFAULT = "default";
private final GlyphResourcePack glyphResourcePack;
private final Map<String, GlyphEntry> avatars = new HashMap<>();
private final Map<String, GlyphEntry> frames = new HashMap<>();
private final GlyphEntry speechIndicator = initSpeechIndicator();
public MonologueResources(GlyphResourcePack glyphResourcePack) {
this.glyphResourcePack = glyphResourcePack;
registerAvatar(DEFAULT, ResourceFromJar.of(ServerBootstrap.CLASS_LOADER, "glyphs/monologue/default_avatar.png"));
registerFrame(DEFAULT, ResourceFromJar.of(ServerBootstrap.CLASS_LOADER, "glyphs/monologue/avatar_frame.png"));
}
private GlyphEntry initSpeechIndicator() {
var writable = ResourceFromJar.of(ServerBootstrap.CLASS_LOADER, "glyphs/monologue/speech_indicator.png");
var glyph = ImageGlyph.of(MONOLOG_FONT_KEY,
Texture.texture().key(Key.key("msb3", "monolog/speech_indicator.png")).data(writable).build(),
new TextureProperties(8, 6));
var glyphIdentifier = StringIdentifier.image("monolog_speech_indicator");
return new GlyphEntry(glyphIdentifier, glyph);
}
private final Map<String, GlyphImage> avatars = new HashMap<>();
private final Map<String, GlyphImage> frames = new HashMap<>();
@Getter private static final GlyphImage speechIndicator = Resources.createGlyph(
Key.key("msb3", "monologue/speech_indicator"),
ResourceFromJar.of("glyphs/monologue/speech_indicator.png"),
8,
6
);
public void registerAvatar(String identifier, Writable writable) {
var glyph = ImageGlyph.of(MONOLOG_FONT_KEY,
Texture.texture().key(Key.key("msb3", "monolog/avatar_" + identifier + ".png")).data(writable).build(),
new TextureProperties(42, 2));
var glyph = Resources.createGlyph(
Key.key("msb3", "monologue/avatar/" + identifier),
writable,
42,
2
);
var glyphIdentifier = StringIdentifier.image("monolog_avatar_" + identifier);
avatars.put(identifier, new GlyphEntry(glyphIdentifier, glyph));
glyphResourcePack.with(glyphIdentifier, glyph);
avatars.put(identifier, glyph);
}
public void registerFrame(String identifier, Writable writable) {
var glyph = ImageGlyph.of(MONOLOG_FONT_KEY,
Texture.texture().key(Key.key("msb3", "monolog/frame_" + identifier + ".png")).data(writable).build(),
new TextureProperties(50, 6));
var glyph = Resources.createGlyph(
Key.key("msb3", "monologue/frame/" + identifier),
writable,
50,
6
);
var glyphIdentifier = StringIdentifier.image("monolog_frame_" + identifier);
frames.put(identifier, new GlyphEntry(glyphIdentifier, glyph));
glyphResourcePack.with(glyphIdentifier, glyph);
frames.put(identifier, glyph);
}
public Optional<ImageGlyph> getAvatar(String identifier) {
return Optional.ofNullable(avatars.get(identifier))
.map(entry -> {
try {
return glyphResourcePack.get(entry.identifier());
} catch (IllegalArgumentException ex) {
return null;
}
});
public Optional<GlyphImage> getAvatar(String identifier) {
return Optional.ofNullable(avatars.get(identifier));
}
public Optional<ImageGlyph> getFrame(String identifier) {
return Optional.ofNullable(frames.get(identifier))
.map(entry -> {
try {
return glyphResourcePack.get(entry.identifier());
} catch (IllegalArgumentException ex) {
return null;
}
});
public Optional<GlyphImage> getFrame(String identifier) {
return Optional.ofNullable(frames.get(identifier));
}
public ImageGlyph getSpeechIndicator() {
return glyphResourcePack.get(speechIndicator.identifier());
}
public void compile(GlyphResourcePack resourcePack) {
resourcePack.with(speechIndicator.identifier(), speechIndicator.glyph());
}
private record GlyphEntry(ResourceIdentifier<@NotNull ImageGlyph> identifier, ImageGlyph glyph) {}
}

View File

@ -1,84 +0,0 @@
package ru.dragonestia.msb3.api.resource;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import net.kyori.adventure.key.Key;
import ru.dragonestia.msb3.api.glyph.compile.GlyphCompiler;
import ru.dragonestia.msb3.api.glyph.font.GlyphFont;
import ru.dragonestia.msb3.api.glyph.pack.GlyphResourcePack;
import ru.dragonestia.msb3.api.item.BlankSlotItem;
import ru.dragonestia.msb3.api.title.BlackScreen;
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.model.Model;
import team.unnamed.creative.serialize.minecraft.MinecraftResourcePackWriter;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
@Getter
@Log4j2
public class ResourcePackManager {
private final ResourcePack resourcePack;
private final GlyphResourcePack glyphResourcePack;
private final MonologueResources monologueResources;
public ResourcePackManager() {
resourcePack = ResourcePack.resourcePack();
resourcePack.packMeta(34, "Dragonestia MSB3 - Resource pack");
resourcePack.icon(Writable.resource(getClass().getClassLoader(), "logo.png"));
resourcePack.unknownFile("credits.txt", Writable.stringUtf8("dragonestia.ru"));
glyphResourcePack = GlyphResourcePack.create();
monologueResources = new MonologueResources(glyphResourcePack);
initDefaultGlyphs();
}
private void initDefaultGlyphs() {
glyphResourcePack.withMojangSpaces();
glyphResourcePack.with(BlankSlotItem.compile());
glyphResourcePack.with(GlyphCompiler.instance().compile(BlackScreen.GLYPH));
glyphResourcePack.with(GlyphFont.compile());
monologueResources.compile(glyphResourcePack);
}
public void compile() {
glyphResourcePack.writeAll(resourcePack);
generateAtlases();
MinecraftResourcePackWriter.minecraft().writeToZipFile(new File("resource-pack.zip"), resourcePack);
log.info("Compiled resource pack. File: ./resource-pack.zip");
}
private void generateAtlases() {
Atlas.Builder atlasBuilder = Atlas.atlas();
atlasBuilder.key(Key.key("minecraft", "blocks"));
Set<String> applicableTextureKeys = new HashSet<>();
for (Model model: resourcePack.models()) {
for (var layer: model.textures().layers()) {
var key = layer.key();
if (key != null) {
applicableTextureKeys.add(key.asString().concat(".png"));
}
}
for (var texture: model.textures().variables().values()) {
var key = texture.key();
if (key != null) {
applicableTextureKeys.add(key.asString().concat(".png"));
}
}
}
var sources = resourcePack.textures()
.stream()
//.filter(texture -> !applicableTextureKeys.contains(texture.key().asString()))
.map(texture -> (AtlasSource) AtlasSource.single(Key.key(texture.key().toString().replace(".png", "")))).toList();
atlasBuilder.sources(sources);
resourcePack.atlas(atlasBuilder.build());
}
}

View File

@ -0,0 +1,10 @@
package ru.dragonestia.msb3.api.resource.dialog;
import ru.dragonestia.msb3.resource.glyph.GlyphImage;
public record Background(
GlyphImage part1,
GlyphImage part2,
GlyphImage part3,
GlyphImage part4
) {}

View File

@ -0,0 +1,20 @@
package ru.dragonestia.msb3.api.resource.dialog;
import ru.dragonestia.msb3.resource.glyph.GlyphImage;
public record Button(
GlyphImage button1,
GlyphImage button2,
GlyphImage button3,
GlyphImage button4
) {
public GlyphImage get(ButtonNumber number) {
return switch (number) {
case BUTTON_1 -> button1;
case BUTTON_2 -> button2;
case BUTTON_3 -> button3;
case BUTTON_4 -> button4;
};
}
}

View File

@ -0,0 +1,15 @@
package ru.dragonestia.msb3.api.resource.dialog;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum ButtonNumber {
BUTTON_1(0),
BUTTON_2(1),
BUTTON_3(2),
BUTTON_4(3);
private final int index;
}

View File

@ -0,0 +1,47 @@
package ru.dragonestia.msb3.api.resource.dialog;
import lombok.experimental.UtilityClass;
@UtilityClass
public class GlyphPositions {
public final GuiBackground guiBackground = new GuiBackground(35, -96, 262);
public final PhraseText phraseText = new PhraseText(11, -11, 340, 8, 8);
public final PhraseSubstrate phraseSubstrate = new PhraseSubstrate(-24, 19, 356, 116);
public final Avatar avatar = new Avatar(-144, 11, 100, 116);
public final AnswerText answerText = new AnswerText(175, 4, 8, -145, 149, -122, -170);
public final AnswerField answerField = new AnswerField(-117, -165, -153, 141, 44);
public final AnswerButton answerButton = new AnswerButton(-124, -182, 41, 97, 20);
public final ScrollPhraseButton scrollPhraseButton = new ScrollPhraseButton(0, 126, -100, 10);
public final ChestSlotsButtons chestSlotsButtons = new ChestSlotsButtons(new int[] {45, 46}, new int[] {52, 53}, new int[] {65, 66}, new int[] {68, 69}, new int[] {56, 57}, new int[] {59, 60});
public record GuiBackground(int topPartsY, int bottomPartsY, int height) {}
public record PhraseText(int maxLines, int lineX, int lineWidth, int fontHeight, int firstLineAscent) {
public TextureProperties firstLineProperties() {
return new TextureProperties(fontHeight, firstLineAscent);
}
}
public record PhraseSubstrate(int x, int y, int width, int height) {}
public record Avatar(int x, int y, int height, int frameHeight) {
public int frameBorderSize() {
return (frameHeight - height) / 2;
}
}
public record AnswerText(int lineWidth, int maxLines, int fontHeight, int leftLineX,
int rightLineX, int topFirstLineAscent, int bottomFirstLineAscent) {}
public record AnswerField(int topFieldY, int bottomFieldY, int leftFieldX, int rightFieldX, int height) {}
public record AnswerButton(int topButtonY, int bottomButtonY, int leftButtonX,
int rightButtonX, int height) {}
public record ScrollPhraseButton(int scrollUpButtonX, int scrollDownButtonX, int buttonY, int height) {}
public record ChestSlotsButtons(int[] scrollUp, int[] scrollDown, int[] answer1, int[] answer2, int[] answer3, int[] answer4) {}
}

View File

@ -0,0 +1,8 @@
package ru.dragonestia.msb3.api.resource.dialog;
import ru.dragonestia.msb3.resource.glyph.GlyphImage;
public record Substrate(
GlyphImage part1,
GlyphImage part2
) {}

View File

@ -0,0 +1,20 @@
package ru.dragonestia.msb3.api.resource.dialog;
import ru.dragonestia.msb3.resource.glyph.GlyphImage;
public record TextField(
GlyphImage field1,
GlyphImage field2,
GlyphImage field3,
GlyphImage field4
) {
public GlyphImage get(ButtonNumber number) {
return switch (number) {
case BUTTON_1 -> field1;
case BUTTON_2 -> field2;
case BUTTON_3 -> field3;
case BUTTON_4 -> field4;
};
}
}

View File

@ -1,3 +1,3 @@
package ru.dragonestia.msb3.api.glyph.glyph.image;
package ru.dragonestia.msb3.api.resource.dialog;
public record TextureProperties(int height, int ascent) {}

View File

@ -1,6 +1,7 @@
package ru.dragonestia.msb3.api.scheduler;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.Instance;
import net.minestom.server.timer.Task;
@ -18,6 +19,10 @@ public interface Scheduler {
return new SchedulerImpl(instance.scheduler());
}
static Scheduler ofEntity(Entity entity) {
return new SchedulerImpl(entity.scheduler());
}
static Scheduler global() {
return new SchedulerImpl(MinecraftServer.getSchedulerManager());
}

View File

@ -0,0 +1,45 @@
package ru.dragonestia.msb3.api.skin;
import com.google.gson.Gson;
import lombok.Getter;
import net.minestom.server.entity.PlayerSkin;
import team.unnamed.creative.base.Writable;
public class SkinData {
private final static Gson gson = new Gson();
@Getter private final String texture;
@Getter private final String signature;
private transient PlayerSkin skin;
public SkinData(String texture, String signature) {
this.texture = texture;
this.signature = signature;
}
public synchronized PlayerSkin getSkin() {
if (skin == null) {
skin = new PlayerSkin(texture, signature);
}
return skin;
}
public String serialize() {
return gson.toJson(new Data(signature, texture));
}
public static SkinData fromSkin(PlayerSkin skin) {
return new SkinData(skin.textures(), skin.signature());
}
public static SkinData fromWriteable(Writable writable) {
try {
return gson.fromJson(writable.toUTF8String(), SkinData.class);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
public record Data(String signature, String texture) {}
}

View File

@ -0,0 +1,53 @@
package ru.dragonestia.msb3.api.skin;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import net.kyori.adventure.key.Key;
import net.minestom.server.entity.PlayerSkin;
import team.unnamed.creative.base.Writable;
import java.io.File;
import java.io.FileWriter;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Log4j2
@UtilityClass
public class SkinStorage {
public final static Key DEFAULT = Key.key("msb3", "default");
private final Map<Key, SkinData> skins = new ConcurrentHashMap<>();
public void saveSkinToFile(PlayerSkin skin, File file) {
try (var writer = new FileWriter(file)) {
writer.write(SkinData.fromSkin(skin).serialize());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
log.info("Saved skin to {}", file.getAbsolutePath());
}
public void loadSkin(Key key, Writable writable) {
var prev = skins.put(key, SkinData.fromWriteable(writable));
if (prev != null) {
log.warn("Detected duplicate skin key: {}", key.asString());
}
}
public boolean existsSkin(Key key) {
return skins.containsKey(key);
}
public PlayerSkin getSkin(Key key) {
var skin = skins.get(key);
if (skin == null) {
log.warn("No skin for key '{}'. Using default", key.asString());
return skins.get(DEFAULT).getSkin();
}
return skin.getSkin();
}
}

View File

@ -1,21 +0,0 @@
package ru.dragonestia.msb3.api.talk;
import lombok.experimental.UtilityClass;
import ru.dragonestia.msb3.api.talk.monologue.MonologueTheme;
import java.util.HashMap;
import java.util.Map;
@UtilityClass
public class Themes {
private final Map<String, MonologueTheme> monologueThemes = new HashMap<>();
public void registerMonologueTheme(String identifier, MonologueTheme theme) {
monologueThemes.put(identifier, theme);
}
public MonologueTheme getMonologueTheme(String identifier) {
return monologueThemes.get(identifier);
}
}

View File

@ -1,35 +0,0 @@
package ru.dragonestia.msb3.api.title;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.Title;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.ServerBootstrap;
import ru.dragonestia.msb3.api.glyph.glyph.Glyph;
import ru.dragonestia.msb3.api.glyph.glyph.image.ImageGlyph;
import ru.dragonestia.msb3.api.glyph.glyph.image.TextureProperties;
import ru.dragonestia.msb3.api.util.ResourceFromJar;
import team.unnamed.creative.base.Writable;
import team.unnamed.creative.texture.Texture;
public class BlackScreen {
private static final Writable BACKGROUND_WRITABLE = ResourceFromJar.of(ServerBootstrap.CLASS_LOADER, "glyphs/defaults/fullscreen_background.png");
private static final Key FULLSCREEN_BACKGROUND_KEY = Key.key(Glyph.DEFAULT_NAMESPACE, "fullscreen_background.png");
public static final ImageGlyph GLYPH = fullscreenBackgroundGlyph();
private BlackScreen() {}
public static void show(Player player) {
player.showTitle(Title.title(
GLYPH.toAdventure(),
Component.empty()
));
}
private static ImageGlyph fullscreenBackgroundGlyph() {
return ImageGlyph.of(FULLSCREEN_BACKGROUND_KEY,
Texture.texture(FULLSCREEN_BACKGROUND_KEY, BACKGROUND_WRITABLE),
new TextureProperties(2500, 256));
}
}

View File

@ -0,0 +1,27 @@
package ru.dragonestia.msb3.api.ui;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.Title;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.util.ResourceFromJar;
import ru.dragonestia.msb3.resource.Resources;
import ru.dragonestia.msb3.resource.glyph.GlyphImage;
@UtilityClass
public class BlackScreen {
public final GlyphImage glyph = Resources.createGlyph(
Key.key("msb3", "black_screen"),
ResourceFromJar.of("glyphs/defaults/fullscreen_background.png"),
2500,
256);
public static void show(Player player) {
player.showTitle(Title.title(
glyph.component(),
Component.empty()
));
}
}

View File

@ -0,0 +1,88 @@
package ru.dragonestia.msb3.api.ui;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.TextComponent;
import net.minestom.server.entity.Player;
import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.InventoryType;
import ru.dragonestia.msb3.api.resource.dialog.GlyphPositions;
import ru.dragonestia.msb3.resource.Resources;
import ru.dragonestia.msb3.resource.glyph.GlyphComponentBuilder;
import ru.dragonestia.msb3.resource.glyph.GlyphImage;
import ru.dragonestia.msb3.resource.utils.ImageUtils;
import team.unnamed.creative.base.Writable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
public class PictureBanner {
public static final int CHEST_GUI_WIDTH = 176;
private final GlyphImage glyph1;
private final GlyphImage glyph2;
private final GlyphImage glyph3;
private final GlyphImage glyph4;
public PictureBanner(String identifier, Writable writable) {
BufferedImage image;
Writable part1;
Writable part2;
Writable part3;
Writable part4;
try (var steam = new ByteArrayInputStream(writable.toByteArray())) {
image = ImageIO.read(steam);
var w = image.getWidth();
var h = image.getHeight();
part1 = ImageUtils.imageToWritable(image.getSubimage(0, 0, w / 2, h / 2));
part2 = ImageUtils.imageToWritable(image.getSubimage(w / 2, 0, w / 2, h / 2));
part3 = ImageUtils.imageToWritable(image.getSubimage(0, h / 2, w / 2, h / 2));
part4 = ImageUtils.imageToWritable(image.getSubimage(w / 2, h / 2, w / 2, h / 2));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
glyph1 = Resources.createGlyph(
Key.key("msb3", "banner/" + identifier + "/1"),
part1,
GlyphPositions.guiBackground.height() / 2,
GlyphPositions.guiBackground.topPartsY()
);
glyph2 = Resources.createGlyph(
Key.key("msb3", "banner/" + identifier + "/2"),
part2,
GlyphPositions.guiBackground.height() / 2,
GlyphPositions.guiBackground.topPartsY()
);
glyph3 = Resources.createGlyph(
Key.key("msb3", "banner/" + identifier + "/3"),
part3,
GlyphPositions.guiBackground.height() / 2,
GlyphPositions.guiBackground.bottomPartsY()
);
glyph4 = Resources.createGlyph(
Key.key("msb3", "banner/" + identifier + "/4"),
part4,
GlyphPositions.guiBackground.height() / 2,
GlyphPositions.guiBackground.bottomPartsY()
);
}
public TextComponent render() {
var builder = new GlyphComponentBuilder();
builder.append(CHEST_GUI_WIDTH / 2 + 2 - glyph1.width(), glyph1);
builder.append(CHEST_GUI_WIDTH / 2 + 1, glyph2);
builder.append(CHEST_GUI_WIDTH / 2 + 2 - glyph3.width(), glyph3);
builder.append(CHEST_GUI_WIDTH / 2 + 1, glyph4);
return builder.build();
}
public void show(Player player) {
var inv = new Inventory(InventoryType.CHEST_6_ROW, render());
player.openInventory(inv);
}
}

View File

@ -0,0 +1,32 @@
package ru.dragonestia.msb3.api.ui;
import lombok.experimental.UtilityClass;
import ru.dragonestia.msb3.api.ui.dialogue.DialogueTheme;
import ru.dragonestia.msb3.api.ui.monologue.MonologueTheme;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@UtilityClass
public class TalksThemes {
private final Map<String, MonologueTheme> monologueThemes = new HashMap<>();
private final Map<String, DialogueTheme> dialogueThemes = new HashMap<>();
public void registerMonologueTheme(String identifier, MonologueTheme theme) {
monologueThemes.put(identifier, theme);
}
public Optional<MonologueTheme> getMonologueTheme(String identifier) {
return Optional.ofNullable(monologueThemes.get(identifier));
}
public void registerDialogueTheme(String identifier, DialogueTheme theme) {
dialogueThemes.put(identifier, theme);
}
public Optional<DialogueTheme> getDialogueTheme(String identifier) {
return Optional.ofNullable(dialogueThemes.get(identifier));
}
}

View File

@ -0,0 +1,44 @@
package ru.dragonestia.msb3.api.ui.bossbar;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.player.PlayerContext;
import ru.dragonestia.msb3.api.player.defaults.KeyedBossBarContext;
import ru.dragonestia.msb3.api.util.ResourceFromJar;
import ru.dragonestia.msb3.resource.Resources;
@UtilityClass
public class KeyedBossBars {
static {
var rp = Resources.getResourcePack();
rp.texture(Key.key("minecraft", "gui/sprites/boss_bar/white_background.png"),
ResourceFromJar.of("glyphs/navigator/notched_20_background.png"));
rp.texture(Key.key("minecraft", "gui/sprites/boss_bar/white_progress.png"),
ResourceFromJar.of("glyphs/navigator/notched_20_progress.png"));
}
public void hideAll(Player player) {
context(player).hideAll();
}
public void hide(Player player, String bossBarId) {
context(player).hide(bossBarId);
}
public BossBar show(Player player, String bossBarId, BossBar bossBar) {
return context(player).show(bossBarId, bossBar);
}
public BossBar showText(Player player, String bossBarId, Component text) {
return show(player, bossBarId, BossBar.bossBar(text, 1, BossBar.Color.WHITE, BossBar.Overlay.PROGRESS));
}
private KeyedBossBarContext context(Player player) {
return PlayerContext.of(player, KeyedBossBarContext.class);
}
}

View File

@ -0,0 +1,6 @@
package ru.dragonestia.msb3.api.ui.dialogue;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.resource.dialog.ButtonNumber;
public record AnswerClickContext(Player player, DialogueRenderer renderer, ButtonNumber buttonNumber, String buttonText) {}

View File

@ -0,0 +1,513 @@
package ru.dragonestia.msb3.api.ui.dialogue;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.minestom.server.entity.Player;
import net.minestom.server.event.EventListener;
import net.minestom.server.event.inventory.InventoryCloseEvent;
import net.minestom.server.event.inventory.InventoryPreClickEvent;
import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.InventoryType;
import net.minestom.server.item.ItemStack;
import net.minestom.server.tag.Tag;
import org.apache.commons.text.WordUtils;
import ru.dragonestia.msb3.api.item.BlankSlotItem;
import ru.dragonestia.msb3.api.item.ItemUtil;
import ru.dragonestia.msb3.api.resource.dialog.ButtonNumber;
import ru.dragonestia.msb3.api.resource.dialog.GlyphPositions;
import ru.dragonestia.msb3.api.resource.dialog.TextureProperties;
import ru.dragonestia.msb3.api.util.DebugMessage;
import ru.dragonestia.msb3.api.util.StringUtil;
import ru.dragonestia.msb3.resource.glyph.GlyphComponent;
import ru.dragonestia.msb3.resource.glyph.GlyphComponentBuilder;
import ru.dragonestia.msb3.resource.glyph.MinecraftFont;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
@Log4j2
public class DialogueRenderer extends Inventory {
public static final int CHEST_GUI_WIDTH = 176;
private static final Tag<String> TAG_CLICK_TYPE = Tag.String("msb3_dialogue_click_type");
private final Player player;
@Getter @Setter private DialogueTheme theme;
@Getter private String text = "";
private int shifted = 0;
private int maxLines = 0;
private final EnumMap<ButtonNumber, String> buttonsText = new EnumMap<>(ButtonNumber.class);
private final EnumMap<ButtonNumber, Consumer<AnswerClickContext>> onAnswerClick = new EnumMap<>(ButtonNumber.class);
private ItemStack[] itemBackup;
private EventListener<InventoryCloseEvent> onCloseListener;
private EventListener<InventoryPreClickEvent> onClickListener;
@Getter @Setter private OnCloseDialog onClose;
private final Object lockButtonExecution = new Object();
private volatile boolean lockedExecution = false;
public DialogueRenderer(Player player, DialogueTheme theme) {
super(InventoryType.CHEST_6_ROW, Component.empty());
this.player = player;
this.theme = theme;
}
public void setText(String text) {
this.text = text;
this.shifted = 0;
}
public void scrollUp() {
if (shifted > 0) {
shifted = Math.max(0, shifted - 3);
}
}
public void scrollDown() {
if (maxLines > shifted + GlyphPositions.phraseText.maxLines()) {
shifted = Math.min(maxLines - GlyphPositions.phraseText.maxLines(), shifted + 3);
}
}
public void rerender() {
setTitle(render());
var slots = GlyphPositions.chestSlotsButtons;
for (var slot: slots.scrollUp()) {
if (shifted != 0) {
setSlotClickable(slot, null, ClickType.SCROLL_UP);
} else {
resetClickableSlot(slot);
}
}
for (var slot: slots.scrollDown()) {
if (maxLines > shifted + GlyphPositions.phraseText.maxLines()) {
setSlotClickable(slot, null, ClickType.SCROLL_DOWN);
} else {
resetClickableSlot(slot);
}
}
for (var slot: slots.answer1()) {
if (buttonsText.containsKey(ButtonNumber.BUTTON_1)) {
setSlotClickable(slot, splitAnswerText(buttonsText.get(ButtonNumber.BUTTON_1)), ClickType.ANSWER_1);
} else {
resetClickableSlot(slot);
}
}
for (var slot: slots.answer2()) {
if (buttonsText.containsKey(ButtonNumber.BUTTON_2)) {
setSlotClickable(slot, splitAnswerText(buttonsText.get(ButtonNumber.BUTTON_2)), ClickType.ANSWER_2);
} else {
resetClickableSlot(slot);
}
}
for (var slot: slots.answer3()) {
if (buttonsText.containsKey(ButtonNumber.BUTTON_3)) {
setSlotClickable(slot, splitAnswerText(buttonsText.get(ButtonNumber.BUTTON_3)), ClickType.ANSWER_3);
} else {
resetClickableSlot(slot);
}
}
for (var slot: slots.answer4()) {
if (buttonsText.containsKey(ButtonNumber.BUTTON_4)) {
setSlotClickable(slot, splitAnswerText(buttonsText.get(ButtonNumber.BUTTON_4)), ClickType.ANSWER_4);
} else {
resetClickableSlot(slot);
}
}
}
private void resetClickableSlot(int slot) {
if (slot < getSize()) {
setItemStack(slot, ItemStack.AIR);
} else {
player.getInventory().setItemStack(slot - getSize(), ItemStack.AIR);
}
}
private void setSlotClickable(int slot, List<Component> component, ClickType clickType) {
var item = BlankSlotItem.getItem().builder()
.set(ItemUtil.TAG_UNDROPPABLE, true)
.set(TAG_CLICK_TYPE, clickType.name());
Component text = null;
switch (clickType) {
case SCROLL_UP -> text = getTheme().getTextScrollUp();
case SCROLL_DOWN -> text = getTheme().getTextScrollDown();
case ANSWER_1, ANSWER_2, ANSWER_3, ANSWER_4 -> {
text = getTheme().getTextPrefixAnswer();
for (var line: component) {
text = text.append(line);
}
}
}
var lines = new ArrayList<>(text.children());
if (lines.isEmpty()) throw new IllegalStateException("Button have zero lines");
item.customName(lines.removeFirst());
if (!lines.isEmpty()) item.lore(lines);
if (slot < getSize()) {
setItemStack(slot, item.build());
} else {
player.getInventory().setItemStack(slot - getSize(), item.build());
}
}
public void show() {
try {
player.openInventory(this);
onOpen();
} catch (Exception ex) {
onClose(false);
player.sendMessage(Component.text("[ERROR] " + ex.getMessage(), NamedTextColor.RED));
log.error(ex.getMessage(), ex);
throw new RuntimeException(ex);
}
}
public void close(boolean closedByPlayer) {
onClose(closedByPlayer);
}
private void onOpen() {
synchronized (this) {
itemBackup = player.getInventory().getItemStacks();
player.getInventory().clear();
rerender();
onCloseListener = EventListener.of(InventoryCloseEvent.class, event -> {
if (event.getInventory() instanceof DialogueRenderer renderer) {
renderer.close(true);
}
});
player.eventNode().addListener(onCloseListener);
onClickListener = EventListener.of(InventoryPreClickEvent.class, event -> {
var player = event.getPlayer();
var item = event.getClickedItem();
event.setCancelled(true);
if (item.isAir()) return;
var type = ClickType.valueOf(item.getTag(TAG_CLICK_TYPE));
switch (type) {
case SCROLL_UP -> {
scrollUp();
rerender();
getTheme().getOnScrollText().accept(player);
}
case SCROLL_DOWN -> {
scrollDown();
rerender();
getTheme().getOnScrollText().accept(player);
}
case ANSWER_1, ANSWER_2, ANSWER_3, ANSWER_4 -> {
if (tryLockButtonActionExecution()) {
try {
var buttonNumber = type.buttonNumber;
if (onAnswerClick.containsKey(buttonNumber)) {
var ctx = new AnswerClickContext(
player,
this,
buttonNumber,
buttonsText.get(buttonNumber)
);
onAnswerClick.get(buttonNumber).accept(ctx);
}
} catch (Exception ex) {
DebugMessage.sendError(player, "Во время обработки нажатия кнопки произошла ошибка: " + ex.getMessage());
log.error(ex.getMessage(), ex);
} finally {
unlockButtonActionExecution();
}
}
}
}
});
player.eventNode().addListener(onClickListener);
}
}
private void onClose(boolean closedByPlayer) {
synchronized (this) {
if (onCloseListener == null) return;
if (itemBackup != null) {
var inv = player.getInventory();
for (int i = 0; i < itemBackup.length; i++) {
inv.setItemStack(i, itemBackup[i]);
}
itemBackup = null;
}
if (onCloseListener != null) {
player.eventNode().removeListener(onCloseListener);
onCloseListener = null;
}
if (onClickListener != null) {
player.eventNode().removeListener(onClickListener);
onClickListener = null;
}
if (!closedByPlayer) player.closeInventory();
}
if (onClose != null) onClose.onClose(closedByPlayer);
}
private Component render() {
var builder = new GlyphComponentBuilder();
// GUI background
builder.append(CHEST_GUI_WIDTH / 2 + 2 - theme.getBackground().part1().width(), theme.getBackground().part1());
builder.append(CHEST_GUI_WIDTH / 2 + 1, theme.getBackground().part2());
builder.append(CHEST_GUI_WIDTH / 2 + 2 - theme.getBackground().part3().width(), theme.getBackground().part3());
builder.append(CHEST_GUI_WIDTH / 2 + 1, theme.getBackground().part4());
// Phrase substrate
builder.append(GlyphPositions.phraseSubstrate.x(), theme.getSubstrate().part1());
builder.append(GlyphPositions.phraseSubstrate.x() + GlyphPositions.phraseSubstrate.width() / 2, theme.getSubstrate().part2());
// Frame and avatar
builder.append(GlyphPositions.avatar.x() - 8, theme.getAvatarFrame());
builder.append(GlyphPositions.avatar.x(), theme.getAvatar());
// Phrase text
renderText(builder, shifted);
// Scroll phrase buttons (optional)
if (shifted != 0) {
builder.append(
GlyphPositions.scrollPhraseButton.scrollUpButtonX(),
theme.getScrollTextUp()
);
}
if (maxLines > shifted + GlyphPositions.phraseText.maxLines()) {
builder.append(
GlyphPositions.scrollPhraseButton.scrollDownButtonX(),
theme.getScrollTextDown()
);
}
// Answers
for (var number: ButtonNumber.values()) {
if (buttonsText.containsKey(number)) {
builder.append(number.getIndex() % 2 == 0
? GlyphPositions.answerField.leftFieldX()
: GlyphPositions.answerField.rightFieldX(),
switch (number) {
case BUTTON_1 -> theme.getActiveTextField1();
case BUTTON_2 -> theme.getActiveTextField2();
case BUTTON_3 -> theme.getActiveTextField3();
case BUTTON_4 -> theme.getActiveTextField4();
});
builder.append(number.getIndex() % 2 == 0
? GlyphPositions.answerButton.leftButtonX()
: GlyphPositions.answerButton.rightButtonX(),
switch (number) {
case BUTTON_1 -> theme.getButton1();
case BUTTON_2 -> theme.getButton2();
case BUTTON_3 -> theme.getButton3();
case BUTTON_4 -> theme.getButton4();
});
var lines = breakIntoLines(
buttonsText.get(number),
number.getIndex() < 2
? new TextureProperties(GlyphPositions.answerText.fontHeight(), GlyphPositions.answerText.topFirstLineAscent())
: new TextureProperties(GlyphPositions.answerText.fontHeight(), GlyphPositions.answerText.bottomFirstLineAscent()),
GlyphPositions.answerText.fontHeight(),
GlyphPositions.answerText.lineWidth());
for (int lineIdx = 0; lineIdx < Math.min(lines.size(), GlyphPositions.answerText.maxLines()); lineIdx++) {
boolean endWithDots = lineIdx + 1 == GlyphPositions.answerText.maxLines() && lineIdx + 1 != lines.size();
var line = lines.get(lineIdx);
builder.setColorTo(switch (number) {
case BUTTON_1 -> theme.getColorAnswerText1();
case BUTTON_2 -> theme.getColorAnswerText2();
case BUTTON_3 -> theme.getColorAnswerText3();
case BUTTON_4 -> theme.getColorAnswerText4();
}).append(
number.getIndex() % 2 == 0
? GlyphPositions.answerText.leftLineX()
: GlyphPositions.answerText.rightLineX(),
line.toGlyphList(line.textureProperties(), 0, endWithDots)
).resetColor();
}
} else {
builder.append(number.getIndex() % 2 == 0
? GlyphPositions.answerField.leftFieldX()
: GlyphPositions.answerField.rightFieldX(),
switch (number) {
case BUTTON_1 -> theme.getNotActiveTextField1();
case BUTTON_2 -> theme.getNotActiveTextField2();
case BUTTON_3 -> theme.getNotActiveTextField3();
case BUTTON_4 -> theme.getNotActiveTextField4();
});
}
}
return builder.build();
}
private List<TextLine> breakIntoLines(String input, TextureProperties firstLineProperties, int fontHeight, int maxWidth) throws IllegalStateException {
var resolvedText = MiniMessage.miniMessage().deserialize(input);
var lines = StringUtil.splitIntoParts(resolvedText, maxWidth, line -> {
int width = 0;
width += MinecraftFont.translate(firstLineProperties.height(), firstLineProperties.ascent(), line).width();
return width;
});
if (lines == null) {
throw new IllegalStateException("Cannot fit text into dialog");
}
var textLines = new ArrayList<TextLine>();
for (int lineIdx = 0; lineIdx < lines.length; lineIdx++) {
var text = lines[lineIdx];
var properties = new TextureProperties(firstLineProperties.height(), firstLineProperties.ascent() - lineIdx * (fontHeight + 1));
textLines.add(new TextLine(theme, ((TextComponent) text).content(), properties));
}
return textLines;
}
private void renderText(GlyphComponentBuilder builder, int shift) {
var textLines = breakIntoLines(
text,
GlyphPositions.phraseText.firstLineProperties(),
GlyphPositions.phraseText.fontHeight(),
GlyphPositions.phraseText.lineWidth());
maxLines = textLines.size();
for (int lineIdx = shift; (lineIdx - shift) < Math.min(textLines.size() - shift, GlyphPositions.phraseText.maxLines()); lineIdx++) {
var line = textLines.get(lineIdx);
builder.setColorTo(getTheme().getColorText())
.append(GlyphPositions.phraseText.lineX(), line.toGlyphList(
line.textureProperties(),
shift,
textLines.size() - lineIdx > 0 && lineIdx - shift == GlyphPositions.phraseText.maxLines() - 1
)).resetColor();
}
}
private List<Component> splitAnswerText(String text) {
var style = Style.style()
.decoration(TextDecoration.ITALIC, false)
.color(getTheme().getColorAnswerItemText())
.build();
var lines = new ArrayList<Component>();
for (var line: text.split("\n")) {
for (var subLine: WordUtils.wrap(line, 45, "\n", false).split("\n")) {
lines.add(Component.text()
.resetStyle()
.append(Component.text(subLine, style))
.build());
}
}
return lines;
}
private boolean tryLockButtonActionExecution() {
synchronized (lockButtonExecution) {
if (lockedExecution) return false; // fail
lockedExecution = true;
return true; // success
}
}
private void unlockButtonActionExecution() {
synchronized (lockButtonExecution) {
lockedExecution = false;
}
}
public void removeButton(ButtonNumber buttonNumber) {
setButton(buttonNumber, null, null);
}
public void setButton(ButtonNumber buttonNumber, String text, Consumer<AnswerClickContext> onClick) {
if (text == null) {
buttonsText.remove(buttonNumber);
onAnswerClick.remove(buttonNumber);
return;
}
buttonsText.put(buttonNumber, text);
onAnswerClick.put(buttonNumber, onClick);
}
public static Optional<DialogueRenderer> getRenderer(Player player) {
var inv = player.getOpenInventory();
if (inv == null) return Optional.empty();
if (inv instanceof DialogueRenderer renderer) {
return Optional.of(renderer);
}
return Optional.empty();
}
private record TextLine(DialogueTheme theme, String text, TextureProperties textureProperties) {
public GlyphComponent toGlyphList(TextureProperties parentProperties, int lineShift, boolean cutEnding) {
var properties = new TextureProperties(
parentProperties.height(),
parentProperties.ascent() + lineShift * (textureProperties().height() + 1)
);
Component text = Component.text(this.text);
if (cutEnding) text = StringUtil.cutEnding(text, " <...>");
return toList(((TextComponent) text).content(), properties);
}
private MinecraftFont.Text toList(String text, TextureProperties properties) {
return MinecraftFont.translate(properties.height(), properties.ascent(), text);
}
}
@RequiredArgsConstructor
private enum ClickType {
ANSWER_1(ButtonNumber.BUTTON_1),
ANSWER_2(ButtonNumber.BUTTON_2),
ANSWER_3(ButtonNumber.BUTTON_3),
ANSWER_4(ButtonNumber.BUTTON_4),
SCROLL_UP(null),
SCROLL_DOWN(null);
private final ButtonNumber buttonNumber;
}
}

Some files were not shown because too many files have changed in this diff Show More