mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'isse/very-cool-commands' into 'master'
Target any entity with commands, and a few new commands. See merge request veloren/veloren!4192
This commit is contained in:
commit
69e827dfda
@ -34,6 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Crafting recipe for Cloverleaf glider.
|
- Crafting recipe for Cloverleaf glider.
|
||||||
- Burning Potion that applies the Burning effect to the user
|
- Burning Potion that applies the Burning effect to the user
|
||||||
- Precision
|
- Precision
|
||||||
|
- A few new commands, `/tether`, `/destroy_tethers`, `/mount` and `/dismount`.
|
||||||
|
- A way to target non-player entities with commands. With rtsim_id: `rtsim@<id>`, with uid: `uid@<id>`.
|
||||||
|
- Shorthand in voxygen for specific entities in commands, some examples `@target`, `@mount`, `@viewpoint`.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -92,3 +92,7 @@ command-kit-inventory-unavailable = Could not get inventory
|
|||||||
command-inventory-cant-fit-item = Can't fit item to inventory
|
command-inventory-cant-fit-item = Can't fit item to inventory
|
||||||
# Emitted by /disconnect_all when you dont exist (?)
|
# Emitted by /disconnect_all when you dont exist (?)
|
||||||
command-you-dont-exist = You do not exist, so you cannot use this command
|
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
|
@ -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 :-)
|
// Please keep this sorted alphabetically :-)
|
||||||
#[derive(Copy, Clone, strum::EnumIter)]
|
#[derive(Copy, Clone, strum::EnumIter)]
|
||||||
pub enum ServerChatCommand {
|
pub enum ServerChatCommand {
|
||||||
@ -283,7 +316,9 @@ pub enum ServerChatCommand {
|
|||||||
DebugColumn,
|
DebugColumn,
|
||||||
DebugWays,
|
DebugWays,
|
||||||
DeleteLocation,
|
DeleteLocation,
|
||||||
|
DestroyTethers,
|
||||||
DisconnectAllPlayers,
|
DisconnectAllPlayers,
|
||||||
|
Dismount,
|
||||||
DropAll,
|
DropAll,
|
||||||
Dummy,
|
Dummy,
|
||||||
Explosion,
|
Explosion,
|
||||||
@ -312,6 +347,7 @@ pub enum ServerChatCommand {
|
|||||||
MakeSprite,
|
MakeSprite,
|
||||||
MakeVolume,
|
MakeVolume,
|
||||||
Motd,
|
Motd,
|
||||||
|
Mount,
|
||||||
Object,
|
Object,
|
||||||
PermitBuild,
|
PermitBuild,
|
||||||
Players,
|
Players,
|
||||||
@ -340,6 +376,7 @@ pub enum ServerChatCommand {
|
|||||||
Spawn,
|
Spawn,
|
||||||
Sudo,
|
Sudo,
|
||||||
Tell,
|
Tell,
|
||||||
|
Tether,
|
||||||
Time,
|
Time,
|
||||||
TimeScale,
|
TimeScale,
|
||||||
Tp,
|
Tp,
|
||||||
@ -739,8 +776,8 @@ impl ServerChatCommand {
|
|||||||
Some(Admin),
|
Some(Admin),
|
||||||
),
|
),
|
||||||
ServerChatCommand::Sudo => cmd(
|
ServerChatCommand::Sudo => cmd(
|
||||||
vec![PlayerName(Required), SubCommand],
|
vec![EntityTarget(Required), SubCommand],
|
||||||
"Run command as if you were another player",
|
"Run command as if you were another entity",
|
||||||
Some(Moderator),
|
Some(Moderator),
|
||||||
),
|
),
|
||||||
ServerChatCommand::Tell => cmd(
|
ServerChatCommand::Tell => cmd(
|
||||||
@ -760,10 +797,10 @@ impl ServerChatCommand {
|
|||||||
),
|
),
|
||||||
ServerChatCommand::Tp => cmd(
|
ServerChatCommand::Tp => cmd(
|
||||||
vec![
|
vec![
|
||||||
PlayerName(Optional),
|
EntityTarget(Optional),
|
||||||
Boolean("Dismount from ship", "true".to_string(), Optional),
|
Boolean("Dismount from ship", "true".to_string(), Optional),
|
||||||
],
|
],
|
||||||
"Teleport to another player",
|
"Teleport to another entity",
|
||||||
Some(Moderator),
|
Some(Moderator),
|
||||||
),
|
),
|
||||||
ServerChatCommand::RtsimTp => cmd(
|
ServerChatCommand::RtsimTp => cmd(
|
||||||
@ -862,6 +899,25 @@ impl ServerChatCommand {
|
|||||||
ServerChatCommand::RepairEquipment => {
|
ServerChatCommand::RepairEquipment => {
|
||||||
cmd(vec![], "Repairs all equipped items", Some(Admin))
|
cmd(vec![], "Repairs all equipped items", Some(Admin))
|
||||||
},
|
},
|
||||||
|
ServerChatCommand::Tether => cmd(
|
||||||
|
vec![
|
||||||
|
EntityTarget(Required),
|
||||||
|
Boolean("automatic length", "true".to_string(), Optional),
|
||||||
|
],
|
||||||
|
"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),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -952,6 +1008,10 @@ impl ServerChatCommand {
|
|||||||
ServerChatCommand::Lightning => "lightning",
|
ServerChatCommand::Lightning => "lightning",
|
||||||
ServerChatCommand::Scale => "scale",
|
ServerChatCommand::Scale => "scale",
|
||||||
ServerChatCommand::RepairEquipment => "repair_equipment",
|
ServerChatCommand::RepairEquipment => "repair_equipment",
|
||||||
|
ServerChatCommand::Tether => "tether",
|
||||||
|
ServerChatCommand::DestroyTethers => "destroy_tethers",
|
||||||
|
ServerChatCommand::Mount => "mount",
|
||||||
|
ServerChatCommand::Dismount => "dismount",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1001,6 +1061,7 @@ impl ServerChatCommand {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|arg| match arg {
|
.map(|arg| match arg {
|
||||||
ArgumentSpec::PlayerName(_) => "{}",
|
ArgumentSpec::PlayerName(_) => "{}",
|
||||||
|
ArgumentSpec::EntityTarget(_) => "{}",
|
||||||
ArgumentSpec::SiteName(_) => "{/.*/}",
|
ArgumentSpec::SiteName(_) => "{/.*/}",
|
||||||
ArgumentSpec::Float(_, _, _) => "{}",
|
ArgumentSpec::Float(_, _, _) => "{}",
|
||||||
ArgumentSpec::Integer(_, _, _) => "{d}",
|
ArgumentSpec::Integer(_, _, _) => "{d}",
|
||||||
@ -1037,7 +1098,7 @@ impl FromStr for ServerChatCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Debug)]
|
#[derive(Eq, PartialEq, Debug, Clone, Copy)]
|
||||||
pub enum Requirement {
|
pub enum Requirement {
|
||||||
Required,
|
Required,
|
||||||
Optional,
|
Optional,
|
||||||
@ -1047,6 +1108,8 @@ pub enum Requirement {
|
|||||||
pub enum ArgumentSpec {
|
pub enum ArgumentSpec {
|
||||||
/// The argument refers to a player by alias
|
/// The argument refers to a player by alias
|
||||||
PlayerName(Requirement),
|
PlayerName(Requirement),
|
||||||
|
/// The arguments refers to an entity in some way.
|
||||||
|
EntityTarget(Requirement),
|
||||||
// The argument refers to a site, by name.
|
// The argument refers to a site, by name.
|
||||||
SiteName(Requirement),
|
SiteName(Requirement),
|
||||||
/// The argument is a float. The associated values are
|
/// The argument is a float. The associated values are
|
||||||
@ -1090,6 +1153,13 @@ impl ArgumentSpec {
|
|||||||
"[player]".to_string()
|
"[player]".to_string()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
ArgumentSpec::EntityTarget(req) => {
|
||||||
|
if &Requirement::Required == req {
|
||||||
|
"<entity>".to_string()
|
||||||
|
} else {
|
||||||
|
"[entity]".to_string()
|
||||||
|
}
|
||||||
|
},
|
||||||
ArgumentSpec::SiteName(req) => {
|
ArgumentSpec::SiteName(req) => {
|
||||||
if &Requirement::Required == req {
|
if &Requirement::Required == req {
|
||||||
"<site>".to_string()
|
"<site>".to_string()
|
||||||
@ -1149,6 +1219,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
|
/// Parse a series of command arguments into values, including collecting all
|
||||||
|
@ -279,6 +279,14 @@ pub struct VolumeRiders {
|
|||||||
riders: HashSet<Vec3<i32>>,
|
riders: HashSet<Vec3<i32>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl VolumeRiders {
|
||||||
|
pub fn clear(&mut self) -> bool {
|
||||||
|
let res = !self.riders.is_empty();
|
||||||
|
self.riders.clear();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Component for VolumeRiders {
|
impl Component for VolumeRiders {
|
||||||
type Storage = DenseVecStorage<Self>;
|
type Storage = DenseVecStorage<Self>;
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ use common::{
|
|||||||
assets,
|
assets,
|
||||||
calendar::Calendar,
|
calendar::Calendar,
|
||||||
cmd::{
|
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,
|
KIT_MANIFEST_PATH, PRESET_MANIFEST_PATH,
|
||||||
},
|
},
|
||||||
comp::{
|
comp::{
|
||||||
@ -208,6 +208,10 @@ fn do_command(
|
|||||||
ServerChatCommand::Lightning => handle_lightning,
|
ServerChatCommand::Lightning => handle_lightning,
|
||||||
ServerChatCommand::Scale => handle_scale,
|
ServerChatCommand::Scale => handle_scale,
|
||||||
ServerChatCommand::RepairEquipment => handle_repair_equipment,
|
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)
|
handler(server, client, target, args, cmd)
|
||||||
@ -1288,9 +1292,9 @@ fn handle_tp(
|
|||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
action: &ServerChatCommand,
|
action: &ServerChatCommand,
|
||||||
) -> CmdResult<()> {
|
) -> CmdResult<()> {
|
||||||
let (player, dismount_volume) = parse_cmd_args!(args, String, bool);
|
let (entity_target, dismount_volume) = parse_cmd_args!(args, EntityTarget, bool);
|
||||||
let player = if let Some(alias) = player {
|
let player = if let Some(entity_target) = entity_target {
|
||||||
find_alias(server.state.ecs(), &alias)?.0
|
get_entity_target(entity_target, server)?
|
||||||
} else if client != target {
|
} else if client != target {
|
||||||
client
|
client
|
||||||
} else {
|
} else {
|
||||||
@ -1582,6 +1586,17 @@ fn handle_spawn(
|
|||||||
) => {
|
) => {
|
||||||
let uid = uid(server, target, "target")?;
|
let uid = uid(server, target, "target")?;
|
||||||
let alignment = parse_alignment(uid, &opt_align)?;
|
let alignment = parse_alignment(uid, &opt_align)?;
|
||||||
|
|
||||||
|
if matches!(alignment, Alignment::Owned(_))
|
||||||
|
&& server
|
||||||
|
.state
|
||||||
|
.ecs()
|
||||||
|
.read_storage::<comp::Anchor>()
|
||||||
|
.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 amount = opt_amount.filter(|x| *x > 0).unwrap_or(1).min(50);
|
||||||
|
|
||||||
let ai = opt_ai.unwrap_or(true);
|
let ai = opt_ai.unwrap_or(true);
|
||||||
@ -3527,10 +3542,12 @@ fn handle_skill_point(
|
|||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
action: &ServerChatCommand,
|
action: &ServerChatCommand,
|
||||||
) -> CmdResult<()> {
|
) -> 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 skill_tree = parse_skill_tree(&a_skill_tree)?;
|
||||||
let player = a_alias
|
let player = entity_target
|
||||||
.map(|alias| find_alias(server.state.ecs(), &alias).map(|(target, _)| target))
|
.map(|entity_target| get_entity_target(entity_target, server))
|
||||||
.unwrap_or(Ok(target))?;
|
.unwrap_or(Ok(target))?;
|
||||||
|
|
||||||
if let Some(mut skill_set) = server
|
if let Some(mut skill_set) = server
|
||||||
@ -3542,7 +3559,7 @@ fn handle_skill_point(
|
|||||||
skill_set.add_skill_points(skill_tree, sp);
|
skill_set.add_skill_points(skill_tree, sp);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err("Player has no stats!".into())
|
Err("Entity has no stats!".into())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(Content::Plain(action.help_string()))
|
Err(Content::Plain(action.help_string()))
|
||||||
@ -3629,6 +3646,37 @@ fn handle_remove_lights(
|
|||||||
Ok(())
|
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(
|
fn handle_sudo(
|
||||||
server: &mut Server,
|
server: &mut Server,
|
||||||
client: EcsEntity,
|
client: EcsEntity,
|
||||||
@ -3636,23 +3684,34 @@ fn handle_sudo(
|
|||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
action: &ServerChatCommand,
|
action: &ServerChatCommand,
|
||||||
) -> CmdResult<()> {
|
) -> CmdResult<()> {
|
||||||
if let (Some(player_alias), Some(cmd), cmd_args) =
|
if let (Some(entity_target), Some(cmd), cmd_args) =
|
||||||
parse_cmd_args!(args, String, String, ..Vec<String>)
|
parse_cmd_args!(args, EntityTarget, String, ..Vec<String>)
|
||||||
{
|
{
|
||||||
if let Ok(action) = cmd.parse() {
|
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")?;
|
let client_uuid = uuid(server, client, "client")?;
|
||||||
|
|
||||||
|
// 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(
|
verify_above_role(
|
||||||
server,
|
server,
|
||||||
(client, client_uuid),
|
(client, client_uuid),
|
||||||
(player, player_uuid),
|
(entity, player_uuid),
|
||||||
"Cannot sudo players with roles higher than your own.",
|
"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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: consider making this into a tail call or loop (to avoid the potential
|
// 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
|
// stack overflow, although it's less of a risk coming from only mods and
|
||||||
// admins).
|
// admins).
|
||||||
do_command(server, client, player, cmd_args, &action)
|
do_command(server, client, entity, cmd_args, &action)
|
||||||
} else {
|
} else {
|
||||||
Err(Content::localized("command-unknown"))
|
Err(Content::localized("command-unknown"))
|
||||||
}
|
}
|
||||||
@ -4497,3 +4556,169 @@ fn handle_repair_equipment(
|
|||||||
Err(Content::Plain(action.help_string()))
|
Err(Content::Plain(action.help_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_tether(
|
||||||
|
server: &mut Server,
|
||||||
|
_client: EcsEntity,
|
||||||
|
target: EcsEntity,
|
||||||
|
args: Vec<String>,
|
||||||
|
action: &ServerChatCommand,
|
||||||
|
) -> CmdResult<()> {
|
||||||
|
enum Either<A, B> {
|
||||||
|
Left(A),
|
||||||
|
Right(B),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: FromStr, B: FromStr> FromStr for Either<A, B> {
|
||||||
|
type Err = B::Err;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
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<f32, bool>) {
|
||||||
|
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 base_len = server
|
||||||
|
.state
|
||||||
|
.read_component_cloned::<comp::Body>(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 {
|
||||||
|
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<String>,
|
||||||
|
_action: &ServerChatCommand,
|
||||||
|
) -> CmdResult<()> {
|
||||||
|
let mut destroyed = false;
|
||||||
|
destroyed |= server
|
||||||
|
.state
|
||||||
|
.ecs()
|
||||||
|
.write_storage::<Is<common::tether::Leader>>()
|
||||||
|
.remove(target)
|
||||||
|
.is_some();
|
||||||
|
destroyed |= server
|
||||||
|
.state
|
||||||
|
.ecs()
|
||||||
|
.write_storage::<Is<common::tether::Follower>>()
|
||||||
|
.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(
|
||||||
|
server: &mut Server,
|
||||||
|
_client: EcsEntity,
|
||||||
|
target: EcsEntity,
|
||||||
|
args: Vec<String>,
|
||||||
|
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 mount 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<String>,
|
||||||
|
_action: &ServerChatCommand,
|
||||||
|
) -> CmdResult<()> {
|
||||||
|
let mut destroyed = false;
|
||||||
|
destroyed |= server
|
||||||
|
.state
|
||||||
|
.ecs()
|
||||||
|
.write_storage::<Is<common::mounting::Rider>>()
|
||||||
|
.remove(target)
|
||||||
|
.is_some();
|
||||||
|
destroyed |= server
|
||||||
|
.state
|
||||||
|
.ecs()
|
||||||
|
.write_storage::<Is<common::mounting::VolumeRider>>()
|
||||||
|
.remove(target)
|
||||||
|
.is_some();
|
||||||
|
destroyed |= server
|
||||||
|
.state
|
||||||
|
.ecs()
|
||||||
|
.write_storage::<Is<common::mounting::Mount>>()
|
||||||
|
.remove(target)
|
||||||
|
.is_some();
|
||||||
|
destroyed |= server
|
||||||
|
.state
|
||||||
|
.ecs()
|
||||||
|
.write_storage::<common::mounting::VolumeRiders>()
|
||||||
|
.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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -31,6 +31,7 @@ use common::{
|
|||||||
resources::{Secs, Time, TimeOfDay},
|
resources::{Secs, Time, TimeOfDay},
|
||||||
rtsim::{Actor, RtSimEntity},
|
rtsim::{Actor, RtSimEntity},
|
||||||
slowjob::SlowJobPool,
|
slowjob::SlowJobPool,
|
||||||
|
tether::Tethered,
|
||||||
uid::{IdMaps, Uid},
|
uid::{IdMaps, Uid},
|
||||||
util::Dir,
|
util::Dir,
|
||||||
LoadoutBuilder, ViewDistances,
|
LoadoutBuilder, ViewDistances,
|
||||||
@ -1164,6 +1165,7 @@ impl StateExt for State {
|
|||||||
|
|
||||||
maintain_link::<Mounting>(self);
|
maintain_link::<Mounting>(self);
|
||||||
maintain_link::<VolumeMounting>(self);
|
maintain_link::<VolumeMounting>(self);
|
||||||
|
maintain_link::<Tethered>(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_entity_recorded(
|
fn delete_entity_recorded(
|
||||||
|
@ -1,12 +1,25 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::{
|
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 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,
|
||||||
|
uid::Uid,
|
||||||
|
uuid::Uuid,
|
||||||
|
};
|
||||||
|
use common_net::sync::WorldSyncExt;
|
||||||
use levenshtein::levenshtein;
|
use levenshtein::levenshtein;
|
||||||
use strum::IntoEnumIterator;
|
use specs::{Join, WorldExt};
|
||||||
|
use strum::{EnumIter, IntoEnumIterator};
|
||||||
|
|
||||||
// Please keep this sorted alphabetically, same as with server commands :-)
|
// Please keep this sorted alphabetically, same as with server commands :-)
|
||||||
#[derive(Clone, Copy, strum::EnumIter)]
|
#[derive(Clone, Copy, strum::EnumIter)]
|
||||||
@ -78,6 +91,7 @@ impl ClientChatCommand {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|arg| match arg {
|
.map(|arg| match arg {
|
||||||
ArgumentSpec::PlayerName(_) => "{}",
|
ArgumentSpec::PlayerName(_) => "{}",
|
||||||
|
ArgumentSpec::EntityTarget(_) => "{}",
|
||||||
ArgumentSpec::SiteName(_) => "{/.*/}",
|
ArgumentSpec::SiteName(_) => "{/.*/}",
|
||||||
ArgumentSpec::Float(_, _, _) => "{}",
|
ArgumentSpec::Float(_, _, _) => "{}",
|
||||||
ArgumentSpec::Integer(_, _, _) => "{d}",
|
ArgumentSpec::Integer(_, _, _) => "{d}",
|
||||||
@ -142,27 +156,155 @@ impl FromStr for ChatCommandKind {
|
|||||||
/// text color
|
/// text color
|
||||||
type CommandResult = Result<Option<String>, String>;
|
type CommandResult = Result<Option<String>, String>;
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
command: &ChatCommandKind,
|
||||||
|
args: &mut [String],
|
||||||
|
) -> CommandResult {
|
||||||
|
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::<PlayerEntity>().0;
|
||||||
|
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())?,
|
||||||
|
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())?,
|
||||||
|
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())?,
|
||||||
|
ClientEntityTarget::Mount => {
|
||||||
|
if let Some(player) = player {
|
||||||
|
ecs.read_storage::<Is<Rider>>()
|
||||||
|
.get(player)
|
||||||
|
.map(|is_rider| is_rider.mount)
|
||||||
|
.or(ecs.read_storage::<Is<VolumeRider>>().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());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ClientEntityTarget::Rider => {
|
||||||
|
if let Some(player) = player {
|
||||||
|
ecs.read_storage::<Is<Mount>>()
|
||||||
|
.get(player)
|
||||||
|
.map(|is_mount| is_mount.rider)
|
||||||
|
.ok_or("No valid rider".to_string())?
|
||||||
|
} else {
|
||||||
|
return Err("No player entity".to_string());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ClientEntityTarget::TargetSelf => player
|
||||||
|
.and_then(|e| ecs.uid_from_entity(e))
|
||||||
|
.ok_or("No player entity")?,
|
||||||
|
};
|
||||||
|
let uid = u64::from(uid);
|
||||||
|
*arg = format!("uid@{uid}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs a command by either sending it to the server or processing it
|
/// Runs a command by either sending it to the server or processing it
|
||||||
/// locally. Returns a String to be output to the chat.
|
/// 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
|
// Note: it's not clear what data future commands will need access to, so the
|
||||||
// signature of this function might change
|
// signature of this function might change
|
||||||
pub fn run_command(
|
pub fn run_command(
|
||||||
client: &mut Client,
|
session_state: &mut SessionState,
|
||||||
global_state: &mut GlobalState,
|
global_state: &mut GlobalState,
|
||||||
cmd: &str,
|
cmd: &str,
|
||||||
args: Vec<String>,
|
mut args: Vec<String>,
|
||||||
) -> CommandResult {
|
) -> CommandResult {
|
||||||
let command = ChatCommandKind::from_str(cmd);
|
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 {
|
match command {
|
||||||
Ok(ChatCommandKind::Server(cmd)) => {
|
ChatCommandKind::Server(cmd) => {
|
||||||
client.send_command(cmd.keyword().into(), args);
|
client.send_command(cmd.keyword().into(), args);
|
||||||
Ok(None) // The server will provide a response when the command is run
|
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)?))
|
Ok(Some(run_client_command(client, global_state, cmd, args)?))
|
||||||
},
|
},
|
||||||
Err(()) => Err(invalid_command_message(client, cmd.to_string())),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,6 +525,47 @@ impl TabComplete for ArgumentSpec {
|
|||||||
fn complete(&self, part: &str, client: &Client) -> Vec<String> {
|
fn complete(&self, part: &str, client: &Client) -> Vec<String> {
|
||||||
match self {
|
match self {
|
||||||
ArgumentSpec::PlayerName(_) => complete_player(part, client),
|
ArgumentSpec::PlayerName(_) => 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 Some(end) =
|
||||||
|
u64::from_str(end).ok().or(end.is_empty().then_some(0))
|
||||||
|
{
|
||||||
|
client
|
||||||
|
.state()
|
||||||
|
.ecs()
|
||||||
|
.read_storage::<Uid>()
|
||||||
|
.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::SiteName(_) => complete_site(part, client),
|
||||||
ArgumentSpec::Float(_, x, _) => {
|
ArgumentSpec::Float(_, x, _) => {
|
||||||
if part.is_empty() {
|
if part.is_empty() {
|
||||||
|
@ -90,7 +90,7 @@ enum TickAction {
|
|||||||
|
|
||||||
pub struct SessionState {
|
pub struct SessionState {
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
client: Rc<RefCell<Client>>,
|
pub(crate) client: Rc<RefCell<Client>>,
|
||||||
metadata: UpdateCharacterMetadata,
|
metadata: UpdateCharacterMetadata,
|
||||||
hud: Hud,
|
hud: Hud,
|
||||||
key_state: KeyState,
|
key_state: KeyState,
|
||||||
@ -104,9 +104,9 @@ pub struct SessionState {
|
|||||||
camera_clamp: bool,
|
camera_clamp: bool,
|
||||||
zoom_lock: bool,
|
zoom_lock: bool,
|
||||||
is_aiming: bool,
|
is_aiming: bool,
|
||||||
target_entity: Option<specs::Entity>,
|
pub(crate) target_entity: Option<specs::Entity>,
|
||||||
selected_entity: Option<(specs::Entity, std::time::Instant)>,
|
pub(crate) selected_entity: Option<(specs::Entity, std::time::Instant)>,
|
||||||
viewpoint_entity: Option<specs::Entity>,
|
pub(crate) viewpoint_entity: Option<specs::Entity>,
|
||||||
interactable: Option<Interactable>,
|
interactable: Option<Interactable>,
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
mumble_link: SharedLink,
|
mumble_link: SharedLink,
|
||||||
@ -1530,8 +1530,7 @@ impl PlayState for SessionState {
|
|||||||
self.client.borrow_mut().send_chat(msg);
|
self.client.borrow_mut().send_chat(msg);
|
||||||
},
|
},
|
||||||
HudEvent::SendCommand(name, args) => {
|
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)) => {
|
Ok(Some(info)) => {
|
||||||
// TODO: Localise
|
// TODO: Localise
|
||||||
self.hud
|
self.hud
|
||||||
|
Loading…
Reference in New Issue
Block a user