feat: add stats

This commit is contained in:
Alexander Medvedev 2026-06-12 20:55:29 +02:00
parent 3b3ae983e4
commit 68bae5899e
50 changed files with 1141 additions and 60 deletions

1
assets/stats.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -54,6 +54,7 @@ mod screen;
mod sound; mod sound;
mod sound_category; mod sound_category;
mod spawn_egg; mod spawn_egg;
mod statistic;
mod structures; mod structures;
mod tag; mod tag;
mod tracked_data; mod tracked_data;
@ -95,6 +96,7 @@ pub fn main() {
(scoreboard_slot::build, "scoreboard_slot.rs"), (scoreboard_slot::build, "scoreboard_slot.rs"),
(world_event::build, "world_event.rs"), (world_event::build, "world_event.rs"),
(entity_type::build, "entity_type.rs"), (entity_type::build, "entity_type.rs"),
(statistic::build, "statistic.rs"),
(noise_parameter::build, "noise_parameter.rs"), (noise_parameter::build, "noise_parameter.rs"),
(biome::build, "biome.rs"), (biome::build, "biome.rs"),
(damage_type::build, "damage_type.rs"), (damage_type::build, "damage_type.rs"),

View File

@ -0,0 +1,115 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use serde::Deserialize;
use indexmap::IndexMap;
use heck::ToPascalCase;
#[derive(Deserialize)]
struct CustomStatisticEntry {
id: i32,
}
#[derive(Deserialize)]
struct StatisticData {
id: i32,
registry: String,
entries: IndexMap<String, CustomStatisticEntry>,
}
pub fn build() -> TokenStream {
let stats_json =
std::fs::read_to_string("../assets/stats.json").expect("Failed to read stats.json");
let stats_data: IndexMap<String, StatisticData> =
serde_json::from_str(&stats_json).expect("Failed to parse stats.json");
let mut category_variants = Vec::new();
let mut category_from_id_arms = Vec::new();
let mut category_from_name_arms = Vec::new();
let mut custom_stat_variants = Vec::new();
let mut custom_stat_from_id_arms = Vec::new();
for (category_name, data) in &stats_data {
let category_ident = format_ident!(
"{}",
category_name.replace("minecraft:", "").to_pascal_case()
);
let category_id = data.id;
category_variants.push(quote! { #category_ident = #category_id });
category_from_id_arms.push(quote! { #category_id => Some(Self::#category_ident) });
let category_name_stripped = category_name.replace("minecraft:", "");
category_from_name_arms.push(quote! { #category_name_stripped => Some(Self::#category_ident) });
if category_name == "minecraft:custom" {
for (stat_name, entry) in &data.entries {
let stat_ident = format_ident!(
"{}",
stat_name.replace("minecraft:", "").to_pascal_case()
);
let stat_id = entry.id;
custom_stat_variants.push(quote! { #stat_ident = #stat_id });
custom_stat_from_id_arms.push(quote! { #stat_id => Some(Self::#stat_ident) });
}
}
}
quote! {
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(i32)]
pub enum StatisticCategory {
#(#category_variants,)*
}
impl StatisticCategory {
#[must_use]
pub const fn from_i32(id: i32) -> Option<Self> {
match id {
#(#category_from_id_arms,)*
_ => None,
}
}
#[must_use]
pub fn from_registry_key(name: &str) -> Option<Self> {
let name = name.strip_prefix("minecraft:").unwrap_or(name);
match name {
#(#category_from_name_arms,)*
_ => None,
}
}
}
impl TryFrom<i32> for StatisticCategory {
type Error = ();
fn try_from(value: i32) -> Result<Self, Self::Error> {
Self::from_i32(value).ok_or(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(i32)]
pub enum CustomStatistic {
#(#custom_stat_variants,)*
}
impl CustomStatistic {
#[must_use]
pub const fn from_i32(id: i32) -> Option<Self> {
match id {
#(#custom_stat_from_id_arms,)*
_ => None,
}
}
}
impl TryFrom<i32> for CustomStatistic {
type Error = ();
fn try_from(value: i32) -> Result<Self, Self::Error> {
Self::from_i32(value).ok_or(())
}
}
}
}

View File

@ -14,6 +14,7 @@ default = [
"registry", "registry",
"screen", "screen",
"particle", "particle",
"statistic",
"sound", "sound",
"recipes", "recipes",
"data_component", "data_component",
@ -57,6 +58,7 @@ translation = []
registry = [] registry = []
screen = [] screen = []
particle = [] particle = []
statistic = []
sound = [] sound = []
recipes = [] recipes = []
data_component = [] data_component = []

View File

@ -0,0 +1,226 @@
/* This file is generated. Do not edit manually. */
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(i32)]
pub enum StatisticCategory {
Mined = 0i32,
Crafted = 1i32,
Used = 2i32,
Broken = 3i32,
PickedUp = 4i32,
Dropped = 5i32,
Killed = 6i32,
KilledBy = 7i32,
Custom = 8i32,
}
impl StatisticCategory {
#[must_use]
pub const fn from_i32(id: i32) -> Option<Self> {
match id {
0i32 => Some(Self::Mined),
1i32 => Some(Self::Crafted),
2i32 => Some(Self::Used),
3i32 => Some(Self::Broken),
4i32 => Some(Self::PickedUp),
5i32 => Some(Self::Dropped),
6i32 => Some(Self::Killed),
7i32 => Some(Self::KilledBy),
8i32 => Some(Self::Custom),
_ => None,
}
}
#[must_use]
pub fn from_registry_key(name: &str) -> Option<Self> {
let name = name.strip_prefix("minecraft:").unwrap_or(name);
match name {
"mined" => Some(Self::Mined),
"crafted" => Some(Self::Crafted),
"used" => Some(Self::Used),
"broken" => Some(Self::Broken),
"picked_up" => Some(Self::PickedUp),
"dropped" => Some(Self::Dropped),
"killed" => Some(Self::Killed),
"killed_by" => Some(Self::KilledBy),
"custom" => Some(Self::Custom),
_ => None,
}
}
}
impl TryFrom<i32> for StatisticCategory {
type Error = ();
fn try_from(value: i32) -> Result<Self, Self::Error> {
Self::from_i32(value).ok_or(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(i32)]
pub enum CustomStatistic {
DamageBlockedByShield = 29i32,
InteractWithBeacon = 46i32,
FallOneCm = 10i32,
OpenChest = 59i32,
PlayTime = 1i32,
WalkOneCm = 6i32,
RaidWin = 72i32,
DamageDealtResisted = 27i32,
CleanArmor = 42i32,
SwimOneCm = 20i32,
HappyGhastOneCm = 17i32,
TalkedToVillager = 37i32,
EnchantItem = 55i32,
Drop = 24i32,
PotFlower = 52i32,
OpenShulkerBox = 61i32,
WalkOnWaterOneCm = 9i32,
InteractWithCampfire = 66i32,
InteractWithFurnace = 57i32,
InteractWithLectern = 65i32,
RaidTrigger = 71i32,
StriderOneCm = 21i32,
CrouchOneCm = 7i32,
EatCakeSlice = 39i32,
PlayRecord = 56i32,
InteractWithCraftingTable = 58i32,
SneakTime = 5i32,
BoatOneCm = 15i32,
DamageResisted = 31i32,
Deaths = 32i32,
TotalWorldTime = 2i32,
PigOneCm = 16i32,
AviateOneCm = 19i32,
Jump = 23i32,
AnimalsBred = 34i32,
InspectHopper = 48i32,
InteractWithBlastFurnace = 63i32,
SprintOneCm = 8i32,
DamageTaken = 28i32,
MobKills = 33i32,
InteractWithCartographyTable = 67i32,
TimeSinceDeath = 3i32,
MinecartOneCm = 14i32,
InteractWithGrindstone = 74i32,
PlayNoteblock = 50i32,
OpenBarrel = 62i32,
NautilusOneCm = 22i32,
CleanShulkerBox = 44i32,
FlyOneCm = 12i32,
PlayerKills = 35i32,
InspectDropper = 47i32,
InteractWithLoom = 68i32,
DamageDealt = 25i32,
InspectDispenser = 49i32,
UseCauldron = 41i32,
TimeSinceRest = 4i32,
InteractWithBrewingstand = 45i32,
InteractWithAnvil = 73i32,
TuneNoteblock = 51i32,
BellRing = 70i32,
FillCauldron = 40i32,
FishCaught = 36i32,
ClimbOneCm = 11i32,
InteractWithSmithingTable = 76i32,
TriggerTrappedChest = 53i32,
InteractWithSmoker = 64i32,
OpenEnderchest = 54i32,
CleanBanner = 43i32,
LeaveGame = 0i32,
SleepInBed = 60i32,
DamageDealtAbsorbed = 26i32,
WalkUnderWaterOneCm = 13i32,
TradedWithVillager = 38i32,
InteractWithStonecutter = 69i32,
HorseOneCm = 18i32,
DamageAbsorbed = 30i32,
TargetHit = 75i32,
}
impl CustomStatistic {
#[must_use]
pub const fn from_i32(id: i32) -> Option<Self> {
match id {
29i32 => Some(Self::DamageBlockedByShield),
46i32 => Some(Self::InteractWithBeacon),
10i32 => Some(Self::FallOneCm),
59i32 => Some(Self::OpenChest),
1i32 => Some(Self::PlayTime),
6i32 => Some(Self::WalkOneCm),
72i32 => Some(Self::RaidWin),
27i32 => Some(Self::DamageDealtResisted),
42i32 => Some(Self::CleanArmor),
20i32 => Some(Self::SwimOneCm),
17i32 => Some(Self::HappyGhastOneCm),
37i32 => Some(Self::TalkedToVillager),
55i32 => Some(Self::EnchantItem),
24i32 => Some(Self::Drop),
52i32 => Some(Self::PotFlower),
61i32 => Some(Self::OpenShulkerBox),
9i32 => Some(Self::WalkOnWaterOneCm),
66i32 => Some(Self::InteractWithCampfire),
57i32 => Some(Self::InteractWithFurnace),
65i32 => Some(Self::InteractWithLectern),
71i32 => Some(Self::RaidTrigger),
21i32 => Some(Self::StriderOneCm),
7i32 => Some(Self::CrouchOneCm),
39i32 => Some(Self::EatCakeSlice),
56i32 => Some(Self::PlayRecord),
58i32 => Some(Self::InteractWithCraftingTable),
5i32 => Some(Self::SneakTime),
15i32 => Some(Self::BoatOneCm),
31i32 => Some(Self::DamageResisted),
32i32 => Some(Self::Deaths),
2i32 => Some(Self::TotalWorldTime),
16i32 => Some(Self::PigOneCm),
19i32 => Some(Self::AviateOneCm),
23i32 => Some(Self::Jump),
34i32 => Some(Self::AnimalsBred),
48i32 => Some(Self::InspectHopper),
63i32 => Some(Self::InteractWithBlastFurnace),
8i32 => Some(Self::SprintOneCm),
28i32 => Some(Self::DamageTaken),
33i32 => Some(Self::MobKills),
67i32 => Some(Self::InteractWithCartographyTable),
3i32 => Some(Self::TimeSinceDeath),
14i32 => Some(Self::MinecartOneCm),
74i32 => Some(Self::InteractWithGrindstone),
50i32 => Some(Self::PlayNoteblock),
62i32 => Some(Self::OpenBarrel),
22i32 => Some(Self::NautilusOneCm),
44i32 => Some(Self::CleanShulkerBox),
12i32 => Some(Self::FlyOneCm),
35i32 => Some(Self::PlayerKills),
47i32 => Some(Self::InspectDropper),
68i32 => Some(Self::InteractWithLoom),
25i32 => Some(Self::DamageDealt),
49i32 => Some(Self::InspectDispenser),
41i32 => Some(Self::UseCauldron),
4i32 => Some(Self::TimeSinceRest),
45i32 => Some(Self::InteractWithBrewingstand),
73i32 => Some(Self::InteractWithAnvil),
51i32 => Some(Self::TuneNoteblock),
70i32 => Some(Self::BellRing),
40i32 => Some(Self::FillCauldron),
36i32 => Some(Self::FishCaught),
11i32 => Some(Self::ClimbOneCm),
76i32 => Some(Self::InteractWithSmithingTable),
53i32 => Some(Self::TriggerTrappedChest),
64i32 => Some(Self::InteractWithSmoker),
54i32 => Some(Self::OpenEnderchest),
43i32 => Some(Self::CleanBanner),
0i32 => Some(Self::LeaveGame),
60i32 => Some(Self::SleepInBed),
26i32 => Some(Self::DamageDealtAbsorbed),
13i32 => Some(Self::WalkUnderWaterOneCm),
38i32 => Some(Self::TradedWithVillager),
69i32 => Some(Self::InteractWithStonecutter),
18i32 => Some(Self::HorseOneCm),
30i32 => Some(Self::DamageAbsorbed),
75i32 => Some(Self::TargetHit),
_ => None,
}
}
}
impl TryFrom<i32> for CustomStatistic {
type Error = ();
fn try_from(value: i32) -> Result<Self, Self::Error> {
Self::from_i32(value).ok_or(())
}
}

View File

@ -42,6 +42,11 @@ pub mod screen;
#[path = "generated/particle.rs"] #[path = "generated/particle.rs"]
pub mod particle; pub mod particle;
#[cfg(feature = "statistic")]
#[rustfmt::skip]
#[path = "generated/statistic.rs"]
pub mod statistic;
#[cfg(feature = "sound")] #[cfg(feature = "sound")]
#[rustfmt::skip] #[rustfmt::skip]
#[path = "generated/sound_category.rs"] #[path = "generated/sound_category.rs"]

View File

@ -16,6 +16,7 @@ pumpkin-data = { workspace = true, features = [
"fuels", "fuels",
"enchantment", "enchantment",
"potion", "potion",
"statistic",
] } ] }
pumpkin-world.workspace = true pumpkin-world.workspace = true

View File

@ -32,6 +32,7 @@ use crate::slot::{BoxFuture, NormalSlot, Slot};
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::recipes::{CraftingRecipeTypes, RECIPES_CRAFTING}; use pumpkin_data::recipes::{CraftingRecipeTypes, RECIPES_CRAFTING};
use pumpkin_data::screen::WindowType; use pumpkin_data::screen::WindowType;
use pumpkin_data::statistic::StatisticCategory;
use pumpkin_data::tag; use pumpkin_data::tag;
use pumpkin_data::tag::Taggable; use pumpkin_data::tag::Taggable;
use pumpkin_protocol::codec::recipe::{DynamicRecipe, OwnedCraftingRecipe}; use pumpkin_protocol::codec::recipe::{DynamicRecipe, OwnedCraftingRecipe};
@ -437,10 +438,17 @@ impl Slot for ResultSlot {
} }
fn on_take_item<'a>( fn on_take_item<'a>(
&'a self, &'a self,
_player: &'a dyn InventoryPlayer, player: &'a dyn InventoryPlayer,
_stack: &'a ItemStack, stack: &'a ItemStack,
) -> BoxFuture<'a, ()> { ) -> BoxFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
player
.increment_stat(
StatisticCategory::Crafted,
stack.item.id as i32,
stack.item_count as i32,
)
.await;
for i in 0..self.inventory.size() { for i in 0..self.inventory.size() {
let slot = self.inventory.get_stack(i).await; let slot = self.inventory.get_stack(i).await;
let mut stack = slot.lock().await; let mut stack = slot.lock().await;
@ -640,7 +648,11 @@ impl ScreenHandler for CraftingTableScreenHandler {
if stack.item_count == stack_prev.item_count { if stack.item_count == stack_prev.item_count {
return ItemStack::EMPTY.clone(); return ItemStack::EMPTY.clone();
} }
slot.on_take_item(player, &stack).await;
let mut taken_stack = stack_prev.clone();
taken_stack.set_count(stack_prev.item_count - stack.item_count);
slot.on_take_item(player, &taken_stack).await;
if slot_index == 0 { if slot_index == 0 {
slot.on_quick_move_crafted(stack.clone(), stack_prev.clone()) slot.on_quick_move_crafted(stack.clone(), stack_prev.clone())
.await; .await;

View File

@ -6,6 +6,7 @@ use pumpkin_data::data_component_impl::EnchantableImpl;
use pumpkin_data::item::Item; use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::screen::WindowType; use pumpkin_data::screen::WindowType;
use pumpkin_data::statistic::{CustomStatistic, StatisticCategory};
use pumpkin_data::tag::{Enchantment as EnchantmentTag, Taggable}; use pumpkin_data::tag::{Enchantment as EnchantmentTag, Taggable};
use pumpkin_util::random::RandomImpl; use pumpkin_util::random::RandomImpl;
use pumpkin_world::inventory::Inventory; use pumpkin_world::inventory::Inventory;
@ -357,6 +358,14 @@ impl ScreenHandler for EnchantingTableScreenHandler {
self.update_enchantments(player).await; self.update_enchantments(player).await;
self.send_content_updates().await; self.send_content_updates().await;
player
.increment_stat(
StatisticCategory::Custom,
CustomStatistic::EnchantItem as i32,
1,
)
.await;
true true
}) })
} }

View File

@ -8,7 +8,7 @@
use std::sync::{Arc, atomic::AtomicU8}; use std::sync::{Arc, atomic::AtomicU8};
use pumpkin_data::{fuels::is_fuel, item::Item}; use pumpkin_data::{fuels::is_fuel, item::Item, statistic::StatisticCategory};
use pumpkin_world::{block::entities::ExperienceContainer, inventory::Inventory}; use pumpkin_world::{block::entities::ExperienceContainer, inventory::Inventory};
use tracing::debug; use tracing::debug;
@ -140,10 +140,17 @@ impl Slot for FurnaceOutputSlot {
fn on_take_item<'a>( fn on_take_item<'a>(
&'a self, &'a self,
player: &'a dyn InventoryPlayer, player: &'a dyn InventoryPlayer,
_stack: &'a pumpkin_data::item_stack::ItemStack, stack: &'a pumpkin_data::item_stack::ItemStack,
) -> BoxFuture<'a, ()> { ) -> BoxFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
debug!("FurnaceOutputSlot: on_take_item called"); debug!("FurnaceOutputSlot: on_take_item called");
player
.increment_stat(
StatisticCategory::Crafted,
stack.item.id as i32,
stack.item_count as i32,
)
.await;
// Extract accumulated experience and award to player // Extract accumulated experience and award to player
let experience = self.experience_container.extract_experience(); let experience = self.experience_container.extract_experience();
debug!("FurnaceOutputSlot: extracted experience = {experience}"); debug!("FurnaceOutputSlot: extracted experience = {experience}");

View File

@ -245,7 +245,9 @@ impl ScreenHandler for PlayerScreenHandler {
return ItemStack::EMPTY.clone(); return ItemStack::EMPTY.clone();
} }
slot.on_take_item(player, &stack).await; let mut taken_stack = stack_prev.clone();
taken_stack.set_count(stack_prev.item_count - stack.item_count);
slot.on_take_item(player, &taken_stack).await;
if slot_index == 0 { if slot_index == 0 {
// From crafting result slot (0) // From crafting result slot (0)

View File

@ -35,6 +35,7 @@ use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::{ use pumpkin_data::{
data_component_impl::{EquipmentSlot, EquipmentType, EquippableImpl}, data_component_impl::{EquipmentSlot, EquipmentType, EquippableImpl},
screen::WindowType, screen::WindowType,
statistic::StatisticCategory,
}; };
use pumpkin_protocol::{ use pumpkin_protocol::{
codec::item_stack_seralizer::OptionalItemStackHash, codec::item_stack_seralizer::OptionalItemStackHash,
@ -187,6 +188,14 @@ pub trait InventoryPlayer: Send + Sync {
/// Awards experience points to the player (used for furnace smelting, etc.) /// Awards experience points to the player (used for furnace smelting, etc.)
fn award_experience(&self, amount: i32) -> PlayerFuture<'_, ()>; fn award_experience(&self, amount: i32) -> PlayerFuture<'_, ()>;
/// Increments a statistic for the player.
fn increment_stat(
&self,
category: StatisticCategory,
stat_id: i32,
amount: i32,
) -> PlayerFuture<'_, ()>;
} }
/// Gives a stack to the player or drops it if inventory is full. /// Gives a stack to the player or drops it if inventory is full.

View File

@ -13,6 +13,7 @@ use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::recipes::{RECIPES_STONECUTTING, StonecutterRecipe}; use pumpkin_data::recipes::{RECIPES_STONECUTTING, StonecutterRecipe};
use pumpkin_data::screen::WindowType; use pumpkin_data::screen::WindowType;
use pumpkin_data::statistic::StatisticCategory;
use pumpkin_protocol::java::server::play::SlotActionType; use pumpkin_protocol::java::server::play::SlotActionType;
use pumpkin_world::inventory::Inventory; use pumpkin_world::inventory::Inventory;
use pumpkin_world::inventory::SimpleInventory; use pumpkin_world::inventory::SimpleInventory;
@ -201,10 +202,17 @@ impl Slot for StonecutterOutputSlot {
fn on_take_item<'a>( fn on_take_item<'a>(
&'a self, &'a self,
_player: &'a dyn InventoryPlayer, player: &'a dyn InventoryPlayer,
_stack: &'a ItemStack, stack: &'a ItemStack,
) -> BoxFuture<'a, ()> { ) -> BoxFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
player
.increment_stat(
StatisticCategory::Crafted,
stack.item.id as i32,
stack.item_count as i32,
)
.await;
let input_stack = self.input_inventory.get_stack(0).await; let input_stack = self.input_inventory.get_stack(0).await;
let mut input_lock = input_stack.lock().await; let mut input_lock = input_stack.lock().await;
if !input_lock.is_empty() { if !input_lock.is_empty() {

View File

@ -0,0 +1,17 @@
use pumpkin_data::packet::clientbound::PLAY_AWARD_STATS;
use pumpkin_macros::java_packet;
use crate::codec::var_int::VarInt;
#[derive(serde::Serialize)]
#[java_packet(PLAY_AWARD_STATS)]
pub struct CAwardStats<'a> {
pub stats: &'a [Statistic],
}
#[derive(serde::Serialize)]
pub struct Statistic {
pub category_id: VarInt,
pub statistic_id: VarInt,
pub value: VarInt,
}

View File

@ -1,3 +1,5 @@
mod award_stats;
pub use award_stats::*;
mod acknowledge_block; mod acknowledge_block;
mod actionbar; mod actionbar;
mod block_destroy_stage; mod block_destroy_stage;

View File

@ -297,7 +297,6 @@ impl WorldAquiferSampler {
let z = block_z + section_coords::section_to_block(offset_z as i32); let z = block_z + section_coords::section_to_block(offset_z as i32);
let n = height_estimator.estimate_height(x, z); let n = height_estimator.estimate_height(x, z);
println!("estimate_height({}, {}) = {}", x, z, n);
let o = n + 8; let o = n + 8;
let bl2 = offset_x == 0 && offset_z == 0; let bl2 = offset_x == 0 && offset_z == 0;

View File

@ -25,6 +25,13 @@ pub struct AnvilBlock;
impl BlockBehaviour for AnvilBlock { impl BlockBehaviour for AnvilBlock {
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> { fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
Box::pin(async move { Box::pin(async move {
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::InteractWithAnvil as i32,
1,
)
.await;
args.player args.player
.open_handled_screen(&AnvilScreenFactory, Some(*args.position)) .open_handled_screen(&AnvilScreenFactory, Some(*args.position))
.await; .await;

View File

@ -64,6 +64,13 @@ impl BlockBehaviour for BarrelBlock {
if let Some(block_entity) = args.world.get_block_entity(args.position) if let Some(block_entity) = args.world.get_block_entity(args.position)
&& let Some(inventory) = block_entity.get_inventory() && let Some(inventory) = block_entity.get_inventory()
{ {
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::OpenBarrel as i32,
1,
)
.await;
args.player args.player
.open_handled_screen(&BarrelScreenFactory(inventory), Some(*args.position)) .open_handled_screen(&BarrelScreenFactory(inventory), Some(*args.position))
.await; .await;

View File

@ -58,6 +58,14 @@ impl BlockBehaviour for BeaconBlock {
return BlockActionResult::Fail; return BlockActionResult::Fail;
}; };
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::InteractWithBeacon as i32,
1,
)
.await;
// Open the screen using the factory // Open the screen using the factory
args.player args.player
.open_handled_screen(&BeaconScreenFactory(inventory), Some(*args.position)) .open_handled_screen(&BeaconScreenFactory(inventory), Some(*args.position))

View File

@ -359,6 +359,13 @@ impl BlockBehaviour for BedBlock {
} }
args.player.sleep(bed_head_pos); args.player.sleep(bed_head_pos);
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::SleepInBed as i32,
1,
)
.await;
Self::set_occupied(true, args.world, args.block, args.position, state_id).await; Self::set_occupied(true, args.world, args.block, args.position, state_id).await;
BlockActionResult::SuccessServer BlockActionResult::SuccessServer

View File

@ -91,6 +91,13 @@ impl BlockBehaviour for BlastFurnaceBlock {
&& let Some(property_delegate) = block_entity.clone().to_property_delegate() && let Some(property_delegate) = block_entity.clone().to_property_delegate()
&& let Some(experience_container) = block_entity.to_experience_container() && let Some(experience_container) = block_entity.to_experience_container()
{ {
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::InteractWithBlastFurnace as i32,
1,
)
.await;
let blasting_furnace_screen_factory = BlastingFurnaceScreenFactory::new( let blasting_furnace_screen_factory = BlastingFurnaceScreenFactory::new(
inventory, inventory,
property_delegate, property_delegate,

View File

@ -56,6 +56,13 @@ impl BlockBehaviour for BrewingStandBlock {
&& let Some(inventory) = block_entity.clone().get_inventory() && let Some(inventory) = block_entity.clone().get_inventory()
&& let Some(pd) = block_entity.clone().to_property_delegate() && let Some(pd) = block_entity.clone().to_property_delegate()
{ {
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::InteractWithBrewingstand as i32,
1,
)
.await;
args.player args.player
.open_handled_screen(&BrewingScreenFactory(inventory, pd), Some(*args.position)) .open_handled_screen(&BrewingScreenFactory(inventory, pd), Some(*args.position))
.await; .await;

View File

@ -54,6 +54,13 @@ impl CakeBlock {
let mut properties = CakeLikeProperties::from_state_id(state_id, block); let mut properties = CakeLikeProperties::from_state_id(state_id, block);
match properties.bites { match properties.bites {
0..=5 => { 0..=5 => {
player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::EatCakeSlice as i32,
1,
)
.await;
properties.bites += 1; properties.bites += 1;
world world
.set_block_state( .set_block_state(
@ -65,6 +72,13 @@ impl CakeBlock {
BlockActionResult::Consume BlockActionResult::Consume
} }
6 => { 6 => {
player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::EatCakeSlice as i32,
1,
)
.await;
world world
.set_block_state( .set_block_state(
location, location,

View File

@ -129,6 +129,13 @@ async fn placed_chest_impl<E: BlockEntity + 'static>(
} }
async fn normal_use_chest_impl(args: NormalUseArgs<'_>) -> BlockActionResult { async fn normal_use_chest_impl(args: NormalUseArgs<'_>) -> BlockActionResult {
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::OpenChest as i32,
1,
)
.await;
let state = args.world.get_block_state_id(args.position); let state = args.world.get_block_state_id(args.position);
let first_chest = args.world.get_block_entity(args.position); let first_chest = args.world.get_block_entity(args.position);

View File

@ -18,6 +18,13 @@ pub struct CraftingTableBlock;
impl BlockBehaviour for CraftingTableBlock { impl BlockBehaviour for CraftingTableBlock {
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> { fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
Box::pin(async move { Box::pin(async move {
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::InteractWithCraftingTable as i32,
1,
)
.await;
args.player args.player
.open_handled_screen( .open_handled_screen(
&CraftingTableScreenFactory(args.server.recipe_manager.clone()), &CraftingTableScreenFactory(args.server.recipe_manager.clone()),

View File

@ -85,14 +85,19 @@ impl BlockBehaviour for EnderChestBlock {
{ {
let inventory = args.player.ender_chest_inventory(); let inventory = args.player.ender_chest_inventory();
inventory.set_tracker(block_entity.get_tracker()).await; inventory.set_tracker(block_entity.get_tracker()).await;
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::OpenEnderchest as i32,
1,
)
.await;
args.player args.player
.open_handled_screen( .open_handled_screen(
&EnderChestScreenFactory(inventory.clone()), &EnderChestScreenFactory(inventory.clone()),
Some(*args.position), Some(*args.position),
) )
.await; .await;
// TODO: player.incrementStat(Stats.OPEN_ENDERCHEST);
// TODO: PiglinBrain.onGuardedBlockInteracted(serverWorld, player, true); // TODO: PiglinBrain.onGuardedBlockInteracted(serverWorld, player, true);
} }

View File

@ -90,6 +90,13 @@ impl BlockBehaviour for FurnaceBlock {
&& let Some(property_delegate) = block_entity.clone().to_property_delegate() && let Some(property_delegate) = block_entity.clone().to_property_delegate()
&& let Some(experience_container) = block_entity.to_experience_container() && let Some(experience_container) = block_entity.to_experience_container()
{ {
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::InteractWithFurnace as i32,
1,
)
.await;
let furnace_screen_factory = let furnace_screen_factory =
FurnaceScreenFactory::new(inventory, property_delegate, experience_container); FurnaceScreenFactory::new(inventory, property_delegate, experience_container);
args.player args.player

View File

@ -62,6 +62,13 @@ impl BlockBehaviour for HopperBlock {
if let Some(block_entity) = args.world.get_block_entity(args.position) if let Some(block_entity) = args.world.get_block_entity(args.position)
&& let Some(inventory) = block_entity.get_inventory() && let Some(inventory) = block_entity.get_inventory()
{ {
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::InspectHopper as i32,
1,
)
.await;
args.player args.player
.open_handled_screen(&HopperBlockScreenFactory(inventory), Some(*args.position)) .open_handled_screen(&HopperBlockScreenFactory(inventory), Some(*args.position))
.await; .await;

View File

@ -168,8 +168,15 @@ impl BlockBehaviour for JukeboxBlock {
// Start playing the music (client-side audio) // Start playing the music (client-side audio)
Self::start_playing(args.position, world, jukebox_song.get_id()); Self::start_playing(args.position, world, jukebox_song.get_id());
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::PlayRecord as i32,
1,
)
.await;
// TODO: world.emitGameEvent(GameEvent.BLOCK_CHANGE, pos, ...) // TODO: world.emitGameEvent(GameEvent.BLOCK_CHANGE, pos, ...)
// TODO: player.incrementStat(Stats.PLAY_RECORD)
BlockActionResult::Success BlockActionResult::Success
}) })

View File

@ -60,6 +60,13 @@ impl BlockBehaviour for LecternBlock {
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> { fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
Box::pin(async move { Box::pin(async move {
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::InteractWithLectern as i32,
1,
)
.await;
if let Some(block_entity) = args.world.get_block_entity(args.position) if let Some(block_entity) = args.world.get_block_entity(args.position)
&& let Some(lectern_entity) = && let Some(lectern_entity) =
block_entity.as_any().downcast_ref::<LecternBlockEntity>() block_entity.as_any().downcast_ref::<LecternBlockEntity>()

View File

@ -95,6 +95,14 @@ impl BlockBehaviour for NoteBlock {
.await; .await;
Self::play_note(&note_props, args.world, args.position).await; Self::play_note(&note_props, args.world, args.position).await;
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::TuneNoteblock as i32,
1,
)
.await;
BlockActionResult::Success BlockActionResult::Success
}) })
} }

View File

@ -139,6 +139,14 @@ impl BlockBehaviour for BellBlock {
args.hit.face.to_horizontal_facing(), args.hit.face.to_horizontal_facing(),
); );
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::BellRing as i32,
1,
)
.await;
BlockActionResult::Success BlockActionResult::Success
}) })
} }

View File

@ -88,6 +88,13 @@ impl BlockBehaviour for ShulkerBoxBlock {
if let Some(block_entity) = args.world.get_block_entity(args.position) if let Some(block_entity) = args.world.get_block_entity(args.position)
&& let Some(inventory) = block_entity.get_inventory() && let Some(inventory) = block_entity.get_inventory()
{ {
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::OpenShulkerBox as i32,
1,
)
.await;
args.player args.player
.open_handled_screen(&ShulkerBoxScreenFactory(inventory), Some(*args.position)) .open_handled_screen(&ShulkerBoxScreenFactory(inventory), Some(*args.position))
.await; .await;

View File

@ -89,6 +89,13 @@ impl BlockBehaviour for SmokerBlock {
&& let Some(property_delegate) = block_entity.clone().to_property_delegate() && let Some(property_delegate) = block_entity.clone().to_property_delegate()
&& let Some(experience_container) = block_entity.to_experience_container() && let Some(experience_container) = block_entity.to_experience_container()
{ {
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::InteractWithSmoker as i32,
1,
)
.await;
let smoker_screen_factory = let smoker_screen_factory =
SmokerScreenFactory::new(inventory, property_delegate, experience_container); SmokerScreenFactory::new(inventory, property_delegate, experience_container);
args.player args.player

View File

@ -19,6 +19,13 @@ pub struct StonecutterBlock;
impl BlockBehaviour for StonecutterBlock { impl BlockBehaviour for StonecutterBlock {
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> { fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
Box::pin(async move { Box::pin(async move {
args.player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::InteractWithStonecutter as i32,
1,
)
.await;
args.player args.player
.open_handled_screen(&StonecutterScreenFactory, Some(*args.position)) .open_handled_screen(&StonecutterScreenFactory, Some(*args.position))
.await; .await;

View File

@ -65,6 +65,20 @@ impl BreedGoal {
let entity = mob.get_entity(); let entity = mob.get_entity();
let world = entity.world.load(); let world = entity.world.load();
if let Some(player) = mob_entity
.breeder
.load()
.and_then(|uuid| world.get_player_by_uuid(uuid))
{
player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::AnimalsBred as i32,
1,
)
.await;
}
mob_entity.reset_love_ticks(); mob_entity.reset_love_ticks();
mob_entity mob_entity
.breeding_cooldown .breeding_cooldown

View File

@ -1,3 +1,4 @@
use crate::entity::player::statistics::StatisticCategory;
use crate::{entity::EntityBaseFuture, server::Server}; use crate::{entity::EntityBaseFuture, server::Server};
use core::f32; use core::f32;
use pumpkin_data::data_component_impl::DamageResistantImpl; use pumpkin_data::data_component_impl::DamageResistantImpl;
@ -453,17 +454,41 @@ impl EntityBase for ItemEntity {
return; return;
} }
let (item_id, count_before) = {
let stack = self.item_stack.lock().await;
(stack.item.id, stack.item_count)
};
let inserted = { let inserted = {
let mut stack = self.item_stack.lock().await; let mut stack = self.item_stack.lock().await;
player.inventory.insert_stack_anywhere(&mut stack).await player.inventory.insert_stack_anywhere(&mut stack).await
}; };
if inserted || player.is_creative() { if inserted || player.is_creative() {
let (item_count, is_empty) = { let (count_after, is_empty) = {
let stack = self.item_stack.lock().await; let stack = self.item_stack.lock().await;
(stack.item_count, stack.is_empty()) (stack.item_count, stack.is_empty())
}; };
player.living_entity.pickup(&self.entity, item_count.into());
let amount_picked_up = if player.is_creative() {
count_before
} else {
count_before - count_after
};
if amount_picked_up > 0 {
player
.increment_stat(
StatisticCategory::PickedUp,
item_id as i32,
amount_picked_up as i32,
)
.await;
}
player
.living_entity
.pickup(&self.entity, amount_picked_up.into());
player player
.current_screen_handler .current_screen_handler

View File

@ -28,6 +28,7 @@ use crate::entity::attributes::AttributeInstance;
use crate::entity::attributes::Modifier; use crate::entity::attributes::Modifier;
use crate::entity::attributes::ModifierOperation; use crate::entity::attributes::ModifierOperation;
use crate::entity::mob::slime::SlimeEntity; use crate::entity::mob::slime::SlimeEntity;
use crate::entity::player::statistics::{CustomStatistic, StatisticCategory};
use crate::entity::{EntityBaseFuture, NbtFuture}; use crate::entity::{EntityBaseFuture, NbtFuture};
use crate::server::Server; use crate::server::Server;
use crate::world::loot::{LootContextParameters, LootTableExt}; use crate::world::loot::{LootContextParameters, LootTableExt};
@ -1309,6 +1310,9 @@ impl LivingEntity {
self.movement_input.store(Vector3::default()); self.movement_input.store(Vector3::default());
self.jumping.store(false, Relaxed); self.jumping.store(false, Relaxed);
// Statistics updates
self.update_death_stats(&*dyn_self, cause).await;
// Plays the death sound // Plays the death sound
world.send_entity_status(&self.entity, EntityStatus::Death); world.send_entity_status(&self.entity, EntityStatus::Death);
let params = LootContextParameters { let params = LootContextParameters {
@ -1336,41 +1340,108 @@ impl LivingEntity {
} }
self.entity.pose.store(EntityPose::Dying); self.entity.pose.store(EntityPose::Dying);
let block_pos = self.entity.block_pos.load(); self.drop_equipment().await;
let armor_slots: Vec<Arc<Mutex<ItemStack>>> = {
let equipment_lock = self.entity_equipment.lock().await;
self.equipment_slots
.values()
.map(|slot| equipment_lock.get(slot))
.collect()
};
for equipment in armor_slots {
let item = {
let mut item_lock = equipment.lock().await;
mem::replace(&mut *item_lock, ItemStack::EMPTY.clone())
};
world.drop_stack(&block_pos, item).await;
}
// Broadcast death message if it's a player and the gamerule is enabled // Broadcast death message if it's a player and the gamerule is enabled
let show_death_messages = { world.level_info.load().game_rules.show_death_messages }; self.broadcast_death_message(&*dyn_self, damage_type, source, cause)
if self.entity.entity_type == &EntityType::PLAYER && show_death_messages { .await;
//TODO: KillCredit
let death_message =
Self::get_death_message(&*dyn_self, damage_type, source, cause).await;
if let Some(server) = world.server.upgrade() {
for player in server.get_all_players() {
player.send_system_message(&death_message).await;
}
}
}
self.reset_effects_and_attributes().await; self.reset_effects_and_attributes().await;
} }
} }
async fn drop_equipment(&self) {
let world = self.entity.world.load();
let block_pos = self.entity.block_pos.load();
let armor_slots: Vec<Arc<Mutex<ItemStack>>> = {
let equipment_lock = self.entity_equipment.lock().await;
self.equipment_slots
.values()
.map(|slot| equipment_lock.get(slot))
.collect()
};
for equipment in armor_slots {
let item = {
let mut item_lock = equipment.lock().await;
mem::replace(&mut *item_lock, ItemStack::EMPTY.clone())
};
world.drop_stack(&block_pos, item).await;
}
}
async fn broadcast_death_message(
&self,
dyn_self: &dyn EntityBase,
damage_type: DamageType,
source: Option<&dyn EntityBase>,
cause: Option<&dyn EntityBase>,
) {
let world = self.entity.world.load();
let show_death_messages = { world.level_info.load().game_rules.show_death_messages };
if self.entity.entity_type == &EntityType::PLAYER && show_death_messages {
//TODO: KillCredit
let death_message = Self::get_death_message(dyn_self, damage_type, source, cause).await;
if let Some(server) = world.server.upgrade() {
for player in server.get_all_players() {
player.send_system_message(&death_message).await;
}
}
}
}
async fn update_death_stats(&self, dyn_self: &dyn EntityBase, cause: Option<&dyn EntityBase>) {
if let Some(victim_player) = dyn_self.get_player() {
victim_player
.increment_stat(StatisticCategory::Custom, CustomStatistic::Deaths as i32, 1)
.await;
victim_player
.set_stat(
StatisticCategory::Custom,
CustomStatistic::TimeSinceDeath as i32,
0,
)
.await;
if let Some(killer_entity) = cause.map(EntityBase::get_entity) {
victim_player
.increment_stat(
StatisticCategory::KilledBy,
killer_entity.entity_type.id as i32,
1,
)
.await;
}
}
if let Some(killer_player) = cause.and_then(|c| c.get_player()) {
if dyn_self.get_player().is_some() {
killer_player
.increment_stat(
StatisticCategory::Custom,
CustomStatistic::PlayerKills as i32,
1,
)
.await;
} else {
killer_player
.increment_stat(
StatisticCategory::Custom,
CustomStatistic::MobKills as i32,
1,
)
.await;
}
killer_player
.increment_stat(
StatisticCategory::Killed,
self.entity.entity_type.id as i32,
1,
)
.await;
}
}
async fn drop_loot(&self, params: LootContextParameters) { async fn drop_loot(&self, params: LootContextParameters) {
if let Some(loot_table) = &self.get_entity().entity_type.loot_table { if let Some(loot_table) = &self.get_entity().entity_type.loot_table {
let pos = self.entity.block_pos.load(); let pos = self.entity.block_pos.load();
@ -1938,6 +2009,28 @@ impl EntityBase for LivingEntity {
// Total damage after reductions // Total damage after reductions
let effective_amount = amount * (1.0 - resistance_reduction); let effective_amount = amount * (1.0 - resistance_reduction);
if resistance_reduction > 0.0 {
let resisted = amount * resistance_reduction;
if let Some(player) = caller.get_player() {
player
.increment_stat(
StatisticCategory::Custom,
CustomStatistic::DamageResisted as i32,
(resisted * 10.0) as i32,
)
.await;
}
if let Some(attacker_player) = cause.and_then(|c| c.get_player()) {
attacker_player
.increment_stat(
StatisticCategory::Custom,
CustomStatistic::DamageDealtResisted as i32,
(resisted * 10.0) as i32,
)
.await;
}
}
// Check for shield blocking // Check for shield blocking
if self.is_blocking().await if self.is_blocking().await
&& !damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_SHIELD) && !damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_SHIELD)
@ -1951,6 +2044,16 @@ impl EntityBase for LivingEntity {
if source_to_player.dot(&look_vec) < 0.0 { if source_to_player.dot(&look_vec) < 0.0 {
world.play_sound(Sound::ItemShieldBlock, SoundCategory::Players, &player_pos); world.play_sound(Sound::ItemShieldBlock, SoundCategory::Players, &player_pos);
if let Some(player) = caller.get_player() {
player
.increment_stat(
StatisticCategory::Custom,
CustomStatistic::DamageBlockedByShield as i32,
(effective_amount * 10.0) as i32,
)
.await;
}
if let Some(attacker_player) = cause.and_then(|c| c.get_player()) { if let Some(attacker_player) = cause.and_then(|c| c.get_player()) {
let held_item = attacker_player.inventory().held_item(); let held_item = attacker_player.inventory().held_item();
let is_axe = held_item.lock().await.is_axe(); let is_axe = held_item.lock().await.is_axe();
@ -1995,6 +2098,15 @@ impl EntityBase for LivingEntity {
let durability_damage = (amount / 1.0).floor().max(1.0) as i32; let durability_damage = (amount / 1.0).floor().max(1.0) as i32;
if stack.damage_item(durability_damage) == DamageResult::Broken { if stack.damage_item(durability_damage) == DamageResult::Broken {
if let Some(player) = caller.get_player() {
player
.increment_stat(
StatisticCategory::Broken,
stack.item.id as i32,
1,
)
.await;
}
world.send_entity_status( world.send_entity_status(
&self.entity, &self.entity,
crate::entity::equipment_break_status(&slot), crate::entity::equipment_break_status(&slot),
@ -2084,6 +2196,27 @@ impl EntityBase for LivingEntity {
let mut remaining = damage_amount; let mut remaining = damage_amount;
let current_abs = self.absorption.load(); let current_abs = self.absorption.load();
if current_abs > 0.0 { if current_abs > 0.0 {
let absorbed = current_abs.min(remaining);
if let Some(player) = caller.get_player() {
player
.increment_stat(
StatisticCategory::Custom,
CustomStatistic::DamageAbsorbed as i32,
(absorbed * 10.0) as i32,
)
.await;
}
if let Some(attacker_player) = cause.and_then(|c| c.get_player()) {
attacker_player
.increment_stat(
StatisticCategory::Custom,
CustomStatistic::DamageDealtAbsorbed as i32,
(absorbed * 10.0) as i32,
)
.await;
}
if current_abs >= remaining { if current_abs >= remaining {
let new_abs = current_abs - remaining; let new_abs = current_abs - remaining;
self.set_absorption(new_abs).await; self.set_absorption(new_abs).await;
@ -2109,6 +2242,27 @@ impl EntityBase for LivingEntity {
if remaining > 0.0 { if remaining > 0.0 {
self.set_health(clamped_health); self.set_health(clamped_health);
// Statistics updates
if let Some(player) = caller.get_player() {
player
.increment_stat(
StatisticCategory::Custom,
CustomStatistic::DamageTaken as i32,
(remaining * 10.0) as i32,
)
.await;
}
if let Some(attacker_player) = cause.and_then(|c| c.get_player()) {
attacker_player
.increment_stat(
StatisticCategory::Custom,
CustomStatistic::DamageDealt as i32,
(remaining * 10.0) as i32,
)
.await;
}
// Track attacker for RevengeGoal (only after confirming damage) // Track attacker for RevengeGoal (only after confirming damage)
if let Some(attacker) = cause.or(source) { if let Some(attacker) = cause.or(source) {
self.last_attacker_id self.last_attacker_id

View File

@ -72,6 +72,7 @@ pub struct MobEntity {
pub position_target_range: AtomicI32, pub position_target_range: AtomicI32,
pub love_ticks: AtomicI32, pub love_ticks: AtomicI32,
pub breeding_cooldown: AtomicI32, pub breeding_cooldown: AtomicI32,
pub breeder: AtomicCell<Option<Uuid>>,
mob_flags: AtomicU8, mob_flags: AtomicU8,
last_sent_yaw: AtomicU8, last_sent_yaw: AtomicU8,
last_sent_pitch: AtomicU8, last_sent_pitch: AtomicU8,
@ -108,6 +109,7 @@ impl MobEntity {
position_target_range: AtomicI32::new(-1), position_target_range: AtomicI32::new(-1),
love_ticks: AtomicI32::new(0), love_ticks: AtomicI32::new(0),
breeding_cooldown: AtomicI32::new(0), breeding_cooldown: AtomicI32::new(0),
breeder: AtomicCell::new(None),
mob_flags: AtomicU8::new(0), mob_flags: AtomicU8::new(0),
last_sent_yaw: AtomicU8::new(0), last_sent_yaw: AtomicU8::new(0),
last_sent_pitch: AtomicU8::new(0), last_sent_pitch: AtomicU8::new(0),
@ -153,8 +155,9 @@ impl MobEntity {
self.love_ticks.load(Relaxed) > 0 self.love_ticks.load(Relaxed) > 0
} }
pub fn set_love_ticks(&self, ticks: i32) { pub fn set_love_ticks(&self, ticks: i32, breeder: Option<Uuid>) {
self.love_ticks.store(ticks, Relaxed); self.love_ticks.store(ticks, Relaxed);
self.breeder.store(breeder);
} }
pub fn reset_love_ticks(&self) { pub fn reset_love_ticks(&self) {

View File

@ -121,7 +121,8 @@ impl Mob for ChickenEntity {
if is_food && self.is_breeding_ready() && !self.is_in_love() { if is_food && self.is_breeding_ready() && !self.is_in_love() {
item_stack.decrement_unless_creative(player.gamemode.load(), 1); item_stack.decrement_unless_creative(player.gamemode.load(), 1);
self.mob_entity.set_love_ticks(600); self.mob_entity
.set_love_ticks(600, Some(player.gameprofile.id));
let entity = &self.mob_entity.living_entity.entity; let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load(); let world = entity.world.load();
let pos = entity.pos.load(); let pos = entity.pos.load();

View File

@ -82,7 +82,8 @@ impl Mob for CowEntity {
if is_food && self.is_breeding_ready() && !self.is_in_love() { if is_food && self.is_breeding_ready() && !self.is_in_love() {
item_stack.decrement_unless_creative(player.gamemode.load(), 1); item_stack.decrement_unless_creative(player.gamemode.load(), 1);
self.mob_entity.set_love_ticks(600); self.mob_entity
.set_love_ticks(600, Some(player.gameprofile.id));
let entity = &self.mob_entity.living_entity.entity; let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load(); let world = entity.world.load();
let pos = entity.pos.load(); let pos = entity.pos.load();

View File

@ -87,7 +87,8 @@ impl Mob for PigEntity {
if is_food && self.is_breeding_ready() && !self.is_in_love() { if is_food && self.is_breeding_ready() && !self.is_in_love() {
item_stack.decrement_unless_creative(player.gamemode.load(), 1); item_stack.decrement_unless_creative(player.gamemode.load(), 1);
self.mob_entity.set_love_ticks(600); self.mob_entity
.set_love_ticks(600, Some(player.gameprofile.id));
let entity = &self.mob_entity.living_entity.entity; let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load(); let world = entity.world.load();
let pos = entity.pos.load(); let pos = entity.pos.load();

View File

@ -150,7 +150,8 @@ impl Mob for SheepEntity {
if is_food && self.is_breeding_ready() && !self.is_in_love() { if is_food && self.is_breeding_ready() && !self.is_in_love() {
item_stack.decrement_unless_creative(player.gamemode.load(), 1); item_stack.decrement_unless_creative(player.gamemode.load(), 1);
self.mob_entity.set_love_ticks(600); self.mob_entity
.set_love_ticks(600, Some(player.gameprofile.id));
let entity = &self.mob_entity.living_entity.entity; let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load(); let world = entity.world.load();
let pos = entity.pos.load(); let pos = entity.pos.load();

View File

@ -466,6 +466,14 @@ impl Mob for VillagerEntity {
} }
drop(offers); drop(offers);
player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::TalkedToVillager as i32,
1,
)
.await;
self.open_trading_screen(&player); self.open_trading_screen(&player);
true true

View File

@ -1,4 +1,5 @@
pub mod advancement; pub mod advancement;
pub mod statistics;
use core::f32; use core::f32;
use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque}; use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
@ -42,6 +43,7 @@ use pumpkin_data::entity::{EntityPose, EntityStatus, EntityType};
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::particle::Particle; use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::statistic::StatisticCategory;
use pumpkin_data::tag::Taggable; use pumpkin_data::tag::Taggable;
use pumpkin_data::{Block, BlockState, Enchantment, screen::WindowType, tag, translation}; use pumpkin_data::{Block, BlockState, Enchantment, screen::WindowType, tag, translation};
use pumpkin_inventory::player::{ use pumpkin_inventory::player::{
@ -61,15 +63,15 @@ use pumpkin_protocol::bedrock::client::container_open::CContainerOpen;
use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::codec::var_long::VarLong; use pumpkin_protocol::codec::var_long::VarLong;
use pumpkin_protocol::java::client::play::{ use pumpkin_protocol::java::client::play::{
Animation, CAcknowledgeBlockChange, CActionBar, CChangeDifficulty, CCloseContainer, Animation, CAcknowledgeBlockChange, CActionBar, CAwardStats, CChangeDifficulty,
CCombatDeath, CCustomPayload, CDisguisedChatMessage, CEntityAnimation, CEntityPositionSync, CCloseContainer, CCombatDeath, CCustomPayload, CDisguisedChatMessage, CEntityAnimation,
CGameEvent, CItemCooldown, CMapItemData, COpenScreen, CParticle, CPlayerAbilities, CEntityPositionSync, CGameEvent, CItemCooldown, CMapItemData, COpenScreen, CParticle,
CPlayerInfoUpdate, CPlayerPosition, CPlayerSpawnPosition, CRespawn, CSetContainerContent, CPlayerAbilities, CPlayerInfoUpdate, CPlayerPosition, CPlayerSpawnPosition, CRespawn,
CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CSetEquipment, CSetExperience, CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CSetEquipment,
CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound, CSubtitle, CSetExperience, CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound,
CSystemChatMessage, CTabList, CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect, CSubtitle, CSystemChatMessage, CTabList, CTitleAnimation, CTitleText, CUnloadChunk,
CUpdateTime, GameEvent, MapIcon, MapPatch, Metadata, PlayerAction, PlayerInfoFlags, CUpdateMobEffect, CUpdateTime, GameEvent, MapIcon, MapPatch, Metadata, PlayerAction,
PreviousMessage, PlayerInfoFlags, PreviousMessage, Statistic,
}; };
use pumpkin_protocol::java::server::play::{ use pumpkin_protocol::java::server::play::{
SClickSlot, SContainerButtonClick, SRenameItem, SlotActionType, SClickSlot, SContainerButtonClick, SRenameItem, SlotActionType,
@ -440,6 +442,8 @@ pub struct Player {
/// ///
/// **Note:** When the `abilities` field is updated, the server should send a `send_abilities_update` packet to the client to notify them of the changes. /// **Note:** When the `abilities` field is updated, the server should send a `send_abilities_update` packet to the client to notify them of the changes.
pub abilities: Mutex<Abilities>, pub abilities: Mutex<Abilities>,
/// Player statistics
pub stats: Mutex<statistics::Statistics>,
/// The current stage of block destruction of the block the player is breaking. /// The current stage of block destruction of the block the player is breaking.
pub current_block_destroy_stage: AtomicI32, pub current_block_destroy_stage: AtomicI32,
/// Indicates if the player is currently mining a block. /// Indicates if the player is currently mining a block.
@ -647,6 +651,7 @@ impl Player {
mining: AtomicBool::new(false), mining: AtomicBool::new(false),
mining_pos: Mutex::new(BlockPos::ZERO), mining_pos: Mutex::new(BlockPos::ZERO),
abilities: Mutex::new(abilities), abilities: Mutex::new(abilities),
stats: Mutex::new(statistics::Statistics::default()),
gamemode: AtomicCell::new(gamemode), gamemode: AtomicCell::new(gamemode),
previous_gamemode: AtomicCell::new(None), previous_gamemode: AtomicCell::new(None),
// TODO: Send the CPlayerSpawnPosition packet when the client connects with proper values // TODO: Send the CPlayerSpawnPosition packet when the client connects with proper values
@ -852,6 +857,10 @@ impl Player {
/// Removes the [`Player`] out of the current [`World`]. /// Removes the [`Player`] out of the current [`World`].
pub async fn remove(self: &Arc<Self>) { pub async fn remove(self: &Arc<Self>) {
self.stats
.lock()
.await
.increment_custom(statistics::CustomStatistic::LeaveGame, 1);
let world = self.world(); let world = self.world();
world.remove_player(self, true).await; world.remove_player(self, true).await;
@ -1170,6 +1179,12 @@ impl Player {
// Send the break status before clearing the slot so the client can // Send the break status before clearing the slot so the client can
// use the item texture for break particles. // use the item texture for break particles.
if result == pumpkin_data::item_stack::DamageResult::Broken { if result == pumpkin_data::item_stack::DamageResult::Broken {
self.increment_stat(
statistics::StatisticCategory::Broken,
updated_stack.item.id as i32,
1,
)
.await;
self.world().send_entity_status( self.world().send_entity_status(
&self.living_entity.entity, &self.living_entity.entity,
super::equipment_break_status(slot), super::equipment_break_status(slot),
@ -1656,6 +1671,13 @@ impl Player {
None::<BlockPos>, None::<BlockPos>,
)]); )]);
self.set_stat(
statistics::StatisticCategory::Custom,
statistics::CustomStatistic::TimeSinceRest as i32,
0,
)
.await;
let chunk_pos = self.living_entity.entity.chunk_pos.load(); let chunk_pos = self.living_entity.entity.chunk_pos.load();
world.broadcast_to_chunk( world.broadcast_to_chunk(
chunk_pos, chunk_pos,
@ -1815,6 +1837,19 @@ impl Player {
.send_packet_now(&CAcknowledgeBlockChange::new(seq.into())) .send_packet_now(&CAcknowledgeBlockChange::new(seq.into()))
.await; .await;
} }
// Statistics updates
{
let mut stats = self.stats.lock().await;
stats.increment_custom(statistics::CustomStatistic::PlayTime, 1);
stats.increment_custom(statistics::CustomStatistic::TotalWorldTime, 1);
stats.increment_custom(statistics::CustomStatistic::TimeSinceDeath, 1);
stats.increment_custom(statistics::CustomStatistic::TimeSinceRest, 1);
if self.living_entity.entity.sneaking.load(Ordering::Relaxed) {
stats.increment_custom(statistics::CustomStatistic::SneakTime, 1);
}
}
{ {
let mut xp = self.experience_pick_up_delay.lock().await; let mut xp = self.experience_pick_up_delay.lock().await;
if *xp > 0 { if *xp > 0 {
@ -1942,6 +1977,10 @@ impl Player {
} }
pub async fn jump(&self) { pub async fn jump(&self) {
self.stats
.lock()
.await
.increment_custom(statistics::CustomStatistic::Jump, 1);
if self.living_entity.entity.is_sprinting() { if self.living_entity.entity.is_sprinting() {
self.add_exhaustion(0.2).await; self.add_exhaustion(0.2).await;
} else { } else {
@ -2144,6 +2183,122 @@ impl Player {
} }
} }
pub async fn send_stats(&self) {
if let ClientPlatform::Java(java) = &self.client {
let stats_guard = self.stats.lock().await;
let packet_stats: Vec<Statistic> = stats_guard
.stats
.iter()
.map(|((category, stat), value)| Statistic {
category_id: VarInt(*category),
statistic_id: VarInt(*stat),
value: VarInt(*value),
})
.collect();
java.enqueue_packet(&CAwardStats {
stats: &packet_stats,
})
.await;
}
}
pub async fn increment_stat(
&self,
category: statistics::StatisticCategory,
stat: i32,
amount: i32,
) {
self.stats.lock().await.increment(category, stat, amount);
}
pub async fn set_stat(&self, category: statistics::StatisticCategory, stat: i32, value: i32) {
self.stats.lock().await.set(category, stat, value);
}
pub async fn get_movement_statistic(&self) -> statistics::CustomStatistic {
let entity = self.get_entity();
if entity.has_vehicle().await {
let vehicle = entity.vehicle.lock().await;
if let Some(vehicle) = vehicle.as_ref() {
let entity_type = vehicle.get_entity().entity_type;
if entity_type == &EntityType::OAK_BOAT
|| entity_type == &EntityType::SPRUCE_BOAT
|| entity_type == &EntityType::BIRCH_BOAT
|| entity_type == &EntityType::JUNGLE_BOAT
|| entity_type == &EntityType::ACACIA_BOAT
|| entity_type == &EntityType::DARK_OAK_BOAT
|| entity_type == &EntityType::MANGROVE_BOAT
|| entity_type == &EntityType::CHERRY_BOAT
|| entity_type == &EntityType::BAMBOO_RAFT
{
return statistics::CustomStatistic::BoatOneCm;
}
if entity_type == &EntityType::MINECART
|| entity_type == &EntityType::CHEST_MINECART
|| entity_type == &EntityType::FURNACE_MINECART
|| entity_type == &EntityType::TNT_MINECART
|| entity_type == &EntityType::HOPPER_MINECART
|| entity_type == &EntityType::COMMAND_BLOCK_MINECART
|| entity_type == &EntityType::SPAWNER_MINECART
{
return statistics::CustomStatistic::MinecartOneCm;
}
if entity_type == &EntityType::HORSE
|| entity_type == &EntityType::DONKEY
|| entity_type == &EntityType::MULE
|| entity_type == &EntityType::SKELETON_HORSE
|| entity_type == &EntityType::ZOMBIE_HORSE
{
return statistics::CustomStatistic::HorseOneCm;
}
if entity_type == &EntityType::PIG {
return statistics::CustomStatistic::PigOneCm;
}
if entity_type == &EntityType::STRIDER {
return statistics::CustomStatistic::StriderOneCm;
}
}
}
if self.is_flying().await {
return statistics::CustomStatistic::FlyOneCm;
}
if entity.fall_flying.load(Ordering::Relaxed) {
return statistics::CustomStatistic::AviateOneCm;
}
if entity.swimming.load(Ordering::Relaxed) {
return statistics::CustomStatistic::SwimOneCm;
}
let pos = entity.block_pos.load();
let world = entity.world.load_full();
let block = world.get_block(&pos);
if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_CLIMBABLE) {
return statistics::CustomStatistic::ClimbOneCm;
}
if entity.touching_water.load(Ordering::Relaxed) {
return statistics::CustomStatistic::WalkUnderWaterOneCm;
}
if entity.sneaking.load(Ordering::Relaxed) {
return statistics::CustomStatistic::CrouchOneCm;
}
if entity.sprinting.load(Ordering::Relaxed) {
return statistics::CustomStatistic::SprintOneCm;
}
if !entity.on_ground.load(Ordering::Relaxed) && entity.velocity.load().y < -0.005 {
return statistics::CustomStatistic::FallOneCm;
}
statistics::CustomStatistic::WalkOneCm
}
/// Updates the client of the player's current permission level. /// Updates the client of the player's current permission level.
pub fn send_permission_lvl_update(&self) { pub fn send_permission_lvl_update(&self) {
let status = match self.permission_lvl.load() { let status = match self.permission_lvl.load() {
@ -2805,6 +2960,18 @@ impl Player {
} }
pub async fn drop_item(&self, item_stack: ItemStack) { pub async fn drop_item(&self, item_stack: ItemStack) {
self.increment_stat(
statistics::StatisticCategory::Dropped,
item_stack.item.id as i32,
item_stack.item_count as i32,
)
.await;
self.increment_stat(
statistics::StatisticCategory::Custom,
statistics::CustomStatistic::Drop as i32,
1,
)
.await;
let item_pos = self.living_entity.entity.pos.load() let item_pos = self.living_entity.entity.pos.load()
+ Vector3::new(0.0, self.living_entity.entity.get_eye_height() - 0.3, 0.0); + Vector3::new(0.0, self.living_entity.entity.get_eye_height() - 0.3, 0.0);
let entity = Entity::new(self.world(), item_pos, &EntityType::ITEM); let entity = Entity::new(self.world(), item_pos, &EntityType::ITEM);
@ -3926,6 +4093,7 @@ impl NBTStorage for Player {
nbt.put_bool("SpawnForced", respawn.force); nbt.put_bool("SpawnForced", respawn.force);
} }
nbt.put_int("XpSeed", self.enchantment_seed.load(Ordering::Relaxed)); nbt.put_int("XpSeed", self.enchantment_seed.load(Ordering::Relaxed));
self.stats.lock().await.write_nbt(nbt);
}) })
} }
@ -3983,6 +4151,7 @@ impl NBTStorage for Player {
nbt.get_int("XpSeed").unwrap_or(rand::random()), nbt.get_int("XpSeed").unwrap_or(rand::random()),
Ordering::Relaxed, Ordering::Relaxed,
); );
self.stats.lock().await.read_nbt(nbt);
}) })
} }
} }
@ -4686,4 +4855,15 @@ impl InventoryPlayer for Player {
} }
}) })
} }
fn increment_stat(
&self,
category: StatisticCategory,
stat_id: i32,
amount: i32,
) -> PlayerFuture<'_, ()> {
Box::pin(async move {
self.increment_stat(category, stat_id, amount).await;
})
}
} }

View File

@ -0,0 +1,52 @@
pub use pumpkin_data::statistic::{CustomStatistic, StatisticCategory};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::tag::NbtTag;
use std::collections::HashMap;
#[derive(Default)]
pub struct Statistics {
/// (Category ID, Statistic ID) -> Value
pub stats: HashMap<(i32, i32), i32>,
}
impl Statistics {
pub fn increment(&mut self, category: StatisticCategory, stat: i32, amount: i32) {
let entry = self.stats.entry((category as i32, stat)).or_insert(0);
*entry += amount;
}
pub fn increment_custom(&mut self, stat: CustomStatistic, amount: i32) {
self.increment(StatisticCategory::Custom, stat as i32, amount);
}
pub fn set(&mut self, category: StatisticCategory, stat: i32, value: i32) {
self.stats.insert((category as i32, stat), value);
}
#[must_use]
pub fn get(&self, category: StatisticCategory, stat: i32) -> i32 {
*self.stats.get(&(category as i32, stat)).unwrap_or(&0)
}
pub fn write_nbt(&self, nbt: &mut NbtCompound) {
let mut stats_compound = NbtCompound::new();
for ((category, stat), value) in &self.stats {
stats_compound.put_int(&format!("{category}:{stat}"), *value);
}
nbt.put_compound("Statistics", stats_compound);
}
pub fn read_nbt(&mut self, nbt: &NbtCompound) {
if let Some(stats_compound) = nbt.get_compound("Statistics") {
for (key, tag) in &stats_compound.child_tags {
let parts: Vec<&str> = key.split(':').collect();
if let (NbtTag::Int(value), [cat_str, stat_str]) = (tag, parts.as_slice())
&& let (Ok(category), Ok(stat)) =
(cat_str.parse::<i32>(), stat_str.parse::<i32>())
{
self.stats.insert((category, stat), *value);
}
}
}
}
}

View File

@ -47,7 +47,8 @@ impl FishingBobberEntity {
} }
} }
pub fn reel_in(&self, player: &Player) -> i32 { pub async fn reel_in(&self, player: &Player) -> i32 {
use pumpkin_data::item::Item;
let world = self.entity.world.load(); let world = self.entity.world.load();
let hooked_id = self.hooked_entity_id.load(Ordering::Relaxed); let hooked_id = self.hooked_entity_id.load(Ordering::Relaxed);
@ -67,8 +68,15 @@ impl FishingBobberEntity {
if self.bite_countdown.load(Ordering::Relaxed) > 0 { if self.bite_countdown.load(Ordering::Relaxed) > 0 {
// Caught something! // Caught something!
player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::FishCaught as i32,
1,
)
.await;
// TODO: Use actual loot tables. For now, just give a raw cod. // TODO: Use actual loot tables. For now, just give a raw cod.
use pumpkin_data::item::Item;
let _item_stack = ItemStack::new(1, &Item::COD); let _item_stack = ItemStack::new(1, &Item::COD);
// player.inventory().add_item(item_stack).await; // Need public add_item // player.inventory().add_item(item_stack).await; // Need public add_item

View File

@ -63,7 +63,7 @@ impl ItemBehaviour for FishingRodItem {
if let Some(bobber) = if let Some(bobber) =
bobber_base.cast_any().downcast_ref::<FishingBobberEntity>() bobber_base.cast_any().downcast_ref::<FishingBobberEntity>()
{ {
let _result = bobber.reel_in(player); let _result = bobber.reel_in(player).await;
// TODO: give items // TODO: give items
} }
bobber_base.get_entity().remove().await; bobber_base.get_entity().remove().await;

View File

@ -15,6 +15,7 @@ use crate::block::registry::BlockActionResult;
use crate::block::{self, BlockIsReplacing}; use crate::block::{self, BlockIsReplacing};
use crate::entity::EntityBase; use crate::entity::EntityBase;
use crate::entity::equipment_break_status; use crate::entity::equipment_break_status;
use crate::entity::player::statistics::{CustomStatistic, StatisticCategory};
use crate::entity::player::{ChatMode, ChatSession, Player}; use crate::entity::player::{ChatMode, ChatSession, Player};
use crate::error::PumpkinError; use crate::error::PumpkinError;
use crate::log_at_level; use crate::log_at_level;
@ -360,6 +361,15 @@ impl JavaClient {
let last_pos = entity.pos.load(); let last_pos = entity.pos.load();
player.get_entity().set_pos(pos); player.get_entity().set_pos(pos);
let distance = last_pos.squared_distance_to_vec(&pos).sqrt();
let cm = (distance * 100.0) as i32;
if cm > 0 {
let stat = player.get_movement_statistic().await;
player
.increment_stat(StatisticCategory::Custom, stat as i32, cm)
.await;
}
let height_difference = pos.y - last_pos.y; let height_difference = pos.y - last_pos.y;
if entity.on_ground.load(Ordering::Relaxed) && packet.collision & FLAG_ON_GROUND == 0 && height_difference > 0.0 { if entity.on_ground.load(Ordering::Relaxed) && packet.collision & FLAG_ON_GROUND == 0 && height_difference > 0.0 {
player.jump().await; player.jump().await;
@ -486,6 +496,15 @@ impl JavaClient {
let last_pos = entity.pos.load(); let last_pos = entity.pos.load();
player.get_entity().set_pos(pos); player.get_entity().set_pos(pos);
let distance = last_pos.squared_distance_to_vec(&pos).sqrt();
let cm = (distance * 100.0) as i32;
if cm > 0 {
let stat = player.get_movement_statistic().await;
player
.increment_stat(StatisticCategory::Custom, stat as i32, cm)
.await;
}
let height_difference = pos.y - last_pos.y; let height_difference = pos.y - last_pos.y;
if entity.on_ground.load(Ordering::Relaxed) if entity.on_ground.load(Ordering::Relaxed)
&& (packet.collision & FLAG_ON_GROUND) != 0 && (packet.collision & FLAG_ON_GROUND) != 0
@ -1609,7 +1628,7 @@ impl JavaClient {
} }
1 => { 1 => {
// Request stats // Request stats
debug!("todo"); player.send_stats().await;
} }
_ => { _ => {
self.kick(TextComponent::text("Invalid client status")) self.kick(TextComponent::text("Invalid client status"))
@ -1807,6 +1826,22 @@ impl JavaClient {
let world = entity.world.load_full(); let world = entity.world.load_full();
let (block, state) = world.get_block_and_state(&position); let (block, state) = world.get_block_and_state(&position);
if block == &pumpkin_data::Block::NOTE_BLOCK {
let props =
pumpkin_data::block_properties::NoteBlockLikeProperties::from_state_id(
state.id, block,
);
crate::block::blocks::note::NoteBlock::play_note(&props, &world, &position)
.await;
player
.increment_stat(
StatisticCategory::Custom,
CustomStatistic::PlayNoteblock as i32,
1,
)
.await;
}
let inventory = player.inventory(); let inventory = player.inventory();
let held = inventory.held_item(); let held = inventory.held_item();
if !server if !server
@ -1865,6 +1900,17 @@ impl JavaClient {
.broken(&world, block, player, &position, server, broken_state) .broken(&world, block, player, &position, server, broken_state)
.await; .await;
player.apply_tool_damage_for_block_break(broken_state).await; player.apply_tool_damage_for_block_break(broken_state).await;
let item_id = player.inventory().held_item().lock().await.item.id;
player
.increment_stat(StatisticCategory::Used, item_id as i32, 1)
.await;
player
.increment_stat(
StatisticCategory::Mined,
broken_state.id as i32,
1,
)
.await;
} }
self.sync_block_state_to_client(&world, position).await; self.sync_block_state_to_client(&world, position).await;
} else { } else {
@ -1936,8 +1982,17 @@ impl JavaClient {
.block_registry .block_registry
.broken(&world, block, player, &location, server, state) .broken(&world, block, player, &location, server, state)
.await; .await;
player.apply_tool_damage_for_block_break(state).await; player.apply_tool_damage_for_block_break(state).await;
let item_id = player.inventory().held_item().lock().await.item.id;
player
.increment_stat(StatisticCategory::Used, item_id as i32, 1)
.await;
player
.increment_stat(StatisticCategory::Mined, state.id as i32, 1)
.await;
} }
self.sync_block_state_to_client(&world, location).await; self.sync_block_state_to_client(&world, location).await;
self.update_sequence(player, player_action.sequence.0); self.update_sequence(player, player_action.sequence.0);
@ -2087,6 +2142,11 @@ impl JavaClient {
off_hand_item off_hand_item
}; };
let item_id = item.lock().await.item.id;
player
.increment_stat(StatisticCategory::Used, item_id as i32, 1)
.await;
let entity = &player.get_entity(); let entity = &player.get_entity();
let world = entity.world.load_full(); let world = entity.world.load_full();
let block = world.get_block(&position); let block = world.get_block(&position);
@ -2303,6 +2363,14 @@ impl JavaClient {
inventory.off_hand_item().await inventory.off_hand_item().await
}; };
let (item_id, _item) = {
let guard = item_in_hand.lock().await;
(guard.item.id, guard.item)
};
player
.increment_stat(StatisticCategory::Used, item_id as i32, 1)
.await;
let hit_result = player let hit_result = player
.world() .world()
.raycast( .raycast(

View File

@ -537,6 +537,13 @@ impl Server {
} }
pub async fn remove_player(&self, player: &Player) { pub async fn remove_player(&self, player: &Player) {
player
.increment_stat(
pumpkin_data::statistic::StatisticCategory::Custom,
pumpkin_data::statistic::CustomStatistic::LeaveGame as i32,
1,
)
.await;
// TODO: Config if we want decrease online // TODO: Config if we want decrease online
self.listing.lock().await.remove_player(player); self.listing.lock().await.remove_player(player);
} }