Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.data.type.ChiseledBookshelf;
Expand All @@ -19,23 +18,30 @@
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.components.CustomModelDataComponent;
import org.bukkit.plugin.Plugin;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import static java.lang.Math.*;

public class ChiseledBookshelfInteraction extends InteractionHandler implements Listener {


private final IAdvBuildService advService;
private final Map<UUID, BookshelfSession> sessions;

// Map to track which player is selecting a texture for which bookshelf
private final Map<UUID, Location> textureSelectionSessions = new HashMap<>();
// Track the GUI's current light/dark state per player (true==light, false==dark)
private final Map<UUID, Boolean> guiIsLight = new HashMap<>();

public ChiseledBookshelfInteraction(IWorldEditService worldEditService, ICoreProtectService coreProtectService, Plugin plugin, IAdvBuildService advService) {
super(worldEditService, coreProtectService, (InteractPredicate) (event) -> {
var player = event.getPlayer();
Expand All @@ -46,8 +52,8 @@ public ChiseledBookshelfInteraction(IWorldEditService worldEditService, ICorePro
},
Material.CHISELED_BOOKSHELF);

this.sessions = new HashMap<>();
this.advService = advService;
this.sessions = new HashMap<>();
Bukkit.getPluginManager().registerEvents(this, plugin);
}

Expand All @@ -60,7 +66,7 @@ public TextComponent getInteractionDescription() {
public TextComponent getInteractionUsage() {
return Component.text("If hand is empty: right click a bookshelf toggles your ").color(NamedTextColor.GRAY).append(Component.text("selecting shelf", NamedTextColor.GRAY, TextDecoration.UNDERLINED)).append(Component.text(" status.", NamedTextColor.GRAY))
.append(Component.text(" ----- ", NamedTextColor.DARK_GRAY))
.append(Component.text("If a bookshelf is selected (aka 'selecting shelf'): The scroll wheel will allow the traversal of all bookshelf options in that color.", NamedTextColor.GRAY))
.append(Component.text("If a bookshelf is selected (aka 'selecting shelf'): A menu will open in which any variant can be selected.", NamedTextColor.GRAY))
.append(Component.text(" ----- ", NamedTextColor.DARK_GRAY))
.append(Component.text("If hand is holding a bookshelf and NOT shifting: clicking on the front or back of the bookshelf will toggle between the available colors", NamedTextColor.GRAY))
.append(Component.text(" ----- ", NamedTextColor.DARK_GRAY))
Expand All @@ -81,28 +87,27 @@ public void onRightClickWithBook(PlayerInteractEvent e) {

@Override
public void onRightClickBlock(PlayerInteractEvent event) {
var session = getSession(event.getPlayer().getUniqueId());
// When hand is empty, open the visual texture-select GUI for the clicked bookshelf immediately.
if (getHandMat(event) == Material.AIR) {
if (event.getClickedBlock() == null) return;
if (!(event.getClickedBlock().getBlockData() instanceof ChiseledBookshelf clicked)) return;

if (session.isSelectingShelf()) {
event.getPlayer().sendMessage(ChatColor.LIGHT_PURPLE + "[AdvBuild] " + ChatColor.GRAY + "Bookshelf locked.");
session.setIsSelectingShelf(false, null, event.getPlayer());
} else {
event.getPlayer().sendMessage(ChatColor.LIGHT_PURPLE + "[AdvBuild] " + ChatColor.GRAY + "Bookshelf selected.");
session.setIsSelectingShelf(true, event.getClickedBlock().getLocation(), event.getPlayer());
}
// slot 0: true == dark, false == light, so isLight is the inverse of slot 0
openTextureSelectionGUI(event.getPlayer(), event.getClickedBlock().getLocation(), !clicked.isSlotOccupied(0));
event.setCancelled(true);
event.setUseInteractedBlock(org.bukkit.event.Event.Result.DENY);
event.setUseItemInHand(org.bukkit.event.Event.Result.DENY);
} else {

// When holding a bookshelf, preserve existing toggle-on-front/back behavior using session.last
if (!event.getPlayer().isSneaking()) {
if (event.getClickedBlock() == null) return;
if (!(event.getClickedBlock().getBlockData() instanceof ChiseledBookshelf clicked)) return;
if (event.getBlockFace() == clicked.getFacing() || event.getBlockFace().getOppositeFace() == clicked.getFacing()) {
event.setCancelled(true);
event.setUseInteractedBlock(org.bukkit.event.Event.Result.DENY);
event.setUseItemInHand(org.bukkit.event.Event.Result.DENY);
clicked.setSlotOccupied(0, !clicked.isSlotOccupied(0));
var session = getSession(event.getPlayer().getUniqueId());
session.setLast(clicked);
log(false, event.getPlayer(), event.getClickedBlock());
event.getClickedBlock().setBlockData(clicked, false);
Expand All @@ -115,14 +120,9 @@ public void onRightClickBlock(PlayerInteractEvent event) {
@EventHandler
public void onBlockPlace(BlockPlaceEvent e) {
if (advService.isEnabledFor(e.getPlayer().getUniqueId())) {
var session = getSession(e.getPlayer().getUniqueId());
if (session.isSelectingShelf()) {
e.getPlayer().sendMessage(ChatColor.LIGHT_PURPLE + "[AdvBuild] " + ChatColor.GRAY + "Bookshelf locked.");
session.setIsSelectingShelf(false, null, e.getPlayer());
}

// previous selection-locking removed; keep last-apply behavior
if (e.getBlock().getBlockData() instanceof ChiseledBookshelf) {
var last = session.getLast();
var last = getSession(e.getPlayer().getUniqueId()).getLast();
if (last != null) {
last.setFacing(e.getPlayer().getFacing());
log(false, e.getPlayer(), e.getBlock());
Expand All @@ -134,42 +134,103 @@ public void onBlockPlace(BlockPlaceEvent e) {
}

@EventHandler
public void onScroll(PlayerItemHeldEvent event) {
if (advService.isEnabledFor(event.getPlayer().getUniqueId())) {
var session = getSession(event.getPlayer().getUniqueId());
if (session.isSelectingShelf()) {
var current = (ChiseledBookshelf) session.getSelected().getBlock().getBlockData();
int diff = event.getNewSlot() - event.getPreviousSlot();
int shift = diff == 8 ? -1 : diff == -8 ? 1 : diff;
session.getSelected().getBlock().setBlockData(getNextState(current, shift > 0), false);
event.setCancelled(true);
public void onInventoryClick(org.bukkit.event.inventory.InventoryClickEvent event) {
Player player = (Player) event.getWhoClicked();
if (!textureSelectionSessions.containsKey(player.getUniqueId())) return;
// Use Component-based title check
if (!event.getView().title().equals(Component.text("Select Bookshelf Texture"))) return;
// Only process clicks in the GUI, not the player's own inventory
if (event.getClickedInventory() == null || event.getClickedInventory() != event.getInventory()) return;
event.setCancelled(true); // Prevent item movement
int slot = event.getRawSlot();
if (slot < 0) return;

// Allow toggling the light/dark toggle in the bottom-right (slot 53)
if (slot == 53) {
// Instead of closing/reopening the GUI, update the existing inventory in-place
UUID uuid = player.getUniqueId();
Location bookshelfLoc = textureSelectionSessions.get(uuid);
if (bookshelfLoc == null) return;

// Determine the new GUI state by flipping the tracked value; fall back to block state if not present
// Safely derive the block's current color bit (slot 0) if available
org.bukkit.block.Block b = bookshelfLoc.getBlock();
boolean defaultGuiLight = true; // fallback to light if we can't read the block
if (b.getBlockData() instanceof ChiseledBookshelf cb) {
defaultGuiLight = !cb.isSlotOccupied(0);
}
boolean currentGuiLight = guiIsLight.getOrDefault(uuid, defaultGuiLight);
boolean newGuiLight = !currentGuiLight;
guiIsLight.put(uuid, newGuiLight);

Inventory gui = event.getInventory();
// Refill the GUI items (slots 0-31 and the toggle item) using the new light/dark state
refillTextureSelectionInventory(gui, newGuiLight);
return;
}
}

@EventHandler
public void onMove(PlayerMoveEvent event) {
if (advService.isEnabledFor(event.getPlayer().getUniqueId())) {
var session = getSession(event.getPlayer().getUniqueId());
if (session.isSelectingShelf()) {
var loc = session.getSelected();
if (loc != null && hasMovedFar(event.getPlayer().getLocation(), loc)) {
event.getPlayer().sendMessage(ChatColor.LIGHT_PURPLE + "[AdvBuild] " + ChatColor.GRAY + "Bookshelf locked.");
session.setIsSelectingShelf(false, null, event.getPlayer());
}
}
// Normal texture selection slots (0-31)
if (slot >= 32) return;

Location bookshelfLoc = textureSelectionSessions.get(player.getUniqueId());
org.bukkit.block.Block block = bookshelfLoc.getBlock();
if (!(block.getBlockData() instanceof ChiseledBookshelf bookshelf)) {
player.closeInventory();
textureSelectionSessions.remove(player.getUniqueId());
guiIsLight.remove(player.getUniqueId());
return;
}
// Use the GUI's tracked light/dark state when applying the selection; if not present, fall back to block state
boolean isLight = guiIsLight.getOrDefault(player.getUniqueId(), !bookshelf.isSlotOccupied(0));
// Set slots 1-5 based on selected texture (slot is 0-31, treated as a 5-bit binary value)
boolean bit1 = (slot & 0b00001) != 0; // bit 0 (LSB)
boolean bit2 = (slot & 0b00010) != 0; // bit 1
boolean bit3 = (slot & 0b00100) != 0; // bit 2
boolean bit4 = (slot & 0b01000) != 0; // bit 3
boolean bit5 = (slot & 0b10000) != 0; // bit 4 (MSB)

bookshelf.setSlotOccupied(1, bit1);
bookshelf.setSlotOccupied(2, bit2);
bookshelf.setSlotOccupied(3, bit3);
bookshelf.setSlotOccupied(4, bit4);
bookshelf.setSlotOccupied(5, bit5);
// Preserve color bit (slot 0): slot0=true == dark, false == light; isLight was set as the inverse
bookshelf.setSlotOccupied(0, !isLight);

// Save this selection as the player's last-used bookshelf state so future placements use it
var session = getSession(player.getUniqueId());
try {
// clone to avoid holding a reference to a Block's live data
session.setLast((ChiseledBookshelf) bookshelf.clone());
} catch (ClassCastException ex) {
// fallback: set direct reference if cloning/casting fails
session.setLast(bookshelf);
}

block.setBlockData(bookshelf, false);
player.sendMessage(Component.text("[AdvBuild] ").color(NamedTextColor.LIGHT_PURPLE).append(Component.text("Bookshelf texture set to #" + (slot + 1)).color(NamedTextColor.GRAY)));
player.closeInventory();
textureSelectionSessions.remove(player.getUniqueId());
guiIsLight.remove(player.getUniqueId());
}

// Close handler: if the player closes the GUI without selecting, remove their session mapping
@EventHandler
public void onLeave(PlayerQuitEvent e) {
var session = getSession(e.getPlayer().getUniqueId());
if (session.isSelectingShelf()) session.setIsSelectingShelf(false, null, e.getPlayer());
public void onInventoryClose(InventoryCloseEvent event) {
if (!(event.getPlayer() instanceof Player player)) return;
if (!Component.text("Select Bookshelf Texture").equals(event.getView().title())) return;
if (textureSelectionSessions.containsKey(player.getUniqueId())) {
textureSelectionSessions.remove(player.getUniqueId());
guiIsLight.remove(player.getUniqueId());
}
}

private static boolean hasMovedFar(Location loc1, Location loc2) {
double dist = sqrt(pow(loc2.getX()-loc1.getX(), 2) + pow(loc2.getY()-loc1.getY(), 2) + pow(loc2.getZ()-loc1.getZ(), 2));
return abs(dist) > 8;
// Player quit cleanup: ensure the texture selection session is removed if they disconnect while selecting
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
textureSelectionSessions.remove(uuid);
guiIsLight.remove(uuid);
}

private BookshelfSession getSession(UUID uuid) {
Expand All @@ -179,35 +240,78 @@ private BookshelfSession getSession(UUID uuid) {
return sessions.get(uuid);
}

private ChiseledBookshelf getNextState(ChiseledBookshelf current, boolean forward) {
var slot1 = current.isSlotOccupied(1) ? 1 : 0;
var slot2 = current.isSlotOccupied(2) ? 1 : 0;
var slot3 = current.isSlotOccupied(3) ? 1 : 0;
var slot4 = current.isSlotOccupied(4) ? 1 : 0;
var slot5 = current.isSlotOccupied(5) ? 1 : 0;

int currentNumber = Integer.parseInt(String.format("%d%d%d%d%d", slot1, slot2, slot3, slot4, slot5), 2);
if (currentNumber == 0 && !forward) currentNumber = 32;
else if (currentNumber == 31 && forward) currentNumber = -1;

int nextNumber = currentNumber + (forward ? 1 : -1);
var binary = String.format("%s%5s", current.isSlotOccupied(0) ? 1 : 0,Integer.toBinaryString(nextNumber)).replace(' ', '0');
/**
* Opens a double chest GUI for the player to select a bookshelf texture.
* @param player The player to open the GUI for.
* @param bookshelfLocation The location of the bookshelf being edited.
* @param isLight Whether the first slot (color) is light (true) or dark (false).
*/
public void openTextureSelectionGUI(Player player, Location bookshelfLocation, boolean isLight) {
// 6 rows, 9 columns = 54 slots (double chest), but we only need 32
org.bukkit.inventory.Inventory gui = Bukkit.createInventory(null, 54, Component.text("Select Bookshelf Texture"));

// Track the session
textureSelectionSessions.put(player.getUniqueId(), bookshelfLocation);
// Track the GUI's current displayed color selection separately from the actual block - toggling should not close GUI
guiIsLight.put(player.getUniqueId(), isLight);

// Fill the inventory contents
refillTextureSelectionInventory(gui, isLight);

player.openInventory(gui);
}

current.setSlotOccupied(0, current.isSlotOccupied(0));
current.setSlotOccupied(1, binary.charAt(1) == '1');
current.setSlotOccupied(2, binary.charAt(2) == '1');
current.setSlotOccupied(3, binary.charAt(3) == '1');
current.setSlotOccupied(4, binary.charAt(4) == '1');
current.setSlotOccupied(5, binary.charAt(5) == '1');
// Helper to (re)fill an existing inventory GUI with items for the given light/dark state
private void refillTextureSelectionInventory(Inventory gui, boolean isLight) {
final int type = isLight ? 1 : 2;
final int TYPE_MULTIPLIER = 100;

for (int i = 0; i < 32; i++) {
org.bukkit.inventory.ItemStack item = new org.bukkit.inventory.ItemStack(org.bukkit.Material.CHISELED_BOOKSHELF);
org.bukkit.inventory.meta.ItemMeta meta = item.getItemMeta();
int cmd = (type * TYPE_MULTIPLIER) + i; // numeric CustomModelData for this (type,variant)
if (meta != null) {
String modelIdentifier = "chiseled_bookshelf_type" + type + "_" + i;

// Ensure the integer CustomModelData is set for resourcepack matching
meta.setCustomModelData(cmd);

// Safely update the CustomModelDataComponent string list (if component exists)
CustomModelDataComponent cmdc = meta.getCustomModelDataComponent();
java.util.List<String> existing = cmdc.getStrings();
java.util.List<String> newStrings = new java.util.ArrayList<>(existing);
newStrings.add(modelIdentifier);
cmdc.setStrings(newStrings);
meta.setCustomModelDataComponent(cmdc);

meta.displayName(Component.text("Texture #" + (i + 1)));
java.util.List<Component> loreComp = new java.util.ArrayList<>();
loreComp.add(Component.text((isLight ? "Light" : "Dark") + " variant"));
loreComp.add(Component.text("Model ID: " + cmd));
loreComp.add(Component.text(modelIdentifier));
meta.lore(loreComp);
item.setItemMeta(meta);
}
gui.setItem(i, item);
}

return current;
// Update toggle slot at bottom-right (53)
ItemStack toggle = new ItemStack(isLight ? Material.WHITE_WOOL : Material.GRAY_WOOL);
ItemMeta toggleMeta = toggle.getItemMeta();
if (toggleMeta != null) {
toggleMeta.displayName(Component.text((isLight ? "Light" : "Dark") + " bookshelf (click to toggle)"));
java.util.List<Component> lore = new java.util.ArrayList<>();
lore.add(Component.text("Toggle between light/dark base color for the textures."));
lore.add(Component.text("Current: " + (isLight ? "Light" : "Dark")));
toggleMeta.lore(lore);
toggle.setItemMeta(toggleMeta);
}
gui.setItem(53, toggle);
}

public class BookshelfSession {
public static class BookshelfSession {

private Location selected;
private ChiseledBookshelf last;
private boolean isSelectingShelf;

public BookshelfSession(ChiseledBookshelf last) {
this.last = last;
Expand All @@ -220,24 +324,6 @@ public ChiseledBookshelf getLast() {
public void setLast(ChiseledBookshelf last) {
this.last = last;
}

public Location getSelected() {
return selected;
}

public boolean isSelectingShelf() {
return isSelectingShelf;
}

public void setIsSelectingShelf(boolean hasShelfSelected, Location selected, Player player) {
this.isSelectingShelf = hasShelfSelected;
if (!hasShelfSelected && this.selected != null) {
this.last = (ChiseledBookshelf) this.selected.getBlock().getBlockData();
log(true, player, this.selected.getBlock());
} else if (hasShelfSelected) log(false, player, selected.getBlock());
this.selected = selected;
}

}

}
}