Compare commits

..

No commits in common. "main" and "v2.1.0" have entirely different histories.
main ... v2.1.0

55 changed files with 1125 additions and 3410 deletions

View file

@ -1,4 +0,0 @@
*
!/Cargo.*
!/src
!/crates

View file

@ -1,14 +1,5 @@
name: 'MinedMap' name: 'MinedMap'
on: on: ['push', 'pull_request', 'workflow_dispatch']
push:
branches:
- 'main'
tags:
- 'v*'
pull_request:
branches:
- 'main'
workflow_dispatch: {}
env: env:
RUSTFLAGS: -Dwarnings RUSTFLAGS: -Dwarnings
@ -16,7 +7,7 @@ env:
jobs: jobs:
viewer: viewer:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-20.04'
steps: steps:
- name: 'Checkout' - name: 'Checkout'
@ -34,10 +25,9 @@ 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@v4' uses: 'actions/upload-artifact@v3'
with: with:
name: 'MinedMap-${{ steps.tag.outputs.tag }}-viewer' name: 'MinedMap-${{ steps.tag.outputs.tag }}-viewer'
path: 'build/pkg' path: 'build/pkg'
@ -48,7 +38,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: '1.86' toolchain: stable
components: rustfmt components: rustfmt
- run: cargo fmt --all -- --check - run: cargo fmt --all -- --check
@ -58,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: '1.86' toolchain: stable
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
@ -72,7 +62,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: '1.86' toolchain: stable
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
@ -83,11 +73,12 @@ 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: '1.86' toolchain: ${{ matrix.rust }}
- 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
@ -100,9 +91,9 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: 'macos-13' - os: 'macos-11'
target: 'aarch64-apple-darwin' target: 'aarch64-apple-darwin'
- os: 'macos-13' - os: 'macos-11'
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'
@ -110,7 +101,7 @@ jobs:
- os: 'windows-2019' - os: 'windows-2019'
target: 'i686-pc-windows-msvc' target: 'i686-pc-windows-msvc'
ext: '.exe' ext: '.exe'
- os: 'ubuntu-22.04' - os: 'ubuntu-20.04'
target: 'x86_64-unknown-linux-gnu' target: 'x86_64-unknown-linux-gnu'
steps: steps:
@ -127,12 +118,10 @@ jobs:
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: '1.86' toolchain: stable
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'
@ -145,94 +134,7 @@ 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@v4' uses: 'actions/upload-artifact@v3'
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 }}

View file

@ -2,124 +2,6 @@
## [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 ## [2.1.0] - 2024-01-27
### Added ### Added
@ -194,13 +76,7 @@ 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.5.0...HEAD [Unreleased]: https://github.com/neocturne/MinedMap/compare/v2.1.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.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

953
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
edition = "2024" edition = "2021"
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"
repository = "https://github.com/neocturne/MinedMap" repository = "https://github.com/neocturne/MinedMap"
@ -13,7 +13,7 @@ pre-release-commit-message = "{{crate_name}} {{version}}"
[package] [package]
name = "minedmap" name = "minedmap"
version = "2.5.0" version = "2.1.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
@ -38,29 +38,23 @@ pre-release-replacements = [
[dependencies] [dependencies]
anyhow = "1.0.68" anyhow = "1.0.68"
bincode = "2.0.1" bincode = "1.3.3"
clap = { version = "4.1.4", features = ["derive", "wrap_help"] } 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"
humantime = "2.1.0" image = { version = "0.24.5", default-features = false, features = ["png"] }
image = { version = "0.25.1", default-features = false, features = ["png", "webp"] } indexmap = { version = "2.0.0", features = ["serde"] }
indexmap = "2.0.0" lru = "0.12.0"
lru = "0.13.0" minedmap-nbt = { version = "0.1.1", path = "crates/nbt", default-features = false }
minedmap-default-alloc = { version = "0.1.0", path = "crates/default-alloc", optional = true } minedmap-resource = { version = "0.3.0", path = "crates/resource" }
minedmap-nbt = { version = "0.2.0", path = "crates/nbt", default-features = false } minedmap-types = { version = "0.1.2", path = "crates/types" }
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"
regex = "1.10.2" regex = "1.10.2"
rustc-hash = "2.0.0" rustc-hash = "1.1.0"
serde = { version = "1.0.152", features = ["derive"] } serde = { version = "1.0.152", features = ["rc", "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"
@ -68,6 +62,5 @@ tracing-subscriber = "0.3.17"
zstd = "0.13.0" zstd = "0.13.0"
[features] [features]
default = ["jemalloc-auto"] default = ["zlib-ng"]
jemalloc-auto = ["dep:minedmap-default-alloc"] zlib-ng = ["minedmap-nbt/zlib-ng"]
jemalloc = ["jemalloc-auto", "minedmap-default-alloc/jemalloc"]

View file

@ -1,17 +0,0 @@
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
View file

@ -1,21 +0,0 @@
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.

View file

@ -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.21.4 (no mod installation required!) * Compatible with unmodified Minecraft Java Edition 1.8 up to 1.20 (no mod installation necessary!)
* 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
@ -22,20 +22,13 @@ based on [Leaflet](https://leafletjs.com/). The map renderer is heavily inspired
## How to use ## How to use
Download the binary release that matches your platform from the Github release
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, Minecraft stores its save data in a directory `~/.minecraft/saves` on Linux,
and `C:\Users\<username>\AppData\Roaming\.minecraft\saves`. To generate MinedMap 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 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 (replacing the first argument with the path to your save data; `viewer` refers
to the directory where you unpacked the MinedMap viewer): to the directory where you unpacked the MinedMap viewer):
```shell ```shell
minedmap ~/.minecraft/saves/World <viewer>/data minedmap ~/.minecraft/saves/World viewer/data
``` ```
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
@ -54,19 +47,6 @@ 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 ### Signs
![Sign screenshot](https://raw.githubusercontent.com/neocturne/MinedMap/e5d9c813ba3118d04dc7e52e3dc6f48808a69120/docs/images/signs.png) ![Sign screenshot](https://raw.githubusercontent.com/neocturne/MinedMap/e5d9c813ba3118d04dc7e52e3dc6f48808a69120/docs/images/signs.png)
@ -98,14 +78,9 @@ matches.
Finally, `--sign-transform` allows to specify sed-style replacement patterns to 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 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-prefix` or `--sign-filter` should not be displayed:
`--sign-transform 's/\[Map\]//'` would replace each occurence of "\[Map\]" with `--sign-filter 's/\[Map\]//'` would replace each occurence of "\[Map\]" with
the empty string. 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
Binary builds of the map generator for Linux and Windows, as well as an archive Binary builds of the map generator for Linux and Windows, as well as an archive
@ -117,6 +92,11 @@ or newer). The following command can be used to build the current development ve
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.

View file

@ -1,17 +0,0 @@
[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"]

View file

@ -1,3 +0,0 @@
#[cfg(any(target_env = "musl", feature = "jemalloc"))]
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;

View file

@ -1,6 +1,6 @@
[package] [package]
name = "minedmap-nbt" name = "minedmap-nbt"
version = "0.2.0" version = "0.1.1"
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,10 +11,12 @@ 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.1.0" flate2 = "1.0.27"
minedmap-types = { version = "0.2.0", path = "../types" } minedmap-types = { version = "0.1.2", 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"] }

View file

@ -2,11 +2,11 @@
use std::{ use std::{
fs::File, fs::File,
io::{SeekFrom, prelude::*}, io::{prelude::*, SeekFrom},
path::Path, path::Path,
}; };
use anyhow::{Context, Result, bail}; use anyhow::{bail, Context, Result};
use flate2::read::ZlibDecoder; use flate2::read::ZlibDecoder;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;

View file

@ -1,6 +1,6 @@
[package] [package]
name = "minedmap-resource" name = "minedmap-resource"
version = "0.7.0" version = "0.3.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]
bincode = "2.0.1" enumflags2 = { version = "0.7.7", features = ["serde"] }
enumflags2 = "0.7.7" glam = "0.25.0"
glam = "0.30.0" serde = { version = "1.0.183", features = ["derive"] }

View file

@ -1,117 +1,437 @@
//! Biome data //! Biome data structures
//!
//! This file is generated using resource/biomes.py, do not edit
use super::*; use serde::{Deserialize, Serialize};
use super::Color;
/// 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::*; use BiomeGrassColorModifier::*;
/// List if known biomes and their properties // Data extracted from Minecraft code decompiled using https://github.com/Hexeption/MCP-Reborn
pub const BIOMES: &[(&str, Biome)] = &[
// 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(200, 0) Biome::new(2_00, 0_00)
.foliage([158, 129, 77]) .foliage([158, 129, 77])
.grass([144, 129, 77]), .grass([144, 129, 77]),
), ),
("bamboo_jungle", Biome::new(95, 90)), ("bamboo_jungle", Biome::new(0_95, 0_90)),
("basalt_deltas", Biome::new(200, 0)), ("beach", Biome::new(0_80, 0_40)),
("beach", Biome::new(80, 40)), ("birch_forest", Biome::new(0_60, 0_60)),
("birch_forest", Biome::new(60, 60)),
( (
"cherry_grove", "cherry_grove",
Biome::new(50, 80) Biome::new(0_50, 0_80)
.foliage([182, 219, 97]) .water([93, 183, 239])
.grass([182, 219, 97]) .grass([182, 219, 97])
.water([93, 183, 239]), .foliage([182, 219, 97]),
),
("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(50, 50).water([69, 173, 242]), Biome::new(0_50, 0_50).water([69, 173, 242]),
), ),
("deep_ocean", Biome::new(50, 50)), ("deep_ocean", Biome::new(0_50, 0_50)),
("desert", Biome::new(200, 0)), ("desert", Biome::new(2_00, 0_00)),
("dripstone_caves", Biome::new(80, 40)), ("dripstone_caves", Biome::new(0_80, 0_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(200, 0) Biome::new(2_00, 0_00)
.foliage([158, 129, 77]) .foliage([158, 129, 77])
.grass([144, 129, 77]), .grass([144, 129, 77]),
), ),
("flower_forest", Biome::new(70, 80)), ("flower_forest", Biome::new(0_70, 0_80)),
("forest", Biome::new(70, 80)), ("forest", Biome::new(0_70, 0_80)),
("frozen_ocean", Biome::new(0, 50).water([57, 56, 201])), ("frozen_ocean", Biome::new(0_00, 0_50).water([57, 56, 201])),
("frozen_peaks", Biome::new(-70, 90)), ("frozen_peaks", Biome::new(-0_70, 0_90)),
("frozen_river", Biome::new(0, 50).water([57, 56, 201])), ("frozen_river", Biome::new(0_00, 0_50).water([57, 56, 201])),
("grove", Biome::new(-20, 80)), ("grove", Biome::new(-0_20, 0_80)),
("ice_spikes", Biome::new(0, 50)), ("ice_spikes", Biome::new(0_00, 0_50)),
("jagged_peaks", Biome::new(-70, 90)), ("jagged_peaks", Biome::new(-0_70, 0_90)),
("jungle", Biome::new(95, 90)), ("jungle", Biome::new(0_95, 0_90)),
("lukewarm_ocean", Biome::new(50, 50).water([69, 173, 242])), (
("lush_caves", Biome::new(50, 50)), "lukewarm_ocean",
Biome::new(0_50, 0_50).water([69, 173, 242]),
),
("lush_caves", Biome::new(0_50, 0_50)),
( (
"mangrove_swamp", "mangrove_swamp",
Biome::new(80, 90) Biome::new(0_80, 0_90)
.water([58, 122, 106])
.foliage([141, 177, 39]) .foliage([141, 177, 39])
.modify(Swamp) .modify(Swamp),
.water([58, 122, 106]),
), ),
("meadow", Biome::new(50, 80).water([14, 78, 207])), ("meadow", Biome::new(0_50, 0_80).water([14, 78, 207])),
("mushroom_fields", Biome::new(90, 100)), ("mushroom_fields", Biome::new(0_90, 1_00)),
("nether_wastes", Biome::new(200, 0)), ("ocean", Biome::new(0_50, 0_50)),
("ocean", Biome::new(50, 50)), ("old_growth_birch_forest", Biome::new(0_60, 0_60)),
("old_growth_birch_forest", Biome::new(60, 60)), ("old_growth_pine_taiga", Biome::new(0_30, 0_80)),
("old_growth_pine_taiga", Biome::new(30, 80)), ("old_growth_spruce_taiga", Biome::new(0_25, 0_80)),
("old_growth_spruce_taiga", Biome::new(25, 80)), ("plains", Biome::new(0_80, 0_40)),
( ("river", Biome::new(0_50, 0_50)),
"pale_garden", ("savanna", Biome::new(2_00, 0_00)),
Biome::new(70, 80) ("savanna_plateau", Biome::new(2_00, 0_00)),
.foliage([135, 141, 118]) ("snowy_beach", Biome::new(0_05, 0_30).water([61, 87, 214])),
.grass([119, 130, 114]) ("snowy_plains", Biome::new(0_00, 0_50)),
.water([118, 136, 157]), ("snowy_slopes", Biome::new(-0_30, 0_90)),
), ("snowy_taiga", Biome::new(-0_50, 0_40).water([61, 87, 214])),
("plains", Biome::new(80, 40)), ("sparse_jungle", Biome::new(0_95, 0_80)),
("river", Biome::new(50, 50)), ("stony_peaks", Biome::new(1_00, 0_30)),
("savanna", Biome::new(200, 0)), ("stony_shore", Biome::new(0_20, 0_30)),
("savanna_plateau", Biome::new(200, 0)), ("sunflower_plains", Biome::new(0_80, 0_40)),
("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(80, 90) Biome::new(0_80, 0_90)
.water([97, 123, 100])
.foliage([106, 112, 57]) .foliage([106, 112, 57])
.modify(Swamp) .modify(Swamp),
.water([97, 123, 100]),
), ),
("taiga", Biome::new(25, 80)), ("taiga", Biome::new(0_25, 0_80)),
("the_end", Biome::new(50, 50)), ("the_void", Biome::new(0_50, 0_50)),
("the_void", Biome::new(50, 50)), ("warm_ocean", Biome::new(0_50, 0_50).water([67, 213, 238])),
("warm_ocean", Biome::new(50, 50).water([67, 213, 238])), ("windswept_forest", Biome::new(0_20, 0_30)),
("warped_forest", Biome::new(200, 0)), ("windswept_gravelly_hills", Biome::new(0_20, 0_30)),
("windswept_forest", Biome::new(20, 30)), ("windswept_hills", Biome::new(0_20, 0_30)),
("windswept_gravelly_hills", Biome::new(20, 30)), ("windswept_savanna", Biome::new(2_00, 0_00)),
("windswept_hills", Biome::new(20, 30)),
("windswept_savanna", Biome::new(200, 0)),
( (
"wooded_badlands", "wooded_badlands",
Biome::new(200, 0) Biome::new(2_00, 0_00)
.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",
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,202 +0,0 @@
//! 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",
}
}

View file

@ -13,7 +13,6 @@ 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"),

View file

@ -4,19 +4,20 @@
mod biomes; mod biomes;
mod block_color; mod block_color;
mod block_types;
mod legacy_biomes;
mod legacy_block_types; mod legacy_block_types;
#[allow(clippy::missing_docs_in_private_items)] // Generated module
mod block_types;
use std::collections::HashMap; use std::collections::HashMap;
use bincode::{BorrowDecode, Decode, Encode}; use enumflags2::{bitflags, BitFlags};
use enumflags2::{BitFlags, bitflags}; use serde::{Deserialize, Serialize};
/// 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)] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum BlockFlag { pub enum BlockFlag {
/// The block type is opaque /// The block type is opaque
Opaque, Opaque,
@ -38,14 +39,14 @@ pub enum BlockFlag {
} }
/// An RGB color with u8 components /// An RGB color with u8 components
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Encode, Decode)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
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)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct BlockColor { 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>,
@ -61,43 +62,6 @@ impl BlockColor {
} }
} }
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) /// A block type specification (for use in constants)
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct ConstBlockType { struct ConstBlockType {
@ -171,112 +135,9 @@ impl BlockTypes {
} }
} }
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 {
@ -284,8 +145,6 @@ 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 {
@ -295,7 +154,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 legacy_biomes::BIOME_ALIASES.iter().rev() { for &(old, new) in biomes::BIOME_ALIASES.iter().rev() {
let biome = biome_map let biome = biome_map
.get(new) .get(new)
.copied() .copied()
@ -305,19 +164,16 @@ impl Default for BiomeTypes {
let legacy_biomes = (0..=255) let legacy_biomes = (0..=255)
.map(|index| { .map(|index| {
let id = legacy_biomes::legacy_biome(index); let id = 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,
} }
} }
} }
@ -335,10 +191,4 @@ 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
}
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "minedmap-types" name = "minedmap-types"
version = "0.2.0" version = "0.1.2"
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]
bincode = "2.0.1" itertools = "0.12.0"
itertools = "0.14.0" serde = { version = "1.0.183", features = ["derive"] }

View file

@ -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,7 +47,8 @@ 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 DoubleEndedIterator<Item = $t<AXIS>> pub fn iter() -> impl Iterator<Item = $t<AXIS>>
+ DoubleEndedIterator
+ ExactSizeIterator + ExactSizeIterator
+ FusedIterator + FusedIterator
+ Clone + Clone
@ -110,7 +111,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, Encode, Decode)] #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
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> {
@ -196,7 +197,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, Encode, Decode)] #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
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> {

View file

@ -1,51 +0,0 @@
# 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: {}

View file

@ -11,15 +11,13 @@ 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 - `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 and biomes of a new Minecraft version ## How to add support for block IDs 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
@ -71,17 +69,6 @@ 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.

View file

@ -1,70 +0,0 @@
#!/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)

View file

@ -316,13 +316,9 @@
"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"
}, },
@ -412,7 +408,6 @@
"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": {},
@ -420,19 +415,13 @@
"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": {},
@ -469,28 +458,16 @@
}, },
"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,
@ -756,14 +733,7 @@
"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"
@ -782,7 +752,6 @@
"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"
}, },
@ -874,7 +843,6 @@
"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"
}, },
@ -992,7 +960,6 @@
"lava_cauldron": { "lava_cauldron": {
"texture": "cauldron_top" "texture": "cauldron_top"
}, },
"leaf_litter": null,
"lectern": { "lectern": {
"texture": "lectern_top" "texture": "lectern_top"
}, },
@ -1280,7 +1247,6 @@
"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,
@ -1303,14 +1269,7 @@
"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"
@ -1320,56 +1279,6 @@
}, },
"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"
}, },
@ -1481,16 +1390,6 @@
"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"
@ -1525,9 +1424,6 @@
"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"
}, },
@ -1565,18 +1461,12 @@
"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"
}, },
@ -1739,18 +1629,6 @@
"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"
}, },
@ -1787,8 +1665,7 @@
"sculk_vein": {}, "sculk_vein": {},
"sea_lantern": {}, "sea_lantern": {},
"sea_pickle": {}, "sea_pickle": {},
"seagrass": {}, "seagrass": null,
"short_dry_grass": {},
"short_grass": null, "short_grass": null,
"shroomlight": {}, "shroomlight": {},
"shulker_box": {}, "shulker_box": {},
@ -1987,12 +1864,6 @@
"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"
}, },
@ -2020,7 +1891,6 @@
"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"
@ -2032,8 +1902,6 @@
"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"
@ -2044,9 +1912,6 @@
"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,
@ -2054,31 +1919,9 @@
"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"
}, },
@ -2148,24 +1991,9 @@
"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"
}, },
@ -2175,24 +2003,9 @@
"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"
}, },
@ -2202,24 +2015,9 @@
"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"
}, },
@ -2229,24 +2027,9 @@
"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"
}, },
@ -2256,14 +2039,7 @@
"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"
@ -2298,7 +2074,6 @@
"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,

View file

@ -14,15 +14,10 @@ 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('/// List if known block types and their properties', file=f);
print('pub const BLOCK_TYPES: &[(&str, ConstBlockType)] = &[', file=f) print('pub const BLOCK_TYPES: &[(&str, ConstBlockType)] = &[', file=f)
for name, info in colors.items(): for name, info in colors.items():

View file

@ -15,7 +15,6 @@ MATERIALS = [
'jungle', 'jungle',
'mangrove', 'mangrove',
'oak', 'oak',
'pale_oak',
'spruce', 'spruce',
'warped', 'warped',
] ]

View file

@ -3,15 +3,13 @@
use std::{ use std::{
collections::{BTreeMap, BTreeSet}, collections::{BTreeMap, BTreeSet},
fmt::Debug, fmt::Debug,
hash::Hash,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use bincode::{Decode, Encode}; use indexmap::IndexSet;
use clap::ValueEnum;
use regex::{Regex, RegexSet}; use regex::{Regex, RegexSet};
use serde::Serialize; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
io::fs::FileMetaVersion, io::fs::FileMetaVersion,
@ -20,13 +18,13 @@ use crate::{
world::{block_entity::BlockEntity, layer}, 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
/// ///
/// Increase when the generation of processed regions from region data changes /// Increase when the generation of processed regions from region data changes
/// (usually because of updated resource data) /// (usually because of updated resource data)
pub const REGION_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(7); pub const REGION_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(1);
/// MinedMap map tile data version number /// MinedMap map tile data version number
/// ///
@ -38,7 +36,7 @@ pub const MAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0);
/// ///
/// Increase when the generation of lightmap tiles from region data changes /// Increase when the generation of lightmap tiles from region data changes
/// (usually because of updated resource data) /// (usually because of updated resource data)
pub const LIGHTMAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(5); pub const LIGHTMAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(1);
/// MinedMap mipmap data version number /// MinedMap mipmap data version number
/// ///
@ -48,7 +46,7 @@ pub const MIPMAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0);
/// MinedMap processed entity data version number /// MinedMap processed entity data version number
/// ///
/// Increase when entity collection changes bacause of code changes. /// Increase when entity collection changes bacause of code changes.
pub const ENTITIES_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(3); pub const ENTITIES_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0);
/// Coordinate pair of a generated tile /// Coordinate pair of a generated tile
/// ///
@ -87,7 +85,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, Encode, Decode)] #[derive(Debug, Serialize, Deserialize)]
pub struct ProcessedChunk { pub struct ProcessedChunk {
/// Block type data /// Block type data
pub blocks: Box<layer::BlockArray>, pub blocks: Box<layer::BlockArray>,
@ -98,18 +96,18 @@ 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, Encode, Decode)] #[derive(Debug, Default, Serialize, Deserialize)]
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: Vec<Biome>, pub biome_list: IndexSet<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 /// Data structure for storing entity data between processing and collection steps
#[derive(Debug, Default, Encode, Decode)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct ProcessedEntities { pub struct ProcessedEntities {
/// List of block entities /// List of block entities
pub block_entities: Vec<BlockEntity>, pub block_entities: Vec<BlockEntity>,
@ -136,14 +134,10 @@ pub enum TileKind {
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
@ -156,8 +150,6 @@ pub struct Config {
pub viewer_info_path: PathBuf, pub viewer_info_path: PathBuf,
/// Path of viewer entities file /// Path of viewer entities file
pub viewer_entities_path: PathBuf, pub viewer_entities_path: PathBuf,
/// Format of generated map tiles
pub image_format: ImageFormat,
/// Sign text filter patterns /// Sign text filter patterns
pub sign_patterns: RegexSet, pub sign_patterns: RegexSet,
/// Sign text transformation pattern /// Sign text transformation pattern
@ -172,13 +164,9 @@ impl Config {
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 level_dat_old_path = [&args.input_dir, Path::new("level.dat_old")]
.iter()
.collect();
let processed_dir: PathBuf = [&args.output_dir, Path::new("processed")].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_dir: PathBuf = [&processed_dir, Path::new("entities")].iter().collect();
let entities_path_final = [&entities_dir, Path::new("entities.bin")].iter().collect(); let entities_path_final = [&entities_dir, Path::new("entities.bin")].iter().collect();
@ -193,17 +181,14 @@ impl Config {
Ok(Config { 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,
entities_dir, entities_dir,
entities_path_final, entities_path_final,
viewer_info_path, viewer_info_path,
viewer_entities_path, viewer_entities_path,
image_format: args.image_format,
sign_patterns, sign_patterns,
sign_transforms, sign_transforms,
}) })
@ -279,39 +264,14 @@ 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, self.tile_extension()); let filename = coord_filename(coords, "png");
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

View file

@ -16,7 +16,7 @@ pub struct EntityCollector<'a> {
regions: &'a [TileCoords], regions: &'a [TileCoords],
} }
impl TileMerger for EntityCollector<'_> { impl<'a> TileMerger for EntityCollector<'a> {
fn file_meta_version(&self) -> fs::FileMetaVersion { fn file_meta_version(&self) -> fs::FileMetaVersion {
ENTITIES_FILE_META_VERSION ENTITIES_FILE_META_VERSION
} }
@ -34,7 +34,7 @@ impl TileMerger for EntityCollector<'_> {
} }
} }
impl TileCollector for EntityCollector<'_> { impl<'a> TileCollector for EntityCollector<'a> {
type CollectOutput = (); type CollectOutput = ();
fn tiles(&self) -> &[TileCoords] { fn tiles(&self) -> &[TileCoords] {
@ -78,7 +78,8 @@ impl<'a> EntityCollector<'a> {
let mut output = ProcessedEntities::default(); let mut output = ProcessedEntities::default();
for source_path in sources { for source_path in sources {
let mut source: ProcessedEntities = match storage::read_file(source_path.as_ref()) { let mut source: ProcessedEntities =
match storage::read_file(source_path.as_ref(), storage::Format::Json) {
Ok(source) => source, Ok(source) => source,
Err(err) => { Err(err) => {
warn!( warn!(
@ -93,7 +94,7 @@ impl<'a> EntityCollector<'a> {
output.block_entities.append(&mut source.block_entities); output.block_entities.append(&mut source.block_entities);
} }
storage::write(file, &output).context("Failed to write entity data") storage::write(file, &output, storage::Format::Json).context("Failed to write entity data")
} }
/// Runs the mipmap generation /// Runs the mipmap generation

View file

@ -61,8 +61,6 @@ struct Metadata<'t> {
spawn: Spawn, spawn: Spawn,
/// Enabled MinedMap features /// Enabled MinedMap features
features: Features, features: Features,
/// Format of generated map tiles
tile_extension: &'static str,
} }
/// Viewer entity JSON data structure /// Viewer entity JSON data structure
@ -124,14 +122,7 @@ 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> {
let res = crate::nbt::data::from_file(&self.config.level_dat_path); crate::nbt::data::from_file(&self.config.level_dat_path).context("Failed to read level.dat")
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]
@ -179,7 +170,8 @@ impl<'a> MetadataWriter<'a> {
/// Generates [Entities] data from collected entity lists /// Generates [Entities] data from collected entity lists
fn entities(&self) -> Result<Entities> { fn entities(&self) -> Result<Entities> {
let data: ProcessedEntities = storage::read_file(&self.config.entities_path_final) let data: ProcessedEntities =
storage::read_file(&self.config.entities_path_final, storage::Format::Json)
.context("Failed to read entity data file")?; .context("Failed to read entity data file")?;
let ret = Entities { let ret = Entities {
@ -213,7 +205,6 @@ impl<'a> MetadataWriter<'a> {
mipmaps: Vec::new(), mipmaps: Vec::new(),
spawn: Self::spawn(&level_dat), spawn: Self::spawn(&level_dat),
features, features,
tile_extension: self.config.tile_extension(),
}; };
for tile_map in self.tiles.iter() { for tile_map in self.tiles.iter() {

View file

@ -10,46 +10,31 @@ mod tile_merger;
mod tile_mipmapper; mod tile_mipmapper;
mod tile_renderer; mod tile_renderer;
use std::{ use std::path::PathBuf;
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, ImageFormat}; use common::Config;
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};
use self::entity_collector::EntityCollector; use self::entity_collector::EntityCollector;
/// Returns the MinedMap version number /// MinedMap version number
fn version() -> &'static str { const VERSION: &str = git_version!(
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( #[command(
about, about,
version = version(), version = VERSION.strip_prefix("v").unwrap(),
max_term_width = 100, max_term_width = 100,
)] )]
pub struct Args { pub struct Args {
@ -59,29 +44,9 @@ 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 /// Prefix for text of signs to show on the map
#[arg(long)] #[arg(long)]
pub sign_prefix: Vec<String>, pub sign_prefix: Vec<String>,
@ -106,73 +71,14 @@ pub struct Args {
pub output_dir: PathBuf, pub output_dir: PathBuf,
} }
/// Configures a Rayon thread pool for parallel processing /// Configures the Rayon thread pool for parallel processing
fn setup_threads(num_threads: usize) -> Result<ThreadPool> { fn setup_threads(num_threads: usize) -> Result<()> {
rayon::ThreadPoolBuilder::new() rayon::ThreadPoolBuilder::new()
.num_threads(num_threads) .num_threads(num_threads)
.build() .build_global()
.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, &regions).run()?;
let tiles = TileMipmapper::new(config, &regions).run()?;
EntityCollector::new(config, &regions).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();
@ -187,28 +93,17 @@ pub fn cli() -> Result<()> {
.with_target(false) .with_target(false)
.init(); .init();
let mut pool = setup_threads(config.num_threads_initial)?; setup_threads(config.num_threads)?;
let rt = tokio::runtime::Builder::new_current_thread() let rt = tokio::runtime::Builder::new_current_thread()
.build() .build()
.unwrap(); .unwrap();
let watch = args.watch.then(|| create_watcher(&args)).transpose()?; let regions = RegionProcessor::new(&config).run()?;
TileRenderer::new(&config, &rt, &regions).run()?;
let tiles = TileMipmapper::new(&config, &regions).run()?;
EntityCollector::new(&config, &regions).run()?;
MetadataWriter::new(&config, &tiles).run()?;
pool.install(|| generate(&config, &rt))?; Ok(())
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)?;
}
})
} }

View file

@ -1,11 +1,16 @@
//! The [RegionProcessor] and related functions //! The [RegionProcessor] and related functions
use std::{ffi::OsStr, path::PathBuf, sync::mpsc, time::SystemTime}; use std::{
ffi::OsStr,
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
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};
@ -31,7 +36,7 @@ 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, Enum)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RegionProcessorStatus { enum RegionProcessorStatus {
/// Region was processed /// Region was processed
Ok, Ok,
@ -45,37 +50,6 @@ 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
@ -106,13 +80,22 @@ struct SingleRegionProcessor<'a> {
lightmap_needed: bool, lightmap_needed: bool,
/// True if entity output file needs to be updated /// True if entity output file needs to be updated
entities_needed: bool, entities_needed: bool,
/// Format of generated map tiles /// Processed region intermediate data
image_format: image::ImageFormat, processed_region: ProcessedRegion,
/// 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<'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)?;
@ -129,6 +112,10 @@ impl<'a> SingleRegionProcessor<'a> {
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 entities_needed = Some(input_timestamp) > entities_timestamp;
let processed_region = ProcessedRegion::default();
let lightmap = image::GrayAlphaImage::new(N, N);
let entities = ProcessedEntities::default();
Ok(SingleRegionProcessor { Ok(SingleRegionProcessor {
block_types: &processor.block_types, block_types: &processor.block_types,
biome_types: &processor.biome_types, biome_types: &processor.biome_types,
@ -144,7 +131,10 @@ impl<'a> SingleRegionProcessor<'a> {
output_needed, output_needed,
lightmap_needed, lightmap_needed,
entities_needed, entities_needed,
image_format: processor.config.tile_image_format(), processed_region,
lightmap,
entities,
has_unknown: false,
}) })
} }
@ -168,14 +158,15 @@ 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, processed_region: &ProcessedRegion) -> Result<()> { fn save_region(&self) -> Result<()> {
if !self.output_needed { if !self.output_needed {
return Ok(()); return Ok(());
} }
storage::write_file( storage::write_file(
&self.output_path, &self.output_path,
processed_region, &self.processed_region,
storage::Format::Bincode,
REGION_FILE_META_VERSION, REGION_FILE_META_VERSION,
self.input_timestamp, self.input_timestamp,
) )
@ -184,7 +175,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, lightmap: &image::GrayAlphaImage) -> Result<()> { fn save_lightmap(&self) -> Result<()> {
if !self.lightmap_needed { if !self.lightmap_needed {
return Ok(()); return Ok(());
} }
@ -194,8 +185,8 @@ impl<'a> SingleRegionProcessor<'a> {
LIGHTMAP_FILE_META_VERSION, LIGHTMAP_FILE_META_VERSION,
self.input_timestamp, self.input_timestamp,
|file| { |file| {
lightmap self.lightmap
.write_to(file, self.image_format) .write_to(file, image::ImageFormat::Png)
.context("Failed to save image") .context("Failed to save image")
}, },
) )
@ -204,32 +195,28 @@ impl<'a> SingleRegionProcessor<'a> {
/// Saves processed entity data /// Saves processed entity 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_entities(&self, entities: &mut ProcessedEntities) -> Result<()> { fn save_entities(&mut self) -> Result<()> {
if !self.entities_needed { if !self.entities_needed {
return Ok(()); return Ok(());
} }
entities.block_entities.sort_unstable(); self.entities.block_entities.sort_unstable();
storage::write_file( storage::write_file(
&self.entities_path, &self.entities_path,
entities, &self.entities,
storage::Format::Json,
ENTITIES_FILE_META_VERSION, ENTITIES_FILE_META_VERSION,
self.input_timestamp, self.input_timestamp,
) )
} }
/// Processes a single chunk /// Processes a single chunk
fn process_chunk( fn process_chunk(&mut self, chunk_coords: ChunkCoords, data: world::de::Chunk) -> Result<()> {
&self,
data: &mut SingleRegionData,
chunk_coords: ChunkCoords,
chunk_data: world::de::Chunk,
) -> Result<()> {
let (chunk, has_unknown) = let (chunk, has_unknown) =
world::chunk::Chunk::new(&chunk_data, self.block_types, self.biome_types) world::chunk::Chunk::new(&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))?;
data.has_unknown |= has_unknown; self.has_unknown |= has_unknown;
if self.output_needed || self.lightmap_needed { if self.output_needed || self.lightmap_needed {
if let Some(layer::LayerData { if let Some(layer::LayerData {
@ -237,11 +224,11 @@ impl<'a> SingleRegionProcessor<'a> {
biomes, biomes,
block_light, block_light,
depths, depths,
}) = world::layer::top_layer(&mut data.biome_list, &chunk) }) = world::layer::top_layer(&mut self.processed_region.biome_list, &chunk)
.with_context(|| format!("Failed to process chunk {:?}", chunk_coords))? .with_context(|| format!("Failed to process chunk {:?}", chunk_coords))?
{ {
if self.output_needed { if self.output_needed {
data.chunks[chunk_coords] = Some(Box::new(ProcessedChunk { self.processed_region.chunks[chunk_coords] = Some(Box::new(ProcessedChunk {
blocks, blocks,
biomes, biomes,
depths, depths,
@ -250,7 +237,7 @@ 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 data.lightmap, &chunk_lightmap, chunk_coords); overlay_chunk(&mut self.lightmap, &chunk_lightmap, chunk_coords);
} }
} }
} }
@ -262,21 +249,20 @@ impl<'a> SingleRegionProcessor<'a> {
chunk_coords, chunk_coords,
) )
})?; })?;
data.entities.block_entities.append(&mut block_entities); self.entities.block_entities.append(&mut block_entities);
} }
Ok(()) Ok(())
} }
/// Processes the chunks of the region /// Processes the chunks of the region
fn process_chunks(&self, data: &mut SingleRegionData) -> Result<()> { fn process_chunks(&mut self) -> Result<()> {
crate::nbt::region::from_file(&self.input_path)?.foreach_chunk( crate::nbt::region::from_file(&self.input_path)?
|chunk_coords, chunk_data| self.process_chunk(data, chunk_coords, chunk_data), .foreach_chunk(|chunk_coords, data| self.process_chunk(chunk_coords, data))
)
} }
/// Processes the region /// Processes the region
fn run(&self) -> Result<RegionProcessorStatus> { fn run(mut self) -> Result<RegionProcessorStatus> {
if !self.output_needed && !self.lightmap_needed && !self.entities_needed { if !self.output_needed && !self.lightmap_needed && !self.entities_needed {
debug!( debug!(
"Skipping unchanged region r.{}.{}.mca", "Skipping unchanged region r.{}.{}.mca",
@ -290,9 +276,7 @@ impl<'a> SingleRegionProcessor<'a> {
self.coords.x, self.coords.z self.coords.x, self.coords.z
); );
let mut data = SingleRegionData::default(); if let Err(err) = self.process_chunks() {
if let Err(err) = self.process_chunks(&mut data) {
if self.output_timestamp.is_some() if self.output_timestamp.is_some()
&& self.lightmap_timestamp.is_some() && self.lightmap_timestamp.is_some()
&& self.entities_timestamp.is_some() && self.entities_timestamp.is_some()
@ -311,16 +295,11 @@ impl<'a> SingleRegionProcessor<'a> {
} }
} }
let processed_region = ProcessedRegion { self.save_region()?;
biome_list: data.biome_list.into_iter().collect(), self.save_lightmap()?;
chunks: data.chunks, self.save_entities()?;
};
self.save_region(&processed_region)?; Ok(if self.has_unknown {
self.save_lightmap(&data.lightmap)?;
self.save_entities(&mut data.entities)?;
Ok(if data.has_unknown {
RegionProcessorStatus::OkWithUnknown RegionProcessorStatus::OkWithUnknown
} else { } else {
RegionProcessorStatus::Ok RegionProcessorStatus::Ok
@ -365,20 +344,11 @@ 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
let file_type = entry.file_type().ok()?; matches!(
if !file_type.is_file() { entry.file_type().map(|file_type| file_type.is_file()),
return None; Ok(true)
} )
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())
@ -393,8 +363,6 @@ 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))?; fs::create_dir_all(&self.config.entities_dir(0))?;
@ -402,18 +370,31 @@ impl<'a> RegionProcessor<'a> {
info!("Processing region files..."); info!("Processing region files...");
let (region_send, region_recv) = mpsc::channel(); let (region_send, region_recv) = mpsc::channel();
let (status_send, status_recv) = mpsc::channel(); let (processed_send, processed_recv) = mpsc::channel();
let (error_send, error_recv) = mpsc::channel();
let has_unknown = AtomicBool::new(false);
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 != Status::ErrorMissing { if ret != RegionProcessorStatus::ErrorMissing {
region_send.send(coords).unwrap(); region_send.send(coords).unwrap();
} }
status_send.send(ret).unwrap(); match ret {
RegionProcessorStatus::Ok => processed_send.send(()).unwrap(),
RegionProcessorStatus::OkWithUnknown => {
has_unknown.store(true, Ordering::Relaxed);
processed_send.send(()).unwrap();
}
RegionProcessorStatus::Skipped => {}
RegionProcessorStatus::ErrorOk | RegionProcessorStatus::ErrorMissing => {
error_send.send(()).unwrap()
}
}
anyhow::Ok(()) anyhow::Ok(())
})?; })?;
@ -421,21 +402,19 @@ 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(status_send); drop(processed_send);
let processed = processed_recv.into_iter().count();
let mut status = EnumMap::<_, usize>::default(); drop(error_send);
for ret in status_recv { let errors = error_recv.into_iter().count();
status[ret] += 1;
}
info!( info!(
"Processed region files ({} processed, {} unchanged, {} errors)", "Processed region files ({} processed, {} unchanged, {} errors)",
status[Status::Ok] + status[Status::OkWithUnknown], processed,
status[Status::Skipped], regions.len() - processed - errors,
status[Status::ErrorOk] + status[Status::ErrorMissing], errors,
); );
if status[Status::OkWithUnknown] > 0 { if has_unknown.into_inner() {
warn!("Unknown block or biome types found during processing"); warn!("Unknown block or biome types found during processing");
eprint!(concat!( eprint!(concat!(
"\n", "\n",

View file

@ -74,7 +74,7 @@ impl<'a, P> MapMerger<'a, P> {
} }
} }
impl<P: image::PixelWithColorType> TileMerger for MapMerger<'_, P> impl<'a, P: image::PixelWithColorType> TileMerger for MapMerger<'a, P>
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>,
@ -144,7 +144,7 @@ where
} }
image image
.write_to(file, self.config.tile_image_format()) .write_to(file, image::ImageFormat::Png)
.context("Failed to save image") .context("Failed to save image")
} }
} }
@ -157,7 +157,7 @@ pub struct TileMipmapper<'a> {
regions: &'a [TileCoords], regions: &'a [TileCoords],
} }
impl TileCollector for TileMipmapper<'_> { impl<'a> TileCollector for TileMipmapper<'a> {
type CollectOutput = MipmapStat; type CollectOutput = MipmapStat;
fn tiles(&self) -> &[TileCoords] { fn tiles(&self) -> &[TileCoords] {

View file

@ -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::{Colorf, block_color, needs_biome}, resource::{block_color, needs_biome, Colorf},
types::*, types::*,
util::coord_offset, util::coord_offset,
}; };
@ -105,7 +105,8 @@ impl<'a> TileRenderer<'a> {
region_loader region_loader
.get_or_try_init(|| async { .get_or_try_init(|| async {
storage::read_file(&processed_path).context("Failed to load processed region data") storage::read_file(&processed_path, storage::Format::Bincode)
.context("Failed to load processed region data")
}) })
.await .await
.cloned() .cloned()
@ -133,7 +134,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 +187,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(usize::from(index))?; let biome = region.biome_list.get_index(index.into())?;
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 +304,7 @@ impl<'a> TileRenderer<'a> {
processed_timestamp, processed_timestamp,
|file| { |file| {
image image
.write_to(file, self.config.tile_image_format()) .write_to(file, image::ImageFormat::Png)
.context("Failed to save image") .context("Failed to save image")
}, },
)?; )?;

View file

@ -10,16 +10,28 @@ use std::{
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use bincode::{Decode, Encode}; use serde::{de::DeserializeOwned, Serialize};
use super::fs; use super::fs;
/// Bincode configuration /// Storage format
const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard(); #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Format {
/// Encode as Bincode
///
/// Bincode is more efficient than JSON, but cannot handle many of
/// serde's features like flatten, conditional skipping, ...
Bincode,
/// Encode as JSON
Json,
}
/// Serializes data and writes it to a writer /// Serializes data and writes it to a writer
pub fn write<W: Write, T: Encode>(writer: &mut W, value: &T) -> Result<()> { pub fn write<W: Write, T: Serialize>(writer: &mut W, value: &T, format: Format) -> Result<()> {
let data = bincode::encode_to_vec(value, BINCODE_CONFIG)?; let data = match format {
Format::Bincode => bincode::serialize(value)?,
Format::Json => serde_json::to_vec(value)?,
};
let len = u32::try_from(data.len())?; let len = u32::try_from(data.len())?;
let compressed = zstd::bulk::compress(&data, 1)?; let compressed = zstd::bulk::compress(&data, 1)?;
drop(data); drop(data);
@ -33,21 +45,18 @@ pub fn write<W: Write, T: Encode>(writer: &mut W, value: &T) -> Result<()> {
/// 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_file<T: Encode>( pub fn write_file<T: Serialize>(
path: &Path, path: &Path,
value: &T, value: &T,
format: Format,
version: fs::FileMetaVersion, version: fs::FileMetaVersion,
timestamp: SystemTime, timestamp: SystemTime,
) -> Result<()> { ) -> Result<()> {
fs::create_with_timestamp(path, version, timestamp, |file| write(file, value)) fs::create_with_timestamp(path, version, timestamp, |file| write(file, value, format))
} }
/// Reads data from a reader and deserializes it /// Reads data from a reader and deserializes it
pub fn read<R, T>(reader: &mut R) -> Result<T> pub fn read<R: Read, T: DeserializeOwned>(reader: &mut R, format: Format) -> Result<T> {
where
R: Read,
T: Decode<()>,
{
let mut len_buf = [0u8; 4]; let mut len_buf = [0u8; 4];
reader.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))?;
@ -57,17 +66,18 @@ where
let data = zstd::bulk::decompress(&compressed, len)?; let data = zstd::bulk::decompress(&compressed, len)?;
drop(compressed); drop(compressed);
Ok(bincode::decode_from_slice(&data, BINCODE_CONFIG)?.0) let value = match format {
Format::Bincode => bincode::deserialize(&data)?,
Format::Json => serde_json::from_slice(&data)?,
};
Ok(value)
} }
/// Reads data from a file and deserializes it /// Reads data from a file and deserializes it
pub fn read_file<T>(path: &Path) -> Result<T> pub fn read_file<T: DeserializeOwned>(path: &Path, format: Format) -> Result<T> {
where
T: Decode<()>,
{
(|| -> Result<T> { (|| -> Result<T> {
let mut file = File::open(path)?; let mut file = File::open(path)?;
read(&mut file) read(&mut file, format)
})() })()
.with_context(|| format!("Failed to read file {}", path.display())) .with_context(|| format!("Failed to read file {}", path.display()))
} }

View file

@ -2,9 +2,6 @@
#![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;

View file

@ -39,9 +39,7 @@ 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)) (region as i32) << (BLOCK_BITS + CHUNK_BITS) | ((chunk.0 as i32) << BLOCK_BITS | block.0 as i32)
| ((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

View file

@ -1,8 +1,7 @@
//! Processing of block entity data //! Processing of block entity data
use bincode::{Decode, Encode};
use minedmap_resource::{BlockFlag, BlockType}; use minedmap_resource::{BlockFlag, BlockType};
use serde::Serialize; use serde::{Deserialize, Serialize};
use super::{ use super::{
de, de,
@ -10,7 +9,7 @@ use super::{
}; };
/// Kind of sign block /// Kind of sign block
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Serialize)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum SignKind { pub enum SignKind {
/// Standing sign /// Standing sign
@ -24,7 +23,7 @@ pub enum SignKind {
} }
/// Processed sign data /// Processed sign data
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Serialize)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Sign { pub struct Sign {
/// The kind of the sign /// The kind of the sign
pub kind: SignKind, pub kind: SignKind,
@ -41,15 +40,10 @@ pub struct Sign {
impl Sign { impl Sign {
/// Processes a [de::BlockEntitySign] into a [Sign] /// Processes a [de::BlockEntitySign] into a [Sign]
fn new( fn new(sign: &de::BlockEntitySign, kind: SignKind, material: Option<String>) -> Sign {
sign: &de::BlockEntitySign,
kind: SignKind,
material: Option<String>,
data_version: u32,
) -> Sign {
let (front_text, back_text) = sign.text(); let (front_text, back_text) = sign.text();
let front_text = front_text.decode(data_version); let front_text = front_text.decode();
let back_text = back_text.decode(data_version); let back_text = back_text.decode();
Sign { Sign {
kind, kind,
material, material,
@ -60,7 +54,7 @@ impl Sign {
} }
/// Data for different kinds of [BlockEntity] /// Data for different kinds of [BlockEntity]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Serialize)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub enum BlockEntityData { pub enum BlockEntityData {
/// A sign block /// A sign block
@ -68,7 +62,7 @@ pub enum BlockEntityData {
} }
/// A processed block entity /// A processed block entity
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Serialize)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct BlockEntity { pub struct BlockEntity {
/// Global X coordinate /// Global X coordinate
pub x: i32, pub x: i32,
@ -83,11 +77,7 @@ pub struct BlockEntity {
impl BlockEntity { impl BlockEntity {
/// Processes a [de::BlockEntity] into a [BlockEntity] /// Processes a [de::BlockEntity] into a [BlockEntity]
pub fn new( pub fn new(entity: &de::BlockEntity, block_type: Option<&BlockType>) -> Option<Self> {
entity: &de::BlockEntity,
block_type: Option<&BlockType>,
data_version: u32,
) -> Option<Self> {
let wall_sign = block_type let wall_sign = block_type
.map(|block_type| block_type.block_color.is(BlockFlag::WallSign)) .map(|block_type| block_type.block_color.is(BlockFlag::WallSign))
.unwrap_or_default(); .unwrap_or_default();
@ -101,7 +91,7 @@ impl BlockEntity {
let material = block_type let material = block_type
.as_ref() .as_ref()
.and_then(|block_type| block_type.sign_material.as_ref()); .and_then(|block_type| block_type.sign_material.as_ref());
let data = BlockEntityData::Sign(Sign::new(sign, kind, material.cloned(), data_version)); let data = BlockEntityData::Sign(Sign::new(sign, kind, material.cloned()));
Some(BlockEntity { Some(BlockEntity {
x: entity.x, x: entity.x,

View file

@ -4,11 +4,11 @@
//! over different data versions as much as possible. //! over different data versions as much as possible.
use std::{ use std::{
collections::{BTreeMap, btree_map}, collections::{btree_map, BTreeMap},
iter::{self, FusedIterator}, iter::{self, FusedIterator},
}; };
use anyhow::{Context, Result, bail}; use anyhow::{bail, Context, Result};
use super::{block_entity::BlockEntity, de, section::*}; use super::{block_entity::BlockEntity, de, section::*};
use crate::{ use crate::{
@ -58,8 +58,6 @@ pub struct Chunk<'a> {
inner: ChunkInner<'a>, inner: ChunkInner<'a>,
/// Unprocessed block entities /// Unprocessed block entities
block_entities: &'a Vec<de::BlockEntity>, block_entities: &'a Vec<de::BlockEntity>,
/// Chunk data version
data_version: u32,
} }
impl<'a> Chunk<'a> { impl<'a> Chunk<'a> {
@ -89,7 +87,6 @@ impl<'a> Chunk<'a> {
Chunk { Chunk {
inner, inner,
block_entities, block_entities,
data_version,
}, },
has_unknown, has_unknown,
)) ))
@ -295,11 +292,7 @@ impl<'a> Chunk<'a> {
.iter() .iter()
.map(|block_entity| { .map(|block_entity| {
let block_type = self.block_type_at_block_entity(block_entity)?; let block_type = self.block_type_at_block_entity(block_entity)?;
Ok(BlockEntity::new( Ok(BlockEntity::new(block_entity, block_type))
block_entity,
block_type,
self.data_version,
))
}) })
.collect::<Result<_>>()?; .collect::<Result<_>>()?;
Ok(entities.into_iter().flatten().collect()) Ok(entities.into_iter().flatten().collect())
@ -419,17 +412,17 @@ impl<'a> Iterator for SectionIter<'a> {
} }
fn last(mut self) -> Option<Self::Item> { fn last(mut self) -> Option<Self::Item> {
self.next_back() self.with_iter(|iter| iter.last())
} }
} }
impl DoubleEndedIterator for SectionIter<'_> { impl<'a> DoubleEndedIterator for SectionIter<'a> {
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 ExactSizeIterator for SectionIter<'_> { impl<'a> ExactSizeIterator for SectionIter<'a> {
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(),
@ -440,4 +433,4 @@ impl ExactSizeIterator for SectionIter<'_> {
} }
} }
impl FusedIterator for SectionIter<'_> {} impl<'a> FusedIterator for SectionIter<'a> {}

View file

@ -2,7 +2,7 @@
use serde::Deserialize; use serde::Deserialize;
use super::text_value::TextValue; use super::json_text::JSONText;
/// 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)]
@ -110,7 +110,7 @@ pub enum BiomesV0 {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct BlockEntitySignV1_20Text { pub struct BlockEntitySignV1_20Text {
/// Lines of sign text /// Lines of sign text
pub messages: Vec<TextValue>, pub messages: Vec<JSONText>,
/// Default text color /// Default text color
pub color: Option<String>, pub color: Option<String>,
} }
@ -125,13 +125,13 @@ pub enum BlockEntitySign {
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
V0 { V0 {
/// Line 1 of the sign text /// Line 1 of the sign text
text1: TextValue, text1: JSONText,
/// Line 2 of the sign text /// Line 2 of the sign text
text2: TextValue, text2: JSONText,
/// Line 3 of the sign text /// Line 3 of the sign text
text3: TextValue, text3: JSONText,
/// Line 4 of the sign text /// Line 4 of the sign text
text4: TextValue, text4: JSONText,
/// Default text color /// Default text color
color: Option<String>, color: Option<String>,
}, },

View file

@ -1,28 +1,24 @@
//! Newtype and helper methods for handling Minecraft text values //! Newtype and helper methods for handling Minecraft Raw JSON Text
use std::{collections::VecDeque, fmt::Display}; use std::{collections::VecDeque, fmt::Display, sync::Arc};
use bincode::{Decode, Encode};
use minedmap_resource::Color;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// A span of formatted text /// A span of formatted text
/// ///
/// A [TextValue] consists of a tree of [FormattedText] nodes (canonically /// A [JSONText] consists of a tree of [FormattedText] nodes (canonically
/// represented as a [FormattedTextTree], but other kinds are possible with /// represented as a [FormattedTextTree], but other kinds are possible with
/// is handled by [DeserializedText]. /// is handled by [DeserializedText].
/// ///
/// Formatting that is not set in a node is inherited from the parent. /// Formatting that is not set in a node is inherited from the parent.
#[derive( #[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Encode, Decode,
)]
pub struct FormattedText { pub struct FormattedText {
#[serde(default)] #[serde(default)]
/// Text content /// Text content
pub text: String, pub text: String,
/// Text color /// Text color
#[serde(skip_serializing_if = "Option::is_none", with = "text_color")] #[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<Color>, pub color: Option<Arc<String>>,
/// Bold formatting /// Bold formatting
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub bold: Option<bool>, pub bold: Option<bool>,
@ -45,7 +41,7 @@ impl FormattedText {
pub fn inherit(self, parent: &Self) -> Self { pub fn inherit(self, parent: &Self) -> Self {
FormattedText { FormattedText {
text: self.text, text: self.text,
color: self.color.or(parent.color), color: self.color.or_else(|| parent.color.clone()),
bold: self.bold.or(parent.bold), bold: self.bold.or(parent.bold),
italic: self.italic.or(parent.italic), italic: self.italic.or(parent.italic),
underlined: self.underlined.or(parent.underlined), underlined: self.underlined.or(parent.underlined),
@ -87,7 +83,7 @@ impl From<String> for FormattedTextTree {
} }
/// List of [FormattedText] /// List of [FormattedText]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Encode, Decode)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct FormattedTextList(pub Vec<FormattedText>); pub struct FormattedTextList(pub Vec<FormattedText>);
impl FormattedTextList { impl FormattedTextList {
@ -107,9 +103,9 @@ impl Display for FormattedTextList {
} }
} }
/// Raw deserialized [TextValue] /// Raw deserialized [JSONText]
/// ///
/// A [TextValue] can contain various different types serialized as JSON or NBT. /// A [JSONText] can contain various different JSON types.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum DeserializedText { pub enum DeserializedText {
@ -169,106 +165,13 @@ impl Default for DeserializedText {
} }
} }
/// Minecraft raw text value /// Minecraft Raw JSON Text
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct TextValue(pub fastnbt::Value); pub struct JSONText(pub String);
impl TextValue { impl JSONText {
/// Deserializes a [TextValue] into a [DeserializedText] /// Deserializes a [JSONText] into a [DeserializedText]
pub fn deserialize(&self, data_version: u32) -> DeserializedText { pub fn deserialize(&self) -> DeserializedText {
// TODO: Improve error handling serde_json::from_str(&self.0).unwrap_or_default()
//
// 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)
} }
} }

View file

@ -3,8 +3,8 @@
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::{
@ -13,7 +13,7 @@ use crate::{
}; };
/// Height (Y coordinate) of a block /// Height (Y coordinate) of a block
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlockHeight(pub i32); pub struct BlockHeight(pub i32);
impl BlockHeight { impl BlockHeight {
@ -58,7 +58,7 @@ struct LayerEntry<'a> {
depth: &'a mut Option<BlockHeight>, depth: &'a mut Option<BlockHeight>,
} }
impl LayerEntry<'_> { impl<'a> LayerEntry<'a> {
/// 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
@ -97,8 +97,7 @@ impl LayerEntry<'_> {
if self.is_empty() { if self.is_empty() {
*self.block = Some(block_type.block_color); *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)
@ -106,6 +105,7 @@ impl LayerEntry<'_> {
.expect("biome index not in range"), .expect("biome index not in range"),
); );
} }
}
if block_type.block_color.is(BlockFlag::Water) { if block_type.block_color.is(BlockFlag::Water) {
return Ok(false); return Ok(false);

View file

@ -3,7 +3,7 @@
pub mod block_entity; pub mod block_entity;
pub mod chunk; pub mod chunk;
pub mod de; pub mod de;
pub mod json_text;
pub mod layer; pub mod layer;
pub mod section; pub mod section;
pub mod sign; pub mod sign;
pub mod text_value;

View file

@ -5,7 +5,7 @@
use std::fmt::Debug; use std::fmt::Debug;
use anyhow::{Context, Result, bail}; use anyhow::{bail, Context, Result};
use num_integer::div_rem; use num_integer::div_rem;
use tracing::debug; use tracing::debug;
@ -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;
4096usize.div_ceil(blocks_per_word) (4096 + blocks_per_word - 1) / blocks_per_word
} else { } else {
64 * bits as usize 64 * bits as usize
}; };
@ -145,7 +145,7 @@ impl<'a> SectionV1_13<'a> {
} }
} }
impl Section for SectionV1_13<'_> { impl<'a> Section for SectionV1_13<'a> {
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
@ -188,7 +188,7 @@ impl<'a> SectionV0<'a> {
} }
} }
impl Section for SectionV0<'_> { impl<'a> Section for SectionV0<'a> {
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;
@ -208,7 +208,7 @@ impl Section for SectionV0<'_> {
/// 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<&Biome>; fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<Option<&Biome>>;
} }
/// Minecraft v1.18+ section biome data /// Minecraft v1.18+ section biome data
@ -226,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<&'a Biome>, palette: Vec<Option<&'a Biome>>,
/// Number of bits used for each entry in *biomes* /// Number of bits used for each entry in *biomes*
bits: u8, bits: u8,
} }
@ -242,7 +242,7 @@ impl<'a> BiomesV1_18<'a> {
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 = 64usize.div_ceil(biomes_per_word); let expected_length = (64 + biomes_per_word - 1) / biomes_per_word;
if biomes.len() != expected_length { if biomes.len() != expected_length {
bail!("Invalid section biome data"); bail!("Invalid section biome data");
} }
@ -253,11 +253,12 @@ impl<'a> BiomesV1_18<'a> {
let palette_types = palette let palette_types = palette
.iter() .iter()
.map(|entry| { .map(|entry| {
biome_types.get(entry).unwrap_or_else(|| { let biome_type = biome_types.get(entry);
if biome_type.is_none() {
debug!("Unknown biome type: {}", entry); debug!("Unknown biome type: {}", entry);
has_unknown = true; has_unknown = true;
biome_types.get_fallback() }
}) biome_type
}) })
.collect(); .collect();
@ -293,8 +294,8 @@ impl<'a> BiomesV1_18<'a> {
} }
} }
impl Biomes for BiomesV1_18<'_> { impl<'a> Biomes for BiomesV1_18<'a> {
fn biome_at(&self, _section: SectionY, coords: SectionBlockCoords) -> Result<&Biome> { fn biome_at(&self, _section: SectionY, coords: SectionBlockCoords) -> Result<Option<&Biome>> {
let index = self.palette_index_at(coords); let index = self.palette_index_at(coords);
Ok(*self Ok(*self
.palette .palette
@ -348,8 +349,8 @@ impl<'a> BiomesV0<'a> {
} }
} }
impl Biomes for BiomesV0<'_> { impl<'a> Biomes for BiomesV0<'a> {
fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<&Biome> { fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<Option<&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;
@ -369,10 +370,7 @@ impl Biomes for BiomesV0<'_> {
} }
BiomesV0Data::ByteArray(data) => data[coords.xz.offset()] as u8, BiomesV0Data::ByteArray(data) => data[coords.xz.offset()] as u8,
}; };
Ok(self Ok(self.biome_types.get_legacy(id))
.biome_types
.get_legacy(id)
.unwrap_or(self.biome_types.get_fallback()))
} }
} }
@ -400,6 +398,10 @@ 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 { byte >> 4 } else { byte & 0xf } if nibble == 1 {
byte >> 4
} else {
byte & 0xf
}
} }
} }

View file

@ -1,14 +1,12 @@
//! Processing of sign text //! Processing of sign text
use std::fmt::Display; use std::{fmt::Display, sync::Arc};
use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize};
use minedmap_resource::Color;
use serde::Serialize;
use super::{ use super::{
de, de,
text_value::{FormattedText, FormattedTextList, TextValue}, json_text::{FormattedText, FormattedTextList, JSONText},
}; };
/// Version-independent reference to (front or back) sign text /// Version-independent reference to (front or back) sign text
@ -18,41 +16,17 @@ pub struct RawSignText<'a> {
/// ///
/// A regular sign always has 4 lines of text. The back of pre-1.20 /// A regular sign always has 4 lines of text. The back of pre-1.20
/// signs is represented as a [SignText] without any `messages`. /// signs is represented as a [SignText] without any `messages`.
pub messages: Vec<&'a TextValue>, pub messages: Vec<&'a JSONText>,
/// Sign color /// Sign color
/// ///
/// Defaults to "black". /// Defaults to "black".
pub color: Option<&'a str>, pub color: Option<&'a str>,
} }
/// The color to use for signs without a color attribute ("black") impl<'a> RawSignText<'a> {
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] /// Decodes the [RawSignText] into a [SignText]
pub fn decode(&self, data_version: u32) -> SignText { pub fn decode(&self) -> SignText {
let color = self let color = self.color.map(|c| Arc::new(c.to_owned()));
.color
.map(|c| DYE_COLORS.get(c).copied().unwrap_or(DEFAULT_COLOR));
let parent = FormattedText { let parent = FormattedText {
color, color,
..Default::default() ..Default::default()
@ -60,7 +34,7 @@ impl RawSignText<'_> {
SignText( SignText(
self.messages self.messages
.iter() .iter()
.map(|message| message.deserialize(data_version).linearize(&parent)) .map(|message| message.deserialize().linearize(&parent))
.collect(), .collect(),
) )
} }
@ -105,7 +79,7 @@ impl BlockEntitySignExt for de::BlockEntitySign {
} }
} }
#[derive(Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)] #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
/// Deserialized and linearized sign text /// Deserialized and linearized sign text
pub struct SignText(pub Vec<FormattedTextList>); pub struct SignText(pub Vec<FormattedTextList>);

View file

@ -1 +0,0 @@
/data

View file

@ -1,3 +0,0 @@
FROM docker.io/library/nginx:alpine-slim
COPY . /usr/share/nginx/html
# datadir should be mounted to: /usr/share/nginx/html/data

View file

@ -73,7 +73,7 @@ function signIcon(material, kind) {
} }
const MinedMapLayer = L.TileLayer.extend({ const MinedMapLayer = L.TileLayer.extend({
initialize: function (mipmaps, layer, tile_extension) { initialize: function (mipmaps, layer) {
L.TileLayer.prototype.initialize.call(this, '', { L.TileLayer.prototype.initialize.call(this, '', {
detectRetina: true, detectRetina: true,
tileSize: 512, tileSize: 512,
@ -88,7 +88,6 @@ 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) {
@ -113,7 +112,7 @@ const MinedMapLayer = L.TileLayer.extend({
return L.Util.emptyImageUrl; return L.Util.emptyImageUrl;
return `data/${this.layer}/${z}/r.${coords.x}.${coords.y}.${this.ext}`; return 'data/'+this.layer+'/'+z+'/r.'+coords.x+'.'+coords.y+'.png';
}, },
}); });
@ -154,6 +153,25 @@ const parseHash = function () {
return args; return args;
} }
const colors = {
black: '#000000',
dark_blue: '#0000AA',
dark_green: '#00AA00',
dark_aqua: '#00AAAA',
dark_red: '#AA0000',
dark_purple: '#AA00AA',
gold: '#FFAA00',
gray: '#AAAAAA',
dark_gray: '#555555',
blue: '#5555FF',
green: '#55FF55',
aqua: '#55FFFF',
red: '#FF5555',
light_purple: '#FF55FF',
yellow: '#FFFF55',
white: '#FFFFFF',
};
function formatSignLine(line) { function formatSignLine(line) {
const el = document.createElement('span'); const el = document.createElement('span');
el.style.whiteSpace = 'pre'; el.style.whiteSpace = 'pre';
@ -162,9 +180,7 @@ function formatSignLine(line) {
const child = document.createElement('span'); const child = document.createElement('span');
child.textContent = span.text; child.textContent = span.text;
let color = span.color ?? ''; const color = colors[span.color ?? 'black'] || colors['black'];
if (color[0] !== '#')
color = '#000000';
if (span.bold) if (span.bold)
child.style.fontWeight = 'bold'; child.style.fontWeight = 'bold';
@ -333,7 +349,6 @@ window.createMap = function () {
const res = await response.json(); const res = await response.json();
const {mipmaps, spawn} = res; const {mipmaps, spawn} = res;
const features = res.features || {}; const features = res.features || {};
const tile_extension = res.tile_extension || 'png';
const updateParams = function () { const updateParams = function () {
const args = parseHash(); const args = parseHash();
@ -371,10 +386,10 @@ window.createMap = function () {
const overlayMaps = {}; const overlayMaps = {};
const mapLayer = new MinedMapLayer(mipmaps, 'map', tile_extension); const mapLayer = new MinedMapLayer(mipmaps, 'map');
mapLayer.addTo(map); mapLayer.addTo(map);
const lightLayer = new MinedMapLayer(mipmaps, 'light', tile_extension); const lightLayer = new MinedMapLayer(mipmaps, 'light');
overlayMaps['Illumination'] = lightLayer; overlayMaps['Illumination'] = lightLayer;
if (params.light) if (params.light)
map.addLayer(lightLayer); map.addLayer(lightLayer);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 B