812 lines
38 KiB
Java
812 lines
38 KiB
Java
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 == Integer.MIN_VALUE) {
|
|
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(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;
|
|
}
|
|
|
|
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<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;
|
|
}
|
|
|
|
private record WeightedBlock(int blockId, int weight) {
|
|
}
|
|
}
|