From 48376feb89067b321475b276e055bd81afec4974 Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Wed, 5 May 2021 23:18:29 -0700 Subject: [PATCH 01/17] Start working on UI About time I got around to this. This commit begins the work on adding an actual user interface for the application. The interface is greatly inspired by bspsrc, as the process for decompiling from bsp to vmf is pretty similar to the user as converting from vmf to obj. The CLI is still used when running from the terminal, but the new gui is used whenever double clicking on the jar. I believe it's functional right now, but there's still a lot of testing, and currently the log only appears in the terminal, regardless of if it actually exists or not. That's the next thing to work on. Also this commit changes the way parameters are computed. I'm not sure this is the final format, but it sure is nicer than the original. (I hate Java UI programming) --- pom.xml | 8 +- src/main/java/com/lathrum/VMF2OBJ/Job.java | 13 + .../com/lathrum/VMF2OBJ/SimpleFormatter.java | 13 + .../VMF2OBJ/{App.java => VMF2OBJ.java} | 113 +-- .../com/lathrum/VMF2OBJ/VMF2OBJLauncher.java | 14 + .../com/lathrum/VMF2OBJ/cli/VMF2OBJCLI.java | 72 ++ .../VMF2OBJ/dataStructure/map/Side.java | 4 +- .../VMF2OBJ/dataStructure/map/VMF.java | 6 +- .../VMF2OBJ/dataStructure/texture/VMT.java | 8 +- .../VMF2OBJ/fileStructure/VMFFileEntry.java | 38 + .../com/lathrum/VMF2OBJ/gui/FileDrop.java | 827 ++++++++++++++++++ .../lathrum/VMF2OBJ/gui/TextAreaHandler.java | 43 + .../com/lathrum/VMF2OBJ/gui/VMF2OBJFrame.java | 590 +++++++++++++ 13 files changed, 1656 insertions(+), 93 deletions(-) create mode 100644 src/main/java/com/lathrum/VMF2OBJ/Job.java create mode 100644 src/main/java/com/lathrum/VMF2OBJ/SimpleFormatter.java rename src/main/java/com/lathrum/VMF2OBJ/{App.java => VMF2OBJ.java} (93%) create mode 100644 src/main/java/com/lathrum/VMF2OBJ/VMF2OBJLauncher.java create mode 100644 src/main/java/com/lathrum/VMF2OBJ/cli/VMF2OBJCLI.java create mode 100644 src/main/java/com/lathrum/VMF2OBJ/fileStructure/VMFFileEntry.java create mode 100644 src/main/java/com/lathrum/VMF2OBJ/gui/FileDrop.java create mode 100644 src/main/java/com/lathrum/VMF2OBJ/gui/TextAreaHandler.java create mode 100644 src/main/java/com/lathrum/VMF2OBJ/gui/VMF2OBJFrame.java diff --git a/pom.xml b/pom.xml index e665bab..9778ca4 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ - com.lathrum.VMF2OBJ.App + com.lathrum.VMF2OBJ.VMF2OBJLauncher @@ -79,10 +79,10 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 + 3.8.1 - 1.7 - 1.7 + 1.8 + 1.8 diff --git a/src/main/java/com/lathrum/VMF2OBJ/Job.java b/src/main/java/com/lathrum/VMF2OBJ/Job.java new file mode 100644 index 0000000..05ec130 --- /dev/null +++ b/src/main/java/com/lathrum/VMF2OBJ/Job.java @@ -0,0 +1,13 @@ +package com.lathrum.VMF2OBJ; + +import java.nio.file.Path; +import java.util.ArrayList; +import com.lathrum.VMF2OBJ.fileStructure.VMFFileEntry; + +public class Job { + + public VMFFileEntry file; + public ArrayList resourcePaths = new ArrayList(); + public boolean SuppressWarnings; + public boolean skipTools; +} \ No newline at end of file diff --git a/src/main/java/com/lathrum/VMF2OBJ/SimpleFormatter.java b/src/main/java/com/lathrum/VMF2OBJ/SimpleFormatter.java new file mode 100644 index 0000000..bdc490b --- /dev/null +++ b/src/main/java/com/lathrum/VMF2OBJ/SimpleFormatter.java @@ -0,0 +1,13 @@ +package com.lathrum.VMF2OBJ; + +import java.util.logging.*; + +public class SimpleFormatter extends Formatter { + @Override + public String format(LogRecord record) { + StringBuilder sb = new StringBuilder(); + sb.append(record.getLevel()).append(": "); + sb.append(record.getMessage()).append("\n"); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/lathrum/VMF2OBJ/App.java b/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJ.java similarity index 93% rename from src/main/java/com/lathrum/VMF2OBJ/App.java rename to src/main/java/com/lathrum/VMF2OBJ/VMF2OBJ.java index 7cdf54f..59d090d 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/App.java +++ b/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJ.java @@ -1,21 +1,20 @@ package com.lathrum.VMF2OBJ; import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.zip.*; -import javax.imageio.ImageIO; import com.google.gson.*; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.net.URI; import java.net.URISyntaxException; -import java.awt.image.BufferedImage; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; import me.tongfei.progressbar.ProgressBar; import me.tongfei.progressbar.ProgressBarBuilder; import me.tongfei.progressbar.ProgressBarStyle; -import org.apache.commons.cli.*; import com.lathrum.VMF2OBJ.fileStructure.*; import com.lathrum.VMF2OBJ.dataStructure.*; @@ -23,10 +22,11 @@ import com.lathrum.VMF2OBJ.dataStructure.model.*; import com.lathrum.VMF2OBJ.dataStructure.texture.*; -public class App { +public class VMF2OBJ { public static Gson gson = new Gson(); public static Process proc; + public static Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); public static String VTFLibPath; public static String CrowbarLibPath; public static boolean quietMode = false; @@ -60,7 +60,7 @@ public static void extractLibraries(String dir) throws URISyntaxException { URI fileURI; try { - uri = App.class.getProtectionDomain().getCodeSource().getLocation().toURI(); + uri = VMF2OBJ.class.getProtectionDomain().getCodeSource().getLocation().toURI(); } catch (Exception e) { System.err.println("Failed to get executable's location, do you have permissions?"); System.err.println(e.toString()); @@ -342,10 +342,10 @@ public static void printProgressBar(String text) { /** * Convert Source .vmf files to generic .obj - * @param args Launch options for the program. Read the first couple lines of main to see valid inputs + * @param job Job object that represents the configuration for the conversion * @throws Exception Unhandled exception */ - public static void main(String[] args) throws Exception { + public static void main(Job job) throws Exception { // The General outline of the program is as follows: // Read Geometry @@ -359,17 +359,10 @@ public static void main(String[] args) throws Exception { // Write Models // Write Materials // Clean Up - - CommandLineParser parser = new DefaultParser(); - Options options = new Options(); - options.addOption("h", "help", false, "Show this message"); - options.addOption("e", "externalPath", true, "Semi-colon separated list of folders for external custom content (such as materials or models)"); - options.addOption("q", "quiet", false, "Suppress warnings"); - options.addOption("t", "tools", false, "Ignore tool brushes"); // Load app version final Properties properties = new Properties(); - properties.load(App.class.getClassLoader().getResourceAsStream("project.properties")); + properties.load(VMF2OBJ.class.getClassLoader().getResourceAsStream("project.properties")); appVersion = properties.getProperty("version"); Scanner in; @@ -379,58 +372,17 @@ public static void main(String[] args) throws Exception { HashMap modelCache = new HashMap(); PrintWriter objFile; PrintWriter materialFile; - String outPath = ""; - String objName = ""; - String matLibName = ""; + String outPath = job.file.outPath; + String objName = job.file.objFile.toString(); + String matLibName = job.file.mtlFile.toString(); + quietMode = job.SuppressWarnings; + ignoreTools = job.skipTools; final File tempDir; - // Prepare Arguments - try { - outPath = args[1]; - objName = outPath + ".obj"; - matLibName = outPath + ".mtl"; - - // parse the command line arguments - CommandLine cmd = parser.parse(options, args); - if (cmd.hasOption("h") || args[0].charAt(0) == '-' || args[1].charAt(0) == '-' || args[2].charAt(0) == '-') { - HelpFormatter formatter = new HelpFormatter(); - formatter.printHelp("vmf2obj [VMF_FILE] [OUTPUT_FILE] [VPK_PATHS] [args...]", options, false); - System.exit(0); - } - if (cmd.hasOption("e")) { - String[] externalFolders = cmd.getOptionValue("e").split(";"); - for (String path : externalFolders) { - vpkEntries.addAll(addExtraFiles(path, new File(path))); - } - } - if (cmd.hasOption("q")) { - quietMode = true; - } - if (cmd.hasOption("t")) { - ignoreTools = true; - } - } catch (ParseException e) { - System.err.println(e.getMessage()); - HelpFormatter formatter = new HelpFormatter(); - formatter.printHelp("vmf2obj [VMF_FILE] [OUTPUT_FILE] [VPK_PATHS] [args...]", options, false); - System.exit(0); - } catch (Exception e) { - HelpFormatter formatter = new HelpFormatter(); - formatter.printHelp("vmf2obj [VMF_FILE] [OUTPUT_FILE] [VPK_PATHS] [args...]", options, false); - System.exit(0); - } - - // Check for valid arguments - if (Paths.get(outPath).getParent() == null) { - System.err.println("Invalid output file. Make sure it's either an absolute or relative path"); - System.exit(0); - } - // Clean working directory try { deleteRecursiveByExtension(new File(Paths.get(outPath).getParent().resolve("materials").toString()), "vtf"); // Delete unconverted textures } catch (Exception ignored) {} - tempDir = new File(System.getProperty("java.io.tmpdir") + File.separator + "vmf2objtemp"); tempDir.mkdirs(); // When the program shuts down, delete temporary directory @@ -456,23 +408,24 @@ public void run() { // Read VPK // - // Open vpk file - System.out.println("[1/5] Reading VPK file(s)..."); - String[] vpkFiles = args[2].split(";"); - for (String path : vpkFiles) { - - File vpkFile = new File(path); - VPK vpk = new VPK(vpkFile); - try { - vpk.load(); - } catch (Exception e) { - System.err.println("Error while loading vpk file: " + e.getMessage()); - return; - } + // Load resources + System.out.println("[1/5] Reading VPK file(s) and custom content..."); + for (Path path : job.resourcePaths) { + if (Files.isDirectory(path)) { + vpkEntries.addAll(addExtraFiles(path.toString(), path.toFile())); + } else { + VPK vpk = new VPK(path.toFile()); + try { + vpk.load(); + } catch (Exception e) { + System.err.println("Error while loading vpk file: " + e.getMessage()); + return; + } - for (Directory directory : vpk.getDirectories()) { - for (Entry entry : directory.getEntries()) { - vpkEntries.add(entry); + for (Directory directory : vpk.getDirectories()) { + for (Entry entry : directory.getEntries()) { + vpkEntries.add(entry); + } } } } @@ -480,9 +433,9 @@ public void run() { // Read input file String text = ""; try { - text = readFile(args[0]); + text = readFile(job.file.vmfFile.toString()); } catch (IOException e) { - System.err.println("Failed to read file: " + args[0] + ", does file exist?"); + System.err.println("Failed to read file: " + job.file.vmfFile + ", does file exist?"); System.err.println(e.toString()); } // System.out.println(text); @@ -493,7 +446,7 @@ public void run() { directory.mkdirs(); } - in = new Scanner(new File(args[0])); + in = new Scanner(job.file.vmfFile); objFile = new PrintWriter(new FileOutputStream(objName)); materialFile = new PrintWriter(new FileOutputStream(matLibName)); } catch (IOException e) { diff --git a/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJLauncher.java b/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJLauncher.java new file mode 100644 index 0000000..810ffe0 --- /dev/null +++ b/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJLauncher.java @@ -0,0 +1,14 @@ +package com.lathrum.VMF2OBJ; + +import com.lathrum.VMF2OBJ.cli.VMF2OBJCLI; +import com.lathrum.VMF2OBJ.gui.VMF2OBJFrame; + +public class VMF2OBJLauncher { + public static void main(String args[]) throws Exception { + if (System.console() == null) { + VMF2OBJFrame.main(args); + } else { + VMF2OBJCLI.main(args); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/lathrum/VMF2OBJ/cli/VMF2OBJCLI.java b/src/main/java/com/lathrum/VMF2OBJ/cli/VMF2OBJCLI.java new file mode 100644 index 0000000..718240d --- /dev/null +++ b/src/main/java/com/lathrum/VMF2OBJ/cli/VMF2OBJCLI.java @@ -0,0 +1,72 @@ +package com.lathrum.VMF2OBJ.cli; + +import org.apache.commons.cli.*; +import java.io.*; +import java.nio.file.*; +import com.lathrum.VMF2OBJ.Job; +import com.lathrum.VMF2OBJ.VMF2OBJ; +import com.lathrum.VMF2OBJ.fileStructure.VMFFileEntry; + +public class VMF2OBJCLI { + + public static void main(String[] args) throws Exception { + + CommandLineParser parser = new DefaultParser(); + Options options = new Options(); + options.addOption("h", "help", false, "Show this message"); + options.addOption("o", "output", true, "Name of the output files. Defaults to the name of the VMF file"); + options.addOption("r", "resourcePaths", true, + "Semi-colon separated list of VPK files and folders for external custom content (such as materials or models)"); + options.addOption("q", "quiet", false, "Suppress warnings"); + options.addOption("t", "tools", false, "Ignore tool brushes"); + + Job job = new Job(); + + // Prepare Arguments + try { + job.file = new VMFFileEntry(new File(args[0]), args[1]); + + // parse the command line arguments + CommandLine cmd = parser.parse(options, args); + if (cmd.hasOption("h") || args[0].charAt(0) == '-') { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("vmf2obj [VMF_FILE] [args...]", options, false); + System.exit(0); + } + if (cmd.hasOption("o")) { + job.file = new VMFFileEntry(new File(args[0]), cmd.getOptionValue("o")); + } else { + job.file = new VMFFileEntry(new File(args[0])); + } + if (cmd.hasOption("r")) { + String[] resourcePaths = cmd.getOptionValue("r").split(";"); + for (String path : resourcePaths) { + job.resourcePaths.add(Paths.get(path)); + } + } + if (cmd.hasOption("q")) { + job.SuppressWarnings = true; + } + if (cmd.hasOption("t")) { + job.skipTools = true; + } + } catch (ParseException e) { + System.err.println(e.getMessage()); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("vmf2obj [VMF_FILE] [args...]", options, false); + System.exit(0); + } catch (Exception e) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("vmf2obj [VMF_FILE] [args...]", options, false); + System.exit(0); + } + + // Check for valid arguments + if (job.file.objFile.getParent() == null) { + System.err.println("Invalid output file. Make sure it's either an absolute or relative path"); + System.exit(0); + } + + VMF2OBJ.main(job); + } +} \ No newline at end of file diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java index 5989ff4..8775f23 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java @@ -7,7 +7,7 @@ import java.util.LinkedList; import java.util.List; -import com.lathrum.VMF2OBJ.App; +import com.lathrum.VMF2OBJ.VMF2OBJ; import com.lathrum.VMF2OBJ.dataStructure.Plane; import com.lathrum.VMF2OBJ.dataStructure.Vector3; @@ -91,7 +91,7 @@ public int compare(Vector3 o1, Vector3 o2) { } }); - Side newSide = App.gson.fromJson(App.gson.toJson(side, Side.class), Side.class); + Side newSide = VMF2OBJ.gson.fromJson(VMF2OBJ.gson.toJson(side, Side.class), Side.class); newSide.points = IntersectionsList.toArray(new Vector3[IntersectionsList.size()]); diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java index 4c5e4fb..ffab734 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java @@ -5,7 +5,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.lathrum.VMF2OBJ.App; +import com.lathrum.VMF2OBJ.VMF2OBJ; import com.lathrum.VMF2OBJ.dataStructure.Vector3; public class VMF { @@ -240,7 +240,7 @@ public static VMF parseVMF(String text) { text = text.replaceAll(",,", ","); // System.out.println(text); - VMF vmf = App.gson.fromJson(text, VMF.class); + VMF vmf = VMF2OBJ.gson.fromJson(text, VMF.class); return vmf; } @@ -306,7 +306,7 @@ public static VMF parseSolids(VMF vmf) { } j = 0; - Solid solidProxy = App.gson.fromJson(App.gson.toJson(solid, Solid.class), Solid.class); + Solid solidProxy = VMF2OBJ.gson.fromJson(VMF2OBJ.gson.toJson(solid, Solid.class), Solid.class); for (Side side : solidProxy.sides) { Side newSide = Side.completeSide(side, solidProxy); if (newSide != null) { diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java index 1b60c94..0cd481a 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java @@ -3,7 +3,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.lathrum.VMF2OBJ.App; +import com.lathrum.VMF2OBJ.VMF2OBJ; public class VMT { public String name; @@ -49,7 +49,7 @@ public static VMT parseVMT(String text) { // which shouldn't really be used anyways in this case. So we'll give it an // obvious texture // So it can be easily changed - VMT vmt = App.gson.fromJson("{\"basetexture\":\"TOOLS/TOOLSDOTTED\"}", VMT.class); + VMT vmt = VMF2OBJ.gson.fromJson("{\"basetexture\":\"TOOLS/TOOLSDOTTED\"}", VMT.class); return vmt; } } @@ -82,7 +82,7 @@ public static VMT parseVMT(String text) { int endIndex = findClosingBracketMatchIndex(text, startIndex); if (endIndex == -1) // Invalid vmt { - VMT vmt = App.gson.fromJson("{\"basetexture\":\"TOOLS/TOOLSDOTTED\"}", VMT.class); + VMT vmt = VMF2OBJ.gson.fromJson("{\"basetexture\":\"TOOLS/TOOLSDOTTED\"}", VMT.class); return vmt; } text = text.substring(startIndex, endIndex + 1); @@ -106,7 +106,7 @@ public static VMT parseVMT(String text) { text = text.replaceAll("([a-zA-Z_]+dx[6-9])", "\"$1\":"); // Fix fallback shaders // System.out.println(text); - VMT vmt = App.gson.fromJson(text, VMT.class); + VMT vmt = VMF2OBJ.gson.fromJson(text, VMT.class); return vmt; } diff --git a/src/main/java/com/lathrum/VMF2OBJ/fileStructure/VMFFileEntry.java b/src/main/java/com/lathrum/VMF2OBJ/fileStructure/VMFFileEntry.java new file mode 100644 index 0000000..7db16b4 --- /dev/null +++ b/src/main/java/com/lathrum/VMF2OBJ/fileStructure/VMFFileEntry.java @@ -0,0 +1,38 @@ +package com.lathrum.VMF2OBJ.fileStructure; + +import java.io.File; + +public class VMFFileEntry { + + public File vmfFile; + public String outPath; + public File objFile; + public File mtlFile; + + public VMFFileEntry(File vmfFile) { + this.vmfFile = vmfFile; + outPath = replaceExtension(vmfFile, "").toString(); + objFile = replaceExtension(vmfFile, ".obj"); + mtlFile = replaceExtension(vmfFile, ".mtl"); + } + + public VMFFileEntry(File vmfFile, String outputString) { + this.vmfFile = vmfFile; + outPath = outputString; + objFile = new File(outputString + ".obj"); + mtlFile = new File(outputString + ".mtl"); + } + + private static File replaceExtension(File file, String newExt) { + String fileName = file.getName(); + String base = fileName.substring(0, fileName.lastIndexOf('.')); + File parentFile = file.getAbsoluteFile().getParentFile(); + + return new File(parentFile, base + newExt); + } + public void setOutpath(String outPath) { + this.outPath = outPath; + this.objFile = new File(outPath + ".obj"); + this.mtlFile = new File(outPath + ".mtl"); + } +} \ No newline at end of file diff --git a/src/main/java/com/lathrum/VMF2OBJ/gui/FileDrop.java b/src/main/java/com/lathrum/VMF2OBJ/gui/FileDrop.java new file mode 100644 index 0000000..1e2d80b --- /dev/null +++ b/src/main/java/com/lathrum/VMF2OBJ/gui/FileDrop.java @@ -0,0 +1,827 @@ +package com.lathrum.VMF2OBJ.gui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.dnd.*; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; +import java.io.*; +import java.net.URI; +import java.util.ArrayList; +import java.util.EventObject; +import java.util.List; +import java.util.TooManyListenersException; +import java.util.function.*; +import javax.swing.BorderFactory; +import javax.swing.JComponent; +import javax.swing.border.Border; + +/** + * This class makes it easy to drag and drop files from the operating system to + * a Java program. Any java.awt.Component can be dropped onto, but only + * javax.swing.JComponents will indicate the drop event with a changed + * border. + *

+ * To use this class, construct a new FileDrop by passing it the target + * component and a Listener to receive notification when file(s) have + * been dropped. Here is an example: + *

+ *

+ *      JPanel myPanel = new JPanel();
+ *      new FileDrop( myPanel, new FileDrop.Listener()
+ *      {   public void filesDropped( java.io.File[] files )
+ *          {
+ *              // handle file drop
+ *              ...
+ *          }   // end filesDropped
+ *      }); // end FileDrop.Listener
+ * 
+ *

+ * You can specify the border that will appear when files are being dragged by + * calling the constructor with a javax.swing.border.Border. Only + * JComponents will show any indication with a border. + *

+ * You can turn on some debugging features by passing a PrintStream + * object (such as System.out) into the full constructor. A + * null value will result in no extra debugging information being + * output. + *

+ * + *

+ * I'm releasing this code into the Public Domain. Enjoy. + *

+ *

+ * Original author: Robert Harder, rharder@usa.net + *

+ *

+ * 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added. + *

+ * + * @author Robert Harder + * @author rharder@users.sf.net + * @version 1.0.1 + */ +public class FileDrop { + + private transient Border normalBorder; + private transient DropTargetListener dropListener; + /** Discover if the running JVM is modern enough to have drag and drop. */ + private static Boolean supportsDnD; + // Default border color + private static Color defaultBorderColor = new Color(0f, 0f, 1f, 0.25f); + + /** + * Constructs a {@link FileDrop} with a default light-blue border and, if + * c is a {@link java.awt.Container}, recursively sets all elements + * contained within as drop targets, though only the top level container will + * change borders. + * + * @param c Component on which files will be dropped. + * @param listener Listens for filesDropped. + * @since 1.0 + */ + public FileDrop(final Component c, final Consumer listener) { + this(null, // Logging stream + c, // Drop target + BorderFactory.createMatteBorder(2, 2, 2, 2, defaultBorderColor), // Drag border + true, // Recursive + listener); + } // end constructor + + /** + * Constructor with a default border and the option to recursively set drop + * targets. If your component is a java.awt.Container, then each of its + * children components will also listen for drops, though only the parent will + * change borders. + * + * @param c Component on which files will be dropped. + * @param recursive Recursively set children as drop targets. + * @param listener Listens for filesDropped. + * @since 1.0 + */ + public FileDrop(final Component c, final boolean recursive, final Consumer listener) { + this(null, // Logging stream + c, // Drop target + BorderFactory.createMatteBorder(2, 2, 2, 2, defaultBorderColor), // Drag border + recursive, // Recursive + listener); + } // end constructor + + /** + * Constructor with a default border and debugging optionally turned on. With + * Debugging turned on, more status messages will be displayed to out. + * A common way to use this constructor is with System.out or + * System.err. A null value for the parameter out + * will result in no debugging output. + * + * @param out PrintStream to record debugging info or null for no + * debugging. + * @param out + * @param c Component on which files will be dropped. + * @param listener Listens for filesDropped. + * @since 1.0 + */ + public FileDrop(final PrintStream out, final Component c, final Consumer listener) { + this(out, // Logging stream + c, // Drop target + BorderFactory.createMatteBorder(2, 2, 2, 2, defaultBorderColor), false, // Recursive + listener); + } // end constructor + + /** + * Constructor with a default border, debugging optionally turned on and the + * option to recursively set drop targets. If your component is a + * java.awt.Container, then each of its children components will also + * listen for drops, though only the parent will change borders. With Debugging + * turned on, more status messages will be displayed to out. A common + * way to use this constructor is with System.out or + * System.err. A null value for the parameter out + * will result in no debugging output. + * + * @param out PrintStream to record debugging info or null for no + * debugging. + * @param out + * @param c Component on which files will be dropped. + * @param recursive Recursively set children as drop targets. + * @param listener Listens for filesDropped. + * @since 1.0 + */ + public FileDrop(final PrintStream out, final Component c, final boolean recursive, final Consumer listener) { + this(out, // Logging stream + c, // Drop target + BorderFactory.createMatteBorder(2, 2, 2, 2, defaultBorderColor), // Drag border + recursive, // Recursive + listener); + } // end constructor + + /** + * Constructor with a specified border + * + * @param c Component on which files will be dropped. + * @param dragBorder Border to use on JComponent when dragging occurs. + * @param listener Listens for filesDropped. + * @since 1.0 + */ + public FileDrop(final Component c, final Border dragBorder, final Consumer listener) { + this(null, // Logging stream + c, // Drop target + dragBorder, // Drag border + false, // Recursive + listener); + } // end constructor + + /** + * Constructor with a specified border and the option to recursively set drop + * targets. If your component is a java.awt.Container, then each of its + * children components will also listen for drops, though only the parent will + * change borders. + * + * @param c Component on which files will be dropped. + * @param dragBorder Border to use on JComponent when dragging occurs. + * @param recursive Recursively set children as drop targets. + * @param listener Listens for filesDropped. + * @since 1.0 + */ + public FileDrop(final Component c, final Border dragBorder, final boolean recursive, + final Consumer listener) { + this(null, c, dragBorder, recursive, listener); + } // end constructor + + /** + * Constructor with a specified border and debugging optionally turned on. With + * Debugging turned on, more status messages will be displayed to out. + * A common way to use this constructor is with System.out or + * System.err. A null value for the parameter out + * will result in no debugging output. + * + * @param out PrintStream to record debugging info or null for no + * debugging. + * @param c Component on which files will be dropped. + * @param dragBorder Border to use on JComponent when dragging occurs. + * @param listener Listens for filesDropped. + * @since 1.0 + */ + public FileDrop(final PrintStream out, final Component c, final Border dragBorder, final Consumer listener) { + this(out, // Logging stream + c, // Drop target + dragBorder, // Drag border + false, // Recursive + listener); + } // end constructor + + /** + * Full constructor with a specified border and debugging optionally turned on. + * With Debugging turned on, more status messages will be displayed to + * out. A common way to use this constructor is with + * System.out or System.err. A null value for the + * parameter out will result in no debugging output. + * + * @param out PrintStream to record debugging info or null for no + * debugging. + * @param c Component on which files will be dropped. + * @param dragBorder Border to use on JComponent when dragging occurs. + * @param recursive Recursively set children as drop targets. + * @param listener Listens for filesDropped. + * @since 1.0 + */ + public FileDrop(final PrintStream out, final Component c, final Border dragBorder, final boolean recursive, + final Consumer listener) { + + if (supportsDnD()) { // Make a drop listener + dropListener = new DropTargetListener() { + + @Override + public void dragEnter(DropTargetDragEvent evt) { + log(out, "FileDrop: dragEnter event."); + + // Is this an acceptable drag event? + if (isDragOk(out, evt)) { + // If it's a Swing component, set its border + if (c instanceof JComponent) { + JComponent jc = (JComponent) c; + normalBorder = jc.getBorder(); + log(out, "FileDrop: normal border saved."); + jc.setBorder(dragBorder); + log(out, "FileDrop: drag border set."); + } // end if: JComponent + + // Acknowledge that it's okay to enter + // evt.acceptDrag( DnDConstants.ACTION_COPY_OR_MOVE ); + evt.acceptDrag(DnDConstants.ACTION_COPY); + log(out, "FileDrop: event accepted."); + } // end if: drag ok + else { // Reject the drag event + evt.rejectDrag(); + log(out, "FileDrop: event rejected."); + } // end else: drag not ok + } // end dragEnter + + @Override + public void dragOver(DropTargetDragEvent evt) { // This is called continually as long as the mouse is + // over the drag target. + } // end dragOver + + @Override + public void drop(DropTargetDropEvent evt) { + log(out, "FileDrop: drop event."); + try { // Get whatever was dropped + Transferable tr = evt.getTransferable(); + + // Is it a file list? + if (tr.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { + // Say we'll take it. + // evt.acceptDrop ( DnDConstants.ACTION_COPY_OR_MOVE ); + evt.acceptDrop(DnDConstants.ACTION_COPY); + log(out, "FileDrop: file list accepted."); + + // Get a useful list + List fileList = (List) tr.getTransferData(DataFlavor.javaFileListFlavor); + + // Convert list to array + File[] filesTemp = new File[fileList.size()]; + fileList.toArray(filesTemp); + final File[] files = filesTemp; + + // Alert listener to drop. + if (listener != null) { + listener.accept(files); + } + + // Mark that drop is completed. + evt.getDropTargetContext().dropComplete(true); + log(out, "FileDrop: drop complete."); + } // end if: file list + else // this section will check for a reader flavor. + { + // Thanks, Nathan! + // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added. + DataFlavor[] flavors = tr.getTransferDataFlavors(); + boolean handled = false; + for (int zz = 0; zz < flavors.length; zz++) { + if (flavors[zz].isRepresentationClassReader()) { + // Say we'll take it. + // evt.acceptDrop ( DnDConstants.ACTION_COPY_OR_MOVE ); + evt.acceptDrop(DnDConstants.ACTION_COPY); + log(out, "FileDrop: reader accepted."); + + Reader reader = flavors[zz].getReaderForText(tr); + + BufferedReader br = new BufferedReader(reader); + + if (listener != null) { + listener.accept(createFileArray(br, out)); + } + + // Mark that drop is completed. + evt.getDropTargetContext().dropComplete(true); + log(out, "FileDrop: drop complete."); + handled = true; + break; + } + } + if (!handled) { + log(out, "FileDrop: not a file list or reader - abort."); + evt.rejectDrop(); + } + // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added. + } // end else: not a file list + } // end try + catch (IOException io) { + log(out, "FileDrop: IOException - abort:"); + io.printStackTrace(out); + evt.rejectDrop(); + } // end catch IOException + catch (UnsupportedFlavorException ufe) { + log(out, "FileDrop: UnsupportedFlavorException - abort:"); + ufe.printStackTrace(out); + evt.rejectDrop(); + } // end catch: UnsupportedFlavorException + finally { + // If it's a Swing component, reset its border + if (c instanceof JComponent) { + JComponent jc = (JComponent) c; + jc.setBorder(normalBorder); + log(out, "FileDrop: normal border restored."); + } // end if: JComponent + } // end finally + } // end drop + + @Override + public void dragExit(DropTargetEvent evt) { + log(out, "FileDrop: dragExit event."); + // If it's a Swing component, reset its border + if (c instanceof JComponent) { + JComponent jc = (JComponent) c; + jc.setBorder(normalBorder); + log(out, "FileDrop: normal border restored."); + } // end if: JComponent + } // end dragExit + + @Override + public void dropActionChanged(DropTargetDragEvent evt) { + log(out, "FileDrop: dropActionChanged event."); + // Is this an acceptable drag event? + if (isDragOk(out, evt)) { // evt.acceptDrag( DnDConstants.ACTION_COPY_OR_MOVE ); + evt.acceptDrag(DnDConstants.ACTION_COPY); + log(out, "FileDrop: event accepted."); + } // end if: drag ok + else { + evt.rejectDrag(); + log(out, "FileDrop: event rejected."); + } // end else: drag not ok + } // end dropActionChanged + }; // end DropTargetListener + + // Make the component (and possibly children) drop targets + makeDropTarget(out, c, recursive); + } // end if: supports dnd + else { + log(out, "FileDrop: Drag and drop is not supported with this JVM"); + } // end else: does not support DnD + } // end constructor + + private static boolean supportsDnD() { // Static Boolean + if (supportsDnD == null) { + try { + Class.forName("java.awt.dnd.DnDConstants"); + supportsDnD = true; + } // end try + catch (Exception e) { + supportsDnD = false; + } // end catch + } // end if: first time through + return supportsDnD.booleanValue(); + } // end supportsDnD + // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added. + + private static String ZERO_CHAR_STRING = "" + (char) 0; + + private static File[] createFileArray(BufferedReader bReader, PrintStream out) { + try { + List list = new ArrayList<>(); + String line; + while ((line = bReader.readLine()) != null) { + try { + // kde seems to append a 0 char to the end of the reader + if (ZERO_CHAR_STRING.equals(line)) { + continue; + } + + File file = new File(new URI(line)); + list.add(file); + } catch (Exception ex) { + log(out, "Error with " + line + ": " + ex.getMessage()); + } + } + + return (File[]) list.toArray(new File[list.size()]); + } catch (IOException ex) { + log(out, "FileDrop: IOException"); + } + return new File[0]; + } + // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added. + + private void makeDropTarget(final PrintStream out, final Component c, boolean recursive) { + // Make drop target + final DropTarget dt = new DropTarget(); + try { + dt.addDropTargetListener(dropListener); + } // end try + catch (TooManyListenersException e) { + e.printStackTrace(); + log(out, "FileDrop: Drop will not work due to previous error. Do you have another listener attached?"); + } // end catch + + // Listen for hierarchy changes and remove the drop target when the parent gets + // cleared out. + c.addHierarchyListener(new HierarchyListener() { + + @Override + public void hierarchyChanged(HierarchyEvent evt) { + log(out, "FileDrop: Hierarchy changed."); + Component parent = c.getParent(); + if (parent == null) { + c.setDropTarget(null); + log(out, "FileDrop: Drop target cleared from component."); + } // end if: null parent + else { + new DropTarget(c, dropListener); + log(out, "FileDrop: Drop target added to component."); + } // end else: parent not null + } // end hierarchyChanged + }); // end hierarchy listener + if (c.getParent() != null) { + new DropTarget(c, dropListener); + } + + if (recursive && (c instanceof Container)) { + // Get the container + Container cont = (Container) c; + + // Get it's components + Component[] comps = cont.getComponents(); + + // Set it's components as listeners also + for (int i = 0; i < comps.length; i++) { + makeDropTarget(out, comps[i], recursive); + } + } // end if: recursively set components as listener + } // end dropListener + + /** Determine if the dragged data is a file list. */ + private boolean isDragOk(final PrintStream out, final DropTargetDragEvent evt) { + boolean ok = false; + + // Get data flavors being dragged + DataFlavor[] flavors = evt.getCurrentDataFlavors(); + + // See if any of the flavors are a file list + int i = 0; + while (!ok && i < flavors.length) { + // BEGIN 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added. + // Is the flavor a file list? + final DataFlavor curFlavor = flavors[i]; + if (curFlavor.equals(DataFlavor.javaFileListFlavor) || curFlavor.isRepresentationClassReader()) { + ok = true; + } + // END 2007-09-12 Nathan Blomquist -- Linux (KDE/Gnome) support added. + i++; + } // end while: through flavors + + // If logging is enabled, show data flavors + if (out != null) { + if (flavors.length == 0) { + log(out, "FileDrop: no data flavors."); + } + for (i = 0; i < flavors.length; i++) { + log(out, flavors[i].toString()); + } + } // end if: logging enabled + + return ok; + } // end isDragOk + + /** Outputs message to out if it's not null. */ + private static void log(PrintStream out, String message) { // Log message if requested + if (out != null) { + out.println(message); + } + } // end log + + /** + * Removes the drag-and-drop hooks from the component and optionally from the + * all children. You should call this if you add and remove components after + * you've set up the drag-and-drop. This will recursively unregister all + * components contained within c if c is a + * {@link java.awt.Container}. + * + * @param c The component to unregister as a drop target + * @since 1.0 + */ + public static boolean remove(Component c) { + return remove(null, c, true); + } // end remove + + /** + * Removes the drag-and-drop hooks from the component and optionally from the + * all children. You should call this if you add and remove components after + * you've set up the drag-and-drop. + * + * @param out Optional {@link java.io.PrintStream} for logging drag and + * drop messages + * @param c The component to unregister + * @param recursive Recursively unregister components within a container + * @since 1.0 + */ + public static boolean remove(PrintStream out, Component c, boolean recursive) { // Make sure we support dnd. + if (supportsDnD()) { + log(out, "FileDrop: Removing drag-and-drop hooks."); + c.setDropTarget(null); + if (recursive && (c instanceof Container)) { + Component[] comps = ((Container) c).getComponents(); + for (int i = 0; i < comps.length; i++) { + remove(out, comps[i], recursive); + } + return true; + } // end if: recursive + else { + return false; + } + } // end if: supports DnD + else { + return false; + } + } // end remove + + /* ******** I N N E R C L A S S ******** */ + /** + * This is the event that is passed to the {@link FileDropListener#filesDropped + * filesDropped(...)} method in your {@link FileDropListener} when files are + * dropped onto a registered drop target. + * + *

+ * I'm releasing this code into the Public Domain. Enjoy. + *

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 1.2 + */ + public static class Event extends EventObject { + + private File[] files; + + /** + * Constructs an {@link Event} with the array of files that were dropped and the + * {@link FileDrop} that initiated the event. + * + * @param files The array of files that were dropped + * @source The event source + * @since 1.1 + */ + public Event(File[] files, Object source) { + super(source); + this.files = files; + } // end constructor + + /** + * Returns an array of files that were dropped on a registered drop target. + * + * @return array of files that were dropped + * @since 1.1 + */ + public File[] getFiles() { + return files; + } // end getFiles + } // end inner class Event + + /* ******** I N N E R C L A S S ******** */ + /** + * At last an easy way to encapsulate your custom objects for dragging and + * dropping in your Java programs! When you need to create a + * {@link java.awt.datatransfer.Transferable} object, use this class to wrap + * your object. For example: + * + *
+	 * 
+	 *      ...
+	 *      MyCoolClass myObj = new MyCoolClass();
+	 *      Transferable xfer = new TransferableObject( myObj );
+	 *      ...
+	 * 
+	 * 
+ * + * Or if you need to know when the data was actually dropped, like when you're + * moving data out of a list, say, you can use the + * {@link TransferableObject.Fetcher} inner class to return your object Just in + * Time. For example: + * + *
+	 * 
+	 *      ...
+	 *      final MyCoolClass myObj = new MyCoolClass();
+	 *
+	 *      TransferableObject.Fetcher fetcher = new TransferableObject.Fetcher()
+	 *      {   public Object getObject(){ return myObj; }
+	 *      }; // end fetcher
+	 *
+	 *      Transferable xfer = new TransferableObject( fetcher );
+	 *      ...
+	 * 
+	 * 
+ * + * The {@link java.awt.datatransfer.DataFlavor} associated with + * {@link TransferableObject} has the representation class + * net.iharder.dnd.TransferableObject.class and MIME type + * application/x-net.iharder.dnd.TransferableObject. This data flavor + * is accessible via the static {@link #DATA_FLAVOR} property. + * + * + *

+ * I'm releasing this code into the Public Domain. Enjoy. + *

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 1.2 + */ + public static class TransferableObject implements Transferable { + + /** + * The MIME type for {@link #DATA_FLAVOR} is + * application/x-net.iharder.dnd.TransferableObject. + * + * @since 1.1 + */ + public final static String MIME_TYPE = "application/x-net.iharder.dnd.TransferableObject"; + /** + * The default {@link java.awt.datatransfer.DataFlavor} for + * {@link TransferableObject} has the representation class + * net.iharder.dnd.TransferableObject.class and the MIME type + * application/x-net.iharder.dnd.TransferableObject. + * + * @since 1.1 + */ + public final static DataFlavor DATA_FLAVOR = new DataFlavor(FileDrop.TransferableObject.class, MIME_TYPE); + private Fetcher fetcher; + private Object data; + private DataFlavor customFlavor; + + /** + * Creates a new {@link TransferableObject} that wraps data. Along + * with the {@link #DATA_FLAVOR} associated with this class, this creates a + * custom data flavor with a representation class determined from + * data.getClass() and the MIME type + * application/x-net.iharder.dnd.TransferableObject. + * + * @param data The data to transfer + * @since 1.1 + */ + public TransferableObject(Object data) { + this.data = data; + this.customFlavor = new DataFlavor(data.getClass(), MIME_TYPE); + } // end constructor + + /** + * Creates a new {@link TransferableObject} that will return the object that is + * returned by fetcher. No custom data flavor is set other than the + * default {@link #DATA_FLAVOR}. + * + * @see Fetcher + * @param fetcher The {@link Fetcher} that will return the data object + * @since 1.1 + */ + public TransferableObject(Fetcher fetcher) { + this.fetcher = fetcher; + } // end constructor + + /** + * Creates a new {@link TransferableObject} that will return the object that is + * returned by fetcher. Along with the {@link #DATA_FLAVOR} + * associated with this class, this creates a custom data flavor with a + * representation class dataClass and the MIME type + * application/x-net.iharder.dnd.TransferableObject. + * + * @see Fetcher + * @param dataClass The {@link java.lang.Class} to use in the custom data flavor + * @param fetcher The {@link Fetcher} that will return the data object + * @since 1.1 + */ + public TransferableObject(Class dataClass, Fetcher fetcher) { + this.fetcher = fetcher; + this.customFlavor = new DataFlavor(dataClass, MIME_TYPE); + } // end constructor + + /** + * Returns the custom {@link java.awt.datatransfer.DataFlavor} associated with + * the encapsulated object or null if the {@link Fetcher} constructor + * was used without passing a {@link java.lang.Class}. + * + * @return The custom data flavor for the encapsulated object + * @since 1.1 + */ + public DataFlavor getCustomDataFlavor() { + return customFlavor; + } // end getCustomDataFlavor + + /* ******** T R A N S F E R A B L E M E T H O D S ******** */ + /** + * Returns a two- or three-element array containing first the custom data + * flavor, if one was created in the constructors, second the default + * {@link #DATA_FLAVOR} associated with {@link TransferableObject}, and third + * the {@link java.awt.datatransfer.DataFlavor.stringFlavor}. + * + * @return An array of supported data flavors + * @since 1.1 + */ + @Override + public DataFlavor[] getTransferDataFlavors() { + if (customFlavor != null) { + return new DataFlavor[] { customFlavor, DATA_FLAVOR, DataFlavor.stringFlavor }; // end flavors array + } else { + return new DataFlavor[] { DATA_FLAVOR, DataFlavor.stringFlavor }; // end flavors array + } + } // end getTransferDataFlavors + + /** + * Returns the data encapsulated in this {@link TransferableObject}. If the + * {@link Fetcher} constructor was used, then this is when the + * {@link Fetcher#getObject getObject()} method will be called. If the requested + * data flavor is not supported, then the {@link Fetcher#getObject getObject()} + * method will not be called. + * + * @param flavor The data flavor for the data to return + * @return The dropped data + * @since 1.1 + */ + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + // Native object + if (flavor.equals(DATA_FLAVOR)) { + return fetcher == null ? data : fetcher.getObject(); + } + + // String + if (flavor.equals(DataFlavor.stringFlavor)) { + return fetcher == null ? data.toString() : fetcher.getObject().toString(); + } + + // We can't do anything else + throw new UnsupportedFlavorException(flavor); + } // end getTransferData + + /** + * Returns true if flavor is one of the supported flavors. + * Flavors are supported using the equals(...) method. + * + * @param flavor The data flavor to check + * @return Whether or not the flavor is supported + * @since 1.1 + */ + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + // Native object + if (flavor.equals(DATA_FLAVOR)) { + return true; + } + + // String + if (flavor.equals(DataFlavor.stringFlavor)) { + return true; + } + + // We can't do anything else + return false; + } // end isDataFlavorSupported + + /* ******** I N N E R I N T E R F A C E F E T C H E R ******** */ + /** + * Instead of passing your data directly to the {@link TransferableObject} + * constructor, you may want to know exactly when your data was received in case + * you need to remove it from its source (or do anyting else to it). When the + * {@link #getTransferData getTransferData(...)} method is called on the + * {@link TransferableObject}, the {@link Fetcher}'s {@link #getObject + * getObject()} method will be called. + * + * @author Robert Harder + * @copyright 2001 + * @version 1.1 + * @since 1.1 + */ + public static interface Fetcher { + + /** + * Return the object being encapsulated in the {@link TransferableObject}. + * + * @return The dropped object + * @since 1.1 + */ + public abstract Object getObject(); + } // end inner interface Fetcher + } // end class TransferableObject +} // end class FileDrop diff --git a/src/main/java/com/lathrum/VMF2OBJ/gui/TextAreaHandler.java b/src/main/java/com/lathrum/VMF2OBJ/gui/TextAreaHandler.java new file mode 100644 index 0000000..be1870a --- /dev/null +++ b/src/main/java/com/lathrum/VMF2OBJ/gui/TextAreaHandler.java @@ -0,0 +1,43 @@ + +package com.lathrum.VMF2OBJ.gui; + +import java.util.logging.ErrorManager; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import javax.swing.JTextArea; + +public class TextAreaHandler extends Handler { + + private JTextArea out; + + public TextAreaHandler(JTextArea out) { + this.out = out; + } + + @Override + public void publish(LogRecord record) { + String msg; + try { + msg = getFormatter().format(record); + } catch (Exception ex) { + reportError(null, ex, ErrorManager.FORMAT_FAILURE); + return; + } + + try { + out.append(msg); + // make sure the last line is always visible + out.setCaretPosition(out.getDocument().getLength()); + } catch (Exception ex) { + reportError(null, ex, ErrorManager.WRITE_FAILURE); + } + } + + @Override + public void flush() { + } + + @Override + public void close() throws SecurityException { + } +} \ No newline at end of file diff --git a/src/main/java/com/lathrum/VMF2OBJ/gui/VMF2OBJFrame.java b/src/main/java/com/lathrum/VMF2OBJ/gui/VMF2OBJFrame.java new file mode 100644 index 0000000..5860d6e --- /dev/null +++ b/src/main/java/com/lathrum/VMF2OBJ/gui/VMF2OBJFrame.java @@ -0,0 +1,590 @@ +package com.lathrum.VMF2OBJ.gui; + +import java.io.File; +import java.util.logging.Logger; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Properties; + +import javax.swing.DefaultListModel; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.ListModel; +import javax.swing.UIManager; +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileNameExtensionFilter; + +import com.lathrum.VMF2OBJ.Job; +import com.lathrum.VMF2OBJ.VMF2OBJ; +import com.lathrum.VMF2OBJ.SimpleFormatter; +import com.lathrum.VMF2OBJ.fileStructure.VMFFileEntry; + +public class VMF2OBJFrame extends javax.swing.JFrame { + + private Job job = new Job(); + private Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + private TextAreaHandler logHandler; + private DefaultListModel listResourcesModel = new DefaultListModel<>(); + + /** + * @param args the command line arguments + */ + public static void main(String args[]) { + + // Set the system look and feel + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception ex) { + } + + // Create and display the form + java.awt.EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + new VMF2OBJFrame().setVisible(true); + } + }); + } + + public VMF2OBJFrame() { + initComponents(); + initComponentsCustom(); + + // VMF filedropper + new FileDrop(fileInput, files -> { + for (File file : files) { + if (file.getName().toLowerCase().endsWith(".vmf")) { + job.file = new VMFFileEntry(file); + fileInput.setText(file.toString()); + } + } + validateConvertButtonEnabled(); + }); + + // VPK and custom content file dropper + new FileDrop(listResources, files -> { + for (File file : files) { + if (file.isDirectory()) { + listResourcesModel.addElement(file.toPath()); + } else if (file.getName().toLowerCase().endsWith(".vpk")) { + listResourcesModel.addElement(file.toPath()); + } + } + validateConvertButtonEnabled(); + }); + } + + public ListModel getFilesModel() { + return listResourcesModel; + } + + public void validateConvertButtonEnabled() { + boolean value = job.file != null && !listResourcesModel.isEmpty(); + buttonConvert.setEnabled(value); + } + + public void setButtonsEnabled(boolean value) { + buttonConvert.setEnabled(value); + } + + private File openFileDialog(File defaultFile, FileFilter filter) { + JFileChooser fc = new JFileChooser() { + + @Override + public void approveSelection() { + File file = getSelectedFile(); + if (file != null && !file.exists()) { + showFileNotFoundDialog(); + return; + } + super.approveSelection(); + } + + private void showFileNotFoundDialog() { + JOptionPane.showMessageDialog(this, "The selected file doesn't exist."); + } + }; + fc.setFileFilter(filter); + + if (defaultFile != null) { + fc.setSelectedFile(defaultFile); + } else { + // use user.dir as default directory + try { + fc.setSelectedFile(new File(System.getProperty("user.dir"))); + } catch (Exception ex) { + } + } + + // show open file dialog + int option = fc.showOpenDialog(this); + + if (option != JFileChooser.APPROVE_OPTION) { + return null; + } + + return fc.getSelectedFile(); + } + + private File[] openFilesDialog(File defaultFile, FileFilter filter) { + JFileChooser fc = new JFileChooser() { + + @Override + public void approveSelection() { + File file = getSelectedFile(); + if (file != null && !file.exists()) { + showFileNotFoundDialog(); + return; + } + super.approveSelection(); + } + + private void showFileNotFoundDialog() { + JOptionPane.showMessageDialog(this, "The selected file doesn't exist."); + } + }; + fc.setMultiSelectionEnabled(true); + fc.setFileFilter(filter); + + if (defaultFile != null) { + fc.setSelectedFile(defaultFile); + } else { + // use user.dir as default directory + try { + fc.setSelectedFile(new File(System.getProperty("user.dir"))); + } catch (Exception ex) { + } + } + + // show open file dialog + int option = fc.showOpenDialog(this); + + if (option != JFileChooser.APPROVE_OPTION) { + return null; + } + + return fc.getSelectedFiles(); + } + + private File saveFileDialog(File defaultFile) { + JFileChooser fc = new JFileChooser() { + + @Override + public void approveSelection() { + File file = getSelectedFile(); + File objFile = new File(file.getAbsolutePath() + ".obj"); + File mtlFile = new File(file.getAbsolutePath() + ".mtl"); + if ((objFile != null && objFile.exists() && !askOverwrite(objFile)) + || (mtlFile != null && mtlFile.exists() && !askOverwrite(mtlFile))) { + return; + } + super.approveSelection(); + } + + private boolean askOverwrite(File file) { + String title = "Overwriting " + file.getPath(); + String message = "File " + file.getName() + " already exists.\n" + "Do you like to replace it?"; + + int choice = JOptionPane.showConfirmDialog(this, message, title, JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE); + + return choice == JOptionPane.OK_OPTION; + } + }; + fc.setMultiSelectionEnabled(false); + fc.setSelectedFile(defaultFile); + + // show save file dialog + int option = fc.showSaveDialog(this); + + if (option != JFileChooser.APPROVE_OPTION) { + return null; + } + + return fc.getSelectedFile(); + } + + private File selectDirectoryDialog(File defaultFile) { + JFileChooser fc = new JFileChooser(); + fc.setMultiSelectionEnabled(false); + fc.setAcceptAllFileFilterUsed(false); + fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + if (defaultFile != null) { + fc.setSelectedFile(defaultFile); + } else { + // use user.dir as default directory + try { + fc.setSelectedFile(new File(System.getProperty("user.dir"))); + } catch (Exception ex) { + } + } + + // show dir selection dialog + int option = fc.showOpenDialog(this); + + if (option != JFileChooser.APPROVE_OPTION) { + return null; + } + + return fc.getSelectedFile(); + } + + private File openVMFFileDialog(File vmfFile) { + return openFileDialog(vmfFile, new FileNameExtensionFilter("Source engine map file (.vmf)", "vmf")); + } + + private File[] openVPKFileDialog(File vpkFile) { + return openFilesDialog(vpkFile, new FileNameExtensionFilter("Valve Pak file (.vpk)", "vpk")); + } + + private File saveVmfFileDialog(File vmfFile) { + return saveFileDialog(vmfFile); + } + + /** + * Start the conversion in a new thread. + */ + private void startConversion() { + new Thread() { + + @Override + public void run() { + + job.resourcePaths = new ArrayList(); + Enumeration entries = listResourcesModel.elements(); + while (entries.hasMoreElements()) { + Path entry = entries.nextElement(); + job.resourcePaths.add(entry); + } + + // deactivate buttons + setButtonsEnabled(false); + + try { + VMF2OBJ.main(job); + } catch (Exception e) { + System.out.println("Fatal error: " + e.toString()); + } finally { + // activate buttons + setButtonsEnabled(true); + } + } + }.start(); + } + + private void initComponentsCustom() { + // Load app version + final Properties properties = new Properties(); + try { + properties.load(VMF2OBJ.class.getClassLoader().getResourceAsStream("project.properties")); + } catch (Exception ignored) { + } + setTitle("VMF2OBJ " + properties.getProperty("version")); + + // try { + // URL iconUrl = getClass().getResource("resources/icon.png"); + // Image icon = Toolkit.getDefaultToolkit().createImage(iconUrl); + // setIconImage(icon); + // logFrame.setIconImage(icon); + // } catch (Exception ex) { + // } + } + + private void initComponents() { + + tabbedPaneOptions = new javax.swing.JTabbedPane(); + panelFiles = new javax.swing.JPanel(); + labelVMFFile = new javax.swing.JLabel(); + fileInput = new javax.swing.JTextField(); + buttonSelect = new javax.swing.JButton(); + labelResources = new javax.swing.JLabel(); + scrollResources = new javax.swing.JScrollPane(); + listResources = new javax.swing.JList(); + buttonAdd = new javax.swing.JButton(); + buttonAddFolder = new javax.swing.JButton(); + buttonRemove = new javax.swing.JButton(); + buttonRemoveAll = new javax.swing.JButton(); + labelDnDTip = new javax.swing.JLabel(); + panelSettings = new javax.swing.JPanel(); + checkBoxQuietMode = new javax.swing.JCheckBox(); + checkBoxSkipToolBrushes = new javax.swing.JCheckBox(); + buttonConvert = new javax.swing.JButton(); + logScrollPane = new javax.swing.JScrollPane(); + logTextArea = new javax.swing.JTextArea(); + + setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); + setLocationByPlatform(true); + setSize(500, 500); + + listResources.setModel(getFilesModel()); + scrollResources.setViewportView(listResources); + + fileInput.setEditable(false); + buttonSelect.setText("Select Map"); + buttonSelect.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + buttonSelectActionPerformed(evt); + } + }); + + buttonAdd.setText("Add"); + buttonAdd.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + buttonAddActionPerformed(evt); + } + }); + + buttonAddFolder.setText("Add Folder"); + buttonAddFolder.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + buttonAddFolderActionPerformed(evt); + } + }); + + buttonRemove.setText("Remove"); + buttonRemove.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + buttonRemoveActionPerformed(evt); + } + }); + + buttonRemoveAll.setText("Remove all"); + buttonRemoveAll.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + buttonRemoveAllActionPerformed(evt); + } + }); + + labelVMFFile.setText("VMF file to convert:"); + labelResources.setText("List of resources (VPK files and external resources):"); + labelDnDTip.setText("Tip: drag and drop files/folders on the boxes above"); + + javax.swing.GroupLayout panelFilesLayout = new javax.swing.GroupLayout(panelFiles); + panelFiles.setLayout(panelFilesLayout); + panelFilesLayout + .setHorizontalGroup(panelFilesLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(panelFilesLayout.createSequentialGroup().addContainerGap().addGroup(panelFilesLayout + .createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING).addComponent(labelVMFFile) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, panelFilesLayout.createSequentialGroup() + .addComponent(fileInput, javax.swing.GroupLayout.DEFAULT_SIZE, 199, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED).addComponent(buttonSelect, + javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(labelResources) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, panelFilesLayout.createSequentialGroup() + .addComponent(scrollResources, javax.swing.GroupLayout.DEFAULT_SIZE, 199, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(panelFilesLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addComponent(buttonAdd, javax.swing.GroupLayout.DEFAULT_SIZE, + javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(buttonAddFolder, javax.swing.GroupLayout.DEFAULT_SIZE, + javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(buttonRemove, javax.swing.GroupLayout.DEFAULT_SIZE, + javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(buttonRemoveAll, javax.swing.GroupLayout.DEFAULT_SIZE, + javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addComponent(labelDnDTip)).addContainerGap())); + panelFilesLayout.setVerticalGroup(panelFilesLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(panelFilesLayout.createSequentialGroup().addContainerGap().addComponent(labelVMFFile) + .addGroup(panelFilesLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(fileInput, javax.swing.GroupLayout.DEFAULT_SIZE, 22, 22) + .addGroup(panelFilesLayout.createSequentialGroup().addComponent(buttonSelect))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED).addComponent(labelResources) + .addGroup(panelFilesLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(scrollResources, javax.swing.GroupLayout.DEFAULT_SIZE, 158, Short.MAX_VALUE) + .addGroup(panelFilesLayout.createSequentialGroup().addComponent(buttonAdd) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED).addComponent(buttonAddFolder) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED).addComponent(buttonRemove) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED).addComponent(buttonRemoveAll))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED).addComponent(labelDnDTip) + .addContainerGap())); + + tabbedPaneOptions.addTab("Files", panelFiles); + + checkBoxQuietMode.setText("Quiet Mode"); + checkBoxQuietMode.setToolTipText("Suppress Warnings"); + checkBoxQuietMode.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + checkBoxQuietModeActionPerformed(evt); + } + }); + + checkBoxSkipToolBrushes.setText("Skip Tool Brushes"); + checkBoxSkipToolBrushes.setToolTipText("Any Tool brushes will not be included in the converted OBJ file"); + checkBoxSkipToolBrushes.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + checkBoxSkipToolBrushesActionPerformed(evt); + } + }); + + javax.swing.GroupLayout panelTexturesLayout = new javax.swing.GroupLayout(panelSettings); + panelSettings.setLayout(panelTexturesLayout); + panelTexturesLayout + .setHorizontalGroup(panelTexturesLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(panelTexturesLayout.createSequentialGroup().addContainerGap() + .addGroup(panelTexturesLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(checkBoxSkipToolBrushes).addComponent(checkBoxQuietMode)))); + panelTexturesLayout + .setVerticalGroup(panelTexturesLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(panelTexturesLayout.createSequentialGroup().addContainerGap().addComponent(checkBoxQuietMode) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(checkBoxSkipToolBrushes))); + + tabbedPaneOptions.addTab("Settings", panelSettings); + + buttonConvert.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N + buttonConvert.setEnabled(false); + buttonConvert.setText("Convert"); + buttonConvert.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + buttonConvertActionPerformed(evt); + } + }); + + logScrollPane.setBorder(javax.swing.BorderFactory.createTitledBorder("Log")); + + logTextArea.setEditable(false); + logTextArea.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N + logTextArea.setRows(1); + logTextArea.setDisabledTextColor(new java.awt.Color(0, 0, 0)); + logScrollPane.setViewportView(logTextArea); + logHandler = new TextAreaHandler(logTextArea); + logHandler.setFormatter(new SimpleFormatter()); + logger.addHandler(logHandler); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout + .setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup( + layout.createSequentialGroup().addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(tabbedPaneOptions, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 151, + Short.MAX_VALUE) + .addComponent(buttonConvert)) + .addComponent(logScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 662, Short.MAX_VALUE)) + .addContainerGap())); + layout + .setVerticalGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup().addContainerGap() + .addComponent(tabbedPaneOptions).addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE).addComponent(buttonConvert)) + .addComponent(logScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 662, Short.MAX_VALUE) + .addContainerGap())); + } + + private void buttonSelectActionPerformed(java.awt.event.ActionEvent evt) { + File vmfFile = new File(fileInput.getText()); + + File newvmfFile = openVMFFileDialog(vmfFile); + + if (newvmfFile == null) { + return; + } + + fileInput.setText(newvmfFile.getAbsolutePath()); + job.file = new VMFFileEntry(newvmfFile); + + validateConvertButtonEnabled(); + } + + private void buttonAddActionPerformed(java.awt.event.ActionEvent evt) { + File vpkFile = null; + + if (listResourcesModel.size() == 1) { + vpkFile = listResourcesModel.firstElement().toFile(); + } + + File[] vpkFiles = openVPKFileDialog(vpkFile); + + if (vpkFiles == null) { + return; + } + + for (File file : vpkFiles) { + listResourcesModel.addElement(file.toPath()); + } + + validateConvertButtonEnabled(); + } + + private void buttonAddFolderActionPerformed(java.awt.event.ActionEvent evt) { + File vpkFile = null; + + if (listResourcesModel.size() == 1) { + vpkFile = listResourcesModel.firstElement().toFile(); + } + + File dir = selectDirectoryDialog(vpkFile); + + if (dir == null) { + return; + } + + listResourcesModel.addElement(dir.toPath()); + + validateConvertButtonEnabled(); + } + + private void buttonRemoveActionPerformed(java.awt.event.ActionEvent evt) { + int[] selected = listResources.getSelectedIndices(); + listResources.clearSelection(); + + for (int index : selected) { + listResourcesModel.remove(index); + } + + validateConvertButtonEnabled(); + } + + private void buttonRemoveAllActionPerformed(java.awt.event.ActionEvent evt) { + listResourcesModel.clear(); + buttonConvert.setEnabled(false); + } + + private void checkBoxQuietModeActionPerformed(java.awt.event.ActionEvent evt) { + job.SuppressWarnings = checkBoxQuietMode.isSelected(); + } + + private void checkBoxSkipToolBrushesActionPerformed(java.awt.event.ActionEvent evt) { + job.skipTools = checkBoxSkipToolBrushes.isSelected(); + } + + private void buttonConvertActionPerformed(java.awt.event.ActionEvent evt) { + VMFFileEntry entry = job.file; + File vmfFile = saveVmfFileDialog(new File(entry.outPath)); + + if (vmfFile == null) { + return; + } + + entry.setOutpath(vmfFile.getAbsolutePath()); + + startConversion(); + } + + private javax.swing.JButton buttonAdd; + private javax.swing.JButton buttonAddFolder; + private javax.swing.JButton buttonConvert; + private javax.swing.JButton buttonRemove; + private javax.swing.JButton buttonRemoveAll; + private javax.swing.JCheckBox checkBoxQuietMode; + private javax.swing.JCheckBox checkBoxSkipToolBrushes; + private javax.swing.JLabel labelDnDTip; + private javax.swing.JList listResources; + private javax.swing.JPanel panelFiles; + private javax.swing.JPanel panelSettings; + private javax.swing.JLabel labelVMFFile; + private javax.swing.JTextField fileInput; + private javax.swing.JLabel labelResources; + private javax.swing.JScrollPane scrollResources; + private javax.swing.JButton buttonSelect; + private javax.swing.JTabbedPane tabbedPaneOptions; + private javax.swing.JScrollPane logScrollPane; + private javax.swing.JTextArea logTextArea; +} \ No newline at end of file From 67eee9b9e094e246360dab9604234f0544adf26a Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Sat, 26 Jun 2021 17:43:56 -0700 Subject: [PATCH 02/17] Rework logging to work with UI and have levels of importance Now both the terminal and UI versions of the app will get logging information. Theoretically the UI is fully functional now. --- pom.xml | 7 +- .../com/lathrum/VMF2OBJ/SimpleFormatter.java | 25 ++- .../java/com/lathrum/VMF2OBJ/VMF2OBJ.java | 142 +++++++++--------- .../com/lathrum/VMF2OBJ/VMF2OBJLauncher.java | 13 ++ .../VMF2OBJ/dataStructure/map/Side.java | 3 +- .../VMF2OBJ/dataStructure/map/VMF.java | 4 +- .../VMF2OBJ/dataStructure/model/QC.java | 5 +- .../VMF2OBJ/dataStructure/texture/VMT.java | 2 +- .../lathrum/VMF2OBJ/gui/TextAreaHandler.java | 11 +- .../com/lathrum/VMF2OBJ/gui/VMF2OBJFrame.java | 13 +- 10 files changed, 137 insertions(+), 88 deletions(-) diff --git a/pom.xml b/pom.xml index 9778ca4..8c010d5 100644 --- a/pom.xml +++ b/pom.xml @@ -29,13 +29,18 @@ me.tongfei progressbar - 0.7.4 + 0.9.1 commons-cli commons-cli 1.4 + + commons-logging + commons-logging + 1.2 + diff --git a/src/main/java/com/lathrum/VMF2OBJ/SimpleFormatter.java b/src/main/java/com/lathrum/VMF2OBJ/SimpleFormatter.java index bdc490b..9093b7a 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/SimpleFormatter.java +++ b/src/main/java/com/lathrum/VMF2OBJ/SimpleFormatter.java @@ -5,9 +5,26 @@ public class SimpleFormatter extends Formatter { @Override public String format(LogRecord record) { - StringBuilder sb = new StringBuilder(); - sb.append(record.getLevel()).append(": "); - sb.append(record.getMessage()).append("\n"); - return sb.toString(); + String output = ""; + String level = record.getLevel().toString(); + String message = record.getMessage(); + + if (record.getLevel() == Level.INFO) { + if (message.endsWith("/s")) { + // If progress bar, put on same line + output = "\r" + message; + } else { + // If INFO, don't put logging level + output = message + "\n"; + } + } else { + if (!(record.getLevel() == Level.WARNING && VMF2OBJ.quietMode)) { + // Dont print warning messages in quiet mode + // [LEVEL]: [MESSAGE] + output = level + ": " + message + "\n"; + } + } + + return output; } } \ No newline at end of file diff --git a/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJ.java b/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJ.java index 59d090d..32c3e37 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJ.java +++ b/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJ.java @@ -12,6 +12,8 @@ import java.net.URISyntaxException; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; + +import me.tongfei.progressbar.DelegatingProgressBarConsumer; import me.tongfei.progressbar.ProgressBar; import me.tongfei.progressbar.ProgressBarBuilder; import me.tongfei.progressbar.ProgressBarStyle; @@ -62,8 +64,8 @@ public static void extractLibraries(String dir) throws URISyntaxException { try { uri = VMF2OBJ.class.getProtectionDomain().getCodeSource().getLocation().toURI(); } catch (Exception e) { - System.err.println("Failed to get executable's location, do you have permissions?"); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to get executable's location, do you have permissions?"); + logger.log(Level.SEVERE, e.toString()); } try { @@ -83,8 +85,8 @@ public static void extractLibraries(String dir) throws URISyntaxException { } zipFile.close(); } catch (Exception e) { - System.err.println("Failed to extract tools, do you have permissions?"); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to extract tools, do you have permissions?"); + logger.log(Level.SEVERE, e.toString()); } } @@ -220,7 +222,7 @@ public static int getEntryIndexByPath(ArrayList object, String path) { if (object != null && object.get(i).getFullPath().equalsIgnoreCase(path)) { return i; } - // else{System.out.println(object.get(i).getFullPath());} + // else{logger.log(Level.FINE, object.get(i).getFullPath());} } return -1; } @@ -237,7 +239,7 @@ public static ArrayList getEntryIndiciesByPattern(ArrayList obje if (object != null && object.get(i).getFullPath().toLowerCase().contains(pattern.toLowerCase())) { indicies.add(i); } - // else{System.out.println(object.get(i).getFullPath());} + // else{logger.log(Level.FINE, object.get(i).getFullPath());} } return indicies; } @@ -290,7 +292,7 @@ public static ArrayList addExtraFiles(String start, File dir) { if (path.charAt(0) == File.separatorChar) { path = path.substring(1); } - // System.out.println("directory: " + path); + // logger.log(Level.FINE, "directory: " + path); entries.addAll(addExtraFiles(start, file)); } else { String path = file.getCanonicalPath().substring(start.length()); @@ -298,7 +300,7 @@ public static ArrayList addExtraFiles(String start, File dir) { path = path.substring(1); } path = path.replaceAll("\\\\", "/"); - // System.out.println("file: " + path); + // logger.log(Level.FINE, "file: " + path); if (path.lastIndexOf("/") == -1) { entries.add(new FileEntry(file.getName().substring(0, file.getName().lastIndexOf('.')), getFileExtension(file), "", file.toString())); @@ -309,8 +311,8 @@ public static ArrayList addExtraFiles(String start, File dir) { } } } catch (IOException e) { - System.err.println("Failed to load external resources"); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to load external resources"); + logger.log(Level.SEVERE, e.toString()); } return entries; } @@ -337,7 +339,7 @@ public static void printProgressBar(String text) { } catch (IOException ignored) { /* */ } String pad = new String(new char[Math.max(consoleWidth - text.length(), 0)]).replace("\0", " "); - System.out.println("\r" + text + pad); + logger.log(Level.INFO, "\r" + text + pad); } /** @@ -398,18 +400,18 @@ public void run() { try { extractLibraries(tempDir.getAbsolutePath()); } catch (Exception e) { - System.err.println("Failed to extract tools, do you have permissions?"); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to extract tools, do you have permissions?"); + logger.log(Level.SEVERE, e.toString()); } - System.out.println("Starting VMF2OBJ conversion v" + appVersion); + logger.log(Level.INFO, "Starting VMF2OBJ conversion v" + appVersion); // // Read VPK // // Load resources - System.out.println("[1/5] Reading VPK file(s) and custom content..."); + logger.log(Level.INFO, "[1/5] Reading VPK file(s) and custom content..."); for (Path path : job.resourcePaths) { if (Files.isDirectory(path)) { vpkEntries.addAll(addExtraFiles(path.toString(), path.toFile())); @@ -418,7 +420,7 @@ public void run() { try { vpk.load(); } catch (Exception e) { - System.err.println("Error while loading vpk file: " + e.getMessage()); + logger.log(Level.SEVERE, "Error while loading vpk file: " + e.getMessage()); return; } @@ -435,10 +437,10 @@ public void run() { try { text = readFile(job.file.vmfFile.toString()); } catch (IOException e) { - System.err.println("Failed to read file: " + job.file.vmfFile + ", does file exist?"); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to read file: " + job.file.vmfFile + ", does file exist?"); + logger.log(Level.SEVERE, e.toString()); } - // System.out.println(text); + // logger.log(Level.FINE, text); try { File directory = new File(new File(outPath).getParent()); @@ -450,7 +452,7 @@ public void run() { objFile = new PrintWriter(new FileOutputStream(objName)); materialFile = new PrintWriter(new FileOutputStream(matLibName)); } catch (IOException e) { - System.err.println("Error while opening file: " + e.getMessage()); + logger.log(Level.SEVERE, "Error while opening file: " + e.getMessage()); return; } @@ -458,7 +460,7 @@ public void run() { // Read Geometry // - System.out.println("[2/5] Reading geometry..."); + logger.log(Level.INFO, "[2/5] Reading geometry..."); VMF vmf = VMF.parseVMF(text); vmf = VMF.parseSolids(vmf); @@ -475,7 +477,7 @@ public void run() { int vertexOffset = 1; int vertexTextureOffset = 1; int vertexNormalOffset = 1; - System.out.println("[3/5] Writing brushes..."); + logger.log(Level.INFO, "[3/5] Writing brushes..."); objFile.println("# Decompiled with VMF2OBJ v" + appVersion + " by Dylancyclone\n"); materialFile.println("# Decompiled with VMF2OBJ v" + appVersion + " by Dylancyclone\n"); @@ -483,7 +485,7 @@ public void run() { + matLibName.substring(formatPath(matLibName).lastIndexOf(File.separatorChar) + 1, matLibName.length())); if (vmf.solids != null) { // There are no brushes in this VMF - pbb = new ProgressBarBuilder().setStyle(ProgressBarStyle.ASCII).setTaskName("Writing Brushes...").showSpeed(); + pbb = new ProgressBarBuilder().setStyle(ProgressBarStyle.ASCII).setTaskName("Writing Brushes...").setConsumer(new DelegatingProgressBarConsumer(logger::info)).showSpeed(); for (Solid solid : ProgressBar.wrap(Arrays.asList(vmf.solids), pbb)) { verticies.clear(); faces.clear(); @@ -512,8 +514,8 @@ public void run() { //Get adjacent points by going around counter-clockwise Vector3 ad = side.points[(startIndex + 1) % 4].subtract(side.points[startIndex]); Vector3 ab = side.points[(startIndex + 3) % 4].subtract(side.points[startIndex]); - // System.out.println(ad); - // System.out.println(ab); + // logger.log(Level.FINE, ad); + // logger.log(Level.FINE, ab); for (int i = 0; i < side.dispinfo.normals.length; i++) { // rows for (int j = 0; j < side.dispinfo.normals[0].length; j++) { // columns Vector3 point = side.points[startIndex] @@ -559,8 +561,8 @@ public void run() { } VMTText = new String(vpkEntries.get(index).readData()); } catch (IOException e) { - System.out.println("Failed to read material: " + el); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to read material: " + el); + logger.log(Level.SEVERE, e.toString()); } try { @@ -583,7 +585,7 @@ public void run() { } int index = getEntryIndexByPath(vpkEntries, "materials/" + vmt.basetexture + ".vtf"); - // System.out.println(index); + // logger.log(Level.FINE, index); if (index != -1) { File materialOutPath = new File(outPath); materialOutPath = new File( @@ -595,8 +597,8 @@ public void run() { directory.mkdirs(); } } catch (Exception e) { - System.out.println("Failed to create directory: " + materialOutPath.getParent()); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to create directory: " + materialOutPath.getParent()); + logger.log(Level.SEVERE, e.toString()); } try { vpkEntries.get(index).extract(materialOutPath); @@ -626,23 +628,23 @@ public void run() { width = TargaReader.getWidth(fileContent); height = TargaReader.getHeight(fileContent); } catch (Exception e) { - System.out.println("Cant read Material: " + materialOutPath); - // System.out.println(e); + logger.log(Level.WARNING, "Cant read Material: " + materialOutPath); + // logger.log(Level.SEVERE, e); } - // System.out.println("Adding Material: "+ el); + // logger.log(Level.FINE, "Adding Material: "+ el); textures.add(new Texture(el, vmt.basetexture, materialOutPath.toString(), width, height)); } catch (Exception e) { - System.err.println("Failed to extract material: " + vmt.basetexture); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to extract material: " + vmt.basetexture); + logger.log(Level.SEVERE, e.toString()); } if (vmt.bumpmap != null) { // If the material has a bump map associated with it if (vmt.bumpmap.endsWith(".vtf")) { vmt.bumpmap = vmt.bumpmap.substring(0, vmt.bumpmap.lastIndexOf('.')); // snip the extension } - // System.out.println("Bump found on "+vmt.basetexture+": "+vmt.bumpmap); + // logger.log(Level.FINE, "Bump found on "+vmt.basetexture+": "+vmt.bumpmap); int bumpMapIndex = getEntryIndexByPath(vpkEntries, "materials/" + vmt.bumpmap + ".vtf"); - // System.out.println(bumpMapIndex); + // logger.log(Level.FINE, bumpMapIndex); if (bumpMapIndex != -1) { File bumpMapOutPath = new File(outPath); bumpMapOutPath = new File(formatPath( @@ -654,8 +656,8 @@ public void run() { directory.mkdirs(); } } catch (Exception e) { - System.out.println("Failed to create directory: " + materialOutPath.getParent()); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to create directory: " + materialOutPath.getParent()); + logger.log(Level.SEVERE, e.toString()); } try { vpkEntries.get(bumpMapIndex).extract(bumpMapOutPath); @@ -670,8 +672,8 @@ public void run() { while ((reader.readLine()) != null) {} proc.waitFor(); } catch (Exception e) { - System.err.println("Failed to extract bump material: " + vmt.bumpmap); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to extract bump material: " + vmt.bumpmap); + logger.log(Level.SEVERE, e.toString()); } } } @@ -698,7 +700,7 @@ public void run() { int textureIndex = getTextureIndexByName(textures, el); if (textureIndex == -1) { // But this is a new material textureIndex = getTextureIndexByFileName(textures, vmt.basetexture); - // System.out.println("Adding Material: "+ el); + // logger.log(Level.FINE, "Adding Material: "+ el); textures.add(new Texture(el, vmt.basetexture, materialOutPath.toString(), textures.get(textureIndex).width, textures.get(textureIndex).height)); @@ -866,10 +868,11 @@ public void run() { // Process Entities // - System.out.println("[4/5] Processing entities..."); + logger.log(Level.INFO, ""); // Newline to make sure progressbar stays visible + logger.log(Level.INFO, "[4/5] Processing entities..."); if (vmf.entities != null) { // There are no entities in this VMF - pbb = new ProgressBarBuilder().setStyle(ProgressBarStyle.ASCII).setTaskName("Processing entities...").showSpeed(); + pbb = new ProgressBarBuilder().setStyle(ProgressBarStyle.ASCII).setTaskName("Processing entities...").setConsumer(new DelegatingProgressBarConsumer(logger::info)).showSpeed(); for (Entity entity : ProgressBar.wrap(Arrays.asList(vmf.entities), pbb)) { if (entity.classname.contains("prop_")) { // If the entity is a prop if (entity.model == null) { @@ -911,14 +914,14 @@ public void run() { directory.mkdirs(); } } catch (Exception e) { - System.out.println("Failed to create directory: " + fileOutPath.getParent()); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to create directory: " + fileOutPath.getParent()); + logger.log(Level.SEVERE, e.toString()); } try { vpkEntries.get(index).extract(fileOutPath); } catch (Exception e) { - System.err.println("Failed to extract: " + fileOutPath); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to extract: " + fileOutPath); + logger.log(Level.SEVERE, e.toString()); } } } @@ -932,7 +935,7 @@ public void run() { // BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream())); // String line = ""; // while ((line = reader.readLine()) != null) { - // System.out.println(line); + // logger.log(Level.FINE, line); // } BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream())); while ((reader.readLine()) != null) {} @@ -944,7 +947,7 @@ public void run() { qcText = readFile(tempDir + File.separator + modelWithoutExtension + ".qc"); } catch (IOException e) { //This will be caught by detecting a blank string - // System.out.println("Exception Occured: " + e.toString()); + // logger.log(Level.SEVERE, "Exception Occured: " + e.toString()); } if (qcText.matches("")) { try { @@ -953,7 +956,7 @@ public void run() { qcText = readFile(tempDir + File.separator + modelWithoutExtension + File.separator + entity.modelName + ".qc"); } catch (IOException e) { //This will be caught by detecting a blank string - // System.out.println("Exception Occured: " + e.toString()); + // logger.log(Level.SEVERE, "Exception Occured: " + e.toString()); } if (qcText.matches("")) { printProgressBar("Error: Could not find QC file for model, skipping: " + entity.model); @@ -1047,8 +1050,8 @@ public void run() { } // Could not find it VMTText = new String(vpkEntries.get(index).readData()); } catch (IOException e) { - System.out.println("Failed to read material: " + el); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to read material: " + el); + logger.log(Level.SEVERE, e.toString()); } if (!VMTText.isEmpty()) { break; @@ -1078,7 +1081,7 @@ public void run() { } int index = getEntryIndexByPath(vpkEntries, "materials/" + vmt.basetexture + ".vtf"); - // System.out.println(index); + // logger.log(Level.FINE, index); if (index != -1) { File materialOutPath = new File(outPath); materialOutPath = new File( @@ -1090,7 +1093,7 @@ public void run() { directory.mkdirs(); } } catch (Exception e) { - System.out.println("Exception Occured: " + e.toString()); + logger.log(Level.SEVERE, "Exception Occured: " + e.toString()); } try { vpkEntries.get(index).extract(materialOutPath); @@ -1120,23 +1123,23 @@ public void run() { width = TargaReader.getWidth(fileContent); height = TargaReader.getHeight(fileContent); } catch (Exception e) { - System.out.println("Cant read Material: " + materialOutPath); - // System.out.println(e); + logger.log(Level.WARNING, "Cant read Material: " + materialOutPath); + // logger.log(Level.WARNING, e); } - // System.out.println("Adding Material: "+ el); + // logger.log(Level.FINE, "Adding Material: "+ el); textures.add(new Texture(el, vmt.basetexture, materialOutPath.toString(), width, height)); } catch (Exception e) { - System.err.println("Failed to extract material: " + vmt.basetexture); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to extract material: " + vmt.basetexture); + logger.log(Level.SEVERE, e.toString()); } if (vmt.bumpmap != null) { // If the material has a bump map associated with it if (vmt.bumpmap.endsWith(".vtf")) { vmt.bumpmap = vmt.bumpmap.substring(0, vmt.bumpmap.lastIndexOf('.')); // snip the extension } - // System.out.println("Bump found on "+vmt.basetexture+": "+vmt.bumpmap); + // logger.log(Level.FINE, "Bump found on "+vmt.basetexture+": "+vmt.bumpmap); int bumpMapIndex = getEntryIndexByPath(vpkEntries, "materials/" + vmt.bumpmap + ".vtf"); - // System.out.println(bumpMapIndex); + // logger.log(Level.FINE, bumpMapIndex); if (bumpMapIndex != -1) { File bumpMapOutPath = new File(outPath); bumpMapOutPath = new File(formatPath( @@ -1148,8 +1151,8 @@ public void run() { directory.mkdirs(); } } catch (Exception e) { - System.out.println("Failed to create directory: " + bumpMapOutPath.getParent()); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to create directory: " + bumpMapOutPath.getParent()); + logger.log(Level.SEVERE, e.toString()); } try { vpkEntries.get(bumpMapIndex).extract(bumpMapOutPath); @@ -1164,8 +1167,8 @@ public void run() { while ((reader.readLine()) != null) {} proc.waitFor(); } catch (Exception e) { - System.err.println("Failed to extract bump material: " + vmt.bumpmap); - System.err.println(e.toString()); + logger.log(Level.SEVERE, "Failed to extract bump material: " + vmt.bumpmap); + logger.log(Level.SEVERE, e.toString()); } } } @@ -1191,7 +1194,7 @@ public void run() { int textureIndex = getTextureIndexByName(textures, el); if (textureIndex == -1) { // But this is a new material textureIndex = getTextureIndexByFileName(textures, vmt.basetexture); - // System.out.println("Adding Material: "+ el); + // logger.log(Level.FINE, "Adding Material: "+ el); textures.add(new Texture(el, vmt.basetexture, materialOutPath.toString(), textures.get(textureIndex).width, textures.get(textureIndex).height)); @@ -1260,7 +1263,8 @@ public void run() { // Clean up // - System.out.println("[5/5] Cleaning up..."); + logger.log(Level.INFO, ""); // Newline to make sure progressbar stays visible + logger.log(Level.INFO, "[5/5] Cleaning up..."); if (vmf.entities != null) { //There are no entities in this VMF deleteRecursive(new File(Paths.get(outPath).getParent().resolve("models").toString())); // Delete models. Everything is now in the OBJ file @@ -1271,6 +1275,6 @@ public void run() { objFile.close(); materialFile.close(); - System.out.println("Conversion complete! Output can be found at: " + Paths.get(outPath).getParent()); + logger.log(Level.INFO, "Conversion complete! Output can be found at: " + Paths.get(outPath).getParent()); } } diff --git a/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJLauncher.java b/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJLauncher.java index 810ffe0..59ebcd9 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJLauncher.java +++ b/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJLauncher.java @@ -1,10 +1,23 @@ package com.lathrum.VMF2OBJ; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Logger; + import com.lathrum.VMF2OBJ.cli.VMF2OBJCLI; import com.lathrum.VMF2OBJ.gui.VMF2OBJFrame; public class VMF2OBJLauncher { public static void main(String args[]) throws Exception { + // Set up logger + Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + Formatter formatter = new SimpleFormatter(); + // logger.getParent().setLevel(Level.FINEST); // Print all levels of logging + for (Handler handler : logger.getParent().getHandlers()) { + handler.setFormatter(formatter); // Use custom formatter + // handler.setLevel(Level.FINEST); // Print all levels of logging + } + if (System.console() == null) { VMF2OBJFrame.main(args); } else { diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java index 8775f23..159aaef 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java @@ -6,6 +6,7 @@ import java.util.Comparator; import java.util.LinkedList; import java.util.List; +import java.util.logging.Level; import com.lathrum.VMF2OBJ.VMF2OBJ; import com.lathrum.VMF2OBJ.dataStructure.Plane; @@ -56,7 +57,7 @@ public static Side completeSide(Side side, Solid solid) { // Theoretically source only allows convex shapes, and fixes any problems upon saving... if (intersections.size() < 3) { - System.out.println("Malformed side " + side.id + ", only " + intersections.size() + " points"); + VMF2OBJ.logger.log(Level.WARNING, "Malformed side " + side.id + ", only " + intersections.size() + " points"); return null; } diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java index ffab734..c8acb50 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java @@ -177,7 +177,7 @@ public static VMF parseVMF(String text) { disp = splice(disp, alphas, disp.length() - 1); disp = disp.replaceAll(cleanUpRegex, "$1"); // remove commas at the end of a list disps = disp; - // System.out.println(disp); + // VMF2OBJ.logger.log(Level.FINE, disp); } dispMatcher = dispPattern.matcher(side); } @@ -239,7 +239,7 @@ public static VMF parseVMF(String text) { text = text.replaceAll(cleanUpRegex, "$1"); // remove commas at the end of a list text = text.replaceAll(",,", ","); - // System.out.println(text); + // VMF2OBJ.logger.log(Level.FINE, text); VMF vmf = VMF2OBJ.gson.fromJson(text, VMF.class); return vmf; } diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/model/QC.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/model/QC.java index f3b97f9..1292aba 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/model/QC.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/model/QC.java @@ -2,9 +2,12 @@ import java.util.Collection; import java.util.LinkedList; +import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.lathrum.VMF2OBJ.VMF2OBJ; + public class QC { public String ModelName; @@ -46,7 +49,7 @@ public static QC parseQC(String text) { if (modelNameMatcher.find()) { modelName = modelNameMatcher.group(1); } else { - System.out.println("Failed to find modelName"); + VMF2OBJ.logger.log(Level.WARNING, "Failed to find modelName"); } Matcher bodyGroupMatcher = Pattern.compile("(\"bodygroup\"\"[a-zA-Z0-9._/]+\"\\{([a-zA-Z0-9._/\" ]+)\\})") diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java index 0cd481a..d8d3200 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java @@ -105,7 +105,7 @@ public static VMT parseVMT(String text) { text = text.replaceAll("([a-zA-Z_]+dx[6-9])", "\"$1\":"); // Fix fallback shaders - // System.out.println(text); + // VMF2OBJ.logger.log(Level.FINE, text); VMT vmt = VMF2OBJ.gson.fromJson(text, VMT.class); return vmt; } diff --git a/src/main/java/com/lathrum/VMF2OBJ/gui/TextAreaHandler.java b/src/main/java/com/lathrum/VMF2OBJ/gui/TextAreaHandler.java index be1870a..6437136 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/gui/TextAreaHandler.java +++ b/src/main/java/com/lathrum/VMF2OBJ/gui/TextAreaHandler.java @@ -25,9 +25,14 @@ public void publish(LogRecord record) { } try { - out.append(msg); - // make sure the last line is always visible - out.setCaretPosition(out.getDocument().getLength()); + if (msg.startsWith("\r")) { + // Simulate carriage return + out.replaceRange(msg, out.getLineStartOffset(out.getLineCount() - 1), out.getDocument().getLength()); + } else { + out.append(msg); + } + // make sure the begining of last line is always visible + out.setCaretPosition(out.getLineStartOffset(out.getLineCount() - 1)); } catch (Exception ex) { reportError(null, ex, ErrorManager.WRITE_FAILURE); } diff --git a/src/main/java/com/lathrum/VMF2OBJ/gui/VMF2OBJFrame.java b/src/main/java/com/lathrum/VMF2OBJ/gui/VMF2OBJFrame.java index 5860d6e..5a6db15 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/gui/VMF2OBJFrame.java +++ b/src/main/java/com/lathrum/VMF2OBJ/gui/VMF2OBJFrame.java @@ -1,6 +1,7 @@ package com.lathrum.VMF2OBJ.gui; import java.io.File; +import java.util.logging.Level; import java.util.logging.Logger; import java.nio.file.Path; import java.util.ArrayList; @@ -265,7 +266,7 @@ public void run() { try { VMF2OBJ.main(job); } catch (Exception e) { - System.out.println("Fatal error: " + e.toString()); + VMF2OBJ.logger.log(Level.SEVERE, "Fatal error: " + e.toString()); } finally { // activate buttons setButtonsEnabled(true); @@ -284,10 +285,10 @@ private void initComponentsCustom() { setTitle("VMF2OBJ " + properties.getProperty("version")); // try { - // URL iconUrl = getClass().getResource("resources/icon.png"); - // Image icon = Toolkit.getDefaultToolkit().createImage(iconUrl); - // setIconImage(icon); - // logFrame.setIconImage(icon); + // URL iconUrl = getClass().getResource("resources/icon.png"); + // Image icon = Toolkit.getDefaultToolkit().createImage(iconUrl); + // setIconImage(icon); + // logFrame.setIconImage(icon); // } catch (Exception ex) { // } } @@ -316,7 +317,7 @@ private void initComponents() { setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); setLocationByPlatform(true); - setSize(500, 500); + setSize(650, 500); listResources.setModel(getFilesModel()); scrollResources.setViewportView(listResources); From c838d291ed06c0b0bcbddc6b2b459281a2b252a1 Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Sun, 27 Jun 2021 18:04:19 -0700 Subject: [PATCH 03/17] Fix sorting colinear and equidistant points Finally! This issue was brought up awhile ago but I was never able to figure it out. Instead of calculating the determinate to figure out the order (which fails for colinear points), and calculating the distance from the center (which fails on equidistant points, this find two perpendicular vectors in the plane given by the normal vector and calculates the triple product of the vector in question. This way it's order is not based off of just the determinate and will always be unique no matter if two vectors are colinear or equidistant from each other. Resolves #14 --- .../VMF2OBJ/dataStructure/Vector3.java | 8 +++++++ .../VMF2OBJ/dataStructure/VectorSorter.java | 20 ++++++++++++++++ .../VMF2OBJ/dataStructure/map/Side.java | 23 ++++--------------- 3 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/lathrum/VMF2OBJ/dataStructure/VectorSorter.java diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/Vector3.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/Vector3.java index d55618f..b5e775c 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/Vector3.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/Vector3.java @@ -219,6 +219,14 @@ public double distance(Vector3 vector) { return Math.sqrt(Math.pow(this.x - vector.x, 2) + Math.pow(this.y - vector.y, 2) + Math.pow(this.z - vector.z, 2)); } + public static Vector3 getLonger(Vector3 vectorA, Vector3 vectorB) { + return vectorA.magnitude() > vectorB.magnitude() ? vectorA : vectorB; + } + + public Vector3 getLonger(Vector3 vector) { + return this.magnitude() > vector.magnitude() ? this : vector; + } + public int closestIndex(Vector3[] vectors) { if (vectors.length == 0) { return -1; diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/VectorSorter.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/VectorSorter.java new file mode 100644 index 0000000..17d1694 --- /dev/null +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/VectorSorter.java @@ -0,0 +1,20 @@ +package com.lathrum.VMF2OBJ.dataStructure; + +public class VectorSorter { + final Vector3 center, normal, pp, qp; + + public VectorSorter(Vector3 normal, Vector3 center) { + this.center = center; + this.normal = normal; + Vector3 i = normal.cross(new Vector3(1,0,0)); + Vector3 j = normal.cross(new Vector3(0,1,0)); + Vector3 k = normal.cross(new Vector3(0,0,1)); + pp = i.getLonger(j).getLonger(k); // Get longest to reduce floating point imprecision + qp = normal.cross(pp); + } + + public double getOrder(Vector3 vector) { + Vector3 normalized = vector.subtract(center); + return Math.atan2(normal.dot(normalized.cross(pp)), normal.dot(normalized.cross(qp))); + } +} \ No newline at end of file diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java index 159aaef..e1b5d81 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java @@ -11,6 +11,7 @@ import com.lathrum.VMF2OBJ.VMF2OBJ; import com.lathrum.VMF2OBJ.dataStructure.Plane; import com.lathrum.VMF2OBJ.dataStructure.Vector3; +import com.lathrum.VMF2OBJ.dataStructure.VectorSorter; public class Side { public String id; @@ -54,7 +55,8 @@ public static Side completeSide(Side side, Solid solid) { } } - // Theoretically source only allows convex shapes, and fixes any problems upon saving... + // Theoretically source only allows convex shapes, and fixes any problems upon + // saving... if (intersections.size() < 3) { VMF2OBJ.logger.log(Level.WARNING, "Malformed side " + side.id + ", only " + intersections.size() + " points"); @@ -69,26 +71,11 @@ public static Side completeSide(Side side, Solid solid) { final Vector3 normal = new Plane(side).normal().normalize(); List IntersectionsList = new ArrayList(intersections); + VectorSorter sorter = new VectorSorter(normal, center); Collections.sort(IntersectionsList, new Comparator() { @Override public int compare(Vector3 o1, Vector3 o2) { - double det = Vector3.dot(normal, Vector3.cross(o1.subtract(center), o2.subtract(center))); - if (det < 0) { - return -1; - } - if (det > 0) { - return 1; - } - - // If 0, then they are colinear, just select which point is further from the - // center - double d1 = o1.subtract(center).magnitude(); - double d2 = o2.subtract(center).magnitude(); - if (d1 < d2) { - return -1; - } else { - return 1; - } + return ((Double) sorter.getOrder(o1)).compareTo((Double) sorter.getOrder(o2)); } }); From 54b1c376df046f5c7896ad129444e168f4f98ed4 Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Mon, 28 Jun 2021 21:15:09 -0700 Subject: [PATCH 04/17] Collapse almost duplicate vertices If a vertex is within 0.2 units of an already calculated vertex on the same side, don't add it to the list. Similarly, if a vertex is within 0.2 units of an already calculated vertex of a different side of the same brush, copy it's exact location so they'll merge in a later step. This greatly reduces the number of extraneous vertices when dealing with complex brushes. This does not effect entities at all. Resolves #3 --- .../VMF2OBJ/dataStructure/map/Side.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java index e1b5d81..b29e01e 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java @@ -51,6 +51,30 @@ public static Side completeSide(Side side, Solid solid) { if (!Vector3.pointInHull(intersection, solid.sides)) { continue; } + + // If the intersection is close to an existing intersection + boolean alreadyExists = false; + for (Vector3 existingIntersection : intersections) { + if (existingIntersection.distance(intersection) < 0.2) { + alreadyExists = true; + break; + } + } + if (alreadyExists) { + continue; + } + + // If the intersection is close to an existing point on another side + for (Side existingSide : solid.sides) { + for (Vector3 existingPoint : existingSide.points) { + if (existingPoint.distance(intersection) < 0.2) { + // Merge with the existing point + intersection = existingPoint; + break; + } + } + } + intersections.add((intersection)); } } From c3cdd159fd5a43fa0726b704ab245c8217cc733a Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Tue, 29 Jun 2021 18:02:10 -0700 Subject: [PATCH 05/17] Fix brushes having inverted normals With the new vertex sorting method from a few commits ago, this fix is very easy to implement. Part of issue #16 --- src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java index b29e01e..46aaaf4 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java @@ -99,7 +99,7 @@ public static Side completeSide(Side side, Solid solid) { Collections.sort(IntersectionsList, new Comparator() { @Override public int compare(Vector3 o1, Vector3 o2) { - return ((Double) sorter.getOrder(o1)).compareTo((Double) sorter.getOrder(o2)); + return ((Double) sorter.getOrder(o2)).compareTo((Double) sorter.getOrder(o1)); } }); From e7eac01ae2253696b948503e0de05f3ec20414b6 Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Tue, 29 Jun 2021 21:30:55 -0700 Subject: [PATCH 06/17] Fix compatibility with new large VMTs Some of the new VMT files for games like CSGO exceed the 1000 byte limit for data that is preloaded in the VPK tree. The rest of it is stored elsewhere in the archive. Before this, the application assumed the data for VMTs (usually a couple hundred bytes) was either all preloaded, or all located elsewhere in the file. Since the only time it's in both is if it's too large to be preloaded, I've only seen this show up with `hr_` textures in csgo. --- .../VMF2OBJ/fileStructure/VPKEntry.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/lathrum/VMF2OBJ/fileStructure/VPKEntry.java b/src/main/java/com/lathrum/VMF2OBJ/fileStructure/VPKEntry.java index 3f4a346..f4e8201 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/fileStructure/VPKEntry.java +++ b/src/main/java/com/lathrum/VMF2OBJ/fileStructure/VPKEntry.java @@ -43,9 +43,22 @@ protected VPKEntry(VPK archive, short archiveIndex, byte[] preloadData, String f } public byte[] readData() throws IOException, Exception { + byte[] data; + int dataOffset = 0; // check for preload data - if (this.preloadData != null) - return this.preloadData; + if (this.preloadData != null) { + if (this.length == 0) { + return this.preloadData; + } + // Allocate enough space for the data + data = new byte[this.preloadData.length + this.length]; + // Copy in the preloaded data + System.arraycopy(this.preloadData, 0, data, 0, this.preloadData.length); + // If there's additional data let it know to insert after the preloaded data + dataOffset = this.preloadData.length; + } else { + data = new byte[this.length]; + } // get target archive File target = null; @@ -63,9 +76,8 @@ public byte[] readData() throws IOException, Exception { } // read data - byte[] data = new byte[this.length]; fileInputStream.skip(this.offset); - fileInputStream.read(data, 0, this.length); + fileInputStream.read(data, dataOffset, this.length); return data; } From afb310491096a6a7c8758e68ec7a3acacff09fce Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Thu, 29 Jul 2021 20:47:43 -0700 Subject: [PATCH 07/17] Bump version to 2.0.0-rc.1 I still want to fix displacements before fully releasing 2.0.0, but with school looming and a working UI, I feel it's worth creating a pre-release for those who'd like to play with it. --- README.md | 20 ++++++++++--------- changelog.txt | 7 +++++++ pom.xml | 2 +- .../com/lathrum/VMF2OBJ/cli/VMF2OBJCLI.java | 2 -- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a03de9c..0cef78c 100644 --- a/README.md +++ b/README.md @@ -10,22 +10,24 @@ Watch a demonstration video: From the root directory, run: -`mvn package;java -jar ./target/VMF2OBJ-1.1.2-jar-with-dependencies.jar [VMF_FILE] [OUTPUT_FILE] [VPK_PATHS]` +`mvn package;java -jar ./target/VMF2OBJ-2.0.0-rc.1-jar-with-dependencies.jar [VMF_FILE] [args...]` ``` -usage: vmf2obj [VMF_FILE] [OUTPUT_FILE] [VPK_PATHS] [args...] - -e,--externalPath Semi-colon separated list of folders for - external custom content (such as materials or - models) - -h,--help Show this message - -q,--quiet Suppress warnings - -t,--tools Ignore tool brushes +usage: vmf2obj [VMF_FILE] [args...] + -h,--help Show this message + -o,--output Name of the output files. Defaults to the name + of the VMF file + -q,--quiet Suppress warnings + -r,--resourcePaths Semi-colon separated list of VPK files and + folders for external custom content (such as + materials or models) + -t,--tools Ignore tool brushes ``` Example: ``` -java -jar .\vmf2obj.jar .\input.vmf .\output "D:\SteamLibrary\steamapps\common\Half-Life 2\hl2\hl2_misc_dir.vpk;D:\SteamLibrary\steamapps\common\Half-Life 2\hl2\hl2_textures_dir.vpk" -e "C:\path\to\custom\content\;C:\path\to\more\custom\content\" -t +java -jar .\vmf2obj.jar .\input.vmf -o .\output -r "D:\SteamLibrary\steamapps\common\Half-Life 2\hl2\hl2_misc_dir.vpk;D:\SteamLibrary\steamapps\common\Half-Life 2\hl2\hl2_textures_dir.vpk;C:\path\to\custom\content\;C:\path\to\more\custom\content\" -t ``` ## Packaged Dependencies diff --git a/changelog.txt b/changelog.txt index 2c83063..277cc9a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +?/??/???? 2.0.0: ++ Added GUI version of application. The GUI is accessible by double-clicking on the .jar file, while the CLI is still accessible through the terminal +* Verticies that are within 0.2 units of each other will be merged together, greatly reducing the number of extraneous vertices when dealing with complex brushes +* Fix colinear and equidistant points causing a crash +* Fix brushes having inverted normals +* Fix compatibility with new large VMT files + 4/27/2021 1.1.3: * Improve transparent material compatibility * Fix handling materials with keyless commands/values diff --git a/pom.xml b/pom.xml index 8c010d5..62773c3 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.lathrum.VMF2OBJ VMF2OBJ - 1.1.3 + 2.0.0-rc.1 jar VMF2OBJ diff --git a/src/main/java/com/lathrum/VMF2OBJ/cli/VMF2OBJCLI.java b/src/main/java/com/lathrum/VMF2OBJ/cli/VMF2OBJCLI.java index 718240d..a1e37ef 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/cli/VMF2OBJCLI.java +++ b/src/main/java/com/lathrum/VMF2OBJ/cli/VMF2OBJCLI.java @@ -24,8 +24,6 @@ public static void main(String[] args) throws Exception { // Prepare Arguments try { - job.file = new VMFFileEntry(new File(args[0]), args[1]); - // parse the command line arguments CommandLine cmd = parser.parse(options, args); if (cmd.hasOption("h") || args[0].charAt(0) == '-') { From b6b30003b738b51f125e0487e3290831c98a59a6 Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Sat, 31 Jul 2021 21:35:19 -0700 Subject: [PATCH 08/17] Fix displacements for real this time Some reworks of the logic surrounding how displacement vertices are calculated, including taking account of irregular shapes and simplifying the process a little bit. When calculating the texture coordinates, I tried to put the calculation in a loop, however because the four points need to be in a specific order, I decided to just keep it expanded for now and try to tackle it later, since right now it works it's just a little ugly in the code. Resolves #16 Resolves #20 --- .../java/com/lathrum/VMF2OBJ/VMF2OBJ.java | 97 +++++++++++++------ 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJ.java b/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJ.java index 32c3e37..191a31e 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJ.java +++ b/src/main/java/com/lathrum/VMF2OBJ/VMF2OBJ.java @@ -505,22 +505,32 @@ public void run() { } } else { // Points are defined in this order: - // 1 4 - // 2 3 + // 3 0 + // 2 1 // -or- - // A D - // B C + // D A + // C B int startIndex = side.dispinfo.startposition.closestIndex(side.points); //Get adjacent points by going around counter-clockwise - Vector3 ad = side.points[(startIndex + 1) % 4].subtract(side.points[startIndex]); - Vector3 ab = side.points[(startIndex + 3) % 4].subtract(side.points[startIndex]); - // logger.log(Level.FINE, ad); - // logger.log(Level.FINE, ab); + Vector3 a = side.points[Math.floorMod((startIndex - 2), 4)]; + Vector3 b = side.points[Math.floorMod((startIndex - 1), 4)]; + Vector3 c = side.points[startIndex]; + Vector3 d = side.points[Math.floorMod((startIndex + 1), 4)]; + Vector3 cd = d.subtract(c); + Vector3 cb = b.subtract(c); + Vector3 ba = a.subtract(b); + // logger.log(Level.FINE, cd); + // logger.log(Level.FINE, cb); for (int i = 0; i < side.dispinfo.normals.length; i++) { // rows for (int j = 0; j < side.dispinfo.normals[0].length; j++) { // columns + double rowProgress = (double)j / (double)(side.dispinfo.normals[0].length - 1); + double colProgress = (double)i / (double)(side.dispinfo.normals.length - 1); Vector3 point = side.points[startIndex] - .add(ad.normalize().multiply(ad.divide(side.dispinfo.normals[0].length - 1).abs().multiply(j))) - .add(ab.normalize().multiply(ab.divide(side.dispinfo.normals.length - 1).abs().multiply(i))) + .add( + cd.multiply(colProgress).multiply(1 - rowProgress).add( + ba.multiply(colProgress).multiply(rowProgress)) + ) + .add(cb.multiply(rowProgress)) .add(side.dispinfo.normals[i][j].multiply(side.dispinfo.distances[i][j])); verticies.add(point); } @@ -528,7 +538,6 @@ public void run() { } } - // TODO: Margin of error? Set uniqueVerticies = new HashSet(verticies); ArrayList uniqueVerticiesList = new ArrayList(uniqueVerticies); @@ -775,69 +784,94 @@ public void run() { vertexTextureOffset += side.points.length; } else { // Points are defined in this order: - // 1 4 - // 2 3 + // 3 0 + // 2 1 // -or- - // A D - // B C + // D A + // C B int startIndex = side.dispinfo.startposition.closestIndex(side.points); //Get adjacent points by going around counter-clockwise - Vector3 ad = side.points[(startIndex + 1) % 4].subtract(side.points[startIndex]); - Vector3 ab = side.points[(startIndex + 3) % 4].subtract(side.points[startIndex]); + Vector3 a = side.points[Math.floorMod((startIndex - 2), 4)]; + Vector3 b = side.points[Math.floorMod((startIndex - 1), 4)]; + Vector3 c = side.points[startIndex]; + Vector3 d = side.points[Math.floorMod((startIndex + 1), 4)]; + Vector3 cd = d.subtract(c); + Vector3 cb = b.subtract(c); + Vector3 ba = a.subtract(b); + // logger.log(Level.FINE, cd); + // logger.log(Level.FINE, cb); for (int i = 0; i < side.dispinfo.normals.length - 1; i++) { // all rows but last for (int j = 0; j < side.dispinfo.normals[0].length - 1; j++) { // all columns but last buffer = ""; + double rowProgress, colProgress, u, v; + + rowProgress = (double)j / (double)(side.dispinfo.normals[0].length - 1); + colProgress = (double)i / (double)(side.dispinfo.normals.length - 1); Vector3 point = side.points[startIndex] - .add(ad.normalize().multiply(ad.divide(side.dispinfo.normals[0].length - 1).abs().multiply(j))) - .add(ab.normalize().multiply(ab.divide(side.dispinfo.normals.length - 1).abs().multiply(i))) + .add( + cd.multiply(colProgress).multiply(1 - rowProgress).add( + ba.multiply(colProgress).multiply(rowProgress)) + ) + .add(cb.multiply(rowProgress)) .add(side.dispinfo.normals[i][j].multiply(side.dispinfo.distances[i][j])); - double u = Vector3.dot(point, side.uAxisVector) / (texture.width * side.uAxisScale) + u = Vector3.dot(point, side.uAxisVector) / (texture.width * side.uAxisScale) + side.uAxisTranslation / texture.width; - double v = Vector3.dot(point, side.vAxisVector) / (texture.height * side.vAxisScale) + v = Vector3.dot(point, side.vAxisVector) / (texture.height * side.vAxisScale) + side.vAxisTranslation / texture.height; - u = -u + texture.width; v = -v + texture.height; objFile.println("vt " + u + " " + v); buffer += (uniqueVerticiesList.indexOf(point) + vertexOffset) + "/" + ((((side.dispinfo.normals.length - 1) * i) + j) * 4 + vertexTextureOffset) + " "; + rowProgress = (double)j / (double)(side.dispinfo.normals[0].length - 1); + colProgress = (double)(i + 1) / (double)(side.dispinfo.normals.length - 1); point = side.points[startIndex] - .add(ad.normalize().multiply(ad.divide(side.dispinfo.normals[0].length - 1).abs().multiply(j))) - .add(ab.normalize().multiply(ab.divide(side.dispinfo.normals.length - 1).abs().multiply(i + 1))) + .add( + cd.multiply(colProgress).multiply(1 - rowProgress).add( + ba.multiply(colProgress).multiply(rowProgress)) + ) + .add(cb.multiply(rowProgress)) .add(side.dispinfo.normals[i + 1][j].multiply(side.dispinfo.distances[i + 1][j])); u = Vector3.dot(point, side.uAxisVector) / (texture.width * side.uAxisScale) + side.uAxisTranslation / texture.width; v = Vector3.dot(point, side.vAxisVector) / (texture.height * side.vAxisScale) + side.vAxisTranslation / texture.height; - u = -u + texture.width; v = -v + texture.height; objFile.println("vt " + u + " " + v); buffer += (uniqueVerticiesList.indexOf(point) + vertexOffset) + "/" + ((((side.dispinfo.normals.length - 1) * i) + j) * 4 + vertexTextureOffset + 1) + " "; + rowProgress = (double)(j + 1) / (double)(side.dispinfo.normals[0].length - 1); + colProgress = (double)(i + 1) / (double)(side.dispinfo.normals.length - 1); point = side.points[startIndex] - .add(ad.normalize().multiply(ad.divide(side.dispinfo.normals[0].length - 1).abs().multiply(j + 1))) - .add(ab.normalize().multiply(ab.divide(side.dispinfo.normals.length - 1).abs().multiply(i + 1))) + .add( + cd.multiply(colProgress).multiply(1 - rowProgress).add( + ba.multiply(colProgress).multiply(rowProgress)) + ) + .add(cb.multiply(rowProgress)) .add(side.dispinfo.normals[i + 1][j + 1].multiply(side.dispinfo.distances[i + 1][j + 1])); u = Vector3.dot(point, side.uAxisVector) / (texture.width * side.uAxisScale) + side.uAxisTranslation / texture.width; v = Vector3.dot(point, side.vAxisVector) / (texture.height * side.vAxisScale) + side.vAxisTranslation / texture.height; - u = -u + texture.width; v = -v + texture.height; objFile.println("vt " + u + " " + v); buffer += (uniqueVerticiesList.indexOf(point) + vertexOffset) + "/" + ((((side.dispinfo.normals.length - 1) * i) + j) * 4 + vertexTextureOffset + 2) + " "; + rowProgress = (double)(j + 1) / (double)(side.dispinfo.normals[0].length - 1); + colProgress = (double)i / (double)(side.dispinfo.normals.length - 1); point = side.points[startIndex] - .add(ad.normalize().multiply(ad.divide(side.dispinfo.normals[0].length - 1).abs().multiply(j + 1))) - .add(ab.normalize().multiply(ab.divide(side.dispinfo.normals.length - 1).abs().multiply(i))) + .add( + cd.multiply(colProgress).multiply(1 - rowProgress).add( + ba.multiply(colProgress).multiply(rowProgress)) + ) + .add(cb.multiply(rowProgress)) .add(side.dispinfo.normals[i][j + 1].multiply(side.dispinfo.distances[i][j + 1])); u = Vector3.dot(point, side.uAxisVector) / (texture.width * side.uAxisScale) + side.uAxisTranslation / texture.width; v = Vector3.dot(point, side.vAxisVector) / (texture.height * side.vAxisScale) + side.vAxisTranslation / texture.height; - u = -u + texture.width; v = -v + texture.height; objFile.println("vt " + u + " " + v); buffer += (uniqueVerticiesList.indexOf(point) + vertexOffset) + "/" @@ -1013,7 +1047,6 @@ public void run() { qc.triangles[i] = temp; } - // TODO: Margin of error? Set uniqueVerticies = new HashSet(verticies); ArrayList uniqueVerticiesList = new ArrayList(uniqueVerticies); From a3290a65d2bdffc6121633b7bce9f412a1a2e31a Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Sun, 1 Aug 2021 16:54:25 -0700 Subject: [PATCH 09/17] Fix compatibility with VMT with GPU conditional textures Some new maps are starting to use materials that have different properties based on the user's graphics' settings. This commit removes those conditionals so they can be parsed like normal --- .../com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java index d8d3200..d086189 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/texture/VMT.java @@ -45,10 +45,8 @@ public static VMT parseVMT(String text) { if (text.substring(1, 6).equals("Water")) // If water texture { // Water is weird. It doesn't really have a displayable texture other than a - // normal map, - // which shouldn't really be used anyways in this case. So we'll give it an - // obvious texture - // So it can be easily changed + // normal map, which shouldn't really be used anyways in this case. So we'll + // give it an obvious texture so it can be easily changed VMT vmt = VMF2OBJ.gson.fromJson("{\"basetexture\":\"TOOLS/TOOLSDOTTED\"}", VMT.class); return vmt; } @@ -66,6 +64,7 @@ public static VMT parseVMT(String text) { text = text.replaceAll("!?srgb\\?", ""); // Remove all weirdos text = text.replaceAll("360\\?", ""); // Remove all weirdos text = text.replaceAll("-dx10", ""); // Remove all dx10 fallback textures + text = text.replaceAll("GPU[<>=]{1,2}[0-3]\\?", ""); // Remove all GPU conditional textures text = text.replaceAll("[^\"](\\$[^\" \\t]+)", "\"$1\""); // fix unquoted keys text = text.replaceAll("(\".+\"[ \\t]+)([^\" \\t\\s].*)", "$1\"$2\""); // fix unquoted values text = text.replaceAll("\\$", ""); // Remove all key prefixes From 23e354742be6b67e66276a80ff36fb1c08499501 Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Sun, 1 Aug 2021 16:56:50 -0700 Subject: [PATCH 10/17] Bump version to 2.0.0-rc2 With the displacements and VMT issues fixed, I think this version is ready for release, however another bug was reported on github that I cannot reproduce locally, so this is to check if the issue still exists --- README.md | 2 +- changelog.txt | 2 ++ pom.xml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0cef78c..774f49b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Watch a demonstration video: From the root directory, run: -`mvn package;java -jar ./target/VMF2OBJ-2.0.0-rc.1-jar-with-dependencies.jar [VMF_FILE] [args...]` +`mvn package;java -jar ./target/VMF2OBJ-2.0.0-rc.2-jar-with-dependencies.jar [VMF_FILE] [args...]` ``` usage: vmf2obj [VMF_FILE] [args...] diff --git a/changelog.txt b/changelog.txt index 277cc9a..cae51ea 100644 --- a/changelog.txt +++ b/changelog.txt @@ -3,7 +3,9 @@ * Verticies that are within 0.2 units of each other will be merged together, greatly reducing the number of extraneous vertices when dealing with complex brushes * Fix colinear and equidistant points causing a crash * Fix brushes having inverted normals +* Fix displacements being exported incorrectly * Fix compatibility with new large VMT files +* Fix compatibility with VMT files that have GPU conditional statements 4/27/2021 1.1.3: * Improve transparent material compatibility diff --git a/pom.xml b/pom.xml index 62773c3..b5e1c5a 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.lathrum.VMF2OBJ VMF2OBJ - 2.0.0-rc.1 + 2.0.0-rc.2 jar VMF2OBJ From 7639ba18f739a418e9130513aef01ccc39a113c9 Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Thu, 26 Aug 2021 18:05:03 -0700 Subject: [PATCH 11/17] Remove achievement arrays Some games have logic_achievement entities that can award part of an achievement, such as the "Door Prize" achievement in Portal two, where each of six doors award part of the achievement through an array, such as `ACH.A3_DOORS[6]` for door number 6. This array breaks the parser, crashing the conversion. This commit removes the array portion of the achievement name, but keeps the rest in tact, just in case. Resolves #25 --- src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java index c8acb50..d8256c1 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java @@ -49,6 +49,7 @@ public static VMF parseVMF(String text) { text = text.replaceAll("\\x1B|#", ""); // Remove all illegal characters text = text.replaceAll("(\".+)[{}](.+\")", "$1$2"); // Remove brackets in quotes text = text.replaceAll("\"Code\"(.*)", ""); // Remove gmod Lua code + text = text.replaceAll("(\"achievement.+)\\[[0-9]\\]\"", "$1\""); // Remove achievement arrays text = text.replaceAll("[\\t\\r\\n]", ""); // Remove all whitespaces and newlines not in quotes text = text.replaceAll("\" \"", "\"\""); // Remove all whitespaces and newlines not in quotes // text = text.replaceAll("\\s+(?=([^\"]*\"[^\"]*\")*[^\"]*$)", ""); // Remove all whitespaces and newlines not in quotes From fda72740e7944dcf65627fe3aebdbb3b5aa7fe3c Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Wed, 20 Oct 2021 15:15:36 -0700 Subject: [PATCH 12/17] Revert inverting brush normals --- src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java index 46aaaf4..b29e01e 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/Side.java @@ -99,7 +99,7 @@ public static Side completeSide(Side side, Solid solid) { Collections.sort(IntersectionsList, new Comparator() { @Override public int compare(Vector3 o1, Vector3 o2) { - return ((Double) sorter.getOrder(o2)).compareTo((Double) sorter.getOrder(o1)); + return ((Double) sorter.getOrder(o1)).compareTo((Double) sorter.getOrder(o2)); } }); From a31c83b143d772aa329fd8ccf6a3d5d61ff15709 Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Wed, 20 Oct 2021 15:26:48 -0700 Subject: [PATCH 13/17] Bump version to 2.0.0-rc.3 --- README.md | 2 +- changelog.txt | 1 + pom.xml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 774f49b..391e10d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Watch a demonstration video: From the root directory, run: -`mvn package;java -jar ./target/VMF2OBJ-2.0.0-rc.2-jar-with-dependencies.jar [VMF_FILE] [args...]` +`mvn package;java -jar ./target/VMF2OBJ-2.0.0-rc.3-jar-with-dependencies.jar [VMF_FILE] [args...]` ``` usage: vmf2obj [VMF_FILE] [args...] diff --git a/changelog.txt b/changelog.txt index cae51ea..3cdf8da 100644 --- a/changelog.txt +++ b/changelog.txt @@ -6,6 +6,7 @@ * Fix displacements being exported incorrectly * Fix compatibility with new large VMT files * Fix compatibility with VMT files that have GPU conditional statements +* Fix achievement arrays causing crashes 4/27/2021 1.1.3: * Improve transparent material compatibility diff --git a/pom.xml b/pom.xml index b5e1c5a..f3aa0b1 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.lathrum.VMF2OBJ VMF2OBJ - 2.0.0-rc.2 + 2.0.0-rc.3 jar VMF2OBJ From 3bd3443f579dceb4a3cf7f0b026ff46077ba6607 Mon Sep 17 00:00:00 2001 From: PorkMuncher Date: Tue, 26 Apr 2022 16:45:33 +0300 Subject: [PATCH 14/17] Handle empty VMF keys and also allow '@' symbol in VMF keys --- src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java index d8256c1..9f3267e 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java @@ -40,7 +40,7 @@ public static String splice(String original, String insert, int index) { public static VMF parseVMF(String text) { String objectRegex = "([a-zA-z._0-9]+)([{\\[])"; - String keyValueRegex = "(\"[a-zA-z._0-9]+\")(\"[^\"]*\")"; + String keyValueRegex = "(\"[a-zA-z._0-9@]+\"|\"\")(\"[^\"]*\")"; String objectCommaRegex = "[}\\]]\""; String cleanUpRegex = ",([}\\]])"; From 61a015285d0c6d75f475bc0c22177a0d6b89058c Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Tue, 24 May 2022 14:24:43 -0700 Subject: [PATCH 15/17] Update readme and dependencies --- README.md | 24 ++++++++++++++++++++---- demo/example.jpg | Bin 0 -> 91979 bytes demo/gui.jpg | Bin 0 -> 45732 bytes pom.xml | 4 ++-- 4 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 demo/example.jpg create mode 100644 demo/gui.jpg diff --git a/README.md b/README.md index 391e10d..2cc5109 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,17 @@ Watch a demonstration video: ## How to run -From the root directory, run: +Download the latest version from the [Releases](https://github.com/Dylancyclone/VMF2OBJ/releases) page, then double click on the .jar file to open it. -`mvn package;java -jar ./target/VMF2OBJ-2.0.0-rc.3-jar-with-dependencies.jar [VMF_FILE] [args...]` +![The VMF2OBJ GUI](demo/gui.jpg) + +Then, simply fill out which VMF file you'd like to convert, and the VPK files (and custom folders) that house the models, materials, etc for the map. + +![An example](demo/example.jpg) + +There is also a Command Line Interface: + +`java -jar ./VMF2OBJ.jar [VMF_FILE] [args...]` ``` usage: vmf2obj [VMF_FILE] [args...] @@ -30,6 +38,14 @@ Example: java -jar .\vmf2obj.jar .\input.vmf -o .\output -r "D:\SteamLibrary\steamapps\common\Half-Life 2\hl2\hl2_misc_dir.vpk;D:\SteamLibrary\steamapps\common\Half-Life 2\hl2\hl2_textures_dir.vpk;C:\path\to\custom\content\;C:\path\to\more\custom\content\" -t ``` +## Building + +To build the app from source, simply run: + +`mvn package` + +The compiled .jar file will be placed in the `target` directory. + ## Packaged Dependencies This project packages the following software and uses them during the conversion process. This project would not be possible without them. @@ -56,13 +72,13 @@ This project packages the following software and uses them during the conversion These are features that I don't have any plans to implement, either because I don't know how to, or the feature is too inconsistant, or would require extreme reworks to the current implementation. Of course, if you have an idea on how to implement any of these please feel free to submit a PR or issue discussing the idea. - [ ] prop\_\* skins - - Textures are defined per triangle in a model's decompiled SMD, but skins are defined in it's QC file. I don't know of a good way to line up skins with multiple textures to a single model. I would theoretically be possible to implement single-material skins, but the results may end up being inconsistant and produce unexpected results. + - Textures are defined per triangle in a model's decompiled SMD, but skins are defined in it's QC file. I don't know of a good way to line up skins with multiple textures to a single model. I would theoretically be possible to implement single-material skins, but the results may end up being inconsistent and produce unexpected results. - [ ] Displacement blend materials - All the data needed for a blend material is included in the displacement object, but in order to implement it into an obj object either a separate texture must be generated with the blend built in, or a per-vertex material must be applied and blended between. The downsides of the first option is that it requires a lot of processing before hand (and Java's compatibility with Targa files is kinda potato at best), and the resulting texture is not editable. The downside of the second option is that the each vertex has to be either entirely one texture or entirely the other, and the linear blend will always go between the two. This will make for very strange looking textures when comparing it to the hammer counterpart, and it would require a complete rework of how the application handles textures. The perfect-world solution is to use a format that supports this kind of thing out of the box, but then you loose compatibility pretty quickly. - [ ] infodecal - infodecal entities don't store any data about where the decal is displayed, meaning it is projected from it's origin to the brush and clipped/sized accordingly. I personally don't know enough about how this process is done, and I don't feel comfortable trying to brute force it. I looked around for the source code associated with it, but I could not find anything to reverse engineer. I know it's a pretty important feature, but I don't know how to make it work _correctly_. - [ ] info_overlay - - info_overlay is basically nextgen infodecal. Instead of just projecting to one side, an info_overlay can be projected to multiple faces, including different orientations and brushes so that the decal can "wrap" around. Again, I honestly don't really know how to approach this without brute forcing every face to create a seperate object with it's own UV wrapping. And that doesn't even include the fact that info_overlays can be distorted before being placed. + - info_overlay is basically nextgen infodecal. Instead of just projecting to one side, an info_overlay can be projected to multiple faces, including different orientations and brushes so that the decal can "wrap" around. Again, I honestly don't really know how to approach this without brute forcing every face to create a separate object with it's own UV wrapping. And that doesn't even include the fact that info_overlays can be distorted before being placed. ## Other Notes diff --git a/demo/example.jpg b/demo/example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8289bf88996342ae118a8060c7e967698d581568 GIT binary patch literal 91979 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 z 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 literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml index f3aa0b1..80f5b47 100644 --- a/pom.xml +++ b/pom.xml @@ -24,12 +24,12 @@ com.google.code.gson gson - 2.8.5 + 2.8.9 me.tongfei progressbar - 0.9.1 + 0.9.3 commons-cli From ef79d0bd8370cab7e7950c8cd48c0d46b865f598 Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Tue, 24 May 2022 14:39:02 -0700 Subject: [PATCH 16/17] Delete `@` symbol instead of accepting it Since the `@` symbol isn't documented anywhere in the VDC, it seems safer to remove it so that the resulting key matches with the expected value --- src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java index 9f3267e..82f2681 100644 --- a/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java +++ b/src/main/java/com/lathrum/VMF2OBJ/dataStructure/map/VMF.java @@ -40,13 +40,13 @@ public static String splice(String original, String insert, int index) { public static VMF parseVMF(String text) { String objectRegex = "([a-zA-z._0-9]+)([{\\[])"; - String keyValueRegex = "(\"[a-zA-z._0-9@]+\"|\"\")(\"[^\"]*\")"; + String keyValueRegex = "(\"[a-zA-z._0-9]+\"|\"\")(\"[^\"]*\")"; String objectCommaRegex = "[}\\]]\""; String cleanUpRegex = ",([}\\]])"; text = text.replaceAll("\\\\", "/"); // Replace backslashs with forwardslashs text = text.replaceAll("(?m)^\\s*//(.*)", ""); // Remove all commented lines - text = text.replaceAll("\\x1B|#", ""); // Remove all illegal characters + text = text.replaceAll("\\x1B|#|@", ""); // Remove all illegal characters text = text.replaceAll("(\".+)[{}](.+\")", "$1$2"); // Remove brackets in quotes text = text.replaceAll("\"Code\"(.*)", ""); // Remove gmod Lua code text = text.replaceAll("(\"achievement.+)\\[[0-9]\\]\"", "$1\""); // Remove achievement arrays From d0f8ad17003c80812f56d218df2eb6e15f60f1e2 Mon Sep 17 00:00:00 2001 From: Dylan Lathrum Date: Tue, 24 May 2022 14:44:12 -0700 Subject: [PATCH 17/17] Bump version to 2.0.0 Huzzah! --- changelog.txt | 4 ++-- pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index 3cdf8da..f89a90c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ -?/??/???? 2.0.0: +5/24/2022 2.0.0: + Added GUI version of application. The GUI is accessible by double-clicking on the .jar file, while the CLI is still accessible through the terminal -* Verticies that are within 0.2 units of each other will be merged together, greatly reducing the number of extraneous vertices when dealing with complex brushes +* Vertices that are within 0.2 units of each other will be merged together, greatly reducing the number of extraneous vertices when dealing with complex brushes * Fix colinear and equidistant points causing a crash * Fix brushes having inverted normals * Fix displacements being exported incorrectly diff --git a/pom.xml b/pom.xml index 80f5b47..e8e5072 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.lathrum.VMF2OBJ VMF2OBJ - 2.0.0-rc.3 + 2.0.0 jar VMF2OBJ