├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── kotlin │ └── br │ └── com │ └── gamemods │ └── regionmanipulator │ ├── Chunk.kt │ ├── ChunkPos.kt │ ├── CorruptChunk.kt │ ├── CorruptChunkException.kt │ ├── Region.kt │ ├── RegionIO.kt │ └── RegionPos.kt ├── pages ├── _config.yml └── assets │ └── css │ └── style.scss └── test ├── kotlin └── br │ └── com │ └── gamemods │ └── regionmanipulator │ └── RegionTest.kt └── resources ├── issue-3.r.0.-1.mca ├── issue-4-v1.15.1-r.0.0.mca ├── issue.2.r.-1.-1.mca ├── r.-1.-2.mca └── r.1.-1.mca /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: 4 | pull_request: 5 | branches-ignore: 'gh-pages' 6 | push: 7 | branches-ignore: 'gh-pages' 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/cache@v1 17 | with: 18 | path: ~/.gradle/caches 19 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 20 | restore-keys: | 21 | ${{ runner.os }}-gradle- 22 | - name: Set up JDK 1.8 23 | uses: actions/setup-java@v1 24 | with: 25 | java-version: 1.8 26 | - name: Build with Gradle 27 | run: ./gradlew build -PbinariesOnly 28 | - name: Rename artifacts 29 | run: mv build/libs/region-manipulator-*.jar build/libs/region-manipulator.jar 30 | - name: Archive artifacts 31 | uses: actions/upload-artifact@v1 32 | if: success() 33 | with: 34 | name: Region-Manipulator 35 | path: build/libs/region-manipulator.jar 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | *.iws 4 | *.ipr 5 | .idea 6 | nukkit-*.jar 7 | out/ 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "gh-pages"] 2 | path = gh-pages 3 | url = https://github.com/GameModsBR/Region-Manipulator.git 4 | branch = gh-pages 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | Click the link above to see the future. 9 | 10 | ## [2.0.0] - 2020-01-24 11 | [Downloads from maven central.][Download 2.0.0] 12 | 13 | [Kotlin Documentation][KDoc 2.0.0] 14 | 15 | ### Changed 16 | - Added new properties to the `data class CorruptChunk` 17 | - Changed the `CorruptChunk` constructors **(Breaking Change)** 18 | - `CorruptChunk.chunkContent` is now nullable **(Breaking Change)** 19 | - `RegionIO.readRegion` and `RegionIO.writeRegion` now throws `IOException` in Java **(Breaking Change)** 20 | - `RegionIO.readRegion` can now handle more corrupted chunks scenarios, preventing total failures while reading the MCA file 21 | - Updated [NBT-Manipulator to `2.0.0`][NBT 2.0.0] 22 | - Updated Kotlin to 1.3.61 23 | 24 | ### Fixed 25 | - [#4] `EOFException` when attempt to read a MCA file which contains incomplete corrupted chunks 26 | 27 | ## [1.1.0] - 2019-06-02 28 | [Downloads from maven central.][Download 1.1.0] 29 | 30 | [Kotlin Documentation][KDoc 1.1.0] 31 | 32 | ### Added 33 | - Methods to manipulate corrupt chunks 34 | 35 | ### Changed 36 | - Updated [NBT-Manipulator to `1.1.0`][NBT 1.1.0] 37 | 38 | ### Fixed 39 | - [#2] Corrupt chunk prevents the entire region to load 40 | - [#3] KotlinNullPointerException when reading some region files 41 | 42 | ## [1.0.1] - 2019-05-27 43 | [Downloads from maven central.][Download 1.0.1] 44 | 45 | [Kotlin Documentation][KDoc 1.0.1] 46 | 47 | ### Changed 48 | - Updated [NBT-Manipulator to `1.0.1`][NBT 1.0.1] 49 | 50 | ## [1.0.0] - 2019-05-27 51 | [Downloads from maven central.][Download 1.0.0] 52 | 53 | [Kotlin Documentation][KDoc 1.0.0] 54 | 55 | ### Changed 56 | - Updated [NBT-Manipulator to `1.0.0`][NBT 1.0.0] 57 | 58 | ## [0.0.4] - 2019-05-27 59 | [Downloads from maven central.][Download 0.0.4] 60 | 61 | ### Added 62 | - This changelog file 63 | - Documentation to all public types, methods and properties. 64 | - Static methods for java users calling `RegionIO` 65 | - `Region.addAll` for java users. 66 | - New constructor to `RegionPos` which accepts the region file name. 67 | 68 | ### Changed 69 | - `Region.put` will now check if the key matches the value's position 70 | - `Region.addAllNotNull` is now synthetic, java users should always call `Region.addAll` 71 | - `Region.addAllNullable`is now deprecated. Java users should always call `Region.addAll` 72 | - Updated [NBT-Manipulator to `0.0.2`][NBT 0.0.2] 73 | 74 | ### Fixed 75 | - Potential exception when trying to remove a chunk that is not valid for the region 76 | 77 | ### Changed 78 | - `RegionIO.ChunkInfo` to private. 79 | 80 | ## [0.0.3] - 2019-05-25 81 | [Downloads from maven central.][Download 0.0.3] 82 | ### Changed 83 | - The dependency to [NBT-Manipulator] from `implementation` to `compile` so it can get inherited. 84 | 85 | ## [0.0.2] - 2019-05-25 86 | [Downloads from maven central.][Download 0.0.2] 87 | ### Changed 88 | - JavaDoc will not generate when building on Java 9+ due to a dokka issue 89 | - The targetCompatibility to Java 8 90 | - The `RegionIO.deflate` method is now private 91 | 92 | ### Fixed 93 | - [#1] IndexOutOfBoundsException when writing an empty chunk 94 | 95 | ## [0.0.1] - 2019-05-23 96 | [Downloads from maven central.][Download 0.0.1] 97 | ### Added 98 | - API to read and write to/from MCA files using `RegionIO` 99 | - API to freely manipulate `Region` and `Chunk` data loaded in memory 100 | 101 | [Unreleased]: https://github.com/GameModsBR/Region-Manipulator/compare/v2.0.0...HEAD 102 | [2.0.0]: https://github.com/GameModsBR/Region-Manipulator/compare/v1.1.0..v2.0.0 103 | [1.1.0]: https://github.com/GameModsBR/Region-Manipulator/compare/v1.0.1..v1.1.0 104 | [1.0.1]: https://github.com/GameModsBR/Region-Manipulator/compare/v1.0.0..v1.0.1 105 | [1.0.0]: https://github.com/GameModsBR/Region-Manipulator/compare/v0.0.4..v1.0.0 106 | [0.0.4]: https://github.com/GameModsBR/Region-Manipulator/compare/v0.0.3..v0.0.4 107 | [0.0.3]: https://github.com/GameModsBR/Region-Manipulator/compare/v0.0.2..v0.0.3 108 | [0.0.2]: https://github.com/GameModsBR/Region-Manipulator/compare/v0.0.1..v0.0.2 109 | [0.0.1]: https://github.com/GameModsBR/Region-Manipulator/compare/v0.0.0..v0.0.1 110 | 111 | [Download 2.0.0]: http://central.maven.org/maven2/br/com/gamemods/region-manipulator/2.0.0/ 112 | [Download 1.1.0]: http://central.maven.org/maven2/br/com/gamemods/region-manipulator/1.1.0/ 113 | [Download 1.0.1]: http://central.maven.org/maven2/br/com/gamemods/region-manipulator/1.0.1/ 114 | [Download 1.0.0]: http://central.maven.org/maven2/br/com/gamemods/region-manipulator/1.0.0/ 115 | [Download 0.0.4]: http://central.maven.org/maven2/br/com/gamemods/region-manipulator/0.0.4/ 116 | [Download 0.0.3]: http://central.maven.org/maven2/br/com/gamemods/region-manipulator/0.0.3/ 117 | [Download 0.0.2]: http://central.maven.org/maven2/br/com/gamemods/region-manipulator/0.0.2/ 118 | [Download 0.0.1]: http://central.maven.org/maven2/br/com/gamemods/region-manipulator/0.0.1/ 119 | 120 | [KDoc 2.0.0]: https://github.com/GameModsBR/Region-Manipulator/blob/fceac1330da02c9a8ebf65ec13c8f48c00694e01/kdoc/br.com.gamemods.regionmanipulator/index.md 121 | [KDoc 1.1.0]: https://github.com/GameModsBR/Region-Manipulator/blob/3f6f29a823df9ce6f0c4b30ff35900119f7a62af/kdoc/br.com.gamemods.regionmanipulator/index.md 122 | [KDoc 1.0.1]: https://github.com/GameModsBR/Region-Manipulator/blob/d8893b801af7a65977b2b457009902da8cd10d47/kdoc/br.com.gamemods.regionmanipulator/index.md 123 | [KDoc 1.0.0]: https://github.com/GameModsBR/Region-Manipulator/blob/4bea23fa037af955505ed1aff78fbae8e87a589a/kdoc/br.com.gamemods.regionmanipulator/index.md 124 | 125 | [NBT-Manipulator]: https://github.com/GameModsBR/NBT-Manipulator/ 126 | [NBT 2.0.0]: https://gamemodsbr.github.io/NBT-Manipulator/CHANGELOG.html#200---2020-01-24 127 | [NBT 1.1.0]: https://gamemodsbr.github.io/NBT-Manipulator/CHANGELOG.html#110---2019-06-02 128 | [NBT 1.0.1]: https://gamemodsbr.github.io/NBT-Manipulator/CHANGELOG.html#101---2019-05-27 129 | [NBT 1.0.0]: https://gamemodsbr.github.io/NBT-Manipulator/CHANGELOG.html#100---2019-05-27 130 | [NBT 0.0.2]: https://gamemodsbr.github.io/NBT-Manipulator/CHANGELOG.html#002---2019-05-27 131 | 132 | [#1]: https://github.com/GameModsBR/Region-Manipulator/issues/1 133 | [#2]: https://github.com/GameModsBR/Region-Manipulator/issues/2 134 | [#3]: https://github.com/GameModsBR/Region-Manipulator/issues/3 135 | [#4]: https://github.com/GameModsBR/Region-Manipulator/issues/4 136 | 137 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 José Roberto de Araújo Júnior 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Region Manipulator 2 | A Kotlin/Java library that allows you to read and write `mca` files in a simple way. 3 | 4 | Here you can find the library documentation: 5 | * [Java Documentation](https://gamemodsbr.github.io/Region-Manipulator/javadoc) 6 | * [Kotlin Documentation](https://gamemodsbr.github.io/Region-Manipulator/kdoc/br.com.gamemods.regionmanipulator/index.html) 7 | 8 | 9 | You may also want to see the [changelog](CHANGELOG.md) file to be aware of all changes in the tool that may impact you. 10 | 11 | ## Adding to your project 12 | The library is shared in the maven center, so you don't need to declare any custom repository. 13 | 14 | ### Gradle 15 | ```groovy 16 | repositories { 17 | mavenCentral() // or jcenter() 18 | } 19 | 20 | dependencies { 21 | compile 'br.com.gamemods:region-manipulator:2.0.0' 22 | } 23 | ``` 24 | 25 | ### Maven 26 | ```xml 27 | 28 | 29 | br.com.gamemods 30 | region-manipulator 31 | 2.0.0 32 | 33 | 34 | ``` 35 | 36 | ### Ivy 37 | ```xml 38 | 39 | ``` 40 | 41 | ### Direct JAR 42 | Download it from [maven central](http://central.maven.org/maven2/br/com/gamemods/region-manipulator/). 43 | 44 | ## Examples 45 | ```kotlin 46 | internal fun clearEntities(from: File, to: File) { 47 | val region = RegionIO.readRegion(from) 48 | val chunk = region[ChunkPos(region.position.xPos * 32, region.position.zPos * 32)] ?: return 49 | chunk.level.getCompoundList("Entities").forEach { 50 | println(it.getString("id") + " "+ it.getDoubleList("Pos")) 51 | } 52 | chunk.level["Entities"] = emptyListOf().toNbtList() 53 | RegionIO.writeRegion(to, region) 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.jetbrains.kotlin.jvm' version '1.3.61' 4 | id 'com.jfrog.bintray' version '1.8.4' 5 | id 'maven' 6 | id 'org.jetbrains.dokka' version '0.9.18' 7 | id 'org.ajoberstar.git-publish' version '2.1.1' 8 | } 9 | 10 | group 'br.com.gamemods' 11 | version '2.0.1-SNAPSHOT' 12 | 13 | sourceSets.main.java.srcDirs = ["src/main/kotlin"] 14 | sourceSets.test.java.srcDirs = ["src/test/kotlin"] 15 | 16 | sourceCompatibility = 1.8 17 | targetCompatibility = sourceCompatibility 18 | 19 | repositories { 20 | jcenter() 21 | } 22 | 23 | dependencies { 24 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 25 | implementation group: 'org.jetbrains.kotlin', name: 'kotlin-reflect' 26 | compile 'br.com.gamemods:nbt-manipulator:2.0.0' 27 | testCompile group: 'junit', name: 'junit', version: '4.12' 28 | } 29 | 30 | compileKotlin { 31 | kotlinOptions.jvmTarget = "1.8" 32 | } 33 | compileTestKotlin { 34 | kotlinOptions.jvmTarget = "1.8" 35 | } 36 | 37 | install { 38 | repositories.mavenInstaller { 39 | pom.project { 40 | packaging 'jar' 41 | groupId project.group 42 | artifactId project.name 43 | version project.version 44 | name project.name 45 | description "A kotlin/java lib that allows you to read and write MCA files in a clean way" 46 | url "https://github.com/GameModsBR/Region-Manipulator" 47 | inceptionYear '2020' 48 | licenses { 49 | license { 50 | name 'MIT' 51 | url 'https://raw.githubusercontent.com/GameModsBR/Region-Manipulator/master/LICENSE' 52 | distribution 'repo' 53 | } 54 | } 55 | developers { 56 | developer { 57 | id = 'joserobjr' 58 | name = 'José Roberto de Araújo Júnior' 59 | email = 'joserobjr@gamemods.com.br' 60 | } 61 | } 62 | scm { 63 | connection "https://github.com/GameModsBR/Region-Manipulator.git" 64 | developerConnection "https://github.com/GameModsBR/Region-Manipulator.git" 65 | url "https://github.com/GameModsBR/Region-Manipulator" 66 | } 67 | } 68 | } 69 | } 70 | 71 | if (!ext.has('gamemodsBintrayUser')) { 72 | ext.gamemodsBintrayUser = "" 73 | } 74 | if (!ext.has('gamemodsBintrayApiKey')) { 75 | ext.gamemodsBintrayApiKey = "" 76 | } 77 | 78 | bintray { 79 | user = "$gamemodsBintrayUser" 80 | key = "$gamemodsBintrayApiKey" 81 | configurations = ['archives'] 82 | pkg { 83 | repo = 'GameMods' 84 | name = 'Region-Manipulator' 85 | userOrg = 'gamemods' 86 | licenses = ['MIT'] 87 | vcsUrl = 'https://github.com/GameModsBR/Region-Manipulator.git' 88 | websiteUrl = 'https://github.com/GameModsBR/Region-Manipulator' 89 | //publish = false 90 | version { 91 | name = project.version 92 | desc = "Region-Manipulator version ${project.version}" 93 | //released = new Date() 94 | vcsTag = "v${project.version}" 95 | gpg { 96 | sign = !project.hasProperty('binariesOnly') //Determines whether to GPG sign the files. The default is false 97 | //passphrase = '123' //Optional. The passphrase for GPG signing' 98 | } 99 | } 100 | } 101 | } 102 | 103 | //signing { 104 | // sign configurations.archives 105 | //} 106 | 107 | task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) { 108 | outputFormat = 'javadoc' 109 | outputDirectory = "$buildDir/javadoc" 110 | } 111 | 112 | task dokkaKdoc(type: org.jetbrains.dokka.gradle.DokkaTask) { 113 | outputFormat = 'gfm' 114 | outputDirectory = "$buildDir/kdoc" 115 | } 116 | 117 | import java.nio.file.Files 118 | import java.nio.file.Paths 119 | import java.nio.file.StandardCopyOption 120 | task createReadmeFiles(dependsOn: dokkaKdoc) { 121 | doFirst { 122 | Files.walk(Paths.get(dokkaKdoc.outputDirectory)) 123 | .filter { it.getFileName().toString().toLowerCase() == "index.md" } 124 | .forEach { 125 | try { 126 | Files.copy(it, it.resolveSibling("README.md"), StandardCopyOption.REPLACE_EXISTING) 127 | } catch (Throwable e) { 128 | throw new RuntimeException(e) 129 | } 130 | } 131 | } 132 | } 133 | 134 | task createIndexMd(type: Copy) { 135 | from file("$projectDir/README.md") 136 | into "$buildDir/pages" 137 | rename 'README.md', 'index.md' 138 | } 139 | 140 | task javadocJar(type: Jar, dependsOn: dokkaJavadoc) { 141 | classifier = 'javadoc' 142 | from dokkaJavadoc.outputDirectory 143 | from file("$projectDir/LICENSE") 144 | from file("$projectDir/README.md") 145 | from file("$projectDir/CHANGELOG.md") 146 | } 147 | 148 | task sourcesJar(type: Jar) { 149 | from sourceSets.main.java.srcDirs 150 | from file("$projectDir/build.gradle") 151 | from file("$projectDir/gradle.properties") 152 | from file("$projectDir/settings.gradle") 153 | from file("$projectDir/LICENSE") 154 | from file("$projectDir/README.md") 155 | from file("$projectDir/CHANGELOG.md") 156 | classifier = 'sources' 157 | } 158 | 159 | jar { 160 | from file("$projectDir/LICENSE") 161 | from file("$projectDir/README.md") 162 | from file("$projectDir/CHANGELOG.md") 163 | } 164 | 165 | if (!project.hasProperty('binariesOnly')) { 166 | artifacts { 167 | archives sourcesJar 168 | } 169 | 170 | // dokka will fail to build the javadoc jar on newest java versions 171 | // https://github.com/Kotlin/dokka/issues/294 172 | if (JavaVersion.current().majorVersion == "8") { 173 | artifacts { 174 | archives javadocJar 175 | } 176 | 177 | if (ext.has('org.ajoberstar.grgit.auth.username')) { 178 | System.setProperty('org.ajoberstar.grgit.auth.username', ext['org.ajoberstar.grgit.auth.username'].toString()) 179 | System.setProperty('org.ajoberstar.grgit.auth.password', ext['org.ajoberstar.grgit.auth.password'].toString()) 180 | } 181 | 182 | dokka { 183 | externalDocumentationLink { 184 | url = new URL('https://gamemodsbr.github.io/NBT-Manipulator/javadoc/') 185 | packageListUrl = new URL("https://gamemodsbr.github.io/NBT-Manipulator/javadoc/package-list") 186 | } 187 | } 188 | 189 | gitPublish { 190 | // where to publish to (repo must exist) 191 | repoUri = 'https://github.com/GameModsBR/Region-Manipulator.git' 192 | 193 | // where to fetch from prior to fetching from the remote (i.e. a local repo to save time) 194 | referenceRepoUri = file("$projectDir/gh-pages").toURI().toString() 195 | 196 | // branch will be created if it doesn't exist 197 | branch = 'gh-pages' 198 | 199 | // generally, you don't need to touch this 200 | repoDir = file("$buildDir/gh-pages-repo") // defaults to $buildDir/gitPublish 201 | 202 | // what to publish, this is a standard CopySpec 203 | contents { 204 | from("$buildDir/javadoc") { 205 | into 'javadoc' 206 | } 207 | from("$buildDir/kdoc") { 208 | into 'kdoc' 209 | } 210 | from "$buildDir/pages" 211 | from 'src/pages' 212 | from 'README.md' 213 | from 'CHANGELOG.md' 214 | } 215 | 216 | // what to keep in the existing branch (include=keep) 217 | preserve { 218 | include '1.0.0/**' 219 | exclude '1.0.0/temp.txt' 220 | } 221 | 222 | // message used when committing changes 223 | commitMessage = 'Github Pages update' // defaults to 'Generated by gradle-git-publish' 224 | } 225 | } 226 | } 227 | 228 | gitPublishCopy.dependsOn dokkaJavadoc 229 | gitPublishCopy.dependsOn createReadmeFiles 230 | gitPublishCopy.dependsOn createIndexMd 231 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PowerNukkit/Region-Manipulator/682910c809b5889702a6e6735c4b5e93a42adc8d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'region-manipulator' 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/br/com/gamemods/regionmanipulator/Chunk.kt: -------------------------------------------------------------------------------- 1 | package br.com.gamemods.regionmanipulator 2 | 3 | import br.com.gamemods.nbtmanipulator.NbtCompound 4 | import br.com.gamemods.nbtmanipulator.NbtFile 5 | import java.util.* 6 | 7 | /** 8 | * A chunk is piece of 16 x 256 x 16 blocks which is contained in a region file. 9 | * 10 | * For more information about chunks please check the [Chunk's page on GamePedia](https://minecraft.gamepedia.com/Chunk). 11 | * 12 | * The chunk represented here will be raw and unparsed, you will have access to it's NBT data to do any modification as you wish. 13 | * @property lastModified The last modification registered in MCA file. It will not be updated automatically. 14 | * @property nbtFile The root NBT that stores all information about this chunk 15 | */ 16 | data class Chunk(var lastModified: Date, var nbtFile: NbtFile) { 17 | /** 18 | * An easy access to the [NbtCompound] inside the [nbtFile]. 19 | */ 20 | val compound: NbtCompound 21 | get() = nbtFile.compound 22 | 23 | /** 24 | * The value of the `DataVersion` tag. 25 | */ 26 | val dataVersion: Int 27 | get() = compound.getInt("DataVersion") 28 | 29 | /** 30 | * The `Level` tag, all chunk details like entities, tile entities, chunk sections, etc are stored here. 31 | */ 32 | val level: NbtCompound 33 | get() = compound.getCompound("Level") 34 | 35 | /** 36 | * The X/Z position in the world where this chunk resides. 37 | */ 38 | val position: ChunkPos 39 | get() = level.let { 40 | ChunkPos(it.getInt("xPos"), it.getInt("zPos")) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/br/com/gamemods/regionmanipulator/ChunkPos.kt: -------------------------------------------------------------------------------- 1 | package br.com.gamemods.regionmanipulator 2 | 3 | /** 4 | * A chunk position. May be used for different contexts but is usually used to indicate the position in the world. 5 | * @property xPos May be negative. 6 | * @property zPos May be negative. 7 | */ 8 | data class ChunkPos(val xPos: Int, val zPos: Int) 9 | -------------------------------------------------------------------------------- /src/main/kotlin/br/com/gamemods/regionmanipulator/CorruptChunk.kt: -------------------------------------------------------------------------------- 1 | package br.com.gamemods.regionmanipulator 2 | 3 | import br.com.gamemods.nbtmanipulator.NbtFile 4 | import java.util.* 5 | 6 | /** 7 | * This is the same as [Chunk] but holding a [ByteArray] instead of [NbtFile] because the chunk content could not be parsed. 8 | * 9 | * The chunk represented here will be raw and unparsed, you will have access to it's NBT data to do any modification as you wish. 10 | * @property lastModified The last modification registered in MCA file. It will not be updated automatically. 11 | * 12 | * @property position The X/Z position in the world where this chunk resides. 13 | * 14 | * @property chunkContent The bytes that compounds this chunk. Be aware that it's corrupt. 15 | * Null if the reader couldn't even reach the chunk's content section in the MCA file. 16 | * 17 | * @property location The position where the chunk is located in the MCA file. 18 | * 19 | * @property allocationSize The amount of data which was allocated to this chunk. The chunk don't need to use all the bytes. 20 | * 21 | * @property length The actual amount of bytes that this chunk is using in it's allocated section. Always less or equals to [allocationSize]. 22 | * Null if the reader couldn't read this information from the chunk's body. 23 | * 24 | * @property compression The compression format used to compress the chunk content. `1` for GZIP and `2` for ZIP. 25 | * Null if the reader couldn't read this information from the chunk's body. 26 | * 27 | * @property throwable The throwable that prevented the chunk from loading. 28 | */ 29 | data class CorruptChunk( 30 | val position: ChunkPos, var lastModified: Date, var chunkContent: ByteArray?, var location: Long, 31 | var allocationSize: Int, var length: Int?, var compression: Int?, var throwable: Throwable 32 | ) { 33 | /** 34 | * Constructs the corrupt chunk calculating the chunk position based on the region file position and the chunk index. 35 | */ 36 | constructor( 37 | regionPos: RegionPos, index: Int, lastModified: Date, chunkContent: ByteArray?, location: Long, 38 | allocationSize: Int, length: Int?, compression: Int?, throwable: Throwable 39 | ) : this(calculateChunkPos(regionPos, index), lastModified, chunkContent, location, allocationSize, length, compression, throwable) 40 | 41 | private companion object { 42 | private fun calculateChunkPos(regionPos: RegionPos, index: Int): ChunkPos { 43 | val offsetX = index % 32 44 | val offsetZ = (index / 32) % 32 45 | 46 | val minRegX = regionPos.xPos * 32 47 | val minRegZ = regionPos.zPos * 32 48 | 49 | val chunkX = minRegX + offsetX 50 | val chunkZ = minRegZ + offsetZ 51 | 52 | return ChunkPos(chunkX, chunkZ) 53 | } 54 | } 55 | 56 | override fun equals(other: Any?): Boolean { 57 | if (this === other) return true 58 | if (javaClass != other?.javaClass) return false 59 | 60 | other as CorruptChunk 61 | 62 | if (position != other.position) return false 63 | if (lastModified != other.lastModified) return false 64 | val chunkContent = chunkContent 65 | val otherChunkContent = other.chunkContent 66 | if (chunkContent != null) { 67 | if (otherChunkContent == null) return false 68 | if (!chunkContent.contentEquals(otherChunkContent)) return false 69 | } else if (otherChunkContent != null) return false 70 | if (location != other.location) return false 71 | if (allocationSize != other.allocationSize) return false 72 | if (length != other.length) return false 73 | if (compression != other.compression) return false 74 | if (throwable != other.throwable) return false 75 | 76 | return true 77 | } 78 | 79 | override fun hashCode(): Int { 80 | var result = position.hashCode() 81 | result = 31 * result + lastModified.hashCode() 82 | result = 31 * result + (chunkContent?.contentHashCode() ?: 0) 83 | result = 31 * result + location.hashCode() 84 | result = 31 * result + allocationSize 85 | result = 31 * result + (length ?: 0) 86 | result = 31 * result + (compression ?: 0) 87 | result = 31 * result + throwable.hashCode() 88 | return result 89 | } 90 | 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/kotlin/br/com/gamemods/regionmanipulator/CorruptChunkException.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused", "CanBeParameter", "MemberVisibilityCanBePrivate") 2 | 3 | package br.com.gamemods.regionmanipulator 4 | 5 | import java.lang.RuntimeException 6 | 7 | /** 8 | * Fired when attempting to access a corrupt chunk from a [Region] object. 9 | * @property chunk The corrupted chunk details 10 | */ 11 | class CorruptChunkException(val chunk: CorruptChunk): RuntimeException("The chunk ${chunk.position} is corrupt!", chunk.throwable) 12 | -------------------------------------------------------------------------------- /src/main/kotlin/br/com/gamemods/regionmanipulator/Region.kt: -------------------------------------------------------------------------------- 1 | package br.com.gamemods.regionmanipulator 2 | 3 | import kotlin.math.floor 4 | 5 | /** 6 | * A region file stores a group of 32 x 32 chunks in it. 7 | * 8 | * For more information about regions please check [Regions's page on GamePedia](https://minecraft.gamepedia.com/Region_file_format) 9 | * 10 | * The region will be represented as a mutable map, you can add, modify and remove chunks easily using it's [ChunkPos] in the world. 11 | * 12 | * Note that all chunks stored here must actually be part of the region. 13 | * 14 | * Corrupt chunks will cause the region to fire [CorruptChunkException] when an attempt to read happens. 15 | * 16 | * Corrupt chunks can be manipulated with [setCorrupt], [getCorrupt] and [remove]. 17 | * 18 | * @property position Where this region resides in the world. 19 | */ 20 | class Region(val position: RegionPos): AbstractMutableMap() { 21 | private val chunks = arrayOfNulls(1024) 22 | private val corruptChunks = mutableMapOf() 23 | 24 | /** 25 | * Creates a region pre-populated with chunks. 26 | * @param position Where this region resides in the world. 27 | * @param corruptChunks A list of corrupt chunks 28 | */ 29 | constructor(position: RegionPos, chunks: List, corruptChunks: List): this(position) { 30 | addAll(chunks) 31 | corruptChunks.asSequence().filterNotNull().forEach { setCorrupt(it) } 32 | } 33 | 34 | /** 35 | * Creates a region pre-populated with chunks. 36 | * @param position Where this region resides in the world. 37 | */ 38 | constructor(position: RegionPos, chunks: List): this(position) { 39 | addAll(chunks) 40 | } 41 | 42 | /** 43 | * Similar to [add] but adding a corrupt chunk. 44 | */ 45 | fun setCorrupt(corruptChunk: CorruptChunk): Chunk? { 46 | corruptChunk.position.checkValid() 47 | val removed = remove(corruptChunk.position) 48 | corruptChunks[corruptChunk.position] = corruptChunk 49 | return removed 50 | } 51 | 52 | /** 53 | * Sililar to [get] but will get only corrupt chunks without firing exception. 54 | */ 55 | fun getCorrupt(key: ChunkPos): CorruptChunk? = corruptChunks[key] 56 | 57 | /** 58 | * Adds a chunk to this region. 59 | * @param key Where the chunk resides. It must be valid for this region. 60 | * @param value The chunk that is being added 61 | */ 62 | override fun put(key: ChunkPos, value: Chunk): Chunk? { 63 | check(key == value.position) { 64 | "The chunk's key doesn't match the chunk's value. Key: $key, Chunk: ${value.position}" 65 | } 66 | key.checkValid() 67 | val offset = offset(key) 68 | val before = chunks[offset] 69 | chunks[offset] = value 70 | corruptChunks -= key 71 | return before 72 | } 73 | 74 | private fun ChunkPos.checkValid() { 75 | val regX = floor(xPos / 32.0).toInt() 76 | val regZ = floor(zPos / 32.0).toInt() 77 | check(regX == position.xPos && regZ == position.zPos) { 78 | "The chunk $this is not part of the region $position. It's part of r.$regX.$regZ.mca" 79 | } 80 | } 81 | 82 | private fun ChunkPos.isValid(): Boolean { 83 | val regX = floor(xPos / 32.0).toInt() 84 | val regZ = floor(zPos / 32.0).toInt() 85 | return regX == position.xPos && regZ == position.zPos 86 | } 87 | 88 | /** 89 | * Returns the chunk content for the given key or null if the chunk is not part of this region or is empty. 90 | * @param key The chunk position in the world 91 | * @throws CorruptChunkException If the [key] points to a corrupt chunk 92 | */ 93 | @Throws(CorruptChunkException::class) 94 | override fun get(key: ChunkPos): Chunk? { 95 | if (!key.isValid()) { 96 | return null 97 | } 98 | 99 | corruptChunks[key]?.let { corruptChunk -> 100 | throw CorruptChunkException(corruptChunk) 101 | } 102 | 103 | return chunks[offset(key)] 104 | } 105 | 106 | /** 107 | * Removes a chunk from this region, thus making it empty. Also removes corrupt chunks. 108 | * @param key The chunk position in the world 109 | */ 110 | override fun remove(key: ChunkPos): Chunk? { 111 | if (!key.isValid()) { 112 | return null 113 | } 114 | val offset = offset(key) 115 | val before = chunks[offset] 116 | chunks[offset] = null 117 | corruptChunks -= key 118 | return before 119 | } 120 | 121 | /** 122 | * Removes a chunk from this region, thus making it empty. But only removes if the current value matches the given value. 123 | * Does **not** remove corrupt chunks. 124 | * @param key The chunk position in the world 125 | * @param value The expected value. Will only remove if the current value matches this value. 126 | */ 127 | override fun remove(key: ChunkPos, value: Chunk): Boolean { 128 | if (!key.isValid()) { 129 | return false 130 | } 131 | val offset = offset(key) 132 | val before = chunks[offset] 133 | return if (value == before) { 134 | chunks[offset] = null 135 | true 136 | } else { 137 | false 138 | } 139 | } 140 | 141 | private fun offset(chunkPos: ChunkPos) = internalOffset(chunkPos.xPos - position.xPos * 32, chunkPos.zPos - position.zPos * 32) 142 | private fun internalOffset(x: Int, z: Int) = ((x % 32) + (z % 32) * 32) 143 | 144 | /** 145 | * Shortcut of [put]. 146 | * @param chunk The Chunk that is being added. It must be within the range of this region. 147 | */ 148 | fun add(chunk: Chunk) { 149 | put(chunk.position, chunk) 150 | } 151 | 152 | /** 153 | * Adds all chunks in the list. 154 | * @param chunks The chunks that will be added. They must be within the range of this region. The list must not contains null values. 155 | */ 156 | @JvmSynthetic 157 | @JvmName("addAllNotNull") 158 | fun addAll(chunks: List) { 159 | chunks.forEach(this::add) 160 | } 161 | 162 | /** 163 | * Adds all chunks in the list. 164 | * @param chunks The chunks that will be added. They must be within the range of this region. Null values are ignored. 165 | */ 166 | @JvmName("addAll") 167 | fun addAll(chunks: List) { 168 | chunks.asSequence().filterNotNull().forEach(this::add) 169 | } 170 | 171 | /** 172 | * Adds all chunks in the list. 173 | * @param chunks The chunks that will be added. They must be within the range of this region. Null values are ignored. 174 | */ 175 | @Deprecated("Java users should call `Region.addAll`", ReplaceWith("addAll(chunks)")) 176 | fun addAllNullable(chunks: List) = addAll(chunks) 177 | 178 | /** 179 | * Gets a immutable map with all corrupt chunks in this region. 180 | */ 181 | fun getCorruptChunks() = corruptChunks.toMap() 182 | 183 | /** 184 | * A mutable set containing mutable entries which when modified will also modify the [Region] object. 185 | * 186 | * Corrupt chunks are skipped. 187 | */ 188 | override val entries: MutableSet> 189 | get() = object : AbstractMutableSet>() { 190 | override val size: Int 191 | get() = chunks.count { it != null } 192 | 193 | override fun add(element: MutableMap.MutableEntry): Boolean { 194 | val removed = put(element.key, element.value) 195 | return removed != element.value 196 | } 197 | 198 | override fun iterator(): MutableIterator> { 199 | return object : MutableIterator> { 200 | lateinit var current: MutableMap.MutableEntry 201 | val iter = chunks.asSequence().filterNotNull().map { chunk -> 202 | object : MutableMap.MutableEntry { 203 | override val key: ChunkPos = chunk.position 204 | override val value: Chunk 205 | get() = get(key) ?: chunk 206 | 207 | override fun setValue(newValue: Chunk): Chunk { 208 | return put(key, newValue) ?: chunk 209 | } 210 | } 211 | }.iterator() 212 | 213 | override fun hasNext(): Boolean { 214 | return iter.hasNext() 215 | } 216 | 217 | override fun next(): MutableMap.MutableEntry { 218 | current = iter.next() 219 | return current 220 | } 221 | 222 | override fun remove() { 223 | this@Region.remove(current.key) 224 | } 225 | } 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/main/kotlin/br/com/gamemods/regionmanipulator/RegionIO.kt: -------------------------------------------------------------------------------- 1 | package br.com.gamemods.regionmanipulator 2 | 3 | import br.com.gamemods.nbtmanipulator.NbtIO 4 | import java.io.* 5 | import java.nio.ByteBuffer 6 | import java.util.* 7 | import java.util.zip.Deflater 8 | import java.util.zip.GZIPInputStream 9 | import java.util.zip.InflaterInputStream 10 | import kotlin.math.ceil 11 | import kotlin.math.min 12 | 13 | /** 14 | * Contains usefull methods do read and write [Region] from [File]. 15 | */ 16 | object RegionIO { 17 | /** 18 | * Reads a region identifying it's [RegionPos] by the name of the file. 19 | * @param file The file to be read. It must be named like r.1.-2.mca where 1 is it's xPos and -2 it's zPos. 20 | * 21 | * @throws IOException If an IO exception occurs while reading the MCA headers. 22 | * Exceptions which happens while loading the chunk's body are reported in [CorruptChunk.throwable] 23 | * which can be acceded using [Region.getCorrupt] or [Region.getCorruptChunks] 24 | */ 25 | @JvmStatic 26 | @Throws(IOException::class) 27 | fun readRegion(file: File): Region { 28 | val nameParts = file.name.split('.', limit = 4) 29 | val xPos = nameParts[1].toInt() 30 | val zPos = nameParts[2].toInt() 31 | val regionPos = RegionPos(xPos, zPos) 32 | return readRegion(file, regionPos) 33 | } 34 | 35 | private data class ChunkInfo(val location: Int, val size: Int, var lastModified: Date = Date(0)) 36 | 37 | /** 38 | * Reads a region using a specified [RegionPos]. 39 | * @param file The file to be read. Can have any name 40 | * @param pos The position of this region. Must match the content's otherwise it won't be manipulable. 41 | * 42 | * @throws IOException If an IO exception occurs while reading the MCA headers. 43 | * Exceptions which happens while loading the chunk's body are reported in [CorruptChunk.throwable] 44 | * which can be acceded using [Region.getCorrupt] or [Region.getCorruptChunks] 45 | */ 46 | @JvmStatic 47 | @Throws(IOException::class) 48 | fun readRegion(file: File, pos: RegionPos): Region { 49 | 50 | RandomAccessFile(file, "r").use { input -> 51 | val chunkInfos = Array(1024) { 52 | val loc = (input.read() shl 16) + (input.read() shl 8) + input.read() 53 | ChunkInfo(loc * 4096, input.read() * 4096).takeUnless { it.size == 0 } 54 | } 55 | 56 | for (i in 0 until 1024) { 57 | input.readInt().takeUnless { it == 0 }?.let { 58 | chunkInfos[i]?.lastModified = Date( it * 1000L) 59 | } 60 | } 61 | 62 | val corruptChunks = mutableListOf() 63 | 64 | val chunks = chunkInfos.mapIndexedNotNull { i, ci -> 65 | val info = ci ?: return@mapIndexedNotNull null 66 | var length: Int? = null 67 | var compression: Int? = null 68 | var data: ByteArray? = null 69 | try { 70 | input.seek(info.location.toLong()) 71 | length = input.readInt() 72 | compression = input.read() 73 | check(compression == 1 || compression == 2) { 74 | "Bad compression $compression . Chunk index: $i" 75 | } 76 | 77 | data = ByteArray(min(length, info.size)) 78 | val read = input.readFullyIfPossible(data) 79 | if (read < data.size) { 80 | data = data.copyOf(read) 81 | } 82 | 83 | if (length > data.size) { 84 | throw EOFException("Could not read all $length bytes. Read only ${data.size} bytes in a sector of ${info.size} bytes") 85 | } 86 | 87 | val inputStream = when (compression) { 88 | 1 -> GZIPInputStream(ByteArrayInputStream(data)) 89 | 2 -> InflaterInputStream(ByteArrayInputStream(data)) 90 | else -> error("Unexpected compression type $compression") 91 | } 92 | 93 | val nbt = NbtIO.readNbtFile(inputStream, false) 94 | Chunk(info.lastModified, nbt) 95 | } catch (e: Throwable) { 96 | corruptChunks += CorruptChunk( 97 | pos, i, info.lastModified, data, 98 | info.location.toLong(), info.size, 99 | length, compression, e 100 | ) 101 | null 102 | } 103 | } 104 | 105 | 106 | return Region(pos, chunks, corruptChunks) 107 | } 108 | } 109 | 110 | /** 111 | * Attempts to read `array.size` bytes from the file into the byte 112 | * array, starting at the current file pointer. This method reads 113 | * repeatedly from the file until the requested number of bytes are 114 | * read or the end of the file is reached. 115 | * This method blocks until the requested number of bytes are 116 | * read, the end of the stream is detected, or an exception is thrown. 117 | * 118 | * Differently from [RandomAccessFile.readFully], [EOFException] is never thrown. 119 | * 120 | * @return The number of bytes which was read into the array, 121 | * if the end of the file was reached the number will be lower then the array size 122 | * and the remaining bytes in the array will not be changed. 123 | */ 124 | private fun RandomAccessFile.readFullyIfPossible(array: ByteArray): Int { 125 | val size = array.size 126 | var currentSize = 0; 127 | do { 128 | val read = this.read(array, currentSize, size - currentSize) 129 | if (read < 0) { 130 | return currentSize 131 | } 132 | currentSize += read 133 | } while (currentSize < size) 134 | return currentSize 135 | } 136 | 137 | private fun deflate(data: ByteArray, level: Int): ByteArray { 138 | val deflater = Deflater(level) 139 | deflater.reset() 140 | deflater.setInput(data) 141 | deflater.finish() 142 | val bos = ByteArrayOutputStream(data.size) 143 | val buf = ByteArray(1024) 144 | try { 145 | while (!deflater.finished()) { 146 | val i = deflater.deflate(buf) 147 | bos.write(buf, 0, i) 148 | } 149 | } finally { 150 | deflater.end() 151 | } 152 | return bos.toByteArray() 153 | } 154 | 155 | /** 156 | * Saves a [Region] in a [File]. The region file will be entirely rebuilt. 157 | * @param file The file which will be written. 158 | * @param region The region which will be saved. 159 | * 160 | * @throws IOException If an IO exception occurs while writing to the file. 161 | */ 162 | @JvmStatic 163 | @Throws(IOException::class) 164 | fun writeRegion(file: File, region: Region) { 165 | val chunkInfoHeader = mutableListOf() 166 | 167 | val heapData = ByteArrayOutputStream() 168 | val heap = DataOutputStream(heapData) 169 | var heapPos = 0 170 | var index = -1 171 | for (z in 0 until 32) { 172 | for (x in 0 until 32) { 173 | index++ 174 | val pos = ChunkPos(region.position.xPos * 32 + x, region.position.zPos * 32 + z) 175 | val chunk = region[pos] 176 | 177 | if (chunk == null) { 178 | chunkInfoHeader += ChunkInfo(0, 0) 179 | } else { 180 | val chunkData = ByteArrayOutputStream() 181 | //val chunkOut = DeflaterOutputStream(chunkData) 182 | val chunkOut = chunkData 183 | NbtIO.writeNbtFile(chunkOut, chunk.nbtFile, false) 184 | //chunkOut.finish() 185 | chunkOut.flush() 186 | chunkOut.close() 187 | val uncompressedChunkBytes = chunkData.toByteArray() 188 | val chunkBytes = deflate(uncompressedChunkBytes, 7) 189 | val sectionBytes = ByteArray((ceil((chunkBytes.size + 5) / 4096.0).toInt() * 4096) - 5) { 190 | if (it >= chunkBytes.size) { 191 | 0 192 | } else { 193 | chunkBytes[it] 194 | } 195 | } 196 | 197 | heap.writeInt(chunkBytes.size + 1) 198 | heap.writeByte(2) 199 | heap.write(sectionBytes) 200 | chunkInfoHeader += ChunkInfo(8192 + heapPos, sectionBytes.size + 5, chunk.lastModified) 201 | heapPos += 5 + sectionBytes.size 202 | } 203 | } 204 | } 205 | heap.flush() 206 | heap.close() 207 | val heapBytes = heapData.toByteArray() 208 | 209 | val headerData = ByteArrayOutputStream() 210 | val header = DataOutputStream(headerData) 211 | 212 | chunkInfoHeader.forEach { 213 | if (it.size > 0) { 214 | assert(it.location >= 8192) { 215 | "Header location is too short, it must be >= 8192! Got ${it.location}" 216 | } 217 | assert(ByteBuffer.wrap(heapBytes, it.location - 8192, 4).int > 0) { 218 | "Header location is pointing to an incorrect heap location" 219 | } 220 | } 221 | val sec = it.location / 4096 222 | header.writeByte((sec shr 16) and 0xFF) 223 | header.writeByte((sec shr 8) and 0xFF) 224 | header.writeByte(sec and 0xFF) 225 | 226 | val size = it.size / 4096 227 | header.writeByte(size) 228 | } 229 | 230 | chunkInfoHeader.forEach { 231 | header.writeInt((it.lastModified.time / 1000L).toInt()) 232 | } 233 | header.close() 234 | val headerBytes = headerData.toByteArray() 235 | check(headerBytes.size == 8192) { 236 | "Failed to write the mca header. Size ${header.size()} != 4096" 237 | } 238 | 239 | file.outputStream().buffered().use { 240 | it.write(headerBytes) 241 | it.write(heapBytes) 242 | it.flush() 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/main/kotlin/br/com/gamemods/regionmanipulator/RegionPos.kt: -------------------------------------------------------------------------------- 1 | package br.com.gamemods.regionmanipulator 2 | 3 | /** 4 | * A region position extracted from the region file name. 5 | * 6 | * `r.-2.3.mca` must be `Region(-2,3)` for example 7 | * @property xPos The first number in the region file name. May be negative. 8 | * @property zPos The second number in the region file name. May be negative. 9 | */ 10 | data class RegionPos(val xPos: Int, val zPos: Int) { 11 | private constructor(mcaFileNameParts: List): this(mcaFileNameParts[1].toInt(), mcaFileNameParts[2].toInt()) 12 | 13 | /** 14 | * Parses a region file name. Only support valid names like `r.-3.2.mca`. 15 | * @param mcaFileName A valid file name 16 | */ 17 | constructor(mcaFileName: String): this(mcaFileName.split('.')) 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-hacker 2 | -------------------------------------------------------------------------------- /src/pages/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | code.highlighter-rouge { 6 | color: #d0d0d0; 7 | } 8 | -------------------------------------------------------------------------------- /src/test/kotlin/br/com/gamemods/regionmanipulator/RegionTest.kt: -------------------------------------------------------------------------------- 1 | package br.com.gamemods.regionmanipulator 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | import java.io.EOFException 6 | import java.io.File 7 | 8 | class RegionTest { 9 | @Test 10 | fun testReadMCA1() { 11 | val tempFile = File.createTempFile("r.1,-1,", ".mca") 12 | tempFile.deleteOnExit() 13 | RegionTest::class.java.getResourceAsStream("/r.1.-1.mca").use { input -> 14 | tempFile.outputStream().use { output -> 15 | input.copyTo(output) 16 | } 17 | } 18 | 19 | val mca = RegionIO.readRegion(tempFile, RegionPos(1, -1)) 20 | RegionIO.writeRegion(tempFile, mca) 21 | } 22 | 23 | @Test 24 | fun testReadMCA2() { 25 | val tempFile = File.createTempFile("r.-1,-2,", ".mca") 26 | tempFile.deleteOnExit() 27 | RegionTest::class.java.getResourceAsStream("/r.-1.-2.mca").use { input -> 28 | tempFile.outputStream().use { output -> 29 | input.copyTo(output) 30 | } 31 | } 32 | 33 | val mca = RegionIO.readRegion(tempFile, RegionPos(-1, -2)) 34 | RegionIO.writeRegion(tempFile, mca) 35 | } 36 | 37 | @Test(expected = CorruptChunkException::class) 38 | fun testIssue2fix() { 39 | val tempFile = File.createTempFile("r.-1,-1,", ".mca") 40 | tempFile.deleteOnExit() 41 | RegionTest::class.java.getResourceAsStream("/issue.2.r.-1.-1.mca").use { input -> 42 | tempFile.outputStream().use { output -> 43 | input.copyTo(output) 44 | } 45 | } 46 | val region = RegionIO.readRegion(tempFile, RegionPos(-1, -1)) 47 | val chunkPos = ChunkPos(-24, -17) 48 | Assert.assertNotNull(region.getCorrupt(chunkPos)) 49 | 50 | region[chunkPos] 51 | } 52 | 53 | @Test 54 | fun testIssue3fix() { 55 | val tempFile = File.createTempFile("r.0,-1,", ".mca") 56 | tempFile.deleteOnExit() 57 | RegionTest::class.java.getResourceAsStream("/issue-3.r.0.-1.mca").use { input -> 58 | tempFile.outputStream().use { output -> 59 | input.copyTo(output) 60 | } 61 | } 62 | RegionIO.readRegion(tempFile, RegionPos(0, -1)) 63 | } 64 | 65 | @Test 66 | fun testMC1_15_1() { 67 | val tempFile = File.createTempFile("r.0,-1,", ".mca") 68 | tempFile.deleteOnExit() 69 | val mcaBytes = RegionTest::class.java.getResourceAsStream("/issue-4-v1.15.1-r.0.0.mca").use { it.buffered().readBytes() } 70 | tempFile.outputStream().use { 71 | it.buffered().use { output -> 72 | output.write(mcaBytes, 0, (mcaBytes.size*2 / 3) + 15) // Writing only 2/3 + 15 of the MCA file to force corruption 73 | } 74 | } 75 | val region = RegionIO.readRegion(tempFile, RegionPos(0, 0)); 76 | Assert.assertEquals(40, region.getCorruptChunks().size) 77 | Assert.assertEquals(90, region.size) 78 | val corrupt = region.getCorrupt(ChunkPos(8, 10)) 79 | Assert.assertNotNull(corrupt); corrupt!! 80 | Assert.assertEquals(4473, corrupt.length) 81 | Assert.assertEquals(1375, corrupt.chunkContent?.size) 82 | Assert.assertTrue(corrupt.throwable is EOFException) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/resources/issue-3.r.0.-1.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PowerNukkit/Region-Manipulator/682910c809b5889702a6e6735c4b5e93a42adc8d/src/test/resources/issue-3.r.0.-1.mca -------------------------------------------------------------------------------- /src/test/resources/issue-4-v1.15.1-r.0.0.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PowerNukkit/Region-Manipulator/682910c809b5889702a6e6735c4b5e93a42adc8d/src/test/resources/issue-4-v1.15.1-r.0.0.mca -------------------------------------------------------------------------------- /src/test/resources/issue.2.r.-1.-1.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PowerNukkit/Region-Manipulator/682910c809b5889702a6e6735c4b5e93a42adc8d/src/test/resources/issue.2.r.-1.-1.mca -------------------------------------------------------------------------------- /src/test/resources/r.-1.-2.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PowerNukkit/Region-Manipulator/682910c809b5889702a6e6735c4b5e93a42adc8d/src/test/resources/r.-1.-2.mca -------------------------------------------------------------------------------- /src/test/resources/r.1.-1.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PowerNukkit/Region-Manipulator/682910c809b5889702a6e6735c4b5e93a42adc8d/src/test/resources/r.1.-1.mca --------------------------------------------------------------------------------