feat: basic initial datapack support

This commit is contained in:
Alexander Medvedev 2026-08-22 18:56:48 +02:00
parent de8c780053
commit df28c321ac
24 changed files with 5090 additions and 479 deletions

View File

@ -9300,7 +9300,7 @@ impl ParameterPoint {
}
}
#[must_use]
pub fn target(
pub const fn target(
temperature: f32,
humidity: f32,
continentalness: f32,
@ -9318,7 +9318,7 @@ pub fn target(
)
}
#[must_use]
pub fn parameters(
pub const fn parameters(
temperature: f32,
humidity: f32,
continentalness: f32,

View File

@ -4,6 +4,7 @@ use pumpkin_util::random::{
RandomGenerator, RandomImpl, get_carver_seed, get_region_seed, legacy_rand::LegacyRand,
xoroshiro128::Xoroshiro,
};
use pumpkin_util::y_offset::{AboveBottom, Absolute, BelowTop, YOffset};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum StructureKeys {
PillagerOutpost,
@ -247,6 +248,125 @@ pub enum StructureType {
SwampHut,
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 biomes: &'static str,
pub step: GenerationStep,
@ -254,12 +374,19 @@ pub struct Structure {
pub start_jigsaw_name: Option<&'static str>,
pub size: Option<i32>,
pub terrain_adaptation: TerrainAdaptation,
pub start_height: Option<i16>,
pub start_height: Option<HeightProvider>,
pub project_start_to_heightmap: Option<&'static str>,
pub max_distance_from_center: Option<i32>,
pub liquid_settings: Option<&'static str>,
pub dimension_padding: Option<i32>,
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,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@ -299,12 +426,63 @@ impl Structure {
start_jigsaw_name: Some("minecraft:city_anchor"),
size: Some(7i32),
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,
max_distance_from_center: Some(116i32),
liquid_settings: None,
dimension_padding: None,
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,
};
pub const BASTION_REMNANT: Self = Structure {
@ -314,12 +492,22 @@ impl Structure {
start_jigsaw_name: None,
size: Some(6i32),
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,
max_distance_from_center: Some(80i32),
liquid_settings: None,
dimension_padding: None,
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,
};
pub const BURIED_TREASURE: Self = Structure {
@ -335,6 +523,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const DESERT_PYRAMID: Self = Structure {
@ -350,6 +545,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const END_CITY: Self = Structure {
@ -365,6 +567,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const FORTRESS: Self = Structure {
@ -380,6 +589,48 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const IGLOO: Self = Structure {
@ -395,6 +646,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const JUNGLE_PYRAMID: Self = Structure {
@ -410,6 +668,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const MANSION: Self = Structure {
@ -425,6 +690,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const MINESHAFT: Self = Structure {
@ -440,6 +712,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const MINESHAFT_MESA: Self = Structure {
@ -455,6 +734,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const MONUMENT: Self = Structure {
@ -470,6 +756,34 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const NETHER_FOSSIL: Self = Structure {
@ -479,12 +793,22 @@ impl Structure {
start_jigsaw_name: None,
size: None,
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,
max_distance_from_center: None,
liquid_settings: None,
dimension_padding: 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,
};
pub const OCEAN_RUIN_COLD: Self = Structure {
@ -500,6 +824,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const OCEAN_RUIN_WARM: Self = Structure {
@ -515,6 +846,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const PILLAGER_OUTPOST: Self = Structure {
@ -524,12 +862,31 @@ impl Structure {
start_jigsaw_name: None,
size: Some(7i32),
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"),
max_distance_from_center: Some(80i32),
liquid_settings: None,
dimension_padding: None,
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,
};
pub const RUINED_PORTAL: Self = Structure {
@ -545,6 +902,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const RUINED_PORTAL_DESERT: Self = Structure {
@ -560,6 +924,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const RUINED_PORTAL_JUNGLE: Self = Structure {
@ -575,6 +946,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const RUINED_PORTAL_MOUNTAIN: Self = Structure {
@ -590,6 +968,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const RUINED_PORTAL_NETHER: Self = Structure {
@ -605,6 +990,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const RUINED_PORTAL_OCEAN: Self = Structure {
@ -620,6 +1012,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const RUINED_PORTAL_SWAMP: Self = Structure {
@ -635,6 +1034,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const SHIPWRECK: Self = Structure {
@ -650,6 +1056,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const SHIPWRECK_BEACHED: Self = Structure {
@ -665,6 +1078,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const STRONGHOLD: Self = Structure {
@ -680,6 +1100,13 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const SWAMP_HUT: Self = Structure {
@ -695,6 +1122,34 @@ impl Structure {
liquid_settings: None,
dimension_padding: 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,
};
pub const TRAIL_RUINS: Self = Structure {
@ -704,12 +1159,22 @@ impl Structure {
start_jigsaw_name: None,
size: Some(7i32),
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"),
max_distance_from_center: Some(80i32),
liquid_settings: None,
dimension_padding: None,
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,
};
pub const TRIAL_CHAMBERS: Self = Structure {
@ -719,12 +1184,145 @@ impl Structure {
start_jigsaw_name: None,
size: Some(20i32),
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,
max_distance_from_center: Some(116i32),
liquid_settings: Some("ignore_waterlogging"),
dimension_padding: Some(10i32),
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,
};
pub const VILLAGE_DESERT: Self = Structure {
@ -734,12 +1332,22 @@ impl Structure {
start_jigsaw_name: None,
size: Some(6i32),
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"),
max_distance_from_center: Some(80i32),
liquid_settings: None,
dimension_padding: None,
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,
};
pub const VILLAGE_PLAINS: Self = Structure {
@ -749,12 +1357,22 @@ impl Structure {
start_jigsaw_name: None,
size: Some(6i32),
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"),
max_distance_from_center: Some(80i32),
liquid_settings: None,
dimension_padding: None,
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,
};
pub const VILLAGE_SAVANNA: Self = Structure {
@ -764,12 +1382,22 @@ impl Structure {
start_jigsaw_name: None,
size: Some(6i32),
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"),
max_distance_from_center: Some(80i32),
liquid_settings: None,
dimension_padding: None,
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,
};
pub const VILLAGE_SNOWY: Self = Structure {
@ -779,12 +1407,22 @@ impl Structure {
start_jigsaw_name: None,
size: Some(6i32),
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"),
max_distance_from_center: Some(80i32),
liquid_settings: None,
dimension_padding: None,
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,
};
pub const VILLAGE_TAIGA: Self = Structure {
@ -794,12 +1432,22 @@ impl Structure {
start_jigsaw_name: None,
size: Some(6i32),
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"),
max_distance_from_center: Some(80i32),
liquid_settings: None,
dimension_padding: None,
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,
};
#[must_use]

View File

@ -9,7 +9,7 @@ use serde::Deserialize;
///
/// The enum is deserialized in an untagged form, meaning the variant
/// is selected based on the provided field.
#[derive(Deserialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
pub enum YOffset {
/// Absolute world Y coordinate.
@ -47,21 +47,21 @@ impl YOffset {
}
/// Absolute vertical position in world coordinates.
#[derive(Deserialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
pub struct Absolute {
/// The fixed world Y level.
pub absolute: i16,
}
/// Offset measured upward from the world's minimum Y level.
#[derive(Deserialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
pub struct AboveBottom {
/// Number of blocks above the minimum Y.
pub above_bottom: i8,
}
/// Offset measured downward from the world's top boundary.
#[derive(Deserialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
pub struct BelowTop {
/// Number of blocks below the maximum Y.
pub below_top: i8,

View File

@ -81,7 +81,8 @@ pub fn generate_structure_position(
let (Some(start_pool), Some(size)) = (structure.start_pool, structure.size) else {
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) {
generator = generator.with_expansion_hack(true);
}
@ -92,7 +93,9 @@ pub fn generate_structure_position(
}
StructureKeys::Shipwreck | StructureKeys::ShipwreckBeached => {
let generator = ShipwreckGenerator {
is_beached: *key == StructureKeys::ShipwreckBeached,
is_beached: structure
.is_beached
.unwrap_or(*key == StructureKeys::ShipwreckBeached),
};
generator.get_structure_position(context)
}
@ -108,7 +111,9 @@ pub fn generate_structure_position(
}
StructureKeys::OceanRuinCold | StructureKeys::OceanRuinWarm => {
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)
}
@ -117,7 +122,9 @@ pub fn generate_structure_position(
StructureKeys::Monument => OceanMonumentGenerator.get_structure_position(context),
StructureKeys::Mineshaft | StructureKeys::MineshaftMesa => {
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)
}

View File

@ -765,6 +765,7 @@ pub struct JigsawGenerator {
pub size: i32,
pub start_jigsaw_name: Option<String>,
pub use_expansion_hack: bool,
pub pool_aliases: &'static [pumpkin_data::structures::PoolAliasBinding],
}
impl JigsawGenerator {
@ -775,6 +776,18 @@ impl JigsawGenerator {
size,
start_jigsaw_name: None,
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
}
#[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 {
@ -801,8 +823,11 @@ impl StructureGenerator for JigsawGenerator {
.structure_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 {
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 {
context.sea_level
};
@ -847,9 +872,27 @@ impl StructureGenerator for JigsawGenerator {
.as_deref()
.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(
&mut context,
&self.start_pool,
start_pool,
start_jigsaw,
self.size,
start_pos,
@ -858,7 +901,7 @@ impl StructureGenerator for JigsawGenerator {
&MaxDistance::new(max_distance),
dimension_padding,
liquid_settings,
&PoolAliasLookup::default(),
&pool_alias_lookup,
)
}
}

View File

@ -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).
#[derive(Debug, Clone, Default)]
pub struct PoolAliasLookup {
aliases: HashMap<String, Vec<(String, u32)>>,
aliases: HashMap<String, String>,
}
impl PoolAliasLookup {
@ -69,38 +69,111 @@ impl PoolAliasLookup {
}
}
pub fn add_direct(&mut self, alias: String, target: String) {
self.aliases.insert(alias, vec![(target, 1)]);
#[must_use]
pub fn from_bindings(
bindings: &[pumpkin_data::structures::PoolAliasBinding],
random: &mut RandomGenerator,
) -> Self {
let mut lookup = Self::new();
for binding in bindings {
lookup.add_binding(binding, random);
}
lookup
}
pub fn add_random(&mut self, alias: String, targets: Vec<(String, u32)>) {
self.aliases.insert(alias, targets);
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();
if total_weight == 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;
for (target, weight) in targets {
if r < *weight {
self.add_direct(alias, target.clone());
return;
}
r -= *weight;
}
if let Some((target, _)) = targets.first() {
self.add_direct(alias, target.clone());
}
}
#[must_use]
pub fn lookup<'a>(&'a self, id: &'a str, random: &mut RandomGenerator) -> &'a str {
if let Some(targets) = self.aliases.get(id) {
if targets.is_empty() {
return id;
}
if targets.len() == 1 {
return &targets[0].0;
}
let total_weight: u32 = targets.iter().map(|(_, w)| *w).sum();
if total_weight == 0 {
return &targets[0].0;
}
let mut r = random.next_bounded_i32(total_weight as i32) as u32;
for (target, weight) in targets {
if r < *weight {
return target.as_str();
}
r -= *weight;
}
&targets[0].0
} else {
id
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
}
}

View File

@ -64,8 +64,16 @@ impl StructureGenerator for NetherFossilGenerator {
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 height_range = HEIGHT_MAX - HEIGHT_MIN + 1;
let initial_y = HEIGHT_MIN + context.random.next_bounded_i32(height_range);
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;
HEIGHT_MIN + context.random.next_bounded_i32(height_range)
};
// Column scan is deferred to place() since we don't have block data here.
// Consume random in vanilla order for determinism.

View File

@ -150,18 +150,16 @@ impl StructurePieceBase for ChestCorridorPiece {
inner.add_block(chunk, slab, 2, 1, i, &box_limit);
}
// if !self.chest_generated {
// // Check if the target chest position is within the current chunk being processed
// let chest_pos = inner.to_world(3, 2, 3);
// if box_limit.contains(&chest_pos) {
// self.chest_generated = true;
// let chest_state = Block::CHEST.default_state;
// // Note: In a full implementation, you would use a helper to set the LootTable NBT here
// inner.add_block(chunk, &chest_state, 3, 2, 3, &box_limit);
// // chunk.set_loot_table(chest_pos, "minecraft:chests/stronghold_corridor");
// }
// }
if !self.chest_generated {
self.chest_generated = inner.add_chest(
chunk,
&box_limit,
random,
3,
2,
3,
"minecraft:chests/stronghold_corridor",
);
}
}
}

View File

@ -73,6 +73,7 @@ impl StructurePieceBase for LibraryPiece {
&mut self.piece.piece
}
#[expect(clippy::too_many_lines)]
fn place(
&mut self,
chunk: &mut ProtoChunk,
@ -234,10 +235,26 @@ impl StructurePieceBase for LibraryPiece {
}
// 6. Chests
// inner.add_chest(chunk, &box_limit, random, 3, 3, 5, "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, "stronghold_library");
// }
inner.add_chest(
chunk,
&box_limit,
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",
);
}
}
}

View File

@ -29,8 +29,8 @@ use crate::world::WorldPortalExt;
const CROSSING_LOOT_TABLE: &str = "minecraft:chests/stronghold_crossing";
pub struct SquareRoomPiece {
piece: StrongholdPiece,
room_type: u32,
pub piece: StrongholdPiece,
pub room_type: u32,
}
impl SquareRoomPiece {

View File

@ -46,7 +46,10 @@ pub use cache::{
};
pub use processor::StructureProcessor;
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;
/// 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(
placer: &mut impl BlockPlacer,
template: &StructureTemplate,
@ -110,6 +113,9 @@ pub fn place_template_with_options(
let world_x = origin.x + rotated_ox;
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 {
let palette_entry = &template.palette[block.state as usize];
@ -182,8 +188,19 @@ pub fn place_template_with_options(
// Apply processors
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 {
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;
break;
};
@ -200,23 +217,20 @@ pub fn place_template_with_options(
placer.set_block_state(&Vector3::new(wx, wy, wz), state);
// 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() {
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();
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("y", wy);
placed_nbt.put_int("z", wz);
if let Some(template_nbt) = &block_entity_nbt {
for (key, value) in &template_nbt.child_tags {
if key.as_ref() != "x"
&& key.as_ref() != "y"
&& key.as_ref() != "z"
&& key.as_ref() != "id"
{
if key.as_ref() != "x" && key.as_ref() != "y" && key.as_ref() != "z" {
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.
pub(crate) fn get_block_entity_id(block_name: &str) -> Option<&'static str> {
match block_name {
"minecraft:furnace" => Some("minecraft:furnace"),
"minecraft:chest" => Some("minecraft:chest"),
"minecraft:trapped_chest" => Some("minecraft:trapped_chest"),
"minecraft:barrel" => Some("minecraft:barrel"),
"minecraft:hopper" => Some("minecraft:hopper"),
"minecraft:dropper" => Some("minecraft:dropper"),
"minecraft:dispenser" => Some("minecraft:dispenser"),
"minecraft:brewing_stand" => Some("minecraft:brewing_stand"),
"minecraft:blast_furnace" => Some("minecraft:blast_furnace"),
"minecraft:smoker" => Some("minecraft:smoker"),
"minecraft:shulker_box" => Some("minecraft:shulker_box"),
"minecraft:bed" => Some("minecraft:bed"),
"minecraft:dragon_head"
| "minecraft:dragon_wall_head"
| "minecraft:skeleton_skull"
| "minecraft:skeleton_wall_skull"
| "minecraft:wither_skeleton_skull"
| "minecraft:wither_skeleton_wall_skull"
| "minecraft:zombie_head"
| "minecraft:zombie_wall_head"
| "minecraft:player_head"
| "minecraft:player_wall_head"
| "minecraft:creeper_head"
| "minecraft:creeper_wall_head"
| "minecraft:piglin_head"
| "minecraft:piglin_wall_head" => Some("minecraft:skull"),
"minecraft:sign"
| "minecraft:oak_sign"
| "minecraft:spruce_sign"
| "minecraft:birch_sign"
| "minecraft:jungle_sign"
| "minecraft:acacia_sign"
| "minecraft:dark_oak_sign"
| "minecraft:mangrove_sign"
| "minecraft:cherry_sign"
| "minecraft:bamboo_sign"
| "minecraft:crimson_sign"
| "minecraft:warped_sign" => Some("minecraft:sign"),
"minecraft:hanging_sign" => Some("minecraft:hanging_sign"),
let name = block_name.strip_prefix("minecraft:").unwrap_or(block_name);
match name {
"furnace" => Some("minecraft:furnace"),
"chest" => Some("minecraft:chest"),
"trapped_chest" => Some("minecraft:trapped_chest"),
"barrel" => Some("minecraft:barrel"),
"hopper" => Some("minecraft:hopper"),
"dropper" => Some("minecraft:dropper"),
"dispenser" => Some("minecraft:dispenser"),
"brewing_stand" => Some("minecraft:brewing_stand"),
"blast_furnace" => Some("minecraft:blast_furnace"),
"smoker" => Some("minecraft:smoker"),
"shulker_box" => Some("minecraft:shulker_box"),
"bed" => Some("minecraft:bed"),
"suspicious_sand" | "suspicious_gravel" => Some("minecraft:brushable_block"),
"decorated_pot" => Some("minecraft:decorated_pot"),
"spawner" => Some("minecraft:mob_spawner"),
"trial_spawner" => Some("minecraft:trial_spawner"),
"vault" => Some("minecraft:vault"),
"crafter" => Some("minecraft:crafter"),
"creaking_heart" => Some("minecraft:creaking_heart"),
"chiseled_bookshelf" => Some("minecraft:chiseled_bookshelf"),
"beehive" | "bee_nest" => Some("minecraft:beehive"),
"campfire" | "soul_campfire" => Some("minecraft:campfire"),
"sculk_sensor" | "calibrated_sculk_sensor" => Some("minecraft:sculk_sensor"),
"sculk_catalyst" => Some("minecraft:sculk_catalyst"),
"sculk_shrieker" => Some("minecraft:sculk_shrieker"),
"dragon_head"
| "dragon_wall_head"
| "skeleton_skull"
| "skeleton_wall_skull"
| "wither_skeleton_skull"
| "wither_skeleton_wall_skull"
| "zombie_head"
| "zombie_wall_head"
| "player_head"
| "player_wall_head"
| "creeper_head"
| "creeper_wall_head"
| "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,
}
}

View 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\""));
}
}

View 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),
),
);
}

View File

@ -17,6 +17,7 @@ mod clear;
mod clone;
mod damage;
mod data;
mod datapack;
mod debug;
pub mod defaultgamemode;
mod deop;
@ -30,6 +31,7 @@ mod fetchprofile;
mod fill;
mod fillbiome;
mod forceload;
mod function;
mod gamemode;
mod gamerule;
mod give;
@ -209,6 +211,8 @@ pub fn default_dispatcher(
teammsg::register(&mut dispatcher, registry);
clone::register(&mut dispatcher, registry);
attribute::register(&mut dispatcher, registry);
datapack::register(&mut dispatcher, registry);
function::register(&mut dispatcher, registry);
fetchprofile::register(&mut dispatcher, registry);
apply_command_overrides(&mut dispatcher, registry, commands_config);

View File

@ -312,7 +312,9 @@ impl CommandExecutor for PlaceStructureExecutor {
.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(
&mut StructureGeneratorContext {
@ -334,7 +336,7 @@ impl CommandExecutor for PlaceStructureExecutor {
&MaxDistance::new(structure.max_distance_from_center.unwrap_or(128)),
DimensionPadding::ZERO,
LiquidSettings::ApplyWaterlog,
&PoolAliasLookup::default(),
&pool_alias_lookup,
)
.ok_or_else(|| {
JIGSAW_FAILED

View 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()
]
);
}
}

View 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;
}
}
}
}

View 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");
}
}
}

View File

@ -12,6 +12,7 @@ pub mod advancement_data;
pub mod banlist_serializer;
pub mod banned_ip;
pub mod banned_player;
pub mod datapack;
pub mod player_server;
pub mod usercache;
pub mod whitelist;

View File

@ -106,6 +106,7 @@ pub struct Server {
/// Assigns unique IDs to containers.
container_id: AtomicU32,
pub recipe_manager: Arc<recipe::RecipeManager>,
pub datapack_manager: Arc<crate::data::datapack::DatapackManager>,
pub enchantment_manager: Arc<enchantment::EnchantmentManager>,
/// Assigns unique IDs to maps.
map_id: AtomicI32,
@ -278,6 +279,7 @@ impl Server {
permission_manager,
container_id: 0.into(),
recipe_manager: Arc::new(recipe::RecipeManager::new()),
datapack_manager: Arc::new(crate::data::datapack::DatapackManager::new()),
enchantment_manager: Arc::new(enchantment::EnchantmentManager::new()),
map_id: level_info.load().map_id.into(),
worlds: ArcSwap::from_pointee(vec![]),
@ -446,6 +448,24 @@ impl Server {
server.worlds.store(Arc::new(worlds_vec));
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
}
@ -559,7 +579,47 @@ impl Server {
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> {
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 {
error!("Failed to save player data: {err}");
return Err(format!("Failed to save player data: {err}"));

View File

@ -26,6 +26,21 @@ impl RecipeManager {
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> {
self.dynamic_recipes.read().await.clone()
}

View File

@ -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.
#[derive(Deserialize, Clone)]
pub struct StructureStruct {
@ -90,6 +138,8 @@ pub struct StructureStruct {
pub terrain_adaptation: Option<String>,
/// Optional jigsaw start height.
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.
pub project_start_to_heightmap: Option<String>,
/// Optional max distance from center.
@ -100,6 +150,22 @@ pub struct StructureStruct {
pub dimension_padding: Option<i32>,
/// Critical for villages to appropriately truncate long village streets.
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").
#[serde(rename = "type")]
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 {
fn to_tokens(&self, tokens: &mut TokenStream) {
let biomes = &self.biomes;
@ -266,27 +388,76 @@ impl ToTokens for StructureStruct {
} else {
quote!(None)
};
let pool_aliases = &self.pool_aliases;
let structure_type = structure_type_to_token(&self.structure_type);
let start_height = if let Some(sh) = &self.start_height {
if let Some(abs) = sh.get("absolute") {
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)
}
let height_val = self.start_height.as_ref().or(self.height.as_ref());
let start_height = if let Some(hv) = height_val {
let hp = value_to_height_provider(hv);
quote!(Some(#hp))
} else {
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!(
Structure {
biomes: #biomes,
@ -301,12 +472,102 @@ impl ToTokens for StructureStruct {
liquid_settings: #liquid_settings,
dimension_padding: #dimension_padding,
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,
}
));
}
}
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
fn structure_type_to_token(st: &str) -> TokenStream {
@ -522,6 +783,7 @@ pub fn build() -> TokenStream {
RandomGenerator, RandomImpl, get_carver_seed, get_region_seed,
legacy_rand::LegacyRand, xoroshiro128::Xoroshiro,
};
use pumpkin_util::y_offset::{AboveBottom, Absolute, BelowTop, YOffset};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum StructureKeys {
@ -681,6 +943,145 @@ pub fn build() -> TokenStream {
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 biomes: &'static str,
pub step: GenerationStep,
@ -688,12 +1089,19 @@ pub fn build() -> TokenStream {
pub start_jigsaw_name: Option<&'static str>,
pub size: Option<i32>,
pub terrain_adaptation: TerrainAdaptation,
pub start_height: Option<i16>,
pub start_height: Option<HeightProvider>,
pub project_start_to_heightmap: Option<&'static str>,
pub max_distance_from_center: Option<i32>,
pub liquid_settings: Option<&'static str>,
pub dimension_padding: Option<i32>,
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,
}