From 979e7c743a26284ce82956d8ed4a88062ab3b460 Mon Sep 17 00:00:00 2001 From: DarkBladeDev Date: Sat, 25 Jul 2026 05:43:30 -0300 Subject: [PATCH 1/5] fix: AddonRuntimeLifecycleService premature dependency check, update settings and wiki --- .agent/workflows/wiki-sync.md | 4 ++++ WIKI/MultiBlockEngine.wiki | 1 + .../service/addon/AddonRuntimeLifecycleService.java | 3 ++- settings.gradle | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) create mode 160000 WIKI/MultiBlockEngine.wiki diff --git a/.agent/workflows/wiki-sync.md b/.agent/workflows/wiki-sync.md index bdca89c..8899d4a 100644 --- a/.agent/workflows/wiki-sync.md +++ b/.agent/workflows/wiki-sync.md @@ -1,3 +1,7 @@ +--- +description: Synchronizes the wiki information (`.\WIKI\MultiBlockEngine.wiki`) with the codebase by comparing the Git history. +--- + # Wiki Sync Workflow Synchronizes the wiki information (`.\WIKI\MultiBlockEngine.wiki`) with the codebase by comparing the Git history. diff --git a/WIKI/MultiBlockEngine.wiki b/WIKI/MultiBlockEngine.wiki new file mode 160000 index 0000000..c966a5c --- /dev/null +++ b/WIKI/MultiBlockEngine.wiki @@ -0,0 +1 @@ +Subproject commit c966a5c8390eec4c49b87da68bd1a25b05ffdf21 diff --git a/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/AddonRuntimeLifecycleService.java b/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/AddonRuntimeLifecycleService.java index 5ec5f31..3155325 100644 --- a/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/AddonRuntimeLifecycleService.java +++ b/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/AddonRuntimeLifecycleService.java @@ -73,7 +73,8 @@ public AddonRuntimeLifecycleService(MultiBlockEngine plugin, MultiblockAPI api, private String missingRequiredEnabledDependencies(AddonMetadata meta) { for (String req : meta.requiredDependencies().keySet()) { - if (registry.states.getOrDefault(req, AddonState.DISABLED) != AddonState.ENABLED) { + AddonState state = registry.states.getOrDefault(req, AddonState.DISABLED); + if (state != AddonState.ENABLED && state != AddonState.LOADED) { return req; } } diff --git a/settings.gradle b/settings.gradle index 70086e6..fed0b01 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ - rootProject.name = 'MultiBlockEngine' include 'core' include 'api' +include 'addons:mbe-ui', 'addons:mbe-wiring', 'addons:mbe-crafting', 'addons:mbe-electrics' include 'platform-bukkit' From c85bd666797a058152f233a40652d630484c6a8e Mon Sep 17 00:00:00 2001 From: DarkBladeDev Date: Sat, 25 Jul 2026 05:49:35 -0300 Subject: [PATCH 2/5] fix: add codemc repo to subprojects --- .gitmodules | 3 +++ build.gradle | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/.gitmodules b/.gitmodules index 7f8fa21..eeac2ae 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,9 @@ [submodule "addons/mbe-crafting"] path = addons/mbe-crafting url = git@github.com:Parallax-Development/MBE-Crafting.git +[submodule "addons/mbe-electrics"] + path = addons/mbe-electrics + url = git@github.com:Parallax-Development/MBE-Electrics.git [submodule "addons/mbe-ui"] path = addons/mbe-ui url = git@github.com:Parallax-Development/MBE-UI.git diff --git a/build.gradle b/build.gradle index 558d4e9..e24e1b2 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,14 @@ subprojects { maven { url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/' } + maven { + name = 'codemc-releases' + url = 'https://repo.codemc.io/repository/maven-releases/' + } + maven { + name = 'codemc-snapshots' + url = 'https://repo.codemc.io/repository/maven-snapshots/' + } } } From 930c8102f5a6d82e73a28eb4c4cc17306263f9c2 Mon Sep 17 00:00:00 2001 From: DarkBladeDev Date: Sat, 25 Jul 2026 17:16:30 -0300 Subject: [PATCH 3/5] feat(core): implement addon multiblock directory injection system --- .../dev/darkblade/mbe/api/addon/AddonContext.java | 3 +++ .../java/dev/darkblade/mbe/core/MultiBlockEngine.java | 6 ++++++ .../core/application/command/MBECommandManager.java | 10 ++++++++++ .../service/addon/AddonLifecycleService.java | 10 ++++++++++ .../service/addon/AddonRuntimeLifecycleService.java | 2 +- .../application/service/addon/SimpleAddonContext.java | 11 +++++++++++ .../application/service/addon/domain/LoadedAddon.java | 3 ++- 7 files changed, 43 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/dev/darkblade/mbe/api/addon/AddonContext.java b/api/src/main/java/dev/darkblade/mbe/api/addon/AddonContext.java index 1083d23..f426751 100644 --- a/api/src/main/java/dev/darkblade/mbe/api/addon/AddonContext.java +++ b/api/src/main/java/dev/darkblade/mbe/api/addon/AddonContext.java @@ -30,6 +30,9 @@ public interface AddonContext { MultiblockAPI getAPI(); Path getDataFolder(); + + void setMultiblockDirectory(Path folder); + Path getMultiblockDirectory(); void registerService(Class serviceType, T service); T getService(Class serviceType); diff --git a/core/src/main/java/dev/darkblade/mbe/core/MultiBlockEngine.java b/core/src/main/java/dev/darkblade/mbe/core/MultiBlockEngine.java index 07fc3ed..d6d3711 100644 --- a/core/src/main/java/dev/darkblade/mbe/core/MultiBlockEngine.java +++ b/core/src/main/java/dev/darkblade/mbe/core/MultiBlockEngine.java @@ -649,6 +649,12 @@ public void onEnable() { // Load definitions log.setCorePhase(LogPhase.LOAD); List loadedTypes = parser.loadAllWithSources(multiblockDir); + for (java.nio.file.Path addonDir : addonManager.getAddonMultiblockDirectories()) { + if (java.nio.file.Files.exists(addonDir)) { + loadedTypes.addAll(parser.loadAllWithSources(addonDir.toFile())); + } + } + List types = new ArrayList<>(loadedTypes.size()); for (MultiblockParser.LoadedType loaded : loadedTypes) { if (loaded == null || loaded.type() == null) { diff --git a/core/src/main/java/dev/darkblade/mbe/core/application/command/MBECommandManager.java b/core/src/main/java/dev/darkblade/mbe/core/application/command/MBECommandManager.java index 2cf02e8..1880109 100644 --- a/core/src/main/java/dev/darkblade/mbe/core/application/command/MBECommandManager.java +++ b/core/src/main/java/dev/darkblade/mbe/core/application/command/MBECommandManager.java @@ -22,6 +22,16 @@ public MBECommandManager(Plugin owningPlugin) { ) ); + this.parameterInjectorRegistry().registerInjector( + CommandSender.class, + (context, annotations) -> context.sender().getSender() + ); + + this.parameterInjectorRegistry().registerInjector( + org.bukkit.entity.Player.class, + (context, annotations) -> context.sender().getPlayer() + ); + this.annotationParser = new org.incendo.cloud.annotations.AnnotationParser<>( this, MBESender.class diff --git a/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/AddonLifecycleService.java b/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/AddonLifecycleService.java index 256d4d9..9ab3cea 100644 --- a/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/AddonLifecycleService.java +++ b/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/AddonLifecycleService.java @@ -211,6 +211,16 @@ public List listLoadedAddons() { } } + public List getAddonMultiblockDirectories() { + List dirs = new ArrayList<>(); + for (LoadedAddon loaded : registry.loadedAddons.values()) { + if (loaded.context() != null && loaded.context().getMultiblockDirectory() != null) { + dirs.add(loaded.context().getMultiblockDirectory()); + } + } + return dirs; + } + public List getAddonInfoList() { List result = new ArrayList<>(); List allIds = new ArrayList<>(registry.states.keySet()); diff --git a/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/AddonRuntimeLifecycleService.java b/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/AddonRuntimeLifecycleService.java index 3155325..15d72c0 100644 --- a/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/AddonRuntimeLifecycleService.java +++ b/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/AddonRuntimeLifecycleService.java @@ -346,7 +346,7 @@ public void loadAddon(DiscoveredAddon discovered) throws IOException { return; } - registry.loadedAddons.put(addonId, new LoadedAddon(metadata, addon, loader, addonLogger, phaseRef, dataFolder)); + registry.loadedAddons.put(addonId, new LoadedAddon(metadata, addon, loader, addonLogger, phaseRef, dataFolder, context)); registry.states.put(addonId, AddonState.LOADED); addonLogger.withPhase(LogPhase.LOAD).info("Loaded", LogKv.kv("version", metadata.version().toString())); } diff --git a/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/SimpleAddonContext.java b/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/SimpleAddonContext.java index a8aa3e1..047dea1 100644 --- a/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/SimpleAddonContext.java +++ b/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/SimpleAddonContext.java @@ -41,6 +41,7 @@ public class SimpleAddonContext implements AddonContext { private final AddonServiceRegistry services; private final ServiceLifecycleOrchestrator serviceLifecycleManager; private final ClassLoader classLoader; + private Path multiblockDirectory; public SimpleAddonContext( String addonId, @@ -95,6 +96,16 @@ public Path getDataFolder() { return dataFolder; } + @Override + public void setMultiblockDirectory(Path folder) { + this.multiblockDirectory = folder; + } + + @Override + public Path getMultiblockDirectory() { + return multiblockDirectory; + } + @Override public void registerService(Class serviceType, T service) { services.register(addonId, serviceType, service); diff --git a/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/domain/LoadedAddon.java b/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/domain/LoadedAddon.java index 9c1f879..cce2a6f 100644 --- a/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/domain/LoadedAddon.java +++ b/core/src/main/java/dev/darkblade/mbe/core/application/service/addon/domain/LoadedAddon.java @@ -14,4 +14,5 @@ public record LoadedAddon( dev.darkblade.mbe.core.application.service.addon.AddonClassLoader classLoader, AddonLogger logger, AtomicReference phase, - Path dataFolder) {} + Path dataFolder, + dev.darkblade.mbe.api.addon.AddonContext context) {} From 4d1feed939495b2cba880106494b870f6bc82dac Mon Sep 17 00:00:00 2001 From: DarkBladeDev Date: Sun, 26 Jul 2026 02:02:55 -0300 Subject: [PATCH 4/5] Update blueprint and preview systems, and UI services --- .../mbe/api/ui/PanelDirectoryRegistry.java | 18 ++++++++ .../mbe/api/ui/PanelMappingRegistry.java | 25 +++++++++++ .../mbe/preview/DisplayEntityRenderer.java | 10 +++++ .../darkblade/mbe/preview/PreviewSession.java | 22 ++++++++++ .../dev/darkblade/mbe/preview/Rotation.java | 16 ++++++++ .../mbe/blueprint/BlueprintController.java | 41 +++++++++++++++++++ .../mbe/blueprint/BlueprintInputListener.java | 3 ++ .../mbe/blueprint/BlueprintItem.java | 8 +++- .../blueprint/PreviewPlacementController.java | 19 +++++++++ .../command/blueprint/BlueprintCommand.java | 3 +- .../service/ui/PanelViewServiceImpl.java | 3 +- .../preview/StructurePreviewServiceImpl.java | 34 +++++++++++++-- .../addon/AddonDependencyResolverTest.java | 18 +++++--- .../service/tick/TickServiceTest.java | 10 ++--- .../service/tool/ToolSessionServiceTest.java | 2 +- .../bridge/item/PdcItemStackBridge.java | 40 ++++++++++++++---- .../bridge/PacketEventsRendererBridge.java | 7 +++- 17 files changed, 253 insertions(+), 26 deletions(-) create mode 100644 api/src/main/java/dev/darkblade/mbe/api/ui/PanelDirectoryRegistry.java create mode 100644 api/src/main/java/dev/darkblade/mbe/api/ui/PanelMappingRegistry.java diff --git a/api/src/main/java/dev/darkblade/mbe/api/ui/PanelDirectoryRegistry.java b/api/src/main/java/dev/darkblade/mbe/api/ui/PanelDirectoryRegistry.java new file mode 100644 index 0000000..1558be3 --- /dev/null +++ b/api/src/main/java/dev/darkblade/mbe/api/ui/PanelDirectoryRegistry.java @@ -0,0 +1,18 @@ +package dev.darkblade.mbe.api.ui; + +import java.nio.file.Path; + +/** + * Service registry that allows addons to register external directories + * containing UI panel configurations. + * + * The UI system will iterate through these directories during its load cycle. + */ +public interface PanelDirectoryRegistry { + /** + * Registers an external directory for panel configuration scanning. + * + * @param directory The path to the directory containing panel YAMLs. + */ + void registerDirectory(Path directory); +} diff --git a/api/src/main/java/dev/darkblade/mbe/api/ui/PanelMappingRegistry.java b/api/src/main/java/dev/darkblade/mbe/api/ui/PanelMappingRegistry.java new file mode 100644 index 0000000..acb841a --- /dev/null +++ b/api/src/main/java/dev/darkblade/mbe/api/ui/PanelMappingRegistry.java @@ -0,0 +1,25 @@ +package dev.darkblade.mbe.api.ui; + +import java.util.Optional; + +/** + * Service registry that allows addons to programmatically declare which + * UI panel should open for a specific multiblock type. + */ +public interface PanelMappingRegistry { + /** + * Registers a mapping between a multiblock ID and a panel ID. + * + * @param multiblockId The fully qualified multiblock ID (e.g. "mbe-electrics:coal_generator") + * @param panelId The target panel ID (e.g. "coal_generator") + */ + void registerMapping(String multiblockId, String panelId); + + /** + * Retrieves the panel ID mapped to the given multiblock ID, if any. + * + * @param multiblockId The fully qualified multiblock ID + * @return An Optional containing the panel ID if mapped + */ + Optional getMapping(String multiblockId); +} diff --git a/api/src/main/java/dev/darkblade/mbe/preview/DisplayEntityRenderer.java b/api/src/main/java/dev/darkblade/mbe/preview/DisplayEntityRenderer.java index 4a5b953..4ee00a3 100644 --- a/api/src/main/java/dev/darkblade/mbe/preview/DisplayEntityRenderer.java +++ b/api/src/main/java/dev/darkblade/mbe/preview/DisplayEntityRenderer.java @@ -8,6 +8,16 @@ public interface DisplayEntityRenderer { int spawnBlockDisplay(Player player, Location location, BlockData blockData); + + default int spawnBlockDisplay(Player player, Location location, BlockData blockData, float tx, float ty, float tz, float sx, float sy, float sz) { + return spawnBlockDisplay(player, location, blockData); + } + void updateBlockDisplay(int entityId, BlockData blockData); + + default void highlightError(Player player, int entityId) { + // Fallback does nothing + } + void destroyEntities(Player player, Collection entityIds); } diff --git a/api/src/main/java/dev/darkblade/mbe/preview/PreviewSession.java b/api/src/main/java/dev/darkblade/mbe/preview/PreviewSession.java index f573ca6..01e31a9 100644 --- a/api/src/main/java/dev/darkblade/mbe/preview/PreviewSession.java +++ b/api/src/main/java/dev/darkblade/mbe/preview/PreviewSession.java @@ -17,6 +17,8 @@ public final class PreviewSession { private volatile Rotation rotation; private volatile PreviewState state; private volatile Instant lastTouchedAt; + private volatile int currentLayer; + private volatile Vector3i nudgeOffset; public PreviewSession(UUID playerId, MultiblockDefinition definition, Location origin, Rotation rotation) { this.playerId = playerId; @@ -24,6 +26,8 @@ public PreviewSession(UUID playerId, MultiblockDefinition definition, Location o this.origin = origin; this.rotation = rotation == null ? Rotation.NORTH : rotation; this.state = PreviewState.MOVING; + this.currentLayer = Integer.MAX_VALUE; + this.nudgeOffset = new Vector3i(0, 0, 0); this.blocks = new ConcurrentHashMap<>(); this.renderVersion = new AtomicLong(0L); this.lastTouchedAt = Instant.now(); @@ -124,6 +128,24 @@ public Instant lastTouchedAt() { return lastTouchedAt; } + public int currentLayer() { + return currentLayer; + } + + public void currentLayer(int layer) { + this.currentLayer = layer; + touch(); + } + + public Vector3i nudgeOffset() { + return nudgeOffset; + } + + public void nudgeOffset(Vector3i offset) { + this.nudgeOffset = offset == null ? new Vector3i(0, 0, 0) : offset; + touch(); + } + public void touch() { this.lastTouchedAt = Instant.now(); } diff --git a/api/src/main/java/dev/darkblade/mbe/preview/Rotation.java b/api/src/main/java/dev/darkblade/mbe/preview/Rotation.java index 7608e88..aa5881f 100644 --- a/api/src/main/java/dev/darkblade/mbe/preview/Rotation.java +++ b/api/src/main/java/dev/darkblade/mbe/preview/Rotation.java @@ -14,4 +14,20 @@ public Rotation nextClockwise() { case WEST -> NORTH; }; } + + public static Rotation fromYaw(float yaw) { + yaw = yaw % 360; + if (yaw < 0) { + yaw += 360; + } + if (yaw >= 45 && yaw < 135) { + return WEST; + } else if (yaw >= 135 && yaw < 225) { + return NORTH; + } else if (yaw >= 225 && yaw < 315) { + return EAST; + } else { + return SOUTH; + } + } } diff --git a/core/src/main/java/dev/darkblade/mbe/blueprint/BlueprintController.java b/core/src/main/java/dev/darkblade/mbe/blueprint/BlueprintController.java index f72d989..aa1885b 100644 --- a/core/src/main/java/dev/darkblade/mbe/blueprint/BlueprintController.java +++ b/core/src/main/java/dev/darkblade/mbe/blueprint/BlueprintController.java @@ -43,6 +43,10 @@ public BlueprintController( this.eventBus = Objects.requireNonNull(eventBus, "eventBus"); } + public boolean isBlueprint(org.bukkit.inventory.ItemStack item) { + return heldItemResolver.blueprintId(item).isPresent(); + } + public boolean handleInput(Player player) { if (player == null) { return false; @@ -104,6 +108,43 @@ public boolean handleRotation(Player player) { return true; } + public boolean handleLayerChange(Player player, int delta) { + if (player == null) { + return false; + } + PlayerBuildContext context = contextService.get(player); + PreviewSession session = context.preview(); + if (session == null) { + return false; + } + int layer = session.currentLayer(); + if (layer == Integer.MAX_VALUE && delta < 0) { + layer = 0; // If starting to go down, start at 0 (or find the max Y of the definition). + // Let's just do a simple increment/decrement from current if it's max. + // Actually, if it's MAX_VALUE, let's figure out max Y. + if (session.definition() != null) { + layer = 0; + for (dev.darkblade.mbe.preview.PreviewBlock block : session.definition().blocks()) { + if (block.localPosition().y() > layer) { + layer = block.localPosition().y(); + } + } + } + } + + int newLayer = layer + delta; + if (newLayer < 0) newLayer = 0; + if (newLayer > 256) newLayer = Integer.MAX_VALUE; // Reset to all + + if (newLayer != session.currentLayer()) { + session.currentLayer(newLayer); + previewService.updatePreviewOrigin(player, session.origin()); // Forces a re-render + previewService.touch(player); + return true; + } + return false; + } + public boolean handleHeldItem(Player player) { if (player == null) { return false; diff --git a/core/src/main/java/dev/darkblade/mbe/blueprint/BlueprintInputListener.java b/core/src/main/java/dev/darkblade/mbe/blueprint/BlueprintInputListener.java index 1040624..e07283d 100644 --- a/core/src/main/java/dev/darkblade/mbe/blueprint/BlueprintInputListener.java +++ b/core/src/main/java/dev/darkblade/mbe/blueprint/BlueprintInputListener.java @@ -31,6 +31,9 @@ public void onInteract(PlayerInteractEvent event) { return; } Player player = event.getPlayer(); + if (!controller.isBlueprint(event.getItem())) { + return; + } if (!controller.handleInput(player)) { return; } diff --git a/core/src/main/java/dev/darkblade/mbe/blueprint/BlueprintItem.java b/core/src/main/java/dev/darkblade/mbe/blueprint/BlueprintItem.java index 74b6716..9f1d2f5 100644 --- a/core/src/main/java/dev/darkblade/mbe/blueprint/BlueprintItem.java +++ b/core/src/main/java/dev/darkblade/mbe/blueprint/BlueprintItem.java @@ -29,7 +29,13 @@ public static ItemStack create(ItemService itemService, ItemStackBridge bridge, if (itemService == null || bridge == null || definition == null || definition.id() == null || definition.id().isBlank()) { return null; } - ItemInstance instance = itemService.factory().create(BLUEPRINT_KEY, Map.of(DATA_STRUCTURE_ID, definition.id())); + java.util.Map data = new java.util.HashMap<>(); + data.put(DATA_STRUCTURE_ID, definition.id()); + data.put("multiblock_display_name", definition.id()); + data.put("count", 0); + data.put("total", definition.blocks() != null ? definition.blocks().size() : 0); + + ItemInstance instance = itemService.factory().create(BLUEPRINT_KEY, data); ItemStack stack = bridge.toItemStack(instance, sender); ItemMeta meta = stack.getItemMeta(); if (meta != null) { diff --git a/core/src/main/java/dev/darkblade/mbe/blueprint/PreviewPlacementController.java b/core/src/main/java/dev/darkblade/mbe/blueprint/PreviewPlacementController.java index 96b53df..c7e7e9e 100644 --- a/core/src/main/java/dev/darkblade/mbe/blueprint/PreviewPlacementController.java +++ b/core/src/main/java/dev/darkblade/mbe/blueprint/PreviewPlacementController.java @@ -23,4 +23,23 @@ public void onPlayerMove(PlayerMoveEvent event) { } controller.updatePreviewOnMove(player); } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onItemSwitch(org.bukkit.event.player.PlayerItemHeldEvent event) { + Player player = event.getPlayer(); + if (player.isSneaking()) { + int previous = event.getPreviousSlot(); + int current = event.getNewSlot(); + // Calculate scroll direction. + // If going from 0 to 8, they scrolled left (decrement) + // If going from 8 to 0, they scrolled right (increment) + int delta = current - previous; + if (previous == 0 && current == 8) delta = -1; + if (previous == 8 && current == 0) delta = 1; + + if (controller.handleLayerChange(player, delta)) { + event.setCancelled(true); + } + } + } } diff --git a/core/src/main/java/dev/darkblade/mbe/core/application/command/blueprint/BlueprintCommand.java b/core/src/main/java/dev/darkblade/mbe/core/application/command/blueprint/BlueprintCommand.java index 6c33d07..074c79c 100644 --- a/core/src/main/java/dev/darkblade/mbe/core/application/command/blueprint/BlueprintCommand.java +++ b/core/src/main/java/dev/darkblade/mbe/core/application/command/blueprint/BlueprintCommand.java @@ -80,7 +80,8 @@ public void list(MBESender mbeSender) { @Command("mbe blueprint give [target]") @Permission("multiblockengine.blueprint") - public void give(MBESender mbeSender, @Argument("id") String id, @Argument("target") Player targetArg) { + public void give(MBESender mbeSender, @Argument("id") org.bukkit.NamespacedKey idKey, @Argument("target") Player targetArg) { + String id = idKey.toString(); CommandSender sender = mbeSender.getSender(); Player receiver = targetArg; diff --git a/core/src/main/java/dev/darkblade/mbe/core/application/service/ui/PanelViewServiceImpl.java b/core/src/main/java/dev/darkblade/mbe/core/application/service/ui/PanelViewServiceImpl.java index b59dfed..84bbdd3 100644 --- a/core/src/main/java/dev/darkblade/mbe/core/application/service/ui/PanelViewServiceImpl.java +++ b/core/src/main/java/dev/darkblade/mbe/core/application/service/ui/PanelViewServiceImpl.java @@ -36,7 +36,8 @@ public String getServiceId() { @Override public void registerPanel(PanelId id, PanelDefinition panel) { - if (addonLifecycleService.getCurrentLifecyclePhase() != ServiceLifecycleOrchestrator.LifecyclePhase.CONTENT_REGISTRATION) { + ServiceLifecycleOrchestrator.LifecyclePhase phase = addonLifecycleService.getCurrentLifecyclePhase(); + if (phase != ServiceLifecycleOrchestrator.LifecyclePhase.CONTENT_REGISTRATION && phase != ServiceLifecycleOrchestrator.LifecyclePhase.RUNTIME) { throw new IllegalStateException("Panel registration outside allowed phase"); } registry.registerPanel(id, panel); diff --git a/core/src/main/java/dev/darkblade/mbe/preview/StructurePreviewServiceImpl.java b/core/src/main/java/dev/darkblade/mbe/preview/StructurePreviewServiceImpl.java index 9cebbf6..e2fa1e4 100644 --- a/core/src/main/java/dev/darkblade/mbe/preview/StructurePreviewServiceImpl.java +++ b/core/src/main/java/dev/darkblade/mbe/preview/StructurePreviewServiceImpl.java @@ -81,7 +81,8 @@ public PreviewSession startPreview(Player player, MultiblockDefinition definitio } destroyPreview(player); Location origin = resolveInitialOrigin(player); - PreviewSession session = new PreviewSession(player.getUniqueId(), definition, origin, Rotation.NORTH); + Rotation rotation = Rotation.fromYaw(player.getLocation().getYaw()); + PreviewSession session = new PreviewSession(player.getUniqueId(), definition, origin, rotation); session.state(PreviewState.MOVING); sessions.put(session); queueSpawn(player, session, session.currentRenderVersion()); @@ -115,7 +116,6 @@ public void updatePreviewOrigin(Player player, Location newOrigin) { rerender(player, session); } - @Override public void rotatePreview(Player player, Rotation rotation) { if (player == null) { return; @@ -128,6 +128,19 @@ public void rotatePreview(Player player, Rotation rotation) { rerender(player, session); } + public void nudgePreview(Player player, int dx, int dy, int dz) { + if (player == null) { + return; + } + PreviewSession session = sessions.get(player.getUniqueId()); + if (session == null) { + return; + } + Vector3i offset = session.nudgeOffset(); + session.nudgeOffset(new Vector3i(offset.x() + dx, offset.y() + dy, offset.z() + dz)); + rerender(player, session); + } + @Override public void destroyPreview(Player player) { destroyPreview(player, true); @@ -200,6 +213,7 @@ void handlePlacedBlock(Player player, BlockPosition position, BlockData placedBl return; } if (!matches(placedBlockData, previewBlock.expected())) { + renderer().highlightError(player, previewBlock.entityId()); return; } if (!session.markCompleted(position)) { @@ -232,8 +246,16 @@ private void queueSpawn(Player player, PreviewSession session, long renderVersio if (block == null || block.localPosition() == null || block.blockData() == null) { continue; } + if (block.localPosition().y() > session.currentLayer()) { + continue; + } Vector3i rotated = VectorUtils.rotate(block.localPosition(), session.rotation()); Location worldLocation = rotated.addTo(session.origin()); + + // Apply nudge offset + Vector3i nudge = session.nudgeOffset(); + worldLocation.add(nudge.x(), nudge.y(), nudge.z()); + if (validationStrategy.validate(worldLocation, block.blockData()) == PreviewBlockState.INVALID) { continue; } @@ -277,7 +299,13 @@ private void processQueue() { if (active.currentRenderVersion() != task.renderVersion()) { continue; } - int entityId = renderer().spawnBlockDisplay(player, task.worldLocation(), task.blockData()); + int entityId = renderer().spawnBlockDisplay( + player, + task.worldLocation(), + task.blockData(), + 0.25f, 0.25f, 0.25f, // Translation offset + 0.5f, 0.5f, 0.5f // Scale (50%) + ); if (entityId > 0) { SessionPreviewBlock previous = active.blocks().get(task.blockPosition()); if (previous != null && previous.completed()) { diff --git a/core/src/test/java/dev/darkblade/mbe/core/application/service/addon/AddonDependencyResolverTest.java b/core/src/test/java/dev/darkblade/mbe/core/application/service/addon/AddonDependencyResolverTest.java index 0d3daff..eab7a97 100644 --- a/core/src/test/java/dev/darkblade/mbe/core/application/service/addon/AddonDependencyResolverTest.java +++ b/core/src/test/java/dev/darkblade/mbe/core/application/service/addon/AddonDependencyResolverTest.java @@ -21,7 +21,8 @@ void resolve_ordersRequiredDependencies() { "com.example.B", Map.of(), Map.of(), - List.of() + List.of(), + "", "", List.of(), "", new AddonMetadata.Environment(null, null, Map.of()), List.of(), List.of(), List.of() ); AddonMetadata a = new AddonMetadata( @@ -31,7 +32,8 @@ void resolve_ordersRequiredDependencies() { "com.example.A", Map.of("b", Version.parse("1.0.0")), Map.of(), - List.of("b") + List.of("b"), + "", "", List.of(), "", new AddonMetadata.Environment(null, null, Map.of()), List.of(), List.of(), List.of() ); AddonDependencyResolver.Resolution res = resolver.resolve(1, Map.of( @@ -54,7 +56,8 @@ void resolve_reportsCycles() { "com.example.A", Map.of("b", Version.parse("1.0.0")), Map.of(), - List.of("b") + List.of("b"), + "", "", List.of(), "", new AddonMetadata.Environment(null, null, Map.of()), List.of(), List.of(), List.of() ); AddonMetadata b = new AddonMetadata( @@ -64,7 +67,8 @@ void resolve_reportsCycles() { "com.example.B", Map.of("a", Version.parse("1.0.0")), Map.of(), - List.of("a") + List.of("a"), + "", "", List.of(), "", new AddonMetadata.Environment(null, null, Map.of()), List.of(), List.of(), List.of() ); AddonDependencyResolver.Resolution res = resolver.resolve(1, Map.of( @@ -88,7 +92,8 @@ void resolve_appliesOptionalOrderingWhenPossible() { "com.example.B", Map.of(), Map.of(), - List.of() + List.of(), + "", "", List.of(), "", new AddonMetadata.Environment(null, null, Map.of()), List.of(), List.of(), List.of() ); AddonMetadata a = new AddonMetadata( @@ -98,7 +103,8 @@ void resolve_appliesOptionalOrderingWhenPossible() { "com.example.A", Map.of(), Map.of("b", Version.parse("1.0.0")), - List.of("b") + List.of("b"), + "", "", List.of(), "", new AddonMetadata.Environment(null, null, Map.of()), List.of(), List.of(), List.of() ); AddonDependencyResolver.Resolution res = resolver.resolve(1, Map.of( diff --git a/core/src/test/java/dev/darkblade/mbe/core/application/service/tick/TickServiceTest.java b/core/src/test/java/dev/darkblade/mbe/core/application/service/tick/TickServiceTest.java index 8dec81b..0b8368f 100644 --- a/core/src/test/java/dev/darkblade/mbe/core/application/service/tick/TickServiceTest.java +++ b/core/src/test/java/dev/darkblade/mbe/core/application/service/tick/TickServiceTest.java @@ -20,7 +20,7 @@ class TickServiceTest { @Test void executesTickablesWithDifferentIntervals() { - TickService service = new TickService(testLogger()); + TickServiceImpl service = new TickServiceImpl(testLogger()); CountingTickable everyTick = new CountingTickable(1); CountingTickable everyFive = new CountingTickable(5); CountingTickable everyTwenty = new CountingTickable(20); @@ -40,7 +40,7 @@ void executesTickablesWithDifferentIntervals() { @Test void isolatesFailuresWithoutBreakingGlobalLoop() { - TickService service = new TickService(testLogger()); + TickServiceImpl service = new TickServiceImpl(testLogger()); AtomicInteger faultyExecutions = new AtomicInteger(); Tickable faulty = new Tickable() { @Override @@ -63,7 +63,7 @@ public void tick() { @Test void preventsDuplicateRegistration() { - TickService service = new TickService(testLogger()); + TickServiceImpl service = new TickServiceImpl(testLogger()); CountingTickable tickable = new CountingTickable(1); assertTrue(service.register(tickable)); @@ -78,7 +78,7 @@ void preventsDuplicateRegistration() { @Test void defaultsInvalidIntervalsToOne() { - TickService service = new TickService(testLogger()); + TickServiceImpl service = new TickServiceImpl(testLogger()); CountingTickable invalid = new CountingTickable(0); service.register(invalid); @@ -91,7 +91,7 @@ void defaultsInvalidIntervalsToOne() { @Test void handlesStressWithManyTickables() { - TickService service = new TickService(testLogger()); + TickServiceImpl service = new TickServiceImpl(testLogger()); List tickables = new ArrayList<>(); for (int i = 1; i <= 300; i++) { int interval = (i % 20) + 1; diff --git a/core/src/test/java/dev/darkblade/mbe/core/application/service/tool/ToolSessionServiceTest.java b/core/src/test/java/dev/darkblade/mbe/core/application/service/tool/ToolSessionServiceTest.java index f7b0de8..6f4718f 100644 --- a/core/src/test/java/dev/darkblade/mbe/core/application/service/tool/ToolSessionServiceTest.java +++ b/core/src/test/java/dev/darkblade/mbe/core/application/service/tool/ToolSessionServiceTest.java @@ -14,7 +14,7 @@ class ToolSessionServiceTest { @Test void storesAndClearsSessionData() { - ToolSessionService service = new ToolSessionService(Duration.ofSeconds(10)); + ToolSessionServiceImpl service = new ToolSessionServiceImpl(Duration.ofSeconds(10)); UUID playerId = UUID.randomUUID(); service.put(playerId, "link_ports", Map.of("origin", "x")); diff --git a/platform-bukkit/src/main/java/dev/darkblade/mbe/core/infrastructure/bridge/item/PdcItemStackBridge.java b/platform-bukkit/src/main/java/dev/darkblade/mbe/core/infrastructure/bridge/item/PdcItemStackBridge.java index f0001fe..da5186b 100644 --- a/platform-bukkit/src/main/java/dev/darkblade/mbe/core/infrastructure/bridge/item/PdcItemStackBridge.java +++ b/platform-bukkit/src/main/java/dev/darkblade/mbe/core/infrastructure/bridge/item/PdcItemStackBridge.java @@ -73,12 +73,38 @@ public ItemStack toItemStack(ItemInstance instance, Locale locale) { ItemDefinition def = Objects.requireNonNull(instance.definition(), "instance.definition()"); ItemKey key = Objects.requireNonNull(def.key(), "definition.key()"); + if ("mbe:blueprint".equals(key.id().toString())) { + Map data = instance.data(); + if (data != null && data.get("mbe:multiblock") instanceof String mbId) { + if (!data.containsKey("multiblock_display_name")) data.put("multiblock_display_name", mbId); + if (!data.containsKey("count")) data.put("count", 0); + if (!data.containsKey("total")) { + int total = 0; + try { + MultiBlockEngine plugin = MultiBlockEngine.getInstance(); + if (plugin != null && plugin.getAddonLifecycleService() != null) { + dev.darkblade.mbe.catalog.StructureCatalogService catalog = plugin.getAddonLifecycleService().getCoreService(dev.darkblade.mbe.catalog.StructureCatalogService.class); + if (catalog != null) { + for (dev.darkblade.mbe.preview.MultiblockDefinition md : catalog.getAll()) { + if (mbId.equalsIgnoreCase(md.id())) { + total = md.blocks() != null ? md.blocks().size() : 0; + break; + } + } + } + } + } catch (Throwable ignored) {} + data.put("total", total); + } + } + } + Material material = resolveMaterial(def); ItemStack stack = new ItemStack(material); ItemMeta meta = stack.getItemMeta(); if (meta != null) { - String name = resolveText(def.displayName(), locale); + String name = resolveText(def.displayName(), locale, instance.data()); if (name != null && !name.isBlank()) { meta.setDisplayName(org.bukkit.ChatColor.translateAlternateColorCodes('&', name)); } @@ -86,7 +112,7 @@ public ItemStack toItemStack(ItemInstance instance, Locale locale) { Map props = def.properties(); if (props != null) { Object loreRaw = props.get("lore"); - List lore = parseLore(loreRaw, locale); + List lore = parseLore(loreRaw, locale, instance.data()); if (!lore.isEmpty()) { meta.setLore(lore); } @@ -200,12 +226,12 @@ private static long readDiskUsed(Map data) { return used; } - private List parseLore(Object raw, Locale locale) { + private List parseLore(Object raw, Locale locale, Map data) { if (raw == null) { return List.of(); } if (raw instanceof String s) { - String resolved = resolveText(s, locale); + String resolved = resolveText(s, locale, data); if (resolved == null || resolved.isBlank()) { return List.of(); } @@ -217,7 +243,7 @@ private List parseLore(Object raw, Locale locale) { if (o == null) { continue; } - String s = resolveText(String.valueOf(o), locale); + String s = resolveText(String.valueOf(o), locale, data); if (s == null || s.isBlank()) { continue; } @@ -252,7 +278,7 @@ private List parseFlags(Object raw) { return out; } - private String resolveText(String raw, Locale locale) { + private String resolveText(String raw, Locale locale, Map data) { if (raw == null || raw.isBlank()) { return raw; } @@ -277,7 +303,7 @@ private String resolveText(String raw, Locale locale) { try { Locale target = locale != null ? locale : (i18n.localeProvider() != null ? i18n.localeProvider().fallbackLocale() : Locale.US); - String translated = i18n.resolve(key, target); + String translated = i18n.resolve(key, target, data == null ? Map.of() : data); if (translated == null || translated.isBlank()) { return raw; } diff --git a/platform-bukkit/src/main/java/dev/darkblade/mbe/platform/bukkit/preview/bridge/PacketEventsRendererBridge.java b/platform-bukkit/src/main/java/dev/darkblade/mbe/platform/bukkit/preview/bridge/PacketEventsRendererBridge.java index c60f37b..f79e1e5 100644 --- a/platform-bukkit/src/main/java/dev/darkblade/mbe/platform/bukkit/preview/bridge/PacketEventsRendererBridge.java +++ b/platform-bukkit/src/main/java/dev/darkblade/mbe/platform/bukkit/preview/bridge/PacketEventsRendererBridge.java @@ -59,6 +59,11 @@ public BlockDisplayRenderer delegate() { @Override public int spawnBlockDisplay(Player player, Location location, BlockData blockData) { + return spawnBlockDisplay(player, location, blockData, 0F, 0F, 0F, 1F, 1F, 1F); + } + + @Override + public int spawnBlockDisplay(Player player, Location location, BlockData blockData, float tx, float ty, float tz, float sx, float sy, float sz) { if (player == null || location == null || blockData == null) { return -1; } @@ -73,7 +78,7 @@ public int spawnBlockDisplay(Player player, Location location, BlockData blockDa location.getY(), location.getZ(), blockState, - DisplayTransform.identity() + new DisplayTransform(tx, ty, tz, sx, sy, sz, 0, 0) ); DisplayEntityHandle handle = renderer.spawn(player, request); if (handle.isValid()) { From 03ac25e71a824bfed6147cd7c8c23dff46e62ce1 Mon Sep 17 00:00:00 2001 From: DarkBladeDev Date: Sun, 26 Jul 2026 12:40:34 -0300 Subject: [PATCH 5/5] chore: bump version to 4.9.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e946576..39c1e88 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.configuration-cache=true -projectVersion=4.8.5 +projectVersion=4.9.0