Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bba3aba
feat: add custom stats
Laptop59 Jul 25, 2026
01cb1de
feat: barebones implementation of stat types to registry
Laptop59 Jul 27, 2026
4762273
feat: implement stat
Laptop59 Jul 28, 2026
72f3671
feat: implement stats counter
Laptop59 Jul 29, 2026
973eabd
refactor: switch from storing stat value registry ID (index) to a &dyn
Laptop59 Jul 29, 2026
2d9c1fa
perf: skip looking up registry every time a new `Stat` is constructed…
Laptop59 Jul 29, 2026
081fae1
feat: send statistics to client
Laptop59 Jul 29, 2026
1a8db5b
feat: implement statistics in some places
Laptop59 Jul 31, 2026
38dcd8e
feat: award stat for breaking a block
Laptop59 Jul 31, 2026
17ea5f9
feat: serialize & deserialize stats
Laptop59 Aug 3, 2026
21bddc0
chore: add test for stat file
Laptop59 Aug 3, 2026
a805f62
chore: add tests for `StatsCounter`
Laptop59 Aug 3, 2026
bd13f43
feat: award leaving stat
Laptop59 Aug 6, 2026
ccdec70
feat: award tick-based player stats
Laptop59 Aug 6, 2026
ca71a4b
feat: finish off the time-based custom stats and item drop-related stats
Laptop59 Aug 7, 2026
b75e3df
chore: remove logging every tick from testing
Laptop59 Aug 7, 2026
4f47a3d
feat: complete the distance-based stats
Laptop59 Aug 8, 2026
e402d5b
feat: complete some miscellaneous stats
Laptop59 Aug 8, 2026
9f8e881
chore: format code after rebase
Laptop59 Aug 9, 2026
6d27939
feat: give ITEM_PICKED_UP stat
Laptop59 Aug 9, 2026
d98fdd5
feat: give ITEM_BROKEN stat from `on_equipped_item_broken`
Laptop59 Aug 9, 2026
0d47acb
feat: award SLEEP_IN_BED custom stat
Laptop59 Aug 9, 2026
0f1afc4
chore: fix merge conflicts
Laptop59 Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion steel-core/src/behavior/blocks/container/anvil_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use steel_macros::block_behavior;
use steel_registry::{
blocks::{BlockRef, block_state_ext::BlockStateExt, properties::BlockStateProperties},
items::item::BlockHitResult,
vanilla_blocks,
vanilla_blocks, vanilla_custom_stats,
};
use steel_utils::{BlockStateId, translations};
use text_components::TextComponent;
Expand Down Expand Up @@ -73,6 +73,7 @@ impl BlockBehavior for AnvilBlock {
TextComponent::translated(translations::CONTAINER_REPAIR.msg()),
move |context| anvil(inventory, context.container_id, pos, context.world),
);
player.award_custom_stat(&vanilla_custom_stats::INTERACT_WITH_ANVIL);
InteractionResult::Success
}
}
4 changes: 2 additions & 2 deletions steel-core/src/behavior/blocks/container/barrel_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use steel_macros::block_behavior;
use steel_registry::blocks::BlockRef;
use steel_registry::blocks::block_state_ext::BlockStateExt;
use steel_registry::blocks::properties::{BlockStateProperties, Direction};
use steel_registry::vanilla_block_entity_types;
use steel_registry::{vanilla_block_entity_types, vanilla_custom_stats};
use steel_utils::{BlockPos, BlockStateId, translations};
use text_components::TextComponent;

Expand Down Expand Up @@ -77,7 +77,7 @@ impl BlockBehavior for BarrelBlock {
move |context| chest(inventory, context.container_id, container_ref, 3),
);

// TODO: Award stat OPEN_BARREL
player.award_custom_stat(&vanilla_custom_stats::OPEN_BARREL);
// TODO: Anger nearby piglins (PiglinAi.angerNearbyPiglins)
// TODO: Implement ContainerOpenersCounter to track open state, play sounds,
// and update OPEN block property. Requires scheduled block ticks (scheduleTick)
Expand Down
12 changes: 6 additions & 6 deletions steel-core/src/behavior/blocks/container/crafting_table_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@

use std::sync::Arc;

use steel_macros::block_behavior;
use steel_registry::blocks::BlockRef;
use steel_utils::{BlockPos, BlockStateId, translations};
use text_components::TextComponent;

use crate::behavior::InventoryAccess;
use crate::behavior::block::BlockBehavior;
use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult};
use crate::inventory::menu::kinds::crafting;
use crate::player::Player;
use crate::world::World;
use steel_macros::block_behavior;
use steel_registry::blocks::BlockRef;
use steel_registry::vanilla_custom_stats;
use steel_utils::{BlockPos, BlockStateId, translations};
use text_components::TextComponent;

/// Behavior for the crafting table block.
///
Expand Down Expand Up @@ -52,7 +52,7 @@ impl BlockBehavior for CraftingTableBlock {
TextComponent::translated(translations::CONTAINER_CRAFTING.msg()),
move |context| crafting(inventory, context.container_id, pos),
);
// TODO: Award stat INTERACT_WITH_CRAFTING_TABLE
player.award_custom_stat(&vanilla_custom_stats::INTERACT_WITH_CRAFTING_TABLE);
InteractionResult::Success
}
}
3 changes: 2 additions & 1 deletion steel-core/src/behavior/blocks/decoration/cake_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use steel_macros::block_behavior;
use steel_registry::{
blocks::{BlockRef, block_state_ext::BlockStateExt, properties::BlockStateProperties},
items::item::BlockHitResult,
sound_events, vanilla_blocks,
sound_events, vanilla_blocks, vanilla_custom_stats,
vanilla_item_tags::ItemTag,
};
use steel_utils::{
Expand Down Expand Up @@ -46,6 +46,7 @@ impl CakeBlock {
player: &Player,
) -> InteractionResult {
if player.can_eat(false) {
player.award_custom_stat(&vanilla_custom_stats::EAT_CAKE_SLICE);
let mut food_data = player.food_data.lock();
food_data.eat(2, 0.1);
let bites = state.get_value(&BlockStateProperties::BITES);
Expand Down
6 changes: 3 additions & 3 deletions steel-core/src/behavior/blocks/redstone/note_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use steel_registry::blocks::block_state_ext::BlockStateExt as _;
use steel_registry::blocks::properties::{BlockStateProperties, Direction, NoteBlockInstrument};
use steel_registry::sound_event::SoundEventRef;
use steel_registry::vanilla_item_tags::ItemTag;
use steel_registry::{sound_events, vanilla_game_events};
use steel_registry::{sound_events, vanilla_custom_stats, vanilla_game_events};
use steel_utils::types::{InteractionHand, UpdateFlags};
use steel_utils::{BlockPos, BlockStateId};

Expand Down Expand Up @@ -212,13 +212,13 @@ impl BlockBehavior for NoteBlock {
let tuned_state = Self::cycle_note(state);
world.set_block(pos, tuned_state, UpdateFlags::UPDATE_ALL);
self.play_note(Some(player), tuned_state, world, pos);
// The tune-noteblock stat awaits Steel's shared statistics foundation.
player.award_custom_stat(&vanilla_custom_stats::TUNE_NOTEBLOCK);
InteractionResult::Success
}

fn attack(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos, player: &Player) {
self.play_note(Some(player), state, world, pos);
// The play-noteblock stat awaits Steel's shared statistics foundation.
player.award_custom_stat(&vanilla_custom_stats::PLAY_NOTEBLOCK);
}

fn trigger_event(
Expand Down
9 changes: 7 additions & 2 deletions steel-core/src/behavior/blocks/redstone/target_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use steel_macros::block_behavior;
use steel_registry::blocks::BlockRef;
use steel_registry::blocks::block_state_ext::BlockStateExt as _;
use steel_registry::blocks::properties::BlockStateProperties;
use steel_registry::vanilla_custom_stats;
use steel_utils::axis::Axis;
use steel_utils::types::UpdateFlags;
use steel_utils::{BlockPos, BlockStateId};
Expand Down Expand Up @@ -91,8 +92,12 @@ impl BlockBehavior for TargetBlock {
projectile: &dyn Projectile,
) {
let _strength = self.update_redstone_output(world, state, hit, projectile);
// The owner-facing target-hit stat and advancement criterion await
// Steel's shared statistics and advancement foundations.
if let Some(owner) = projectile.projectile_owner()
&& let Some(player) = owner.as_player()
{
player.award_custom_stat(&vanilla_custom_stats::TARGET_HIT);
// TODO: The advancement criterion awaits Steel's shared advancement foundations.
}
}

fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
Expand Down
3 changes: 2 additions & 1 deletion steel-core/src/behavior/items/brush.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ impl ItemBehavior for BrushItem {

if outcome.durability_damage {
let slot = equipped_brush_slot(player, stack);
let item_ref = stack.item;
if stack.hurt_and_break(1, player.has_infinite_materials()) {
player.on_equipped_item_broken(slot);
player.on_equipped_item_broken(item_ref, slot);
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions steel-core/src/behavior/items/ender_pearl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@

use std::sync::Arc;

use glam::DVec3;
use steel_macros::item_behavior;
use steel_protocol::packets::game::SoundSource;
use steel_registry::{sound_events, vanilla_entities};

use crate::behavior::context::{InteractionResult, UseItemContext};
use crate::behavior::item::ItemBehavior;
use crate::entity::entities::EnderPearlEntity;
use crate::entity::{Entity, Projectile, SharedEntity, ThrowableItemProjectile, next_entity_id};
use glam::DVec3;
use steel_macros::item_behavior;
use steel_protocol::packets::game::SoundSource;
use steel_registry::stat::vanilla_stat_types;
use steel_registry::{sound_events, vanilla_entities, vanilla_items};

/// Vanilla `EnderpearlItem.PROJECTILE_SHOOT_POWER`.
const SHOOT_POWER: f32 = 1.5;
Expand Down Expand Up @@ -68,7 +68,7 @@ impl ItemBehavior for EnderPearlItem {
}
player.register_ender_pearl(&entity);

// TODO: award the ITEM_USED stat once a stats system exists.
player.award_stat(&vanilla_stat_types::ITEM_USED, &vanilla_items::ENDER_PEARL);
context.inv.with_item(|item| item.shrink(1));

InteractionResult::Success
Expand Down
9 changes: 7 additions & 2 deletions steel-core/src/behavior/items/firework_rocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use std::sync::Arc;
use glam::DVec3;
use steel_macros::item_behavior;
use steel_protocol::packets::game::SoundSource;
use steel_registry::{sound_events, vanilla_entities};
use steel_registry::stat::vanilla_stat_types;
use steel_registry::{sound_events, vanilla_entities, vanilla_items};
use steel_utils::Direction;

use crate::behavior::context::{InteractionResult, UseItemContext, UseOnContext};
Expand Down Expand Up @@ -109,7 +110,11 @@ impl ItemBehavior for FireworkRocketItem {
);
item.shrink(1);
});
// TODO: Award `Stats.ITEM_USED` once Steel has a statistics foundation.

context.player.award_stat(
&vanilla_stat_types::ITEM_USED,
&vanilla_items::FIREWORK_ROCKET,
);

InteractionResult::Success
}
Expand Down
17 changes: 11 additions & 6 deletions steel-core/src/behavior/items/food_on_a_stick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use steel_macros::item_behavior;
use steel_registry::entity_type::EntityTypeRef;
use steel_registry::stat::vanilla_stat_types;
use steel_registry::vanilla_items;

use crate::behavior::{InteractionResult, ItemBehavior, UseItemContext};
Expand Down Expand Up @@ -30,16 +31,16 @@ impl FoodOnAStickItem {
impl ItemBehavior for FoodOnAStickItem {
fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
let Some(vehicle) = context.player.controlled_vehicle() else {
return Self::pass_without_boost();
return Self::pass_without_boost(context);
};
if vehicle.entity_type() != self.can_interact_with {
return Self::pass_without_boost();
return Self::pass_without_boost(context);
}
let Some(steerable) = vehicle.as_item_steerable() else {
return Self::pass_without_boost();
return Self::pass_without_boost(context);
};
if !steerable.boost() {
return Self::pass_without_boost();
return Self::pass_without_boost(context);
}

let has_infinite_materials = context.player.has_infinite_materials();
Expand All @@ -57,8 +58,12 @@ impl ItemBehavior for FoodOnAStickItem {
}

impl FoodOnAStickItem {
const fn pass_without_boost() -> InteractionResult {
// TODO: Award Stats.ITEM_USED once the stat foundation exists.
fn pass_without_boost(context: &UseItemContext) -> InteractionResult {
let item = context.inv.with_item(|item_stack| item_stack.item);
context
.player
.award_stat(&vanilla_stat_types::ITEM_USED, item);

InteractionResult::Pass
}
}
11 changes: 7 additions & 4 deletions steel-core/src/enchantment_helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use steel_registry::enchantment_effect::{
};
use steel_registry::entity_type::EntityTypeRef;
use steel_registry::item_stack::ItemStack;
use steel_registry::{REGISTRY, RegistryExt, TaggedRegistryExt, vanilla_entities};
use steel_registry::{REGISTRY, RegistryExt, TaggedRegistryExt, vanilla_entities, vanilla_items};

use crate::entity::damage::DamageSource;
use crate::entity::{Entity, LivingEntity, MobEffectInstance};
Expand Down Expand Up @@ -224,7 +224,9 @@ pub(crate) fn do_post_attack_effects_with_item_source(
if let Some(living_victim) = victim.as_living_entity() {
for slot in EquipmentSlot::ALL {
let mut item_broke = false;
let mut item_ref = &*vanilla_items::AIR;
living_victim.with_equipment_slot_mut(slot, &mut |item| {
item_ref = item.item;
item_broke = apply_post_attack_effects(
world,
item,
Expand All @@ -234,7 +236,7 @@ pub(crate) fn do_post_attack_effects_with_item_source(
);
});
if item_broke {
living_victim.on_equipped_item_broken(slot);
living_victim.on_equipped_item_broken(item_ref, slot);
}
}
}
Expand Down Expand Up @@ -442,6 +444,7 @@ pub(crate) fn do_post_piercing_attack_effects(world: &World, user: &dyn LivingEn
return;
};

let item_ref = item_stack.item;
for (key, level) in enchantments.iter() {
if *level == 0 {
continue;
Expand All @@ -459,7 +462,7 @@ pub(crate) fn do_post_piercing_attack_effects(world: &World, user: &dyn LivingEn
continue;
}
if apply_post_piercing_entity_effect(world, &effect.effect, level, user) {
user.on_equipped_item_broken(EquipmentSlot::MainHand);
user.on_equipped_item_broken(item_ref, EquipmentSlot::MainHand);
}
}
}
Expand Down Expand Up @@ -1074,7 +1077,7 @@ mod tests {

fn set_absorption_amount(&self, _amount: f32) {}

fn on_equipped_item_broken(&self, slot: EquipmentSlot) {
fn on_equipped_item_broken(&self, _item: ItemRef, slot: EquipmentSlot) {
self.broken_slots.lock().push(slot);
}
}
Expand Down
10 changes: 10 additions & 0 deletions steel-core/src/entity/entities/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
use simdnbt::owned::{NbtCompound, NbtTag};
use steel_protocol::packets::game::CTakeItemEntity;
use steel_registry::blocks::block_state_ext::BlockStateExt;
use steel_registry::stat::vanilla_stat_types;
use steel_utils::BlockPos;

/// Maximum age in ticks before despawn (5 minutes = 6000 ticks).
Expand Down Expand Up @@ -333,6 +334,15 @@ impl ItemEntity {
// Calculate how many items were picked up
let picked_up_count = original_count - item.count();

// Vanilla uses the stack's count to award the stat that many items picked up, which is
// wrong if not all the items get picked up by the player. We use the correct difference
// of counts instead to award the stat for picking up the items.
player.award_stat_with_count(
&vanilla_stat_types::ITEM_PICKED_UP,
item.item,
picked_up_count,
);

// Send the take animation packet to nearby players
if let Some(world) = self.level() {
let pos = self.position();
Expand Down
11 changes: 8 additions & 3 deletions steel-core/src/entity/entity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -863,14 +863,19 @@ pub trait Entity: EntityEventSource + ErasedType + Send + Sync + 'static {
///
/// Mirrors vanilla `Entity.rideTick`.
fn ride_tick(&self) {
self.default_ride_tick();
if let Some(living) = self.as_living_entity() {
living.reset_fall_distance();
}
}

/// The default implementation of `Entity.rideTick` when not overridden.
fn default_ride_tick(&self) {
self.set_velocity(DVec3::ZERO);
self.tick();
if let Some(vehicle) = self.vehicle() {
vehicle.position_rider(self.as_entity_event_source());
}
if let Some(living) = self.as_living_entity() {
living.reset_fall_distance();
}
}

/// Runs vanilla `Entity.baseTick` pieces Steel currently implements.
Expand Down
Loading
Loading