Merge pull request #40 from neocturne/sign-marker

Display markers for signs
This commit is contained in:
Matthias Schiffer 2024-01-11 12:59:32 +01:00 committed by GitHub
commit 3ceb7ae188
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
125 changed files with 8369 additions and 3443 deletions

View file

@ -2,6 +2,13 @@
## [Unreleased] - ReleaseDate
### 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.
### Changed
- Without `--verbose`, only a single warning is printed at the end of

102
Cargo.lock generated
View file

@ -29,6 +29,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.16"
@ -70,7 +79,7 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys",
"windows-sys 0.52.0",
]
[[package]]
@ -80,7 +89,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys",
"windows-sys 0.52.0",
]
[[package]]
@ -125,6 +134,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "bytemuck"
version = "1.14.0"
@ -179,6 +194,7 @@ dependencies = [
"anstyle",
"clap_lex",
"strsim",
"terminal_size",
]
[[package]]
@ -287,6 +303,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "fastnbt"
version = "2.4.4"
@ -481,6 +507,12 @@ dependencies = [
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
[[package]]
name = "lock_api"
version = "0.4.11"
@ -531,6 +563,7 @@ dependencies = [
"num-integer",
"num_cpus",
"rayon",
"regex",
"rustc-hash",
"serde",
"serde_json",
@ -698,7 +731,7 @@ version = "0.17.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
@ -749,9 +782,38 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags",
"bitflags 1.3.2",
]
[[package]]
name = "regex"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "rustc-demangle"
version = "0.1.23"
@ -764,6 +826,19 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
dependencies = [
"bitflags 2.4.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "ryu"
version = "1.0.16"
@ -863,6 +938,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "terminal_size"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "thread_local"
version = "1.1.7"
@ -987,6 +1072,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"

View file

@ -39,7 +39,7 @@ pre-release-replacements = [
[dependencies]
anyhow = "1.0.68"
bincode = "1.3.3"
clap = { version = "4.1.4", features = ["derive"] }
clap = { version = "4.1.4", features = ["derive", "wrap_help"] }
fastnbt = "2.3.2"
futures-util = "0.3.28"
git-version = "0.3.5"
@ -52,6 +52,7 @@ minedmap-types = { version = "0.1.2", path = "crates/types" }
num-integer = "0.1.45"
num_cpus = "1.16.0"
rayon = "1.7.0"
regex = "1.10.2"
rustc-hash = "1.1.0"
serde = { version = "1.0.152", features = ["rc", "derive"] }
serde_json = "1.0.99"

View file

@ -23,7 +23,7 @@ based on [Leaflet](https://leafletjs.com/). The map renderer is heavily inspired
## How to use
Minecraft stores its save data in a directory `~/.minecraft/saves` on Linux,
and `C:\Users\<username>\AppData\Roaming\.minecraft\saves`. To generate minedmap
and `C:\Users\<username>\AppData\Roaming\.minecraft\saves`. To generate MinedMap
tile data from a save game called "World", use the a command like the following
(replacing the first argument with the path to your save data; `viewer` refers
to the directory where you unpacked the MinedMap viewer):
@ -47,6 +47,34 @@ 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.
### 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.
## Installation
Binary builds of the map generator for Linux and Windows, as well as an archive

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

@ -27,10 +27,15 @@ 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
@ -42,21 +47,48 @@ pub type Colorf = glam::Vec3;
/// A block type specification
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct BlockType {
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)
}
}
/// 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 +102,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,15 +123,15 @@ 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])
}
}

View file

@ -11,6 +11,7 @@ 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`
- `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.
@ -42,12 +43,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.

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"
},
@ -326,7 +362,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 +376,8 @@
},
"cherry_sapling": null,
"cherry_sign": {
"texture": "cherry_planks"
"sign_material": "cherry",
"texture": null
},
"cherry_slab": {
"texture": "cherry_planks"
@ -346,8 +386,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"
},
@ -433,7 +481,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 +495,8 @@
},
"crimson_roots": {},
"crimson_sign": {
"texture": "crimson_planks"
"sign_material": "crimson",
"texture": null
},
"crimson_slab": {
"texture": "crimson_planks"
@ -456,8 +508,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 +572,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 +588,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 +598,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"
},
@ -832,7 +904,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 +920,8 @@
},
"jungle_sapling": {},
"jungle_sign": {
"texture": "jungle_planks"
"sign_material": "jungle",
"texture": null
},
"jungle_slab": {
"texture": "jungle_planks"
@ -854,8 +930,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"
},
@ -991,7 +1075,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 +1094,8 @@
"texture": "mangrove_roots_top"
},
"mangrove_sign": {
"texture": "mangrove_planks"
"sign_material": "mangrove",
"texture": null
},
"mangrove_slab": {
"texture": "mangrove_planks"
@ -1016,8 +1104,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 +1201,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 +1217,8 @@
},
"oak_sapling": {},
"oak_sign": {
"texture": "oak_planks"
"sign_material": "oak",
"texture": null
},
"oak_slab": {
"texture": "oak_planks"
@ -1127,8 +1227,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"
},
@ -1562,7 +1670,8 @@
"shroomlight": {},
"shulker_box": {},
"sign": {
"texture": "oak_planks"
"sign_material": "oak",
"texture": null
},
"skeleton_skull": null,
"skeleton_wall_skull": null,
@ -1638,7 +1747,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 +1763,8 @@
},
"spruce_sapling": {},
"spruce_sign": {
"texture": "spruce_planks"
"sign_material": "spruce",
"texture": null
},
"spruce_slab": {
"texture": "spruce_planks"
@ -1660,8 +1773,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"
},
@ -1808,7 +1929,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 +1946,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 +1960,8 @@
},
"warped_roots": {},
"warped_sign": {
"texture": "warped_planks"
"sign_material": "warped",
"texture": null
},
"warped_slab": {
"texture": "warped_planks"
@ -1844,8 +1973,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",

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]['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

@ -18,7 +18,7 @@ with open(sys.argv[2], 'w') as f:
print('', file=f)
print('use super::*;', file=f)
print('', file=f)
print('pub const BLOCK_TYPES: &[(&str, BlockType)] = &[', file=f)
print('pub const BLOCK_TYPES: &[(&str, ConstBlockType)] = &[', file=f)
for name, info in colors.items():
flags = []
@ -34,13 +34,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)

90
resource/sign_textures.py Executable file
View file

@ -0,0 +1,90 @@
#!/usr/bin/env python3
import shutil
import sys
from PIL import Image
MATERIALS = [
'acacia',
'bamboo',
'birch',
'cherry',
'crimson',
'dark_oak',
'jungle',
'mangrove',
'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

@ -6,21 +6,47 @@ use std::{
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use indexmap::IndexSet;
use regex::RegexSet;
use serde::{Deserialize, 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
/// 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(1);
/// 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(1);
/// 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(0);
/// Coordinate pair of a generated tile
///
@ -80,6 +106,13 @@ pub struct ProcessedRegion {
pub chunks: ChunkArray<Option<Box<ProcessedChunk>>>,
}
/// Data structure for storing entity data between processing and collection steps
#[derive(Debug, Default, Serialize, Deserialize)]
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
@ -108,13 +141,21 @@ pub struct Config {
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,
/// Sign text filter patterns
pub sign_patterns: RegexSet,
}
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,
@ -123,17 +164,40 @@ impl Config {
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 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")?;
Ok(Config {
num_threads,
region_dir,
level_dat_path,
output_dir: args.output_dir.clone(),
processed_dir,
metadata_path,
entities_dir,
entities_path_final,
viewer_info_path,
viewer_entities_path,
sign_patterns,
})
}
/// 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()),
)?)
}
/// Constructs the path to an input region file
@ -148,6 +212,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 {

View file

@ -0,0 +1,123 @@
//! 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<'a> TileMerger for EntityCollector<'a> {
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<'a> TileCollector for EntityCollector<'a> {
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(), storage::Format::Json) {
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, storage::Format::Json).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

@ -3,7 +3,14 @@
use anyhow::{Context, Result};
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,
},
};
/// Minimum and maximum X and Z tile coordinates for a mipmap level
#[derive(Debug, Serialize)]
@ -37,6 +44,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 +58,15 @@ struct Metadata<'t> {
mipmaps: Vec<Mipmap<'t>>,
/// Initial spawn point for new players
spawn: Spawn,
/// Enabled MinedMap features
features: Features,
}
/// 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
@ -109,21 +132,65 @@ 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
}
/// Generates [Entities] data from collected entity lists
fn entities(&self) -> Result<Entities> {
let data: ProcessedEntities =
storage::read_file(&self.config.entities_path_final, storage::Format::Json)
.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),
})
.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,
};
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,9 +1,12 @@
//! 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;
@ -19,6 +22,8 @@ use region_processor::RegionProcessor;
use tile_mipmapper::TileMipmapper;
use tile_renderer::TileRenderer;
use self::entity_collector::EntityCollector;
/// MinedMap version number
const VERSION: &str = git_version!(
args = ["--abbrev=7", "--match=v*", "--dirty=-modified"],
@ -27,7 +32,11 @@ const VERSION: &str = git_version!(
/// Command line arguments for minedmap CLI
#[derive(Debug, Parser)]
#[command(about, version = VERSION.strip_prefix("v").unwrap())]
#[command(
about,
version = VERSION.strip_prefix("v").unwrap(),
max_term_width = 100,
)]
pub struct Args {
/// Number of parallel threads to use for processing
///
@ -38,6 +47,18 @@ pub struct Args {
/// Enable verbose messages
#[arg(short, long)]
pub verbose: bool,
/// 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>,
/// Minecraft save directory
pub input_dir: PathBuf,
/// MinedMap data directory
@ -55,7 +76,7 @@ fn setup_threads(num_threads: usize) -> Result<()> {
/// 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 {
@ -75,6 +96,7 @@ pub fn cli() -> 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()?;
Ok(())

View file

@ -64,20 +64,28 @@ 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,
/// True if entity output file needs to be updated
entities_needed: bool,
/// Processed region intermediate data
processed_region: ProcessedRegion,
/// Lightmap intermediate data
lightmap: image::GrayAlphaImage,
/// Processed entity intermediate data
entities: ProcessedEntities,
/// True if any unknown block or biome types were encountered during processing
has_unknown: bool,
}
@ -93,14 +101,20 @@ impl<'a> SingleRegionProcessor<'a> {
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 entities_needed = Some(input_timestamp) > entities_timestamp;
let processed_region = ProcessedRegion::default();
let lightmap = image::GrayAlphaImage::new(N, N);
let entities = ProcessedEntities::default();
Ok(SingleRegionProcessor {
block_types: &processor.block_types,
@ -109,13 +123,17 @@ impl<'a> SingleRegionProcessor<'a> {
input_path,
output_path,
lightmap_path,
entities_path,
input_timestamp,
output_timestamp,
lightmap_timestamp,
entities_timestamp,
output_needed,
lightmap_needed,
entities_needed,
processed_region,
lightmap,
entities,
has_unknown: false,
})
}
@ -145,9 +163,10 @@ impl<'a> SingleRegionProcessor<'a> {
return Ok(());
}
storage::write(
storage::write_file(
&self.output_path,
&self.processed_region,
storage::Format::Bincode,
REGION_FILE_META_VERSION,
self.input_timestamp,
)
@ -173,23 +192,41 @@ impl<'a> SingleRegionProcessor<'a> {
)
}
/// Saves processed entity data
///
/// The timestamp is the time of the last modification of the input region data.
fn save_entities(&mut self) -> Result<()> {
if !self.entities_needed {
return Ok(());
}
self.entities.block_entities.sort_unstable();
storage::write_file(
&self.entities_path,
&self.entities,
storage::Format::Json,
ENTITIES_FILE_META_VERSION,
self.input_timestamp,
)
}
/// Processes a single chunk
fn process_chunk(&mut self, chunk_coords: ChunkCoords, data: world::de::Chunk) -> Result<()> {
let (chunk, has_unknown) =
world::chunk::Chunk::new(&data, self.block_types, self.biome_types)
.with_context(|| format!("Failed to decode chunk {:?}", chunk_coords))?;
self.has_unknown |= has_unknown;
let Some(layer::LayerData {
if self.output_needed || self.lightmap_needed {
if 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 {
return Ok(());
};
{
if self.output_needed {
self.processed_region.chunks[chunk_coords] = Some(Box::new(ProcessedChunk {
blocks,
@ -202,6 +239,18 @@ impl<'a> SingleRegionProcessor<'a> {
let chunk_lightmap = Self::render_chunk_lightmap(block_light);
overlay_chunk(&mut self.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,
)
})?;
self.entities.block_entities.append(&mut block_entities);
}
Ok(())
}
@ -214,7 +263,7 @@ impl<'a> SingleRegionProcessor<'a> {
/// Processes the region
fn run(mut self) -> Result<RegionProcessorStatus> {
if !self.output_needed && !self.lightmap_needed {
if !self.output_needed && !self.lightmap_needed && !self.entities_needed {
debug!(
"Skipping unchanged region r.{}.{}.mca",
self.coords.x, self.coords.z
@ -228,7 +277,10 @@ impl<'a> SingleRegionProcessor<'a> {
);
if let Err(err) = self.process_chunks() {
if self.output_timestamp.is_some() && self.lightmap_timestamp.is_some() {
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
@ -245,6 +297,7 @@ impl<'a> SingleRegionProcessor<'a> {
self.save_region()?;
self.save_lightmap()?;
self.save_entities()?;
Ok(if self.has_unknown {
RegionProcessorStatus::OkWithUnknown
@ -312,6 +365,7 @@ impl<'a> RegionProcessor<'a> {
pub fn run(self) -> Result<Vec<TileCoords>> {
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...");

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<'a, P: image::PixelWithColorType> TileMerger for MapMerger<'a, 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, image::ImageFormat::Png)
.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<'a> TileCollector for TileMipmapper<'a> {
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

@ -105,7 +105,8 @@ 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, storage::Format::Bincode)
.context("Failed to load processed region data")
})
.await
.cloned()

View file

@ -14,43 +14,70 @@ use serde::{de::DeserializeOwned, Serialize};
use super::fs;
/// Serializes data and stores it in a file
/// Storage format
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Format {
/// Encode as Bincode
///
/// A timestamp is stored in an assiciated metadata file.
pub fn write<T: Serialize>(
path: &Path,
value: &T,
version: fs::FileMetaVersion,
timestamp: SystemTime,
) -> Result<()> {
fs::create_with_timestamp(path, version, timestamp, |file| {
let data = bincode::serialize(value)?;
/// Bincode is more efficient than JSON, but cannot handle many of
/// serde's features like flatten, conditional skipping, ...
Bincode,
/// Encode as JSON
Json,
}
/// Serializes data and writes it to a writer
pub fn write<W: Write, T: Serialize>(writer: &mut W, value: &T, format: Format) -> Result<()> {
let data = match format {
Format::Bincode => bincode::serialize(value)?,
Format::Json => serde_json::to_vec(value)?,
};
let len = u32::try_from(data.len())?;
let compressed = zstd::bulk::compress(&data, 1)?;
drop(data);
file.write_all(&len.to_be_bytes())?;
file.write_all(&compressed)?;
writer.write_all(&len.to_be_bytes())?;
writer.write_all(&compressed)?;
Ok(())
})
}
/// Reads data from a file and deserializes it
pub fn read<T: DeserializeOwned>(path: &Path) -> Result<T> {
(|| -> Result<T> {
let mut file = File::open(path)?;
/// Serializes data and stores it in a file
///
/// A timestamp is stored in an assiciated metadata file.
pub fn write_file<T: Serialize>(
path: &Path,
value: &T,
format: Format,
version: fs::FileMetaVersion,
timestamp: SystemTime,
) -> Result<()> {
fs::create_with_timestamp(path, version, timestamp, |file| write(file, value, format))
}
/// Reads data from a reader and deserializes it
pub fn read<R: Read, T: DeserializeOwned>(reader: &mut R, format: Format) -> Result<T> {
let mut len_buf = [0u8; 4];
file.read_exact(&mut len_buf)?;
reader.read_exact(&mut len_buf)?;
let len = usize::try_from(u32::from_be_bytes(len_buf))?;
let mut compressed = vec![];
file.read_to_end(&mut compressed)?;
reader.read_to_end(&mut compressed)?;
let data = zstd::bulk::decompress(&compressed, len)?;
drop(compressed);
Ok(bincode::deserialize(&data)?)
let value = match format {
Format::Bincode => bincode::deserialize(&data)?,
Format::Json => serde_json::from_slice(&data)?,
};
Ok(value)
}
/// Reads data from a file and deserializes it
pub fn read_file<T: DeserializeOwned>(path: &Path, format: Format) -> Result<T> {
(|| -> Result<T> {
let mut file = File::open(path)?;
read(&mut file, format)
})()
.with_context(|| format!("Failed to read file {}", path.display()))
}

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

@ -0,0 +1,103 @@
//! Processing of block entity data
use minedmap_resource::{BlockFlag, BlockType};
use serde::{Deserialize, Serialize};
use super::{
de,
sign::{BlockEntitySignExt, SignText},
};
/// Kind of sign block
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[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, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
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>) -> Sign {
let (front_text, back_text) = sign.text();
let front_text = front_text.decode();
let back_text = back_text.decode();
Sign {
kind,
material,
front_text,
back_text,
}
}
}
/// Data for different kinds of [BlockEntity]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum BlockEntityData {
/// A sign block
Sign(Sign),
}
/// A processed block entity
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
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>) -> 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()));
Some(BlockEntity {
x: entity.x,
y: entity.y,
z: entity.z,
data,
})
}
}

View file

@ -10,16 +10,16 @@ use std::{
use anyhow::{bail, Context, Result};
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,6 +50,16 @@ pub enum Chunk<'a> {
Empty,
}
/// 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>,
}
impl<'a> Chunk<'a> {
/// Creates a new [Chunk] from a deserialized [de::Chunk]
pub fn new(
@ -59,14 +69,27 @@ impl<'a> Chunk<'a> {
) -> 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,
},
has_unknown,
))
}
/// [Chunk::new] implementation for Minecraft v1.18+ chunks
@ -75,7 +98,7 @@ impl<'a> Chunk<'a> {
sections: &'a Vec<de::SectionV1_18>,
block_types: &'a BlockTypes,
biome_types: &'a BiomeTypes,
) -> Result<(Self, bool)> {
) -> Result<(ChunkInner<'a>, bool)> {
let mut section_map = BTreeMap::new();
let mut has_unknown = false;
@ -117,7 +140,7 @@ impl<'a> Chunk<'a> {
};
}
let chunk = Chunk::V1_18 { section_map };
let chunk = ChunkInner::V1_18 { section_map };
Ok((chunk, has_unknown))
}
@ -127,7 +150,7 @@ impl<'a> Chunk<'a> {
level: &'a de::LevelV0,
block_types: &'a BlockTypes,
biome_types: &'a BiomeTypes,
) -> Result<(Self, bool)> {
) -> Result<(ChunkInner<'a>, bool)> {
let mut section_map_v1_13 = BTreeMap::new();
let mut section_map_v0 = BTreeMap::new();
let mut has_unknown = false;
@ -167,12 +190,12 @@ impl<'a> Chunk<'a> {
let biomes = BiomesV0::new(level.biomes.as_ref(), biome_types);
let chunk = match (section_map_v1_13.is_empty(), section_map_v0.is_empty()) {
(true, true) => Chunk::Empty,
(false, true) => Chunk::V1_13 {
(true, true) => ChunkInner::Empty,
(false, true) => ChunkInner::V1_13 {
section_map: section_map_v1_13,
biomes: biomes?,
},
(true, false) => Chunk::V0 {
(true, false) => ChunkInner::V0 {
section_map: section_map_v0,
biomes: biomes?,
},
@ -186,11 +209,11 @@ impl<'a> Chunk<'a> {
/// 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,
}
}
@ -198,28 +221,82 @@ 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))
})
.collect::<Result<_>>()?;
Ok(entities.into_iter().flatten().collect())
}
}
/// Reference to block, biome and block light data of a section
@ -252,26 +329,26 @@ impl<'a, T> SectionIterTrait<'a> for T where
/// Inner data structure of [SectionIter]
#[derive(Debug, Clone)]
enum SectionIterInner<'a> {
/// Iterator over sections of [Chunk::V1_18]
/// 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 [Chunk::V1_13]
/// 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 [Chunk::V0]
/// 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 ([Chunk::Empty])
/// Empty iterator over an unpopulated chunk ([ChunkInner::Empty])
Empty,
}

View file

@ -2,6 +2,8 @@
use serde::Deserialize;
use super::json_text::JSONText;
/// 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<JSONText>,
/// 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: JSONText,
/// Line 2 of the sign text
text2: JSONText,
/// Line 3 of the sign text
text3: JSONText,
/// Line 4 of the sign text
text4: JSONText,
/// 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")]

177
src/world/json_text.rs Normal file
View file

@ -0,0 +1,177 @@
//! Newtype and helper methods for handling Minecraft Raw JSON Text
use std::{collections::VecDeque, fmt::Display, sync::Arc};
use serde::{Deserialize, Serialize};
/// A span of formatted text
///
/// A [JSONText] 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, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct FormattedText {
#[serde(default)]
/// Text content
pub text: String,
/// Text color
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<Arc<String>>,
/// 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_else(|| parent.color.clone()),
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, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
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 [JSONText]
///
/// A [JSONText] can contain various different JSON types.
#[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 JSON Text
#[derive(Debug, Deserialize)]
pub struct JSONText(pub String);
impl JSONText {
/// Deserializes a [JSONText] into a [DeserializedText]
pub fn deserialize(&self) -> DeserializedText {
serde_json::from_str(&self.0).unwrap_or_default()
}
}

View file

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use super::chunk::{Chunk, SectionIterItem};
use crate::{
resource::{Biome, BlockFlag, BlockType},
resource::{Biome, BlockColor, BlockFlag},
types::*,
};
@ -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
@ -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,7 +96,7 @@ impl<'a> LayerEntry<'a> {
};
if self.is_empty() {
*self.block = Some(block_type);
*self.block = Some(block_type.block_color);
if let Some(biome) = section.biomes.biome_at(section.y, coords)? {
let (biome_index, _) = biome_list.insert_full(*biome);
*self.biome = NonZeroU16::new(
@ -107,7 +107,7 @@ impl<'a> LayerEntry<'a> {
}
}
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 json_text;
pub mod layer;
pub mod section;
pub mod sign;

View file

@ -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
@ -146,7 +146,7 @@ impl<'a> SectionV1_13<'a> {
}
impl<'a> Section for SectionV1_13<'a> {
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<BlockType>> {
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<&BlockType>> {
let index = self.palette_index_at(coords);
Ok(*self
.palette
@ -189,7 +189,7 @@ impl<'a> SectionV0<'a> {
}
impl<'a> Section for SectionV0<'a> {
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<BlockType>> {
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<&BlockType>> {
let offset = coords.offset();
let block = self.blocks[offset] as u8;

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

@ -0,0 +1,132 @@
//! Processing of sign text
use std::{fmt::Display, sync::Arc};
use serde::{Deserialize, Serialize};
use super::{
de,
json_text::{FormattedText, FormattedTextList, JSONText},
};
/// 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 JSONText>,
/// Sign color
///
/// Defaults to "black".
pub color: Option<&'a str>,
}
impl<'a> RawSignText<'a> {
/// Decodes the [RawSignText] into a [SignText]
pub fn decode(&self) -> SignText {
let color = self.color.map(|c| Arc::new(c.to_owned()));
let parent = FormattedText {
color,
..Default::default()
};
SignText(
self.messages
.iter()
.map(|message| message.deserialize().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, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
/// 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());
}
}

View file

@ -17,6 +17,61 @@ 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) {
L.TileLayer.prototype.initialize.call(this, '', {
@ -98,39 +153,230 @@ const parseHash = function () {
return args;
}
const colors = {
black: '#000000',
dark_blue: '#0000AA',
dark_green: '#00AA00',
dark_aqua: '#00AAAA',
dark_red: '#AA0000',
dark_purple: '#AA00AA',
gold: '#FFAA00',
gray: '#AAAAAA',
dark_gray: '#555555',
blue: '#5555FF',
green: '#55FF55',
aqua: '#55FFFF',
red: '#FF5555',
light_purple: '#FF55FF',
yellow: '#FFFF55',
white: '#FFFFFF',
};
function formatSignLine(line) {
const el = document.createElement('span');
el.style.whiteSpace = 'pre';
for (const span of line) {
const child = document.createElement('span');
child.textContent = span.text;
const color = colors[span.color ?? 'black'] || colors['black'];
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 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 +384,25 @@ window.createMap = function () {
],
});
const mapLayer = new MinedMapLayer(mipmaps, 'map');
const lightLayer = new MinedMapLayer(mipmaps, 'light');
const overlayMaps = {};
const mapLayer = new MinedMapLayer(mipmaps, 'map');
mapLayer.addTo(map);
if (light)
const lightLayer = new MinedMapLayer(mipmaps, 'light');
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 +414,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 +460,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: 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

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