package com.hypixel.hytale.builtin.buildertools.objimport; 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.asset.type.blocktype.config.BlockType; 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.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Map.Entry; import java.util.logging.Level; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class ObjImportPage extends InteractiveCustomUIPage { private static final String DEFAULT_BLOCK = "Rock_Stone"; private static final int DEFAULT_HEIGHT = 20; private static final int MIN_HEIGHT = 1; private static final int MAX_HEIGHT = 320; private static final float DEFAULT_SCALE = 1.0F; private static final float MIN_SCALE = 0.01F; private static final float MAX_SCALE = 100.0F; private static final String PASTE_TOOL_ID = "EditorTool_Paste"; private static final Path IMPORTS_DIR = Paths.get("imports", "models"); @Nonnull private String objPath = ""; private int targetHeight = 20; private boolean useScaleMode = false; private float scale = 1.0F; @Nonnull private String blockPattern = "Rock_Stone"; private boolean fillSolid = true; private boolean useMaterials = true; private boolean autoDetectTextures = false; @Nonnull private String originStr = "bottom_center"; @Nonnull private ObjImportPage.Origin origin = ObjImportPage.Origin.BOTTOM_CENTER; @Nonnull private String rotationStr = "y_up"; @Nonnull private ObjImportPage.MeshRotation rotation = ObjImportPage.MeshRotation.NONE; @Nullable private String statusMessage = null; private boolean isError = false; private boolean isProcessing = false; private boolean showBrowser = false; @Nonnull private final ServerFileBrowser browser; private static final String[] AUTO_DETECT_SUFFIXES = new String[]{"", "_dif", "_diffuse"}; private static final String[] AUTO_DETECT_EXTENSIONS = new String[]{".png", ".jpg", ".jpeg"}; public ObjImportPage(@Nonnull PlayerRef playerRef) { super(playerRef, CustomPageLifetime.CanDismiss, ObjImportPage.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(".obj") .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/ObjImportPage.ui"); commandBuilder.set("#ObjPath #Input.Value", this.objPath); commandBuilder.set("#HeightInput #Input.Value", this.targetHeight); commandBuilder.set("#ScaleInput #Input.Value", this.scale); commandBuilder.set("#BlockPattern #Input.Value", this.blockPattern); commandBuilder.set("#FillModeCheckbox #CheckBox.Value", this.fillSolid); commandBuilder.set("#UseMaterialsCheckbox #CheckBox.Value", this.useMaterials); commandBuilder.set("#AutoDetectTexturesCheckbox #CheckBox.Value", this.autoDetectTextures); commandBuilder.set("#HeightInput.Visible", !this.useScaleMode); commandBuilder.set("#ScaleInput.Visible", this.useScaleMode); List sizeModeEntries = new ArrayList<>(); sizeModeEntries.add(new DropdownEntryInfo(LocalizableString.fromMessageId("server.customUI.objImport.sizeMode.height"), "height")); sizeModeEntries.add(new DropdownEntryInfo(LocalizableString.fromMessageId("server.customUI.objImport.sizeMode.scale"), "scale")); commandBuilder.set("#SizeModeInput #Input.Entries", sizeModeEntries); commandBuilder.set("#SizeModeInput #Input.Value", this.useScaleMode ? "scale" : "height"); 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); List axisEntries = new ArrayList<>(); axisEntries.add(new DropdownEntryInfo(LocalizableString.fromMessageId("server.customUI.objImport.axis.yUp"), "y_up")); axisEntries.add(new DropdownEntryInfo(LocalizableString.fromMessageId("server.customUI.objImport.axis.zUp"), "z_up")); axisEntries.add(new DropdownEntryInfo(LocalizableString.fromMessageId("server.customUI.objImport.axis.xUp"), "x_up")); commandBuilder.set("#RotationInput #Input.Entries", axisEntries); commandBuilder.set("#RotationInput #Input.Value", this.rotationStr); this.updateStatus(commandBuilder); eventBuilder.addEventBinding(CustomUIEventBindingType.ValueChanged, "#ObjPath #Input", EventData.of("@ObjPath", "#ObjPath #Input.Value"), false); eventBuilder.addEventBinding(CustomUIEventBindingType.ValueChanged, "#HeightInput #Input", EventData.of("@Height", "#HeightInput #Input.Value"), false); eventBuilder.addEventBinding(CustomUIEventBindingType.ValueChanged, "#ScaleInput #Input", EventData.of("@Scale", "#ScaleInput #Input.Value"), false); eventBuilder.addEventBinding( CustomUIEventBindingType.ValueChanged, "#SizeModeInput #Input", EventData.of("SizeMode", "#SizeModeInput #Input.Value"), false ); eventBuilder.addEventBinding( CustomUIEventBindingType.ValueChanged, "#BlockPattern #Input", EventData.of("@BlockPattern", "#BlockPattern #Input.Value"), false ); eventBuilder.addEventBinding( CustomUIEventBindingType.ValueChanged, "#FillModeCheckbox #CheckBox", EventData.of("@FillSolid", "#FillModeCheckbox #CheckBox.Value"), false ); eventBuilder.addEventBinding( CustomUIEventBindingType.ValueChanged, "#UseMaterialsCheckbox #CheckBox", EventData.of("@UseMaterials", "#UseMaterialsCheckbox #CheckBox.Value"), false ); eventBuilder.addEventBinding( CustomUIEventBindingType.ValueChanged, "#AutoDetectTexturesCheckbox #CheckBox", EventData.of("@AutoDetectTextures", "#AutoDetectTexturesCheckbox #CheckBox.Value"), false ); eventBuilder.addEventBinding(CustomUIEventBindingType.ValueChanged, "#OriginInput #Input", EventData.of("@Origin", "#OriginInput #Input.Value"), false); eventBuilder.addEventBinding( CustomUIEventBindingType.ValueChanged, "#RotationInput #Input", EventData.of("@Rotation", "#RotationInput #Input.Value"), false ); eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#ImportButton", EventData.of("Import", "true")); eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#ObjPath #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 ObjImportPage.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.objPath = 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.objPath = 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.objPath = 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.objPath != null) { this.objPath = StringUtil.stripQuotes(data.objPath.trim()); this.statusMessage = null; needsUpdate = true; } if (data.height != null) { this.targetHeight = Math.max(1, Math.min(320, data.height)); needsUpdate = true; } if (data.scale != null) { this.scale = Math.max(0.01F, Math.min(100.0F, data.scale)); needsUpdate = true; } if (data.sizeMode != null) { this.useScaleMode = "scale".equalsIgnoreCase(data.sizeMode); this.rebuild(); } else { if (data.blockPattern != null) { this.blockPattern = data.blockPattern.trim(); needsUpdate = true; } if (data.fillSolid != null) { this.fillSolid = data.fillSolid; needsUpdate = true; } if (data.useMaterials != null) { this.useMaterials = data.useMaterials; needsUpdate = true; } if (data.autoDetectTextures != null) { this.autoDetectTextures = data.autoDetectTextures; needsUpdate = true; } if (data.origin != null) { this.originStr = data.origin.trim().toLowerCase(); String var8 = this.originStr; this.origin = switch (var8) { case "bottom_front_left" -> ObjImportPage.Origin.BOTTOM_FRONT_LEFT; case "center" -> ObjImportPage.Origin.CENTER; case "top_center" -> ObjImportPage.Origin.TOP_CENTER; default -> ObjImportPage.Origin.BOTTOM_CENTER; }; needsUpdate = true; } if (data.rotation != null) { this.rotationStr = data.rotation.trim().toLowerCase(); String var9 = this.rotationStr; this.rotation = switch (var9) { case "z_up" -> ObjImportPage.MeshRotation.Z_UP_TO_Y_UP; case "x_up" -> ObjImportPage.MeshRotation.X_UP_TO_Y_UP; default -> ObjImportPage.MeshRotation.NONE; }; needsUpdate = true; } if (data.doImport != null && data.doImport && !this.isProcessing) { this.performImport(ref, store); } else { if (needsUpdate) { this.sendUpdate(); } } } } } @Nullable private List parseBlockPattern(@Nonnull String pattern) { List result = new ArrayList<>(); String[] parts = pattern.split(","); for (String part : parts) { part = part.trim(); if (!part.isEmpty()) { int weight = 100; String blockName = part; int pctIdx = part.indexOf(37); if (pctIdx > 0) { try { weight = Integer.parseInt(part.substring(0, pctIdx).trim()); blockName = part.substring(pctIdx + 1).trim(); } catch (NumberFormatException var12) { return null; } } int blockId = BlockType.getAssetMap().getIndex(blockName); if (blockId == Integer.MIN_VALUE) { return null; } result.add(new ObjImportPage.WeightedBlock(blockId, weight)); } } return result.isEmpty() ? null : result; } private int selectRandomBlock(@Nonnull List blocks, @Nonnull Random random) { if (blocks.isEmpty()) { throw new IllegalStateException("Cannot select from empty blocks list"); } else if (blocks.size() == 1) { return blocks.get(0).blockId; } else { int totalWeight = 0; for (ObjImportPage.WeightedBlock wb : blocks) { totalWeight += wb.weight; } if (totalWeight <= 0) { return blocks.get(0).blockId; } else { int roll = random.nextInt(totalWeight); int cumulative = 0; for (ObjImportPage.WeightedBlock wb : blocks) { cumulative += wb.weight; if (roll < cumulative) { return wb.blockId; } } return blocks.get(0).blockId; } } } private void performImport(@Nonnull Ref ref, @Nonnull Store store) { if (this.objPath.isEmpty()) { this.setError("Please enter a path to an OBJ file"); } else { Path path = Paths.get(this.objPath); 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/models directory"); return; } } if (!Files.exists(path)) { this.setError("File not found: " + this.objPath); } else if (!this.objPath.toLowerCase().endsWith(".obj")) { this.setError("File must be a .obj file"); } else { List blocks = this.parseBlockPattern(this.blockPattern); if (blocks == null) { this.setError("Invalid block pattern: " + this.blockPattern); } 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) { int finalHeight = this.targetHeight; boolean finalUseScaleMode = this.useScaleMode; float finalScale = this.scale; String finalPath = this.objPath; boolean finalFillSolid = this.fillSolid; boolean finalUseMaterials = this.useMaterials; boolean finalAutoDetectTextures = this.autoDetectTextures; ObjImportPage.Origin finalOrigin = this.origin; ObjImportPage.MeshRotation finalRotation = this.rotation; BuilderToolsPlugin.addToQueue( playerComponent, playerRefComponent, (r, builderState, componentAccessor) -> { try { Path objFilePath = Paths.get(finalPath); ObjParser.ObjMesh mesh = ObjParser.parse(objFilePath); switch (finalRotation) { case Z_UP_TO_Y_UP: mesh.transformZUpToYUp(); break; case X_UP_TO_Y_UP: mesh.transformXUpToYUp(); } int computedHeight; if (finalUseScaleMode) { float[] bounds = mesh.getBounds(); float meshHeight = bounds[4] - bounds[1]; computedHeight = Math.max(1, (int)Math.ceil(meshHeight * finalScale)); } else { computedHeight = finalHeight; } if (blocks.isEmpty()) { this.setError("No blocks available for import"); return; } BlockColorIndex colorIndex = BuilderToolsPlugin.get().getBlockColorIndex(); Map materialTextures = new HashMap<>(); Map materialToBlockId = new HashMap<>(); int defaultBlockId = blocks.get(0).blockId; if (finalUseMaterials && mesh.mtlLib() != null) { this.loadMaterialData(objFilePath, mesh, colorIndex, materialTextures, materialToBlockId, finalAutoDetectTextures); if (!materialToBlockId.isEmpty()) { defaultBlockId = materialToBlockId.values().iterator().next(); } } boolean hasUvTextures = mesh.hasUvCoordinates() && !materialTextures.isEmpty(); boolean preserveOrigin = finalOrigin == ObjImportPage.Origin.BOTTOM_FRONT_LEFT; MeshVoxelizer.VoxelResult result; if (hasUvTextures) { result = MeshVoxelizer.voxelize( mesh, computedHeight, finalFillSolid, materialTextures, materialToBlockId, colorIndex, defaultBlockId, preserveOrigin ); } else { result = MeshVoxelizer.voxelize( mesh, computedHeight, finalFillSolid, null, materialToBlockId, colorIndex, defaultBlockId, preserveOrigin ); } TextureSampler.clearCache(); int offsetX = 0; int offsetY = 0; int offsetZ = 0; switch (finalOrigin) { case BOTTOM_FRONT_LEFT: default: break; case BOTTOM_CENTER: offsetX = -result.sizeX() / 2; offsetZ = -result.sizeZ() / 2; break; case CENTER: offsetX = -result.sizeX() / 2; offsetY = -result.sizeY() / 2; offsetZ = -result.sizeZ() / 2; break; case TOP_CENTER: offsetX = -result.sizeX() / 2; offsetY = -result.sizeY(); offsetZ = -result.sizeZ() / 2; } BlockSelection selection = new BlockSelection(result.countSolid(), 0); selection.setPosition(0, 0, 0); Random random = new Random(); boolean hasMaterialBlockIds = result.blockIds() != null; for (int x = 0; x < result.sizeX(); x++) { for (int y = 0; y < result.sizeY(); y++) { for (int z = 0; z < result.sizeZ(); z++) { if (result.voxels()[x][y][z]) { int blockId; if (hasMaterialBlockIds) { blockId = result.getBlockId(x, y, z); if (blockId == 0) { blockId = this.selectRandomBlock(blocks, random); } } else { blockId = this.selectRandomBlock(blocks, random); } selection.addBlockAtLocalPos(x + offsetX, y + offsetY, z + offsetZ, blockId, 0, 0, 0); } } } } selection.setSelectionArea( new Vector3i(offsetX, offsetY, offsetZ), new Vector3i(result.sizeX() - 1 + offsetX, result.sizeY() - 1 + offsetY, result.sizeZ() - 1 + offsetZ) ); builderState.setSelection(selection); builderState.sendSelectionToClient(); int blockCount = result.countSolid(); String textureInfo = hasUvTextures ? " (UV textured)" : ""; this.statusMessage = String.format( "Success! %d blocks copied to clipboard (%dx%dx%d)%s", blockCount, result.sizeX(), result.sizeY(), result.sizeZ(), textureInfo ); this.isProcessing = false; playerRefComponent.sendMessage( Message.translation("server.builderTools.objImport.success") .param("count", blockCount) .param("width", result.sizeX()) .param("height", result.sizeY()) .param("depth", result.sizeZ()) ); playerComponent.getPageManager().setPage(r, store, Page.None); this.switchToPasteTool(playerComponent, playerRefComponent); } catch (ObjParser.ObjParseException var37) { BuilderToolsPlugin.get().getLogger().at(Level.WARNING).log("OBJ parse error: %s", var37.getMessage()); this.setError("Parse error: " + var37.getMessage()); } catch (IOException var38) { ((HytaleLogger.Api)BuilderToolsPlugin.get().getLogger().at(Level.WARNING).withCause(var38)).log("OBJ import IO error"); this.setError("IO error: " + var38.getMessage()); } catch (Exception var39) { ((HytaleLogger.Api)BuilderToolsPlugin.get().getLogger().at(Level.WARNING).withCause(var39)).log("OBJ 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)); } } } private void loadMaterialData( @Nonnull Path objPath, @Nonnull ObjParser.ObjMesh mesh, @Nonnull BlockColorIndex colorIndex, @Nonnull Map materialTextures, @Nonnull Map materialToBlockId, boolean autoDetectTextures ) throws IOException { if (mesh.mtlLib() != null) { Path mtlPath = objPath.getParent().resolve(mesh.mtlLib()); if (Files.exists(mtlPath)) { Map materials = MtlParser.parse(mtlPath); Path textureDir = mtlPath.getParent(); for (Entry entry : materials.entrySet()) { String materialName = entry.getKey(); MtlParser.MtlMaterial material = entry.getValue(); String texturePath = material.diffuseTexturePath(); if (texturePath == null && autoDetectTextures) { texturePath = findMatchingTexture(textureDir, materialName); } if (texturePath != null) { Path resolvedPath = textureDir.resolve(texturePath); BufferedImage texture = TextureSampler.loadTexture(resolvedPath); if (texture != null) { materialTextures.put(materialName, texture); int[] avgColor = TextureSampler.getAverageColor(resolvedPath); if (avgColor != null) { int blockId = colorIndex.findClosestBlock(avgColor[0], avgColor[1], avgColor[2]); if (blockId > 0) { materialToBlockId.put(materialName, blockId); } } continue; } } int[] rgb = material.getDiffuseColorRGB(); if (rgb != null) { int blockId = colorIndex.findClosestBlock(rgb[0], rgb[1], rgb[2]); if (blockId > 0) { materialToBlockId.put(materialName, blockId); } } } } } } @Nullable private static String findMatchingTexture(@Nonnull Path directory, @Nonnull String materialName) { for (String suffix : AUTO_DETECT_SUFFIXES) { for (String ext : AUTO_DETECT_EXTENSIONS) { String filename = materialName + suffix + ext; if (Files.exists(directory.resolve(filename))) { return filename; } } } return null; } public static enum MeshRotation { NONE, Z_UP_TO_Y_UP, X_UP_TO_Y_UP; } public static enum Origin { BOTTOM_FRONT_LEFT, BOTTOM_CENTER, CENTER, TOP_CENTER; } public static class PageData { static final String KEY_OBJ_PATH = "@ObjPath"; static final String KEY_HEIGHT = "@Height"; static final String KEY_SCALE = "@Scale"; static final String KEY_SIZE_MODE = "SizeMode"; static final String KEY_BLOCK_PATTERN = "@BlockPattern"; static final String KEY_FILL_SOLID = "@FillSolid"; static final String KEY_USE_MATERIALS = "@UseMaterials"; static final String KEY_AUTO_DETECT_TEXTURES = "@AutoDetectTextures"; static final String KEY_ORIGIN = "@Origin"; static final String KEY_ROTATION = "@Rotation"; 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(ObjImportPage.PageData.class, ObjImportPage.PageData::new) .addField(new KeyedCodec<>("@ObjPath", Codec.STRING), (entry, s) -> entry.objPath = s, entry -> entry.objPath) .addField(new KeyedCodec<>("@Height", Codec.INTEGER), (entry, i) -> entry.height = i, entry -> entry.height) .addField(new KeyedCodec<>("@Scale", Codec.FLOAT), (entry, f) -> entry.scale = f, entry -> entry.scale) .addField(new KeyedCodec<>("SizeMode", Codec.STRING), (entry, s) -> entry.sizeMode = s, entry -> entry.sizeMode) .addField(new KeyedCodec<>("@BlockPattern", Codec.STRING), (entry, s) -> entry.blockPattern = s, entry -> entry.blockPattern) .addField(new KeyedCodec<>("@FillSolid", Codec.BOOLEAN), (entry, b) -> entry.fillSolid = b, entry -> entry.fillSolid) .addField(new KeyedCodec<>("@UseMaterials", Codec.BOOLEAN), (entry, b) -> entry.useMaterials = b, entry -> entry.useMaterials) .addField(new KeyedCodec<>("@AutoDetectTextures", Codec.BOOLEAN), (entry, b) -> entry.autoDetectTextures = b, entry -> entry.autoDetectTextures) .addField(new KeyedCodec<>("@Origin", Codec.STRING), (entry, s) -> entry.origin = s, entry -> entry.origin) .addField(new KeyedCodec<>("@Rotation", Codec.STRING), (entry, s) -> entry.rotation = s, entry -> entry.rotation) .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 objPath; @Nullable private Integer height; @Nullable private Float scale; @Nullable private String sizeMode; @Nullable private String blockPattern; @Nullable private Boolean fillSolid; @Nullable private Boolean useMaterials; @Nullable private Boolean autoDetectTextures; @Nullable private String origin; @Nullable private String rotation; @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; } private record WeightedBlock(int blockId, int weight) { } }