Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
<!-- Do not change unless you want different name for local builds. -->
<build.number>-LOCAL</build.number>
<!-- This allows to change between versions. -->
<build.version>1.26.0</build.version>
<build.version>1.26.1</build.version>
<!-- SonarCloud -->
<sonar.projectKey>BentoBoxWorld_AOneBlock</sonar.projectKey>
<sonar.organization>bentobox-world</sonar.organization>
Expand Down
585 changes: 461 additions & 124 deletions src/main/java/world/bentobox/aoneblock/oneblocks/OneBlocksManager.java

Large diffs are not rendered by default.

148 changes: 145 additions & 3 deletions src/main/java/world/bentobox/aoneblock/panels/AdminPhasesPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,19 @@
import java.util.List;
import java.util.Objects;

import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitTask;
import org.eclipse.jdt.annotation.Nullable;

import io.papermc.paper.event.player.AsyncChatEvent;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;

import world.bentobox.aoneblock.AOneBlock;
import world.bentobox.aoneblock.oneblocks.OneBlockPhase;
Expand All @@ -23,8 +34,8 @@
* shoves it and everything after it right and drops the held phase there;
* clicking the slot after the last phase drops it at the end. Clicking
* anywhere else, or closing the panel, puts the held phase back. Right-click
* toggles a phase on or off. Changes are saved to the phase index and applied
* immediately.
* toggles a phase on or off. Shift-left-click asks for a new phase length in
* chat. Changes are saved to the phase index and applied immediately.
*
* @author tastybento
*/
Expand All @@ -36,6 +47,7 @@ public class AdminPhasesPanel {
private static final int HAND_SLOT = 4;
private static final int ROW_START = 9;
private static final int PANEL_SIZE = 54;
private static final int LENGTH_PROMPT_TIMEOUT_SECONDS = 60;

private final AOneBlock addon;
private final User user;
Expand Down Expand Up @@ -114,13 +126,16 @@ private PanelItem phaseItem(PhaseIndexEntry entry, int displayIndex, int startBl
} else {
description.add(user.getTranslation(REF + "pick-up"));
description.add(user.getTranslation(REF + "toggle"));
description.add(user.getTranslation(REF + "set-length"));
}
return new PanelItemBuilder().icon(phaseIcon(entry))
.name(user.getTranslation(REF + "phase-name", NAME_PLACEHOLDER, entry.getName()))
.description(description)
.clickHandler((panel, u, clickType, slot) -> {
if (heldIndex >= 0) {
dropAt(displayIndex);
} else if (clickType == ClickType.SHIFT_LEFT) {
promptForLength(entry);
} else if (clickType.isRightClick()) {
toggle(entry);
} else {
Expand Down Expand Up @@ -176,10 +191,25 @@ private ItemStack phaseIcon(PhaseIndexEntry entry) {
return new ItemStack(Material.BARRIER);
}
return manager().getBlockProbs().values().stream().filter(p -> entry.equals(p.getIndexEntry()))
.map(OneBlockPhase::getIconBlock).filter(Objects::nonNull).findFirst().map(ItemStack::clone)
.map(this::loadedPhaseIcon).filter(Objects::nonNull).findFirst()
.orElseGet(() -> new ItemStack(Material.STONE));
}

/**
* @return the phase's configured icon, falling back to its first block, or
* null if it has neither
*/
private ItemStack loadedPhaseIcon(OneBlockPhase phase) {
if (phase.getIconBlock() != null) {
return phase.getIconBlock().clone();
}
if (phase.getFirstBlock() != null && phase.getFirstBlock().getMaterial() != null
&& phase.getFirstBlock().getMaterial().isItem()) {
return new ItemStack(phase.getFirstBlock().getMaterial());
}
return null;
}

/**
* Picks up the phase at this position in the index.
*/
Expand Down Expand Up @@ -226,6 +256,118 @@ void toggle(PhaseIndexEntry entry) {
persist();
}

/**
* Closes the panel and asks the admin for a new length for this phase in
* chat. The prompt shows the current length. A valid number is applied and
* the panel reopens; typing the cancel word or timing out leaves the length
* unchanged. Bukkit's conversation API is deprecated for removal, so this
* listens for the admin's next chat message directly.
*/
void promptForLength(PhaseIndexEntry entry) {
user.closeInventory();
LengthChatListener listener = new LengthChatListener(entry);
Bukkit.getPluginManager().registerEvents(listener, addon.getPlugin());
listener.timeoutTask = Bukkit.getScheduler().runTaskLater(addon.getPlugin(), listener::timeout,
20L * LENGTH_PROMPT_TIMEOUT_SECONDS);
sendLengthPrompt(entry);
}

private void sendLengthPrompt(PhaseIndexEntry entry) {
user.sendMessage(REF + "enter-length", NAME_PLACEHOLDER, entry.getName(), NUMBER_PLACEHOLDER,
String.valueOf(entry.getLength()));
}

/**
* Captures the admin's next chat message as the new phase length. Chat
* arrives off the server thread, so the input is applied on the main thread.
*/
class LengthChatListener implements Listener {

private final PhaseIndexEntry entry;
BukkitTask timeoutTask;
private boolean done;

LengthChatListener(PhaseIndexEntry entry) {
this.entry = entry;
}

@EventHandler(priority = EventPriority.LOWEST)
public void onChat(AsyncChatEvent event) {
if (!event.getPlayer().getUniqueId().equals(user.getUniqueId())) {
return;
}
event.setCancelled(true);
String input = PlainTextComponentSerializer.plainText().serialize(event.message()).trim();
Bukkit.getScheduler().runTask(addon.getPlugin(), () -> consume(input));
}

/**
* Handles one line of chat input on the main thread: cancel word keeps
* the length, a valid number applies it, anything else re-prompts.
*/
void consume(String input) {
if (done) {
return;
}
if (input.equalsIgnoreCase(user.getTranslation(REF + "cancel-word"))) {
finish();
user.sendMessage(REF + "length-cancelled");
return;
}
Integer length = parseLength(input);
if (length == null) {
user.sendMessage(REF + "invalid-length");
sendLengthPrompt(entry);
return;
}
finish();
setLength(entry, length);
}

/**
* Gives up waiting for input.
*/
void timeout() {
if (!done) {
timeoutTask = null;
finish();
user.sendMessage(REF + "length-cancelled");
}
}

private void finish() {
done = true;
HandlerList.unregisterAll(this);
if (timeoutTask != null) {
timeoutTask.cancel();
}
}
}

/**
* @return the input as a phase length, or null if it is not a whole number
* above 0
*/
@Nullable
static Integer parseLength(String input) {
if (!input.matches("\\d{1,8}")) {
return null;
}
int length = Integer.parseInt(input);
return length > 0 ? length : null;
}

/**
* Applies a new length to a phase, marks the index as holding admin-set
* lengths so reconciliation never overwrites them, then saves, reloads, and
* reopens the panel.
*/
void setLength(PhaseIndexEntry entry, int length) {
entry.setLength(length);
manager().setAdminLengths();
persist();
}

/**
* Saves the index, reloads the phases so the new order takes effect, and
* re-renders the panel from the fresh state.
Expand Down
25 changes: 17 additions & 8 deletions src/main/resources/locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,28 @@ aoneblock:
gui:
title: "&2 Phase Order"
info-title: "&f How to use"
instructions: "&7 Click a phase to pick it up, then click where it should go. Right-click toggles a phase on or off."
instructions: |-
&7 Click a phase to pick it up,
&7 then click where it should go.
&7 Right-click toggles a phase
&7 on or off.
repeat: "&7 After the last phase the count jumps to &b [number]"
phase-name: "&a [name]"
start: "&7 Starts at block &b [number]"
length: "&7 Length &b [number] &7 blocks"
start: "&7 Start: &b [number]"
length: "&7 Length: &b [number]"
disabled: "&c Disabled"
version-locked: "&c Needs Minecraft [version] or later"
pick-up: "&e Click to pick up and move"
toggle: "&e Right-click to enable or disable"
drop-here: "&e Click to drop the held phase here"
version-locked: "&c Needs Minecraft [version]+"
pick-up: "&e Click to move"
toggle: "&e Right-click to toggle"
set-length: "&e Shift-left-click to set length"
drop-here: "&e Click to drop here"
drop-at-end: "&a Drop at the end"
held: "&e Moving: [name]"
put-back: "&e Click to put it back"
put-back: "&e Click to put back"
enter-length: "&e Enter a new length in chat for &a [name] &e - it is currently &b [number] &e blocks. Type &c cancel &e to keep it."
invalid-length: "&c The length must be a whole number above 0"
length-cancelled: "&c Length unchanged"
cancel-word: "cancel"
count:
description: show the block count and phase
info: "&a You are on block &b [number] in the &a [name] phase"
Expand Down
9 changes: 9 additions & 0 deletions src/main/resources/phases_index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
# that need a newer Minecraft version are skipped without their files (and any
# serialized items inside them) ever being touched.
#
# The index is reconciled with the phases folder on every load: phase files
# dropped into the folder are added to the index automatically, and entries
# whose files are gone are repaired or removed. Custom phase files may use any
# unique top-level key (e.g. 'my_phase') - numbers are only needed by very old
# addon versions. Files with a numeric key slot in by that legacy start block;
# any other key is added at the end for you to arrange with /oba phases.
#
# Each entry:
# file: base name of the phase file in the phases folder (without .yml).
# The chest file is <file>_chests.yml.
Expand All @@ -15,6 +22,8 @@
# enabled: optional, defaults to true. Set false to leave a phase out.
# requiredMinecraftVersion: optional. The phase is skipped (taking up no
# blocks) when the server is older than this version.
# adminLengths: (top level) set automatically once lengths are edited in the
# admin GUI; stops reconciliation from recomputing lengths.
#
# After the last phase, the block count jumps to gotoAtEnd.
phases:
Expand Down
Loading
Loading