alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
root crates server src dedicated commands.rs
use std::sync::atomic::{AtomicBool, Ordering};
use common::item::{Item, ItemStack};
use common::protocol::Packet;
use super::admin::Admin;
use super::properties::Properties;
use crate::Server;
use crate::player::{after_word, java_split};
use crate::save::SaveError;
const HELP: &[&str] = &[
"Console commands:",
" help or ? shows this message",
" kick <player> removes a player from the server",
" ban <player> bans a player from the server",
" pardon <player> pardons a banned player so that they can connect again",
" ban-ip <ip> bans an IP address from the server",
" pardon-ip <ip> pardons a banned IP address so that they can connect again",
" op <player> turns a player into an op",
" deop <player> removes op status from a player",
" tp <player1> <player2> moves one player to the same location as another player",
" give <player> <id> [num] gives a player a resource",
" tell <player> <message> sends a private message to a player",
" stop gracefully stops the server",
" save-all forces a server-wide level save",
" save-off disables terrain saving (useful for backup scripts)",
" save-on re-enables terrain saving",
" list lists all currently connected players",
" say <message> broadcasts a message to all players",
" time <add|set> <amount> adds to or sets the world time (0-24000)",
];
/// `ConsoleCommandHandler` with the server state it changes.
pub(super) struct Commands<'a> {
pub server: &'a mut Server,
pub admin: &'a mut Admin,
pub properties: &'a mut Properties,
pub running: &'a AtomicBool,
}
impl Commands<'_> {
/// `ConsoleCommandHandler.handleCommand`, from the console or from the named operator.
pub fn handle(&mut self, source: Option<&str>, command: &str) -> Result<(), SaveError> {
let user = source.unwrap_or("CONSOLE");
let lower = command.to_lowercase();
let argument = after_word(command);
if lower.starts_with("help") || lower.starts_with('?') {
for line in HELP {
self.reply(source, line);
}
} else if lower.starts_with("list") {
let names: Vec<&str> = self.server.players().map(|p| p.name.as_str()).collect();
let message = format!("Connected players: {}", names.join(", "));
self.reply(source, &message);
} else if lower.starts_with("stop") {
self.notice(user, "Stopping the server..");
self.running.store(false, Ordering::Relaxed);
} else if lower.starts_with("save-all") {
self.notice(user, "Forcing save..");
self.server.save(true)?;
self.notice(user, "Save complete.");
} else if lower.starts_with("save-off") {
self.notice(user, "Disabling level saving..");
self.set_saving(false);
} else if lower.starts_with("save-on") {
self.notice(user, "Enabling level saving..");
self.set_saving(true);
} else if lower.starts_with("op ") {
self.admin.ops.add(argument);
self.notice(user, &format!("Opping {argument}"));
self.tell(argument, "§eYou are now op!");
} else if lower.starts_with("deop ") {
self.admin.ops.remove(argument);
self.tell(argument, "§eYou are no longer op!");
self.notice(user, &format!("De-opping {argument}"));
} else if lower.starts_with("ban-ip ") {
self.admin.banned_ips.add(argument);
self.notice(user, &format!("Banning ip {argument}"));
} else if lower.starts_with("pardon-ip ") {
self.admin.banned_ips.remove(argument);
self.notice(user, &format!("Pardoning ip {argument}"));
} else if lower.starts_with("ban ") {
self.admin.banned.add(argument);
self.notice(user, &format!("Banning {argument}"));
if let Some(player) = self.server.player_mut(argument) {
player.kick("Banned by admin");
}
} else if lower.starts_with("pardon ") {
self.admin.banned.remove(argument);
self.notice(user, &format!("Pardoning {argument}"));
} else if lower.starts_with("kick ") {
match self.server.player_mut(argument) {
Some(player) => {
player.kick("Kicked by admin");
let name = player.name.clone();
self.notice(user, &format!("Kicking {name}"));
}
None => self.reply(source, &format!("Can't find user {argument}. No kick.")),
}
} else if lower.starts_with("tp ") {
self.teleport(source, user, &java_split(command));
} else if lower.starts_with("give ") {
self.give(source, user, &java_split(command));
} else if lower.starts_with("time ") {
self.time(source, user, &java_split(command));
} else if lower.starts_with("say ") {
tracing::info!("[{user}] {argument}");
self.server
.broadcast(Packet::chat(format!("§d[Server] {argument}")));
} else if lower.starts_with("tell ") {
let words = java_split(command);
if words.len() >= 3 {
let message = after_word(argument);
tracing::info!("[{user}->{}] {message}", words[1]);
let message = format!("§7{user} whispers {message}");
tracing::info!("{message}");
if !self.tell(words[1], &message) {
self.reply(source, "There's no player by that name online.");
}
}
} else if lower.starts_with("whitelist ") {
self.whitelist(source, user, &java_split(command));
} else {
tracing::info!("Unknown console command. Type \"help\" for help.");
}
for player in self.server.players_mut() {
player.op = self.admin.ops.contains(&player.name);
}
Ok(())
}
/// `ICommandListener.log`.
fn reply(&mut self, source: Option<&str>, message: &str) {
match source {
None => tracing::info!("{message}"),
Some(name) => _ = self.tell(name, &format!("§7{message}")),
}
}
/// `ConsoleCommandHandler.sendNoticeToOps`.
fn notice(&mut self, user: &str, message: &str) {
let message = format!("{user}: {message}");
let packet = Packet::chat(format!("§7({message})"));
for player in self.server.players_mut() {
if self.admin.ops.contains(&player.name) {
player.send(packet.clone());
}
}
tracing::info!("{message}");
}
/// `ServerConfigurationManager.sendPacketToPlayer`.
fn tell(&mut self, name: &str, message: &str) -> bool {
self.server
.player_mut(name)
.map(|player| player.send(Packet::chat(message)))
.is_some()
}
fn set_saving(&mut self, saving: bool) {
for world in &mut self.server.worlds {
world.level.saving = saving;
}
}
fn teleport(&mut self, source: Option<&str>, user: &str, words: &[&str]) {
let &[_, from, to] = words else {
return self.reply(
source,
"Syntax error, please provice a source and a target.",
);
};
let Some((world, index)) = self.find(from) else {
return self.reply(source, &format!("Can't find user {from}. No tp."));
};
let Some((target_world, target)) = self.find(to) else {
return self.reply(source, &format!("Can't find user {to}. No tp."));
};
if world != target_world {
return self.reply(
source,
&format!("User {from} and {to} are in different dimensions. No tp."),
);
}
let crate::World { level, players, .. } = &mut self.server.worlds[world];
if let Some(entity) = players[target].entity(level) {
let (pos, yaw, pitch) = (entity.pos, entity.yaw, entity.pitch);
players[index].teleport(level, pos, yaw, pitch);
}
self.notice(user, &format!("Teleporting {from} to {to}."));
}
fn give(&mut self, source: Option<&str>, user: &str, words: &[&str]) {
if !matches!(words.len(), 3 | 4) {
return;
}
let name = words[1];
let Some((world, index)) = self.find(name) else {
return self.reply(source, &format!("Can't find user {name}"));
};
let item = words[2]
.parse()
.ok()
.map(Item)
.filter(|item| item.0 > 0 && item.properties().is_some());
let Some(item) = item else {
return self.reply(source, &format!("There's no item with id {}", words[2]));
};
let count = words
.get(3)
.map_or(1, |count| count.parse().unwrap_or(1))
.clamp(1, 64);
let crate::World { level, players, .. } = &mut self.server.worlds[world];
let player = &mut players[index];
let username = player.name.clone();
player.drop_stack(level, ItemStack::new(item, count, 0));
self.notice(user, &format!("Giving {username} some {}", item.0));
}
fn time(&mut self, source: Option<&str>, user: &str, words: &[&str]) {
let &[_, method, value] = words else {
return;
};
let Ok(value) = value.parse::<i32>() else {
return self.reply(source, &format!("Unable to convert time value, {value}"));
};
let add = method.eq_ignore_ascii_case("add");
if !add && !method.eq_ignore_ascii_case("set") {
return self.reply(source, "Unknown method, use either \"add\" or \"set\"");
}
for world in &mut self.server.worlds {
let base = if add { world.level.time } else { 0 };
world.level.advance_time(base + i64::from(value));
}
if add {
self.notice(user, &format!("Added {value} to time"));
} else {
self.notice(user, &format!("Set time to {value}"));
}
}
/// The world and index of the named player.
fn find(&self, name: &str) -> Option<(usize, usize)> {
let worlds = &self.server.worlds;
worlds.iter().enumerate().find_map(|(world, w)| {
w.players
.iter()
.position(|p| p.name.eq_ignore_ascii_case(name))
.map(|index| (world, index))
})
}
/// `ConsoleCommandHandler.handleWhitelist`.
fn whitelist(&mut self, source: Option<&str>, user: &str, words: &[&str]) {
let Some(action) = words.get(1).map(|word| word.to_lowercase()) else {
return;
};
match (action.as_str(), words.get(2)) {
("on" | "off", _) => {
let on = action == "on";
self.notice(user, &format!("Turned {action} white-listing"));
self.properties.set_bool("white-list", on);
self.admin.whitelist_enforced = on;
}
("list", _) => {
let names: String = self
.admin
.whitelist
.iter()
.map(|n| format!("{n} "))
.collect();
self.reply(source, &format!("White-listed players: {names}"));
}
("add", Some(name)) if words.len() == 3 => {
let name = name.to_lowercase();
self.admin.whitelist.add(&name);
self.notice(user, &format!("Added {name} to white-list"));
}
("remove", Some(name)) if words.len() == 3 => {
let name = name.to_lowercase();
self.admin.whitelist.remove(&name);
self.notice(user, &format!("Removed {name} from white-list"));
}
("reload", _) => {
self.admin.whitelist.reload("white-list");
self.notice(user, "Reloaded white-list from file");
}
_ => {}
}
}
}