feat: add waypoint command

This commit is contained in:
Alexander Medvedev
2026-08-07 10:58:46 +02:00
parent 7e60303689
commit 19ccd152f1
17 changed files with 1008 additions and 138 deletions

View File

@@ -26,6 +26,20 @@ impl CUpdateTime {
clock_updates: vec![(overworld_id, day_time, partial_tick, rate)],
}
}
#[must_use]
pub fn new_clock(
game_time: i64,
clock_id: i32,
total_ticks: i64,
partial_tick: f32,
rate: f32,
) -> Self {
Self {
game_time,
clock_updates: vec![(clock_id, total_ticks, partial_tick, rate)],
}
}
}
impl ClientPacket for CUpdateTime {

View File

@@ -329,6 +329,35 @@ impl NamedColor {
Self::White => 'f',
}
}
/// Returns the Minecraft string identifier of this named color (e.g. `"black"`, `"dark_blue"`).
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Black => "black",
Self::DarkBlue => "dark_blue",
Self::DarkGreen => "dark_green",
Self::DarkAqua => "dark_aqua",
Self::DarkRed => "dark_red",
Self::DarkPurple => "dark_purple",
Self::Gold => "gold",
Self::Gray => "gray",
Self::DarkGray => "dark_gray",
Self::Blue => "blue",
Self::Green => "green",
Self::Aqua => "aqua",
Self::Red => "red",
Self::LightPurple => "light_purple",
Self::Yellow => "yellow",
Self::White => "white",
}
}
}
impl std::fmt::Display for NamedColor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
impl TryFrom<&str> for NamedColor {

View File

@@ -0,0 +1,132 @@
use pumpkin_data::translation;
use pumpkin_protocol::java::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders};
use pumpkin_util::text::TextComponent;
use crate::command::CommandSender;
use crate::command::args::{
Arg, ArgumentConsumer, ConsumeResult, ConsumeResultWithSyntax, DefaultNameArgConsumer, FindArg,
GetClientSideArgParser, SuggestResult,
};
use crate::command::dispatcher::CommandError;
use crate::command::errors::error_types::CommandErrorType;
use crate::command::tree::RawArgs;
use crate::server::Server;
pub const INVALID_HEX_ERROR_TYPE: CommandErrorType<1> = CommandErrorType::new(
translation::java::ARGUMENT_HEXCOLOR_INVALID,
translation::java::ARGUMENT_HEXCOLOR_INVALID,
);
pub struct HexColorArgumentConsumer;
fn parse_hex_color(color_str: &str) -> Option<u32> {
match color_str.len() {
3 => {
let r = u32::from_str_radix(&color_str[0..1], 16).ok()? * 17;
let g = u32::from_str_radix(&color_str[1..2], 16).ok()? * 17;
let b = u32::from_str_radix(&color_str[2..3], 16).ok()? * 17;
Some((r << 16) | (g << 8) | b)
}
6 => {
let r = u32::from_str_radix(&color_str[0..2], 16).ok()?;
let g = u32::from_str_radix(&color_str[2..4], 16).ok()?;
let b = u32::from_str_radix(&color_str[4..6], 16).ok()?;
Some((r << 16) | (g << 8) | b)
}
_ => None,
}
}
impl GetClientSideArgParser for HexColorArgumentConsumer {
fn get_client_side_parser(&self) -> ArgumentType {
ArgumentType::HexColor
}
fn get_client_side_suggestion_type_override(&self) -> Option<SuggestionProviders> {
None
}
}
impl ArgumentConsumer for HexColorArgumentConsumer {
fn consume<'a, 'b>(
&'a self,
_sender: &'a CommandSender,
_server: &'a Server,
args: &'b mut RawArgs<'a>,
) -> ConsumeResult<'a> {
let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value);
let result = s_opt.and_then(|s| parse_hex_color(s).map(Arg::HexColor));
Box::pin(async move { result })
}
fn consume_with_syntax<'a>(
&'a self,
_sender: &'a CommandSender,
_server: &'a Server,
args: &mut RawArgs<'a>,
) -> ConsumeResultWithSyntax<'a> {
let Some(raw_arg) = args.pop() else {
return Box::pin(async { Ok(None) });
};
let result = parse_hex_color(raw_arg.value)
.map(|color| Some(Arg::HexColor(color)))
.ok_or_else(|| {
INVALID_HEX_ERROR_TYPE.create_without_context(TextComponent::translate(
translation::java::ARGUMENT_HEXCOLOR_INVALID,
[TextComponent::text(raw_arg.value.to_string())],
))
});
Box::pin(async move { result })
}
fn suggest<'a>(
&'a self,
_sender: &CommandSender,
_server: &'a Server,
_input: &'a str,
) -> SuggestResult<'a> {
let suggestions = vec![
CommandSuggestion::new("F00".to_string(), None),
CommandSuggestion::new("FF0000".to_string(), None),
];
Box::pin(async move { Ok(Some(suggestions)) })
}
}
impl DefaultNameArgConsumer for HexColorArgumentConsumer {
fn default_name(&self) -> &'static str {
"color"
}
}
impl<'a> FindArg<'a> for HexColorArgumentConsumer {
type Data = u32;
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::HexColor(color)) => Ok(*color),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_hex_color_works() {
assert_eq!(parse_hex_color("F00"), Some(0xFF0000));
assert_eq!(parse_hex_color("0F0"), Some(0x00FF00));
assert_eq!(parse_hex_color("00F"), Some(0x0000FF));
assert_eq!(parse_hex_color("FF0000"), Some(0xFF0000));
assert_eq!(parse_hex_color("123456"), Some(0x123456));
assert_eq!(parse_hex_color("FFFFF"), None);
assert_eq!(parse_hex_color("F"), None);
assert_eq!(parse_hex_color("GGGGGG"), None);
}
}

View File

@@ -41,6 +41,7 @@ pub mod entity;
pub mod entity_anchor;
pub mod gamemode;
pub mod gameprofile;
pub mod hex_color;
pub mod message;
pub mod players;
pub mod position_2d;
@@ -54,6 +55,7 @@ pub mod slot;
pub mod sound;
pub mod sound_category;
pub mod summonable_entities;
pub mod team_color;
pub mod textcomponent;
pub mod time;
@@ -143,6 +145,8 @@ pub enum Arg<'a> {
EntityAnchor(EntityAnchor),
Slot(usize, String),
Slots(&'static [usize], String),
TeamColor(pumpkin_util::text::color::NamedColor),
HexColor(u32),
}
/// see [`crate::command::tree::builder::argument`] and [`CommandTree::execute`]/[`crate::command::tree::builder::NonLeafNodeBuilder::execute`]

View File

@@ -0,0 +1,130 @@
use pumpkin_data::translation;
use pumpkin_protocol::java::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders};
use pumpkin_util::text::TextComponent;
use pumpkin_util::text::color::NamedColor;
use crate::command::CommandSender;
use crate::command::args::{
Arg, ArgumentConsumer, ConsumeResult, ConsumeResultWithSyntax, DefaultNameArgConsumer, FindArg,
GetClientSideArgParser, SuggestResult,
};
use crate::command::dispatcher::CommandError;
use crate::command::errors::error_types::CommandErrorType;
use crate::command::tree::RawArgs;
use crate::server::Server;
pub const INVALID_COLOR_ERROR_TYPE: CommandErrorType<1> = CommandErrorType::new(
translation::java::ARGUMENT_COLOR_INVALID,
translation::java::ARGUMENT_COLOR_INVALID,
);
const TEAM_COLORS: [&str; 16] = [
"black",
"dark_blue",
"dark_green",
"dark_aqua",
"dark_red",
"dark_purple",
"gold",
"gray",
"dark_gray",
"blue",
"green",
"aqua",
"red",
"light_purple",
"yellow",
"white",
];
pub struct TeamColorArgumentConsumer;
impl GetClientSideArgParser for TeamColorArgumentConsumer {
fn get_client_side_parser(&self) -> ArgumentType {
ArgumentType::Color
}
fn get_client_side_suggestion_type_override(&self) -> Option<SuggestionProviders> {
Some(SuggestionProviders::AskServer)
}
}
impl ArgumentConsumer for TeamColorArgumentConsumer {
fn consume<'a, 'b>(
&'a self,
_sender: &'a CommandSender,
_server: &'a Server,
args: &'b mut RawArgs<'a>,
) -> ConsumeResult<'a> {
let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value);
let result = s_opt.and_then(|s| NamedColor::try_from(s).ok().map(Arg::TeamColor));
Box::pin(async move { result })
}
fn consume_with_syntax<'a>(
&'a self,
_sender: &'a CommandSender,
_server: &'a Server,
args: &mut RawArgs<'a>,
) -> ConsumeResultWithSyntax<'a> {
let Some(raw_arg) = args.pop() else {
return Box::pin(async { Ok(None) });
};
let result = NamedColor::try_from(raw_arg.value)
.map(|color| Some(Arg::TeamColor(color)))
.map_err(|()| {
INVALID_COLOR_ERROR_TYPE.create_without_context(TextComponent::translate(
translation::java::ARGUMENT_COLOR_INVALID,
[TextComponent::text(raw_arg.value.to_string())],
))
});
Box::pin(async move { result })
}
fn suggest<'a>(
&'a self,
_sender: &CommandSender,
_server: &'a Server,
input: &'a str,
) -> SuggestResult<'a> {
let suggestions: Vec<CommandSuggestion> = TEAM_COLORS
.iter()
.filter(|color| color.starts_with(input))
.map(|color| CommandSuggestion::new((*color).to_string(), None))
.collect();
Box::pin(async move { Ok(Some(suggestions)) })
}
}
impl DefaultNameArgConsumer for TeamColorArgumentConsumer {
fn default_name(&self) -> &'static str {
"color"
}
}
impl<'a> FindArg<'a> for TeamColorArgumentConsumer {
type Data = NamedColor;
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::TeamColor(color)) => Ok(*color),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_team_color() {
assert_eq!(NamedColor::try_from("red"), Ok(NamedColor::Red));
assert_eq!(NamedColor::try_from("dark_blue"), Ok(NamedColor::DarkBlue));
assert_eq!(NamedColor::try_from("invalid"), Err(()));
}
}

View File

@@ -11,11 +11,32 @@ use crate::command::{
};
use crate::server::Server;
pub struct TimeArgumentConsumer;
#[derive(Clone, Copy, Debug)]
pub struct TimeArgumentConsumer {
min: i32,
}
impl Default for TimeArgumentConsumer {
fn default() -> Self {
Self::new()
}
}
impl TimeArgumentConsumer {
#[must_use]
pub const fn new() -> Self {
Self { min: 0 }
}
#[must_use]
pub const fn min(min: i32) -> Self {
Self { min }
}
}
impl GetClientSideArgParser for TimeArgumentConsumer {
fn get_client_side_parser(&self) -> ArgumentType {
ArgumentType::Time { min: 0 }
ArgumentType::Time { min: self.min }
}
fn get_client_side_suggestion_type_override(&self) -> Option<SuggestionProviders> {
@@ -34,23 +55,23 @@ impl ArgumentConsumer for TimeArgumentConsumer {
let result: Option<Arg<'a>> = s_opt.and_then(|s| {
let (num_str, unit) = s
.find(|c: char| c.is_alphabetic())
.find(|c: char| c.is_alphabetic() && c != '-')
.map_or((s, "t"), |pos| (&s[..pos], &s[pos..]));
let number = num_str.parse::<f32>().ok()?; // Replaces .ok()?
let number = num_str.parse::<f32>().ok()?;
if number < 0.0 {
return None;
}
let ticks = match unit {
"d" => number * 24000.0,
"s" => number * 20.0,
"t" => number,
let factor = match unit {
"d" => 24000.0,
"s" => 20.0,
"t" | "" => 1.0,
_ => return None,
};
let ticks = ticks.round() as i32;
let ticks = (number * factor).round() as i32;
if ticks < self.min {
return None;
}
Some(Arg::Time(ticks))
});

View File

@@ -77,6 +77,7 @@ mod title;
mod tps;
mod transfer;
mod trigger;
mod waypoint;
mod weather;
mod whitelist;
mod worldborder;
@@ -150,6 +151,7 @@ pub async fn default_dispatcher(
);
dispatcher.register(spectate::init_command_tree(), "minecraft:command.spectate");
dispatcher.register(data::init_command_tree(), "minecraft:command.data");
dispatcher.register(waypoint::init_command_tree(), "minecraft:command.waypoint");
// Three
dispatcher.register(deop::init_command_tree(), "minecraft:command.deop");
dispatcher.register(kick::init_command_tree(), "minecraft:command.kick");
@@ -281,6 +283,13 @@ fn register_level_2_permissions(registry: &mut PermissionRegistry) {
PermissionDefault::Op(PermissionLvl::Two),
))
.expect("Permission already registered");
registry
.register_permission(Permission::new(
"minecraft:command.waypoint",
"List or modify waypoints",
PermissionDefault::Op(PermissionLvl::Two),
))
.expect("Permission already registered");
registry
.register_permission(Permission::new(
"minecraft:command.give",

View File

@@ -449,6 +449,50 @@ impl CommandExecutor for TeamListExecutor {
}
}
struct TeamModifyColorResetExecutor;
impl CommandExecutor for TeamModifyColorResetExecutor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move {
let team_name = TeamArgumentType::get(context, ARG_TEAM)?;
let world = context.world();
let mut scoreboard = world.scoreboard.lock().await;
let mut team = scoreboard
.get_teams()
.get(team_name)
.ok_or_else(|| {
TEAM_NOT_FOUND_ERROR
.create_without_context(TextComponent::text(team_name.to_string()))
})?
.clone();
if team.color == NamedColor::White {
return Err(COLOR_UNCHANGED_ERROR.create_without_context());
}
team.color = NamedColor::White;
let team_display_name = team.display_name.clone();
scoreboard.update_team(world, team);
context
.source
.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_TEAM_OPTION_COLOR_CLEAR_SUCCESS,
translation::java::COMMANDS_TEAM_OPTION_COLOR_CLEAR_SUCCESS,
[team_display_name],
),
true,
)
.await;
Ok(1)
})
}
}
struct TeamModifyColorExecutor;
impl CommandExecutor for TeamModifyColorExecutor {
@@ -483,10 +527,7 @@ impl CommandExecutor for TeamModifyColorExecutor {
TextComponent::translate_cross(
translation::java::COMMANDS_TEAM_OPTION_COLOR_SUCCESS,
translation::java::COMMANDS_TEAM_OPTION_COLOR_SUCCESS,
[
team_display_name,
TextComponent::text(format!("{new_color:?}").to_lowercase()),
],
[team_display_name, TextComponent::text(new_color.name())],
),
true,
)
@@ -962,12 +1003,16 @@ fn list_branch() -> LiteralArgumentBuilder {
.then(argument(ARG_TEAM, TeamArgumentType).executes(TeamListExecutor { has_team: true }))
}
#[expect(clippy::too_many_lines)]
fn modify_branch() -> LiteralArgumentBuilder {
literal("modify").then(
argument(ARG_TEAM, TeamArgumentType)
.then(
literal("color").then(
argument(ARG_VALUE, TeamColorArgumentType).executes(TeamModifyColorExecutor),
literal("color")
.then(literal("reset").executes(TeamModifyColorResetExecutor))
.then(
argument(ARG_VALUE, TeamColorArgumentType)
.executes(TeamModifyColorExecutor),
),
)
.then(

View File

@@ -2,14 +2,21 @@ use pumpkin_data::translation;
use pumpkin_util::text::TextComponent;
use crate::command::CommandResult;
use crate::command::args::{FindArg, time::TimeArgumentConsumer};
use crate::command::args::{
FindArg, bounded_num::BoundedNumArgumentConsumer,
resource_location::ResourceLocationArgumentConsumer, time::TimeArgumentConsumer,
};
use crate::command::dispatcher::CommandError;
use crate::command::tree::builder::{argument, literal};
use crate::command::{CommandExecutor, CommandSender, ConsumedArgs, tree::CommandTree};
const NAMES: [&str; 1] = ["time"];
const DESCRIPTION: &str = "Query the world time.";
const DESCRIPTION: &str = "Query or modify the world time and clocks.";
const ARG_TIME: &str = "time";
const ARG_RATE: &str = "rate";
const ARG_CLOCK: &str = "clock";
const DEFAULT_CLOCK: &str = "minecraft:overworld";
#[derive(Clone, Copy)]
enum PresetTime {
@@ -31,18 +38,26 @@ impl PresetTime {
}
#[derive(Clone, Copy)]
enum Mode {
Add,
enum Action {
Set(Option<PresetTime>),
Add,
Pause,
Resume,
Rate,
}
#[derive(Clone, Copy)]
enum QueryMode {
DayTime,
Time,
GameTime,
DayTime,
Day,
}
const fn wrap_time(ticks: i64) -> i32 {
(ticks % 2_147_483_647) as i32
}
struct QueryExecutor(QueryMode);
impl CommandExecutor for QueryExecutor {
@@ -50,42 +65,72 @@ impl CommandExecutor for QueryExecutor {
&'a self,
sender: &'a CommandSender,
server: &'a crate::server::Server,
_args: &'a ConsumedArgs<'a>,
args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let clock_name = ResourceLocationArgumentConsumer::find_arg(args, ARG_CLOCK)
.unwrap_or(DEFAULT_CLOCK);
let mode = self.0;
// TODO: Maybe ask player for world, or get the current world
let worlds = server.worlds.load();
let world = worlds
.first()
.expect("There should always be at least one world");
let level_time = world.level_time.lock().await;
let curr_time = match mode {
QueryMode::DayTime => level_time.query_daytime(),
QueryMode::GameTime => level_time.query_gametime(),
QueryMode::Day => level_time.query_day(),
};
let bedrock_key = match mode {
QueryMode::DayTime => translation::bedrock::COMMANDS_TIME_QUERY_DAYTIME,
QueryMode::GameTime => translation::bedrock::COMMANDS_TIME_QUERY_GAMETIME,
QueryMode::Day => translation::bedrock::COMMANDS_TIME_QUERY_DAY,
};
match mode {
QueryMode::GameTime => {
let game_time = level_time.query_gametime();
sender
.send_message(TextComponent::translate(
translation::java::COMMANDS_TIME_QUERY_GAMETIME,
[TextComponent::text(game_time.to_string())],
))
.await;
Ok(wrap_time(game_time))
}
QueryMode::Time => {
let total_ticks = level_time.time_of_day;
sender
.send_message(TextComponent::translate(
translation::java::COMMANDS_TIME_QUERY_ABSOLUTE,
[
TextComponent::text(clock_name.to_string()),
TextComponent::text(total_ticks.to_string()),
],
))
.await;
Ok(wrap_time(total_ticks))
}
QueryMode::DayTime => {
let curr_time = level_time.query_daytime();
sender
.send_message(TextComponent::translate_cross(
translation::java::COMMANDS_TIME_QUERY,
bedrock_key,
translation::bedrock::COMMANDS_TIME_QUERY_DAYTIME,
[TextComponent::text(curr_time.to_string())],
))
.await;
Ok(curr_time as i32)
}
QueryMode::Day => {
let curr_time = level_time.query_day();
sender
.send_message(TextComponent::translate_cross(
translation::java::COMMANDS_TIME_QUERY,
translation::bedrock::COMMANDS_TIME_QUERY_DAY,
[TextComponent::text(curr_time.to_string())],
))
.await;
Ok(curr_time as i32)
}
}
})
}
}
struct ChangeExecutor(Mode);
struct ActionExecutor(Action);
impl CommandExecutor for ChangeExecutor {
impl CommandExecutor for ActionExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
@@ -93,8 +138,19 @@ impl CommandExecutor for ChangeExecutor {
args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let time_count = if let Mode::Set(Some(preset)) = &self.0 {
preset.to_ticks()
let clock_name = ResourceLocationArgumentConsumer::find_arg(args, ARG_CLOCK)
.unwrap_or(DEFAULT_CLOCK);
let action = self.0;
let worlds = server.worlds.load();
let world = worlds
.first()
.expect("There should always be at least one world");
let mut level_time = world.level_time.lock().await;
match action {
Action::Set(preset) => {
let time_count = if let Some(p) = preset {
p.to_ticks()
} else if let Ok(ticks) = TimeArgumentConsumer::find_arg(args, ARG_TIME) {
ticks
} else {
@@ -102,72 +158,174 @@ impl CommandExecutor for ChangeExecutor {
"Invalid time specified.",
)));
};
let mode = self.0;
// TODO: Maybe ask player for world, or get the current world
let worlds = server.worlds.load();
let world = worlds
.first()
.expect("There should always be at least one world");
let mut level_time = world.level_time.lock().await;
match mode {
Mode::Add => {
// add
level_time.add_time(time_count.into());
level_time.send_time(world).await;
let curr_time = level_time.query_daytime();
sender
.send_message(TextComponent::translate_cross(
translation::java::COMMANDS_TIME_SET,
translation::bedrock::COMMANDS_TIME_SET,
[TextComponent::text(curr_time.to_string())],
))
.await;
Ok(curr_time as i32)
}
Mode::Set(_) => {
// set
level_time.set_time(time_count.into());
level_time.send_time(world).await;
sender
.send_message(TextComponent::translate_cross(
translation::java::COMMANDS_TIME_SET,
translation::bedrock::COMMANDS_TIME_SET,
[TextComponent::text(time_count.to_string())],
.send_message(TextComponent::translate(
translation::java::COMMANDS_TIME_SET_ABSOLUTE,
[
TextComponent::text(clock_name.to_string()),
TextComponent::text(time_count.to_string()),
],
))
.await;
Ok(time_count)
}
Action::Add => {
let time_count = TimeArgumentConsumer::find_arg(args, ARG_TIME)?;
level_time.add_time(time_count.into());
level_time.send_time(world).await;
let total_ticks = level_time.time_of_day;
sender
.send_message(TextComponent::translate(
translation::java::COMMANDS_TIME_SET_ABSOLUTE,
[
TextComponent::text(clock_name.to_string()),
TextComponent::text(total_ticks.to_string()),
],
))
.await;
Ok(wrap_time(total_ticks))
}
Action::Pause => {
level_time.set_paused(true);
level_time.send_time(world).await;
sender
.send_message(TextComponent::translate(
translation::java::COMMANDS_TIME_PAUSE,
[TextComponent::text(clock_name.to_string())],
))
.await;
Ok(1)
}
Action::Resume => {
level_time.set_paused(false);
level_time.send_time(world).await;
sender
.send_message(TextComponent::translate(
translation::java::COMMANDS_TIME_RESUME,
[TextComponent::text(clock_name.to_string())],
))
.await;
Ok(1)
}
Action::Rate => {
let rate_res = BoundedNumArgumentConsumer::<f32>::find_arg(args, ARG_RATE)?;
let rate = match rate_res {
Ok(val) => val,
Err(err) => return Err(err.into()),
};
level_time.set_rate(rate);
level_time.send_time(world).await;
sender
.send_message(TextComponent::translate(
translation::java::COMMANDS_TIME_RATE,
[
TextComponent::text(clock_name.to_string()),
TextComponent::text(rate.to_string()),
],
))
.await;
Ok(1)
}
}
})
}
}
pub fn init_command_tree() -> CommandTree {
CommandTree::new(NAMES, DESCRIPTION)
let set_node = literal("set")
.then(literal("day").execute(ActionExecutor(Action::Set(Some(PresetTime::Day)))))
.then(literal("noon").execute(ActionExecutor(Action::Set(Some(PresetTime::Noon)))))
.then(literal("night").execute(ActionExecutor(Action::Set(Some(PresetTime::Night)))))
.then(literal("midnight").execute(ActionExecutor(Action::Set(Some(PresetTime::Midnight)))))
.then(
literal("add")
.then(argument(ARG_TIME, TimeArgumentConsumer).execute(ChangeExecutor(Mode::Add))),
argument(ARG_TIME, TimeArgumentConsumer::new())
.execute(ActionExecutor(Action::Set(None))),
);
let add_node = literal("add").then(
argument(ARG_TIME, TimeArgumentConsumer::min(i32::MIN))
.execute(ActionExecutor(Action::Add)),
);
let pause_node = literal("pause").execute(ActionExecutor(Action::Pause));
let resume_node = literal("resume").execute(ActionExecutor(Action::Resume));
let rate_node = literal("rate").then(
argument(
ARG_RATE,
BoundedNumArgumentConsumer::<f32>::new()
.min(1.0e-5)
.max(1000.0),
)
.execute(ActionExecutor(Action::Rate)),
);
let query_node = literal("query")
.then(literal("time").execute(QueryExecutor(QueryMode::Time)))
.then(literal("gametime").execute(QueryExecutor(QueryMode::GameTime)))
.then(literal("daytime").execute(QueryExecutor(QueryMode::DayTime)))
.then(literal("day").execute(QueryExecutor(QueryMode::Day)));
let of_clock_node = literal("of").then(
argument(ARG_CLOCK, ResourceLocationArgumentConsumer)
.then(
literal("set")
.then(
literal("day").execute(ActionExecutor(Action::Set(Some(PresetTime::Day)))),
)
.then(
literal("noon")
.execute(ActionExecutor(Action::Set(Some(PresetTime::Noon)))),
)
.then(
literal("night")
.execute(ActionExecutor(Action::Set(Some(PresetTime::Night)))),
)
.then(
literal("midnight")
.execute(ActionExecutor(Action::Set(Some(PresetTime::Midnight)))),
)
.then(
argument(ARG_TIME, TimeArgumentConsumer::new())
.execute(ActionExecutor(Action::Set(None))),
),
)
.then(
literal("add").then(
argument(ARG_TIME, TimeArgumentConsumer::min(i32::MIN))
.execute(ActionExecutor(Action::Add)),
),
)
.then(literal("pause").execute(ActionExecutor(Action::Pause)))
.then(literal("resume").execute(ActionExecutor(Action::Resume)))
.then(
literal("rate").then(
argument(
ARG_RATE,
BoundedNumArgumentConsumer::<f32>::new()
.min(1.0e-5)
.max(1000.0),
)
.execute(ActionExecutor(Action::Rate)),
),
)
.then(
literal("query")
.then(literal("daytime").execute(QueryExecutor(QueryMode::DayTime)))
.then(literal("time").execute(QueryExecutor(QueryMode::Time)))
.then(literal("gametime").execute(QueryExecutor(QueryMode::GameTime)))
.then(literal("daytime").execute(QueryExecutor(QueryMode::DayTime)))
.then(literal("day").execute(QueryExecutor(QueryMode::Day))),
)
.then(
literal("set")
.then(literal("day").execute(ChangeExecutor(Mode::Set(Some(PresetTime::Day)))))
.then(literal("noon").execute(ChangeExecutor(Mode::Set(Some(PresetTime::Noon)))))
.then(literal("night").execute(ChangeExecutor(Mode::Set(Some(PresetTime::Night)))))
.then(
literal("midnight")
.execute(ChangeExecutor(Mode::Set(Some(PresetTime::Midnight)))),
)
.then(
argument(ARG_TIME, TimeArgumentConsumer)
.execute(ChangeExecutor(Mode::Set(None))),
),
)
);
CommandTree::new(NAMES, DESCRIPTION)
.then(set_node)
.then(add_node)
.then(pause_node)
.then(resume_node)
.then(rate_node)
.then(query_node)
.then(of_clock_node)
}

View File

@@ -187,12 +187,15 @@ pub fn init_command_tree() -> CommandTree {
.execute(TitleExecutor(TitleMode::ActionBar)),
),
)
.then(literal("times").then(
argument(ARG_FADE_IN, TimeArgumentConsumer).then(
argument(ARG_STAY, TimeArgumentConsumer).then(
argument(ARG_FADE_OUT, TimeArgumentConsumer).execute(TimesTitleExecutor),
.then(
literal("times").then(
argument(ARG_FADE_IN, TimeArgumentConsumer::new()).then(
argument(ARG_STAY, TimeArgumentConsumer::new()).then(
argument(ARG_FADE_OUT, TimeArgumentConsumer::new())
.execute(TimesTitleExecutor),
),
),
),
),
)),
)
}

View File

@@ -0,0 +1,164 @@
use pumpkin_data::translation;
use pumpkin_util::text::TextComponent;
use crate::command::CommandResult;
use crate::command::args::{
FindArg, entity::EntityArgumentConsumer, hex_color::HexColorArgumentConsumer,
resource_location::ResourceLocationArgumentConsumer, team_color::TeamColorArgumentConsumer,
};
use crate::command::tree::builder::{argument, literal};
use crate::command::{CommandExecutor, CommandSender, ConsumedArgs, tree::CommandTree};
const NAMES: [&str; 1] = ["waypoint"];
const DESCRIPTION: &str = "List or modify waypoints.";
const ARG_WAYPOINT: &str = "waypoint";
const ARG_COLOR: &str = "color";
const ARG_STYLE: &str = "style";
struct ListExecutor;
impl CommandExecutor for ListExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
server: &'a crate::server::Server,
_args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let worlds = server.worlds.load();
let world = worlds
.first()
.expect("There should always be at least one world");
let dimension = world.dimension.minecraft_name.to_string();
// Currently no active waypoints are tracked in the level
sender
.send_message(TextComponent::translate(
translation::java::COMMANDS_WAYPOINT_LIST_EMPTY,
[TextComponent::text(dimension)],
))
.await;
Ok(0)
})
}
}
enum ColorAction {
Named,
Hex,
Reset,
}
struct ColorExecutor(ColorAction);
impl CommandExecutor for ColorExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
_server: &'a crate::server::Server,
args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let _waypoint_entity = EntityArgumentConsumer::find_arg(args, ARG_WAYPOINT)?;
match self.0 {
ColorAction::Named => {
let color = TeamColorArgumentConsumer::find_arg(args, ARG_COLOR)?;
sender
.send_message(TextComponent::translate(
translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR,
[TextComponent::text(color.name()).color_named(color)],
))
.await;
}
ColorAction::Hex => {
let color_val = HexColorArgumentConsumer::find_arg(args, ARG_COLOR)?;
let hex_str = format!("{:06X}", color_val & 0xFFFFFF);
sender
.send_message(TextComponent::translate(
translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR,
[TextComponent::text(hex_str)],
))
.await;
}
ColorAction::Reset => {
sender
.send_message(TextComponent::translate(
translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR_RESET,
[],
))
.await;
}
}
Ok(0)
})
}
}
enum StyleAction {
Set,
Reset,
}
struct StyleExecutor(StyleAction);
impl CommandExecutor for StyleExecutor {
fn execute<'a>(
&'a self,
sender: &'a CommandSender,
_server: &'a crate::server::Server,
args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> {
Box::pin(async move {
let _waypoint_entity = EntityArgumentConsumer::find_arg(args, ARG_WAYPOINT)?;
match self.0 {
StyleAction::Set => {
let _style = ResourceLocationArgumentConsumer::find_arg(args, ARG_STYLE)?;
}
StyleAction::Reset => {}
}
sender
.send_message(TextComponent::translate(
translation::java::COMMANDS_WAYPOINT_MODIFY_STYLE,
[],
))
.await;
Ok(0)
})
}
}
pub fn init_command_tree() -> CommandTree {
let color_node = literal("color")
.then(
argument(ARG_COLOR, TeamColorArgumentConsumer)
.execute(ColorExecutor(ColorAction::Named)),
)
.then(literal("hex").then(
argument(ARG_COLOR, HexColorArgumentConsumer).execute(ColorExecutor(ColorAction::Hex)),
))
.then(literal("reset").execute(ColorExecutor(ColorAction::Reset)));
let style_node = literal("style")
.then(literal("reset").execute(StyleExecutor(StyleAction::Reset)))
.then(
literal("set").then(
argument(ARG_STYLE, ResourceLocationArgumentConsumer)
.execute(StyleExecutor(StyleAction::Set)),
),
);
let modify_node = literal("modify").then(
argument(ARG_WAYPOINT, EntityArgumentConsumer)
.then(color_node)
.then(style_node),
);
CommandTree::new(NAMES, DESCRIPTION)
.then(literal("list").execute(ListExecutor))
.then(modify_node)
}

View File

@@ -96,7 +96,7 @@ pub fn init_command_tree() -> CommandTree {
.then(
literal("clear")
.then(
argument(ARG_DURATION, TimeArgumentConsumer).execute(Executor {
argument(ARG_DURATION, TimeArgumentConsumer::new()).execute(Executor {
mode: WeatherMode::Clear,
}),
)
@@ -107,7 +107,7 @@ pub fn init_command_tree() -> CommandTree {
.then(
literal("rain")
.then(
argument(ARG_DURATION, TimeArgumentConsumer).execute(Executor {
argument(ARG_DURATION, TimeArgumentConsumer::new()).execute(Executor {
mode: WeatherMode::Rain,
}),
)
@@ -118,7 +118,7 @@ pub fn init_command_tree() -> CommandTree {
.then(
literal("thunder")
.then(
argument(ARG_DURATION, TimeArgumentConsumer).execute(Executor {
argument(ARG_DURATION, TimeArgumentConsumer::new()).execute(Executor {
mode: WeatherMode::Thunder,
}),
)

View File

@@ -2548,27 +2548,39 @@ impl Player {
/// Sends the world time to only this player.
pub async fn send_time(&self, world: &World) {
let advance_time = {
let lock = world.level_info.load();
lock.game_rules.advance_time
};
let l_world = world.level_time.lock().await;
if let Some((custom_time, relative)) = self.per_player_time.load() {
let time_of_day = if relative {
let l_world = world.level_time.lock().await;
(l_world.time_of_day as u64 + custom_time) as i64
} else {
custom_time as i64
};
let l_world = world.level_time.lock().await;
let paused = l_world.paused || !advance_time;
let rate = if paused { 0.0 } else { l_world.rate };
self.client
.enqueue_packet_editioned(
&CUpdateTime::new(l_world.world_age, time_of_day, true),
&CUpdateTime::new_clock(
l_world.world_age,
0,
time_of_day,
l_world.partial_tick,
rate,
),
&CSetTime::new(time_of_day as _),
)
.await;
return;
}
let l_world = world.level_time.lock().await;
let (total_ticks, partial_tick, rate) = l_world.pack_network_state(advance_time);
self.client
.enqueue_packet_editioned(
&CUpdateTime::new(l_world.world_age, l_world.time_of_day, true),
&CUpdateTime::new_clock(l_world.world_age, 0, total_ticks, partial_tick, rate),
&CSetTime::new(l_world.query_daytime() as _),
)
.await;

View File

@@ -81,7 +81,9 @@ impl OwnedArg {
Arg::Enchantment(e) => Self::Enchantment(e),
Arg::EntityAnchor(a) => Self::EntityAnchor(*a),
Arg::Advancement(a) => Self::Advancement(a),
Arg::Slot(_, _) | Arg::Slots(_, _) => unreachable!(),
Arg::Slot(_, _) | Arg::Slots(_, _) | Arg::TeamColor(_) | Arg::HexColor(_) => {
unreachable!()
}
}
}
}

View File

@@ -598,7 +598,7 @@ impl pumpkin::plugin::command::HostCommandNode for PluginHostState {
ArgumentType::EntityAnchor => argument(name, EntityAnchorArgumentConsumer),
ArgumentType::Gamemode => argument(name, GamemodeArgumentConsumer),
ArgumentType::Difficulty => argument(name, DifficultyArgumentConsumer),
ArgumentType::Time(_) => argument(name, TimeArgumentConsumer),
ArgumentType::Time(min) => argument(name, TimeArgumentConsumer::min(min.unwrap_or(0))),
_ => {
return Err(wasmtime::Error::msg(format!(
"Unimplemented argument type: {arg_type:?}"

View File

@@ -1128,14 +1128,8 @@ impl World {
async fn tick_environment(&self) {
let (world_age, is_night, time_of_day) = {
let mut level_time = self.level_time.lock().await;
let (advance_time, advance_weather) = {
let lock = self.level_info.load();
(
lock.game_rules.advance_time,
lock.game_rules.advance_weather,
)
};
level_time.tick_time(advance_time, advance_weather);
let advance_time = self.level_info.load().game_rules.advance_time;
level_time.tick(advance_time);
// Auto-save logic
if level_time.world_age % 100 == 0 {

View File

@@ -2,10 +2,85 @@ use pumpkin_protocol::{bedrock::client::set_time::CSetTime, java::client::play::
use super::World;
#[derive(Clone, Debug, PartialEq)]
pub struct ClockInstance {
pub total_ticks: i64,
pub partial_tick: f32,
pub rate: f32,
pub paused: bool,
}
impl Default for ClockInstance {
fn default() -> Self {
Self::new()
}
}
impl ClockInstance {
#[must_use]
pub const fn new() -> Self {
Self {
total_ticks: 0,
partial_tick: 0.0,
rate: 1.0,
paused: false,
}
}
pub const fn load_from(
&mut self,
total_ticks: i64,
partial_tick: f32,
rate: f32,
paused: bool,
) {
self.total_ticks = total_ticks;
self.partial_tick = partial_tick;
self.rate = rate;
self.paused = paused;
}
pub fn tick(&mut self) {
if !self.paused {
self.partial_tick += self.rate;
let full_ticks = self.partial_tick.floor() as i32;
self.partial_tick -= full_ticks as f32;
self.total_ticks += full_ticks as i64;
}
}
pub const fn set_total_ticks(&mut self, total_ticks: i64) {
self.total_ticks = total_ticks;
self.partial_tick = 0.0;
}
pub fn add_ticks(&mut self, ticks: i64) {
self.total_ticks = (self.total_ticks + ticks).max(0);
}
pub const fn set_paused(&mut self, paused: bool) {
self.paused = paused;
}
pub const fn set_rate(&mut self, rate: f32) {
self.rate = rate;
}
#[must_use]
pub const fn pack_network_state(&self, advance_time: bool) -> (i64, f32, f32) {
let paused = self.paused || !advance_time;
let rate = if paused { 0.0 } else { self.rate };
(self.total_ticks, self.partial_tick, rate)
}
}
#[derive(Clone, Debug)]
pub struct LevelTime {
pub world_age: i64,
pub time_of_day: i64,
pub rain_time: i64,
pub world_age: i64,
pub partial_tick: f32,
pub rate: f32,
pub paused: bool,
}
impl Default for LevelTime {
@@ -18,19 +93,26 @@ impl LevelTime {
#[must_use]
pub const fn new() -> Self {
Self {
world_age: 0,
time_of_day: 0,
rain_time: 0,
world_age: 0,
partial_tick: 0.0,
rate: 1.0,
paused: false,
}
}
pub const fn tick_time(&mut self, advance_time: bool, advance_weather: bool) {
self.world_age += 1;
if advance_weather {
self.rain_time += 1;
pub const fn load_from(&mut self, time_of_day: i64, world_age: i64) {
self.time_of_day = time_of_day;
self.world_age = world_age;
}
if advance_time {
self.time_of_day += 1;
pub fn tick(&mut self, advance_time: bool) {
self.world_age += 1;
if advance_time && !self.paused {
self.partial_tick += self.rate;
let full_ticks = self.partial_tick.floor() as i32;
self.partial_tick -= full_ticks as f32;
self.time_of_day += full_ticks as i64;
}
}
@@ -40,20 +122,38 @@ impl LevelTime {
lock.game_rules.advance_time
};
let (total_ticks, partial_tick, rate) = self.pack_network_state(advance_time);
world
.broadcast_editioned(
&CUpdateTime::new(self.world_age, self.time_of_day, advance_time),
&CUpdateTime::new_clock(self.world_age, 0, total_ticks, partial_tick, rate),
&CSetTime::new(self.time_of_day as _), // TODO do we need to tell bedrock that time is frozen?
)
.await;
}
pub const fn add_time(&mut self, time: i64) {
self.time_of_day += time;
pub fn add_time(&mut self, time: i64) {
self.time_of_day = (self.time_of_day + time).max(0);
}
pub const fn set_time(&mut self, time: i64) {
self.time_of_day = time;
self.partial_tick = 0.0;
}
pub const fn set_paused(&mut self, paused: bool) {
self.paused = paused;
}
pub const fn set_rate(&mut self, rate: f32) {
self.rate = rate;
}
#[must_use]
pub const fn pack_network_state(&self, advance_time: bool) -> (i64, f32, f32) {
let paused = self.paused || !advance_time;
let rate = if paused { 0.0 } else { self.rate };
(self.time_of_day, self.partial_tick, rate)
}
#[must_use]
@@ -76,3 +176,56 @@ impl LevelTime {
(self.time_of_day % 24000) >= 12000 && (self.time_of_day % 24000) <= 23999
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clock_instance_ticking() {
let mut clock = ClockInstance::new();
assert_eq!(clock.total_ticks, 0);
assert_eq!(clock.partial_tick, 0.0);
assert_eq!(clock.rate, 1.0);
assert!(!clock.paused);
// Standard tick at rate 1.0
clock.tick();
assert_eq!(clock.total_ticks, 1);
assert_eq!(clock.partial_tick, 0.0);
// Half rate tick
clock.set_rate(0.5);
clock.tick();
assert_eq!(clock.total_ticks, 1);
assert_eq!(clock.partial_tick, 0.5);
clock.tick();
assert_eq!(clock.total_ticks, 2);
assert_eq!(clock.partial_tick, 0.0);
// Double rate tick
clock.set_rate(2.0);
clock.tick();
assert_eq!(clock.total_ticks, 4);
assert_eq!(clock.partial_tick, 0.0);
// Paused clock
clock.set_paused(true);
clock.tick();
assert_eq!(clock.total_ticks, 4);
}
#[test]
fn level_time_set_and_add() {
let mut time = LevelTime::new();
time.set_time(1000);
assert_eq!(time.time_of_day, 1000);
assert_eq!(time.partial_tick, 0.0);
time.add_time(500);
assert_eq!(time.time_of_day, 1500);
time.add_time(-2000);
assert_eq!(time.time_of_day, 0);
}
}