mirror of
https://github.com/louis-e/arnis.git
synced 2026-01-29 16:33:20 -05:00
Compare commits
25 Commits
parallel-p
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1dc3b8ffb | ||
|
|
602767c1d1 | ||
|
|
96e6d9e129 | ||
|
|
c722ea689f | ||
|
|
f473e980a2 | ||
|
|
1901c21049 | ||
|
|
bc41838671 | ||
|
|
11de6cfd85 | ||
|
|
92f629fc96 | ||
|
|
880d86971d | ||
|
|
1421247ea4 | ||
|
|
1b21dec366 | ||
|
|
c9a9d55f76 | ||
|
|
53846a7b5a | ||
|
|
0593615909 | ||
|
|
ceec7cc190 | ||
|
|
d876f5ce60 | ||
|
|
d3a416754d | ||
|
|
fc6c2a255f | ||
|
|
3b70694167 | ||
|
|
e5f0b1050a | ||
|
|
a0fd0c12e2 | ||
|
|
9b87e3538a | ||
|
|
46959365df | ||
|
|
f57d14b200 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -208,6 +208,7 @@ dependencies = [
|
|||||||
"rayon",
|
"rayon",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rfd",
|
"rfd",
|
||||||
|
"rusty-leveldb",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ overflow-checks = true
|
|||||||
[features]
|
[features]
|
||||||
default = ["gui"]
|
default = ["gui"]
|
||||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
|
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]
|
[build-dependencies]
|
||||||
tauri-build = {version = "2", optional = true}
|
tauri-build = {version = "2", optional = true}
|
||||||
@@ -54,6 +54,7 @@ bedrockrs_shared = { git = "https://github.com/bedrock-crustaceans/bedrock-rs",
|
|||||||
nbtx = { git = "https://github.com/bedrock-crustaceans/nbtx", optional = true }
|
nbtx = { git = "https://github.com/bedrock-crustaceans/nbtx", optional = true }
|
||||||
vek = { version = "0.17", optional = true }
|
vek = { version = "0.17", optional = true }
|
||||||
zip = { version = "0.6", default-features = false, features = ["deflate"], optional = true }
|
zip = { version = "0.6", default-features = false, features = ["deflate"], optional = true }
|
||||||
|
rusty-leveldb = { version = "3", optional = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
||||||
|
|||||||
Binary file not shown.
@@ -578,6 +578,11 @@ pub fn to_bedrock_block_with_properties(
|
|||||||
return convert_stairs(java_name, props_map);
|
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)
|
// Handle slabs with type property (top/bottom/double)
|
||||||
if java_name.ends_with("_slab") {
|
if java_name.ends_with("_slab") {
|
||||||
return convert_slab(java_name, props_map);
|
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.
|
/// Convert Java slab block to Bedrock format with proper type.
|
||||||
fn convert_slab(
|
fn convert_slab(
|
||||||
java_name: &str,
|
java_name: &str,
|
||||||
|
|||||||
@@ -266,7 +266,15 @@ impl Block {
|
|||||||
185 => "quartz_stairs",
|
185 => "quartz_stairs",
|
||||||
186 => "polished_andesite_stairs",
|
186 => "polished_andesite_stairs",
|
||||||
187 => "nether_brick_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",
|
||||||
_ => panic!("Invalid id"),
|
_ => panic!("Invalid id"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -464,6 +472,26 @@ impl Block {
|
|||||||
map.insert("half".to_string(), Value::String("top".to_string()));
|
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||||
map
|
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
|
||||||
|
})),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -698,7 +726,17 @@ pub const SMOOTH_SANDSTONE_STAIRS: Block = Block::new(184);
|
|||||||
pub const QUARTZ_STAIRS: Block = Block::new(185);
|
pub const QUARTZ_STAIRS: Block = Block::new(185);
|
||||||
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
|
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
|
||||||
pub const NETHER_BRICK_STAIRS: Block = Block::new(187);
|
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);
|
||||||
|
|
||||||
/// Maps a block to its corresponding stair variant
|
/// Maps a block to its corresponding stair variant
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ use crate::floodfill::flood_fill_area; // Needed for inline amenity flood fills
|
|||||||
use crate::floodfill_cache::FloodFillCache;
|
use crate::floodfill_cache::FloodFillCache;
|
||||||
use crate::osm_parser::ProcessedElement;
|
use crate::osm_parser::ProcessedElement;
|
||||||
use crate::world_editor::WorldEditor;
|
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(
|
pub fn generate_amenities(
|
||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
@@ -34,6 +36,49 @@ pub fn generate_amenities(
|
|||||||
.map(|n: &crate::osm_parser::ProcessedNode| XZPoint::new(n.x, n.z))
|
.map(|n: &crate::osm_parser::ProcessedNode| XZPoint::new(n.x, n.z))
|
||||||
.next();
|
.next();
|
||||||
match amenity_type.as_str() {
|
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" => {
|
"waste_disposal" | "waste_basket" => {
|
||||||
// Place a cauldron for waste disposal or waste basket
|
// Place a cauldron for waste disposal or waste basket
|
||||||
if let Some(pt) = first_node {
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ pub fn generate_buildings(
|
|||||||
let lev = levels - min_level;
|
let lev = levels - min_level;
|
||||||
|
|
||||||
if lev >= 1 {
|
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);
|
building_height = building_height.max(3);
|
||||||
|
|
||||||
// Mark as tall building if more than 7 stories
|
// 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() {
|
for (x, z) in floor_area.iter().cloned() {
|
||||||
if processed_points.insert((x, z)) {
|
if processed_points.insert((x, z)) {
|
||||||
// Create foundation columns for the floor area when using terrain
|
// 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 {
|
if x % 5 == 0 && z % 5 == 0 {
|
||||||
// Light fixtures
|
// Light fixtures
|
||||||
editor.set_block_absolute(
|
editor.set_block_absolute(
|
||||||
GLOWSTONE,
|
ceiling_light_block,
|
||||||
x,
|
x,
|
||||||
h + abs_terrain_offset,
|
h + abs_terrain_offset,
|
||||||
z,
|
z,
|
||||||
@@ -593,7 +607,7 @@ pub fn generate_buildings(
|
|||||||
}
|
}
|
||||||
} else if x % 5 == 0 && z % 5 == 0 {
|
} else if x % 5 == 0 && z % 5 == 0 {
|
||||||
editor.set_block_absolute(
|
editor.set_block_absolute(
|
||||||
GLOWSTONE,
|
ceiling_light_block,
|
||||||
x,
|
x,
|
||||||
start_y_offset + building_height + abs_terrain_offset,
|
start_y_offset + building_height + abs_terrain_offset,
|
||||||
z,
|
z,
|
||||||
@@ -648,6 +662,7 @@ pub fn generate_buildings(
|
|||||||
args,
|
args,
|
||||||
element,
|
element,
|
||||||
abs_terrain_offset,
|
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
|
// Set base height for roof to be at least one block above building top
|
||||||
let base_height = start_y_offset + building_height + 1;
|
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 {
|
match roof_type {
|
||||||
RoofType::Flat => {
|
RoofType::Flat => {
|
||||||
// Simple flat roof
|
// Simple flat roof
|
||||||
@@ -798,8 +816,13 @@ fn generate_roof(
|
|||||||
let roof_peak_height = base_height + roof_height_boost;
|
let roof_peak_height = base_height + roof_height_boost;
|
||||||
|
|
||||||
// Pre-determine orientation and material
|
// Pre-determine orientation and material
|
||||||
let is_wider_than_long = width > length;
|
let width_is_longer = width >= length;
|
||||||
let max_distance = if is_wider_than_long {
|
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
|
length >> 1
|
||||||
} else {
|
} else {
|
||||||
width >> 1
|
width >> 1
|
||||||
@@ -819,15 +842,15 @@ fn generate_roof(
|
|||||||
|
|
||||||
// First pass: calculate all roof heights using vectorized operations
|
// First pass: calculate all roof heights using vectorized operations
|
||||||
for &(x, z) in floor_area {
|
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()
|
(z - center_z).abs()
|
||||||
} else {
|
} else {
|
||||||
(x - center_x).abs()
|
(x - center_x).abs()
|
||||||
};
|
};
|
||||||
|
|
||||||
let roof_height = if distance_to_ridge == 0
|
let roof_height = if distance_to_ridge == 0
|
||||||
&& ((is_wider_than_long && z == center_z)
|
&& ((ridge_runs_along_x && z == center_z)
|
||||||
|| (!is_wider_than_long && x == center_x))
|
|| (!ridge_runs_along_x && x == center_x))
|
||||||
{
|
{
|
||||||
roof_peak_height
|
roof_peak_height
|
||||||
} else {
|
} else {
|
||||||
@@ -859,7 +882,7 @@ fn generate_roof(
|
|||||||
for y in base_height..=roof_height {
|
for y in base_height..=roof_height {
|
||||||
if y == roof_height && has_lower_neighbor {
|
if y == roof_height && has_lower_neighbor {
|
||||||
// Pre-compute stair direction
|
// 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 {
|
if z < center_z {
|
||||||
create_stair_with_properties(
|
create_stair_with_properties(
|
||||||
stair_block_material,
|
stair_block_material,
|
||||||
@@ -919,7 +942,12 @@ fn generate_roof(
|
|||||||
// Determine if building is significantly rectangular or more square-shaped
|
// Determine if building is significantly rectangular or more square-shaped
|
||||||
let is_rectangular =
|
let is_rectangular =
|
||||||
(width as f64 / length as f64 > 1.3) || (length as f64 / width as f64 > 1.3);
|
(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
|
// Make roof taller and more pointy
|
||||||
let roof_peak_height = base_height + if width.max(length) > 20 { 7 } else { 5 };
|
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 {
|
for &(x, z) in floor_area {
|
||||||
// Calculate distance to the ridge line
|
// 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
|
// Distance in Z direction for X-axis ridge
|
||||||
(z - center_z).abs()
|
(z - center_z).abs()
|
||||||
} else {
|
} else {
|
||||||
@@ -948,7 +976,7 @@ fn generate_roof(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Calculate maximum distance from ridge to edge
|
// 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
|
(max_z - min_z) / 2
|
||||||
} else {
|
} else {
|
||||||
(max_x - min_x) / 2
|
(max_x - min_x) / 2
|
||||||
@@ -988,7 +1016,7 @@ fn generate_roof(
|
|||||||
if has_lower_neighbor {
|
if has_lower_neighbor {
|
||||||
// Determine stair direction based on ridge orientation and position
|
// Determine stair direction based on ridge orientation and position
|
||||||
let stair_block_material = get_stair_block_for_material(roof_block);
|
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
|
// Ridge runs along X, slopes in Z direction
|
||||||
if z < center_z {
|
if z < center_z {
|
||||||
create_stair_with_properties(
|
create_stair_with_properties(
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use crate::args::Args;
|
use crate::args::Args;
|
||||||
use crate::block_definitions::*;
|
use crate::block_definitions::*;
|
||||||
use crate::deterministic_rng::element_rng;
|
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::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
|
use rand::prelude::SliceRandom;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
pub fn generate_landuse(
|
pub fn generate_landuse(
|
||||||
@@ -58,6 +59,29 @@ pub fn generate_landuse(
|
|||||||
let floor_area: Vec<(i32, i32)> =
|
let floor_area: Vec<(i32, i32)> =
|
||||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
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 {
|
for (x, z) in floor_area {
|
||||||
// Apply per-block randomness for certain landuse types
|
// Apply per-block randomness for certain landuse types
|
||||||
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
|
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
|
||||||
@@ -141,7 +165,15 @@ pub fn generate_landuse(
|
|||||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||||
let random_choice: i32 = rng.gen_range(0..30);
|
let random_choice: i32 = rng.gen_range(0..30);
|
||||||
if random_choice == 20 {
|
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 {
|
} else if random_choice == 2 {
|
||||||
let flower_block: Block = match rng.gen_range(1..=5) {
|
let flower_block: Block = match rng.gen_range(1..=5) {
|
||||||
1 => OAK_LEAVES,
|
1 => OAK_LEAVES,
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ use crate::args::Args;
|
|||||||
use crate::block_definitions::*;
|
use crate::block_definitions::*;
|
||||||
use crate::bresenham::bresenham_line;
|
use crate::bresenham::bresenham_line;
|
||||||
use crate::deterministic_rng::element_rng;
|
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::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||||
use crate::world_editor::WorldEditor;
|
use crate::world_editor::WorldEditor;
|
||||||
|
use rand::prelude::SliceRandom;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
pub fn generate_natural(
|
pub fn generate_natural(
|
||||||
@@ -21,7 +22,66 @@ pub fn generate_natural(
|
|||||||
let x: i32 = node.x;
|
let x: i32 = node.x;
|
||||||
let z: i32 = node.z;
|
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 {
|
} else {
|
||||||
let mut previous_node: Option<(i32, i32)> = None;
|
let mut previous_node: Option<(i32, i32)> = None;
|
||||||
@@ -81,6 +141,29 @@ pub fn generate_natural(
|
|||||||
let filled_area: Vec<(i32, i32)> =
|
let filled_area: Vec<(i32, i32)> =
|
||||||
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
|
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
|
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||||
let mut rng = element_rng(way.id);
|
let mut rng = element_rng(way.id);
|
||||||
|
|
||||||
@@ -164,7 +247,15 @@ pub fn generate_natural(
|
|||||||
}
|
}
|
||||||
let random_choice: i32 = rng.gen_range(0..30);
|
let random_choice: i32 = rng.gen_range(0..30);
|
||||||
if random_choice == 0 {
|
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 {
|
} else if random_choice == 1 {
|
||||||
let flower_block = match rng.gen_range(1..=4) {
|
let flower_block = match rng.gen_range(1..=4) {
|
||||||
1 => RED_FLOWER,
|
1 => RED_FLOWER,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const INTERIOR1_LAYER2: [[char; 23]; 23] = [
|
|||||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
['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]
|
#[rustfmt::skip]
|
||||||
const INTERIOR2_LAYER1: [[char; 23]; 23] = [
|
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',],
|
['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',],
|
['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
|
/// Maps interior layout characters to actual block types for different floor layers
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option<Block> {
|
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)
|
Some(DARK_OAK_DOOR_LOWER)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'J' => Some(NOTE_BLOCK), // Note block
|
'J' => Some(NOTE_BLOCK), // Note block
|
||||||
'G' => Some(GLOWSTONE), // Glowstone
|
'G' => Some(GLOWSTONE), // Glowstone
|
||||||
'N' => Some(BREWING_STAND), // Brewing Stand
|
'N' => Some(BREWING_STAND), // Brewing Stand
|
||||||
'T' => Some(WHITE_CARPET), // White Carpet
|
'T' => Some(WHITE_CARPET), // White Carpet
|
||||||
'E' => Some(OAK_LEAVES), // Oak Leaves
|
'E' => Some(OAK_LEAVES), // Oak Leaves
|
||||||
_ => None, // Default case for unknown characters
|
'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,
|
args: &crate::args::Args,
|
||||||
element: &crate::osm_parser::ProcessedWay,
|
element: &crate::osm_parser::ProcessedWay,
|
||||||
abs_terrain_offset: i32,
|
abs_terrain_offset: i32,
|
||||||
|
is_abandoned_building: bool,
|
||||||
) {
|
) {
|
||||||
// Skip interior generation for very small buildings
|
// Skip interior generation for very small buildings
|
||||||
let width = max_x - min_x + 1;
|
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
|
// 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
|
// Ground floor uses INTERIOR1 patterns
|
||||||
(&INTERIOR1_LAYER1, &INTERIOR1_LAYER2)
|
(&INTERIOR1_LAYER1, &INTERIOR1_LAYER2)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ fn round(editor: &mut WorldEditor, material: Block, (x, y, z): Coord, block_patt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
pub enum TreeType {
|
pub enum TreeType {
|
||||||
Oak,
|
Oak,
|
||||||
Spruce,
|
Spruce,
|
||||||
@@ -120,6 +121,27 @@ impl Tree<'_> {
|
|||||||
editor: &mut WorldEditor,
|
editor: &mut WorldEditor,
|
||||||
(x, y, z): Coord,
|
(x, y, z): Coord,
|
||||||
building_footprints: Option<&BuildingFootprintBitmap>,
|
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
|
// Skip if this coordinate is inside a building
|
||||||
if let Some(footprints) = building_footprints {
|
if let Some(footprints) = building_footprints {
|
||||||
@@ -135,16 +157,7 @@ impl Tree<'_> {
|
|||||||
blacklist.extend(Self::get_functional_blocks());
|
blacklist.extend(Self::get_functional_blocks());
|
||||||
blacklist.push(WATER);
|
blacklist.push(WATER);
|
||||||
|
|
||||||
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
let tree = Self::get_tree(tree_type);
|
||||||
// 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!(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build the logs
|
// Build the logs
|
||||||
editor.fill_blocks(
|
editor.fill_blocks(
|
||||||
|
|||||||
4
src/gui/js/maps/wkt.parser.js
vendored
4
src/gui/js/maps/wkt.parser.js
vendored
@@ -195,11 +195,7 @@ Wkt.Wkt.prototype.toObject = function (config) {
|
|||||||
* Absorbs the geometry of another Wkt.Wkt instance, merging it with its own,
|
* 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.
|
* creating a collection (MULTI-geometry) based on their types, which must agree.
|
||||||
* For example, creates a MULTIPOLYGON from a POLYGON type merged with another
|
* 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.
|
* POLYGON type, or adds a POLYGON instance to a MULTIPOLYGON instance.
|
||||||
>>>>>>> dev
|
|
||||||
* @memberof Wkt.Wkt
|
* @memberof Wkt.Wkt
|
||||||
* @method
|
* @method
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,11 +14,14 @@ use crate::ground::Ground;
|
|||||||
use crate::progress::emit_gui_progress_update;
|
use crate::progress::emit_gui_progress_update;
|
||||||
|
|
||||||
use bedrockrs_level::level::db_interface::bedrock_key::ChunkKey;
|
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_level::level::file_interface::RawWorldTrait;
|
||||||
use bedrockrs_shared::world::dimension::Dimension;
|
use bedrockrs_shared::world::dimension::Dimension;
|
||||||
use byteorder::{LittleEndian, WriteBytesExt};
|
use byteorder::{LittleEndian, WriteBytesExt};
|
||||||
|
use fastnbt::Value;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
use rusty_leveldb::DB;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap as StdHashMap;
|
use std::collections::HashMap as StdHashMap;
|
||||||
use std::fs::{self, File};
|
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
|
/// Metadata for Bedrock worlds
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct BedrockMetadata {
|
struct BedrockMetadata {
|
||||||
@@ -402,7 +407,7 @@ impl BedrockWriter {
|
|||||||
// Open LevelDB with Bedrock-compatible options
|
// Open LevelDB with Bedrock-compatible options
|
||||||
let mut state = ();
|
let mut state = ();
|
||||||
let mut db: RustyDBInterface<()> =
|
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)))?;
|
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||||
|
|
||||||
// Count total chunks for progress
|
// Count total chunks for progress
|
||||||
@@ -416,63 +421,128 @@ impl BedrockWriter {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let progress_bar = ProgressBar::new(total_chunks as u64);
|
{
|
||||||
progress_bar.set_style(
|
let progress_bar = ProgressBar::new(total_chunks as u64);
|
||||||
ProgressStyle::default_bar()
|
progress_bar.set_style(
|
||||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} chunks ({eta})")
|
ProgressStyle::default_bar()
|
||||||
.unwrap()
|
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} chunks ({eta})")
|
||||||
.progress_chars("█▓░"),
|
.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 ®ion.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 (§ion_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 ((region_x, region_z), region) in &world.regions {
|
||||||
for ((local_chunk_x, local_chunk_z), chunk) in ®ion.chunks {
|
for ((local_chunk_x, local_chunk_z), chunk) in ®ion.chunks {
|
||||||
// Calculate absolute chunk coordinates
|
let chunk_pos =
|
||||||
let abs_chunk_x = region_x * 32 + local_chunk_x;
|
Vec2::new(region_x * 32 + local_chunk_x, region_z * 32 + local_chunk_z);
|
||||||
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+)
|
self.write_compound_list_record(
|
||||||
let version_key = ChunkKey::chunk_marker(chunk_pos, Dimension::Overworld);
|
&mut db,
|
||||||
db.set_subchunk_raw(version_key, &[42], &mut state)
|
chunk_pos,
|
||||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
KeyTypeTag::BlockEntity,
|
||||||
|
chunk.other.get("block_entities"),
|
||||||
// Write Data3D (heightmap + biomes) - required for chunk to be valid
|
)?;
|
||||||
let data3d_key = ChunkKey::data3d(chunk_pos, Dimension::Overworld);
|
self.write_compound_list_record(
|
||||||
let data3d = self.create_data3d(chunk);
|
&mut db,
|
||||||
db.set_subchunk_raw(data3d_key, &data3d, &mut state)
|
chunk_pos,
|
||||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
KeyTypeTag::Entity,
|
||||||
|
chunk.other.get("entities"),
|
||||||
// Process each section (subchunk)
|
)?;
|
||||||
for (§ion_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");
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// LevelDB writes are flushed when the database is dropped
|
fn write_compound_list_record(
|
||||||
drop(db);
|
&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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -737,6 +807,91 @@ fn bedrock_bits_per_block(palette_count: u32) -> u8 {
|
|||||||
16 // Maximum
|
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
|
/// Level.dat structure for Bedrock Edition
|
||||||
/// This struct contains all required fields for a valid Bedrock world
|
/// This struct contains all required fields for a valid Bedrock world
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
|
|||||||
@@ -174,40 +174,9 @@ impl<'a> WorldEditor<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve existing block entities and merge with new ones
|
// Preserve existing block entities and entities and merge with new ones
|
||||||
if let Some(existing_entities) = chunk.other.get_mut("block_entities") {
|
merge_compound_list(&mut chunk, chunk_to_modify, "block_entities");
|
||||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities") {
|
merge_compound_list(&mut chunk, chunk_to_modify, "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
|
// Update chunk coordinates and flags
|
||||||
chunk.x_pos = chunk_x + (region_x * 32);
|
chunk.x_pos = chunk_x + (region_x * 32);
|
||||||
@@ -246,87 +215,129 @@ impl<'a> WorldEditor<'a> {
|
|||||||
|
|
||||||
/// Helper function to get entity coordinates
|
/// Helper function to get entity coordinates
|
||||||
#[inline]
|
#[inline]
|
||||||
fn get_entity_coords(entity: &HashMap<String, Value>) -> (i32, i32, i32) {
|
fn get_entity_coords(entity: &HashMap<String, Value>) -> Option<(i32, i32, i32)> {
|
||||||
let x = if let Value::Int(x) = entity.get("x").unwrap_or(&Value::Int(0)) {
|
if let Some(Value::List(pos)) = entity.get("Pos") {
|
||||||
*x
|
if pos.len() == 3 {
|
||||||
} else {
|
if let (Some(x), Some(y), Some(z)) = (
|
||||||
0
|
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
|
Some((x, y, z))
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
let z = if let Value::Int(z) = entity.get("z").unwrap_or(&Value::Int(0)) {
|
|
||||||
*z
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
(x, y, z)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a Level wrapper for chunk data (Java Edition format)
|
/// Creates a Level wrapper for chunk data (Java Edition format)
|
||||||
#[inline]
|
#[inline]
|
||||||
fn create_level_wrapper(chunk: &Chunk) -> HashMap<String, Value> {
|
fn create_level_wrapper(chunk: &Chunk) -> HashMap<String, Value> {
|
||||||
HashMap::from([(
|
let mut level_map = HashMap::from([
|
||||||
"Level".to_string(),
|
("xPos".to_string(), Value::Int(chunk.x_pos)),
|
||||||
Value::Compound(HashMap::from([
|
("zPos".to_string(), Value::Int(chunk.z_pos)),
|
||||||
("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()),
|
||||||
"isLightOn".to_string(),
|
),
|
||||||
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
|
(
|
||||||
),
|
"sections".to_string(),
|
||||||
(
|
Value::List(
|
||||||
"sections".to_string(),
|
chunk
|
||||||
Value::List(
|
.sections
|
||||||
chunk
|
.iter()
|
||||||
.sections
|
.map(|section| {
|
||||||
.iter()
|
let mut block_states = HashMap::from([(
|
||||||
.map(|section| {
|
"palette".to_string(),
|
||||||
let mut block_states = HashMap::from([(
|
Value::List(
|
||||||
"palette".to_string(),
|
section
|
||||||
Value::List(
|
.block_states
|
||||||
section
|
.palette
|
||||||
.block_states
|
.iter()
|
||||||
.palette
|
.map(|item| {
|
||||||
.iter()
|
let mut palette_item = HashMap::from([(
|
||||||
.map(|item| {
|
"Name".to_string(),
|
||||||
let mut palette_item = HashMap::from([(
|
Value::String(item.name.clone()),
|
||||||
"Name".to_string(),
|
)]);
|
||||||
Value::String(item.name.clone()),
|
if let Some(props) = &item.properties {
|
||||||
)]);
|
palette_item
|
||||||
if let Some(props) = &item.properties {
|
.insert("Properties".to_string(), props.clone());
|
||||||
palette_item.insert(
|
}
|
||||||
"Properties".to_string(),
|
Value::Compound(palette_item)
|
||||||
props.clone(),
|
})
|
||||||
);
|
.collect(),
|
||||||
}
|
),
|
||||||
Value::Compound(palette_item)
|
)]);
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
)]);
|
|
||||||
|
|
||||||
// Only add the `data` attribute if it's non-empty
|
// Only add the `data` attribute if it's non-empty
|
||||||
// to maintain compatibility with third-party tools like Dynmap
|
// to maintain compatibility with third-party tools like Dynmap
|
||||||
if let Some(data) = §ion.block_states.data {
|
if let Some(data) = §ion.block_states.data {
|
||||||
if !data.is_empty() {
|
if !data.is_empty() {
|
||||||
block_states.insert(
|
block_states
|
||||||
"data".to_string(),
|
.insert("data".to_string(), Value::LongArray(data.to_owned()));
|
||||||
Value::LongArray(data.to_owned()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Value::Compound(HashMap::from([
|
Value::Compound(HashMap::from([
|
||||||
("Y".to_string(), Value::Byte(section.y)),
|
("Y".to_string(), Value::Byte(section.y)),
|
||||||
("block_states".to_string(), Value::Compound(block_states)),
|
("block_states".to_string(), Value::Compound(block_states)),
|
||||||
]))
|
]))
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
])),
|
),
|
||||||
)])
|
]);
|
||||||
|
|
||||||
|
for (key, value) in &chunk.other {
|
||||||
|
level_map.insert(key.clone(), value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
HashMap::from([("Level".to_string(), Value::Compound(level_map))])
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ use crate::coordinate_system::geographic::LLBBox;
|
|||||||
use crate::ground::Ground;
|
use crate::ground::Ground;
|
||||||
use crate::progress::emit_gui_progress_update;
|
use crate::progress::emit_gui_progress_update;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use fastnbt::Value;
|
use fastnbt::{IntArray, Value};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::{hash_map::Entry, HashMap};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -243,6 +243,212 @@ impl<'a> WorldEditor<'a> {
|
|||||||
self.set_block(SIGN, x, y, z, None, None);
|
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.
|
/// Sets a block of the specified type at the given coordinates.
|
||||||
///
|
///
|
||||||
/// Y value is interpreted as an offset from ground level.
|
/// Y value is interpreted as an offset from ground level.
|
||||||
@@ -599,3 +805,30 @@ impl<'a> WorldEditor<'a> {
|
|||||||
Ok(())
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user