diff --git a/crates/pumpkin-data/Cargo.toml b/crates/pumpkin-data/Cargo.toml index 478ee3e78..63fb092a0 100644 --- a/crates/pumpkin-data/Cargo.toml +++ b/crates/pumpkin-data/Cargo.toml @@ -26,6 +26,7 @@ default = [ "game_rules", "entity", "dimension", + "environment_attribute", "enchantment", "world", "scoreboard", @@ -54,9 +55,11 @@ default = [ "map_color", "map_decoration", "dye_color", - "block_transformer" + "block_transformer", + "trial_spawner", ] +trial_spawner = [] block_transformer = ["block", "sound", "world", "tag"] map_color = [] map_decoration = [] @@ -83,6 +86,7 @@ game_event = [] game_rules = [] entity = ["entity_pose", "entity_status", "entity_type", "spawn_egg"] dimension = [] +environment_attribute = [] enchantment = [] world = ["world_event", "message_type"] scoreboard = [] diff --git a/crates/pumpkin-data/src/generated/dimension.rs b/crates/pumpkin-data/src/generated/dimension.rs index dd2e0ea77..5f44f4ee9 100644 --- a/crates/pumpkin-data/src/generated/dimension.rs +++ b/crates/pumpkin-data/src/generated/dimension.rs @@ -4,11 +4,44 @@ use pumpkin_util::math::int_provider::{ IntProvider, NormalIntProvider, TrapezoidIntProvider, UniformIntProvider, WeightedEntry, WeightedListIntProvider, }; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BedRuleOption { + Always, + WhenDark, + Never, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BedRule { + pub can_sleep: BedRuleOption, + pub can_set_spawn: BedRuleOption, + pub explodes: bool, +} +impl BedRule { + #[inline] + #[must_use] + pub const fn can_sleep(&self, is_dark_outside: bool) -> bool { + match self.can_sleep { + BedRuleOption::Always => true, + BedRuleOption::WhenDark => is_dark_outside, + BedRuleOption::Never => false, + } + } + #[inline] + #[must_use] + pub const fn can_set_spawn(&self, is_dark_outside: bool) -> bool { + match self.can_set_spawn { + BedRuleOption::Always => true, + BedRuleOption::WhenDark => is_dark_outside, + BedRuleOption::Never => false, + } + } +} #[derive(Debug, Clone)] pub struct Dimension { pub id: u8, pub minecraft_name: &'static str, pub fixed_time: Option, + pub has_fixed_time: bool, pub has_skylight: bool, pub has_ceiling: bool, pub coordinate_scale: f64, @@ -22,6 +55,21 @@ pub struct Dimension { pub sky_color: Option, pub fog_color: Option, pub cloud_color: Option, + pub ambient_light_color: Option, + pub sky_light_color: Option, + pub sky_light_factor: Option, + pub fog_start_distance: Option, + pub fog_end_distance: Option, + pub cloud_height: Option, + pub sky_light_level: Option, + pub water_evaporates: bool, + pub fast_lava: bool, + pub respawn_anchor_works: bool, + pub piglins_zombify: bool, + pub snow_golem_melts: bool, + pub can_start_raid: bool, + pub nether_portal_spawns_piglin: bool, + pub bed_rule: BedRule, pub timelines: Option<&'static str>, } impl Dimension { @@ -29,6 +77,7 @@ impl Dimension { id: 0u8, minecraft_name: "minecraft:overworld", fixed_time: None, + has_fixed_time: false, has_skylight: true, has_ceiling: false, coordinate_scale: 1f64, @@ -46,13 +95,33 @@ impl Dimension { monster_spawn_block_light_limit: 0u8, sky_color: Some(7907327i32), fog_color: Some(12638463i32), - cloud_color: None, + cloud_color: Some(-855638017i32), + ambient_light_color: Some(657930i32), + sky_light_color: None, + sky_light_factor: None, + fog_start_distance: None, + fog_end_distance: None, + cloud_height: Some(192.33f32), + sky_light_level: None, + water_evaporates: false, + fast_lava: false, + respawn_anchor_works: false, + piglins_zombify: true, + snow_golem_melts: false, + can_start_raid: true, + nether_portal_spawns_piglin: true, + bed_rule: BedRule { + can_sleep: BedRuleOption::WhenDark, + can_set_spawn: BedRuleOption::Always, + explodes: false, + }, timelines: Some("#minecraft:in_overworld"), }; pub const OVERWORLD_CAVES: Self = Self { id: 1u8, minecraft_name: "minecraft:overworld_caves", fixed_time: None, + has_fixed_time: false, has_skylight: true, has_ceiling: true, coordinate_scale: 1f64, @@ -70,13 +139,33 @@ impl Dimension { monster_spawn_block_light_limit: 0u8, sky_color: Some(7907327i32), fog_color: Some(12638463i32), - cloud_color: None, + cloud_color: Some(-855638017i32), + ambient_light_color: Some(657930i32), + sky_light_color: None, + sky_light_factor: None, + fog_start_distance: None, + fog_end_distance: None, + cloud_height: Some(192.33f32), + sky_light_level: None, + water_evaporates: false, + fast_lava: false, + respawn_anchor_works: false, + piglins_zombify: true, + snow_golem_melts: false, + can_start_raid: true, + nether_portal_spawns_piglin: true, + bed_rule: BedRule { + can_sleep: BedRuleOption::WhenDark, + can_set_spawn: BedRuleOption::Always, + explodes: false, + }, timelines: Some("#minecraft:in_overworld"), }; pub const THE_END: Self = Self { id: 2u8, minecraft_name: "minecraft:the_end", fixed_time: None, + has_fixed_time: true, has_skylight: true, has_ceiling: false, coordinate_scale: 1f64, @@ -90,12 +179,32 @@ impl Dimension { sky_color: Some(0i32), fog_color: Some(1577752i32), cloud_color: None, + ambient_light_color: Some(4147007i32), + sky_light_color: Some(11296973i32), + sky_light_factor: Some(0f32), + fog_start_distance: None, + fog_end_distance: None, + cloud_height: None, + sky_light_level: None, + water_evaporates: false, + fast_lava: false, + respawn_anchor_works: false, + piglins_zombify: true, + snow_golem_melts: false, + can_start_raid: true, + nether_portal_spawns_piglin: false, + bed_rule: BedRule { + can_sleep: BedRuleOption::Never, + can_set_spawn: BedRuleOption::Never, + explodes: true, + }, timelines: Some("#minecraft:in_end"), }; pub const THE_NETHER: Self = Self { id: 3u8, minecraft_name: "minecraft:the_nether", fixed_time: None, + has_fixed_time: true, has_skylight: false, has_ceiling: true, coordinate_scale: 8f64, @@ -109,6 +218,25 @@ impl Dimension { sky_color: None, fog_color: None, cloud_color: None, + ambient_light_color: Some(3156001i32), + sky_light_color: Some(8026879i32), + sky_light_factor: Some(0f32), + fog_start_distance: Some(10f32), + fog_end_distance: Some(96f32), + cloud_height: None, + sky_light_level: Some(4f32), + water_evaporates: true, + fast_lava: true, + respawn_anchor_works: true, + piglins_zombify: false, + snow_golem_melts: true, + can_start_raid: false, + nether_portal_spawns_piglin: false, + bed_rule: BedRule { + can_sleep: BedRuleOption::Never, + can_set_spawn: BedRuleOption::Never, + explodes: true, + }, timelines: Some("#minecraft:in_nether"), }; pub fn from_name(name: &str) -> Option<&'static Self> { @@ -120,6 +248,17 @@ impl Dimension { _ => None, } } + #[inline] + #[must_use] + pub const fn effective_sky_light_level(&self) -> f32 { + if let Some(level) = self.sky_light_level { + level + } else if self.has_skylight { + 15.0 + } else { + 0.0 + } + } } impl PartialEq for Dimension { fn eq(&self, other: &Self) -> bool { diff --git a/crates/pumpkin-data/src/generated/environment_attribute.rs b/crates/pumpkin-data/src/generated/environment_attribute.rs new file mode 100644 index 000000000..da4806fe2 --- /dev/null +++ b/crates/pumpkin-data/src/generated/environment_attribute.rs @@ -0,0 +1,1050 @@ +/* This file is generated. Do not edit manually. */ +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum EnvironmentAttributeType { + RgbColor, + ArgbColor, + Float, + AngleDegrees, + Boolean, + TriState, + MoonPhase, + Activity, + BedRule, + Particle, + AmbientParticles, + BackgroundMusic, + AmbientSounds, + Integer, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum EnvironmentAttribute { + VisualFogColor, + VisualFogStartDistance, + VisualFogEndDistance, + VisualSkyFogEndDistance, + VisualCloudFogEndDistance, + VisualWaterFogColor, + VisualWaterFogStartDistance, + VisualWaterFogEndDistance, + VisualSkyColor, + VisualSunriseSunsetColor, + VisualCloudColor, + VisualCloudHeight, + VisualSunAngle, + VisualMoonAngle, + VisualStarAngle, + VisualMoonPhase, + VisualStarBrightness, + VisualBlockLightTint, + VisualSkyLightColor, + VisualSkyLightFactor, + VisualNightVisionColor, + VisualAmbientLightColor, + VisualDefaultDripstoneParticle, + VisualAmbientParticles, + AudioBackgroundMusic, + AudioMusicVolume, + AudioAmbientSounds, + AudioFireflyBushSounds, + GameplaySkyLightLevel, + GameplayCanStartRaid, + GameplayWaterEvaporates, + GameplayBedRule, + GameplayRespawnAnchorWorks, + GameplayNetherPortalSpawnsPiglin, + GameplayFastLava, + GameplayIncreasedFireBurnout, + GameplayEyeblossomOpen, + GameplayTurtleEggHatchChance, + GameplayPiglinsZombify, + GameplaySnowGolemMelts, + GameplayCreakingActive, + GameplaySurfaceSlimeSpawnChance, + GameplayCatWakingUpGiftChance, + GameplayBeesStayInHive, + GameplayMonstersBurn, + GameplayCanPillagerPatrolSpawn, + GameplayVillagerActivity, + GameplayBabyVillagerActivity, +} +impl EnvironmentAttribute { + #[must_use] + pub const fn id(&self) -> u8 { + match self { + Self::VisualFogColor => 0u8, + Self::VisualFogStartDistance => 1u8, + Self::VisualFogEndDistance => 2u8, + Self::VisualSkyFogEndDistance => 3u8, + Self::VisualCloudFogEndDistance => 4u8, + Self::VisualWaterFogColor => 5u8, + Self::VisualWaterFogStartDistance => 6u8, + Self::VisualWaterFogEndDistance => 7u8, + Self::VisualSkyColor => 8u8, + Self::VisualSunriseSunsetColor => 9u8, + Self::VisualCloudColor => 10u8, + Self::VisualCloudHeight => 11u8, + Self::VisualSunAngle => 12u8, + Self::VisualMoonAngle => 13u8, + Self::VisualStarAngle => 14u8, + Self::VisualMoonPhase => 15u8, + Self::VisualStarBrightness => 16u8, + Self::VisualBlockLightTint => 17u8, + Self::VisualSkyLightColor => 18u8, + Self::VisualSkyLightFactor => 19u8, + Self::VisualNightVisionColor => 20u8, + Self::VisualAmbientLightColor => 21u8, + Self::VisualDefaultDripstoneParticle => 22u8, + Self::VisualAmbientParticles => 23u8, + Self::AudioBackgroundMusic => 24u8, + Self::AudioMusicVolume => 25u8, + Self::AudioAmbientSounds => 26u8, + Self::AudioFireflyBushSounds => 27u8, + Self::GameplaySkyLightLevel => 28u8, + Self::GameplayCanStartRaid => 29u8, + Self::GameplayWaterEvaporates => 30u8, + Self::GameplayBedRule => 31u8, + Self::GameplayRespawnAnchorWorks => 32u8, + Self::GameplayNetherPortalSpawnsPiglin => 33u8, + Self::GameplayFastLava => 34u8, + Self::GameplayIncreasedFireBurnout => 35u8, + Self::GameplayEyeblossomOpen => 36u8, + Self::GameplayTurtleEggHatchChance => 37u8, + Self::GameplayPiglinsZombify => 38u8, + Self::GameplaySnowGolemMelts => 39u8, + Self::GameplayCreakingActive => 40u8, + Self::GameplaySurfaceSlimeSpawnChance => 41u8, + Self::GameplayCatWakingUpGiftChance => 42u8, + Self::GameplayBeesStayInHive => 43u8, + Self::GameplayMonstersBurn => 44u8, + Self::GameplayCanPillagerPatrolSpawn => 45u8, + Self::GameplayVillagerActivity => 46u8, + Self::GameplayBabyVillagerActivity => 47u8, + } + } + #[must_use] + pub const fn name(&self) -> &'static str { + match self { + Self::VisualFogColor => "visual/fog_color", + Self::VisualFogStartDistance => "visual/fog_start_distance", + Self::VisualFogEndDistance => "visual/fog_end_distance", + Self::VisualSkyFogEndDistance => "visual/sky_fog_end_distance", + Self::VisualCloudFogEndDistance => "visual/cloud_fog_end_distance", + Self::VisualWaterFogColor => "visual/water_fog_color", + Self::VisualWaterFogStartDistance => "visual/water_fog_start_distance", + Self::VisualWaterFogEndDistance => "visual/water_fog_end_distance", + Self::VisualSkyColor => "visual/sky_color", + Self::VisualSunriseSunsetColor => "visual/sunrise_sunset_color", + Self::VisualCloudColor => "visual/cloud_color", + Self::VisualCloudHeight => "visual/cloud_height", + Self::VisualSunAngle => "visual/sun_angle", + Self::VisualMoonAngle => "visual/moon_angle", + Self::VisualStarAngle => "visual/star_angle", + Self::VisualMoonPhase => "visual/moon_phase", + Self::VisualStarBrightness => "visual/star_brightness", + Self::VisualBlockLightTint => "visual/block_light_tint", + Self::VisualSkyLightColor => "visual/sky_light_color", + Self::VisualSkyLightFactor => "visual/sky_light_factor", + Self::VisualNightVisionColor => "visual/night_vision_color", + Self::VisualAmbientLightColor => "visual/ambient_light_color", + Self::VisualDefaultDripstoneParticle => "visual/default_dripstone_particle", + Self::VisualAmbientParticles => "visual/ambient_particles", + Self::AudioBackgroundMusic => "audio/background_music", + Self::AudioMusicVolume => "audio/music_volume", + Self::AudioAmbientSounds => "audio/ambient_sounds", + Self::AudioFireflyBushSounds => "audio/firefly_bush_sounds", + Self::GameplaySkyLightLevel => "gameplay/sky_light_level", + Self::GameplayCanStartRaid => "gameplay/can_start_raid", + Self::GameplayWaterEvaporates => "gameplay/water_evaporates", + Self::GameplayBedRule => "gameplay/bed_rule", + Self::GameplayRespawnAnchorWorks => "gameplay/respawn_anchor_works", + Self::GameplayNetherPortalSpawnsPiglin => "gameplay/nether_portal_spawns_piglin", + Self::GameplayFastLava => "gameplay/fast_lava", + Self::GameplayIncreasedFireBurnout => "gameplay/increased_fire_burnout", + Self::GameplayEyeblossomOpen => "gameplay/eyeblossom_open", + Self::GameplayTurtleEggHatchChance => "gameplay/turtle_egg_hatch_chance", + Self::GameplayPiglinsZombify => "gameplay/piglins_zombify", + Self::GameplaySnowGolemMelts => "gameplay/snow_golem_melts", + Self::GameplayCreakingActive => "gameplay/creaking_active", + Self::GameplaySurfaceSlimeSpawnChance => "gameplay/surface_slime_spawn_chance", + Self::GameplayCatWakingUpGiftChance => "gameplay/cat_waking_up_gift_chance", + Self::GameplayBeesStayInHive => "gameplay/bees_stay_in_hive", + Self::GameplayMonstersBurn => "gameplay/monsters_burn", + Self::GameplayCanPillagerPatrolSpawn => "gameplay/can_pillager_patrol_spawn", + Self::GameplayVillagerActivity => "gameplay/villager_activity", + Self::GameplayBabyVillagerActivity => "gameplay/baby_villager_activity", + } + } + #[must_use] + pub const fn resource_location(&self) -> &'static str { + match self { + Self::VisualFogColor => "minecraft:visual/fog_color", + Self::VisualFogStartDistance => "minecraft:visual/fog_start_distance", + Self::VisualFogEndDistance => "minecraft:visual/fog_end_distance", + Self::VisualSkyFogEndDistance => "minecraft:visual/sky_fog_end_distance", + Self::VisualCloudFogEndDistance => "minecraft:visual/cloud_fog_end_distance", + Self::VisualWaterFogColor => "minecraft:visual/water_fog_color", + Self::VisualWaterFogStartDistance => "minecraft:visual/water_fog_start_distance", + Self::VisualWaterFogEndDistance => "minecraft:visual/water_fog_end_distance", + Self::VisualSkyColor => "minecraft:visual/sky_color", + Self::VisualSunriseSunsetColor => "minecraft:visual/sunrise_sunset_color", + Self::VisualCloudColor => "minecraft:visual/cloud_color", + Self::VisualCloudHeight => "minecraft:visual/cloud_height", + Self::VisualSunAngle => "minecraft:visual/sun_angle", + Self::VisualMoonAngle => "minecraft:visual/moon_angle", + Self::VisualStarAngle => "minecraft:visual/star_angle", + Self::VisualMoonPhase => "minecraft:visual/moon_phase", + Self::VisualStarBrightness => "minecraft:visual/star_brightness", + Self::VisualBlockLightTint => "minecraft:visual/block_light_tint", + Self::VisualSkyLightColor => "minecraft:visual/sky_light_color", + Self::VisualSkyLightFactor => "minecraft:visual/sky_light_factor", + Self::VisualNightVisionColor => "minecraft:visual/night_vision_color", + Self::VisualAmbientLightColor => "minecraft:visual/ambient_light_color", + Self::VisualDefaultDripstoneParticle => "minecraft:visual/default_dripstone_particle", + Self::VisualAmbientParticles => "minecraft:visual/ambient_particles", + Self::AudioBackgroundMusic => "minecraft:audio/background_music", + Self::AudioMusicVolume => "minecraft:audio/music_volume", + Self::AudioAmbientSounds => "minecraft:audio/ambient_sounds", + Self::AudioFireflyBushSounds => "minecraft:audio/firefly_bush_sounds", + Self::GameplaySkyLightLevel => "minecraft:gameplay/sky_light_level", + Self::GameplayCanStartRaid => "minecraft:gameplay/can_start_raid", + Self::GameplayWaterEvaporates => "minecraft:gameplay/water_evaporates", + Self::GameplayBedRule => "minecraft:gameplay/bed_rule", + Self::GameplayRespawnAnchorWorks => "minecraft:gameplay/respawn_anchor_works", + Self::GameplayNetherPortalSpawnsPiglin => { + "minecraft:gameplay/nether_portal_spawns_piglin" + } + Self::GameplayFastLava => "minecraft:gameplay/fast_lava", + Self::GameplayIncreasedFireBurnout => "minecraft:gameplay/increased_fire_burnout", + Self::GameplayEyeblossomOpen => "minecraft:gameplay/eyeblossom_open", + Self::GameplayTurtleEggHatchChance => "minecraft:gameplay/turtle_egg_hatch_chance", + Self::GameplayPiglinsZombify => "minecraft:gameplay/piglins_zombify", + Self::GameplaySnowGolemMelts => "minecraft:gameplay/snow_golem_melts", + Self::GameplayCreakingActive => "minecraft:gameplay/creaking_active", + Self::GameplaySurfaceSlimeSpawnChance => { + "minecraft:gameplay/surface_slime_spawn_chance" + } + Self::GameplayCatWakingUpGiftChance => "minecraft:gameplay/cat_waking_up_gift_chance", + Self::GameplayBeesStayInHive => "minecraft:gameplay/bees_stay_in_hive", + Self::GameplayMonstersBurn => "minecraft:gameplay/monsters_burn", + Self::GameplayCanPillagerPatrolSpawn => "minecraft:gameplay/can_pillager_patrol_spawn", + Self::GameplayVillagerActivity => "minecraft:gameplay/villager_activity", + Self::GameplayBabyVillagerActivity => "minecraft:gameplay/baby_villager_activity", + } + } + #[must_use] + pub const fn attribute_type(&self) -> EnvironmentAttributeType { + match self { + Self::VisualFogColor => EnvironmentAttributeType::RgbColor, + Self::VisualFogStartDistance => EnvironmentAttributeType::Float, + Self::VisualFogEndDistance => EnvironmentAttributeType::Float, + Self::VisualSkyFogEndDistance => EnvironmentAttributeType::Float, + Self::VisualCloudFogEndDistance => EnvironmentAttributeType::Float, + Self::VisualWaterFogColor => EnvironmentAttributeType::RgbColor, + Self::VisualWaterFogStartDistance => EnvironmentAttributeType::Float, + Self::VisualWaterFogEndDistance => EnvironmentAttributeType::Float, + Self::VisualSkyColor => EnvironmentAttributeType::RgbColor, + Self::VisualSunriseSunsetColor => EnvironmentAttributeType::ArgbColor, + Self::VisualCloudColor => EnvironmentAttributeType::ArgbColor, + Self::VisualCloudHeight => EnvironmentAttributeType::Float, + Self::VisualSunAngle => EnvironmentAttributeType::AngleDegrees, + Self::VisualMoonAngle => EnvironmentAttributeType::AngleDegrees, + Self::VisualStarAngle => EnvironmentAttributeType::AngleDegrees, + Self::VisualMoonPhase => EnvironmentAttributeType::MoonPhase, + Self::VisualStarBrightness => EnvironmentAttributeType::Float, + Self::VisualBlockLightTint => EnvironmentAttributeType::RgbColor, + Self::VisualSkyLightColor => EnvironmentAttributeType::RgbColor, + Self::VisualSkyLightFactor => EnvironmentAttributeType::Float, + Self::VisualNightVisionColor => EnvironmentAttributeType::RgbColor, + Self::VisualAmbientLightColor => EnvironmentAttributeType::RgbColor, + Self::VisualDefaultDripstoneParticle => EnvironmentAttributeType::Particle, + Self::VisualAmbientParticles => EnvironmentAttributeType::AmbientParticles, + Self::AudioBackgroundMusic => EnvironmentAttributeType::BackgroundMusic, + Self::AudioMusicVolume => EnvironmentAttributeType::Float, + Self::AudioAmbientSounds => EnvironmentAttributeType::AmbientSounds, + Self::AudioFireflyBushSounds => EnvironmentAttributeType::Boolean, + Self::GameplaySkyLightLevel => EnvironmentAttributeType::Float, + Self::GameplayCanStartRaid => EnvironmentAttributeType::Boolean, + Self::GameplayWaterEvaporates => EnvironmentAttributeType::Boolean, + Self::GameplayBedRule => EnvironmentAttributeType::BedRule, + Self::GameplayRespawnAnchorWorks => EnvironmentAttributeType::Boolean, + Self::GameplayNetherPortalSpawnsPiglin => EnvironmentAttributeType::Boolean, + Self::GameplayFastLava => EnvironmentAttributeType::Boolean, + Self::GameplayIncreasedFireBurnout => EnvironmentAttributeType::Boolean, + Self::GameplayEyeblossomOpen => EnvironmentAttributeType::TriState, + Self::GameplayTurtleEggHatchChance => EnvironmentAttributeType::Float, + Self::GameplayPiglinsZombify => EnvironmentAttributeType::Boolean, + Self::GameplaySnowGolemMelts => EnvironmentAttributeType::Boolean, + Self::GameplayCreakingActive => EnvironmentAttributeType::Boolean, + Self::GameplaySurfaceSlimeSpawnChance => EnvironmentAttributeType::Float, + Self::GameplayCatWakingUpGiftChance => EnvironmentAttributeType::Float, + Self::GameplayBeesStayInHive => EnvironmentAttributeType::Boolean, + Self::GameplayMonstersBurn => EnvironmentAttributeType::Boolean, + Self::GameplayCanPillagerPatrolSpawn => EnvironmentAttributeType::Boolean, + Self::GameplayVillagerActivity => EnvironmentAttributeType::Activity, + Self::GameplayBabyVillagerActivity => EnvironmentAttributeType::Activity, + } + } + #[must_use] + pub const fn is_syncable(&self) -> bool { + match self { + Self::VisualFogColor => true, + Self::VisualFogStartDistance => true, + Self::VisualFogEndDistance => true, + Self::VisualSkyFogEndDistance => true, + Self::VisualCloudFogEndDistance => true, + Self::VisualWaterFogColor => true, + Self::VisualWaterFogStartDistance => true, + Self::VisualWaterFogEndDistance => true, + Self::VisualSkyColor => true, + Self::VisualSunriseSunsetColor => true, + Self::VisualCloudColor => true, + Self::VisualCloudHeight => true, + Self::VisualSunAngle => true, + Self::VisualMoonAngle => true, + Self::VisualStarAngle => true, + Self::VisualMoonPhase => true, + Self::VisualStarBrightness => true, + Self::VisualBlockLightTint => true, + Self::VisualSkyLightColor => true, + Self::VisualSkyLightFactor => true, + Self::VisualNightVisionColor => true, + Self::VisualAmbientLightColor => true, + Self::VisualDefaultDripstoneParticle => true, + Self::VisualAmbientParticles => true, + Self::AudioBackgroundMusic => true, + Self::AudioMusicVolume => true, + Self::AudioAmbientSounds => true, + Self::AudioFireflyBushSounds => true, + Self::GameplaySkyLightLevel => true, + Self::GameplayCanStartRaid => false, + Self::GameplayWaterEvaporates => true, + Self::GameplayBedRule => false, + Self::GameplayRespawnAnchorWorks => false, + Self::GameplayNetherPortalSpawnsPiglin => false, + Self::GameplayFastLava => true, + Self::GameplayIncreasedFireBurnout => false, + Self::GameplayEyeblossomOpen => false, + Self::GameplayTurtleEggHatchChance => false, + Self::GameplayPiglinsZombify => true, + Self::GameplaySnowGolemMelts => false, + Self::GameplayCreakingActive => true, + Self::GameplaySurfaceSlimeSpawnChance => false, + Self::GameplayCatWakingUpGiftChance => false, + Self::GameplayBeesStayInHive => false, + Self::GameplayMonstersBurn => false, + Self::GameplayCanPillagerPatrolSpawn => false, + Self::GameplayVillagerActivity => false, + Self::GameplayBabyVillagerActivity => false, + } + } + #[must_use] + pub const fn is_positional(&self) -> bool { + match self { + Self::VisualFogColor => true, + Self::VisualFogStartDistance => true, + Self::VisualFogEndDistance => true, + Self::VisualSkyFogEndDistance => true, + Self::VisualCloudFogEndDistance => true, + Self::VisualWaterFogColor => true, + Self::VisualWaterFogStartDistance => true, + Self::VisualWaterFogEndDistance => true, + Self::VisualSkyColor => true, + Self::VisualSunriseSunsetColor => true, + Self::VisualCloudColor => true, + Self::VisualCloudHeight => true, + Self::VisualSunAngle => true, + Self::VisualMoonAngle => true, + Self::VisualStarAngle => true, + Self::VisualMoonPhase => true, + Self::VisualStarBrightness => true, + Self::VisualBlockLightTint => true, + Self::VisualSkyLightColor => true, + Self::VisualSkyLightFactor => true, + Self::VisualNightVisionColor => true, + Self::VisualAmbientLightColor => true, + Self::VisualDefaultDripstoneParticle => true, + Self::VisualAmbientParticles => true, + Self::AudioBackgroundMusic => true, + Self::AudioMusicVolume => true, + Self::AudioAmbientSounds => true, + Self::AudioFireflyBushSounds => true, + Self::GameplaySkyLightLevel => false, + Self::GameplayCanStartRaid => true, + Self::GameplayWaterEvaporates => true, + Self::GameplayBedRule => true, + Self::GameplayRespawnAnchorWorks => true, + Self::GameplayNetherPortalSpawnsPiglin => true, + Self::GameplayFastLava => false, + Self::GameplayIncreasedFireBurnout => true, + Self::GameplayEyeblossomOpen => true, + Self::GameplayTurtleEggHatchChance => true, + Self::GameplayPiglinsZombify => true, + Self::GameplaySnowGolemMelts => true, + Self::GameplayCreakingActive => true, + Self::GameplaySurfaceSlimeSpawnChance => true, + Self::GameplayCatWakingUpGiftChance => true, + Self::GameplayBeesStayInHive => true, + Self::GameplayMonstersBurn => true, + Self::GameplayCanPillagerPatrolSpawn => true, + Self::GameplayVillagerActivity => true, + Self::GameplayBabyVillagerActivity => true, + } + } + #[must_use] + pub const fn is_spatially_interpolated(&self) -> bool { + match self { + Self::VisualFogColor => true, + Self::VisualFogStartDistance => true, + Self::VisualFogEndDistance => true, + Self::VisualSkyFogEndDistance => true, + Self::VisualCloudFogEndDistance => true, + Self::VisualWaterFogColor => true, + Self::VisualWaterFogStartDistance => true, + Self::VisualWaterFogEndDistance => true, + Self::VisualSkyColor => true, + Self::VisualSunriseSunsetColor => true, + Self::VisualCloudColor => true, + Self::VisualCloudHeight => true, + Self::VisualSunAngle => true, + Self::VisualMoonAngle => true, + Self::VisualStarAngle => true, + Self::VisualMoonPhase => false, + Self::VisualStarBrightness => true, + Self::VisualBlockLightTint => true, + Self::VisualSkyLightColor => true, + Self::VisualSkyLightFactor => true, + Self::VisualNightVisionColor => true, + Self::VisualAmbientLightColor => true, + Self::VisualDefaultDripstoneParticle => false, + Self::VisualAmbientParticles => false, + Self::AudioBackgroundMusic => false, + Self::AudioMusicVolume => false, + Self::AudioAmbientSounds => false, + Self::AudioFireflyBushSounds => false, + Self::GameplaySkyLightLevel => false, + Self::GameplayCanStartRaid => false, + Self::GameplayWaterEvaporates => false, + Self::GameplayBedRule => false, + Self::GameplayRespawnAnchorWorks => false, + Self::GameplayNetherPortalSpawnsPiglin => false, + Self::GameplayFastLava => false, + Self::GameplayIncreasedFireBurnout => false, + Self::GameplayEyeblossomOpen => false, + Self::GameplayTurtleEggHatchChance => false, + Self::GameplayPiglinsZombify => false, + Self::GameplaySnowGolemMelts => false, + Self::GameplayCreakingActive => false, + Self::GameplaySurfaceSlimeSpawnChance => false, + Self::GameplayCatWakingUpGiftChance => false, + Self::GameplayBeesStayInHive => false, + Self::GameplayMonstersBurn => false, + Self::GameplayCanPillagerPatrolSpawn => false, + Self::GameplayVillagerActivity => false, + Self::GameplayBabyVillagerActivity => false, + } + } + #[must_use] + pub fn from_name(name: &str) -> Option { + match name { + "visual/fog_color" | "minecraft:visual/fog_color" => Some(Self::VisualFogColor), + "visual/fog_start_distance" | "minecraft:visual/fog_start_distance" => { + Some(Self::VisualFogStartDistance) + } + "visual/fog_end_distance" | "minecraft:visual/fog_end_distance" => { + Some(Self::VisualFogEndDistance) + } + "visual/sky_fog_end_distance" | "minecraft:visual/sky_fog_end_distance" => { + Some(Self::VisualSkyFogEndDistance) + } + "visual/cloud_fog_end_distance" | "minecraft:visual/cloud_fog_end_distance" => { + Some(Self::VisualCloudFogEndDistance) + } + "visual/water_fog_color" | "minecraft:visual/water_fog_color" => { + Some(Self::VisualWaterFogColor) + } + "visual/water_fog_start_distance" | "minecraft:visual/water_fog_start_distance" => { + Some(Self::VisualWaterFogStartDistance) + } + "visual/water_fog_end_distance" | "minecraft:visual/water_fog_end_distance" => { + Some(Self::VisualWaterFogEndDistance) + } + "visual/sky_color" | "minecraft:visual/sky_color" => Some(Self::VisualSkyColor), + "visual/sunrise_sunset_color" | "minecraft:visual/sunrise_sunset_color" => { + Some(Self::VisualSunriseSunsetColor) + } + "visual/cloud_color" | "minecraft:visual/cloud_color" => Some(Self::VisualCloudColor), + "visual/cloud_height" | "minecraft:visual/cloud_height" => { + Some(Self::VisualCloudHeight) + } + "visual/sun_angle" | "minecraft:visual/sun_angle" => Some(Self::VisualSunAngle), + "visual/moon_angle" | "minecraft:visual/moon_angle" => Some(Self::VisualMoonAngle), + "visual/star_angle" | "minecraft:visual/star_angle" => Some(Self::VisualStarAngle), + "visual/moon_phase" | "minecraft:visual/moon_phase" => Some(Self::VisualMoonPhase), + "visual/star_brightness" | "minecraft:visual/star_brightness" => { + Some(Self::VisualStarBrightness) + } + "visual/block_light_tint" | "minecraft:visual/block_light_tint" => { + Some(Self::VisualBlockLightTint) + } + "visual/sky_light_color" | "minecraft:visual/sky_light_color" => { + Some(Self::VisualSkyLightColor) + } + "visual/sky_light_factor" | "minecraft:visual/sky_light_factor" => { + Some(Self::VisualSkyLightFactor) + } + "visual/night_vision_color" | "minecraft:visual/night_vision_color" => { + Some(Self::VisualNightVisionColor) + } + "visual/ambient_light_color" | "minecraft:visual/ambient_light_color" => { + Some(Self::VisualAmbientLightColor) + } + "visual/default_dripstone_particle" | "minecraft:visual/default_dripstone_particle" => { + Some(Self::VisualDefaultDripstoneParticle) + } + "visual/ambient_particles" | "minecraft:visual/ambient_particles" => { + Some(Self::VisualAmbientParticles) + } + "audio/background_music" | "minecraft:audio/background_music" => { + Some(Self::AudioBackgroundMusic) + } + "audio/music_volume" | "minecraft:audio/music_volume" => Some(Self::AudioMusicVolume), + "audio/ambient_sounds" | "minecraft:audio/ambient_sounds" => { + Some(Self::AudioAmbientSounds) + } + "audio/firefly_bush_sounds" | "minecraft:audio/firefly_bush_sounds" => { + Some(Self::AudioFireflyBushSounds) + } + "gameplay/sky_light_level" | "minecraft:gameplay/sky_light_level" => { + Some(Self::GameplaySkyLightLevel) + } + "gameplay/can_start_raid" | "minecraft:gameplay/can_start_raid" => { + Some(Self::GameplayCanStartRaid) + } + "gameplay/water_evaporates" | "minecraft:gameplay/water_evaporates" => { + Some(Self::GameplayWaterEvaporates) + } + "gameplay/bed_rule" | "minecraft:gameplay/bed_rule" => Some(Self::GameplayBedRule), + "gameplay/respawn_anchor_works" | "minecraft:gameplay/respawn_anchor_works" => { + Some(Self::GameplayRespawnAnchorWorks) + } + "gameplay/nether_portal_spawns_piglin" + | "minecraft:gameplay/nether_portal_spawns_piglin" => { + Some(Self::GameplayNetherPortalSpawnsPiglin) + } + "gameplay/fast_lava" | "minecraft:gameplay/fast_lava" => Some(Self::GameplayFastLava), + "gameplay/increased_fire_burnout" | "minecraft:gameplay/increased_fire_burnout" => { + Some(Self::GameplayIncreasedFireBurnout) + } + "gameplay/eyeblossom_open" | "minecraft:gameplay/eyeblossom_open" => { + Some(Self::GameplayEyeblossomOpen) + } + "gameplay/turtle_egg_hatch_chance" | "minecraft:gameplay/turtle_egg_hatch_chance" => { + Some(Self::GameplayTurtleEggHatchChance) + } + "gameplay/piglins_zombify" | "minecraft:gameplay/piglins_zombify" => { + Some(Self::GameplayPiglinsZombify) + } + "gameplay/snow_golem_melts" | "minecraft:gameplay/snow_golem_melts" => { + Some(Self::GameplaySnowGolemMelts) + } + "gameplay/creaking_active" | "minecraft:gameplay/creaking_active" => { + Some(Self::GameplayCreakingActive) + } + "gameplay/surface_slime_spawn_chance" + | "minecraft:gameplay/surface_slime_spawn_chance" => { + Some(Self::GameplaySurfaceSlimeSpawnChance) + } + "gameplay/cat_waking_up_gift_chance" + | "minecraft:gameplay/cat_waking_up_gift_chance" => { + Some(Self::GameplayCatWakingUpGiftChance) + } + "gameplay/bees_stay_in_hive" | "minecraft:gameplay/bees_stay_in_hive" => { + Some(Self::GameplayBeesStayInHive) + } + "gameplay/monsters_burn" | "minecraft:gameplay/monsters_burn" => { + Some(Self::GameplayMonstersBurn) + } + "gameplay/can_pillager_patrol_spawn" + | "minecraft:gameplay/can_pillager_patrol_spawn" => { + Some(Self::GameplayCanPillagerPatrolSpawn) + } + "gameplay/villager_activity" | "minecraft:gameplay/villager_activity" => { + Some(Self::GameplayVillagerActivity) + } + "gameplay/baby_villager_activity" | "minecraft:gameplay/baby_villager_activity" => { + Some(Self::GameplayBabyVillagerActivity) + } + _ => None, + } + } + #[must_use] + pub const fn from_id(id: u8) -> Option { + match id { + 0u8 => Some(Self::VisualFogColor), + 1u8 => Some(Self::VisualFogStartDistance), + 2u8 => Some(Self::VisualFogEndDistance), + 3u8 => Some(Self::VisualSkyFogEndDistance), + 4u8 => Some(Self::VisualCloudFogEndDistance), + 5u8 => Some(Self::VisualWaterFogColor), + 6u8 => Some(Self::VisualWaterFogStartDistance), + 7u8 => Some(Self::VisualWaterFogEndDistance), + 8u8 => Some(Self::VisualSkyColor), + 9u8 => Some(Self::VisualSunriseSunsetColor), + 10u8 => Some(Self::VisualCloudColor), + 11u8 => Some(Self::VisualCloudHeight), + 12u8 => Some(Self::VisualSunAngle), + 13u8 => Some(Self::VisualMoonAngle), + 14u8 => Some(Self::VisualStarAngle), + 15u8 => Some(Self::VisualMoonPhase), + 16u8 => Some(Self::VisualStarBrightness), + 17u8 => Some(Self::VisualBlockLightTint), + 18u8 => Some(Self::VisualSkyLightColor), + 19u8 => Some(Self::VisualSkyLightFactor), + 20u8 => Some(Self::VisualNightVisionColor), + 21u8 => Some(Self::VisualAmbientLightColor), + 22u8 => Some(Self::VisualDefaultDripstoneParticle), + 23u8 => Some(Self::VisualAmbientParticles), + 24u8 => Some(Self::AudioBackgroundMusic), + 25u8 => Some(Self::AudioMusicVolume), + 26u8 => Some(Self::AudioAmbientSounds), + 27u8 => Some(Self::AudioFireflyBushSounds), + 28u8 => Some(Self::GameplaySkyLightLevel), + 29u8 => Some(Self::GameplayCanStartRaid), + 30u8 => Some(Self::GameplayWaterEvaporates), + 31u8 => Some(Self::GameplayBedRule), + 32u8 => Some(Self::GameplayRespawnAnchorWorks), + 33u8 => Some(Self::GameplayNetherPortalSpawnsPiglin), + 34u8 => Some(Self::GameplayFastLava), + 35u8 => Some(Self::GameplayIncreasedFireBurnout), + 36u8 => Some(Self::GameplayEyeblossomOpen), + 37u8 => Some(Self::GameplayTurtleEggHatchChance), + 38u8 => Some(Self::GameplayPiglinsZombify), + 39u8 => Some(Self::GameplaySnowGolemMelts), + 40u8 => Some(Self::GameplayCreakingActive), + 41u8 => Some(Self::GameplaySurfaceSlimeSpawnChance), + 42u8 => Some(Self::GameplayCatWakingUpGiftChance), + 43u8 => Some(Self::GameplayBeesStayInHive), + 44u8 => Some(Self::GameplayMonstersBurn), + 45u8 => Some(Self::GameplayCanPillagerPatrolSpawn), + 46u8 => Some(Self::GameplayVillagerActivity), + 47u8 => Some(Self::GameplayBabyVillagerActivity), + _ => None, + } + } +} +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct FloatKeyframe { + pub ticks: i32, + pub value: f32, +} +impl FloatKeyframe { + pub const fn new(ticks: i32, value: f32) -> Self { + Self { ticks, value } + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BoolKeyframe { + pub ticks: i32, + pub value: bool, +} +impl BoolKeyframe { + pub const fn new(ticks: i32, value: bool) -> Self { + Self { ticks, value } + } +} +pub struct DayTimeline; +impl DayTimeline { + pub const PERIOD_TICKS: i32 = 24000i32; + pub const SKY_LIGHT_LEVEL_KEYFRAMES: &'static [FloatKeyframe] = &[ + FloatKeyframe::new(133i32, 1f32), + FloatKeyframe::new(11867i32, 1f32), + FloatKeyframe::new(13670i32, 0.26666668f32), + FloatKeyframe::new(22330i32, 0.26666668f32), + ]; + pub const MONSTERS_BURN_KEYFRAMES: &'static [BoolKeyframe] = &[ + BoolKeyframe::new(12542i32, false), + BoolKeyframe::new(23460i32, true), + ]; + pub const BEES_STAY_IN_HIVE_KEYFRAMES: &'static [BoolKeyframe] = &[ + BoolKeyframe::new(12542i32, true), + BoolKeyframe::new(23460i32, false), + ]; + pub const CREAKING_ACTIVE_KEYFRAMES: &'static [BoolKeyframe] = &[ + BoolKeyframe::new(12600i32, true), + BoolKeyframe::new(23401i32, false), + ]; + pub const EYEBLOSSOM_OPEN_KEYFRAMES: &'static [BoolKeyframe] = &[ + BoolKeyframe::new(12600i32, true), + BoolKeyframe::new(23401i32, false), + ]; +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum MoonPhase { + FullMoon, + WaningGibbous, + ThirdQuarter, + WaningCrescent, + NewMoon, + WaxingCrescent, + FirstQuarter, + WaxingGibbous, +} +impl MoonPhase { + #[must_use] + pub const fn index(&self) -> u8 { + match self { + Self::FullMoon => 0, + Self::WaningGibbous => 1, + Self::ThirdQuarter => 2, + Self::WaningCrescent => 3, + Self::NewMoon => 4, + Self::WaxingCrescent => 5, + Self::FirstQuarter => 6, + Self::WaxingGibbous => 7, + } + } + #[must_use] + pub const fn from_index(index: u8) -> Option { + match index { + 0 => Some(Self::FullMoon), + 1 => Some(Self::WaningGibbous), + 2 => Some(Self::ThirdQuarter), + 3 => Some(Self::WaningCrescent), + 4 => Some(Self::NewMoon), + 5 => Some(Self::WaxingCrescent), + 6 => Some(Self::FirstQuarter), + 7 => Some(Self::WaxingGibbous), + _ => None, + } + } + #[must_use] + pub const fn name(&self) -> &'static str { + match self { + Self::FullMoon => "full_moon", + Self::WaningGibbous => "waning_gibbous", + Self::ThirdQuarter => "third_quarter", + Self::WaningCrescent => "waning_crescent", + Self::NewMoon => "new_moon", + Self::WaxingCrescent => "waxing_crescent", + Self::FirstQuarter => "first_quarter", + Self::WaxingGibbous => "waxing_gibbous", + } + } + #[must_use] + pub fn from_name(name: &str) -> Option { + match name { + "full_moon" => Some(Self::FullMoon), + "waning_gibbous" => Some(Self::WaningGibbous), + "third_quarter" => Some(Self::ThirdQuarter), + "waning_crescent" => Some(Self::WaningCrescent), + "new_moon" => Some(Self::NewMoon), + "waxing_crescent" => Some(Self::WaxingCrescent), + "first_quarter" => Some(Self::FirstQuarter), + "waxing_gibbous" => Some(Self::WaxingGibbous), + _ => None, + } + } + #[must_use] + pub const fn start_tick(&self) -> i32 { + self.index() as i32 * 24000 + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MoonPhaseKeyframe { + pub ticks: i32, + pub value: MoonPhase, +} +impl MoonPhaseKeyframe { + pub const fn new(ticks: i32, value: MoonPhase) -> Self { + Self { ticks, value } + } +} +pub struct MoonTimeline; +impl MoonTimeline { + pub const PERIOD_TICKS: i32 = 192000i32; + pub const SURFACE_SLIME_SPAWN_CHANCE_KEYFRAMES: &'static [FloatKeyframe] = &[ + FloatKeyframe::new(0i32, 0.5f32), + FloatKeyframe::new(24000i32, 0.375f32), + FloatKeyframe::new(48000i32, 0.25f32), + FloatKeyframe::new(72000i32, 0.125f32), + FloatKeyframe::new(96000i32, 0f32), + FloatKeyframe::new(120000i32, 0.125f32), + FloatKeyframe::new(144000i32, 0.25f32), + FloatKeyframe::new(168000i32, 0.375f32), + ]; + pub const MOON_PHASE_KEYFRAMES: &'static [MoonPhaseKeyframe] = &[ + MoonPhaseKeyframe::new(0i32, MoonPhase::FullMoon), + MoonPhaseKeyframe::new(24000i32, MoonPhase::WaningGibbous), + MoonPhaseKeyframe::new(48000i32, MoonPhase::ThirdQuarter), + MoonPhaseKeyframe::new(72000i32, MoonPhase::WaningCrescent), + MoonPhaseKeyframe::new(96000i32, MoonPhase::NewMoon), + MoonPhaseKeyframe::new(120000i32, MoonPhase::WaxingCrescent), + MoonPhaseKeyframe::new(144000i32, MoonPhase::FirstQuarter), + MoonPhaseKeyframe::new(168000i32, MoonPhase::WaxingGibbous), + ]; +} +pub struct EarlyGameTimeline; +impl EarlyGameTimeline { + pub const CAN_PILLAGER_PATROL_SPAWN_KEYFRAMES: &'static [BoolKeyframe] = &[ + BoolKeyframe::new(0i32, false), + BoolKeyframe::new(120000i32, true), + ]; +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Activity { + Core, + Idle, + Work, + Play, + Rest, + Meet, + Panic, + Raid, + PreRaid, + Hide, + Fight, + Celebrate, + AdmireItem, + Avoid, + Ride, + PlayDead, + LongJump, + Ram, + Tongue, + Swim, + LaySpawn, + Sniff, + Investigate, + Roar, + Emerge, + Dig, +} +impl Activity { + #[must_use] + pub const fn name(&self) -> &'static str { + match self { + Self::Core => "core", + Self::Idle => "idle", + Self::Work => "work", + Self::Play => "play", + Self::Rest => "rest", + Self::Meet => "meet", + Self::Panic => "panic", + Self::Raid => "raid", + Self::PreRaid => "pre_raid", + Self::Hide => "hide", + Self::Fight => "fight", + Self::Celebrate => "celebrate", + Self::AdmireItem => "admire_item", + Self::Avoid => "avoid", + Self::Ride => "ride", + Self::PlayDead => "play_dead", + Self::LongJump => "long_jump", + Self::Ram => "ram", + Self::Tongue => "tongue", + Self::Swim => "swim", + Self::LaySpawn => "lay_spawn", + Self::Sniff => "sniff", + Self::Investigate => "investigate", + Self::Roar => "roar", + Self::Emerge => "emerge", + Self::Dig => "dig", + } + } + #[must_use] + pub fn from_name(name: &str) -> Option { + let name = name.strip_prefix("minecraft:").unwrap_or(name); + match name { + "core" => Some(Self::Core), + "idle" => Some(Self::Idle), + "work" => Some(Self::Work), + "play" => Some(Self::Play), + "rest" => Some(Self::Rest), + "meet" => Some(Self::Meet), + "panic" => Some(Self::Panic), + "raid" => Some(Self::Raid), + "pre_raid" => Some(Self::PreRaid), + "hide" => Some(Self::Hide), + "fight" => Some(Self::Fight), + "celebrate" => Some(Self::Celebrate), + "admire_item" => Some(Self::AdmireItem), + "avoid" => Some(Self::Avoid), + "ride" => Some(Self::Ride), + "play_dead" => Some(Self::PlayDead), + "long_jump" => Some(Self::LongJump), + "ram" => Some(Self::Ram), + "tongue" => Some(Self::Tongue), + "swim" => Some(Self::Swim), + "lay_spawn" => Some(Self::LaySpawn), + "sniff" => Some(Self::Sniff), + "investigate" => Some(Self::Investigate), + "roar" => Some(Self::Roar), + "emerge" => Some(Self::Emerge), + "dig" => Some(Self::Dig), + _ => None, + } + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ActivityKeyframe { + pub ticks: i32, + pub value: Activity, +} +impl ActivityKeyframe { + pub const fn new(ticks: i32, value: Activity) -> Self { + Self { ticks, value } + } +} +pub struct VillagerScheduleTimeline; +impl VillagerScheduleTimeline { + pub const PERIOD_TICKS: i32 = 24000i32; + pub const VILLAGER_ACTIVITY_KEYFRAMES: &'static [ActivityKeyframe] = &[ + ActivityKeyframe::new(10i32, Activity::Idle), + ActivityKeyframe::new(2000i32, Activity::Work), + ActivityKeyframe::new(9000i32, Activity::Meet), + ActivityKeyframe::new(11000i32, Activity::Idle), + ActivityKeyframe::new(12000i32, Activity::Rest), + ]; + pub const BABY_VILLAGER_ACTIVITY_KEYFRAMES: &'static [ActivityKeyframe] = &[ + ActivityKeyframe::new(10i32, Activity::Idle), + ActivityKeyframe::new(3000i32, Activity::Play), + ActivityKeyframe::new(6000i32, Activity::Idle), + ActivityKeyframe::new(10000i32, Activity::Play), + ActivityKeyframe::new(12000i32, Activity::Rest), + ]; +} +#[doc = r" Samples a cyclic float keyframe track with linear interpolation across `period_ticks`."] +#[must_use] +pub fn sample_float_track(keyframes: &[FloatKeyframe], period_ticks: i32, ticks: i64) -> f32 { + if keyframes.is_empty() { + return 1.0; + } + if keyframes.len() == 1 { + return keyframes[0].value; + } + let t = ticks.rem_euclid(period_ticks as i64) as f32; + let n = keyframes.len(); + let first = &keyframes[0]; + let last = &keyframes[n - 1]; + if t <= first.ticks as f32 { + let seg_len = (first.ticks + period_ticks - last.ticks) as f32; + if seg_len <= 0.0 { + return first.value; + } + let alpha = (t + period_ticks as f32 - last.ticks as f32) / seg_len; + return last.value + alpha * (first.value - last.value); + } + if t >= last.ticks as f32 { + let seg_len = (first.ticks + period_ticks - last.ticks) as f32; + if seg_len <= 0.0 { + return last.value; + } + let alpha = (t - last.ticks as f32) / seg_len; + return last.value + alpha * (first.value - last.value); + } + for i in 0..n - 1 { + let k1 = &keyframes[i]; + let k2 = &keyframes[i + 1]; + if t >= k1.ticks as f32 && t <= k2.ticks as f32 { + let seg_len = (k2.ticks - k1.ticks) as f32; + if seg_len <= 0.0 { + return k1.value; + } + let alpha = (t - k1.ticks as f32) / seg_len; + return k1.value + alpha * (k2.value - k1.value); + } + } + last.value +} +#[doc = r" Samples a cyclic float keyframe track with step interpolation (constant easing) across `period_ticks`."] +#[must_use] +pub fn sample_step_float_track(keyframes: &[FloatKeyframe], period_ticks: i32, ticks: i64) -> f32 { + if keyframes.is_empty() { + return 0.0; + } + let t = ticks.rem_euclid(period_ticks as i64) as i32; + let mut current = keyframes[keyframes.len() - 1].value; + for kf in keyframes { + if t >= kf.ticks { + current = kf.value; + } else { + break; + } + } + current +} +#[doc = r" Samples a cyclic boolean keyframe track (step function) across `period_ticks`."] +#[must_use] +pub fn sample_bool_track(keyframes: &[BoolKeyframe], period_ticks: i32, ticks: i64) -> bool { + if keyframes.is_empty() { + return false; + } + let t = ticks.rem_euclid(period_ticks as i64) as i32; + let mut current = keyframes[keyframes.len() - 1].value; + for kf in keyframes { + if t >= kf.ticks { + current = kf.value; + } else { + break; + } + } + current +} +#[doc = r" Samples an unbounded (non-cyclic) boolean keyframe track (step function)."] +#[must_use] +pub fn sample_unbounded_bool_track(keyframes: &[BoolKeyframe], ticks: i64) -> bool { + if keyframes.is_empty() { + return false; + } + let mut current = keyframes[0].value; + for kf in keyframes { + if ticks >= kf.ticks as i64 { + current = kf.value; + } else { + break; + } + } + current +} +#[doc = r" Samples a cyclic moon phase keyframe track across `period_ticks`."] +#[must_use] +pub fn sample_moon_phase_track( + keyframes: &[MoonPhaseKeyframe], + period_ticks: i32, + ticks: i64, +) -> MoonPhase { + if keyframes.is_empty() { + return MoonPhase::FullMoon; + } + let t = ticks.rem_euclid(period_ticks as i64) as i32; + let mut current = keyframes[keyframes.len() - 1].value; + for kf in keyframes { + if t >= kf.ticks { + current = kf.value; + } else { + break; + } + } + current +} +#[doc = r" Samples a cyclic activity keyframe track across `period_ticks`."] +#[must_use] +pub fn sample_activity_track( + keyframes: &[ActivityKeyframe], + period_ticks: i32, + ticks: i64, +) -> Activity { + if keyframes.is_empty() { + return Activity::Idle; + } + let t = ticks.rem_euclid(period_ticks as i64) as i32; + let mut current = keyframes[keyframes.len() - 1].value; + for kf in keyframes { + if t >= kf.ticks { + current = kf.value; + } else { + break; + } + } + current +} diff --git a/crates/pumpkin-data/src/generated/trial_spawner.rs b/crates/pumpkin-data/src/generated/trial_spawner.rs new file mode 100644 index 000000000..30b405520 --- /dev/null +++ b/crates/pumpkin-data/src/generated/trial_spawner.rs @@ -0,0 +1,855 @@ +/* This file is generated. Do not edit manually. */ +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct TrialSpawnerConfigData { + pub spawn_range: i32, + pub total_mobs: f32, + pub simultaneous_mobs: f32, + pub total_mobs_added_per_player: f32, + pub simultaneous_mobs_added_per_player: f32, + pub ticks_between_spawn: i32, + pub spawn_potentials: &'static [WeightedSpawnData], + pub loot_tables_to_eject: &'static [WeightedLootTable], + pub items_to_drop_when_ominous: Option<&'static str>, +} +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct WeightedSpawnData { + pub entity_id: &'static str, + pub equipment_loot_table: Option<&'static str>, + pub equipment_slot_drop_chances: Option, + pub weight: i32, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WeightedLootTable { + pub loot_table: &'static str, + pub weight: i32, +} +pub const TRIAL_CHAMBER_BREEZE_NORMAL: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 2f32, + simultaneous_mobs: 1f32, + total_mobs_added_per_player: 1f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:breeze", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_BREEZE_OMINOUS: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 4f32, + simultaneous_mobs: 2f32, + total_mobs_added_per_player: 1f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:breeze", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_MELEE_HUSK_NORMAL: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:husk", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_MELEE_HUSK_OMINOUS: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:husk", + equipment_loot_table: Some("minecraft:equipment/trial_chamber_melee"), + equipment_slot_drop_chances: Some(0f32), + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_MELEE_SPIDER_NORMAL: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:spider", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_MELEE_SPIDER_OMINOUS: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 12f32, + simultaneous_mobs: 4f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:spider", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_MELEE_ZOMBIE_NORMAL: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:zombie", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_MELEE_ZOMBIE_OMINOUS: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:zombie", + equipment_loot_table: Some("minecraft:equipment/trial_chamber_melee"), + equipment_slot_drop_chances: Some(0f32), + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_RANGED_POISON_SKELETON_NORMAL: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:bogged", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_RANGED_POISON_SKELETON_OMINOUS: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:bogged", + equipment_loot_table: Some("minecraft:equipment/trial_chamber_ranged"), + equipment_slot_drop_chances: Some(0f32), + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_RANGED_SKELETON_NORMAL: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:skeleton", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_RANGED_SKELETON_OMINOUS: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:skeleton", + equipment_loot_table: Some("minecraft:equipment/trial_chamber_ranged"), + equipment_slot_drop_chances: Some(0f32), + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_RANGED_STRAY_NORMAL: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:stray", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_RANGED_STRAY_OMINOUS: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:stray", + equipment_loot_table: Some("minecraft:equipment/trial_chamber_ranged"), + equipment_slot_drop_chances: Some(0f32), + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_SLOW_RANGED_POISON_SKELETON_NORMAL: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 4f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 2f32, + ticks_between_spawn: 160i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:bogged", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_SLOW_RANGED_POISON_SKELETON_OMINOUS: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 4f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 2f32, + ticks_between_spawn: 160i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:bogged", + equipment_loot_table: Some("minecraft:equipment/trial_chamber_ranged"), + equipment_slot_drop_chances: Some(0f32), + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_SLOW_RANGED_SKELETON_NORMAL: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 4f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 2f32, + ticks_between_spawn: 160i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:skeleton", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_SLOW_RANGED_SKELETON_OMINOUS: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 4f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 2f32, + ticks_between_spawn: 160i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:skeleton", + equipment_loot_table: Some("minecraft:equipment/trial_chamber_ranged"), + equipment_slot_drop_chances: Some(0f32), + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_SLOW_RANGED_STRAY_NORMAL: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 4f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 2f32, + ticks_between_spawn: 160i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:stray", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_SLOW_RANGED_STRAY_OMINOUS: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 4f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 2f32, + ticks_between_spawn: 160i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:stray", + equipment_loot_table: Some("minecraft:equipment/trial_chamber_ranged"), + equipment_slot_drop_chances: Some(0f32), + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_SMALL_MELEE_BABY_ZOMBIE_NORMAL: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 2f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:zombie", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_SMALL_MELEE_BABY_ZOMBIE_OMINOUS: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 2f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:zombie", + equipment_loot_table: Some("minecraft:equipment/trial_chamber_melee"), + equipment_slot_drop_chances: Some(0f32), + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_SMALL_MELEE_CAVE_SPIDER_NORMAL: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:cave_spider", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_SMALL_MELEE_CAVE_SPIDER_OMINOUS: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 12f32, + simultaneous_mobs: 4f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:cave_spider", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_SMALL_MELEE_SILVERFISH_NORMAL: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:silverfish", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_SMALL_MELEE_SILVERFISH_OMINOUS: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 12f32, + simultaneous_mobs: 4f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[WeightedSpawnData { + entity_id: "minecraft:silverfish", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, + }; +pub const TRIAL_CHAMBER_SMALL_MELEE_SLIME_NORMAL: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 6f32, + simultaneous_mobs: 3f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[ + WeightedSpawnData { + entity_id: "minecraft:slime", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 3i32, + }, + WeightedSpawnData { + entity_id: "minecraft:slime", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }, + ], + loot_tables_to_eject: &[], + items_to_drop_when_ominous: None, +}; +pub const TRIAL_CHAMBER_SMALL_MELEE_SLIME_OMINOUS: TrialSpawnerConfigData = + TrialSpawnerConfigData { + spawn_range: 4i32, + total_mobs: 12f32, + simultaneous_mobs: 4f32, + total_mobs_added_per_player: 2f32, + simultaneous_mobs_added_per_player: 0.5f32, + ticks_between_spawn: 20i32, + spawn_potentials: &[ + WeightedSpawnData { + entity_id: "minecraft:slime", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 3i32, + }, + WeightedSpawnData { + entity_id: "minecraft:slime", + equipment_loot_table: None, + equipment_slot_drop_chances: None, + weight: 1i32, + }, + ], + loot_tables_to_eject: &[ + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/key", + weight: 3i32, + }, + WeightedLootTable { + loot_table: "minecraft:spawners/ominous/trial_chamber/consumables", + weight: 7i32, + }, + ], + items_to_drop_when_ominous: None, + }; +#[must_use] +pub fn get_trial_spawner_config(key: &str) -> Option<&'static TrialSpawnerConfigData> { + match key { + "trial_chamber/breeze/normal" | "minecraft:trial_chamber/breeze/normal" => { + Some(&TRIAL_CHAMBER_BREEZE_NORMAL) + } + "trial_chamber/breeze/ominous" | "minecraft:trial_chamber/breeze/ominous" => { + Some(&TRIAL_CHAMBER_BREEZE_OMINOUS) + } + "trial_chamber/melee/husk/normal" | "minecraft:trial_chamber/melee/husk/normal" => { + Some(&TRIAL_CHAMBER_MELEE_HUSK_NORMAL) + } + "trial_chamber/melee/husk/ominous" | "minecraft:trial_chamber/melee/husk/ominous" => { + Some(&TRIAL_CHAMBER_MELEE_HUSK_OMINOUS) + } + "trial_chamber/melee/spider/normal" | "minecraft:trial_chamber/melee/spider/normal" => { + Some(&TRIAL_CHAMBER_MELEE_SPIDER_NORMAL) + } + "trial_chamber/melee/spider/ominous" | "minecraft:trial_chamber/melee/spider/ominous" => { + Some(&TRIAL_CHAMBER_MELEE_SPIDER_OMINOUS) + } + "trial_chamber/melee/zombie/normal" | "minecraft:trial_chamber/melee/zombie/normal" => { + Some(&TRIAL_CHAMBER_MELEE_ZOMBIE_NORMAL) + } + "trial_chamber/melee/zombie/ominous" | "minecraft:trial_chamber/melee/zombie/ominous" => { + Some(&TRIAL_CHAMBER_MELEE_ZOMBIE_OMINOUS) + } + "trial_chamber/ranged/poison_skeleton/normal" + | "minecraft:trial_chamber/ranged/poison_skeleton/normal" => { + Some(&TRIAL_CHAMBER_RANGED_POISON_SKELETON_NORMAL) + } + "trial_chamber/ranged/poison_skeleton/ominous" + | "minecraft:trial_chamber/ranged/poison_skeleton/ominous" => { + Some(&TRIAL_CHAMBER_RANGED_POISON_SKELETON_OMINOUS) + } + "trial_chamber/ranged/skeleton/normal" + | "minecraft:trial_chamber/ranged/skeleton/normal" => { + Some(&TRIAL_CHAMBER_RANGED_SKELETON_NORMAL) + } + "trial_chamber/ranged/skeleton/ominous" + | "minecraft:trial_chamber/ranged/skeleton/ominous" => { + Some(&TRIAL_CHAMBER_RANGED_SKELETON_OMINOUS) + } + "trial_chamber/ranged/stray/normal" | "minecraft:trial_chamber/ranged/stray/normal" => { + Some(&TRIAL_CHAMBER_RANGED_STRAY_NORMAL) + } + "trial_chamber/ranged/stray/ominous" | "minecraft:trial_chamber/ranged/stray/ominous" => { + Some(&TRIAL_CHAMBER_RANGED_STRAY_OMINOUS) + } + "trial_chamber/slow_ranged/poison_skeleton/normal" + | "minecraft:trial_chamber/slow_ranged/poison_skeleton/normal" => { + Some(&TRIAL_CHAMBER_SLOW_RANGED_POISON_SKELETON_NORMAL) + } + "trial_chamber/slow_ranged/poison_skeleton/ominous" + | "minecraft:trial_chamber/slow_ranged/poison_skeleton/ominous" => { + Some(&TRIAL_CHAMBER_SLOW_RANGED_POISON_SKELETON_OMINOUS) + } + "trial_chamber/slow_ranged/skeleton/normal" + | "minecraft:trial_chamber/slow_ranged/skeleton/normal" => { + Some(&TRIAL_CHAMBER_SLOW_RANGED_SKELETON_NORMAL) + } + "trial_chamber/slow_ranged/skeleton/ominous" + | "minecraft:trial_chamber/slow_ranged/skeleton/ominous" => { + Some(&TRIAL_CHAMBER_SLOW_RANGED_SKELETON_OMINOUS) + } + "trial_chamber/slow_ranged/stray/normal" + | "minecraft:trial_chamber/slow_ranged/stray/normal" => { + Some(&TRIAL_CHAMBER_SLOW_RANGED_STRAY_NORMAL) + } + "trial_chamber/slow_ranged/stray/ominous" + | "minecraft:trial_chamber/slow_ranged/stray/ominous" => { + Some(&TRIAL_CHAMBER_SLOW_RANGED_STRAY_OMINOUS) + } + "trial_chamber/small_melee/baby_zombie/normal" + | "minecraft:trial_chamber/small_melee/baby_zombie/normal" => { + Some(&TRIAL_CHAMBER_SMALL_MELEE_BABY_ZOMBIE_NORMAL) + } + "trial_chamber/small_melee/baby_zombie/ominous" + | "minecraft:trial_chamber/small_melee/baby_zombie/ominous" => { + Some(&TRIAL_CHAMBER_SMALL_MELEE_BABY_ZOMBIE_OMINOUS) + } + "trial_chamber/small_melee/cave_spider/normal" + | "minecraft:trial_chamber/small_melee/cave_spider/normal" => { + Some(&TRIAL_CHAMBER_SMALL_MELEE_CAVE_SPIDER_NORMAL) + } + "trial_chamber/small_melee/cave_spider/ominous" + | "minecraft:trial_chamber/small_melee/cave_spider/ominous" => { + Some(&TRIAL_CHAMBER_SMALL_MELEE_CAVE_SPIDER_OMINOUS) + } + "trial_chamber/small_melee/silverfish/normal" + | "minecraft:trial_chamber/small_melee/silverfish/normal" => { + Some(&TRIAL_CHAMBER_SMALL_MELEE_SILVERFISH_NORMAL) + } + "trial_chamber/small_melee/silverfish/ominous" + | "minecraft:trial_chamber/small_melee/silverfish/ominous" => { + Some(&TRIAL_CHAMBER_SMALL_MELEE_SILVERFISH_OMINOUS) + } + "trial_chamber/small_melee/slime/normal" + | "minecraft:trial_chamber/small_melee/slime/normal" => { + Some(&TRIAL_CHAMBER_SMALL_MELEE_SLIME_NORMAL) + } + "trial_chamber/small_melee/slime/ominous" + | "minecraft:trial_chamber/small_melee/slime/ominous" => { + Some(&TRIAL_CHAMBER_SMALL_MELEE_SLIME_OMINOUS) + } + _ => None, + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum TrialSpawnerKey { + Breeze, + MeleeHusk, + MeleeSpider, + MeleeZombie, + RangedPoisonSkeleton, + RangedSkeleton, + RangedStray, + SlowRangedPoisonSkeleton, + SlowRangedSkeleton, + SlowRangedStray, + SmallMeleeBabyZombie, + SmallMeleeCaveSpider, + SmallMeleeSilverfish, + SmallMeleeSlime, +} +impl TrialSpawnerKey { + #[must_use] + pub const fn name(&self) -> &'static str { + match self { + Self::Breeze => "trial_chamber/breeze", + Self::MeleeHusk => "trial_chamber/melee/husk", + Self::MeleeSpider => "trial_chamber/melee/spider", + Self::MeleeZombie => "trial_chamber/melee/zombie", + Self::RangedPoisonSkeleton => "trial_chamber/ranged/poison_skeleton", + Self::RangedSkeleton => "trial_chamber/ranged/skeleton", + Self::RangedStray => "trial_chamber/ranged/stray", + Self::SlowRangedPoisonSkeleton => "trial_chamber/slow_ranged/poison_skeleton", + Self::SlowRangedSkeleton => "trial_chamber/slow_ranged/skeleton", + Self::SlowRangedStray => "trial_chamber/slow_ranged/stray", + Self::SmallMeleeBabyZombie => "trial_chamber/small_melee/baby_zombie", + Self::SmallMeleeCaveSpider => "trial_chamber/small_melee/cave_spider", + Self::SmallMeleeSilverfish => "trial_chamber/small_melee/silverfish", + Self::SmallMeleeSlime => "trial_chamber/small_melee/slime", + } + } + #[must_use] + pub const fn normal_config(&self) -> &'static TrialSpawnerConfigData { + match self { + Self::Breeze => &TRIAL_CHAMBER_BREEZE_NORMAL, + Self::MeleeHusk => &TRIAL_CHAMBER_MELEE_HUSK_NORMAL, + Self::MeleeSpider => &TRIAL_CHAMBER_MELEE_SPIDER_NORMAL, + Self::MeleeZombie => &TRIAL_CHAMBER_MELEE_ZOMBIE_NORMAL, + Self::RangedPoisonSkeleton => &TRIAL_CHAMBER_RANGED_POISON_SKELETON_NORMAL, + Self::RangedSkeleton => &TRIAL_CHAMBER_RANGED_SKELETON_NORMAL, + Self::RangedStray => &TRIAL_CHAMBER_RANGED_STRAY_NORMAL, + Self::SlowRangedPoisonSkeleton => &TRIAL_CHAMBER_SLOW_RANGED_POISON_SKELETON_NORMAL, + Self::SlowRangedSkeleton => &TRIAL_CHAMBER_SLOW_RANGED_SKELETON_NORMAL, + Self::SlowRangedStray => &TRIAL_CHAMBER_SLOW_RANGED_STRAY_NORMAL, + Self::SmallMeleeBabyZombie => &TRIAL_CHAMBER_SMALL_MELEE_BABY_ZOMBIE_NORMAL, + Self::SmallMeleeCaveSpider => &TRIAL_CHAMBER_SMALL_MELEE_CAVE_SPIDER_NORMAL, + Self::SmallMeleeSilverfish => &TRIAL_CHAMBER_SMALL_MELEE_SILVERFISH_NORMAL, + Self::SmallMeleeSlime => &TRIAL_CHAMBER_SMALL_MELEE_SLIME_NORMAL, + } + } + #[must_use] + pub const fn ominous_config(&self) -> &'static TrialSpawnerConfigData { + match self { + Self::Breeze => &TRIAL_CHAMBER_BREEZE_OMINOUS, + Self::MeleeHusk => &TRIAL_CHAMBER_MELEE_HUSK_OMINOUS, + Self::MeleeSpider => &TRIAL_CHAMBER_MELEE_SPIDER_OMINOUS, + Self::MeleeZombie => &TRIAL_CHAMBER_MELEE_ZOMBIE_OMINOUS, + Self::RangedPoisonSkeleton => &TRIAL_CHAMBER_RANGED_POISON_SKELETON_OMINOUS, + Self::RangedSkeleton => &TRIAL_CHAMBER_RANGED_SKELETON_OMINOUS, + Self::RangedStray => &TRIAL_CHAMBER_RANGED_STRAY_OMINOUS, + Self::SlowRangedPoisonSkeleton => &TRIAL_CHAMBER_SLOW_RANGED_POISON_SKELETON_OMINOUS, + Self::SlowRangedSkeleton => &TRIAL_CHAMBER_SLOW_RANGED_SKELETON_OMINOUS, + Self::SlowRangedStray => &TRIAL_CHAMBER_SLOW_RANGED_STRAY_OMINOUS, + Self::SmallMeleeBabyZombie => &TRIAL_CHAMBER_SMALL_MELEE_BABY_ZOMBIE_OMINOUS, + Self::SmallMeleeCaveSpider => &TRIAL_CHAMBER_SMALL_MELEE_CAVE_SPIDER_OMINOUS, + Self::SmallMeleeSilverfish => &TRIAL_CHAMBER_SMALL_MELEE_SILVERFISH_OMINOUS, + Self::SmallMeleeSlime => &TRIAL_CHAMBER_SMALL_MELEE_SLIME_OMINOUS, + } + } + #[must_use] + pub fn from_name(name: &str) -> Option { + match name { + "trial_chamber/breeze" | "minecraft:trial_chamber/breeze" => Some(Self::Breeze), + "trial_chamber/melee/husk" | "minecraft:trial_chamber/melee/husk" => { + Some(Self::MeleeHusk) + } + "trial_chamber/melee/spider" | "minecraft:trial_chamber/melee/spider" => { + Some(Self::MeleeSpider) + } + "trial_chamber/melee/zombie" | "minecraft:trial_chamber/melee/zombie" => { + Some(Self::MeleeZombie) + } + "trial_chamber/ranged/poison_skeleton" + | "minecraft:trial_chamber/ranged/poison_skeleton" => Some(Self::RangedPoisonSkeleton), + "trial_chamber/ranged/skeleton" | "minecraft:trial_chamber/ranged/skeleton" => { + Some(Self::RangedSkeleton) + } + "trial_chamber/ranged/stray" | "minecraft:trial_chamber/ranged/stray" => { + Some(Self::RangedStray) + } + "trial_chamber/slow_ranged/poison_skeleton" + | "minecraft:trial_chamber/slow_ranged/poison_skeleton" => { + Some(Self::SlowRangedPoisonSkeleton) + } + "trial_chamber/slow_ranged/skeleton" + | "minecraft:trial_chamber/slow_ranged/skeleton" => Some(Self::SlowRangedSkeleton), + "trial_chamber/slow_ranged/stray" | "minecraft:trial_chamber/slow_ranged/stray" => { + Some(Self::SlowRangedStray) + } + "trial_chamber/small_melee/baby_zombie" + | "minecraft:trial_chamber/small_melee/baby_zombie" => Some(Self::SmallMeleeBabyZombie), + "trial_chamber/small_melee/cave_spider" + | "minecraft:trial_chamber/small_melee/cave_spider" => Some(Self::SmallMeleeCaveSpider), + "trial_chamber/small_melee/silverfish" + | "minecraft:trial_chamber/small_melee/silverfish" => Some(Self::SmallMeleeSilverfish), + "trial_chamber/small_melee/slime" | "minecraft:trial_chamber/small_melee/slime" => { + Some(Self::SmallMeleeSlime) + } + _ => None, + } + } +} diff --git a/crates/pumpkin-data/src/lib.rs b/crates/pumpkin-data/src/lib.rs index c8320a188..02eea4547 100644 --- a/crates/pumpkin-data/src/lib.rs +++ b/crates/pumpkin-data/src/lib.rs @@ -179,6 +179,14 @@ mod spawn_egg; #[path = "generated/dimension.rs"] pub mod dimension; +#[cfg(feature = "environment_attribute")] +#[rustfmt::skip] +#[path = "generated/environment_attribute.rs"] +pub mod environment_attribute; + +#[cfg(feature = "environment_attribute")] +pub use environment_attribute::*; + #[cfg(feature = "enchantment")] #[rustfmt::skip] #[path = "generated/enchantment.rs"] @@ -456,3 +464,8 @@ pub mod dye_color; #[rustfmt::skip] #[path = "generated/block_transformer.rs"] pub mod block_transformer; + +#[cfg(feature = "trial_spawner")] +#[rustfmt::skip] +#[path = "generated/trial_spawner.rs"] +pub mod trial_spawner; diff --git a/crates/pumpkin-plugin-wit b/crates/pumpkin-plugin-wit index f481f5677..1ad73fff1 160000 --- a/crates/pumpkin-plugin-wit +++ b/crates/pumpkin-plugin-wit @@ -1 +1 @@ -Subproject commit f481f5677dd0c36788288378b7b1f650ed2266e9 +Subproject commit 1ad73fff1e0a9e21b99255816df5f99f6260c1b9 diff --git a/crates/pumpkin-world/benches/chunk_gen.rs b/crates/pumpkin-world/benches/chunk_gen.rs index 999074271..d55d951f9 100644 --- a/crates/pumpkin-world/benches/chunk_gen.rs +++ b/crates/pumpkin-world/benches/chunk_gen.rs @@ -55,8 +55,8 @@ impl WorldPortalExt for BlockRegistry { } } -fn make_world_gen() -> Box { - get_world_gen(SEED, Dimension::OVERWORLD, false, Vec::new(), String::new()) +fn make_world_gen() -> WorldGenerator { + *get_world_gen(SEED, Dimension::OVERWORLD, false, Vec::new(), String::new()) } fn setup_cache( diff --git a/crates/pumpkin-world/src/generation/generator/biome_finder.rs b/crates/pumpkin-world/src/generation/generator/biome_finder.rs index 858715d26..350346abf 100644 --- a/crates/pumpkin-world/src/generation/generator/biome_finder.rs +++ b/crates/pumpkin-world/src/generation/generator/biome_finder.rs @@ -259,12 +259,12 @@ mod test { #[test] fn flat_world_has_a_single_fixed_biome() { - let world_gen = WorldGenerator::Flat(FlatGenerator::new( + let world_gen = WorldGenerator::Flat(Box::new(FlatGenerator::new( Seed(0), Dimension::OVERWORLD, Vec::new(), "minecraft:plains".to_string(), - )); + ))); let origin = BlockPos::new(17, 64, -3); let (pos, biome) = find_closest_biome_3d( diff --git a/crates/pumpkin-world/src/generation/generator/mod.rs b/crates/pumpkin-world/src/generation/generator/mod.rs index a9e8e3038..1b7cd29c5 100644 --- a/crates/pumpkin-world/src/generation/generator/mod.rs +++ b/crates/pumpkin-world/src/generation/generator/mod.rs @@ -79,7 +79,7 @@ pub trait CustomChunkGenerator: Send + Sync { pub enum WorldGenerator { Noise(Box), - Flat(flat::FlatGenerator), + Flat(Box), Custom(Arc), } diff --git a/crates/pumpkin-world/src/generation/height_provider.rs b/crates/pumpkin-world/src/generation/height_provider.rs index f2c0183f2..765321768 100644 --- a/crates/pumpkin-world/src/generation/height_provider.rs +++ b/crates/pumpkin-world/src/generation/height_provider.rs @@ -56,7 +56,7 @@ impl UniformHeightProvider { let max = self.max_inclusive.get_y(min_y as i16, height); if min > max { - warn!("Empty height range: min={min}, max={max}"); + // TODO: investigate why min > max occurs (e.g. min=136, max=127) return min; } diff --git a/crates/pumpkin-world/src/generation/mod.rs b/crates/pumpkin-world/src/generation/mod.rs index 95d86d8cf..cdb201f41 100644 --- a/crates/pumpkin-world/src/generation/mod.rs +++ b/crates/pumpkin-world/src/generation/mod.rs @@ -34,9 +34,9 @@ pub fn get_world_gen( flat_biome: String, ) -> Box { if is_flat { - Box::new(generator::WorldGenerator::Flat( + Box::new(generator::WorldGenerator::Flat(Box::new( generator::flat::FlatGenerator::new(seed, dimension, flat_layers, flat_biome), - )) + ))) } else { Box::new(generator::WorldGenerator::Noise(Box::new( VanillaGenerator::new(seed, dimension), diff --git a/crates/pumpkin/src/block/blocks/bed.rs b/crates/pumpkin/src/block/blocks/bed.rs index df46b1f4b..a003b934a 100644 --- a/crates/pumpkin/src/block/blocks/bed.rs +++ b/crates/pumpkin/src/block/blocks/bed.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use crate::block::entities::bed::BedBlockEntity; use pumpkin_data::block_properties::BedPart; -use pumpkin_data::dimension::Dimension; use pumpkin_data::entity::EntityType; use pumpkin_data::translation; use pumpkin_data::{Block, BlockState, BlockStateId}; @@ -213,8 +212,8 @@ impl BedBlock { (position.offset(bed_props.facing.to_offset()), *position) }; - // Explode if not in the overworld - if world.dimension != Dimension::OVERWORLD { + // Explode if bed rule explodes (EnvironmentAttributes.BED_RULE) + if world.dimension.bed_rule.explodes { world.break_block(&bed_head_pos, None, BlockFlags::SKIP_DROPS); world.break_block(&bed_foot_pos, None, BlockFlags::SKIP_DROPS); @@ -227,6 +226,21 @@ impl BedBlock { return BlockActionResult::SuccessServer; } + let is_dark = world.is_dark_outside(); + let can_sleep = world.dimension.bed_rule.can_sleep(is_dark); + let can_set_spawn = world.dimension.bed_rule.can_set_spawn(is_dark); + + if !can_set_spawn && !can_sleep { + player.send_system_message_raw( + &pumpkin_macros::translate_cross!( + translation::java::BLOCK_MINECRAFT_BED_NO_SLEEP, + translation::bedrock::TILE_BED_NOSLEEP + ), + true, + ); + return BlockActionResult::SuccessServer; + } + // Make sure the bed is not obstructed if world.get_block_state(&bed_head_pos.up()).is_solid() || world.get_block_state(&bed_foot_pos.up()).is_solid() @@ -274,13 +288,15 @@ impl BedBlock { } // Set respawn point - if player.set_respawn_point( - world.dimension.clone(), - bed_head_pos, - player.get_entity().yaw.load(), - player.get_entity().pitch.load(), - false, - ) { + if can_set_spawn + && player.set_respawn_point( + world.dimension.clone(), + bed_head_pos, + player.get_entity().yaw.load(), + player.get_entity().pitch.load(), + false, + ) + { player.send_system_message(&pumpkin_macros::translate_cross!( translation::java::BLOCK_MINECRAFT_SET_SPAWN, translation::bedrock::TILE_BED_RESPAWNSET @@ -288,7 +304,7 @@ impl BedBlock { } // Make sure the time and weather allows sleep - if !can_sleep(world) { + if !can_sleep { player.send_system_message_raw( &pumpkin_macros::translate_cross!( translation::java::BLOCK_MINECRAFT_BED_NO_SLEEP, @@ -381,25 +397,6 @@ impl BedBlock { } } -fn can_sleep(world: &Arc) -> bool { - let time = world - .level_time - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - let weather = world - .weather - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - - if weather.thundering { - true - } else if weather.raining { - time.time_of_day > 12010 && time.time_of_day < 23991 - } else { - time.time_of_day > 12542 && time.time_of_day < 23459 - } -} - fn entity_prevents_sleep(entity: &Entity) -> bool { NO_SLEEP_IDS.contains(&entity.entity_type.id) } diff --git a/crates/pumpkin/src/block/blocks/flower_pots.rs b/crates/pumpkin/src/block/blocks/flower_pots.rs index 290f8e61b..82dac6c22 100644 --- a/crates/pumpkin/src/block/blocks/flower_pots.rs +++ b/crates/pumpkin/src/block/blocks/flower_pots.rs @@ -1,6 +1,5 @@ use crate::block::registry::BlockActionResult; use crate::block::{BlockBehaviour, PathComputationType, RandomTickArgs, UseWithItemArgs}; -use pumpkin_data::dimension::Dimension; use pumpkin_data::flower_pot_transformations::get_potted_item; use pumpkin_data::{Block, BlockId, BlockState}; use pumpkin_macros::pumpkin_block_from_tag; @@ -45,23 +44,24 @@ impl BlockBehaviour for FlowerPotBlock { } fn random_tick(&self, args: RandomTickArgs<'_>) { - if (args.world.dimension.eq(&Dimension::OVERWORLD) - || args.world.dimension.eq(&Dimension::OVERWORLD_CAVES)) - && args.block.eq(&Block::POTTED_CLOSED_EYEBLOSSOM) - && args.world.get_time_of_day() % 24000 > 14500 - { - args.world.set_block_state( - args.position, - Block::POTTED_OPEN_EYEBLOSSOM.default_state.id, - BlockFlags::NOTIFY_ALL, - ); + let is_open_potted = args.block.eq(&Block::POTTED_OPEN_EYEBLOSSOM); + let is_closed_potted = args.block.eq(&Block::POTTED_CLOSED_EYEBLOSSOM); + if !is_open_potted && !is_closed_potted { + return; } - if args.block.eq(&Block::POTTED_OPEN_EYEBLOSSOM) - && args.world.get_time_of_day() % 24000 <= 14500 - { + + let is_open = is_open_potted; + let should_be_open = args.world.eyeblossom_open(args.position).unwrap_or(is_open); + + if is_open != should_be_open { + let next_block = if should_be_open { + &Block::POTTED_OPEN_EYEBLOSSOM + } else { + &Block::POTTED_CLOSED_EYEBLOSSOM + }; args.world.set_block_state( args.position, - Block::POTTED_CLOSED_EYEBLOSSOM.default_state.id, + next_block.default_state.id, BlockFlags::NOTIFY_ALL, ); } diff --git a/crates/pumpkin/src/block/blocks/ice.rs b/crates/pumpkin/src/block/blocks/ice.rs index e100001a6..6f96af65f 100644 --- a/crates/pumpkin/src/block/blocks/ice.rs +++ b/crates/pumpkin/src/block/blocks/ice.rs @@ -14,9 +14,9 @@ use crate::block::{ }; use crate::world::World; -/// Melts ice at the given position into water (or removes it in ultrawarm dimensions like the Nether). +/// Melts ice at the given position into water (or removes it where water evaporates). pub fn melt(world: &Arc, position: &BlockPos) { - if world.dimension == Dimension::THE_NETHER { + if world.dimension.water_evaporates { world.set_block_state(position, BlockStateId::AIR, BlockFlags::NOTIFY_ALL); } else { world.set_block_state( @@ -68,7 +68,7 @@ impl BlockBehaviour for IceBlock { let held_item = args.player.inventory().held_item(); let has_silk_touch = held_item.get_enchantment_level(&Enchantment::SILK_TOUCH) > 0; if !has_silk_touch { - if args.world.dimension == Dimension::THE_NETHER { + if args.world.dimension.water_evaporates { args.world.set_block_state( args.position, BlockStateId::AIR, diff --git a/crates/pumpkin/src/block/blocks/nether_portal.rs b/crates/pumpkin/src/block/blocks/nether_portal.rs index 969855df7..7bb760682 100644 --- a/crates/pumpkin/src/block/blocks/nether_portal.rs +++ b/crates/pumpkin/src/block/blocks/nether_portal.rs @@ -73,8 +73,7 @@ impl BlockBehaviour for NetherPortalBlock { let difficulty = level_info.difficulty; if !level_info.game_rules.spawn_mobs || difficulty == Difficulty::Peaceful - || (args.world.dimension != Dimension::OVERWORLD - && args.world.dimension != Dimension::OVERWORLD_CAVES) + || !args.world.dimension.nether_portal_spawns_piglin { return; } diff --git a/crates/pumpkin/src/block/blocks/plant/eyeblossom.rs b/crates/pumpkin/src/block/blocks/plant/eyeblossom.rs index 51241f9ac..87e6c5daa 100644 --- a/crates/pumpkin/src/block/blocks/plant/eyeblossom.rs +++ b/crates/pumpkin/src/block/blocks/plant/eyeblossom.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use pumpkin_data::{ Block, BlockId, BlockStateId, - dimension::Dimension, effect::StatusEffect, entity::EntityType, particle::Particle, @@ -117,17 +116,7 @@ impl PlantBlockBase for EyeblossomBlock {} pub fn try_changing_state(world: &Arc, current_block: &Block, pos: &BlockPos) -> bool { let is_open = current_block == &Block::OPEN_EYEBLOSSOM; - let should_be_open = if world.dimension == Dimension::OVERWORLD - || world.dimension == Dimension::OVERWORLD_CAVES - { - world - .level_time - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .is_night() - } else { - is_open - }; + let should_be_open = world.eyeblossom_open(pos).unwrap_or(is_open); if should_be_open == is_open { return false; diff --git a/crates/pumpkin/src/block/blocks/redstone/daylight_detector.rs b/crates/pumpkin/src/block/blocks/redstone/daylight_detector.rs index 2585cf9a2..c4212dd85 100644 --- a/crates/pumpkin/src/block/blocks/redstone/daylight_detector.rs +++ b/crates/pumpkin/src/block/blocks/redstone/daylight_detector.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use crate::block::entities::daylight_detector::DaylightDetectorBlockEntity; -use pumpkin_data::Block; +use pumpkin_data::game_event::GameEvent; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; use pumpkin_world::world::BlockFlags; @@ -38,11 +38,18 @@ impl BlockBehaviour for DaylightDetectorBlock { } let state = args.world.get_block_state(args.position); - let props = DaylightDetectorProperties::from_state_id(state.id); + let mut props = DaylightDetectorProperties::from_state_id(state.id); + props.inverted = !props.inverted; - Self::update_inverted(props, args.world, args.position, args.block); + let new_state = props.to_state_id(args.block); + args.world + .set_block_state(args.position, new_state, BlockFlags::NOTIFY_LISTENERS); + args.world.emit_game_event( + GameEvent::BlockChange.name(), + args.position.to_centered_f64(), + ); - DaylightDetectorBlockEntity::update_power(args.world, args.position); + Self::update_signal_strength(args.world, args.position); BlockActionResult::Success } @@ -58,16 +65,43 @@ impl BlockBehaviour for DaylightDetectorBlock { } impl DaylightDetectorBlock { - fn update_inverted( - mut props: DaylightDetectorProperties, - world: &Arc, - block_pos: &BlockPos, - block: &Block, - ) { - props.inverted = !props.inverted; + #[must_use] + pub fn calculate_signal_strength( + effective_sky_brightness: i32, + sun_angle_radians: f32, + is_inverted: bool, + ) -> u8 { + let mut target = effective_sky_brightness; + let mut sun_angle = sun_angle_radians; + if is_inverted { + target = 15 - target; + } else if target > 0 { + let offset = if sun_angle < std::f32::consts::PI { + 0.0 + } else { + std::f32::consts::PI * 2.0 + }; + sun_angle += (offset - sun_angle) * 0.2; + target = ((target as f32 * sun_angle.cos()) + 0.5).floor() as i32; + } - let state = props.to_state_id(block); + target.clamp(0, 15) as u8 + } - world.set_block_state(block_pos, state, BlockFlags::NOTIFY_LISTENERS); + pub fn update_signal_strength(world: &Arc, block_pos: &BlockPos) { + let (block, state) = world.get_block_and_state(block_pos); + let mut props = DaylightDetectorProperties::from_state_id(state.id); + + let target = Self::calculate_signal_strength( + world.get_effective_sky_brightness(block_pos), + world.get_sun_angle(block_pos), + props.inverted, + ); + + if props.power != target { + props.power = target; + let new_state = props.to_state_id(block); + world.set_block_state(block_pos, new_state, BlockFlags::NOTIFY_ALL); + } } } diff --git a/crates/pumpkin/src/block/blocks/respawn_anchor.rs b/crates/pumpkin/src/block/blocks/respawn_anchor.rs index 2189c767f..3d89936b1 100644 --- a/crates/pumpkin/src/block/blocks/respawn_anchor.rs +++ b/crates/pumpkin/src/block/blocks/respawn_anchor.rs @@ -1,5 +1,4 @@ use pumpkin_data::block_properties::RespawnAnchorLikeProperties; -use pumpkin_data::dimension::Dimension; use pumpkin_data::item::Item; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::{BlockState, translation}; @@ -49,7 +48,7 @@ impl BlockBehaviour for RespawnAnchorBlock { let state_id = args.world.get_block_state_id(args.position); let props = RespawnAnchorLikeProperties::from_state_id(state_id); - if args.world.dimension != Dimension::THE_NETHER { + if !args.world.dimension.respawn_anchor_works { args.world .break_block(args.position, None, BlockFlags::SKIP_DROPS); let center_pos = args.position.to_centered_f64(); diff --git a/crates/pumpkin/src/block/blocks/sponge.rs b/crates/pumpkin/src/block/blocks/sponge.rs index 8d7f6012a..ec6b1c409 100644 --- a/crates/pumpkin/src/block/blocks/sponge.rs +++ b/crates/pumpkin/src/block/blocks/sponge.rs @@ -4,7 +4,6 @@ use std::collections::{HashSet, VecDeque}; use std::sync::Arc; use crate::block::{BlockBehaviour, OnNeighborUpdateArgs, PlacedArgs}; -use pumpkin_data::dimension::Dimension; use pumpkin_data::particle::Particle; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::{Block, BlockStateId}; @@ -112,8 +111,8 @@ pub struct WetSpongeBlock; impl BlockBehaviour for WetSpongeBlock { fn placed(&self, args: PlacedArgs<'_>) { { - // Check if placed in Nether, if so, dry out - if args.world.dimension == Dimension::THE_NETHER { + // Check if placed where water evaporates (EnvironmentAttributes.WATER_EVAPORATES), if so, dry out + if args.world.dimension.water_evaporates { args.world.set_block_state( args.position, Block::SPONGE.default_state.id, diff --git a/crates/pumpkin/src/block/blocks/trial_spawner.rs b/crates/pumpkin/src/block/blocks/trial_spawner.rs index 4ce2aa228..46e9c7b8e 100644 --- a/crates/pumpkin/src/block/blocks/trial_spawner.rs +++ b/crates/pumpkin/src/block/blocks/trial_spawner.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use pumpkin_macros::pumpkin_block; use crate::block::entities::trial_spawner::TrialSpawnerBlockEntity; -use crate::block::{BlockBehaviour, PlacedArgs}; +use crate::block::{BlockBehaviour, BrokenArgs, PlacedArgs}; #[pumpkin_block("minecraft:trial_spawner")] pub struct TrialSpawnerBlock; @@ -13,4 +13,8 @@ impl BlockBehaviour for TrialSpawnerBlock { let entity = TrialSpawnerBlockEntity::new(*args.position); args.world.add_block_entity(Arc::new(entity)); } + + fn broken(&self, args: BrokenArgs<'_>) { + args.world.remove_block_entity(args.position); + } } diff --git a/crates/pumpkin/src/block/entities/daylight_detector.rs b/crates/pumpkin/src/block/entities/daylight_detector.rs index 00796c990..18b9a2d1b 100644 --- a/crates/pumpkin/src/block/entities/daylight_detector.rs +++ b/crates/pumpkin/src/block/entities/daylight_detector.rs @@ -3,12 +3,11 @@ use std::sync::Arc; use pumpkin_nbt::compound::NbtCompound; use pumpkin_util::math::position::BlockPos; -use crate::world::{BlockFlags, World}; +use crate::block::blocks::redstone::daylight_detector::DaylightDetectorBlock; +use crate::world::World; use super::BlockEntity; -type DaylightDetectorProperties = pumpkin_data::block_properties::DaylightDetectorLikeProperties; - pub struct DaylightDetectorBlockEntity { pub position: BlockPos, } @@ -37,7 +36,7 @@ impl BlockEntity for DaylightDetectorBlockEntity { fn tick(&self, world: &Arc) { if world.get_world_age() % 20 == 0 && world.dimension.has_skylight { - Self::update_power(world, &self.position); + DaylightDetectorBlock::update_signal_strength(world, &self.position); } } } @@ -51,65 +50,6 @@ impl DaylightDetectorBlockEntity { } pub fn update_power(world: &Arc, block_pos: &BlockPos) { - use std::f32::consts::PI; - - let (block, state) = world.get_block_and_state(block_pos); - let mut props = DaylightDetectorProperties::from_state_id(state.id); - - let level = world.level.clone(); - - let inverted = props.inverted; - - // TODO: finish power signal calculation - // see: https://minecraft.wiki/w/Light#Internal_sky_light - /* - * THIS IS A PLACE HOLDER - * - * For now the light engine is not finished and we do not have access to such things as - * getAmbientDarkness() or EnvironmentAttributes.SUN_ANGLE_VISUAL as in java code from yarn: - * see: net.minecraft.blocks.DaylightDetectorBlock.java - * - * so this is a fake implementation using the time of day... - * is not as accurate as vanilla and doesnt consider weather - */ - - let time_of_day = world.get_time_of_day(); - - // Sun Angle - let sun_angle_fraction = (time_of_day as f32 / 24000.0) - 0.25; - let mut sun_angle_radians = sun_angle_fraction * (PI * 2.0); - - // Ambient darkness (considering only night, not weather) - let cos_angle = sun_angle_radians.cos(); - let ambient_darkness = if cos_angle < 0.0 { - // night time = ~11 darkness - (cos_angle.abs() * 11.0) as u8 - } else { - 0 // full daylight - }; - - let sky_light_level = level.light_engine.get_sky_light_level(&level, block_pos); - - let mut power = sky_light_level - ambient_darkness; - - if inverted { - power = 15 - power; - } else if power > 0 { - let transition_offset = if sun_angle_radians < PI { - 0.0 - } else { - PI * 2.0 - }; - - sun_angle_radians += (transition_offset - sun_angle_radians) * 0.2; - power = (power as f32 * sun_angle_radians.cos()).round() as u8; - } - - let power = power.clamp(0, 15); - if power != props.power { - props.power = power; - let state = props.to_state_id(block); - world.set_block_state(block_pos, state, BlockFlags::NOTIFY_ALL); - } + DaylightDetectorBlock::update_signal_strength(world, block_pos); } } diff --git a/crates/pumpkin/src/block/entities/trial_spawner.rs b/crates/pumpkin/src/block/entities/trial_spawner.rs index d931f5045..7a8ebb723 100644 --- a/crates/pumpkin/src/block/entities/trial_spawner.rs +++ b/crates/pumpkin/src/block/entities/trial_spawner.rs @@ -2,10 +2,14 @@ use rustc_hash::FxHashSet; use std::sync::{Arc, Mutex}; use pumpkin_data::block_properties::{TrialSpawnerLikeProperties, TrialSpawnerState}; +use pumpkin_data::data_component_impl::EquipmentSlot; +use pumpkin_data::effect::StatusEffect; use pumpkin_data::entity::EntityType; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::potion::Effect; use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::trial_spawner::TrialSpawnerKey; use pumpkin_data::world::WorldEvent; use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::tag::NbtTag; @@ -203,7 +207,91 @@ impl Default for TrialSpawnerConfig { } } +impl From<&pumpkin_data::trial_spawner::TrialSpawnerConfigData> for TrialSpawnerConfig { + fn from(data: &pumpkin_data::trial_spawner::TrialSpawnerConfigData) -> Self { + let spawn_potentials = data + .spawn_potentials + .iter() + .map(|sp| { + let entity_type = EntityType::from_name( + sp.entity_id + .strip_prefix("minecraft:") + .unwrap_or(sp.entity_id), + ); + let equipment = sp.equipment_loot_table.map(|loot_table| { + let mut cmp = NbtCompound::new(); + cmp.put_string("loot_table", loot_table.to_string()); + if let Some(chances) = sp.equipment_slot_drop_chances { + cmp.put_float("slot_drop_chances", chances); + } + cmp + }); + WeightedSpawnData { + data: SpawnData { + entity_type, + custom_spawn_rules: None, + equipment, + raw_entity_nbt: None, + }, + weight: sp.weight, + } + }) + .collect(); + + let loot_tables_to_eject = data + .loot_tables_to_eject + .iter() + .map(|lt| WeightedLootTable { + data: lt.loot_table.to_string(), + weight: lt.weight, + }) + .collect(); + + Self { + spawn_range: data.spawn_range, + total_mobs: data.total_mobs, + simultaneous_mobs: data.simultaneous_mobs, + total_mobs_added_per_player: data.total_mobs_added_per_player, + simultaneous_mobs_added_per_player: data.simultaneous_mobs_added_per_player, + ticks_between_spawn: data.ticks_between_spawn, + spawn_potentials, + loot_tables_to_eject, + items_to_drop_when_ominous: data.items_to_drop_when_ominous.map(ToString::to_string), + } + } +} + impl TrialSpawnerConfig { + #[must_use] + pub fn from_key(key: &str) -> Option { + if let Some(cfg) = pumpkin_data::trial_spawner::get_trial_spawner_config(key) { + return Some(Self::from(cfg)); + } + if let Some(trial_key) = TrialSpawnerKey::from_name(key) { + return Some(Self::from(trial_key.normal_config())); + } + let with_normal = format!("{key}/normal"); + if let Some(cfg) = pumpkin_data::trial_spawner::get_trial_spawner_config(&with_normal) { + return Some(Self::from(cfg)); + } + None + } + + #[must_use] + pub fn from_ominous_key(key: &str) -> Option { + if let Some(cfg) = pumpkin_data::trial_spawner::get_trial_spawner_config(key) { + return Some(Self::from(cfg)); + } + if let Some(trial_key) = TrialSpawnerKey::from_name(key) { + return Some(Self::from(trial_key.ominous_config())); + } + let with_ominous = format!("{key}/ominous"); + if let Some(cfg) = pumpkin_data::trial_spawner::get_trial_spawner_config(&with_ominous) { + return Some(Self::from(cfg)); + } + None + } + #[must_use] pub fn calculate_target_total_mobs(&self, player_count: usize) -> i32 { let additional = player_count.saturating_sub(1) as f32 * self.total_mobs_added_per_player; @@ -419,6 +507,16 @@ impl Default for TrialSpawnerFullConfig { } impl TrialSpawnerFullConfig { + #[must_use] + pub fn from_key(key: &pumpkin_data::trial_spawner::TrialSpawnerKey) -> Self { + Self { + normal: TrialSpawnerConfig::from(key.normal_config()), + ominous: TrialSpawnerConfig::from(key.ominous_config()), + target_cooldown_length: 36000, + required_player_range: 14, + } + } + #[must_use] pub fn override_entity(&self, entity_type: &'static EntityType) -> Self { Self { @@ -431,13 +529,27 @@ impl TrialSpawnerFullConfig { #[must_use] pub fn from_nbt(nbt: &NbtCompound) -> Self { + if let Some(key_str) = nbt.get_string("key").or_else(|| nbt.get_string("config")) + && let Some(trial_key) = TrialSpawnerKey::from_name(key_str) + { + return Self::from_key(&trial_key); + } + let normal = nbt .get_compound("normal_config") .map(TrialSpawnerConfig::from_nbt) + .or_else(|| { + nbt.get_string("normal_config") + .and_then(TrialSpawnerConfig::from_key) + }) .unwrap_or_default(); let ominous = nbt .get_compound("ominous_config") .map(TrialSpawnerConfig::from_nbt) + .or_else(|| { + nbt.get_string("ominous_config") + .and_then(TrialSpawnerConfig::from_ominous_key) + }) .unwrap_or_default(); let target_cooldown_length = nbt @@ -713,7 +825,78 @@ impl TrialSpawner { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .query_gametime(); + + for &id in &self.data.current_mobs { + if let Some(entity) = world.get_entity_by_uuid(id) { + let ent = entity.get_entity(); + let block_pos = + BlockPos::floored(ent.pos.load().x, ent.pos.load().y, ent.pos.load().z); + world.sync_world_event( + WorldEvent::ParticlesTrialSpawnerSpawnMobAt, + block_pos, + TrialSpawnerFlameParticle::Normal.encode(), + ); + world.remove_entity(entity.as_ref()); + } + } + self.data.reset_after_becoming_ominous(game_time); + self.data.next_mob_spawns_at = + game_time + i64::from(self.active_config().ticks_between_spawn); + } + + pub fn try_detect_players(&mut self, world: &Arc, spawner_pos: BlockPos) -> bool { + let detected = + self.player_detector + .detect(world, spawner_pos, self.config.required_player_range); + + if !self.is_ominous { + let mut ominous_player = None; + for &uuid in &detected { + if let Some(player) = world.get_player_by_uuid(uuid) { + if player.has_effect(&StatusEffect::TRIAL_OMEN) { + ominous_player = Some((player, false)); + break; + } + if player.has_effect(&StatusEffect::BAD_OMEN) { + ominous_player = Some((player, true)); + } + } + } + + if let Some((player, has_bad_omen)) = ominous_player { + if has_bad_omen && let Some(bad_omen) = player.get_effect(&StatusEffect::BAD_OMEN) { + let amplifier = i32::from(bad_omen.amplifier) + 1; + let duration = 18000 * amplifier; + player.remove_effect(&StatusEffect::BAD_OMEN); + player.add_effect(Effect { + effect_type: &StatusEffect::TRIAL_OMEN, + duration, + amplifier: 0, + ambient: false, + show_particles: true, + show_icon: true, + blend: true, + }); + } + let eye_pos = player.eye_position(); + let eye_block_pos = BlockPos::floored(eye_pos.x, eye_pos.y, eye_pos.z); + world.sync_world_event( + WorldEvent::ParticlesTrialSpawnerBecomeOminous, + eye_block_pos, + 0, + ); + self.apply_ominous(world, spawner_pos); + } + } + + let mut newly_detected = false; + for uuid in detected { + if self.data.registered_players.insert(uuid) { + newly_detected = true; + } + } + newly_detected } pub fn remove_ominous(&mut self, world: &Arc, spawner_pos: BlockPos) { @@ -793,8 +976,9 @@ impl TrialSpawner { level_info.game_rules.spawn_mobs } + #[allow(clippy::too_many_lines)] pub fn spawn_mob(&mut self, world: &Arc, spawner_pos: BlockPos) -> Option { - let (entity_type, spawn_range) = { + let (entity_type, spawn_range, equipment_loot_table, is_baby, slime_size) = { let active_cfg = if self.is_ominous { &self.config.ominous } else { @@ -802,7 +986,25 @@ impl TrialSpawner { }; let spawn_data = self.data.get_or_create_next_spawn_data(active_cfg); let ent_type = spawn_data.and_then(|sd| sd.entity_type)?; - (ent_type, active_cfg.spawn_range) + let equip = spawn_data + .and_then(|sd| { + sd.equipment + .as_ref() + .and_then(|eq| eq.get_string("loot_table")) + }) + .map(ToString::to_string); + let raw = spawn_data.and_then(|sd| sd.raw_entity_nbt.as_ref()); + let baby = raw + .and_then(|r| { + r.get_bool("IsBaby") + .or_else(|| r.get_byte("IsBaby").map(|b| b != 0)) + }) + .unwrap_or(false); + let size = raw.and_then(|r| { + r.get_byte("Size") + .or_else(|| r.get_int("Size").map(|i| i as i8)) + }); + (ent_type, active_cfg.spawn_range, equip, baby, size) }; let mut rng = rand::rng(); @@ -840,6 +1042,40 @@ impl TrialSpawner { let yaw = rng.random::() * 360.0; entity.get_entity().set_rotation(yaw, 0.0); + if is_baby { + let ent = entity.get_entity(); + ent.age.store(-24000, std::sync::atomic::Ordering::Relaxed); + ent.set_synced_data(pumpkin_data::tracked_data::ageable_mob::DATA_BABY_ID, true); + } + + if let Some(size) = slime_size { + entity + .get_entity() + .set_synced_data(pumpkin_data::tracked_data::slime::ID_SIZE, i32::from(size)); + } + + if let Some(equip) = equipment_loot_table + && let Some(loot_table) = pumpkin_data::loot_table::get_loot_table(&equip) + && let Some(living) = entity.get_living_entity() + { + let seed = rand::random::(); + let items = crate::world::loot::generate_loot(loot_table, seed); + let mut equipment = living + .entity_equipment + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let mut equipment_changes = Vec::new(); + for item in items { + let slot = item + .get_data_component::() + .map_or(EquipmentSlot::MAIN_HAND, |eq| eq.slot.clone()); + equipment.put(&slot, item.clone()); + equipment_changes.push((slot, item)); + } + drop(equipment); + living.send_equipment_changes(&equipment_changes); + } + let mut event = crate::plugin::api::events::entity::trial_spawner_spawn::TrialSpawnerSpawnEvent::new( entity.get_entity().entity_id, @@ -975,12 +1211,7 @@ impl TrialSpawner { return TrialSpawnerState::Inactive; } - let newly_detected = self.data.try_detect_players( - world, - spawner_pos, - self.config.required_player_range, - &self.player_detector, - ); + let newly_detected = self.try_detect_players(world, spawner_pos); if self.data.registered_players.is_empty() { TrialSpawnerState::WaitingForPlayers @@ -1013,12 +1244,7 @@ impl TrialSpawner { return TrialSpawnerState::Inactive; } - self.data.try_detect_players( - world, - spawner_pos, - self.config.required_player_range, - &self.player_detector, - ); + self.try_detect_players(world, spawner_pos); let (total_mobs_needed, max_simultaneous, ticks_between_spawn) = { let active_config = self.active_config(); @@ -1055,7 +1281,7 @@ impl TrialSpawner { TrialSpawnerState::WaitingForRewardEjection => { if game_time >= self.data.next_mob_spawns_at { world.play_sound( - Sound::BlockTrialSpawnerSpawnItemBegin, + Sound::BlockTrialSpawnerOpenShutter, SoundCategory::Blocks, &spawner_pos.to_f64(), ); @@ -1067,24 +1293,40 @@ impl TrialSpawner { } TrialSpawnerState::EjectingReward => { if game_time >= self.data.next_mob_spawns_at { - self.eject_reward(world, spawner_pos, self.data.ejecting_loot_table.as_deref()); - world.play_sound( - Sound::BlockTrialSpawnerEjectItem, - SoundCategory::Blocks, - &spawner_pos.to_f64(), - ); - world.play_sound( - Sound::BlockTrialSpawnerCloseShutter, - SoundCategory::Blocks, - &spawner_pos.to_f64(), - ); - self.data.next_mob_spawns_at = game_time + 20; - TrialSpawnerState::Cooldown + if let Some(player_id) = self.data.registered_players.iter().copied().next() { + self.data.registered_players.remove(&player_id); + let loot_table = self.active_config().pick_loot_table(); + self.eject_reward(world, spawner_pos, loot_table.as_deref()); + world.play_sound( + Sound::BlockTrialSpawnerEjectItem, + SoundCategory::Blocks, + &spawner_pos.to_f64(), + ); + self.data.next_mob_spawns_at = game_time + 20; + TrialSpawnerState::EjectingReward + } else { + world.play_sound( + Sound::BlockTrialSpawnerCloseShutter, + SoundCategory::Blocks, + &spawner_pos.to_f64(), + ); + self.data.next_mob_spawns_at = game_time + 20; + TrialSpawnerState::Cooldown + } } else { TrialSpawnerState::EjectingReward } } TrialSpawnerState::Cooldown => { + if !self.is_ominous { + self.try_detect_players(world, spawner_pos); + if self.is_ominous { + self.data.total_mobs_spawned = 0; + self.data.next_mob_spawns_at = 0; + return TrialSpawnerState::Active; + } + } + if game_time >= self.data.cooldown_ends_at { self.data.reset(); if self.is_ominous { @@ -1187,6 +1429,33 @@ impl TrialSpawnerBlockEntity { trial_spawner: Mutex::new(TrialSpawner::default()), } } + + #[must_use] + pub fn with_config(position: BlockPos, config: TrialSpawnerFullConfig) -> Self { + Self { + position, + trial_spawner: Mutex::new(TrialSpawner::new(config, PlayerDetector::default())), + } + } + + #[must_use] + pub fn from_key(position: BlockPos, key: &TrialSpawnerKey) -> Self { + Self::with_config(position, TrialSpawnerFullConfig::from_key(key)) + } + + pub fn set_entity_type(&self, entity_type: &'static EntityType, world: &Arc) { + if let Ok(mut spawner) = self.trial_spawner.lock() { + spawner.override_entity_to_spawn(entity_type, world, self.position); + } + } + + pub fn set_trial_spawner_key(&self, key: &TrialSpawnerKey, world: &Arc) { + if let Ok(mut spawner) = self.trial_spawner.lock() { + spawner.config = TrialSpawnerFullConfig::from_key(key); + spawner.data.reset(); + spawner.set_state(world, self.position, TrialSpawnerState::Inactive); + } + } } #[cfg(test)] diff --git a/crates/pumpkin/src/block/fluid/lava.rs b/crates/pumpkin/src/block/fluid/lava.rs index 559ad703f..2f066f4d8 100644 --- a/crates/pumpkin/src/block/fluid/lava.rs +++ b/crates/pumpkin/src/block/fluid/lava.rs @@ -7,7 +7,6 @@ use crate::{ use pumpkin_data::{ Block, BlockDirection, BlockState, BlockStateId, damage::DamageType, - dimension::Dimension, fluid::{Falling, Fluid, FluidProperties, Level}, world::WorldEvent, }; @@ -244,17 +243,12 @@ impl FluidBehaviour for FlowingLava { impl FlowingFluid for FlowingLava { fn get_level_decrease_per_block(&self, world: &World) -> i32 { - // Ultrawarm logic - if world.dimension == Dimension::THE_NETHER { - 1 - } else { - 2 - } + if world.dimension.fast_lava { 1 } else { 2 } } fn get_flow_speed(&self, world: &World) -> u8 { - // Ultrawarm logic - lava flows faster in the Nether - if world.dimension == Dimension::THE_NETHER { + // EnvironmentAttributes.FAST_LAVA + if world.dimension.fast_lava { LAVA_FLOW_SPEED_NETHER } else { LAVA_FLOW_SPEED_SLOW @@ -262,12 +256,7 @@ impl FlowingFluid for FlowingLava { } fn get_max_flow_distance(&self, world: &World) -> i32 { - // Ultrawarm logic - if world.dimension == Dimension::THE_NETHER { - 5 - } else { - 3 - } + if world.dimension.fast_lava { 5 } else { 3 } } /// Determines if lava can convert to source blocks based on game rules. diff --git a/crates/pumpkin/src/block/mod.rs b/crates/pumpkin/src/block/mod.rs index 026f1b03e..baf304d1e 100644 --- a/crates/pumpkin/src/block/mod.rs +++ b/crates/pumpkin/src/block/mod.rs @@ -24,6 +24,7 @@ use crate::server::Server; use pumpkin_data::BlockDirection; use pumpkin_data::block_rotation::{Mirror, Rotation}; use pumpkin_data::data_component_impl::EquipmentSlot; +use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; use pumpkin_inventory::screen_handler::ScreenHandlerFactory; use pumpkin_protocol::java::server::play::SUseItemOn; @@ -460,7 +461,11 @@ pub fn drop_loot( let key = format!("minecraft:blocks/{}", block.name); if let Some(loot_table) = pumpkin_data::loot_table::get_loot_table(&key) { let seed: i64 = rand::random(); - let items = crate::world::loot::generate_loot_with_context(loot_table, seed, params); + let mut items = crate::world::loot::generate_loot_with_context(loot_table, seed, params); + if block.has_tag(&tag::Block::MINECRAFT_LEAVES) { + // TODO: Re-enable apple and stick drops for leaves once table bonus/drop chances are properly implemented + items.retain(|stack| stack.item != &Item::APPLE && stack.item != &Item::STICK); + } if !items.is_empty() { let mut event = crate::plugin::block::block_drop_item::BlockDropItemEvent { block_pos: *pos, diff --git a/crates/pumpkin/src/entity/effect/raid_omen.rs b/crates/pumpkin/src/entity/effect/raid_omen.rs index 1c5932a02..6428e45c6 100644 --- a/crates/pumpkin/src/entity/effect/raid_omen.rs +++ b/crates/pumpkin/src/entity/effect/raid_omen.rs @@ -10,6 +10,9 @@ impl MobEffect for RaidOmenMobEffect { fn apply_effect_tick(&self, living: &LivingEntity, _amplifier: u8) { let world = living.entity.world.load(); + if !world.dimension.can_start_raid { + return; + } if let Some(entity) = world.get_entity_by_id(living.entity.entity_id) && let Some(player) = entity.get_player() && !player.is_spectator() diff --git a/crates/pumpkin/src/entity/mob/hoglin.rs b/crates/pumpkin/src/entity/mob/hoglin.rs index 5207233dd..940a9a9c1 100644 --- a/crates/pumpkin/src/entity/mob/hoglin.rs +++ b/crates/pumpkin/src/entity/mob/hoglin.rs @@ -2,7 +2,6 @@ use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::{Arc, Weak}; use pumpkin_data::attributes::Attributes; -use pumpkin_data::dimension::Dimension; use pumpkin_data::entity::EntityType; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_nbt::compound::NbtCompound; @@ -122,7 +121,7 @@ impl HoglinEntity { pub fn is_converting(&self, world: &World) -> bool { !self.is_immune_to_zombification() && !self.mob_entity.is_no_ai() - && world.dimension.minecraft_name != Dimension::THE_NETHER.minecraft_name + && world.dimension.piglins_zombify } fn convert_to_zoglin(&self) { diff --git a/crates/pumpkin/src/entity/mob/mod.rs b/crates/pumpkin/src/entity/mob/mod.rs index 7c42a9af9..98c45f019 100644 --- a/crates/pumpkin/src/entity/mob/mod.rs +++ b/crates/pumpkin/src/entity/mob/mod.rs @@ -89,17 +89,6 @@ pub struct MobEntity { last_sent_pitch: AtomicU8, last_sent_head_yaw: AtomicU8, } - -/// Tick boundaries (both inclusive) when monsters do not burn in sunlight (26.1). -/// -/// Sourced from `data/minecraft/timeline/day.json` — `monsters_burn` keyframes: -/// `value=false` at tick 12542 (dusk), `value=true` at tick 23460 (dawn). -/// -/// TODO: Replace with `EnvironmentAttributes::MONSTERS_BURN` lookup once the -/// `EnvironmentAttributeSystem` is implemented in `pumpkin-data`. -pub(crate) const NIGHT_START: i64 = 12542; -pub(crate) const NIGHT_END: i64 = 23459; - impl MobEntity { const AI_DISABLED_FLAG: u8 = 1; const LEFT_HANDED_FLAG: u8 = 2; @@ -535,22 +524,16 @@ impl MobEntity { let world_arc = entity.world.load(); let world = world_arc.as_ref(); - // Night boundary from data/minecraft/timeline/day.json — monsters_burn keyframes: - // value=false at tick 12542 (dusk), value=true at tick 23460 (dawn). - // TODO: read directly from EnvironmentAttributes::MONSTERS_BURN once implemented. - - let day_time = world.get_time_of_day() % 24000; - if (NIGHT_START..=NIGHT_END).contains(&day_time) { + let eye_block_pos = entity.get_eye_pos().to_block_pos(); + if !world.monsters_burn(&eye_block_pos) { return false; } // Vanilla: getLightLevelDependentMagicValue() — sky light at eye pos, scaled 0–1. - let eye_block_pos = entity.get_eye_pos(); let brightness = world .level .light_engine - .get_sky_light_level(&world.level, &eye_block_pos.to_block_pos()) - as f32 + .get_sky_light_level(&world.level, &eye_block_pos) as f32 / 15.0; if brightness <= 0.5 { diff --git a/crates/pumpkin/src/entity/mob/piglin.rs b/crates/pumpkin/src/entity/mob/piglin.rs index 857858bae..2d81aaa11 100644 --- a/crates/pumpkin/src/entity/mob/piglin.rs +++ b/crates/pumpkin/src/entity/mob/piglin.rs @@ -5,7 +5,6 @@ use std::sync::{ use pumpkin_data::Block; use pumpkin_data::data_component_impl::EquipmentSlot; -use pumpkin_data::dimension::Dimension; use pumpkin_data::entity::EntityType; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; @@ -180,7 +179,7 @@ impl PiglinEntity { pub fn is_converting(&self, world: &World) -> bool { !self.is_immune_to_zombification() && !self.mob_entity.is_no_ai() - && world.dimension.minecraft_name != Dimension::THE_NETHER.minecraft_name + && world.dimension.piglins_zombify } #[must_use] diff --git a/crates/pumpkin/src/entity/mob/piglin_brute.rs b/crates/pumpkin/src/entity/mob/piglin_brute.rs index 3464bd011..957fd2e85 100644 --- a/crates/pumpkin/src/entity/mob/piglin_brute.rs +++ b/crates/pumpkin/src/entity/mob/piglin_brute.rs @@ -4,7 +4,6 @@ use std::sync::{ }; use pumpkin_data::Block; -use pumpkin_data::dimension::Dimension; use pumpkin_data::entity::EntityType; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::tracked_data; @@ -107,7 +106,7 @@ impl PiglinBruteEntity { pub fn is_converting(&self, world: &World) -> bool { !self.is_immune_to_zombification() && !self.mob_entity.is_no_ai() - && world.dimension.minecraft_name != Dimension::THE_NETHER.minecraft_name + && world.dimension.piglins_zombify } #[must_use] diff --git a/crates/pumpkin/src/entity/mod.rs b/crates/pumpkin/src/entity/mod.rs index 7d64f9b1c..88823e7e9 100644 --- a/crates/pumpkin/src/entity/mod.rs +++ b/crates/pumpkin/src/entity/mod.rs @@ -2030,7 +2030,7 @@ impl Entity { .on_entity_collision_fluid(fluid, caller); } - let lava_speed = if world.dimension == Dimension::THE_NETHER { + let lava_speed = if world.dimension.fast_lava { 0.007 } else { 0.002_333_333 diff --git a/crates/pumpkin/src/entity/passive/wandering_trader.rs b/crates/pumpkin/src/entity/passive/wandering_trader.rs index 6ab05ef18..62b82c05d 100644 --- a/crates/pumpkin/src/entity/passive/wandering_trader.rs +++ b/crates/pumpkin/src/entity/passive/wandering_trader.rs @@ -43,7 +43,7 @@ use crate::entity::ai::goal::wander_around::WanderAroundGoal; use crate::entity::ai::goal::{Controls, Goal}; use crate::entity::ai::pathfinder::NavigatorGoal; use crate::entity::experience_orb::ExperienceOrbEntity; -use crate::entity::mob::{Mob, MobEntity, NIGHT_END, NIGHT_START}; +use crate::entity::mob::{Mob, MobEntity}; use crate::entity::player::Player; use crate::entity::{Entity, EntityBase}; use crate::world::World; @@ -1103,8 +1103,8 @@ impl Goal for WanderingTraderUseItemGoal { return false; } let world = trader.mob_entity.living_entity.entity.world.load(); - let day_time = world.get_time_of_day() % 24000; - let is_dark = (NIGHT_START..=NIGHT_END).contains(&day_time); + let is_dark = world.is_dark_outside(); + let is_bright = world.is_bright_outside(); let is_invisible = trader .mob_entity .living_entity @@ -1114,7 +1114,7 @@ impl Goal for WanderingTraderUseItemGoal { self.goal_type = Some(PotionGoalType::Invisibility); return true; } - if !is_dark && is_invisible { + if is_bright && is_invisible { self.goal_type = Some(PotionGoalType::Milk); return true; } diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs index deb985a12..501aa8080 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs @@ -4176,6 +4176,9 @@ impl Player { && !self.has_effect(&StatusEffect::RAID_OMEN) { let world = self.world(); + if !world.dimension.can_start_raid { + return; + } let player_pos = self.living_entity.entity.block_pos.load(); let pos_f64 = self.living_entity.entity.pos.load(); diff --git a/crates/pumpkin/src/item/items/bucket.rs b/crates/pumpkin/src/item/items/bucket.rs index 4f4ce19b1..9d00c6020 100644 --- a/crates/pumpkin/src/item/items/bucket.rs +++ b/crates/pumpkin/src/item/items/bucket.rs @@ -7,7 +7,6 @@ use crate::{ }; use pumpkin_data::{ Block, BlockDirection, - dimension::Dimension, entity::EntityType, fluid::Fluid, item::Item, @@ -204,10 +203,10 @@ fn try_pickup_bucket_item( Some(&Item::WATER_BUCKET) } -pub(crate) fn should_evaporate_in_nether(item: &Item, world: &World) -> bool { +pub(crate) const fn should_evaporate_in_nether(item: &Item, world: &World) -> bool { item.id != Item::LAVA_BUCKET.id && item.id != Item::POWDER_SNOW_BUCKET.id - && world.dimension == Dimension::THE_NETHER + && world.dimension.water_evaporates } pub(crate) fn play_bucket_evaporation(world: &Arc, position: &Vector3) { diff --git a/crates/pumpkin/src/item/items/spawn_egg.rs b/crates/pumpkin/src/item/items/spawn_egg.rs index 12eb5884a..b0da71741 100644 --- a/crates/pumpkin/src/item/items/spawn_egg.rs +++ b/crates/pumpkin/src/item/items/spawn_egg.rs @@ -133,15 +133,25 @@ impl ItemBehaviour for SpawnEggItem { if let Some(entity_type) = entity_from_egg(item.item.id) { let world = player.world(); - if let Some(block_entity) = player.world().get_block_entity(&location) - && let Some(spawner) = block_entity + if let Some(block_entity) = player.world().get_block_entity(&location) { + if let Some(spawner) = block_entity .as_any() .downcast_ref::() - { - spawner.set_entity_type(entity_type); - world.update_block_entity(&block_entity); - item.decrement_unless_creative(player.gamemode.load(), 1); - return; + { + spawner.set_entity_type(entity_type); + world.update_block_entity(&block_entity); + item.decrement_unless_creative(player.gamemode.load(), 1); + return; + } + if let Some(trial_spawner) = block_entity + .as_any() + .downcast_ref::() + { + trial_spawner.set_entity_type(entity_type, &world); + world.update_block_entity(&block_entity); + item.decrement_unless_creative(player.gamemode.load(), 1); + return; + } } let target_state = world.get_block_state(&location); diff --git a/crates/pumpkin/src/world/environment.rs b/crates/pumpkin/src/world/environment.rs new file mode 100644 index 000000000..5ade2a002 --- /dev/null +++ b/crates/pumpkin/src/world/environment.rs @@ -0,0 +1,238 @@ +use pumpkin_data::dimension::BedRule; +use pumpkin_data::environment_attribute::{ + Activity, DayTimeline, EarlyGameTimeline, EnvironmentAttribute, MoonPhase, MoonTimeline, + VillagerScheduleTimeline, sample_activity_track, sample_bool_track, sample_float_track, + sample_moon_phase_track, sample_step_float_track, sample_unbounded_bool_track, +}; +use pumpkin_util::math::position::BlockPos; + +use super::World; + +/// Environment attribute accessor for a world, matching vanilla's `EnvironmentAttributeSystem`. +pub struct EnvironmentAttributes<'a> { + world: &'a World, +} + +impl<'a> EnvironmentAttributes<'a> { + #[must_use] + pub const fn new(world: &'a World) -> Self { + Self { world } + } + + /// Evaluates a float environment attribute at the dimension level (e.g. `EnvironmentAttributes.SKY_LIGHT_LEVEL`). + #[must_use] + pub fn get_dimension_value_f32(&self, attribute: EnvironmentAttribute) -> f32 { + match attribute { + EnvironmentAttribute::GameplaySkyLightLevel => { + let base = self.world.dimension.effective_sky_light_level(); + let multiplier = + if self.world.dimension.has_skylight && !self.world.dimension.has_fixed_time { + sample_float_track( + DayTimeline::SKY_LIGHT_LEVEL_KEYFRAMES, + DayTimeline::PERIOD_TICKS, + self.world.get_time_of_day(), + ) + } else { + 1.0 + }; + let mut level = base * multiplier; + + // Weather modifier (WeatherAttributes.RAIN and THUNDER) + if self.world.dimension.has_skylight { + let (rain_level, thunder_level) = { + let weather = self + .world + .weather + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + (weather.rain_level, weather.thunder_level) + }; + let rain_adj = (rain_level - thunder_level).max(0.0); + if rain_adj > 0.0 { + let rain_target = 4.0; + level += rain_adj * 0.3125 * (rain_target - level); + } + if thunder_level > 0.0 { + let thunder_target = 4.0; + level += thunder_level * 0.52734375 * (thunder_target - level); + } + } + level.clamp(0.0, 15.0) + } + EnvironmentAttribute::GameplaySurfaceSlimeSpawnChance => { + if self.world.dimension.has_skylight && !self.world.dimension.has_fixed_time { + sample_step_float_track( + MoonTimeline::SURFACE_SLIME_SPAWN_CHANCE_KEYFRAMES, + MoonTimeline::PERIOD_TICKS, + self.world.get_time_of_day(), + ) + } else { + 0.0 + } + } + EnvironmentAttribute::VisualSunAngle => { + super::calculate_celestial_angle(self.world.get_time_of_day()) * 360.0 + } + EnvironmentAttribute::VisualMoonAngle => { + (super::calculate_celestial_angle(self.world.get_time_of_day()) * 360.0 + 180.0) + .rem_euclid(360.0) + } + _ => 0.0, + } + } + + /// Evaluates a boolean environment attribute at the dimension level. + #[must_use] + pub fn get_dimension_value_bool(&self, attribute: EnvironmentAttribute) -> bool { + match attribute { + EnvironmentAttribute::GameplayFastLava => self.world.dimension.fast_lava, + EnvironmentAttribute::GameplayWaterEvaporates => self.world.dimension.water_evaporates, + EnvironmentAttribute::GameplayRespawnAnchorWorks => { + self.world.dimension.respawn_anchor_works + } + EnvironmentAttribute::GameplayPiglinsZombify => self.world.dimension.piglins_zombify, + EnvironmentAttribute::GameplaySnowGolemMelts => self.world.dimension.snow_golem_melts, + EnvironmentAttribute::GameplayCanStartRaid => self.world.dimension.can_start_raid, + EnvironmentAttribute::GameplayNetherPortalSpawnsPiglin => { + self.world.dimension.nether_portal_spawns_piglin + } + EnvironmentAttribute::GameplayCanPillagerPatrolSpawn => { + if self.world.dimension.has_skylight && !self.world.dimension.has_fixed_time { + sample_unbounded_bool_track( + EarlyGameTimeline::CAN_PILLAGER_PATROL_SPAWN_KEYFRAMES, + self.world.get_time_of_day(), + ) + } else { + false + } + } + EnvironmentAttribute::GameplayMonstersBurn => { + self.world.dimension.has_skylight + && !self.world.dimension.has_fixed_time + && sample_bool_track( + DayTimeline::MONSTERS_BURN_KEYFRAMES, + DayTimeline::PERIOD_TICKS, + self.world.get_time_of_day(), + ) + } + EnvironmentAttribute::GameplayBeesStayInHive => { + let raining = { + let weather = self + .world + .weather + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + weather.raining + }; + raining + || !self.world.dimension.has_skylight + || sample_bool_track( + DayTimeline::BEES_STAY_IN_HIVE_KEYFRAMES, + DayTimeline::PERIOD_TICKS, + self.world.get_time_of_day(), + ) + } + EnvironmentAttribute::GameplayCreakingActive => { + self.world.dimension.has_skylight + && !self.world.dimension.has_fixed_time + && sample_bool_track( + DayTimeline::CREAKING_ACTIVE_KEYFRAMES, + DayTimeline::PERIOD_TICKS, + self.world.get_time_of_day(), + ) + } + _ => false, + } + } + + /// Evaluates a `BedRule` environment attribute at the dimension level. + #[must_use] + pub const fn get_dimension_value_bed_rule(&self) -> BedRule { + self.world.dimension.bed_rule + } + + /// Evaluates a `TriState` environment attribute at the dimension level. + #[must_use] + pub fn get_dimension_value_tri_state(&self, attribute: EnvironmentAttribute) -> Option { + match attribute { + EnvironmentAttribute::GameplayEyeblossomOpen => (self.world.dimension.has_skylight + && !self.world.dimension.has_fixed_time) + .then(|| { + sample_bool_track( + DayTimeline::EYEBLOSSOM_OPEN_KEYFRAMES, + DayTimeline::PERIOD_TICKS, + self.world.get_time_of_day(), + ) + }), + _ => None, + } + } + + /// Evaluates the current `MoonPhase` from the moon timeline. + #[must_use] + pub fn get_dimension_value_moon_phase(&self) -> MoonPhase { + sample_moon_phase_track( + MoonTimeline::MOON_PHASE_KEYFRAMES, + MoonTimeline::PERIOD_TICKS, + self.world.get_time_of_day(), + ) + } + + /// Evaluates the current villager `Activity` from the villager schedule timeline. + #[must_use] + pub fn get_dimension_value_activity(&self, baby: bool) -> Activity { + if baby { + sample_activity_track( + VillagerScheduleTimeline::BABY_VILLAGER_ACTIVITY_KEYFRAMES, + VillagerScheduleTimeline::PERIOD_TICKS, + self.world.get_time_of_day(), + ) + } else { + sample_activity_track( + VillagerScheduleTimeline::VILLAGER_ACTIVITY_KEYFRAMES, + VillagerScheduleTimeline::PERIOD_TICKS, + self.world.get_time_of_day(), + ) + } + } + + /// Evaluates a boolean environment attribute at a position. + #[must_use] + pub fn get_value_bool(&self, attribute: EnvironmentAttribute, _pos: &BlockPos) -> bool { + self.get_dimension_value_bool(attribute) + } + + /// Evaluates a float environment attribute at a position. + #[must_use] + pub fn get_value_f32(&self, attribute: EnvironmentAttribute, _pos: &BlockPos) -> f32 { + self.get_dimension_value_f32(attribute) + } + + /// Evaluates a `BedRule` environment attribute at a position. + #[must_use] + pub const fn get_value_bed_rule(&self, _pos: &BlockPos) -> BedRule { + self.get_dimension_value_bed_rule() + } + + /// Evaluates a `TriState` environment attribute at a position. + #[must_use] + pub fn get_value_tri_state( + &self, + attribute: EnvironmentAttribute, + _pos: &BlockPos, + ) -> Option { + self.get_dimension_value_tri_state(attribute) + } + + /// Evaluates the current `MoonPhase` at a position. + #[must_use] + pub fn get_value_moon_phase(&self, _pos: &BlockPos) -> MoonPhase { + self.get_dimension_value_moon_phase() + } + + /// Evaluates the current villager `Activity` at a position. + #[must_use] + pub fn get_value_activity(&self, baby: bool, _pos: &BlockPos) -> Activity { + self.get_dimension_value_activity(baby) + } +} diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index 4cede76d1..53187e7a6 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -169,10 +169,14 @@ pub mod custom_bossbar; pub mod dragon_fight; pub mod end_podium; pub mod entity_tracker; +pub mod environment; pub mod natural_spawner; pub mod scoreboard; pub mod weather; +pub use environment::EnvironmentAttributes; +pub use pumpkin_data::environment_attribute::{Activity, MoonPhase}; + use crate::world::natural_spawner::{SpawnState, spawn_for_chunk}; use pumpkin_config::lighting::LightingEngineConfig; use pumpkin_data::effect::StatusEffect; @@ -5556,10 +5560,111 @@ impl World { Some(self.set_block_state(position, new_state_id, flags)) } + #[must_use] + pub const fn environment_attributes(&self) -> EnvironmentAttributes<'_> { + EnvironmentAttributes::new(self) + } + + #[must_use] + pub fn get_sky_darken(&self) -> i32 { + let sky_light_level = self.environment_attributes().get_dimension_value_f32( + pumpkin_data::environment_attribute::EnvironmentAttribute::GameplaySkyLightLevel, + ); + (15.0 - sky_light_level).clamp(0.0, 15.0) as i32 + } + + #[must_use] + pub fn is_bright_outside(&self) -> bool { + !self.dimension.has_fixed_time && self.get_sky_darken() < 4 + } + + #[must_use] + pub fn is_dark_outside(&self) -> bool { + !self.dimension.has_fixed_time && !self.is_bright_outside() + } + + /// Checks if daylight burns undead monsters (`EnvironmentAttributes.MONSTERS_BURN`). + #[must_use] + pub fn monsters_burn(&self, pos: &BlockPos) -> bool { + self.environment_attributes().get_value_bool( + pumpkin_data::environment_attribute::EnvironmentAttribute::GameplayMonstersBurn, + pos, + ) + } + + /// Checks if bees should stay inside beehives/nests (`EnvironmentAttributes.BEES_STAY_IN_HIVE`). + #[must_use] + pub fn bees_stay_in_hive(&self, pos: &BlockPos) -> bool { + self.environment_attributes().get_value_bool( + pumpkin_data::environment_attribute::EnvironmentAttribute::GameplayBeesStayInHive, + pos, + ) + } + + /// Checks if a creaking heart is active (`EnvironmentAttributes.CREAKING_ACTIVE`). + #[must_use] + pub fn creaking_active(&self, pos: &BlockPos) -> bool { + self.environment_attributes().get_value_bool( + pumpkin_data::environment_attribute::EnvironmentAttribute::GameplayCreakingActive, + pos, + ) + } + + /// Checks if an eyeblossom flower should be open (`EnvironmentAttributes.EYEBLOSSOM_OPEN`). + #[must_use] + pub fn eyeblossom_open(&self, pos: &BlockPos) -> Option { + self.environment_attributes().get_value_tri_state( + pumpkin_data::environment_attribute::EnvironmentAttribute::GameplayEyeblossomOpen, + pos, + ) + } + + #[must_use] + pub fn get_effective_sky_brightness(&self, pos: &BlockPos) -> i32 { + let sky_light = self.get_sky_light_level(pos) as i32; + sky_light - self.get_sky_darken() + } + + #[must_use] + pub fn get_sun_angle(&self, pos: &BlockPos) -> f32 { + let sun_angle_deg = self.environment_attributes().get_value_f32( + pumpkin_data::environment_attribute::EnvironmentAttribute::VisualSunAngle, + pos, + ); + sun_angle_deg * (std::f32::consts::PI / 180.0) + } + + #[must_use] + pub fn get_moon_phase(&self) -> MoonPhase { + self.environment_attributes() + .get_dimension_value_moon_phase() + } + + #[must_use] + pub fn can_pillager_patrol_spawn(&self, pos: &BlockPos) -> bool { + self.environment_attributes().get_value_bool( + pumpkin_data::environment_attribute::EnvironmentAttribute::GameplayCanPillagerPatrolSpawn, + pos, + ) + } + + #[must_use] + pub fn surface_slime_spawn_chance(&self, pos: &BlockPos) -> f32 { + self.environment_attributes().get_value_f32( + pumpkin_data::environment_attribute::EnvironmentAttribute::GameplaySurfaceSlimeSpawnChance, + pos, + ) + } + + #[must_use] + pub fn villager_activity(&self, pos: &BlockPos, baby: bool) -> Activity { + self.environment_attributes().get_value_activity(baby, pos) + } + pub fn get_max_local_raw_brightness(&self, pos: &BlockPos) -> u8 { - let sky_light = self.get_sky_light_level(pos); + let sky_light = (self.get_sky_light_level(pos) as i32 - self.get_sky_darken()).max(0) as u8; let block_light = self.get_block_light_level(pos).unwrap_or(0); - sky_light.max(block_light) // TODO: getSkyDarken + sky_light.max(block_light) } pub fn get_block_light_level(&self, position: &BlockPos) -> Option { @@ -7351,6 +7456,82 @@ impl WorldPortalExt for WorldPortal { } } +struct CubicCurve { + a: f32, + b: f32, + c: f32, +} + +impl CubicCurve { + fn new(v1: f32, v2: f32) -> Self { + Self { + a: 3.0 * v1 - 3.0 * v2 + 1.0, + b: -6.0 * v1 + 3.0 * v2, + c: 3.0 * v1, + } + } + + fn sample(&self, t: f32) -> f32 { + ((self.a * t + self.b) * t + self.c) * t + } + + fn sample_gradient(&self, t: f32) -> f32 { + (3.0 * self.a * t + 2.0 * self.b) * t + self.c + } +} + +/// Calculates the celestial (sun) angle fraction in `[0.0, 1.0]`. +/// Matches vanilla 26.2 `EnvironmentAttributes.SUN_ANGLE` easing with `symmetricCubicBezier(0.362, 0.241)`. +#[must_use] +pub fn calculate_celestial_angle(time_of_day: i64) -> f32 { + let ticks = time_of_day.rem_euclid(24000); + let alpha = if ticks < 6000 { + (ticks + 18000) as f32 / 24000.0 + } else { + (ticks - 6000) as f32 / 24000.0 + }; + + let x_curve = CubicCurve::new(0.362, 0.638); + let y_curve = CubicCurve::new(0.241, 0.759); + + let mut t = alpha; + let mut solved = false; + for _ in 0..4 { + let error = x_curve.sample(t) - alpha; + if error.abs() < 1e-5 { + solved = true; + break; + } + let gradient = x_curve.sample_gradient(t); + if gradient < 1e-5 { + break; + } + t -= (error / gradient).clamp(-0.25, 0.25); + } + + if !solved { + let mut t0 = 0.0f32; + let mut t1 = 1.0f32; + for _ in 0..64 { + if t0 >= t1 { + break; + } + let error = x_curve.sample(t) - alpha; + if error.abs() < 1e-5 { + break; + } + if error < 0.0 { + t0 = t; + } else { + t1 = t; + } + t = f32::midpoint(t1, t0); + } + } + + y_curve.sample(t) +} + #[cfg(test)] mod tests { use pumpkin_data::{ diff --git a/tools/pumpkin-codegen/src/dimension.rs b/tools/pumpkin-codegen/src/dimension.rs index b1f3f005c..8ee31702b 100644 --- a/tools/pumpkin-codegen/src/dimension.rs +++ b/tools/pumpkin-codegen/src/dimension.rs @@ -6,13 +6,13 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use serde::Deserialize; -/// Parses a CSS-style hex color string (e.g. `"#78a7ff"`) into a signed 32-bit integer. +/// Parses a CSS-style hex color string (e.g. `"#78a7ff"` or `"#ccffffff"`) into a signed 32-bit integer. /// /// # Returns /// The color as an `i32`, or `None` if the input is not a valid hex color. fn parse_hex_color(s: &str) -> Option { if let Some(stripped) = s.strip_prefix('#') { - i32::from_str_radix(stripped, 16).ok() + u32::from_str_radix(stripped, 16).ok().map(|v| v as i32) } else { None } @@ -42,7 +42,10 @@ pub struct Dimension { /// Fixed day-time value in this dimension, or `None` if time progresses normally. #[serde(rename = "fixed_time")] pub fixed_time: Option, - /// Optional bedrock-style visual attributes map (sky color, fog color, etc.). + /// Whether time is fixed in this dimension (modern 26.2 field). + #[serde(default, rename = "has_fixed_time")] + pub has_fixed_time: Option, + /// Environment attributes map (visual, gameplay, audio). #[serde(default)] pub attributes: Option, /// Optional timeline resource key controlling day/night progression. @@ -84,34 +87,120 @@ pub fn build() -> TokenStream { .to_shouty_snake_case() ); - // convert optional hex colors from attributes into ints - let sky_color = dim - .attributes - .as_ref() + let attrs = dim.attributes.as_ref(); + + // Visual environment attributes + let sky_color = attrs .and_then(|a| a.get("minecraft:visual/sky_color")) .and_then(|v| v.as_str()) .and_then(parse_hex_color); - let fog_color = dim - .attributes - .as_ref() + let fog_color = attrs .and_then(|a| a.get("minecraft:visual/fog_color")) .and_then(|v| v.as_str()) .and_then(parse_hex_color); - let cloud_color = dim - .attributes - .as_ref() + let cloud_color = attrs .and_then(|a| a.get("minecraft:visual/cloud_color")) .and_then(|v| v.as_str()) .and_then(parse_hex_color); + let ambient_light_color = attrs + .and_then(|a| a.get("minecraft:visual/ambient_light_color")) + .and_then(|v| v.as_str()) + .and_then(parse_hex_color); + let sky_light_color = attrs + .and_then(|a| a.get("minecraft:visual/sky_light_color")) + .and_then(|v| v.as_str()) + .and_then(parse_hex_color); + + let sky_light_factor = attrs + .and_then(|a| a.get("minecraft:visual/sky_light_factor")) + .and_then(|v| v.as_f64()) + .map(|f| f as f32); + let fog_start_distance = attrs + .and_then(|a| a.get("minecraft:visual/fog_start_distance")) + .and_then(|v| v.as_f64()) + .map(|f| f as f32); + let fog_end_distance = attrs + .and_then(|a| a.get("minecraft:visual/fog_end_distance")) + .and_then(|v| v.as_f64()) + .map(|f| f as f32); + let cloud_height = attrs + .and_then(|a| a.get("minecraft:visual/cloud_height")) + .and_then(|v| v.as_f64()) + .map(|f| f as f32); + + // Gameplay environment attributes + let sky_light_level = attrs + .and_then(|a| a.get("minecraft:gameplay/sky_light_level")) + .and_then(|v| v.as_f64()) + .map(|f| f as f32); + let water_evaporates = attrs + .and_then(|a| a.get("minecraft:gameplay/water_evaporates")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let fast_lava = attrs + .and_then(|a| a.get("minecraft:gameplay/fast_lava")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let respawn_anchor_works = attrs + .and_then(|a| a.get("minecraft:gameplay/respawn_anchor_works")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let piglins_zombify = attrs + .and_then(|a| a.get("minecraft:gameplay/piglins_zombify")) + .and_then(|v| v.as_bool()) + .unwrap_or(true); + let snow_golem_melts = attrs + .and_then(|a| a.get("minecraft:gameplay/snow_golem_melts")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let can_start_raid = attrs + .and_then(|a| a.get("minecraft:gameplay/can_start_raid")) + .and_then(|v| v.as_bool()) + .unwrap_or(true); + let nether_portal_spawns_piglin = attrs + .and_then(|a| a.get("minecraft:gameplay/nether_portal_spawns_piglin")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let bed_rule_obj = attrs.and_then(|a| a.get("minecraft:gameplay/bed_rule")); + let (can_sleep_token, can_set_spawn_token, explodes) = if let Some(b) = bed_rule_obj { + let can_sleep = b + .get("can_sleep") + .and_then(|v| v.as_str()) + .unwrap_or("when_dark"); + let can_set_spawn = b + .get("can_set_spawn") + .and_then(|v| v.as_str()) + .unwrap_or("always"); + let explodes = b.get("explodes").and_then(|v| v.as_bool()).unwrap_or(false); + + let sleep_ident = match can_sleep { + "always" => quote!(BedRuleOption::Always), + "never" => quote!(BedRuleOption::Never), + _ => quote!(BedRuleOption::WhenDark), + }; + let spawn_ident = match can_set_spawn { + "when_dark" => quote!(BedRuleOption::WhenDark), + "never" => quote!(BedRuleOption::Never), + _ => quote!(BedRuleOption::Always), + }; + (sleep_ident, spawn_ident, explodes) + } else { + ( + quote!(BedRuleOption::WhenDark), + quote!(BedRuleOption::Always), + false, + ) + }; let fixed_time = if let Some(t) = dim.fixed_time { quote! { Some(#t) } } else { quote! { None } }; + let has_fixed_time = dim.has_fixed_time.unwrap_or(dim.fixed_time.is_some()); let monster_spawn_light_level = value_to_int_provider(&dim.monster_spawn_light_level); - let monster_spawn_block_light_limit = dim.monster_spawn_block_light_limit; let ambient_light = dim.ambient_light; let coordinate_scale = dim.coordinate_scale; @@ -120,6 +209,7 @@ pub fn build() -> TokenStream { let logical_height = dim.logical_height; let has_skylight = dim.has_skylight; let has_ceiling = dim.has_ceiling; + // normalize infiniburn to always have namespace let infiniburn = if dim.infiniburn.contains(':') { dim.infiniburn.clone() @@ -155,6 +245,41 @@ pub fn build() -> TokenStream { } else { quote! { None } }; + let ambient_light_color_literal = if let Some(c) = ambient_light_color { + quote! { Some(#c) } + } else { + quote! { None } + }; + let sky_light_color_literal = if let Some(c) = sky_light_color { + quote! { Some(#c) } + } else { + quote! { None } + }; + let sky_light_factor_literal = if let Some(f) = sky_light_factor { + quote! { Some(#f) } + } else { + quote! { None } + }; + let fog_start_distance_literal = if let Some(f) = fog_start_distance { + quote! { Some(#f) } + } else { + quote! { None } + }; + let fog_end_distance_literal = if let Some(f) = fog_end_distance { + quote! { Some(#f) } + } else { + quote! { None } + }; + let cloud_height_literal = if let Some(f) = cloud_height { + quote! { Some(#f) } + } else { + quote! { None } + }; + let sky_light_level_literal = if let Some(f) = sky_light_level { + quote! { Some(#f) } + } else { + quote! { None } + }; let timelines_literal = if let Some(t) = timelines.clone() { quote! { Some(#t) } } else { @@ -166,6 +291,7 @@ pub fn build() -> TokenStream { id: #id, minecraft_name: #minecraft_name, fixed_time: #fixed_time, + has_fixed_time: #has_fixed_time, has_skylight: #has_skylight, has_ceiling: #has_ceiling, coordinate_scale: #coordinate_scale, @@ -179,6 +305,25 @@ pub fn build() -> TokenStream { sky_color: #sky_color_literal, fog_color: #fog_color_literal, cloud_color: #cloud_color_literal, + ambient_light_color: #ambient_light_color_literal, + sky_light_color: #sky_light_color_literal, + sky_light_factor: #sky_light_factor_literal, + fog_start_distance: #fog_start_distance_literal, + fog_end_distance: #fog_end_distance_literal, + cloud_height: #cloud_height_literal, + sky_light_level: #sky_light_level_literal, + water_evaporates: #water_evaporates, + fast_lava: #fast_lava, + respawn_anchor_works: #respawn_anchor_works, + piglins_zombify: #piglins_zombify, + snow_golem_melts: #snow_golem_melts, + can_start_raid: #can_start_raid, + nether_portal_spawns_piglin: #nether_portal_spawns_piglin, + bed_rule: BedRule { + can_sleep: #can_sleep_token, + can_set_spawn: #can_set_spawn_token, + explodes: #explodes, + }, timelines: #timelines_literal, }; }); @@ -195,11 +340,48 @@ pub fn build() -> TokenStream { WeightedEntry, WeightedListIntProvider, }; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum BedRuleOption { + Always, + WhenDark, + Never, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct BedRule { + pub can_sleep: BedRuleOption, + pub can_set_spawn: BedRuleOption, + pub explodes: bool, + } + + impl BedRule { + #[inline] + #[must_use] + pub const fn can_sleep(&self, is_dark_outside: bool) -> bool { + match self.can_sleep { + BedRuleOption::Always => true, + BedRuleOption::WhenDark => is_dark_outside, + BedRuleOption::Never => false, + } + } + + #[inline] + #[must_use] + pub const fn can_set_spawn(&self, is_dark_outside: bool) -> bool { + match self.can_set_spawn { + BedRuleOption::Always => true, + BedRuleOption::WhenDark => is_dark_outside, + BedRuleOption::Never => false, + } + } + } + #[derive(Debug, Clone)] pub struct Dimension { pub id: u8, pub minecraft_name: &'static str, pub fixed_time: Option, + pub has_fixed_time: bool, pub has_skylight: bool, pub has_ceiling: bool, pub coordinate_scale: f64, @@ -213,6 +395,21 @@ pub fn build() -> TokenStream { pub sky_color: Option, pub fog_color: Option, pub cloud_color: Option, + pub ambient_light_color: Option, + pub sky_light_color: Option, + pub sky_light_factor: Option, + pub fog_start_distance: Option, + pub fog_end_distance: Option, + pub cloud_height: Option, + pub sky_light_level: Option, + pub water_evaporates: bool, + pub fast_lava: bool, + pub respawn_anchor_works: bool, + pub piglins_zombify: bool, + pub snow_golem_melts: bool, + pub can_start_raid: bool, + pub nether_portal_spawns_piglin: bool, + pub bed_rule: BedRule, pub timelines: Option<&'static str>, } @@ -225,6 +422,18 @@ pub fn build() -> TokenStream { _ => None } } + + #[inline] + #[must_use] + pub const fn effective_sky_light_level(&self) -> f32 { + if let Some(level) = self.sky_light_level { + level + } else if self.has_skylight { + 15.0 + } else { + 0.0 + } + } } impl PartialEq for Dimension { fn eq(&self, other: &Self) -> bool { diff --git a/tools/pumpkin-codegen/src/environment_attribute.rs b/tools/pumpkin-codegen/src/environment_attribute.rs new file mode 100644 index 000000000..6fce88a4b --- /dev/null +++ b/tools/pumpkin-codegen/src/environment_attribute.rs @@ -0,0 +1,1170 @@ +use heck::ToPascalCase; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +struct AttributeDef { + id: u8, + name: &'static str, + attr_type: &'static str, + syncable: bool, + positional: bool, + spatially_interpolated: bool, +} + +const STATIC_ATTRIBUTES: &[AttributeDef] = &[ + AttributeDef { + id: 0, + name: "visual/fog_color", + attr_type: "RgbColor", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 1, + name: "visual/fog_start_distance", + attr_type: "Float", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 2, + name: "visual/fog_end_distance", + attr_type: "Float", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 3, + name: "visual/sky_fog_end_distance", + attr_type: "Float", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 4, + name: "visual/cloud_fog_end_distance", + attr_type: "Float", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 5, + name: "visual/water_fog_color", + attr_type: "RgbColor", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 6, + name: "visual/water_fog_start_distance", + attr_type: "Float", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 7, + name: "visual/water_fog_end_distance", + attr_type: "Float", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 8, + name: "visual/sky_color", + attr_type: "RgbColor", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 9, + name: "visual/sunrise_sunset_color", + attr_type: "ArgbColor", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 10, + name: "visual/cloud_color", + attr_type: "ArgbColor", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 11, + name: "visual/cloud_height", + attr_type: "Float", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 12, + name: "visual/sun_angle", + attr_type: "AngleDegrees", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 13, + name: "visual/moon_angle", + attr_type: "AngleDegrees", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 14, + name: "visual/star_angle", + attr_type: "AngleDegrees", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 15, + name: "visual/moon_phase", + attr_type: "MoonPhase", + syncable: true, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 16, + name: "visual/star_brightness", + attr_type: "Float", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 17, + name: "visual/block_light_tint", + attr_type: "RgbColor", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 18, + name: "visual/sky_light_color", + attr_type: "RgbColor", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 19, + name: "visual/sky_light_factor", + attr_type: "Float", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 20, + name: "visual/night_vision_color", + attr_type: "RgbColor", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 21, + name: "visual/ambient_light_color", + attr_type: "RgbColor", + syncable: true, + positional: true, + spatially_interpolated: true, + }, + AttributeDef { + id: 22, + name: "visual/default_dripstone_particle", + attr_type: "Particle", + syncable: true, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 23, + name: "visual/ambient_particles", + attr_type: "AmbientParticles", + syncable: true, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 24, + name: "audio/background_music", + attr_type: "BackgroundMusic", + syncable: true, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 25, + name: "audio/music_volume", + attr_type: "Float", + syncable: true, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 26, + name: "audio/ambient_sounds", + attr_type: "AmbientSounds", + syncable: true, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 27, + name: "audio/firefly_bush_sounds", + attr_type: "Boolean", + syncable: true, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 28, + name: "gameplay/sky_light_level", + attr_type: "Float", + syncable: true, + positional: false, + spatially_interpolated: false, + }, + AttributeDef { + id: 29, + name: "gameplay/can_start_raid", + attr_type: "Boolean", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 30, + name: "gameplay/water_evaporates", + attr_type: "Boolean", + syncable: true, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 31, + name: "gameplay/bed_rule", + attr_type: "BedRule", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 32, + name: "gameplay/respawn_anchor_works", + attr_type: "Boolean", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 33, + name: "gameplay/nether_portal_spawns_piglin", + attr_type: "Boolean", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 34, + name: "gameplay/fast_lava", + attr_type: "Boolean", + syncable: true, + positional: false, + spatially_interpolated: false, + }, + AttributeDef { + id: 35, + name: "gameplay/increased_fire_burnout", + attr_type: "Boolean", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 36, + name: "gameplay/eyeblossom_open", + attr_type: "TriState", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 37, + name: "gameplay/turtle_egg_hatch_chance", + attr_type: "Float", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 38, + name: "gameplay/piglins_zombify", + attr_type: "Boolean", + syncable: true, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 39, + name: "gameplay/snow_golem_melts", + attr_type: "Boolean", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 40, + name: "gameplay/creaking_active", + attr_type: "Boolean", + syncable: true, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 41, + name: "gameplay/surface_slime_spawn_chance", + attr_type: "Float", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 42, + name: "gameplay/cat_waking_up_gift_chance", + attr_type: "Float", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 43, + name: "gameplay/bees_stay_in_hive", + attr_type: "Boolean", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 44, + name: "gameplay/monsters_burn", + attr_type: "Boolean", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 45, + name: "gameplay/can_pillager_patrol_spawn", + attr_type: "Boolean", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 46, + name: "gameplay/villager_activity", + attr_type: "Activity", + syncable: false, + positional: true, + spatially_interpolated: false, + }, + AttributeDef { + id: 47, + name: "gameplay/baby_villager_activity", + attr_type: "Activity", + syncable: false, + positional: true, + spatially_interpolated: false, + }, +]; + +#[derive(serde::Deserialize)] +struct TimelineJson { + #[serde(default)] + period_ticks: Option, + #[serde(default)] + tracks: std::collections::BTreeMap, +} + +#[derive(serde::Deserialize)] +struct TimelineTrackJson { + #[serde(default)] + keyframes: Vec, +} + +#[derive(serde::Deserialize)] +struct KeyframeJson { + ticks: i32, + value: serde_json::Value, +} + +pub fn build() -> TokenStream { + let timeline_path = + std::path::Path::new("../../assets/datapacks/26_2/data/minecraft/timeline/day.json"); + let ( + period_ticks, + sky_light_level_keyframes, + monsters_burn_keyframes, + bees_stay_in_hive_keyframes, + creaking_active_keyframes, + eyeblossom_open_keyframes, + ) = if let Ok(content) = std::fs::read_to_string(timeline_path) { + if let Ok(timeline) = serde_json::from_str::(&content) { + let period = timeline.period_ticks.unwrap_or(24000); + let mut sky_kfs = Vec::new(); + let mut monsters_kfs = Vec::new(); + let mut bees_kfs = Vec::new(); + let mut creaking_kfs = Vec::new(); + let mut eyeblossom_kfs = Vec::new(); + + if let Some(track) = timeline.tracks.get("minecraft:gameplay/sky_light_level") { + for kf in &track.keyframes { + let ticks = kf.ticks; + let val = kf.value.as_f64().unwrap_or(1.0) as f32; + sky_kfs.push(quote! { FloatKeyframe::new(#ticks, #val) }); + } + } + if let Some(track) = timeline.tracks.get("minecraft:gameplay/monsters_burn") { + for kf in &track.keyframes { + let ticks = kf.ticks; + let val = kf.value.as_bool().unwrap_or(false); + monsters_kfs.push(quote! { BoolKeyframe::new(#ticks, #val) }); + } + } + if let Some(track) = timeline.tracks.get("minecraft:gameplay/bees_stay_in_hive") { + for kf in &track.keyframes { + let ticks = kf.ticks; + let val = kf.value.as_bool().unwrap_or(false); + bees_kfs.push(quote! { BoolKeyframe::new(#ticks, #val) }); + } + } + if let Some(track) = timeline.tracks.get("minecraft:gameplay/creaking_active") { + for kf in &track.keyframes { + let ticks = kf.ticks; + let val = kf.value.as_bool().unwrap_or(false); + creaking_kfs.push(quote! { BoolKeyframe::new(#ticks, #val) }); + } + } + if let Some(track) = timeline.tracks.get("minecraft:gameplay/eyeblossom_open") { + for kf in &track.keyframes { + let ticks = kf.ticks; + let val = kf.value.as_bool().unwrap_or(false); + eyeblossom_kfs.push(quote! { BoolKeyframe::new(#ticks, #val) }); + } + } + ( + period, + sky_kfs, + monsters_kfs, + bees_kfs, + creaking_kfs, + eyeblossom_kfs, + ) + } else { + ( + 24000, + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + ) + } + } else { + ( + 24000, + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + ) + }; + + let moon_path = + std::path::Path::new("../../assets/datapacks/26_2/data/minecraft/timeline/moon.json"); + let (moon_period_ticks, surface_slime_spawn_chance_keyframes, moon_phase_keyframes) = + if let Ok(content) = std::fs::read_to_string(moon_path) { + if let Ok(timeline) = serde_json::from_str::(&content) { + let period = timeline.period_ticks.unwrap_or(192000); + let mut slime_kfs = Vec::new(); + let mut moon_kfs = Vec::new(); + + if let Some(track) = timeline + .tracks + .get("minecraft:gameplay/surface_slime_spawn_chance") + { + for kf in &track.keyframes { + let ticks = kf.ticks; + let val = kf.value.as_f64().unwrap_or(0.0) as f32; + slime_kfs.push(quote! { FloatKeyframe::new(#ticks, #val) }); + } + } + if let Some(track) = timeline.tracks.get("minecraft:visual/moon_phase") { + for kf in &track.keyframes { + let ticks = kf.ticks; + let phase_str = kf.value.as_str().unwrap_or("full_moon"); + let phase_ident = format_ident!("{}", phase_str.to_pascal_case()); + moon_kfs.push( + quote! { MoonPhaseKeyframe::new(#ticks, MoonPhase::#phase_ident) }, + ); + } + } + (period, slime_kfs, moon_kfs) + } else { + (192000, Vec::new(), Vec::new()) + } + } else { + (192000, Vec::new(), Vec::new()) + }; + + let early_game_path = + std::path::Path::new("../../assets/datapacks/26_2/data/minecraft/timeline/early_game.json"); + let can_pillager_patrol_spawn_keyframes = + if let Ok(content) = std::fs::read_to_string(early_game_path) { + if let Ok(timeline) = serde_json::from_str::(&content) { + let mut patrol_kfs = Vec::new(); + if let Some(track) = timeline + .tracks + .get("minecraft:gameplay/can_pillager_patrol_spawn") + { + for kf in &track.keyframes { + let ticks = kf.ticks; + let val = kf.value.as_bool().unwrap_or(false); + patrol_kfs.push(quote! { BoolKeyframe::new(#ticks, #val) }); + } + } + patrol_kfs + } else { + Vec::new() + } + } else { + Vec::new() + }; + + let villager_schedule_path = std::path::Path::new( + "../../assets/datapacks/26_2/data/minecraft/timeline/villager_schedule.json", + ); + let (schedule_period_ticks, villager_activity_keyframes, baby_villager_activity_keyframes) = + if let Ok(content) = std::fs::read_to_string(villager_schedule_path) { + if let Ok(timeline) = serde_json::from_str::(&content) { + let period = timeline.period_ticks.unwrap_or(24000); + let mut villager_kfs = Vec::new(); + let mut baby_kfs = Vec::new(); + + if let Some(track) = timeline.tracks.get("minecraft:gameplay/villager_activity") { + for kf in &track.keyframes { + let ticks = kf.ticks; + let val_str = kf.value.as_str().unwrap_or("idle"); + let clean_str = val_str.strip_prefix("minecraft:").unwrap_or(val_str); + let ident = format_ident!("{}", clean_str.to_pascal_case()); + villager_kfs + .push(quote! { ActivityKeyframe::new(#ticks, Activity::#ident) }); + } + } + if let Some(track) = timeline + .tracks + .get("minecraft:gameplay/baby_villager_activity") + { + for kf in &track.keyframes { + let ticks = kf.ticks; + let val_str = kf.value.as_str().unwrap_or("idle"); + let clean_str = val_str.strip_prefix("minecraft:").unwrap_or(val_str); + let ident = format_ident!("{}", clean_str.to_pascal_case()); + baby_kfs.push(quote! { ActivityKeyframe::new(#ticks, Activity::#ident) }); + } + } + (period, villager_kfs, baby_kfs) + } else { + (24000, Vec::new(), Vec::new()) + } + } else { + (24000, Vec::new(), Vec::new()) + }; + + let mut variants = TokenStream::new(); + let mut to_name_match = TokenStream::new(); + let mut to_resource_match = TokenStream::new(); + let mut to_id_match = TokenStream::new(); + let mut from_id_match = TokenStream::new(); + let mut from_name_match = TokenStream::new(); + let mut to_type_match = TokenStream::new(); + let mut syncable_match = TokenStream::new(); + let mut positional_match = TokenStream::new(); + let mut spatially_interpolated_match = TokenStream::new(); + + for def in STATIC_ATTRIBUTES { + let ident = format_ident!("{}", def.name.to_pascal_case()); + let id_lit = def.id; + let name_str = def.name; + let resource_str = format!("minecraft:{name_str}"); + let type_ident = format_ident!("{}", def.attr_type); + let syncable = def.syncable; + let positional = def.positional; + let spatially_interpolated = def.spatially_interpolated; + + variants.extend(quote! { + #ident, + }); + + to_id_match.extend(quote! { + Self::#ident => #id_lit, + }); + + from_id_match.extend(quote! { + #id_lit => Some(Self::#ident), + }); + + to_name_match.extend(quote! { + Self::#ident => #name_str, + }); + + to_resource_match.extend(quote! { + Self::#ident => #resource_str, + }); + + from_name_match.extend(quote! { + #name_str | #resource_str => Some(Self::#ident), + }); + + to_type_match.extend(quote! { + Self::#ident => EnvironmentAttributeType::#type_ident, + }); + + syncable_match.extend(quote! { + Self::#ident => #syncable, + }); + + positional_match.extend(quote! { + Self::#ident => #positional, + }); + + spatially_interpolated_match.extend(quote! { + Self::#ident => #spatially_interpolated, + }); + } + + quote! { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub enum EnvironmentAttributeType { + RgbColor, + ArgbColor, + Float, + AngleDegrees, + Boolean, + TriState, + MoonPhase, + Activity, + BedRule, + Particle, + AmbientParticles, + BackgroundMusic, + AmbientSounds, + Integer, + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub enum EnvironmentAttribute { + #variants + } + + impl EnvironmentAttribute { + #[must_use] + pub const fn id(&self) -> u8 { + match self { + #to_id_match + } + } + + #[must_use] + pub const fn name(&self) -> &'static str { + match self { + #to_name_match + } + } + + #[must_use] + pub const fn resource_location(&self) -> &'static str { + match self { + #to_resource_match + } + } + + #[must_use] + pub const fn attribute_type(&self) -> EnvironmentAttributeType { + match self { + #to_type_match + } + } + + #[must_use] + pub const fn is_syncable(&self) -> bool { + match self { + #syncable_match + } + } + + #[must_use] + pub const fn is_positional(&self) -> bool { + match self { + #positional_match + } + } + + #[must_use] + pub const fn is_spatially_interpolated(&self) -> bool { + match self { + #spatially_interpolated_match + } + } + + #[must_use] + pub fn from_name(name: &str) -> Option { + match name { + #from_name_match + _ => None, + } + } + + #[must_use] + pub const fn from_id(id: u8) -> Option { + match id { + #from_id_match + _ => None, + } + } + } + + #[derive(Debug, Clone, Copy, PartialEq)] + pub struct FloatKeyframe { + pub ticks: i32, + pub value: f32, + } + + impl FloatKeyframe { + pub const fn new(ticks: i32, value: f32) -> Self { + Self { ticks, value } + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct BoolKeyframe { + pub ticks: i32, + pub value: bool, + } + + impl BoolKeyframe { + pub const fn new(ticks: i32, value: bool) -> Self { + Self { ticks, value } + } + } + + pub struct DayTimeline; + + impl DayTimeline { + pub const PERIOD_TICKS: i32 = #period_ticks; + pub const SKY_LIGHT_LEVEL_KEYFRAMES: &'static [FloatKeyframe] = &[ + #(#sky_light_level_keyframes),* + ]; + pub const MONSTERS_BURN_KEYFRAMES: &'static [BoolKeyframe] = &[ + #(#monsters_burn_keyframes),* + ]; + pub const BEES_STAY_IN_HIVE_KEYFRAMES: &'static [BoolKeyframe] = &[ + #(#bees_stay_in_hive_keyframes),* + ]; + pub const CREAKING_ACTIVE_KEYFRAMES: &'static [BoolKeyframe] = &[ + #(#creaking_active_keyframes),* + ]; + pub const EYEBLOSSOM_OPEN_KEYFRAMES: &'static [BoolKeyframe] = &[ + #(#eyeblossom_open_keyframes),* + ]; + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub enum MoonPhase { + FullMoon, + WaningGibbous, + ThirdQuarter, + WaningCrescent, + NewMoon, + WaxingCrescent, + FirstQuarter, + WaxingGibbous, + } + + impl MoonPhase { + #[must_use] + pub const fn index(&self) -> u8 { + match self { + Self::FullMoon => 0, + Self::WaningGibbous => 1, + Self::ThirdQuarter => 2, + Self::WaningCrescent => 3, + Self::NewMoon => 4, + Self::WaxingCrescent => 5, + Self::FirstQuarter => 6, + Self::WaxingGibbous => 7, + } + } + + #[must_use] + pub const fn from_index(index: u8) -> Option { + match index { + 0 => Some(Self::FullMoon), + 1 => Some(Self::WaningGibbous), + 2 => Some(Self::ThirdQuarter), + 3 => Some(Self::WaningCrescent), + 4 => Some(Self::NewMoon), + 5 => Some(Self::WaxingCrescent), + 6 => Some(Self::FirstQuarter), + 7 => Some(Self::WaxingGibbous), + _ => None, + } + } + + #[must_use] + pub const fn name(&self) -> &'static str { + match self { + Self::FullMoon => "full_moon", + Self::WaningGibbous => "waning_gibbous", + Self::ThirdQuarter => "third_quarter", + Self::WaningCrescent => "waning_crescent", + Self::NewMoon => "new_moon", + Self::WaxingCrescent => "waxing_crescent", + Self::FirstQuarter => "first_quarter", + Self::WaxingGibbous => "waxing_gibbous", + } + } + + #[must_use] + pub fn from_name(name: &str) -> Option { + match name { + "full_moon" => Some(Self::FullMoon), + "waning_gibbous" => Some(Self::WaningGibbous), + "third_quarter" => Some(Self::ThirdQuarter), + "waning_crescent" => Some(Self::WaningCrescent), + "new_moon" => Some(Self::NewMoon), + "waxing_crescent" => Some(Self::WaxingCrescent), + "first_quarter" => Some(Self::FirstQuarter), + "waxing_gibbous" => Some(Self::WaxingGibbous), + _ => None, + } + } + + #[must_use] + pub const fn start_tick(&self) -> i32 { + self.index() as i32 * 24000 + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct MoonPhaseKeyframe { + pub ticks: i32, + pub value: MoonPhase, + } + + impl MoonPhaseKeyframe { + pub const fn new(ticks: i32, value: MoonPhase) -> Self { + Self { ticks, value } + } + } + + pub struct MoonTimeline; + + impl MoonTimeline { + pub const PERIOD_TICKS: i32 = #moon_period_ticks; + pub const SURFACE_SLIME_SPAWN_CHANCE_KEYFRAMES: &'static [FloatKeyframe] = &[ + #(#surface_slime_spawn_chance_keyframes),* + ]; + pub const MOON_PHASE_KEYFRAMES: &'static [MoonPhaseKeyframe] = &[ + #(#moon_phase_keyframes),* + ]; + } + + pub struct EarlyGameTimeline; + + impl EarlyGameTimeline { + pub const CAN_PILLAGER_PATROL_SPAWN_KEYFRAMES: &'static [BoolKeyframe] = &[ + #(#can_pillager_patrol_spawn_keyframes),* + ]; + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub enum Activity { + Core, + Idle, + Work, + Play, + Rest, + Meet, + Panic, + Raid, + PreRaid, + Hide, + Fight, + Celebrate, + AdmireItem, + Avoid, + Ride, + PlayDead, + LongJump, + Ram, + Tongue, + Swim, + LaySpawn, + Sniff, + Investigate, + Roar, + Emerge, + Dig, + } + + impl Activity { + #[must_use] + pub const fn name(&self) -> &'static str { + match self { + Self::Core => "core", + Self::Idle => "idle", + Self::Work => "work", + Self::Play => "play", + Self::Rest => "rest", + Self::Meet => "meet", + Self::Panic => "panic", + Self::Raid => "raid", + Self::PreRaid => "pre_raid", + Self::Hide => "hide", + Self::Fight => "fight", + Self::Celebrate => "celebrate", + Self::AdmireItem => "admire_item", + Self::Avoid => "avoid", + Self::Ride => "ride", + Self::PlayDead => "play_dead", + Self::LongJump => "long_jump", + Self::Ram => "ram", + Self::Tongue => "tongue", + Self::Swim => "swim", + Self::LaySpawn => "lay_spawn", + Self::Sniff => "sniff", + Self::Investigate => "investigate", + Self::Roar => "roar", + Self::Emerge => "emerge", + Self::Dig => "dig", + } + } + + #[must_use] + pub fn from_name(name: &str) -> Option { + let name = name.strip_prefix("minecraft:").unwrap_or(name); + match name { + "core" => Some(Self::Core), + "idle" => Some(Self::Idle), + "work" => Some(Self::Work), + "play" => Some(Self::Play), + "rest" => Some(Self::Rest), + "meet" => Some(Self::Meet), + "panic" => Some(Self::Panic), + "raid" => Some(Self::Raid), + "pre_raid" => Some(Self::PreRaid), + "hide" => Some(Self::Hide), + "fight" => Some(Self::Fight), + "celebrate" => Some(Self::Celebrate), + "admire_item" => Some(Self::AdmireItem), + "avoid" => Some(Self::Avoid), + "ride" => Some(Self::Ride), + "play_dead" => Some(Self::PlayDead), + "long_jump" => Some(Self::LongJump), + "ram" => Some(Self::Ram), + "tongue" => Some(Self::Tongue), + "swim" => Some(Self::Swim), + "lay_spawn" => Some(Self::LaySpawn), + "sniff" => Some(Self::Sniff), + "investigate" => Some(Self::Investigate), + "roar" => Some(Self::Roar), + "emerge" => Some(Self::Emerge), + "dig" => Some(Self::Dig), + _ => None, + } + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct ActivityKeyframe { + pub ticks: i32, + pub value: Activity, + } + + impl ActivityKeyframe { + pub const fn new(ticks: i32, value: Activity) -> Self { + Self { ticks, value } + } + } + + pub struct VillagerScheduleTimeline; + + impl VillagerScheduleTimeline { + pub const PERIOD_TICKS: i32 = #schedule_period_ticks; + pub const VILLAGER_ACTIVITY_KEYFRAMES: &'static [ActivityKeyframe] = &[ + #(#villager_activity_keyframes),* + ]; + pub const BABY_VILLAGER_ACTIVITY_KEYFRAMES: &'static [ActivityKeyframe] = &[ + #(#baby_villager_activity_keyframes),* + ]; + } + + /// Samples a cyclic float keyframe track with linear interpolation across `period_ticks`. + #[must_use] + pub fn sample_float_track(keyframes: &[FloatKeyframe], period_ticks: i32, ticks: i64) -> f32 { + if keyframes.is_empty() { + return 1.0; + } + if keyframes.len() == 1 { + return keyframes[0].value; + } + let t = ticks.rem_euclid(period_ticks as i64) as f32; + let n = keyframes.len(); + let first = &keyframes[0]; + let last = &keyframes[n - 1]; + + if t <= first.ticks as f32 { + let seg_len = (first.ticks + period_ticks - last.ticks) as f32; + if seg_len <= 0.0 { + return first.value; + } + let alpha = (t + period_ticks as f32 - last.ticks as f32) / seg_len; + return last.value + alpha * (first.value - last.value); + } + if t >= last.ticks as f32 { + let seg_len = (first.ticks + period_ticks - last.ticks) as f32; + if seg_len <= 0.0 { + return last.value; + } + let alpha = (t - last.ticks as f32) / seg_len; + return last.value + alpha * (first.value - last.value); + } + + for i in 0..n - 1 { + let k1 = &keyframes[i]; + let k2 = &keyframes[i + 1]; + if t >= k1.ticks as f32 && t <= k2.ticks as f32 { + let seg_len = (k2.ticks - k1.ticks) as f32; + if seg_len <= 0.0 { + return k1.value; + } + let alpha = (t - k1.ticks as f32) / seg_len; + return k1.value + alpha * (k2.value - k1.value); + } + } + + last.value + } + + /// Samples a cyclic float keyframe track with step interpolation (constant easing) across `period_ticks`. + #[must_use] + pub fn sample_step_float_track(keyframes: &[FloatKeyframe], period_ticks: i32, ticks: i64) -> f32 { + if keyframes.is_empty() { + return 0.0; + } + let t = ticks.rem_euclid(period_ticks as i64) as i32; + let mut current = keyframes[keyframes.len() - 1].value; + for kf in keyframes { + if t >= kf.ticks { + current = kf.value; + } else { + break; + } + } + current + } + + /// Samples a cyclic boolean keyframe track (step function) across `period_ticks`. + #[must_use] + pub fn sample_bool_track(keyframes: &[BoolKeyframe], period_ticks: i32, ticks: i64) -> bool { + if keyframes.is_empty() { + return false; + } + let t = ticks.rem_euclid(period_ticks as i64) as i32; + let mut current = keyframes[keyframes.len() - 1].value; + for kf in keyframes { + if t >= kf.ticks { + current = kf.value; + } else { + break; + } + } + current + } + + /// Samples an unbounded (non-cyclic) boolean keyframe track (step function). + #[must_use] + pub fn sample_unbounded_bool_track(keyframes: &[BoolKeyframe], ticks: i64) -> bool { + if keyframes.is_empty() { + return false; + } + let mut current = keyframes[0].value; + for kf in keyframes { + if ticks >= kf.ticks as i64 { + current = kf.value; + } else { + break; + } + } + current + } + + /// Samples a cyclic moon phase keyframe track across `period_ticks`. + #[must_use] + pub fn sample_moon_phase_track(keyframes: &[MoonPhaseKeyframe], period_ticks: i32, ticks: i64) -> MoonPhase { + if keyframes.is_empty() { + return MoonPhase::FullMoon; + } + let t = ticks.rem_euclid(period_ticks as i64) as i32; + let mut current = keyframes[keyframes.len() - 1].value; + for kf in keyframes { + if t >= kf.ticks { + current = kf.value; + } else { + break; + } + } + current + } + + /// Samples a cyclic activity keyframe track across `period_ticks`. + #[must_use] + pub fn sample_activity_track(keyframes: &[ActivityKeyframe], period_ticks: i32, ticks: i64) -> Activity { + if keyframes.is_empty() { + return Activity::Idle; + } + let t = ticks.rem_euclid(period_ticks as i64) as i32; + let mut current = keyframes[keyframes.len() - 1].value; + for kf in keyframes { + if t >= kf.ticks { + current = kf.value; + } else { + break; + } + } + current + } + } +} diff --git a/tools/pumpkin-codegen/src/main.rs b/tools/pumpkin-codegen/src/main.rs index b8da85d43..75accb82f 100644 --- a/tools/pumpkin-codegen/src/main.rs +++ b/tools/pumpkin-codegen/src/main.rs @@ -47,6 +47,7 @@ mod enchantments; mod entity_pose; mod entity_status; mod entity_type; +mod environment_attribute; mod flower_pot_transformations; mod fluid; mod fuels; @@ -83,6 +84,7 @@ mod structures; mod tag; mod tracked_data; mod translations; +mod trial_spawner; mod version; mod villager; mod wit; @@ -153,6 +155,7 @@ pub fn main() { (fuels::build, "fuels.rs"), (data_component::build, "data_component.rs"), (attributes::build, "attributes.rs"), + (environment_attribute::build, "environment_attribute.rs"), (effect::build, "effect.rs"), (potion::build, "potion.rs"), (potion_brewing::build, "potion_brewing.rs"), @@ -170,6 +173,7 @@ pub fn main() { (map_decoration::build, "map_decoration.rs"), (dye_color::build, "dye_color.rs"), (block_transformer::build, "block_transformer.rs"), + (trial_spawner::build, "trial_spawner.rs"), ]; build_functions.extend(remap::build()); diff --git a/tools/pumpkin-codegen/src/trial_spawner.rs b/tools/pumpkin-codegen/src/trial_spawner.rs new file mode 100644 index 000000000..52ae24566 --- /dev/null +++ b/tools/pumpkin-codegen/src/trial_spawner.rs @@ -0,0 +1,377 @@ +use heck::{ToPascalCase, ToShoutySnakeCase}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use serde::Deserialize; +use std::fs; +use std::path::Path; + +const fn default_spawn_range() -> i32 { + 4 +} +const fn default_total_mobs() -> f32 { + 6.0 +} +const fn default_simultaneous_mobs() -> f32 { + 2.0 +} +const fn default_total_mobs_added_per_player() -> f32 { + 2.0 +} +const fn default_simultaneous_mobs_added_per_player() -> f32 { + 1.0 +} +const fn default_ticks_between_spawn() -> i32 { + 40 +} +const fn default_weight() -> i32 { + 1 +} + +#[derive(Deserialize, Debug)] +struct TrialSpawnerJson { + #[serde(default = "default_spawn_range")] + spawn_range: i32, + #[serde(default = "default_total_mobs")] + total_mobs: f32, + #[serde(default = "default_simultaneous_mobs")] + simultaneous_mobs: f32, + #[serde(default = "default_total_mobs_added_per_player")] + total_mobs_added_per_player: f32, + #[serde(default = "default_simultaneous_mobs_added_per_player")] + simultaneous_mobs_added_per_player: f32, + #[serde(default = "default_ticks_between_spawn")] + ticks_between_spawn: i32, + #[serde(default)] + spawn_potentials: Vec, + #[serde(default)] + loot_tables_to_eject: Vec, + #[serde(default)] + items_to_drop_when_ominous: Option, +} + +#[derive(Deserialize, Debug)] +struct WeightedSpawnPotentialJson { + data: SpawnDataJson, + #[serde(default = "default_weight")] + weight: i32, +} + +#[derive(Deserialize, Debug)] +struct SpawnDataJson { + entity: EntityRefJson, + #[serde(default)] + equipment: Option, +} + +#[derive(Deserialize, Debug)] +struct EntityRefJson { + id: String, +} + +#[derive(Deserialize, Debug)] +struct EquipmentJson { + loot_table: String, + #[serde(default)] + slot_drop_chances: Option, +} + +#[derive(Deserialize, Debug)] +struct WeightedLootTableJson { + data: String, + #[serde(default = "default_weight")] + weight: i32, +} + +fn collect_json_files(dir: &Path, base_dir: &Path, results: &mut Vec<(String, TrialSpawnerJson)>) { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_json_files(&path, base_dir, results); + } else if path.extension().and_then(|s| s.to_str()) == Some("json") { + if let Ok(relative) = path.strip_prefix(base_dir) { + let mut rel_str = relative.to_string_lossy().replace('\\', "/"); + if let Some(stripped) = rel_str.strip_suffix(".json") { + rel_str = stripped.to_string(); + } + if let Ok(content) = fs::read_to_string(&path) { + if let Ok(parsed) = serde_json::from_str::(&content) { + results.push((rel_str, parsed)); + } + } + } + } + } + } +} + +pub fn build() -> TokenStream { + let base_dir = Path::new("../../assets/datapacks/26_2/data/minecraft/trial_spawner"); + let mut files = Vec::new(); + collect_json_files(base_dir, base_dir, &mut files); + files.sort_by(|a, b| a.0.cmp(&b.0)); + + let mut const_defs = TokenStream::new(); + let mut match_arms = TokenStream::new(); + + let chamber_keys = [ + ("breeze", "Breeze", "trial_chamber/breeze"), + ("melee_husk", "MeleeHusk", "trial_chamber/melee/husk"), + ("melee_spider", "MeleeSpider", "trial_chamber/melee/spider"), + ("melee_zombie", "MeleeZombie", "trial_chamber/melee/zombie"), + ( + "ranged_poison_skeleton", + "RangedPoisonSkeleton", + "trial_chamber/ranged/poison_skeleton", + ), + ( + "ranged_skeleton", + "RangedSkeleton", + "trial_chamber/ranged/skeleton", + ), + ("ranged_stray", "RangedStray", "trial_chamber/ranged/stray"), + ( + "slow_ranged_poison_skeleton", + "SlowRangedPoisonSkeleton", + "trial_chamber/slow_ranged/poison_skeleton", + ), + ( + "slow_ranged_skeleton", + "SlowRangedSkeleton", + "trial_chamber/slow_ranged/skeleton", + ), + ( + "slow_ranged_stray", + "SlowRangedStray", + "trial_chamber/slow_ranged/stray", + ), + ( + "small_melee_baby_zombie", + "SmallMeleeBabyZombie", + "trial_chamber/small_melee/baby_zombie", + ), + ( + "small_melee_cave_spider", + "SmallMeleeCaveSpider", + "trial_chamber/small_melee/cave_spider", + ), + ( + "small_melee_silverfish", + "SmallMeleeSilverfish", + "trial_chamber/small_melee/silverfish", + ), + ( + "small_melee_slime", + "SmallMeleeSlime", + "trial_chamber/small_melee/slime", + ), + ]; + + for (rel_key, cfg) in &files { + let const_name = rel_key.replace('/', "_").to_shouty_snake_case(); + let const_ident = format_ident!("{}", const_name); + + let spawn_range = cfg.spawn_range; + let total_mobs = cfg.total_mobs; + let simultaneous_mobs = cfg.simultaneous_mobs; + let total_mobs_added_per_player = cfg.total_mobs_added_per_player; + let simultaneous_mobs_added_per_player = cfg.simultaneous_mobs_added_per_player; + let ticks_between_spawn = cfg.ticks_between_spawn; + + let spawn_potentials: Vec<_> = cfg + .spawn_potentials + .iter() + .map(|sp| { + let entity_id = &sp.data.entity.id; + let weight = sp.weight; + let (has_eq, eq_table, has_chance, eq_chance) = match &sp.data.equipment { + Some(eq) => ( + true, + eq.loot_table.as_str(), + eq.slot_drop_chances.is_some(), + eq.slot_drop_chances.unwrap_or(0.0), + ), + None => (false, "", false, 0.0), + }; + + let eq_loot_stream = if has_eq { + quote! { Some(#eq_table) } + } else { + quote! { None } + }; + + let eq_chance_stream = if has_chance { + quote! { Some(#eq_chance) } + } else { + quote! { None } + }; + + quote! { + WeightedSpawnData { + entity_id: #entity_id, + equipment_loot_table: #eq_loot_stream, + equipment_slot_drop_chances: #eq_chance_stream, + weight: #weight, + } + } + }) + .collect(); + + let loot_tables: Vec<_> = cfg + .loot_tables_to_eject + .iter() + .map(|lt| { + let table = <.data; + let weight = lt.weight; + quote! { + WeightedLootTable { + loot_table: #table, + weight: #weight, + } + } + }) + .collect(); + + let ominous_items = match &cfg.items_to_drop_when_ominous { + Some(s) => quote! { Some(#s) }, + None => quote! { None }, + }; + + const_defs.extend(quote! { + pub const #const_ident: TrialSpawnerConfigData = TrialSpawnerConfigData { + spawn_range: #spawn_range, + total_mobs: #total_mobs, + simultaneous_mobs: #simultaneous_mobs, + total_mobs_added_per_player: #total_mobs_added_per_player, + simultaneous_mobs_added_per_player: #simultaneous_mobs_added_per_player, + ticks_between_spawn: #ticks_between_spawn, + spawn_potentials: &[ + #(#spawn_potentials),* + ], + loot_tables_to_eject: &[ + #(#loot_tables),* + ], + items_to_drop_when_ominous: #ominous_items, + }; + }); + + let mc_key = format!("minecraft:{rel_key}"); + match_arms.extend(quote! { + #rel_key | #mc_key => Some(&#const_ident), + }); + } + + let mut key_variants = TokenStream::new(); + let mut key_name_match = TokenStream::new(); + let mut key_normal_match = TokenStream::new(); + let mut key_ominous_match = TokenStream::new(); + let mut key_from_name_match = TokenStream::new(); + + for (_, variant_name, path_str) in chamber_keys { + let variant_ident = format_ident!("{}", variant_name); + let normal_ident = format_ident!( + "{}_NORMAL", + path_str.replace('/', "_").to_shouty_snake_case() + ); + let ominous_ident = format_ident!( + "{}_OMINOUS", + path_str.replace('/', "_").to_shouty_snake_case() + ); + let mc_path = format!("minecraft:{path_str}"); + + key_variants.extend(quote! { + #variant_ident, + }); + + key_name_match.extend(quote! { + Self::#variant_ident => #path_str, + }); + + key_normal_match.extend(quote! { + Self::#variant_ident => &#normal_ident, + }); + + key_ominous_match.extend(quote! { + Self::#variant_ident => &#ominous_ident, + }); + + key_from_name_match.extend(quote! { + #path_str | #mc_path => Some(Self::#variant_ident), + }); + } + + quote! { + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct TrialSpawnerConfigData { + pub spawn_range: i32, + pub total_mobs: f32, + pub simultaneous_mobs: f32, + pub total_mobs_added_per_player: f32, + pub simultaneous_mobs_added_per_player: f32, + pub ticks_between_spawn: i32, + pub spawn_potentials: &'static [WeightedSpawnData], + pub loot_tables_to_eject: &'static [WeightedLootTable], + pub items_to_drop_when_ominous: Option<&'static str>, + } + + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct WeightedSpawnData { + pub entity_id: &'static str, + pub equipment_loot_table: Option<&'static str>, + pub equipment_slot_drop_chances: Option, + pub weight: i32, + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct WeightedLootTable { + pub loot_table: &'static str, + pub weight: i32, + } + + #const_defs + + #[must_use] + pub fn get_trial_spawner_config(key: &str) -> Option<&'static TrialSpawnerConfigData> { + match key { + #match_arms + _ => None, + } + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub enum TrialSpawnerKey { + #key_variants + } + + impl TrialSpawnerKey { + #[must_use] + pub const fn name(&self) -> &'static str { + match self { + #key_name_match + } + } + + #[must_use] + pub const fn normal_config(&self) -> &'static TrialSpawnerConfigData { + match self { + #key_normal_match + } + } + + #[must_use] + pub const fn ominous_config(&self) -> &'static TrialSpawnerConfigData { + match self { + #key_ominous_match + } + } + + #[must_use] + pub fn from_name(name: &str) -> Option { + match name { + #key_from_name_match + _ => None, + } + } + } + } +}