diff --git a/README.md b/README.md index 64a5ca36..13ffbdaa 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,34 @@ #### An external tool to export or delete selected chunks and regions from a world save of Minecraft Java Edition. --- + +* [Usage](#usage) + * [Navigation](#navigation) + * [Selections](#selections) + * [Chunk filter](#chunk-filter) + * [NBT Changer](#nbt-changer) + * [Caching](#caching) + * [Debugging](#debugging) +* [Supported Versions](#supported-versions) +* [Headless mode](#headless-mode) + * [Mandatory and optional parameters](#mandatory-and-optional-parameters) + * [Create selection](#create-selection) + * [Export chunks](#export-chunks) + * [Import chunks](#import-chunks) + * [Delete chunks](#delete-chunks) + * [Change NBT](#change-nbt) + * [Cache images](#cache-images) + * [Configuration parameters](#configuration-parameters) + * [Filter query](#filter-query) + * [Change values](#change-values) +* [Checkout and building](#checkout-and-building) +* [Translation](#translation) +* [Download and installation](#download-and-installation) + * [If you have Java from Oracle installed on your system](#if-you-have-java-from-oracle-installed-on-your-system) + * [If you have Minecraft Java Edition installed on your system](#if-you-have-minecraft-java-edition-installed-on-your-system) + * [If you are using OpenJDK](#if-you-are-using-openjdk) + + ## Usage ### Navigation Executing the tool, it shows an empty window with a chunk and a region grid. To actually show a world, open a folder containing Minecraft Anvil (\*.mca) files. The tool will then render a top-down view of this world that you can zoom into and zoom out of by scrolling up and down and that you can move around using the middle mouse button (`Cmd+LMB` on Mac OS) or using `WASD`. @@ -33,7 +61,7 @@ Because the conditions use internal values used by Minecraft, the following tabl | LastUpdate | int | The time a chunk was last updated in seconds since 1970-01-01. Also accepts a timestamp in the `yyyy-MM-dd HH-mm-ss`-format such as `2018-01-02 15:03:04`. If the time is omitted, it will default to `00:00:00`. | | xPos | int | The location of the chunk on the x-axis in chunk coordinates. | | zPos | int | The location of the chunk on the z-axis in chunk coordinates. | -| Palette | String | A list of comma (,) separated 1.13 block names. The block names will be converted to block ids for chunks with DataVersion 1343 or below. The validation of block names can be skipped by writing them in double quotes ("). Example: `sand,"new_block",gravel`.| +| Palette | String | A list of comma (,) separated 1.13 block names. The block names will be converted to block ids for chunks with DataVersion 1343 or below. The validation of block names can be skipped by writing them in single quotes ('). Example: `sand,'new_block',gravel`.| | Status | String | The status of the chunk generation. Only recognized by Minecraft 1.13+ (DataVersion 1444+) | | LightPopulated | byte | Whether the light levels for the chunk have been calculated. If this is set to 0, converting a world from 1.12.x to 1.13 will omit that chunk. Allowed values are `0` and `1`. | | Biome | String | One or multiple biome names, separated by comma (,). For a reference of biome names, have a look at the [Wiki](https://minecraft.gamepedia.com/Java_Edition_data_values#Biomes). | @@ -78,15 +106,103 @@ The MCA Selector currently supports the following Minecraft versions: | 1.14 | 1901 - ? | Yes | --- -## Translation -The UI language of the MCA Selector can be dynamically changed in the settings. -The following languages are available: +## Headless mode -* English (UK) -* German (Germany) -* Chinese (China) (thanks to [@LovesAsuna](https://github.com/LovesAsuna) for translating) +The MCA Selector can be run in a headless mode without showing the UI. Use the program parameter `--headless` to do so. +Headless mode can be run in different modes: -If you would like to contribute a translation, you can find the language files in [resources/lang/](https://github.com/Querz/mcaselector/tree/master/src/main/resources/lang). The files are automatically detected and shown in the settings drowdown menu once they are placed in this folder. +| Mode | Parameter | Description | +| ---- | --------- | ----------- | +| Create selection | `--mode select` | Create a selection from a filter query and save it as a CSV file. | +| Export chunks | `--mode export` | Export chunks based on a filter query and/or a selection. | +| Import chunks | `--mode import` | Import chunks with an optional offset. | +| Delete chunks | `--mode delete` | Delete chunks based on a filter query and/or a selection. | +| Change NBT | `--mode change` | Changes NBT values in an entire world or only in chunks based on a selection. | +| Cache images | `--mode cache` | Generates the cache images for an entire world. | + +### Mandatory and optional parameters + +#### Create selection + +| Parameter | Description | Mandatory | +| --------- | ----------- | :-------: | +| `--world ` | The world for which to create the selection. | Yes | +| `--output ` | The CSV-file to save the selection to. | Yes | +| `--query ` | The filter query to use to create a selection. | Yes | + +#### Export chunks + +| Parameter | Description | Mandatory | +| --------- | ----------- | :-------: | +| `--world ` | The world to export chunks from. | Yes | +| `--output ` | The destination of the exported chunks. The directory MUST be empty. | Yes | +| `--query ` | The filter query to use to export the chunks. | Yes (if `--input` is not set) | +| `--input ` | The csv-file to load a selection from. | Yes (if `--query` is not set) | + +#### Import chunks + +| Parameter | Description | Mandatory | +| --------- | ----------- | :-------: | +| `--world ` | The world to import chunks to. | Yes | +| `--input ` | The world to import the chunks from. | Yes | +| `--offset-x ` | The offset in chunks in x-direction. | No, default `0` | +| `--offset-z ` | The offset in chunks in z-direction. | No, default `0` | +| `--overwrite` | Whether to overwrite existing chunks. | No, default `false` | + +#### Delete chunks + +| Parameter | Description | Mandatory | +| --------- | ----------- | :-------: | +| `--world ` | The world to delete chunks from. | Yes | +| `--query ` | The filter query to use to delete the chunks. | Yes (if `--input` is not set) | +| `--input ` | The csv-file to load a selection from. | Yes (if `--query` is not set) | + +#### Change NBT + +| Parameter | Description | Mandatory | +| --------- | ----------- | :-------: | +| `--world ` | The world in which NBT values should be changed. | Yes | +| `--query ` | The values to be changed. | Yes | +| `--input ` | The csv-file to load a selection from. | No | +| `--force` | Whether the value should be created if the key doesn't exist. | No, default `false` | + +#### Cache images + +| Parameter | Description | Mandatory | +| --------- | ----------- | :-------: | +| `--world ` | The world to create cache images from. | Yes | +| `--output ` | Where the cache files fille be saved. | Yes | +| `--zoom-level <1\|2\|4>` | The zoom level for which to generate the images. | No, generates images for all zoom levels if not specified | + +#### Configuration parameters + +| Parameter | Description | Mandatory | +| --------- | ----------- | :-------: | +| `--debug` | Enables debug messages. | No | +| `--read-threads ` | The amount of Threads to be used for reading files. | No, default `1` | +| `--process-threads ` | The amounts of Threads to be used for processing data. | No, defaults to the amount of processor cores | +| `--write-threads ` | The amount of Threads to be used for writing data to disk. | No, default `4` | +| `--max-loaded-files ` | The maximum amount of simultaneously loaded files. | No, defaults to 1.5 * amount of processor cores | + + +### Filter query + +A filter query is a text representation of the chunk filter that can be created in the UI of the program. When the debug mode is enabled, it will be printed into the console. +Example: +``` +--query "xPos >= 10 AND xPos <= 20 AND Palette contains \"sand,water\"" +``` +This will select all chunks that contain sand and water blocks and their x-position ranges from 10 to 20. +As shown, double quotes (") must be escaped with a backslash. + +### Change values + +The query for changing NBT values in chunks looks slightly different to the filter query. It is a comma (,) separated list of assignments. +Example: +``` +--query "LightPopulated = 1, Status = empty" +``` +This will set the field "LightPopulated" to "1" and "Status" to "empty". Just like the filter query, the query to change values is printed to the console when using the UI in debug mode. --- ## Checkout and building @@ -104,11 +220,23 @@ On Windows, run gradlew.bat build minifyCss shadowJar ``` +--- +## Translation +The UI language of the MCA Selector can be dynamically changed in the settings. +The following languages are available: + +* English (UK) +* German (Germany) +* Chinese (China) (thanks to [@LovesAsuna](https://github.com/LovesAsuna) for translating) +* Czech (Czech Republic) (thanks to [@mkyral](https://github.com/mkyral) for translating) + +If you would like to contribute a translation, you can find the language files in [resources/lang/](https://github.com/Querz/mcaselector/tree/master/src/main/resources/lang). The files are automatically detected and shown in the settings drowdown menu once they are placed in this folder. + --- ## Download and installation -[**Download Version 1.7.4**](https://github.com/Querz/mcaselector/releases/download/1.7.4/mcaselector-1.7.4.jar) +[**Download Version 1.8**](https://github.com/Querz/mcaselector/releases/download/1.8/mcaselector-1.8.jar) "Requirements": * Either: @@ -117,15 +245,15 @@ gradlew.bat build minifyCss shadowJar * A computer * A brain -#### If you have Java from Oracle installed on your system: +#### If you have Java from Oracle installed on your system -Most likely, `.jar` files are associated with java on your computer, it should therefore launch by simply double clicking the file (or however your OS is configured to open files using your mouse or keyboard). If not, you can try `java -jar mcaselector-1.7.4.jar` from your console. If this doesn't work, you might want to look into how to modify the `PATH` variable on your system to tell your system that java is an executable program. +Most likely, `.jar` files are associated with java on your computer, it should therefore launch by simply double clicking the file (or however your OS is configured to open files using your mouse or keyboard). If not, you can try `java -jar mcaselector-1.8.jar` from your console. If this doesn't work, you might want to look into how to modify the `PATH` variable on your system to tell your system that java is an executable program. -#### If you have Minecraft Java Edition installed on your system: +#### If you have Minecraft Java Edition installed on your system -Minecraft Java Edition comes with a JRE that you can use to start the MCA Selector, so there is no need to install another version of java on your system. On Windows, that java version is usually located in `C:\Program Files (x86)\Minecraft\runtime\jre-x64\bin\` and once inside this folder you can simply run `java.exe -jar `. On Mac OS you should find it in `Applications/Minecraft.app/Contents/runtime/jre-x64/1.8.0_74/bin` where you can execute `./java -jar `. +Minecraft Java Edition comes with a JRE that you can use to start the MCA Selector, so there is no need to install another version of java on your system. On Windows, that java version is usually located in `C:\Program Files (x86)\Minecraft\runtime\jre-x64\bin\` and once inside this folder you can simply run `java.exe -jar `. On Mac OS you should find it in `Applications/Minecraft.app/Contents/runtime/jre-x64/1.8.0_74/bin` where you can execute `./java -jar `. -#### If you are using OpenJDK: +#### If you are using OpenJDK If you are using a distribution of OpenJDK, you have to make sure that it comes with JavaFX, as it is needed to run the MCA Selector. Some distributions like AdoptOpenJDK (shipped with most Linux distributions) do not ship with JavaFX by default. On Debian distributions, an open version of JavaFX is contained in the `openjfx` package. This or some other installation of JavaFX is required to run the `.jar`. ## diff --git a/build.gradle b/build.gradle index b070bc1b..87124058 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,8 @@ +import java.nio.file.Files +import java.util.function.Consumer + plugins { - id 'com.github.johnrengelman.shadow' version '2.0.1' + id 'com.github.johnrengelman.shadow' version '5.1.0' id 'com.eriwen.gradle.css' version '2.14.0' } @@ -9,7 +12,7 @@ apply plugin: 'idea' apply plugin: 'css' group 'net.querz.mcaselector' -version '1.7.4' +version '1.8' sourceCompatibility = 1.8 @@ -48,7 +51,7 @@ minifyCss { dest = "${sourceSets.main.output.resourcesDir}/style.css" } -task updateVersion { +task updateReadme { doLast { ant.replaceregexp( match: '(?:Download Version )\\d+\\.\\d+(?:\\.\\d+)?', @@ -73,5 +76,44 @@ task updateVersion { byline: true) { fileset(dir: '.', includes: 'README.md') } + + ant.replaceregexp( + match: '[\\s\\S]*', + replace: createTOC('README.md', '', '', true, true), + flags: 'g') { + fileset(dir: '.', includes: 'README.md') + } } +} + +static String createTOC(String f, String st, String en, boolean it, boolean is) throws IOException { + File e = new File(f) + if (!e.isFile()) { + throw new IllegalArgumentException("file doesn't exist or is not a file") + } + StringBuilder b = new StringBuilder(st + "\n") + int[] s = new int[1] + Files.lines(e.toPath()).forEach(new Consumer() { + @Override + void accept(String n) { + String t + if ((t = n.trim()).startsWith("#")) { + int l = 0 + while (l < t.length() && t.charAt(l) == '#') { + l++ + } + s[0] += l == 4 ? 1 : 0 + if (t.length() == l || t.charAt(l) != ' ' || it && l == 1 || is && l == 4 && s[0] == 1) { + return + } + String x = t.substring(l + 1) + for (int i = it ? 2 : 1; i < l; i++) { + b.append(" ") + } + String k = x.toLowerCase().replaceAll("[^a-z0-9 \\-_]", "").replace(' ', '-') + b.append("* [").append(x).append("](#").append(k).append(")\n") + } + } + }) + return b.append(en).toString() } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5fd10a31..ec6129dd 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e06299f7..1920c6ee 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Apr 23 00:26:22 CEST 2018 +#Tue Jul 23 10:36:27 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-bin.zip diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/gradlew.bat b/gradlew.bat index f9553162..e95643d6 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,84 +1,84 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/net/querz/mcaselector/Config.java b/src/main/java/net/querz/mcaselector/Config.java index aa224a72..a9092257 100644 --- a/src/main/java/net/querz/mcaselector/Config.java +++ b/src/main/java/net/querz/mcaselector/Config.java @@ -50,6 +50,10 @@ public static File getCacheDir() { return cacheDir; } + public static void setCacheDir(File cacheDir) { + Config.cacheDir = cacheDir; + } + public static File[] getCacheDirs() { int lodLevels = 0; for (int i = Helper.getMaxZoomLevel(); i >= 1; i /= 2) { diff --git a/src/main/java/net/querz/mcaselector/Main.java b/src/main/java/net/querz/mcaselector/Main.java index 28c151f4..5a825027 100644 --- a/src/main/java/net/querz/mcaselector/Main.java +++ b/src/main/java/net/querz/mcaselector/Main.java @@ -1,18 +1,29 @@ package net.querz.mcaselector; +import net.querz.mcaselector.headless.ParamExecutor; import net.querz.mcaselector.ui.Window; import net.querz.mcaselector.util.Debug; import net.querz.mcaselector.util.Translation; import java.util.Locale; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; public class Main { - public static void main(String[] args) { + public static void main(String[] args) throws ExecutionException, InterruptedException { + Debug.dumpf("java version: %s", System.getProperty("java.version")); + Debug.dumpf("jvm max mem: %d", Runtime.getRuntime().maxMemory()); + + Future headless = new ParamExecutor(args).parseAndRun(); + if (headless != null && headless.get()) { + // we already ran headless mode, so we exit here + Debug.print("exiting"); + System.exit(0); + } + Config.loadFromIni(); Translation.load(Config.getLocale()); Locale.setDefault(Config.getLocale()); - Debug.dumpf("java version: %s", System.getProperty("java.version")); - Debug.dumpf("jvm max mem: %d", Runtime.getRuntime().maxMemory()); Runtime.getRuntime().addShutdownHook(new Thread(Config::exportConfig)); Window.launch(Window.class, args); } diff --git a/src/main/java/net/querz/mcaselector/changer/BiomeField.java b/src/main/java/net/querz/mcaselector/changer/BiomeField.java index 9329c2d3..dbe08277 100644 --- a/src/main/java/net/querz/mcaselector/changer/BiomeField.java +++ b/src/main/java/net/querz/mcaselector/changer/BiomeField.java @@ -15,6 +15,7 @@ public class BiomeField extends Field { private static Map validNames = new HashMap<>(); + private String name; static { try (BufferedReader bis = new BufferedReader( @@ -42,11 +43,17 @@ public BiomeField() { super(FieldType.BIOME); } + @Override + public String toString() { + return "Biome = " + name; + } + @Override public boolean parseNewValue(String s) { String low = s.toLowerCase(); if (validNames.containsKey(low)) { setNewValue(validNames.get(low)); + name = low; return true; } return super.parseNewValue(s); diff --git a/src/main/java/net/querz/mcaselector/changer/Field.java b/src/main/java/net/querz/mcaselector/changer/Field.java index 6702794f..4ee80990 100644 --- a/src/main/java/net/querz/mcaselector/changer/Field.java +++ b/src/main/java/net/querz/mcaselector/changer/Field.java @@ -28,9 +28,13 @@ public T getNewValue() { return newValue; } + public FieldType getType() { + return type; + } + @Override public String toString() { - return type.toString(); + return type.toString() + " = " + newValue; } //returns true if the value has been correctly parsed and value is not null diff --git a/src/main/java/net/querz/mcaselector/changer/FieldType.java b/src/main/java/net/querz/mcaselector/changer/FieldType.java index 7048d708..227f44ab 100644 --- a/src/main/java/net/querz/mcaselector/changer/FieldType.java +++ b/src/main/java/net/querz/mcaselector/changer/FieldType.java @@ -36,6 +36,15 @@ public static List> instantiateAll() { return list; } + public static FieldType getByName(String name) { + for (FieldType f : FieldType.values()) { + if (f.name.equals(name)) { + return f; + } + } + return null; + } + @Override public String toString() { return name; diff --git a/src/main/java/net/querz/mcaselector/filter/BiomeFilter.java b/src/main/java/net/querz/mcaselector/filter/BiomeFilter.java index 28bdf528..6847d078 100644 --- a/src/main/java/net/querz/mcaselector/filter/BiomeFilter.java +++ b/src/main/java/net/querz/mcaselector/filter/BiomeFilter.java @@ -110,12 +110,12 @@ public void setFilterValue(String raw) { @Override public String toString(FilterData data) { - return "BiomeFilter " + getComparator() + " "; + return "BiomeFilter " + getComparator().getQueryString() + " "; } @Override public String toString() { - return "Biome " + getComparator() + " " + (getFilterValue() != null ? Arrays.toString(getFilterValue().toArray()) : "null"); + return "Biome " + getComparator().getQueryString() + " \"" + getRawValue() + "\""; } @Override diff --git a/src/main/java/net/querz/mcaselector/filter/Comparator.java b/src/main/java/net/querz/mcaselector/filter/Comparator.java index b90b4906..307792a6 100644 --- a/src/main/java/net/querz/mcaselector/filter/Comparator.java +++ b/src/main/java/net/querz/mcaselector/filter/Comparator.java @@ -8,17 +8,47 @@ public enum Comparator { LT(">"), LEQ(">="), SEQ("<="), - CONTAINS("\u2287"), - CONTAINS_NOT("!\u2287"); + CONTAINS("\u2287", "contains"), + CONTAINS_NOT("!\u2287", "!contains"); private String string; + private String query; Comparator(String string) { + this.string = this.query = string; + } + + // string is the representation used to display the comparator in UI + // query is the representation used in headless queries + Comparator(String string, String query) { this.string = string; + this.query = query; } @Override public String toString() { return string; } + + public String getQueryString() { + return query == null ? string : query; + } + + public static Comparator fromString(String s) { + for (Comparator c : Comparator.values()) { + if (c.string.equals(s)) { + return c; + } + } + return null; + } + + public static Comparator fromQuery(String s) { + for (Comparator c : Comparator.values()) { + if (c.query.equals(s)) { + return c; + } + } + return null; + } } diff --git a/src/main/java/net/querz/mcaselector/filter/Filter.java b/src/main/java/net/querz/mcaselector/filter/Filter.java index 5382b473..95c3e687 100644 --- a/src/main/java/net/querz/mcaselector/filter/Filter.java +++ b/src/main/java/net/querz/mcaselector/filter/Filter.java @@ -63,6 +63,8 @@ public Filter getParent() { public abstract Comparator getComparator(); + public abstract void setComparator(Comparator comparator); + public abstract boolean matches(FilterData data); public abstract String toString(FilterData data); diff --git a/src/main/java/net/querz/mcaselector/filter/FilterType.java b/src/main/java/net/querz/mcaselector/filter/FilterType.java index 70782e2e..7253d75a 100644 --- a/src/main/java/net/querz/mcaselector/filter/FilterType.java +++ b/src/main/java/net/querz/mcaselector/filter/FilterType.java @@ -41,6 +41,15 @@ public String toString() { return string; } + public static FilterType getByName(String name) { + for (FilterType t : FilterType.values()) { + if (t.string.equals(name)) { + return t; + } + } + return null; + } + public enum Format { GROUP, NUMBER, TEXT } diff --git a/src/main/java/net/querz/mcaselector/filter/GroupFilter.java b/src/main/java/net/querz/mcaselector/filter/GroupFilter.java index 4df95a08..33d2b93a 100644 --- a/src/main/java/net/querz/mcaselector/filter/GroupFilter.java +++ b/src/main/java/net/querz/mcaselector/filter/GroupFilter.java @@ -67,6 +67,9 @@ public Comparator getComparator() { return null; } + @Override + public void setComparator(Comparator comparator) {} + @Override public boolean matches(FilterData data) { boolean currentResult = true; diff --git a/src/main/java/net/querz/mcaselector/filter/InhabitedTimeFilter.java b/src/main/java/net/querz/mcaselector/filter/InhabitedTimeFilter.java index 570995ea..f1ac48a0 100644 --- a/src/main/java/net/querz/mcaselector/filter/InhabitedTimeFilter.java +++ b/src/main/java/net/querz/mcaselector/filter/InhabitedTimeFilter.java @@ -34,6 +34,11 @@ public void setFilterValue(String raw) { } } + @Override + public String toString() { + return "InhabitedTime " + getComparator().getQueryString() + " \"" + getRawValue() + "\""; + } + @Override public String getFormatText() { return "duration"; diff --git a/src/main/java/net/querz/mcaselector/filter/LastUpdateFilter.java b/src/main/java/net/querz/mcaselector/filter/LastUpdateFilter.java index 4a43acca..b3d62b50 100644 --- a/src/main/java/net/querz/mcaselector/filter/LastUpdateFilter.java +++ b/src/main/java/net/querz/mcaselector/filter/LastUpdateFilter.java @@ -37,6 +37,11 @@ public String getFormatText() { return "YYYY-MM-DD hh:mm:ss"; } + @Override + public String toString() { + return "LastUpdate " + getComparator().getQueryString() + " \"" + getRawValue() + "\""; + } + @Override public LastUpdateFilter clone() { return new LastUpdateFilter(getOperator(), getComparator(), value); diff --git a/src/main/java/net/querz/mcaselector/filter/NumberFilter.java b/src/main/java/net/querz/mcaselector/filter/NumberFilter.java index 642a7f39..1ea87de1 100644 --- a/src/main/java/net/querz/mcaselector/filter/NumberFilter.java +++ b/src/main/java/net/querz/mcaselector/filter/NumberFilter.java @@ -62,12 +62,12 @@ public boolean matches(FilterData data) { @Override public String toString(FilterData data) { - return getFilterValue() + " " + comparator + " " + getNumber(data); + return getFilterValue() + " " + comparator.getQueryString() + " " + getNumber(data); } @Override public String toString() { - return getType() + " " + comparator + " " + getFilterValue(); + return getType() + " " + comparator.getQueryString() + " " + getFilterValue(); } public abstract String getFormatText(); diff --git a/src/main/java/net/querz/mcaselector/filter/Operator.java b/src/main/java/net/querz/mcaselector/filter/Operator.java index e54868e1..eed86cdc 100644 --- a/src/main/java/net/querz/mcaselector/filter/Operator.java +++ b/src/main/java/net/querz/mcaselector/filter/Operator.java @@ -15,4 +15,13 @@ public enum Operator { public String toString() { return string; } + + public static Operator getByName(String name) { + for (Operator o : Operator.values()) { + if (o.string.equals(name)) { + return o; + } + } + return null; + } } diff --git a/src/main/java/net/querz/mcaselector/filter/PaletteFilter.java b/src/main/java/net/querz/mcaselector/filter/PaletteFilter.java index 629679af..746bc882 100644 --- a/src/main/java/net/querz/mcaselector/filter/PaletteFilter.java +++ b/src/main/java/net/querz/mcaselector/filter/PaletteFilter.java @@ -2,6 +2,8 @@ import net.querz.mcaselector.util.Debug; import net.querz.mcaselector.version.VersionController; +import net.querz.nbt.Tag; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -10,6 +12,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.regex.Matcher; public class PaletteFilter extends TextFilter> { @@ -56,7 +59,7 @@ public void setFilterValue(String raw) { for (int i = 0; i < rawBlockNames.length; i++) { String name = rawBlockNames[i]; if (!validNames.contains(name)) { - if (name.startsWith("\"") && name.endsWith("\"") && name.length() >= 2) { + if (name.startsWith("'") && name.endsWith("'") && name.length() >= 2 && !name.contains("\"")) { rawBlockNames[i] = name.substring(1, name.length() - 1); continue; } @@ -78,12 +81,12 @@ public String getFormatText() { @Override public String toString(FilterData data) { - return "PaletteFilter " + getComparator() + " "; + return "PaletteFilter " + getComparator().getQueryString() + " "; } @Override public String toString() { - return "Palette " + getComparator() + " " + (getFilterValue() != null ? Arrays.toString(getFilterValue().toArray()) : "null"); + return "Palette " + getComparator().getQueryString() + " \"" + getRawValue() + "\""; } @Override diff --git a/src/main/java/net/querz/mcaselector/filter/StatusFilter.java b/src/main/java/net/querz/mcaselector/filter/StatusFilter.java index 7588a7e7..7f220a14 100644 --- a/src/main/java/net/querz/mcaselector/filter/StatusFilter.java +++ b/src/main/java/net/querz/mcaselector/filter/StatusFilter.java @@ -62,12 +62,12 @@ public void setFilterValue(String raw) { @Override public String toString(FilterData data) { StringTag tag = data.getChunk().getCompoundTag("Level").getStringTag("Status"); - return getFilterValue() + " " + getComparator() + " " + (tag == null ? "null" : tag.getValue()); + return getFilterValue() + " " + getComparator().getQueryString() + " " + (tag == null ? "null" : tag.getValue()); } @Override public String toString() { - return "Status " + getComparator() + " " + (getFilterValue() != null ? getFilterValue() : "null"); + return "Status " + getComparator().getQueryString() + " " + getFilterValue(); } @Override diff --git a/src/main/java/net/querz/mcaselector/headless/ChangeParser.java b/src/main/java/net/querz/mcaselector/headless/ChangeParser.java new file mode 100644 index 00000000..ad14a5af --- /dev/null +++ b/src/main/java/net/querz/mcaselector/headless/ChangeParser.java @@ -0,0 +1,63 @@ +package net.querz.mcaselector.headless; + +import net.querz.mcaselector.changer.Field; +import net.querz.mcaselector.changer.FieldType; +import java.util.ArrayList; +import java.util.List; + +public class ChangeParser { + + private StringPointer ptr; + + public ChangeParser(String change) { + ptr = new StringPointer(change); + } + + public List> parse() throws ParseException { + List> fields = new ArrayList<>(); + + while (ptr.hasNext()) { + ptr.skipWhitespace(); + // read key, operator, value + String key = ptr.parseSimpleString(this::isValidCharacter); + FieldType fieldType = FieldType.getByName(key); + if (fieldType == null) { + throw ptr.parseException("invalid field"); + } + Field field = fieldType.newInstance(); + if (field == null) { + throw ptr.parseException("unable to create change field " + key); + } + + ptr.skipWhitespace(); + + ptr.expectChar('='); + + ptr.skipWhitespace(); + + String value = ptr.parseSimpleString(this::isValidCharacter); + + if (!field.parseNewValue(value)) { + throw ptr.parseException("invalid value"); + } + + ptr.skipWhitespace(); + + //expect , if we didn't reach the end + if (ptr.hasNext()) { + ptr.expectChar(','); + } + + fields.add(field); + } + return fields; + } + + private boolean isValidCharacter(char c) { + return c >= 'a' && c <= 'z' + || c >= 'A' && c <= 'Z' + || c >= '0' && c <= '9' + || c == '-' + || c == '+'; + } +} diff --git a/src/main/java/net/querz/mcaselector/headless/ConsoleProgress.java b/src/main/java/net/querz/mcaselector/headless/ConsoleProgress.java new file mode 100644 index 00000000..6b487b5c --- /dev/null +++ b/src/main/java/net/querz/mcaselector/headless/ConsoleProgress.java @@ -0,0 +1,59 @@ +package net.querz.mcaselector.headless; + +import net.querz.mcaselector.util.Debug; +import net.querz.mcaselector.util.Progress; +import java.util.concurrent.atomic.AtomicInteger; + +public class ConsoleProgress implements Progress { + + private int max; + private AtomicInteger progress = new AtomicInteger(0); + private Runnable doneAction; + + @Override + public void setMax(int max) { + this.max = max; + } + + @Override + public void updateProgress(String msg, int progress) { + this.progress.set(progress); + printProgress(msg); + if (this.progress.get() >= max) { + doneAction.run(); + } + } + + @Override + public void done(String msg) { + max = 1; + updateProgress(msg, 1); + } + + @Override + public void incrementProgress(String msg) { + incrementProgress(msg, 1); + } + + @Override + public void incrementProgress(String msg, int progress) { + int currentProgress = this.progress.incrementAndGet(); + printProgress(msg); + if (currentProgress >= max) { + doneAction.run(); + } + } + + @Override + public void setMessage(String msg) { + Debug.print(msg); + } + + public void onDone(Runnable doneAction) { + this.doneAction = doneAction; + } + + private void printProgress(String msg) { + Debug.printf("%.2f%%\t%s", ((double) progress.get() / max * 100), msg); + } +} diff --git a/src/main/java/net/querz/mcaselector/headless/ExceptionConsumer.java b/src/main/java/net/querz/mcaselector/headless/ExceptionConsumer.java new file mode 100644 index 00000000..718ffda7 --- /dev/null +++ b/src/main/java/net/querz/mcaselector/headless/ExceptionConsumer.java @@ -0,0 +1,7 @@ +package net.querz.mcaselector.headless; + +@FunctionalInterface +public interface ExceptionConsumer { + + void accept(T t) throws E; +} diff --git a/src/main/java/net/querz/mcaselector/headless/FilterParser.java b/src/main/java/net/querz/mcaselector/headless/FilterParser.java new file mode 100644 index 00000000..17f288f0 --- /dev/null +++ b/src/main/java/net/querz/mcaselector/headless/FilterParser.java @@ -0,0 +1,123 @@ +package net.querz.mcaselector.headless; + +import net.querz.mcaselector.filter.Comparator; +import net.querz.mcaselector.filter.Filter; +import net.querz.mcaselector.filter.FilterType; +import net.querz.mcaselector.filter.GroupFilter; +import net.querz.mcaselector.filter.Operator; + +public class FilterParser { + + private StringPointer ptr; + + public FilterParser(String filter) { + ptr = new StringPointer(filter); + } + + public GroupFilter parse() throws ParseException { + GroupFilter group = new GroupFilter(); + ptr.skipWhitespace(); + boolean first = true; + while (ptr.hasNext() && ptr.currentChar() != ')') { + // read operator + Operator operator; + if (first) { + operator = Operator.AND; + first = false; + } else { + operator = parseOperator(); + } + + ptr.skipWhitespace(); + + // parse group + if (ptr.currentChar() == '(') { + ptr.next(); + GroupFilter child = parse(); + child.setOperator(operator); + group.addFilter(child); + ptr.skipWhitespace(); + ptr.expectChar(')'); + ptr.skipWhitespace(); + continue; + } + + group.addFilter(parseFilterType(operator)); + + ptr.skipWhitespace(); + } + ptr.skipWhitespace(); + return group; + } + + private Filter parseFilterType(Operator operator) throws ParseException { + // parse value + String type = ptr.parseSimpleString(); + FilterType t = FilterType.getByName(type); + + if (t == null) { + throw ptr.parseException("invalid filter type"); + } + + Comparator comparator = parseComparator(); + + Filter f = t.create(); + if (f == null) { + throw ptr.parseException("unable to create filter for type " + type); + } + Comparator allowed = null; + for (Comparator c : f.getComparators()) { + if (c == comparator) { + allowed = c; + break; + } + } + if (allowed == null) { + throw ptr.parseException("comparator " + comparator + " not allowed for filter type " + type); + } + f.setOperator(operator); + f.setComparator(allowed); + return parseFilterValue(f); + } + + private Filter parseFilterValue(Filter filter) throws ParseException { + ptr.skipWhitespace(); + if (ptr.currentChar() == '"') { + filter.setFilterValue(ptr.parseQuotedString()); + } else { + filter.setFilterValue(ptr.parseSimpleString(this::isValidCharacter)); + } + if (!filter.isValid()) { + throw ptr.parseException("invalid value"); + } + return filter; + } + + private Comparator parseComparator() throws ParseException { + ptr.skipWhitespace(); + Comparator comparator = Comparator.fromQuery(ptr.parseSimpleString()); + if (comparator == null) { + throw ptr.parseException("invalid comparator"); + } + return comparator; + } + + private Operator parseOperator() throws ParseException { + String op = ptr.parseSimpleString(); + Operator operator = Operator.getByName(op); + if (operator == null) { + throw ptr.parseException("invalid operator " + op); + } + return operator; + } + + private boolean isValidCharacter(char c) { + return c >= 'a' && c <= 'z' + || c >= 'A' && c <= 'Z' + || c >= '0' && c <= '9' + || c == ',' + || c == '-' + || c == '+' + || c == ':'; + } +} diff --git a/src/main/java/net/querz/mcaselector/headless/ParamExecutor.java b/src/main/java/net/querz/mcaselector/headless/ParamExecutor.java new file mode 100644 index 00000000..af6a5b3c --- /dev/null +++ b/src/main/java/net/querz/mcaselector/headless/ParamExecutor.java @@ -0,0 +1,354 @@ +package net.querz.mcaselector.headless; + +import net.querz.mcaselector.Config; +import net.querz.mcaselector.changer.Field; +import net.querz.mcaselector.filter.GroupFilter; +import net.querz.mcaselector.headless.ParamInterpreter.ActionKey; +import net.querz.mcaselector.io.ChunkFilterDeleter; +import net.querz.mcaselector.io.ChunkFilterExporter; +import net.querz.mcaselector.io.ChunkFilterSelector; +import net.querz.mcaselector.io.ChunkImporter; +import net.querz.mcaselector.io.FieldChanger; +import net.querz.mcaselector.io.SelectionDeleter; +import net.querz.mcaselector.io.SelectionExporter; +import net.querz.mcaselector.io.SelectionUtil; +import net.querz.mcaselector.util.DataProperty; +import net.querz.mcaselector.util.Debug; +import net.querz.mcaselector.util.Helper; +import net.querz.mcaselector.util.Point2i; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +public class ParamExecutor { + + private String[] args; + + public ParamExecutor(String[] args) { + this.args = args; + } + + public Future parseAndRun() { + + FutureTask future = new FutureTask<>(() -> {}, true); + + DataProperty> params = new DataProperty<>(); + + try { + params.set(new ParamParser(args).parse()); + ParamInterpreter pi = new ParamInterpreter(params.get()); + + // register parameter dependencies and restrictions + pi.registerDependencies("headless", null, new ActionKey("mode", null)); + pi.registerDependencies("mode", null, new ActionKey("headless", null)); + pi.registerRestrictions("mode", "select", "export", "import", "delete", "change", "cache"); + pi.registerDependencies("mode", null, new ActionKey("world", null)); // every mode param needs a world dir + pi.registerDependencies("mode", "select", new ActionKey("output", null), new ActionKey("query", null)); + pi.registerDependencies("mode", "export", new ActionKey("output", null)); + pi.registerDependencies("mode", "import", new ActionKey("input", null)); + pi.registerDependencies("mode", "change", new ActionKey("query", null)); + pi.registerDependencies("mode", "cache", new ActionKey("output", null)); + pi.registerDependencies("world", null, new ActionKey("mode", null)); // world param needs mode param + pi.registerSoftDependencies("output", null, new ActionKey("mode", "select"), new ActionKey("mode", "export"), new ActionKey("mode", "cache")); + pi.registerSoftDependencies("input", null, new ActionKey("mode", "export"), new ActionKey("mode", "import"), new ActionKey("mode", "delete"), new ActionKey("mode", "change")); + pi.registerSoftDependencies("query", null, new ActionKey("mode", "select"), new ActionKey("mode", "export"), new ActionKey("mode", "delete"), new ActionKey("mode", "change")); + pi.registerDependencies("force", null, new ActionKey("mode", "change")); + pi.registerDependencies("offset-x", null, new ActionKey("mode", "import")); + pi.registerDependencies("offset-z", null, new ActionKey("mode", "import")); + pi.registerDependencies("overwrite", null, new ActionKey("mode", "import")); + pi.registerDependencies("zoom-level", null, new ActionKey("mode", "cache")); + for (int z = Helper.getMinZoomLevel(); z <= Helper.getMaxZoomLevel(); z *= 2) { + pi.registerRestrictions("zoom-level", z + ""); + } + pi.registerDependencies("debug", null, new ActionKey("headless", null)); + pi.registerDependencies("read-threads", null, new ActionKey("headless", null)); + pi.registerDependencies("process-threads", null, new ActionKey("headless", null)); + pi.registerDependencies("write-threads", null, new ActionKey("headless", null)); + + parseConfig(params.get()); + + DataProperty isHeadless = new DataProperty<>(); + + pi.registerAction("headless", null, v -> runModeHeadless(isHeadless::set)); + pi.registerAction("mode", "select", v -> runModeSelect(params.get(), future)); + pi.registerAction("mode", "export", v -> runModeExport(params.get(), future)); + pi.registerAction("mode", "import", v -> runModeImport(params.get(), future)); + pi.registerAction("mode", "delete", v -> runModeDelete(params.get(), future)); + pi.registerAction("mode", "change", v -> runModeChange(params.get(), future)); + pi.registerAction("mode", "cache", v -> runModeCache(params.get(), future)); + + pi.execute(); + + if (isHeadless.get() != null && isHeadless.get()) { + return future; + } + + } catch (Exception ex) { + Debug.error("Error: " + ex.getMessage()); + if (params.get() != null && params.get().containsKey("debug")) { + ex.printStackTrace(); + } + future.run(); + return future; + } + return null; + } + + private static void runModeHeadless(Consumer isHeadless) { + isHeadless.accept(true); + } + + private void parseConfig(Map params) throws IOException { + Config.setDebug(params.containsKey("debug")); + Config.setLoadThreads(parsePositiveInt(params.getOrDefault("read-threads", "" + Config.DEFAULT_LOAD_THREADS))); + Config.setProcessThreads(parsePositiveInt(params.getOrDefault("process-threads", "" + Config.DEFAULT_PROCESS_THREADS))); + Config.setWriteThreads(parsePositiveInt(params.getOrDefault("write-threads", "" + Config.DEFAULT_WRITE_THREADS))); + Config.setMaxLoadedFiles(parsePositiveInt(params.getOrDefault("max-loaded-files", "" + Config.DEFAULT_MAX_LOADED_FILES))); + } + + private static void runModeCache(Map params, FutureTask future) throws IOException { + File world = parseDirectory(params.get("world")); + checkDirectoryForFiles(world, Helper.MCA_FILE_PATTERN); + Config.setWorldDir(world); + + File output = parseDirectory(params.get("output")); + createDirectoryIfNotExists(output); + checkDirectoryIsEmpty(output); + Config.setCacheDir(output); + + printHeadlessSettings(); + + Integer zoomLevel = params.containsKey("zoom-level") ? parseInt(params.get("zoom-level")) : null; + + ConsoleProgress progress = new ConsoleProgress(); + progress.onDone(future); + + Helper.forceGenerateCache(zoomLevel, progress); + } + + private static void runModeChange(Map params, FutureTask future) throws IOException { + File world = parseDirectory(params.get("world")); + checkDirectoryForFiles(world, Helper.MCA_FILE_PATTERN); + Config.setWorldDir(world); + + List> fields = new ChangeParser(params.get("query")).parse(); + + Map> selection = loadSelection(params, "input"); + + printHeadlessSettings(); + + boolean force = params.containsKey("force"); + + ConsoleProgress progress = new ConsoleProgress(); + progress.onDone(future); + + FieldChanger.changeNBTFields(fields, force, selection, progress); + } + + + + private static void runModeDelete(Map params, FutureTask future) throws IOException { + File world = parseDirectory(params.get("world")); + checkDirectoryForFiles(world, Helper.MCA_FILE_PATTERN); + Config.setWorldDir(world); + + printHeadlessSettings(); + + GroupFilter g = null; + if (params.containsKey("query")) { + g = new FilterParser(params.get("query")).parse(); + Debug.print("filter set: " + g); + } + + Map> selection = loadSelection(params, "input"); + + ConsoleProgress progress = new ConsoleProgress(); + progress.onDone(future); + + if (g != null) { + ChunkFilterDeleter.deleteFilter(g, selection, progress); + } else if (selection != null) { + SelectionDeleter.deleteSelection(selection, progress); + } else { + throw new ParseException("missing parameter --query and/or --selection"); + } + } + + private static void runModeImport(Map params, FutureTask future) throws IOException { + File world = parseDirectory(params.get("world")); + createDirectoryIfNotExists(world); + Config.setWorldDir(world); + // don't check for files, world dir can be empty. + // this might be used to just apply an offset to the input without merging anything. + + File input = parseDirectory(params.get("input")); + checkDirectoryForFiles(world, Helper.MCA_FILE_PATTERN); + + Config.setWorldDir(world); + + int offsetX = parseInt(params.get("offset-x")); + int offsetZ = parseInt(params.get("offset-z")); + boolean overwrite = params.containsKey("overwrite"); + + printHeadlessSettings(); + + ConsoleProgress progress = new ConsoleProgress(); + progress.onDone(future); + + ChunkImporter.importChunks(input, progress, overwrite, new Point2i(offsetX, offsetZ)); + } + + private static void runModeExport(Map params, FutureTask future) throws IOException { + File world = parseDirectory(params.get("world")); + checkDirectoryForFiles(world, Helper.MCA_FILE_PATTERN); + Config.setWorldDir(world); + + File output = parseDirectory(params.get("output")); + createDirectoryIfNotExists(output); + checkDirectoryIsEmpty(output); + + printHeadlessSettings(); + + GroupFilter g = null; + if (params.containsKey("query")) { + g = new FilterParser(params.get("query")).parse(); + Debug.print("filter set: " + g); + } + + Map> selection = loadSelection(params, "input"); + + Debug.print("exporting chunks..."); + + ConsoleProgress progress = new ConsoleProgress(); + progress.onDone(future); + + if (g != null) { + ChunkFilterExporter.exportFilter(g, selection, output, progress); + } else if (selection != null) { + SelectionExporter.exportSelection(selection, output, progress); + } else { + throw new ParseException("missing parameter --query and/or --selection"); + } + } + + private static void runModeSelect(Map params, FutureTask future) throws IOException { + File world = parseDirectory(params.get("world")); + checkDirectoryForFiles(world, Helper.MCA_FILE_PATTERN); + Config.setWorldDir(world); + + File output = parseFile(params.get("output"), "csv"); + createParentDirectoryIfNotExists(output); + + printHeadlessSettings(); + + GroupFilter g = new FilterParser(params.get("query")).parse(); + + Debug.print("filter set: " + g); + Debug.print("selecting chunks..."); + + Map> selection = new HashMap<>(); + + ConsoleProgress progress = new ConsoleProgress(); + progress.onDone(() -> { + SelectionUtil.exportSelection(selection, output); + future.run(); + }); + + ChunkFilterSelector.selectFilter(g, selection::putAll, progress); + } + + private static void printHeadlessSettings() { + Debug.print("read threads: " + Config.getLoadThreads()); + Debug.print("process threads: " + Config.getProcessThreads()); + Debug.print("write threads: " + Config.getWriteThreads()); + } + + private static int parseInt(String value) throws ParseException { + if (value != null && !value.isEmpty()) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException ex) { + throw new ParseException(ex.getMessage()); + } + } + return 0; + } + + private static int parsePositiveInt(String value) throws ParseException { + if (value != null) { + try { + int i = Integer.parseInt(value); + if (i <= 0) { + throw new ParseException("number cannot be negative: \"" + value + "\""); + } + return i; + } catch (NumberFormatException ex) { + throw new ParseException(ex.getMessage()); + } + } + return 1; + } + + private static Map> loadSelection(Map params, String key) throws ParseException { + if (params.containsKey(key)) { + Debug.print("loading selection..."); + + File input = parseFile(params.get("input"), "csv"); + fileMustExist(input); + return SelectionUtil.importSelection(input); + } + return null; + } + + private static File parseFile(String value, String ending) throws ParseException { + File file = new File(value); + if (file.getName().equals("." + ending) || !file.getName().endsWith("." + ending)) { + throw new ParseException("invalid file \"" + value + "\", expected ." + ending + " file"); + } + return file; + } + + private static File parseDirectory(String value) { + return new File(value); + } + + private static void fileMustExist(File file) throws ParseException { + if (!file.exists()) { + throw new ParseException("file \"" + file + "\" does not exist"); + } + } + + private static void createDirectoryIfNotExists(File file) throws IOException { + if (!file.exists() && !file.mkdirs()) { + throw new IOException("unable to create directory \"" + file + "\""); + } + } + + private static void checkDirectoryForFiles(File dir, String regexp) { + Pattern p = Pattern.compile(regexp); + File[] found = dir.listFiles((d, n) -> p.matcher(n).find()); + if (found == null || found.length == 0) { + throw new IllegalArgumentException("no valid files found in \"" + dir + "\""); + } + } + + private static void checkDirectoryIsEmpty(File dir) { + File[] found = dir.listFiles(); + if (found != null && found.length > 0) { + throw new IllegalArgumentException("directory \"" + dir + "\" is not empty"); + } + } + + private static void createParentDirectoryIfNotExists(File file) throws IOException { + if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) { + throw new IOException("unable to create directory for \"" + file + "\""); + } + } +} diff --git a/src/main/java/net/querz/mcaselector/headless/ParamInterpreter.java b/src/main/java/net/querz/mcaselector/headless/ParamInterpreter.java new file mode 100644 index 00000000..0257ebb7 --- /dev/null +++ b/src/main/java/net/querz/mcaselector/headless/ParamInterpreter.java @@ -0,0 +1,174 @@ +package net.querz.mcaselector.headless; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class ParamInterpreter { + + private Map params; + private Set knownParams = new HashSet<>(); + private Map> actions = new HashMap<>(); + private Map> dependencies = new HashMap<>(); + private Map> softDependencies = new HashMap<>(); + private Map> restrictions = new HashMap<>(); + + public ParamInterpreter(Map params) { + this.params = params; + } + + public void registerAction(String key, String value, ExceptionConsumer action) { + actions.put(new ActionKey(key, value), action); + knownParams.add(key); + } + + public void registerDependencies(String key, String value, ActionKey... dependencies) { + Set restr = this.dependencies.computeIfAbsent(new ActionKey(key, value), k -> new HashSet<>()); + restr.addAll(Arrays.asList(dependencies)); + addKnownParams(dependencies); + knownParams.add(key); + } + + public void registerSoftDependencies(String key, String value, ActionKey... dependencies) { + Set restr = softDependencies.computeIfAbsent(new ActionKey(key, value), k -> new HashSet<>()); + restr.addAll(Arrays.asList(dependencies)); + addKnownParams(dependencies); + knownParams.add(key); + } + + public void registerRestrictions(String key, String... values) { + Set restr = restrictions.computeIfAbsent(key, k -> new HashSet<>()); + restr.addAll(Arrays.asList(values)); + knownParams.add(key); + } + + private void addKnownParams(ActionKey... params) { + for (ActionKey param : params) { + knownParams.add(param.key); + } + } + + public void execute() throws IOException { + // check dependencies and restrictions + for (Map.Entry param : params.entrySet()) { + if (!knownParams.contains(param.getKey())) { + throw new IllegalArgumentException("unknown param \"--" + param.getKey() + "\""); + } + + Set dependencies = new HashSet<>(); + Set keyValueDep = this.dependencies.get(new ActionKey(param.getKey(), param.getValue())); + if (keyValueDep != null) { + dependencies.addAll(keyValueDep); + } + + + Set all = this.dependencies.get(new ActionKey(param.getKey(), null)); + if (all != null) { + dependencies.addAll(all); + } + + for (ActionKey dep : dependencies) { + if (!params.containsKey(dep.key)) { + throw new IllegalArgumentException("missing param \"--" + dep.key + "\""); + } + if (dep.value != null && (params.get(dep.key) == null || !params.get(dep.key).equals(dep.value))) { + throw new IllegalArgumentException("invalid value \"" + params.get(dep.key) + "\" for param \"--" + dep.key + "\" in context"); + } + } + + if (restrictions.get(param.getKey()) != null && !restrictions.get(param.getKey()).contains(param.getValue())) { + throw new IllegalArgumentException("invalid value \"" + param.getValue() + "\" for param \"--" + param.getKey() + "\""); + } + + // soft dependencies: we need only ONE of the params + + Set softDependencies = new HashSet<>(); + Set keyValueSoft = this.softDependencies.get(new ActionKey(param.getKey(), param.getValue())); + if (keyValueSoft != null) { + softDependencies.addAll(keyValueSoft); + } + + Set keySoft = this.softDependencies.get(new ActionKey(param.getKey(), null)); + if (keySoft != null) { + softDependencies.addAll(keySoft); + } + + + if (softDependencies.size() > 0) { + int found = 0; + for (ActionKey dep : softDependencies) { + if (params.containsKey(dep.key)) { + if (dep.value != null) { + if (params.get(dep.key).equals(dep.value)) { + found++; + } + } else { + found++; + } + } + } + if (found == 0) { + throw new IllegalArgumentException("did not find mandatory param required to complete \"--" + param.getKey() + " " + param.getValue() + "\""); + } + if (found > 1) { + throw new IllegalArgumentException("found more than one optional param to complete \"--" + param.getKey() + " " + param.getValue() + "\""); + } + } + } + + // execute actions + // actions that are only registered to a key will be executed first + for (Map.Entry param : params.entrySet()) { + ActionKey key = new ActionKey(param.getKey(), null); + if (actions.containsKey(key) && actions.get(key) != null) { + actions.get(key).accept(param.getValue()); + continue; + } + ActionKey keyValue = new ActionKey(param.getKey(), param.getValue()); + if (actions.containsKey(keyValue) && actions.get(keyValue) != null) { + actions.get(keyValue).accept(param.getValue()); + } + } + } + + public static class ActionKey { + String key; + String value; + + public ActionKey(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public int hashCode() { + if (key == null) { + return 0; + } + return Objects.hash(key, value); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof ActionKey)) { + return false; + } + if (key == null) { + return ((ActionKey) other).key == null; + } + if (value == null) { + return key.equals(((ActionKey) other).key) && ((ActionKey) other).value == null; + } + return key.equals(((ActionKey) other).key) && value.equals(((ActionKey) other).value); + } + + @Override + public String toString() { + return "ActionKey " + key + "/" + value; + } + } +} diff --git a/src/main/java/net/querz/mcaselector/headless/ParamParser.java b/src/main/java/net/querz/mcaselector/headless/ParamParser.java new file mode 100644 index 00000000..0a2c9816 --- /dev/null +++ b/src/main/java/net/querz/mcaselector/headless/ParamParser.java @@ -0,0 +1,36 @@ +package net.querz.mcaselector.headless; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class ParamParser { + + private String[] args; + + public ParamParser(String[] args) { + this.args = args; + } + + public Map parse() throws IOException { + Map values = new HashMap<>(); + + String currentKey = null; + for (String s : args) { + if (s.startsWith("--")) { + if (values.containsKey(s)) { + throw new ParseException("duplicate paramter " + s); + } + currentKey = s.substring(2); + values.put(currentKey, null); + } else { + if (values.get(currentKey) != null) { + throw new ParseException("multiple values for parameter " + currentKey); + } else { + values.put(currentKey, s); + } + } + } + return values; + } +} diff --git a/src/main/java/net/querz/mcaselector/headless/ParseException.java b/src/main/java/net/querz/mcaselector/headless/ParseException.java new file mode 100644 index 00000000..4796e23c --- /dev/null +++ b/src/main/java/net/querz/mcaselector/headless/ParseException.java @@ -0,0 +1,25 @@ +package net.querz.mcaselector.headless; + +import java.io.IOException; + +public class ParseException extends IOException { + + public ParseException(String msg) { + super(msg); + } + + public ParseException(String msg, String value, int index) { + super(msg + " at: " + formatError(value, index)); + } + + private static String formatError(String value, int index) { + StringBuilder builder = new StringBuilder(); + int i = Math.min(value.length(), index); + if (i > 35) { + builder.append("..."); + } + builder.append(value, Math.max(0, i - 35), i); + builder.append("<--[HERE]"); + return builder.toString(); + } +} diff --git a/src/main/java/net/querz/mcaselector/headless/StringPointer.java b/src/main/java/net/querz/mcaselector/headless/StringPointer.java new file mode 100644 index 00000000..7cdc0baa --- /dev/null +++ b/src/main/java/net/querz/mcaselector/headless/StringPointer.java @@ -0,0 +1,122 @@ +package net.querz.mcaselector.headless; + +import java.util.function.Function; + +public class StringPointer { + + private String value; + private int index; + + public StringPointer(String value) { + this.value = value; + } + + public String parseSimpleString() { + int oldIndex = index; + while (hasNext() && !Character.isWhitespace(currentChar())) { + index++; + } + return value.substring(oldIndex, index); + } + + public String parseSimpleString(Function valid) { + int oldIndex = index; + while (hasNext() && valid.apply(currentChar())) { + index++; + } + return value.substring(oldIndex, index); + } + + public String parseQuotedString() throws ParseException { + int oldIndex = ++index; //ignore beginning quotes + StringBuilder sb = null; + boolean escape = false; + while (hasNext()) { + char c = next(); + if (escape) { + if (c != '\\' && c != '"') { + throw parseException("invalid escape of '" + c + "'"); + } + escape = false; + } else { + if (c == '\\') { //escape + escape = true; + if (sb != null) { + continue; + } + sb = new StringBuilder(value.substring(oldIndex, index - 1)); + continue; + } + if (c == '"') { + return sb == null ? value.substring(oldIndex, index - 1) : sb.toString(); + } + } + if (sb != null) { + sb.append(c); + } + } + throw parseException("missing end quote"); + } + + public boolean nextArrayElement() { + skipWhitespace(); + if (hasNext() && currentChar() == ',') { + index++; + skipWhitespace(); + return true; + } + return false; + } + + public void expectChar(char c) throws ParseException { + skipWhitespace(); + boolean hasNext = hasNext(); + if (hasNext && currentChar() == c) { + index++; + return; + } + throw parseException("expected '" + c + "' but got " + (hasNext ? "'" + currentChar() + "'" : "EOF")); + } + + public void skipWhitespace() { + while (hasNext() && Character.isWhitespace(currentChar())) { + index++; + } + } + + public boolean hasNext() { + return index < value.length(); + } + + public boolean hasCharsLeft(int num) { + return this.index + num < value.length(); + } + + public char currentChar() { + return value.charAt(index); + } + + public int index() { + return index; + } + + public int size() { + return value.length(); + } + + public char next() { + return value.charAt(index++); + } + + public void skip(int offset) { + index += offset; + } + + public char lookAhead(int offset) { + return value.charAt(index + offset); + } + + public ParseException parseException(String msg) { + return new ParseException(msg, value, index); + } +} diff --git a/src/main/java/net/querz/mcaselector/io/ChunkFilterDeleter.java b/src/main/java/net/querz/mcaselector/io/ChunkFilterDeleter.java index c9cddb8c..6440293f 100644 --- a/src/main/java/net/querz/mcaselector/io/ChunkFilterDeleter.java +++ b/src/main/java/net/querz/mcaselector/io/ChunkFilterDeleter.java @@ -2,10 +2,10 @@ import net.querz.mcaselector.Config; import net.querz.mcaselector.filter.GroupFilter; -import net.querz.mcaselector.ui.ProgressTask; import net.querz.mcaselector.util.Debug; import net.querz.mcaselector.util.Helper; import net.querz.mcaselector.util.Point2i; +import net.querz.mcaselector.util.Progress; import net.querz.mcaselector.util.Timer; import net.querz.mcaselector.util.Translation; import java.io.File; @@ -20,7 +20,7 @@ public class ChunkFilterDeleter { private ChunkFilterDeleter() {} - public static void deleteFilter(GroupFilter filter, Map> selection, ProgressTask progressChannel) { + public static void deleteFilter(GroupFilter filter, Map> selection, Progress progressChannel) { File[] files = Config.getWorldDir().listFiles((d, n) -> n.matches(Helper.MCA_FILE_PATTERN)); if (files == null || files.length == 0) { progressChannel.done(Translation.DIALOG_PROGRESS_NO_FILES.toString()); @@ -41,9 +41,9 @@ public static class MCADeleteFilterLoadJob extends LoadDataJob { private GroupFilter filter; private Map> selection; - private ProgressTask progressChannel; + private Progress progressChannel; - MCADeleteFilterLoadJob(File file, GroupFilter filter, Map> selection, ProgressTask progressChannel) { + MCADeleteFilterLoadJob(File file, GroupFilter filter, Map> selection, Progress progressChannel) { super(file); this.filter = filter; this.selection = selection; @@ -80,11 +80,11 @@ public void execute() { public static class MCADeleteFilterProcessJob extends ProcessDataJob { - private ProgressTask progressChannel; + private Progress progressChannel; private GroupFilter filter; private Set selection; - MCADeleteFilterProcessJob(File file, byte[] data, GroupFilter filter, Set selection, ProgressTask progressChannel) { + MCADeleteFilterProcessJob(File file, byte[] data, GroupFilter filter, Set selection, Progress progressChannel) { super(file, data); this.filter = filter; this.selection = selection; @@ -111,9 +111,9 @@ public void execute() { public static class MCADeleteFilterSaveJob extends SaveDataJob { - private ProgressTask progressChannel; + private Progress progressChannel; - MCADeleteFilterSaveJob(File file, MCAFile data, ProgressTask progressChannel) { + MCADeleteFilterSaveJob(File file, MCAFile data, Progress progressChannel) { super(file, data); this.progressChannel = progressChannel; } diff --git a/src/main/java/net/querz/mcaselector/io/ChunkFilterExporter.java b/src/main/java/net/querz/mcaselector/io/ChunkFilterExporter.java index fcc4439f..b9fdd7f5 100644 --- a/src/main/java/net/querz/mcaselector/io/ChunkFilterExporter.java +++ b/src/main/java/net/querz/mcaselector/io/ChunkFilterExporter.java @@ -2,10 +2,10 @@ import net.querz.mcaselector.Config; import net.querz.mcaselector.filter.GroupFilter; -import net.querz.mcaselector.ui.ProgressTask; import net.querz.mcaselector.util.Debug; import net.querz.mcaselector.util.Helper; import net.querz.mcaselector.util.Point2i; +import net.querz.mcaselector.util.Progress; import net.querz.mcaselector.util.Timer; import net.querz.mcaselector.util.Translation; import java.io.File; @@ -20,7 +20,7 @@ public class ChunkFilterExporter { private ChunkFilterExporter() {} - public static void exportFilter(GroupFilter filter, Map> selection, File destination, ProgressTask progressChannel) { + public static void exportFilter(GroupFilter filter, Map> selection, File destination, Progress progressChannel) { File[] files = Config.getWorldDir().listFiles((d, n) -> n.matches(Helper.MCA_FILE_PATTERN)); if (files == null || files.length == 0) { progressChannel.done(Translation.DIALOG_PROGRESS_NO_FILES.toString()); @@ -41,10 +41,10 @@ public static class MCAExportFilterLoadJob extends LoadDataJob { private GroupFilter filter; private Map> selection; - private ProgressTask progressChannel; + private Progress progressChannel; private File destination; - MCAExportFilterLoadJob(File file, GroupFilter filter, Map> selection, File destination, ProgressTask progressChannel) { + MCAExportFilterLoadJob(File file, GroupFilter filter, Map> selection, File destination, Progress progressChannel) { super(file); this.filter = filter; this.selection = selection; @@ -90,12 +90,12 @@ public void execute() { public static class MCAExportFilterProcessJob extends ProcessDataJob { - private ProgressTask progressChannel; + private Progress progressChannel; private GroupFilter filter; private Set selection; private File destination; - MCAExportFilterProcessJob(File file, byte[] data, GroupFilter filter, Set selection, File destination, ProgressTask progressChannel) { + MCAExportFilterProcessJob(File file, byte[] data, GroupFilter filter, Set selection, File destination, Progress progressChannel) { super(file, data); this.filter = filter; this.selection = selection; @@ -124,9 +124,9 @@ public void execute() { public static class MCAExportFilterSaveJob extends SaveDataJob { private File destination; - private ProgressTask progressChannel; + private Progress progressChannel; - MCAExportFilterSaveJob(File file, MCAFile data, File destination, ProgressTask progressChannel) { + MCAExportFilterSaveJob(File file, MCAFile data, File destination, Progress progressChannel) { super(file, data); this.destination = destination; this.progressChannel = progressChannel; diff --git a/src/main/java/net/querz/mcaselector/io/ChunkFilterSelector.java b/src/main/java/net/querz/mcaselector/io/ChunkFilterSelector.java index b1e401b5..1bad84ce 100644 --- a/src/main/java/net/querz/mcaselector/io/ChunkFilterSelector.java +++ b/src/main/java/net/querz/mcaselector/io/ChunkFilterSelector.java @@ -1,26 +1,26 @@ package net.querz.mcaselector.io; -import javafx.application.Platform; import net.querz.mcaselector.Config; import net.querz.mcaselector.filter.GroupFilter; -import net.querz.mcaselector.tiles.TileMap; -import net.querz.mcaselector.ui.ProgressTask; +import net.querz.mcaselector.tiles.Tile; import net.querz.mcaselector.util.Debug; import net.querz.mcaselector.util.Helper; import net.querz.mcaselector.util.Point2i; +import net.querz.mcaselector.util.Progress; import net.querz.mcaselector.util.Timer; import net.querz.mcaselector.util.Translation; import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import java.util.regex.Matcher; public class ChunkFilterSelector { private ChunkFilterSelector() {} - public static void selectFilter(GroupFilter filter, TileMap tileMap, ProgressTask progressChannel) { + public static void selectFilter(GroupFilter filter, Consumer>> callback, Progress progressChannel) { File[] files = Config.getWorldDir().listFiles((d, n) -> n.matches(Helper.MCA_FILE_PATTERN)); if (files == null || files.length == 0) { progressChannel.done(Translation.DIALOG_PROGRESS_NO_FILES.toString()); @@ -33,20 +33,20 @@ public static void selectFilter(GroupFilter filter, TileMap tileMap, ProgressTas progressChannel.updateProgress(files[0].getName(), 0); for (File file : files) { - MCAFilePipe.addJob(new MCASelectFilterLoadJob(file, filter, tileMap, progressChannel)); + MCAFilePipe.addJob(new MCASelectFilterLoadJob(file, filter, callback, progressChannel)); } } public static class MCASelectFilterLoadJob extends LoadDataJob { private GroupFilter filter; - private ProgressTask progressChannel; - private TileMap tileMap; + private Progress progressChannel; + private Consumer>> callback; - MCASelectFilterLoadJob(File file, GroupFilter filter, TileMap tileMap, ProgressTask progressChannel) { + MCASelectFilterLoadJob(File file, GroupFilter filter, Consumer>> callback, Progress progressChannel) { super(file); this.filter = filter; - this.tileMap = tileMap; + this.callback = callback; this.progressChannel = progressChannel; } @@ -65,7 +65,7 @@ public void execute() { byte[] data = load(); if (data != null) { - MCAFilePipe.executeProcessData(new MCASelectFilterProcessJob(getFile(), data, filter, tileMap, new Point2i(regionX, regionZ), progressChannel)); + MCAFilePipe.executeProcessData(new MCASelectFilterProcessJob(getFile(), data, filter, callback, new Point2i(regionX, regionZ), progressChannel)); } else { Debug.errorf("error loading mca file %s", getFile().getName()); progressChannel.incrementProgress(getFile().getName()); @@ -79,15 +79,15 @@ public void execute() { public static class MCASelectFilterProcessJob extends ProcessDataJob { - private ProgressTask progressChannel; + private Progress progressChannel; private GroupFilter filter; - private TileMap tileMap; + private Consumer>> callback; private Point2i location; - MCASelectFilterProcessJob(File file, byte[] data, GroupFilter filter, TileMap tileMap, Point2i location, ProgressTask progressChannel) { + MCASelectFilterProcessJob(File file, byte[] data, GroupFilter filter, Consumer>> callback, Point2i location, Progress progressChannel) { super(file, data); this.filter = filter; - this.tileMap = tileMap; + this.callback = callback; this.location = location; this.progressChannel = progressChannel; } @@ -100,13 +100,14 @@ public void execute() { MCAFile mca = MCAFile.readAll(getFile(), new ByteArrayPointer(getData())); if (mca != null) { Set chunks = mca.getFilteredChunks(filter); + if (chunks.size() == Tile.CHUNKS) { + chunks = null; + } Map> region = new HashMap<>(); region.put(location, chunks); - Platform.runLater(() -> { - tileMap.addMarkedChunks(region); - tileMap.update(); - }); + callback.accept(region); + Debug.dumpf("took %s to delete chunk indices in %s", t, getFile().getName()); } } catch (Exception ex) { diff --git a/src/main/java/net/querz/mcaselector/io/ChunkImporter.java b/src/main/java/net/querz/mcaselector/io/ChunkImporter.java index 907851c3..764fa8ae 100644 --- a/src/main/java/net/querz/mcaselector/io/ChunkImporter.java +++ b/src/main/java/net/querz/mcaselector/io/ChunkImporter.java @@ -1,9 +1,9 @@ package net.querz.mcaselector.io; -import net.querz.mcaselector.ui.ProgressTask; import net.querz.mcaselector.util.Debug; import net.querz.mcaselector.util.Helper; import net.querz.mcaselector.util.Point2i; +import net.querz.mcaselector.util.Progress; import net.querz.mcaselector.util.Timer; import net.querz.mcaselector.util.Translation; import java.io.File; @@ -20,7 +20,7 @@ public class ChunkImporter { private ChunkImporter() {} - public static void importChunks(File importDir, ProgressTask progressChannel, boolean overwrite, Point2i offset) { + public static void importChunks(File importDir, Progress progressChannel, boolean overwrite, Point2i offset) { try { File[] importFiles = importDir.listFiles((dir, name) -> name.matches(Helper.MCA_FILE_PATTERN)); if (importFiles == null || importFiles.length == 0) { @@ -31,7 +31,7 @@ public static void importChunks(File importDir, ProgressTask progressChannel, bo MCAFilePipe.clearQueues(); progressChannel.setMax(importFiles.length * (offset.getX() % 32 != 0 ? 2 : 1) * (offset.getY() % 32 != 0 ? 2 : 1)); - progressChannel.infoProperty().setValue(Translation.DIALOG_PROGRESS_COLLECTING_DATA.toString()); + progressChannel.setMessage(Translation.DIALOG_PROGRESS_COLLECTING_DATA.toString()); Map> targetMapping = new HashMap<>(); @@ -69,10 +69,10 @@ public static class MCAChunkImporterLoadJob extends LoadDataJob { private Set sources; private File sourceDir; private Point2i offset; - private ProgressTask progressChannel; + private Progress progressChannel; private boolean overwrite; - MCAChunkImporterLoadJob(File targetFile, File sourceDir, Point2i target, Set sources, Point2i offset, ProgressTask progressChannel, boolean overwrite) { + MCAChunkImporterLoadJob(File targetFile, File sourceDir, Point2i target, Set sources, Point2i offset, Progress progressChannel, boolean overwrite) { super(targetFile); this.target = target; this.sources = sources; @@ -146,10 +146,10 @@ public static class MCAChunkImporterProcessJob extends ProcessDataJob { private Point2i target; private Map sourceDataMapping; private Point2i offset; - private ProgressTask progressChannel; + private Progress progressChannel; private boolean overwrite; - MCAChunkImporterProcessJob(File targetFile, File sourceDir, Point2i target, Map sourceDataMapping, byte[] destData, Point2i offset, ProgressTask progressChannel, boolean overwrite) { + MCAChunkImporterProcessJob(File targetFile, File sourceDir, Point2i target, Map sourceDataMapping, byte[] destData, Point2i offset, Progress progressChannel, boolean overwrite) { super(targetFile, destData); this.sourceDir = sourceDir; this.target = target; @@ -199,9 +199,9 @@ public void execute() { public static class MCAChunkImporterSaveJob extends SaveDataJob { private int sourceCount; - private ProgressTask progressChannel; + private Progress progressChannel; - MCAChunkImporterSaveJob(File file, MCAFile data, int sourceCount, ProgressTask progressChannel) { + MCAChunkImporterSaveJob(File file, MCAFile data, int sourceCount, Progress progressChannel) { super(file, data); this.sourceCount = sourceCount; this.progressChannel = progressChannel; diff --git a/src/main/java/net/querz/mcaselector/io/FieldChanger.java b/src/main/java/net/querz/mcaselector/io/FieldChanger.java index 0608f1d2..18dcf802 100644 --- a/src/main/java/net/querz/mcaselector/io/FieldChanger.java +++ b/src/main/java/net/querz/mcaselector/io/FieldChanger.java @@ -2,9 +2,9 @@ import net.querz.mcaselector.Config; import net.querz.mcaselector.changer.Field; -import net.querz.mcaselector.ui.ProgressTask; import net.querz.mcaselector.util.Debug; import net.querz.mcaselector.util.Point2i; +import net.querz.mcaselector.util.Progress; import net.querz.mcaselector.util.Timer; import java.io.File; import java.io.RandomAccessFile; @@ -22,7 +22,7 @@ public class FieldChanger { private FieldChanger() {} - public static void changeNBTFields(List fields, boolean force, Map> selection, ProgressTask progressChannel) { + public static void changeNBTFields(List> fields, boolean force, Map> selection, Progress progressChannel) { File[] files = Config.getWorldDir().listFiles((d, n) -> n.matches("^r\\.-?\\d+\\.-?\\d+\\.mca$")); if (files == null || files.length == 0) { return; @@ -40,12 +40,12 @@ public static void changeNBTFields(List fields, boolean force, Map fields; + private Progress progressChannel; + private List> fields; private boolean force; private Map> selection; - MCAFieldChangeLoadJob(File file, List fields, boolean force, Map> selection, ProgressTask progressChannel) { + MCAFieldChangeLoadJob(File file, List> fields, boolean force, Map> selection, Progress progressChannel) { super(file); this.fields = fields; this.force = force; @@ -82,12 +82,12 @@ public void execute() { public static class MCAFieldChangeProcessJob extends ProcessDataJob { - private ProgressTask progressChannel; - private List fields; + private Progress progressChannel; + private List> fields; private boolean force; private Set selection; - MCAFieldChangeProcessJob(File file, byte[] data, List fields, boolean force, Set selection, ProgressTask progressChannel) { + MCAFieldChangeProcessJob(File file, byte[] data, List> fields, boolean force, Set selection, Progress progressChannel) { super(file, data); this.fields = fields; this.force = force; @@ -113,9 +113,9 @@ public void execute() { public static class MCAFieldChangeSaveJob extends SaveDataJob { - private ProgressTask progressChannel; + private Progress progressChannel; - MCAFieldChangeSaveJob(File file, MCAFile data, ProgressTask progressChannel) { + MCAFieldChangeSaveJob(File file, MCAFile data, Progress progressChannel) { super(file, data); this.progressChannel = progressChannel; } diff --git a/src/main/java/net/querz/mcaselector/io/MCAChunkData.java b/src/main/java/net/querz/mcaselector/io/MCAChunkData.java index 7f62cf86..b3068411 100644 --- a/src/main/java/net/querz/mcaselector/io/MCAChunkData.java +++ b/src/main/java/net/querz/mcaselector/io/MCAChunkData.java @@ -95,7 +95,7 @@ public int saveData(RandomAccessFile raf) throws Exception { return rawData.length + 5; } - public void changeData(List fields, boolean force) { + public void changeData(List> fields, boolean force) { for (Field field : fields) { try { if (force) { diff --git a/src/main/java/net/querz/mcaselector/io/MCAFile.java b/src/main/java/net/querz/mcaselector/io/MCAFile.java index 2ea5163c..87f5742a 100644 --- a/src/main/java/net/querz/mcaselector/io/MCAFile.java +++ b/src/main/java/net/querz/mcaselector/io/MCAFile.java @@ -207,7 +207,7 @@ public Set getFilteredChunks(Filter filter) { return chunks; } - public void applyFieldChanges(List fields, boolean force, Set selection) { + public void applyFieldChanges(List> fields, boolean force, Set selection) { for (int cx = 0; cx < Tile.SIZE_IN_CHUNKS; cx++) { for (int cz = 0; cz < Tile.SIZE_IN_CHUNKS; cz++) { int index = cz * Tile.SIZE_IN_CHUNKS + cx; diff --git a/src/main/java/net/querz/mcaselector/io/RegionImageGenerator.java b/src/main/java/net/querz/mcaselector/io/RegionImageGenerator.java index 11bfa219..5b306013 100644 --- a/src/main/java/net/querz/mcaselector/io/RegionImageGenerator.java +++ b/src/main/java/net/querz/mcaselector/io/RegionImageGenerator.java @@ -4,10 +4,10 @@ import javafx.scene.image.Image; import net.querz.mcaselector.Config; import net.querz.mcaselector.tiles.Tile; -import net.querz.mcaselector.tiles.TileMap; import net.querz.mcaselector.util.Debug; import net.querz.mcaselector.util.Helper; import net.querz.mcaselector.util.Point2i; +import net.querz.mcaselector.util.Progress; import net.querz.mcaselector.util.Timer; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; @@ -15,6 +15,7 @@ import java.io.IOException; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; public class RegionImageGenerator { @@ -22,9 +23,9 @@ public class RegionImageGenerator { private RegionImageGenerator() {} - public static void generate(Tile tile, TileMap tileMap) { + public static void generate(Tile tile, Runnable callback, Supplier scaleSupplier, boolean force, boolean scaleOnly, Progress progressChannel) { setLoading(tile, true); - MCAFilePipe.addJob(new MCAImageLoadJob(tile.getMCAFile(), tile, tileMap)); + MCAFilePipe.addJob(new MCAImageLoadJob(tile.getMCAFile(), tile, callback, scaleSupplier, force, scaleOnly, progressChannel)); } public static boolean isLoading(Tile tile) { @@ -43,26 +44,38 @@ public static void setLoading(Tile tile, boolean loading) { public static class MCAImageLoadJob extends LoadDataJob { private Tile tile; - private TileMap tileMap; + private Runnable callback; + private Supplier scaleSupplier; + private boolean force, scaleOnly; + private Progress progressChannel; - MCAImageLoadJob(File file, Tile tile, TileMap tileMap) { + MCAImageLoadJob(File file, Tile tile, Runnable callback, Supplier scaleSupplier, boolean force, boolean scaleOnly, Progress progressChannel) { super(file); this.tile = tile; - this.tileMap = tileMap; + this.callback = callback; + this.scaleSupplier = scaleSupplier; + this.force = force; + this.scaleOnly = scaleOnly; + this.progressChannel = progressChannel; } @Override public void execute() { - tile.loadFromCache(tileMap); + if (!force) { + tile.loadFromCache(callback, scaleSupplier); + } if (!tile.isLoaded()) { byte[] data = load(); if (data != null) { - MCAFilePipe.executeProcessData(new MCAImageProcessJob(getFile(), data, tile, tileMap)); + MCAFilePipe.executeProcessData(new MCAImageProcessJob(getFile(), data, tile, callback, scaleSupplier, scaleOnly, progressChannel)); return; } } setLoading(tile, false); + if (progressChannel != null) { + progressChannel.incrementProgress(Helper.createMCAFileName(tile.getLocation())); + } } public Tile getTile() { @@ -73,21 +86,31 @@ public Tile getTile() { public static class MCAImageProcessJob extends ProcessDataJob { private Tile tile; - private TileMap tileMap; + private Runnable callback; + private Supplier scaleSupplier; + private boolean scaleOnly; + private Progress progressChannel; - MCAImageProcessJob(File file, byte[] data, Tile tile, TileMap tileMap) { + MCAImageProcessJob(File file, byte[] data, Tile tile, Runnable callback, Supplier scaleSupplier, boolean scaleOnly, Progress progressChannel) { super(file, data); this.tile = tile; - this.tileMap = tileMap; + this.callback = callback; + this.scaleSupplier = scaleSupplier; + this.scaleOnly = scaleOnly; + this.progressChannel = progressChannel; } @Override public void execute() { - Image image = tile.generateImage(tileMap, getData()); + Image image = tile.generateImage(callback, getData()); if (image != null) { - MCAFilePipe.executeSaveData(new MCAImageSaveCacheJob(getFile(), image, tile, tileMap)); + MCAFilePipe.executeSaveData(new MCAImageSaveCacheJob(getFile(), image, tile, scaleSupplier, scaleOnly, progressChannel)); } else { setLoading(tile, false); + if (progressChannel != null) { + progressChannel.incrementProgress(Helper.createMCAFileName(tile.getLocation())); + } + } } @@ -99,12 +122,16 @@ public Tile getTile() { public static class MCAImageSaveCacheJob extends SaveDataJob { private Tile tile; - private TileMap tileMap; + private Supplier scaleSupplier; + private boolean scaleOnly; + private Progress progressChannel; - MCAImageSaveCacheJob(File file, Image data, Tile tile, TileMap tileMap) { + MCAImageSaveCacheJob(File file, Image data, Tile tile, Supplier scaleSupplier, boolean scaleOnly, Progress progressChannel) { super(file, data); this.tile = tile; - this.tileMap = tileMap; + this.scaleSupplier = scaleSupplier; + this.scaleOnly = scaleOnly; + this.progressChannel = progressChannel; } @Override @@ -114,20 +141,35 @@ public void execute() { //save image to cache try { BufferedImage img = SwingFXUtils.fromFXImage(getData(), null); - for (int i = Helper.getMinZoomLevel(); i <= Helper.getMaxZoomLevel(); i *= 2) { - File cacheFile = Helper.createPNGFilePath(new File(Config.getCacheDir().getAbsolutePath(), i + ""), tile.getLocation()); + if (scaleOnly) { + int zoomLevel = Helper.getZoomLevel(scaleSupplier.get()); + File cacheFile = Helper.createPNGFilePath(new File(Config.getCacheDir().getAbsolutePath(), zoomLevel + ""), tile.getLocation()); if (!cacheFile.getParentFile().exists() && !cacheFile.getParentFile().mkdirs()) { Debug.errorf("failed to create cache directory for %s", cacheFile.getAbsolutePath()); } - BufferedImage scaled = Helper.scaleImage(img, (double) Tile.SIZE / (double) i); + BufferedImage scaled = Helper.scaleImage(img, (double) Tile.SIZE / (double) zoomLevel); ImageIO.write(scaled, "png", cacheFile); + + } else { + for (int i = Helper.getMinZoomLevel(); i <= Helper.getMaxZoomLevel(); i *= 2) { + File cacheFile = Helper.createPNGFilePath(new File(Config.getCacheDir().getAbsolutePath(), i + ""), tile.getLocation()); + if (!cacheFile.getParentFile().exists() && !cacheFile.getParentFile().mkdirs()) { + Debug.errorf("failed to create cache directory for %s", cacheFile.getAbsolutePath()); + } + + BufferedImage scaled = Helper.scaleImage(img, (double) Tile.SIZE / (double) i); + ImageIO.write(scaled, "png", cacheFile); + } } } catch (IOException e) { e.printStackTrace(); } setLoading(tile, false); + if (progressChannel != null) { + progressChannel.incrementProgress(Helper.createMCAFileName(tile.getLocation())); + } Debug.dumpf("took %s to cache image of %s to %s", t, tile.getMCAFile().getName(), Helper.createPNGFileName(tile.getLocation())); } diff --git a/src/main/java/net/querz/mcaselector/io/SelectionDeleter.java b/src/main/java/net/querz/mcaselector/io/SelectionDeleter.java index 5b124bad..64113f2e 100644 --- a/src/main/java/net/querz/mcaselector/io/SelectionDeleter.java +++ b/src/main/java/net/querz/mcaselector/io/SelectionDeleter.java @@ -1,9 +1,9 @@ package net.querz.mcaselector.io; -import net.querz.mcaselector.ui.ProgressTask; import net.querz.mcaselector.util.Debug; import net.querz.mcaselector.util.Helper; import net.querz.mcaselector.util.Point2i; +import net.querz.mcaselector.util.Progress; import net.querz.mcaselector.util.Timer; import java.io.File; import java.io.RandomAccessFile; @@ -16,7 +16,7 @@ public class SelectionDeleter { private SelectionDeleter() {} - public static void deleteSelection(Map> chunksToBeDeleted, ProgressTask progressChannel) { + public static void deleteSelection(Map> chunksToBeDeleted, Progress progressChannel) { if (chunksToBeDeleted.isEmpty()) { progressChannel.done("no selection"); return; @@ -36,9 +36,9 @@ public static void deleteSelection(Map> chunksToBeDeleted, public static class MCADeleteSelectionLoadJob extends LoadDataJob { private Set chunksToBeDeleted; - private ProgressTask progressChannel; + private Progress progressChannel; - MCADeleteSelectionLoadJob(File file, Set chunksToBeDeleted, ProgressTask progressChannel) { + MCADeleteSelectionLoadJob(File file, Set chunksToBeDeleted, Progress progressChannel) { super(file); this.chunksToBeDeleted = chunksToBeDeleted; this.progressChannel = progressChannel; @@ -67,10 +67,10 @@ public void execute() { public static class MCADeleteSelectionProcessJob extends ProcessDataJob { - private ProgressTask progressChannel; + private Progress progressChannel; private Set chunksToBeDeleted; - MCADeleteSelectionProcessJob(File file, byte[] data, Set chunksToBeDeleted, ProgressTask progressChannel) { + MCADeleteSelectionProcessJob(File file, byte[] data, Set chunksToBeDeleted, Progress progressChannel) { super(file, data); this.chunksToBeDeleted = chunksToBeDeleted; this.progressChannel = progressChannel; @@ -96,9 +96,9 @@ public void execute() { public static class MCADeleteSelectionSaveJob extends SaveDataJob { - private ProgressTask progressChannel; + private Progress progressChannel; - MCADeleteSelectionSaveJob(File file, MCAFile data, ProgressTask progressChannel) { + MCADeleteSelectionSaveJob(File file, MCAFile data, Progress progressChannel) { super(file, data); this.progressChannel = progressChannel; } diff --git a/src/main/java/net/querz/mcaselector/io/SelectionExporter.java b/src/main/java/net/querz/mcaselector/io/SelectionExporter.java index 604ad663..146fa4b6 100644 --- a/src/main/java/net/querz/mcaselector/io/SelectionExporter.java +++ b/src/main/java/net/querz/mcaselector/io/SelectionExporter.java @@ -1,10 +1,10 @@ package net.querz.mcaselector.io; -import net.querz.mcaselector.ui.ProgressTask; import net.querz.mcaselector.tiles.Tile; import net.querz.mcaselector.util.Debug; import net.querz.mcaselector.util.Helper; import net.querz.mcaselector.util.Point2i; +import net.querz.mcaselector.util.Progress; import net.querz.mcaselector.util.Timer; import java.io.*; import java.nio.file.Files; @@ -17,7 +17,7 @@ public class SelectionExporter { private SelectionExporter() {} - public static void exportSelection(Map> chunksToBeExported, File destination, ProgressTask progressChannel) { + public static void exportSelection(Map> chunksToBeExported, File destination, Progress progressChannel) { if (chunksToBeExported.isEmpty()) { progressChannel.done("no selection"); return; @@ -42,9 +42,9 @@ public static class MCADeleteSelectionLoadJob extends LoadDataJob { private Set chunksToBeExported; private File destination; - private ProgressTask progressChannel; + private Progress progressChannel; - MCADeleteSelectionLoadJob(File file, Set chunksToBeExported, File destination, ProgressTask progressChannel) { + MCADeleteSelectionLoadJob(File file, Set chunksToBeExported, File destination, Progress progressChannel) { super(file); this.chunksToBeExported = chunksToBeExported; this.destination = destination; @@ -75,11 +75,11 @@ public void execute() { public static class MCADeleteSelectionProcessJob extends ProcessDataJob { - private ProgressTask progressChannel; + private Progress progressChannel; private Set chunksToBeExported; private File destination; - MCADeleteSelectionProcessJob(File file, byte[] data, Set chunksToBeExported, File destination, ProgressTask progressChannel) { + MCADeleteSelectionProcessJob(File file, byte[] data, Set chunksToBeExported, File destination, Progress progressChannel) { super(file, data); this.chunksToBeExported = chunksToBeExported; this.destination = destination; @@ -119,10 +119,10 @@ public void execute() { public static class MCADeleteSelectionSaveJob extends SaveDataJob { - private ProgressTask progressChannel; + private Progress progressChannel; private File destination; - MCADeleteSelectionSaveJob(File file, MCAFile data, File destination, ProgressTask progressChannel) { + MCADeleteSelectionSaveJob(File file, MCAFile data, File destination, Progress progressChannel) { super(file, data); this.destination = destination; this.progressChannel = progressChannel; diff --git a/src/main/java/net/querz/mcaselector/tiles/Tile.java b/src/main/java/net/querz/mcaselector/tiles/Tile.java index 45098bc9..b8f67f7c 100644 --- a/src/main/java/net/querz/mcaselector/tiles/Tile.java +++ b/src/main/java/net/querz/mcaselector/tiles/Tile.java @@ -2,7 +2,6 @@ import javafx.application.Platform; import javafx.embed.swing.SwingFXUtils; -import javafx.scene.Scene; import javafx.scene.SnapshotParameters; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; @@ -18,11 +17,12 @@ import net.querz.mcaselector.util.Point2f; import net.querz.mcaselector.util.Point2i; import net.querz.mcaselector.util.Timer; - import java.awt.image.BufferedImage; import java.io.*; import java.util.HashSet; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; public class Tile { @@ -238,7 +238,7 @@ public void mergeImage(BufferedImage image, Point2i location) { this.image = SwingFXUtils.toFXImage(bufferedImage, null); } - public void loadFromCache(TileMap tileMap) { + public void loadFromCache(Runnable callback, Supplier scaleSupplier) { if (loaded) { Debug.dump("region at " + location + " already loaded"); return; @@ -247,25 +247,25 @@ public void loadFromCache(TileMap tileMap) { if (Config.getCacheDir() == null) { //load empty map (start screen) loaded = true; - Platform.runLater(tileMap::update); + callback.run(); return; } - String res = String.format(Config.getCacheDir().getAbsolutePath() + "/" + Helper.getZoomLevel(tileMap.getScale()) + "/r.%d.%d.png", location.getX(), location.getY()); + String res = String.format(Config.getCacheDir().getAbsolutePath() + "/" + Helper.getZoomLevel(scaleSupplier.get()) + "/r.%d.%d.png", location.getX(), location.getY()); Debug.dump("loading region " + location + " from cache: " + res); try (InputStream inputStream = new FileInputStream(res)) { image = new Image(inputStream); loaded = true; - Platform.runLater(tileMap::update); + callback.run(); } catch (IOException ex) { Debug.dump("region " + location + " not cached"); //do nothing } } - public Image generateImage(TileMap tileMap, byte[] rawData) { + public Image generateImage(Runnable callback, byte[] rawData) { if (loaded) { Debug.dump("region at " + location + " already loaded"); return image; @@ -291,7 +291,7 @@ public Image generateImage(TileMap tileMap, byte[] rawData) { image = mcaFile.createImage(ptr); loaded = true; - Platform.runLater(tileMap::update); + callback.run(); Debug.dumpf("took %s to generate image of %s", t, file.getName()); diff --git a/src/main/java/net/querz/mcaselector/tiles/TileMap.java b/src/main/java/net/querz/mcaselector/tiles/TileMap.java index f8869a5d..ba5e6f5e 100644 --- a/src/main/java/net/querz/mcaselector/tiles/TileMap.java +++ b/src/main/java/net/querz/mcaselector/tiles/TileMap.java @@ -423,7 +423,7 @@ private void draw(GraphicsContext ctx) { Point2i regionOffset = Helper.regionToBlock(region).sub((int) offset.getX(), (int) offset.getY()); if (!tile.isLoaded() && !tile.isLoading()) { - RegionImageGenerator.generate(tile, this); + RegionImageGenerator.generate(tile, () -> Platform.runLater(this::update), this::getScale, false, false, null); } Point2f p = new Point2f(regionOffset.getX() / scale, regionOffset.getY() / scale); tile.draw(ctx, scale, p); diff --git a/src/main/java/net/querz/mcaselector/ui/ChangeNBTDialog.java b/src/main/java/net/querz/mcaselector/ui/ChangeNBTDialog.java index 3c0bf6bd..a4d07325 100644 --- a/src/main/java/net/querz/mcaselector/ui/ChangeNBTDialog.java +++ b/src/main/java/net/querz/mcaselector/ui/ChangeNBTDialog.java @@ -10,13 +10,13 @@ import javafx.scene.control.Separator; import javafx.scene.control.TextField; import javafx.scene.control.ToggleGroup; -import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.stage.StageStyle; import net.querz.mcaselector.changer.Field; import net.querz.mcaselector.changer.FieldType; +import net.querz.mcaselector.util.Debug; import net.querz.mcaselector.util.Translation; import net.querz.mcaselector.util.UIFactory; @@ -28,11 +28,11 @@ public class ChangeNBTDialog extends Dialog { /* * List of fields that can be changed * () change () force - * change --> only set if the value existed before - * force --> set value even if it didn't exist. + * change --> only set if the fields existed before + * force --> set fields even if it didn't exist. * */ - private List value = new ArrayList<>(); + private List> fields = new ArrayList<>(); private ToggleGroup toggleGroup = new ToggleGroup(); private RadioButton change = UIFactory.radio(Translation.DIALOG_CHANGE_NBT_CHANGE); private RadioButton force = UIFactory.radio(Translation.DIALOG_CHANGE_NBT_FORCE); @@ -47,13 +47,13 @@ public ChangeNBTDialog(Stage primaryStage) { setResultConverter(p -> { if (p == ButtonType.OK) { - for (int i = 0; i < value.size(); i++) { - if (!value.get(i).needsChange()) { - value.remove(i); + for (int i = 0; i < fields.size(); i++) { + if (!fields.get(i).needsChange()) { + fields.remove(i); i--; } } - return value.isEmpty() ? null : new Result(value, force.isSelected(), selectionOnly.isSelected()); + return fields.isEmpty() ? null : new Result(fields, force.isSelected(), selectionOnly.isSelected()); } return null; }); @@ -67,7 +67,7 @@ public ChangeNBTDialog(Stage primaryStage) { for (FieldType ft : FieldType.values()) { Field f = ft.newInstance(); fw.addField(f); - value.add(f); + fields.add(f); } toggleGroup.getToggles().addAll(change, force); @@ -114,17 +114,29 @@ public FieldCell(Field value, int index) { getStyleClass().add("field-cell-" + (index % 2 == 0 ? "even" : "odd")); this.value = value; textField = new TextField(); - getChildren().addAll(new Label(value.toString()), textField); + getChildren().addAll(new Label(value.getType().toString()), textField); textField.textProperty().addListener((a, o, n) -> onInput(n)); textField.setAlignment(Pos.CENTER); } private void onInput(String newValue) { - Boolean result = value.parseNewValue(newValue); + boolean result = value.parseNewValue(newValue); if (result) { if (!textField.getStyleClass().contains("field-cell-valid")) { textField.getStyleClass().add("field-cell-valid"); } + + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Field field : fields) { + if (field.needsChange()) { + sb.append(first ? "" : ", ").append(field); + first = false; + } + } + if (sb.length() > 0) { + Debug.dump(sb); + } } else { textField.getStyleClass().remove("field-cell-valid"); } @@ -134,10 +146,10 @@ private void onInput(String newValue) { public class Result { private boolean force; - private List fields; + private List> fields; private boolean selectionOnly; - public Result(List fields, boolean force, boolean selectionOnly) { + public Result(List> fields, boolean force, boolean selectionOnly) { this.force = force; this.fields = fields; this.selectionOnly = selectionOnly; @@ -147,7 +159,7 @@ public boolean isForce() { return force; } - public List getFields() { + public List> getFields() { return fields; } diff --git a/src/main/java/net/querz/mcaselector/ui/FilterChunksDialog.java b/src/main/java/net/querz/mcaselector/ui/FilterChunksDialog.java index 0e72daa0..47be8066 100644 --- a/src/main/java/net/querz/mcaselector/ui/FilterChunksDialog.java +++ b/src/main/java/net/querz/mcaselector/ui/FilterChunksDialog.java @@ -68,7 +68,9 @@ public FilterChunksDialog(Stage primaryStage) { groupFilterBox.setOnUpdate(f -> { getDialogPane().lookupButton(ButtonType.OK).setDisable(!value.isValid()); - Debug.dump(value); + if (value.isValid()) { + Debug.dump(value); + } }); VBox actionBox = new VBox(); diff --git a/src/main/java/net/querz/mcaselector/ui/OptionBar.java b/src/main/java/net/querz/mcaselector/ui/OptionBar.java index 0e3e5816..1bd62e1b 100644 --- a/src/main/java/net/querz/mcaselector/ui/OptionBar.java +++ b/src/main/java/net/querz/mcaselector/ui/OptionBar.java @@ -1,7 +1,8 @@ package net.querz.mcaselector.ui; import javafx.scene.control.*; -import javafx.scene.input.KeyCombination; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; import javafx.stage.Stage; import net.querz.mcaselector.tiles.TileMap; import net.querz.mcaselector.util.Helper; @@ -85,21 +86,22 @@ public OptionBar(TileMap tileMap, Stage primaryStage) { filterChunks.setOnAction(e -> Helper.filterChunks(tileMap, primaryStage)); changeFields.setOnAction(e -> Helper.changeFields(tileMap, primaryStage)); - open.setAccelerator(KeyCombination.keyCombination("Ctrl+O")); - chunkGrid.setAccelerator(KeyCombination.keyCombination("Ctrl+R")); - regionGrid.setAccelerator(KeyCombination.keyCombination("Ctrl+T")); - goTo.setAccelerator(KeyCombination.keyCombination("Ctrl+G")); - clearAllCache.setAccelerator(KeyCombination.keyCombination("Ctrl+Shift+K")); - clearViewCache.setAccelerator(KeyCombination.keyCombination("Ctrl+K")); - clear.setAccelerator(KeyCombination.keyCombination("Ctrl+L")); - exportChunks.setAccelerator(KeyCombination.keyCombination("Ctrl+Shift+E")); - importChunks.setAccelerator(KeyCombination.keyCombination("Ctrl+Shift+I")); - delete.setAccelerator(KeyCombination.keyCombination("Ctrl+D")); - importSelection.setAccelerator(KeyCombination.keyCombination("Ctrl+I")); - exportSelection.setAccelerator(KeyCombination.keyCombination("Ctrl+E")); - clearSelectionCache.setAccelerator(KeyCombination.keyCombination("Ctrl+J")); - filterChunks.setAccelerator(KeyCombination.keyCombination("Ctrl+F")); - changeFields.setAccelerator(KeyCombination.keyCombination("Ctrl+N")); + open.setAccelerator(new KeyCodeCombination(KeyCode.O, KeyCodeCombination.SHORTCUT_DOWN)); + quit.setAccelerator(new KeyCodeCombination(KeyCode.Q, KeyCodeCombination.SHORTCUT_DOWN)); + chunkGrid.setAccelerator(new KeyCodeCombination(KeyCode.T, KeyCodeCombination.SHORTCUT_DOWN)); + regionGrid.setAccelerator(new KeyCodeCombination(KeyCode.R, KeyCodeCombination.SHORTCUT_DOWN)); + goTo.setAccelerator(new KeyCodeCombination(KeyCode.G, KeyCodeCombination.SHORTCUT_DOWN)); + clearAllCache.setAccelerator(new KeyCodeCombination(KeyCode.K, KeyCodeCombination.SHORTCUT_DOWN, KeyCodeCombination.SHIFT_DOWN)); + clearViewCache.setAccelerator(new KeyCodeCombination(KeyCode.K, KeyCodeCombination.SHORTCUT_DOWN)); + clear.setAccelerator(new KeyCodeCombination(KeyCode.L, KeyCodeCombination.SHORTCUT_DOWN)); + exportChunks.setAccelerator(new KeyCodeCombination(KeyCode.E, KeyCodeCombination.SHORTCUT_DOWN, KeyCodeCombination.SHIFT_DOWN)); + importChunks.setAccelerator(new KeyCodeCombination(KeyCode.I, KeyCodeCombination.SHORTCUT_DOWN, KeyCodeCombination.SHIFT_DOWN)); + delete.setAccelerator(new KeyCodeCombination(KeyCode.D, KeyCodeCombination.SHORTCUT_DOWN)); + importSelection.setAccelerator(new KeyCodeCombination(KeyCode.I, KeyCodeCombination.SHORTCUT_DOWN)); + exportSelection.setAccelerator(new KeyCodeCombination(KeyCode.E, KeyCodeCombination.SHORTCUT_DOWN)); + clearSelectionCache.setAccelerator(new KeyCodeCombination(KeyCode.J, KeyCodeCombination.SHORTCUT_DOWN)); + filterChunks.setAccelerator(new KeyCodeCombination(KeyCode.F, KeyCodeCombination.SHORTCUT_DOWN)); + changeFields.setAccelerator(new KeyCodeCombination(KeyCode.N, KeyCodeCombination.SHORTCUT_DOWN)); setSelectionDependentMenuItemsEnabled(false); setWorldDependentMenuItemsEnabled(false); diff --git a/src/main/java/net/querz/mcaselector/ui/ProgressTask.java b/src/main/java/net/querz/mcaselector/ui/ProgressTask.java index 45f8de74..d4ffc635 100644 --- a/src/main/java/net/querz/mcaselector/ui/ProgressTask.java +++ b/src/main/java/net/querz/mcaselector/ui/ProgressTask.java @@ -4,9 +4,10 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.concurrent.Task; +import net.querz.mcaselector.util.Progress; import net.querz.mcaselector.util.Translation; -public abstract class ProgressTask extends Task { +public abstract class ProgressTask extends Task implements Progress { public int max; private int current = 0; @@ -20,14 +21,17 @@ public ProgressTask(int max) { this.max = max; } + @Override public void setMax(int max) { this.max = max; } + @Override public void incrementProgress(String info) { updateProgress(info, ++current, max); } + @Override public void incrementProgress(String info, int count) { updateProgress(info, current += count, max); } @@ -36,16 +40,23 @@ public void setLocked(boolean locked) { this.locked = locked; } + @Override public void done(String info) { updateProgress(info, 1, 1); } + @Override + public void setMessage(String msg) { + infoProperty.setValue(msg); + } + public void setIndeterminate(String info) { Platform.runLater(() -> infoProperty.setValue(info)); updateProgress(-1, 0); } - public void updateProgress(String info, double progress) { + @Override + public void updateProgress(String info, int progress) { updateProgress(info, progress, max); } @@ -66,17 +77,4 @@ public StringProperty infoProperty() { public void setOnFinish(Runnable r) { onFinish = r; } - - public static class Dummy extends ProgressTask { - - @Override - public void updateProgress(String info, double progress, int max) { - - } - - @Override - protected Void call() { - return null; - } - } } diff --git a/src/main/java/net/querz/mcaselector/util/Debug.java b/src/main/java/net/querz/mcaselector/util/Debug.java index 5fb1492f..bb4ce238 100644 --- a/src/main/java/net/querz/mcaselector/util/Debug.java +++ b/src/main/java/net/querz/mcaselector/util/Debug.java @@ -2,6 +2,8 @@ import net.querz.mcaselector.Config; +import java.util.Arrays; + public class Debug { public static void dump(Object... objects) { @@ -31,4 +33,12 @@ public static void error(Object... objects) { public static void errorf(String format, Object... objects) { System.out.printf(format + "\n", objects); } + + public static void print(Object... objects) { + Arrays.stream(objects).forEach(System.out::println); + } + + public static void printf(String format, Object... objects) { + System.out.printf(format + "\n", objects); + } } diff --git a/src/main/java/net/querz/mcaselector/util/Helper.java b/src/main/java/net/querz/mcaselector/util/Helper.java index 76003f43..4d5a3513 100644 --- a/src/main/java/net/querz/mcaselector/util/Helper.java +++ b/src/main/java/net/querz/mcaselector/util/Helper.java @@ -1,5 +1,6 @@ package net.querz.mcaselector.util; +import javafx.application.Platform; import javafx.scene.control.ButtonType; import javafx.scene.control.Slider; import javafx.scene.control.TextField; @@ -225,6 +226,27 @@ public static Point2i getZoomLevelOrigin(Point2i region, int zoomLevel) { return result; } + public static void forceGenerateCache(Integer zoomLevel, Progress progressChannel) { + File[] files = Config.getWorldDir().listFiles((d, n) -> n.matches(Helper.MCA_FILE_PATTERN)); + if (files == null || files.length == 0) { + return; + } + + progressChannel.setMax(files.length); + progressChannel.updateProgress(files[0].getName(), 0); + + for (File file : files) { + Matcher m = REGION_GROUP_PATTERN.matcher(file.getName()); + if (m.find()) { + int x = Integer.parseInt(m.group("regionX")); + int z = Integer.parseInt(m.group("regionZ")); + boolean scaleOnly = zoomLevel != null; + float zoomLevelSupplier = scaleOnly ? zoomLevel : 1; + RegionImageGenerator.generate(new Tile(new Point2i(x, z)), () -> {}, () -> zoomLevelSupplier, true, scaleOnly, progressChannel); + } + } + } + public static Image loadCachedImage(Point2i origin, int zoomLevel) { try (FileInputStream fis = new FileInputStream(Helper.createPNGFilePath(origin, zoomLevel))) { return new Image(fis); @@ -488,7 +510,10 @@ public static void filterChunks(TileMap tileMap, Stage primaryStage) { case SELECT: tileMap.clearSelection(); new ProgressDialog(Translation.DIALOG_PROGRESS_TITLE_SELECTING_FILTERED_CHUNKS, primaryStage) - .showProgressBar(t -> ChunkFilterSelector.selectFilter(r.getFilter(), tileMap, t)); + .showProgressBar(t -> ChunkFilterSelector.selectFilter(r.getFilter(), selection -> Platform.runLater(() -> { + tileMap.addMarkedChunks(selection); + tileMap.update(); + }), t)); break; default: Debug.dump("i have no idea how you got no selection there..."); diff --git a/src/main/java/net/querz/mcaselector/util/Progress.java b/src/main/java/net/querz/mcaselector/util/Progress.java new file mode 100644 index 00000000..8b7a80f7 --- /dev/null +++ b/src/main/java/net/querz/mcaselector/util/Progress.java @@ -0,0 +1,16 @@ +package net.querz.mcaselector.util; + +public interface Progress { + + void setMax(int max); + + void updateProgress(String msg, int progress); + + void done(String msg); + + void incrementProgress(String msg); + + void incrementProgress(String msg, int progress); + + void setMessage(String msg); +} diff --git a/src/main/java/net/querz/mcaselector/util/Translation.java b/src/main/java/net/querz/mcaselector/util/Translation.java index 3c4e5a22..261ebaf6 100644 --- a/src/main/java/net/querz/mcaselector/util/Translation.java +++ b/src/main/java/net/querz/mcaselector/util/Translation.java @@ -74,7 +74,6 @@ public enum Translation { DIALOG_EXPORT_CHUNKS_CONFIRMATION_HEADER_SHORT("dialog.export_chunks_confirmation.header_short"), DIALOG_EXPORT_CHUNKS_CONFIRMATION_HEADER_VERBOSE("dialog.export_chunks_confirmation.header_verbose"), DIALOG_FILTER_CHUNKS_TITLE("dialog.filter_chunks.title"), - DIALOG_FILTER_CHUNKS_FILTER_GROUP("dialog.filter_chunks.filter.group"), DIALOG_FILTER_CHUNKS_FILTER_ADD_TOOLTIP("dialog.filter_chunks.filter.add.tooltip"), DIALOG_FILTER_CHUNKS_FILTER_DELETE_TOOLTIP("dialog.filter_chunks.filter.delete.tooltip"), DIALOG_FILTER_CHUNKS_FILTER_TYPE_TOOLTIP("dialog.filter_chunks.filter.type.tooltip"), diff --git a/src/main/resources/lang/cs_CZ.txt b/src/main/resources/lang/cs_CZ.txt index 271de6f8..0073226c 100644 --- a/src/main/resources/lang/cs_CZ.txt +++ b/src/main/resources/lang/cs_CZ.txt @@ -56,7 +56,6 @@ dialog.export_chunks_confirmation.title;Exportovat chunky dialog.export_chunks_confirmation.header_short;Chcete exportovat neznámý počet chunků vybraného světa. dialog.export_chunks_confirmation.header_verbose;Chcete exportovat %d chunků vybraného světa. dialog.filter_chunks.title;Filtrovat chunky -dialog.filter_chunks.filter.group;Skupina dialog.filter_chunks.filter.add.tooltip;Přidá další podmínku pod stávající. dialog.filter_chunks.filter.delete.tooltip;Smaže tuto podmínku. dialog.filter_chunks.filter.type.tooltip;Typ podmínky. diff --git a/src/main/resources/lang/en_GB.txt b/src/main/resources/lang/en_GB.txt index f87cd4b1..a3313bac 100644 --- a/src/main/resources/lang/en_GB.txt +++ b/src/main/resources/lang/en_GB.txt @@ -56,7 +56,6 @@ dialog.export_chunks_confirmation.title;Export chunks dialog.export_chunks_confirmation.header_short;You are about to export an unknown number of chunks from this world. dialog.export_chunks_confirmation.header_verbose;You are about to export %d chunks from this world. dialog.filter_chunks.title;Filter chunks -dialog.filter_chunks.filter.group;Group dialog.filter_chunks.filter.add.tooltip;Add a new condition below this one. dialog.filter_chunks.filter.delete.tooltip;Delete this condition. dialog.filter_chunks.filter.type.tooltip;The type of this condition. diff --git a/src/main/resources/lang/zh_CN.txt b/src/main/resources/lang/zh_CN.txt index 4c0b7f08..50c09f1c 100644 --- a/src/main/resources/lang/zh_CN.txt +++ b/src/main/resources/lang/zh_CN.txt @@ -56,7 +56,6 @@ dialog.export_chunks_confirmation.title;导出区块 dialog.export_chunks_confirmation.header_short;您将要从这个世界导出未知数量的区块. dialog.export_chunks_confirmation.header_verbose;您将要从这个世界导出 %d 个区块. dialog.filter_chunks.title;筛选区块 -dialog.filter_chunks.filter.group;组 dialog.filter_chunks.filter.add.tooltip;在此条件下添加新条件. dialog.filter_chunks.filter.delete.tooltip;删除此条件. dialog.filter_chunks.filter.type.tooltip;此条件的类型.