├── .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
--------------------------------------------------------------------------------