diff --git a/build.gradle.kts b/build.gradle.kts index 35f47d0..5914706 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -82,7 +82,9 @@ repositories { dependencies { compile("com.flowpowered:flow-nbt:1.0.1-SNAPSHOT") - compile("io.github.opencubicchunks:regionlib:0.54.0-SNAPSHOT") + compile("io.github.opencubicchunks:regionlib:0.55.0-SNAPSHOT") + compile("com.carrotsearch:hppc:0.8.1") + compile("com.google.guava:guava:27.0.1-jre") testCompile("junit:junit:4.11") } @@ -91,11 +93,11 @@ jar.apply { attributes["Main-Class"] = "cubicchunks.converter.gui.ConverterGui" } } - +/* val javadocJar by tasks.creating(Jar::class) { classifier = "javadoc" from(tasks["javadoc"]) -} +}*/ val sourcesJar by tasks.creating(Jar::class) { classifier = "sources" from(sourceSets["main"].java.srcDirs) @@ -182,7 +184,7 @@ uploadArchives.apply { // tasks must be before artifacts, don't change the order artifacts { withGroovyBuilder { - "archives"(tasks["jar"], shadowJar, sourcesJar, javadocJar) + "archives"(tasks["jar"], shadowJar, sourcesJar) } } diff --git a/src/main/java/cubicchunks/converter/gui/ConverterWorker.java b/src/main/java/cubicchunks/converter/gui/ConverterWorker.java index 67fe071..707030d 100644 --- a/src/main/java/cubicchunks/converter/gui/ConverterWorker.java +++ b/src/main/java/cubicchunks/converter/gui/ConverterWorker.java @@ -23,38 +23,73 @@ */ package cubicchunks.converter.gui; +import static java.awt.GridBagConstraints.HORIZONTAL; +import static java.awt.GridBagConstraints.NONE; +import static java.awt.GridBagConstraints.NORTH; +import static java.awt.GridBagConstraints.NORTHWEST; + +import cubicchunks.converter.lib.IProgressListener; import cubicchunks.converter.lib.convert.WorldConverter; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import javax.swing.JButton; +import javax.swing.JFrame; import javax.swing.JOptionPane; +import javax.swing.JPanel; import javax.swing.JProgressBar; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; import javax.swing.SwingWorker; public class ConverterWorker extends SwingWorker { private final WorldConverter converter; + private final JFrame parent; private JProgressBar progressBar; private JProgressBar convertQueueFill; private JProgressBar ioQueueFill; private Runnable onDone; public ConverterWorker(WorldConverter converter, JProgressBar progressBar, JProgressBar convertQueueFill, - JProgressBar ioQueueFill, Runnable onDone) { + JProgressBar ioQueueFill, Runnable onDone, JFrame parent) { this.converter = converter; this.progressBar = progressBar; this.convertQueueFill = convertQueueFill; this.ioQueueFill = ioQueueFill; this.onDone = onDone; + this.parent = parent; } @Override protected Throwable doInBackground() { try { - this.converter.convert(this::publish); + this.converter.convert(new IProgressListener() { + @Override public void update(Void progress) { + publish(progress); + } + + @Override public IProgressListener.ErrorHandleResult error(Throwable t) { + try { + CompletableFuture future = new CompletableFuture<>(); + EventQueue.invokeAndWait(() -> future.complete(showErrorMessage(t))); + return future.get(); + } catch (Exception e) { + e.printStackTrace(); + return ErrorHandleResult.STOP_KEEP_DATA; + } + } + }); } catch (Throwable t) { t.printStackTrace(); return t; @@ -62,6 +97,60 @@ public ConverterWorker(WorldConverter converter, JProgressBar progressBar, JProg return null; } + private IProgressListener.ErrorHandleResult showErrorMessage(Throwable error) { + String[] options = {"Ignore", "Ignore all", "Stop, delete results", "Stop, keep result"}; + JPanel infoPanel = new JPanel(new GridBagLayout()); + + Insets insets = new Insets(3, 10, 3, 10); + + JTextArea errorInfo = new JTextArea("An error occurred while converting chunks. Do you want to continue?"); + errorInfo.setLineWrap(true); + errorInfo.setEditable(false); + errorInfo.setEnabled(false); + infoPanel.add(errorInfo, new GridBagConstraints(0, 0, 1, 1, 1, 1, NORTH, HORIZONTAL, insets, 0, 0)); + + + JTextArea exceptionDetails = new JTextArea(exceptionString(error)); + exceptionDetails.setEditable(false); + exceptionDetails.setLineWrap(false); + + JScrollPane scrollPane = new JScrollPane(exceptionDetails); + scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + scrollPane.setMinimumSize(new Dimension(100, 250)); + scrollPane.setMaximumSize(new Dimension(500, 320)); + scrollPane.setPreferredSize(new Dimension(640, 400)); + + JButton moreDetails = new JButton(); + moreDetails.setText("Show details..."); + moreDetails.addActionListener(e -> { + JOptionPane.showConfirmDialog(infoPanel, scrollPane, "Error details", JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE); + }); + infoPanel.add(moreDetails, new GridBagConstraints(0, 1, 1, 1, 1, 1, NORTHWEST, NONE, insets, 0, 0)); + + int code = JOptionPane.showOptionDialog( + parent, + infoPanel, + "An error occurred while converting chunks", 0, JOptionPane.ERROR_MESSAGE, + null, options, "Ignore"); + if (code == JOptionPane.CLOSED_OPTION) { + return IProgressListener.ErrorHandleResult.IGNORE; + } + switch (code) { + case 0: + return IProgressListener.ErrorHandleResult.IGNORE; + case 1: + return IProgressListener.ErrorHandleResult.IGNORE_ALL; + case 2: + return IProgressListener.ErrorHandleResult.STOP_DISCARD; + case 3: + return IProgressListener.ErrorHandleResult.STOP_KEEP_DATA; + default: + assert false; + return IProgressListener.ErrorHandleResult.IGNORE; + } + } + @Override protected void process(List l) { int submitted = converter.getSubmittedChunks(); int total = converter.getTotalChunks(); @@ -99,6 +188,10 @@ protected void done() { if (t == null) { return; } + JOptionPane.showMessageDialog(null, exceptionString(t)); + } + + private static String exceptionString(Throwable t) { ByteArrayOutputStream out = new ByteArrayOutputStream(); PrintStream ps; try { @@ -108,12 +201,6 @@ protected void done() { } t.printStackTrace(ps); ps.close(); - String str; - try { - str = new String(out.toByteArray(), "UTF-8"); - } catch (UnsupportedEncodingException e1) { - throw new Error(e1); - } - JOptionPane.showMessageDialog(null, str); + return new String(out.toByteArray(), StandardCharsets.UTF_8); } } diff --git a/src/main/java/cubicchunks/converter/gui/GuiFrame.java b/src/main/java/cubicchunks/converter/gui/GuiFrame.java index 521fc17..bc7e294 100644 --- a/src/main/java/cubicchunks/converter/gui/GuiFrame.java +++ b/src/main/java/cubicchunks/converter/gui/GuiFrame.java @@ -23,26 +23,29 @@ */ package cubicchunks.converter.gui; -import cubicchunks.converter.lib.Utils; -import cubicchunks.converter.lib.anvil2cc.Anvil2CCDataConverter; -import cubicchunks.converter.lib.anvil2cc.Anvil2CCLevelInfoConverter; -import cubicchunks.converter.lib.anvil2cc.AnvilChunkReader; -import cubicchunks.converter.lib.anvil2cc.CubicChunkWriter; +import cubicchunks.converter.lib.Registry; +import cubicchunks.converter.lib.util.Utils; import cubicchunks.converter.lib.convert.WorldConverter; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; +import java.awt.FlowLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Objects; import javax.swing.JButton; +import javax.swing.JComboBox; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.JTextField; @@ -66,6 +69,11 @@ public class GuiFrame extends JFrame { private JTextField srcPathField; private JTextField dstPathField; + private String inFormat = "Anvil"; + private String outFormat = "CubicChunks"; + + private JComboBox selectConverter; + public void init() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); @@ -135,6 +143,28 @@ public void init() { label.setForeground(Color.RED); mainPanel.add(label, gbc); + JPanel formatSelect = new JPanel(new FlowLayout()); + { + formatSelect.add(new JLabel("Converter: ")); + selectConverter = new JComboBox<>(); + for (Registry.ClassPair converter : Registry.getConverters()) { + ConverterDesc desc = new ConverterDesc( + Registry.getReader(converter.getIn()), + Registry.getWriter(converter.getOut()) + ); + selectConverter.addItem(desc); + } + selectConverter.setSelectedIndex(0); + formatSelect.add(selectConverter); + } + + gbc.gridx = 0; + gbc.gridy = 5; + gbc.gridwidth = 1; + gbc.weightx = 1; + gbc.fill = GridBagConstraints.NONE; + mainPanel.add(formatSelect, gbc); + root.add(mainPanel, BorderLayout.CENTER); root.setBorder(new EmptyBorder(10, 10, 10, 10)); @@ -158,7 +188,7 @@ private void addSelectFilePanel(JPanel panel, boolean isSrc) { JLabel label = new JLabel(isSrc ? "Source: " : "Destination: "); Path srcPath = Utils.getApplicationDirectory().resolve("saves").resolve("New World"); - Path dstPath = getDstForSrc(srcPath); + Path dstPath = getDstForSrc(srcPath, outFormat); JTextField path = new JTextField((isSrc ? srcPath : dstPath).toString()); if (isSrc) { this.srcPathField = path; @@ -195,7 +225,7 @@ private void addSelectFilePanel(JPanel panel, boolean isSrc) { chooser.setCurrentDirectory(getDefaultSaveLocation().toFile()); int result = chooser.showDialog(this, "Select"); if (result == JFileChooser.APPROVE_OPTION) { - onSelectLocation(isSrc, chooser); + onSelectLocation(isSrc, chooser, outFormat); } }); selectBtn.setPreferredSize(new Dimension(30, (int) path.getPreferredSize().getHeight())); @@ -231,13 +261,13 @@ private void updateConvertBtn() { convertBtn.setEnabled(!isConverting && Utils.isValidPath(dstPathField.getText()) && Utils.fileExists(srcPathField.getText())); } - private void onSelectLocation(boolean isSrc, JFileChooser chooser) { + private void onSelectLocation(boolean isSrc, JFileChooser chooser, String format) { Path file = chooser.getSelectedFile().toPath(); if (isSrc) { srcPathField.setText(file.toString()); if (!hasChangedDst) { - dstPathField.setText(getDstForSrc(file).toString()); + dstPathField.setText(getDstForSrc(file, format).toString()); } } else { dstPathField.setText(file.toString()); @@ -246,8 +276,8 @@ private void onSelectLocation(boolean isSrc, JFileChooser chooser) { updateConvertBtn(); } - private Path getDstForSrc(Path src) { - return src.getParent().resolve(src.getFileName().toString() + " - CubicChunks"); + private Path getDstForSrc(Path src, String outFormat) { + return src.getParent().resolve(src.getFileName().toString() + " - " + outFormat); } private Path getDefaultSaveLocation() { @@ -267,23 +297,88 @@ private void convert() { updateConvertBtn(); return; } + if (Files.exists(dstPath) && !Files.isDirectory(dstPath)) { + JOptionPane.showMessageDialog(this, "The destination is not a directory!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + try { + if (!Utils.isEmpty(dstPath)) { + String[] options = {"Cancel", "Continue"}; + int result = JOptionPane.showOptionDialog(this, "The selected destination directory is not empty.\nThis may result in overwriting " + + "or losing all data in this directory!\n\nDo you want cancel and select another directory?", + "Warning", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, "Cancel"); + if (result == JOptionPane.CLOSED_OPTION) { + return; // assume cancel + } + if (result == 0) { + return; + } + } + } catch (IOException e) { + JOptionPane.showMessageDialog(this, "Error while checking if destination directory is empty!", "Error", JOptionPane.ERROR_MESSAGE); + return; + } progressBar.setStringPainted(true); convertFill.setStringPainted(true); ioFill.setStringPainted(true); isConverting = true; updateConvertBtn(); + + ConverterDesc desc = (ConverterDesc) selectConverter.getSelectedItem(); + this.inFormat = desc.getIn(); + this.outFormat = desc.getOut(); WorldConverter converter = new WorldConverter<>( - new Anvil2CCLevelInfoConverter(srcPath, dstPath), - new AnvilChunkReader(srcPath), - new Anvil2CCDataConverter(), - new CubicChunkWriter(dstPath) + Registry.getLevelConverter(inFormat, outFormat).apply(srcPath, dstPath), + Registry.getReader(inFormat).apply(srcPath), + Registry.getConverter(inFormat, outFormat).get(), + Registry.getWriter(outFormat).apply(dstPath) ); ConverterWorker w = new ConverterWorker(converter, progressBar, convertFill, ioFill, () -> { isConverting = false; progressBar.setString("Done!"); progressBar.setValue(0); + convertFill.setValue(0); + ioFill.setValue(0); updateConvertBtn(); - }); + }, this); w.execute(); } + + private static class ConverterDesc { + private final String in; + private final String out; + + private ConverterDesc(String in, String out) { + this.in = in; + this.out = out; + } + + public String getIn() { + return in; + } + + public String getOut() { + return out; + } + + @Override public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConverterDesc that = (ConverterDesc) o; + return in.equals(that.in) && + out.equals(that.out); + } + + @Override public int hashCode() { + return Objects.hash(in, out); + } + + @Override public String toString() { + return in + " -> " + out; + } + } } diff --git a/src/main/java/cubicchunks/converter/lib/Dimensions.java b/src/main/java/cubicchunks/converter/lib/Dimensions.java index 01eb111..ace4a3a 100644 --- a/src/main/java/cubicchunks/converter/lib/Dimensions.java +++ b/src/main/java/cubicchunks/converter/lib/Dimensions.java @@ -35,7 +35,7 @@ public class Dimensions { static { addDimension(new Dimension("Overworld", "")); addDimension(new Dimension("The Nether", "DIM-1")); - addDimension(new Dimension("Overworld", "DIM1")); + addDimension(new Dimension("The End", "DIM1")); } public static void addDimension(Dimension dim) { diff --git a/src/main/java/cubicchunks/converter/lib/IProgressListener.java b/src/main/java/cubicchunks/converter/lib/IProgressListener.java index 327b7fd..a239d92 100644 --- a/src/main/java/cubicchunks/converter/lib/IProgressListener.java +++ b/src/main/java/cubicchunks/converter/lib/IProgressListener.java @@ -26,4 +26,10 @@ public interface IProgressListener { void update(Void progress); + + ErrorHandleResult error(Throwable t); + + enum ErrorHandleResult { + IGNORE, IGNORE_ALL, STOP_KEEP_DATA, STOP_DISCARD + } } diff --git a/src/main/java/cubicchunks/converter/lib/Registry.java b/src/main/java/cubicchunks/converter/lib/Registry.java new file mode 100644 index 0000000..baa38e6 --- /dev/null +++ b/src/main/java/cubicchunks/converter/lib/Registry.java @@ -0,0 +1,201 @@ +/* + * This file is part of CubicChunksConverter, licensed under the MIT License (MIT). + * + * Copyright (c) 2017 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package cubicchunks.converter.lib; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.Maps; +import cubicchunks.converter.lib.convert.ChunkDataConverter; +import cubicchunks.converter.lib.convert.ChunkDataReader; +import cubicchunks.converter.lib.convert.ChunkDataWriter; +import cubicchunks.converter.lib.convert.LevelInfoConverter; +import cubicchunks.converter.lib.convert.anvil2cc.Anvil2CCDataConverter; +import cubicchunks.converter.lib.convert.anvil2cc.Anvil2CCLevelInfoConverter; +import cubicchunks.converter.lib.convert.cc2anvil.CC2AnvilDataConverter; +import cubicchunks.converter.lib.convert.cc2anvil.CC2AnvilLevelInfoConverter; +import cubicchunks.converter.lib.convert.data.AnvilChunkData; +import cubicchunks.converter.lib.convert.data.CubicChunksColumnData; +import cubicchunks.converter.lib.convert.data.MultilayerAnvilChunkData; +import cubicchunks.converter.lib.convert.io.AnvilChunkReader; +import cubicchunks.converter.lib.convert.io.AnvilChunkWriter; +import cubicchunks.converter.lib.convert.io.CubicChunkReader; +import cubicchunks.converter.lib.convert.io.CubicChunkWriter; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +public class Registry { + private static final BiMap>> readersByName = Maps.synchronizedBiMap(HashBiMap.create()); + private static final BiMap, Function>> readersByClass = Maps.synchronizedBiMap(HashBiMap.create()); + + private static final BiMap>> writersByName = Maps.synchronizedBiMap(HashBiMap.create()); + private static final BiMap, Function>> writersByClass = Maps.synchronizedBiMap(HashBiMap.create()); + + private static final BiMap, Supplier>> convertersByClass = Maps.synchronizedBiMap(HashBiMap.create()); + private static final BiMap, BiFunction>> levelConvertersByClass = Maps.synchronizedBiMap(HashBiMap.create()); + + static { + registerReader("Anvil", AnvilChunkReader::new, AnvilChunkData.class); + registerReader("CubicChunks", CubicChunkReader::new, CubicChunksColumnData.class); + + registerWriter("Anvil", AnvilChunkWriter::new, MultilayerAnvilChunkData.class); + registerWriter("CubicChunks", CubicChunkWriter::new, CubicChunksColumnData.class); + + registerConverter(Anvil2CCDataConverter::new, Anvil2CCLevelInfoConverter::new, AnvilChunkData.class, CubicChunksColumnData.class); + registerConverter(CC2AnvilDataConverter::new, CC2AnvilLevelInfoConverter::new, CubicChunksColumnData.class, MultilayerAnvilChunkData.class); + } + + // can't have all named register because of type erasure + + public static void registerReader(String name, Function> reader, Class clazz) { + readersByName.put(name, reader); + readersByClass.put(clazz, reader); + } + + public static void registerWriter(String name, Function> writer, Class clazz) { + writersByName.put(name, writer); + writersByClass.put(clazz, writer); + } + + public static void registerConverter(Supplier> conv, + BiFunction> levelConv, Class in, Class out) { + + convertersByClass.put(new ClassPair<>(in, out), conv); + levelConvertersByClass.put(new ClassPair<>(in, out), levelConv); + } + + public static Iterable getWriters() { + return writersByName.keySet(); + } + + public static Iterable getReaders() { + return readersByName.keySet(); + } + + public static Iterable> getConverters() { + return convertersByClass.keySet(); + } + + @SuppressWarnings("unchecked") + public static Function> getReader(String name) { + return (Function>) readersByName.get(name); + } + + @SuppressWarnings("unchecked") + public static Function> getWriter(String name) { + return (Function>) writersByName.get(name); + } + + @SuppressWarnings("unchecked") + public static String getReader(Class clazz) { + return readersByName.inverse().get(readersByClass.get(clazz)); + } + + @SuppressWarnings("unchecked") + public static String getWriter(Class clazz) { + return writersByName.inverse().get(writersByClass.get(clazz)); + } + + @SuppressWarnings("unchecked") + public static Supplier> getConverter(String inputName, String outputName) { + ClassPair pair = new ClassPair<>( + getReaderClass(inputName), + getWriterClass(outputName) + ); + return (Supplier>) convertersByClass.get(pair); + } + + @SuppressWarnings("unchecked") + public static Supplier> getConverter(ClassPair classes) { + return (Supplier>) convertersByClass.get(classes); + } + + @SuppressWarnings("unchecked") + public static BiFunction> getLevelConverter(String inputName, String outputName) { + ClassPair pair = new ClassPair<>( + getReaderClass(inputName), + getWriterClass(outputName) + ); + return (BiFunction>) levelConvertersByClass.get(pair); + } + + @SuppressWarnings("unchecked") + public static BiFunction> getLevelConverter(ClassPair classes) { + return (BiFunction>) levelConvertersByClass.get(classes); + } + + @SuppressWarnings("unchecked") + public static Class getReaderClass(String name) { + return (Class) readersByClass.inverse().get(getReader(name)); + } + + @SuppressWarnings("unchecked") + public static Class getWriterClass(String writer) { + return (Class) writersByClass.inverse().get(getWriter(writer)); + } + + public static class ClassPair { + private final Class in; + private final Class out; + + private ClassPair(Class in, Class out) { + this.in = in; + this.out = out; + } + + public Class getIn() { + return in; + } + + public Class getOut() { + return out; + } + + @Override public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClassPair classPair = (ClassPair) o; + return in.equals(classPair.in) && + out.equals(classPair.out); + } + + @Override public int hashCode() { + return Objects.hash(in, out); + } + + @Override public String toString() { + return "ClassPair{" + + "in=" + in + + ", out=" + out + + '}'; + } + } +} diff --git a/src/main/java/cubicchunks/converter/lib/conf/ConverterConfig.java b/src/main/java/cubicchunks/converter/lib/conf/ConverterConfig.java new file mode 100644 index 0000000..59a3623 --- /dev/null +++ b/src/main/java/cubicchunks/converter/lib/conf/ConverterConfig.java @@ -0,0 +1,89 @@ +/* + * This file is part of CubicChunksConverter, licensed under the MIT License (MIT). + * + * Copyright (c) 2017 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package cubicchunks.converter.lib.conf; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ConverterConfig { + private Map defaults; + private Map overrides; + + public ConverterConfig(Map defaults) { + this.defaults = new ConcurrentHashMap<>(defaults); + this.overrides = new ConcurrentHashMap<>(); + } + + public void set(String location, Object value) { + overrides.put(location, value); + } + + public void setDefault(String location, Object defaultValue) { + defaults.put(location, defaultValue); + } + + public double getDouble(String location) { + return ((Number) getValue(location)).doubleValue(); + } + + public float getFloat(String location) { + return ((Number) getValue(location)).floatValue(); + } + + public int getInt(String location) { + return ((Number) getValue(location)).intValue(); + } + + public long getLong(String location) { + return ((Number) getValue(location)).longValue(); + } + + public boolean getBool(String location) { + Object value = getValue(location); + if (value instanceof Number) { + return ((Number) value).longValue() != 0L; + } + return ((Boolean) getValue(location)); + } + + public String getString(String location) { + return getValue(location).toString(); + } + + public Object getValue(String location) { + if (overrides.containsKey(location)) { + return overrides.get(location); + } + return this.defaults.get(location); + } + + public Map getDefaults() { + return new HashMap<>(defaults); + } + + public Map getOverrides() { + return new HashMap<>(overrides); + } +} diff --git a/src/main/java/cubicchunks/converter/lib/convert/ChunkDataConverter.java b/src/main/java/cubicchunks/converter/lib/convert/ChunkDataConverter.java index efb8ae3..846b9d1 100644 --- a/src/main/java/cubicchunks/converter/lib/convert/ChunkDataConverter.java +++ b/src/main/java/cubicchunks/converter/lib/convert/ChunkDataConverter.java @@ -23,6 +23,10 @@ */ package cubicchunks.converter.lib.convert; +import cubicchunks.converter.lib.conf.ConverterConfig; + +import java.util.HashMap; + /** * Converts chunk data from {@link IN} format to {@link OUT} format. */ @@ -35,4 +39,8 @@ public interface ChunkDataConverter { * @return The converted chunk data */ OUT convert(IN input); + + default ConverterConfig getConfig() { + return null; + } } diff --git a/src/main/java/cubicchunks/converter/lib/convert/ChunkDataReader.java b/src/main/java/cubicchunks/converter/lib/convert/ChunkDataReader.java index 0e123b0..b49e969 100644 --- a/src/main/java/cubicchunks/converter/lib/convert/ChunkDataReader.java +++ b/src/main/java/cubicchunks/converter/lib/convert/ChunkDataReader.java @@ -37,11 +37,18 @@ public interface ChunkDataReader extends AutoCloseable { * @param increment Runnable to run when a new chunk is detected, * to increment the internal counter and update progress report */ - void countInputChunks(Runnable increment) throws IOException; + void countInputChunks(Runnable increment) throws IOException, InterruptedException; /** * Loads chunks into memory, and gives them to the provided consumer. * The provided consumer will block if data is provided too fast. */ - void loadChunks(Consumer accept) throws IOException; + void loadChunks(Consumer accept) throws IOException, InterruptedException; + + /** + * Indicates that reading chunks should be stopped and + * {@link #loadChunks(Consumer)} method should return. + * Can be called from any thread. + */ + void stop(); } diff --git a/src/main/java/cubicchunks/converter/lib/convert/ChunkDataWriter.java b/src/main/java/cubicchunks/converter/lib/convert/ChunkDataWriter.java index cf5ff50..f6ded91 100644 --- a/src/main/java/cubicchunks/converter/lib/convert/ChunkDataWriter.java +++ b/src/main/java/cubicchunks/converter/lib/convert/ChunkDataWriter.java @@ -39,4 +39,9 @@ public interface ChunkDataWriter extends AutoCloseable { * called from a thread running in the background. */ void accept(T t) throws IOException; + + /** + * Deletes all written data. + */ + void discardData() throws IOException; } diff --git a/src/main/java/cubicchunks/converter/lib/convert/WorldConverter.java b/src/main/java/cubicchunks/converter/lib/convert/WorldConverter.java index 3e4ca2a..c26a30a 100644 --- a/src/main/java/cubicchunks/converter/lib/convert/WorldConverter.java +++ b/src/main/java/cubicchunks/converter/lib/convert/WorldConverter.java @@ -23,11 +23,9 @@ */ package cubicchunks.converter.lib.convert; -import cubicchunks.converter.lib.Dimension; import cubicchunks.converter.lib.IProgressListener; import java.io.IOException; -import java.nio.file.Path; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.RejectedExecutionException; @@ -35,39 +33,43 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiFunction; public class WorldConverter { private static final int THREADS = Runtime.getRuntime().availableProcessors(); private static final int CONVERT_QUEUE_SIZE = 32 * THREADS; private static final int IO_QUEUE_SIZE = 8 * THREADS; + private final LevelInfoConverter levelConverter; private final ChunkDataReader reader; private final ChunkDataConverter converter; private final ChunkDataWriter writer; - private AtomicInteger chunkCount = new AtomicInteger(-1); + private final AtomicInteger chunkCount; + private int copyChunks; - private static final BiFunction LOCATION_FUNC_DST = (d, p) -> { - if (!d.getDirectory().isEmpty()) { - p = p.resolve(d.getDirectory()); - } - return p; - }; - private int copyChunks = -1; + private final ArrayBlockingQueue convertQueueImpl; + private final ArrayBlockingQueue ioQueueImpl; - private boolean countingChunks; + private final ThreadPoolExecutor convertQueue; + private final ThreadPoolExecutor ioQueue; - private ArrayBlockingQueue convertQueueImpl = new ArrayBlockingQueue<>(CONVERT_QUEUE_SIZE); - private ArrayBlockingQueue ioQueueImpl = new ArrayBlockingQueue<>(IO_QUEUE_SIZE); + private volatile boolean discardConverted = false; + private volatile boolean errored = false; + // handle errors one at a time + private final Object errorLock = new Object(); - private ThreadPoolExecutor convertQueue = new ThreadPoolExecutor( - THREADS, THREADS, 0L, TimeUnit.MILLISECONDS, convertQueueImpl); - private ThreadPoolExecutor ioQueue = new ThreadPoolExecutor( - THREADS, THREADS, 0L, TimeUnit.MILLISECONDS, ioQueueImpl); + public WorldConverter( + LevelInfoConverter levelConverter, + ChunkDataReader reader, + ChunkDataConverter converter, + ChunkDataWriter writer) { + + this.levelConverter = levelConverter; + this.reader = reader; + this.converter = converter; + this.writer = writer; - { RejectedExecutionHandler handler = ((r, executor) -> { try { if (!executor.isShutdown()) { @@ -78,41 +80,33 @@ public class WorldConverter { throw new RejectedExecutionException("Executor was interrupted while the task was waiting to put on work queue", e); } }); - convertQueue.setRejectedExecutionHandler(handler); - ioQueue.setRejectedExecutionHandler(handler); - } - public WorldConverter( - LevelInfoConverter levelConverter, - ChunkDataReader reader, - ChunkDataConverter converter, - ChunkDataWriter writer) { + chunkCount = new AtomicInteger(0); - this.levelConverter = levelConverter; - this.reader = reader; - this.converter = converter; - this.writer = writer; + convertQueueImpl = new ArrayBlockingQueue<>(CONVERT_QUEUE_SIZE); + convertQueue = new ThreadPoolExecutor(THREADS, THREADS, 0L, TimeUnit.MILLISECONDS, convertQueueImpl); + convertQueue.setRejectedExecutionHandler(handler); + + ioQueueImpl = new ArrayBlockingQueue<>(IO_QUEUE_SIZE); + ioQueue = new ThreadPoolExecutor(THREADS, THREADS, 0L, TimeUnit.MILLISECONDS, ioQueueImpl); + ioQueue.setRejectedExecutionHandler(handler); } public void convert(IProgressListener progress) throws IOException { - chunkCount.set(0); - countingChunks = true; - copyChunks = 0; startCounting(); - levelConverter.convert(); - try (ChunkDataReader reader = this.reader; ChunkDataWriter writer = this.writer) { + try { reader.loadChunks(inData -> { - convertQueue.submit(new ChunkConvertTask<>(converter, writer, progress, ioQueue, inData)); + convertQueue.submit(new ChunkConvertTask<>(converter, writer, progress, this, ioQueue, inData)); copyChunks++; }); - } catch (Exception ex) { - ex.printStackTrace(); + } catch (InterruptedException e) { + // just shutdown } finally { convertQueue.shutdown(); boolean shutdownNow = false; try { - convertQueue.awaitTermination(Long.MAX_VALUE / 2, TimeUnit.NANOSECONDS); + convertQueue.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); } catch (InterruptedException e) { e.printStackTrace(); convertQueue.shutdownNow(); @@ -131,6 +125,26 @@ public void convert(IProgressListener progress) throws IOException { e.printStackTrace(); ioQueue.shutdownNow(); } + try { + reader.close(); + } catch (Exception e) { + e.printStackTrace(); + } + try { + writer.close(); + } catch (Exception e) { + e.printStackTrace(); + } + if (discardConverted) { + try { + writer.discardData(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + if (!errored) { + levelConverter.convert(); } } @@ -159,61 +173,95 @@ public int getIOBufferMaxSize() { } private void startCounting() { - countingChunks = true; new Thread(() -> { try { - reader.countInputChunks(() -> chunkCount.getAndIncrement()); + reader.countInputChunks(chunkCount::getAndIncrement); } catch (IOException e) { - e.printStackTrace(); + throw new RuntimeException(e); + } catch (InterruptedException e) { + // stop } - countingChunks = false; }, "Chunk and File counting thread").start(); } + private void handleError(Throwable t, IProgressListener progress) { + synchronized (errorLock) { + if (errored) { + return; + } + IProgressListener.ErrorHandleResult result = progress.error(t); + switch (result) { + case STOP_DISCARD: + discardConverted = true; + // fallthrough + case STOP_KEEP_DATA: + reader.stop(); + convertQueue.shutdownNow(); + ioQueue.shutdownNow(); + // fallthrough + case IGNORE_ALL: + errored = true; + } + } + } - public static class ChunkConvertTask implements Callable { - + private static class ChunkConvertTask implements Callable { private final ChunkDataConverter converter; private final ChunkDataWriter writer; private final IProgressListener progress; + private WorldConverter worldConv; private final ThreadPoolExecutor ioExecutor; private final IN toConvert; - public ChunkConvertTask( + ChunkConvertTask( ChunkDataConverter converter, ChunkDataWriter writer, IProgressListener progress, + WorldConverter worldConv, ThreadPoolExecutor ioExecutor, IN toConvert) { this.converter = converter; this.writer = writer; this.progress = progress; + this.worldConv = worldConv; this.ioExecutor = ioExecutor; this.toConvert = toConvert; } - @Override public IOWriteTask call() { - OUT converted = converter.convert(toConvert); - IOWriteTask data = new IOWriteTask<>(converted, writer); - progress.update(null); - ioExecutor.submit(data); - return data; + @Override public Void call() { + try { + OUT converted = converter.convert(toConvert); + IOWriteTask data = new IOWriteTask<>(converted, writer, worldConv, progress); + progress.update(null); + ioExecutor.submit(data); + } catch (Throwable t) { + worldConv.handleError(t, progress); + } + return null; } } - public static class IOWriteTask implements Callable { + private static class IOWriteTask implements Callable { private final OUT toWrite; private final ChunkDataWriter writer; + private final WorldConverter worldConv; + private final IProgressListener progress; - public IOWriteTask(OUT toWrite, ChunkDataWriter writer) { + IOWriteTask(OUT toWrite, ChunkDataWriter writer, WorldConverter worldConv, IProgressListener progress) { this.toWrite = toWrite; this.writer = writer; + this.worldConv = worldConv; + this.progress = progress; } - @Override public Void call() throws Exception { - writer.accept(toWrite); + @Override public Void call() { + try { + writer.accept(toWrite); + } catch (Throwable t) { + worldConv.handleError(t, progress); + } return null; } } diff --git a/src/main/java/cubicchunks/converter/lib/anvil2cc/Anvil2CCDataConverter.java b/src/main/java/cubicchunks/converter/lib/convert/anvil2cc/Anvil2CCDataConverter.java similarity index 95% rename from src/main/java/cubicchunks/converter/lib/anvil2cc/Anvil2CCDataConverter.java rename to src/main/java/cubicchunks/converter/lib/convert/anvil2cc/Anvil2CCDataConverter.java index 044acee..62d0a64 100644 --- a/src/main/java/cubicchunks/converter/lib/anvil2cc/Anvil2CCDataConverter.java +++ b/src/main/java/cubicchunks/converter/lib/convert/anvil2cc/Anvil2CCDataConverter.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package cubicchunks.converter.lib.anvil2cc; +package cubicchunks.converter.lib.convert.anvil2cc; import static java.util.Collections.singletonList; @@ -33,7 +33,9 @@ import com.flowpowered.nbt.IntArrayTag; import com.flowpowered.nbt.IntTag; import com.flowpowered.nbt.ListTag; -import cubicchunks.converter.lib.Utils; +import cubicchunks.converter.lib.util.Utils; +import cubicchunks.converter.lib.convert.data.AnvilChunkData; +import cubicchunks.converter.lib.convert.data.CubicChunksColumnData; import cubicchunks.converter.lib.convert.ChunkDataConverter; import cubicchunks.regionlib.impl.EntryLocation2D; @@ -47,14 +49,14 @@ import java.util.List; import java.util.Map; -public class Anvil2CCDataConverter implements ChunkDataConverter { +public class Anvil2CCDataConverter implements ChunkDataConverter { - public ConvertedCubicChunksData convert(AnvilChunkData input) { + public CubicChunksColumnData convert(AnvilChunkData input) { try { Map cubes = extractCubeData(input.getData()); ByteBuffer column = extractColumnData(input.getData()); EntryLocation2D location = new EntryLocation2D(input.getPosition().getEntryX(), input.getPosition().getEntryZ()); - return new ConvertedCubicChunksData(input.getDimension(), location, column, cubes); + return new CubicChunksColumnData(input.getDimension(), location, column, cubes); } catch (IOException impossible) { throw new Error("ByteArrayInputStream doesn't throw IOException", impossible); } @@ -65,7 +67,7 @@ private ByteBuffer extractColumnData(ByteBuffer vanillaData) throws IOException ByteArrayInputStream in = new ByteArrayInputStream(vanillaData.array()); CompoundTag tag = Utils.readCompressed(in); CompoundTag columnTag = extractColumnData(tag); - return Utils.writeCompressed(columnTag, true); + return Utils.writeCompressed(columnTag, false); } private CompoundTag extractColumnData(CompoundTag tag) throws IOException { @@ -132,7 +134,7 @@ private CompoundTag extractColumnData(CompoundTag tag) throws IOException { private int[] fixHeightmap(int[] heights) { for (int i = 0; i < heights.length; i++) { - heights[i]--; // vanilla = 1 above top, cc = top block + heights[i]--; // vanilla = 1 above top, data = top block } return heights; } @@ -156,7 +158,7 @@ private Map extractCubeData(ByteBuffer vanillaData) throws Map tags = extractCubeData(Utils.readCompressed(in)); Map bytes = new HashMap<>(); for (Integer y : tags.keySet()) { - bytes.put(y, Utils.writeCompressed(tags.get(y), true)); + bytes.put(y, Utils.writeCompressed(tags.get(y), false)); } return bytes; } diff --git a/src/main/java/cubicchunks/converter/lib/anvil2cc/Anvil2CCLevelInfoConverter.java b/src/main/java/cubicchunks/converter/lib/convert/anvil2cc/Anvil2CCLevelInfoConverter.java similarity index 94% rename from src/main/java/cubicchunks/converter/lib/anvil2cc/Anvil2CCLevelInfoConverter.java rename to src/main/java/cubicchunks/converter/lib/convert/anvil2cc/Anvil2CCLevelInfoConverter.java index fccaabc..a8b10a8 100644 --- a/src/main/java/cubicchunks/converter/lib/anvil2cc/Anvil2CCLevelInfoConverter.java +++ b/src/main/java/cubicchunks/converter/lib/convert/anvil2cc/Anvil2CCLevelInfoConverter.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package cubicchunks.converter.lib.anvil2cc; +package cubicchunks.converter.lib.convert.anvil2cc; import com.flowpowered.nbt.ByteTag; import com.flowpowered.nbt.CompoundMap; @@ -31,7 +31,9 @@ import com.flowpowered.nbt.stream.NBTInputStream; import com.flowpowered.nbt.stream.NBTOutputStream; import cubicchunks.converter.lib.Dimensions; -import cubicchunks.converter.lib.Utils; +import cubicchunks.converter.lib.util.Utils; +import cubicchunks.converter.lib.convert.data.AnvilChunkData; +import cubicchunks.converter.lib.convert.data.CubicChunksColumnData; import cubicchunks.converter.lib.convert.LevelInfoConverter; import java.io.FileInputStream; @@ -40,7 +42,7 @@ import java.nio.file.Files; import java.nio.file.Path; -public class Anvil2CCLevelInfoConverter implements LevelInfoConverter { +public class Anvil2CCLevelInfoConverter implements LevelInfoConverter { private final Path srcDir; private final Path dstDir; diff --git a/src/main/java/cubicchunks/converter/lib/convert/cc2anvil/CC2AnvilDataConverter.java b/src/main/java/cubicchunks/converter/lib/convert/cc2anvil/CC2AnvilDataConverter.java new file mode 100644 index 0000000..9574fb8 --- /dev/null +++ b/src/main/java/cubicchunks/converter/lib/convert/cc2anvil/CC2AnvilDataConverter.java @@ -0,0 +1,341 @@ +/* + * This file is part of CubicChunksConverter, licensed under the MIT License (MIT). + * + * Copyright (c) 2017 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package cubicchunks.converter.lib.convert.cc2anvil; + +import static cubicchunks.converter.lib.util.Utils.readCompressedCC; +import static cubicchunks.converter.lib.util.Utils.writeCompressed; + +import com.flowpowered.nbt.ByteTag; +import com.flowpowered.nbt.CompoundMap; +import com.flowpowered.nbt.CompoundTag; +import com.flowpowered.nbt.IntArrayTag; +import com.flowpowered.nbt.IntTag; +import com.flowpowered.nbt.ListTag; +import com.flowpowered.nbt.Tag; +import cubicchunks.converter.lib.conf.ConverterConfig; +import cubicchunks.converter.lib.convert.ChunkDataConverter; +import cubicchunks.converter.lib.convert.data.AnvilChunkData; +import cubicchunks.converter.lib.convert.data.CubicChunksColumnData; +import cubicchunks.converter.lib.convert.data.MultilayerAnvilChunkData; +import cubicchunks.regionlib.impl.MinecraftChunkLocation; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipException; + +import javax.annotation.Nullable; + +public class CC2AnvilDataConverter implements ChunkDataConverter { + + @Override public MultilayerAnvilChunkData convert(CubicChunksColumnData input) { + Map data = new HashMap<>(); + MinecraftChunkLocation chunkPos = new MinecraftChunkLocation(input.getPosition().getEntryX(), input.getPosition().getEntryZ(), "mca"); + + // split the data into world layers + Map worldLayers = new HashMap<>(); + input.getCubeData().forEach((key, value) -> { + ByteBuffer[] sections = worldLayers.computeIfAbsent(toWorldLayerY(key), y -> new ByteBuffer[16]); + sections[toLayerSection(key)] = value; + }); + // convert each world layer separately + worldLayers.forEach((key, value) -> + data.put(key, new AnvilChunkData(input.getDimension(), chunkPos, convertWorldLayer(input.getColumnData(), value, key))) + ); + return new MultilayerAnvilChunkData(data); + } + + private ByteBuffer convertWorldLayer(@Nullable ByteBuffer columnData, ByteBuffer[] cubes, int layerIdx) { + try { + if (dropChunk(cubes, layerIdx)) { + return null; + } + CompoundTag columnTag = columnData == null ? null : readCompressedCC(new ByteArrayInputStream(columnData.array())); + CompoundTag[] cubeTags = new CompoundTag[cubes.length]; + for (int i = 0; i < cubes.length; i++) { + if (cubes[i] != null) { + cubeTags[i] = readCompressedCC(new ByteArrayInputStream(cubes[i].array())); + } + } + CompoundTag tag = convertWorldLayer(columnTag, cubeTags, layerIdx); + return writeCompressed(tag, true); + } catch (ZipException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + throw new Error("ByteArrayInputStream doesn't throw IOException", e); + } + } + + private boolean dropChunk(ByteBuffer[] cubes, int layerIdx) { + return false; + } + + private CompoundTag convertWorldLayer(@Nullable CompoundTag column, CompoundTag[] cubes, int layerIdx) { + /* + * + * Vanilla Chunk NBT structure: + * + * ROOT + * |- DataVersion + * |- Level + * |- v + * |- xPos + * |- zPos + * |- LastUpdate + * |- TerrainPopulated + * |- LightPopulated + * |- InhabitedTime + * |- Biomes + * |- HeightMap + * |- Sections + * ||* Section list: + * | |- Y + * | |- Blocks + * | |- Data + * | |- Add + * | |- BlockLight + * | |- SkyLight + * |- Entities + * |- TileEntities + * |- TileTicks + * + * CubicChunks Column format: + * + * ROOT + * |- DataVersion + * |- Level + * |- v + * |- x + * |- z + * |- InhabitedTime + * |- Biomes + * |- OpacityIndex + */ + CompoundMap vanillaMap = new CompoundMap(); + // TODO: we expect DataVersion to be the same as column for all cubes, this is not necessarily true. Can we do something about it? + if (column != null) { + for (Tag tag : column.getValue()) { + if ("Level".equals(tag.getName())) { + CompoundMap level = new CompoundMap(); + convertLevel(level, column, cubes, layerIdx); + vanillaMap.put(new CompoundTag("Level", level)); + } else { + vanillaMap.put(tag); + } + } + } else { + CompoundMap level = new CompoundMap(); + convertLevel(level, null, cubes, layerIdx); + vanillaMap.put(new CompoundTag("Level", level)); + } + + return new CompoundTag("", vanillaMap); + + } + + private void convertLevel(CompoundMap level, @Nullable CompoundTag column, CompoundTag[] cubes, int layerIdx) { + /* + * + * Vanilla Chunk NBT structure: + * + * ROOT + * |- DataVersion + * |- Level + * |- v + * |- xPos + * |- zPos + * |- LastUpdate + * |- TerrainPopulated + * |- LightPopulated + * |- InhabitedTime + * |- Biomes + * |- HeightMap + * |- Sections + * ||* Section list: + * | |- Y + * | |- Blocks + * | |- Data + * | |- Add + * | |- BlockLight + * | |- SkyLight + * |- Entities + * |- TileEntities + * |- TileTicks + * + * CubicChunks Cube NBT structure: + * + * ROOT + * |- DataVersion + * |- Level + * |- v + * |- x + * |- y + * |- z + * |- populated + * |- fullyPopulated + * |- initLightDone + * |- Sections + * ||* A single section + * | |- Blocks + * | |- Data + * | |- Add + * | |- BlockLight + * | |- SkyLight + * |- Entities + * |- TileEntities + * |- TileTicks + * |- LightingInfo + * |- LastHeightMap + */ + if (column != null) { + CompoundMap columnLevel = (CompoundMap) column.getValue().get("Level").getValue(); + for (Tag tag : columnLevel) { + if ("OpacityIndex".equals(tag.getName())) { + level.put(getHeightMap(tag, layerIdx)); + } else { + level.put(tag); + } + } + level.put(columnLevel.get("InhabitedTime")); + level.put(columnLevel.get("Biomes")); + } + for (CompoundTag cube : cubes) { + if (cube != null) { + for (Tag tag : cube.getValue()) { + switch (tag.getName()) { + case "x": + level.put(renamedInt(tag, "xPos")); + break; + case "z": + level.put(renamedInt(tag, "zPos")); + break; + } + } + break; + } + } + + level.put(getIsPopulated(cubes, layerIdx)); + level.put(new ByteTag("LightPopulated", (byte) 0)); // let vanilla recalculate lighting + + level.put(getSections(cubes)); + level.put(getEntities(cubes)); + level.put(getTileEntities(cubes)); + level.put(getTileTicks(cubes)); + } + + private Tag getSections(CompoundTag[] cubes) { + List sections = new ArrayList<>(); + for (int y = 0; y < cubes.length; y++) { + if (cubes[y] == null) { + continue; + } + CompoundMap oldLevel = (CompoundMap) cubes[y].getValue().get("Level").getValue(); + if (oldLevel.get("Sections") == null) { + continue; + } + CompoundMap newSection = new CompoundMap(); + @SuppressWarnings("unchecked") + List oldSections = (List) oldLevel.get("Sections").getValue(); + CompoundMap oldSection = oldSections.get(0).getValue(); + newSection.putAll(oldSection); + newSection.put(new ByteTag("Y", (byte) y)); + sections.add(new CompoundTag("", newSection)); + } + return new ListTag<>("Sections", CompoundTag.class, sections); + } + + private Tag getEntities(CompoundTag[] cubes) { + return new ListTag<>("Entities", CompoundTag.class, new ArrayList<>()); + } + + private Tag getTileEntities(CompoundTag[] cubes) { + return new ListTag<>("TileEntities", CompoundTag.class, new ArrayList<>()); + } + + private Tag getTileTicks(CompoundTag[] cubes) { + return new ListTag<>("TileTicks", CompoundTag.class, new ArrayList<>()); + } + + private Tag getIsPopulated(CompoundTag[] cubes, int layerIdx) { + // with default world, only those cubes really matter + for (int y = 0; y < 8; y++) { + if (cubes[y] == null) { + return new ByteTag("TerrainPopulated", (byte) 0); + } + CompoundMap map = (CompoundMap) cubes[y].getValue().get("Level").getValue(); + if ((Byte) map.get("populated").getValue() == 0) { + return new ByteTag("TerrainPopulated", (byte) 0); + } + } + return new ByteTag("TerrainPopulated", (byte) 1); + } + + private Tag getHeightMap(Tag opacityIndex, int layerIdx) { + byte[] array = (byte[]) opacityIndex.getValue(); + int[] output = new int[256]; + ByteArrayInputStream buf = new ByteArrayInputStream(array); + try (DataInputStream in = new DataInputStream(buf)) { + + for (int i = 0; i < output.length; i++) { + in.readInt(); // yMin + output[i] = (in.readInt() + 1) - (layerIdx * 256); + int segmentCount = in.readUnsignedShort(); + for (int j = 0; j < segmentCount; j++) { + in.readInt(); + } + } + } catch (EOFException e) { + e.printStackTrace(); + Arrays.fill(output, -999); + } catch (IOException e) { + throw new Error("ByteArrayInputStream doesn't throw IOException"); + } + return new IntArrayTag("HeightMap", output); + } + + private IntTag renamedInt(Tag old, String newName) { + return new IntTag(newName, (Integer) old.getValue()); + } + + @Override public ConverterConfig getConfig() { + return new ConverterConfig(new HashMap<>()); + } + + private static int toWorldLayerY(int cubeY) { + return cubeY >> 4; + } + + private static int toLayerSection(int cubeY) { + return cubeY & 0xF; + } +} diff --git a/src/main/java/cubicchunks/converter/lib/convert/cc2anvil/CC2AnvilLevelInfoConverter.java b/src/main/java/cubicchunks/converter/lib/convert/cc2anvil/CC2AnvilLevelInfoConverter.java new file mode 100644 index 0000000..5fa0ec9 --- /dev/null +++ b/src/main/java/cubicchunks/converter/lib/convert/cc2anvil/CC2AnvilLevelInfoConverter.java @@ -0,0 +1,82 @@ +/* + * This file is part of CubicChunksConverter, licensed under the MIT License (MIT). + * + * Copyright (c) 2017 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package cubicchunks.converter.lib.convert.cc2anvil; + +import com.flowpowered.nbt.CompoundMap; +import com.flowpowered.nbt.CompoundTag; +import com.flowpowered.nbt.Tag; +import com.flowpowered.nbt.stream.NBTInputStream; +import com.flowpowered.nbt.stream.NBTOutputStream; +import cubicchunks.converter.lib.Dimensions; +import cubicchunks.converter.lib.util.Utils; +import cubicchunks.converter.lib.convert.LevelInfoConverter; +import cubicchunks.converter.lib.convert.data.CubicChunksColumnData; +import cubicchunks.converter.lib.convert.data.MultilayerAnvilChunkData; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class CC2AnvilLevelInfoConverter implements LevelInfoConverter { + + private final Path srcPath; + private final Path dstPath; + + public CC2AnvilLevelInfoConverter(Path srcPath, Path dstPath) { + this.srcPath = srcPath; + this.dstPath = dstPath; + } + + @Override public void convert() throws IOException { + Utils.forEachDirectory(dstPath, dir -> { + CompoundTag root; + try (NBTInputStream nbtIn = new NBTInputStream(new FileInputStream(srcPath.resolve("level.dat").toFile())); + NBTOutputStream nbtOut = new NBTOutputStream(new FileOutputStream(dir.resolve("level.dat").toFile()))) { + root = (CompoundTag) nbtIn.readTag(); + + CompoundMap newRoot = new CompoundMap(); + for (Tag tag : root.getValue()) { + if (!tag.getName().equals("isCubicWorld")) { + newRoot.put(tag); + } + } + Files.createDirectories(dir); + + nbtOut.writeTag(new CompoundTag(root.getName(), newRoot)); + + Utils.copyEverythingExcept(srcPath, srcPath, dir, file -> + file.toString().contains("level.dat") || + Dimensions.getDimensions().stream().anyMatch(dim -> + srcPath.resolve(dim.getDirectory()).resolve("region2d").equals(file) + || srcPath.resolve(dim.getDirectory()).resolve("region3d").equals(file) + ), + f -> { + } // TODO: counting files + ); + } + }); + } +} diff --git a/src/main/java/cubicchunks/converter/lib/anvil2cc/AnvilChunkData.java b/src/main/java/cubicchunks/converter/lib/convert/data/AnvilChunkData.java similarity index 98% rename from src/main/java/cubicchunks/converter/lib/anvil2cc/AnvilChunkData.java rename to src/main/java/cubicchunks/converter/lib/convert/data/AnvilChunkData.java index 6744744..941835e 100644 --- a/src/main/java/cubicchunks/converter/lib/anvil2cc/AnvilChunkData.java +++ b/src/main/java/cubicchunks/converter/lib/convert/data/AnvilChunkData.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package cubicchunks.converter.lib.anvil2cc; +package cubicchunks.converter.lib.convert.data; import cubicchunks.converter.lib.Dimension; import cubicchunks.regionlib.impl.MinecraftChunkLocation; diff --git a/src/main/java/cubicchunks/converter/lib/anvil2cc/ConvertedCubicChunksData.java b/src/main/java/cubicchunks/converter/lib/convert/data/CubicChunksColumnData.java similarity index 88% rename from src/main/java/cubicchunks/converter/lib/anvil2cc/ConvertedCubicChunksData.java rename to src/main/java/cubicchunks/converter/lib/convert/data/CubicChunksColumnData.java index 95d211c..e7854c2 100644 --- a/src/main/java/cubicchunks/converter/lib/anvil2cc/ConvertedCubicChunksData.java +++ b/src/main/java/cubicchunks/converter/lib/convert/data/CubicChunksColumnData.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package cubicchunks.converter.lib.anvil2cc; +package cubicchunks.converter.lib.convert.data; import cubicchunks.converter.lib.Dimension; import cubicchunks.regionlib.impl.EntryLocation2D; @@ -30,14 +30,14 @@ import java.util.Map; import java.util.Objects; -public class ConvertedCubicChunksData { +public class CubicChunksColumnData { private final Dimension dimension; private final EntryLocation2D position; private final ByteBuffer columnData; private final Map cubeData; - public ConvertedCubicChunksData(Dimension dimension, EntryLocation2D position, ByteBuffer columnData, + public CubicChunksColumnData(Dimension dimension, EntryLocation2D position, ByteBuffer columnData, Map cubeData) { this.dimension = dimension; this.position = position; @@ -68,10 +68,10 @@ public Map getCubeData() { if (o == null || getClass() != o.getClass()) { return false; } - ConvertedCubicChunksData that = (ConvertedCubicChunksData) o; + CubicChunksColumnData that = (CubicChunksColumnData) o; return dimension.equals(that.dimension) && position.equals(that.position) && - columnData.equals(that.columnData) && + Objects.equals(columnData, that.columnData) && cubeData.equals(that.cubeData); } @@ -80,7 +80,7 @@ public Map getCubeData() { } @Override public String toString() { - return "ConvertedCubicChunksData{" + + return "CubicChunksColumnData{" + "dimension='" + dimension + '\'' + ", position=" + position + ", columnData=" + columnData + diff --git a/src/main/java/cubicchunks/converter/lib/convert/data/MultilayerAnvilChunkData.java b/src/main/java/cubicchunks/converter/lib/convert/data/MultilayerAnvilChunkData.java new file mode 100644 index 0000000..e8390e8 --- /dev/null +++ b/src/main/java/cubicchunks/converter/lib/convert/data/MultilayerAnvilChunkData.java @@ -0,0 +1,38 @@ +/* + * This file is part of CubicChunksConverter, licensed under the MIT License (MIT). + * + * Copyright (c) 2017 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package cubicchunks.converter.lib.convert.data; + +import java.util.Map; + +public class MultilayerAnvilChunkData { + private final Map worlds; + + public MultilayerAnvilChunkData(Map worlds) { + this.worlds = worlds; + } + + public Map getWorlds() { + return worlds; + } +} diff --git a/src/main/java/cubicchunks/converter/lib/convert/io/AnvilChunkReader.java b/src/main/java/cubicchunks/converter/lib/convert/io/AnvilChunkReader.java new file mode 100644 index 0000000..6b93b02 --- /dev/null +++ b/src/main/java/cubicchunks/converter/lib/convert/io/AnvilChunkReader.java @@ -0,0 +1,93 @@ +/* + * This file is part of CubicChunksConverter, licensed under the MIT License (MIT). + * + * Copyright (c) 2017 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package cubicchunks.converter.lib.convert.io; + +import static cubicchunks.converter.lib.util.Utils.interruptibleConsumer; +import static cubicchunks.regionlib.impl.save.MinecraftSaveSection.MinecraftRegionType.MCA; +import static java.nio.file.Files.exists; + +import cubicchunks.converter.lib.Dimension; +import cubicchunks.converter.lib.convert.data.AnvilChunkData; +import cubicchunks.converter.lib.util.UncheckedInterruptedException; +import cubicchunks.regionlib.impl.save.MinecraftSaveSection; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Consumer; + +public class AnvilChunkReader extends BaseMinecraftReader { + + private final Thread loadThread; + + public AnvilChunkReader(Path srcDir) { + super(srcDir, (dim, path) -> exists(getDimensionPath(dim, path)) ? MinecraftSaveSection.createAt(getDimensionPath(dim, path), MCA) : null); + loadThread = Thread.currentThread(); + } + + private static Path getDimensionPath(Dimension d, Path worldDir) { + if (!d.getDirectory().isEmpty()) { + worldDir = worldDir.resolve(d.getDirectory()); + } + return worldDir.resolve("region"); + } + + @Override public void countInputChunks(Runnable increment) throws IOException { + try { + doCountChunks(increment); + } catch (UncheckedInterruptedException ex) { + // return + } + } + + private void doCountChunks(Runnable increment) throws IOException, UncheckedInterruptedException { + for (MinecraftSaveSection save : saves.values()) { + save.forAllKeys(interruptibleConsumer(loc -> increment.run())); + } + } + + @Override public void loadChunks(Consumer consumer) throws IOException { + try { + doLoadChunks(consumer); + } catch (UncheckedInterruptedException ex) { + // return + } + } + + private void doLoadChunks(Consumer consumer) throws IOException, UncheckedInterruptedException { + for (Map.Entry entry : saves.entrySet()) { + if (Thread.interrupted()) { + return; + } + MinecraftSaveSection vanillaSave = entry.getValue(); + Dimension d = entry.getKey(); + vanillaSave.forAllKeys(interruptibleConsumer(mcPos -> consumer.accept(new AnvilChunkData(d, mcPos, vanillaSave.load(mcPos).orElse(null))))); + } + } + + @Override public void stop() { + loadThread.interrupt(); + } + +} diff --git a/src/main/java/cubicchunks/converter/lib/convert/io/AnvilChunkWriter.java b/src/main/java/cubicchunks/converter/lib/convert/io/AnvilChunkWriter.java new file mode 100644 index 0000000..e3318e8 --- /dev/null +++ b/src/main/java/cubicchunks/converter/lib/convert/io/AnvilChunkWriter.java @@ -0,0 +1,100 @@ +/* + * This file is part of CubicChunksConverter, licensed under the MIT License (MIT). + * + * Copyright (c) 2017 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package cubicchunks.converter.lib.convert.io; + + +import static cubicchunks.converter.lib.util.Utils.propagateExceptions; +import static cubicchunks.regionlib.impl.save.MinecraftSaveSection.MinecraftRegionType.MCA; + +import cubicchunks.converter.lib.Dimension; +import cubicchunks.converter.lib.convert.ChunkDataWriter; +import cubicchunks.converter.lib.convert.data.AnvilChunkData; +import cubicchunks.converter.lib.convert.data.MultilayerAnvilChunkData; +import cubicchunks.converter.lib.util.Utils; +import cubicchunks.regionlib.impl.save.MinecraftSaveSection; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class AnvilChunkWriter implements ChunkDataWriter { + + private Path dstPath; + private Map> saves = new ConcurrentHashMap<>(); + + public AnvilChunkWriter(Path dstPath) { + this.dstPath = dstPath; + } + + @Override public void accept(MultilayerAnvilChunkData data) throws IOException { + for (Map.Entry entry : data.getWorlds().entrySet()) { + int layerY = entry.getKey(); + AnvilChunkData chunk = entry.getValue(); + Map layer = saves.computeIfAbsent(layerY, i -> new HashMap<>()); + MinecraftSaveSection save = layer.computeIfAbsent(chunk.getDimension(), propagateExceptions(dim -> { + Path regionDir = getDimensionPath(entry.getValue().getDimension(), dstPath.resolve(dirName(layerY))); + Utils.createDirectories(regionDir); + return MinecraftSaveSection.createAt(regionDir, MCA); + })); + save.save(chunk.getPosition(), chunk.getData()); + } + } + + static Path getDimensionPath(Dimension d, Path worldDir) { + if (!d.getDirectory().isEmpty()) { + worldDir = worldDir.resolve(d.getDirectory()); + } + return worldDir.resolve("region"); + } + + + static String dirName(int layerY) { + return String.format("layer [%d, %d)", layerY * 256, (layerY + 1) * 256); + } + + @Override public void discardData() throws IOException { + Utils.rm(dstPath); + } + + @Override public void close() throws Exception { + boolean exception = false; + for (Map saves : this.saves.values()) { + for (Closeable save : saves.values()) { + try { + save.close(); + } catch (IOException e) { + e.printStackTrace(); + exception = true; + } + } + } + + if (exception) { + throw new IOException(); + } + } +} diff --git a/src/main/java/cubicchunks/converter/lib/anvil2cc/AnvilChunkReader.java b/src/main/java/cubicchunks/converter/lib/convert/io/BaseMinecraftReader.java similarity index 52% rename from src/main/java/cubicchunks/converter/lib/anvil2cc/AnvilChunkReader.java rename to src/main/java/cubicchunks/converter/lib/convert/io/BaseMinecraftReader.java index 5caebf6..1e0ac51 100644 --- a/src/main/java/cubicchunks/converter/lib/anvil2cc/AnvilChunkReader.java +++ b/src/main/java/cubicchunks/converter/lib/convert/io/BaseMinecraftReader.java @@ -21,69 +21,37 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package cubicchunks.converter.lib.anvil2cc; - -import static cubicchunks.regionlib.impl.save.MinecraftSaveSection.MinecraftRegionType.MCA; +package cubicchunks.converter.lib.convert.io; import cubicchunks.converter.lib.Dimension; import cubicchunks.converter.lib.Dimensions; import cubicchunks.converter.lib.convert.ChunkDataReader; -import cubicchunks.regionlib.impl.save.MinecraftSaveSection; +import java.io.Closeable; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; -import java.util.function.Consumer; - -public class AnvilChunkReader implements ChunkDataReader { - - private static final BiFunction LOCATION_FUNC_SRC = (d, p) -> { - if (!d.getDirectory().isEmpty()) { - p = p.resolve(d.getDirectory()); - } - return p.resolve("region"); - }; - private final Map saves = new ConcurrentHashMap<>(); - private final Path srcDir; +public abstract class BaseMinecraftReader implements ChunkDataReader { + protected final Path srcDir; + protected final Map saves; - public AnvilChunkReader(Path srcDir) { + public BaseMinecraftReader(Path srcDir, BiFunction pathToSave) { this.srcDir = srcDir; + this.saves = new ConcurrentHashMap<>(); for (Dimension d : Dimensions.getDimensions()) { - Path srcLoc = LOCATION_FUNC_SRC.apply(d, srcDir); - if (!Files.exists(srcLoc)) { - continue; - } - - MinecraftSaveSection vanillaSave = MinecraftSaveSection.createAt(LOCATION_FUNC_SRC.apply(d, srcDir), MCA); - saves.put(d, vanillaSave); - } - } - - @Override public void countInputChunks(Runnable increment) throws IOException { - for (MinecraftSaveSection save : saves.values()) { - save.forAllKeys(loc -> increment.run()); - } - } - - @Override public void loadChunks(Consumer consumer) throws IOException { - for (Dimension d : Dimensions.getDimensions()) { - Path srcLoc = LOCATION_FUNC_SRC.apply(d, srcDir); - if (!Files.exists(srcLoc)) { - continue; + SAVE save = pathToSave.apply(d, srcDir); + if (save != null) { + saves.put(d, save); } - - MinecraftSaveSection vanillaSave = saves.get(d); - vanillaSave.forAllKeys(mcPos -> consumer.accept(new AnvilChunkData(d, mcPos, vanillaSave.load(mcPos).orElse(null)))); } } @Override public void close() throws Exception { boolean exception = false; - for (MinecraftSaveSection save : saves.values()) { + for (SAVE save : saves.values()) { try { save.close(); } catch (IOException e) { diff --git a/src/main/java/cubicchunks/converter/lib/convert/io/CubicChunkReader.java b/src/main/java/cubicchunks/converter/lib/convert/io/CubicChunkReader.java new file mode 100644 index 0000000..97554a5 --- /dev/null +++ b/src/main/java/cubicchunks/converter/lib/convert/io/CubicChunkReader.java @@ -0,0 +1,157 @@ +/* + * This file is part of CubicChunksConverter, licensed under the MIT License (MIT). + * + * Copyright (c) 2017 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package cubicchunks.converter.lib.convert.io; + +import static cubicchunks.converter.lib.util.Utils.interruptibleConsumer; + +import com.carrotsearch.hppc.IntArrayList; +import com.carrotsearch.hppc.cursors.IntCursor; +import cubicchunks.converter.lib.Dimension; +import cubicchunks.converter.lib.convert.data.CubicChunksColumnData; +import cubicchunks.converter.lib.util.UncheckedInterruptedException; +import cubicchunks.regionlib.impl.EntryLocation2D; +import cubicchunks.regionlib.impl.EntryLocation3D; +import cubicchunks.regionlib.impl.SaveCubeColumns; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +public class CubicChunkReader extends BaseMinecraftReader { + + private final CompletableFuture chunkList = new CompletableFuture<>(); + private final Thread loadThread; + + public CubicChunkReader(Path srcDir) { + super(srcDir, (dim, path) -> Files.exists(getDimensionPath(dim, path)) ? createSave(getDimensionPath(dim, path)) : null); + loadThread = Thread.currentThread(); + } + + private static Path getDimensionPath(Dimension d, Path worldDir) { + if (!d.getDirectory().isEmpty()) { + worldDir = worldDir.resolve(d.getDirectory()); + } + return worldDir; + } + + @Override public void countInputChunks(Runnable increment) throws IOException { + try { + Map> dimensions = doCountChunks(increment); + chunkList.complete(new ChunkList(dimensions)); + } catch (UncheckedInterruptedException ex) { + chunkList.complete(null); + } + } + + private Map> doCountChunks(Runnable increment) throws IOException, UncheckedInterruptedException { + Map> dimensions = new ConcurrentHashMap<>(); + for (Map.Entry entry : saves.entrySet()) { + SaveCubeColumns save = entry.getValue(); + Dimension dim = entry.getKey(); + Map chunks = dimensions.computeIfAbsent(dim, p -> new ConcurrentHashMap<>()); + save.getSaveSection3D().forAllKeys(interruptibleConsumer(loc -> { + + EntryLocation2D loc2d = new EntryLocation2D(loc.getEntryX(), loc.getEntryZ()); + chunks.computeIfAbsent(loc2d, l -> { + increment.run(); + return new IntArrayList(); + }).add(loc.getEntryY()); + })); + } + return dimensions; + } + + @Override public void loadChunks(Consumer consumer) throws IOException, InterruptedException { + try { + ChunkList list = chunkList.get(); + if (list == null) { + return; // counting interrupted + } + doLoadChunks(consumer, list); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private void doLoadChunks(Consumer consumer, ChunkList list) throws IOException { + for (Map.Entry> dimEntry : list.getChunks().entrySet()) { + if (Thread.interrupted()) { + return; + } + Dimension dim = dimEntry.getKey(); + SaveCubeColumns save = saves.get(dim); + for (Map.Entry chunksEntry : dimEntry.getValue().entrySet()) { + if (Thread.interrupted()) { + return; + } + EntryLocation2D pos2d = chunksEntry.getKey(); + IntArrayList yCoords = chunksEntry.getValue(); + ByteBuffer column = save.load(pos2d).orElse(null); + Map cubes = new ConcurrentHashMap<>(); + for (IntCursor yCursor : yCoords) { + if (Thread.interrupted()) { + return; + } + int y = yCursor.value; + ByteBuffer cube = save.load(new EntryLocation3D(pos2d.getEntryX(), y, pos2d.getEntryZ())).orElseThrow( + () -> new IllegalStateException("Expected cube at " + pos2d + " at y=" + y + " in dimension " + dim)); + cubes.put(y, cube); + } + CubicChunksColumnData data = new CubicChunksColumnData(dim, pos2d, column, cubes); + consumer.accept(data); + } + } + } + + @Override public void stop() { + loadThread.interrupt(); + } + + private static SaveCubeColumns createSave(Path path) { + try { + return SaveCubeColumns.create(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static class ChunkList { + + private final Map> chunks; + + private ChunkList(Map> chunks) { + this.chunks = chunks; + } + + Map> getChunks() { + return chunks; + } + } +} diff --git a/src/main/java/cubicchunks/converter/lib/anvil2cc/CubicChunkWriter.java b/src/main/java/cubicchunks/converter/lib/convert/io/CubicChunkWriter.java similarity index 84% rename from src/main/java/cubicchunks/converter/lib/anvil2cc/CubicChunkWriter.java rename to src/main/java/cubicchunks/converter/lib/convert/io/CubicChunkWriter.java index 6ce8412..17c7ee6 100644 --- a/src/main/java/cubicchunks/converter/lib/anvil2cc/CubicChunkWriter.java +++ b/src/main/java/cubicchunks/converter/lib/convert/io/CubicChunkWriter.java @@ -21,10 +21,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package cubicchunks.converter.lib.anvil2cc; +package cubicchunks.converter.lib.convert.io; import cubicchunks.converter.lib.Dimension; +import cubicchunks.converter.lib.convert.data.CubicChunksColumnData; import cubicchunks.converter.lib.convert.ChunkDataWriter; +import cubicchunks.converter.lib.util.Utils; import cubicchunks.regionlib.impl.EntryLocation2D; import cubicchunks.regionlib.impl.EntryLocation3D; import cubicchunks.regionlib.impl.SaveCubeColumns; @@ -35,7 +37,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -public class CubicChunkWriter implements ChunkDataWriter { +public class CubicChunkWriter implements ChunkDataWriter { private Path dstPath; private Map saves = new ConcurrentHashMap<>(); @@ -44,7 +46,7 @@ public CubicChunkWriter(Path dstPath) { this.dstPath = dstPath; } - @Override public void accept(ConvertedCubicChunksData data) throws IOException { + @Override public void accept(CubicChunksColumnData data) throws IOException { SaveCubeColumns save = saves.computeIfAbsent(data.getDimension(), dim -> { try { return SaveCubeColumns.create(dstPath.resolve(dim.getDirectory())); @@ -53,12 +55,18 @@ public CubicChunkWriter(Path dstPath) { } }); EntryLocation2D pos = data.getPosition(); - save.save2d(pos, data.getColumnData()); + if (data.getColumnData() != null) { + save.save2d(pos, data.getColumnData()); + } for (Map.Entry entry : data.getCubeData().entrySet()) { save.save3d(new EntryLocation3D(pos.getEntryX(), entry.getKey(), pos.getEntryZ()), entry.getValue()); } } + @Override public void discardData() throws IOException { + Utils.rm(dstPath); + } + @Override public void close() throws Exception { boolean exception = false; for (SaveCubeColumns save : saves.values()) { diff --git a/src/main/java/cubicchunks/converter/lib/util/UncheckedInterruptedException.java b/src/main/java/cubicchunks/converter/lib/util/UncheckedInterruptedException.java new file mode 100644 index 0000000..68e3379 --- /dev/null +++ b/src/main/java/cubicchunks/converter/lib/util/UncheckedInterruptedException.java @@ -0,0 +1,27 @@ +/* + * This file is part of CubicChunksConverter, licensed under the MIT License (MIT). + * + * Copyright (c) 2017 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package cubicchunks.converter.lib.util; + +public class UncheckedInterruptedException extends RuntimeException { +} diff --git a/src/main/java/cubicchunks/converter/lib/Utils.java b/src/main/java/cubicchunks/converter/lib/util/Utils.java similarity index 69% rename from src/main/java/cubicchunks/converter/lib/Utils.java rename to src/main/java/cubicchunks/converter/lib/util/Utils.java index dd4ae6e..5107653 100644 --- a/src/main/java/cubicchunks/converter/lib/Utils.java +++ b/src/main/java/cubicchunks/converter/lib/util/Utils.java @@ -21,11 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package cubicchunks.converter.lib; +package cubicchunks.converter.lib.util; import com.flowpowered.nbt.CompoundTag; import com.flowpowered.nbt.stream.NBTInputStream; import com.flowpowered.nbt.stream.NBTOutputStream; +import cubicchunks.regionlib.util.CheckedConsumer; +import cubicchunks.regionlib.util.CheckedFunction; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -34,13 +36,19 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.ByteBuffer; +import java.nio.file.DirectoryStream; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.Iterator; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.Deflater; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -48,13 +56,44 @@ public class Utils { + /** + * Returns a consumer that checks for interruption, and throws {@link UncheckedInterruptedException} + * if thread is interrupted. + */ + public static CheckedConsumer interruptibleConsumer(CheckedConsumer cons) { + return x -> { + if (Thread.interrupted()) { + throw new UncheckedInterruptedException(); + } + cons.accept(x); + }; + } + + // from one of the replies in https://stackoverflow.com/questions/27644361/how-can-i-throw-checked-exceptions-from-inside-java-8-streams + public static Function propagateExceptions(CheckedFunction func) { + return x -> { + try { + return func.apply(x); + } catch (Throwable t) { + return throwUnchecked(t); + } + }; + } + + @SuppressWarnings("unchecked") + private static T throwUnchecked(Throwable t) throws E { + throw (E) t; + } + // Files.createDirectories doesn't handle symlinks public static void createDirectories(Path dir) throws IOException { if (Files.isDirectory(dir)) { return; } createDirectories(dir.getParent()); - Files.createDirectory(dir); + try { + Files.createDirectory(dir); + } catch (FileAlreadyExistsException ex) {} } public static boolean isValidPath(String text) { @@ -145,19 +184,58 @@ public static CompoundTag readCompressed(InputStream is) throws IOException { return (CompoundTag) new NBTInputStream(data, false).readTag(); } - public static ByteBuffer writeCompressed(CompoundTag tag, boolean compress) throws IOException { + public static CompoundTag readCompressedCC(InputStream is) throws IOException { + BufferedInputStream data = new BufferedInputStream(new GZIPInputStream(is)); + return (CompoundTag) new NBTInputStream(data, false).readTag(); + } + + public static ByteBuffer writeCompressed(CompoundTag tag, boolean prefixFormat) throws IOException { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - NBTOutputStream nbtOut = new NBTOutputStream(new BufferedOutputStream(new GZIPOutputStream(bytes) {{ - if (!compress) { - this.def.setLevel(Deflater.NO_COMPRESSION); - } - }}), false); + if (prefixFormat) { + bytes.write(1); // mark as GZIP + } + NBTOutputStream nbtOut = new NBTOutputStream(new BufferedOutputStream(new GZIPOutputStream(bytes)), false); nbtOut.writeTag(tag); nbtOut.close(); bytes.flush(); return ByteBuffer.wrap(bytes.toByteArray()); } + /** + * Deletes the specified file or directory, recursively + */ + public static void rm(Path toDelete) throws IOException { + if (Files.isDirectory(toDelete)) { + try (Stream files = Files.list(toDelete)) { + for (Path path : files.collect(Collectors.toList())) { + rm(path); + } + } + Files.delete(toDelete); + } else { + Files.delete(toDelete); + } + } + + public static boolean isEmpty(final Path directory) throws IOException { + if (!Files.exists(directory)) { + return true; + } + try(DirectoryStream dirStream = Files.newDirectoryStream(directory)) { + return !dirStream.iterator().hasNext(); + } + } + + public static void forEachDirectory(Path directory, CheckedConsumer consumer) throws E, IOException { + try(DirectoryStream dirStream = Files.newDirectoryStream(directory)) { + for (Path path : dirStream) { + if (Files.isDirectory(path)) { + consumer.accept(path); + } + } + } + } + private enum OS { WINDOWS, MACOS, SOLARIS, LINUX, UNKNOWN; }