From 1071fd0bca19af8f370650edc0c2843b7858cc68 Mon Sep 17 00:00:00 2001 From: Isse Date: Wed, 22 Nov 2023 22:40:56 +0100 Subject: [PATCH] entity targets --- common/src/cmd.rs | 51 +++++++++++++++++++++++++++--- server/src/cmd.rs | 78 +++++++++++++++++++++++++++++++++++----------- voxygen/src/cmd.rs | 2 ++ 3 files changed, 109 insertions(+), 22 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 8a4093c0c5..423a948b65 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -263,6 +263,39 @@ lazy_static! { }; } +pub enum EntityTarget { + Player(String), + RtsimNpc(u64), + Uid(crate::uid::Uid), +} + +impl FromStr for EntityTarget { + type Err = String; + + fn from_str(s: &str) -> Result { + // NOTE: `@` is an invalid character in usernames, so we can use it here. + if let Some((spec, data)) = s.split_once('@') { + match spec { + "rtsim" => Ok(EntityTarget::RtsimNpc(u64::from_str(data).map_err( + |_| format!("Expected a valid number after 'rtsim@' but found {data}."), + )?)), + "uid" => Ok(EntityTarget::Uid( + u64::from_str(data) + .map_err(|_| { + format!("Expected a valid number after 'uid@' but found {data}.") + })? + .into(), + )), + _ => Err(format!( + "Expected either 'rtsim' or 'uid' before '@' but found '{spec}'" + )), + } + } else { + Ok(EntityTarget::Player(s.to_string())) + } + } +} + // Please keep this sorted alphabetically :-) #[derive(Copy, Clone, strum::EnumIter)] pub enum ServerChatCommand { @@ -739,8 +772,8 @@ impl ServerChatCommand { Some(Admin), ), ServerChatCommand::Sudo => cmd( - vec![PlayerName(Required), SubCommand], - "Run command as if you were another player", + vec![EntityTarget(Required), SubCommand], + "Run command as if you were another entity", Some(Moderator), ), ServerChatCommand::Tell => cmd( @@ -760,10 +793,10 @@ impl ServerChatCommand { ), ServerChatCommand::Tp => cmd( vec![ - PlayerName(Optional), + EntityTarget(Optional), Boolean("Dismount from ship", "true".to_string(), Optional), ], - "Teleport to another player", + "Teleport to another entity", Some(Moderator), ), ServerChatCommand::RtsimTp => cmd( @@ -1001,6 +1034,7 @@ impl ServerChatCommand { .iter() .map(|arg| match arg { ArgumentSpec::PlayerName(_) => "{}", + ArgumentSpec::EntityTarget(_) => "{}", ArgumentSpec::SiteName(_) => "{/.*/}", ArgumentSpec::Float(_, _, _) => "{}", ArgumentSpec::Integer(_, _, _) => "{d}", @@ -1047,6 +1081,8 @@ pub enum Requirement { pub enum ArgumentSpec { /// The argument refers to a player by alias PlayerName(Requirement), + /// The arguments refers to an entity in some way. + EntityTarget(Requirement), // The argument refers to a site, by name. SiteName(Requirement), /// The argument is a float. The associated values are @@ -1090,6 +1126,13 @@ impl ArgumentSpec { "[player]".to_string() } }, + ArgumentSpec::EntityTarget(req) => { + if &Requirement::Required == req { + "".to_string() + } else { + "[entity]".to_string() + } + }, ArgumentSpec::SiteName(req) => { if &Requirement::Required == req { "".to_string() diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 678ce6608d..50ce0ec47e 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -21,7 +21,7 @@ use common::{ assets, calendar::Calendar, cmd::{ - AreaKind, KitSpec, ServerChatCommand, BUFF_PACK, BUFF_PARSER, ITEM_SPECS, + AreaKind, EntityTarget, KitSpec, ServerChatCommand, BUFF_PACK, BUFF_PARSER, ITEM_SPECS, KIT_MANIFEST_PATH, PRESET_MANIFEST_PATH, }, comp::{ @@ -1288,9 +1288,9 @@ fn handle_tp( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - let (player, dismount_volume) = parse_cmd_args!(args, String, bool); - let player = if let Some(alias) = player { - find_alias(server.state.ecs(), &alias)?.0 + let (entity_target, dismount_volume) = parse_cmd_args!(args, EntityTarget, bool); + let player = if let Some(entity_target) = entity_target { + get_entity_target(entity_target, server)? } else if client != target { client } else { @@ -3527,10 +3527,12 @@ fn handle_skill_point( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(a_skill_tree), Some(sp), a_alias) = parse_cmd_args!(args, String, u16, String) { + if let (Some(a_skill_tree), Some(sp), entity_target) = + parse_cmd_args!(args, String, u16, EntityTarget) + { let skill_tree = parse_skill_tree(&a_skill_tree)?; - let player = a_alias - .map(|alias| find_alias(server.state.ecs(), &alias).map(|(target, _)| target)) + let player = entity_target + .map(|entity_target| get_entity_target(entity_target, server)) .unwrap_or(Ok(target))?; if let Some(mut skill_set) = server @@ -3542,7 +3544,7 @@ fn handle_skill_point( skill_set.add_skill_points(skill_tree, sp); Ok(()) } else { - Err("Player has no stats!".into()) + Err("Entity has no stats!".into()) } } else { Err(Content::Plain(action.help_string())) @@ -3629,6 +3631,37 @@ fn handle_remove_lights( Ok(()) } +fn get_entity_target(entity_target: EntityTarget, server: &Server) -> CmdResult { + match entity_target { + EntityTarget::Player(alias) => Ok(find_alias(server.state.ecs(), &alias)?.0), + EntityTarget::RtsimNpc(id) => { + let (npc_id, _) = server + .state + .ecs() + .read_resource::() + .state() + .data() + .npcs + .iter() + .find(|(_, npc)| npc.uid == id) + .ok_or(Content::Plain(format!( + "Could not find rtsim npc with id {id}." + )))?; + server + .state() + .ecs() + .read_resource::() + .rtsim_entity(common::rtsim::RtSimEntity(npc_id)) + .ok_or(Content::Plain(format!("Npc with id {id} isn't loaded."))) + }, + EntityTarget::Uid(uid) => server + .state + .ecs() + .entity_from_uid(uid) + .ok_or(Content::Plain(format!("{uid:?} not found."))), + } +} + fn handle_sudo( server: &mut Server, client: EcsEntity, @@ -3636,23 +3669,32 @@ fn handle_sudo( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(player_alias), Some(cmd), cmd_args) = - parse_cmd_args!(args, String, String, ..Vec) + if let (Some(entity_target), Some(cmd), cmd_args) = + parse_cmd_args!(args, EntityTarget, String, ..Vec) { if let Ok(action) = cmd.parse() { - let (player, player_uuid) = find_alias(server.state.ecs(), &player_alias)?; + let entity = get_entity_target(entity_target, server)?; let client_uuid = uuid(server, client, "client")?; - verify_above_role( - server, - (client, client_uuid), - (player, player_uuid), - "Cannot sudo players with roles higher than your own.", - )?; + + // If the entity target is a player check if client has authority to sudo it. + { + let players = server.state.ecs().read_storage::(); + if let Some(player) = players.get(entity) { + let player_uuid = player.uuid(); + drop(players); + verify_above_role( + server, + (client, client_uuid), + (entity, player_uuid), + "Cannot sudo players with roles higher than your own.", + )?; + } + } // TODO: consider making this into a tail call or loop (to avoid the potential // stack overflow, although it's less of a risk coming from only mods and // admins). - do_command(server, client, player, cmd_args, &action) + do_command(server, client, entity, cmd_args, &action) } else { Err(Content::localized("command-unknown")) } diff --git a/voxygen/src/cmd.rs b/voxygen/src/cmd.rs index 7b052ed9dc..de2ea09374 100644 --- a/voxygen/src/cmd.rs +++ b/voxygen/src/cmd.rs @@ -78,6 +78,7 @@ impl ClientChatCommand { .iter() .map(|arg| match arg { ArgumentSpec::PlayerName(_) => "{}", + ArgumentSpec::EntityTarget(_) => "{}", ArgumentSpec::SiteName(_) => "{/.*/}", ArgumentSpec::Float(_, _, _) => "{}", ArgumentSpec::Integer(_, _, _) => "{d}", @@ -383,6 +384,7 @@ impl TabComplete for ArgumentSpec { fn complete(&self, part: &str, client: &Client) -> Vec { match self { ArgumentSpec::PlayerName(_) => complete_player(part, client), + ArgumentSpec::EntityTarget(_) => complete_player(part, client), ArgumentSpec::SiteName(_) => complete_site(part, client), ArgumentSpec::Float(_, x, _) => { if part.is_empty() {