From 393b05ea3bd62b60ff9e5f6355e9a107232c1081 Mon Sep 17 00:00:00 2001 From: drakeerv Date: Thu, 30 Jan 2025 22:34:14 -0500 Subject: [PATCH 1/9] Initial experience support For some reason I am getting link errors --- .../src/client/play/c_set_experience.rs | 23 +++ pumpkin-protocol/src/client/play/mod.rs | 2 + pumpkin/src/command/commands/experience.rs | 184 ++++++++++++++++++ pumpkin/src/command/commands/mod.rs | 1 + pumpkin/src/command/mod.rs | 5 +- pumpkin/src/entity/player.rs | 37 +++- 6 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 pumpkin-protocol/src/client/play/c_set_experience.rs create mode 100644 pumpkin/src/command/commands/experience.rs diff --git a/pumpkin-protocol/src/client/play/c_set_experience.rs b/pumpkin-protocol/src/client/play/c_set_experience.rs new file mode 100644 index 00000000..0e565a56 --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_set_experience.rs @@ -0,0 +1,23 @@ +use pumpkin_data::packet::clientbound::PLAY_SET_EXPERIENCE; +use pumpkin_macros::client_packet; +use serde::Serialize; + +use crate::VarInt; + +#[derive(Serialize)] +#[client_packet(PLAY_SET_EXPERIENCE)] +pub struct CSetExperience { + progress: f32, + level: VarInt, + total_experience: VarInt, +} + +impl CSetExperience { + pub fn new(progress: f32, level: VarInt, total_experience: VarInt) -> Self { + Self { + progress, + level, + total_experience, + } + } +} diff --git a/pumpkin-protocol/src/client/play/mod.rs b/pumpkin-protocol/src/client/play/mod.rs index c166a587..ffbd3c30 100644 --- a/pumpkin-protocol/src/client/play/mod.rs +++ b/pumpkin-protocol/src/client/play/mod.rs @@ -50,6 +50,7 @@ mod c_set_border_warning_distance; mod c_set_container_content; mod c_set_container_property; mod c_set_container_slot; +mod c_set_experience; mod c_set_health; mod c_set_held_item; mod c_set_time; @@ -122,6 +123,7 @@ pub use c_set_border_warning_distance::*; pub use c_set_container_content::*; pub use c_set_container_property::*; pub use c_set_container_slot::*; +pub use c_set_experience::*; pub use c_set_health::*; pub use c_set_held_item::*; pub use c_set_time::*; diff --git a/pumpkin/src/command/commands/experience.rs b/pumpkin/src/command/commands/experience.rs new file mode 100644 index 00000000..0ff12cf7 --- /dev/null +++ b/pumpkin/src/command/commands/experience.rs @@ -0,0 +1,184 @@ +use async_trait::async_trait; +use pumpkin_util::text::TextComponent; + +use crate::command::args::bounded_num::BoundedNumArgumentConsumer; +use crate::command::args::players::PlayersArgumentConsumer; +use crate::command::args::{ConsumedArgs, FindArg}; +use crate::command::tree::CommandTree; +use crate::command::tree_builder::{argument, literal}; +use crate::command::{CommandError, CommandExecutor, CommandSender}; + +const NAMES: [&str; 2] = ["experience", "xp"]; +const DESCRIPTION: &str = "Add, set or query player experience."; +const ARG_TARGETS: &str = "targets"; +const ARG_AMOUNT: &str = "amount"; + +fn xp_amount() -> BoundedNumArgumentConsumer { + BoundedNumArgumentConsumer::new() + .name(ARG_AMOUNT) + .min(0) + .max(i32::MAX) +} + +#[derive(Clone, Copy, PartialEq)] +enum Mode { + Add, + Set, + Query, +} + +struct ExperienceExecutor(Mode); + +#[async_trait] +impl CommandExecutor for ExperienceExecutor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + _server: &crate::server::Server, + args: &ConsumedArgs<'a>, + ) -> Result<(), CommandError> { + // Get target players + let targets = PlayersArgumentConsumer::find_arg(args, ARG_TARGETS)?; + + match self.0 { + Mode::Query => { + for target in targets { + let level = target + .experience_level + .load(std::sync::atomic::Ordering::Relaxed); + let progress = target.experience_progress.load(); + let total = target + .total_experience + .load(std::sync::atomic::Ordering::Relaxed); + + // Send both levels and points queries + sender + .send_message(TextComponent::translate( + "commands.experience.query.levels", + [ + TextComponent::text(target.gameprofile.name.clone()), + TextComponent::text(level.to_string()), + ] + .into(), + )) + .await; + + sender + .send_message(TextComponent::translate( + "commands.experience.query.points", + [ + TextComponent::text(target.gameprofile.name.clone()), + TextComponent::text(total.to_string()), + ] + .into(), + )) + .await; + } + } + Mode::Add | Mode::Set => { + // Use proper FindArg trait function call and handle both error cases + let amount = match BoundedNumArgumentConsumer::::find_arg(args, ARG_AMOUNT) { + Ok(Ok(value)) => value, + Ok(Err(_)) => { + sender + .send_message(TextComponent::translate( + "commands.experience.set.points.invalid", + [].into(), + )) + .await; + return Ok(()); + } + Err(e) => return Err(e), + }; + + for target in targets { + let current_level = target + .experience_level + .load(std::sync::atomic::Ordering::Relaxed); + let new_level = if self.0 == Mode::Add { + current_level + amount + } else { + amount + }; + + // Ensure level doesn't go below 0 + if new_level < 0 { + sender + .send_message(TextComponent::translate( + "commands.experience.set.points.invalid", + [].into(), + )) + .await; + continue; + } + + target.set_experience(new_level, 0.0, new_level * 100).await; + + // Send appropriate success message based on operation and number of targets + let msg = if self.0 == Mode::Add { + if targets.len() > 1 { + TextComponent::translate( + "commands.experience.add.levels.success.multiple", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(targets.len().to_string()), + ] + .into(), + ) + } else { + TextComponent::translate( + "commands.experience.add.levels.success.single", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(target.gameprofile.name.clone()), + ] + .into(), + ) + } + } else if targets.len() > 1 { + TextComponent::translate( + "commands.experience.set.levels.success.multiple", + [ + TextComponent::text(new_level.to_string()), + TextComponent::text(targets.len().to_string()), + ] + .into(), + ) + } else { + TextComponent::translate( + "commands.experience.set.levels.success.single", + [ + TextComponent::text(new_level.to_string()), + TextComponent::text(target.gameprofile.name.clone()), + ] + .into(), + ) + }; + sender.send_message(msg).await; + break; // Only send message once for multiple targets + } + } + } + + Ok(()) + } +} + +pub fn init_command_tree() -> CommandTree { + CommandTree::new(NAMES, DESCRIPTION) + .then( + literal("add") + .then(argument(ARG_TARGETS, PlayersArgumentConsumer).then( + argument(ARG_AMOUNT, xp_amount()).execute(ExperienceExecutor(Mode::Add)), + )), + ) + .then( + literal("set") + .then(argument(ARG_TARGETS, PlayersArgumentConsumer).then( + argument(ARG_AMOUNT, xp_amount()).execute(ExperienceExecutor(Mode::Set)), + )), + ) + .then(literal("query").then( + argument(ARG_TARGETS, PlayersArgumentConsumer).execute(ExperienceExecutor(Mode::Query)), + )) +} diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index 77f9ec21..0c1cf6e4 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -4,6 +4,7 @@ pub mod banlist; pub mod bossbar; pub mod clear; pub mod deop; +pub mod experience; pub mod fill; pub mod gamemode; pub mod give; diff --git a/pumpkin/src/command/mod.rs b/pumpkin/src/command/mod.rs index 0ac970db..61e9ce92 100644 --- a/pumpkin/src/command/mod.rs +++ b/pumpkin/src/command/mod.rs @@ -10,9 +10,7 @@ use crate::world::World; use args::ConsumedArgs; use async_trait::async_trait; use commands::{ - ban, banip, banlist, clear, deop, fill, gamemode, give, help, kick, kill, list, me, msg, op, - pardon, pardonip, playsound, plugin, plugins, pumpkin, say, setblock, stop, summon, teleport, - time, title, worldborder, + ban, banip, banlist, clear, deop, experience, fill, gamemode, give, help, kick, kill, list, me, msg, op, pardon, pardonip, playsound, plugin, plugins, pumpkin, say, setblock, stop, summon, teleport, time, title, worldborder }; use dispatcher::CommandError; use pumpkin_util::math::vector3::Vector3; @@ -144,6 +142,7 @@ pub fn default_dispatcher() -> CommandDispatcher { dispatcher.register(banlist::init_command_tree(), PermissionLvl::Three); dispatcher.register(pardon::init_command_tree(), PermissionLvl::Three); dispatcher.register(pardonip::init_command_tree(), PermissionLvl::Three); + dispatcher.register(experience::init_command_tree(), PermissionLvl::Two); dispatcher } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 282d99b2..0575b0d7 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -21,7 +21,8 @@ use pumpkin_protocol::{ client::play::{ CActionBar, CCombatDeath, CDisguisedChatMessage, CEntityStatus, CGameEvent, CHurtAnimation, CKeepAlive, CPlayDisconnect, CPlayerAbilities, CPlayerInfoUpdate, CPlayerPosition, - CSetHealth, CSubtitle, CSystemChatMessage, CTitleText, GameEvent, PlayerAction, + CSetExperience, CSetHealth, CSubtitle, CSystemChatMessage, CTitleText, GameEvent, + PlayerAction, }, server::play::{ SChatCommand, SChatMessage, SClientCommand, SClientInformationPlay, SClientTickEnd, @@ -135,6 +136,12 @@ pub struct Player { pub client_loaded: AtomicBool, /// timeout (in ticks) client has to report it has finished loading. pub client_loaded_timeout: AtomicU32, + /// The player's experience level + pub experience_level: AtomicI32, + /// The player's experience progress (0.0 to 1.0) + pub experience_progress: AtomicCell, + /// The player's total experience points + pub total_experience: AtomicI32, } impl Player { @@ -216,6 +223,9 @@ impl Player { |op| AtomicCell::new(op.level), ), inventory: Mutex::new(PlayerInventory::new()), + experience_level: AtomicI32::new(0), + experience_progress: AtomicCell::new(0.0), + total_experience: AtomicI32::new(0), } } @@ -724,7 +734,23 @@ impl Player { .send_packet(&CSystemChatMessage::new(text, overlay)) .await; } + + /// Sets the player's experience level and updates the client + pub async fn set_experience(&self, level: i32, progress: f32, total_exp: i32) { + self.experience_level.store(level, Ordering::Relaxed); + self.experience_progress.store(progress.clamp(0.0, 1.0)); + self.total_experience.store(total_exp, Ordering::Relaxed); + + self.client + .send_packet(&CSetExperience::new( + progress.clamp(0.0, 1.0), + total_exp.into(), + level.into(), + )) + .await; + } } + #[async_trait] impl NBTStorage for Player { async fn write_nbt(&self, nbt: &mut NbtCompound) { @@ -734,12 +760,21 @@ impl NBTStorage for Player { self.inventory.lock().await.selected as i32, ); self.abilities.lock().await.write_nbt(nbt).await; + nbt.put_int("XpLevel", self.experience_level.load(Ordering::Relaxed)); + nbt.put_float("XpProgress", self.experience_progress.load()); + nbt.put_int("XpTotal", self.total_experience.load(Ordering::Relaxed)); } async fn read_nbt(&mut self, nbt: &mut NbtCompound) { self.living_entity.read_nbt(nbt).await; self.inventory.lock().await.selected = nbt.get_int("SelectedItemSlot").unwrap_or(0) as u32; self.abilities.lock().await.read_nbt(nbt).await; + self.experience_level + .store(nbt.get_int("XpLevel").unwrap_or(0), Ordering::Relaxed); + self.experience_progress + .store(nbt.get_float("XpProgress").unwrap_or(0.0)); + self.total_experience + .store(nbt.get_int("XpTotal").unwrap_or(0), Ordering::Relaxed); } } From e72428672a1b3437f2786171002362f1298285f2 Mon Sep 17 00:00:00 2001 From: drakeerv Date: Thu, 30 Jan 2025 23:00:36 -0500 Subject: [PATCH 2/9] Implement experience math --- pumpkin-util/src/math/experience.rs | 34 +++ pumpkin-util/src/math/mod.rs | 1 + pumpkin/src/command/commands/experience.rs | 303 ++++++++++++++------- pumpkin/src/entity/player.rs | 40 +++ 4 files changed, 277 insertions(+), 101 deletions(-) create mode 100644 pumpkin-util/src/math/experience.rs diff --git a/pumpkin-util/src/math/experience.rs b/pumpkin-util/src/math/experience.rs new file mode 100644 index 00000000..46309d62 --- /dev/null +++ b/pumpkin-util/src/math/experience.rs @@ -0,0 +1,34 @@ +/// Returns the amount of experience required to reach the next level from a given level +pub fn get_exp_to_next_level(current_level: i32) -> i32 { + match current_level { + 0..=15 => 2 * current_level + 7, + 16..=30 => 5 * current_level - 38, + _ => 9 * current_level - 158, + } +} + +/// Returns the total amount of experience points required to reach a specific level +pub fn get_total_exp_to_level(level: i32) -> i32 { + match level { + 0..=16 => level * level + 6 * level, + 17..=31 => ((2.5 * f64::from(level * level)) - (40.5 * f64::from(level)) + 360.0) as i32, + _ => ((4.5 * f64::from(level * level)) - (162.5 * f64::from(level)) + 2220.0) as i32, + } +} + +/// Calculates level from total experience points +pub fn get_level_from_total_exp(total_exp: i32) -> i32 { + match total_exp { + 0..=352 => ((total_exp as f64 + 9.0).sqrt() - 3.0) as i32, + 353..=1507 => (81.0 + (total_exp as f64 - 7839.0) / 40.0).sqrt() as i32, + _ => (325.0 + (total_exp as f64 - 54215.0) / 72.0).sqrt() as i32, + } +} + +/// Calculate experience progress (0.0 to 1.0) for a given total experience amount +pub fn get_progress_from_total_exp(total_exp: i32) -> f32 { + let level = get_level_from_total_exp(total_exp); + let next_level_exp = get_total_exp_to_level(level + 1); + let current_level_exp = get_total_exp_to_level(level); + (total_exp - current_level_exp) as f32 / (next_level_exp - current_level_exp) as f32 +} diff --git a/pumpkin-util/src/math/mod.rs b/pumpkin-util/src/math/mod.rs index 6f49757f..345fbd8f 100644 --- a/pumpkin-util/src/math/mod.rs +++ b/pumpkin-util/src/math/mod.rs @@ -1,6 +1,7 @@ use num_traits::PrimInt; pub mod boundingbox; +pub mod experience; pub mod position; pub mod vector2; pub mod vector3; diff --git a/pumpkin/src/command/commands/experience.rs b/pumpkin/src/command/commands/experience.rs index 0ff12cf7..300d7dd1 100644 --- a/pumpkin/src/command/commands/experience.rs +++ b/pumpkin/src/command/commands/experience.rs @@ -1,5 +1,8 @@ +use std::sync::atomic::Ordering; + use async_trait::async_trait; use pumpkin_util::text::TextComponent; +use pumpkin_util::math::experience; use crate::command::args::bounded_num::BoundedNumArgumentConsumer; use crate::command::args::players::PlayersArgumentConsumer; @@ -27,7 +30,16 @@ enum Mode { Query, } -struct ExperienceExecutor(Mode); +#[derive(Clone, Copy, PartialEq)] +enum ExpType { + Points, + Levels, +} + +struct ExperienceExecutor { + mode: Mode, + exp_type: Option, +} #[async_trait] impl CommandExecutor for ExperienceExecutor { @@ -40,46 +52,48 @@ impl CommandExecutor for ExperienceExecutor { // Get target players let targets = PlayersArgumentConsumer::find_arg(args, ARG_TARGETS)?; - match self.0 { + match self.mode { Mode::Query => { - for target in targets { - let level = target - .experience_level - .load(std::sync::atomic::Ordering::Relaxed); - let progress = target.experience_progress.load(); - let total = target - .total_experience - .load(std::sync::atomic::Ordering::Relaxed); - - // Send both levels and points queries - sender - .send_message(TextComponent::translate( - "commands.experience.query.levels", - [ - TextComponent::text(target.gameprofile.name.clone()), - TextComponent::text(level.to_string()), - ] - .into(), - )) - .await; + // For query mode, we can only target a single player + if targets.len() != 1 { + // TODO: Add proper error message for multiple players in query mode + return Ok(()); + } - sender - .send_message(TextComponent::translate( - "commands.experience.query.points", - [ - TextComponent::text(target.gameprofile.name.clone()), - TextComponent::text(total.to_string()), - ] - .into(), - )) - .await; + let target = &targets[0]; + match self.exp_type.unwrap() { + ExpType::Levels => { + let level = target.experience_level.load(Ordering::Relaxed); + sender + .send_message(TextComponent::translate( + "commands.experience.query.levels", + [ + TextComponent::text(target.gameprofile.name.clone()), + TextComponent::text(level.to_string()), + ] + .into(), + )) + .await; + } + ExpType::Points => { + let total = target.total_experience.load(Ordering::Relaxed); + sender + .send_message(TextComponent::translate( + "commands.experience.query.points", + [ + TextComponent::text(target.gameprofile.name.clone()), + TextComponent::text(total.to_string()), + ] + .into(), + )) + .await; + } } } Mode::Add | Mode::Set => { - // Use proper FindArg trait function call and handle both error cases - let amount = match BoundedNumArgumentConsumer::::find_arg(args, ARG_AMOUNT) { - Ok(Ok(value)) => value, - Ok(Err(_)) => { + let amount = match BoundedNumArgumentConsumer::::find_arg(args, ARG_AMOUNT)? { + Ok(value) => value, + Err(_) => { sender .send_message(TextComponent::translate( "commands.experience.set.points.invalid", @@ -88,74 +102,127 @@ impl CommandExecutor for ExperienceExecutor { .await; return Ok(()); } - Err(e) => return Err(e), }; - for target in targets { - let current_level = target - .experience_level - .load(std::sync::atomic::Ordering::Relaxed); - let new_level = if self.0 == Mode::Add { - current_level + amount - } else { - amount - }; + if self.mode == Mode::Set && amount < 0 { + sender + .send_message(TextComponent::translate( + "commands.experience.set.points.invalid", + [].into(), + )) + .await; + return Ok(()); + } - // Ensure level doesn't go below 0 - if new_level < 0 { - sender - .send_message(TextComponent::translate( - "commands.experience.set.points.invalid", - [].into(), - )) - .await; - continue; - } + for target in targets.iter() { + match self.exp_type.unwrap() { + ExpType::Levels => { + let current_level = target.experience_level.load(Ordering::Relaxed); + let new_level = if self.mode == Mode::Add { + current_level + amount + } else { + amount + }; - target.set_experience(new_level, 0.0, new_level * 100).await; + if new_level < 0 { + sender + .send_message(TextComponent::translate( + "commands.experience.set.points.invalid", + [].into(), + )) + .await; + continue; + } - // Send appropriate success message based on operation and number of targets - let msg = if self.0 == Mode::Add { - if targets.len() > 1 { - TextComponent::translate( - "commands.experience.add.levels.success.multiple", - [ - TextComponent::text(amount.to_string()), - TextComponent::text(targets.len().to_string()), - ] - .into(), - ) - } else { - TextComponent::translate( - "commands.experience.add.levels.success.single", - [ - TextComponent::text(amount.to_string()), - TextComponent::text(target.gameprofile.name.clone()), - ] - .into(), - ) + target.set_level(new_level).await; + } + ExpType::Points => { + if self.mode == Mode::Add { + target.add_experience(amount).await; + } else { + let level = experience::get_level_from_total_exp(amount); + let progress = experience::get_progress_from_total_exp(amount); + target.set_experience(level, progress, amount).await; + } } - } else if targets.len() > 1 { - TextComponent::translate( - "commands.experience.set.levels.success.multiple", - [ - TextComponent::text(new_level.to_string()), - TextComponent::text(targets.len().to_string()), - ] - .into(), - ) - } else { - TextComponent::translate( - "commands.experience.set.levels.success.single", - [ - TextComponent::text(new_level.to_string()), - TextComponent::text(target.gameprofile.name.clone()), - ] - .into(), - ) + } + + // Send appropriate success message + let msg = match (self.mode, self.exp_type.unwrap()) { + (Mode::Add, ExpType::Points) => { + if targets.len() > 1 { + TextComponent::translate( + "commands.experience.add.points.success.multiple", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(targets.len().to_string()), + ] + .into(), + ) + } else { + TextComponent::translate( + "commands.experience.add.points.success.single", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(target.gameprofile.name.clone()), + ] + .into(), + ) + } + } + (Mode::Add, ExpType::Levels) => { + if targets.len() > 1 { + TextComponent::translate( + "commands.experience.add.levels.success.multiple", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(targets.len().to_string()), + ] + .into(), + ) + } else { + TextComponent::translate( + "commands.experience.add.levels.success.single", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(target.gameprofile.name.clone()), + ] + .into(), + ) + } + } + (Mode::Set, exp_type) => { + if targets.len() > 1 { + TextComponent::translate( + if exp_type == ExpType::Levels { + "commands.experience.set.levels.success.multiple" + } else { + "commands.experience.set.points.success.multiple" + }, + [ + TextComponent::text(amount.to_string()), + TextComponent::text(targets.len().to_string()), + ] + .into(), + ) + } else { + TextComponent::translate( + if exp_type == ExpType::Levels { + "commands.experience.set.levels.success.single" + } else { + "commands.experience.set.points.success.single" + }, + [ + TextComponent::text(amount.to_string()), + TextComponent::text(target.gameprofile.name.clone()), + ] + .into(), + ) + } + } + _ => unreachable!(), }; sender.send_message(msg).await; - break; // Only send message once for multiple targets } } } @@ -169,16 +236,50 @@ pub fn init_command_tree() -> CommandTree { .then( literal("add") .then(argument(ARG_TARGETS, PlayersArgumentConsumer).then( - argument(ARG_AMOUNT, xp_amount()).execute(ExperienceExecutor(Mode::Add)), + argument(ARG_AMOUNT, xp_amount()) + .then(literal("levels").execute(ExperienceExecutor { + mode: Mode::Add, + exp_type: Some(ExpType::Levels), + })) + .then(literal("points").execute(ExperienceExecutor { + mode: Mode::Add, + exp_type: Some(ExpType::Points), + })) + .execute(ExperienceExecutor { + mode: Mode::Add, + exp_type: Some(ExpType::Points), + }), )), ) .then( literal("set") .then(argument(ARG_TARGETS, PlayersArgumentConsumer).then( - argument(ARG_AMOUNT, xp_amount()).execute(ExperienceExecutor(Mode::Set)), + argument(ARG_AMOUNT, xp_amount()) + .then(literal("levels").execute(ExperienceExecutor { + mode: Mode::Set, + exp_type: Some(ExpType::Levels), + })) + .then(literal("points").execute(ExperienceExecutor { + mode: Mode::Set, + exp_type: Some(ExpType::Points), + })) + .execute(ExperienceExecutor { + mode: Mode::Set, + exp_type: Some(ExpType::Points), + }), )), ) - .then(literal("query").then( - argument(ARG_TARGETS, PlayersArgumentConsumer).execute(ExperienceExecutor(Mode::Query)), - )) + .then( + literal("query").then( + argument(ARG_TARGETS, PlayersArgumentConsumer) + .then(literal("levels").execute(ExperienceExecutor { + mode: Mode::Query, + exp_type: Some(ExpType::Levels), + })) + .then(literal("points").execute(ExperienceExecutor { + mode: Mode::Query, + exp_type: Some(ExpType::Points), + })), + ), + ) } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 0575b0d7..eacfd741 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -55,6 +55,7 @@ use pumpkin_util::{ text::TextComponent, GameMode, }; +use pumpkin_util::math::experience; use pumpkin_world::{ cylindrical_chunk_iterator::Cylindrical, item::{ @@ -749,6 +750,45 @@ impl Player { )) .await; } + + /// Returns the amount of experience required to reach the next level from the current level + pub fn get_exp_to_next_level(&self) -> i32 { + experience::get_exp_to_next_level(self.experience_level.load(Ordering::Relaxed)) + } + + /// Adds experience points to the player + pub async fn add_experience(&self, exp: i32) { + let current_total = self.total_experience.load(Ordering::Relaxed); + let new_total = current_total + exp; + let new_level = experience::get_level_from_total_exp(new_total); + let progress = experience::get_progress_from_total_exp(new_total); + + self.set_experience(new_level, progress, new_total).await; + } + + /// Sets the player's experience level directly + pub async fn set_level(&self, level: i32) { + let total_exp = experience::get_total_exp_to_level(level); + self.set_experience(level, 0.0, total_exp).await; + } + + /// Returns the total amount of experience points required to reach a specific level + pub fn get_total_exp_to_level(level: i32) -> i32 { + match level { + 0..=16 => level * level + 6 * level, + 17..=31 => ((2.5 * f64::from(level * level)) - (40.5 * f64::from(level)) + 360.0) as i32, + _ => ((4.5 * f64::from(level * level)) - (162.5 * f64::from(level)) + 2220.0) as i32, + } + } + + /// Calculates level from total experience points + pub fn get_level_from_total_exp(total_exp: i32) -> i32 { + match total_exp { + 0..=352 => ((total_exp as f64 + 9.0).sqrt() - 3.0) as i32, + 353..=1507 => (81.0 + (total_exp as f64 - 7839.0) / 40.0).sqrt() as i32, + _ => (325.0 + (total_exp as f64 - 54215.0) / 72.0).sqrt() as i32, + } + } } #[async_trait] From ca4de426f2b18cf7900518f13a682e240042bf67 Mon Sep 17 00:00:00 2001 From: drakeerv Date: Thu, 30 Jan 2025 23:26:05 -0500 Subject: [PATCH 3/9] Fix packet and format/clippy --- pumpkin/src/command/commands/experience.rs | 303 ++++++++++----------- pumpkin/src/command/mod.rs | 4 +- pumpkin/src/entity/player.rs | 29 +- 3 files changed, 171 insertions(+), 165 deletions(-) diff --git a/pumpkin/src/command/commands/experience.rs b/pumpkin/src/command/commands/experience.rs index 300d7dd1..c7f7fb92 100644 --- a/pumpkin/src/command/commands/experience.rs +++ b/pumpkin/src/command/commands/experience.rs @@ -1,8 +1,8 @@ use std::sync::atomic::Ordering; use async_trait::async_trait; -use pumpkin_util::text::TextComponent; use pumpkin_util::math::experience; +use pumpkin_util::text::TextComponent; use crate::command::args::bounded_num::BoundedNumArgumentConsumer; use crate::command::args::players::PlayersArgumentConsumer; @@ -10,6 +10,7 @@ use crate::command::args::{ConsumedArgs, FindArg}; use crate::command::tree::CommandTree; use crate::command::tree_builder::{argument, literal}; use crate::command::{CommandError, CommandExecutor, CommandSender}; +use crate::entity::player::Player; const NAMES: [&str; 2] = ["experience", "xp"]; const DESCRIPTION: &str = "Add, set or query player experience."; @@ -41,6 +42,113 @@ struct ExperienceExecutor { exp_type: Option, } +impl ExperienceExecutor { + async fn handle_query( + &self, + sender: &mut CommandSender<'_>, + target: &Player, + exp_type: ExpType, + ) { + match exp_type { + ExpType::Levels => { + let level = target.experience_level.load(Ordering::Relaxed); + sender + .send_message(TextComponent::translate( + "commands.experience.query.levels", + [ + TextComponent::text(target.gameprofile.name.clone()), + TextComponent::text(level.to_string()), + ] + .into(), + )) + .await; + } + ExpType::Points => { + let total = target.total_experience.load(Ordering::Relaxed); + sender + .send_message(TextComponent::translate( + "commands.experience.query.points", + [ + TextComponent::text(target.gameprofile.name.clone()), + TextComponent::text(total.to_string()), + ] + .into(), + )) + .await; + } + } + } + + fn get_success_message( + mode: Mode, + exp_type: ExpType, + amount: i32, + targets_len: usize, + target_name: Option, + ) -> TextComponent { + match (mode, exp_type) { + (Mode::Add, ExpType::Points) => { + if targets_len > 1 { + TextComponent::translate( + "commands.experience.add.points.success.multiple", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(targets_len.to_string()), + ] + .into(), + ) + } else { + TextComponent::translate( + "commands.experience.add.points.success.single", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(target_name.unwrap()), + ] + .into(), + ) + } + } + _ => TextComponent::text(""), // Unreachable + } + } + + async fn handle_modify( + &self, + _sender: &mut CommandSender<'_>, + target: &Player, + amount: i32, + exp_type: ExpType, + mode: Mode, + ) -> bool { + match exp_type { + ExpType::Levels => { + let current_level = target.experience_level.load(Ordering::Relaxed); + let new_level = if mode == Mode::Add { + current_level + amount + } else { + amount + }; + + if new_level < 0 { + return false; + } + + target.set_level(new_level).await; + } + ExpType::Points => { + if mode == Mode::Add { + target.add_experience(amount).await; + } else { + let level = experience::get_level_from_total_exp(amount); + let progress = experience::get_progress_from_total_exp(amount); + target.set_experience(level, progress, amount).await; + } + } + } + true + } +} + #[async_trait] impl CommandExecutor for ExperienceExecutor { async fn execute<'a>( @@ -49,59 +157,27 @@ impl CommandExecutor for ExperienceExecutor { _server: &crate::server::Server, args: &ConsumedArgs<'a>, ) -> Result<(), CommandError> { - // Get target players let targets = PlayersArgumentConsumer::find_arg(args, ARG_TARGETS)?; match self.mode { Mode::Query => { - // For query mode, we can only target a single player if targets.len() != 1 { // TODO: Add proper error message for multiple players in query mode return Ok(()); } - - let target = &targets[0]; - match self.exp_type.unwrap() { - ExpType::Levels => { - let level = target.experience_level.load(Ordering::Relaxed); - sender - .send_message(TextComponent::translate( - "commands.experience.query.levels", - [ - TextComponent::text(target.gameprofile.name.clone()), - TextComponent::text(level.to_string()), - ] - .into(), - )) - .await; - } - ExpType::Points => { - let total = target.total_experience.load(Ordering::Relaxed); - sender - .send_message(TextComponent::translate( - "commands.experience.query.points", - [ - TextComponent::text(target.gameprofile.name.clone()), - TextComponent::text(total.to_string()), - ] - .into(), - )) - .await; - } - } + self.handle_query(sender, &targets[0], self.exp_type.unwrap()) + .await; } Mode::Add | Mode::Set => { - let amount = match BoundedNumArgumentConsumer::::find_arg(args, ARG_AMOUNT)? { - Ok(value) => value, - Err(_) => { - sender - .send_message(TextComponent::translate( - "commands.experience.set.points.invalid", - [].into(), - )) - .await; - return Ok(()); - } + let Ok(amount) = BoundedNumArgumentConsumer::::find_arg(args, ARG_AMOUNT)? + else { + sender + .send_message(TextComponent::translate( + "commands.experience.set.points.invalid", + [].into(), + )) + .await; + return Ok(()); }; if self.mode == Mode::Set && amount < 0 { @@ -114,114 +190,27 @@ impl CommandExecutor for ExperienceExecutor { return Ok(()); } - for target in targets.iter() { - match self.exp_type.unwrap() { - ExpType::Levels => { - let current_level = target.experience_level.load(Ordering::Relaxed); - let new_level = if self.mode == Mode::Add { - current_level + amount - } else { - amount - }; - - if new_level < 0 { - sender - .send_message(TextComponent::translate( - "commands.experience.set.points.invalid", - [].into(), - )) - .await; - continue; - } - - target.set_level(new_level).await; - } - ExpType::Points => { - if self.mode == Mode::Add { - target.add_experience(amount).await; - } else { - let level = experience::get_level_from_total_exp(amount); - let progress = experience::get_progress_from_total_exp(amount); - target.set_experience(level, progress, amount).await; - } - } + for target in targets { + if !self + .handle_modify(sender, target, amount, self.exp_type.unwrap(), self.mode) + .await + { + sender + .send_message(TextComponent::translate( + "commands.experience.set.points.invalid", + [].into(), + )) + .await; + continue; } - // Send appropriate success message - let msg = match (self.mode, self.exp_type.unwrap()) { - (Mode::Add, ExpType::Points) => { - if targets.len() > 1 { - TextComponent::translate( - "commands.experience.add.points.success.multiple", - [ - TextComponent::text(amount.to_string()), - TextComponent::text(targets.len().to_string()), - ] - .into(), - ) - } else { - TextComponent::translate( - "commands.experience.add.points.success.single", - [ - TextComponent::text(amount.to_string()), - TextComponent::text(target.gameprofile.name.clone()), - ] - .into(), - ) - } - } - (Mode::Add, ExpType::Levels) => { - if targets.len() > 1 { - TextComponent::translate( - "commands.experience.add.levels.success.multiple", - [ - TextComponent::text(amount.to_string()), - TextComponent::text(targets.len().to_string()), - ] - .into(), - ) - } else { - TextComponent::translate( - "commands.experience.add.levels.success.single", - [ - TextComponent::text(amount.to_string()), - TextComponent::text(target.gameprofile.name.clone()), - ] - .into(), - ) - } - } - (Mode::Set, exp_type) => { - if targets.len() > 1 { - TextComponent::translate( - if exp_type == ExpType::Levels { - "commands.experience.set.levels.success.multiple" - } else { - "commands.experience.set.points.success.multiple" - }, - [ - TextComponent::text(amount.to_string()), - TextComponent::text(targets.len().to_string()), - ] - .into(), - ) - } else { - TextComponent::translate( - if exp_type == ExpType::Levels { - "commands.experience.set.levels.success.single" - } else { - "commands.experience.set.points.success.single" - }, - [ - TextComponent::text(amount.to_string()), - TextComponent::text(target.gameprofile.name.clone()), - ] - .into(), - ) - } - } - _ => unreachable!(), - }; + let msg = Self::get_success_message( + self.mode, + self.exp_type.unwrap(), + amount, + targets.len(), + Some(target.gameprofile.name.clone()), + ); sender.send_message(msg).await; } } @@ -234,8 +223,8 @@ impl CommandExecutor for ExperienceExecutor { pub fn init_command_tree() -> CommandTree { CommandTree::new(NAMES, DESCRIPTION) .then( - literal("add") - .then(argument(ARG_TARGETS, PlayersArgumentConsumer).then( + literal("add").then( + argument(ARG_TARGETS, PlayersArgumentConsumer).then( argument(ARG_AMOUNT, xp_amount()) .then(literal("levels").execute(ExperienceExecutor { mode: Mode::Add, @@ -249,11 +238,12 @@ pub fn init_command_tree() -> CommandTree { mode: Mode::Add, exp_type: Some(ExpType::Points), }), - )), + ), + ), ) .then( - literal("set") - .then(argument(ARG_TARGETS, PlayersArgumentConsumer).then( + literal("set").then( + argument(ARG_TARGETS, PlayersArgumentConsumer).then( argument(ARG_AMOUNT, xp_amount()) .then(literal("levels").execute(ExperienceExecutor { mode: Mode::Set, @@ -267,7 +257,8 @@ pub fn init_command_tree() -> CommandTree { mode: Mode::Set, exp_type: Some(ExpType::Points), }), - )), + ), + ), ) .then( literal("query").then( diff --git a/pumpkin/src/command/mod.rs b/pumpkin/src/command/mod.rs index 61e9ce92..a9714feb 100644 --- a/pumpkin/src/command/mod.rs +++ b/pumpkin/src/command/mod.rs @@ -10,7 +10,9 @@ use crate::world::World; use args::ConsumedArgs; use async_trait::async_trait; use commands::{ - ban, banip, banlist, clear, deop, experience, fill, gamemode, give, help, kick, kill, list, me, msg, op, pardon, pardonip, playsound, plugin, plugins, pumpkin, say, setblock, stop, summon, teleport, time, title, worldborder + ban, banip, banlist, clear, deop, experience, fill, gamemode, give, help, kick, kill, list, me, + msg, op, pardon, pardonip, playsound, plugin, plugins, pumpkin, say, setblock, stop, summon, + teleport, time, title, worldborder, }; use dispatcher::CommandError; use pumpkin_util::math::vector3::Vector3; diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index eacfd741..51ff9683 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -44,6 +44,7 @@ use pumpkin_protocol::{ client::play::{CSetEntityMetadata, Metadata}, server::play::{SClickContainer, SKeepAlive}, }; +use pumpkin_util::math::experience; use pumpkin_util::{ math::{ boundingbox::{BoundingBox, BoundingBoxSize}, @@ -55,7 +56,6 @@ use pumpkin_util::{ text::TextComponent, GameMode, }; -use pumpkin_util::math::experience; use pumpkin_world::{ cylindrical_chunk_iterator::Cylindrical, item::{ @@ -745,8 +745,8 @@ impl Player { self.client .send_packet(&CSetExperience::new( progress.clamp(0.0, 1.0), - total_exp.into(), level.into(), + total_exp.into(), )) .await; } @@ -762,31 +762,44 @@ impl Player { let new_total = current_total + exp; let new_level = experience::get_level_from_total_exp(new_total); let progress = experience::get_progress_from_total_exp(new_total); - + self.set_experience(new_level, progress, new_total).await; } /// Sets the player's experience level directly pub async fn set_level(&self, level: i32) { + let current_progress = self.experience_progress.load(); let total_exp = experience::get_total_exp_to_level(level); - self.set_experience(level, 0.0, total_exp).await; + // Add the partial progress towards next level + let next_level_exp = experience::get_total_exp_to_level(level + 1); + let exp_for_current_level = next_level_exp - total_exp; + #[allow(clippy::cast_precision_loss)] + let additional_exp = (current_progress * exp_for_current_level as f32) as i32; + let final_exp = total_exp + additional_exp; + + self.set_experience(level, current_progress, final_exp) + .await; } /// Returns the total amount of experience points required to reach a specific level + #[must_use] pub fn get_total_exp_to_level(level: i32) -> i32 { match level { 0..=16 => level * level + 6 * level, - 17..=31 => ((2.5 * f64::from(level * level)) - (40.5 * f64::from(level)) + 360.0) as i32, + 17..=31 => { + ((2.5 * f64::from(level * level)) - (40.5 * f64::from(level)) + 360.0) as i32 + } _ => ((4.5 * f64::from(level * level)) - (162.5 * f64::from(level)) + 2220.0) as i32, } } /// Calculates level from total experience points + #[must_use] pub fn get_level_from_total_exp(total_exp: i32) -> i32 { match total_exp { - 0..=352 => ((total_exp as f64 + 9.0).sqrt() - 3.0) as i32, - 353..=1507 => (81.0 + (total_exp as f64 - 7839.0) / 40.0).sqrt() as i32, - _ => (325.0 + (total_exp as f64 - 54215.0) / 72.0).sqrt() as i32, + 0..=352 => ((f64::from(total_exp) + 9.0).sqrt() - 3.0) as i32, + 353..=1507 => (81.0 + (f64::from(total_exp) - 7839.0) / 40.0).sqrt() as i32, + _ => (325.0 + (f64::from(total_exp) - 54215.0) / 72.0).sqrt() as i32, } } } From b56d57672d4f03b85c9e0e3d530b6a83a5226806 Mon Sep 17 00:00:00 2001 From: drakeerv Date: Thu, 30 Jan 2025 23:39:59 -0500 Subject: [PATCH 4/9] Fix translate strings --- pumpkin/src/command/commands/experience.rs | 65 +++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/pumpkin/src/command/commands/experience.rs b/pumpkin/src/command/commands/experience.rs index c7f7fb92..8e24ddd8 100644 --- a/pumpkin/src/command/commands/experience.rs +++ b/pumpkin/src/command/commands/experience.rs @@ -108,7 +108,70 @@ impl ExperienceExecutor { ) } } - _ => TextComponent::text(""), // Unreachable + (Mode::Add, ExpType::Levels) => { + if targets_len > 1 { + TextComponent::translate( + "commands.experience.add.levels.success.multiple", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(targets_len.to_string()), + ] + .into(), + ) + } else { + TextComponent::translate( + "commands.experience.add.levels.success.single", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(target_name.unwrap()), + ] + .into(), + ) + } + } + (Mode::Set, ExpType::Points) => { + if targets_len > 1 { + TextComponent::translate( + "commands.experience.set.points.success.multiple", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(targets_len.to_string()), + ] + .into(), + ) + } else { + TextComponent::translate( + "commands.experience.set.points.success.single", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(target_name.unwrap()), + ] + .into(), + ) + } + } + (Mode::Set, ExpType::Levels) => { + if targets_len > 1 { + TextComponent::translate( + "commands.experience.set.levels.success.multiple", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(targets_len.to_string()), + ] + .into(), + ) + } else { + TextComponent::translate( + "commands.experience.set.levels.success.single", + [ + TextComponent::text(amount.to_string()), + TextComponent::text(target_name.unwrap()), + ] + .into(), + ) + } + } + (Mode::Query, _) => unreachable!("Query mode doesn't use success messages"), } } From 409f20c9afeb3e515b4368f9bf4e547ea326799e Mon Sep 17 00:00:00 2001 From: drakeerv Date: Fri, 31 Jan 2025 00:39:42 -0500 Subject: [PATCH 5/9] Fix experience handling --- pumpkin/src/command/commands/experience.rs | 65 ++++++++++++---------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/pumpkin/src/command/commands/experience.rs b/pumpkin/src/command/commands/experience.rs index 8e24ddd8..d2845846 100644 --- a/pumpkin/src/command/commands/experience.rs +++ b/pumpkin/src/command/commands/experience.rs @@ -177,12 +177,11 @@ impl ExperienceExecutor { async fn handle_modify( &self, - _sender: &mut CommandSender<'_>, - target: &Player, + target: &Player, // Remove sender parameter since we'll handle errors in execute amount: i32, exp_type: ExpType, mode: Mode, - ) -> bool { + ) -> Result<(), &'static str> { // Change return type to indicate error reason match exp_type { ExpType::Levels => { let current_level = target.experience_level.load(Ordering::Relaxed); @@ -193,7 +192,7 @@ impl ExperienceExecutor { }; if new_level < 0 { - return false; + return Err("commands.experience.set.points.invalid"); } target.set_level(new_level).await; @@ -202,13 +201,26 @@ impl ExperienceExecutor { if mode == Mode::Add { target.add_experience(amount).await; } else { - let level = experience::get_level_from_total_exp(amount); - let progress = experience::get_progress_from_total_exp(amount); - target.set_experience(level, progress, amount).await; + // When setting points, check if they exceed current level's max + let current_level = target.experience_level.load(Ordering::Relaxed); + let current_level_start = experience::get_total_exp_to_level(current_level); + let next_level_start = experience::get_total_exp_to_level(current_level + 1); + + // Amount must be between current level's start and next level's start (exclusive) + if amount < current_level_start || amount >= next_level_start { + return Err("commands.experience.set.points.invalid"); + } + + // Calculate progress within current level + let level_points = amount - current_level_start; + let points_needed = next_level_start - current_level_start; + let progress = level_points as f32 / points_needed as f32; + + target.set_experience(current_level, progress, amount).await; } } } - true + Ok(()) } } @@ -254,27 +266,24 @@ impl CommandExecutor for ExperienceExecutor { } for target in targets { - if !self - .handle_modify(sender, target, amount, self.exp_type.unwrap(), self.mode) - .await - { - sender - .send_message(TextComponent::translate( - "commands.experience.set.points.invalid", - [].into(), - )) - .await; - continue; + match self.handle_modify(target, amount, self.exp_type.unwrap(), self.mode).await { + Ok(()) => { + let msg = Self::get_success_message( + self.mode, + self.exp_type.unwrap(), + amount, + targets.len(), + Some(target.gameprofile.name.clone()), + ); + sender.send_message(msg).await; + } + Err(error_msg) => { + sender + .send_message(TextComponent::translate(error_msg, [].into())) + .await; + continue; + } } - - let msg = Self::get_success_message( - self.mode, - self.exp_type.unwrap(), - amount, - targets.len(), - Some(target.gameprofile.name.clone()), - ); - sender.send_message(msg).await; } } } From 21d73dd1711dcd4039394618f6466c21d828b5da Mon Sep 17 00:00:00 2001 From: drakeerv Date: Fri, 31 Jan 2025 00:43:34 -0500 Subject: [PATCH 6/9] Clippy & Format --- pumpkin/src/command/commands/experience.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pumpkin/src/command/commands/experience.rs b/pumpkin/src/command/commands/experience.rs index d2845846..794e532a 100644 --- a/pumpkin/src/command/commands/experience.rs +++ b/pumpkin/src/command/commands/experience.rs @@ -177,11 +177,12 @@ impl ExperienceExecutor { async fn handle_modify( &self, - target: &Player, // Remove sender parameter since we'll handle errors in execute + target: &Player, // Remove sender parameter since we'll handle errors in execute amount: i32, exp_type: ExpType, mode: Mode, - ) -> Result<(), &'static str> { // Change return type to indicate error reason + ) -> Result<(), &'static str> { + // Change return type to indicate error reason match exp_type { ExpType::Levels => { let current_level = target.experience_level.load(Ordering::Relaxed); @@ -205,17 +206,18 @@ impl ExperienceExecutor { let current_level = target.experience_level.load(Ordering::Relaxed); let current_level_start = experience::get_total_exp_to_level(current_level); let next_level_start = experience::get_total_exp_to_level(current_level + 1); - + // Amount must be between current level's start and next level's start (exclusive) if amount < current_level_start || amount >= next_level_start { return Err("commands.experience.set.points.invalid"); } - + // Calculate progress within current level let level_points = amount - current_level_start; let points_needed = next_level_start - current_level_start; + #[allow(clippy::cast_precision_loss)] let progress = level_points as f32 / points_needed as f32; - + target.set_experience(current_level, progress, amount).await; } } @@ -266,7 +268,10 @@ impl CommandExecutor for ExperienceExecutor { } for target in targets { - match self.handle_modify(target, amount, self.exp_type.unwrap(), self.mode).await { + match self + .handle_modify(target, amount, self.exp_type.unwrap(), self.mode) + .await + { Ok(()) => { let msg = Self::get_success_message( self.mode, From 0e0cfbdb13e07af2acb3841163cbade1cce46739 Mon Sep 17 00:00:00 2001 From: drakeerv Date: Fri, 31 Jan 2025 00:55:45 -0500 Subject: [PATCH 7/9] Fix experience calculations --- pumpkin/src/command/commands/experience.rs | 25 ++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/pumpkin/src/command/commands/experience.rs b/pumpkin/src/command/commands/experience.rs index 794e532a..a3858c7a 100644 --- a/pumpkin/src/command/commands/experience.rs +++ b/pumpkin/src/command/commands/experience.rs @@ -177,12 +177,11 @@ impl ExperienceExecutor { async fn handle_modify( &self, - target: &Player, // Remove sender parameter since we'll handle errors in execute + target: &Player, amount: i32, exp_type: ExpType, mode: Mode, ) -> Result<(), &'static str> { - // Change return type to indicate error reason match exp_type { ExpType::Levels => { let current_level = target.experience_level.load(Ordering::Relaxed); @@ -202,23 +201,27 @@ impl ExperienceExecutor { if mode == Mode::Add { target.add_experience(amount).await; } else { - // When setting points, check if they exceed current level's max + // When setting points, keep current level but check maximum let current_level = target.experience_level.load(Ordering::Relaxed); - let current_level_start = experience::get_total_exp_to_level(current_level); let next_level_start = experience::get_total_exp_to_level(current_level + 1); + let current_level_start = experience::get_total_exp_to_level(current_level); + let max_points_in_level = next_level_start - current_level_start; - // Amount must be between current level's start and next level's start (exclusive) - if amount < current_level_start || amount >= next_level_start { + // Points can't exceed maximum for current level + if amount > max_points_in_level { return Err("commands.experience.set.points.invalid"); } - // Calculate progress within current level - let level_points = amount - current_level_start; - let points_needed = next_level_start - current_level_start; + // Calculate progress directly from points (0 to max_points_in_level) #[allow(clippy::cast_precision_loss)] - let progress = level_points as f32 / points_needed as f32; + let progress = (amount as f32) / (max_points_in_level as f32); + let progress = progress.clamp(0.0, 1.0); - target.set_experience(current_level, progress, amount).await; + // Convert local level points to global XP amount + let total_exp = current_level_start + amount; + target + .set_experience(current_level, progress, total_exp) + .await; } } } From 0fbc913d55b3f22097a15419638828efd6278ba2 Mon Sep 17 00:00:00 2001 From: drakeerv Date: Fri, 31 Jan 2025 16:06:31 -0500 Subject: [PATCH 8/9] Fix how we store experience. Math WIP --- pumpkin-util/src/math/experience.rs | 45 ++++++++---- pumpkin/src/command/commands/experience.rs | 13 ++-- pumpkin/src/entity/player.rs | 80 +++++++++++++--------- 3 files changed, 85 insertions(+), 53 deletions(-) diff --git a/pumpkin-util/src/math/experience.rs b/pumpkin-util/src/math/experience.rs index 46309d62..1141f2c1 100644 --- a/pumpkin-util/src/math/experience.rs +++ b/pumpkin-util/src/math/experience.rs @@ -1,4 +1,5 @@ /// Returns the amount of experience required to reach the next level from a given level +#[must_use] pub fn get_exp_to_next_level(current_level: i32) -> i32 { match current_level { 0..=15 => 2 * current_level + 7, @@ -7,28 +8,42 @@ pub fn get_exp_to_next_level(current_level: i32) -> i32 { } } -/// Returns the total amount of experience points required to reach a specific level -pub fn get_total_exp_to_level(level: i32) -> i32 { - match level { +/// Calculate total experience points from level and progress +#[must_use] +pub fn calculate_total_exp(level: i32, progress: f32) -> i32 { + let level_base = match level { 0..=16 => level * level + 6 * level, 17..=31 => ((2.5 * f64::from(level * level)) - (40.5 * f64::from(level)) + 360.0) as i32, _ => ((4.5 * f64::from(level * level)) - (162.5 * f64::from(level)) + 2220.0) as i32, - } + }; + + let next_level_exp = get_exp_to_next_level(level); + #[allow(clippy::cast_precision_loss)] + let progress_exp = (next_level_exp as f32 * progress) as i32; + + level_base + progress_exp } -/// Calculates level from total experience points -pub fn get_level_from_total_exp(total_exp: i32) -> i32 { - match total_exp { +/// Calculate level and progress from total experience points +#[must_use] +pub fn calculate_level_and_progress(total_exp: i32) -> (i32, f32) { + let level = match total_exp { 0..=352 => ((total_exp as f64 + 9.0).sqrt() - 3.0) as i32, 353..=1507 => (81.0 + (total_exp as f64 - 7839.0) / 40.0).sqrt() as i32, _ => (325.0 + (total_exp as f64 - 54215.0) / 72.0).sqrt() as i32, - } -} + }; + + let level_start = match level { + 0..=16 => level * level + 6 * level, + 17..=31 => ((2.5 * f64::from(level * level)) - (40.5 * f64::from(level)) + 360.0) as i32, + _ => ((4.5 * f64::from(level * level)) - (162.5 * f64::from(level)) + 2220.0) as i32, + }; + + let next_level_exp = get_exp_to_next_level(level); + let exp_into_level = total_exp - level_start; + + #[allow(clippy::cast_precision_loss)] + let progress = (exp_into_level as f32) / (next_level_exp as f32); -/// Calculate experience progress (0.0 to 1.0) for a given total experience amount -pub fn get_progress_from_total_exp(total_exp: i32) -> f32 { - let level = get_level_from_total_exp(total_exp); - let next_level_exp = get_total_exp_to_level(level + 1); - let current_level_exp = get_total_exp_to_level(level); - (total_exp - current_level_exp) as f32 / (next_level_exp - current_level_exp) as f32 + (level, progress.clamp(0.0, 1.0)) } diff --git a/pumpkin/src/command/commands/experience.rs b/pumpkin/src/command/commands/experience.rs index a3858c7a..29a41b0e 100644 --- a/pumpkin/src/command/commands/experience.rs +++ b/pumpkin/src/command/commands/experience.rs @@ -64,13 +64,13 @@ impl ExperienceExecutor { .await; } ExpType::Points => { - let total = target.total_experience.load(Ordering::Relaxed); + let points = target.experience_points.load(Ordering::Relaxed); sender .send_message(TextComponent::translate( "commands.experience.query.points", [ TextComponent::text(target.gameprofile.name.clone()), - TextComponent::text(total.to_string()), + TextComponent::text(points.to_string()), ] .into(), )) @@ -203,9 +203,10 @@ impl ExperienceExecutor { } else { // When setting points, keep current level but check maximum let current_level = target.experience_level.load(Ordering::Relaxed); - let next_level_start = experience::get_total_exp_to_level(current_level + 1); - let current_level_start = experience::get_total_exp_to_level(current_level); - let max_points_in_level = next_level_start - current_level_start; + let current_progress = target.experience_progress.load(); + let current_total = experience::calculate_total_exp(current_level, current_progress); + let next_level_total = experience::calculate_total_exp(current_level + 1, 0.0); + let max_points_in_level = next_level_total - current_total; // Points can't exceed maximum for current level if amount > max_points_in_level { @@ -218,7 +219,7 @@ impl ExperienceExecutor { let progress = progress.clamp(0.0, 1.0); // Convert local level points to global XP amount - let total_exp = current_level_start + amount; + let total_exp = current_total + amount; target .set_experience(current_level, progress, total_exp) .await; diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 51ff9683..74f8b181 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -44,13 +44,13 @@ use pumpkin_protocol::{ client::play::{CSetEntityMetadata, Metadata}, server::play::{SClickContainer, SKeepAlive}, }; -use pumpkin_util::math::experience; use pumpkin_util::{ math::{ boundingbox::{BoundingBox, BoundingBoxSize}, position::BlockPos, vector2::Vector2, vector3::Vector3, + experience }, permission::PermissionLvl, text::TextComponent, @@ -142,7 +142,7 @@ pub struct Player { /// The player's experience progress (0.0 to 1.0) pub experience_progress: AtomicCell, /// The player's total experience points - pub total_experience: AtomicI32, + pub experience_points: AtomicI32, } impl Player { @@ -226,7 +226,7 @@ impl Player { inventory: Mutex::new(PlayerInventory::new()), experience_level: AtomicI32::new(0), experience_progress: AtomicCell::new(0.0), - total_experience: AtomicI32::new(0), + experience_points: AtomicI32::new(0), } } @@ -737,16 +737,16 @@ impl Player { } /// Sets the player's experience level and updates the client - pub async fn set_experience(&self, level: i32, progress: f32, total_exp: i32) { + pub async fn set_experience(&self, level: i32, progress: f32, points: i32) { self.experience_level.store(level, Ordering::Relaxed); self.experience_progress.store(progress.clamp(0.0, 1.0)); - self.total_experience.store(total_exp, Ordering::Relaxed); + self.experience_points.store(points, Ordering::Relaxed); self.client .send_packet(&CSetExperience::new( progress.clamp(0.0, 1.0), level.into(), - total_exp.into(), + points.into(), )) .await; } @@ -758,27 +758,38 @@ impl Player { /// Adds experience points to the player pub async fn add_experience(&self, exp: i32) { - let current_total = self.total_experience.load(Ordering::Relaxed); - let new_total = current_total + exp; - let new_level = experience::get_level_from_total_exp(new_total); - let progress = experience::get_progress_from_total_exp(new_total); - - self.set_experience(new_level, progress, new_total).await; + let current_points = self.experience_points.load(Ordering::Relaxed); + let new_points = current_points + exp; + let current_level = self.experience_level.load(Ordering::Relaxed); + + let next_level_points = experience::get_exp_to_next_level(current_level); + let mut new_level = current_level; + let mut progress = self.experience_progress.load(); + + #[allow(clippy::cast_precision_loss)] + let delta_progress = (exp as f32) / (next_level_points as f32); + progress += delta_progress; + + while progress >= 1.0 { + progress -= 1.0; + new_level += 1; + } + + while progress < 0.0 && new_level > 0 { + new_level -= 1; + progress += 1.0; + } + + progress = progress.clamp(0.0, 1.0); + + self.set_experience(new_level, progress, new_points).await; } /// Sets the player's experience level directly pub async fn set_level(&self, level: i32) { - let current_progress = self.experience_progress.load(); - let total_exp = experience::get_total_exp_to_level(level); - // Add the partial progress towards next level - let next_level_exp = experience::get_total_exp_to_level(level + 1); - let exp_for_current_level = next_level_exp - total_exp; - #[allow(clippy::cast_precision_loss)] - let additional_exp = (current_progress * exp_for_current_level as f32) as i32; - let final_exp = total_exp + additional_exp; - - self.set_experience(level, current_progress, final_exp) - .await; + let progress = self.experience_progress.load(); + let points = self.experience_points.load(Ordering::Relaxed); + self.set_experience(level, progress, points).await; } /// Returns the total amount of experience points required to reach a specific level @@ -813,21 +824,26 @@ impl NBTStorage for Player { self.inventory.lock().await.selected as i32, ); self.abilities.lock().await.write_nbt(nbt).await; - nbt.put_int("XpLevel", self.experience_level.load(Ordering::Relaxed)); - nbt.put_float("XpProgress", self.experience_progress.load()); - nbt.put_int("XpTotal", self.total_experience.load(Ordering::Relaxed)); + + // Store total XP instead of individual components + let total_exp = experience::calculate_total_exp( + self.experience_level.load(Ordering::Relaxed), + self.experience_progress.load() + ); + nbt.put_int("XpTotal", total_exp); } async fn read_nbt(&mut self, nbt: &mut NbtCompound) { self.living_entity.read_nbt(nbt).await; self.inventory.lock().await.selected = nbt.get_int("SelectedItemSlot").unwrap_or(0) as u32; self.abilities.lock().await.read_nbt(nbt).await; - self.experience_level - .store(nbt.get_int("XpLevel").unwrap_or(0), Ordering::Relaxed); - self.experience_progress - .store(nbt.get_float("XpProgress").unwrap_or(0.0)); - self.total_experience - .store(nbt.get_int("XpTotal").unwrap_or(0), Ordering::Relaxed); + + // Load from total XP + let total_exp = nbt.get_int("XpTotal").unwrap_or(0); + let (level, progress) = experience::calculate_level_and_progress(total_exp); + self.experience_level.store(level, Ordering::Relaxed); + self.experience_progress.store(progress); + self.experience_points.store(total_exp, Ordering::Relaxed); } } From 12c1cae5fcf1825780f9afbbd4ea0d1acc737695 Mon Sep 17 00:00:00 2001 From: drakeerv Date: Sat, 1 Feb 2025 00:17:05 -0500 Subject: [PATCH 9/9] Redo all experience calculations This should be 100% vanilla parity. --- pumpkin-util/src/math/experience.rs | 66 +++++------ pumpkin/src/command/commands/experience.rs | 42 +++---- pumpkin/src/entity/player.rs | 125 ++++++++++----------- 3 files changed, 103 insertions(+), 130 deletions(-) diff --git a/pumpkin-util/src/math/experience.rs b/pumpkin-util/src/math/experience.rs index 1141f2c1..c6b1a3c8 100644 --- a/pumpkin-util/src/math/experience.rs +++ b/pumpkin-util/src/math/experience.rs @@ -1,49 +1,39 @@ -/// Returns the amount of experience required to reach the next level from a given level -#[must_use] -pub fn get_exp_to_next_level(current_level: i32) -> i32 { - match current_level { - 0..=15 => 2 * current_level + 7, - 16..=30 => 5 * current_level - 38, - _ => 9 * current_level - 158, +/// Get the number of points in a level +pub fn points_in_level(level: i32) -> i32 { + match level { + 0..=15 => 2 * level + 7, + 16..=30 => 5 * level - 38, + _ => 9 * level - 158, } } -/// Calculate total experience points from level and progress -#[must_use] -pub fn calculate_total_exp(level: i32, progress: f32) -> i32 { - let level_base = match level { - 0..=16 => level * level + 6 * level, - 17..=31 => ((2.5 * f64::from(level * level)) - (40.5 * f64::from(level)) + 360.0) as i32, +/// Calculate the total number of points to reach a level +pub fn points_to_level(level: i32) -> i32 { + match level { + 0..=15 => (level * level + 6 * level) / 2, + 16..=30 => ((2.5 * f64::from(level * level)) - (40.5 * f64::from(level)) + 360.0) as i32, _ => ((4.5 * f64::from(level * level)) - (162.5 * f64::from(level)) + 2220.0) as i32, - }; - - let next_level_exp = get_exp_to_next_level(level); - #[allow(clippy::cast_precision_loss)] - let progress_exp = (next_level_exp as f32 * progress) as i32; - - level_base + progress_exp + } } -/// Calculate level and progress from total experience points -#[must_use] -pub fn calculate_level_and_progress(total_exp: i32) -> (i32, f32) { - let level = match total_exp { - 0..=352 => ((total_exp as f64 + 9.0).sqrt() - 3.0) as i32, - 353..=1507 => (81.0 + (total_exp as f64 - 7839.0) / 40.0).sqrt() as i32, - _ => (325.0 + (total_exp as f64 - 54215.0) / 72.0).sqrt() as i32, +/// Calculate level and points from total points +pub fn total_to_level_and_points(total_points: i32) -> (i32, i32) { + let level = match total_points { + 0..=352 => ((total_points as f64 + 9.0).sqrt() - 3.0) as i32, + 353..=1507 => (81.0 + (total_points as f64 - 7839.0) / 40.0).sqrt() as i32, + _ => (325.0 + (total_points as f64 - 54215.0) / 72.0).sqrt() as i32, }; - let level_start = match level { - 0..=16 => level * level + 6 * level, - 17..=31 => ((2.5 * f64::from(level * level)) - (40.5 * f64::from(level)) + 360.0) as i32, - _ => ((4.5 * f64::from(level * level)) - (162.5 * f64::from(level)) + 2220.0) as i32, - }; + let level_start = points_to_level(level); + let points_into_level = total_points - level_start; - let next_level_exp = get_exp_to_next_level(level); - let exp_into_level = total_exp - level_start; - - #[allow(clippy::cast_precision_loss)] - let progress = (exp_into_level as f32) / (next_level_exp as f32); + (level, points_into_level) +} - (level, progress.clamp(0.0, 1.0)) +/// Calculate progress (0.0 to 1.0) from points within a level +pub fn progress_in_level(points: i32, level: i32) -> f32 { + let max_points = points_in_level(level); + #[allow(clippy::cast_precision_loss)] + let progress = (points as f32) / (max_points as f32); + progress.clamp(0.0, 1.0) } diff --git a/pumpkin/src/command/commands/experience.rs b/pumpkin/src/command/commands/experience.rs index 29a41b0e..6b623bfb 100644 --- a/pumpkin/src/command/commands/experience.rs +++ b/pumpkin/src/command/commands/experience.rs @@ -2,6 +2,7 @@ use std::sync::atomic::Ordering; use async_trait::async_trait; use pumpkin_util::math::experience; +use pumpkin_util::text::color::{Color, NamedColor}; use pumpkin_util::text::TextComponent; use crate::command::args::bounded_num::BoundedNumArgumentConsumer; @@ -184,45 +185,25 @@ impl ExperienceExecutor { ) -> Result<(), &'static str> { match exp_type { ExpType::Levels => { - let current_level = target.experience_level.load(Ordering::Relaxed); - let new_level = if mode == Mode::Add { - current_level + amount + if mode == Mode::Add { + target.add_experience_levels(amount).await; } else { - amount - }; - - if new_level < 0 { - return Err("commands.experience.set.points.invalid"); + target.set_experience_level(amount, true).await; } - - target.set_level(new_level).await; } ExpType::Points => { if mode == Mode::Add { - target.add_experience(amount).await; + target.add_experience_points(amount).await; } else { - // When setting points, keep current level but check maximum + // target.set_experience_points(amount).await; This could let current_level = target.experience_level.load(Ordering::Relaxed); - let current_progress = target.experience_progress.load(); - let current_total = experience::calculate_total_exp(current_level, current_progress); - let next_level_total = experience::calculate_total_exp(current_level + 1, 0.0); - let max_points_in_level = next_level_total - current_total; + let current_max_points = experience::points_in_level(current_level); - // Points can't exceed maximum for current level - if amount > max_points_in_level { + if amount > current_max_points { return Err("commands.experience.set.points.invalid"); } - // Calculate progress directly from points (0 to max_points_in_level) - #[allow(clippy::cast_precision_loss)] - let progress = (amount as f32) / (max_points_in_level as f32); - let progress = progress.clamp(0.0, 1.0); - - // Convert local level points to global XP amount - let total_exp = current_total + amount; - target - .set_experience(current_level, progress, total_exp) - .await; + target.set_experience_points(amount).await; } } } @@ -288,7 +269,10 @@ impl CommandExecutor for ExperienceExecutor { } Err(error_msg) => { sender - .send_message(TextComponent::translate(error_msg, [].into())) + .send_message( + TextComponent::translate(error_msg, [].into()) + .color(Color::Named(NamedColor::Red)), + ) .await; continue; } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index d59de108..2373fbff 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -21,8 +21,8 @@ use pumpkin_protocol::{ client::play::{ CActionBar, CCombatDeath, CDisguisedChatMessage, CEntityStatus, CGameEvent, CHurtAnimation, CKeepAlive, CPlayDisconnect, CPlayerAbilities, CPlayerInfoUpdate, CPlayerPosition, - CSetExperience, CSetHealth, CSubtitle, CSystemChatMessage, CTitleText, GameEvent, MetaDataType, - PlayerAction, + CSetExperience, CSetHealth, CSubtitle, CSystemChatMessage, CTitleText, GameEvent, + MetaDataType, PlayerAction, }, server::play::{ SChatCommand, SChatMessage, SClientCommand, SClientInformationPlay, SClientTickEnd, @@ -47,10 +47,10 @@ use pumpkin_protocol::{ use pumpkin_util::{ math::{ boundingbox::{BoundingBox, BoundingBoxSize}, + experience, position::BlockPos, vector2::Vector2, vector3::Vector3, - experience }, permission::PermissionLvl, text::TextComponent, @@ -750,6 +750,7 @@ impl Player { } /// Sets the player's experience level and updates the client + #[allow(clippy::cast_precision_loss)] pub async fn set_experience(&self, level: i32, progress: f32, points: i32) { self.experience_level.store(level, Ordering::Relaxed); self.experience_progress.store(progress.clamp(0.0, 1.0)); @@ -764,67 +765,66 @@ impl Player { .await; } - /// Returns the amount of experience required to reach the next level from the current level - pub fn get_exp_to_next_level(&self) -> i32 { - experience::get_exp_to_next_level(self.experience_level.load(Ordering::Relaxed)) + /// Sets the player's experience level directly + #[allow(clippy::cast_precision_loss)] + pub async fn set_experience_level(&self, new_level: i32, keep_progress: bool) { + let progress = self.experience_progress.load(); + let mut points = self.experience_points.load(Ordering::Relaxed); + + // If keep progress is true then calculate the number of points needed to keep the same progress scaled + if keep_progress { + // Get our current level + let current_level = self.experience_level.load(Ordering::Relaxed); + let current_max_points = experience::points_in_level(current_level); + // Calculate the max value for new level + let new_max_points = experience::points_in_level(new_level); + // Calculate the scaling factor + let scale = new_max_points as f32 / current_max_points as f32; + // Scale the points (Vanilla doesn't seem to recalculate progress so we won't) + points = (points as f32 * scale) as i32; + } + + self.set_experience(new_level, progress, points).await; } - /// Adds experience points to the player - pub async fn add_experience(&self, exp: i32) { - let current_points = self.experience_points.load(Ordering::Relaxed); - let new_points = current_points + exp; + /// Add experience levels to the player + pub async fn add_experience_levels(&self, added_levels: i32) { let current_level = self.experience_level.load(Ordering::Relaxed); - - let next_level_points = experience::get_exp_to_next_level(current_level); - let mut new_level = current_level; - let mut progress = self.experience_progress.load(); - - #[allow(clippy::cast_precision_loss)] - let delta_progress = (exp as f32) / (next_level_points as f32); - progress += delta_progress; - - while progress >= 1.0 { - progress -= 1.0; - new_level += 1; - } - - while progress < 0.0 && new_level > 0 { - new_level -= 1; - progress += 1.0; - } - - progress = progress.clamp(0.0, 1.0); - - self.set_experience(new_level, progress, new_points).await; + let new_level = current_level + added_levels; + self.set_experience_level(new_level, true).await; } - /// Sets the player's experience level directly - pub async fn set_level(&self, level: i32) { - let progress = self.experience_progress.load(); - let points = self.experience_points.load(Ordering::Relaxed); - self.set_experience(level, progress, points).await; - } + /// Set the player's experience points directly, Returns true if successful. + #[allow(clippy::cast_precision_loss)] + pub async fn set_experience_points(&self, new_points: i32) -> bool { + let current_points = self.experience_points.load(Ordering::Relaxed); - /// Returns the total amount of experience points required to reach a specific level - #[must_use] - pub fn get_total_exp_to_level(level: i32) -> i32 { - match level { - 0..=16 => level * level + 6 * level, - 17..=31 => { - ((2.5 * f64::from(level * level)) - (40.5 * f64::from(level)) + 360.0) as i32 - } - _ => ((4.5 * f64::from(level * level)) - (162.5 * f64::from(level)) + 2220.0) as i32, + if new_points == current_points { + return true; } - } - /// Calculates level from total experience points - #[must_use] - pub fn get_level_from_total_exp(total_exp: i32) -> i32 { - match total_exp { - 0..=352 => ((f64::from(total_exp) + 9.0).sqrt() - 3.0) as i32, - 353..=1507 => (81.0 + (f64::from(total_exp) - 7839.0) / 40.0).sqrt() as i32, - _ => (325.0 + (f64::from(total_exp) - 54215.0) / 72.0).sqrt() as i32, + let current_level = self.experience_level.load(Ordering::Relaxed); + let max_points = experience::points_in_level(current_level); + + if new_points < 0 || new_points > max_points { + return false; } + + let progress = new_points as f32 / max_points as f32; + self.set_experience(current_level, progress, new_points) + .await; + true + } + + /// Add experience points to the player + pub async fn add_experience_points(&self, added_points: i32) { + let current_level = self.experience_level.load(Ordering::Relaxed); + let current_points = self.experience_points.load(Ordering::Relaxed); + let total_exp = experience::points_to_level(current_level) + current_points; + let new_total_exp = total_exp + added_points; + let (new_level, new_points) = experience::total_to_level_and_points(new_total_exp); + let progress = experience::progress_in_level(new_level, new_points); + self.set_experience(new_level, progress, new_points).await; } } @@ -837,12 +837,10 @@ impl NBTStorage for Player { self.inventory.lock().await.selected as i32, ); self.abilities.lock().await.write_nbt(nbt).await; - + // Store total XP instead of individual components - let total_exp = experience::calculate_total_exp( - self.experience_level.load(Ordering::Relaxed), - self.experience_progress.load() - ); + let total_exp = experience::points_to_level(self.experience_level.load(Ordering::Relaxed)) + + self.experience_points.load(Ordering::Relaxed); nbt.put_int("XpTotal", total_exp); } @@ -850,13 +848,14 @@ impl NBTStorage for Player { self.living_entity.read_nbt(nbt).await; self.inventory.lock().await.selected = nbt.get_int("SelectedItemSlot").unwrap_or(0) as u32; self.abilities.lock().await.read_nbt(nbt).await; - + // Load from total XP let total_exp = nbt.get_int("XpTotal").unwrap_or(0); - let (level, progress) = experience::calculate_level_and_progress(total_exp); + let (level, points) = experience::total_to_level_and_points(total_exp); + let progress = experience::progress_in_level(level, points); self.experience_level.store(level, Ordering::Relaxed); self.experience_progress.store(progress); - self.experience_points.store(total_exp, Ordering::Relaxed); + self.experience_points.store(points, Ordering::Relaxed); } }