Compare commits

...

167 commits
v2.0.2 ... main

Author SHA1 Message Date
118034dd27
Merge pull request #77 from neocturne/textvalue
Some checks failed
MinedMap / viewer (push) Has been cancelled
MinedMap / fmt (push) Has been cancelled
MinedMap / clippy (push) Has been cancelled
MinedMap / docs (push) Has been cancelled
MinedMap / test (macOS-latest) (push) Has been cancelled
MinedMap / test (ubuntu-latest) (push) Has been cancelled
MinedMap / test (windows-latest) (push) Has been cancelled
MinedMap / build (.exe, windows-2019, i686-pc-windows-msvc) (push) Has been cancelled
MinedMap / build (.exe, windows-2019, x86_64-pc-windows-msvc) (push) Has been cancelled
MinedMap / build (macos-13, aarch64-apple-darwin) (push) Has been cancelled
MinedMap / build (macos-13, x86_64-apple-darwin) (push) Has been cancelled
MinedMap / build (ubuntu-22.04, x86_64-unknown-linux-gnu) (push) Has been cancelled
MinedMap / build-container (push) Has been cancelled
MinedMap / viewer-container (push) Has been cancelled
Update to Rust 1.86, make TextValue handling more correct
2025-04-03 19:00:14 +02:00
ca880ab3b4
world/text_value: do not fall back to NBT deserialization after DataVersion 4290
An invalid JSON string should not be emitted verbatim; ignore the
content instead.

Also increment entity meta version, which had been forgotten in the
previous commit.
2025-04-03 18:52:39 +02:00
dd56e842b5
ci: update to Rust 1.86
There is no official 1.86 Docker image yet.
2025-04-03 18:52:33 +02:00
69b62576ea
world/chunk: fix new Rust 1.86 clippy warning 2025-04-03 18:37:54 +02:00
58f2ff53b9
Merge pull request #76 from neocturne/mc-1.21.5
Some checks are pending
MinedMap / viewer (push) Waiting to run
MinedMap / fmt (push) Waiting to run
MinedMap / clippy (push) Waiting to run
MinedMap / docs (push) Waiting to run
MinedMap / test (macOS-latest) (push) Waiting to run
MinedMap / test (ubuntu-latest) (push) Waiting to run
MinedMap / test (windows-latest) (push) Waiting to run
MinedMap / build (.exe, windows-2019, i686-pc-windows-msvc) (push) Waiting to run
MinedMap / build (.exe, windows-2019, x86_64-pc-windows-msvc) (push) Waiting to run
MinedMap / build (macos-13, aarch64-apple-darwin) (push) Waiting to run
MinedMap / build (macos-13, x86_64-apple-darwin) (push) Waiting to run
MinedMap / build (ubuntu-22.04, x86_64-unknown-linux-gnu) (push) Waiting to run
MinedMap / build-container (push) Blocked by required conditions
MinedMap / viewer-container (push) Blocked by required conditions
Updates and Minecraft 1.21.5 support
2025-04-02 23:06:58 +02:00
42b302f493
resource: add new Minecraft 1.21.5 block types 2025-04-02 22:59:28 +02:00
5f84ec8ed2
world/text_value: add support for new NBT text serialization
Starting with DataVersion 4290, text is stored as NBT instead of JSON.
The structure remains the same.
2025-04-02 22:52:22 +02:00
ba6e4bae7f
world: rename JSONText to TextValue
New Minecraft version do not store text as JSON anymore.
2025-04-02 22:52:22 +02:00
442009eb08
Update dependencies 2025-04-02 19:06:01 +02:00
23b2f274be
ci, docker: update to Rust 1.85.1 2025-04-02 19:04:15 +02:00
3008203080
minedmap 2.5.0
Some checks failed
MinedMap / viewer (push) Has been cancelled
MinedMap / fmt (push) Has been cancelled
MinedMap / clippy (push) Has been cancelled
MinedMap / docs (push) Has been cancelled
MinedMap / test (macOS-latest) (push) Has been cancelled
MinedMap / test (ubuntu-latest) (push) Has been cancelled
MinedMap / test (windows-latest) (push) Has been cancelled
MinedMap / build (.exe, windows-2019, i686-pc-windows-msvc) (push) Has been cancelled
MinedMap / build (.exe, windows-2019, x86_64-pc-windows-msvc) (push) Has been cancelled
MinedMap / build (macos-13, aarch64-apple-darwin) (push) Has been cancelled
MinedMap / build (macos-13, x86_64-apple-darwin) (push) Has been cancelled
MinedMap / build (ubuntu-22.04, x86_64-unknown-linux-gnu) (push) Has been cancelled
MinedMap / build-container (push) Has been cancelled
MinedMap / viewer-container (push) Has been cancelled
2025-03-16 12:24:29 +01:00
d638c810f2
Merge pull request #74 from neocturne/no-wildcard-dep
default-alloc: fix wildcard dependency
2025-03-16 12:23:40 +01:00
6e5b958912
default-alloc: fix wildcard dependency 2025-03-16 12:17:20 +01:00
974a0f37df
minedmap-nbt 0.2.0 2025-03-14 20:02:49 +01:00
54ea2b2f28
minedmap-resource 0.7.0 2025-03-14 20:02:36 +01:00
6a54f57c50
minedmap-types 0.2.0 2025-03-14 20:02:13 +01:00
dca365f4e2
CHANGELOG.md: mention new Docker images and docker-compose example
Some checks failed
MinedMap / viewer (push) Has been cancelled
MinedMap / fmt (push) Has been cancelled
MinedMap / clippy (push) Has been cancelled
MinedMap / docs (push) Has been cancelled
MinedMap / test (macOS-latest) (push) Has been cancelled
MinedMap / test (ubuntu-latest) (push) Has been cancelled
MinedMap / test (windows-latest) (push) Has been cancelled
MinedMap / build (.exe, windows-2019, i686-pc-windows-msvc) (push) Has been cancelled
MinedMap / build (.exe, windows-2019, x86_64-pc-windows-msvc) (push) Has been cancelled
MinedMap / build (macos-13, aarch64-apple-darwin) (push) Has been cancelled
MinedMap / build (macos-13, x86_64-apple-darwin) (push) Has been cancelled
MinedMap / build (ubuntu-22.04, x86_64-unknown-linux-gnu) (push) Has been cancelled
MinedMap / build-container (push) Has been cancelled
MinedMap / viewer-container (push) Has been cancelled
2025-03-14 16:29:23 +01:00
e600a9dabb
CHANGELOG.md: mention Bincode update
While the update is an internal change, it does affect the MSRV, so it
should be mentioned in the changelog.
2025-03-14 16:24:22 +01:00
0f52f6a2c1
Merge pull request #72 from neocturne/ci-cache-key
ci: include target in cache key
2025-03-14 12:26:57 +01:00
d6cd0fc53b
ci: include target in cache key
Avoid cache keys colliding between build jobs running on the same OS.
2025-03-14 12:19:39 +01:00
c49ad6e608
Merge pull request #71 from neocturne/bincode2
Some checks are pending
MinedMap / viewer (push) Waiting to run
MinedMap / fmt (push) Waiting to run
MinedMap / clippy (push) Waiting to run
MinedMap / docs (push) Waiting to run
MinedMap / test (macOS-latest) (push) Waiting to run
MinedMap / test (ubuntu-latest) (push) Waiting to run
MinedMap / test (windows-latest) (push) Waiting to run
MinedMap / build (.exe, windows-2019, i686-pc-windows-msvc) (push) Waiting to run
MinedMap / build (.exe, windows-2019, x86_64-pc-windows-msvc) (push) Waiting to run
MinedMap / build (macos-13, aarch64-apple-darwin) (push) Waiting to run
MinedMap / build (macos-13, x86_64-apple-darwin) (push) Waiting to run
MinedMap / build (ubuntu-22.04, x86_64-unknown-linux-gnu) (push) Waiting to run
MinedMap / build-container (push) Blocked by required conditions
MinedMap / viewer-container (push) Blocked by required conditions
Bincode 2
2025-03-13 22:09:18 +01:00
5ee826a11b
ci: set fixed Rust version 1.85
Avoid CI failure due to new warnings or fmt changes in new Rust version.
2025-03-13 21:55:36 +01:00
775fcb2d1b
treewide: cargo fmt with 1.85 2025-03-13 21:50:50 +01:00
7bba5bae55
Update to Rust 2024
With bincode 2, we require rust 1.85 anyways, so we might as well
upgrade, too.
2025-03-13 21:50:50 +01:00
5c85687554
Clean up dependency features 2025-03-13 21:50:50 +01:00
708fb9645d
core/region_processor: refactor
Separate configuration and mutable state, also allowing to avoid a few
mem::take().
2025-03-13 21:50:50 +01:00
53a0f24600
treewide: update to bincode 2
Consistently use bincode's Encode/Decode to avoid issues with
incompatible serde features. Support for storing some temporary files as
JSON is removed.

The size of the "processed" directory is reduced by ~8% with the new
default encoding of bincode 2. Performance is more or less unaffected.
2025-03-13 21:50:50 +01:00
404ad74235
core: deserialize biome list into Vec
Only use IndexSet for deduplication while processing the biome; when
deserializing, no deduplication is required, so using a Vec is faster
(besides IndexSet missing non-serde support for bincode 2).
2025-03-13 21:50:50 +01:00
deb232ddf3
Update dependencies 2025-03-13 21:50:50 +01:00
fbdd5ed457
Dockerfile: switch back to docker.io/library/rust:alpine image
bincode 2 required Rust 1.85, so our options are to switch to Alpine
edge or to use the rust image. While using the rust image results in a
statically linked binary, this does not actually increase the size of
the image, as we were already using jemalloc, so almost nothing of libc
is actually used.
2025-03-13 21:50:50 +01:00
b1c0f316cb
Merge pull request #70 from neocturne/seagrass
resource: make seagrass opaque
2025-02-28 16:27:32 +01:00
7686996fd3
resource: make seagrass opaque
See changelog for rationale.
2025-02-28 16:16:29 +01:00
b8a5d1580b
Merge pull request #69 from neocturne/zlib-rs
Switch to zlib-rs
2025-02-28 13:54:53 +01:00
f8c8ca78ba
Switch from zlib-ng to zlib-rs
zlib-rs provides the same performance as zlib-ng with minedmap, while
reducing the amount of C code and avoiding the external build dependency
on CMake.
2025-02-28 12:43:47 +01:00
40bc6cd2a9
Update dependencies 2025-02-28 11:55:42 +01:00
5ee8e493d4
docker-compose.yml: mount volumes read-only where appropriate 2025-02-25 18:23:54 +01:00
6e2f2697fc
Merge pull request #68 from neocturne/docker
docker: set ARG after apk add
2025-02-22 11:34:55 +01:00
24c266fc78
docker: set ARG after apk add
Allow reusing apk add layer when MINEDMAP_VERSION has changed.

Signed-off-by: Matthias Schiffer <mschiffer@universe-factory.net>
2025-02-22 11:29:10 +01:00
0a08e8cc46
Merge pull request #67 from neocturne/docker
Docker image improvements and docker-compose.yml example
2025-02-22 11:23:51 +01:00
90f2c5fdd0
docker: add example docker-compose.yml 2025-02-22 11:18:03 +01:00
8cb1eee60b
docker, ci: fix --version output
When building the docker image manually, MINEDMAP_VERSION needs to be
set explicitly to get a proper version string.
2025-02-22 11:15:47 +01:00
282f62fc30
docker: run minedmap as unpriviledged user 2025-02-22 04:02:10 +01:00
7bc15f97de
docker: viewer: use nginx:alpine-slim as base 2025-02-22 03:13:43 +01:00
901489dbc3
Merge pull request #66 from neocturne/docker
Watch mode and Docker publish
2025-02-21 21:52:32 +01:00
dba3dd551e
ci: build Docker images, publish to GHCR 2025-02-21 21:43:05 +01:00
850b1a668b
docker: use lowercase stage name 2025-02-21 19:08:51 +01:00
3b5ce82873
docker: include Alpine base tools, tini
Including tini fixes forwarding signals to MinedMap, allowing to
interrupt it using Ctrl-C. The base tools may be used to add a wrapper
script to configure MinedMap with environment variables.

As the Alpine base is included now, we can switch from the rust:alpine
image to alpine:latest, resulting in MinedMap to be linked dynamically.
2025-02-21 10:55:50 +01:00
cb0aa235db
docker: move viewer Dockerfile to viewer subdirectory 2025-02-21 10:55:50 +01:00
d02ca9aea2
ci: update OS 2025-02-21 10:55:50 +01:00
c10e9e4902
Implement watch mode 2025-02-21 10:55:50 +01:00
37126f69fc
MetadataWriter: add fallback to level.dat_old
Looking at inotify dumps, it appears like because of bad implementation
choices, Minecraft's level.dat may not exist for a brief moment between
moving the old file to level.dat_old and moving a new version into place.

Add a fallback to level.dat_old, so generation will not fail if were
unlucky enough to hit this moment.
2025-02-21 10:55:50 +01:00
971afea727
Fix new clippy warnings 2025-02-21 10:55:50 +01:00
f661f854a4
Merge pull request #65 from neocturne/jemalloc
Add jemalloc and jemalloc-auto features
2025-02-12 20:51:51 +01:00
1d9be9a41c
Add jemalloc and jemalloc-auto features
Introduce the new features jemalloc (set jemalloc global allocator
unconditionally) and jemalloc-auto (set jemalloc global allocator on
musl-based targets to fix multithreaded performance, see [1]).

Because cargo does not support target-specific features or feature
defaults, the default is handled using a helper crate
minedmap-default-alloc.

[1] https://nickb.dev/blog/default-musl-allocator-considered-harmful-to-performance/
2025-02-12 20:34:09 +01:00
a25b3cdbd7
Merge pull request #64 from neocturne/fallback-biome
resource, world: implement fallback to plains for unknown biomes
2025-02-11 23:25:17 +01:00
d96bb727f7
ci: upgrade to upload-artifact v4 2025-02-11 23:04:08 +01:00
0dd36a409a
Update dependencies
Signed-off-by: Matthias Schiffer <mschiffer@universe-factory.net>
2025-02-11 23:03:33 +01:00
a10151a4f3
resource, world: implement fallback to plains for unknown biomes
Closes #63
2025-02-11 23:00:21 +01:00
d7fc95c950
README.md: fix size reduction estimate for WebP tiles
I accidentally measured the output size including the `processed`
directory for the previous numbers.
2025-01-11 02:10:00 +01:00
561a1e6577
minedmap 2.4.0 2025-01-11 01:54:53 +01:00
0bf1d46aad
minedmap-types 0.1.4 2025-01-11 01:53:05 +01:00
a2f0ad401d
CHANGELOG.md: fix missing word 2025-01-11 01:51:35 +01:00
ec309dc15f
Merge pull request #62 from neocturne/webp
core, viewer: add support for WebP output
2025-01-11 01:50:36 +01:00
6763e2b4ec
Update dependencies 2025-01-11 01:42:42 +01:00
c23b53a8c3
core, viewer: add support for WebP output
WebP can be selected by passing `--image-format webp` on the command
line. For typical Minecraft worlds, this results in a size reduction of
10-15% without increasing processing time.
2025-01-11 01:41:04 +01:00
bb11b29e92
Merge pull request #60 from neocturne/updates
Updates
2025-01-09 21:40:34 +01:00
28a191a23a
resource: ignore array size clippy warning
The const is used only once, so there is no reason not to inline it.
2025-01-09 21:09:48 +01:00
52572a9e81
Update dependencies 2025-01-09 20:57:12 +01:00
b9cd94b235
CHANGELOG.md: remove reference to Minecraft internals
Remove a sentence that refers to internal color names, as the
information is more confusing than helpful in a user-facing changlog.
2025-01-06 22:31:56 +01:00
f9954d1ce4
minedmap 2.3.1 2025-01-06 21:25:29 +01:00
a67bdb3b67
minedmap-resource 0.6.0 2025-01-06 21:25:20 +01:00
0a3f6d7765
Merge pull request #59 from neocturne/sign-colors
Sign color fixes
2025-01-06 21:24:46 +01:00
ff6e28d381
world, viewer: fix sign text colors
- 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. Dyed signs use different
color names.

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.
2025-01-06 21:16:41 +01:00
9375af8d54
resource: impl Ord for Color
Allow using Color in FormattedText.
2025-01-06 20:30:32 +01:00
7f329ac8e7
CHANGELOG.md: fix heading for previous release 2025-01-06 20:30:32 +01:00
650cd23198
minedmap 2.3.0 2025-01-02 13:42:11 +01:00
941cd16337
minedmap-resource 0.5.0 2025-01-02 13:41:56 +01:00
c1d2eaa17e
Merge pull request #58 from neocturne/update-1.21.4
Updates + 1.21.4 support
2025-01-02 01:58:26 +01:00
00237101f2
README.md: update for 1.21.4 support 2025-01-02 01:31:26 +01:00
8f408e78a0
resource: codegen biome list 2025-01-02 01:31:26 +01:00
7b98954c80
resource: add comments to generated file 2025-01-02 01:31:26 +01:00
00216341ca
resource: separate current biome data from aliases/legacy IDs
Prepare for automatic generation of current biome data.
2025-01-02 01:31:26 +01:00
83bd936f80
viewer: add images for pale oak signs 2024-12-31 12:21:09 +01:00
9b3b345318
resource: update block types and biomes for 1.21.4 2024-12-31 12:21:09 +01:00
11e25106cf
Update dependencies 2024-12-31 03:05:42 +01:00
491ec5cdca
Merge pull request #57 from neocturne/clippy-fixes
treewide: clippy fixes + dependency updates
2024-12-17 00:40:40 +01:00
feaf90c96c
Update dependencies 2024-12-17 00:31:38 +01:00
4933d8e15f
treewide: clippy fixes
Fix all clippy warnings as of Rust 1.83.0.
2024-12-17 00:24:56 +01:00
kek.rs
2156bff59a
Add Dockerfiles (#56) 2024-12-17 00:05:12 +01:00
7b44ee30d6
Add LICENSE file again
It can be useful for Github to show the license next to the repo, and
it's clearer that the license applies both to the Rust code and the
viewer this way.

The project was relicensed from BSD-2-Clause to MIT in 2023, but the
first year of the copyright is when the project started in 2015.
2024-11-20 14:27:30 +01:00
f9a03b332c
README.md: fix incorrect --sign-transform example 2024-07-18 23:22:05 +02:00
f43e84eb2b
minedmap 2.2.0 2024-06-23 11:31:45 +02:00
269f08d356
minedmap-resource 0.4.0 2024-06-23 11:30:41 +02:00
f1bc18add1
minedmap-types 0.1.3 2024-06-23 11:30:20 +02:00
6d6f3e5b49
Merge pull request #54 from neocturne/minecraft-1.21
Update dependencies and add Minecraft 1.21 support
2024-06-23 11:23:53 +02:00
414ad5a493
resource: add Minecraft 1.21 block types 2024-06-23 01:24:07 +02:00
7f3e47fcb4
treewide: update dependencies 2024-06-23 01:21:58 +02:00
bc8219772f
README.md: mention argument quoting difference on Windows 2024-06-23 01:21:58 +02:00
661da4698d
workflows: update MacOS jobs to macos-13 2024-06-19 19:39:30 +02:00
e9abe6b502
minedmap 2.1.1 2024-06-14 16:31:18 +02:00
4eb963f147
Merge pull request #53 from neocturne/empty-region-fixes
Empty region fixes
2024-06-14 16:30:34 +02:00
e74e7be686
core: region_processor: ignore empty region files
Minecraft generates empty region files in some cases. Just ignore these
files instead of printing an error message for each.
2024-06-14 16:19:55 +02:00
66189d279c
core: region_processor: fix crash due to incorrect counting in info message
Fixes #52
2024-06-14 16:02:56 +02:00
d435643cfc
Merge pull request #48 from neocturne/update
Update dependencies + fix new clippy lint
2024-03-04 00:04:56 +01:00
39b073f24b
Update dependencies 2024-03-03 23:56:59 +01:00
44e914599b
types: fix new clippy lint 2024-03-03 23:55:59 +01:00
bccd6d6cb4
README.md: some rewording 2024-03-03 23:44:56 +01:00
Myrddin Emrys
00eea45375
Improve README.md instructions.
Added instructions that would have made my initial attempts to use the tool less confusing.
2024-02-14 18:01:25 -06:00
9fd3989a95
minedmap 2.1.0 2024-01-27 14:16:03 +01:00
d6716a598b
minedmap-resource 0.3.0 2024-01-27 14:15:28 +01:00
f186681b41
release: move commit message configuration to workspace 2024-01-27 14:14:48 +01:00
3d84e9c9e4
CHANGELOG.md: mention --sign-transform 2024-01-27 14:08:42 +01:00
7009dd791d
Merge pull request #45 from neocturne/sign-transform
Sign text transformation support
2024-01-26 23:00:07 +01:00
8a1a26c13c
core: add support for sign text transformations
Add support for regexp replacement patterns, which can be useful when
the text matched by --sign-filter or --sign-prefix should not be
displayed.
2024-01-26 22:40:19 +01:00
a99a734df8
Update dependencies 2024-01-25 19:44:37 +01:00
3ceb7ae188
Merge pull request #40 from neocturne/sign-marker
Display markers for signs
2024-01-11 12:59:32 +01:00
e18761a3e4
resource: extract.py: pass directory of unpacked Minecraft JAR, not full path to assets
Make the script easier to use, and more consistent with sign_textures.py.
2024-01-11 12:44:42 +01:00
05d8faeb5c
resource: README.md: sign-related updates 2024-01-11 12:44:42 +01:00
6299c871a9
CHANGELOG.md: add sign layer 2024-01-11 00:02:25 +01:00
03521684b9
README.md: add description of sign feature 2024-01-10 23:57:23 +01:00
643035eaed
viewer: switch to modern fetch API, do not cache metadata and entity files 2024-01-10 22:56:33 +01:00
3d024c6cd8
viewer: update opened marker popup on URL hash change 2024-01-10 22:56:33 +01:00
08f84fa339
viewer: do not show sign layer when the feature is disabled 2024-01-10 22:56:32 +01:00
625f2a13a3
core/metadata_writer: add enabled features to metadata
Only consider sign support enabled when at least one pattern is
configured.
2024-01-10 22:56:32 +01:00
7daddd6bbc
viewer: include open marker in URL
Allow sharing URLs that reference a marker.
2024-01-10 22:56:32 +01:00
7b3ac8647e
viewer: store URL paramters in a 'params' variable 2024-01-10 22:56:32 +01:00
76df56c9ce
viewer: sort signs at the same x/z coordinates from top to bottom 2024-01-10 22:56:32 +01:00
31de0dc0bd
viewer: sign popup styling 2024-01-10 22:56:31 +01:00
ac0fd06b16
viewer: add icon shadows 2024-01-10 22:56:31 +01:00
989428f78d
viewer: use sign icons instead of default markers 2024-01-10 22:56:31 +01:00
43d7dc751e
viewer: allow zooming to level 5
Allow zooming in far enough so neighboring signs don't overlap.
2024-01-10 22:56:31 +01:00
7834315dd3
viewer: add sign images 2024-01-10 22:56:31 +01:00
dca9c394f2
viewer: display markers for signs
Still needs better styling. It might also be nice to include the currently
opened marker in the URL.
2024-01-10 22:56:30 +01:00
87d4371922
core/metadata_writer: implement filtering for signs 2024-01-10 22:56:30 +01:00
a1dd77c8fd
world/sign: implement Display for SignText 2024-01-10 22:56:30 +01:00
9fd5689ebb
core: add sign pattern command line argument handling 2024-01-10 22:56:30 +01:00
6da921cca3
core: wrap long help texts 2024-01-10 22:56:30 +01:00
fb361145eb
world: add sign position and material information to block entities 2024-01-10 22:56:29 +01:00
9aacd9cc2b
resource: make regular signs transparent
For consistency with other sign types. Signs will be shown on a separate
layer.
2024-01-10 22:56:29 +01:00
48a6e242ea
resource: add WallSign flag to block types
Allow to distinguish wall signs from freestanding or -hanging signs.
2024-01-10 22:56:29 +01:00
abf87e75ee
resource: add sign material to block types 2024-01-10 22:56:29 +01:00
8814dcff89
resource: split BlockType in Copy and !Copy parts
Preparation for actually adding !Copy fields to BlockType.

Only the Copy parts are added to the processed data, avoiding .clone()
for the most part.
2024-01-10 22:56:29 +01:00
1874d3082d
resource: fix typo in doc comment 2024-01-10 22:56:28 +01:00
d29c0df25d
core/metadata_writer: write entity list for viewer 2024-01-10 22:56:28 +01:00
cde6a4b6e6
core: merge entity data for all regions into one file
Introduce the EntityCollector, using the TileCollector and TileMerger
traits.
2024-01-10 22:56:28 +01:00
1143396068
core: use debug format for logging errors
Show the full cause stack.
2024-01-10 22:56:28 +01:00
0f308788ef
core: split TileMerger out of TileMipmapper
Reusable mipmap-style tile merging
2024-01-10 22:56:28 +01:00
7740ce0522
core/region_processor: sort processed block entities
Make the block entity list more reproducible when the stored chunk order
changes.
2024-01-10 22:56:27 +01:00
e36ae4601d
world: add Eq + Ord to all block entity types 2024-01-10 22:56:27 +01:00
825cf70e51
core: split TileCollector trait out of TileMipmapper
Make mipmap-style recursive processing of tiles reusable.
2024-01-10 22:56:27 +01:00
7297c03567
core/region_processor: store processed block entities for each region 2024-01-10 22:56:27 +01:00
c44f6ab859
io/storage: add JSON support
Bincode can't deal with a number of serde's features that would be
desirable if we're using the same Serialize impls for generating JSON
data for the viewer.
2024-01-10 22:56:27 +01:00
5d40d061a4
io/storage: split out inner part of read/write
Allow reusing the read/write logic without requiring control over the file
open.
2024-01-10 22:56:26 +01:00
f0e0db63d3
world: process sign data, prepare for serialization 2024-01-10 22:56:26 +01:00
638d5046c9
world/chunk: change Chunk into a struct
The version-specific part is extracted as an enum ChunkInner.
2024-01-10 22:56:26 +01:00
61d456846a
world: implement decoding raw JSON text into a linear list of formatted strings 2024-01-10 22:56:26 +01:00
f78dd795ca
world/de: add deserialization of sign block entities 2024-01-10 22:56:26 +01:00
0988ebe095
core/tile_mipmapper: fix file meta version for mipmap tiles
The meta version should only change when the outputs must be regenerated
even if the inputs stay the same. This should never be the case for
mipmap tiles, so we separate the meta version from map/lightmap tiles.
2024-01-10 22:56:26 +01:00
f79edb462c
core/common: add better explanation to file meta version definitions 2024-01-10 22:56:25 +01:00
e5d9c813ba
docs/images: add screenshot for sign marker documentation 2024-01-10 22:55:42 +01:00
7b12c4aea3
README.md: some improvements to the "How to use" and "Installation" sections 2024-01-10 22:42:58 +01:00
2e6a200c6c
README.md: reference screenshot by commit ID 2024-01-10 22:07:20 +01:00
f08acd06e2
Merge pull request #44 from neocturne/hide-unknown-warnings
Hide unknown block/biome type warnings by default
2024-01-10 13:14:59 +01:00
e8165aa47d
world: hide unknown block/biome type warnings by default
While using MinedMap with modded Minecraft version is not officially
supported, it should still work reasonably well if you're okay with
custom block types being invisible and custom biomes using default
grass/color/foliage colors.

Avoid spamming the log with messages for each section in this case
without --verbose, and instead just print a single warning at the end of
processing.
2024-01-10 13:02:29 +01:00
1e41e1bc6a
world/chunk: move SectionIter structs to impl 2024-01-10 13:02:29 +01:00
5273e4b366
CHANGELOG.md: reformat previous release changes
Use the "Added" and "Changed" headings like in previous releases.
2024-01-10 13:02:29 +01:00
3a796325ba
Update dependencies 2024-01-10 13:02:29 +01:00
154 changed files with 11837 additions and 4512 deletions

4
.dockerignore Normal file
View file

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

View file

@ -1,5 +1,14 @@
name: 'MinedMap'
on: ['push', 'pull_request', 'workflow_dispatch']
on:
push:
branches:
- 'main'
tags:
- 'v*'
pull_request:
branches:
- 'main'
workflow_dispatch: {}
env:
RUSTFLAGS: -Dwarnings
@ -7,7 +16,7 @@ env:
jobs:
viewer:
runs-on: 'ubuntu-20.04'
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout'
@ -25,9 +34,10 @@ jobs:
pkgdir='build/pkg/MinedMap-${{ steps.tag.outputs.tag }}-viewer'
mkdir -p "$pkgdir"
cp -r viewer/* "$pkgdir"/
rm "$pkgdir"/Dockerfile
- name: 'Archive'
uses: 'actions/upload-artifact@v3'
uses: 'actions/upload-artifact@v4'
with:
name: 'MinedMap-${{ steps.tag.outputs.tag }}-viewer'
path: 'build/pkg'
@ -38,7 +48,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
toolchain: '1.86'
components: rustfmt
- run: cargo fmt --all -- --check
@ -48,7 +58,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
toolchain: '1.86'
components: clippy
- uses: swatinem/rust-cache@v2
- uses: actions-rs/clippy-check@v1
@ -62,7 +72,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
toolchain: '1.86'
components: rust-docs
- uses: swatinem/rust-cache@v2
- run: cargo doc --workspace --no-deps --document-private-items
@ -73,12 +83,11 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
rust: [stable]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
toolchain: '1.86'
- uses: swatinem/rust-cache@v2
- run: cargo test --workspace
- run: cargo test --workspace --no-default-features
@ -91,9 +100,9 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-11'
- os: 'macos-13'
target: 'aarch64-apple-darwin'
- os: 'macos-11'
- os: 'macos-13'
target: 'x86_64-apple-darwin'
- os: 'windows-2019'
target: 'x86_64-pc-windows-msvc'
@ -101,7 +110,7 @@ jobs:
- os: 'windows-2019'
target: 'i686-pc-windows-msvc'
ext: '.exe'
- os: 'ubuntu-20.04'
- os: 'ubuntu-22.04'
target: 'x86_64-unknown-linux-gnu'
steps:
@ -118,10 +127,12 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
toolchain: '1.86'
targets: '${{ matrix.target }}'
- uses: swatinem/rust-cache@v2
with:
key: '${{ matrix.target }}'
- name: 'Build'
shell: 'bash'
@ -134,7 +145,94 @@ jobs:
cp target/${{ matrix.target }}/release/minedmap${{ matrix.ext }} "$pkgdir"/
- name: 'Archive'
uses: 'actions/upload-artifact@v3'
uses: 'actions/upload-artifact@v4'
with:
name: 'MinedMap-${{ steps.tag.outputs.tag }}-${{ matrix.target }}'
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,13 +2,150 @@
## [Unreleased] - ReleaseDate
### Added
- Added support for Minecraft 1.21.5
Added new block types and handling for changed sign text storage format.
## [2.5.0] - 2025-03-16
### Added
- Added experimental watch mode
Passing `--watch` will cause MinedMap to run continuously instead of exiting
after map generation, regenerating tiles whenever they change.
`--watch-delay` can be used to configure the delay between detecting a change
and runing the map generation, also limiting how often the regeneration
happens. This defaults to `30s`; significantly smaller values probably don't
make sense because Minecraft writes out changes in batches anyways.
Finally, `--jobs-initial` can be used to configure the number of parallel
generation threads for the initial cycle separately from the value used for
subsequent cycles after a change is detected (`-j`/`--jobs`). Subsequent
cycles usually need to regenerate only a small number of tiles, so setting
`--jobs` to a smaller value than `--jobs-initial` may be advantageous.
- Added jemalloc support to fix performace on musl targets
The global allocator can be switched to jemalloc by enabling the `jemalloc`
cargo feature now. This is not the default because it is not always faster
than the default system allocator; in particular, the glibc allocator has
slightly better performance in multithreaded mode. In addition, jemalloc
uses a bit more memory.
In addition, the `jemalloc-auto` feature has been introduced, which is enabled
by default and sets the global allocator to jemalloc on platforms where it is
clearly advantageous. For now, this is only done on musl-based targets, as
musl's default allocator is very slow in multithreaded operation (which was
making higher thread counts like `-j8` basically useless due to 7-8x
slowdowns). With the new default, performance on musl is basically identical
to glibc.
Note that some platforms like `msvc` are unsupported by jemalloc, and trying
to enable the `jemalloc` feature on these platforms may break the MinedMap
build or cause issues at runtime.
- Docker images can be downloaded from the GitHub Container registry
Two images are provided, one for the tile renderer and one with the viewer
and a web server. A `docker-compose.yml` example can be found in the
repository as a starting point.
### Changed
- Unknown biome types (from not yet supported or modded versions of Minecraft)
will now use plains biome colors as a fallback instead of resulting in water,
grass and foliage blocks to be rendered as transparent pixels
- Switched from zlib-ng to zlib-rs
This should have no noticable effect on the usage of MinedMap, but avoids
an external build dependency on CMake.
- Small (1-block) seagrass is now visible on the map
1-block seagrass in 1-block deep water would previously result in the ground
to be shown instead of water, as MinedMap currently doesn't handle the
"waterlogged" block status. As 1-block seagrass is relatively big compared to
other "small" plants, just considering it opaque seems like a good enough
solution that avoids having to implement advanced block status flags.
- Use Bincode 2 for storage of intermediate data
The update from Bincode 1 to 2 slightly reduces the size of the `processed`
directory used for intermediate data. At least Rust 1.85 is now required to
build MinedMap.
## [2.4.0] - 2025-01-11
### Added
- Added support for rendering tiles in WebP format using the `--image-format` option
## [2.3.1] - 2025-01-06
### Fixed
- Fix text colors for signs modified using dye
- Fix text colors specified using `#rrggbb` CSS syntax in JSON text
Only named colors specified via JSON text were working as intended.
The mapping of color names to values is now handled by the generator. Both the generator and the
viewer must be updated for sign text colors to work.
## [2.3.0] - 2025-01-02
### Added
- Added support for Minecraft 1.21.4 block types
- Added support for Minecraft 1.21.4 Pale Garden biome
- viewer: added images for pale oak signs
## [2.2.0] - 2024-06-23
### Added
- Added support for Minecraft 1.21 block types
## [2.1.1] - 2024-06-14
### Fixed
- Fix crash due to incorrect counting in info message
The calculation of the number of skipped regions could underflow when more invalid than valid
regions were encountered.
- Ignore empty region files instead of treating them as invalid
Minecraft generates empty region files in some cases. Just ignore them instead of printing an
error message every time.
## [2.1.0] - 2024-01-27
### Added
- Added sign layer
This feature is disabled by default. Use the `--sign-prefix` and `--sign-filter` options to
configure which signs to show on the map. `--sign-transform` allows to modify the displayed
sign text.
### Changed
- Without `--verbose`, only a single warning is printed at the end of
processing for unknown block/biome types, rather than once for every
section where such a block/biome is encountered.
## [2.0.2] - 2024-01-07
### Updated
### Added
- Added support for Minecraft 1.20.3+
Minecraft 1.20.3 renamed the `grass` block type to `short_grass`.
### Changed
- Updated [Leaflet](https://leafletjs.com/) to 1.9.4
- Updated attribution URL to https://github.com/neocturne/MinedMap
@ -57,7 +194,14 @@ intermediate data.
Full support for custom biomes datapacks might be added in a future release.
<!-- next-url -->
[Unreleased]: https://github.com/neocturne/MinedMap/compare/v2.0.2...HEAD
[Unreleased]: https://github.com/neocturne/MinedMap/compare/v2.5.0...HEAD
[2.5.0]: https://github.com/neocturne/MinedMap/compare/v2.4.0...v2.5.0
[2.4.0]: https://github.com/neocturne/MinedMap/compare/v2.3.1...v2.4.0
[2.3.1]: https://github.com/neocturne/MinedMap/compare/v2.3.0...v2.3.1
[2.3.0]: https://github.com/neocturne/MinedMap/compare/v2.2.0...v2.3.0
[2.2.0]: https://github.com/neocturne/MinedMap/compare/v2.1.1...v2.2.0
[2.1.1]: https://github.com/neocturne/MinedMap/compare/v2.1.0...v2.1.1
[2.1.0]: https://github.com/neocturne/MinedMap/compare/v2.0.2...v2.1.0
[2.0.2]: https://github.com/neocturne/MinedMap/compare/v2.0.1...v2.0.2
[2.0.1]: https://github.com/neocturne/MinedMap/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/neocturne/MinedMap/compare/v1.19.1...v2.0.0

1041
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,17 +2,18 @@
members = ["crates/*"]
[workspace.package]
edition = "2021"
edition = "2024"
license = "MIT"
readme = "README.md"
repository = "https://github.com/neocturne/MinedMap"
[workspace.metadata.release]
consolidate-commits = false
pre-release-commit-message = "{{crate_name}} {{version}}"
[package]
name = "minedmap"
version = "2.0.2"
version = "2.5.0"
description = "Generate browsable maps from Minecraft save data"
edition.workspace = true
license.workspace = true
@ -26,7 +27,6 @@ exclude = [
]
[package.metadata.release]
pre-release-commit-message = "{{crate_name}} {{version}}"
tag-message = "{{crate_name}} {{version}}"
pre-release-replacements = [
{file="CHANGELOG.md", search="Unreleased", replace="{{version}}"},
@ -38,22 +38,29 @@ pre-release-replacements = [
[dependencies]
anyhow = "1.0.68"
bincode = "1.3.3"
clap = { version = "4.1.4", features = ["derive"] }
bincode = "2.0.1"
clap = { version = "4.1.4", features = ["derive", "wrap_help"] }
enum-map = "2.7.3"
fastnbt = "2.3.2"
flate2 = { version = "1.1.0", features = ["zlib-rs"] }
futures-util = "0.3.28"
git-version = "0.3.5"
image = { version = "0.24.5", default-features = false, features = ["png"] }
indexmap = { version = "2.0.0", features = ["serde"] }
lru = "0.12.0"
minedmap-nbt = { version = "0.1.1", path = "crates/nbt", default-features = false }
minedmap-resource = { version = "0.2.0", path = "crates/resource" }
minedmap-types = { version = "0.1.2", path = "crates/types" }
humantime = "2.1.0"
image = { version = "0.25.1", default-features = false, features = ["png", "webp"] }
indexmap = "2.0.0"
lru = "0.13.0"
minedmap-default-alloc = { version = "0.1.0", path = "crates/default-alloc", optional = true }
minedmap-nbt = { version = "0.2.0", path = "crates/nbt", default-features = false }
minedmap-resource = { version = "0.7.0", path = "crates/resource" }
minedmap-types = { version = "0.2.0", path = "crates/types" }
notify = "8.0.0"
num-integer = "0.1.45"
num_cpus = "1.16.0"
phf = { version = "0.11.2", features = ["macros"] }
rayon = "1.7.0"
rustc-hash = "1.1.0"
serde = { version = "1.0.152", features = ["rc", "derive"] }
regex = "1.10.2"
rustc-hash = "2.0.0"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.99"
tokio = { version = "1.31.0", features = ["rt", "parking_lot", "sync"] }
tracing = "0.1.37"
@ -61,5 +68,6 @@ tracing-subscriber = "0.3.17"
zstd = "0.13.0"
[features]
default = ["zlib-ng"]
zlib-ng = ["minedmap-nbt/zlib-ng"]
default = ["jemalloc-auto"]
jemalloc-auto = ["dep:minedmap-default-alloc"]
jemalloc = ["jemalloc-auto", "minedmap-default-alloc/jemalloc"]

17
Dockerfile Normal file
View file

@ -0,0 +1,17 @@
FROM docker.io/library/rust:1.85.1-alpine AS builder
WORKDIR /build
RUN apk add --no-cache build-base tini-static
ARG MINEDMAP_VERSION
COPY . .
RUN cargo build -r
RUN strip target/release/minedmap
FROM scratch
COPY --from=builder /sbin/tini-static /build/target/release/minedmap /bin/
ENTRYPOINT [ "/bin/tini-static", "--", "/bin/minedmap" ]
USER 1000:1000

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2015 Matthias Schiffer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -2,7 +2,7 @@
* Render beautiful maps of your [Minecraft](https://minecraft.net/) worlds!
* Put them on a webserver and view them in your browser!
* Compatible with unmodified Minecraft Java Edition 1.8 up to 1.20 (no mod installation necessary!)
* Compatible with unmodified Minecraft Java Edition 1.8 up to 1.21.4 (no mod installation required!)
* Illumination layer: the world at night
* 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
@ -10,9 +10,9 @@
* Typically uses less than 100MB of RAM in single-threaded operation (may be higher when `-j` is passed)
* Cross-platform: runs on Linux, Windows, and likely other systems like MacOS as well
![Screenshot](https://raw.githubusercontent.com/neocturne/MinedMap/main/docs/images/MinedMap.png)
![Screenshot](https://raw.githubusercontent.com/neocturne/MinedMap/997a4fb24e89d2cd3c671d77eafaa47084d14304/docs/images/MinedMap.png)
## How to use
## About
MinedMap consists of two components: a map renderer generating map tiles from
Minecraft save games, and a viewer for displaying and navigating maps in a browser
@ -20,16 +20,23 @@ based on [Leaflet](https://leafletjs.com/). The map renderer is heavily inspired
[MapRend](https://github.com/YSelfTool/MapRend), but has been reimplemented from scratch
(first in C++, now in Rust) for highest performance.
The viewer expects the the map data in a directory named `data`. To generate a new
map, create this empty directory inside the viewer directory. Next, to generate the
map files run MinedMap passing the source and the destination paths on the command
line:
## 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,
and `C:\Users\<username>\AppData\Roaming\.minecraft\saves`. To generate MinedMap
tile data from a save game called "World", use the a command like the following
(replacing the first argument with the path to your save data; `<viewer>` refers
to the directory where you unpacked the MinedMap viewer):
```shell
minedmap /path/to/save/game /path/to/viewer/data
minedmap ~/.minecraft/saves/World <viewer>/data
```
The save game is stored in `saves` inside your Minecraft main directory
(`~/.minecraft` on Linux, `C:\Users\<username>\AppData\Roaming\.minecraft` on Windows)
in a subdirectory with the name of your world.
The first map generation might take a while for big worlds, but subsequent calls will
only rebuild tiles for region files that have changed, rarely taking more than a second
@ -38,7 +45,8 @@ MinedMap as a Cron job every minute.
Note that it is not possible to open the viewer *index.html* without a webserver, as
it cannot load the generated map information from `file://` URIs. For testing purposes,
you can use a minimal HTTP server, e.g. (if you have Python installed):
you can use a minimal HTTP server, e.g. if you have Python installed just run the
following in the viewer directory:
```shell
python3 -m http.server
```
@ -46,19 +54,69 @@ This test server is very slow and cannot handle multiple requests concurrently,
a proper webserver like [nginx](https://nginx.org/) or upload the viewer together with
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
![Sign screenshot](https://raw.githubusercontent.com/neocturne/MinedMap/e5d9c813ba3118d04dc7e52e3dc6f48808a69120/docs/images/signs.png)
MinedMap can display sign markers on the map, which will open a popup showing
the sign text when clicked.
Generation of the sign layer is disabled by default. It can be enabled by passing
the `--sign-prefix` or `--sign-filter` options to MinedMap. The options allow
to configure which signs should be displayed, and they can be passed multiple
times to show every sign that matches at least one prefix or filter.
`--sign-prefix` will make all signs visible the text of which starts with the
given prefix, so something like `--sign-prefix '[Map]'` would allow to put up
signs that start with "\[Map\]" in Minecraft to add markers to the map. An
empty prefix (`--sign-prefix ''`) can be used to make *all* signs visible on
the map.
`--sign-filter` can be used for more advanced filters based on regular expressions.
`--sign-filter '\[Map\]'` would show all signs that contain "\[Map\]"
anywhere in their text, and `--sign-filter '.'` makes all non-empty signs (signs
containing at least one character) visible. See the documentation of the
[regex crate](https://docs.rs/regex) for more information on the supported syntax.
All prefixes and filters are applied to the front and back text separately, but
both the front and the back text will be shown in the popup when one of them
matches.
Finally, `--sign-transform` allows to specify sed-style replacement patterns to
modify the text displayed on the map. This can be used if the text matched by
`--sign-prefix` or `--sign-filter` should not be displayed:
`--sign-transform 's/\[Map\]//'` would replace each occurence of "\[Map\]" with
the empty string.
**Note:** On Windows, double quotes (`"`) must be used for arguments instead
of single quotes (`'`), and all backslashes in the arguments must be escaped
by doubling them. This can make regular expressions somewhat difficult to
write and to read.
## Installation
Building the MinedMap map generator from source requires a recent Rust toolchain (1.72.0
Binary builds of the map generator for Linux and Windows, as well as an archive
containing the viewer can be found on the GitHub release page.
Building the generator from source requires a recent Rust toolchain (1.72.0
or newer). The following command can be used to build the current development version:
```shell
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,
see the [v1.19.1](https://github.com/neocturne/MinedMap/tree/v1.19.1) tag.

View file

@ -0,0 +1,17 @@
[package]
name = "minedmap-default-alloc"
version = "0.1.0"
description = "Helper crate for target-specific selection of global allocator default"
edition.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
[dependencies]
tikv-jemallocator = { version = "0.6.0", optional = true }
[target.'cfg(target_env = "musl")'.dependencies]
tikv-jemallocator = "0.6.0"
[features]
jemalloc = ["dep:tikv-jemallocator"]

View file

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

View file

@ -1,6 +1,6 @@
[package]
name = "minedmap-nbt"
version = "0.1.1"
version = "0.2.0"
description = "MinedMap's handling of Minecraft NBT data and region files"
edition.workspace = true
license.workspace = true
@ -11,12 +11,10 @@ repository.workspace = true
anyhow = "1.0.75"
bytemuck = "1.13.1"
fastnbt = "2.4.4"
flate2 = "1.0.27"
minedmap-types = { version = "0.1.2", path = "../types" }
flate2 = "1.1.0"
minedmap-types = { version = "0.2.0", path = "../types" }
serde = "1.0.183"
[features]
zlib-ng = ["flate2/zlib-ng"]
[dev-dependencies]
clap = { version = "4.3.23", features = ["derive"] }
flate2 = { version = "1.1.0", features = ["zlib-rs"] }

View file

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

View file

@ -1,6 +1,6 @@
[package]
name = "minedmap-resource"
version = "0.2.0"
version = "0.7.0"
description = "Data describing Minecraft biomes and block types"
edition.workspace = true
license.workspace = true
@ -8,6 +8,6 @@ readme.workspace = true
repository.workspace = true
[dependencies]
enumflags2 = { version = "0.7.7", features = ["serde"] }
glam = "0.25.0"
serde = { version = "1.0.183", features = ["derive"] }
bincode = "2.0.1"
enumflags2 = "0.7.7"
glam = "0.30.0"

View file

@ -1,437 +1,117 @@
//! Biome data structures
//! Biome data
//!
//! This file is generated using resource/biomes.py, do not edit
use serde::{Deserialize, Serialize};
use super::*;
use BiomeGrassColorModifier::*;
use super::Color;
/// Grass color modifier used by a biome
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BiomeGrassColorModifier {
/// Grass color modifier used by the dark forest biome
DarkForest,
/// Grass color modifier used by swamp biomes
Swamp,
}
/// A biome specification
///
/// A Biome contains all information about a biome necessary to compute a block
/// color given a block type and depth
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Biome {
/// Temperature value
///
/// For more efficient storage, the temperature is stored as an integer
/// after mutiplying the raw value by 20
pub temp: i8,
/// Downfall value
///
/// For more efficient storage, the downfall is stored as an integer
/// after mutiplying the raw value by 20
pub downfall: i8,
/// Water color override
pub water_color: Option<Color>,
/// Foliage color override
pub foliage_color: Option<Color>,
/// Grass color override
pub grass_color: Option<Color>,
/// Grass color modifier
pub grass_color_modifier: Option<BiomeGrassColorModifier>,
}
impl Biome {
/// Constructs a new Biome
const fn new(temp: i16, downfall: i16) -> Biome {
/// Helper to encode temperature and downfall values
///
/// Converts temperatue and downfall from the input format
/// (mutiplied by 100) to i8 range for more efficient storage.
const fn encode(v: i16) -> i8 {
(v / 5) as i8
}
Biome {
temp: encode(temp),
downfall: encode(downfall),
grass_color_modifier: None,
water_color: None,
foliage_color: None,
grass_color: None,
}
}
/// Builder function to override the biome water color
const fn water(self, water_color: [u8; 3]) -> Biome {
Biome {
water_color: Some(Color(water_color)),
..self
}
}
/// Builder function to override the biome foliage color
const fn foliage(self, foliage_color: [u8; 3]) -> Biome {
Biome {
foliage_color: Some(Color(foliage_color)),
..self
}
}
/// Builder function to override the biome grass color
const fn grass(self, grass_color: [u8; 3]) -> Biome {
Biome {
grass_color: Some(Color(grass_color)),
..self
}
}
/// Builder function to set a grass color modifier
const fn modify(self, grass_color_modifier: BiomeGrassColorModifier) -> Biome {
Biome {
grass_color_modifier: Some(grass_color_modifier),
..self
}
}
/// Decodes a temperature or downfall value from the storage format to
/// f32 for further calculation
fn decode(val: i8) -> f32 {
f32::from(val) / 20.0
}
/// Returns the biome's temperature decoded to its original float value
pub fn temp(&self) -> f32 {
Self::decode(self.temp)
}
/// Returns the biome's downfall decoded to its original float value
pub fn downfall(&self) -> f32 {
Self::decode(self.downfall)
}
}
/// Standard biome specifications
pub const BIOMES: &[(&str, Biome)] = {
use BiomeGrassColorModifier::*;
// Data extracted from Minecraft code decompiled using https://github.com/Hexeption/MCP-Reborn
// We can't use floats in const functions, to temperature and downfall values
// are specified multipled by 100. The underscore is used in place of the decimal point
// of the original values.
#[allow(clippy::zero_prefixed_literal)]
&[
// Overworld
(
"badlands",
Biome::new(2_00, 0_00)
.foliage([158, 129, 77])
.grass([144, 129, 77]),
),
("bamboo_jungle", Biome::new(0_95, 0_90)),
("beach", Biome::new(0_80, 0_40)),
("birch_forest", Biome::new(0_60, 0_60)),
(
"cherry_grove",
Biome::new(0_50, 0_80)
.water([93, 183, 239])
.grass([182, 219, 97])
.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]),
),
(
"deep_lukewarm_ocean",
Biome::new(0_50, 0_50).water([69, 173, 242]),
),
("deep_ocean", Biome::new(0_50, 0_50)),
("desert", Biome::new(2_00, 0_00)),
("dripstone_caves", Biome::new(0_80, 0_40)),
(
"eroded_badlands",
Biome::new(2_00, 0_00)
.foliage([158, 129, 77])
.grass([144, 129, 77]),
),
("flower_forest", Biome::new(0_70, 0_80)),
("forest", Biome::new(0_70, 0_80)),
("frozen_ocean", Biome::new(0_00, 0_50).water([57, 56, 201])),
("frozen_peaks", Biome::new(-0_70, 0_90)),
("frozen_river", Biome::new(0_00, 0_50).water([57, 56, 201])),
("grove", Biome::new(-0_20, 0_80)),
("ice_spikes", Biome::new(0_00, 0_50)),
("jagged_peaks", Biome::new(-0_70, 0_90)),
("jungle", Biome::new(0_95, 0_90)),
(
"lukewarm_ocean",
Biome::new(0_50, 0_50).water([69, 173, 242]),
),
("lush_caves", Biome::new(0_50, 0_50)),
(
"mangrove_swamp",
Biome::new(0_80, 0_90)
.water([58, 122, 106])
.foliage([141, 177, 39])
.modify(Swamp),
),
("meadow", Biome::new(0_50, 0_80).water([14, 78, 207])),
("mushroom_fields", Biome::new(0_90, 1_00)),
("ocean", Biome::new(0_50, 0_50)),
("old_growth_birch_forest", Biome::new(0_60, 0_60)),
("old_growth_pine_taiga", Biome::new(0_30, 0_80)),
("old_growth_spruce_taiga", Biome::new(0_25, 0_80)),
("plains", Biome::new(0_80, 0_40)),
("river", Biome::new(0_50, 0_50)),
("savanna", Biome::new(2_00, 0_00)),
("savanna_plateau", Biome::new(2_00, 0_00)),
("snowy_beach", Biome::new(0_05, 0_30).water([61, 87, 214])),
("snowy_plains", Biome::new(0_00, 0_50)),
("snowy_slopes", Biome::new(-0_30, 0_90)),
("snowy_taiga", Biome::new(-0_50, 0_40).water([61, 87, 214])),
("sparse_jungle", Biome::new(0_95, 0_80)),
("stony_peaks", Biome::new(1_00, 0_30)),
("stony_shore", Biome::new(0_20, 0_30)),
("sunflower_plains", Biome::new(0_80, 0_40)),
(
"swamp",
Biome::new(0_80, 0_90)
.water([97, 123, 100])
.foliage([106, 112, 57])
.modify(Swamp),
),
("taiga", Biome::new(0_25, 0_80)),
("the_void", Biome::new(0_50, 0_50)),
("warm_ocean", Biome::new(0_50, 0_50).water([67, 213, 238])),
("windswept_forest", Biome::new(0_20, 0_30)),
("windswept_gravelly_hills", Biome::new(0_20, 0_30)),
("windswept_hills", Biome::new(0_20, 0_30)),
("windswept_savanna", Biome::new(2_00, 0_00)),
(
"wooded_badlands",
Biome::new(2_00, 0_00)
.foliage([158, 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"),
/// List if known biomes and their properties
pub const BIOMES: &[(&str, Biome)] = &[
(
"mutated_extreme_hills_with_trees",
"modified_gravelly_mountains",
"badlands",
Biome::new(200, 0)
.foliage([158, 129, 77])
.grass([144, 129, 77]),
),
("bamboo_jungle", Biome::new(95, 90)),
("basalt_deltas", Biome::new(200, 0)),
("beach", Biome::new(80, 40)),
("birch_forest", Biome::new(60, 60)),
(
"cherry_grove",
Biome::new(50, 80)
.foliage([182, 219, 97])
.grass([182, 219, 97])
.water([93, 183, 239]),
),
("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",
Biome::new(50, 50).water([69, 173, 242]),
),
("deep_ocean", Biome::new(50, 50)),
("desert", Biome::new(200, 0)),
("dripstone_caves", Biome::new(80, 40)),
("end_barrens", Biome::new(50, 50)),
("end_highlands", Biome::new(50, 50)),
("end_midlands", Biome::new(50, 50)),
(
"eroded_badlands",
Biome::new(200, 0)
.foliage([158, 129, 77])
.grass([144, 129, 77]),
),
("flower_forest", Biome::new(70, 80)),
("forest", Biome::new(70, 80)),
("frozen_ocean", Biome::new(0, 50).water([57, 56, 201])),
("frozen_peaks", Biome::new(-70, 90)),
("frozen_river", Biome::new(0, 50).water([57, 56, 201])),
("grove", Biome::new(-20, 80)),
("ice_spikes", Biome::new(0, 50)),
("jagged_peaks", Biome::new(-70, 90)),
("jungle", Biome::new(95, 90)),
("lukewarm_ocean", Biome::new(50, 50).water([69, 173, 242])),
("lush_caves", Biome::new(50, 50)),
(
"mangrove_swamp",
Biome::new(80, 90)
.foliage([141, 177, 39])
.modify(Swamp)
.water([58, 122, 106]),
),
("meadow", Biome::new(50, 80).water([14, 78, 207])),
("mushroom_fields", Biome::new(90, 100)),
("nether_wastes", Biome::new(200, 0)),
("ocean", Biome::new(50, 50)),
("old_growth_birch_forest", Biome::new(60, 60)),
("old_growth_pine_taiga", Biome::new(30, 80)),
("old_growth_spruce_taiga", Biome::new(25, 80)),
(
"pale_garden",
Biome::new(70, 80)
.foliage([135, 141, 118])
.grass([119, 130, 114])
.water([118, 136, 157]),
),
("plains", Biome::new(80, 40)),
("river", Biome::new(50, 50)),
("savanna", Biome::new(200, 0)),
("savanna_plateau", Biome::new(200, 0)),
("small_end_islands", Biome::new(50, 50)),
("snowy_beach", Biome::new(5, 30).water([61, 87, 214])),
("snowy_plains", Biome::new(0, 50)),
("snowy_slopes", Biome::new(-30, 90)),
("snowy_taiga", Biome::new(-50, 40).water([61, 87, 214])),
("soul_sand_valley", Biome::new(200, 0)),
("sparse_jungle", Biome::new(95, 80)),
("stony_peaks", Biome::new(100, 30)),
("stony_shore", Biome::new(20, 30)),
("sunflower_plains", Biome::new(80, 40)),
(
"swamp",
Biome::new(80, 90)
.foliage([106, 112, 57])
.modify(Swamp)
.water([97, 123, 100]),
),
("taiga", Biome::new(25, 80)),
("the_end", Biome::new(50, 50)),
("the_void", Biome::new(50, 50)),
("warm_ocean", Biome::new(50, 50).water([67, 213, 238])),
("warped_forest", Biome::new(200, 0)),
("windswept_forest", Biome::new(20, 30)),
("windswept_gravelly_hills", Biome::new(20, 30)),
("windswept_hills", Biome::new(20, 30)),
("windswept_savanna", Biome::new(200, 0)),
(
"wooded_badlands",
Biome::new(200, 0)
.foliage([158, 129, 77])
.grass([144, 129, 77]),
),
("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

@ -1,6 +1,6 @@
//! Functions for computations of block colors
use super::{Biome, BlockType, Color, Colorf};
use super::{Biome, BlockColor, Color, Colorf};
/// Converts an u8 RGB color to a float vector
fn color_vec_unscaled(color: Color) -> Colorf {
@ -91,18 +91,18 @@ const BIRCH_COLOR: Colorf = Colorf::new(0.502, 0.655, 0.333); // == color_vec(Co
/// Color multiplier for spruce leaves
const EVERGREEN_COLOR: Colorf = Colorf::new(0.380, 0.600, 0.380); // == color_vec(Color([97, 153, 97]))
/// Determined if calling [block_color] for a given [BlockType] needs biome information
pub fn needs_biome(block: BlockType) -> bool {
/// Determined if calling [block_color] for a given [BlockColor] needs biome information
pub fn needs_biome(block: BlockColor) -> bool {
use super::BlockFlag::*;
block.is(Grass) || block.is(Foliage) || block.is(Water)
}
/// Determined the block color to display for a given [BlockType]
/// Determined the block color to display for a given [BlockColor]
///
/// [needs_biome] must be used to determine whether passing a [Biome] is necessary.
/// Will panic if a [Biome] is necessary, but none is passed.
pub fn block_color(block: BlockType, biome: Option<&Biome>, depth: f32) -> Colorf {
pub fn block_color(block: BlockColor, biome: Option<&Biome>, depth: f32) -> Colorf {
use super::BlockFlag::*;
let get_biome = || biome.expect("needs biome to determine block color");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,202 @@
//! Manually maintained biome data (aliases and legacy biome IDs)
/// Biome ID aliases
///
/// Some biomes have been renamed or merged in recent Minecraft versions.
/// Maintain a list of aliases to support chunks saved by older versions.
pub const BIOME_ALIASES: &[(&str, &str)] = &[
// Biomes fix
("beaches", "beach"),
("cold_beach", "snowy_beach"),
("cold_deep_ocean", "deep_cold_ocean"),
("extreme_hills", "mountains"),
("extreme_hills_with_trees", "wooded_mountains"),
("forest_hills", "wooded_hills"),
("frozen_deep_ocean", "deep_frozen_ocean"),
("hell", "nether_wastes"),
("ice_flats", "snowy_tundra"),
("ice_mountains", "snowy_mountains"),
("lukewarm_deep_ocean", "deep_lukewarm_ocean"),
("mesa", "badlands"),
("mesa_clear_rock", "badlands_plateau"),
("mesa_rock", "wooded_badlands_plateau"),
("mushroom_island", "mushroom_fields"),
("mushroom_island_shore", "mushroom_field_shore"),
("mutated_birch_forest", "tall_birch_forest"),
("mutated_birch_forest_hills", "tall_birch_hills"),
("mutated_desert", "desert_lakes"),
("mutated_extreme_hills", "gravelly_mountains"),
(
"mutated_extreme_hills_with_trees",
"modified_gravelly_mountains",
),
("mutated_forest", "flower_forest"),
("mutated_ice_flats", "ice_spikes"),
("mutated_jungle", "modified_jungle"),
("mutated_jungle_edge", "modified_jungle_edge"),
("mutated_mesa", "eroded_badlands"),
("mutated_mesa_clear_rock", "modified_badlands_plateau"),
("mutated_mesa_rock", "modified_wooded_badlands_plateau"),
("mutated_plains", "sunflower_plains"),
("mutated_redwood_taiga", "giant_spruce_taiga"),
("mutated_redwood_taiga_hills", "giant_spruce_taiga_hills"),
("mutated_roofed_forest", "dark_forest_hills"),
("mutated_savanna", "shattered_savanna"),
("mutated_savanna_rock", "shattered_savanna_plateau"),
("mutated_swampland", "swamp_hills"),
("mutated_taiga", "taiga_mountains"),
("mutated_taiga_cold", "snowy_taiga_mountains"),
("redwood_taiga", "giant_tree_taiga"),
("redwood_taiga_hills", "giant_tree_taiga_hills"),
("roofed_forest", "dark_forest"),
("savanna_rock", "savanna_plateau"),
("sky", "the_end"),
("sky_island_barren", "end_barrens"),
("sky_island_high", "end_highlands"),
("sky_island_low", "small_end_islands"),
("sky_island_medium", "end_midlands"),
("smaller_extreme_hills", "mountain_edge"),
("stone_beach", "stone_shore"),
("swampland", "swamp"),
("taiga_cold", "snowy_taiga"),
("taiga_cold_hills", "snowy_taiga_hills"),
("void", "the_void"),
("warm_deep_ocean", "deep_warm_ocean"),
// Nether biome rename
("nether", "nether_wastes"),
// Caves and Cliffs biome renames
("badlands_plateau", "badlands"),
("bamboo_jungle_hills", "bamboo_jungle"),
("birch_forest_hills", "birch_forest"),
("dark_forest_hills", "dark_forest"),
("desert_hills", "desert"),
("desert_lakes", "desert"),
("giant_spruce_taiga", "old_growth_spruce_taiga"),
("giant_spruce_taiga_hills", "old_growth_spruce_taiga"),
("giant_tree_taiga", "old_growth_pine_taiga"),
("giant_tree_taiga_hills", "old_growth_pine_taiga"),
("gravelly_mountains", "windswept_gravelly_hills"),
("jungle_edge", "sparse_jungle"),
("jungle_hills", "jungle"),
("lofty_peaks", "jagged_peaks"),
("modified_badlands_plateau", "badlands"),
("modified_gravelly_mountains", "windswept_gravelly_hills"),
("modified_jungle", "jungle"),
("modified_jungle_edge", "sparse_jungle"),
("modified_wooded_badlands_plateau", "wooded_badlands"),
("mountain_edge", "windswept_hills"),
("mountains", "windswept_hills"),
("mushroom_field_shore", "mushroom_fields"),
("shattered_savanna", "windswept_savanna"),
("shattered_savanna_plateau", "windswept_savanna"),
("snowcapped_peaks", "frozen_peaks"),
("snowy_mountains", "snowy_plains"),
("snowy_taiga_hills", "snowy_taiga"),
("snowy_taiga_mountains", "snowy_taiga"),
("snowy_tundra", "snowy_plains"),
("stone_shore", "stony_shore"),
("swamp_hills", "swamp"),
("taiga_hills", "taiga"),
("taiga_mountains", "taiga"),
("tall_birch_forest", "old_growth_birch_forest"),
("tall_birch_hills", "old_growth_birch_forest"),
("wooded_badlands_plateau", "wooded_badlands"),
("wooded_hills", "forest"),
("wooded_mountains", "windswept_forest"),
// Remove Deep Warm Ocean
("deep_warm_ocean", "warm_ocean"),
];
/// Maps old numeric biome IDs to new string IDs
pub fn legacy_biome(index: u8) -> &'static str {
match index {
0 => "ocean",
1 => "plains",
2 => "desert",
3 => "mountains",
4 => "forest",
5 => "taiga",
6 => "swamp",
7 => "river",
8 => "nether_wastes",
9 => "the_end",
10 => "frozen_ocean",
11 => "frozen_river",
12 => "snowy_tundra",
13 => "snowy_mountains",
14 => "mushroom_fields",
15 => "mushroom_field_shore",
16 => "beach",
17 => "desert_hills",
18 => "wooded_hills",
19 => "taiga_hills",
20 => "mountain_edge",
21 => "jungle",
22 => "jungle_hills",
23 => "jungle_edge",
24 => "deep_ocean",
25 => "stone_shore",
26 => "snowy_beach",
27 => "birch_forest",
28 => "birch_forest_hills",
29 => "dark_forest",
30 => "snowy_taiga",
31 => "snowy_taiga_hills",
32 => "giant_tree_taiga",
33 => "giant_tree_taiga_hills",
34 => "wooded_mountains",
35 => "savanna",
36 => "savanna_plateau",
37 => "badlands",
38 => "wooded_badlands_plateau",
39 => "badlands_plateau",
40 => "small_end_islands",
41 => "end_midlands",
42 => "end_highlands",
43 => "end_barrens",
44 => "warm_ocean",
45 => "lukewarm_ocean",
46 => "cold_ocean",
47 => "deep_warm_ocean",
48 => "deep_lukewarm_ocean",
49 => "deep_cold_ocean",
50 => "deep_frozen_ocean",
127 => "the_void",
129 => "sunflower_plains",
130 => "desert_lakes",
131 => "gravelly_mountains",
132 => "flower_forest",
133 => "taiga_mountains",
134 => "swamp_hills",
140 => "ice_spikes",
149 => "modified_jungle",
151 => "modified_jungle_edge",
155 => "tall_birch_forest",
156 => "tall_birch_hills",
157 => "dark_forest_hills",
158 => "snowy_taiga_mountains",
160 => "giant_spruce_taiga",
161 => "giant_spruce_taiga_hills",
162 => "modified_gravelly_mountains",
163 => "shattered_savanna",
164 => "shattered_savanna_plateau",
165 => "eroded_badlands",
166 => "modified_wooded_badlands_plateau",
167 => "modified_badlands_plateau",
168 => "bamboo_jungle",
169 => "bamboo_jungle_hills",
170 => "soul_sand_valley",
171 => "crimson_forest",
172 => "warped_forest",
173 => "basalt_deltas",
174 => "dripstone_caves",
175 => "lush_caves",
177 => "meadow",
178 => "grove",
179 => "snowy_slopes",
180 => "snowcapped_peaks",
181 => "lofty_peaks",
182 => "stony_peaks",
_ => "ocean",
}
}

View file

@ -13,6 +13,7 @@ const DEF: &str = "air";
const EMPTY: [&str; 16] = simple(DEF);
/// 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] = [
/* 0 */
simple("air"),

View file

@ -4,20 +4,19 @@
mod biomes;
mod block_color;
mod legacy_block_types;
#[allow(clippy::missing_docs_in_private_items)] // Generated module
mod block_types;
mod legacy_biomes;
mod legacy_block_types;
use std::collections::HashMap;
use enumflags2::{bitflags, BitFlags};
use serde::{Deserialize, Serialize};
use bincode::{BorrowDecode, Decode, Encode};
use enumflags2::{BitFlags, bitflags};
/// Flags describing special properties of [BlockType]s
#[bitflags]
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BlockFlag {
/// The block type is opaque
Opaque,
@ -27,36 +26,105 @@ pub enum BlockFlag {
Foliage,
/// The block type is birch foliage
Birch,
/// The block type is spurce foliage
/// The block type is spruce foliage
Spruce,
/// The block type is colored using biome water colors
Water,
/// The block type is a wall sign
///
/// The WallSign flag is used to distinguish wall signs from
/// freestanding or -hanging signs.
WallSign,
}
/// An RGB color with u8 components
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Encode, Decode)]
pub struct Color(pub [u8; 3]);
/// An RGB color with f32 components
pub type Colorf = glam::Vec3;
/// A block type specification
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct BlockType {
#[derive(Debug, Clone, Copy)]
pub struct BlockColor {
/// Bit set of [BlockFlag]s describing special properties of the block type
pub flags: BitFlags<BlockFlag>,
/// Base color of the block type
pub color: Color,
}
impl BlockType {
/// Checks whether a block type has a given [BlockFlag] set
impl BlockColor {
/// Checks whether a block color has a given [BlockFlag] set
#[inline]
pub fn is(&self, flag: BlockFlag) -> bool {
self.flags.contains(flag)
}
}
impl Encode for BlockColor {
fn encode<E: bincode::enc::Encoder>(
&self,
encoder: &mut E,
) -> Result<(), bincode::error::EncodeError> {
bincode::Encode::encode(&self.flags.bits(), encoder)?;
bincode::Encode::encode(&self.color, encoder)?;
Ok(())
}
}
impl<Context> Decode<Context> for BlockColor {
fn decode<D: bincode::de::Decoder<Context = Context>>(
decoder: &mut D,
) -> Result<Self, bincode::error::DecodeError> {
Ok(BlockColor {
flags: BitFlags::from_bits(bincode::Decode::decode(decoder)?).or(Err(
bincode::error::DecodeError::Other("invalid block flags"),
))?,
color: bincode::Decode::decode(decoder)?,
})
}
}
impl<'de, Context> BorrowDecode<'de, Context> for BlockColor {
fn borrow_decode<D: bincode::de::BorrowDecoder<'de, Context = Context>>(
decoder: &mut D,
) -> Result<Self, bincode::error::DecodeError> {
Ok(BlockColor {
flags: BitFlags::from_bits(bincode::BorrowDecode::borrow_decode(decoder)?).or(Err(
bincode::error::DecodeError::Other("invalid block flags"),
))?,
color: bincode::BorrowDecode::borrow_decode(decoder)?,
})
}
}
/// A block type specification (for use in constants)
#[derive(Debug, Clone)]
struct ConstBlockType {
/// Determines the rendered color of the block type
pub block_color: BlockColor,
/// Material of a sign block
pub sign_material: Option<&'static str>,
}
/// A block type specification
#[derive(Debug, Clone)]
pub struct BlockType {
/// Determines the rendered color of the block type
pub block_color: BlockColor,
/// Material of a sign block
pub sign_material: Option<String>,
}
impl From<&ConstBlockType> for BlockType {
fn from(value: &ConstBlockType) -> Self {
BlockType {
block_color: value.block_color,
sign_material: value.sign_material.map(String::from),
}
}
}
/// Used to look up standard Minecraft block types
#[derive(Debug)]
pub struct BlockTypes {
@ -70,10 +138,15 @@ impl Default for BlockTypes {
fn default() -> Self {
let block_type_map: HashMap<_, _> = block_types::BLOCK_TYPES
.iter()
.map(|(k, v)| (String::from(*k), *v))
.map(|(k, v)| (String::from(*k), BlockType::from(v)))
.collect();
let legacy_block_types = Box::new(legacy_block_types::LEGACY_BLOCK_TYPES.map(|inner| {
inner.map(|id| *block_type_map.get(id).expect("Unknown legacy block type"))
inner.map(|id| {
block_type_map
.get(id)
.expect("Unknown legacy block type")
.clone()
})
}));
BlockTypes {
@ -86,21 +159,124 @@ impl Default for BlockTypes {
impl BlockTypes {
/// Resolves a Minecraft 1.13+ string block type ID
#[inline]
pub fn get(&self, id: &str) -> Option<BlockType> {
pub fn get(&self, id: &str) -> Option<&BlockType> {
let suffix = id.strip_prefix("minecraft:")?;
self.block_type_map.get(suffix).copied()
self.block_type_map.get(suffix)
}
/// Resolves a Minecraft pre-1.13 numeric block type ID
#[inline]
pub fn get_legacy(&self, id: u8, data: u8) -> Option<BlockType> {
Some(self.legacy_block_types[id as usize][data as usize])
pub fn get_legacy(&self, id: u8, data: u8) -> Option<&BlockType> {
Some(&self.legacy_block_types[id as usize][data as usize])
}
}
pub use biomes::{Biome, BiomeGrassColorModifier};
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
#[derive(Debug)]
pub struct BiomeTypes {
@ -108,6 +284,8 @@ pub struct BiomeTypes {
biome_map: HashMap<String, &'static Biome>,
/// Array used to look up old numeric biome IDs
legacy_biomes: Box<[&'static Biome; 256]>,
/// Fallback for unknown (new/modded) biomes
fallback_biome: &'static Biome,
}
impl Default for BiomeTypes {
@ -117,7 +295,7 @@ impl Default for BiomeTypes {
.map(|(k, v)| (String::from(*k), v))
.collect();
for &(old, new) in biomes::BIOME_ALIASES.iter().rev() {
for &(old, new) in legacy_biomes::BIOME_ALIASES.iter().rev() {
let biome = biome_map
.get(new)
.copied()
@ -127,16 +305,19 @@ impl Default for BiomeTypes {
let legacy_biomes = (0..=255)
.map(|index| {
let id = biomes::legacy_biome(index);
let id = legacy_biomes::legacy_biome(index);
*biome_map.get(id).expect("Unknown legacy biome")
})
.collect::<Box<[_]>>()
.try_into()
.unwrap();
let fallback_biome = *biome_map.get("plains").expect("Plains biome undefined");
Self {
biome_map,
legacy_biomes,
fallback_biome,
}
}
}
@ -154,4 +335,10 @@ impl BiomeTypes {
pub fn get_legacy(&self, id: u8) -> Option<&Biome> {
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]
name = "minedmap-types"
version = "0.1.2"
version = "0.2.0"
description = "Common types used by several MinedMap crates"
edition.workspace = true
license.workspace = true
@ -8,5 +8,5 @@ readme.workspace = true
repository.workspace = true
[dependencies]
itertools = "0.12.0"
serde = { version = "1.0.183", features = ["derive"] }
bincode = "2.0.1"
itertools = "0.14.0"

View file

@ -8,8 +8,8 @@ use std::{
ops::{Index, IndexMut},
};
use bincode::{Decode, Encode};
use itertools::iproduct;
use serde::{Deserialize, Serialize};
/// Const generic AXIS arguments for coordinate types
pub mod axis {
@ -47,12 +47,11 @@ macro_rules! coord_type {
/// Returns an iterator over all possible values of the type
#[inline]
pub fn iter() -> impl Iterator<Item = $t<AXIS>>
+ DoubleEndedIterator
+ ExactSizeIterator
+ FusedIterator
+ Clone
+ Debug {
pub fn iter() -> impl DoubleEndedIterator<Item = $t<AXIS>>
+ ExactSizeIterator
+ FusedIterator
+ Clone
+ Debug {
(0..Self::MAX as u8).map($t)
}
}
@ -111,7 +110,7 @@ impl LayerBlockCoords {
/// Generic array for data stored per block of a chunk layer
///
/// Includes various convenient iteration functions.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
pub struct LayerBlockArray<T>(pub [[T; BLOCKS_PER_CHUNK]; BLOCKS_PER_CHUNK]);
impl<T> Index<LayerBlockCoords> for LayerBlockArray<T> {
@ -197,7 +196,7 @@ impl Debug for ChunkCoords {
/// Generic array for data stored per chunk of a region
///
/// Includes various convenient iteration functions.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
pub struct ChunkArray<T>(pub [[T; CHUNKS_PER_REGION]; CHUNKS_PER_REGION]);
impl<T> ChunkArray<T> {

51
docker-compose.yml Normal file
View file

@ -0,0 +1,51 @@
# This is an example docker-compose configuration providing a Minecraft server,
# map generator and webserver. Visit http://localhost:8080 to view the map.
#
# See https://docker-minecraft-server.readthedocs.io/ for more information on
# the itzg/minecraft-server image and its configuration.
services:
mc:
image: docker.io/itzg/minecraft-server
environment:
EULA: 'true'
ports:
- '25565:25565'
volumes:
- data:/data
stdin_open: true
tty: true
restart: unless-stopped
minedmap:
image: ghcr.io/neocturne/minedmap/minedmap
command:
- '--jobs-initial=2'
- '--image-format=webp'
- '--sign-filter=\[Map\]'
- '--sign-transform=s/\[Map\]//'
- '--watch'
- '/input/world'
- '/output'
volumes:
- data:/input:ro
- output:/output
- processed:/output/processed
network_mode: 'none'
depends_on:
mc:
condition: service_healthy
restart: unless-stopped
viewer:
image: ghcr.io/neocturne/minedmap/viewer
ports:
- '8080:80'
volumes:
- output:/usr/share/nginx/html/data:ro
restart: unless-stopped
volumes:
data: {}
processed: {}
output: {}

BIN
docs/images/signs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View file

@ -11,12 +11,15 @@ work.
- `extract.py`: Takes the block type information from `blocks.json` and texture data
from an unpacked Minecraft JAR, storing the result in `colors.json`
- `generate.py`: Generates `block_types.rs` from `colors.json`
- `biomes.py`: Generates `biomes.rs` from biome JSON files of an unpacked
Minecraft JAR
- `sign_textures.py`: Generates all needed sign graphics from Minecraft assets
In addition to these scripts, the JSON processor *jq* is a useful tool to work
with MinedMap's resource metadata.
## How to add support for block IDs of a new Minecraft version
## How to add support for block IDs and biomes of a new Minecraft version
1. Download the Minecraft version you want to support as well as the previous
version currently supported by MinedMap. You can use the Minecraft launcher
@ -42,12 +45,13 @@ with MinedMap's resource metadata.
5. Edit `blocks.json` until the following command passes without errors:
```sh
./extract.py blocks.json data/new/assets/minecraft/textures/block colors.json
./extract.py blocks.json data/new colors.json
```
If possible, the top texture of blocks should be used where different sides
exist. Block types that should not be visible on the map are just set to
`null` in the JSON.
`null` in the JSON (or have a `null` `texture` field when other flags need
to be set, like for sign blocks).
The `water`, `grass` and `foliage` flags control biome-dependent texture color modifiers.
@ -67,6 +71,17 @@ with MinedMap's resource metadata.
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
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.

70
resource/biomes.py Executable file
View file

@ -0,0 +1,70 @@
#!/usr/bin/env python3
import json
import os
import sys
if len(sys.argv) != 3:
sys.exit('Usage: biomes.py <data directory> <biomes.rs>')
biomes = {}
for file in os.scandir(os.path.join(sys.argv[1], 'data/minecraft/worldgen/biome')):
(name, ext) = os.path.splitext(file.name)
if ext != '.json':
continue
with open(file) as f:
data = json.load(f)
biomes[name] = {
'downfall': data['downfall'],
'temperature': data['temperature'],
'foliage_color': data['effects'].get('foliage_color'),
'grass_color': data['effects'].get('grass_color'),
'grass_color_modifier': data['effects'].get('grass_color_modifier'),
'water_color': data['effects'].get('water_color'),
}
def color(v):
return f'[{v>>16}, {(v>>8)&0xff}, {v&0xff}]'
# Converts the snake_case grass color modifier to CamelCase
def modify(v):
return ''.join([s.capitalize() for s in v.split('_')])
def gen_biome(name, info, f):
temp = round(100*info['temperature'])
downfall = round(100*info['downfall'])
foliage_color = info['foliage_color']
grass_color = info['grass_color']
grass_color_modifier = info['grass_color_modifier']
water_color = info['water_color']
print(f'\t("{name}", Biome::new({temp}, {downfall})', file=f)
if foliage_color is not None:
print(f'\t\t.foliage({color(foliage_color)})', file=f)
if grass_color is not None:
print(f'\t\t.grass({color(grass_color)})', file=f)
if grass_color_modifier is not None:
print(f'\t\t.modify({modify(grass_color_modifier)})', file=f)
if water_color is not None and water_color != 0x3f76e4:
print(f'\t\t.water({color(water_color)})', file=f)
print('\t),', file=f)
with open(sys.argv[2], 'w') as f:
print('//! Biome data', file=f);
print('//!', file=f);
print('//! This file is generated using resource/biomes.py, do not edit', file=f);
print('', file=f)
print('use super::*;', file=f)
print('use BiomeGrassColorModifier::*;', file=f)
print('', file=f)
print('/// List if known biomes and their properties', file=f);
print('pub const BIOMES: &[(&str, Biome)] = &[', file=f)
for name in sorted(biomes):
gen_biome(name, biomes[name], f)
print('];', file=f)

View file

@ -9,7 +9,10 @@
"acacia_fence_gate": {
"texture": "acacia_planks"
},
"acacia_hanging_sign": null,
"acacia_hanging_sign": {
"sign_material": "acacia",
"texture": null
},
"acacia_leaves": {
"foliage": true
},
@ -22,7 +25,8 @@
},
"acacia_sapling": {},
"acacia_sign": {
"texture": "acacia_planks"
"sign_material": "acacia",
"texture": null
},
"acacia_slab": {
"texture": "acacia_planks"
@ -31,8 +35,16 @@
"texture": "acacia_planks"
},
"acacia_trapdoor": {},
"acacia_wall_hanging_sign": null,
"acacia_wall_sign": null,
"acacia_wall_hanging_sign": {
"sign_material": "acacia",
"texture": null,
"wall_sign": true
},
"acacia_wall_sign": {
"sign_material": "acacia",
"texture": null,
"wall_sign": true
},
"acacia_wood": {
"texture": "acacia_log"
},
@ -84,7 +96,10 @@
"bamboo_fence_gate": {
"texture": "bamboo_planks"
},
"bamboo_hanging_sign": null,
"bamboo_hanging_sign": {
"sign_material": "bamboo",
"texture": null
},
"bamboo_mosaic": {},
"bamboo_mosaic_slab": {
"texture": "bamboo_mosaic"
@ -98,7 +113,8 @@
},
"bamboo_sapling": null,
"bamboo_sign": {
"texture": "bamboo_planks"
"sign_material": "bamboo",
"texture": null
},
"bamboo_slab": {
"texture": "bamboo_planks"
@ -107,8 +123,16 @@
"texture": "bamboo_planks"
},
"bamboo_trapdoor": {},
"bamboo_wall_hanging_sign": null,
"bamboo_wall_sign": null,
"bamboo_wall_hanging_sign": {
"sign_material": "bamboo",
"texture": null,
"wall_sign": true
},
"bamboo_wall_sign": {
"sign_material": "bamboo",
"texture": null,
"wall_sign": true
},
"barrel": {
"texture": "barrel_top"
},
@ -144,7 +168,10 @@
"birch_fence_gate": {
"texture": "birch_planks"
},
"birch_hanging_sign": null,
"birch_hanging_sign": {
"sign_material": "birch",
"texture": null
},
"birch_leaves": {
"birch": true
},
@ -157,7 +184,8 @@
},
"birch_sapling": {},
"birch_sign": {
"texture": "birch_planks"
"sign_material": "birch",
"texture": null
},
"birch_slab": {
"texture": "birch_planks"
@ -166,8 +194,16 @@
"texture": "birch_planks"
},
"birch_trapdoor": {},
"birch_wall_hanging_sign": null,
"birch_wall_sign": null,
"birch_wall_hanging_sign": {
"sign_material": "birch",
"texture": null,
"wall_sign": true
},
"birch_wall_sign": {
"sign_material": "birch",
"texture": null,
"wall_sign": true
},
"birch_wood": {
"texture": "birch_log"
},
@ -280,9 +316,13 @@
"bubble_coral_fan": null,
"bubble_coral_wall_fan": null,
"budding_amethyst": {},
"bush": {
"grass": true
},
"cactus": {
"texture": "cactus_top"
},
"cactus_flower": {},
"cake": {
"texture": "cake_top"
},
@ -326,7 +366,10 @@
"cherry_fence_gate": {
"texture": "cherry_planks"
},
"cherry_hanging_sign": null,
"cherry_hanging_sign": {
"sign_material": "cherry",
"texture": null
},
"cherry_leaves": {},
"cherry_log": {
"texture": "cherry_log_top"
@ -337,7 +380,8 @@
},
"cherry_sapling": null,
"cherry_sign": {
"texture": "cherry_planks"
"sign_material": "cherry",
"texture": null
},
"cherry_slab": {
"texture": "cherry_planks"
@ -346,8 +390,16 @@
"texture": "cherry_planks"
},
"cherry_trapdoor": {},
"cherry_wall_hanging_sign": null,
"cherry_wall_sign": null,
"cherry_wall_hanging_sign": {
"sign_material": "cherry",
"texture": null,
"wall_sign": true
},
"cherry_wall_sign": {
"sign_material": "cherry",
"texture": null,
"wall_sign": true
},
"cherry_wood": {
"texture": "cherry_log"
},
@ -360,6 +412,7 @@
"chiseled_bookshelf": {
"texture": "chiseled_bookshelf_top"
},
"chiseled_copper": {},
"chiseled_deepslate": {},
"chiseled_nether_bricks": {},
"chiseled_polished_blackstone": {},
@ -367,13 +420,19 @@
"chiseled_red_sandstone": {
"texture": "red_sandstone_top"
},
"chiseled_resin_bricks": {},
"chiseled_sandstone": {
"texture": "sandstone_top"
},
"chiseled_stone_bricks": {},
"chiseled_tuff": {},
"chiseled_tuff_bricks": {
"texture": "chiseled_tuff_bricks_top"
},
"chorus_flower": {},
"chorus_plant": {},
"clay": {},
"closed_eyeblossom": null,
"coal_block": {},
"coal_ore": {},
"coarse_dirt": {},
@ -410,16 +469,28 @@
},
"conduit": {},
"copper_block": {},
"copper_bulb": {},
"copper_door": {
"texture": "copper_door_top"
},
"copper_grate": {},
"copper_ore": {},
"copper_trapdoor": {},
"cornflower": null,
"cracked_deepslate_bricks": {},
"cracked_deepslate_tiles": {},
"cracked_nether_bricks": {},
"cracked_polished_blackstone_bricks": {},
"cracked_stone_bricks": {},
"crafter": {
"texture": "crafter_top"
},
"crafting_table": {
"texture": "crafting_table_top"
},
"creaking_heart": {
"texture": "creaking_heart_top"
},
"creeper_head": null,
"creeper_wall_head": null,
"crimson_button": null,
@ -433,7 +504,10 @@
"texture": "crimson_planks"
},
"crimson_fungus": null,
"crimson_hanging_sign": null,
"crimson_hanging_sign": {
"sign_material": "crimson",
"texture": null
},
"crimson_hyphae": {
"texture": "crimson_stem"
},
@ -444,7 +518,8 @@
},
"crimson_roots": {},
"crimson_sign": {
"texture": "crimson_planks"
"sign_material": "crimson",
"texture": null
},
"crimson_slab": {
"texture": "crimson_planks"
@ -456,8 +531,16 @@
"texture": "crimson_stem_top"
},
"crimson_trapdoor": {},
"crimson_wall_hanging_sign": null,
"crimson_wall_sign": null,
"crimson_wall_hanging_sign": {
"sign_material": "crimson",
"texture": null,
"wall_sign": true
},
"crimson_wall_sign": {
"sign_material": "crimson",
"texture": null,
"wall_sign": true
},
"crying_obsidian": {},
"cut_copper": {},
"cut_copper_slab": {
@ -512,7 +595,10 @@
"dark_oak_fence_gate": {
"texture": "dark_oak_planks"
},
"dark_oak_hanging_sign": null,
"dark_oak_hanging_sign": {
"sign_material": "dark_oak",
"texture": null
},
"dark_oak_leaves": {
"foliage": true
},
@ -525,7 +611,8 @@
},
"dark_oak_sapling": {},
"dark_oak_sign": {
"texture": "dark_oak_planks"
"sign_material": "dark_oak",
"texture": null
},
"dark_oak_slab": {
"texture": "dark_oak_planks"
@ -534,8 +621,16 @@
"texture": "dark_oak_planks"
},
"dark_oak_trapdoor": {},
"dark_oak_wall_hanging_sign": null,
"dark_oak_wall_sign": null,
"dark_oak_wall_hanging_sign": {
"sign_material": "dark_oak",
"texture": null,
"wall_sign": true
},
"dark_oak_wall_sign": {
"sign_material": "dark_oak",
"texture": null,
"wall_sign": true
},
"dark_oak_wood": {
"texture": "dark_oak_log"
},
@ -661,7 +756,14 @@
"ender_chest": {
"texture": "obsidian"
},
"exposed_chiseled_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_slab": {
"texture": "exposed_cut_copper"
@ -680,6 +782,7 @@
"fire_coral_block": {},
"fire_coral_fan": null,
"fire_coral_wall_fan": null,
"firefly_bush": {},
"fletching_table": {
"texture": "fletching_table_top"
},
@ -771,6 +874,7 @@
"hay_block": {
"texture": "hay_block_top"
},
"heavy_core": {},
"heavy_weighted_pressure_plate": {
"texture": "iron_block"
},
@ -832,7 +936,10 @@
"jungle_fence_gate": {
"texture": "jungle_planks"
},
"jungle_hanging_sign": null,
"jungle_hanging_sign": {
"sign_material": "jungle",
"texture": null
},
"jungle_leaves": {
"foliage": true
},
@ -845,7 +952,8 @@
},
"jungle_sapling": {},
"jungle_sign": {
"texture": "jungle_planks"
"sign_material": "jungle",
"texture": null
},
"jungle_slab": {
"texture": "jungle_planks"
@ -854,8 +962,16 @@
"texture": "jungle_planks"
},
"jungle_trapdoor": {},
"jungle_wall_hanging_sign": null,
"jungle_wall_sign": null,
"jungle_wall_hanging_sign": {
"sign_material": "jungle",
"texture": null,
"wall_sign": true
},
"jungle_wall_sign": {
"sign_material": "jungle",
"texture": null,
"wall_sign": true
},
"jungle_wood": {
"texture": "jungle_log"
},
@ -876,6 +992,7 @@
"lava_cauldron": {
"texture": "cauldron_top"
},
"leaf_litter": null,
"lectern": {
"texture": "lectern_top"
},
@ -991,7 +1108,10 @@
"mangrove_fence_gate": {
"texture": "mangrove_planks"
},
"mangrove_hanging_sign": null,
"mangrove_hanging_sign": {
"sign_material": "mangrove",
"texture": null
},
"mangrove_leaves": {
"foliage": true
},
@ -1007,7 +1127,8 @@
"texture": "mangrove_roots_top"
},
"mangrove_sign": {
"texture": "mangrove_planks"
"sign_material": "mangrove",
"texture": null
},
"mangrove_slab": {
"texture": "mangrove_planks"
@ -1016,8 +1137,16 @@
"texture": "mangrove_planks"
},
"mangrove_trapdoor": {},
"mangrove_wall_hanging_sign": null,
"mangrove_wall_sign": null,
"mangrove_wall_hanging_sign": {
"sign_material": "mangrove",
"texture": null,
"wall_sign": true
},
"mangrove_wall_sign": {
"sign_material": "mangrove",
"texture": null,
"wall_sign": true
},
"mangrove_wood": {
"texture": "mangrove_log"
},
@ -1105,7 +1234,10 @@
"oak_fence_gate": {
"texture": "oak_planks"
},
"oak_hanging_sign": null,
"oak_hanging_sign": {
"sign_material": "oak",
"texture": null
},
"oak_leaves": {
"foliage": true
},
@ -1118,7 +1250,8 @@
},
"oak_sapling": {},
"oak_sign": {
"texture": "oak_planks"
"sign_material": "oak",
"texture": null
},
"oak_slab": {
"texture": "oak_planks"
@ -1127,8 +1260,16 @@
"texture": "oak_planks"
},
"oak_trapdoor": {},
"oak_wall_hanging_sign": null,
"oak_wall_sign": null,
"oak_wall_hanging_sign": {
"sign_material": "oak",
"texture": null,
"wall_sign": true
},
"oak_wall_sign": {
"sign_material": "oak",
"texture": null,
"wall_sign": true
},
"oak_wood": {
"texture": "oak_log"
},
@ -1139,6 +1280,7 @@
"ochre_froglight": {
"texture": "ochre_froglight_top"
},
"open_eyeblossom": null,
"orange_banner": null,
"orange_bed": null,
"orange_candle": null,
@ -1161,7 +1303,14 @@
"orange_wall_banner": null,
"orange_wool": {},
"oxeye_daisy": null,
"oxidized_chiseled_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_slab": {
"texture": "oxidized_cut_copper"
@ -1171,6 +1320,56 @@
},
"packed_ice": {},
"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": {
"texture": "pearlescent_froglight_top"
},
@ -1282,6 +1481,16 @@
"polished_granite_stairs": {
"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,
"potatoes": {
"texture": "potatoes_stage3"
@ -1316,6 +1525,9 @@
"potted_cherry_sapling": {
"texture": "cherry_sapling"
},
"potted_closed_eyeblossom": {
"texture": "closed_eyeblossom"
},
"potted_cornflower": {
"texture": "cornflower"
},
@ -1353,12 +1565,18 @@
"potted_oak_sapling": {
"texture": "oak_sapling"
},
"potted_open_eyeblossom": {
"texture": "open_eyeblossom"
},
"potted_orange_tulip": {
"texture": "orange_tulip"
},
"potted_oxeye_daisy": {
"texture": "oxeye_daisy"
},
"potted_pale_oak_sapling": {
"texture": "pale_oak_sapling"
},
"potted_pink_tulip": {
"texture": "pink_tulip"
},
@ -1521,6 +1739,18 @@
"repeating_command_block": {
"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": {
"texture": "respawn_anchor_top"
},
@ -1557,12 +1787,14 @@
"sculk_vein": {},
"sea_lantern": {},
"sea_pickle": {},
"seagrass": null,
"seagrass": {},
"short_dry_grass": {},
"short_grass": null,
"shroomlight": {},
"shulker_box": {},
"sign": {
"texture": "oak_planks"
"sign_material": "oak",
"texture": null
},
"skeleton_skull": null,
"skeleton_wall_skull": null,
@ -1638,7 +1870,10 @@
"spruce_fence_gate": {
"texture": "spruce_planks"
},
"spruce_hanging_sign": null,
"spruce_hanging_sign": {
"sign_material": "spruce",
"texture": null
},
"spruce_leaves": {
"spruce": true
},
@ -1651,7 +1886,8 @@
},
"spruce_sapling": {},
"spruce_sign": {
"texture": "spruce_planks"
"sign_material": "spruce",
"texture": null
},
"spruce_slab": {
"texture": "spruce_planks"
@ -1660,8 +1896,16 @@
"texture": "spruce_planks"
},
"spruce_trapdoor": {},
"spruce_wall_hanging_sign": null,
"spruce_wall_sign": null,
"spruce_wall_hanging_sign": {
"sign_material": "spruce",
"texture": null,
"wall_sign": true
},
"spruce_wall_sign": {
"sign_material": "spruce",
"texture": null,
"wall_sign": true
},
"spruce_wood": {
"texture": "spruce_log"
},
@ -1743,6 +1987,12 @@
"stripped_oak_wood": {
"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": {
"texture": "stripped_spruce_log_top"
},
@ -1770,6 +2020,7 @@
"sweet_berry_bush": {
"texture": "sweet_berry_bush_stage3"
},
"tall_dry_grass": {},
"tall_grass": {
"grass": true,
"texture": "tall_grass_top"
@ -1781,6 +2032,8 @@
"texture": "target_top"
},
"terracotta": {},
"test_block": null,
"test_instance_block": null,
"tinted_glass": {},
"tnt": {
"texture": "tnt_top"
@ -1791,6 +2044,9 @@
"trapped_chest": {
"texture": "oak_planks"
},
"trial_spawner": {
"texture": "trial_spawner_top_inactive"
},
"tripwire": null,
"tripwire_hook": null,
"tube_coral": null,
@ -1798,9 +2054,31 @@
"tube_coral_fan": null,
"tube_coral_wall_fan": null,
"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": {},
"twisting_vines": {},
"twisting_vines_plant": {},
"vault": {
"texture": "vault_top"
},
"verdant_froglight": {
"texture": "verdant_froglight_top"
},
@ -1808,7 +2086,11 @@
"grass": true
},
"void_air": null,
"wall_sign": null,
"wall_sign": {
"sign_material": "oak",
"texture": null,
"wall_sign": true
},
"wall_torch": null,
"warped_button": null,
"warped_door": {
@ -1821,7 +2103,10 @@
"texture": "warped_planks"
},
"warped_fungus": null,
"warped_hanging_sign": null,
"warped_hanging_sign": {
"sign_material": "warped",
"texture": null
},
"warped_hyphae": {
"texture": "warped_stem"
},
@ -1832,7 +2117,8 @@
},
"warped_roots": {},
"warped_sign": {
"texture": "warped_planks"
"sign_material": "warped",
"texture": null
},
"warped_slab": {
"texture": "warped_planks"
@ -1844,8 +2130,16 @@
"texture": "warped_stem_top"
},
"warped_trapdoor": {},
"warped_wall_hanging_sign": null,
"warped_wall_sign": null,
"warped_wall_hanging_sign": {
"sign_material": "warped",
"texture": null,
"wall_sign": true
},
"warped_wall_sign": {
"sign_material": "warped",
"texture": null,
"wall_sign": true
},
"warped_wart_block": {},
"water": {
"texture": "water_still",
@ -1854,9 +2148,24 @@
"water_cauldron": {
"texture": "cauldron_top"
},
"waxed_chiseled_copper": {
"texture": "chiseled_copper"
},
"waxed_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": {
"texture": "cut_copper"
},
@ -1866,9 +2175,24 @@
"waxed_cut_copper_stairs": {
"texture": "cut_copper"
},
"waxed_exposed_chiseled_copper": {
"texture": "exposed_chiseled_copper"
},
"waxed_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": {
"texture": "exposed_cut_copper"
},
@ -1878,9 +2202,24 @@
"waxed_exposed_cut_copper_stairs": {
"texture": "exposed_cut_copper"
},
"waxed_oxidized_chiseled_copper": {
"texture": "oxidized_chiseled_copper"
},
"waxed_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": {
"texture": "oxidized_cut_copper"
},
@ -1890,9 +2229,24 @@
"waxed_oxidized_cut_copper_stairs": {
"texture": "oxidized_cut_copper"
},
"waxed_weathered_chiseled_copper": {
"texture": "weathered_chiseled_copper"
},
"waxed_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": {
"texture": "weathered_cut_copper"
},
@ -1902,7 +2256,14 @@
"waxed_weathered_cut_copper_stairs": {
"texture": "weathered_cut_copper"
},
"weathered_chiseled_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_slab": {
"texture": "weathered_cut_copper"
@ -1937,6 +2298,7 @@
"white_tulip": null,
"white_wall_banner": null,
"white_wool": {},
"wildflowers": null,
"wither_rose": null,
"wither_skeleton_skull": null,
"wither_skeleton_wall_skull": null,

View file

@ -11,7 +11,7 @@ if len(sys.argv) != 4:
sys.exit('Usage: extract.py <blocks.json> <asset directory> <colors.json>')
def mean_color(texture):
path = os.path.join(sys.argv[2], texture + '.png')
path = os.path.join(sys.argv[2], 'assets/minecraft/textures/block', texture + '.png')
im = Image.open(path)
data = im.convert('RGBA').getdata()
@ -45,20 +45,30 @@ for name, info in blocks.items():
'birch': False,
'spruce': False,
'water': False,
'wall_sign': False,
'sign_material': None,
}
if info is None:
continue
color = mean_color(info.get('texture', name))
texture = info.get('texture', name)
color = None
if texture:
color = mean_color(texture)
if color:
output[id]['color'] = color
output[id]['opaque'] = True
output[id]['grass'] = info.get('grass', False)
output[id]['foliage'] = info.get('foliage', False)
output[id]['birch'] = info.get('birch', False)
output[id]['spruce'] = info.get('spruce', False)
output[id]['water'] = info.get('water', False)
output[id]['grass'] = info.get('grass', False)
output[id]['foliage'] = info.get('foliage', False)
output[id]['birch'] = info.get('birch', False)
output[id]['spruce'] = info.get('spruce', False)
output[id]['water'] = info.get('water', False)
output[id]['wall_sign'] = info.get('wall_sign', False)
output[id]['sign_material'] = info.get('sign_material')
with open(sys.argv[3], 'w') as f:
json.dump(output, f)

View file

@ -14,11 +14,16 @@ with open(sys.argv[1]) as f:
output = {}
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('', file=f)
print('use super::*;', file=f)
print('', file=f)
print('pub const BLOCK_TYPES: &[(&str, BlockType)] = &[', file=f)
print('/// List if known block types and their properties', file=f);
print('pub const BLOCK_TYPES: &[(&str, ConstBlockType)] = &[', file=f)
for name, info in colors.items():
flags = []
@ -34,13 +39,22 @@ with open(sys.argv[2], 'w') as f:
flags.append('Spruce')
if info['water']:
flags.append('Water')
if info['wall_sign']:
flags.append('WallSign')
flags = 'make_bitflags!(BlockFlag::{' + '|'.join(flags) + '})'
print('\t("%s", BlockType { flags: %s, color: Color([%u, %u, %u]) }),' % (
name,
sign_material = 'None'
if info['sign_material']:
sign_material = 'Some("%s")' % info['sign_material']
print('\t("%s", ConstBlockType { ' % name, file=f)
print('\t\tblock_color: BlockColor { flags: %s, color: Color([%u, %u, %u]) },' % (
flags,
info['color']['r'],
info['color']['g'],
info['color']['b'],
), file=f)
print('\t\tsign_material: %s,' % sign_material, file=f)
print('}),', file=f)
print('];', file=f)

91
resource/sign_textures.py Executable file
View file

@ -0,0 +1,91 @@
#!/usr/bin/env python3
import shutil
import sys
from PIL import Image
MATERIALS = [
'acacia',
'bamboo',
'birch',
'cherry',
'crimson',
'dark_oak',
'jungle',
'mangrove',
'oak',
'pale_oak',
'spruce',
'warped',
]
in_dir = sys.argv[1]
out_dir = sys.argv[2]
def sign_bg_image(material):
in_path = f'{in_dir}/assets/minecraft/textures/entity/signs/{material}.png'
out_path = f'{out_dir}/bg/{material}_sign.png'
out_path_wall = f'{out_dir}/bg/{material}_wall_sign.png'
in_image = Image.open(in_path)
out_image = Image.new('RGBA', (24, 26))
out_image.paste(in_image.crop((2, 2, 26, 14)), (0, 0))
out_image.paste(in_image.crop((2, 16, 4, 30)), (11, 12))
out_image.save(out_path)
out_image = Image.new('RGBA', (24, 12))
out_image.paste(in_image.crop((2, 2, 26, 14)), (0, 0))
out_image.save(out_path_wall)
def hanging_sign_bg_image(material):
in_path = f'{in_dir}/assets/minecraft/textures/gui/hanging_signs/{material}.png'
out_path = f'{out_dir}/bg/{material}_hanging_sign.png'
out_path_wall = f'{out_dir}/bg/{material}_hanging_wall_sign.png'
in_image = Image.open(in_path)
out_image = Image.new('RGBA', (16, 14))
out_image.paste(in_image.crop((0, 2, 16, 16)), (0, 0))
out_image.save(out_path)
shutil.copyfile(in_path, out_path_wall)
def sign_icon_image(material):
in_path = f'{in_dir}/assets/minecraft/textures/item/{material}_sign.png'
out_path = f'{out_dir}/icon/{material}_sign.png'
out_path_wall = f'{out_dir}/icon/{material}_wall_sign.png'
in_image = Image.open(in_path)
out_image = Image.new('RGBA', (13, 14))
out_image.paste(in_image.crop((2, 2, 15, 16)), (0, 0))
out_image.save(out_path)
out_image = Image.new('RGBA', (13, 9))
out_image.paste(in_image.crop((2, 2, 15, 11)), (0, 0))
out_image.save(out_path_wall)
def hanging_sign_icon_image(material):
in_path = f'{in_dir}/assets/minecraft/textures/item/{material}_hanging_sign.png'
out_path = f'{out_dir}/icon/{material}_hanging_sign.png'
out_path_wall = f'{out_dir}/icon/{material}_hanging_wall_sign.png'
in_image = Image.open(in_path)
out_image = Image.new('RGBA', (14, 12))
out_image.paste(in_image.crop((1, 3, 15, 15)), (0, 0))
out_image.save(out_path)
out_image = Image.new('RGBA', (14, 14))
out_image.paste(in_image.crop((1, 1, 15, 15)), (0, 0))
out_image.save(out_path_wall)
for material in MATERIALS:
sign_bg_image(material)
hanging_sign_bg_image(material)
sign_icon_image(material)
hanging_sign_icon_image(material)

View file

@ -3,24 +3,52 @@
use std::{
collections::{BTreeMap, BTreeSet},
fmt::Debug,
hash::Hash,
path::{Path, PathBuf},
};
use indexmap::IndexSet;
use serde::{Deserialize, Serialize};
use anyhow::{Context, Result};
use bincode::{Decode, Encode};
use clap::ValueEnum;
use regex::{Regex, RegexSet};
use serde::Serialize;
use crate::{io::fs::FileMetaVersion, resource::Biome, types::*, world::layer};
use crate::{
io::fs::FileMetaVersion,
resource::Biome,
types::*,
world::{block_entity::BlockEntity, layer},
};
/// Increase to force regeneration of all output files
// Increase to force regeneration of all output files
/// MinedMap processed region data version number
pub const REGION_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0);
///
/// Increase when the generation of processed regions from region data changes
/// (usually because of updated resource data)
pub const REGION_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(7);
/// MinedMap map tile data version number
///
/// Increase when the generation of map tiles from processed regions changes
/// (because of code changes in tile generation)
pub const MAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0);
/// MinedMap lightmap data version number
pub const LIGHTMAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0);
///
/// Increase when the generation of lightmap tiles from region data changes
/// (usually because of updated resource data)
pub const LIGHTMAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(5);
/// MinedMap mipmap data version number
///
/// Increase when the mipmap generation changes (this should not happen)
pub const MIPMAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0);
/// MinedMap processed entity data version number
///
/// Increase when entity collection changes bacause of code changes.
pub const ENTITIES_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(3);
/// Coordinate pair of a generated tile
///
@ -59,7 +87,7 @@ impl TileCoordMap {
}
/// Data structure for storing chunk data between processing and rendering steps
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Encode, Decode)]
pub struct ProcessedChunk {
/// Block type data
pub blocks: Box<layer::BlockArray>,
@ -70,16 +98,23 @@ pub struct ProcessedChunk {
}
/// Data structure for storing region data between processing and rendering steps
#[derive(Debug, Default, Serialize, Deserialize)]
#[derive(Debug, Default, Encode, Decode)]
pub struct ProcessedRegion {
/// List of biomes used in the region
///
/// Indexed by [ProcessedChunk] biome data
pub biome_list: IndexSet<Biome>,
pub biome_list: Vec<Biome>,
/// Processed chunk data
pub chunks: ChunkArray<Option<Box<ProcessedChunk>>>,
}
/// Data structure for storing entity data between processing and collection steps
#[derive(Debug, Default, Encode, Decode)]
pub struct ProcessedEntities {
/// List of block entities
pub block_entities: Vec<BlockEntity>,
}
/// Derives a filename from region coordinates and a file extension
///
/// Can be used for input regions, processed data or rendered tiles
@ -97,43 +132,115 @@ pub enum TileKind {
}
/// Common configuration based on command line arguments
#[derive(Debug)]
pub struct Config {
/// Number of threads for parallel processing
pub num_threads: usize,
/// Number of threads for initial parallel processing
pub num_threads_initial: usize,
/// Path of input region directory
pub region_dir: PathBuf,
/// Path of input `level.dat` file
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
pub output_dir: PathBuf,
/// Path for storage of intermediate processed data files
pub processed_dir: PathBuf,
/// Path for storage of processed entity data files
pub entities_dir: PathBuf,
/// Path for storage of the final merged processed entity data file
pub entities_path_final: PathBuf,
/// Path of viewer metadata file
pub metadata_path: PathBuf,
pub viewer_info_path: PathBuf,
/// Path of viewer entities file
pub viewer_entities_path: PathBuf,
/// Format of generated map tiles
pub image_format: ImageFormat,
/// Sign text filter patterns
pub sign_patterns: RegexSet,
/// Sign text transformation pattern
pub sign_transforms: Vec<(Regex, String)>,
}
impl Config {
/// Crates a new [Config] from [command line arguments](super::Args)
pub fn new(args: &super::Args) -> Self {
pub fn new(args: &super::Args) -> Result<Self> {
let num_threads = match args.jobs {
Some(0) => num_cpus::get(),
Some(threads) => threads,
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 level_dat_path = [&args.input_dir, Path::new("level.dat")].iter().collect();
let processed_dir = [&args.output_dir, Path::new("processed")].iter().collect();
let metadata_path = [&args.output_dir, Path::new("info.json")].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 entities_dir: PathBuf = [&processed_dir, Path::new("entities")].iter().collect();
let entities_path_final = [&entities_dir, Path::new("entities.bin")].iter().collect();
let viewer_info_path = [&args.output_dir, Path::new("info.json")].iter().collect();
let viewer_entities_path = [&args.output_dir, Path::new("entities.json")]
.iter()
.collect();
Config {
let sign_patterns = Self::sign_patterns(args).context("Failed to parse sign patterns")?;
let sign_transforms =
Self::sign_transforms(args).context("Failed to parse sign transforms")?;
Ok(Config {
num_threads,
num_threads_initial,
region_dir,
level_dat_path,
level_dat_old_path,
output_dir: args.output_dir.clone(),
processed_dir,
metadata_path,
}
entities_dir,
entities_path_final,
viewer_info_path,
viewer_entities_path,
image_format: args.image_format,
sign_patterns,
sign_transforms,
})
}
/// Parses the sign prefixes and sign filters into a [RegexSet]
fn sign_patterns(args: &super::Args) -> Result<RegexSet> {
let prefix_patterns: Vec<_> = args
.sign_prefix
.iter()
.map(|prefix| format!("^{}", regex::escape(prefix)))
.collect();
Ok(RegexSet::new(
prefix_patterns.iter().chain(args.sign_filter.iter()),
)?)
}
/// Parses the sign transform argument into a vector of [Regex] and
/// corresponding replacement strings
fn sign_transforms(args: &super::Args) -> Result<Vec<(Regex, String)>> {
let splitter = Regex::new(r"^s/((?:[^\\/]|\\.)*)/((?:[^\\/]|\\.)*)/$").unwrap();
args.sign_transform
.iter()
.map(|t| Self::sign_transform(&splitter, t))
.collect()
}
/// Parses the sign transform argument into a [Regex] and its corresponding
/// replacement string
fn sign_transform(splitter: &Regex, transform: &str) -> Result<(Regex, String)> {
let captures = splitter
.captures(transform)
.with_context(|| format!("Invalid transform pattern '{}'", transform))?;
let regexp = Regex::new(&captures[1])?;
let replacement = captures[2].to_string();
Ok((regexp, replacement))
}
/// Constructs the path to an input region file
@ -148,6 +255,20 @@ impl Config {
[&self.processed_dir, Path::new(&filename)].iter().collect()
}
/// Constructs the base output path for processed entity data
pub fn entities_dir(&self, level: usize) -> PathBuf {
[&self.entities_dir, Path::new(&level.to_string())]
.iter()
.collect()
}
/// Constructs the path of a processed entity data file
pub fn entities_path(&self, level: usize, coords: TileCoords) -> PathBuf {
let filename = coord_filename(coords, "bin");
let dir = self.entities_dir(level);
[Path::new(&dir), Path::new(&filename)].iter().collect()
}
/// Constructs the base output path for a [TileKind] and mipmap level
pub fn tile_dir(&self, kind: TileKind, level: usize) -> PathBuf {
let prefix = match kind {
@ -158,14 +279,39 @@ impl Config {
[&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
pub fn tile_path(&self, kind: TileKind, level: usize, coords: TileCoords) -> PathBuf {
let filename = coord_filename(coords, "png");
let filename = coord_filename(coords, self.tile_extension());
let dir = self.tile_dir(kind, level);
[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
pub fn overlay_chunk<I, J>(image: &mut I, chunk: &J, coords: ChunkCoords)
where

View file

@ -0,0 +1,122 @@
//! The [EntityCollector]
use std::path::Path;
use anyhow::{Context, Result};
use tracing::{info, warn};
use super::{common::*, tile_collector::TileCollector, tile_merger::TileMerger};
use crate::io::{fs, storage};
/// Generates mipmap tiles from full-resolution tile images
pub struct EntityCollector<'a> {
/// Common MinedMap configuration from command line
config: &'a Config,
/// List of populated tiles for base mipmap level (level 0)
regions: &'a [TileCoords],
}
impl TileMerger for EntityCollector<'_> {
fn file_meta_version(&self) -> fs::FileMetaVersion {
ENTITIES_FILE_META_VERSION
}
fn tile_path(&self, level: usize, coords: TileCoords) -> std::path::PathBuf {
self.config.entities_path(level, coords)
}
fn write_tile(
&self,
file: &mut std::io::BufWriter<std::fs::File>,
sources: &[super::tile_merger::Source],
) -> Result<()> {
Self::merge_entity_lists(file, sources.iter().map(|source| &source.1))
}
}
impl TileCollector for EntityCollector<'_> {
type CollectOutput = ();
fn tiles(&self) -> &[TileCoords] {
self.regions
}
fn prepare(&self, level: usize) -> Result<()> {
fs::create_dir_all(&self.config.entities_dir(level))
}
fn finish(
&self,
_level: usize,
_outputs: impl Iterator<Item = Self::CollectOutput>,
) -> Result<()> {
Ok(())
}
fn collect_one(
&self,
level: usize,
coords: TileCoords,
prev: &TileCoordMap,
) -> Result<Self::CollectOutput> {
self.merge_tiles(level, coords, prev)?;
Ok(())
}
}
impl<'a> EntityCollector<'a> {
/// Constructs a new EntityCollector
pub fn new(config: &'a Config, regions: &'a [TileCoords]) -> Self {
EntityCollector { config, regions }
}
/// Merges multiple entity lists into one
fn merge_entity_lists<P: AsRef<Path>>(
file: &mut std::io::BufWriter<std::fs::File>,
sources: impl Iterator<Item = P>,
) -> Result<()> {
let mut output = ProcessedEntities::default();
for source_path in sources {
let mut source: ProcessedEntities = match storage::read_file(source_path.as_ref()) {
Ok(source) => source,
Err(err) => {
warn!(
"Failed to read entity data file {}: {:?}",
source_path.as_ref().display(),
err,
);
continue;
}
};
output.block_entities.append(&mut source.block_entities);
}
storage::write(file, &output).context("Failed to write entity data")
}
/// Runs the mipmap generation
pub fn run(self) -> Result<()> {
info!("Collecting entity data...");
let tile_stack = self.collect_tiles()?;
// Final merge
let level = tile_stack.len() - 1;
let tile_map = &tile_stack[level];
let sources: Vec<_> = [(-1, -1), (-1, 0), (0, -1), (0, 0)]
.into_iter()
.map(|(x, z)| TileCoords { x, z })
.filter(|&coords| tile_map.contains(coords))
.map(|coords| self.tile_path(level, coords))
.collect();
fs::create_with_tmpfile(&self.config.entities_path_final, |file| {
Self::merge_entity_lists(file, sources.iter())
})?;
info!("Collected entity data.");
Ok(())
}
}

View file

@ -1,9 +1,17 @@
//! The [MetadataWriter] and related types
use anyhow::{Context, Result};
use regex::Regex;
use serde::Serialize;
use crate::{core::common::*, io::fs, world::de};
use crate::{
core::common::*,
io::{fs, storage},
world::{
block_entity::{self, BlockEntity, BlockEntityData},
de, sign,
},
};
/// Minimum and maximum X and Z tile coordinates for a mipmap level
#[derive(Debug, Serialize)]
@ -37,6 +45,13 @@ struct Spawn {
z: i32,
}
/// Keeps track of enabled MinedMap features
#[derive(Debug, Serialize)]
struct Features {
/// Sign layer
signs: bool,
}
/// Viewer metadata JSON data structure
#[derive(Debug, Serialize)]
struct Metadata<'t> {
@ -44,6 +59,17 @@ struct Metadata<'t> {
mipmaps: Vec<Mipmap<'t>>,
/// Initial spawn point for new players
spawn: Spawn,
/// Enabled MinedMap features
features: Features,
/// Format of generated map tiles
tile_extension: &'static str,
}
/// Viewer entity JSON data structure
#[derive(Debug, Serialize, Default)]
struct Entities {
/// List of signs
signs: Vec<BlockEntity>,
}
/// The MetadataWriter is used to generate the viewer metadata file
@ -98,7 +124,14 @@ impl<'a> MetadataWriter<'a> {
/// Reads and deserializes the `level.dat` of the Minecraft save data
fn read_level_dat(&self) -> Result<de::LevelDat> {
crate::nbt::data::from_file(&self.config.level_dat_path).context("Failed to read level.dat")
let res = crate::nbt::data::from_file(&self.config.level_dat_path);
if res.is_err() {
if let Ok(level_dat_old) = crate::nbt::data::from_file(&self.config.level_dat_old_path)
{
return Ok(level_dat_old);
}
}
res.context("Failed to read level.dat")
}
/// Generates [Spawn] data from a [de::LevelDat]
@ -109,21 +142,93 @@ impl<'a> MetadataWriter<'a> {
}
}
/// Filter signs according to the sign pattern configuration
fn sign_filter(&self, sign: &block_entity::Sign) -> bool {
let front_text = sign.front_text.to_string();
if self.config.sign_patterns.is_match(front_text.trim()) {
return true;
}
let back_text = sign.back_text.to_string();
if self.config.sign_patterns.is_match(back_text.trim()) {
return true;
}
false
}
/// Applies a single transform to a [sign::SignText]
///
/// The regular expression is applied for each line of the sign text
/// separately (actually for each element when JSON text is used)
fn sign_text_transform(sign_text: &mut sign::SignText, transform: &(Regex, String)) {
let (regexp, replacement) = transform;
for line in &mut sign_text.0 {
for text in &mut line.0 {
text.text = regexp.replace_all(&text.text, replacement).into_owned()
}
}
}
/// Applies the configured transforms to the text of a sign
fn sign_transform(&self, sign: &mut block_entity::Sign) {
for transform in &self.config.sign_transforms {
Self::sign_text_transform(&mut sign.front_text, transform);
Self::sign_text_transform(&mut sign.back_text, transform);
}
}
/// Generates [Entities] data from collected entity lists
fn entities(&self) -> Result<Entities> {
let data: ProcessedEntities = storage::read_file(&self.config.entities_path_final)
.context("Failed to read entity data file")?;
let ret = Entities {
signs: data
.block_entities
.into_iter()
.filter(|entity| match &entity.data {
BlockEntityData::Sign(sign) => self.sign_filter(sign),
})
.map(|mut entity| {
match &mut entity.data {
BlockEntityData::Sign(sign) => self.sign_transform(sign),
};
entity
})
.collect(),
};
Ok(ret)
}
/// Runs the viewer metadata file generation
pub fn run(self) -> Result<()> {
let level_dat = self.read_level_dat()?;
let features = Features {
signs: !self.config.sign_patterns.is_empty(),
};
let mut metadata = Metadata {
mipmaps: Vec::new(),
spawn: Self::spawn(&level_dat),
features,
tile_extension: self.config.tile_extension(),
};
for tile_map in self.tiles.iter() {
metadata.mipmaps.push(Self::mipmap_entry(tile_map));
}
fs::create_with_tmpfile(&self.config.metadata_path, |file| {
serde_json::to_writer(file, &metadata).context("Failed to write metadata")
})
fs::create_with_tmpfile(&self.config.viewer_info_path, |file| {
serde_json::to_writer(file, &metadata).context("Failed to write info.json")
})?;
let entities = self.entities()?;
fs::create_with_tmpfile(&self.config.viewer_entities_path, |file| {
serde_json::to_writer(file, &entities).context("Failed to write entities.json")
})?;
Ok(())
}
}

View file

@ -1,33 +1,57 @@
//! Core functions of the MinedMap CLI
mod common;
mod entity_collector;
mod metadata_writer;
mod region_group;
mod region_processor;
mod tile_collector;
mod tile_merger;
mod tile_mipmapper;
mod tile_renderer;
use std::path::PathBuf;
use std::{
path::PathBuf,
sync::mpsc::{self, Receiver},
thread,
time::Duration,
};
use anyhow::{Context, Result};
use clap::Parser;
use git_version::git_version;
use common::Config;
use common::{Config, ImageFormat};
use metadata_writer::MetadataWriter;
use notify::{RecommendedWatcher, RecursiveMode, Watcher as _};
use rayon::ThreadPool;
use region_processor::RegionProcessor;
use tile_mipmapper::TileMipmapper;
use tile_renderer::TileRenderer;
use tokio::runtime::Runtime;
use tracing::{info, warn};
/// MinedMap version number
const VERSION: &str = git_version!(
args = ["--abbrev=7", "--match=v*", "--dirty=-modified"],
cargo_prefix = "v",
);
use self::entity_collector::EntityCollector;
/// Returns the MinedMap version number
fn version() -> &'static str {
option_env!("MINEDMAP_VERSION").unwrap_or(
git_version!(
args = ["--abbrev=7", "--match=v*", "--dirty=-modified"],
cargo_prefix = "v",
)
.strip_prefix("v")
.unwrap(),
)
}
/// Command line arguments for minedmap CLI
#[derive(Debug, Parser)]
#[command(about, version = VERSION.strip_prefix("v").unwrap())]
#[command(
about,
version = version(),
max_term_width = 100,
)]
pub struct Args {
/// Number of parallel threads to use for processing
///
@ -35,27 +59,124 @@ pub struct Args {
/// use one thread per logical CPU core.
#[arg(short, long)]
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
#[arg(short, long)]
pub verbose: bool,
/// Watch for file changes and regenerate tiles automatically instead of
/// exiting after generation
#[arg(long)]
pub watch: bool,
/// Minimum delay between map generation cycles in watch mode
#[arg(long, value_parser = humantime::parse_duration, default_value = "30s")]
pub watch_delay: Duration,
/// Format of generated map tiles
#[arg(long, value_enum, default_value_t)]
pub image_format: ImageFormat,
/// Prefix for text of signs to show on the map
#[arg(long)]
pub sign_prefix: Vec<String>,
/// Regular expression for text of signs to show on the map
///
/// --sign-prefix and --sign-filter allow to filter for signs to display;
/// by default, none are visible. The options may be passed multiple times,
/// and a sign will be visible if it matches any pattern.
///
/// To make all signs visible, pass an empty string to either option.
#[arg(long)]
pub sign_filter: Vec<String>,
/// Regular expression replacement pattern for sign texts
///
/// Accepts patterns of the form 's/regexp/replacement/'. Transforms
/// are applied to each line of sign texts separately.
#[arg(long)]
pub sign_transform: Vec<String>,
/// Minecraft save directory
pub input_dir: PathBuf,
/// MinedMap data directory
pub output_dir: PathBuf,
}
/// Configures the Rayon thread pool for parallel processing
fn setup_threads(num_threads: usize) -> Result<()> {
/// Configures a Rayon thread pool for parallel processing
fn setup_threads(num_threads: usize) -> Result<ThreadPool> {
rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.build_global()
.build()
.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
pub fn cli() -> Result<()> {
let args = Args::parse();
let config = Config::new(&args);
let config = Config::new(&args)?;
tracing_subscriber::fmt()
.with_max_level(if args.verbose {
@ -66,16 +187,28 @@ pub fn cli() -> Result<()> {
.with_target(false)
.init();
setup_threads(config.num_threads)?;
let mut pool = setup_threads(config.num_threads_initial)?;
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let regions = RegionProcessor::new(&config).run()?;
TileRenderer::new(&config, &rt, &regions).run()?;
let tiles = TileMipmapper::new(&config, &regions).run()?;
MetadataWriter::new(&config, &tiles).run()?;
let watch = args.watch.then(|| create_watcher(&args)).transpose()?;
Ok(())
pool.install(|| generate(&config, &rt))?;
let Some((_watcher, watch_channel)) = watch else {
// watch mode disabled
return Ok(());
};
if config.num_threads != config.num_threads_initial {
pool = setup_threads(config.num_threads)?;
}
pool.install(move || {
loop {
wait_watcher(&args, &watch_channel)?;
generate(&config, &rt)?;
}
})
}

View file

@ -3,6 +3,9 @@
use std::{ffi::OsStr, path::PathBuf, sync::mpsc, time::SystemTime};
use anyhow::{Context, Result};
use enum_map::{Enum, EnumMap};
use indexmap::IndexSet;
use minedmap_resource::Biome;
use rayon::prelude::*;
use tracing::{debug, info, warn};
@ -28,10 +31,12 @@ fn parse_region_filename(file_name: &OsStr) -> Option<TileCoords> {
}
/// [RegionProcessor::process_region] return values
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Enum)]
enum RegionProcessorStatus {
/// Region was processed
Ok,
/// Region was processed, unknown blocks or biomes were encountered
OkWithUnknown,
/// Region was unchanged and skipped
Skipped,
/// Reading the region failed, previous processed data is reused
@ -40,6 +45,37 @@ enum RegionProcessorStatus {
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
struct SingleRegionProcessor<'a> {
/// Registry of known block types
@ -54,41 +90,44 @@ struct SingleRegionProcessor<'a> {
output_path: PathBuf,
/// Lightmap output filename
lightmap_path: PathBuf,
/// Processed entity output filename
entities_path: PathBuf,
/// Timestamp of last modification of input file
input_timestamp: SystemTime,
/// Timestamp of last modification of processed region output file (if valid)
output_timestamp: Option<SystemTime>,
/// Timestamp of last modification of lightmap output file (if valid)
lightmap_timestamp: Option<SystemTime>,
/// Timestamp of last modification of entity list output file (if valid)
entities_timestamp: Option<SystemTime>,
/// True if processed region output file needs to be updated
output_needed: bool,
/// True if lightmap output file needs to be updated
lightmap_needed: bool,
/// Processed region intermediate data
processed_region: ProcessedRegion,
/// Lightmap intermediate data
lightmap: image::GrayAlphaImage,
/// True if entity output file needs to be updated
entities_needed: bool,
/// Format of generated map tiles
image_format: image::ImageFormat,
}
impl<'a> SingleRegionProcessor<'a> {
/// Initializes a [SingleRegionProcessor]
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_timestamp = fs::modified_timestamp(&input_path)?;
let output_path = processor.config.processed_path(coords);
let output_timestamp = fs::read_timestamp(&output_path, REGION_FILE_META_VERSION);
let lightmap_path = processor.config.tile_path(TileKind::Lightmap, 0, coords);
let lightmap_timestamp = fs::read_timestamp(&lightmap_path, LIGHTMAP_FILE_META_VERSION);
let entities_path = processor.config.entities_path(0, coords);
let entities_timestamp = fs::read_timestamp(&entities_path, ENTITIES_FILE_META_VERSION);
let output_needed = Some(input_timestamp) > output_timestamp;
let lightmap_needed = Some(input_timestamp) > lightmap_timestamp;
let processed_region = ProcessedRegion::default();
let lightmap = image::GrayAlphaImage::new(N, N);
let entities_needed = Some(input_timestamp) > entities_timestamp;
Ok(SingleRegionProcessor {
block_types: &processor.block_types,
@ -97,13 +136,15 @@ impl<'a> SingleRegionProcessor<'a> {
input_path,
output_path,
lightmap_path,
entities_path,
input_timestamp,
output_timestamp,
lightmap_timestamp,
entities_timestamp,
output_needed,
lightmap_needed,
processed_region,
lightmap,
entities_needed,
image_format: processor.config.tile_image_format(),
})
}
@ -127,14 +168,14 @@ impl<'a> SingleRegionProcessor<'a> {
/// Saves processed region data
///
/// The timestamp is the time of the last modification of the input region data.
fn save_region(&self) -> Result<()> {
fn save_region(&self, processed_region: &ProcessedRegion) -> Result<()> {
if !self.output_needed {
return Ok(());
}
storage::write(
storage::write_file(
&self.output_path,
&self.processed_region,
processed_region,
REGION_FILE_META_VERSION,
self.input_timestamp,
)
@ -143,7 +184,7 @@ impl<'a> SingleRegionProcessor<'a> {
/// Saves a lightmap tile
///
/// The timestamp is the time of the last modification of the input region data.
fn save_lightmap(&self) -> Result<()> {
fn save_lightmap(&self, lightmap: &image::GrayAlphaImage) -> Result<()> {
if !self.lightmap_needed {
return Ok(());
}
@ -153,53 +194,90 @@ impl<'a> SingleRegionProcessor<'a> {
LIGHTMAP_FILE_META_VERSION,
self.input_timestamp,
|file| {
self.lightmap
.write_to(file, image::ImageFormat::Png)
lightmap
.write_to(file, self.image_format)
.context("Failed to save image")
},
)
}
/// Processes a single chunk
fn process_chunk(&mut self, chunk_coords: ChunkCoords, data: world::de::Chunk) -> Result<()> {
let chunk = world::chunk::Chunk::new(&data, self.block_types, self.biome_types)
.with_context(|| format!("Failed to decode chunk {:?}", chunk_coords))?;
let Some(layer::LayerData {
blocks,
biomes,
block_light,
depths,
}) = world::layer::top_layer(&mut self.processed_region.biome_list, &chunk)
.with_context(|| format!("Failed to process chunk {:?}", chunk_coords))?
else {
/// Saves processed entity data
///
/// The timestamp is the time of the last modification of the input region data.
fn save_entities(&self, entities: &mut ProcessedEntities) -> Result<()> {
if !self.entities_needed {
return Ok(());
};
if self.output_needed {
self.processed_region.chunks[chunk_coords] = Some(Box::new(ProcessedChunk {
blocks,
biomes,
depths,
}));
}
if self.lightmap_needed {
let chunk_lightmap = Self::render_chunk_lightmap(block_light);
overlay_chunk(&mut self.lightmap, &chunk_lightmap, chunk_coords);
entities.block_entities.sort_unstable();
storage::write_file(
&self.entities_path,
entities,
ENTITIES_FILE_META_VERSION,
self.input_timestamp,
)
}
/// Processes a single chunk
fn process_chunk(
&self,
data: &mut SingleRegionData,
chunk_coords: ChunkCoords,
chunk_data: world::de::Chunk,
) -> Result<()> {
let (chunk, has_unknown) =
world::chunk::Chunk::new(&chunk_data, self.block_types, self.biome_types)
.with_context(|| format!("Failed to decode chunk {:?}", chunk_coords))?;
data.has_unknown |= has_unknown;
if self.output_needed || self.lightmap_needed {
if let Some(layer::LayerData {
blocks,
biomes,
block_light,
depths,
}) = world::layer::top_layer(&mut data.biome_list, &chunk)
.with_context(|| format!("Failed to process chunk {:?}", chunk_coords))?
{
if self.output_needed {
data.chunks[chunk_coords] = Some(Box::new(ProcessedChunk {
blocks,
biomes,
depths,
}));
}
if self.lightmap_needed {
let chunk_lightmap = Self::render_chunk_lightmap(block_light);
overlay_chunk(&mut data.lightmap, &chunk_lightmap, chunk_coords);
}
}
}
if self.entities_needed {
let mut block_entities = chunk.block_entities().with_context(|| {
format!(
"Failed to process block entities for chunk {:?}",
chunk_coords,
)
})?;
data.entities.block_entities.append(&mut block_entities);
}
Ok(())
}
/// Processes the chunks of the region
fn process_chunks(&mut self) -> Result<()> {
crate::nbt::region::from_file(&self.input_path)?
.foreach_chunk(|chunk_coords, data| self.process_chunk(chunk_coords, data))
fn process_chunks(&self, data: &mut SingleRegionData) -> Result<()> {
crate::nbt::region::from_file(&self.input_path)?.foreach_chunk(
|chunk_coords, chunk_data| self.process_chunk(data, chunk_coords, chunk_data),
)
}
/// Processes the region
fn run(mut self) -> Result<RegionProcessorStatus> {
if !self.output_needed && !self.lightmap_needed {
fn run(&self) -> Result<RegionProcessorStatus> {
if !self.output_needed && !self.lightmap_needed && !self.entities_needed {
debug!(
"Skipping unchanged region r.{}.{}.mca",
self.coords.x, self.coords.z
@ -212,8 +290,13 @@ impl<'a> SingleRegionProcessor<'a> {
self.coords.x, self.coords.z
);
if let Err(err) = self.process_chunks() {
if self.output_timestamp.is_some() && self.lightmap_timestamp.is_some() {
let mut data = SingleRegionData::default();
if let Err(err) = self.process_chunks(&mut data) {
if self.output_timestamp.is_some()
&& self.lightmap_timestamp.is_some()
&& self.entities_timestamp.is_some()
{
warn!(
"Failed to process region {:?}, using old data: {:?}",
self.coords, err
@ -228,10 +311,20 @@ impl<'a> SingleRegionProcessor<'a> {
}
}
self.save_region()?;
self.save_lightmap()?;
let processed_region = ProcessedRegion {
biome_list: data.biome_list.into_iter().collect(),
chunks: data.chunks,
};
Ok(RegionProcessorStatus::Ok)
self.save_region(&processed_region)?;
self.save_lightmap(&data.lightmap)?;
self.save_entities(&mut data.entities)?;
Ok(if data.has_unknown {
RegionProcessorStatus::OkWithUnknown
} else {
RegionProcessorStatus::Ok
})
}
}
@ -272,11 +365,20 @@ impl<'a> RegionProcessor<'a> {
})?
.filter_map(|entry| entry.ok())
.filter(|entry| {
// We are only interested in regular files
matches!(
entry.file_type().map(|file_type| file_type.is_file()),
Ok(true)
)
(|| {
// We are only interested in regular files
let file_type = entry.file_type().ok()?;
if !file_type.is_file() {
return None;
}
let metadata = entry.metadata().ok()?;
if metadata.len() == 0 {
return None;
}
Some(())
})()
.is_some()
})
.filter_map(|entry| parse_region_filename(&entry.file_name()))
.collect())
@ -291,31 +393,27 @@ impl<'a> RegionProcessor<'a> {
///
/// Returns a list of the coordinates of all processed regions
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.tile_dir(TileKind::Lightmap, 0))?;
fs::create_dir_all(&self.config.entities_dir(0))?;
info!("Processing region files...");
let (region_send, region_recv) = mpsc::channel();
let (processed_send, processed_recv) = mpsc::channel();
let (error_send, error_recv) = mpsc::channel();
let (status_send, status_recv) = mpsc::channel();
self.collect_regions()?.par_iter().try_for_each(|&coords| {
let ret = self
.process_region(coords)
.with_context(|| format!("Failed to process region {:?}", coords))?;
if ret != RegionProcessorStatus::ErrorMissing {
if ret != Status::ErrorMissing {
region_send.send(coords).unwrap();
}
match ret {
RegionProcessorStatus::Ok => processed_send.send(()).unwrap(),
RegionProcessorStatus::Skipped => {}
RegionProcessorStatus::ErrorOk | RegionProcessorStatus::ErrorMissing => {
error_send.send(()).unwrap()
}
}
status_send.send(ret).unwrap();
anyhow::Ok(())
})?;
@ -323,18 +421,30 @@ impl<'a> RegionProcessor<'a> {
drop(region_send);
let mut regions: Vec<_> = region_recv.into_iter().collect();
drop(processed_send);
let processed = processed_recv.into_iter().count();
drop(error_send);
let errors = error_recv.into_iter().count();
drop(status_send);
let mut status = EnumMap::<_, usize>::default();
for ret in status_recv {
status[ret] += 1;
}
info!(
"Processed region files ({} processed, {} unchanged, {} errors)",
processed,
regions.len() - processed - errors,
errors,
status[Status::Ok] + status[Status::OkWithUnknown],
status[Status::Skipped],
status[Status::ErrorOk] + status[Status::ErrorMissing],
);
if status[Status::OkWithUnknown] > 0 {
warn!("Unknown block or biome types found during processing");
eprint!(concat!(
"\n",
" If you're encountering this issue with an unmodified Minecraft version supported by MinedMap,\n",
" please file a bug report including the output with the --verbose flag.\n",
"\n",
));
}
// Sort regions in a zig-zag pattern to optimize cache usage
regions.sort_unstable_by_key(|&TileCoords { x, z }| (x, if x % 2 == 0 { z } else { -z }));

107
src/core/tile_collector.rs Normal file
View file

@ -0,0 +1,107 @@
//! A trait for recursively processing tiles
//!
//! Used for mipmap generation and collecting entity data
use std::sync::mpsc;
use anyhow::Result;
use rayon::prelude::*;
use super::common::*;
/// Helper to determine if no further mipmap levels are needed
///
/// If all tile coordinates are -1 or 0, further mipmap levels will not
/// decrease the number of tiles and mipmap generated is considered finished.
fn done(tiles: &TileCoordMap) -> bool {
tiles
.0
.iter()
.all(|(z, xs)| (-1..=0).contains(z) && xs.iter().all(|x| (-1..=0).contains(x)))
}
/// Derives the map of populated tile coordinates for the next mipmap level
fn map_coords(tiles: &TileCoordMap) -> TileCoordMap {
let mut ret = TileCoordMap::default();
for (&z, xs) in &tiles.0 {
for &x in xs {
let xt = x >> 1;
let zt = z >> 1;
ret.0.entry(zt).or_default().insert(xt);
}
}
ret
}
/// Trait to implement for collecting tiles recursively
pub trait TileCollector: Sync {
/// Return value of [TileCollector::collect_one]
type CollectOutput: Send;
/// List of level 0 tiles
fn tiles(&self) -> &[TileCoords];
/// Called at the beginning of each level of processing
fn prepare(&self, level: usize) -> Result<()>;
/// Called at the end of each level of processing
fn finish(
&self,
level: usize,
outputs: impl Iterator<Item = Self::CollectOutput>,
) -> Result<()>;
/// Called for each tile coordinate of the level that is currently being generated
fn collect_one(
&self,
level: usize,
coords: TileCoords,
prev: &TileCoordMap,
) -> Result<Self::CollectOutput>;
/// Collects tiles recursively
fn collect_tiles(&self) -> Result<Vec<TileCoordMap>> {
let mut tile_stack = {
let mut tile_map = TileCoordMap::default();
for &TileCoords { x, z } in self.tiles() {
tile_map.0.entry(z).or_default().insert(x);
}
vec![tile_map]
};
loop {
let level = tile_stack.len();
let prev = &tile_stack[level - 1];
if done(prev) {
break;
}
self.prepare(level)?;
let next = map_coords(prev);
let (send, recv) = mpsc::channel();
next.0
.par_iter()
.flat_map(|(&z, xs)| xs.par_iter().map(move |&x| TileCoords { x, z }))
.try_for_each(|coords| {
let output = self.collect_one(level, coords, prev)?;
send.send(output).unwrap();
anyhow::Ok(())
})?;
drop(send);
self.finish(level, recv.into_iter())?;
tile_stack.push(next);
}
Ok(tile_stack)
}
}

97
src/core/tile_merger.rs Normal file
View file

@ -0,0 +1,97 @@
//! Mipmap-style merging of tiles
use std::{
fs::File,
io::BufWriter,
path::{Path, PathBuf},
time::SystemTime,
};
use anyhow::Result;
use tracing::warn;
use super::common::*;
use crate::io::fs;
/// [TileMerger::merge_tiles] return
#[derive(Debug, Clone, Copy)]
pub enum Stat {
/// None of the input files were found
NotFound,
/// The output file is up-to-date
Skipped,
/// The output file is regenerated
Regenerate,
}
/// A source file for the [TileMerger]
///
/// The tuple elements are X and Z coordinate offsets in the range [0, 1],
/// the file path and the time of last change of the input.
pub type Source = ((i32, i32), PathBuf, SystemTime);
/// Reusable trait for mipmap-style tile merging with change tracking
pub trait TileMerger {
/// [fs::FileMetaVersion] of input and output files
///
/// The version in the file metadata on disk must match the returned
/// version for the a to be considered up-to-date.
fn file_meta_version(&self) -> fs::FileMetaVersion;
/// Returns the paths of input and output files
fn tile_path(&self, level: usize, coords: TileCoords) -> PathBuf;
/// Can be used to log the processing status
fn log(&self, _output_path: &Path, _stat: Stat) {}
/// Handles the actual merging of source files
fn write_tile(&self, file: &mut BufWriter<File>, sources: &[Source]) -> Result<()>;
/// Generates a tile at given coordinates and mipmap level
fn merge_tiles(&self, level: usize, coords: TileCoords, prev: &TileCoordMap) -> Result<Stat> {
let version = self.file_meta_version();
let output_path = self.tile_path(level, coords);
let output_timestamp = fs::read_timestamp(&output_path, version);
let sources: Vec<_> = [(0, 0), (0, 1), (1, 0), (1, 1)]
.into_iter()
.filter_map(|(dx, dz)| {
let source_coords = TileCoords {
x: 2 * coords.x + dx,
z: 2 * coords.z + dz,
};
if !prev.contains(source_coords) {
return None;
}
let source_path = self.tile_path(level - 1, source_coords);
let timestamp = match fs::modified_timestamp(&source_path) {
Ok(timestamp) => timestamp,
Err(err) => {
warn!("{:?}", err);
return None;
}
};
Some(((dx, dz), source_path, timestamp))
})
.collect();
let Some(input_timestamp) = sources.iter().map(|(_, _, ts)| *ts).max() else {
self.log(&output_path, Stat::NotFound);
return Ok(Stat::NotFound);
};
if Some(input_timestamp) <= output_timestamp {
self.log(&output_path, Stat::Skipped);
return Ok(Stat::Skipped);
}
self.log(&output_path, Stat::Regenerate);
fs::create_with_timestamp(&output_path, version, input_timestamp, |file| {
self.write_tile(file, &sources)
})?;
Ok(Stat::Regenerate)
}
}

View file

@ -1,14 +1,154 @@
//! The [TileMipmapper]
use std::sync::mpsc;
use std::{marker::PhantomData, ops::Add};
use anyhow::{Context, Result};
use rayon::prelude::*;
use tracing::{debug, info, warn};
use super::common::*;
use super::{
common::*,
tile_collector::TileCollector,
tile_merger::{self, TileMerger},
};
use crate::{io::fs, types::*};
/// Counters for the number of processed and total tiles
///
/// Used as return of [TileMipmapper::collect_one]
#[derive(Debug, Clone, Copy)]
pub struct MipmapStat {
/// Total number of tiles
total: usize,
/// Processed number of tiles
processed: usize,
}
impl From<tile_merger::Stat> for MipmapStat {
fn from(value: tile_merger::Stat) -> Self {
match value {
tile_merger::Stat::NotFound => MipmapStat {
total: 0,
processed: 0,
},
tile_merger::Stat::Skipped => MipmapStat {
total: 1,
processed: 0,
},
tile_merger::Stat::Regenerate => MipmapStat {
total: 1,
processed: 1,
},
}
}
}
impl Add for MipmapStat {
type Output = MipmapStat;
fn add(self, rhs: Self) -> Self::Output {
MipmapStat {
total: self.total + rhs.total,
processed: self.processed + rhs.processed,
}
}
}
/// [TileMerger] for map tile images
struct MapMerger<'a, P> {
/// Common MinedMap configuration from command line
config: &'a Config,
/// Tile kind (map or lightmap)
kind: TileKind,
/// Pixel format type
_pixel: PhantomData<P>,
}
impl<'a, P> MapMerger<'a, P> {
/// Creates a new [MapMerger]
fn new(config: &'a Config, kind: TileKind) -> Self {
MapMerger {
config,
kind,
_pixel: PhantomData,
}
}
}
impl<P: image::PixelWithColorType> TileMerger for MapMerger<'_, P>
where
[P::Subpixel]: image::EncodableLayout,
image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>,
{
fn file_meta_version(&self) -> fs::FileMetaVersion {
MIPMAP_FILE_META_VERSION
}
fn tile_path(&self, level: usize, coords: TileCoords) -> std::path::PathBuf {
self.config.tile_path(self.kind, level, coords)
}
fn log(&self, output_path: &std::path::Path, stat: super::tile_merger::Stat) {
match stat {
super::tile_merger::Stat::NotFound => {}
super::tile_merger::Stat::Skipped => {
debug!(
"Skipping unchanged mipmap tile {}",
output_path
.strip_prefix(&self.config.output_dir)
.expect("tile path must be in output directory")
.display(),
);
}
super::tile_merger::Stat::Regenerate => {
debug!(
"Rendering mipmap tile {}",
output_path
.strip_prefix(&self.config.output_dir)
.expect("tile path must be in output directory")
.display(),
);
}
};
}
fn write_tile(
&self,
file: &mut std::io::BufWriter<std::fs::File>,
sources: &[super::tile_merger::Source],
) -> Result<()> {
/// Tile width/height
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
let mut image: image::DynamicImage =
image::ImageBuffer::<P, Vec<P::Subpixel>>::new(N, N).into();
for ((dx, dz), source_path, _) in sources {
let source = match image::open(source_path) {
Ok(source) => source,
Err(err) => {
warn!(
"Failed to read source image {}: {:?}",
source_path.display(),
err,
);
continue;
}
};
let resized = source.resize(N / 2, N / 2, image::imageops::FilterType::Triangle);
image::imageops::overlay(
&mut image,
&resized,
*dx as i64 * (N / 2) as i64,
*dz as i64 * (N / 2) as i64,
);
}
image
.write_to(file, self.config.tile_image_format())
.context("Failed to save image")
}
}
/// Generates mipmap tiles from full-resolution tile images
pub struct TileMipmapper<'a> {
/// Common MinedMap configuration from command line
@ -17,39 +157,63 @@ pub struct TileMipmapper<'a> {
regions: &'a [TileCoords],
}
impl TileCollector for TileMipmapper<'_> {
type CollectOutput = MipmapStat;
fn tiles(&self) -> &[TileCoords] {
self.regions
}
fn prepare(&self, level: usize) -> Result<()> {
info!("Generating level {} mipmaps...", level);
fs::create_dir_all(&self.config.tile_dir(TileKind::Map, level))?;
fs::create_dir_all(&self.config.tile_dir(TileKind::Lightmap, level))?;
Ok(())
}
fn finish(
&self,
level: usize,
outputs: impl Iterator<Item = Self::CollectOutput>,
) -> Result<()> {
let stat = outputs.fold(
MipmapStat {
total: 0,
processed: 0,
},
MipmapStat::add,
);
info!(
"Generated level {} mipmaps ({} processed, {} unchanged)",
level,
stat.processed,
stat.total - stat.processed,
);
Ok(())
}
fn collect_one(
&self,
level: usize,
coords: TileCoords,
prev: &TileCoordMap,
) -> Result<Self::CollectOutput> {
let map_stat = self.render_mipmap::<image::Rgba<u8>>(TileKind::Map, level, coords, prev)?;
let lightmap_stat =
self.render_mipmap::<image::LumaA<u8>>(TileKind::Lightmap, level, coords, prev)?;
Ok(map_stat + lightmap_stat)
}
}
impl<'a> TileMipmapper<'a> {
/// Constructs a new TileMipmapper
pub fn new(config: &'a Config, regions: &'a [TileCoords]) -> Self {
TileMipmapper { config, regions }
}
/// Helper to determine if no further mipmap levels are needed
///
/// If all tile coordinates are -1 or 0, further mipmap levels will not
/// decrease the number of tiles and mipmap generated is considered finished.
fn done(tiles: &TileCoordMap) -> bool {
tiles
.0
.iter()
.all(|(z, xs)| (-1..=0).contains(z) && xs.iter().all(|x| (-1..=0).contains(x)))
}
/// Derives the map of populated tile coordinates for the next mipmap level
fn map_coords(tiles: &TileCoordMap) -> TileCoordMap {
let mut ret = TileCoordMap::default();
for (&z, xs) in &tiles.0 {
for &x in xs {
let xt = x >> 1;
let zt = z >> 1;
ret.0.entry(zt).or_default().insert(xt);
}
}
ret
}
/// Renders and saves a single mipmap tile image
///
/// Each mipmap tile is rendered by taking 2x2 tiles from the
@ -60,174 +224,18 @@ impl<'a> TileMipmapper<'a> {
level: usize,
coords: TileCoords,
prev: &TileCoordMap,
count_total: &mpsc::Sender<()>,
count_processed: &mpsc::Sender<()>,
) -> Result<()>
) -> Result<MipmapStat>
where
[P::Subpixel]: image::EncodableLayout,
image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>,
{
/// Tile width/height
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
let version = match kind {
TileKind::Map => REGION_FILE_META_VERSION,
TileKind::Lightmap => LIGHTMAP_FILE_META_VERSION,
};
let output_path = self.config.tile_path(kind, level, coords);
let output_timestamp = fs::read_timestamp(&output_path, version);
let sources: Vec<_> = [(0, 0), (0, 1), (1, 0), (1, 1)]
.into_iter()
.filter_map(|(dx, dz)| {
let source_coords = TileCoords {
x: 2 * coords.x + dx,
z: 2 * coords.z + dz,
};
if !prev.contains(source_coords) {
return None;
}
let source_path = self.config.tile_path(kind, level - 1, source_coords);
let timestamp = match fs::modified_timestamp(&source_path) {
Ok(timestamp) => timestamp,
Err(err) => {
warn!("{}", err);
return None;
}
};
Some(((dx, dz), source_path, timestamp))
})
.collect();
let Some(input_timestamp) = sources.iter().map(|(_, _, ts)| *ts).max() else {
return Ok(());
};
count_total.send(()).unwrap();
if Some(input_timestamp) <= output_timestamp {
debug!(
"Skipping unchanged mipmap tile {}",
output_path
.strip_prefix(&self.config.output_dir)
.expect("tile path must be in output directory")
.display(),
);
return Ok(());
}
debug!(
"Rendering mipmap tile {}",
output_path
.strip_prefix(&self.config.output_dir)
.expect("tile path must be in output directory")
.display(),
);
let mut image: image::DynamicImage =
image::ImageBuffer::<P, Vec<P::Subpixel>>::new(N, N).into();
for ((dx, dz), source_path, _) in sources {
let source = match image::open(&source_path) {
Ok(source) => source,
Err(err) => {
warn!(
"Failed to read source image {}: {}",
source_path.display(),
err,
);
continue;
}
};
let resized = source.resize(N / 2, N / 2, image::imageops::FilterType::Triangle);
image::imageops::overlay(
&mut image,
&resized,
dx as i64 * (N / 2) as i64,
dz as i64 * (N / 2) as i64,
);
}
fs::create_with_timestamp(&output_path, version, input_timestamp, |file| {
image
.write_to(file, image::ImageFormat::Png)
.context("Failed to save image")
})?;
count_processed.send(()).unwrap();
Ok(())
let merger = MapMerger::<P>::new(self.config, kind);
let ret = merger.merge_tiles(level, coords, prev)?;
Ok(ret.into())
}
/// Runs the mipmap generation
pub fn run(self) -> Result<Vec<TileCoordMap>> {
let mut tile_stack = {
let mut tile_map = TileCoordMap::default();
for &TileCoords { x, z } in self.regions {
tile_map.0.entry(z).or_default().insert(x);
}
vec![tile_map]
};
loop {
let level = tile_stack.len();
let prev = &tile_stack[level - 1];
if Self::done(prev) {
break;
}
info!("Generating level {} mipmaps...", level);
fs::create_dir_all(&self.config.tile_dir(TileKind::Map, level))?;
fs::create_dir_all(&self.config.tile_dir(TileKind::Lightmap, level))?;
let next = Self::map_coords(prev);
let (total_send, total_recv) = mpsc::channel();
let (processed_send, processed_recv) = mpsc::channel();
next.0
.par_iter()
.flat_map(|(&z, xs)| xs.par_iter().map(move |&x| TileCoords { x, z }))
.try_for_each(|coords| {
self.render_mipmap::<image::Rgba<u8>>(
TileKind::Map,
level,
coords,
prev,
&total_send,
&processed_send,
)?;
self.render_mipmap::<image::LumaA<u8>>(
TileKind::Lightmap,
level,
coords,
prev,
&total_send,
&processed_send,
)?;
anyhow::Ok(())
})?;
drop(total_send);
let total = total_recv.into_iter().count();
drop(processed_send);
let processed = processed_recv.into_iter().count();
info!(
"Generated level {} mipmaps ({} processed, {} unchanged)",
level,
processed,
total - processed,
);
tile_stack.push(next);
}
Ok(tile_stack)
self.collect_tiles()
}
}

View file

@ -16,7 +16,7 @@ use tracing::{debug, info};
use super::{common::*, region_group::RegionGroup};
use crate::{
io::{fs, storage},
resource::{block_color, needs_biome, Colorf},
resource::{Colorf, block_color, needs_biome},
types::*,
util::coord_offset,
};
@ -105,7 +105,7 @@ impl<'a> TileRenderer<'a> {
region_loader
.get_or_try_init(|| async {
storage::read(&processed_path).context("Failed to load processed region data")
storage::read_file(&processed_path).context("Failed to load processed region data")
})
.await
.cloned()
@ -133,7 +133,7 @@ impl<'a> TileRenderer<'a> {
/// Hashing the value as a single u32 is more efficient than hashing
/// the tuple elements separately.
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
@ -186,7 +186,7 @@ impl<'a> TileRenderer<'a> {
for ((region_x, region_z, index), w) in weights.into_values() {
let region = region_group.get(region_x, region_z)?;
let biome = region.biome_list.get_index(index.into())?;
let biome = region.biome_list.get(usize::from(index))?;
total += w;
color += w * block_color(block, Some(biome), depth.0 as f32);
@ -303,7 +303,7 @@ impl<'a> TileRenderer<'a> {
processed_timestamp,
|file| {
image
.write_to(file, image::ImageFormat::Png)
.write_to(file, self.config.tile_image_format())
.context("Failed to save image")
},
)?;

View file

@ -10,47 +10,64 @@ use std::{
};
use anyhow::{Context, Result};
use serde::{de::DeserializeOwned, Serialize};
use bincode::{Decode, Encode};
use super::fs;
/// Bincode configuration
const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard();
/// Serializes data and writes it to a writer
pub fn write<W: Write, T: Encode>(writer: &mut W, value: &T) -> Result<()> {
let data = bincode::encode_to_vec(value, BINCODE_CONFIG)?;
let len = u32::try_from(data.len())?;
let compressed = zstd::bulk::compress(&data, 1)?;
drop(data);
writer.write_all(&len.to_be_bytes())?;
writer.write_all(&compressed)?;
Ok(())
}
/// Serializes data and stores it in a file
///
/// A timestamp is stored in an assiciated metadata file.
pub fn write<T: Serialize>(
pub fn write_file<T: Encode>(
path: &Path,
value: &T,
version: fs::FileMetaVersion,
timestamp: SystemTime,
) -> Result<()> {
fs::create_with_timestamp(path, version, timestamp, |file| {
let data = bincode::serialize(value)?;
let len = u32::try_from(data.len())?;
let compressed = zstd::bulk::compress(&data, 1)?;
drop(data);
fs::create_with_timestamp(path, version, timestamp, |file| write(file, value))
}
file.write_all(&len.to_be_bytes())?;
file.write_all(&compressed)?;
/// Reads data from a reader and deserializes it
pub fn read<R, T>(reader: &mut R) -> Result<T>
where
R: Read,
T: Decode<()>,
{
let mut len_buf = [0u8; 4];
reader.read_exact(&mut len_buf)?;
let len = usize::try_from(u32::from_be_bytes(len_buf))?;
Ok(())
})
let mut compressed = vec![];
reader.read_to_end(&mut compressed)?;
let data = zstd::bulk::decompress(&compressed, len)?;
drop(compressed);
Ok(bincode::decode_from_slice(&data, BINCODE_CONFIG)?.0)
}
/// Reads data from a file and deserializes it
pub fn read<T: DeserializeOwned>(path: &Path) -> Result<T> {
pub fn read_file<T>(path: &Path) -> Result<T>
where
T: Decode<()>,
{
(|| -> Result<T> {
let mut file = File::open(path)?;
let mut len_buf = [0u8; 4];
file.read_exact(&mut len_buf)?;
let len = usize::try_from(u32::from_be_bytes(len_buf))?;
let mut compressed = vec![];
file.read_to_end(&mut compressed)?;
let data = zstd::bulk::decompress(&compressed, len)?;
drop(compressed);
Ok(bincode::deserialize(&data)?)
read(&mut file)
})()
.with_context(|| format!("Failed to read file {}", path.display()))
}

View file

@ -2,6 +2,9 @@
#![warn(missing_docs)]
#![warn(clippy::missing_docs_in_private_items)]
#[cfg(feature = "jemalloc-auto")]
extern crate minedmap_default_alloc;
mod core;
mod io;
mod util;

View file

@ -39,7 +39,9 @@ pub fn to_flat_coord<const AXIS: u8>(
chunk: ChunkCoord<AXIS>,
block: BlockCoord<AXIS>,
) -> i32 {
(region as i32) << (BLOCK_BITS + CHUNK_BITS) | ((chunk.0 as i32) << BLOCK_BITS | block.0 as i32)
((region as i32) << (BLOCK_BITS + CHUNK_BITS))
| ((chunk.0 as i32) << BLOCK_BITS)
| (block.0 as i32)
}
/// Splits a flat (linear) coordinate into region, chunk and block numbers

113
src/world/block_entity.rs Normal file
View file

@ -0,0 +1,113 @@
//! Processing of block entity data
use bincode::{Decode, Encode};
use minedmap_resource::{BlockFlag, BlockType};
use serde::Serialize;
use super::{
de,
sign::{BlockEntitySignExt, SignText},
};
/// Kind of sign block
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SignKind {
/// Standing sign
Sign,
/// Sign attached to wall
WallSign,
/// Hanging sign
HangingSign,
/// Hanging sign attached to wall
HangingWallSign,
}
/// Processed sign data
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Serialize)]
pub struct Sign {
/// The kind of the sign
pub kind: SignKind,
/// The material of the sign
#[serde(skip_serializing_if = "Option::is_none", default)]
pub material: Option<String>,
/// The sign's front text
#[serde(skip_serializing_if = "SignText::is_empty", default)]
pub front_text: SignText,
/// The sign's back text
#[serde(skip_serializing_if = "SignText::is_empty", default)]
pub back_text: SignText,
}
impl Sign {
/// Processes a [de::BlockEntitySign] into a [Sign]
fn new(
sign: &de::BlockEntitySign,
kind: SignKind,
material: Option<String>,
data_version: u32,
) -> Sign {
let (front_text, back_text) = sign.text();
let front_text = front_text.decode(data_version);
let back_text = back_text.decode(data_version);
Sign {
kind,
material,
front_text,
back_text,
}
}
}
/// Data for different kinds of [BlockEntity]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum BlockEntityData {
/// A sign block
Sign(Sign),
}
/// A processed block entity
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, Serialize)]
pub struct BlockEntity {
/// Global X coordinate
pub x: i32,
/// Global Y coordinate
pub y: i32,
/// Global Z coordinate
pub z: i32,
/// Entity data
#[serde(flatten)]
pub data: BlockEntityData,
}
impl BlockEntity {
/// Processes a [de::BlockEntity] into a [BlockEntity]
pub fn new(
entity: &de::BlockEntity,
block_type: Option<&BlockType>,
data_version: u32,
) -> Option<Self> {
let wall_sign = block_type
.map(|block_type| block_type.block_color.is(BlockFlag::WallSign))
.unwrap_or_default();
let (kind, sign) = match (&entity.data, wall_sign) {
(de::BlockEntityData::Sign(sign), false) => (SignKind::Sign, sign),
(de::BlockEntityData::Sign(sign), true) => (SignKind::WallSign, sign),
(de::BlockEntityData::HangingSign(sign), false) => (SignKind::HangingSign, sign),
(de::BlockEntityData::HangingSign(sign), true) => (SignKind::HangingWallSign, sign),
(de::BlockEntityData::Other, _) => return None,
};
let material = block_type
.as_ref()
.and_then(|block_type| block_type.sign_material.as_ref());
let data = BlockEntityData::Sign(Sign::new(sign, kind, material.cloned(), data_version));
Some(BlockEntity {
x: entity.x,
y: entity.y,
z: entity.z,
data,
})
}
}

View file

@ -4,22 +4,22 @@
//! over different data versions as much as possible.
use std::{
collections::{btree_map, BTreeMap},
collections::{BTreeMap, btree_map},
iter::{self, FusedIterator},
};
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use super::{de, section::*};
use super::{block_entity::BlockEntity, de, section::*};
use crate::{
resource::{BiomeTypes, BlockTypes},
resource::{BiomeTypes, BlockType, BlockTypes},
types::*,
util::{self, ShiftMask},
};
/// Chunk data structure wrapping a [de::Chunk] for convenient access to
/// block and biome data
/// Version-specific part of [Chunk]
#[derive(Debug)]
pub enum Chunk<'a> {
pub enum ChunkInner<'a> {
/// Minecraft v1.18+ chunk with biome data moved into sections
V1_18 {
/// Section data
@ -50,37 +50,16 @@ pub enum Chunk<'a> {
Empty,
}
/// Inner data structure of [SectionIter]
#[derive(Debug, Clone)]
enum SectionIterInner<'a> {
/// Iterator over sections of [Chunk::V1_18]
V1_18 {
/// Inner iterator into section map
iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BiomesV1_18<'a>, BlockLight<'a>)>,
},
/// Iterator over sections of [Chunk::V1_13]
V1_13 {
/// Inner iterator into section map
iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BlockLight<'a>)>,
/// Chunk biome data
biomes: &'a BiomesV0<'a>,
},
/// Iterator over sections of [Chunk::V0]
V0 {
/// Inner iterator into section map
iter: btree_map::Iter<'a, SectionY, (SectionV0<'a>, BlockLight<'a>)>,
/// Chunk biome data
biomes: &'a BiomesV0<'a>,
},
/// Empty iterator over an unpopulated chunk ([Chunk::Empty])
Empty,
}
/// Iterator over the sections of a [Chunk]
#[derive(Debug, Clone)]
pub struct SectionIter<'a> {
/// Inner iterator enum
inner: SectionIterInner<'a>,
/// Chunk data structure wrapping a [de::Chunk] for convenient access to
/// block and biome data
#[derive(Debug)]
pub struct Chunk<'a> {
/// Version-specific data
inner: ChunkInner<'a>,
/// Unprocessed block entities
block_entities: &'a Vec<de::BlockEntity>,
/// Chunk data version
data_version: u32,
}
impl<'a> Chunk<'a> {
@ -89,17 +68,31 @@ impl<'a> Chunk<'a> {
data: &'a de::Chunk,
block_types: &'a BlockTypes,
biome_types: &'a BiomeTypes,
) -> Result<Self> {
) -> Result<(Self, bool)> {
let data_version = data.data_version.unwrap_or_default();
match &data.chunk {
de::ChunkVariant::V1_18 { sections } => {
Self::new_v1_18(data_version, sections, block_types, biome_types)
}
de::ChunkVariant::V0 { level } => {
Self::new_v0(data_version, level, block_types, biome_types)
}
}
let ((inner, has_unknown), block_entities) = match &data.chunk {
de::ChunkVariant::V1_18 {
sections,
block_entities,
} => (
Self::new_v1_18(data_version, sections, block_types, biome_types)?,
block_entities,
),
de::ChunkVariant::V0 { level } => (
Self::new_v0(data_version, level, block_types, biome_types)?,
&level.tile_entities,
),
};
Ok((
Chunk {
inner,
block_entities,
data_version,
},
has_unknown,
))
}
/// [Chunk::new] implementation for Minecraft v1.18+ chunks
@ -108,8 +101,9 @@ impl<'a> Chunk<'a> {
sections: &'a Vec<de::SectionV1_18>,
block_types: &'a BlockTypes,
biome_types: &'a BiomeTypes,
) -> Result<Self> {
) -> Result<(ChunkInner<'a>, bool)> {
let mut section_map = BTreeMap::new();
let mut has_unknown = false;
for section in sections {
match &section.section {
@ -118,22 +112,27 @@ impl<'a> Chunk<'a> {
biomes,
block_light,
} => {
let (loaded_section, unknown_blocks) = SectionV1_13::new(
data_version,
block_states.data.as_deref(),
&block_states.palette,
block_types,
)
.with_context(|| format!("Failed to load section at Y={}", section.y))?;
has_unknown |= unknown_blocks;
let (loaded_biomes, unknown_biomes) =
BiomesV1_18::new(biomes.data.as_deref(), &biomes.palette, biome_types)
.with_context(|| {
format!("Failed to load section biomes at Y={}", section.y)
})?;
has_unknown |= unknown_biomes;
section_map.insert(
SectionY(section.y),
(
SectionV1_13::new(
data_version,
block_states.data.as_deref(),
&block_states.palette,
block_types,
)
.with_context(|| {
format!("Failed to load section at Y={}", section.y)
})?,
BiomesV1_18::new(biomes.data.as_deref(), &biomes.palette, biome_types)
.with_context(|| {
format!("Failed to load section biomes at Y={}", section.y)
})?,
loaded_section,
loaded_biomes,
BlockLight::new(block_light.as_deref()).with_context(|| {
format!("Failed to load section block light at Y={}", section.y)
})?,
@ -144,7 +143,8 @@ impl<'a> Chunk<'a> {
};
}
Ok(Chunk::V1_18 { section_map })
let chunk = ChunkInner::V1_18 { section_map };
Ok((chunk, has_unknown))
}
/// [Chunk::new] implementation for all pre-1.18 chunk variants
@ -153,9 +153,10 @@ impl<'a> Chunk<'a> {
level: &'a de::LevelV0,
block_types: &'a BlockTypes,
biome_types: &'a BiomeTypes,
) -> Result<Self> {
) -> Result<(ChunkInner<'a>, bool)> {
let mut section_map_v1_13 = BTreeMap::new();
let mut section_map_v0 = BTreeMap::new();
let mut has_unknown = false;
for section in &level.sections {
let block_light =
@ -167,21 +168,13 @@ impl<'a> Chunk<'a> {
block_states,
palette,
} => {
section_map_v1_13.insert(
SectionY(section.y.into()),
(
SectionV1_13::new(
data_version,
Some(block_states),
palette,
block_types,
)
.with_context(|| {
format!("Failed to load section at Y={}", section.y)
})?,
block_light,
),
);
let (loaded_section, unknown_blocks) =
SectionV1_13::new(data_version, Some(block_states), palette, block_types)
.with_context(|| format!("Failed to load section at Y={}", section.y))?;
has_unknown |= unknown_blocks;
section_map_v1_13
.insert(SectionY(section.y.into()), (loaded_section, block_light));
}
de::SectionV0Variant::V0 { blocks, data } => {
section_map_v0.insert(
@ -199,32 +192,31 @@ impl<'a> Chunk<'a> {
}
let biomes = BiomesV0::new(level.biomes.as_ref(), biome_types);
Ok(
match (section_map_v1_13.is_empty(), section_map_v0.is_empty()) {
(true, true) => Chunk::Empty,
(false, true) => Chunk::V1_13 {
section_map: section_map_v1_13,
biomes: biomes?,
},
(true, false) => Chunk::V0 {
section_map: section_map_v0,
biomes: biomes?,
},
(false, false) => {
bail!("Mixed section versions");
}
let chunk = match (section_map_v1_13.is_empty(), section_map_v0.is_empty()) {
(true, true) => ChunkInner::Empty,
(false, true) => ChunkInner::V1_13 {
section_map: section_map_v1_13,
biomes: biomes?,
},
)
(true, false) => ChunkInner::V0 {
section_map: section_map_v0,
biomes: biomes?,
},
(false, false) => {
bail!("Mixed section versions");
}
};
Ok((chunk, has_unknown))
}
/// Returns true if the chunk does not contain any sections
pub fn is_empty(&self) -> bool {
match self {
Chunk::V1_18 { section_map } => section_map.is_empty(),
Chunk::V1_13 { section_map, .. } => section_map.is_empty(),
Chunk::V0 { section_map, .. } => section_map.is_empty(),
Chunk::Empty => true,
match &self.inner {
ChunkInner::V1_18 { section_map } => section_map.is_empty(),
ChunkInner::V1_13 { section_map, .. } => section_map.is_empty(),
ChunkInner::V0 { section_map, .. } => section_map.is_empty(),
ChunkInner::Empty => true,
}
}
@ -232,28 +224,86 @@ impl<'a> Chunk<'a> {
pub fn sections(&self) -> SectionIter {
use SectionIterInner::*;
SectionIter {
inner: match self {
Chunk::V1_18 { section_map } => V1_18 {
inner: match &self.inner {
ChunkInner::V1_18 { section_map } => V1_18 {
iter: section_map.iter(),
},
Chunk::V1_13 {
ChunkInner::V1_13 {
section_map,
biomes,
} => V1_13 {
iter: section_map.iter(),
biomes,
},
Chunk::V0 {
ChunkInner::V0 {
section_map,
biomes,
} => V0 {
iter: section_map.iter(),
biomes,
},
Chunk::Empty => Empty,
ChunkInner::Empty => Empty,
},
}
}
/// Returns the section at a [SectionY] coordinate
fn section_at(&self, y: SectionY) -> Option<&dyn Section> {
match &self.inner {
ChunkInner::V1_18 { section_map } => section_map
.get(&y)
.map(|(section, _, _)| -> &dyn Section { section }),
ChunkInner::V1_13 { section_map, .. } => section_map
.get(&y)
.map(|(section, _)| -> &dyn Section { section }),
ChunkInner::V0 { section_map, .. } => section_map
.get(&y)
.map(|(section, _)| -> &dyn Section { section }),
ChunkInner::Empty => None,
}
}
/// Returns the [BlockType] at a given coordinate
fn block_type_at(&self, y: SectionY, coords: SectionBlockCoords) -> Result<Option<&BlockType>> {
let Some(section) = self.section_at(y) else {
return Ok(None);
};
section.block_at(coords)
}
/// Returns the [BlockType] at the coordinates of a [de::BlockEntity]
fn block_type_at_block_entity(
&self,
block_entity: &de::BlockEntity,
) -> Result<Option<&BlockType>> {
let x: BlockX = util::from_flat_coord(block_entity.x).2;
let z: BlockZ = util::from_flat_coord(block_entity.z).2;
let (section_y, block_y) = block_entity.y.shift_mask(BLOCK_BITS);
let coords = SectionBlockCoords {
xz: LayerBlockCoords { x, z },
y: BlockY::new(block_y),
};
self.block_type_at(SectionY(section_y), coords)
}
/// Processes all of the chunk's block entities
pub fn block_entities(&self) -> Result<Vec<BlockEntity>> {
let entities: Vec<Option<BlockEntity>> = self
.block_entities
.iter()
.map(|block_entity| {
let block_type = self.block_type_at_block_entity(block_entity)?;
Ok(BlockEntity::new(
block_entity,
block_type,
self.data_version,
))
})
.collect::<Result<_>>()?;
Ok(entities.into_iter().flatten().collect())
}
}
/// Reference to block, biome and block light data of a section
@ -283,6 +333,39 @@ impl<'a, T> SectionIterTrait<'a> for T where
{
}
/// Inner data structure of [SectionIter]
#[derive(Debug, Clone)]
enum SectionIterInner<'a> {
/// Iterator over sections of [ChunkInner::V1_18]
V1_18 {
/// Inner iterator into section map
iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BiomesV1_18<'a>, BlockLight<'a>)>,
},
/// Iterator over sections of [ChunkInner::V1_13]
V1_13 {
/// Inner iterator into section map
iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BlockLight<'a>)>,
/// Chunk biome data
biomes: &'a BiomesV0<'a>,
},
/// Iterator over sections of [ChunkInner::V0]
V0 {
/// Inner iterator into section map
iter: btree_map::Iter<'a, SectionY, (SectionV0<'a>, BlockLight<'a>)>,
/// Chunk biome data
biomes: &'a BiomesV0<'a>,
},
/// Empty iterator over an unpopulated chunk ([ChunkInner::Empty])
Empty,
}
/// Iterator over the sections of a [Chunk]
#[derive(Debug, Clone)]
pub struct SectionIter<'a> {
/// Inner iterator enum
inner: SectionIterInner<'a>,
}
impl<'a> SectionIter<'a> {
/// Helper to run a closure on the inner section iterator
fn with_iter<F, T>(&mut self, f: F) -> T
@ -336,17 +419,17 @@ impl<'a> Iterator for SectionIter<'a> {
}
fn last(mut self) -> Option<Self::Item> {
self.with_iter(|iter| iter.last())
self.next_back()
}
}
impl<'a> DoubleEndedIterator for SectionIter<'a> {
impl DoubleEndedIterator for SectionIter<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
self.with_iter(|iter| iter.next_back())
}
}
impl<'a> ExactSizeIterator for SectionIter<'a> {
impl ExactSizeIterator for SectionIter<'_> {
fn len(&self) -> usize {
match &self.inner {
SectionIterInner::V1_18 { iter } => iter.len(),
@ -357,4 +440,4 @@ impl<'a> ExactSizeIterator for SectionIter<'a> {
}
}
impl<'a> FusedIterator for SectionIter<'a> {}
impl FusedIterator for SectionIter<'_> {}

View file

@ -2,6 +2,8 @@
use serde::Deserialize;
use super::text_value::TextValue;
/// Element of the `palette` list of 1.18+ [block states](BlockStatesV1_18)
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
@ -104,6 +106,77 @@ pub enum BiomesV0 {
ByteArray(fastnbt::ByteArray),
}
/// Front/back text of a Minecraft 1.20+ sign block entry
#[derive(Debug, Deserialize)]
pub struct BlockEntitySignV1_20Text {
/// Lines of sign text
pub messages: Vec<TextValue>,
/// Default text color
pub color: Option<String>,
}
/// A sign (standing or hanging) block entity
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum BlockEntitySign {
/// Pre-1.20 sign block entity
///
/// Pre-1.20 signs only have front text.
#[serde(rename_all = "PascalCase")]
V0 {
/// Line 1 of the sign text
text1: TextValue,
/// Line 2 of the sign text
text2: TextValue,
/// Line 3 of the sign text
text3: TextValue,
/// Line 4 of the sign text
text4: TextValue,
/// Default text color
color: Option<String>,
},
/// 1.20+ sign block entity
V1_20 {
/// The sign's front text
front_text: BlockEntitySignV1_20Text,
/// The sign's back text
back_text: BlockEntitySignV1_20Text,
},
}
/// Data for different kinds of block entities
#[derive(Debug, Deserialize)]
#[serde(tag = "id")]
pub enum BlockEntityData {
/// Regular sign
///
/// This includes standing signs and signs attached to the side of blocks
#[serde(rename = "minecraft:sign", alias = "minecraft:standing_sign")]
Sign(BlockEntitySign),
/// Hanging sign
#[serde(rename = "minecraft:hanging_sign")]
HangingSign(BlockEntitySign),
/// Other block entity types not handled by MinedMap
#[serde(other)]
Other,
}
/// A block entity
///
/// Block entities were called tile entities pre-1.18
#[derive(Debug, Deserialize)]
pub struct BlockEntity {
/// Entity global X coordinate
pub x: i32,
/// Entity global Y coordinate
pub y: i32,
/// Entity global Z coordinate
pub z: i32,
/// Kind-specific entity data
#[serde(flatten)]
pub data: BlockEntityData,
}
/// `Level` compound element found in pre-1.18 [chunks](Chunk)
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
@ -113,6 +186,9 @@ pub struct LevelV0 {
pub sections: Vec<SectionV0>,
/// Biome data
pub biomes: Option<BiomesV0>,
/// List of block entities
#[serde(default)]
pub tile_entities: Vec<BlockEntity>,
}
/// Version-specific part of a [Chunk] compound
@ -123,6 +199,9 @@ pub enum ChunkVariant {
V1_18 {
/// List of chunk sections
sections: Vec<SectionV1_18>,
/// List of block entities
#[serde(default)]
block_entities: Vec<BlockEntity>,
},
/// Pre-1.18 chunk data
#[serde(rename_all = "PascalCase")]

View file

@ -3,17 +3,17 @@
use std::num::NonZeroU16;
use anyhow::{Context, Result};
use bincode::{Decode, Encode};
use indexmap::IndexSet;
use serde::{Deserialize, Serialize};
use super::chunk::{Chunk, SectionIterItem};
use crate::{
resource::{Biome, BlockFlag, BlockType},
resource::{Biome, BlockColor, BlockFlag},
types::*,
};
/// Height (Y coordinate) of a block
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
pub struct BlockHeight(pub i32);
impl BlockHeight {
@ -31,8 +31,8 @@ impl BlockHeight {
}
}
/// Array optionally storing a [BlockType] for each coordinate of a chunk
pub type BlockArray = LayerBlockArray<Option<BlockType>>;
/// Array optionally storing a [BlockColor] for each coordinate of a chunk
pub type BlockArray = LayerBlockArray<Option<BlockColor>>;
/// Array optionally storing a biome index for each coordinate of a chunk
///
@ -49,7 +49,7 @@ pub type DepthArray = LayerBlockArray<Option<BlockHeight>>;
/// References to LayerData entries for a single coordinate pair
struct LayerEntry<'a> {
/// The block type of the referenced entry
block: &'a mut Option<BlockType>,
block: &'a mut Option<BlockColor>,
/// The biome type of the referenced entry
biome: &'a mut Option<NonZeroU16>,
/// The block light of the referenced entry
@ -58,7 +58,7 @@ struct LayerEntry<'a> {
depth: &'a mut Option<BlockHeight>,
}
impl<'a> LayerEntry<'a> {
impl LayerEntry<'_> {
/// Returns true if the entry has not been filled yet (no opaque block has been encountered)
///
/// The depth value is filled separately when a non-water block is encountered after the block type
@ -86,7 +86,7 @@ impl<'a> LayerEntry<'a> {
let Some(block_type) = section
.section
.block_at(coords)?
.filter(|block_type| block_type.is(BlockFlag::Opaque))
.filter(|block_type| block_type.block_color.is(BlockFlag::Opaque))
else {
if self.is_empty() {
*self.block_light = section.block_light.block_light_at(coords);
@ -96,18 +96,18 @@ impl<'a> LayerEntry<'a> {
};
if self.is_empty() {
*self.block = Some(block_type);
if let Some(biome) = section.biomes.biome_at(section.y, coords)? {
let (biome_index, _) = biome_list.insert_full(*biome);
*self.biome = NonZeroU16::new(
(biome_index + 1)
.try_into()
.expect("biome index not in range"),
);
}
*self.block = Some(block_type.block_color);
let biome = section.biomes.biome_at(section.y, coords)?;
let (biome_index, _) = biome_list.insert_full(*biome);
*self.biome = NonZeroU16::new(
(biome_index + 1)
.try_into()
.expect("biome index not in range"),
);
}
if block_type.is(BlockFlag::Water) {
if block_type.block_color.is(BlockFlag::Water) {
return Ok(false);
}

View file

@ -1,6 +1,9 @@
//! Data structures describing Minecraft save data
pub mod block_entity;
pub mod chunk;
pub mod de;
pub mod layer;
pub mod section;
pub mod sign;
pub mod text_value;

View file

@ -5,9 +5,9 @@
use std::fmt::Debug;
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use num_integer::div_rem;
use tracing::warn;
use tracing::debug;
use super::de;
use crate::{
@ -44,7 +44,7 @@ fn palette_bits(len: usize, min: u8, max: u8) -> Option<u8> {
/// Trait for common functions of [SectionV1_13] and [SectionV0]
pub trait Section: Debug {
/// Returns the [BlockType] at a coordinate tuple inside the section
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<BlockType>>;
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<&BlockType>>;
}
/// Minecraft v1.13+ section block data
@ -53,7 +53,7 @@ pub struct SectionV1_13<'a> {
/// Packed block type data
block_states: Option<&'a [i64]>,
/// List of block types indexed by entries encoded in *block_states*
palette: Vec<Option<BlockType>>,
palette: Vec<Option<&'a BlockType>>,
/// Number of bits per block in *block_states*
bits: u8,
/// Set to true if packed block entries in *block_states* are aligned to i64
@ -73,7 +73,7 @@ impl<'a> SectionV1_13<'a> {
block_states: Option<&'a [i64]>,
palette: &'a [de::BlockStatePaletteEntry],
block_types: &'a BlockTypes,
) -> Result<Self> {
) -> Result<(Self, bool)> {
let aligned_blocks = data_version >= 2529;
let bits = palette_bits(palette.len(), 4, 12).context("Unsupported block palette size")?;
@ -81,7 +81,7 @@ impl<'a> SectionV1_13<'a> {
if let Some(block_states) = block_states {
let expected_length = if aligned_blocks {
let blocks_per_word = 64 / bits as usize;
(4096 + blocks_per_word - 1) / blocks_per_word
4096usize.div_ceil(blocks_per_word)
} else {
64 * bits as usize
};
@ -90,23 +90,29 @@ impl<'a> SectionV1_13<'a> {
}
}
let mut has_unknown = false;
let palette_types = palette
.iter()
.map(|entry| {
let block_type = block_types.get(&entry.name);
if block_type.is_none() {
warn!("Unknown block type: {}", entry.name);
debug!("Unknown block type: {}", entry.name);
has_unknown = true;
}
block_type
})
.collect();
Ok(Self {
block_states,
palette: palette_types,
bits,
aligned_blocks,
})
Ok((
Self {
block_states,
palette: palette_types,
bits,
aligned_blocks,
},
has_unknown,
))
}
/// Looks up the block type palette index at the given coordinates
@ -139,8 +145,8 @@ impl<'a> SectionV1_13<'a> {
}
}
impl<'a> Section for SectionV1_13<'a> {
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<BlockType>> {
impl Section for SectionV1_13<'_> {
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<&BlockType>> {
let index = self.palette_index_at(coords);
Ok(*self
.palette
@ -182,8 +188,8 @@ impl<'a> SectionV0<'a> {
}
}
impl<'a> Section for SectionV0<'a> {
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<BlockType>> {
impl Section for SectionV0<'_> {
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<&BlockType>> {
let offset = coords.offset();
let block = self.blocks[offset] as u8;
@ -202,7 +208,7 @@ impl<'a> Section for SectionV0<'a> {
/// Trait for common functions of [BiomesV1_18] and [BiomesV0]
pub trait Biomes: Debug {
/// Returns the [Biome] at a coordinate tuple inside the chunk
fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<Option<&Biome>>;
fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<&Biome>;
}
/// Minecraft v1.18+ section biome data
@ -220,7 +226,7 @@ pub struct BiomesV1_18<'a> {
/// to whole i64 values.
biomes: Option<&'a [i64]>,
/// Biome palette indexed by entries encoded in *biomes*
palette: Vec<Option<&'a Biome>>,
palette: Vec<&'a Biome>,
/// Number of bits used for each entry in *biomes*
bits: u8,
}
@ -231,33 +237,38 @@ impl<'a> BiomesV1_18<'a> {
biomes: Option<&'a [i64]>,
palette: &'a [String],
biome_types: &'a BiomeTypes,
) -> Result<Self> {
) -> Result<(Self, bool)> {
let bits = palette_bits(palette.len(), 1, 6).context("Unsupported block palette size")?;
if let Some(biomes) = biomes {
let biomes_per_word = 64 / bits as usize;
let expected_length = (64 + biomes_per_word - 1) / biomes_per_word;
let expected_length = 64usize.div_ceil(biomes_per_word);
if biomes.len() != expected_length {
bail!("Invalid section biome data");
}
}
let mut has_unknown = false;
let palette_types = palette
.iter()
.map(|entry| {
let biome_type = biome_types.get(entry);
if biome_type.is_none() {
warn!("Unknown biome type: {}", entry);
}
biome_type
biome_types.get(entry).unwrap_or_else(|| {
debug!("Unknown biome type: {}", entry);
has_unknown = true;
biome_types.get_fallback()
})
})
.collect();
Ok(BiomesV1_18 {
biomes,
palette: palette_types,
bits,
})
Ok((
BiomesV1_18 {
biomes,
palette: palette_types,
bits,
},
has_unknown,
))
}
/// Looks up the block type palette index at the given coordinates
@ -282,8 +293,8 @@ impl<'a> BiomesV1_18<'a> {
}
}
impl<'a> Biomes for BiomesV1_18<'a> {
fn biome_at(&self, _section: SectionY, coords: SectionBlockCoords) -> Result<Option<&Biome>> {
impl Biomes for BiomesV1_18<'_> {
fn biome_at(&self, _section: SectionY, coords: SectionBlockCoords) -> Result<&Biome> {
let index = self.palette_index_at(coords);
Ok(*self
.palette
@ -337,8 +348,8 @@ impl<'a> BiomesV0<'a> {
}
}
impl<'a> Biomes for BiomesV0<'a> {
fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<Option<&Biome>> {
impl Biomes for BiomesV0<'_> {
fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<&Biome> {
let id = match self.data {
BiomesV0Data::IntArrayV15(data) => {
let LayerBlockCoords { x, z } = coords.xz;
@ -358,7 +369,10 @@ impl<'a> Biomes for BiomesV0<'a> {
}
BiomesV0Data::ByteArray(data) => data[coords.xz.offset()] as u8,
};
Ok(self.biome_types.get_legacy(id))
Ok(self
.biome_types
.get_legacy(id)
.unwrap_or(self.biome_types.get_fallback()))
}
}
@ -386,10 +400,6 @@ impl<'a> BlockLight<'a> {
let (offset, nibble) = div_rem(coords.offset(), 2);
let byte = block_light[offset] as u8;
if nibble == 1 {
byte >> 4
} else {
byte & 0xf
}
if nibble == 1 { byte >> 4 } else { byte & 0xf }
}
}

158
src/world/sign.rs Normal file
View file

@ -0,0 +1,158 @@
//! Processing of sign text
use std::fmt::Display;
use bincode::{Decode, Encode};
use minedmap_resource::Color;
use serde::Serialize;
use super::{
de,
text_value::{FormattedText, FormattedTextList, TextValue},
};
/// Version-independent reference to (front or back) sign text
#[derive(Debug, Default)]
pub struct RawSignText<'a> {
/// Lines of sign text
///
/// A regular sign always has 4 lines of text. The back of pre-1.20
/// signs is represented as a [SignText] without any `messages`.
pub messages: Vec<&'a TextValue>,
/// Sign color
///
/// Defaults to "black".
pub color: Option<&'a str>,
}
/// The color to use for signs without a color attribute ("black")
const DEFAULT_COLOR: Color = Color([0, 0, 0]);
/// Map of text colors associated with dyes (except for black)
static DYE_COLORS: phf::Map<&'static str, Color> = phf::phf_map! {
"white" => Color([255, 255, 255]),
"orange" => Color([255, 104, 31]),
"magenta" => Color([255, 0, 255]),
"light_blue" => Color([154, 192, 205]),
"yellow" => Color([255, 255, 0]),
"lime" => Color([191, 255, 0]),
"pink" => Color([255, 105, 180]),
"gray" => Color([128, 128, 128]),
"light_gray" => Color([211, 211, 211]),
"cyan" => Color([0, 255, 255]),
"purple" => Color([160, 32, 240]),
"blue" => Color([0, 0, 255]),
"brown" => Color([139, 69, 19]),
"green" => Color([0, 255, 0]),
"red" => Color([255, 0, 0]),
};
impl RawSignText<'_> {
/// Decodes the [RawSignText] into a [SignText]
pub fn decode(&self, data_version: u32) -> SignText {
let color = self
.color
.map(|c| DYE_COLORS.get(c).copied().unwrap_or(DEFAULT_COLOR));
let parent = FormattedText {
color,
..Default::default()
};
SignText(
self.messages
.iter()
.map(|message| message.deserialize(data_version).linearize(&parent))
.collect(),
)
}
}
impl<'a> From<&'a de::BlockEntitySignV1_20Text> for RawSignText<'a> {
fn from(value: &'a de::BlockEntitySignV1_20Text) -> Self {
RawSignText {
messages: value.messages.iter().collect(),
color: value.color.as_deref(),
}
}
}
/// Helper methods for [de::BlockEntitySign]
pub trait BlockEntitySignExt {
/// Returns the front and back text of a sign in a version-indepentent format
fn text(&self) -> (RawSignText, RawSignText);
}
impl BlockEntitySignExt for de::BlockEntitySign {
fn text(&self) -> (RawSignText, RawSignText) {
match self {
de::BlockEntitySign::V0 {
text1,
text2,
text3,
text4,
color,
} => (
RawSignText {
messages: vec![text1, text2, text3, text4],
color: color.as_deref(),
},
Default::default(),
),
de::BlockEntitySign::V1_20 {
front_text,
back_text,
} => (front_text.into(), back_text.into()),
}
}
}
#[derive(Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)]
/// Deserialized and linearized sign text
pub struct SignText(pub Vec<FormattedTextList>);
impl SignText {
/// Checks if all lines of the sign text are empty
pub fn is_empty(&self) -> bool {
self.0.iter().all(|line| line.is_empty())
}
}
impl Display for SignText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut iter = self.0.iter();
let Some(first) = iter.next() else {
return Ok(());
};
first.fmt(f)?;
for text in iter {
f.write_str("\n")?;
text.fmt(f)?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
fn formatted_text(text: &str) -> FormattedText {
FormattedText {
text: text.to_string(),
..Default::default()
}
}
#[test]
fn test_sign_text_display() {
let sign_text = SignText(vec![
FormattedTextList(vec![formatted_text("a"), formatted_text("b")]),
FormattedTextList(vec![formatted_text("c")]),
FormattedTextList(vec![formatted_text("d")]),
FormattedTextList(vec![formatted_text("e")]),
]);
assert_eq!("ab\nc\nd\ne", sign_text.to_string());
}
}

274
src/world/text_value.rs Normal file
View file

@ -0,0 +1,274 @@
//! Newtype and helper methods for handling Minecraft text values
use std::{collections::VecDeque, fmt::Display};
use bincode::{Decode, Encode};
use minedmap_resource::Color;
use serde::{Deserialize, Serialize};
/// A span of formatted text
///
/// A [TextValue] consists of a tree of [FormattedText] nodes (canonically
/// represented as a [FormattedTextTree], but other kinds are possible with
/// is handled by [DeserializedText].
///
/// Formatting that is not set in a node is inherited from the parent.
#[derive(
Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Encode, Decode,
)]
pub struct FormattedText {
#[serde(default)]
/// Text content
pub text: String,
/// Text color
#[serde(skip_serializing_if = "Option::is_none", with = "text_color")]
pub color: Option<Color>,
/// Bold formatting
#[serde(skip_serializing_if = "Option::is_none")]
pub bold: Option<bool>,
/// Italic formatting
#[serde(skip_serializing_if = "Option::is_none")]
pub italic: Option<bool>,
/// Underlines formatting
#[serde(skip_serializing_if = "Option::is_none")]
pub underlined: Option<bool>,
/// Strikethrough formatting
#[serde(skip_serializing_if = "Option::is_none")]
pub strikethrough: Option<bool>,
/// Obfuscated formatting
#[serde(skip_serializing_if = "Option::is_none")]
pub obfuscated: Option<bool>,
}
impl FormattedText {
/// Fills in unset formatting fields from a parent node
pub fn inherit(self, parent: &Self) -> Self {
FormattedText {
text: self.text,
color: self.color.or(parent.color),
bold: self.bold.or(parent.bold),
italic: self.italic.or(parent.italic),
underlined: self.underlined.or(parent.underlined),
strikethrough: self.strikethrough.or(parent.strikethrough),
obfuscated: self.obfuscated.or(parent.obfuscated),
}
}
}
impl Display for FormattedText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.text.fmt(f)
}
}
/// A tree of [FormattedText] nodes
///
/// Each node including the root has a `text` and a list of children (`extra`).
#[derive(Debug, Deserialize, Default)]
pub struct FormattedTextTree {
/// Root node content
#[serde(flatten)]
text: FormattedText,
/// List of child trees
#[serde(default)]
extra: VecDeque<DeserializedText>,
}
impl From<String> for FormattedTextTree {
fn from(value: String) -> Self {
FormattedTextTree {
text: FormattedText {
text: value,
..Default::default()
},
extra: VecDeque::new(),
}
}
}
/// List of [FormattedText]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Encode, Decode)]
pub struct FormattedTextList(pub Vec<FormattedText>);
impl FormattedTextList {
/// Returns `true` when [FormattedTextList] does not contain any text
pub fn is_empty(&self) -> bool {
self.0.iter().all(|text| text.text.is_empty())
}
}
impl Display for FormattedTextList {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for text in &self.0 {
text.fmt(f)?;
}
Ok(())
}
}
/// Raw deserialized [TextValue]
///
/// A [TextValue] can contain various different types serialized as JSON or NBT.
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum DeserializedText {
/// Unformatted string
String(String),
/// Unformatted number (will be converted to a string)
Number(f32),
/// Unformatted boolean (will be converted to a string)
Boolean(bool),
/// List of [DeserializedText]
///
/// The tail elements are appended as children of the head element.
List(VecDeque<DeserializedText>),
/// The canonical [FormattedTextTree] structure
Object(FormattedTextTree),
}
impl DeserializedText {
/// Converts a [DeserializedText] into the regular [FormattedTextTree] format
///
/// Most variants are simply converted to strings. A list is handled by
/// appending all tail elements to the `extra` field of the head.
pub fn canonicalize(self) -> FormattedTextTree {
match self {
DeserializedText::Object(obj) => obj,
DeserializedText::String(s) => FormattedTextTree::from(s),
DeserializedText::Number(n) => FormattedTextTree::from(n.to_string()),
DeserializedText::Boolean(b) => FormattedTextTree::from(b.to_string()),
DeserializedText::List(mut list) => {
let mut obj = list
.pop_front()
.map(|t| t.canonicalize())
.unwrap_or_default();
obj.extra.append(&mut list);
obj
}
}
}
/// Converts the tree of [FormattedText] nodes into a linear list by
/// copying formatting flags into each node.
pub fn linearize(self, parent: &FormattedText) -> FormattedTextList {
let obj = self.canonicalize();
let mut ret = vec![obj.text.inherit(parent)];
for extra in obj.extra {
ret.append(&mut extra.linearize(&ret[0]).0);
}
FormattedTextList(ret)
}
}
impl Default for DeserializedText {
fn default() -> Self {
DeserializedText::Object(FormattedTextTree::from(String::new()))
}
}
/// Minecraft raw text value
#[derive(Debug, Deserialize)]
pub struct TextValue(pub fastnbt::Value);
impl TextValue {
/// Deserializes a [TextValue] into a [DeserializedText]
pub fn deserialize(&self, data_version: u32) -> DeserializedText {
// TODO: Improve error handling
//
// Unfortunately, there are a number of weird ways an empty sign coould
// be encoded (for example a compound with an "" key), so for now we
// simply interpret undecodable data as empty.
if data_version < 4290 {
let fastnbt::Value::String(json) = &self.0 else {
return DeserializedText::default();
};
serde_json::from_str(json).unwrap_or_default()
} else {
fastnbt::from_value(&self.0).unwrap_or_default()
}
}
}
mod text_color {
//! Helpers for serializing and deserializing [FormattedText](super::FormattedText) colors
use minedmap_resource::Color;
use serde::{
Deserializer, Serializer,
de::{self, Visitor},
ser::Error as _,
};
/// Named text colors
static COLORS: phf::Map<&'static str, Color> = phf::phf_map! {
"black" => Color([0x00, 0x00, 0x00]),
"dark_blue" => Color([0x00, 0x00, 0xAA]),
"dark_green" => Color([0x00, 0xAA, 0x00]),
"dark_aqua" => Color([0x00, 0xAA, 0xAA]),
"dark_red" => Color([0xAA, 0x00, 0x00]),
"dark_purple" => Color([0xAA, 0x00, 0xAA]),
"gold" => Color([0xFF, 0xAA, 0x00]),
"gray" => Color([0xAA, 0xAA, 0xAA]),
"dark_gray" => Color([0x55, 0x55, 0x55]),
"blue" => Color([0x55, 0x55, 0xFF]),
"green" => Color([0x55, 0xFF, 0x55]),
"aqua" => Color([0x55, 0xFF, 0xFF]),
"red" => Color([0xFF, 0x55, 0x55]),
"light_purple" => Color([0xFF, 0x55, 0xFF]),
"yellow" => Color([0xFF, 0xFF, 0x55]),
"white" => Color([0xFF, 0xFF, 0xFF]),
};
/// serde serialize function for [FormattedText::color](super::FormattedText::color)
pub fn serialize<S>(color: &Option<Color>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let &Some(color) = color else {
return Err(S::Error::custom("serialize called for None sign color"));
};
let text = format!("#{:02x}{:02x}{:02x}", color.0[0], color.0[1], color.0[2]);
serializer.serialize_str(&text)
}
/// serde [Visitor] for use by [deserialize]
struct ColorVisitor;
impl Visitor<'_> for ColorVisitor {
type Value = Option<Color>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string representing a color")
}
fn visit_str<E>(self, color: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
if let Some(hex) = color.strip_prefix("#") {
if let Ok(value) = u32::from_str_radix(hex, 16) {
return Ok(Some(Color([
(value >> 16) as u8,
(value >> 8) as u8,
value as u8,
])));
}
}
Ok(COLORS.get(color).copied())
}
}
/// serde deserialize function for [FormattedText::color](super::FormattedText::color)
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(ColorVisitor)
}
}

1
viewer/.dockerignore Normal file
View file

@ -0,0 +1 @@
/data

3
viewer/Dockerfile Normal file
View file

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

View file

@ -17,8 +17,63 @@ function contains(array, elem) {
return false;
}
const signKinds = {
sign: {
iconSize: [26, 28],
popupAnchor: [0, -20],
},
wall_sign: {
iconSize: [26, 18],
popupAnchor: [0, -15],
},
hanging_sign: {
iconSize: [28, 24],
popupAnchor: [0, -18],
},
hanging_wall_sign: {
iconSize: [28, 28],
popupAnchor: [0, -20],
},
}
const params = {};
const signIcons = {};
const markers = {};
let updateHash = () => {};
function coordKey(coords) {
if (!coords)
return null;
return `${coords[0]},${coords[1]}`;
}
function getMarker(coords) {
return markers[coordKey(coords)];
}
function signIcon(material, kind) {
function createSignIcon(material, kind) {
const {iconSize, popupAnchor} = signKinds[kind];
return L.icon({
iconUrl: `images/icon/${material}_${kind}.png`,
iconSize,
popupAnchor,
shadowUrl: `images/icon/shadow_${kind}.png`,
shadowSize: [iconSize[0]+8, iconSize[1]+8],
className: 'overzoomed',
});
}
let icons = signIcons[material] ??= {};
return icons[kind] ??= createSignIcon(material, kind);
}
const MinedMapLayer = L.TileLayer.extend({
initialize: function (mipmaps, layer) {
initialize: function (mipmaps, layer, tile_extension) {
L.TileLayer.prototype.initialize.call(this, '', {
detectRetina: true,
tileSize: 512,
@ -33,6 +88,7 @@ const MinedMapLayer = L.TileLayer.extend({
this.mipmaps = mipmaps;
this.layer = layer;
this.ext = tile_extension;
},
createTile: function (coords, done) {
@ -57,7 +113,7 @@ const MinedMapLayer = L.TileLayer.extend({
return L.Util.emptyImageUrl;
return 'data/'+this.layer+'/'+z+'/r.'+coords.x+'.'+coords.y+'.png';
return `data/${this.layer}/${z}/r.${coords.x}.${coords.y}.${this.ext}`;
},
});
@ -98,39 +154,214 @@ const parseHash = function () {
return args;
}
function formatSignLine(line) {
const el = document.createElement('span');
el.style.whiteSpace = 'pre';
for (const span of line) {
const child = document.createElement('span');
child.textContent = span.text;
let color = span.color ?? '';
if (color[0] !== '#')
color = '#000000';
if (span.bold)
child.style.fontWeight = 'bold';
if (span.italic)
child.style.fontStyle = 'italic';
child.style.textDecoration = '';
if (span.underlined)
child.style.textDecoration += ' underline';
if (span.strikethrough)
child.style.textDecoration += ' line-through';
child.style.color = color;
if (span.obfuscated) {
child.style.backgroundColor = color;
child.className = 'obfuscated';
}
el.appendChild(child);
}
return el;
}
function createSign(sign, back) {
// standing signs
function px(base) {
const scale = 11;
return (base*scale)+'px';
}
// hanging signs
function pxh(base) {
const scale = 16;
return (base*scale)+'px';
}
const sizes = {
sign: {
width: px(24),
height: px(12),
paddingTop: px(0),
paddingBottom: px(14),
},
wall_sign: {
width: px(24),
height: px(12),
paddingTop: px(0),
paddingBottom: px(0),
},
hanging_sign: {
width: pxh(16),
height: pxh(10),
paddingTop: pxh(4),
paddingBottom: pxh(0),
},
hanging_wall_sign: {
width: pxh(16),
height: pxh(10),
paddingTop: pxh(6),
paddingBottom: pxh(0),
},
};
const size = sizes[sign.kind];
const wrapper = document.createElement('div');
wrapper.classList = 'sign-wrapper';
const title = document.createElement('div');
title.classList = 'sign-title'
title.textContent = `Sign at ${sign.x}/${sign.y}/${sign.z}`;
if (back)
title.textContent += ' (back)';
title.textContent += ':';
wrapper.appendChild(title);
const container = document.createElement('div');
container.style.width = size.width;
container.style.height = size.height;
container.style.paddingTop = size.paddingTop;
container.style.paddingBottom = size.paddingBottom;
container.style.backgroundImage = `url(images/bg/${sign.material}_${sign.kind}.png)`;
container.classList = 'sign-container overzoomed';
const content = document.createElement('div');
content.classList = 'sign-content';
let text = [];
if (!back && sign.front_text)
text = sign.front_text;
else if (back && sign.back_text)
text = sign.back_text;
for (const line of text) {
content.appendChild(formatSignLine(line));
content.appendChild(document.createElement('br'));
}
container.appendChild(content);
wrapper.appendChild(container);
return wrapper;
}
async function loadSigns(signLayer) {
const response = await fetch('data/entities.json', {cache: 'no-store'});
const res = await response.json();
const groups = {};
// Group signs by x,z coordinates
for (const sign of res.signs) {
const key = coordKey([sign.x, sign.z]);
const group = groups[key] ??= [];
group.push(sign);
}
for (const [key, group] of Object.entries(groups)) {
const el = document.createElement('div');
let material;
let kind;
// Sort from top to bottom
group.sort((a, b) => b.y - a.y);
for (const sign of group) {
el.appendChild(createSign(sign, false));
if (sign.back_text)
el.appendChild(createSign(sign, true));
material ??= sign.material;
kind ??= sign.kind;
}
// Default material
material ??= 'oak';
const [x, z] = key.split(',').map((i) => +i);
const popup = L.popup().setContent(el);
popup.on('add', () => {
params.marker = [x, z];
updateHash();
});
popup.on('remove', () => {
params.marker = null;
updateHash();
});
const marker = L.marker([-z-0.5, x+0.5], {
icon: signIcon(material, kind),
}).addTo(signLayer).bindPopup(popup);
markers[coordKey([x, z])] = marker;
if (params.marker && x === params.marker[0] && z === params.marker[1])
marker.openPopup();
}
}
window.createMap = function () {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
const res = JSON.parse(this.responseText),
mipmaps = res.mipmaps,
spawn = res.spawn;
let x, z, zoom, light;
(async function () {
const response = await fetch('data/info.json', {cache: 'no-store'});
const res = await response.json();
const {mipmaps, spawn} = res;
const features = res.features || {};
const tile_extension = res.tile_extension || 'png';
const updateParams = function () {
const args = parseHash();
zoom = parseInt(args['zoom']);
x = parseFloat(args['x']);
z = parseFloat(args['z']);
light = parseInt(args['light']);
params.zoom = parseInt(args['zoom']);
params.x = parseFloat(args['x']);
params.z = parseFloat(args['z']);
params.light = parseInt(args['light']);
params.signs = parseInt(args['signs'] ?? '1');
params.marker = (args['marker'] ?? '').split(',').map((i) => +i);
if (isNaN(zoom))
zoom = 0;
if (isNaN(x))
x = spawn.x;
if (isNaN(z))
z = spawn.z;
if (isNaN(params.zoom))
params.zoom = 0;
if (isNaN(params.x))
params.x = spawn.x;
if (isNaN(params.z))
params.z = spawn.z;
if (!features.signs || isNaN(params.marker[0]) || isNaN(params.marker[1]))
params.marker = null;
};
updateParams();
const map = L.map('map', {
center: [-z, x],
zoom: zoom,
center: [-params.z, params.x],
zoom: params.zoom,
minZoom: -(mipmaps.length-1),
maxZoom: 3,
maxZoom: 5,
crs: L.CRS.Simple,
maxBounds: [
[-512*(mipmaps[0].bounds.maxZ+1), 512*mipmaps[0].bounds.minX],
@ -138,17 +369,25 @@ window.createMap = function () {
],
});
const mapLayer = new MinedMapLayer(mipmaps, 'map');
const lightLayer = new MinedMapLayer(mipmaps, 'light');
const overlayMaps = {};
const mapLayer = new MinedMapLayer(mipmaps, 'map', tile_extension);
mapLayer.addTo(map);
if (light)
const lightLayer = new MinedMapLayer(mipmaps, 'light', tile_extension);
overlayMaps['Illumination'] = lightLayer;
if (params.light)
map.addLayer(lightLayer);
const overlayMaps = {
"Illumination": lightLayer,
};
let signLayer;
if (features.signs) {
signLayer = L.layerGroup();
loadSigns(signLayer);
if (params.signs)
map.addLayer(signLayer);
overlayMaps['Signs'] = signLayer;
}
L.control.layers({}, overlayMaps).addTo(map);
@ -160,26 +399,37 @@ window.createMap = function () {
});
const makeHash = function () {
let ret = '#x='+x+'&z='+z;
let ret = '#x='+params.x+'&z='+params.z;
if (zoom != 0)
ret += '&zoom='+zoom;
if (params.zoom != 0)
ret += '&zoom='+params.zoom;
if (map.hasLayer(lightLayer))
ret += '&light=1';
if (features.signs && !map.hasLayer(signLayer))
ret += '&signs=0';
if (params.marker) {
ret += `&marker=${params.marker[0]},${params.marker[1]}`;
}
return ret;
};
const updateHash = function () {
updateHash = function () {
window.location.hash = makeHash();
};
const refreshHash = function () {
zoom = map.getZoom();
center = map.getCenter();
x = Math.round(center.lng);
z = Math.round(-center.lat);
const refreshHash = function (ev) {
if (ev.type === 'layeradd' || ev.type === 'layerremove') {
if (ev.layer !== lightLayer && ev.layer !== signLayer)
return;
}
const center = map.getCenter();
params.zoom = map.getZoom();
params.x = Math.round(center.lng);
params.z = Math.round(-center.lat);
updateHash();
}
@ -195,20 +445,29 @@ window.createMap = function () {
if (window.location.hash === makeHash())
return;
const prevMarkerCoords = params.marker;
updateParams();
map.setView([-z, x], zoom);
if (light)
if (params.light)
map.addLayer(lightLayer);
else
map.removeLayer(lightLayer);
if (features.signs) {
if (params.signs)
map.addLayer(signLayer);
else
map.removeLayer(signLayer);
if (coordKey(prevMarkerCoords) !== coordKey(params.marker))
getMarker(params.marker)?.openPopup();
}
map.setView([-params.z, params.x], params.zoom);
updateHash();
};
};
xhr.open('GET', 'data/info.json', true);
xhr.send();
})();
}

7
viewer/images/README.md Normal file
View file

@ -0,0 +1,7 @@
# README
The images in this directory are assets directly taken from Minecraft, or are derived from Minecraft
assets. They are copyrighted by Mojang/Microsoft, and are used in accordance with the
[Minecraft Usage Guidelines](https://www.minecraft.net/en-us/usage-guidelines).

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Some files were not shown because too many files have changed in this diff Show more