Compare commits

...

40 Commits

Author SHA1 Message Date
dependabot[bot]
96b5c21df0 Bump reqwest from 0.12.15 to 0.13.1
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.15 to 0.13.1.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.15...v0.13.1)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-version: 0.13.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 02:47:02 +00:00
Louis Erbkamm
d4af5ce7ef Merge pull request #732 from louis-e/copy-coords
Copy coordinates on right click
2026-02-01 01:09:08 +01:00
louis-e
382ab19a0d Remove keyframe animation 2026-02-01 01:08:51 +01:00
louis-e
38678deefc Copy coordinates on right click 2026-02-01 01:04:37 +01:00
Louis Erbkamm
674a2d9656 Merge pull request #731 from louis-e/add-boundaries
Add boundaries for city ground blocks
2026-02-01 00:23:16 +01:00
louis-e
c4ad3dd61a Fix cargo fmt 2026-02-01 00:21:11 +01:00
louis-e
e42bc121fa Address code review feedback 2026-02-01 00:18:14 +01:00
louis-e
19da1fe55d Address code review feedback 2026-02-01 00:02:11 +01:00
louis-e
663394f3b5 Remove debug files 2026-01-31 23:55:03 +01:00
louis-e
bef7bd3965 Fix wrong clipping of boundaries 2026-01-31 23:54:24 +01:00
Louis Erbkamm
d8e1b29146 Merge branch 'main' into add-boundaries 2026-01-31 18:56:46 +01:00
louis-e
a9f53b2cd6 Add boundaries for city ground blocks 2026-01-31 18:56:25 +01:00
Louis Erbkamm
516b9ecf33 Merge pull request #730 from louis-e/save-faster
Improve saving
2026-01-31 18:42:14 +01:00
louis-e
babe610cca Remove unused vars 2026-01-31 18:21:36 +01:00
louis-e
20922a3be6 Improve saving 2026-01-31 18:18:20 +01:00
Louis Erbkamm
f1dc3b8ffb Merge pull request #675 from wielandb/entities
Add recycling containers (and ability to add entities during world generation)
2026-01-27 18:23:14 +01:00
Louis Erbkamm
602767c1d1 Remove unreachable wheat seeds in amenities 2026-01-27 17:41:49 +01:00
Louis Erbkamm
96e6d9e129 Fix Bedrock LevelDB lock error 2026-01-27 17:32:37 +01:00
Louis Erbkamm
c722ea689f Fix typos in interior comments 2026-01-27 17:31:51 +01:00
Louis Erbkamm
f473e980a2 Merge pull request #662 from amir16yp/enable_structures
set generate_features to 1 in level.dat nbt template
2026-01-27 16:46:43 +01:00
Wieland
1901c21049 Merge branch 'wielandb-modified' into entities 2026-01-26 00:07:30 +01:00
Wieland
bc41838671 Resolve merge conflicts 2026-01-25 23:57:20 +01:00
Wieland
11de6cfd85 Resolve merge conflict in wkt.parser.js 2026-01-25 23:45:27 +01:00
Wieland
92f629fc96 Merge pull request #2 from wielandb/buildings-enhancements
[to modified] Buildings enhancements
2026-01-25 23:37:18 +01:00
Wieland
880d86971d Refactor abandoned building checks and comments
Replaces map_or with is_some_and for abandoned tag check in generate_buildings for clarity. Also updates a doc comment to a regular comment in buildings_interior.rs for consistency.
2026-01-25 23:31:04 +01:00
Wieland
1421247ea4 run cargo fmt 2026-01-25 23:24:59 +01:00
Wieland
1b21dec366 Merge origin/main into buildings-enhancements 2026-01-25 23:20:18 +01:00
Wieland
c9a9d55f76 Merge pull request #1 from wielandb/trees
[to modified] Change what trees are generated based on tag content
2026-01-25 23:10:40 +01:00
Wieland
53846a7b5a cargo fmt check 2026-01-25 22:58:45 +01:00
Wieland
0593615909 Merge branch 'wielandb-modified' into trees 2026-01-25 22:52:29 +01:00
Wieland
ceec7cc190 Use roof:orientation if available 2025-12-17 14:17:22 +01:00
Wieland
d876f5ce60 Corrections on abandoned building interiors 2025-12-17 14:02:15 +01:00
Wieland
d3a416754d Fix min_level and level interaction and make special interior for abandoned buildings 2025-12-17 00:39:26 +01:00
Wieland
fc6c2a255f First steps for generic abandoned buildings 2025-12-16 20:44:42 +01:00
Wieland
3b70694167 Make single-item recycling containers show a sign of what is thrown away there 2025-12-15 17:38:16 +01:00
Wieland
e5f0b1050a Add generation of recycling containers 2025-12-15 10:46:20 +01:00
Wieland
a0fd0c12e2 Update from linting suggestions 2025-12-14 09:08:30 +01:00
Wieland
9b87e3538a Change what trees are generated based on tag content 2025-12-14 08:30:32 +01:00
Wieland
46959365df Add ability to add entities 2025-12-13 22:50:16 +01:00
Amir Y. Perehodnik
f57d14b200 set generate_features to 1 in level.dat nbt 2025-12-07 18:47:09 +02:00
29 changed files with 2336 additions and 548 deletions

425
Cargo.lock generated
View File

@@ -206,8 +206,9 @@ dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
"rayon",
"reqwest",
"reqwest 0.13.1",
"rfd",
"rusty-leveldb",
"semver",
"serde",
"serde_json",
@@ -462,6 +463,28 @@ dependencies = [
"arrayvec",
]
[[package]]
name = "aws-lc-rs"
version = "1.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
]
[[package]]
name = "base64"
version = "0.21.7"
@@ -805,10 +828,11 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.18"
version = "1.2.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
@@ -912,6 +936,15 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "cmake"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
dependencies = [
"cc",
]
[[package]]
name = "color_quant"
version = "1.1.0"
@@ -1026,7 +1059,7 @@ dependencies = [
"bitflags 2.6.0",
"core-foundation 0.10.0",
"core-graphics-types",
"foreign-types 0.5.0",
"foreign-types",
"libc",
]
@@ -1602,6 +1635,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "flate2"
version = "1.1.2"
@@ -1624,15 +1663,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -1640,7 +1670,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared 0.3.1",
"foreign-types-shared",
]
[[package]]
@@ -1654,12 +1684,6 @@ dependencies = [
"syn 2.0.95",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
@@ -1685,6 +1709,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "funty"
version = "2.0.0"
@@ -1976,9 +2006,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
]
[[package]]
@@ -2290,19 +2322,21 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
[[package]]
name = "hyper"
version = "1.5.2"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-util",
"futures-core",
"h2",
"http",
"http-body",
"httparse",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
"want",
@@ -2325,39 +2359,30 @@ dependencies = [
"tower-service",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.10"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.5.8",
"socket2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@@ -2701,6 +2726,16 @@ version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
[[package]]
name = "iri-string"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "is-docker"
version = "0.2.0"
@@ -3023,6 +3058,12 @@ dependencies = [
"imgref",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lz4-java-wrc"
version = "0.2.0"
@@ -3155,23 +3196,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "native-tls"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nbtx"
version = "0.1.0"
@@ -3646,49 +3670,11 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "openssl"
version = "0.10.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
dependencies = [
"bitflags 2.6.0",
"cfg-if 1.0.0",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "option-ext"
@@ -4174,6 +4160,62 @@ dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.9",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [
"aws-lc-rs",
"bytes",
"getrandom 0.3.3",
"lru-slab",
"rand 0.9.1",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.9",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.60.2",
]
[[package]]
name = "quote"
version = "1.0.38"
@@ -4441,9 +4483,44 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.12.15"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
]
[[package]]
name = "reqwest"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -4457,33 +4534,28 @@ dependencies = [
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile",
"quinn",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-util",
"tokio-rustls",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"windows-registry",
]
[[package]]
@@ -4592,6 +4664,12 @@ dependencies = [
"serde_json",
]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -4629,10 +4707,11 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.20"
version = "0.23.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b"
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
dependencies = [
"aws-lc-rs",
"once_cell",
"rustls-pki-types",
"rustls-webpki",
@@ -4641,26 +4720,61 @@ dependencies = [
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
name = "rustls-native-certs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.10.1"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.0",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.102.8"
version = "0.103.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@@ -4757,12 +4871,12 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "security-framework"
version = "2.11.1"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
dependencies = [
"bitflags 2.6.0",
"core-foundation 0.9.4",
"core-foundation 0.10.0",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -4770,9 +4884,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.13.0"
version = "2.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5"
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
dependencies = [
"core-foundation-sys",
"libc",
@@ -5084,16 +5198,6 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b"
[[package]]
name = "socket2"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.0"
@@ -5113,7 +5217,7 @@ dependencies = [
"bytemuck",
"cfg_aliases",
"core-graphics",
"foreign-types 0.5.0",
"foreign-types",
"js-sys",
"log",
"objc2 0.5.2",
@@ -5404,7 +5508,7 @@ dependencies = [
"percent-encoding",
"plist",
"raw-window-handle",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
"serde_repr",
@@ -5795,7 +5899,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.6.0",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
@@ -5811,16 +5915,6 @@ dependencies = [
"syn 2.0.95",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.1"
@@ -5954,6 +6048,24 @@ dependencies = [
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags 2.6.0",
"bytes",
"futures-util",
"http",
"http-body",
"iri-string",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
@@ -6216,12 +6328,6 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vek"
version = "0.17.2"
@@ -6515,6 +6621,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.0"

View File

@@ -15,7 +15,7 @@ overflow-checks = true
[features]
default = ["gui"]
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek"]
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek", "rusty-leveldb"]
[build-dependencies]
tauri-build = {version = "2", optional = true}
@@ -40,7 +40,7 @@ once_cell = "1.21.3"
rand = "0.8.5"
rand_chacha = "0.3"
rayon = "1.10.0"
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
reqwest = { version = "0.13.1", features = ["blocking", "json"] }
rfd = { version = "0.16.0", optional = true }
semver = "1.0.27"
serde = { version = "1.0", features = ["derive"] }
@@ -54,6 +54,7 @@ bedrockrs_shared = { git = "https://github.com/bedrock-crustaceans/bedrock-rs",
nbtx = { git = "https://github.com/bedrock-crustaceans/nbtx", optional = true }
vek = { version = "0.17", optional = true }
zip = { version = "0.6", default-features = false, features = ["deflate"], optional = true }
rusty-leveldb = { version = "3", optional = true }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61.1", features = ["Win32_System_Console"] }

View File

Binary file not shown.

View File

@@ -578,6 +578,11 @@ pub fn to_bedrock_block_with_properties(
return convert_stairs(java_name, props_map);
}
// Handle barrel facing direction
if java_name == "barrel" {
return convert_barrel(java_name, props_map);
}
// Handle slabs with type property (top/bottom/double)
if java_name.ends_with("_slab") {
return convert_slab(java_name, props_map);
@@ -650,6 +655,46 @@ fn convert_stairs(
}
}
/// Convert Java barrel to Bedrock format with facing direction.
fn convert_barrel(
java_name: &str,
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
) -> BedrockBlock {
let mut states = HashMap::new();
if let Some(props) = props {
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
let facing_direction = match facing.as_str() {
"down" => 0,
"up" => 1,
"north" => 2,
"south" => 3,
"west" => 4,
"east" => 5,
_ => 1,
};
states.insert(
"facing_direction".to_string(),
BedrockBlockStateValue::Int(facing_direction),
);
}
}
if !states.contains_key("facing_direction") {
states.insert(
"facing_direction".to_string(),
BedrockBlockStateValue::Int(1),
);
}
states.insert("open_bit".to_string(), BedrockBlockStateValue::Bool(false));
BedrockBlock {
name: format!("minecraft:{java_name}"),
states,
}
}
/// Convert Java slab block to Bedrock format with proper type.
fn convert_slab(
java_name: &str,

View File

@@ -266,7 +266,17 @@ impl Block {
185 => "quartz_stairs",
186 => "polished_andesite_stairs",
187 => "nether_brick_stairs",
188 => "fern",
188 => "barrel",
189 => "fern",
190 => "cobweb",
191 => "chiseled_bookshelf",
192 => "chiseled_bookshelf",
193 => "chiseled_bookshelf",
194 => "chiseled_bookshelf",
195 => "chipped_anvil",
196 => "damaged_anvil",
197 => "large_fern",
198 => "large_fern",
_ => panic!("Invalid id"),
}
}
@@ -464,6 +474,37 @@ impl Block {
map.insert("half".to_string(), Value::String("top".to_string()));
map
})),
191 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("facing".to_string(), Value::String("north".to_string()));
map
})),
192 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("facing".to_string(), Value::String("east".to_string()));
map
})),
193 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("facing".to_string(), Value::String("south".to_string()));
map
})),
194 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("facing".to_string(), Value::String("west".to_string()));
map
})),
197 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("half".to_string(), Value::String("lower".to_string()));
map
})),
198 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("half".to_string(), Value::String("upper".to_string()));
map
})),
_ => None,
}
}
@@ -698,7 +739,19 @@ pub const SMOOTH_SANDSTONE_STAIRS: Block = Block::new(184);
pub const QUARTZ_STAIRS: Block = Block::new(185);
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
pub const NETHER_BRICK_STAIRS: Block = Block::new(187);
pub const FERN: Block = Block::new(188);
pub const BARREL: Block = Block::new(188);
pub const FERN: Block = Block::new(189);
pub const COBWEB: Block = Block::new(190);
pub const CHISELLED_BOOKSHELF_NORTH: Block = Block::new(191);
pub const CHISELLED_BOOKSHELF_EAST: Block = Block::new(192);
pub const CHISELLED_BOOKSHELF_SOUTH: Block = Block::new(193);
pub const CHISELLED_BOOKSHELF_WEST: Block = Block::new(194);
// Backwards-compatible alias (defaults to north-facing)
pub const CHISELLED_BOOKSHELF: Block = CHISELLED_BOOKSHELF_NORTH;
pub const CHIPPED_ANVIL: Block = Block::new(195);
pub const DAMAGED_ANVIL: Block = Block::new(196);
pub const LARGE_FERN_LOWER: Block = Block::new(197);
pub const LARGE_FERN_UPPER: Block = Block::new(198);
/// Maps a block to its corresponding stair variant
#[inline]

View File

@@ -14,6 +14,9 @@ pub fn clip_way_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<Process
return Vec::new();
}
// Get way ID for ID generation
let way_id = nodes.first().map(|n| n.id).unwrap_or(0);
let is_closed = is_closed_polygon(nodes);
if !is_closed {
@@ -54,12 +57,13 @@ pub fn clip_way_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<Process
}
let polygon = insert_bbox_corners(polygon, min_x, min_z, max_x, max_z);
let polygon = remove_consecutive_duplicates(polygon);
if polygon.len() < 3 {
return Vec::new();
}
let way_id = nodes.first().map(|n| n.id).unwrap_or(0);
assign_node_ids_preserving_endpoints(nodes, polygon, way_id)
}
@@ -496,12 +500,15 @@ fn find_bbox_intersections(
/// Returns which bbox edge a point lies on: 0=bottom, 1=right, 2=top, 3=left, -1=interior.
fn get_bbox_edge(point: (f64, f64), min_x: f64, min_z: f64, max_x: f64, max_z: f64) -> i32 {
let eps = 0.5;
// Use a slightly larger epsilon to handle floating-point errors from Sutherland-Hodgman.
// Points should be clamped to bbox before this function is called, so any point
// at or very near the boundary should be considered ON that edge.
let eps = 1.0;
let on_left = (point.0 - min_x).abs() < eps;
let on_right = (point.0 - max_x).abs() < eps;
let on_bottom = (point.1 - min_z).abs() < eps;
let on_top = (point.1 - max_z).abs() < eps;
let on_left = (point.0 - min_x).abs() <= eps;
let on_right = (point.0 - max_x).abs() <= eps;
let on_bottom = (point.1 - min_z).abs() <= eps;
let on_top = (point.1 - max_z).abs() <= eps;
// Handle corners (assign to edge in counter-clockwise order)
if on_bottom && on_left {
@@ -556,20 +563,21 @@ fn get_corners_between_edges(
let ccw_dist = ((edge2 - edge1 + 4) % 4) as usize;
let cw_dist = ((edge1 - edge2 + 4) % 4) as usize;
// Opposite edges: don't insert corners
if ccw_dist == 2 && cw_dist == 2 {
return Vec::new();
}
// For opposite edges (distance = 2), we need to pick a direction.
// Use counter-clockwise by default to ensure corners are inserted.
// This prevents diagonal lines when polygon spans opposite bbox edges.
let mut result = Vec::new();
if ccw_dist <= cw_dist {
// Go counter-clockwise
let mut current = edge1;
for _ in 0..ccw_dist {
result.push(corners[current as usize]);
current = (current + 1) % 4;
}
} else {
// Go clockwise
let mut current = edge1;
for _ in 0..cw_dist {
current = (current + 4 - 1) % 4;
@@ -580,6 +588,12 @@ fn get_corners_between_edges(
result
}
/// Checks if two points are approximately equal (within epsilon tolerance).
fn points_approx_equal(p1: (f64, f64), p2: (f64, f64)) -> bool {
let eps = 1.0;
(p1.0 - p2.0).abs() <= eps && (p1.1 - p2.1).abs() <= eps
}
/// Inserts bbox corners where polygon transitions between different bbox edges.
fn insert_bbox_corners(
polygon: Vec<(f64, f64)>,
@@ -604,8 +618,13 @@ fn insert_bbox_corners(
let edge2 = get_bbox_edge(next, min_x, min_z, max_x, max_z);
if edge1 >= 0 && edge2 >= 0 && edge1 != edge2 {
for corner in get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z) {
result.push(corner);
let corners = get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z);
// Filter out corners that match the current point or the next point
for corner in corners {
if !points_approx_equal(corner, current) && !points_approx_equal(corner, next) {
result.push(corner);
}
}
}
}

View File

@@ -85,9 +85,14 @@ pub fn generate_world_with_options(
// Uses a memory-efficient bitmap (~1 bit per coordinate) instead of a HashSet (~24 bytes per coordinate)
let building_footprints = flood_fill_cache.collect_building_footprints(&elements, &xzbbox);
// Partition elements: separate boundary elements for deferred processing
// This avoids cloning by moving elements instead of copying them
let (boundary_elements, other_elements): (Vec<_>, Vec<_>) = elements
.into_iter()
.partition(|element| element.tags().contains_key("boundary"));
// Process data
let elements_count: usize = elements.len();
let mut elements = elements; // Take ownership for consuming
let elements_count: usize = other_elements.len() + boundary_elements.len();
let process_pb: ProgressBar = ProgressBar::new(elements_count as u64);
process_pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} elements ({eta}) {msg}")
@@ -98,8 +103,8 @@ pub fn generate_world_with_options(
let mut current_progress_prcs: f64 = 25.0;
let mut last_emitted_progress: f64 = current_progress_prcs;
// Process elements by draining in insertion order
for element in elements.drain(..) {
// Process non-boundary elements first
for element in other_elements.into_iter() {
process_pb.inc(1);
current_progress_prcs += progress_increment_prcs;
if (current_progress_prcs - last_emitted_progress).abs() > 0.25 {
@@ -265,6 +270,33 @@ pub fn generate_world_with_options(
process_pb.finish();
// Process deferred boundary elements after all other elements
// This ensures boundaries only fill empty areas, they won't overwrite
// any ground blocks set by landuse, leisure, natural, etc.
for element in boundary_elements.into_iter() {
match &element {
ProcessedElement::Way(way) => {
boundaries::generate_boundary(&mut editor, way, args, &flood_fill_cache);
// Clean up cache entry for consistency with other element processing
flood_fill_cache.remove_way(way.id);
}
ProcessedElement::Relation(rel) => {
boundaries::generate_boundary_from_relation(
&mut editor,
rel,
args,
&flood_fill_cache,
&xzbbox,
);
// Clean up cache entries for consistency with other element processing
let way_ids: Vec<u64> = rel.members.iter().map(|m| m.way.id).collect();
flood_fill_cache.remove_relation_ways(&way_ids);
}
_ => {}
}
// Element is dropped here, freeing its memory immediately
}
// Drop remaining caches
drop(highway_connectivity);
drop(flood_fill_cache);

View File

@@ -7,7 +7,9 @@ use crate::floodfill::flood_fill_area; // Needed for inline amenity flood fills
use crate::floodfill_cache::FloodFillCache;
use crate::osm_parser::ProcessedElement;
use crate::world_editor::WorldEditor;
use rand::Rng;
use fastnbt::Value;
use rand::{seq::SliceRandom, Rng};
use std::collections::{HashMap, HashSet};
pub fn generate_amenities(
editor: &mut WorldEditor,
@@ -34,6 +36,49 @@ pub fn generate_amenities(
.map(|n: &crate::osm_parser::ProcessedNode| XZPoint::new(n.x, n.z))
.next();
match amenity_type.as_str() {
"recycling" => {
let is_container = element
.tags()
.get("recycling_type")
.is_some_and(|value| value == "container");
if !is_container {
return;
}
if let Some(pt) = first_node {
let mut rng = rand::thread_rng();
let loot_pool = build_recycling_loot_pool(element.tags());
let items = build_recycling_items(&loot_pool, &mut rng);
let properties = Value::Compound(recycling_barrel_properties());
let barrel_block = BlockWithProperties::new(BARREL, Some(properties));
let absolute_y = editor.get_absolute_y(pt.x, 1, pt.z);
editor.set_block_entity_with_items(
barrel_block,
pt.x,
1,
pt.z,
"minecraft:barrel",
items,
);
if let Some(category) = single_loot_category(&loot_pool) {
if let Some(display_item) =
build_display_item_for_category(category, &mut rng)
{
place_item_frame_on_random_side(
editor,
pt.x,
absolute_y,
pt.z,
display_item,
);
}
}
}
}
"waste_disposal" | "waste_basket" => {
// Place a cauldron for waste disposal or waste basket
if let Some(pt) = first_node {
@@ -263,3 +308,420 @@ pub fn generate_amenities(
}
}
}
#[derive(Clone, Copy)]
enum RecyclingLootKind {
GlassBottle,
Paper,
GlassBlock,
GlassPane,
LeatherArmor,
EmptyBucket,
LeatherBoots,
ScrapMetal,
GreenWaste,
}
#[derive(Clone, Copy)]
enum LeatherPiece {
Helmet,
Chestplate,
Leggings,
Boots,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum LootCategory {
GlassBottle,
Paper,
Glass,
Leather,
EmptyBucket,
ScrapMetal,
GreenWaste,
}
fn recycling_barrel_properties() -> HashMap<String, Value> {
let mut props = HashMap::new();
props.insert("facing".to_string(), Value::String("up".to_string()));
props
}
fn build_recycling_loot_pool(tags: &HashMap<String, String>) -> Vec<RecyclingLootKind> {
let mut loot_pool: Vec<RecyclingLootKind> = Vec::new();
if tag_enabled(tags, "recycling:glass_bottles") {
loot_pool.push(RecyclingLootKind::GlassBottle);
}
if tag_enabled(tags, "recycling:paper") {
loot_pool.push(RecyclingLootKind::Paper);
}
if tag_enabled(tags, "recycling:glass") {
loot_pool.push(RecyclingLootKind::GlassBlock);
loot_pool.push(RecyclingLootKind::GlassPane);
}
if tag_enabled(tags, "recycling:clothes") {
loot_pool.push(RecyclingLootKind::LeatherArmor);
}
if tag_enabled(tags, "recycling:cans") {
loot_pool.push(RecyclingLootKind::EmptyBucket);
}
if tag_enabled(tags, "recycling:shoes") {
loot_pool.push(RecyclingLootKind::LeatherBoots);
}
if tag_enabled(tags, "recycling:scrap_metal") {
loot_pool.push(RecyclingLootKind::ScrapMetal);
}
if tag_enabled(tags, "recycling:green_waste") {
loot_pool.push(RecyclingLootKind::GreenWaste);
}
loot_pool
}
fn build_recycling_items(
loot_pool: &[RecyclingLootKind],
rng: &mut impl Rng,
) -> Vec<HashMap<String, Value>> {
if loot_pool.is_empty() {
return Vec::new();
}
let mut items = Vec::new();
for slot in 0..27 {
if rng.gen_bool(0.2) {
let kind = loot_pool[rng.gen_range(0..loot_pool.len())];
if let Some(item) = build_item_for_kind(kind, slot as i8, rng) {
items.push(item);
}
}
}
items
}
fn kind_to_category(kind: RecyclingLootKind) -> LootCategory {
match kind {
RecyclingLootKind::GlassBottle => LootCategory::GlassBottle,
RecyclingLootKind::Paper => LootCategory::Paper,
RecyclingLootKind::GlassBlock | RecyclingLootKind::GlassPane => LootCategory::Glass,
RecyclingLootKind::LeatherArmor | RecyclingLootKind::LeatherBoots => LootCategory::Leather,
RecyclingLootKind::EmptyBucket => LootCategory::EmptyBucket,
RecyclingLootKind::ScrapMetal => LootCategory::ScrapMetal,
RecyclingLootKind::GreenWaste => LootCategory::GreenWaste,
}
}
fn single_loot_category(loot_pool: &[RecyclingLootKind]) -> Option<LootCategory> {
let mut categories: HashSet<LootCategory> = HashSet::new();
for kind in loot_pool {
categories.insert(kind_to_category(*kind));
if categories.len() > 1 {
return None;
}
}
categories.iter().next().copied()
}
fn build_display_item_for_category(
category: LootCategory,
rng: &mut impl Rng,
) -> Option<HashMap<String, Value>> {
match category {
LootCategory::GlassBottle => Some(make_display_item("minecraft:glass_bottle", 1)),
LootCategory::Paper => Some(make_display_item("minecraft:paper", rng.gen_range(1..=4))),
LootCategory::Glass => Some(make_display_item("minecraft:glass", 1)),
LootCategory::Leather => Some(build_leather_display_item(rng)),
LootCategory::EmptyBucket => Some(make_display_item("minecraft:bucket", 1)),
LootCategory::ScrapMetal => {
let metals = [
"minecraft:copper_ingot",
"minecraft:iron_ingot",
"minecraft:gold_ingot",
];
let metal = metals.choose(rng)?;
Some(make_display_item(metal, rng.gen_range(1..=2)))
}
LootCategory::GreenWaste => {
let options = [
"minecraft:oak_sapling",
"minecraft:birch_sapling",
"minecraft:tall_grass",
"minecraft:sweet_berries",
"minecraft:wheat_seeds",
];
let choice = options.choose(rng)?;
Some(make_display_item(choice, rng.gen_range(1..=3)))
}
}
}
fn place_item_frame_on_random_side(
editor: &mut WorldEditor,
x: i32,
barrel_absolute_y: i32,
z: i32,
item: HashMap<String, Value>,
) {
let mut rng = rand::thread_rng();
let mut directions = [
((0, 0, -1), 2), // North
((0, 0, 1), 3), // South
((-1, 0, 0), 4), // West
((1, 0, 0), 5), // East
];
directions.shuffle(&mut rng);
let (min_x, min_z) = editor.get_min_coords();
let (max_x, max_z) = editor.get_max_coords();
let ((dx, _dy, dz), facing) = directions
.into_iter()
.find(|((dx, _dy, dz), _)| {
let target_x = x + dx;
let target_z = z + dz;
target_x >= min_x && target_x <= max_x && target_z >= min_z && target_z <= max_z
})
.unwrap_or(((0, 0, 1), 3)); // Fallback south if all directions are out of bounds
let target_x = x + dx;
let target_y = barrel_absolute_y;
let target_z = z + dz;
let ground_y = editor.get_absolute_y(target_x, 0, target_z);
let mut extra = HashMap::new();
extra.insert("Facing".to_string(), Value::Byte(facing)); // 2=north, 3=south, 4=west, 5=east
extra.insert("ItemRotation".to_string(), Value::Byte(0));
extra.insert("Item".to_string(), Value::Compound(item));
extra.insert("ItemDropChance".to_string(), Value::Float(1.0));
extra.insert(
"block_pos".to_string(),
Value::List(vec![
Value::Int(target_x),
Value::Int(target_y),
Value::Int(target_z),
]),
);
extra.insert("TileX".to_string(), Value::Int(target_x));
extra.insert("TileY".to_string(), Value::Int(target_y));
extra.insert("TileZ".to_string(), Value::Int(target_z));
extra.insert("Fixed".to_string(), Value::Byte(1));
let relative_y = target_y - ground_y;
editor.add_entity(
"minecraft:item_frame",
target_x,
relative_y,
target_z,
Some(extra),
);
}
fn make_display_item(id: &str, count: i8) -> HashMap<String, Value> {
let mut item = HashMap::new();
item.insert("id".to_string(), Value::String(id.to_string()));
item.insert("Count".to_string(), Value::Byte(count));
item
}
fn build_leather_display_item(rng: &mut impl Rng) -> HashMap<String, Value> {
let mut item = make_display_item("minecraft:leather_chestplate", 1);
let damage = biased_damage(80, rng);
let mut tag = HashMap::new();
tag.insert("Damage".to_string(), Value::Int(damage));
if let Some(color) = maybe_leather_color(rng) {
let mut display = HashMap::new();
display.insert("color".to_string(), Value::Int(color));
tag.insert("display".to_string(), Value::Compound(display));
}
item.insert("tag".to_string(), Value::Compound(tag));
let mut components = HashMap::new();
components.insert("minecraft:damage".to_string(), Value::Int(damage));
item.insert("components".to_string(), Value::Compound(components));
item
}
fn build_item_for_kind(
kind: RecyclingLootKind,
slot: i8,
rng: &mut impl Rng,
) -> Option<HashMap<String, Value>> {
match kind {
RecyclingLootKind::GlassBottle => Some(make_basic_item(
"minecraft:glass_bottle",
slot,
rng.gen_range(1..=4),
)),
RecyclingLootKind::Paper => Some(make_basic_item(
"minecraft:paper",
slot,
rng.gen_range(1..=10),
)),
RecyclingLootKind::GlassBlock => Some(build_glass_item(false, slot, rng)),
RecyclingLootKind::GlassPane => Some(build_glass_item(true, slot, rng)),
RecyclingLootKind::LeatherArmor => {
Some(build_leather_item(random_leather_piece(rng), slot, rng))
}
RecyclingLootKind::EmptyBucket => Some(make_basic_item("minecraft:bucket", slot, 1)),
RecyclingLootKind::LeatherBoots => Some(build_leather_item(LeatherPiece::Boots, slot, rng)),
RecyclingLootKind::ScrapMetal => Some(build_scrap_metal_item(slot, rng)),
RecyclingLootKind::GreenWaste => Some(build_green_waste_item(slot, rng)),
}
}
fn build_scrap_metal_item(slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
let metals = ["copper_ingot", "iron_ingot", "gold_ingot"];
let metal = metals.choose(rng).expect("scrap metal list is non-empty");
let count = rng.gen_range(1..=3);
make_basic_item(&format!("minecraft:{metal}"), slot, count)
}
fn build_green_waste_item(slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
#[allow(clippy::match_same_arms)]
let (id, count) = match rng.gen_range(0..8) {
0 => ("minecraft:tall_grass", rng.gen_range(1..=4)),
1 => ("minecraft:sweet_berries", rng.gen_range(2..=6)),
2 => ("minecraft:oak_sapling", rng.gen_range(1..=2)),
3 => ("minecraft:birch_sapling", rng.gen_range(1..=2)),
4 => ("minecraft:spruce_sapling", rng.gen_range(1..=2)),
5 => ("minecraft:jungle_sapling", rng.gen_range(1..=2)),
6 => ("minecraft:acacia_sapling", rng.gen_range(1..=2)),
_ => ("minecraft:dark_oak_sapling", rng.gen_range(1..=2)),
};
// 25% chance to replace with seeds instead
let id = if rng.gen_bool(0.25) {
match rng.gen_range(0..4) {
0 => "minecraft:wheat_seeds",
1 => "minecraft:pumpkin_seeds",
2 => "minecraft:melon_seeds",
_ => "minecraft:beetroot_seeds",
}
} else {
id
};
make_basic_item(id, slot, count)
}
fn build_glass_item(is_pane: bool, slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
const GLASS_COLORS: &[&str] = &[
"white",
"orange",
"magenta",
"light_blue",
"yellow",
"lime",
"pink",
"gray",
"light_gray",
"cyan",
"purple",
"blue",
"brown",
"green",
"red",
"black",
];
let use_colorless = rng.gen_bool(0.7);
let id = if use_colorless {
if is_pane {
"minecraft:glass_pane".to_string()
} else {
"minecraft:glass".to_string()
}
} else {
let color = GLASS_COLORS
.choose(rng)
.expect("glass color array is non-empty");
if is_pane {
format!("minecraft:{color}_stained_glass_pane")
} else {
format!("minecraft:{color}_stained_glass")
}
};
let count = if is_pane {
rng.gen_range(4..=16)
} else {
rng.gen_range(1..=6)
};
make_basic_item(&id, slot, count)
}
fn build_leather_item(piece: LeatherPiece, slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
let (id, max_damage) = match piece {
LeatherPiece::Helmet => ("minecraft:leather_helmet", 55),
LeatherPiece::Chestplate => ("minecraft:leather_chestplate", 80),
LeatherPiece::Leggings => ("minecraft:leather_leggings", 75),
LeatherPiece::Boots => ("minecraft:leather_boots", 65),
};
let mut item = make_basic_item(id, slot, 1);
let damage = biased_damage(max_damage, rng);
let mut tag = HashMap::new();
tag.insert("Damage".to_string(), Value::Int(damage));
if let Some(color) = maybe_leather_color(rng) {
let mut display = HashMap::new();
display.insert("color".to_string(), Value::Int(color));
tag.insert("display".to_string(), Value::Compound(display));
}
item.insert("tag".to_string(), Value::Compound(tag));
let mut components = HashMap::new();
components.insert("minecraft:damage".to_string(), Value::Int(damage));
item.insert("components".to_string(), Value::Compound(components));
item
}
fn biased_damage(max_damage: i32, rng: &mut impl Rng) -> i32 {
let safe_max = max_damage.max(1);
let upper = safe_max.saturating_sub(1);
let lower = (safe_max / 2).min(upper);
let heavy_wear = rng.gen_range(lower..=upper);
let random_wear = rng.gen_range(0..=upper);
heavy_wear.max(random_wear)
}
fn maybe_leather_color(rng: &mut impl Rng) -> Option<i32> {
if rng.gen_bool(0.3) {
Some(rng.gen_range(0..=0x00FF_FFFF))
} else {
None
}
}
fn random_leather_piece(rng: &mut impl Rng) -> LeatherPiece {
match rng.gen_range(0..4) {
0 => LeatherPiece::Helmet,
1 => LeatherPiece::Chestplate,
2 => LeatherPiece::Leggings,
_ => LeatherPiece::Boots,
}
}
fn make_basic_item(id: &str, slot: i8, count: i8) -> HashMap<String, Value> {
let mut item = HashMap::new();
item.insert("id".to_string(), Value::String(id.to_string()));
item.insert("Slot".to_string(), Value::Byte(slot));
item.insert("Count".to_string(), Value::Byte(count));
item
}
fn tag_enabled(tags: &HashMap<String, String>, key: &str) -> bool {
tags.get(key).is_some_and(|value| value == "yes")
}

View File

@@ -69,7 +69,7 @@ pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement) {
barrier_material = LIGHT_GRAY_CONCRETE;
}
if barrier_mat == "metal" {
barrier_material = STONE_BRICK_WALL; // IRON_BARS
barrier_material = STONE_BRICK_WALL;
}
}
@@ -80,7 +80,8 @@ pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement) {
.get("height")
.and_then(|height: &String| height.parse::<f32>().ok())
.map(|height: f32| height.round() as i32)
.unwrap_or(barrier_height);
.unwrap_or(barrier_height)
.max(2); // Minimum height of 2
// Process nodes to create the barrier wall
for i in 1..way.nodes.len() {

View File

@@ -0,0 +1,127 @@
//! Processing of administrative and urban boundaries.
//!
//! This module handles boundary elements that define urban areas (cities, boroughs, etc.)
//! and sets appropriate ground blocks for them.
//!
//! Boundaries are processed last but only fill empty areas, allowing more specific
//! landuse areas (parks, residential, etc.) to take priority over the general
//! urban ground.
use crate::args::Args;
use crate::block_definitions::*;
use crate::clipping::clip_way_to_bbox;
use crate::coordinate_system::cartesian::XZBBox;
use crate::floodfill_cache::FloodFillCache;
use crate::osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
/// Checks if a boundary element represents an urban area that should have stone ground.
///
/// Returns true for:
/// - `boundary=administrative` with `admin_level >= 8` (city/borough level or smaller)
/// - `boundary=low_emission_zone` (urban traffic zones)
/// - `boundary=limited_traffic_zone` (urban traffic zones)
/// - `boundary=special_economic_zone` (developed industrial/commercial zones)
/// - `boundary=political` (electoral districts, usually urban)
fn is_urban_boundary(tags: &std::collections::HashMap<String, String>) -> bool {
let Some(boundary_value) = tags.get("boundary") else {
return false;
};
match boundary_value.as_str() {
"administrative" => {
// Only consider city-level or smaller (admin_level >= 8)
// admin_level 2 = country, 4 = state, 6 = county, 8 = city/municipality
if let Some(admin_level_str) = tags.get("admin_level") {
if let Ok(admin_level) = admin_level_str.parse::<u8>() {
return admin_level >= 8;
}
}
false
}
// Urban zones that should have stone ground
"low_emission_zone" | "limited_traffic_zone" | "special_economic_zone" | "political" => {
true
}
// Natural/protected areas should keep grass - don't process these
// "national_park" | "protected_area" | "forest" | "forest_compartment" | "aboriginal_lands"
// Statistical/administrative-only boundaries - don't affect ground
// "census" | "statistical" | "postal_code" | "timezone" | "disputed" | "maritime" | etc.
_ => false,
}
}
/// Generate ground blocks for an urban boundary way.
pub fn generate_boundary(
editor: &mut WorldEditor,
element: &ProcessedWay,
args: &Args,
flood_fill_cache: &FloodFillCache,
) {
// Check if this is an urban boundary
if !is_urban_boundary(&element.tags) {
return;
}
// Get the area of the boundary element using cache
let floor_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
// Fill the area with smooth stone as ground block
// Use None, None to only set where no block exists yet - don't overwrite anything
for (x, z) in floor_area {
editor.set_block(SMOOTH_STONE, x, 0, z, None, None);
}
}
/// Generate ground blocks for an urban boundary relation.
pub fn generate_boundary_from_relation(
editor: &mut WorldEditor,
rel: &ProcessedRelation,
args: &Args,
flood_fill_cache: &FloodFillCache,
xzbbox: &XZBBox,
) {
// Check if this is an urban boundary
if !is_urban_boundary(&rel.tags) {
return;
}
// Collect outer ways (unclipped) for merging
let mut outers: Vec<Vec<ProcessedNode>> = rel
.members
.iter()
.filter(|m| m.role == ProcessedMemberRole::Outer)
.map(|m| m.way.nodes.clone())
.collect();
if outers.is_empty() {
return;
}
// Merge way segments into closed rings
super::merge_way_segments(&mut outers);
// Clip each merged ring to bbox and process
for ring in outers {
if ring.len() < 3 {
continue;
}
// Clip the merged ring to bbox
let clipped_nodes = clip_way_to_bbox(&ring, xzbbox);
if clipped_nodes.len() < 3 {
continue;
}
// Create a ProcessedWay for the clipped ring
let clipped_way = ProcessedWay {
id: rel.id,
nodes: clipped_nodes,
tags: rel.tags.clone(),
};
// Generate boundary area from clipped way
generate_boundary(editor, &clipped_way, args, flood_fill_cache);
}
}

View File

@@ -157,7 +157,7 @@ pub fn generate_buildings(
let lev = levels - min_level;
if lev >= 1 {
building_height = multiply_scale(levels * 4 + 2, scale_factor);
building_height = multiply_scale(lev * 4 + 2, scale_factor);
building_height = building_height.max(3);
// Mark as tall building if more than 7 stories
@@ -542,6 +542,20 @@ pub fn generate_buildings(
}
}
// Detect abandoned buildings via explicit tags
let is_abandoned_building = element
.tags
.get("abandoned")
.is_some_and(|value| value == "yes")
|| element.tags.contains_key("abandoned:building");
// Use cobwebs instead of glowstone for abandoned buildings
let ceiling_light_block = if is_abandoned_building {
COBWEB
} else {
GLOWSTONE
};
for (x, z) in floor_area.iter().cloned() {
if processed_points.insert((x, z)) {
// Create foundation columns for the floor area when using terrain
@@ -573,7 +587,7 @@ pub fn generate_buildings(
if x % 5 == 0 && z % 5 == 0 {
// Light fixtures
editor.set_block_absolute(
GLOWSTONE,
ceiling_light_block,
x,
h + abs_terrain_offset,
z,
@@ -593,7 +607,7 @@ pub fn generate_buildings(
}
} else if x % 5 == 0 && z % 5 == 0 {
editor.set_block_absolute(
GLOWSTONE,
ceiling_light_block,
x,
start_y_offset + building_height + abs_terrain_offset,
z,
@@ -648,6 +662,7 @@ pub fn generate_buildings(
args,
element,
abs_terrain_offset,
is_abandoned_building,
);
}
}
@@ -772,6 +787,9 @@ fn generate_roof(
// Set base height for roof to be at least one block above building top
let base_height = start_y_offset + building_height + 1;
// Optional OSM hint for ridge orientation
let roof_orientation = element.tags.get("roof:orientation").map(|s| s.as_str());
match roof_type {
RoofType::Flat => {
// Simple flat roof
@@ -798,8 +816,13 @@ fn generate_roof(
let roof_peak_height = base_height + roof_height_boost;
// Pre-determine orientation and material
let is_wider_than_long = width > length;
let max_distance = if is_wider_than_long {
let width_is_longer = width >= length;
let ridge_runs_along_x = match roof_orientation {
Some(orientation) if orientation.eq_ignore_ascii_case("along") => width_is_longer,
Some(orientation) if orientation.eq_ignore_ascii_case("across") => !width_is_longer,
_ => width_is_longer,
};
let max_distance = if ridge_runs_along_x {
length >> 1
} else {
width >> 1
@@ -819,15 +842,15 @@ fn generate_roof(
// First pass: calculate all roof heights using vectorized operations
for &(x, z) in floor_area {
let distance_to_ridge = if is_wider_than_long {
let distance_to_ridge = if ridge_runs_along_x {
(z - center_z).abs()
} else {
(x - center_x).abs()
};
let roof_height = if distance_to_ridge == 0
&& ((is_wider_than_long && z == center_z)
|| (!is_wider_than_long && x == center_x))
&& ((ridge_runs_along_x && z == center_z)
|| (!ridge_runs_along_x && x == center_x))
{
roof_peak_height
} else {
@@ -859,7 +882,7 @@ fn generate_roof(
for y in base_height..=roof_height {
if y == roof_height && has_lower_neighbor {
// Pre-compute stair direction
let stair_block_with_props = if is_wider_than_long {
let stair_block_with_props = if ridge_runs_along_x {
if z < center_z {
create_stair_with_properties(
stair_block_material,
@@ -919,7 +942,12 @@ fn generate_roof(
// Determine if building is significantly rectangular or more square-shaped
let is_rectangular =
(width as f64 / length as f64 > 1.3) || (length as f64 / width as f64 > 1.3);
let long_axis_is_x = width > length;
let width_is_longer = width >= length;
let ridge_axis_is_x = match roof_orientation {
Some(orientation) if orientation.eq_ignore_ascii_case("along") => width_is_longer,
Some(orientation) if orientation.eq_ignore_ascii_case("across") => !width_is_longer,
_ => width_is_longer,
};
// Make roof taller and more pointy
let roof_peak_height = base_height + if width.max(length) > 20 { 7 } else { 5 };
@@ -939,7 +967,7 @@ fn generate_roof(
for &(x, z) in floor_area {
// Calculate distance to the ridge line
let distance_to_ridge = if long_axis_is_x {
let distance_to_ridge = if ridge_axis_is_x {
// Distance in Z direction for X-axis ridge
(z - center_z).abs()
} else {
@@ -948,7 +976,7 @@ fn generate_roof(
};
// Calculate maximum distance from ridge to edge
let max_distance_from_ridge = if long_axis_is_x {
let max_distance_from_ridge = if ridge_axis_is_x {
(max_z - min_z) / 2
} else {
(max_x - min_x) / 2
@@ -988,7 +1016,7 @@ fn generate_roof(
if has_lower_neighbor {
// Determine stair direction based on ridge orientation and position
let stair_block_material = get_stair_block_for_material(roof_block);
let stair_block_with_props = if long_axis_is_x {
let stair_block_with_props = if ridge_axis_is_x {
// Ridge runs along X, slopes in Z direction
if z < center_z {
create_stair_with_properties(

View File

@@ -1,10 +1,11 @@
use crate::args::Args;
use crate::block_definitions::*;
use crate::deterministic_rng::element_rng;
use crate::element_processing::tree::Tree;
use crate::element_processing::tree::{Tree, TreeType};
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::prelude::SliceRandom;
use rand::Rng;
pub fn generate_landuse(
@@ -58,6 +59,29 @@ pub fn generate_landuse(
let floor_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
let trees_ok_to_generate: Vec<TreeType> = {
let mut trees: Vec<TreeType> = vec![];
if let Some(leaf_type) = element.tags.get("leaf_type") {
match leaf_type.as_str() {
"broadleaved" => {
trees.push(TreeType::Oak);
trees.push(TreeType::Birch);
}
"needleleaved" => trees.push(TreeType::Spruce),
_ => {
trees.push(TreeType::Oak);
trees.push(TreeType::Spruce);
trees.push(TreeType::Birch);
}
}
} else {
trees.push(TreeType::Oak);
trees.push(TreeType::Spruce);
trees.push(TreeType::Birch);
}
trees
};
for (x, z) in floor_area {
// Apply per-block randomness for certain landuse types
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
@@ -134,6 +158,11 @@ pub fn generate_landuse(
Tree::create(editor, (x, 1, z), Some(building_footprints));
} else if random_choice < 35 {
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
} else if random_choice < 37 {
editor.set_block(FERN, x, 1, z, None, None);
} else if random_choice < 41 {
editor.set_block(LARGE_FERN_LOWER, x, 1, z, None, None);
editor.set_block(LARGE_FERN_UPPER, x, 2, z, None, None);
}
}
}
@@ -141,13 +170,22 @@ pub fn generate_landuse(
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
let random_choice: i32 = rng.gen_range(0..30);
if random_choice == 20 {
Tree::create(editor, (x, 1, z), Some(building_footprints));
let tree_type = *trees_ok_to_generate
.choose(&mut rng)
.unwrap_or(&TreeType::Oak);
Tree::create_of_type(
editor,
(x, 1, z),
tree_type,
Some(building_footprints),
);
} else if random_choice == 2 {
let flower_block: Block = match rng.gen_range(1..=5) {
let flower_block: Block = match rng.gen_range(1..=6) {
1 => OAK_LEAVES,
2 => RED_FLOWER,
3 => BLUE_FLOWER,
4 => YELLOW_FLOWER,
5 => FERN,
_ => WHITE_FLOWER,
};
editor.set_block(flower_block, x, 1, z, None, None);
@@ -269,7 +307,7 @@ pub fn generate_landuse(
match rng.gen_range(0..200) {
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
3..=17 => editor.set_block(GRASS, x, 1, z, None, None),
3..=16 => editor.set_block(GRASS, x, 1, z, None, None),
_ => {}
}
}
@@ -285,7 +323,10 @@ pub fn generate_landuse(
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
} else if random_choice < 40 {
editor.set_block(FERN, x, 1, z, None, None);
} else if random_choice < 800 {
} else if random_choice < 65 {
editor.set_block(LARGE_FERN_LOWER, x, 1, z, None, None);
editor.set_block(LARGE_FERN_UPPER, x, 2, z, None, None);
} else if random_choice < 825 {
editor.set_block(GRASS, x, 1, z, None, None);
}
}

View File

@@ -100,14 +100,16 @@ pub fn generate_leisure(
match random_choice {
0..30 => {
// Flowers
let flower_choice = match random_choice {
0..10 => RED_FLOWER,
10..20 => YELLOW_FLOWER,
20..30 => BLUE_FLOWER,
_ => WHITE_FLOWER,
// Plants
let plant_choice = match random_choice {
0..5 => RED_FLOWER,
5..10 => YELLOW_FLOWER,
10..16 => BLUE_FLOWER,
16..22 => WHITE_FLOWER,
22..30 => FERN,
_ => unreachable!(),
};
editor.set_block(flower_choice, x, 1, z, None, None);
editor.set_block(plant_choice, x, 1, z, None, None);
}
30..90 => {
// Grass

View File

@@ -1,5 +1,6 @@
pub mod amenities;
pub mod barriers;
pub mod boundaries;
pub mod bridges;
pub mod buildings;
pub mod doors;
@@ -14,3 +15,105 @@ pub mod tourisms;
pub mod tree;
pub mod water_areas;
pub mod waterways;
use crate::osm_parser::ProcessedNode;
/// Merges way segments that share endpoints into closed rings.
/// Used by water_areas.rs and boundaries.rs for assembling relation members.
pub fn merge_way_segments(rings: &mut Vec<Vec<ProcessedNode>>) {
let mut removed: Vec<usize> = vec![];
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
// Match nodes by ID or proximity (handles synthetic nodes from bbox clipping)
let nodes_match = |a: &ProcessedNode, b: &ProcessedNode| -> bool {
if a.id == b.id {
return true;
}
let dx = (a.x - b.x).abs();
let dz = (a.z - b.z).abs();
dx <= 1 && dz <= 1
};
for i in 0..rings.len() {
for j in 0..rings.len() {
if i == j {
continue;
}
if removed.contains(&i) || removed.contains(&j) {
continue;
}
let x: &Vec<ProcessedNode> = &rings[i];
let y: &Vec<ProcessedNode> = &rings[j];
// Skip empty rings (can happen after clipping)
if x.is_empty() || y.is_empty() {
continue;
}
let x_first = &x[0];
let x_last = x.last().unwrap();
let y_first = &y[0];
let y_last = y.last().unwrap();
// Skip already-closed rings
if nodes_match(x_first, x_last) {
continue;
}
if nodes_match(y_first, y_last) {
continue;
}
if nodes_match(x_first, y_first) {
removed.push(i);
removed.push(j);
let mut x: Vec<ProcessedNode> = x.clone();
x.reverse();
x.extend(y.iter().skip(1).cloned());
merged.push(x);
} else if nodes_match(x_last, y_last) {
removed.push(i);
removed.push(j);
let mut x: Vec<ProcessedNode> = x.clone();
x.extend(y.iter().rev().skip(1).cloned());
merged.push(x);
} else if nodes_match(x_first, y_last) {
removed.push(i);
removed.push(j);
let mut y: Vec<ProcessedNode> = y.clone();
y.extend(x.iter().skip(1).cloned());
merged.push(y);
} else if nodes_match(x_last, y_first) {
removed.push(i);
removed.push(j);
let mut x: Vec<ProcessedNode> = x.clone();
x.extend(y.iter().skip(1).cloned());
merged.push(x);
}
}
}
removed.sort();
for r in removed.iter().rev() {
rings.remove(*r);
}
let merged_len: usize = merged.len();
for m in merged {
rings.push(m);
}
if merged_len > 0 {
merge_way_segments(rings);
}
}

View File

@@ -2,10 +2,11 @@ use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::deterministic_rng::element_rng;
use crate::element_processing::tree::Tree;
use crate::element_processing::tree::{Tree, TreeType};
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::prelude::SliceRandom;
use rand::Rng;
pub fn generate_natural(
@@ -21,7 +22,66 @@ pub fn generate_natural(
let x: i32 = node.x;
let z: i32 = node.z;
Tree::create(editor, (x, 1, z), Some(building_footprints));
let mut trees_ok_to_generate: Vec<TreeType> = vec![];
if let Some(species) = element.tags().get("species") {
if species.contains("Betula") {
trees_ok_to_generate.push(TreeType::Birch);
}
if species.contains("Quercus") {
trees_ok_to_generate.push(TreeType::Oak);
}
if species.contains("Picea") {
trees_ok_to_generate.push(TreeType::Spruce);
}
} else if let Some(genus_wikidata) = element.tags().get("genus:wikidata") {
match genus_wikidata.as_str() {
"Q12004" => trees_ok_to_generate.push(TreeType::Birch),
"Q26782" => trees_ok_to_generate.push(TreeType::Oak),
"Q25243" => trees_ok_to_generate.push(TreeType::Spruce),
_ => {
trees_ok_to_generate.push(TreeType::Oak);
trees_ok_to_generate.push(TreeType::Spruce);
trees_ok_to_generate.push(TreeType::Birch);
}
}
} else if let Some(genus) = element.tags().get("genus") {
match genus.as_str() {
"Betula" => trees_ok_to_generate.push(TreeType::Birch),
"Quercus" => trees_ok_to_generate.push(TreeType::Oak),
"Picea" => trees_ok_to_generate.push(TreeType::Spruce),
_ => trees_ok_to_generate.push(TreeType::Oak),
}
} else if let Some(leaf_type) = element.tags().get("leaf_type") {
match leaf_type.as_str() {
"broadleaved" => {
trees_ok_to_generate.push(TreeType::Oak);
trees_ok_to_generate.push(TreeType::Birch);
}
"needleleaved" => trees_ok_to_generate.push(TreeType::Spruce),
_ => {
trees_ok_to_generate.push(TreeType::Oak);
trees_ok_to_generate.push(TreeType::Spruce);
trees_ok_to_generate.push(TreeType::Birch);
}
}
} else {
trees_ok_to_generate.push(TreeType::Oak);
trees_ok_to_generate.push(TreeType::Spruce);
trees_ok_to_generate.push(TreeType::Birch);
}
if trees_ok_to_generate.is_empty() {
trees_ok_to_generate.push(TreeType::Oak);
trees_ok_to_generate.push(TreeType::Spruce);
trees_ok_to_generate.push(TreeType::Birch);
}
let mut rng = element_rng(element.id());
let tree_type = *trees_ok_to_generate
.choose(&mut rng)
.unwrap_or(&TreeType::Oak);
Tree::create_of_type(editor, (x, 1, z), tree_type, Some(building_footprints));
}
} else {
let mut previous_node: Option<(i32, i32)> = None;
@@ -81,6 +141,29 @@ pub fn generate_natural(
let filled_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
let trees_ok_to_generate: Vec<TreeType> = {
let mut trees: Vec<TreeType> = vec![];
if let Some(leaf_type) = element.tags().get("leaf_type") {
match leaf_type.as_str() {
"broadleaved" => {
trees.push(TreeType::Oak);
trees.push(TreeType::Birch);
}
"needleleaved" => trees.push(TreeType::Spruce),
_ => {
trees.push(TreeType::Oak);
trees.push(TreeType::Spruce);
trees.push(TreeType::Birch);
}
}
} else {
trees.push(TreeType::Oak);
trees.push(TreeType::Spruce);
trees.push(TreeType::Birch);
}
trees
};
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
let mut rng = element_rng(way.id);
@@ -164,7 +247,15 @@ pub fn generate_natural(
}
let random_choice: i32 = rng.gen_range(0..30);
if random_choice == 0 {
Tree::create(editor, (x, 1, z), Some(building_footprints));
let tree_type = *trees_ok_to_generate
.choose(&mut rng)
.unwrap_or(&TreeType::Oak);
Tree::create_of_type(
editor,
(x, 1, z),
tree_type,
Some(building_footprints),
);
} else if random_choice == 1 {
let flower_block = match rng.gen_range(1..=4) {
1 => RED_FLOWER,

View File

@@ -58,7 +58,7 @@ const INTERIOR1_LAYER2: [[char; 23]; 23] = [
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
];
/// Interior layout for building level floors (1nd layer above floor)
/// Interior layout for building level floors (1st layer above floor)
#[rustfmt::skip]
const INTERIOR2_LAYER1: [[char; 23]; 23] = [
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
@@ -114,6 +114,119 @@ const INTERIOR2_LAYER2: [[char; 23]; 23] = [
['P', 'P', ' ', ' ', ' ', 'E', 'B', 'B', 'B', ' ', ' ', 'W', 'B', 'B', 'B', 'B', 'B', 'B', 'B', ' ', 'B', ' ', 'D',],
];
// Generic Abandoned Building Interiors
/// Interior layout for building ground floors (1st layer above floor)
#[rustfmt::skip]
const ABANDONED_INTERIOR1_LAYER1: [[char; 23]; 23] = [
['1', 'U', ' ', 'W', 'C', ' ', ' ', ' ', 'S', 'S', 'W', 'b', 'T', 'T', 'd', 'W', '7', '8', ' ', ' ', ' ', ' ', 'W',],
['2', ' ', ' ', 'W', 'F', ' ', ' ', ' ', 'U', 'U', 'W', 'b', 'T', 'T', 'd', 'W', '7', '8', ' ', ' ', ' ', 'B', 'W',],
[' ', ' ', ' ', 'W', 'F', ' ', ' ', ' ', ' ', ' ', 'W', 'b', 'T', 'T', 'd', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
['W', 'W', 'D', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'M', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'c', 'c', 'c', ' ', ' ', 'J', 'W', ' ', ' ', ' ', 'd', 'W', 'W', 'W',],
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', 'T', 'S', 'S', 'T', ' ', ' ', 'W', 'S', 'S', ' ', 'd', 'W', 'W', 'W',],
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'T', 'T', 'T', 'T', ' ', ' ', 'W', 'U', 'U', ' ', 'd', 'W', ' ', ' ',],
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', 'T', 'T', 'T', 'T', ' ', 'B', 'W', ' ', ' ', ' ', 'd', 'W', ' ', ' ',],
['L', ' ', 'M', 'L', 'W', 'W', ' ', ' ', 'W', 'J', 'U', 'U', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
['c', 'c', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
[' ', '6', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
['U', '5', ' ', 'W', ' ', ' ', 'W', 'C', 'F', 'F', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', 'W', 'M', ' ', 'b', 'W', ' ', ' ', 'W',],
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'b', 'W', 'J', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'U', ' ', ' ', 'W', 'B', ' ', 'D',],
['J', ' ', ' ', 'C', 'a', 'a', 'W', 'L', 'F', ' ', 'W', 'F', ' ', 'W', 'L', 'W', '7', '8', ' ', 'W', 'B', ' ', 'W',],
['B', ' ', ' ', 'd', 'W', 'W', 'W', 'W', 'W', ' ', 'W', 'M', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'C', ' ', 'W',],
['B', ' ', ' ', 'd', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'C', ' ', ' ', 'W', 'W', 'c', 'c', 'c', 'c', 'W', 'D', 'W',],
['W', 'W', 'D', 'W', 'C', ' ', ' ', ' ', 'W', 'W', 'W', 'b', 'T', 'T', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
];
/// Interior layout for building ground floors (2nd layer above floor)
#[rustfmt::skip]
const ABANDONED_INTERIOR1_LAYER2: [[char; 23]; 23] = [
[' ', 'P', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'P', 'P', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'B', 'W',],
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', 'B', 'B', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', 'P', ' ', 'B', 'W', ' ', ' ',],
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'B', 'W', ' ', ' ',],
[' ', ' ', ' ', ' ', 'W', 'W', ' ', ' ', 'W', ' ', 'P', 'P', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
['B', 'B', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
[' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
['P', ' ', ' ', 'W', ' ', ' ', 'W', 'N', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'B', 'W', ' ', ' ', 'W',],
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'C', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'P', ' ', ' ', 'W', 'B', ' ', 'D',],
[' ', ' ', ' ', ' ', 'B', 'B', 'W', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'P', 'W', ' ', ' ', ' ', 'W', 'B', ' ', 'W',],
['B', ' ', ' ', 'B', 'W', 'W', 'W', 'W', 'W', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W',],
['B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'N', ' ', ' ', 'W', 'W', 'B', 'B', 'B', 'B', 'W', 'D', 'W',],
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
];
/// Interior layout for building level floors (1st layer above floor)
#[rustfmt::skip]
const ABANDONED_INTERIOR2_LAYER1: [[char; 23]; 23] = [
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
['U', ' ', ' ', ' ', ' ', ' ', 'C', 'W', 'L', ' ', ' ', 'L', 'W', 'M', 'M', 'W', ' ', ' ', ' ', ' ', ' ', 'L', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'S', 'S', 'S', ' ', 'W',],
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'Q', 'C', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'J', ' ', 'U', 'U', 'U', ' ', 'D',],
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', 'T', 'T', 'W', ' ', ' ', ' ', ' ', ' ', 'U', 'W', ' ', 'L', 'W',],
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'T', 'J', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', 'L', ' ', 'W',],
['J', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
['W', 'W', 'W', 'W', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
[' ', 'M', 'c', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', 'd', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', 'd', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'U', ' ', ' ', ' ', 'D', ' ', ' ', 'F', 'F', 'W', 'M', 'M', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'U', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', 'C', ' ', ' ', 'W', ' ', ' ', 'W',],
['C', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'L', ' ', ' ', 'W', 'W', 'D', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
['L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'U', 'U', ' ', 'Q', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'U', 'U', ' ', 'Q', 'b', ' ', 'U', 'U', 'B', ' ', ' ', ' ', ' ', ' ', 'W',],
['S', 'S', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'Q', 'b', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'd', ' ', 'W',],
['U', 'U', ' ', ' ', ' ', 'L', 'a', 'a', 'a', ' ', ' ', 'Q', 'B', 'a', 'a', 'a', 'a', 'a', 'a', ' ', 'd', 'D', 'W',],
];
/// Interior layout for building level floors (2nd layer above floor)
#[rustfmt::skip]
const ABANDONED_INTERIOR2_LAYER2: [[char; 23]; 23] = [
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
['P', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', 'O', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'O', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', 'P', 'P', ' ', 'D',],
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'P', 'W', ' ', 'P', 'W',],
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
['W', 'W', 'W', 'W', 'W', 'O', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
[' ', ' ', 'c', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', 'd', 'W', 'O', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', 'd', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'P', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'O', ' ', ' ', 'W', 'W', 'D', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
['O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'P', 'P', ' ', 'Q', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'P', 'P', ' ', 'Q', 'b', ' ', 'P', 'P', 'c', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'Q', 'b', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'd', ' ', 'W',],
['P', 'P', ' ', ' ', ' ', 'O', 'a', 'a', 'a', ' ', ' ', 'Q', 'b', 'a', 'a', 'a', 'a', 'a', 'a', ' ', 'd', ' ', 'D',],
];
/// Maps interior layout characters to actual block types for different floor layers
#[inline(always)]
pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option<Block> {
@@ -145,12 +258,19 @@ pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option
Some(DARK_OAK_DOOR_LOWER)
}
}
'J' => Some(NOTE_BLOCK), // Note block
'G' => Some(GLOWSTONE), // Glowstone
'N' => Some(BREWING_STAND), // Brewing Stand
'T' => Some(WHITE_CARPET), // White Carpet
'E' => Some(OAK_LEAVES), // Oak Leaves
_ => None, // Default case for unknown characters
'J' => Some(NOTE_BLOCK), // Note block
'G' => Some(GLOWSTONE), // Glowstone
'N' => Some(BREWING_STAND), // Brewing Stand
'T' => Some(WHITE_CARPET), // White Carpet
'E' => Some(OAK_LEAVES), // Oak Leaves
'O' => Some(COBWEB), // Cobweb
'a' => Some(CHISELLED_BOOKSHELF_NORTH), // Chiseled Bookshelf
'b' => Some(CHISELLED_BOOKSHELF_EAST), // Chiseled Bookshelf East
'c' => Some(CHISELLED_BOOKSHELF_SOUTH), // Chiseled Bookshelf South
'd' => Some(CHISELLED_BOOKSHELF_WEST), // Chiseled Bookshelf West
'M' => Some(DAMAGED_ANVIL), // Damaged Anvil
'Q' => Some(SCAFFOLDING), // Scaffolding
_ => None, // Default case for unknown characters
}
}
@@ -170,6 +290,7 @@ pub fn generate_building_interior(
args: &crate::args::Args,
element: &crate::osm_parser::ProcessedWay,
abs_terrain_offset: i32,
is_abandoned_building: bool,
) {
// Skip interior generation for very small buildings
let width = max_x - min_x + 1;
@@ -214,7 +335,13 @@ pub fn generate_building_interior(
};
// Choose the appropriate interior pattern based on floor number
let (layer1, layer2) = if floor_index == 0 {
let (layer1, layer2) = if is_abandoned_building {
if floor_index == 0 {
(&ABANDONED_INTERIOR1_LAYER1, &ABANDONED_INTERIOR1_LAYER2)
} else {
(&ABANDONED_INTERIOR2_LAYER1, &ABANDONED_INTERIOR2_LAYER2)
}
} else if floor_index == 0 {
// Ground floor uses INTERIOR1 patterns
(&INTERIOR1_LAYER1, &INTERIOR1_LAYER2)
} else {

View File

@@ -92,6 +92,7 @@ fn round(editor: &mut WorldEditor, material: Block, (x, y, z): Coord, block_patt
}
}
#[derive(Clone, Copy)]
pub enum TreeType {
Oak,
Spruce,
@@ -120,6 +121,27 @@ impl Tree<'_> {
editor: &mut WorldEditor,
(x, y, z): Coord,
building_footprints: Option<&BuildingFootprintBitmap>,
) {
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
// The element_id of 0 is used as a salt for tree-specific randomness
let mut rng = coord_rng(x, z, 0);
let tree_type = match rng.gen_range(1..=3) {
1 => TreeType::Oak,
2 => TreeType::Spruce,
3 => TreeType::Birch,
_ => unreachable!(),
};
Self::create_of_type(editor, (x, y, z), tree_type, building_footprints);
}
/// Creates a tree of a specific type at the specified coordinates.
pub fn create_of_type(
editor: &mut WorldEditor,
(x, y, z): Coord,
tree_type: TreeType,
building_footprints: Option<&BuildingFootprintBitmap>,
) {
// Skip if this coordinate is inside a building
if let Some(footprints) = building_footprints {
@@ -135,16 +157,7 @@ impl Tree<'_> {
blacklist.extend(Self::get_functional_blocks());
blacklist.push(WATER);
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
// The element_id of 0 is used as a salt for tree-specific randomness
let mut rng = coord_rng(x, z, 0);
let tree = Self::get_tree(match rng.gen_range(1..=3) {
1 => TreeType::Oak,
2 => TreeType::Spruce,
3 => TreeType::Birch,
_ => unreachable!(),
});
let tree = Self::get_tree(tree_type);
// Build the logs
editor.fill_blocks(

View File

@@ -58,14 +58,14 @@ pub fn generate_water_areas_from_relation(
}
// Preserve OSM-defined outer/inner roles without modification
merge_way_segments(&mut outers);
super::merge_way_segments(&mut outers);
// Clip assembled rings to bbox (must happen after merging to preserve ring connectivity)
outers = outers
.into_iter()
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
.collect();
merge_way_segments(&mut inners);
super::merge_way_segments(&mut inners);
inners = inners
.into_iter()
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
@@ -112,7 +112,7 @@ pub fn generate_water_areas_from_relation(
}
}
merge_way_segments(&mut inners);
super::merge_way_segments(&mut inners);
if !verify_closed_rings(&inners) {
println!("Skipping relation {} due to invalid polygon", element.id);
return;
@@ -166,105 +166,6 @@ fn generate_water_areas(
inverse_floodfill(min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor);
}
/// Merges way segments that share endpoints into closed rings.
fn merge_way_segments(rings: &mut Vec<Vec<ProcessedNode>>) {
let mut removed: Vec<usize> = vec![];
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
// Match nodes by ID or proximity (handles synthetic nodes from bbox clipping)
let nodes_match = |a: &ProcessedNode, b: &ProcessedNode| -> bool {
if a.id == b.id {
return true;
}
let dx = (a.x - b.x).abs();
let dz = (a.z - b.z).abs();
dx <= 1 && dz <= 1
};
for i in 0..rings.len() {
for j in 0..rings.len() {
if i == j {
continue;
}
if removed.contains(&i) || removed.contains(&j) {
continue;
}
let x: &Vec<ProcessedNode> = &rings[i];
let y: &Vec<ProcessedNode> = &rings[j];
// Skip empty rings (can happen after clipping)
if x.is_empty() || y.is_empty() {
continue;
}
let x_first = &x[0];
let x_last = x.last().unwrap();
let y_first = &y[0];
let y_last = y.last().unwrap();
// Skip already-closed rings
if nodes_match(x_first, x_last) {
continue;
}
if nodes_match(y_first, y_last) {
continue;
}
if nodes_match(x_first, y_first) {
removed.push(i);
removed.push(j);
let mut x: Vec<ProcessedNode> = x.clone();
x.reverse();
x.extend(y.iter().skip(1).cloned());
merged.push(x);
} else if nodes_match(x_last, y_last) {
removed.push(i);
removed.push(j);
let mut x: Vec<ProcessedNode> = x.clone();
x.extend(y.iter().rev().skip(1).cloned());
merged.push(x);
} else if nodes_match(x_first, y_last) {
removed.push(i);
removed.push(j);
let mut y: Vec<ProcessedNode> = y.clone();
y.extend(x.iter().skip(1).cloned());
merged.push(y);
} else if nodes_match(x_last, y_first) {
removed.push(i);
removed.push(j);
let mut x: Vec<ProcessedNode> = x.clone();
x.extend(y.iter().skip(1).cloned());
merged.push(x);
}
}
}
removed.sort();
for r in removed.iter().rev() {
rings.remove(*r);
}
let merged_len: usize = merged.len();
for m in merged {
rings.push(m);
}
if merged_len > 0 {
merge_way_segments(rings);
}
}
/// Verifies all rings are properly closed (first node matches last).
fn verify_closed_rings(rings: &[Vec<ProcessedNode>]) -> bool {
let mut valid = true;

View File

@@ -222,6 +222,7 @@ impl FloodFillCache {
fn way_needs_flood_fill(way: &ProcessedWay) -> bool {
way.tags.contains_key("building")
|| way.tags.contains_key("building:part")
|| way.tags.contains_key("boundary")
|| way.tags.contains_key("landuse")
|| way.tags.contains_key("leisure")
|| way.tags.contains_key("amenity")

View File

@@ -717,12 +717,22 @@ fn gui_get_world_map_data(world_path: String) -> Result<Option<WorldMapData>, St
.as_f64()
.ok_or("Missing maxGeoLon in metadata")?;
// Extract Minecraft coordinate bounds
let min_mc_x = metadata["minMcX"].as_i64().unwrap_or(0) as i32;
let max_mc_x = metadata["maxMcX"].as_i64().unwrap_or(0) as i32;
let min_mc_z = metadata["minMcZ"].as_i64().unwrap_or(0) as i32;
let max_mc_z = metadata["maxMcZ"].as_i64().unwrap_or(0) as i32;
Ok(Some(WorldMapData {
image_base64: format!("data:image/png;base64,{}", base64_image),
min_lat,
max_lat,
min_lon,
max_lon,
min_mc_x,
max_mc_x,
min_mc_z,
max_mc_z,
}))
}
@@ -734,6 +744,11 @@ struct WorldMapData {
max_lat: f64,
min_lon: f64,
max_lon: f64,
// Minecraft coordinate bounds for coordinate copying
min_mc_x: i32,
max_mc_x: i32,
min_mc_z: i32,
max_mc_z: i32,
}
/// Opens the file with default application (Windows) or shows in file explorer (macOS/Linux)

40
src/gui/css/bbox.css vendored
View File

@@ -375,4 +375,42 @@ body,
accent-color: #3887BE;
display: block;
margin: 0;
}
}
/* Context menu for coordinate copying */
.coordinate-context-menu {
position: fixed;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 10000;
min-width: 160px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 13px;
}
.coordinate-context-menu-item {
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
color: #333;
}
.coordinate-context-menu-item:hover {
background: #f0f0f0;
}
.coordinate-context-menu-item svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.coordinate-context-menu-separator {
height: 1px;
background: #e0e0e0;
margin: 4px 0;
}

182
src/gui/js/bbox.js vendored
View File

@@ -749,6 +749,188 @@ $(document).ready(function () {
}
}
// ========== Context Menu for Coordinate Copying ==========
var contextMenuElement = null;
// Create the context menu element
function createContextMenu() {
if (contextMenuElement) return contextMenuElement;
contextMenuElement = document.createElement('div');
contextMenuElement.className = 'coordinate-context-menu';
contextMenuElement.style.display = 'none';
contextMenuElement.innerHTML = `
<div class="coordinate-context-menu-item" id="copy-coords-item">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span id="copy-coords-text">Copy coordinates</span>
</div>
`;
document.body.appendChild(contextMenuElement);
// Handle click on the copy coordinates item
var copyItem = contextMenuElement.querySelector('#copy-coords-item');
copyItem.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
copyMinecraftCoordinates();
hideContextMenu();
});
return contextMenuElement;
}
// Show context menu at position
function showContextMenu(x, y, latLng) {
if (!worldPreviewAvailable || !worldOverlayData) return;
var menu = createContextMenu();
// Position the menu, ensuring it stays within viewport
var menuWidth = 180;
var menuHeight = 40;
var viewportWidth = window.innerWidth;
var viewportHeight = window.innerHeight;
var posX = x;
var posY = y;
// Adjust if menu would go off-screen
if (x + menuWidth > viewportWidth) {
posX = viewportWidth - menuWidth - 10;
}
if (y + menuHeight > viewportHeight) {
posY = viewportHeight - menuHeight - 10;
}
menu.style.left = posX + 'px';
menu.style.top = posY + 'px';
menu.style.display = 'block';
// Store the latLng for copying
menu.dataset.lat = latLng.lat;
menu.dataset.lng = latLng.lng;
}
// Hide context menu
function hideContextMenu() {
if (contextMenuElement) {
contextMenuElement.style.display = 'none';
}
}
// Calculate Minecraft coordinates from lat/lng
function calculateMinecraftCoords(lat, lng) {
if (!worldOverlayData) return null;
var data = worldOverlayData;
// Check if Minecraft coordinate bounds are available (not all zeros)
if (data.min_mc_x === 0 && data.max_mc_x === 0 &&
data.min_mc_z === 0 && data.max_mc_z === 0) {
return null;
}
// Calculate the relative position within the geo bounds (0 to 1)
// Note: Latitude increases northward, but Minecraft Z increases southward
var relX = (lng - data.min_lon) / (data.max_lon - data.min_lon);
var relZ = (data.max_lat - lat) / (data.max_lat - data.min_lat);
// Clamp to 0-1 range
relX = Math.max(0, Math.min(1, relX));
relZ = Math.max(0, Math.min(1, relZ));
// Calculate Minecraft X and Z coordinates
var mcX = Math.round(data.min_mc_x + relX * (data.max_mc_x - data.min_mc_x));
var mcZ = Math.round(data.min_mc_z + relZ * (data.max_mc_z - data.min_mc_z));
// Default Y coordinate (ground level, typically around 64-70)
var mcY = 100;
return { x: mcX, y: mcY, z: mcZ };
}
// Copy Minecraft coordinates to clipboard
function copyMinecraftCoordinates() {
if (!contextMenuElement) return;
var lat = parseFloat(contextMenuElement.dataset.lat);
var lng = parseFloat(contextMenuElement.dataset.lng);
var coords = calculateMinecraftCoords(lat, lng);
if (!coords) return;
var tpCommand = '/tp ' + coords.x + ' ' + coords.y + ' ' + coords.z;
// Copy to clipboard using modern API with fallback
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(tpCommand).catch(function(err) {
// Fallback for clipboard API failure
fallbackCopyToClipboard(tpCommand);
});
} else {
// Fallback for older browsers
fallbackCopyToClipboard(tpCommand);
}
}
// Fallback clipboard copy method for older browsers
function fallbackCopyToClipboard(text) {
var textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
textArea.style.top = '-9999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
} catch (err) {
console.error('Failed to copy coordinates:', err);
}
document.body.removeChild(textArea);
}
// Check if Minecraft coordinate bounds are available
function hasMinecraftCoords() {
if (!worldOverlayData) return false;
var data = worldOverlayData;
return !(data.min_mc_x === 0 && data.max_mc_x === 0 &&
data.min_mc_z === 0 && data.max_mc_z === 0);
}
// Handle right-click on the map
map.on('contextmenu', function(e) {
// Only show context menu if world preview is available and has Minecraft coords
if (worldPreviewAvailable && worldOverlayData && hasMinecraftCoords()) {
// Check if the click is within the world bounds
var data = worldOverlayData;
var lat = e.latlng.lat;
var lng = e.latlng.lng;
if (lat >= data.min_lat && lat <= data.max_lat &&
lng >= data.min_lon && lng <= data.max_lon) {
showContextMenu(e.originalEvent.clientX, e.originalEvent.clientY, e.latlng);
}
}
});
// Hide context menu on any click or map interaction
document.addEventListener('click', function(e) {
if (contextMenuElement && !contextMenuElement.contains(e.target)) {
hideContextMenu();
}
});
map.on('movestart', hideContextMenu);
map.on('zoomstart', hideContextMenu);
// ========== End Context Menu ==========
// Listen for messages from parent window
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'changeTileTheme') {

View File

@@ -195,11 +195,7 @@ Wkt.Wkt.prototype.toObject = function (config) {
* Absorbs the geometry of another Wkt.Wkt instance, merging it with its own,
* creating a collection (MULTI-geometry) based on their types, which must agree.
* For example, creates a MULTIPOLYGON from a POLYGON type merged with another
<<<<<<< HEAD
* POLYGON type.
=======
* POLYGON type, or adds a POLYGON instance to a MULTIPOLYGON instance.
>>>>>>> dev
* @memberof Wkt.Wkt
* @method
*/

View File

@@ -272,13 +272,17 @@ pub fn parse_osm_data(
continue;
};
// Only process multipolygons for now
if tags.get("type").map(|x: &String| x.as_str()) != Some("multipolygon") {
// Process multipolygons and boundary relations
let relation_type = tags.get("type").map(|x: &String| x.as_str());
if relation_type != Some("multipolygon") && relation_type != Some("boundary") {
continue;
};
// Water relations require unclipped ways for ring merging in water_areas.rs
// Boundary relations also require unclipped ways for proper ring assembly
let is_water_relation = is_water_element(tags);
let is_boundary_relation = tags.contains_key("boundary");
let keep_unclipped = is_water_relation || is_boundary_relation;
let members: Vec<ProcessedMember> = element
.members
@@ -304,9 +308,9 @@ pub fn parse_osm_data(
}
};
// Water relations: keep unclipped for ring merging
// Non-water relations: clip member ways now
let final_way = if is_water_relation {
// Water and boundary relations: keep unclipped for ring merging
// Other relations: clip member ways now
let final_way = if keep_unclipped {
way
} else {
let clipped_nodes = clip_way_to_bbox(&way.nodes, &xzbbox);

View File

@@ -137,6 +137,7 @@ pub fn fetch_data_from_overpass(
nwr["barrier"];
nwr["entrance"];
nwr["door"];
nwr["boundary"];
way;
)->.relsinbbox;
(

View File

@@ -14,11 +14,14 @@ use crate::ground::Ground;
use crate::progress::emit_gui_progress_update;
use bedrockrs_level::level::db_interface::bedrock_key::ChunkKey;
use bedrockrs_level::level::db_interface::rusty::RustyDBInterface;
use bedrockrs_level::level::db_interface::key_level::KeyTypeTag;
use bedrockrs_level::level::db_interface::rusty::{mcpe_options, RustyDBInterface};
use bedrockrs_level::level::file_interface::RawWorldTrait;
use bedrockrs_shared::world::dimension::Dimension;
use byteorder::{LittleEndian, WriteBytesExt};
use fastnbt::Value;
use indicatif::{ProgressBar, ProgressStyle};
use rusty_leveldb::DB;
use serde::Serialize;
use std::collections::HashMap as StdHashMap;
use std::fs::{self, File};
@@ -82,6 +85,8 @@ impl From<serde_json::Error> for BedrockSaveError {
}
}
const DEFAULT_BEDROCK_COMPRESSION_LEVEL: u8 = 6;
/// Metadata for Bedrock worlds
#[derive(Serialize)]
struct BedrockMetadata {
@@ -402,7 +407,7 @@ impl BedrockWriter {
// Open LevelDB with Bedrock-compatible options
let mut state = ();
let mut db: RustyDBInterface<()> =
RustyDBInterface::new(db_path.into_boxed_path(), true, &mut state)
RustyDBInterface::new(db_path.clone().into_boxed_path(), true, &mut state)
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
// Count total chunks for progress
@@ -416,63 +421,128 @@ impl BedrockWriter {
return Ok(());
}
let progress_bar = ProgressBar::new(total_chunks as u64);
progress_bar.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} chunks ({eta})")
.unwrap()
.progress_chars("█▓░"),
);
{
let progress_bar = ProgressBar::new(total_chunks as u64);
progress_bar.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} chunks ({eta})")
.unwrap()
.progress_chars("█▓░"),
);
let mut chunks_processed: usize = 0;
let mut chunks_processed: usize = 0;
// Process each region and chunk
for ((region_x, region_z), region) in &world.regions {
for ((local_chunk_x, local_chunk_z), chunk) in &region.chunks {
// Calculate absolute chunk coordinates
let abs_chunk_x = region_x * 32 + local_chunk_x;
let abs_chunk_z = region_z * 32 + local_chunk_z;
let chunk_pos = Vec2::new(abs_chunk_x, abs_chunk_z);
// Write chunk version marker (42 is current Bedrock version as of 1.21+)
let version_key = ChunkKey::chunk_marker(chunk_pos, Dimension::Overworld);
db.set_subchunk_raw(version_key, &[42], &mut state)
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
// Write Data3D (heightmap + biomes) - required for chunk to be valid
let data3d_key = ChunkKey::data3d(chunk_pos, Dimension::Overworld);
let data3d = self.create_data3d(chunk);
db.set_subchunk_raw(data3d_key, &data3d, &mut state)
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
// Process each section (subchunk)
for (&section_y, section) in &chunk.sections {
// Encode the subchunk
let subchunk_bytes = self.encode_subchunk(section, section_y)?;
// Write to database
let subchunk_key =
ChunkKey::new_subchunk(chunk_pos, Dimension::Overworld, section_y);
db.set_subchunk_raw(subchunk_key, &subchunk_bytes, &mut state)
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
}
chunks_processed += 1;
progress_bar.inc(1);
// Update GUI progress (92% to 97% range for chunk writing)
if chunks_processed.is_multiple_of(10) || chunks_processed == total_chunks {
let chunk_progress = chunks_processed as f64 / total_chunks as f64;
let gui_progress = 92.0 + (chunk_progress * 5.0); // 92% to 97%
emit_gui_progress_update(gui_progress, "");
}
}
}
progress_bar.finish_with_message("Chunks written to LevelDB");
}
// Ensure the RustyDBInterface handle is dropped before opening another DB for the same path.
drop(db);
self.write_chunk_entities(world, &db_path)?;
Ok(())
}
fn write_chunk_entities(
&self,
world: &WorldToModify,
db_path: &std::path::Path,
) -> Result<(), BedrockSaveError> {
let mut opts = mcpe_options(DEFAULT_BEDROCK_COMPRESSION_LEVEL);
opts.create_if_missing = true;
let mut db = DB::open(db_path.to_path_buf().into_boxed_path(), opts)
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
// Process each region and chunk
for ((region_x, region_z), region) in &world.regions {
for ((local_chunk_x, local_chunk_z), chunk) in &region.chunks {
// Calculate absolute chunk coordinates
let abs_chunk_x = region_x * 32 + local_chunk_x;
let abs_chunk_z = region_z * 32 + local_chunk_z;
let chunk_pos = Vec2::new(abs_chunk_x, abs_chunk_z);
let chunk_pos =
Vec2::new(region_x * 32 + local_chunk_x, region_z * 32 + local_chunk_z);
// Write chunk version marker (42 is current Bedrock version as of 1.21+)
let version_key = ChunkKey::chunk_marker(chunk_pos, Dimension::Overworld);
db.set_subchunk_raw(version_key, &[42], &mut state)
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
// Write Data3D (heightmap + biomes) - required for chunk to be valid
let data3d_key = ChunkKey::data3d(chunk_pos, Dimension::Overworld);
let data3d = self.create_data3d(chunk);
db.set_subchunk_raw(data3d_key, &data3d, &mut state)
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
// Process each section (subchunk)
for (&section_y, section) in &chunk.sections {
// Encode the subchunk
let subchunk_bytes = self.encode_subchunk(section, section_y)?;
// Write to database
let subchunk_key =
ChunkKey::new_subchunk(chunk_pos, Dimension::Overworld, section_y);
db.set_subchunk_raw(subchunk_key, &subchunk_bytes, &mut state)
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
}
chunks_processed += 1;
progress_bar.inc(1);
// Update GUI progress (92% to 97% range for chunk writing)
if chunks_processed.is_multiple_of(10) || chunks_processed == total_chunks {
let chunk_progress = chunks_processed as f64 / total_chunks as f64;
let gui_progress = 92.0 + (chunk_progress * 5.0); // 92% to 97%
emit_gui_progress_update(gui_progress, "");
}
self.write_compound_list_record(
&mut db,
chunk_pos,
KeyTypeTag::BlockEntity,
chunk.other.get("block_entities"),
)?;
self.write_compound_list_record(
&mut db,
chunk_pos,
KeyTypeTag::Entity,
chunk.other.get("entities"),
)?;
}
}
progress_bar.finish_with_message("Chunks written to LevelDB");
Ok(())
}
// LevelDB writes are flushed when the database is dropped
drop(db);
fn write_compound_list_record(
&self,
db: &mut DB,
chunk_pos: Vec2<i32>,
key_type: KeyTypeTag,
value: Option<&Value>,
) -> Result<(), BedrockSaveError> {
let Some(Value::List(values)) = value else {
return Ok(());
};
if values.is_empty() {
return Ok(());
}
let deduped = dedup_compound_list(values);
if deduped.is_empty() {
return Ok(());
}
let data = nbtx::to_le_bytes(&deduped).map_err(|e| BedrockSaveError::Nbt(e.to_string()))?;
let key = build_chunk_key_bytes(chunk_pos, Dimension::Overworld, key_type, None);
db.put(&key, &data)
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
Ok(())
}
@@ -737,6 +807,91 @@ fn bedrock_bits_per_block(palette_count: u32) -> u8 {
16 // Maximum
}
fn build_chunk_key_bytes(
chunk_pos: Vec2<i32>,
dimension: Dimension,
key_type: KeyTypeTag,
y_index: Option<i8>,
) -> Vec<u8> {
let mut buffer = Vec::with_capacity(
9 + if dimension != Dimension::Overworld {
4
} else {
0
} + 1,
);
buffer.extend_from_slice(&chunk_pos.x.to_le_bytes());
buffer.extend_from_slice(&chunk_pos.y.to_le_bytes());
if dimension != Dimension::Overworld {
buffer.extend_from_slice(&i32::from(dimension).to_le_bytes());
}
buffer.push(key_type.to_byte());
if let Some(y) = y_index {
buffer.push(y as u8);
}
buffer
}
fn dedup_compound_list(values: &[Value]) -> Vec<Value> {
let mut coord_index: StdHashMap<(i32, i32, i32), usize> = StdHashMap::new();
let mut deduped: Vec<Value> = Vec::with_capacity(values.len());
for value in values {
if let Value::Compound(map) = value {
if let Some(coords) = get_entity_coords(map) {
if let Some(idx) = coord_index.get(&coords).copied() {
deduped[idx] = value.clone();
continue;
} else {
coord_index.insert(coords, deduped.len());
}
}
}
deduped.push(value.clone());
}
deduped
}
fn get_entity_coords(entity: &StdHashMap<String, Value>) -> Option<(i32, i32, i32)> {
if let Some(Value::List(pos)) = entity.get("Pos") {
if pos.len() == 3 {
if let (Some(x), Some(y), Some(z)) = (
value_to_i32(&pos[0]),
value_to_i32(&pos[1]),
value_to_i32(&pos[2]),
) {
return Some((x, y, z));
}
}
}
let (Some(x), Some(y), Some(z)) = (
entity.get("x").and_then(value_to_i32),
entity.get("y").and_then(value_to_i32),
entity.get("z").and_then(value_to_i32),
) else {
return None;
};
Some((x, y, z))
}
fn value_to_i32(value: &Value) -> Option<i32> {
match value {
Value::Byte(v) => Some(i32::from(*v)),
Value::Short(v) => Some(i32::from(*v)),
Value::Int(v) => Some(*v),
Value::Long(v) => i32::try_from(*v).ok(),
Value::Float(v) => Some(*v as i32),
Value::Double(v) => Some(*v as i32),
_ => None,
}
}
/// Level.dat structure for Bedrock Edition
/// This struct contains all required fields for a valid Bedrock world
#[derive(serde::Serialize)]

View File

@@ -27,7 +27,7 @@ pub(crate) struct Chunk {
}
/// Section within a chunk (16x16x16 blocks)
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
pub(crate) struct Section {
pub block_states: Blockstates,
#[serde(rename = "Y")]
@@ -37,7 +37,7 @@ pub(crate) struct Section {
}
/// Block states within a section
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
pub(crate) struct Blockstates {
pub palette: Vec<PaletteItem>,
pub data: Option<LongArray>,
@@ -46,7 +46,7 @@ pub(crate) struct Blockstates {
}
/// Palette item for block state encoding
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
pub(crate) struct PaletteItem {
#[serde(rename = "Name")]
pub name: String,

View File

@@ -16,6 +16,24 @@ use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::OnceLock;
/// Cached base chunk sections (grass at Y=-62)
/// Computed once on first use and reused for all empty chunks
static BASE_CHUNK_SECTIONS: OnceLock<Vec<Section>> = OnceLock::new();
/// Get or create the cached base chunk sections
fn get_base_chunk_sections() -> &'static [Section] {
BASE_CHUNK_SECTIONS.get_or_init(|| {
let mut chunk = ChunkToModify::default();
for x in 0..16 {
for z in 0..16 {
chunk.set_block(x, -62, z, GRASS_BLOCK);
}
}
chunk.sections().collect()
})
}
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
@@ -47,23 +65,18 @@ impl<'a> WorldEditor<'a> {
}
/// Helper function to create a base chunk with grass blocks at Y -62
/// Uses cached sections for efficiency - only serialization happens per chunk
pub(super) fn create_base_chunk(abs_chunk_x: i32, abs_chunk_z: i32) -> (Vec<u8>, bool) {
let mut chunk = ChunkToModify::default();
// Use cached sections (computed once on first call)
let sections = get_base_chunk_sections();
// Fill the bottom layer with grass blocks at Y -62
for x in 0..16 {
for z in 0..16 {
chunk.set_block(x, -62, z, GRASS_BLOCK);
}
}
// Prepare chunk data
// Prepare chunk data with cloned sections
let chunk_data = Chunk {
sections: chunk.sections().collect(),
sections: sections.to_vec(),
x_pos: abs_chunk_x,
z_pos: abs_chunk_z,
is_light_on: 0,
other: chunk.other,
other: FnvHashMap::default(),
};
// Create the Level wrapper
@@ -128,7 +141,8 @@ impl<'a> WorldEditor<'a> {
/// Saves a single region to disk.
///
/// This is extracted to allow streaming mode to save and release regions one at a time.
/// Optimized for new world creation, writes chunks directly without reading existing data.
/// This assumes we're creating a fresh world, not modifying an existing one.
fn save_single_region(
&self,
region_x: i32,
@@ -138,81 +152,19 @@ impl<'a> WorldEditor<'a> {
let mut region = self.create_region(region_x, region_z);
let mut ser_buffer = Vec::with_capacity(8192);
// First pass: write all chunks that have content
for (&(chunk_x, chunk_z), chunk_to_modify) in &region_to_modify.chunks {
if !chunk_to_modify.sections.is_empty() || !chunk_to_modify.other.is_empty() {
// Read existing chunk data if it exists
let existing_data = region
.read_chunk(chunk_x as usize, chunk_z as usize)
.unwrap()
.unwrap_or_default();
// Parse existing chunk or create new one
let mut chunk: Chunk = if !existing_data.is_empty() {
fastnbt::from_bytes(&existing_data).unwrap()
} else {
Chunk {
sections: Vec::new(),
x_pos: chunk_x + (region_x * 32),
z_pos: chunk_z + (region_z * 32),
is_light_on: 0,
other: FnvHashMap::default(),
}
// Create chunk directly, we're writing to a fresh region file
// so there's no existing data to preserve
let chunk = Chunk {
sections: chunk_to_modify.sections().collect(),
x_pos: chunk_x + (region_x * 32),
z_pos: chunk_z + (region_z * 32),
is_light_on: 0,
other: chunk_to_modify.other.clone(),
};
// Update sections while preserving existing data
let new_sections: Vec<Section> = chunk_to_modify.sections().collect();
for new_section in new_sections {
if let Some(existing_section) =
chunk.sections.iter_mut().find(|s| s.y == new_section.y)
{
// Merge block states
existing_section.block_states.palette = new_section.block_states.palette;
existing_section.block_states.data = new_section.block_states.data;
} else {
// Add new section if it doesn't exist
chunk.sections.push(new_section);
}
}
// Preserve existing block entities and merge with new ones
if let Some(existing_entities) = chunk.other.get_mut("block_entities") {
if let Some(new_entities) = chunk_to_modify.other.get("block_entities") {
if let (Value::List(existing), Value::List(new)) =
(existing_entities, new_entities)
{
// Remove old entities that are replaced by new ones
existing.retain(|e| {
if let Value::Compound(map) = e {
let (x, y, z) = get_entity_coords(map);
!new.iter().any(|new_e| {
if let Value::Compound(new_map) = new_e {
let (nx, ny, nz) = get_entity_coords(new_map);
x == nx && y == ny && z == nz
} else {
false
}
})
} else {
true
}
});
// Add new entities
existing.extend(new.clone());
}
}
} else {
// If no existing entities, just add the new ones
if let Some(new_entities) = chunk_to_modify.other.get("block_entities") {
chunk
.other
.insert("block_entities".to_string(), new_entities.clone());
}
}
// Update chunk coordinates and flags
chunk.x_pos = chunk_x + (region_x * 32);
chunk.z_pos = chunk_z + (region_z * 32);
// Create Level wrapper and save
let level_data = create_level_wrapper(&chunk);
ser_buffer.clear();
@@ -223,7 +175,7 @@ impl<'a> WorldEditor<'a> {
}
}
// Second pass: ensure all chunks exist
// Second pass: ensure all chunks exist (fill with base layer if not)
for chunk_x in 0..32 {
for chunk_z in 0..32 {
let abs_chunk_x = chunk_x + (region_x * 32);
@@ -245,88 +197,138 @@ impl<'a> WorldEditor<'a> {
}
/// Helper function to get entity coordinates
/// Note: Currently unused since we write directly without merging, but kept for potential future use
#[inline]
fn get_entity_coords(entity: &HashMap<String, Value>) -> (i32, i32, i32) {
let x = if let Value::Int(x) = entity.get("x").unwrap_or(&Value::Int(0)) {
*x
} else {
0
#[allow(dead_code)]
fn get_entity_coords(entity: &HashMap<String, Value>) -> Option<(i32, i32, i32)> {
if let Some(Value::List(pos)) = entity.get("Pos") {
if pos.len() == 3 {
if let (Some(x), Some(y), Some(z)) = (
value_to_i32(&pos[0]),
value_to_i32(&pos[1]),
value_to_i32(&pos[2]),
) {
return Some((x, y, z));
}
}
}
let (Some(x), Some(y), Some(z)) = (
entity.get("x").and_then(value_to_i32),
entity.get("y").and_then(value_to_i32),
entity.get("z").and_then(value_to_i32),
) else {
return None;
};
let y = if let Value::Int(y) = entity.get("y").unwrap_or(&Value::Int(0)) {
*y
} else {
0
};
let z = if let Value::Int(z) = entity.get("z").unwrap_or(&Value::Int(0)) {
*z
} else {
0
};
(x, y, z)
Some((x, y, z))
}
/// Creates a Level wrapper for chunk data (Java Edition format)
#[inline]
fn create_level_wrapper(chunk: &Chunk) -> HashMap<String, Value> {
HashMap::from([(
"Level".to_string(),
Value::Compound(HashMap::from([
("xPos".to_string(), Value::Int(chunk.x_pos)),
("zPos".to_string(), Value::Int(chunk.z_pos)),
(
"isLightOn".to_string(),
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
),
(
"sections".to_string(),
Value::List(
chunk
.sections
.iter()
.map(|section| {
let mut block_states = HashMap::from([(
"palette".to_string(),
Value::List(
section
.block_states
.palette
.iter()
.map(|item| {
let mut palette_item = HashMap::from([(
"Name".to_string(),
Value::String(item.name.clone()),
)]);
if let Some(props) = &item.properties {
palette_item.insert(
"Properties".to_string(),
props.clone(),
);
}
Value::Compound(palette_item)
})
.collect(),
),
)]);
let mut level_map = HashMap::from([
("xPos".to_string(), Value::Int(chunk.x_pos)),
("zPos".to_string(), Value::Int(chunk.z_pos)),
(
"isLightOn".to_string(),
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
),
(
"sections".to_string(),
Value::List(
chunk
.sections
.iter()
.map(|section| {
let mut block_states = HashMap::from([(
"palette".to_string(),
Value::List(
section
.block_states
.palette
.iter()
.map(|item| {
let mut palette_item = HashMap::from([(
"Name".to_string(),
Value::String(item.name.clone()),
)]);
if let Some(props) = &item.properties {
palette_item
.insert("Properties".to_string(), props.clone());
}
Value::Compound(palette_item)
})
.collect(),
),
)]);
// Only add the `data` attribute if it's non-empty
// to maintain compatibility with third-party tools like Dynmap
if let Some(data) = &section.block_states.data {
if !data.is_empty() {
block_states.insert(
"data".to_string(),
Value::LongArray(data.to_owned()),
);
}
// Only add the `data` attribute if it's non-empty
// to maintain compatibility with third-party tools like Dynmap
if let Some(data) = &section.block_states.data {
if !data.is_empty() {
block_states
.insert("data".to_string(), Value::LongArray(data.to_owned()));
}
}
Value::Compound(HashMap::from([
("Y".to_string(), Value::Byte(section.y)),
("block_states".to_string(), Value::Compound(block_states)),
]))
})
.collect(),
),
Value::Compound(HashMap::from([
("Y".to_string(), Value::Byte(section.y)),
("block_states".to_string(), Value::Compound(block_states)),
]))
})
.collect(),
),
])),
)])
),
]);
for (key, value) in &chunk.other {
level_map.insert(key.clone(), value.clone());
}
HashMap::from([("Level".to_string(), Value::Compound(level_map))])
}
/// Merge compound lists (entities, block_entities) from chunk_to_modify into chunk
/// Note: Currently unused since we write directly without merging, but kept for potential future use
#[allow(dead_code)]
fn merge_compound_list(chunk: &mut Chunk, chunk_to_modify: &ChunkToModify, key: &str) {
if let Some(existing_entities) = chunk.other.get_mut(key) {
if let Some(new_entities) = chunk_to_modify.other.get(key) {
if let (Value::List(existing), Value::List(new)) = (existing_entities, new_entities) {
existing.retain(|e| {
if let Value::Compound(map) = e {
if let Some((x, y, z)) = get_entity_coords(map) {
return !new.iter().any(|new_e| {
if let Value::Compound(new_map) = new_e {
get_entity_coords(new_map) == Some((x, y, z))
} else {
false
}
});
}
}
true
});
existing.extend(new.clone());
}
}
} else if let Some(new_entities) = chunk_to_modify.other.get(key) {
chunk.other.insert(key.to_string(), new_entities.clone());
}
}
/// Convert NBT Value to i32
/// Note: Currently unused since we write directly without merging, but kept for potential future use
#[allow(dead_code)]
fn value_to_i32(value: &Value) -> Option<i32> {
match value {
Value::Byte(v) => Some(i32::from(*v)),
Value::Short(v) => Some(i32::from(*v)),
Value::Int(v) => Some(*v),
Value::Long(v) => i32::try_from(*v).ok(),
Value::Float(v) => Some(*v as i32),
Value::Double(v) => Some(*v as i32),
_ => None,
}
}

View File

@@ -27,9 +27,9 @@ use crate::coordinate_system::geographic::LLBBox;
use crate::ground::Ground;
use crate::progress::emit_gui_progress_update;
use colored::Colorize;
use fastnbt::Value;
use fastnbt::{IntArray, Value};
use serde::Serialize;
use std::collections::HashMap;
use std::collections::{hash_map::Entry, HashMap};
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
@@ -243,6 +243,212 @@ impl<'a> WorldEditor<'a> {
self.set_block(SIGN, x, y, z, None, None);
}
/// Adds an entity at the given coordinates (Y is ground-relative).
#[allow(dead_code)]
pub fn add_entity(
&mut self,
id: &str,
x: i32,
y: i32,
z: i32,
extra_data: Option<HashMap<String, Value>>,
) {
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
return;
}
let absolute_y = self.get_absolute_y(x, y, z);
let mut entity = HashMap::new();
entity.insert("id".to_string(), Value::String(id.to_string()));
entity.insert(
"Pos".to_string(),
Value::List(vec![
Value::Double(x as f64 + 0.5),
Value::Double(absolute_y as f64),
Value::Double(z as f64 + 0.5),
]),
);
entity.insert(
"Motion".to_string(),
Value::List(vec![
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
]),
);
entity.insert(
"Rotation".to_string(),
Value::List(vec![Value::Float(0.0), Value::Float(0.0)]),
);
entity.insert("OnGround".to_string(), Value::Byte(1));
entity.insert("FallDistance".to_string(), Value::Float(0.0));
entity.insert("Fire".to_string(), Value::Short(-20));
entity.insert("Air".to_string(), Value::Short(300));
entity.insert("PortalCooldown".to_string(), Value::Int(0));
entity.insert(
"UUID".to_string(),
Value::IntArray(build_deterministic_uuid(id, x, absolute_y, z)),
);
if let Some(extra) = extra_data {
for (key, value) in extra {
entity.insert(key, value);
}
}
let chunk_x: i32 = x >> 4;
let chunk_z: i32 = z >> 4;
let region_x: i32 = chunk_x >> 5;
let region_z: i32 = chunk_z >> 5;
let region = self.world.get_or_create_region(region_x, region_z);
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
match chunk.other.entry("entities".to_string()) {
Entry::Occupied(mut entry) => {
if let Value::List(list) = entry.get_mut() {
list.push(Value::Compound(entity));
}
}
Entry::Vacant(entry) => {
entry.insert(Value::List(vec![Value::Compound(entity)]));
}
}
}
/// Places a chest with the provided items at the given coordinates (ground-relative Y).
#[allow(dead_code)]
pub fn set_chest_with_items(
&mut self,
x: i32,
y: i32,
z: i32,
items: Vec<HashMap<String, Value>>,
) {
let absolute_y = self.get_absolute_y(x, y, z);
self.set_chest_with_items_absolute(x, absolute_y, z, items);
}
/// Places a chest with the provided items at the given coordinates (absolute Y).
#[allow(dead_code)]
pub fn set_chest_with_items_absolute(
&mut self,
x: i32,
absolute_y: i32,
z: i32,
items: Vec<HashMap<String, Value>>,
) {
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
return;
}
let chunk_x: i32 = x >> 4;
let chunk_z: i32 = z >> 4;
let region_x: i32 = chunk_x >> 5;
let region_z: i32 = chunk_z >> 5;
let mut chest_data = HashMap::new();
chest_data.insert(
"id".to_string(),
Value::String("minecraft:chest".to_string()),
);
chest_data.insert("x".to_string(), Value::Int(x));
chest_data.insert("y".to_string(), Value::Int(absolute_y));
chest_data.insert("z".to_string(), Value::Int(z));
chest_data.insert(
"Items".to_string(),
Value::List(items.into_iter().map(Value::Compound).collect()),
);
chest_data.insert("keepPacked".to_string(), Value::Byte(0));
let region = self.world.get_or_create_region(region_x, region_z);
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
match chunk.other.entry("block_entities".to_string()) {
Entry::Occupied(mut entry) => {
if let Value::List(list) = entry.get_mut() {
list.push(Value::Compound(chest_data));
}
}
Entry::Vacant(entry) => {
entry.insert(Value::List(vec![Value::Compound(chest_data)]));
}
}
self.set_block_absolute(CHEST, x, absolute_y, z, None, None);
}
/// Places a block entity with items at the given coordinates (ground-relative Y).
#[allow(dead_code)]
pub fn set_block_entity_with_items(
&mut self,
block_with_props: BlockWithProperties,
x: i32,
y: i32,
z: i32,
block_entity_id: &str,
items: Vec<HashMap<String, Value>>,
) {
let absolute_y = self.get_absolute_y(x, y, z);
self.set_block_entity_with_items_absolute(
block_with_props,
x,
absolute_y,
z,
block_entity_id,
items,
);
}
/// Places a block entity with items at the given coordinates (absolute Y).
#[allow(dead_code)]
pub fn set_block_entity_with_items_absolute(
&mut self,
block_with_props: BlockWithProperties,
x: i32,
absolute_y: i32,
z: i32,
block_entity_id: &str,
items: Vec<HashMap<String, Value>>,
) {
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
return;
}
let chunk_x: i32 = x >> 4;
let chunk_z: i32 = z >> 4;
let region_x: i32 = chunk_x >> 5;
let region_z: i32 = chunk_z >> 5;
let mut block_entity = HashMap::new();
block_entity.insert("id".to_string(), Value::String(block_entity_id.to_string()));
block_entity.insert("x".to_string(), Value::Int(x));
block_entity.insert("y".to_string(), Value::Int(absolute_y));
block_entity.insert("z".to_string(), Value::Int(z));
block_entity.insert(
"Items".to_string(),
Value::List(items.into_iter().map(Value::Compound).collect()),
);
block_entity.insert("keepPacked".to_string(), Value::Byte(0));
let region = self.world.get_or_create_region(region_x, region_z);
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
match chunk.other.entry("block_entities".to_string()) {
Entry::Occupied(mut entry) => {
if let Value::List(list) = entry.get_mut() {
list.push(Value::Compound(block_entity));
}
}
Entry::Vacant(entry) => {
entry.insert(Value::List(vec![Value::Compound(block_entity)]));
}
}
self.set_block_with_properties_absolute(block_with_props, x, absolute_y, z, None, None);
}
/// Sets a block of the specified type at the given coordinates.
///
/// Y value is interpreted as an offset from ground level.
@@ -599,3 +805,30 @@ impl<'a> WorldEditor<'a> {
Ok(())
}
}
#[allow(dead_code)]
fn build_deterministic_uuid(id: &str, x: i32, y: i32, z: i32) -> IntArray {
let mut hash: i64 = 17;
for byte in id.bytes() {
hash = hash.wrapping_mul(31).wrapping_add(byte as i64);
}
let seed_a = hash ^ (x as i64).wrapping_shl(32) ^ (y as i64).wrapping_mul(17);
let seed_b = hash.rotate_left(7) ^ (z as i64).wrapping_mul(31) ^ (x as i64).wrapping_mul(13);
IntArray::new(vec![
(seed_a >> 32) as i32,
seed_a as i32,
(seed_b >> 32) as i32,
seed_b as i32,
])
}
#[allow(dead_code)]
fn single_item(id: &str, slot: i8, count: i8) -> HashMap<String, Value> {
let mut item = HashMap::new();
item.insert("id".to_string(), Value::String(id.to_string()));
item.insert("Slot".to_string(), Value::Byte(slot));
item.insert("Count".to_string(), Value::Byte(count));
item
}