Merge pull request #40 from neocturne/sign-marker
Display markers for signs
|
@ -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
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
30
README.md
|
@ -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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -11,7 +11,7 @@ if len(sys.argv) != 4:
|
|||
sys.exit('Usage: extract.py <blocks.json> <asset directory> <colors.json>')
|
||||
|
||||
def mean_color(texture):
|
||||
path = os.path.join(sys.argv[2], texture + '.png')
|
||||
path = os.path.join(sys.argv[2], 'assets/minecraft/textures/block', texture + '.png')
|
||||
im = Image.open(path)
|
||||
|
||||
data = im.convert('RGBA').getdata()
|
||||
|
@ -45,20 +45,30 @@ for name, info in blocks.items():
|
|||
'birch': False,
|
||||
'spruce': False,
|
||||
'water': False,
|
||||
'wall_sign': False,
|
||||
'sign_material': None,
|
||||
}
|
||||
|
||||
if info is None:
|
||||
continue
|
||||
|
||||
color = mean_color(info.get('texture', name))
|
||||
texture = info.get('texture', name)
|
||||
|
||||
color = None
|
||||
if texture:
|
||||
color = mean_color(texture)
|
||||
if color:
|
||||
output[id]['color'] = color
|
||||
output[id]['opaque'] = True
|
||||
output[id]['grass'] = info.get('grass', False)
|
||||
output[id]['foliage'] = info.get('foliage', False)
|
||||
output[id]['birch'] = info.get('birch', False)
|
||||
output[id]['spruce'] = info.get('spruce', False)
|
||||
output[id]['water'] = info.get('water', False)
|
||||
|
||||
output[id]['grass'] = info.get('grass', False)
|
||||
output[id]['foliage'] = info.get('foliage', False)
|
||||
output[id]['birch'] = info.get('birch', False)
|
||||
output[id]['spruce'] = info.get('spruce', False)
|
||||
output[id]['water'] = info.get('water', False)
|
||||
output[id]['wall_sign'] = info.get('wall_sign', False)
|
||||
|
||||
output[id]['sign_material'] = info.get('sign_material')
|
||||
|
||||
with open(sys.argv[3], 'w') as f:
|
||||
json.dump(output, f)
|
||||
|
|
|
@ -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
|
@ -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)
|
|
@ -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 {
|
||||
|
|
123
src/core/entity_collector.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, ®ions).run()?;
|
||||
let tiles = TileMipmapper::new(&config, ®ions).run()?;
|
||||
EntityCollector::new(&config, ®ions).run()?;
|
||||
MetadataWriter::new(&config, &tiles).run()?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -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,34 +192,64 @@ 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 {
|
||||
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 {
|
||||
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))?
|
||||
{
|
||||
if self.output_needed {
|
||||
self.processed_region.chunks[chunk_coords] = Some(Box::new(ProcessedChunk {
|
||||
blocks,
|
||||
biomes,
|
||||
depths,
|
||||
}));
|
||||
}
|
||||
|
||||
if self.lightmap_needed {
|
||||
let chunk_lightmap = Self::render_chunk_lightmap(block_light);
|
||||
overlay_chunk(&mut self.lightmap, &chunk_lightmap, chunk_coords);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.lightmap_needed {
|
||||
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
|
@ -0,0 +1,107 @@
|
|||
//! A trait for recursively processing tiles
|
||||
//!
|
||||
//! Used for mipmap generation and collecting entity data
|
||||
|
||||
use std::sync::mpsc;
|
||||
|
||||
use anyhow::Result;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
/// Helper to determine if no further mipmap levels are needed
|
||||
///
|
||||
/// If all tile coordinates are -1 or 0, further mipmap levels will not
|
||||
/// decrease the number of tiles and mipmap generated is considered finished.
|
||||
fn done(tiles: &TileCoordMap) -> bool {
|
||||
tiles
|
||||
.0
|
||||
.iter()
|
||||
.all(|(z, xs)| (-1..=0).contains(z) && xs.iter().all(|x| (-1..=0).contains(x)))
|
||||
}
|
||||
|
||||
/// Derives the map of populated tile coordinates for the next mipmap level
|
||||
fn map_coords(tiles: &TileCoordMap) -> TileCoordMap {
|
||||
let mut ret = TileCoordMap::default();
|
||||
|
||||
for (&z, xs) in &tiles.0 {
|
||||
for &x in xs {
|
||||
let xt = x >> 1;
|
||||
let zt = z >> 1;
|
||||
|
||||
ret.0.entry(zt).or_default().insert(xt);
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
/// Trait to implement for collecting tiles recursively
|
||||
pub trait TileCollector: Sync {
|
||||
/// Return value of [TileCollector::collect_one]
|
||||
type CollectOutput: Send;
|
||||
|
||||
/// List of level 0 tiles
|
||||
fn tiles(&self) -> &[TileCoords];
|
||||
|
||||
/// Called at the beginning of each level of processing
|
||||
fn prepare(&self, level: usize) -> Result<()>;
|
||||
|
||||
/// Called at the end of each level of processing
|
||||
fn finish(
|
||||
&self,
|
||||
level: usize,
|
||||
outputs: impl Iterator<Item = Self::CollectOutput>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Called for each tile coordinate of the level that is currently being generated
|
||||
fn collect_one(
|
||||
&self,
|
||||
level: usize,
|
||||
coords: TileCoords,
|
||||
prev: &TileCoordMap,
|
||||
) -> Result<Self::CollectOutput>;
|
||||
|
||||
/// Collects tiles recursively
|
||||
fn collect_tiles(&self) -> Result<Vec<TileCoordMap>> {
|
||||
let mut tile_stack = {
|
||||
let mut tile_map = TileCoordMap::default();
|
||||
|
||||
for &TileCoords { x, z } in self.tiles() {
|
||||
tile_map.0.entry(z).or_default().insert(x);
|
||||
}
|
||||
|
||||
vec![tile_map]
|
||||
};
|
||||
|
||||
loop {
|
||||
let level = tile_stack.len();
|
||||
let prev = &tile_stack[level - 1];
|
||||
if done(prev) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.prepare(level)?;
|
||||
|
||||
let next = map_coords(prev);
|
||||
|
||||
let (send, recv) = mpsc::channel();
|
||||
|
||||
next.0
|
||||
.par_iter()
|
||||
.flat_map(|(&z, xs)| xs.par_iter().map(move |&x| TileCoords { x, z }))
|
||||
.try_for_each(|coords| {
|
||||
let output = self.collect_one(level, coords, prev)?;
|
||||
send.send(output).unwrap();
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
|
||||
drop(send);
|
||||
self.finish(level, recv.into_iter())?;
|
||||
|
||||
tile_stack.push(next);
|
||||
}
|
||||
|
||||
Ok(tile_stack)
|
||||
}
|
||||
}
|
97
src/core/tile_merger.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
//! Mipmap-style merging of tiles
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::BufWriter,
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::warn;
|
||||
|
||||
use super::common::*;
|
||||
use crate::io::fs;
|
||||
|
||||
/// [TileMerger::merge_tiles] return
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Stat {
|
||||
/// None of the input files were found
|
||||
NotFound,
|
||||
/// The output file is up-to-date
|
||||
Skipped,
|
||||
/// The output file is regenerated
|
||||
Regenerate,
|
||||
}
|
||||
|
||||
/// A source file for the [TileMerger]
|
||||
///
|
||||
/// The tuple elements are X and Z coordinate offsets in the range [0, 1],
|
||||
/// the file path and the time of last change of the input.
|
||||
pub type Source = ((i32, i32), PathBuf, SystemTime);
|
||||
|
||||
/// Reusable trait for mipmap-style tile merging with change tracking
|
||||
pub trait TileMerger {
|
||||
/// [fs::FileMetaVersion] of input and output files
|
||||
///
|
||||
/// The version in the file metadata on disk must match the returned
|
||||
/// version for the a to be considered up-to-date.
|
||||
fn file_meta_version(&self) -> fs::FileMetaVersion;
|
||||
|
||||
/// Returns the paths of input and output files
|
||||
fn tile_path(&self, level: usize, coords: TileCoords) -> PathBuf;
|
||||
|
||||
/// Can be used to log the processing status
|
||||
fn log(&self, _output_path: &Path, _stat: Stat) {}
|
||||
|
||||
/// Handles the actual merging of source files
|
||||
fn write_tile(&self, file: &mut BufWriter<File>, sources: &[Source]) -> Result<()>;
|
||||
|
||||
/// Generates a tile at given coordinates and mipmap level
|
||||
fn merge_tiles(&self, level: usize, coords: TileCoords, prev: &TileCoordMap) -> Result<Stat> {
|
||||
let version = self.file_meta_version();
|
||||
let output_path = self.tile_path(level, coords);
|
||||
let output_timestamp = fs::read_timestamp(&output_path, version);
|
||||
|
||||
let sources: Vec<_> = [(0, 0), (0, 1), (1, 0), (1, 1)]
|
||||
.into_iter()
|
||||
.filter_map(|(dx, dz)| {
|
||||
let source_coords = TileCoords {
|
||||
x: 2 * coords.x + dx,
|
||||
z: 2 * coords.z + dz,
|
||||
};
|
||||
if !prev.contains(source_coords) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let source_path = self.tile_path(level - 1, source_coords);
|
||||
let timestamp = match fs::modified_timestamp(&source_path) {
|
||||
Ok(timestamp) => timestamp,
|
||||
Err(err) => {
|
||||
warn!("{:?}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(((dx, dz), source_path, timestamp))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let Some(input_timestamp) = sources.iter().map(|(_, _, ts)| *ts).max() else {
|
||||
self.log(&output_path, Stat::NotFound);
|
||||
return Ok(Stat::NotFound);
|
||||
};
|
||||
|
||||
if Some(input_timestamp) <= output_timestamp {
|
||||
self.log(&output_path, Stat::Skipped);
|
||||
return Ok(Stat::Skipped);
|
||||
}
|
||||
|
||||
self.log(&output_path, Stat::Regenerate);
|
||||
|
||||
fs::create_with_timestamp(&output_path, version, input_timestamp, |file| {
|
||||
self.write_tile(file, &sources)
|
||||
})?;
|
||||
|
||||
Ok(Stat::Regenerate)
|
||||
}
|
||||
}
|
|
@ -1,14 +1,154 @@
|
|||
//! The [TileMipmapper]
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -14,43 +14,70 @@ use serde::{de::DeserializeOwned, Serialize};
|
|||
|
||||
use super::fs;
|
||||
|
||||
/// Storage format
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Format {
|
||||
/// Encode as Bincode
|
||||
///
|
||||
/// Bincode is more efficient than JSON, but cannot handle many of
|
||||
/// serde's features like flatten, conditional skipping, ...
|
||||
Bincode,
|
||||
/// Encode as JSON
|
||||
Json,
|
||||
}
|
||||
|
||||
/// Serializes data and writes it to a writer
|
||||
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);
|
||||
|
||||
writer.write_all(&len.to_be_bytes())?;
|
||||
writer.write_all(&compressed)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Serializes data and stores it in a file
|
||||
///
|
||||
/// A timestamp is stored in an assiciated metadata file.
|
||||
pub fn write<T: Serialize>(
|
||||
pub fn write_file<T: Serialize>(
|
||||
path: &Path,
|
||||
value: &T,
|
||||
format: Format,
|
||||
version: fs::FileMetaVersion,
|
||||
timestamp: SystemTime,
|
||||
) -> Result<()> {
|
||||
fs::create_with_timestamp(path, version, timestamp, |file| {
|
||||
let data = bincode::serialize(value)?;
|
||||
let len = u32::try_from(data.len())?;
|
||||
let compressed = zstd::bulk::compress(&data, 1)?;
|
||||
drop(data);
|
||||
fs::create_with_timestamp(path, version, timestamp, |file| write(file, value, format))
|
||||
}
|
||||
|
||||
file.write_all(&len.to_be_bytes())?;
|
||||
file.write_all(&compressed)?;
|
||||
/// 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];
|
||||
reader.read_exact(&mut len_buf)?;
|
||||
let len = usize::try_from(u32::from_be_bytes(len_buf))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
let mut compressed = vec![];
|
||||
reader.read_to_end(&mut compressed)?;
|
||||
let data = zstd::bulk::decompress(&compressed, len)?;
|
||||
drop(compressed);
|
||||
|
||||
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<T: DeserializeOwned>(path: &Path) -> Result<T> {
|
||||
pub fn read_file<T: DeserializeOwned>(path: &Path, format: Format) -> Result<T> {
|
||||
(|| -> Result<T> {
|
||||
let mut file = File::open(path)?;
|
||||
|
||||
let mut len_buf = [0u8; 4];
|
||||
file.read_exact(&mut len_buf)?;
|
||||
let len = usize::try_from(u32::from_be_bytes(len_buf))?;
|
||||
|
||||
let mut compressed = vec![];
|
||||
file.read_to_end(&mut compressed)?;
|
||||
let data = zstd::bulk::decompress(&compressed, len)?;
|
||||
drop(compressed);
|
||||
|
||||
Ok(bincode::deserialize(&data)?)
|
||||
read(&mut file, format)
|
||||
})()
|
||||
.with_context(|| format!("Failed to read file {}", path.display()))
|
||||
}
|
||||
|
|
103
src/world/block_entity.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
@ -0,0 +1,7 @@
|
|||
# README
|
||||
|
||||
The images in this directory are assets directly taken from Minecraft, or are derived from Minecraft
|
||||
assets. They are copyrighted by Mojang/Microsoft, and are used in accordance with the
|
||||
[Minecraft Usage Guidelines](https://www.minecraft.net/en-us/usage-guidelines).
|
||||
|
||||
|
BIN
viewer/images/bg/acacia_hanging_sign.png
Normal file
After Width: | Height: | Size: 321 B |
BIN
viewer/images/bg/acacia_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 232 B |
BIN
viewer/images/bg/acacia_sign.png
Normal file
After Width: | Height: | Size: 441 B |
BIN
viewer/images/bg/acacia_wall_sign.png
Normal file
After Width: | Height: | Size: 328 B |
BIN
viewer/images/bg/bamboo_hanging_sign.png
Normal file
After Width: | Height: | Size: 384 B |
BIN
viewer/images/bg/bamboo_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 244 B |
BIN
viewer/images/bg/bamboo_sign.png
Normal file
After Width: | Height: | Size: 564 B |
BIN
viewer/images/bg/bamboo_wall_sign.png
Normal file
After Width: | Height: | Size: 459 B |
BIN
viewer/images/bg/birch_hanging_sign.png
Normal file
After Width: | Height: | Size: 363 B |
BIN
viewer/images/bg/birch_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 239 B |
BIN
viewer/images/bg/birch_sign.png
Normal file
After Width: | Height: | Size: 460 B |
BIN
viewer/images/bg/birch_wall_sign.png
Normal file
After Width: | Height: | Size: 341 B |
BIN
viewer/images/bg/cherry_hanging_sign.png
Normal file
After Width: | Height: | Size: 311 B |
BIN
viewer/images/bg/cherry_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 224 B |
BIN
viewer/images/bg/cherry_sign.png
Normal file
After Width: | Height: | Size: 442 B |
BIN
viewer/images/bg/cherry_wall_sign.png
Normal file
After Width: | Height: | Size: 323 B |
BIN
viewer/images/bg/crimson_hanging_sign.png
Normal file
After Width: | Height: | Size: 371 B |
BIN
viewer/images/bg/crimson_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 235 B |
BIN
viewer/images/bg/crimson_sign.png
Normal file
After Width: | Height: | Size: 531 B |
BIN
viewer/images/bg/crimson_wall_sign.png
Normal file
After Width: | Height: | Size: 392 B |
BIN
viewer/images/bg/dark_oak_hanging_sign.png
Normal file
After Width: | Height: | Size: 307 B |
BIN
viewer/images/bg/dark_oak_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 240 B |
BIN
viewer/images/bg/dark_oak_sign.png
Normal file
After Width: | Height: | Size: 416 B |
BIN
viewer/images/bg/dark_oak_wall_sign.png
Normal file
After Width: | Height: | Size: 315 B |
BIN
viewer/images/bg/jungle_hanging_sign.png
Normal file
After Width: | Height: | Size: 412 B |
BIN
viewer/images/bg/jungle_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 242 B |
BIN
viewer/images/bg/jungle_sign.png
Normal file
After Width: | Height: | Size: 451 B |
BIN
viewer/images/bg/jungle_wall_sign.png
Normal file
After Width: | Height: | Size: 308 B |
BIN
viewer/images/bg/mangrove_hanging_sign.png
Normal file
After Width: | Height: | Size: 329 B |
BIN
viewer/images/bg/mangrove_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 224 B |
BIN
viewer/images/bg/mangrove_sign.png
Normal file
After Width: | Height: | Size: 451 B |
BIN
viewer/images/bg/mangrove_wall_sign.png
Normal file
After Width: | Height: | Size: 342 B |
BIN
viewer/images/bg/oak_hanging_sign.png
Normal file
After Width: | Height: | Size: 339 B |
BIN
viewer/images/bg/oak_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 225 B |
BIN
viewer/images/bg/oak_sign.png
Normal file
After Width: | Height: | Size: 440 B |
BIN
viewer/images/bg/oak_wall_sign.png
Normal file
After Width: | Height: | Size: 332 B |
BIN
viewer/images/bg/spruce_hanging_sign.png
Normal file
After Width: | Height: | Size: 345 B |
BIN
viewer/images/bg/spruce_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 236 B |
BIN
viewer/images/bg/spruce_sign.png
Normal file
After Width: | Height: | Size: 421 B |
BIN
viewer/images/bg/spruce_wall_sign.png
Normal file
After Width: | Height: | Size: 316 B |
BIN
viewer/images/bg/warped_hanging_sign.png
Normal file
After Width: | Height: | Size: 374 B |
BIN
viewer/images/bg/warped_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 236 B |
BIN
viewer/images/bg/warped_sign.png
Normal file
After Width: | Height: | Size: 531 B |
BIN
viewer/images/bg/warped_wall_sign.png
Normal file
After Width: | Height: | Size: 394 B |
BIN
viewer/images/icon/acacia_hanging_sign.png
Normal file
After Width: | Height: | Size: 268 B |
BIN
viewer/images/icon/acacia_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 327 B |
BIN
viewer/images/icon/acacia_sign.png
Normal file
After Width: | Height: | Size: 301 B |
BIN
viewer/images/icon/acacia_wall_sign.png
Normal file
After Width: | Height: | Size: 237 B |
BIN
viewer/images/icon/bamboo_hanging_sign.png
Normal file
After Width: | Height: | Size: 391 B |
BIN
viewer/images/icon/bamboo_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 469 B |
BIN
viewer/images/icon/bamboo_sign.png
Normal file
After Width: | Height: | Size: 416 B |
BIN
viewer/images/icon/bamboo_wall_sign.png
Normal file
After Width: | Height: | Size: 348 B |
BIN
viewer/images/icon/birch_hanging_sign.png
Normal file
After Width: | Height: | Size: 273 B |
BIN
viewer/images/icon/birch_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 340 B |
BIN
viewer/images/icon/birch_sign.png
Normal file
After Width: | Height: | Size: 298 B |
BIN
viewer/images/icon/birch_wall_sign.png
Normal file
After Width: | Height: | Size: 235 B |
BIN
viewer/images/icon/cherry_hanging_sign.png
Normal file
After Width: | Height: | Size: 280 B |
BIN
viewer/images/icon/cherry_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 347 B |
BIN
viewer/images/icon/cherry_sign.png
Normal file
After Width: | Height: | Size: 298 B |
BIN
viewer/images/icon/cherry_wall_sign.png
Normal file
After Width: | Height: | Size: 229 B |
BIN
viewer/images/icon/crimson_hanging_sign.png
Normal file
After Width: | Height: | Size: 284 B |
BIN
viewer/images/icon/crimson_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 341 B |
BIN
viewer/images/icon/crimson_sign.png
Normal file
After Width: | Height: | Size: 340 B |
BIN
viewer/images/icon/crimson_wall_sign.png
Normal file
After Width: | Height: | Size: 262 B |
BIN
viewer/images/icon/dark_oak_hanging_sign.png
Normal file
After Width: | Height: | Size: 276 B |
BIN
viewer/images/icon/dark_oak_hanging_wall_sign.png
Normal file
After Width: | Height: | Size: 332 B |
BIN
viewer/images/icon/dark_oak_sign.png
Normal file
After Width: | Height: | Size: 302 B |
BIN
viewer/images/icon/dark_oak_wall_sign.png
Normal file
After Width: | Height: | Size: 244 B |