From 8b9d1fb2fb2988920a88ae112d05ba3cbb0b64ad Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 8 Sep 2026 05:39:22 -0400 Subject: [PATCH] Split jigsaw studio menus - Move each jigsaw menu kind into its own renderer class - Move workcell capacity staging into JigsawStudioWorkcellMenu - Move menu presentation helpers and adjustment math into JigsawStudioMenuFormat - Promote the menu action contract to JigsawStudioMenuActions - Point the menu controller test at the relocated menu types --- .../core/service/JigsawStudioDetailsMenu.java | 325 +++ .../core/service/JigsawStudioMainMenu.java | 286 ++ .../core/service/JigsawStudioMenuActions.java | 111 + .../service/JigsawStudioMenuController.java | 2428 +---------------- .../core/service/JigsawStudioMenuFormat.java | 477 ++++ .../core/service/JigsawStudioService.java | 6 +- .../service/JigsawStudioStructureMenu.java | 194 ++ .../core/service/JigsawStudioToolboxMenu.java | 370 +++ .../core/service/JigsawStudioVariantMenu.java | 213 ++ .../service/JigsawStudioWorkcellMenu.java | 536 ++++ .../JigsawStudioMenuControllerTest.java | 133 +- 11 files changed, 2668 insertions(+), 2411 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/core/service/JigsawStudioDetailsMenu.java create mode 100644 core/src/main/java/art/arcane/iris/core/service/JigsawStudioMainMenu.java create mode 100644 core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuActions.java create mode 100644 core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuFormat.java create mode 100644 core/src/main/java/art/arcane/iris/core/service/JigsawStudioStructureMenu.java create mode 100644 core/src/main/java/art/arcane/iris/core/service/JigsawStudioToolboxMenu.java create mode 100644 core/src/main/java/art/arcane/iris/core/service/JigsawStudioVariantMenu.java create mode 100644 core/src/main/java/art/arcane/iris/core/service/JigsawStudioWorkcellMenu.java diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioDetailsMenu.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioDetailsMenu.java new file mode 100644 index 000000000..bd3295e6b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioDetailsMenu.java @@ -0,0 +1,325 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolAction; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolPayload; +import art.arcane.volmlib.util.inventorygui.UIElement; +import art.arcane.volmlib.util.inventorygui.UIWindow; +import org.bukkit.ChatColor; +import org.bukkit.Material; + +import java.util.List; +import java.util.Objects; + +import static art.arcane.iris.core.service.JigsawStudioMenuController.CHANCE_STEP_PERCENTAGE_POINTS; +import static art.arcane.iris.core.service.JigsawStudioMenuController.GRID_POSITIONS; +import static art.arcane.iris.core.service.JigsawStudioMenuController.MEMBERSHIPS_PER_PAGE; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.chance; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.clampPage; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.dimensions; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.displayKey; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.element; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.evaluationElement; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.maximumPlacements; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.page; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.pageCount; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.safe; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.themes; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.yesNo; + +final class JigsawStudioDetailsMenu { + private final JigsawStudioMenuController controller; + + JigsawStudioDetailsMenu(JigsawStudioMenuController controller) { + this.controller = Objects.requireNonNull(controller, "Jigsaw Studio menu controller"); + } + + void render( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + int requestedPage + ) { + int page = clampPage(requestedPage, variant.memberships().size(), MEMBERSHIPS_PER_PAGE); + controller.purgeExpiredConfirmations(window.getViewer().getUniqueId()); + window.batch(() -> { + window.clearElements(); + addDetailsHeader(window, state, workcell, variant, page); + addMemberships(window, state, workcell, variant, page); + addDetailsFooter(window, state, workcell, variant, page); + }); + } + + private void addDetailsHeader( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + int page + ) { + UIElement back = element("back", Material.ARROW, ChatColor.YELLOW + "Back to Variants"); + back.onLeftClick(clicked -> controller.refreshMain( + window.getViewer(), state.requestId(), workcell.stableId(), 0)); + window.setElement(-4, 0, back); + + UIElement identity = element("identity", Material.NAME_TAG, ChatColor.AQUA + safe(variant.displayName())); + identity.addLore(ChatColor.DARK_GRAY + safe(variant.pieceKey())); + identity.addLore(variant.owned() + ? ChatColor.GREEN + "Owned variant" + : ChatColor.GRAY + "Read-only variant"); + identity.addLore(variant.active() + ? ChatColor.GREEN + "Loaded in this workcell" + : ChatColor.GRAY + "Not currently loaded"); + identity.addLore(ChatColor.GRAY + "Size: " + variant.dimensions() + .map(JigsawStudioMenuFormat::dimensions) + .orElse("Object missing")); + identity.addLore(ChatColor.GRAY + "Themes: " + themes(variant.themes())); + identity.addLore(ChatColor.GRAY + "Depth: " + variant.rules().minimumDepth() + + "-" + variant.rules().maximumDepth()); + identity.addLore(ChatColor.GRAY + "Placements: " + variant.rules().minimumPlacements() + + "-" + maximumPlacements(variant.rules().maximumPlacements())); + identity.addLore(ChatColor.GRAY + "Terminal: " + yesNo(variant.rules().terminal())); + identity.addLore(ChatColor.GRAY + "Pool entries: " + variant.memberships().size()); + if (variant.owned()) { + identity.addLore(ChatColor.YELLOW + "Left-click for a rename stick"); + identity.addLore(ChatColor.GRAY + "Rename that stick in an anvil, then right-click it."); + identity.addLore(ChatColor.GRAY + "Sneak-right-click the stick to reset this label."); + identity.onLeftClick(clicked -> controller.actions.giveTool( + window.getViewer(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.RENAME_VARIANT, + state.requestId(), + workcell.stableId(), + variant.pieceKey()))); + } + window.setElement(0, 0, identity); + + boolean activeOwned = variant.active() && variant.owned(); + boolean irisRuleEditing = activeOwned && state.irisExtended(); + UIElement settings = element( + "variant-settings", + irisRuleEditing ? Material.COMPARATOR : Material.GRAY_DYE, + irisRuleEditing + ? ChatColor.GOLD + "Themes & Piece Rules" + : ChatColor.GRAY + "Themes & Rules Unavailable"); + settings.addLore(irisRuleEditing + ? ChatColor.GRAY + "Edit this loaded variant's theme membership and placement rules." + : state.irisExtended() + ? ChatColor.GRAY + "Load an owned variant before editing its rules." + : ChatColor.GRAY + "Vanilla-portable pieces cannot encode Iris theme or rule metadata."); + if (irisRuleEditing) { + settings.addLore(ChatColor.YELLOW + "Left-click to edit"); + settings.onLeftClick(clicked -> controller.openVariantSettings( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); + } + window.setElement(-2, 0, settings); + + boolean rotationEditable = variant.active() && variant.rotationEditable(); + UIElement rotation = element( + "rotation", + rotationEditable ? Material.REPEATER : Material.BARRIER, + rotationEditable + ? ChatColor.LIGHT_PURPLE + "Toggle Rotation" + : ChatColor.GRAY + "Rotation is Read-only"); + rotation.addLore(ChatColor.GRAY + "Currently: " + (variant.rotatable() ? "Enabled" : "Disabled")); + if (rotationEditable) { + rotation.addLore(ChatColor.YELLOW + "Left-click to toggle"); + rotation.onLeftClick(clicked -> controller.toggleRotation( + window.getViewer(), state.requestId(), workcell.stableId())); + } else if (!variant.active()) { + rotation.addLore(ChatColor.GRAY + "Load this variant before editing rotation."); + } else if (variant.owned() && variant.rotatable()) { + rotation.addLore(ChatColor.GRAY + "Vanilla-portable variants must remain rotatable."); + } + window.setElement(3, 0, rotation); + + boolean sizeEditable = variant.owned() && variant.dimensions().isPresent(); + UIElement size = element( + "variant-size", + sizeEditable ? Material.SCAFFOLDING : Material.BARRIER, + sizeEditable + ? ChatColor.GREEN + "Edit This Variant's Size" + : ChatColor.GRAY + "Variant Size is Read-only"); + size.addLore(ChatColor.GRAY + "Current: " + variant.dimensions() + .map(JigsawStudioMenuFormat::dimensions) + .orElse("Object missing")); + size.addLore(ChatColor.GRAY + "Workcell capacity: " + dimensions(workcell.capacity())); + if (sizeEditable) { + size.addLore(ChatColor.YELLOW + "Left-click for width, height, and depth controls"); + size.onLeftClick(clicked -> controller.openVariantSizeSettings( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey())); + if (variant.resizableToCapacity()) { + size.addLore(ChatColor.YELLOW + "Shift-left: resize this variant to capacity"); + size.onShiftLeftClick(clicked -> controller.resizeVariant( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + workcell.capacity())); + } + } + window.setElement(2, 0, size); + + boolean lastVariant = workcell.variants().size() <= 1; + boolean deletionAvailable = variant.owned() && !variant.active() && !lastVariant; + boolean confirmed = deletionAvailable && controller.isDeleteConfirmed( + window.getViewer().getUniqueId(), + state.requestId(), + workcell.stableId(), + variant.pieceKey()); + UIElement delete = element( + "delete-variant", + deletionAvailable ? confirmed ? Material.REDSTONE : Material.LAVA_BUCKET : Material.BARRIER, + deletionAvailable + ? confirmed ? ChatColor.RED + "Confirm Delete Variant" : ChatColor.RED + "Delete Variant" + : variant.active() + ? ChatColor.GRAY + "Loaded Variant Cannot Be Deleted" + : lastVariant + ? ChatColor.GRAY + "Last Variant Cannot Be Deleted" + : ChatColor.GRAY + "Deletion is Read-only") + .setEnchanted(confirmed); + if (deletionAvailable) { + delete.addLore(confirmed + ? ChatColor.RED + "Click again to permanently delete this owned variant." + : ChatColor.RED + "Click twice within 10 seconds to confirm."); + delete.onLeftClick(clicked -> controller.confirmOrDeleteVariant( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + page)); + } else if (variant.active()) { + delete.addLore(ChatColor.GRAY + "Load another variant in this workcell first."); + } else if (lastVariant) { + delete.addLore(ChatColor.GRAY + "Create another variant before deleting this one."); + } + window.setElement(4, 0, delete); + } + + private void addMemberships( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + int page + ) { + List memberships = page( + variant.memberships(), page, MEMBERSHIPS_PER_PAGE); + if (memberships.isEmpty()) { + UIElement empty = element("no-memberships", Material.BARRIER, ChatColor.RED + "No Pool Entries"); + empty.addLore(ChatColor.GRAY + "This variant is not linked from an owned pool."); + window.setElement(0, 2, empty); + return; + } + + for (int index = 0; index < memberships.size(); index++) { + JigsawStudioMenuState.Membership membership = memberships.get(index); + int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; + int row = 1 + index / GRID_POSITIONS.length; + boolean confirmed = variant.active() && variant.owned() && controller.isUnlinkConfirmed( + window.getViewer().getUniqueId(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + membership); + UIElement element = element( + "membership-" + index, + confirmed ? Material.REDSTONE : Material.GOLD_NUGGET, + (confirmed ? ChatColor.RED : ChatColor.GOLD) + + safe(displayKey(membership.poolKey())) + + " [" + membership.entryIndex() + "]") + .setEnchanted(confirmed); + element.addLore(ChatColor.DARK_GRAY + safe(membership.poolKey())); + element.addLore(ChatColor.GRAY + "Exact entry: " + membership.entryIndex()); + element.addLore(ChatColor.WHITE + "Weight: " + membership.weight()); + element.addLore((state.irisExtended() ? ChatColor.WHITE : ChatColor.GRAY) + + "Chance: " + chance(membership.chance()) + + (state.irisExtended() ? "" : " (Iris only)")); + if (variant.active() && variant.owned()) { + element.addLore(ChatColor.GREEN + "Left-click: weight +1"); + element.addLore(ChatColor.YELLOW + "Right-click: weight -1"); + if (state.irisExtended()) { + element.addLore(ChatColor.GREEN + "Shift-left: chance +" + + CHANCE_STEP_PERCENTAGE_POINTS + "%"); + element.addLore(ChatColor.YELLOW + "Shift-right: chance -" + + CHANCE_STEP_PERCENTAGE_POINTS + "%"); + } else { + element.addLore(ChatColor.GRAY + "Vanilla-portable pool entries always have 100% chance."); + } + element.addLore(confirmed + ? ChatColor.RED + "Middle-click again to unlink" + : ChatColor.RED + "Middle-click twice to unlink"); + element.onLeftClick(clicked -> controller.adjustMembershipWeight( + window.getViewer(), state.requestId(), workcell.stableId(), membership, 1)); + element.onRightClick(clicked -> controller.adjustMembershipWeight( + window.getViewer(), state.requestId(), workcell.stableId(), membership, -1)); + if (state.irisExtended()) { + element.onShiftLeftClick(clicked -> controller.adjustMembershipChance( + window.getViewer(), + state.requestId(), + workcell.stableId(), + membership, + CHANCE_STEP_PERCENTAGE_POINTS)); + element.onShiftRightClick(clicked -> controller.adjustMembershipChance( + window.getViewer(), + state.requestId(), + workcell.stableId(), + membership, + -CHANCE_STEP_PERCENTAGE_POINTS)); + } + element.onMiddleClick(clicked -> controller.confirmOrUnlink( + window.getViewer(), state.requestId(), workcell.stableId(), membership, page)); + } else { + element.addLore(variant.active() + ? ChatColor.GRAY + "Read-only pool entry" + : ChatColor.GRAY + "Load this variant to edit its pool entry"); + } + window.setElement(position, row, element); + } + } + + private void addDetailsFooter( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + int page + ) { + UIElement back = element("footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); + back.onLeftClick(clicked -> controller.refreshMain( + window.getViewer(), state.requestId(), workcell.stableId(), 0)); + window.setElement(-4, 5, back); + + int pageCount = pageCount(variant.memberships().size(), MEMBERSHIPS_PER_PAGE); + if (page > 0) { + UIElement previous = element("membership-previous", Material.ARROW, ChatColor.YELLOW + "Previous Page"); + previous.onLeftClick(clicked -> controller.refreshDetails( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + page - 1)); + window.setElement(-1, 5, previous); + } + UIElement indicator = element("membership-page", Material.BOOK, ChatColor.WHITE + "Page " + + (page + 1) + "/" + pageCount); + indicator.addLore(ChatColor.GRAY + "" + variant.memberships().size() + " exact pool entries"); + window.setElement(0, 5, indicator); + if (page + 1 < pageCount) { + UIElement next = element("membership-next", Material.ARROW, ChatColor.YELLOW + "Next Page"); + next.onLeftClick(clicked -> controller.refreshDetails( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + page + 1)); + window.setElement(1, 5, next); + } + window.setElement(2, 5, evaluationElement(state.evaluation())); + + UIElement toolbox = element("details-toolbox", Material.STICK, ChatColor.AQUA + "Toolbox"); + toolbox.onLeftClick(clicked -> controller.openToolbox( + window.getViewer(), state.requestId(), workcell.stableId(), 0)); + window.setElement(4, 5, toolbox); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMainMenu.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMainMenu.java new file mode 100644 index 000000000..7c9f739fc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMainMenu.java @@ -0,0 +1,286 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.inventorygui.UIElement; +import art.arcane.volmlib.util.inventorygui.UIWindow; +import org.bukkit.ChatColor; +import org.bukkit.Material; + +import java.util.List; +import java.util.Objects; + +import static art.arcane.iris.core.service.JigsawStudioMenuController.GRID_POSITIONS; +import static art.arcane.iris.core.service.JigsawStudioMenuController.VARIANTS_PER_PAGE; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.clampPage; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.compatibilityName; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.dimensions; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.element; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.evaluationElement; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.nextThemeSetKey; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.page; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.pageCount; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.safe; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.themes; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.variantMaterial; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.workcellMaterial; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.workcellStatus; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.yesNo; + +final class JigsawStudioMainMenu { + private final JigsawStudioMenuController controller; + + JigsawStudioMainMenu(JigsawStudioMenuController controller) { + this.controller = Objects.requireNonNull(controller, "Jigsaw Studio menu controller"); + } + + void render( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell selected, + int requestedPage + ) { + int page = clampPage(requestedPage, selected.variants().size(), VARIANTS_PER_PAGE); + window.batch(() -> { + window.clearElements(); + addWorkcells(window, state, selected.stableId()); + addFamilyControl(window, state); + addStructureControl(window, state, selected.stableId()); + addVariants(window, state, selected, page); + addMainActions(window, state, selected, page); + }); + } + + private void addFamilyControl(UIWindow window, JigsawStudioMenuState state) { + String nextThemeKey = nextThemeSetKey(state.themeSets()); + boolean available = state.irisExtended() && state.workcells().stream() + .filter(JigsawStudioMenuState.Workcell::enabled) + .allMatch(workcell -> workcell.activeVariant() != null && workcell.activeVariant().owned()); + UIElement family = element( + "duplicate-family", + available ? Material.PURPLE_DYE : Material.GRAY_DYE, + available + ? ChatColor.LIGHT_PURPLE + "Duplicate All Enabled Cells as Family" + : ChatColor.GRAY + "Duplicate All Enabled Cells as Family"); + family.addLore(ChatColor.GRAY + "Copies each enabled cell's loaded variant into one coherent family."); + family.addLore(ChatColor.GRAY + "New family: " + safe(nextThemeKey)); + if (available) { + family.addLore(ChatColor.YELLOW + "Left-click to duplicate the complete family atomically"); + family.onLeftClick(clicked -> controller.duplicateActiveFamily( + window.getViewer(), state.requestId(), nextThemeKey)); + } else if (!state.irisExtended()) { + family.addLore(ChatColor.GRAY + "Coherent families require Iris compatibility."); + } else { + family.addLore(ChatColor.GRAY + "Load an owned variant in every enabled workcell first."); + } + window.setElement(3, 0, family); + } + + private void addStructureControl( + UIWindow window, + JigsawStudioMenuState state, + String selectedWorkcellId + ) { + UIElement structure = element( + "structure-rules", + state.irisExtended() + ? state.requireCaps() ? Material.IRON_BARS : Material.TRIPWIRE_HOOK + : Material.BARRIER, + ChatColor.LIGHT_PURPLE + "Structure Rules"); + structure.addLore(ChatColor.GRAY + "Mandatory caps: " + yesNo(state.requireCaps())); + structure.addLore(ChatColor.GRAY + "Theme sets: " + state.themeSets().size()); + structure.addLore(ChatColor.GRAY + "Compatibility: " + compatibilityName(state.compatibilityTarget())); + structure.addLore(ChatColor.YELLOW + "Left-click for themes and rules"); + structure.addLore(state.irisExtended() + ? ChatColor.YELLOW + "Right-click to toggle mandatory caps" + : ChatColor.GRAY + "Mandatory caps and themes require Iris compatibility"); + structure.onLeftClick(clicked -> controller.openStructureSettings( + window.getViewer(), state.requestId(), selectedWorkcellId, 0)); + if (state.irisExtended()) { + structure.onRightClick(clicked -> controller.setRequireCaps( + window.getViewer(), state.requestId(), !state.requireCaps())); + } + window.setElement(4, 0, structure); + } + + private void addWorkcells(UIWindow window, JigsawStudioMenuState state, String selectedWorkcellId) { + int count = state.workcells().size(); + int start = count == 1 ? 0 : -(count / 2); + for (int index = 0; index < count; index++) { + JigsawStudioMenuState.Workcell workcell = state.workcells().get(index); + boolean selected = workcell.stableId().equals(selectedWorkcellId); + UIElement element = element( + "workcell-" + index, + workcellMaterial(workcell, selected), + (selected ? ChatColor.AQUA : workcell.enabled() ? ChatColor.WHITE : ChatColor.GRAY) + + safe(workcell.displayName())) + .setEnchanted(selected); + element.addLore(workcell.enabled() + ? ChatColor.GREEN + "Enabled" + : ChatColor.RED + "Disabled for assembly and export"); + if (!workcell.canonicalName().equals(workcell.displayName())) { + element.addLore(ChatColor.GRAY + "Solver role: " + safe(workcell.canonicalName())); + } + element.addLore(ChatColor.GRAY + "Capacity: " + dimensions(workcell.capacity())); + element.addLore(ChatColor.GRAY + "Variants: " + workcell.variants().size()); + JigsawStudioMenuState.Variant active = workcell.activeVariant(); + element.addLore(ChatColor.GRAY + "Loaded: " + + (active == null ? "None" : safe(active.displayName()))); + element.addLore(workcellStatus(workcell)); + element.addLore(ChatColor.GRAY + "Connector blocks: " + + (workcell.connectorsVisible() ? "Visible" : "Hidden")); + element.addLore(ChatColor.YELLOW + "Left-click to select and teleport"); + element.addLore(ChatColor.YELLOW + "Right-click for workcell settings"); + if (workcell.dirty() && !workcell.saving()) { + element.addLore(ChatColor.GOLD + "Shift-left: Flush Autosave Now"); + element.onShiftLeftClick(clicked -> controller.flushNow( + window.getViewer(), state.requestId(), workcell.stableId())); + } + element.onLeftClick(clicked -> controller.selectWorkcell( + window.getViewer(), + state.requestId(), + workcell.stableId())); + element.onRightClick(clicked -> J.runEntity( + window.getViewer(), + () -> controller.openWorkcellSettings( + window.getViewer(), + state.requestId(), + workcell.stableId()), + 1)); + window.setElement(start + index, 0, element); + } + } + + private void addVariants( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + int page + ) { + List variants = page( + workcell.variants(), page, VARIANTS_PER_PAGE); + if (variants.isEmpty()) { + UIElement empty = element("no-variants", Material.BARRIER, ChatColor.RED + "No variants"); + empty.addLore(ChatColor.GRAY + "Create a variant to begin authoring this workcell."); + window.setElement(0, 2, empty); + return; + } + + for (int index = 0; index < variants.size(); index++) { + JigsawStudioMenuState.Variant variant = variants.get(index); + int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; + int row = 1 + index / GRID_POSITIONS.length; + UIElement element = element( + "variant-" + index, + variantMaterial(variant), + (variant.active() ? ChatColor.GREEN : ChatColor.WHITE) + + safe(variant.displayName()) + + (variant.active() ? ChatColor.AQUA + " [Loaded]" : "")) + .setEnchanted(variant.active()); + element.addLore(ChatColor.DARK_GRAY + safe(variant.pieceKey())); + element.addLore(variant.owned() + ? ChatColor.GREEN + "Owned" + : ChatColor.GRAY + "Read-only"); + element.addLore(ChatColor.GRAY + "Rotation: " + (variant.rotatable() ? "Enabled" : "Disabled")); + element.addLore(ChatColor.GRAY + "Size: " + variant.dimensions() + .map(JigsawStudioMenuFormat::dimensions) + .orElse("Object missing")); + element.addLore(ChatColor.GRAY + "Themes: " + themes(variant.themes())); + element.addLore(ChatColor.GRAY + "Pool entries: " + variant.memberships().size()); + element.addLore(variant.active() + ? ChatColor.YELLOW + "Loaded; right-click for details" + : ChatColor.YELLOW + "Left-click to load"); + if (!variant.active()) { + element.addLore(ChatColor.YELLOW + "Right-click for details or deletion"); + } + element.onLeftClick(clicked -> controller.switchVariant( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey())); + element.onRightClick(clicked -> J.runEntity( + window.getViewer(), + () -> controller.openDetails( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + 0), + 1)); + window.setElement(position, row, element); + } + } + + private void addMainActions( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + int page + ) { + UIElement create = element("create", Material.NETHER_STAR, ChatColor.GREEN + "New Blank Variant"); + create.addLore(ChatColor.GRAY + "Create an empty owned variant with this cell's current size."); + create.onLeftClick(clicked -> controller.createVariant( + window.getViewer(), state.requestId(), workcell.stableId(), false)); + window.setElement(-4, 5, create); + + JigsawStudioMenuState.Variant active = workcell.activeVariant(); + boolean duplicateAvailable = active != null && active.owned(); + UIElement duplicate = element( + "duplicate", + duplicateAvailable ? Material.PAPER : Material.GRAY_DYE, + duplicateAvailable + ? ChatColor.AQUA + "Duplicate This Cell's Variant" + : active == null + ? ChatColor.GRAY + "Duplicate This Cell's Variant" + : ChatColor.GRAY + "Duplicate This Cell's Variant"); + duplicate.addLore(duplicateAvailable + ? ChatColor.GRAY + "Clone the loaded variant into an owned variant." + : active == null + ? ChatColor.GRAY + "Load a variant before duplicating it." + : ChatColor.GRAY + "Adopt the read-only graph before duplicating it."); + if (duplicateAvailable) { + duplicate.onLeftClick(clicked -> controller.createVariant( + window.getViewer(), state.requestId(), workcell.stableId(), true)); + } + window.setElement(-3, 5, duplicate); + + UIElement settings = element("settings", Material.CRAFTING_TABLE, ChatColor.GOLD + "Workcell Settings"); + settings.addLore(ChatColor.GRAY + "Enabled: " + yesNo(workcell.enabled())); + settings.addLore(ChatColor.GRAY + "Capacity: " + dimensions(workcell.capacity())); + settings.onLeftClick(clicked -> controller.openWorkcellSettings( + window.getViewer(), state.requestId(), workcell.stableId())); + window.setElement(-2, 5, settings); + + int pageCount = pageCount(workcell.variants().size(), VARIANTS_PER_PAGE); + if (page > 0) { + UIElement previous = element("previous", Material.ARROW, ChatColor.YELLOW + "Previous Page"); + previous.onLeftClick(clicked -> controller.refreshMain( + window.getViewer(), state.requestId(), workcell.stableId(), page - 1)); + window.setElement(-1, 5, previous); + } + + UIElement pageIndicator = element("page", Material.BOOK, ChatColor.WHITE + "Page " + + (page + 1) + "/" + pageCount); + pageIndicator.addLore(ChatColor.GRAY + "" + workcell.variants().size() + " variants"); + window.setElement(0, 5, pageIndicator); + + if (page + 1 < pageCount) { + UIElement next = element("next", Material.ARROW, ChatColor.YELLOW + "Next Page"); + next.onLeftClick(clicked -> controller.refreshMain( + window.getViewer(), state.requestId(), workcell.stableId(), page + 1)); + window.setElement(1, 5, next); + } + + window.setElement(2, 5, evaluationElement(state.evaluation())); + + UIElement preview = element("preview", Material.ENDER_EYE, ChatColor.LIGHT_PURPLE + "Go to Preview"); + preview.addLore(ChatColor.GRAY + "Open the deterministic assembly preview."); + preview.onLeftClick(clicked -> controller.goToPreview(window.getViewer(), state.requestId())); + window.setElement(3, 5, preview); + + UIElement toolbox = element("toolbox", Material.STICK, ChatColor.AQUA + "Toolbox"); + toolbox.addLore(ChatColor.GRAY + "Take named sticks bound to this Studio session."); + toolbox.onLeftClick(clicked -> controller.openToolbox( + window.getViewer(), state.requestId(), workcell.stableId(), 0)); + window.setElement(4, 5, toolbox); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuActions.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuActions.java new file mode 100644 index 000000000..7a3f675cc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuActions.java @@ -0,0 +1,111 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPieceRules; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolPayload; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.Optional; + +public interface JigsawStudioMenuActions { + Optional menuState(Player player); + + boolean selectWorkcell(Player player, String workcellId); + + boolean teleportToWorkcell(Player player, String workcellId); + + boolean setConnectorBlocksVisible(Player player, String workcellId, boolean visible); + + boolean resetConnectorBlocks(Player player, String workcellId); + + boolean undoAutosave(Player player); + + boolean switchVariant(Player player, String workcellId, String pieceKey, boolean discardDirty); + + boolean createVariant(Player player, String workcellId, boolean duplicateActive); + + boolean setWorkcellEnabled(Player player, String workcellId, boolean enabled); + + boolean updateWorkcellDimensions( + Player player, + String workcellId, + JigsawStudioCellDimensions dimensions + ); + + boolean updateWorkcellDisplayName(Player player, String workcellId, String displayName); + + boolean setRequireCaps(Player player, boolean requireCaps); + + boolean duplicateActiveFamily(Player player, String themeKey); + + boolean updateThemeSetWeight(Player player, String themeKey, int weight); + + boolean flushAutosave(Player player, String workcellId); + + boolean goToPreview(Player player); + + boolean toggleVariantRotatable(Player player, String workcellId); + + boolean expandVariantToCell(Player player, String workcellId); + + boolean resizeVariant( + Player player, + String workcellId, + String pieceKey, + JigsawStudioCellDimensions dimensions + ); + + boolean updateVariantDisplayName( + Player player, + String workcellId, + String pieceKey, + String displayName + ); + + boolean updateVariantThemes( + Player player, + String workcellId, + String pieceKey, + List themes + ); + + boolean updateVariantRules( + Player player, + String workcellId, + String pieceKey, + JigsawStudioPieceRules rules + ); + + boolean adjustVariantWeight( + Player player, + String workcellId, + String pieceKey, + String poolKey, + int entryIndex, + int delta + ); + + boolean adjustVariantChance( + Player player, + String workcellId, + String pieceKey, + String poolKey, + int entryIndex, + int deltaPercentagePoints + ); + + boolean unlinkVariantMembership( + Player player, + String workcellId, + String pieceKey, + String poolKey, + int entryIndex + ); + + boolean deleteVariant(Player player, String workcellId, String pieceKey); + + boolean deleteProject(Player player); + + boolean giveTool(Player player, JigsawStudioToolPayload payload); +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuController.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuController.java index f1028b0ef..99b833a17 100644 --- a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuController.java +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuController.java @@ -1,14 +1,11 @@ package art.arcane.iris.core.service; +import art.arcane.iris.core.service.JigsawStudioMenuFormat.DimensionAxis; +import art.arcane.iris.core.service.JigsawStudioMenuFormat.RuleField; import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions; -import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget; -import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode; import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPieceRules; -import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolAction; import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolPayload; import art.arcane.iris.util.common.scheduling.J; -import art.arcane.volmlib.util.data.MaterialBlock; -import art.arcane.volmlib.util.inventorygui.UIElement; import art.arcane.volmlib.util.inventorygui.UIPaneDecorator; import art.arcane.volmlib.util.inventorygui.UIWindow; import art.arcane.volmlib.util.inventorygui.WindowResolution; @@ -20,13 +17,21 @@ import org.bukkit.plugin.java.JavaPlugin; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.activeVariant; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.adjustedDimensions; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.adjustedPositiveValue; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.adjustedRules; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.hasMembership; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.nextThemeSetKey; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.title; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.variant; + public final class JigsawStudioMenuController { static final int VARIANTS_PER_PAGE = 28; static final int MEMBERSHIPS_PER_PAGE = 28; @@ -37,21 +42,30 @@ public final class JigsawStudioMenuController { static final int PLACEMENT_RULE_SHIFT_STEP = 16; private static final long DESTRUCTIVE_CONFIRM_NANOS = 10_000_000_000L; - private static final int WORKCELL_RESIZE_REFRESH_TICKS = 2; - private static final int WORKCELL_RESIZE_REFRESH_ATTEMPTS = 200; - private static final int[] GRID_POSITIONS = {-3, -2, -1, 0, 1, 2, 3}; + static final int[] GRID_POSITIONS = {-3, -2, -1, 0, 1, 2, 3}; private final JavaPlugin plugin; - private final Actions actions; - private final Map windows = new ConcurrentHashMap<>(); + final JigsawStudioMenuActions actions; + final Map windows = new ConcurrentHashMap<>(); private final Map pendingUnlinks = new ConcurrentHashMap<>(); private final Map pendingDeletes = new ConcurrentHashMap<>(); private final Map pendingProjectDeletes = new ConcurrentHashMap<>(); - private final Map pendingWorkcellResizes = new ConcurrentHashMap<>(); + private final JigsawStudioMainMenu mainMenu; + private final JigsawStudioWorkcellMenu workcellMenu; + private final JigsawStudioStructureMenu structureMenu; + private final JigsawStudioDetailsMenu detailsMenu; + private final JigsawStudioVariantMenu variantMenu; + private final JigsawStudioToolboxMenu toolboxMenu; - public JigsawStudioMenuController(JavaPlugin plugin, Actions actions) { + public JigsawStudioMenuController(JavaPlugin plugin, JigsawStudioMenuActions actions) { this.plugin = Objects.requireNonNull(plugin, "Jigsaw Studio menu plugin"); this.actions = Objects.requireNonNull(actions, "Jigsaw Studio menu actions"); + mainMenu = new JigsawStudioMainMenu(this); + workcellMenu = new JigsawStudioWorkcellMenu(this); + structureMenu = new JigsawStudioStructureMenu(this); + detailsMenu = new JigsawStudioDetailsMenu(this); + variantMenu = new JigsawStudioVariantMenu(this); + toolboxMenu = new JigsawStudioToolboxMenu(this); } public boolean open(Player player) { @@ -104,14 +118,14 @@ public final class JigsawStudioMenuController { pendingUnlinks.remove(playerId); pendingDeletes.remove(playerId); pendingProjectDeletes.remove(playerId); - pendingWorkcellResizes.remove(playerId); + workcellMenu.clearStagedResize(playerId); }); windows.put(playerId, window); pendingUnlinks.remove(playerId); pendingDeletes.remove(playerId); pendingProjectDeletes.remove(playerId); - pendingWorkcellResizes.remove(playerId); - renderMain(window, state, selected, 0); + workcellMenu.clearStagedResize(playerId); + mainMenu.render(window, state, selected, 0); window.open(); return true; } @@ -196,7 +210,7 @@ public final class JigsawStudioMenuController { if (window == null) { return false; } - renderVariantSizeSettings(window, current.get(), workcell, variant); + workcellMenu.renderSize(window, current.get(), workcell, variant); return true; } @@ -248,7 +262,7 @@ public final class JigsawStudioMenuController { pendingUnlinks.clear(); pendingDeletes.clear(); pendingProjectDeletes.clear(); - pendingWorkcellResizes.clear(); + workcellMenu.clearStagedResizes(); for (UIWindow window : activeWindows) { Player player = window.getViewer(); if (J.isOwnedByCurrentRegion(player)) { @@ -259,1266 +273,7 @@ public final class JigsawStudioMenuController { } } - private void renderMain( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell selected, - int requestedPage - ) { - int page = clampPage(requestedPage, selected.variants().size(), VARIANTS_PER_PAGE); - window.batch(() -> { - window.clearElements(); - addWorkcells(window, state, selected.stableId()); - addFamilyControl(window, state); - addStructureControl(window, state, selected.stableId()); - addVariants(window, state, selected, page); - addMainActions(window, state, selected, page); - }); - } - - private void addFamilyControl(UIWindow window, JigsawStudioMenuState state) { - String nextThemeKey = nextThemeSetKey(state.themeSets()); - boolean available = state.irisExtended() && state.workcells().stream() - .filter(JigsawStudioMenuState.Workcell::enabled) - .allMatch(workcell -> workcell.activeVariant() != null && workcell.activeVariant().owned()); - UIElement family = element( - "duplicate-family", - available ? Material.PURPLE_DYE : Material.GRAY_DYE, - available - ? ChatColor.LIGHT_PURPLE + "Duplicate All Enabled Cells as Family" - : ChatColor.GRAY + "Duplicate All Enabled Cells as Family"); - family.addLore(ChatColor.GRAY + "Copies each enabled cell's loaded variant into one coherent family."); - family.addLore(ChatColor.GRAY + "New family: " + safe(nextThemeKey)); - if (available) { - family.addLore(ChatColor.YELLOW + "Left-click to duplicate the complete family atomically"); - family.onLeftClick(clicked -> duplicateActiveFamily( - window.getViewer(), state.requestId(), nextThemeKey)); - } else if (!state.irisExtended()) { - family.addLore(ChatColor.GRAY + "Coherent families require Iris compatibility."); - } else { - family.addLore(ChatColor.GRAY + "Load an owned variant in every enabled workcell first."); - } - window.setElement(3, 0, family); - } - - private void addStructureControl( - UIWindow window, - JigsawStudioMenuState state, - String selectedWorkcellId - ) { - UIElement structure = element( - "structure-rules", - state.irisExtended() - ? state.requireCaps() ? Material.IRON_BARS : Material.TRIPWIRE_HOOK - : Material.BARRIER, - ChatColor.LIGHT_PURPLE + "Structure Rules"); - structure.addLore(ChatColor.GRAY + "Mandatory caps: " + yesNo(state.requireCaps())); - structure.addLore(ChatColor.GRAY + "Theme sets: " + state.themeSets().size()); - structure.addLore(ChatColor.GRAY + "Compatibility: " + compatibilityName(state.compatibilityTarget())); - structure.addLore(ChatColor.YELLOW + "Left-click for themes and rules"); - structure.addLore(state.irisExtended() - ? ChatColor.YELLOW + "Right-click to toggle mandatory caps" - : ChatColor.GRAY + "Mandatory caps and themes require Iris compatibility"); - structure.onLeftClick(clicked -> openStructureSettings( - window.getViewer(), state.requestId(), selectedWorkcellId, 0)); - if (state.irisExtended()) { - structure.onRightClick(clicked -> setRequireCaps( - window.getViewer(), state.requestId(), !state.requireCaps())); - } - window.setElement(4, 0, structure); - } - - private void addWorkcells(UIWindow window, JigsawStudioMenuState state, String selectedWorkcellId) { - int count = state.workcells().size(); - int start = count == 1 ? 0 : -(count / 2); - for (int index = 0; index < count; index++) { - JigsawStudioMenuState.Workcell workcell = state.workcells().get(index); - boolean selected = workcell.stableId().equals(selectedWorkcellId); - UIElement element = element( - "workcell-" + index, - workcellMaterial(workcell, selected), - (selected ? ChatColor.AQUA : workcell.enabled() ? ChatColor.WHITE : ChatColor.GRAY) - + safe(workcell.displayName())) - .setEnchanted(selected); - element.addLore(workcell.enabled() - ? ChatColor.GREEN + "Enabled" - : ChatColor.RED + "Disabled for assembly and export"); - if (!workcell.canonicalName().equals(workcell.displayName())) { - element.addLore(ChatColor.GRAY + "Solver role: " + safe(workcell.canonicalName())); - } - element.addLore(ChatColor.GRAY + "Capacity: " + dimensions(workcell.capacity())); - element.addLore(ChatColor.GRAY + "Variants: " + workcell.variants().size()); - JigsawStudioMenuState.Variant active = workcell.activeVariant(); - element.addLore(ChatColor.GRAY + "Loaded: " - + (active == null ? "None" : safe(active.displayName()))); - element.addLore(workcellStatus(workcell)); - element.addLore(ChatColor.GRAY + "Connector blocks: " - + (workcell.connectorsVisible() ? "Visible" : "Hidden")); - element.addLore(ChatColor.YELLOW + "Left-click to select and teleport"); - element.addLore(ChatColor.YELLOW + "Right-click for workcell settings"); - if (workcell.dirty() && !workcell.saving()) { - element.addLore(ChatColor.GOLD + "Shift-left: Flush Autosave Now"); - element.onShiftLeftClick(clicked -> flushNow( - window.getViewer(), state.requestId(), workcell.stableId())); - } - element.onLeftClick(clicked -> selectWorkcell( - window.getViewer(), - state.requestId(), - workcell.stableId())); - element.onRightClick(clicked -> J.runEntity( - window.getViewer(), - () -> openWorkcellSettings( - window.getViewer(), - state.requestId(), - workcell.stableId()), - 1)); - window.setElement(start + index, 0, element); - } - } - - private void addVariants( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - int page - ) { - List variants = page( - workcell.variants(), page, VARIANTS_PER_PAGE); - if (variants.isEmpty()) { - UIElement empty = element("no-variants", Material.BARRIER, ChatColor.RED + "No variants"); - empty.addLore(ChatColor.GRAY + "Create a variant to begin authoring this workcell."); - window.setElement(0, 2, empty); - return; - } - - for (int index = 0; index < variants.size(); index++) { - JigsawStudioMenuState.Variant variant = variants.get(index); - int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; - int row = 1 + index / GRID_POSITIONS.length; - UIElement element = element( - "variant-" + index, - variantMaterial(variant), - (variant.active() ? ChatColor.GREEN : ChatColor.WHITE) - + safe(variant.displayName()) - + (variant.active() ? ChatColor.AQUA + " [Loaded]" : "")) - .setEnchanted(variant.active()); - element.addLore(ChatColor.DARK_GRAY + safe(variant.pieceKey())); - element.addLore(variant.owned() - ? ChatColor.GREEN + "Owned" - : ChatColor.GRAY + "Read-only"); - element.addLore(ChatColor.GRAY + "Rotation: " + (variant.rotatable() ? "Enabled" : "Disabled")); - element.addLore(ChatColor.GRAY + "Size: " + variant.dimensions() - .map(JigsawStudioMenuController::dimensions) - .orElse("Object missing")); - element.addLore(ChatColor.GRAY + "Themes: " + themes(variant.themes())); - element.addLore(ChatColor.GRAY + "Pool entries: " + variant.memberships().size()); - element.addLore(variant.active() - ? ChatColor.YELLOW + "Loaded; right-click for details" - : ChatColor.YELLOW + "Left-click to load"); - if (!variant.active()) { - element.addLore(ChatColor.YELLOW + "Right-click for details or deletion"); - } - element.onLeftClick(clicked -> switchVariant( - window.getViewer(), - state.requestId(), - workcell.stableId(), - variant.pieceKey())); - element.onRightClick(clicked -> J.runEntity( - window.getViewer(), - () -> openDetails( - window.getViewer(), - state.requestId(), - workcell.stableId(), - variant.pieceKey(), - 0), - 1)); - window.setElement(position, row, element); - } - } - - private void addMainActions( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - int page - ) { - UIElement create = element("create", Material.NETHER_STAR, ChatColor.GREEN + "New Blank Variant"); - create.addLore(ChatColor.GRAY + "Create an empty owned variant with this cell's current size."); - create.onLeftClick(clicked -> createVariant( - window.getViewer(), state.requestId(), workcell.stableId(), false)); - window.setElement(-4, 5, create); - - JigsawStudioMenuState.Variant active = workcell.activeVariant(); - boolean duplicateAvailable = active != null && active.owned(); - UIElement duplicate = element( - "duplicate", - duplicateAvailable ? Material.PAPER : Material.GRAY_DYE, - duplicateAvailable - ? ChatColor.AQUA + "Duplicate This Cell's Variant" - : active == null - ? ChatColor.GRAY + "Duplicate This Cell's Variant" - : ChatColor.GRAY + "Duplicate This Cell's Variant"); - duplicate.addLore(duplicateAvailable - ? ChatColor.GRAY + "Clone the loaded variant into an owned variant." - : active == null - ? ChatColor.GRAY + "Load a variant before duplicating it." - : ChatColor.GRAY + "Adopt the read-only graph before duplicating it."); - if (duplicateAvailable) { - duplicate.onLeftClick(clicked -> createVariant( - window.getViewer(), state.requestId(), workcell.stableId(), true)); - } - window.setElement(-3, 5, duplicate); - - UIElement settings = element("settings", Material.CRAFTING_TABLE, ChatColor.GOLD + "Workcell Settings"); - settings.addLore(ChatColor.GRAY + "Enabled: " + yesNo(workcell.enabled())); - settings.addLore(ChatColor.GRAY + "Capacity: " + dimensions(workcell.capacity())); - settings.onLeftClick(clicked -> openWorkcellSettings( - window.getViewer(), state.requestId(), workcell.stableId())); - window.setElement(-2, 5, settings); - - int pageCount = pageCount(workcell.variants().size(), VARIANTS_PER_PAGE); - if (page > 0) { - UIElement previous = element("previous", Material.ARROW, ChatColor.YELLOW + "Previous Page"); - previous.onLeftClick(clicked -> refreshMain( - window.getViewer(), state.requestId(), workcell.stableId(), page - 1)); - window.setElement(-1, 5, previous); - } - - UIElement pageIndicator = element("page", Material.BOOK, ChatColor.WHITE + "Page " - + (page + 1) + "/" + pageCount); - pageIndicator.addLore(ChatColor.GRAY + "" + workcell.variants().size() + " variants"); - window.setElement(0, 5, pageIndicator); - - if (page + 1 < pageCount) { - UIElement next = element("next", Material.ARROW, ChatColor.YELLOW + "Next Page"); - next.onLeftClick(clicked -> refreshMain( - window.getViewer(), state.requestId(), workcell.stableId(), page + 1)); - window.setElement(1, 5, next); - } - - window.setElement(2, 5, evaluationElement(state.evaluation())); - - UIElement preview = element("preview", Material.ENDER_EYE, ChatColor.LIGHT_PURPLE + "Go to Preview"); - preview.addLore(ChatColor.GRAY + "Open the deterministic assembly preview."); - preview.onLeftClick(clicked -> goToPreview(window.getViewer(), state.requestId())); - window.setElement(3, 5, preview); - - UIElement toolbox = element("toolbox", Material.STICK, ChatColor.AQUA + "Toolbox"); - toolbox.addLore(ChatColor.GRAY + "Take named sticks bound to this Studio session."); - toolbox.onLeftClick(clicked -> openToolbox( - window.getViewer(), state.requestId(), workcell.stableId(), 0)); - window.setElement(4, 5, toolbox); - } - - private void renderWorkcellSettings( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell - ) { - PendingWorkcellResize pendingResize = pendingWorkcellResize( - window.getViewer().getUniqueId(), - state.requestId(), - workcell.stableId()); - JigsawStudioMenuState.Workcell renderedWorkcell = pendingResize == null - ? workcell - : withCapacity(workcell, pendingResize.dimensions()); - window.batch(() -> { - window.clearElements(); - - UIElement back = element("settings-back", Material.ARROW, ChatColor.YELLOW + "Back to Variants"); - back.onLeftClick(clicked -> leaveWorkcellSettings( - window.getViewer(), state.requestId(), renderedWorkcell.stableId())); - window.setElement(-4, 0, back); - - UIElement identity = element( - "settings-identity", - renderedWorkcell.enabled() ? Material.LIME_WOOL : Material.GRAY_WOOL, - ChatColor.AQUA + safe(renderedWorkcell.displayName())); - identity.addLore(ChatColor.DARK_GRAY + safe(renderedWorkcell.stableId())); - identity.addLore(renderedWorkcell.enabled() - ? ChatColor.GREEN + "Enabled" - : ChatColor.RED + "Disabled for assembly and export"); - if (!renderedWorkcell.canonicalName().equals(renderedWorkcell.displayName())) { - identity.addLore(ChatColor.GRAY + "Solver role: " + safe(renderedWorkcell.canonicalName())); - } - identity.addLore(ChatColor.GRAY + "Capacity: " + dimensions(renderedWorkcell.capacity())); - if (pendingResize != null) { - identity.addLore(pendingResize.applying() - ? ChatColor.YELLOW + "Applying one live relayout" - : ChatColor.GOLD + "Pending; apply when all dimensions are ready"); - } - identity.addLore(ChatColor.YELLOW + "Left-click for a rename stick"); - identity.addLore(ChatColor.GRAY + "Rename that stick in an anvil, then right-click it."); - identity.addLore(ChatColor.GRAY + "Sneak-right-click the stick to reset this label."); - identity.onLeftClick(clicked -> actions.giveTool( - window.getViewer(), - JigsawStudioToolPayload.workcell( - JigsawStudioToolAction.RENAME_WORKCELL, - state.requestId(), - renderedWorkcell.stableId()))); - window.setElement(0, 0, identity); - window.setElement(4, 0, evaluationElement(state.evaluation())); - - if (state.mode() == JigsawStudioMode.PLANAR_JIGSAW) { - UIElement enabled = element( - "settings-enabled", - renderedWorkcell.enabled() ? Material.LEVER : Material.REDSTONE_TORCH, - renderedWorkcell.enabled() - ? ChatColor.GREEN + "Workcell Enabled" - : ChatColor.RED + "Workcell Disabled"); - enabled.addLore(ChatColor.GRAY + "Disabled workcells remain editable and keep their size."); - enabled.addLore(ChatColor.YELLOW + "Left-click to " - + (renderedWorkcell.enabled() ? "disable" : "enable")); - enabled.onLeftClick(clicked -> setWorkcellEnabled( - window.getViewer(), - state.requestId(), - renderedWorkcell.stableId(), - !renderedWorkcell.enabled())); - window.setElement(0, 1, enabled); - } else { - UIElement spatial = element( - "spatial-bounds", - Material.SCAFFOLDING, - ChatColor.AQUA + "Spatial Workcell Bounds"); - spatial.addLore(ChatColor.GRAY + "Spatial project bounds are shared by the project."); - spatial.addLore(ChatColor.GRAY + "Changing them regenerates the Studio layout."); - window.setElement(0, 1, spatial); - } - - UIElement connectors = element( - "settings-connectors", - renderedWorkcell.connectorsVisible() ? Material.JIGSAW : Material.STRUCTURE_VOID, - renderedWorkcell.connectorsVisible() - ? ChatColor.GREEN + "Connector Blocks Visible" - : ChatColor.YELLOW + "Connector Blocks Hidden"); - connectors.addLore(ChatColor.GRAY + "Hidden connectors retain their metadata and final block state."); - connectors.addLore(ChatColor.YELLOW + "Left-click to " - + (renderedWorkcell.connectorsVisible() ? "hide" : "show") + " connector blocks"); - connectors.onLeftClick(clicked -> toggleConnectorBlocks( - window.getViewer(), - state.requestId(), - renderedWorkcell.stableId(), - !renderedWorkcell.connectorsVisible())); - window.setElement(2, 1, connectors); - - UIElement resetConnectors = element( - "settings-reset-connectors", - Material.RECOVERY_COMPASS, - ChatColor.AQUA + "Reset Connector Blocks"); - resetConnectors.addLore(ChatColor.GRAY + "Restore every connector in this workcell from disk."); - resetConnectors.addLore(ChatColor.GRAY + "Other edited blocks are left unchanged."); - resetConnectors.onLeftClick(clicked -> resetConnectorBlocks( - window.getViewer(), - state.requestId(), - renderedWorkcell.stableId())); - window.setElement(4, 1, resetConnectors); - - window.setElement(-2, 2, axisElement( - window, - state, - renderedWorkcell, - DimensionAxis.WIDTH, - Material.IRON_INGOT)); - window.setElement(0, 2, axisElement( - window, - state, - renderedWorkcell, - DimensionAxis.HEIGHT, - Material.GOLD_INGOT)); - window.setElement(2, 2, axisElement( - window, - state, - renderedWorkcell, - DimensionAxis.DEPTH, - Material.COPPER_INGOT)); - - if (pendingResize != null) { - UIElement apply = element( - "settings-apply-capacity", - pendingResize.applying() ? Material.CLOCK : Material.EMERALD_BLOCK, - pendingResize.applying() - ? ChatColor.YELLOW + "Applying Cell Size" - : ChatColor.GREEN + "Apply Cell Size"); - apply.addLore(ChatColor.WHITE + dimensions(pendingResize.dimensions())); - apply.addLore(ChatColor.GRAY + "Regenerates the layout once after all size edits."); - if (!pendingResize.applying()) { - apply.onLeftClick(clicked -> applyWorkcellResize( - window.getViewer(), state.requestId(), renderedWorkcell.stableId())); - } - window.setElement(0, 3, apply); - - if (!pendingResize.applying()) { - UIElement discard = element( - "settings-discard-capacity", - Material.BARRIER, - ChatColor.RED + "Discard Size Changes"); - discard.onLeftClick(clicked -> discardWorkcellResize( - window.getViewer(), state.requestId(), renderedWorkcell.stableId())); - window.setElement(2, 3, discard); - } - } - - UIElement footerBack = element("settings-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); - footerBack.onLeftClick(clicked -> leaveWorkcellSettings( - window.getViewer(), state.requestId(), renderedWorkcell.stableId())); - window.setElement(-4, 5, footerBack); - - UIElement undo = element( - "settings-undo", - Material.CLOCK, - ChatColor.LIGHT_PURPLE + "Undo Last Autosave"); - undo.addLore(ChatColor.GRAY + "Restore the previous owned graph iteration."); - undo.addLore(ChatColor.GRAY + "Up to five autosave iterations are retained on disk."); - undo.onLeftClick(clicked -> undoAutosave( - window.getViewer(), - state.requestId())); - window.setElement(0, 5, undo); - - if (renderedWorkcell.dirty() && !renderedWorkcell.saving()) { - UIElement saveNow = element( - "save-now", - Material.EMERALD, - ChatColor.GOLD + "Flush Autosave Now"); - saveNow.addLore(ChatColor.GRAY + "Autosave is automatic."); - saveNow.addLore(ChatColor.GRAY + "Use this only to flush pending work or recover immediately."); - saveNow.onLeftClick(clicked -> flushNow( - window.getViewer(), state.requestId(), renderedWorkcell.stableId())); - window.setElement(2, 5, saveNow); - } - - UIElement preview = element( - "settings-preview", - Material.ENDER_EYE, - ChatColor.LIGHT_PURPLE + "Go to Preview"); - preview.onLeftClick(clicked -> goToPreview(window.getViewer(), state.requestId())); - window.setElement(3, 5, preview); - - UIElement toolbox = element("settings-toolbox", Material.STICK, ChatColor.AQUA + "Toolbox"); - toolbox.onLeftClick(clicked -> openToolbox( - window.getViewer(), state.requestId(), renderedWorkcell.stableId(), 0)); - window.setElement(4, 5, toolbox); - }); - } - - private void renderVariantSizeSettings( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - JigsawStudioMenuState.Variant variant - ) { - JigsawStudioCellDimensions current = variant.dimensions().orElseThrow(); - window.batch(() -> { - window.clearElements(); - - UIElement back = element("variant-size-back", Material.ARROW, ChatColor.YELLOW + "Back to Details"); - back.onLeftClick(clicked -> openDetails( - window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); - window.setElement(-4, 0, back); - - UIElement identity = element( - "variant-size-identity", - Material.SCAFFOLDING, - ChatColor.AQUA + safe(variant.displayName()) + " Size"); - identity.addLore(ChatColor.DARK_GRAY + safe(variant.pieceKey())); - identity.addLore(ChatColor.WHITE + "Current: " + dimensions(current)); - identity.addLore(ChatColor.GRAY + "Workcell capacity: " + dimensions(workcell.capacity())); - identity.addLore(ChatColor.GRAY + "Only this variant object and its edge connectors change."); - window.setElement(0, 0, identity); - window.setElement(4, 0, evaluationElement(state.evaluation())); - - window.setElement(-2, 2, variantAxisElement( - window, state, workcell, variant, DimensionAxis.WIDTH, Material.IRON_INGOT)); - window.setElement(0, 2, variantAxisElement( - window, state, workcell, variant, DimensionAxis.HEIGHT, Material.GOLD_INGOT)); - window.setElement(2, 2, variantAxisElement( - window, state, workcell, variant, DimensionAxis.DEPTH, Material.COPPER_INGOT)); - - UIElement capacity = element( - "variant-size-capacity", - variant.resizableToCapacity() ? Material.NETHER_STAR : Material.GRAY_DYE, - variant.resizableToCapacity() - ? ChatColor.GREEN + "Resize This Variant to Capacity" - : ChatColor.GRAY + "Variant Already Matches Capacity"); - capacity.addLore(ChatColor.GRAY + dimensions(workcell.capacity())); - if (variant.resizableToCapacity()) { - capacity.addLore(ChatColor.YELLOW + "Left-click to resize only this variant"); - capacity.onLeftClick(clicked -> resizeVariant( - window.getViewer(), - state.requestId(), - workcell.stableId(), - variant.pieceKey(), - workcell.capacity())); - } - window.setElement(0, 3, capacity); - - UIElement footerBack = element("variant-size-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); - footerBack.onLeftClick(clicked -> openDetails( - window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); - window.setElement(-4, 5, footerBack); - }); - } - - private UIElement variantAxisElement( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - JigsawStudioMenuState.Variant variant, - DimensionAxis axis, - Material material - ) { - JigsawStudioCellDimensions current = variant.dimensions().orElseThrow(); - UIElement element = element( - "variant-axis-" + axis.name().toLowerCase(Locale.ROOT), - material, - ChatColor.AQUA + axis.displayName() + ": " + axis.value(current)); - element.addLore(ChatColor.GREEN + "Left-click: +1"); - element.addLore(ChatColor.YELLOW + "Right-click: -1"); - element.addLore(ChatColor.GREEN + "Shift-left: +8"); - element.addLore(ChatColor.YELLOW + "Shift-right: -8"); - element.addLore(ChatColor.GRAY + "Maximum: " + axis.value(workcell.capacity())); - element.onLeftClick(clicked -> resizeVariantAxis( - window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), axis, 1)); - element.onRightClick(clicked -> resizeVariantAxis( - window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), axis, -1)); - element.onShiftLeftClick(clicked -> resizeVariantAxis( - window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), axis, 8)); - element.onShiftRightClick(clicked -> resizeVariantAxis( - window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), axis, -8)); - return element; - } - - private void renderStructureSettings( - UIWindow window, - JigsawStudioMenuState state, - String selectedWorkcellId, - int requestedPage - ) { - int page = clampPage(requestedPage, state.themeSets().size(), THEME_SETS_PER_PAGE); - purgeExpiredConfirmations(window.getViewer().getUniqueId()); - window.batch(() -> { - window.clearElements(); - - UIElement back = element("structure-back", Material.ARROW, ChatColor.YELLOW + "Back to Variants"); - back.onLeftClick(clicked -> refreshMain( - window.getViewer(), state.requestId(), selectedWorkcellId, 0)); - window.setElement(-4, 0, back); - - UIElement identity = element( - "structure-identity", - Material.JIGSAW, - ChatColor.LIGHT_PURPLE + "Structure Themes & Caps"); - identity.addLore(ChatColor.DARK_GRAY + safe(state.structureKey())); - identity.addLore(ChatColor.GRAY + "Compatibility: " + compatibilityName(state.compatibilityTarget())); - identity.addLore(ChatColor.GRAY + "Theme sets select one coherent variant family per assembly."); - identity.addLore(ChatColor.GRAY + "Empty piece themes remain available to every family."); - window.setElement(0, 0, identity); - window.setElement(4, 0, evaluationElement(state.evaluation())); - - UIElement requireCaps = element( - "require-caps", - state.irisExtended() - ? state.requireCaps() ? Material.IRON_BARS : Material.TRIPWIRE_HOOK - : Material.BARRIER, - !state.irisExtended() - ? ChatColor.GRAY + "Mandatory Caps Require Iris Compatibility" - : state.requireCaps() - ? ChatColor.GREEN + "Mandatory Caps Enabled" - : ChatColor.YELLOW + "Mandatory Caps Disabled"); - requireCaps.addLore(!state.irisExtended() - ? ChatColor.GRAY + "Vanilla-portable export rejects Iris mandatory-cap metadata." - : state.requireCaps() - ? ChatColor.GRAY + "Every open connector must close with a terminal piece." - : ChatColor.GRAY + "Assembly may leave an open connector when no cap can fit."); - if (state.irisExtended()) { - requireCaps.addLore(ChatColor.YELLOW + "Left-click to toggle"); - requireCaps.onLeftClick(clicked -> setRequireCaps( - window.getViewer(), state.requestId(), !state.requireCaps())); - } - window.setElement(-3, 1, requireCaps); - - String nextThemeKey = nextThemeSetKey(state.themeSets()); - boolean themeEditingAvailable = state.irisExtended(); - UIElement createTheme = element( - "new-theme-set", - themeEditingAvailable ? Material.NETHER_STAR : Material.BARRIER, - themeEditingAvailable - ? ChatColor.GREEN + "Duplicate All Enabled Cells as Family: " + safe(nextThemeKey) - : ChatColor.GRAY + "Theme Sets Require Iris Compatibility"); - if (themeEditingAvailable) { - createTheme.addLore(ChatColor.GRAY + "Creates one new owned variant for every enabled workcell."); - createTheme.addLore(ChatColor.GRAY + "All created variants join " + safe(nextThemeKey) + "."); - createTheme.addLore(ChatColor.GRAY + "One family is selected for the complete assembly."); - createTheme.addLore(ChatColor.YELLOW + "Left-click to create the complete set"); - createTheme.onLeftClick(clicked -> duplicateActiveFamily( - window.getViewer(), state.requestId(), nextThemeKey)); - } else { - createTheme.addLore(ChatColor.GRAY + "Vanilla jigsaw resources cannot encode Iris theme metadata."); - } - window.setElement(0, 1, createTheme); - - boolean deleteConfirmed = isProjectDeleteConfirmed( - window.getViewer().getUniqueId(), state.requestId()); - UIElement deleteProject = element( - "delete-project", - deleteConfirmed ? Material.REDSTONE : Material.LAVA_BUCKET, - deleteConfirmed - ? ChatColor.RED + "Confirm Delete Jigsaw" - : ChatColor.RED + "Delete Jigsaw") - .setEnchanted(deleteConfirmed); - deleteProject.addLore(deleteConfirmed - ? ChatColor.RED + "Click again to delete this entire managed project." - : ChatColor.RED + "Click twice within 10 seconds to confirm."); - deleteProject.onLeftClick(clicked -> confirmOrDeleteProject( - window.getViewer(), state.requestId(), selectedWorkcellId, page)); - window.setElement(3, 1, deleteProject); - - addThemeSets(window, state, page); - - UIElement footerBack = element("structure-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); - footerBack.onLeftClick(clicked -> refreshMain( - window.getViewer(), state.requestId(), selectedWorkcellId, 0)); - window.setElement(-4, 5, footerBack); - - int pageCount = pageCount(state.themeSets().size(), THEME_SETS_PER_PAGE); - if (page > 0) { - UIElement previous = element( - "theme-previous", - Material.ARROW, - ChatColor.YELLOW + "Previous Page"); - previous.onLeftClick(clicked -> refreshStructureSettings( - window.getViewer(), state.requestId(), selectedWorkcellId, page - 1)); - window.setElement(-1, 5, previous); - } - UIElement indicator = element( - "theme-page", - Material.BOOK, - ChatColor.WHITE + "Page " + (page + 1) + "/" + pageCount); - indicator.addLore(ChatColor.GRAY + "" + state.themeSets().size() + " theme sets"); - window.setElement(0, 5, indicator); - if (page + 1 < pageCount) { - UIElement next = element("theme-next", Material.ARROW, ChatColor.YELLOW + "Next Page"); - next.onLeftClick(clicked -> refreshStructureSettings( - window.getViewer(), state.requestId(), selectedWorkcellId, page + 1)); - window.setElement(1, 5, next); - } - window.setElement(4, 5, evaluationElement(state.evaluation())); - }); - } - - private void addThemeSets( - UIWindow window, - JigsawStudioMenuState state, - int page - ) { - List themeSets = page( - state.themeSets(), page, THEME_SETS_PER_PAGE); - if (themeSets.isEmpty()) { - UIElement empty = element( - "no-theme-sets", - Material.GRAY_DYE, - ChatColor.GRAY + "Implicit Unthemed Assembly"); - empty.addLore(ChatColor.GRAY + "Create variant-1 to begin coherent theme selection."); - window.setElement(0, 3, empty); - return; - } - for (int index = 0; index < themeSets.size(); index++) { - JigsawStudioMenuState.ThemeSet themeSet = themeSets.get(index); - int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; - int row = 2 + index / GRID_POSITIONS.length; - UIElement element = element( - "theme-set-" + index, - state.irisExtended() ? Material.PURPLE_DYE : Material.GRAY_DYE, - (state.irisExtended() ? ChatColor.LIGHT_PURPLE : ChatColor.GRAY) + safe(themeSet.key())); - element.addLore(ChatColor.WHITE + "Selection weight: " + themeSet.weight()); - element.addLore(ChatColor.WHITE + "Whole-assembly chance: " - + themeSelectionPercent(state.themeSets(), themeSet)); - if (state.irisExtended()) { - element.addLore(ChatColor.GREEN + "Left-click: weight +1"); - element.addLore(ChatColor.YELLOW + "Right-click: weight -1"); - element.addLore(ChatColor.GREEN + "Shift-left: weight +8"); - element.addLore(ChatColor.YELLOW + "Shift-right: weight -8"); - element.onLeftClick(clicked -> adjustThemeSetWeight( - window.getViewer(), state.requestId(), themeSet, 1)); - element.onRightClick(clicked -> adjustThemeSetWeight( - window.getViewer(), state.requestId(), themeSet, -1)); - element.onShiftLeftClick(clicked -> adjustThemeSetWeight( - window.getViewer(), state.requestId(), themeSet, 8)); - element.onShiftRightClick(clicked -> adjustThemeSetWeight( - window.getViewer(), state.requestId(), themeSet, -8)); - } else { - element.addLore(ChatColor.GRAY + "Theme metadata is unavailable for vanilla-portable graphs."); - } - window.setElement(position, row, element); - } - } - - private UIElement axisElement( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - DimensionAxis axis, - Material material - ) { - UIElement element = element( - "axis-" + axis.name().toLowerCase(Locale.ROOT), - material, - ChatColor.AQUA + axis.displayName() + " Capacity: " + axis.value(workcell.capacity())); - element.addLore(ChatColor.GREEN + "Left-click: +1"); - element.addLore(ChatColor.YELLOW + "Right-click: -1"); - element.addLore(ChatColor.GREEN + "Shift-left: +8"); - element.addLore(ChatColor.YELLOW + "Shift-right: -8"); - element.addLore(ChatColor.GRAY + "Changes stay in this menu until Apply Cell Size."); - element.onLeftClick(clicked -> resizeWorkcell( - window.getViewer(), state.requestId(), workcell.stableId(), axis, 1)); - element.onRightClick(clicked -> resizeWorkcell( - window.getViewer(), state.requestId(), workcell.stableId(), axis, -1)); - element.onShiftLeftClick(clicked -> resizeWorkcell( - window.getViewer(), state.requestId(), workcell.stableId(), axis, 8)); - element.onShiftRightClick(clicked -> resizeWorkcell( - window.getViewer(), state.requestId(), workcell.stableId(), axis, -8)); - return element; - } - - private void renderDetails( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - JigsawStudioMenuState.Variant variant, - int requestedPage - ) { - int page = clampPage(requestedPage, variant.memberships().size(), MEMBERSHIPS_PER_PAGE); - purgeExpiredConfirmations(window.getViewer().getUniqueId()); - window.batch(() -> { - window.clearElements(); - addDetailsHeader(window, state, workcell, variant, page); - addMemberships(window, state, workcell, variant, page); - addDetailsFooter(window, state, workcell, variant, page); - }); - } - - private void addDetailsHeader( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - JigsawStudioMenuState.Variant variant, - int page - ) { - UIElement back = element("back", Material.ARROW, ChatColor.YELLOW + "Back to Variants"); - back.onLeftClick(clicked -> refreshMain( - window.getViewer(), state.requestId(), workcell.stableId(), 0)); - window.setElement(-4, 0, back); - - UIElement identity = element("identity", Material.NAME_TAG, ChatColor.AQUA + safe(variant.displayName())); - identity.addLore(ChatColor.DARK_GRAY + safe(variant.pieceKey())); - identity.addLore(variant.owned() - ? ChatColor.GREEN + "Owned variant" - : ChatColor.GRAY + "Read-only variant"); - identity.addLore(variant.active() - ? ChatColor.GREEN + "Loaded in this workcell" - : ChatColor.GRAY + "Not currently loaded"); - identity.addLore(ChatColor.GRAY + "Size: " + variant.dimensions() - .map(JigsawStudioMenuController::dimensions) - .orElse("Object missing")); - identity.addLore(ChatColor.GRAY + "Themes: " + themes(variant.themes())); - identity.addLore(ChatColor.GRAY + "Depth: " + variant.rules().minimumDepth() - + "-" + variant.rules().maximumDepth()); - identity.addLore(ChatColor.GRAY + "Placements: " + variant.rules().minimumPlacements() - + "-" + maximumPlacements(variant.rules().maximumPlacements())); - identity.addLore(ChatColor.GRAY + "Terminal: " + yesNo(variant.rules().terminal())); - identity.addLore(ChatColor.GRAY + "Pool entries: " + variant.memberships().size()); - if (variant.owned()) { - identity.addLore(ChatColor.YELLOW + "Left-click for a rename stick"); - identity.addLore(ChatColor.GRAY + "Rename that stick in an anvil, then right-click it."); - identity.addLore(ChatColor.GRAY + "Sneak-right-click the stick to reset this label."); - identity.onLeftClick(clicked -> actions.giveTool( - window.getViewer(), - JigsawStudioToolPayload.variant( - JigsawStudioToolAction.RENAME_VARIANT, - state.requestId(), - workcell.stableId(), - variant.pieceKey()))); - } - window.setElement(0, 0, identity); - - boolean activeOwned = variant.active() && variant.owned(); - boolean irisRuleEditing = activeOwned && state.irisExtended(); - UIElement settings = element( - "variant-settings", - irisRuleEditing ? Material.COMPARATOR : Material.GRAY_DYE, - irisRuleEditing - ? ChatColor.GOLD + "Themes & Piece Rules" - : ChatColor.GRAY + "Themes & Rules Unavailable"); - settings.addLore(irisRuleEditing - ? ChatColor.GRAY + "Edit this loaded variant's theme membership and placement rules." - : state.irisExtended() - ? ChatColor.GRAY + "Load an owned variant before editing its rules." - : ChatColor.GRAY + "Vanilla-portable pieces cannot encode Iris theme or rule metadata."); - if (irisRuleEditing) { - settings.addLore(ChatColor.YELLOW + "Left-click to edit"); - settings.onLeftClick(clicked -> openVariantSettings( - window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); - } - window.setElement(-2, 0, settings); - - boolean rotationEditable = variant.active() && variant.rotationEditable(); - UIElement rotation = element( - "rotation", - rotationEditable ? Material.REPEATER : Material.BARRIER, - rotationEditable - ? ChatColor.LIGHT_PURPLE + "Toggle Rotation" - : ChatColor.GRAY + "Rotation is Read-only"); - rotation.addLore(ChatColor.GRAY + "Currently: " + (variant.rotatable() ? "Enabled" : "Disabled")); - if (rotationEditable) { - rotation.addLore(ChatColor.YELLOW + "Left-click to toggle"); - rotation.onLeftClick(clicked -> toggleRotation( - window.getViewer(), state.requestId(), workcell.stableId())); - } else if (!variant.active()) { - rotation.addLore(ChatColor.GRAY + "Load this variant before editing rotation."); - } else if (variant.owned() && variant.rotatable()) { - rotation.addLore(ChatColor.GRAY + "Vanilla-portable variants must remain rotatable."); - } - window.setElement(3, 0, rotation); - - boolean sizeEditable = variant.owned() && variant.dimensions().isPresent(); - UIElement size = element( - "variant-size", - sizeEditable ? Material.SCAFFOLDING : Material.BARRIER, - sizeEditable - ? ChatColor.GREEN + "Edit This Variant's Size" - : ChatColor.GRAY + "Variant Size is Read-only"); - size.addLore(ChatColor.GRAY + "Current: " + variant.dimensions() - .map(JigsawStudioMenuController::dimensions) - .orElse("Object missing")); - size.addLore(ChatColor.GRAY + "Workcell capacity: " + dimensions(workcell.capacity())); - if (sizeEditable) { - size.addLore(ChatColor.YELLOW + "Left-click for width, height, and depth controls"); - size.onLeftClick(clicked -> openVariantSizeSettings( - window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey())); - if (variant.resizableToCapacity()) { - size.addLore(ChatColor.YELLOW + "Shift-left: resize this variant to capacity"); - size.onShiftLeftClick(clicked -> resizeVariant( - window.getViewer(), - state.requestId(), - workcell.stableId(), - variant.pieceKey(), - workcell.capacity())); - } - } - window.setElement(2, 0, size); - - boolean lastVariant = workcell.variants().size() <= 1; - boolean deletionAvailable = variant.owned() && !variant.active() && !lastVariant; - boolean confirmed = deletionAvailable && isDeleteConfirmed( - window.getViewer().getUniqueId(), - state.requestId(), - workcell.stableId(), - variant.pieceKey()); - UIElement delete = element( - "delete-variant", - deletionAvailable ? confirmed ? Material.REDSTONE : Material.LAVA_BUCKET : Material.BARRIER, - deletionAvailable - ? confirmed ? ChatColor.RED + "Confirm Delete Variant" : ChatColor.RED + "Delete Variant" - : variant.active() - ? ChatColor.GRAY + "Loaded Variant Cannot Be Deleted" - : lastVariant - ? ChatColor.GRAY + "Last Variant Cannot Be Deleted" - : ChatColor.GRAY + "Deletion is Read-only") - .setEnchanted(confirmed); - if (deletionAvailable) { - delete.addLore(confirmed - ? ChatColor.RED + "Click again to permanently delete this owned variant." - : ChatColor.RED + "Click twice within 10 seconds to confirm."); - delete.onLeftClick(clicked -> confirmOrDeleteVariant( - window.getViewer(), - state.requestId(), - workcell.stableId(), - variant.pieceKey(), - page)); - } else if (variant.active()) { - delete.addLore(ChatColor.GRAY + "Load another variant in this workcell first."); - } else if (lastVariant) { - delete.addLore(ChatColor.GRAY + "Create another variant before deleting this one."); - } - window.setElement(4, 0, delete); - } - - private void addMemberships( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - JigsawStudioMenuState.Variant variant, - int page - ) { - List memberships = page( - variant.memberships(), page, MEMBERSHIPS_PER_PAGE); - if (memberships.isEmpty()) { - UIElement empty = element("no-memberships", Material.BARRIER, ChatColor.RED + "No Pool Entries"); - empty.addLore(ChatColor.GRAY + "This variant is not linked from an owned pool."); - window.setElement(0, 2, empty); - return; - } - - for (int index = 0; index < memberships.size(); index++) { - JigsawStudioMenuState.Membership membership = memberships.get(index); - int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; - int row = 1 + index / GRID_POSITIONS.length; - boolean confirmed = variant.active() && variant.owned() && isUnlinkConfirmed( - window.getViewer().getUniqueId(), - state.requestId(), - workcell.stableId(), - variant.pieceKey(), - membership); - UIElement element = element( - "membership-" + index, - confirmed ? Material.REDSTONE : Material.GOLD_NUGGET, - (confirmed ? ChatColor.RED : ChatColor.GOLD) - + safe(displayKey(membership.poolKey())) - + " [" + membership.entryIndex() + "]") - .setEnchanted(confirmed); - element.addLore(ChatColor.DARK_GRAY + safe(membership.poolKey())); - element.addLore(ChatColor.GRAY + "Exact entry: " + membership.entryIndex()); - element.addLore(ChatColor.WHITE + "Weight: " + membership.weight()); - element.addLore((state.irisExtended() ? ChatColor.WHITE : ChatColor.GRAY) - + "Chance: " + chance(membership.chance()) - + (state.irisExtended() ? "" : " (Iris only)")); - if (variant.active() && variant.owned()) { - element.addLore(ChatColor.GREEN + "Left-click: weight +1"); - element.addLore(ChatColor.YELLOW + "Right-click: weight -1"); - if (state.irisExtended()) { - element.addLore(ChatColor.GREEN + "Shift-left: chance +" - + CHANCE_STEP_PERCENTAGE_POINTS + "%"); - element.addLore(ChatColor.YELLOW + "Shift-right: chance -" - + CHANCE_STEP_PERCENTAGE_POINTS + "%"); - } else { - element.addLore(ChatColor.GRAY + "Vanilla-portable pool entries always have 100% chance."); - } - element.addLore(confirmed - ? ChatColor.RED + "Middle-click again to unlink" - : ChatColor.RED + "Middle-click twice to unlink"); - element.onLeftClick(clicked -> adjustMembershipWeight( - window.getViewer(), state.requestId(), workcell.stableId(), membership, 1)); - element.onRightClick(clicked -> adjustMembershipWeight( - window.getViewer(), state.requestId(), workcell.stableId(), membership, -1)); - if (state.irisExtended()) { - element.onShiftLeftClick(clicked -> adjustMembershipChance( - window.getViewer(), - state.requestId(), - workcell.stableId(), - membership, - CHANCE_STEP_PERCENTAGE_POINTS)); - element.onShiftRightClick(clicked -> adjustMembershipChance( - window.getViewer(), - state.requestId(), - workcell.stableId(), - membership, - -CHANCE_STEP_PERCENTAGE_POINTS)); - } - element.onMiddleClick(clicked -> confirmOrUnlink( - window.getViewer(), state.requestId(), workcell.stableId(), membership, page)); - } else { - element.addLore(variant.active() - ? ChatColor.GRAY + "Read-only pool entry" - : ChatColor.GRAY + "Load this variant to edit its pool entry"); - } - window.setElement(position, row, element); - } - } - - private void addDetailsFooter( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - JigsawStudioMenuState.Variant variant, - int page - ) { - UIElement back = element("footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); - back.onLeftClick(clicked -> refreshMain( - window.getViewer(), state.requestId(), workcell.stableId(), 0)); - window.setElement(-4, 5, back); - - int pageCount = pageCount(variant.memberships().size(), MEMBERSHIPS_PER_PAGE); - if (page > 0) { - UIElement previous = element("membership-previous", Material.ARROW, ChatColor.YELLOW + "Previous Page"); - previous.onLeftClick(clicked -> refreshDetails( - window.getViewer(), - state.requestId(), - workcell.stableId(), - variant.pieceKey(), - page - 1)); - window.setElement(-1, 5, previous); - } - UIElement indicator = element("membership-page", Material.BOOK, ChatColor.WHITE + "Page " - + (page + 1) + "/" + pageCount); - indicator.addLore(ChatColor.GRAY + "" + variant.memberships().size() + " exact pool entries"); - window.setElement(0, 5, indicator); - if (page + 1 < pageCount) { - UIElement next = element("membership-next", Material.ARROW, ChatColor.YELLOW + "Next Page"); - next.onLeftClick(clicked -> refreshDetails( - window.getViewer(), - state.requestId(), - workcell.stableId(), - variant.pieceKey(), - page + 1)); - window.setElement(1, 5, next); - } - window.setElement(2, 5, evaluationElement(state.evaluation())); - - UIElement toolbox = element("details-toolbox", Material.STICK, ChatColor.AQUA + "Toolbox"); - toolbox.onLeftClick(clicked -> openToolbox( - window.getViewer(), state.requestId(), workcell.stableId(), 0)); - window.setElement(4, 5, toolbox); - } - - private void renderVariantSettings( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - JigsawStudioMenuState.Variant variant, - int requestedPage - ) { - int page = clampPage(requestedPage, state.themeSets().size(), THEME_SETS_PER_PAGE); - window.batch(() -> { - window.clearElements(); - - UIElement back = element("variant-settings-back", Material.ARROW, ChatColor.YELLOW + "Back to Details"); - back.onLeftClick(clicked -> openDetails( - window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); - window.setElement(-4, 0, back); - - UIElement identity = element( - "variant-settings-identity", - Material.COMPARATOR, - ChatColor.GOLD + safe(variant.displayName()) + " Rules"); - identity.addLore(ChatColor.DARK_GRAY + safe(variant.pieceKey())); - identity.addLore(ChatColor.GRAY + "Theme memberships: " + themes(variant.themes())); - identity.addLore(ChatColor.GRAY + "Use Duplicate All Enabled Cells as Family for coherent sets."); - window.setElement(0, 0, identity); - window.setElement(4, 0, evaluationElement(state.evaluation())); - - if (!variant.active() || !variant.owned() || !state.irisExtended()) { - UIElement unavailable = element( - "variant-settings-unavailable", - Material.BARRIER, - state.irisExtended() - ? ChatColor.RED + "Load an Owned Variant First" - : ChatColor.GRAY + "Iris Metadata Unavailable"); - unavailable.addLore(state.irisExtended() - ? ChatColor.GRAY + "Rules and themes can only change for the loaded owned variant." - : ChatColor.GRAY + "Vanilla-portable pieces use vanilla placement behavior."); - window.setElement(0, 2, unavailable); - } else { - window.setElement(-4, 1, ruleElement( - window, state, workcell, variant, RuleField.MINIMUM_DEPTH)); - window.setElement(-2, 1, ruleElement( - window, state, workcell, variant, RuleField.MAXIMUM_DEPTH)); - window.setElement(0, 1, ruleElement( - window, state, workcell, variant, RuleField.MINIMUM_PLACEMENTS)); - window.setElement(2, 1, ruleElement( - window, state, workcell, variant, RuleField.MAXIMUM_PLACEMENTS)); - - UIElement terminal = element( - "rule-terminal", - variant.rules().terminal() ? Material.REDSTONE_TORCH : Material.LEVER, - variant.rules().terminal() - ? ChatColor.GREEN + "Terminal Piece: Yes" - : ChatColor.YELLOW + "Terminal Piece: No"); - terminal.addLore(ChatColor.GRAY + "Terminal pieces consume a connector without expanding."); - terminal.addLore(ChatColor.YELLOW + "Left-click to toggle"); - terminal.onLeftClick(clicked -> updateVariantRules( - window.getViewer(), - state.requestId(), - workcell.stableId(), - variant.pieceKey(), - withTerminal(variant.rules(), !variant.rules().terminal()))); - window.setElement(4, 1, terminal); - - addVariantThemeMemberships(window, state, workcell, variant, page); - } - - UIElement footerBack = element("variant-rules-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); - footerBack.onLeftClick(clicked -> openDetails( - window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); - window.setElement(-4, 5, footerBack); - - int pageCount = pageCount(state.themeSets().size(), THEME_SETS_PER_PAGE); - if (page > 0) { - UIElement previous = element( - "variant-theme-previous", - Material.ARROW, - ChatColor.YELLOW + "Previous Page"); - previous.onLeftClick(clicked -> refreshVariantSettings( - window.getViewer(), - state.requestId(), - workcell.stableId(), - variant.pieceKey(), - page - 1)); - window.setElement(-1, 5, previous); - } - UIElement indicator = element( - "variant-theme-page", - Material.BOOK, - ChatColor.WHITE + "Theme Page " + (page + 1) + "/" + pageCount); - indicator.addLore(ChatColor.GRAY + "" + state.themeSets().size() + " declared themes"); - window.setElement(0, 5, indicator); - if (page + 1 < pageCount) { - UIElement next = element( - "variant-theme-next", - Material.ARROW, - ChatColor.YELLOW + "Next Page"); - next.onLeftClick(clicked -> refreshVariantSettings( - window.getViewer(), - state.requestId(), - workcell.stableId(), - variant.pieceKey(), - page + 1)); - window.setElement(1, 5, next); - } - window.setElement(4, 5, evaluationElement(state.evaluation())); - }); - } - - private UIElement ruleElement( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - JigsawStudioMenuState.Variant variant, - RuleField field - ) { - int value = field.value(variant.rules()); - UIElement element = element( - "rule-" + field.name().toLowerCase(Locale.ROOT), - field.material(), - ChatColor.AQUA + field.displayName() + ": " + field.displayValue(value)); - boolean unlimitedMaximum = field == RuleField.MAXIMUM_PLACEMENTS && value == 0; - if (unlimitedMaximum) { - element.addLore(ChatColor.GRAY + "Increase is already unlimited."); - } else { - element.addLore(ChatColor.GREEN + "Left-click: +1"); - element.addLore(ChatColor.GREEN + "Shift-left: +" + field.shiftStep()); - element.onLeftClick(clicked -> adjustVariantRule( - window.getViewer(), state.requestId(), workcell, variant, field, 1)); - element.onShiftLeftClick(clicked -> adjustVariantRule( - window.getViewer(), state.requestId(), workcell, variant, field, field.shiftStep())); - } - element.addLore(unlimitedMaximum - ? ChatColor.YELLOW + "Right-click: set 512" - : ChatColor.YELLOW + "Right-click: -1"); - element.addLore(unlimitedMaximum - ? ChatColor.YELLOW + "Shift-right: set 497" - : ChatColor.YELLOW + "Shift-right: -" + field.shiftStep()); - element.onRightClick(clicked -> adjustVariantRule( - window.getViewer(), state.requestId(), workcell, variant, field, -1)); - element.onShiftRightClick(clicked -> adjustVariantRule( - window.getViewer(), state.requestId(), workcell, variant, field, -field.shiftStep())); - return element; - } - - private void addVariantThemeMemberships( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - JigsawStudioMenuState.Variant variant, - int page - ) { - List themeSets = page( - state.themeSets(), page, THEME_SETS_PER_PAGE); - if (themeSets.isEmpty()) { - UIElement empty = element( - "variant-no-themes", - Material.GRAY_DYE, - ChatColor.GRAY + "Available to Every Theme"); - empty.addLore(ChatColor.GRAY + "Create a coherent theme set from Structure Rules first."); - window.setElement(0, 3, empty); - return; - } - for (int index = 0; index < themeSets.size(); index++) { - JigsawStudioMenuState.ThemeSet themeSet = themeSets.get(index); - boolean member = variant.themes().contains(themeSet.key()); - int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; - int row = 2 + index / GRID_POSITIONS.length; - UIElement element = element( - "variant-theme-" + index, - member ? Material.LIME_DYE : Material.GRAY_DYE, - (member ? ChatColor.GREEN : ChatColor.GRAY) + safe(themeSet.key())) - .setEnchanted(member); - element.addLore(member - ? ChatColor.GREEN + "This variant belongs to the theme." - : ChatColor.GRAY + "This variant does not belong to the theme."); - element.addLore(ChatColor.GRAY + "Only variants in the selected family are eligible."); - element.addLore(ChatColor.YELLOW + "Left-click to toggle membership"); - element.onLeftClick(clicked -> toggleVariantTheme( - window.getViewer(), state.requestId(), workcell, variant, themeSet.key())); - window.setElement(position, row, element); - } - } - - private void renderToolbox( - UIWindow window, - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell, - int requestedPage - ) { - List allTools = toolboxTools(state, workcell); - int page = clampPage(requestedPage, allTools.size(), TOOLS_PER_PAGE); - List tools = page(allTools, page, TOOLS_PER_PAGE); - window.batch(() -> { - window.clearElements(); - - UIElement back = element("toolbox-back", Material.ARROW, ChatColor.YELLOW + "Back to Variants"); - back.onLeftClick(clicked -> refreshMain( - window.getViewer(), state.requestId(), workcell.stableId(), 0)); - window.setElement(-4, 0, back); - - UIElement heading = element("toolbox-heading", Material.CHEST, ChatColor.AQUA + "Bound Tool Sticks"); - heading.addLore(ChatColor.GRAY + "Every stick is bound to request " + state.requestId()); - heading.addLore(ChatColor.GRAY + "Stale sticks stop working when this Studio closes."); - window.setElement(0, 0, heading); - window.setElement(4, 0, evaluationElement(state.evaluation())); - - for (int index = 0; index < tools.size(); index++) { - ToolboxTool tool = tools.get(index); - int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; - int row = 1 + index / GRID_POSITIONS.length; - UIElement element = element( - "tool-" + index, - Material.STICK, - ChatColor.AQUA + safe(tool.displayName())); - element.addLore(ChatColor.GRAY + "Action: " + tool.payload().action().displayName()); - addToolBindingLore(element, tool.payload()); - element.addLore(tool.payload().action().destructive() - ? ChatColor.RED + "Bound tools require confirmation when used." - : ChatColor.YELLOW + "Left-click to receive this named stick."); - element.onLeftClick(clicked -> giveTool( - window.getViewer(), state.requestId(), tool.payload())); - window.setElement(position, row, element); - } - - UIElement footerBack = element("toolbox-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); - footerBack.onLeftClick(clicked -> refreshMain( - window.getViewer(), state.requestId(), workcell.stableId(), 0)); - window.setElement(-4, 5, footerBack); - - int pageCount = pageCount(allTools.size(), TOOLS_PER_PAGE); - if (page > 0) { - UIElement previous = element("toolbox-previous", Material.ARROW, ChatColor.YELLOW + "Previous Page"); - previous.onLeftClick(clicked -> refreshToolbox( - window.getViewer(), state.requestId(), workcell.stableId(), page - 1)); - window.setElement(-1, 5, previous); - } - UIElement indicator = element("toolbox-page", Material.BOOK, ChatColor.WHITE + "Page " - + (page + 1) + "/" + pageCount); - indicator.addLore(ChatColor.GRAY + "" + allTools.size() + " bound tools"); - window.setElement(0, 5, indicator); - if (page + 1 < pageCount) { - UIElement next = element("toolbox-next", Material.ARROW, ChatColor.YELLOW + "Next Page"); - next.onLeftClick(clicked -> refreshToolbox( - window.getViewer(), state.requestId(), workcell.stableId(), page + 1)); - window.setElement(1, 5, next); - } - }); - } - - private void selectWorkcell(Player player, UUID requestId, String workcellId) { + void selectWorkcell(Player player, UUID requestId, String workcellId) { if (matchingState(player, requestId, true).isEmpty()) { return; } @@ -1528,7 +283,7 @@ public final class JigsawStudioMenuController { } } - private void toggleConnectorBlocks( + void toggleConnectorBlocks( Player player, UUID requestId, String workcellId, @@ -1543,7 +298,7 @@ public final class JigsawStudioMenuController { } } - private void resetConnectorBlocks(Player player, UUID requestId, String workcellId) { + void resetConnectorBlocks(Player player, UUID requestId, String workcellId) { Optional current = matchingState(player, requestId, true); if (current.isEmpty() || current.get().workcell(workcellId) == null) { return; @@ -1553,7 +308,7 @@ public final class JigsawStudioMenuController { } } - private void undoAutosave(Player player, UUID requestId) { + void undoAutosave(Player player, UUID requestId) { if (matchingState(player, requestId, true).isEmpty()) { return; } @@ -1562,7 +317,7 @@ public final class JigsawStudioMenuController { } } - private void switchVariant(Player player, UUID requestId, String workcellId, String pieceKey) { + void switchVariant(Player player, UUID requestId, String workcellId, String pieceKey) { Optional current = matchingState(player, requestId, true); if (current.isEmpty()) { return; @@ -1582,7 +337,7 @@ public final class JigsawStudioMenuController { } } - private void openDetails( + void openDetails( Player player, UUID requestId, String workcellId, @@ -1601,11 +356,11 @@ public final class JigsawStudioMenuController { } UIWindow window = windows.get(player.getUniqueId()); if (window != null) { - renderDetails(window, current.get(), workcell, variant, page); + detailsMenu.render(window, current.get(), workcell, variant, page); } } - private void openWorkcellSettings(Player player, UUID requestId, String workcellId) { + void openWorkcellSettings(Player player, UUID requestId, String workcellId) { Optional current = matchingState(player, requestId, true); if (current.isEmpty()) { return; @@ -1616,10 +371,10 @@ public final class JigsawStudioMenuController { stale(player); return; } - renderWorkcellSettings(window, current.get(), workcell); + workcellMenu.render(window, current.get(), workcell); } - private void openStructureSettings( + void openStructureSettings( Player player, UUID requestId, String selectedWorkcellId, @@ -1634,10 +389,10 @@ public final class JigsawStudioMenuController { stale(player); return; } - renderStructureSettings(window, current.get(), selectedWorkcellId, page); + structureMenu.render(window, current.get(), selectedWorkcellId, page); } - private void openVariantSettings( + void openVariantSettings( Player player, UUID requestId, String workcellId, @@ -1664,10 +419,10 @@ public final class JigsawStudioMenuController { + "Vanilla-portable pieces cannot encode Iris theme or piece-rule metadata."); return; } - renderVariantSettings(window, current.get(), workcell, variant, page); + variantMenu.render(window, current.get(), workcell, variant, page); } - private void openVariantSizeSettings( + void openVariantSizeSettings( Player player, UUID requestId, String workcellId, @@ -1685,10 +440,10 @@ public final class JigsawStudioMenuController { stale(player); return; } - renderVariantSizeSettings(window, current.get(), workcell, variant); + workcellMenu.renderSize(window, current.get(), workcell, variant); } - private void openToolbox(Player player, UUID requestId, String workcellId, int page) { + void openToolbox(Player player, UUID requestId, String workcellId, int page) { Optional current = matchingState(player, requestId, true); if (current.isEmpty()) { return; @@ -1699,10 +454,10 @@ public final class JigsawStudioMenuController { stale(player); return; } - renderToolbox(window, current.get(), workcell, page); + toolboxMenu.render(window, current.get(), workcell, page); } - private void createVariant( + void createVariant( Player player, UUID requestId, String workcellId, @@ -1716,7 +471,7 @@ public final class JigsawStudioMenuController { } } - private void setWorkcellEnabled( + void setWorkcellEnabled( Player player, UUID requestId, String workcellId, @@ -1735,179 +490,7 @@ public final class JigsawStudioMenuController { } } - private void resizeWorkcell( - Player player, - UUID requestId, - String workcellId, - DimensionAxis axis, - int delta - ) { - Optional current = matchingState(player, requestId, true); - if (current.isEmpty()) { - return; - } - JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); - if (workcell == null) { - stale(player); - return; - } - UUID playerId = player.getUniqueId(); - PendingWorkcellResize existing = pendingWorkcellResize(playerId, requestId, workcellId); - if (existing != null && existing.applying()) { - send(player, ChatColor.YELLOW + "That cell size is already being applied."); - return; - } - JigsawStudioCellDimensions base = existing == null - ? workcell.capacity() - : existing.dimensions(); - Optional adjusted = adjustedDimensions( - base, axis, delta); - if (adjusted.isEmpty()) { - send(player, ChatColor.RED + "That workcell size is outside Iris limits."); - return; - } - PendingWorkcellResize pending = new PendingWorkcellResize( - requestId, - workcellId, - adjusted.get(), - false); - pendingWorkcellResizes.put(playerId, pending); - UIWindow window = windows.get(playerId); - if (window != null) { - renderWorkcellSettings(window, current.get(), withCapacity(workcell, pending.dimensions())); - } - } - - private void applyWorkcellResize(Player player, UUID requestId, String workcellId) { - Optional current = matchingState(player, requestId, true); - if (current.isEmpty()) { - return; - } - UUID playerId = player.getUniqueId(); - PendingWorkcellResize pending = pendingWorkcellResize(playerId, requestId, workcellId); - JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); - if (pending == null || pending.applying() || workcell == null) { - return; - } - if (workcell.capacity().equals(pending.dimensions())) { - pendingWorkcellResizes.remove(playerId, pending); - refreshWorkcellSettings(player, requestId, workcellId); - return; - } - PendingWorkcellResize applying = new PendingWorkcellResize( - requestId, - workcellId, - pending.dimensions(), - true); - pendingWorkcellResizes.put(playerId, applying); - UIWindow window = windows.get(playerId); - if (window != null) { - renderWorkcellSettings(window, current.get(), withCapacity(workcell, applying.dimensions())); - } - if (!actions.updateWorkcellDimensions(player, workcellId, applying.dimensions())) { - pendingWorkcellResizes.replace(playerId, applying, pending); - refreshWorkcellSettings(player, requestId, workcellId); - return; - } - scheduleWorkcellResizeRefresh(player, applying, WORKCELL_RESIZE_REFRESH_ATTEMPTS); - } - - private void scheduleWorkcellResizeRefresh( - Player player, - PendingWorkcellResize pending, - int attemptsRemaining - ) { - boolean scheduled = J.runEntity( - player, - () -> refreshAppliedWorkcellResize(player, pending, attemptsRemaining), - WORKCELL_RESIZE_REFRESH_TICKS); - if (!scheduled) { - pendingWorkcellResizes.remove(player.getUniqueId(), pending); - } - } - - private void refreshAppliedWorkcellResize( - Player player, - PendingWorkcellResize pending, - int attemptsRemaining - ) { - UUID playerId = player.getUniqueId(); - if (!pending.equals(pendingWorkcellResizes.get(playerId))) { - return; - } - Optional current = matchingState(player, pending.requestId(), false); - JigsawStudioMenuState.Workcell workcell = current - .map(state -> state.workcell(pending.workcellId())) - .orElse(null); - if (workcell == null) { - pendingWorkcellResizes.remove(playerId, pending); - return; - } - if (workcell.capacity().equals(pending.dimensions())) { - pendingWorkcellResizes.remove(playerId, pending); - UIWindow window = windows.get(playerId); - if (window != null) { - renderWorkcellSettings(window, current.orElseThrow(), workcell); - } - return; - } - if (attemptsRemaining > 0) { - scheduleWorkcellResizeRefresh(player, pending, attemptsRemaining - 1); - return; - } - PendingWorkcellResize retry = new PendingWorkcellResize( - pending.requestId(), - pending.workcellId(), - pending.dimensions(), - false); - pendingWorkcellResizes.replace(playerId, pending, retry); - UIWindow window = windows.get(playerId); - if (window != null) { - renderWorkcellSettings(window, current.orElseThrow(), withCapacity(workcell, retry.dimensions())); - } - send(player, ChatColor.YELLOW - + "Cell resizing is still pending; use Apply Cell Size to retry after the current operation settles."); - } - - private void discardWorkcellResize(Player player, UUID requestId, String workcellId) { - PendingWorkcellResize pending = pendingWorkcellResize(player.getUniqueId(), requestId, workcellId); - if (pending != null && !pending.applying()) { - pendingWorkcellResizes.remove(player.getUniqueId(), pending); - } - refreshWorkcellSettings(player, requestId, workcellId); - } - - private void leaveWorkcellSettings(Player player, UUID requestId, String workcellId) { - PendingWorkcellResize pending = pendingWorkcellResize(player.getUniqueId(), requestId, workcellId); - if (pending != null && !pending.applying()) { - pendingWorkcellResizes.remove(player.getUniqueId(), pending); - } - refreshMain(player, requestId, workcellId, 0); - } - - private void refreshWorkcellSettings(Player player, UUID requestId, String workcellId) { - Optional current = matchingState(player, requestId, true); - UIWindow window = windows.get(player.getUniqueId()); - JigsawStudioMenuState.Workcell workcell = current - .map(state -> state.workcell(workcellId)) - .orElse(null); - if (window == null || workcell == null) { - return; - } - renderWorkcellSettings(window, current.orElseThrow(), workcell); - } - - private PendingWorkcellResize pendingWorkcellResize(UUID playerId, UUID requestId, String workcellId) { - PendingWorkcellResize pending = pendingWorkcellResizes.get(playerId); - if (pending == null - || !pending.requestId().equals(requestId) - || !pending.workcellId().equals(workcellId)) { - return null; - } - return pending; - } - - private void resizeVariantAxis( + void resizeVariantAxis( Player player, UUID requestId, String workcellId, @@ -1934,7 +517,7 @@ public final class JigsawStudioMenuController { resizeVariant(player, requestId, workcellId, pieceKey, adjusted.get()); } - private void resizeVariant( + void resizeVariant( Player player, UUID requestId, String workcellId, @@ -1962,7 +545,7 @@ public final class JigsawStudioMenuController { } } - private void setRequireCaps(Player player, UUID requestId, boolean requireCaps) { + void setRequireCaps(Player player, UUID requestId, boolean requireCaps) { Optional current = matchingState(player, requestId, true); if (current.isEmpty()) { return; @@ -1982,7 +565,7 @@ public final class JigsawStudioMenuController { } } - private void duplicateActiveFamily(Player player, UUID requestId, String themeKey) { + void duplicateActiveFamily(Player player, UUID requestId, String themeKey) { Optional current = matchingState(player, requestId, true); if (current.isEmpty()) { return; @@ -2001,7 +584,7 @@ public final class JigsawStudioMenuController { } } - private void adjustThemeSetWeight( + void adjustThemeSetWeight( Player player, UUID requestId, JigsawStudioMenuState.ThemeSet expected, @@ -2031,7 +614,7 @@ public final class JigsawStudioMenuController { } } - private void flushNow(Player player, UUID requestId, String workcellId) { + void flushNow(Player player, UUID requestId, String workcellId) { Optional current = matchingState(player, requestId, true); if (current.isEmpty()) { return; @@ -2050,14 +633,14 @@ public final class JigsawStudioMenuController { } } - private void goToPreview(Player player, UUID requestId) { + void goToPreview(Player player, UUID requestId) { if (matchingState(player, requestId, true).isPresent() && actions.goToPreview(player)) { closeAfterAction(player); } } - private void toggleRotation(Player player, UUID requestId, String workcellId) { + void toggleRotation(Player player, UUID requestId, String workcellId) { Optional current = matchingState(player, requestId, true); if (current.isEmpty() || activeVariant(current.get(), workcellId) == null) { return; @@ -2067,7 +650,7 @@ public final class JigsawStudioMenuController { } } - private void adjustVariantRule( + void adjustVariantRule( Player player, UUID requestId, JigsawStudioMenuState.Workcell workcell, @@ -2097,7 +680,7 @@ public final class JigsawStudioMenuController { rules.get()); } - private void updateVariantRules( + void updateVariantRules( Player player, UUID requestId, String workcellId, @@ -2123,7 +706,7 @@ public final class JigsawStudioMenuController { } } - private void toggleVariantTheme( + void toggleVariantTheme( Player player, UUID requestId, JigsawStudioMenuState.Workcell workcell, @@ -2160,7 +743,7 @@ public final class JigsawStudioMenuController { } } - private void adjustMembershipWeight( + void adjustMembershipWeight( Player player, UUID requestId, String workcellId, @@ -2189,7 +772,7 @@ public final class JigsawStudioMenuController { } } - private void adjustMembershipChance( + void adjustMembershipChance( Player player, UUID requestId, String workcellId, @@ -2222,7 +805,7 @@ public final class JigsawStudioMenuController { } } - private void confirmOrUnlink( + void confirmOrUnlink( Player player, UUID requestId, String workcellId, @@ -2264,7 +847,7 @@ public final class JigsawStudioMenuController { } } - private void confirmOrDeleteVariant( + void confirmOrDeleteVariant( Player player, UUID requestId, String workcellId, @@ -2300,7 +883,7 @@ public final class JigsawStudioMenuController { } } - private void confirmOrDeleteProject( + void confirmOrDeleteProject( Player player, UUID requestId, String selectedWorkcellId, @@ -2325,7 +908,7 @@ public final class JigsawStudioMenuController { } } - private void giveTool(Player player, UUID requestId, JigsawStudioToolPayload payload) { + void giveTool(Player player, UUID requestId, JigsawStudioToolPayload payload) { if (matchingState(player, requestId, true).isEmpty()) { return; } @@ -2336,7 +919,7 @@ public final class JigsawStudioMenuController { actions.giveTool(player, payload); } - private void refreshMain(Player player, UUID requestId, String workcellId, int page) { + void refreshMain(Player player, UUID requestId, String workcellId, int page) { Optional current = matchingState(player, requestId, true); if (current.isEmpty()) { return; @@ -2347,10 +930,10 @@ public final class JigsawStudioMenuController { stale(player); return; } - renderMain(window, current.get(), workcell, page); + mainMenu.render(window, current.get(), workcell, page); } - private void refreshDetails(Player player, UUID requestId, String workcellId, int page) { + void refreshDetails(Player player, UUID requestId, String workcellId, int page) { Optional current = matchingState(player, requestId, true); if (current.isEmpty()) { return; @@ -2363,7 +946,7 @@ public final class JigsawStudioMenuController { refreshDetails(player, requestId, workcellId, active.pieceKey(), page); } - private void refreshDetails( + void refreshDetails( Player player, UUID requestId, String workcellId, @@ -2381,10 +964,10 @@ public final class JigsawStudioMenuController { stale(player); return; } - renderDetails(window, current.get(), workcell, variant, page); + detailsMenu.render(window, current.get(), workcell, variant, page); } - private void refreshStructureSettings( + void refreshStructureSettings( Player player, UUID requestId, String selectedWorkcellId, @@ -2399,10 +982,10 @@ public final class JigsawStudioMenuController { stale(player); return; } - renderStructureSettings(window, current.get(), selectedWorkcellId, page); + structureMenu.render(window, current.get(), selectedWorkcellId, page); } - private void refreshVariantSettings( + void refreshVariantSettings( Player player, UUID requestId, String workcellId, @@ -2420,10 +1003,10 @@ public final class JigsawStudioMenuController { stale(player); return; } - renderVariantSettings(window, current.get(), workcell, variant, page); + variantMenu.render(window, current.get(), workcell, variant, page); } - private void refreshToolbox(Player player, UUID requestId, String workcellId, int page) { + void refreshToolbox(Player player, UUID requestId, String workcellId, int page) { Optional current = matchingState(player, requestId, true); if (current.isEmpty()) { return; @@ -2434,10 +1017,10 @@ public final class JigsawStudioMenuController { stale(player); return; } - renderToolbox(window, current.get(), workcell, page); + toolboxMenu.render(window, current.get(), workcell, page); } - private Optional matchingState(Player player, UUID requestId, boolean closeStale) { + Optional matchingState(Player player, UUID requestId, boolean closeStale) { Optional current = actions.menuState(player); if (current.isPresent() && current.get().requestId().equals(requestId)) { return current; @@ -2448,12 +1031,12 @@ public final class JigsawStudioMenuController { return Optional.empty(); } - private void stale(Player player) { + void stale(Player player) { closeAfterAction(player); send(player, ChatColor.RED + "This Jigsaw Studio menu is stale. Open the control chest again."); } - private static void send(Player player, String message) { + static void send(Player player, String message) { ComponentMessenger.sendSection(player, message); } @@ -2461,7 +1044,7 @@ public final class JigsawStudioMenuController { J.runEntity(player, () -> close(player), 1); } - private boolean isUnlinkConfirmed( + boolean isUnlinkConfirmed( UUID playerId, UUID requestId, String workcellId, @@ -2483,7 +1066,7 @@ public final class JigsawStudioMenuController { && pending.entryIndex() == membership.entryIndex(); } - private boolean isDeleteConfirmed( + boolean isDeleteConfirmed( UUID playerId, UUID requestId, String workcellId, @@ -2502,7 +1085,7 @@ public final class JigsawStudioMenuController { && pending.pieceKey().equals(pieceKey); } - private boolean isProjectDeleteConfirmed(UUID playerId, UUID requestId) { + boolean isProjectDeleteConfirmed(UUID playerId, UUID requestId) { PendingProjectDelete pending = pendingProjectDeletes.get(playerId); if (pending == null) { return false; @@ -2514,7 +1097,7 @@ public final class JigsawStudioMenuController { return pending.requestId().equals(requestId); } - private void purgeExpiredConfirmations(UUID playerId) { + void purgeExpiredConfirmations(UUID playerId) { PendingUnlink unlink = pendingUnlinks.get(playerId); if (unlink != null && unlink.expiresAtNanos() < System.nanoTime()) { pendingUnlinks.remove(playerId, unlink); @@ -2535,833 +1118,6 @@ public final class JigsawStudioMenuController { pendingProjectDeletes.remove(playerId); } - private static boolean hasMembership( - JigsawStudioMenuState state, - String workcellId, - JigsawStudioMenuState.Membership expected - ) { - JigsawStudioMenuState.Variant active = activeVariant(state, workcellId); - if (active == null) { - return false; - } - for (JigsawStudioMenuState.Membership membership : active.memberships()) { - if (membership.poolKey().equals(expected.poolKey()) - && membership.entryIndex() == expected.entryIndex()) { - return true; - } - } - return false; - } - - private static JigsawStudioMenuState.Variant activeVariant( - JigsawStudioMenuState state, - String workcellId - ) { - JigsawStudioMenuState.Workcell workcell = state.workcell(workcellId); - return workcell == null ? null : workcell.activeVariant(); - } - - private static JigsawStudioMenuState.Variant variant( - JigsawStudioMenuState.Workcell workcell, - String pieceKey - ) { - if (workcell == null || pieceKey == null) { - return null; - } - for (JigsawStudioMenuState.Variant variant : workcell.variants()) { - if (variant.pieceKey().equals(pieceKey)) { - return variant; - } - } - return null; - } - - static String nextThemeSetKey(List themeSets) { - List values = List.copyOf(Objects.requireNonNull( - themeSets, - "Jigsaw Studio theme sets")); - int candidate = 1; - while (candidate > 0) { - String key = "variant-" + candidate; - boolean present = false; - for (JigsawStudioMenuState.ThemeSet themeSet : values) { - if (Objects.requireNonNull(themeSet, "Jigsaw Studio theme set").key().equals(key)) { - present = true; - break; - } - } - if (!present) { - return key; - } - candidate = Math.incrementExact(candidate); - } - throw new IllegalStateException("Jigsaw Studio cannot allocate another numbered theme set"); - } - - static String themeSelectionPercent( - List themeSets, - JigsawStudioMenuState.ThemeSet target - ) { - List activeThemeSets = Objects.requireNonNull( - themeSets, - "Jigsaw Studio theme sets"); - JigsawStudioMenuState.ThemeSet activeTarget = Objects.requireNonNull( - target, - "Jigsaw Studio target theme set"); - int totalWeight = 0; - for (JigsawStudioMenuState.ThemeSet themeSet : activeThemeSets) { - totalWeight = Math.addExact(totalWeight, Objects.requireNonNull( - themeSet, - "Jigsaw Studio theme set").weight()); - } - if (totalWeight < 1) { - return "0.0%"; - } - return String.format( - Locale.ROOT, - "%.1f%%", - activeTarget.weight() * 100.0D / totalWeight); - } - - static Optional adjustedPositiveValue(int value, int delta) { - if (value < 1 || delta == 0) { - throw new IllegalArgumentException("Jigsaw Studio positive value adjustment is invalid"); - } - try { - int adjusted = Math.addExact(value, delta); - return adjusted < 1 ? Optional.empty() : Optional.of(adjusted); - } catch (ArithmeticException exception) { - return Optional.empty(); - } - } - - static Optional adjustedRules( - JigsawStudioPieceRules rules, - RuleField field, - int delta - ) { - JigsawStudioPieceRules current = Objects.requireNonNull(rules, "Jigsaw Studio piece rules"); - RuleField target = Objects.requireNonNull(field, "Jigsaw Studio piece rule field"); - if (delta == 0) { - throw new IllegalArgumentException("Jigsaw Studio piece rule delta cannot be zero"); - } - try { - int minimumDepth = current.minimumDepth(); - int maximumDepth = current.maximumDepth(); - int minimumPlacements = current.minimumPlacements(); - int maximumPlacements = current.maximumPlacements(); - switch (target) { - case MINIMUM_DEPTH -> minimumDepth = Math.addExact(minimumDepth, delta); - case MAXIMUM_DEPTH -> maximumDepth = Math.addExact(maximumDepth, delta); - case MINIMUM_PLACEMENTS -> minimumPlacements = Math.addExact(minimumPlacements, delta); - case MAXIMUM_PLACEMENTS -> { - Optional adjustedMaximum = adjustedMaximumPlacements(maximumPlacements, delta); - if (adjustedMaximum.isEmpty()) { - return Optional.empty(); - } - maximumPlacements = adjustedMaximum.get(); - } - } - return Optional.of(new JigsawStudioPieceRules( - minimumDepth, - maximumDepth, - minimumPlacements, - maximumPlacements, - current.terminal())); - } catch (ArithmeticException | IllegalArgumentException exception) { - return Optional.empty(); - } - } - - static JigsawStudioPieceRules withTerminal(JigsawStudioPieceRules rules, boolean terminal) { - JigsawStudioPieceRules current = Objects.requireNonNull(rules, "Jigsaw Studio piece rules"); - return new JigsawStudioPieceRules( - current.minimumDepth(), - current.maximumDepth(), - current.minimumPlacements(), - current.maximumPlacements(), - terminal); - } - - private static Optional adjustedMaximumPlacements(int value, int delta) { - if (value == 0) { - if (delta > 0) { - return Optional.empty(); - } - int adjusted = Math.addExact(513, delta); - return adjusted < 1 ? Optional.empty() : Optional.of(adjusted); - } - int adjusted = Math.addExact(value, delta); - if (adjusted > 512) { - return Optional.of(0); - } - return adjusted < 1 ? Optional.empty() : Optional.of(adjusted); - } - - static Optional adjustedDimensions( - JigsawStudioCellDimensions dimensions, - DimensionAxis axis, - int delta - ) { - JigsawStudioCellDimensions current = Objects.requireNonNull( - dimensions, - "Jigsaw Studio workcell dimensions"); - DimensionAxis target = Objects.requireNonNull(axis, "Jigsaw Studio workcell dimension axis"); - if (delta == 0) { - throw new IllegalArgumentException("Jigsaw Studio workcell dimension delta cannot be zero"); - } - try { - int width = target == DimensionAxis.WIDTH - ? Math.addExact(current.width(), delta) - : current.width(); - int height = target == DimensionAxis.HEIGHT - ? Math.addExact(current.height(), delta) - : current.height(); - int depth = target == DimensionAxis.DEPTH - ? Math.addExact(current.depth(), delta) - : current.depth(); - return Optional.of(new JigsawStudioCellDimensions(width, height, depth)); - } catch (ArithmeticException | IllegalArgumentException exception) { - return Optional.empty(); - } - } - - static JigsawStudioMenuState.Workcell withCapacity( - JigsawStudioMenuState.Workcell workcell, - JigsawStudioCellDimensions capacity - ) { - JigsawStudioMenuState.Workcell source = Objects.requireNonNull( - workcell, - "Jigsaw Studio menu workcell"); - return new JigsawStudioMenuState.Workcell( - source.stableId(), - source.canonicalName(), - source.displayName(), - Objects.requireNonNull(capacity, "Jigsaw Studio staged workcell capacity"), - source.enabled(), - source.activeVariantKey(), - source.dirty(), - source.saving(), - source.loading(), - source.connectorsVisible(), - source.variants()); - } - - static List toolboxTools( - JigsawStudioMenuState state, - JigsawStudioMenuState.Workcell workcell - ) { - JigsawStudioMenuState menu = Objects.requireNonNull(state, "Jigsaw Studio toolbox state"); - JigsawStudioMenuState.Workcell selected = Objects.requireNonNull( - workcell, - "Jigsaw Studio toolbox workcell"); - UUID requestId = menu.requestId(); - List tools = new ArrayList<>(); - tools.add(new ToolboxTool( - "Open Control Menu", - JigsawStudioToolPayload.request(JigsawStudioToolAction.OPEN_MENU, requestId))); - tools.add(new ToolboxTool( - "Select " + selected.displayName(), - JigsawStudioToolPayload.workcell( - JigsawStudioToolAction.SELECT_WORKCELL, - requestId, - selected.stableId()))); - if (menu.mode() == JigsawStudioMode.PLANAR_JIGSAW) { - tools.add(new ToolboxTool( - "Toggle " + selected.displayName(), - JigsawStudioToolPayload.workcell( - JigsawStudioToolAction.TOGGLE_WORKCELL, - requestId, - selected.stableId()))); - } - tools.add(new ToolboxTool( - "Resize " + selected.displayName() + " Capacity", - JigsawStudioToolPayload.workcell( - JigsawStudioToolAction.RESIZE_WORKCELL, - requestId, - selected.stableId()))); - tools.add(new ToolboxTool( - "Rename " + selected.displayName(), - JigsawStudioToolPayload.workcell( - JigsawStudioToolAction.RENAME_WORKCELL, - requestId, - selected.stableId()))); - tools.add(new ToolboxTool( - "New Blank " + selected.displayName() + " Variant", - JigsawStudioToolPayload.workcell( - JigsawStudioToolAction.CREATE_VARIANT, - requestId, - selected.stableId()))); - tools.add(new ToolboxTool( - "Go to Preview", - JigsawStudioToolPayload.request(JigsawStudioToolAction.PREVIEW_GRAPH, requestId))); - tools.add(new ToolboxTool( - "Flush " + selected.displayName() + " Autosave", - JigsawStudioToolPayload.workcell( - JigsawStudioToolAction.FLUSH_AUTOSAVE, - requestId, - selected.stableId()))); - if (menu.irisExtended()) { - tools.add(new ToolboxTool( - "Duplicate All Enabled Cells as Family: " + nextThemeSetKey(menu.themeSets()), - JigsawStudioToolPayload.request(JigsawStudioToolAction.DUPLICATE_FAMILY, requestId))); - } - - JigsawStudioMenuState.Variant active = selected.activeVariant(); - if (active != null) { - if (active.owned()) { - tools.add(new ToolboxTool( - "Duplicate This Cell's Variant: " + active.displayName(), - JigsawStudioToolPayload.variant( - JigsawStudioToolAction.DUPLICATE_VARIANT, - requestId, - selected.stableId(), - active.pieceKey()))); - } - if (active.owned()) { - tools.add(new ToolboxTool( - "Rename This Variant: " + active.displayName(), - JigsawStudioToolPayload.variant( - JigsawStudioToolAction.RENAME_VARIANT, - requestId, - selected.stableId(), - active.pieceKey()))); - tools.add(new ToolboxTool( - "Resize This Variant: " + active.displayName(), - JigsawStudioToolPayload.variant( - JigsawStudioToolAction.RESIZE_VARIANT, - requestId, - selected.stableId(), - active.pieceKey()))); - } - if (active.rotationEditable()) { - tools.add(new ToolboxTool( - "Toggle Rotation: " + active.displayName(), - JigsawStudioToolPayload.variant( - JigsawStudioToolAction.TOGGLE_ROTATION, - requestId, - selected.stableId(), - active.pieceKey()))); - } - if (active.owned() && active.resizableToCapacity()) { - tools.add(new ToolboxTool( - "Resize " + active.displayName() + " to Capacity", - JigsawStudioToolPayload.variant( - JigsawStudioToolAction.EXPAND_TO_CELL, - requestId, - selected.stableId(), - active.pieceKey()))); - } - if (active.owned() && menu.irisExtended()) { - tools.add(new ToolboxTool( - "Open Theme Memberships: " + active.displayName(), - JigsawStudioToolPayload.variant( - JigsawStudioToolAction.SET_THEME, - requestId, - selected.stableId(), - active.pieceKey()))); - tools.add(new ToolboxTool( - "Open Piece Rules: " + active.displayName(), - JigsawStudioToolPayload.variant( - JigsawStudioToolAction.SET_PIECE_RULES, - requestId, - selected.stableId(), - active.pieceKey()))); - } - if (active.owned()) { - for (JigsawStudioMenuState.Membership membership : active.memberships()) { - addMembershipTools( - tools, - requestId, - selected, - active, - membership, - menu.irisExtended()); - } - } - } - for (JigsawStudioMenuState.Variant variant : selected.variants()) { - if (variant.active()) { - continue; - } - tools.add(new ToolboxTool( - "Load " + variant.displayName(), - JigsawStudioToolPayload.variant( - JigsawStudioToolAction.LOAD_VARIANT, - requestId, - selected.stableId(), - variant.pieceKey()))); - if (variant.owned()) { - tools.add(new ToolboxTool( - "Rename " + variant.displayName(), - JigsawStudioToolPayload.variant( - JigsawStudioToolAction.RENAME_VARIANT, - requestId, - selected.stableId(), - variant.pieceKey()))); - tools.add(new ToolboxTool( - "Resize " + variant.displayName(), - JigsawStudioToolPayload.variant( - JigsawStudioToolAction.RESIZE_VARIANT, - requestId, - selected.stableId(), - variant.pieceKey()))); - } - if (variant.owned() && selected.variants().size() > 1) { - tools.add(new ToolboxTool( - "Delete " + variant.displayName(), - JigsawStudioToolPayload.variant( - JigsawStudioToolAction.DELETE_VARIANT, - requestId, - selected.stableId(), - variant.pieceKey()))); - } - } - if (menu.irisExtended()) { - tools.add(new ToolboxTool( - menu.requireCaps() ? "Disable Mandatory Caps" : "Enable Mandatory Caps", - JigsawStudioToolPayload.request(JigsawStudioToolAction.TOGGLE_REQUIRE_CAPS, requestId))); - } - tools.add(new ToolboxTool( - "Delete Project", - JigsawStudioToolPayload.request(JigsawStudioToolAction.DELETE_PROJECT, requestId))); - return List.copyOf(tools); - } - - private static void addMembershipTools( - List tools, - UUID requestId, - JigsawStudioMenuState.Workcell workcell, - JigsawStudioMenuState.Variant variant, - JigsawStudioMenuState.Membership membership, - boolean chanceEditable - ) { - String membershipName = displayKey(membership.poolKey()) + " [" + membership.entryIndex() + "]"; - tools.add(new ToolboxTool( - "Weight +1: " + membershipName, - JigsawStudioToolPayload.membership( - JigsawStudioToolAction.ADJUST_VARIANT_WEIGHT, - requestId, - workcell.stableId(), - variant.pieceKey(), - membership.poolKey(), - membership.entryIndex(), - 1))); - tools.add(new ToolboxTool( - "Weight -1: " + membershipName, - JigsawStudioToolPayload.membership( - JigsawStudioToolAction.ADJUST_VARIANT_WEIGHT, - requestId, - workcell.stableId(), - variant.pieceKey(), - membership.poolKey(), - membership.entryIndex(), - -1))); - if (chanceEditable) { - tools.add(new ToolboxTool( - "Chance +" + CHANCE_STEP_PERCENTAGE_POINTS + "%: " + membershipName, - JigsawStudioToolPayload.membership( - JigsawStudioToolAction.ADJUST_VARIANT_CHANCE, - requestId, - workcell.stableId(), - variant.pieceKey(), - membership.poolKey(), - membership.entryIndex(), - CHANCE_STEP_PERCENTAGE_POINTS))); - tools.add(new ToolboxTool( - "Chance -" + CHANCE_STEP_PERCENTAGE_POINTS + "%: " + membershipName, - JigsawStudioToolPayload.membership( - JigsawStudioToolAction.ADJUST_VARIANT_CHANCE, - requestId, - workcell.stableId(), - variant.pieceKey(), - membership.poolKey(), - membership.entryIndex(), - -CHANCE_STEP_PERCENTAGE_POINTS))); - } - tools.add(new ToolboxTool( - "Unlink " + membershipName, - JigsawStudioToolPayload.membership( - JigsawStudioToolAction.UNLINK_MEMBERSHIP, - requestId, - workcell.stableId(), - variant.pieceKey(), - membership.poolKey(), - membership.entryIndex(), - 0))); - } - - private static void addToolBindingLore(UIElement element, JigsawStudioToolPayload payload) { - if (!payload.workcellId().isEmpty()) { - element.addLore(ChatColor.GRAY + "Workcell: " + safe(payload.workcellId())); - } - if (!payload.pieceKey().isEmpty()) { - element.addLore(ChatColor.GRAY + "Variant: " + safe(payload.pieceKey())); - } - if (!payload.poolKey().isEmpty()) { - element.addLore(ChatColor.GRAY + "Pool: " + safe(payload.poolKey()) - + (payload.entryIndex() < 0 ? "" : " [" + payload.entryIndex() + "]")); - } - if (payload.amount() != 0) { - element.addLore(ChatColor.GRAY + "Amount: " + payload.amount()); - } - } - - static int pageCount(int itemCount, int pageSize) { - if (itemCount < 0 || pageSize < 1) { - throw new IllegalArgumentException("Jigsaw Studio menu page bounds are invalid"); - } - return Math.max(1, (itemCount + pageSize - 1) / pageSize); - } - - static int clampPage(int requestedPage, int itemCount, int pageSize) { - int maximum = pageCount(itemCount, pageSize) - 1; - return Math.max(0, Math.min(requestedPage, maximum)); - } - - static List page(List items, int requestedPage, int pageSize) { - List values = List.copyOf(Objects.requireNonNull(items, "Jigsaw Studio menu page items")); - int page = clampPage(requestedPage, values.size(), pageSize); - int start = page * pageSize; - int end = Math.min(values.size(), start + pageSize); - return values.subList(start, end); - } - - private static UIElement evaluationElement(JigsawStudioMenuState.Evaluation evaluation) { - UIElement element = element( - "evaluation", - evaluationMaterial(evaluation.state()), - evaluationColor(evaluation.state()) + evaluationName(evaluation.state())); - element.addLore(ChatColor.GRAY + "Evaluation updates automatically."); - element.addLore(ChatColor.GRAY + "Seed: " + evaluation.seed()); - if (evaluation.generation() > 0L) { - element.addLore(ChatColor.GRAY + "Generation: " + evaluation.generation()); - } - if (!evaluation.selectedTheme().isEmpty()) { - element.addLore(ChatColor.GRAY + "Theme: " + safe(evaluation.selectedTheme())); - } - element.addLore(ChatColor.GRAY + "Pieces: " + evaluation.pieceCount()); - if (!evaluation.detail().isEmpty()) { - element.addLore(ChatColor.GRAY + safe(evaluation.detail())); - } - return element; - } - - static Material evaluationMaterial(JigsawStudioEvaluationState state) { - return switch (state) { - case PENDING -> Material.CLOCK; - case VALID -> Material.EMERALD; - case WARNING -> Material.YELLOW_DYE; - case INVALID -> Material.RED_DYE; - case STALE -> Material.GRAY_DYE; - }; - } - - private static ChatColor evaluationColor(JigsawStudioEvaluationState state) { - return switch (state) { - case PENDING -> ChatColor.AQUA; - case VALID -> ChatColor.GREEN; - case WARNING -> ChatColor.YELLOW; - case INVALID -> ChatColor.RED; - case STALE -> ChatColor.GRAY; - }; - } - - private static String evaluationName(JigsawStudioEvaluationState state) { - return switch (state) { - case PENDING -> "Evaluation Pending"; - case VALID -> "Ready to Assemble"; - case WARNING -> "Ready with Warnings"; - case INVALID -> "Assembly Blocked"; - case STALE -> "Evaluation Stale"; - }; - } - - private static Material workcellMaterial(JigsawStudioMenuState.Workcell workcell, boolean selected) { - if (!workcell.enabled()) { - return Material.GRAY_WOOL; - } - if (workcell.loading() || workcell.saving()) { - return Material.YELLOW_WOOL; - } - if (workcell.dirty()) { - return Material.ORANGE_WOOL; - } - return selected ? Material.LIME_WOOL : Material.LIGHT_GRAY_WOOL; - } - - static Material variantMaterial(JigsawStudioMenuState.Variant variant) { - if (variant.active()) { - return Material.JIGSAW; - } - return variant.owned() ? Material.PAPER : Material.GRAY_DYE; - } - - static String workcellStatus(JigsawStudioMenuState.Workcell workcell) { - if (workcell.loading()) { - return ChatColor.YELLOW + "Loading variant"; - } - if (workcell.saving()) { - return ChatColor.YELLOW + "Autosave in progress"; - } - if (workcell.dirty()) { - return ChatColor.GOLD + "Autosave pending"; - } - return ChatColor.GREEN + "Autosaved"; - } - - private static String dimensions(JigsawStudioCellDimensions dimensions) { - return dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth(); - } - - private static String themes(List themes) { - return themes.isEmpty() ? "None" : safe(String.join(", ", themes)); - } - - private static String maximumPlacements(int maximum) { - return maximum == 0 ? "Unlimited" : Integer.toString(maximum); - } - - private static String chance(double chance) { - return String.format(Locale.ROOT, "%.1f%%", chance * 100D); - } - - private static String yesNo(boolean value) { - return value ? "Yes" : "No"; - } - - private static String compatibilityName(JigsawStudioCompatibilityTarget target) { - return switch (target) { - case IRIS_EXTENDED -> "Iris Extended"; - case VANILLA_PORTABLE -> "Vanilla Portable"; - }; - } - - private static UIElement element(String id, Material material, String name) { - return new UIElement(id) - .setMaterial(new MaterialBlock(material)) - .setName(name); - } - - private static String title(String structureKey) { - String value = "Iris Jigsaw: " + safe(displayKey(structureKey)); - return value.length() > 32 ? value.substring(0, 32) : value; - } - - private static String displayKey(String resourceKey) { - int separator = resourceKey.lastIndexOf('/'); - return separator < 0 ? resourceKey : resourceKey.substring(separator + 1); - } - - private static String safe(String value) { - return BoardSVC.untrustedBoardValue(value); - } - - public interface Actions { - Optional menuState(Player player); - - boolean selectWorkcell(Player player, String workcellId); - - boolean teleportToWorkcell(Player player, String workcellId); - - boolean setConnectorBlocksVisible(Player player, String workcellId, boolean visible); - - boolean resetConnectorBlocks(Player player, String workcellId); - - boolean undoAutosave(Player player); - - boolean switchVariant(Player player, String workcellId, String pieceKey, boolean discardDirty); - - boolean createVariant(Player player, String workcellId, boolean duplicateActive); - - boolean setWorkcellEnabled(Player player, String workcellId, boolean enabled); - - boolean updateWorkcellDimensions( - Player player, - String workcellId, - JigsawStudioCellDimensions dimensions - ); - - boolean updateWorkcellDisplayName(Player player, String workcellId, String displayName); - - boolean setRequireCaps(Player player, boolean requireCaps); - - boolean duplicateActiveFamily(Player player, String themeKey); - - boolean updateThemeSetWeight(Player player, String themeKey, int weight); - - boolean flushAutosave(Player player, String workcellId); - - boolean goToPreview(Player player); - - boolean toggleVariantRotatable(Player player, String workcellId); - - boolean expandVariantToCell(Player player, String workcellId); - - boolean resizeVariant( - Player player, - String workcellId, - String pieceKey, - JigsawStudioCellDimensions dimensions - ); - - boolean updateVariantDisplayName( - Player player, - String workcellId, - String pieceKey, - String displayName - ); - - boolean updateVariantThemes( - Player player, - String workcellId, - String pieceKey, - List themes - ); - - boolean updateVariantRules( - Player player, - String workcellId, - String pieceKey, - JigsawStudioPieceRules rules - ); - - boolean adjustVariantWeight( - Player player, - String workcellId, - String pieceKey, - String poolKey, - int entryIndex, - int delta - ); - - boolean adjustVariantChance( - Player player, - String workcellId, - String pieceKey, - String poolKey, - int entryIndex, - int deltaPercentagePoints - ); - - boolean unlinkVariantMembership( - Player player, - String workcellId, - String pieceKey, - String poolKey, - int entryIndex - ); - - boolean deleteVariant(Player player, String workcellId, String pieceKey); - - boolean deleteProject(Player player); - - boolean giveTool(Player player, JigsawStudioToolPayload payload); - } - - enum RuleField { - MINIMUM_DEPTH("Minimum Depth", Material.LIGHT_BLUE_DYE, RULE_SHIFT_STEP) { - @Override - int value(JigsawStudioPieceRules rules) { - return rules.minimumDepth(); - } - }, - MAXIMUM_DEPTH("Maximum Depth", Material.BLUE_DYE, RULE_SHIFT_STEP) { - @Override - int value(JigsawStudioPieceRules rules) { - return rules.maximumDepth(); - } - }, - MINIMUM_PLACEMENTS("Minimum Placements", Material.TARGET, PLACEMENT_RULE_SHIFT_STEP) { - @Override - int value(JigsawStudioPieceRules rules) { - return rules.minimumPlacements(); - } - }, - MAXIMUM_PLACEMENTS("Maximum Placements", Material.GREEN_DYE, PLACEMENT_RULE_SHIFT_STEP) { - @Override - int value(JigsawStudioPieceRules rules) { - return rules.maximumPlacements(); - } - - @Override - String displayValue(int value) { - return maximumPlacements(value); - } - }; - - private final String displayName; - private final Material material; - private final int shiftStep; - - RuleField(String displayName, Material material, int shiftStep) { - this.displayName = displayName; - this.material = material; - this.shiftStep = shiftStep; - } - - String displayName() { - return displayName; - } - - Material material() { - return material; - } - - int shiftStep() { - return shiftStep; - } - - String displayValue(int value) { - return Integer.toString(value); - } - - abstract int value(JigsawStudioPieceRules rules); - } - - enum DimensionAxis { - WIDTH("Width") { - @Override - int value(JigsawStudioCellDimensions dimensions) { - return dimensions.width(); - } - }, - HEIGHT("Height") { - @Override - int value(JigsawStudioCellDimensions dimensions) { - return dimensions.height(); - } - }, - DEPTH("Depth") { - @Override - int value(JigsawStudioCellDimensions dimensions) { - return dimensions.depth(); - } - }; - - private final String displayName; - - DimensionAxis(String displayName) { - this.displayName = displayName; - } - - String displayName() { - return displayName; - } - - abstract int value(JigsawStudioCellDimensions dimensions); - } - - record ToolboxTool(String displayName, JigsawStudioToolPayload payload) { - ToolboxTool { - displayName = Objects.requireNonNull(displayName, "Jigsaw Studio toolbox display name").trim(); - if (displayName.isEmpty()) { - throw new IllegalArgumentException("Jigsaw Studio toolbox display name cannot be blank"); - } - payload = Objects.requireNonNull(payload, "Jigsaw Studio toolbox payload"); - } - } - private record PendingUnlink( UUID requestId, String workcellId, @@ -3383,16 +1139,4 @@ public final class JigsawStudioMenuController { private record PendingProjectDelete(UUID requestId, long expiresAtNanos) { } - private record PendingWorkcellResize( - UUID requestId, - String workcellId, - JigsawStudioCellDimensions dimensions, - boolean applying - ) { - private PendingWorkcellResize { - requestId = Objects.requireNonNull(requestId, "Jigsaw Studio resize request ID"); - workcellId = Objects.requireNonNull(workcellId, "Jigsaw Studio resize workcell ID"); - dimensions = Objects.requireNonNull(dimensions, "Jigsaw Studio resize dimensions"); - } - } } diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuFormat.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuFormat.java new file mode 100644 index 000000000..e44130646 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioMenuFormat.java @@ -0,0 +1,477 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPieceRules; +import art.arcane.volmlib.util.data.MaterialBlock; +import art.arcane.volmlib.util.inventorygui.UIElement; +import org.bukkit.ChatColor; +import org.bukkit.Material; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; + +import static art.arcane.iris.core.service.JigsawStudioMenuController.PLACEMENT_RULE_SHIFT_STEP; +import static art.arcane.iris.core.service.JigsawStudioMenuController.RULE_SHIFT_STEP; + +final class JigsawStudioMenuFormat { + private JigsawStudioMenuFormat() { + } + + static boolean hasMembership( + JigsawStudioMenuState state, + String workcellId, + JigsawStudioMenuState.Membership expected + ) { + JigsawStudioMenuState.Variant active = activeVariant(state, workcellId); + if (active == null) { + return false; + } + for (JigsawStudioMenuState.Membership membership : active.memberships()) { + if (membership.poolKey().equals(expected.poolKey()) + && membership.entryIndex() == expected.entryIndex()) { + return true; + } + } + return false; + } + + static JigsawStudioMenuState.Variant activeVariant( + JigsawStudioMenuState state, + String workcellId + ) { + JigsawStudioMenuState.Workcell workcell = state.workcell(workcellId); + return workcell == null ? null : workcell.activeVariant(); + } + + static JigsawStudioMenuState.Variant variant( + JigsawStudioMenuState.Workcell workcell, + String pieceKey + ) { + if (workcell == null || pieceKey == null) { + return null; + } + for (JigsawStudioMenuState.Variant variant : workcell.variants()) { + if (variant.pieceKey().equals(pieceKey)) { + return variant; + } + } + return null; + } + + static String nextThemeSetKey(List themeSets) { + List values = List.copyOf(Objects.requireNonNull( + themeSets, + "Jigsaw Studio theme sets")); + int candidate = 1; + while (candidate > 0) { + String key = "variant-" + candidate; + boolean present = false; + for (JigsawStudioMenuState.ThemeSet themeSet : values) { + if (Objects.requireNonNull(themeSet, "Jigsaw Studio theme set").key().equals(key)) { + present = true; + break; + } + } + if (!present) { + return key; + } + candidate = Math.incrementExact(candidate); + } + throw new IllegalStateException("Jigsaw Studio cannot allocate another numbered theme set"); + } + + static String themeSelectionPercent( + List themeSets, + JigsawStudioMenuState.ThemeSet target + ) { + List activeThemeSets = Objects.requireNonNull( + themeSets, + "Jigsaw Studio theme sets"); + JigsawStudioMenuState.ThemeSet activeTarget = Objects.requireNonNull( + target, + "Jigsaw Studio target theme set"); + int totalWeight = 0; + for (JigsawStudioMenuState.ThemeSet themeSet : activeThemeSets) { + totalWeight = Math.addExact(totalWeight, Objects.requireNonNull( + themeSet, + "Jigsaw Studio theme set").weight()); + } + if (totalWeight < 1) { + return "0.0%"; + } + return String.format( + Locale.ROOT, + "%.1f%%", + activeTarget.weight() * 100.0D / totalWeight); + } + + static Optional adjustedPositiveValue(int value, int delta) { + if (value < 1 || delta == 0) { + throw new IllegalArgumentException("Jigsaw Studio positive value adjustment is invalid"); + } + try { + int adjusted = Math.addExact(value, delta); + return adjusted < 1 ? Optional.empty() : Optional.of(adjusted); + } catch (ArithmeticException exception) { + return Optional.empty(); + } + } + + static Optional adjustedRules( + JigsawStudioPieceRules rules, + RuleField field, + int delta + ) { + JigsawStudioPieceRules current = Objects.requireNonNull(rules, "Jigsaw Studio piece rules"); + RuleField target = Objects.requireNonNull(field, "Jigsaw Studio piece rule field"); + if (delta == 0) { + throw new IllegalArgumentException("Jigsaw Studio piece rule delta cannot be zero"); + } + try { + int minimumDepth = current.minimumDepth(); + int maximumDepth = current.maximumDepth(); + int minimumPlacements = current.minimumPlacements(); + int maximumPlacements = current.maximumPlacements(); + switch (target) { + case MINIMUM_DEPTH -> minimumDepth = Math.addExact(minimumDepth, delta); + case MAXIMUM_DEPTH -> maximumDepth = Math.addExact(maximumDepth, delta); + case MINIMUM_PLACEMENTS -> minimumPlacements = Math.addExact(minimumPlacements, delta); + case MAXIMUM_PLACEMENTS -> { + Optional adjustedMaximum = adjustedMaximumPlacements(maximumPlacements, delta); + if (adjustedMaximum.isEmpty()) { + return Optional.empty(); + } + maximumPlacements = adjustedMaximum.get(); + } + } + return Optional.of(new JigsawStudioPieceRules( + minimumDepth, + maximumDepth, + minimumPlacements, + maximumPlacements, + current.terminal())); + } catch (ArithmeticException | IllegalArgumentException exception) { + return Optional.empty(); + } + } + + static JigsawStudioPieceRules withTerminal(JigsawStudioPieceRules rules, boolean terminal) { + JigsawStudioPieceRules current = Objects.requireNonNull(rules, "Jigsaw Studio piece rules"); + return new JigsawStudioPieceRules( + current.minimumDepth(), + current.maximumDepth(), + current.minimumPlacements(), + current.maximumPlacements(), + terminal); + } + + private static Optional adjustedMaximumPlacements(int value, int delta) { + if (value == 0) { + if (delta > 0) { + return Optional.empty(); + } + int adjusted = Math.addExact(513, delta); + return adjusted < 1 ? Optional.empty() : Optional.of(adjusted); + } + int adjusted = Math.addExact(value, delta); + if (adjusted > 512) { + return Optional.of(0); + } + return adjusted < 1 ? Optional.empty() : Optional.of(adjusted); + } + + static Optional adjustedDimensions( + JigsawStudioCellDimensions dimensions, + DimensionAxis axis, + int delta + ) { + JigsawStudioCellDimensions current = Objects.requireNonNull( + dimensions, + "Jigsaw Studio workcell dimensions"); + DimensionAxis target = Objects.requireNonNull(axis, "Jigsaw Studio workcell dimension axis"); + if (delta == 0) { + throw new IllegalArgumentException("Jigsaw Studio workcell dimension delta cannot be zero"); + } + try { + int width = target == DimensionAxis.WIDTH + ? Math.addExact(current.width(), delta) + : current.width(); + int height = target == DimensionAxis.HEIGHT + ? Math.addExact(current.height(), delta) + : current.height(); + int depth = target == DimensionAxis.DEPTH + ? Math.addExact(current.depth(), delta) + : current.depth(); + return Optional.of(new JigsawStudioCellDimensions(width, height, depth)); + } catch (ArithmeticException | IllegalArgumentException exception) { + return Optional.empty(); + } + } + + static JigsawStudioMenuState.Workcell withCapacity( + JigsawStudioMenuState.Workcell workcell, + JigsawStudioCellDimensions capacity + ) { + JigsawStudioMenuState.Workcell source = Objects.requireNonNull( + workcell, + "Jigsaw Studio menu workcell"); + return new JigsawStudioMenuState.Workcell( + source.stableId(), + source.canonicalName(), + source.displayName(), + Objects.requireNonNull(capacity, "Jigsaw Studio staged workcell capacity"), + source.enabled(), + source.activeVariantKey(), + source.dirty(), + source.saving(), + source.loading(), + source.connectorsVisible(), + source.variants()); + } + + static int pageCount(int itemCount, int pageSize) { + if (itemCount < 0 || pageSize < 1) { + throw new IllegalArgumentException("Jigsaw Studio menu page bounds are invalid"); + } + return Math.max(1, (itemCount + pageSize - 1) / pageSize); + } + + static int clampPage(int requestedPage, int itemCount, int pageSize) { + int maximum = pageCount(itemCount, pageSize) - 1; + return Math.max(0, Math.min(requestedPage, maximum)); + } + + static List page(List items, int requestedPage, int pageSize) { + List values = List.copyOf(Objects.requireNonNull(items, "Jigsaw Studio menu page items")); + int page = clampPage(requestedPage, values.size(), pageSize); + int start = page * pageSize; + int end = Math.min(values.size(), start + pageSize); + return values.subList(start, end); + } + + static UIElement evaluationElement(JigsawStudioMenuState.Evaluation evaluation) { + UIElement element = element( + "evaluation", + evaluationMaterial(evaluation.state()), + evaluationColor(evaluation.state()) + evaluationName(evaluation.state())); + element.addLore(ChatColor.GRAY + "Evaluation updates automatically."); + element.addLore(ChatColor.GRAY + "Seed: " + evaluation.seed()); + if (evaluation.generation() > 0L) { + element.addLore(ChatColor.GRAY + "Generation: " + evaluation.generation()); + } + if (!evaluation.selectedTheme().isEmpty()) { + element.addLore(ChatColor.GRAY + "Theme: " + safe(evaluation.selectedTheme())); + } + element.addLore(ChatColor.GRAY + "Pieces: " + evaluation.pieceCount()); + if (!evaluation.detail().isEmpty()) { + element.addLore(ChatColor.GRAY + safe(evaluation.detail())); + } + return element; + } + + static Material evaluationMaterial(JigsawStudioEvaluationState state) { + return switch (state) { + case PENDING -> Material.CLOCK; + case VALID -> Material.EMERALD; + case WARNING -> Material.YELLOW_DYE; + case INVALID -> Material.RED_DYE; + case STALE -> Material.GRAY_DYE; + }; + } + + private static ChatColor evaluationColor(JigsawStudioEvaluationState state) { + return switch (state) { + case PENDING -> ChatColor.AQUA; + case VALID -> ChatColor.GREEN; + case WARNING -> ChatColor.YELLOW; + case INVALID -> ChatColor.RED; + case STALE -> ChatColor.GRAY; + }; + } + + private static String evaluationName(JigsawStudioEvaluationState state) { + return switch (state) { + case PENDING -> "Evaluation Pending"; + case VALID -> "Ready to Assemble"; + case WARNING -> "Ready with Warnings"; + case INVALID -> "Assembly Blocked"; + case STALE -> "Evaluation Stale"; + }; + } + + static Material workcellMaterial(JigsawStudioMenuState.Workcell workcell, boolean selected) { + if (!workcell.enabled()) { + return Material.GRAY_WOOL; + } + if (workcell.loading() || workcell.saving()) { + return Material.YELLOW_WOOL; + } + if (workcell.dirty()) { + return Material.ORANGE_WOOL; + } + return selected ? Material.LIME_WOOL : Material.LIGHT_GRAY_WOOL; + } + + static Material variantMaterial(JigsawStudioMenuState.Variant variant) { + if (variant.active()) { + return Material.JIGSAW; + } + return variant.owned() ? Material.PAPER : Material.GRAY_DYE; + } + + static String workcellStatus(JigsawStudioMenuState.Workcell workcell) { + if (workcell.loading()) { + return ChatColor.YELLOW + "Loading variant"; + } + if (workcell.saving()) { + return ChatColor.YELLOW + "Autosave in progress"; + } + if (workcell.dirty()) { + return ChatColor.GOLD + "Autosave pending"; + } + return ChatColor.GREEN + "Autosaved"; + } + + static String dimensions(JigsawStudioCellDimensions dimensions) { + return dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth(); + } + + static String themes(List themes) { + return themes.isEmpty() ? "None" : safe(String.join(", ", themes)); + } + + static String maximumPlacements(int maximum) { + return maximum == 0 ? "Unlimited" : Integer.toString(maximum); + } + + static String chance(double chance) { + return String.format(Locale.ROOT, "%.1f%%", chance * 100D); + } + + static String yesNo(boolean value) { + return value ? "Yes" : "No"; + } + + static String compatibilityName(JigsawStudioCompatibilityTarget target) { + return switch (target) { + case IRIS_EXTENDED -> "Iris Extended"; + case VANILLA_PORTABLE -> "Vanilla Portable"; + }; + } + + static UIElement element(String id, Material material, String name) { + return new UIElement(id) + .setMaterial(new MaterialBlock(material)) + .setName(name); + } + + static String title(String structureKey) { + String value = "Iris Jigsaw: " + safe(displayKey(structureKey)); + return value.length() > 32 ? value.substring(0, 32) : value; + } + + static String displayKey(String resourceKey) { + int separator = resourceKey.lastIndexOf('/'); + return separator < 0 ? resourceKey : resourceKey.substring(separator + 1); + } + + static String safe(String value) { + return BoardSVC.untrustedBoardValue(value); + } + + enum RuleField { + MINIMUM_DEPTH("Minimum Depth", Material.LIGHT_BLUE_DYE, RULE_SHIFT_STEP) { + @Override + int value(JigsawStudioPieceRules rules) { + return rules.minimumDepth(); + } + }, + MAXIMUM_DEPTH("Maximum Depth", Material.BLUE_DYE, RULE_SHIFT_STEP) { + @Override + int value(JigsawStudioPieceRules rules) { + return rules.maximumDepth(); + } + }, + MINIMUM_PLACEMENTS("Minimum Placements", Material.TARGET, PLACEMENT_RULE_SHIFT_STEP) { + @Override + int value(JigsawStudioPieceRules rules) { + return rules.minimumPlacements(); + } + }, + MAXIMUM_PLACEMENTS("Maximum Placements", Material.GREEN_DYE, PLACEMENT_RULE_SHIFT_STEP) { + @Override + int value(JigsawStudioPieceRules rules) { + return rules.maximumPlacements(); + } + + @Override + String displayValue(int value) { + return maximumPlacements(value); + } + }; + + private final String displayName; + private final Material material; + private final int shiftStep; + + RuleField(String displayName, Material material, int shiftStep) { + this.displayName = displayName; + this.material = material; + this.shiftStep = shiftStep; + } + + String displayName() { + return displayName; + } + + Material material() { + return material; + } + + int shiftStep() { + return shiftStep; + } + + String displayValue(int value) { + return Integer.toString(value); + } + + abstract int value(JigsawStudioPieceRules rules); + } + + enum DimensionAxis { + WIDTH("Width") { + @Override + int value(JigsawStudioCellDimensions dimensions) { + return dimensions.width(); + } + }, + HEIGHT("Height") { + @Override + int value(JigsawStudioCellDimensions dimensions) { + return dimensions.height(); + } + }, + DEPTH("Depth") { + @Override + int value(JigsawStudioCellDimensions dimensions) { + return dimensions.depth(); + } + }; + + private final String displayName; + + DimensionAxis(String displayName) { + this.displayName = displayName; + } + + String displayName() { + return displayName; + } + + abstract int value(JigsawStudioCellDimensions dimensions); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioService.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioService.java index f40fe1426..765459e03 100644 --- a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioService.java +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioService.java @@ -156,7 +156,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -public final class JigsawStudioService implements IrisService, JigsawStudioMenuController.Actions { +public final class JigsawStudioService implements IrisService, JigsawStudioMenuActions { private static final int AUTOSAVE_DEBOUNCE_TICKS = 40; private static final int AUTOSAVE_RETRY_TICKS = 5; private static final int REPLACEMENT_CLOSE_WAIT_TICKS = 2_400; @@ -3185,14 +3185,14 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC } return duplicateActiveFamily( player, - JigsawStudioMenuController.nextThemeSetKey(state.get().themeSets())); + JigsawStudioMenuFormat.nextThemeSetKey(state.get().themeSets())); } private boolean duplicateFamilyFromTool(Player player) { Optional state = menuState(player); return state.isPresent() && duplicateActiveFamily( player, - JigsawStudioMenuController.nextThemeSetKey(state.get().themeSets())); + JigsawStudioMenuFormat.nextThemeSetKey(state.get().themeSets())); } private boolean toggleRequireCapsFromTool(Player player) { diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioStructureMenu.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioStructureMenu.java new file mode 100644 index 000000000..f0a5db8d8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioStructureMenu.java @@ -0,0 +1,194 @@ +package art.arcane.iris.core.service; + +import art.arcane.volmlib.util.inventorygui.UIElement; +import art.arcane.volmlib.util.inventorygui.UIWindow; +import org.bukkit.ChatColor; +import org.bukkit.Material; + +import java.util.List; +import java.util.Objects; + +import static art.arcane.iris.core.service.JigsawStudioMenuController.GRID_POSITIONS; +import static art.arcane.iris.core.service.JigsawStudioMenuController.THEME_SETS_PER_PAGE; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.clampPage; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.compatibilityName; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.element; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.evaluationElement; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.nextThemeSetKey; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.page; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.pageCount; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.safe; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.themeSelectionPercent; + +final class JigsawStudioStructureMenu { + private final JigsawStudioMenuController controller; + + JigsawStudioStructureMenu(JigsawStudioMenuController controller) { + this.controller = Objects.requireNonNull(controller, "Jigsaw Studio menu controller"); + } + + void render( + UIWindow window, + JigsawStudioMenuState state, + String selectedWorkcellId, + int requestedPage + ) { + int page = clampPage(requestedPage, state.themeSets().size(), THEME_SETS_PER_PAGE); + controller.purgeExpiredConfirmations(window.getViewer().getUniqueId()); + window.batch(() -> { + window.clearElements(); + + UIElement back = element("structure-back", Material.ARROW, ChatColor.YELLOW + "Back to Variants"); + back.onLeftClick(clicked -> controller.refreshMain( + window.getViewer(), state.requestId(), selectedWorkcellId, 0)); + window.setElement(-4, 0, back); + + UIElement identity = element( + "structure-identity", + Material.JIGSAW, + ChatColor.LIGHT_PURPLE + "Structure Themes & Caps"); + identity.addLore(ChatColor.DARK_GRAY + safe(state.structureKey())); + identity.addLore(ChatColor.GRAY + "Compatibility: " + compatibilityName(state.compatibilityTarget())); + identity.addLore(ChatColor.GRAY + "Theme sets select one coherent variant family per assembly."); + identity.addLore(ChatColor.GRAY + "Empty piece themes remain available to every family."); + window.setElement(0, 0, identity); + window.setElement(4, 0, evaluationElement(state.evaluation())); + + UIElement requireCaps = element( + "require-caps", + state.irisExtended() + ? state.requireCaps() ? Material.IRON_BARS : Material.TRIPWIRE_HOOK + : Material.BARRIER, + !state.irisExtended() + ? ChatColor.GRAY + "Mandatory Caps Require Iris Compatibility" + : state.requireCaps() + ? ChatColor.GREEN + "Mandatory Caps Enabled" + : ChatColor.YELLOW + "Mandatory Caps Disabled"); + requireCaps.addLore(!state.irisExtended() + ? ChatColor.GRAY + "Vanilla-portable export rejects Iris mandatory-cap metadata." + : state.requireCaps() + ? ChatColor.GRAY + "Every open connector must close with a terminal piece." + : ChatColor.GRAY + "Assembly may leave an open connector when no cap can fit."); + if (state.irisExtended()) { + requireCaps.addLore(ChatColor.YELLOW + "Left-click to toggle"); + requireCaps.onLeftClick(clicked -> controller.setRequireCaps( + window.getViewer(), state.requestId(), !state.requireCaps())); + } + window.setElement(-3, 1, requireCaps); + + String nextThemeKey = nextThemeSetKey(state.themeSets()); + boolean themeEditingAvailable = state.irisExtended(); + UIElement createTheme = element( + "new-theme-set", + themeEditingAvailable ? Material.NETHER_STAR : Material.BARRIER, + themeEditingAvailable + ? ChatColor.GREEN + "Duplicate All Enabled Cells as Family: " + safe(nextThemeKey) + : ChatColor.GRAY + "Theme Sets Require Iris Compatibility"); + if (themeEditingAvailable) { + createTheme.addLore(ChatColor.GRAY + "Creates one new owned variant for every enabled workcell."); + createTheme.addLore(ChatColor.GRAY + "All created variants join " + safe(nextThemeKey) + "."); + createTheme.addLore(ChatColor.GRAY + "One family is selected for the complete assembly."); + createTheme.addLore(ChatColor.YELLOW + "Left-click to create the complete set"); + createTheme.onLeftClick(clicked -> controller.duplicateActiveFamily( + window.getViewer(), state.requestId(), nextThemeKey)); + } else { + createTheme.addLore(ChatColor.GRAY + "Vanilla jigsaw resources cannot encode Iris theme metadata."); + } + window.setElement(0, 1, createTheme); + + boolean deleteConfirmed = controller.isProjectDeleteConfirmed( + window.getViewer().getUniqueId(), state.requestId()); + UIElement deleteProject = element( + "delete-project", + deleteConfirmed ? Material.REDSTONE : Material.LAVA_BUCKET, + deleteConfirmed + ? ChatColor.RED + "Confirm Delete Jigsaw" + : ChatColor.RED + "Delete Jigsaw") + .setEnchanted(deleteConfirmed); + deleteProject.addLore(deleteConfirmed + ? ChatColor.RED + "Click again to delete this entire managed project." + : ChatColor.RED + "Click twice within 10 seconds to confirm."); + deleteProject.onLeftClick(clicked -> controller.confirmOrDeleteProject( + window.getViewer(), state.requestId(), selectedWorkcellId, page)); + window.setElement(3, 1, deleteProject); + + addThemeSets(window, state, page); + + UIElement footerBack = element("structure-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); + footerBack.onLeftClick(clicked -> controller.refreshMain( + window.getViewer(), state.requestId(), selectedWorkcellId, 0)); + window.setElement(-4, 5, footerBack); + + int pageCount = pageCount(state.themeSets().size(), THEME_SETS_PER_PAGE); + if (page > 0) { + UIElement previous = element( + "theme-previous", + Material.ARROW, + ChatColor.YELLOW + "Previous Page"); + previous.onLeftClick(clicked -> controller.refreshStructureSettings( + window.getViewer(), state.requestId(), selectedWorkcellId, page - 1)); + window.setElement(-1, 5, previous); + } + UIElement indicator = element( + "theme-page", + Material.BOOK, + ChatColor.WHITE + "Page " + (page + 1) + "/" + pageCount); + indicator.addLore(ChatColor.GRAY + "" + state.themeSets().size() + " theme sets"); + window.setElement(0, 5, indicator); + if (page + 1 < pageCount) { + UIElement next = element("theme-next", Material.ARROW, ChatColor.YELLOW + "Next Page"); + next.onLeftClick(clicked -> controller.refreshStructureSettings( + window.getViewer(), state.requestId(), selectedWorkcellId, page + 1)); + window.setElement(1, 5, next); + } + window.setElement(4, 5, evaluationElement(state.evaluation())); + }); + } + + private void addThemeSets( + UIWindow window, + JigsawStudioMenuState state, + int page + ) { + List themeSets = page( + state.themeSets(), page, THEME_SETS_PER_PAGE); + if (themeSets.isEmpty()) { + UIElement empty = element( + "no-theme-sets", + Material.GRAY_DYE, + ChatColor.GRAY + "Implicit Unthemed Assembly"); + empty.addLore(ChatColor.GRAY + "Create variant-1 to begin coherent theme selection."); + window.setElement(0, 3, empty); + return; + } + for (int index = 0; index < themeSets.size(); index++) { + JigsawStudioMenuState.ThemeSet themeSet = themeSets.get(index); + int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; + int row = 2 + index / GRID_POSITIONS.length; + UIElement element = element( + "theme-set-" + index, + state.irisExtended() ? Material.PURPLE_DYE : Material.GRAY_DYE, + (state.irisExtended() ? ChatColor.LIGHT_PURPLE : ChatColor.GRAY) + safe(themeSet.key())); + element.addLore(ChatColor.WHITE + "Selection weight: " + themeSet.weight()); + element.addLore(ChatColor.WHITE + "Whole-assembly chance: " + + themeSelectionPercent(state.themeSets(), themeSet)); + if (state.irisExtended()) { + element.addLore(ChatColor.GREEN + "Left-click: weight +1"); + element.addLore(ChatColor.YELLOW + "Right-click: weight -1"); + element.addLore(ChatColor.GREEN + "Shift-left: weight +8"); + element.addLore(ChatColor.YELLOW + "Shift-right: weight -8"); + element.onLeftClick(clicked -> controller.adjustThemeSetWeight( + window.getViewer(), state.requestId(), themeSet, 1)); + element.onRightClick(clicked -> controller.adjustThemeSetWeight( + window.getViewer(), state.requestId(), themeSet, -1)); + element.onShiftLeftClick(clicked -> controller.adjustThemeSetWeight( + window.getViewer(), state.requestId(), themeSet, 8)); + element.onShiftRightClick(clicked -> controller.adjustThemeSetWeight( + window.getViewer(), state.requestId(), themeSet, -8)); + } else { + element.addLore(ChatColor.GRAY + "Theme metadata is unavailable for vanilla-portable graphs."); + } + window.setElement(position, row, element); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioToolboxMenu.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioToolboxMenu.java new file mode 100644 index 000000000..0f4b3d998 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioToolboxMenu.java @@ -0,0 +1,370 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolAction; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolPayload; +import art.arcane.volmlib.util.inventorygui.UIElement; +import art.arcane.volmlib.util.inventorygui.UIWindow; +import org.bukkit.ChatColor; +import org.bukkit.Material; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import static art.arcane.iris.core.service.JigsawStudioMenuController.CHANCE_STEP_PERCENTAGE_POINTS; +import static art.arcane.iris.core.service.JigsawStudioMenuController.GRID_POSITIONS; +import static art.arcane.iris.core.service.JigsawStudioMenuController.TOOLS_PER_PAGE; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.clampPage; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.displayKey; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.element; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.evaluationElement; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.nextThemeSetKey; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.page; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.pageCount; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.safe; + +final class JigsawStudioToolboxMenu { + private final JigsawStudioMenuController controller; + + JigsawStudioToolboxMenu(JigsawStudioMenuController controller) { + this.controller = Objects.requireNonNull(controller, "Jigsaw Studio menu controller"); + } + + void render( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + int requestedPage + ) { + List allTools = toolboxTools(state, workcell); + int page = clampPage(requestedPage, allTools.size(), TOOLS_PER_PAGE); + List tools = page(allTools, page, TOOLS_PER_PAGE); + window.batch(() -> { + window.clearElements(); + + UIElement back = element("toolbox-back", Material.ARROW, ChatColor.YELLOW + "Back to Variants"); + back.onLeftClick(clicked -> controller.refreshMain( + window.getViewer(), state.requestId(), workcell.stableId(), 0)); + window.setElement(-4, 0, back); + + UIElement heading = element("toolbox-heading", Material.CHEST, ChatColor.AQUA + "Bound Tool Sticks"); + heading.addLore(ChatColor.GRAY + "Every stick is bound to request " + state.requestId()); + heading.addLore(ChatColor.GRAY + "Stale sticks stop working when this Studio closes."); + window.setElement(0, 0, heading); + window.setElement(4, 0, evaluationElement(state.evaluation())); + + for (int index = 0; index < tools.size(); index++) { + ToolboxTool tool = tools.get(index); + int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; + int row = 1 + index / GRID_POSITIONS.length; + UIElement element = element( + "tool-" + index, + Material.STICK, + ChatColor.AQUA + safe(tool.displayName())); + element.addLore(ChatColor.GRAY + "Action: " + tool.payload().action().displayName()); + addToolBindingLore(element, tool.payload()); + element.addLore(tool.payload().action().destructive() + ? ChatColor.RED + "Bound tools require confirmation when used." + : ChatColor.YELLOW + "Left-click to receive this named stick."); + element.onLeftClick(clicked -> controller.giveTool( + window.getViewer(), state.requestId(), tool.payload())); + window.setElement(position, row, element); + } + + UIElement footerBack = element("toolbox-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); + footerBack.onLeftClick(clicked -> controller.refreshMain( + window.getViewer(), state.requestId(), workcell.stableId(), 0)); + window.setElement(-4, 5, footerBack); + + int pageCount = pageCount(allTools.size(), TOOLS_PER_PAGE); + if (page > 0) { + UIElement previous = element("toolbox-previous", Material.ARROW, ChatColor.YELLOW + "Previous Page"); + previous.onLeftClick(clicked -> controller.refreshToolbox( + window.getViewer(), state.requestId(), workcell.stableId(), page - 1)); + window.setElement(-1, 5, previous); + } + UIElement indicator = element("toolbox-page", Material.BOOK, ChatColor.WHITE + "Page " + + (page + 1) + "/" + pageCount); + indicator.addLore(ChatColor.GRAY + "" + allTools.size() + " bound tools"); + window.setElement(0, 5, indicator); + if (page + 1 < pageCount) { + UIElement next = element("toolbox-next", Material.ARROW, ChatColor.YELLOW + "Next Page"); + next.onLeftClick(clicked -> controller.refreshToolbox( + window.getViewer(), state.requestId(), workcell.stableId(), page + 1)); + window.setElement(1, 5, next); + } + }); + } + + static List toolboxTools( + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell + ) { + JigsawStudioMenuState menu = Objects.requireNonNull(state, "Jigsaw Studio toolbox state"); + JigsawStudioMenuState.Workcell selected = Objects.requireNonNull( + workcell, + "Jigsaw Studio toolbox workcell"); + UUID requestId = menu.requestId(); + List tools = new ArrayList<>(); + tools.add(new ToolboxTool( + "Open Control Menu", + JigsawStudioToolPayload.request(JigsawStudioToolAction.OPEN_MENU, requestId))); + tools.add(new ToolboxTool( + "Select " + selected.displayName(), + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.SELECT_WORKCELL, + requestId, + selected.stableId()))); + if (menu.mode() == JigsawStudioMode.PLANAR_JIGSAW) { + tools.add(new ToolboxTool( + "Toggle " + selected.displayName(), + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.TOGGLE_WORKCELL, + requestId, + selected.stableId()))); + } + tools.add(new ToolboxTool( + "Resize " + selected.displayName() + " Capacity", + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.RESIZE_WORKCELL, + requestId, + selected.stableId()))); + tools.add(new ToolboxTool( + "Rename " + selected.displayName(), + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.RENAME_WORKCELL, + requestId, + selected.stableId()))); + tools.add(new ToolboxTool( + "New Blank " + selected.displayName() + " Variant", + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.CREATE_VARIANT, + requestId, + selected.stableId()))); + tools.add(new ToolboxTool( + "Go to Preview", + JigsawStudioToolPayload.request(JigsawStudioToolAction.PREVIEW_GRAPH, requestId))); + tools.add(new ToolboxTool( + "Flush " + selected.displayName() + " Autosave", + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.FLUSH_AUTOSAVE, + requestId, + selected.stableId()))); + if (menu.irisExtended()) { + tools.add(new ToolboxTool( + "Duplicate All Enabled Cells as Family: " + nextThemeSetKey(menu.themeSets()), + JigsawStudioToolPayload.request(JigsawStudioToolAction.DUPLICATE_FAMILY, requestId))); + } + + JigsawStudioMenuState.Variant active = selected.activeVariant(); + if (active != null) { + if (active.owned()) { + tools.add(new ToolboxTool( + "Duplicate This Cell's Variant: " + active.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.DUPLICATE_VARIANT, + requestId, + selected.stableId(), + active.pieceKey()))); + } + if (active.owned()) { + tools.add(new ToolboxTool( + "Rename This Variant: " + active.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.RENAME_VARIANT, + requestId, + selected.stableId(), + active.pieceKey()))); + tools.add(new ToolboxTool( + "Resize This Variant: " + active.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.RESIZE_VARIANT, + requestId, + selected.stableId(), + active.pieceKey()))); + } + if (active.rotationEditable()) { + tools.add(new ToolboxTool( + "Toggle Rotation: " + active.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.TOGGLE_ROTATION, + requestId, + selected.stableId(), + active.pieceKey()))); + } + if (active.owned() && active.resizableToCapacity()) { + tools.add(new ToolboxTool( + "Resize " + active.displayName() + " to Capacity", + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.EXPAND_TO_CELL, + requestId, + selected.stableId(), + active.pieceKey()))); + } + if (active.owned() && menu.irisExtended()) { + tools.add(new ToolboxTool( + "Open Theme Memberships: " + active.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.SET_THEME, + requestId, + selected.stableId(), + active.pieceKey()))); + tools.add(new ToolboxTool( + "Open Piece Rules: " + active.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.SET_PIECE_RULES, + requestId, + selected.stableId(), + active.pieceKey()))); + } + if (active.owned()) { + for (JigsawStudioMenuState.Membership membership : active.memberships()) { + addMembershipTools( + tools, + requestId, + selected, + active, + membership, + menu.irisExtended()); + } + } + } + for (JigsawStudioMenuState.Variant variant : selected.variants()) { + if (variant.active()) { + continue; + } + tools.add(new ToolboxTool( + "Load " + variant.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.LOAD_VARIANT, + requestId, + selected.stableId(), + variant.pieceKey()))); + if (variant.owned()) { + tools.add(new ToolboxTool( + "Rename " + variant.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.RENAME_VARIANT, + requestId, + selected.stableId(), + variant.pieceKey()))); + tools.add(new ToolboxTool( + "Resize " + variant.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.RESIZE_VARIANT, + requestId, + selected.stableId(), + variant.pieceKey()))); + } + if (variant.owned() && selected.variants().size() > 1) { + tools.add(new ToolboxTool( + "Delete " + variant.displayName(), + JigsawStudioToolPayload.variant( + JigsawStudioToolAction.DELETE_VARIANT, + requestId, + selected.stableId(), + variant.pieceKey()))); + } + } + if (menu.irisExtended()) { + tools.add(new ToolboxTool( + menu.requireCaps() ? "Disable Mandatory Caps" : "Enable Mandatory Caps", + JigsawStudioToolPayload.request(JigsawStudioToolAction.TOGGLE_REQUIRE_CAPS, requestId))); + } + tools.add(new ToolboxTool( + "Delete Project", + JigsawStudioToolPayload.request(JigsawStudioToolAction.DELETE_PROJECT, requestId))); + return List.copyOf(tools); + } + + private static void addMembershipTools( + List tools, + UUID requestId, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + JigsawStudioMenuState.Membership membership, + boolean chanceEditable + ) { + String membershipName = displayKey(membership.poolKey()) + " [" + membership.entryIndex() + "]"; + tools.add(new ToolboxTool( + "Weight +1: " + membershipName, + JigsawStudioToolPayload.membership( + JigsawStudioToolAction.ADJUST_VARIANT_WEIGHT, + requestId, + workcell.stableId(), + variant.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + 1))); + tools.add(new ToolboxTool( + "Weight -1: " + membershipName, + JigsawStudioToolPayload.membership( + JigsawStudioToolAction.ADJUST_VARIANT_WEIGHT, + requestId, + workcell.stableId(), + variant.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + -1))); + if (chanceEditable) { + tools.add(new ToolboxTool( + "Chance +" + CHANCE_STEP_PERCENTAGE_POINTS + "%: " + membershipName, + JigsawStudioToolPayload.membership( + JigsawStudioToolAction.ADJUST_VARIANT_CHANCE, + requestId, + workcell.stableId(), + variant.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + CHANCE_STEP_PERCENTAGE_POINTS))); + tools.add(new ToolboxTool( + "Chance -" + CHANCE_STEP_PERCENTAGE_POINTS + "%: " + membershipName, + JigsawStudioToolPayload.membership( + JigsawStudioToolAction.ADJUST_VARIANT_CHANCE, + requestId, + workcell.stableId(), + variant.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + -CHANCE_STEP_PERCENTAGE_POINTS))); + } + tools.add(new ToolboxTool( + "Unlink " + membershipName, + JigsawStudioToolPayload.membership( + JigsawStudioToolAction.UNLINK_MEMBERSHIP, + requestId, + workcell.stableId(), + variant.pieceKey(), + membership.poolKey(), + membership.entryIndex(), + 0))); + } + + private static void addToolBindingLore(UIElement element, JigsawStudioToolPayload payload) { + if (!payload.workcellId().isEmpty()) { + element.addLore(ChatColor.GRAY + "Workcell: " + safe(payload.workcellId())); + } + if (!payload.pieceKey().isEmpty()) { + element.addLore(ChatColor.GRAY + "Variant: " + safe(payload.pieceKey())); + } + if (!payload.poolKey().isEmpty()) { + element.addLore(ChatColor.GRAY + "Pool: " + safe(payload.poolKey()) + + (payload.entryIndex() < 0 ? "" : " [" + payload.entryIndex() + "]")); + } + if (payload.amount() != 0) { + element.addLore(ChatColor.GRAY + "Amount: " + payload.amount()); + } + } + + record ToolboxTool(String displayName, JigsawStudioToolPayload payload) { + ToolboxTool { + displayName = Objects.requireNonNull(displayName, "Jigsaw Studio toolbox display name").trim(); + if (displayName.isEmpty()) { + throw new IllegalArgumentException("Jigsaw Studio toolbox display name cannot be blank"); + } + payload = Objects.requireNonNull(payload, "Jigsaw Studio toolbox payload"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioVariantMenu.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioVariantMenu.java new file mode 100644 index 000000000..7a0581fb2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioVariantMenu.java @@ -0,0 +1,213 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.service.JigsawStudioMenuFormat.RuleField; +import art.arcane.volmlib.util.inventorygui.UIElement; +import art.arcane.volmlib.util.inventorygui.UIWindow; +import org.bukkit.ChatColor; +import org.bukkit.Material; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +import static art.arcane.iris.core.service.JigsawStudioMenuController.GRID_POSITIONS; +import static art.arcane.iris.core.service.JigsawStudioMenuController.THEME_SETS_PER_PAGE; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.clampPage; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.element; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.evaluationElement; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.page; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.pageCount; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.safe; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.themes; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.withTerminal; + +final class JigsawStudioVariantMenu { + private final JigsawStudioMenuController controller; + + JigsawStudioVariantMenu(JigsawStudioMenuController controller) { + this.controller = Objects.requireNonNull(controller, "Jigsaw Studio menu controller"); + } + + void render( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + int requestedPage + ) { + int page = clampPage(requestedPage, state.themeSets().size(), THEME_SETS_PER_PAGE); + window.batch(() -> { + window.clearElements(); + + UIElement back = element("variant-settings-back", Material.ARROW, ChatColor.YELLOW + "Back to Details"); + back.onLeftClick(clicked -> controller.openDetails( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); + window.setElement(-4, 0, back); + + UIElement identity = element( + "variant-settings-identity", + Material.COMPARATOR, + ChatColor.GOLD + safe(variant.displayName()) + " Rules"); + identity.addLore(ChatColor.DARK_GRAY + safe(variant.pieceKey())); + identity.addLore(ChatColor.GRAY + "Theme memberships: " + themes(variant.themes())); + identity.addLore(ChatColor.GRAY + "Use Duplicate All Enabled Cells as Family for coherent sets."); + window.setElement(0, 0, identity); + window.setElement(4, 0, evaluationElement(state.evaluation())); + + if (!variant.active() || !variant.owned() || !state.irisExtended()) { + UIElement unavailable = element( + "variant-settings-unavailable", + Material.BARRIER, + state.irisExtended() + ? ChatColor.RED + "Load an Owned Variant First" + : ChatColor.GRAY + "Iris Metadata Unavailable"); + unavailable.addLore(state.irisExtended() + ? ChatColor.GRAY + "Rules and themes can only change for the loaded owned variant." + : ChatColor.GRAY + "Vanilla-portable pieces use vanilla placement behavior."); + window.setElement(0, 2, unavailable); + } else { + window.setElement(-4, 1, ruleElement( + window, state, workcell, variant, RuleField.MINIMUM_DEPTH)); + window.setElement(-2, 1, ruleElement( + window, state, workcell, variant, RuleField.MAXIMUM_DEPTH)); + window.setElement(0, 1, ruleElement( + window, state, workcell, variant, RuleField.MINIMUM_PLACEMENTS)); + window.setElement(2, 1, ruleElement( + window, state, workcell, variant, RuleField.MAXIMUM_PLACEMENTS)); + + UIElement terminal = element( + "rule-terminal", + variant.rules().terminal() ? Material.REDSTONE_TORCH : Material.LEVER, + variant.rules().terminal() + ? ChatColor.GREEN + "Terminal Piece: Yes" + : ChatColor.YELLOW + "Terminal Piece: No"); + terminal.addLore(ChatColor.GRAY + "Terminal pieces consume a connector without expanding."); + terminal.addLore(ChatColor.YELLOW + "Left-click to toggle"); + terminal.onLeftClick(clicked -> controller.updateVariantRules( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + withTerminal(variant.rules(), !variant.rules().terminal()))); + window.setElement(4, 1, terminal); + + addVariantThemeMemberships(window, state, workcell, variant, page); + } + + UIElement footerBack = element("variant-rules-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); + footerBack.onLeftClick(clicked -> controller.openDetails( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); + window.setElement(-4, 5, footerBack); + + int pageCount = pageCount(state.themeSets().size(), THEME_SETS_PER_PAGE); + if (page > 0) { + UIElement previous = element( + "variant-theme-previous", + Material.ARROW, + ChatColor.YELLOW + "Previous Page"); + previous.onLeftClick(clicked -> controller.refreshVariantSettings( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + page - 1)); + window.setElement(-1, 5, previous); + } + UIElement indicator = element( + "variant-theme-page", + Material.BOOK, + ChatColor.WHITE + "Theme Page " + (page + 1) + "/" + pageCount); + indicator.addLore(ChatColor.GRAY + "" + state.themeSets().size() + " declared themes"); + window.setElement(0, 5, indicator); + if (page + 1 < pageCount) { + UIElement next = element( + "variant-theme-next", + Material.ARROW, + ChatColor.YELLOW + "Next Page"); + next.onLeftClick(clicked -> controller.refreshVariantSettings( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + page + 1)); + window.setElement(1, 5, next); + } + window.setElement(4, 5, evaluationElement(state.evaluation())); + }); + } + + private UIElement ruleElement( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + RuleField field + ) { + int value = field.value(variant.rules()); + UIElement element = element( + "rule-" + field.name().toLowerCase(Locale.ROOT), + field.material(), + ChatColor.AQUA + field.displayName() + ": " + field.displayValue(value)); + boolean unlimitedMaximum = field == RuleField.MAXIMUM_PLACEMENTS && value == 0; + if (unlimitedMaximum) { + element.addLore(ChatColor.GRAY + "Increase is already unlimited."); + } else { + element.addLore(ChatColor.GREEN + "Left-click: +1"); + element.addLore(ChatColor.GREEN + "Shift-left: +" + field.shiftStep()); + element.onLeftClick(clicked -> controller.adjustVariantRule( + window.getViewer(), state.requestId(), workcell, variant, field, 1)); + element.onShiftLeftClick(clicked -> controller.adjustVariantRule( + window.getViewer(), state.requestId(), workcell, variant, field, field.shiftStep())); + } + element.addLore(unlimitedMaximum + ? ChatColor.YELLOW + "Right-click: set 512" + : ChatColor.YELLOW + "Right-click: -1"); + element.addLore(unlimitedMaximum + ? ChatColor.YELLOW + "Shift-right: set 497" + : ChatColor.YELLOW + "Shift-right: -" + field.shiftStep()); + element.onRightClick(clicked -> controller.adjustVariantRule( + window.getViewer(), state.requestId(), workcell, variant, field, -1)); + element.onShiftRightClick(clicked -> controller.adjustVariantRule( + window.getViewer(), state.requestId(), workcell, variant, field, -field.shiftStep())); + return element; + } + + private void addVariantThemeMemberships( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + int page + ) { + List themeSets = page( + state.themeSets(), page, THEME_SETS_PER_PAGE); + if (themeSets.isEmpty()) { + UIElement empty = element( + "variant-no-themes", + Material.GRAY_DYE, + ChatColor.GRAY + "Available to Every Theme"); + empty.addLore(ChatColor.GRAY + "Create a coherent theme set from Structure Rules first."); + window.setElement(0, 3, empty); + return; + } + for (int index = 0; index < themeSets.size(); index++) { + JigsawStudioMenuState.ThemeSet themeSet = themeSets.get(index); + boolean member = variant.themes().contains(themeSet.key()); + int position = GRID_POSITIONS[index % GRID_POSITIONS.length]; + int row = 2 + index / GRID_POSITIONS.length; + UIElement element = element( + "variant-theme-" + index, + member ? Material.LIME_DYE : Material.GRAY_DYE, + (member ? ChatColor.GREEN : ChatColor.GRAY) + safe(themeSet.key())) + .setEnchanted(member); + element.addLore(member + ? ChatColor.GREEN + "This variant belongs to the theme." + : ChatColor.GRAY + "This variant does not belong to the theme."); + element.addLore(ChatColor.GRAY + "Only variants in the selected family are eligible."); + element.addLore(ChatColor.YELLOW + "Left-click to toggle membership"); + element.onLeftClick(clicked -> controller.toggleVariantTheme( + window.getViewer(), state.requestId(), workcell, variant, themeSet.key())); + window.setElement(position, row, element); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/service/JigsawStudioWorkcellMenu.java b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioWorkcellMenu.java new file mode 100644 index 000000000..a80694191 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/JigsawStudioWorkcellMenu.java @@ -0,0 +1,536 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.service.JigsawStudioMenuFormat.DimensionAxis; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolAction; +import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolPayload; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.inventorygui.UIElement; +import art.arcane.volmlib.util.inventorygui.UIWindow; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; + +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import static art.arcane.iris.core.service.JigsawStudioMenuController.send; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.adjustedDimensions; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.dimensions; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.element; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.evaluationElement; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.safe; +import static art.arcane.iris.core.service.JigsawStudioMenuFormat.withCapacity; + +final class JigsawStudioWorkcellMenu { + private static final int WORKCELL_RESIZE_REFRESH_TICKS = 2; + private static final int WORKCELL_RESIZE_REFRESH_ATTEMPTS = 200; + private final Map pendingWorkcellResizes = new ConcurrentHashMap<>(); + + private final JigsawStudioMenuController controller; + + JigsawStudioWorkcellMenu(JigsawStudioMenuController controller) { + this.controller = Objects.requireNonNull(controller, "Jigsaw Studio menu controller"); + } + + void render( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell + ) { + PendingWorkcellResize pendingResize = pendingWorkcellResize( + window.getViewer().getUniqueId(), + state.requestId(), + workcell.stableId()); + JigsawStudioMenuState.Workcell renderedWorkcell = pendingResize == null + ? workcell + : withCapacity(workcell, pendingResize.dimensions()); + window.batch(() -> { + window.clearElements(); + + UIElement back = element("settings-back", Material.ARROW, ChatColor.YELLOW + "Back to Variants"); + back.onLeftClick(clicked -> leaveWorkcellSettings( + window.getViewer(), state.requestId(), renderedWorkcell.stableId())); + window.setElement(-4, 0, back); + + UIElement identity = element( + "settings-identity", + renderedWorkcell.enabled() ? Material.LIME_WOOL : Material.GRAY_WOOL, + ChatColor.AQUA + safe(renderedWorkcell.displayName())); + identity.addLore(ChatColor.DARK_GRAY + safe(renderedWorkcell.stableId())); + identity.addLore(renderedWorkcell.enabled() + ? ChatColor.GREEN + "Enabled" + : ChatColor.RED + "Disabled for assembly and export"); + if (!renderedWorkcell.canonicalName().equals(renderedWorkcell.displayName())) { + identity.addLore(ChatColor.GRAY + "Solver role: " + safe(renderedWorkcell.canonicalName())); + } + identity.addLore(ChatColor.GRAY + "Capacity: " + dimensions(renderedWorkcell.capacity())); + if (pendingResize != null) { + identity.addLore(pendingResize.applying() + ? ChatColor.YELLOW + "Applying one live relayout" + : ChatColor.GOLD + "Pending; apply when all dimensions are ready"); + } + identity.addLore(ChatColor.YELLOW + "Left-click for a rename stick"); + identity.addLore(ChatColor.GRAY + "Rename that stick in an anvil, then right-click it."); + identity.addLore(ChatColor.GRAY + "Sneak-right-click the stick to reset this label."); + identity.onLeftClick(clicked -> controller.actions.giveTool( + window.getViewer(), + JigsawStudioToolPayload.workcell( + JigsawStudioToolAction.RENAME_WORKCELL, + state.requestId(), + renderedWorkcell.stableId()))); + window.setElement(0, 0, identity); + window.setElement(4, 0, evaluationElement(state.evaluation())); + + if (state.mode() == JigsawStudioMode.PLANAR_JIGSAW) { + UIElement enabled = element( + "settings-enabled", + renderedWorkcell.enabled() ? Material.LEVER : Material.REDSTONE_TORCH, + renderedWorkcell.enabled() + ? ChatColor.GREEN + "Workcell Enabled" + : ChatColor.RED + "Workcell Disabled"); + enabled.addLore(ChatColor.GRAY + "Disabled workcells remain editable and keep their size."); + enabled.addLore(ChatColor.YELLOW + "Left-click to " + + (renderedWorkcell.enabled() ? "disable" : "enable")); + enabled.onLeftClick(clicked -> controller.setWorkcellEnabled( + window.getViewer(), + state.requestId(), + renderedWorkcell.stableId(), + !renderedWorkcell.enabled())); + window.setElement(0, 1, enabled); + } else { + UIElement spatial = element( + "spatial-bounds", + Material.SCAFFOLDING, + ChatColor.AQUA + "Spatial Workcell Bounds"); + spatial.addLore(ChatColor.GRAY + "Spatial project bounds are shared by the project."); + spatial.addLore(ChatColor.GRAY + "Changing them regenerates the Studio layout."); + window.setElement(0, 1, spatial); + } + + UIElement connectors = element( + "settings-connectors", + renderedWorkcell.connectorsVisible() ? Material.JIGSAW : Material.STRUCTURE_VOID, + renderedWorkcell.connectorsVisible() + ? ChatColor.GREEN + "Connector Blocks Visible" + : ChatColor.YELLOW + "Connector Blocks Hidden"); + connectors.addLore(ChatColor.GRAY + "Hidden connectors retain their metadata and final block state."); + connectors.addLore(ChatColor.YELLOW + "Left-click to " + + (renderedWorkcell.connectorsVisible() ? "hide" : "show") + " connector blocks"); + connectors.onLeftClick(clicked -> controller.toggleConnectorBlocks( + window.getViewer(), + state.requestId(), + renderedWorkcell.stableId(), + !renderedWorkcell.connectorsVisible())); + window.setElement(2, 1, connectors); + + UIElement resetConnectors = element( + "settings-reset-connectors", + Material.RECOVERY_COMPASS, + ChatColor.AQUA + "Reset Connector Blocks"); + resetConnectors.addLore(ChatColor.GRAY + "Restore every connector in this workcell from disk."); + resetConnectors.addLore(ChatColor.GRAY + "Other edited blocks are left unchanged."); + resetConnectors.onLeftClick(clicked -> controller.resetConnectorBlocks( + window.getViewer(), + state.requestId(), + renderedWorkcell.stableId())); + window.setElement(4, 1, resetConnectors); + + window.setElement(-2, 2, axisElement( + window, + state, + renderedWorkcell, + DimensionAxis.WIDTH, + Material.IRON_INGOT)); + window.setElement(0, 2, axisElement( + window, + state, + renderedWorkcell, + DimensionAxis.HEIGHT, + Material.GOLD_INGOT)); + window.setElement(2, 2, axisElement( + window, + state, + renderedWorkcell, + DimensionAxis.DEPTH, + Material.COPPER_INGOT)); + + if (pendingResize != null) { + UIElement apply = element( + "settings-apply-capacity", + pendingResize.applying() ? Material.CLOCK : Material.EMERALD_BLOCK, + pendingResize.applying() + ? ChatColor.YELLOW + "Applying Cell Size" + : ChatColor.GREEN + "Apply Cell Size"); + apply.addLore(ChatColor.WHITE + dimensions(pendingResize.dimensions())); + apply.addLore(ChatColor.GRAY + "Regenerates the layout once after all size edits."); + if (!pendingResize.applying()) { + apply.onLeftClick(clicked -> applyWorkcellResize( + window.getViewer(), state.requestId(), renderedWorkcell.stableId())); + } + window.setElement(0, 3, apply); + + if (!pendingResize.applying()) { + UIElement discard = element( + "settings-discard-capacity", + Material.BARRIER, + ChatColor.RED + "Discard Size Changes"); + discard.onLeftClick(clicked -> discardWorkcellResize( + window.getViewer(), state.requestId(), renderedWorkcell.stableId())); + window.setElement(2, 3, discard); + } + } + + UIElement footerBack = element("settings-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); + footerBack.onLeftClick(clicked -> leaveWorkcellSettings( + window.getViewer(), state.requestId(), renderedWorkcell.stableId())); + window.setElement(-4, 5, footerBack); + + UIElement undo = element( + "settings-undo", + Material.CLOCK, + ChatColor.LIGHT_PURPLE + "Undo Last Autosave"); + undo.addLore(ChatColor.GRAY + "Restore the previous owned graph iteration."); + undo.addLore(ChatColor.GRAY + "Up to five autosave iterations are retained on disk."); + undo.onLeftClick(clicked -> controller.undoAutosave( + window.getViewer(), + state.requestId())); + window.setElement(0, 5, undo); + + if (renderedWorkcell.dirty() && !renderedWorkcell.saving()) { + UIElement saveNow = element( + "save-now", + Material.EMERALD, + ChatColor.GOLD + "Flush Autosave Now"); + saveNow.addLore(ChatColor.GRAY + "Autosave is automatic."); + saveNow.addLore(ChatColor.GRAY + "Use this only to flush pending work or recover immediately."); + saveNow.onLeftClick(clicked -> controller.flushNow( + window.getViewer(), state.requestId(), renderedWorkcell.stableId())); + window.setElement(2, 5, saveNow); + } + + UIElement preview = element( + "settings-preview", + Material.ENDER_EYE, + ChatColor.LIGHT_PURPLE + "Go to Preview"); + preview.onLeftClick(clicked -> controller.goToPreview(window.getViewer(), state.requestId())); + window.setElement(3, 5, preview); + + UIElement toolbox = element("settings-toolbox", Material.STICK, ChatColor.AQUA + "Toolbox"); + toolbox.onLeftClick(clicked -> controller.openToolbox( + window.getViewer(), state.requestId(), renderedWorkcell.stableId(), 0)); + window.setElement(4, 5, toolbox); + }); + } + + void renderSize( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant + ) { + JigsawStudioCellDimensions current = variant.dimensions().orElseThrow(); + window.batch(() -> { + window.clearElements(); + + UIElement back = element("variant-size-back", Material.ARROW, ChatColor.YELLOW + "Back to Details"); + back.onLeftClick(clicked -> controller.openDetails( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); + window.setElement(-4, 0, back); + + UIElement identity = element( + "variant-size-identity", + Material.SCAFFOLDING, + ChatColor.AQUA + safe(variant.displayName()) + " Size"); + identity.addLore(ChatColor.DARK_GRAY + safe(variant.pieceKey())); + identity.addLore(ChatColor.WHITE + "Current: " + dimensions(current)); + identity.addLore(ChatColor.GRAY + "Workcell capacity: " + dimensions(workcell.capacity())); + identity.addLore(ChatColor.GRAY + "Only this variant object and its edge connectors change."); + window.setElement(0, 0, identity); + window.setElement(4, 0, evaluationElement(state.evaluation())); + + window.setElement(-2, 2, variantAxisElement( + window, state, workcell, variant, DimensionAxis.WIDTH, Material.IRON_INGOT)); + window.setElement(0, 2, variantAxisElement( + window, state, workcell, variant, DimensionAxis.HEIGHT, Material.GOLD_INGOT)); + window.setElement(2, 2, variantAxisElement( + window, state, workcell, variant, DimensionAxis.DEPTH, Material.COPPER_INGOT)); + + UIElement capacity = element( + "variant-size-capacity", + variant.resizableToCapacity() ? Material.NETHER_STAR : Material.GRAY_DYE, + variant.resizableToCapacity() + ? ChatColor.GREEN + "Resize This Variant to Capacity" + : ChatColor.GRAY + "Variant Already Matches Capacity"); + capacity.addLore(ChatColor.GRAY + dimensions(workcell.capacity())); + if (variant.resizableToCapacity()) { + capacity.addLore(ChatColor.YELLOW + "Left-click to resize only this variant"); + capacity.onLeftClick(clicked -> controller.resizeVariant( + window.getViewer(), + state.requestId(), + workcell.stableId(), + variant.pieceKey(), + workcell.capacity())); + } + window.setElement(0, 3, capacity); + + UIElement footerBack = element("variant-size-footer-back", Material.ARROW, ChatColor.YELLOW + "Back"); + footerBack.onLeftClick(clicked -> controller.openDetails( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), 0)); + window.setElement(-4, 5, footerBack); + }); + } + + private UIElement variantAxisElement( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + JigsawStudioMenuState.Variant variant, + DimensionAxis axis, + Material material + ) { + JigsawStudioCellDimensions current = variant.dimensions().orElseThrow(); + UIElement element = element( + "variant-axis-" + axis.name().toLowerCase(Locale.ROOT), + material, + ChatColor.AQUA + axis.displayName() + ": " + axis.value(current)); + element.addLore(ChatColor.GREEN + "Left-click: +1"); + element.addLore(ChatColor.YELLOW + "Right-click: -1"); + element.addLore(ChatColor.GREEN + "Shift-left: +8"); + element.addLore(ChatColor.YELLOW + "Shift-right: -8"); + element.addLore(ChatColor.GRAY + "Maximum: " + axis.value(workcell.capacity())); + element.onLeftClick(clicked -> controller.resizeVariantAxis( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), axis, 1)); + element.onRightClick(clicked -> controller.resizeVariantAxis( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), axis, -1)); + element.onShiftLeftClick(clicked -> controller.resizeVariantAxis( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), axis, 8)); + element.onShiftRightClick(clicked -> controller.resizeVariantAxis( + window.getViewer(), state.requestId(), workcell.stableId(), variant.pieceKey(), axis, -8)); + return element; + } + + private UIElement axisElement( + UIWindow window, + JigsawStudioMenuState state, + JigsawStudioMenuState.Workcell workcell, + DimensionAxis axis, + Material material + ) { + UIElement element = element( + "axis-" + axis.name().toLowerCase(Locale.ROOT), + material, + ChatColor.AQUA + axis.displayName() + " Capacity: " + axis.value(workcell.capacity())); + element.addLore(ChatColor.GREEN + "Left-click: +1"); + element.addLore(ChatColor.YELLOW + "Right-click: -1"); + element.addLore(ChatColor.GREEN + "Shift-left: +8"); + element.addLore(ChatColor.YELLOW + "Shift-right: -8"); + element.addLore(ChatColor.GRAY + "Changes stay in this menu until Apply Cell Size."); + element.onLeftClick(clicked -> resizeWorkcell( + window.getViewer(), state.requestId(), workcell.stableId(), axis, 1)); + element.onRightClick(clicked -> resizeWorkcell( + window.getViewer(), state.requestId(), workcell.stableId(), axis, -1)); + element.onShiftLeftClick(clicked -> resizeWorkcell( + window.getViewer(), state.requestId(), workcell.stableId(), axis, 8)); + element.onShiftRightClick(clicked -> resizeWorkcell( + window.getViewer(), state.requestId(), workcell.stableId(), axis, -8)); + return element; + } + + private void resizeWorkcell( + Player player, + UUID requestId, + String workcellId, + DimensionAxis axis, + int delta + ) { + Optional current = controller.matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + if (workcell == null) { + controller.stale(player); + return; + } + UUID playerId = player.getUniqueId(); + PendingWorkcellResize existing = pendingWorkcellResize(playerId, requestId, workcellId); + if (existing != null && existing.applying()) { + send(player, ChatColor.YELLOW + "That cell size is already being applied."); + return; + } + JigsawStudioCellDimensions base = existing == null + ? workcell.capacity() + : existing.dimensions(); + Optional adjusted = adjustedDimensions( + base, axis, delta); + if (adjusted.isEmpty()) { + send(player, ChatColor.RED + "That workcell size is outside Iris limits."); + return; + } + PendingWorkcellResize pending = new PendingWorkcellResize( + requestId, + workcellId, + adjusted.get(), + false); + pendingWorkcellResizes.put(playerId, pending); + UIWindow window = controller.windows.get(playerId); + if (window != null) { + render(window, current.get(), withCapacity(workcell, pending.dimensions())); + } + } + + private void applyWorkcellResize(Player player, UUID requestId, String workcellId) { + Optional current = controller.matchingState(player, requestId, true); + if (current.isEmpty()) { + return; + } + UUID playerId = player.getUniqueId(); + PendingWorkcellResize pending = pendingWorkcellResize(playerId, requestId, workcellId); + JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId); + if (pending == null || pending.applying() || workcell == null) { + return; + } + if (workcell.capacity().equals(pending.dimensions())) { + pendingWorkcellResizes.remove(playerId, pending); + refreshWorkcellSettings(player, requestId, workcellId); + return; + } + PendingWorkcellResize applying = new PendingWorkcellResize( + requestId, + workcellId, + pending.dimensions(), + true); + pendingWorkcellResizes.put(playerId, applying); + UIWindow window = controller.windows.get(playerId); + if (window != null) { + render(window, current.get(), withCapacity(workcell, applying.dimensions())); + } + if (!controller.actions.updateWorkcellDimensions(player, workcellId, applying.dimensions())) { + pendingWorkcellResizes.replace(playerId, applying, pending); + refreshWorkcellSettings(player, requestId, workcellId); + return; + } + scheduleWorkcellResizeRefresh(player, applying, WORKCELL_RESIZE_REFRESH_ATTEMPTS); + } + + private void scheduleWorkcellResizeRefresh( + Player player, + PendingWorkcellResize pending, + int attemptsRemaining + ) { + boolean scheduled = J.runEntity( + player, + () -> refreshAppliedWorkcellResize(player, pending, attemptsRemaining), + WORKCELL_RESIZE_REFRESH_TICKS); + if (!scheduled) { + pendingWorkcellResizes.remove(player.getUniqueId(), pending); + } + } + + private void refreshAppliedWorkcellResize( + Player player, + PendingWorkcellResize pending, + int attemptsRemaining + ) { + UUID playerId = player.getUniqueId(); + if (!pending.equals(pendingWorkcellResizes.get(playerId))) { + return; + } + Optional current = controller.matchingState(player, pending.requestId(), false); + JigsawStudioMenuState.Workcell workcell = current + .map(state -> state.workcell(pending.workcellId())) + .orElse(null); + if (workcell == null) { + pendingWorkcellResizes.remove(playerId, pending); + return; + } + if (workcell.capacity().equals(pending.dimensions())) { + pendingWorkcellResizes.remove(playerId, pending); + UIWindow window = controller.windows.get(playerId); + if (window != null) { + render(window, current.orElseThrow(), workcell); + } + return; + } + if (attemptsRemaining > 0) { + scheduleWorkcellResizeRefresh(player, pending, attemptsRemaining - 1); + return; + } + PendingWorkcellResize retry = new PendingWorkcellResize( + pending.requestId(), + pending.workcellId(), + pending.dimensions(), + false); + pendingWorkcellResizes.replace(playerId, pending, retry); + UIWindow window = controller.windows.get(playerId); + if (window != null) { + render(window, current.orElseThrow(), withCapacity(workcell, retry.dimensions())); + } + send(player, ChatColor.YELLOW + + "Cell resizing is still pending; use Apply Cell Size to retry after the current operation settles."); + } + + private void discardWorkcellResize(Player player, UUID requestId, String workcellId) { + PendingWorkcellResize pending = pendingWorkcellResize(player.getUniqueId(), requestId, workcellId); + if (pending != null && !pending.applying()) { + pendingWorkcellResizes.remove(player.getUniqueId(), pending); + } + refreshWorkcellSettings(player, requestId, workcellId); + } + + private void leaveWorkcellSettings(Player player, UUID requestId, String workcellId) { + PendingWorkcellResize pending = pendingWorkcellResize(player.getUniqueId(), requestId, workcellId); + if (pending != null && !pending.applying()) { + pendingWorkcellResizes.remove(player.getUniqueId(), pending); + } + controller.refreshMain(player, requestId, workcellId, 0); + } + + private void refreshWorkcellSettings(Player player, UUID requestId, String workcellId) { + Optional current = controller.matchingState(player, requestId, true); + UIWindow window = controller.windows.get(player.getUniqueId()); + JigsawStudioMenuState.Workcell workcell = current + .map(state -> state.workcell(workcellId)) + .orElse(null); + if (window == null || workcell == null) { + return; + } + render(window, current.orElseThrow(), workcell); + } + + private PendingWorkcellResize pendingWorkcellResize(UUID playerId, UUID requestId, String workcellId) { + PendingWorkcellResize pending = pendingWorkcellResizes.get(playerId); + if (pending == null + || !pending.requestId().equals(requestId) + || !pending.workcellId().equals(workcellId)) { + return null; + } + return pending; + } + + private record PendingWorkcellResize( + UUID requestId, + String workcellId, + JigsawStudioCellDimensions dimensions, + boolean applying + ) { + private PendingWorkcellResize { + requestId = Objects.requireNonNull(requestId, "Jigsaw Studio resize request ID"); + workcellId = Objects.requireNonNull(workcellId, "Jigsaw Studio resize workcell ID"); + dimensions = Objects.requireNonNull(dimensions, "Jigsaw Studio resize dimensions"); + } + } + void clearStagedResize(UUID playerId) { + pendingWorkcellResizes.remove(playerId); + } + + void clearStagedResizes() { + pendingWorkcellResizes.clear(); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/service/JigsawStudioMenuControllerTest.java b/core/src/test/java/art/arcane/iris/core/service/JigsawStudioMenuControllerTest.java index 7725abc38..9b86e7ac7 100644 --- a/core/src/test/java/art/arcane/iris/core/service/JigsawStudioMenuControllerTest.java +++ b/core/src/test/java/art/arcane/iris/core/service/JigsawStudioMenuControllerTest.java @@ -57,13 +57,13 @@ public class JigsawStudioMenuControllerTest { values.add(index); } - assertEquals(1, JigsawStudioMenuController.pageCount(0, 28)); - assertEquals(1, JigsawStudioMenuController.pageCount(28, 28)); - assertEquals(2, JigsawStudioMenuController.pageCount(29, 28)); - assertEquals(List.of(0, 1, 2), JigsawStudioMenuController.page(values, -10, 3)); - assertEquals(List.of(54, 55, 56), JigsawStudioMenuController.page(values, 999, 3)); + assertEquals(1, JigsawStudioMenuFormat.pageCount(0, 28)); + assertEquals(1, JigsawStudioMenuFormat.pageCount(28, 28)); + assertEquals(2, JigsawStudioMenuFormat.pageCount(29, 28)); + assertEquals(List.of(0, 1, 2), JigsawStudioMenuFormat.page(values, -10, 3)); + assertEquals(List.of(54, 55, 56), JigsawStudioMenuFormat.page(values, 999, 3)); assertThrows(IllegalArgumentException.class, - () -> JigsawStudioMenuController.pageCount(1, 0)); + () -> JigsawStudioMenuFormat.pageCount(1, 0)); } @Test @@ -86,7 +86,7 @@ public class JigsawStudioMenuControllerTest { false, List.of(active)); - assertEquals(ChatColor.GREEN + "Autosaved", JigsawStudioMenuController.workcellStatus(fresh)); + assertEquals(ChatColor.GREEN + "Autosaved", JigsawStudioMenuFormat.workcellStatus(fresh)); } @Test @@ -97,11 +97,11 @@ public class JigsawStudioMenuControllerTest { List.of(), List.of()); - assertEquals(Material.JIGSAW, JigsawStudioMenuController.variantMaterial(active)); + assertEquals(Material.JIGSAW, JigsawStudioMenuFormat.variantMaterial(active)); assertEquals(Material.EMERALD, - JigsawStudioMenuController.evaluationMaterial(JigsawStudioEvaluationState.VALID)); + JigsawStudioMenuFormat.evaluationMaterial(JigsawStudioEvaluationState.VALID)); assertEquals(Material.TARGET, - JigsawStudioMenuController.RuleField.MINIMUM_PLACEMENTS.material()); + JigsawStudioMenuFormat.RuleField.MINIMUM_PLACEMENTS.material()); } @Test @@ -217,33 +217,33 @@ public class JigsawStudioMenuControllerTest { assertEquals( new JigsawStudioCellDimensions(25, 12, 18), - JigsawStudioMenuController.adjustedDimensions( + JigsawStudioMenuFormat.adjustedDimensions( dimensions, - JigsawStudioMenuController.DimensionAxis.WIDTH, + JigsawStudioMenuFormat.DimensionAxis.WIDTH, 1).orElseThrow()); assertEquals( new JigsawStudioCellDimensions(24, 20, 18), - JigsawStudioMenuController.adjustedDimensions( + JigsawStudioMenuFormat.adjustedDimensions( dimensions, - JigsawStudioMenuController.DimensionAxis.HEIGHT, + JigsawStudioMenuFormat.DimensionAxis.HEIGHT, 8).orElseThrow()); assertEquals( new JigsawStudioCellDimensions(24, 12, 10), - JigsawStudioMenuController.adjustedDimensions( + JigsawStudioMenuFormat.adjustedDimensions( dimensions, - JigsawStudioMenuController.DimensionAxis.DEPTH, + JigsawStudioMenuFormat.DimensionAxis.DEPTH, -8).orElseThrow()); - assertTrue(JigsawStudioMenuController.adjustedDimensions( + assertTrue(JigsawStudioMenuFormat.adjustedDimensions( new JigsawStudioCellDimensions(1, 1, 1), - JigsawStudioMenuController.DimensionAxis.WIDTH, + JigsawStudioMenuFormat.DimensionAxis.WIDTH, -1).isEmpty()); - assertTrue(JigsawStudioMenuController.adjustedDimensions( + assertTrue(JigsawStudioMenuFormat.adjustedDimensions( new JigsawStudioCellDimensions(128, 1, 1), - JigsawStudioMenuController.DimensionAxis.WIDTH, + JigsawStudioMenuFormat.DimensionAxis.WIDTH, 1).isEmpty()); - assertThrows(IllegalArgumentException.class, () -> JigsawStudioMenuController.adjustedDimensions( + assertThrows(IllegalArgumentException.class, () -> JigsawStudioMenuFormat.adjustedDimensions( dimensions, - JigsawStudioMenuController.DimensionAxis.WIDTH, + JigsawStudioMenuFormat.DimensionAxis.WIDTH, 0)); } @@ -251,21 +251,22 @@ public class JigsawStudioMenuControllerTest { public void stagesMultipleCellSizeEditsAndAppliesOneRelayoutWithoutClosing() throws Exception { UUID playerId = UUID.fromString("55555555-5555-5555-5555-555555555555"); Player player = mock(Player.class); - JigsawStudioMenuController.Actions actions = mock(JigsawStudioMenuController.Actions.class); + JigsawStudioMenuActions actions = mock(JigsawStudioMenuActions.class); JigsawStudioMenuState.Variant active = variant("pieces/corner", true, List.of(), List.of()); JigsawStudioMenuState.Workcell workcell = workcell(active); JigsawStudioMenuState state = state(JigsawStudioMenuState.Evaluation.pending(), workcell); JigsawStudioMenuController controller = new JigsawStudioMenuController( mock(JavaPlugin.class), actions); - Method resize = JigsawStudioMenuController.class.getDeclaredMethod( + JigsawStudioWorkcellMenu workcellMenu = new JigsawStudioWorkcellMenu(controller); + Method resize = JigsawStudioWorkcellMenu.class.getDeclaredMethod( "resizeWorkcell", Player.class, UUID.class, String.class, - JigsawStudioMenuController.DimensionAxis.class, + JigsawStudioMenuFormat.DimensionAxis.class, int.class); - Method apply = JigsawStudioMenuController.class.getDeclaredMethod( + Method apply = JigsawStudioWorkcellMenu.class.getDeclaredMethod( "applyWorkcellResize", Player.class, UUID.class, @@ -280,18 +281,18 @@ public class JigsawStudioMenuControllerTest { new JigsawStudioCellDimensions(25, 20, 18))).thenReturn(true); resize.invoke( - controller, + workcellMenu, player, REQUEST_ID, workcell.stableId(), - JigsawStudioMenuController.DimensionAxis.WIDTH, + JigsawStudioMenuFormat.DimensionAxis.WIDTH, 1); resize.invoke( - controller, + workcellMenu, player, REQUEST_ID, workcell.stableId(), - JigsawStudioMenuController.DimensionAxis.HEIGHT, + JigsawStudioMenuFormat.DimensionAxis.HEIGHT, 8); verify(actions, never()).updateWorkcellDimensions( @@ -304,7 +305,7 @@ public class JigsawStudioMenuControllerTest { any(Runnable.class), eq(2))).thenReturn(true); - apply.invoke(controller, player, REQUEST_ID, workcell.stableId()); + apply.invoke(workcellMenu, player, REQUEST_ID, workcell.stableId()); verify(actions).updateWorkcellDimensions( player, @@ -319,7 +320,7 @@ public class JigsawStudioMenuControllerTest { JigsawStudioMenuState.Workcell source = workcell(active); JigsawStudioCellDimensions capacity = new JigsawStudioCellDimensions(31, 19, 27); - JigsawStudioMenuState.Workcell staged = JigsawStudioMenuController.withCapacity(source, capacity); + JigsawStudioMenuState.Workcell staged = JigsawStudioMenuFormat.withCapacity(source, capacity); assertEquals(capacity, staged.capacity()); assertEquals(source.stableId(), staged.stableId()); @@ -335,12 +336,12 @@ public class JigsawStudioMenuControllerTest { new JigsawStudioMenuState.ThemeSet("seasonal", 4), new JigsawStudioMenuState.ThemeSet("variant-2", 2)); - assertEquals("variant-3", JigsawStudioMenuController.nextThemeSetKey(themeSets)); - assertEquals("variant-1", JigsawStudioMenuController.nextThemeSetKey(List.of())); - assertEquals(Integer.valueOf(12), JigsawStudioMenuController.adjustedPositiveValue(4, 8).orElseThrow()); - assertTrue(JigsawStudioMenuController.adjustedPositiveValue(4, -8).isEmpty()); + assertEquals("variant-3", JigsawStudioMenuFormat.nextThemeSetKey(themeSets)); + assertEquals("variant-1", JigsawStudioMenuFormat.nextThemeSetKey(List.of())); + assertEquals(Integer.valueOf(12), JigsawStudioMenuFormat.adjustedPositiveValue(4, 8).orElseThrow()); + assertTrue(JigsawStudioMenuFormat.adjustedPositiveValue(4, -8).isEmpty()); assertThrows(IllegalArgumentException.class, - () -> JigsawStudioMenuController.adjustedPositiveValue(4, 0)); + () -> JigsawStudioMenuFormat.adjustedPositiveValue(4, 0)); } @Test @@ -349,10 +350,10 @@ public class JigsawStudioMenuControllerTest { new JigsawStudioMenuState.ThemeSet("variant-1", 3), new JigsawStudioMenuState.ThemeSet("variant-2", 1)); - assertEquals("75.0%", JigsawStudioMenuController.themeSelectionPercent( + assertEquals("75.0%", JigsawStudioMenuFormat.themeSelectionPercent( themes, themes.getFirst())); - assertEquals("25.0%", JigsawStudioMenuController.themeSelectionPercent( + assertEquals("25.0%", JigsawStudioMenuFormat.themeSelectionPercent( themes, themes.getLast())); } @@ -361,47 +362,47 @@ public class JigsawStudioMenuControllerTest { public void editsPieceRuleFieldsWithinRuntimeBounds() { assertEquals( new JigsawStudioPieceRules(2, 12, 2, 8, false), - JigsawStudioMenuController.adjustedRules( + JigsawStudioMenuFormat.adjustedRules( RULES, - JigsawStudioMenuController.RuleField.MINIMUM_DEPTH, + JigsawStudioMenuFormat.RuleField.MINIMUM_DEPTH, 1).orElseThrow()); assertEquals( new JigsawStudioPieceRules(1, 17, 2, 8, false), - JigsawStudioMenuController.adjustedRules( + JigsawStudioMenuFormat.adjustedRules( RULES, - JigsawStudioMenuController.RuleField.MAXIMUM_DEPTH, + JigsawStudioMenuFormat.RuleField.MAXIMUM_DEPTH, 5).orElseThrow()); assertEquals( new JigsawStudioPieceRules(1, 12, 3, 8, false), - JigsawStudioMenuController.adjustedRules( + JigsawStudioMenuFormat.adjustedRules( RULES, - JigsawStudioMenuController.RuleField.MINIMUM_PLACEMENTS, + JigsawStudioMenuFormat.RuleField.MINIMUM_PLACEMENTS, 1).orElseThrow()); assertEquals( new JigsawStudioPieceRules(1, 12, 2, 24, false), - JigsawStudioMenuController.adjustedRules( + JigsawStudioMenuFormat.adjustedRules( RULES, - JigsawStudioMenuController.RuleField.MAXIMUM_PLACEMENTS, + JigsawStudioMenuFormat.RuleField.MAXIMUM_PLACEMENTS, 16).orElseThrow()); assertEquals( new JigsawStudioPieceRules(0, 30, 0, 0, false), - JigsawStudioMenuController.adjustedRules( + JigsawStudioMenuFormat.adjustedRules( new JigsawStudioPieceRules(0, 30, 0, 512, false), - JigsawStudioMenuController.RuleField.MAXIMUM_PLACEMENTS, + JigsawStudioMenuFormat.RuleField.MAXIMUM_PLACEMENTS, 1).orElseThrow()); assertEquals( new JigsawStudioPieceRules(0, 30, 0, 512, false), - JigsawStudioMenuController.adjustedRules( + JigsawStudioMenuFormat.adjustedRules( new JigsawStudioPieceRules(0, 30, 0, 0, false), - JigsawStudioMenuController.RuleField.MAXIMUM_PLACEMENTS, + JigsawStudioMenuFormat.RuleField.MAXIMUM_PLACEMENTS, -1).orElseThrow()); - assertTrue(JigsawStudioMenuController.adjustedRules( + assertTrue(JigsawStudioMenuFormat.adjustedRules( RULES, - JigsawStudioMenuController.RuleField.MINIMUM_DEPTH, + JigsawStudioMenuFormat.RuleField.MINIMUM_DEPTH, 12).isEmpty()); assertEquals( new JigsawStudioPieceRules(1, 12, 2, 8, true), - JigsawStudioMenuController.withTerminal(RULES, true)); + JigsawStudioMenuFormat.withTerminal(RULES, true)); } @Test @@ -424,23 +425,23 @@ public class JigsawStudioMenuControllerTest { JigsawStudioMenuState.Workcell corner = workcell(active, List.of(active, inactive)); JigsawStudioMenuState state = state(JigsawStudioMenuState.Evaluation.pending(), corner); - List tools = JigsawStudioMenuController.toolboxTools( + List tools = JigsawStudioToolboxMenu.toolboxTools( state, corner); - JigsawStudioMenuController.ToolboxTool preview = tool(tools, JigsawStudioToolAction.PREVIEW_GRAPH, 0); - JigsawStudioMenuController.ToolboxTool chanceIncrease = tool( + JigsawStudioToolboxMenu.ToolboxTool preview = tool(tools, JigsawStudioToolAction.PREVIEW_GRAPH, 0); + JigsawStudioToolboxMenu.ToolboxTool chanceIncrease = tool( tools, JigsawStudioToolAction.ADJUST_VARIANT_CHANCE, JigsawStudioMenuController.CHANCE_STEP_PERCENTAGE_POINTS); - JigsawStudioMenuController.ToolboxTool delete = tool( + JigsawStudioToolboxMenu.ToolboxTool delete = tool( tools, JigsawStudioToolAction.DELETE_VARIANT, 0); - JigsawStudioMenuController.ToolboxTool newThemeSet = tools.stream() + JigsawStudioToolboxMenu.ToolboxTool newThemeSet = tools.stream() .filter(tool -> tool.payload().action() == JigsawStudioToolAction.DUPLICATE_FAMILY) .findFirst() .orElseThrow(); - JigsawStudioMenuController.ToolboxTool pieceRules = tools.stream() + JigsawStudioToolboxMenu.ToolboxTool pieceRules = tools.stream() .filter(tool -> tool.payload().action() == JigsawStudioToolAction.SET_PIECE_RULES) .findFirst() .orElseThrow(); @@ -489,7 +490,7 @@ public class JigsawStudioMenuControllerTest { JigsawStudioMenuState.Evaluation.pending(), List.of(room)); - List tools = JigsawStudioMenuController.toolboxTools(state, room); + List tools = JigsawStudioToolboxMenu.toolboxTools(state, room); assertTrue(tools.stream().anyMatch(tool -> tool.payload().action() == JigsawStudioToolAction.RESIZE_WORKCELL)); @@ -519,7 +520,7 @@ public class JigsawStudioMenuControllerTest { JigsawStudioMenuState.Evaluation.pending(), List.of(corner)); - List tools = JigsawStudioMenuController.toolboxTools(state, corner); + List tools = JigsawStudioToolboxMenu.toolboxTools(state, corner); assertFalse(state.irisExtended()); assertTrue(tools.stream().noneMatch(tool -> tool.payload().action() == JigsawStudioToolAction.SET_THEME)); @@ -536,7 +537,7 @@ public class JigsawStudioMenuControllerTest { @Test public void openMarshalsToThePlayerRegionBeforeReadingStudioState() { Player player = mock(Player.class); - JigsawStudioMenuController.Actions actions = mock(JigsawStudioMenuController.Actions.class); + JigsawStudioMenuActions actions = mock(JigsawStudioMenuActions.class); JigsawStudioMenuController controller = new JigsawStudioMenuController( mock(JavaPlugin.class), actions); @@ -649,12 +650,12 @@ public class JigsawStudioMenuControllerTest { memberships); } - private JigsawStudioMenuController.ToolboxTool tool( - List tools, + private JigsawStudioToolboxMenu.ToolboxTool tool( + List tools, JigsawStudioToolAction action, int amount ) { - Optional match = tools.stream() + Optional match = tools.stream() .filter(tool -> tool.payload().action() == action && tool.payload().amount() == amount) .findFirst(); return match.orElseThrow();