Translate almost everything

The command output in Console/RCON is a bit broken now, i will fix that tomorrow, But in-game it works great
This commit is contained in:
Alexander Medvedev 2025-01-22 23:31:56 +01:00
parent 1a3b7e0ad6
commit e7f1420da3
20 changed files with 309 additions and 218 deletions

View File

@ -42,14 +42,15 @@ impl TextComponent {
}
}
pub fn translate<P>(key: P, with: Vec<Cow<'static, str>>) -> Self
pub fn translate<K, W>(key: K, with: W) -> Self
where
P: Into<Cow<'static, str>>,
K: Into<Cow<'static, str>>,
W: Into<Vec<Cow<'static, str>>>,
{
Self {
content: TextContent::Translate {
translate: key.into(),
with,
with: with.into(),
},
style: Style::default(),
extra: vec![],
@ -61,9 +62,19 @@ impl TextComponent {
self
}
pub fn get_text(self) -> String {
match self.content {
TextContent::Text { text } => text.into_owned(),
TextContent::Translate { translate, with: _ } => translate.into_owned(),
TextContent::EntityNames {
selector,
separator: _,
} => selector.into_owned(),
TextContent::Keybind { keybind } => keybind.into_owned(),
}
}
pub fn to_pretty_console(self) -> String {
let style = self.style;
let color = style.color;
let mut text = match self.content {
TextContent::Text { text } => text.into_owned(),
TextContent::Translate { translate, with: _ } => translate.into_owned(),
@ -73,6 +84,8 @@ impl TextComponent {
} => selector.into_owned(),
TextContent::Keybind { keybind } => keybind.into_owned(),
};
let style = self.style;
let color = style.color;
if let Some(color) = color {
text = color.console_color(&text).to_string();
}

View File

@ -51,12 +51,8 @@ impl CommandExecutor for DeopExecutor {
.await;
let player_name = &player.gameprofile.name;
let message = format!("Revoked {player_name}'s server operator status.");
let msg = TextComponent::text(message);
let msg = TextComponent::translate("commands.deop.success", [player_name.clone().into()]);
sender.send_message(msg).await;
player
.send_system_message(&TextComponent::text("You are no longer a server operator."))
.await;
Ok(())
}

View File

@ -144,10 +144,10 @@ impl CommandExecutor for SetblockExecutor {
};
sender
.send_message(TextComponent::text(format!(
"Placed {} blocks of {} from {from} to {to}",
placed_blocks, block.name
)))
.send_message(TextComponent::translate(
"commands.fill.success",
[placed_blocks.to_string().into()],
))
.await;
Ok(())

View File

@ -38,18 +38,15 @@ impl CommandExecutor for GamemodeTargetSelf {
};
if let Player(target) = sender {
if target.gamemode.load() == gamemode {
target
.send_system_message(&TextComponent::text(format!(
"You already in {gamemode:?} gamemode"
)))
.await;
} else {
if target.gamemode.load() != gamemode {
target.set_gamemode(gamemode).await;
let gamemode_string = format!("{gamemode:?}");
// TODO: translate gamemode
target
.send_system_message(&TextComponent::text(format!(
"Game mode was set to {gamemode:?}"
)))
.send_system_message(&TextComponent::translate(
"commands.gamemode.success.self",
[gamemode_string.into()],
))
.await;
}
Ok(())
@ -79,23 +76,25 @@ impl CommandExecutor for GamemodeTargetPlayer {
let target_count = targets.len();
for target in targets {
if target.gamemode.load() == gamemode {
if target_count == 1 {
sender
.send_message(TextComponent::text(format!(
"{} is already in {:?} gamemode",
target.gameprofile.name, gamemode
)))
.await;
}
} else {
if target.gamemode.load() != gamemode {
target.set_gamemode(gamemode).await;
let gamemode_string = format!("{gamemode:?}");
// TODO: translate gamemode
target
.send_system_message(&TextComponent::translate(
"gameMode.changed",
[gamemode_string.clone().into()],
))
.await;
if target_count == 1 {
sender
.send_message(TextComponent::text(format!(
"{}'s Game mode was set to {:?}",
target.gameprofile.name, gamemode
)))
.send_message(TextComponent::translate(
"commands.gamemode.success.other",
[
target.gameprofile.name.clone().into(),
gamemode_string.into(),
],
))
.await;
}
}

View File

@ -55,19 +55,26 @@ impl CommandExecutor for GiveExecutor {
target.give_items(item, item_count as u32).await;
}
sender
.send_message(TextComponent::text(match targets {
[target] => format!(
"Gave {item_count} {} to {}",
item_name, target.gameprofile.name
),
_ => format!(
"Gave {item_count} {} to {} players",
item_name,
targets.len()
),
}))
.await;
let msg = if targets.len() == 1 {
TextComponent::translate(
"commands.give.success.single",
[
item_count.to_string().into(),
item_name.to_string().into(),
targets[0].gameprofile.name.to_string().into(),
],
)
} else {
TextComponent::translate(
"commands.give.success.single",
[
item_count.to_string().into(),
item_name.to_string().into(),
targets.len().to_string().into(),
],
)
};
sender.send_message(msg).await;
Ok(())
}

View File

@ -29,21 +29,17 @@ impl CommandExecutor for KickExecutor {
return Err(InvalidConsumption(Some(ARG_TARGET.into())));
};
let target_count = targets.len();
for target in targets {
target
.kick(TextComponent::text("Kicked by an operator"))
.kick(TextComponent::translate(
"multiplayer.disconnect.kicked",
[],
))
.await;
}
let msg = if target_count == 1 {
TextComponent::text("Player has been kicked.")
} else {
TextComponent::text(format!("{target_count} players have been kicked."))
};
let name = &target.gameprofile.name;
let msg = TextComponent::text(format!("Kicked: {name}"));
sender.send_message(msg.color_named(NamedColor::Blue)).await;
}
Ok(())
}

View File

@ -29,15 +29,19 @@ impl CommandExecutor for KillExecutor {
};
let target_count = targets.len();
let mut name = String::new();
for target in targets {
target.living_entity.kill().await;
name.clone_from(&target.gameprofile.name);
}
let msg = if target_count == 1 {
TextComponent::text("Entity has been killed.")
TextComponent::translate("commands.kill.success.single", [name.into()])
} else {
TextComponent::text(format!("{target_count} entities have been killed."))
TextComponent::translate(
"commands.kill.success.multiple",
[target_count.to_string().into()],
)
};
sender.send_message(msg.color_named(NamedColor::Blue)).await;

View File

@ -27,18 +27,16 @@ impl CommandExecutor for ListExecutor {
) -> Result<(), CommandError> {
let players: Vec<Arc<Player>> = server.get_all_players().await;
let message = if players.is_empty() {
"There are no players online.".to_owned()
} else {
format!(
"There are {} of a max of {} players online: {}",
players.len(),
BASIC_CONFIG.max_players,
get_player_names(players)
)
};
sender.send_message(TextComponent::text(message)).await;
sender
.send_message(TextComponent::translate(
"commands.list.players",
[
players.len().to_string().into(),
BASIC_CONFIG.max_players.to_string().into(),
get_player_names(players).into(),
],
))
.await;
Ok(())
}

View File

@ -39,6 +39,13 @@ impl CommandExecutor for OpExecutor {
.op_permission_level
.min(sender.permission_lvl());
if player.permission_lvl.load() == new_level {
sender
.send_message(TextComponent::translate("commands.op.failed", []))
.await;
return Ok(());
}
if let Some(op) = config
.ops
.iter_mut()
@ -62,11 +69,11 @@ impl CommandExecutor for OpExecutor {
.await;
let player_name = &player.gameprofile.name;
let message = format!("Made {player_name} a server operator.");
let msg = TextComponent::text(message);
sender.send_message(msg).await;
player
.send_system_message(&TextComponent::text("You are now a server operator."))
sender
.send_message(TextComponent::translate(
"commands.op.success",
[player_name.clone().into()],
))
.await;
Ok(())

View File

@ -1,5 +1,4 @@
use async_trait::async_trait;
use pumpkin_util::text::color::NamedColor;
use pumpkin_util::text::TextComponent;
use crate::command::args::arg_block::BlockArgumentConsumer;
@ -67,9 +66,16 @@ impl CommandExecutor for SetblockExecutor {
sender
.send_message(if success {
TextComponent::text(format!("Placed block {} at {pos}", block.name,))
TextComponent::translate(
"commands.setblock.success",
[
pos.0.x.to_string().into(),
pos.0.y.to_string().into(),
pos.0.z.to_string().into(),
],
)
} else {
TextComponent::text(format!("Kept block at {pos}")).color_named(NamedColor::Red)
TextComponent::translate("commands.setblock.failed", [])
})
.await;

View File

@ -21,7 +21,9 @@ impl CommandExecutor for StopExecutor {
_args: &ConsumedArgs<'a>,
) -> Result<(), CommandError> {
sender
.send_message(TextComponent::text("Stopping Server").color_named(NamedColor::Red))
.send_message(
TextComponent::translate("commands.stop.stopping", []).color_named(NamedColor::Red),
)
.await;
// TODO: Gracefully stop

View File

@ -62,19 +62,19 @@ impl CommandExecutor for TimeQueryExecutor {
let msg = match mode {
QueryMode::DayTime => {
let curr_time = level_time.query_daytime();
format!("Daytime is: {curr_time}")
TextComponent::translate("commands.time.query", [curr_time.to_string().into()])
}
QueryMode::GameTime => {
let curr_time = level_time.query_gametime();
format!("Gametime is: {curr_time}")
TextComponent::translate("commands.time.query", [curr_time.to_string().into()])
}
QueryMode::Day => {
let curr_time = level_time.query_day();
format!("Day is: {curr_time}")
TextComponent::translate("commands.time.query", [curr_time.to_string().into()])
}
};
sender.send_message(TextComponent::text(msg)).await;
sender.send_message(msg).await;
Ok(())
}
}
@ -124,17 +124,17 @@ impl CommandExecutor for TimeChangeExecutor {
level_time.add_time(time_count.into());
level_time.send_time(world).await;
let curr_time = level_time.query_daytime();
format!("Added {time_count} time for result: {curr_time}")
TextComponent::translate("commands.time.add", [curr_time.to_string().into()])
}
Mode::Set(_) => {
// set
level_time.set_time(time_count.into());
level_time.send_time(world).await;
format!("Changed time to: {time_count}")
TextComponent::translate("commands.time.set", [time_count.to_string().into()])
}
};
sender.send_message(TextComponent::text(msg)).await;
sender.send_message(msg).await;
Ok(())
}
}

View File

@ -25,6 +25,8 @@ const NAMES: [&str; 1] = ["worldborder"];
const DESCRIPTION: &str = "Worldborder command.";
const NOTHING_CHANGED_EXCEPTION: &str = "commands.worldborder.set.failed.nochange";
fn distance_consumer() -> BoundedNumArgumentConsumer<f64> {
BoundedNumArgumentConsumer::new().min(0.0).name("distance")
}
@ -65,9 +67,10 @@ impl CommandExecutor for WorldborderGetExecutor {
let diameter = border.new_diameter.round() as i32;
sender
.send_message(TextComponent::text(format!(
"The world border is currently {diameter} block(s) wide"
)))
.send_message(TextComponent::translate(
"commands.worldborder.get",
[diameter.to_string().into()],
))
.await;
Ok(())
}
@ -105,17 +108,19 @@ impl CommandExecutor for WorldborderSetExecutor {
if (distance - border.new_diameter).abs() < f64::EPSILON {
sender
.send_message(
TextComponent::text("Nothing changed. The world border is already that size")
TextComponent::translate(NOTHING_CHANGED_EXCEPTION, [])
.color(Color::Named(NamedColor::Red)),
)
.await;
return Ok(());
}
let dist = format!("{distance:.1}");
sender
.send_message(TextComponent::text(format!(
"Set the world border to {distance:.1} block(s) wide"
)))
.send_message(TextComponent::translate(
"commands.worldborder.set.immediate",
[dist.into()],
))
.await;
border.set_diameter(world, distance, None).await;
Ok(())
@ -167,22 +172,28 @@ impl CommandExecutor for WorldborderSetTimeExecutor {
std::cmp::Ordering::Equal => {
sender
.send_message(
TextComponent::text(
"Nothing changed. The world border is already that size",
)
TextComponent::translate(NOTHING_CHANGED_EXCEPTION, [])
.color(Color::Named(NamedColor::Red)),
)
.await;
return Ok(());
}
std::cmp::Ordering::Less => {
sender.send_message(TextComponent::text(format!("Shrinking the world border to {distance:.2} blocks wide over {time} second(s)"))).await;
let dist = format!("{distance:.1}");
sender
.send_message(TextComponent::translate(
"commands.worldborder.set.shrink",
[dist.into(), time.to_string().into()],
))
.await;
}
std::cmp::Ordering::Greater => {
let dist = format!("{distance:.1}");
sender
.send_message(TextComponent::text(format!(
"Growing the world border to {distance:.2} blocks wide over {time} seconds"
)))
.send_message(TextComponent::translate(
"commands.worldborder.set.grow",
[dist.into(), time.to_string().into()],
))
.await;
}
}
@ -226,7 +237,7 @@ impl CommandExecutor for WorldborderAddExecutor {
if distance == 0.0 {
sender
.send_message(
TextComponent::text("Nothing changed. The world border is already that size")
TextComponent::translate(NOTHING_CHANGED_EXCEPTION, [])
.color(Color::Named(NamedColor::Red)),
)
.await;
@ -235,10 +246,12 @@ impl CommandExecutor for WorldborderAddExecutor {
let distance = border.new_diameter + distance;
let dist = format!("{distance:.1}");
sender
.send_message(TextComponent::text(format!(
"Set the world border to {distance:.1} block(s) wide"
)))
.send_message(TextComponent::translate(
"commands.worldborder.set.immediate",
[dist.into()],
))
.await;
border.set_diameter(world, distance, None).await;
Ok(())
@ -292,22 +305,28 @@ impl CommandExecutor for WorldborderAddTimeExecutor {
std::cmp::Ordering::Equal => {
sender
.send_message(
TextComponent::text(
"Nothing changed. The world border is already that size",
)
TextComponent::translate(NOTHING_CHANGED_EXCEPTION, [])
.color(Color::Named(NamedColor::Red)),
)
.await;
return Ok(());
}
std::cmp::Ordering::Less => {
sender.send_message(TextComponent::text(format!("Shrinking the world border to {distance:.2} blocks wide over {time} second(s)"))).await;
let dist = format!("{distance:.1}");
sender
.send_message(TextComponent::translate(
"commands.worldborder.set.shrink",
[dist.into(), time.to_string().into()],
))
.await;
}
std::cmp::Ordering::Greater => {
let dist = format!("{distance:.1}");
sender
.send_message(TextComponent::text(format!(
"Growing the world border to {distance:.2} blocks wide over {time} seconds"
)))
.send_message(TextComponent::translate(
"commands.worldborder.set.grow",
[dist.into(), time.to_string().into()],
))
.await;
}
}
@ -338,9 +357,10 @@ impl CommandExecutor for WorldborderCenterExecutor {
let Vector2 { x, z } = Position2DArgumentConsumer.find_arg_default_name(args)?;
sender
.send_message(TextComponent::text(format!(
"Set the center of world border to {x:.2}, {z:.2}"
)))
.send_message(TextComponent::translate(
"commands.worldborder.center.success",
[format!("{x:.2}").into(), format!("{z:.2}").into()],
))
.await;
border.set_center(world, x, z).await;
Ok(())
@ -379,19 +399,19 @@ impl CommandExecutor for WorldborderDamageAmountExecutor {
if (damage_per_block - border.damage_per_block).abs() < f32::EPSILON {
sender
.send_message(
TextComponent::text(
"Nothing changed. The world border damage is already that amount",
)
TextComponent::translate("commands.worldborder.damage.amount.failed", [])
.color(Color::Named(NamedColor::Red)),
)
.await;
return Ok(());
}
let damage = format!("{damage_per_block:.2}");
sender
.send_message(TextComponent::text(format!(
"Set the world border damage to {damage_per_block:.2} per block each second"
)))
.send_message(TextComponent::translate(
"commands.worldborder.damage.amount.success",
[damage.into()],
))
.await;
border.damage_per_block = damage_per_block;
Ok(())
@ -430,19 +450,19 @@ impl CommandExecutor for WorldborderDamageBufferExecutor {
if (buffer - border.buffer).abs() < f32::EPSILON {
sender
.send_message(
TextComponent::text(
"Nothing changed. The world border damage buffer is already that distance",
)
TextComponent::translate("commands.worldborder.damage.buffer.failed", [])
.color(Color::Named(NamedColor::Red)),
)
.await;
return Ok(());
}
let buf = format!("{buffer:.2}");
sender
.send_message(TextComponent::text(format!(
"Set the world border damage buffer to {buffer:.2} block(s)"
)))
.send_message(TextComponent::translate(
"commands.worldborder.damage.buffer.success",
[buf.into()],
))
.await;
border.buffer = buffer;
Ok(())
@ -481,9 +501,7 @@ impl CommandExecutor for WorldborderWarningDistanceExecutor {
if distance == border.warning_blocks {
sender
.send_message(
TextComponent::text(
"Nothing changed. The world border warning is already that distance",
)
TextComponent::translate("commands.worldborder.warning.distance.failed", [])
.color(Color::Named(NamedColor::Red)),
)
.await;
@ -491,9 +509,10 @@ impl CommandExecutor for WorldborderWarningDistanceExecutor {
}
sender
.send_message(TextComponent::text(format!(
"Set the world border warning distance to {distance} block(s)"
)))
.send_message(TextComponent::translate(
"commands.worldborder.warning.distance.success",
[distance.to_string().into()],
))
.await;
border.set_warning_distance(world, distance).await;
Ok(())
@ -532,9 +551,7 @@ impl CommandExecutor for WorldborderWarningTimeExecutor {
if time == border.warning_time {
sender
.send_message(
TextComponent::text(
"Nothing changed. The world border warning is already that amount of time",
)
TextComponent::translate("commands.worldborder.warning.time.failed", [])
.color(Color::Named(NamedColor::Red)),
)
.await;
@ -542,9 +559,10 @@ impl CommandExecutor for WorldborderWarningTimeExecutor {
}
sender
.send_message(TextComponent::text(format!(
"Set the world border warning time to {time} second(s)"
)))
.send_message(TextComponent::translate(
"commands.worldborder.warning.time.success",
[time.to_string().into()],
))
.await;
border.set_warning_delay(world, time).await;
Ok(())

View File

@ -419,7 +419,8 @@ impl Player {
.wait_for_keep_alive
.load(std::sync::atomic::Ordering::Relaxed)
{
self.kick(TextComponent::text("Timeout")).await;
self.kick(TextComponent::translate("disconnect.timeout", []))
.await;
return;
}
self.wait_for_keep_alive
@ -697,8 +698,12 @@ impl Player {
}
pub async fn send_system_message(&self, text: &TextComponent) {
self.send_system_message_raw(text, false).await;
}
pub async fn send_system_message_raw(&self, text: &TextComponent, overlay: bool) {
self.client
.send_packet(&CSystemChatMessage::new(text, false))
.send_packet(&CSystemChatMessage::new(text, overlay))
.await;
}
}

View File

@ -248,7 +248,7 @@ impl Client {
let mut enc = self.enc.lock().await;
if let Err(error) = enc.append_packet(packet) {
self.kick(&error.to_string()).await;
self.kick(&TextComponent::text(error.to_string())).await;
return;
}
@ -336,7 +336,7 @@ impl Client {
i32::from(packet.id),
error
);
self.kick(&text).await;
self.kick(&TextComponent::text(text)).await;
};
}
}
@ -564,23 +564,20 @@ impl Client {
/// # Arguments
///
/// * `reason`: A string describing the reason for kicking the client.
pub async fn kick(&self, reason: &str) {
log::info!("Kicking Client id {} for {}", self.id, reason);
pub async fn kick(&self, reason: &TextComponent) {
let text = reason.clone().get_text();
log::info!("Kicking Client id {} for {}", self.id, &text);
let result = match self.connection_state.load() {
ConnectionState::Login => {
// TextComponent implements Serialze and writes in bytes instead of String, thats the reasib we only use content
self.try_send_packet(&CLoginDisconnect::new(
&serde_json::to_string_pretty(&reason).unwrap_or_else(|_| String::new()),
&serde_json::to_string(&reason.content).unwrap_or_else(|_| String::new()),
))
.await
}
ConnectionState::Config => self.try_send_packet(&CConfigDisconnect::new(reason)).await,
ConnectionState::Config => self.try_send_packet(&CConfigDisconnect::new(&text)).await,
// This way players get kicked when players using client functions (e.g. poll, send_packet)
ConnectionState::Play => {
self.try_send_packet(&CPlayDisconnect::new(&TextComponent::text(
reason.to_owned(),
)))
.await
}
ConnectionState::Play => self.try_send_packet(&CPlayDisconnect::new(reason)).await,
_ => {
log::warn!("Can't kick in {:?} State", self.connection_state);
Ok(())

View File

@ -14,6 +14,7 @@ use pumpkin_protocol::{
},
ConnectionState,
};
use pumpkin_util::text::TextComponent;
impl Client {
pub async fn handle_client_information_config(
@ -22,7 +23,9 @@ impl Client {
) {
log::debug!("Handling client settings");
if client_information.view_distance <= 0 {
self.kick("Cannot have zero or negative view distance!")
self.kick(&TextComponent::text(
"Cannot have zero or negative view distance!",
))
.await;
return;
}
@ -44,7 +47,8 @@ impl Client {
server_listing: client_information.server_listing,
});
} else {
self.kick("Invalid hand or chat type").await;
self.kick(&TextComponent::text("Invalid hand or chat type"))
.await;
}
}
@ -58,7 +62,7 @@ impl Client {
log::debug!("got a client brand");
match str::from_utf8(&plugin_message.data) {
Ok(brand) => *self.brand.lock().await = Some(brand.to_string()),
Err(e) => self.kick(&e.to_string()).await,
Err(e) => self.kick(&TextComponent::text(e.to_string())).await,
}
}
}

View File

@ -1,6 +1,7 @@
use std::num::NonZeroI32;
use pumpkin_protocol::{server::handshake::SHandShake, ConnectionState, CURRENT_MC_PROTOCOL};
use pumpkin_util::text::TextComponent;
use crate::{net::Client, server::CURRENT_MC_VERSION};
@ -17,11 +18,19 @@ impl Client {
let protocol = version;
match protocol.cmp(&NonZeroI32::from(CURRENT_MC_PROTOCOL).get()) {
std::cmp::Ordering::Less => {
self.kick(&format!("Client outdated ({protocol}), Server uses Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL}")).await;
self.kick(&TextComponent::translate(
"multiplayer.disconnect.outdated_client",
[CURRENT_MC_VERSION.to_string().into()],
))
.await;
}
std::cmp::Ordering::Equal => {}
std::cmp::Ordering::Greater => {
self.kick(&format!("Server outdated, Server uses Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL}")).await;
self.kick(&TextComponent::translate(
"multiplayer.disconnect.incompatible",
[CURRENT_MC_VERSION.to_string().into()],
))
.await;
}
}
}

View File

@ -93,13 +93,17 @@ impl Client {
// TODO: If client is an operator or otherwise suitable elevated permissions, allow client to bypass this requirement.
let max_players = BASIC_CONFIG.max_players;
if max_players > 0 && server.get_player_count().await >= max_players as usize {
self.kick("The server is currently full, please try again later")
self.kick(&TextComponent::translate(
"multiplayer.disconnect.server_full",
[],
))
.await;
return;
}
if !is_valid_player_name(&login_start.name) {
self.kick("Invalid characters in username").await;
self.kick(&TextComponent::text("Invalid characters in username"))
.await;
return;
}
// default game profile, when no online mode
@ -122,7 +126,7 @@ impl Client {
self.finish_login(&profile).await;
*gameprofile = Some(profile);
}
Err(error) => self.kick(&error.to_string()).await,
Err(error) => self.kick(&TextComponent::text(error.to_string())).await,
}
}
} else {
@ -165,14 +169,14 @@ impl Client {
let shared_secret = server.decrypt(&encryption_response.shared_secret).unwrap();
if let Err(error) = self.set_encryption(Some(&shared_secret)).await {
self.kick(&error.to_string()).await;
self.kick(&TextComponent::text(error.to_string())).await;
return;
}
let mut gameprofile = self.gameprofile.lock().await;
let Some(profile) = gameprofile.as_mut() else {
self.kick("No Game profile").await;
self.kick(&TextComponent::text("No Game profile")).await;
return;
};
@ -184,7 +188,7 @@ impl Client {
{
Ok(new_profile) => *profile = new_profile,
Err(e) => {
self.kick(&e.to_string()).await;
self.kick(&TextComponent::text(e.to_string())).await;
return;
}
}
@ -193,14 +197,21 @@ impl Client {
// Don't allow duplicate UUIDs
if let Some(online_player) = &server.get_player_by_uuid(profile.id).await {
log::debug!("Player (IP '{}', username '{}') tried to log in with the same UUID ('{}') as an online player (IP '{}', username '{}')", &self.address.lock().await, &profile.name, &profile.id, &online_player.client.address.lock().await, &online_player.gameprofile.name);
self.kick("You are already connected to this server").await;
self.kick(&TextComponent::translate(
"multiplayer.disconnect.duplicate_login",
[],
))
.await;
return;
}
// Don't allow a duplicate username
if let Some(online_player) = &server.get_player_by_name(&profile.name).await {
log::debug!("A player (IP '{}', attempted username '{}') tried to log in with the same username as an online player (UUID '{}', IP '{}', username '{}')", &self.address.lock().await, &profile.name, &profile.id, &online_player.client.address.lock().await, &online_player.gameprofile.name);
self.kick("A player with this username is already connected")
self.kick(&TextComponent::translate(
"multiplayer.disconnect.duplicate_login",
[],
))
.await;
return;
}
@ -300,7 +311,7 @@ impl Client {
*self.gameprofile.lock().await = Some(profile);
*address = new_address;
}
Err(error) => self.kick(&error.to_string()).await,
Err(error) => self.kick(&TextComponent::text(error.to_string())).await,
}
}
}

View File

@ -15,9 +15,7 @@ use pumpkin_config::ADVANCED_CONFIG;
use pumpkin_data::entity::EntityType;
use pumpkin_inventory::player::PlayerInventory;
use pumpkin_inventory::InventoryError;
use pumpkin_protocol::client::play::{
CSetContainerSlot, CSetHeldItem, CSpawnEntity, CSystemChatMessage,
};
use pumpkin_protocol::client::play::{CSetContainerSlot, CSetHeldItem, CSpawnEntity};
use pumpkin_protocol::codec::slot::Slot;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::server::play::SCookieResponse as SPCookieResponse;
@ -81,11 +79,8 @@ impl PumpkinError for BlockPlacingError {
fn severity(&self) -> log::Level {
match self {
Self::BlockOutOfReach
| Self::BlockOutOfWorld
| Self::InvalidBlockFace
| Self::InvalidGamemode
| Self::NoBaseBlock => log::Level::Warn,
Self::BlockOutOfWorld | Self::InvalidGamemode | Self::NoBaseBlock => log::Level::Trace,
Self::BlockOutOfReach | Self::InvalidBlockFace => log::Level::Warn,
Self::InventoryInvalid => log::Level::Error,
}
}
@ -141,7 +136,11 @@ impl Player {
// y = feet Y
let position = packet.position;
if position.x.is_nan() || position.y.is_nan() || position.z.is_nan() {
self.kick(TextComponent::text("Invalid movement")).await;
self.kick(TextComponent::translate(
"multiplayer.disconnect.invalid_player_movement",
[],
))
.await;
return;
}
let position = Vector3::new(
@ -199,15 +198,20 @@ impl Player {
}
// y = feet Y
let position = packet.position;
if position.x.is_nan() || position.y.is_nan() || position.z.is_nan() {
self.kick(TextComponent::text("Invalid movement")).await;
if position.x.is_nan()
|| position.y.is_nan()
|| position.z.is_nan()
|| packet.yaw.is_infinite()
|| packet.pitch.is_infinite()
{
self.kick(TextComponent::translate(
"multiplayer.disconnect.invalid_player_movement",
[],
))
.await;
return;
}
if packet.yaw.is_infinite() || packet.pitch.is_infinite() {
self.kick(TextComponent::text("Invalid rotation")).await;
return;
}
let position = Vector3::new(
Self::clamp_horizontal(position.x),
Self::clamp_vertical(position.y),
@ -280,7 +284,11 @@ impl Player {
return;
}
if !rotation.yaw.is_finite() || !rotation.pitch.is_finite() {
self.kick(TextComponent::text("Invalid rotation")).await;
self.kick(TextComponent::translate(
"multiplayer.disconnect.invalid_player_movement",
[],
))
.await;
return;
}
let entity = &self.living_entity.entity;
@ -528,7 +536,10 @@ impl Player {
}
if message.chars().any(|c| c == '§' || c < ' ' || c == '\x7F') {
self.kick(TextComponent::text("Illegal characters in chat"))
self.kick(TextComponent::translate(
"multiplayer.disconnect.illegal_characters",
[],
))
.await;
return;
}
@ -705,7 +716,10 @@ impl Player {
self.entity_id(),
entity_id.0
);
self.kick(TextComponent::text("Interacted with invalid entity id"))
self.kick(TextComponent::translate(
"multiplayer.disconnect.invalid_entity_attacked",
[],
))
.await;
return;
};
@ -1134,15 +1148,14 @@ impl Player {
//check max world build height
if location.0.y + face.to_offset().y >= WORLD_MAX_Y.into() {
self.client
.send_packet(&CSystemChatMessage::new(
self.send_system_message_raw(
&TextComponent::translate(
"build.tooHigh",
vec![(WORLD_MAX_Y - 1).to_string().into()],
)
.color_named(NamedColor::Red),
true,
))
)
.await;
self.client
.send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence))

View File

@ -745,8 +745,11 @@ impl World {
// Handle join message
// TODO: Config
let msg_txt = format!("{} joined the game.", player.gameprofile.name.as_str());
let msg_comp = TextComponent::text(msg_txt).color_named(NamedColor::Yellow);
let msg_comp = TextComponent::translate(
"multiplayer.player.joined",
[player.gameprofile.name.clone().into()],
)
.color_named(NamedColor::Yellow);
for player in current_players.values() {
player.send_system_message(&msg_comp).await;
}
@ -787,12 +790,15 @@ impl World {
// Send disconnect message / quit message to players in the same world
// TODO: Config
let disconn_msg_txt = format!("{} left the game.", player.gameprofile.name.as_str());
let disconn_msg_cmp = TextComponent::text(disconn_msg_txt).color_named(NamedColor::Yellow);
let disconn_msg = TextComponent::translate(
"multiplayer.player.left",
[player.gameprofile.name.clone().into()],
)
.color_named(NamedColor::Yellow);
for player in self.current_players.lock().await.values() {
player.send_system_message(&disconn_msg_cmp).await;
player.send_system_message(&disconn_msg).await;
}
log::info!("{}", disconn_msg_cmp.to_pretty_console());
log::info!("{}", disconn_msg.to_pretty_console());
}
/// Adds a living entity to the world.