HyCodeYourTale
classpublicPriority 3

ObjImportPage

com.hypixel.hytale.builtin.buildertools.objimport.ObjImportPage

extends InteractiveCustomUIPage

1

Methods

1

Public Methods

17

Fields

1

Constructors

Constants

String[]AUTO_DETECT_EXTENSIONS= <complex>
String[]AUTO_DETECT_SUFFIXES= <complex>
BuilderCodec<ObjImportPage.PageData>CODEC= BuilderCodec.builder(ObjImportPage.PageData.class, ObjImportPage.PageData::new) .addFiel...
StringDEFAULT_BLOCK= "Rock_Stone"
intDEFAULT_HEIGHT= 20
floatDEFAULT_SCALE= 1.0F
PathIMPORTS_DIR= Paths.get("imports", "models")
StringKEY_AUTO_DETECT_TEXTURES= "@AutoDetectTextures"
StringKEY_BLOCK_PATTERN= "@BlockPattern"
StringKEY_BROWSE= "Browse"
StringKEY_BROWSER_CANCEL= "BrowserCancel"
StringKEY_BROWSER_SELECT= "BrowserSelect"
StringKEY_FILL_SOLID= "@FillSolid"
StringKEY_HEIGHT= "@Height"
StringKEY_IMPORT= "Import"
StringKEY_OBJ_PATH= "@ObjPath"
StringKEY_ORIGIN= "@Origin"
StringKEY_ROTATION= "@Rotation"
StringKEY_SCALE= "@Scale"
StringKEY_SIZE_MODE= "SizeMode"
StringKEY_USE_MATERIALS= "@UseMaterials"
intMAX_HEIGHT= 320
floatMAX_SCALE= 100.0F
intMIN_HEIGHT= 1
floatMIN_SCALE= 0.01F
StringPASTE_TOOL_ID= "EditorTool_Paste"

Constructors

public
ObjImportPage(PlayerRef playerRef)

Methods

Public Methods (1)

public
void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, ObjImportPage.PageData data)

Fields

Private/Package Fields (17)

privateboolean autoDetectTextures
privateString blockPattern
privateServerFileBrowser browser
privateboolean fillSolid
privateboolean isError
privateboolean isProcessing
privateString objPath
privateObjImportPage.Origin origin
privateString originStr
privateObjImportPage.MeshRotation rotation
privateString rotationStr
privatefloat scale
privateboolean showBrowser
privateString statusMessage
privateint targetHeight
privateboolean useMaterials
privateboolean useScaleMode

Inheritance

Parent
Current
Interface
Child

Use mouse wheel to zoom, drag to pan. Click nodes to navigate.

Related Classes

Source Code

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<ObjImportPage.PageData> {
   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<EntityStore> ref, @Nonnull UICommandBuilder commandBuilder, @Nonnull UIEventBuilder eventBuilder, @Nonnull Store<EntityStore> 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<DropdownEntryInfo> 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<DropdownEntryInfo> 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<DropdownEntryInfo> 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<EntityStore> ref, @Nonnull Store<EntityStore> 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<ObjImportPage.WeightedBlock> parseBlockPattern(@Nonnull String pattern) {
      List<ObjImportPage.WeightedBlock> 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 == -2147483648) {
               return null;
            }

            result.add(new ObjImportPage.WeightedBlock(blockId, weight));
         }
      }

      return result.isEmpty() ? null : result;
   }

   private int selectRandomBlock(@Nonnull List<ObjImportPage.WeightedBlock> 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<EntityStore> ref, @Nonnull Store<EntityStore> 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<ObjImportPage.WeightedBlock> 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((double)(meshHeight * finalScale)));
                           } else {
                              computedHeight = finalHeight;
                           }

                           if (blocks.isEmpty()) {
                              this.setError("No blocks available for import");
                              return;
                           }

                           BlockColorIndex colorIndex = BuilderToolsPlugin.get().getBlockColorIndex();
                           Map<String, BufferedImage> materialTextures = new HashMap<>();
                           Map<String, Integer> 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<String, BufferedImage> materialTextures,
      @Nonnull Map<String, Integer> materialToBlockId,
      boolean autoDetectTextures
   ) throws IOException {
      if (mesh.mtlLib() != null) {
         Path mtlPath = objPath.getParent().resolve(mesh.mtlLib());
         if (Files.exists(mtlPath)) {
            Map<String, MtlParser.MtlMaterial> materials = MtlParser.parse(mtlPath);
            Path textureDir = mtlPath.getParent();

            for (Entry<String, MtlParser.MtlMaterial> 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;

      private MeshRotation() {
      }
   }

   public static enum Origin {
      BOTTOM_FRONT_LEFT,
      BOTTOM_CENTER,
      CENTER,
      TOP_CENTER;

      private Origin() {
      }
   }

   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<ObjImportPage.PageData> 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;

      public PageData() {
      }
   }

   private static record WeightedBlock(int blockId, int weight) {
   }
}