Skip to content

Commit 22b5924

Browse files
authored
commands (#143)
ily bby
1 parent 7a6d183 commit 22b5924

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2481
-11
lines changed

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ members = [
1717
"src/lib/adapters/anvil",
1818
"src/lib/adapters/nbt",
1919
"src/lib/adapters/nbt",
20+
"src/lib/commands",
21+
"src/lib/default_commands",
2022
"src/lib/core",
2123
"src/lib/core/state",
2224
"src/lib/derive_macros",
@@ -88,6 +90,8 @@ debug = true
8890
ferrumc-anvil = { path = "src/lib/adapters/anvil" }
8991
ferrumc-config = { path = "src/lib/config" }
9092
ferrumc-core = { path = "src/lib/core" }
93+
ferrumc-default-commands = { path = "src/lib/default_commands" }
94+
ferrumc-commands = { path = "src/lib/commands" }
9195
ferrumc-general-purpose = { path = "src/lib/utils/general_purpose" }
9296
ferrumc-logging = { path = "src/lib/utils/logging" }
9397
ferrumc-macros = { path = "src/lib/derive_macros" }
@@ -106,7 +110,6 @@ ferrumc-utils = { path = "src/lib/utils" }
106110
ferrumc-world = { path = "src/lib/world" }
107111
ferrumc-world-gen = { path = "src/lib/world_gen" }
108112

109-
110113
# Asynchronous
111114
tokio = { version = "1.47.1", features = ["macros", "net", "rt", "sync", "time", "io-util", "test-util"], default-features = false }
112115

@@ -171,6 +174,7 @@ macro_rules_attribute = "0.2.2"
171174

172175
# Magic
173176
dhat = "0.3.3"
177+
ctor = "0.4.2"
174178

175179
# Compression/Decompression
176180
flate2 = { version = "1.1.2", features = ["zlib"], default-features = false }
@@ -191,6 +195,7 @@ colored = "3.0.0"
191195
# Misc
192196
deepsize = "0.2.0"
193197
page_size = "0.6.0"
198+
enum-ordinalize = "4.3.0"
194199
regex = "1.11.1"
195200
noise = "0.9.0"
196201
ctrlc = "3.4.7"

src/bin/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ ferrumc-world = { workspace = true }
2525
ferrumc-macros = { workspace = true }
2626
ferrumc-general-purpose = { workspace = true }
2727
ferrumc-state = { workspace = true }
28+
ferrumc-commands = { workspace = true }
29+
ferrumc-default-commands = { workspace = true }
2830
ferrumc-world-gen = { workspace = true }
2931
ferrumc-threadpool = { workspace = true }
3032

31-
3233
tracing = { workspace = true }
3334
clap = { workspace = true, features = ["derive", "env"] }
3435
flate2 = { workspace = true }

src/bin/src/game_loop.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::systems::shutdown_systems::register_shutdown_systems;
77
use bevy_ecs::prelude::World;
88
use bevy_ecs::schedule::ExecutorKind;
99
use crossbeam_channel::Sender;
10+
use ferrumc_commands::infrastructure::register_command_systems;
1011
use ferrumc_config::server_config::get_global_config;
1112
use ferrumc_net::connection::{handle_connection, NewConnection};
1213
use ferrumc_net::server::create_server_listener;
@@ -37,13 +38,16 @@ pub fn start_game_loop(global_state: GlobalState) -> Result<(), BinaryError> {
3738
let (shutdown_send, shutdown_recv) = tokio::sync::oneshot::channel();
3839
let (shutdown_response_send, shutdown_response_recv) = crossbeam_channel::unbounded();
3940

41+
ferrumc_default_commands::init();
42+
4043
// Register systems and resources
4144
let global_state_res = GlobalStateResource(global_state.clone());
4245

4346
register_events(&mut ecs_world);
4447
register_resources(&mut ecs_world, new_conn_recv, global_state_res);
4548
register_packet_handlers(&mut schedule);
4649
register_player_systems(&mut schedule);
50+
register_command_systems(&mut schedule);
4751
register_game_systems(&mut schedule);
4852

4953
register_shutdown_systems(&mut shutdown_schedule);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use bevy_ecs::prelude::*;
2+
use ferrumc_core::{identity::player_identity::PlayerIdentity, mq};
3+
use ferrumc_net::ChatMessagePacketReceiver;
4+
use ferrumc_text::TextComponent;
5+
6+
pub fn handle(events: Res<ChatMessagePacketReceiver>, query: Query<&PlayerIdentity>) {
7+
for (message, sender) in events.0.try_iter() {
8+
let Ok(identity) = query.get(sender) else {
9+
continue;
10+
};
11+
12+
let message = format!("<{}> {}", identity.username, message.message);
13+
mq::broadcast(TextComponent::from(message), false);
14+
}
15+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use std::sync::Arc;
2+
3+
use bevy_ecs::prelude::*;
4+
use ferrumc_commands::{
5+
events::{CommandDispatchEvent, ResolvedCommandDispatchEvent},
6+
infrastructure, Command, CommandContext, CommandInput, Sender,
7+
};
8+
use ferrumc_core::mq;
9+
use ferrumc_net::ChatCommandPacketReceiver;
10+
use ferrumc_text::{NamedColor, TextComponent, TextComponentBuilder};
11+
12+
fn resolve(
13+
input: String,
14+
sender: Sender,
15+
) -> Result<(Arc<Command>, CommandContext), Box<TextComponent>> {
16+
let command = infrastructure::find_command(&input);
17+
if command.is_none() {
18+
return Err(Box::new(
19+
TextComponentBuilder::new("Unknown command")
20+
.color(NamedColor::Red)
21+
.build(),
22+
));
23+
}
24+
25+
let command = command.unwrap();
26+
let input = input
27+
.strip_prefix(command.name)
28+
.unwrap_or(&input)
29+
.trim_start();
30+
let input = CommandInput::of(input.to_string());
31+
let ctx = CommandContext {
32+
input: input.clone(),
33+
command: command.clone(),
34+
sender,
35+
};
36+
37+
Ok((command, ctx))
38+
}
39+
40+
pub fn handle(
41+
events: Res<ChatCommandPacketReceiver>,
42+
mut dispatch_events: EventWriter<CommandDispatchEvent>,
43+
mut resolved_dispatch_events: EventWriter<ResolvedCommandDispatchEvent>,
44+
) {
45+
for (event, entity) in events.0.try_iter() {
46+
let sender = Sender::Player(entity);
47+
dispatch_events.write(CommandDispatchEvent {
48+
command: event.command.clone(),
49+
sender,
50+
});
51+
52+
let resolved = resolve(event.command, sender);
53+
match resolved {
54+
Err(err) => {
55+
mq::queue(*err, false, entity);
56+
}
57+
58+
Ok((command, ctx)) => {
59+
resolved_dispatch_events.write(ResolvedCommandDispatchEvent {
60+
command,
61+
ctx,
62+
sender,
63+
});
64+
}
65+
}
66+
}
67+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use std::sync::Arc;
2+
3+
use bevy_ecs::prelude::*;
4+
use ferrumc_commands::{Command, CommandContext, CommandInput, Sender, ROOT_COMMAND};
5+
use ferrumc_net::{
6+
connection::StreamWriter,
7+
packets::outgoing::command_suggestions::{CommandSuggestionsPacket, Match},
8+
CommandSuggestionRequestReceiver,
9+
};
10+
use ferrumc_net_codec::net_types::{
11+
length_prefixed_vec::LengthPrefixedVec, prefixed_optional::PrefixedOptional, var_int::VarInt,
12+
};
13+
use ferrumc_state::GlobalStateResource;
14+
use tracing::error;
15+
16+
fn find_command(input: String) -> Option<Arc<Command>> {
17+
let mut input = input;
18+
if input.starts_with("/") {
19+
input.remove(0);
20+
}
21+
22+
if let Some(command) = ferrumc_commands::infrastructure::get_command_by_name(&input) {
23+
return Some(command);
24+
}
25+
26+
if let Some(command) = ferrumc_commands::infrastructure::find_command(&input) {
27+
return Some(command);
28+
}
29+
30+
while !input.is_empty() {
31+
// remove the last word and retry
32+
if let Some(pos) = input.rfind(char::is_whitespace) {
33+
input.truncate(pos);
34+
35+
if let Some(command) = ferrumc_commands::infrastructure::get_command_by_name(&input) {
36+
return Some(command);
37+
}
38+
39+
if let Some(command) = ferrumc_commands::infrastructure::find_command(&input) {
40+
return Some(command);
41+
}
42+
} else {
43+
break; // string does not have any further words, meaning it's just whitespace?
44+
}
45+
}
46+
47+
None
48+
}
49+
50+
fn create_ctx(input: String, command: Option<Arc<Command>>, sender: Sender) -> CommandContext {
51+
let input = input
52+
.strip_prefix(command.clone().map(|c| c.name).unwrap_or_default())
53+
.unwrap_or(&input)
54+
.trim_start();
55+
56+
let input = CommandInput::of(input.to_string());
57+
CommandContext {
58+
input: input.clone(),
59+
command: command.unwrap_or(ROOT_COMMAND.clone()),
60+
sender,
61+
}
62+
}
63+
64+
pub fn handle(
65+
events: Res<CommandSuggestionRequestReceiver>,
66+
query: Query<&StreamWriter>,
67+
state: Res<GlobalStateResource>,
68+
) {
69+
for (request, entity) in events.0.try_iter() {
70+
if !state.0.players.is_connected(entity) {
71+
return;
72+
}
73+
74+
let input = request.input;
75+
76+
let command = find_command(input.clone());
77+
let command_arg = input
78+
.clone()
79+
.strip_prefix(&format!(
80+
"/{} ",
81+
command.clone().map(|c| c.name).unwrap_or_default()
82+
))
83+
.unwrap_or(&input)
84+
.to_string();
85+
let mut ctx = create_ctx(command_arg.clone(), command.clone(), Sender::Player(entity));
86+
let command_arg = command_arg.clone(); // ok borrow checker
87+
let tokens = command_arg.split(" ").collect::<Vec<&str>>();
88+
let Some(current_token) = tokens.last() else {
89+
return; // whitespace
90+
};
91+
92+
let mut suggestions = Vec::new();
93+
94+
if let Some(command) = command {
95+
for arg in command.args.clone() {
96+
let arg_suggestions = (arg.suggester)(&mut ctx);
97+
ctx.input.skip_whitespace(u32::MAX, true);
98+
if !ctx.input.has_remaining_input() {
99+
suggestions = arg_suggestions;
100+
break;
101+
}
102+
}
103+
}
104+
105+
let length = input.len();
106+
let start = length - current_token.len();
107+
108+
if let Err(e) = query
109+
.get(entity)
110+
.unwrap()
111+
.send_packet(CommandSuggestionsPacket {
112+
transaction_id: request.transaction_id,
113+
matches: LengthPrefixedVec::new(
114+
suggestions
115+
.into_iter()
116+
.filter(|sug| sug.content.starts_with(current_token))
117+
.map(|sug| Match {
118+
content: sug.content,
119+
tooltip: PrefixedOptional::new(sug.tooltip),
120+
})
121+
.collect(),
122+
),
123+
length: VarInt::new(length as i32),
124+
start: VarInt::new(start as i32),
125+
})
126+
{
127+
error!("failed sending command suggestions to player: {e}")
128+
}
129+
}
130+
}

src/bin/src/packet_handlers/play_packets/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use bevy_ecs::schedule::Schedule;
22

3+
mod chat_message;
34
mod chunk_batch_ack;
5+
mod command;
6+
mod command_suggestions;
47
mod confirm_player_teleport;
58
mod keep_alive;
69
mod place_block;
@@ -26,4 +29,7 @@ pub fn register_packet_handlers(schedule: &mut Schedule) {
2629
schedule.add_systems(set_player_rotation::handle);
2730
schedule.add_systems(swing_arm::handle);
2831
schedule.add_systems(player_loaded::handle);
32+
schedule.add_systems(command::handle);
33+
schedule.add_systems(command_suggestions::handle);
34+
schedule.add_systems(chat_message::handle);
2935
}

src/bin/src/register_events.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use bevy_ecs::event::EventRegistry;
22
use bevy_ecs::prelude::World;
3+
use ferrumc_commands::events::{CommandDispatchEvent, ResolvedCommandDispatchEvent};
34
use ferrumc_core::chunks::cross_chunk_boundary_event::CrossChunkBoundaryEvent;
45
use ferrumc_core::conn::force_player_recount_event::ForcePlayerRecountEvent;
56
use ferrumc_net::packets::packet_events::TransformEvent;
@@ -8,4 +9,6 @@ pub fn register_events(world: &mut World) {
89
EventRegistry::register_event::<TransformEvent>(world);
910
EventRegistry::register_event::<CrossChunkBoundaryEvent>(world);
1011
EventRegistry::register_event::<ForcePlayerRecountEvent>(world);
12+
EventRegistry::register_event::<CommandDispatchEvent>(world);
13+
EventRegistry::register_event::<ResolvedCommandDispatchEvent>(world);
1114
}

src/bin/src/systems/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod connection_killer;
22
mod cross_chunk_boundary;
33
mod keep_alive_system;
4+
mod mq;
45
pub mod new_connections;
56
mod player_count_update;
67
pub mod send_chunks;
@@ -13,6 +14,7 @@ pub fn register_game_systems(schedule: &mut bevy_ecs::schedule::Schedule) {
1314
schedule.add_systems(cross_chunk_boundary::cross_chunk_boundary);
1415
schedule.add_systems(player_count_update::player_count_updater);
1516
schedule.add_systems(world_sync::sync_world);
17+
schedule.add_systems(mq::process);
1618

1719
// Should always be last
1820
schedule.add_systems(connection_killer::connection_killer);

src/bin/src/systems/mq.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use bevy_ecs::prelude::*;
2+
use ferrumc_core::mq;
3+
use ferrumc_net::{
4+
connection::StreamWriter, packets::outgoing::system_message::SystemMessagePacket,
5+
};
6+
use ferrumc_state::GlobalStateResource;
7+
use tracing::error;
8+
9+
fn send(
10+
writer: &StreamWriter,
11+
receiver: Entity,
12+
state: &GlobalStateResource,
13+
entry: ferrumc_core::mq::QueueEntry,
14+
) {
15+
if !state.0.players.is_connected(receiver) {
16+
return;
17+
}
18+
19+
if let Err(err) = writer.send_packet(SystemMessagePacket {
20+
message: entry.message,
21+
overlay: entry.overlay,
22+
}) {
23+
error!("failed sending queued message to player: {err}");
24+
}
25+
}
26+
27+
pub fn process(query: Query<(Entity, &StreamWriter)>, state: Res<GlobalStateResource>) {
28+
while !mq::QUEUE.is_empty() {
29+
let entry = mq::QUEUE.pop().unwrap();
30+
31+
match entry.receiver {
32+
Some(receiver) => {
33+
let Ok((_, writer)) = query.get(receiver) else {
34+
continue;
35+
};
36+
37+
send(writer, receiver, &state, entry);
38+
}
39+
40+
None => {
41+
for (receiver, writer) in query {
42+
send(writer, receiver, &state, entry.clone());
43+
}
44+
}
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)