Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
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
Download file

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");
            }
            _ => {}
        }
    }
}