feat: basic initial datapack support
This commit is contained in:
parent
de8c780053
commit
df28c321ac
@ -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,
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
681
crates/pumpkin/src/command/commands/datapack.rs
Normal file
681
crates/pumpkin/src/command/commands/datapack.rs
Normal file
@ -0,0 +1,681 @@
|
||||
use std::fs;
|
||||
|
||||
use pumpkin_data::translation;
|
||||
use pumpkin_util::PermissionLvl;
|
||||
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
|
||||
use pumpkin_util::text::TextComponent;
|
||||
use pumpkin_util::text::color::NamedColor;
|
||||
use tracing::error;
|
||||
|
||||
use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal};
|
||||
use crate::command::argument_types::core::string::StringArgumentType;
|
||||
use crate::command::context::command_context::CommandContext;
|
||||
use crate::command::errors::error_types::CommandErrorType;
|
||||
use crate::command::node::dispatcher::CommandDispatcher;
|
||||
use crate::command::node::{CommandExecutor, CommandExecutorResult};
|
||||
use crate::command::suggestion::provider::{SuggestionProvider, SuggestionProviderResult};
|
||||
use crate::command::suggestion::suggestions::SuggestionsBuilder;
|
||||
use crate::server::Server;
|
||||
|
||||
const DESCRIPTION: &str = "Controls loaded data packs.";
|
||||
const PERMISSION: &str = "minecraft:command.datapack";
|
||||
|
||||
static ERROR_UNKNOWN_DATAPACK: CommandErrorType<1> = CommandErrorType::new(
|
||||
translation::java::COMMANDS_DATAPACK_UNKNOWN,
|
||||
translation::java::COMMANDS_DATAPACK_UNKNOWN,
|
||||
);
|
||||
|
||||
static ERROR_ENABLE_FAILED: CommandErrorType<1> = CommandErrorType::new(
|
||||
translation::java::COMMANDS_DATAPACK_ENABLE_FAILED,
|
||||
translation::java::COMMANDS_DATAPACK_ENABLE_FAILED,
|
||||
);
|
||||
|
||||
static ERROR_DISABLE_FAILED: CommandErrorType<1> = CommandErrorType::new(
|
||||
translation::java::COMMANDS_DATAPACK_DISABLE_FAILED,
|
||||
translation::java::COMMANDS_DATAPACK_DISABLE_FAILED,
|
||||
);
|
||||
|
||||
static ERROR_CREATE_ALREADY_EXISTS: CommandErrorType<1> = CommandErrorType::new(
|
||||
translation::java::COMMANDS_DATAPACK_CREATE_ALREADY_EXISTS,
|
||||
translation::java::COMMANDS_DATAPACK_CREATE_ALREADY_EXISTS,
|
||||
);
|
||||
|
||||
static ERROR_CREATE_INVALID_NAME: CommandErrorType<1> = CommandErrorType::new(
|
||||
translation::java::COMMANDS_DATAPACK_CREATE_INVALID_NAME,
|
||||
translation::java::COMMANDS_DATAPACK_CREATE_INVALID_NAME,
|
||||
);
|
||||
|
||||
static ERROR_CREATE_IO_FAILURE: CommandErrorType<1> = CommandErrorType::new(
|
||||
translation::java::COMMANDS_DATAPACK_CREATE_IO_FAILURE,
|
||||
translation::java::COMMANDS_DATAPACK_CREATE_IO_FAILURE,
|
||||
);
|
||||
|
||||
fn get_all_known_packs(server: &Server) -> Vec<String> {
|
||||
let mut packs = Vec::new();
|
||||
packs.push("vanilla".to_string());
|
||||
|
||||
// Bundled feature packs
|
||||
for bundled in [
|
||||
"trade_rebalance",
|
||||
"minecart_improvements",
|
||||
"redstone_experiments",
|
||||
] {
|
||||
if !packs.iter().any(|p| p == bundled) {
|
||||
packs.push(bundled.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// World datapacks directory
|
||||
let datapacks_dir = server.basic_config.get_world_path().join("datapacks");
|
||||
if let Ok(entries) = fs::read_dir(datapacks_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
if file_name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
if path.is_dir()
|
||||
|| path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
|
||||
{
|
||||
let pack_name = format!("file/{file_name}");
|
||||
if !packs.iter().any(|p| p == &pack_name) {
|
||||
packs.push(pack_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let level_info = server.level_info.load();
|
||||
for pack in &level_info.data_packs.enabled {
|
||||
if !packs.iter().any(|p| p == pack) {
|
||||
packs.push(pack.clone());
|
||||
}
|
||||
}
|
||||
for pack in &level_info.data_packs.disabled {
|
||||
if !packs.iter().any(|p| p == pack) {
|
||||
packs.push(pack.clone());
|
||||
}
|
||||
}
|
||||
|
||||
packs
|
||||
}
|
||||
|
||||
fn get_enabled_packs(server: &Server) -> Vec<String> {
|
||||
server.level_info.load().data_packs.enabled.clone()
|
||||
}
|
||||
|
||||
fn get_available_packs(server: &Server) -> Vec<String> {
|
||||
let enabled = get_enabled_packs(server);
|
||||
let all = get_all_known_packs(server);
|
||||
all.into_iter().filter(|p| !enabled.contains(p)).collect()
|
||||
}
|
||||
|
||||
fn find_pack_name(server: &Server, input: &str) -> Option<String> {
|
||||
let known = get_all_known_packs(server);
|
||||
if let Some(p) = known.iter().find(|p| *p == input) {
|
||||
return Some(p.clone());
|
||||
}
|
||||
let file_input = format!("file/{input}");
|
||||
if let Some(p) = known.iter().find(|p| **p == file_input) {
|
||||
return Some(p.clone());
|
||||
}
|
||||
if let Some(p) = known
|
||||
.iter()
|
||||
.find(|p| p.strip_prefix("file/") == Some(input))
|
||||
{
|
||||
return Some(p.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn format_pack(name: &str) -> TextComponent {
|
||||
TextComponent::text(format!("[{name}]")).color_named(NamedColor::Green)
|
||||
}
|
||||
|
||||
struct AvailablePackSuggestionProvider;
|
||||
|
||||
impl SuggestionProvider for AvailablePackSuggestionProvider {
|
||||
fn suggest<'a>(
|
||||
&'a self,
|
||||
context: &'a CommandContext,
|
||||
mut builder: SuggestionsBuilder,
|
||||
) -> SuggestionProviderResult<'a> {
|
||||
Box::pin(async move {
|
||||
let server = context.server();
|
||||
for pack in get_available_packs(server) {
|
||||
if pack.contains(' ') || pack.contains('/') {
|
||||
builder = builder.suggest(format!("\"{pack}\""));
|
||||
} else {
|
||||
builder = builder.suggest(pack.clone());
|
||||
}
|
||||
if let Some(short) = pack.strip_prefix("file/") {
|
||||
if short.contains(' ') {
|
||||
builder = builder.suggest(format!("\"{short}\""));
|
||||
} else {
|
||||
builder = builder.suggest(short.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct EnabledPackSuggestionProvider;
|
||||
|
||||
impl SuggestionProvider for EnabledPackSuggestionProvider {
|
||||
fn suggest<'a>(
|
||||
&'a self,
|
||||
context: &'a CommandContext,
|
||||
mut builder: SuggestionsBuilder,
|
||||
) -> SuggestionProviderResult<'a> {
|
||||
Box::pin(async move {
|
||||
let server = context.server();
|
||||
for pack in get_enabled_packs(server) {
|
||||
if pack.contains(' ') || pack.contains('/') {
|
||||
builder = builder.suggest(format!("\"{pack}\""));
|
||||
} else {
|
||||
builder = builder.suggest(pack.clone());
|
||||
}
|
||||
if let Some(short) = pack.strip_prefix("file/") {
|
||||
if short.contains(' ') {
|
||||
builder = builder.suggest(format!("\"{short}\""));
|
||||
} else {
|
||||
builder = builder.suggest(short.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ListFilter {
|
||||
All,
|
||||
Available,
|
||||
Enabled,
|
||||
}
|
||||
|
||||
struct DatapackListExecutor(ListFilter);
|
||||
|
||||
impl CommandExecutor for DatapackListExecutor {
|
||||
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||
Box::pin(async move {
|
||||
let server = context.server();
|
||||
let enabled = get_enabled_packs(server);
|
||||
let available = get_available_packs(server);
|
||||
|
||||
match self.0 {
|
||||
ListFilter::All => {
|
||||
send_enabled_list(context, &enabled).await;
|
||||
send_available_list(context, &available).await;
|
||||
Ok(enabled.len() as i32)
|
||||
}
|
||||
ListFilter::Enabled => {
|
||||
send_enabled_list(context, &enabled).await;
|
||||
Ok(enabled.len() as i32)
|
||||
}
|
||||
ListFilter::Available => {
|
||||
send_available_list(context, &available).await;
|
||||
Ok(available.len() as i32)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_enabled_list(context: &CommandContext<'_>, enabled: &[String]) {
|
||||
if enabled.is_empty() {
|
||||
context
|
||||
.source
|
||||
.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_DATAPACK_LIST_ENABLED_NONE,
|
||||
translation::java::COMMANDS_DATAPACK_LIST_ENABLED_NONE,
|
||||
[],
|
||||
),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
let packs_component =
|
||||
TextComponent::join_with_comma(enabled.iter().map(|p| format_pack(p)).collect());
|
||||
context
|
||||
.source
|
||||
.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_DATAPACK_LIST_ENABLED_SUCCESS,
|
||||
translation::java::COMMANDS_DATAPACK_LIST_ENABLED_SUCCESS,
|
||||
[
|
||||
TextComponent::text(enabled.len().to_string()),
|
||||
packs_component,
|
||||
],
|
||||
),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_available_list(context: &CommandContext<'_>, available: &[String]) {
|
||||
if available.is_empty() {
|
||||
context
|
||||
.source
|
||||
.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_DATAPACK_LIST_AVAILABLE_NONE,
|
||||
translation::java::COMMANDS_DATAPACK_LIST_AVAILABLE_NONE,
|
||||
[],
|
||||
),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
let packs_component =
|
||||
TextComponent::join_with_comma(available.iter().map(|p| format_pack(p)).collect());
|
||||
context
|
||||
.source
|
||||
.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_DATAPACK_LIST_AVAILABLE_SUCCESS,
|
||||
translation::java::COMMANDS_DATAPACK_LIST_AVAILABLE_SUCCESS,
|
||||
[
|
||||
TextComponent::text(available.len().to_string()),
|
||||
packs_component,
|
||||
],
|
||||
),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum EnablePosition {
|
||||
First,
|
||||
Last,
|
||||
}
|
||||
|
||||
struct DatapackEnableExecutor(EnablePosition);
|
||||
|
||||
impl CommandExecutor for DatapackEnableExecutor {
|
||||
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||
Box::pin(async move {
|
||||
let name_str = StringArgumentType::get(context, "name")?;
|
||||
let server = context.server();
|
||||
|
||||
let Some(resolved_name) = find_pack_name(server, name_str) else {
|
||||
return Err(ERROR_UNKNOWN_DATAPACK
|
||||
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||
};
|
||||
|
||||
let enabled = get_enabled_packs(server);
|
||||
if enabled.contains(&resolved_name) {
|
||||
return Err(
|
||||
ERROR_ENABLE_FAILED.create_without_context(TextComponent::text(resolved_name))
|
||||
);
|
||||
}
|
||||
|
||||
let target = resolved_name.clone();
|
||||
let pos = self.0;
|
||||
server.level_info.rcu(|level_info| {
|
||||
let mut new_info = (**level_info).clone();
|
||||
new_info.data_packs.disabled.retain(|p| p != &target);
|
||||
new_info.data_packs.enabled.retain(|p| p != &target);
|
||||
match pos {
|
||||
EnablePosition::First => {
|
||||
new_info.data_packs.enabled.insert(0, target.clone());
|
||||
}
|
||||
EnablePosition::Last => {
|
||||
new_info.data_packs.enabled.push(target.clone());
|
||||
}
|
||||
}
|
||||
new_info
|
||||
});
|
||||
|
||||
if let Err(err) = server.save_world_info() {
|
||||
error!("Failed to save world info: {err}");
|
||||
}
|
||||
|
||||
server.reload_datapacks(server).await;
|
||||
|
||||
context
|
||||
.source
|
||||
.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_DATAPACK_MODIFY_ENABLE,
|
||||
translation::java::COMMANDS_DATAPACK_MODIFY_ENABLE,
|
||||
[format_pack(&resolved_name)],
|
||||
),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(server.level_info.load().data_packs.enabled.len() as i32)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum BeforeOrAfter {
|
||||
Before,
|
||||
After,
|
||||
}
|
||||
|
||||
struct DatapackEnableExistingExecutor(BeforeOrAfter);
|
||||
|
||||
impl CommandExecutor for DatapackEnableExistingExecutor {
|
||||
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||
Box::pin(async move {
|
||||
let name_str = StringArgumentType::get(context, "name")?;
|
||||
let existing_str = StringArgumentType::get(context, "existing")?;
|
||||
let server = context.server();
|
||||
|
||||
let Some(target_pack) = find_pack_name(server, name_str) else {
|
||||
return Err(ERROR_UNKNOWN_DATAPACK
|
||||
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||
};
|
||||
|
||||
let Some(existing_pack) = find_pack_name(server, existing_str) else {
|
||||
return Err(ERROR_UNKNOWN_DATAPACK
|
||||
.create_without_context(TextComponent::text(existing_str.to_string())));
|
||||
};
|
||||
|
||||
let enabled = get_enabled_packs(server);
|
||||
if enabled.contains(&target_pack) {
|
||||
return Err(
|
||||
ERROR_ENABLE_FAILED.create_without_context(TextComponent::text(target_pack))
|
||||
);
|
||||
}
|
||||
|
||||
if !enabled.contains(&existing_pack) {
|
||||
return Err(
|
||||
ERROR_DISABLE_FAILED.create_without_context(TextComponent::text(existing_pack))
|
||||
);
|
||||
}
|
||||
|
||||
let target = target_pack.clone();
|
||||
let existing = existing_pack.clone();
|
||||
let before_or_after = self.0;
|
||||
server.level_info.rcu(|level_info| {
|
||||
let mut new_info = (**level_info).clone();
|
||||
new_info.data_packs.disabled.retain(|p| p != &target);
|
||||
new_info.data_packs.enabled.retain(|p| p != &target);
|
||||
|
||||
if let Some(idx) = new_info
|
||||
.data_packs
|
||||
.enabled
|
||||
.iter()
|
||||
.position(|p| p == &existing)
|
||||
{
|
||||
let insert_pos = match before_or_after {
|
||||
BeforeOrAfter::Before => idx,
|
||||
BeforeOrAfter::After => idx + 1,
|
||||
};
|
||||
new_info
|
||||
.data_packs
|
||||
.enabled
|
||||
.insert(insert_pos, target.clone());
|
||||
} else {
|
||||
new_info.data_packs.enabled.push(target.clone());
|
||||
}
|
||||
new_info
|
||||
});
|
||||
|
||||
if let Err(err) = server.save_world_info() {
|
||||
error!("Failed to save world info: {err}");
|
||||
}
|
||||
|
||||
server.reload_datapacks(server).await;
|
||||
|
||||
context
|
||||
.source
|
||||
.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_DATAPACK_MODIFY_ENABLE,
|
||||
translation::java::COMMANDS_DATAPACK_MODIFY_ENABLE,
|
||||
[format_pack(&target_pack)],
|
||||
),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(server.level_info.load().data_packs.enabled.len() as i32)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct DatapackDisableExecutor;
|
||||
|
||||
impl CommandExecutor for DatapackDisableExecutor {
|
||||
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||
Box::pin(async move {
|
||||
let name_str = StringArgumentType::get(context, "name")?;
|
||||
let server = context.server();
|
||||
|
||||
let Some(target_pack) = find_pack_name(server, name_str) else {
|
||||
return Err(ERROR_DISABLE_FAILED
|
||||
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||
};
|
||||
|
||||
let enabled = get_enabled_packs(server);
|
||||
if !enabled.contains(&target_pack) {
|
||||
return Err(ERROR_DISABLE_FAILED
|
||||
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||
}
|
||||
|
||||
if target_pack == "vanilla" {
|
||||
return Err(ERROR_DISABLE_FAILED
|
||||
.create_without_context(TextComponent::text("vanilla".to_string())));
|
||||
}
|
||||
|
||||
let target = target_pack.clone();
|
||||
server.level_info.rcu(|level_info| {
|
||||
let mut new_info = (**level_info).clone();
|
||||
new_info.data_packs.enabled.retain(|p| p != &target);
|
||||
if !new_info.data_packs.disabled.contains(&target) {
|
||||
new_info.data_packs.disabled.push(target.clone());
|
||||
}
|
||||
new_info
|
||||
});
|
||||
|
||||
if let Err(err) = server.save_world_info() {
|
||||
error!("Failed to save world info: {err}");
|
||||
}
|
||||
|
||||
server.reload_datapacks(server).await;
|
||||
|
||||
context
|
||||
.source
|
||||
.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_DATAPACK_MODIFY_DISABLE,
|
||||
translation::java::COMMANDS_DATAPACK_MODIFY_DISABLE,
|
||||
[format_pack(&target_pack)],
|
||||
),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(server.level_info.load().data_packs.enabled.len() as i32)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_pack_name(name: &str) -> bool {
|
||||
!name.is_empty()
|
||||
&& !name.contains('/')
|
||||
&& !name.contains('\\')
|
||||
&& !name.contains("..")
|
||||
&& name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
|
||||
}
|
||||
|
||||
struct DatapackCreateExecutor;
|
||||
|
||||
impl CommandExecutor for DatapackCreateExecutor {
|
||||
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||
Box::pin(async move {
|
||||
let name_str = StringArgumentType::get(context, "name")?;
|
||||
let description = StringArgumentType::get(context, "description").unwrap_or(name_str);
|
||||
|
||||
if !is_valid_pack_name(name_str) {
|
||||
return Err(ERROR_CREATE_INVALID_NAME
|
||||
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||
}
|
||||
|
||||
let server = context.server();
|
||||
let datapacks_dir = server.basic_config.get_world_path().join("datapacks");
|
||||
let pack_dir = datapacks_dir.join(name_str);
|
||||
|
||||
if pack_dir.exists() {
|
||||
return Err(ERROR_CREATE_ALREADY_EXISTS
|
||||
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||
}
|
||||
|
||||
let data_dir = pack_dir.join("data");
|
||||
if let Err(err) = fs::create_dir_all(&data_dir) {
|
||||
error!("Failed to create datapack directory: {err}");
|
||||
return Err(ERROR_CREATE_IO_FAILURE
|
||||
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||
}
|
||||
|
||||
let mcmeta_content = serde_json::json!({
|
||||
"pack": {
|
||||
"pack_format": 61,
|
||||
"description": description
|
||||
}
|
||||
});
|
||||
|
||||
let mcmeta_path = pack_dir.join("pack.mcmeta");
|
||||
let mcmeta_str = match serde_json::to_string_pretty(&mcmeta_content) {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
error!("Failed to serialize pack.mcmeta: {err}");
|
||||
return Err(ERROR_CREATE_IO_FAILURE
|
||||
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = fs::write(&mcmeta_path, mcmeta_str) {
|
||||
error!("Failed to write pack.mcmeta: {err}");
|
||||
return Err(ERROR_CREATE_IO_FAILURE
|
||||
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||
}
|
||||
|
||||
context
|
||||
.source
|
||||
.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_DATAPACK_CREATE_SUCCESS,
|
||||
translation::java::COMMANDS_DATAPACK_CREATE_SUCCESS,
|
||||
[TextComponent::text(name_str.to_string())],
|
||||
),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) {
|
||||
registry.register_permission_or_panic(Permission::new(
|
||||
PERMISSION,
|
||||
DESCRIPTION,
|
||||
PermissionDefault::Op(PermissionLvl::Two),
|
||||
));
|
||||
|
||||
let list_builder = literal("list")
|
||||
.then(literal("available").executes(DatapackListExecutor(ListFilter::Available)))
|
||||
.then(literal("enabled").executes(DatapackListExecutor(ListFilter::Enabled)))
|
||||
.executes(DatapackListExecutor(ListFilter::All));
|
||||
|
||||
let enable_builder = literal("enable").then(
|
||||
argument("name", StringArgumentType::QuotablePhrase)
|
||||
.suggests(AvailablePackSuggestionProvider)
|
||||
.then(literal("first").executes(DatapackEnableExecutor(EnablePosition::First)))
|
||||
.then(literal("last").executes(DatapackEnableExecutor(EnablePosition::Last)))
|
||||
.then(
|
||||
literal("before").then(
|
||||
argument("existing", StringArgumentType::QuotablePhrase)
|
||||
.suggests(EnabledPackSuggestionProvider)
|
||||
.executes(DatapackEnableExistingExecutor(BeforeOrAfter::Before)),
|
||||
),
|
||||
)
|
||||
.then(
|
||||
literal("after").then(
|
||||
argument("existing", StringArgumentType::QuotablePhrase)
|
||||
.suggests(EnabledPackSuggestionProvider)
|
||||
.executes(DatapackEnableExistingExecutor(BeforeOrAfter::After)),
|
||||
),
|
||||
)
|
||||
.executes(DatapackEnableExecutor(EnablePosition::Last)),
|
||||
);
|
||||
|
||||
let disable_builder = literal("disable").then(
|
||||
argument("name", StringArgumentType::QuotablePhrase)
|
||||
.suggests(EnabledPackSuggestionProvider)
|
||||
.executes(DatapackDisableExecutor),
|
||||
);
|
||||
|
||||
let create_builder = literal("create").then(
|
||||
argument("name", StringArgumentType::SingleWord)
|
||||
.then(
|
||||
argument("description", StringArgumentType::GreedyPhrase)
|
||||
.executes(DatapackCreateExecutor),
|
||||
)
|
||||
.executes(DatapackCreateExecutor),
|
||||
);
|
||||
|
||||
dispatcher.register(
|
||||
command("datapack", DESCRIPTION)
|
||||
.requires(PERMISSION)
|
||||
.then(list_builder)
|
||||
.then(enable_builder)
|
||||
.then(disable_builder)
|
||||
.then(create_builder),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn valid_pack_name_check() {
|
||||
assert!(is_valid_pack_name("my_pack"));
|
||||
assert!(is_valid_pack_name("test-1.0"));
|
||||
assert!(is_valid_pack_name("pack123"));
|
||||
|
||||
assert!(!is_valid_pack_name(""));
|
||||
assert!(!is_valid_pack_name("../hack"));
|
||||
assert!(!is_valid_pack_name("folder/pack"));
|
||||
assert!(!is_valid_pack_name("pack\\test"));
|
||||
assert!(!is_valid_pack_name("invalid char"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_pack_brackets() {
|
||||
let formatted = format_pack("vanilla");
|
||||
assert!(formatted.to_pretty_console().contains("[vanilla]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_mcmeta_json_structure() {
|
||||
let mcmeta_content = serde_json::json!({
|
||||
"pack": {
|
||||
"pack_format": 61,
|
||||
"description": "Test description"
|
||||
}
|
||||
});
|
||||
let mcmeta_str = serde_json::to_string(&mcmeta_content).unwrap();
|
||||
assert!(mcmeta_str.contains("\"pack_format\":61"));
|
||||
assert!(mcmeta_str.contains("\"description\":\"Test description\""));
|
||||
}
|
||||
}
|
||||
110
crates/pumpkin/src/command/commands/function.rs
Normal file
110
crates/pumpkin/src/command/commands/function.rs
Normal file
@ -0,0 +1,110 @@
|
||||
use pumpkin_data::translation;
|
||||
use pumpkin_util::PermissionLvl;
|
||||
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
|
||||
use pumpkin_util::text::TextComponent;
|
||||
|
||||
use crate::command::argument_builder::{ArgumentBuilder, argument, command};
|
||||
use crate::command::argument_types::core::string::StringArgumentType;
|
||||
use crate::command::context::command_context::CommandContext;
|
||||
use crate::command::errors::error_types::CommandErrorType;
|
||||
use crate::command::node::dispatcher::CommandDispatcher;
|
||||
use crate::command::node::{CommandExecutor, CommandExecutorResult};
|
||||
use crate::command::suggestion::provider::{SuggestionProvider, SuggestionProviderResult};
|
||||
use crate::command::suggestion::suggestions::SuggestionsBuilder;
|
||||
|
||||
const DESCRIPTION: &str = "Runs commands found in the corresponding function files.";
|
||||
const PERMISSION: &str = "minecraft:command.function";
|
||||
|
||||
static ERROR_UNKNOWN_FUNCTION: CommandErrorType<1> = CommandErrorType::new(
|
||||
translation::java::ARGUMENTS_FUNCTION_UNKNOWN,
|
||||
translation::java::ARGUMENTS_FUNCTION_UNKNOWN,
|
||||
);
|
||||
|
||||
struct FunctionSuggestionProvider;
|
||||
|
||||
impl SuggestionProvider for FunctionSuggestionProvider {
|
||||
fn suggest<'a>(
|
||||
&'a self,
|
||||
context: &'a CommandContext,
|
||||
mut builder: SuggestionsBuilder,
|
||||
) -> SuggestionProviderResult<'a> {
|
||||
Box::pin(async move {
|
||||
let server = context.server();
|
||||
let function_names = server.datapack_manager.get_function_names().await;
|
||||
for name in function_names {
|
||||
builder = builder.suggest(name);
|
||||
}
|
||||
builder.build()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct FunctionExecutor;
|
||||
|
||||
impl CommandExecutor for FunctionExecutor {
|
||||
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||
Box::pin(async move {
|
||||
let name_str = StringArgumentType::get(context, "name")?;
|
||||
let server = context.server();
|
||||
|
||||
let Ok(executed_count) = server
|
||||
.datapack_manager
|
||||
.execute_function(server, &context.source, name_str)
|
||||
.await
|
||||
else {
|
||||
return Err(ERROR_UNKNOWN_FUNCTION
|
||||
.create_without_context(TextComponent::text(name_str.to_string())));
|
||||
};
|
||||
|
||||
if name_str.starts_with('#') {
|
||||
context
|
||||
.source
|
||||
.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_FUNCTION_SUCCESS_MULTIPLE,
|
||||
translation::java::COMMANDS_FUNCTION_SUCCESS_MULTIPLE,
|
||||
[
|
||||
TextComponent::text(executed_count.to_string()),
|
||||
TextComponent::text(name_str.to_string()),
|
||||
],
|
||||
),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
context
|
||||
.source
|
||||
.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_FUNCTION_SUCCESS_SINGLE,
|
||||
translation::java::COMMANDS_FUNCTION_SUCCESS_SINGLE,
|
||||
[
|
||||
TextComponent::text(executed_count.to_string()),
|
||||
TextComponent::text(name_str.to_string()),
|
||||
],
|
||||
),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(executed_count as i32)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) {
|
||||
registry.register_permission_or_panic(Permission::new(
|
||||
PERMISSION,
|
||||
DESCRIPTION,
|
||||
PermissionDefault::Op(PermissionLvl::Two),
|
||||
));
|
||||
|
||||
dispatcher.register(
|
||||
command("function", DESCRIPTION).requires(PERMISSION).then(
|
||||
argument("name", StringArgumentType::SingleWord)
|
||||
.suggests(FunctionSuggestionProvider)
|
||||
.executes(FunctionExecutor),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -17,6 +17,7 @@ mod clear;
|
||||
mod clone;
|
||||
mod 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);
|
||||
|
||||
@ -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
|
||||
|
||||
127
crates/pumpkin/src/data/datapack/function_loader.rs
Normal file
127
crates/pumpkin/src/data/datapack/function_loader.rs
Normal file
@ -0,0 +1,127 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn load_functions_from_dir<S: std::hash::BuildHasher>(
|
||||
namespace: &str,
|
||||
function_dir: &Path,
|
||||
functions: &mut HashMap<String, Vec<String>, S>,
|
||||
) {
|
||||
if !function_dir.is_dir() {
|
||||
return;
|
||||
}
|
||||
load_functions_recursive(namespace, function_dir, function_dir, functions);
|
||||
}
|
||||
|
||||
fn load_functions_recursive<S: std::hash::BuildHasher>(
|
||||
namespace: &str,
|
||||
base_dir: &Path,
|
||||
current_dir: &Path,
|
||||
functions: &mut HashMap<String, Vec<String>, S>,
|
||||
) {
|
||||
let Ok(entries) = fs::read_dir(current_dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
load_functions_recursive(namespace, base_dir, &path, functions);
|
||||
} else if path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("mcfunction"))
|
||||
&& let Ok(rel_path) = path.strip_prefix(base_dir)
|
||||
{
|
||||
let mut stem_path = rel_path.to_string_lossy().to_string();
|
||||
if let Some(s) = stem_path.strip_suffix(".mcfunction") {
|
||||
stem_path = s.to_string();
|
||||
}
|
||||
// Convert Windows backslashes to forward slashes if any
|
||||
let stem_path = stem_path.replace('\\', "/");
|
||||
let function_id = format!("{namespace}:{stem_path}");
|
||||
if let Ok(content) = fs::read_to_string(&path) {
|
||||
let lines: Vec<String> = content
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
||||
.map(|line| line.strip_prefix('/').unwrap_or(line).to_string())
|
||||
.collect();
|
||||
functions.insert(function_id, lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_function_tags_from_dir<S: std::hash::BuildHasher>(
|
||||
namespace: &str,
|
||||
tags_dir: &Path,
|
||||
function_tags: &mut HashMap<String, Vec<String>, S>,
|
||||
) {
|
||||
let function_tags_dir = tags_dir.join("function");
|
||||
let function_tags_dir_plural = tags_dir.join("functions");
|
||||
for dir in [&function_tags_dir, &function_tags_dir_plural] {
|
||||
if dir.is_dir() {
|
||||
load_tags_recursive(namespace, dir, dir, function_tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_tags_recursive<S: std::hash::BuildHasher>(
|
||||
namespace: &str,
|
||||
base_dir: &Path,
|
||||
current_dir: &Path,
|
||||
tags: &mut HashMap<String, Vec<String>, S>,
|
||||
) {
|
||||
let Ok(entries) = fs::read_dir(current_dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
load_tags_recursive(namespace, base_dir, &path, tags);
|
||||
} else if path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
|
||||
&& let Ok(rel_path) = path.strip_prefix(base_dir)
|
||||
{
|
||||
let mut stem_path = rel_path.to_string_lossy().to_string();
|
||||
if let Some(s) = stem_path.strip_suffix(".json") {
|
||||
stem_path = s.to_string();
|
||||
}
|
||||
let stem_path = stem_path.replace('\\', "/");
|
||||
let tag_id = format!("{namespace}:{stem_path}");
|
||||
if let Ok(content) = fs::read_to_string(&path)
|
||||
&& let Ok(val) = serde_json::from_str::<serde_json::Value>(&content)
|
||||
&& let Some(values_arr) = val.get("values").and_then(serde_json::Value::as_array)
|
||||
{
|
||||
let list: Vec<String> = values_arr
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str().map(ToString::to_string))
|
||||
.collect();
|
||||
tags.entry(tag_id).or_default().extend(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn parse_mcfunction_content() {
|
||||
let content = "# This is a comment\nsay Hello world\n/give @p diamond 1\n\n# Another comment\ngive @p stick 5\n";
|
||||
let lines: Vec<String> = content
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
||||
.map(|line| line.strip_prefix('/').unwrap_or(line).to_string())
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"say Hello world".to_string(),
|
||||
"give @p diamond 1".to_string(),
|
||||
"give @p stick 5".to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
275
crates/pumpkin/src/data/datapack/mod.rs
Normal file
275
crates/pumpkin/src/data/datapack/mod.rs
Normal file
@ -0,0 +1,275 @@
|
||||
pub mod function_loader;
|
||||
pub mod recipe_loader;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use pumpkin_protocol::codec::recipe::DynamicRecipe;
|
||||
|
||||
use crate::command::context::command_source::CommandSource;
|
||||
use crate::server::Server;
|
||||
use crate::server::recipe::RecipeManager;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LoadedDatapack {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub pack_format: u32,
|
||||
pub root_path: PathBuf,
|
||||
pub recipe_count: usize,
|
||||
pub function_count: usize,
|
||||
}
|
||||
|
||||
pub struct DatapackManager {
|
||||
loaded_packs: RwLock<Vec<LoadedDatapack>>,
|
||||
functions: RwLock<HashMap<String, Vec<String>>>,
|
||||
function_tags: RwLock<HashMap<String, Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Default for DatapackManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DatapackManager {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
loaded_packs: RwLock::new(Vec::new()),
|
||||
functions: RwLock::new(HashMap::new()),
|
||||
function_tags: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_all(
|
||||
&self,
|
||||
world_path: &Path,
|
||||
enabled_packs: &[String],
|
||||
recipe_manager: &RecipeManager,
|
||||
) {
|
||||
let datapacks_dir = world_path.join("datapacks");
|
||||
let mut loaded_packs_vec = Vec::new();
|
||||
let mut all_recipes: Vec<DynamicRecipe> = Vec::new();
|
||||
let mut all_functions: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let mut all_function_tags: HashMap<String, Vec<String>> = HashMap::new();
|
||||
|
||||
if datapacks_dir.is_dir() {
|
||||
let Ok(entries) = fs::read_dir(&datapacks_dir) else {
|
||||
warn!(
|
||||
"Failed to read datapacks directory: {}",
|
||||
datapacks_dir.display()
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let pack_path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
if file_name.starts_with('.') || !pack_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pack_id = format!("file/{file_name}");
|
||||
let is_enabled = enabled_packs
|
||||
.iter()
|
||||
.any(|p| p == &pack_id || p == &file_name);
|
||||
if !is_enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (description, pack_format) = read_pack_mcmeta(&pack_path);
|
||||
|
||||
let data_dir = pack_path.join("data");
|
||||
let mut pack_recipe_count = 0;
|
||||
let mut pack_function_count = 0;
|
||||
|
||||
if data_dir.is_dir()
|
||||
&& let Ok(ns_entries) = fs::read_dir(&data_dir)
|
||||
{
|
||||
for ns_entry in ns_entries.flatten() {
|
||||
let ns_path = ns_entry.path();
|
||||
if !ns_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let namespace = ns_entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
// Load recipes
|
||||
for recipe_sub in ["recipe", "recipes"] {
|
||||
let recipe_dir = ns_path.join(recipe_sub);
|
||||
if recipe_dir.is_dir() {
|
||||
load_recipes_from_dir(
|
||||
&namespace,
|
||||
&recipe_dir,
|
||||
&mut all_recipes,
|
||||
&mut pack_recipe_count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load functions
|
||||
for fn_sub in ["function", "functions"] {
|
||||
let fn_dir = ns_path.join(fn_sub);
|
||||
if fn_dir.is_dir() {
|
||||
let before = all_functions.len();
|
||||
function_loader::load_functions_from_dir(
|
||||
&namespace,
|
||||
&fn_dir,
|
||||
&mut all_functions,
|
||||
);
|
||||
pack_function_count += all_functions.len() - before;
|
||||
}
|
||||
}
|
||||
|
||||
// Load tags
|
||||
let tags_dir = ns_path.join("tags");
|
||||
if tags_dir.is_dir() {
|
||||
function_loader::load_function_tags_from_dir(
|
||||
&namespace,
|
||||
&tags_dir,
|
||||
&mut all_function_tags,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Loaded datapack '{file_name}': {pack_recipe_count} recipe(s), {pack_function_count} function(s)"
|
||||
);
|
||||
|
||||
loaded_packs_vec.push(LoadedDatapack {
|
||||
id: pack_id,
|
||||
name: file_name,
|
||||
description,
|
||||
pack_format,
|
||||
root_path: pack_path,
|
||||
recipe_count: pack_recipe_count,
|
||||
function_count: pack_function_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
recipe_manager.set_recipes(all_recipes).await;
|
||||
*self.loaded_packs.write().await = loaded_packs_vec;
|
||||
*self.functions.write().await = all_functions;
|
||||
*self.function_tags.write().await = all_function_tags;
|
||||
}
|
||||
|
||||
pub async fn get_loaded_packs(&self) -> Vec<LoadedDatapack> {
|
||||
self.loaded_packs.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn get_functions(&self) -> HashMap<String, Vec<String>> {
|
||||
self.functions.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn get_function_names(&self) -> Vec<String> {
|
||||
let fns = self.functions.read().await;
|
||||
let tags = self.function_tags.read().await;
|
||||
let mut names = Vec::with_capacity(fns.len() + tags.len());
|
||||
names.extend(fns.keys().cloned());
|
||||
for tag in tags.keys() {
|
||||
names.push(format!("#{tag}"));
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
pub async fn execute_function(
|
||||
&self,
|
||||
server: &Arc<Server>,
|
||||
source: &CommandSource,
|
||||
name: &str,
|
||||
) -> Result<usize, String> {
|
||||
let (functions_to_run, is_tag) = if let Some(tag_name) = name.strip_prefix('#') {
|
||||
let tags = self.function_tags.read().await;
|
||||
let Some(fns) = tags.get(tag_name) else {
|
||||
return Err(format!("Unknown function tag: #{tag_name}"));
|
||||
};
|
||||
(fns.clone(), true)
|
||||
} else {
|
||||
(vec![name.to_string()], false)
|
||||
};
|
||||
|
||||
let all_fns = self.functions.read().await;
|
||||
let mut total_executed = 0;
|
||||
|
||||
for fn_id in functions_to_run {
|
||||
let Some(lines) = all_fns.get(&fn_id) else {
|
||||
if !is_tag {
|
||||
return Err(format!("Unknown function: {fn_id}"));
|
||||
}
|
||||
continue;
|
||||
};
|
||||
|
||||
for line in lines {
|
||||
server
|
||||
.command_dispatcher
|
||||
.load()
|
||||
.handle_command(source, line)
|
||||
.await;
|
||||
total_executed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total_executed)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_pack_mcmeta(pack_path: &Path) -> (String, u32) {
|
||||
let mcmeta_path = pack_path.join("pack.mcmeta");
|
||||
if let Ok(content) = fs::read_to_string(mcmeta_path)
|
||||
&& let Ok(val) = serde_json::from_str::<serde_json::Value>(&content)
|
||||
{
|
||||
let pack = val.get("pack");
|
||||
let description = pack
|
||||
.and_then(|p| p.get("description"))
|
||||
.map(|d| {
|
||||
d.as_str()
|
||||
.map_or_else(|| d.to_string(), ToString::to_string)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let pack_format = pack
|
||||
.and_then(|p| p.get("pack_format"))
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.unwrap_or(61) as u32;
|
||||
return (description, pack_format);
|
||||
}
|
||||
(String::new(), 61)
|
||||
}
|
||||
|
||||
fn load_recipes_from_dir(
|
||||
namespace: &str,
|
||||
dir: &Path,
|
||||
all_recipes: &mut Vec<DynamicRecipe>,
|
||||
count: &mut usize,
|
||||
) {
|
||||
let Ok(entries) = fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
load_recipes_from_dir(namespace, &path, all_recipes, count);
|
||||
} else if path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
|
||||
{
|
||||
let stem = path
|
||||
.file_stem()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
if let Ok(content) = fs::read_to_string(&path)
|
||||
&& let Some(recipe) = recipe_loader::parse_recipe(namespace, &stem, &content)
|
||||
{
|
||||
all_recipes.push(recipe);
|
||||
*count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
294
crates/pumpkin/src/data/datapack/recipe_loader.rs
Normal file
294
crates/pumpkin/src/data/datapack/recipe_loader.rs
Normal file
@ -0,0 +1,294 @@
|
||||
use pumpkin_data::recipes::RecipeCategoryTypes;
|
||||
use pumpkin_protocol::codec::recipe::{
|
||||
DynamicRecipe, OwnedCookingRecipe, OwnedCookingRecipeType, OwnedCraftingRecipe,
|
||||
OwnedRecipeIngredient, OwnedRecipeResult,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn parse_recipe(namespace: &str, name: &str, json_str: &str) -> Option<DynamicRecipe> {
|
||||
let value: Value = serde_json::from_str(json_str).ok()?;
|
||||
let recipe_id = format!("{namespace}:{name}");
|
||||
let raw_type = value.get("type")?.as_str()?;
|
||||
let recipe_type = raw_type.strip_prefix("minecraft:").unwrap_or(raw_type);
|
||||
|
||||
match recipe_type {
|
||||
"crafting_shaped" => parse_shaped(recipe_id, &value).map(DynamicRecipe::Crafting),
|
||||
"crafting_shapeless" => parse_shapeless(recipe_id, &value).map(DynamicRecipe::Crafting),
|
||||
"smelting" => parse_cooking(recipe_id, &value, 200)
|
||||
.map(|c| DynamicRecipe::Cooking(OwnedCookingRecipeType::Smelting(c))),
|
||||
"blasting" => parse_cooking(recipe_id, &value, 100)
|
||||
.map(|c| DynamicRecipe::Cooking(OwnedCookingRecipeType::Blasting(c))),
|
||||
"smoking" => parse_cooking(recipe_id, &value, 100)
|
||||
.map(|c| DynamicRecipe::Cooking(OwnedCookingRecipeType::Smoking(c))),
|
||||
"campfire_cooking" => parse_cooking(recipe_id, &value, 600)
|
||||
.map(|c| DynamicRecipe::Cooking(OwnedCookingRecipeType::CampfireCooking(c))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_id(id: &str) -> String {
|
||||
if id.contains(':') {
|
||||
id.to_string()
|
||||
} else {
|
||||
format!("minecraft:{id}")
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ingredient(value: &Value) -> Option<OwnedRecipeIngredient> {
|
||||
match value {
|
||||
Value::String(s) => Some(OwnedRecipeIngredient::Simple(normalize_id(s))),
|
||||
Value::Object(map) => map
|
||||
.get("tag")
|
||||
.and_then(Value::as_str)
|
||||
.map(|tag| OwnedRecipeIngredient::Tagged(normalize_id(tag)))
|
||||
.or_else(|| {
|
||||
map.get("item")
|
||||
.and_then(Value::as_str)
|
||||
.map(|item| OwnedRecipeIngredient::Simple(normalize_id(item)))
|
||||
}),
|
||||
Value::Array(arr) => {
|
||||
let mut ids = Vec::new();
|
||||
for item in arr {
|
||||
if let Some(s) = item.as_str() {
|
||||
ids.push(normalize_id(s));
|
||||
} else if let Some(obj_item) = item.get("item").and_then(Value::as_str) {
|
||||
ids.push(normalize_id(obj_item));
|
||||
}
|
||||
}
|
||||
if ids.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(OwnedRecipeIngredient::OneOf(ids))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_result(value: &Value) -> Option<OwnedRecipeResult> {
|
||||
match value {
|
||||
Value::String(s) => Some(OwnedRecipeResult {
|
||||
item_id: normalize_id(s),
|
||||
count: 1,
|
||||
}),
|
||||
Value::Object(map) => {
|
||||
let id = map
|
||||
.get("id")
|
||||
.or_else(|| map.get("item"))
|
||||
.and_then(Value::as_str)?;
|
||||
let count = map
|
||||
.get("count")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(1)
|
||||
.min(99) as u8;
|
||||
Some(OwnedRecipeResult {
|
||||
item_id: normalize_id(id),
|
||||
count,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_category(value: Option<&Value>) -> RecipeCategoryTypes {
|
||||
let Some(val) = value.and_then(Value::as_str) else {
|
||||
return RecipeCategoryTypes::Misc;
|
||||
};
|
||||
match val.to_ascii_lowercase().as_str() {
|
||||
"equipment" => RecipeCategoryTypes::Equipment,
|
||||
"building" => RecipeCategoryTypes::Building,
|
||||
"redstone" | "restone" => RecipeCategoryTypes::Restone,
|
||||
"food" => RecipeCategoryTypes::Food,
|
||||
"blocks" => RecipeCategoryTypes::Blocks,
|
||||
_ => RecipeCategoryTypes::Misc,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_shaped(recipe_id: String, value: &Value) -> Option<OwnedCraftingRecipe> {
|
||||
let pattern = value
|
||||
.get("pattern")?
|
||||
.as_array()?
|
||||
.iter()
|
||||
.map(|v| v.as_str().map(ToString::to_string))
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
|
||||
let key_map = value.get("key")?.as_object()?;
|
||||
let mut key = Vec::new();
|
||||
for (k, v) in key_map {
|
||||
let ch = k.chars().next()?;
|
||||
let ingredient = parse_ingredient(v)?;
|
||||
key.push((ch, ingredient));
|
||||
}
|
||||
|
||||
let result = parse_result(value.get("result")?)?;
|
||||
let category = parse_category(value.get("category"));
|
||||
let group = value
|
||||
.get("group")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
let show_notification = value
|
||||
.get("show_notification")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(true);
|
||||
|
||||
Some(OwnedCraftingRecipe::Shaped {
|
||||
recipe_id: Some(recipe_id),
|
||||
category,
|
||||
group,
|
||||
show_notification,
|
||||
key,
|
||||
pattern,
|
||||
result,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_shapeless(recipe_id: String, value: &Value) -> Option<OwnedCraftingRecipe> {
|
||||
let ingredients_arr = value.get("ingredients")?.as_array()?;
|
||||
let mut ingredients = Vec::new();
|
||||
for ing_val in ingredients_arr {
|
||||
ingredients.push(parse_ingredient(ing_val)?);
|
||||
}
|
||||
|
||||
let result = parse_result(value.get("result")?)?;
|
||||
let category = parse_category(value.get("category"));
|
||||
let group = value
|
||||
.get("group")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
|
||||
Some(OwnedCraftingRecipe::Shapeless {
|
||||
recipe_id: Some(recipe_id),
|
||||
category,
|
||||
group,
|
||||
ingredients,
|
||||
result,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_cooking(
|
||||
recipe_id: String,
|
||||
value: &Value,
|
||||
default_cooking_time: i32,
|
||||
) -> Option<OwnedCookingRecipe> {
|
||||
let ingredient = parse_ingredient(value.get("ingredient")?)?;
|
||||
let result = parse_result(value.get("result")?)?;
|
||||
let category = parse_category(value.get("category"));
|
||||
let group = value
|
||||
.get("group")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
let cooking_time = value
|
||||
.get("cookingtime")
|
||||
.and_then(Value::as_i64)
|
||||
.map_or(default_cooking_time, |t| t as i32);
|
||||
let experience = value
|
||||
.get("experience")
|
||||
.and_then(Value::as_f64)
|
||||
.map_or(0.0, |e| e as f32);
|
||||
|
||||
Some(OwnedCookingRecipe {
|
||||
recipe_id,
|
||||
category,
|
||||
group,
|
||||
ingredient,
|
||||
cooking_time,
|
||||
experience,
|
||||
result,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_shaped_recipe() {
|
||||
let json = r##"{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"group": "sword",
|
||||
"pattern": ["X", "X", "#"],
|
||||
"key": {
|
||||
"#": "minecraft:stick",
|
||||
"X": "minecraft:diamond"
|
||||
},
|
||||
"result": {
|
||||
"id": "minecraft:diamond_sword",
|
||||
"count": 1
|
||||
}
|
||||
}"##;
|
||||
|
||||
let parsed = parse_recipe("test", "diamond_sword", json);
|
||||
assert!(parsed.is_some());
|
||||
if let Some(DynamicRecipe::Crafting(OwnedCraftingRecipe::Shaped {
|
||||
recipe_id,
|
||||
result,
|
||||
pattern,
|
||||
..
|
||||
})) = parsed
|
||||
{
|
||||
assert_eq!(recipe_id.as_deref(), Some("test:diamond_sword"));
|
||||
assert_eq!(result.item_id, "minecraft:diamond_sword");
|
||||
assert_eq!(pattern.len(), 3);
|
||||
} else {
|
||||
panic!("Expected shaped recipe");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_shapeless_recipe() {
|
||||
let json = r#"{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"ingredients": ["minecraft:oak_log"],
|
||||
"result": {
|
||||
"id": "minecraft:oak_planks",
|
||||
"count": 4
|
||||
}
|
||||
}"#;
|
||||
|
||||
let parsed = parse_recipe("minecraft", "oak_planks_from_oak_log", json);
|
||||
assert!(parsed.is_some());
|
||||
if let Some(DynamicRecipe::Crafting(OwnedCraftingRecipe::Shapeless {
|
||||
recipe_id,
|
||||
result,
|
||||
ingredients,
|
||||
..
|
||||
})) = parsed
|
||||
{
|
||||
assert_eq!(
|
||||
recipe_id.as_deref(),
|
||||
Some("minecraft:oak_planks_from_oak_log")
|
||||
);
|
||||
assert_eq!(result.item_id, "minecraft:oak_planks");
|
||||
assert_eq!(result.count, 4);
|
||||
assert_eq!(ingredients.len(), 1);
|
||||
} else {
|
||||
panic!("Expected shapeless recipe");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_smelting_recipe() {
|
||||
let json = r#"{
|
||||
"type": "minecraft:smelting",
|
||||
"ingredient": "minecraft:raw_iron",
|
||||
"result": {
|
||||
"id": "minecraft:iron_ingot"
|
||||
},
|
||||
"experience": 0.7,
|
||||
"cookingtime": 200
|
||||
}"#;
|
||||
|
||||
let parsed = parse_recipe("minecraft", "iron_ingot_from_smelting_raw_iron", json);
|
||||
assert!(parsed.is_some());
|
||||
if let Some(DynamicRecipe::Cooking(OwnedCookingRecipeType::Smelting(cooking))) = parsed {
|
||||
assert_eq!(
|
||||
cooking.recipe_id,
|
||||
"minecraft:iron_ingot_from_smelting_raw_iron"
|
||||
);
|
||||
assert_eq!(cooking.result.item_id, "minecraft:iron_ingot");
|
||||
assert_eq!(cooking.cooking_time, 200);
|
||||
} else {
|
||||
panic!("Expected smelting recipe");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ pub mod advancement_data;
|
||||
pub mod banlist_serializer;
|
||||
pub mod banned_ip;
|
||||
pub mod banned_player;
|
||||
pub mod datapack;
|
||||
pub mod player_server;
|
||||
pub mod usercache;
|
||||
pub mod whitelist;
|
||||
|
||||
@ -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}"));
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user