pumpkin-world(structures): Add structure placement calculation

This commit is contained in:
Alexander Medvedev 2025-08-13 17:11:56 +02:00
parent f63a039918
commit fae24008db
15 changed files with 613 additions and 44 deletions

8
Cargo.lock generated
View File

@ -2264,9 +2264,9 @@ dependencies = [
[[package]]
name = "rayon"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
@ -2274,9 +2274,9 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.12.1"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",

View File

@ -48,7 +48,7 @@ bytes = "1.10"
# Concurrency/Parallelism and Synchronization
futures = { version = "0.3", default-features = false, features = ["executor"] }
rayon = "1.10"
rayon = "1.11"
crossbeam = "0.8"
uuid = { version = "1.18", features = ["serde", "v3", "v4"] }

358
assets/structure_set.json Normal file
View File

@ -0,0 +1,358 @@
{
"ancient_cities": {
"structures": [
{
"structure": "minecraft:ancient_city",
"weight": 1
}
],
"placement": {
"spacing": 24,
"separation": 8,
"salt": 20083232,
"type": "minecraft:random_spread"
}
},
"buried_treasures": {
"structures": [
{
"structure": "minecraft:buried_treasure",
"weight": 1
}
],
"placement": {
"spacing": 1,
"separation": 0,
"locate_offset": [
9,
0,
9
],
"frequency_reduction_method": "legacy_type_2",
"frequency": 0.01,
"salt": 0,
"type": "minecraft:random_spread"
}
},
"desert_pyramids": {
"structures": [
{
"structure": "minecraft:desert_pyramid",
"weight": 1
}
],
"placement": {
"spacing": 32,
"separation": 8,
"salt": 14357617,
"type": "minecraft:random_spread"
}
},
"end_cities": {
"structures": [
{
"structure": "minecraft:end_city",
"weight": 1
}
],
"placement": {
"spacing": 20,
"separation": 11,
"spread_type": "triangular",
"salt": 10387313,
"type": "minecraft:random_spread"
}
},
"igloos": {
"structures": [
{
"structure": "minecraft:igloo",
"weight": 1
}
],
"placement": {
"spacing": 32,
"separation": 8,
"salt": 14357618,
"type": "minecraft:random_spread"
}
},
"jungle_temples": {
"structures": [
{
"structure": "minecraft:jungle_pyramid",
"weight": 1
}
],
"placement": {
"spacing": 32,
"separation": 8,
"salt": 14357619,
"type": "minecraft:random_spread"
}
},
"mineshafts": {
"structures": [
{
"structure": "minecraft:mineshaft",
"weight": 1
},
{
"structure": "minecraft:mineshaft_mesa",
"weight": 1
}
],
"placement": {
"spacing": 1,
"separation": 0,
"frequency_reduction_method": "legacy_type_3",
"frequency": 0.004,
"salt": 0,
"type": "minecraft:random_spread"
}
},
"nether_complexes": {
"structures": [
{
"structure": "minecraft:fortress",
"weight": 2
},
{
"structure": "minecraft:bastion_remnant",
"weight": 3
}
],
"placement": {
"spacing": 27,
"separation": 4,
"salt": 30084232,
"type": "minecraft:random_spread"
}
},
"nether_fossils": {
"structures": [
{
"structure": "minecraft:nether_fossil",
"weight": 1
}
],
"placement": {
"spacing": 2,
"separation": 1,
"salt": 14357921,
"type": "minecraft:random_spread"
}
},
"ocean_monuments": {
"structures": [
{
"structure": "minecraft:monument",
"weight": 1
}
],
"placement": {
"spacing": 32,
"separation": 5,
"spread_type": "triangular",
"salt": 10387313,
"type": "minecraft:random_spread"
}
},
"ocean_ruins": {
"structures": [
{
"structure": "minecraft:ocean_ruin_cold",
"weight": 1
},
{
"structure": "minecraft:ocean_ruin_warm",
"weight": 1
}
],
"placement": {
"spacing": 20,
"separation": 8,
"salt": 14357621,
"type": "minecraft:random_spread"
}
},
"pillager_outposts": {
"structures": [
{
"structure": "minecraft:pillager_outpost",
"weight": 1
}
],
"placement": {
"exclusion_zone": {
"other_set": "minecraft:villages",
"chunk_count": 10
},
"spacing": 32,
"separation": 8,
"frequency_reduction_method": "legacy_type_1",
"frequency": 0.2,
"salt": 165745296,
"type": "minecraft:random_spread"
}
},
"ruined_portals": {
"structures": [
{
"structure": "minecraft:ruined_portal",
"weight": 1
},
{
"structure": "minecraft:ruined_portal_desert",
"weight": 1
},
{
"structure": "minecraft:ruined_portal_jungle",
"weight": 1
},
{
"structure": "minecraft:ruined_portal_swamp",
"weight": 1
},
{
"structure": "minecraft:ruined_portal_mountain",
"weight": 1
},
{
"structure": "minecraft:ruined_portal_ocean",
"weight": 1
},
{
"structure": "minecraft:ruined_portal_nether",
"weight": 1
}
],
"placement": {
"spacing": 40,
"separation": 15,
"salt": 34222645,
"type": "minecraft:random_spread"
}
},
"shipwrecks": {
"structures": [
{
"structure": "minecraft:shipwreck",
"weight": 1
},
{
"structure": "minecraft:shipwreck_beached",
"weight": 1
}
],
"placement": {
"spacing": 24,
"separation": 4,
"salt": 165745295,
"type": "minecraft:random_spread"
}
},
"strongholds": {
"structures": [
{
"structure": "minecraft:stronghold",
"weight": 1
}
],
"placement": {
"spread": 3,
"count": 128,
"preferred_biomes": "#minecraft:stronghold_biased_to",
"distance": 32,
"salt": 0,
"type": "minecraft:concentric_rings"
}
},
"swamp_huts": {
"structures": [
{
"structure": "minecraft:swamp_hut",
"weight": 1
}
],
"placement": {
"spacing": 32,
"separation": 8,
"salt": 14357620,
"type": "minecraft:random_spread"
}
},
"trail_ruins": {
"structures": [
{
"structure": "minecraft:trail_ruins",
"weight": 1
}
],
"placement": {
"spacing": 34,
"separation": 8,
"salt": 83469867,
"type": "minecraft:random_spread"
}
},
"trial_chambers": {
"structures": [
{
"structure": "minecraft:trial_chambers",
"weight": 1
}
],
"placement": {
"spacing": 34,
"separation": 12,
"salt": 94251327,
"type": "minecraft:random_spread"
}
},
"villages": {
"structures": [
{
"structure": "minecraft:village_plains",
"weight": 1
},
{
"structure": "minecraft:village_desert",
"weight": 1
},
{
"structure": "minecraft:village_savanna",
"weight": 1
},
{
"structure": "minecraft:village_snowy",
"weight": 1
},
{
"structure": "minecraft:village_taiga",
"weight": 1
}
],
"placement": {
"spacing": 34,
"separation": 8,
"salt": 10387312,
"type": "minecraft:random_spread"
}
},
"woodland_mansions": {
"structures": [
{
"structure": "minecraft:mansion",
"weight": 1
}
],
"placement": {
"spacing": 80,
"separation": 20,
"spread_type": "triangular",
"salt": 10387319,
"type": "minecraft:random_spread"
}
}
}

View File

@ -68,6 +68,30 @@ pub fn get_decorator_seed(population_seed: u64, index: usize, step: usize) -> u6
.wrapping_add(10_000u64.wrapping_mul(step as u64))
}
#[inline]
pub fn get_region_seed(world_seed: u64, region_x: i32, region_z: i32, salt: i32) -> u64 {
let region_x_long: u64 = region_x as u64;
let region_z_long: u64 = region_z as u64;
let salt_long: u64 = salt as u64;
region_x_long
.wrapping_mul(341873128712)
.wrapping_add(region_z_long.wrapping_mul(132897987541))
.wrapping_add(world_seed)
.wrapping_add(salt_long)
}
#[inline]
pub fn get_carver_seed(
random: &mut RandomGenerator,
world_seed: u64,
chunk_x: i32,
chunk_z: i32,
) -> u64 {
let x = random.next_i64();
let z = random.next_i64();
(chunk_x as u64).wrapping_mul(x as u64) ^ (chunk_z as u64).wrapping_mul(z as u64) ^ world_seed
}
#[enum_dispatch]
pub trait RandomImpl {
fn split(&mut self) -> Self;

View File

@ -4,9 +4,9 @@ use crate::inventory::{Clearable, Inventory, split_stack};
use crate::item::ItemStack;
use crate::world::SimpleWorld;
use async_trait::async_trait;
use pumpkin_data::Block;
use pumpkin_data::block_properties::{BlockProperties, HopperFacing, HopperLikeProperties};
use pumpkin_data::tag::Taggable;
use pumpkin_data::{Block, tag};
use pumpkin_nbt::tag::NbtTag;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
@ -184,9 +184,7 @@ impl HopperBlockEntity {
}
let (block, state) = world.get_block_and_state(pos_up).await;
if !(state.is_solid()
&& block
.is_tagged_with("minecraft:does_not_block_hoppers")
.unwrap())
&& block.is_tagged_with_by_tag(&tag::Block::MINECRAFT_DOES_NOT_BLOCK_HOPPERS))
{
// TODO getItemsAtAndAbove(level, hopper)
return false;

View File

@ -37,7 +37,7 @@ pub const CHUNK_COUNT: usize = REGION_SIZE * REGION_SIZE;
/// The number of bytes in a sector (4 KiB)
const SECTOR_BYTES: usize = 4096;
// 1.21.7
// 1.21.8
pub const WORLD_DATA_VERSION: i32 = 4438;
#[derive(Clone, Default)]

View File

@ -102,7 +102,7 @@ impl WorldGenerator for VanillaGenerator {
proto_chunk.populate_biomes(self.dimension);
proto_chunk.populate_noise();
proto_chunk.build_surface();
proto_chunk.generate_features(level, block_registry);
proto_chunk.generate_features_and_structure(level, block_registry);
for y in 0..biome_coords::from_block(generation_settings.shape.height) {
let relative_y = y as usize;

View File

@ -19,6 +19,7 @@ pub mod rule;
mod rule_test;
mod seed;
pub mod settings;
pub mod structure;
mod surface;
pub mod y_offset;

View File

@ -12,6 +12,8 @@ use pumpkin_util::{
};
use crate::generation::noise::perlin::DoublePerlinNoiseSampler;
use crate::generation::structure::STRUCTURE_SETS;
use crate::generation::structure::placement::StructurePlacementCalculator;
use crate::{
BlockStateId,
biome::{BiomeSupplier, MultiNoiseBiomeSupplier, end::TheEndBiomeSupplier, hash_seed},
@ -702,7 +704,7 @@ impl<'a> ProtoChunk<'a> {
}
}
/// This generates "Features," also known as decorations, which include things like trees, grass, ores, and more.
/// This generates "Structure Pieces" and "Features" also known as decorations, which include things like trees, grass, ores, and more.
/// Essentially, it encompasses everything above the surface or underground. It's crucial that this step is executed after biomes are generated,
/// as the decoration directly depends on the biome. Similarly, running this after the surface is built is logical, as it often involves checking block types.
/// For example, flowers are typically placed only on grass blocks.
@ -711,7 +713,11 @@ impl<'a> ProtoChunk<'a> {
///
/// 1. First, we determine **whether** to generate a feature and **at which block positions** to place it.
/// 2. Then, using the second file, we determine **how** to generate the feature.
pub fn generate_features(&mut self, level: &Arc<Level>, block_registry: &dyn BlockRegistryExt) {
pub fn generate_features_and_structure(
&mut self,
level: &Arc<Level>,
block_registry: &dyn BlockRegistryExt,
) {
let chunk_pos = self.chunk_pos;
let min_y = self.noise_sampler.min_y();
let height = self.noise_sampler.height();
@ -745,6 +751,17 @@ impl<'a> ProtoChunk<'a> {
}
}
pub fn set_structure_starts(&self) {
for (_name, set) in STRUCTURE_SETS.iter() {
let calculator = StructurePlacementCalculator {
seed: self.random_config.seed as i64,
};
if !set.placement.should_generate(calculator, self.chunk_pos) {
continue; // ??
}
}
}
fn start_cell_x(&self) -> i32 {
self.start_block_x() / self.noise_sampler.horizontal_cell_block_count() as i32
}

View File

@ -0,0 +1,16 @@
use std::{collections::HashMap, sync::LazyLock};
use pumpkin_util::read_data_from_file;
use serde::Deserialize;
use crate::generation::structure::placement::StructurePlacement;
pub mod placement;
#[derive(Deserialize)]
pub struct StructureSet {
pub placement: StructurePlacement,
}
pub static STRUCTURE_SETS: LazyLock<HashMap<String, StructureSet>> =
LazyLock::new(|| read_data_from_file!("../../../assets/structure_set.json"));

View File

@ -0,0 +1,167 @@
use pumpkin_util::{
math::{floor_div, vector2::Vector2},
random::{
RandomGenerator, RandomImpl, get_carver_seed, get_region_seed, xoroshiro128::Xoroshiro,
},
};
use serde::Deserialize;
#[derive(Deserialize)]
pub struct StructurePlacement {
frequency_reduction_method: Option<FrequencyReductionMethod>,
frequency: Option<f32>,
salt: i32,
#[serde(flatten)]
r#type: StructurePlacementType,
}
impl StructurePlacement {
pub fn should_generate(
&self,
calculator: StructurePlacementCalculator,
chunk_pos: Vector2<i32>,
) -> bool {
self.r#type
.is_start_chunk(&calculator, chunk_pos, self.salt)
&& self.apply_frequency_reduction(calculator.seed, chunk_pos)
// TODO: add exclusion_zone, only used for pillager_outposts
}
fn apply_frequency_reduction(&self, seed: i64, chunk_pos: Vector2<i32>) -> bool {
let frequency = self.frequency.unwrap_or(1.0);
frequency >= 1.0
|| self
.frequency_reduction_method
.as_ref()
.unwrap_or(&FrequencyReductionMethod::Default)
.should_generate(seed, chunk_pos, self.salt, frequency)
}
}
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrequencyReductionMethod {
Default,
LegacyType1,
LegacyType2,
LegacyType3,
}
impl FrequencyReductionMethod {
pub fn should_generate(
&self,
seed: i64,
chunk_pos: Vector2<i32>,
salt: i32,
frequency: f32,
) -> bool {
match self {
FrequencyReductionMethod::Default => {
let region_seed = get_region_seed(seed as u64, chunk_pos.x, chunk_pos.y, salt);
let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(region_seed));
random.next_f32() < frequency
}
FrequencyReductionMethod::LegacyType1 => {
let x = chunk_pos.x >> 4;
let z = chunk_pos.y >> 4;
let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(
(x ^ z << 4) as u64 ^ seed as u64,
));
random.next_i32(); // yeah mojang just does that and does not use the value
random.next_bounded_i32((1.0 / frequency) as i32) == 0
}
FrequencyReductionMethod::LegacyType2 => {
let region_seed = get_region_seed(seed as u64, chunk_pos.x, chunk_pos.y, 10387320);
let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(region_seed));
random.next_f32() < frequency
}
FrequencyReductionMethod::LegacyType3 => {
let mut random: RandomGenerator =
RandomGenerator::Xoroshiro(Xoroshiro::from_seed(seed as u64));
let carver_seed =
get_carver_seed(&mut random, seed as u64, chunk_pos.x, chunk_pos.y);
let mut random: RandomGenerator =
RandomGenerator::Xoroshiro(Xoroshiro::from_seed(carver_seed));
random.next_f64() < frequency as f64
}
}
}
}
#[derive(Deserialize)]
#[serde(tag = "type")]
pub enum StructurePlacementType {
#[serde(rename = "minecraft:random_spread")]
RandomSpread(RandomSpreadStructurePlacement),
#[serde(rename = "minecraft:concentric_rings")]
ConcentricRings,
}
impl StructurePlacementType {
pub fn is_start_chunk(
&self,
calculator: &StructurePlacementCalculator,
chunk_pos: Vector2<i32>,
salt: i32,
) -> bool {
match self {
StructurePlacementType::RandomSpread(placement) => {
placement.is_start_chunk(calculator, chunk_pos, salt)
}
StructurePlacementType::ConcentricRings => false, // TODO, This is needed for Stronghold, since it is placed in rings
}
}
}
#[derive(Deserialize)]
pub struct RandomSpreadStructurePlacement {
spacing: i32,
separation: i32,
spread_type: SpreadType,
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SpreadType {
Linear,
Triangular,
}
impl SpreadType {
pub fn get(&self, random: &mut RandomGenerator, bound: i32) -> i32 {
match self {
SpreadType::Linear => random.next_bounded_i32(bound),
SpreadType::Triangular => {
(random.next_bounded_i32(bound) + random.next_bounded_i32(bound)) / 2
}
}
}
}
impl RandomSpreadStructurePlacement {
fn get_start_chunk(&self, seed: i64, chunk_pos: Vector2<i32>, salt: i32) -> Vector2<i32> {
let x = floor_div(chunk_pos.x, self.spacing);
let z = floor_div(chunk_pos.y, self.spacing);
let region_seed = get_region_seed(seed as u64, x, z, salt);
let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(region_seed));
let bound = self.spacing - self.separation;
let rand_x = self.spread_type.get(&mut random, bound);
let rand_z = self.spread_type.get(&mut random, bound);
Vector2::new(x * self.spacing + rand_x, z * self.spacing + rand_z)
}
pub fn is_start_chunk(
&self,
calculator: &StructurePlacementCalculator,
chunk_pos: Vector2<i32>,
salt: i32,
) -> bool {
let pos = self.get_start_chunk(calculator.seed, chunk_pos, salt);
pos == chunk_pos
}
}
pub struct StructurePlacementCalculator {
pub seed: i64,
}

View File

@ -1,54 +1,42 @@
use pumpkin_data::tag;
use pumpkin_data::tag::Taggable;
use crate::item::ItemStack;
const SWORDS_TAG: &str = "#minecraft:swords";
const HEAD_ARMOR_TAG: &str = "#minecraft:head_armor";
const SKULL_TAG: &str = "#minecraft:skulls";
const CHEST_ARMOR_TAG: &str = "#minecraft:chest_armor";
const LEG_ARMOR_TAG: &str = "#minecraft:leg_armor";
const FOOT_ARMOR_TAG: &str = "#minecraft:foot_armor";
impl ItemStack {
#[inline]
pub fn is_sword(&self) -> bool {
self.item.is_tagged_with(SWORDS_TAG).expect(
"This is a default minecraft tag that should have been gotten from the extractor",
)
self.item
.is_tagged_with_by_tag(&tag::Item::MINECRAFT_SWORDS)
}
#[inline]
pub fn is_helmet(&self) -> bool {
self.item.is_tagged_with(HEAD_ARMOR_TAG).expect(
"This is a default minecraft tag that should have been gotten from the extractor",
)
self.item
.is_tagged_with_by_tag(&tag::Item::MINECRAFT_HEAD_ARMOR)
}
#[inline]
pub fn is_skull(&self) -> bool {
self.item.is_tagged_with(SKULL_TAG).expect(
"This is a default minecraft tag that should have been gotten from the extractor",
)
self.item
.is_tagged_with_by_tag(&tag::Item::MINECRAFT_SKULLS)
}
#[inline]
pub fn is_chestplate(&self) -> bool {
self.item.is_tagged_with(CHEST_ARMOR_TAG).expect(
"This is a default minecraft tag that should have been gotten from the extractor",
)
self.item
.is_tagged_with_by_tag(&tag::Item::MINECRAFT_CHEST_ARMOR)
}
#[inline]
pub fn is_leggings(&self) -> bool {
self.item.is_tagged_with(LEG_ARMOR_TAG).expect(
"This is a default minecraft tag that should have been gotten from the extractor",
)
self.item
.is_tagged_with_by_tag(&tag::Item::MINECRAFT_LEG_ARMOR)
}
#[inline]
pub fn is_boots(&self) -> bool {
self.item.is_tagged_with(FOOT_ARMOR_TAG).expect(
"This is a default minecraft tag that should have been gotten from the extractor",
)
self.item
.is_tagged_with_by_tag(&tag::Item::MINECRAFT_FOOT_ARMOR)
}
}

View File

@ -22,7 +22,7 @@ pub mod world_info;
pub type BlockId = u16;
pub type BlockStateId = u16;
pub const CURRENT_MC_VERSION: &str = "1.21.7";
pub const CURRENT_MC_VERSION: &str = "1.21.8";
pub const CURRENT_BEDROCK_MC_VERSION: &str = "1.21.94";
#[macro_export]

View File

@ -12,11 +12,11 @@ use thiserror::Error;
pub mod anvil;
// Constraint: disk biome palette serialization changed in 1.21.5
pub const MINIMUM_SUPPORTED_WORLD_DATA_VERSION: i32 = 4435; // 1.21.7
pub const MAXIMUM_SUPPORTED_WORLD_DATA_VERSION: i32 = 4438; // 1.21.7
pub const MINIMUM_SUPPORTED_WORLD_DATA_VERSION: i32 = 4435; // 1.21.8
pub const MAXIMUM_SUPPORTED_WORLD_DATA_VERSION: i32 = 4438; // 1.21.8
pub const MINIMUM_SUPPORTED_LEVEL_VERSION: i32 = 19132; // 1.21.7
pub const MAXIMUM_SUPPORTED_LEVEL_VERSION: i32 = 19133; // 1.21.7
pub const MINIMUM_SUPPORTED_LEVEL_VERSION: i32 = 19132; // 1.21.8
pub const MAXIMUM_SUPPORTED_LEVEL_VERSION: i32 = 19133; // 1.21.8
pub trait WorldInfoReader {
fn read_world_info(&self, level_folder: &Path) -> Result<LevelData, WorldInfoError>;

View File

@ -93,7 +93,7 @@ use pumpkin_data::potion::Effect;
const MAX_CACHED_SIGNATURES: u8 = 128; // Vanilla: 128
const MAX_PREVIOUS_MESSAGES: u8 = 20; // Vanilla: 20
pub const DATA_VERSION: i32 = 4438; // 1.21.7
pub const DATA_VERSION: i32 = 4438; // 1.21.8
enum BatchState {
Initial,