mirror of
https://github.com/louis-e/arnis.git
synced 2026-01-29 08:23:18 -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",
|
||||
"reqwest",
|
||||
"rfd",
|
||||
"rusty-leveldb",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -15,7 +15,7 @@ overflow-checks = true
|
||||
[features]
|
||||
default = ["gui"]
|
||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
|
||||
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek"]
|
||||
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek", "rusty-leveldb"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = {version = "2", optional = true}
|
||||
@@ -54,6 +54,7 @@ bedrockrs_shared = { git = "https://github.com/bedrock-crustaceans/bedrock-rs",
|
||||
nbtx = { git = "https://github.com/bedrock-crustaceans/nbtx", optional = true }
|
||||
vek = { version = "0.17", optional = true }
|
||||
zip = { version = "0.6", default-features = false, features = ["deflate"], optional = true }
|
||||
rusty-leveldb = { version = "3", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
||||
|
||||
Binary file not shown.
@@ -578,6 +578,11 @@ pub fn to_bedrock_block_with_properties(
|
||||
return convert_stairs(java_name, props_map);
|
||||
}
|
||||
|
||||
// Handle barrel facing direction
|
||||
if java_name == "barrel" {
|
||||
return convert_barrel(java_name, props_map);
|
||||
}
|
||||
|
||||
// Handle slabs with type property (top/bottom/double)
|
||||
if java_name.ends_with("_slab") {
|
||||
return convert_slab(java_name, props_map);
|
||||
@@ -650,6 +655,46 @@ fn convert_stairs(
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Java barrel to Bedrock format with facing direction.
|
||||
fn convert_barrel(
|
||||
java_name: &str,
|
||||
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||
) -> BedrockBlock {
|
||||
let mut states = HashMap::new();
|
||||
|
||||
if let Some(props) = props {
|
||||
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
|
||||
let facing_direction = match facing.as_str() {
|
||||
"down" => 0,
|
||||
"up" => 1,
|
||||
"north" => 2,
|
||||
"south" => 3,
|
||||
"west" => 4,
|
||||
"east" => 5,
|
||||
_ => 1,
|
||||
};
|
||||
states.insert(
|
||||
"facing_direction".to_string(),
|
||||
BedrockBlockStateValue::Int(facing_direction),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !states.contains_key("facing_direction") {
|
||||
states.insert(
|
||||
"facing_direction".to_string(),
|
||||
BedrockBlockStateValue::Int(1),
|
||||
);
|
||||
}
|
||||
|
||||
states.insert("open_bit".to_string(), BedrockBlockStateValue::Bool(false));
|
||||
|
||||
BedrockBlock {
|
||||
name: format!("minecraft:{java_name}"),
|
||||
states,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Java slab block to Bedrock format with proper type.
|
||||
fn convert_slab(
|
||||
java_name: &str,
|
||||
|
||||
@@ -266,7 +266,15 @@ impl Block {
|
||||
185 => "quartz_stairs",
|
||||
186 => "polished_andesite_stairs",
|
||||
187 => "nether_brick_stairs",
|
||||
188 => "fern",
|
||||
188 => "barrel",
|
||||
189 => "fern",
|
||||
190 => "cobweb",
|
||||
191 => "chiseled_bookshelf",
|
||||
192 => "chiseled_bookshelf",
|
||||
193 => "chiseled_bookshelf",
|
||||
194 => "chiseled_bookshelf",
|
||||
195 => "chipped_anvil",
|
||||
196 => "damaged_anvil",
|
||||
_ => panic!("Invalid id"),
|
||||
}
|
||||
}
|
||||
@@ -464,6 +472,26 @@ impl Block {
|
||||
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||
map
|
||||
})),
|
||||
191 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("north".to_string()));
|
||||
map
|
||||
})),
|
||||
192 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("east".to_string()));
|
||||
map
|
||||
})),
|
||||
193 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("south".to_string()));
|
||||
map
|
||||
})),
|
||||
194 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("west".to_string()));
|
||||
map
|
||||
})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -698,7 +726,17 @@ pub const SMOOTH_SANDSTONE_STAIRS: Block = Block::new(184);
|
||||
pub const QUARTZ_STAIRS: Block = Block::new(185);
|
||||
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
|
||||
pub const NETHER_BRICK_STAIRS: Block = Block::new(187);
|
||||
pub const FERN: Block = Block::new(188);
|
||||
pub const BARREL: Block = Block::new(188);
|
||||
pub const FERN: Block = Block::new(189);
|
||||
pub const COBWEB: Block = Block::new(190);
|
||||
pub const CHISELLED_BOOKSHELF_NORTH: Block = Block::new(191);
|
||||
pub const CHISELLED_BOOKSHELF_EAST: Block = Block::new(192);
|
||||
pub const CHISELLED_BOOKSHELF_SOUTH: Block = Block::new(193);
|
||||
pub const CHISELLED_BOOKSHELF_WEST: Block = Block::new(194);
|
||||
// Backwards-compatible alias (defaults to north-facing)
|
||||
pub const CHISELLED_BOOKSHELF: Block = CHISELLED_BOOKSHELF_NORTH;
|
||||
pub const CHIPPED_ANVIL: Block = Block::new(195);
|
||||
pub const DAMAGED_ANVIL: Block = Block::new(196);
|
||||
|
||||
/// Maps a block to its corresponding stair variant
|
||||
#[inline]
|
||||
|
||||
@@ -7,7 +7,9 @@ use crate::floodfill::flood_fill_area; // Needed for inline amenity flood fills
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
use fastnbt::Value;
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub fn generate_amenities(
|
||||
editor: &mut WorldEditor,
|
||||
@@ -34,6 +36,49 @@ pub fn generate_amenities(
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| XZPoint::new(n.x, n.z))
|
||||
.next();
|
||||
match amenity_type.as_str() {
|
||||
"recycling" => {
|
||||
let is_container = element
|
||||
.tags()
|
||||
.get("recycling_type")
|
||||
.is_some_and(|value| value == "container");
|
||||
|
||||
if !is_container {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(pt) = first_node {
|
||||
let mut rng = rand::thread_rng();
|
||||
let loot_pool = build_recycling_loot_pool(element.tags());
|
||||
let items = build_recycling_items(&loot_pool, &mut rng);
|
||||
|
||||
let properties = Value::Compound(recycling_barrel_properties());
|
||||
let barrel_block = BlockWithProperties::new(BARREL, Some(properties));
|
||||
let absolute_y = editor.get_absolute_y(pt.x, 1, pt.z);
|
||||
|
||||
editor.set_block_entity_with_items(
|
||||
barrel_block,
|
||||
pt.x,
|
||||
1,
|
||||
pt.z,
|
||||
"minecraft:barrel",
|
||||
items,
|
||||
);
|
||||
|
||||
if let Some(category) = single_loot_category(&loot_pool) {
|
||||
if let Some(display_item) =
|
||||
build_display_item_for_category(category, &mut rng)
|
||||
{
|
||||
place_item_frame_on_random_side(
|
||||
editor,
|
||||
pt.x,
|
||||
absolute_y,
|
||||
pt.z,
|
||||
display_item,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"waste_disposal" | "waste_basket" => {
|
||||
// Place a cauldron for waste disposal or waste basket
|
||||
if let Some(pt) = first_node {
|
||||
@@ -263,3 +308,420 @@ pub fn generate_amenities(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum RecyclingLootKind {
|
||||
GlassBottle,
|
||||
Paper,
|
||||
GlassBlock,
|
||||
GlassPane,
|
||||
LeatherArmor,
|
||||
EmptyBucket,
|
||||
LeatherBoots,
|
||||
ScrapMetal,
|
||||
GreenWaste,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum LeatherPiece {
|
||||
Helmet,
|
||||
Chestplate,
|
||||
Leggings,
|
||||
Boots,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum LootCategory {
|
||||
GlassBottle,
|
||||
Paper,
|
||||
Glass,
|
||||
Leather,
|
||||
EmptyBucket,
|
||||
ScrapMetal,
|
||||
GreenWaste,
|
||||
}
|
||||
|
||||
fn recycling_barrel_properties() -> HashMap<String, Value> {
|
||||
let mut props = HashMap::new();
|
||||
props.insert("facing".to_string(), Value::String("up".to_string()));
|
||||
props
|
||||
}
|
||||
|
||||
fn build_recycling_loot_pool(tags: &HashMap<String, String>) -> Vec<RecyclingLootKind> {
|
||||
let mut loot_pool: Vec<RecyclingLootKind> = Vec::new();
|
||||
|
||||
if tag_enabled(tags, "recycling:glass_bottles") {
|
||||
loot_pool.push(RecyclingLootKind::GlassBottle);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:paper") {
|
||||
loot_pool.push(RecyclingLootKind::Paper);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:glass") {
|
||||
loot_pool.push(RecyclingLootKind::GlassBlock);
|
||||
loot_pool.push(RecyclingLootKind::GlassPane);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:clothes") {
|
||||
loot_pool.push(RecyclingLootKind::LeatherArmor);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:cans") {
|
||||
loot_pool.push(RecyclingLootKind::EmptyBucket);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:shoes") {
|
||||
loot_pool.push(RecyclingLootKind::LeatherBoots);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:scrap_metal") {
|
||||
loot_pool.push(RecyclingLootKind::ScrapMetal);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:green_waste") {
|
||||
loot_pool.push(RecyclingLootKind::GreenWaste);
|
||||
}
|
||||
|
||||
loot_pool
|
||||
}
|
||||
|
||||
fn build_recycling_items(
|
||||
loot_pool: &[RecyclingLootKind],
|
||||
rng: &mut impl Rng,
|
||||
) -> Vec<HashMap<String, Value>> {
|
||||
if loot_pool.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut items = Vec::new();
|
||||
for slot in 0..27 {
|
||||
if rng.gen_bool(0.2) {
|
||||
let kind = loot_pool[rng.gen_range(0..loot_pool.len())];
|
||||
if let Some(item) = build_item_for_kind(kind, slot as i8, rng) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
fn kind_to_category(kind: RecyclingLootKind) -> LootCategory {
|
||||
match kind {
|
||||
RecyclingLootKind::GlassBottle => LootCategory::GlassBottle,
|
||||
RecyclingLootKind::Paper => LootCategory::Paper,
|
||||
RecyclingLootKind::GlassBlock | RecyclingLootKind::GlassPane => LootCategory::Glass,
|
||||
RecyclingLootKind::LeatherArmor | RecyclingLootKind::LeatherBoots => LootCategory::Leather,
|
||||
RecyclingLootKind::EmptyBucket => LootCategory::EmptyBucket,
|
||||
RecyclingLootKind::ScrapMetal => LootCategory::ScrapMetal,
|
||||
RecyclingLootKind::GreenWaste => LootCategory::GreenWaste,
|
||||
}
|
||||
}
|
||||
|
||||
fn single_loot_category(loot_pool: &[RecyclingLootKind]) -> Option<LootCategory> {
|
||||
let mut categories: HashSet<LootCategory> = HashSet::new();
|
||||
for kind in loot_pool {
|
||||
categories.insert(kind_to_category(*kind));
|
||||
if categories.len() > 1 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
categories.iter().next().copied()
|
||||
}
|
||||
|
||||
fn build_display_item_for_category(
|
||||
category: LootCategory,
|
||||
rng: &mut impl Rng,
|
||||
) -> Option<HashMap<String, Value>> {
|
||||
match category {
|
||||
LootCategory::GlassBottle => Some(make_display_item("minecraft:glass_bottle", 1)),
|
||||
LootCategory::Paper => Some(make_display_item("minecraft:paper", rng.gen_range(1..=4))),
|
||||
LootCategory::Glass => Some(make_display_item("minecraft:glass", 1)),
|
||||
LootCategory::Leather => Some(build_leather_display_item(rng)),
|
||||
LootCategory::EmptyBucket => Some(make_display_item("minecraft:bucket", 1)),
|
||||
LootCategory::ScrapMetal => {
|
||||
let metals = [
|
||||
"minecraft:copper_ingot",
|
||||
"minecraft:iron_ingot",
|
||||
"minecraft:gold_ingot",
|
||||
];
|
||||
let metal = metals.choose(rng)?;
|
||||
Some(make_display_item(metal, rng.gen_range(1..=2)))
|
||||
}
|
||||
LootCategory::GreenWaste => {
|
||||
let options = [
|
||||
"minecraft:oak_sapling",
|
||||
"minecraft:birch_sapling",
|
||||
"minecraft:tall_grass",
|
||||
"minecraft:sweet_berries",
|
||||
"minecraft:wheat_seeds",
|
||||
];
|
||||
let choice = options.choose(rng)?;
|
||||
Some(make_display_item(choice, rng.gen_range(1..=3)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn place_item_frame_on_random_side(
|
||||
editor: &mut WorldEditor,
|
||||
x: i32,
|
||||
barrel_absolute_y: i32,
|
||||
z: i32,
|
||||
item: HashMap<String, Value>,
|
||||
) {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut directions = [
|
||||
((0, 0, -1), 2), // North
|
||||
((0, 0, 1), 3), // South
|
||||
((-1, 0, 0), 4), // West
|
||||
((1, 0, 0), 5), // East
|
||||
];
|
||||
directions.shuffle(&mut rng);
|
||||
|
||||
let (min_x, min_z) = editor.get_min_coords();
|
||||
let (max_x, max_z) = editor.get_max_coords();
|
||||
|
||||
let ((dx, _dy, dz), facing) = directions
|
||||
.into_iter()
|
||||
.find(|((dx, _dy, dz), _)| {
|
||||
let target_x = x + dx;
|
||||
let target_z = z + dz;
|
||||
target_x >= min_x && target_x <= max_x && target_z >= min_z && target_z <= max_z
|
||||
})
|
||||
.unwrap_or(((0, 0, 1), 3)); // Fallback south if all directions are out of bounds
|
||||
|
||||
let target_x = x + dx;
|
||||
let target_y = barrel_absolute_y;
|
||||
let target_z = z + dz;
|
||||
|
||||
let ground_y = editor.get_absolute_y(target_x, 0, target_z);
|
||||
|
||||
let mut extra = HashMap::new();
|
||||
extra.insert("Facing".to_string(), Value::Byte(facing)); // 2=north, 3=south, 4=west, 5=east
|
||||
extra.insert("ItemRotation".to_string(), Value::Byte(0));
|
||||
extra.insert("Item".to_string(), Value::Compound(item));
|
||||
extra.insert("ItemDropChance".to_string(), Value::Float(1.0));
|
||||
extra.insert(
|
||||
"block_pos".to_string(),
|
||||
Value::List(vec![
|
||||
Value::Int(target_x),
|
||||
Value::Int(target_y),
|
||||
Value::Int(target_z),
|
||||
]),
|
||||
);
|
||||
extra.insert("TileX".to_string(), Value::Int(target_x));
|
||||
extra.insert("TileY".to_string(), Value::Int(target_y));
|
||||
extra.insert("TileZ".to_string(), Value::Int(target_z));
|
||||
extra.insert("Fixed".to_string(), Value::Byte(1));
|
||||
|
||||
let relative_y = target_y - ground_y;
|
||||
editor.add_entity(
|
||||
"minecraft:item_frame",
|
||||
target_x,
|
||||
relative_y,
|
||||
target_z,
|
||||
Some(extra),
|
||||
);
|
||||
}
|
||||
|
||||
fn make_display_item(id: &str, count: i8) -> HashMap<String, Value> {
|
||||
let mut item = HashMap::new();
|
||||
item.insert("id".to_string(), Value::String(id.to_string()));
|
||||
item.insert("Count".to_string(), Value::Byte(count));
|
||||
item
|
||||
}
|
||||
|
||||
fn build_leather_display_item(rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
let mut item = make_display_item("minecraft:leather_chestplate", 1);
|
||||
let damage = biased_damage(80, rng);
|
||||
|
||||
let mut tag = HashMap::new();
|
||||
tag.insert("Damage".to_string(), Value::Int(damage));
|
||||
|
||||
if let Some(color) = maybe_leather_color(rng) {
|
||||
let mut display = HashMap::new();
|
||||
display.insert("color".to_string(), Value::Int(color));
|
||||
tag.insert("display".to_string(), Value::Compound(display));
|
||||
}
|
||||
|
||||
item.insert("tag".to_string(), Value::Compound(tag));
|
||||
|
||||
let mut components = HashMap::new();
|
||||
components.insert("minecraft:damage".to_string(), Value::Int(damage));
|
||||
item.insert("components".to_string(), Value::Compound(components));
|
||||
|
||||
item
|
||||
}
|
||||
|
||||
fn build_item_for_kind(
|
||||
kind: RecyclingLootKind,
|
||||
slot: i8,
|
||||
rng: &mut impl Rng,
|
||||
) -> Option<HashMap<String, Value>> {
|
||||
match kind {
|
||||
RecyclingLootKind::GlassBottle => Some(make_basic_item(
|
||||
"minecraft:glass_bottle",
|
||||
slot,
|
||||
rng.gen_range(1..=4),
|
||||
)),
|
||||
RecyclingLootKind::Paper => Some(make_basic_item(
|
||||
"minecraft:paper",
|
||||
slot,
|
||||
rng.gen_range(1..=10),
|
||||
)),
|
||||
RecyclingLootKind::GlassBlock => Some(build_glass_item(false, slot, rng)),
|
||||
RecyclingLootKind::GlassPane => Some(build_glass_item(true, slot, rng)),
|
||||
RecyclingLootKind::LeatherArmor => {
|
||||
Some(build_leather_item(random_leather_piece(rng), slot, rng))
|
||||
}
|
||||
RecyclingLootKind::EmptyBucket => Some(make_basic_item("minecraft:bucket", slot, 1)),
|
||||
RecyclingLootKind::LeatherBoots => Some(build_leather_item(LeatherPiece::Boots, slot, rng)),
|
||||
RecyclingLootKind::ScrapMetal => Some(build_scrap_metal_item(slot, rng)),
|
||||
RecyclingLootKind::GreenWaste => Some(build_green_waste_item(slot, rng)),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_scrap_metal_item(slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
let metals = ["copper_ingot", "iron_ingot", "gold_ingot"];
|
||||
let metal = metals.choose(rng).expect("scrap metal list is non-empty");
|
||||
let count = rng.gen_range(1..=3);
|
||||
make_basic_item(&format!("minecraft:{metal}"), slot, count)
|
||||
}
|
||||
|
||||
fn build_green_waste_item(slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
#[allow(clippy::match_same_arms)]
|
||||
let (id, count) = match rng.gen_range(0..8) {
|
||||
0 => ("minecraft:tall_grass", rng.gen_range(1..=4)),
|
||||
1 => ("minecraft:sweet_berries", rng.gen_range(2..=6)),
|
||||
2 => ("minecraft:oak_sapling", rng.gen_range(1..=2)),
|
||||
3 => ("minecraft:birch_sapling", rng.gen_range(1..=2)),
|
||||
4 => ("minecraft:spruce_sapling", rng.gen_range(1..=2)),
|
||||
5 => ("minecraft:jungle_sapling", rng.gen_range(1..=2)),
|
||||
6 => ("minecraft:acacia_sapling", rng.gen_range(1..=2)),
|
||||
_ => ("minecraft:dark_oak_sapling", rng.gen_range(1..=2)),
|
||||
};
|
||||
|
||||
// 25% chance to replace with seeds instead
|
||||
let id = if rng.gen_bool(0.25) {
|
||||
match rng.gen_range(0..4) {
|
||||
0 => "minecraft:wheat_seeds",
|
||||
1 => "minecraft:pumpkin_seeds",
|
||||
2 => "minecraft:melon_seeds",
|
||||
_ => "minecraft:beetroot_seeds",
|
||||
}
|
||||
} else {
|
||||
id
|
||||
};
|
||||
|
||||
make_basic_item(id, slot, count)
|
||||
}
|
||||
|
||||
fn build_glass_item(is_pane: bool, slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
const GLASS_COLORS: &[&str] = &[
|
||||
"white",
|
||||
"orange",
|
||||
"magenta",
|
||||
"light_blue",
|
||||
"yellow",
|
||||
"lime",
|
||||
"pink",
|
||||
"gray",
|
||||
"light_gray",
|
||||
"cyan",
|
||||
"purple",
|
||||
"blue",
|
||||
"brown",
|
||||
"green",
|
||||
"red",
|
||||
"black",
|
||||
];
|
||||
|
||||
let use_colorless = rng.gen_bool(0.7);
|
||||
|
||||
let id = if use_colorless {
|
||||
if is_pane {
|
||||
"minecraft:glass_pane".to_string()
|
||||
} else {
|
||||
"minecraft:glass".to_string()
|
||||
}
|
||||
} else {
|
||||
let color = GLASS_COLORS
|
||||
.choose(rng)
|
||||
.expect("glass color array is non-empty");
|
||||
if is_pane {
|
||||
format!("minecraft:{color}_stained_glass_pane")
|
||||
} else {
|
||||
format!("minecraft:{color}_stained_glass")
|
||||
}
|
||||
};
|
||||
|
||||
let count = if is_pane {
|
||||
rng.gen_range(4..=16)
|
||||
} else {
|
||||
rng.gen_range(1..=6)
|
||||
};
|
||||
|
||||
make_basic_item(&id, slot, count)
|
||||
}
|
||||
|
||||
fn build_leather_item(piece: LeatherPiece, slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
let (id, max_damage) = match piece {
|
||||
LeatherPiece::Helmet => ("minecraft:leather_helmet", 55),
|
||||
LeatherPiece::Chestplate => ("minecraft:leather_chestplate", 80),
|
||||
LeatherPiece::Leggings => ("minecraft:leather_leggings", 75),
|
||||
LeatherPiece::Boots => ("minecraft:leather_boots", 65),
|
||||
};
|
||||
|
||||
let mut item = make_basic_item(id, slot, 1);
|
||||
let damage = biased_damage(max_damage, rng);
|
||||
|
||||
let mut tag = HashMap::new();
|
||||
tag.insert("Damage".to_string(), Value::Int(damage));
|
||||
|
||||
if let Some(color) = maybe_leather_color(rng) {
|
||||
let mut display = HashMap::new();
|
||||
display.insert("color".to_string(), Value::Int(color));
|
||||
tag.insert("display".to_string(), Value::Compound(display));
|
||||
}
|
||||
|
||||
item.insert("tag".to_string(), Value::Compound(tag));
|
||||
|
||||
let mut components = HashMap::new();
|
||||
components.insert("minecraft:damage".to_string(), Value::Int(damage));
|
||||
item.insert("components".to_string(), Value::Compound(components));
|
||||
|
||||
item
|
||||
}
|
||||
|
||||
fn biased_damage(max_damage: i32, rng: &mut impl Rng) -> i32 {
|
||||
let safe_max = max_damage.max(1);
|
||||
let upper = safe_max.saturating_sub(1);
|
||||
let lower = (safe_max / 2).min(upper);
|
||||
|
||||
let heavy_wear = rng.gen_range(lower..=upper);
|
||||
let random_wear = rng.gen_range(0..=upper);
|
||||
heavy_wear.max(random_wear)
|
||||
}
|
||||
|
||||
fn maybe_leather_color(rng: &mut impl Rng) -> Option<i32> {
|
||||
if rng.gen_bool(0.3) {
|
||||
Some(rng.gen_range(0..=0x00FF_FFFF))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn random_leather_piece(rng: &mut impl Rng) -> LeatherPiece {
|
||||
match rng.gen_range(0..4) {
|
||||
0 => LeatherPiece::Helmet,
|
||||
1 => LeatherPiece::Chestplate,
|
||||
2 => LeatherPiece::Leggings,
|
||||
_ => LeatherPiece::Boots,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_basic_item(id: &str, slot: i8, count: i8) -> HashMap<String, Value> {
|
||||
let mut item = HashMap::new();
|
||||
item.insert("id".to_string(), Value::String(id.to_string()));
|
||||
item.insert("Slot".to_string(), Value::Byte(slot));
|
||||
item.insert("Count".to_string(), Value::Byte(count));
|
||||
item
|
||||
}
|
||||
|
||||
fn tag_enabled(tags: &HashMap<String, String>, key: &str) -> bool {
|
||||
tags.get(key).is_some_and(|value| value == "yes")
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ pub fn generate_buildings(
|
||||
let lev = levels - min_level;
|
||||
|
||||
if lev >= 1 {
|
||||
building_height = multiply_scale(levels * 4 + 2, scale_factor);
|
||||
building_height = multiply_scale(lev * 4 + 2, scale_factor);
|
||||
building_height = building_height.max(3);
|
||||
|
||||
// Mark as tall building if more than 7 stories
|
||||
@@ -542,6 +542,20 @@ pub fn generate_buildings(
|
||||
}
|
||||
}
|
||||
|
||||
// Detect abandoned buildings via explicit tags
|
||||
let is_abandoned_building = element
|
||||
.tags
|
||||
.get("abandoned")
|
||||
.is_some_and(|value| value == "yes")
|
||||
|| element.tags.contains_key("abandoned:building");
|
||||
|
||||
// Use cobwebs instead of glowstone for abandoned buildings
|
||||
let ceiling_light_block = if is_abandoned_building {
|
||||
COBWEB
|
||||
} else {
|
||||
GLOWSTONE
|
||||
};
|
||||
|
||||
for (x, z) in floor_area.iter().cloned() {
|
||||
if processed_points.insert((x, z)) {
|
||||
// Create foundation columns for the floor area when using terrain
|
||||
@@ -573,7 +587,7 @@ pub fn generate_buildings(
|
||||
if x % 5 == 0 && z % 5 == 0 {
|
||||
// Light fixtures
|
||||
editor.set_block_absolute(
|
||||
GLOWSTONE,
|
||||
ceiling_light_block,
|
||||
x,
|
||||
h + abs_terrain_offset,
|
||||
z,
|
||||
@@ -593,7 +607,7 @@ pub fn generate_buildings(
|
||||
}
|
||||
} else if x % 5 == 0 && z % 5 == 0 {
|
||||
editor.set_block_absolute(
|
||||
GLOWSTONE,
|
||||
ceiling_light_block,
|
||||
x,
|
||||
start_y_offset + building_height + abs_terrain_offset,
|
||||
z,
|
||||
@@ -648,6 +662,7 @@ pub fn generate_buildings(
|
||||
args,
|
||||
element,
|
||||
abs_terrain_offset,
|
||||
is_abandoned_building,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -772,6 +787,9 @@ fn generate_roof(
|
||||
// Set base height for roof to be at least one block above building top
|
||||
let base_height = start_y_offset + building_height + 1;
|
||||
|
||||
// Optional OSM hint for ridge orientation
|
||||
let roof_orientation = element.tags.get("roof:orientation").map(|s| s.as_str());
|
||||
|
||||
match roof_type {
|
||||
RoofType::Flat => {
|
||||
// Simple flat roof
|
||||
@@ -798,8 +816,13 @@ fn generate_roof(
|
||||
let roof_peak_height = base_height + roof_height_boost;
|
||||
|
||||
// Pre-determine orientation and material
|
||||
let is_wider_than_long = width > length;
|
||||
let max_distance = if is_wider_than_long {
|
||||
let width_is_longer = width >= length;
|
||||
let ridge_runs_along_x = match roof_orientation {
|
||||
Some(orientation) if orientation.eq_ignore_ascii_case("along") => width_is_longer,
|
||||
Some(orientation) if orientation.eq_ignore_ascii_case("across") => !width_is_longer,
|
||||
_ => width_is_longer,
|
||||
};
|
||||
let max_distance = if ridge_runs_along_x {
|
||||
length >> 1
|
||||
} else {
|
||||
width >> 1
|
||||
@@ -819,15 +842,15 @@ fn generate_roof(
|
||||
|
||||
// First pass: calculate all roof heights using vectorized operations
|
||||
for &(x, z) in floor_area {
|
||||
let distance_to_ridge = if is_wider_than_long {
|
||||
let distance_to_ridge = if ridge_runs_along_x {
|
||||
(z - center_z).abs()
|
||||
} else {
|
||||
(x - center_x).abs()
|
||||
};
|
||||
|
||||
let roof_height = if distance_to_ridge == 0
|
||||
&& ((is_wider_than_long && z == center_z)
|
||||
|| (!is_wider_than_long && x == center_x))
|
||||
&& ((ridge_runs_along_x && z == center_z)
|
||||
|| (!ridge_runs_along_x && x == center_x))
|
||||
{
|
||||
roof_peak_height
|
||||
} else {
|
||||
@@ -859,7 +882,7 @@ fn generate_roof(
|
||||
for y in base_height..=roof_height {
|
||||
if y == roof_height && has_lower_neighbor {
|
||||
// Pre-compute stair direction
|
||||
let stair_block_with_props = if is_wider_than_long {
|
||||
let stair_block_with_props = if ridge_runs_along_x {
|
||||
if z < center_z {
|
||||
create_stair_with_properties(
|
||||
stair_block_material,
|
||||
@@ -919,7 +942,12 @@ fn generate_roof(
|
||||
// Determine if building is significantly rectangular or more square-shaped
|
||||
let is_rectangular =
|
||||
(width as f64 / length as f64 > 1.3) || (length as f64 / width as f64 > 1.3);
|
||||
let long_axis_is_x = width > length;
|
||||
let width_is_longer = width >= length;
|
||||
let ridge_axis_is_x = match roof_orientation {
|
||||
Some(orientation) if orientation.eq_ignore_ascii_case("along") => width_is_longer,
|
||||
Some(orientation) if orientation.eq_ignore_ascii_case("across") => !width_is_longer,
|
||||
_ => width_is_longer,
|
||||
};
|
||||
|
||||
// Make roof taller and more pointy
|
||||
let roof_peak_height = base_height + if width.max(length) > 20 { 7 } else { 5 };
|
||||
@@ -939,7 +967,7 @@ fn generate_roof(
|
||||
|
||||
for &(x, z) in floor_area {
|
||||
// Calculate distance to the ridge line
|
||||
let distance_to_ridge = if long_axis_is_x {
|
||||
let distance_to_ridge = if ridge_axis_is_x {
|
||||
// Distance in Z direction for X-axis ridge
|
||||
(z - center_z).abs()
|
||||
} else {
|
||||
@@ -948,7 +976,7 @@ fn generate_roof(
|
||||
};
|
||||
|
||||
// Calculate maximum distance from ridge to edge
|
||||
let max_distance_from_ridge = if long_axis_is_x {
|
||||
let max_distance_from_ridge = if ridge_axis_is_x {
|
||||
(max_z - min_z) / 2
|
||||
} else {
|
||||
(max_x - min_x) / 2
|
||||
@@ -988,7 +1016,7 @@ fn generate_roof(
|
||||
if has_lower_neighbor {
|
||||
// Determine stair direction based on ridge orientation and position
|
||||
let stair_block_material = get_stair_block_for_material(roof_block);
|
||||
let stair_block_with_props = if long_axis_is_x {
|
||||
let stair_block_with_props = if ridge_axis_is_x {
|
||||
// Ridge runs along X, slopes in Z direction
|
||||
if z < center_z {
|
||||
create_stair_with_properties(
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::Tree;
|
||||
use crate::element_processing::tree::{Tree, TreeType};
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_landuse(
|
||||
@@ -58,6 +59,29 @@ pub fn generate_landuse(
|
||||
let floor_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
|
||||
let trees_ok_to_generate: Vec<TreeType> = {
|
||||
let mut trees: Vec<TreeType> = vec![];
|
||||
if let Some(leaf_type) = element.tags.get("leaf_type") {
|
||||
match leaf_type.as_str() {
|
||||
"broadleaved" => {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
"needleleaved" => trees.push(TreeType::Spruce),
|
||||
_ => {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Spruce);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Spruce);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
trees
|
||||
};
|
||||
|
||||
for (x, z) in floor_area {
|
||||
// Apply per-block randomness for certain landuse types
|
||||
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
|
||||
@@ -141,7 +165,15 @@ pub fn generate_landuse(
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
let random_choice: i32 = rng.gen_range(0..30);
|
||||
if random_choice == 20 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
let tree_type = *trees_ok_to_generate
|
||||
.choose(&mut rng)
|
||||
.unwrap_or(&TreeType::Oak);
|
||||
Tree::create_of_type(
|
||||
editor,
|
||||
(x, 1, z),
|
||||
tree_type,
|
||||
Some(building_footprints),
|
||||
);
|
||||
} else if random_choice == 2 {
|
||||
let flower_block: Block = match rng.gen_range(1..=5) {
|
||||
1 => OAK_LEAVES,
|
||||
|
||||
@@ -2,10 +2,11 @@ use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::Tree;
|
||||
use crate::element_processing::tree::{Tree, TreeType};
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_natural(
|
||||
@@ -21,7 +22,66 @@ pub fn generate_natural(
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
let mut trees_ok_to_generate: Vec<TreeType> = vec![];
|
||||
if let Some(species) = element.tags().get("species") {
|
||||
if species.contains("Betula") {
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
if species.contains("Quercus") {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
}
|
||||
if species.contains("Picea") {
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
}
|
||||
} else if let Some(genus_wikidata) = element.tags().get("genus:wikidata") {
|
||||
match genus_wikidata.as_str() {
|
||||
"Q12004" => trees_ok_to_generate.push(TreeType::Birch),
|
||||
"Q26782" => trees_ok_to_generate.push(TreeType::Oak),
|
||||
"Q25243" => trees_ok_to_generate.push(TreeType::Spruce),
|
||||
_ => {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
}
|
||||
} else if let Some(genus) = element.tags().get("genus") {
|
||||
match genus.as_str() {
|
||||
"Betula" => trees_ok_to_generate.push(TreeType::Birch),
|
||||
"Quercus" => trees_ok_to_generate.push(TreeType::Oak),
|
||||
"Picea" => trees_ok_to_generate.push(TreeType::Spruce),
|
||||
_ => trees_ok_to_generate.push(TreeType::Oak),
|
||||
}
|
||||
} else if let Some(leaf_type) = element.tags().get("leaf_type") {
|
||||
match leaf_type.as_str() {
|
||||
"broadleaved" => {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
"needleleaved" => trees_ok_to_generate.push(TreeType::Spruce),
|
||||
_ => {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
|
||||
if trees_ok_to_generate.is_empty() {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
|
||||
let mut rng = element_rng(element.id());
|
||||
let tree_type = *trees_ok_to_generate
|
||||
.choose(&mut rng)
|
||||
.unwrap_or(&TreeType::Oak);
|
||||
|
||||
Tree::create_of_type(editor, (x, 1, z), tree_type, Some(building_footprints));
|
||||
}
|
||||
} else {
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
@@ -81,6 +141,29 @@ pub fn generate_natural(
|
||||
let filled_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
|
||||
|
||||
let trees_ok_to_generate: Vec<TreeType> = {
|
||||
let mut trees: Vec<TreeType> = vec![];
|
||||
if let Some(leaf_type) = element.tags().get("leaf_type") {
|
||||
match leaf_type.as_str() {
|
||||
"broadleaved" => {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
"needleleaved" => trees.push(TreeType::Spruce),
|
||||
_ => {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Spruce);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Spruce);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
trees
|
||||
};
|
||||
|
||||
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||
let mut rng = element_rng(way.id);
|
||||
|
||||
@@ -164,7 +247,15 @@ pub fn generate_natural(
|
||||
}
|
||||
let random_choice: i32 = rng.gen_range(0..30);
|
||||
if random_choice == 0 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
let tree_type = *trees_ok_to_generate
|
||||
.choose(&mut rng)
|
||||
.unwrap_or(&TreeType::Oak);
|
||||
Tree::create_of_type(
|
||||
editor,
|
||||
(x, 1, z),
|
||||
tree_type,
|
||||
Some(building_footprints),
|
||||
);
|
||||
} else if random_choice == 1 {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
|
||||
@@ -58,7 +58,7 @@ const INTERIOR1_LAYER2: [[char; 23]; 23] = [
|
||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building level floors (1nd layer above floor)
|
||||
/// Interior layout for building level floors (1st layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const INTERIOR2_LAYER1: [[char; 23]; 23] = [
|
||||
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||
@@ -114,6 +114,119 @@ const INTERIOR2_LAYER2: [[char; 23]; 23] = [
|
||||
['P', 'P', ' ', ' ', ' ', 'E', 'B', 'B', 'B', ' ', ' ', 'W', 'B', 'B', 'B', 'B', 'B', 'B', 'B', ' ', 'B', ' ', 'D',],
|
||||
];
|
||||
|
||||
// Generic Abandoned Building Interiors
|
||||
/// Interior layout for building ground floors (1st layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const ABANDONED_INTERIOR1_LAYER1: [[char; 23]; 23] = [
|
||||
['1', 'U', ' ', 'W', 'C', ' ', ' ', ' ', 'S', 'S', 'W', 'b', 'T', 'T', 'd', 'W', '7', '8', ' ', ' ', ' ', ' ', 'W',],
|
||||
['2', ' ', ' ', 'W', 'F', ' ', ' ', ' ', 'U', 'U', 'W', 'b', 'T', 'T', 'd', 'W', '7', '8', ' ', ' ', ' ', 'B', 'W',],
|
||||
[' ', ' ', ' ', 'W', 'F', ' ', ' ', ' ', ' ', ' ', 'W', 'b', 'T', 'T', 'd', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
|
||||
['W', 'W', 'D', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'M', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'c', 'c', 'c', ' ', ' ', 'J', 'W', ' ', ' ', ' ', 'd', 'W', 'W', 'W',],
|
||||
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', 'T', 'S', 'S', 'T', ' ', ' ', 'W', 'S', 'S', ' ', 'd', 'W', 'W', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'T', 'T', 'T', 'T', ' ', ' ', 'W', 'U', 'U', ' ', 'd', 'W', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', 'T', 'T', 'T', 'T', ' ', 'B', 'W', ' ', ' ', ' ', 'd', 'W', ' ', ' ',],
|
||||
['L', ' ', 'M', 'L', 'W', 'W', ' ', ' ', 'W', 'J', 'U', 'U', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
|
||||
['c', 'c', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
|
||||
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
|
||||
[' ', '6', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['U', '5', ' ', 'W', ' ', ' ', 'W', 'C', 'F', 'F', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', 'W', 'M', ' ', 'b', 'W', ' ', ' ', 'W',],
|
||||
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'b', 'W', 'J', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'U', ' ', ' ', 'W', 'B', ' ', 'D',],
|
||||
['J', ' ', ' ', 'C', 'a', 'a', 'W', 'L', 'F', ' ', 'W', 'F', ' ', 'W', 'L', 'W', '7', '8', ' ', 'W', 'B', ' ', 'W',],
|
||||
['B', ' ', ' ', 'd', 'W', 'W', 'W', 'W', 'W', ' ', 'W', 'M', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'C', ' ', 'W',],
|
||||
['B', ' ', ' ', 'd', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'C', ' ', ' ', 'W', 'W', 'c', 'c', 'c', 'c', 'W', 'D', 'W',],
|
||||
['W', 'W', 'D', 'W', 'C', ' ', ' ', ' ', 'W', 'W', 'W', 'b', 'T', 'T', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building ground floors (2nd layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const ABANDONED_INTERIOR1_LAYER2: [[char; 23]; 23] = [
|
||||
[' ', 'P', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'P', 'P', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'B', 'W',],
|
||||
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
|
||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', 'B', 'B', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
|
||||
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', 'P', ' ', 'B', 'W', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'B', 'W', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', 'W', 'W', ' ', ' ', 'W', ' ', 'P', 'P', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
|
||||
['B', 'B', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
|
||||
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['P', ' ', ' ', 'W', ' ', ' ', 'W', 'N', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'B', 'W', ' ', ' ', 'W',],
|
||||
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'C', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'P', ' ', ' ', 'W', 'B', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', 'B', 'B', 'W', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'P', 'W', ' ', ' ', ' ', 'W', 'B', ' ', 'W',],
|
||||
['B', ' ', ' ', 'B', 'W', 'W', 'W', 'W', 'W', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W',],
|
||||
['B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'N', ' ', ' ', 'W', 'W', 'B', 'B', 'B', 'B', 'W', 'D', 'W',],
|
||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building level floors (1st layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const ABANDONED_INTERIOR2_LAYER1: [[char; 23]; 23] = [
|
||||
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||
['U', ' ', ' ', ' ', ' ', ' ', 'C', 'W', 'L', ' ', ' ', 'L', 'W', 'M', 'M', 'W', ' ', ' ', ' ', ' ', ' ', 'L', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'S', 'S', 'S', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'Q', 'C', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'J', ' ', 'U', 'U', 'U', ' ', 'D',],
|
||||
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
|
||||
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', 'T', 'T', 'W', ' ', ' ', ' ', ' ', ' ', 'U', 'W', ' ', 'L', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'T', 'J', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', 'L', ' ', 'W',],
|
||||
['J', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', 'M', 'c', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'd', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'd', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'U', ' ', ' ', ' ', 'D', ' ', ' ', 'F', 'F', 'W', 'M', 'M', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'U', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', 'C', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
['C', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'L', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'U', 'U', ' ', 'Q', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'U', 'U', ' ', 'Q', 'b', ' ', 'U', 'U', 'B', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['S', 'S', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'Q', 'b', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'd', ' ', 'W',],
|
||||
['U', 'U', ' ', ' ', ' ', 'L', 'a', 'a', 'a', ' ', ' ', 'Q', 'B', 'a', 'a', 'a', 'a', 'a', 'a', ' ', 'd', 'D', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building level floors (2nd layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const ABANDONED_INTERIOR2_LAYER2: [[char; 23]; 23] = [
|
||||
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||
['P', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', 'O', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'O', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', 'P', 'P', ' ', 'D',],
|
||||
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
|
||||
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'P', 'W', ' ', 'P', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'O', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', ' ', 'c', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'd', 'W', 'O', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'd', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'P', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'O', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'P', 'P', ' ', 'Q', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'P', 'P', ' ', 'Q', 'b', ' ', 'P', 'P', 'c', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'Q', 'b', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'd', ' ', 'W',],
|
||||
['P', 'P', ' ', ' ', ' ', 'O', 'a', 'a', 'a', ' ', ' ', 'Q', 'b', 'a', 'a', 'a', 'a', 'a', 'a', ' ', 'd', ' ', 'D',],
|
||||
];
|
||||
|
||||
/// Maps interior layout characters to actual block types for different floor layers
|
||||
#[inline(always)]
|
||||
pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option<Block> {
|
||||
@@ -145,12 +258,19 @@ pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option
|
||||
Some(DARK_OAK_DOOR_LOWER)
|
||||
}
|
||||
}
|
||||
'J' => Some(NOTE_BLOCK), // Note block
|
||||
'G' => Some(GLOWSTONE), // Glowstone
|
||||
'N' => Some(BREWING_STAND), // Brewing Stand
|
||||
'T' => Some(WHITE_CARPET), // White Carpet
|
||||
'E' => Some(OAK_LEAVES), // Oak Leaves
|
||||
_ => None, // Default case for unknown characters
|
||||
'J' => Some(NOTE_BLOCK), // Note block
|
||||
'G' => Some(GLOWSTONE), // Glowstone
|
||||
'N' => Some(BREWING_STAND), // Brewing Stand
|
||||
'T' => Some(WHITE_CARPET), // White Carpet
|
||||
'E' => Some(OAK_LEAVES), // Oak Leaves
|
||||
'O' => Some(COBWEB), // Cobweb
|
||||
'a' => Some(CHISELLED_BOOKSHELF_NORTH), // Chiseled Bookshelf
|
||||
'b' => Some(CHISELLED_BOOKSHELF_EAST), // Chiseled Bookshelf East
|
||||
'c' => Some(CHISELLED_BOOKSHELF_SOUTH), // Chiseled Bookshelf South
|
||||
'd' => Some(CHISELLED_BOOKSHELF_WEST), // Chiseled Bookshelf West
|
||||
'M' => Some(DAMAGED_ANVIL), // Damaged Anvil
|
||||
'Q' => Some(SCAFFOLDING), // Scaffolding
|
||||
_ => None, // Default case for unknown characters
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +290,7 @@ pub fn generate_building_interior(
|
||||
args: &crate::args::Args,
|
||||
element: &crate::osm_parser::ProcessedWay,
|
||||
abs_terrain_offset: i32,
|
||||
is_abandoned_building: bool,
|
||||
) {
|
||||
// Skip interior generation for very small buildings
|
||||
let width = max_x - min_x + 1;
|
||||
@@ -214,7 +335,13 @@ pub fn generate_building_interior(
|
||||
};
|
||||
|
||||
// Choose the appropriate interior pattern based on floor number
|
||||
let (layer1, layer2) = if floor_index == 0 {
|
||||
let (layer1, layer2) = if is_abandoned_building {
|
||||
if floor_index == 0 {
|
||||
(&ABANDONED_INTERIOR1_LAYER1, &ABANDONED_INTERIOR1_LAYER2)
|
||||
} else {
|
||||
(&ABANDONED_INTERIOR2_LAYER1, &ABANDONED_INTERIOR2_LAYER2)
|
||||
}
|
||||
} else if floor_index == 0 {
|
||||
// Ground floor uses INTERIOR1 patterns
|
||||
(&INTERIOR1_LAYER1, &INTERIOR1_LAYER2)
|
||||
} else {
|
||||
|
||||
@@ -92,6 +92,7 @@ fn round(editor: &mut WorldEditor, material: Block, (x, y, z): Coord, block_patt
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum TreeType {
|
||||
Oak,
|
||||
Spruce,
|
||||
@@ -120,6 +121,27 @@ impl Tree<'_> {
|
||||
editor: &mut WorldEditor,
|
||||
(x, y, z): Coord,
|
||||
building_footprints: Option<&BuildingFootprintBitmap>,
|
||||
) {
|
||||
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
||||
// The element_id of 0 is used as a salt for tree-specific randomness
|
||||
let mut rng = coord_rng(x, z, 0);
|
||||
|
||||
let tree_type = match rng.gen_range(1..=3) {
|
||||
1 => TreeType::Oak,
|
||||
2 => TreeType::Spruce,
|
||||
3 => TreeType::Birch,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Self::create_of_type(editor, (x, y, z), tree_type, building_footprints);
|
||||
}
|
||||
|
||||
/// Creates a tree of a specific type at the specified coordinates.
|
||||
pub fn create_of_type(
|
||||
editor: &mut WorldEditor,
|
||||
(x, y, z): Coord,
|
||||
tree_type: TreeType,
|
||||
building_footprints: Option<&BuildingFootprintBitmap>,
|
||||
) {
|
||||
// Skip if this coordinate is inside a building
|
||||
if let Some(footprints) = building_footprints {
|
||||
@@ -135,16 +157,7 @@ impl Tree<'_> {
|
||||
blacklist.extend(Self::get_functional_blocks());
|
||||
blacklist.push(WATER);
|
||||
|
||||
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
||||
// The element_id of 0 is used as a salt for tree-specific randomness
|
||||
let mut rng = coord_rng(x, z, 0);
|
||||
|
||||
let tree = Self::get_tree(match rng.gen_range(1..=3) {
|
||||
1 => TreeType::Oak,
|
||||
2 => TreeType::Spruce,
|
||||
3 => TreeType::Birch,
|
||||
_ => unreachable!(),
|
||||
});
|
||||
let tree = Self::get_tree(tree_type);
|
||||
|
||||
// Build the logs
|
||||
editor.fill_blocks(
|
||||
|
||||
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,
|
||||
* creating a collection (MULTI-geometry) based on their types, which must agree.
|
||||
* For example, creates a MULTIPOLYGON from a POLYGON type merged with another
|
||||
<<<<<<< HEAD
|
||||
* POLYGON type.
|
||||
=======
|
||||
* POLYGON type, or adds a POLYGON instance to a MULTIPOLYGON instance.
|
||||
>>>>>>> dev
|
||||
* @memberof Wkt.Wkt
|
||||
* @method
|
||||
*/
|
||||
|
||||
@@ -14,11 +14,14 @@ use crate::ground::Ground;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
|
||||
use bedrockrs_level::level::db_interface::bedrock_key::ChunkKey;
|
||||
use bedrockrs_level::level::db_interface::rusty::RustyDBInterface;
|
||||
use bedrockrs_level::level::db_interface::key_level::KeyTypeTag;
|
||||
use bedrockrs_level::level::db_interface::rusty::{mcpe_options, RustyDBInterface};
|
||||
use bedrockrs_level::level::file_interface::RawWorldTrait;
|
||||
use bedrockrs_shared::world::dimension::Dimension;
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use fastnbt::Value;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use rusty_leveldb::DB;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap as StdHashMap;
|
||||
use std::fs::{self, File};
|
||||
@@ -82,6 +85,8 @@ impl From<serde_json::Error> for BedrockSaveError {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_BEDROCK_COMPRESSION_LEVEL: u8 = 6;
|
||||
|
||||
/// Metadata for Bedrock worlds
|
||||
#[derive(Serialize)]
|
||||
struct BedrockMetadata {
|
||||
@@ -402,7 +407,7 @@ impl BedrockWriter {
|
||||
// Open LevelDB with Bedrock-compatible options
|
||||
let mut state = ();
|
||||
let mut db: RustyDBInterface<()> =
|
||||
RustyDBInterface::new(db_path.into_boxed_path(), true, &mut state)
|
||||
RustyDBInterface::new(db_path.clone().into_boxed_path(), true, &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Count total chunks for progress
|
||||
@@ -416,63 +421,128 @@ impl BedrockWriter {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let progress_bar = ProgressBar::new(total_chunks as u64);
|
||||
progress_bar.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} chunks ({eta})")
|
||||
.unwrap()
|
||||
.progress_chars("█▓░"),
|
||||
);
|
||||
{
|
||||
let progress_bar = ProgressBar::new(total_chunks as u64);
|
||||
progress_bar.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} chunks ({eta})")
|
||||
.unwrap()
|
||||
.progress_chars("█▓░"),
|
||||
);
|
||||
|
||||
let mut chunks_processed: usize = 0;
|
||||
let mut chunks_processed: usize = 0;
|
||||
|
||||
// Process each region and chunk
|
||||
for ((region_x, region_z), region) in &world.regions {
|
||||
for ((local_chunk_x, local_chunk_z), chunk) in ®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 ((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);
|
||||
let chunk_pos =
|
||||
Vec2::new(region_x * 32 + local_chunk_x, region_z * 32 + local_chunk_z);
|
||||
|
||||
// Write chunk version marker (42 is current Bedrock version as of 1.21+)
|
||||
let version_key = ChunkKey::chunk_marker(chunk_pos, Dimension::Overworld);
|
||||
db.set_subchunk_raw(version_key, &[42], &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Write Data3D (heightmap + biomes) - required for chunk to be valid
|
||||
let data3d_key = ChunkKey::data3d(chunk_pos, Dimension::Overworld);
|
||||
let data3d = self.create_data3d(chunk);
|
||||
db.set_subchunk_raw(data3d_key, &data3d, &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Process each section (subchunk)
|
||||
for (§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, "");
|
||||
}
|
||||
self.write_compound_list_record(
|
||||
&mut db,
|
||||
chunk_pos,
|
||||
KeyTypeTag::BlockEntity,
|
||||
chunk.other.get("block_entities"),
|
||||
)?;
|
||||
self.write_compound_list_record(
|
||||
&mut db,
|
||||
chunk_pos,
|
||||
KeyTypeTag::Entity,
|
||||
chunk.other.get("entities"),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
progress_bar.finish_with_message("Chunks written to LevelDB");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// LevelDB writes are flushed when the database is dropped
|
||||
drop(db);
|
||||
fn write_compound_list_record(
|
||||
&self,
|
||||
db: &mut DB,
|
||||
chunk_pos: Vec2<i32>,
|
||||
key_type: KeyTypeTag,
|
||||
value: Option<&Value>,
|
||||
) -> Result<(), BedrockSaveError> {
|
||||
let Some(Value::List(values)) = value else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if values.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let deduped = dedup_compound_list(values);
|
||||
if deduped.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let data = nbtx::to_le_bytes(&deduped).map_err(|e| BedrockSaveError::Nbt(e.to_string()))?;
|
||||
let key = build_chunk_key_bytes(chunk_pos, Dimension::Overworld, key_type, None);
|
||||
db.put(&key, &data)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -737,6 +807,91 @@ fn bedrock_bits_per_block(palette_count: u32) -> u8 {
|
||||
16 // Maximum
|
||||
}
|
||||
|
||||
fn build_chunk_key_bytes(
|
||||
chunk_pos: Vec2<i32>,
|
||||
dimension: Dimension,
|
||||
key_type: KeyTypeTag,
|
||||
y_index: Option<i8>,
|
||||
) -> Vec<u8> {
|
||||
let mut buffer = Vec::with_capacity(
|
||||
9 + if dimension != Dimension::Overworld {
|
||||
4
|
||||
} else {
|
||||
0
|
||||
} + 1,
|
||||
);
|
||||
buffer.extend_from_slice(&chunk_pos.x.to_le_bytes());
|
||||
buffer.extend_from_slice(&chunk_pos.y.to_le_bytes());
|
||||
|
||||
if dimension != Dimension::Overworld {
|
||||
buffer.extend_from_slice(&i32::from(dimension).to_le_bytes());
|
||||
}
|
||||
|
||||
buffer.push(key_type.to_byte());
|
||||
if let Some(y) = y_index {
|
||||
buffer.push(y as u8);
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
fn dedup_compound_list(values: &[Value]) -> Vec<Value> {
|
||||
let mut coord_index: StdHashMap<(i32, i32, i32), usize> = StdHashMap::new();
|
||||
let mut deduped: Vec<Value> = Vec::with_capacity(values.len());
|
||||
|
||||
for value in values {
|
||||
if let Value::Compound(map) = value {
|
||||
if let Some(coords) = get_entity_coords(map) {
|
||||
if let Some(idx) = coord_index.get(&coords).copied() {
|
||||
deduped[idx] = value.clone();
|
||||
continue;
|
||||
} else {
|
||||
coord_index.insert(coords, deduped.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
deduped.push(value.clone());
|
||||
}
|
||||
|
||||
deduped
|
||||
}
|
||||
|
||||
fn get_entity_coords(entity: &StdHashMap<String, Value>) -> Option<(i32, i32, i32)> {
|
||||
if let Some(Value::List(pos)) = entity.get("Pos") {
|
||||
if pos.len() == 3 {
|
||||
if let (Some(x), Some(y), Some(z)) = (
|
||||
value_to_i32(&pos[0]),
|
||||
value_to_i32(&pos[1]),
|
||||
value_to_i32(&pos[2]),
|
||||
) {
|
||||
return Some((x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (Some(x), Some(y), Some(z)) = (
|
||||
entity.get("x").and_then(value_to_i32),
|
||||
entity.get("y").and_then(value_to_i32),
|
||||
entity.get("z").and_then(value_to_i32),
|
||||
) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((x, y, z))
|
||||
}
|
||||
|
||||
fn value_to_i32(value: &Value) -> Option<i32> {
|
||||
match value {
|
||||
Value::Byte(v) => Some(i32::from(*v)),
|
||||
Value::Short(v) => Some(i32::from(*v)),
|
||||
Value::Int(v) => Some(*v),
|
||||
Value::Long(v) => i32::try_from(*v).ok(),
|
||||
Value::Float(v) => Some(*v as i32),
|
||||
Value::Double(v) => Some(*v as i32),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Level.dat structure for Bedrock Edition
|
||||
/// This struct contains all required fields for a valid Bedrock world
|
||||
#[derive(serde::Serialize)]
|
||||
|
||||
@@ -174,40 +174,9 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve existing block entities and merge with new ones
|
||||
if let Some(existing_entities) = chunk.other.get_mut("block_entities") {
|
||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities") {
|
||||
if let (Value::List(existing), Value::List(new)) =
|
||||
(existing_entities, new_entities)
|
||||
{
|
||||
// Remove old entities that are replaced by new ones
|
||||
existing.retain(|e| {
|
||||
if let Value::Compound(map) = e {
|
||||
let (x, y, z) = get_entity_coords(map);
|
||||
!new.iter().any(|new_e| {
|
||||
if let Value::Compound(new_map) = new_e {
|
||||
let (nx, ny, nz) = get_entity_coords(new_map);
|
||||
x == nx && y == ny && z == nz
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
// Add new entities
|
||||
existing.extend(new.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no existing entities, just add the new ones
|
||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities") {
|
||||
chunk
|
||||
.other
|
||||
.insert("block_entities".to_string(), new_entities.clone());
|
||||
}
|
||||
}
|
||||
// Preserve existing block entities and entities and merge with new ones
|
||||
merge_compound_list(&mut chunk, chunk_to_modify, "block_entities");
|
||||
merge_compound_list(&mut chunk, chunk_to_modify, "entities");
|
||||
|
||||
// Update chunk coordinates and flags
|
||||
chunk.x_pos = chunk_x + (region_x * 32);
|
||||
@@ -246,87 +215,129 @@ impl<'a> WorldEditor<'a> {
|
||||
|
||||
/// Helper function to get entity coordinates
|
||||
#[inline]
|
||||
fn get_entity_coords(entity: &HashMap<String, Value>) -> (i32, i32, i32) {
|
||||
let x = if let Value::Int(x) = entity.get("x").unwrap_or(&Value::Int(0)) {
|
||||
*x
|
||||
} else {
|
||||
0
|
||||
fn get_entity_coords(entity: &HashMap<String, Value>) -> Option<(i32, i32, i32)> {
|
||||
if let Some(Value::List(pos)) = entity.get("Pos") {
|
||||
if pos.len() == 3 {
|
||||
if let (Some(x), Some(y), Some(z)) = (
|
||||
value_to_i32(&pos[0]),
|
||||
value_to_i32(&pos[1]),
|
||||
value_to_i32(&pos[2]),
|
||||
) {
|
||||
return Some((x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (Some(x), Some(y), Some(z)) = (
|
||||
entity.get("x").and_then(value_to_i32),
|
||||
entity.get("y").and_then(value_to_i32),
|
||||
entity.get("z").and_then(value_to_i32),
|
||||
) else {
|
||||
return None;
|
||||
};
|
||||
let y = if let Value::Int(y) = entity.get("y").unwrap_or(&Value::Int(0)) {
|
||||
*y
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let z = if let Value::Int(z) = entity.get("z").unwrap_or(&Value::Int(0)) {
|
||||
*z
|
||||
} else {
|
||||
0
|
||||
};
|
||||
(x, y, z)
|
||||
|
||||
Some((x, y, z))
|
||||
}
|
||||
|
||||
/// Creates a Level wrapper for chunk data (Java Edition format)
|
||||
#[inline]
|
||||
fn create_level_wrapper(chunk: &Chunk) -> HashMap<String, Value> {
|
||||
HashMap::from([(
|
||||
"Level".to_string(),
|
||||
Value::Compound(HashMap::from([
|
||||
("xPos".to_string(), Value::Int(chunk.x_pos)),
|
||||
("zPos".to_string(), Value::Int(chunk.z_pos)),
|
||||
(
|
||||
"isLightOn".to_string(),
|
||||
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
|
||||
),
|
||||
(
|
||||
"sections".to_string(),
|
||||
Value::List(
|
||||
chunk
|
||||
.sections
|
||||
.iter()
|
||||
.map(|section| {
|
||||
let mut block_states = HashMap::from([(
|
||||
"palette".to_string(),
|
||||
Value::List(
|
||||
section
|
||||
.block_states
|
||||
.palette
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let mut palette_item = HashMap::from([(
|
||||
"Name".to_string(),
|
||||
Value::String(item.name.clone()),
|
||||
)]);
|
||||
if let Some(props) = &item.properties {
|
||||
palette_item.insert(
|
||||
"Properties".to_string(),
|
||||
props.clone(),
|
||||
);
|
||||
}
|
||||
Value::Compound(palette_item)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
)]);
|
||||
let mut level_map = HashMap::from([
|
||||
("xPos".to_string(), Value::Int(chunk.x_pos)),
|
||||
("zPos".to_string(), Value::Int(chunk.z_pos)),
|
||||
(
|
||||
"isLightOn".to_string(),
|
||||
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
|
||||
),
|
||||
(
|
||||
"sections".to_string(),
|
||||
Value::List(
|
||||
chunk
|
||||
.sections
|
||||
.iter()
|
||||
.map(|section| {
|
||||
let mut block_states = HashMap::from([(
|
||||
"palette".to_string(),
|
||||
Value::List(
|
||||
section
|
||||
.block_states
|
||||
.palette
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let mut palette_item = HashMap::from([(
|
||||
"Name".to_string(),
|
||||
Value::String(item.name.clone()),
|
||||
)]);
|
||||
if let Some(props) = &item.properties {
|
||||
palette_item
|
||||
.insert("Properties".to_string(), props.clone());
|
||||
}
|
||||
Value::Compound(palette_item)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
)]);
|
||||
|
||||
// Only add the `data` attribute if it's non-empty
|
||||
// to maintain compatibility with third-party tools like Dynmap
|
||||
if let Some(data) = §ion.block_states.data {
|
||||
if !data.is_empty() {
|
||||
block_states.insert(
|
||||
"data".to_string(),
|
||||
Value::LongArray(data.to_owned()),
|
||||
);
|
||||
}
|
||||
// Only add the `data` attribute if it's non-empty
|
||||
// to maintain compatibility with third-party tools like Dynmap
|
||||
if let Some(data) = §ion.block_states.data {
|
||||
if !data.is_empty() {
|
||||
block_states
|
||||
.insert("data".to_string(), Value::LongArray(data.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
Value::Compound(HashMap::from([
|
||||
("Y".to_string(), Value::Byte(section.y)),
|
||||
("block_states".to_string(), Value::Compound(block_states)),
|
||||
]))
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
Value::Compound(HashMap::from([
|
||||
("Y".to_string(), Value::Byte(section.y)),
|
||||
("block_states".to_string(), Value::Compound(block_states)),
|
||||
]))
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
])),
|
||||
)])
|
||||
),
|
||||
]);
|
||||
|
||||
for (key, value) in &chunk.other {
|
||||
level_map.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
HashMap::from([("Level".to_string(), Value::Compound(level_map))])
|
||||
}
|
||||
|
||||
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::progress::emit_gui_progress_update;
|
||||
use colored::Colorize;
|
||||
use fastnbt::Value;
|
||||
use fastnbt::{IntArray, Value};
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{hash_map::Entry, HashMap};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
@@ -243,6 +243,212 @@ impl<'a> WorldEditor<'a> {
|
||||
self.set_block(SIGN, x, y, z, None, None);
|
||||
}
|
||||
|
||||
/// Adds an entity at the given coordinates (Y is ground-relative).
|
||||
#[allow(dead_code)]
|
||||
pub fn add_entity(
|
||||
&mut self,
|
||||
id: &str,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
extra_data: Option<HashMap<String, Value>>,
|
||||
) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
|
||||
let mut entity = HashMap::new();
|
||||
entity.insert("id".to_string(), Value::String(id.to_string()));
|
||||
entity.insert(
|
||||
"Pos".to_string(),
|
||||
Value::List(vec![
|
||||
Value::Double(x as f64 + 0.5),
|
||||
Value::Double(absolute_y as f64),
|
||||
Value::Double(z as f64 + 0.5),
|
||||
]),
|
||||
);
|
||||
entity.insert(
|
||||
"Motion".to_string(),
|
||||
Value::List(vec![
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
]),
|
||||
);
|
||||
entity.insert(
|
||||
"Rotation".to_string(),
|
||||
Value::List(vec![Value::Float(0.0), Value::Float(0.0)]),
|
||||
);
|
||||
entity.insert("OnGround".to_string(), Value::Byte(1));
|
||||
entity.insert("FallDistance".to_string(), Value::Float(0.0));
|
||||
entity.insert("Fire".to_string(), Value::Short(-20));
|
||||
entity.insert("Air".to_string(), Value::Short(300));
|
||||
entity.insert("PortalCooldown".to_string(), Value::Int(0));
|
||||
entity.insert(
|
||||
"UUID".to_string(),
|
||||
Value::IntArray(build_deterministic_uuid(id, x, absolute_y, z)),
|
||||
);
|
||||
|
||||
if let Some(extra) = extra_data {
|
||||
for (key, value) in extra {
|
||||
entity.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let region = self.world.get_or_create_region(region_x, region_z);
|
||||
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
match chunk.other.entry("entities".to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let Value::List(list) = entry.get_mut() {
|
||||
list.push(Value::Compound(entity));
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(Value::List(vec![Value::Compound(entity)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Places a chest with the provided items at the given coordinates (ground-relative Y).
|
||||
#[allow(dead_code)]
|
||||
pub fn set_chest_with_items(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
items: Vec<HashMap<String, Value>>,
|
||||
) {
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
self.set_chest_with_items_absolute(x, absolute_y, z, items);
|
||||
}
|
||||
|
||||
/// Places a chest with the provided items at the given coordinates (absolute Y).
|
||||
#[allow(dead_code)]
|
||||
pub fn set_chest_with_items_absolute(
|
||||
&mut self,
|
||||
x: i32,
|
||||
absolute_y: i32,
|
||||
z: i32,
|
||||
items: Vec<HashMap<String, Value>>,
|
||||
) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let mut chest_data = HashMap::new();
|
||||
chest_data.insert(
|
||||
"id".to_string(),
|
||||
Value::String("minecraft:chest".to_string()),
|
||||
);
|
||||
chest_data.insert("x".to_string(), Value::Int(x));
|
||||
chest_data.insert("y".to_string(), Value::Int(absolute_y));
|
||||
chest_data.insert("z".to_string(), Value::Int(z));
|
||||
chest_data.insert(
|
||||
"Items".to_string(),
|
||||
Value::List(items.into_iter().map(Value::Compound).collect()),
|
||||
);
|
||||
chest_data.insert("keepPacked".to_string(), Value::Byte(0));
|
||||
|
||||
let region = self.world.get_or_create_region(region_x, region_z);
|
||||
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
match chunk.other.entry("block_entities".to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let Value::List(list) = entry.get_mut() {
|
||||
list.push(Value::Compound(chest_data));
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(Value::List(vec![Value::Compound(chest_data)]));
|
||||
}
|
||||
}
|
||||
|
||||
self.set_block_absolute(CHEST, x, absolute_y, z, None, None);
|
||||
}
|
||||
|
||||
/// Places a block entity with items at the given coordinates (ground-relative Y).
|
||||
#[allow(dead_code)]
|
||||
pub fn set_block_entity_with_items(
|
||||
&mut self,
|
||||
block_with_props: BlockWithProperties,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
block_entity_id: &str,
|
||||
items: Vec<HashMap<String, Value>>,
|
||||
) {
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
self.set_block_entity_with_items_absolute(
|
||||
block_with_props,
|
||||
x,
|
||||
absolute_y,
|
||||
z,
|
||||
block_entity_id,
|
||||
items,
|
||||
);
|
||||
}
|
||||
|
||||
/// Places a block entity with items at the given coordinates (absolute Y).
|
||||
#[allow(dead_code)]
|
||||
pub fn set_block_entity_with_items_absolute(
|
||||
&mut self,
|
||||
block_with_props: BlockWithProperties,
|
||||
x: i32,
|
||||
absolute_y: i32,
|
||||
z: i32,
|
||||
block_entity_id: &str,
|
||||
items: Vec<HashMap<String, Value>>,
|
||||
) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let mut block_entity = HashMap::new();
|
||||
block_entity.insert("id".to_string(), Value::String(block_entity_id.to_string()));
|
||||
block_entity.insert("x".to_string(), Value::Int(x));
|
||||
block_entity.insert("y".to_string(), Value::Int(absolute_y));
|
||||
block_entity.insert("z".to_string(), Value::Int(z));
|
||||
block_entity.insert(
|
||||
"Items".to_string(),
|
||||
Value::List(items.into_iter().map(Value::Compound).collect()),
|
||||
);
|
||||
block_entity.insert("keepPacked".to_string(), Value::Byte(0));
|
||||
|
||||
let region = self.world.get_or_create_region(region_x, region_z);
|
||||
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
match chunk.other.entry("block_entities".to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let Value::List(list) = entry.get_mut() {
|
||||
list.push(Value::Compound(block_entity));
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(Value::List(vec![Value::Compound(block_entity)]));
|
||||
}
|
||||
}
|
||||
|
||||
self.set_block_with_properties_absolute(block_with_props, x, absolute_y, z, None, None);
|
||||
}
|
||||
|
||||
/// Sets a block of the specified type at the given coordinates.
|
||||
///
|
||||
/// Y value is interpreted as an offset from ground level.
|
||||
@@ -599,3 +805,30 @@ impl<'a> WorldEditor<'a> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn build_deterministic_uuid(id: &str, x: i32, y: i32, z: i32) -> IntArray {
|
||||
let mut hash: i64 = 17;
|
||||
for byte in id.bytes() {
|
||||
hash = hash.wrapping_mul(31).wrapping_add(byte as i64);
|
||||
}
|
||||
|
||||
let seed_a = hash ^ (x as i64).wrapping_shl(32) ^ (y as i64).wrapping_mul(17);
|
||||
let seed_b = hash.rotate_left(7) ^ (z as i64).wrapping_mul(31) ^ (x as i64).wrapping_mul(13);
|
||||
|
||||
IntArray::new(vec![
|
||||
(seed_a >> 32) as i32,
|
||||
seed_a as i32,
|
||||
(seed_b >> 32) as i32,
|
||||
seed_b as i32,
|
||||
])
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn single_item(id: &str, slot: i8, count: i8) -> HashMap<String, Value> {
|
||||
let mut item = HashMap::new();
|
||||
item.insert("id".to_string(), Value::String(id.to_string()));
|
||||
item.insert("Slot".to_string(), Value::Byte(slot));
|
||||
item.insert("Count".to_string(), Value::Byte(count));
|
||||
item
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user