From 41bcd1eb2c88a7615c5543094e58f759475eb5e1 Mon Sep 17 00:00:00 2001
From: Jenya705 <51133999+Jenya705@users.noreply.github.com>
Date: Tue, 2 May 2023 10:35:35 +0200
Subject: [PATCH] Advancement api (#329)

## Description

Did an api for advancements.
Issue: https://github.com/valence-rs/valence/issues/325

Each advancement is an entity, it's children is either criteria, either
advancement.
Root advancement has no parent.

Also did an event AdvancementTabChange (listens if client changes
advancement's tab)

## Test Plan

Use an example "advancements"
---
 Cargo.toml                                    |   2 +
 crates/README.md                              |   1 +
 crates/valence/Cargo.toml                     |   4 +-
 crates/valence/examples/advancement.rs        | 227 +++++++++
 crates/valence/src/lib.rs                     |  12 +
 crates/valence_advancement/Cargo.toml         |  13 +
 crates/valence_advancement/README.md          |   7 +
 crates/valence_advancement/src/event.rs       |  29 ++
 crates/valence_advancement/src/lib.rs         | 478 ++++++++++++++++++
 crates/valence_core/src/ident.rs              |   6 +
 .../src/packet/s2c/play/advancement_update.rs |  21 +-
 11 files changed, 790 insertions(+), 10 deletions(-)
 create mode 100644 crates/valence/examples/advancement.rs
 create mode 100644 crates/valence_advancement/Cargo.toml
 create mode 100644 crates/valence_advancement/README.md
 create mode 100644 crates/valence_advancement/src/event.rs
 create mode 100644 crates/valence_advancement/src/lib.rs

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<Server>,
+    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<Client>>,
+    instances: Query<Entity, With<Instance>>,
+) {
+    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<AdvancementClientUpdate>>,
+    root_advancement_query: Query<Entity, (Without<Parent>, With<Advancement>)>,
+    children_query: Query<&Children>,
+    advancement_check_query: Query<(), With<Advancement>>,
+) {
+    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<Sneaking>,
+    mut client: Query<(&mut AdvancementClientUpdate, &mut RootCriteriaDone)>,
+    root_criteria: Query<Entity, With<RootCriteria>>,
+) {
+    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<AdvancementTabChange>,
+    mut client: Query<(&mut AdvancementClientUpdate, &mut TabChangeCount)>,
+    root2_criteria: Query<Entity, With<Root2Criteria>>,
+    root: Query<Entity, With<RootAdvancement>>,
+) {
+    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<Ident<String>>,
+}
+
+pub(crate) fn handle_advancement_tab_change(
+    mut packets: EventReader<PacketEvent>,
+    mut advancement_tab_change_events: EventWriter<AdvancementTabChange>,
+) {
+    for packet in packets.iter() {
+        if let Some(pkt) = packet.decode::<AdvancementTabC2s>() {
+            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::<AdvancementTabChange>()
+        .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<Entity, Added<Client>>,
+) {
+    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<AdvancementDisplay>,
+            Changed<Children>,
+            Changed<Parent>,
+            Changed<AdvancementRequirements>,
+        )>,
+    >,
+    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<Entity, Vec<(Entity, Option<i64>)>> =
+            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<Self> {
+        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<Cow<'static, str>>);
+
+impl Advancement {
+    pub fn new(ident: Ident<Cow<'static, str>>) -> Advancement {
+        Self(ident)
+    }
+
+    pub fn get(&self) -> &Ident<Cow<'static, str>> {
+        &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<ItemStack>,
+    pub frame_type: AdvancementFrameType,
+    pub show_toast: bool,
+    pub hidden: bool,
+    pub background_texture: Option<Ident<Cow<'static, str>>>,
+    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<Cow<'static, str>>);
+
+impl AdvancementCriteria {
+    pub fn new(ident: Ident<Cow<'static, str>>) -> Self {
+        Self(ident)
+    }
+
+    pub fn get(&self) -> &Ident<Cow<'static, str>> {
+        &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<Vec<Entity>>);
+
+#[derive(Component, Default)]
+pub struct AdvancementCachedBytes(pub(crate) Vec<u8>);
+
+#[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<Entity>,
+    /// Which advancements remove from client
+    pub remove_advancements: Vec<Entity>,
+    /// Criteria progress update.
+    /// If None then criteria is not done otherwise it is done
+    pub progress: Vec<(Entity, Option<i64>)>,
+    /// 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<Advancement>>,
+        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<Advancement>>,
+    ) {
+        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<Advancement>>,
+    ) {
+        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<S> Ident<S> {
     }
 }
 
+impl<'a> Ident<Cow<'a, str>> {
+    pub fn borrowed(&self) -> Ident<Cow<str>> {
+        Ident::new_unchecked(Cow::Borrowed(self.as_str()))
+    }
+}
+
 fn parse(string: Cow<str>) -> Result<Ident<Cow<str>>, 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<Cow<'a, str>>, Advancement<'a, Option<ItemStack>>)>;
+
 #[derive(Clone, Debug, Encode, Decode)]
-pub struct AdvancementUpdateS2c<'a> {
+pub struct GenericAdvancementUpdateS2c<'a, AM: 'a> {
     pub reset: bool,
-    pub advancement_mapping: Vec<(Ident<Cow<'a, str>>, Advancement<'a>)>,
+    pub advancement_mapping: Vec<AM>,
     pub identifiers: Vec<Ident<Cow<'a, str>>>,
     pub progress_mapping: Vec<(Ident<Cow<'a, str>>, Vec<AdvancementCriteria<'a>>)>,
 }
 
 #[derive(Clone, PartialEq, Debug, Encode, Decode)]
-pub struct Advancement<'a> {
+pub struct Advancement<'a, I> {
     pub parent_id: Option<Ident<Cow<'a, str>>>,
-    pub display_data: Option<AdvancementDisplay<'a>>,
+    pub display_data: Option<AdvancementDisplay<'a, I>>,
     pub criteria: Vec<(Ident<Cow<'a, str>>, ())>,
     pub requirements: Vec<AdvancementRequirements<'a>>,
 }
@@ -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<ItemStack>,
+    pub icon: I,
     pub frame_type: VarInt,
     pub flags: i32,
     pub background_texture: Option<Ident<Cow<'a, str>>>,
@@ -48,7 +51,7 @@ pub struct AdvancementCriteria<'a> {
     pub criterion_progress: Option<i64>,
 }
 
-impl Encode for AdvancementDisplay<'_> {
+impl<I: Encode> 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<Self> {
         let title = <Cow<'a, Text>>::decode(r)?;
         let description = <Cow<'a, Text>>::decode(r)?;
-        let icon = Option::<ItemStack>::decode(r)?;
+        let icon = I::decode(r)?;
         let frame_type = VarInt::decode(r)?;
         let flags = i32::decode(r)?;