Base implementation of /group /faction /say /region

This commit is contained in:
CapsizeGlimmer 2020-06-01 00:33:39 -04:00 committed by Forest Anderson
parent 8e67782a49
commit 702a21302c
10 changed files with 378 additions and 75 deletions

View File

@ -25,7 +25,7 @@ impl TabComplete for ArgumentSpec {
},
ArgumentSpec::Any(_, _) => vec![],
ArgumentSpec::Command(_) => complete_command(part),
ArgumentSpec::Message => complete_player(part, &client),
ArgumentSpec::Message(_) => complete_player(part, &client),
ArgumentSpec::SubCommand => complete_command(part),
ArgumentSpec::Enum(_, strings, _) => strings
.iter()
@ -92,10 +92,25 @@ pub fn complete(line: &str, client: &Client) -> Vec<String> {
if i == 0 {
// Completing chat command name
complete_command(word)
} else if let Ok(cmd) = cmd.parse::<ChatCommand>() {
if let Some(arg) = cmd.data().args.get(i - 1) {
// Complete ith argument
arg.complete(word, &client)
} else {
if let Ok(cmd) = cmd.parse::<ChatCommand>() {
if let Some(arg) = cmd.data().args.get(i - 1) {
// Complete ith argument
arg.complete(word, &client)
} else {
// Complete past the last argument
match cmd.data().args.last() {
Some(ArgumentSpec::SubCommand) => {
if let Some(index) = nth_word(line, cmd.data().args.len()) {
complete(&line[index..], &client)
} else {
vec![]
}
},
Some(ArgumentSpec::Message(_)) => complete_player(word, &client),
_ => vec![], // End of command. Nothing to complete
}
}
} else {
// Complete past the last argument
match cmd.data().args.last() {

View File

@ -9,13 +9,16 @@ pub struct ChatCommandData {
pub args: Vec<ArgumentSpec>,
/// A one-line message that explains what the command does
pub description: &'static str,
/// A boolean that is used to check whether the command requires
/// administrator permissions or not.
pub needs_admin: bool,
/// Whether the command requires administrator permissions.
pub needs_admin: IsAdminOnly,
}
impl ChatCommandData {
pub fn new(args: Vec<ArgumentSpec>, description: &'static str, needs_admin: bool) -> Self {
pub fn new(
args: Vec<ArgumentSpec>,
description: &'static str,
needs_admin: IsAdminOnly,
) -> Self {
Self {
args,
description,
@ -33,9 +36,11 @@ pub enum ChatCommand {
Debug,
DebugColumn,
Explosion,
Faction,
GiveExp,
GiveItem,
Goto,
Group,
Health,
Help,
Jump,
@ -46,7 +51,9 @@ pub enum ChatCommand {
Motd,
Object,
Players,
Region,
RemoveLights,
Say,
SetLevel,
SetMotd,
Spawn,
@ -56,6 +63,7 @@ pub enum ChatCommand {
Tp,
Version,
Waypoint,
World,
}
// Thank you for keeping this sorted alphabetically :-)
@ -66,9 +74,11 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[
ChatCommand::Debug,
ChatCommand::DebugColumn,
ChatCommand::Explosion,
ChatCommand::Faction,
ChatCommand::GiveExp,
ChatCommand::GiveItem,
ChatCommand::Goto,
ChatCommand::Group,
ChatCommand::Health,
ChatCommand::Help,
ChatCommand::Jump,
@ -79,7 +89,9 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[
ChatCommand::Motd,
ChatCommand::Object,
ChatCommand::Players,
ChatCommand::Region,
ChatCommand::RemoveLights,
ChatCommand::Say,
ChatCommand::SetLevel,
ChatCommand::SetMotd,
ChatCommand::Spawn,
@ -89,6 +101,7 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[
ChatCommand::Tp,
ChatCommand::Version,
ChatCommand::Waypoint,
ChatCommand::World,
];
lazy_static! {
@ -141,31 +154,37 @@ lazy_static! {
impl ChatCommand {
pub fn data(&self) -> ChatCommandData {
use ArgumentSpec::*;
use IsAdminOnly::*;
use Requirement::*;
let cmd = ChatCommandData::new;
match self {
ChatCommand::Adminify => cmd(
vec![PlayerName(Required)],
"Temporarily gives a player admin permissions or removes them",
true,
Admin,
),
ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", false),
ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", true),
ChatCommand::Debug => cmd(vec![], "Place all debug items into your pack.", true),
ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", NoAdmin),
ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", Admin),
ChatCommand::Debug => cmd(vec![], "Place all debug items into your pack.", Admin),
ChatCommand::DebugColumn => cmd(
vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
"Prints some debug information about a column",
false,
NoAdmin,
),
ChatCommand::Explosion => cmd(
vec![Float("radius", 5.0, Required)],
"Explodes the ground around you",
true,
Admin,
),
ChatCommand::Faction => cmd(
vec![Message(Optional)],
"Send messages to your faction",
NoAdmin,
),
ChatCommand::GiveExp => cmd(
vec![Integer("amount", 50, Required)],
"Give experience to yourself",
true,
Admin,
),
ChatCommand::GiveItem => cmd(
vec![
@ -173,7 +192,7 @@ impl ChatCommand {
Integer("num", 1, Optional),
],
"Give yourself some items",
true,
Admin,
),
ChatCommand::Goto => cmd(
vec![
@ -182,17 +201,22 @@ impl ChatCommand {
Float("z", 0.0, Required),
],
"Teleport to a position",
true,
Admin,
),
ChatCommand::Group => cmd(
vec![Message(Optional)],
"Send messages to your group",
NoAdmin,
),
ChatCommand::Health => cmd(
vec![Integer("hp", 100, Required)],
"Set your current health",
true,
Admin,
),
ChatCommand::Help => ChatCommandData::new(
vec![Command(Optional)],
"Display information about commands",
false,
NoAdmin,
),
ChatCommand::Jump => cmd(
vec![
@ -201,10 +225,10 @@ impl ChatCommand {
Float("z", 0.0, Required),
],
"Offset your current position",
true,
Admin,
),
ChatCommand::Kill => cmd(vec![], "Kill yourself", false),
ChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", true),
ChatCommand::Kill => cmd(vec![], "Kill yourself", NoAdmin),
ChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", Admin),
ChatCommand::Lantern => cmd(
vec![
Float("strength", 5.0, Required),
@ -213,7 +237,7 @@ impl ChatCommand {
Float("b", 1.0, Optional),
],
"Change your lantern's strength and color",
true,
Admin,
),
ChatCommand::Light => cmd(
vec![
@ -226,24 +250,34 @@ impl ChatCommand {
Float("strength", 5.0, Optional),
],
"Spawn entity with light",
true,
Admin,
),
ChatCommand::Motd => cmd(vec![Message], "View the server description", false),
ChatCommand::Object => cmd(
vec![Enum("object", OBJECTS.clone(), Required)],
"Spawn an object",
true,
Admin,
),
ChatCommand::Players => cmd(vec![], "Lists players currently online", false),
ChatCommand::Players => cmd(vec![], "Lists players currently online", NoAdmin),
ChatCommand::RemoveLights => cmd(
vec![Float("radius", 20.0, Optional)],
"Removes all lights spawned by players",
true,
Admin,
),
ChatCommand::Region => cmd(
vec![Message(Optional)],
"Send messages to everyone in your region of the world",
NoAdmin,
),
ChatCommand::Say => cmd(
vec![Message(Optional)],
"Send messages to everyone within shouting distance",
NoAdmin,
),
ChatCommand::SetLevel => cmd(
vec![Integer("level", 10, Required)],
"Set player Level",
true,
Admin,
),
ChatCommand::SetMotd => cmd(vec![Message], "Set the server description", true),
ChatCommand::Spawn => cmd(
@ -253,32 +287,37 @@ impl ChatCommand {
Integer("amount", 1, Optional),
],
"Spawn a test entity",
true,
Admin,
),
ChatCommand::Sudo => cmd(
vec![PlayerName(Required), SubCommand],
"Run command as if you were another player",
true,
Admin,
),
ChatCommand::Tell => cmd(
vec![PlayerName(Required), Message],
vec![PlayerName(Required), Message(Optional)],
"Send a message to another player",
false,
NoAdmin,
),
ChatCommand::Time => cmd(
vec![Enum("time", TIMES.clone(), Optional)],
"Set the time of day",
true,
Admin,
),
ChatCommand::Tp => cmd(
vec![PlayerName(Optional)],
"Teleport to another player",
true,
Admin,
),
ChatCommand::Version => cmd(vec![], "Prints server version", false),
ChatCommand::Version => cmd(vec![], "Prints server version", NoAdmin),
ChatCommand::Waypoint => {
cmd(vec![], "Set your waypoint to your current position", true)
cmd(vec![], "Set your waypoint to your current position", Admin)
},
ChatCommand::World => cmd(
vec![Message(Optional)],
"Send messages to everyone on the server",
NoAdmin,
),
}
}
@ -291,9 +330,11 @@ impl ChatCommand {
ChatCommand::Debug => "debug",
ChatCommand::DebugColumn => "debug_column",
ChatCommand::Explosion => "explosion",
ChatCommand::Faction => "faction",
ChatCommand::GiveExp => "give_exp",
ChatCommand::GiveItem => "give_item",
ChatCommand::Goto => "goto",
ChatCommand::Group => "group",
ChatCommand::Health => "health",
ChatCommand::Help => "help",
ChatCommand::Jump => "jump",
@ -304,7 +345,9 @@ impl ChatCommand {
ChatCommand::Motd => "motd",
ChatCommand::Object => "object",
ChatCommand::Players => "players",
ChatCommand::Region => "region",
ChatCommand::RemoveLights => "remove_lights",
ChatCommand::Say => "say",
ChatCommand::SetLevel => "set_level",
ChatCommand::SetMotd => "set_motd",
ChatCommand::Spawn => "spawn",
@ -314,6 +357,7 @@ impl ChatCommand {
ChatCommand::Tp => "tp",
ChatCommand::Version => "version",
ChatCommand::Waypoint => "waypoint",
ChatCommand::World => "world",
}
}
@ -329,7 +373,7 @@ impl ChatCommand {
/// A boolean that is used to check whether the command requires
/// administrator permissions or not.
pub fn needs_admin(&self) -> bool { self.data().needs_admin }
pub fn needs_admin(&self) -> bool { *self.data().needs_admin }
/// Returns a format string for parsing arguments with scan_fmt
pub fn arg_fmt(&self) -> String {
@ -342,7 +386,7 @@ impl ChatCommand {
ArgumentSpec::Integer(_, _, _) => "{d}",
ArgumentSpec::Any(_, _) => "{}",
ArgumentSpec::Command(_) => "{}",
ArgumentSpec::Message => "{/.*/}",
ArgumentSpec::Message(_) => "{/.*/}",
ArgumentSpec::SubCommand => "{} {/.*/}",
ArgumentSpec::Enum(_, _, _) => "{}", // TODO
})
@ -369,6 +413,20 @@ impl FromStr for ChatCommand {
}
}
pub enum IsAdminOnly {
Admin,
NoAdmin,
}
impl Deref for IsAdminOnly {
type Target = bool;
fn deref(&self) -> &bool {
match self {
IsAdminOnly::Admin => &true,
IsAdminOnly::NoAdmin => &false,
}
}
}
pub enum Requirement {
Required,
Optional,
@ -404,7 +462,7 @@ pub enum ArgumentSpec {
Command(Requirement),
/// This is the final argument, consuming all characters until the end of
/// input.
Message,
Message(Requirement),
/// This command is followed by another command (such as in /sudo)
SubCommand,
/// The argument is likely an enum. The associated values are
@ -452,7 +510,13 @@ impl ArgumentSpec {
"[[/]command]".to_string()
}
},
ArgumentSpec::Message => "<message>".to_string(),
ArgumentSpec::Message(req) => {
if **req {
"<message>".to_string()
} else {
"<message>".to_string()
}
},
ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
ArgumentSpec::Enum(label, _, req) => {
if **req {

39
common/src/comp/chat.rs Normal file
View File

@ -0,0 +1,39 @@
use specs::{Component, Entity};
use specs_idvs::IDVStorage;
/// Limit chat to a subset of players
pub enum ChatMode {
/// Private message to another player (by entity)
Tell(Entity),
/// Talk to players within shouting distance
Say,
/// Talk to players in your region of the world
Region,
/// Talk to your current group of players
Group,
/// Talk to your faction
Faction,
/// Talk to every player on the server
World,
}
impl Component for ChatMode {
type Storage = IDVStorage<Self>;
}
/// Player groups are useful when forming raiding parties and coordinating
/// gameplay.
///
/// Groups are currently just an associated String (the group's name)
pub struct Group(String);
impl Component for Group {
type Storage = IDVStorage<Self>;
}
/// Player factions are used to coordinate pvp vs hostile factions or segment
/// chat from the world
///
/// Factions are currently just an associated String (the faction's name)
pub struct Faction(String);
impl Component for Faction {
type Storage = IDVStorage<Self>;
}

View File

@ -3,6 +3,7 @@ mod admin;
pub mod agent;
mod body;
mod character_state;
mod chat;
mod controller;
mod energy;
mod inputs;
@ -24,6 +25,7 @@ pub use body::{
humanoid, object, quadruped_medium, quadruped_small, AllBodies, Body, BodyData,
};
pub use character_state::{Attacking, CharacterState, StateUpdate};
pub use chat::{ChatMode, Faction, Group};
pub use controller::{
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, Input, InventoryManip,
MountState, Mounting,

View File

@ -69,14 +69,22 @@ pub mod net;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChatType {
/// Tell all players something (such as players connecting or alias changes)
Broadcast,
Chat,
GameUpdate,
Private,
Chat, // TODO Is this still needed?
GameUpdate, // TODO What is this?
Private, // TODO What is this?
/// One-on-one chat
Tell,
/// Chat with nearby players
Say,
/// Group chat
Group,
/// Factional chat
Faction,
Meta,
Meta, // TODO What is this?
/// Inform players that someone died
Kill,
/// Regional chat
Region,
}

View File

@ -113,6 +113,7 @@ impl From<AuthClientError> for RegisterError {
}
impl ServerMsg {
// TODO is this needed?
pub fn chat(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::Chat,
@ -141,6 +142,7 @@ impl ServerMsg {
}
}
// TODO is this needed?
pub fn private(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::Private,
@ -148,6 +150,41 @@ impl ServerMsg {
}
}
pub fn group(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::Group,
message,
}
}
pub fn region(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::Region,
message,
}
}
pub fn say(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::Say,
message,
}
}
pub fn faction(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::Faction,
message,
}
}
pub fn world(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::Chat,
message,
}
}
pub fn kill(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::Kill,

View File

@ -155,6 +155,7 @@ impl State {
ecs.register::<comp::Projectile>();
ecs.register::<comp::Attacking>();
ecs.register::<comp::ItemDrop>();
ecs.register::<comp::ChatMode>();
// Register synced resources used by the ECS.
ecs.insert(TimeOfDay(0.0));

View File

@ -32,7 +32,7 @@ impl ChatCommandExt for ChatCommand {
#[allow(clippy::needless_return)] // TODO: Pending review in #587
fn execute(&self, server: &mut Server, entity: EcsEntity, args: String) {
let cmd_data = self.data();
if cmd_data.needs_admin && !server.entity_is_admin(entity) {
if *cmd_data.needs_admin && !server.entity_is_admin(entity) {
server.notify_client(
entity,
ServerMsg::private(format!(
@ -68,9 +68,11 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler {
ChatCommand::Debug => handle_debug,
ChatCommand::DebugColumn => handle_debug_column,
ChatCommand::Explosion => handle_explosion,
ChatCommand::Faction => handle_faction,
ChatCommand::GiveExp => handle_give_exp,
ChatCommand::GiveItem => handle_give_item,
ChatCommand::Goto => handle_goto,
ChatCommand::Group => handle_group,
ChatCommand::Health => handle_health,
ChatCommand::Help => handle_help,
ChatCommand::Jump => handle_jump,
@ -81,7 +83,9 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler {
ChatCommand::Motd => handle_motd,
ChatCommand::Object => handle_object,
ChatCommand::Players => handle_players,
ChatCommand::Region => handle_region,
ChatCommand::RemoveLights => handle_remove_lights,
ChatCommand::Say => handle_say,
ChatCommand::SetLevel => handle_set_level,
ChatCommand::SetMotd => handle_set_motd,
ChatCommand::Spawn => handle_spawn,
@ -91,6 +95,7 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler {
ChatCommand::Tp => handle_tp,
ChatCommand::Version => handle_version,
ChatCommand::Waypoint => handle_waypoint,
ChatCommand::World => handle_world,
}
}
@ -967,6 +972,7 @@ fn handle_tell(
action: &ChatCommand,
) {
if client != target {
// This happens when [ab]using /sudo
server.notify_client(
client,
ServerMsg::tell(String::from("It's rude to impersonate people")),
@ -981,38 +987,32 @@ fn handle_tell(
.find(|(_, player)| player.alias == alias)
.map(|(entity, _)| entity)
{
if player != target {
if msg.len() > 1 {
if let Some(name) = ecs
.read_storage::<comp::Player>()
.get(target)
.map(|s| s.alias.clone())
{
server.notify_client(
player,
ServerMsg::tell(format!("[{}] tells:{}", name, msg)),
);
server.notify_client(
client,
ServerMsg::tell(format!("To [{}]:{}", alias, msg)),
);
} else {
server.notify_client(
client,
ServerMsg::private(String::from("Failed to send message.")),
);
}
} else {
server.notify_client(
client,
ServerMsg::private(format!("[{}] wants to talk to you.", alias)),
);
}
} else {
if player == client {
server.notify_client(
client,
ServerMsg::private(format!("You can't /tell yourself.")),
);
return;
}
if msg.is_empty() {
server.notify_client(
client,
ServerMsg::private(format!("[{}] wants to talk to you.", alias)),
);
return;
}
if let Some(name) = ecs
.read_storage::<comp::Player>()
.get(client)
.map(|s| s.alias.clone())
{
server.notify_client(player, ServerMsg::tell(format!("[{}] tells:{}", name, msg)));
server.notify_client(client, ServerMsg::tell(format!("To [{}]:{}", alias, msg)));
} else {
server.notify_client(
client,
ServerMsg::private(String::from("Failed to send message.")),
);
}
} else {
server.notify_client(
@ -1028,6 +1028,141 @@ fn handle_tell(
}
}
fn handle_faction(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
msg: String,
_action: &ChatCommand,
) {
if client != target {
// This happens when [ab]using /sudo
server.notify_client(
client,
ServerMsg::tell(String::from("It's rude to impersonate people")),
);
return;
}
let _ = server
.state
.ecs()
.write_storage()
.insert(client, comp::ChatMode::Faction);
if !msg.is_empty() {
server
.state
.notify_registered_clients(ServerMsg::faction(msg.to_string()));
}
}
fn handle_group(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
msg: String,
_action: &ChatCommand,
) {
if client != target {
// This happens when [ab]using /sudo
server.notify_client(
client,
ServerMsg::tell(String::from("It's rude to impersonate people")),
);
return;
}
let _ = server
.state
.ecs()
.write_storage()
.insert(client, comp::ChatMode::Group);
if !msg.is_empty() {
server
.state
.notify_registered_clients(ServerMsg::group(msg.to_string()));
}
}
fn handle_region(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
msg: String,
_action: &ChatCommand,
) {
if client != target {
// This happens when [ab]using /sudo
server.notify_client(
client,
ServerMsg::tell(String::from("It's rude to impersonate people")),
);
return;
}
let _ = server
.state
.ecs()
.write_storage()
.insert(client, comp::ChatMode::Region);
if !msg.is_empty() {
server
.state
.notify_registered_clients(ServerMsg::region(msg.to_string()));
}
}
fn handle_say(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
msg: String,
_action: &ChatCommand,
) {
if client != target {
// This happens when [ab]using /sudo
server.notify_client(
client,
ServerMsg::tell(String::from("It's rude to impersonate people")),
);
return;
}
let _ = server
.state
.ecs()
.write_storage()
.insert(client, comp::ChatMode::Say);
if !msg.is_empty() {
server
.state
.notify_registered_clients(ServerMsg::say(msg.to_string()));
}
}
fn handle_world(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
msg: String,
_action: &ChatCommand,
) {
if client != target {
// This happens when [ab]using /sudo
server.notify_client(
client,
ServerMsg::tell(String::from("It's rude to impersonate people")),
);
return;
}
let _ = server
.state
.ecs()
.write_storage()
.insert(client, comp::ChatMode::World);
if !msg.is_empty() {
server
.state
.notify_registered_clients(ServerMsg::world(msg.to_string()));
}
}
#[cfg(not(feature = "worldgen"))]
fn handle_debug_column(
server: &mut Server,

View File

@ -1,6 +1,6 @@
use super::{
img_ids::Imgs, BROADCAST_COLOR, FACTION_COLOR, GAME_UPDATE_COLOR, GROUP_COLOR, KILL_COLOR,
META_COLOR, PRIVATE_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR,
META_COLOR, PRIVATE_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR,
};
use crate::{ui::fonts::ConrodVoxygenFonts, GlobalState};
use client::{cmd, Client, Event as ClientEvent};
@ -324,6 +324,7 @@ impl<'a> Widget for Chat<'a> {
ChatType::Say => SAY_COLOR,
ChatType::Group => GROUP_COLOR,
ChatType::Faction => FACTION_COLOR,
ChatType::Region => REGION_COLOR,
ChatType::Kill => KILL_COLOR,
};
let text = Text::new(&message)

View File

@ -82,6 +82,7 @@ const GAME_UPDATE_COLOR: Color = Color::Rgba(1.0, 1.0, 0.0, 1.0);
const SAY_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
const GROUP_COLOR: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0);
const FACTION_COLOR: Color = Color::Rgba(0.24, 1.0, 0.48, 1.0);
const REGION_COLOR: Color = Color::Rgba(1.0, 1.0, 0.0, 1.0);
const KILL_COLOR: Color = Color::Rgba(1.0, 0.17, 0.17, 1.0);
// UI Color-Theme