Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
0355bed
create Inventory::clear_slot
dylanopen Dec 27, 2025
06b5b4d
update Inventory::clear to use Inventory::clear_slot
dylanopen Dec 27, 2025
0cda160
create Inventory::clear_with_update
dylanopen Dec 27, 2025
c1abe5c
rebase feature/more-default-commands from master
dylanopen Dec 27, 2025
25a2308
create listener for ClearPlayerInventory
dylanopen Dec 27, 2025
c61597e
create command /clear
dylanopen Dec 27, 2025
d8081f7
remove unused imports from default_commands::clear
dylanopen Dec 27, 2025
9d618e4
create Inventory::clear_slot
dylanopen Dec 27, 2025
76dd4bc
update Inventory::clear to use Inventory::clear_slot
dylanopen Dec 27, 2025
81e18e1
create Inventory::clear_with_update
dylanopen Dec 27, 2025
09871ad
rebase feature/more-default-commands from master
dylanopen Dec 27, 2025
245520b
create listener for ClearPlayerInventory
dylanopen Dec 27, 2025
9893d73
create command /clear
dylanopen Dec 27, 2025
fab2270
remove unused imports from default_commands::clear
dylanopen Dec 27, 2025
5079243
fix incorrect bounds check in Integer::parse argtype
dylanopen Dec 28, 2025
96132df
fix incorrect bounds in Integer::primitive
dylanopen Dec 28, 2025
72886f9
create handler for PlayerGainedXP
dylanopen Dec 28, 2025
276fd02
create command /experience query
dylanopen Dec 28, 2025
d841e81
create command /experience add
dylanopen Dec 28, 2025
05012b7
reame function experience_command to experience_add_command
dylanopen Dec 28, 2025
197d0e9
document experience commands
dylanopen Dec 28, 2025
98c61ab
write PlayerLeveledUp message in handler for PlayerGainedXP
dylanopen Dec 28, 2025
d3ff8eb
create SetExperience packet
dylanopen Dec 28, 2025
bd4b535
update PlayerGainedXP handler to send packet
dylanopen Dec 28, 2025
b6a3dc4
change /experience query to display rounded progress (nearest int)
dylanopen Dec 28, 2025
db01426
add `/xp query` as alias for `/experience query`
dylanopen Dec 28, 2025
23e1e4d
add `/xp add` as alias for `/experience add`
dylanopen Dec 28, 2025
3a7ff1c
add confirmation message for /experience query
dylanopen Dec 28, 2025
5b4e86d
Merge remote-tracking branch 'origin/feature/more-default-commands' i…
dylanopen Dec 28, 2025
9be3aca
remove duplicate mod declaration
dylanopen Dec 28, 2025
c726437
remove unused imports of gamemode structs in default_commands::experi…
dylanopen Dec 28, 2025
3f834c4
default_commands: depend on config
dylanopen Dec 28, 2025
a655b7a
create command /list
dylanopen Dec 28, 2025
5e5a2cd
make default_commands::list mod public
dylanopen Dec 28, 2025
34e683b
commands: depend on data
dylanopen Dec 29, 2025
84f5ef8
default_commands: depend on data
dylanopen Dec 29, 2025
618dff6
default_commands: depend on inventories
dylanopen Dec 29, 2025
310f793
messages: depend on data
dylanopen Dec 29, 2025
ff48e8e
create message GiveItemToPlayer
dylanopen Dec 29, 2025
1a56a00
impl CommandArgument for Item
dylanopen Dec 29, 2025
1d8bd66
register message GiveItemToPlayer
dylanopen Dec 29, 2025
49602c3
create listener for GiveItemToPlayer
dylanopen Dec 29, 2025
2301289
create command /give
dylanopen Dec 29, 2025
06a2a63
remove commented out code in give_item_to_player listener
dylanopen Dec 29, 2025
cde25f1
remove dbg! from default_commands::clear
dylanopen Dec 29, 2025
11be724
use inventory update functions instead of manual update queue
dylanopen Dec 29, 2025
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
9 changes: 6 additions & 3 deletions src/bin/src/register_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ use ferrumc_messages::chunk_calc::ChunkCalc;
use ferrumc_messages::entity_update::SendEntityUpdate;
use ferrumc_messages::particle::SendParticle;
use ferrumc_messages::{
BlockBrokenEvent, PlayerCancelledDigging, PlayerDamaged, PlayerDied, PlayerEating,
PlayerFinishedDigging, PlayerGainedXP, PlayerGameModeChanged, PlayerJoined, PlayerLeft,
PlayerLeveledUp, PlayerStartedDigging, SpawnEntityCommand, SpawnEntityEvent,
BlockBrokenEvent, ClearPlayerInventory, GiveItemToPlayer, PlayerCancelledDigging,
PlayerDamaged, PlayerDied, PlayerEating, PlayerFinishedDigging, PlayerGainedXP,
PlayerGameModeChanged, PlayerJoined, PlayerLeft, PlayerLeveledUp, PlayerStartedDigging,
SpawnEntityCommand, SpawnEntityEvent,
};
use ferrumc_net::packets::packet_messages::Movement;

Expand All @@ -32,7 +33,9 @@ pub fn register_messages(world: &mut World) {
MessageRegistry::register_message::<PlayerGameModeChanged>(world);
MessageRegistry::register_message::<SpawnEntityCommand>(world);
MessageRegistry::register_message::<SpawnEntityEvent>(world);
MessageRegistry::register_message::<ClearPlayerInventory>(world);
MessageRegistry::register_message::<SendEntityUpdate>(world);
MessageRegistry::register_message::<SendParticle>(world);
MessageRegistry::register_message::<BlockBrokenEvent>(world);
MessageRegistry::register_message::<GiveItemToPlayer>(world);
}
17 changes: 17 additions & 0 deletions src/bin/src/systems/listeners/clear_player_inventory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use bevy_ecs::message::MessageReader;
use bevy_ecs::query::With;
use bevy_ecs::system::Query;
use ferrumc_components::player::abilities::PlayerAbilities;
use ferrumc_inventories::inventory::Inventory;
use ferrumc_messages::ClearPlayerInventory;

pub fn handle_clear_player_inventory(
mut events: MessageReader<ClearPlayerInventory>,
mut player_inventories: Query<&mut Inventory, With<PlayerAbilities>>, // querying for PlayerAbilities in order to check if player??
) {
for event in events.read() {
let Ok(mut inventory) = player_inventories.get_mut(event.player) else {return};
inventory.clear_with_update(event.player);
dbg!(&inventory);
}
}
42 changes: 42 additions & 0 deletions src/bin/src/systems/listeners/experience.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use bevy_ecs::message::{MessageReader, MessageWriter};
use bevy_ecs::prelude::Query;
use bevy_ecs::query::With;
use tracing::error;
use ferrumc_components::player::experience::Experience;
use ferrumc_core::identity::player_identity::PlayerIdentity;
use ferrumc_messages::{PlayerGainedXP, PlayerLeveledUp};
use ferrumc_net::connection::StreamWriter;
use ferrumc_net::packets::outgoing::set_experience::SetExperience;
use ferrumc_net_codec::net_types::var_int::VarInt;

pub fn player_gained_xp_handler(
mut events: MessageReader<PlayerGainedXP>,
mut player_leveled_up: MessageWriter<PlayerLeveledUp>,
mut xp_players: Query<(&mut Experience, &StreamWriter), With<PlayerIdentity>>
) {
for event in events.read() {
let Ok(player) = xp_players.get_mut(event.player) else {continue};
let mut xp = player.0;
let writer = player.1;
let old_level = xp.level;
xp.total_xp += event.amount;
let level = (xp.total_xp as f32 + 9.0).sqrt() - 3.0;
xp.level = level.floor() as u32;
if xp.level != old_level {
player_leveled_up.write(PlayerLeveledUp {
player: event.player,
new_level: xp.level,
});
}
xp.progress = level % 1.0;

let packet = SetExperience {
level: VarInt(xp.level as i32),
experience_bar: xp.progress,
total_experience: VarInt(xp.total_xp as i32),
};
if let Err(err) = writer.send_packet_ref(&packet) {
error!("Failed to send set_experience packet: {:?}", err);
};
}
}
62 changes: 62 additions & 0 deletions src/bin/src/systems/listeners/give_item_to_player.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use bevy_ecs::message::MessageReader;
use bevy_ecs::query::With;
use bevy_ecs::system::Query;
use ferrumc_core::identity::player_identity::PlayerIdentity;
use ferrumc_inventories::inventory::Inventory;
use ferrumc_inventories::item::ItemID;
use ferrumc_inventories::slot::InventorySlot;
use ferrumc_messages::GiveItemToPlayer;
use ferrumc_net_codec::net_types::var_int::VarInt;

pub fn give_item_to_player_handler(
mut events: MessageReader<GiveItemToPlayer>,
mut player_inventories: Query<&mut Inventory, With<PlayerIdentity>>,
) {
for event in events.read() {
let Ok(mut inventory) = player_inventories.get_mut(event.player) else {
continue;
};
let mut quantity = event.quantity;

let ordered_slot_indexes = vec![36..45, 9..36]
.into_iter() // hotbar before main inventory
.flatten()
.collect::<Vec<usize>>();

// fill *existing* stacks of items
for i in ordered_slot_indexes.clone() {
let slot = inventory.slots.get_mut(i).unwrap().clone();
let Some(mut slot) = slot else { continue };
let Some(item_id) = slot.item_id else {
continue;
};
if item_id.as_u32() as u16 != event.item_id {
continue;
}
let slot_quantity_to_add = (64 - slot.count.0).min(quantity as i32);
slot.count.0 += slot_quantity_to_add;
quantity -= slot_quantity_to_add as u32;
let _ = inventory.set_item_with_update(i, slot.clone(), event.player);
}

// add *new* stacks of items
for i in ordered_slot_indexes {
if inventory.get_item(i).unwrap().is_some() {
continue;
}
let slot_quantity_to_add = quantity.min(64);
quantity -= slot_quantity_to_add;
let slot = InventorySlot {
item_id: Some(ItemID::new(event.item_id as i32)),
count: VarInt(slot_quantity_to_add as i32),
components_to_add: None,
components_to_add_count: None,
components_to_remove: None,
components_to_remove_count: None,
};
inventory
.set_item_with_update(i, slot.clone(), event.player)
.unwrap();
}
}
}
6 changes: 6 additions & 0 deletions src/bin/src/systems/listeners/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
mod clear_player_inventory;
pub mod digging_system;
pub mod entity_spawn;
pub mod experience;
pub mod gamemode_change;
pub mod give_item_to_player;
pub mod player_join_message;
pub mod player_leave_message;

Expand All @@ -13,4 +16,7 @@ pub fn register_gameplay_listeners(schedule: &mut bevy_ecs::schedule::Schedule)
schedule.add_systems(digging_system::handle_start_digging);
schedule.add_systems(digging_system::handle_cancel_digging);
schedule.add_systems(digging_system::handle_finish_digging);
schedule.add_systems(clear_player_inventory::handle_clear_player_inventory);
schedule.add_systems(experience::player_gained_xp_handler);
schedule.add_systems(give_item_to_player::give_item_to_player_handler);
}
1 change: 1 addition & 0 deletions src/lib/commands/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ferrumc-net-codec = { workspace = true }
regex = { workspace = true }
ferrumc-components = {workspace = true }
ferrumc-nbt = { workspace = true }
ferrumc-data = { workspace = true }

[dev-dependencies] # Needed for the ServerState mock... :concern:
ferrumc-world = { workspace = true }
39 changes: 39 additions & 0 deletions src/lib/commands/src/arg/item.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use crate::{
arg::{CommandArgument, ParserResult},
CommandContext, Suggestion,
};

use super::PrimitiveArgument;
use ferrumc_data::items::Item;
use ferrumc_text::TextComponent;

// Implement the trait directly for the enum
impl CommandArgument for Item {
fn parse(ctx: &mut CommandContext) -> ParserResult<Self> {
let str = ctx.input.read_string();

let item = match Item::from_registry_key(&str) {
Some(item) => item,
None => match Item::from_registry_key(&format!("minecraft:{str}")) {
Some(item) => item,
None => {
return Err(Box::new(TextComponent::from(format!(
"Unknown item type: {str}"
))))
}
},
};

Ok(item.clone())
}

fn primitive() -> PrimitiveArgument {
// We're parsing a single word
PrimitiveArgument::word()
}

fn suggest(ctx: &mut CommandContext) -> Vec<Suggestion> {
ctx.input.read_string();
Vec::new() // unsure currently how to suggest registry keys
}
}
1 change: 1 addition & 0 deletions src/lib/commands/src/arg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{ctx::CommandContext, Suggestion};

pub mod duration;
pub mod gamemode;
pub mod item;
pub mod primitive;

pub type ParserResult<T> = Result<T, Box<TextComponent>>;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/commands/src/arg/primitive/int.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ impl<const MIN: i32, const MAX: i32> CommandArgument for Integer<MIN, MAX> {
)));
}

if int > MIN {
if int > MAX {
return Err(parser_error(&format!(
"integer too large: {int}, expected at most {MIN}"
)));
Expand All @@ -87,6 +87,6 @@ impl<const MIN: i32, const MAX: i32> CommandArgument for Integer<MIN, MAX> {
}

fn primitive() -> PrimitiveArgument {
PrimitiveArgument::int(Some(MIN), Some(MIN))
PrimitiveArgument::int(Some(MIN), Some(MAX))
}
}
3 changes: 3 additions & 0 deletions src/lib/default_commands/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ ferrumc-commands = { workspace = true }
ferrumc-macros = { workspace = true }
ferrumc-text = { workspace = true }
ferrumc-core = { workspace = true }
ferrumc-config = { workspace = true }
ferrumc-net = { workspace = true }
ferrumc-performance = { workspace = true }
ferrumc-data = { workspace = true }
ferrumc-inventories = { workspace = true }
ferrumc-entities = { workspace = true }
lazy_static = { workspace = true }
bimap = { workspace = true }
Expand Down
19 changes: 19 additions & 0 deletions src/lib/default_commands/src/clear.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use bevy_ecs::message::MessageWriter;
use ferrumc_commands::Sender;
use ferrumc_macros::command;
use ferrumc_messages::ClearPlayerInventory;

#[command("clear")]
fn tps_command(#[sender] sender: Sender, mut clear_inventory: MessageWriter<ClearPlayerInventory>) {
// 1. Ensure the sender is a player
let player_entity = match sender {
Sender::Server => {
sender.send_message("Error: cannot change gamemode of server.".into(), false);
return;
}
Sender::Player(entity) => entity,
};
let _ = clear_inventory.write(ClearPlayerInventory {
player: player_entity,
});
}
76 changes: 76 additions & 0 deletions src/lib/default_commands/src/experience.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use bevy_ecs::prelude::*;
use ferrumc_commands::arg::primitive::int::Integer;
use ferrumc_commands::Sender;
use ferrumc_components::player::experience::Experience;
use ferrumc_core::identity::player_identity::PlayerIdentity;
use ferrumc_macros::command;
use ferrumc_messages::PlayerGainedXP;
use ferrumc_text::TextComponent;

/// Adds experience points to the sender.
#[command("experience add")]
fn experience_add_command(
#[sender] sender: Sender,
#[arg] amount: Integer,
mut gained_xp_events: MessageWriter<PlayerGainedXP>,
) {
// 1. Ensure the sender is a player
let player_entity = match sender {
Sender::Server => {
sender.send_message("Error: The server can't change its XP level.".into(), false);
return;
},
Sender::Player(entity) => entity,
};

let amount = *amount as u32;

// 2. Fire the event
gained_xp_events.write(PlayerGainedXP {
player: player_entity,
amount,
});

let msg = TextComponent::from(format!("Added {amount} experience points to player."));
sender.send_message(msg, false);
}

/// Returns information about the experience points & levels of the sender.
#[command("experience query")]
fn experience_query_command(
#[sender] sender: Sender,
xp_players: Query<&Experience, With<PlayerIdentity>>,
) {
let player_entity = match sender {
Sender::Server => {
sender.send_message("Error: The server doesn't have an XP level.".into(), false);
return;
},
Sender::Player(entity) => entity,
};

let Ok(xp) = xp_players.get(player_entity) else {return};
let levels = xp.level;
let progress = xp.progress * 100.0;

let msg = TextComponent::from(format!("You have {levels} experience levels and are {progress:.0}% to the next level."));
sender.send_message(msg, false);
}

#[command("xp add")]
fn xp_add_command(
#[sender] sender: Sender,
#[arg] amount: Integer,
gained_xp_events: MessageWriter<PlayerGainedXP>,
) {
experience_add_command(sender, amount, gained_xp_events);
}

#[command("xp query")]
fn xp_query_command(
#[sender] sender: Sender,
xp_players: Query<&Experience, With<PlayerIdentity>>,
) {
experience_query_command(sender, xp_players);
}

38 changes: 38 additions & 0 deletions src/lib/default_commands/src/give.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use bevy_ecs::message::MessageWriter;
use bevy_ecs::system::Query;
use ferrumc_commands::arg::primitive::int::Integer;
use ferrumc_commands::Sender;
use ferrumc_core::identity::player_identity::PlayerIdentity;
use ferrumc_data::items::Item;
use ferrumc_macros::command;
use ferrumc_messages::GiveItemToPlayer;
use ferrumc_text::TextComponent;

#[command("give")]
fn give_command(
#[sender] sender: Sender,
#[arg] item: Item,
#[arg] quantity: Integer,
args: (Query<&PlayerIdentity>, MessageWriter<GiveItemToPlayer>),
) {
let player_identities = args.0;
let mut give_events = args.1;

let item_name = item.registry_key;
let player_entity = match sender {
Sender::Server => {
sender.send_message("Error: server does not have an inventory.".into(), false);
return;
}
Sender::Player(entity) => entity,
};
let quantity = *quantity as u32;
let username = &player_identities.get(player_entity).unwrap().username;
let msg = TextComponent::from(format!("Gave {quantity} {item_name} to {username}"));
sender.send_message(msg, false);
give_events.write(GiveItemToPlayer {
item_id: item.id,
player: player_entity,
quantity,
});
}
4 changes: 4 additions & 0 deletions src/lib/default_commands/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
pub mod clear;
pub mod echo;
pub mod experience;
pub mod fly;
pub mod gamemode;
mod give;
pub mod list;
pub mod nested;
pub mod spawn;
pub mod tps;
Expand Down
Loading
Loading