package com.hypixel.hytale.builtin.buildertools.imageimport; import com.hypixel.hytale.builtin.buildertools.BlockColorIndex; import com.hypixel.hytale.builtin.buildertools.BuilderToolsPlugin; import com.hypixel.hytale.codec.Codec; import com.hypixel.hytale.codec.KeyedCodec; import com.hypixel.hytale.codec.builder.BuilderCodec; import com.hypixel.hytale.common.util.StringUtil; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.math.vector.Vector3i; import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; import com.hypixel.hytale.protocol.packets.interface_.Page; import com.hypixel.hytale.protocol.packets.inventory.SetActiveSlot; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.entity.entities.Player; import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; import com.hypixel.hytale.server.core.inventory.Inventory; import com.hypixel.hytale.server.core.inventory.ItemStack; import com.hypixel.hytale.server.core.inventory.container.ItemContainer; import com.hypixel.hytale.server.core.modules.singleplayer.SingleplayerModule; import com.hypixel.hytale.server.core.prefab.selection.standard.BlockSelection; import com.hypixel.hytale.server.core.ui.DropdownEntryInfo; import com.hypixel.hytale.server.core.ui.LocalizableString; import com.hypixel.hytale.server.core.ui.browser.FileBrowserConfig; import com.hypixel.hytale.server.core.ui.browser.ServerFileBrowser; import com.hypixel.hytale.server.core.ui.builder.EventData; import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import java.awt.image.BufferedImage; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.imageio.ImageIO; public class ImageImportPage extends InteractiveCustomUIPage { private static final int DEFAULT_MAX_SIZE = 128; private static final int MIN_SIZE = 1; private static final int MAX_SIZE = 512; private static final String PASTE_TOOL_ID = "EditorTool_Paste"; private static final Path IMPORTS_DIR = Paths.get("imports", "images"); @Nonnull private String imagePath = ""; private int maxDimension = 128; @Nonnull private String orientationStr = "wall_xy"; @Nonnull private ImageImportPage.Orientation orientation = ImageImportPage.Orientation.VERTICAL_XY; @Nonnull private String originStr = "bottom_center"; @Nonnull private ImageImportPage.Origin origin = ImageImportPage.Origin.BOTTOM_CENTER; @Nullable private String statusMessage = null; private boolean isError = false; private boolean isProcessing = false; private boolean showBrowser = false; @Nonnull private final ServerFileBrowser browser; public ImageImportPage(@Nonnull PlayerRef playerRef) { super(playerRef, CustomPageLifetime.CanDismiss, ImageImportPage.PageData.CODEC); FileBrowserConfig config = FileBrowserConfig.builder() .listElementId("#BrowserPage #FileList") .searchInputId("#BrowserPage #SearchInput") .currentPathId("#BrowserPage #CurrentPath") .roots(List.of(new FileBrowserConfig.RootEntry("Imports", IMPORTS_DIR))) .allowedExtensions(".png", ".jpg", ".jpeg", ".gif", ".bmp") .enableRootSelector(false) .enableSearch(true) .enableDirectoryNav(true) .maxResults(50) .build(); try { Files.createDirectories(IMPORTS_DIR); } catch (IOException var4) { } this.browser = new ServerFileBrowser(config); } @Override public void build( @Nonnull Ref ref, @Nonnull UICommandBuilder commandBuilder, @Nonnull UIEventBuilder eventBuilder, @Nonnull Store store ) { commandBuilder.append("Pages/ImageImportPage.ui"); commandBuilder.set("#ImagePath #Input.Value", this.imagePath); commandBuilder.set("#MaxSizeInput #Input.Value", this.maxDimension); List orientationEntries = new ArrayList<>(); orientationEntries.add(new DropdownEntryInfo(LocalizableString.fromMessageId("server.customUI.imageImport.orientation.wall_xy"), "wall_xy")); orientationEntries.add(new DropdownEntryInfo(LocalizableString.fromMessageId("server.customUI.imageImport.orientation.wall_xz"), "wall_xz")); orientationEntries.add(new DropdownEntryInfo(LocalizableString.fromMessageId("server.customUI.imageImport.orientation.floor"), "floor")); commandBuilder.set("#OrientationInput #Input.Entries", orientationEntries); commandBuilder.set("#OrientationInput #Input.Value", this.orientationStr); List originEntries = new ArrayList<>(); originEntries.add(new DropdownEntryInfo(LocalizableString.fromMessageId("server.customUI.origin.bottom_front_left"), "bottom_front_left")); originEntries.add(new DropdownEntryInfo(LocalizableString.fromMessageId("server.customUI.origin.bottom_center"), "bottom_center")); originEntries.add(new DropdownEntryInfo(LocalizableString.fromMessageId("server.customUI.origin.center"), "center")); originEntries.add(new DropdownEntryInfo(LocalizableString.fromMessageId("server.customUI.origin.top_center"), "top_center")); commandBuilder.set("#OriginInput #Input.Entries", originEntries); commandBuilder.set("#OriginInput #Input.Value", this.originStr); this.updateStatus(commandBuilder); eventBuilder.addEventBinding(CustomUIEventBindingType.ValueChanged, "#ImagePath #Input", EventData.of("@ImagePath", "#ImagePath #Input.Value"), false); eventBuilder.addEventBinding(CustomUIEventBindingType.ValueChanged, "#MaxSizeInput #Input", EventData.of("@MaxSize", "#MaxSizeInput #Input.Value"), false); eventBuilder.addEventBinding( CustomUIEventBindingType.ValueChanged, "#OrientationInput #Input", EventData.of("@Orientation", "#OrientationInput #Input.Value"), false ); eventBuilder.addEventBinding(CustomUIEventBindingType.ValueChanged, "#OriginInput #Input", EventData.of("@Origin", "#OriginInput #Input.Value"), false); eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#ImportButton", EventData.of("Import", "true")); eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#ImagePath #BrowseButton", EventData.of("Browse", "true")); commandBuilder.set("#FormContainer.Visible", !this.showBrowser); commandBuilder.set("#BrowserPage.Visible", this.showBrowser); if (this.showBrowser) { this.buildBrowserPage(commandBuilder, eventBuilder); } } private void buildBrowserPage(@Nonnull UICommandBuilder commandBuilder, @Nonnull UIEventBuilder eventBuilder) { this.browser.buildSearchInput(commandBuilder, eventBuilder); this.browser.buildCurrentPath(commandBuilder); this.browser.buildFileList(commandBuilder, eventBuilder); eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#BrowserPage #SelectButton", EventData.of("BrowserSelect", "true")); eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#BrowserPage #CancelButton", EventData.of("BrowserCancel", "true")); } private void updateStatus(@Nonnull UICommandBuilder commandBuilder) { if (this.statusMessage != null) { commandBuilder.set("#StatusText.Text", this.statusMessage); commandBuilder.set("#StatusText.Visible", true); commandBuilder.set("#StatusText.Style.TextColor", this.isError ? "#e74c3c" : "#cfd8e3"); } else { commandBuilder.set("#StatusText.Visible", false); } } private void setError(@Nonnull String message) { this.statusMessage = message; this.isError = true; this.isProcessing = false; this.rebuild(); } private void setStatus(@Nonnull String message) { this.statusMessage = message; this.isError = false; this.rebuild(); } public void handleDataEvent(@Nonnull Ref ref, @Nonnull Store store, @Nonnull ImageImportPage.PageData data) { boolean needsUpdate = false; if (data.browse != null && data.browse) { this.showBrowser = true; this.rebuild(); } else if (data.browserCancel != null && data.browserCancel) { this.showBrowser = false; this.rebuild(); } else if (data.browserSelect != null && data.browserSelect) { if (!this.browser.getSelectedItems().isEmpty()) { String selectedPath = this.browser.getSelectedItems().iterator().next(); this.imagePath = this.browser.getRoot().resolve(selectedPath).toString(); } this.showBrowser = false; this.rebuild(); } else { if (this.showBrowser && (data.file != null || data.searchQuery != null || data.searchResult != null)) { boolean handled = false; if (data.searchQuery != null) { this.browser.setSearchQuery(data.searchQuery.trim().toLowerCase()); handled = true; } if (data.file != null) { String fileName = data.file; if ("..".equals(fileName)) { this.browser.navigateUp(); handled = true; } else { Path targetPath = this.browser.resolveFromCurrent(fileName); if (targetPath != null && Files.isDirectory(targetPath)) { this.browser.navigateTo(Paths.get(fileName)); handled = true; } else if (targetPath != null && Files.isRegularFile(targetPath)) { this.imagePath = targetPath.toString(); this.showBrowser = false; this.rebuild(); return; } } } if (data.searchResult != null) { Path resolvedPath = this.browser.resolveSecure(data.searchResult); if (resolvedPath != null && Files.isRegularFile(resolvedPath)) { this.imagePath = resolvedPath.toString(); this.showBrowser = false; this.rebuild(); return; } } if (handled) { UICommandBuilder commandBuilder = new UICommandBuilder(); UIEventBuilder eventBuilder = new UIEventBuilder(); this.browser.buildFileList(commandBuilder, eventBuilder); this.browser.buildCurrentPath(commandBuilder); this.sendUpdate(commandBuilder, eventBuilder, false); return; } } if (data.imagePath != null) { this.imagePath = StringUtil.stripQuotes(data.imagePath.trim()); this.statusMessage = null; needsUpdate = true; } if (data.maxSize != null) { this.maxDimension = Math.max(1, Math.min(512, data.maxSize)); needsUpdate = true; } if (data.orientation != null) { this.orientationStr = data.orientation.trim().toLowerCase(); String var8 = this.orientationStr; this.orientation = switch (var8) { case "wall_xz", "xz", "vertical_xz" -> ImageImportPage.Orientation.VERTICAL_XZ; case "floor", "horizontal", "horizontal_xz" -> ImageImportPage.Orientation.HORIZONTAL_XZ; default -> ImageImportPage.Orientation.VERTICAL_XY; }; needsUpdate = true; } if (data.origin != null) { this.originStr = data.origin.trim().toLowerCase(); String var9 = this.originStr; this.origin = switch (var9) { case "bottom_front_left" -> ImageImportPage.Origin.BOTTOM_FRONT_LEFT; case "center" -> ImageImportPage.Origin.CENTER; case "top_center" -> ImageImportPage.Origin.TOP_CENTER; default -> ImageImportPage.Origin.BOTTOM_CENTER; }; needsUpdate = true; } if (data.doImport != null && data.doImport && !this.isProcessing) { this.performImport(ref, store); } else { if (needsUpdate) { this.sendUpdate(); } } } } private void performImport(@Nonnull Ref ref, @Nonnull Store store) { if (this.imagePath.isEmpty()) { this.setError("Please enter a path to an image file"); } else { Path path = Paths.get(this.imagePath); if (!SingleplayerModule.isOwner(this.playerRef)) { Path normalizedPath = path.toAbsolutePath().normalize(); Path normalizedImports = IMPORTS_DIR.toAbsolutePath().normalize(); if (!normalizedPath.startsWith(normalizedImports)) { this.setError("Files must be in the server's imports/images directory"); return; } } if (!Files.exists(path)) { this.setError("File not found: " + this.imagePath); } else { this.isProcessing = true; this.setStatus("Processing..."); Player playerComponent = store.getComponent(ref, Player.getComponentType()); PlayerRef playerRefComponent = store.getComponent(ref, PlayerRef.getComponentType()); if (playerComponent != null && playerRefComponent != null) { String finalPath = this.imagePath; int finalMaxSize = this.maxDimension; ImageImportPage.Orientation finalOrientation = this.orientation; ImageImportPage.Origin finalOrigin = this.origin; BuilderToolsPlugin.addToQueue( playerComponent, playerRefComponent, (r, builderState, componentAccessor) -> { try { BufferedImage image = null; try { image = ImageIO.read(Paths.get(finalPath).toFile()); } catch (Exception var38) { } if (image == null) { this.setError("Unable to read image file (unsupported format or corrupted). Try PNG format."); return; } int width = image.getWidth(); int height = image.getHeight(); float scale = 1.0F; if (width > finalMaxSize || height > finalMaxSize) { scale = (float)finalMaxSize / Math.max(width, height); width = Math.round(width * scale); height = Math.round(height * scale); } BlockColorIndex colorIndex = BuilderToolsPlugin.get().getBlockColorIndex(); if (colorIndex.isEmpty()) { this.setError("Block color index not initialized"); return; } int sizeX; int sizeY; int sizeZ; switch (finalOrientation) { case VERTICAL_XY: sizeX = width; sizeY = height; sizeZ = 1; break; case VERTICAL_XZ: sizeX = width; sizeY = 1; sizeZ = height; break; case HORIZONTAL_XZ: sizeX = width; sizeY = 1; sizeZ = height; break; default: sizeX = width; sizeY = height; sizeZ = 1; } int offsetX = 0; int offsetY = 0; int offsetZ = 0; switch (finalOrigin) { case BOTTOM_FRONT_LEFT: default: break; case BOTTOM_CENTER: offsetX = -sizeX / 2; offsetZ = -sizeZ / 2; break; case CENTER: offsetX = -sizeX / 2; offsetY = -sizeY / 2; offsetZ = -sizeZ / 2; break; case TOP_CENTER: offsetX = -sizeX / 2; offsetY = -sizeY; offsetZ = -sizeZ / 2; } BlockSelection selection = new BlockSelection(width * height, 0); selection.setPosition(0, 0, 0); int blockCount = 0; float finalScale = scale; for (int imgY = 0; imgY < height; imgY++) { for (int imgX = 0; imgX < width; imgX++) { int srcX = Math.min((int)(imgX / finalScale), image.getWidth() - 1); int srcY = Math.min((int)(imgY / finalScale), image.getHeight() - 1); int rgba = image.getRGB(srcX, srcY); int alpha = rgba >> 24 & 0xFF; if (alpha >= 128) { int red = rgba >> 16 & 0xFF; int green = rgba >> 8 & 0xFF; int blue = rgba & 0xFF; int blockId = colorIndex.findClosestBlock(red, green, blue); if (blockId > 0) { int blockX; int blockY; int blockZ; switch (finalOrientation) { case VERTICAL_XY: blockX = imgX; blockY = height - 1 - imgY; blockZ = 0; break; case VERTICAL_XZ: blockX = imgX; blockY = 0; blockZ = height - 1 - imgY; break; case HORIZONTAL_XZ: blockX = imgX; blockY = 0; blockZ = imgY; break; default: blockX = imgX; blockY = height - 1 - imgY; blockZ = 0; } selection.addBlockAtLocalPos(blockX + offsetX, blockY + offsetY, blockZ + offsetZ, blockId, 0, 0, 0); blockCount++; } } } } selection.setSelectionArea( new Vector3i(offsetX, offsetY, offsetZ), new Vector3i(sizeX - 1 + offsetX, sizeY - 1 + offsetY, sizeZ - 1 + offsetZ) ); builderState.setSelection(selection); builderState.sendSelectionToClient(); this.statusMessage = String.format("Success! %d blocks copied to clipboard (%dx%dx%d)", blockCount, sizeX, sizeY, sizeZ); this.isProcessing = false; playerRefComponent.sendMessage( Message.translation("server.builderTools.imageImport.success") .param("count", blockCount) .param("width", sizeX) .param("height", sizeY) .param("depth", sizeZ) ); playerComponent.getPageManager().setPage(r, store, Page.None); this.switchToPasteTool(playerComponent, playerRefComponent); } catch (Exception var39) { ((HytaleLogger.Api)BuilderToolsPlugin.get().getLogger().at(Level.WARNING).withCause(var39)).log("Image import error"); this.setError("Error: " + var39.getMessage()); } } ); } else { this.setError("Player not found"); } } } } private void switchToPasteTool(@Nonnull Player playerComponent, @Nonnull PlayerRef playerRef) { Inventory inventory = playerComponent.getInventory(); ItemContainer hotbar = inventory.getHotbar(); ItemContainer storage = inventory.getStorage(); ItemContainer tools = inventory.getTools(); int hotbarSize = hotbar.getCapacity(); for (short slot = 0; slot < hotbarSize; slot++) { ItemStack itemStack = hotbar.getItemStack(slot); if (itemStack != null && !itemStack.isEmpty() && "EditorTool_Paste".equals(itemStack.getItemId())) { inventory.setActiveHotbarSlot((byte)slot); playerRef.getPacketHandler().writeNoCache(new SetActiveSlot(-1, (byte)slot)); return; } } short emptySlot = -1; for (short slotx = 0; slotx < hotbarSize; slotx++) { ItemStack itemStack = hotbar.getItemStack(slotx); if (itemStack == null || itemStack.isEmpty()) { emptySlot = slotx; break; } } if (emptySlot != -1) { for (short slotxx = 0; slotxx < storage.getCapacity(); slotxx++) { ItemStack itemStack = storage.getItemStack(slotxx); if (itemStack != null && !itemStack.isEmpty() && "EditorTool_Paste".equals(itemStack.getItemId())) { storage.moveItemStackFromSlotToSlot(slotxx, 1, hotbar, emptySlot); inventory.setActiveHotbarSlot((byte)emptySlot); playerRef.getPacketHandler().writeNoCache(new SetActiveSlot(-1, (byte)emptySlot)); return; } } ItemStack pasteToolStack = null; for (short slotxxx = 0; slotxxx < tools.getCapacity(); slotxxx++) { ItemStack itemStack = tools.getItemStack(slotxxx); if (itemStack != null && !itemStack.isEmpty() && "EditorTool_Paste".equals(itemStack.getItemId())) { pasteToolStack = itemStack; break; } } if (pasteToolStack != null) { hotbar.setItemStackForSlot(emptySlot, new ItemStack(pasteToolStack.getItemId())); inventory.setActiveHotbarSlot((byte)emptySlot); playerRef.getPacketHandler().writeNoCache(new SetActiveSlot(-1, (byte)emptySlot)); } } } public static enum Orientation { VERTICAL_XY, VERTICAL_XZ, HORIZONTAL_XZ; } public static enum Origin { BOTTOM_FRONT_LEFT, BOTTOM_CENTER, CENTER, TOP_CENTER; } public static class PageData { static final String KEY_IMAGE_PATH = "@ImagePath"; static final String KEY_MAX_SIZE = "@MaxSize"; static final String KEY_ORIENTATION = "@Orientation"; static final String KEY_ORIGIN = "@Origin"; static final String KEY_IMPORT = "Import"; static final String KEY_BROWSE = "Browse"; static final String KEY_BROWSER_SELECT = "BrowserSelect"; static final String KEY_BROWSER_CANCEL = "BrowserCancel"; public static final BuilderCodec CODEC = BuilderCodec.builder(ImageImportPage.PageData.class, ImageImportPage.PageData::new) .addField(new KeyedCodec<>("@ImagePath", Codec.STRING), (entry, s) -> entry.imagePath = s, entry -> entry.imagePath) .addField(new KeyedCodec<>("@MaxSize", Codec.INTEGER), (entry, i) -> entry.maxSize = i, entry -> entry.maxSize) .addField(new KeyedCodec<>("@Orientation", Codec.STRING), (entry, s) -> entry.orientation = s, entry -> entry.orientation) .addField(new KeyedCodec<>("@Origin", Codec.STRING), (entry, s) -> entry.origin = s, entry -> entry.origin) .addField( new KeyedCodec<>("Import", Codec.STRING), (entry, s) -> entry.doImport = "true".equalsIgnoreCase(s), entry -> entry.doImport != null && entry.doImport ? "true" : null ) .addField( new KeyedCodec<>("Browse", Codec.STRING), (entry, s) -> entry.browse = "true".equalsIgnoreCase(s), entry -> entry.browse != null && entry.browse ? "true" : null ) .addField( new KeyedCodec<>("BrowserSelect", Codec.STRING), (entry, s) -> entry.browserSelect = "true".equalsIgnoreCase(s), entry -> entry.browserSelect != null && entry.browserSelect ? "true" : null ) .addField( new KeyedCodec<>("BrowserCancel", Codec.STRING), (entry, s) -> entry.browserCancel = "true".equalsIgnoreCase(s), entry -> entry.browserCancel != null && entry.browserCancel ? "true" : null ) .addField(new KeyedCodec<>("File", Codec.STRING), (entry, s) -> entry.file = s, entry -> entry.file) .addField(new KeyedCodec<>("@SearchQuery", Codec.STRING), (entry, s) -> entry.searchQuery = s, entry -> entry.searchQuery) .addField(new KeyedCodec<>("SearchResult", Codec.STRING), (entry, s) -> entry.searchResult = s, entry -> entry.searchResult) .build(); @Nullable private String imagePath; @Nullable private Integer maxSize; @Nullable private String orientation; @Nullable private String origin; @Nullable private Boolean doImport; @Nullable private Boolean browse; @Nullable private Boolean browserSelect; @Nullable private Boolean browserCancel; @Nullable private String file; @Nullable private String searchQuery; @Nullable private String searchResult; } }