feat: basic initial datapack support
This commit is contained in:
parent
de8c780053
commit
df28c321ac
@ -9300,7 +9300,7 @@ impl ParameterPoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn target(
|
pub const fn target(
|
||||||
temperature: f32,
|
temperature: f32,
|
||||||
humidity: f32,
|
humidity: f32,
|
||||||
continentalness: f32,
|
continentalness: f32,
|
||||||
@ -9318,7 +9318,7 @@ pub fn target(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn parameters(
|
pub const fn parameters(
|
||||||
temperature: f32,
|
temperature: f32,
|
||||||
humidity: f32,
|
humidity: f32,
|
||||||
continentalness: f32,
|
continentalness: f32,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ use pumpkin_util::random::{
|
|||||||
RandomGenerator, RandomImpl, get_carver_seed, get_region_seed, legacy_rand::LegacyRand,
|
RandomGenerator, RandomImpl, get_carver_seed, get_region_seed, legacy_rand::LegacyRand,
|
||||||
xoroshiro128::Xoroshiro,
|
xoroshiro128::Xoroshiro,
|
||||||
};
|
};
|
||||||
|
use pumpkin_util::y_offset::{AboveBottom, Absolute, BelowTop, YOffset};
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
pub enum StructureKeys {
|
pub enum StructureKeys {
|
||||||
PillagerOutpost,
|
PillagerOutpost,
|
||||||
@ -247,6 +248,125 @@ pub enum StructureType {
|
|||||||
SwampHut,
|
SwampHut,
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum BoundingBoxType {
|
||||||
|
Piece,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SpawnEntry {
|
||||||
|
pub entity_type: &'static str,
|
||||||
|
pub min_count: u32,
|
||||||
|
pub max_count: u32,
|
||||||
|
pub weight: u32,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SpawnOverride {
|
||||||
|
pub category: &'static str,
|
||||||
|
pub bounding_box: BoundingBoxType,
|
||||||
|
pub spawns: &'static [SpawnEntry],
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum HeightProvider {
|
||||||
|
Uniform(UniformHeightProvider),
|
||||||
|
Trapezoid(TrapezoidHeightProvider),
|
||||||
|
VeryBiasedToBottom(VeryBiasedToBottomHeightProvider),
|
||||||
|
}
|
||||||
|
impl HeightProvider {
|
||||||
|
#[must_use]
|
||||||
|
pub fn get(&self, random: &mut RandomGenerator, min_y: i8, height: u16) -> i32 {
|
||||||
|
match self {
|
||||||
|
Self::Uniform(provider) => provider.get(random, min_y, height),
|
||||||
|
Self::Trapezoid(provider) => provider.get(random, min_y, height),
|
||||||
|
Self::VeryBiasedToBottom(provider) => provider.get(random, min_y, height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct UniformHeightProvider {
|
||||||
|
pub min_inclusive: YOffset,
|
||||||
|
pub max_inclusive: YOffset,
|
||||||
|
}
|
||||||
|
impl UniformHeightProvider {
|
||||||
|
#[must_use]
|
||||||
|
pub fn get(&self, random: &mut RandomGenerator, min_y: i8, height: u16) -> i32 {
|
||||||
|
let min = self.min_inclusive.get_y(min_y as i16, height);
|
||||||
|
let max = self.max_inclusive.get_y(min_y as i16, height);
|
||||||
|
if min >= max {
|
||||||
|
min
|
||||||
|
} else {
|
||||||
|
random.next_inbetween_i32(min, max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct TrapezoidHeightProvider {
|
||||||
|
pub min_inclusive: YOffset,
|
||||||
|
pub max_inclusive: YOffset,
|
||||||
|
pub plateau: Option<i32>,
|
||||||
|
}
|
||||||
|
impl TrapezoidHeightProvider {
|
||||||
|
#[must_use]
|
||||||
|
pub fn get(&self, random: &mut RandomGenerator, min_y: i8, height: u16) -> i32 {
|
||||||
|
let plateau = self.plateau.unwrap_or(0);
|
||||||
|
let i = self.min_inclusive.get_y(min_y as i16, height);
|
||||||
|
let j = self.max_inclusive.get_y(min_y as i16, height);
|
||||||
|
if i >= j {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
let k = j - i;
|
||||||
|
if plateau >= k {
|
||||||
|
return random.next_inbetween_i32(i, j);
|
||||||
|
}
|
||||||
|
let l = (k - plateau) / 2;
|
||||||
|
let m = k - l;
|
||||||
|
i + random.next_inbetween_i32(0, m) + random.next_inbetween_i32(0, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct VeryBiasedToBottomHeightProvider {
|
||||||
|
pub min_inclusive: YOffset,
|
||||||
|
pub max_inclusive: YOffset,
|
||||||
|
pub inner: Option<std::num::NonZero<u32>>,
|
||||||
|
}
|
||||||
|
impl VeryBiasedToBottomHeightProvider {
|
||||||
|
#[must_use]
|
||||||
|
pub fn get(&self, random: &mut RandomGenerator, min_y: i8, height: u16) -> i32 {
|
||||||
|
let min = self.min_inclusive.get_y(min_y as i16, height);
|
||||||
|
let max = self.max_inclusive.get_y(min_y as i16, height);
|
||||||
|
let inner = self.inner.map_or(1, std::num::NonZero::get) as i32;
|
||||||
|
if min >= max {
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
let min_rnd = random.next_inbetween_i32(min + inner, max);
|
||||||
|
let max_rnd = random.next_inbetween_i32(min, min_rnd - 1);
|
||||||
|
random.next_inbetween_i32(min, max_rnd - 1 + inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum PoolAliasBinding {
|
||||||
|
Direct {
|
||||||
|
alias: &'static str,
|
||||||
|
target: &'static str,
|
||||||
|
},
|
||||||
|
Random {
|
||||||
|
alias: &'static str,
|
||||||
|
targets: &'static [WeightedPoolAliasTarget],
|
||||||
|
},
|
||||||
|
RandomGroup {
|
||||||
|
groups: &'static [WeightedPoolAliasGroup],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct WeightedPoolAliasTarget {
|
||||||
|
pub target: &'static str,
|
||||||
|
pub weight: u32,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct WeightedPoolAliasGroup {
|
||||||
|
pub bindings: &'static [PoolAliasBinding],
|
||||||
|
pub weight: u32,
|
||||||
|
}
|
||||||
pub struct Structure {
|
pub struct Structure {
|
||||||
pub biomes: &'static str,
|
pub biomes: &'static str,
|
||||||
pub step: GenerationStep,
|
pub step: GenerationStep,
|
||||||
@ -254,12 +374,19 @@ pub struct Structure {
|
|||||||
pub start_jigsaw_name: Option<&'static str>,
|
pub start_jigsaw_name: Option<&'static str>,
|
||||||
pub size: Option<i32>,
|
pub size: Option<i32>,
|
||||||
pub terrain_adaptation: TerrainAdaptation,
|
pub terrain_adaptation: TerrainAdaptation,
|
||||||
pub start_height: Option<i16>,
|
pub start_height: Option<HeightProvider>,
|
||||||
pub project_start_to_heightmap: Option<&'static str>,
|
pub project_start_to_heightmap: Option<&'static str>,
|
||||||
pub max_distance_from_center: Option<i32>,
|
pub max_distance_from_center: Option<i32>,
|
||||||
pub liquid_settings: Option<&'static str>,
|
pub liquid_settings: Option<&'static str>,
|
||||||
pub dimension_padding: Option<i32>,
|
pub dimension_padding: Option<i32>,
|
||||||
pub use_expansion_hack: Option<bool>,
|
pub use_expansion_hack: Option<bool>,
|
||||||
|
pub pool_aliases: &'static [PoolAliasBinding],
|
||||||
|
pub spawn_overrides: &'static [SpawnOverride],
|
||||||
|
pub is_beached: Option<bool>,
|
||||||
|
pub mineshaft_type: Option<&'static str>,
|
||||||
|
pub biome_temp: Option<&'static str>,
|
||||||
|
pub cluster_probability: Option<f32>,
|
||||||
|
pub large_probability: Option<f32>,
|
||||||
pub structure_type: StructureType,
|
pub structure_type: StructureType,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
@ -299,12 +426,63 @@ impl Structure {
|
|||||||
start_jigsaw_name: Some("minecraft:city_anchor"),
|
start_jigsaw_name: Some("minecraft:city_anchor"),
|
||||||
size: Some(7i32),
|
size: Some(7i32),
|
||||||
terrain_adaptation: TerrainAdaptation::BeardBox,
|
terrain_adaptation: TerrainAdaptation::BeardBox,
|
||||||
start_height: Some(-27i16),
|
start_height: Some(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: YOffset::Absolute(Absolute { absolute: -27i16 }),
|
||||||
|
max_inclusive: YOffset::Absolute(Absolute { absolute: -27i16 }),
|
||||||
|
})),
|
||||||
project_start_to_heightmap: None,
|
project_start_to_heightmap: None,
|
||||||
max_distance_from_center: Some(116i32),
|
max_distance_from_center: Some(116i32),
|
||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: Some(false),
|
use_expansion_hack: Some(false),
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[
|
||||||
|
SpawnOverride {
|
||||||
|
category: "ambient",
|
||||||
|
bounding_box: BoundingBoxType::Full,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "axolotls",
|
||||||
|
bounding_box: BoundingBoxType::Full,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "creature",
|
||||||
|
bounding_box: BoundingBoxType::Full,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "misc",
|
||||||
|
bounding_box: BoundingBoxType::Full,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "monster",
|
||||||
|
bounding_box: BoundingBoxType::Full,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "underground_water_creature",
|
||||||
|
bounding_box: BoundingBoxType::Full,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "water_ambient",
|
||||||
|
bounding_box: BoundingBoxType::Full,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "water_creature",
|
||||||
|
bounding_box: BoundingBoxType::Full,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Jigsaw,
|
structure_type: StructureType::Jigsaw,
|
||||||
};
|
};
|
||||||
pub const BASTION_REMNANT: Self = Structure {
|
pub const BASTION_REMNANT: Self = Structure {
|
||||||
@ -314,12 +492,22 @@ impl Structure {
|
|||||||
start_jigsaw_name: None,
|
start_jigsaw_name: None,
|
||||||
size: Some(6i32),
|
size: Some(6i32),
|
||||||
terrain_adaptation: TerrainAdaptation::None,
|
terrain_adaptation: TerrainAdaptation::None,
|
||||||
start_height: Some(33i16),
|
start_height: Some(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: YOffset::Absolute(Absolute { absolute: 33i16 }),
|
||||||
|
max_inclusive: YOffset::Absolute(Absolute { absolute: 33i16 }),
|
||||||
|
})),
|
||||||
project_start_to_heightmap: None,
|
project_start_to_heightmap: None,
|
||||||
max_distance_from_center: Some(80i32),
|
max_distance_from_center: Some(80i32),
|
||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: Some(false),
|
use_expansion_hack: Some(false),
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Jigsaw,
|
structure_type: StructureType::Jigsaw,
|
||||||
};
|
};
|
||||||
pub const BURIED_TREASURE: Self = Structure {
|
pub const BURIED_TREASURE: Self = Structure {
|
||||||
@ -335,6 +523,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::BuriedTreasure,
|
structure_type: StructureType::BuriedTreasure,
|
||||||
};
|
};
|
||||||
pub const DESERT_PYRAMID: Self = Structure {
|
pub const DESERT_PYRAMID: Self = Structure {
|
||||||
@ -350,6 +545,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::DesertPyramid,
|
structure_type: StructureType::DesertPyramid,
|
||||||
};
|
};
|
||||||
pub const END_CITY: Self = Structure {
|
pub const END_CITY: Self = Structure {
|
||||||
@ -365,6 +567,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::EndCity,
|
structure_type: StructureType::EndCity,
|
||||||
};
|
};
|
||||||
pub const FORTRESS: Self = Structure {
|
pub const FORTRESS: Self = Structure {
|
||||||
@ -380,6 +589,48 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[SpawnOverride {
|
||||||
|
category: "monster",
|
||||||
|
bounding_box: BoundingBoxType::Piece,
|
||||||
|
spawns: &[
|
||||||
|
SpawnEntry {
|
||||||
|
entity_type: "minecraft:blaze",
|
||||||
|
min_count: 2u32,
|
||||||
|
max_count: 3u32,
|
||||||
|
weight: 10u32,
|
||||||
|
},
|
||||||
|
SpawnEntry {
|
||||||
|
entity_type: "minecraft:zombified_piglin",
|
||||||
|
min_count: 4u32,
|
||||||
|
max_count: 4u32,
|
||||||
|
weight: 5u32,
|
||||||
|
},
|
||||||
|
SpawnEntry {
|
||||||
|
entity_type: "minecraft:wither_skeleton",
|
||||||
|
min_count: 5u32,
|
||||||
|
max_count: 5u32,
|
||||||
|
weight: 8u32,
|
||||||
|
},
|
||||||
|
SpawnEntry {
|
||||||
|
entity_type: "minecraft:skeleton",
|
||||||
|
min_count: 5u32,
|
||||||
|
max_count: 5u32,
|
||||||
|
weight: 2u32,
|
||||||
|
},
|
||||||
|
SpawnEntry {
|
||||||
|
entity_type: "minecraft:magma_cube",
|
||||||
|
min_count: 4u32,
|
||||||
|
max_count: 4u32,
|
||||||
|
weight: 3u32,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Fortress,
|
structure_type: StructureType::Fortress,
|
||||||
};
|
};
|
||||||
pub const IGLOO: Self = Structure {
|
pub const IGLOO: Self = Structure {
|
||||||
@ -395,6 +646,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Igloo,
|
structure_type: StructureType::Igloo,
|
||||||
};
|
};
|
||||||
pub const JUNGLE_PYRAMID: Self = Structure {
|
pub const JUNGLE_PYRAMID: Self = Structure {
|
||||||
@ -410,6 +668,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::JungleTemple,
|
structure_type: StructureType::JungleTemple,
|
||||||
};
|
};
|
||||||
pub const MANSION: Self = Structure {
|
pub const MANSION: Self = Structure {
|
||||||
@ -425,6 +690,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::WoodlandMansion,
|
structure_type: StructureType::WoodlandMansion,
|
||||||
};
|
};
|
||||||
pub const MINESHAFT: Self = Structure {
|
pub const MINESHAFT: Self = Structure {
|
||||||
@ -440,6 +712,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: Some("normal"),
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Mineshaft,
|
structure_type: StructureType::Mineshaft,
|
||||||
};
|
};
|
||||||
pub const MINESHAFT_MESA: Self = Structure {
|
pub const MINESHAFT_MESA: Self = Structure {
|
||||||
@ -455,6 +734,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: Some("mesa"),
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Mineshaft,
|
structure_type: StructureType::Mineshaft,
|
||||||
};
|
};
|
||||||
pub const MONUMENT: Self = Structure {
|
pub const MONUMENT: Self = Structure {
|
||||||
@ -470,6 +756,34 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[
|
||||||
|
SpawnOverride {
|
||||||
|
category: "axolotls",
|
||||||
|
bounding_box: BoundingBoxType::Full,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "monster",
|
||||||
|
bounding_box: BoundingBoxType::Full,
|
||||||
|
spawns: &[SpawnEntry {
|
||||||
|
entity_type: "minecraft:guardian",
|
||||||
|
min_count: 2u32,
|
||||||
|
max_count: 4u32,
|
||||||
|
weight: 1u32,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "underground_water_creature",
|
||||||
|
bounding_box: BoundingBoxType::Full,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::OceanMonument,
|
structure_type: StructureType::OceanMonument,
|
||||||
};
|
};
|
||||||
pub const NETHER_FOSSIL: Self = Structure {
|
pub const NETHER_FOSSIL: Self = Structure {
|
||||||
@ -479,12 +793,22 @@ impl Structure {
|
|||||||
start_jigsaw_name: None,
|
start_jigsaw_name: None,
|
||||||
size: None,
|
size: None,
|
||||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||||
start_height: None,
|
start_height: Some(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: YOffset::Absolute(Absolute { absolute: 32i16 }),
|
||||||
|
max_inclusive: YOffset::BelowTop(BelowTop { below_top: 2i8 }),
|
||||||
|
})),
|
||||||
project_start_to_heightmap: None,
|
project_start_to_heightmap: None,
|
||||||
max_distance_from_center: None,
|
max_distance_from_center: None,
|
||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::NetherFossil,
|
structure_type: StructureType::NetherFossil,
|
||||||
};
|
};
|
||||||
pub const OCEAN_RUIN_COLD: Self = Structure {
|
pub const OCEAN_RUIN_COLD: Self = Structure {
|
||||||
@ -500,6 +824,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: Some("cold"),
|
||||||
|
cluster_probability: Some(0.9f32),
|
||||||
|
large_probability: Some(0.3f32),
|
||||||
structure_type: StructureType::OceanRuin,
|
structure_type: StructureType::OceanRuin,
|
||||||
};
|
};
|
||||||
pub const OCEAN_RUIN_WARM: Self = Structure {
|
pub const OCEAN_RUIN_WARM: Self = Structure {
|
||||||
@ -515,6 +846,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: Some("warm"),
|
||||||
|
cluster_probability: Some(0.9f32),
|
||||||
|
large_probability: Some(0.3f32),
|
||||||
structure_type: StructureType::OceanRuin,
|
structure_type: StructureType::OceanRuin,
|
||||||
};
|
};
|
||||||
pub const PILLAGER_OUTPOST: Self = Structure {
|
pub const PILLAGER_OUTPOST: Self = Structure {
|
||||||
@ -524,12 +862,31 @@ impl Structure {
|
|||||||
start_jigsaw_name: None,
|
start_jigsaw_name: None,
|
||||||
size: Some(7i32),
|
size: Some(7i32),
|
||||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||||
start_height: Some(0i16),
|
start_height: Some(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: YOffset::Absolute(Absolute { absolute: 0i16 }),
|
||||||
|
max_inclusive: YOffset::Absolute(Absolute { absolute: 0i16 }),
|
||||||
|
})),
|
||||||
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
||||||
max_distance_from_center: Some(80i32),
|
max_distance_from_center: Some(80i32),
|
||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: Some(true),
|
use_expansion_hack: Some(true),
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[SpawnOverride {
|
||||||
|
category: "monster",
|
||||||
|
bounding_box: BoundingBoxType::Full,
|
||||||
|
spawns: &[SpawnEntry {
|
||||||
|
entity_type: "minecraft:pillager",
|
||||||
|
min_count: 1u32,
|
||||||
|
max_count: 1u32,
|
||||||
|
weight: 1u32,
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Jigsaw,
|
structure_type: StructureType::Jigsaw,
|
||||||
};
|
};
|
||||||
pub const RUINED_PORTAL: Self = Structure {
|
pub const RUINED_PORTAL: Self = Structure {
|
||||||
@ -545,6 +902,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::RuinedPortal,
|
structure_type: StructureType::RuinedPortal,
|
||||||
};
|
};
|
||||||
pub const RUINED_PORTAL_DESERT: Self = Structure {
|
pub const RUINED_PORTAL_DESERT: Self = Structure {
|
||||||
@ -560,6 +924,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::RuinedPortal,
|
structure_type: StructureType::RuinedPortal,
|
||||||
};
|
};
|
||||||
pub const RUINED_PORTAL_JUNGLE: Self = Structure {
|
pub const RUINED_PORTAL_JUNGLE: Self = Structure {
|
||||||
@ -575,6 +946,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::RuinedPortal,
|
structure_type: StructureType::RuinedPortal,
|
||||||
};
|
};
|
||||||
pub const RUINED_PORTAL_MOUNTAIN: Self = Structure {
|
pub const RUINED_PORTAL_MOUNTAIN: Self = Structure {
|
||||||
@ -590,6 +968,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::RuinedPortal,
|
structure_type: StructureType::RuinedPortal,
|
||||||
};
|
};
|
||||||
pub const RUINED_PORTAL_NETHER: Self = Structure {
|
pub const RUINED_PORTAL_NETHER: Self = Structure {
|
||||||
@ -605,6 +990,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::RuinedPortal,
|
structure_type: StructureType::RuinedPortal,
|
||||||
};
|
};
|
||||||
pub const RUINED_PORTAL_OCEAN: Self = Structure {
|
pub const RUINED_PORTAL_OCEAN: Self = Structure {
|
||||||
@ -620,6 +1012,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::RuinedPortal,
|
structure_type: StructureType::RuinedPortal,
|
||||||
};
|
};
|
||||||
pub const RUINED_PORTAL_SWAMP: Self = Structure {
|
pub const RUINED_PORTAL_SWAMP: Self = Structure {
|
||||||
@ -635,6 +1034,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::RuinedPortal,
|
structure_type: StructureType::RuinedPortal,
|
||||||
};
|
};
|
||||||
pub const SHIPWRECK: Self = Structure {
|
pub const SHIPWRECK: Self = Structure {
|
||||||
@ -650,6 +1056,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: Some(false),
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Shipwreck,
|
structure_type: StructureType::Shipwreck,
|
||||||
};
|
};
|
||||||
pub const SHIPWRECK_BEACHED: Self = Structure {
|
pub const SHIPWRECK_BEACHED: Self = Structure {
|
||||||
@ -665,6 +1078,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: Some(true),
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Shipwreck,
|
structure_type: StructureType::Shipwreck,
|
||||||
};
|
};
|
||||||
pub const STRONGHOLD: Self = Structure {
|
pub const STRONGHOLD: Self = Structure {
|
||||||
@ -680,6 +1100,13 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Stronghold,
|
structure_type: StructureType::Stronghold,
|
||||||
};
|
};
|
||||||
pub const SWAMP_HUT: Self = Structure {
|
pub const SWAMP_HUT: Self = Structure {
|
||||||
@ -695,6 +1122,34 @@ impl Structure {
|
|||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: None,
|
use_expansion_hack: None,
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[
|
||||||
|
SpawnOverride {
|
||||||
|
category: "creature",
|
||||||
|
bounding_box: BoundingBoxType::Piece,
|
||||||
|
spawns: &[SpawnEntry {
|
||||||
|
entity_type: "minecraft:cat",
|
||||||
|
min_count: 1u32,
|
||||||
|
max_count: 1u32,
|
||||||
|
weight: 1u32,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "monster",
|
||||||
|
bounding_box: BoundingBoxType::Piece,
|
||||||
|
spawns: &[SpawnEntry {
|
||||||
|
entity_type: "minecraft:witch",
|
||||||
|
min_count: 1u32,
|
||||||
|
max_count: 1u32,
|
||||||
|
weight: 1u32,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::SwampHut,
|
structure_type: StructureType::SwampHut,
|
||||||
};
|
};
|
||||||
pub const TRAIL_RUINS: Self = Structure {
|
pub const TRAIL_RUINS: Self = Structure {
|
||||||
@ -704,12 +1159,22 @@ impl Structure {
|
|||||||
start_jigsaw_name: None,
|
start_jigsaw_name: None,
|
||||||
size: Some(7i32),
|
size: Some(7i32),
|
||||||
terrain_adaptation: TerrainAdaptation::Bury,
|
terrain_adaptation: TerrainAdaptation::Bury,
|
||||||
start_height: Some(-15i16),
|
start_height: Some(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: YOffset::Absolute(Absolute { absolute: -15i16 }),
|
||||||
|
max_inclusive: YOffset::Absolute(Absolute { absolute: -15i16 }),
|
||||||
|
})),
|
||||||
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
||||||
max_distance_from_center: Some(80i32),
|
max_distance_from_center: Some(80i32),
|
||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: Some(false),
|
use_expansion_hack: Some(false),
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Jigsaw,
|
structure_type: StructureType::Jigsaw,
|
||||||
};
|
};
|
||||||
pub const TRIAL_CHAMBERS: Self = Structure {
|
pub const TRIAL_CHAMBERS: Self = Structure {
|
||||||
@ -719,12 +1184,145 @@ impl Structure {
|
|||||||
start_jigsaw_name: None,
|
start_jigsaw_name: None,
|
||||||
size: Some(20i32),
|
size: Some(20i32),
|
||||||
terrain_adaptation: TerrainAdaptation::Encapsulate,
|
terrain_adaptation: TerrainAdaptation::Encapsulate,
|
||||||
start_height: Some(-40i16),
|
start_height: Some(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: YOffset::Absolute(Absolute { absolute: -40i16 }),
|
||||||
|
max_inclusive: YOffset::Absolute(Absolute { absolute: -20i16 }),
|
||||||
|
})),
|
||||||
project_start_to_heightmap: None,
|
project_start_to_heightmap: None,
|
||||||
max_distance_from_center: Some(116i32),
|
max_distance_from_center: Some(116i32),
|
||||||
liquid_settings: Some("ignore_waterlogging"),
|
liquid_settings: Some("ignore_waterlogging"),
|
||||||
dimension_padding: Some(10i32),
|
dimension_padding: Some(10i32),
|
||||||
use_expansion_hack: Some(false),
|
use_expansion_hack: Some(false),
|
||||||
|
pool_aliases: &[
|
||||||
|
PoolAliasBinding::RandomGroup {
|
||||||
|
groups: &[
|
||||||
|
WeightedPoolAliasGroup {
|
||||||
|
bindings: &[
|
||||||
|
PoolAliasBinding::Direct {
|
||||||
|
alias: "minecraft:trial_chambers/spawner/contents/ranged",
|
||||||
|
target: "minecraft:trial_chambers/spawner/ranged/skeleton",
|
||||||
|
},
|
||||||
|
PoolAliasBinding::Direct {
|
||||||
|
alias: "minecraft:trial_chambers/spawner/contents/slow_ranged",
|
||||||
|
target: "minecraft:trial_chambers/spawner/slow_ranged/skeleton",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
weight: 1u32,
|
||||||
|
},
|
||||||
|
WeightedPoolAliasGroup {
|
||||||
|
bindings: &[
|
||||||
|
PoolAliasBinding::Direct {
|
||||||
|
alias: "minecraft:trial_chambers/spawner/contents/ranged",
|
||||||
|
target: "minecraft:trial_chambers/spawner/ranged/stray",
|
||||||
|
},
|
||||||
|
PoolAliasBinding::Direct {
|
||||||
|
alias: "minecraft:trial_chambers/spawner/contents/slow_ranged",
|
||||||
|
target: "minecraft:trial_chambers/spawner/slow_ranged/stray",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
weight: 1u32,
|
||||||
|
},
|
||||||
|
WeightedPoolAliasGroup {
|
||||||
|
bindings: &[
|
||||||
|
PoolAliasBinding::Direct {
|
||||||
|
alias: "minecraft:trial_chambers/spawner/contents/ranged",
|
||||||
|
target: "minecraft:trial_chambers/spawner/ranged/poison_skeleton",
|
||||||
|
},
|
||||||
|
PoolAliasBinding::Direct {
|
||||||
|
alias: "minecraft:trial_chambers/spawner/contents/slow_ranged",
|
||||||
|
target: "minecraft:trial_chambers/spawner/slow_ranged/poison_skeleton",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
weight: 1u32,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
PoolAliasBinding::Random {
|
||||||
|
alias: "minecraft:trial_chambers/spawner/contents/melee",
|
||||||
|
targets: &[
|
||||||
|
WeightedPoolAliasTarget {
|
||||||
|
target: "minecraft:trial_chambers/spawner/melee/zombie",
|
||||||
|
weight: 1u32,
|
||||||
|
},
|
||||||
|
WeightedPoolAliasTarget {
|
||||||
|
target: "minecraft:trial_chambers/spawner/melee/husk",
|
||||||
|
weight: 1u32,
|
||||||
|
},
|
||||||
|
WeightedPoolAliasTarget {
|
||||||
|
target: "minecraft:trial_chambers/spawner/melee/spider",
|
||||||
|
weight: 1u32,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
PoolAliasBinding::Random {
|
||||||
|
alias: "minecraft:trial_chambers/spawner/contents/small_melee",
|
||||||
|
targets: &[
|
||||||
|
WeightedPoolAliasTarget {
|
||||||
|
target: "minecraft:trial_chambers/spawner/small_melee/slime",
|
||||||
|
weight: 1u32,
|
||||||
|
},
|
||||||
|
WeightedPoolAliasTarget {
|
||||||
|
target: "minecraft:trial_chambers/spawner/small_melee/cave_spider",
|
||||||
|
weight: 1u32,
|
||||||
|
},
|
||||||
|
WeightedPoolAliasTarget {
|
||||||
|
target: "minecraft:trial_chambers/spawner/small_melee/silverfish",
|
||||||
|
weight: 1u32,
|
||||||
|
},
|
||||||
|
WeightedPoolAliasTarget {
|
||||||
|
target: "minecraft:trial_chambers/spawner/small_melee/baby_zombie",
|
||||||
|
weight: 1u32,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
spawn_overrides: &[
|
||||||
|
SpawnOverride {
|
||||||
|
category: "ambient",
|
||||||
|
bounding_box: BoundingBoxType::Piece,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "axolotls",
|
||||||
|
bounding_box: BoundingBoxType::Piece,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "creature",
|
||||||
|
bounding_box: BoundingBoxType::Piece,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "misc",
|
||||||
|
bounding_box: BoundingBoxType::Piece,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "monster",
|
||||||
|
bounding_box: BoundingBoxType::Piece,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "underground_water_creature",
|
||||||
|
bounding_box: BoundingBoxType::Piece,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "water_ambient",
|
||||||
|
bounding_box: BoundingBoxType::Piece,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
SpawnOverride {
|
||||||
|
category: "water_creature",
|
||||||
|
bounding_box: BoundingBoxType::Piece,
|
||||||
|
spawns: &[],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Jigsaw,
|
structure_type: StructureType::Jigsaw,
|
||||||
};
|
};
|
||||||
pub const VILLAGE_DESERT: Self = Structure {
|
pub const VILLAGE_DESERT: Self = Structure {
|
||||||
@ -734,12 +1332,22 @@ impl Structure {
|
|||||||
start_jigsaw_name: None,
|
start_jigsaw_name: None,
|
||||||
size: Some(6i32),
|
size: Some(6i32),
|
||||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||||
start_height: Some(0i16),
|
start_height: Some(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: YOffset::Absolute(Absolute { absolute: 0i16 }),
|
||||||
|
max_inclusive: YOffset::Absolute(Absolute { absolute: 0i16 }),
|
||||||
|
})),
|
||||||
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
||||||
max_distance_from_center: Some(80i32),
|
max_distance_from_center: Some(80i32),
|
||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: Some(true),
|
use_expansion_hack: Some(true),
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Jigsaw,
|
structure_type: StructureType::Jigsaw,
|
||||||
};
|
};
|
||||||
pub const VILLAGE_PLAINS: Self = Structure {
|
pub const VILLAGE_PLAINS: Self = Structure {
|
||||||
@ -749,12 +1357,22 @@ impl Structure {
|
|||||||
start_jigsaw_name: None,
|
start_jigsaw_name: None,
|
||||||
size: Some(6i32),
|
size: Some(6i32),
|
||||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||||
start_height: Some(0i16),
|
start_height: Some(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: YOffset::Absolute(Absolute { absolute: 0i16 }),
|
||||||
|
max_inclusive: YOffset::Absolute(Absolute { absolute: 0i16 }),
|
||||||
|
})),
|
||||||
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
||||||
max_distance_from_center: Some(80i32),
|
max_distance_from_center: Some(80i32),
|
||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: Some(true),
|
use_expansion_hack: Some(true),
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Jigsaw,
|
structure_type: StructureType::Jigsaw,
|
||||||
};
|
};
|
||||||
pub const VILLAGE_SAVANNA: Self = Structure {
|
pub const VILLAGE_SAVANNA: Self = Structure {
|
||||||
@ -764,12 +1382,22 @@ impl Structure {
|
|||||||
start_jigsaw_name: None,
|
start_jigsaw_name: None,
|
||||||
size: Some(6i32),
|
size: Some(6i32),
|
||||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||||
start_height: Some(0i16),
|
start_height: Some(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: YOffset::Absolute(Absolute { absolute: 0i16 }),
|
||||||
|
max_inclusive: YOffset::Absolute(Absolute { absolute: 0i16 }),
|
||||||
|
})),
|
||||||
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
||||||
max_distance_from_center: Some(80i32),
|
max_distance_from_center: Some(80i32),
|
||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: Some(true),
|
use_expansion_hack: Some(true),
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Jigsaw,
|
structure_type: StructureType::Jigsaw,
|
||||||
};
|
};
|
||||||
pub const VILLAGE_SNOWY: Self = Structure {
|
pub const VILLAGE_SNOWY: Self = Structure {
|
||||||
@ -779,12 +1407,22 @@ impl Structure {
|
|||||||
start_jigsaw_name: None,
|
start_jigsaw_name: None,
|
||||||
size: Some(6i32),
|
size: Some(6i32),
|
||||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||||
start_height: Some(0i16),
|
start_height: Some(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: YOffset::Absolute(Absolute { absolute: 0i16 }),
|
||||||
|
max_inclusive: YOffset::Absolute(Absolute { absolute: 0i16 }),
|
||||||
|
})),
|
||||||
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
||||||
max_distance_from_center: Some(80i32),
|
max_distance_from_center: Some(80i32),
|
||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: Some(true),
|
use_expansion_hack: Some(true),
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Jigsaw,
|
structure_type: StructureType::Jigsaw,
|
||||||
};
|
};
|
||||||
pub const VILLAGE_TAIGA: Self = Structure {
|
pub const VILLAGE_TAIGA: Self = Structure {
|
||||||
@ -794,12 +1432,22 @@ impl Structure {
|
|||||||
start_jigsaw_name: None,
|
start_jigsaw_name: None,
|
||||||
size: Some(6i32),
|
size: Some(6i32),
|
||||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||||
start_height: Some(0i16),
|
start_height: Some(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: YOffset::Absolute(Absolute { absolute: 0i16 }),
|
||||||
|
max_inclusive: YOffset::Absolute(Absolute { absolute: 0i16 }),
|
||||||
|
})),
|
||||||
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
project_start_to_heightmap: Some("WORLD_SURFACE_WG"),
|
||||||
max_distance_from_center: Some(80i32),
|
max_distance_from_center: Some(80i32),
|
||||||
liquid_settings: None,
|
liquid_settings: None,
|
||||||
dimension_padding: None,
|
dimension_padding: None,
|
||||||
use_expansion_hack: Some(true),
|
use_expansion_hack: Some(true),
|
||||||
|
pool_aliases: &[],
|
||||||
|
spawn_overrides: &[],
|
||||||
|
is_beached: None,
|
||||||
|
mineshaft_type: None,
|
||||||
|
biome_temp: None,
|
||||||
|
cluster_probability: None,
|
||||||
|
large_probability: None,
|
||||||
structure_type: StructureType::Jigsaw,
|
structure_type: StructureType::Jigsaw,
|
||||||
};
|
};
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use serde::Deserialize;
|
|||||||
///
|
///
|
||||||
/// The enum is deserialized in an untagged form, meaning the variant
|
/// The enum is deserialized in an untagged form, meaning the variant
|
||||||
/// is selected based on the provided field.
|
/// is selected based on the provided field.
|
||||||
#[derive(Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum YOffset {
|
pub enum YOffset {
|
||||||
/// Absolute world Y coordinate.
|
/// Absolute world Y coordinate.
|
||||||
@ -47,21 +47,21 @@ impl YOffset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Absolute vertical position in world coordinates.
|
/// Absolute vertical position in world coordinates.
|
||||||
#[derive(Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||||
pub struct Absolute {
|
pub struct Absolute {
|
||||||
/// The fixed world Y level.
|
/// The fixed world Y level.
|
||||||
pub absolute: i16,
|
pub absolute: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Offset measured upward from the world's minimum Y level.
|
/// Offset measured upward from the world's minimum Y level.
|
||||||
#[derive(Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||||
pub struct AboveBottom {
|
pub struct AboveBottom {
|
||||||
/// Number of blocks above the minimum Y.
|
/// Number of blocks above the minimum Y.
|
||||||
pub above_bottom: i8,
|
pub above_bottom: i8,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Offset measured downward from the world's top boundary.
|
/// Offset measured downward from the world's top boundary.
|
||||||
#[derive(Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||||
pub struct BelowTop {
|
pub struct BelowTop {
|
||||||
/// Number of blocks below the maximum Y.
|
/// Number of blocks below the maximum Y.
|
||||||
pub below_top: i8,
|
pub below_top: i8,
|
||||||
|
|||||||
@ -81,7 +81,8 @@ pub fn generate_structure_position(
|
|||||||
let (Some(start_pool), Some(size)) = (structure.start_pool, structure.size) else {
|
let (Some(start_pool), Some(size)) = (structure.start_pool, structure.size) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let mut generator = JigsawGenerator::new(start_pool, size);
|
let mut generator =
|
||||||
|
JigsawGenerator::new(start_pool, size).with_pool_aliases(structure.pool_aliases);
|
||||||
if structure.use_expansion_hack.unwrap_or(false) {
|
if structure.use_expansion_hack.unwrap_or(false) {
|
||||||
generator = generator.with_expansion_hack(true);
|
generator = generator.with_expansion_hack(true);
|
||||||
}
|
}
|
||||||
@ -92,7 +93,9 @@ pub fn generate_structure_position(
|
|||||||
}
|
}
|
||||||
StructureKeys::Shipwreck | StructureKeys::ShipwreckBeached => {
|
StructureKeys::Shipwreck | StructureKeys::ShipwreckBeached => {
|
||||||
let generator = ShipwreckGenerator {
|
let generator = ShipwreckGenerator {
|
||||||
is_beached: *key == StructureKeys::ShipwreckBeached,
|
is_beached: structure
|
||||||
|
.is_beached
|
||||||
|
.unwrap_or(*key == StructureKeys::ShipwreckBeached),
|
||||||
};
|
};
|
||||||
generator.get_structure_position(context)
|
generator.get_structure_position(context)
|
||||||
}
|
}
|
||||||
@ -108,7 +111,9 @@ pub fn generate_structure_position(
|
|||||||
}
|
}
|
||||||
StructureKeys::OceanRuinCold | StructureKeys::OceanRuinWarm => {
|
StructureKeys::OceanRuinCold | StructureKeys::OceanRuinWarm => {
|
||||||
let generator = OceanRuinGenerator {
|
let generator = OceanRuinGenerator {
|
||||||
is_warm: *key == StructureKeys::OceanRuinWarm,
|
is_warm: structure
|
||||||
|
.biome_temp
|
||||||
|
.map_or(*key == StructureKeys::OceanRuinWarm, |t| t == "warm"),
|
||||||
};
|
};
|
||||||
generator.get_structure_position(context)
|
generator.get_structure_position(context)
|
||||||
}
|
}
|
||||||
@ -117,7 +122,9 @@ pub fn generate_structure_position(
|
|||||||
StructureKeys::Monument => OceanMonumentGenerator.get_structure_position(context),
|
StructureKeys::Monument => OceanMonumentGenerator.get_structure_position(context),
|
||||||
StructureKeys::Mineshaft | StructureKeys::MineshaftMesa => {
|
StructureKeys::Mineshaft | StructureKeys::MineshaftMesa => {
|
||||||
let generator = MineshaftGenerator {
|
let generator = MineshaftGenerator {
|
||||||
is_mesa: *key == StructureKeys::MineshaftMesa,
|
is_mesa: structure
|
||||||
|
.mineshaft_type
|
||||||
|
.map_or(*key == StructureKeys::MineshaftMesa, |t| t == "mesa"),
|
||||||
};
|
};
|
||||||
generator.get_structure_position(context)
|
generator.get_structure_position(context)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -765,6 +765,7 @@ pub struct JigsawGenerator {
|
|||||||
pub size: i32,
|
pub size: i32,
|
||||||
pub start_jigsaw_name: Option<String>,
|
pub start_jigsaw_name: Option<String>,
|
||||||
pub use_expansion_hack: bool,
|
pub use_expansion_hack: bool,
|
||||||
|
pub pool_aliases: &'static [pumpkin_data::structures::PoolAliasBinding],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JigsawGenerator {
|
impl JigsawGenerator {
|
||||||
@ -775,6 +776,18 @@ impl JigsawGenerator {
|
|||||||
size,
|
size,
|
||||||
start_jigsaw_name: None,
|
start_jigsaw_name: None,
|
||||||
use_expansion_hack: false,
|
use_expansion_hack: false,
|
||||||
|
pool_aliases: &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn new_with_pool(start_pool: &str, size: i32) -> Self {
|
||||||
|
Self {
|
||||||
|
start_pool: start_pool.to_string(),
|
||||||
|
size,
|
||||||
|
start_jigsaw_name: None,
|
||||||
|
use_expansion_hack: false,
|
||||||
|
pool_aliases: &[],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -789,6 +802,15 @@ impl JigsawGenerator {
|
|||||||
self.use_expansion_hack = use_hack;
|
self.use_expansion_hack = use_hack;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn with_pool_aliases(
|
||||||
|
mut self,
|
||||||
|
aliases: &'static [pumpkin_data::structures::PoolAliasBinding],
|
||||||
|
) -> Self {
|
||||||
|
self.pool_aliases = aliases;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StructureGenerator for JigsawGenerator {
|
impl StructureGenerator for JigsawGenerator {
|
||||||
@ -801,8 +823,11 @@ impl StructureGenerator for JigsawGenerator {
|
|||||||
.structure_key
|
.structure_key
|
||||||
.map(|key| pumpkin_data::structures::Structure::get(&key));
|
.map(|key| pumpkin_data::structures::Structure::get(&key));
|
||||||
|
|
||||||
|
let height = if context.min_y < 0 { 384 } else { 256 };
|
||||||
let start_y = if let Some(s) = structure {
|
let start_y = if let Some(s) = structure {
|
||||||
s.start_height.unwrap_or(context.sea_level as i16) as i32
|
s.start_height.map_or(context.sea_level, |hp| {
|
||||||
|
hp.get(&mut context.random, context.min_y as i8, height)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
context.sea_level
|
context.sea_level
|
||||||
};
|
};
|
||||||
@ -847,9 +872,27 @@ impl StructureGenerator for JigsawGenerator {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.or_else(|| structure.and_then(|s| s.start_jigsaw_name));
|
.or_else(|| structure.and_then(|s| s.start_jigsaw_name));
|
||||||
|
|
||||||
|
let pool_aliases = if !self.pool_aliases.is_empty() {
|
||||||
|
self.pool_aliases
|
||||||
|
} else if let Some(s) = structure {
|
||||||
|
s.pool_aliases
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
|
|
||||||
|
let pool_alias_lookup = PoolAliasLookup::from_bindings(pool_aliases, &mut context.random);
|
||||||
|
|
||||||
|
let start_pool = if self.start_pool.is_empty() {
|
||||||
|
structure
|
||||||
|
.and_then(|s| s.start_pool)
|
||||||
|
.unwrap_or(&self.start_pool)
|
||||||
|
} else {
|
||||||
|
&self.start_pool
|
||||||
|
};
|
||||||
|
|
||||||
JigsawPlacement::add_pieces(
|
JigsawPlacement::add_pieces(
|
||||||
&mut context,
|
&mut context,
|
||||||
&self.start_pool,
|
start_pool,
|
||||||
start_jigsaw,
|
start_jigsaw,
|
||||||
self.size,
|
self.size,
|
||||||
start_pos,
|
start_pos,
|
||||||
@ -858,7 +901,7 @@ impl StructureGenerator for JigsawGenerator {
|
|||||||
&MaxDistance::new(max_distance),
|
&MaxDistance::new(max_distance),
|
||||||
dimension_padding,
|
dimension_padding,
|
||||||
liquid_settings,
|
liquid_settings,
|
||||||
&PoolAliasLookup::default(),
|
&pool_alias_lookup,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,7 +58,7 @@ pub const MAX_DEPTH: i32 = 20;
|
|||||||
/// Dynamic lookup for Pool Aliases introduced in 1.20+ (e.g. Trial Chambers spawner/contents aliases).
|
/// Dynamic lookup for Pool Aliases introduced in 1.20+ (e.g. Trial Chambers spawner/contents aliases).
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct PoolAliasLookup {
|
pub struct PoolAliasLookup {
|
||||||
aliases: HashMap<String, Vec<(String, u32)>>,
|
aliases: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PoolAliasLookup {
|
impl PoolAliasLookup {
|
||||||
@ -69,39 +69,112 @@ impl PoolAliasLookup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_direct(&mut self, alias: String, target: String) {
|
|
||||||
self.aliases.insert(alias, vec![(target, 1)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_random(&mut self, alias: String, targets: Vec<(String, u32)>) {
|
|
||||||
self.aliases.insert(alias, targets);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn lookup<'a>(&'a self, id: &'a str, random: &mut RandomGenerator) -> &'a str {
|
pub fn from_bindings(
|
||||||
if let Some(targets) = self.aliases.get(id) {
|
bindings: &[pumpkin_data::structures::PoolAliasBinding],
|
||||||
if targets.is_empty() {
|
random: &mut RandomGenerator,
|
||||||
return id;
|
) -> Self {
|
||||||
|
let mut lookup = Self::new();
|
||||||
|
for binding in bindings {
|
||||||
|
lookup.add_binding(binding, random);
|
||||||
}
|
}
|
||||||
if targets.len() == 1 {
|
lookup
|
||||||
return &targets[0].0;
|
}
|
||||||
|
|
||||||
|
fn add_binding(
|
||||||
|
&mut self,
|
||||||
|
binding: &pumpkin_data::structures::PoolAliasBinding,
|
||||||
|
random: &mut RandomGenerator,
|
||||||
|
) {
|
||||||
|
use pumpkin_data::structures::PoolAliasBinding;
|
||||||
|
match binding {
|
||||||
|
PoolAliasBinding::Direct { alias, target } => {
|
||||||
|
self.add_direct((*alias).to_string(), (*target).to_string());
|
||||||
|
}
|
||||||
|
PoolAliasBinding::Random { alias, targets } => {
|
||||||
|
if let Some(target) = Self::pick_weighted(targets, random, |t| t.weight) {
|
||||||
|
self.add_direct((*alias).to_string(), target.target.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PoolAliasBinding::RandomGroup { groups } => {
|
||||||
|
if let Some(group) = Self::pick_weighted(groups, random, |g| g.weight) {
|
||||||
|
for b in group.bindings {
|
||||||
|
self.add_binding(b, random);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick_weighted<'a, T>(
|
||||||
|
items: &'a [T],
|
||||||
|
random: &mut RandomGenerator,
|
||||||
|
get_weight: impl Fn(&T) -> u32,
|
||||||
|
) -> Option<&'a T> {
|
||||||
|
if items.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let total_weight: u32 = items.iter().map(&get_weight).sum();
|
||||||
|
if total_weight == 0 {
|
||||||
|
return items.first();
|
||||||
|
}
|
||||||
|
let mut r = random.next_bounded_i32(total_weight as i32) as u32;
|
||||||
|
for item in items {
|
||||||
|
let w = get_weight(item);
|
||||||
|
if r < w {
|
||||||
|
return Some(item);
|
||||||
|
}
|
||||||
|
r -= w;
|
||||||
|
}
|
||||||
|
items.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_direct(&mut self, alias: String, target: String) {
|
||||||
|
if let Some(stripped) = alias.strip_prefix("minecraft:") {
|
||||||
|
self.aliases.insert(stripped.to_string(), target.clone());
|
||||||
|
} else {
|
||||||
|
self.aliases
|
||||||
|
.insert(format!("minecraft:{alias}"), target.clone());
|
||||||
|
}
|
||||||
|
self.aliases.insert(alias, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_random(
|
||||||
|
&mut self,
|
||||||
|
alias: String,
|
||||||
|
targets: &[(String, u32)],
|
||||||
|
random: &mut RandomGenerator,
|
||||||
|
) {
|
||||||
|
if targets.is_empty() {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
let total_weight: u32 = targets.iter().map(|(_, w)| *w).sum();
|
let total_weight: u32 = targets.iter().map(|(_, w)| *w).sum();
|
||||||
if total_weight == 0 {
|
if total_weight == 0 {
|
||||||
return &targets[0].0;
|
if let Some((target, _)) = targets.first() {
|
||||||
|
self.add_direct(alias, target.clone());
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
let mut r = random.next_bounded_i32(total_weight as i32) as u32;
|
let mut r = random.next_bounded_i32(total_weight as i32) as u32;
|
||||||
for (target, weight) in targets {
|
for (target, weight) in targets {
|
||||||
if r < *weight {
|
if r < *weight {
|
||||||
return target.as_str();
|
self.add_direct(alias, target.clone());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
r -= *weight;
|
r -= *weight;
|
||||||
}
|
}
|
||||||
&targets[0].0
|
if let Some((target, _)) = targets.first() {
|
||||||
} else {
|
self.add_direct(alias, target.clone());
|
||||||
id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn lookup<'a>(&'a self, mut id: &'a str, _random: &mut RandomGenerator) -> &'a str {
|
||||||
|
while let Some(target) = self.aliases.get(id) {
|
||||||
|
id = target.as_str();
|
||||||
|
}
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@ -64,8 +64,16 @@ impl StructureGenerator for NetherFossilGenerator {
|
|||||||
let x = start_block_x(context.chunk_x) + context.random.next_bounded_i32(16);
|
let x = start_block_x(context.chunk_x) + context.random.next_bounded_i32(16);
|
||||||
let z = start_block_z(context.chunk_z) + context.random.next_bounded_i32(16);
|
let z = start_block_z(context.chunk_z) + context.random.next_bounded_i32(16);
|
||||||
|
|
||||||
|
let structure = context
|
||||||
|
.structure_key
|
||||||
|
.map(|key| pumpkin_data::structures::Structure::get(&key));
|
||||||
|
|
||||||
|
let initial_y = if let Some(hp) = structure.and_then(|s| s.start_height) {
|
||||||
|
hp.get(&mut context.random, context.min_y as i8, 256)
|
||||||
|
} else {
|
||||||
let height_range = HEIGHT_MAX - HEIGHT_MIN + 1;
|
let height_range = HEIGHT_MAX - HEIGHT_MIN + 1;
|
||||||
let initial_y = HEIGHT_MIN + context.random.next_bounded_i32(height_range);
|
HEIGHT_MIN + context.random.next_bounded_i32(height_range)
|
||||||
|
};
|
||||||
|
|
||||||
// Column scan is deferred to place() since we don't have block data here.
|
// Column scan is deferred to place() since we don't have block data here.
|
||||||
// Consume random in vanilla order for determinism.
|
// Consume random in vanilla order for determinism.
|
||||||
|
|||||||
@ -150,18 +150,16 @@ impl StructurePieceBase for ChestCorridorPiece {
|
|||||||
inner.add_block(chunk, slab, 2, 1, i, &box_limit);
|
inner.add_block(chunk, slab, 2, 1, i, &box_limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if !self.chest_generated {
|
if !self.chest_generated {
|
||||||
// // Check if the target chest position is within the current chunk being processed
|
self.chest_generated = inner.add_chest(
|
||||||
// let chest_pos = inner.to_world(3, 2, 3);
|
chunk,
|
||||||
// if box_limit.contains(&chest_pos) {
|
&box_limit,
|
||||||
// self.chest_generated = true;
|
random,
|
||||||
|
3,
|
||||||
// let chest_state = Block::CHEST.default_state;
|
2,
|
||||||
// // Note: In a full implementation, you would use a helper to set the LootTable NBT here
|
3,
|
||||||
// inner.add_block(chunk, &chest_state, 3, 2, 3, &box_limit);
|
"minecraft:chests/stronghold_corridor",
|
||||||
|
);
|
||||||
// // chunk.set_loot_table(chest_pos, "minecraft:chests/stronghold_corridor");
|
}
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,6 +73,7 @@ impl StructurePieceBase for LibraryPiece {
|
|||||||
&mut self.piece.piece
|
&mut self.piece.piece
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::too_many_lines)]
|
||||||
fn place(
|
fn place(
|
||||||
&mut self,
|
&mut self,
|
||||||
chunk: &mut ProtoChunk,
|
chunk: &mut ProtoChunk,
|
||||||
@ -234,10 +235,26 @@ impl StructurePieceBase for LibraryPiece {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 6. Chests
|
// 6. Chests
|
||||||
// inner.add_chest(chunk, &box_limit, random, 3, 3, 5, "stronghold_library");
|
inner.add_chest(
|
||||||
// if self.tall {
|
chunk,
|
||||||
// inner.add_block(chunk, &Block::AIR.default_state, 12, 9, 1, &box_limit); // Clear space above top chest
|
&box_limit,
|
||||||
// inner.add_chest(chunk, &box_limit, random, 12, 8, 1, "stronghold_library");
|
random,
|
||||||
// }
|
3,
|
||||||
|
3,
|
||||||
|
5,
|
||||||
|
"minecraft:chests/stronghold_library",
|
||||||
|
);
|
||||||
|
if self.tall {
|
||||||
|
inner.add_block(chunk, Block::AIR.default_state, 12, 9, 1, &box_limit); // Clear space above top chest
|
||||||
|
inner.add_chest(
|
||||||
|
chunk,
|
||||||
|
&box_limit,
|
||||||
|
random,
|
||||||
|
12,
|
||||||
|
8,
|
||||||
|
1,
|
||||||
|
"minecraft:chests/stronghold_library",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,8 +29,8 @@ use crate::world::WorldPortalExt;
|
|||||||
const CROSSING_LOOT_TABLE: &str = "minecraft:chests/stronghold_crossing";
|
const CROSSING_LOOT_TABLE: &str = "minecraft:chests/stronghold_crossing";
|
||||||
|
|
||||||
pub struct SquareRoomPiece {
|
pub struct SquareRoomPiece {
|
||||||
piece: StrongholdPiece,
|
pub piece: StrongholdPiece,
|
||||||
room_type: u32,
|
pub room_type: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SquareRoomPiece {
|
impl SquareRoomPiece {
|
||||||
|
|||||||
@ -46,7 +46,10 @@ pub use cache::{
|
|||||||
};
|
};
|
||||||
pub use processor::StructureProcessor;
|
pub use processor::StructureProcessor;
|
||||||
pub use pumpkin_data::{BlockState, Mirror as BlockMirror, Rotation as BlockRotation};
|
pub use pumpkin_data::{BlockState, Mirror as BlockMirror, Rotation as BlockRotation};
|
||||||
pub use structure_template::{PaletteEntry, StructureTemplate, TemplateBlock, TemplateEntity};
|
pub use structure_template::{
|
||||||
|
JigsawBlockInfo, Palette, PaletteEntry, SimplePalette, StructureBlockInfo, StructureEntityInfo,
|
||||||
|
StructurePlaceSettings, StructureTemplate, TemplateBlock, TemplateEntity,
|
||||||
|
};
|
||||||
pub use template_piece::TemplatePiece;
|
pub use template_piece::TemplatePiece;
|
||||||
|
|
||||||
/// Abstraction over block placement, implemented by both [`ProtoChunk`] (worldgen) and
|
/// Abstraction over block placement, implemented by both [`ProtoChunk`] (worldgen) and
|
||||||
@ -93,7 +96,7 @@ pub fn place_template(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
||||||
pub fn place_template_with_options(
|
pub fn place_template_with_options(
|
||||||
placer: &mut impl BlockPlacer,
|
placer: &mut impl BlockPlacer,
|
||||||
template: &StructureTemplate,
|
template: &StructureTemplate,
|
||||||
@ -110,6 +113,9 @@ pub fn place_template_with_options(
|
|||||||
let world_x = origin.x + rotated_ox;
|
let world_x = origin.x + rotated_ox;
|
||||||
let world_z = origin.z + rotated_oz;
|
let world_z = origin.z + rotated_oz;
|
||||||
|
|
||||||
|
let mut context_rng = LegacyRand::from_seed(hash_block_pos(world_x, origin.y, world_z) as u64);
|
||||||
|
let mut context = processor::ProcessorContext::new(origin, processors, &mut context_rng);
|
||||||
|
|
||||||
for block in &template.blocks {
|
for block in &template.blocks {
|
||||||
let palette_entry = &template.palette[block.state as usize];
|
let palette_entry = &template.palette[block.state as usize];
|
||||||
|
|
||||||
@ -182,8 +188,19 @@ pub fn place_template_with_options(
|
|||||||
|
|
||||||
// Apply processors
|
// Apply processors
|
||||||
let mut should_place = true;
|
let mut should_place = true;
|
||||||
|
let mut capped_idx = 0;
|
||||||
|
let mut rng =
|
||||||
|
LegacyRand::from_seed(hash_block_pos(world_pos.x, world_pos.y, world_pos.z) as u64);
|
||||||
for processor in processors {
|
for processor in processors {
|
||||||
let Some(processed_state) = processor.process(placer, world_pos, state) else {
|
let Some(processed_state) = processor.process_with_context(
|
||||||
|
placer,
|
||||||
|
world_pos,
|
||||||
|
state,
|
||||||
|
&mut block_entity_nbt,
|
||||||
|
&mut context,
|
||||||
|
&mut capped_idx,
|
||||||
|
&mut rng,
|
||||||
|
) else {
|
||||||
should_place = false;
|
should_place = false;
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
@ -200,23 +217,20 @@ pub fn place_template_with_options(
|
|||||||
placer.set_block_state(&Vector3::new(wx, wy, wz), state);
|
placer.set_block_state(&Vector3::new(wx, wy, wz), state);
|
||||||
|
|
||||||
// Create block entities for interactive blocks (furnaces, chests, etc.)
|
// Create block entities for interactive blocks (furnaces, chests, etc.)
|
||||||
let block_entity_id = get_block_entity_id(&placed_entry.name);
|
let final_block = pumpkin_data::Block::from_id(state.id.to_block_id());
|
||||||
|
let block_entity_id = get_block_entity_id(final_block.name);
|
||||||
if block_entity_nbt.is_some() || block_entity_id.is_some() {
|
if block_entity_nbt.is_some() || block_entity_id.is_some() {
|
||||||
let block_entity_id = block_entity_id.unwrap_or(&placed_entry.name);
|
let fallback_id = block_entity_id.unwrap_or(final_block.name);
|
||||||
let mut placed_nbt = NbtCompound::new();
|
let mut placed_nbt = NbtCompound::new();
|
||||||
|
|
||||||
placed_nbt.put_string("id", block_entity_id.to_string());
|
placed_nbt.put_string("id", fallback_id.to_string());
|
||||||
placed_nbt.put_int("x", wx);
|
placed_nbt.put_int("x", wx);
|
||||||
placed_nbt.put_int("y", wy);
|
placed_nbt.put_int("y", wy);
|
||||||
placed_nbt.put_int("z", wz);
|
placed_nbt.put_int("z", wz);
|
||||||
|
|
||||||
if let Some(template_nbt) = &block_entity_nbt {
|
if let Some(template_nbt) = &block_entity_nbt {
|
||||||
for (key, value) in &template_nbt.child_tags {
|
for (key, value) in &template_nbt.child_tags {
|
||||||
if key.as_ref() != "x"
|
if key.as_ref() != "x" && key.as_ref() != "y" && key.as_ref() != "z" {
|
||||||
&& key.as_ref() != "y"
|
|
||||||
&& key.as_ref() != "z"
|
|
||||||
&& key.as_ref() != "id"
|
|
||||||
{
|
|
||||||
placed_nbt.child_tags.insert(key.clone(), value.clone());
|
placed_nbt.child_tags.insert(key.clone(), value.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -301,46 +315,63 @@ pub(crate) fn place_template_entities(
|
|||||||
|
|
||||||
/// Returns the block entity ID for blocks that require one, or None if not needed.
|
/// Returns the block entity ID for blocks that require one, or None if not needed.
|
||||||
pub(crate) fn get_block_entity_id(block_name: &str) -> Option<&'static str> {
|
pub(crate) fn get_block_entity_id(block_name: &str) -> Option<&'static str> {
|
||||||
match block_name {
|
let name = block_name.strip_prefix("minecraft:").unwrap_or(block_name);
|
||||||
"minecraft:furnace" => Some("minecraft:furnace"),
|
match name {
|
||||||
"minecraft:chest" => Some("minecraft:chest"),
|
"furnace" => Some("minecraft:furnace"),
|
||||||
"minecraft:trapped_chest" => Some("minecraft:trapped_chest"),
|
"chest" => Some("minecraft:chest"),
|
||||||
"minecraft:barrel" => Some("minecraft:barrel"),
|
"trapped_chest" => Some("minecraft:trapped_chest"),
|
||||||
"minecraft:hopper" => Some("minecraft:hopper"),
|
"barrel" => Some("minecraft:barrel"),
|
||||||
"minecraft:dropper" => Some("minecraft:dropper"),
|
"hopper" => Some("minecraft:hopper"),
|
||||||
"minecraft:dispenser" => Some("minecraft:dispenser"),
|
"dropper" => Some("minecraft:dropper"),
|
||||||
"minecraft:brewing_stand" => Some("minecraft:brewing_stand"),
|
"dispenser" => Some("minecraft:dispenser"),
|
||||||
"minecraft:blast_furnace" => Some("minecraft:blast_furnace"),
|
"brewing_stand" => Some("minecraft:brewing_stand"),
|
||||||
"minecraft:smoker" => Some("minecraft:smoker"),
|
"blast_furnace" => Some("minecraft:blast_furnace"),
|
||||||
"minecraft:shulker_box" => Some("minecraft:shulker_box"),
|
"smoker" => Some("minecraft:smoker"),
|
||||||
"minecraft:bed" => Some("minecraft:bed"),
|
"shulker_box" => Some("minecraft:shulker_box"),
|
||||||
"minecraft:dragon_head"
|
"bed" => Some("minecraft:bed"),
|
||||||
| "minecraft:dragon_wall_head"
|
"suspicious_sand" | "suspicious_gravel" => Some("minecraft:brushable_block"),
|
||||||
| "minecraft:skeleton_skull"
|
"decorated_pot" => Some("minecraft:decorated_pot"),
|
||||||
| "minecraft:skeleton_wall_skull"
|
"spawner" => Some("minecraft:mob_spawner"),
|
||||||
| "minecraft:wither_skeleton_skull"
|
"trial_spawner" => Some("minecraft:trial_spawner"),
|
||||||
| "minecraft:wither_skeleton_wall_skull"
|
"vault" => Some("minecraft:vault"),
|
||||||
| "minecraft:zombie_head"
|
"crafter" => Some("minecraft:crafter"),
|
||||||
| "minecraft:zombie_wall_head"
|
"creaking_heart" => Some("minecraft:creaking_heart"),
|
||||||
| "minecraft:player_head"
|
"chiseled_bookshelf" => Some("minecraft:chiseled_bookshelf"),
|
||||||
| "minecraft:player_wall_head"
|
"beehive" | "bee_nest" => Some("minecraft:beehive"),
|
||||||
| "minecraft:creeper_head"
|
"campfire" | "soul_campfire" => Some("minecraft:campfire"),
|
||||||
| "minecraft:creeper_wall_head"
|
"sculk_sensor" | "calibrated_sculk_sensor" => Some("minecraft:sculk_sensor"),
|
||||||
| "minecraft:piglin_head"
|
"sculk_catalyst" => Some("minecraft:sculk_catalyst"),
|
||||||
| "minecraft:piglin_wall_head" => Some("minecraft:skull"),
|
"sculk_shrieker" => Some("minecraft:sculk_shrieker"),
|
||||||
"minecraft:sign"
|
"dragon_head"
|
||||||
| "minecraft:oak_sign"
|
| "dragon_wall_head"
|
||||||
| "minecraft:spruce_sign"
|
| "skeleton_skull"
|
||||||
| "minecraft:birch_sign"
|
| "skeleton_wall_skull"
|
||||||
| "minecraft:jungle_sign"
|
| "wither_skeleton_skull"
|
||||||
| "minecraft:acacia_sign"
|
| "wither_skeleton_wall_skull"
|
||||||
| "minecraft:dark_oak_sign"
|
| "zombie_head"
|
||||||
| "minecraft:mangrove_sign"
|
| "zombie_wall_head"
|
||||||
| "minecraft:cherry_sign"
|
| "player_head"
|
||||||
| "minecraft:bamboo_sign"
|
| "player_wall_head"
|
||||||
| "minecraft:crimson_sign"
|
| "creeper_head"
|
||||||
| "minecraft:warped_sign" => Some("minecraft:sign"),
|
| "creeper_wall_head"
|
||||||
"minecraft:hanging_sign" => Some("minecraft:hanging_sign"),
|
| "piglin_head"
|
||||||
|
| "piglin_wall_head" => Some("minecraft:skull"),
|
||||||
|
"sign" | "oak_sign" | "spruce_sign" | "birch_sign" | "jungle_sign" | "acacia_sign"
|
||||||
|
| "dark_oak_sign" | "mangrove_sign" | "cherry_sign" | "bamboo_sign" | "crimson_sign"
|
||||||
|
| "warped_sign" | "pale_oak_sign" => Some("minecraft:sign"),
|
||||||
|
"hanging_sign"
|
||||||
|
| "oak_hanging_sign"
|
||||||
|
| "spruce_hanging_sign"
|
||||||
|
| "birch_hanging_sign"
|
||||||
|
| "jungle_hanging_sign"
|
||||||
|
| "acacia_hanging_sign"
|
||||||
|
| "dark_oak_hanging_sign"
|
||||||
|
| "mangrove_hanging_sign"
|
||||||
|
| "cherry_hanging_sign"
|
||||||
|
| "bamboo_hanging_sign"
|
||||||
|
| "crimson_hanging_sign"
|
||||||
|
| "warped_hanging_sign"
|
||||||
|
| "pale_oak_hanging_sign" => Some("minecraft:hanging_sign"),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
681
crates/pumpkin/src/command/commands/datapack.rs
Normal file
681
crates/pumpkin/src/command/commands/datapack.rs
Normal file
@ -0,0 +1,681 @@
|
|||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use pumpkin_data::translation;
|
||||||
|
use pumpkin_util::PermissionLvl;
|
||||||
|
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
|
||||||
|
use pumpkin_util::text::TextComponent;
|
||||||
|
use pumpkin_util::text::color::NamedColor;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal};
|
||||||
|
use crate::command::argument_types::core::string::StringArgumentType;
|
||||||
|
use crate::command::context::command_context::CommandContext;
|
||||||
|
use crate::command::errors::error_types::CommandErrorType;
|
||||||
|
use crate::command::node::dispatcher::CommandDispatcher;
|
||||||
|
use crate::command::node::{CommandExecutor, CommandExecutorResult};
|
||||||
|
use crate::command::suggestion::provider::{SuggestionProvider, SuggestionProviderResult};
|
||||||
|
use crate::command::suggestion::suggestions::SuggestionsBuilder;
|
||||||
|
use crate::server::Server;
|
||||||
|
|
||||||
|
const DESCRIPTION: &str = "Controls loaded data packs.";
|
||||||
|
const PERMISSION: &str = "minecraft:command.datapack";
|
||||||
|
|
||||||
|
static ERROR_UNKNOWN_DATAPACK: CommandErrorType<1> = CommandErrorType::new(
|
||||||
|
translation::java::COMMANDS_DATAPACK_UNKNOWN,
|
||||||
|
translation::java::COMMANDS_DATAPACK_UNKNOWN,
|
||||||
|
);
|
||||||
|
|
||||||
|
static ERROR_ENABLE_FAILED: CommandErrorType<1> = CommandErrorType::new(
|
||||||
|
translation::java::COMMANDS_DATAPACK_ENABLE_FAILED,
|
||||||
|
translation::java::COMMANDS_DATAPACK_ENABLE_FAILED,
|
||||||
|
);
|
||||||
|
|
||||||
|
static ERROR_DISABLE_FAILED: CommandErrorType<1> = CommandErrorType::new(
|
||||||
|
translation::java::COMMANDS_DATAPACK_DISABLE_FAILED,
|
||||||
|
translation::java::COMMANDS_DATAPACK_DISABLE_FAILED,
|
||||||
|
);
|
||||||
|
|
||||||
|
static ERROR_CREATE_ALREADY_EXISTS: CommandErrorType<1> = CommandErrorType::new(
|
||||||
|
translation::java::COMMANDS_DATAPACK_CREATE_ALREADY_EXISTS,
|
||||||
|
translation::java::COMMANDS_DATAPACK_CREATE_ALREADY_EXISTS,
|
||||||
|
);
|
||||||
|
|
||||||
|
static ERROR_CREATE_INVALID_NAME: CommandErrorType<1> = CommandErrorType::new(
|
||||||
|
translation::java::COMMANDS_DATAPACK_CREATE_INVALID_NAME,
|
||||||
|
translation::java::COMMANDS_DATAPACK_CREATE_INVALID_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
static ERROR_CREATE_IO_FAILURE: CommandErrorType<1> = CommandErrorType::new(
|
||||||
|
translation::java::COMMANDS_DATAPACK_CREATE_IO_FAILURE,
|
||||||
|
translation::java::COMMANDS_DATAPACK_CREATE_IO_FAILURE,
|
||||||
|
);
|
||||||
|
|
||||||
|
fn get_all_known_packs(server: &Server) -> Vec<String> {
|
||||||
|
let mut packs = Vec::new();
|
||||||
|
packs.push("vanilla".to_string());
|
||||||
|
|
||||||
|
// Bundled feature packs
|
||||||
|
for bundled in [
|
||||||
|
"trade_rebalance",
|
||||||
|
"minecart_improvements",
|
||||||
|
"redstone_experiments",
|
||||||
|
] {
|
||||||
|
if !packs.iter().any(|p| p == bundled) {
|
||||||
|
packs.push(bundled.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// World datapacks directory
|
||||||
|
let datapacks_dir = server.basic_config.get_world_path().join("datapacks");
|
||||||
|
if let Ok(entries) = fs::read_dir(datapacks_dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if file_name.starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if path.is_dir()
|
||||||
|
|| path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
|
||||||
|
{
|
||||||
|
let pack_name = format!("file/{file_name}");
|
||||||
|
if !packs.iter().any(|p| p == &pack_name) {
|
||||||
|
packs.push(pack_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let level_info = server.level_info.load();
|
||||||
|
for pack in &level_info.data_packs.enabled {
|
||||||
|
if !packs.iter().any(|p| p == pack) {
|
||||||
|
packs.push(pack.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for pack in &level_info.data_packs.disabled {
|
||||||
|
if !packs.iter().any(|p| p == pack) {
|
||||||
|
packs.push(pack.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_enabled_packs(server: &Server) -> Vec<String> {
|
||||||
|
server.level_info.load().data_packs.enabled.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_available_packs(server: &Server) -> Vec<String> {
|
||||||
|
let enabled = get_enabled_packs(server);
|
||||||
|
let all = get_all_known_packs(server);
|
||||||
|
all.into_iter().filter(|p| !enabled.contains(p)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_pack_name(server: &Server, input: &str) -> Option<String> {
|
||||||
|
let known = get_all_known_packs(server);
|
||||||
|
if let Some(p) = known.iter().find(|p| *p == input) {
|
||||||
|
return Some(p.clone());
|
||||||
|
}
|
||||||
|
let file_input = format!("file/{input}");
|
||||||
|
if let Some(p) = known.iter().find(|p| **p == file_input) {
|
||||||
|
return Some(p.clone());
|
||||||
|
}
|
||||||
|
if let Some(p) = known
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.strip_prefix("file/") == Some(input))
|
||||||
|
{
|
||||||
|
return Some(p.clone());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_pack(name: &str) -> TextComponent {
|
||||||
|
TextComponent::text(format!("[{name}]")).color_named(NamedColor::Green)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AvailablePackSuggestionProvider;
|
||||||
|
|
||||||
|
impl SuggestionProvider for AvailablePackSuggestionProvider {
|
||||||
|
fn suggest<'a>(
|
||||||
|
&'a self,
|
||||||
|
context: &'a CommandContext,
|
||||||
|
mut builder: SuggestionsBuilder,
|
||||||
|
) -> SuggestionProviderResult<'a> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let server = context.server();
|
||||||
|
for pack in get_available_packs(server) {
|
||||||
|
if pack.contains(' ') || pack.contains('/') {
|
||||||
|
builder = builder.suggest(format!("\"{pack}\""));
|
||||||
|
} else {
|
||||||
|
builder = builder.suggest(pack.clone());
|
||||||
|
}
|
||||||
|
if let Some(short) = pack.strip_prefix("file/") {
|
||||||
|
if short.contains(' ') {
|
||||||
|
builder = builder.suggest(format!("\"{short}\""));
|
||||||
|
} else {
|
||||||
|
builder = builder.suggest(short.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EnabledPackSuggestionProvider;
|
||||||
|
|
||||||
|
impl SuggestionProvider for EnabledPackSuggestionProvider {
|
||||||
|
fn suggest<'a>(
|
||||||
|
&'a self,
|
||||||
|
context: &'a CommandContext,
|
||||||
|
mut builder: SuggestionsBuilder,
|
||||||
|
) -> SuggestionProviderResult<'a> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let server = context.server();
|
||||||
|
for pack in get_enabled_packs(server) {
|
||||||
|
if pack.contains(' ') || pack.contains('/') {
|
||||||
|
builder = builder.suggest(format!("\"{pack}\""));
|
||||||
|
} else {
|
||||||
|
builder = builder.suggest(pack.clone());
|
||||||
|
}
|
||||||
|
if let Some(short) = pack.strip_prefix("file/") {
|
||||||
|
if short.contains(' ') {
|
||||||
|
builder = builder.suggest(format!("\"{short}\""));
|
||||||
|
} else {
|
||||||
|
builder = builder.suggest(short.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum ListFilter {
|
||||||
|
All,
|
||||||
|
Available,
|
||||||
|
Enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DatapackListExecutor(ListFilter);
|
||||||
|
|
||||||
|
impl CommandExecutor for DatapackListExecutor {
|
||||||
|
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let server = context.server();
|
||||||
|
let enabled = get_enabled_packs(server);
|
||||||
|
let available = get_available_packs(server);
|
||||||
|
|
||||||
|
match self.0 {
|
||||||
|
ListFilter::All => {
|
||||||
|
send_enabled_list(context, &enabled).await;
|
||||||
|
send_available_list(context, &available).await;
|
||||||
|
Ok(enabled.len() as i32)
|
||||||
|
}
|
||||||
|
ListFilter::Enabled => {
|
||||||
|
send_enabled_list(context, &enabled).await;
|
||||||
|
Ok(enabled.len() as i32)
|
||||||
|
}
|
||||||
|
ListFilter::Available => {
|
||||||
|
send_available_list(context, &available).await;
|
||||||
|
Ok(available.len() as i32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_enabled_list(context: &CommandContext<'_>, enabled: &[String]) {
|
||||||
|
if enabled.is_empty() {
|
||||||
|
context
|
||||||
|
.source
|
||||||
|
.send_feedback(
|
||||||
|
TextComponent::translate_cross(
|
||||||
|
translation::java::COMMANDS_DATAPACK_LIST_ENABLED_NONE,
|
||||||
|
translation::java::COMMANDS_DATAPACK_LIST_ENABLED_NONE,
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
let packs_component =
|
||||||
|
TextComponent::join_with_comma(enabled.iter().map(|p| format_pack(p)).collect());
|
||||||
|
context
|
||||||
|
.source
|
||||||
|
.send_feedback(
|
||||||
|
TextComponent::translate_cross(
|
||||||
|
translation::java::COMMANDS_DATAPACK_LIST_ENABLED_SUCCESS,
|
||||||
|
translation::java::COMMANDS_DATAPACK_LIST_ENABLED_SUCCESS,
|
||||||
|
[
|
||||||
|
TextComponent::text(enabled.len().to_string()),
|
||||||
|
packs_component,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_available_list(context: &CommandContext<'_>, available: &[String]) {
|
||||||
|
if available.is_empty() {
|
||||||
|
context
|
||||||
|
.source
|
||||||
|
.send_feedback(
|
||||||
|
TextComponent::translate_cross(
|
||||||
|
translation::java::COMMANDS_DATAPACK_LIST_AVAILABLE_NONE,
|
||||||
|
translation::java::COMMANDS_DATAPACK_LIST_AVAILABLE_NONE,
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
let packs_component =
|
||||||
|
TextComponent::join_with_comma(available.iter().map(|p| format_pack(p)).collect());
|
||||||
|
context
|
||||||
|
.source
|
||||||
|
.send_feedback(
|
||||||
|
TextComponent::translate_cross(
|
||||||
|
translation::java::COMMANDS_DATAPACK_LIST_AVAILABLE_SUCCESS,
|
||||||
|
translation::java::COMMANDS_DATAPACK_LIST_AVAILABLE_SUCCESS,
|
||||||
|
[
|
||||||
|
TextComponent::text(available.len().to_string()),
|
||||||
|
packs_component,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum EnablePosition {
|
||||||
|
First,
|
||||||
|
Last,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DatapackEnableExecutor(EnablePosition);
|
||||||
|
|
||||||
|
impl CommandExecutor for DatapackEnableExecutor {
|
||||||
|
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let name_str = StringArgumentType::get(context, "name")?;
|
||||||
|
let server = context.server();
|
||||||
|
|
||||||
|
let Some(resolved_name) = find_pack_name(server, name_str) else {
|
||||||
|
return Err(ERROR_UNKNOWN_DATAPACK
|
||||||
|
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||||
|
};
|
||||||
|
|
||||||
|
let enabled = get_enabled_packs(server);
|
||||||
|
if enabled.contains(&resolved_name) {
|
||||||
|
return Err(
|
||||||
|
ERROR_ENABLE_FAILED.create_without_context(TextComponent::text(resolved_name))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = resolved_name.clone();
|
||||||
|
let pos = self.0;
|
||||||
|
server.level_info.rcu(|level_info| {
|
||||||
|
let mut new_info = (**level_info).clone();
|
||||||
|
new_info.data_packs.disabled.retain(|p| p != &target);
|
||||||
|
new_info.data_packs.enabled.retain(|p| p != &target);
|
||||||
|
match pos {
|
||||||
|
EnablePosition::First => {
|
||||||
|
new_info.data_packs.enabled.insert(0, target.clone());
|
||||||
|
}
|
||||||
|
EnablePosition::Last => {
|
||||||
|
new_info.data_packs.enabled.push(target.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new_info
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(err) = server.save_world_info() {
|
||||||
|
error!("Failed to save world info: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
server.reload_datapacks(server).await;
|
||||||
|
|
||||||
|
context
|
||||||
|
.source
|
||||||
|
.send_feedback(
|
||||||
|
TextComponent::translate_cross(
|
||||||
|
translation::java::COMMANDS_DATAPACK_MODIFY_ENABLE,
|
||||||
|
translation::java::COMMANDS_DATAPACK_MODIFY_ENABLE,
|
||||||
|
[format_pack(&resolved_name)],
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(server.level_info.load().data_packs.enabled.len() as i32)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum BeforeOrAfter {
|
||||||
|
Before,
|
||||||
|
After,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DatapackEnableExistingExecutor(BeforeOrAfter);
|
||||||
|
|
||||||
|
impl CommandExecutor for DatapackEnableExistingExecutor {
|
||||||
|
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let name_str = StringArgumentType::get(context, "name")?;
|
||||||
|
let existing_str = StringArgumentType::get(context, "existing")?;
|
||||||
|
let server = context.server();
|
||||||
|
|
||||||
|
let Some(target_pack) = find_pack_name(server, name_str) else {
|
||||||
|
return Err(ERROR_UNKNOWN_DATAPACK
|
||||||
|
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(existing_pack) = find_pack_name(server, existing_str) else {
|
||||||
|
return Err(ERROR_UNKNOWN_DATAPACK
|
||||||
|
.create_without_context(TextComponent::text(existing_str.to_string())));
|
||||||
|
};
|
||||||
|
|
||||||
|
let enabled = get_enabled_packs(server);
|
||||||
|
if enabled.contains(&target_pack) {
|
||||||
|
return Err(
|
||||||
|
ERROR_ENABLE_FAILED.create_without_context(TextComponent::text(target_pack))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enabled.contains(&existing_pack) {
|
||||||
|
return Err(
|
||||||
|
ERROR_DISABLE_FAILED.create_without_context(TextComponent::text(existing_pack))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = target_pack.clone();
|
||||||
|
let existing = existing_pack.clone();
|
||||||
|
let before_or_after = self.0;
|
||||||
|
server.level_info.rcu(|level_info| {
|
||||||
|
let mut new_info = (**level_info).clone();
|
||||||
|
new_info.data_packs.disabled.retain(|p| p != &target);
|
||||||
|
new_info.data_packs.enabled.retain(|p| p != &target);
|
||||||
|
|
||||||
|
if let Some(idx) = new_info
|
||||||
|
.data_packs
|
||||||
|
.enabled
|
||||||
|
.iter()
|
||||||
|
.position(|p| p == &existing)
|
||||||
|
{
|
||||||
|
let insert_pos = match before_or_after {
|
||||||
|
BeforeOrAfter::Before => idx,
|
||||||
|
BeforeOrAfter::After => idx + 1,
|
||||||
|
};
|
||||||
|
new_info
|
||||||
|
.data_packs
|
||||||
|
.enabled
|
||||||
|
.insert(insert_pos, target.clone());
|
||||||
|
} else {
|
||||||
|
new_info.data_packs.enabled.push(target.clone());
|
||||||
|
}
|
||||||
|
new_info
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(err) = server.save_world_info() {
|
||||||
|
error!("Failed to save world info: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
server.reload_datapacks(server).await;
|
||||||
|
|
||||||
|
context
|
||||||
|
.source
|
||||||
|
.send_feedback(
|
||||||
|
TextComponent::translate_cross(
|
||||||
|
translation::java::COMMANDS_DATAPACK_MODIFY_ENABLE,
|
||||||
|
translation::java::COMMANDS_DATAPACK_MODIFY_ENABLE,
|
||||||
|
[format_pack(&target_pack)],
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(server.level_info.load().data_packs.enabled.len() as i32)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DatapackDisableExecutor;
|
||||||
|
|
||||||
|
impl CommandExecutor for DatapackDisableExecutor {
|
||||||
|
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let name_str = StringArgumentType::get(context, "name")?;
|
||||||
|
let server = context.server();
|
||||||
|
|
||||||
|
let Some(target_pack) = find_pack_name(server, name_str) else {
|
||||||
|
return Err(ERROR_DISABLE_FAILED
|
||||||
|
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||||
|
};
|
||||||
|
|
||||||
|
let enabled = get_enabled_packs(server);
|
||||||
|
if !enabled.contains(&target_pack) {
|
||||||
|
return Err(ERROR_DISABLE_FAILED
|
||||||
|
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
if target_pack == "vanilla" {
|
||||||
|
return Err(ERROR_DISABLE_FAILED
|
||||||
|
.create_without_context(TextComponent::text("vanilla".to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = target_pack.clone();
|
||||||
|
server.level_info.rcu(|level_info| {
|
||||||
|
let mut new_info = (**level_info).clone();
|
||||||
|
new_info.data_packs.enabled.retain(|p| p != &target);
|
||||||
|
if !new_info.data_packs.disabled.contains(&target) {
|
||||||
|
new_info.data_packs.disabled.push(target.clone());
|
||||||
|
}
|
||||||
|
new_info
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(err) = server.save_world_info() {
|
||||||
|
error!("Failed to save world info: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
server.reload_datapacks(server).await;
|
||||||
|
|
||||||
|
context
|
||||||
|
.source
|
||||||
|
.send_feedback(
|
||||||
|
TextComponent::translate_cross(
|
||||||
|
translation::java::COMMANDS_DATAPACK_MODIFY_DISABLE,
|
||||||
|
translation::java::COMMANDS_DATAPACK_MODIFY_DISABLE,
|
||||||
|
[format_pack(&target_pack)],
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(server.level_info.load().data_packs.enabled.len() as i32)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_pack_name(name: &str) -> bool {
|
||||||
|
!name.is_empty()
|
||||||
|
&& !name.contains('/')
|
||||||
|
&& !name.contains('\\')
|
||||||
|
&& !name.contains("..")
|
||||||
|
&& name
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DatapackCreateExecutor;
|
||||||
|
|
||||||
|
impl CommandExecutor for DatapackCreateExecutor {
|
||||||
|
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let name_str = StringArgumentType::get(context, "name")?;
|
||||||
|
let description = StringArgumentType::get(context, "description").unwrap_or(name_str);
|
||||||
|
|
||||||
|
if !is_valid_pack_name(name_str) {
|
||||||
|
return Err(ERROR_CREATE_INVALID_NAME
|
||||||
|
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
let server = context.server();
|
||||||
|
let datapacks_dir = server.basic_config.get_world_path().join("datapacks");
|
||||||
|
let pack_dir = datapacks_dir.join(name_str);
|
||||||
|
|
||||||
|
if pack_dir.exists() {
|
||||||
|
return Err(ERROR_CREATE_ALREADY_EXISTS
|
||||||
|
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
let data_dir = pack_dir.join("data");
|
||||||
|
if let Err(err) = fs::create_dir_all(&data_dir) {
|
||||||
|
error!("Failed to create datapack directory: {err}");
|
||||||
|
return Err(ERROR_CREATE_IO_FAILURE
|
||||||
|
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mcmeta_content = serde_json::json!({
|
||||||
|
"pack": {
|
||||||
|
"pack_format": 61,
|
||||||
|
"description": description
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mcmeta_path = pack_dir.join("pack.mcmeta");
|
||||||
|
let mcmeta_str = match serde_json::to_string_pretty(&mcmeta_content) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to serialize pack.mcmeta: {err}");
|
||||||
|
return Err(ERROR_CREATE_IO_FAILURE
|
||||||
|
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = fs::write(&mcmeta_path, mcmeta_str) {
|
||||||
|
error!("Failed to write pack.mcmeta: {err}");
|
||||||
|
return Err(ERROR_CREATE_IO_FAILURE
|
||||||
|
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
context
|
||||||
|
.source
|
||||||
|
.send_feedback(
|
||||||
|
TextComponent::translate_cross(
|
||||||
|
translation::java::COMMANDS_DATAPACK_CREATE_SUCCESS,
|
||||||
|
translation::java::COMMANDS_DATAPACK_CREATE_SUCCESS,
|
||||||
|
[TextComponent::text(name_str.to_string())],
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) {
|
||||||
|
registry.register_permission_or_panic(Permission::new(
|
||||||
|
PERMISSION,
|
||||||
|
DESCRIPTION,
|
||||||
|
PermissionDefault::Op(PermissionLvl::Two),
|
||||||
|
));
|
||||||
|
|
||||||
|
let list_builder = literal("list")
|
||||||
|
.then(literal("available").executes(DatapackListExecutor(ListFilter::Available)))
|
||||||
|
.then(literal("enabled").executes(DatapackListExecutor(ListFilter::Enabled)))
|
||||||
|
.executes(DatapackListExecutor(ListFilter::All));
|
||||||
|
|
||||||
|
let enable_builder = literal("enable").then(
|
||||||
|
argument("name", StringArgumentType::QuotablePhrase)
|
||||||
|
.suggests(AvailablePackSuggestionProvider)
|
||||||
|
.then(literal("first").executes(DatapackEnableExecutor(EnablePosition::First)))
|
||||||
|
.then(literal("last").executes(DatapackEnableExecutor(EnablePosition::Last)))
|
||||||
|
.then(
|
||||||
|
literal("before").then(
|
||||||
|
argument("existing", StringArgumentType::QuotablePhrase)
|
||||||
|
.suggests(EnabledPackSuggestionProvider)
|
||||||
|
.executes(DatapackEnableExistingExecutor(BeforeOrAfter::Before)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
literal("after").then(
|
||||||
|
argument("existing", StringArgumentType::QuotablePhrase)
|
||||||
|
.suggests(EnabledPackSuggestionProvider)
|
||||||
|
.executes(DatapackEnableExistingExecutor(BeforeOrAfter::After)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.executes(DatapackEnableExecutor(EnablePosition::Last)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let disable_builder = literal("disable").then(
|
||||||
|
argument("name", StringArgumentType::QuotablePhrase)
|
||||||
|
.suggests(EnabledPackSuggestionProvider)
|
||||||
|
.executes(DatapackDisableExecutor),
|
||||||
|
);
|
||||||
|
|
||||||
|
let create_builder = literal("create").then(
|
||||||
|
argument("name", StringArgumentType::SingleWord)
|
||||||
|
.then(
|
||||||
|
argument("description", StringArgumentType::GreedyPhrase)
|
||||||
|
.executes(DatapackCreateExecutor),
|
||||||
|
)
|
||||||
|
.executes(DatapackCreateExecutor),
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatcher.register(
|
||||||
|
command("datapack", DESCRIPTION)
|
||||||
|
.requires(PERMISSION)
|
||||||
|
.then(list_builder)
|
||||||
|
.then(enable_builder)
|
||||||
|
.then(disable_builder)
|
||||||
|
.then(create_builder),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn valid_pack_name_check() {
|
||||||
|
assert!(is_valid_pack_name("my_pack"));
|
||||||
|
assert!(is_valid_pack_name("test-1.0"));
|
||||||
|
assert!(is_valid_pack_name("pack123"));
|
||||||
|
|
||||||
|
assert!(!is_valid_pack_name(""));
|
||||||
|
assert!(!is_valid_pack_name("../hack"));
|
||||||
|
assert!(!is_valid_pack_name("folder/pack"));
|
||||||
|
assert!(!is_valid_pack_name("pack\\test"));
|
||||||
|
assert!(!is_valid_pack_name("invalid char"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_pack_brackets() {
|
||||||
|
let formatted = format_pack("vanilla");
|
||||||
|
assert!(formatted.to_pretty_console().contains("[vanilla]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pack_mcmeta_json_structure() {
|
||||||
|
let mcmeta_content = serde_json::json!({
|
||||||
|
"pack": {
|
||||||
|
"pack_format": 61,
|
||||||
|
"description": "Test description"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mcmeta_str = serde_json::to_string(&mcmeta_content).unwrap();
|
||||||
|
assert!(mcmeta_str.contains("\"pack_format\":61"));
|
||||||
|
assert!(mcmeta_str.contains("\"description\":\"Test description\""));
|
||||||
|
}
|
||||||
|
}
|
||||||
110
crates/pumpkin/src/command/commands/function.rs
Normal file
110
crates/pumpkin/src/command/commands/function.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
use pumpkin_data::translation;
|
||||||
|
use pumpkin_util::PermissionLvl;
|
||||||
|
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
|
||||||
|
use pumpkin_util::text::TextComponent;
|
||||||
|
|
||||||
|
use crate::command::argument_builder::{ArgumentBuilder, argument, command};
|
||||||
|
use crate::command::argument_types::core::string::StringArgumentType;
|
||||||
|
use crate::command::context::command_context::CommandContext;
|
||||||
|
use crate::command::errors::error_types::CommandErrorType;
|
||||||
|
use crate::command::node::dispatcher::CommandDispatcher;
|
||||||
|
use crate::command::node::{CommandExecutor, CommandExecutorResult};
|
||||||
|
use crate::command::suggestion::provider::{SuggestionProvider, SuggestionProviderResult};
|
||||||
|
use crate::command::suggestion::suggestions::SuggestionsBuilder;
|
||||||
|
|
||||||
|
const DESCRIPTION: &str = "Runs commands found in the corresponding function files.";
|
||||||
|
const PERMISSION: &str = "minecraft:command.function";
|
||||||
|
|
||||||
|
static ERROR_UNKNOWN_FUNCTION: CommandErrorType<1> = CommandErrorType::new(
|
||||||
|
translation::java::ARGUMENTS_FUNCTION_UNKNOWN,
|
||||||
|
translation::java::ARGUMENTS_FUNCTION_UNKNOWN,
|
||||||
|
);
|
||||||
|
|
||||||
|
struct FunctionSuggestionProvider;
|
||||||
|
|
||||||
|
impl SuggestionProvider for FunctionSuggestionProvider {
|
||||||
|
fn suggest<'a>(
|
||||||
|
&'a self,
|
||||||
|
context: &'a CommandContext,
|
||||||
|
mut builder: SuggestionsBuilder,
|
||||||
|
) -> SuggestionProviderResult<'a> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let server = context.server();
|
||||||
|
let function_names = server.datapack_manager.get_function_names().await;
|
||||||
|
for name in function_names {
|
||||||
|
builder = builder.suggest(name);
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FunctionExecutor;
|
||||||
|
|
||||||
|
impl CommandExecutor for FunctionExecutor {
|
||||||
|
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let name_str = StringArgumentType::get(context, "name")?;
|
||||||
|
let server = context.server();
|
||||||
|
|
||||||
|
let Ok(executed_count) = server
|
||||||
|
.datapack_manager
|
||||||
|
.execute_function(server, &context.source, name_str)
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
return Err(ERROR_UNKNOWN_FUNCTION
|
||||||
|
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||||
|
};
|
||||||
|
|
||||||
|
if name_str.starts_with('#') {
|
||||||
|
context
|
||||||
|
.source
|
||||||
|
.send_feedback(
|
||||||
|
TextComponent::translate_cross(
|
||||||
|
translation::java::COMMANDS_FUNCTION_SUCCESS_MULTIPLE,
|
||||||
|
translation::java::COMMANDS_FUNCTION_SUCCESS_MULTIPLE,
|
||||||
|
[
|
||||||
|
TextComponent::text(executed_count.to_string()),
|
||||||
|
TextComponent::text(name_str.to_string()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
context
|
||||||
|
.source
|
||||||
|
.send_feedback(
|
||||||
|
TextComponent::translate_cross(
|
||||||
|
translation::java::COMMANDS_FUNCTION_SUCCESS_SINGLE,
|
||||||
|
translation::java::COMMANDS_FUNCTION_SUCCESS_SINGLE,
|
||||||
|
[
|
||||||
|
TextComponent::text(executed_count.to_string()),
|
||||||
|
TextComponent::text(name_str.to_string()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(executed_count as i32)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) {
|
||||||
|
registry.register_permission_or_panic(Permission::new(
|
||||||
|
PERMISSION,
|
||||||
|
DESCRIPTION,
|
||||||
|
PermissionDefault::Op(PermissionLvl::Two),
|
||||||
|
));
|
||||||
|
|
||||||
|
dispatcher.register(
|
||||||
|
command("function", DESCRIPTION).requires(PERMISSION).then(
|
||||||
|
argument("name", StringArgumentType::SingleWord)
|
||||||
|
.suggests(FunctionSuggestionProvider)
|
||||||
|
.executes(FunctionExecutor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@ mod clear;
|
|||||||
mod clone;
|
mod clone;
|
||||||
mod damage;
|
mod damage;
|
||||||
mod data;
|
mod data;
|
||||||
|
mod datapack;
|
||||||
mod debug;
|
mod debug;
|
||||||
pub mod defaultgamemode;
|
pub mod defaultgamemode;
|
||||||
mod deop;
|
mod deop;
|
||||||
@ -30,6 +31,7 @@ mod fetchprofile;
|
|||||||
mod fill;
|
mod fill;
|
||||||
mod fillbiome;
|
mod fillbiome;
|
||||||
mod forceload;
|
mod forceload;
|
||||||
|
mod function;
|
||||||
mod gamemode;
|
mod gamemode;
|
||||||
mod gamerule;
|
mod gamerule;
|
||||||
mod give;
|
mod give;
|
||||||
@ -209,6 +211,8 @@ pub fn default_dispatcher(
|
|||||||
teammsg::register(&mut dispatcher, registry);
|
teammsg::register(&mut dispatcher, registry);
|
||||||
clone::register(&mut dispatcher, registry);
|
clone::register(&mut dispatcher, registry);
|
||||||
attribute::register(&mut dispatcher, registry);
|
attribute::register(&mut dispatcher, registry);
|
||||||
|
datapack::register(&mut dispatcher, registry);
|
||||||
|
function::register(&mut dispatcher, registry);
|
||||||
fetchprofile::register(&mut dispatcher, registry);
|
fetchprofile::register(&mut dispatcher, registry);
|
||||||
|
|
||||||
apply_command_overrides(&mut dispatcher, registry, commands_config);
|
apply_command_overrides(&mut dispatcher, registry, commands_config);
|
||||||
|
|||||||
@ -312,7 +312,9 @@ impl CommandExecutor for PlaceStructureExecutor {
|
|||||||
.create_without_context(TextComponent::text(structure_name.clone()))
|
.create_without_context(TextComponent::text(structure_name.clone()))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let random = RandomGenerator::Legacy(LegacyRand::from_seed(seed));
|
let mut random = RandomGenerator::Legacy(LegacyRand::from_seed(seed));
|
||||||
|
let pool_alias_lookup =
|
||||||
|
PoolAliasLookup::from_bindings(structure.pool_aliases, &mut random);
|
||||||
|
|
||||||
let position = JigsawPlacement::add_pieces(
|
let position = JigsawPlacement::add_pieces(
|
||||||
&mut StructureGeneratorContext {
|
&mut StructureGeneratorContext {
|
||||||
@ -334,7 +336,7 @@ impl CommandExecutor for PlaceStructureExecutor {
|
|||||||
&MaxDistance::new(structure.max_distance_from_center.unwrap_or(128)),
|
&MaxDistance::new(structure.max_distance_from_center.unwrap_or(128)),
|
||||||
DimensionPadding::ZERO,
|
DimensionPadding::ZERO,
|
||||||
LiquidSettings::ApplyWaterlog,
|
LiquidSettings::ApplyWaterlog,
|
||||||
&PoolAliasLookup::default(),
|
&pool_alias_lookup,
|
||||||
)
|
)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
JIGSAW_FAILED
|
JIGSAW_FAILED
|
||||||
|
|||||||
127
crates/pumpkin/src/data/datapack/function_loader.rs
Normal file
127
crates/pumpkin/src/data/datapack/function_loader.rs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn load_functions_from_dir<S: std::hash::BuildHasher>(
|
||||||
|
namespace: &str,
|
||||||
|
function_dir: &Path,
|
||||||
|
functions: &mut HashMap<String, Vec<String>, S>,
|
||||||
|
) {
|
||||||
|
if !function_dir.is_dir() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
load_functions_recursive(namespace, function_dir, function_dir, functions);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_functions_recursive<S: std::hash::BuildHasher>(
|
||||||
|
namespace: &str,
|
||||||
|
base_dir: &Path,
|
||||||
|
current_dir: &Path,
|
||||||
|
functions: &mut HashMap<String, Vec<String>, S>,
|
||||||
|
) {
|
||||||
|
let Ok(entries) = fs::read_dir(current_dir) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
load_functions_recursive(namespace, base_dir, &path, functions);
|
||||||
|
} else if path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("mcfunction"))
|
||||||
|
&& let Ok(rel_path) = path.strip_prefix(base_dir)
|
||||||
|
{
|
||||||
|
let mut stem_path = rel_path.to_string_lossy().to_string();
|
||||||
|
if let Some(s) = stem_path.strip_suffix(".mcfunction") {
|
||||||
|
stem_path = s.to_string();
|
||||||
|
}
|
||||||
|
// Convert Windows backslashes to forward slashes if any
|
||||||
|
let stem_path = stem_path.replace('\\', "/");
|
||||||
|
let function_id = format!("{namespace}:{stem_path}");
|
||||||
|
if let Ok(content) = fs::read_to_string(&path) {
|
||||||
|
let lines: Vec<String> = content
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
||||||
|
.map(|line| line.strip_prefix('/').unwrap_or(line).to_string())
|
||||||
|
.collect();
|
||||||
|
functions.insert(function_id, lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_function_tags_from_dir<S: std::hash::BuildHasher>(
|
||||||
|
namespace: &str,
|
||||||
|
tags_dir: &Path,
|
||||||
|
function_tags: &mut HashMap<String, Vec<String>, S>,
|
||||||
|
) {
|
||||||
|
let function_tags_dir = tags_dir.join("function");
|
||||||
|
let function_tags_dir_plural = tags_dir.join("functions");
|
||||||
|
for dir in [&function_tags_dir, &function_tags_dir_plural] {
|
||||||
|
if dir.is_dir() {
|
||||||
|
load_tags_recursive(namespace, dir, dir, function_tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_tags_recursive<S: std::hash::BuildHasher>(
|
||||||
|
namespace: &str,
|
||||||
|
base_dir: &Path,
|
||||||
|
current_dir: &Path,
|
||||||
|
tags: &mut HashMap<String, Vec<String>, S>,
|
||||||
|
) {
|
||||||
|
let Ok(entries) = fs::read_dir(current_dir) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
load_tags_recursive(namespace, base_dir, &path, tags);
|
||||||
|
} else if path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
|
||||||
|
&& let Ok(rel_path) = path.strip_prefix(base_dir)
|
||||||
|
{
|
||||||
|
let mut stem_path = rel_path.to_string_lossy().to_string();
|
||||||
|
if let Some(s) = stem_path.strip_suffix(".json") {
|
||||||
|
stem_path = s.to_string();
|
||||||
|
}
|
||||||
|
let stem_path = stem_path.replace('\\', "/");
|
||||||
|
let tag_id = format!("{namespace}:{stem_path}");
|
||||||
|
if let Ok(content) = fs::read_to_string(&path)
|
||||||
|
&& let Ok(val) = serde_json::from_str::<serde_json::Value>(&content)
|
||||||
|
&& let Some(values_arr) = val.get("values").and_then(serde_json::Value::as_array)
|
||||||
|
{
|
||||||
|
let list: Vec<String> = values_arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str().map(ToString::to_string))
|
||||||
|
.collect();
|
||||||
|
tags.entry(tag_id).or_default().extend(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn parse_mcfunction_content() {
|
||||||
|
let content = "# This is a comment\nsay Hello world\n/give @p diamond 1\n\n# Another comment\ngive @p stick 5\n";
|
||||||
|
let lines: Vec<String> = content
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
||||||
|
.map(|line| line.strip_prefix('/').unwrap_or(line).to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
lines,
|
||||||
|
vec![
|
||||||
|
"say Hello world".to_string(),
|
||||||
|
"give @p diamond 1".to_string(),
|
||||||
|
"give @p stick 5".to_string()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
275
crates/pumpkin/src/data/datapack/mod.rs
Normal file
275
crates/pumpkin/src/data/datapack/mod.rs
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
pub mod function_loader;
|
||||||
|
pub mod recipe_loader;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use pumpkin_protocol::codec::recipe::DynamicRecipe;
|
||||||
|
|
||||||
|
use crate::command::context::command_source::CommandSource;
|
||||||
|
use crate::server::Server;
|
||||||
|
use crate::server::recipe::RecipeManager;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LoadedDatapack {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub pack_format: u32,
|
||||||
|
pub root_path: PathBuf,
|
||||||
|
pub recipe_count: usize,
|
||||||
|
pub function_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DatapackManager {
|
||||||
|
loaded_packs: RwLock<Vec<LoadedDatapack>>,
|
||||||
|
functions: RwLock<HashMap<String, Vec<String>>>,
|
||||||
|
function_tags: RwLock<HashMap<String, Vec<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DatapackManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatapackManager {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
loaded_packs: RwLock::new(Vec::new()),
|
||||||
|
functions: RwLock::new(HashMap::new()),
|
||||||
|
function_tags: RwLock::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_all(
|
||||||
|
&self,
|
||||||
|
world_path: &Path,
|
||||||
|
enabled_packs: &[String],
|
||||||
|
recipe_manager: &RecipeManager,
|
||||||
|
) {
|
||||||
|
let datapacks_dir = world_path.join("datapacks");
|
||||||
|
let mut loaded_packs_vec = Vec::new();
|
||||||
|
let mut all_recipes: Vec<DynamicRecipe> = Vec::new();
|
||||||
|
let mut all_functions: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
|
let mut all_function_tags: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
|
|
||||||
|
if datapacks_dir.is_dir() {
|
||||||
|
let Ok(entries) = fs::read_dir(&datapacks_dir) else {
|
||||||
|
warn!(
|
||||||
|
"Failed to read datapacks directory: {}",
|
||||||
|
datapacks_dir.display()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let pack_path = entry.path();
|
||||||
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
if file_name.starts_with('.') || !pack_path.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pack_id = format!("file/{file_name}");
|
||||||
|
let is_enabled = enabled_packs
|
||||||
|
.iter()
|
||||||
|
.any(|p| p == &pack_id || p == &file_name);
|
||||||
|
if !is_enabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (description, pack_format) = read_pack_mcmeta(&pack_path);
|
||||||
|
|
||||||
|
let data_dir = pack_path.join("data");
|
||||||
|
let mut pack_recipe_count = 0;
|
||||||
|
let mut pack_function_count = 0;
|
||||||
|
|
||||||
|
if data_dir.is_dir()
|
||||||
|
&& let Ok(ns_entries) = fs::read_dir(&data_dir)
|
||||||
|
{
|
||||||
|
for ns_entry in ns_entries.flatten() {
|
||||||
|
let ns_path = ns_entry.path();
|
||||||
|
if !ns_path.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let namespace = ns_entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
// Load recipes
|
||||||
|
for recipe_sub in ["recipe", "recipes"] {
|
||||||
|
let recipe_dir = ns_path.join(recipe_sub);
|
||||||
|
if recipe_dir.is_dir() {
|
||||||
|
load_recipes_from_dir(
|
||||||
|
&namespace,
|
||||||
|
&recipe_dir,
|
||||||
|
&mut all_recipes,
|
||||||
|
&mut pack_recipe_count,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load functions
|
||||||
|
for fn_sub in ["function", "functions"] {
|
||||||
|
let fn_dir = ns_path.join(fn_sub);
|
||||||
|
if fn_dir.is_dir() {
|
||||||
|
let before = all_functions.len();
|
||||||
|
function_loader::load_functions_from_dir(
|
||||||
|
&namespace,
|
||||||
|
&fn_dir,
|
||||||
|
&mut all_functions,
|
||||||
|
);
|
||||||
|
pack_function_count += all_functions.len() - before;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load tags
|
||||||
|
let tags_dir = ns_path.join("tags");
|
||||||
|
if tags_dir.is_dir() {
|
||||||
|
function_loader::load_function_tags_from_dir(
|
||||||
|
&namespace,
|
||||||
|
&tags_dir,
|
||||||
|
&mut all_function_tags,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Loaded datapack '{file_name}': {pack_recipe_count} recipe(s), {pack_function_count} function(s)"
|
||||||
|
);
|
||||||
|
|
||||||
|
loaded_packs_vec.push(LoadedDatapack {
|
||||||
|
id: pack_id,
|
||||||
|
name: file_name,
|
||||||
|
description,
|
||||||
|
pack_format,
|
||||||
|
root_path: pack_path,
|
||||||
|
recipe_count: pack_recipe_count,
|
||||||
|
function_count: pack_function_count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recipe_manager.set_recipes(all_recipes).await;
|
||||||
|
*self.loaded_packs.write().await = loaded_packs_vec;
|
||||||
|
*self.functions.write().await = all_functions;
|
||||||
|
*self.function_tags.write().await = all_function_tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_loaded_packs(&self) -> Vec<LoadedDatapack> {
|
||||||
|
self.loaded_packs.read().await.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_functions(&self) -> HashMap<String, Vec<String>> {
|
||||||
|
self.functions.read().await.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_function_names(&self) -> Vec<String> {
|
||||||
|
let fns = self.functions.read().await;
|
||||||
|
let tags = self.function_tags.read().await;
|
||||||
|
let mut names = Vec::with_capacity(fns.len() + tags.len());
|
||||||
|
names.extend(fns.keys().cloned());
|
||||||
|
for tag in tags.keys() {
|
||||||
|
names.push(format!("#{tag}"));
|
||||||
|
}
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute_function(
|
||||||
|
&self,
|
||||||
|
server: &Arc<Server>,
|
||||||
|
source: &CommandSource,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<usize, String> {
|
||||||
|
let (functions_to_run, is_tag) = if let Some(tag_name) = name.strip_prefix('#') {
|
||||||
|
let tags = self.function_tags.read().await;
|
||||||
|
let Some(fns) = tags.get(tag_name) else {
|
||||||
|
return Err(format!("Unknown function tag: #{tag_name}"));
|
||||||
|
};
|
||||||
|
(fns.clone(), true)
|
||||||
|
} else {
|
||||||
|
(vec![name.to_string()], false)
|
||||||
|
};
|
||||||
|
|
||||||
|
let all_fns = self.functions.read().await;
|
||||||
|
let mut total_executed = 0;
|
||||||
|
|
||||||
|
for fn_id in functions_to_run {
|
||||||
|
let Some(lines) = all_fns.get(&fn_id) else {
|
||||||
|
if !is_tag {
|
||||||
|
return Err(format!("Unknown function: {fn_id}"));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
server
|
||||||
|
.command_dispatcher
|
||||||
|
.load()
|
||||||
|
.handle_command(source, line)
|
||||||
|
.await;
|
||||||
|
total_executed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(total_executed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_pack_mcmeta(pack_path: &Path) -> (String, u32) {
|
||||||
|
let mcmeta_path = pack_path.join("pack.mcmeta");
|
||||||
|
if let Ok(content) = fs::read_to_string(mcmeta_path)
|
||||||
|
&& let Ok(val) = serde_json::from_str::<serde_json::Value>(&content)
|
||||||
|
{
|
||||||
|
let pack = val.get("pack");
|
||||||
|
let description = pack
|
||||||
|
.and_then(|p| p.get("description"))
|
||||||
|
.map(|d| {
|
||||||
|
d.as_str()
|
||||||
|
.map_or_else(|| d.to_string(), ToString::to_string)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let pack_format = pack
|
||||||
|
.and_then(|p| p.get("pack_format"))
|
||||||
|
.and_then(serde_json::Value::as_u64)
|
||||||
|
.unwrap_or(61) as u32;
|
||||||
|
return (description, pack_format);
|
||||||
|
}
|
||||||
|
(String::new(), 61)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_recipes_from_dir(
|
||||||
|
namespace: &str,
|
||||||
|
dir: &Path,
|
||||||
|
all_recipes: &mut Vec<DynamicRecipe>,
|
||||||
|
count: &mut usize,
|
||||||
|
) {
|
||||||
|
let Ok(entries) = fs::read_dir(dir) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
load_recipes_from_dir(namespace, &path, all_recipes, count);
|
||||||
|
} else if path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
|
||||||
|
{
|
||||||
|
let stem = path
|
||||||
|
.file_stem()
|
||||||
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if let Ok(content) = fs::read_to_string(&path)
|
||||||
|
&& let Some(recipe) = recipe_loader::parse_recipe(namespace, &stem, &content)
|
||||||
|
{
|
||||||
|
all_recipes.push(recipe);
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
294
crates/pumpkin/src/data/datapack/recipe_loader.rs
Normal file
294
crates/pumpkin/src/data/datapack/recipe_loader.rs
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
use pumpkin_data::recipes::RecipeCategoryTypes;
|
||||||
|
use pumpkin_protocol::codec::recipe::{
|
||||||
|
DynamicRecipe, OwnedCookingRecipe, OwnedCookingRecipeType, OwnedCraftingRecipe,
|
||||||
|
OwnedRecipeIngredient, OwnedRecipeResult,
|
||||||
|
};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub fn parse_recipe(namespace: &str, name: &str, json_str: &str) -> Option<DynamicRecipe> {
|
||||||
|
let value: Value = serde_json::from_str(json_str).ok()?;
|
||||||
|
let recipe_id = format!("{namespace}:{name}");
|
||||||
|
let raw_type = value.get("type")?.as_str()?;
|
||||||
|
let recipe_type = raw_type.strip_prefix("minecraft:").unwrap_or(raw_type);
|
||||||
|
|
||||||
|
match recipe_type {
|
||||||
|
"crafting_shaped" => parse_shaped(recipe_id, &value).map(DynamicRecipe::Crafting),
|
||||||
|
"crafting_shapeless" => parse_shapeless(recipe_id, &value).map(DynamicRecipe::Crafting),
|
||||||
|
"smelting" => parse_cooking(recipe_id, &value, 200)
|
||||||
|
.map(|c| DynamicRecipe::Cooking(OwnedCookingRecipeType::Smelting(c))),
|
||||||
|
"blasting" => parse_cooking(recipe_id, &value, 100)
|
||||||
|
.map(|c| DynamicRecipe::Cooking(OwnedCookingRecipeType::Blasting(c))),
|
||||||
|
"smoking" => parse_cooking(recipe_id, &value, 100)
|
||||||
|
.map(|c| DynamicRecipe::Cooking(OwnedCookingRecipeType::Smoking(c))),
|
||||||
|
"campfire_cooking" => parse_cooking(recipe_id, &value, 600)
|
||||||
|
.map(|c| DynamicRecipe::Cooking(OwnedCookingRecipeType::CampfireCooking(c))),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_id(id: &str) -> String {
|
||||||
|
if id.contains(':') {
|
||||||
|
id.to_string()
|
||||||
|
} else {
|
||||||
|
format!("minecraft:{id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ingredient(value: &Value) -> Option<OwnedRecipeIngredient> {
|
||||||
|
match value {
|
||||||
|
Value::String(s) => Some(OwnedRecipeIngredient::Simple(normalize_id(s))),
|
||||||
|
Value::Object(map) => map
|
||||||
|
.get("tag")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(|tag| OwnedRecipeIngredient::Tagged(normalize_id(tag)))
|
||||||
|
.or_else(|| {
|
||||||
|
map.get("item")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(|item| OwnedRecipeIngredient::Simple(normalize_id(item)))
|
||||||
|
}),
|
||||||
|
Value::Array(arr) => {
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
for item in arr {
|
||||||
|
if let Some(s) = item.as_str() {
|
||||||
|
ids.push(normalize_id(s));
|
||||||
|
} else if let Some(obj_item) = item.get("item").and_then(Value::as_str) {
|
||||||
|
ids.push(normalize_id(obj_item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ids.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(OwnedRecipeIngredient::OneOf(ids))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_result(value: &Value) -> Option<OwnedRecipeResult> {
|
||||||
|
match value {
|
||||||
|
Value::String(s) => Some(OwnedRecipeResult {
|
||||||
|
item_id: normalize_id(s),
|
||||||
|
count: 1,
|
||||||
|
}),
|
||||||
|
Value::Object(map) => {
|
||||||
|
let id = map
|
||||||
|
.get("id")
|
||||||
|
.or_else(|| map.get("item"))
|
||||||
|
.and_then(Value::as_str)?;
|
||||||
|
let count = map
|
||||||
|
.get("count")
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.unwrap_or(1)
|
||||||
|
.min(99) as u8;
|
||||||
|
Some(OwnedRecipeResult {
|
||||||
|
item_id: normalize_id(id),
|
||||||
|
count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_category(value: Option<&Value>) -> RecipeCategoryTypes {
|
||||||
|
let Some(val) = value.and_then(Value::as_str) else {
|
||||||
|
return RecipeCategoryTypes::Misc;
|
||||||
|
};
|
||||||
|
match val.to_ascii_lowercase().as_str() {
|
||||||
|
"equipment" => RecipeCategoryTypes::Equipment,
|
||||||
|
"building" => RecipeCategoryTypes::Building,
|
||||||
|
"redstone" | "restone" => RecipeCategoryTypes::Restone,
|
||||||
|
"food" => RecipeCategoryTypes::Food,
|
||||||
|
"blocks" => RecipeCategoryTypes::Blocks,
|
||||||
|
_ => RecipeCategoryTypes::Misc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_shaped(recipe_id: String, value: &Value) -> Option<OwnedCraftingRecipe> {
|
||||||
|
let pattern = value
|
||||||
|
.get("pattern")?
|
||||||
|
.as_array()?
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.as_str().map(ToString::to_string))
|
||||||
|
.collect::<Option<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let key_map = value.get("key")?.as_object()?;
|
||||||
|
let mut key = Vec::new();
|
||||||
|
for (k, v) in key_map {
|
||||||
|
let ch = k.chars().next()?;
|
||||||
|
let ingredient = parse_ingredient(v)?;
|
||||||
|
key.push((ch, ingredient));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = parse_result(value.get("result")?)?;
|
||||||
|
let category = parse_category(value.get("category"));
|
||||||
|
let group = value
|
||||||
|
.get("group")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(ToString::to_string);
|
||||||
|
let show_notification = value
|
||||||
|
.get("show_notification")
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
Some(OwnedCraftingRecipe::Shaped {
|
||||||
|
recipe_id: Some(recipe_id),
|
||||||
|
category,
|
||||||
|
group,
|
||||||
|
show_notification,
|
||||||
|
key,
|
||||||
|
pattern,
|
||||||
|
result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_shapeless(recipe_id: String, value: &Value) -> Option<OwnedCraftingRecipe> {
|
||||||
|
let ingredients_arr = value.get("ingredients")?.as_array()?;
|
||||||
|
let mut ingredients = Vec::new();
|
||||||
|
for ing_val in ingredients_arr {
|
||||||
|
ingredients.push(parse_ingredient(ing_val)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = parse_result(value.get("result")?)?;
|
||||||
|
let category = parse_category(value.get("category"));
|
||||||
|
let group = value
|
||||||
|
.get("group")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(ToString::to_string);
|
||||||
|
|
||||||
|
Some(OwnedCraftingRecipe::Shapeless {
|
||||||
|
recipe_id: Some(recipe_id),
|
||||||
|
category,
|
||||||
|
group,
|
||||||
|
ingredients,
|
||||||
|
result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cooking(
|
||||||
|
recipe_id: String,
|
||||||
|
value: &Value,
|
||||||
|
default_cooking_time: i32,
|
||||||
|
) -> Option<OwnedCookingRecipe> {
|
||||||
|
let ingredient = parse_ingredient(value.get("ingredient")?)?;
|
||||||
|
let result = parse_result(value.get("result")?)?;
|
||||||
|
let category = parse_category(value.get("category"));
|
||||||
|
let group = value
|
||||||
|
.get("group")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(ToString::to_string);
|
||||||
|
let cooking_time = value
|
||||||
|
.get("cookingtime")
|
||||||
|
.and_then(Value::as_i64)
|
||||||
|
.map_or(default_cooking_time, |t| t as i32);
|
||||||
|
let experience = value
|
||||||
|
.get("experience")
|
||||||
|
.and_then(Value::as_f64)
|
||||||
|
.map_or(0.0, |e| e as f32);
|
||||||
|
|
||||||
|
Some(OwnedCookingRecipe {
|
||||||
|
recipe_id,
|
||||||
|
category,
|
||||||
|
group,
|
||||||
|
ingredient,
|
||||||
|
cooking_time,
|
||||||
|
experience,
|
||||||
|
result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_shaped_recipe() {
|
||||||
|
let json = r##"{
|
||||||
|
"type": "minecraft:crafting_shaped",
|
||||||
|
"group": "sword",
|
||||||
|
"pattern": ["X", "X", "#"],
|
||||||
|
"key": {
|
||||||
|
"#": "minecraft:stick",
|
||||||
|
"X": "minecraft:diamond"
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"id": "minecraft:diamond_sword",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
}"##;
|
||||||
|
|
||||||
|
let parsed = parse_recipe("test", "diamond_sword", json);
|
||||||
|
assert!(parsed.is_some());
|
||||||
|
if let Some(DynamicRecipe::Crafting(OwnedCraftingRecipe::Shaped {
|
||||||
|
recipe_id,
|
||||||
|
result,
|
||||||
|
pattern,
|
||||||
|
..
|
||||||
|
})) = parsed
|
||||||
|
{
|
||||||
|
assert_eq!(recipe_id.as_deref(), Some("test:diamond_sword"));
|
||||||
|
assert_eq!(result.item_id, "minecraft:diamond_sword");
|
||||||
|
assert_eq!(pattern.len(), 3);
|
||||||
|
} else {
|
||||||
|
panic!("Expected shaped recipe");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_shapeless_recipe() {
|
||||||
|
let json = r#"{
|
||||||
|
"type": "minecraft:crafting_shapeless",
|
||||||
|
"ingredients": ["minecraft:oak_log"],
|
||||||
|
"result": {
|
||||||
|
"id": "minecraft:oak_planks",
|
||||||
|
"count": 4
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let parsed = parse_recipe("minecraft", "oak_planks_from_oak_log", json);
|
||||||
|
assert!(parsed.is_some());
|
||||||
|
if let Some(DynamicRecipe::Crafting(OwnedCraftingRecipe::Shapeless {
|
||||||
|
recipe_id,
|
||||||
|
result,
|
||||||
|
ingredients,
|
||||||
|
..
|
||||||
|
})) = parsed
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
recipe_id.as_deref(),
|
||||||
|
Some("minecraft:oak_planks_from_oak_log")
|
||||||
|
);
|
||||||
|
assert_eq!(result.item_id, "minecraft:oak_planks");
|
||||||
|
assert_eq!(result.count, 4);
|
||||||
|
assert_eq!(ingredients.len(), 1);
|
||||||
|
} else {
|
||||||
|
panic!("Expected shapeless recipe");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_smelting_recipe() {
|
||||||
|
let json = r#"{
|
||||||
|
"type": "minecraft:smelting",
|
||||||
|
"ingredient": "minecraft:raw_iron",
|
||||||
|
"result": {
|
||||||
|
"id": "minecraft:iron_ingot"
|
||||||
|
},
|
||||||
|
"experience": 0.7,
|
||||||
|
"cookingtime": 200
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let parsed = parse_recipe("minecraft", "iron_ingot_from_smelting_raw_iron", json);
|
||||||
|
assert!(parsed.is_some());
|
||||||
|
if let Some(DynamicRecipe::Cooking(OwnedCookingRecipeType::Smelting(cooking))) = parsed {
|
||||||
|
assert_eq!(
|
||||||
|
cooking.recipe_id,
|
||||||
|
"minecraft:iron_ingot_from_smelting_raw_iron"
|
||||||
|
);
|
||||||
|
assert_eq!(cooking.result.item_id, "minecraft:iron_ingot");
|
||||||
|
assert_eq!(cooking.cooking_time, 200);
|
||||||
|
} else {
|
||||||
|
panic!("Expected smelting recipe");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ pub mod advancement_data;
|
|||||||
pub mod banlist_serializer;
|
pub mod banlist_serializer;
|
||||||
pub mod banned_ip;
|
pub mod banned_ip;
|
||||||
pub mod banned_player;
|
pub mod banned_player;
|
||||||
|
pub mod datapack;
|
||||||
pub mod player_server;
|
pub mod player_server;
|
||||||
pub mod usercache;
|
pub mod usercache;
|
||||||
pub mod whitelist;
|
pub mod whitelist;
|
||||||
|
|||||||
@ -106,6 +106,7 @@ pub struct Server {
|
|||||||
/// Assigns unique IDs to containers.
|
/// Assigns unique IDs to containers.
|
||||||
container_id: AtomicU32,
|
container_id: AtomicU32,
|
||||||
pub recipe_manager: Arc<recipe::RecipeManager>,
|
pub recipe_manager: Arc<recipe::RecipeManager>,
|
||||||
|
pub datapack_manager: Arc<crate::data::datapack::DatapackManager>,
|
||||||
pub enchantment_manager: Arc<enchantment::EnchantmentManager>,
|
pub enchantment_manager: Arc<enchantment::EnchantmentManager>,
|
||||||
/// Assigns unique IDs to maps.
|
/// Assigns unique IDs to maps.
|
||||||
map_id: AtomicI32,
|
map_id: AtomicI32,
|
||||||
@ -278,6 +279,7 @@ impl Server {
|
|||||||
permission_manager,
|
permission_manager,
|
||||||
container_id: 0.into(),
|
container_id: 0.into(),
|
||||||
recipe_manager: Arc::new(recipe::RecipeManager::new()),
|
recipe_manager: Arc::new(recipe::RecipeManager::new()),
|
||||||
|
datapack_manager: Arc::new(crate::data::datapack::DatapackManager::new()),
|
||||||
enchantment_manager: Arc::new(enchantment::EnchantmentManager::new()),
|
enchantment_manager: Arc::new(enchantment::EnchantmentManager::new()),
|
||||||
map_id: level_info.load().map_id.into(),
|
map_id: level_info.load().map_id.into(),
|
||||||
worlds: ArcSwap::from_pointee(vec![]),
|
worlds: ArcSwap::from_pointee(vec![]),
|
||||||
@ -446,6 +448,24 @@ impl Server {
|
|||||||
server.worlds.store(Arc::new(worlds_vec));
|
server.worlds.store(Arc::new(worlds_vec));
|
||||||
|
|
||||||
info!("All worlds loaded successfully.");
|
info!("All worlds loaded successfully.");
|
||||||
|
|
||||||
|
let enabled_packs = server.level_info.load().data_packs.enabled.clone();
|
||||||
|
server
|
||||||
|
.datapack_manager
|
||||||
|
.load_all(&world_path, &enabled_packs, &server.recipe_manager)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let server_for_load = server.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let source = crate::command::CommandSender::Console
|
||||||
|
.into_source(&server_for_load)
|
||||||
|
.await;
|
||||||
|
let _ = server_for_load
|
||||||
|
.datapack_manager
|
||||||
|
.execute_function(&server_for_load, &source, "#minecraft:load")
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
|
||||||
server
|
server
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,7 +579,47 @@ impl Server {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save_world_info(&self) -> Result<(), WorldInfoError> {
|
||||||
|
let level_data = self.level_info.load();
|
||||||
|
self.world_info_writer
|
||||||
|
.write_world_info(&level_data, &self.basic_config.get_world_path())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reload_datapacks(&self, server: &Arc<Self>) {
|
||||||
|
let enabled_packs = self.level_info.load().data_packs.enabled.clone();
|
||||||
|
let world_path = self.basic_config.get_world_path();
|
||||||
|
self.datapack_manager
|
||||||
|
.load_all(&world_path, &enabled_packs, &self.recipe_manager)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let source = crate::command::CommandSender::Console
|
||||||
|
.into_source(server)
|
||||||
|
.await;
|
||||||
|
let _ = self
|
||||||
|
.datapack_manager
|
||||||
|
.execute_function(server, &source, "#minecraft:load")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let dynamic_recipes = self.recipe_manager.get_dynamic_recipes_internal().await;
|
||||||
|
for player in self.get_all_players() {
|
||||||
|
if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref() {
|
||||||
|
let add_packet = pumpkin_protocol::java::client::play::CRecipeBookAdd::new(
|
||||||
|
true,
|
||||||
|
&dynamic_recipes,
|
||||||
|
);
|
||||||
|
if let Ok(data) = java_client.serialize_packet(&add_packet) {
|
||||||
|
java_client.send_packet_now(data).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn save_all(&self) -> Result<(), String> {
|
pub async fn save_all(&self) -> Result<(), String> {
|
||||||
|
if let Err(err) = self.save_world_info() {
|
||||||
|
error!("Failed to save world info: {err}");
|
||||||
|
return Err(format!("Failed to save world info: {err}"));
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(err) = self.player_data_storage.save_all_players(self).await {
|
if let Err(err) = self.player_data_storage.save_all_players(self).await {
|
||||||
error!("Failed to save player data: {err}");
|
error!("Failed to save player data: {err}");
|
||||||
return Err(format!("Failed to save player data: {err}"));
|
return Err(format!("Failed to save player data: {err}"));
|
||||||
|
|||||||
@ -26,6 +26,21 @@ impl RecipeManager {
|
|||||||
recipes.push(recipe);
|
recipes.push(recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn add_recipes(&self, new_recipes: impl IntoIterator<Item = DynamicRecipe>) {
|
||||||
|
let mut recipes = self.dynamic_recipes.write().await;
|
||||||
|
recipes.extend(new_recipes);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_recipes(&self, new_recipes: Vec<DynamicRecipe>) {
|
||||||
|
let mut recipes = self.dynamic_recipes.write().await;
|
||||||
|
*recipes = new_recipes;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear(&self) {
|
||||||
|
let mut recipes = self.dynamic_recipes.write().await;
|
||||||
|
recipes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_dynamic_recipes_internal(&self) -> Vec<DynamicRecipe> {
|
pub async fn get_dynamic_recipes_internal(&self) -> Vec<DynamicRecipe> {
|
||||||
self.dynamic_recipes.read().await.clone()
|
self.dynamic_recipes.read().await.clone()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,6 +73,54 @@ pub enum StructurePlacementTypeStruct {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deserialized structure pool alias binding.
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum PoolAliasBindingStruct {
|
||||||
|
#[serde(rename = "minecraft:direct")]
|
||||||
|
Direct { alias: String, target: String },
|
||||||
|
#[serde(rename = "minecraft:random")]
|
||||||
|
Random {
|
||||||
|
alias: String,
|
||||||
|
targets: Vec<WeightedTargetStruct>,
|
||||||
|
},
|
||||||
|
#[serde(rename = "minecraft:random_group")]
|
||||||
|
RandomGroup { groups: Vec<WeightedGroupStruct> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct WeightedTargetStruct {
|
||||||
|
pub data: String,
|
||||||
|
pub weight: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct WeightedGroupStruct {
|
||||||
|
pub data: Vec<PoolAliasBindingStruct>,
|
||||||
|
pub weight: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct SpawnOverrideStruct {
|
||||||
|
pub bounding_box: String,
|
||||||
|
pub spawns: Vec<SpawnEntryStruct>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct SpawnEntryStruct {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub entity_type: String,
|
||||||
|
#[serde(default = "default_spawn_count", rename = "minCount")]
|
||||||
|
pub min_count: u32,
|
||||||
|
#[serde(default = "default_spawn_count", rename = "maxCount")]
|
||||||
|
pub max_count: u32,
|
||||||
|
pub weight: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_spawn_count() -> u32 {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
/// Deserialized structure entry specifying its valid biomes and generation step.
|
/// Deserialized structure entry specifying its valid biomes and generation step.
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
pub struct StructureStruct {
|
pub struct StructureStruct {
|
||||||
@ -90,6 +138,8 @@ pub struct StructureStruct {
|
|||||||
pub terrain_adaptation: Option<String>,
|
pub terrain_adaptation: Option<String>,
|
||||||
/// Optional jigsaw start height.
|
/// Optional jigsaw start height.
|
||||||
pub start_height: Option<serde_json::Value>,
|
pub start_height: Option<serde_json::Value>,
|
||||||
|
/// Optional height provider (e.g. for nether fossils).
|
||||||
|
pub height: Option<serde_json::Value>,
|
||||||
/// Optional heightmap to project the start to.
|
/// Optional heightmap to project the start to.
|
||||||
pub project_start_to_heightmap: Option<String>,
|
pub project_start_to_heightmap: Option<String>,
|
||||||
/// Optional max distance from center.
|
/// Optional max distance from center.
|
||||||
@ -100,6 +150,22 @@ pub struct StructureStruct {
|
|||||||
pub dimension_padding: Option<i32>,
|
pub dimension_padding: Option<i32>,
|
||||||
/// Critical for villages to appropriately truncate long village streets.
|
/// Critical for villages to appropriately truncate long village streets.
|
||||||
pub use_expansion_hack: Option<bool>,
|
pub use_expansion_hack: Option<bool>,
|
||||||
|
/// Optional pool alias bindings (e.g. for Trial Chambers).
|
||||||
|
#[serde(default)]
|
||||||
|
pub pool_aliases: Vec<PoolAliasBindingStruct>,
|
||||||
|
/// Spawn overrides (e.g. for Ocean Monuments).
|
||||||
|
#[serde(default)]
|
||||||
|
pub spawn_overrides: BTreeMap<String, SpawnOverrideStruct>,
|
||||||
|
/// Optional beached flag for shipwrecks.
|
||||||
|
pub is_beached: Option<bool>,
|
||||||
|
/// Optional mineshaft variant ("normal" or "mesa").
|
||||||
|
pub mineshaft_type: Option<String>,
|
||||||
|
/// Optional ocean ruin temperature ("cold" or "warm").
|
||||||
|
pub biome_temp: Option<String>,
|
||||||
|
/// Optional cluster probability for ocean ruins.
|
||||||
|
pub cluster_probability: Option<f32>,
|
||||||
|
/// Optional large probability for ocean ruins.
|
||||||
|
pub large_probability: Option<f32>,
|
||||||
/// Defines the generation behavior (e.g. "minecraft:jigsaw").
|
/// Defines the generation behavior (e.g. "minecraft:jigsaw").
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub structure_type: String,
|
pub structure_type: String,
|
||||||
@ -216,6 +282,62 @@ impl ToTokens for StructurePlacementTypeStruct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToTokens for PoolAliasBindingStruct {
|
||||||
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
|
match self {
|
||||||
|
Self::Direct { alias, target } => {
|
||||||
|
tokens.extend(quote!(
|
||||||
|
PoolAliasBinding::Direct {
|
||||||
|
alias: #alias,
|
||||||
|
target: #target,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Self::Random { alias, targets } => {
|
||||||
|
tokens.extend(quote!(
|
||||||
|
PoolAliasBinding::Random {
|
||||||
|
alias: #alias,
|
||||||
|
targets: &[#(#targets),*],
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Self::RandomGroup { groups } => {
|
||||||
|
tokens.extend(quote!(
|
||||||
|
PoolAliasBinding::RandomGroup {
|
||||||
|
groups: &[#(#groups),*],
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for WeightedTargetStruct {
|
||||||
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
|
let target = &self.data;
|
||||||
|
let weight = self.weight;
|
||||||
|
tokens.extend(quote!(
|
||||||
|
WeightedPoolAliasTarget {
|
||||||
|
target: #target,
|
||||||
|
weight: #weight,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for WeightedGroupStruct {
|
||||||
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
|
let bindings = &self.data;
|
||||||
|
let weight = self.weight;
|
||||||
|
tokens.extend(quote!(
|
||||||
|
WeightedPoolAliasGroup {
|
||||||
|
bindings: &[#(#bindings),*],
|
||||||
|
weight: #weight,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ToTokens for StructureStruct {
|
impl ToTokens for StructureStruct {
|
||||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
let biomes = &self.biomes;
|
let biomes = &self.biomes;
|
||||||
@ -266,27 +388,76 @@ impl ToTokens for StructureStruct {
|
|||||||
} else {
|
} else {
|
||||||
quote!(None)
|
quote!(None)
|
||||||
};
|
};
|
||||||
|
let pool_aliases = &self.pool_aliases;
|
||||||
|
|
||||||
let structure_type = structure_type_to_token(&self.structure_type);
|
let height_val = self.start_height.as_ref().or(self.height.as_ref());
|
||||||
|
let start_height = if let Some(hv) = height_val {
|
||||||
let start_height = if let Some(sh) = &self.start_height {
|
let hp = value_to_height_provider(hv);
|
||||||
if let Some(abs) = sh.get("absolute") {
|
quote!(Some(#hp))
|
||||||
let abs_val = abs.as_i64().unwrap() as i16;
|
|
||||||
quote!(Some(#abs_val))
|
|
||||||
} else if let Some(min_inc) = sh.get("min_inclusive") {
|
|
||||||
if let Some(abs) = min_inc.get("absolute") {
|
|
||||||
let abs_val = abs.as_i64().unwrap() as i16;
|
|
||||||
quote!(Some(#abs_val))
|
|
||||||
} else {
|
|
||||||
quote!(None)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote!(None)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
quote!(None)
|
quote!(None)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let spawn_overrides: Vec<TokenStream> = self
|
||||||
|
.spawn_overrides
|
||||||
|
.iter()
|
||||||
|
.map(|(category, so)| {
|
||||||
|
let bbox = match so.bounding_box.as_str() {
|
||||||
|
"piece" => quote!(BoundingBoxType::Piece),
|
||||||
|
_ => quote!(BoundingBoxType::Full),
|
||||||
|
};
|
||||||
|
let spawns: Vec<TokenStream> = so
|
||||||
|
.spawns
|
||||||
|
.iter()
|
||||||
|
.map(|entry| {
|
||||||
|
let et = &entry.entity_type;
|
||||||
|
let min = entry.min_count;
|
||||||
|
let max = entry.max_count;
|
||||||
|
let weight = entry.weight;
|
||||||
|
quote!(SpawnEntry {
|
||||||
|
entity_type: #et,
|
||||||
|
min_count: #min,
|
||||||
|
max_count: #max,
|
||||||
|
weight: #weight,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
quote!(SpawnOverride {
|
||||||
|
category: #category,
|
||||||
|
bounding_box: #bbox,
|
||||||
|
spawns: &[#(#spawns),*],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let is_beached = if let Some(ib) = self.is_beached {
|
||||||
|
quote!(Some(#ib))
|
||||||
|
} else {
|
||||||
|
quote!(None)
|
||||||
|
};
|
||||||
|
let mineshaft_type = if let Some(mt) = &self.mineshaft_type {
|
||||||
|
quote!(Some(#mt))
|
||||||
|
} else {
|
||||||
|
quote!(None)
|
||||||
|
};
|
||||||
|
let biome_temp = if let Some(bt) = &self.biome_temp {
|
||||||
|
quote!(Some(#bt))
|
||||||
|
} else {
|
||||||
|
quote!(None)
|
||||||
|
};
|
||||||
|
let cluster_probability = if let Some(cp) = self.cluster_probability {
|
||||||
|
quote!(Some(#cp))
|
||||||
|
} else {
|
||||||
|
quote!(None)
|
||||||
|
};
|
||||||
|
let large_probability = if let Some(lp) = self.large_probability {
|
||||||
|
quote!(Some(#lp))
|
||||||
|
} else {
|
||||||
|
quote!(None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let structure_type = structure_type_to_token(&self.structure_type);
|
||||||
|
|
||||||
tokens.extend(quote!(
|
tokens.extend(quote!(
|
||||||
Structure {
|
Structure {
|
||||||
biomes: #biomes,
|
biomes: #biomes,
|
||||||
@ -301,12 +472,102 @@ impl ToTokens for StructureStruct {
|
|||||||
liquid_settings: #liquid_settings,
|
liquid_settings: #liquid_settings,
|
||||||
dimension_padding: #dimension_padding,
|
dimension_padding: #dimension_padding,
|
||||||
use_expansion_hack: #use_expansion_hack,
|
use_expansion_hack: #use_expansion_hack,
|
||||||
|
pool_aliases: &[#(#pool_aliases),*],
|
||||||
|
spawn_overrides: &[#(#spawn_overrides),*],
|
||||||
|
is_beached: #is_beached,
|
||||||
|
mineshaft_type: #mineshaft_type,
|
||||||
|
biome_temp: #biome_temp,
|
||||||
|
cluster_probability: #cluster_probability,
|
||||||
|
large_probability: #large_probability,
|
||||||
structure_type: #structure_type,
|
structure_type: #structure_type,
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn value_to_height_provider(v: &serde_json::Value) -> TokenStream {
|
||||||
|
if let Some(type_str) = v.get("type").and_then(|t| t.as_str()) {
|
||||||
|
match type_str {
|
||||||
|
"minecraft:uniform" => {
|
||||||
|
let min = value_to_y_offset(&v["min_inclusive"]);
|
||||||
|
let max = value_to_y_offset(&v["max_inclusive"]);
|
||||||
|
quote!(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: #min,
|
||||||
|
max_inclusive: #max,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
"minecraft:trapezoid" => {
|
||||||
|
let min = value_to_y_offset(&v["min_inclusive"]);
|
||||||
|
let max = value_to_y_offset(&v["max_inclusive"]);
|
||||||
|
let plateau = v.get("plateau").and_then(|p| p.as_i64()).map(|p| p as i32);
|
||||||
|
let plateau_token = if let Some(p) = plateau {
|
||||||
|
quote!(Some(#p))
|
||||||
|
} else {
|
||||||
|
quote!(None)
|
||||||
|
};
|
||||||
|
quote!(HeightProvider::Trapezoid(TrapezoidHeightProvider {
|
||||||
|
min_inclusive: #min,
|
||||||
|
max_inclusive: #max,
|
||||||
|
plateau: #plateau_token,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
"minecraft:very_biased_to_bottom" => {
|
||||||
|
let min = value_to_y_offset(&v["min_inclusive"]);
|
||||||
|
let max = value_to_y_offset(&v["max_inclusive"]);
|
||||||
|
let inner = v.get("inner").and_then(|i| i.as_u64()).map(|i| i as u32);
|
||||||
|
let inner_token = if let Some(i) = inner {
|
||||||
|
quote!(std::num::NonZero::new(#i))
|
||||||
|
} else {
|
||||||
|
quote!(None)
|
||||||
|
};
|
||||||
|
quote!(HeightProvider::VeryBiasedToBottom(VeryBiasedToBottomHeightProvider {
|
||||||
|
min_inclusive: #min,
|
||||||
|
max_inclusive: #max,
|
||||||
|
inner: #inner_token,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
"minecraft:constant" => {
|
||||||
|
let y = value_to_y_offset(&v["value"]);
|
||||||
|
quote!(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: #y,
|
||||||
|
max_inclusive: #y,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let y = value_to_y_offset(v);
|
||||||
|
quote!(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: #y,
|
||||||
|
max_inclusive: #y,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let y = value_to_y_offset(v);
|
||||||
|
quote!(HeightProvider::Uniform(UniformHeightProvider {
|
||||||
|
min_inclusive: #y,
|
||||||
|
max_inclusive: #y,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_to_y_offset(v: &serde_json::Value) -> TokenStream {
|
||||||
|
if let Some(abs) = v.get("absolute") {
|
||||||
|
let val = abs.as_i64().unwrap_or(0) as i16;
|
||||||
|
quote!(YOffset::Absolute(Absolute { absolute: #val }))
|
||||||
|
} else if let Some(ab) = v.get("above_bottom") {
|
||||||
|
let val = ab.as_i64().unwrap_or(0) as i8;
|
||||||
|
quote!(YOffset::AboveBottom(AboveBottom { above_bottom: #val }))
|
||||||
|
} else if let Some(bt) = v.get("below_top") {
|
||||||
|
let val = bt.as_i64().unwrap_or(0) as i8;
|
||||||
|
quote!(YOffset::BelowTop(BelowTop { below_top: #val }))
|
||||||
|
} else if let Some(val) = v.as_i64() {
|
||||||
|
let val = val as i16;
|
||||||
|
quote!(YOffset::Absolute(Absolute { absolute: #val }))
|
||||||
|
} else {
|
||||||
|
quote!(YOffset::Absolute(Absolute { absolute: 0 }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
||||||
fn structure_type_to_token(st: &str) -> TokenStream {
|
fn structure_type_to_token(st: &str) -> TokenStream {
|
||||||
@ -522,6 +783,7 @@ pub fn build() -> TokenStream {
|
|||||||
RandomGenerator, RandomImpl, get_carver_seed, get_region_seed,
|
RandomGenerator, RandomImpl, get_carver_seed, get_region_seed,
|
||||||
legacy_rand::LegacyRand, xoroshiro128::Xoroshiro,
|
legacy_rand::LegacyRand, xoroshiro128::Xoroshiro,
|
||||||
};
|
};
|
||||||
|
use pumpkin_util::y_offset::{AboveBottom, Absolute, BelowTop, YOffset};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
pub enum StructureKeys {
|
pub enum StructureKeys {
|
||||||
@ -681,6 +943,145 @@ pub fn build() -> TokenStream {
|
|||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum BoundingBoxType {
|
||||||
|
Piece,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SpawnEntry {
|
||||||
|
pub entity_type: &'static str,
|
||||||
|
pub min_count: u32,
|
||||||
|
pub max_count: u32,
|
||||||
|
pub weight: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SpawnOverride {
|
||||||
|
pub category: &'static str,
|
||||||
|
pub bounding_box: BoundingBoxType,
|
||||||
|
pub spawns: &'static [SpawnEntry],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum HeightProvider {
|
||||||
|
Uniform(UniformHeightProvider),
|
||||||
|
Trapezoid(TrapezoidHeightProvider),
|
||||||
|
VeryBiasedToBottom(VeryBiasedToBottomHeightProvider),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeightProvider {
|
||||||
|
#[must_use]
|
||||||
|
pub fn get(&self, random: &mut RandomGenerator, min_y: i8, height: u16) -> i32 {
|
||||||
|
match self {
|
||||||
|
Self::Uniform(provider) => provider.get(random, min_y, height),
|
||||||
|
Self::Trapezoid(provider) => provider.get(random, min_y, height),
|
||||||
|
Self::VeryBiasedToBottom(provider) => provider.get(random, min_y, height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct UniformHeightProvider {
|
||||||
|
pub min_inclusive: YOffset,
|
||||||
|
pub max_inclusive: YOffset,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniformHeightProvider {
|
||||||
|
#[must_use]
|
||||||
|
pub fn get(&self, random: &mut RandomGenerator, min_y: i8, height: u16) -> i32 {
|
||||||
|
let min = self.min_inclusive.get_y(min_y as i16, height);
|
||||||
|
let max = self.max_inclusive.get_y(min_y as i16, height);
|
||||||
|
if min >= max {
|
||||||
|
min
|
||||||
|
} else {
|
||||||
|
random.next_inbetween_i32(min, max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct TrapezoidHeightProvider {
|
||||||
|
pub min_inclusive: YOffset,
|
||||||
|
pub max_inclusive: YOffset,
|
||||||
|
pub plateau: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrapezoidHeightProvider {
|
||||||
|
#[must_use]
|
||||||
|
pub fn get(&self, random: &mut RandomGenerator, min_y: i8, height: u16) -> i32 {
|
||||||
|
let plateau = self.plateau.unwrap_or(0);
|
||||||
|
let i = self.min_inclusive.get_y(min_y as i16, height);
|
||||||
|
let j = self.max_inclusive.get_y(min_y as i16, height);
|
||||||
|
|
||||||
|
if i >= j {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
let k = j - i;
|
||||||
|
if plateau >= k {
|
||||||
|
return random.next_inbetween_i32(i, j);
|
||||||
|
}
|
||||||
|
|
||||||
|
let l = (k - plateau) / 2;
|
||||||
|
let m = k - l;
|
||||||
|
i + random.next_inbetween_i32(0, m) + random.next_inbetween_i32(0, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct VeryBiasedToBottomHeightProvider {
|
||||||
|
pub min_inclusive: YOffset,
|
||||||
|
pub max_inclusive: YOffset,
|
||||||
|
pub inner: Option<std::num::NonZero<u32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VeryBiasedToBottomHeightProvider {
|
||||||
|
#[must_use]
|
||||||
|
pub fn get(&self, random: &mut RandomGenerator, min_y: i8, height: u16) -> i32 {
|
||||||
|
let min = self.min_inclusive.get_y(min_y as i16, height);
|
||||||
|
let max = self.max_inclusive.get_y(min_y as i16, height);
|
||||||
|
let inner = self.inner.map_or(1, std::num::NonZero::get) as i32;
|
||||||
|
|
||||||
|
if min >= max {
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
let min_rnd = random.next_inbetween_i32(min + inner, max);
|
||||||
|
let max_rnd = random.next_inbetween_i32(min, min_rnd - 1);
|
||||||
|
|
||||||
|
random.next_inbetween_i32(min, max_rnd - 1 + inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum PoolAliasBinding {
|
||||||
|
Direct {
|
||||||
|
alias: &'static str,
|
||||||
|
target: &'static str,
|
||||||
|
},
|
||||||
|
Random {
|
||||||
|
alias: &'static str,
|
||||||
|
targets: &'static [WeightedPoolAliasTarget],
|
||||||
|
},
|
||||||
|
RandomGroup {
|
||||||
|
groups: &'static [WeightedPoolAliasGroup],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct WeightedPoolAliasTarget {
|
||||||
|
pub target: &'static str,
|
||||||
|
pub weight: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct WeightedPoolAliasGroup {
|
||||||
|
pub bindings: &'static [PoolAliasBinding],
|
||||||
|
pub weight: u32,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Structure {
|
pub struct Structure {
|
||||||
pub biomes: &'static str,
|
pub biomes: &'static str,
|
||||||
pub step: GenerationStep,
|
pub step: GenerationStep,
|
||||||
@ -688,12 +1089,19 @@ pub fn build() -> TokenStream {
|
|||||||
pub start_jigsaw_name: Option<&'static str>,
|
pub start_jigsaw_name: Option<&'static str>,
|
||||||
pub size: Option<i32>,
|
pub size: Option<i32>,
|
||||||
pub terrain_adaptation: TerrainAdaptation,
|
pub terrain_adaptation: TerrainAdaptation,
|
||||||
pub start_height: Option<i16>,
|
pub start_height: Option<HeightProvider>,
|
||||||
pub project_start_to_heightmap: Option<&'static str>,
|
pub project_start_to_heightmap: Option<&'static str>,
|
||||||
pub max_distance_from_center: Option<i32>,
|
pub max_distance_from_center: Option<i32>,
|
||||||
pub liquid_settings: Option<&'static str>,
|
pub liquid_settings: Option<&'static str>,
|
||||||
pub dimension_padding: Option<i32>,
|
pub dimension_padding: Option<i32>,
|
||||||
pub use_expansion_hack: Option<bool>,
|
pub use_expansion_hack: Option<bool>,
|
||||||
|
pub pool_aliases: &'static [PoolAliasBinding],
|
||||||
|
pub spawn_overrides: &'static [SpawnOverride],
|
||||||
|
pub is_beached: Option<bool>,
|
||||||
|
pub mineshaft_type: Option<&'static str>,
|
||||||
|
pub biome_temp: Option<&'static str>,
|
||||||
|
pub cluster_probability: Option<f32>,
|
||||||
|
pub large_probability: Option<f32>,
|
||||||
pub structure_type: StructureType,
|
pub structure_type: StructureType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user