From 1071fd0bca19af8f370650edc0c2843b7858cc68 Mon Sep 17 00:00:00 2001 From: Isse Date: Wed, 22 Nov 2023 22:40:56 +0100 Subject: [PATCH 01/11] 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() { From 34edfdb7a2395e4b03a12e637f6b6cd28d19607f Mon Sep 17 00:00:00 2001 From: Isse Date: Wed, 22 Nov 2023 23:10:32 +0100 Subject: [PATCH 02/11] link related commands --- common/src/cmd.rs | 24 ++++++++++ server/src/cmd.rs | 115 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 423a948b65..1e7f708e8c 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -316,7 +316,9 @@ pub enum ServerChatCommand { DebugColumn, DebugWays, DeleteLocation, + DestroyTethers, DisconnectAllPlayers, + Dismount, DropAll, Dummy, Explosion, @@ -345,6 +347,7 @@ pub enum ServerChatCommand { MakeSprite, MakeVolume, Motd, + Mount, Object, PermitBuild, Players, @@ -373,6 +376,7 @@ pub enum ServerChatCommand { Spawn, Sudo, Tell, + Tether, Time, TimeScale, Tp, @@ -895,6 +899,22 @@ impl ServerChatCommand { ServerChatCommand::RepairEquipment => { cmd(vec![], "Repairs all equipped items", Some(Admin)) }, + ServerChatCommand::Tether => cmd( + vec![EntityTarget(Required)], + "Tether another entity to yourself", + Some(Admin), + ), + ServerChatCommand::DestroyTethers => { + cmd(vec![], "Destroy all tethers connected to you", Some(Admin)) + }, + ServerChatCommand::Mount => { + cmd(vec![EntityTarget(Required)], "Mount an entity", Some(Admin)) + }, + ServerChatCommand::Dismount => cmd( + vec![EntityTarget(Required)], + "Dismount if you are riding, or dismount anything riding you", + Some(Admin), + ), } } @@ -985,6 +1005,10 @@ impl ServerChatCommand { ServerChatCommand::Lightning => "lightning", ServerChatCommand::Scale => "scale", ServerChatCommand::RepairEquipment => "repair_equipment", + ServerChatCommand::Tether => "tether", + ServerChatCommand::DestroyTethers => "destroy_tethers", + ServerChatCommand::Mount => "mount", + ServerChatCommand::Dismount => "dismount", } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 50ce0ec47e..e5eed4ad67 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -208,6 +208,10 @@ fn do_command( ServerChatCommand::Lightning => handle_lightning, ServerChatCommand::Scale => handle_scale, ServerChatCommand::RepairEquipment => handle_repair_equipment, + ServerChatCommand::Tether => handle_tether, + ServerChatCommand::DestroyTethers => handle_destroy_tethers, + ServerChatCommand::Mount => handle_mount, + ServerChatCommand::Dismount => handle_dismount, }; handler(server, client, target, args, cmd) @@ -4539,3 +4543,114 @@ fn handle_repair_equipment( Err(Content::Plain(action.help_string())) } } + +fn handle_tether( + server: &mut Server, + _client: EcsEntity, + target: EcsEntity, + args: Vec, + action: &ServerChatCommand, +) -> CmdResult<()> { + if let Some(entity_target) = parse_cmd_args!(args, EntityTarget) { + let entity_target = get_entity_target(entity_target, server)?; + + let tether_leader = server.state.ecs().uid_from_entity(target); + let tether_follower = server.state.ecs().uid_from_entity(entity_target); + + if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) { + let tether_length = tether_leader + .and_then(|uid| server.state.ecs().entity_from_uid(uid)) + .and_then(|e| server.state.read_component_cloned::(e)) + .map(|b| b.dimensions().y * 1.5 + 1.0) + .unwrap_or(6.0); + server + .state + .link(Tethered { + leader, + follower, + tether_length, + }) + .map_err(|_| "Failed to tether entities".into()) + } else { + Err("Tether members don't have Uids.".into()) + } + } else { + Err(Content::Plain(action.help_string())) + } +} + +fn handle_destroy_tethers( + server: &mut Server, + _client: EcsEntity, + target: EcsEntity, + _args: Vec, + _action: &ServerChatCommand, +) -> CmdResult<()> { + server + .state + .ecs() + .write_storage::>() + .remove(target); + server + .state + .ecs() + .write_storage::>() + .remove(target); + Ok(()) +} + +fn handle_mount( + server: &mut Server, + _client: EcsEntity, + target: EcsEntity, + args: Vec, + action: &ServerChatCommand, +) -> CmdResult<()> { + if let Some(entity_target) = parse_cmd_args!(args, EntityTarget) { + let entity_target = get_entity_target(entity_target, server)?; + + let rider = server.state.ecs().uid_from_entity(target); + let mount = server.state.ecs().uid_from_entity(entity_target); + + if let (Some(rider), Some(mount)) = (rider, mount) { + server + .state + .link(common::mounting::Mounting { mount, rider }) + .map_err(|_| "Failed to tether entities".into()) + } else { + Err("Mount and/or rider doesn't have an Uid component.".into()) + } + } else { + Err(Content::Plain(action.help_string())) + } +} + +fn handle_dismount( + server: &mut Server, + _client: EcsEntity, + target: EcsEntity, + _args: Vec, + _action: &ServerChatCommand, +) -> CmdResult<()> { + server + .state + .ecs() + .write_storage::>() + .remove(target); + server + .state + .ecs() + .write_storage::>() + .remove(target); + server + .state + .ecs() + .write_storage::>() + .remove(target); + server + .state + .ecs() + .write_storage::() + .remove(target); + Ok(()) +} From 51c67d73943e8bf974aaafa8616a5f4db228b3d5 Mon Sep 17 00:00:00 2001 From: Isse Date: Wed, 22 Nov 2023 23:40:13 +0100 Subject: [PATCH 03/11] client shorthands --- voxygen/src/cmd.rs | 78 ++++++++++++++++++++++++++++++++++++-- voxygen/src/session/mod.rs | 11 +++--- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/voxygen/src/cmd.rs b/voxygen/src/cmd.rs index de2ea09374..af0d794baf 100644 --- a/voxygen/src/cmd.rs +++ b/voxygen/src/cmd.rs @@ -1,11 +1,23 @@ use std::str::FromStr; use crate::{ - render::ExperimentalShader, session::settings_change::change_render_mode, GlobalState, + render::ExperimentalShader, + session::{settings_change::change_render_mode, SessionState}, + GlobalState, }; use client::Client; -use common::{cmd::*, comp::Admin, parse_cmd_args, uuid::Uuid}; +use common::{ + cmd::*, + comp::Admin, + link::Is, + mounting::{Mount, Rider, VolumeRider}, + parse_cmd_args, + resources::PlayerEntity, + uuid::Uuid, +}; +use common_net::sync::WorldSyncExt; use levenshtein::levenshtein; +use specs::WorldExt; use strum::IntoEnumIterator; // Please keep this sorted alphabetically, same as with server commands :-) @@ -148,12 +160,70 @@ type CommandResult = Result, String>; // Note: it's not clear what data future commands will need access to, so the // signature of this function might change pub fn run_command( - client: &mut Client, + session_state: &mut SessionState, global_state: &mut GlobalState, cmd: &str, - args: Vec, + mut args: Vec, ) -> CommandResult { let command = ChatCommandKind::from_str(cmd); + let client = &mut session_state.client.borrow_mut(); + let ecs = client.state().ecs(); + let player = ecs.read_resource::().0; + + for arg in args.iter_mut() { + if arg.starts_with('@') { + let uid = match arg.trim_start_matches('@') { + "target" => session_state + .target_entity + .and_then(|e| ecs.uid_from_entity(e)) + .ok_or("Not looking at a valid target".to_string())?, + "selected" => session_state + .selected_entity + .and_then(|(e, _)| ecs.uid_from_entity(e)) + .ok_or("You don't have a valid target selected".to_string())?, + "viewpoint" => session_state + .viewpoint_entity + .and_then(|e| ecs.uid_from_entity(e)) + .ok_or("Not viewing from a valid viewpoint entity".to_string())?, + "mount" => { + if let Some(player) = player { + ecs.read_storage::>() + .get(player) + .map(|is_rider| is_rider.mount) + .or(ecs.read_storage::>().get(player).and_then( + |is_rider| match is_rider.pos.kind { + common::mounting::Volume::Terrain => None, + common::mounting::Volume::Entity(uid) => Some(uid), + }, + )) + .ok_or("Not riding a valid entity".to_string())? + } else { + return Err("No player entity".to_string()); + } + }, + "rider" => { + if let Some(player) = player { + ecs.read_storage::>() + .get(player) + .map(|is_mount| is_mount.rider) + .ok_or("No valid rider".to_string())? + } else { + return Err("No player entity".to_string()); + } + }, + "self" => player + .and_then(|e| ecs.uid_from_entity(e)) + .ok_or("No player entity")?, + ident => { + return Err(format!( + "Expected target/selected/viewpoint/mount/rider found {ident}" + )); + }, + }; + let uid = u64::from(uid); + *arg = format!("uid@{uid}"); + } + } match command { Ok(ChatCommandKind::Server(cmd)) => { diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index bc3a24d512..a8eee89fd5 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -90,7 +90,7 @@ enum TickAction { pub struct SessionState { scene: Scene, - client: Rc>, + pub(crate) client: Rc>, metadata: UpdateCharacterMetadata, hud: Hud, key_state: KeyState, @@ -104,9 +104,9 @@ pub struct SessionState { camera_clamp: bool, zoom_lock: bool, is_aiming: bool, - target_entity: Option, - selected_entity: Option<(specs::Entity, std::time::Instant)>, - viewpoint_entity: Option, + pub(crate) target_entity: Option, + pub(crate) selected_entity: Option<(specs::Entity, std::time::Instant)>, + pub(crate) viewpoint_entity: Option, interactable: Option, #[cfg(not(target_os = "macos"))] mumble_link: SharedLink, @@ -1530,8 +1530,7 @@ impl PlayState for SessionState { self.client.borrow_mut().send_chat(msg); }, HudEvent::SendCommand(name, args) => { - match run_command(&mut self.client.borrow_mut(), global_state, &name, args) - { + match run_command(self, global_state, &name, args) { Ok(Some(info)) => { // TODO: Localise self.hud From 5a3e6316a5f1b638930478305b3e211504e9ad2c Mon Sep 17 00:00:00 2001 From: Isse Date: Thu, 23 Nov 2023 01:01:31 +0100 Subject: [PATCH 04/11] fix tethering --- assets/voxygen/i18n/en/command.ftl | 6 ++- common/src/mounting.rs | 8 ++++ server/src/cmd.rs | 65 ++++++++++++++++++++++-------- server/src/state_ext.rs | 2 + voxygen/src/cmd.rs | 3 +- 5 files changed, 65 insertions(+), 19 deletions(-) diff --git a/assets/voxygen/i18n/en/command.ftl b/assets/voxygen/i18n/en/command.ftl index 4f807e121b..cbc6007b08 100644 --- a/assets/voxygen/i18n/en/command.ftl +++ b/assets/voxygen/i18n/en/command.ftl @@ -91,4 +91,8 @@ command-unimplemented-teleporter-spawn = Teleporter spawning is not implemented command-kit-inventory-unavailable = Could not get inventory command-inventory-cant-fit-item = Can't fit item to inventory # Emitted by /disconnect_all when you dont exist (?) -command-you-dont-exist = You do not exist, so you cannot use this command \ No newline at end of file +command-you-dont-exist = You do not exist, so you cannot use this command +command-destroyed-tethers = All tethers destroyed! You are now free +command-destroyed-no-tethers = You're not connected to any tethers +command-dismounted = Dismounted +command-no-dismount = You're not riding or being ridden \ No newline at end of file diff --git a/common/src/mounting.rs b/common/src/mounting.rs index 94e90a4c6b..79b666b0d3 100644 --- a/common/src/mounting.rs +++ b/common/src/mounting.rs @@ -279,6 +279,14 @@ pub struct VolumeRiders { riders: HashSet>, } +impl VolumeRiders { + pub fn clear(&mut self) -> bool { + let res = !self.riders.is_empty(); + self.riders.clear(); + res + } +} + impl Component for VolumeRiders { type Storage = DenseVecStorage; } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index e5eed4ad67..a6111bc423 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -4581,22 +4581,36 @@ fn handle_tether( fn handle_destroy_tethers( server: &mut Server, - _client: EcsEntity, + client: EcsEntity, target: EcsEntity, _args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { - server + let mut destroyed = false; + destroyed |= server .state .ecs() .write_storage::>() - .remove(target); - server + .remove(target) + .is_some(); + destroyed |= server .state .ecs() .write_storage::>() - .remove(target); - Ok(()) + .remove(target) + .is_some(); + if destroyed { + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandInfo, + Content::localized("command-destroyed-tethers"), + ), + ); + Ok(()) + } else { + Err(Content::localized("command-destroyed-no-tethers")) + } } fn handle_mount( @@ -4616,7 +4630,7 @@ fn handle_mount( server .state .link(common::mounting::Mounting { mount, rider }) - .map_err(|_| "Failed to tether entities".into()) + .map_err(|_| "Failed to mount entities".into()) } else { Err("Mount and/or rider doesn't have an Uid component.".into()) } @@ -4627,30 +4641,47 @@ fn handle_mount( fn handle_dismount( server: &mut Server, - _client: EcsEntity, + client: EcsEntity, target: EcsEntity, _args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { - server + let mut destroyed = false; + destroyed |= server .state .ecs() .write_storage::>() - .remove(target); - server + .remove(target) + .is_some(); + destroyed |= server .state .ecs() .write_storage::>() - .remove(target); - server + .remove(target) + .is_some(); + destroyed |= server .state .ecs() .write_storage::>() - .remove(target); - server + .remove(target) + .is_some(); + destroyed |= server .state .ecs() .write_storage::() - .remove(target); - Ok(()) + .get_mut(target) + .map_or(false, |volume_riders| volume_riders.clear()); + + if destroyed { + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandInfo, + Content::localized("command-dismounted"), + ), + ); + Ok(()) + } else { + Err(Content::localized("command-no-dismount")) + } } diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index fd6813230a..0a4734247c 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -31,6 +31,7 @@ use common::{ resources::{Secs, Time, TimeOfDay}, rtsim::{Actor, RtSimEntity}, slowjob::SlowJobPool, + tether::Tethered, uid::{IdMaps, Uid}, util::Dir, LoadoutBuilder, ViewDistances, @@ -1164,6 +1165,7 @@ impl StateExt for State { maintain_link::(self); maintain_link::(self); + maintain_link::(self); } fn delete_entity_recorded( diff --git a/voxygen/src/cmd.rs b/voxygen/src/cmd.rs index af0d794baf..6adf4d95d0 100644 --- a/voxygen/src/cmd.rs +++ b/voxygen/src/cmd.rs @@ -216,7 +216,8 @@ pub fn run_command( .ok_or("No player entity")?, ident => { return Err(format!( - "Expected target/selected/viewpoint/mount/rider found {ident}" + "Expected target/selected/viewpoint/mount/rider/self after '@' found \ + {ident}" )); }, }; From ef47ed6f62ea1449a2cb614cff4fd55bfa084525 Mon Sep 17 00:00:00 2001 From: Isse Date: Thu, 23 Nov 2023 01:06:54 +0100 Subject: [PATCH 05/11] Add to changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 506c61a84a..4a7f2c5520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Crafting recipe for Cloverleaf glider. - Burning Potion that applies the Burning effect to the user - Precision +- A few new commands, `/tether`, `/destroy_tethers`, `/mount` and `/dismount`. +- A way to target non-player entities with commands. With rtsim_id: `rtsim@`, with uid: `uid@`. +- Shorthand in voxygen for specific entities in commands, some examples `@target`, `@mount`, `@viewpoint`. ### Changed From 4d8bcf0a92a914f2d7200ce514a064b3e23a8417 Mon Sep 17 00:00:00 2001 From: Isse Date: Thu, 23 Nov 2023 11:04:30 +0100 Subject: [PATCH 06/11] only preprocess for entity target argument --- common/src/cmd.rs | 18 ++++- server/src/cmd.rs | 2 + voxygen/src/cmd.rs | 170 ++++++++++++++++++++++++++++++++++++--------- 3 files changed, 158 insertions(+), 32 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 1e7f708e8c..0a60c8adbb 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -1095,7 +1095,7 @@ impl FromStr for ServerChatCommand { } } -#[derive(Eq, PartialEq, Debug)] +#[derive(Eq, PartialEq, Debug, Clone, Copy)] pub enum Requirement { Required, Optional, @@ -1216,6 +1216,22 @@ impl ArgumentSpec { }, } } + + pub fn requirement(&self) -> Requirement { + match self { + ArgumentSpec::PlayerName(r) + | ArgumentSpec::EntityTarget(r) + | ArgumentSpec::SiteName(r) + | ArgumentSpec::Float(_, _, r) + | ArgumentSpec::Integer(_, _, r) + | ArgumentSpec::Any(_, r) + | ArgumentSpec::Command(r) + | ArgumentSpec::Message(r) + | ArgumentSpec::Enum(_, _, r) + | ArgumentSpec::Boolean(_, _, r) => *r, + ArgumentSpec::SubCommand => Requirement::Required, + } + } } /// Parse a series of command arguments into values, including collecting all diff --git a/server/src/cmd.rs b/server/src/cmd.rs index a6111bc423..09d0dfdc45 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -3692,6 +3692,8 @@ fn handle_sudo( (entity, player_uuid), "Cannot sudo players with roles higher than your own.", )?; + } else if server.entity_admin_role(client) < Some(AdminRole::Admin) { + return Err("You don't have permission to sudo non-players.".into()); } } diff --git a/voxygen/src/cmd.rs b/voxygen/src/cmd.rs index 6adf4d95d0..450441631e 100644 --- a/voxygen/src/cmd.rs +++ b/voxygen/src/cmd.rs @@ -13,12 +13,13 @@ use common::{ mounting::{Mount, Rider, VolumeRider}, parse_cmd_args, resources::PlayerEntity, + uid::Uid, uuid::Uuid, }; use common_net::sync::WorldSyncExt; use levenshtein::levenshtein; -use specs::WorldExt; -use strum::IntoEnumIterator; +use specs::{Join, WorldExt}; +use strum::{EnumIter, IntoEnumIterator}; // Please keep this sorted alphabetically, same as with server commands :-) #[derive(Clone, Copy, strum::EnumIter)] @@ -155,37 +156,93 @@ impl FromStr for ChatCommandKind { /// text color type CommandResult = Result, String>; -/// Runs a command by either sending it to the server or processing it -/// locally. Returns a String to be output to the chat. -// Note: it's not clear what data future commands will need access to, so the -// signature of this function might change -pub fn run_command( +#[derive(EnumIter)] +enum ClientEntityTarget { + Target, + Selected, + Viewpoint, + Mount, + Rider, + TargetSelf, +} + +impl ClientEntityTarget { + const PREFIX: char = '@'; + + fn keyword(&self) -> &'static str { + match self { + ClientEntityTarget::Target => "target", + ClientEntityTarget::Selected => "selected", + ClientEntityTarget::Viewpoint => "viewpoint", + ClientEntityTarget::Mount => "mount", + ClientEntityTarget::Rider => "rider", + ClientEntityTarget::TargetSelf => "self", + } + } +} + +fn preproccess_command( session_state: &mut SessionState, - global_state: &mut GlobalState, - cmd: &str, - mut args: Vec, + command: &ChatCommandKind, + args: &mut Vec, ) -> CommandResult { - let command = ChatCommandKind::from_str(cmd); + let mut cmd_args = match command { + ChatCommandKind::Client(cmd) => cmd.data().args, + ChatCommandKind::Server(cmd) => cmd.data().args, + }; let client = &mut session_state.client.borrow_mut(); let ecs = client.state().ecs(); let player = ecs.read_resource::().0; - - for arg in args.iter_mut() { - if arg.starts_with('@') { - let uid = match arg.trim_start_matches('@') { - "target" => session_state + let mut command_start = 0; + for (i, arg) in args.iter_mut().enumerate() { + let mut could_be_entity_target = false; + if let Some(post_cmd_args) = cmd_args.get(i - command_start..) { + for (j, arg_spec) in post_cmd_args.iter().enumerate() { + match arg_spec { + ArgumentSpec::EntityTarget(_) => could_be_entity_target = true, + ArgumentSpec::SubCommand => { + if let Some(sub_command) = + ServerChatCommand::iter().find(|cmd| cmd.keyword() == arg) + { + cmd_args = sub_command.data().args; + command_start = i + j + 1; + break; + } + }, + _ => {}, + } + if matches!(arg_spec.requirement(), Requirement::Required) { + break; + } + } + } else if matches!(cmd_args.last(), Some(ArgumentSpec::SubCommand)) { + could_be_entity_target = true; + } + if could_be_entity_target && arg.starts_with(ClientEntityTarget::PREFIX) { + let target_str = arg.trim_start_matches(ClientEntityTarget::PREFIX); + let target = ClientEntityTarget::iter() + .find(|t| t.keyword() == target_str) + .ok_or_else(|| { + let help_string = ClientEntityTarget::iter() + .map(|t| t.keyword().to_string()) + .reduce(|a, b| format!("{a}/{b}")) + .unwrap_or_default(); + format!("Expected {help_string} after '@' found {target_str}") + })?; + let uid = match target { + ClientEntityTarget::Target => session_state .target_entity .and_then(|e| ecs.uid_from_entity(e)) .ok_or("Not looking at a valid target".to_string())?, - "selected" => session_state + ClientEntityTarget::Selected => session_state .selected_entity .and_then(|(e, _)| ecs.uid_from_entity(e)) .ok_or("You don't have a valid target selected".to_string())?, - "viewpoint" => session_state + ClientEntityTarget::Viewpoint => session_state .viewpoint_entity .and_then(|e| ecs.uid_from_entity(e)) .ok_or("Not viewing from a valid viewpoint entity".to_string())?, - "mount" => { + ClientEntityTarget::Mount => { if let Some(player) = player { ecs.read_storage::>() .get(player) @@ -201,7 +258,7 @@ pub fn run_command( return Err("No player entity".to_string()); } }, - "rider" => { + ClientEntityTarget::Rider => { if let Some(player) = player { ecs.read_storage::>() .get(player) @@ -211,30 +268,43 @@ pub fn run_command( return Err("No player entity".to_string()); } }, - "self" => player + ClientEntityTarget::TargetSelf => player .and_then(|e| ecs.uid_from_entity(e)) .ok_or("No player entity")?, - ident => { - return Err(format!( - "Expected target/selected/viewpoint/mount/rider/self after '@' found \ - {ident}" - )); - }, }; let uid = u64::from(uid); *arg = format!("uid@{uid}"); } } + Ok(None) +} + +/// Runs a command by either sending it to the server or processing it +/// locally. Returns a String to be output to the chat. +// Note: it's not clear what data future commands will need access to, so the +// signature of this function might change +pub fn run_command( + session_state: &mut SessionState, + global_state: &mut GlobalState, + cmd: &str, + mut args: Vec, +) -> CommandResult { + let command = ChatCommandKind::from_str(cmd) + .map_err(|_| invalid_command_message(&session_state.client.borrow(), cmd.to_string()))?; + + preproccess_command(session_state, &command, &mut args)?; + + let client = &mut session_state.client.borrow_mut(); + match command { - Ok(ChatCommandKind::Server(cmd)) => { + ChatCommandKind::Server(cmd) => { client.send_command(cmd.keyword().into(), args); Ok(None) // The server will provide a response when the command is run }, - Ok(ChatCommandKind::Client(cmd)) => { + ChatCommandKind::Client(cmd) => { Ok(Some(run_client_command(client, global_state, cmd, args)?)) }, - Err(()) => Err(invalid_command_message(client, cmd.to_string())), } } @@ -455,7 +525,45 @@ 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::EntityTarget(_) => { + if let Some((spec, end)) = part.split_once(ClientEntityTarget::PREFIX) { + match spec { + "" => ClientEntityTarget::iter() + .filter_map(|target| { + let ident = target.keyword(); + if ident.starts_with(end) { + Some(format!("@{ident}")) + } else { + None + } + }) + .collect(), + "uid" => { + if let Ok(end) = u64::from_str(end) { + client + .state() + .ecs() + .read_storage::() + .join() + .filter_map(|uid| { + let uid = u64::from(*uid); + if end < uid { + Some(format!("uid@{uid}")) + } else { + None + } + }) + .collect() + } else { + vec![] + } + }, + _ => vec![], + } + } else { + complete_player(part, client) + } + }, ArgumentSpec::SiteName(_) => complete_site(part, client), ArgumentSpec::Float(_, x, _) => { if part.is_empty() { From 88a7e1de863fe403fe9c5ff4141096c6bdefa6f6 Mon Sep 17 00:00:00 2001 From: Isse Date: Thu, 23 Nov 2023 11:08:21 +0100 Subject: [PATCH 07/11] better tab completion for uid --- voxygen/src/cmd.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/voxygen/src/cmd.rs b/voxygen/src/cmd.rs index 450441631e..fe413fd553 100644 --- a/voxygen/src/cmd.rs +++ b/voxygen/src/cmd.rs @@ -539,7 +539,9 @@ impl TabComplete for ArgumentSpec { }) .collect(), "uid" => { - if let Ok(end) = u64::from_str(end) { + if let Some(end) = + u64::from_str(end).ok().or(end.is_empty().then_some(0)) + { client .state() .ecs() From 31f67a97b33a3c6f5216addb6c7a84cd20682217 Mon Sep 17 00:00:00 2001 From: Isse Date: Thu, 23 Nov 2023 12:06:58 +0100 Subject: [PATCH 08/11] clippy fix --- voxygen/src/cmd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voxygen/src/cmd.rs b/voxygen/src/cmd.rs index fe413fd553..e5b0e485fc 100644 --- a/voxygen/src/cmd.rs +++ b/voxygen/src/cmd.rs @@ -184,7 +184,7 @@ impl ClientEntityTarget { fn preproccess_command( session_state: &mut SessionState, command: &ChatCommandKind, - args: &mut Vec, + args: &mut [String], ) -> CommandResult { let mut cmd_args = match command { ChatCommandKind::Client(cmd) => cmd.data().args, From 2500fa2b42d3059cef2c449c318da540a11533c5 Mon Sep 17 00:00:00 2001 From: Isse Date: Thu, 23 Nov 2023 20:52:20 +0100 Subject: [PATCH 09/11] Command error on trying to spawn a pet for an anchored entity --- server/src/cmd.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 09d0dfdc45..5612ff95ed 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1586,6 +1586,17 @@ fn handle_spawn( ) => { let uid = uid(server, target, "target")?; let alignment = parse_alignment(uid, &opt_align)?; + + if matches!(alignment, Alignment::Owned(_)) + && server + .state + .ecs() + .read_storage::() + .contains(target) + { + return Err("Spawning this pet would create an anchor chain".into()); + } + let amount = opt_amount.filter(|x| *x > 0).unwrap_or(1).min(50); let ai = opt_ai.unwrap_or(true); From a151a6f5509f7de5ddcbbbdd02df514fcdd23098 Mon Sep 17 00:00:00 2001 From: Isse Date: Thu, 23 Nov 2023 22:22:34 +0100 Subject: [PATCH 10/11] tether length argument --- common/src/cmd.rs | 5 ++++- server/src/cmd.rs | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 0a60c8adbb..1d84f1ed5f 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -900,7 +900,10 @@ impl ServerChatCommand { cmd(vec![], "Repairs all equipped items", Some(Admin)) }, ServerChatCommand::Tether => cmd( - vec![EntityTarget(Required)], + vec![ + EntityTarget(Required), + Boolean("automatic length", "true".to_string(), Optional), + ], "Tether another entity to yourself", Some(Admin), ), diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 5612ff95ed..2862b3ac23 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -4564,18 +4564,46 @@ fn handle_tether( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - if let Some(entity_target) = parse_cmd_args!(args, EntityTarget) { + enum Either { + Left(A), + Right(B), + } + + impl FromStr for Either { + type Err = B::Err; + + fn from_str(s: &str) -> Result { + match A::from_str(s) { + Ok(a) => Ok(Either::Left(a)), + Err(_) => match B::from_str(s) { + Ok(b) => Ok(Either::Right(b)), + Err(e) => Err(e), + }, + } + } + } + if let (Some(entity_target), length) = parse_cmd_args!(args, EntityTarget, Either) { let entity_target = get_entity_target(entity_target, server)?; let tether_leader = server.state.ecs().uid_from_entity(target); let tether_follower = server.state.ecs().uid_from_entity(entity_target); if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) { - let tether_length = tether_leader - .and_then(|uid| server.state.ecs().entity_from_uid(uid)) - .and_then(|e| server.state.read_component_cloned::(e)) + let base_len = server + .state + .read_component_cloned::(target) .map(|b| b.dimensions().y * 1.5 + 1.0) .unwrap_or(6.0); + let tether_length = match length { + Some(Either::Left(l)) => l.max(0.0) + base_len, + Some(Either::Right(true)) => { + let leader_pos = position(server, target, "leader")?; + let follower_pos = position(server, entity_target, "follower")?; + + leader_pos.0.distance(follower_pos.0) + base_len + }, + _ => base_len, + }; server .state .link(Tethered { From a4caa2bc261df02a1c856ee1cf346b4cb984e490 Mon Sep 17 00:00:00 2001 From: Isse Date: Sun, 26 Nov 2023 15:37:14 +0100 Subject: [PATCH 11/11] use Result::or for Either::from_str --- server/src/cmd.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 2862b3ac23..664e85acdb 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -4573,13 +4573,9 @@ fn handle_tether( type Err = B::Err; fn from_str(s: &str) -> Result { - match A::from_str(s) { - Ok(a) => Ok(Either::Left(a)), - Err(_) => match B::from_str(s) { - Ok(b) => Ok(Either::Right(b)), - Err(e) => Err(e), - }, - } + A::from_str(s) + .map(Either::Left) + .or_else(|_| B::from_str(s).map(Either::Right)) } } if let (Some(entity_target), length) = parse_cmd_args!(args, EntityTarget, Either) {