forked from SteamWar/SteamWar
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e07298e748 | ||
|
|
d7ba19049a | ||
|
|
47909b6f07 | ||
|
|
669235709e | ||
|
|
796e154c1c | ||
|
|
9efe35fe0d | ||
|
|
e0aaacd66a | ||
|
|
27877938bc
|
+1
-1
@@ -57,7 +57,7 @@ public class WorldEditListener implements Listener {
|
||||
|
||||
private static final Set<String> commands = new HashSet<>();
|
||||
private static final Set<String> commandExclusions = new HashSet<>();
|
||||
private static final String[] shortcutCommands = {"//1", "//2", "//90", "//-90", "//180", "//p", "//c", "//flopy", "//floppy", "//flopyp", "//floppyp", "//u", "//r"};
|
||||
private static final String[] shortcutCommands = {"//1", "//2", "//90", "//-90", "//180", "//p", "//c", "//flopy", "//floppy", "//flopyp", "//floppyp", "//u", "//r", "//download", "/download"};
|
||||
|
||||
public static boolean isWorldEditCommand(String command) {
|
||||
for (String shortcut : shortcutCommands) {
|
||||
|
||||
@@ -18,6 +18,7 @@ dependencies {
|
||||
implementation("com.github.ajalt.clikt:clikt:5.0.3")
|
||||
implementation("com.github.ajalt.mordant:mordant:3.0.2")
|
||||
implementation(libs.logback)
|
||||
implementation("org.yaml:snakeyaml:2.2")
|
||||
implementation("org.mariadb.jdbc:mariadb-java-client:3.3.1")
|
||||
|
||||
implementation(libs.exposedCore)
|
||||
|
||||
@@ -14,9 +14,11 @@ import com.github.ajalt.clikt.parameters.types.file
|
||||
import com.github.ajalt.clikt.parameters.types.long
|
||||
import com.github.ajalt.clikt.parameters.types.path
|
||||
import com.sun.security.auth.module.UnixSystem
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
import java.io.File
|
||||
import kotlin.io.path.absolute
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.random.Random
|
||||
|
||||
const val LOG4J_CONFIG = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="WARN" packages="com.mojang.util">
|
||||
@@ -69,32 +71,42 @@ class DevCommand : CliktCommand("dev") {
|
||||
override fun run() {
|
||||
val args = mutableListOf<String>()
|
||||
|
||||
val serverDirectory = File(workingDir, server)
|
||||
val serverDir =
|
||||
if (serverDirectory.exists() && serverDirectory.isDirectory) serverDirectory else File(workingDir, server)
|
||||
var serverDir = resolveServerDirectory(server)
|
||||
|
||||
if (isVelocity(server)) {
|
||||
runServer(
|
||||
args, jvmArgs, listOf(
|
||||
jar?.absolutePath
|
||||
?: File("/jar/Velocity.jar").absolutePath
|
||||
?: File("/jars/Velocity.jar").absolutePath
|
||||
), serverDir
|
||||
)
|
||||
} else {
|
||||
setLogConfig(args)
|
||||
val version = findVersion(server)
|
||||
?: throw CliktError("Unknown Server Version")
|
||||
val gameModeTemplate = if (serverDir.isDirectory) null else loadGameModeTemplate(server)
|
||||
if (gameModeTemplate != null) {
|
||||
serverDir = gameModeTemplate.serverDir
|
||||
args += "-Dconfig=$server.yml"
|
||||
}
|
||||
val worldFile = world?.absolute()?.toFile()
|
||||
?: File(serverDir, "devtempworld")
|
||||
val jarFile = jar?.absolutePath
|
||||
?: File(workingDir, "devtempworld")
|
||||
var jarFile = jar?.absolutePath
|
||||
?: additionalVersions[server]?.let { supportedVersionJars[it] }
|
||||
?: supportedVersionJars[version]
|
||||
?: throw CliktError("Unknown Server Version")
|
||||
if (gameModeTemplate != null) {
|
||||
jarFile = if (gameModeTemplate.spigot) {
|
||||
jarFile.replace("paper", "spigot")
|
||||
} else {
|
||||
jarFile.replace("spigot", "paper")
|
||||
}
|
||||
}
|
||||
|
||||
if (!worldFile.exists()) {
|
||||
val templateFile = File(serverDir, "Bauwelt")
|
||||
val templateFile = gameModeTemplate?.worldTemplate ?: File(serverDir, "Bauwelt")
|
||||
if (!templateFile.exists()) {
|
||||
throw CliktError("World Template not found!")
|
||||
throw CliktError("Could not find world template: ${templateFile.absolutePath}")
|
||||
}
|
||||
templateFile.copyRecursively(worldFile)
|
||||
}
|
||||
@@ -123,6 +135,12 @@ class DevCommand : CliktCommand("dev") {
|
||||
}
|
||||
}
|
||||
|
||||
data class GameModeTemplate(
|
||||
val serverDir: File,
|
||||
val worldTemplate: File,
|
||||
val spigot: Boolean
|
||||
)
|
||||
|
||||
val jvmDefaultParams = arrayOf(
|
||||
"-Xmx1G",
|
||||
"-Xgc:excessiveGCratio=80",
|
||||
@@ -165,6 +183,41 @@ class DevCommand : CliktCommand("dev") {
|
||||
fun isVelocity(server: String): Boolean =
|
||||
server.endsWith("Velocity")
|
||||
|
||||
fun resolveServerDirectory(server: String): File {
|
||||
val localServer = File(workingDir, server)
|
||||
if (localServer.isDirectory) {
|
||||
return localServer
|
||||
}
|
||||
return File("/servers", server)
|
||||
}
|
||||
|
||||
fun loadGameModeTemplate(server: String): GameModeTemplate? {
|
||||
val configFile = File("/configs/GameModes/$server.yml")
|
||||
if (!configFile.exists()) {
|
||||
throw CliktError("Server/GameMode not found")
|
||||
}
|
||||
|
||||
val document = configFile.reader().use { reader ->
|
||||
Yaml().load<Map<String, Any?>>(reader)
|
||||
} ?: throw CliktError("GameMode config is empty: ${configFile.absolutePath}")
|
||||
val serverConfig = document["Server"] as? Map<*, *>
|
||||
?: throw CliktError("GameMode config is missing Server section: ${configFile.absolutePath}")
|
||||
val folder = serverConfig["Folder"] as? String
|
||||
?: throw CliktError("GameMode config is missing Server.Folder: ${configFile.absolutePath}")
|
||||
val maps = (serverConfig["Maps"] as? List<*>)
|
||||
?.filterIsInstance<String>()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: throw CliktError("GameMode config is missing Server.Maps: ${configFile.absolutePath}")
|
||||
|
||||
val serverDir = File("/servers", folder)
|
||||
val worldTemplate = File(File(serverDir, "arenas"), maps[Random.nextInt(maps.size)])
|
||||
return GameModeTemplate(
|
||||
serverDir = serverDir,
|
||||
worldTemplate = worldTemplate,
|
||||
spigot = serverConfig["Spigot"] == true
|
||||
)
|
||||
}
|
||||
|
||||
fun setLogConfig(args: MutableList<String>) {
|
||||
args += "-DlogPath=${workingDir.absolutePath}/logs"
|
||||
args += "-Dlog4j.configurationFile=${log4jConfig.absolutePath}"
|
||||
@@ -190,4 +243,4 @@ class DevCommand : CliktCommand("dev") {
|
||||
Runtime.getRuntime().addShutdownHook(Thread { if (process.isAlive) process.destroyForcibly() })
|
||||
process.waitFor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2025 SteamWar.de-Serverteam
|
||||
* Copyright (C) 2026 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -31,17 +31,16 @@ import de.steamwar.linkage.Linked;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Tag;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.type.Dispenser;
|
||||
import org.bukkit.block.data.type.DriedGhast;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.TNTPrimed;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockDispenseEvent;
|
||||
import org.bukkit.event.block.BlockFromToEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.block.*;
|
||||
import org.bukkit.event.entity.EntityExplodeEvent;
|
||||
import org.bukkit.event.entity.FoodLevelChangeEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
@@ -258,4 +257,16 @@ public class Permanent implements Listener {
|
||||
event.setCancelled(true);
|
||||
FightSystem.getMessage().sendPrefixless("NO_BLOCK_BREAK", event.getPlayer(), ChatMessageType.ACTION_BAR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents Dried Ghast from spawning and plants from growing
|
||||
*/
|
||||
@EventHandler
|
||||
public void onBlockGrow(BlockGrowEvent event) {
|
||||
var type = event.getBlock().getType();
|
||||
|
||||
if (event.getBlock().getBlockData() instanceof DriedGhast || Tag.CROPS.isTagged(type) || Tag.SAPLINGS.isTagged(type)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@ widener {
|
||||
fromCatalog(libs.paperapi)
|
||||
}
|
||||
|
||||
tasks.register<DevServer>("DevLobby20") {
|
||||
tasks.register<DevServer>("DevLobby") {
|
||||
group = "run"
|
||||
description = "Run a 1.20 Dev Lobby"
|
||||
description = "Run a Dev Lobby"
|
||||
dependsOn(":SpigotCore:shadowJar")
|
||||
dependsOn(":LobbySystem:jar")
|
||||
template = "Lobby20"
|
||||
template = "Lobby21"
|
||||
worldName = "Lobby"
|
||||
}
|
||||
|
||||
@@ -59,8 +59,6 @@ dependencies {
|
||||
compileOnly(libs.netty)
|
||||
compileOnly(libs.brigadier)
|
||||
compileOnly(libs.fastutil)
|
||||
|
||||
implementation(libs.anvilgui)
|
||||
}
|
||||
|
||||
widener {
|
||||
|
||||
@@ -0,0 +1,890 @@
|
||||
package net.wesjd.anvilgui;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import net.wesjd.anvilgui.version.VersionWrapper;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.*;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* An anvil gui, used for gathering a user's input
|
||||
*
|
||||
* @author Wesley Smith
|
||||
* @since 1.0
|
||||
*/
|
||||
public class AnvilGUI {
|
||||
|
||||
/**
|
||||
* The local {@link VersionWrapper} object for the server's version
|
||||
*/
|
||||
private static final VersionWrapper WRAPPER = VersionWrapper.INSTANCE;
|
||||
|
||||
/**
|
||||
* The variable containing an item with air. Used when the item would be null.
|
||||
* To keep the heap clean, this object only gets iniziaised once
|
||||
*/
|
||||
private static final ItemStack AIR = new ItemStack(Material.AIR);
|
||||
/**
|
||||
* If the given ItemStack is null, return an air ItemStack, otherwise return the given ItemStack
|
||||
*
|
||||
* @param stack The ItemStack to check
|
||||
* @return air or the given ItemStack
|
||||
*/
|
||||
private static ItemStack itemNotNull(ItemStack stack) {
|
||||
return stack == null ? AIR : stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link Plugin} that this anvil GUI is associated with
|
||||
*/
|
||||
private final Plugin plugin;
|
||||
/**
|
||||
* The player who has the GUI open
|
||||
*/
|
||||
private final Player player;
|
||||
/**
|
||||
* An {@link Executor} that executes tasks on the main server thread
|
||||
*/
|
||||
private final Executor mainThreadExecutor;
|
||||
/**
|
||||
* The title of the anvil inventory
|
||||
*/
|
||||
private final Object titleComponent;
|
||||
/**
|
||||
* The initial contents of the inventory
|
||||
*/
|
||||
private final ItemStack[] initialContents;
|
||||
/**
|
||||
* A state that decides where the anvil GUI is able to get closed by the user
|
||||
*/
|
||||
private final boolean preventClose;
|
||||
|
||||
/**
|
||||
* A set of slot numbers that are permitted to be interacted with by the user. An interactable
|
||||
* slot is one that is able to be minipulated by the player, i.e. clicking and picking up an item,
|
||||
* placing in a new one, etc.
|
||||
*/
|
||||
private final Set<Integer> interactableSlots;
|
||||
|
||||
/** An {@link Consumer} that is called when the anvil GUI is close */
|
||||
private final Consumer<StateSnapshot> closeListener;
|
||||
/** A flag that decides whether the async click handler can be run concurrently */
|
||||
private final boolean concurrentClickHandlerExecution;
|
||||
/** An {@link BiFunction} that is called when a slot is clicked */
|
||||
private final ClickHandler clickHandler;
|
||||
|
||||
/**
|
||||
* The container id of the inventory, used for NMS methods
|
||||
*/
|
||||
private int containerId;
|
||||
|
||||
/**
|
||||
* The inventory that is used on the Bukkit side of things
|
||||
*/
|
||||
@Getter
|
||||
private Inventory inventory;
|
||||
/**
|
||||
* The listener holder class
|
||||
*/
|
||||
private final ListenUp listener = new ListenUp();
|
||||
|
||||
/**
|
||||
* Represents the state of the inventory being open
|
||||
*/
|
||||
private boolean open;
|
||||
|
||||
/**
|
||||
* The actual container backing the Anvil GUI
|
||||
*/
|
||||
private VersionWrapper.AnvilContainerWrapper container;
|
||||
|
||||
/**
|
||||
* Create an AnvilGUI
|
||||
*
|
||||
* @param plugin A {@link org.bukkit.plugin.java.JavaPlugin} instance
|
||||
* @param player The {@link Player} to open the inventory for
|
||||
* @param mainThreadExecutor An {@link Executor} that executes on the main server thread
|
||||
* @param titleComponent What to have the text already set to
|
||||
* @param initialContents The initial contents of the inventory
|
||||
* @param preventClose Whether to prevent the inventory from closing
|
||||
* @param closeListener A {@link Consumer} when the inventory closes
|
||||
* @param concurrentClickHandlerExecution Flag to allow concurrent execution of the click handler
|
||||
* @param clickHandler A {@link ClickHandler} that is called when the player clicks a slot
|
||||
*/
|
||||
private AnvilGUI(
|
||||
Plugin plugin,
|
||||
Player player,
|
||||
Executor mainThreadExecutor,
|
||||
Object titleComponent,
|
||||
ItemStack[] initialContents,
|
||||
boolean preventClose,
|
||||
Set<Integer> interactableSlots,
|
||||
Consumer<StateSnapshot> closeListener,
|
||||
boolean concurrentClickHandlerExecution,
|
||||
ClickHandler clickHandler) {
|
||||
this.plugin = plugin;
|
||||
this.player = player;
|
||||
this.mainThreadExecutor = mainThreadExecutor;
|
||||
this.titleComponent = titleComponent;
|
||||
this.initialContents = initialContents;
|
||||
this.preventClose = preventClose;
|
||||
this.interactableSlots = Collections.unmodifiableSet(interactableSlots);
|
||||
this.closeListener = closeListener;
|
||||
this.concurrentClickHandlerExecution = concurrentClickHandlerExecution;
|
||||
this.clickHandler = clickHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the anvil GUI
|
||||
*/
|
||||
private void openInventory() {
|
||||
Bukkit.getPluginManager().registerEvents(listener, plugin);
|
||||
|
||||
container = WRAPPER.newContainerAnvil(player, titleComponent);
|
||||
|
||||
inventory = container.getBukkitInventory();
|
||||
// We need to use setItem instead of setContents because a Minecraft ContainerAnvil
|
||||
// contains two separate inventories: the result inventory and the ingredients inventory.
|
||||
// The setContents method only updates the ingredients inventory unfortunately,
|
||||
// but setItem handles the index going into the result inventory.
|
||||
for (int i = 0; i < initialContents.length; i++) {
|
||||
inventory.setItem(i, initialContents[i]);
|
||||
}
|
||||
|
||||
containerId = WRAPPER.getNextContainerId(player, container);
|
||||
WRAPPER.handleInventoryCloseEvent(player);
|
||||
WRAPPER.sendPacketOpenWindow(player, containerId, titleComponent);
|
||||
WRAPPER.setActiveContainer(player, container);
|
||||
WRAPPER.setActiveContainerId(container, containerId);
|
||||
WRAPPER.addActiveContainerSlotListener(container, player);
|
||||
|
||||
open = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the inventory if it's open.
|
||||
*/
|
||||
public void closeInventory() {
|
||||
closeInventory(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the inventory if it's open, only sending the close inventory packets if the arg is true
|
||||
*
|
||||
* @param sendClosePacket Whether to send the close inventory event, packet, etc
|
||||
*/
|
||||
private void closeInventory(boolean sendClosePacket) {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
open = false;
|
||||
|
||||
HandlerList.unregisterAll(listener);
|
||||
|
||||
inventory = container.getBukkitInventory();
|
||||
// We need to use setItem instead of setContents because a Minecraft ContainerAnvil
|
||||
// contains two separate inventories: the result inventory and the ingredients inventory.
|
||||
// The setContents method only updates the ingredients inventory unfortunately,
|
||||
// but setItem handles the index going into the result inventory.
|
||||
for (int i = 0; i < initialContents.length; i++) {
|
||||
inventory.setItem(i, new ItemStack(Material.AIR));
|
||||
}
|
||||
|
||||
if (sendClosePacket) {
|
||||
WRAPPER.handleInventoryCloseEvent(player);
|
||||
WRAPPER.setActiveContainerDefault(player);
|
||||
WRAPPER.sendPacketCloseWindow(player, containerId);
|
||||
}
|
||||
|
||||
if (closeListener != null) {
|
||||
closeListener.accept(StateSnapshot.fromAnvilGUI(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the title of the AnvilGUI to the new one.
|
||||
*
|
||||
* @param literalTitle The title to use as literal text
|
||||
* @param preserveRenameText Whether to preserve the entered rename text
|
||||
* @throws IllegalArgumentException when literalTitle is null
|
||||
* @see Builder#title(String)
|
||||
*/
|
||||
public void setTitle(@NonNull String literalTitle, boolean preserveRenameText) {
|
||||
setTitle(WRAPPER.literalChatComponent(literalTitle), preserveRenameText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the title of the AnvilGUI to the new one.
|
||||
*
|
||||
* @param json The json used to parse into a rich chat component
|
||||
* @param preserveRenameText Whether to preserve the entered rename text
|
||||
* @throws IllegalArgumentException when json is null
|
||||
* @see Builder#jsonTitle(String)
|
||||
*/
|
||||
public void setJsonTitle(@NonNull String json, boolean preserveRenameText) {
|
||||
setTitle(WRAPPER.jsonChatComponent(json), preserveRenameText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the title of the AnvilGUI to the new one.
|
||||
*
|
||||
* @param title The title as a NMS ChatComponent
|
||||
* @param preserveRenameText Whether to preserve the entered rename text
|
||||
*/
|
||||
private void setTitle(Object title, boolean preserveRenameText) {
|
||||
String renameText = container.getRenameText();
|
||||
WRAPPER.sendPacketOpenWindow(player, containerId, title);
|
||||
if (preserveRenameText) {
|
||||
// The renameText field is marked as @Nullable in newer versions
|
||||
container.setRenameText(renameText == null ? "" : renameText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply holds the listeners for the GUI
|
||||
*/
|
||||
private class ListenUp implements Listener {
|
||||
|
||||
/**
|
||||
* Boolean storing the running status of the latest click handler to prevent double execution.
|
||||
* All accesses to this boolean will be from the main server thread, except for the rare event
|
||||
* that the plugin is disabled and the mainThreadExecutor throws an exception
|
||||
*/
|
||||
private boolean clickHandlerRunning = false;
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
if (!event.getInventory().equals(inventory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int rawSlot = event.getRawSlot();
|
||||
// ignore items dropped outside the window
|
||||
if (rawSlot == -999) return;
|
||||
|
||||
final Player clicker = (Player) event.getWhoClicked();
|
||||
final Inventory clickedInventory = event.getClickedInventory();
|
||||
|
||||
if (clickedInventory != null) {
|
||||
if (clickedInventory.equals(clicker.getInventory())) {
|
||||
// prevent players from merging items from the anvil inventory
|
||||
if (event.getClick().equals(ClickType.DOUBLE_CLICK)) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
// prevent shift moving items from players inv to the anvil inventory
|
||||
if (event.isShiftClick()) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// prevent players from swapping items in the anvil gui
|
||||
if ((event.getCursor() != null && event.getCursor().getType() != Material.AIR)
|
||||
&& !interactableSlots.contains(rawSlot)
|
||||
&& event.getClickedInventory().equals(inventory)) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (rawSlot < 3 && rawSlot >= 0 || event.getAction().equals(InventoryAction.MOVE_TO_OTHER_INVENTORY)) {
|
||||
event.setCancelled(true);
|
||||
if (clickHandlerRunning && !concurrentClickHandlerExecution) {
|
||||
// A click handler is running, don't launch another one
|
||||
return;
|
||||
}
|
||||
|
||||
final CompletableFuture<List<ResponseAction>> actionsFuture =
|
||||
clickHandler.apply(rawSlot, StateSnapshot.fromAnvilGUI(AnvilGUI.this));
|
||||
|
||||
final Consumer<List<ResponseAction>> actionsConsumer = actions -> {
|
||||
for (final ResponseAction action : actions) {
|
||||
action.accept(AnvilGUI.this, clicker);
|
||||
}
|
||||
};
|
||||
|
||||
if (actionsFuture.isDone()) {
|
||||
// Fast-path without scheduling if clickHandler is performed in sync
|
||||
// Because the future is already completed, .join() will not block the server thread
|
||||
actionsFuture.thenAccept(actionsConsumer).join();
|
||||
} else {
|
||||
clickHandlerRunning = true;
|
||||
// If the plugin is disabled and the Executor throws an exception, the exception will be passed to
|
||||
// the .handle method
|
||||
actionsFuture
|
||||
.thenAcceptAsync(actionsConsumer, mainThreadExecutor)
|
||||
.handle((results, exception) -> {
|
||||
if (exception != null) {
|
||||
plugin.getLogger()
|
||||
.log(
|
||||
Level.SEVERE,
|
||||
"An exception occurred in the AnvilGUI clickHandler",
|
||||
exception);
|
||||
}
|
||||
// Whether an exception occurred or not, set running to false
|
||||
clickHandlerRunning = false;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryDrag(InventoryDragEvent event) {
|
||||
if (event.getInventory().equals(inventory)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClose(InventoryCloseEvent event) {
|
||||
if (open && event.getInventory().equals(inventory)) {
|
||||
closeInventory(false);
|
||||
if (preventClose) {
|
||||
mainThreadExecutor.execute(AnvilGUI.this::openInventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A builder class for an {@link AnvilGUI} object */
|
||||
public static class Builder {
|
||||
|
||||
/** An {@link Executor} that executes tasks on the main server thread */
|
||||
private Executor mainThreadExecutor;
|
||||
/** An {@link Consumer} that is called when the anvil GUI is close */
|
||||
private Consumer<StateSnapshot> closeListener;
|
||||
/** A flag that decides whether the async click handler can be run concurrently */
|
||||
private boolean concurrentClickHandlerExecution = false;
|
||||
/** An {@link Function} that is called when a slot in the inventory has been clicked */
|
||||
private ClickHandler clickHandler;
|
||||
/** A state that decides where the anvil GUI is able to be closed by the user */
|
||||
private boolean preventClose = false;
|
||||
/** A set of integers containing the slot numbers that should be modifiable by the user. */
|
||||
private Set<Integer> interactableSlots = Collections.emptySet();
|
||||
/** The {@link Plugin} that this anvil GUI is associated with */
|
||||
private Plugin plugin;
|
||||
/** The text that will be displayed to the user */
|
||||
private Object titleComponent = WRAPPER.literalChatComponent("Repair & Name");
|
||||
/** The starting text on the item */
|
||||
private String itemText;
|
||||
/** An {@link ItemStack} to be put in the left input slot */
|
||||
private ItemStack itemLeft;
|
||||
/** An {@link ItemStack} to be put in the right input slot */
|
||||
private ItemStack itemRight;
|
||||
/** An {@link ItemStack} to be placed in the output slot */
|
||||
private ItemStack itemOutput;
|
||||
|
||||
/**
|
||||
* Set a custom main server thread executor. Useful for plugins targeting Folia.
|
||||
*
|
||||
* @param executor The executor to run tasks on
|
||||
* @return The {@link Builder} instance
|
||||
* @throws IllegalArgumentException when the executor is null
|
||||
*/
|
||||
public Builder mainThreadExecutor(@NonNull Executor executor) {
|
||||
this.mainThreadExecutor = executor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the closing of the anvil GUI by the user
|
||||
*
|
||||
* @return The {@link Builder} instance
|
||||
*/
|
||||
public Builder preventClose() {
|
||||
preventClose = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permit the user to modify (take items in and out) the slot numbers provided.
|
||||
*
|
||||
* @param slots A varags param for the slot numbers. You can avoid relying on magic constants by using
|
||||
* the {@link AnvilGUI.Slot} class.
|
||||
* @return The {@link Builder} instance
|
||||
*/
|
||||
public Builder interactableSlots(int... slots) {
|
||||
final Set<Integer> newValue = new HashSet<>();
|
||||
for (int slot : slots) {
|
||||
newValue.add(slot);
|
||||
}
|
||||
interactableSlots = newValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for when the inventory is closed
|
||||
*
|
||||
* @param closeListener An {@link Consumer} that is called when the anvil GUI is closed
|
||||
* @return The {@link Builder} instance
|
||||
* @throws IllegalArgumentException when the closeListener is null
|
||||
*/
|
||||
public Builder onClose(@NonNull Consumer<StateSnapshot> closeListener) {
|
||||
this.closeListener = closeListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an action when a slot is clicked in the inventory
|
||||
* <p>
|
||||
* The ClickHandler is only called when the previous execution of the ClickHandler has finished.
|
||||
* To alter this behaviour use {@link #allowConcurrentClickHandlerExecution()}
|
||||
*
|
||||
* @param clickHandler A {@link ClickHandler} that is called when the user clicks a slot. The
|
||||
* {@link Integer} is the slot number corresponding to {@link Slot}, the
|
||||
* {@link StateSnapshot} contains information about the current state of the anvil,
|
||||
* and the response is a {@link CompletableFuture} that will eventually return a
|
||||
* list of {@link ResponseAction} to execute in the order that they are supplied.
|
||||
* @return The {@link Builder} instance
|
||||
* @throws IllegalArgumentException when the function supplied is null
|
||||
*/
|
||||
public Builder onClickAsync(@NonNull ClickHandler clickHandler) {
|
||||
this.clickHandler = clickHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, the {@link #onClickAsync(ClickHandler) async click handler} will not run concurrently
|
||||
* and instead wait for the previous {@link CompletableFuture} to finish before executing it again.
|
||||
* <p>
|
||||
* If this trait is desired, it can be enabled by calling this method but may lead to inconsistent
|
||||
* behaviour if not handled properly.
|
||||
*
|
||||
* @return The {@link Builder} instance
|
||||
*/
|
||||
public Builder allowConcurrentClickHandlerExecution() {
|
||||
this.concurrentClickHandlerExecution = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an action when a slot is clicked in the inventory
|
||||
*
|
||||
* @param clickHandler A {@link BiFunction} that is called when the user clicks a slot. The
|
||||
* {@link Integer} is the slot number corresponding to {@link Slot}, the
|
||||
* {@link StateSnapshot} contains information about the current state of the anvil,
|
||||
* and the response is a list of {@link ResponseAction} to execute in the order
|
||||
* that they are supplied.
|
||||
* @return The {@link Builder} instance
|
||||
* @throws IllegalArgumentException when the function supplied is null
|
||||
*/
|
||||
public Builder onClick(@NonNull BiFunction<Integer, StateSnapshot, List<ResponseAction>> clickHandler) {
|
||||
this.clickHandler =
|
||||
(slot, stateSnapshot) -> CompletableFuture.completedFuture(clickHandler.apply(slot, stateSnapshot));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the plugin for the {@link AnvilGUI}
|
||||
*
|
||||
* @param plugin The {@link Plugin} this anvil GUI is associated with
|
||||
* @return The {@link Builder} instance
|
||||
* @throws IllegalArgumentException if the plugin is null
|
||||
*/
|
||||
public Builder plugin(@NonNull Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the initial item-text that is displayed to the user.
|
||||
* <br><br>
|
||||
* If the usage of Adventure Components is desired, you must create an item, set the displayname of it
|
||||
* and put it into the AnvilGUI via {@link #itemLeft(ItemStack)} manually.
|
||||
*
|
||||
* @param text The initial name of the item in the anvil
|
||||
* @return The {@link Builder} instance
|
||||
* @throws IllegalArgumentException if the text is null
|
||||
*/
|
||||
public Builder text(@NonNull String text) {
|
||||
this.itemText = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the AnvilGUI title that is to be displayed to the user.
|
||||
* <br>
|
||||
* The provided title will be treated as literal text.
|
||||
*
|
||||
* @param title The title that is to be displayed to the user
|
||||
* @return The {@link Builder} instance
|
||||
* @throws IllegalArgumentException if the title is null
|
||||
*/
|
||||
public Builder title(@NonNull String title) {
|
||||
this.titleComponent = WRAPPER.literalChatComponent(title);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the AnvilGUI title that is to be displayed to the user.
|
||||
* <br>
|
||||
* The provided json will be parsed into rich chat components.
|
||||
*
|
||||
* @param json The title that is to be displayed to the user
|
||||
* @return The {@link Builder} instance
|
||||
* @throws IllegalArgumentException if the title is null
|
||||
* @see net.md_5.bungee.chat.ComponentSerializer#toString(BaseComponent)
|
||||
*/
|
||||
public Builder jsonTitle(@NonNull String json) {
|
||||
this.titleComponent = WRAPPER.jsonChatComponent(json);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ItemStack} to be put in the first slot
|
||||
*
|
||||
* @param item The {@link ItemStack} to be put in the first slot
|
||||
* @return The {@link Builder} instance
|
||||
* @throws IllegalArgumentException if the {@link ItemStack} is null
|
||||
*/
|
||||
public Builder itemLeft(@NonNull ItemStack item) {
|
||||
this.itemLeft = item;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ItemStack} to be put in the second slot
|
||||
*
|
||||
* @param item The {@link ItemStack} to be put in the second slot
|
||||
* @return The {@link Builder} instance
|
||||
*/
|
||||
public Builder itemRight(ItemStack item) {
|
||||
this.itemRight = item;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ItemStack} to be put in the output slot
|
||||
*
|
||||
* @param item The {@link ItemStack} to be put in the output slot
|
||||
* @return The {@link Builder} instance
|
||||
*/
|
||||
public Builder itemOutput(ItemStack item) {
|
||||
this.itemOutput = item;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the anvil GUI and opens it for the player
|
||||
*
|
||||
* @param player The {@link Player} the anvil GUI should open for
|
||||
* @return The {@link AnvilGUI} instance from this builder
|
||||
* @throws IllegalArgumentException when the onClick function, plugin, or player is null
|
||||
*/
|
||||
public AnvilGUI open(@NonNull Player player) {
|
||||
if (plugin == null) {
|
||||
throw new IllegalStateException("An AnvilGUI plugin has not been set!");
|
||||
}
|
||||
if (clickHandler == null) {
|
||||
throw new IllegalStateException("An AnvilGUI clickHandler has not been set!");
|
||||
}
|
||||
|
||||
if (itemText != null) {
|
||||
if (itemLeft == null) {
|
||||
itemLeft = new ItemStack(Material.PAPER);
|
||||
}
|
||||
|
||||
ItemMeta paperMeta = itemLeft.getItemMeta();
|
||||
paperMeta.setDisplayName(itemText);
|
||||
itemLeft.setItemMeta(paperMeta);
|
||||
}
|
||||
|
||||
// If no executor is specified, execute all tasks with the BukkitScheduler
|
||||
if (mainThreadExecutor == null) {
|
||||
mainThreadExecutor = task -> Bukkit.getScheduler().runTask(plugin, task);
|
||||
}
|
||||
|
||||
final AnvilGUI anvilGUI = new AnvilGUI(
|
||||
plugin,
|
||||
player,
|
||||
mainThreadExecutor,
|
||||
titleComponent,
|
||||
new ItemStack[] {itemLeft, itemRight, itemOutput},
|
||||
preventClose,
|
||||
interactableSlots,
|
||||
closeListener,
|
||||
concurrentClickHandlerExecution,
|
||||
clickHandler);
|
||||
anvilGUI.openInventory();
|
||||
return anvilGUI;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler that is called when the user clicks a slot. The
|
||||
* {@link Integer} is the slot number corresponding to {@link Slot}, the
|
||||
* {@link StateSnapshot} contains information about the current state of the anvil,
|
||||
* and the response is a {@link CompletableFuture} that will eventually return a
|
||||
* list of {@link ResponseAction} to execute in the order that they are supplied.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ClickHandler extends BiFunction<Integer, StateSnapshot, CompletableFuture<List<ResponseAction>>> {}
|
||||
|
||||
/** An action to run in response to a player clicking the output slot in the GUI. This interface is public
|
||||
* and permits you, the developer, to add additional response features easily to your custom AnvilGUIs. */
|
||||
@FunctionalInterface
|
||||
public interface ResponseAction extends BiConsumer<AnvilGUI, Player> {
|
||||
|
||||
/**
|
||||
* Replace the input text box value with the provided text value.
|
||||
*
|
||||
* Before using this method, it must be verified by the caller that items are either in
|
||||
* {@link Slot#INPUT_LEFT} or {@link Slot#OUTPUT} present.
|
||||
*
|
||||
* @param text The text to write in the input box
|
||||
* @return The {@link ResponseAction} to achieve the text replacement
|
||||
* @throws IllegalArgumentException when the text is null
|
||||
* @throws IllegalStateException when the slots {@link Slot#INPUT_LEFT} and {@link Slot#OUTPUT} are <code>null</code>
|
||||
*/
|
||||
static ResponseAction replaceInputText(@NonNull String text) {
|
||||
return (anvilgui, player) -> {
|
||||
ItemStack item = anvilgui.getInventory().getItem(Slot.OUTPUT);
|
||||
if (item == null) {
|
||||
// Fallback on left input slot if player hasn't typed anything yet
|
||||
item = anvilgui.getInventory().getItem(Slot.INPUT_LEFT);
|
||||
}
|
||||
if (item == null) {
|
||||
throw new IllegalStateException(
|
||||
"replaceInputText can only be used if slots OUTPUT or INPUT_LEFT are not empty");
|
||||
}
|
||||
|
||||
final ItemStack cloned = item.clone();
|
||||
final ItemMeta meta = cloned.getItemMeta();
|
||||
meta.setDisplayName(text);
|
||||
cloned.setItemMeta(meta);
|
||||
anvilgui.getInventory().setItem(Slot.INPUT_LEFT, cloned);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the title of the AnvilGUI to the new one.
|
||||
*
|
||||
* @param literalTitle The title to use as literal text
|
||||
* @param preserveRenameText Whether to preserve the entered rename text
|
||||
* @throws IllegalArgumentException when literalTitle is null
|
||||
* @see Builder#title(String)
|
||||
*/
|
||||
static ResponseAction updateTitle(@NonNull String literalTitle, boolean preserveRenameText) {
|
||||
return (anvilGUI, player) -> anvilGUI.setTitle(literalTitle, preserveRenameText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the title of the AnvilGUI to the new one.
|
||||
*
|
||||
* @param json The json used to parse into a rich chat component
|
||||
* @param preserveRenameText Whether to preserve the entered rename text
|
||||
* @throws IllegalArgumentException when json is null
|
||||
* @see Builder#jsonTitle(String)
|
||||
*/
|
||||
static ResponseAction updateJsonTitle(@NonNull String json, boolean preserveRenameText) {
|
||||
return (anvilGUI, player) -> anvilGUI.setJsonTitle(json, preserveRenameText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open another inventory
|
||||
* @param otherInventory The inventory to open
|
||||
* @return The {@link ResponseAction} to achieve the inventory open
|
||||
* @throws IllegalArgumentException when the otherInventory is null
|
||||
*/
|
||||
static ResponseAction openInventory(@NonNull Inventory otherInventory) {
|
||||
return (anvilgui, player) -> player.openInventory(otherInventory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the AnvilGUI
|
||||
* @return The {@link ResponseAction} to achieve closing the AnvilGUI
|
||||
*/
|
||||
static ResponseAction close() {
|
||||
return (anvilgui, player) -> anvilgui.closeInventory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the provided runnable
|
||||
* @param runnable The runnable to run
|
||||
* @return The {@link ResponseAction} to achieve running the runnable
|
||||
* @throws IllegalArgumentException when the runnable is null
|
||||
*/
|
||||
static ResponseAction run(@NonNull Runnable runnable) {
|
||||
return (anvilgui, player) -> runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a response when the player clicks the output item in the anvil GUI
|
||||
* @deprecated Since 1.6.2, use {@link ResponseAction}
|
||||
*/
|
||||
@Deprecated
|
||||
public static class Response {
|
||||
/**
|
||||
* Returns an {@link Response} object for when the anvil GUI is to close
|
||||
* @return An {@link Response} object for when the anvil GUI is to display text to the user
|
||||
* @deprecated Since 1.6.2, use {@link ResponseAction#close()}
|
||||
*/
|
||||
public static List<ResponseAction> close() {
|
||||
return Arrays.asList(ResponseAction.close());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link Response} object for when the anvil GUI is to display text to the user
|
||||
*
|
||||
* @param text The text that is to be displayed to the user
|
||||
* @return A list containing the {@link ResponseAction} for legacy compat
|
||||
* @deprecated Since 1.6.2, use {@link ResponseAction#replaceInputText(String)}
|
||||
*/
|
||||
public static List<ResponseAction> text(String text) {
|
||||
return Arrays.asList(ResponseAction.replaceInputText(text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link Response} object for when the GUI should open the provided inventory
|
||||
*
|
||||
* @param inventory The inventory to open
|
||||
* @return A list containing the {@link ResponseAction} for legacy compat
|
||||
* @deprecated Since 1.6.2, use {@link ResponseAction#openInventory(Inventory)}
|
||||
*/
|
||||
public static List<ResponseAction> openInventory(Inventory inventory) {
|
||||
return Arrays.asList(ResponseAction.openInventory(inventory));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class wrapping the magic constants of slot numbers in an anvil GUI
|
||||
*/
|
||||
public static class Slot {
|
||||
|
||||
private static final int[] values = new int[] {Slot.INPUT_LEFT, Slot.INPUT_RIGHT, Slot.OUTPUT};
|
||||
|
||||
/**
|
||||
* The slot on the far left, where the first input is inserted. An {@link ItemStack} is always inserted
|
||||
* here to be renamed
|
||||
*/
|
||||
public static final int INPUT_LEFT = 0;
|
||||
/**
|
||||
* Not used, but in a real anvil you are able to put the second item you want to combine here
|
||||
*/
|
||||
public static final int INPUT_RIGHT = 1;
|
||||
/**
|
||||
* The output slot, where an item is put when two items are combined from {@link #INPUT_LEFT} and
|
||||
* {@link #INPUT_RIGHT} or {@link #INPUT_LEFT} is renamed
|
||||
*/
|
||||
public static final int OUTPUT = 2;
|
||||
|
||||
/**
|
||||
* Get all anvil slot values
|
||||
*
|
||||
* @return The array containing all possible anvil slots
|
||||
*/
|
||||
public static int[] values() {
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents a snapshot of the state of an AnvilGUI */
|
||||
public static final class StateSnapshot {
|
||||
|
||||
/**
|
||||
* Create an {@link StateSnapshot} from the current state of an {@link AnvilGUI}
|
||||
* @param anvilGUI The instance to take the snapshot of
|
||||
* @return The snapshot
|
||||
*/
|
||||
private static StateSnapshot fromAnvilGUI(AnvilGUI anvilGUI) {
|
||||
final Inventory inventory = anvilGUI.getInventory();
|
||||
return new StateSnapshot(
|
||||
itemNotNull(inventory.getItem(Slot.INPUT_LEFT)).clone(),
|
||||
itemNotNull(inventory.getItem(Slot.INPUT_RIGHT)).clone(),
|
||||
itemNotNull(inventory.getItem(Slot.OUTPUT)).clone(),
|
||||
anvilGUI.player);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ItemStack} in the anvilGui slots
|
||||
*/
|
||||
private final ItemStack leftItem, rightItem, outputItem;
|
||||
|
||||
/**
|
||||
* The {@link Player} that clicked the output slot
|
||||
*/
|
||||
private final Player player;
|
||||
|
||||
/**
|
||||
* The event parameter constructor
|
||||
* @param leftItem The left item in the combine slot of the anvilGUI
|
||||
* @param rightItem The right item in the combine slot of the anvilGUI
|
||||
* @param outputItem The item that would have been outputted, when the items would have been combined
|
||||
* @param player The player that clicked the output slot
|
||||
*/
|
||||
public StateSnapshot(ItemStack leftItem, ItemStack rightItem, ItemStack outputItem, Player player) {
|
||||
this.leftItem = leftItem;
|
||||
this.rightItem = rightItem;
|
||||
this.outputItem = outputItem;
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
/**
|
||||
* It returns the item in the left combine slot of the gui
|
||||
*
|
||||
* @return The leftItem
|
||||
*/
|
||||
public ItemStack getLeftItem() {
|
||||
return leftItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* It returns the item in the right combine slot of the gui
|
||||
*
|
||||
* @return The rightItem
|
||||
*/
|
||||
public ItemStack getRightItem() {
|
||||
return rightItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* It returns the output item that would have been the result
|
||||
* by combining the left and right one
|
||||
*
|
||||
* @return The outputItem
|
||||
*/
|
||||
public ItemStack getOutputItem() {
|
||||
return outputItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* It returns the player that clicked onto the output slot
|
||||
*
|
||||
* @return The player
|
||||
*/
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
/**
|
||||
* It returns the text the player typed into the rename field
|
||||
*
|
||||
* @return The text of the rename field
|
||||
*/
|
||||
public String getText() {
|
||||
return outputItem.hasItemMeta() ? outputItem.getItemMeta().getDisplayName() : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package net.wesjd.anvilgui.version;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.game.ClientboundContainerClosePacket;
|
||||
import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket;
|
||||
import net.minecraft.network.protocol.game.ClientboundSetExperiencePacket;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.inventory.AnvilMenu;
|
||||
import net.minecraft.world.inventory.ContainerLevelAccess;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
||||
import org.bukkit.craftbukkit.event.CraftEventFactory;
|
||||
import org.bukkit.craftbukkit.util.CraftChatMessage;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
|
||||
public final class VersionWrapper {
|
||||
public static final VersionWrapper INSTANCE = new VersionWrapper();
|
||||
|
||||
/**
|
||||
* Interface implemented by the custom NMS AnvilContainer used to interact with it directly
|
||||
*/
|
||||
public interface AnvilContainerWrapper {
|
||||
|
||||
/**
|
||||
* Retrieves the raw text that has been entered into the Anvil at the moment
|
||||
* <br><br>
|
||||
* This field is marked as public in the Minecraft AnvilContainer only from Minecraft 1.11 and upwards
|
||||
*
|
||||
* @return The raw text in the rename field
|
||||
*/
|
||||
default String getRenameText() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the provided text as the literal hovername of the item in the left input slot
|
||||
*
|
||||
* @param text The text to set
|
||||
*/
|
||||
default void setRenameText(String text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Inventory} wrapper of the NMS container
|
||||
*
|
||||
* @return The inventory of the NMS container
|
||||
*/
|
||||
Inventory getBukkitInventory();
|
||||
}
|
||||
|
||||
private static class AnvilContainer extends AnvilMenu implements AnvilContainerWrapper {
|
||||
public AnvilContainer(Player player, int containerId, Component guiTitle) {
|
||||
super(
|
||||
containerId,
|
||||
((CraftPlayer) player).getHandle().getInventory(),
|
||||
ContainerLevelAccess.create(((CraftWorld) player.getWorld()).getHandle(), new BlockPos(0, 0, 0))
|
||||
);
|
||||
this.checkReachable = false;
|
||||
setTitle(guiTitle);
|
||||
}
|
||||
|
||||
public int getContainerId() {
|
||||
return this.containerId;
|
||||
}
|
||||
|
||||
public String getRenameText() {
|
||||
return this.itemName;
|
||||
}
|
||||
|
||||
public void setRenameText(String text) {
|
||||
// If an item is present in the left input slot change its hover name to the literal text.
|
||||
Slot inputLeft = getSlot(0);
|
||||
if (inputLeft.hasItem()) {
|
||||
inputLeft.getItem()
|
||||
.set(DataComponents.CUSTOM_NAME, Component.literal(text));
|
||||
}
|
||||
}
|
||||
|
||||
public Inventory getBukkitInventory() {
|
||||
return this.getBukkitView().getTopInventory();
|
||||
}
|
||||
}
|
||||
|
||||
private int getRealNextContainerId(Player player) {
|
||||
return toNMS(player).nextContainerCounter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a {@link Player} into an NMS one
|
||||
*
|
||||
* @param player The player to be converted
|
||||
* @return the NMS EntityPlayer
|
||||
*/
|
||||
private ServerPlayer toNMS(Player player) {
|
||||
return ((CraftPlayer) player).getHandle();
|
||||
}
|
||||
|
||||
public int getNextContainerId(Player player, AnvilContainerWrapper container) {
|
||||
return ((AnvilContainer) container).getContainerId();
|
||||
}
|
||||
|
||||
public void handleInventoryCloseEvent(Player player) {
|
||||
CraftEventFactory.handleInventoryCloseEvent(toNMS(player), InventoryCloseEvent.Reason.UNKNOWN);
|
||||
toNMS(player).doCloseContainer();
|
||||
}
|
||||
|
||||
public void sendPacketOpenWindow(Player player, int containerId, Object inventoryTitle) {
|
||||
toNMS(player).connection.send(new ClientboundOpenScreenPacket(containerId, MenuType.ANVIL, (Component) inventoryTitle));
|
||||
}
|
||||
|
||||
public void sendPacketCloseWindow(Player player, int containerId) {
|
||||
toNMS(player).connection.send(new ClientboundContainerClosePacket(containerId));
|
||||
}
|
||||
|
||||
public void sendPacketExperienceChange(Player player, int experienceLevel) {
|
||||
toNMS(player).connection.send(new ClientboundSetExperiencePacket(0f, 0, experienceLevel));
|
||||
}
|
||||
|
||||
public void setActiveContainerDefault(Player player) {
|
||||
toNMS(player).containerMenu = toNMS(player).inventoryMenu;
|
||||
}
|
||||
|
||||
public void setActiveContainer(Player player, AnvilContainerWrapper container) {
|
||||
toNMS(player).containerMenu = (AnvilMenu) container;
|
||||
}
|
||||
|
||||
public void setActiveContainerId(AnvilContainerWrapper container, int containerId) {
|
||||
}
|
||||
|
||||
public void addActiveContainerSlotListener(AnvilContainerWrapper container, Player player) {
|
||||
toNMS(player).initMenu((AnvilMenu) container);
|
||||
}
|
||||
|
||||
public AnvilContainerWrapper newContainerAnvil(Player player, Object title) {
|
||||
return new AnvilContainer(player, getRealNextContainerId(player), (Component) title);
|
||||
}
|
||||
|
||||
public Object literalChatComponent(String content) {
|
||||
return Component.literal(content);
|
||||
}
|
||||
|
||||
public Object jsonChatComponent(String json) {
|
||||
return CraftChatMessage.fromJSON(json);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user