diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs index 302a1b6ac0..777346fa0e 100644 --- a/crates/horizon-client/src/game.rs +++ b/crates/horizon-client/src/game.rs @@ -252,6 +252,7 @@ mod native_item_container; mod native_item_energy; mod native_item_fluids; mod native_level_refs; +mod native_level_world; mod native_lifecycle; mod native_living_refs; mod native_machines; diff --git a/crates/horizon-client/src/game/native_level_refs.rs b/crates/horizon-client/src/game/native_level_refs.rs index be33b4b431..6e4214fd00 100644 --- a/crates/horizon-client/src/game/native_level_refs.rs +++ b/crates/horizon-client/src/game/native_level_refs.rs @@ -9,7 +9,7 @@ use horizon_client::world::World; impl Game<'_> { /// The world a handle names now, or None for a stale or foreign one. - fn native_level_world(&self, level: LevelRef) -> Option<&World> { + pub(super) fn native_level_world(&self, level: LevelRef) -> Option<&World> { if self.remote.is_some() || self.native_lifecycle.epoch() != Some(level.epoch()) { return None; } diff --git a/crates/horizon-client/src/game/native_level_world.rs b/crates/horizon-client/src/game/native_level_world.rs new file mode 100644 index 0000000000..3a378d7ffc --- /dev/null +++ b/crates/horizon-client/src/game/native_level_world.rs @@ -0,0 +1,68 @@ +//! A level's world settings for native server mods, through a `LevelRef` +//! (docs/native-mods.md, "Entity and level handles", #7387): the seed, the +//! difficulty, game rules and weather. They return `None` on a client or for +//! a handle of an earlier open. +use super::Game; +use horizon_client::command::vanilla::DIFFICULTIES; +use horizon_client::mod_api::{GameRuleValue, LevelRef}; + +/// `Level.isRaining` and `isThundering` from the rain and thunder levels. +fn weather_flags((rain, thunder): (f32, f32)) -> (bool, bool) { + (rain > 0.2, thunder > 0.9) +} + +impl Game<'_> { + /// The world's seed (`WorldOptions.seed`), as `/seed` shows it. API + /// @API:level_world@. + pub(super) fn native_level_seed(&self, level: LevelRef) -> Option { + self.native_level_world(level)?; + Some(self.level.seed) + } + /// The difficulty (`Level.getDifficulty`): `peaceful`, `easy`, `normal` + /// or `hard`, as `/difficulty` names it. API @API:level_world@. + pub(super) fn native_level_difficulty(&self, level: LevelRef) -> Option<&'static str> { + self.native_level_world(level)?; + DIFFICULTIES.get(usize::from(self.ui.difficulty)).copied() + } + /// Game rule `rule` (`minecraft:keep_inventory` or `keep_inventory`), as + /// `/gamerule` queries it: None for an unknown rule. API @API:level_world@. + pub(super) fn native_level_game_rule( + &self, + level: LevelRef, + rule: &str, + ) -> Option { + self.native_level_world(level)?; + let r = horizon_client::gamerules::rule(rule)?; + self.ui.rules.get(r.name) + } + /// The rain and thunder levels, each 0.0 to 1.0 (`Level.getRainLevel`, + /// `getThunderLevel`): both 0 in a level with no weather, as the Nether + /// and the End. API @API:level_world@. + pub(super) fn native_level_weather(&self, level: LevelRef) -> Option<(f32, f32)> { + let world = self.native_level_world(level)?; + if level.dimension() != super::Dim::Overworld.id() { + return Some((0.0, 0.0)); + } + Some(world.weather.levels(1.0)) + } + /// Whether it rains (`Level.isRaining`: a rain level above 0.2). API + /// @API:level_world@. + pub(super) fn native_level_is_raining(&self, level: LevelRef) -> Option { + Some(weather_flags(self.native_level_weather(level)?).0) + } + /// Whether it thunders (`Level.isThundering`: a thunder level above 0.9). + /// API @API:level_world@. + pub(super) fn native_level_is_thundering(&self, level: LevelRef) -> Option { + Some(weather_flags(self.native_level_weather(level)?).1) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn rain_and_thunder_start_above_vanillas_thresholds() { + assert_eq!(super::weather_flags((0.2, 0.0)), (false, false)); + assert_eq!(super::weather_flags((0.21, 0.9)), (true, false)); + assert_eq!(super::weather_flags((1.0, 0.95)), (true, true)); + } +} diff --git a/crates/horizon-client/src/mod_api.rs b/crates/horizon-client/src/mod_api.rs index be010ce790..3aac9f3e2e 100644 --- a/crates/horizon-client/src/mod_api.rs +++ b/crates/horizon-client/src/mod_api.rs @@ -40,6 +40,7 @@ pub use crate::game::{ NativePlayer, NativePlayerRender, NativeReloadListener, NativeShake, NativeStructureBlock, NativeStructureTemplate, NativeWorldDraw, }; +pub use crate::gamerules::Value as GameRuleValue; pub use crate::gfx::native_render::{ BlockSpriteArrays, RenderHooks, RenderNeeds, RenderNeedsEvent, WorldDrawn, WorldDrawnEvent, wgpu, diff --git a/docs/native-mods.md b/docs/native-mods.md index d8dcddc364..235606dc89 100644 --- a/docs/native-mods.md +++ b/docs/native-mods.md @@ -3711,6 +3711,9 @@ a dimension. - `level.block_state(host, pos)` (`minecraft:oak_stairs[facing=east,…]`, as `/setblock` takes it), `block` (the name alone), `block_property(host, pos, key)`, `is_loaded`, `height` (lowest `y` and height), `game_time` and `day_time` read any level the server has loaded, not only the one in place. A block in a chunk that isn't loaded reads as `None`. +- `level.seed(host)`, `difficulty` (`"normal"`, as `/difficulty` names it), `game_rule(host, "keep_inventory")` (a + `GameRuleValue`, `Bool` or `Int`; None for an unknown rule), `weather` (rain and thunder levels, 0.0 to 1.0; both 0 + where the level has no weather), `is_raining` and `is_thundering` read its world settings. - `host.living(entity)` gives a `LivingRef` for a mob, a player, an armor stand or the dragon (vanilla's `LivingEntity`); `living.entity()` goes back. `living.equipment(host, EquipmentSlot::Head)` and `hand_item(host, Hand::Main)` read its equipment (an empty slot reads as `None`); `set_equipment(host, slot, stack)` puts a stack in a diff --git a/tools/native-hook-fragments/level_world.json b/tools/native-hook-fragments/level_world.json new file mode 100644 index 0000000000..b0da6e84dc --- /dev/null +++ b/tools/native-hook-fragments/level_world.json @@ -0,0 +1,40 @@ +{ + "hooks": [], + "helpers": [ + { + "name": "level_seed", + "target": "horizon_client::game::native_level_world::Game<'_>::native_level_seed", + "receiver": "level" + }, + { + "name": "level_difficulty", + "target": "horizon_client::game::native_level_world::Game<'_>::native_level_difficulty", + "receiver": "level" + }, + { + "name": "level_game_rule", + "target": "horizon_client::game::native_level_world::Game<'_>::native_level_game_rule", + "receiver": "level" + }, + { + "name": "level_weather", + "target": "horizon_client::game::native_level_world::Game<'_>::native_level_weather", + "receiver": "level" + }, + { + "name": "level_is_raining", + "target": "horizon_client::game::native_level_world::Game<'_>::native_level_is_raining", + "receiver": "level" + }, + { + "name": "level_is_thundering", + "target": "horizon_client::game::native_level_world::Game<'_>::native_level_is_thundering", + "receiver": "level" + } + ], + "rust_summary": "a level's world settings through a `LevelRef`: the seed, the difficulty, game rules and weather.", + "doc_summary": "`level.seed(host)`, `difficulty`, `game_rule(host, name)` (`GameRuleValue`), `weather`, `is_raining` and `is_thundering`. Bundles need \"hooks\": @API:level_world@.", + "comments": [ + "crates/horizon-client/src/game/native_level_world.rs" + ] +}