feat: add stats
This commit is contained in:
parent
3b3ae983e4
commit
68bae5899e
1
assets/stats.json
Normal file
1
assets/stats.json
Normal file
File diff suppressed because one or more lines are too long
@ -54,6 +54,7 @@ mod screen;
|
||||
mod sound;
|
||||
mod sound_category;
|
||||
mod spawn_egg;
|
||||
mod statistic;
|
||||
mod structures;
|
||||
mod tag;
|
||||
mod tracked_data;
|
||||
@ -95,6 +96,7 @@ pub fn main() {
|
||||
(scoreboard_slot::build, "scoreboard_slot.rs"),
|
||||
(world_event::build, "world_event.rs"),
|
||||
(entity_type::build, "entity_type.rs"),
|
||||
(statistic::build, "statistic.rs"),
|
||||
(noise_parameter::build, "noise_parameter.rs"),
|
||||
(biome::build, "biome.rs"),
|
||||
(damage_type::build, "damage_type.rs"),
|
||||
|
||||
115
pumpkin-codegen/src/statistic.rs
Normal file
115
pumpkin-codegen/src/statistic.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ default = [
|
||||
"registry",
|
||||
"screen",
|
||||
"particle",
|
||||
"statistic",
|
||||
"sound",
|
||||
"recipes",
|
||||
"data_component",
|
||||
@ -57,6 +58,7 @@ translation = []
|
||||
registry = []
|
||||
screen = []
|
||||
particle = []
|
||||
statistic = []
|
||||
sound = []
|
||||
recipes = []
|
||||
data_component = []
|
||||
|
||||
226
pumpkin-data/src/generated/statistic.rs
Normal file
226
pumpkin-data/src/generated/statistic.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@ -42,6 +42,11 @@ pub mod screen;
|
||||
#[path = "generated/particle.rs"]
|
||||
pub mod particle;
|
||||
|
||||
#[cfg(feature = "statistic")]
|
||||
#[rustfmt::skip]
|
||||
#[path = "generated/statistic.rs"]
|
||||
pub mod statistic;
|
||||
|
||||
#[cfg(feature = "sound")]
|
||||
#[rustfmt::skip]
|
||||
#[path = "generated/sound_category.rs"]
|
||||
|
||||
@ -16,6 +16,7 @@ pumpkin-data = { workspace = true, features = [
|
||||
"fuels",
|
||||
"enchantment",
|
||||
"potion",
|
||||
"statistic",
|
||||
] }
|
||||
|
||||
pumpkin-world.workspace = true
|
||||
|
||||
@ -32,6 +32,7 @@ use crate::slot::{BoxFuture, NormalSlot, Slot};
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::recipes::{CraftingRecipeTypes, RECIPES_CRAFTING};
|
||||
use pumpkin_data::screen::WindowType;
|
||||
use pumpkin_data::statistic::StatisticCategory;
|
||||
use pumpkin_data::tag;
|
||||
use pumpkin_data::tag::Taggable;
|
||||
use pumpkin_protocol::codec::recipe::{DynamicRecipe, OwnedCraftingRecipe};
|
||||
@ -437,10 +438,17 @@ impl Slot for ResultSlot {
|
||||
}
|
||||
fn on_take_item<'a>(
|
||||
&'a self,
|
||||
_player: &'a dyn InventoryPlayer,
|
||||
_stack: &'a ItemStack,
|
||||
player: &'a dyn InventoryPlayer,
|
||||
stack: &'a ItemStack,
|
||||
) -> BoxFuture<'a, ()> {
|
||||
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() {
|
||||
let slot = self.inventory.get_stack(i).await;
|
||||
let mut stack = slot.lock().await;
|
||||
@ -640,7 +648,11 @@ impl ScreenHandler for CraftingTableScreenHandler {
|
||||
if stack.item_count == stack_prev.item_count {
|
||||
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 {
|
||||
slot.on_quick_move_crafted(stack.clone(), stack_prev.clone())
|
||||
.await;
|
||||
|
||||
@ -6,6 +6,7 @@ use pumpkin_data::data_component_impl::EnchantableImpl;
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::screen::WindowType;
|
||||
use pumpkin_data::statistic::{CustomStatistic, StatisticCategory};
|
||||
use pumpkin_data::tag::{Enchantment as EnchantmentTag, Taggable};
|
||||
use pumpkin_util::random::RandomImpl;
|
||||
use pumpkin_world::inventory::Inventory;
|
||||
@ -357,6 +358,14 @@ impl ScreenHandler for EnchantingTableScreenHandler {
|
||||
self.update_enchantments(player).await;
|
||||
self.send_content_updates().await;
|
||||
|
||||
player
|
||||
.increment_stat(
|
||||
StatisticCategory::Custom,
|
||||
CustomStatistic::EnchantItem as i32,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
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 tracing::debug;
|
||||
@ -140,10 +140,17 @@ impl Slot for FurnaceOutputSlot {
|
||||
fn on_take_item<'a>(
|
||||
&'a self,
|
||||
player: &'a dyn InventoryPlayer,
|
||||
_stack: &'a pumpkin_data::item_stack::ItemStack,
|
||||
stack: &'a pumpkin_data::item_stack::ItemStack,
|
||||
) -> BoxFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
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
|
||||
let experience = self.experience_container.extract_experience();
|
||||
debug!("FurnaceOutputSlot: extracted experience = {experience}");
|
||||
|
||||
@ -245,7 +245,9 @@ impl ScreenHandler for PlayerScreenHandler {
|
||||
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 {
|
||||
// From crafting result slot (0)
|
||||
|
||||
@ -35,6 +35,7 @@ use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::{
|
||||
data_component_impl::{EquipmentSlot, EquipmentType, EquippableImpl},
|
||||
screen::WindowType,
|
||||
statistic::StatisticCategory,
|
||||
};
|
||||
use pumpkin_protocol::{
|
||||
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.)
|
||||
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.
|
||||
|
||||
@ -13,6 +13,7 @@ use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::recipes::{RECIPES_STONECUTTING, StonecutterRecipe};
|
||||
use pumpkin_data::screen::WindowType;
|
||||
use pumpkin_data::statistic::StatisticCategory;
|
||||
use pumpkin_protocol::java::server::play::SlotActionType;
|
||||
use pumpkin_world::inventory::Inventory;
|
||||
use pumpkin_world::inventory::SimpleInventory;
|
||||
@ -201,10 +202,17 @@ impl Slot for StonecutterOutputSlot {
|
||||
|
||||
fn on_take_item<'a>(
|
||||
&'a self,
|
||||
_player: &'a dyn InventoryPlayer,
|
||||
_stack: &'a ItemStack,
|
||||
player: &'a dyn InventoryPlayer,
|
||||
stack: &'a ItemStack,
|
||||
) -> BoxFuture<'a, ()> {
|
||||
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 mut input_lock = input_stack.lock().await;
|
||||
if !input_lock.is_empty() {
|
||||
|
||||
17
pumpkin-protocol/src/java/client/play/award_stats.rs
Normal file
17
pumpkin-protocol/src/java/client/play/award_stats.rs
Normal 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,
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
mod award_stats;
|
||||
pub use award_stats::*;
|
||||
mod acknowledge_block;
|
||||
mod actionbar;
|
||||
mod block_destroy_stage;
|
||||
|
||||
@ -297,7 +297,6 @@ impl WorldAquiferSampler {
|
||||
let z = block_z + section_coords::section_to_block(offset_z as i32);
|
||||
|
||||
let n = height_estimator.estimate_height(x, z);
|
||||
println!("estimate_height({}, {}) = {}", x, z, n);
|
||||
let o = n + 8;
|
||||
let bl2 = offset_x == 0 && offset_z == 0;
|
||||
|
||||
|
||||
@ -25,6 +25,13 @@ pub struct AnvilBlock;
|
||||
impl BlockBehaviour for AnvilBlock {
|
||||
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
||||
Box::pin(async move {
|
||||
args.player
|
||||
.increment_stat(
|
||||
pumpkin_data::statistic::StatisticCategory::Custom,
|
||||
pumpkin_data::statistic::CustomStatistic::InteractWithAnvil as i32,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
args.player
|
||||
.open_handled_screen(&AnvilScreenFactory, Some(*args.position))
|
||||
.await;
|
||||
|
||||
@ -64,6 +64,13 @@ impl BlockBehaviour for BarrelBlock {
|
||||
if let Some(block_entity) = args.world.get_block_entity(args.position)
|
||||
&& 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
|
||||
.open_handled_screen(&BarrelScreenFactory(inventory), Some(*args.position))
|
||||
.await;
|
||||
|
||||
@ -58,6 +58,14 @@ impl BlockBehaviour for BeaconBlock {
|
||||
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
|
||||
args.player
|
||||
.open_handled_screen(&BeaconScreenFactory(inventory), Some(*args.position))
|
||||
|
||||
@ -359,6 +359,13 @@ impl BlockBehaviour for BedBlock {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
BlockActionResult::SuccessServer
|
||||
|
||||
@ -91,6 +91,13 @@ impl BlockBehaviour for BlastFurnaceBlock {
|
||||
&& let Some(property_delegate) = block_entity.clone().to_property_delegate()
|
||||
&& 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(
|
||||
inventory,
|
||||
property_delegate,
|
||||
|
||||
@ -56,6 +56,13 @@ impl BlockBehaviour for BrewingStandBlock {
|
||||
&& let Some(inventory) = block_entity.clone().get_inventory()
|
||||
&& 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
|
||||
.open_handled_screen(&BrewingScreenFactory(inventory, pd), Some(*args.position))
|
||||
.await;
|
||||
|
||||
@ -54,6 +54,13 @@ impl CakeBlock {
|
||||
let mut properties = CakeLikeProperties::from_state_id(state_id, block);
|
||||
match properties.bites {
|
||||
0..=5 => {
|
||||
player
|
||||
.increment_stat(
|
||||
pumpkin_data::statistic::StatisticCategory::Custom,
|
||||
pumpkin_data::statistic::CustomStatistic::EatCakeSlice as i32,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
properties.bites += 1;
|
||||
world
|
||||
.set_block_state(
|
||||
@ -65,6 +72,13 @@ impl CakeBlock {
|
||||
BlockActionResult::Consume
|
||||
}
|
||||
6 => {
|
||||
player
|
||||
.increment_stat(
|
||||
pumpkin_data::statistic::StatisticCategory::Custom,
|
||||
pumpkin_data::statistic::CustomStatistic::EatCakeSlice as i32,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
world
|
||||
.set_block_state(
|
||||
location,
|
||||
|
||||
@ -129,6 +129,13 @@ async fn placed_chest_impl<E: BlockEntity + 'static>(
|
||||
}
|
||||
|
||||
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 first_chest = args.world.get_block_entity(args.position);
|
||||
|
||||
|
||||
@ -18,6 +18,13 @@ pub struct CraftingTableBlock;
|
||||
impl BlockBehaviour for CraftingTableBlock {
|
||||
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
||||
Box::pin(async move {
|
||||
args.player
|
||||
.increment_stat(
|
||||
pumpkin_data::statistic::StatisticCategory::Custom,
|
||||
pumpkin_data::statistic::CustomStatistic::InteractWithCraftingTable as i32,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
args.player
|
||||
.open_handled_screen(
|
||||
&CraftingTableScreenFactory(args.server.recipe_manager.clone()),
|
||||
|
||||
@ -85,14 +85,19 @@ impl BlockBehaviour for EnderChestBlock {
|
||||
{
|
||||
let inventory = args.player.ender_chest_inventory();
|
||||
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
|
||||
.open_handled_screen(
|
||||
&EnderChestScreenFactory(inventory.clone()),
|
||||
Some(*args.position),
|
||||
)
|
||||
.await;
|
||||
|
||||
// TODO: player.incrementStat(Stats.OPEN_ENDERCHEST);
|
||||
// TODO: PiglinBrain.onGuardedBlockInteracted(serverWorld, player, true);
|
||||
}
|
||||
|
||||
|
||||
@ -90,6 +90,13 @@ impl BlockBehaviour for FurnaceBlock {
|
||||
&& let Some(property_delegate) = block_entity.clone().to_property_delegate()
|
||||
&& 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 =
|
||||
FurnaceScreenFactory::new(inventory, property_delegate, experience_container);
|
||||
args.player
|
||||
|
||||
@ -62,6 +62,13 @@ impl BlockBehaviour for HopperBlock {
|
||||
if let Some(block_entity) = args.world.get_block_entity(args.position)
|
||||
&& 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
|
||||
.open_handled_screen(&HopperBlockScreenFactory(inventory), Some(*args.position))
|
||||
.await;
|
||||
|
||||
@ -168,8 +168,15 @@ impl BlockBehaviour for JukeboxBlock {
|
||||
// Start playing the music (client-side audio)
|
||||
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: player.incrementStat(Stats.PLAY_RECORD)
|
||||
|
||||
BlockActionResult::Success
|
||||
})
|
||||
|
||||
@ -60,6 +60,13 @@ impl BlockBehaviour for LecternBlock {
|
||||
|
||||
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
||||
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)
|
||||
&& let Some(lectern_entity) =
|
||||
block_entity.as_any().downcast_ref::<LecternBlockEntity>()
|
||||
|
||||
@ -95,6 +95,14 @@ impl BlockBehaviour for NoteBlock {
|
||||
.await;
|
||||
Self::play_note(¬e_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
|
||||
})
|
||||
}
|
||||
|
||||
@ -139,6 +139,14 @@ impl BlockBehaviour for BellBlock {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@ -88,6 +88,13 @@ impl BlockBehaviour for ShulkerBoxBlock {
|
||||
if let Some(block_entity) = args.world.get_block_entity(args.position)
|
||||
&& 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
|
||||
.open_handled_screen(&ShulkerBoxScreenFactory(inventory), Some(*args.position))
|
||||
.await;
|
||||
|
||||
@ -89,6 +89,13 @@ impl BlockBehaviour for SmokerBlock {
|
||||
&& let Some(property_delegate) = block_entity.clone().to_property_delegate()
|
||||
&& 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 =
|
||||
SmokerScreenFactory::new(inventory, property_delegate, experience_container);
|
||||
args.player
|
||||
|
||||
@ -19,6 +19,13 @@ pub struct StonecutterBlock;
|
||||
impl BlockBehaviour for StonecutterBlock {
|
||||
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
||||
Box::pin(async move {
|
||||
args.player
|
||||
.increment_stat(
|
||||
pumpkin_data::statistic::StatisticCategory::Custom,
|
||||
pumpkin_data::statistic::CustomStatistic::InteractWithStonecutter as i32,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
args.player
|
||||
.open_handled_screen(&StonecutterScreenFactory, Some(*args.position))
|
||||
.await;
|
||||
|
||||
@ -65,6 +65,20 @@ impl BreedGoal {
|
||||
let entity = mob.get_entity();
|
||||
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
|
||||
.breeding_cooldown
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use crate::entity::player::statistics::StatisticCategory;
|
||||
use crate::{entity::EntityBaseFuture, server::Server};
|
||||
use core::f32;
|
||||
use pumpkin_data::data_component_impl::DamageResistantImpl;
|
||||
@ -453,17 +454,41 @@ impl EntityBase for ItemEntity {
|
||||
return;
|
||||
}
|
||||
|
||||
let (item_id, count_before) = {
|
||||
let stack = self.item_stack.lock().await;
|
||||
(stack.item.id, stack.item_count)
|
||||
};
|
||||
|
||||
let inserted = {
|
||||
let mut stack = self.item_stack.lock().await;
|
||||
player.inventory.insert_stack_anywhere(&mut stack).await
|
||||
};
|
||||
|
||||
if inserted || player.is_creative() {
|
||||
let (item_count, is_empty) = {
|
||||
let (count_after, is_empty) = {
|
||||
let stack = self.item_stack.lock().await;
|
||||
(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
|
||||
.current_screen_handler
|
||||
|
||||
@ -28,6 +28,7 @@ use crate::entity::attributes::AttributeInstance;
|
||||
use crate::entity::attributes::Modifier;
|
||||
use crate::entity::attributes::ModifierOperation;
|
||||
use crate::entity::mob::slime::SlimeEntity;
|
||||
use crate::entity::player::statistics::{CustomStatistic, StatisticCategory};
|
||||
use crate::entity::{EntityBaseFuture, NbtFuture};
|
||||
use crate::server::Server;
|
||||
use crate::world::loot::{LootContextParameters, LootTableExt};
|
||||
@ -1309,6 +1310,9 @@ impl LivingEntity {
|
||||
self.movement_input.store(Vector3::default());
|
||||
self.jumping.store(false, Relaxed);
|
||||
|
||||
// Statistics updates
|
||||
self.update_death_stats(&*dyn_self, cause).await;
|
||||
|
||||
// Plays the death sound
|
||||
world.send_entity_status(&self.entity, EntityStatus::Death);
|
||||
let params = LootContextParameters {
|
||||
@ -1336,41 +1340,108 @@ impl LivingEntity {
|
||||
}
|
||||
self.entity.pose.store(EntityPose::Dying);
|
||||
|
||||
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;
|
||||
}
|
||||
self.drop_equipment().await;
|
||||
|
||||
// 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 };
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.broadcast_death_message(&*dyn_self, damage_type, source, cause)
|
||||
.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) {
|
||||
if let Some(loot_table) = &self.get_entity().entity_type.loot_table {
|
||||
let pos = self.entity.block_pos.load();
|
||||
@ -1938,6 +2009,28 @@ impl EntityBase for LivingEntity {
|
||||
// Total damage after reductions
|
||||
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
|
||||
if self.is_blocking().await
|
||||
&& !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 {
|
||||
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()) {
|
||||
let held_item = attacker_player.inventory().held_item();
|
||||
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;
|
||||
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(
|
||||
&self.entity,
|
||||
crate::entity::equipment_break_status(&slot),
|
||||
@ -2084,6 +2196,27 @@ impl EntityBase for LivingEntity {
|
||||
let mut remaining = damage_amount;
|
||||
let current_abs = self.absorption.load();
|
||||
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 {
|
||||
let new_abs = current_abs - remaining;
|
||||
self.set_absorption(new_abs).await;
|
||||
@ -2109,6 +2242,27 @@ impl EntityBase for LivingEntity {
|
||||
if remaining > 0.0 {
|
||||
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)
|
||||
if let Some(attacker) = cause.or(source) {
|
||||
self.last_attacker_id
|
||||
|
||||
@ -72,6 +72,7 @@ pub struct MobEntity {
|
||||
pub position_target_range: AtomicI32,
|
||||
pub love_ticks: AtomicI32,
|
||||
pub breeding_cooldown: AtomicI32,
|
||||
pub breeder: AtomicCell<Option<Uuid>>,
|
||||
mob_flags: AtomicU8,
|
||||
last_sent_yaw: AtomicU8,
|
||||
last_sent_pitch: AtomicU8,
|
||||
@ -108,6 +109,7 @@ impl MobEntity {
|
||||
position_target_range: AtomicI32::new(-1),
|
||||
love_ticks: AtomicI32::new(0),
|
||||
breeding_cooldown: AtomicI32::new(0),
|
||||
breeder: AtomicCell::new(None),
|
||||
mob_flags: AtomicU8::new(0),
|
||||
last_sent_yaw: AtomicU8::new(0),
|
||||
last_sent_pitch: AtomicU8::new(0),
|
||||
@ -153,8 +155,9 @@ impl MobEntity {
|
||||
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.breeder.store(breeder);
|
||||
}
|
||||
|
||||
pub fn reset_love_ticks(&self) {
|
||||
|
||||
@ -121,7 +121,8 @@ impl Mob for ChickenEntity {
|
||||
if is_food && self.is_breeding_ready() && !self.is_in_love() {
|
||||
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 world = entity.world.load();
|
||||
let pos = entity.pos.load();
|
||||
|
||||
@ -82,7 +82,8 @@ impl Mob for CowEntity {
|
||||
if is_food && self.is_breeding_ready() && !self.is_in_love() {
|
||||
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 world = entity.world.load();
|
||||
let pos = entity.pos.load();
|
||||
|
||||
@ -87,7 +87,8 @@ impl Mob for PigEntity {
|
||||
if is_food && self.is_breeding_ready() && !self.is_in_love() {
|
||||
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 world = entity.world.load();
|
||||
let pos = entity.pos.load();
|
||||
|
||||
@ -150,7 +150,8 @@ impl Mob for SheepEntity {
|
||||
if is_food && self.is_breeding_ready() && !self.is_in_love() {
|
||||
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 world = entity.world.load();
|
||||
let pos = entity.pos.load();
|
||||
|
||||
@ -466,6 +466,14 @@ impl Mob for VillagerEntity {
|
||||
}
|
||||
drop(offers);
|
||||
|
||||
player
|
||||
.increment_stat(
|
||||
pumpkin_data::statistic::StatisticCategory::Custom,
|
||||
pumpkin_data::statistic::CustomStatistic::TalkedToVillager as i32,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
self.open_trading_screen(&player);
|
||||
|
||||
true
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
pub mod advancement;
|
||||
pub mod statistics;
|
||||
|
||||
use core::f32;
|
||||
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::particle::Particle;
|
||||
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||
use pumpkin_data::statistic::StatisticCategory;
|
||||
use pumpkin_data::tag::Taggable;
|
||||
use pumpkin_data::{Block, BlockState, Enchantment, screen::WindowType, tag, translation};
|
||||
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_long::VarLong;
|
||||
use pumpkin_protocol::java::client::play::{
|
||||
Animation, CAcknowledgeBlockChange, CActionBar, CChangeDifficulty, CCloseContainer,
|
||||
CCombatDeath, CCustomPayload, CDisguisedChatMessage, CEntityAnimation, CEntityPositionSync,
|
||||
CGameEvent, CItemCooldown, CMapItemData, COpenScreen, CParticle, CPlayerAbilities,
|
||||
CPlayerInfoUpdate, CPlayerPosition, CPlayerSpawnPosition, CRespawn, CSetContainerContent,
|
||||
CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CSetEquipment, CSetExperience,
|
||||
CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound, CSubtitle,
|
||||
CSystemChatMessage, CTabList, CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect,
|
||||
CUpdateTime, GameEvent, MapIcon, MapPatch, Metadata, PlayerAction, PlayerInfoFlags,
|
||||
PreviousMessage,
|
||||
Animation, CAcknowledgeBlockChange, CActionBar, CAwardStats, CChangeDifficulty,
|
||||
CCloseContainer, CCombatDeath, CCustomPayload, CDisguisedChatMessage, CEntityAnimation,
|
||||
CEntityPositionSync, CGameEvent, CItemCooldown, CMapItemData, COpenScreen, CParticle,
|
||||
CPlayerAbilities, CPlayerInfoUpdate, CPlayerPosition, CPlayerSpawnPosition, CRespawn,
|
||||
CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CSetEquipment,
|
||||
CSetExperience, CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound,
|
||||
CSubtitle, CSystemChatMessage, CTabList, CTitleAnimation, CTitleText, CUnloadChunk,
|
||||
CUpdateMobEffect, CUpdateTime, GameEvent, MapIcon, MapPatch, Metadata, PlayerAction,
|
||||
PlayerInfoFlags, PreviousMessage, Statistic,
|
||||
};
|
||||
use pumpkin_protocol::java::server::play::{
|
||||
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.
|
||||
pub abilities: Mutex<Abilities>,
|
||||
/// Player statistics
|
||||
pub stats: Mutex<statistics::Statistics>,
|
||||
/// The current stage of block destruction of the block the player is breaking.
|
||||
pub current_block_destroy_stage: AtomicI32,
|
||||
/// Indicates if the player is currently mining a block.
|
||||
@ -647,6 +651,7 @@ impl Player {
|
||||
mining: AtomicBool::new(false),
|
||||
mining_pos: Mutex::new(BlockPos::ZERO),
|
||||
abilities: Mutex::new(abilities),
|
||||
stats: Mutex::new(statistics::Statistics::default()),
|
||||
gamemode: AtomicCell::new(gamemode),
|
||||
previous_gamemode: AtomicCell::new(None),
|
||||
// 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`].
|
||||
pub async fn remove(self: &Arc<Self>) {
|
||||
self.stats
|
||||
.lock()
|
||||
.await
|
||||
.increment_custom(statistics::CustomStatistic::LeaveGame, 1);
|
||||
let world = self.world();
|
||||
world.remove_player(self, true).await;
|
||||
|
||||
@ -1170,6 +1179,12 @@ impl Player {
|
||||
// Send the break status before clearing the slot so the client can
|
||||
// use the item texture for break particles.
|
||||
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.living_entity.entity,
|
||||
super::equipment_break_status(slot),
|
||||
@ -1656,6 +1671,13 @@ impl Player {
|
||||
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();
|
||||
world.broadcast_to_chunk(
|
||||
chunk_pos,
|
||||
@ -1815,6 +1837,19 @@ impl Player {
|
||||
.send_packet_now(&CAcknowledgeBlockChange::new(seq.into()))
|
||||
.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;
|
||||
if *xp > 0 {
|
||||
@ -1942,6 +1977,10 @@ impl Player {
|
||||
}
|
||||
|
||||
pub async fn jump(&self) {
|
||||
self.stats
|
||||
.lock()
|
||||
.await
|
||||
.increment_custom(statistics::CustomStatistic::Jump, 1);
|
||||
if self.living_entity.entity.is_sprinting() {
|
||||
self.add_exhaustion(0.2).await;
|
||||
} 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.
|
||||
pub fn send_permission_lvl_update(&self) {
|
||||
let status = match self.permission_lvl.load() {
|
||||
@ -2805,6 +2960,18 @@ impl Player {
|
||||
}
|
||||
|
||||
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()
|
||||
+ 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);
|
||||
@ -3926,6 +4093,7 @@ impl NBTStorage for Player {
|
||||
nbt.put_bool("SpawnForced", respawn.force);
|
||||
}
|
||||
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()),
|
||||
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;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
52
pumpkin/src/entity/player/statistics.rs
Normal file
52
pumpkin/src/entity/player/statistics.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 hooked_id = self.hooked_entity_id.load(Ordering::Relaxed);
|
||||
|
||||
@ -67,8 +68,15 @@ impl FishingBobberEntity {
|
||||
|
||||
if self.bite_countdown.load(Ordering::Relaxed) > 0 {
|
||||
// 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.
|
||||
use pumpkin_data::item::Item;
|
||||
let _item_stack = ItemStack::new(1, &Item::COD);
|
||||
// player.inventory().add_item(item_stack).await; // Need public add_item
|
||||
|
||||
|
||||
@ -63,7 +63,7 @@ impl ItemBehaviour for FishingRodItem {
|
||||
if let Some(bobber) =
|
||||
bobber_base.cast_any().downcast_ref::<FishingBobberEntity>()
|
||||
{
|
||||
let _result = bobber.reel_in(player);
|
||||
let _result = bobber.reel_in(player).await;
|
||||
// TODO: give items
|
||||
}
|
||||
bobber_base.get_entity().remove().await;
|
||||
|
||||
@ -15,6 +15,7 @@ use crate::block::registry::BlockActionResult;
|
||||
use crate::block::{self, BlockIsReplacing};
|
||||
use crate::entity::EntityBase;
|
||||
use crate::entity::equipment_break_status;
|
||||
use crate::entity::player::statistics::{CustomStatistic, StatisticCategory};
|
||||
use crate::entity::player::{ChatMode, ChatSession, Player};
|
||||
use crate::error::PumpkinError;
|
||||
use crate::log_at_level;
|
||||
@ -360,6 +361,15 @@ impl JavaClient {
|
||||
let last_pos = entity.pos.load();
|
||||
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;
|
||||
if entity.on_ground.load(Ordering::Relaxed) && packet.collision & FLAG_ON_GROUND == 0 && height_difference > 0.0 {
|
||||
player.jump().await;
|
||||
@ -486,6 +496,15 @@ impl JavaClient {
|
||||
let last_pos = entity.pos.load();
|
||||
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;
|
||||
if entity.on_ground.load(Ordering::Relaxed)
|
||||
&& (packet.collision & FLAG_ON_GROUND) != 0
|
||||
@ -1609,7 +1628,7 @@ impl JavaClient {
|
||||
}
|
||||
1 => {
|
||||
// Request stats
|
||||
debug!("todo");
|
||||
player.send_stats().await;
|
||||
}
|
||||
_ => {
|
||||
self.kick(TextComponent::text("Invalid client status"))
|
||||
@ -1807,6 +1826,22 @@ impl JavaClient {
|
||||
let world = entity.world.load_full();
|
||||
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 held = inventory.held_item();
|
||||
if !server
|
||||
@ -1865,6 +1900,17 @@ impl JavaClient {
|
||||
.broken(&world, block, player, &position, server, 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;
|
||||
} else {
|
||||
@ -1936,8 +1982,17 @@ impl JavaClient {
|
||||
.block_registry
|
||||
.broken(&world, block, player, &location, server, 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.update_sequence(player, player_action.sequence.0);
|
||||
@ -2087,6 +2142,11 @@ impl JavaClient {
|
||||
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 world = entity.world.load_full();
|
||||
let block = world.get_block(&position);
|
||||
@ -2303,6 +2363,14 @@ impl JavaClient {
|
||||
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
|
||||
.world()
|
||||
.raycast(
|
||||
|
||||
@ -537,6 +537,13 @@ impl Server {
|
||||
}
|
||||
|
||||
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
|
||||
self.listing.lock().await.remove_player(player);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user