Some checks failed
Native code generation / deterministic (push) Failing after 2m0s
Imaging and meshing gate / native (push) Failing after 4m11s
JPEG 2000 feature / linux (push) Successful in 2m52s
Native Rust workspace compile / compile (push) Failing after 6m15s
Skia feature / linux (push) Has been cancelled
1544 lines
50 KiB
Rust
1544 lines
50 KiB
Rust
//! Pure RLV message parsing and typed protocol values.
|
|
|
|
#![allow(clippy::missing_errors_doc)] // Error variants carry stable source locations.
|
|
#![allow(clippy::missing_panics_doc)] // Parser code contains no intentional panics.
|
|
#![allow(clippy::must_use_candidate)] // Mapped methods preserve their source signatures.
|
|
#![allow(clippy::needless_pass_by_value)] // Owned values mirror mapped C# signatures.
|
|
#![allow(clippy::option_option)] // Nullable out parameters require three states.
|
|
#![allow(clippy::unnecessary_wraps)] // Mapped constructors are fallible at the API boundary.
|
|
#![allow(clippy::cast_possible_truncation)] // Mapped numeric object coercion intentionally narrows to Single.
|
|
|
|
use std::fmt;
|
|
use std::hash::{Hash, Hasher};
|
|
|
|
use libremetaverse_types::compat::{Guid, ImmutableList, Object};
|
|
use libremetaverse_types::{Error, UUID};
|
|
|
|
use crate::{RlvAttachmentPoint, RlvRestrictionType, RlvWearableType};
|
|
|
|
/// Maximum accepted chat/instant-message payload size.
|
|
pub const MAX_RLV_MESSAGE_BYTES: usize = 64 * 1024;
|
|
/// Maximum comma-separated commands accepted in one payload.
|
|
pub const MAX_RLV_COMMANDS: usize = 128;
|
|
|
|
/// Byte location in the original UTF-8 message.
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
pub struct RlvSourceSpan {
|
|
pub start: usize,
|
|
pub end: usize,
|
|
}
|
|
|
|
/// Stable malformed-input categories exposed by the native parser.
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
pub enum RlvParseErrorKind {
|
|
MessageTooLong,
|
|
MissingPrefix,
|
|
TooManyCommands,
|
|
EmptyCommand,
|
|
MissingEquals,
|
|
EmptyBehavior,
|
|
EmptyParameter,
|
|
UnknownAction,
|
|
UnknownRestriction,
|
|
UnknownQuery,
|
|
UnsupportedParameter,
|
|
InvalidOption,
|
|
InvalidNumber,
|
|
InvalidUuid,
|
|
InvalidChannel,
|
|
ZeroQueryChannel,
|
|
}
|
|
|
|
/// A typed parse failure with command index and exact byte span.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct RlvParseError {
|
|
pub kind: RlvParseErrorKind,
|
|
pub command_index: usize,
|
|
pub span: RlvSourceSpan,
|
|
}
|
|
|
|
impl fmt::Display for RlvParseError {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(
|
|
formatter,
|
|
"RLV {:?} at command {} bytes {}..{}",
|
|
self.kind, self.command_index, self.span.start, self.span.end
|
|
)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for RlvParseError {}
|
|
|
|
/// Parsed RLV scalar with protocol type retained.
|
|
#[derive(Clone, Debug)]
|
|
pub enum RlvValue {
|
|
Integer(i32),
|
|
Real(f32),
|
|
Uuid(Guid),
|
|
String(String),
|
|
AttachmentPoint(RlvAttachmentPoint),
|
|
WearableType(RlvWearableType),
|
|
Object(Object),
|
|
}
|
|
|
|
impl PartialEq for RlvValue {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
match (self, other) {
|
|
(Self::Integer(left), Self::Integer(right)) => left == right,
|
|
(Self::Real(left), Self::Real(right)) => left.to_bits() == right.to_bits(),
|
|
(Self::Uuid(left), Self::Uuid(right)) => left == right,
|
|
(Self::String(left), Self::String(right)) => left == right,
|
|
(Self::AttachmentPoint(left), Self::AttachmentPoint(right)) => left == right,
|
|
(Self::WearableType(left), Self::WearableType(right)) => left == right,
|
|
(Self::Object(left), Self::Object(right)) => left == right,
|
|
_ => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Eq for RlvValue {}
|
|
|
|
impl Hash for RlvValue {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
std::mem::discriminant(self).hash(state);
|
|
match self {
|
|
Self::Integer(value) => value.hash(state),
|
|
Self::Real(value) => value.to_bits().hash(state),
|
|
Self::Uuid(value) => value.hash(state),
|
|
Self::String(value) => value.hash(state),
|
|
Self::AttachmentPoint(value) => value.hash(state),
|
|
Self::WearableType(value) => value.hash(state),
|
|
Self::Object(value) => value.hash(state),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for RlvValue {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Integer(value) => value.fmt(formatter),
|
|
Self::Real(value) => value.fmt(formatter),
|
|
Self::Uuid(value) => formatter.write_str(&guid_to_string(*value)),
|
|
Self::String(value) => formatter.write_str(value),
|
|
Self::AttachmentPoint(value) => write!(formatter, "{value:?}"),
|
|
Self::WearableType(value) => write!(formatter, "{value:?}"),
|
|
Self::Object(value) => write!(formatter, "{value:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Restriction add/remove operation selected by the RLV parameter.
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
pub enum RlvRestrictionOperation {
|
|
Add,
|
|
Remove,
|
|
}
|
|
|
|
/// A UUID, wearable, attachment point, folder path, or implicit sender target.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum RlvTarget {
|
|
Sender,
|
|
Uuid(Guid),
|
|
AttachmentPoint(RlvAttachmentPoint),
|
|
WearableType(RlvWearableType),
|
|
FolderPath(String),
|
|
}
|
|
|
|
/// A group selected either by UUID or by its case-preserved name.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum RlvGroupTarget {
|
|
Uuid(Guid),
|
|
Name(String),
|
|
}
|
|
|
|
/// Fully parsed side-effect-free action request.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum RlvAction {
|
|
SetRotation(f32),
|
|
AdjustHeight {
|
|
distance: f32,
|
|
factor: f32,
|
|
delta: f32,
|
|
},
|
|
SetCameraFov(f32),
|
|
Teleport {
|
|
x: f32,
|
|
y: f32,
|
|
z: f32,
|
|
region: Option<String>,
|
|
look_at: Option<f32>,
|
|
},
|
|
Sit(Guid),
|
|
Unsit,
|
|
SitGround,
|
|
RemoveOutfit(RlvTarget),
|
|
DetachMe,
|
|
RemoveAttachment(RlvTarget),
|
|
DetachAll {
|
|
folder_path: String,
|
|
},
|
|
DetachThis {
|
|
target: RlvTarget,
|
|
recursive: bool,
|
|
},
|
|
SetGroup {
|
|
target: RlvGroupTarget,
|
|
role: Option<String>,
|
|
},
|
|
SetDebug {
|
|
name: String,
|
|
value: String,
|
|
},
|
|
SetEnvironment {
|
|
name: String,
|
|
value: String,
|
|
},
|
|
Attach {
|
|
folder_path: String,
|
|
replace: bool,
|
|
recursive: bool,
|
|
},
|
|
AttachThis {
|
|
target: RlvTarget,
|
|
replace: bool,
|
|
recursive: bool,
|
|
},
|
|
}
|
|
|
|
/// Camera value requested by a numeric-channel query.
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
pub enum RlvCameraQuery {
|
|
AvatarDistanceMin,
|
|
AvatarDistanceMax,
|
|
FovMin,
|
|
FovMax,
|
|
ZoomMin,
|
|
CurrentFov,
|
|
}
|
|
|
|
/// Fully parsed side-effect-free query request.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum RlvQuery {
|
|
Version {
|
|
modern: bool,
|
|
},
|
|
VersionNumber {
|
|
include_blacklist: bool,
|
|
},
|
|
Blacklist {
|
|
filter: String,
|
|
},
|
|
Status {
|
|
all_senders: bool,
|
|
filter: String,
|
|
separator: String,
|
|
},
|
|
Camera(RlvCameraQuery),
|
|
SitId,
|
|
Outfit(Option<RlvWearableType>),
|
|
Attachment(Option<RlvAttachmentPoint>),
|
|
Inventory {
|
|
path: String,
|
|
worn: bool,
|
|
},
|
|
FindFolders {
|
|
first_only: bool,
|
|
terms: Vec<String>,
|
|
separator: String,
|
|
},
|
|
Path {
|
|
legacy: bool,
|
|
target: RlvTarget,
|
|
},
|
|
Group,
|
|
Debug(String),
|
|
Environment(String),
|
|
}
|
|
|
|
/// Classified meaning of one syntactically valid RLV command.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum RlvDirective {
|
|
Clear,
|
|
Action(RlvAction),
|
|
Restriction {
|
|
behavior: RlvRestrictionType,
|
|
operation: RlvRestrictionOperation,
|
|
values: Vec<RlvValue>,
|
|
},
|
|
Query {
|
|
channel: i32,
|
|
query: RlvQuery,
|
|
},
|
|
}
|
|
|
|
/// One parsed command, retaining original and normalized token forms.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct RlvCommand {
|
|
pub raw_behavior: String,
|
|
pub behavior: String,
|
|
pub option: String,
|
|
pub raw_parameter: String,
|
|
pub parameter: String,
|
|
pub sender: Guid,
|
|
pub sender_name: String,
|
|
pub source: RlvSourceSpan,
|
|
pub directive: RlvDirective,
|
|
}
|
|
|
|
/// Parsed comma-separated RLV message.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct RlvMessage {
|
|
pub commands: Vec<RlvCommand>,
|
|
}
|
|
|
|
/// Pure parser for RLV chat messages.
|
|
pub struct RlvParser;
|
|
|
|
impl RlvParser {
|
|
pub fn parse_message(
|
|
message: &str,
|
|
sender: Guid,
|
|
sender_name: impl Into<String>,
|
|
) -> Result<RlvMessage, RlvParseError> {
|
|
if message.len() > MAX_RLV_MESSAGE_BYTES {
|
|
return Err(parse_error(
|
|
RlvParseErrorKind::MessageTooLong,
|
|
0,
|
|
0,
|
|
message.len(),
|
|
));
|
|
}
|
|
if !message.starts_with('@') {
|
|
return Err(parse_error(
|
|
RlvParseErrorKind::MissingPrefix,
|
|
0,
|
|
0,
|
|
message.len(),
|
|
));
|
|
}
|
|
let body = &message[1..];
|
|
let count = body.bytes().filter(|byte| *byte == b',').count() + 1;
|
|
if count > MAX_RLV_COMMANDS {
|
|
return Err(parse_error(
|
|
RlvParseErrorKind::TooManyCommands,
|
|
0,
|
|
1,
|
|
message.len(),
|
|
));
|
|
}
|
|
let sender_name = sender_name.into();
|
|
let mut commands = Vec::with_capacity(count);
|
|
let mut relative_start = 0usize;
|
|
for (command_index, raw) in body.split(',').enumerate() {
|
|
let start = 1 + relative_start;
|
|
let end = start + raw.len();
|
|
let span = RlvSourceSpan { start, end };
|
|
commands.push(parse_command(
|
|
raw,
|
|
sender,
|
|
&sender_name,
|
|
command_index,
|
|
span,
|
|
)?);
|
|
relative_start += raw.len() + 1;
|
|
}
|
|
Ok(RlvMessage { commands })
|
|
}
|
|
}
|
|
|
|
fn parse_command(
|
|
raw: &str,
|
|
sender: Guid,
|
|
sender_name: &str,
|
|
command_index: usize,
|
|
span: RlvSourceSpan,
|
|
) -> Result<RlvCommand, RlvParseError> {
|
|
if raw.is_empty() {
|
|
return Err(error_for(
|
|
RlvParseErrorKind::EmptyCommand,
|
|
command_index,
|
|
span,
|
|
));
|
|
}
|
|
if raw.eq_ignore_ascii_case("clear") {
|
|
return Ok(RlvCommand {
|
|
raw_behavior: raw.to_owned(),
|
|
behavior: "clear".to_owned(),
|
|
option: String::new(),
|
|
raw_parameter: String::new(),
|
|
parameter: String::new(),
|
|
sender,
|
|
sender_name: sender_name.to_owned(),
|
|
source: span,
|
|
directive: RlvDirective::Clear,
|
|
});
|
|
}
|
|
let equals = raw
|
|
.find('=')
|
|
.ok_or_else(|| error_for(RlvParseErrorKind::MissingEquals, command_index, span))?;
|
|
let left = &raw[..equals];
|
|
let raw_parameter = &raw[equals + 1..];
|
|
if raw_parameter.is_empty() {
|
|
return Err(error_for(
|
|
RlvParseErrorKind::EmptyParameter,
|
|
command_index,
|
|
span,
|
|
));
|
|
}
|
|
let (raw_behavior, option) = left
|
|
.split_once(':')
|
|
.map_or((left, ""), |(behavior, option)| (behavior, option));
|
|
if raw_behavior.is_empty() || raw_behavior.contains(':') || option.contains('=') {
|
|
return Err(error_for(
|
|
RlvParseErrorKind::EmptyBehavior,
|
|
command_index,
|
|
span,
|
|
));
|
|
}
|
|
let behavior = raw_behavior.to_lowercase();
|
|
let parameter = raw_parameter.to_lowercase();
|
|
let directive = classify(&behavior, option, ¶meter)
|
|
.map_err(|kind| error_for(kind, command_index, span))?;
|
|
Ok(RlvCommand {
|
|
raw_behavior: raw_behavior.to_owned(),
|
|
behavior,
|
|
option: option.to_owned(),
|
|
raw_parameter: raw_parameter.to_owned(),
|
|
parameter,
|
|
sender,
|
|
sender_name: sender_name.to_owned(),
|
|
source: span,
|
|
directive,
|
|
})
|
|
}
|
|
|
|
fn classify(
|
|
behavior: &str,
|
|
option: &str,
|
|
parameter: &str,
|
|
) -> Result<RlvDirective, RlvParseErrorKind> {
|
|
match parameter {
|
|
"force" => parse_action(behavior, option).map(RlvDirective::Action),
|
|
"n" | "add" | "y" | "rem" => {
|
|
let restriction =
|
|
restriction_from_name(behavior).ok_or(RlvParseErrorKind::UnknownRestriction)?;
|
|
let values = parse_restriction_values(restriction, option)?;
|
|
let operation = if matches!(parameter, "n" | "add") {
|
|
RlvRestrictionOperation::Add
|
|
} else {
|
|
RlvRestrictionOperation::Remove
|
|
};
|
|
Ok(RlvDirective::Restriction {
|
|
behavior: restriction,
|
|
operation,
|
|
values,
|
|
})
|
|
}
|
|
_ => {
|
|
let channel = parse_i32(parameter).ok_or(RlvParseErrorKind::UnsupportedParameter)?;
|
|
if channel == 0 {
|
|
return Err(RlvParseErrorKind::ZeroQueryChannel);
|
|
}
|
|
let query = parse_query(behavior, option)?;
|
|
Ok(RlvDirective::Query { channel, query })
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_action(behavior: &str, option: &str) -> Result<RlvAction, RlvParseErrorKind> {
|
|
match behavior {
|
|
"setrot" => Ok(RlvAction::SetRotation(parse_f32(option)?)),
|
|
"adjustheight" => parse_adjust_height(option),
|
|
"setcam_fov" => Ok(RlvAction::SetCameraFov(parse_f32(option)?)),
|
|
"tpto" => parse_teleport(option),
|
|
"sit" => Ok(RlvAction::Sit(parse_guid(option)?)),
|
|
"unsit" => Ok(RlvAction::Unsit),
|
|
"sitground" => Ok(RlvAction::SitGround),
|
|
"remoutfit" => Ok(RlvAction::RemoveOutfit(parse_general_target(option))),
|
|
"detachme" => Ok(RlvAction::DetachMe),
|
|
"remattach" | "detach" => Ok(RlvAction::RemoveAttachment(parse_general_target(option))),
|
|
"detachall" => Ok(RlvAction::DetachAll {
|
|
folder_path: option.to_owned(),
|
|
}),
|
|
"detachthis" => Ok(RlvAction::DetachThis {
|
|
target: parse_this_target(option)?,
|
|
recursive: false,
|
|
}),
|
|
"detachallthis" => Ok(RlvAction::DetachThis {
|
|
target: parse_this_target(option)?,
|
|
recursive: true,
|
|
}),
|
|
"setgroup" => parse_group(option),
|
|
_ if behavior.starts_with("setdebug_") => parse_setting(behavior, option, true),
|
|
_ if behavior.starts_with("setenv_") => parse_setting(behavior, option, false),
|
|
"attach" | "addoutfit" | "attachoverorreplace" => Ok(RlvAction::Attach {
|
|
folder_path: option.to_owned(),
|
|
replace: true,
|
|
recursive: false,
|
|
}),
|
|
"attachall" | "addoutfitall" | "attachalloverorreplace" => Ok(RlvAction::Attach {
|
|
folder_path: option.to_owned(),
|
|
replace: true,
|
|
recursive: true,
|
|
}),
|
|
"attachover" | "addoutfitover" => Ok(RlvAction::Attach {
|
|
folder_path: option.to_owned(),
|
|
replace: false,
|
|
recursive: false,
|
|
}),
|
|
"attachallover" | "addoutfitallover" => Ok(RlvAction::Attach {
|
|
folder_path: option.to_owned(),
|
|
replace: false,
|
|
recursive: true,
|
|
}),
|
|
"attachthis" | "addoutfitthis" | "attachthisoverorreplace" => Ok(RlvAction::AttachThis {
|
|
target: parse_this_target(option)?,
|
|
replace: true,
|
|
recursive: false,
|
|
}),
|
|
"attachallthis" | "addoutfitallthis" | "attachallthisoverorreplace" => {
|
|
Ok(RlvAction::AttachThis {
|
|
target: parse_this_target(option)?,
|
|
replace: true,
|
|
recursive: true,
|
|
})
|
|
}
|
|
"attachthisover" | "addoutfitthisover" => Ok(RlvAction::AttachThis {
|
|
target: parse_this_target(option)?,
|
|
replace: false,
|
|
recursive: false,
|
|
}),
|
|
"attachallthisover" | "addoutfitallthisover" => Ok(RlvAction::AttachThis {
|
|
target: parse_this_target(option)?,
|
|
replace: false,
|
|
recursive: true,
|
|
}),
|
|
_ => Err(RlvParseErrorKind::UnknownAction),
|
|
}
|
|
}
|
|
|
|
fn parse_adjust_height(option: &str) -> Result<RlvAction, RlvParseErrorKind> {
|
|
let parts = nonempty_parts(option);
|
|
let Some(distance) = parts.first().and_then(|value| parse_f32_value(value)) else {
|
|
return Err(RlvParseErrorKind::InvalidNumber);
|
|
};
|
|
let factor = parts
|
|
.get(1)
|
|
.and_then(|value| parse_f32_value(value))
|
|
.unwrap_or(1.0);
|
|
let delta = parts
|
|
.get(2)
|
|
.and_then(|value| parse_f32_value(value))
|
|
.unwrap_or(0.0);
|
|
Ok(RlvAction::AdjustHeight {
|
|
distance,
|
|
factor,
|
|
delta,
|
|
})
|
|
}
|
|
|
|
fn parse_teleport(option: &str) -> Result<RlvAction, RlvParseErrorKind> {
|
|
let parts = nonempty_parts(option);
|
|
let location = parts.first().ok_or(RlvParseErrorKind::InvalidOption)?;
|
|
let location: Vec<_> = location.split('/').collect();
|
|
if !(3..=4).contains(&location.len()) {
|
|
return Err(RlvParseErrorKind::InvalidOption);
|
|
}
|
|
let base = usize::from(location.len() == 4);
|
|
let region = (base == 1).then(|| location[0].to_owned());
|
|
let x = parse_f32(location[base])?;
|
|
let y = parse_f32(location[base + 1])?;
|
|
let z = parse_f32(location[base + 2])?;
|
|
let look_at = parts.get(1).map(|value| parse_f32(value)).transpose()?;
|
|
Ok(RlvAction::Teleport {
|
|
x,
|
|
y,
|
|
z,
|
|
region,
|
|
look_at,
|
|
})
|
|
}
|
|
|
|
fn parse_group(option: &str) -> Result<RlvAction, RlvParseErrorKind> {
|
|
let parts = nonempty_parts(option);
|
|
let name = parts.first().ok_or(RlvParseErrorKind::InvalidOption)?;
|
|
let target = parse_guid(name).map_or_else(
|
|
|_| RlvGroupTarget::Name((*name).to_owned()),
|
|
RlvGroupTarget::Uuid,
|
|
);
|
|
Ok(RlvAction::SetGroup {
|
|
target,
|
|
role: parts.get(1).map(|value| (*value).to_owned()),
|
|
})
|
|
}
|
|
|
|
fn parse_setting(
|
|
behavior: &str,
|
|
option: &str,
|
|
debug: bool,
|
|
) -> Result<RlvAction, RlvParseErrorKind> {
|
|
let name = behavior
|
|
.split_once('_')
|
|
.map(|(_, name)| name)
|
|
.unwrap_or_default();
|
|
if name.is_empty() {
|
|
return Err(RlvParseErrorKind::InvalidOption);
|
|
}
|
|
if debug {
|
|
Ok(RlvAction::SetDebug {
|
|
name: name.to_owned(),
|
|
value: option.to_owned(),
|
|
})
|
|
} else {
|
|
Ok(RlvAction::SetEnvironment {
|
|
name: name.to_owned(),
|
|
value: option.to_owned(),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn parse_general_target(option: &str) -> RlvTarget {
|
|
if option.is_empty() {
|
|
RlvTarget::Sender
|
|
} else if let Ok(value) = parse_guid(option) {
|
|
RlvTarget::Uuid(value)
|
|
} else if let Some(value) = wearable_from_name(option) {
|
|
RlvTarget::WearableType(value)
|
|
} else if let Some(value) = attachment_from_name(option) {
|
|
RlvTarget::AttachmentPoint(value)
|
|
} else {
|
|
RlvTarget::FolderPath(option.to_owned())
|
|
}
|
|
}
|
|
|
|
fn parse_this_target(option: &str) -> Result<RlvTarget, RlvParseErrorKind> {
|
|
let target = parse_general_target(option);
|
|
if matches!(target, RlvTarget::FolderPath(_)) {
|
|
Err(RlvParseErrorKind::InvalidOption)
|
|
} else {
|
|
Ok(target)
|
|
}
|
|
}
|
|
|
|
fn parse_query(behavior: &str, option: &str) -> Result<RlvQuery, RlvParseErrorKind> {
|
|
let query = match behavior {
|
|
"version" => RlvQuery::Version { modern: false },
|
|
"versionnew" => RlvQuery::Version { modern: true },
|
|
"versionnum" => RlvQuery::VersionNumber {
|
|
include_blacklist: false,
|
|
},
|
|
"versionnumbl" => RlvQuery::VersionNumber {
|
|
include_blacklist: true,
|
|
},
|
|
"getblacklist" => RlvQuery::Blacklist {
|
|
filter: option.to_owned(),
|
|
},
|
|
"getstatus" | "getstatusall" => {
|
|
let mut parts = option.split(';');
|
|
let filter = parts.next().unwrap_or_default().to_lowercase();
|
|
let separator = parts.next().unwrap_or("/").to_owned();
|
|
RlvQuery::Status {
|
|
all_senders: behavior == "getstatusall",
|
|
filter,
|
|
separator,
|
|
}
|
|
}
|
|
"getcam_avdistmin" => RlvQuery::Camera(RlvCameraQuery::AvatarDistanceMin),
|
|
"getcam_avdistmax" => RlvQuery::Camera(RlvCameraQuery::AvatarDistanceMax),
|
|
"getcam_fovmin" => RlvQuery::Camera(RlvCameraQuery::FovMin),
|
|
"getcam_fovmax" => RlvQuery::Camera(RlvCameraQuery::FovMax),
|
|
"getcam_zoommin" => RlvQuery::Camera(RlvCameraQuery::ZoomMin),
|
|
"getcam_fov" => RlvQuery::Camera(RlvCameraQuery::CurrentFov),
|
|
"getsitid" => RlvQuery::SitId,
|
|
"getoutfit" => RlvQuery::Outfit(wearable_from_name(option)),
|
|
"getattach" => RlvQuery::Attachment(attachment_from_name(option)),
|
|
"getinv" => RlvQuery::Inventory {
|
|
path: option.to_owned(),
|
|
worn: false,
|
|
},
|
|
"getinvworn" => RlvQuery::Inventory {
|
|
path: option.to_owned(),
|
|
worn: true,
|
|
},
|
|
"findfolder" | "findfolders" => {
|
|
let mut parts = option.split(';');
|
|
let terms = parts
|
|
.next()
|
|
.unwrap_or_default()
|
|
.split("&&")
|
|
.filter(|value| !value.is_empty())
|
|
.map(str::to_owned)
|
|
.collect();
|
|
let separator = parts.next().unwrap_or(",").to_owned();
|
|
RlvQuery::FindFolders {
|
|
first_only: behavior == "findfolder",
|
|
terms,
|
|
separator,
|
|
}
|
|
}
|
|
"getpath" | "getpathnew" => RlvQuery::Path {
|
|
legacy: behavior == "getpath",
|
|
target: parse_query_path_target(option)?,
|
|
},
|
|
"getgroup" => RlvQuery::Group,
|
|
_ if behavior.starts_with("getdebug_") => {
|
|
RlvQuery::Debug(behavior["getdebug_".len()..].to_owned())
|
|
}
|
|
_ if behavior.starts_with("getenv_") => {
|
|
RlvQuery::Environment(behavior["getenv_".len()..].to_owned())
|
|
}
|
|
_ => return Err(RlvParseErrorKind::UnknownQuery),
|
|
};
|
|
Ok(query)
|
|
}
|
|
|
|
fn parse_query_path_target(option: &str) -> Result<RlvTarget, RlvParseErrorKind> {
|
|
let parts = nonempty_parts(option);
|
|
match parts.as_slice() {
|
|
[] => Ok(RlvTarget::Sender),
|
|
[value] => parse_this_target(value),
|
|
_ => Err(RlvParseErrorKind::InvalidOption),
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_lines)] // One exhaustive match documents every protocol option grammar.
|
|
fn parse_restriction_values(
|
|
behavior: RlvRestrictionType,
|
|
option: &str,
|
|
) -> Result<Vec<RlvValue>, RlvParseErrorKind> {
|
|
use RlvRestrictionType as T;
|
|
let args = nonempty_parts(option);
|
|
let first_float = || {
|
|
args.first()
|
|
.ok_or(RlvParseErrorKind::InvalidOption)
|
|
.and_then(|value| parse_f32(value))
|
|
.map(|value| vec![RlvValue::Real(value)])
|
|
};
|
|
let one_int = || {
|
|
one(&args)
|
|
.and_then(|value| parse_i32(value).ok_or(RlvParseErrorKind::InvalidNumber))
|
|
.map(|value| vec![RlvValue::Integer(value)])
|
|
};
|
|
let optional_guid = || {
|
|
optional_one(&args)
|
|
.and_then(|value| value.map(parse_guid).transpose())
|
|
.map(|value| value.into_iter().map(RlvValue::Uuid).collect())
|
|
};
|
|
match behavior {
|
|
T::Notify => {
|
|
let channel = args
|
|
.first()
|
|
.and_then(|value| parse_i32(value))
|
|
.ok_or(RlvParseErrorKind::InvalidChannel)?;
|
|
let mut values = vec![RlvValue::Integer(channel)];
|
|
if args.len() == 2 {
|
|
values.push(RlvValue::String(args[1].to_owned()));
|
|
}
|
|
Ok(values)
|
|
}
|
|
T::CamDrawMin | T::CamDrawMax => {
|
|
let values = first_float()?;
|
|
if matches!(values.first(), Some(RlvValue::Real(value)) if *value < 0.40) {
|
|
Err(RlvParseErrorKind::InvalidOption)
|
|
} else {
|
|
Ok(values)
|
|
}
|
|
}
|
|
T::CamZoomMax
|
|
| T::CamZoomMin
|
|
| T::SetCamFovMin
|
|
| T::SetCamFovMax
|
|
| T::CamDistMax
|
|
| T::SetCamAvDistMax
|
|
| T::CamDistMin
|
|
| T::SetCamAvDistMin
|
|
| T::CamDrawAlphaMin
|
|
| T::CamDrawAlphaMax
|
|
| T::CamAvDist => first_float(),
|
|
T::SitTp | T::FarTouch | T::TouchFar | T::TpLocal => optional_one(&args)
|
|
.and_then(|value| value.map(parse_f32).transpose())
|
|
.map(|value| value.into_iter().map(RlvValue::Real).collect()),
|
|
T::CamDrawColor => {
|
|
if args.len() != 3 {
|
|
return Err(RlvParseErrorKind::InvalidOption);
|
|
}
|
|
args.iter()
|
|
.map(|value| parse_f32(value).map(RlvValue::Real))
|
|
.collect()
|
|
}
|
|
T::RedirChat | T::RedirEmote | T::SendChannelExcept => one_int(),
|
|
T::SendChannel | T::SendChannelSec => optional_one(&args)
|
|
.and_then(|value| {
|
|
value
|
|
.map(|value| parse_i32(value).ok_or(RlvParseErrorKind::InvalidNumber))
|
|
.transpose()
|
|
})
|
|
.map(|value| value.into_iter().map(RlvValue::Integer).collect()),
|
|
T::SendImTo | T::RecvImFrom => one(&args).map(|value| {
|
|
vec![
|
|
parse_guid(value)
|
|
.map_or_else(|_| RlvValue::String(value.to_owned()), RlvValue::Uuid),
|
|
]
|
|
}),
|
|
T::SendIm | T::RecvIm => optional_one(&args).map(|value| {
|
|
value
|
|
.map(|value| {
|
|
parse_guid(value)
|
|
.map_or_else(|_| RlvValue::String(value.to_owned()), RlvValue::Uuid)
|
|
})
|
|
.into_iter()
|
|
.collect()
|
|
}),
|
|
T::Detach | T::AddAttach | T::RemAttach => optional_one(&args)
|
|
.and_then(|value| {
|
|
value
|
|
.map(|value| {
|
|
attachment_from_name(value).ok_or(RlvParseErrorKind::InvalidOption)
|
|
})
|
|
.transpose()
|
|
})
|
|
.map(|value| value.into_iter().map(RlvValue::AttachmentPoint).collect()),
|
|
T::AddOutfit | T::RemOutfit => optional_one(&args)
|
|
.and_then(|value| {
|
|
value
|
|
.map(|value| wearable_from_name(value).ok_or(RlvParseErrorKind::InvalidOption))
|
|
.transpose()
|
|
})
|
|
.map(|value| value.into_iter().map(RlvValue::WearableType).collect()),
|
|
T::DetachThis | T::DetachAllThis | T::AttachThis | T::AttachAllThis => optional_one(&args)
|
|
.map(|value| {
|
|
value
|
|
.map(|value| {
|
|
wearable_from_name(value)
|
|
.map(RlvValue::WearableType)
|
|
.or_else(|| attachment_from_name(value).map(RlvValue::AttachmentPoint))
|
|
.unwrap_or_else(|| RlvValue::String(value.to_owned()))
|
|
})
|
|
.into_iter()
|
|
.collect()
|
|
}),
|
|
T::DetachThisExcept
|
|
| T::DetachAllThisExcept
|
|
| T::AttachThisExcept
|
|
| T::AttachAllThisExcept => {
|
|
one(&args).map(|value| vec![RlvValue::String(value.to_owned())])
|
|
}
|
|
T::CamTextures
|
|
| T::SetCamTextures
|
|
| T::RecvChat
|
|
| T::RecvEmote
|
|
| T::StartIm
|
|
| T::TpLure
|
|
| T::AcceptTp
|
|
| T::AcceptTpRequest
|
|
| T::TpRequest
|
|
| T::Edit
|
|
| T::Share
|
|
| T::TouchWorld
|
|
| T::TouchAttachOther
|
|
| T::TouchHud
|
|
| T::ShowNames
|
|
| T::ShowNamesSec
|
|
| T::ShowNameTags => optional_guid(),
|
|
T::RecvChatFrom
|
|
| T::RecvEmoteFrom
|
|
| T::StartImTo
|
|
| T::EditObj
|
|
| T::TouchThis
|
|
| T::ShowHoverText => one(&args)
|
|
.and_then(parse_guid)
|
|
.map(|value| vec![RlvValue::Uuid(value)]),
|
|
_ if restriction_takes_no_values(behavior) => {
|
|
if args.is_empty() {
|
|
Ok(Vec::new())
|
|
} else {
|
|
Err(RlvParseErrorKind::InvalidOption)
|
|
}
|
|
}
|
|
_ => Err(RlvParseErrorKind::InvalidOption),
|
|
}
|
|
}
|
|
|
|
fn restriction_takes_no_values(value: RlvRestrictionType) -> bool {
|
|
use RlvRestrictionType as T;
|
|
matches!(
|
|
value,
|
|
T::Permissive
|
|
| T::SendChat
|
|
| T::ChatShout
|
|
| T::ChatNormal
|
|
| T::ChatWhisper
|
|
| T::Emote
|
|
| T::RecvChatSec
|
|
| T::RecvEmoteSec
|
|
| T::SendGesture
|
|
| T::SendImSec
|
|
| T::RecvImSec
|
|
| T::TpLureSec
|
|
| T::TpRequestSec
|
|
| T::ShareSec
|
|
| T::Fly
|
|
| T::Jump
|
|
| T::TempRun
|
|
| T::AlwaysRun
|
|
| T::CamUnlock
|
|
| T::SetCamUnlock
|
|
| T::TpLm
|
|
| T::TpLoc
|
|
| T::StandTp
|
|
| T::ShowInv
|
|
| T::ViewNote
|
|
| T::ViewScript
|
|
| T::ViewTexture
|
|
| T::Unsit
|
|
| T::Sit
|
|
| T::DefaultWear
|
|
| T::SetGroup
|
|
| T::SetDebug
|
|
| T::SetEnv
|
|
| T::AllowIdle
|
|
| T::ShowWorldMap
|
|
| T::ShowMiniMap
|
|
| T::ShowLoc
|
|
| T::ShowNearby
|
|
| T::EditWorld
|
|
| T::EditAttach
|
|
| T::Rez
|
|
| T::DenyPermission
|
|
| T::AcceptPermission
|
|
| T::UnsharedWear
|
|
| T::UnsharedUnwear
|
|
| T::SharedWear
|
|
| T::SharedUnwear
|
|
| T::TouchAll
|
|
| T::TouchMe
|
|
| T::TouchAttach
|
|
| T::TouchAttachSelf
|
|
| T::Interact
|
|
| T::ShowHoverTextAll
|
|
| T::ShowHoverTextHud
|
|
| T::ShowHoverTextWorld
|
|
)
|
|
}
|
|
|
|
/// Native mapped RLV helpers.
|
|
pub struct RlvCommon;
|
|
|
|
impl RlvCommon {
|
|
pub(crate) fn native_try_get_attachment_point_from_item_name(
|
|
item_name: String,
|
|
output: &mut Option<Option<RlvAttachmentPoint>>,
|
|
) -> bool {
|
|
*output = None;
|
|
let mut remaining = item_name.as_str();
|
|
let mut found = None;
|
|
while let Some(open) = remaining.find('(') {
|
|
let after_open = &remaining[open + 1..];
|
|
let Some(close) = after_open.find(')') else {
|
|
break;
|
|
};
|
|
if let Some(point) = attachment_from_name(&after_open[..close]) {
|
|
found = Some(point);
|
|
}
|
|
remaining = &after_open[close + 1..];
|
|
}
|
|
if let Some(point) = found {
|
|
*output = Some(Some(point));
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Native mapped immutable restriction value.
|
|
#[derive(Clone, Debug)]
|
|
pub struct RlvRestriction {
|
|
behavior: RlvRestrictionType,
|
|
original_behavior: RlvRestrictionType,
|
|
sender: Guid,
|
|
sender_name: String,
|
|
args: ImmutableList<Object>,
|
|
values: Vec<RlvValue>,
|
|
}
|
|
|
|
impl RlvRestriction {
|
|
pub(crate) fn native_new(
|
|
behavior: RlvRestrictionType,
|
|
sender: Guid,
|
|
sender_name: String,
|
|
args: Vec<Object>,
|
|
) -> Result<Self, Error> {
|
|
let values = args
|
|
.iter()
|
|
.cloned()
|
|
.map(|value| value_from_object(behavior, value))
|
|
.collect();
|
|
Ok(Self::from_parts(
|
|
behavior,
|
|
sender,
|
|
sender_name,
|
|
args.into(),
|
|
values,
|
|
))
|
|
}
|
|
|
|
pub fn from_values(
|
|
original_behavior: RlvRestrictionType,
|
|
sender: Guid,
|
|
sender_name: String,
|
|
values: Vec<RlvValue>,
|
|
) -> Self {
|
|
let args = values
|
|
.iter()
|
|
.cloned()
|
|
.map(value_to_object)
|
|
.collect::<Vec<_>>()
|
|
.into();
|
|
Self::from_parts(original_behavior, sender, sender_name, args, values)
|
|
}
|
|
|
|
fn from_parts(
|
|
original_behavior: RlvRestrictionType,
|
|
sender: Guid,
|
|
sender_name: String,
|
|
args: ImmutableList<Object>,
|
|
values: Vec<RlvValue>,
|
|
) -> Self {
|
|
let mut behavior = real_restriction(original_behavior);
|
|
if behavior == RlvRestrictionType::SendChannelSec && args.len() == 1 {
|
|
behavior = RlvRestrictionType::SendChannel;
|
|
} else if behavior == RlvRestrictionType::ShowNamesSec && args.len() == 1 {
|
|
behavior = RlvRestrictionType::ShowNames;
|
|
}
|
|
Self {
|
|
behavior,
|
|
original_behavior,
|
|
sender,
|
|
sender_name,
|
|
args,
|
|
values,
|
|
}
|
|
}
|
|
|
|
pub fn values(&self) -> &[RlvValue] {
|
|
&self.values
|
|
}
|
|
|
|
pub(crate) fn native_equals(&self, obj: Option<Object>) -> bool {
|
|
obj.as_ref().and_then(Object::downcast_ref::<Self>) == Some(self)
|
|
}
|
|
|
|
pub(crate) fn native_get_hash_code(&self) -> i32 {
|
|
let mut hash = StableHasher::default();
|
|
self.hash(&mut hash);
|
|
let bytes = hash.finish().to_le_bytes();
|
|
i32::from_le_bytes([
|
|
bytes[0] ^ bytes[4],
|
|
bytes[1] ^ bytes[5],
|
|
bytes[2] ^ bytes[6],
|
|
bytes[3] ^ bytes[7],
|
|
])
|
|
}
|
|
|
|
pub(crate) fn native_to_string(&self) -> String {
|
|
let args = self
|
|
.args
|
|
.iter()
|
|
.map(object_to_string)
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
format!(
|
|
"RlvRestriction: Behavior={:?} SenderName=\"{}\" Args=[{}]",
|
|
self.behavior, self.sender_name, args
|
|
)
|
|
}
|
|
|
|
pub(crate) fn native_args(&self) -> ImmutableList<Object> {
|
|
self.args.clone()
|
|
}
|
|
pub(crate) const fn native_behavior(&self) -> RlvRestrictionType {
|
|
self.behavior
|
|
}
|
|
pub(crate) fn native_is_exception(&self) -> bool {
|
|
is_exception(self.behavior, self.args.len())
|
|
}
|
|
pub(crate) const fn native_original_behavior(&self) -> RlvRestrictionType {
|
|
self.original_behavior
|
|
}
|
|
pub(crate) const fn native_sender(&self) -> Guid {
|
|
self.sender
|
|
}
|
|
pub(crate) fn native_sender_name(&self) -> String {
|
|
self.sender_name.clone()
|
|
}
|
|
}
|
|
|
|
impl PartialEq for RlvRestriction {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.behavior == other.behavior && self.sender == other.sender && self.args == other.args
|
|
}
|
|
}
|
|
impl Eq for RlvRestriction {}
|
|
impl Hash for RlvRestriction {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
self.behavior.hash(state);
|
|
self.sender.hash(state);
|
|
self.args.hash(state);
|
|
}
|
|
}
|
|
|
|
fn is_exception(behavior: RlvRestrictionType, value_count: usize) -> bool {
|
|
use RlvRestrictionType as T;
|
|
matches!(
|
|
behavior,
|
|
T::DetachThisExcept | T::DetachAllThisExcept | T::AttachThisExcept | T::AttachAllThisExcept
|
|
) || (value_count > 0
|
|
&& matches!(
|
|
behavior,
|
|
T::RecvEmote
|
|
| T::RecvChat
|
|
| T::SendIm
|
|
| T::StartIm
|
|
| T::RecvIm
|
|
| T::SendChannel
|
|
| T::TpRequest
|
|
| T::TpLure
|
|
| T::Edit
|
|
| T::Share
|
|
| T::TouchWorld
|
|
| T::ShowNamesSec
|
|
| T::ShowNames
|
|
| T::ShowNameTags
|
|
| T::AcceptTp
|
|
| T::AcceptTpRequest
|
|
))
|
|
}
|
|
|
|
const fn real_restriction(value: RlvRestrictionType) -> RlvRestrictionType {
|
|
match value {
|
|
RlvRestrictionType::CamDistMax => RlvRestrictionType::SetCamAvDistMax,
|
|
RlvRestrictionType::CamDistMin => RlvRestrictionType::SetCamAvDistMin,
|
|
RlvRestrictionType::CamUnlock => RlvRestrictionType::SetCamUnlock,
|
|
RlvRestrictionType::CamTextures => RlvRestrictionType::SetCamTextures,
|
|
RlvRestrictionType::FarTouch => RlvRestrictionType::TouchFar,
|
|
other => other,
|
|
}
|
|
}
|
|
|
|
macro_rules! restriction_table {
|
|
($macro:ident) => {
|
|
$macro! {
|
|
"notify" => Notify, "permissive" => Permissive, "fly" => Fly, "jump" => Jump,
|
|
"temprun" => TempRun, "alwaysrun" => AlwaysRun, "camzoommax" => CamZoomMax,
|
|
"camzoommin" => CamZoomMin, "camdrawmin" => CamDrawMin, "camdrawmax" => CamDrawMax,
|
|
"setcam_fovmin" => SetCamFovMin, "setcam_fovmax" => SetCamFovMax,
|
|
"camdistmax" => CamDistMax, "camdistmin" => CamDistMin,
|
|
"camdrawalphamin" => CamDrawAlphaMin, "camdrawalphamax" => CamDrawAlphaMax,
|
|
"setcam_avdistmax" => SetCamAvDistMax, "setcam_avdistmin" => SetCamAvDistMin,
|
|
"camdrawcolor" => CamDrawColor, "camunlock" => CamUnlock,
|
|
"setcam_unlock" => SetCamUnlock, "camavdist" => CamAvDist,
|
|
"camtextures" => CamTextures, "setcam_textures" => SetCamTextures,
|
|
"sendchat" => SendChat, "chatshout" => ChatShout, "chatnormal" => ChatNormal,
|
|
"chatwhisper" => ChatWhisper, "redirchat" => RedirChat, "recvchat" => RecvChat,
|
|
"recvchat_sec" => RecvChatSec, "recvchatfrom" => RecvChatFrom,
|
|
"sendgesture" => SendGesture, "emote" => Emote, "rediremote" => RedirEmote,
|
|
"recvemote" => RecvEmote, "recvemotefrom" => RecvEmoteFrom,
|
|
"recvemote_sec" => RecvEmoteSec, "sendchannel" => SendChannel,
|
|
"sendchannel_sec" => SendChannelSec, "sendchannel_except" => SendChannelExcept,
|
|
"sendim" => SendIm, "sendim_sec" => SendImSec, "sendimto" => SendImTo,
|
|
"startim" => StartIm, "startimto" => StartImTo, "recvim" => RecvIm,
|
|
"recvim_sec" => RecvImSec, "recvimfrom" => RecvImFrom, "tplocal" => TpLocal,
|
|
"tplm" => TpLm, "tploc" => TpLoc, "tplure" => TpLure, "tplure_sec" => TpLureSec,
|
|
"sittp" => SitTp, "standtp" => StandTp, "accepttp" => AcceptTp,
|
|
"accepttprequest" => AcceptTpRequest, "tprequest" => TpRequest,
|
|
"tprequest_sec" => TpRequestSec, "showinv" => ShowInv, "viewnote" => ViewNote,
|
|
"viewscript" => ViewScript, "viewtexture" => ViewTexture, "edit" => Edit,
|
|
"rez" => Rez, "editobj" => EditObj, "editworld" => EditWorld,
|
|
"editattach" => EditAttach, "share" => Share, "share_sec" => ShareSec,
|
|
"unsit" => Unsit, "sit" => Sit, "detach" => Detach, "addattach" => AddAttach,
|
|
"remattach" => RemAttach, "defaultwear" => DefaultWear, "addoutfit" => AddOutfit,
|
|
"remoutfit" => RemOutfit, "acceptpermission" => AcceptPermission,
|
|
"denypermission" => DenyPermission, "unsharedwear" => UnsharedWear,
|
|
"unsharedunwear" => UnsharedUnwear, "sharedwear" => SharedWear,
|
|
"sharedunwear" => SharedUnwear, "detachthis" => DetachThis,
|
|
"detachallthis" => DetachAllThis, "attachthis" => AttachThis,
|
|
"attachallthis" => AttachAllThis, "detachthis_except" => DetachThisExcept,
|
|
"detachallthis_except" => DetachAllThisExcept, "attachthis_except" => AttachThisExcept,
|
|
"attachallthis_except" => AttachAllThisExcept, "fartouch" => FarTouch,
|
|
"touchfar" => TouchFar, "touchall" => TouchAll, "touchworld" => TouchWorld,
|
|
"touchthis" => TouchThis, "touchme" => TouchMe, "touchattach" => TouchAttach,
|
|
"touchattachself" => TouchAttachSelf, "touchattachother" => TouchAttachOther,
|
|
"touchhud" => TouchHud, "interact" => Interact, "showworldmap" => ShowWorldMap,
|
|
"showminimap" => ShowMiniMap, "showloc" => ShowLoc, "shownames" => ShowNames,
|
|
"shownames_sec" => ShowNamesSec, "shownametags" => ShowNameTags,
|
|
"shownearby" => ShowNearby, "showhovertextall" => ShowHoverTextAll,
|
|
"showhovertext" => ShowHoverText, "showhovertexthud" => ShowHoverTextHud,
|
|
"showhovertextworld" => ShowHoverTextWorld, "setgroup" => SetGroup,
|
|
"setdebug" => SetDebug, "setenv" => SetEnv, "allowidle" => AllowIdle
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! make_restriction_lookup {
|
|
($($name:literal => $variant:ident),+ $(,)?) => {
|
|
/// Complete pinned behavior-name table in protocol order.
|
|
pub const RLV_RESTRICTION_NAMES: &[(&str, RlvRestrictionType)] = &[
|
|
$(($name, RlvRestrictionType::$variant),)+
|
|
];
|
|
|
|
/// Maps the exact RLV behavior spelling to its typed restriction.
|
|
pub fn restriction_from_name(name: &str) -> Option<RlvRestrictionType> {
|
|
match name.to_ascii_lowercase().as_str() {
|
|
$($name => Some(RlvRestrictionType::$variant),)+
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Returns the canonical protocol spelling for a restriction.
|
|
pub const fn restriction_name(value: RlvRestrictionType) -> &'static str {
|
|
match value { $(RlvRestrictionType::$variant => $name,)+ }
|
|
}
|
|
};
|
|
}
|
|
restriction_table!(make_restriction_lookup);
|
|
|
|
/// Maps a case-insensitive attachment alias without trimming or punctuation changes.
|
|
pub fn attachment_from_name(name: &str) -> Option<RlvAttachmentPoint> {
|
|
use RlvAttachmentPoint as P;
|
|
Some(match name.to_ascii_lowercase().as_str() {
|
|
"none" => P::Default,
|
|
"chest" => P::Chest,
|
|
"skull" => P::Skull,
|
|
"left shoulder" => P::LeftShoulder,
|
|
"right shoulder" => P::RightShoulder,
|
|
"left hand" => P::LeftHand,
|
|
"right hand" => P::RightHand,
|
|
"left foot" => P::LeftFoot,
|
|
"right foot" => P::RightFoot,
|
|
"spine" => P::Spine,
|
|
"pelvis" => P::Pelvis,
|
|
"mouth" => P::Mouth,
|
|
"chin" => P::Chin,
|
|
"left ear" => P::LeftEar,
|
|
"right ear" => P::RightEar,
|
|
"left eyeball" => P::LeftEyeball,
|
|
"right eyeball" => P::RightEyeball,
|
|
"nose" => P::Nose,
|
|
"r upper arm" => P::RightUpperArm,
|
|
"r forearm" => P::RightForearm,
|
|
"l upper arm" => P::LeftUpperArm,
|
|
"l forearm" => P::LeftForearm,
|
|
"right hip" => P::RightHip,
|
|
"r upper leg" => P::RightUpperLeg,
|
|
"r lower leg" => P::RightLowerLeg,
|
|
"left hip" => P::LeftHip,
|
|
"l upper leg" => P::LeftUpperLeg,
|
|
"l lower leg" => P::LeftLowerLeg,
|
|
"stomach" => P::Stomach,
|
|
"left pec" => P::LeftPec,
|
|
"right pec" => P::RightPec,
|
|
"center 2" => P::HUDCenter2,
|
|
"top right" => P::HUDTopRight,
|
|
"top" => P::HUDTop,
|
|
"top left" => P::HUDTopLeft,
|
|
"center" => P::HUDCenter,
|
|
"bottom left" => P::HUDBottomLeft,
|
|
"bottom" => P::HUDBottom,
|
|
"bottom right" => P::HUDBottomRight,
|
|
"neck" => P::Neck,
|
|
"root" | "avatar center" => P::AvatarCenter,
|
|
"left ring finger" => P::LeftHandRing,
|
|
"right ring finger" => P::RightHandRing,
|
|
"tail base" => P::TailBase,
|
|
"tail tip" => P::TailTip,
|
|
"left wing" => P::LeftWing,
|
|
"right wing" => P::RightWing,
|
|
"jaw" => P::Jaw,
|
|
"alt left ear" => P::AltLeftEar,
|
|
"alt right ear" => P::AltRightEar,
|
|
"alt left eye" => P::AltLeftEye,
|
|
"alt right eye" => P::AltRightEye,
|
|
"tongue" => P::Tongue,
|
|
"groin" => P::Groin,
|
|
"left hind foot" => P::LeftHindFoot,
|
|
"right hind foot" => P::RightHindFoot,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
/// Maps a case-insensitive wearable alias without trimming.
|
|
pub fn wearable_from_name(name: &str) -> Option<RlvWearableType> {
|
|
use RlvWearableType as W;
|
|
Some(match name.to_ascii_lowercase().as_str() {
|
|
"gloves" => W::Gloves,
|
|
"jacket" => W::Jacket,
|
|
"pants" => W::Pants,
|
|
"shirt" => W::Shirt,
|
|
"shoes" => W::Shoes,
|
|
"skirt" => W::Skirt,
|
|
"socks" => W::Socks,
|
|
"underpants" => W::Underpants,
|
|
"undershirt" => W::Undershirt,
|
|
"skin" => W::Skin,
|
|
"eyes" => W::Eyes,
|
|
"hair" => W::Hair,
|
|
"shape" => W::Shape,
|
|
"alpha" => W::Alpha,
|
|
"tattoo" => W::Tattoo,
|
|
"physics" => W::Physics,
|
|
"universal" => W::Universal,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
fn one<'a>(args: &[&'a str]) -> Result<&'a str, RlvParseErrorKind> {
|
|
if let [value] = args {
|
|
Ok(value)
|
|
} else {
|
|
Err(RlvParseErrorKind::InvalidOption)
|
|
}
|
|
}
|
|
|
|
fn optional_one<'a>(args: &[&'a str]) -> Result<Option<&'a str>, RlvParseErrorKind> {
|
|
match args {
|
|
[] => Ok(None),
|
|
[value] => Ok(Some(value)),
|
|
_ => Err(RlvParseErrorKind::InvalidOption),
|
|
}
|
|
}
|
|
|
|
fn nonempty_parts(value: &str) -> Vec<&str> {
|
|
value.split(';').filter(|part| !part.is_empty()).collect()
|
|
}
|
|
|
|
fn parse_f32(value: &str) -> Result<f32, RlvParseErrorKind> {
|
|
parse_f32_value(value).ok_or(RlvParseErrorKind::InvalidNumber)
|
|
}
|
|
|
|
fn parse_f32_value(value: &str) -> Option<f32> {
|
|
value.trim().parse::<f32>().ok()
|
|
}
|
|
|
|
fn parse_i32(value: &str) -> Option<i32> {
|
|
value.trim().parse::<i32>().ok()
|
|
}
|
|
|
|
fn parse_guid(value: &str) -> Result<Guid, RlvParseErrorKind> {
|
|
UUID::parse(value.to_owned())
|
|
.map(|value| value.guid())
|
|
.map_err(|_| RlvParseErrorKind::InvalidUuid)
|
|
}
|
|
|
|
fn guid_to_string(value: Guid) -> String {
|
|
UUID::new_with_guid(value).map_or_else(
|
|
|_| "00000000-0000-0000-0000-000000000000".to_owned(),
|
|
|value| value.to_string(),
|
|
)
|
|
}
|
|
|
|
fn value_to_object(value: RlvValue) -> Object {
|
|
match value {
|
|
RlvValue::Integer(value) => Object::Integer(value),
|
|
RlvValue::Real(value) => Object::Real(f64::from(value)),
|
|
RlvValue::Uuid(value) => UUID::new_with_guid(value).map_or(Object::Undefined, Object::UUID),
|
|
RlvValue::String(value) => Object::String(value),
|
|
RlvValue::AttachmentPoint(value) => Object::Integer(value as i32),
|
|
RlvValue::WearableType(value) => Object::Integer(value as i32),
|
|
RlvValue::Object(value) => value,
|
|
}
|
|
}
|
|
|
|
fn object_to_string(value: &Object) -> String {
|
|
match value {
|
|
Object::Undefined => String::new(),
|
|
Object::Boolean(value) => value.to_string(),
|
|
Object::Integer(value) => value.to_string(),
|
|
Object::UInteger(value) => value.to_string(),
|
|
Object::Long(value) => value.to_string(),
|
|
Object::ULong(value) => value.to_string(),
|
|
Object::Real(value) => value.to_string(),
|
|
Object::String(value) => value.clone(),
|
|
Object::UUID(value) => value.to_string(),
|
|
Object::Opaque(value) => format!("{value:?}"),
|
|
other => format!("{other:?}"),
|
|
}
|
|
}
|
|
|
|
fn value_from_object(behavior: RlvRestrictionType, value: Object) -> RlvValue {
|
|
if let Some(value) = value.downcast_ref::<RlvAttachmentPoint>() {
|
|
return RlvValue::AttachmentPoint(*value);
|
|
}
|
|
if let Some(value) = value.downcast_ref::<RlvWearableType>() {
|
|
return RlvValue::WearableType(*value);
|
|
}
|
|
match value {
|
|
Object::Integer(value)
|
|
if matches!(
|
|
behavior,
|
|
RlvRestrictionType::Detach
|
|
| RlvRestrictionType::AddAttach
|
|
| RlvRestrictionType::RemAttach
|
|
) =>
|
|
{
|
|
attachment_from_discriminant(value)
|
|
.map_or(RlvValue::Integer(value), RlvValue::AttachmentPoint)
|
|
}
|
|
Object::Integer(value)
|
|
if matches!(
|
|
behavior,
|
|
RlvRestrictionType::AddOutfit | RlvRestrictionType::RemOutfit
|
|
) =>
|
|
{
|
|
wearable_from_discriminant(value)
|
|
.map_or(RlvValue::Integer(value), RlvValue::WearableType)
|
|
}
|
|
Object::Integer(value) => RlvValue::Integer(value),
|
|
Object::Real(value) => RlvValue::Real(value as f32),
|
|
Object::UUID(value) => RlvValue::Uuid(value.guid()),
|
|
Object::String(value) => RlvValue::String(value),
|
|
other => RlvValue::Object(other),
|
|
}
|
|
}
|
|
|
|
fn attachment_from_discriminant(value: i32) -> Option<RlvAttachmentPoint> {
|
|
(0..=55).find_map(|candidate| {
|
|
let point = attachment_from_name(restriction_attachment_name(candidate)?)?;
|
|
(point as i32 == value).then_some(point)
|
|
})
|
|
}
|
|
|
|
fn restriction_attachment_name(value: i32) -> Option<&'static str> {
|
|
const NAMES: [&str; 56] = [
|
|
"none",
|
|
"chest",
|
|
"skull",
|
|
"left shoulder",
|
|
"right shoulder",
|
|
"left hand",
|
|
"right hand",
|
|
"left foot",
|
|
"right foot",
|
|
"spine",
|
|
"pelvis",
|
|
"mouth",
|
|
"chin",
|
|
"left ear",
|
|
"right ear",
|
|
"left eyeball",
|
|
"right eyeball",
|
|
"nose",
|
|
"r upper arm",
|
|
"r forearm",
|
|
"l upper arm",
|
|
"l forearm",
|
|
"right hip",
|
|
"r upper leg",
|
|
"r lower leg",
|
|
"left hip",
|
|
"l upper leg",
|
|
"l lower leg",
|
|
"stomach",
|
|
"left pec",
|
|
"right pec",
|
|
"center 2",
|
|
"top right",
|
|
"top",
|
|
"top left",
|
|
"center",
|
|
"bottom left",
|
|
"bottom",
|
|
"bottom right",
|
|
"neck",
|
|
"avatar center",
|
|
"left ring finger",
|
|
"right ring finger",
|
|
"tail base",
|
|
"tail tip",
|
|
"left wing",
|
|
"right wing",
|
|
"jaw",
|
|
"alt left ear",
|
|
"alt right ear",
|
|
"alt left eye",
|
|
"alt right eye",
|
|
"tongue",
|
|
"groin",
|
|
"left hind foot",
|
|
"right hind foot",
|
|
];
|
|
usize::try_from(value)
|
|
.ok()
|
|
.and_then(|index| NAMES.get(index).copied())
|
|
}
|
|
|
|
fn wearable_from_discriminant(value: i32) -> Option<RlvWearableType> {
|
|
[
|
|
RlvWearableType::Shape,
|
|
RlvWearableType::Skin,
|
|
RlvWearableType::Hair,
|
|
RlvWearableType::Eyes,
|
|
RlvWearableType::Shirt,
|
|
RlvWearableType::Pants,
|
|
RlvWearableType::Shoes,
|
|
RlvWearableType::Socks,
|
|
RlvWearableType::Jacket,
|
|
RlvWearableType::Gloves,
|
|
RlvWearableType::Undershirt,
|
|
RlvWearableType::Underpants,
|
|
RlvWearableType::Skirt,
|
|
RlvWearableType::Alpha,
|
|
RlvWearableType::Tattoo,
|
|
RlvWearableType::Physics,
|
|
RlvWearableType::Universal,
|
|
RlvWearableType::Invalid,
|
|
]
|
|
.into_iter()
|
|
.find(|item| *item as i32 == value)
|
|
}
|
|
|
|
const fn parse_error(
|
|
kind: RlvParseErrorKind,
|
|
command_index: usize,
|
|
start: usize,
|
|
end: usize,
|
|
) -> RlvParseError {
|
|
RlvParseError {
|
|
kind,
|
|
command_index,
|
|
span: RlvSourceSpan { start, end },
|
|
}
|
|
}
|
|
|
|
const fn error_for(
|
|
kind: RlvParseErrorKind,
|
|
command_index: usize,
|
|
span: RlvSourceSpan,
|
|
) -> RlvParseError {
|
|
RlvParseError {
|
|
kind,
|
|
command_index,
|
|
span,
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct StableHasher(u64);
|
|
impl Hasher for StableHasher {
|
|
fn finish(&self) -> u64 {
|
|
self.0
|
|
}
|
|
fn write(&mut self, bytes: &[u8]) {
|
|
let mut hash = if self.0 == 0 {
|
|
0xcbf2_9ce4_8422_2325
|
|
} else {
|
|
self.0
|
|
};
|
|
for byte in bytes {
|
|
hash ^= u64::from(*byte);
|
|
hash = hash.wrapping_mul(0x1000_0000_01b3);
|
|
}
|
|
self.0 = hash;
|
|
}
|
|
}
|