diff --git a/Cargo.toml b/Cargo.toml index 9f0ba57..f5e5e2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ atty = "0.2.14" base64 = "0.21.0" bevy_app = { version = "0.10.1", default-features = false } bevy_ecs = { version = "0.10.1", default-features = false, features = ["trace"] } +bevy_hierarchy = { version = "0.10.1", default-features = false } bevy_mod_debugdump = "0.7.0" bitfield-struct = "0.3.1" byteorder = "1.4.3" @@ -71,6 +72,7 @@ tracing = "0.1.37" tracing-subscriber = "0.3.16" url = { version = "2.2.2", features = ["serde"] } uuid = "1.3.1" +valence_advancement.path = "crates/valence_advancement" valence_anvil.path = "crates/valence_anvil" valence_biome.path = "crates/valence_biome" valence_block.path = "crates/valence_block" diff --git a/crates/README.md b/crates/README.md index a5456a3..f17011e 100644 --- a/crates/README.md +++ b/crates/README.md @@ -21,4 +21,5 @@ graph TD inventory --> client anvil --> instance entity --> block + advancement --> client ``` diff --git a/crates/valence/Cargo.toml b/crates/valence/Cargo.toml index 7c84c43..a69c885 100644 --- a/crates/valence/Cargo.toml +++ b/crates/valence/Cargo.toml @@ -11,11 +11,12 @@ keywords = ["minecraft", "gamedev", "server", "ecs"] categories = ["game-engines"] [features] -default = ["network", "player_list", "inventory", "anvil"] +default = ["network", "player_list", "inventory", "anvil", "advancement"] network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] inventory = ["dep:valence_inventory"] anvil = ["dep:valence_anvil"] +advancement = ["dep:valence_advancement"] [dependencies] bevy_app.workspace = true @@ -35,6 +36,7 @@ valence_network = { workspace = true, optional = true } valence_player_list = { workspace = true, optional = true } valence_inventory = { workspace = true, optional = true } valence_anvil = { workspace = true, optional = true } +valence_advancement = { workspace = true, optional = true } [dev-dependencies] anyhow.workspace = true diff --git a/crates/valence/examples/advancement.rs b/crates/valence/examples/advancement.rs new file mode 100644 index 0000000..37b67cc --- /dev/null +++ b/crates/valence/examples/advancement.rs @@ -0,0 +1,227 @@ +use valence::prelude::*; +use valence_advancement::bevy_hierarchy::{BuildChildren, Children, Parent}; +use valence_advancement::ForceTabUpdate; + +#[derive(Component)] +struct RootCriteria; + +#[derive(Component)] +struct Root2Criteria; + +#[derive(Component)] +struct RootAdvancement; + +#[derive(Component)] +struct RootCriteriaDone(bool); + +#[derive(Component)] +struct TabChangeCount(u8); + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_systems((init_clients, init_advancements, sneak, tab_change)) + .run(); +} + +fn setup( + mut commands: Commands, + server: Res, + dimensions: Query<&DimensionType>, + biomes: Query<&Biome>, +) { + let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); + } + } + + for z in -25..25 { + for x in -25..25 { + instance.set_block([x, 64, z], BlockState::GRASS_BLOCK); + } + } + + commands.spawn(instance); + + let root_criteria = commands + .spawn(( + AdvancementCriteria::new(ident!("custom:root_criteria").into()), + RootCriteria, + )) + .id(); + + let root_advancement = commands + .spawn(( + AdvancementBundle { + advancement: Advancement::new(ident!("custom:root").into()), + requirements: AdvancementRequirements(vec![vec![root_criteria]]), + cached_bytes: Default::default(), + }, + AdvancementDisplay { + title: "Root".into(), + description: "Toggles when you sneak".into(), + icon: Some(ItemStack::new(ItemKind::Stone, 1, None)), + frame_type: AdvancementFrameType::Task, + show_toast: true, + hidden: false, + background_texture: Some(ident!("textures/block/stone.png").into()), + x_coord: 0.0, + y_coord: 0.0, + }, + RootAdvancement, + )) + .add_child(root_criteria) + .id(); + + commands + .spawn(( + AdvancementBundle { + advancement: Advancement::new(ident!("custom:first").into()), + requirements: AdvancementRequirements::default(), + cached_bytes: Default::default(), + }, + AdvancementDisplay { + title: "First".into(), + description: "First advancement".into(), + icon: Some(ItemStack::new(ItemKind::OakWood, 1, None)), + frame_type: AdvancementFrameType::Task, + show_toast: false, + hidden: false, + background_texture: None, + x_coord: 1.0, + y_coord: -0.5, + }, + )) + .set_parent(root_advancement); + + commands + .spawn(( + AdvancementBundle { + advancement: Advancement::new(ident!("custom:second").into()), + requirements: AdvancementRequirements::default(), + cached_bytes: Default::default(), + }, + AdvancementDisplay { + title: "Second".into(), + description: "Second advancement".into(), + icon: Some(ItemStack::new(ItemKind::AcaciaWood, 1, None)), + frame_type: AdvancementFrameType::Task, + show_toast: false, + hidden: false, + background_texture: None, + x_coord: 1.0, + y_coord: 0.5, + }, + )) + .set_parent(root_advancement); + + let root2_criteria = commands + .spawn(( + AdvancementCriteria::new(ident!("custom:root2_criteria").into()), + Root2Criteria, + )) + .id(); + + commands + .spawn(( + AdvancementBundle { + advancement: Advancement::new(ident!("custom:root2").into()), + requirements: AdvancementRequirements(vec![vec![root2_criteria]]), + cached_bytes: Default::default(), + }, + AdvancementDisplay { + title: "Root2".into(), + description: "Go to this tab 5 times to earn this advancement".into(), + icon: Some(ItemStack::new(ItemKind::IronSword, 1, None)), + frame_type: AdvancementFrameType::Challenge, + show_toast: false, + hidden: false, + background_texture: Some(Ident::new("textures/block/andesite.png").unwrap()), + x_coord: 0.0, + y_coord: 0.0, + }, + )) + .add_child(root2_criteria); +} + +fn init_clients( + mut commands: Commands, + mut clients: Query<(Entity, &mut Location, &mut Position, &mut GameMode), Added>, + instances: Query>, +) { + for (client, mut loc, mut pos, mut game_mode) in &mut clients { + loc.0 = instances.single(); + pos.set([0.5, 65.0, 0.5]); + *game_mode = GameMode::Creative; + commands + .entity(client) + .insert((RootCriteriaDone(false), TabChangeCount(0))); + } +} + +fn init_advancements( + mut clients: Query<&mut AdvancementClientUpdate, Added>, + root_advancement_query: Query, With)>, + children_query: Query<&Children>, + advancement_check_query: Query<(), With>, +) { + for mut advancement_client_update in clients.iter_mut() { + for root_advancement in root_advancement_query.iter() { + advancement_client_update.send_advancements( + root_advancement, + &children_query, + &advancement_check_query, + ); + } + } +} + +fn sneak( + mut sneaking: EventReader, + mut client: Query<(&mut AdvancementClientUpdate, &mut RootCriteriaDone)>, + root_criteria: Query>, +) { + let root_criteria = root_criteria.single(); + for sneaking in sneaking.iter() { + if sneaking.state == SneakState::Stop { + continue; + } + let Ok((mut advancement_client_update, mut root_criteria_done)) = client.get_mut(sneaking.client) else { continue; }; + root_criteria_done.0 = !root_criteria_done.0; + match root_criteria_done.0 { + true => advancement_client_update.criteria_done(root_criteria), + false => advancement_client_update.criteria_undone(root_criteria), + } + } +} + +fn tab_change( + mut tab_change: EventReader, + mut client: Query<(&mut AdvancementClientUpdate, &mut TabChangeCount)>, + root2_criteria: Query>, + root: Query>, +) { + let root2_criteria = root2_criteria.single(); + let root = root.single(); + for tab_change in tab_change.iter() { + let Ok((mut advancement_client_update, mut tab_change_count)) = client.get_mut(tab_change.client) else { continue; }; + if let Some(ref opened) = tab_change.opened_tab { + if opened.as_str() == "custom:root2" { + tab_change_count.0 += 1; + } else { + continue; + } + } else { + continue; + } + if tab_change_count.0 == 5 { + advancement_client_update.criteria_done(root2_criteria); + } else if tab_change_count.0 >= 10 { + advancement_client_update.force_tab_update = ForceTabUpdate::Spec(root); + } + } +} diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index e316d0e..7a4b914 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -97,6 +97,11 @@ pub mod prelude { #[cfg(feature = "player_list")] pub use player_list::{PlayerList, PlayerListEntry}; pub use text::{Color, Text, TextFormat}; + #[cfg(feature = "advancement")] + pub use valence_advancement::{ + event::AdvancementTabChange, Advancement, AdvancementBundle, AdvancementClientUpdate, + AdvancementCriteria, AdvancementDisplay, AdvancementFrameType, AdvancementRequirements, + }; pub use valence_core::ident; // Export the `ident!` macro. pub use valence_core::uuid::UniqueId; pub use valence_core::{translation_key, CoreSettings, Server}; @@ -145,6 +150,13 @@ impl PluginGroup for DefaultPlugins { // No plugin... yet. } + #[cfg(feature = "advancement")] + { + group = group + .add(valence_advancement::AdvancementPlugin) + .add(valence_advancement::bevy_hierarchy::HierarchyPlugin); + } + group } } diff --git a/crates/valence_advancement/Cargo.toml b/crates/valence_advancement/Cargo.toml new file mode 100644 index 0000000..76d0aeb --- /dev/null +++ b/crates/valence_advancement/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "valence_advancement" +version.workspace = true +edition.workspace = true + +[dependencies] +valence_core.workspace = true +valence_client.workspace = true +bevy_app.workspace = true +bevy_ecs.workspace = true +bevy_hierarchy.workspace = true +rustc-hash.workspace = true +anyhow.workspace = true \ No newline at end of file diff --git a/crates/valence_advancement/README.md b/crates/valence_advancement/README.md new file mode 100644 index 0000000..4aa5d3a --- /dev/null +++ b/crates/valence_advancement/README.md @@ -0,0 +1,7 @@ +# valence_advancement + +Everything related to Minecraft advancements. + +### Warning +- Each advancement should be scheduled to be sent to each unique client. +- Advancement identifier is not mutable and changing it can cause bugs. \ No newline at end of file diff --git a/crates/valence_advancement/src/event.rs b/crates/valence_advancement/src/event.rs new file mode 100644 index 0000000..9226171 --- /dev/null +++ b/crates/valence_advancement/src/event.rs @@ -0,0 +1,29 @@ +use bevy_ecs::prelude::{Entity, EventReader, EventWriter}; +use valence_client::event_loop::PacketEvent; +use valence_core::ident::Ident; +use valence_core::packet::c2s::play::AdvancementTabC2s; + +/// This event sends when the client changes or closes advancement's tab. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct AdvancementTabChange { + pub client: Entity, + /// If None then the client has closed advancement's tabs. + pub opened_tab: Option>, +} + +pub(crate) fn handle_advancement_tab_change( + mut packets: EventReader, + mut advancement_tab_change_events: EventWriter, +) { + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + advancement_tab_change_events.send(AdvancementTabChange { + client: packet.client, + opened_tab: match pkt { + AdvancementTabC2s::ClosedScreen => None, + AdvancementTabC2s::OpenedTab { tab_id } => Some(tab_id.into()), + }, + }) + } + } +} diff --git a/crates/valence_advancement/src/lib.rs b/crates/valence_advancement/src/lib.rs new file mode 100644 index 0000000..df6abe7 --- /dev/null +++ b/crates/valence_advancement/src/lib.rs @@ -0,0 +1,478 @@ +#[doc = include_str!("../README.md")] +pub mod event; + +use std::borrow::Cow; +use std::io::Write; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::Context; +use bevy_app::{CoreSet, Plugin}; +use bevy_ecs::prelude::{Bundle, Component, Entity}; +use bevy_ecs::query::{Added, Changed, Or, With}; +use bevy_ecs::schedule::{IntoSystemConfig, IntoSystemSetConfig, SystemSet}; +use bevy_ecs::system::{Commands, Query, SystemParam}; +pub use bevy_hierarchy; +use bevy_hierarchy::{Children, Parent}; +use event::{handle_advancement_tab_change, AdvancementTabChange}; +use rustc_hash::FxHashMap; +use valence_client::{Client, FlushPacketsSet, SpawnClientsSet}; +use valence_core::ident::Ident; +use valence_core::item::ItemStack; +use valence_core::packet::encode::WritePacket; +use valence_core::packet::raw::RawBytes; +use valence_core::packet::s2c::play::advancement_update::GenericAdvancementUpdateS2c; +use valence_core::packet::s2c::play::{ + advancement_update as protocol, AdvancementUpdateS2c, SelectAdvancementTabS2c, +}; +use valence_core::packet::var_int::VarInt; +use valence_core::packet::{Encode, Packet}; +use valence_core::text::Text; + +pub struct AdvancementPlugin; + +#[derive(SystemSet, Clone, Copy, Eq, PartialEq, Hash, Debug)] +pub struct WriteAdvancementPacketToClientsSet; + +#[derive(SystemSet, Clone, Copy, Eq, PartialEq, Hash, Debug)] +pub struct WriteAdvancementToCacheSet; + +impl Plugin for AdvancementPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.configure_sets(( + WriteAdvancementPacketToClientsSet + .in_base_set(CoreSet::PostUpdate) + .before(FlushPacketsSet), + WriteAdvancementToCacheSet + .in_base_set(CoreSet::PostUpdate) + .before(WriteAdvancementPacketToClientsSet), + )) + .add_event::() + .add_system( + add_advancement_update_component_to_new_clients + .after(SpawnClientsSet) + .in_base_set(CoreSet::PreUpdate), + ) + .add_system(handle_advancement_tab_change.in_base_set(CoreSet::PreUpdate)) + .add_system(update_advancement_cached_bytes.in_set(WriteAdvancementToCacheSet)) + .add_system(send_advancement_update_packet.in_set(WriteAdvancementPacketToClientsSet)); + } +} + +/// Components for advancement that are required +/// Optional components: +/// [AdvancementDisplay] +/// [Parent] - parent advancement +#[derive(Bundle)] +pub struct AdvancementBundle { + pub advancement: Advancement, + pub requirements: AdvancementRequirements, + pub cached_bytes: AdvancementCachedBytes, +} + +fn add_advancement_update_component_to_new_clients( + mut commands: Commands, + query: Query>, +) { + for client in query.iter() { + commands + .entity(client) + .insert(AdvancementClientUpdate::default()); + } +} + +#[derive(SystemParam, Debug)] +#[allow(clippy::type_complexity)] +struct UpdateAdvancementCachedBytesQuery<'w, 's> { + advancement_id_query: Query<'w, 's, &'static Advancement>, + criteria_query: Query<'w, 's, &'static AdvancementCriteria>, +} + +impl<'w, 's> UpdateAdvancementCachedBytesQuery<'w, 's> { + fn write( + &self, + a_identifier: &Advancement, + a_requirements: &AdvancementRequirements, + a_display: Option<&AdvancementDisplay>, + a_children: Option<&Children>, + a_parent: Option<&Parent>, + w: impl Write, + ) -> anyhow::Result<()> { + let Self { + advancement_id_query, + criteria_query, + } = self; + + let mut pkt = protocol::Advancement { + parent_id: None, + display_data: None, + criteria: vec![], + requirements: vec![], + }; + + if let Some(a_parent) = a_parent { + let a_identifier = advancement_id_query.get(a_parent.get())?; + pkt.parent_id = Some(a_identifier.0.borrowed()); + } + + if let Some(a_display) = a_display { + pkt.display_data = Some(protocol::AdvancementDisplay { + title: Cow::Borrowed(&a_display.title), + description: Cow::Borrowed(&a_display.description), + icon: &a_display.icon, + frame_type: VarInt(a_display.frame_type as _), + flags: a_display.flags(), + background_texture: a_display.background_texture.as_ref().map(|v| v.borrowed()), + x_coord: a_display.x_coord, + y_coord: a_display.y_coord, + }); + } + + if let Some(a_children) = a_children { + for a_child in a_children.iter() { + let Ok(c_identifier) = criteria_query.get(*a_child) else { continue; }; + pkt.criteria.push((c_identifier.0.borrowed(), ())); + } + } + + for requirements in a_requirements.0.iter() { + let mut requirements_p = vec![]; + for requirement in requirements { + let c_identifier = criteria_query.get(*requirement)?; + requirements_p.push(c_identifier.0.as_str()); + } + pkt.requirements.push(protocol::AdvancementRequirements { + requirement: requirements_p, + }); + } + + (&a_identifier.0, pkt).encode(w) + } +} + +#[allow(clippy::type_complexity)] +fn update_advancement_cached_bytes( + mut query: Query< + ( + &Advancement, + &AdvancementRequirements, + &mut AdvancementCachedBytes, + Option<&AdvancementDisplay>, + Option<&Children>, + Option<&Parent>, + ), + Or<( + Changed, + Changed, + Changed, + Changed, + )>, + >, + update_advancement_cached_bytes_query: UpdateAdvancementCachedBytesQuery, +) { + for (a_identifier, a_requirements, mut a_bytes, a_display, a_children, a_parent) in + query.iter_mut() + { + a_bytes.0.clear(); + update_advancement_cached_bytes_query + .write( + a_identifier, + a_requirements, + a_display, + a_children, + a_parent, + &mut a_bytes.0, + ) + .expect("Failed to write an advancement"); + } +} + +#[derive(SystemParam, Debug)] +#[allow(clippy::type_complexity)] +pub(crate) struct SingleAdvancementUpdateQuery<'w, 's> { + advancement_bytes_query: Query<'w, 's, &'static AdvancementCachedBytes>, + advancement_id_query: Query<'w, 's, &'static Advancement>, + criteria_query: Query<'w, 's, &'static AdvancementCriteria>, + parent_query: Query<'w, 's, &'static Parent>, +} + +#[derive(Debug)] +pub(crate) struct AdvancementUpdateEncodeS2c<'w, 's, 'a> { + client_update: AdvancementClientUpdate, + queries: &'a SingleAdvancementUpdateQuery<'w, 's>, +} + +impl<'w, 's, 'a> Encode for AdvancementUpdateEncodeS2c<'w, 's, 'a> { + fn encode(&self, w: impl Write) -> anyhow::Result<()> { + let SingleAdvancementUpdateQuery { + advancement_bytes_query, + advancement_id_query, + criteria_query, + parent_query, + } = self.queries; + + let AdvancementClientUpdate { + new_advancements, + remove_advancements, + progress, + force_tab_update: _, + } = &self.client_update; + + let mut pkt = GenericAdvancementUpdateS2c { + reset: false, + advancement_mapping: vec![], + identifiers: vec![], + progress_mapping: vec![], + }; + + for new_advancement in new_advancements { + let a_cached_bytes = advancement_bytes_query.get(*new_advancement)?; + pkt.advancement_mapping + .push(RawBytes(a_cached_bytes.0.as_slice())); + } + + for remove_advancement in remove_advancements { + let a_identifier = advancement_id_query.get(*remove_advancement)?; + pkt.identifiers.push(a_identifier.0.borrowed()); + } + + let mut progress_mapping: FxHashMap)>> = + FxHashMap::default(); + for progress in progress { + let a = parent_query.get(progress.0)?; + progress_mapping + .entry(a.get()) + .and_modify(|v| v.push(*progress)) + .or_insert(vec![*progress]); + } + + for (a, c_progresses) in progress_mapping { + let a_identifier = advancement_id_query.get(a)?; + let mut c_progresses_p = vec![]; + for (c, c_progress) in c_progresses { + let c_identifier = criteria_query.get(c)?; + c_progresses_p.push(protocol::AdvancementCriteria { + criterion_identifier: c_identifier.0.borrowed(), + criterion_progress: c_progress, + }); + } + pkt.progress_mapping + .push((a_identifier.0.borrowed(), c_progresses_p)); + } + + pkt.encode(w) + } +} + +impl<'w, 's, 'a, 'b> Packet<'b> for AdvancementUpdateEncodeS2c<'w, 's, 'a> { + const PACKET_ID: i32 = AdvancementUpdateS2c::PACKET_ID; + + fn packet_id(&self) -> i32 { + Self::PACKET_ID + } + + fn packet_name(&self) -> &str { + "AdvancementUpdateEncodeS2c" + } + + fn encode_packet(&self, mut w: impl Write) -> anyhow::Result<()> { + VarInt(Self::PACKET_ID) + .encode(&mut w) + .context("failed to encode packet ID")?; + self.encode(w) + } + + fn decode_packet(_r: &mut &'b [u8]) -> anyhow::Result { + panic!("Packet can not be decoded") + } +} + +#[allow(clippy::type_complexity)] +fn send_advancement_update_packet( + mut client: Query<(&mut AdvancementClientUpdate, &mut Client)>, + update_single_query: SingleAdvancementUpdateQuery, +) { + for (mut advancement_client_update, mut client) in client.iter_mut() { + match advancement_client_update.force_tab_update { + ForceTabUpdate::None => {} + ForceTabUpdate::First => { + client.write_packet(&SelectAdvancementTabS2c { identifier: None }) + } + ForceTabUpdate::Spec(spec) => { + if let Ok(a_identifier) = update_single_query.advancement_id_query.get(spec) { + client.write_packet(&SelectAdvancementTabS2c { + identifier: Some(a_identifier.0.borrowed()), + }); + } + } + } + + if ForceTabUpdate::None != advancement_client_update.force_tab_update { + advancement_client_update.force_tab_update = ForceTabUpdate::None; + } + + if advancement_client_update.new_advancements.is_empty() + && advancement_client_update.progress.is_empty() + && advancement_client_update.remove_advancements.is_empty() + { + continue; + } + + let advancement_client_update = std::mem::take(advancement_client_update.as_mut()); + + client.write_packet(&AdvancementUpdateEncodeS2c { + queries: &update_single_query, + client_update: advancement_client_update, + }); + } +} + +/// Advancement's id. May not be updated. +#[derive(Component)] +pub struct Advancement(Ident>); + +impl Advancement { + pub fn new(ident: Ident>) -> Advancement { + Self(ident) + } + + pub fn get(&self) -> &Ident> { + &self.0 + } +} + +#[derive(Clone, Copy)] +pub enum AdvancementFrameType { + Task, + Challenge, + Goal, +} + +/// Advancement display. Optional component +#[derive(Component)] +pub struct AdvancementDisplay { + pub title: Text, + pub description: Text, + pub icon: Option, + pub frame_type: AdvancementFrameType, + pub show_toast: bool, + pub hidden: bool, + pub background_texture: Option>>, + pub x_coord: f32, + pub y_coord: f32, +} + +impl AdvancementDisplay { + pub(crate) fn flags(&self) -> i32 { + let mut flags = 0; + flags |= self.background_texture.is_some() as i32; + flags |= (self.show_toast as i32) << 1; + flags |= (self.hidden as i32) << 2; + flags + } +} + +/// Criteria's identifier. May not be updated +#[derive(Component)] +pub struct AdvancementCriteria(Ident>); + +impl AdvancementCriteria { + pub fn new(ident: Ident>) -> Self { + Self(ident) + } + + pub fn get(&self) -> &Ident> { + &self.0 + } +} + +/// Requirements for advancement to be completed. +/// All columns should be completed, column is completed when any of criteria in +/// this column is completed. +#[derive(Component, Default)] +pub struct AdvancementRequirements(pub Vec>); + +#[derive(Component, Default)] +pub struct AdvancementCachedBytes(pub(crate) Vec); + +#[derive(Default, Debug, PartialEq)] +pub enum ForceTabUpdate { + #[default] + None, + First, + /// Should contain only root advancement otherwise the first will be chosen + Spec(Entity), +} + +#[derive(Component, Default, Debug)] +pub struct AdvancementClientUpdate { + /// Which advancement's descriptions send to client + pub new_advancements: Vec, + /// Which advancements remove from client + pub remove_advancements: Vec, + /// Criteria progress update. + /// If None then criteria is not done otherwise it is done + pub progress: Vec<(Entity, Option)>, + /// Forces client to open a tab + pub force_tab_update: ForceTabUpdate, +} + +impl AdvancementClientUpdate { + pub(crate) fn walk_advancements( + root: Entity, + children_query: &Query<&Children>, + advancement_check_query: &Query<(), With>, + func: &mut impl FnMut(Entity), + ) { + func(root); + if let Ok(children) = children_query.get(root) { + for child in children.iter() { + let child = *child; + if advancement_check_query.get(child).is_ok() { + Self::walk_advancements(child, children_query, advancement_check_query, func); + } + } + } + } + + /// Sends all advancements from the root + pub fn send_advancements( + &mut self, + root: Entity, + children_query: &Query<&Children>, + advancement_check_query: &Query<(), With>, + ) { + Self::walk_advancements(root, children_query, advancement_check_query, &mut |e| { + self.new_advancements.push(e) + }); + } + + /// Removes all advancements from the root + pub fn remove_advancements( + &mut self, + root: Entity, + children_query: &Query<&Children>, + advancement_check_query: &Query<(), With>, + ) { + Self::walk_advancements(root, children_query, advancement_check_query, &mut |e| { + self.remove_advancements.push(e) + }); + } + + /// Marks criteria as done + pub fn criteria_done(&mut self, criteria: Entity) { + self.progress.push(( + criteria, + Some( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64, + ), + )) + } + + /// Marks criteria as undone + pub fn criteria_undone(&mut self, criteria: Entity) { + self.progress.push((criteria, None)) + } +} diff --git a/crates/valence_core/src/ident.rs b/crates/valence_core/src/ident.rs index 66df6a4..a4c56fc 100644 --- a/crates/valence_core/src/ident.rs +++ b/crates/valence_core/src/ident.rs @@ -132,6 +132,12 @@ impl Ident { } } +impl<'a> Ident> { + pub fn borrowed(&self) -> Ident> { + Ident::new_unchecked(Cow::Borrowed(self.as_str())) + } +} + fn parse(string: Cow) -> Result>, IdentError> { let check_namespace = |s: &str| { !s.is_empty() diff --git a/crates/valence_core/src/packet/s2c/play/advancement_update.rs b/crates/valence_core/src/packet/s2c/play/advancement_update.rs index 91bcd4e..468f0dc 100644 --- a/crates/valence_core/src/packet/s2c/play/advancement_update.rs +++ b/crates/valence_core/src/packet/s2c/play/advancement_update.rs @@ -7,18 +7,21 @@ use crate::packet::var_int::VarInt; use crate::packet::{Decode, Encode}; use crate::text::Text; +pub type AdvancementUpdateS2c<'a> = + GenericAdvancementUpdateS2c<'a, (Ident>, Advancement<'a, Option>)>; + #[derive(Clone, Debug, Encode, Decode)] -pub struct AdvancementUpdateS2c<'a> { +pub struct GenericAdvancementUpdateS2c<'a, AM: 'a> { pub reset: bool, - pub advancement_mapping: Vec<(Ident>, Advancement<'a>)>, + pub advancement_mapping: Vec, pub identifiers: Vec>>, pub progress_mapping: Vec<(Ident>, Vec>)>, } #[derive(Clone, PartialEq, Debug, Encode, Decode)] -pub struct Advancement<'a> { +pub struct Advancement<'a, I> { pub parent_id: Option>>, - pub display_data: Option>, + pub display_data: Option>, pub criteria: Vec<(Ident>, ())>, pub requirements: Vec>, } @@ -29,10 +32,10 @@ pub struct AdvancementRequirements<'a> { } #[derive(Clone, PartialEq, Debug)] -pub struct AdvancementDisplay<'a> { +pub struct AdvancementDisplay<'a, I> { pub title: Cow<'a, Text>, pub description: Cow<'a, Text>, - pub icon: Option, + pub icon: I, pub frame_type: VarInt, pub flags: i32, pub background_texture: Option>>, @@ -48,7 +51,7 @@ pub struct AdvancementCriteria<'a> { pub criterion_progress: Option, } -impl Encode for AdvancementDisplay<'_> { +impl Encode for AdvancementDisplay<'_, I> { fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { self.title.encode(&mut w)?; self.description.encode(&mut w)?; @@ -68,11 +71,11 @@ impl Encode for AdvancementDisplay<'_> { } } -impl<'a> Decode<'a> for AdvancementDisplay<'a> { +impl<'a, I: Decode<'a>> Decode<'a> for AdvancementDisplay<'a, I> { fn decode(r: &mut &'a [u8]) -> anyhow::Result { let title = >::decode(r)?; let description = >::decode(r)?; - let icon = Option::::decode(r)?; + let icon = I::decode(r)?; let frame_type = VarInt::decode(r)?; let flags = i32::decode(r)?;