Compare commits
167 commits
Author | SHA1 | Date | |
---|---|---|---|
118034dd27 | |||
ca880ab3b4 | |||
dd56e842b5 | |||
69b62576ea | |||
58f2ff53b9 | |||
42b302f493 | |||
5f84ec8ed2 | |||
ba6e4bae7f | |||
442009eb08 | |||
23b2f274be | |||
3008203080 | |||
d638c810f2 | |||
6e5b958912 | |||
974a0f37df | |||
54ea2b2f28 | |||
6a54f57c50 | |||
dca365f4e2 | |||
e600a9dabb | |||
0f52f6a2c1 | |||
d6cd0fc53b | |||
c49ad6e608 | |||
5ee826a11b | |||
775fcb2d1b | |||
7bba5bae55 | |||
5c85687554 | |||
708fb9645d | |||
53a0f24600 | |||
404ad74235 | |||
deb232ddf3 | |||
fbdd5ed457 | |||
b1c0f316cb | |||
7686996fd3 | |||
b8a5d1580b | |||
f8c8ca78ba | |||
40bc6cd2a9 | |||
5ee8e493d4 | |||
6e2f2697fc | |||
24c266fc78 | |||
0a08e8cc46 | |||
90f2c5fdd0 | |||
8cb1eee60b | |||
282f62fc30 | |||
7bc15f97de | |||
901489dbc3 | |||
dba3dd551e | |||
850b1a668b | |||
3b5ce82873 | |||
cb0aa235db | |||
d02ca9aea2 | |||
c10e9e4902 | |||
37126f69fc | |||
971afea727 | |||
f661f854a4 | |||
1d9be9a41c | |||
a25b3cdbd7 | |||
d96bb727f7 | |||
0dd36a409a | |||
a10151a4f3 | |||
d7fc95c950 | |||
561a1e6577 | |||
0bf1d46aad | |||
a2f0ad401d | |||
ec309dc15f | |||
6763e2b4ec | |||
c23b53a8c3 | |||
bb11b29e92 | |||
28a191a23a | |||
52572a9e81 | |||
b9cd94b235 | |||
f9954d1ce4 | |||
a67bdb3b67 | |||
0a3f6d7765 | |||
ff6e28d381 | |||
9375af8d54 | |||
7f329ac8e7 | |||
650cd23198 | |||
941cd16337 | |||
c1d2eaa17e | |||
00237101f2 | |||
8f408e78a0 | |||
7b98954c80 | |||
00216341ca | |||
83bd936f80 | |||
9b3b345318 | |||
11e25106cf | |||
491ec5cdca | |||
feaf90c96c | |||
4933d8e15f | |||
![]() |
2156bff59a | ||
7b44ee30d6 | |||
f9a03b332c | |||
f43e84eb2b | |||
269f08d356 | |||
f1bc18add1 | |||
6d6f3e5b49 | |||
414ad5a493 | |||
7f3e47fcb4 | |||
bc8219772f | |||
661da4698d | |||
e9abe6b502 | |||
4eb963f147 | |||
e74e7be686 | |||
66189d279c | |||
d435643cfc | |||
39b073f24b | |||
44e914599b | |||
bccd6d6cb4 | |||
![]() |
00eea45375 | ||
9fd3989a95 | |||
d6716a598b | |||
f186681b41 | |||
3d84e9c9e4 | |||
7009dd791d | |||
8a1a26c13c | |||
a99a734df8 | |||
3ceb7ae188 | |||
e18761a3e4 | |||
05d8faeb5c | |||
6299c871a9 | |||
03521684b9 | |||
643035eaed | |||
3d024c6cd8 | |||
08f84fa339 | |||
625f2a13a3 | |||
7daddd6bbc | |||
7b3ac8647e | |||
76df56c9ce | |||
31de0dc0bd | |||
ac0fd06b16 | |||
989428f78d | |||
43d7dc751e | |||
7834315dd3 | |||
dca9c394f2 | |||
87d4371922 | |||
a1dd77c8fd | |||
9fd5689ebb | |||
6da921cca3 | |||
fb361145eb | |||
9aacd9cc2b | |||
48a6e242ea | |||
abf87e75ee | |||
8814dcff89 | |||
1874d3082d | |||
d29c0df25d | |||
cde6a4b6e6 | |||
1143396068 | |||
0f308788ef | |||
7740ce0522 | |||
e36ae4601d | |||
825cf70e51 | |||
7297c03567 | |||
c44f6ab859 | |||
5d40d061a4 | |||
f0e0db63d3 | |||
638d5046c9 | |||
61d456846a | |||
f78dd795ca | |||
0988ebe095 | |||
f79edb462c | |||
e5d9c813ba | |||
7b12c4aea3 | |||
2e6a200c6c | |||
f08acd06e2 | |||
e8165aa47d | |||
1e41e1bc6a | |||
5273e4b366 | |||
3a796325ba |
4
.dockerignore
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
*
|
||||||
|
!/Cargo.*
|
||||||
|
!/src
|
||||||
|
!/crates
|
124
.github/workflows/MinedMap.yml
vendored
|
@ -1,5 +1,14 @@
|
||||||
name: 'MinedMap'
|
name: 'MinedMap'
|
||||||
on: ['push', 'pull_request', 'workflow_dispatch']
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: -Dwarnings
|
RUSTFLAGS: -Dwarnings
|
||||||
|
@ -7,7 +16,7 @@ env:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
viewer:
|
viewer:
|
||||||
runs-on: 'ubuntu-20.04'
|
runs-on: 'ubuntu-latest'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 'Checkout'
|
- name: 'Checkout'
|
||||||
|
@ -25,9 +34,10 @@ jobs:
|
||||||
pkgdir='build/pkg/MinedMap-${{ steps.tag.outputs.tag }}-viewer'
|
pkgdir='build/pkg/MinedMap-${{ steps.tag.outputs.tag }}-viewer'
|
||||||
mkdir -p "$pkgdir"
|
mkdir -p "$pkgdir"
|
||||||
cp -r viewer/* "$pkgdir"/
|
cp -r viewer/* "$pkgdir"/
|
||||||
|
rm "$pkgdir"/Dockerfile
|
||||||
|
|
||||||
- name: 'Archive'
|
- name: 'Archive'
|
||||||
uses: 'actions/upload-artifact@v3'
|
uses: 'actions/upload-artifact@v4'
|
||||||
with:
|
with:
|
||||||
name: 'MinedMap-${{ steps.tag.outputs.tag }}-viewer'
|
name: 'MinedMap-${{ steps.tag.outputs.tag }}-viewer'
|
||||||
path: 'build/pkg'
|
path: 'build/pkg'
|
||||||
|
@ -38,7 +48,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: '1.86'
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
- run: cargo fmt --all -- --check
|
- run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
@ -48,7 +58,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: '1.86'
|
||||||
components: clippy
|
components: clippy
|
||||||
- uses: swatinem/rust-cache@v2
|
- uses: swatinem/rust-cache@v2
|
||||||
- uses: actions-rs/clippy-check@v1
|
- uses: actions-rs/clippy-check@v1
|
||||||
|
@ -62,7 +72,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: '1.86'
|
||||||
components: rust-docs
|
components: rust-docs
|
||||||
- uses: swatinem/rust-cache@v2
|
- uses: swatinem/rust-cache@v2
|
||||||
- run: cargo doc --workspace --no-deps --document-private-items
|
- run: cargo doc --workspace --no-deps --document-private-items
|
||||||
|
@ -73,12 +83,11 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||||
rust: [stable]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.rust }}
|
toolchain: '1.86'
|
||||||
- uses: swatinem/rust-cache@v2
|
- uses: swatinem/rust-cache@v2
|
||||||
- run: cargo test --workspace
|
- run: cargo test --workspace
|
||||||
- run: cargo test --workspace --no-default-features
|
- run: cargo test --workspace --no-default-features
|
||||||
|
@ -91,9 +100,9 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: 'macos-11'
|
- os: 'macos-13'
|
||||||
target: 'aarch64-apple-darwin'
|
target: 'aarch64-apple-darwin'
|
||||||
- os: 'macos-11'
|
- os: 'macos-13'
|
||||||
target: 'x86_64-apple-darwin'
|
target: 'x86_64-apple-darwin'
|
||||||
- os: 'windows-2019'
|
- os: 'windows-2019'
|
||||||
target: 'x86_64-pc-windows-msvc'
|
target: 'x86_64-pc-windows-msvc'
|
||||||
|
@ -101,7 +110,7 @@ jobs:
|
||||||
- os: 'windows-2019'
|
- os: 'windows-2019'
|
||||||
target: 'i686-pc-windows-msvc'
|
target: 'i686-pc-windows-msvc'
|
||||||
ext: '.exe'
|
ext: '.exe'
|
||||||
- os: 'ubuntu-20.04'
|
- os: 'ubuntu-22.04'
|
||||||
target: 'x86_64-unknown-linux-gnu'
|
target: 'x86_64-unknown-linux-gnu'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -118,10 +127,12 @@ jobs:
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: '1.86'
|
||||||
targets: '${{ matrix.target }}'
|
targets: '${{ matrix.target }}'
|
||||||
|
|
||||||
- uses: swatinem/rust-cache@v2
|
- uses: swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
key: '${{ matrix.target }}'
|
||||||
|
|
||||||
- name: 'Build'
|
- name: 'Build'
|
||||||
shell: 'bash'
|
shell: 'bash'
|
||||||
|
@ -134,7 +145,94 @@ jobs:
|
||||||
cp target/${{ matrix.target }}/release/minedmap${{ matrix.ext }} "$pkgdir"/
|
cp target/${{ matrix.target }}/release/minedmap${{ matrix.ext }} "$pkgdir"/
|
||||||
|
|
||||||
- name: 'Archive'
|
- name: 'Archive'
|
||||||
uses: 'actions/upload-artifact@v3'
|
uses: 'actions/upload-artifact@v4'
|
||||||
with:
|
with:
|
||||||
name: 'MinedMap-${{ steps.tag.outputs.tag }}-${{ matrix.target }}'
|
name: 'MinedMap-${{ steps.tag.outputs.tag }}-${{ matrix.target }}'
|
||||||
path: 'target/pkg'
|
path: 'target/pkg'
|
||||||
|
|
||||||
|
build-container:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
steps:
|
||||||
|
- name: 'Checkout'
|
||||||
|
uses: 'actions/checkout@v4'
|
||||||
|
|
||||||
|
- name: 'Get version'
|
||||||
|
id: 'tag'
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
git fetch --prune --unshallow --tags -f
|
||||||
|
echo "tag=$(git describe --abbrev=7 --match='v*' | sed 's/^v//')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
ghcr.io/neocturne/minedmap/minedmap
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=branch,suffix=-{{sha}}
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
build-args: |
|
||||||
|
MINEDMAP_VERSION=${{ steps.tag.outputs.tag }}
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
viewer-container:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
steps:
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
ghcr.io/neocturne/minedmap/viewer
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=branch,suffix=-{{sha}}
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: "{{defaultContext}}:viewer"
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
148
CHANGELOG.md
|
@ -2,13 +2,150 @@
|
||||||
|
|
||||||
## [Unreleased] - ReleaseDate
|
## [Unreleased] - ReleaseDate
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for Minecraft 1.21.5
|
||||||
|
|
||||||
|
Added new block types and handling for changed sign text storage format.
|
||||||
|
|
||||||
|
## [2.5.0] - 2025-03-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added experimental watch mode
|
||||||
|
|
||||||
|
Passing `--watch` will cause MinedMap to run continuously instead of exiting
|
||||||
|
after map generation, regenerating tiles whenever they change.
|
||||||
|
|
||||||
|
`--watch-delay` can be used to configure the delay between detecting a change
|
||||||
|
and runing the map generation, also limiting how often the regeneration
|
||||||
|
happens. This defaults to `30s`; significantly smaller values probably don't
|
||||||
|
make sense because Minecraft writes out changes in batches anyways.
|
||||||
|
|
||||||
|
Finally, `--jobs-initial` can be used to configure the number of parallel
|
||||||
|
generation threads for the initial cycle separately from the value used for
|
||||||
|
subsequent cycles after a change is detected (`-j`/`--jobs`). Subsequent
|
||||||
|
cycles usually need to regenerate only a small number of tiles, so setting
|
||||||
|
`--jobs` to a smaller value than `--jobs-initial` may be advantageous.
|
||||||
|
|
||||||
|
- Added jemalloc support to fix performace on musl targets
|
||||||
|
|
||||||
|
The global allocator can be switched to jemalloc by enabling the `jemalloc`
|
||||||
|
cargo feature now. This is not the default because it is not always faster
|
||||||
|
than the default system allocator; in particular, the glibc allocator has
|
||||||
|
slightly better performance in multithreaded mode. In addition, jemalloc
|
||||||
|
uses a bit more memory.
|
||||||
|
|
||||||
|
In addition, the `jemalloc-auto` feature has been introduced, which is enabled
|
||||||
|
by default and sets the global allocator to jemalloc on platforms where it is
|
||||||
|
clearly advantageous. For now, this is only done on musl-based targets, as
|
||||||
|
musl's default allocator is very slow in multithreaded operation (which was
|
||||||
|
making higher thread counts like `-j8` basically useless due to 7-8x
|
||||||
|
slowdowns). With the new default, performance on musl is basically identical
|
||||||
|
to glibc.
|
||||||
|
|
||||||
|
Note that some platforms like `msvc` are unsupported by jemalloc, and trying
|
||||||
|
to enable the `jemalloc` feature on these platforms may break the MinedMap
|
||||||
|
build or cause issues at runtime.
|
||||||
|
- Docker images can be downloaded from the GitHub Container registry
|
||||||
|
|
||||||
|
Two images are provided, one for the tile renderer and one with the viewer
|
||||||
|
and a web server. A `docker-compose.yml` example can be found in the
|
||||||
|
repository as a starting point.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Unknown biome types (from not yet supported or modded versions of Minecraft)
|
||||||
|
will now use plains biome colors as a fallback instead of resulting in water,
|
||||||
|
grass and foliage blocks to be rendered as transparent pixels
|
||||||
|
- Switched from zlib-ng to zlib-rs
|
||||||
|
|
||||||
|
This should have no noticable effect on the usage of MinedMap, but avoids
|
||||||
|
an external build dependency on CMake.
|
||||||
|
- Small (1-block) seagrass is now visible on the map
|
||||||
|
|
||||||
|
1-block seagrass in 1-block deep water would previously result in the ground
|
||||||
|
to be shown instead of water, as MinedMap currently doesn't handle the
|
||||||
|
"waterlogged" block status. As 1-block seagrass is relatively big compared to
|
||||||
|
other "small" plants, just considering it opaque seems like a good enough
|
||||||
|
solution that avoids having to implement advanced block status flags.
|
||||||
|
- Use Bincode 2 for storage of intermediate data
|
||||||
|
|
||||||
|
The update from Bincode 1 to 2 slightly reduces the size of the `processed`
|
||||||
|
directory used for intermediate data. At least Rust 1.85 is now required to
|
||||||
|
build MinedMap.
|
||||||
|
|
||||||
|
## [2.4.0] - 2025-01-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for rendering tiles in WebP format using the `--image-format` option
|
||||||
|
|
||||||
|
## [2.3.1] - 2025-01-06
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix text colors for signs modified using dye
|
||||||
|
- Fix text colors specified using `#rrggbb` CSS syntax in JSON text
|
||||||
|
|
||||||
|
Only named colors specified via JSON text were working as intended.
|
||||||
|
|
||||||
|
The mapping of color names to values is now handled by the generator. Both the generator and the
|
||||||
|
viewer must be updated for sign text colors to work.
|
||||||
|
|
||||||
|
## [2.3.0] - 2025-01-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for Minecraft 1.21.4 block types
|
||||||
|
- Added support for Minecraft 1.21.4 Pale Garden biome
|
||||||
|
- viewer: added images for pale oak signs
|
||||||
|
|
||||||
|
## [2.2.0] - 2024-06-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for Minecraft 1.21 block types
|
||||||
|
|
||||||
|
## [2.1.1] - 2024-06-14
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix crash due to incorrect counting in info message
|
||||||
|
|
||||||
|
The calculation of the number of skipped regions could underflow when more invalid than valid
|
||||||
|
regions were encountered.
|
||||||
|
- Ignore empty region files instead of treating them as invalid
|
||||||
|
|
||||||
|
Minecraft generates empty region files in some cases. Just ignore them instead of printing an
|
||||||
|
error message every time.
|
||||||
|
|
||||||
|
## [2.1.0] - 2024-01-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added sign layer
|
||||||
|
|
||||||
|
This feature is disabled by default. Use the `--sign-prefix` and `--sign-filter` options to
|
||||||
|
configure which signs to show on the map. `--sign-transform` allows to modify the displayed
|
||||||
|
sign text.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Without `--verbose`, only a single warning is printed at the end of
|
||||||
|
processing for unknown block/biome types, rather than once for every
|
||||||
|
section where such a block/biome is encountered.
|
||||||
|
|
||||||
## [2.0.2] - 2024-01-07
|
## [2.0.2] - 2024-01-07
|
||||||
|
|
||||||
### Updated
|
### Added
|
||||||
|
|
||||||
- Added support for Minecraft 1.20.3+
|
- Added support for Minecraft 1.20.3+
|
||||||
|
|
||||||
Minecraft 1.20.3 renamed the `grass` block type to `short_grass`.
|
Minecraft 1.20.3 renamed the `grass` block type to `short_grass`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
- Updated [Leaflet](https://leafletjs.com/) to 1.9.4
|
- Updated [Leaflet](https://leafletjs.com/) to 1.9.4
|
||||||
- Updated attribution URL to https://github.com/neocturne/MinedMap
|
- Updated attribution URL to https://github.com/neocturne/MinedMap
|
||||||
|
|
||||||
|
@ -57,7 +194,14 @@ intermediate data.
|
||||||
Full support for custom biomes datapacks might be added in a future release.
|
Full support for custom biomes datapacks might be added in a future release.
|
||||||
|
|
||||||
<!-- next-url -->
|
<!-- next-url -->
|
||||||
[Unreleased]: https://github.com/neocturne/MinedMap/compare/v2.0.2...HEAD
|
[Unreleased]: https://github.com/neocturne/MinedMap/compare/v2.5.0...HEAD
|
||||||
|
[2.5.0]: https://github.com/neocturne/MinedMap/compare/v2.4.0...v2.5.0
|
||||||
|
[2.4.0]: https://github.com/neocturne/MinedMap/compare/v2.3.1...v2.4.0
|
||||||
|
[2.3.1]: https://github.com/neocturne/MinedMap/compare/v2.3.0...v2.3.1
|
||||||
|
[2.3.0]: https://github.com/neocturne/MinedMap/compare/v2.2.0...v2.3.0
|
||||||
|
[2.2.0]: https://github.com/neocturne/MinedMap/compare/v2.1.1...v2.2.0
|
||||||
|
[2.1.1]: https://github.com/neocturne/MinedMap/compare/v2.1.0...v2.1.1
|
||||||
|
[2.1.0]: https://github.com/neocturne/MinedMap/compare/v2.0.2...v2.1.0
|
||||||
[2.0.2]: https://github.com/neocturne/MinedMap/compare/v2.0.1...v2.0.2
|
[2.0.2]: https://github.com/neocturne/MinedMap/compare/v2.0.1...v2.0.2
|
||||||
[2.0.1]: https://github.com/neocturne/MinedMap/compare/v2.0.0...v2.0.1
|
[2.0.1]: https://github.com/neocturne/MinedMap/compare/v2.0.0...v2.0.1
|
||||||
[2.0.0]: https://github.com/neocturne/MinedMap/compare/v1.19.1...v2.0.0
|
[2.0.0]: https://github.com/neocturne/MinedMap/compare/v1.19.1...v2.0.0
|
||||||
|
|
1041
Cargo.lock
generated
38
Cargo.toml
|
@ -2,17 +2,18 @@
|
||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/neocturne/MinedMap"
|
repository = "https://github.com/neocturne/MinedMap"
|
||||||
|
|
||||||
[workspace.metadata.release]
|
[workspace.metadata.release]
|
||||||
consolidate-commits = false
|
consolidate-commits = false
|
||||||
|
pre-release-commit-message = "{{crate_name}} {{version}}"
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "minedmap"
|
name = "minedmap"
|
||||||
version = "2.0.2"
|
version = "2.5.0"
|
||||||
description = "Generate browsable maps from Minecraft save data"
|
description = "Generate browsable maps from Minecraft save data"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
@ -26,7 +27,6 @@ exclude = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.release]
|
[package.metadata.release]
|
||||||
pre-release-commit-message = "{{crate_name}} {{version}}"
|
|
||||||
tag-message = "{{crate_name}} {{version}}"
|
tag-message = "{{crate_name}} {{version}}"
|
||||||
pre-release-replacements = [
|
pre-release-replacements = [
|
||||||
{file="CHANGELOG.md", search="Unreleased", replace="{{version}}"},
|
{file="CHANGELOG.md", search="Unreleased", replace="{{version}}"},
|
||||||
|
@ -38,22 +38,29 @@ pre-release-replacements = [
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.68"
|
anyhow = "1.0.68"
|
||||||
bincode = "1.3.3"
|
bincode = "2.0.1"
|
||||||
clap = { version = "4.1.4", features = ["derive"] }
|
clap = { version = "4.1.4", features = ["derive", "wrap_help"] }
|
||||||
|
enum-map = "2.7.3"
|
||||||
fastnbt = "2.3.2"
|
fastnbt = "2.3.2"
|
||||||
|
flate2 = { version = "1.1.0", features = ["zlib-rs"] }
|
||||||
futures-util = "0.3.28"
|
futures-util = "0.3.28"
|
||||||
git-version = "0.3.5"
|
git-version = "0.3.5"
|
||||||
image = { version = "0.24.5", default-features = false, features = ["png"] }
|
humantime = "2.1.0"
|
||||||
indexmap = { version = "2.0.0", features = ["serde"] }
|
image = { version = "0.25.1", default-features = false, features = ["png", "webp"] }
|
||||||
lru = "0.12.0"
|
indexmap = "2.0.0"
|
||||||
minedmap-nbt = { version = "0.1.1", path = "crates/nbt", default-features = false }
|
lru = "0.13.0"
|
||||||
minedmap-resource = { version = "0.2.0", path = "crates/resource" }
|
minedmap-default-alloc = { version = "0.1.0", path = "crates/default-alloc", optional = true }
|
||||||
minedmap-types = { version = "0.1.2", path = "crates/types" }
|
minedmap-nbt = { version = "0.2.0", path = "crates/nbt", default-features = false }
|
||||||
|
minedmap-resource = { version = "0.7.0", path = "crates/resource" }
|
||||||
|
minedmap-types = { version = "0.2.0", path = "crates/types" }
|
||||||
|
notify = "8.0.0"
|
||||||
num-integer = "0.1.45"
|
num-integer = "0.1.45"
|
||||||
num_cpus = "1.16.0"
|
num_cpus = "1.16.0"
|
||||||
|
phf = { version = "0.11.2", features = ["macros"] }
|
||||||
rayon = "1.7.0"
|
rayon = "1.7.0"
|
||||||
rustc-hash = "1.1.0"
|
regex = "1.10.2"
|
||||||
serde = { version = "1.0.152", features = ["rc", "derive"] }
|
rustc-hash = "2.0.0"
|
||||||
|
serde = { version = "1.0.152", features = ["derive"] }
|
||||||
serde_json = "1.0.99"
|
serde_json = "1.0.99"
|
||||||
tokio = { version = "1.31.0", features = ["rt", "parking_lot", "sync"] }
|
tokio = { version = "1.31.0", features = ["rt", "parking_lot", "sync"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
|
@ -61,5 +68,6 @@ tracing-subscriber = "0.3.17"
|
||||||
zstd = "0.13.0"
|
zstd = "0.13.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["zlib-ng"]
|
default = ["jemalloc-auto"]
|
||||||
zlib-ng = ["minedmap-nbt/zlib-ng"]
|
jemalloc-auto = ["dep:minedmap-default-alloc"]
|
||||||
|
jemalloc = ["jemalloc-auto", "minedmap-default-alloc/jemalloc"]
|
||||||
|
|
17
Dockerfile
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
FROM docker.io/library/rust:1.85.1-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
RUN apk add --no-cache build-base tini-static
|
||||||
|
|
||||||
|
ARG MINEDMAP_VERSION
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN cargo build -r
|
||||||
|
RUN strip target/release/minedmap
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
COPY --from=builder /sbin/tini-static /build/target/release/minedmap /bin/
|
||||||
|
ENTRYPOINT [ "/bin/tini-static", "--", "/bin/minedmap" ]
|
||||||
|
|
||||||
|
USER 1000:1000
|
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2015 Matthias Schiffer
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
94
README.md
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
* Render beautiful maps of your [Minecraft](https://minecraft.net/) worlds!
|
* Render beautiful maps of your [Minecraft](https://minecraft.net/) worlds!
|
||||||
* Put them on a webserver and view them in your browser!
|
* Put them on a webserver and view them in your browser!
|
||||||
* Compatible with unmodified Minecraft Java Edition 1.8 up to 1.20 (no mod installation necessary!)
|
* Compatible with unmodified Minecraft Java Edition 1.8 up to 1.21.4 (no mod installation required!)
|
||||||
* Illumination layer: the world at night
|
* Illumination layer: the world at night
|
||||||
* Fast: create a full map for a huge 3GB savegame in less than 5 minutes in single-threaded operation
|
* Fast: create a full map for a huge 3GB savegame in less than 5 minutes in single-threaded operation
|
||||||
* Multi-threading support: pass `-j N` to the renderer to use `N` parallel threads for generation
|
* Multi-threading support: pass `-j N` to the renderer to use `N` parallel threads for generation
|
||||||
|
@ -10,9 +10,9 @@
|
||||||
* Typically uses less than 100MB of RAM in single-threaded operation (may be higher when `-j` is passed)
|
* Typically uses less than 100MB of RAM in single-threaded operation (may be higher when `-j` is passed)
|
||||||
* Cross-platform: runs on Linux, Windows, and likely other systems like MacOS as well
|
* Cross-platform: runs on Linux, Windows, and likely other systems like MacOS as well
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## How to use
|
## About
|
||||||
|
|
||||||
MinedMap consists of two components: a map renderer generating map tiles from
|
MinedMap consists of two components: a map renderer generating map tiles from
|
||||||
Minecraft save games, and a viewer for displaying and navigating maps in a browser
|
Minecraft save games, and a viewer for displaying and navigating maps in a browser
|
||||||
|
@ -20,16 +20,23 @@ based on [Leaflet](https://leafletjs.com/). The map renderer is heavily inspired
|
||||||
[MapRend](https://github.com/YSelfTool/MapRend), but has been reimplemented from scratch
|
[MapRend](https://github.com/YSelfTool/MapRend), but has been reimplemented from scratch
|
||||||
(first in C++, now in Rust) for highest performance.
|
(first in C++, now in Rust) for highest performance.
|
||||||
|
|
||||||
The viewer expects the the map data in a directory named `data`. To generate a new
|
## How to use
|
||||||
map, create this empty directory inside the viewer directory. Next, to generate the
|
|
||||||
map files run MinedMap passing the source and the destination paths on the command
|
Download the binary release that matches your platform from the Github release
|
||||||
line:
|
page (or install from source using `cargo`), as well as the platform-independent
|
||||||
|
viewer archive. Extract the viewer archive. The extracted directory contains the
|
||||||
|
HTML and JavaScript to operate the viewer and will be made publicly accessible
|
||||||
|
on a web server. The image data generated by MinedMap will be stored in the
|
||||||
|
`data` subdirectory of the extracted viewer.
|
||||||
|
|
||||||
|
Minecraft stores its save data in a directory `~/.minecraft/saves` on Linux,
|
||||||
|
and `C:\Users\<username>\AppData\Roaming\.minecraft\saves`. To generate MinedMap
|
||||||
|
tile data from a save game called "World", use the a command like the following
|
||||||
|
(replacing the first argument with the path to your save data; `<viewer>` refers
|
||||||
|
to the directory where you unpacked the MinedMap viewer):
|
||||||
```shell
|
```shell
|
||||||
minedmap /path/to/save/game /path/to/viewer/data
|
minedmap ~/.minecraft/saves/World <viewer>/data
|
||||||
```
|
```
|
||||||
The save game is stored in `saves` inside your Minecraft main directory
|
|
||||||
(`~/.minecraft` on Linux, `C:\Users\<username>\AppData\Roaming\.minecraft` on Windows)
|
|
||||||
in a subdirectory with the name of your world.
|
|
||||||
|
|
||||||
The first map generation might take a while for big worlds, but subsequent calls will
|
The first map generation might take a while for big worlds, but subsequent calls will
|
||||||
only rebuild tiles for region files that have changed, rarely taking more than a second
|
only rebuild tiles for region files that have changed, rarely taking more than a second
|
||||||
|
@ -38,7 +45,8 @@ MinedMap as a Cron job every minute.
|
||||||
|
|
||||||
Note that it is not possible to open the viewer *index.html* without a webserver, as
|
Note that it is not possible to open the viewer *index.html* without a webserver, as
|
||||||
it cannot load the generated map information from `file://` URIs. For testing purposes,
|
it cannot load the generated map information from `file://` URIs. For testing purposes,
|
||||||
you can use a minimal HTTP server, e.g. (if you have Python installed):
|
you can use a minimal HTTP server, e.g. if you have Python installed just run the
|
||||||
|
following in the viewer directory:
|
||||||
```shell
|
```shell
|
||||||
python3 -m http.server
|
python3 -m http.server
|
||||||
```
|
```
|
||||||
|
@ -46,19 +54,69 @@ This test server is very slow and cannot handle multiple requests concurrently,
|
||||||
a proper webserver like [nginx](https://nginx.org/) or upload the viewer together with
|
a proper webserver like [nginx](https://nginx.org/) or upload the viewer together with
|
||||||
the generated map files to public webspace to make the map available to others.
|
the generated map files to public webspace to make the map available to others.
|
||||||
|
|
||||||
|
If you are uploading the directory to a remote webserver, you do not need to upload the
|
||||||
|
`<viewer>/data/processed` directory, as it is only used locally to allow processing
|
||||||
|
updates more quickly.
|
||||||
|
|
||||||
|
### Image formats
|
||||||
|
|
||||||
|
MinedMap renders map tiles as PNG by default. Pass `--image-format webp` to select
|
||||||
|
WebP instead. For typical Minecraft worlds, using WebP reduces file sizes by 20-25%
|
||||||
|
without increasing processing time.
|
||||||
|
|
||||||
|
MinedMap always uses lossless compression for tile images, regardless of the
|
||||||
|
image format.
|
||||||
|
|
||||||
|
### Signs
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
MinedMap can display sign markers on the map, which will open a popup showing
|
||||||
|
the sign text when clicked.
|
||||||
|
|
||||||
|
Generation of the sign layer is disabled by default. It can be enabled by passing
|
||||||
|
the `--sign-prefix` or `--sign-filter` options to MinedMap. The options allow
|
||||||
|
to configure which signs should be displayed, and they can be passed multiple
|
||||||
|
times to show every sign that matches at least one prefix or filter.
|
||||||
|
|
||||||
|
`--sign-prefix` will make all signs visible the text of which starts with the
|
||||||
|
given prefix, so something like `--sign-prefix '[Map]'` would allow to put up
|
||||||
|
signs that start with "\[Map\]" in Minecraft to add markers to the map. An
|
||||||
|
empty prefix (`--sign-prefix ''`) can be used to make *all* signs visible on
|
||||||
|
the map.
|
||||||
|
|
||||||
|
`--sign-filter` can be used for more advanced filters based on regular expressions.
|
||||||
|
`--sign-filter '\[Map\]'` would show all signs that contain "\[Map\]"
|
||||||
|
anywhere in their text, and `--sign-filter '.'` makes all non-empty signs (signs
|
||||||
|
containing at least one character) visible. See the documentation of the
|
||||||
|
[regex crate](https://docs.rs/regex) for more information on the supported syntax.
|
||||||
|
|
||||||
|
All prefixes and filters are applied to the front and back text separately, but
|
||||||
|
both the front and the back text will be shown in the popup when one of them
|
||||||
|
matches.
|
||||||
|
|
||||||
|
Finally, `--sign-transform` allows to specify sed-style replacement patterns to
|
||||||
|
modify the text displayed on the map. This can be used if the text matched by
|
||||||
|
`--sign-prefix` or `--sign-filter` should not be displayed:
|
||||||
|
`--sign-transform 's/\[Map\]//'` would replace each occurence of "\[Map\]" with
|
||||||
|
the empty string.
|
||||||
|
|
||||||
|
**Note:** On Windows, double quotes (`"`) must be used for arguments instead
|
||||||
|
of single quotes (`'`), and all backslashes in the arguments must be escaped
|
||||||
|
by doubling them. This can make regular expressions somewhat difficult to
|
||||||
|
write and to read.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Building the MinedMap map generator from source requires a recent Rust toolchain (1.72.0
|
Binary builds of the map generator for Linux and Windows, as well as an archive
|
||||||
|
containing the viewer can be found on the GitHub release page.
|
||||||
|
|
||||||
|
Building the generator from source requires a recent Rust toolchain (1.72.0
|
||||||
or newer). The following command can be used to build the current development version:
|
or newer). The following command can be used to build the current development version:
|
||||||
```shell
|
```shell
|
||||||
cargo install --git 'https://github.com/neocturne/MinedMap.git'
|
cargo install --git 'https://github.com/neocturne/MinedMap.git'
|
||||||
```
|
```
|
||||||
|
|
||||||
In addition, CMake is needed to build the zlib-ng library. If you do not have
|
|
||||||
CMake installed, you can disable the zlib-ng feature by passing `--no-default-features`
|
|
||||||
to cargo. A pure-Rust zlib implementation will be used, which is more portable,
|
|
||||||
but slower than zlib-ng.
|
|
||||||
|
|
||||||
If you are looking for the older C++ implementation of the MinedMap tile renderer,
|
If you are looking for the older C++ implementation of the MinedMap tile renderer,
|
||||||
see the [v1.19.1](https://github.com/neocturne/MinedMap/tree/v1.19.1) tag.
|
see the [v1.19.1](https://github.com/neocturne/MinedMap/tree/v1.19.1) tag.
|
||||||
|
|
||||||
|
|
17
crates/default-alloc/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "minedmap-default-alloc"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Helper crate for target-specific selection of global allocator default"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tikv-jemallocator = { version = "0.6.0", optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_env = "musl")'.dependencies]
|
||||||
|
tikv-jemallocator = "0.6.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
jemalloc = ["dep:tikv-jemallocator"]
|
3
crates/default-alloc/src/lib.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#[cfg(any(target_env = "musl", feature = "jemalloc"))]
|
||||||
|
#[global_allocator]
|
||||||
|
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "minedmap-nbt"
|
name = "minedmap-nbt"
|
||||||
version = "0.1.1"
|
version = "0.2.0"
|
||||||
description = "MinedMap's handling of Minecraft NBT data and region files"
|
description = "MinedMap's handling of Minecraft NBT data and region files"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
@ -11,12 +11,10 @@ repository.workspace = true
|
||||||
anyhow = "1.0.75"
|
anyhow = "1.0.75"
|
||||||
bytemuck = "1.13.1"
|
bytemuck = "1.13.1"
|
||||||
fastnbt = "2.4.4"
|
fastnbt = "2.4.4"
|
||||||
flate2 = "1.0.27"
|
flate2 = "1.1.0"
|
||||||
minedmap-types = { version = "0.1.2", path = "../types" }
|
minedmap-types = { version = "0.2.0", path = "../types" }
|
||||||
serde = "1.0.183"
|
serde = "1.0.183"
|
||||||
|
|
||||||
[features]
|
|
||||||
zlib-ng = ["flate2/zlib-ng"]
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
clap = { version = "4.3.23", features = ["derive"] }
|
clap = { version = "4.3.23", features = ["derive"] }
|
||||||
|
flate2 = { version = "1.1.0", features = ["zlib-rs"] }
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{prelude::*, SeekFrom},
|
io::{SeekFrom, prelude::*},
|
||||||
path::Path,
|
path::Path,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use flate2::read::ZlibDecoder;
|
use flate2::read::ZlibDecoder;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "minedmap-resource"
|
name = "minedmap-resource"
|
||||||
version = "0.2.0"
|
version = "0.7.0"
|
||||||
description = "Data describing Minecraft biomes and block types"
|
description = "Data describing Minecraft biomes and block types"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
@ -8,6 +8,6 @@ readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
enumflags2 = { version = "0.7.7", features = ["serde"] }
|
bincode = "2.0.1"
|
||||||
glam = "0.25.0"
|
enumflags2 = "0.7.7"
|
||||||
serde = { version = "1.0.183", features = ["derive"] }
|
glam = "0.30.0"
|
||||||
|
|
|
@ -1,437 +1,117 @@
|
||||||
//! Biome data structures
|
//! Biome data
|
||||||
|
//!
|
||||||
|
//! This file is generated using resource/biomes.py, do not edit
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use super::*;
|
||||||
|
use BiomeGrassColorModifier::*;
|
||||||
|
|
||||||
use super::Color;
|
/// List if known biomes and their properties
|
||||||
|
pub const BIOMES: &[(&str, Biome)] = &[
|
||||||
/// Grass color modifier used by a biome
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
pub enum BiomeGrassColorModifier {
|
|
||||||
/// Grass color modifier used by the dark forest biome
|
|
||||||
DarkForest,
|
|
||||||
/// Grass color modifier used by swamp biomes
|
|
||||||
Swamp,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A biome specification
|
|
||||||
///
|
|
||||||
/// A Biome contains all information about a biome necessary to compute a block
|
|
||||||
/// color given a block type and depth
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
pub struct Biome {
|
|
||||||
/// Temperature value
|
|
||||||
///
|
|
||||||
/// For more efficient storage, the temperature is stored as an integer
|
|
||||||
/// after mutiplying the raw value by 20
|
|
||||||
pub temp: i8,
|
|
||||||
/// Downfall value
|
|
||||||
///
|
|
||||||
/// For more efficient storage, the downfall is stored as an integer
|
|
||||||
/// after mutiplying the raw value by 20
|
|
||||||
pub downfall: i8,
|
|
||||||
/// Water color override
|
|
||||||
pub water_color: Option<Color>,
|
|
||||||
/// Foliage color override
|
|
||||||
pub foliage_color: Option<Color>,
|
|
||||||
/// Grass color override
|
|
||||||
pub grass_color: Option<Color>,
|
|
||||||
/// Grass color modifier
|
|
||||||
pub grass_color_modifier: Option<BiomeGrassColorModifier>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Biome {
|
|
||||||
/// Constructs a new Biome
|
|
||||||
const fn new(temp: i16, downfall: i16) -> Biome {
|
|
||||||
/// Helper to encode temperature and downfall values
|
|
||||||
///
|
|
||||||
/// Converts temperatue and downfall from the input format
|
|
||||||
/// (mutiplied by 100) to i8 range for more efficient storage.
|
|
||||||
const fn encode(v: i16) -> i8 {
|
|
||||||
(v / 5) as i8
|
|
||||||
}
|
|
||||||
Biome {
|
|
||||||
temp: encode(temp),
|
|
||||||
downfall: encode(downfall),
|
|
||||||
grass_color_modifier: None,
|
|
||||||
water_color: None,
|
|
||||||
foliage_color: None,
|
|
||||||
grass_color: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builder function to override the biome water color
|
|
||||||
const fn water(self, water_color: [u8; 3]) -> Biome {
|
|
||||||
Biome {
|
|
||||||
water_color: Some(Color(water_color)),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builder function to override the biome foliage color
|
|
||||||
const fn foliage(self, foliage_color: [u8; 3]) -> Biome {
|
|
||||||
Biome {
|
|
||||||
foliage_color: Some(Color(foliage_color)),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builder function to override the biome grass color
|
|
||||||
const fn grass(self, grass_color: [u8; 3]) -> Biome {
|
|
||||||
Biome {
|
|
||||||
grass_color: Some(Color(grass_color)),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builder function to set a grass color modifier
|
|
||||||
const fn modify(self, grass_color_modifier: BiomeGrassColorModifier) -> Biome {
|
|
||||||
Biome {
|
|
||||||
grass_color_modifier: Some(grass_color_modifier),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decodes a temperature or downfall value from the storage format to
|
|
||||||
/// f32 for further calculation
|
|
||||||
fn decode(val: i8) -> f32 {
|
|
||||||
f32::from(val) / 20.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the biome's temperature decoded to its original float value
|
|
||||||
pub fn temp(&self) -> f32 {
|
|
||||||
Self::decode(self.temp)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the biome's downfall decoded to its original float value
|
|
||||||
pub fn downfall(&self) -> f32 {
|
|
||||||
Self::decode(self.downfall)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Standard biome specifications
|
|
||||||
pub const BIOMES: &[(&str, Biome)] = {
|
|
||||||
use BiomeGrassColorModifier::*;
|
|
||||||
|
|
||||||
// Data extracted from Minecraft code decompiled using https://github.com/Hexeption/MCP-Reborn
|
|
||||||
|
|
||||||
// We can't use floats in const functions, to temperature and downfall values
|
|
||||||
// are specified multipled by 100. The underscore is used in place of the decimal point
|
|
||||||
// of the original values.
|
|
||||||
|
|
||||||
#[allow(clippy::zero_prefixed_literal)]
|
|
||||||
&[
|
|
||||||
// Overworld
|
|
||||||
(
|
(
|
||||||
"badlands",
|
"badlands",
|
||||||
Biome::new(2_00, 0_00)
|
Biome::new(200, 0)
|
||||||
.foliage([158, 129, 77])
|
.foliage([158, 129, 77])
|
||||||
.grass([144, 129, 77]),
|
.grass([144, 129, 77]),
|
||||||
),
|
),
|
||||||
("bamboo_jungle", Biome::new(0_95, 0_90)),
|
("bamboo_jungle", Biome::new(95, 90)),
|
||||||
("beach", Biome::new(0_80, 0_40)),
|
("basalt_deltas", Biome::new(200, 0)),
|
||||||
("birch_forest", Biome::new(0_60, 0_60)),
|
("beach", Biome::new(80, 40)),
|
||||||
|
("birch_forest", Biome::new(60, 60)),
|
||||||
(
|
(
|
||||||
"cherry_grove",
|
"cherry_grove",
|
||||||
Biome::new(0_50, 0_80)
|
Biome::new(50, 80)
|
||||||
.water([93, 183, 239])
|
.foliage([182, 219, 97])
|
||||||
.grass([182, 219, 97])
|
.grass([182, 219, 97])
|
||||||
.foliage([182, 219, 97]),
|
.water([93, 183, 239]),
|
||||||
),
|
|
||||||
("cold_ocean", Biome::new(0_50, 0_50).water([61, 87, 214])),
|
|
||||||
("dark_forest", Biome::new(0_70, 0_80).modify(DarkForest)),
|
|
||||||
(
|
|
||||||
"deep_cold_ocean",
|
|
||||||
Biome::new(0_50, 0_50).water([61, 87, 214]),
|
|
||||||
),
|
|
||||||
("deep_dark", Biome::new(0_80, 0_40)),
|
|
||||||
(
|
|
||||||
"deep_frozen_ocean",
|
|
||||||
Biome::new(0_50, 0_50).water([57, 56, 201]),
|
|
||||||
),
|
),
|
||||||
|
("cold_ocean", Biome::new(50, 50).water([61, 87, 214])),
|
||||||
|
("crimson_forest", Biome::new(200, 0)),
|
||||||
|
("dark_forest", Biome::new(70, 80).modify(DarkForest)),
|
||||||
|
("deep_cold_ocean", Biome::new(50, 50).water([61, 87, 214])),
|
||||||
|
("deep_dark", Biome::new(80, 40)),
|
||||||
|
("deep_frozen_ocean", Biome::new(50, 50).water([57, 56, 201])),
|
||||||
(
|
(
|
||||||
"deep_lukewarm_ocean",
|
"deep_lukewarm_ocean",
|
||||||
Biome::new(0_50, 0_50).water([69, 173, 242]),
|
Biome::new(50, 50).water([69, 173, 242]),
|
||||||
),
|
),
|
||||||
("deep_ocean", Biome::new(0_50, 0_50)),
|
("deep_ocean", Biome::new(50, 50)),
|
||||||
("desert", Biome::new(2_00, 0_00)),
|
("desert", Biome::new(200, 0)),
|
||||||
("dripstone_caves", Biome::new(0_80, 0_40)),
|
("dripstone_caves", Biome::new(80, 40)),
|
||||||
|
("end_barrens", Biome::new(50, 50)),
|
||||||
|
("end_highlands", Biome::new(50, 50)),
|
||||||
|
("end_midlands", Biome::new(50, 50)),
|
||||||
(
|
(
|
||||||
"eroded_badlands",
|
"eroded_badlands",
|
||||||
Biome::new(2_00, 0_00)
|
Biome::new(200, 0)
|
||||||
.foliage([158, 129, 77])
|
.foliage([158, 129, 77])
|
||||||
.grass([144, 129, 77]),
|
.grass([144, 129, 77]),
|
||||||
),
|
),
|
||||||
("flower_forest", Biome::new(0_70, 0_80)),
|
("flower_forest", Biome::new(70, 80)),
|
||||||
("forest", Biome::new(0_70, 0_80)),
|
("forest", Biome::new(70, 80)),
|
||||||
("frozen_ocean", Biome::new(0_00, 0_50).water([57, 56, 201])),
|
("frozen_ocean", Biome::new(0, 50).water([57, 56, 201])),
|
||||||
("frozen_peaks", Biome::new(-0_70, 0_90)),
|
("frozen_peaks", Biome::new(-70, 90)),
|
||||||
("frozen_river", Biome::new(0_00, 0_50).water([57, 56, 201])),
|
("frozen_river", Biome::new(0, 50).water([57, 56, 201])),
|
||||||
("grove", Biome::new(-0_20, 0_80)),
|
("grove", Biome::new(-20, 80)),
|
||||||
("ice_spikes", Biome::new(0_00, 0_50)),
|
("ice_spikes", Biome::new(0, 50)),
|
||||||
("jagged_peaks", Biome::new(-0_70, 0_90)),
|
("jagged_peaks", Biome::new(-70, 90)),
|
||||||
("jungle", Biome::new(0_95, 0_90)),
|
("jungle", Biome::new(95, 90)),
|
||||||
(
|
("lukewarm_ocean", Biome::new(50, 50).water([69, 173, 242])),
|
||||||
"lukewarm_ocean",
|
("lush_caves", Biome::new(50, 50)),
|
||||||
Biome::new(0_50, 0_50).water([69, 173, 242]),
|
|
||||||
),
|
|
||||||
("lush_caves", Biome::new(0_50, 0_50)),
|
|
||||||
(
|
(
|
||||||
"mangrove_swamp",
|
"mangrove_swamp",
|
||||||
Biome::new(0_80, 0_90)
|
Biome::new(80, 90)
|
||||||
.water([58, 122, 106])
|
|
||||||
.foliage([141, 177, 39])
|
.foliage([141, 177, 39])
|
||||||
.modify(Swamp),
|
.modify(Swamp)
|
||||||
|
.water([58, 122, 106]),
|
||||||
),
|
),
|
||||||
("meadow", Biome::new(0_50, 0_80).water([14, 78, 207])),
|
("meadow", Biome::new(50, 80).water([14, 78, 207])),
|
||||||
("mushroom_fields", Biome::new(0_90, 1_00)),
|
("mushroom_fields", Biome::new(90, 100)),
|
||||||
("ocean", Biome::new(0_50, 0_50)),
|
("nether_wastes", Biome::new(200, 0)),
|
||||||
("old_growth_birch_forest", Biome::new(0_60, 0_60)),
|
("ocean", Biome::new(50, 50)),
|
||||||
("old_growth_pine_taiga", Biome::new(0_30, 0_80)),
|
("old_growth_birch_forest", Biome::new(60, 60)),
|
||||||
("old_growth_spruce_taiga", Biome::new(0_25, 0_80)),
|
("old_growth_pine_taiga", Biome::new(30, 80)),
|
||||||
("plains", Biome::new(0_80, 0_40)),
|
("old_growth_spruce_taiga", Biome::new(25, 80)),
|
||||||
("river", Biome::new(0_50, 0_50)),
|
(
|
||||||
("savanna", Biome::new(2_00, 0_00)),
|
"pale_garden",
|
||||||
("savanna_plateau", Biome::new(2_00, 0_00)),
|
Biome::new(70, 80)
|
||||||
("snowy_beach", Biome::new(0_05, 0_30).water([61, 87, 214])),
|
.foliage([135, 141, 118])
|
||||||
("snowy_plains", Biome::new(0_00, 0_50)),
|
.grass([119, 130, 114])
|
||||||
("snowy_slopes", Biome::new(-0_30, 0_90)),
|
.water([118, 136, 157]),
|
||||||
("snowy_taiga", Biome::new(-0_50, 0_40).water([61, 87, 214])),
|
),
|
||||||
("sparse_jungle", Biome::new(0_95, 0_80)),
|
("plains", Biome::new(80, 40)),
|
||||||
("stony_peaks", Biome::new(1_00, 0_30)),
|
("river", Biome::new(50, 50)),
|
||||||
("stony_shore", Biome::new(0_20, 0_30)),
|
("savanna", Biome::new(200, 0)),
|
||||||
("sunflower_plains", Biome::new(0_80, 0_40)),
|
("savanna_plateau", Biome::new(200, 0)),
|
||||||
|
("small_end_islands", Biome::new(50, 50)),
|
||||||
|
("snowy_beach", Biome::new(5, 30).water([61, 87, 214])),
|
||||||
|
("snowy_plains", Biome::new(0, 50)),
|
||||||
|
("snowy_slopes", Biome::new(-30, 90)),
|
||||||
|
("snowy_taiga", Biome::new(-50, 40).water([61, 87, 214])),
|
||||||
|
("soul_sand_valley", Biome::new(200, 0)),
|
||||||
|
("sparse_jungle", Biome::new(95, 80)),
|
||||||
|
("stony_peaks", Biome::new(100, 30)),
|
||||||
|
("stony_shore", Biome::new(20, 30)),
|
||||||
|
("sunflower_plains", Biome::new(80, 40)),
|
||||||
(
|
(
|
||||||
"swamp",
|
"swamp",
|
||||||
Biome::new(0_80, 0_90)
|
Biome::new(80, 90)
|
||||||
.water([97, 123, 100])
|
|
||||||
.foliage([106, 112, 57])
|
.foliage([106, 112, 57])
|
||||||
.modify(Swamp),
|
.modify(Swamp)
|
||||||
|
.water([97, 123, 100]),
|
||||||
),
|
),
|
||||||
("taiga", Biome::new(0_25, 0_80)),
|
("taiga", Biome::new(25, 80)),
|
||||||
("the_void", Biome::new(0_50, 0_50)),
|
("the_end", Biome::new(50, 50)),
|
||||||
("warm_ocean", Biome::new(0_50, 0_50).water([67, 213, 238])),
|
("the_void", Biome::new(50, 50)),
|
||||||
("windswept_forest", Biome::new(0_20, 0_30)),
|
("warm_ocean", Biome::new(50, 50).water([67, 213, 238])),
|
||||||
("windswept_gravelly_hills", Biome::new(0_20, 0_30)),
|
("warped_forest", Biome::new(200, 0)),
|
||||||
("windswept_hills", Biome::new(0_20, 0_30)),
|
("windswept_forest", Biome::new(20, 30)),
|
||||||
("windswept_savanna", Biome::new(2_00, 0_00)),
|
("windswept_gravelly_hills", Biome::new(20, 30)),
|
||||||
|
("windswept_hills", Biome::new(20, 30)),
|
||||||
|
("windswept_savanna", Biome::new(200, 0)),
|
||||||
(
|
(
|
||||||
"wooded_badlands",
|
"wooded_badlands",
|
||||||
Biome::new(2_00, 0_00)
|
Biome::new(200, 0)
|
||||||
.foliage([158, 129, 77])
|
.foliage([158, 129, 77])
|
||||||
.grass([144, 129, 77]),
|
.grass([144, 129, 77]),
|
||||||
),
|
),
|
||||||
// Nether
|
|
||||||
("basalt_deltas", Biome::new(2_00, 0_00)),
|
|
||||||
("crimson_forest", Biome::new(2_00, 0_00)),
|
|
||||||
("nether_wastes", Biome::new(2_00, 0_00)),
|
|
||||||
("soul_sand_valley", Biome::new(2_00, 0_00)),
|
|
||||||
("warped_forest", Biome::new(2_00, 0_00)),
|
|
||||||
// End
|
|
||||||
("end_barrens", Biome::new(0_50, 0_50)),
|
|
||||||
("end_highlands", Biome::new(0_50, 0_50)),
|
|
||||||
("end_midlands", Biome::new(0_50, 0_50)),
|
|
||||||
("small_end_islands", Biome::new(0_50, 0_50)),
|
|
||||||
("the_end", Biome::new(0_50, 0_50)),
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Biome ID aliases
|
|
||||||
///
|
|
||||||
/// Some biomes have been renamed or merged in recent Minecraft versions.
|
|
||||||
/// Maintain a list of aliases to support chunks saved by older versions.
|
|
||||||
pub const BIOME_ALIASES: &[(&str, &str)] = &[
|
|
||||||
// Biomes fix
|
|
||||||
("beaches", "beach"),
|
|
||||||
("cold_beach", "snowy_beach"),
|
|
||||||
("cold_deep_ocean", "deep_cold_ocean"),
|
|
||||||
("extreme_hills", "mountains"),
|
|
||||||
("extreme_hills_with_trees", "wooded_mountains"),
|
|
||||||
("forest_hills", "wooded_hills"),
|
|
||||||
("frozen_deep_ocean", "deep_frozen_ocean"),
|
|
||||||
("hell", "nether_wastes"),
|
|
||||||
("ice_flats", "snowy_tundra"),
|
|
||||||
("ice_mountains", "snowy_mountains"),
|
|
||||||
("lukewarm_deep_ocean", "deep_lukewarm_ocean"),
|
|
||||||
("mesa", "badlands"),
|
|
||||||
("mesa_clear_rock", "badlands_plateau"),
|
|
||||||
("mesa_rock", "wooded_badlands_plateau"),
|
|
||||||
("mushroom_island", "mushroom_fields"),
|
|
||||||
("mushroom_island_shore", "mushroom_field_shore"),
|
|
||||||
("mutated_birch_forest", "tall_birch_forest"),
|
|
||||||
("mutated_birch_forest_hills", "tall_birch_hills"),
|
|
||||||
("mutated_desert", "desert_lakes"),
|
|
||||||
("mutated_extreme_hills", "gravelly_mountains"),
|
|
||||||
(
|
|
||||||
"mutated_extreme_hills_with_trees",
|
|
||||||
"modified_gravelly_mountains",
|
|
||||||
),
|
|
||||||
("mutated_forest", "flower_forest"),
|
|
||||||
("mutated_ice_flats", "ice_spikes"),
|
|
||||||
("mutated_jungle", "modified_jungle"),
|
|
||||||
("mutated_jungle_edge", "modified_jungle_edge"),
|
|
||||||
("mutated_mesa", "eroded_badlands"),
|
|
||||||
("mutated_mesa_clear_rock", "modified_badlands_plateau"),
|
|
||||||
("mutated_mesa_rock", "modified_wooded_badlands_plateau"),
|
|
||||||
("mutated_plains", "sunflower_plains"),
|
|
||||||
("mutated_redwood_taiga", "giant_spruce_taiga"),
|
|
||||||
("mutated_redwood_taiga_hills", "giant_spruce_taiga_hills"),
|
|
||||||
("mutated_roofed_forest", "dark_forest_hills"),
|
|
||||||
("mutated_savanna", "shattered_savanna"),
|
|
||||||
("mutated_savanna_rock", "shattered_savanna_plateau"),
|
|
||||||
("mutated_swampland", "swamp_hills"),
|
|
||||||
("mutated_taiga", "taiga_mountains"),
|
|
||||||
("mutated_taiga_cold", "snowy_taiga_mountains"),
|
|
||||||
("redwood_taiga", "giant_tree_taiga"),
|
|
||||||
("redwood_taiga_hills", "giant_tree_taiga_hills"),
|
|
||||||
("roofed_forest", "dark_forest"),
|
|
||||||
("savanna_rock", "savanna_plateau"),
|
|
||||||
("sky", "the_end"),
|
|
||||||
("sky_island_barren", "end_barrens"),
|
|
||||||
("sky_island_high", "end_highlands"),
|
|
||||||
("sky_island_low", "small_end_islands"),
|
|
||||||
("sky_island_medium", "end_midlands"),
|
|
||||||
("smaller_extreme_hills", "mountain_edge"),
|
|
||||||
("stone_beach", "stone_shore"),
|
|
||||||
("swampland", "swamp"),
|
|
||||||
("taiga_cold", "snowy_taiga"),
|
|
||||||
("taiga_cold_hills", "snowy_taiga_hills"),
|
|
||||||
("void", "the_void"),
|
|
||||||
("warm_deep_ocean", "deep_warm_ocean"),
|
|
||||||
// Nether biome rename
|
|
||||||
("nether", "nether_wastes"),
|
|
||||||
// Caves and Cliffs biome renames
|
|
||||||
("badlands_plateau", "badlands"),
|
|
||||||
("bamboo_jungle_hills", "bamboo_jungle"),
|
|
||||||
("birch_forest_hills", "birch_forest"),
|
|
||||||
("dark_forest_hills", "dark_forest"),
|
|
||||||
("desert_hills", "desert"),
|
|
||||||
("desert_lakes", "desert"),
|
|
||||||
("giant_spruce_taiga", "old_growth_spruce_taiga"),
|
|
||||||
("giant_spruce_taiga_hills", "old_growth_spruce_taiga"),
|
|
||||||
("giant_tree_taiga", "old_growth_pine_taiga"),
|
|
||||||
("giant_tree_taiga_hills", "old_growth_pine_taiga"),
|
|
||||||
("gravelly_mountains", "windswept_gravelly_hills"),
|
|
||||||
("jungle_edge", "sparse_jungle"),
|
|
||||||
("jungle_hills", "jungle"),
|
|
||||||
("lofty_peaks", "jagged_peaks"),
|
|
||||||
("modified_badlands_plateau", "badlands"),
|
|
||||||
("modified_gravelly_mountains", "windswept_gravelly_hills"),
|
|
||||||
("modified_jungle", "jungle"),
|
|
||||||
("modified_jungle_edge", "sparse_jungle"),
|
|
||||||
("modified_wooded_badlands_plateau", "wooded_badlands"),
|
|
||||||
("mountain_edge", "windswept_hills"),
|
|
||||||
("mountains", "windswept_hills"),
|
|
||||||
("mushroom_field_shore", "mushroom_fields"),
|
|
||||||
("shattered_savanna", "windswept_savanna"),
|
|
||||||
("shattered_savanna_plateau", "windswept_savanna"),
|
|
||||||
("snowcapped_peaks", "frozen_peaks"),
|
|
||||||
("snowy_mountains", "snowy_plains"),
|
|
||||||
("snowy_taiga_hills", "snowy_taiga"),
|
|
||||||
("snowy_taiga_mountains", "snowy_taiga"),
|
|
||||||
("snowy_tundra", "snowy_plains"),
|
|
||||||
("stone_shore", "stony_shore"),
|
|
||||||
("swamp_hills", "swamp"),
|
|
||||||
("taiga_hills", "taiga"),
|
|
||||||
("taiga_mountains", "taiga"),
|
|
||||||
("tall_birch_forest", "old_growth_birch_forest"),
|
|
||||||
("tall_birch_hills", "old_growth_birch_forest"),
|
|
||||||
("wooded_badlands_plateau", "wooded_badlands"),
|
|
||||||
("wooded_hills", "forest"),
|
|
||||||
("wooded_mountains", "windswept_forest"),
|
|
||||||
// Remove Deep Warm Ocean
|
|
||||||
("deep_warm_ocean", "warm_ocean"),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Maps old numeric biome IDs to new string IDs
|
|
||||||
pub fn legacy_biome(index: u8) -> &'static str {
|
|
||||||
match index {
|
|
||||||
0 => "ocean",
|
|
||||||
1 => "plains",
|
|
||||||
2 => "desert",
|
|
||||||
3 => "mountains",
|
|
||||||
4 => "forest",
|
|
||||||
5 => "taiga",
|
|
||||||
6 => "swamp",
|
|
||||||
7 => "river",
|
|
||||||
8 => "nether_wastes",
|
|
||||||
9 => "the_end",
|
|
||||||
10 => "frozen_ocean",
|
|
||||||
11 => "frozen_river",
|
|
||||||
12 => "snowy_tundra",
|
|
||||||
13 => "snowy_mountains",
|
|
||||||
14 => "mushroom_fields",
|
|
||||||
15 => "mushroom_field_shore",
|
|
||||||
16 => "beach",
|
|
||||||
17 => "desert_hills",
|
|
||||||
18 => "wooded_hills",
|
|
||||||
19 => "taiga_hills",
|
|
||||||
20 => "mountain_edge",
|
|
||||||
21 => "jungle",
|
|
||||||
22 => "jungle_hills",
|
|
||||||
23 => "jungle_edge",
|
|
||||||
24 => "deep_ocean",
|
|
||||||
25 => "stone_shore",
|
|
||||||
26 => "snowy_beach",
|
|
||||||
27 => "birch_forest",
|
|
||||||
28 => "birch_forest_hills",
|
|
||||||
29 => "dark_forest",
|
|
||||||
30 => "snowy_taiga",
|
|
||||||
31 => "snowy_taiga_hills",
|
|
||||||
32 => "giant_tree_taiga",
|
|
||||||
33 => "giant_tree_taiga_hills",
|
|
||||||
34 => "wooded_mountains",
|
|
||||||
35 => "savanna",
|
|
||||||
36 => "savanna_plateau",
|
|
||||||
37 => "badlands",
|
|
||||||
38 => "wooded_badlands_plateau",
|
|
||||||
39 => "badlands_plateau",
|
|
||||||
40 => "small_end_islands",
|
|
||||||
41 => "end_midlands",
|
|
||||||
42 => "end_highlands",
|
|
||||||
43 => "end_barrens",
|
|
||||||
44 => "warm_ocean",
|
|
||||||
45 => "lukewarm_ocean",
|
|
||||||
46 => "cold_ocean",
|
|
||||||
47 => "deep_warm_ocean",
|
|
||||||
48 => "deep_lukewarm_ocean",
|
|
||||||
49 => "deep_cold_ocean",
|
|
||||||
50 => "deep_frozen_ocean",
|
|
||||||
127 => "the_void",
|
|
||||||
129 => "sunflower_plains",
|
|
||||||
130 => "desert_lakes",
|
|
||||||
131 => "gravelly_mountains",
|
|
||||||
132 => "flower_forest",
|
|
||||||
133 => "taiga_mountains",
|
|
||||||
134 => "swamp_hills",
|
|
||||||
140 => "ice_spikes",
|
|
||||||
149 => "modified_jungle",
|
|
||||||
151 => "modified_jungle_edge",
|
|
||||||
155 => "tall_birch_forest",
|
|
||||||
156 => "tall_birch_hills",
|
|
||||||
157 => "dark_forest_hills",
|
|
||||||
158 => "snowy_taiga_mountains",
|
|
||||||
160 => "giant_spruce_taiga",
|
|
||||||
161 => "giant_spruce_taiga_hills",
|
|
||||||
162 => "modified_gravelly_mountains",
|
|
||||||
163 => "shattered_savanna",
|
|
||||||
164 => "shattered_savanna_plateau",
|
|
||||||
165 => "eroded_badlands",
|
|
||||||
166 => "modified_wooded_badlands_plateau",
|
|
||||||
167 => "modified_badlands_plateau",
|
|
||||||
168 => "bamboo_jungle",
|
|
||||||
169 => "bamboo_jungle_hills",
|
|
||||||
170 => "soul_sand_valley",
|
|
||||||
171 => "crimson_forest",
|
|
||||||
172 => "warped_forest",
|
|
||||||
173 => "basalt_deltas",
|
|
||||||
174 => "dripstone_caves",
|
|
||||||
175 => "lush_caves",
|
|
||||||
177 => "meadow",
|
|
||||||
178 => "grove",
|
|
||||||
179 => "snowy_slopes",
|
|
||||||
180 => "snowcapped_peaks",
|
|
||||||
181 => "lofty_peaks",
|
|
||||||
182 => "stony_peaks",
|
|
||||||
_ => "ocean",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//! Functions for computations of block colors
|
//! Functions for computations of block colors
|
||||||
|
|
||||||
use super::{Biome, BlockType, Color, Colorf};
|
use super::{Biome, BlockColor, Color, Colorf};
|
||||||
|
|
||||||
/// Converts an u8 RGB color to a float vector
|
/// Converts an u8 RGB color to a float vector
|
||||||
fn color_vec_unscaled(color: Color) -> Colorf {
|
fn color_vec_unscaled(color: Color) -> Colorf {
|
||||||
|
@ -91,18 +91,18 @@ const BIRCH_COLOR: Colorf = Colorf::new(0.502, 0.655, 0.333); // == color_vec(Co
|
||||||
/// Color multiplier for spruce leaves
|
/// Color multiplier for spruce leaves
|
||||||
const EVERGREEN_COLOR: Colorf = Colorf::new(0.380, 0.600, 0.380); // == color_vec(Color([97, 153, 97]))
|
const EVERGREEN_COLOR: Colorf = Colorf::new(0.380, 0.600, 0.380); // == color_vec(Color([97, 153, 97]))
|
||||||
|
|
||||||
/// Determined if calling [block_color] for a given [BlockType] needs biome information
|
/// Determined if calling [block_color] for a given [BlockColor] needs biome information
|
||||||
pub fn needs_biome(block: BlockType) -> bool {
|
pub fn needs_biome(block: BlockColor) -> bool {
|
||||||
use super::BlockFlag::*;
|
use super::BlockFlag::*;
|
||||||
|
|
||||||
block.is(Grass) || block.is(Foliage) || block.is(Water)
|
block.is(Grass) || block.is(Foliage) || block.is(Water)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determined the block color to display for a given [BlockType]
|
/// Determined the block color to display for a given [BlockColor]
|
||||||
///
|
///
|
||||||
/// [needs_biome] must be used to determine whether passing a [Biome] is necessary.
|
/// [needs_biome] must be used to determine whether passing a [Biome] is necessary.
|
||||||
/// Will panic if a [Biome] is necessary, but none is passed.
|
/// Will panic if a [Biome] is necessary, but none is passed.
|
||||||
pub fn block_color(block: BlockType, biome: Option<&Biome>, depth: f32) -> Colorf {
|
pub fn block_color(block: BlockColor, biome: Option<&Biome>, depth: f32) -> Colorf {
|
||||||
use super::BlockFlag::*;
|
use super::BlockFlag::*;
|
||||||
|
|
||||||
let get_biome = || biome.expect("needs biome to determine block color");
|
let get_biome = || biome.expect("needs biome to determine block color");
|
||||||
|
|
202
crates/resource/src/legacy_biomes.rs
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
//! Manually maintained biome data (aliases and legacy biome IDs)
|
||||||
|
|
||||||
|
/// Biome ID aliases
|
||||||
|
///
|
||||||
|
/// Some biomes have been renamed or merged in recent Minecraft versions.
|
||||||
|
/// Maintain a list of aliases to support chunks saved by older versions.
|
||||||
|
pub const BIOME_ALIASES: &[(&str, &str)] = &[
|
||||||
|
// Biomes fix
|
||||||
|
("beaches", "beach"),
|
||||||
|
("cold_beach", "snowy_beach"),
|
||||||
|
("cold_deep_ocean", "deep_cold_ocean"),
|
||||||
|
("extreme_hills", "mountains"),
|
||||||
|
("extreme_hills_with_trees", "wooded_mountains"),
|
||||||
|
("forest_hills", "wooded_hills"),
|
||||||
|
("frozen_deep_ocean", "deep_frozen_ocean"),
|
||||||
|
("hell", "nether_wastes"),
|
||||||
|
("ice_flats", "snowy_tundra"),
|
||||||
|
("ice_mountains", "snowy_mountains"),
|
||||||
|
("lukewarm_deep_ocean", "deep_lukewarm_ocean"),
|
||||||
|
("mesa", "badlands"),
|
||||||
|
("mesa_clear_rock", "badlands_plateau"),
|
||||||
|
("mesa_rock", "wooded_badlands_plateau"),
|
||||||
|
("mushroom_island", "mushroom_fields"),
|
||||||
|
("mushroom_island_shore", "mushroom_field_shore"),
|
||||||
|
("mutated_birch_forest", "tall_birch_forest"),
|
||||||
|
("mutated_birch_forest_hills", "tall_birch_hills"),
|
||||||
|
("mutated_desert", "desert_lakes"),
|
||||||
|
("mutated_extreme_hills", "gravelly_mountains"),
|
||||||
|
(
|
||||||
|
"mutated_extreme_hills_with_trees",
|
||||||
|
"modified_gravelly_mountains",
|
||||||
|
),
|
||||||
|
("mutated_forest", "flower_forest"),
|
||||||
|
("mutated_ice_flats", "ice_spikes"),
|
||||||
|
("mutated_jungle", "modified_jungle"),
|
||||||
|
("mutated_jungle_edge", "modified_jungle_edge"),
|
||||||
|
("mutated_mesa", "eroded_badlands"),
|
||||||
|
("mutated_mesa_clear_rock", "modified_badlands_plateau"),
|
||||||
|
("mutated_mesa_rock", "modified_wooded_badlands_plateau"),
|
||||||
|
("mutated_plains", "sunflower_plains"),
|
||||||
|
("mutated_redwood_taiga", "giant_spruce_taiga"),
|
||||||
|
("mutated_redwood_taiga_hills", "giant_spruce_taiga_hills"),
|
||||||
|
("mutated_roofed_forest", "dark_forest_hills"),
|
||||||
|
("mutated_savanna", "shattered_savanna"),
|
||||||
|
("mutated_savanna_rock", "shattered_savanna_plateau"),
|
||||||
|
("mutated_swampland", "swamp_hills"),
|
||||||
|
("mutated_taiga", "taiga_mountains"),
|
||||||
|
("mutated_taiga_cold", "snowy_taiga_mountains"),
|
||||||
|
("redwood_taiga", "giant_tree_taiga"),
|
||||||
|
("redwood_taiga_hills", "giant_tree_taiga_hills"),
|
||||||
|
("roofed_forest", "dark_forest"),
|
||||||
|
("savanna_rock", "savanna_plateau"),
|
||||||
|
("sky", "the_end"),
|
||||||
|
("sky_island_barren", "end_barrens"),
|
||||||
|
("sky_island_high", "end_highlands"),
|
||||||
|
("sky_island_low", "small_end_islands"),
|
||||||
|
("sky_island_medium", "end_midlands"),
|
||||||
|
("smaller_extreme_hills", "mountain_edge"),
|
||||||
|
("stone_beach", "stone_shore"),
|
||||||
|
("swampland", "swamp"),
|
||||||
|
("taiga_cold", "snowy_taiga"),
|
||||||
|
("taiga_cold_hills", "snowy_taiga_hills"),
|
||||||
|
("void", "the_void"),
|
||||||
|
("warm_deep_ocean", "deep_warm_ocean"),
|
||||||
|
// Nether biome rename
|
||||||
|
("nether", "nether_wastes"),
|
||||||
|
// Caves and Cliffs biome renames
|
||||||
|
("badlands_plateau", "badlands"),
|
||||||
|
("bamboo_jungle_hills", "bamboo_jungle"),
|
||||||
|
("birch_forest_hills", "birch_forest"),
|
||||||
|
("dark_forest_hills", "dark_forest"),
|
||||||
|
("desert_hills", "desert"),
|
||||||
|
("desert_lakes", "desert"),
|
||||||
|
("giant_spruce_taiga", "old_growth_spruce_taiga"),
|
||||||
|
("giant_spruce_taiga_hills", "old_growth_spruce_taiga"),
|
||||||
|
("giant_tree_taiga", "old_growth_pine_taiga"),
|
||||||
|
("giant_tree_taiga_hills", "old_growth_pine_taiga"),
|
||||||
|
("gravelly_mountains", "windswept_gravelly_hills"),
|
||||||
|
("jungle_edge", "sparse_jungle"),
|
||||||
|
("jungle_hills", "jungle"),
|
||||||
|
("lofty_peaks", "jagged_peaks"),
|
||||||
|
("modified_badlands_plateau", "badlands"),
|
||||||
|
("modified_gravelly_mountains", "windswept_gravelly_hills"),
|
||||||
|
("modified_jungle", "jungle"),
|
||||||
|
("modified_jungle_edge", "sparse_jungle"),
|
||||||
|
("modified_wooded_badlands_plateau", "wooded_badlands"),
|
||||||
|
("mountain_edge", "windswept_hills"),
|
||||||
|
("mountains", "windswept_hills"),
|
||||||
|
("mushroom_field_shore", "mushroom_fields"),
|
||||||
|
("shattered_savanna", "windswept_savanna"),
|
||||||
|
("shattered_savanna_plateau", "windswept_savanna"),
|
||||||
|
("snowcapped_peaks", "frozen_peaks"),
|
||||||
|
("snowy_mountains", "snowy_plains"),
|
||||||
|
("snowy_taiga_hills", "snowy_taiga"),
|
||||||
|
("snowy_taiga_mountains", "snowy_taiga"),
|
||||||
|
("snowy_tundra", "snowy_plains"),
|
||||||
|
("stone_shore", "stony_shore"),
|
||||||
|
("swamp_hills", "swamp"),
|
||||||
|
("taiga_hills", "taiga"),
|
||||||
|
("taiga_mountains", "taiga"),
|
||||||
|
("tall_birch_forest", "old_growth_birch_forest"),
|
||||||
|
("tall_birch_hills", "old_growth_birch_forest"),
|
||||||
|
("wooded_badlands_plateau", "wooded_badlands"),
|
||||||
|
("wooded_hills", "forest"),
|
||||||
|
("wooded_mountains", "windswept_forest"),
|
||||||
|
// Remove Deep Warm Ocean
|
||||||
|
("deep_warm_ocean", "warm_ocean"),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Maps old numeric biome IDs to new string IDs
|
||||||
|
pub fn legacy_biome(index: u8) -> &'static str {
|
||||||
|
match index {
|
||||||
|
0 => "ocean",
|
||||||
|
1 => "plains",
|
||||||
|
2 => "desert",
|
||||||
|
3 => "mountains",
|
||||||
|
4 => "forest",
|
||||||
|
5 => "taiga",
|
||||||
|
6 => "swamp",
|
||||||
|
7 => "river",
|
||||||
|
8 => "nether_wastes",
|
||||||
|
9 => "the_end",
|
||||||
|
10 => "frozen_ocean",
|
||||||
|
11 => "frozen_river",
|
||||||
|
12 => "snowy_tundra",
|
||||||
|
13 => "snowy_mountains",
|
||||||
|
14 => "mushroom_fields",
|
||||||
|
15 => "mushroom_field_shore",
|
||||||
|
16 => "beach",
|
||||||
|
17 => "desert_hills",
|
||||||
|
18 => "wooded_hills",
|
||||||
|
19 => "taiga_hills",
|
||||||
|
20 => "mountain_edge",
|
||||||
|
21 => "jungle",
|
||||||
|
22 => "jungle_hills",
|
||||||
|
23 => "jungle_edge",
|
||||||
|
24 => "deep_ocean",
|
||||||
|
25 => "stone_shore",
|
||||||
|
26 => "snowy_beach",
|
||||||
|
27 => "birch_forest",
|
||||||
|
28 => "birch_forest_hills",
|
||||||
|
29 => "dark_forest",
|
||||||
|
30 => "snowy_taiga",
|
||||||
|
31 => "snowy_taiga_hills",
|
||||||
|
32 => "giant_tree_taiga",
|
||||||
|
33 => "giant_tree_taiga_hills",
|
||||||
|
34 => "wooded_mountains",
|
||||||
|
35 => "savanna",
|
||||||
|
36 => "savanna_plateau",
|
||||||
|
37 => "badlands",
|
||||||
|
38 => "wooded_badlands_plateau",
|
||||||
|
39 => "badlands_plateau",
|
||||||
|
40 => "small_end_islands",
|
||||||
|
41 => "end_midlands",
|
||||||
|
42 => "end_highlands",
|
||||||
|
43 => "end_barrens",
|
||||||
|
44 => "warm_ocean",
|
||||||
|
45 => "lukewarm_ocean",
|
||||||
|
46 => "cold_ocean",
|
||||||
|
47 => "deep_warm_ocean",
|
||||||
|
48 => "deep_lukewarm_ocean",
|
||||||
|
49 => "deep_cold_ocean",
|
||||||
|
50 => "deep_frozen_ocean",
|
||||||
|
127 => "the_void",
|
||||||
|
129 => "sunflower_plains",
|
||||||
|
130 => "desert_lakes",
|
||||||
|
131 => "gravelly_mountains",
|
||||||
|
132 => "flower_forest",
|
||||||
|
133 => "taiga_mountains",
|
||||||
|
134 => "swamp_hills",
|
||||||
|
140 => "ice_spikes",
|
||||||
|
149 => "modified_jungle",
|
||||||
|
151 => "modified_jungle_edge",
|
||||||
|
155 => "tall_birch_forest",
|
||||||
|
156 => "tall_birch_hills",
|
||||||
|
157 => "dark_forest_hills",
|
||||||
|
158 => "snowy_taiga_mountains",
|
||||||
|
160 => "giant_spruce_taiga",
|
||||||
|
161 => "giant_spruce_taiga_hills",
|
||||||
|
162 => "modified_gravelly_mountains",
|
||||||
|
163 => "shattered_savanna",
|
||||||
|
164 => "shattered_savanna_plateau",
|
||||||
|
165 => "eroded_badlands",
|
||||||
|
166 => "modified_wooded_badlands_plateau",
|
||||||
|
167 => "modified_badlands_plateau",
|
||||||
|
168 => "bamboo_jungle",
|
||||||
|
169 => "bamboo_jungle_hills",
|
||||||
|
170 => "soul_sand_valley",
|
||||||
|
171 => "crimson_forest",
|
||||||
|
172 => "warped_forest",
|
||||||
|
173 => "basalt_deltas",
|
||||||
|
174 => "dripstone_caves",
|
||||||
|
175 => "lush_caves",
|
||||||
|
177 => "meadow",
|
||||||
|
178 => "grove",
|
||||||
|
179 => "snowy_slopes",
|
||||||
|
180 => "snowcapped_peaks",
|
||||||
|
181 => "lofty_peaks",
|
||||||
|
182 => "stony_peaks",
|
||||||
|
_ => "ocean",
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ const DEF: &str = "air";
|
||||||
const EMPTY: [&str; 16] = simple(DEF);
|
const EMPTY: [&str; 16] = simple(DEF);
|
||||||
|
|
||||||
/// Mapping from each numeric block type and damage/subtype ID to new string ID
|
/// Mapping from each numeric block type and damage/subtype ID to new string ID
|
||||||
|
#[allow(clippy::large_const_arrays)]
|
||||||
pub const LEGACY_BLOCK_TYPES: [[&str; 16]; 256] = [
|
pub const LEGACY_BLOCK_TYPES: [[&str; 16]; 256] = [
|
||||||
/* 0 */
|
/* 0 */
|
||||||
simple("air"),
|
simple("air"),
|
||||||
|
|
|
@ -4,20 +4,19 @@
|
||||||
|
|
||||||
mod biomes;
|
mod biomes;
|
||||||
mod block_color;
|
mod block_color;
|
||||||
mod legacy_block_types;
|
|
||||||
|
|
||||||
#[allow(clippy::missing_docs_in_private_items)] // Generated module
|
|
||||||
mod block_types;
|
mod block_types;
|
||||||
|
mod legacy_biomes;
|
||||||
|
mod legacy_block_types;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use enumflags2::{bitflags, BitFlags};
|
use bincode::{BorrowDecode, Decode, Encode};
|
||||||
use serde::{Deserialize, Serialize};
|
use enumflags2::{BitFlags, bitflags};
|
||||||
|
|
||||||
/// Flags describing special properties of [BlockType]s
|
/// Flags describing special properties of [BlockType]s
|
||||||
#[bitflags]
|
#[bitflags]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum BlockFlag {
|
pub enum BlockFlag {
|
||||||
/// The block type is opaque
|
/// The block type is opaque
|
||||||
Opaque,
|
Opaque,
|
||||||
|
@ -27,36 +26,105 @@ pub enum BlockFlag {
|
||||||
Foliage,
|
Foliage,
|
||||||
/// The block type is birch foliage
|
/// The block type is birch foliage
|
||||||
Birch,
|
Birch,
|
||||||
/// The block type is spurce foliage
|
/// The block type is spruce foliage
|
||||||
Spruce,
|
Spruce,
|
||||||
/// The block type is colored using biome water colors
|
/// The block type is colored using biome water colors
|
||||||
Water,
|
Water,
|
||||||
|
/// The block type is a wall sign
|
||||||
|
///
|
||||||
|
/// The WallSign flag is used to distinguish wall signs from
|
||||||
|
/// freestanding or -hanging signs.
|
||||||
|
WallSign,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An RGB color with u8 components
|
/// An RGB color with u8 components
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Encode, Decode)]
|
||||||
pub struct Color(pub [u8; 3]);
|
pub struct Color(pub [u8; 3]);
|
||||||
|
|
||||||
/// An RGB color with f32 components
|
/// An RGB color with f32 components
|
||||||
pub type Colorf = glam::Vec3;
|
pub type Colorf = glam::Vec3;
|
||||||
|
|
||||||
/// A block type specification
|
/// A block type specification
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct BlockType {
|
pub struct BlockColor {
|
||||||
/// Bit set of [BlockFlag]s describing special properties of the block type
|
/// Bit set of [BlockFlag]s describing special properties of the block type
|
||||||
pub flags: BitFlags<BlockFlag>,
|
pub flags: BitFlags<BlockFlag>,
|
||||||
/// Base color of the block type
|
/// Base color of the block type
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlockType {
|
impl BlockColor {
|
||||||
/// Checks whether a block type has a given [BlockFlag] set
|
/// Checks whether a block color has a given [BlockFlag] set
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is(&self, flag: BlockFlag) -> bool {
|
pub fn is(&self, flag: BlockFlag) -> bool {
|
||||||
self.flags.contains(flag)
|
self.flags.contains(flag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Encode for BlockColor {
|
||||||
|
fn encode<E: bincode::enc::Encoder>(
|
||||||
|
&self,
|
||||||
|
encoder: &mut E,
|
||||||
|
) -> Result<(), bincode::error::EncodeError> {
|
||||||
|
bincode::Encode::encode(&self.flags.bits(), encoder)?;
|
||||||
|
bincode::Encode::encode(&self.color, encoder)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Context> Decode<Context> for BlockColor {
|
||||||
|
fn decode<D: bincode::de::Decoder<Context = Context>>(
|
||||||
|
decoder: &mut D,
|
||||||
|
) -> Result<Self, bincode::error::DecodeError> {
|
||||||
|
Ok(BlockColor {
|
||||||
|
flags: BitFlags::from_bits(bincode::Decode::decode(decoder)?).or(Err(
|
||||||
|
bincode::error::DecodeError::Other("invalid block flags"),
|
||||||
|
))?,
|
||||||
|
color: bincode::Decode::decode(decoder)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de, Context> BorrowDecode<'de, Context> for BlockColor {
|
||||||
|
fn borrow_decode<D: bincode::de::BorrowDecoder<'de, Context = Context>>(
|
||||||
|
decoder: &mut D,
|
||||||
|
) -> Result<Self, bincode::error::DecodeError> {
|
||||||
|
Ok(BlockColor {
|
||||||
|
flags: BitFlags::from_bits(bincode::BorrowDecode::borrow_decode(decoder)?).or(Err(
|
||||||
|
bincode::error::DecodeError::Other("invalid block flags"),
|
||||||
|
))?,
|
||||||
|
color: bincode::BorrowDecode::borrow_decode(decoder)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A block type specification (for use in constants)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct ConstBlockType {
|
||||||
|
/// Determines the rendered color of the block type
|
||||||
|
pub block_color: BlockColor,
|
||||||
|
/// Material of a sign block
|
||||||
|
pub sign_material: Option<&'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A block type specification
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BlockType {
|
||||||
|
/// Determines the rendered color of the block type
|
||||||
|
pub block_color: BlockColor,
|
||||||
|
/// Material of a sign block
|
||||||
|
pub sign_material: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ConstBlockType> for BlockType {
|
||||||
|
fn from(value: &ConstBlockType) -> Self {
|
||||||
|
BlockType {
|
||||||
|
block_color: value.block_color,
|
||||||
|
sign_material: value.sign_material.map(String::from),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Used to look up standard Minecraft block types
|
/// Used to look up standard Minecraft block types
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct BlockTypes {
|
pub struct BlockTypes {
|
||||||
|
@ -70,10 +138,15 @@ impl Default for BlockTypes {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let block_type_map: HashMap<_, _> = block_types::BLOCK_TYPES
|
let block_type_map: HashMap<_, _> = block_types::BLOCK_TYPES
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| (String::from(*k), *v))
|
.map(|(k, v)| (String::from(*k), BlockType::from(v)))
|
||||||
.collect();
|
.collect();
|
||||||
let legacy_block_types = Box::new(legacy_block_types::LEGACY_BLOCK_TYPES.map(|inner| {
|
let legacy_block_types = Box::new(legacy_block_types::LEGACY_BLOCK_TYPES.map(|inner| {
|
||||||
inner.map(|id| *block_type_map.get(id).expect("Unknown legacy block type"))
|
inner.map(|id| {
|
||||||
|
block_type_map
|
||||||
|
.get(id)
|
||||||
|
.expect("Unknown legacy block type")
|
||||||
|
.clone()
|
||||||
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
BlockTypes {
|
BlockTypes {
|
||||||
|
@ -86,21 +159,124 @@ impl Default for BlockTypes {
|
||||||
impl BlockTypes {
|
impl BlockTypes {
|
||||||
/// Resolves a Minecraft 1.13+ string block type ID
|
/// Resolves a Minecraft 1.13+ string block type ID
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get(&self, id: &str) -> Option<BlockType> {
|
pub fn get(&self, id: &str) -> Option<&BlockType> {
|
||||||
let suffix = id.strip_prefix("minecraft:")?;
|
let suffix = id.strip_prefix("minecraft:")?;
|
||||||
self.block_type_map.get(suffix).copied()
|
self.block_type_map.get(suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves a Minecraft pre-1.13 numeric block type ID
|
/// Resolves a Minecraft pre-1.13 numeric block type ID
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_legacy(&self, id: u8, data: u8) -> Option<BlockType> {
|
pub fn get_legacy(&self, id: u8, data: u8) -> Option<&BlockType> {
|
||||||
Some(self.legacy_block_types[id as usize][data as usize])
|
Some(&self.legacy_block_types[id as usize][data as usize])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use biomes::{Biome, BiomeGrassColorModifier};
|
|
||||||
pub use block_color::{block_color, needs_biome};
|
pub use block_color::{block_color, needs_biome};
|
||||||
|
|
||||||
|
/// Grass color modifier used by a biome
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)]
|
||||||
|
pub enum BiomeGrassColorModifier {
|
||||||
|
/// Grass color modifier used by the dark forest biome
|
||||||
|
DarkForest,
|
||||||
|
/// Grass color modifier used by swamp biomes
|
||||||
|
Swamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A biome specification
|
||||||
|
///
|
||||||
|
/// A Biome contains all information about a biome necessary to compute a block
|
||||||
|
/// color given a block type and depth
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)]
|
||||||
|
pub struct Biome {
|
||||||
|
/// Temperature value
|
||||||
|
///
|
||||||
|
/// For more efficient storage, the temperature is stored as an integer
|
||||||
|
/// after mutiplying the raw value by 20
|
||||||
|
pub temp: i8,
|
||||||
|
/// Downfall value
|
||||||
|
///
|
||||||
|
/// For more efficient storage, the downfall is stored as an integer
|
||||||
|
/// after mutiplying the raw value by 20
|
||||||
|
pub downfall: i8,
|
||||||
|
/// Water color override
|
||||||
|
pub water_color: Option<Color>,
|
||||||
|
/// Foliage color override
|
||||||
|
pub foliage_color: Option<Color>,
|
||||||
|
/// Grass color override
|
||||||
|
pub grass_color: Option<Color>,
|
||||||
|
/// Grass color modifier
|
||||||
|
pub grass_color_modifier: Option<BiomeGrassColorModifier>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Biome {
|
||||||
|
/// Constructs a new Biome
|
||||||
|
const fn new(temp: i16, downfall: i16) -> Biome {
|
||||||
|
/// Helper to encode temperature and downfall values
|
||||||
|
///
|
||||||
|
/// Converts temperatue and downfall from the input format
|
||||||
|
/// (mutiplied by 100) to i8 range for more efficient storage.
|
||||||
|
const fn encode(v: i16) -> i8 {
|
||||||
|
(v / 5) as i8
|
||||||
|
}
|
||||||
|
Biome {
|
||||||
|
temp: encode(temp),
|
||||||
|
downfall: encode(downfall),
|
||||||
|
grass_color_modifier: None,
|
||||||
|
water_color: None,
|
||||||
|
foliage_color: None,
|
||||||
|
grass_color: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder function to override the biome water color
|
||||||
|
const fn water(self, water_color: [u8; 3]) -> Biome {
|
||||||
|
Biome {
|
||||||
|
water_color: Some(Color(water_color)),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder function to override the biome foliage color
|
||||||
|
const fn foliage(self, foliage_color: [u8; 3]) -> Biome {
|
||||||
|
Biome {
|
||||||
|
foliage_color: Some(Color(foliage_color)),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder function to override the biome grass color
|
||||||
|
const fn grass(self, grass_color: [u8; 3]) -> Biome {
|
||||||
|
Biome {
|
||||||
|
grass_color: Some(Color(grass_color)),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder function to set a grass color modifier
|
||||||
|
const fn modify(self, grass_color_modifier: BiomeGrassColorModifier) -> Biome {
|
||||||
|
Biome {
|
||||||
|
grass_color_modifier: Some(grass_color_modifier),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes a temperature or downfall value from the storage format to
|
||||||
|
/// f32 for further calculation
|
||||||
|
fn decode(val: i8) -> f32 {
|
||||||
|
f32::from(val) / 20.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the biome's temperature decoded to its original float value
|
||||||
|
pub fn temp(&self) -> f32 {
|
||||||
|
Self::decode(self.temp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the biome's downfall decoded to its original float value
|
||||||
|
pub fn downfall(&self) -> f32 {
|
||||||
|
Self::decode(self.downfall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Used to look up standard Minecraft biome types
|
/// Used to look up standard Minecraft biome types
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct BiomeTypes {
|
pub struct BiomeTypes {
|
||||||
|
@ -108,6 +284,8 @@ pub struct BiomeTypes {
|
||||||
biome_map: HashMap<String, &'static Biome>,
|
biome_map: HashMap<String, &'static Biome>,
|
||||||
/// Array used to look up old numeric biome IDs
|
/// Array used to look up old numeric biome IDs
|
||||||
legacy_biomes: Box<[&'static Biome; 256]>,
|
legacy_biomes: Box<[&'static Biome; 256]>,
|
||||||
|
/// Fallback for unknown (new/modded) biomes
|
||||||
|
fallback_biome: &'static Biome,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for BiomeTypes {
|
impl Default for BiomeTypes {
|
||||||
|
@ -117,7 +295,7 @@ impl Default for BiomeTypes {
|
||||||
.map(|(k, v)| (String::from(*k), v))
|
.map(|(k, v)| (String::from(*k), v))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for &(old, new) in biomes::BIOME_ALIASES.iter().rev() {
|
for &(old, new) in legacy_biomes::BIOME_ALIASES.iter().rev() {
|
||||||
let biome = biome_map
|
let biome = biome_map
|
||||||
.get(new)
|
.get(new)
|
||||||
.copied()
|
.copied()
|
||||||
|
@ -127,16 +305,19 @@ impl Default for BiomeTypes {
|
||||||
|
|
||||||
let legacy_biomes = (0..=255)
|
let legacy_biomes = (0..=255)
|
||||||
.map(|index| {
|
.map(|index| {
|
||||||
let id = biomes::legacy_biome(index);
|
let id = legacy_biomes::legacy_biome(index);
|
||||||
*biome_map.get(id).expect("Unknown legacy biome")
|
*biome_map.get(id).expect("Unknown legacy biome")
|
||||||
})
|
})
|
||||||
.collect::<Box<[_]>>()
|
.collect::<Box<[_]>>()
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let fallback_biome = *biome_map.get("plains").expect("Plains biome undefined");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
biome_map,
|
biome_map,
|
||||||
legacy_biomes,
|
legacy_biomes,
|
||||||
|
fallback_biome,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,4 +335,10 @@ impl BiomeTypes {
|
||||||
pub fn get_legacy(&self, id: u8) -> Option<&Biome> {
|
pub fn get_legacy(&self, id: u8) -> Option<&Biome> {
|
||||||
Some(self.legacy_biomes[id as usize])
|
Some(self.legacy_biomes[id as usize])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the fallback for unknown (new/modded) biomes
|
||||||
|
#[inline]
|
||||||
|
pub fn get_fallback(&self) -> &Biome {
|
||||||
|
self.fallback_biome
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "minedmap-types"
|
name = "minedmap-types"
|
||||||
version = "0.1.2"
|
version = "0.2.0"
|
||||||
description = "Common types used by several MinedMap crates"
|
description = "Common types used by several MinedMap crates"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
@ -8,5 +8,5 @@ readme.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
itertools = "0.12.0"
|
bincode = "2.0.1"
|
||||||
serde = { version = "1.0.183", features = ["derive"] }
|
itertools = "0.14.0"
|
||||||
|
|
|
@ -8,8 +8,8 @@ use std::{
|
||||||
ops::{Index, IndexMut},
|
ops::{Index, IndexMut},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
use itertools::iproduct;
|
use itertools::iproduct;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Const generic AXIS arguments for coordinate types
|
/// Const generic AXIS arguments for coordinate types
|
||||||
pub mod axis {
|
pub mod axis {
|
||||||
|
@ -47,8 +47,7 @@ macro_rules! coord_type {
|
||||||
|
|
||||||
/// Returns an iterator over all possible values of the type
|
/// Returns an iterator over all possible values of the type
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn iter() -> impl Iterator<Item = $t<AXIS>>
|
pub fn iter() -> impl DoubleEndedIterator<Item = $t<AXIS>>
|
||||||
+ DoubleEndedIterator
|
|
||||||
+ ExactSizeIterator
|
+ ExactSizeIterator
|
||||||
+ FusedIterator
|
+ FusedIterator
|
||||||
+ Clone
|
+ Clone
|
||||||
|
@ -111,7 +110,7 @@ impl LayerBlockCoords {
|
||||||
/// Generic array for data stored per block of a chunk layer
|
/// Generic array for data stored per block of a chunk layer
|
||||||
///
|
///
|
||||||
/// Includes various convenient iteration functions.
|
/// Includes various convenient iteration functions.
|
||||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
|
||||||
pub struct LayerBlockArray<T>(pub [[T; BLOCKS_PER_CHUNK]; BLOCKS_PER_CHUNK]);
|
pub struct LayerBlockArray<T>(pub [[T; BLOCKS_PER_CHUNK]; BLOCKS_PER_CHUNK]);
|
||||||
|
|
||||||
impl<T> Index<LayerBlockCoords> for LayerBlockArray<T> {
|
impl<T> Index<LayerBlockCoords> for LayerBlockArray<T> {
|
||||||
|
@ -197,7 +196,7 @@ impl Debug for ChunkCoords {
|
||||||
/// Generic array for data stored per chunk of a region
|
/// Generic array for data stored per chunk of a region
|
||||||
///
|
///
|
||||||
/// Includes various convenient iteration functions.
|
/// Includes various convenient iteration functions.
|
||||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
|
||||||
pub struct ChunkArray<T>(pub [[T; CHUNKS_PER_REGION]; CHUNKS_PER_REGION]);
|
pub struct ChunkArray<T>(pub [[T; CHUNKS_PER_REGION]; CHUNKS_PER_REGION]);
|
||||||
|
|
||||||
impl<T> ChunkArray<T> {
|
impl<T> ChunkArray<T> {
|
||||||
|
|
51
docker-compose.yml
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# This is an example docker-compose configuration providing a Minecraft server,
|
||||||
|
# map generator and webserver. Visit http://localhost:8080 to view the map.
|
||||||
|
#
|
||||||
|
# See https://docker-minecraft-server.readthedocs.io/ for more information on
|
||||||
|
# the itzg/minecraft-server image and its configuration.
|
||||||
|
|
||||||
|
services:
|
||||||
|
mc:
|
||||||
|
image: docker.io/itzg/minecraft-server
|
||||||
|
environment:
|
||||||
|
EULA: 'true'
|
||||||
|
ports:
|
||||||
|
- '25565:25565'
|
||||||
|
volumes:
|
||||||
|
- data:/data
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
minedmap:
|
||||||
|
image: ghcr.io/neocturne/minedmap/minedmap
|
||||||
|
command:
|
||||||
|
- '--jobs-initial=2'
|
||||||
|
- '--image-format=webp'
|
||||||
|
- '--sign-filter=\[Map\]'
|
||||||
|
- '--sign-transform=s/\[Map\]//'
|
||||||
|
- '--watch'
|
||||||
|
- '/input/world'
|
||||||
|
- '/output'
|
||||||
|
volumes:
|
||||||
|
- data:/input:ro
|
||||||
|
- output:/output
|
||||||
|
- processed:/output/processed
|
||||||
|
network_mode: 'none'
|
||||||
|
depends_on:
|
||||||
|
mc:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
viewer:
|
||||||
|
image: ghcr.io/neocturne/minedmap/viewer
|
||||||
|
ports:
|
||||||
|
- '8080:80'
|
||||||
|
volumes:
|
||||||
|
- output:/usr/share/nginx/html/data:ro
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data: {}
|
||||||
|
processed: {}
|
||||||
|
output: {}
|
BIN
docs/images/signs.png
Normal file
After Width: | Height: | Size: 90 KiB |
|
@ -11,12 +11,15 @@ work.
|
||||||
- `extract.py`: Takes the block type information from `blocks.json` and texture data
|
- `extract.py`: Takes the block type information from `blocks.json` and texture data
|
||||||
from an unpacked Minecraft JAR, storing the result in `colors.json`
|
from an unpacked Minecraft JAR, storing the result in `colors.json`
|
||||||
- `generate.py`: Generates `block_types.rs` from `colors.json`
|
- `generate.py`: Generates `block_types.rs` from `colors.json`
|
||||||
|
- `biomes.py`: Generates `biomes.rs` from biome JSON files of an unpacked
|
||||||
|
Minecraft JAR
|
||||||
|
- `sign_textures.py`: Generates all needed sign graphics from Minecraft assets
|
||||||
|
|
||||||
In addition to these scripts, the JSON processor *jq* is a useful tool to work
|
In addition to these scripts, the JSON processor *jq* is a useful tool to work
|
||||||
with MinedMap's resource metadata.
|
with MinedMap's resource metadata.
|
||||||
|
|
||||||
|
|
||||||
## How to add support for block IDs of a new Minecraft version
|
## How to add support for block IDs and biomes of a new Minecraft version
|
||||||
|
|
||||||
1. Download the Minecraft version you want to support as well as the previous
|
1. Download the Minecraft version you want to support as well as the previous
|
||||||
version currently supported by MinedMap. You can use the Minecraft launcher
|
version currently supported by MinedMap. You can use the Minecraft launcher
|
||||||
|
@ -42,12 +45,13 @@ with MinedMap's resource metadata.
|
||||||
5. Edit `blocks.json` until the following command passes without errors:
|
5. Edit `blocks.json` until the following command passes without errors:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./extract.py blocks.json data/new/assets/minecraft/textures/block colors.json
|
./extract.py blocks.json data/new colors.json
|
||||||
```
|
```
|
||||||
|
|
||||||
If possible, the top texture of blocks should be used where different sides
|
If possible, the top texture of blocks should be used where different sides
|
||||||
exist. Block types that should not be visible on the map are just set to
|
exist. Block types that should not be visible on the map are just set to
|
||||||
`null` in the JSON.
|
`null` in the JSON (or have a `null` `texture` field when other flags need
|
||||||
|
to be set, like for sign blocks).
|
||||||
|
|
||||||
The `water`, `grass` and `foliage` flags control biome-dependent texture color modifiers.
|
The `water`, `grass` and `foliage` flags control biome-dependent texture color modifiers.
|
||||||
|
|
||||||
|
@ -67,6 +71,17 @@ with MinedMap's resource metadata.
|
||||||
cargo fmt --all
|
cargo fmt --all
|
||||||
```
|
```
|
||||||
|
|
||||||
|
8. Update the source code for new biome data:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./biomes.py data/new ../crates/resource/src/biomes.rs
|
||||||
|
cargo fmt --all
|
||||||
|
```
|
||||||
|
|
||||||
|
After regenerating, check if only new biomes were added. If entries
|
||||||
|
got removed, biomes may have been renamed or merged, requiring updates
|
||||||
|
to the alias list in `crates/resource/src/legacy_biomes.rs`.
|
||||||
|
|
||||||
After the update, the new version should be tested with old savegames (both
|
After the update, the new version should be tested with old savegames (both
|
||||||
before and after migration by the new version) as well as newly generated
|
before and after migration by the new version) as well as newly generated
|
||||||
worlds. Use creative mode to add the new block types to your test world.
|
worlds. Use creative mode to add the new block types to your test world.
|
||||||
|
|
70
resource/biomes.py
Executable file
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
sys.exit('Usage: biomes.py <data directory> <biomes.rs>')
|
||||||
|
|
||||||
|
biomes = {}
|
||||||
|
|
||||||
|
for file in os.scandir(os.path.join(sys.argv[1], 'data/minecraft/worldgen/biome')):
|
||||||
|
(name, ext) = os.path.splitext(file.name)
|
||||||
|
if ext != '.json':
|
||||||
|
continue
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
biomes[name] = {
|
||||||
|
'downfall': data['downfall'],
|
||||||
|
'temperature': data['temperature'],
|
||||||
|
'foliage_color': data['effects'].get('foliage_color'),
|
||||||
|
'grass_color': data['effects'].get('grass_color'),
|
||||||
|
'grass_color_modifier': data['effects'].get('grass_color_modifier'),
|
||||||
|
'water_color': data['effects'].get('water_color'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def color(v):
|
||||||
|
return f'[{v>>16}, {(v>>8)&0xff}, {v&0xff}]'
|
||||||
|
|
||||||
|
# Converts the snake_case grass color modifier to CamelCase
|
||||||
|
def modify(v):
|
||||||
|
return ''.join([s.capitalize() for s in v.split('_')])
|
||||||
|
|
||||||
|
def gen_biome(name, info, f):
|
||||||
|
temp = round(100*info['temperature'])
|
||||||
|
downfall = round(100*info['downfall'])
|
||||||
|
foliage_color = info['foliage_color']
|
||||||
|
grass_color = info['grass_color']
|
||||||
|
grass_color_modifier = info['grass_color_modifier']
|
||||||
|
water_color = info['water_color']
|
||||||
|
|
||||||
|
print(f'\t("{name}", Biome::new({temp}, {downfall})', file=f)
|
||||||
|
|
||||||
|
if foliage_color is not None:
|
||||||
|
print(f'\t\t.foliage({color(foliage_color)})', file=f)
|
||||||
|
if grass_color is not None:
|
||||||
|
print(f'\t\t.grass({color(grass_color)})', file=f)
|
||||||
|
if grass_color_modifier is not None:
|
||||||
|
print(f'\t\t.modify({modify(grass_color_modifier)})', file=f)
|
||||||
|
if water_color is not None and water_color != 0x3f76e4:
|
||||||
|
print(f'\t\t.water({color(water_color)})', file=f)
|
||||||
|
|
||||||
|
print('\t),', file=f)
|
||||||
|
|
||||||
|
with open(sys.argv[2], 'w') as f:
|
||||||
|
print('//! Biome data', file=f);
|
||||||
|
print('//!', file=f);
|
||||||
|
print('//! This file is generated using resource/biomes.py, do not edit', file=f);
|
||||||
|
print('', file=f)
|
||||||
|
print('use super::*;', file=f)
|
||||||
|
print('use BiomeGrassColorModifier::*;', file=f)
|
||||||
|
print('', file=f)
|
||||||
|
print('/// List if known biomes and their properties', file=f);
|
||||||
|
print('pub const BIOMES: &[(&str, Biome)] = &[', file=f)
|
||||||
|
|
||||||
|
for name in sorted(biomes):
|
||||||
|
gen_biome(name, biomes[name], f)
|
||||||
|
|
||||||
|
print('];', file=f)
|
|
@ -9,7 +9,10 @@
|
||||||
"acacia_fence_gate": {
|
"acacia_fence_gate": {
|
||||||
"texture": "acacia_planks"
|
"texture": "acacia_planks"
|
||||||
},
|
},
|
||||||
"acacia_hanging_sign": null,
|
"acacia_hanging_sign": {
|
||||||
|
"sign_material": "acacia",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
"acacia_leaves": {
|
"acacia_leaves": {
|
||||||
"foliage": true
|
"foliage": true
|
||||||
},
|
},
|
||||||
|
@ -22,7 +25,8 @@
|
||||||
},
|
},
|
||||||
"acacia_sapling": {},
|
"acacia_sapling": {},
|
||||||
"acacia_sign": {
|
"acacia_sign": {
|
||||||
"texture": "acacia_planks"
|
"sign_material": "acacia",
|
||||||
|
"texture": null
|
||||||
},
|
},
|
||||||
"acacia_slab": {
|
"acacia_slab": {
|
||||||
"texture": "acacia_planks"
|
"texture": "acacia_planks"
|
||||||
|
@ -31,8 +35,16 @@
|
||||||
"texture": "acacia_planks"
|
"texture": "acacia_planks"
|
||||||
},
|
},
|
||||||
"acacia_trapdoor": {},
|
"acacia_trapdoor": {},
|
||||||
"acacia_wall_hanging_sign": null,
|
"acacia_wall_hanging_sign": {
|
||||||
"acacia_wall_sign": null,
|
"sign_material": "acacia",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"acacia_wall_sign": {
|
||||||
|
"sign_material": "acacia",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
"acacia_wood": {
|
"acacia_wood": {
|
||||||
"texture": "acacia_log"
|
"texture": "acacia_log"
|
||||||
},
|
},
|
||||||
|
@ -84,7 +96,10 @@
|
||||||
"bamboo_fence_gate": {
|
"bamboo_fence_gate": {
|
||||||
"texture": "bamboo_planks"
|
"texture": "bamboo_planks"
|
||||||
},
|
},
|
||||||
"bamboo_hanging_sign": null,
|
"bamboo_hanging_sign": {
|
||||||
|
"sign_material": "bamboo",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
"bamboo_mosaic": {},
|
"bamboo_mosaic": {},
|
||||||
"bamboo_mosaic_slab": {
|
"bamboo_mosaic_slab": {
|
||||||
"texture": "bamboo_mosaic"
|
"texture": "bamboo_mosaic"
|
||||||
|
@ -98,7 +113,8 @@
|
||||||
},
|
},
|
||||||
"bamboo_sapling": null,
|
"bamboo_sapling": null,
|
||||||
"bamboo_sign": {
|
"bamboo_sign": {
|
||||||
"texture": "bamboo_planks"
|
"sign_material": "bamboo",
|
||||||
|
"texture": null
|
||||||
},
|
},
|
||||||
"bamboo_slab": {
|
"bamboo_slab": {
|
||||||
"texture": "bamboo_planks"
|
"texture": "bamboo_planks"
|
||||||
|
@ -107,8 +123,16 @@
|
||||||
"texture": "bamboo_planks"
|
"texture": "bamboo_planks"
|
||||||
},
|
},
|
||||||
"bamboo_trapdoor": {},
|
"bamboo_trapdoor": {},
|
||||||
"bamboo_wall_hanging_sign": null,
|
"bamboo_wall_hanging_sign": {
|
||||||
"bamboo_wall_sign": null,
|
"sign_material": "bamboo",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"bamboo_wall_sign": {
|
||||||
|
"sign_material": "bamboo",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
"barrel": {
|
"barrel": {
|
||||||
"texture": "barrel_top"
|
"texture": "barrel_top"
|
||||||
},
|
},
|
||||||
|
@ -144,7 +168,10 @@
|
||||||
"birch_fence_gate": {
|
"birch_fence_gate": {
|
||||||
"texture": "birch_planks"
|
"texture": "birch_planks"
|
||||||
},
|
},
|
||||||
"birch_hanging_sign": null,
|
"birch_hanging_sign": {
|
||||||
|
"sign_material": "birch",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
"birch_leaves": {
|
"birch_leaves": {
|
||||||
"birch": true
|
"birch": true
|
||||||
},
|
},
|
||||||
|
@ -157,7 +184,8 @@
|
||||||
},
|
},
|
||||||
"birch_sapling": {},
|
"birch_sapling": {},
|
||||||
"birch_sign": {
|
"birch_sign": {
|
||||||
"texture": "birch_planks"
|
"sign_material": "birch",
|
||||||
|
"texture": null
|
||||||
},
|
},
|
||||||
"birch_slab": {
|
"birch_slab": {
|
||||||
"texture": "birch_planks"
|
"texture": "birch_planks"
|
||||||
|
@ -166,8 +194,16 @@
|
||||||
"texture": "birch_planks"
|
"texture": "birch_planks"
|
||||||
},
|
},
|
||||||
"birch_trapdoor": {},
|
"birch_trapdoor": {},
|
||||||
"birch_wall_hanging_sign": null,
|
"birch_wall_hanging_sign": {
|
||||||
"birch_wall_sign": null,
|
"sign_material": "birch",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"birch_wall_sign": {
|
||||||
|
"sign_material": "birch",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
"birch_wood": {
|
"birch_wood": {
|
||||||
"texture": "birch_log"
|
"texture": "birch_log"
|
||||||
},
|
},
|
||||||
|
@ -280,9 +316,13 @@
|
||||||
"bubble_coral_fan": null,
|
"bubble_coral_fan": null,
|
||||||
"bubble_coral_wall_fan": null,
|
"bubble_coral_wall_fan": null,
|
||||||
"budding_amethyst": {},
|
"budding_amethyst": {},
|
||||||
|
"bush": {
|
||||||
|
"grass": true
|
||||||
|
},
|
||||||
"cactus": {
|
"cactus": {
|
||||||
"texture": "cactus_top"
|
"texture": "cactus_top"
|
||||||
},
|
},
|
||||||
|
"cactus_flower": {},
|
||||||
"cake": {
|
"cake": {
|
||||||
"texture": "cake_top"
|
"texture": "cake_top"
|
||||||
},
|
},
|
||||||
|
@ -326,7 +366,10 @@
|
||||||
"cherry_fence_gate": {
|
"cherry_fence_gate": {
|
||||||
"texture": "cherry_planks"
|
"texture": "cherry_planks"
|
||||||
},
|
},
|
||||||
"cherry_hanging_sign": null,
|
"cherry_hanging_sign": {
|
||||||
|
"sign_material": "cherry",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
"cherry_leaves": {},
|
"cherry_leaves": {},
|
||||||
"cherry_log": {
|
"cherry_log": {
|
||||||
"texture": "cherry_log_top"
|
"texture": "cherry_log_top"
|
||||||
|
@ -337,7 +380,8 @@
|
||||||
},
|
},
|
||||||
"cherry_sapling": null,
|
"cherry_sapling": null,
|
||||||
"cherry_sign": {
|
"cherry_sign": {
|
||||||
"texture": "cherry_planks"
|
"sign_material": "cherry",
|
||||||
|
"texture": null
|
||||||
},
|
},
|
||||||
"cherry_slab": {
|
"cherry_slab": {
|
||||||
"texture": "cherry_planks"
|
"texture": "cherry_planks"
|
||||||
|
@ -346,8 +390,16 @@
|
||||||
"texture": "cherry_planks"
|
"texture": "cherry_planks"
|
||||||
},
|
},
|
||||||
"cherry_trapdoor": {},
|
"cherry_trapdoor": {},
|
||||||
"cherry_wall_hanging_sign": null,
|
"cherry_wall_hanging_sign": {
|
||||||
"cherry_wall_sign": null,
|
"sign_material": "cherry",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"cherry_wall_sign": {
|
||||||
|
"sign_material": "cherry",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
"cherry_wood": {
|
"cherry_wood": {
|
||||||
"texture": "cherry_log"
|
"texture": "cherry_log"
|
||||||
},
|
},
|
||||||
|
@ -360,6 +412,7 @@
|
||||||
"chiseled_bookshelf": {
|
"chiseled_bookshelf": {
|
||||||
"texture": "chiseled_bookshelf_top"
|
"texture": "chiseled_bookshelf_top"
|
||||||
},
|
},
|
||||||
|
"chiseled_copper": {},
|
||||||
"chiseled_deepslate": {},
|
"chiseled_deepslate": {},
|
||||||
"chiseled_nether_bricks": {},
|
"chiseled_nether_bricks": {},
|
||||||
"chiseled_polished_blackstone": {},
|
"chiseled_polished_blackstone": {},
|
||||||
|
@ -367,13 +420,19 @@
|
||||||
"chiseled_red_sandstone": {
|
"chiseled_red_sandstone": {
|
||||||
"texture": "red_sandstone_top"
|
"texture": "red_sandstone_top"
|
||||||
},
|
},
|
||||||
|
"chiseled_resin_bricks": {},
|
||||||
"chiseled_sandstone": {
|
"chiseled_sandstone": {
|
||||||
"texture": "sandstone_top"
|
"texture": "sandstone_top"
|
||||||
},
|
},
|
||||||
"chiseled_stone_bricks": {},
|
"chiseled_stone_bricks": {},
|
||||||
|
"chiseled_tuff": {},
|
||||||
|
"chiseled_tuff_bricks": {
|
||||||
|
"texture": "chiseled_tuff_bricks_top"
|
||||||
|
},
|
||||||
"chorus_flower": {},
|
"chorus_flower": {},
|
||||||
"chorus_plant": {},
|
"chorus_plant": {},
|
||||||
"clay": {},
|
"clay": {},
|
||||||
|
"closed_eyeblossom": null,
|
||||||
"coal_block": {},
|
"coal_block": {},
|
||||||
"coal_ore": {},
|
"coal_ore": {},
|
||||||
"coarse_dirt": {},
|
"coarse_dirt": {},
|
||||||
|
@ -410,16 +469,28 @@
|
||||||
},
|
},
|
||||||
"conduit": {},
|
"conduit": {},
|
||||||
"copper_block": {},
|
"copper_block": {},
|
||||||
|
"copper_bulb": {},
|
||||||
|
"copper_door": {
|
||||||
|
"texture": "copper_door_top"
|
||||||
|
},
|
||||||
|
"copper_grate": {},
|
||||||
"copper_ore": {},
|
"copper_ore": {},
|
||||||
|
"copper_trapdoor": {},
|
||||||
"cornflower": null,
|
"cornflower": null,
|
||||||
"cracked_deepslate_bricks": {},
|
"cracked_deepslate_bricks": {},
|
||||||
"cracked_deepslate_tiles": {},
|
"cracked_deepslate_tiles": {},
|
||||||
"cracked_nether_bricks": {},
|
"cracked_nether_bricks": {},
|
||||||
"cracked_polished_blackstone_bricks": {},
|
"cracked_polished_blackstone_bricks": {},
|
||||||
"cracked_stone_bricks": {},
|
"cracked_stone_bricks": {},
|
||||||
|
"crafter": {
|
||||||
|
"texture": "crafter_top"
|
||||||
|
},
|
||||||
"crafting_table": {
|
"crafting_table": {
|
||||||
"texture": "crafting_table_top"
|
"texture": "crafting_table_top"
|
||||||
},
|
},
|
||||||
|
"creaking_heart": {
|
||||||
|
"texture": "creaking_heart_top"
|
||||||
|
},
|
||||||
"creeper_head": null,
|
"creeper_head": null,
|
||||||
"creeper_wall_head": null,
|
"creeper_wall_head": null,
|
||||||
"crimson_button": null,
|
"crimson_button": null,
|
||||||
|
@ -433,7 +504,10 @@
|
||||||
"texture": "crimson_planks"
|
"texture": "crimson_planks"
|
||||||
},
|
},
|
||||||
"crimson_fungus": null,
|
"crimson_fungus": null,
|
||||||
"crimson_hanging_sign": null,
|
"crimson_hanging_sign": {
|
||||||
|
"sign_material": "crimson",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
"crimson_hyphae": {
|
"crimson_hyphae": {
|
||||||
"texture": "crimson_stem"
|
"texture": "crimson_stem"
|
||||||
},
|
},
|
||||||
|
@ -444,7 +518,8 @@
|
||||||
},
|
},
|
||||||
"crimson_roots": {},
|
"crimson_roots": {},
|
||||||
"crimson_sign": {
|
"crimson_sign": {
|
||||||
"texture": "crimson_planks"
|
"sign_material": "crimson",
|
||||||
|
"texture": null
|
||||||
},
|
},
|
||||||
"crimson_slab": {
|
"crimson_slab": {
|
||||||
"texture": "crimson_planks"
|
"texture": "crimson_planks"
|
||||||
|
@ -456,8 +531,16 @@
|
||||||
"texture": "crimson_stem_top"
|
"texture": "crimson_stem_top"
|
||||||
},
|
},
|
||||||
"crimson_trapdoor": {},
|
"crimson_trapdoor": {},
|
||||||
"crimson_wall_hanging_sign": null,
|
"crimson_wall_hanging_sign": {
|
||||||
"crimson_wall_sign": null,
|
"sign_material": "crimson",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"crimson_wall_sign": {
|
||||||
|
"sign_material": "crimson",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
"crying_obsidian": {},
|
"crying_obsidian": {},
|
||||||
"cut_copper": {},
|
"cut_copper": {},
|
||||||
"cut_copper_slab": {
|
"cut_copper_slab": {
|
||||||
|
@ -512,7 +595,10 @@
|
||||||
"dark_oak_fence_gate": {
|
"dark_oak_fence_gate": {
|
||||||
"texture": "dark_oak_planks"
|
"texture": "dark_oak_planks"
|
||||||
},
|
},
|
||||||
"dark_oak_hanging_sign": null,
|
"dark_oak_hanging_sign": {
|
||||||
|
"sign_material": "dark_oak",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
"dark_oak_leaves": {
|
"dark_oak_leaves": {
|
||||||
"foliage": true
|
"foliage": true
|
||||||
},
|
},
|
||||||
|
@ -525,7 +611,8 @@
|
||||||
},
|
},
|
||||||
"dark_oak_sapling": {},
|
"dark_oak_sapling": {},
|
||||||
"dark_oak_sign": {
|
"dark_oak_sign": {
|
||||||
"texture": "dark_oak_planks"
|
"sign_material": "dark_oak",
|
||||||
|
"texture": null
|
||||||
},
|
},
|
||||||
"dark_oak_slab": {
|
"dark_oak_slab": {
|
||||||
"texture": "dark_oak_planks"
|
"texture": "dark_oak_planks"
|
||||||
|
@ -534,8 +621,16 @@
|
||||||
"texture": "dark_oak_planks"
|
"texture": "dark_oak_planks"
|
||||||
},
|
},
|
||||||
"dark_oak_trapdoor": {},
|
"dark_oak_trapdoor": {},
|
||||||
"dark_oak_wall_hanging_sign": null,
|
"dark_oak_wall_hanging_sign": {
|
||||||
"dark_oak_wall_sign": null,
|
"sign_material": "dark_oak",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"dark_oak_wall_sign": {
|
||||||
|
"sign_material": "dark_oak",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
"dark_oak_wood": {
|
"dark_oak_wood": {
|
||||||
"texture": "dark_oak_log"
|
"texture": "dark_oak_log"
|
||||||
},
|
},
|
||||||
|
@ -661,7 +756,14 @@
|
||||||
"ender_chest": {
|
"ender_chest": {
|
||||||
"texture": "obsidian"
|
"texture": "obsidian"
|
||||||
},
|
},
|
||||||
|
"exposed_chiseled_copper": {},
|
||||||
"exposed_copper": {},
|
"exposed_copper": {},
|
||||||
|
"exposed_copper_bulb": {},
|
||||||
|
"exposed_copper_door": {
|
||||||
|
"texture": "exposed_copper_door_top"
|
||||||
|
},
|
||||||
|
"exposed_copper_grate": {},
|
||||||
|
"exposed_copper_trapdoor": {},
|
||||||
"exposed_cut_copper": {},
|
"exposed_cut_copper": {},
|
||||||
"exposed_cut_copper_slab": {
|
"exposed_cut_copper_slab": {
|
||||||
"texture": "exposed_cut_copper"
|
"texture": "exposed_cut_copper"
|
||||||
|
@ -680,6 +782,7 @@
|
||||||
"fire_coral_block": {},
|
"fire_coral_block": {},
|
||||||
"fire_coral_fan": null,
|
"fire_coral_fan": null,
|
||||||
"fire_coral_wall_fan": null,
|
"fire_coral_wall_fan": null,
|
||||||
|
"firefly_bush": {},
|
||||||
"fletching_table": {
|
"fletching_table": {
|
||||||
"texture": "fletching_table_top"
|
"texture": "fletching_table_top"
|
||||||
},
|
},
|
||||||
|
@ -771,6 +874,7 @@
|
||||||
"hay_block": {
|
"hay_block": {
|
||||||
"texture": "hay_block_top"
|
"texture": "hay_block_top"
|
||||||
},
|
},
|
||||||
|
"heavy_core": {},
|
||||||
"heavy_weighted_pressure_plate": {
|
"heavy_weighted_pressure_plate": {
|
||||||
"texture": "iron_block"
|
"texture": "iron_block"
|
||||||
},
|
},
|
||||||
|
@ -832,7 +936,10 @@
|
||||||
"jungle_fence_gate": {
|
"jungle_fence_gate": {
|
||||||
"texture": "jungle_planks"
|
"texture": "jungle_planks"
|
||||||
},
|
},
|
||||||
"jungle_hanging_sign": null,
|
"jungle_hanging_sign": {
|
||||||
|
"sign_material": "jungle",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
"jungle_leaves": {
|
"jungle_leaves": {
|
||||||
"foliage": true
|
"foliage": true
|
||||||
},
|
},
|
||||||
|
@ -845,7 +952,8 @@
|
||||||
},
|
},
|
||||||
"jungle_sapling": {},
|
"jungle_sapling": {},
|
||||||
"jungle_sign": {
|
"jungle_sign": {
|
||||||
"texture": "jungle_planks"
|
"sign_material": "jungle",
|
||||||
|
"texture": null
|
||||||
},
|
},
|
||||||
"jungle_slab": {
|
"jungle_slab": {
|
||||||
"texture": "jungle_planks"
|
"texture": "jungle_planks"
|
||||||
|
@ -854,8 +962,16 @@
|
||||||
"texture": "jungle_planks"
|
"texture": "jungle_planks"
|
||||||
},
|
},
|
||||||
"jungle_trapdoor": {},
|
"jungle_trapdoor": {},
|
||||||
"jungle_wall_hanging_sign": null,
|
"jungle_wall_hanging_sign": {
|
||||||
"jungle_wall_sign": null,
|
"sign_material": "jungle",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"jungle_wall_sign": {
|
||||||
|
"sign_material": "jungle",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
"jungle_wood": {
|
"jungle_wood": {
|
||||||
"texture": "jungle_log"
|
"texture": "jungle_log"
|
||||||
},
|
},
|
||||||
|
@ -876,6 +992,7 @@
|
||||||
"lava_cauldron": {
|
"lava_cauldron": {
|
||||||
"texture": "cauldron_top"
|
"texture": "cauldron_top"
|
||||||
},
|
},
|
||||||
|
"leaf_litter": null,
|
||||||
"lectern": {
|
"lectern": {
|
||||||
"texture": "lectern_top"
|
"texture": "lectern_top"
|
||||||
},
|
},
|
||||||
|
@ -991,7 +1108,10 @@
|
||||||
"mangrove_fence_gate": {
|
"mangrove_fence_gate": {
|
||||||
"texture": "mangrove_planks"
|
"texture": "mangrove_planks"
|
||||||
},
|
},
|
||||||
"mangrove_hanging_sign": null,
|
"mangrove_hanging_sign": {
|
||||||
|
"sign_material": "mangrove",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
"mangrove_leaves": {
|
"mangrove_leaves": {
|
||||||
"foliage": true
|
"foliage": true
|
||||||
},
|
},
|
||||||
|
@ -1007,7 +1127,8 @@
|
||||||
"texture": "mangrove_roots_top"
|
"texture": "mangrove_roots_top"
|
||||||
},
|
},
|
||||||
"mangrove_sign": {
|
"mangrove_sign": {
|
||||||
"texture": "mangrove_planks"
|
"sign_material": "mangrove",
|
||||||
|
"texture": null
|
||||||
},
|
},
|
||||||
"mangrove_slab": {
|
"mangrove_slab": {
|
||||||
"texture": "mangrove_planks"
|
"texture": "mangrove_planks"
|
||||||
|
@ -1016,8 +1137,16 @@
|
||||||
"texture": "mangrove_planks"
|
"texture": "mangrove_planks"
|
||||||
},
|
},
|
||||||
"mangrove_trapdoor": {},
|
"mangrove_trapdoor": {},
|
||||||
"mangrove_wall_hanging_sign": null,
|
"mangrove_wall_hanging_sign": {
|
||||||
"mangrove_wall_sign": null,
|
"sign_material": "mangrove",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"mangrove_wall_sign": {
|
||||||
|
"sign_material": "mangrove",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
"mangrove_wood": {
|
"mangrove_wood": {
|
||||||
"texture": "mangrove_log"
|
"texture": "mangrove_log"
|
||||||
},
|
},
|
||||||
|
@ -1105,7 +1234,10 @@
|
||||||
"oak_fence_gate": {
|
"oak_fence_gate": {
|
||||||
"texture": "oak_planks"
|
"texture": "oak_planks"
|
||||||
},
|
},
|
||||||
"oak_hanging_sign": null,
|
"oak_hanging_sign": {
|
||||||
|
"sign_material": "oak",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
"oak_leaves": {
|
"oak_leaves": {
|
||||||
"foliage": true
|
"foliage": true
|
||||||
},
|
},
|
||||||
|
@ -1118,7 +1250,8 @@
|
||||||
},
|
},
|
||||||
"oak_sapling": {},
|
"oak_sapling": {},
|
||||||
"oak_sign": {
|
"oak_sign": {
|
||||||
"texture": "oak_planks"
|
"sign_material": "oak",
|
||||||
|
"texture": null
|
||||||
},
|
},
|
||||||
"oak_slab": {
|
"oak_slab": {
|
||||||
"texture": "oak_planks"
|
"texture": "oak_planks"
|
||||||
|
@ -1127,8 +1260,16 @@
|
||||||
"texture": "oak_planks"
|
"texture": "oak_planks"
|
||||||
},
|
},
|
||||||
"oak_trapdoor": {},
|
"oak_trapdoor": {},
|
||||||
"oak_wall_hanging_sign": null,
|
"oak_wall_hanging_sign": {
|
||||||
"oak_wall_sign": null,
|
"sign_material": "oak",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"oak_wall_sign": {
|
||||||
|
"sign_material": "oak",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
"oak_wood": {
|
"oak_wood": {
|
||||||
"texture": "oak_log"
|
"texture": "oak_log"
|
||||||
},
|
},
|
||||||
|
@ -1139,6 +1280,7 @@
|
||||||
"ochre_froglight": {
|
"ochre_froglight": {
|
||||||
"texture": "ochre_froglight_top"
|
"texture": "ochre_froglight_top"
|
||||||
},
|
},
|
||||||
|
"open_eyeblossom": null,
|
||||||
"orange_banner": null,
|
"orange_banner": null,
|
||||||
"orange_bed": null,
|
"orange_bed": null,
|
||||||
"orange_candle": null,
|
"orange_candle": null,
|
||||||
|
@ -1161,7 +1303,14 @@
|
||||||
"orange_wall_banner": null,
|
"orange_wall_banner": null,
|
||||||
"orange_wool": {},
|
"orange_wool": {},
|
||||||
"oxeye_daisy": null,
|
"oxeye_daisy": null,
|
||||||
|
"oxidized_chiseled_copper": {},
|
||||||
"oxidized_copper": {},
|
"oxidized_copper": {},
|
||||||
|
"oxidized_copper_bulb": {},
|
||||||
|
"oxidized_copper_door": {
|
||||||
|
"texture": "oxidized_copper_door_top"
|
||||||
|
},
|
||||||
|
"oxidized_copper_grate": {},
|
||||||
|
"oxidized_copper_trapdoor": {},
|
||||||
"oxidized_cut_copper": {},
|
"oxidized_cut_copper": {},
|
||||||
"oxidized_cut_copper_slab": {
|
"oxidized_cut_copper_slab": {
|
||||||
"texture": "oxidized_cut_copper"
|
"texture": "oxidized_cut_copper"
|
||||||
|
@ -1171,6 +1320,56 @@
|
||||||
},
|
},
|
||||||
"packed_ice": {},
|
"packed_ice": {},
|
||||||
"packed_mud": {},
|
"packed_mud": {},
|
||||||
|
"pale_hanging_moss": null,
|
||||||
|
"pale_moss_block": {},
|
||||||
|
"pale_moss_carpet": {},
|
||||||
|
"pale_oak_button": null,
|
||||||
|
"pale_oak_door": {
|
||||||
|
"texture": "pale_oak_door_top"
|
||||||
|
},
|
||||||
|
"pale_oak_fence": {
|
||||||
|
"texture": "pale_oak_planks"
|
||||||
|
},
|
||||||
|
"pale_oak_fence_gate": {
|
||||||
|
"texture": "pale_oak_planks"
|
||||||
|
},
|
||||||
|
"pale_oak_hanging_sign": {
|
||||||
|
"sign_material": "pale_oak",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
|
"pale_oak_leaves": {},
|
||||||
|
"pale_oak_log": {
|
||||||
|
"texture": "pale_oak_log_top"
|
||||||
|
},
|
||||||
|
"pale_oak_planks": {},
|
||||||
|
"pale_oak_pressure_plate": {
|
||||||
|
"texture": "pale_oak_planks"
|
||||||
|
},
|
||||||
|
"pale_oak_sapling": {},
|
||||||
|
"pale_oak_sign": {
|
||||||
|
"sign_material": "pale_oak",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
|
"pale_oak_slab": {
|
||||||
|
"texture": "pale_oak_planks"
|
||||||
|
},
|
||||||
|
"pale_oak_stairs": {
|
||||||
|
"texture": "pale_oak_planks"
|
||||||
|
},
|
||||||
|
"pale_oak_trapdoor": {},
|
||||||
|
"pale_oak_wall_hanging_sign": {
|
||||||
|
"sign_material": "pale_oak",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"pale_oak_wall_sign": {
|
||||||
|
"sign_material": "pale_oak",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"pale_oak_wood": {
|
||||||
|
"texture": "pale_oak_log"
|
||||||
|
},
|
||||||
"pearlescent_froglight": {
|
"pearlescent_froglight": {
|
||||||
"texture": "pearlescent_froglight_top"
|
"texture": "pearlescent_froglight_top"
|
||||||
},
|
},
|
||||||
|
@ -1282,6 +1481,16 @@
|
||||||
"polished_granite_stairs": {
|
"polished_granite_stairs": {
|
||||||
"texture": "polished_granite"
|
"texture": "polished_granite"
|
||||||
},
|
},
|
||||||
|
"polished_tuff": {},
|
||||||
|
"polished_tuff_slab": {
|
||||||
|
"texture": "polished_tuff"
|
||||||
|
},
|
||||||
|
"polished_tuff_stairs": {
|
||||||
|
"texture": "polished_tuff"
|
||||||
|
},
|
||||||
|
"polished_tuff_wall": {
|
||||||
|
"texture": "polished_tuff"
|
||||||
|
},
|
||||||
"poppy": null,
|
"poppy": null,
|
||||||
"potatoes": {
|
"potatoes": {
|
||||||
"texture": "potatoes_stage3"
|
"texture": "potatoes_stage3"
|
||||||
|
@ -1316,6 +1525,9 @@
|
||||||
"potted_cherry_sapling": {
|
"potted_cherry_sapling": {
|
||||||
"texture": "cherry_sapling"
|
"texture": "cherry_sapling"
|
||||||
},
|
},
|
||||||
|
"potted_closed_eyeblossom": {
|
||||||
|
"texture": "closed_eyeblossom"
|
||||||
|
},
|
||||||
"potted_cornflower": {
|
"potted_cornflower": {
|
||||||
"texture": "cornflower"
|
"texture": "cornflower"
|
||||||
},
|
},
|
||||||
|
@ -1353,12 +1565,18 @@
|
||||||
"potted_oak_sapling": {
|
"potted_oak_sapling": {
|
||||||
"texture": "oak_sapling"
|
"texture": "oak_sapling"
|
||||||
},
|
},
|
||||||
|
"potted_open_eyeblossom": {
|
||||||
|
"texture": "open_eyeblossom"
|
||||||
|
},
|
||||||
"potted_orange_tulip": {
|
"potted_orange_tulip": {
|
||||||
"texture": "orange_tulip"
|
"texture": "orange_tulip"
|
||||||
},
|
},
|
||||||
"potted_oxeye_daisy": {
|
"potted_oxeye_daisy": {
|
||||||
"texture": "oxeye_daisy"
|
"texture": "oxeye_daisy"
|
||||||
},
|
},
|
||||||
|
"potted_pale_oak_sapling": {
|
||||||
|
"texture": "pale_oak_sapling"
|
||||||
|
},
|
||||||
"potted_pink_tulip": {
|
"potted_pink_tulip": {
|
||||||
"texture": "pink_tulip"
|
"texture": "pink_tulip"
|
||||||
},
|
},
|
||||||
|
@ -1521,6 +1739,18 @@
|
||||||
"repeating_command_block": {
|
"repeating_command_block": {
|
||||||
"texture": "repeating_command_block_front"
|
"texture": "repeating_command_block_front"
|
||||||
},
|
},
|
||||||
|
"resin_block": {},
|
||||||
|
"resin_brick_slab": {
|
||||||
|
"texture": "resin_bricks"
|
||||||
|
},
|
||||||
|
"resin_brick_stairs": {
|
||||||
|
"texture": "resin_bricks"
|
||||||
|
},
|
||||||
|
"resin_brick_wall": {
|
||||||
|
"texture": "resin_bricks"
|
||||||
|
},
|
||||||
|
"resin_bricks": {},
|
||||||
|
"resin_clump": null,
|
||||||
"respawn_anchor": {
|
"respawn_anchor": {
|
||||||
"texture": "respawn_anchor_top"
|
"texture": "respawn_anchor_top"
|
||||||
},
|
},
|
||||||
|
@ -1557,12 +1787,14 @@
|
||||||
"sculk_vein": {},
|
"sculk_vein": {},
|
||||||
"sea_lantern": {},
|
"sea_lantern": {},
|
||||||
"sea_pickle": {},
|
"sea_pickle": {},
|
||||||
"seagrass": null,
|
"seagrass": {},
|
||||||
|
"short_dry_grass": {},
|
||||||
"short_grass": null,
|
"short_grass": null,
|
||||||
"shroomlight": {},
|
"shroomlight": {},
|
||||||
"shulker_box": {},
|
"shulker_box": {},
|
||||||
"sign": {
|
"sign": {
|
||||||
"texture": "oak_planks"
|
"sign_material": "oak",
|
||||||
|
"texture": null
|
||||||
},
|
},
|
||||||
"skeleton_skull": null,
|
"skeleton_skull": null,
|
||||||
"skeleton_wall_skull": null,
|
"skeleton_wall_skull": null,
|
||||||
|
@ -1638,7 +1870,10 @@
|
||||||
"spruce_fence_gate": {
|
"spruce_fence_gate": {
|
||||||
"texture": "spruce_planks"
|
"texture": "spruce_planks"
|
||||||
},
|
},
|
||||||
"spruce_hanging_sign": null,
|
"spruce_hanging_sign": {
|
||||||
|
"sign_material": "spruce",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
"spruce_leaves": {
|
"spruce_leaves": {
|
||||||
"spruce": true
|
"spruce": true
|
||||||
},
|
},
|
||||||
|
@ -1651,7 +1886,8 @@
|
||||||
},
|
},
|
||||||
"spruce_sapling": {},
|
"spruce_sapling": {},
|
||||||
"spruce_sign": {
|
"spruce_sign": {
|
||||||
"texture": "spruce_planks"
|
"sign_material": "spruce",
|
||||||
|
"texture": null
|
||||||
},
|
},
|
||||||
"spruce_slab": {
|
"spruce_slab": {
|
||||||
"texture": "spruce_planks"
|
"texture": "spruce_planks"
|
||||||
|
@ -1660,8 +1896,16 @@
|
||||||
"texture": "spruce_planks"
|
"texture": "spruce_planks"
|
||||||
},
|
},
|
||||||
"spruce_trapdoor": {},
|
"spruce_trapdoor": {},
|
||||||
"spruce_wall_hanging_sign": null,
|
"spruce_wall_hanging_sign": {
|
||||||
"spruce_wall_sign": null,
|
"sign_material": "spruce",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"spruce_wall_sign": {
|
||||||
|
"sign_material": "spruce",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
"spruce_wood": {
|
"spruce_wood": {
|
||||||
"texture": "spruce_log"
|
"texture": "spruce_log"
|
||||||
},
|
},
|
||||||
|
@ -1743,6 +1987,12 @@
|
||||||
"stripped_oak_wood": {
|
"stripped_oak_wood": {
|
||||||
"texture": "stripped_oak_log"
|
"texture": "stripped_oak_log"
|
||||||
},
|
},
|
||||||
|
"stripped_pale_oak_log": {
|
||||||
|
"texture": "stripped_pale_oak_log_top"
|
||||||
|
},
|
||||||
|
"stripped_pale_oak_wood": {
|
||||||
|
"texture": "stripped_pale_oak_log"
|
||||||
|
},
|
||||||
"stripped_spruce_log": {
|
"stripped_spruce_log": {
|
||||||
"texture": "stripped_spruce_log_top"
|
"texture": "stripped_spruce_log_top"
|
||||||
},
|
},
|
||||||
|
@ -1770,6 +2020,7 @@
|
||||||
"sweet_berry_bush": {
|
"sweet_berry_bush": {
|
||||||
"texture": "sweet_berry_bush_stage3"
|
"texture": "sweet_berry_bush_stage3"
|
||||||
},
|
},
|
||||||
|
"tall_dry_grass": {},
|
||||||
"tall_grass": {
|
"tall_grass": {
|
||||||
"grass": true,
|
"grass": true,
|
||||||
"texture": "tall_grass_top"
|
"texture": "tall_grass_top"
|
||||||
|
@ -1781,6 +2032,8 @@
|
||||||
"texture": "target_top"
|
"texture": "target_top"
|
||||||
},
|
},
|
||||||
"terracotta": {},
|
"terracotta": {},
|
||||||
|
"test_block": null,
|
||||||
|
"test_instance_block": null,
|
||||||
"tinted_glass": {},
|
"tinted_glass": {},
|
||||||
"tnt": {
|
"tnt": {
|
||||||
"texture": "tnt_top"
|
"texture": "tnt_top"
|
||||||
|
@ -1791,6 +2044,9 @@
|
||||||
"trapped_chest": {
|
"trapped_chest": {
|
||||||
"texture": "oak_planks"
|
"texture": "oak_planks"
|
||||||
},
|
},
|
||||||
|
"trial_spawner": {
|
||||||
|
"texture": "trial_spawner_top_inactive"
|
||||||
|
},
|
||||||
"tripwire": null,
|
"tripwire": null,
|
||||||
"tripwire_hook": null,
|
"tripwire_hook": null,
|
||||||
"tube_coral": null,
|
"tube_coral": null,
|
||||||
|
@ -1798,9 +2054,31 @@
|
||||||
"tube_coral_fan": null,
|
"tube_coral_fan": null,
|
||||||
"tube_coral_wall_fan": null,
|
"tube_coral_wall_fan": null,
|
||||||
"tuff": {},
|
"tuff": {},
|
||||||
|
"tuff_brick_slab": {
|
||||||
|
"texture": "tuff_bricks"
|
||||||
|
},
|
||||||
|
"tuff_brick_stairs": {
|
||||||
|
"texture": "tuff_bricks"
|
||||||
|
},
|
||||||
|
"tuff_brick_wall": {
|
||||||
|
"texture": "tuff_bricks"
|
||||||
|
},
|
||||||
|
"tuff_bricks": {},
|
||||||
|
"tuff_slab": {
|
||||||
|
"texture": "tuff"
|
||||||
|
},
|
||||||
|
"tuff_stairs": {
|
||||||
|
"texture": "tuff"
|
||||||
|
},
|
||||||
|
"tuff_wall": {
|
||||||
|
"texture": "tuff"
|
||||||
|
},
|
||||||
"turtle_egg": {},
|
"turtle_egg": {},
|
||||||
"twisting_vines": {},
|
"twisting_vines": {},
|
||||||
"twisting_vines_plant": {},
|
"twisting_vines_plant": {},
|
||||||
|
"vault": {
|
||||||
|
"texture": "vault_top"
|
||||||
|
},
|
||||||
"verdant_froglight": {
|
"verdant_froglight": {
|
||||||
"texture": "verdant_froglight_top"
|
"texture": "verdant_froglight_top"
|
||||||
},
|
},
|
||||||
|
@ -1808,7 +2086,11 @@
|
||||||
"grass": true
|
"grass": true
|
||||||
},
|
},
|
||||||
"void_air": null,
|
"void_air": null,
|
||||||
"wall_sign": null,
|
"wall_sign": {
|
||||||
|
"sign_material": "oak",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
"wall_torch": null,
|
"wall_torch": null,
|
||||||
"warped_button": null,
|
"warped_button": null,
|
||||||
"warped_door": {
|
"warped_door": {
|
||||||
|
@ -1821,7 +2103,10 @@
|
||||||
"texture": "warped_planks"
|
"texture": "warped_planks"
|
||||||
},
|
},
|
||||||
"warped_fungus": null,
|
"warped_fungus": null,
|
||||||
"warped_hanging_sign": null,
|
"warped_hanging_sign": {
|
||||||
|
"sign_material": "warped",
|
||||||
|
"texture": null
|
||||||
|
},
|
||||||
"warped_hyphae": {
|
"warped_hyphae": {
|
||||||
"texture": "warped_stem"
|
"texture": "warped_stem"
|
||||||
},
|
},
|
||||||
|
@ -1832,7 +2117,8 @@
|
||||||
},
|
},
|
||||||
"warped_roots": {},
|
"warped_roots": {},
|
||||||
"warped_sign": {
|
"warped_sign": {
|
||||||
"texture": "warped_planks"
|
"sign_material": "warped",
|
||||||
|
"texture": null
|
||||||
},
|
},
|
||||||
"warped_slab": {
|
"warped_slab": {
|
||||||
"texture": "warped_planks"
|
"texture": "warped_planks"
|
||||||
|
@ -1844,8 +2130,16 @@
|
||||||
"texture": "warped_stem_top"
|
"texture": "warped_stem_top"
|
||||||
},
|
},
|
||||||
"warped_trapdoor": {},
|
"warped_trapdoor": {},
|
||||||
"warped_wall_hanging_sign": null,
|
"warped_wall_hanging_sign": {
|
||||||
"warped_wall_sign": null,
|
"sign_material": "warped",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
|
"warped_wall_sign": {
|
||||||
|
"sign_material": "warped",
|
||||||
|
"texture": null,
|
||||||
|
"wall_sign": true
|
||||||
|
},
|
||||||
"warped_wart_block": {},
|
"warped_wart_block": {},
|
||||||
"water": {
|
"water": {
|
||||||
"texture": "water_still",
|
"texture": "water_still",
|
||||||
|
@ -1854,9 +2148,24 @@
|
||||||
"water_cauldron": {
|
"water_cauldron": {
|
||||||
"texture": "cauldron_top"
|
"texture": "cauldron_top"
|
||||||
},
|
},
|
||||||
|
"waxed_chiseled_copper": {
|
||||||
|
"texture": "chiseled_copper"
|
||||||
|
},
|
||||||
"waxed_copper_block": {
|
"waxed_copper_block": {
|
||||||
"texture": "copper_block"
|
"texture": "copper_block"
|
||||||
},
|
},
|
||||||
|
"waxed_copper_bulb": {
|
||||||
|
"texture": "copper_bulb"
|
||||||
|
},
|
||||||
|
"waxed_copper_door": {
|
||||||
|
"texture": "copper_door_top"
|
||||||
|
},
|
||||||
|
"waxed_copper_grate": {
|
||||||
|
"texture": "copper_grate"
|
||||||
|
},
|
||||||
|
"waxed_copper_trapdoor": {
|
||||||
|
"texture": "copper_trapdoor"
|
||||||
|
},
|
||||||
"waxed_cut_copper": {
|
"waxed_cut_copper": {
|
||||||
"texture": "cut_copper"
|
"texture": "cut_copper"
|
||||||
},
|
},
|
||||||
|
@ -1866,9 +2175,24 @@
|
||||||
"waxed_cut_copper_stairs": {
|
"waxed_cut_copper_stairs": {
|
||||||
"texture": "cut_copper"
|
"texture": "cut_copper"
|
||||||
},
|
},
|
||||||
|
"waxed_exposed_chiseled_copper": {
|
||||||
|
"texture": "exposed_chiseled_copper"
|
||||||
|
},
|
||||||
"waxed_exposed_copper": {
|
"waxed_exposed_copper": {
|
||||||
"texture": "exposed_copper"
|
"texture": "exposed_copper"
|
||||||
},
|
},
|
||||||
|
"waxed_exposed_copper_bulb": {
|
||||||
|
"texture": "exposed_copper_bulb"
|
||||||
|
},
|
||||||
|
"waxed_exposed_copper_door": {
|
||||||
|
"texture": "exposed_copper_door_top"
|
||||||
|
},
|
||||||
|
"waxed_exposed_copper_grate": {
|
||||||
|
"texture": "exposed_copper_grate"
|
||||||
|
},
|
||||||
|
"waxed_exposed_copper_trapdoor": {
|
||||||
|
"texture": "exposed_copper_trapdoor"
|
||||||
|
},
|
||||||
"waxed_exposed_cut_copper": {
|
"waxed_exposed_cut_copper": {
|
||||||
"texture": "exposed_cut_copper"
|
"texture": "exposed_cut_copper"
|
||||||
},
|
},
|
||||||
|
@ -1878,9 +2202,24 @@
|
||||||
"waxed_exposed_cut_copper_stairs": {
|
"waxed_exposed_cut_copper_stairs": {
|
||||||
"texture": "exposed_cut_copper"
|
"texture": "exposed_cut_copper"
|
||||||
},
|
},
|
||||||
|
"waxed_oxidized_chiseled_copper": {
|
||||||
|
"texture": "oxidized_chiseled_copper"
|
||||||
|
},
|
||||||
"waxed_oxidized_copper": {
|
"waxed_oxidized_copper": {
|
||||||
"texture": "oxidized_copper"
|
"texture": "oxidized_copper"
|
||||||
},
|
},
|
||||||
|
"waxed_oxidized_copper_bulb": {
|
||||||
|
"texture": "oxidized_copper_bulb"
|
||||||
|
},
|
||||||
|
"waxed_oxidized_copper_door": {
|
||||||
|
"texture": "oxidized_copper_door_top"
|
||||||
|
},
|
||||||
|
"waxed_oxidized_copper_grate": {
|
||||||
|
"texture": "oxidized_copper_grate"
|
||||||
|
},
|
||||||
|
"waxed_oxidized_copper_trapdoor": {
|
||||||
|
"texture": "oxidized_copper_trapdoor"
|
||||||
|
},
|
||||||
"waxed_oxidized_cut_copper": {
|
"waxed_oxidized_cut_copper": {
|
||||||
"texture": "oxidized_cut_copper"
|
"texture": "oxidized_cut_copper"
|
||||||
},
|
},
|
||||||
|
@ -1890,9 +2229,24 @@
|
||||||
"waxed_oxidized_cut_copper_stairs": {
|
"waxed_oxidized_cut_copper_stairs": {
|
||||||
"texture": "oxidized_cut_copper"
|
"texture": "oxidized_cut_copper"
|
||||||
},
|
},
|
||||||
|
"waxed_weathered_chiseled_copper": {
|
||||||
|
"texture": "weathered_chiseled_copper"
|
||||||
|
},
|
||||||
"waxed_weathered_copper": {
|
"waxed_weathered_copper": {
|
||||||
"texture": "weathered_copper"
|
"texture": "weathered_copper"
|
||||||
},
|
},
|
||||||
|
"waxed_weathered_copper_bulb": {
|
||||||
|
"texture": "weathered_copper_bulb"
|
||||||
|
},
|
||||||
|
"waxed_weathered_copper_door": {
|
||||||
|
"texture": "weathered_copper_door_top"
|
||||||
|
},
|
||||||
|
"waxed_weathered_copper_grate": {
|
||||||
|
"texture": "weathered_copper_grate"
|
||||||
|
},
|
||||||
|
"waxed_weathered_copper_trapdoor": {
|
||||||
|
"texture": "weathered_copper_trapdoor"
|
||||||
|
},
|
||||||
"waxed_weathered_cut_copper": {
|
"waxed_weathered_cut_copper": {
|
||||||
"texture": "weathered_cut_copper"
|
"texture": "weathered_cut_copper"
|
||||||
},
|
},
|
||||||
|
@ -1902,7 +2256,14 @@
|
||||||
"waxed_weathered_cut_copper_stairs": {
|
"waxed_weathered_cut_copper_stairs": {
|
||||||
"texture": "weathered_cut_copper"
|
"texture": "weathered_cut_copper"
|
||||||
},
|
},
|
||||||
|
"weathered_chiseled_copper": {},
|
||||||
"weathered_copper": {},
|
"weathered_copper": {},
|
||||||
|
"weathered_copper_bulb": {},
|
||||||
|
"weathered_copper_door": {
|
||||||
|
"texture": "weathered_copper_door_top"
|
||||||
|
},
|
||||||
|
"weathered_copper_grate": {},
|
||||||
|
"weathered_copper_trapdoor": {},
|
||||||
"weathered_cut_copper": {},
|
"weathered_cut_copper": {},
|
||||||
"weathered_cut_copper_slab": {
|
"weathered_cut_copper_slab": {
|
||||||
"texture": "weathered_cut_copper"
|
"texture": "weathered_cut_copper"
|
||||||
|
@ -1937,6 +2298,7 @@
|
||||||
"white_tulip": null,
|
"white_tulip": null,
|
||||||
"white_wall_banner": null,
|
"white_wall_banner": null,
|
||||||
"white_wool": {},
|
"white_wool": {},
|
||||||
|
"wildflowers": null,
|
||||||
"wither_rose": null,
|
"wither_rose": null,
|
||||||
"wither_skeleton_skull": null,
|
"wither_skeleton_skull": null,
|
||||||
"wither_skeleton_wall_skull": null,
|
"wither_skeleton_wall_skull": null,
|
||||||
|
|
|
@ -11,7 +11,7 @@ if len(sys.argv) != 4:
|
||||||
sys.exit('Usage: extract.py <blocks.json> <asset directory> <colors.json>')
|
sys.exit('Usage: extract.py <blocks.json> <asset directory> <colors.json>')
|
||||||
|
|
||||||
def mean_color(texture):
|
def mean_color(texture):
|
||||||
path = os.path.join(sys.argv[2], texture + '.png')
|
path = os.path.join(sys.argv[2], 'assets/minecraft/textures/block', texture + '.png')
|
||||||
im = Image.open(path)
|
im = Image.open(path)
|
||||||
|
|
||||||
data = im.convert('RGBA').getdata()
|
data = im.convert('RGBA').getdata()
|
||||||
|
@ -45,20 +45,30 @@ for name, info in blocks.items():
|
||||||
'birch': False,
|
'birch': False,
|
||||||
'spruce': False,
|
'spruce': False,
|
||||||
'water': False,
|
'water': False,
|
||||||
|
'wall_sign': False,
|
||||||
|
'sign_material': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if info is None:
|
if info is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
color = mean_color(info.get('texture', name))
|
texture = info.get('texture', name)
|
||||||
|
|
||||||
|
color = None
|
||||||
|
if texture:
|
||||||
|
color = mean_color(texture)
|
||||||
if color:
|
if color:
|
||||||
output[id]['color'] = color
|
output[id]['color'] = color
|
||||||
output[id]['opaque'] = True
|
output[id]['opaque'] = True
|
||||||
|
|
||||||
output[id]['grass'] = info.get('grass', False)
|
output[id]['grass'] = info.get('grass', False)
|
||||||
output[id]['foliage'] = info.get('foliage', False)
|
output[id]['foliage'] = info.get('foliage', False)
|
||||||
output[id]['birch'] = info.get('birch', False)
|
output[id]['birch'] = info.get('birch', False)
|
||||||
output[id]['spruce'] = info.get('spruce', False)
|
output[id]['spruce'] = info.get('spruce', False)
|
||||||
output[id]['water'] = info.get('water', False)
|
output[id]['water'] = info.get('water', False)
|
||||||
|
output[id]['wall_sign'] = info.get('wall_sign', False)
|
||||||
|
|
||||||
|
output[id]['sign_material'] = info.get('sign_material')
|
||||||
|
|
||||||
with open(sys.argv[3], 'w') as f:
|
with open(sys.argv[3], 'w') as f:
|
||||||
json.dump(output, f)
|
json.dump(output, f)
|
||||||
|
|
|
@ -14,11 +14,16 @@ with open(sys.argv[1]) as f:
|
||||||
output = {}
|
output = {}
|
||||||
|
|
||||||
with open(sys.argv[2], 'w') as f:
|
with open(sys.argv[2], 'w') as f:
|
||||||
|
print('//! Block type information', file=f);
|
||||||
|
print('//!', file=f);
|
||||||
|
print('//! This file is generated using resource/generate.py, do not edit', file=f);
|
||||||
|
print('', file=f)
|
||||||
print('use enumflags2::make_bitflags;', file=f);
|
print('use enumflags2::make_bitflags;', file=f);
|
||||||
print('', file=f)
|
print('', file=f)
|
||||||
print('use super::*;', file=f)
|
print('use super::*;', file=f)
|
||||||
print('', file=f)
|
print('', file=f)
|
||||||
print('pub const BLOCK_TYPES: &[(&str, BlockType)] = &[', file=f)
|
print('/// List if known block types and their properties', file=f);
|
||||||
|
print('pub const BLOCK_TYPES: &[(&str, ConstBlockType)] = &[', file=f)
|
||||||
|
|
||||||
for name, info in colors.items():
|
for name, info in colors.items():
|
||||||
flags = []
|
flags = []
|
||||||
|
@ -34,13 +39,22 @@ with open(sys.argv[2], 'w') as f:
|
||||||
flags.append('Spruce')
|
flags.append('Spruce')
|
||||||
if info['water']:
|
if info['water']:
|
||||||
flags.append('Water')
|
flags.append('Water')
|
||||||
|
if info['wall_sign']:
|
||||||
|
flags.append('WallSign')
|
||||||
flags = 'make_bitflags!(BlockFlag::{' + '|'.join(flags) + '})'
|
flags = 'make_bitflags!(BlockFlag::{' + '|'.join(flags) + '})'
|
||||||
|
|
||||||
print('\t("%s", BlockType { flags: %s, color: Color([%u, %u, %u]) }),' % (
|
sign_material = 'None'
|
||||||
name,
|
if info['sign_material']:
|
||||||
|
sign_material = 'Some("%s")' % info['sign_material']
|
||||||
|
|
||||||
|
print('\t("%s", ConstBlockType { ' % name, file=f)
|
||||||
|
print('\t\tblock_color: BlockColor { flags: %s, color: Color([%u, %u, %u]) },' % (
|
||||||
flags,
|
flags,
|
||||||
info['color']['r'],
|
info['color']['r'],
|
||||||
info['color']['g'],
|
info['color']['g'],
|
||||||
info['color']['b'],
|
info['color']['b'],
|
||||||
), file=f)
|
), file=f)
|
||||||
|
print('\t\tsign_material: %s,' % sign_material, file=f)
|
||||||
|
print('}),', file=f)
|
||||||
|
|
||||||
print('];', file=f)
|
print('];', file=f)
|
||||||
|
|
91
resource/sign_textures.py
Executable file
|
@ -0,0 +1,91 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
MATERIALS = [
|
||||||
|
'acacia',
|
||||||
|
'bamboo',
|
||||||
|
'birch',
|
||||||
|
'cherry',
|
||||||
|
'crimson',
|
||||||
|
'dark_oak',
|
||||||
|
'jungle',
|
||||||
|
'mangrove',
|
||||||
|
'oak',
|
||||||
|
'pale_oak',
|
||||||
|
'spruce',
|
||||||
|
'warped',
|
||||||
|
]
|
||||||
|
|
||||||
|
in_dir = sys.argv[1]
|
||||||
|
out_dir = sys.argv[2]
|
||||||
|
|
||||||
|
def sign_bg_image(material):
|
||||||
|
in_path = f'{in_dir}/assets/minecraft/textures/entity/signs/{material}.png'
|
||||||
|
out_path = f'{out_dir}/bg/{material}_sign.png'
|
||||||
|
out_path_wall = f'{out_dir}/bg/{material}_wall_sign.png'
|
||||||
|
|
||||||
|
in_image = Image.open(in_path)
|
||||||
|
|
||||||
|
out_image = Image.new('RGBA', (24, 26))
|
||||||
|
out_image.paste(in_image.crop((2, 2, 26, 14)), (0, 0))
|
||||||
|
out_image.paste(in_image.crop((2, 16, 4, 30)), (11, 12))
|
||||||
|
out_image.save(out_path)
|
||||||
|
|
||||||
|
out_image = Image.new('RGBA', (24, 12))
|
||||||
|
out_image.paste(in_image.crop((2, 2, 26, 14)), (0, 0))
|
||||||
|
out_image.save(out_path_wall)
|
||||||
|
|
||||||
|
def hanging_sign_bg_image(material):
|
||||||
|
in_path = f'{in_dir}/assets/minecraft/textures/gui/hanging_signs/{material}.png'
|
||||||
|
out_path = f'{out_dir}/bg/{material}_hanging_sign.png'
|
||||||
|
out_path_wall = f'{out_dir}/bg/{material}_hanging_wall_sign.png'
|
||||||
|
|
||||||
|
in_image = Image.open(in_path)
|
||||||
|
|
||||||
|
out_image = Image.new('RGBA', (16, 14))
|
||||||
|
out_image.paste(in_image.crop((0, 2, 16, 16)), (0, 0))
|
||||||
|
out_image.save(out_path)
|
||||||
|
|
||||||
|
shutil.copyfile(in_path, out_path_wall)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_icon_image(material):
|
||||||
|
in_path = f'{in_dir}/assets/minecraft/textures/item/{material}_sign.png'
|
||||||
|
out_path = f'{out_dir}/icon/{material}_sign.png'
|
||||||
|
out_path_wall = f'{out_dir}/icon/{material}_wall_sign.png'
|
||||||
|
|
||||||
|
in_image = Image.open(in_path)
|
||||||
|
|
||||||
|
out_image = Image.new('RGBA', (13, 14))
|
||||||
|
out_image.paste(in_image.crop((2, 2, 15, 16)), (0, 0))
|
||||||
|
out_image.save(out_path)
|
||||||
|
|
||||||
|
out_image = Image.new('RGBA', (13, 9))
|
||||||
|
out_image.paste(in_image.crop((2, 2, 15, 11)), (0, 0))
|
||||||
|
out_image.save(out_path_wall)
|
||||||
|
|
||||||
|
|
||||||
|
def hanging_sign_icon_image(material):
|
||||||
|
in_path = f'{in_dir}/assets/minecraft/textures/item/{material}_hanging_sign.png'
|
||||||
|
out_path = f'{out_dir}/icon/{material}_hanging_sign.png'
|
||||||
|
out_path_wall = f'{out_dir}/icon/{material}_hanging_wall_sign.png'
|
||||||
|
|
||||||
|
in_image = Image.open(in_path)
|
||||||
|
|
||||||
|
out_image = Image.new('RGBA', (14, 12))
|
||||||
|
out_image.paste(in_image.crop((1, 3, 15, 15)), (0, 0))
|
||||||
|
out_image.save(out_path)
|
||||||
|
|
||||||
|
out_image = Image.new('RGBA', (14, 14))
|
||||||
|
out_image.paste(in_image.crop((1, 1, 15, 15)), (0, 0))
|
||||||
|
out_image.save(out_path_wall)
|
||||||
|
|
||||||
|
for material in MATERIALS:
|
||||||
|
sign_bg_image(material)
|
||||||
|
hanging_sign_bg_image(material)
|
||||||
|
sign_icon_image(material)
|
||||||
|
hanging_sign_icon_image(material)
|
|
@ -3,24 +3,52 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, BTreeSet},
|
collections::{BTreeMap, BTreeSet},
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
|
hash::Hash,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use indexmap::IndexSet;
|
use anyhow::{Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use bincode::{Decode, Encode};
|
||||||
|
use clap::ValueEnum;
|
||||||
|
use regex::{Regex, RegexSet};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{io::fs::FileMetaVersion, resource::Biome, types::*, world::layer};
|
use crate::{
|
||||||
|
io::fs::FileMetaVersion,
|
||||||
|
resource::Biome,
|
||||||
|
types::*,
|
||||||
|
world::{block_entity::BlockEntity, layer},
|
||||||
|
};
|
||||||
|
|
||||||
/// Increase to force regeneration of all output files
|
// Increase to force regeneration of all output files
|
||||||
|
|
||||||
/// MinedMap processed region data version number
|
/// MinedMap processed region data version number
|
||||||
pub const REGION_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0);
|
///
|
||||||
|
/// Increase when the generation of processed regions from region data changes
|
||||||
|
/// (usually because of updated resource data)
|
||||||
|
pub const REGION_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(7);
|
||||||
|
|
||||||
/// MinedMap map tile data version number
|
/// MinedMap map tile data version number
|
||||||
|
///
|
||||||
|
/// Increase when the generation of map tiles from processed regions changes
|
||||||
|
/// (because of code changes in tile generation)
|
||||||
pub const MAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0);
|
pub const MAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0);
|
||||||
|
|
||||||
/// MinedMap lightmap data version number
|
/// MinedMap lightmap data version number
|
||||||
pub const LIGHTMAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0);
|
///
|
||||||
|
/// Increase when the generation of lightmap tiles from region data changes
|
||||||
|
/// (usually because of updated resource data)
|
||||||
|
pub const LIGHTMAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(5);
|
||||||
|
|
||||||
|
/// MinedMap mipmap data version number
|
||||||
|
///
|
||||||
|
/// Increase when the mipmap generation changes (this should not happen)
|
||||||
|
pub const MIPMAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0);
|
||||||
|
|
||||||
|
/// MinedMap processed entity data version number
|
||||||
|
///
|
||||||
|
/// Increase when entity collection changes bacause of code changes.
|
||||||
|
pub const ENTITIES_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(3);
|
||||||
|
|
||||||
/// Coordinate pair of a generated tile
|
/// Coordinate pair of a generated tile
|
||||||
///
|
///
|
||||||
|
@ -59,7 +87,7 @@ impl TileCoordMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data structure for storing chunk data between processing and rendering steps
|
/// Data structure for storing chunk data between processing and rendering steps
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Encode, Decode)]
|
||||||
pub struct ProcessedChunk {
|
pub struct ProcessedChunk {
|
||||||
/// Block type data
|
/// Block type data
|
||||||
pub blocks: Box<layer::BlockArray>,
|
pub blocks: Box<layer::BlockArray>,
|
||||||
|
@ -70,16 +98,23 @@ pub struct ProcessedChunk {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data structure for storing region data between processing and rendering steps
|
/// Data structure for storing region data between processing and rendering steps
|
||||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
#[derive(Debug, Default, Encode, Decode)]
|
||||||
pub struct ProcessedRegion {
|
pub struct ProcessedRegion {
|
||||||
/// List of biomes used in the region
|
/// List of biomes used in the region
|
||||||
///
|
///
|
||||||
/// Indexed by [ProcessedChunk] biome data
|
/// Indexed by [ProcessedChunk] biome data
|
||||||
pub biome_list: IndexSet<Biome>,
|
pub biome_list: Vec<Biome>,
|
||||||
/// Processed chunk data
|
/// Processed chunk data
|
||||||
pub chunks: ChunkArray<Option<Box<ProcessedChunk>>>,
|
pub chunks: ChunkArray<Option<Box<ProcessedChunk>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Data structure for storing entity data between processing and collection steps
|
||||||
|
#[derive(Debug, Default, Encode, Decode)]
|
||||||
|
pub struct ProcessedEntities {
|
||||||
|
/// List of block entities
|
||||||
|
pub block_entities: Vec<BlockEntity>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Derives a filename from region coordinates and a file extension
|
/// Derives a filename from region coordinates and a file extension
|
||||||
///
|
///
|
||||||
/// Can be used for input regions, processed data or rendered tiles
|
/// Can be used for input regions, processed data or rendered tiles
|
||||||
|
@ -97,43 +132,115 @@ pub enum TileKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Common configuration based on command line arguments
|
/// Common configuration based on command line arguments
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Number of threads for parallel processing
|
/// Number of threads for parallel processing
|
||||||
pub num_threads: usize,
|
pub num_threads: usize,
|
||||||
|
/// Number of threads for initial parallel processing
|
||||||
|
pub num_threads_initial: usize,
|
||||||
/// Path of input region directory
|
/// Path of input region directory
|
||||||
pub region_dir: PathBuf,
|
pub region_dir: PathBuf,
|
||||||
/// Path of input `level.dat` file
|
/// Path of input `level.dat` file
|
||||||
pub level_dat_path: PathBuf,
|
pub level_dat_path: PathBuf,
|
||||||
|
/// Path of input `level.dat_old` file
|
||||||
|
pub level_dat_old_path: PathBuf,
|
||||||
/// Base path for storage of rendered tile data
|
/// Base path for storage of rendered tile data
|
||||||
pub output_dir: PathBuf,
|
pub output_dir: PathBuf,
|
||||||
/// Path for storage of intermediate processed data files
|
/// Path for storage of intermediate processed data files
|
||||||
pub processed_dir: PathBuf,
|
pub processed_dir: PathBuf,
|
||||||
|
/// Path for storage of processed entity data files
|
||||||
|
pub entities_dir: PathBuf,
|
||||||
|
/// Path for storage of the final merged processed entity data file
|
||||||
|
pub entities_path_final: PathBuf,
|
||||||
/// Path of viewer metadata file
|
/// Path of viewer metadata file
|
||||||
pub metadata_path: PathBuf,
|
pub viewer_info_path: PathBuf,
|
||||||
|
/// Path of viewer entities file
|
||||||
|
pub viewer_entities_path: PathBuf,
|
||||||
|
/// Format of generated map tiles
|
||||||
|
pub image_format: ImageFormat,
|
||||||
|
/// Sign text filter patterns
|
||||||
|
pub sign_patterns: RegexSet,
|
||||||
|
/// Sign text transformation pattern
|
||||||
|
pub sign_transforms: Vec<(Regex, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
/// Crates a new [Config] from [command line arguments](super::Args)
|
/// Crates a new [Config] from [command line arguments](super::Args)
|
||||||
pub fn new(args: &super::Args) -> Self {
|
pub fn new(args: &super::Args) -> Result<Self> {
|
||||||
let num_threads = match args.jobs {
|
let num_threads = match args.jobs {
|
||||||
Some(0) => num_cpus::get(),
|
Some(0) => num_cpus::get(),
|
||||||
Some(threads) => threads,
|
Some(threads) => threads,
|
||||||
None => 1,
|
None => 1,
|
||||||
};
|
};
|
||||||
|
let num_threads_initial = args.jobs_initial.unwrap_or(num_threads);
|
||||||
|
|
||||||
let region_dir = [&args.input_dir, Path::new("region")].iter().collect();
|
let region_dir = [&args.input_dir, Path::new("region")].iter().collect();
|
||||||
let level_dat_path = [&args.input_dir, Path::new("level.dat")].iter().collect();
|
let level_dat_path = [&args.input_dir, Path::new("level.dat")].iter().collect();
|
||||||
let processed_dir = [&args.output_dir, Path::new("processed")].iter().collect();
|
let level_dat_old_path = [&args.input_dir, Path::new("level.dat_old")]
|
||||||
let metadata_path = [&args.output_dir, Path::new("info.json")].iter().collect();
|
.iter()
|
||||||
|
.collect();
|
||||||
|
let processed_dir: PathBuf = [&args.output_dir, Path::new("processed")].iter().collect();
|
||||||
|
let entities_dir: PathBuf = [&processed_dir, Path::new("entities")].iter().collect();
|
||||||
|
let entities_path_final = [&entities_dir, Path::new("entities.bin")].iter().collect();
|
||||||
|
let viewer_info_path = [&args.output_dir, Path::new("info.json")].iter().collect();
|
||||||
|
let viewer_entities_path = [&args.output_dir, Path::new("entities.json")]
|
||||||
|
.iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
Config {
|
let sign_patterns = Self::sign_patterns(args).context("Failed to parse sign patterns")?;
|
||||||
|
let sign_transforms =
|
||||||
|
Self::sign_transforms(args).context("Failed to parse sign transforms")?;
|
||||||
|
|
||||||
|
Ok(Config {
|
||||||
num_threads,
|
num_threads,
|
||||||
|
num_threads_initial,
|
||||||
region_dir,
|
region_dir,
|
||||||
level_dat_path,
|
level_dat_path,
|
||||||
|
level_dat_old_path,
|
||||||
output_dir: args.output_dir.clone(),
|
output_dir: args.output_dir.clone(),
|
||||||
processed_dir,
|
processed_dir,
|
||||||
metadata_path,
|
entities_dir,
|
||||||
|
entities_path_final,
|
||||||
|
viewer_info_path,
|
||||||
|
viewer_entities_path,
|
||||||
|
image_format: args.image_format,
|
||||||
|
sign_patterns,
|
||||||
|
sign_transforms,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses the sign prefixes and sign filters into a [RegexSet]
|
||||||
|
fn sign_patterns(args: &super::Args) -> Result<RegexSet> {
|
||||||
|
let prefix_patterns: Vec<_> = args
|
||||||
|
.sign_prefix
|
||||||
|
.iter()
|
||||||
|
.map(|prefix| format!("^{}", regex::escape(prefix)))
|
||||||
|
.collect();
|
||||||
|
Ok(RegexSet::new(
|
||||||
|
prefix_patterns.iter().chain(args.sign_filter.iter()),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the sign transform argument into a vector of [Regex] and
|
||||||
|
/// corresponding replacement strings
|
||||||
|
fn sign_transforms(args: &super::Args) -> Result<Vec<(Regex, String)>> {
|
||||||
|
let splitter = Regex::new(r"^s/((?:[^\\/]|\\.)*)/((?:[^\\/]|\\.)*)/$").unwrap();
|
||||||
|
|
||||||
|
args.sign_transform
|
||||||
|
.iter()
|
||||||
|
.map(|t| Self::sign_transform(&splitter, t))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the sign transform argument into a [Regex] and its corresponding
|
||||||
|
/// replacement string
|
||||||
|
fn sign_transform(splitter: &Regex, transform: &str) -> Result<(Regex, String)> {
|
||||||
|
let captures = splitter
|
||||||
|
.captures(transform)
|
||||||
|
.with_context(|| format!("Invalid transform pattern '{}'", transform))?;
|
||||||
|
let regexp = Regex::new(&captures[1])?;
|
||||||
|
let replacement = captures[2].to_string();
|
||||||
|
Ok((regexp, replacement))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs the path to an input region file
|
/// Constructs the path to an input region file
|
||||||
|
@ -148,6 +255,20 @@ impl Config {
|
||||||
[&self.processed_dir, Path::new(&filename)].iter().collect()
|
[&self.processed_dir, Path::new(&filename)].iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constructs the base output path for processed entity data
|
||||||
|
pub fn entities_dir(&self, level: usize) -> PathBuf {
|
||||||
|
[&self.entities_dir, Path::new(&level.to_string())]
|
||||||
|
.iter()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs the path of a processed entity data file
|
||||||
|
pub fn entities_path(&self, level: usize, coords: TileCoords) -> PathBuf {
|
||||||
|
let filename = coord_filename(coords, "bin");
|
||||||
|
let dir = self.entities_dir(level);
|
||||||
|
[Path::new(&dir), Path::new(&filename)].iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Constructs the base output path for a [TileKind] and mipmap level
|
/// Constructs the base output path for a [TileKind] and mipmap level
|
||||||
pub fn tile_dir(&self, kind: TileKind, level: usize) -> PathBuf {
|
pub fn tile_dir(&self, kind: TileKind, level: usize) -> PathBuf {
|
||||||
let prefix = match kind {
|
let prefix = match kind {
|
||||||
|
@ -158,14 +279,39 @@ impl Config {
|
||||||
[&self.output_dir, Path::new(&dir)].iter().collect()
|
[&self.output_dir, Path::new(&dir)].iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the file extension for the configured image format
|
||||||
|
pub fn tile_extension(&self) -> &'static str {
|
||||||
|
match self.image_format {
|
||||||
|
ImageFormat::Png => "png",
|
||||||
|
ImageFormat::Webp => "webp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Returns the configurured image format for the image library
|
||||||
|
pub fn tile_image_format(&self) -> image::ImageFormat {
|
||||||
|
match self.image_format {
|
||||||
|
ImageFormat::Png => image::ImageFormat::Png,
|
||||||
|
ImageFormat::Webp => image::ImageFormat::WebP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Constructs the path of an output tile image
|
/// Constructs the path of an output tile image
|
||||||
pub fn tile_path(&self, kind: TileKind, level: usize, coords: TileCoords) -> PathBuf {
|
pub fn tile_path(&self, kind: TileKind, level: usize, coords: TileCoords) -> PathBuf {
|
||||||
let filename = coord_filename(coords, "png");
|
let filename = coord_filename(coords, self.tile_extension());
|
||||||
let dir = self.tile_dir(kind, level);
|
let dir = self.tile_dir(kind, level);
|
||||||
[Path::new(&dir), Path::new(&filename)].iter().collect()
|
[Path::new(&dir), Path::new(&filename)].iter().collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format of generated map tiles
|
||||||
|
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
|
||||||
|
pub enum ImageFormat {
|
||||||
|
/// Generate PNG images
|
||||||
|
#[default]
|
||||||
|
Png,
|
||||||
|
/// Generate WebP images
|
||||||
|
Webp,
|
||||||
|
}
|
||||||
|
|
||||||
/// Copies a chunk image into a region tile
|
/// Copies a chunk image into a region tile
|
||||||
pub fn overlay_chunk<I, J>(image: &mut I, chunk: &J, coords: ChunkCoords)
|
pub fn overlay_chunk<I, J>(image: &mut I, chunk: &J, coords: ChunkCoords)
|
||||||
where
|
where
|
||||||
|
|
122
src/core/entity_collector.rs
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
//! The [EntityCollector]
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use super::{common::*, tile_collector::TileCollector, tile_merger::TileMerger};
|
||||||
|
use crate::io::{fs, storage};
|
||||||
|
|
||||||
|
/// Generates mipmap tiles from full-resolution tile images
|
||||||
|
pub struct EntityCollector<'a> {
|
||||||
|
/// Common MinedMap configuration from command line
|
||||||
|
config: &'a Config,
|
||||||
|
/// List of populated tiles for base mipmap level (level 0)
|
||||||
|
regions: &'a [TileCoords],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TileMerger for EntityCollector<'_> {
|
||||||
|
fn file_meta_version(&self) -> fs::FileMetaVersion {
|
||||||
|
ENTITIES_FILE_META_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tile_path(&self, level: usize, coords: TileCoords) -> std::path::PathBuf {
|
||||||
|
self.config.entities_path(level, coords)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_tile(
|
||||||
|
&self,
|
||||||
|
file: &mut std::io::BufWriter<std::fs::File>,
|
||||||
|
sources: &[super::tile_merger::Source],
|
||||||
|
) -> Result<()> {
|
||||||
|
Self::merge_entity_lists(file, sources.iter().map(|source| &source.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TileCollector for EntityCollector<'_> {
|
||||||
|
type CollectOutput = ();
|
||||||
|
|
||||||
|
fn tiles(&self) -> &[TileCoords] {
|
||||||
|
self.regions
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare(&self, level: usize) -> Result<()> {
|
||||||
|
fs::create_dir_all(&self.config.entities_dir(level))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(
|
||||||
|
&self,
|
||||||
|
_level: usize,
|
||||||
|
_outputs: impl Iterator<Item = Self::CollectOutput>,
|
||||||
|
) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_one(
|
||||||
|
&self,
|
||||||
|
level: usize,
|
||||||
|
coords: TileCoords,
|
||||||
|
prev: &TileCoordMap,
|
||||||
|
) -> Result<Self::CollectOutput> {
|
||||||
|
self.merge_tiles(level, coords, prev)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EntityCollector<'a> {
|
||||||
|
/// Constructs a new EntityCollector
|
||||||
|
pub fn new(config: &'a Config, regions: &'a [TileCoords]) -> Self {
|
||||||
|
EntityCollector { config, regions }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merges multiple entity lists into one
|
||||||
|
fn merge_entity_lists<P: AsRef<Path>>(
|
||||||
|
file: &mut std::io::BufWriter<std::fs::File>,
|
||||||
|
sources: impl Iterator<Item = P>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut output = ProcessedEntities::default();
|
||||||
|
|
||||||
|
for source_path in sources {
|
||||||
|
let mut source: ProcessedEntities = match storage::read_file(source_path.as_ref()) {
|
||||||
|
Ok(source) => source,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"Failed to read entity data file {}: {:?}",
|
||||||
|
source_path.as_ref().display(),
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
output.block_entities.append(&mut source.block_entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
storage::write(file, &output).context("Failed to write entity data")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the mipmap generation
|
||||||
|
pub fn run(self) -> Result<()> {
|
||||||
|
info!("Collecting entity data...");
|
||||||
|
|
||||||
|
let tile_stack = self.collect_tiles()?;
|
||||||
|
|
||||||
|
// Final merge
|
||||||
|
let level = tile_stack.len() - 1;
|
||||||
|
let tile_map = &tile_stack[level];
|
||||||
|
let sources: Vec<_> = [(-1, -1), (-1, 0), (0, -1), (0, 0)]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(x, z)| TileCoords { x, z })
|
||||||
|
.filter(|&coords| tile_map.contains(coords))
|
||||||
|
.map(|coords| self.tile_path(level, coords))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
fs::create_with_tmpfile(&self.config.entities_path_final, |file| {
|
||||||
|
Self::merge_entity_lists(file, sources.iter())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
info!("Collected entity data.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,17 @@
|
||||||
//! The [MetadataWriter] and related types
|
//! The [MetadataWriter] and related types
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use regex::Regex;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{core::common::*, io::fs, world::de};
|
use crate::{
|
||||||
|
core::common::*,
|
||||||
|
io::{fs, storage},
|
||||||
|
world::{
|
||||||
|
block_entity::{self, BlockEntity, BlockEntityData},
|
||||||
|
de, sign,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/// Minimum and maximum X and Z tile coordinates for a mipmap level
|
/// Minimum and maximum X and Z tile coordinates for a mipmap level
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
@ -37,6 +45,13 @@ struct Spawn {
|
||||||
z: i32,
|
z: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keeps track of enabled MinedMap features
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct Features {
|
||||||
|
/// Sign layer
|
||||||
|
signs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Viewer metadata JSON data structure
|
/// Viewer metadata JSON data structure
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct Metadata<'t> {
|
struct Metadata<'t> {
|
||||||
|
@ -44,6 +59,17 @@ struct Metadata<'t> {
|
||||||
mipmaps: Vec<Mipmap<'t>>,
|
mipmaps: Vec<Mipmap<'t>>,
|
||||||
/// Initial spawn point for new players
|
/// Initial spawn point for new players
|
||||||
spawn: Spawn,
|
spawn: Spawn,
|
||||||
|
/// Enabled MinedMap features
|
||||||
|
features: Features,
|
||||||
|
/// Format of generated map tiles
|
||||||
|
tile_extension: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Viewer entity JSON data structure
|
||||||
|
#[derive(Debug, Serialize, Default)]
|
||||||
|
struct Entities {
|
||||||
|
/// List of signs
|
||||||
|
signs: Vec<BlockEntity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The MetadataWriter is used to generate the viewer metadata file
|
/// The MetadataWriter is used to generate the viewer metadata file
|
||||||
|
@ -98,7 +124,14 @@ impl<'a> MetadataWriter<'a> {
|
||||||
|
|
||||||
/// Reads and deserializes the `level.dat` of the Minecraft save data
|
/// Reads and deserializes the `level.dat` of the Minecraft save data
|
||||||
fn read_level_dat(&self) -> Result<de::LevelDat> {
|
fn read_level_dat(&self) -> Result<de::LevelDat> {
|
||||||
crate::nbt::data::from_file(&self.config.level_dat_path).context("Failed to read level.dat")
|
let res = crate::nbt::data::from_file(&self.config.level_dat_path);
|
||||||
|
if res.is_err() {
|
||||||
|
if let Ok(level_dat_old) = crate::nbt::data::from_file(&self.config.level_dat_old_path)
|
||||||
|
{
|
||||||
|
return Ok(level_dat_old);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.context("Failed to read level.dat")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates [Spawn] data from a [de::LevelDat]
|
/// Generates [Spawn] data from a [de::LevelDat]
|
||||||
|
@ -109,21 +142,93 @@ impl<'a> MetadataWriter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Filter signs according to the sign pattern configuration
|
||||||
|
fn sign_filter(&self, sign: &block_entity::Sign) -> bool {
|
||||||
|
let front_text = sign.front_text.to_string();
|
||||||
|
if self.config.sign_patterns.is_match(front_text.trim()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let back_text = sign.back_text.to_string();
|
||||||
|
if self.config.sign_patterns.is_match(back_text.trim()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies a single transform to a [sign::SignText]
|
||||||
|
///
|
||||||
|
/// The regular expression is applied for each line of the sign text
|
||||||
|
/// separately (actually for each element when JSON text is used)
|
||||||
|
fn sign_text_transform(sign_text: &mut sign::SignText, transform: &(Regex, String)) {
|
||||||
|
let (regexp, replacement) = transform;
|
||||||
|
|
||||||
|
for line in &mut sign_text.0 {
|
||||||
|
for text in &mut line.0 {
|
||||||
|
text.text = regexp.replace_all(&text.text, replacement).into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies the configured transforms to the text of a sign
|
||||||
|
fn sign_transform(&self, sign: &mut block_entity::Sign) {
|
||||||
|
for transform in &self.config.sign_transforms {
|
||||||
|
Self::sign_text_transform(&mut sign.front_text, transform);
|
||||||
|
Self::sign_text_transform(&mut sign.back_text, transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates [Entities] data from collected entity lists
|
||||||
|
fn entities(&self) -> Result<Entities> {
|
||||||
|
let data: ProcessedEntities = storage::read_file(&self.config.entities_path_final)
|
||||||
|
.context("Failed to read entity data file")?;
|
||||||
|
|
||||||
|
let ret = Entities {
|
||||||
|
signs: data
|
||||||
|
.block_entities
|
||||||
|
.into_iter()
|
||||||
|
.filter(|entity| match &entity.data {
|
||||||
|
BlockEntityData::Sign(sign) => self.sign_filter(sign),
|
||||||
|
})
|
||||||
|
.map(|mut entity| {
|
||||||
|
match &mut entity.data {
|
||||||
|
BlockEntityData::Sign(sign) => self.sign_transform(sign),
|
||||||
|
};
|
||||||
|
entity
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs the viewer metadata file generation
|
/// Runs the viewer metadata file generation
|
||||||
pub fn run(self) -> Result<()> {
|
pub fn run(self) -> Result<()> {
|
||||||
let level_dat = self.read_level_dat()?;
|
let level_dat = self.read_level_dat()?;
|
||||||
|
|
||||||
|
let features = Features {
|
||||||
|
signs: !self.config.sign_patterns.is_empty(),
|
||||||
|
};
|
||||||
|
|
||||||
let mut metadata = Metadata {
|
let mut metadata = Metadata {
|
||||||
mipmaps: Vec::new(),
|
mipmaps: Vec::new(),
|
||||||
spawn: Self::spawn(&level_dat),
|
spawn: Self::spawn(&level_dat),
|
||||||
|
features,
|
||||||
|
tile_extension: self.config.tile_extension(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for tile_map in self.tiles.iter() {
|
for tile_map in self.tiles.iter() {
|
||||||
metadata.mipmaps.push(Self::mipmap_entry(tile_map));
|
metadata.mipmaps.push(Self::mipmap_entry(tile_map));
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::create_with_tmpfile(&self.config.metadata_path, |file| {
|
fs::create_with_tmpfile(&self.config.viewer_info_path, |file| {
|
||||||
serde_json::to_writer(file, &metadata).context("Failed to write metadata")
|
serde_json::to_writer(file, &metadata).context("Failed to write info.json")
|
||||||
})
|
})?;
|
||||||
|
|
||||||
|
let entities = self.entities()?;
|
||||||
|
fs::create_with_tmpfile(&self.config.viewer_entities_path, |file| {
|
||||||
|
serde_json::to_writer(file, &entities).context("Failed to write entities.json")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
165
src/core/mod.rs
|
@ -1,33 +1,57 @@
|
||||||
//! Core functions of the MinedMap CLI
|
//! Core functions of the MinedMap CLI
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
mod entity_collector;
|
||||||
mod metadata_writer;
|
mod metadata_writer;
|
||||||
mod region_group;
|
mod region_group;
|
||||||
mod region_processor;
|
mod region_processor;
|
||||||
|
mod tile_collector;
|
||||||
|
mod tile_merger;
|
||||||
mod tile_mipmapper;
|
mod tile_mipmapper;
|
||||||
mod tile_renderer;
|
mod tile_renderer;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::{
|
||||||
|
path::PathBuf,
|
||||||
|
sync::mpsc::{self, Receiver},
|
||||||
|
thread,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use git_version::git_version;
|
use git_version::git_version;
|
||||||
|
|
||||||
use common::Config;
|
use common::{Config, ImageFormat};
|
||||||
use metadata_writer::MetadataWriter;
|
use metadata_writer::MetadataWriter;
|
||||||
|
use notify::{RecommendedWatcher, RecursiveMode, Watcher as _};
|
||||||
|
use rayon::ThreadPool;
|
||||||
use region_processor::RegionProcessor;
|
use region_processor::RegionProcessor;
|
||||||
use tile_mipmapper::TileMipmapper;
|
use tile_mipmapper::TileMipmapper;
|
||||||
use tile_renderer::TileRenderer;
|
use tile_renderer::TileRenderer;
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
/// MinedMap version number
|
use self::entity_collector::EntityCollector;
|
||||||
const VERSION: &str = git_version!(
|
|
||||||
|
/// Returns the MinedMap version number
|
||||||
|
fn version() -> &'static str {
|
||||||
|
option_env!("MINEDMAP_VERSION").unwrap_or(
|
||||||
|
git_version!(
|
||||||
args = ["--abbrev=7", "--match=v*", "--dirty=-modified"],
|
args = ["--abbrev=7", "--match=v*", "--dirty=-modified"],
|
||||||
cargo_prefix = "v",
|
cargo_prefix = "v",
|
||||||
);
|
)
|
||||||
|
.strip_prefix("v")
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Command line arguments for minedmap CLI
|
/// Command line arguments for minedmap CLI
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(about, version = VERSION.strip_prefix("v").unwrap())]
|
#[command(
|
||||||
|
about,
|
||||||
|
version = version(),
|
||||||
|
max_term_width = 100,
|
||||||
|
)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
/// Number of parallel threads to use for processing
|
/// Number of parallel threads to use for processing
|
||||||
///
|
///
|
||||||
|
@ -35,27 +59,124 @@ pub struct Args {
|
||||||
/// use one thread per logical CPU core.
|
/// use one thread per logical CPU core.
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub jobs: Option<usize>,
|
pub jobs: Option<usize>,
|
||||||
|
/// Number of parallel threads to use for initial processing
|
||||||
|
///
|
||||||
|
/// Passing this option only makes sense with --watch. The first run after
|
||||||
|
/// starting MinedMap will use as many parallel jobs as configured using
|
||||||
|
/// --job-initial, while subsequent regenerations of tiles will use the
|
||||||
|
/// the number configured using --jobs.
|
||||||
|
///
|
||||||
|
/// If not given, the value from the --jobs option is used.
|
||||||
|
#[arg(long)]
|
||||||
|
pub jobs_initial: Option<usize>,
|
||||||
/// Enable verbose messages
|
/// Enable verbose messages
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
|
/// Watch for file changes and regenerate tiles automatically instead of
|
||||||
|
/// exiting after generation
|
||||||
|
#[arg(long)]
|
||||||
|
pub watch: bool,
|
||||||
|
/// Minimum delay between map generation cycles in watch mode
|
||||||
|
#[arg(long, value_parser = humantime::parse_duration, default_value = "30s")]
|
||||||
|
pub watch_delay: Duration,
|
||||||
|
/// Format of generated map tiles
|
||||||
|
#[arg(long, value_enum, default_value_t)]
|
||||||
|
pub image_format: ImageFormat,
|
||||||
|
/// Prefix for text of signs to show on the map
|
||||||
|
#[arg(long)]
|
||||||
|
pub sign_prefix: Vec<String>,
|
||||||
|
/// Regular expression for text of signs to show on the map
|
||||||
|
///
|
||||||
|
/// --sign-prefix and --sign-filter allow to filter for signs to display;
|
||||||
|
/// by default, none are visible. The options may be passed multiple times,
|
||||||
|
/// and a sign will be visible if it matches any pattern.
|
||||||
|
///
|
||||||
|
/// To make all signs visible, pass an empty string to either option.
|
||||||
|
#[arg(long)]
|
||||||
|
pub sign_filter: Vec<String>,
|
||||||
|
/// Regular expression replacement pattern for sign texts
|
||||||
|
///
|
||||||
|
/// Accepts patterns of the form 's/regexp/replacement/'. Transforms
|
||||||
|
/// are applied to each line of sign texts separately.
|
||||||
|
#[arg(long)]
|
||||||
|
pub sign_transform: Vec<String>,
|
||||||
/// Minecraft save directory
|
/// Minecraft save directory
|
||||||
pub input_dir: PathBuf,
|
pub input_dir: PathBuf,
|
||||||
/// MinedMap data directory
|
/// MinedMap data directory
|
||||||
pub output_dir: PathBuf,
|
pub output_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configures the Rayon thread pool for parallel processing
|
/// Configures a Rayon thread pool for parallel processing
|
||||||
fn setup_threads(num_threads: usize) -> Result<()> {
|
fn setup_threads(num_threads: usize) -> Result<ThreadPool> {
|
||||||
rayon::ThreadPoolBuilder::new()
|
rayon::ThreadPoolBuilder::new()
|
||||||
.num_threads(num_threads)
|
.num_threads(num_threads)
|
||||||
.build_global()
|
.build()
|
||||||
.context("Failed to configure thread pool")
|
.context("Failed to configure thread pool")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs all MinedMap generation steps, updating all tiles as needed
|
||||||
|
fn generate(config: &Config, rt: &Runtime) -> Result<()> {
|
||||||
|
let regions = RegionProcessor::new(config).run()?;
|
||||||
|
TileRenderer::new(config, rt, ®ions).run()?;
|
||||||
|
let tiles = TileMipmapper::new(config, ®ions).run()?;
|
||||||
|
EntityCollector::new(config, ®ions).run()?;
|
||||||
|
MetadataWriter::new(config, &tiles).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a file watcher for the
|
||||||
|
fn create_watcher(args: &Args) -> Result<(RecommendedWatcher, Receiver<()>)> {
|
||||||
|
let (tx, rx) = mpsc::sync_channel::<()>(1);
|
||||||
|
let mut watcher = notify::recommended_watcher(move |res| {
|
||||||
|
// Ignore errors - we already have a watch trigger queued if try_send() fails
|
||||||
|
let event: notify::Event = match res {
|
||||||
|
Ok(event) => event,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Watch error: {err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let notify::EventKind::Modify(modify_kind) = event.kind else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !matches!(
|
||||||
|
modify_kind,
|
||||||
|
notify::event::ModifyKind::Data(_)
|
||||||
|
| notify::event::ModifyKind::Name(notify::event::RenameMode::To)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !event
|
||||||
|
.paths
|
||||||
|
.iter()
|
||||||
|
.any(|path| path.ends_with("level.dat") || path.extension() == Some("mcu".as_ref()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let _ = tx.try_send(());
|
||||||
|
})?;
|
||||||
|
watcher.watch(&args.input_dir, RecursiveMode::Recursive)?;
|
||||||
|
Ok((watcher, rx))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Watches the data directory for changes, returning when a change has happened
|
||||||
|
fn wait_watcher(args: &Args, watch_channel: &Receiver<()>) -> Result<()> {
|
||||||
|
info!("Watching for changes...");
|
||||||
|
let () = watch_channel
|
||||||
|
.recv()
|
||||||
|
.context("Failed to read watch event channel")?;
|
||||||
|
info!("Change detected.");
|
||||||
|
|
||||||
|
thread::sleep(args.watch_delay);
|
||||||
|
|
||||||
|
let _ = watch_channel.try_recv();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// MinedMap CLI main function
|
/// MinedMap CLI main function
|
||||||
pub fn cli() -> Result<()> {
|
pub fn cli() -> Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let config = Config::new(&args);
|
let config = Config::new(&args)?;
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_max_level(if args.verbose {
|
.with_max_level(if args.verbose {
|
||||||
|
@ -66,16 +187,28 @@ pub fn cli() -> Result<()> {
|
||||||
.with_target(false)
|
.with_target(false)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
setup_threads(config.num_threads)?;
|
let mut pool = setup_threads(config.num_threads_initial)?;
|
||||||
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let regions = RegionProcessor::new(&config).run()?;
|
let watch = args.watch.then(|| create_watcher(&args)).transpose()?;
|
||||||
TileRenderer::new(&config, &rt, ®ions).run()?;
|
|
||||||
let tiles = TileMipmapper::new(&config, ®ions).run()?;
|
|
||||||
MetadataWriter::new(&config, &tiles).run()?;
|
|
||||||
|
|
||||||
Ok(())
|
pool.install(|| generate(&config, &rt))?;
|
||||||
|
|
||||||
|
let Some((_watcher, watch_channel)) = watch else {
|
||||||
|
// watch mode disabled
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if config.num_threads != config.num_threads_initial {
|
||||||
|
pool = setup_threads(config.num_threads)?;
|
||||||
|
}
|
||||||
|
pool.install(move || {
|
||||||
|
loop {
|
||||||
|
wait_watcher(&args, &watch_channel)?;
|
||||||
|
generate(&config, &rt)?;
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
use std::{ffi::OsStr, path::PathBuf, sync::mpsc, time::SystemTime};
|
use std::{ffi::OsStr, path::PathBuf, sync::mpsc, time::SystemTime};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use enum_map::{Enum, EnumMap};
|
||||||
|
use indexmap::IndexSet;
|
||||||
|
use minedmap_resource::Biome;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
@ -28,10 +31,12 @@ fn parse_region_filename(file_name: &OsStr) -> Option<TileCoords> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [RegionProcessor::process_region] return values
|
/// [RegionProcessor::process_region] return values
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Enum)]
|
||||||
enum RegionProcessorStatus {
|
enum RegionProcessorStatus {
|
||||||
/// Region was processed
|
/// Region was processed
|
||||||
Ok,
|
Ok,
|
||||||
|
/// Region was processed, unknown blocks or biomes were encountered
|
||||||
|
OkWithUnknown,
|
||||||
/// Region was unchanged and skipped
|
/// Region was unchanged and skipped
|
||||||
Skipped,
|
Skipped,
|
||||||
/// Reading the region failed, previous processed data is reused
|
/// Reading the region failed, previous processed data is reused
|
||||||
|
@ -40,6 +45,37 @@ enum RegionProcessorStatus {
|
||||||
ErrorMissing,
|
ErrorMissing,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Data of a region being processed by a [SingleRegionProcessor]
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct SingleRegionData {
|
||||||
|
/// [IndexSet] of biomes used by the processed region
|
||||||
|
biome_list: IndexSet<Biome>,
|
||||||
|
/// Processed region chunk intermediate data
|
||||||
|
chunks: ChunkArray<Option<Box<ProcessedChunk>>>,
|
||||||
|
/// Lightmap intermediate data
|
||||||
|
lightmap: image::GrayAlphaImage,
|
||||||
|
/// Processed entity intermediate data
|
||||||
|
entities: ProcessedEntities,
|
||||||
|
/// True if any unknown block or biome types were encountered during processing
|
||||||
|
has_unknown: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SingleRegionData {
|
||||||
|
fn default() -> Self {
|
||||||
|
/// Width/height of the region data
|
||||||
|
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
|
||||||
|
|
||||||
|
let lightmap = image::GrayAlphaImage::new(N, N);
|
||||||
|
Self {
|
||||||
|
biome_list: Default::default(),
|
||||||
|
chunks: Default::default(),
|
||||||
|
lightmap,
|
||||||
|
entities: Default::default(),
|
||||||
|
has_unknown: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles processing for a single region
|
/// Handles processing for a single region
|
||||||
struct SingleRegionProcessor<'a> {
|
struct SingleRegionProcessor<'a> {
|
||||||
/// Registry of known block types
|
/// Registry of known block types
|
||||||
|
@ -54,41 +90,44 @@ struct SingleRegionProcessor<'a> {
|
||||||
output_path: PathBuf,
|
output_path: PathBuf,
|
||||||
/// Lightmap output filename
|
/// Lightmap output filename
|
||||||
lightmap_path: PathBuf,
|
lightmap_path: PathBuf,
|
||||||
|
/// Processed entity output filename
|
||||||
|
entities_path: PathBuf,
|
||||||
/// Timestamp of last modification of input file
|
/// Timestamp of last modification of input file
|
||||||
input_timestamp: SystemTime,
|
input_timestamp: SystemTime,
|
||||||
/// Timestamp of last modification of processed region output file (if valid)
|
/// Timestamp of last modification of processed region output file (if valid)
|
||||||
output_timestamp: Option<SystemTime>,
|
output_timestamp: Option<SystemTime>,
|
||||||
/// Timestamp of last modification of lightmap output file (if valid)
|
/// Timestamp of last modification of lightmap output file (if valid)
|
||||||
lightmap_timestamp: Option<SystemTime>,
|
lightmap_timestamp: Option<SystemTime>,
|
||||||
|
/// Timestamp of last modification of entity list output file (if valid)
|
||||||
|
entities_timestamp: Option<SystemTime>,
|
||||||
/// True if processed region output file needs to be updated
|
/// True if processed region output file needs to be updated
|
||||||
output_needed: bool,
|
output_needed: bool,
|
||||||
/// True if lightmap output file needs to be updated
|
/// True if lightmap output file needs to be updated
|
||||||
lightmap_needed: bool,
|
lightmap_needed: bool,
|
||||||
/// Processed region intermediate data
|
/// True if entity output file needs to be updated
|
||||||
processed_region: ProcessedRegion,
|
entities_needed: bool,
|
||||||
/// Lightmap intermediate data
|
/// Format of generated map tiles
|
||||||
lightmap: image::GrayAlphaImage,
|
image_format: image::ImageFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SingleRegionProcessor<'a> {
|
impl<'a> SingleRegionProcessor<'a> {
|
||||||
/// Initializes a [SingleRegionProcessor]
|
/// Initializes a [SingleRegionProcessor]
|
||||||
fn new(processor: &'a RegionProcessor<'a>, coords: TileCoords) -> Result<Self> {
|
fn new(processor: &'a RegionProcessor<'a>, coords: TileCoords) -> Result<Self> {
|
||||||
/// Width/height of the region data
|
|
||||||
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
|
|
||||||
|
|
||||||
let input_path = processor.config.region_path(coords);
|
let input_path = processor.config.region_path(coords);
|
||||||
let input_timestamp = fs::modified_timestamp(&input_path)?;
|
let input_timestamp = fs::modified_timestamp(&input_path)?;
|
||||||
|
|
||||||
let output_path = processor.config.processed_path(coords);
|
let output_path = processor.config.processed_path(coords);
|
||||||
let output_timestamp = fs::read_timestamp(&output_path, REGION_FILE_META_VERSION);
|
let output_timestamp = fs::read_timestamp(&output_path, REGION_FILE_META_VERSION);
|
||||||
|
|
||||||
let lightmap_path = processor.config.tile_path(TileKind::Lightmap, 0, coords);
|
let lightmap_path = processor.config.tile_path(TileKind::Lightmap, 0, coords);
|
||||||
let lightmap_timestamp = fs::read_timestamp(&lightmap_path, LIGHTMAP_FILE_META_VERSION);
|
let lightmap_timestamp = fs::read_timestamp(&lightmap_path, LIGHTMAP_FILE_META_VERSION);
|
||||||
|
|
||||||
|
let entities_path = processor.config.entities_path(0, coords);
|
||||||
|
let entities_timestamp = fs::read_timestamp(&entities_path, ENTITIES_FILE_META_VERSION);
|
||||||
|
|
||||||
let output_needed = Some(input_timestamp) > output_timestamp;
|
let output_needed = Some(input_timestamp) > output_timestamp;
|
||||||
let lightmap_needed = Some(input_timestamp) > lightmap_timestamp;
|
let lightmap_needed = Some(input_timestamp) > lightmap_timestamp;
|
||||||
|
let entities_needed = Some(input_timestamp) > entities_timestamp;
|
||||||
let processed_region = ProcessedRegion::default();
|
|
||||||
let lightmap = image::GrayAlphaImage::new(N, N);
|
|
||||||
|
|
||||||
Ok(SingleRegionProcessor {
|
Ok(SingleRegionProcessor {
|
||||||
block_types: &processor.block_types,
|
block_types: &processor.block_types,
|
||||||
|
@ -97,13 +136,15 @@ impl<'a> SingleRegionProcessor<'a> {
|
||||||
input_path,
|
input_path,
|
||||||
output_path,
|
output_path,
|
||||||
lightmap_path,
|
lightmap_path,
|
||||||
|
entities_path,
|
||||||
input_timestamp,
|
input_timestamp,
|
||||||
output_timestamp,
|
output_timestamp,
|
||||||
lightmap_timestamp,
|
lightmap_timestamp,
|
||||||
|
entities_timestamp,
|
||||||
output_needed,
|
output_needed,
|
||||||
lightmap_needed,
|
lightmap_needed,
|
||||||
processed_region,
|
entities_needed,
|
||||||
lightmap,
|
image_format: processor.config.tile_image_format(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,14 +168,14 @@ impl<'a> SingleRegionProcessor<'a> {
|
||||||
/// Saves processed region data
|
/// Saves processed region data
|
||||||
///
|
///
|
||||||
/// The timestamp is the time of the last modification of the input region data.
|
/// The timestamp is the time of the last modification of the input region data.
|
||||||
fn save_region(&self) -> Result<()> {
|
fn save_region(&self, processed_region: &ProcessedRegion) -> Result<()> {
|
||||||
if !self.output_needed {
|
if !self.output_needed {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
storage::write(
|
storage::write_file(
|
||||||
&self.output_path,
|
&self.output_path,
|
||||||
&self.processed_region,
|
processed_region,
|
||||||
REGION_FILE_META_VERSION,
|
REGION_FILE_META_VERSION,
|
||||||
self.input_timestamp,
|
self.input_timestamp,
|
||||||
)
|
)
|
||||||
|
@ -143,7 +184,7 @@ impl<'a> SingleRegionProcessor<'a> {
|
||||||
/// Saves a lightmap tile
|
/// Saves a lightmap tile
|
||||||
///
|
///
|
||||||
/// The timestamp is the time of the last modification of the input region data.
|
/// The timestamp is the time of the last modification of the input region data.
|
||||||
fn save_lightmap(&self) -> Result<()> {
|
fn save_lightmap(&self, lightmap: &image::GrayAlphaImage) -> Result<()> {
|
||||||
if !self.lightmap_needed {
|
if !self.lightmap_needed {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -153,30 +194,54 @@ impl<'a> SingleRegionProcessor<'a> {
|
||||||
LIGHTMAP_FILE_META_VERSION,
|
LIGHTMAP_FILE_META_VERSION,
|
||||||
self.input_timestamp,
|
self.input_timestamp,
|
||||||
|file| {
|
|file| {
|
||||||
self.lightmap
|
lightmap
|
||||||
.write_to(file, image::ImageFormat::Png)
|
.write_to(file, self.image_format)
|
||||||
.context("Failed to save image")
|
.context("Failed to save image")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Saves processed entity data
|
||||||
|
///
|
||||||
|
/// The timestamp is the time of the last modification of the input region data.
|
||||||
|
fn save_entities(&self, entities: &mut ProcessedEntities) -> Result<()> {
|
||||||
|
if !self.entities_needed {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
entities.block_entities.sort_unstable();
|
||||||
|
|
||||||
|
storage::write_file(
|
||||||
|
&self.entities_path,
|
||||||
|
entities,
|
||||||
|
ENTITIES_FILE_META_VERSION,
|
||||||
|
self.input_timestamp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Processes a single chunk
|
/// Processes a single chunk
|
||||||
fn process_chunk(&mut self, chunk_coords: ChunkCoords, data: world::de::Chunk) -> Result<()> {
|
fn process_chunk(
|
||||||
let chunk = world::chunk::Chunk::new(&data, self.block_types, self.biome_types)
|
&self,
|
||||||
|
data: &mut SingleRegionData,
|
||||||
|
chunk_coords: ChunkCoords,
|
||||||
|
chunk_data: world::de::Chunk,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (chunk, has_unknown) =
|
||||||
|
world::chunk::Chunk::new(&chunk_data, self.block_types, self.biome_types)
|
||||||
.with_context(|| format!("Failed to decode chunk {:?}", chunk_coords))?;
|
.with_context(|| format!("Failed to decode chunk {:?}", chunk_coords))?;
|
||||||
let Some(layer::LayerData {
|
data.has_unknown |= has_unknown;
|
||||||
|
|
||||||
|
if self.output_needed || self.lightmap_needed {
|
||||||
|
if let Some(layer::LayerData {
|
||||||
blocks,
|
blocks,
|
||||||
biomes,
|
biomes,
|
||||||
block_light,
|
block_light,
|
||||||
depths,
|
depths,
|
||||||
}) = world::layer::top_layer(&mut self.processed_region.biome_list, &chunk)
|
}) = world::layer::top_layer(&mut data.biome_list, &chunk)
|
||||||
.with_context(|| format!("Failed to process chunk {:?}", chunk_coords))?
|
.with_context(|| format!("Failed to process chunk {:?}", chunk_coords))?
|
||||||
else {
|
{
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.output_needed {
|
if self.output_needed {
|
||||||
self.processed_region.chunks[chunk_coords] = Some(Box::new(ProcessedChunk {
|
data.chunks[chunk_coords] = Some(Box::new(ProcessedChunk {
|
||||||
blocks,
|
blocks,
|
||||||
biomes,
|
biomes,
|
||||||
depths,
|
depths,
|
||||||
|
@ -185,21 +250,34 @@ impl<'a> SingleRegionProcessor<'a> {
|
||||||
|
|
||||||
if self.lightmap_needed {
|
if self.lightmap_needed {
|
||||||
let chunk_lightmap = Self::render_chunk_lightmap(block_light);
|
let chunk_lightmap = Self::render_chunk_lightmap(block_light);
|
||||||
overlay_chunk(&mut self.lightmap, &chunk_lightmap, chunk_coords);
|
overlay_chunk(&mut data.lightmap, &chunk_lightmap, chunk_coords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.entities_needed {
|
||||||
|
let mut block_entities = chunk.block_entities().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to process block entities for chunk {:?}",
|
||||||
|
chunk_coords,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
data.entities.block_entities.append(&mut block_entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes the chunks of the region
|
/// Processes the chunks of the region
|
||||||
fn process_chunks(&mut self) -> Result<()> {
|
fn process_chunks(&self, data: &mut SingleRegionData) -> Result<()> {
|
||||||
crate::nbt::region::from_file(&self.input_path)?
|
crate::nbt::region::from_file(&self.input_path)?.foreach_chunk(
|
||||||
.foreach_chunk(|chunk_coords, data| self.process_chunk(chunk_coords, data))
|
|chunk_coords, chunk_data| self.process_chunk(data, chunk_coords, chunk_data),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes the region
|
/// Processes the region
|
||||||
fn run(mut self) -> Result<RegionProcessorStatus> {
|
fn run(&self) -> Result<RegionProcessorStatus> {
|
||||||
if !self.output_needed && !self.lightmap_needed {
|
if !self.output_needed && !self.lightmap_needed && !self.entities_needed {
|
||||||
debug!(
|
debug!(
|
||||||
"Skipping unchanged region r.{}.{}.mca",
|
"Skipping unchanged region r.{}.{}.mca",
|
||||||
self.coords.x, self.coords.z
|
self.coords.x, self.coords.z
|
||||||
|
@ -212,8 +290,13 @@ impl<'a> SingleRegionProcessor<'a> {
|
||||||
self.coords.x, self.coords.z
|
self.coords.x, self.coords.z
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(err) = self.process_chunks() {
|
let mut data = SingleRegionData::default();
|
||||||
if self.output_timestamp.is_some() && self.lightmap_timestamp.is_some() {
|
|
||||||
|
if let Err(err) = self.process_chunks(&mut data) {
|
||||||
|
if self.output_timestamp.is_some()
|
||||||
|
&& self.lightmap_timestamp.is_some()
|
||||||
|
&& self.entities_timestamp.is_some()
|
||||||
|
{
|
||||||
warn!(
|
warn!(
|
||||||
"Failed to process region {:?}, using old data: {:?}",
|
"Failed to process region {:?}, using old data: {:?}",
|
||||||
self.coords, err
|
self.coords, err
|
||||||
|
@ -228,10 +311,20 @@ impl<'a> SingleRegionProcessor<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save_region()?;
|
let processed_region = ProcessedRegion {
|
||||||
self.save_lightmap()?;
|
biome_list: data.biome_list.into_iter().collect(),
|
||||||
|
chunks: data.chunks,
|
||||||
|
};
|
||||||
|
|
||||||
Ok(RegionProcessorStatus::Ok)
|
self.save_region(&processed_region)?;
|
||||||
|
self.save_lightmap(&data.lightmap)?;
|
||||||
|
self.save_entities(&mut data.entities)?;
|
||||||
|
|
||||||
|
Ok(if data.has_unknown {
|
||||||
|
RegionProcessorStatus::OkWithUnknown
|
||||||
|
} else {
|
||||||
|
RegionProcessorStatus::Ok
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,11 +365,20 @@ impl<'a> RegionProcessor<'a> {
|
||||||
})?
|
})?
|
||||||
.filter_map(|entry| entry.ok())
|
.filter_map(|entry| entry.ok())
|
||||||
.filter(|entry| {
|
.filter(|entry| {
|
||||||
|
(|| {
|
||||||
// We are only interested in regular files
|
// We are only interested in regular files
|
||||||
matches!(
|
let file_type = entry.file_type().ok()?;
|
||||||
entry.file_type().map(|file_type| file_type.is_file()),
|
if !file_type.is_file() {
|
||||||
Ok(true)
|
return None;
|
||||||
)
|
}
|
||||||
|
|
||||||
|
let metadata = entry.metadata().ok()?;
|
||||||
|
if metadata.len() == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
|
})()
|
||||||
|
.is_some()
|
||||||
})
|
})
|
||||||
.filter_map(|entry| parse_region_filename(&entry.file_name()))
|
.filter_map(|entry| parse_region_filename(&entry.file_name()))
|
||||||
.collect())
|
.collect())
|
||||||
|
@ -291,31 +393,27 @@ impl<'a> RegionProcessor<'a> {
|
||||||
///
|
///
|
||||||
/// Returns a list of the coordinates of all processed regions
|
/// Returns a list of the coordinates of all processed regions
|
||||||
pub fn run(self) -> Result<Vec<TileCoords>> {
|
pub fn run(self) -> Result<Vec<TileCoords>> {
|
||||||
|
use RegionProcessorStatus as Status;
|
||||||
|
|
||||||
fs::create_dir_all(&self.config.processed_dir)?;
|
fs::create_dir_all(&self.config.processed_dir)?;
|
||||||
fs::create_dir_all(&self.config.tile_dir(TileKind::Lightmap, 0))?;
|
fs::create_dir_all(&self.config.tile_dir(TileKind::Lightmap, 0))?;
|
||||||
|
fs::create_dir_all(&self.config.entities_dir(0))?;
|
||||||
|
|
||||||
info!("Processing region files...");
|
info!("Processing region files...");
|
||||||
|
|
||||||
let (region_send, region_recv) = mpsc::channel();
|
let (region_send, region_recv) = mpsc::channel();
|
||||||
let (processed_send, processed_recv) = mpsc::channel();
|
let (status_send, status_recv) = mpsc::channel();
|
||||||
let (error_send, error_recv) = mpsc::channel();
|
|
||||||
|
|
||||||
self.collect_regions()?.par_iter().try_for_each(|&coords| {
|
self.collect_regions()?.par_iter().try_for_each(|&coords| {
|
||||||
let ret = self
|
let ret = self
|
||||||
.process_region(coords)
|
.process_region(coords)
|
||||||
.with_context(|| format!("Failed to process region {:?}", coords))?;
|
.with_context(|| format!("Failed to process region {:?}", coords))?;
|
||||||
|
|
||||||
if ret != RegionProcessorStatus::ErrorMissing {
|
if ret != Status::ErrorMissing {
|
||||||
region_send.send(coords).unwrap();
|
region_send.send(coords).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
match ret {
|
status_send.send(ret).unwrap();
|
||||||
RegionProcessorStatus::Ok => processed_send.send(()).unwrap(),
|
|
||||||
RegionProcessorStatus::Skipped => {}
|
|
||||||
RegionProcessorStatus::ErrorOk | RegionProcessorStatus::ErrorMissing => {
|
|
||||||
error_send.send(()).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
@ -323,18 +421,30 @@ impl<'a> RegionProcessor<'a> {
|
||||||
drop(region_send);
|
drop(region_send);
|
||||||
let mut regions: Vec<_> = region_recv.into_iter().collect();
|
let mut regions: Vec<_> = region_recv.into_iter().collect();
|
||||||
|
|
||||||
drop(processed_send);
|
drop(status_send);
|
||||||
let processed = processed_recv.into_iter().count();
|
|
||||||
drop(error_send);
|
let mut status = EnumMap::<_, usize>::default();
|
||||||
let errors = error_recv.into_iter().count();
|
for ret in status_recv {
|
||||||
|
status[ret] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Processed region files ({} processed, {} unchanged, {} errors)",
|
"Processed region files ({} processed, {} unchanged, {} errors)",
|
||||||
processed,
|
status[Status::Ok] + status[Status::OkWithUnknown],
|
||||||
regions.len() - processed - errors,
|
status[Status::Skipped],
|
||||||
errors,
|
status[Status::ErrorOk] + status[Status::ErrorMissing],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if status[Status::OkWithUnknown] > 0 {
|
||||||
|
warn!("Unknown block or biome types found during processing");
|
||||||
|
eprint!(concat!(
|
||||||
|
"\n",
|
||||||
|
" If you're encountering this issue with an unmodified Minecraft version supported by MinedMap,\n",
|
||||||
|
" please file a bug report including the output with the --verbose flag.\n",
|
||||||
|
"\n",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Sort regions in a zig-zag pattern to optimize cache usage
|
// Sort regions in a zig-zag pattern to optimize cache usage
|
||||||
regions.sort_unstable_by_key(|&TileCoords { x, z }| (x, if x % 2 == 0 { z } else { -z }));
|
regions.sort_unstable_by_key(|&TileCoords { x, z }| (x, if x % 2 == 0 { z } else { -z }));
|
||||||
|
|
||||||
|
|
107
src/core/tile_collector.rs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
//! A trait for recursively processing tiles
|
||||||
|
//!
|
||||||
|
//! Used for mipmap generation and collecting entity data
|
||||||
|
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
use super::common::*;
|
||||||
|
|
||||||
|
/// Helper to determine if no further mipmap levels are needed
|
||||||
|
///
|
||||||
|
/// If all tile coordinates are -1 or 0, further mipmap levels will not
|
||||||
|
/// decrease the number of tiles and mipmap generated is considered finished.
|
||||||
|
fn done(tiles: &TileCoordMap) -> bool {
|
||||||
|
tiles
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.all(|(z, xs)| (-1..=0).contains(z) && xs.iter().all(|x| (-1..=0).contains(x)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derives the map of populated tile coordinates for the next mipmap level
|
||||||
|
fn map_coords(tiles: &TileCoordMap) -> TileCoordMap {
|
||||||
|
let mut ret = TileCoordMap::default();
|
||||||
|
|
||||||
|
for (&z, xs) in &tiles.0 {
|
||||||
|
for &x in xs {
|
||||||
|
let xt = x >> 1;
|
||||||
|
let zt = z >> 1;
|
||||||
|
|
||||||
|
ret.0.entry(zt).or_default().insert(xt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait to implement for collecting tiles recursively
|
||||||
|
pub trait TileCollector: Sync {
|
||||||
|
/// Return value of [TileCollector::collect_one]
|
||||||
|
type CollectOutput: Send;
|
||||||
|
|
||||||
|
/// List of level 0 tiles
|
||||||
|
fn tiles(&self) -> &[TileCoords];
|
||||||
|
|
||||||
|
/// Called at the beginning of each level of processing
|
||||||
|
fn prepare(&self, level: usize) -> Result<()>;
|
||||||
|
|
||||||
|
/// Called at the end of each level of processing
|
||||||
|
fn finish(
|
||||||
|
&self,
|
||||||
|
level: usize,
|
||||||
|
outputs: impl Iterator<Item = Self::CollectOutput>,
|
||||||
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Called for each tile coordinate of the level that is currently being generated
|
||||||
|
fn collect_one(
|
||||||
|
&self,
|
||||||
|
level: usize,
|
||||||
|
coords: TileCoords,
|
||||||
|
prev: &TileCoordMap,
|
||||||
|
) -> Result<Self::CollectOutput>;
|
||||||
|
|
||||||
|
/// Collects tiles recursively
|
||||||
|
fn collect_tiles(&self) -> Result<Vec<TileCoordMap>> {
|
||||||
|
let mut tile_stack = {
|
||||||
|
let mut tile_map = TileCoordMap::default();
|
||||||
|
|
||||||
|
for &TileCoords { x, z } in self.tiles() {
|
||||||
|
tile_map.0.entry(z).or_default().insert(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![tile_map]
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let level = tile_stack.len();
|
||||||
|
let prev = &tile_stack[level - 1];
|
||||||
|
if done(prev) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prepare(level)?;
|
||||||
|
|
||||||
|
let next = map_coords(prev);
|
||||||
|
|
||||||
|
let (send, recv) = mpsc::channel();
|
||||||
|
|
||||||
|
next.0
|
||||||
|
.par_iter()
|
||||||
|
.flat_map(|(&z, xs)| xs.par_iter().map(move |&x| TileCoords { x, z }))
|
||||||
|
.try_for_each(|coords| {
|
||||||
|
let output = self.collect_one(level, coords, prev)?;
|
||||||
|
send.send(output).unwrap();
|
||||||
|
anyhow::Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
drop(send);
|
||||||
|
self.finish(level, recv.into_iter())?;
|
||||||
|
|
||||||
|
tile_stack.push(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tile_stack)
|
||||||
|
}
|
||||||
|
}
|
97
src/core/tile_merger.rs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
//! Mipmap-style merging of tiles
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::BufWriter,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::SystemTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use super::common::*;
|
||||||
|
use crate::io::fs;
|
||||||
|
|
||||||
|
/// [TileMerger::merge_tiles] return
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Stat {
|
||||||
|
/// None of the input files were found
|
||||||
|
NotFound,
|
||||||
|
/// The output file is up-to-date
|
||||||
|
Skipped,
|
||||||
|
/// The output file is regenerated
|
||||||
|
Regenerate,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A source file for the [TileMerger]
|
||||||
|
///
|
||||||
|
/// The tuple elements are X and Z coordinate offsets in the range [0, 1],
|
||||||
|
/// the file path and the time of last change of the input.
|
||||||
|
pub type Source = ((i32, i32), PathBuf, SystemTime);
|
||||||
|
|
||||||
|
/// Reusable trait for mipmap-style tile merging with change tracking
|
||||||
|
pub trait TileMerger {
|
||||||
|
/// [fs::FileMetaVersion] of input and output files
|
||||||
|
///
|
||||||
|
/// The version in the file metadata on disk must match the returned
|
||||||
|
/// version for the a to be considered up-to-date.
|
||||||
|
fn file_meta_version(&self) -> fs::FileMetaVersion;
|
||||||
|
|
||||||
|
/// Returns the paths of input and output files
|
||||||
|
fn tile_path(&self, level: usize, coords: TileCoords) -> PathBuf;
|
||||||
|
|
||||||
|
/// Can be used to log the processing status
|
||||||
|
fn log(&self, _output_path: &Path, _stat: Stat) {}
|
||||||
|
|
||||||
|
/// Handles the actual merging of source files
|
||||||
|
fn write_tile(&self, file: &mut BufWriter<File>, sources: &[Source]) -> Result<()>;
|
||||||
|
|
||||||
|
/// Generates a tile at given coordinates and mipmap level
|
||||||
|
fn merge_tiles(&self, level: usize, coords: TileCoords, prev: &TileCoordMap) -> Result<Stat> {
|
||||||
|
let version = self.file_meta_version();
|
||||||
|
let output_path = self.tile_path(level, coords);
|
||||||
|
let output_timestamp = fs::read_timestamp(&output_path, version);
|
||||||
|
|
||||||
|
let sources: Vec<_> = [(0, 0), (0, 1), (1, 0), (1, 1)]
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(dx, dz)| {
|
||||||
|
let source_coords = TileCoords {
|
||||||
|
x: 2 * coords.x + dx,
|
||||||
|
z: 2 * coords.z + dz,
|
||||||
|
};
|
||||||
|
if !prev.contains(source_coords) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let source_path = self.tile_path(level - 1, source_coords);
|
||||||
|
let timestamp = match fs::modified_timestamp(&source_path) {
|
||||||
|
Ok(timestamp) => timestamp,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("{:?}", err);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(((dx, dz), source_path, timestamp))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let Some(input_timestamp) = sources.iter().map(|(_, _, ts)| *ts).max() else {
|
||||||
|
self.log(&output_path, Stat::NotFound);
|
||||||
|
return Ok(Stat::NotFound);
|
||||||
|
};
|
||||||
|
|
||||||
|
if Some(input_timestamp) <= output_timestamp {
|
||||||
|
self.log(&output_path, Stat::Skipped);
|
||||||
|
return Ok(Stat::Skipped);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.log(&output_path, Stat::Regenerate);
|
||||||
|
|
||||||
|
fs::create_with_timestamp(&output_path, version, input_timestamp, |file| {
|
||||||
|
self.write_tile(file, &sources)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Stat::Regenerate)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,154 @@
|
||||||
//! The [TileMipmapper]
|
//! The [TileMipmapper]
|
||||||
|
|
||||||
use std::sync::mpsc;
|
use std::{marker::PhantomData, ops::Add};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use rayon::prelude::*;
|
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use super::common::*;
|
use super::{
|
||||||
|
common::*,
|
||||||
|
tile_collector::TileCollector,
|
||||||
|
tile_merger::{self, TileMerger},
|
||||||
|
};
|
||||||
use crate::{io::fs, types::*};
|
use crate::{io::fs, types::*};
|
||||||
|
|
||||||
|
/// Counters for the number of processed and total tiles
|
||||||
|
///
|
||||||
|
/// Used as return of [TileMipmapper::collect_one]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct MipmapStat {
|
||||||
|
/// Total number of tiles
|
||||||
|
total: usize,
|
||||||
|
/// Processed number of tiles
|
||||||
|
processed: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<tile_merger::Stat> for MipmapStat {
|
||||||
|
fn from(value: tile_merger::Stat) -> Self {
|
||||||
|
match value {
|
||||||
|
tile_merger::Stat::NotFound => MipmapStat {
|
||||||
|
total: 0,
|
||||||
|
processed: 0,
|
||||||
|
},
|
||||||
|
tile_merger::Stat::Skipped => MipmapStat {
|
||||||
|
total: 1,
|
||||||
|
processed: 0,
|
||||||
|
},
|
||||||
|
tile_merger::Stat::Regenerate => MipmapStat {
|
||||||
|
total: 1,
|
||||||
|
processed: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add for MipmapStat {
|
||||||
|
type Output = MipmapStat;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self::Output {
|
||||||
|
MipmapStat {
|
||||||
|
total: self.total + rhs.total,
|
||||||
|
processed: self.processed + rhs.processed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [TileMerger] for map tile images
|
||||||
|
struct MapMerger<'a, P> {
|
||||||
|
/// Common MinedMap configuration from command line
|
||||||
|
config: &'a Config,
|
||||||
|
/// Tile kind (map or lightmap)
|
||||||
|
kind: TileKind,
|
||||||
|
/// Pixel format type
|
||||||
|
_pixel: PhantomData<P>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, P> MapMerger<'a, P> {
|
||||||
|
/// Creates a new [MapMerger]
|
||||||
|
fn new(config: &'a Config, kind: TileKind) -> Self {
|
||||||
|
MapMerger {
|
||||||
|
config,
|
||||||
|
kind,
|
||||||
|
_pixel: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: image::PixelWithColorType> TileMerger for MapMerger<'_, P>
|
||||||
|
where
|
||||||
|
[P::Subpixel]: image::EncodableLayout,
|
||||||
|
image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>,
|
||||||
|
{
|
||||||
|
fn file_meta_version(&self) -> fs::FileMetaVersion {
|
||||||
|
MIPMAP_FILE_META_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tile_path(&self, level: usize, coords: TileCoords) -> std::path::PathBuf {
|
||||||
|
self.config.tile_path(self.kind, level, coords)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log(&self, output_path: &std::path::Path, stat: super::tile_merger::Stat) {
|
||||||
|
match stat {
|
||||||
|
super::tile_merger::Stat::NotFound => {}
|
||||||
|
super::tile_merger::Stat::Skipped => {
|
||||||
|
debug!(
|
||||||
|
"Skipping unchanged mipmap tile {}",
|
||||||
|
output_path
|
||||||
|
.strip_prefix(&self.config.output_dir)
|
||||||
|
.expect("tile path must be in output directory")
|
||||||
|
.display(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
super::tile_merger::Stat::Regenerate => {
|
||||||
|
debug!(
|
||||||
|
"Rendering mipmap tile {}",
|
||||||
|
output_path
|
||||||
|
.strip_prefix(&self.config.output_dir)
|
||||||
|
.expect("tile path must be in output directory")
|
||||||
|
.display(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_tile(
|
||||||
|
&self,
|
||||||
|
file: &mut std::io::BufWriter<std::fs::File>,
|
||||||
|
sources: &[super::tile_merger::Source],
|
||||||
|
) -> Result<()> {
|
||||||
|
/// Tile width/height
|
||||||
|
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
|
||||||
|
|
||||||
|
let mut image: image::DynamicImage =
|
||||||
|
image::ImageBuffer::<P, Vec<P::Subpixel>>::new(N, N).into();
|
||||||
|
|
||||||
|
for ((dx, dz), source_path, _) in sources {
|
||||||
|
let source = match image::open(source_path) {
|
||||||
|
Ok(source) => source,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"Failed to read source image {}: {:?}",
|
||||||
|
source_path.display(),
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let resized = source.resize(N / 2, N / 2, image::imageops::FilterType::Triangle);
|
||||||
|
image::imageops::overlay(
|
||||||
|
&mut image,
|
||||||
|
&resized,
|
||||||
|
*dx as i64 * (N / 2) as i64,
|
||||||
|
*dz as i64 * (N / 2) as i64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
image
|
||||||
|
.write_to(file, self.config.tile_image_format())
|
||||||
|
.context("Failed to save image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Generates mipmap tiles from full-resolution tile images
|
/// Generates mipmap tiles from full-resolution tile images
|
||||||
pub struct TileMipmapper<'a> {
|
pub struct TileMipmapper<'a> {
|
||||||
/// Common MinedMap configuration from command line
|
/// Common MinedMap configuration from command line
|
||||||
|
@ -17,39 +157,63 @@ pub struct TileMipmapper<'a> {
|
||||||
regions: &'a [TileCoords],
|
regions: &'a [TileCoords],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TileCollector for TileMipmapper<'_> {
|
||||||
|
type CollectOutput = MipmapStat;
|
||||||
|
|
||||||
|
fn tiles(&self) -> &[TileCoords] {
|
||||||
|
self.regions
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare(&self, level: usize) -> Result<()> {
|
||||||
|
info!("Generating level {} mipmaps...", level);
|
||||||
|
|
||||||
|
fs::create_dir_all(&self.config.tile_dir(TileKind::Map, level))?;
|
||||||
|
fs::create_dir_all(&self.config.tile_dir(TileKind::Lightmap, level))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(
|
||||||
|
&self,
|
||||||
|
level: usize,
|
||||||
|
outputs: impl Iterator<Item = Self::CollectOutput>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let stat = outputs.fold(
|
||||||
|
MipmapStat {
|
||||||
|
total: 0,
|
||||||
|
processed: 0,
|
||||||
|
},
|
||||||
|
MipmapStat::add,
|
||||||
|
);
|
||||||
|
info!(
|
||||||
|
"Generated level {} mipmaps ({} processed, {} unchanged)",
|
||||||
|
level,
|
||||||
|
stat.processed,
|
||||||
|
stat.total - stat.processed,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_one(
|
||||||
|
&self,
|
||||||
|
level: usize,
|
||||||
|
coords: TileCoords,
|
||||||
|
prev: &TileCoordMap,
|
||||||
|
) -> Result<Self::CollectOutput> {
|
||||||
|
let map_stat = self.render_mipmap::<image::Rgba<u8>>(TileKind::Map, level, coords, prev)?;
|
||||||
|
let lightmap_stat =
|
||||||
|
self.render_mipmap::<image::LumaA<u8>>(TileKind::Lightmap, level, coords, prev)?;
|
||||||
|
Ok(map_stat + lightmap_stat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> TileMipmapper<'a> {
|
impl<'a> TileMipmapper<'a> {
|
||||||
/// Constructs a new TileMipmapper
|
/// Constructs a new TileMipmapper
|
||||||
pub fn new(config: &'a Config, regions: &'a [TileCoords]) -> Self {
|
pub fn new(config: &'a Config, regions: &'a [TileCoords]) -> Self {
|
||||||
TileMipmapper { config, regions }
|
TileMipmapper { config, regions }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to determine if no further mipmap levels are needed
|
|
||||||
///
|
|
||||||
/// If all tile coordinates are -1 or 0, further mipmap levels will not
|
|
||||||
/// decrease the number of tiles and mipmap generated is considered finished.
|
|
||||||
fn done(tiles: &TileCoordMap) -> bool {
|
|
||||||
tiles
|
|
||||||
.0
|
|
||||||
.iter()
|
|
||||||
.all(|(z, xs)| (-1..=0).contains(z) && xs.iter().all(|x| (-1..=0).contains(x)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Derives the map of populated tile coordinates for the next mipmap level
|
|
||||||
fn map_coords(tiles: &TileCoordMap) -> TileCoordMap {
|
|
||||||
let mut ret = TileCoordMap::default();
|
|
||||||
|
|
||||||
for (&z, xs) in &tiles.0 {
|
|
||||||
for &x in xs {
|
|
||||||
let xt = x >> 1;
|
|
||||||
let zt = z >> 1;
|
|
||||||
|
|
||||||
ret.0.entry(zt).or_default().insert(xt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders and saves a single mipmap tile image
|
/// Renders and saves a single mipmap tile image
|
||||||
///
|
///
|
||||||
/// Each mipmap tile is rendered by taking 2x2 tiles from the
|
/// Each mipmap tile is rendered by taking 2x2 tiles from the
|
||||||
|
@ -60,174 +224,18 @@ impl<'a> TileMipmapper<'a> {
|
||||||
level: usize,
|
level: usize,
|
||||||
coords: TileCoords,
|
coords: TileCoords,
|
||||||
prev: &TileCoordMap,
|
prev: &TileCoordMap,
|
||||||
count_total: &mpsc::Sender<()>,
|
) -> Result<MipmapStat>
|
||||||
count_processed: &mpsc::Sender<()>,
|
|
||||||
) -> Result<()>
|
|
||||||
where
|
where
|
||||||
[P::Subpixel]: image::EncodableLayout,
|
[P::Subpixel]: image::EncodableLayout,
|
||||||
image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>,
|
image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>,
|
||||||
{
|
{
|
||||||
/// Tile width/height
|
let merger = MapMerger::<P>::new(self.config, kind);
|
||||||
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
|
let ret = merger.merge_tiles(level, coords, prev)?;
|
||||||
|
Ok(ret.into())
|
||||||
let version = match kind {
|
|
||||||
TileKind::Map => REGION_FILE_META_VERSION,
|
|
||||||
TileKind::Lightmap => LIGHTMAP_FILE_META_VERSION,
|
|
||||||
};
|
|
||||||
let output_path = self.config.tile_path(kind, level, coords);
|
|
||||||
let output_timestamp = fs::read_timestamp(&output_path, version);
|
|
||||||
|
|
||||||
let sources: Vec<_> = [(0, 0), (0, 1), (1, 0), (1, 1)]
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|(dx, dz)| {
|
|
||||||
let source_coords = TileCoords {
|
|
||||||
x: 2 * coords.x + dx,
|
|
||||||
z: 2 * coords.z + dz,
|
|
||||||
};
|
|
||||||
if !prev.contains(source_coords) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let source_path = self.config.tile_path(kind, level - 1, source_coords);
|
|
||||||
let timestamp = match fs::modified_timestamp(&source_path) {
|
|
||||||
Ok(timestamp) => timestamp,
|
|
||||||
Err(err) => {
|
|
||||||
warn!("{}", err);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Some(((dx, dz), source_path, timestamp))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let Some(input_timestamp) = sources.iter().map(|(_, _, ts)| *ts).max() else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
count_total.send(()).unwrap();
|
|
||||||
|
|
||||||
if Some(input_timestamp) <= output_timestamp {
|
|
||||||
debug!(
|
|
||||||
"Skipping unchanged mipmap tile {}",
|
|
||||||
output_path
|
|
||||||
.strip_prefix(&self.config.output_dir)
|
|
||||||
.expect("tile path must be in output directory")
|
|
||||||
.display(),
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Rendering mipmap tile {}",
|
|
||||||
output_path
|
|
||||||
.strip_prefix(&self.config.output_dir)
|
|
||||||
.expect("tile path must be in output directory")
|
|
||||||
.display(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut image: image::DynamicImage =
|
|
||||||
image::ImageBuffer::<P, Vec<P::Subpixel>>::new(N, N).into();
|
|
||||||
|
|
||||||
for ((dx, dz), source_path, _) in sources {
|
|
||||||
let source = match image::open(&source_path) {
|
|
||||||
Ok(source) => source,
|
|
||||||
Err(err) => {
|
|
||||||
warn!(
|
|
||||||
"Failed to read source image {}: {}",
|
|
||||||
source_path.display(),
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let resized = source.resize(N / 2, N / 2, image::imageops::FilterType::Triangle);
|
|
||||||
image::imageops::overlay(
|
|
||||||
&mut image,
|
|
||||||
&resized,
|
|
||||||
dx as i64 * (N / 2) as i64,
|
|
||||||
dz as i64 * (N / 2) as i64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::create_with_timestamp(&output_path, version, input_timestamp, |file| {
|
|
||||||
image
|
|
||||||
.write_to(file, image::ImageFormat::Png)
|
|
||||||
.context("Failed to save image")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
count_processed.send(()).unwrap();
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs the mipmap generation
|
/// Runs the mipmap generation
|
||||||
pub fn run(self) -> Result<Vec<TileCoordMap>> {
|
pub fn run(self) -> Result<Vec<TileCoordMap>> {
|
||||||
let mut tile_stack = {
|
self.collect_tiles()
|
||||||
let mut tile_map = TileCoordMap::default();
|
|
||||||
|
|
||||||
for &TileCoords { x, z } in self.regions {
|
|
||||||
tile_map.0.entry(z).or_default().insert(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
vec![tile_map]
|
|
||||||
};
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let level = tile_stack.len();
|
|
||||||
let prev = &tile_stack[level - 1];
|
|
||||||
if Self::done(prev) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Generating level {} mipmaps...", level);
|
|
||||||
|
|
||||||
fs::create_dir_all(&self.config.tile_dir(TileKind::Map, level))?;
|
|
||||||
fs::create_dir_all(&self.config.tile_dir(TileKind::Lightmap, level))?;
|
|
||||||
|
|
||||||
let next = Self::map_coords(prev);
|
|
||||||
|
|
||||||
let (total_send, total_recv) = mpsc::channel();
|
|
||||||
let (processed_send, processed_recv) = mpsc::channel();
|
|
||||||
|
|
||||||
next.0
|
|
||||||
.par_iter()
|
|
||||||
.flat_map(|(&z, xs)| xs.par_iter().map(move |&x| TileCoords { x, z }))
|
|
||||||
.try_for_each(|coords| {
|
|
||||||
self.render_mipmap::<image::Rgba<u8>>(
|
|
||||||
TileKind::Map,
|
|
||||||
level,
|
|
||||||
coords,
|
|
||||||
prev,
|
|
||||||
&total_send,
|
|
||||||
&processed_send,
|
|
||||||
)?;
|
|
||||||
self.render_mipmap::<image::LumaA<u8>>(
|
|
||||||
TileKind::Lightmap,
|
|
||||||
level,
|
|
||||||
coords,
|
|
||||||
prev,
|
|
||||||
&total_send,
|
|
||||||
&processed_send,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
drop(total_send);
|
|
||||||
let total = total_recv.into_iter().count();
|
|
||||||
|
|
||||||
drop(processed_send);
|
|
||||||
let processed = processed_recv.into_iter().count();
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Generated level {} mipmaps ({} processed, {} unchanged)",
|
|
||||||
level,
|
|
||||||
processed,
|
|
||||||
total - processed,
|
|
||||||
);
|
|
||||||
|
|
||||||
tile_stack.push(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(tile_stack)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ use tracing::{debug, info};
|
||||||
use super::{common::*, region_group::RegionGroup};
|
use super::{common::*, region_group::RegionGroup};
|
||||||
use crate::{
|
use crate::{
|
||||||
io::{fs, storage},
|
io::{fs, storage},
|
||||||
resource::{block_color, needs_biome, Colorf},
|
resource::{Colorf, block_color, needs_biome},
|
||||||
types::*,
|
types::*,
|
||||||
util::coord_offset,
|
util::coord_offset,
|
||||||
};
|
};
|
||||||
|
@ -105,7 +105,7 @@ impl<'a> TileRenderer<'a> {
|
||||||
|
|
||||||
region_loader
|
region_loader
|
||||||
.get_or_try_init(|| async {
|
.get_or_try_init(|| async {
|
||||||
storage::read(&processed_path).context("Failed to load processed region data")
|
storage::read_file(&processed_path).context("Failed to load processed region data")
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.cloned()
|
.cloned()
|
||||||
|
@ -133,7 +133,7 @@ impl<'a> TileRenderer<'a> {
|
||||||
/// Hashing the value as a single u32 is more efficient than hashing
|
/// Hashing the value as a single u32 is more efficient than hashing
|
||||||
/// the tuple elements separately.
|
/// the tuple elements separately.
|
||||||
fn biome_key((dx, dz, index): (i8, i8, u16)) -> u32 {
|
fn biome_key((dx, dz, index): (i8, i8, u16)) -> u32 {
|
||||||
(dx as u8 as u32) | (dz as u8 as u32) << 8 | (index as u32) << 16
|
(dx as u8 as u32) | ((dz as u8 as u32) << 8) | ((index as u32) << 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One quadrant of the kernel used to smooth biome edges
|
/// One quadrant of the kernel used to smooth biome edges
|
||||||
|
@ -186,7 +186,7 @@ impl<'a> TileRenderer<'a> {
|
||||||
|
|
||||||
for ((region_x, region_z, index), w) in weights.into_values() {
|
for ((region_x, region_z, index), w) in weights.into_values() {
|
||||||
let region = region_group.get(region_x, region_z)?;
|
let region = region_group.get(region_x, region_z)?;
|
||||||
let biome = region.biome_list.get_index(index.into())?;
|
let biome = region.biome_list.get(usize::from(index))?;
|
||||||
|
|
||||||
total += w;
|
total += w;
|
||||||
color += w * block_color(block, Some(biome), depth.0 as f32);
|
color += w * block_color(block, Some(biome), depth.0 as f32);
|
||||||
|
@ -303,7 +303,7 @@ impl<'a> TileRenderer<'a> {
|
||||||
processed_timestamp,
|
processed_timestamp,
|
||||||
|file| {
|
|file| {
|
||||||
image
|
image
|
||||||
.write_to(file, image::ImageFormat::Png)
|
.write_to(file, self.config.tile_image_format())
|
||||||
.context("Failed to save image")
|
.context("Failed to save image")
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
|
@ -10,47 +10,64 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use bincode::{Decode, Encode};
|
||||||
|
|
||||||
use super::fs;
|
use super::fs;
|
||||||
|
|
||||||
|
/// Bincode configuration
|
||||||
|
const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard();
|
||||||
|
|
||||||
|
/// Serializes data and writes it to a writer
|
||||||
|
pub fn write<W: Write, T: Encode>(writer: &mut W, value: &T) -> Result<()> {
|
||||||
|
let data = bincode::encode_to_vec(value, BINCODE_CONFIG)?;
|
||||||
|
let len = u32::try_from(data.len())?;
|
||||||
|
let compressed = zstd::bulk::compress(&data, 1)?;
|
||||||
|
drop(data);
|
||||||
|
|
||||||
|
writer.write_all(&len.to_be_bytes())?;
|
||||||
|
writer.write_all(&compressed)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Serializes data and stores it in a file
|
/// Serializes data and stores it in a file
|
||||||
///
|
///
|
||||||
/// A timestamp is stored in an assiciated metadata file.
|
/// A timestamp is stored in an assiciated metadata file.
|
||||||
pub fn write<T: Serialize>(
|
pub fn write_file<T: Encode>(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
value: &T,
|
value: &T,
|
||||||
version: fs::FileMetaVersion,
|
version: fs::FileMetaVersion,
|
||||||
timestamp: SystemTime,
|
timestamp: SystemTime,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
fs::create_with_timestamp(path, version, timestamp, |file| {
|
fs::create_with_timestamp(path, version, timestamp, |file| write(file, value))
|
||||||
let data = bincode::serialize(value)?;
|
|
||||||
let len = u32::try_from(data.len())?;
|
|
||||||
let compressed = zstd::bulk::compress(&data, 1)?;
|
|
||||||
drop(data);
|
|
||||||
|
|
||||||
file.write_all(&len.to_be_bytes())?;
|
|
||||||
file.write_all(&compressed)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads data from a file and deserializes it
|
/// Reads data from a reader and deserializes it
|
||||||
pub fn read<T: DeserializeOwned>(path: &Path) -> Result<T> {
|
pub fn read<R, T>(reader: &mut R) -> Result<T>
|
||||||
(|| -> Result<T> {
|
where
|
||||||
let mut file = File::open(path)?;
|
R: Read,
|
||||||
|
T: Decode<()>,
|
||||||
|
{
|
||||||
let mut len_buf = [0u8; 4];
|
let mut len_buf = [0u8; 4];
|
||||||
file.read_exact(&mut len_buf)?;
|
reader.read_exact(&mut len_buf)?;
|
||||||
let len = usize::try_from(u32::from_be_bytes(len_buf))?;
|
let len = usize::try_from(u32::from_be_bytes(len_buf))?;
|
||||||
|
|
||||||
let mut compressed = vec![];
|
let mut compressed = vec![];
|
||||||
file.read_to_end(&mut compressed)?;
|
reader.read_to_end(&mut compressed)?;
|
||||||
let data = zstd::bulk::decompress(&compressed, len)?;
|
let data = zstd::bulk::decompress(&compressed, len)?;
|
||||||
drop(compressed);
|
drop(compressed);
|
||||||
|
|
||||||
Ok(bincode::deserialize(&data)?)
|
Ok(bincode::decode_from_slice(&data, BINCODE_CONFIG)?.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads data from a file and deserializes it
|
||||||
|
pub fn read_file<T>(path: &Path) -> Result<T>
|
||||||
|
where
|
||||||
|
T: Decode<()>,
|
||||||
|
{
|
||||||
|
(|| -> Result<T> {
|
||||||
|
let mut file = File::open(path)?;
|
||||||
|
read(&mut file)
|
||||||
})()
|
})()
|
||||||
.with_context(|| format!("Failed to read file {}", path.display()))
|
.with_context(|| format!("Failed to read file {}", path.display()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![warn(clippy::missing_docs_in_private_items)]
|
#![warn(clippy::missing_docs_in_private_items)]
|
||||||
|
|
||||||
|
#[cfg(feature = "jemalloc-auto")]
|
||||||
|
extern crate minedmap_default_alloc;
|
||||||
|
|
||||||
mod core;
|
mod core;
|
||||||
mod io;
|
mod io;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
|
@ -39,7 +39,9 @@ pub fn to_flat_coord<const AXIS: u8>(
|
||||||
chunk: ChunkCoord<AXIS>,
|
chunk: ChunkCoord<AXIS>,
|
||||||
block: BlockCoord<AXIS>,
|
block: BlockCoord<AXIS>,
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
(region as i32) << (BLOCK_BITS + CHUNK_BITS) | ((chunk.0 as i32) << BLOCK_BITS | block.0 as i32)
|
((region as i32) << (BLOCK_BITS + CHUNK_BITS))
|
||||||
|
| ((chunk.0 as i32) << BLOCK_BITS)
|
||||||
|
| (block.0 as i32)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Splits a flat (linear) coordinate into region, chunk and block numbers
|
/// Splits a flat (linear) coordinate into region, chunk and block numbers
|
||||||
|
|
113
src/world/block_entity.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
//! Processing of block entity data
|
||||||
|
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
|
use minedmap_resource::{BlockFlag, BlockType};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
de,
|
||||||
|
sign::{BlockEntitySignExt, SignText},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Kind of sign block
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum SignKind {
|
||||||
|
/// Standing sign
|
||||||
|
Sign,
|
||||||
|
/// Sign attached to wall
|
||||||
|
WallSign,
|
||||||
|
/// Hanging sign
|
||||||
|
HangingSign,
|
||||||
|
/// Hanging sign attached to wall
|
||||||
|
HangingWallSign,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processed sign data
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Serialize)]
|
||||||
|
pub struct Sign {
|
||||||
|
/// The kind of the sign
|
||||||
|
pub kind: SignKind,
|
||||||
|
/// The material of the sign
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
|
pub material: Option<String>,
|
||||||
|
/// The sign's front text
|
||||||
|
#[serde(skip_serializing_if = "SignText::is_empty", default)]
|
||||||
|
pub front_text: SignText,
|
||||||
|
/// The sign's back text
|
||||||
|
#[serde(skip_serializing_if = "SignText::is_empty", default)]
|
||||||
|
pub back_text: SignText,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sign {
|
||||||
|
/// Processes a [de::BlockEntitySign] into a [Sign]
|
||||||
|
fn new(
|
||||||
|
sign: &de::BlockEntitySign,
|
||||||
|
kind: SignKind,
|
||||||
|
material: Option<String>,
|
||||||
|
data_version: u32,
|
||||||
|
) -> Sign {
|
||||||
|
let (front_text, back_text) = sign.text();
|
||||||
|
let front_text = front_text.decode(data_version);
|
||||||
|
let back_text = back_text.decode(data_version);
|
||||||
|
Sign {
|
||||||
|
kind,
|
||||||
|
material,
|
||||||
|
front_text,
|
||||||
|
back_text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data for different kinds of [BlockEntity]
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Serialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum BlockEntityData {
|
||||||
|
/// A sign block
|
||||||
|
Sign(Sign),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A processed block entity
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Serialize)]
|
||||||
|
pub struct BlockEntity {
|
||||||
|
/// Global X coordinate
|
||||||
|
pub x: i32,
|
||||||
|
/// Global Y coordinate
|
||||||
|
pub y: i32,
|
||||||
|
/// Global Z coordinate
|
||||||
|
pub z: i32,
|
||||||
|
/// Entity data
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub data: BlockEntityData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockEntity {
|
||||||
|
/// Processes a [de::BlockEntity] into a [BlockEntity]
|
||||||
|
pub fn new(
|
||||||
|
entity: &de::BlockEntity,
|
||||||
|
block_type: Option<&BlockType>,
|
||||||
|
data_version: u32,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let wall_sign = block_type
|
||||||
|
.map(|block_type| block_type.block_color.is(BlockFlag::WallSign))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let (kind, sign) = match (&entity.data, wall_sign) {
|
||||||
|
(de::BlockEntityData::Sign(sign), false) => (SignKind::Sign, sign),
|
||||||
|
(de::BlockEntityData::Sign(sign), true) => (SignKind::WallSign, sign),
|
||||||
|
(de::BlockEntityData::HangingSign(sign), false) => (SignKind::HangingSign, sign),
|
||||||
|
(de::BlockEntityData::HangingSign(sign), true) => (SignKind::HangingWallSign, sign),
|
||||||
|
(de::BlockEntityData::Other, _) => return None,
|
||||||
|
};
|
||||||
|
let material = block_type
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|block_type| block_type.sign_material.as_ref());
|
||||||
|
let data = BlockEntityData::Sign(Sign::new(sign, kind, material.cloned(), data_version));
|
||||||
|
|
||||||
|
Some(BlockEntity {
|
||||||
|
x: entity.x,
|
||||||
|
y: entity.y,
|
||||||
|
z: entity.z,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,22 +4,22 @@
|
||||||
//! over different data versions as much as possible.
|
//! over different data versions as much as possible.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{btree_map, BTreeMap},
|
collections::{BTreeMap, btree_map},
|
||||||
iter::{self, FusedIterator},
|
iter::{self, FusedIterator},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
|
|
||||||
use super::{de, section::*};
|
use super::{block_entity::BlockEntity, de, section::*};
|
||||||
use crate::{
|
use crate::{
|
||||||
resource::{BiomeTypes, BlockTypes},
|
resource::{BiomeTypes, BlockType, BlockTypes},
|
||||||
types::*,
|
types::*,
|
||||||
|
util::{self, ShiftMask},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Chunk data structure wrapping a [de::Chunk] for convenient access to
|
/// Version-specific part of [Chunk]
|
||||||
/// block and biome data
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Chunk<'a> {
|
pub enum ChunkInner<'a> {
|
||||||
/// Minecraft v1.18+ chunk with biome data moved into sections
|
/// Minecraft v1.18+ chunk with biome data moved into sections
|
||||||
V1_18 {
|
V1_18 {
|
||||||
/// Section data
|
/// Section data
|
||||||
|
@ -50,37 +50,16 @@ pub enum Chunk<'a> {
|
||||||
Empty,
|
Empty,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inner data structure of [SectionIter]
|
/// Chunk data structure wrapping a [de::Chunk] for convenient access to
|
||||||
#[derive(Debug, Clone)]
|
/// block and biome data
|
||||||
enum SectionIterInner<'a> {
|
#[derive(Debug)]
|
||||||
/// Iterator over sections of [Chunk::V1_18]
|
pub struct Chunk<'a> {
|
||||||
V1_18 {
|
/// Version-specific data
|
||||||
/// Inner iterator into section map
|
inner: ChunkInner<'a>,
|
||||||
iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BiomesV1_18<'a>, BlockLight<'a>)>,
|
/// Unprocessed block entities
|
||||||
},
|
block_entities: &'a Vec<de::BlockEntity>,
|
||||||
/// Iterator over sections of [Chunk::V1_13]
|
/// Chunk data version
|
||||||
V1_13 {
|
data_version: u32,
|
||||||
/// Inner iterator into section map
|
|
||||||
iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BlockLight<'a>)>,
|
|
||||||
/// Chunk biome data
|
|
||||||
biomes: &'a BiomesV0<'a>,
|
|
||||||
},
|
|
||||||
/// Iterator over sections of [Chunk::V0]
|
|
||||||
V0 {
|
|
||||||
/// Inner iterator into section map
|
|
||||||
iter: btree_map::Iter<'a, SectionY, (SectionV0<'a>, BlockLight<'a>)>,
|
|
||||||
/// Chunk biome data
|
|
||||||
biomes: &'a BiomesV0<'a>,
|
|
||||||
},
|
|
||||||
/// Empty iterator over an unpopulated chunk ([Chunk::Empty])
|
|
||||||
Empty,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterator over the sections of a [Chunk]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SectionIter<'a> {
|
|
||||||
/// Inner iterator enum
|
|
||||||
inner: SectionIterInner<'a>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Chunk<'a> {
|
impl<'a> Chunk<'a> {
|
||||||
|
@ -89,17 +68,31 @@ impl<'a> Chunk<'a> {
|
||||||
data: &'a de::Chunk,
|
data: &'a de::Chunk,
|
||||||
block_types: &'a BlockTypes,
|
block_types: &'a BlockTypes,
|
||||||
biome_types: &'a BiomeTypes,
|
biome_types: &'a BiomeTypes,
|
||||||
) -> Result<Self> {
|
) -> Result<(Self, bool)> {
|
||||||
let data_version = data.data_version.unwrap_or_default();
|
let data_version = data.data_version.unwrap_or_default();
|
||||||
|
|
||||||
match &data.chunk {
|
let ((inner, has_unknown), block_entities) = match &data.chunk {
|
||||||
de::ChunkVariant::V1_18 { sections } => {
|
de::ChunkVariant::V1_18 {
|
||||||
Self::new_v1_18(data_version, sections, block_types, biome_types)
|
sections,
|
||||||
}
|
block_entities,
|
||||||
de::ChunkVariant::V0 { level } => {
|
} => (
|
||||||
Self::new_v0(data_version, level, block_types, biome_types)
|
Self::new_v1_18(data_version, sections, block_types, biome_types)?,
|
||||||
}
|
block_entities,
|
||||||
}
|
),
|
||||||
|
de::ChunkVariant::V0 { level } => (
|
||||||
|
Self::new_v0(data_version, level, block_types, biome_types)?,
|
||||||
|
&level.tile_entities,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
Chunk {
|
||||||
|
inner,
|
||||||
|
block_entities,
|
||||||
|
data_version,
|
||||||
|
},
|
||||||
|
has_unknown,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [Chunk::new] implementation for Minecraft v1.18+ chunks
|
/// [Chunk::new] implementation for Minecraft v1.18+ chunks
|
||||||
|
@ -108,8 +101,9 @@ impl<'a> Chunk<'a> {
|
||||||
sections: &'a Vec<de::SectionV1_18>,
|
sections: &'a Vec<de::SectionV1_18>,
|
||||||
block_types: &'a BlockTypes,
|
block_types: &'a BlockTypes,
|
||||||
biome_types: &'a BiomeTypes,
|
biome_types: &'a BiomeTypes,
|
||||||
) -> Result<Self> {
|
) -> Result<(ChunkInner<'a>, bool)> {
|
||||||
let mut section_map = BTreeMap::new();
|
let mut section_map = BTreeMap::new();
|
||||||
|
let mut has_unknown = false;
|
||||||
|
|
||||||
for section in sections {
|
for section in sections {
|
||||||
match §ion.section {
|
match §ion.section {
|
||||||
|
@ -118,22 +112,27 @@ impl<'a> Chunk<'a> {
|
||||||
biomes,
|
biomes,
|
||||||
block_light,
|
block_light,
|
||||||
} => {
|
} => {
|
||||||
section_map.insert(
|
let (loaded_section, unknown_blocks) = SectionV1_13::new(
|
||||||
SectionY(section.y),
|
|
||||||
(
|
|
||||||
SectionV1_13::new(
|
|
||||||
data_version,
|
data_version,
|
||||||
block_states.data.as_deref(),
|
block_states.data.as_deref(),
|
||||||
&block_states.palette,
|
&block_states.palette,
|
||||||
block_types,
|
block_types,
|
||||||
)
|
)
|
||||||
.with_context(|| {
|
.with_context(|| format!("Failed to load section at Y={}", section.y))?;
|
||||||
format!("Failed to load section at Y={}", section.y)
|
has_unknown |= unknown_blocks;
|
||||||
})?,
|
|
||||||
|
let (loaded_biomes, unknown_biomes) =
|
||||||
BiomesV1_18::new(biomes.data.as_deref(), &biomes.palette, biome_types)
|
BiomesV1_18::new(biomes.data.as_deref(), &biomes.palette, biome_types)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!("Failed to load section biomes at Y={}", section.y)
|
format!("Failed to load section biomes at Y={}", section.y)
|
||||||
})?,
|
})?;
|
||||||
|
has_unknown |= unknown_biomes;
|
||||||
|
|
||||||
|
section_map.insert(
|
||||||
|
SectionY(section.y),
|
||||||
|
(
|
||||||
|
loaded_section,
|
||||||
|
loaded_biomes,
|
||||||
BlockLight::new(block_light.as_deref()).with_context(|| {
|
BlockLight::new(block_light.as_deref()).with_context(|| {
|
||||||
format!("Failed to load section block light at Y={}", section.y)
|
format!("Failed to load section block light at Y={}", section.y)
|
||||||
})?,
|
})?,
|
||||||
|
@ -144,7 +143,8 @@ impl<'a> Chunk<'a> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Chunk::V1_18 { section_map })
|
let chunk = ChunkInner::V1_18 { section_map };
|
||||||
|
Ok((chunk, has_unknown))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [Chunk::new] implementation for all pre-1.18 chunk variants
|
/// [Chunk::new] implementation for all pre-1.18 chunk variants
|
||||||
|
@ -153,9 +153,10 @@ impl<'a> Chunk<'a> {
|
||||||
level: &'a de::LevelV0,
|
level: &'a de::LevelV0,
|
||||||
block_types: &'a BlockTypes,
|
block_types: &'a BlockTypes,
|
||||||
biome_types: &'a BiomeTypes,
|
biome_types: &'a BiomeTypes,
|
||||||
) -> Result<Self> {
|
) -> Result<(ChunkInner<'a>, bool)> {
|
||||||
let mut section_map_v1_13 = BTreeMap::new();
|
let mut section_map_v1_13 = BTreeMap::new();
|
||||||
let mut section_map_v0 = BTreeMap::new();
|
let mut section_map_v0 = BTreeMap::new();
|
||||||
|
let mut has_unknown = false;
|
||||||
|
|
||||||
for section in &level.sections {
|
for section in &level.sections {
|
||||||
let block_light =
|
let block_light =
|
||||||
|
@ -167,21 +168,13 @@ impl<'a> Chunk<'a> {
|
||||||
block_states,
|
block_states,
|
||||||
palette,
|
palette,
|
||||||
} => {
|
} => {
|
||||||
section_map_v1_13.insert(
|
let (loaded_section, unknown_blocks) =
|
||||||
SectionY(section.y.into()),
|
SectionV1_13::new(data_version, Some(block_states), palette, block_types)
|
||||||
(
|
.with_context(|| format!("Failed to load section at Y={}", section.y))?;
|
||||||
SectionV1_13::new(
|
has_unknown |= unknown_blocks;
|
||||||
data_version,
|
|
||||||
Some(block_states),
|
section_map_v1_13
|
||||||
palette,
|
.insert(SectionY(section.y.into()), (loaded_section, block_light));
|
||||||
block_types,
|
|
||||||
)
|
|
||||||
.with_context(|| {
|
|
||||||
format!("Failed to load section at Y={}", section.y)
|
|
||||||
})?,
|
|
||||||
block_light,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
de::SectionV0Variant::V0 { blocks, data } => {
|
de::SectionV0Variant::V0 { blocks, data } => {
|
||||||
section_map_v0.insert(
|
section_map_v0.insert(
|
||||||
|
@ -199,32 +192,31 @@ impl<'a> Chunk<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let biomes = BiomesV0::new(level.biomes.as_ref(), biome_types);
|
let biomes = BiomesV0::new(level.biomes.as_ref(), biome_types);
|
||||||
|
let chunk = match (section_map_v1_13.is_empty(), section_map_v0.is_empty()) {
|
||||||
Ok(
|
(true, true) => ChunkInner::Empty,
|
||||||
match (section_map_v1_13.is_empty(), section_map_v0.is_empty()) {
|
(false, true) => ChunkInner::V1_13 {
|
||||||
(true, true) => Chunk::Empty,
|
|
||||||
(false, true) => Chunk::V1_13 {
|
|
||||||
section_map: section_map_v1_13,
|
section_map: section_map_v1_13,
|
||||||
biomes: biomes?,
|
biomes: biomes?,
|
||||||
},
|
},
|
||||||
(true, false) => Chunk::V0 {
|
(true, false) => ChunkInner::V0 {
|
||||||
section_map: section_map_v0,
|
section_map: section_map_v0,
|
||||||
biomes: biomes?,
|
biomes: biomes?,
|
||||||
},
|
},
|
||||||
(false, false) => {
|
(false, false) => {
|
||||||
bail!("Mixed section versions");
|
bail!("Mixed section versions");
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
)
|
|
||||||
|
Ok((chunk, has_unknown))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the chunk does not contain any sections
|
/// Returns true if the chunk does not contain any sections
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
match self {
|
match &self.inner {
|
||||||
Chunk::V1_18 { section_map } => section_map.is_empty(),
|
ChunkInner::V1_18 { section_map } => section_map.is_empty(),
|
||||||
Chunk::V1_13 { section_map, .. } => section_map.is_empty(),
|
ChunkInner::V1_13 { section_map, .. } => section_map.is_empty(),
|
||||||
Chunk::V0 { section_map, .. } => section_map.is_empty(),
|
ChunkInner::V0 { section_map, .. } => section_map.is_empty(),
|
||||||
Chunk::Empty => true,
|
ChunkInner::Empty => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,28 +224,86 @@ impl<'a> Chunk<'a> {
|
||||||
pub fn sections(&self) -> SectionIter {
|
pub fn sections(&self) -> SectionIter {
|
||||||
use SectionIterInner::*;
|
use SectionIterInner::*;
|
||||||
SectionIter {
|
SectionIter {
|
||||||
inner: match self {
|
inner: match &self.inner {
|
||||||
Chunk::V1_18 { section_map } => V1_18 {
|
ChunkInner::V1_18 { section_map } => V1_18 {
|
||||||
iter: section_map.iter(),
|
iter: section_map.iter(),
|
||||||
},
|
},
|
||||||
Chunk::V1_13 {
|
ChunkInner::V1_13 {
|
||||||
section_map,
|
section_map,
|
||||||
biomes,
|
biomes,
|
||||||
} => V1_13 {
|
} => V1_13 {
|
||||||
iter: section_map.iter(),
|
iter: section_map.iter(),
|
||||||
biomes,
|
biomes,
|
||||||
},
|
},
|
||||||
Chunk::V0 {
|
ChunkInner::V0 {
|
||||||
section_map,
|
section_map,
|
||||||
biomes,
|
biomes,
|
||||||
} => V0 {
|
} => V0 {
|
||||||
iter: section_map.iter(),
|
iter: section_map.iter(),
|
||||||
biomes,
|
biomes,
|
||||||
},
|
},
|
||||||
Chunk::Empty => Empty,
|
ChunkInner::Empty => Empty,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the section at a [SectionY] coordinate
|
||||||
|
fn section_at(&self, y: SectionY) -> Option<&dyn Section> {
|
||||||
|
match &self.inner {
|
||||||
|
ChunkInner::V1_18 { section_map } => section_map
|
||||||
|
.get(&y)
|
||||||
|
.map(|(section, _, _)| -> &dyn Section { section }),
|
||||||
|
ChunkInner::V1_13 { section_map, .. } => section_map
|
||||||
|
.get(&y)
|
||||||
|
.map(|(section, _)| -> &dyn Section { section }),
|
||||||
|
ChunkInner::V0 { section_map, .. } => section_map
|
||||||
|
.get(&y)
|
||||||
|
.map(|(section, _)| -> &dyn Section { section }),
|
||||||
|
ChunkInner::Empty => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [BlockType] at a given coordinate
|
||||||
|
fn block_type_at(&self, y: SectionY, coords: SectionBlockCoords) -> Result<Option<&BlockType>> {
|
||||||
|
let Some(section) = self.section_at(y) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
section.block_at(coords)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [BlockType] at the coordinates of a [de::BlockEntity]
|
||||||
|
fn block_type_at_block_entity(
|
||||||
|
&self,
|
||||||
|
block_entity: &de::BlockEntity,
|
||||||
|
) -> Result<Option<&BlockType>> {
|
||||||
|
let x: BlockX = util::from_flat_coord(block_entity.x).2;
|
||||||
|
let z: BlockZ = util::from_flat_coord(block_entity.z).2;
|
||||||
|
let (section_y, block_y) = block_entity.y.shift_mask(BLOCK_BITS);
|
||||||
|
|
||||||
|
let coords = SectionBlockCoords {
|
||||||
|
xz: LayerBlockCoords { x, z },
|
||||||
|
y: BlockY::new(block_y),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.block_type_at(SectionY(section_y), coords)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes all of the chunk's block entities
|
||||||
|
pub fn block_entities(&self) -> Result<Vec<BlockEntity>> {
|
||||||
|
let entities: Vec<Option<BlockEntity>> = self
|
||||||
|
.block_entities
|
||||||
|
.iter()
|
||||||
|
.map(|block_entity| {
|
||||||
|
let block_type = self.block_type_at_block_entity(block_entity)?;
|
||||||
|
Ok(BlockEntity::new(
|
||||||
|
block_entity,
|
||||||
|
block_type,
|
||||||
|
self.data_version,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
|
Ok(entities.into_iter().flatten().collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reference to block, biome and block light data of a section
|
/// Reference to block, biome and block light data of a section
|
||||||
|
@ -283,6 +333,39 @@ impl<'a, T> SectionIterTrait<'a> for T where
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inner data structure of [SectionIter]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum SectionIterInner<'a> {
|
||||||
|
/// Iterator over sections of [ChunkInner::V1_18]
|
||||||
|
V1_18 {
|
||||||
|
/// Inner iterator into section map
|
||||||
|
iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BiomesV1_18<'a>, BlockLight<'a>)>,
|
||||||
|
},
|
||||||
|
/// Iterator over sections of [ChunkInner::V1_13]
|
||||||
|
V1_13 {
|
||||||
|
/// Inner iterator into section map
|
||||||
|
iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BlockLight<'a>)>,
|
||||||
|
/// Chunk biome data
|
||||||
|
biomes: &'a BiomesV0<'a>,
|
||||||
|
},
|
||||||
|
/// Iterator over sections of [ChunkInner::V0]
|
||||||
|
V0 {
|
||||||
|
/// Inner iterator into section map
|
||||||
|
iter: btree_map::Iter<'a, SectionY, (SectionV0<'a>, BlockLight<'a>)>,
|
||||||
|
/// Chunk biome data
|
||||||
|
biomes: &'a BiomesV0<'a>,
|
||||||
|
},
|
||||||
|
/// Empty iterator over an unpopulated chunk ([ChunkInner::Empty])
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over the sections of a [Chunk]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SectionIter<'a> {
|
||||||
|
/// Inner iterator enum
|
||||||
|
inner: SectionIterInner<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> SectionIter<'a> {
|
impl<'a> SectionIter<'a> {
|
||||||
/// Helper to run a closure on the inner section iterator
|
/// Helper to run a closure on the inner section iterator
|
||||||
fn with_iter<F, T>(&mut self, f: F) -> T
|
fn with_iter<F, T>(&mut self, f: F) -> T
|
||||||
|
@ -336,17 +419,17 @@ impl<'a> Iterator for SectionIter<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn last(mut self) -> Option<Self::Item> {
|
fn last(mut self) -> Option<Self::Item> {
|
||||||
self.with_iter(|iter| iter.last())
|
self.next_back()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> DoubleEndedIterator for SectionIter<'a> {
|
impl DoubleEndedIterator for SectionIter<'_> {
|
||||||
fn next_back(&mut self) -> Option<Self::Item> {
|
fn next_back(&mut self) -> Option<Self::Item> {
|
||||||
self.with_iter(|iter| iter.next_back())
|
self.with_iter(|iter| iter.next_back())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ExactSizeIterator for SectionIter<'a> {
|
impl ExactSizeIterator for SectionIter<'_> {
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
SectionIterInner::V1_18 { iter } => iter.len(),
|
SectionIterInner::V1_18 { iter } => iter.len(),
|
||||||
|
@ -357,4 +440,4 @@ impl<'a> ExactSizeIterator for SectionIter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FusedIterator for SectionIter<'a> {}
|
impl FusedIterator for SectionIter<'_> {}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::text_value::TextValue;
|
||||||
|
|
||||||
/// Element of the `palette` list of 1.18+ [block states](BlockStatesV1_18)
|
/// Element of the `palette` list of 1.18+ [block states](BlockStatesV1_18)
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
@ -104,6 +106,77 @@ pub enum BiomesV0 {
|
||||||
ByteArray(fastnbt::ByteArray),
|
ByteArray(fastnbt::ByteArray),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Front/back text of a Minecraft 1.20+ sign block entry
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct BlockEntitySignV1_20Text {
|
||||||
|
/// Lines of sign text
|
||||||
|
pub messages: Vec<TextValue>,
|
||||||
|
/// Default text color
|
||||||
|
pub color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A sign (standing or hanging) block entity
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum BlockEntitySign {
|
||||||
|
/// Pre-1.20 sign block entity
|
||||||
|
///
|
||||||
|
/// Pre-1.20 signs only have front text.
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
V0 {
|
||||||
|
/// Line 1 of the sign text
|
||||||
|
text1: TextValue,
|
||||||
|
/// Line 2 of the sign text
|
||||||
|
text2: TextValue,
|
||||||
|
/// Line 3 of the sign text
|
||||||
|
text3: TextValue,
|
||||||
|
/// Line 4 of the sign text
|
||||||
|
text4: TextValue,
|
||||||
|
/// Default text color
|
||||||
|
color: Option<String>,
|
||||||
|
},
|
||||||
|
/// 1.20+ sign block entity
|
||||||
|
V1_20 {
|
||||||
|
/// The sign's front text
|
||||||
|
front_text: BlockEntitySignV1_20Text,
|
||||||
|
/// The sign's back text
|
||||||
|
back_text: BlockEntitySignV1_20Text,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data for different kinds of block entities
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(tag = "id")]
|
||||||
|
pub enum BlockEntityData {
|
||||||
|
/// Regular sign
|
||||||
|
///
|
||||||
|
/// This includes standing signs and signs attached to the side of blocks
|
||||||
|
#[serde(rename = "minecraft:sign", alias = "minecraft:standing_sign")]
|
||||||
|
Sign(BlockEntitySign),
|
||||||
|
/// Hanging sign
|
||||||
|
#[serde(rename = "minecraft:hanging_sign")]
|
||||||
|
HangingSign(BlockEntitySign),
|
||||||
|
/// Other block entity types not handled by MinedMap
|
||||||
|
#[serde(other)]
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A block entity
|
||||||
|
///
|
||||||
|
/// Block entities were called tile entities pre-1.18
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct BlockEntity {
|
||||||
|
/// Entity global X coordinate
|
||||||
|
pub x: i32,
|
||||||
|
/// Entity global Y coordinate
|
||||||
|
pub y: i32,
|
||||||
|
/// Entity global Z coordinate
|
||||||
|
pub z: i32,
|
||||||
|
/// Kind-specific entity data
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub data: BlockEntityData,
|
||||||
|
}
|
||||||
|
|
||||||
/// `Level` compound element found in pre-1.18 [chunks](Chunk)
|
/// `Level` compound element found in pre-1.18 [chunks](Chunk)
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
@ -113,6 +186,9 @@ pub struct LevelV0 {
|
||||||
pub sections: Vec<SectionV0>,
|
pub sections: Vec<SectionV0>,
|
||||||
/// Biome data
|
/// Biome data
|
||||||
pub biomes: Option<BiomesV0>,
|
pub biomes: Option<BiomesV0>,
|
||||||
|
/// List of block entities
|
||||||
|
#[serde(default)]
|
||||||
|
pub tile_entities: Vec<BlockEntity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Version-specific part of a [Chunk] compound
|
/// Version-specific part of a [Chunk] compound
|
||||||
|
@ -123,6 +199,9 @@ pub enum ChunkVariant {
|
||||||
V1_18 {
|
V1_18 {
|
||||||
/// List of chunk sections
|
/// List of chunk sections
|
||||||
sections: Vec<SectionV1_18>,
|
sections: Vec<SectionV1_18>,
|
||||||
|
/// List of block entities
|
||||||
|
#[serde(default)]
|
||||||
|
block_entities: Vec<BlockEntity>,
|
||||||
},
|
},
|
||||||
/// Pre-1.18 chunk data
|
/// Pre-1.18 chunk data
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
|
|
@ -3,17 +3,17 @@
|
||||||
use std::num::NonZeroU16;
|
use std::num::NonZeroU16;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::chunk::{Chunk, SectionIterItem};
|
use super::chunk::{Chunk, SectionIterItem};
|
||||||
use crate::{
|
use crate::{
|
||||||
resource::{Biome, BlockFlag, BlockType},
|
resource::{Biome, BlockColor, BlockFlag},
|
||||||
types::*,
|
types::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Height (Y coordinate) of a block
|
/// Height (Y coordinate) of a block
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
|
||||||
pub struct BlockHeight(pub i32);
|
pub struct BlockHeight(pub i32);
|
||||||
|
|
||||||
impl BlockHeight {
|
impl BlockHeight {
|
||||||
|
@ -31,8 +31,8 @@ impl BlockHeight {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Array optionally storing a [BlockType] for each coordinate of a chunk
|
/// Array optionally storing a [BlockColor] for each coordinate of a chunk
|
||||||
pub type BlockArray = LayerBlockArray<Option<BlockType>>;
|
pub type BlockArray = LayerBlockArray<Option<BlockColor>>;
|
||||||
|
|
||||||
/// Array optionally storing a biome index for each coordinate of a chunk
|
/// Array optionally storing a biome index for each coordinate of a chunk
|
||||||
///
|
///
|
||||||
|
@ -49,7 +49,7 @@ pub type DepthArray = LayerBlockArray<Option<BlockHeight>>;
|
||||||
/// References to LayerData entries for a single coordinate pair
|
/// References to LayerData entries for a single coordinate pair
|
||||||
struct LayerEntry<'a> {
|
struct LayerEntry<'a> {
|
||||||
/// The block type of the referenced entry
|
/// The block type of the referenced entry
|
||||||
block: &'a mut Option<BlockType>,
|
block: &'a mut Option<BlockColor>,
|
||||||
/// The biome type of the referenced entry
|
/// The biome type of the referenced entry
|
||||||
biome: &'a mut Option<NonZeroU16>,
|
biome: &'a mut Option<NonZeroU16>,
|
||||||
/// The block light of the referenced entry
|
/// The block light of the referenced entry
|
||||||
|
@ -58,7 +58,7 @@ struct LayerEntry<'a> {
|
||||||
depth: &'a mut Option<BlockHeight>,
|
depth: &'a mut Option<BlockHeight>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> LayerEntry<'a> {
|
impl LayerEntry<'_> {
|
||||||
/// Returns true if the entry has not been filled yet (no opaque block has been encountered)
|
/// Returns true if the entry has not been filled yet (no opaque block has been encountered)
|
||||||
///
|
///
|
||||||
/// The depth value is filled separately when a non-water block is encountered after the block type
|
/// The depth value is filled separately when a non-water block is encountered after the block type
|
||||||
|
@ -86,7 +86,7 @@ impl<'a> LayerEntry<'a> {
|
||||||
let Some(block_type) = section
|
let Some(block_type) = section
|
||||||
.section
|
.section
|
||||||
.block_at(coords)?
|
.block_at(coords)?
|
||||||
.filter(|block_type| block_type.is(BlockFlag::Opaque))
|
.filter(|block_type| block_type.block_color.is(BlockFlag::Opaque))
|
||||||
else {
|
else {
|
||||||
if self.is_empty() {
|
if self.is_empty() {
|
||||||
*self.block_light = section.block_light.block_light_at(coords);
|
*self.block_light = section.block_light.block_light_at(coords);
|
||||||
|
@ -96,8 +96,9 @@ impl<'a> LayerEntry<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.is_empty() {
|
if self.is_empty() {
|
||||||
*self.block = Some(block_type);
|
*self.block = Some(block_type.block_color);
|
||||||
if let Some(biome) = section.biomes.biome_at(section.y, coords)? {
|
|
||||||
|
let biome = section.biomes.biome_at(section.y, coords)?;
|
||||||
let (biome_index, _) = biome_list.insert_full(*biome);
|
let (biome_index, _) = biome_list.insert_full(*biome);
|
||||||
*self.biome = NonZeroU16::new(
|
*self.biome = NonZeroU16::new(
|
||||||
(biome_index + 1)
|
(biome_index + 1)
|
||||||
|
@ -105,9 +106,8 @@ impl<'a> LayerEntry<'a> {
|
||||||
.expect("biome index not in range"),
|
.expect("biome index not in range"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if block_type.is(BlockFlag::Water) {
|
if block_type.block_color.is(BlockFlag::Water) {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
//! Data structures describing Minecraft save data
|
//! Data structures describing Minecraft save data
|
||||||
|
|
||||||
|
pub mod block_entity;
|
||||||
pub mod chunk;
|
pub mod chunk;
|
||||||
pub mod de;
|
pub mod de;
|
||||||
pub mod layer;
|
pub mod layer;
|
||||||
pub mod section;
|
pub mod section;
|
||||||
|
pub mod sign;
|
||||||
|
pub mod text_value;
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use num_integer::div_rem;
|
use num_integer::div_rem;
|
||||||
use tracing::warn;
|
use tracing::debug;
|
||||||
|
|
||||||
use super::de;
|
use super::de;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -44,7 +44,7 @@ fn palette_bits(len: usize, min: u8, max: u8) -> Option<u8> {
|
||||||
/// Trait for common functions of [SectionV1_13] and [SectionV0]
|
/// Trait for common functions of [SectionV1_13] and [SectionV0]
|
||||||
pub trait Section: Debug {
|
pub trait Section: Debug {
|
||||||
/// Returns the [BlockType] at a coordinate tuple inside the section
|
/// Returns the [BlockType] at a coordinate tuple inside the section
|
||||||
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<BlockType>>;
|
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<&BlockType>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minecraft v1.13+ section block data
|
/// Minecraft v1.13+ section block data
|
||||||
|
@ -53,7 +53,7 @@ pub struct SectionV1_13<'a> {
|
||||||
/// Packed block type data
|
/// Packed block type data
|
||||||
block_states: Option<&'a [i64]>,
|
block_states: Option<&'a [i64]>,
|
||||||
/// List of block types indexed by entries encoded in *block_states*
|
/// List of block types indexed by entries encoded in *block_states*
|
||||||
palette: Vec<Option<BlockType>>,
|
palette: Vec<Option<&'a BlockType>>,
|
||||||
/// Number of bits per block in *block_states*
|
/// Number of bits per block in *block_states*
|
||||||
bits: u8,
|
bits: u8,
|
||||||
/// Set to true if packed block entries in *block_states* are aligned to i64
|
/// Set to true if packed block entries in *block_states* are aligned to i64
|
||||||
|
@ -73,7 +73,7 @@ impl<'a> SectionV1_13<'a> {
|
||||||
block_states: Option<&'a [i64]>,
|
block_states: Option<&'a [i64]>,
|
||||||
palette: &'a [de::BlockStatePaletteEntry],
|
palette: &'a [de::BlockStatePaletteEntry],
|
||||||
block_types: &'a BlockTypes,
|
block_types: &'a BlockTypes,
|
||||||
) -> Result<Self> {
|
) -> Result<(Self, bool)> {
|
||||||
let aligned_blocks = data_version >= 2529;
|
let aligned_blocks = data_version >= 2529;
|
||||||
|
|
||||||
let bits = palette_bits(palette.len(), 4, 12).context("Unsupported block palette size")?;
|
let bits = palette_bits(palette.len(), 4, 12).context("Unsupported block palette size")?;
|
||||||
|
@ -81,7 +81,7 @@ impl<'a> SectionV1_13<'a> {
|
||||||
if let Some(block_states) = block_states {
|
if let Some(block_states) = block_states {
|
||||||
let expected_length = if aligned_blocks {
|
let expected_length = if aligned_blocks {
|
||||||
let blocks_per_word = 64 / bits as usize;
|
let blocks_per_word = 64 / bits as usize;
|
||||||
(4096 + blocks_per_word - 1) / blocks_per_word
|
4096usize.div_ceil(blocks_per_word)
|
||||||
} else {
|
} else {
|
||||||
64 * bits as usize
|
64 * bits as usize
|
||||||
};
|
};
|
||||||
|
@ -90,23 +90,29 @@ impl<'a> SectionV1_13<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut has_unknown = false;
|
||||||
|
|
||||||
let palette_types = palette
|
let palette_types = palette
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| {
|
.map(|entry| {
|
||||||
let block_type = block_types.get(&entry.name);
|
let block_type = block_types.get(&entry.name);
|
||||||
if block_type.is_none() {
|
if block_type.is_none() {
|
||||||
warn!("Unknown block type: {}", entry.name);
|
debug!("Unknown block type: {}", entry.name);
|
||||||
|
has_unknown = true;
|
||||||
}
|
}
|
||||||
block_type
|
block_type
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Self {
|
Ok((
|
||||||
|
Self {
|
||||||
block_states,
|
block_states,
|
||||||
palette: palette_types,
|
palette: palette_types,
|
||||||
bits,
|
bits,
|
||||||
aligned_blocks,
|
aligned_blocks,
|
||||||
})
|
},
|
||||||
|
has_unknown,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Looks up the block type palette index at the given coordinates
|
/// Looks up the block type palette index at the given coordinates
|
||||||
|
@ -139,8 +145,8 @@ impl<'a> SectionV1_13<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Section for SectionV1_13<'a> {
|
impl Section for SectionV1_13<'_> {
|
||||||
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<BlockType>> {
|
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<&BlockType>> {
|
||||||
let index = self.palette_index_at(coords);
|
let index = self.palette_index_at(coords);
|
||||||
Ok(*self
|
Ok(*self
|
||||||
.palette
|
.palette
|
||||||
|
@ -182,8 +188,8 @@ impl<'a> SectionV0<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Section for SectionV0<'a> {
|
impl Section for SectionV0<'_> {
|
||||||
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<BlockType>> {
|
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<&BlockType>> {
|
||||||
let offset = coords.offset();
|
let offset = coords.offset();
|
||||||
let block = self.blocks[offset] as u8;
|
let block = self.blocks[offset] as u8;
|
||||||
|
|
||||||
|
@ -202,7 +208,7 @@ impl<'a> Section for SectionV0<'a> {
|
||||||
/// Trait for common functions of [BiomesV1_18] and [BiomesV0]
|
/// Trait for common functions of [BiomesV1_18] and [BiomesV0]
|
||||||
pub trait Biomes: Debug {
|
pub trait Biomes: Debug {
|
||||||
/// Returns the [Biome] at a coordinate tuple inside the chunk
|
/// Returns the [Biome] at a coordinate tuple inside the chunk
|
||||||
fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<Option<&Biome>>;
|
fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<&Biome>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minecraft v1.18+ section biome data
|
/// Minecraft v1.18+ section biome data
|
||||||
|
@ -220,7 +226,7 @@ pub struct BiomesV1_18<'a> {
|
||||||
/// to whole i64 values.
|
/// to whole i64 values.
|
||||||
biomes: Option<&'a [i64]>,
|
biomes: Option<&'a [i64]>,
|
||||||
/// Biome palette indexed by entries encoded in *biomes*
|
/// Biome palette indexed by entries encoded in *biomes*
|
||||||
palette: Vec<Option<&'a Biome>>,
|
palette: Vec<&'a Biome>,
|
||||||
/// Number of bits used for each entry in *biomes*
|
/// Number of bits used for each entry in *biomes*
|
||||||
bits: u8,
|
bits: u8,
|
||||||
}
|
}
|
||||||
|
@ -231,33 +237,38 @@ impl<'a> BiomesV1_18<'a> {
|
||||||
biomes: Option<&'a [i64]>,
|
biomes: Option<&'a [i64]>,
|
||||||
palette: &'a [String],
|
palette: &'a [String],
|
||||||
biome_types: &'a BiomeTypes,
|
biome_types: &'a BiomeTypes,
|
||||||
) -> Result<Self> {
|
) -> Result<(Self, bool)> {
|
||||||
let bits = palette_bits(palette.len(), 1, 6).context("Unsupported block palette size")?;
|
let bits = palette_bits(palette.len(), 1, 6).context("Unsupported block palette size")?;
|
||||||
|
|
||||||
if let Some(biomes) = biomes {
|
if let Some(biomes) = biomes {
|
||||||
let biomes_per_word = 64 / bits as usize;
|
let biomes_per_word = 64 / bits as usize;
|
||||||
let expected_length = (64 + biomes_per_word - 1) / biomes_per_word;
|
let expected_length = 64usize.div_ceil(biomes_per_word);
|
||||||
if biomes.len() != expected_length {
|
if biomes.len() != expected_length {
|
||||||
bail!("Invalid section biome data");
|
bail!("Invalid section biome data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut has_unknown = false;
|
||||||
|
|
||||||
let palette_types = palette
|
let palette_types = palette
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| {
|
.map(|entry| {
|
||||||
let biome_type = biome_types.get(entry);
|
biome_types.get(entry).unwrap_or_else(|| {
|
||||||
if biome_type.is_none() {
|
debug!("Unknown biome type: {}", entry);
|
||||||
warn!("Unknown biome type: {}", entry);
|
has_unknown = true;
|
||||||
}
|
biome_types.get_fallback()
|
||||||
biome_type
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(BiomesV1_18 {
|
Ok((
|
||||||
|
BiomesV1_18 {
|
||||||
biomes,
|
biomes,
|
||||||
palette: palette_types,
|
palette: palette_types,
|
||||||
bits,
|
bits,
|
||||||
})
|
},
|
||||||
|
has_unknown,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Looks up the block type palette index at the given coordinates
|
/// Looks up the block type palette index at the given coordinates
|
||||||
|
@ -282,8 +293,8 @@ impl<'a> BiomesV1_18<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Biomes for BiomesV1_18<'a> {
|
impl Biomes for BiomesV1_18<'_> {
|
||||||
fn biome_at(&self, _section: SectionY, coords: SectionBlockCoords) -> Result<Option<&Biome>> {
|
fn biome_at(&self, _section: SectionY, coords: SectionBlockCoords) -> Result<&Biome> {
|
||||||
let index = self.palette_index_at(coords);
|
let index = self.palette_index_at(coords);
|
||||||
Ok(*self
|
Ok(*self
|
||||||
.palette
|
.palette
|
||||||
|
@ -337,8 +348,8 @@ impl<'a> BiomesV0<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Biomes for BiomesV0<'a> {
|
impl Biomes for BiomesV0<'_> {
|
||||||
fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<Option<&Biome>> {
|
fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<&Biome> {
|
||||||
let id = match self.data {
|
let id = match self.data {
|
||||||
BiomesV0Data::IntArrayV15(data) => {
|
BiomesV0Data::IntArrayV15(data) => {
|
||||||
let LayerBlockCoords { x, z } = coords.xz;
|
let LayerBlockCoords { x, z } = coords.xz;
|
||||||
|
@ -358,7 +369,10 @@ impl<'a> Biomes for BiomesV0<'a> {
|
||||||
}
|
}
|
||||||
BiomesV0Data::ByteArray(data) => data[coords.xz.offset()] as u8,
|
BiomesV0Data::ByteArray(data) => data[coords.xz.offset()] as u8,
|
||||||
};
|
};
|
||||||
Ok(self.biome_types.get_legacy(id))
|
Ok(self
|
||||||
|
.biome_types
|
||||||
|
.get_legacy(id)
|
||||||
|
.unwrap_or(self.biome_types.get_fallback()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,10 +400,6 @@ impl<'a> BlockLight<'a> {
|
||||||
let (offset, nibble) = div_rem(coords.offset(), 2);
|
let (offset, nibble) = div_rem(coords.offset(), 2);
|
||||||
let byte = block_light[offset] as u8;
|
let byte = block_light[offset] as u8;
|
||||||
|
|
||||||
if nibble == 1 {
|
if nibble == 1 { byte >> 4 } else { byte & 0xf }
|
||||||
byte >> 4
|
|
||||||
} else {
|
|
||||||
byte & 0xf
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
158
src/world/sign.rs
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
//! Processing of sign text
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
|
use minedmap_resource::Color;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
de,
|
||||||
|
text_value::{FormattedText, FormattedTextList, TextValue},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Version-independent reference to (front or back) sign text
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct RawSignText<'a> {
|
||||||
|
/// Lines of sign text
|
||||||
|
///
|
||||||
|
/// A regular sign always has 4 lines of text. The back of pre-1.20
|
||||||
|
/// signs is represented as a [SignText] without any `messages`.
|
||||||
|
pub messages: Vec<&'a TextValue>,
|
||||||
|
/// Sign color
|
||||||
|
///
|
||||||
|
/// Defaults to "black".
|
||||||
|
pub color: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The color to use for signs without a color attribute ("black")
|
||||||
|
const DEFAULT_COLOR: Color = Color([0, 0, 0]);
|
||||||
|
|
||||||
|
/// Map of text colors associated with dyes (except for black)
|
||||||
|
static DYE_COLORS: phf::Map<&'static str, Color> = phf::phf_map! {
|
||||||
|
"white" => Color([255, 255, 255]),
|
||||||
|
"orange" => Color([255, 104, 31]),
|
||||||
|
"magenta" => Color([255, 0, 255]),
|
||||||
|
"light_blue" => Color([154, 192, 205]),
|
||||||
|
"yellow" => Color([255, 255, 0]),
|
||||||
|
"lime" => Color([191, 255, 0]),
|
||||||
|
"pink" => Color([255, 105, 180]),
|
||||||
|
"gray" => Color([128, 128, 128]),
|
||||||
|
"light_gray" => Color([211, 211, 211]),
|
||||||
|
"cyan" => Color([0, 255, 255]),
|
||||||
|
"purple" => Color([160, 32, 240]),
|
||||||
|
"blue" => Color([0, 0, 255]),
|
||||||
|
"brown" => Color([139, 69, 19]),
|
||||||
|
"green" => Color([0, 255, 0]),
|
||||||
|
"red" => Color([255, 0, 0]),
|
||||||
|
};
|
||||||
|
|
||||||
|
impl RawSignText<'_> {
|
||||||
|
/// Decodes the [RawSignText] into a [SignText]
|
||||||
|
pub fn decode(&self, data_version: u32) -> SignText {
|
||||||
|
let color = self
|
||||||
|
.color
|
||||||
|
.map(|c| DYE_COLORS.get(c).copied().unwrap_or(DEFAULT_COLOR));
|
||||||
|
let parent = FormattedText {
|
||||||
|
color,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
SignText(
|
||||||
|
self.messages
|
||||||
|
.iter()
|
||||||
|
.map(|message| message.deserialize(data_version).linearize(&parent))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a de::BlockEntitySignV1_20Text> for RawSignText<'a> {
|
||||||
|
fn from(value: &'a de::BlockEntitySignV1_20Text) -> Self {
|
||||||
|
RawSignText {
|
||||||
|
messages: value.messages.iter().collect(),
|
||||||
|
color: value.color.as_deref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper methods for [de::BlockEntitySign]
|
||||||
|
pub trait BlockEntitySignExt {
|
||||||
|
/// Returns the front and back text of a sign in a version-indepentent format
|
||||||
|
fn text(&self) -> (RawSignText, RawSignText);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockEntitySignExt for de::BlockEntitySign {
|
||||||
|
fn text(&self) -> (RawSignText, RawSignText) {
|
||||||
|
match self {
|
||||||
|
de::BlockEntitySign::V0 {
|
||||||
|
text1,
|
||||||
|
text2,
|
||||||
|
text3,
|
||||||
|
text4,
|
||||||
|
color,
|
||||||
|
} => (
|
||||||
|
RawSignText {
|
||||||
|
messages: vec![text1, text2, text3, text4],
|
||||||
|
color: color.as_deref(),
|
||||||
|
},
|
||||||
|
Default::default(),
|
||||||
|
),
|
||||||
|
de::BlockEntitySign::V1_20 {
|
||||||
|
front_text,
|
||||||
|
back_text,
|
||||||
|
} => (front_text.into(), back_text.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)]
|
||||||
|
/// Deserialized and linearized sign text
|
||||||
|
pub struct SignText(pub Vec<FormattedTextList>);
|
||||||
|
|
||||||
|
impl SignText {
|
||||||
|
/// Checks if all lines of the sign text are empty
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.0.iter().all(|line| line.is_empty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SignText {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut iter = self.0.iter();
|
||||||
|
|
||||||
|
let Some(first) = iter.next() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
first.fmt(f)?;
|
||||||
|
|
||||||
|
for text in iter {
|
||||||
|
f.write_str("\n")?;
|
||||||
|
text.fmt(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn formatted_text(text: &str) -> FormattedText {
|
||||||
|
FormattedText {
|
||||||
|
text: text.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_text_display() {
|
||||||
|
let sign_text = SignText(vec![
|
||||||
|
FormattedTextList(vec![formatted_text("a"), formatted_text("b")]),
|
||||||
|
FormattedTextList(vec![formatted_text("c")]),
|
||||||
|
FormattedTextList(vec![formatted_text("d")]),
|
||||||
|
FormattedTextList(vec![formatted_text("e")]),
|
||||||
|
]);
|
||||||
|
assert_eq!("ab\nc\nd\ne", sign_text.to_string());
|
||||||
|
}
|
||||||
|
}
|
274
src/world/text_value.rs
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
//! Newtype and helper methods for handling Minecraft text values
|
||||||
|
|
||||||
|
use std::{collections::VecDeque, fmt::Display};
|
||||||
|
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
|
use minedmap_resource::Color;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A span of formatted text
|
||||||
|
///
|
||||||
|
/// A [TextValue] consists of a tree of [FormattedText] nodes (canonically
|
||||||
|
/// represented as a [FormattedTextTree], but other kinds are possible with
|
||||||
|
/// is handled by [DeserializedText].
|
||||||
|
///
|
||||||
|
/// Formatting that is not set in a node is inherited from the parent.
|
||||||
|
#[derive(
|
||||||
|
Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Encode, Decode,
|
||||||
|
)]
|
||||||
|
pub struct FormattedText {
|
||||||
|
#[serde(default)]
|
||||||
|
/// Text content
|
||||||
|
pub text: String,
|
||||||
|
/// Text color
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none", with = "text_color")]
|
||||||
|
pub color: Option<Color>,
|
||||||
|
/// Bold formatting
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub bold: Option<bool>,
|
||||||
|
/// Italic formatting
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub italic: Option<bool>,
|
||||||
|
/// Underlines formatting
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub underlined: Option<bool>,
|
||||||
|
/// Strikethrough formatting
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub strikethrough: Option<bool>,
|
||||||
|
/// Obfuscated formatting
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub obfuscated: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormattedText {
|
||||||
|
/// Fills in unset formatting fields from a parent node
|
||||||
|
pub fn inherit(self, parent: &Self) -> Self {
|
||||||
|
FormattedText {
|
||||||
|
text: self.text,
|
||||||
|
color: self.color.or(parent.color),
|
||||||
|
bold: self.bold.or(parent.bold),
|
||||||
|
italic: self.italic.or(parent.italic),
|
||||||
|
underlined: self.underlined.or(parent.underlined),
|
||||||
|
strikethrough: self.strikethrough.or(parent.strikethrough),
|
||||||
|
obfuscated: self.obfuscated.or(parent.obfuscated),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FormattedText {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.text.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tree of [FormattedText] nodes
|
||||||
|
///
|
||||||
|
/// Each node including the root has a `text` and a list of children (`extra`).
|
||||||
|
#[derive(Debug, Deserialize, Default)]
|
||||||
|
pub struct FormattedTextTree {
|
||||||
|
/// Root node content
|
||||||
|
#[serde(flatten)]
|
||||||
|
text: FormattedText,
|
||||||
|
/// List of child trees
|
||||||
|
#[serde(default)]
|
||||||
|
extra: VecDeque<DeserializedText>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for FormattedTextTree {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
FormattedTextTree {
|
||||||
|
text: FormattedText {
|
||||||
|
text: value,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
extra: VecDeque::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List of [FormattedText]
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Encode, Decode)]
|
||||||
|
pub struct FormattedTextList(pub Vec<FormattedText>);
|
||||||
|
|
||||||
|
impl FormattedTextList {
|
||||||
|
/// Returns `true` when [FormattedTextList] does not contain any text
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.0.iter().all(|text| text.text.is_empty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FormattedTextList {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
for text in &self.0 {
|
||||||
|
text.fmt(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raw deserialized [TextValue]
|
||||||
|
///
|
||||||
|
/// A [TextValue] can contain various different types serialized as JSON or NBT.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum DeserializedText {
|
||||||
|
/// Unformatted string
|
||||||
|
String(String),
|
||||||
|
/// Unformatted number (will be converted to a string)
|
||||||
|
Number(f32),
|
||||||
|
/// Unformatted boolean (will be converted to a string)
|
||||||
|
Boolean(bool),
|
||||||
|
/// List of [DeserializedText]
|
||||||
|
///
|
||||||
|
/// The tail elements are appended as children of the head element.
|
||||||
|
List(VecDeque<DeserializedText>),
|
||||||
|
/// The canonical [FormattedTextTree] structure
|
||||||
|
Object(FormattedTextTree),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeserializedText {
|
||||||
|
/// Converts a [DeserializedText] into the regular [FormattedTextTree] format
|
||||||
|
///
|
||||||
|
/// Most variants are simply converted to strings. A list is handled by
|
||||||
|
/// appending all tail elements to the `extra` field of the head.
|
||||||
|
pub fn canonicalize(self) -> FormattedTextTree {
|
||||||
|
match self {
|
||||||
|
DeserializedText::Object(obj) => obj,
|
||||||
|
DeserializedText::String(s) => FormattedTextTree::from(s),
|
||||||
|
DeserializedText::Number(n) => FormattedTextTree::from(n.to_string()),
|
||||||
|
DeserializedText::Boolean(b) => FormattedTextTree::from(b.to_string()),
|
||||||
|
DeserializedText::List(mut list) => {
|
||||||
|
let mut obj = list
|
||||||
|
.pop_front()
|
||||||
|
.map(|t| t.canonicalize())
|
||||||
|
.unwrap_or_default();
|
||||||
|
obj.extra.append(&mut list);
|
||||||
|
obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the tree of [FormattedText] nodes into a linear list by
|
||||||
|
/// copying formatting flags into each node.
|
||||||
|
pub fn linearize(self, parent: &FormattedText) -> FormattedTextList {
|
||||||
|
let obj = self.canonicalize();
|
||||||
|
let mut ret = vec![obj.text.inherit(parent)];
|
||||||
|
|
||||||
|
for extra in obj.extra {
|
||||||
|
ret.append(&mut extra.linearize(&ret[0]).0);
|
||||||
|
}
|
||||||
|
|
||||||
|
FormattedTextList(ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DeserializedText {
|
||||||
|
fn default() -> Self {
|
||||||
|
DeserializedText::Object(FormattedTextTree::from(String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minecraft raw text value
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct TextValue(pub fastnbt::Value);
|
||||||
|
|
||||||
|
impl TextValue {
|
||||||
|
/// Deserializes a [TextValue] into a [DeserializedText]
|
||||||
|
pub fn deserialize(&self, data_version: u32) -> DeserializedText {
|
||||||
|
// TODO: Improve error handling
|
||||||
|
//
|
||||||
|
// Unfortunately, there are a number of weird ways an empty sign coould
|
||||||
|
// be encoded (for example a compound with an "" key), so for now we
|
||||||
|
// simply interpret undecodable data as empty.
|
||||||
|
if data_version < 4290 {
|
||||||
|
let fastnbt::Value::String(json) = &self.0 else {
|
||||||
|
return DeserializedText::default();
|
||||||
|
};
|
||||||
|
|
||||||
|
serde_json::from_str(json).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
fastnbt::from_value(&self.0).unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod text_color {
|
||||||
|
//! Helpers for serializing and deserializing [FormattedText](super::FormattedText) colors
|
||||||
|
|
||||||
|
use minedmap_resource::Color;
|
||||||
|
use serde::{
|
||||||
|
Deserializer, Serializer,
|
||||||
|
de::{self, Visitor},
|
||||||
|
ser::Error as _,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Named text colors
|
||||||
|
static COLORS: phf::Map<&'static str, Color> = phf::phf_map! {
|
||||||
|
"black" => Color([0x00, 0x00, 0x00]),
|
||||||
|
"dark_blue" => Color([0x00, 0x00, 0xAA]),
|
||||||
|
"dark_green" => Color([0x00, 0xAA, 0x00]),
|
||||||
|
"dark_aqua" => Color([0x00, 0xAA, 0xAA]),
|
||||||
|
"dark_red" => Color([0xAA, 0x00, 0x00]),
|
||||||
|
"dark_purple" => Color([0xAA, 0x00, 0xAA]),
|
||||||
|
"gold" => Color([0xFF, 0xAA, 0x00]),
|
||||||
|
"gray" => Color([0xAA, 0xAA, 0xAA]),
|
||||||
|
"dark_gray" => Color([0x55, 0x55, 0x55]),
|
||||||
|
"blue" => Color([0x55, 0x55, 0xFF]),
|
||||||
|
"green" => Color([0x55, 0xFF, 0x55]),
|
||||||
|
"aqua" => Color([0x55, 0xFF, 0xFF]),
|
||||||
|
"red" => Color([0xFF, 0x55, 0x55]),
|
||||||
|
"light_purple" => Color([0xFF, 0x55, 0xFF]),
|
||||||
|
"yellow" => Color([0xFF, 0xFF, 0x55]),
|
||||||
|
"white" => Color([0xFF, 0xFF, 0xFF]),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// serde serialize function for [FormattedText::color](super::FormattedText::color)
|
||||||
|
pub fn serialize<S>(color: &Option<Color>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let &Some(color) = color else {
|
||||||
|
return Err(S::Error::custom("serialize called for None sign color"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = format!("#{:02x}{:02x}{:02x}", color.0[0], color.0[1], color.0[2]);
|
||||||
|
serializer.serialize_str(&text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// serde [Visitor] for use by [deserialize]
|
||||||
|
struct ColorVisitor;
|
||||||
|
|
||||||
|
impl Visitor<'_> for ColorVisitor {
|
||||||
|
type Value = Option<Color>;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a string representing a color")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, color: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
if let Some(hex) = color.strip_prefix("#") {
|
||||||
|
if let Ok(value) = u32::from_str_radix(hex, 16) {
|
||||||
|
return Ok(Some(Color([
|
||||||
|
(value >> 16) as u8,
|
||||||
|
(value >> 8) as u8,
|
||||||
|
value as u8,
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(COLORS.get(color).copied())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// serde deserialize function for [FormattedText::color](super::FormattedText::color)
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_str(ColorVisitor)
|
||||||
|
}
|
||||||
|
}
|
1
viewer/.dockerignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/data
|
3
viewer/Dockerfile
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
FROM docker.io/library/nginx:alpine-slim
|
||||||
|
COPY . /usr/share/nginx/html
|
||||||
|
# datadir should be mounted to: /usr/share/nginx/html/data
|
|
@ -17,8 +17,63 @@ function contains(array, elem) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const signKinds = {
|
||||||
|
sign: {
|
||||||
|
iconSize: [26, 28],
|
||||||
|
popupAnchor: [0, -20],
|
||||||
|
},
|
||||||
|
wall_sign: {
|
||||||
|
iconSize: [26, 18],
|
||||||
|
popupAnchor: [0, -15],
|
||||||
|
},
|
||||||
|
hanging_sign: {
|
||||||
|
iconSize: [28, 24],
|
||||||
|
popupAnchor: [0, -18],
|
||||||
|
},
|
||||||
|
hanging_wall_sign: {
|
||||||
|
iconSize: [28, 28],
|
||||||
|
popupAnchor: [0, -20],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
const signIcons = {};
|
||||||
|
const markers = {};
|
||||||
|
|
||||||
|
let updateHash = () => {};
|
||||||
|
|
||||||
|
function coordKey(coords) {
|
||||||
|
if (!coords)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return `${coords[0]},${coords[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarker(coords) {
|
||||||
|
return markers[coordKey(coords)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function signIcon(material, kind) {
|
||||||
|
function createSignIcon(material, kind) {
|
||||||
|
const {iconSize, popupAnchor} = signKinds[kind];
|
||||||
|
|
||||||
|
return L.icon({
|
||||||
|
iconUrl: `images/icon/${material}_${kind}.png`,
|
||||||
|
iconSize,
|
||||||
|
popupAnchor,
|
||||||
|
shadowUrl: `images/icon/shadow_${kind}.png`,
|
||||||
|
shadowSize: [iconSize[0]+8, iconSize[1]+8],
|
||||||
|
className: 'overzoomed',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let icons = signIcons[material] ??= {};
|
||||||
|
return icons[kind] ??= createSignIcon(material, kind);
|
||||||
|
}
|
||||||
|
|
||||||
const MinedMapLayer = L.TileLayer.extend({
|
const MinedMapLayer = L.TileLayer.extend({
|
||||||
initialize: function (mipmaps, layer) {
|
initialize: function (mipmaps, layer, tile_extension) {
|
||||||
L.TileLayer.prototype.initialize.call(this, '', {
|
L.TileLayer.prototype.initialize.call(this, '', {
|
||||||
detectRetina: true,
|
detectRetina: true,
|
||||||
tileSize: 512,
|
tileSize: 512,
|
||||||
|
@ -33,6 +88,7 @@ const MinedMapLayer = L.TileLayer.extend({
|
||||||
|
|
||||||
this.mipmaps = mipmaps;
|
this.mipmaps = mipmaps;
|
||||||
this.layer = layer;
|
this.layer = layer;
|
||||||
|
this.ext = tile_extension;
|
||||||
},
|
},
|
||||||
|
|
||||||
createTile: function (coords, done) {
|
createTile: function (coords, done) {
|
||||||
|
@ -57,7 +113,7 @@ const MinedMapLayer = L.TileLayer.extend({
|
||||||
return L.Util.emptyImageUrl;
|
return L.Util.emptyImageUrl;
|
||||||
|
|
||||||
|
|
||||||
return 'data/'+this.layer+'/'+z+'/r.'+coords.x+'.'+coords.y+'.png';
|
return `data/${this.layer}/${z}/r.${coords.x}.${coords.y}.${this.ext}`;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -98,39 +154,214 @@ const parseHash = function () {
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatSignLine(line) {
|
||||||
|
const el = document.createElement('span');
|
||||||
|
el.style.whiteSpace = 'pre';
|
||||||
|
|
||||||
|
for (const span of line) {
|
||||||
|
const child = document.createElement('span');
|
||||||
|
child.textContent = span.text;
|
||||||
|
|
||||||
|
let color = span.color ?? '';
|
||||||
|
if (color[0] !== '#')
|
||||||
|
color = '#000000';
|
||||||
|
|
||||||
|
if (span.bold)
|
||||||
|
child.style.fontWeight = 'bold';
|
||||||
|
if (span.italic)
|
||||||
|
child.style.fontStyle = 'italic';
|
||||||
|
|
||||||
|
child.style.textDecoration = '';
|
||||||
|
if (span.underlined)
|
||||||
|
child.style.textDecoration += ' underline';
|
||||||
|
if (span.strikethrough)
|
||||||
|
child.style.textDecoration += ' line-through';
|
||||||
|
|
||||||
|
child.style.color = color;
|
||||||
|
if (span.obfuscated) {
|
||||||
|
child.style.backgroundColor = color;
|
||||||
|
child.className = 'obfuscated';
|
||||||
|
}
|
||||||
|
|
||||||
|
el.appendChild(child);
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSign(sign, back) {
|
||||||
|
// standing signs
|
||||||
|
function px(base) {
|
||||||
|
const scale = 11;
|
||||||
|
return (base*scale)+'px';
|
||||||
|
}
|
||||||
|
// hanging signs
|
||||||
|
function pxh(base) {
|
||||||
|
const scale = 16;
|
||||||
|
return (base*scale)+'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
sign: {
|
||||||
|
width: px(24),
|
||||||
|
height: px(12),
|
||||||
|
paddingTop: px(0),
|
||||||
|
paddingBottom: px(14),
|
||||||
|
},
|
||||||
|
wall_sign: {
|
||||||
|
width: px(24),
|
||||||
|
height: px(12),
|
||||||
|
paddingTop: px(0),
|
||||||
|
paddingBottom: px(0),
|
||||||
|
},
|
||||||
|
hanging_sign: {
|
||||||
|
width: pxh(16),
|
||||||
|
height: pxh(10),
|
||||||
|
paddingTop: pxh(4),
|
||||||
|
paddingBottom: pxh(0),
|
||||||
|
},
|
||||||
|
hanging_wall_sign: {
|
||||||
|
width: pxh(16),
|
||||||
|
height: pxh(10),
|
||||||
|
paddingTop: pxh(6),
|
||||||
|
paddingBottom: pxh(0),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const size = sizes[sign.kind];
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.classList = 'sign-wrapper';
|
||||||
|
|
||||||
|
const title = document.createElement('div');
|
||||||
|
title.classList = 'sign-title'
|
||||||
|
title.textContent = `Sign at ${sign.x}/${sign.y}/${sign.z}`;
|
||||||
|
if (back)
|
||||||
|
title.textContent += ' (back)';
|
||||||
|
title.textContent += ':';
|
||||||
|
|
||||||
|
wrapper.appendChild(title);
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.style.width = size.width;
|
||||||
|
container.style.height = size.height;
|
||||||
|
container.style.paddingTop = size.paddingTop;
|
||||||
|
container.style.paddingBottom = size.paddingBottom;
|
||||||
|
container.style.backgroundImage = `url(images/bg/${sign.material}_${sign.kind}.png)`;
|
||||||
|
container.classList = 'sign-container overzoomed';
|
||||||
|
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.classList = 'sign-content';
|
||||||
|
|
||||||
|
let text = [];
|
||||||
|
if (!back && sign.front_text)
|
||||||
|
text = sign.front_text;
|
||||||
|
else if (back && sign.back_text)
|
||||||
|
text = sign.back_text;
|
||||||
|
|
||||||
|
for (const line of text) {
|
||||||
|
content.appendChild(formatSignLine(line));
|
||||||
|
content.appendChild(document.createElement('br'));
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(content);
|
||||||
|
wrapper.appendChild(container);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSigns(signLayer) {
|
||||||
|
const response = await fetch('data/entities.json', {cache: 'no-store'});
|
||||||
|
const res = await response.json();
|
||||||
|
|
||||||
|
const groups = {};
|
||||||
|
|
||||||
|
// Group signs by x,z coordinates
|
||||||
|
for (const sign of res.signs) {
|
||||||
|
const key = coordKey([sign.x, sign.z]);
|
||||||
|
const group = groups[key] ??= [];
|
||||||
|
group.push(sign);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, group] of Object.entries(groups)) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
|
||||||
|
let material;
|
||||||
|
let kind;
|
||||||
|
|
||||||
|
// Sort from top to bottom
|
||||||
|
group.sort((a, b) => b.y - a.y);
|
||||||
|
|
||||||
|
for (const sign of group) {
|
||||||
|
el.appendChild(createSign(sign, false));
|
||||||
|
|
||||||
|
if (sign.back_text)
|
||||||
|
el.appendChild(createSign(sign, true));
|
||||||
|
|
||||||
|
material ??= sign.material;
|
||||||
|
kind ??= sign.kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default material
|
||||||
|
material ??= 'oak';
|
||||||
|
|
||||||
|
const [x, z] = key.split(',').map((i) => +i);
|
||||||
|
|
||||||
|
const popup = L.popup().setContent(el);
|
||||||
|
|
||||||
|
popup.on('add', () => {
|
||||||
|
params.marker = [x, z];
|
||||||
|
updateHash();
|
||||||
|
});
|
||||||
|
popup.on('remove', () => {
|
||||||
|
params.marker = null;
|
||||||
|
updateHash();
|
||||||
|
});
|
||||||
|
|
||||||
|
const marker = L.marker([-z-0.5, x+0.5], {
|
||||||
|
icon: signIcon(material, kind),
|
||||||
|
}).addTo(signLayer).bindPopup(popup);
|
||||||
|
|
||||||
|
markers[coordKey([x, z])] = marker;
|
||||||
|
|
||||||
|
if (params.marker && x === params.marker[0] && z === params.marker[1])
|
||||||
|
marker.openPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.createMap = function () {
|
window.createMap = function () {
|
||||||
const xhr = new XMLHttpRequest();
|
(async function () {
|
||||||
xhr.onload = function () {
|
const response = await fetch('data/info.json', {cache: 'no-store'});
|
||||||
const res = JSON.parse(this.responseText),
|
const res = await response.json();
|
||||||
mipmaps = res.mipmaps,
|
const {mipmaps, spawn} = res;
|
||||||
spawn = res.spawn;
|
const features = res.features || {};
|
||||||
|
const tile_extension = res.tile_extension || 'png';
|
||||||
let x, z, zoom, light;
|
|
||||||
|
|
||||||
const updateParams = function () {
|
const updateParams = function () {
|
||||||
const args = parseHash();
|
const args = parseHash();
|
||||||
|
|
||||||
zoom = parseInt(args['zoom']);
|
params.zoom = parseInt(args['zoom']);
|
||||||
x = parseFloat(args['x']);
|
params.x = parseFloat(args['x']);
|
||||||
z = parseFloat(args['z']);
|
params.z = parseFloat(args['z']);
|
||||||
light = parseInt(args['light']);
|
params.light = parseInt(args['light']);
|
||||||
|
params.signs = parseInt(args['signs'] ?? '1');
|
||||||
|
params.marker = (args['marker'] ?? '').split(',').map((i) => +i);
|
||||||
|
|
||||||
if (isNaN(zoom))
|
if (isNaN(params.zoom))
|
||||||
zoom = 0;
|
params.zoom = 0;
|
||||||
if (isNaN(x))
|
if (isNaN(params.x))
|
||||||
x = spawn.x;
|
params.x = spawn.x;
|
||||||
if (isNaN(z))
|
if (isNaN(params.z))
|
||||||
z = spawn.z;
|
params.z = spawn.z;
|
||||||
|
if (!features.signs || isNaN(params.marker[0]) || isNaN(params.marker[1]))
|
||||||
|
params.marker = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
updateParams();
|
updateParams();
|
||||||
|
|
||||||
const map = L.map('map', {
|
const map = L.map('map', {
|
||||||
center: [-z, x],
|
center: [-params.z, params.x],
|
||||||
zoom: zoom,
|
zoom: params.zoom,
|
||||||
minZoom: -(mipmaps.length-1),
|
minZoom: -(mipmaps.length-1),
|
||||||
maxZoom: 3,
|
maxZoom: 5,
|
||||||
crs: L.CRS.Simple,
|
crs: L.CRS.Simple,
|
||||||
maxBounds: [
|
maxBounds: [
|
||||||
[-512*(mipmaps[0].bounds.maxZ+1), 512*mipmaps[0].bounds.minX],
|
[-512*(mipmaps[0].bounds.maxZ+1), 512*mipmaps[0].bounds.minX],
|
||||||
|
@ -138,17 +369,25 @@ window.createMap = function () {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapLayer = new MinedMapLayer(mipmaps, 'map');
|
const overlayMaps = {};
|
||||||
const lightLayer = new MinedMapLayer(mipmaps, 'light');
|
|
||||||
|
|
||||||
|
const mapLayer = new MinedMapLayer(mipmaps, 'map', tile_extension);
|
||||||
mapLayer.addTo(map);
|
mapLayer.addTo(map);
|
||||||
|
|
||||||
if (light)
|
const lightLayer = new MinedMapLayer(mipmaps, 'light', tile_extension);
|
||||||
|
overlayMaps['Illumination'] = lightLayer;
|
||||||
|
if (params.light)
|
||||||
map.addLayer(lightLayer);
|
map.addLayer(lightLayer);
|
||||||
|
|
||||||
const overlayMaps = {
|
let signLayer;
|
||||||
"Illumination": lightLayer,
|
if (features.signs) {
|
||||||
};
|
signLayer = L.layerGroup();
|
||||||
|
loadSigns(signLayer);
|
||||||
|
if (params.signs)
|
||||||
|
map.addLayer(signLayer);
|
||||||
|
|
||||||
|
overlayMaps['Signs'] = signLayer;
|
||||||
|
}
|
||||||
|
|
||||||
L.control.layers({}, overlayMaps).addTo(map);
|
L.control.layers({}, overlayMaps).addTo(map);
|
||||||
|
|
||||||
|
@ -160,26 +399,37 @@ window.createMap = function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeHash = function () {
|
const makeHash = function () {
|
||||||
let ret = '#x='+x+'&z='+z;
|
let ret = '#x='+params.x+'&z='+params.z;
|
||||||
|
|
||||||
if (zoom != 0)
|
if (params.zoom != 0)
|
||||||
ret += '&zoom='+zoom;
|
ret += '&zoom='+params.zoom;
|
||||||
|
|
||||||
if (map.hasLayer(lightLayer))
|
if (map.hasLayer(lightLayer))
|
||||||
ret += '&light=1';
|
ret += '&light=1';
|
||||||
|
if (features.signs && !map.hasLayer(signLayer))
|
||||||
|
ret += '&signs=0';
|
||||||
|
if (params.marker) {
|
||||||
|
ret += `&marker=${params.marker[0]},${params.marker[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateHash = function () {
|
updateHash = function () {
|
||||||
window.location.hash = makeHash();
|
window.location.hash = makeHash();
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshHash = function () {
|
const refreshHash = function (ev) {
|
||||||
zoom = map.getZoom();
|
if (ev.type === 'layeradd' || ev.type === 'layerremove') {
|
||||||
center = map.getCenter();
|
if (ev.layer !== lightLayer && ev.layer !== signLayer)
|
||||||
x = Math.round(center.lng);
|
return;
|
||||||
z = Math.round(-center.lat);
|
}
|
||||||
|
|
||||||
|
const center = map.getCenter();
|
||||||
|
|
||||||
|
params.zoom = map.getZoom();
|
||||||
|
params.x = Math.round(center.lng);
|
||||||
|
params.z = Math.round(-center.lat);
|
||||||
|
|
||||||
updateHash();
|
updateHash();
|
||||||
}
|
}
|
||||||
|
@ -195,20 +445,29 @@ window.createMap = function () {
|
||||||
if (window.location.hash === makeHash())
|
if (window.location.hash === makeHash())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const prevMarkerCoords = params.marker;
|
||||||
|
|
||||||
updateParams();
|
updateParams();
|
||||||
|
|
||||||
map.setView([-z, x], zoom);
|
if (params.light)
|
||||||
|
|
||||||
if (light)
|
|
||||||
map.addLayer(lightLayer);
|
map.addLayer(lightLayer);
|
||||||
else
|
else
|
||||||
map.removeLayer(lightLayer);
|
map.removeLayer(lightLayer);
|
||||||
|
|
||||||
|
if (features.signs) {
|
||||||
|
if (params.signs)
|
||||||
|
map.addLayer(signLayer);
|
||||||
|
else
|
||||||
|
map.removeLayer(signLayer);
|
||||||
|
|
||||||
|
if (coordKey(prevMarkerCoords) !== coordKey(params.marker))
|
||||||
|
getMarker(params.marker)?.openPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
map.setView([-params.z, params.x], params.zoom);
|
||||||
|
|
||||||
updateHash();
|
updateHash();
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
})();
|
||||||
|
|
||||||
xhr.open('GET', 'data/info.json', true);
|
|
||||||
xhr.send();
|
|
||||||
}
|
}
|
||||||
|
|
7
viewer/images/README.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# README
|
||||||
|
|
||||||
|
The images in this directory are assets directly taken from Minecraft, or are derived from Minecraft
|
||||||
|
assets. They are copyrighted by Mojang/Microsoft, and are used in accordance with the
|
||||||
|
[Minecraft Usage Guidelines](https://www.minecraft.net/en-us/usage-guidelines).
|
||||||
|
|
||||||
|
|
BIN
viewer/images/bg/acacia_hanging_sign.png
Normal file
After Width: | Height: | Size: 321 B |
BIN
viewer/images/bg/acacia_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 232 B |
BIN
viewer/images/bg/acacia_sign.png
Normal file
After Width: | Height: | Size: 441 B |
BIN
viewer/images/bg/acacia_wall_sign.png
Normal file
After Width: | Height: | Size: 328 B |
BIN
viewer/images/bg/bamboo_hanging_sign.png
Normal file
After Width: | Height: | Size: 384 B |
BIN
viewer/images/bg/bamboo_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 244 B |
BIN
viewer/images/bg/bamboo_sign.png
Normal file
After Width: | Height: | Size: 564 B |
BIN
viewer/images/bg/bamboo_wall_sign.png
Normal file
After Width: | Height: | Size: 459 B |
BIN
viewer/images/bg/birch_hanging_sign.png
Normal file
After Width: | Height: | Size: 363 B |
BIN
viewer/images/bg/birch_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 239 B |
BIN
viewer/images/bg/birch_sign.png
Normal file
After Width: | Height: | Size: 460 B |
BIN
viewer/images/bg/birch_wall_sign.png
Normal file
After Width: | Height: | Size: 341 B |
BIN
viewer/images/bg/cherry_hanging_sign.png
Normal file
After Width: | Height: | Size: 311 B |
BIN
viewer/images/bg/cherry_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 224 B |
BIN
viewer/images/bg/cherry_sign.png
Normal file
After Width: | Height: | Size: 442 B |
BIN
viewer/images/bg/cherry_wall_sign.png
Normal file
After Width: | Height: | Size: 323 B |
BIN
viewer/images/bg/crimson_hanging_sign.png
Normal file
After Width: | Height: | Size: 371 B |
BIN
viewer/images/bg/crimson_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 235 B |
BIN
viewer/images/bg/crimson_sign.png
Normal file
After Width: | Height: | Size: 531 B |
BIN
viewer/images/bg/crimson_wall_sign.png
Normal file
After Width: | Height: | Size: 392 B |
BIN
viewer/images/bg/dark_oak_hanging_sign.png
Normal file
After Width: | Height: | Size: 307 B |
BIN
viewer/images/bg/dark_oak_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 240 B |
BIN
viewer/images/bg/dark_oak_sign.png
Normal file
After Width: | Height: | Size: 416 B |
BIN
viewer/images/bg/dark_oak_wall_sign.png
Normal file
After Width: | Height: | Size: 315 B |
BIN
viewer/images/bg/jungle_hanging_sign.png
Normal file
After Width: | Height: | Size: 412 B |
BIN
viewer/images/bg/jungle_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 242 B |
BIN
viewer/images/bg/jungle_sign.png
Normal file
After Width: | Height: | Size: 451 B |
BIN
viewer/images/bg/jungle_wall_sign.png
Normal file
After Width: | Height: | Size: 308 B |
BIN
viewer/images/bg/mangrove_hanging_sign.png
Normal file
After Width: | Height: | Size: 329 B |
BIN
viewer/images/bg/mangrove_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 224 B |
BIN
viewer/images/bg/mangrove_sign.png
Normal file
After Width: | Height: | Size: 451 B |
BIN
viewer/images/bg/mangrove_wall_sign.png
Normal file
After Width: | Height: | Size: 342 B |
BIN
viewer/images/bg/oak_hanging_sign.png
Normal file
After Width: | Height: | Size: 339 B |
BIN
viewer/images/bg/oak_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 225 B |
BIN
viewer/images/bg/oak_sign.png
Normal file
After Width: | Height: | Size: 440 B |
BIN
viewer/images/bg/oak_wall_sign.png
Normal file
After Width: | Height: | Size: 332 B |
BIN
viewer/images/bg/pale_oak_hanging_sign.png
Normal file
After Width: | Height: | Size: 318 B |
BIN
viewer/images/bg/pale_oak_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 222 B |
BIN
viewer/images/bg/pale_oak_sign.png
Normal file
After Width: | Height: | Size: 419 B |
BIN
viewer/images/bg/pale_oak_wall_sign.png
Normal file
After Width: | Height: | Size: 317 B |
BIN
viewer/images/bg/spruce_hanging_sign.png
Normal file
After Width: | Height: | Size: 345 B |
BIN
viewer/images/bg/spruce_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 236 B |
BIN
viewer/images/bg/spruce_sign.png
Normal file
After Width: | Height: | Size: 421 B |
BIN
viewer/images/bg/spruce_wall_sign.png
Normal file
After Width: | Height: | Size: 316 B |
BIN
viewer/images/bg/warped_hanging_sign.png
Normal file
After Width: | Height: | Size: 374 B |
BIN
viewer/images/bg/warped_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 236 B |
BIN
viewer/images/bg/warped_sign.png
Normal file
After Width: | Height: | Size: 531 B |