mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat: add waypoint command
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
132
crates/pumpkin/src/command/args/hex_color.rs
Normal file
132
crates/pumpkin/src/command/args/hex_color.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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`]
|
||||
|
||||
130
crates/pumpkin/src/command/args/team_color.rs
Normal file
130
crates/pumpkin/src/command/args/team_color.rs
Normal 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(()));
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
164
crates/pumpkin/src/command/commands/waypoint.rs
Normal file
164
crates/pumpkin/src/command/commands/waypoint.rs
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:?}"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user