entity targets

This commit is contained in:
Isse 2023-11-22 22:40:56 +01:00
parent 456c0ad3e8
commit 1071fd0bca
3 changed files with 109 additions and 22 deletions

View File

@ -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<Self, Self::Err> {
// 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 {
"<entity>".to_string()
} else {
"[entity]".to_string()
}
},
ArgumentSpec::SiteName(req) => {
if &Requirement::Required == req {
"<site>".to_string()

View File

@ -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<String>,
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<String>,
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<EcsEntity> {
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::<crate::rtsim::RtSim>()
.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::<common::uid::IdMaps>()
.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<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
if let (Some(player_alias), Some(cmd), cmd_args) =
parse_cmd_args!(args, String, String, ..Vec<String>)
if let (Some(entity_target), Some(cmd), cmd_args) =
parse_cmd_args!(args, EntityTarget, String, ..Vec<String>)
{
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::<comp::Player>();
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"))
}

View File

@ -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<String> {
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() {