Complete first release candidate audit (#107)
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
This commit is contained in:
1 parent
dceb394378
commit
c9a1170a27
140 files changed
+82175
-27179
No files matched your search
@@ -17,12 +17,13 @@ use crate::messages::linden::{
|
||||
};
|
||||
use crate::{
|
||||
AgentAccessEventArgs, AgentDataReplyEventArgs, AgentExperiencesEventArgs,
|
||||
AgentPreferencesEventArgs, AnimationsChangedEventArgs, AvatarRenderInfoEventArgs,
|
||||
BalanceEventArgs, ChatEventArgs, ChatSessionMemberAddedEventArgs,
|
||||
AgentPreferencesEventArgs, AlertMessageEventArgs, AnimationsChangedEventArgs,
|
||||
AvatarRenderInfoEventArgs, BalanceEventArgs, ChatEventArgs, ChatSessionMemberAddedEventArgs,
|
||||
ChatSessionMemberLeftEventArgs, ExperienceInfoEventArgs, ExperiencePreferencesEventArgs,
|
||||
GroupChatJoinedEventArgs, MoneyBalanceReplyEventArgs, NavMeshStatusUpdateEventArgs,
|
||||
ProductInfoEventArgs, RegionExperiencesEventArgs, SetDisplayNameReplyEventArgs,
|
||||
ViewerBenefitsEventArgs,
|
||||
GroupChatJoinedEventArgs, LoadUrlEventArgs, MeanCollisionEventArgs, MoneyBalanceReplyEventArgs,
|
||||
NavMeshStatusUpdateEventArgs, ProductInfoEventArgs, RegionExperiencesEventArgs,
|
||||
ScriptControlEventArgs, ScriptDialogEventArgs, ScriptQuestionEventArgs,
|
||||
ScriptSensorReplyEventArgs, SetDisplayNameReplyEventArgs, ViewerBenefitsEventArgs,
|
||||
};
|
||||
use crate::{
|
||||
AgentManagerAgentMovement, ChatSessionMember, ChatType, EffectType, Error, GridClient,
|
||||
@@ -31,11 +32,11 @@ use crate::{
|
||||
};
|
||||
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
||||
use libremetaverse_types::compat::{EventHandler, Subscription, Uri};
|
||||
use libremetaverse_types::{Color4, UUID, Vector3, Vector3d};
|
||||
use libremetaverse_types::{Color4, UUID, Vector3, Vector3d, Vector4};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fmt;
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock, RwLock};
|
||||
use std::time::{Duration, Instant, UNIX_EPOCH};
|
||||
|
||||
@@ -317,8 +318,17 @@ pub(crate) struct AgentManagerInner {
|
||||
disposed: AtomicBool,
|
||||
agent_id: RwLock<UUID>,
|
||||
session_id: RwLock<UUID>,
|
||||
secure_session_id: RwLock<UUID>,
|
||||
first_name: RwLock<String>,
|
||||
last_name: RwLock<String>,
|
||||
start_location: RwLock<String>,
|
||||
look_at: RwLock<Vector3>,
|
||||
home_region_handle: RwLock<u64>,
|
||||
home_look_at: RwLock<Vector3>,
|
||||
local_id: RwLock<u32>,
|
||||
collision_plane: RwLock<Vector4>,
|
||||
health: RwLock<f32>,
|
||||
height_width_generation: AtomicU32,
|
||||
active_group: RwLock<UUID>,
|
||||
active_group_powers: RwLock<crate::GroupPowers>,
|
||||
agent_access: RwLock<String>,
|
||||
@@ -344,6 +354,7 @@ pub(crate) struct AgentManagerInner {
|
||||
network: Mutex<Option<crate::NetworkManager>>,
|
||||
raw_packet_subscription: Mutex<Option<Subscription>>,
|
||||
caps_subscriptions: Mutex<Vec<Subscription>>,
|
||||
login_callback: Mutex<Option<crate::NetworkManagerLoginResponseCallback>>,
|
||||
movement: OnceLock<Arc<crate::agent_movement::AgentMovementRuntime>>,
|
||||
chat_from_simulator: EventRegistry<ChatEventArgs>,
|
||||
money_balance: EventRegistry<BalanceEventArgs>,
|
||||
@@ -362,6 +373,13 @@ pub(crate) struct AgentManagerInner {
|
||||
set_display_name_reply: EventRegistry<SetDisplayNameReplyEventArgs>,
|
||||
nav_mesh_status_update: EventRegistry<NavMeshStatusUpdateEventArgs>,
|
||||
mute_list_updated: EventRegistry<()>,
|
||||
alert_message: EventRegistry<AlertMessageEventArgs>,
|
||||
load_url: EventRegistry<LoadUrlEventArgs>,
|
||||
mean_collision: EventRegistry<MeanCollisionEventArgs>,
|
||||
script_control_change: EventRegistry<ScriptControlEventArgs>,
|
||||
script_dialog: EventRegistry<ScriptDialogEventArgs>,
|
||||
script_question: EventRegistry<ScriptQuestionEventArgs>,
|
||||
script_sensor_reply: EventRegistry<ScriptSensorReplyEventArgs>,
|
||||
}
|
||||
|
||||
impl Default for AgentManagerInner {
|
||||
@@ -370,8 +388,17 @@ impl Default for AgentManagerInner {
|
||||
disposed: AtomicBool::new(false),
|
||||
agent_id: RwLock::new(UUID::zero()),
|
||||
session_id: RwLock::new(UUID::zero()),
|
||||
secure_session_id: RwLock::new(UUID::zero()),
|
||||
first_name: RwLock::new(String::new()),
|
||||
last_name: RwLock::new(String::new()),
|
||||
start_location: RwLock::new(String::new()),
|
||||
look_at: RwLock::new(Vector3::zero()),
|
||||
home_region_handle: RwLock::new(0),
|
||||
home_look_at: RwLock::new(Vector3::zero()),
|
||||
local_id: RwLock::new(0),
|
||||
collision_plane: RwLock::new(Vector4::zero()),
|
||||
health: RwLock::new(0.0),
|
||||
height_width_generation: AtomicU32::new(0),
|
||||
active_group: RwLock::new(UUID::zero()),
|
||||
active_group_powers: RwLock::new(crate::GroupPowers(0)),
|
||||
agent_access: RwLock::new(String::new()),
|
||||
@@ -397,6 +424,7 @@ impl Default for AgentManagerInner {
|
||||
network: Mutex::new(None),
|
||||
raw_packet_subscription: Mutex::new(None),
|
||||
caps_subscriptions: Mutex::new(Vec::new()),
|
||||
login_callback: Mutex::new(None),
|
||||
movement: OnceLock::new(),
|
||||
chat_from_simulator: EventRegistry::default(),
|
||||
money_balance: EventRegistry::default(),
|
||||
@@ -415,6 +443,13 @@ impl Default for AgentManagerInner {
|
||||
set_display_name_reply: EventRegistry::default(),
|
||||
nav_mesh_status_update: EventRegistry::default(),
|
||||
mute_list_updated: EventRegistry::default(),
|
||||
alert_message: EventRegistry::default(),
|
||||
load_url: EventRegistry::default(),
|
||||
mean_collision: EventRegistry::default(),
|
||||
script_control_change: EventRegistry::default(),
|
||||
script_dialog: EventRegistry::default(),
|
||||
script_question: EventRegistry::default(),
|
||||
script_sensor_reply: EventRegistry::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,6 +479,31 @@ impl AgentManagerInner {
|
||||
crate::packets::PacketType::AvatarAnimation => {
|
||||
self.handle_avatar_animation_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::AlertMessage => {
|
||||
self.handle_alert_message_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::AgentAlertMessage => {
|
||||
self.handle_agent_alert_message_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::HealthMessage => {
|
||||
self.handle_health_message_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::LoadURL => self.handle_load_url_packet(&event.data),
|
||||
crate::packets::PacketType::MeanCollisionAlert => {
|
||||
self.handle_mean_collision_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::ScriptControlChange => {
|
||||
self.handle_script_control_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::ScriptDialog => {
|
||||
self.handle_script_dialog_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::ScriptQuestion => {
|
||||
self.handle_script_question_packet(&event.data, event.simulator)
|
||||
}
|
||||
crate::packets::PacketType::ScriptSensorReply => {
|
||||
self.handle_script_sensor_reply_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::MuteListUpdate => {
|
||||
self.handle_mute_list_update_packet(&event.data, event.simulator)
|
||||
}
|
||||
@@ -458,6 +518,164 @@ impl AgentManagerInner {
|
||||
let _ = result;
|
||||
}
|
||||
|
||||
fn handle_alert_message_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::AlertMessagePacket>(data)?;
|
||||
let (notification_id, extra_params) =
|
||||
packet
|
||||
.alert_info
|
||||
.first()
|
||||
.map_or(Ok::<_, Error>((None, None)), |info| {
|
||||
let extra_params = if info.extra_params.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match OSDParser::deserialize_with_bytes(info.extra_params.clone())? {
|
||||
OSD::Map(map) => Some(OSDMap::new_with_dictionary(map)?),
|
||||
_ => return Err(Error::InvalidOperation),
|
||||
}
|
||||
};
|
||||
Ok((Some(wire_string(info.message.clone())), extra_params))
|
||||
})?;
|
||||
self.alert_message.emit(AlertMessageEventArgs::new(
|
||||
wire_string(packet.alert_data.message),
|
||||
notification_id,
|
||||
extra_params,
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_agent_alert_message_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::AgentAlertMessagePacket>(data)?;
|
||||
self.alert_message.emit(AlertMessageEventArgs::new(
|
||||
wire_string(packet.alert_data.message),
|
||||
None,
|
||||
None,
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_health_message_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::HealthMessagePacket>(data)?;
|
||||
*self
|
||||
.health
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = packet.health_data.health;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_load_url_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::LoadURLPacket>(data)?;
|
||||
let data = packet.data;
|
||||
self.load_url.emit(LoadUrlEventArgs::new(
|
||||
wire_string(data.object_name),
|
||||
data.object_id,
|
||||
data.owner_id,
|
||||
data.owner_is_group,
|
||||
wire_string(data.message),
|
||||
wire_string(data.url),
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_mean_collision_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::MeanCollisionAlertPacket>(data)?;
|
||||
for collision in packet.mean_collision {
|
||||
let type_ = match collision.type_ {
|
||||
0 => crate::MeanCollisionType::None,
|
||||
1 => crate::MeanCollisionType::Bump,
|
||||
2 => crate::MeanCollisionType::LLPushObject,
|
||||
3 => crate::MeanCollisionType::SelectedObjectCollide,
|
||||
4 => crate::MeanCollisionType::ScriptedObjectCollide,
|
||||
5 => crate::MeanCollisionType::PhysicalObjectCollide,
|
||||
_ => return Err(Error::Argument),
|
||||
};
|
||||
self.mean_collision.emit(MeanCollisionEventArgs::new(
|
||||
type_,
|
||||
collision.perp,
|
||||
collision.victim,
|
||||
collision.mag,
|
||||
UNIX_EPOCH + Duration::from_secs(u64::from(collision.time)),
|
||||
)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_script_control_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::ScriptControlChangePacket>(data)?;
|
||||
for change in packet.data {
|
||||
self.script_control_change.emit(ScriptControlEventArgs::new(
|
||||
crate::ScriptControlChange(change.controls),
|
||||
change.pass_to_agent,
|
||||
change.take_controls,
|
||||
)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_script_dialog_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::ScriptDialogPacket>(data)?;
|
||||
let owner_id = packet
|
||||
.owner_data
|
||||
.first()
|
||||
.map_or(UUID::zero(), |owner| owner.owner_id);
|
||||
let buttons = packet
|
||||
.buttons
|
||||
.into_iter()
|
||||
.map(|button| wire_string(button.button_label))
|
||||
.collect();
|
||||
let dialog = packet.data;
|
||||
self.script_dialog.emit(ScriptDialogEventArgs::new(
|
||||
wire_string(dialog.message),
|
||||
wire_string(dialog.object_name),
|
||||
dialog.image_id,
|
||||
dialog.object_id,
|
||||
wire_string(dialog.first_name),
|
||||
wire_string(dialog.last_name),
|
||||
dialog.chat_channel,
|
||||
buttons,
|
||||
owner_id,
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_script_question_packet(
|
||||
&self,
|
||||
data: &[u8],
|
||||
simulator: Simulator,
|
||||
) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::ScriptQuestionPacket>(data)?;
|
||||
let question = packet.data;
|
||||
self.script_question.emit(ScriptQuestionEventArgs::new(
|
||||
simulator,
|
||||
question.task_id,
|
||||
question.item_id,
|
||||
wire_string(question.object_name),
|
||||
wire_string(question.object_owner),
|
||||
crate::ScriptPermission(question.questions),
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_script_sensor_reply_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::ScriptSensorReplyPacket>(data)?;
|
||||
let requester_id = packet.requester.source_id;
|
||||
for sensed in packet.sensed_data {
|
||||
self.script_sensor_reply
|
||||
.emit(ScriptSensorReplyEventArgs::new(
|
||||
requester_id,
|
||||
sensed.group_id,
|
||||
wire_string(sensed.name),
|
||||
sensed.object_id,
|
||||
sensed.owner_id,
|
||||
sensed.position,
|
||||
sensed.range,
|
||||
sensed.rotation,
|
||||
crate::ScriptSensorTypeFlags(sensed.type_),
|
||||
sensed.velocity,
|
||||
)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_mute_list_update_packet(
|
||||
&self,
|
||||
data: &[u8],
|
||||
@@ -747,6 +965,81 @@ impl AgentManagerInner {
|
||||
)
|
||||
}
|
||||
|
||||
fn apply_login_response(&self, response: &crate::LoginResponseData) {
|
||||
*self
|
||||
.agent_id
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.agent_id();
|
||||
*self
|
||||
.session_id
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.session_id();
|
||||
*self
|
||||
.secure_session_id
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.secure_session_id();
|
||||
*self
|
||||
.first_name
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.first_name();
|
||||
*self
|
||||
.last_name
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.last_name();
|
||||
*self
|
||||
.start_location
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.start_location();
|
||||
*self
|
||||
.agent_access
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.agent_access();
|
||||
let look_at = response.look_at();
|
||||
*self
|
||||
.look_at
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = look_at;
|
||||
let home = response.home();
|
||||
*self
|
||||
.home_region_handle
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = home.region_handle;
|
||||
*self
|
||||
.home_look_at
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = home.look_at;
|
||||
*self
|
||||
.benefits
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.account_level_benefits();
|
||||
*self
|
||||
.active_gestures
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.gestures();
|
||||
if let Some(movement) = self.movement.get() {
|
||||
movement.set_home_position(home.position);
|
||||
let _ = movement.set_login_look_at(look_at);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_avatar_object_state(&self, local_id: u32, collision_plane: Vector4) {
|
||||
*self
|
||||
.local_id
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = local_id;
|
||||
*self
|
||||
.collision_plane
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = collision_plane;
|
||||
}
|
||||
|
||||
pub(crate) fn avatar_local_id(&self) -> u32 {
|
||||
*self
|
||||
.local_id
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn handle_caps_event(
|
||||
self: &Arc<Self>,
|
||||
client: Arc<GridClient>,
|
||||
@@ -1044,6 +1337,39 @@ impl AgentManager {
|
||||
Arc::downgrade(&inner),
|
||||
)
|
||||
}));
|
||||
{
|
||||
let mut callback = mutex(&inner.login_callback);
|
||||
if callback.is_none() {
|
||||
let weak = Arc::downgrade(&inner);
|
||||
let registered = crate::NetworkManagerLoginResponseCallback::from_handler(
|
||||
move |success, redirect, _message, _reason, response| {
|
||||
if success
|
||||
&& !redirect
|
||||
&& let (Some(inner), Some(response)) = (weak.upgrade(), response)
|
||||
{
|
||||
inner.apply_login_response(&response);
|
||||
}
|
||||
},
|
||||
);
|
||||
network.native_register_login_response_callback(
|
||||
registered.clone(),
|
||||
Some(vec![
|
||||
"agent_id".into(),
|
||||
"session_id".into(),
|
||||
"secure_session_id".into(),
|
||||
"first_name".into(),
|
||||
"last_name".into(),
|
||||
"start_location".into(),
|
||||
"agent_access".into(),
|
||||
"look_at".into(),
|
||||
"home".into(),
|
||||
"gestures".into(),
|
||||
"account_level_benefits".into(),
|
||||
]),
|
||||
)?;
|
||||
*callback = Some(registered);
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
agent_state_status: None,
|
||||
group_chat_sessions: Arc::clone(&inner.group_chat_sessions),
|
||||
@@ -1076,12 +1402,66 @@ impl AgentManager {
|
||||
self.inner.animations_changed.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_alert_message(
|
||||
&self,
|
||||
handler: EventHandler<AlertMessageEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.alert_message.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_load_url(
|
||||
&self,
|
||||
handler: EventHandler<LoadUrlEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.load_url.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_mean_collision(
|
||||
&self,
|
||||
handler: EventHandler<MeanCollisionEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.mean_collision.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_script_control_change(
|
||||
&self,
|
||||
handler: EventHandler<ScriptControlEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.script_control_change.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_script_dialog(
|
||||
&self,
|
||||
handler: EventHandler<ScriptDialogEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.script_dialog.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_script_question(
|
||||
&self,
|
||||
handler: EventHandler<ScriptQuestionEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.script_question.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_script_sensor_reply(
|
||||
&self,
|
||||
handler: EventHandler<ScriptSensorReplyEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.script_sensor_reply.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_dispose(&self) -> Result<(), Error> {
|
||||
if self.inner.disposed.swap(true, Ordering::AcqRel) {
|
||||
return Ok(());
|
||||
}
|
||||
mutex(&self.inner.raw_packet_subscription).take();
|
||||
mutex(&self.inner.caps_subscriptions).clear();
|
||||
if let Some(callback) = mutex(&self.inner.login_callback).take()
|
||||
&& let Some(network) = mutex(&self.inner.network).as_ref()
|
||||
{
|
||||
network.native_unregister_login_response_callback(&callback)?;
|
||||
}
|
||||
if let Some(movement) = self.inner.movement.get() {
|
||||
movement.stop();
|
||||
}
|
||||
@@ -1388,6 +1768,297 @@ impl AgentManager {
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
pub(crate) fn native_secure_session_id(&self) -> UUID {
|
||||
*self
|
||||
.inner
|
||||
.secure_session_id
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_secure_session_id(&self, value: UUID) {
|
||||
*self
|
||||
.inner
|
||||
.secure_session_id
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
pub(crate) fn native_start_location(&self) -> String {
|
||||
self.inner
|
||||
.start_location
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_start_location(&self, value: String) {
|
||||
*self
|
||||
.inner
|
||||
.start_location
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
pub(crate) fn native_look_at(&self) -> Vector3 {
|
||||
*self
|
||||
.inner
|
||||
.look_at
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_look_at(&self, value: Vector3) {
|
||||
*self
|
||||
.inner
|
||||
.look_at
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
let _ = self
|
||||
.inner
|
||||
.movement
|
||||
.get()
|
||||
.map(|movement| movement.set_login_look_at(value));
|
||||
}
|
||||
|
||||
pub(crate) fn native_home_region_handle(&self) -> u64 {
|
||||
*self
|
||||
.inner
|
||||
.home_region_handle
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_home_look_at(&self) -> Vector3 {
|
||||
*self
|
||||
.inner
|
||||
.home_look_at
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_local_id(&self) -> u32 {
|
||||
*self
|
||||
.inner
|
||||
.local_id
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_collision_plane(&self) -> Vector4 {
|
||||
*self
|
||||
.inner
|
||||
.collision_plane
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_health(&self) -> f32 {
|
||||
*self
|
||||
.inner
|
||||
.health
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_health(&self, value: f32) {
|
||||
*self
|
||||
.inner
|
||||
.health
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn native_grab(
|
||||
&self,
|
||||
object_local_id: u32,
|
||||
grab_offset: Vector3,
|
||||
uv_coord: Vector3,
|
||||
st_coord: Vector3,
|
||||
face_index: i32,
|
||||
position: Vector3,
|
||||
normal: Vector3,
|
||||
binormal: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::ObjectGrabPacket::new_with_constructor()?;
|
||||
packet.agent_data.agent_id = self.native_agent_id();
|
||||
packet.agent_data.session_id = self.native_session_id();
|
||||
packet.object_data.local_id = object_local_id;
|
||||
packet.object_data.grab_offset = grab_offset;
|
||||
let mut surface = crate::packets::ObjectGrabPacketSurfaceInfoBlock::new_with_constructor()?;
|
||||
surface.uv_coord = uv_coord;
|
||||
surface.st_coord = st_coord;
|
||||
surface.face_index = face_index;
|
||||
surface.position = position;
|
||||
surface.normal = normal;
|
||||
surface.binormal = binormal;
|
||||
packet.surface_info = vec![surface];
|
||||
self.send_current_packet(
|
||||
crate::packets::PacketType::ObjectGrab,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn native_grab_update(
|
||||
&self,
|
||||
object_id: UUID,
|
||||
grab_position: Vector3,
|
||||
grab_offset: Vector3,
|
||||
uv_coord: Vector3,
|
||||
st_coord: Vector3,
|
||||
face_index: i32,
|
||||
position: Vector3,
|
||||
normal: Vector3,
|
||||
binormal: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::ObjectGrabUpdatePacket::new_with_constructor()?;
|
||||
packet.agent_data.agent_id = self.native_agent_id();
|
||||
packet.agent_data.session_id = self.native_session_id();
|
||||
packet.object_data.object_id = object_id;
|
||||
packet.object_data.grab_offset_initial = grab_offset;
|
||||
packet.object_data.grab_position = grab_position;
|
||||
packet.object_data.time_since_last = 0;
|
||||
let mut surface =
|
||||
crate::packets::ObjectGrabUpdatePacketSurfaceInfoBlock::new_with_constructor()?;
|
||||
surface.uv_coord = uv_coord;
|
||||
surface.st_coord = st_coord;
|
||||
surface.face_index = face_index;
|
||||
surface.position = position;
|
||||
surface.normal = normal;
|
||||
surface.binormal = binormal;
|
||||
packet.surface_info = vec![surface];
|
||||
self.send_current_packet(
|
||||
crate::packets::PacketType::ObjectGrabUpdate,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn native_de_grab(
|
||||
&self,
|
||||
object_local_id: u32,
|
||||
uv_coord: Vector3,
|
||||
st_coord: Vector3,
|
||||
face_index: i32,
|
||||
position: Vector3,
|
||||
normal: Vector3,
|
||||
binormal: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::ObjectDeGrabPacket::new_with_constructor()?;
|
||||
packet.agent_data.agent_id = self.native_agent_id();
|
||||
packet.agent_data.session_id = self.native_session_id();
|
||||
packet.object_data.local_id = object_local_id;
|
||||
let mut surface =
|
||||
crate::packets::ObjectDeGrabPacketSurfaceInfoBlock::new_with_constructor()?;
|
||||
surface.uv_coord = uv_coord;
|
||||
surface.st_coord = st_coord;
|
||||
surface.face_index = face_index;
|
||||
surface.position = position;
|
||||
surface.normal = normal;
|
||||
surface.binormal = binormal;
|
||||
packet.surface_info = vec![surface];
|
||||
self.send_current_packet(
|
||||
crate::packets::PacketType::ObjectDeGrab,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
fn send_current_packet(
|
||||
&self,
|
||||
packet_type: crate::packets::PacketType,
|
||||
data: Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let simulator = self
|
||||
.client
|
||||
.network()
|
||||
.native_current_sim()
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
send_simulator_packet(&simulator, packet_type, data)
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_height_width(&self, height: u16, width: u16) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::AgentHeightWidthPacket::new_with_constructor()?;
|
||||
packet.agent_data.agent_id = self.native_agent_id();
|
||||
packet.agent_data.session_id = self.native_session_id();
|
||||
packet.agent_data.circuit_code = self.client.network().native_circuit_code();
|
||||
packet.height_width_block.height = height;
|
||||
packet.height_width_block.width = width;
|
||||
packet.height_width_block.gen_counter = self
|
||||
.inner
|
||||
.height_width_generation
|
||||
.fetch_add(1, Ordering::AcqRel);
|
||||
self.send_current_packet(
|
||||
crate::packets::PacketType::AgentHeightWidth,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_home(&self) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::SetStartLocationRequestPacket::new_with_constructor()?;
|
||||
packet.agent_data.agent_id = self.native_agent_id();
|
||||
packet.agent_data.session_id = self.native_session_id();
|
||||
packet.start_location_data.location_pos = self.native_movement_runtime().sim_position();
|
||||
packet.start_location_data.location_id = 1;
|
||||
packet.start_location_data.sim_name = Vec::new();
|
||||
packet.start_location_data.location_look_at =
|
||||
self.native_movement_runtime().camera_at_axis();
|
||||
self.send_current_packet(
|
||||
crate::packets::PacketType::SetStartLocationRequest,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn native_script_question_reply(
|
||||
&self,
|
||||
simulator: Simulator,
|
||||
item_id: UUID,
|
||||
task_id: UUID,
|
||||
permissions: crate::ScriptPermission,
|
||||
) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::ScriptAnswerYesPacket::new_with_constructor()?;
|
||||
packet.agent_data.agent_id = self.native_agent_id();
|
||||
packet.agent_data.session_id = self.native_session_id();
|
||||
packet.data.item_id = item_id;
|
||||
packet.data.task_id = task_id;
|
||||
packet.data.questions = permissions.0;
|
||||
send_simulator_packet(
|
||||
&simulator,
|
||||
crate::packets::PacketType::ScriptAnswerYes,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn native_request_script_sensor(
|
||||
&self,
|
||||
name: String,
|
||||
search_id: UUID,
|
||||
type_: crate::ScriptSensorTypeFlags,
|
||||
range: f32,
|
||||
arc: f32,
|
||||
request_id: UUID,
|
||||
simulator: Simulator,
|
||||
) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::ScriptSensorRequestPacket::new_with_constructor()?;
|
||||
packet.requester.arc = arc;
|
||||
packet.requester.range = range;
|
||||
packet.requester.region_handle = simulator.handle;
|
||||
packet.requester.request_id = request_id;
|
||||
packet.requester.search_dir = libremetaverse_types::Quaternion::identity();
|
||||
packet.requester.search_id = search_id;
|
||||
packet.requester.search_name = name.into_bytes();
|
||||
packet.requester.search_pos = Vector3::zero();
|
||||
packet.requester.search_regions = 0;
|
||||
packet.requester.source_id = self.native_agent_id();
|
||||
packet.requester.type_ = type_.0;
|
||||
send_simulator_packet(
|
||||
&simulator,
|
||||
crate::packets::PacketType::ScriptSensorRequest,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn native_last_name(&self) -> String {
|
||||
self.inner
|
||||
.last_name
|
||||
@@ -4064,6 +4735,7 @@ fn osd_uuid_array_contains(map: &OSDMap, key: &str, id: UUID) -> Result<bool, Er
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::too_many_lines)] // Protocol scenarios remain readable as contiguous exchanges.
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
@@ -2388,6 +2388,18 @@ impl AgentMovementRuntime {
|
||||
read(&self.kinematics).home_position
|
||||
}
|
||||
|
||||
pub(crate) fn set_home_position(&self, value: Vector3) {
|
||||
write(&self.kinematics).home_position = value;
|
||||
}
|
||||
|
||||
pub(crate) fn set_login_look_at(&self, value: Vector3) -> Result<(), Error> {
|
||||
self.camera(128.0).look_direction_with_vector3(value)
|
||||
}
|
||||
|
||||
pub(crate) fn camera_at_axis(&self) -> Vector3 {
|
||||
self.camera(128.0).at_axis()
|
||||
}
|
||||
|
||||
pub(crate) fn last_position_update(&self) -> SystemTime {
|
||||
read(&self.kinematics).last_position_update
|
||||
}
|
||||
@@ -2853,6 +2865,7 @@ impl AgentManager {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::float_cmp)] // Wire fixtures use exactly representable values.
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::packets::{
|
||||
@@ -3431,7 +3444,7 @@ mod tests {
|
||||
let (prediction_sender, prediction_receiver) = mpsc::channel();
|
||||
let _prediction =
|
||||
manager.native_subscribe_region_crossing_predicted(Arc::new(move |event| {
|
||||
prediction_sender.send(event).unwrap()
|
||||
prediction_sender.send(event).unwrap();
|
||||
}));
|
||||
runtime.update_multi_simulator_state(&simulator);
|
||||
let prediction = prediction_receiver.try_recv().unwrap();
|
||||
|
||||
@@ -177,6 +177,13 @@ pub(crate) fn builtin_animations() -> std::collections::HashMap<UUID, String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn builtin_animation(name: &str) -> UUID {
|
||||
builtin_animations()
|
||||
.into_iter()
|
||||
.find_map(|(id, candidate)| (candidate == name).then_some(id))
|
||||
.unwrap_or_else(|| panic!("unknown built-in animation {name}"))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BinBVHJointKey {
|
||||
pub key_element: Vector3,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Exact Rust translations of LibreMetaverse.Tests/BakerTests.cs.
|
||||
// Source SHA-256: 7727140f0fbf2c77ed7a6f3f468798d8f7ba96c5d9889246a2bccd2d4bd8def2
|
||||
|
||||
#![allow(clippy::similar_names)] // Baker/baked terminology matches the API under test.
|
||||
|
||||
use super::*;
|
||||
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
|
||||
use libremetaverse_types::Color4;
|
||||
|
||||
@@ -159,6 +159,7 @@ pub(crate) struct AppearanceManagerInner {
|
||||
appearance_cancel: Mutex<Option<CancellationTokenSource>>,
|
||||
appearance_gate: Arc<tokio::sync::Semaphore>,
|
||||
events: AppearanceEvents,
|
||||
login_callback: Mutex<Option<crate::NetworkManagerLoginResponseCallback>>,
|
||||
network_subscriptions: Mutex<Vec<Subscription>>,
|
||||
}
|
||||
|
||||
@@ -232,11 +233,35 @@ fn handle_appearance_packet(
|
||||
match packet_type {
|
||||
PacketType::AgentWearablesUpdate => handle_wearables_update(inner, data),
|
||||
PacketType::AgentCachedTextureResponse => handle_cached_bakes(inner, data),
|
||||
PacketType::AvatarAppearance => handle_avatar_appearance(inner, data),
|
||||
PacketType::RebakeAvatarTextures => handle_rebake_request(inner, data),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_avatar_appearance(inner: &Arc<AppearanceManagerInner>, data: Vec<u8>) {
|
||||
let mut offset = 0;
|
||||
let Ok(packet) =
|
||||
crate::packets::AvatarAppearancePacket::new_with_bytes_int32(data, &mut offset)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = inner.client.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Ok(network) = client.native_network() else {
|
||||
return;
|
||||
};
|
||||
if packet.sender.id != network.native_agent_id() {
|
||||
return;
|
||||
}
|
||||
if let Some(appearance) = packet.appearance_data.first() {
|
||||
inner
|
||||
.last_cof_version
|
||||
.fetch_max(appearance.cof_version, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_wearables_update(inner: &Arc<AppearanceManagerInner>, data: Vec<u8>) {
|
||||
let mut offset = 0;
|
||||
let Ok(packet) = AgentWearablesUpdatePacket::new_with_bytes_int32(data, &mut offset) else {
|
||||
@@ -371,9 +396,31 @@ impl AppearanceManager {
|
||||
appearance_cancel: Mutex::new(None),
|
||||
appearance_gate: Arc::new(tokio::sync::Semaphore::new(1)),
|
||||
events: AppearanceEvents::default(),
|
||||
login_callback: Mutex::new(None),
|
||||
network_subscriptions: Mutex::new(Vec::new()),
|
||||
});
|
||||
let weak = Arc::downgrade(&inner);
|
||||
let login_callback = crate::NetworkManagerLoginResponseCallback::from_handler(
|
||||
move |success, redirect, _message, _reason, response| {
|
||||
if success
|
||||
&& !redirect
|
||||
&& let (Some(inner), Some(response)) = (weak.upgrade(), response)
|
||||
{
|
||||
inner.last_cof_version.fetch_max(
|
||||
i32::try_from(response.cof_version()).unwrap_or(i32::MAX),
|
||||
Ordering::AcqRel,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
client
|
||||
.native_network()?
|
||||
.native_register_login_response_callback(
|
||||
login_callback.clone(),
|
||||
Some(vec!["cof_version".to_owned()]),
|
||||
)?;
|
||||
*mutex(&inner.login_callback) = Some(login_callback);
|
||||
let weak = Arc::downgrade(&inner);
|
||||
let subscription = client
|
||||
.native_network()?
|
||||
.subscribe_raw_packet(Arc::new(move |event| {
|
||||
@@ -463,6 +510,12 @@ impl AppearanceManager {
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)] // The mapped Dispose API is fallible.
|
||||
pub(crate) fn native_dispose(&self) -> Result<(), Error> {
|
||||
if let Some(callback) = mutex(&self.inner.login_callback).take()
|
||||
&& let Some(client) = self.inner.client.upgrade()
|
||||
&& let Ok(network) = client.native_network()
|
||||
{
|
||||
network.native_unregister_login_response_callback(&callback)?;
|
||||
}
|
||||
self.inner.disposed.store(true, Ordering::Release);
|
||||
write(&self.inner.wearables).clear();
|
||||
self.inner
|
||||
@@ -1049,6 +1102,70 @@ impl AppearanceManager {
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn native_apply_archetype(
|
||||
&self,
|
||||
archetype: crate::GenepoolArchetype,
|
||||
) -> Result<(), Error> {
|
||||
let parameters = archetype.params();
|
||||
{
|
||||
let mut assets = write(&self.inner.wearable_assets);
|
||||
for asset in assets.values_mut() {
|
||||
for parameter in ¶meters {
|
||||
if let Some(value) = asset.params.get_mut(¶meter.id()) {
|
||||
*value = parameter.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.inner
|
||||
.server_visual_parameters
|
||||
.store(false, Ordering::Release);
|
||||
self.native_request_set_appearance(true).await
|
||||
}
|
||||
|
||||
pub(crate) async fn native_blend_to_archetype(
|
||||
&self,
|
||||
archetype: crate::GenepoolArchetype,
|
||||
amount: f32,
|
||||
) -> Result<(), Error> {
|
||||
if !amount.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if amount <= 0.0 {
|
||||
return Ok(());
|
||||
}
|
||||
let amount = amount.min(1.0);
|
||||
let parameters = archetype.params();
|
||||
{
|
||||
let mut assets = write(&self.inner.wearable_assets);
|
||||
for asset in assets.values_mut() {
|
||||
for parameter in ¶meters {
|
||||
if let Some(current) = asset.params.get_mut(¶meter.id()) {
|
||||
*current += amount * (parameter.value() - *current);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.inner
|
||||
.server_visual_parameters
|
||||
.store(false, Ordering::Release);
|
||||
self.native_request_set_appearance(true).await
|
||||
}
|
||||
|
||||
pub(crate) async fn native_randomize_appearance(
|
||||
&self,
|
||||
random: Option<libremetaverse_types::compat::RandomSource>,
|
||||
) -> Result<(), Error> {
|
||||
let archetypes = crate::Genepool::archetypes();
|
||||
let maximum = i32::try_from(archetypes.len()).map_err(|_| Error::IndexOutOfRange)?;
|
||||
let index = random.unwrap_or_default().next(maximum)?;
|
||||
let archetype = archetypes
|
||||
.into_iter()
|
||||
.nth(usize::try_from(index).map_err(|_| Error::IndexOutOfRange)?)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
self.native_apply_archetype(archetype).await
|
||||
}
|
||||
|
||||
pub(crate) fn native_server_baking_region(&self) -> Result<bool, Error> {
|
||||
let protocols = self
|
||||
.client()?
|
||||
@@ -2303,7 +2420,7 @@ mod tests {
|
||||
let delivered_handler = Arc::clone(&delivered);
|
||||
let releasing = Arc::clone(&released);
|
||||
let healthy = registry.subscribe(Arc::new(move |value| {
|
||||
let _keep_alive = &releasing;
|
||||
std::hint::black_box(&releasing);
|
||||
delivered_handler.fetch_add(value as u64, Ordering::AcqRel);
|
||||
}));
|
||||
drop(released);
|
||||
@@ -2555,6 +2672,40 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_avatar_appearance_advances_cof_version_monotonically() {
|
||||
let client = GridClient::new().unwrap();
|
||||
let manager = client.appearance();
|
||||
let mut packet = crate::packets::AvatarAppearancePacket::new_with_constructor().unwrap();
|
||||
packet.sender.id = client.network().native_agent_id();
|
||||
let mut appearance =
|
||||
crate::packets::AvatarAppearancePacketAppearanceDataBlock::new_with_constructor()
|
||||
.unwrap();
|
||||
appearance.cof_version = 17;
|
||||
packet.appearance_data.push(appearance);
|
||||
|
||||
handle_appearance_packet(
|
||||
&manager.inner,
|
||||
PacketType::AvatarAppearance,
|
||||
packet.to_bytes_with_method().unwrap(),
|
||||
);
|
||||
assert_eq!(manager.native_last_cof_version(), 17);
|
||||
|
||||
let mut older = crate::packets::AvatarAppearancePacket::new_with_constructor().unwrap();
|
||||
older.sender.id = client.network().native_agent_id();
|
||||
let mut older_data =
|
||||
crate::packets::AvatarAppearancePacketAppearanceDataBlock::new_with_constructor()
|
||||
.unwrap();
|
||||
older_data.cof_version = 3;
|
||||
older.appearance_data.push(older_data);
|
||||
handle_appearance_packet(
|
||||
&manager.inner,
|
||||
PacketType::AvatarAppearance,
|
||||
older.to_bytes_with_method().unwrap(),
|
||||
);
|
||||
assert_eq!(manager.native_last_cof_version(), 17);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn failed_appearance_request_emits_failure_and_empty_cache_request_does_not_wait() {
|
||||
let client = GridClient::new().unwrap();
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
// inputs intentionally and RegionSettings intentionally exposes boolean fields.
|
||||
#![allow(clippy::pedantic)]
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use base64::Engine as _;
|
||||
use flate2::{Compression, GzBuilder, read::GzDecoder};
|
||||
use libremetaverse_types::compat::ReadWrite;
|
||||
use libremetaverse_types::compat::{IDictionary, ReadWrite, XmlTextWriter};
|
||||
use libremetaverse_types::{AssetType, Error, UUID};
|
||||
|
||||
use crate::assets::TarArchiveReaderTarEntryType;
|
||||
@@ -305,6 +308,34 @@ pub struct TarArchiveWriter {
|
||||
state: Mutex<WriterState>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AssetsArchiver {
|
||||
assets: std::collections::HashMap<UUID, crate::assets::Asset>,
|
||||
}
|
||||
|
||||
impl AssetsArchiver {
|
||||
pub fn new(assets: IDictionary<UUID, crate::assets::Asset>) -> Result<Self, Error> {
|
||||
if assets.0.len() > MAX_ENTRIES {
|
||||
return Err(archive_error("asset count limit"));
|
||||
}
|
||||
Ok(Self { assets: assets.0 })
|
||||
}
|
||||
|
||||
pub fn archive(&self, archive: TarArchiveWriter) -> Result<(), Error> {
|
||||
let mut assets = self.assets.iter().collect::<Vec<_>>();
|
||||
assets.sort_by_key(|(id, _)| id.to_string());
|
||||
for (id, asset) in assets {
|
||||
asset.encode()?;
|
||||
let extension = asset_extension(asset.asset_type()).unwrap_or_default();
|
||||
archive.write_file_with_string_bytes(
|
||||
format!("assets/{id}{extension}"),
|
||||
asset.asset_data.clone(),
|
||||
)?;
|
||||
}
|
||||
archive.close()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OarFileAssetLoadedCallback(
|
||||
std::sync::Arc<dyn Fn(crate::assets::Asset, i64, i64) + Send + Sync>,
|
||||
@@ -833,6 +864,227 @@ fn prepare_output_directory(path: &str) -> Result<PathBuf, Error> {
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
fn reset_output_directory(path: &str) -> Result<PathBuf, Error> {
|
||||
let root = PathBuf::from(path);
|
||||
if root.exists() {
|
||||
let metadata = fs::symlink_metadata(&root).map_err(io_error)?;
|
||||
if metadata.file_type().is_symlink() || !metadata.is_dir() {
|
||||
return Err(archive_error("archive output root"));
|
||||
}
|
||||
fs::remove_dir_all(&root).map_err(io_error)?;
|
||||
}
|
||||
fs::create_dir_all(&root).map_err(io_error)?;
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
pub(crate) fn load_objects(
|
||||
object_data: Vec<u8>,
|
||||
object_callback: OarFileSceneObjectLoadedCallback,
|
||||
bytes_read: i64,
|
||||
total_bytes: i64,
|
||||
) -> Result<(), Error> {
|
||||
if object_data.len() > MAX_ENTRY_BYTES {
|
||||
return Err(archive_error("object XML size limit"));
|
||||
}
|
||||
let xml = std::str::from_utf8(&object_data).map_err(|_| archive_error("object XML UTF-8"))?;
|
||||
let document = roxmltree::Document::parse(xml).map_err(|_| archive_error("object XML"))?;
|
||||
let root = document.root_element();
|
||||
let nodes = if root.tag_name().name() == "scene" {
|
||||
root.children()
|
||||
.filter(roxmltree::Node::is_element)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![root]
|
||||
};
|
||||
if nodes.len() > MAX_ENTRIES {
|
||||
return Err(archive_error("object count limit"));
|
||||
}
|
||||
for node in nodes {
|
||||
let range = node.range();
|
||||
let linkset = crate::assets::AssetPrim::new_with_string(xml[range].to_owned())?;
|
||||
object_callback.invoke(linkset, bytes_read, total_bytes)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn sog_to_xml2(
|
||||
mut writer: XmlTextWriter,
|
||||
prim: crate::assets::AssetPrim,
|
||||
) -> Result<(), Error> {
|
||||
let xml = prim.encode_xml()?;
|
||||
writer.0.push_str(
|
||||
xml.strip_prefix("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
|
||||
.unwrap_or(&xml),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn xml_escape(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
fn push_element(output: &mut String, name: &str, value: impl std::fmt::Display) {
|
||||
let _ = write!(
|
||||
output,
|
||||
"<{name}>{}</{name}>",
|
||||
xml_escape(&value.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
fn push_vector(output: &mut String, name: &str, value: libremetaverse_types::Vector3) {
|
||||
let _ = write!(
|
||||
output,
|
||||
"<{name}><X>{}</X><Y>{}</Y><Z>{}</Z></{name}>",
|
||||
value.x, value.y, value.z
|
||||
);
|
||||
}
|
||||
|
||||
fn parcel_xml(parcel: &crate::Parcel, global_id: UUID) -> String {
|
||||
let mut output = String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?><LandData>");
|
||||
push_element(&mut output, "Area", parcel.area);
|
||||
push_element(&mut output, "AuctionID", parcel.auction_id);
|
||||
push_element(&mut output, "AuthBuyerID", parcel.auth_buyer_id);
|
||||
push_element(&mut output, "Category", parcel.category as i8);
|
||||
push_element(
|
||||
&mut output,
|
||||
"ClaimDate",
|
||||
parcel
|
||||
.claim_date
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs(),
|
||||
);
|
||||
push_element(&mut output, "ClaimPrice", parcel.claim_price);
|
||||
push_element(&mut output, "GlobalID", global_id);
|
||||
push_element(&mut output, "GroupID", parcel.group_id);
|
||||
push_element(&mut output, "IsGroupOwned", parcel.is_group_owned);
|
||||
push_element(
|
||||
&mut output,
|
||||
"Bitmap",
|
||||
base64::engine::general_purpose::STANDARD.encode(&parcel.bitmap),
|
||||
);
|
||||
push_element(&mut output, "Description", &parcel.desc);
|
||||
push_element(&mut output, "Flags", parcel.flags.0);
|
||||
push_element(&mut output, "LandingType", parcel.landing as u8);
|
||||
push_element(&mut output, "Name", &parcel.name);
|
||||
push_element(&mut output, "Status", parcel.status as i8);
|
||||
push_element(&mut output, "LocalID", parcel.local_id);
|
||||
push_element(
|
||||
&mut output,
|
||||
"MediaAutoScale",
|
||||
u8::from(parcel.media.media_auto_scale),
|
||||
);
|
||||
push_element(&mut output, "MediaID", parcel.media.media_id);
|
||||
push_element(&mut output, "MediaURL", &parcel.media.media_url);
|
||||
push_element(&mut output, "MusicURL", &parcel.music_url);
|
||||
push_element(&mut output, "OwnerID", parcel.owner_id);
|
||||
output.push_str("<ParcelAccessList>");
|
||||
for entry in parcel
|
||||
.access_black_list
|
||||
.iter()
|
||||
.chain(&parcel.access_white_list)
|
||||
{
|
||||
output.push_str("<ParcelAccessEntry>");
|
||||
push_element(&mut output, "AgentID", entry.agent_id);
|
||||
push_element(
|
||||
&mut output,
|
||||
"Time",
|
||||
entry
|
||||
.time
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs(),
|
||||
);
|
||||
push_element(&mut output, "AccessList", entry.flags.0);
|
||||
output.push_str("</ParcelAccessEntry>");
|
||||
}
|
||||
output.push_str("</ParcelAccessList>");
|
||||
push_element(&mut output, "PassHours", parcel.pass_hours);
|
||||
push_element(&mut output, "PassPrice", parcel.pass_price);
|
||||
push_element(&mut output, "SalePrice", parcel.sale_price);
|
||||
push_element(&mut output, "SnapshotID", parcel.snapshot_id);
|
||||
push_vector(&mut output, "UserLocation", parcel.user_location);
|
||||
push_vector(&mut output, "UserLookAt", parcel.user_look_at);
|
||||
push_element(&mut output, "Dwell", parcel.dwell);
|
||||
push_element(&mut output, "OtherCleanTime", parcel.other_clean_time);
|
||||
output.push_str("</LandData>");
|
||||
output
|
||||
}
|
||||
|
||||
pub(crate) fn save_parcels(sim: crate::Simulator, parcel_path: String) -> Result<(), Error> {
|
||||
let root = reset_output_directory(&parcel_path)?;
|
||||
let mut parcels = sim
|
||||
.native_parcels_snapshot()
|
||||
.into_values()
|
||||
.collect::<Vec<_>>();
|
||||
parcels.sort_by_key(|parcel| parcel.local_id);
|
||||
if parcels.len() > MAX_ENTRIES {
|
||||
return Err(archive_error("parcel count limit"));
|
||||
}
|
||||
for parcel in parcels {
|
||||
let global_id = UUID::random()?;
|
||||
fs::write(
|
||||
root.join(format!("{global_id}.xml")),
|
||||
parcel_xml(&parcel, global_id),
|
||||
)
|
||||
.map_err(io_error)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_prim_textures(prim: &crate::prim_object::PrimObject, textures: &mut Vec<UUID>) {
|
||||
if let Some(entry) = &prim.textures {
|
||||
if let Some(default) = &entry.default_texture {
|
||||
textures.push(default.texture_id());
|
||||
}
|
||||
textures.extend(
|
||||
entry
|
||||
.face_textures
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(crate::PrimitiveTextureEntryFace::texture_id),
|
||||
);
|
||||
}
|
||||
textures.push(prim.sculpt.texture);
|
||||
textures.push(prim.particles.texture_id);
|
||||
}
|
||||
|
||||
pub(crate) async fn save_prims(
|
||||
manager: crate::AssetManager,
|
||||
prims: Vec<crate::assets::AssetPrim>,
|
||||
prims_path: String,
|
||||
assets_path: String,
|
||||
) -> Result<(), Error> {
|
||||
if prims.len() > MAX_ENTRIES {
|
||||
return Err(archive_error("linkset count limit"));
|
||||
}
|
||||
let root = reset_output_directory(&prims_path)?;
|
||||
let mut textures = Vec::new();
|
||||
for prim in prims {
|
||||
let Some(parent) = &prim.parent else {
|
||||
continue;
|
||||
};
|
||||
fs::write(
|
||||
root.join(format!("Primitive_{}.xml", parent.id)),
|
||||
prim.encode_xml()?,
|
||||
)
|
||||
.map_err(io_error)?;
|
||||
collect_prim_textures(parent, &mut textures);
|
||||
for child in &prim.children {
|
||||
collect_prim_textures(child, &mut textures);
|
||||
}
|
||||
}
|
||||
textures.retain(|id| *id != UUID::zero());
|
||||
textures.sort_by_key(UUID::to_string);
|
||||
textures.dedup();
|
||||
save_assets(manager, AssetType::Texture, textures, assets_path).await
|
||||
}
|
||||
|
||||
fn write_asset(root: &Path, id: UUID, extension: &str, data: &[u8]) -> Result<(), Error> {
|
||||
if data.len() > MAX_ENTRY_BYTES {
|
||||
return Err(archive_error("asset size limit"));
|
||||
@@ -1201,10 +1453,42 @@ pub(crate) fn save_region_settings(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
BLOCK, MAX_ENTRY_BYTES, MAX_PATH_DEPTH, TarArchiveReader, padded_octal,
|
||||
AssetsArchiver, BLOCK, MAX_ENTRY_BYTES, MAX_PATH_DEPTH, OarFileSceneObjectLoadedCallback,
|
||||
TarArchiveReader, TarArchiveWriter, load_objects, padded_octal, sog_to_xml2,
|
||||
validate_archive_path,
|
||||
};
|
||||
use crate::assets::TarArchiveReaderTarEntryType;
|
||||
use crate::assets::{Asset, AssetPrim, TarArchiveReaderTarEntryType};
|
||||
use crate::prim_object::PrimObject;
|
||||
use libremetaverse_types::compat::{IDictionary, XmlTextWriter};
|
||||
use libremetaverse_types::{AssetType, UUID};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct SharedCursor(Arc<Mutex<Cursor<Vec<u8>>>>);
|
||||
|
||||
impl Read for SharedCursor {
|
||||
fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.0.lock().expect("cursor").read(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for SharedCursor {
|
||||
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
|
||||
self.0.lock().expect("cursor").write(buffer)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.0.lock().expect("cursor").flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for SharedCursor {
|
||||
fn seek(&mut self, position: SeekFrom) -> std::io::Result<u64> {
|
||||
self.0.lock().expect("cursor").seek(position)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_absolute_parent_and_windows_style_archive_paths() {
|
||||
@@ -1258,4 +1542,53 @@ mod tests {
|
||||
let mut kind = TarArchiveReaderTarEntryType::TYPE_UNKNOWN;
|
||||
assert!(reader.read_entry(&mut path, &mut kind).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assets_archiver_writes_encoded_assets_and_closes_the_tar() {
|
||||
let id = UUID::new_with_u_int64(42).expect("asset id");
|
||||
let asset = Asset::native_new(AssetType::Texture, id, vec![1, 2, 3]).expect("asset");
|
||||
let archiver = AssetsArchiver::new(IDictionary(HashMap::from([(id, asset)])))
|
||||
.expect("assets archiver");
|
||||
let shared = SharedCursor::default();
|
||||
let writer = TarArchiveWriter::new(Box::new(shared.clone())).expect("tar writer");
|
||||
archiver.archive(writer).expect("archive assets");
|
||||
|
||||
let bytes = shared.0.lock().expect("cursor").get_ref().clone();
|
||||
let reader = TarArchiveReader::new(Box::new(Cursor::new(bytes))).expect("tar reader");
|
||||
let mut path = String::new();
|
||||
let mut kind = TarArchiveReaderTarEntryType::TYPE_UNKNOWN;
|
||||
assert_eq!(
|
||||
reader.read_entry(&mut path, &mut kind).expect("tar entry"),
|
||||
Some(vec![1, 2, 3])
|
||||
);
|
||||
assert_eq!(path, format!("assets/{id}_texture.jp2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_loader_handles_scene_wrappers_and_xml2_writer_accepts_linksets() {
|
||||
let mut parent = PrimObject::new().expect("parent");
|
||||
parent.id = UUID::new_with_u_int64(7).expect("parent id");
|
||||
parent.name = "archive fixture".into();
|
||||
let prim = AssetPrim::new_with_prim_object_list(parent, Vec::new()).expect("linkset");
|
||||
let encoded = prim.encode_xml().expect("linkset XML");
|
||||
let group = encoded
|
||||
.strip_prefix("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
|
||||
.expect("XML declaration");
|
||||
let scene = format!("<scene>{group}</scene>");
|
||||
let loaded = Arc::new(Mutex::new(Vec::new()));
|
||||
let observed = Arc::clone(&loaded);
|
||||
let callback = OarFileSceneObjectLoadedCallback::from_callback(move |prim, read, total| {
|
||||
observed.lock().expect("loaded").push((prim, read, total));
|
||||
});
|
||||
|
||||
load_objects(scene.into_bytes(), callback, 12, 24).expect("load objects");
|
||||
let loaded = loaded.lock().expect("loaded");
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert_eq!(
|
||||
loaded[0].0.parent.as_ref().expect("parent").name,
|
||||
"archive fixture"
|
||||
);
|
||||
assert_eq!((loaded[0].1, loaded[0].2), (12, 24));
|
||||
sog_to_xml2(XmlTextWriter(String::new()), loaded[0].0.clone()).expect("write XML2");
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ mod event_slot_tests {
|
||||
let delivered_handler = Arc::clone(&delivered);
|
||||
let releasing = Arc::clone(&released);
|
||||
let healthy = slot.subscribe(Arc::new(move |value| {
|
||||
let _keep_alive = &releasing;
|
||||
std::hint::black_box(&releasing);
|
||||
delivered_handler.fetch_add(value as u64, Ordering::AcqRel);
|
||||
}));
|
||||
drop(released);
|
||||
@@ -1046,6 +1046,20 @@ impl AssetManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_image_transfer_count(&self) -> i32 {
|
||||
i32::try_from(mutex(&self.inner.image_cancellations).len()).unwrap_or(i32::MAX)
|
||||
}
|
||||
|
||||
pub(crate) fn native_cancel_image_requests(&self) {
|
||||
let requests = mutex(&self.inner.image_cancellations)
|
||||
.drain()
|
||||
.map(|(_, source)| source)
|
||||
.collect::<Vec<_>>();
|
||||
for source in requests {
|
||||
source.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_server_baked_image(
|
||||
&self,
|
||||
avatar_id: UUID,
|
||||
|
||||
@@ -465,28 +465,35 @@ impl AssetMesh {
|
||||
})
|
||||
}
|
||||
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||
let raw = RawAsset::with_data(AssetType::Mesh, asset_id, asset_data.clone())?;
|
||||
let mesh_data = match OSDParser::deserialize_llsd_binary_with_bytes(asset_data)? {
|
||||
OSD::Map(map) => OSDMap::new_with_dictionary(map)?,
|
||||
_ => return Err(Error::Argument),
|
||||
};
|
||||
Ok(Self { raw, mesh_data })
|
||||
Ok(Self {
|
||||
raw: RawAsset::with_data(AssetType::Mesh, asset_id, asset_data)?,
|
||||
mesh_data: OSDMap::new_with_constructor()?,
|
||||
})
|
||||
}
|
||||
pub fn decode(&self) -> Result<bool, Error> {
|
||||
Ok(matches!(
|
||||
OSDParser::deserialize_llsd_binary_with_bytes(self.raw.bytes()),
|
||||
Ok(OSD::Map(_))
|
||||
))
|
||||
self.mesh_data.clear()?;
|
||||
let Ok(decoded) = crate::mesh_asset::decode_asset_map(&self.raw.bytes(), UUID::zero())
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
for entry in decoded.snapshot() {
|
||||
self.mesh_data.add_with_key_value_pair(entry)?;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
pub fn encode(&self) -> Result<(), Error> {
|
||||
self.raw
|
||||
.replace(OSDParser::serialize_llsd_binary_with_osd(OSD::Map(
|
||||
self.mesh_data.snapshot(),
|
||||
))?)
|
||||
// LLMesh encoding from arbitrary decoded domains is intentionally not
|
||||
// defined by the pinned API. Preserve the original asset bytes, just
|
||||
// as the reference implementation's sealed no-op encoder does.
|
||||
Ok(())
|
||||
}
|
||||
pub const fn asset_type(&self) -> AssetType {
|
||||
AssetType::Mesh
|
||||
}
|
||||
|
||||
pub(crate) fn native_bytes(&self) -> Vec<u8> {
|
||||
self.raw.bytes()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AssetTexture {
|
||||
|
||||
@@ -32,7 +32,9 @@ fn asset_models_retain_type_and_reject_malformed_meshes() {
|
||||
.unwrap();
|
||||
assert_eq!(asset.asset_type(), AssetType::Sound);
|
||||
assert!(asset.decode().unwrap());
|
||||
assert!(AssetMesh::new_with_uuid_bytes(UUID::random().unwrap(), b"not-llsd".to_vec()).is_err());
|
||||
let malformed =
|
||||
AssetMesh::new_with_uuid_bytes(UUID::random().unwrap(), b"not-llsd".to_vec()).unwrap();
|
||||
assert!(!malformed.decode().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,774 @@
|
||||
//! OpenSim-compatible XML2 linkset assets.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
#![allow(clippy::too_many_lines)]
|
||||
#![allow(clippy::format_push_string)]
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use base64::Engine as _;
|
||||
use libremetaverse_types::compat::XmlTextReader;
|
||||
use libremetaverse_types::{AssetType, Color4, InventoryType, Quaternion, UUID, Vector3};
|
||||
use roxmltree::Node;
|
||||
|
||||
use crate::prim_object::{PrimObject, PrimObjectInventoryBlockItemBlock, asset_type};
|
||||
use crate::{Error, Primitive, PrimitiveTextureEntry};
|
||||
|
||||
const MAX_XML_BYTES: usize = 64 * 1024 * 1024;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AssetPrim {
|
||||
pub children: Vec<PrimObject>,
|
||||
pub parent: Option<PrimObject>,
|
||||
asset_data: Vec<u8>,
|
||||
asset_id: UUID,
|
||||
}
|
||||
|
||||
fn escape(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
fn element(output: &mut String, name: &str, value: impl std::fmt::Display) {
|
||||
let _ = write!(output, "<{name}>{}</{name}>", escape(&value.to_string()));
|
||||
}
|
||||
|
||||
fn uuid_element(output: &mut String, name: &str, value: UUID) {
|
||||
let _ = write!(output, "<{name}><UUID>{value}</UUID></{name}>");
|
||||
}
|
||||
|
||||
fn vector_element(output: &mut String, name: &str, value: Vector3) {
|
||||
let _ = write!(
|
||||
output,
|
||||
"<{name}><X>{}</X><Y>{}</Y><Z>{}</Z></{name}>",
|
||||
value.x, value.y, value.z
|
||||
);
|
||||
}
|
||||
|
||||
fn quaternion_element(output: &mut String, name: &str, value: Quaternion) {
|
||||
let _ = write!(
|
||||
output,
|
||||
"<{name}><X>{}</X><Y>{}</Y><Z>{}</Z><W>{}</W></{name}>",
|
||||
value.x, value.y, value.z, value.w
|
||||
);
|
||||
}
|
||||
|
||||
fn color_element(output: &mut String, name: &str, value: Color4) {
|
||||
let _ = write!(
|
||||
output,
|
||||
"<{name}><R>{}</R><G>{}</G><B>{}</B><A>{}</A></{name}>",
|
||||
value.r, value.g, value.b, value.a
|
||||
);
|
||||
}
|
||||
|
||||
fn unix_seconds(value: SystemTime) -> u64 {
|
||||
value
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or(Duration::ZERO)
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn child<'a, 'input>(node: Node<'a, 'input>, name: &str) -> Option<Node<'a, 'input>> {
|
||||
node.children()
|
||||
.find(|candidate| candidate.is_element() && candidate.tag_name().name() == name)
|
||||
}
|
||||
|
||||
fn text(node: Node<'_, '_>, name: &str) -> Option<String> {
|
||||
child(node, name)
|
||||
.and_then(|value| value.text())
|
||||
.map(str::trim)
|
||||
.map(str::to_owned)
|
||||
}
|
||||
|
||||
fn parsed<T>(node: Node<'_, '_>, name: &str, default: T) -> Result<T, Error>
|
||||
where
|
||||
T: FromStr,
|
||||
{
|
||||
text(node, name).map_or(Ok(default), |value| {
|
||||
value.parse().map_err(|_| Error::Argument)
|
||||
})
|
||||
}
|
||||
|
||||
fn boolean(node: Node<'_, '_>, name: &str, default: bool) -> Result<bool, Error> {
|
||||
text(node, name).map_or(Ok(default), |value| {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"true" | "1" => Ok(true),
|
||||
"false" | "0" => Ok(false),
|
||||
_ => Err(Error::Argument),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn uuid(node: Node<'_, '_>, name: &str) -> Result<UUID, Error> {
|
||||
let Some(container) = child(node, name) else {
|
||||
return Ok(UUID::zero());
|
||||
};
|
||||
let value = container
|
||||
.descendants()
|
||||
.find(|candidate| {
|
||||
candidate.is_element() && matches!(candidate.tag_name().name(), "UUID" | "Guid")
|
||||
})
|
||||
.and_then(|candidate| candidate.text())
|
||||
.or_else(|| container.text())
|
||||
.map(str::trim)
|
||||
.unwrap_or_default();
|
||||
if value.is_empty() {
|
||||
Ok(UUID::zero())
|
||||
} else {
|
||||
UUID::new_with_string(value.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn vector(node: Node<'_, '_>, name: &str) -> Result<Vector3, Error> {
|
||||
let Some(value) = child(node, name) else {
|
||||
return Ok(Vector3::zero());
|
||||
};
|
||||
Ok(Vector3 {
|
||||
x: parsed(value, "X", 0.0_f32)?,
|
||||
y: parsed(value, "Y", 0.0_f32)?,
|
||||
z: parsed(value, "Z", 0.0_f32)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn quaternion(node: Node<'_, '_>, name: &str) -> Result<Quaternion, Error> {
|
||||
let Some(value) = child(node, name) else {
|
||||
return Ok(Quaternion::identity());
|
||||
};
|
||||
Quaternion::new_with_single_single_single_single(
|
||||
parsed(value, "X", 0.0_f32)?,
|
||||
parsed(value, "Y", 0.0_f32)?,
|
||||
parsed(value, "Z", 0.0_f32)?,
|
||||
parsed(value, "W", 1.0_f32)?,
|
||||
)
|
||||
}
|
||||
|
||||
fn color(node: Node<'_, '_>, name: &str) -> Result<Color4, Error> {
|
||||
let Some(value) = child(node, name) else {
|
||||
return Ok(Color4::default());
|
||||
};
|
||||
Ok(Color4 {
|
||||
r: parsed(value, "R", 0.0_f32)?.clamp(0.0, 1.0),
|
||||
g: parsed(value, "G", 0.0_f32)?.clamp(0.0, 1.0),
|
||||
b: parsed(value, "B", 0.0_f32)?.clamp(0.0, 1.0),
|
||||
a: parsed(value, "A", 0.0_f32)?.clamp(0.0, 1.0),
|
||||
})
|
||||
}
|
||||
|
||||
fn write_inventory(output: &mut String, prim: &PrimObject) {
|
||||
output.push_str("<TaskInventory>");
|
||||
for item in &prim.inventory.items {
|
||||
output.push_str("<TaskInventoryItem>");
|
||||
uuid_element(output, "AssetID", item.asset_id);
|
||||
element(output, "BasePermissions", item.perms_base);
|
||||
element(output, "CreationDate", unix_seconds(item.creation_date));
|
||||
uuid_element(output, "CreatorID", item.creator_id);
|
||||
element(output, "Description", &item.description);
|
||||
element(output, "EveryonePermissions", item.perms_everyone);
|
||||
element(output, "Flags", item.flags);
|
||||
uuid_element(output, "GroupID", item.group_id);
|
||||
element(output, "GroupPermissions", item.perms_group);
|
||||
element(output, "InvType", item.inv_type.raw());
|
||||
uuid_element(output, "ItemID", item.id);
|
||||
uuid_element(output, "OldItemID", UUID::zero());
|
||||
uuid_element(output, "LastOwnerID", item.last_owner_id);
|
||||
element(output, "Name", &item.name);
|
||||
element(output, "NextPermissions", item.perms_next_owner);
|
||||
uuid_element(output, "OwnerID", item.owner_id);
|
||||
element(output, "CurrentPermissions", item.perms_owner);
|
||||
uuid_element(output, "ParentID", prim.id);
|
||||
uuid_element(output, "ParentPartID", prim.id);
|
||||
uuid_element(output, "PermsGranter", item.perms_granter_id);
|
||||
element(output, "PermsMask", item.perms_base);
|
||||
element(output, "Type", item.type_ as i8);
|
||||
element(output, "OwnerChanged", false);
|
||||
output.push_str("</TaskInventoryItem>");
|
||||
}
|
||||
output.push_str("</TaskInventory>");
|
||||
}
|
||||
|
||||
fn read_inventory(node: Node<'_, '_>) -> Result<Vec<PrimObjectInventoryBlockItemBlock>, Error> {
|
||||
let Some(inventory) = child(node, "TaskInventory") else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
inventory
|
||||
.children()
|
||||
.filter(|entry| entry.is_element() && entry.tag_name().name() == "TaskInventoryItem")
|
||||
.map(|entry| {
|
||||
let inv_type = parsed(entry, "InvType", -1_i32)?;
|
||||
Ok(PrimObjectInventoryBlockItemBlock {
|
||||
asset_id: uuid(entry, "AssetID")?,
|
||||
creation_date: UNIX_EPOCH
|
||||
.checked_add(Duration::from_secs(parsed(entry, "CreationDate", 0_u64)?))
|
||||
.ok_or(Error::Argument)?,
|
||||
creator_id: uuid(entry, "CreatorID")?,
|
||||
description: text(entry, "Description").unwrap_or_default(),
|
||||
flags: parsed(entry, "Flags", 0_i32)?,
|
||||
group_id: uuid(entry, "GroupID")?,
|
||||
id: uuid(entry, "ItemID")?,
|
||||
inv_type: InventoryType::from_raw(
|
||||
i8::try_from(inv_type).map_err(|_| Error::Argument)?,
|
||||
),
|
||||
last_owner_id: uuid(entry, "LastOwnerID")?,
|
||||
name: text(entry, "Name").unwrap_or_default(),
|
||||
owner_id: uuid(entry, "OwnerID")?,
|
||||
perms_base: parsed(entry, "BasePermissions", 0_u32)?,
|
||||
perms_everyone: parsed(entry, "EveryonePermissions", 0_u32)?,
|
||||
perms_granter_id: uuid(entry, "PermsGranter")?,
|
||||
perms_group: parsed(entry, "GroupPermissions", 0_u32)?,
|
||||
perms_next_owner: parsed(entry, "NextPermissions", 0_u32)?,
|
||||
perms_owner: parsed(entry, "CurrentPermissions", 0_u32)?,
|
||||
type_: asset_type(parsed(entry, "Type", -1_i32)?)?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn write_prim(
|
||||
output: &mut String,
|
||||
prim: &PrimObject,
|
||||
parent: Option<&PrimObject>,
|
||||
) -> Result<(), Error> {
|
||||
output.push_str("<SceneObjectPart xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">");
|
||||
element(output, "AllowedDrop", prim.allowed_drop);
|
||||
uuid_element(output, "CreatorID", prim.creator_id);
|
||||
uuid_element(output, "FolderID", prim.folder_id);
|
||||
element(output, "InventorySerial", prim.inventory.serial);
|
||||
write_inventory(output, prim);
|
||||
let mut flags = 0_u32;
|
||||
for (enabled, flag) in [
|
||||
(prim.use_physics, libremetaverse_types::PrimFlags::PHYSICS),
|
||||
(prim.phantom, libremetaverse_types::PrimFlags::PHANTOM),
|
||||
(
|
||||
prim.die_at_edge,
|
||||
libremetaverse_types::PrimFlags::DIE_AT_EDGE,
|
||||
),
|
||||
(
|
||||
prim.return_at_edge,
|
||||
libremetaverse_types::PrimFlags::RETURN_AT_EDGE,
|
||||
),
|
||||
(prim.temporary, libremetaverse_types::PrimFlags::TEMPORARY),
|
||||
(prim.sandbox, libremetaverse_types::PrimFlags::SANDBOX),
|
||||
(
|
||||
prim.allowed_drop,
|
||||
libremetaverse_types::PrimFlags::ALLOW_INVENTORY_DROP,
|
||||
),
|
||||
] {
|
||||
if enabled {
|
||||
flags |= flag.0;
|
||||
}
|
||||
}
|
||||
element(output, "ObjectFlags", flags);
|
||||
uuid_element(output, "UUID", prim.id);
|
||||
element(output, "LocalId", prim.local_id);
|
||||
element(output, "Name", &prim.name);
|
||||
element(output, "Material", prim.material);
|
||||
element(output, "PassTouches", prim.pass_touches);
|
||||
element(output, "RegionHandle", prim.region_handle);
|
||||
element(output, "ScriptAccessPin", prim.remote_script_access_pin);
|
||||
vector_element(
|
||||
output,
|
||||
"GroupPosition",
|
||||
parent.map_or(prim.position, |root| root.position),
|
||||
);
|
||||
vector_element(
|
||||
output,
|
||||
"OffsetPosition",
|
||||
if parent.is_some() {
|
||||
prim.position
|
||||
} else {
|
||||
Vector3::zero()
|
||||
},
|
||||
);
|
||||
quaternion_element(output, "RotationOffset", prim.rotation);
|
||||
vector_element(output, "Velocity", prim.velocity);
|
||||
vector_element(output, "RotationalVelocity", Vector3::zero());
|
||||
vector_element(output, "AngularVelocity", prim.angular_velocity);
|
||||
vector_element(output, "Acceleration", prim.acceleration);
|
||||
element(output, "Description", &prim.description);
|
||||
color_element(output, "Color", prim.text_color);
|
||||
element(output, "Text", &prim.text);
|
||||
element(output, "SitName", &prim.sit_name);
|
||||
element(output, "TouchName", &prim.touch_name);
|
||||
element(output, "LinkNum", prim.link_number);
|
||||
element(output, "ClickAction", prim.click_action);
|
||||
output.push_str("<Shape>");
|
||||
element(
|
||||
output,
|
||||
"PathBegin",
|
||||
Primitive::pack_begin_cut(prim.shape.path_begin)?,
|
||||
);
|
||||
element(output, "PathCurve", prim.shape.path_curve);
|
||||
element(
|
||||
output,
|
||||
"PathEnd",
|
||||
Primitive::pack_end_cut(prim.shape.path_end)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathRadiusOffset",
|
||||
Primitive::pack_path_twist(prim.shape.path_radius_offset)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathRevolutions",
|
||||
Primitive::pack_path_revolutions(prim.shape.path_revolutions)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathScaleX",
|
||||
Primitive::pack_path_scale(prim.shape.path_scale_x)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathScaleY",
|
||||
Primitive::pack_path_scale(prim.shape.path_scale_y)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathShearX",
|
||||
Primitive::pack_path_shear(prim.shape.path_shear_x)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathShearY",
|
||||
Primitive::pack_path_shear(prim.shape.path_shear_y)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathSkew",
|
||||
Primitive::pack_path_twist(prim.shape.path_skew)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathTaperX",
|
||||
Primitive::pack_path_taper(prim.shape.path_taper_x)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathTaperY",
|
||||
Primitive::pack_path_taper(prim.shape.path_taper_y)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathTwist",
|
||||
Primitive::pack_path_twist(prim.shape.path_twist)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathTwistBegin",
|
||||
Primitive::pack_path_twist(prim.shape.path_twist_begin)?,
|
||||
);
|
||||
element(output, "PCode", prim.p_code);
|
||||
element(
|
||||
output,
|
||||
"ProfileBegin",
|
||||
Primitive::pack_begin_cut(prim.shape.profile_begin)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"ProfileEnd",
|
||||
Primitive::pack_end_cut(prim.shape.profile_end)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"ProfileHollow",
|
||||
Primitive::pack_profile_hollow(prim.shape.profile_hollow)?,
|
||||
);
|
||||
vector_element(output, "Scale", prim.scale);
|
||||
element(output, "State", prim.state);
|
||||
element(output, "ProfileShape", prim.shape.profile_curve & 0x0f);
|
||||
element(output, "HollowShape", prim.shape.profile_curve & 0xf0);
|
||||
element(output, "ProfileCurve", prim.shape.profile_curve);
|
||||
let texture_bytes = prim
|
||||
.textures
|
||||
.as_ref()
|
||||
.map(PrimitiveTextureEntry::get_bytes)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
element(
|
||||
output,
|
||||
"TextureEntry",
|
||||
base64::engine::general_purpose::STANDARD.encode(texture_bytes),
|
||||
);
|
||||
output.push_str("<ExtraParams></ExtraParams>");
|
||||
uuid_element(output, "SculptTexture", prim.sculpt.texture);
|
||||
element(output, "SculptType", prim.sculpt.type_);
|
||||
output.push_str("<SculptData></SculptData>");
|
||||
element(output, "FlexiSoftness", prim.flexible.softness);
|
||||
element(output, "FlexiTension", prim.flexible.tension);
|
||||
element(output, "FlexiDrag", prim.flexible.drag);
|
||||
element(output, "FlexiGravity", prim.flexible.gravity);
|
||||
element(output, "FlexiWind", prim.flexible.wind);
|
||||
element(output, "FlexiForceX", prim.flexible.force.x);
|
||||
element(output, "FlexiForceY", prim.flexible.force.y);
|
||||
element(output, "FlexiForceZ", prim.flexible.force.z);
|
||||
element(output, "LightColorR", prim.light.color.r);
|
||||
element(output, "LightColorG", prim.light.color.g);
|
||||
element(output, "LightColorB", prim.light.color.b);
|
||||
element(output, "LightColorA", prim.light.color.a);
|
||||
element(output, "LightRadius", prim.light.radius);
|
||||
element(output, "LightCutoff", prim.light.cutoff);
|
||||
element(output, "LightFalloff", prim.light.falloff);
|
||||
element(output, "LightIntensity", prim.light.intensity);
|
||||
element(
|
||||
output,
|
||||
"FlexiEntry",
|
||||
prim.flexible != crate::prim_object::PrimObjectFlexibleBlock::new()?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"LightEntry",
|
||||
prim.light != crate::prim_object::PrimObjectLightBlock::new()?,
|
||||
);
|
||||
element(output, "SculptEntry", prim.sculpt.texture != UUID::zero());
|
||||
output.push_str("</Shape>");
|
||||
vector_element(output, "Scale", prim.scale);
|
||||
element(output, "UpdateFlag", 0);
|
||||
quaternion_element(output, "SitTargetOrientation", prim.sit_rotation);
|
||||
vector_element(output, "SitTargetPosition", prim.sit_offset);
|
||||
vector_element(output, "SitTargetPositionLL", prim.sit_offset);
|
||||
quaternion_element(output, "SitTargetOrientationLL", prim.sit_rotation);
|
||||
element(output, "ParentID", prim.parent_id);
|
||||
element(output, "CreationDate", unix_seconds(prim.creation_date));
|
||||
element(output, "RezDate", unix_seconds(prim.rez_date));
|
||||
element(output, "Category", 0);
|
||||
element(output, "SalePrice", prim.sale_price);
|
||||
element(output, "ObjectSaleType", prim.sale_type);
|
||||
element(output, "OwnershipCost", 0);
|
||||
uuid_element(output, "GroupID", prim.group_id);
|
||||
uuid_element(output, "OwnerID", prim.owner_id);
|
||||
uuid_element(output, "LastOwnerID", prim.last_owner_id);
|
||||
element(output, "BaseMask", prim.perms_base);
|
||||
element(output, "OwnerMask", prim.perms_owner);
|
||||
element(output, "GroupMask", prim.perms_group);
|
||||
element(output, "EveryoneMask", prim.perms_everyone);
|
||||
element(output, "NextOwnerMask", prim.perms_next_owner);
|
||||
element(output, "Flags", "None");
|
||||
uuid_element(output, "SitTargetAvatar", UUID::zero());
|
||||
uuid_element(output, "CollisionSound", prim.collision_sound);
|
||||
element(output, "CollisionSoundVolume", prim.collision_sound_volume);
|
||||
element(
|
||||
output,
|
||||
"ScriptState",
|
||||
base64::engine::general_purpose::STANDARD.encode(&prim.script_state),
|
||||
);
|
||||
output.push_str("</SceneObjectPart>");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_prim(node: Node<'_, '_>) -> Result<PrimObject, Error> {
|
||||
let mut prim = PrimObject::new()?;
|
||||
prim.allowed_drop = boolean(node, "AllowedDrop", true)?;
|
||||
prim.creator_id = uuid(node, "CreatorID")?;
|
||||
prim.folder_id = uuid(node, "FolderID")?;
|
||||
prim.inventory.serial = parsed(node, "InventorySerial", 0_i32)?;
|
||||
prim.inventory.items = read_inventory(node)?;
|
||||
let flags = parsed(node, "ObjectFlags", 0_u32)?;
|
||||
prim.use_physics = flags & libremetaverse_types::PrimFlags::PHYSICS.0 != 0;
|
||||
prim.phantom = flags & libremetaverse_types::PrimFlags::PHANTOM.0 != 0;
|
||||
prim.die_at_edge = flags & libremetaverse_types::PrimFlags::DIE_AT_EDGE.0 != 0;
|
||||
prim.return_at_edge = flags & libremetaverse_types::PrimFlags::RETURN_AT_EDGE.0 != 0;
|
||||
prim.temporary = flags & libremetaverse_types::PrimFlags::TEMPORARY.0 != 0;
|
||||
prim.sandbox = flags & libremetaverse_types::PrimFlags::SANDBOX.0 != 0;
|
||||
prim.allowed_drop |= flags & libremetaverse_types::PrimFlags::ALLOW_INVENTORY_DROP.0 != 0;
|
||||
prim.id = uuid(node, "UUID")?;
|
||||
prim.local_id = parsed(node, "LocalId", 0_u32)?;
|
||||
prim.name = text(node, "Name").unwrap_or_default();
|
||||
prim.material = parsed(node, "Material", 0_i32)?;
|
||||
prim.pass_touches = boolean(node, "PassTouches", false)?;
|
||||
prim.region_handle = parsed(node, "RegionHandle", 0_u64)?;
|
||||
prim.remote_script_access_pin = parsed(node, "ScriptAccessPin", 0_i32)?;
|
||||
let group_position = vector(node, "GroupPosition")?;
|
||||
let offset_position = vector(node, "OffsetPosition")?;
|
||||
prim.rotation = quaternion(node, "RotationOffset")?;
|
||||
prim.velocity = vector(node, "Velocity")?;
|
||||
prim.angular_velocity = vector(node, "AngularVelocity")?;
|
||||
prim.acceleration = vector(node, "Acceleration")?;
|
||||
prim.description = text(node, "Description").unwrap_or_default();
|
||||
prim.text_color = color(node, "Color")?;
|
||||
prim.text = text(node, "Text").unwrap_or_default();
|
||||
prim.sit_name = text(node, "SitName").unwrap_or_default();
|
||||
prim.touch_name = text(node, "TouchName").unwrap_or_default();
|
||||
prim.link_number = parsed(node, "LinkNum", 0_i32)?;
|
||||
prim.click_action = parsed(node, "ClickAction", 0_i32)?;
|
||||
if let Some(shape) = child(node, "Shape") {
|
||||
prim.shape.path_begin = Primitive::unpack_begin_cut(parsed(shape, "PathBegin", 0_u16)?)?;
|
||||
prim.shape.path_curve = parsed(shape, "PathCurve", 0_i32)?;
|
||||
prim.shape.path_end = Primitive::unpack_end_cut(parsed(shape, "PathEnd", 0_u16)?)?;
|
||||
prim.shape.path_radius_offset =
|
||||
Primitive::unpack_path_twist(parsed(shape, "PathRadiusOffset", 0_i8)?)?;
|
||||
prim.shape.path_revolutions =
|
||||
Primitive::unpack_path_revolutions(parsed(shape, "PathRevolutions", 0_u8)?)?;
|
||||
prim.shape.path_scale_x = Primitive::unpack_path_scale(parsed(shape, "PathScaleX", 0_u8)?)?;
|
||||
prim.shape.path_scale_y = Primitive::unpack_path_scale(parsed(shape, "PathScaleY", 0_u8)?)?;
|
||||
prim.shape.path_shear_x = Primitive::unpack_path_shear(parsed(shape, "PathShearX", 0_i8)?)?;
|
||||
prim.shape.path_shear_y = Primitive::unpack_path_shear(parsed(shape, "PathShearY", 0_i8)?)?;
|
||||
prim.shape.path_skew = Primitive::unpack_path_twist(parsed(shape, "PathSkew", 0_i8)?)?;
|
||||
prim.shape.path_taper_x = Primitive::unpack_path_taper(parsed(shape, "PathTaperX", 0_i8)?)?;
|
||||
prim.shape.path_taper_y = Primitive::unpack_path_taper(parsed(shape, "PathTaperY", 0_i8)?)?;
|
||||
prim.shape.path_twist = Primitive::unpack_path_twist(parsed(shape, "PathTwist", 0_i8)?)?;
|
||||
prim.shape.path_twist_begin =
|
||||
Primitive::unpack_path_twist(parsed(shape, "PathTwistBegin", 0_i8)?)?;
|
||||
prim.p_code = parsed(shape, "PCode", 0_i32)?;
|
||||
prim.shape.profile_begin =
|
||||
Primitive::unpack_begin_cut(parsed(shape, "ProfileBegin", 0_u16)?)?;
|
||||
prim.shape.profile_end = Primitive::unpack_end_cut(parsed(shape, "ProfileEnd", 0_u16)?)?;
|
||||
prim.shape.profile_hollow =
|
||||
Primitive::unpack_profile_hollow(parsed(shape, "ProfileHollow", 0_u16)?)?;
|
||||
prim.scale = vector(shape, "Scale")?;
|
||||
prim.state = parsed(shape, "State", 0_i32)?;
|
||||
prim.shape.profile_curve = parsed(shape, "ProfileCurve", 0_i32)?;
|
||||
if let Some(encoded) = text(shape, "TextureEntry") {
|
||||
let bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(encoded)
|
||||
.map_err(|_| Error::Argument)?;
|
||||
if !bytes.is_empty() {
|
||||
prim.textures = Some(PrimitiveTextureEntry::new_with_bytes_int32_int32(
|
||||
bytes.clone(),
|
||||
0,
|
||||
i32::try_from(bytes.len()).map_err(|_| Error::Argument)?,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
prim.sculpt.texture = uuid(shape, "SculptTexture")?;
|
||||
prim.sculpt.type_ = parsed(shape, "SculptType", 0_i32)?;
|
||||
prim.flexible.softness = parsed(shape, "FlexiSoftness", 0_i32)?;
|
||||
prim.flexible.tension = parsed(shape, "FlexiTension", 0.0_f32)?;
|
||||
prim.flexible.drag = parsed(shape, "FlexiDrag", 0.0_f32)?;
|
||||
prim.flexible.gravity = parsed(shape, "FlexiGravity", 0.0_f32)?;
|
||||
prim.flexible.wind = parsed(shape, "FlexiWind", 0.0_f32)?;
|
||||
prim.flexible.force = Vector3 {
|
||||
x: parsed(shape, "FlexiForceX", 0.0_f32)?,
|
||||
y: parsed(shape, "FlexiForceY", 0.0_f32)?,
|
||||
z: parsed(shape, "FlexiForceZ", 0.0_f32)?,
|
||||
};
|
||||
prim.light.color = Color4 {
|
||||
r: parsed(shape, "LightColorR", 0.0_f32)?,
|
||||
g: parsed(shape, "LightColorG", 0.0_f32)?,
|
||||
b: parsed(shape, "LightColorB", 0.0_f32)?,
|
||||
a: parsed(shape, "LightColorA", 0.0_f32)?,
|
||||
};
|
||||
prim.light.radius = parsed(shape, "LightRadius", 0.0_f32)?;
|
||||
prim.light.cutoff = parsed(shape, "LightCutoff", 0.0_f32)?;
|
||||
prim.light.falloff = parsed(shape, "LightFalloff", 0.0_f32)?;
|
||||
prim.light.intensity = parsed(shape, "LightIntensity", 0.0_f32)?;
|
||||
}
|
||||
prim.scale = child(node, "Scale").map_or(Ok(prim.scale), |_| vector(node, "Scale"))?;
|
||||
prim.sit_offset = vector(node, "SitTargetPositionLL")?;
|
||||
prim.sit_rotation = quaternion(node, "SitTargetOrientationLL")?;
|
||||
prim.parent_id = parsed(node, "ParentID", 0_u32)?;
|
||||
prim.position = if prim.parent_id == 0 {
|
||||
group_position
|
||||
} else {
|
||||
offset_position
|
||||
};
|
||||
prim.creation_date = UNIX_EPOCH
|
||||
.checked_add(Duration::from_secs(parsed(node, "CreationDate", 0_u64)?))
|
||||
.ok_or(Error::Argument)?;
|
||||
prim.rez_date = UNIX_EPOCH
|
||||
.checked_add(Duration::from_secs(parsed(node, "RezDate", 0_u64)?))
|
||||
.ok_or(Error::Argument)?;
|
||||
prim.sale_price = parsed(node, "SalePrice", 0_i32)?;
|
||||
prim.sale_type = parsed(node, "ObjectSaleType", 0_i32)?;
|
||||
prim.group_id = uuid(node, "GroupID")?;
|
||||
prim.owner_id = uuid(node, "OwnerID")?;
|
||||
prim.last_owner_id = uuid(node, "LastOwnerID")?;
|
||||
prim.perms_base = parsed(node, "BaseMask", 0_u32)?;
|
||||
prim.perms_owner = parsed(node, "OwnerMask", 0_u32)?;
|
||||
prim.perms_group = parsed(node, "GroupMask", 0_u32)?;
|
||||
prim.perms_everyone = parsed(node, "EveryoneMask", 0_u32)?;
|
||||
prim.perms_next_owner = parsed(node, "NextOwnerMask", 0_u32)?;
|
||||
prim.collision_sound = uuid(node, "CollisionSound")?;
|
||||
prim.collision_sound_volume = parsed(node, "CollisionSoundVolume", 0.0_f32)?;
|
||||
if let Some(encoded) = text(node, "ScriptState") {
|
||||
prim.script_state = base64::engine::general_purpose::STANDARD
|
||||
.decode(encoded)
|
||||
.map_err(|_| Error::Argument)?;
|
||||
}
|
||||
Ok(prim)
|
||||
}
|
||||
|
||||
impl AssetPrim {
|
||||
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
children: Vec::new(),
|
||||
parent: None,
|
||||
asset_data: Vec::new(),
|
||||
asset_id: UUID::zero(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_prim_object_list(
|
||||
parent: PrimObject,
|
||||
children: Vec<PrimObject>,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
asset_id: parent.id,
|
||||
children,
|
||||
parent: Some(parent),
|
||||
asset_data: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||
if asset_data.len() > MAX_XML_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Self {
|
||||
children: Vec::new(),
|
||||
parent: None,
|
||||
asset_data,
|
||||
asset_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_string(xml_data: String) -> Result<Self, Error> {
|
||||
let mut value = Self::new_with_uuid_bytes(UUID::zero(), xml_data.as_bytes().to_vec())?;
|
||||
if !value.decode_xml(xml_data)? {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub const fn asset_type(&self) -> AssetType {
|
||||
AssetType::Object
|
||||
}
|
||||
pub const fn asset_id(&self) -> UUID {
|
||||
self.asset_id
|
||||
}
|
||||
pub fn native_bytes(&self) -> &[u8] {
|
||||
&self.asset_data
|
||||
}
|
||||
|
||||
pub fn encode(&mut self) -> Result<(), Error> {
|
||||
self.asset_data = self.encode_xml()?.into_bytes();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decode(&mut self) -> Result<bool, Error> {
|
||||
let Ok(xml) = String::from_utf8(self.asset_data.clone()) else {
|
||||
return Ok(false);
|
||||
};
|
||||
match self.decode_xml(xml) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_xml(&self) -> Result<String, Error> {
|
||||
let mut output =
|
||||
String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?><SceneObjectGroup>");
|
||||
if let Some(parent) = &self.parent {
|
||||
write_prim(&mut output, parent, None)?;
|
||||
}
|
||||
output.push_str("<OtherParts>");
|
||||
for child in &self.children {
|
||||
write_prim(&mut output, child, self.parent.as_ref())?;
|
||||
}
|
||||
output.push_str("</OtherParts></SceneObjectGroup>");
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn decode_xml(&mut self, xml_data: String) -> Result<bool, Error> {
|
||||
if xml_data.len() > MAX_XML_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let document = roxmltree::Document::parse(&xml_data).map_err(|_| Error::Argument)?;
|
||||
let root = document.root_element();
|
||||
if root.tag_name().name() != "SceneObjectGroup" {
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(parent_node) = root
|
||||
.children()
|
||||
.find(|node| node.is_element() && node.tag_name().name() == "SceneObjectPart")
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let parent = read_prim(parent_node)?;
|
||||
let children = child(root, "OtherParts")
|
||||
.into_iter()
|
||||
.flat_map(|container| container.children())
|
||||
.filter(|node| node.is_element() && node.tag_name().name() == "SceneObjectPart")
|
||||
.map(read_prim)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
if self.asset_id == UUID::zero() {
|
||||
self.asset_id = parent.id;
|
||||
}
|
||||
self.parent = Some(parent);
|
||||
self.children = children;
|
||||
self.asset_data = xml_data.into_bytes();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn load_prim(reader: XmlTextReader) -> Result<PrimObject, Error> {
|
||||
if reader.0.len() > MAX_XML_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let document = roxmltree::Document::parse(&reader.0).map_err(|_| Error::Argument)?;
|
||||
let node = document
|
||||
.descendants()
|
||||
.find(|node| node.is_element() && node.tag_name().name() == "SceneObjectPart")
|
||||
.ok_or(Error::Argument)?;
|
||||
read_prim(node)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn xml2_round_trip_preserves_linkset_and_inventory() {
|
||||
let mut root = PrimObject::new().unwrap();
|
||||
root.id = UUID::new_with_string("7bd3f7e3-2afc-44aa-a495-b741e4189f61".into()).unwrap();
|
||||
root.name = "OpenSim root & <fixture>".into();
|
||||
root.position = Vector3 {
|
||||
x: 128.0,
|
||||
y: 64.0,
|
||||
z: 25.0,
|
||||
};
|
||||
root.p_code = 9;
|
||||
root.shape.path_end = 1.0;
|
||||
root.shape.path_scale_x = 1.0;
|
||||
root.shape.path_scale_y = 1.0;
|
||||
root.shape.path_revolutions = 1.0;
|
||||
root.shape.profile_end = 1.0;
|
||||
root.script_state = vec![1, 2, 3];
|
||||
let mut item = PrimObjectInventoryBlockItemBlock::new().unwrap();
|
||||
item.name = "script".into();
|
||||
item.type_ = AssetType::LSLText;
|
||||
root.inventory.items.push(item);
|
||||
let mut child = root.clone();
|
||||
child.id = UUID::new_with_string("ff6863c7-7acb-4e81-a28b-f0f162f06455".into()).unwrap();
|
||||
child.name = "child".into();
|
||||
child.parent_id = root.local_id.max(1);
|
||||
child.position = Vector3 {
|
||||
x: 1.0,
|
||||
y: 2.0,
|
||||
z: 3.0,
|
||||
};
|
||||
let asset =
|
||||
AssetPrim::new_with_prim_object_list(root.clone(), vec![child.clone()]).unwrap();
|
||||
let xml = asset.encode_xml().unwrap();
|
||||
let decoded = AssetPrim::new_with_string(xml).unwrap();
|
||||
assert_eq!(decoded.parent.as_ref().unwrap().name, root.name);
|
||||
assert_eq!(decoded.children.len(), 1);
|
||||
assert_eq!(decoded.children[0].position, child.position);
|
||||
assert_eq!(decoded.children[0].script_state, child.script_state);
|
||||
assert_eq!(decoded.asset_type(), AssetType::Object);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_or_non_group_xml_is_rejected_without_partial_state() {
|
||||
let mut asset = AssetPrim::new_with_constructor().unwrap();
|
||||
assert!(!asset.decode_xml("<not-a-group/>".into()).unwrap());
|
||||
assert!(asset.decode_xml("<SceneObjectGroup>".into()).is_err());
|
||||
assert!(asset.parent.is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
//! Avatar inheritance, naming, and LLSD persistence.
|
||||
|
||||
use crate::{
|
||||
AgentManagerControlFlags, Animation, AppearanceFlags, AvatarAttachment, AvatarAvatarProperties,
|
||||
AvatarInterests, AvatarStatistics, Error, NameValue, NameValueValueType, Primitive,
|
||||
};
|
||||
use libremetaverse_structured_data::OSD;
|
||||
use libremetaverse_types::{UUID, Vector3};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) fn statistics_get_osd(value: &AvatarStatistics) -> Result<OSD, Error> {
|
||||
Ok(OSD::Map(HashMap::from([
|
||||
(
|
||||
"behavior_positive".into(),
|
||||
OSD::Integer(value.behavior_positive),
|
||||
),
|
||||
(
|
||||
"behavior_negative".into(),
|
||||
OSD::Integer(value.behavior_negative),
|
||||
),
|
||||
(
|
||||
"appearance_positive".into(),
|
||||
OSD::Integer(value.appearance_positive),
|
||||
),
|
||||
(
|
||||
"appearance_negative".into(),
|
||||
OSD::Integer(value.appearance_negative),
|
||||
),
|
||||
(
|
||||
"buildings_positive".into(),
|
||||
OSD::Integer(value.building_positive),
|
||||
),
|
||||
(
|
||||
"buildings_negative".into(),
|
||||
OSD::Integer(value.building_negative),
|
||||
),
|
||||
("given_positive".into(), OSD::Integer(value.given_positive)),
|
||||
("given_negative".into(), OSD::Integer(value.given_negative)),
|
||||
])))
|
||||
}
|
||||
|
||||
pub(crate) fn statistics_from_osd(value: OSD) -> Result<AvatarStatistics, Error> {
|
||||
let OSD::Map(map) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let integer = |key: &str| map.get(key).ok_or(Error::Argument)?.as_integer();
|
||||
Ok(AvatarStatistics {
|
||||
appearance_negative: integer("appearance_negative")?,
|
||||
appearance_positive: integer("appearance_positive")?,
|
||||
// Retain the pinned C# decoder's observable assignment behavior.
|
||||
behavior_negative: 0,
|
||||
behavior_positive: integer("behavior_positive")?,
|
||||
building_negative: integer("buildings_negative")?,
|
||||
building_positive: integer("buildings_positive")?,
|
||||
given_negative: integer("given_negative")?,
|
||||
given_positive: integer("given_positive")?,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Avatar {
|
||||
pub base: Primitive,
|
||||
pub animations: Vec<Animation>,
|
||||
pub appearance_flags: AppearanceFlags,
|
||||
pub appearance_version: u8,
|
||||
pub attachments: Vec<AvatarAttachment>,
|
||||
pub cof_version: i32,
|
||||
pub control_flags: AgentManagerControlFlags,
|
||||
pub groups: Vec<UUID>,
|
||||
pub hover_height: Vector3,
|
||||
pub profile_interests: AvatarInterests,
|
||||
pub profile_properties: AvatarAvatarProperties,
|
||||
pub profile_statistics: AvatarStatistics,
|
||||
pub visual_parameters: Vec<u8>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Avatar {
|
||||
type Target = Primitive;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.base
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Avatar {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.base
|
||||
}
|
||||
}
|
||||
|
||||
impl Avatar {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
crate::visual_catalog::new_avatar()
|
||||
}
|
||||
|
||||
pub fn decode_visual_params(&self) -> Result<HashMap<i32, f32>, Error> {
|
||||
Ok(crate::visual_catalog::decode_visual_params(
|
||||
&self.visual_parameters,
|
||||
))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn first_name(&self) -> String {
|
||||
self.name_value("FirstName")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn last_name(&self) -> String {
|
||||
self.name_value("LastName")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn name(&self) -> String {
|
||||
let first = self.first_name();
|
||||
let last = self.last_name();
|
||||
if first.is_empty() || last.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{first} {last}")
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn group_name(&self) -> String {
|
||||
self.name_value("Title")
|
||||
}
|
||||
|
||||
fn name_value(&self, name: &str) -> String {
|
||||
self.base
|
||||
.name_values
|
||||
.as_ref()
|
||||
.and_then(|values| {
|
||||
values
|
||||
.iter()
|
||||
.find(|value| value.name == name && value.type_ == NameValueValueType::String)
|
||||
})
|
||||
.and_then(|value| value.value.as_ref())
|
||||
.and_then(|value| match value {
|
||||
libremetaverse_types::compat::Object::String(value) => Some(value.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_osd(&self) -> Result<OSD, Error> {
|
||||
let OSD::Map(mut map) = self.base.get_osd()? else {
|
||||
return Err(Error::InvalidOperation);
|
||||
};
|
||||
map.insert(
|
||||
"groups".into(),
|
||||
OSD::Array(self.groups.iter().copied().map(OSD::UUID).collect()),
|
||||
);
|
||||
map.insert(
|
||||
"profile_statistics".into(),
|
||||
self.profile_statistics.get_osd()?,
|
||||
);
|
||||
map.insert(
|
||||
"profile_properties".into(),
|
||||
self.profile_properties.get_osd()?,
|
||||
);
|
||||
map.insert("profile_interest".into(), self.profile_interests.get_osd()?);
|
||||
map.insert("control_flags".into(), OSD::Integer(self.control_flags.0));
|
||||
map.insert(
|
||||
"visual_parameters".into(),
|
||||
OSD::Array(
|
||||
self.visual_parameters
|
||||
.iter()
|
||||
.map(|value| OSD::Integer(i32::from(*value)))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
map.insert("first_name".into(), OSD::String(self.first_name()));
|
||||
map.insert("last_name".into(), OSD::String(self.last_name()));
|
||||
map.insert("group_name".into(), OSD::String(self.group_name()));
|
||||
Ok(OSD::Map(map))
|
||||
}
|
||||
|
||||
pub fn from_osd(osd: OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(map) = &osd else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let mut avatar = Self::new()?;
|
||||
avatar.base = Primitive::from_osd(osd.clone())?;
|
||||
avatar.groups = osd_array(map.get("groups"))
|
||||
.iter()
|
||||
.filter_map(|value| value.as_uuid().ok())
|
||||
.collect();
|
||||
if let Some(value) = map.get("profile_statistics") {
|
||||
avatar.profile_statistics = AvatarStatistics::from_osd(value.clone())?;
|
||||
}
|
||||
if let Some(value) = map.get("profile_properties") {
|
||||
avatar.profile_properties = AvatarAvatarProperties::from_osd(value.clone())?;
|
||||
}
|
||||
if let Some(value) = map.get("profile_interest") {
|
||||
avatar.profile_interests = AvatarInterests::from_osd(value.clone())?;
|
||||
}
|
||||
avatar.control_flags =
|
||||
AgentManagerControlFlags(map.get("control_flags").map_or(Ok(0), OSD::as_integer)?);
|
||||
avatar.visual_parameters = osd_array(map.get("visual_parameters"))
|
||||
.iter()
|
||||
.map(|value| value.as_integer().map(|number| number.clamp(0, 255) as u8))
|
||||
.collect::<Result<_, _>>()?;
|
||||
avatar.base.name_values = Some(vec![
|
||||
string_name_value("FirstName", map.get("first_name"))?,
|
||||
string_name_value("LastName", map.get("last_name"))?,
|
||||
string_name_value("Title", map.get("group_name"))?,
|
||||
]);
|
||||
Ok(avatar)
|
||||
}
|
||||
}
|
||||
|
||||
fn osd_array(value: Option<&OSD>) -> &[OSD] {
|
||||
match value {
|
||||
Some(OSD::Array(values)) => values,
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn string_name_value(name: &str, value: Option<&OSD>) -> Result<NameValue, Error> {
|
||||
NameValue::new_with_string_value_type_class_type_sendto_type_object(
|
||||
name.into(),
|
||||
NameValueValueType::String,
|
||||
crate::NameValueClassType::ReadOnly,
|
||||
crate::NameValueSendtoType::Sim,
|
||||
libremetaverse_types::compat::Object::String(
|
||||
value.map_or(Ok(String::new()), OSD::as_string)?,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
//! Native spring-mass-damper simulation for avatar physics wearables.
|
||||
|
||||
use crate::Error;
|
||||
use libremetaverse_types::compat::Vector3;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
type BoneProvider = Arc<dyn Fn(String) -> Vector3 + Send + Sync>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Channel {
|
||||
driver: i32,
|
||||
driven: &'static [i32],
|
||||
direction: [f32; 3],
|
||||
params: [i32; 7],
|
||||
position: f32,
|
||||
velocity: f32,
|
||||
joint_velocity: f32,
|
||||
joint_acceleration: f32,
|
||||
last_time: f32,
|
||||
world_position: [f32; 3],
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
fn new(driver: i32, driven: &'static [i32], direction: [f32; 3], params: [i32; 7]) -> Self {
|
||||
Self {
|
||||
driver,
|
||||
driven,
|
||||
direction,
|
||||
params,
|
||||
position: 0.5,
|
||||
velocity: 0.0,
|
||||
joint_velocity: 0.0,
|
||||
joint_acceleration: 0.0,
|
||||
last_time: -1.0,
|
||||
world_position: [0.0; 3],
|
||||
}
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.position = 0.5;
|
||||
self.velocity = 0.0;
|
||||
self.joint_velocity = 0.0;
|
||||
self.joint_acceleration = 0.0;
|
||||
self.last_time = -1.0;
|
||||
self.world_position = [0.0; 3];
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
channels: Vec<Channel>,
|
||||
weights: HashMap<i32, f32>,
|
||||
provider: Option<BoneProvider>,
|
||||
}
|
||||
|
||||
/// Stateful simulator matching the viewer's six LLPhysicsMotion channels.
|
||||
pub struct AvatarPhysicsSimulator {
|
||||
state: Mutex<State>,
|
||||
}
|
||||
|
||||
impl AvatarPhysicsSimulator {
|
||||
pub(crate) fn native_new() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
state: Mutex::new(State {
|
||||
channels: vec![
|
||||
Channel::new(
|
||||
1100,
|
||||
&[1200],
|
||||
[0.0, 0.0, -1.0],
|
||||
[10000, 10001, 10002, 10006, 10003, 10004, 10005],
|
||||
),
|
||||
Channel::new(
|
||||
1101,
|
||||
&[1201],
|
||||
[-1.0, 0.0, 0.0],
|
||||
[10000, 10001, 10002, 10010, 10007, 10008, 10009],
|
||||
),
|
||||
Channel::new(
|
||||
1105,
|
||||
&[1207],
|
||||
[0.0, -1.0, 0.0],
|
||||
[10000, 10001, 10002, 10032, 10029, 10030, 10031],
|
||||
),
|
||||
Channel::new(
|
||||
1103,
|
||||
&[1205],
|
||||
[0.0, 0.0, -1.0],
|
||||
[10018, 10019, 10020, 10024, 10021, 10022, 10023],
|
||||
),
|
||||
Channel::new(
|
||||
1104,
|
||||
&[1206],
|
||||
[0.0, -1.0, 0.0],
|
||||
[10018, 10019, 10020, 10028, 10025, 10026, 10027],
|
||||
),
|
||||
Channel::new(
|
||||
1102,
|
||||
&[1202, 1203, 1204],
|
||||
[0.0, 0.0, -1.0],
|
||||
[10011, 10012, 10013, 10017, 10014, 10015, 10016],
|
||||
),
|
||||
],
|
||||
weights: HashMap::new(),
|
||||
provider: None,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_wearable_params(
|
||||
&self,
|
||||
values: HashMap<i32, f32>,
|
||||
) -> Result<(), Error> {
|
||||
let mut state = self
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
state.weights = values;
|
||||
state.channels.iter_mut().for_each(Channel::reset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_bone_position_provider(
|
||||
&self,
|
||||
provider: Box<dyn Fn(String) -> Vector3 + Send + Sync>,
|
||||
) -> Result<(), Error> {
|
||||
self.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.provider = Some(provider.into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_tick(&self, time: f32) -> Result<HashMap<i32, f32>, Error> {
|
||||
if !time.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut state = self
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let weights = state.weights.clone();
|
||||
let provider = state.provider.clone();
|
||||
let mut output = HashMap::new();
|
||||
for channel in &mut state.channels {
|
||||
let position = step(channel, time, &weights, provider.as_ref());
|
||||
for id in channel.driven {
|
||||
output.insert(*id, position);
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
fn param(weights: &HashMap<i32, f32>, id: i32, fallback: f32) -> f32 {
|
||||
weights
|
||||
.get(&id)
|
||||
.copied()
|
||||
.or_else(|| crate::visual_catalog::param_by_id(id).map(|p| p.default_value))
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
|
||||
fn driver_position(channel: &Channel, weights: &HashMap<i32, f32>) -> f32 {
|
||||
let Some(definition) = crate::visual_catalog::param_by_id(channel.driver) else {
|
||||
return 0.5;
|
||||
};
|
||||
let range = definition.max_value - definition.min_value;
|
||||
if range.abs() < 1.0e-6 {
|
||||
return 0.0;
|
||||
}
|
||||
((weights
|
||||
.get(&channel.driver)
|
||||
.copied()
|
||||
.unwrap_or(definition.default_value)
|
||||
- definition.min_value)
|
||||
/ range)
|
||||
.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
fn dot(left: [f32; 3], right: [f32; 3]) -> f32 {
|
||||
left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
|
||||
}
|
||||
fn subtract(left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
|
||||
[left[0] - right[0], left[1] - right[1], left[2] - right[2]]
|
||||
}
|
||||
fn vector(value: Vector3) -> [f32; 3] {
|
||||
value.0
|
||||
}
|
||||
|
||||
fn step(
|
||||
channel: &mut Channel,
|
||||
time: f32,
|
||||
weights: &HashMap<i32, f32>,
|
||||
provider: Option<&BoneProvider>,
|
||||
) -> f32 {
|
||||
let joint_name = if matches!(channel.driver, 1100 | 1101 | 1105) {
|
||||
"mChest"
|
||||
} else {
|
||||
"mPelvis"
|
||||
};
|
||||
let joint = provider.map_or(channel.world_position, |get| vector(get(joint_name.into())));
|
||||
let target = driver_position(channel, weights);
|
||||
if channel.last_time < 0.0 {
|
||||
channel.last_time = time;
|
||||
channel.position = target;
|
||||
channel.world_position = joint;
|
||||
return target;
|
||||
}
|
||||
let dt = time - channel.last_time;
|
||||
if dt <= 0.0 || dt > 1.0 {
|
||||
channel.last_time = time;
|
||||
return channel.position.clamp(0.0, 1.0);
|
||||
}
|
||||
let [
|
||||
mass_id,
|
||||
gravity_id,
|
||||
drag_id,
|
||||
damping_id,
|
||||
max_id,
|
||||
spring_id,
|
||||
gain_id,
|
||||
] = channel.params;
|
||||
let mass = param(weights, mass_id, 0.1).max(1.0e-6);
|
||||
let gravity = param(weights, gravity_id, 0.0);
|
||||
let drag = param(weights, drag_id, 1.0);
|
||||
let damping = param(weights, damping_id, 0.2);
|
||||
let max_effect = param(weights, max_id, 0.0);
|
||||
let spring = param(weights, spring_id, 10.0);
|
||||
let gain = param(weights, gain_id, 10.0);
|
||||
let scaled_dt = dt * 30.0;
|
||||
let joint_velocity =
|
||||
dot(subtract(joint, channel.world_position), channel.direction) * 100.0 / scaled_dt;
|
||||
let acceleration = ((joint_velocity - channel.joint_velocity) / scaled_dt) / 3.0
|
||||
+ channel.joint_acceleration * (2.0 / 3.0);
|
||||
if max_effect == 0.0 && channel.position.clamp(0.0, 1.0) == target {
|
||||
channel.last_time = time;
|
||||
channel.world_position = joint;
|
||||
channel.joint_velocity = joint_velocity;
|
||||
channel.joint_acceleration = acceleration;
|
||||
return target;
|
||||
}
|
||||
let steps = (dt / 0.05) as u32 + 1;
|
||||
let sub_dt = dt / steps as f32;
|
||||
let mut position = channel.position.clamp(0.0, 1.0);
|
||||
let mut velocity = channel.velocity;
|
||||
for _ in 0..steps {
|
||||
let force = gain * acceleration * mass
|
||||
+ dot([0.0, 0.0, 1.0], channel.direction) * gravity * mass
|
||||
- (position - target) * spring
|
||||
- damping * velocity
|
||||
+ 0.5 * drag * joint_velocity * joint_velocity * joint_velocity.signum();
|
||||
velocity = (velocity + force / mass * sub_dt).clamp(-100.0, 100.0);
|
||||
position += velocity * sub_dt;
|
||||
if (position < 0.0 && velocity < 0.0) || (position > 1.0 && velocity > 0.0) {
|
||||
velocity = 0.0;
|
||||
}
|
||||
}
|
||||
if !position.is_finite() || !velocity.is_finite() {
|
||||
position = 0.0;
|
||||
velocity = 0.0;
|
||||
channel.joint_velocity = 0.0;
|
||||
channel.joint_acceleration = 0.0;
|
||||
}
|
||||
channel.last_time = time;
|
||||
channel.position = position;
|
||||
channel.velocity = velocity;
|
||||
channel.joint_velocity = joint_velocity;
|
||||
channel.joint_acceleration = acceleration;
|
||||
channel.world_position = joint;
|
||||
position.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn exposes_all_driven_channels_and_resets_to_driver_weights() {
|
||||
let simulator = AvatarPhysicsSimulator::native_new().unwrap();
|
||||
simulator
|
||||
.native_set_wearable_params(HashMap::from([(1100, 0.25)]))
|
||||
.unwrap();
|
||||
let output = simulator.native_tick(1.0).unwrap();
|
||||
assert_eq!(output.len(), 8);
|
||||
assert!((0.0..=1.0).contains(&output[&1200]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
//! Deterministic reference renderer built on the checked native prim mesher.
|
||||
|
||||
#![allow(clippy::cast_possible_truncation)] // Checked C# float-to-int twist conversion.
|
||||
#![allow(clippy::missing_errors_doc)] // Public signatures are fixed by the API mapping.
|
||||
#![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped surface.
|
||||
#![allow(clippy::needless_pass_by_value)] // Mapped value parameters are owned.
|
||||
#![allow(clippy::unnecessary_wraps)] // The mapped constructor is fallible in the C# API.
|
||||
#![allow(clippy::unused_self)] // IRendering exposes these operations as instance methods.
|
||||
|
||||
use libremetaverse::rendering::{
|
||||
DetailLevel, Face, FaceMask, FacetedMesh, IRendering, SimpleMesh, Vertex,
|
||||
};
|
||||
use libremetaverse::{MappingType, Primitive, PrimitiveTextureEntryFace};
|
||||
use libremetaverse_imaging::ManagedImage;
|
||||
use libremetaverse_prim_mesher::{
|
||||
PathType, PrimMesh, SculptMesh, SculptMeshSculptType, VertexIndexer,
|
||||
};
|
||||
use libremetaverse_types::compat::Object;
|
||||
use libremetaverse_types::{
|
||||
Error, HoleType, PathCurve, ProfileCurve, SculptType, Vector2, Vector3,
|
||||
};
|
||||
|
||||
const MAX_FACE_VERTICES: usize = 65_536;
|
||||
const MAX_TOTAL_VERTICES: usize = MAX_FACE_VERTICES;
|
||||
const MAX_TOTAL_INDICES: usize = MAX_TOTAL_VERTICES * 6;
|
||||
|
||||
/// Small, deterministic renderer for prim and sculpt geometry.
|
||||
pub(crate) struct SimpleRenderer;
|
||||
|
||||
impl SimpleRenderer {
|
||||
pub(crate) fn native_new() -> Result<Self, Error> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
pub(crate) fn native_generate_simple_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
lod: DetailLevel,
|
||||
) -> Result<SimpleMesh, Error> {
|
||||
let source = prim.id;
|
||||
let faceted = self.native_generate_faceted_mesh(prim, lod)?;
|
||||
flatten_faces(faceted.faces, source)
|
||||
}
|
||||
|
||||
pub(crate) fn native_generate_simple_sculpt_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
sculpt_texture: ManagedImage,
|
||||
lod: DetailLevel,
|
||||
) -> Result<SimpleMesh, Error> {
|
||||
let source = prim.id;
|
||||
let faceted = self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)?;
|
||||
flatten_faces(faceted.faces, source)
|
||||
}
|
||||
|
||||
pub(crate) fn native_generate_faceted_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
lod: DetailLevel,
|
||||
) -> Result<FacetedMesh, Error> {
|
||||
let source = prim.id;
|
||||
let mesh = generate_prim_mesh(&prim, lod, source)?;
|
||||
let indexer = mesh
|
||||
.get_vertex_indexer()
|
||||
.map_err(|_| render_error(source, "index primitive viewer faces"))?
|
||||
.ok_or_else(|| render_error(source, "primitive viewer faces are unavailable"))?;
|
||||
let faces = indexed_faces(&prim, indexer, source)?;
|
||||
Ok(FacetedMesh {
|
||||
faces,
|
||||
skin_data: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_generate_faceted_sculpt_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
sculpt_texture: ManagedImage,
|
||||
lod: DetailLevel,
|
||||
) -> Result<FacetedMesh, Error> {
|
||||
let source = prim.id;
|
||||
let sculpt = prim
|
||||
.sculpt
|
||||
.as_ref()
|
||||
.ok_or_else(|| render_error(source, "primitive has no sculpt metadata"))?;
|
||||
let sculpt_type = match sculpt.type_() {
|
||||
SculptType::Cylinder => SculptMeshSculptType::Cylinder,
|
||||
SculptType::Plane => SculptMeshSculptType::Plane,
|
||||
SculptType::Sphere => SculptMeshSculptType::Sphere,
|
||||
SculptType::Torus => SculptMeshSculptType::Torus,
|
||||
SculptType::Mesh => {
|
||||
return Err(render_error(
|
||||
source,
|
||||
"mesh sculpt requires a decoded mesh asset",
|
||||
));
|
||||
}
|
||||
SculptType::None | SculptType::Invert | SculptType::Mirror => {
|
||||
return Err(render_error(source, "unsupported sculpt topology"));
|
||||
}
|
||||
};
|
||||
let mesher_lod = match lod {
|
||||
DetailLevel::Highest | DetailLevel::High => 32,
|
||||
DetailLevel::Medium => 16,
|
||||
DetailLevel::Low => 8,
|
||||
};
|
||||
let mesh = SculptMesh::new_with_managed_image_sculpt_type_int32_boolean_boolean_boolean(
|
||||
sculpt_texture,
|
||||
sculpt_type,
|
||||
mesher_lod,
|
||||
true,
|
||||
sculpt.mirror(),
|
||||
sculpt.invert(),
|
||||
)
|
||||
.map_err(|_| render_error(source, "generate bounded sculpt geometry"))?;
|
||||
if mesh.coords.len() > MAX_FACE_VERTICES
|
||||
|| mesh.coords.len() != mesh.normals.len()
|
||||
|| mesh.coords.len() != mesh.uvs.len()
|
||||
{
|
||||
return Err(render_error(source, "invalid sculpt vertex domains"));
|
||||
}
|
||||
let vertices = mesh
|
||||
.coords
|
||||
.iter()
|
||||
.zip(&mesh.normals)
|
||||
.zip(&mesh.uvs)
|
||||
.map(|((position, normal), uv)| {
|
||||
checked_vertex(
|
||||
Vector3 {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
z: position.z,
|
||||
},
|
||||
Vector3 {
|
||||
x: normal.x,
|
||||
y: normal.y,
|
||||
z: normal.z,
|
||||
},
|
||||
Vector2 { x: uv.u, y: uv.v },
|
||||
source,
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut indices = Vec::with_capacity(
|
||||
mesh.faces
|
||||
.len()
|
||||
.checked_mul(3)
|
||||
.ok_or_else(|| render_error(source, "sculpt index count overflow"))?,
|
||||
);
|
||||
for face in mesh.faces {
|
||||
push_triangle(
|
||||
&mut indices,
|
||||
face.v1,
|
||||
face.v2,
|
||||
face.v3,
|
||||
vertices.len(),
|
||||
source,
|
||||
)?;
|
||||
}
|
||||
let texture = texture_for_face(&prim, 0, source)?;
|
||||
let face = make_face(0, vertices, indices, texture, source)?;
|
||||
Ok(FacetedMesh {
|
||||
faces: vec![face],
|
||||
skin_data: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_transform_tex_coords(
|
||||
&self,
|
||||
vertices: &mut Vec<Vertex>,
|
||||
center: Vector3,
|
||||
te_face: PrimitiveTextureEntryFace,
|
||||
prim_scale: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
if !finite3(center)
|
||||
|| !finite3(prim_scale)
|
||||
|| ![
|
||||
te_face.repeat_u(),
|
||||
te_face.repeat_v(),
|
||||
te_face.offset_u(),
|
||||
te_face.offset_v(),
|
||||
te_face.rotation(),
|
||||
]
|
||||
.into_iter()
|
||||
.all(f32::is_finite)
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let cosine = te_face.rotation().cos();
|
||||
let sine = te_face.rotation().sin();
|
||||
for vertex in vertices {
|
||||
if !finite3(vertex.position) || !finite3(vertex.normal) || !finite2(vertex.tex_coord) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if te_face.tex_map_type() == MappingType::Planar {
|
||||
let mut binormal;
|
||||
let normal_x = vertex.normal.x;
|
||||
if (-0.5..0.5).contains(&normal_x) {
|
||||
binormal = Vector3::unit_x();
|
||||
if vertex.normal.y > 0.0 {
|
||||
binormal.x = -1.0;
|
||||
}
|
||||
} else {
|
||||
binormal = Vector3::unit_y();
|
||||
if normal_x < 0.0 {
|
||||
binormal.y = -1.0;
|
||||
}
|
||||
}
|
||||
let tangent = cross(binormal, vertex.normal);
|
||||
let scaled = Vector3 {
|
||||
x: vertex.position.x * prim_scale.x,
|
||||
y: vertex.position.y * prim_scale.y,
|
||||
z: vertex.position.z * prim_scale.z,
|
||||
};
|
||||
vertex.tex_coord.x = 0.5 + dot(binormal, scaled) * 2.0;
|
||||
vertex.tex_coord.y = 0.5 - dot(tangent, scaled) * 2.0;
|
||||
}
|
||||
let x = vertex.tex_coord.x - 0.5;
|
||||
let y = vertex.tex_coord.y - 0.5;
|
||||
vertex.tex_coord.x =
|
||||
(x * cosine + y * sine) * te_face.repeat_u() + te_face.offset_u() + 0.5;
|
||||
vertex.tex_coord.y =
|
||||
(-x * sine + y * cosine) * te_face.repeat_v() + te_face.offset_v() + 0.5;
|
||||
if !finite2(vertex.tex_coord) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl IRendering for SimpleRenderer {
|
||||
fn generate_faceted_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
lod: DetailLevel,
|
||||
) -> Result<Option<FacetedMesh>, Error> {
|
||||
self.native_generate_faceted_mesh(prim, lod).map(Some)
|
||||
}
|
||||
|
||||
fn generate_faceted_sculpt_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
sculpt_texture: ManagedImage,
|
||||
lod: DetailLevel,
|
||||
) -> Result<Option<FacetedMesh>, Error> {
|
||||
self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn generate_simple_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
lod: DetailLevel,
|
||||
) -> Result<Option<SimpleMesh>, Error> {
|
||||
self.native_generate_simple_mesh(prim, lod).map(Some)
|
||||
}
|
||||
|
||||
fn generate_simple_sculpt_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
sculpt_texture: ManagedImage,
|
||||
lod: DetailLevel,
|
||||
) -> Result<Option<SimpleMesh>, Error> {
|
||||
self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn transform_tex_coords(
|
||||
&self,
|
||||
vertices: &mut Vec<Vertex>,
|
||||
center: Vector3,
|
||||
te_face: PrimitiveTextureEntryFace,
|
||||
prim_scale: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
self.native_transform_tex_coords(vertices, center, te_face, prim_scale)
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_prim_mesh(
|
||||
prim: &Primitive,
|
||||
lod: DetailLevel,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<PrimMesh, Error> {
|
||||
let data = &prim.prim_data;
|
||||
let parameters = [
|
||||
data.profile_begin,
|
||||
data.profile_end,
|
||||
data.profile_hollow,
|
||||
data.path_scale_x,
|
||||
data.path_scale_y,
|
||||
data.path_begin,
|
||||
data.path_end,
|
||||
data.path_shear_x,
|
||||
data.path_shear_y,
|
||||
data.path_radius_offset,
|
||||
data.path_revolutions,
|
||||
data.path_skew,
|
||||
data.path_taper_x,
|
||||
data.path_taper_y,
|
||||
data.path_twist_begin,
|
||||
data.path_twist,
|
||||
];
|
||||
if !parameters.into_iter().all(f32::is_finite) {
|
||||
return Err(render_error(
|
||||
source,
|
||||
"non-finite primitive construction data",
|
||||
));
|
||||
}
|
||||
let (mut sides, mut profile_begin, mut profile_end, sphere_mode) =
|
||||
match data.profile_curve_with_property() {
|
||||
ProfileCurve::Circle => (lod_sides(lod), data.profile_begin, data.profile_end, false),
|
||||
ProfileCurve::EqualTriangle
|
||||
| ProfileCurve::IsoTriangle
|
||||
| ProfileCurve::RightTriangle => (3, data.profile_begin, data.profile_end, false),
|
||||
ProfileCurve::HalfCircle => (
|
||||
lod_sides(lod),
|
||||
data.profile_begin.mul_add(0.5, 0.5),
|
||||
data.profile_end.mul_add(0.5, 0.5),
|
||||
true,
|
||||
),
|
||||
ProfileCurve::Square => (4, data.profile_begin, data.profile_end, false),
|
||||
};
|
||||
if sides < 3 {
|
||||
sides = 3;
|
||||
}
|
||||
profile_begin = profile_begin.clamp(0.0, 1.0);
|
||||
profile_end = profile_end.clamp(0.0, 1.0);
|
||||
let hollow_sides = if data.profile_hole() == HoleType::Circle {
|
||||
lod_sides(lod)
|
||||
} else if data.profile_hole() == HoleType::Triangle {
|
||||
3
|
||||
} else if data.profile_hole() == HoleType::Same {
|
||||
sides
|
||||
} else {
|
||||
4
|
||||
};
|
||||
let mut mesh = PrimMesh::new(
|
||||
sides,
|
||||
profile_begin,
|
||||
profile_end,
|
||||
data.profile_hollow,
|
||||
hollow_sides,
|
||||
)
|
||||
.map_err(|_| render_error(source, "initialize primitive profile"))?;
|
||||
mesh.viewer_mode = true;
|
||||
mesh.sphere_mode = sphere_mode;
|
||||
mesh.hole_size_x = data.path_scale_x;
|
||||
mesh.hole_size_y = data.path_scale_y;
|
||||
mesh.path_cut_begin = data.path_begin;
|
||||
mesh.path_cut_end = data.path_end;
|
||||
mesh.top_shear_x = data.path_shear_x;
|
||||
mesh.top_shear_y = data.path_shear_y;
|
||||
mesh.radius = data.path_radius_offset;
|
||||
mesh.revolutions = data.path_revolutions;
|
||||
mesh.skew = data.path_skew;
|
||||
mesh.steps_per_revolution = lod_sides(lod);
|
||||
let path_type = if matches!(data.path_curve, PathCurve::Line | PathCurve::Flexible) {
|
||||
mesh.taper_x = 1.0 - data.path_scale_x;
|
||||
mesh.taper_y = 1.0 - data.path_scale_y;
|
||||
mesh.twist_begin = checked_twist(data.path_twist_begin, 180.0, source)?;
|
||||
mesh.twist_end = checked_twist(data.path_twist, 180.0, source)?;
|
||||
PathType::Linear
|
||||
} else {
|
||||
mesh.taper_x = data.path_taper_x;
|
||||
mesh.taper_y = data.path_taper_y;
|
||||
mesh.twist_begin = checked_twist(data.path_twist_begin, 360.0, source)?;
|
||||
mesh.twist_end = checked_twist(data.path_twist, 360.0, source)?;
|
||||
PathType::Circular
|
||||
};
|
||||
mesh.extrude(path_type)
|
||||
.map_err(|_| render_error(source, "extrude primitive geometry"))?;
|
||||
if mesh.coords.len() > MAX_TOTAL_VERTICES || mesh.viewer_faces.len() > MAX_TOTAL_INDICES / 3 {
|
||||
return Err(render_error(
|
||||
source,
|
||||
"primitive geometry exceeds renderer bounds",
|
||||
));
|
||||
}
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
fn indexed_faces(
|
||||
prim: &Primitive,
|
||||
indexer: VertexIndexer,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<Vec<Face>, Error> {
|
||||
let count = usize::try_from(indexer.num_prim_faces)
|
||||
.map_err(|_| render_error(source, "invalid primitive face count"))?;
|
||||
if count != indexer.viewer_vertices.len() || count != indexer.viewer_polygons.len() {
|
||||
return Err(render_error(source, "misaligned primitive face domains"));
|
||||
}
|
||||
let mut total_vertices = 0usize;
|
||||
let mut total_indices = 0usize;
|
||||
let mut faces = Vec::with_capacity(count);
|
||||
for face_index in 0..count {
|
||||
let source_vertices = &indexer.viewer_vertices[face_index];
|
||||
let polygons = indexer.viewer_polygons[face_index]
|
||||
.as_ref()
|
||||
.ok_or_else(|| render_error(source, "primitive face has no polygon domain"))?;
|
||||
if source_vertices.is_empty() {
|
||||
if polygons.is_empty() {
|
||||
continue;
|
||||
}
|
||||
return Err(render_error(
|
||||
source,
|
||||
"primitive face polygons have no vertex domain",
|
||||
));
|
||||
}
|
||||
if source_vertices.len() > MAX_FACE_VERTICES {
|
||||
return Err(render_error(
|
||||
source,
|
||||
"primitive face exceeds 16-bit vertex range",
|
||||
));
|
||||
}
|
||||
total_vertices = total_vertices
|
||||
.checked_add(source_vertices.len())
|
||||
.ok_or_else(|| render_error(source, "primitive vertex count overflow"))?;
|
||||
if total_vertices > MAX_TOTAL_VERTICES {
|
||||
return Err(render_error(source, "primitive vertex budget exceeded"));
|
||||
}
|
||||
let vertices = source_vertices
|
||||
.iter()
|
||||
.map(|vertex| {
|
||||
checked_vertex(
|
||||
Vector3 {
|
||||
x: vertex.v.x,
|
||||
y: vertex.v.y,
|
||||
z: vertex.v.z,
|
||||
},
|
||||
Vector3 {
|
||||
x: vertex.n.x,
|
||||
y: vertex.n.y,
|
||||
z: vertex.n.z,
|
||||
},
|
||||
Vector2 {
|
||||
x: vertex.uv.u,
|
||||
y: 1.0 - vertex.uv.v,
|
||||
},
|
||||
source,
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut indices = Vec::with_capacity(
|
||||
polygons
|
||||
.len()
|
||||
.checked_mul(3)
|
||||
.ok_or_else(|| render_error(source, "primitive index count overflow"))?,
|
||||
);
|
||||
for polygon in polygons {
|
||||
if polygon.v1 == polygon.v2 || polygon.v1 == polygon.v3 || polygon.v2 == polygon.v3 {
|
||||
continue;
|
||||
}
|
||||
push_triangle(
|
||||
&mut indices,
|
||||
polygon.v1,
|
||||
polygon.v2,
|
||||
polygon.v3,
|
||||
vertices.len(),
|
||||
source,
|
||||
)?;
|
||||
}
|
||||
total_indices = total_indices
|
||||
.checked_add(indices.len())
|
||||
.ok_or_else(|| render_error(source, "primitive index count overflow"))?;
|
||||
if total_indices > MAX_TOTAL_INDICES {
|
||||
return Err(render_error(source, "primitive index budget exceeded"));
|
||||
}
|
||||
let texture = texture_for_face(prim, face_index, source)?;
|
||||
faces.push(make_face(
|
||||
i32::try_from(face_index)
|
||||
.map_err(|_| render_error(source, "primitive face id overflow"))?,
|
||||
vertices,
|
||||
indices,
|
||||
texture,
|
||||
source,
|
||||
)?);
|
||||
}
|
||||
Ok(faces)
|
||||
}
|
||||
|
||||
fn flatten_faces(
|
||||
faces: Vec<Face>,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<SimpleMesh, Error> {
|
||||
let mut vertices = Vec::new();
|
||||
let mut indices = Vec::new();
|
||||
for face in faces {
|
||||
let base = vertices.len();
|
||||
let combined = base
|
||||
.checked_add(face.vertices.len())
|
||||
.ok_or_else(|| render_error(source, "simple mesh vertex count overflow"))?;
|
||||
if combined > MAX_TOTAL_VERTICES {
|
||||
return Err(render_error(
|
||||
source,
|
||||
"simple mesh exceeds 16-bit vertex range",
|
||||
));
|
||||
}
|
||||
for index in face.indices {
|
||||
let global = base
|
||||
.checked_add(usize::from(index))
|
||||
.ok_or_else(|| render_error(source, "simple mesh index overflow"))?;
|
||||
if global >= combined {
|
||||
return Err(render_error(source, "simple mesh index is out of range"));
|
||||
}
|
||||
indices.push(
|
||||
u16::try_from(global)
|
||||
.map_err(|_| render_error(source, "simple mesh index exceeds 16 bits"))?,
|
||||
);
|
||||
}
|
||||
vertices.extend(face.vertices);
|
||||
}
|
||||
if indices.len() > MAX_TOTAL_INDICES {
|
||||
return Err(render_error(source, "simple mesh index budget exceeded"));
|
||||
}
|
||||
Ok(SimpleMesh { indices, vertices })
|
||||
}
|
||||
|
||||
fn texture_for_face(
|
||||
prim: &Primitive,
|
||||
index: usize,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<PrimitiveTextureEntryFace, Error> {
|
||||
let Some(textures) = prim.textures.as_ref() else {
|
||||
return Ok(PrimitiveTextureEntryFace::default());
|
||||
};
|
||||
let index =
|
||||
u32::try_from(index).map_err(|_| render_error(source, "texture face index overflow"))?;
|
||||
textures
|
||||
.get_face(index)
|
||||
.map_err(|_| render_error(source, "texture face index is out of range"))
|
||||
.map(|face| face.cloned().unwrap_or_default())
|
||||
}
|
||||
|
||||
fn make_face(
|
||||
id: i32,
|
||||
vertices: Vec<Vertex>,
|
||||
indices: Vec<u16>,
|
||||
texture_face: PrimitiveTextureEntryFace,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<Face, Error> {
|
||||
let first = vertices
|
||||
.first()
|
||||
.ok_or_else(|| render_error(source, "render face has no vertices"))?;
|
||||
let mut min = first.position;
|
||||
let mut max = first.position;
|
||||
for vertex in &vertices[1..] {
|
||||
min.x = min.x.min(vertex.position.x);
|
||||
min.y = min.y.min(vertex.position.y);
|
||||
min.z = min.z.min(vertex.position.z);
|
||||
max.x = max.x.max(vertex.position.x);
|
||||
max.y = max.y.max(vertex.position.y);
|
||||
max.z = max.z.max(vertex.position.z);
|
||||
}
|
||||
let center = Vector3 {
|
||||
x: (min.x + max.x) * 0.5,
|
||||
y: (min.y + max.y) * 0.5,
|
||||
z: (min.z + max.z) * 0.5,
|
||||
};
|
||||
Ok(Face {
|
||||
begin_s: 0,
|
||||
begin_t: 0,
|
||||
center,
|
||||
edge: Vec::new(),
|
||||
id,
|
||||
indices,
|
||||
mask: FaceMask::SINGLE,
|
||||
max_extent: max,
|
||||
min_extent: min,
|
||||
normalized_scale: Vector3 {
|
||||
x: max.x - min.x,
|
||||
y: max.y - min.y,
|
||||
z: max.z - min.z,
|
||||
},
|
||||
num_s: 0,
|
||||
num_t: 0,
|
||||
tex_coords1: None,
|
||||
texture_face,
|
||||
user_data: Object::Undefined,
|
||||
vertices,
|
||||
weights: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn checked_vertex(
|
||||
position: Vector3,
|
||||
normal: Vector3,
|
||||
tex_coord: Vector2,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<Vertex, Error> {
|
||||
if !finite3(position) || !finite3(normal) || !finite2(tex_coord) {
|
||||
return Err(render_error(
|
||||
source,
|
||||
"renderer produced a non-finite vertex",
|
||||
));
|
||||
}
|
||||
Ok(Vertex {
|
||||
position,
|
||||
normal,
|
||||
tex_coord,
|
||||
})
|
||||
}
|
||||
|
||||
fn push_triangle(
|
||||
indices: &mut Vec<u16>,
|
||||
v1: i32,
|
||||
v2: i32,
|
||||
v3: i32,
|
||||
vertex_count: usize,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<(), Error> {
|
||||
for index in [v1, v2, v3] {
|
||||
let index =
|
||||
usize::try_from(index).map_err(|_| render_error(source, "negative renderer index"))?;
|
||||
if index >= vertex_count {
|
||||
return Err(render_error(source, "renderer index is out of range"));
|
||||
}
|
||||
indices.push(
|
||||
u16::try_from(index)
|
||||
.map_err(|_| render_error(source, "renderer index exceeds 16 bits"))?,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn checked_twist(
|
||||
value: f32,
|
||||
multiplier: f32,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<i32, Error> {
|
||||
let degrees = value * multiplier;
|
||||
if !degrees.is_finite() || degrees.abs() > 360_000.0 {
|
||||
return Err(render_error(source, "primitive twist is out of range"));
|
||||
}
|
||||
Ok(degrees as i32)
|
||||
}
|
||||
|
||||
const fn lod_sides(lod: DetailLevel) -> i32 {
|
||||
match lod {
|
||||
DetailLevel::Low => 6,
|
||||
DetailLevel::Medium => 12,
|
||||
DetailLevel::High | DetailLevel::Highest => 24,
|
||||
}
|
||||
}
|
||||
|
||||
const fn render_error(source: libremetaverse_types::UUID, context: &'static str) -> Error {
|
||||
Error::Rendering { source, context }
|
||||
}
|
||||
|
||||
const fn finite2(value: Vector2) -> bool {
|
||||
value.x.is_finite() && value.y.is_finite()
|
||||
}
|
||||
|
||||
const fn finite3(value: Vector3) -> bool {
|
||||
value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
|
||||
}
|
||||
|
||||
const fn dot(left: Vector3, right: Vector3) -> f32 {
|
||||
left.x * right.x + left.y * right.y + left.z * right.z
|
||||
}
|
||||
|
||||
const fn cross(left: Vector3, right: Vector3) -> Vector3 {
|
||||
Vector3 {
|
||||
x: left.y * right.z - left.z * right.y,
|
||||
y: left.z * right.x - left.x * right.z,
|
||||
z: left.x * right.y - left.y * right.x,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
//! Native callback and legacy asynchronous-delegate adapters.
|
||||
|
||||
use std::any::Any;
|
||||
use std::fmt;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
||||
use libremetaverse_types::compat::{DictionaryEntry, Object};
|
||||
|
||||
use crate::{DictionaryEventAction, Error};
|
||||
|
||||
/// Completion object returned by mapped delegate `BeginInvoke` methods.
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncInvocation {
|
||||
state: Arc<AsyncInvocationState>,
|
||||
async_state: Object,
|
||||
}
|
||||
|
||||
struct AsyncInvocationState {
|
||||
result: Mutex<Option<Result<(), Error>>>,
|
||||
completed: Condvar,
|
||||
}
|
||||
|
||||
impl fmt::Debug for AsyncInvocation {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("AsyncInvocation")
|
||||
.field("async_state", &self.async_state)
|
||||
.field("completed", &self.is_completed())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncInvocation {
|
||||
pub(crate) fn pending(async_state: Object) -> Self {
|
||||
Self {
|
||||
state: Arc::new(AsyncInvocationState {
|
||||
result: Mutex::new(None),
|
||||
completed: Condvar::new(),
|
||||
}),
|
||||
async_state,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn complete(&self, result: Result<(), Error>) {
|
||||
*self
|
||||
.state
|
||||
.result
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(result);
|
||||
self.state.completed.notify_all();
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_completed(&self) -> bool {
|
||||
self.state
|
||||
.result
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn async_state(&self) -> &Object {
|
||||
&self.async_state
|
||||
}
|
||||
|
||||
pub(crate) fn wait(&self) -> Result<(), Error> {
|
||||
let result = self
|
||||
.state
|
||||
.result
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let result = self
|
||||
.state
|
||||
.completed
|
||||
.wait_while(result, |result| result.is_none())
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
result.expect("completion predicate guarantees a result")
|
||||
}
|
||||
}
|
||||
|
||||
type DictionaryHandler = Arc<dyn Fn(DictionaryEventAction, DictionaryEntry) + Send + Sync>;
|
||||
|
||||
/// Opaque target accepted by the metadata-compatible delegate constructor.
|
||||
pub struct DictionaryCallbackTarget(pub DictionaryHandler);
|
||||
|
||||
impl fmt::Debug for DictionaryCallbackTarget {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("DictionaryCallbackTarget(..)")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DictionaryChangeCallback(DictionaryHandler);
|
||||
|
||||
impl DictionaryChangeCallback {
|
||||
pub fn from_callback(
|
||||
callback: impl Fn(DictionaryEventAction, DictionaryEntry) + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Self(Arc::new(callback))
|
||||
}
|
||||
|
||||
pub fn new(object: Object, method: isize) -> Result<Self, Error> {
|
||||
if method != 0 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
object
|
||||
.downcast_arc::<DictionaryCallbackTarget>()
|
||||
.map(|target| Self(Arc::clone(&target.0)))
|
||||
.ok_or(Error::Argument)
|
||||
}
|
||||
|
||||
pub fn begin_invoke(
|
||||
&self,
|
||||
action: DictionaryEventAction,
|
||||
entry: DictionaryEntry,
|
||||
callback: Box<dyn Fn(&dyn Any) + Send + Sync>,
|
||||
object: Object,
|
||||
) -> Result<Box<dyn Any + Send + Sync>, Error> {
|
||||
let invocation = AsyncInvocation::pending(object);
|
||||
let worker_invocation = invocation.clone();
|
||||
let handler = Arc::clone(&self.0);
|
||||
std::thread::Builder::new()
|
||||
.name("dictionary-callback".to_owned())
|
||||
.spawn(move || {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
handler(action, entry);
|
||||
}))
|
||||
.map_err(|_| Error::InvalidOperation);
|
||||
worker_invocation.complete(result);
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
callback(&worker_invocation);
|
||||
}));
|
||||
})
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
Ok(Box::new(invocation))
|
||||
}
|
||||
|
||||
pub fn end_invoke(&self, result: Box<dyn Any + Send + Sync>) -> Result<(), Error> {
|
||||
result
|
||||
.downcast::<AsyncInvocation>()
|
||||
.map_err(|_| Error::Argument)?
|
||||
.wait()
|
||||
}
|
||||
|
||||
pub fn invoke(
|
||||
&self,
|
||||
action: DictionaryEventAction,
|
||||
entry: DictionaryEntry,
|
||||
) -> Result<(), Error> {
|
||||
(self.0)(action, entry);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[test]
|
||||
fn asynchronous_delegate_can_be_observed_and_ended() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&calls);
|
||||
let callback = DictionaryChangeCallback::from_callback(move |_, _| {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
});
|
||||
let result = callback
|
||||
.begin_invoke(
|
||||
DictionaryEventAction::Add,
|
||||
DictionaryEntry(Object::Integer(1), Object::Integer(2)),
|
||||
Box::new(|_| {}),
|
||||
Object::Undefined,
|
||||
)
|
||||
.unwrap();
|
||||
callback.end_invoke(result).unwrap();
|
||||
assert_eq!(calls.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
}
|
||||
@@ -614,6 +614,7 @@ const ALL_CAPABILITIES: &[&str] = &[
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::too_many_lines)] // The seed exchange is one contiguous protocol scenario.
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{GridClient, HttpCapsClient};
|
||||
|
||||
@@ -1152,6 +1152,7 @@ impl HttpCapsClient {
|
||||
body: payload,
|
||||
};
|
||||
let response = tokio::select! {
|
||||
biased;
|
||||
response = handler.send(request, cancellation_token.clone()) => response,
|
||||
() = cancellation_token.cancelled() => return Err(Error::Cancelled),
|
||||
};
|
||||
|
||||
@@ -117,6 +117,8 @@ struct ClientRuntime {
|
||||
agent_throttle_sender: Mutex<Option<Arc<dyn crate::udp_transport::AgentThrottleSender>>>,
|
||||
http_caps_client: Mutex<crate::caps_http::HttpCapsClient>,
|
||||
caps_rate_limiter: Mutex<crate::caps_http::CapsRateLimiter>,
|
||||
utilization_statistics: Mutex<crate::stats::UtilizationStatistics>,
|
||||
throttle: Mutex<crate::AgentThrottle>,
|
||||
network_manager: Mutex<std::sync::Weak<crate::network_manager::NetworkManagerInner>>,
|
||||
agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>,
|
||||
object_manager: Mutex<Option<Arc<crate::object_manager::ObjectManagerInner>>>,
|
||||
@@ -169,6 +171,11 @@ impl ClientRuntime {
|
||||
agent_throttle_sender: Mutex::new(None),
|
||||
http_caps_client: Mutex::new(http_caps_client),
|
||||
caps_rate_limiter: Mutex::new(caps_rate_limiter),
|
||||
utilization_statistics: Mutex::new(
|
||||
crate::stats::UtilizationStatistics::new()
|
||||
.expect("utilization statistics construction is infallible"),
|
||||
),
|
||||
throttle: Mutex::new(crate::AgentThrottle::default()),
|
||||
network_manager: Mutex::new(std::sync::Weak::new()),
|
||||
agent_manager: Mutex::new(std::sync::Weak::new()),
|
||||
object_manager: Mutex::new(None),
|
||||
@@ -651,6 +658,10 @@ impl GridClient {
|
||||
self.agent_manager.as_deref_mut().expect("agent manager")
|
||||
}
|
||||
|
||||
pub(crate) fn native_agent_manager(&self) -> Result<crate::AgentManager, crate::Error> {
|
||||
crate::agent_manager::AgentManager::native_new(Some(Arc::new(self.clone())))
|
||||
}
|
||||
|
||||
pub(crate) fn cached_agent_manager_inner(
|
||||
&self,
|
||||
) -> Option<Arc<crate::agent_manager::AgentManagerInner>> {
|
||||
@@ -1142,6 +1153,38 @@ impl GridClient {
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
pub(crate) fn native_stats(&self) -> crate::stats::UtilizationStatistics {
|
||||
self.runtime
|
||||
.utilization_statistics
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_stats(&self, value: crate::stats::UtilizationStatistics) {
|
||||
*self
|
||||
.runtime
|
||||
.utilization_statistics
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
pub(crate) fn native_throttle(&self) -> crate::AgentThrottle {
|
||||
self.runtime
|
||||
.throttle
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_throttle(&self, value: crate::AgentThrottle) {
|
||||
*self
|
||||
.runtime
|
||||
.throttle
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&self) -> Result<(), ClientCoreError> {
|
||||
self.runtime.shutdown()
|
||||
}
|
||||
@@ -1181,14 +1224,115 @@ impl Drop for GridClient {
|
||||
}
|
||||
|
||||
impl crate::IGridClient for GridClient {
|
||||
fn ais_client(&self) -> crate::InventoryAISClient {
|
||||
self.native_ais_client()
|
||||
.unwrap_or_else(|_| panic!("failed to construct InventoryAISClient"))
|
||||
}
|
||||
|
||||
fn appearance(&self) -> crate::AppearanceManager {
|
||||
self.native_appearance()
|
||||
.unwrap_or_else(|_| panic!("failed to construct AppearanceManager"))
|
||||
}
|
||||
|
||||
fn assets(&self) -> crate::AssetManager {
|
||||
self.native_assets()
|
||||
.unwrap_or_else(|_| panic!("failed to construct AssetManager"))
|
||||
}
|
||||
|
||||
fn avatars(&self) -> crate::AvatarManager {
|
||||
self.native_avatars()
|
||||
.unwrap_or_else(|_| panic!("failed to construct AvatarManager"))
|
||||
}
|
||||
|
||||
fn directory(&self) -> crate::DirectoryManager {
|
||||
self.native_directory()
|
||||
.unwrap_or_else(|_| panic!("failed to construct DirectoryManager"))
|
||||
}
|
||||
|
||||
fn environment(&self) -> crate::EnvironmentManager {
|
||||
self.native_environment()
|
||||
.unwrap_or_else(|_| panic!("failed to construct EnvironmentManager"))
|
||||
}
|
||||
|
||||
fn estate(&self) -> crate::EstateTools {
|
||||
self.native_estate()
|
||||
.unwrap_or_else(|_| panic!("failed to construct EstateTools"))
|
||||
}
|
||||
|
||||
fn friends(&self) -> crate::FriendsManager {
|
||||
self.native_friends()
|
||||
.unwrap_or_else(|_| panic!("failed to construct FriendsManager"))
|
||||
}
|
||||
|
||||
fn grid(&self) -> crate::GridManager {
|
||||
self.native_grid()
|
||||
.unwrap_or_else(|_| panic!("failed to construct GridManager"))
|
||||
}
|
||||
|
||||
fn groups(&self) -> crate::GroupManager {
|
||||
self.native_groups()
|
||||
}
|
||||
|
||||
fn http_caps_client(&self) -> crate::caps_http::HttpCapsClient {
|
||||
self.native_http_caps_client()
|
||||
}
|
||||
|
||||
fn interest_list(&self) -> crate::InterestListManager {
|
||||
self.native_interest_list()
|
||||
.unwrap_or_else(|_| panic!("failed to construct InterestListManager"))
|
||||
}
|
||||
|
||||
fn inventory(&self) -> crate::InventoryManager {
|
||||
self.native_inventory()
|
||||
.unwrap_or_else(|_| panic!("failed to construct InventoryManager"))
|
||||
}
|
||||
|
||||
fn marketplace(&self) -> crate::marketplace::MarketplaceManager {
|
||||
self.native_marketplace()
|
||||
}
|
||||
|
||||
fn network(&self) -> crate::NetworkManager {
|
||||
self.native_network()
|
||||
.unwrap_or_else(|_| panic!("failed to construct NetworkManager"))
|
||||
}
|
||||
|
||||
fn objects(&self) -> crate::ObjectManager {
|
||||
self.native_objects()
|
||||
.unwrap_or_else(|_| panic!("failed to construct ObjectManager"))
|
||||
}
|
||||
|
||||
fn parcels(&self) -> crate::ParcelManager {
|
||||
self.native_parcels()
|
||||
.unwrap_or_else(|_| panic!("failed to construct ParcelManager"))
|
||||
}
|
||||
|
||||
fn self_(&self) -> crate::AgentManager {
|
||||
self.native_agent_manager()
|
||||
.unwrap_or_else(|_| panic!("failed to construct AgentManager"))
|
||||
}
|
||||
|
||||
fn settings(&self) -> Settings {
|
||||
self.settings.clone()
|
||||
}
|
||||
|
||||
fn sound(&self) -> crate::SoundManager {
|
||||
self.native_sound()
|
||||
.unwrap_or_else(|_| panic!("failed to construct SoundManager"))
|
||||
}
|
||||
|
||||
fn stats(&self) -> crate::stats::UtilizationStatistics {
|
||||
self.native_stats()
|
||||
}
|
||||
|
||||
fn terrain(&self) -> crate::TerrainManager {
|
||||
self.native_terrain()
|
||||
.unwrap_or_else(|_| panic!("failed to construct TerrainManager"))
|
||||
}
|
||||
|
||||
fn throttle(&self) -> crate::AgentThrottle {
|
||||
self.native_throttle()
|
||||
}
|
||||
|
||||
fn time_provider(&self) -> TimeProvider {
|
||||
self.time_provider.clone()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//! Value conversion helpers used by the generated `COLLADA 1.4` data model.
|
||||
|
||||
fn regex_whitespace_split(value: &str) -> Vec<&str> {
|
||||
let mut values = Vec::new();
|
||||
let mut start = 0;
|
||||
let mut whitespace = false;
|
||||
for (index, character) in value.char_indices() {
|
||||
if character.is_whitespace() {
|
||||
if !whitespace {
|
||||
values.push(&value[start..index]);
|
||||
whitespace = true;
|
||||
}
|
||||
} else if whitespace {
|
||||
start = index;
|
||||
whitespace = false;
|
||||
}
|
||||
}
|
||||
if whitespace {
|
||||
values.push("");
|
||||
} else {
|
||||
values.push(&value[start..]);
|
||||
}
|
||||
values
|
||||
}
|
||||
|
||||
pub(crate) fn parse_bools(value: &str) -> Vec<bool> {
|
||||
regex_whitespace_split(value)
|
||||
.into_iter()
|
||||
.map(|value| value == "true")
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_doubles(value: &str) -> Vec<f64> {
|
||||
regex_whitespace_split(value)
|
||||
.into_iter()
|
||||
.map(|value| value.parse().unwrap_or_default())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_longs(value: &str) -> Vec<i64> {
|
||||
regex_whitespace_split(value)
|
||||
.into_iter()
|
||||
.map(|value| value.parse().unwrap_or_default())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_names(value: &str) -> Vec<String> {
|
||||
regex_whitespace_split(value)
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn format_bools(values: &[bool]) -> String {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| if *value { "true" } else { "false" })
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
pub(crate) fn format_values<T: ToString>(values: &[T]) -> String {
|
||||
values
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::import_export::collada14::{AssetUnit, Node, NodeType};
|
||||
|
||||
#[test]
|
||||
fn whitespace_split_matches_regex_split_boundaries() {
|
||||
assert_eq!(parse_names(""), vec![""]);
|
||||
assert_eq!(parse_names(" a\t b "), vec!["", "a", "b", ""]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_numeric_elements_follow_try_parse_defaults() {
|
||||
assert_eq!(parse_doubles("1 nope 2.5"), vec![1.0, 0.0, 2.5]);
|
||||
assert_eq!(parse_longs("1 nope -2"), vec![1, 0, -2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_schema_properties_round_trip_owned_values() {
|
||||
let mut unit = AssetUnit::new().unwrap();
|
||||
assert_eq!(unit.meter(), 1.0);
|
||||
assert_eq!(unit.name(), "meter");
|
||||
unit.set_meter(0.3048);
|
||||
unit.set_name("foot".into());
|
||||
assert_eq!(unit.meter(), 0.3048);
|
||||
assert_eq!(unit.name(), "foot");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_schema_constructors_preserve_pinned_defaults() {
|
||||
let node = Node::new().unwrap();
|
||||
assert_eq!(node.type_(), NodeType::NODE);
|
||||
assert!(node.items().is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
//! Mutable orthonormal coordinate frame matching the pinned grid math behavior.
|
||||
|
||||
use libremetaverse_types::{Error, Matrix4, Quaternion, Vector3};
|
||||
use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct FrameState {
|
||||
origin: Vector3,
|
||||
x_axis: Vector3,
|
||||
y_axis: Vector3,
|
||||
z_axis: Vector3,
|
||||
}
|
||||
|
||||
/// Origin plus forward, left, and up axes used by agent camera/movement code.
|
||||
#[derive(Debug)]
|
||||
pub struct CoordinateFrame {
|
||||
state: RwLock<FrameState>,
|
||||
}
|
||||
|
||||
impl Clone for CoordinateFrame {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
state: RwLock::new(*read(&self.state)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read<T>(value: &RwLock<T>) -> RwLockReadGuard<'_, T> {
|
||||
value
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn write<T>(value: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
|
||||
value
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn finite(value: Vector3) -> bool {
|
||||
value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
|
||||
}
|
||||
|
||||
fn frame_finite(value: FrameState) -> bool {
|
||||
finite(value.origin) && finite(value.x_axis) && finite(value.y_axis) && finite(value.z_axis)
|
||||
}
|
||||
|
||||
fn orthonormalize(value: &mut FrameState) -> Result<(), Error> {
|
||||
value.x_axis = Vector3::normalize(value.x_axis)?;
|
||||
let projection = Vector3::multiply_with_vector3_single(
|
||||
value.x_axis,
|
||||
Vector3::dot(value.x_axis, value.y_axis)?,
|
||||
)?;
|
||||
value.y_axis = Vector3::normalize(Vector3::subtract(value.y_axis, projection)?)?;
|
||||
value.z_axis = Vector3::cross(value.x_axis, value.y_axis)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl CoordinateFrame {
|
||||
pub fn x_axis_with_field() -> Vector3 {
|
||||
Vector3::unit_x()
|
||||
}
|
||||
|
||||
pub fn y_axis_with_field() -> Vector3 {
|
||||
Vector3::unit_y()
|
||||
}
|
||||
|
||||
pub fn z_axis_with_field() -> Vector3 {
|
||||
Vector3::unit_z()
|
||||
}
|
||||
|
||||
pub fn new_with_vector3(origin: Vector3) -> Result<Self, Error> {
|
||||
Self::from_state(FrameState {
|
||||
origin,
|
||||
x_axis: Self::x_axis_with_field(),
|
||||
y_axis: Self::y_axis_with_field(),
|
||||
z_axis: Self::z_axis_with_field(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_vector3_matrix4(origin: Vector3, rotation: Matrix4) -> Result<Self, Error> {
|
||||
Self::from_state(FrameState {
|
||||
origin,
|
||||
x_axis: rotation.at_axis(),
|
||||
y_axis: rotation.left_axis(),
|
||||
z_axis: rotation.up_axis(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_vector3_quaternion(
|
||||
origin: Vector3,
|
||||
rotation: Quaternion,
|
||||
) -> Result<Self, Error> {
|
||||
Self::new_with_vector3_matrix4(origin, Matrix4::create_from_quaternion(rotation)?)
|
||||
}
|
||||
|
||||
pub fn new_with_vector3_vector3(origin: Vector3, direction: Vector3) -> Result<Self, Error> {
|
||||
let frame = Self::new_with_vector3(origin)?;
|
||||
frame.look_direction_with_vector3(direction)?;
|
||||
if !frame_finite(*read(&frame.state)) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
pub fn new_with_vector3_vector3_vector3_vector3(
|
||||
origin: Vector3,
|
||||
x_axis: Vector3,
|
||||
y_axis: Vector3,
|
||||
z_axis: Vector3,
|
||||
) -> Result<Self, Error> {
|
||||
Self::from_state(FrameState {
|
||||
origin,
|
||||
x_axis,
|
||||
y_axis,
|
||||
z_axis,
|
||||
})
|
||||
}
|
||||
|
||||
fn from_state(state: FrameState) -> Result<Self, Error> {
|
||||
if !frame_finite(state) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Self {
|
||||
state: RwLock::new(state),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn look_at_with_vector3_vector3(
|
||||
&self,
|
||||
origin: Vector3,
|
||||
target: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
self.look_at_with_vector3_vector3_vector3(origin, target, Self::z_axis_with_field())
|
||||
}
|
||||
|
||||
pub fn look_at_with_vector3_vector3_vector3(
|
||||
&self,
|
||||
origin: Vector3,
|
||||
target: Vector3,
|
||||
up_direction: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
if !finite(origin) || !finite(target) || !finite(up_direction) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let at = Vector3::normalize(Vector3::subtract(target, origin)?)?;
|
||||
let mut state = write(&self.state);
|
||||
state.origin = origin;
|
||||
set_look_direction(&mut state, at, up_direction)
|
||||
}
|
||||
|
||||
pub fn look_direction_with_vector3(&self, at: Vector3) -> Result<(), Error> {
|
||||
self.look_direction_with_vector3_vector3(at, Self::z_axis_with_field())
|
||||
}
|
||||
|
||||
pub fn look_direction_with_vector3_vector3(
|
||||
&self,
|
||||
at: Vector3,
|
||||
up_direction: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
if !finite(at) || !finite(up_direction) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
set_look_direction(&mut write(&self.state), at, up_direction)
|
||||
}
|
||||
|
||||
pub fn look_direction_with_double(&self, heading: f64) -> Result<(), Error> {
|
||||
if !heading.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut state = write(&self.state);
|
||||
state.y_axis = Vector3 {
|
||||
x: heading.cos() as f32,
|
||||
y: heading.sin() as f32,
|
||||
z: state.y_axis.z,
|
||||
};
|
||||
state.x_axis = Vector3 {
|
||||
x: -heading.sin() as f32,
|
||||
y: heading.cos() as f32,
|
||||
z: state.x_axis.z,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pitch(&self, angle: f32) -> Result<(), Error> {
|
||||
let axis = read(&self.state).y_axis;
|
||||
self.rotate_with_quaternion(Quaternion::create_from_axis_angle_with_vector3_single(
|
||||
axis, angle,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn reset_axes(&self) -> Result<(), Error> {
|
||||
let mut state = write(&self.state);
|
||||
state.x_axis = Self::x_axis_with_field();
|
||||
state.y_axis = Self::y_axis_with_field();
|
||||
state.z_axis = Self::z_axis_with_field();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn roll(&self, angle: f32) -> Result<(), Error> {
|
||||
let axis = read(&self.state).x_axis;
|
||||
self.rotate_with_quaternion(Quaternion::create_from_axis_angle_with_vector3_single(
|
||||
axis, angle,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn rotate_with_matrix4(&self, matrix: Matrix4) -> Result<(), Error> {
|
||||
let mut state = write(&self.state);
|
||||
state.x_axis = Vector3::transform(state.x_axis, matrix)?;
|
||||
state.y_axis = Vector3::transform(state.y_axis, matrix)?;
|
||||
orthonormalize(&mut state)?;
|
||||
if !frame_finite(*state) {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rotate_with_quaternion(&self, rotation: Quaternion) -> Result<(), Error> {
|
||||
self.rotate_with_matrix4(Matrix4::create_from_quaternion(rotation)?)
|
||||
}
|
||||
|
||||
pub fn rotate_with_single_vector3(
|
||||
&self,
|
||||
angle: f32,
|
||||
rotation_axis: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
self.rotate_with_quaternion(Quaternion::create_from_axis_angle_with_vector3_single(
|
||||
rotation_axis,
|
||||
angle,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn yaw(&self, angle: f32) -> Result<(), Error> {
|
||||
let axis = read(&self.state).z_axis;
|
||||
self.rotate_with_quaternion(Quaternion::create_from_axis_angle_with_vector3_single(
|
||||
axis, angle,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> Vector3 {
|
||||
read(&self.state).origin
|
||||
}
|
||||
|
||||
pub fn set_origin(&mut self, value: Vector3) {
|
||||
assert!(finite(value), "non-finite CoordinateFrame origin");
|
||||
write(&self.state).origin = value;
|
||||
}
|
||||
|
||||
pub fn x_axis_with_property(&self) -> Vector3 {
|
||||
read(&self.state).x_axis
|
||||
}
|
||||
|
||||
pub fn set_x_axis_with_property(&mut self, value: Vector3) {
|
||||
assert!(finite(value), "non-finite CoordinateFrame X axis");
|
||||
write(&self.state).x_axis = value;
|
||||
}
|
||||
|
||||
pub fn y_axis_with_property(&self) -> Vector3 {
|
||||
read(&self.state).y_axis
|
||||
}
|
||||
|
||||
pub fn set_y_axis_with_property(&mut self, value: Vector3) {
|
||||
assert!(finite(value), "non-finite CoordinateFrame Y axis");
|
||||
write(&self.state).y_axis = value;
|
||||
}
|
||||
|
||||
pub fn z_axis_with_property(&self) -> Vector3 {
|
||||
read(&self.state).z_axis
|
||||
}
|
||||
|
||||
pub fn set_z_axis_with_property(&mut self, value: Vector3) {
|
||||
assert!(finite(value), "non-finite CoordinateFrame Z axis");
|
||||
write(&self.state).z_axis = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_look_direction(
|
||||
state: &mut FrameState,
|
||||
mut at: Vector3,
|
||||
up_direction: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
let mut left = Vector3::cross(up_direction, at)?;
|
||||
if left == Vector3::zero() {
|
||||
at.x += 0.01;
|
||||
at = Vector3::normalize(at)?;
|
||||
left = Vector3::cross(up_direction, at)?;
|
||||
}
|
||||
state.x_axis = at;
|
||||
state.y_axis = Vector3::normalize(left)?;
|
||||
state.z_axis = Vector3::cross(at, state.y_axis)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn origin_constructor_uses_grid_axes() {
|
||||
let frame = CoordinateFrame::new_with_vector3(Vector3::zero()).unwrap();
|
||||
assert_eq!(frame.x_axis_with_property(), Vector3::unit_x());
|
||||
assert_eq!(frame.y_axis_with_property(), Vector3::unit_y());
|
||||
assert_eq!(frame.z_axis_with_property(), Vector3::unit_z());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn look_direction_handles_parallel_up_without_zero_left_axis() {
|
||||
let frame = CoordinateFrame::new_with_vector3(Vector3::zero()).unwrap();
|
||||
frame
|
||||
.look_direction_with_vector3_vector3(Vector3::unit_z(), Vector3::unit_z())
|
||||
.unwrap();
|
||||
assert_ne!(frame.y_axis_with_property(), Vector3::zero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructor_rejects_non_finite_input() {
|
||||
assert!(matches!(
|
||||
CoordinateFrame::new_with_vector3(Vector3 {
|
||||
x: f32::NAN,
|
||||
y: 0.0,
|
||||
z: 0.0,
|
||||
}),
|
||||
Err(Error::Argument)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotations_keep_axes_orthonormal() {
|
||||
let frame = CoordinateFrame::new_with_vector3(Vector3::zero()).unwrap();
|
||||
frame.yaw(0.75).unwrap();
|
||||
let x = frame.x_axis_with_property();
|
||||
let y = frame.y_axis_with_property();
|
||||
let z = frame.z_axis_with_property();
|
||||
assert!((Vector3::dot(x, y).unwrap()).abs() < 1.0e-5);
|
||||
assert!((Vector3::dot(x, z).unwrap()).abs() < 1.0e-5);
|
||||
assert!((Vector3::dot(y, z).unwrap()).abs() < 1.0e-5);
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,12 @@ struct LinkRequest {
|
||||
|
||||
trait OutfitBackend: Send + Sync {
|
||||
fn store(&self) -> Option<Inventory>;
|
||||
fn folder_contents(
|
||||
&self,
|
||||
folder_id: UUID,
|
||||
owner_id: UUID,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> BackendFuture<'_, Vec<Box<dyn InventoryObjectClass>>>;
|
||||
fn attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem>;
|
||||
fn attachment_point(&self, item: UUID) -> Option<AttachmentPoint>;
|
||||
fn create_links(
|
||||
@@ -204,6 +210,28 @@ impl OutfitBackend for ClientOutfitBackend {
|
||||
.and_then(|manager| manager.native_store())
|
||||
}
|
||||
|
||||
fn folder_contents(
|
||||
&self,
|
||||
folder_id: UUID,
|
||||
owner_id: UUID,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> BackendFuture<'_, Vec<Box<dyn InventoryObjectClass>>> {
|
||||
Box::pin(async move {
|
||||
self.client()?
|
||||
.native_inventory()?
|
||||
.native_folder_contents(
|
||||
folder_id,
|
||||
owner_id,
|
||||
true,
|
||||
true,
|
||||
crate::InventorySortOrder::BY_NAME,
|
||||
cancellation_token,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem> {
|
||||
self.client
|
||||
.upgrade()
|
||||
@@ -487,7 +515,6 @@ impl CurrentOutfitFolder {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary.
|
||||
pub(crate) async fn native_get_current_outfit_links(
|
||||
&self,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
@@ -496,16 +523,20 @@ impl CurrentOutfitFolder {
|
||||
let Some(cof) = self.native_cof() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let contents = self
|
||||
.backend
|
||||
.folder_contents(
|
||||
cof.base.uuid(),
|
||||
cof.base.owner_id(),
|
||||
cancellation_token.clone(),
|
||||
)
|
||||
.await?;
|
||||
let mut links = HashMap::new();
|
||||
if let Some(store) = self.backend.store()
|
||||
&& let Ok(contents) = store.get_contents_with_uuid(cof.base.uuid())
|
||||
{
|
||||
for value in contents {
|
||||
if let Some(item) = inventory_item(value.as_ref())
|
||||
&& item.is_link()?
|
||||
{
|
||||
links.insert(item.base.uuid(), item);
|
||||
}
|
||||
for value in contents {
|
||||
if let Some(item) = inventory_item(value.as_ref())
|
||||
&& item.is_link()?
|
||||
{
|
||||
links.insert(item.base.uuid(), item);
|
||||
}
|
||||
}
|
||||
for (id, item) in &read(&self.state).shadow_links {
|
||||
@@ -1203,6 +1234,18 @@ mod tests {
|
||||
None
|
||||
}
|
||||
|
||||
fn folder_contents(
|
||||
&self,
|
||||
_folder_id: UUID,
|
||||
_owner_id: UUID,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> BackendFuture<'_, Vec<Box<dyn InventoryObjectClass>>> {
|
||||
Box::pin(async move {
|
||||
check_cancelled(cancellation_token.as_ref())?;
|
||||
Ok(Vec::new())
|
||||
})
|
||||
}
|
||||
|
||||
fn attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem> {
|
||||
read(&self.attachments)
|
||||
.iter()
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
//! Cross-platform resource cleanup helpers.
|
||||
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use libremetaverse_types::compat::{CancellationTokenSource, Close, ExternalError, Task, Thread};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
type ErrorLogger = Box<dyn Fn(String, Option<ExternalError>) + Send + Sync>;
|
||||
|
||||
fn panic_error(payload: Box<dyn std::any::Any + Send>) -> ExternalError {
|
||||
if let Some(message) = payload.downcast_ref::<&str>() {
|
||||
ExternalError((*message).to_owned())
|
||||
} else if let Some(message) = payload.downcast_ref::<String>() {
|
||||
ExternalError(message.clone())
|
||||
} else {
|
||||
ExternalError("action panicked with a non-string payload".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn report(logger: &Option<ErrorLogger>, message: impl Into<String>, error: Option<ExternalError>) {
|
||||
if let Some(logger) = logger {
|
||||
logger(message.into(), error);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helpers that contain cleanup failures and report them through a caller-owned
|
||||
/// logger, matching the upstream disposal contract.
|
||||
pub struct DisposalHelper;
|
||||
|
||||
impl DisposalHelper {
|
||||
pub fn safe_action(
|
||||
action: Box<dyn Fn() + Send + Sync>,
|
||||
action_name: Option<String>,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<(), Error> {
|
||||
if let Err(payload) = catch_unwind(AssertUnwindSafe(action)) {
|
||||
let message = action_name.filter(|name| !name.is_empty()).map_or_else(
|
||||
|| "Error executing action".to_owned(),
|
||||
|name| format!("Error executing {name}"),
|
||||
);
|
||||
report(&logger, message, Some(panic_error(payload)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn safe_cancel_and_dispose(
|
||||
mut source: Option<CancellationTokenSource>,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<(), Error> {
|
||||
let Some(source) = source.as_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
source.cancel();
|
||||
if let Err(error) = source.close() {
|
||||
report(
|
||||
&logger,
|
||||
"Error disposing CancellationTokenSource",
|
||||
Some(error),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn safe_dispose(
|
||||
mut resource: Box<dyn Close>,
|
||||
resource_name: Option<String>,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<(), Error> {
|
||||
if let Err(error) = resource.close() {
|
||||
let message = resource_name.filter(|name| !name.is_empty()).map_or_else(
|
||||
|| "Error disposing resource".to_owned(),
|
||||
|name| format!("Error disposing {name}"),
|
||||
);
|
||||
report(&logger, message, Some(error));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn safe_dispose_all(
|
||||
resources: Box<dyn Iterator<Item = Box<dyn Close>>>,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<(), Error> {
|
||||
for mut resource in resources {
|
||||
if let Err(error) = resource.close() {
|
||||
report(&logger, "Error disposing resource", Some(error));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn safe_dispose_clear<T: Close>(
|
||||
mut collection: Vec<T>,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<(), Error> {
|
||||
for resource in &mut collection {
|
||||
if let Err(error) = resource.close() {
|
||||
report(&logger, "Error disposing resource", Some(error));
|
||||
}
|
||||
}
|
||||
collection.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn safe_join_thread(
|
||||
thread: Thread,
|
||||
timeout: Duration,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<bool, Error> {
|
||||
if !thread.is_alive() {
|
||||
return Ok(true);
|
||||
}
|
||||
match thread.join_timeout(timeout) {
|
||||
Ok(true) => Ok(true),
|
||||
Ok(false) => {
|
||||
report(
|
||||
&logger,
|
||||
format!(
|
||||
"Thread {} did not exit in time",
|
||||
thread.name().unwrap_or("unnamed")
|
||||
),
|
||||
None,
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
Err(error) => {
|
||||
report(
|
||||
&logger,
|
||||
format!(
|
||||
"Error waiting for thread {}",
|
||||
thread.name().unwrap_or("unnamed")
|
||||
),
|
||||
Some(error),
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn safe_wait_task_with_task_time_span_action(
|
||||
task: Task<()>,
|
||||
timeout: Duration,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<bool, Error> {
|
||||
match task.wait_timeout(timeout) {
|
||||
Ok(Some(())) => Ok(true),
|
||||
Ok(None) => {
|
||||
report(&logger, "Task did not complete in time", None);
|
||||
Ok(false)
|
||||
}
|
||||
Err(error) => {
|
||||
report(
|
||||
&logger,
|
||||
"Error waiting for task",
|
||||
Some(ExternalError(error.to_string())),
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn safe_wait_task_with_task_time_span_action_b4795938(
|
||||
task: Task<()>,
|
||||
timeout: Duration,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<bool, Error> {
|
||||
// `Task` is executor-neutral; its bounded driver parks only this worker
|
||||
// thread and is woken by the underlying future when progress is ready.
|
||||
Self::safe_wait_task_with_task_time_span_action(task, timeout, logger)
|
||||
}
|
||||
|
||||
pub async fn using_with_func_func<TDisposable: Close, TResult>(
|
||||
factory: Box<dyn Fn() -> TDisposable + Send + Sync>,
|
||||
action: Box<dyn Fn(&mut TDisposable) -> Task<Option<TResult>> + Send + Sync>,
|
||||
) -> Result<Option<TResult>, Error> {
|
||||
let mut guard = CloseGuard::new(factory());
|
||||
let result = action(guard.resource_mut()).await;
|
||||
guard.close()?;
|
||||
result
|
||||
}
|
||||
|
||||
pub fn using_with_func_func_3fbb9598<TDisposable: Close, TResult>(
|
||||
factory: Box<dyn Fn() -> TDisposable + Send + Sync>,
|
||||
action: Box<dyn Fn(&mut TDisposable) -> Option<TResult> + Send + Sync>,
|
||||
) -> Result<Option<TResult>, Error> {
|
||||
let mut guard = CloseGuard::new(factory());
|
||||
let result = action(guard.resource_mut());
|
||||
guard.close()?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
struct CloseGuard<T: Close> {
|
||||
resource: Option<T>,
|
||||
}
|
||||
|
||||
impl<T: Close> CloseGuard<T> {
|
||||
fn new(resource: T) -> Self {
|
||||
Self {
|
||||
resource: Some(resource),
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_mut(&mut self) -> &mut T {
|
||||
self.resource
|
||||
.as_mut()
|
||||
.expect("resource remains until close")
|
||||
}
|
||||
|
||||
fn close(&mut self) -> Result<(), Error> {
|
||||
let mut resource = self.resource.take().ok_or(Error::InvalidOperation)?;
|
||||
resource.close().map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Close> Drop for CloseGuard<T> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(resource) = self.resource.as_mut() {
|
||||
let _ = resource.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs one callback exactly once when explicitly disposed or dropped.
|
||||
pub struct DisposalHelperDisposalGuard {
|
||||
on_dispose: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
|
||||
}
|
||||
|
||||
impl DisposalHelperDisposalGuard {
|
||||
pub fn new(on_dispose: Box<dyn Fn() + Send + Sync>) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
on_dispose: Mutex::new(Some(on_dispose)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dispose(&self) -> Result<(), Error> {
|
||||
let callback = self
|
||||
.on_dispose
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.take();
|
||||
let Some(callback) = callback else {
|
||||
return Ok(());
|
||||
};
|
||||
catch_unwind(AssertUnwindSafe(callback)).map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
}
|
||||
|
||||
impl Close for DisposalHelperDisposalGuard {
|
||||
fn close(&mut self) -> Result<(), ExternalError> {
|
||||
self.dispose()
|
||||
.map_err(|error| ExternalError(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DisposalHelperDisposalGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Arc, atomic::AtomicUsize};
|
||||
|
||||
struct CountedClose(Arc<AtomicUsize>);
|
||||
|
||||
impl Close for CountedClose {
|
||||
fn close(&mut self) -> Result<(), ExternalError> {
|
||||
self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disposal_guard_runs_callback_once() {
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&count);
|
||||
let guard = DisposalHelperDisposalGuard::new(Box::new(move || {
|
||||
observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}))
|
||||
.unwrap();
|
||||
guard.dispose().unwrap();
|
||||
guard.dispose().unwrap();
|
||||
drop(guard);
|
||||
assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn using_closes_after_action() {
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let factory_count = Arc::clone(&count);
|
||||
let result = DisposalHelper::using_with_func_func_3fbb9598(
|
||||
Box::new(move || CountedClose(factory_count.clone())),
|
||||
Box::new(|_| Some(42)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, Some(42));
|
||||
assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_thread_join_reports_timeout_then_completion() {
|
||||
let thread = Thread::spawn(Some("disposal-test".to_owned()), || {
|
||||
std::thread::sleep(Duration::from_millis(30));
|
||||
})
|
||||
.unwrap();
|
||||
assert!(
|
||||
!DisposalHelper::safe_join_thread(thread.clone(), Duration::from_millis(1), None,)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(DisposalHelper::safe_join_thread(thread, Duration::from_secs(1), None,).unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
use libremetaverse_structured_data::{OSD, OSDMap};
|
||||
|
||||
use crate::Error;
|
||||
use crate::message_codec::MessageValue;
|
||||
|
||||
fn empty_map() -> OSDMap {
|
||||
OSDMap::new_with_constructor().expect("empty OSD map construction is infallible")
|
||||
}
|
||||
|
||||
fn map_value(map: &OSDMap, key: &str) -> Option<OSD> {
|
||||
map.get(key)
|
||||
}
|
||||
|
||||
/// Payload-preserving representation of the C# abstract upload response block.
|
||||
pub struct AssetUploaderBlock {
|
||||
pub state: String,
|
||||
payload: OSDMap,
|
||||
}
|
||||
|
||||
impl AssetUploaderBlock {
|
||||
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
|
||||
self.state = map_value(&map, "state")
|
||||
.map(|value| value.as_string())
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
self.payload = map;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<OSDMap, Error> {
|
||||
if self.payload.count() == 0 {
|
||||
let map = empty_map();
|
||||
if !self.state.is_empty() {
|
||||
map.add_with_string_osd("state".into(), OSD::from_string(self.state.clone())?)?;
|
||||
}
|
||||
Ok(map)
|
||||
} else {
|
||||
Ok(self.payload.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for AssetUploaderBlock {
|
||||
fn message_default() -> Self {
|
||||
Self {
|
||||
state: String::new(),
|
||||
payload: empty_map(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.serialize()?.copy()
|
||||
}
|
||||
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let map = OSDMap::new_with_dictionary(values.clone())?;
|
||||
let mut block = Self::message_default();
|
||||
block.deserialize(map)?;
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChatSessionRequestBlock {
|
||||
pub method: String,
|
||||
payload: OSDMap,
|
||||
}
|
||||
|
||||
impl ChatSessionRequestBlock {
|
||||
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
|
||||
self.method = map_value(&map, "method")
|
||||
.ok_or(Error::Argument)?
|
||||
.as_string()?;
|
||||
self.payload = map;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<OSDMap, Error> {
|
||||
if self.payload.count() == 0 {
|
||||
let map = empty_map();
|
||||
map.add_with_string_osd("method".into(), OSD::from_string(self.method.clone())?)?;
|
||||
Ok(map)
|
||||
} else {
|
||||
Ok(self.payload.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for ChatSessionRequestBlock {
|
||||
fn message_default() -> Self {
|
||||
Self {
|
||||
method: String::new(),
|
||||
payload: empty_map(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.serialize()?.copy()
|
||||
}
|
||||
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let mut block = Self::message_default();
|
||||
block.deserialize(OSDMap::new_with_dictionary(values.clone())?)?;
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MapLayerMessageBase {
|
||||
pub flags: i32,
|
||||
payload: OSDMap,
|
||||
}
|
||||
|
||||
impl MapLayerMessageBase {
|
||||
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
|
||||
self.flags = if let Some(value) = map.get("Flags") {
|
||||
value.as_integer()?
|
||||
} else if let Some(OSD::Map(agent)) = map.get("AgentData") {
|
||||
agent.get("Flags").ok_or(Error::Argument)?.as_integer()?
|
||||
} else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
self.payload = map;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<OSDMap, Error> {
|
||||
if self.payload.count() == 0 {
|
||||
let map = empty_map();
|
||||
map.add_with_string_osd("Flags".into(), OSD::from_integer_with_int32(self.flags)?)?;
|
||||
Ok(map)
|
||||
} else {
|
||||
Ok(self.payload.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for MapLayerMessageBase {
|
||||
fn message_default() -> Self {
|
||||
Self {
|
||||
flags: 0,
|
||||
payload: empty_map(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.serialize()?.copy()
|
||||
}
|
||||
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let mut block = Self::message_default();
|
||||
block.deserialize(OSDMap::new_with_dictionary(values.clone())?)?;
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MapLayerRequestVariant {
|
||||
pub flags: i32,
|
||||
}
|
||||
|
||||
impl MapLayerRequestVariant {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self { flags: 0 })
|
||||
}
|
||||
|
||||
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
|
||||
self.flags = map.get("Flags").ok_or(Error::Argument)?.as_integer()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<OSDMap, Error> {
|
||||
let map = empty_map();
|
||||
map.add_with_string_osd("Flags".into(), OSD::from_integer_with_int32(self.flags)?)?;
|
||||
Ok(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for MapLayerRequestVariant {
|
||||
fn message_default() -> Self {
|
||||
Self { flags: 0 }
|
||||
}
|
||||
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.serialize()?.copy()
|
||||
}
|
||||
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let mut block = Self::message_default();
|
||||
block.deserialize(OSDMap::new_with_dictionary(values.clone())?)?;
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! opaque_block {
|
||||
($name:ident) => {
|
||||
pub struct $name {
|
||||
payload: OSDMap,
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
|
||||
self.payload = map;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<OSDMap, Error> {
|
||||
Ok(self.payload.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for $name {
|
||||
fn message_default() -> Self {
|
||||
Self {
|
||||
payload: empty_map(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.serialize()?.copy()
|
||||
}
|
||||
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
Ok(Self {
|
||||
payload: OSDMap::new_with_dictionary(values.clone())?,
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
opaque_block!(ObjectMediaBlock);
|
||||
opaque_block!(RemoteParcelRequestBlock);
|
||||
opaque_block!(SearchStatRequestBlock);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dynamic_upload_block_preserves_unmodeled_fields() {
|
||||
let map = empty_map();
|
||||
map.add_with_string_osd("state".into(), OSD::from_string("complete".into()).unwrap())
|
||||
.unwrap();
|
||||
map.add_with_string_osd(
|
||||
"new_asset".into(),
|
||||
OSD::from_string("asset".into()).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut block = AssetUploaderBlock::message_default();
|
||||
block.deserialize(map).unwrap();
|
||||
assert_eq!(block.state, "complete");
|
||||
assert_eq!(
|
||||
block.serialize().unwrap().get("new_asset"),
|
||||
Some(OSD::String("asset".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_layer_base_reads_both_request_and_reply_shapes() {
|
||||
let request = empty_map();
|
||||
request
|
||||
.add_with_string_osd("Flags".into(), OSD::from_integer_with_int32(7).unwrap())
|
||||
.unwrap();
|
||||
let mut block = MapLayerMessageBase::message_default();
|
||||
block.deserialize(request).unwrap();
|
||||
assert_eq!(block.flags, 7);
|
||||
|
||||
let reply = empty_map();
|
||||
reply
|
||||
.add_with_string_osd(
|
||||
"AgentData".into(),
|
||||
OSD::Map(std::collections::HashMap::from([(
|
||||
"Flags".into(),
|
||||
OSD::Integer(9),
|
||||
)])),
|
||||
)
|
||||
.unwrap();
|
||||
block.deserialize(reply).unwrap();
|
||||
assert_eq!(block.flags, 9);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
//! Temporary event subscriptions with timeout and cancellation support.
|
||||
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::time::Duration;
|
||||
|
||||
use libremetaverse_types::compat::{
|
||||
CancellationToken, Close, EventHandler, ExternalError, TaskCompletionSource,
|
||||
};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
fn timeout_duration(timeout_ms: i32) -> Result<Option<Duration>, Error> {
|
||||
match timeout_ms {
|
||||
-1 => Ok(None),
|
||||
value if value >= 0 => Ok(Some(Duration::from_millis(value as u64))),
|
||||
_ => Err(Error::Argument),
|
||||
}
|
||||
}
|
||||
|
||||
/// Event wait helpers matching the upstream subscribe/unsubscribe lifecycle.
|
||||
pub struct EventSubscriptionHelper;
|
||||
|
||||
impl EventSubscriptionHelper {
|
||||
pub fn wait_for_condition<TEventArgs: 'static>(
|
||||
subscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
unsubscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
condition: Box<dyn Fn(&Option<TEventArgs>) -> bool + Send + Sync>,
|
||||
timeout_ms: i32,
|
||||
) -> Result<(), Error> {
|
||||
let timeout = timeout_duration(timeout_ms)?;
|
||||
let (sender, receiver) = mpsc::sync_channel(1);
|
||||
let handler: EventHandler<Option<TEventArgs>> = Arc::new(move |event| {
|
||||
if condition(&event) {
|
||||
let _ = sender.try_send(());
|
||||
}
|
||||
});
|
||||
subscribe(Arc::clone(&handler));
|
||||
let subscription = EventSubscription::from_registered(unsubscribe, handler);
|
||||
match timeout {
|
||||
Some(timeout) => {
|
||||
let _ = receiver.recv_timeout(timeout);
|
||||
}
|
||||
None => {
|
||||
let _ = receiver.recv();
|
||||
}
|
||||
}
|
||||
subscription.dispose()
|
||||
}
|
||||
|
||||
pub async fn wait_for_event_with_action_action_func_func_int32_cancellation_token_t_result<
|
||||
TEventArgs: 'static,
|
||||
TResult,
|
||||
>(
|
||||
subscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
unsubscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
filter: Box<dyn Fn(&Option<TEventArgs>) -> bool + Send + Sync>,
|
||||
result_selector: Box<dyn Fn(&Option<TEventArgs>) -> Option<TResult> + Send + Sync>,
|
||||
timeout_ms: i32,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
default_value: Option<TResult>,
|
||||
) -> Result<Option<TResult>, Error>
|
||||
where
|
||||
TResult: Send + 'static,
|
||||
{
|
||||
let timeout = timeout_duration(timeout_ms)?;
|
||||
let completion = TaskCompletionSource::new();
|
||||
let event_completion = completion.clone();
|
||||
let handler: EventHandler<Option<TEventArgs>> = Arc::new(move |event| {
|
||||
if filter(&event) {
|
||||
let _ = event_completion.try_set_result(result_selector(&event));
|
||||
}
|
||||
});
|
||||
subscribe(Arc::clone(&handler));
|
||||
let subscription = EventSubscription::from_registered(unsubscribe, handler);
|
||||
|
||||
let cancellation = cancellation_token.map(|token| {
|
||||
let completion = completion.clone();
|
||||
token.register_callback(Arc::new(move || {
|
||||
let _ = completion.try_set_cancelled();
|
||||
}))
|
||||
});
|
||||
if let Some(timeout) = timeout {
|
||||
let timeout_completion = completion.clone();
|
||||
if std::thread::Builder::new()
|
||||
.name("event-wait-timeout".to_owned())
|
||||
.spawn(move || {
|
||||
std::thread::sleep(timeout);
|
||||
let _ = timeout_completion.try_set_result(default_value);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
subscription.dispose()?;
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
}
|
||||
|
||||
let result = completion.future().await;
|
||||
drop(cancellation);
|
||||
subscription.dispose()?;
|
||||
result
|
||||
}
|
||||
|
||||
pub fn wait_for_event_with_action_action_func_func_int32_t_result<
|
||||
TEventArgs: 'static,
|
||||
TResult,
|
||||
>(
|
||||
subscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
unsubscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
filter: Box<dyn Fn(&Option<TEventArgs>) -> bool + Send + Sync>,
|
||||
result_selector: Box<dyn Fn(&Option<TEventArgs>) -> Option<TResult> + Send + Sync>,
|
||||
timeout_ms: i32,
|
||||
default_value: Option<TResult>,
|
||||
) -> Result<Option<TResult>, Error>
|
||||
where
|
||||
TResult: Send + 'static,
|
||||
{
|
||||
let timeout = timeout_duration(timeout_ms)?;
|
||||
let (sender, receiver) = mpsc::sync_channel(1);
|
||||
let handler: EventHandler<Option<TEventArgs>> = Arc::new(move |event| {
|
||||
if filter(&event) {
|
||||
let _ = sender.try_send(result_selector(&event));
|
||||
}
|
||||
});
|
||||
subscribe(Arc::clone(&handler));
|
||||
let subscription = EventSubscription::from_registered(unsubscribe, handler);
|
||||
let result = match timeout {
|
||||
Some(timeout) => receiver.recv_timeout(timeout).unwrap_or(default_value),
|
||||
None => receiver.recv().unwrap_or(default_value),
|
||||
};
|
||||
subscription.dispose()?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Exactly-once unsubscribe guard.
|
||||
pub struct EventSubscription<TEventArgs> {
|
||||
registered: Mutex<RegisteredEvent<TEventArgs>>,
|
||||
}
|
||||
|
||||
struct RegisteredEvent<TEventArgs> {
|
||||
unsubscribe: Option<Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>>,
|
||||
handler: Option<EventHandler<Option<TEventArgs>>>,
|
||||
}
|
||||
|
||||
impl<TEventArgs> EventSubscription<TEventArgs> {
|
||||
pub fn new(
|
||||
subscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
unsubscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
handler: EventHandler<Option<TEventArgs>>,
|
||||
) -> Result<Self, Error> {
|
||||
subscribe(Arc::clone(&handler));
|
||||
Ok(Self::from_registered(unsubscribe, handler))
|
||||
}
|
||||
|
||||
fn from_registered(
|
||||
unsubscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
handler: EventHandler<Option<TEventArgs>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
registered: Mutex::new(RegisteredEvent {
|
||||
unsubscribe: Some(unsubscribe),
|
||||
handler: Some(handler),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispose(&self) -> Result<(), Error> {
|
||||
let (unsubscribe, handler) = {
|
||||
let mut registered = self
|
||||
.registered
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
(registered.unsubscribe.take(), registered.handler.take())
|
||||
};
|
||||
if let (Some(unsubscribe), Some(handler)) = (unsubscribe, handler) {
|
||||
unsubscribe(handler);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<TEventArgs> Close for EventSubscription<TEventArgs> {
|
||||
fn close(&mut self) -> Result<(), ExternalError> {
|
||||
self.dispose()
|
||||
.map_err(|error| ExternalError(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<TEventArgs> Drop for EventSubscription<TEventArgs> {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[test]
|
||||
fn subscription_unsubscribes_exactly_once() {
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&count);
|
||||
let subscription = EventSubscription::<i32>::new(
|
||||
Box::new(|_| {}),
|
||||
Box::new(move |_| {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
}),
|
||||
Arc::new(|_| {}),
|
||||
)
|
||||
.unwrap();
|
||||
subscription.dispose().unwrap();
|
||||
subscription.dispose().unwrap();
|
||||
drop(subscription);
|
||||
assert_eq!(count.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synchronous_wait_selects_matching_event_and_unsubscribes() {
|
||||
let slot = Arc::new(Mutex::new(None::<EventHandler<Option<i32>>>));
|
||||
let subscribe_slot = Arc::clone(&slot);
|
||||
let unsubscribe_slot = Arc::clone(&slot);
|
||||
let emitter_slot = Arc::clone(&slot);
|
||||
let emitter = std::thread::spawn(move || {
|
||||
while emitter_slot.lock().unwrap().is_none() {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
let handler = emitter_slot.lock().unwrap().as_ref().unwrap().clone();
|
||||
handler(Some(7));
|
||||
});
|
||||
let result =
|
||||
EventSubscriptionHelper::wait_for_event_with_action_action_func_func_int32_t_result(
|
||||
Box::new(move |handler| *subscribe_slot.lock().unwrap() = Some(handler)),
|
||||
Box::new(move |_| *unsubscribe_slot.lock().unwrap() = None),
|
||||
Box::new(|event| event == &Some(7)),
|
||||
Box::new(|event| *event),
|
||||
1_000,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
emitter.join().unwrap();
|
||||
assert_eq!(result, Some(7));
|
||||
assert!(slot.lock().unwrap().is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Deterministic, cross-platform filename and two-name helpers.
|
||||
|
||||
use crate::Error;
|
||||
use libremetaverse_types::compat::Utf16CodeUnit;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub struct FileHelper;
|
||||
|
||||
impl FileHelper {
|
||||
pub fn safe_file_name(original: String) -> Result<String, Error> {
|
||||
Ok(replace_utf16(
|
||||
&original,
|
||||
is_invalid_file_name,
|
||||
u16::from(b'_'),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn safe_dir_name(original: String) -> Result<String, Error> {
|
||||
Ok(replace_utf16(&original, is_invalid_path, u16::from(b'_')))
|
||||
}
|
||||
|
||||
pub fn sanitize(
|
||||
input: String,
|
||||
invalid_chars: Vec<Utf16CodeUnit>,
|
||||
replacement: Option<Utf16CodeUnit>,
|
||||
) -> Result<String, Error> {
|
||||
let invalid: HashSet<u16> = invalid_chars.into_iter().map(|unit| unit.0).collect();
|
||||
Ok(replace_utf16(
|
||||
&input,
|
||||
|unit| invalid.contains(&unit),
|
||||
replacement.unwrap_or(Utf16CodeUnit(u16::from(b'_'))).0,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn try_parse_two_names(
|
||||
input: String,
|
||||
first: &mut Option<String>,
|
||||
last: &mut Option<String>,
|
||||
) -> bool {
|
||||
*first = None;
|
||||
*last = None;
|
||||
let mut names = input.split_whitespace();
|
||||
let Some(first_name) = names.next() else {
|
||||
return false;
|
||||
};
|
||||
let Some(last_name) = names.next() else {
|
||||
return false;
|
||||
};
|
||||
if names.next().is_some() {
|
||||
return false;
|
||||
}
|
||||
*first = Some(first_name.to_owned());
|
||||
*last = Some(last_name.to_owned());
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_utf16(input: &str, invalid: impl Fn(u16) -> bool, replacement: u16) -> String {
|
||||
let units: Vec<u16> = input
|
||||
.encode_utf16()
|
||||
.map(|unit| if invalid(unit) { replacement } else { unit })
|
||||
.collect();
|
||||
String::from_utf16_lossy(&units)
|
||||
}
|
||||
|
||||
fn is_invalid_file_name(unit: u16) -> bool {
|
||||
unit <= 31 || matches!(unit, 34 | 42 | 47 | 58 | 60 | 62 | 63 | 92 | 124)
|
||||
}
|
||||
|
||||
fn is_invalid_path(unit: u16) -> bool {
|
||||
unit <= 31 || matches!(unit, 34 | 42 | 58 | 60 | 62 | 63 | 124)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn safe_names_use_the_portable_invalid_character_union() {
|
||||
assert_eq!(
|
||||
FileHelper::safe_file_name("a:b/c\\d?.txt".to_owned()).unwrap(),
|
||||
"a_b_c_d_.txt"
|
||||
);
|
||||
assert_eq!(
|
||||
FileHelper::safe_dir_name("root/child:name".to_owned()).unwrap(),
|
||||
"root/child_name"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_operates_on_csharp_utf16_code_units() {
|
||||
let result = FileHelper::sanitize(
|
||||
"a:b🙂".to_owned(),
|
||||
vec![Utf16CodeUnit(u16::from(b':'))],
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, "a_b🙂");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exactly_two_unicode_whitespace_delimited_names_are_accepted() {
|
||||
let mut first = Some("stale".to_owned());
|
||||
let mut last = Some("stale".to_owned());
|
||||
assert!(FileHelper::try_parse_two_names(
|
||||
" Ada\u{2003}Lovelace ".to_owned(),
|
||||
&mut first,
|
||||
&mut last,
|
||||
));
|
||||
assert_eq!(first.as_deref(), Some("Ada"));
|
||||
assert_eq!(last.as_deref(), Some("Lovelace"));
|
||||
assert!(!FileHelper::try_parse_two_names(
|
||||
"Ada Byron Lovelace".to_owned(),
|
||||
&mut first,
|
||||
&mut last,
|
||||
));
|
||||
assert_eq!(first, None);
|
||||
assert_eq!(last, None);
|
||||
}
|
||||
}
|
||||
+34108
-19390
File diff suppressed because it is too large.
Load diff
@@ -877,6 +877,11 @@ impl GridManagerInner {
|
||||
&event.data,
|
||||
&mut pos,
|
||||
) {
|
||||
if let Ok(prey_index) = usize::try_from(packet.index.prey)
|
||||
&& let Some(prey) = packet.agent_data.get(prey_index)
|
||||
{
|
||||
event.simulator.native_set_prey_id(prey.agent_id);
|
||||
}
|
||||
let positions: HashMap<_, _> = packet
|
||||
.agent_data
|
||||
.into_iter()
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
//! Native implementations of the pinned static helper surface.
|
||||
|
||||
use crate::{Error, Primitive, Simulator};
|
||||
use flate2::Compression;
|
||||
use flate2::read::ZlibDecoder;
|
||||
use flate2::write::ZlibEncoder;
|
||||
use libremetaverse_structured_data::{OSD, OSDParser};
|
||||
use libremetaverse_types::UUID;
|
||||
use libremetaverse_types::compat::{ExternalError, Object, ReadWrite};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const MAX_DECOMPRESSED_OSD: u64 = 128 * 1024 * 1024;
|
||||
|
||||
pub struct Helpers;
|
||||
|
||||
impl Helpers {
|
||||
pub const MSG_APPENDED_ACKS: u8 = 0x10;
|
||||
pub const MSG_RESENT: u8 = 0x20;
|
||||
pub const MSG_RELIABLE: u8 = 0x40;
|
||||
pub const MSG_ZEROCODED: u8 = 0x80;
|
||||
|
||||
pub fn copy_stream(
|
||||
mut input: Box<dyn ReadWrite + Send>,
|
||||
mut output: Box<dyn ReadWrite + Send>,
|
||||
) -> Result<(), Error> {
|
||||
std::io::copy(&mut input, &mut output).map_err(|_| Error::InvalidOperation)?;
|
||||
output.flush().map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
|
||||
pub fn decompress_osd(mesh_bytes: Vec<u8>) -> Result<OSD, Error> {
|
||||
Self::decode_zlib(&mesh_bytes)
|
||||
}
|
||||
|
||||
pub fn float_to_terse_string(val: f32) -> Result<String, Error> {
|
||||
if val == 0.0 {
|
||||
return Ok(".00".to_owned());
|
||||
}
|
||||
if val.is_nan() {
|
||||
return Ok("NaN".to_owned());
|
||||
}
|
||||
if val == f32::INFINITY {
|
||||
return Ok("Infinity".to_owned());
|
||||
}
|
||||
if val == f32::NEG_INFINITY {
|
||||
return Ok("-Infinity".to_owned());
|
||||
}
|
||||
let mut value = format!("{val:.2}");
|
||||
while value.ends_with('0') {
|
||||
value.pop();
|
||||
}
|
||||
if value.ends_with('.') {
|
||||
value.pop();
|
||||
} else if value.starts_with("-0") {
|
||||
value.remove(1);
|
||||
} else if value.starts_with('0') {
|
||||
value.remove(0);
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn get_resource_stream_with_string(
|
||||
resource_name: String,
|
||||
) -> Result<Option<Box<dyn ReadWrite + Send>>, Error> {
|
||||
Self::get_resource_stream_with_string_string(resource_name, "openmetaverse_data".to_owned())
|
||||
}
|
||||
|
||||
pub fn get_resource_stream_with_string_string(
|
||||
resource_name: String,
|
||||
search_path: String,
|
||||
) -> Result<Option<Box<dyn ReadWrite + Send>>, Error> {
|
||||
let base = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|path| path.parent().map(std::path::Path::to_path_buf))
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
let filename = base.join(search_path).join(resource_name);
|
||||
match std::fs::OpenOptions::new().read(true).open(filename) {
|
||||
Ok(file) => Ok(Some(Box::new(file))),
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn global_pos_to_region_handle(
|
||||
global_x: f32,
|
||||
global_y: f32,
|
||||
local_x: &mut f32,
|
||||
local_y: &mut f32,
|
||||
) -> Result<u64, Error> {
|
||||
let x =
|
||||
(global_x as u32 / Simulator::DEFAULT_REGION_SIZE_X) * Simulator::DEFAULT_REGION_SIZE_X;
|
||||
let y =
|
||||
(global_y as u32 / Simulator::DEFAULT_REGION_SIZE_Y) * Simulator::DEFAULT_REGION_SIZE_Y;
|
||||
*local_x = global_x - x as f32;
|
||||
*local_y = global_y - y as f32;
|
||||
Ok((u64::from(x) << 32) | u64::from(y))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn inventory_crc(
|
||||
creation_date: i32,
|
||||
sale_type: u8,
|
||||
inv_type: i8,
|
||||
type_: i8,
|
||||
mut asset_id: UUID,
|
||||
mut group_id: UUID,
|
||||
sale_price: i32,
|
||||
mut owner_id: UUID,
|
||||
mut creator_id: UUID,
|
||||
mut item_id: UUID,
|
||||
mut folder_id: UUID,
|
||||
everyone_mask: u32,
|
||||
flags: u32,
|
||||
next_owner_mask: u32,
|
||||
group_mask: u32,
|
||||
owner_mask: u32,
|
||||
) -> Result<u32, Error> {
|
||||
let values = [
|
||||
asset_id.crc()?,
|
||||
folder_id.crc()?,
|
||||
item_id.crc()?,
|
||||
creator_id.crc()?,
|
||||
owner_id.crc()?,
|
||||
group_id.crc()?,
|
||||
owner_mask,
|
||||
next_owner_mask,
|
||||
everyone_mask,
|
||||
group_mask,
|
||||
flags,
|
||||
inv_type as u32,
|
||||
type_ as u32,
|
||||
creation_date as u32,
|
||||
sale_price as u32,
|
||||
u32::from(sale_type).wrapping_mul(0x0707_3096),
|
||||
];
|
||||
Ok(values.into_iter().fold(0_u32, u32::wrapping_add))
|
||||
}
|
||||
|
||||
pub fn osd_to_prim_list(osd: OSD) -> Result<Vec<Primitive>, Error> {
|
||||
let OSD::Map(map) = osd else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
map.into_iter()
|
||||
.map(|(local_id, value)| {
|
||||
let mut primitive = Primitive::from_osd(value)?;
|
||||
primitive.local_id = local_id.parse().map_err(|_| Error::Argument)?;
|
||||
Ok(primitive)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn prim_list_to_osd(prims: Vec<Primitive>) -> Result<OSD, Error> {
|
||||
let mut map = HashMap::with_capacity(prims.len());
|
||||
for primitive in prims {
|
||||
map.insert(primitive.local_id.to_string(), primitive.get_osd()?);
|
||||
}
|
||||
Ok(OSD::Map(map))
|
||||
}
|
||||
|
||||
pub fn safe_action(
|
||||
action: Box<dyn Fn() + Send + Sync>,
|
||||
action_name: Option<String>,
|
||||
logger: Option<Box<dyn Fn(String, Option<ExternalError>) + Send + Sync>>,
|
||||
) -> Result<(), Error> {
|
||||
if let Err(payload) = catch_unwind(AssertUnwindSafe(action)) {
|
||||
if let Some(logger) = logger {
|
||||
let label = action_name.filter(|name| !name.is_empty());
|
||||
let message = label.map_or_else(
|
||||
|| "Error executing action".to_owned(),
|
||||
|name| format!("Error executing {name}"),
|
||||
);
|
||||
let detail = payload.downcast_ref::<&str>().map_or_else(
|
||||
|| {
|
||||
payload
|
||||
.downcast_ref::<String>()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "panic".to_owned())
|
||||
},
|
||||
|value| (*value).to_owned(),
|
||||
);
|
||||
logger(message, Some(ExternalError(detail)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn split_by(
|
||||
str: String,
|
||||
chunk_length: i32,
|
||||
) -> Result<Box<dyn Iterator<Item = String>>, Error> {
|
||||
if str.is_empty() || chunk_length < 1 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let chunk_length = usize::try_from(chunk_length).map_err(|_| Error::Argument)?;
|
||||
let units: Vec<u16> = str.encode_utf16().collect();
|
||||
let chunks: Vec<String> = units
|
||||
.chunks(chunk_length)
|
||||
.map(String::from_utf16_lossy)
|
||||
.collect();
|
||||
Ok(Box::new(chunks.into_iter()))
|
||||
}
|
||||
|
||||
pub fn struct_to_string(value: Object) -> Result<String, Error> {
|
||||
if let Object::Map(fields) = value {
|
||||
let mut fields: Vec<_> = fields.into_iter().collect();
|
||||
fields.sort_unstable_by(|left, right| left.0.cmp(&right.0));
|
||||
Ok(fields
|
||||
.into_iter()
|
||||
.map(|(name, value)| format!("{name}: {value:?}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "))
|
||||
} else {
|
||||
Ok(format!("{value:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn te_glow_byte(glow: f32) -> Result<u8, Error> {
|
||||
Ok((glow * 255.0) as u8)
|
||||
}
|
||||
|
||||
pub fn te_glow_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
Ok(f32::from(*bytes.get(index(pos)?).ok_or(Error::IndexOutOfRange)?) / 255.0)
|
||||
}
|
||||
|
||||
pub fn te_offset_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
let pos = index(pos)?;
|
||||
let pair: [u8; 2] = bytes
|
||||
.get(pos..pos.checked_add(2).ok_or(Error::IndexOutOfRange)?)
|
||||
.ok_or(Error::IndexOutOfRange)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::IndexOutOfRange)?;
|
||||
Ok(f32::from(i16::from_le_bytes(pair)) / 32767.0)
|
||||
}
|
||||
|
||||
pub fn te_offset_short(offset: f32) -> Result<i16, Error> {
|
||||
Ok((offset.clamp(-1.0, 1.0) * 32767.0).round_ties_even() as i16)
|
||||
}
|
||||
|
||||
pub fn te_rotation_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
let pos = index(pos)?;
|
||||
let pair: [u8; 2] = bytes
|
||||
.get(pos..pos.checked_add(2).ok_or(Error::IndexOutOfRange)?)
|
||||
.ok_or(Error::IndexOutOfRange)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::IndexOutOfRange)?;
|
||||
Ok(f32::from(u16::from_le_bytes(pair)) / 32768.0 * std::f32::consts::TAU)
|
||||
}
|
||||
|
||||
pub fn te_rotation_short(rotation: f32) -> Result<i16, Error> {
|
||||
let remainder =
|
||||
rotation - (rotation / std::f32::consts::TAU).round_ties_even() * std::f32::consts::TAU;
|
||||
Ok((remainder / std::f32::consts::TAU * 32768.0 + 0.5).round_ties_even() as i16)
|
||||
}
|
||||
|
||||
pub fn z_compress_osd(data: OSD) -> Result<Vec<u8>, Error> {
|
||||
let binary = OSDParser::serialize_llsd_binary_with_osd_boolean(data, false)?;
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
|
||||
encoder
|
||||
.write_all(&binary)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
encoder.finish().map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
|
||||
pub fn z_decompress_osd(data: Vec<u8>) -> Result<OSD, Error> {
|
||||
Self::decode_zlib(&data)
|
||||
}
|
||||
|
||||
pub fn zero_decode(
|
||||
src: Option<&[u8]>,
|
||||
srclen: i32,
|
||||
dest: Option<&mut [u8]>,
|
||||
) -> Result<i32, Error> {
|
||||
crate::packet_wire::zero_decode(src, srclen, dest)
|
||||
}
|
||||
|
||||
pub fn zero_encode(
|
||||
src: Option<&[u8]>,
|
||||
srclen: i32,
|
||||
dest: Option<&mut [u8]>,
|
||||
) -> Result<i32, Error> {
|
||||
crate::packet_wire::zero_encode(src, srclen, dest)
|
||||
}
|
||||
|
||||
fn decode_zlib(data: &[u8]) -> Result<OSD, Error> {
|
||||
let decoder = ZlibDecoder::new(data);
|
||||
let mut limited = decoder.take(MAX_DECOMPRESSED_OSD + 1);
|
||||
let mut decoded = Vec::new();
|
||||
limited
|
||||
.read_to_end(&mut decoded)
|
||||
.map_err(|_| Error::Parse {
|
||||
position: 0,
|
||||
context: "invalid zlib-compressed LLSD",
|
||||
})?;
|
||||
if decoded.len() as u64 > MAX_DECOMPRESSED_OSD {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
OSDParser::deserialize_llsd_binary_with_bytes(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
fn index(value: i32) -> Result<usize, Error> {
|
||||
usize::try_from(value).map_err(|_| Error::IndexOutOfRange)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn terse_float_and_texture_entry_conversions_match_pinned_examples() {
|
||||
assert_eq!(Helpers::float_to_terse_string(0.0).unwrap(), ".00");
|
||||
assert_eq!(Helpers::float_to_terse_string(0.5).unwrap(), ".5");
|
||||
assert_eq!(Helpers::float_to_terse_string(-0.25).unwrap(), "-.25");
|
||||
for offset in [-1.0, -0.25, 0.0, 0.75, 1.0] {
|
||||
let encoded = Helpers::te_offset_short(offset).unwrap();
|
||||
let decoded = Helpers::te_offset_float(encoded.to_le_bytes().to_vec(), 0).unwrap();
|
||||
assert!((decoded - offset).abs() <= 1.0 / 32767.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compressed_llsd_round_trips_and_rejects_non_zlib_input() {
|
||||
let value = OSD::Map(HashMap::from([
|
||||
("grid".to_owned(), OSD::String("OpenSim".to_owned())),
|
||||
("count".to_owned(), OSD::Integer(3)),
|
||||
]));
|
||||
let compressed = Helpers::z_compress_osd(value.clone()).unwrap();
|
||||
assert_eq!(Helpers::z_decompress_osd(compressed).unwrap(), value);
|
||||
assert!(Helpers::z_decompress_osd(vec![1, 2, 3]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_by_counts_utf16_units() {
|
||||
let chunks: Vec<_> = Helpers::split_by("ab🙂cd".to_owned(), 2).unwrap().collect();
|
||||
assert_eq!(chunks, ["ab", "🙂", "cd"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_handle_returns_local_coordinates() {
|
||||
let mut local_x = 0.0;
|
||||
let mut local_y = 0.0;
|
||||
let handle =
|
||||
Helpers::global_pos_to_region_handle(300.5, 600.25, &mut local_x, &mut local_y)
|
||||
.unwrap();
|
||||
assert_eq!(handle, (256_u64 << 32) | 512);
|
||||
assert_eq!(local_x, 44.5);
|
||||
assert_eq!(local_y, 88.25);
|
||||
}
|
||||
}
|
||||
@@ -295,9 +295,10 @@ impl InventoryAISClient {
|
||||
{
|
||||
Ok((success, _, _, _)) => Ok(success),
|
||||
Err(Error::Cancelled) => Err(Error::Cancelled),
|
||||
Err(_error) => {
|
||||
#[cfg(test)]
|
||||
eprintln!("AIS mutation failed: {_error:?}");
|
||||
Err(error) => {
|
||||
if cfg!(test) {
|
||||
eprintln!("AIS mutation failed: {error:?}");
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//! Typed inventory operation failure.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use libremetaverse_types::compat::ExternalError;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct InventoryException {
|
||||
message: String,
|
||||
inner: Option<ExternalError>,
|
||||
}
|
||||
|
||||
impl InventoryException {
|
||||
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
message: String::new(),
|
||||
inner: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_string(message: String) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
message,
|
||||
inner: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_string_exception(
|
||||
message: String,
|
||||
inner_exception: ExternalError,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
message,
|
||||
inner: Some(inner_exception),
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for InventoryException {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.message.is_empty() {
|
||||
formatter.write_str("inventory operation failed")
|
||||
} else {
|
||||
formatter.write_str(&self.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for InventoryException {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.inner
|
||||
.as_ref()
|
||||
.map(|error| error as &(dyn std::error::Error + 'static))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn preserves_message_and_inner_error() {
|
||||
let error = InventoryException::new_with_string_exception(
|
||||
"missing item".to_owned(),
|
||||
ExternalError("not found".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(error.to_string(), "missing item");
|
||||
assert_eq!(
|
||||
std::error::Error::source(&error).unwrap().to_string(),
|
||||
"not found"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
#![allow(clippy::missing_errors_doc)] // Public signatures mirror the C# API.
|
||||
#![allow(clippy::must_use_candidate)]
|
||||
#![allow(clippy::needless_pass_by_value)]
|
||||
#![allow(clippy::too_many_arguments)] // Capability calls preserve mapped request fields.
|
||||
#![allow(clippy::too_many_lines)] // Protocol parsers intentionally keep each wire state machine together.
|
||||
#![allow(clippy::unnecessary_wraps)] // Mapped C# methods require Result-shaped Rust APIs.
|
||||
|
||||
@@ -1657,7 +1658,7 @@ impl InventoryManager {
|
||||
}
|
||||
|
||||
pub(crate) fn native_no_results() -> Task<Vec<InventoryBase>> {
|
||||
Task(std::marker::PhantomData)
|
||||
Task::ready(Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) fn native_create_or_retrieve_inventory_item(
|
||||
@@ -4426,6 +4427,507 @@ fn parse_task_inventory_values(
|
||||
}
|
||||
|
||||
impl InventoryManager {
|
||||
async fn post_capability_osd(
|
||||
&self,
|
||||
uri: Uri,
|
||||
body: OSD,
|
||||
token: CancellationToken,
|
||||
) -> Result<OSD, Error> {
|
||||
token.throw_if_cancellation_requested()?;
|
||||
let payload = OSDParser::serialize_llsd_xml_bytes(body)?;
|
||||
let (response, bytes) = self
|
||||
.client()?
|
||||
.native_http_caps_client()
|
||||
.post_with_uri_string_bytes_cancellation_token_i_progress(
|
||||
uri,
|
||||
"application/llsd+xml".into(),
|
||||
payload,
|
||||
token,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
if !response.is_success_status_code() {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
OSDParser::deserialize_with_bytes(bytes)
|
||||
}
|
||||
|
||||
async fn post_asset_bytes_osd(
|
||||
&self,
|
||||
uri: Uri,
|
||||
data: Vec<u8>,
|
||||
token: CancellationToken,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<OSD, Error> {
|
||||
token.throw_if_cancellation_requested()?;
|
||||
if data.is_empty() || data.len() > crate::asset_models::MAX_ASSET_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let (response, bytes) = self
|
||||
.client()?
|
||||
.native_http_caps_client()
|
||||
.post_with_uri_string_bytes_cancellation_token_i_progress(
|
||||
uri,
|
||||
"application/octet-stream".into(),
|
||||
data,
|
||||
token,
|
||||
progress,
|
||||
)
|
||||
.await?;
|
||||
if !response.is_success_status_code() {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
OSDParser::deserialize_with_bytes(bytes)
|
||||
}
|
||||
|
||||
async fn begin_inventory_capability(
|
||||
&self,
|
||||
capability: &str,
|
||||
query: HashMap<String, OSD>,
|
||||
token: &CancellationToken,
|
||||
) -> Result<OSD, Error> {
|
||||
let simulator = self
|
||||
.client()?
|
||||
.native_network()?
|
||||
.current_sim()
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
let uri = self
|
||||
.wait_for_capability(&simulator, capability, token)
|
||||
.await?
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
self.post_capability_osd(uri, OSD::Map(query), token.clone())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn finish_inventory_upload(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
item_id: Option<UUID>,
|
||||
mut result: OSD,
|
||||
token: CancellationToken,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<((bool, String, UUID, UUID), OSD), Error> {
|
||||
let mut progress = progress;
|
||||
for _ in 0..3 {
|
||||
let OSD::Map(contents) = &result else {
|
||||
return Ok((
|
||||
(false, "invalid_response".into(), UUID::zero(), UUID::zero()),
|
||||
result,
|
||||
));
|
||||
};
|
||||
let state = contents
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
match state.as_str() {
|
||||
"upload" => {
|
||||
let uploader = contents
|
||||
.get("uploader")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uri()?
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
result = self
|
||||
.post_asset_bytes_osd(
|
||||
uploader,
|
||||
data.clone(),
|
||||
token.clone(),
|
||||
progress.take(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
"complete" => {
|
||||
let resolved_item = match item_id {
|
||||
Some(id) => id,
|
||||
None => contents
|
||||
.get("new_inventory_item")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?,
|
||||
};
|
||||
let asset_id = contents
|
||||
.get("new_asset")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?;
|
||||
let owner = self.client()?.native_agent_manager()?.native_agent_id();
|
||||
let _ = self.native_request_fetch_inventory(
|
||||
HashMap::from([(resolved_item, owner)]),
|
||||
Some(token),
|
||||
);
|
||||
return Ok(((true, String::new(), resolved_item, asset_id), result));
|
||||
}
|
||||
_ => {
|
||||
return Ok(((false, state, UUID::zero(), UUID::zero()), result));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((
|
||||
(
|
||||
false,
|
||||
"Upload state machine exceeded maximum iterations".into(),
|
||||
UUID::zero(),
|
||||
UUID::zero(),
|
||||
),
|
||||
result,
|
||||
))
|
||||
}
|
||||
|
||||
fn new_asset_query(
|
||||
&self,
|
||||
name: String,
|
||||
description: String,
|
||||
asset_type: AssetType,
|
||||
inventory_type: InventoryType,
|
||||
folder_id: UUID,
|
||||
permissions: &Permissions,
|
||||
include_expected_cost: bool,
|
||||
) -> Result<HashMap<String, OSD>, Error> {
|
||||
let mut query = HashMap::from([
|
||||
("folder_id".into(), OSD::UUID(folder_id)),
|
||||
(
|
||||
"asset_type".into(),
|
||||
OSD::String(Utils::asset_type_to_string(asset_type)?),
|
||||
),
|
||||
(
|
||||
"inventory_type".into(),
|
||||
OSD::String(Utils::inventory_type_to_string(inventory_type)?),
|
||||
),
|
||||
("name".into(), OSD::String(name)),
|
||||
("description".into(), OSD::String(description)),
|
||||
(
|
||||
"everyone_mask".into(),
|
||||
OSD::Integer(permissions.everyone_mask.0.cast_signed()),
|
||||
),
|
||||
(
|
||||
"group_mask".into(),
|
||||
OSD::Integer(permissions.group_mask.0.cast_signed()),
|
||||
),
|
||||
(
|
||||
"next_owner_mask".into(),
|
||||
OSD::Integer(permissions.next_owner_mask.0.cast_signed()),
|
||||
),
|
||||
]);
|
||||
if include_expected_cost {
|
||||
query.insert(
|
||||
"expected_upload_cost".into(),
|
||||
OSD::Integer(self.client()?.settings_ref().upload_cost()),
|
||||
);
|
||||
}
|
||||
Ok(query)
|
||||
}
|
||||
|
||||
pub(crate) async fn native_request_create_item_from_asset(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
name: String,
|
||||
description: String,
|
||||
asset_type: AssetType,
|
||||
inventory_type: InventoryType,
|
||||
folder_id: UUID,
|
||||
permissions: Permissions,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<(bool, String, UUID, UUID), Error> {
|
||||
let token = cancellation_token.unwrap_or_default();
|
||||
let query = self.new_asset_query(
|
||||
name,
|
||||
description,
|
||||
asset_type,
|
||||
inventory_type,
|
||||
folder_id,
|
||||
&permissions,
|
||||
true,
|
||||
)?;
|
||||
let initial = self
|
||||
.begin_inventory_capability("NewFileAgentInventory", query, &token)
|
||||
.await?;
|
||||
self.finish_inventory_upload(data, None, initial, token, progress)
|
||||
.await
|
||||
.map(|(result, _)| result)
|
||||
}
|
||||
|
||||
pub(crate) async fn native_create_item_from_asset(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
name: String,
|
||||
description: String,
|
||||
asset_type: AssetType,
|
||||
inventory_type: InventoryType,
|
||||
folder_id: UUID,
|
||||
permissions: Permissions,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<crate::InventoryManagerCreateItemFromAssetResult, Error> {
|
||||
let mut output = crate::InventoryManagerCreateItemFromAssetResult::default();
|
||||
match self
|
||||
.native_request_create_item_from_asset(
|
||||
data,
|
||||
name,
|
||||
description,
|
||||
asset_type,
|
||||
inventory_type,
|
||||
folder_id,
|
||||
permissions,
|
||||
cancellation_token,
|
||||
progress,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((success, status, item_id, asset_id)) => {
|
||||
output.set_success(success);
|
||||
output.set_status(Some(status));
|
||||
output.set_item_id(item_id);
|
||||
output.set_asset_id(asset_id);
|
||||
}
|
||||
Err(error) => {
|
||||
let status = error.to_string();
|
||||
output.set_status(Some(status.clone()));
|
||||
output.set_error(Some(ExternalError(status)));
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) fn native_can_upload_large_textures(&self) -> bool {
|
||||
let Ok(client) = self.client() else {
|
||||
return false;
|
||||
};
|
||||
let premium = client
|
||||
.native_agent_manager()
|
||||
.is_ok_and(|manager| manager.native_benefits().premium_access() > 0);
|
||||
premium
|
||||
&& client
|
||||
.native_network()
|
||||
.ok()
|
||||
.and_then(|network| network.current_sim())
|
||||
.and_then(|simulator| simulator.native_caps())
|
||||
.and_then(|caps| {
|
||||
caps.capability_uri("NewFileAgentInventoryVariablePrice".into())
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub(crate) async fn native_create_item_from_asset_variable_price(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
name: String,
|
||||
description: String,
|
||||
asset_type: AssetType,
|
||||
inventory_type: InventoryType,
|
||||
folder_id: UUID,
|
||||
permissions: Permissions,
|
||||
confirm_cost: Option<Box<dyn Fn(i32) -> bool + Send + Sync>>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<crate::InventoryManagerCreateItemFromAssetResult, Error> {
|
||||
let mut output = crate::InventoryManagerCreateItemFromAssetResult::default();
|
||||
if !self.native_can_upload_large_textures() {
|
||||
let status = "membership_or_capability_required".to_owned();
|
||||
output.set_status(Some(status.clone()));
|
||||
output.set_error(Some(ExternalError(status)));
|
||||
return Ok(output);
|
||||
}
|
||||
let token = cancellation_token.unwrap_or_default();
|
||||
let query = self.new_asset_query(
|
||||
name,
|
||||
description,
|
||||
asset_type,
|
||||
inventory_type,
|
||||
folder_id,
|
||||
&permissions,
|
||||
false,
|
||||
)?;
|
||||
let initial = self
|
||||
.begin_inventory_capability("NewFileAgentInventoryVariablePrice", query, &token)
|
||||
.await?;
|
||||
output.set_raw_result(Some(initial.clone()));
|
||||
let OSD::Map(contents) = initial else {
|
||||
output.set_status(Some("invalid_response".into()));
|
||||
return Ok(output);
|
||||
};
|
||||
let state = contents
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
if state != "confirm_upload" {
|
||||
output.set_status(Some(format!("unexpected_state:{state}")));
|
||||
return Ok(output);
|
||||
}
|
||||
let price = contents
|
||||
.get("upload_price")
|
||||
.map(OSD::as_integer)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
if confirm_cost.as_ref().is_some_and(|confirm| !confirm(price)) {
|
||||
let status = "cost_rejected".to_owned();
|
||||
output.set_status(Some(status.clone()));
|
||||
output.set_error(Some(ExternalError(status)));
|
||||
return Ok(output);
|
||||
}
|
||||
let rsvp = contents
|
||||
.get("rsvp")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uri()?
|
||||
.filter(|uri| uri.0 != "about:blank")
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
let uploaded = self
|
||||
.post_asset_bytes_osd(rsvp, data, token.clone(), progress)
|
||||
.await?;
|
||||
output.set_raw_result(Some(uploaded.clone()));
|
||||
let OSD::Map(uploaded) = uploaded else {
|
||||
output.set_status(Some("invalid_upload_response".into()));
|
||||
return Ok(output);
|
||||
};
|
||||
let state = uploaded
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
output.set_status(Some(state.clone()));
|
||||
if state == "complete" {
|
||||
let item_id = uploaded
|
||||
.get("new_inventory_item")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?;
|
||||
let asset_id = uploaded
|
||||
.get("new_asset")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?;
|
||||
output.set_item_id(item_id);
|
||||
output.set_asset_id(asset_id);
|
||||
output.set_success(true);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) async fn native_upload_inventory_asset(
|
||||
&self,
|
||||
capability: &str,
|
||||
data: Vec<u8>,
|
||||
item_id: UUID,
|
||||
task_id: Option<UUID>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<(bool, String, UUID, UUID), Error> {
|
||||
let token = cancellation_token.unwrap_or_default();
|
||||
let mut query = HashMap::from([("item_id".into(), OSD::UUID(item_id))]);
|
||||
if let Some(task_id) = task_id {
|
||||
query.insert("task_id".into(), OSD::UUID(task_id));
|
||||
}
|
||||
let initial = self
|
||||
.begin_inventory_capability(capability, query, &token)
|
||||
.await?;
|
||||
self.finish_inventory_upload(data, Some(item_id), initial, token, progress)
|
||||
.await
|
||||
.map(|(result, _)| result)
|
||||
}
|
||||
|
||||
pub(crate) async fn native_upload_script(
|
||||
&self,
|
||||
capability: &str,
|
||||
data: Vec<u8>,
|
||||
item_id: UUID,
|
||||
task_id: Option<UUID>,
|
||||
mono: bool,
|
||||
running: Option<bool>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<(bool, String, bool, Option<Vec<String>>, UUID, UUID), Error> {
|
||||
let token = cancellation_token.unwrap_or_default();
|
||||
let mut progress = progress;
|
||||
let mut query = HashMap::from([
|
||||
("item_id".into(), OSD::UUID(item_id)),
|
||||
(
|
||||
"target".into(),
|
||||
OSD::String(if mono { "mono" } else { "lsl2" }.into()),
|
||||
),
|
||||
]);
|
||||
if let Some(task_id) = task_id {
|
||||
query.insert("task_id".into(), OSD::UUID(task_id));
|
||||
}
|
||||
if let Some(running) = running {
|
||||
query.insert("is_script_running".into(), OSD::Boolean(running));
|
||||
}
|
||||
let mut result = self
|
||||
.begin_inventory_capability(capability, query, &token)
|
||||
.await?;
|
||||
for _ in 0..3 {
|
||||
let OSD::Map(contents) = &result else {
|
||||
return Ok((
|
||||
false,
|
||||
"invalid_response".into(),
|
||||
false,
|
||||
None,
|
||||
UUID::zero(),
|
||||
UUID::zero(),
|
||||
));
|
||||
};
|
||||
let state = contents
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
if state == "upload" {
|
||||
let uploader = contents
|
||||
.get("uploader")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uri()?
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
result = self
|
||||
.post_asset_bytes_osd(uploader, data.clone(), token.clone(), progress.take())
|
||||
.await?;
|
||||
continue;
|
||||
}
|
||||
if state == "complete" {
|
||||
let asset_id = contents
|
||||
.get("new_asset")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?;
|
||||
let compiled = contents
|
||||
.get("compiled")
|
||||
.map(OSD::as_boolean)
|
||||
.transpose()?
|
||||
.unwrap_or(false);
|
||||
let errors = match contents.get("errors") {
|
||||
Some(OSD::Array(values)) => Some(
|
||||
values
|
||||
.iter()
|
||||
.map(OSD::as_string)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
return Ok((true, state, compiled, errors, item_id, asset_id));
|
||||
}
|
||||
return Ok((false, state, false, None, UUID::zero(), UUID::zero()));
|
||||
}
|
||||
Ok((
|
||||
false,
|
||||
"Upload state machine exceeded maximum iterations".into(),
|
||||
false,
|
||||
None,
|
||||
UUID::zero(),
|
||||
UUID::zero(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn native_upload_thumbnail(
|
||||
&self,
|
||||
inventory_id: UUID,
|
||||
@@ -5030,7 +5532,7 @@ mod tests {
|
||||
.expect("network")
|
||||
.dispatch_caps_event("ScriptRunningReply", &reply, simulator);
|
||||
|
||||
let event = observed.lock().expect("observed").clone().expect("event");
|
||||
let event = (*observed.lock().expect("observed")).expect("event");
|
||||
assert_eq!(event.object_id(), object_id);
|
||||
assert_eq!(event.script_id(), script_id);
|
||||
assert!(event.is_mono());
|
||||
|
||||
@@ -24,10 +24,16 @@ mod asset_cache;
|
||||
mod asset_manager;
|
||||
mod asset_material;
|
||||
mod asset_models;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod asset_prim;
|
||||
mod avatar_manager;
|
||||
mod avatar_rig;
|
||||
mod baking_texture_provider;
|
||||
mod bit_pack;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod builtin_simple_renderer;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod callback_runtime;
|
||||
mod caps;
|
||||
mod caps_http;
|
||||
mod client_core;
|
||||
@@ -36,13 +42,21 @@ mod download_manager;
|
||||
mod environment_manager;
|
||||
mod estate_tools;
|
||||
mod event_queue;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod event_subscription;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod file_helper;
|
||||
mod friends_manager;
|
||||
mod gesture;
|
||||
mod gltf;
|
||||
mod group_manager;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod helpers;
|
||||
mod initial_outfit;
|
||||
mod inventory;
|
||||
mod inventory_ais;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod inventory_exception;
|
||||
mod inventory_manager;
|
||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||
mod foliage_catalog;
|
||||
@@ -50,24 +64,64 @@ mod generated;
|
||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||
mod genepool_catalog;
|
||||
mod j2k;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod linden_mesh;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod logger;
|
||||
mod login;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod lsl_syntax;
|
||||
#[path = "marketplace.rs"]
|
||||
mod marketplace_runtime;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod mesh_asset;
|
||||
mod message_codec;
|
||||
mod message_decoder;
|
||||
mod model_workflow;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod name_value;
|
||||
mod network_manager;
|
||||
mod object_manager;
|
||||
mod object_material;
|
||||
mod object_model;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod observable_dictionary;
|
||||
#[rustfmt::skip]
|
||||
pub mod packet_catalog;
|
||||
mod directory_manager;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod disposal_helper;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod dynamic_messages;
|
||||
mod grid_manager;
|
||||
pub use grid_manager::MapItemData;
|
||||
mod interest_list;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod packet_base;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod packet_decoder;
|
||||
mod packet_wire;
|
||||
mod parcel_manager;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod particle_simulator;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod port_errors;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod position_helper;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod prim_object;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod protocol_manager;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod rendering_binary_reader;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod rendering_loader;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod rendering_metadata;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod repeat;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod service_collection;
|
||||
mod sim_stats;
|
||||
mod skeleton;
|
||||
mod slurl;
|
||||
@@ -80,8 +134,14 @@ mod terrain_manager;
|
||||
mod transfers;
|
||||
mod udp_transport;
|
||||
mod user_report;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod uuid_formatter;
|
||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||
mod visual_catalog;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod avatar_model;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod avatar_physics;
|
||||
|
||||
#[cfg(test)]
|
||||
mod appearance_baker_semantics;
|
||||
@@ -190,6 +250,7 @@ impl imaging::Baker {
|
||||
}
|
||||
}
|
||||
|
||||
pub use callback_runtime::{AsyncInvocation, DictionaryCallbackTarget};
|
||||
pub use caps_http::CapsHttpLimits;
|
||||
pub use client_core::{
|
||||
ClientCoreError, ClientLifecycleState, ClientService, GridClientBuilder, ShutdownPhase,
|
||||
@@ -199,6 +260,7 @@ pub use libremetaverse_imaging as imaging_abstractions;
|
||||
pub use libremetaverse_structured_data as structured_data;
|
||||
pub use libremetaverse_types as types;
|
||||
pub use libremetaverse_types::Error;
|
||||
pub use logger::LoggerCallbackTarget;
|
||||
pub use network_manager::SimulatorCollection;
|
||||
pub use udp_transport::{
|
||||
AgentThrottleSender, UdpPacketHandler, UdpThrottleCategory, UdpTransportConfig,
|
||||
@@ -213,6 +275,23 @@ pub(crate) fn is_offline_fixture_uri(uri: &libremetaverse_types::compat::Uri) ->
|
||||
.is_some_and(|host| host == "invalid" || host.ends_with(".invalid"))
|
||||
}
|
||||
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod collada;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod coordinate_frame;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod physics_volume_morphs;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod simulator_data_pool;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod sound_catalog;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod statistics_runtime;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod texture_pipeline;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod threading_runtime;
|
||||
|
||||
/// Rust object-safe view of the C# `InventoryBase` inheritance hierarchy.
|
||||
pub trait InventoryObjectClass: std::any::Any {
|
||||
/// Returns the concrete value for exact C# runtime-type checks.
|
||||
@@ -320,19 +399,177 @@ impl InventoryItemClass for InventoryTexture {}
|
||||
impl InventoryItemClass for InventoryWearable {}
|
||||
|
||||
/// Project-owned boundary types for external `MessagePack` signatures.
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the external API boundary.
|
||||
pub mod message_pack {
|
||||
pub trait IMessagePackFormatter<T = ()> {}
|
||||
pub struct MessagePackReader;
|
||||
use libremetaverse_types::Error;
|
||||
|
||||
pub trait IMessagePackFormatter<T = ()> {
|
||||
fn serialize(
|
||||
&self,
|
||||
writer: &mut MessagePackWriter,
|
||||
value: T,
|
||||
options: MessagePackSerializerOptions,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
reader: &mut MessagePackReader,
|
||||
options: MessagePackSerializerOptions,
|
||||
) -> Result<T, Error>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MessagePackReader {
|
||||
bytes: Vec<u8>,
|
||||
position: usize,
|
||||
}
|
||||
|
||||
impl MessagePackReader {
|
||||
#[must_use]
|
||||
pub fn new(bytes: Vec<u8>) -> Self {
|
||||
Self { bytes, position: 0 }
|
||||
}
|
||||
|
||||
pub fn read_bytes(&mut self) -> Result<Option<Vec<u8>>, Error> {
|
||||
let marker = *self
|
||||
.bytes
|
||||
.get(self.position)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
self.position += 1;
|
||||
if marker == 0xc0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let length = match marker {
|
||||
0xc4 => {
|
||||
let length = *self
|
||||
.bytes
|
||||
.get(self.position)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
self.position += 1;
|
||||
usize::from(length)
|
||||
}
|
||||
0xc5 => {
|
||||
let length = self
|
||||
.bytes
|
||||
.get(self.position..self.position + 2)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
self.position += 2;
|
||||
usize::from(u16::from_be_bytes([length[0], length[1]]))
|
||||
}
|
||||
0xc6 => {
|
||||
let length = self
|
||||
.bytes
|
||||
.get(self.position..self.position + 4)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
self.position += 4;
|
||||
usize::try_from(u32::from_be_bytes([
|
||||
length[0], length[1], length[2], length[3],
|
||||
]))
|
||||
.map_err(|_| Error::IndexOutOfRange)?
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::Parse {
|
||||
position: self.position - 1,
|
||||
context: "MessagePack binary marker",
|
||||
});
|
||||
}
|
||||
};
|
||||
let end = self
|
||||
.position
|
||||
.checked_add(length)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
let value = self
|
||||
.bytes
|
||||
.get(self.position..end)
|
||||
.ok_or(Error::IndexOutOfRange)?
|
||||
.to_vec();
|
||||
self.position = end;
|
||||
Ok(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct MessagePackSerializerOptions;
|
||||
pub struct MessagePackWriter;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MessagePackWriter {
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl MessagePackWriter {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn write_bytes(&mut self, value: &[u8]) -> Result<(), Error> {
|
||||
match value.len() {
|
||||
length if u8::try_from(length).is_ok() => {
|
||||
self.bytes.push(0xc4);
|
||||
self.bytes.push(length as u8);
|
||||
}
|
||||
length if u16::try_from(length).is_ok() => {
|
||||
self.bytes.push(0xc5);
|
||||
self.bytes.extend_from_slice(&(length as u16).to_be_bytes());
|
||||
}
|
||||
length => {
|
||||
self.bytes.push(0xc6);
|
||||
let length = u32::try_from(length).map_err(|_| Error::Argument)?;
|
||||
self.bytes.extend_from_slice(&length.to_be_bytes());
|
||||
}
|
||||
}
|
||||
self.bytes.extend_from_slice(value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.bytes
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
self.bytes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Project-owned boundary types for external logging signatures.
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the external API boundary.
|
||||
pub mod logging {
|
||||
pub trait ILoggerFactory {}
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
use libremetaverse_types::compat::{Close, ExternalError, Object};
|
||||
|
||||
pub trait ILogger: Send + Sync {
|
||||
fn is_enabled(&self, level: LogLevel) -> bool;
|
||||
fn log(
|
||||
&self,
|
||||
level: LogLevel,
|
||||
message: &Object,
|
||||
exception: Option<&ExternalError>,
|
||||
client_name: Option<&str>,
|
||||
);
|
||||
fn begin_scope(&self, state: Object) -> Box<dyn Close>;
|
||||
}
|
||||
|
||||
pub trait ILoggerFactory: Send + Sync {
|
||||
fn create_logger(&self, name: &str) -> Arc<dyn ILogger>;
|
||||
fn shutdown(&self) -> Result<(), ExternalError>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct LogLevel(pub i32);
|
||||
|
||||
impl LogLevel {
|
||||
pub const TRACE: Self = Self(0);
|
||||
pub const DEBUG: Self = Self(1);
|
||||
pub const INFORMATION: Self = Self(2);
|
||||
pub const WARNING: Self = Self(3);
|
||||
pub const ERROR: Self = Self(4);
|
||||
pub const CRITICAL: Self = Self(5);
|
||||
pub const NONE: Self = Self(6);
|
||||
}
|
||||
}
|
||||
|
||||
/// Project-owned boundary types for external networking signatures.
|
||||
@@ -345,7 +582,7 @@ pub mod net {
|
||||
|
||||
/// Project-owned boundary types for external service-collection signatures.
|
||||
pub mod services {
|
||||
pub trait IServiceCollection {}
|
||||
pub use crate::service_collection::{IServiceCollection, ServiceCollection};
|
||||
}
|
||||
|
||||
/// Common behavior for program shims while the network stack is unimplemented.
|
||||
|
||||
@@ -0,0 +1,723 @@
|
||||
//! Validated reader for Linden Lab's legacy `.llm` avatar mesh format.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use libremetaverse_types::compat::{Object, SortedList};
|
||||
use libremetaverse_types::{Error, Vector2, Vector3};
|
||||
|
||||
use crate::rendering::{
|
||||
LindenMeshFace, LindenMeshMorph, LindenMeshMorphVertex, LindenMeshSkinWeightElement,
|
||||
LindenMeshVertex, LindenMeshVertexRemap, LindenSkeleton,
|
||||
};
|
||||
|
||||
const MESH_HEADER: &str = "Linden Binary Mesh 1.0";
|
||||
const MORPH_FOOTER: &str = "End Morphs";
|
||||
const MAX_MESH_BYTES: usize = 64 * 1024 * 1024;
|
||||
const MAX_MORPHS: usize = 16_384;
|
||||
const MORPH_VERTEX_BYTES: usize = 48;
|
||||
|
||||
/// Complete native state for a full Linden avatar mesh.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LindenMesh {
|
||||
pub min_pixel_width: f32,
|
||||
pub morphs: Vec<LindenMeshMorph>,
|
||||
pub skin_weights: Vec<LindenMeshSkinWeightElement>,
|
||||
name: String,
|
||||
header: String,
|
||||
has_weights: bool,
|
||||
has_detail_tex_coords: bool,
|
||||
position: Vector3,
|
||||
rotation_angles: Vector3,
|
||||
scale: Vector3,
|
||||
num_vertices: u16,
|
||||
vertices: Vec<LindenMeshVertex>,
|
||||
num_faces: u16,
|
||||
faces: Vec<LindenMeshFace>,
|
||||
num_skin_joints: u16,
|
||||
skin_joints: Vec<String>,
|
||||
num_remaps: i32,
|
||||
vertex_remaps: Vec<LindenMeshVertexRemap>,
|
||||
lod_meshes: SortedList<i32, Object>,
|
||||
skeleton: Option<LindenSkeleton>,
|
||||
}
|
||||
|
||||
/// Complete native state for a reference-only Linden LOD mesh.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ReferenceMesh {
|
||||
pub min_pixel_width: f32,
|
||||
pub header: String,
|
||||
pub has_weights: bool,
|
||||
pub has_detail_tex_coords: bool,
|
||||
pub position: Vector3,
|
||||
pub rotation_angles: Vector3,
|
||||
pub rotation_order: u8,
|
||||
pub scale: Vector3,
|
||||
pub num_faces: u16,
|
||||
pub faces: Vec<LindenMeshFace>,
|
||||
}
|
||||
|
||||
impl LindenMesh {
|
||||
pub(crate) fn native_new(
|
||||
name: String,
|
||||
skeleton: Option<LindenSkeleton>,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
min_pixel_width: 0.0,
|
||||
morphs: Vec::new(),
|
||||
skin_weights: Vec::new(),
|
||||
name,
|
||||
header: String::new(),
|
||||
has_weights: false,
|
||||
has_detail_tex_coords: false,
|
||||
position: Vector3::zero(),
|
||||
rotation_angles: Vector3::zero(),
|
||||
scale: Vector3::zero(),
|
||||
num_vertices: 0,
|
||||
vertices: Vec::new(),
|
||||
num_faces: 0,
|
||||
faces: Vec::new(),
|
||||
num_skin_joints: 0,
|
||||
skin_joints: Vec::new(),
|
||||
num_remaps: 0,
|
||||
vertex_remaps: Vec::new(),
|
||||
lod_meshes: SortedList(BTreeMap::new()),
|
||||
skeleton: Some(skeleton.unwrap_or(LindenSkeleton::load_with_method()?)),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_load_mesh(&mut self, filename: &str) -> Result<(), Error> {
|
||||
let bytes = read_mesh_file(filename)?;
|
||||
let parsed = ParsedFullMesh::parse(&bytes)?;
|
||||
|
||||
self.header = parsed.header;
|
||||
self.has_weights = parsed.has_weights;
|
||||
self.has_detail_tex_coords = parsed.has_detail_tex_coords;
|
||||
self.position = parsed.position;
|
||||
self.rotation_angles = parsed.rotation_angles;
|
||||
self.scale = parsed.scale;
|
||||
self.num_vertices = parsed.num_vertices;
|
||||
self.vertices = parsed.vertices;
|
||||
self.num_faces = parsed.num_faces;
|
||||
self.faces = parsed.faces;
|
||||
self.num_skin_joints = parsed.num_skin_joints;
|
||||
self.skin_joints = parsed.skin_joints;
|
||||
self.morphs = parsed.morphs;
|
||||
self.num_remaps = parsed.num_remaps;
|
||||
self.vertex_remaps = parsed.vertex_remaps;
|
||||
self.expand_skin_weights()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_load_lod_mesh(
|
||||
&mut self,
|
||||
level: i32,
|
||||
filename: &str,
|
||||
) -> Result<Object, Error> {
|
||||
let object = if is_eye_lod(filename) {
|
||||
let mut mesh = Self::native_new(String::new(), None)?;
|
||||
mesh.native_load_mesh(filename)?;
|
||||
Object::opaque(mesh)
|
||||
} else {
|
||||
let mut mesh = ReferenceMesh::native_new()?;
|
||||
mesh.native_load_mesh(filename)?;
|
||||
Object::opaque(mesh)
|
||||
};
|
||||
self.lod_meshes.0.insert(level, object.clone());
|
||||
Ok(object)
|
||||
}
|
||||
|
||||
pub(crate) fn native_load_reference_mesh(
|
||||
&mut self,
|
||||
lod_level: i32,
|
||||
filename: &str,
|
||||
) -> Result<ReferenceMesh, Error> {
|
||||
if is_eye_lod(filename) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut mesh = ReferenceMesh::native_new()?;
|
||||
mesh.native_load_mesh(filename)?;
|
||||
self.lod_meshes
|
||||
.0
|
||||
.insert(lod_level, Object::opaque(mesh.clone()));
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
fn expand_skin_weights(&mut self) -> Result<(), Error> {
|
||||
self.skin_weights.clear();
|
||||
let Some(skeleton) = &self.skeleton else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut joints =
|
||||
skeleton.build_expanded_joint_list(Box::new(self.skin_joints.clone().into_iter()))?;
|
||||
if joints.is_empty() {
|
||||
match self.name.as_str() {
|
||||
"eyeBallLeftMesh" => joints.extend(["mEyeLeft".to_owned(), "mSkull".to_owned()]),
|
||||
"eyeBallRightMesh" => {
|
||||
joints.extend(["mEyeRight".to_owned(), "mSkull".to_owned()]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if joints.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
self.skin_weights.reserve(self.vertices.len());
|
||||
for vertex in &self.vertices {
|
||||
let raw_index = vertex.weight.floor() as i32;
|
||||
let bone_index = raw_index - 1;
|
||||
let blend = (vertex.weight - raw_index as f32).clamp(0.0, 1.0);
|
||||
let valid_pair = usize::try_from(bone_index)
|
||||
.ok()
|
||||
.filter(|index| index.saturating_add(1) < joints.len());
|
||||
let weight = if let Some(index) = valid_pair {
|
||||
LindenMeshSkinWeightElement {
|
||||
bone1: joints[index].clone(),
|
||||
bone2: joints[index + 1].clone(),
|
||||
weight1: 1.0 - blend,
|
||||
weight2: blend,
|
||||
}
|
||||
} else {
|
||||
let nearest = bone_index.clamp(0, joints.len().saturating_sub(1) as i32) as usize;
|
||||
LindenMeshSkinWeightElement {
|
||||
bone1: joints[nearest].clone(),
|
||||
bone2: "mPelvis".to_owned(),
|
||||
weight1: 1.0,
|
||||
weight2: 0.0,
|
||||
}
|
||||
};
|
||||
self.skin_weights.push(weight);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_faces(&self) -> Vec<LindenMeshFace> {
|
||||
self.faces.clone()
|
||||
}
|
||||
pub(crate) fn native_set_faces(&mut self, value: Vec<LindenMeshFace>) {
|
||||
self.num_faces = value.len().try_into().unwrap_or(u16::MAX);
|
||||
self.faces = value;
|
||||
}
|
||||
pub(crate) fn native_has_detail_tex_coords(&self) -> bool {
|
||||
self.has_detail_tex_coords
|
||||
}
|
||||
pub(crate) fn native_set_has_detail_tex_coords(&mut self, value: bool) {
|
||||
self.has_detail_tex_coords = value;
|
||||
}
|
||||
pub(crate) fn native_has_weights(&self) -> bool {
|
||||
self.has_weights
|
||||
}
|
||||
pub(crate) fn native_set_has_weights(&mut self, value: bool) {
|
||||
self.has_weights = value;
|
||||
}
|
||||
pub(crate) fn native_header(&self) -> String {
|
||||
self.header.clone()
|
||||
}
|
||||
pub(crate) fn native_set_header(&mut self, value: String) {
|
||||
self.header = value;
|
||||
}
|
||||
pub(crate) fn native_lod_meshes(&self) -> SortedList<i32, Object> {
|
||||
self.lod_meshes.clone()
|
||||
}
|
||||
pub(crate) fn native_set_lod_meshes(&mut self, value: SortedList<i32, Object>) {
|
||||
self.lod_meshes = value;
|
||||
}
|
||||
pub(crate) fn native_name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
pub(crate) fn native_set_name(&mut self, value: String) {
|
||||
self.name = value;
|
||||
}
|
||||
pub(crate) fn native_num_faces(&self) -> u16 {
|
||||
self.num_faces
|
||||
}
|
||||
pub(crate) fn native_set_num_faces(&mut self, value: u16) {
|
||||
self.num_faces = value;
|
||||
}
|
||||
pub(crate) fn native_num_remaps(&self) -> i32 {
|
||||
self.num_remaps
|
||||
}
|
||||
pub(crate) fn native_set_num_remaps(&mut self, value: i32) {
|
||||
self.num_remaps = value;
|
||||
}
|
||||
pub(crate) fn native_num_skin_joints(&self) -> u16 {
|
||||
self.num_skin_joints
|
||||
}
|
||||
pub(crate) fn native_set_num_skin_joints(&mut self, value: u16) {
|
||||
self.num_skin_joints = value;
|
||||
}
|
||||
pub(crate) fn native_num_vertices(&self) -> u16 {
|
||||
self.num_vertices
|
||||
}
|
||||
pub(crate) fn native_set_num_vertices(&mut self, value: u16) {
|
||||
self.num_vertices = value;
|
||||
}
|
||||
pub(crate) fn native_position(&self) -> Vector3 {
|
||||
self.position
|
||||
}
|
||||
pub(crate) fn native_set_position(&mut self, value: Vector3) {
|
||||
self.position = value;
|
||||
}
|
||||
pub(crate) fn native_rotation_angles(&self) -> Vector3 {
|
||||
self.rotation_angles
|
||||
}
|
||||
pub(crate) fn native_set_rotation_angles(&mut self, value: Vector3) {
|
||||
self.rotation_angles = value;
|
||||
}
|
||||
pub(crate) fn native_scale(&self) -> Vector3 {
|
||||
self.scale
|
||||
}
|
||||
pub(crate) fn native_set_scale(&mut self, value: Vector3) {
|
||||
self.scale = value;
|
||||
}
|
||||
pub(crate) fn native_skeleton(&self) -> Option<LindenSkeleton> {
|
||||
self.skeleton.clone()
|
||||
}
|
||||
pub(crate) fn native_set_skeleton(&mut self, value: Option<LindenSkeleton>) {
|
||||
self.skeleton = value;
|
||||
}
|
||||
pub(crate) fn native_skin_joints(&self) -> Vec<String> {
|
||||
self.skin_joints.clone()
|
||||
}
|
||||
pub(crate) fn native_set_skin_joints(&mut self, value: Vec<String>) {
|
||||
self.num_skin_joints = value.len().try_into().unwrap_or(u16::MAX);
|
||||
self.skin_joints = value;
|
||||
}
|
||||
pub(crate) fn native_vertex_remaps(&self) -> Vec<LindenMeshVertexRemap> {
|
||||
self.vertex_remaps.clone()
|
||||
}
|
||||
pub(crate) fn native_set_vertex_remaps(&mut self, value: Vec<LindenMeshVertexRemap>) {
|
||||
self.num_remaps = value.len().try_into().unwrap_or(i32::MAX);
|
||||
self.vertex_remaps = value;
|
||||
}
|
||||
pub(crate) fn native_vertices(&self) -> Vec<LindenMeshVertex> {
|
||||
self.vertices.clone()
|
||||
}
|
||||
pub(crate) fn native_set_vertices(&mut self, value: Vec<LindenMeshVertex>) {
|
||||
self.num_vertices = value.len().try_into().unwrap_or(u16::MAX);
|
||||
self.vertices = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl ReferenceMesh {
|
||||
pub(crate) fn native_new() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
min_pixel_width: 0.0,
|
||||
header: String::new(),
|
||||
has_weights: false,
|
||||
has_detail_tex_coords: false,
|
||||
position: Vector3::zero(),
|
||||
rotation_angles: Vector3::zero(),
|
||||
rotation_order: 0,
|
||||
scale: Vector3::zero(),
|
||||
num_faces: 0,
|
||||
faces: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_load_mesh(&mut self, filename: &str) -> Result<(), Error> {
|
||||
let bytes = read_mesh_file(filename)?;
|
||||
let mut reader = Reader::new(&bytes);
|
||||
self.header = reader.fixed_string(24, "reference mesh header")?;
|
||||
validate_header(&self.header)?;
|
||||
self.has_weights = reader.byte("reference weights flag")? != 0;
|
||||
self.has_detail_tex_coords = reader.byte("reference detail flag")? != 0;
|
||||
self.position = reader.vector3("reference position")?;
|
||||
self.rotation_angles = reader.vector3("reference rotation")?;
|
||||
self.rotation_order = reader.byte("reference rotation order")?;
|
||||
self.scale = reader.vector3("reference scale")?;
|
||||
self.num_faces = reader.u16("reference face count")?;
|
||||
self.faces = read_faces(&mut reader, self.num_faces)?;
|
||||
reader.finish("reference mesh trailing bytes")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ParsedFullMesh {
|
||||
header: String,
|
||||
has_weights: bool,
|
||||
has_detail_tex_coords: bool,
|
||||
position: Vector3,
|
||||
rotation_angles: Vector3,
|
||||
scale: Vector3,
|
||||
num_vertices: u16,
|
||||
vertices: Vec<LindenMeshVertex>,
|
||||
num_faces: u16,
|
||||
faces: Vec<LindenMeshFace>,
|
||||
num_skin_joints: u16,
|
||||
skin_joints: Vec<String>,
|
||||
morphs: Vec<LindenMeshMorph>,
|
||||
num_remaps: i32,
|
||||
vertex_remaps: Vec<LindenMeshVertexRemap>,
|
||||
}
|
||||
|
||||
impl ParsedFullMesh {
|
||||
fn parse(bytes: &[u8]) -> Result<Self, Error> {
|
||||
let mut reader = Reader::new(bytes);
|
||||
let header = reader.fixed_string(24, "mesh header")?;
|
||||
validate_header(&header)?;
|
||||
let has_weights = reader.byte("weights flag")? != 0;
|
||||
let has_detail_tex_coords = reader.byte("detail flag")? != 0;
|
||||
let position = reader.vector3("position")?;
|
||||
let rotation_angles = reader.vector3("rotation")?;
|
||||
let _rotation_order = reader.byte("rotation order")?;
|
||||
let scale = reader.vector3("scale")?;
|
||||
let num_vertices = reader.u16("vertex count")?;
|
||||
let count = usize::from(num_vertices);
|
||||
let mut vertices = vec![empty_vertex(); count];
|
||||
for vertex in &mut vertices {
|
||||
vertex.coord = reader.vector3("vertex coordinates")?;
|
||||
}
|
||||
for vertex in &mut vertices {
|
||||
vertex.normal = reader.vector3("vertex normals")?;
|
||||
}
|
||||
for vertex in &mut vertices {
|
||||
vertex.bi_normal = reader.vector3("vertex binormals")?;
|
||||
}
|
||||
for vertex in &mut vertices {
|
||||
vertex.tex_coord = reader.vector2("vertex texture coordinates")?;
|
||||
}
|
||||
if has_detail_tex_coords {
|
||||
for vertex in &mut vertices {
|
||||
vertex.detail_tex_coord = reader.vector2("vertex detail texture coordinates")?;
|
||||
}
|
||||
}
|
||||
if has_weights {
|
||||
for vertex in &mut vertices {
|
||||
vertex.weight = reader.f32("vertex weight")?;
|
||||
if !vertex.weight.is_finite() {
|
||||
return Err(parse_error(reader.position(), "non-finite vertex weight"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let num_faces = reader.u16("face count")?;
|
||||
let faces = read_faces(&mut reader, num_faces)?;
|
||||
let (num_skin_joints, skin_joints) = if has_weights {
|
||||
let count = reader.u16("skin joint count")?;
|
||||
let mut joints = Vec::with_capacity(usize::from(count));
|
||||
for _ in 0..count {
|
||||
joints.push(reader.fixed_string(64, "skin joint name")?);
|
||||
}
|
||||
(count, joints)
|
||||
} else {
|
||||
(0, Vec::new())
|
||||
};
|
||||
let mut morphs = Vec::new();
|
||||
loop {
|
||||
let name = reader.fixed_string(64, "morph name")?;
|
||||
if name == MORPH_FOOTER {
|
||||
break;
|
||||
}
|
||||
if morphs.len() >= MAX_MORPHS {
|
||||
return Err(parse_error(reader.position(), "morph count exceeds limit"));
|
||||
}
|
||||
let signed_count = reader.i32("morph vertex count")?;
|
||||
let morph_count = usize::try_from(signed_count)
|
||||
.map_err(|_| parse_error(reader.position(), "negative morph vertex count"))?;
|
||||
reader.require(
|
||||
morph_count
|
||||
.checked_mul(MORPH_VERTEX_BYTES)
|
||||
.ok_or_else(|| parse_error(reader.position(), "morph size overflow"))?,
|
||||
"morph vertices",
|
||||
)?;
|
||||
let mut morph_vertices = Vec::with_capacity(morph_count);
|
||||
for _ in 0..morph_count {
|
||||
morph_vertices.push(LindenMeshMorphVertex {
|
||||
vertex_index: reader.u32("morph vertex index")?,
|
||||
coord: reader.vector3("morph coordinates")?,
|
||||
normal: reader.vector3("morph normal")?,
|
||||
bi_normal: reader.vector3("morph binormal")?,
|
||||
tex_coord: reader.vector2("morph texture coordinates")?,
|
||||
});
|
||||
}
|
||||
morphs.push(LindenMeshMorph {
|
||||
name,
|
||||
num_vertices: signed_count,
|
||||
vertices: morph_vertices,
|
||||
});
|
||||
}
|
||||
let (num_remaps, vertex_remaps) = if reader.remaining() == 0 {
|
||||
(0, Vec::new())
|
||||
} else {
|
||||
let signed_count = reader.i32("vertex remap count")?;
|
||||
let count = usize::try_from(signed_count)
|
||||
.map_err(|_| parse_error(reader.position(), "negative vertex remap count"))?;
|
||||
reader.require(
|
||||
count
|
||||
.checked_mul(8)
|
||||
.ok_or_else(|| parse_error(reader.position(), "vertex remap size overflow"))?,
|
||||
"vertex remaps",
|
||||
)?;
|
||||
let mut remaps = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
remaps.push(LindenMeshVertexRemap {
|
||||
remap_source: reader.i32("remap source")?,
|
||||
remap_destination: reader.i32("remap destination")?,
|
||||
});
|
||||
}
|
||||
(signed_count, remaps)
|
||||
};
|
||||
reader.finish("mesh trailing bytes")?;
|
||||
Ok(Self {
|
||||
header,
|
||||
has_weights,
|
||||
has_detail_tex_coords,
|
||||
position,
|
||||
rotation_angles,
|
||||
scale,
|
||||
num_vertices,
|
||||
vertices,
|
||||
num_faces,
|
||||
faces,
|
||||
num_skin_joints,
|
||||
skin_joints,
|
||||
morphs,
|
||||
num_remaps,
|
||||
vertex_remaps,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_vertex() -> LindenMeshVertex {
|
||||
LindenMeshVertex {
|
||||
bi_normal: Vector3::zero(),
|
||||
coord: Vector3::zero(),
|
||||
detail_tex_coord: Vector2::zero(),
|
||||
normal: Vector3::zero(),
|
||||
tex_coord: Vector2::zero(),
|
||||
weight: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_faces(reader: &mut Reader<'_>, count: u16) -> Result<Vec<LindenMeshFace>, Error> {
|
||||
reader.require(usize::from(count) * 6, "face indices")?;
|
||||
let mut faces = Vec::with_capacity(usize::from(count));
|
||||
for _ in 0..count {
|
||||
faces.push(LindenMeshFace {
|
||||
indices: vec![
|
||||
reader.i16("face index")?,
|
||||
reader.i16("face index")?,
|
||||
reader.i16("face index")?,
|
||||
],
|
||||
});
|
||||
}
|
||||
Ok(faces)
|
||||
}
|
||||
|
||||
fn read_mesh_file(filename: &str) -> Result<Vec<u8>, Error> {
|
||||
let metadata = std::fs::metadata(filename).map_err(|_| Error::Argument)?;
|
||||
let length = usize::try_from(metadata.len()).map_err(|_| Error::Argument)?;
|
||||
if length > MAX_MESH_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let bytes = std::fs::read(filename).map_err(|_| Error::Argument)?;
|
||||
if bytes.len() > MAX_MESH_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn is_eye_lod(filename: &str) -> bool {
|
||||
Path::new(filename)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
== Some("avatar_eye_1.llm")
|
||||
}
|
||||
|
||||
fn validate_header(header: &str) -> Result<(), Error> {
|
||||
if header == MESH_HEADER {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(parse_error(0, "unrecognized Linden mesh header"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn trim_at_nul(value: &str) -> String {
|
||||
value
|
||||
.split_once('\0')
|
||||
.map_or(value, |(prefix, _)| prefix)
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
struct Reader<'a> {
|
||||
bytes: &'a [u8],
|
||||
position: usize,
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
fn new(bytes: &'a [u8]) -> Self {
|
||||
Self { bytes, position: 0 }
|
||||
}
|
||||
fn position(&self) -> usize {
|
||||
self.position
|
||||
}
|
||||
fn remaining(&self) -> usize {
|
||||
self.bytes.len().saturating_sub(self.position)
|
||||
}
|
||||
fn require(&self, count: usize, context: &'static str) -> Result<(), Error> {
|
||||
if count <= self.remaining() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(parse_error(self.position, context))
|
||||
}
|
||||
}
|
||||
fn take(&mut self, count: usize, context: &'static str) -> Result<&'a [u8], Error> {
|
||||
self.require(count, context)?;
|
||||
let start = self.position;
|
||||
self.position += count;
|
||||
Ok(&self.bytes[start..self.position])
|
||||
}
|
||||
fn byte(&mut self, context: &'static str) -> Result<u8, Error> {
|
||||
Ok(self.take(1, context)?[0])
|
||||
}
|
||||
fn i16(&mut self, context: &'static str) -> Result<i16, Error> {
|
||||
let bytes: [u8; 2] = self
|
||||
.take(2, context)?
|
||||
.try_into()
|
||||
.map_err(|_| parse_error(self.position, context))?;
|
||||
Ok(i16::from_le_bytes(bytes))
|
||||
}
|
||||
fn u16(&mut self, context: &'static str) -> Result<u16, Error> {
|
||||
let bytes: [u8; 2] = self
|
||||
.take(2, context)?
|
||||
.try_into()
|
||||
.map_err(|_| parse_error(self.position, context))?;
|
||||
Ok(u16::from_le_bytes(bytes))
|
||||
}
|
||||
fn i32(&mut self, context: &'static str) -> Result<i32, Error> {
|
||||
let bytes: [u8; 4] = self
|
||||
.take(4, context)?
|
||||
.try_into()
|
||||
.map_err(|_| parse_error(self.position, context))?;
|
||||
Ok(i32::from_le_bytes(bytes))
|
||||
}
|
||||
fn u32(&mut self, context: &'static str) -> Result<u32, Error> {
|
||||
let bytes: [u8; 4] = self
|
||||
.take(4, context)?
|
||||
.try_into()
|
||||
.map_err(|_| parse_error(self.position, context))?;
|
||||
Ok(u32::from_le_bytes(bytes))
|
||||
}
|
||||
fn f32(&mut self, context: &'static str) -> Result<f32, Error> {
|
||||
let bytes: [u8; 4] = self
|
||||
.take(4, context)?
|
||||
.try_into()
|
||||
.map_err(|_| parse_error(self.position, context))?;
|
||||
let value = f32::from_le_bytes(bytes);
|
||||
if value.is_finite() {
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(parse_error(self.position, context))
|
||||
}
|
||||
}
|
||||
fn vector2(&mut self, context: &'static str) -> Result<Vector2, Error> {
|
||||
Ok(Vector2 {
|
||||
x: self.f32(context)?,
|
||||
y: self.f32(context)?,
|
||||
})
|
||||
}
|
||||
fn vector3(&mut self, context: &'static str) -> Result<Vector3, Error> {
|
||||
Ok(Vector3 {
|
||||
x: self.f32(context)?,
|
||||
y: self.f32(context)?,
|
||||
z: self.f32(context)?,
|
||||
})
|
||||
}
|
||||
fn fixed_string(&mut self, count: usize, context: &'static str) -> Result<String, Error> {
|
||||
Ok(trim_at_nul(&String::from_utf8_lossy(
|
||||
self.take(count, context)?,
|
||||
)))
|
||||
}
|
||||
fn finish(&self, context: &'static str) -> Result<(), Error> {
|
||||
if self.remaining() == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(parse_error(self.position, context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn parse_error(position: usize, context: &'static str) -> Error {
|
||||
Error::Parse { position, context }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn base_header(weights: bool, detail: bool) -> Vec<u8> {
|
||||
let mut bytes = [0_u8; 24].to_vec();
|
||||
bytes[..MESH_HEADER.len()].copy_from_slice(MESH_HEADER.as_bytes());
|
||||
bytes.push(u8::from(weights));
|
||||
bytes.push(u8::from(detail));
|
||||
for value in [1.0_f32, 2.0, 3.0, 0.1, 0.2, 0.3] {
|
||||
bytes.extend(value.to_le_bytes());
|
||||
}
|
||||
bytes.push(7);
|
||||
for value in [4.0_f32, 5.0, 6.0] {
|
||||
bytes.extend(value.to_le_bytes());
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_full_mesh_arrays_morphs_and_remaps() {
|
||||
let mut bytes = base_header(false, true);
|
||||
bytes.extend(1_u16.to_le_bytes());
|
||||
for value in [
|
||||
1.0_f32, 2.0, 3.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.25, 0.75, 0.5, 0.5,
|
||||
] {
|
||||
bytes.extend(value.to_le_bytes());
|
||||
}
|
||||
bytes.extend(1_u16.to_le_bytes());
|
||||
for index in [0_i16, 0, 0] {
|
||||
bytes.extend(index.to_le_bytes());
|
||||
}
|
||||
let mut morph_name = [0_u8; 64];
|
||||
morph_name[..5].copy_from_slice(b"smile");
|
||||
bytes.extend(morph_name);
|
||||
bytes.extend(1_i32.to_le_bytes());
|
||||
bytes.extend(0_u32.to_le_bytes());
|
||||
for value in [0.1_f32; 11] {
|
||||
bytes.extend(value.to_le_bytes());
|
||||
}
|
||||
let mut footer = [0_u8; 64];
|
||||
footer[..MORPH_FOOTER.len()].copy_from_slice(MORPH_FOOTER.as_bytes());
|
||||
bytes.extend(footer);
|
||||
bytes.extend(1_i32.to_le_bytes());
|
||||
bytes.extend(3_i32.to_le_bytes());
|
||||
bytes.extend(4_i32.to_le_bytes());
|
||||
let mesh = ParsedFullMesh::parse(&bytes).unwrap();
|
||||
assert_eq!(mesh.vertices.len(), 1);
|
||||
assert_eq!(mesh.faces[0].indices, vec![0, 0, 0]);
|
||||
assert_eq!(mesh.morphs[0].name, "smile");
|
||||
assert_eq!(mesh.vertex_remaps[0].remap_destination, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_bad_header_truncation_negative_counts_and_trailing_data() {
|
||||
assert!(ParsedFullMesh::parse(b"bad").is_err());
|
||||
let mut negative = base_header(false, false);
|
||||
negative.extend(0_u16.to_le_bytes());
|
||||
negative.extend(0_u16.to_le_bytes());
|
||||
let mut name = [0_u8; 64];
|
||||
name[0] = b'x';
|
||||
negative.extend(name);
|
||||
negative.extend((-1_i32).to_le_bytes());
|
||||
assert!(ParsedFullMesh::parse(&negative).is_err());
|
||||
let mut valid = base_header(false, false);
|
||||
valid.extend(0_u16.to_le_bytes());
|
||||
valid.extend(0_u16.to_le_bytes());
|
||||
let mut footer = [0_u8; 64];
|
||||
footer[..MORPH_FOOTER.len()].copy_from_slice(MORPH_FOOTER.as_bytes());
|
||||
valid.extend(footer);
|
||||
valid.push(1);
|
||||
assert!(ParsedFullMesh::parse(&valid).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_stops_at_first_nul() {
|
||||
assert_eq!(trim_at_nul("abc\0def\0"), "abc");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
//! Process-wide logging facade and callback compatibility layer.
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fmt;
|
||||
use std::sync::{
|
||||
Arc, Mutex, OnceLock,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
};
|
||||
use std::thread::ThreadId;
|
||||
use std::time::Duration;
|
||||
|
||||
use libremetaverse_types::compat::{
|
||||
CancellationToken, Close, ExternalError, Object, Subscription, Task, TaskCompletionSource,
|
||||
};
|
||||
|
||||
use crate::callback_runtime::AsyncInvocation;
|
||||
use crate::logging::{ILogger, ILoggerFactory, LogLevel};
|
||||
use crate::{Error, GridClient, Settings, Simulator};
|
||||
|
||||
struct LoggerState {
|
||||
factory: Option<Arc<dyn ILoggerFactory>>,
|
||||
logger: Option<Arc<dyn ILogger>>,
|
||||
callbacks: BTreeMap<u64, LoggerLogCallback>,
|
||||
next_callback: u64,
|
||||
}
|
||||
|
||||
impl Default for LoggerState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
factory: None,
|
||||
logger: None,
|
||||
callbacks: BTreeMap::new(),
|
||||
next_callback: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn state() -> &'static Mutex<LoggerState> {
|
||||
static STATE: OnceLock<Mutex<LoggerState>> = OnceLock::new();
|
||||
STATE.get_or_init(|| Mutex::new(LoggerState::default()))
|
||||
}
|
||||
|
||||
fn ensure_initialized() -> Arc<dyn ILogger> {
|
||||
let mut state = state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(logger) = state.logger.as_ref() {
|
||||
return Arc::clone(logger);
|
||||
}
|
||||
let factory: Arc<dyn ILoggerFactory> = Arc::new(ConsoleLoggerFactory::default());
|
||||
let logger = factory.create_logger("LibreMetaverse.Logger");
|
||||
state.factory = Some(factory);
|
||||
state.logger = Some(Arc::clone(&logger));
|
||||
logger
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ConsoleLoggerFactory {
|
||||
closed: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl ILoggerFactory for ConsoleLoggerFactory {
|
||||
fn create_logger(&self, name: &str) -> Arc<dyn ILogger> {
|
||||
Arc::new(ConsoleLogger {
|
||||
name: name.to_owned(),
|
||||
closed: Arc::clone(&self.closed),
|
||||
scopes: Arc::new(Mutex::new(HashMap::new())),
|
||||
next_scope: Arc::new(AtomicU64::new(1)),
|
||||
})
|
||||
}
|
||||
|
||||
fn shutdown(&self) -> Result<(), ExternalError> {
|
||||
self.closed.store(true, Ordering::Release);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
type ScopeMap = HashMap<ThreadId, Vec<(u64, Object)>>;
|
||||
|
||||
struct ConsoleLogger {
|
||||
name: String,
|
||||
closed: Arc<AtomicBool>,
|
||||
scopes: Arc<Mutex<ScopeMap>>,
|
||||
next_scope: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl ILogger for ConsoleLogger {
|
||||
fn is_enabled(&self, level: LogLevel) -> bool {
|
||||
!self.closed.load(Ordering::Acquire) && level != LogLevel::NONE && level >= LogLevel::DEBUG
|
||||
}
|
||||
|
||||
fn log(
|
||||
&self,
|
||||
level: LogLevel,
|
||||
message: &Object,
|
||||
exception: Option<&ExternalError>,
|
||||
client_name: Option<&str>,
|
||||
) {
|
||||
if !self.is_enabled(level) {
|
||||
return;
|
||||
}
|
||||
let scopes = self
|
||||
.scopes
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(&std::thread::current().id())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let scope_text = scopes
|
||||
.iter()
|
||||
.map(|(_, state)| format!("{state:?}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let level_name = match level {
|
||||
LogLevel::TRACE => "TRACE",
|
||||
LogLevel::DEBUG => "DEBUG",
|
||||
LogLevel::INFORMATION => "INFO",
|
||||
LogLevel::WARNING => "WARN",
|
||||
LogLevel::ERROR => "ERROR",
|
||||
LogLevel::CRITICAL => "CRITICAL",
|
||||
_ => "LOG",
|
||||
};
|
||||
let client = client_name
|
||||
.filter(|name| !name.is_empty())
|
||||
.map_or(String::new(), |name| format!(" [{name}]"));
|
||||
let scope = if scope_text.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" [{scope_text}]")
|
||||
};
|
||||
let exception = exception.map_or(String::new(), |error| format!(": {}", error.0));
|
||||
eprintln!(
|
||||
"[{level_name}] [{}]{client}{scope} {message:?}{exception}",
|
||||
self.name
|
||||
);
|
||||
}
|
||||
|
||||
fn begin_scope(&self, state: Object) -> Box<dyn Close> {
|
||||
let thread_id = std::thread::current().id();
|
||||
let id = self.next_scope.fetch_add(1, Ordering::Relaxed);
|
||||
self.scopes
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.entry(thread_id)
|
||||
.or_default()
|
||||
.push((id, state));
|
||||
Box::new(ConsoleScope {
|
||||
scopes: Arc::clone(&self.scopes),
|
||||
thread_id,
|
||||
id: Some(id),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsoleScope {
|
||||
scopes: Arc<Mutex<ScopeMap>>,
|
||||
thread_id: ThreadId,
|
||||
id: Option<u64>,
|
||||
}
|
||||
|
||||
impl Close for ConsoleScope {
|
||||
fn close(&mut self) -> Result<(), ExternalError> {
|
||||
let Some(id) = self.id.take() else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut scopes = self
|
||||
.scopes
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(thread_scopes) = scopes.get_mut(&self.thread_id) {
|
||||
thread_scopes.retain(|(scope_id, _)| *scope_id != id);
|
||||
if thread_scopes.is_empty() {
|
||||
scopes.remove(&self.thread_id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConsoleScope {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.close();
|
||||
}
|
||||
}
|
||||
|
||||
struct NoopScope;
|
||||
|
||||
impl Close for NoopScope {
|
||||
fn close(&mut self) -> Result<(), ExternalError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
type LogHandler = Arc<dyn Fn(Object, LogLevel) + Send + Sync>;
|
||||
|
||||
pub struct LoggerCallbackTarget(pub LogHandler);
|
||||
|
||||
impl fmt::Debug for LoggerCallbackTarget {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("LoggerCallbackTarget(..)")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LoggerLogCallback(LogHandler);
|
||||
|
||||
impl LoggerLogCallback {
|
||||
pub fn from_callback(callback: impl Fn(Object, LogLevel) + Send + Sync + 'static) -> Self {
|
||||
Self(Arc::new(callback))
|
||||
}
|
||||
|
||||
pub fn new(object: Object, method: isize) -> Result<Self, Error> {
|
||||
if method != 0 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
object
|
||||
.downcast_arc::<LoggerCallbackTarget>()
|
||||
.map(|target| Self(Arc::clone(&target.0)))
|
||||
.ok_or(Error::Argument)
|
||||
}
|
||||
|
||||
pub fn begin_invoke(
|
||||
&self,
|
||||
message: Object,
|
||||
level: LogLevel,
|
||||
callback: Box<dyn Fn(&dyn Any) + Send + Sync>,
|
||||
object: Object,
|
||||
) -> Result<Box<dyn Any + Send + Sync>, Error> {
|
||||
let invocation = AsyncInvocation::pending(object);
|
||||
let worker_invocation = invocation.clone();
|
||||
let handler = Arc::clone(&self.0);
|
||||
std::thread::Builder::new()
|
||||
.name("logger-callback".to_owned())
|
||||
.spawn(move || {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
handler(message, level);
|
||||
}))
|
||||
.map_err(|_| Error::InvalidOperation);
|
||||
worker_invocation.complete(result);
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
callback(&worker_invocation);
|
||||
}));
|
||||
})
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
Ok(Box::new(invocation))
|
||||
}
|
||||
|
||||
pub fn end_invoke(&self, result: Box<dyn Any + Send + Sync>) -> Result<(), Error> {
|
||||
result
|
||||
.downcast::<AsyncInvocation>()
|
||||
.map_err(|_| Error::Argument)?
|
||||
.wait()
|
||||
}
|
||||
|
||||
pub fn invoke(&self, message: Object, level: LogLevel) -> Result<(), Error> {
|
||||
(self.0)(message, level);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Logger;
|
||||
|
||||
impl Logger {
|
||||
pub fn subscribe_on_log_message(handler: Option<LoggerLogCallback>) -> Subscription {
|
||||
let Some(handler) = handler else {
|
||||
return Subscription::detached();
|
||||
};
|
||||
let id = {
|
||||
let mut state = state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let id = state.next_callback;
|
||||
state.next_callback = state.next_callback.wrapping_add(1).max(1);
|
||||
state.callbacks.insert(id, handler);
|
||||
id
|
||||
};
|
||||
Subscription::new(move || {
|
||||
state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.callbacks
|
||||
.remove(&id);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn begin_scope(state_value: Object) -> Result<Box<dyn Close>, Error> {
|
||||
let logger = state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.logger
|
||||
.clone();
|
||||
Ok(logger.map_or_else(
|
||||
|| Box::new(NoopScope) as Box<dyn Close>,
|
||||
|logger| logger.begin_scope(state_value),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn begin_client_scope(mut client: GridClient) -> Result<Box<dyn Close>, Error> {
|
||||
let log_names = client.settings().logging.log_names;
|
||||
if !log_names {
|
||||
return Ok(Box::new(NoopScope));
|
||||
}
|
||||
let name = client.self_().name();
|
||||
if name.is_empty() {
|
||||
return Ok(Box::new(NoopScope));
|
||||
}
|
||||
Self::begin_scope(Object::Map(HashMap::from([(
|
||||
"Client".to_owned(),
|
||||
Object::String(name),
|
||||
)])))
|
||||
}
|
||||
|
||||
pub fn begin_region_scope(simulator: Simulator) -> Result<Box<dyn Close>, Error> {
|
||||
let name = if simulator.name.is_empty() {
|
||||
simulator.handle.to_string()
|
||||
} else {
|
||||
simulator.name.clone()
|
||||
};
|
||||
Self::begin_scope(Object::Map(HashMap::from([(
|
||||
"Region".to_owned(),
|
||||
Object::String(name),
|
||||
)])))
|
||||
}
|
||||
|
||||
pub fn create_default_console_logger_factory() -> Result<Box<dyn ILoggerFactory>, Error> {
|
||||
Ok(Box::new(ConsoleLoggerFactory::default()))
|
||||
}
|
||||
|
||||
pub fn set_logger_factory(factory: Box<dyn ILoggerFactory>, name: String) -> Result<(), Error> {
|
||||
let factory: Arc<dyn ILoggerFactory> = Arc::from(factory);
|
||||
let logger = factory.create_logger(&name);
|
||||
let previous = {
|
||||
let mut state = state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let previous = state.factory.replace(Arc::clone(&factory));
|
||||
state.logger = Some(logger);
|
||||
previous
|
||||
};
|
||||
if let Some(previous) = previous {
|
||||
let _ = previous.shutdown();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn shutdown_with_method() -> Result<(), Error> {
|
||||
let factory = {
|
||||
let mut state = state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
state.logger = None;
|
||||
state.factory.take()
|
||||
};
|
||||
if let Some(factory) = factory {
|
||||
let _ = factory.shutdown();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown_with_nullable_cancellation_token(
|
||||
timeout: Option<Option<Duration>>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<bool, Error> {
|
||||
let factory = {
|
||||
let mut state = state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
state.logger = None;
|
||||
state.factory.take()
|
||||
};
|
||||
let Some(factory) = factory else {
|
||||
return Ok(true);
|
||||
};
|
||||
let completion = TaskCompletionSource::new();
|
||||
let shutdown_completion = completion.clone();
|
||||
if std::thread::Builder::new()
|
||||
.name("logger-shutdown".to_owned())
|
||||
.spawn(move || {
|
||||
let _ = shutdown_completion.try_set_result(factory.shutdown().is_ok());
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
let cancellation = cancellation_token.map(|token| {
|
||||
let completion = completion.clone();
|
||||
token.register_callback(Arc::new(move || {
|
||||
let _ = completion.try_set_result(false);
|
||||
}))
|
||||
});
|
||||
if let Some(timeout) = timeout.flatten() {
|
||||
let timeout_completion = completion.clone();
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("logger-shutdown-timeout".to_owned())
|
||||
.spawn(move || {
|
||||
std::thread::sleep(timeout);
|
||||
let _ = timeout_completion.try_set_result(false);
|
||||
});
|
||||
}
|
||||
let result = completion.future().await;
|
||||
drop(cancellation);
|
||||
result
|
||||
}
|
||||
|
||||
fn callbacks() -> Vec<LoggerLogCallback> {
|
||||
state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.callbacks
|
||||
.values()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn raise(message: &Object, level: LogLevel) {
|
||||
for callback in Self::callbacks() {
|
||||
let message = message.clone();
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("logger-event".to_owned())
|
||||
.spawn(move || {
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let _ = callback.invoke(message, level);
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn client_name(mut client: Option<GridClient>) -> Option<String> {
|
||||
let client = client.as_mut()?;
|
||||
if !client.settings().logging.log_names {
|
||||
return None;
|
||||
}
|
||||
let name = client.self_().name();
|
||||
(!name.is_empty()).then_some(name)
|
||||
}
|
||||
|
||||
fn log_to_sink(
|
||||
message: &Object,
|
||||
level: LogLevel,
|
||||
exception: Option<&ExternalError>,
|
||||
client_name: Option<&str>,
|
||||
) {
|
||||
let logger = ensure_initialized();
|
||||
if logger.is_enabled(level) {
|
||||
logger.log(level, message, exception, client_name);
|
||||
}
|
||||
}
|
||||
|
||||
fn log(
|
||||
message: Object,
|
||||
level: LogLevel,
|
||||
client: Option<GridClient>,
|
||||
exception: Option<ExternalError>,
|
||||
) -> Result<(), Error> {
|
||||
Self::raise(&message, level);
|
||||
let configured = Settings::log_level();
|
||||
if configured == LogLevel::NONE || configured > level {
|
||||
return Ok(());
|
||||
}
|
||||
let client_name = Self::client_name(client);
|
||||
Self::log_to_sink(&message, level, exception.as_ref(), client_name.as_deref());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn log_named(
|
||||
message: Object,
|
||||
level: LogLevel,
|
||||
exception: Option<ExternalError>,
|
||||
client_name: String,
|
||||
) -> Result<(), Error> {
|
||||
Self::log_to_sink(&message, level, exception.as_ref(), Some(&client_name));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_enabled(level: LogLevel) -> Result<bool, Error> {
|
||||
Ok(ensure_initialized().is_enabled(level))
|
||||
}
|
||||
|
||||
pub fn log_with_object_log_level_grid_client(
|
||||
message: Object,
|
||||
level: LogLevel,
|
||||
client: Option<GridClient>,
|
||||
) -> Result<(), Error> {
|
||||
Self::log(message, level, client, None)
|
||||
}
|
||||
|
||||
pub fn log_with_object_log_level_grid_client_exception(
|
||||
message: Object,
|
||||
level: LogLevel,
|
||||
client: Option<GridClient>,
|
||||
exception: Option<ExternalError>,
|
||||
) -> Result<(), Error> {
|
||||
Self::log(message, level, client, exception)
|
||||
}
|
||||
|
||||
pub fn log_with_object_log_level_exception(
|
||||
message: Object,
|
||||
level: LogLevel,
|
||||
exception: ExternalError,
|
||||
) -> Result<(), Error> {
|
||||
Self::log(message, level, None, Some(exception))
|
||||
}
|
||||
|
||||
pub fn debug_log(message: Object, client: Option<GridClient>) -> Result<(), Error> {
|
||||
let level = Settings::log_level();
|
||||
if level > LogLevel::DEBUG {
|
||||
return Ok(());
|
||||
}
|
||||
Self::log(message, level, client, None)
|
||||
}
|
||||
|
||||
pub async fn use_scope(
|
||||
state: Object,
|
||||
action: Box<dyn Fn() -> Task<()> + Send + Sync>,
|
||||
) -> Result<(), Error> {
|
||||
let mut scope = Self::begin_scope(state)?;
|
||||
let result = action().await;
|
||||
let close = scope.close().map_err(|_| Error::InvalidOperation);
|
||||
result.and(close)
|
||||
}
|
||||
|
||||
pub async fn use_client_scope(
|
||||
client: GridClient,
|
||||
action: Box<dyn Fn() -> Task<()> + Send + Sync>,
|
||||
) -> Result<(), Error> {
|
||||
let mut scope = Self::begin_client_scope(client)?;
|
||||
let result = action().await;
|
||||
let close = scope.close().map_err(|_| Error::InvalidOperation);
|
||||
result.and(close)
|
||||
}
|
||||
|
||||
pub async fn use_region_scope(
|
||||
simulator: Simulator,
|
||||
action: Box<dyn Fn() -> Task<()> + Send + Sync>,
|
||||
) -> Result<(), Error> {
|
||||
let mut scope = Self::begin_region_scope(simulator)?;
|
||||
let result = action().await;
|
||||
let close = scope.close().map_err(|_| Error::InvalidOperation);
|
||||
result.and(close)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! level_methods {
|
||||
($client:ident, $client_error:ident, $named:ident, $named_error:ident, $level:expr) => {
|
||||
impl Logger {
|
||||
pub fn $client(message: Object, client: Option<GridClient>) -> Result<(), Error> {
|
||||
Self::log(message, $level, client, None)
|
||||
}
|
||||
|
||||
pub fn $client_error(
|
||||
message: Object,
|
||||
exception: ExternalError,
|
||||
client: Option<GridClient>,
|
||||
) -> Result<(), Error> {
|
||||
Self::log(message, $level, client, Some(exception))
|
||||
}
|
||||
|
||||
pub fn $named(message: Object, client_name: String) -> Result<(), Error> {
|
||||
Self::log_named(message, $level, None, client_name)
|
||||
}
|
||||
|
||||
pub fn $named_error(
|
||||
message: Object,
|
||||
exception: ExternalError,
|
||||
client_name: String,
|
||||
) -> Result<(), Error> {
|
||||
Self::log_named(message, $level, Some(exception), client_name)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
level_methods!(
|
||||
trace_with_object_grid_client,
|
||||
trace_with_object_exception_grid_client,
|
||||
trace_with_object_string,
|
||||
trace_with_object_exception_string,
|
||||
LogLevel::TRACE
|
||||
);
|
||||
level_methods!(
|
||||
debug_with_object_grid_client,
|
||||
debug_with_object_exception_grid_client,
|
||||
debug_with_object_string,
|
||||
debug_with_object_exception_string,
|
||||
LogLevel::DEBUG
|
||||
);
|
||||
level_methods!(
|
||||
info_with_object_grid_client,
|
||||
info_with_object_exception_grid_client,
|
||||
info_with_object_string,
|
||||
info_with_object_exception_string,
|
||||
LogLevel::INFORMATION
|
||||
);
|
||||
level_methods!(
|
||||
warn_with_object_grid_client,
|
||||
warn_with_object_exception_grid_client,
|
||||
warn_with_object_string,
|
||||
warn_with_object_exception_string,
|
||||
LogLevel::WARNING
|
||||
);
|
||||
level_methods!(
|
||||
error_with_object_grid_client,
|
||||
error_with_object_exception_grid_client,
|
||||
error_with_object_string,
|
||||
error_with_object_exception_string,
|
||||
LogLevel::ERROR
|
||||
);
|
||||
level_methods!(
|
||||
critical_with_object_grid_client,
|
||||
critical_with_object_exception_grid_client,
|
||||
critical_with_object_string,
|
||||
critical_with_object_exception_string,
|
||||
LogLevel::CRITICAL
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
|
||||
#[test]
|
||||
fn event_subscription_delivers_and_unregisters() {
|
||||
Logger::shutdown_with_method().unwrap();
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&calls);
|
||||
let subscription = Logger::subscribe_on_log_message(Some(
|
||||
LoggerLogCallback::from_callback(move |_, _| {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
}),
|
||||
));
|
||||
Logger::info_with_object_grid_client(Object::String("one".to_owned()), None).unwrap();
|
||||
for _ in 0..100 {
|
||||
if calls.load(Ordering::Relaxed) == 1 {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
assert_eq!(calls.load(Ordering::Relaxed), 1);
|
||||
drop(subscription);
|
||||
Logger::info_with_object_grid_client(Object::String("two".to_owned()), None).unwrap();
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
assert_eq!(calls.load(Ordering::Relaxed), 1);
|
||||
Logger::shutdown_with_method().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scope_close_is_idempotent() {
|
||||
let _ = ensure_initialized();
|
||||
let mut scope = Logger::begin_scope(Object::String("test".to_owned())).unwrap();
|
||||
scope.close().unwrap();
|
||||
scope.close().unwrap();
|
||||
Logger::shutdown_with_method().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1634,7 +1634,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let result = response.parse_with_osd_map(map).unwrap();
|
||||
assert!(result.success());
|
||||
assert_eq!(response.home().region_handle, (1000_u64 << 32) | 2000);
|
||||
assert_eq!(response.home().region_handle, (1000_u64 << 32) | 0x07d0);
|
||||
assert_eq!(response.inventory_skeleton().len(), 1);
|
||||
let inventory = response.inventory_skeleton();
|
||||
assert_eq!(inventory[0].base.uuid(), folder);
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
//! Runtime LSL keyword catalog with OpenSim capability refresh support.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)] // Public signatures mirror the compatibility map.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::Engine as _;
|
||||
use flate2::read::GzDecoder;
|
||||
use libremetaverse_structured_data::{OSD, OSDParser};
|
||||
use libremetaverse_types::UUID;
|
||||
use libremetaverse_types::compat::{
|
||||
CancellationToken, EventHandler, FrozenDictionary, Subscription, Uri,
|
||||
};
|
||||
|
||||
use crate::{Error, GridClient, LslSyntaxLslCategory, LslSyntaxLslKeyword, Simulator};
|
||||
|
||||
const VERSION_KEY: &str = "llsd-lsl-syntax-version";
|
||||
const SYNTAX_FEATURE: &str = "LSLSyntaxId";
|
||||
const SYNTAX_CAPABILITY: &str = "LSLSyntax";
|
||||
const MAX_SYNTAX_BYTES: usize = 8 * 1024 * 1024;
|
||||
const FETCH_WAIT: Duration = Duration::from_secs(20);
|
||||
const DEFAULT_SYNTAX_GZIP_BASE64: &str =
|
||||
include_str!("../resources/keywords_lsl_default.xml.gz.b64");
|
||||
|
||||
static KEYWORDS: OnceLock<RwLock<HashMap<String, LslSyntaxLslKeyword>>> = OnceLock::new();
|
||||
|
||||
fn keyword_store() -> &'static RwLock<HashMap<String, LslSyntaxLslKeyword>> {
|
||||
KEYWORDS.get_or_init(|| RwLock::new(HashMap::new()))
|
||||
}
|
||||
|
||||
struct SyntaxEvents {
|
||||
next_id: AtomicU64,
|
||||
handlers: Mutex<Vec<(u64, EventHandler<()>)>>,
|
||||
}
|
||||
|
||||
impl Default for SyntaxEvents {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
next_id: AtomicU64::new(1),
|
||||
handlers: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LslSyntaxInner {
|
||||
events: Arc<SyntaxEvents>,
|
||||
client: Mutex<Option<GridClient>>,
|
||||
syntax_id: Mutex<UUID>,
|
||||
subscriptions: Mutex<Vec<Subscription>>,
|
||||
}
|
||||
|
||||
/// Process-wide LSL keyword catalog and per-registration change notification.
|
||||
#[derive(Clone)]
|
||||
pub struct LslSyntax(Arc<LslSyntaxInner>);
|
||||
|
||||
impl LslSyntax {
|
||||
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||
let syntax = Self::empty();
|
||||
syntax.load_default()?;
|
||||
Ok(syntax)
|
||||
}
|
||||
|
||||
pub fn new_with_grid_client(client: GridClient) -> Result<Self, Error> {
|
||||
let syntax = Self::empty();
|
||||
syntax.register(client)?;
|
||||
Ok(syntax)
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
Self(Arc::new(LslSyntaxInner {
|
||||
events: Arc::new(SyntaxEvents::default()),
|
||||
client: Mutex::new(None),
|
||||
syntax_id: Mutex::new(UUID::zero()),
|
||||
subscriptions: Mutex::new(Vec::new()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn subscribe_syntax_changed(&self, handler: EventHandler<()>) -> Subscription {
|
||||
let id = self.0.events.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
self.0
|
||||
.events
|
||||
.handlers
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push((id, handler));
|
||||
let events = Arc::downgrade(&self.0.events);
|
||||
Subscription::new(move || {
|
||||
if let Some(events) = events.upgrade() {
|
||||
events
|
||||
.handlers
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.retain(|(candidate, _)| *candidate != id);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn keywords() -> FrozenDictionary<String, LslSyntaxLslKeyword> {
|
||||
FrozenDictionary(
|
||||
keyword_store()
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn register(&self, client: GridClient) -> Result<(), Error> {
|
||||
*self
|
||||
.0
|
||||
.client
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(client.clone());
|
||||
|
||||
// Always establish a useful catalog, including on OpenSim grids that
|
||||
// do not advertise the optional LSLSyntax capability.
|
||||
self.load_default()?;
|
||||
|
||||
let inner = Arc::clone(&self.0);
|
||||
let network = client.native_network()?;
|
||||
let client_for_change = client.clone();
|
||||
let changed = network.native_subscribe_sim_changed(Arc::new(move |_| {
|
||||
if let Ok(network) = client_for_change.native_network()
|
||||
&& let Some(simulator) = network.native_current_sim()
|
||||
{
|
||||
LslSyntaxInner::observe_simulator(&inner, simulator, false);
|
||||
}
|
||||
}));
|
||||
self.retain_subscription(changed);
|
||||
|
||||
if let Some(simulator) = network.native_current_sim() {
|
||||
LslSyntaxInner::observe_simulator(&self.0, simulator, true);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn retain_subscription(&self, subscription: Subscription) {
|
||||
self.0
|
||||
.subscriptions
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push(subscription);
|
||||
}
|
||||
|
||||
fn load_default(&self) -> Result<(), Error> {
|
||||
let compact = DEFAULT_SYNTAX_GZIP_BASE64
|
||||
.split_ascii_whitespace()
|
||||
.collect::<String>();
|
||||
let compressed = base64::engine::general_purpose::STANDARD
|
||||
.decode(compact)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
let mut decoder = GzDecoder::new(compressed.as_slice());
|
||||
let mut bytes = Vec::new();
|
||||
decoder
|
||||
.by_ref()
|
||||
.take((MAX_SYNTAX_BYTES + 1) as u64)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
if bytes.len() > MAX_SYNTAX_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
self.0.parse_and_publish(&bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl LslSyntaxInner {
|
||||
fn observe_simulator(this: &Arc<Self>, simulator: Simulator, wait: bool) {
|
||||
if let Some(caps) = simulator.native_caps() {
|
||||
let weak = Arc::downgrade(this);
|
||||
let subscription = caps.subscribe_capabilities_received(Some(Arc::new(move |args| {
|
||||
if let Some(inner) = weak.upgrade() {
|
||||
inner.refresh(args.simulator(), false);
|
||||
}
|
||||
})));
|
||||
this.subscriptions
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push(subscription);
|
||||
}
|
||||
this.refresh(simulator, wait);
|
||||
}
|
||||
|
||||
fn refresh(self: &Arc<Self>, simulator: Simulator, wait: bool) {
|
||||
let syntax_id = match simulator.features.get(SYNTAX_FEATURE.to_owned()) {
|
||||
Ok(Some(value)) => match value.as_uuid() {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
},
|
||||
_ => return,
|
||||
};
|
||||
if *self
|
||||
.syntax_id
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
== syntax_id
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(client) = self
|
||||
.client
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let cache = cache_path(&client, syntax_id);
|
||||
if let Ok(bytes) = std::fs::read(&cache)
|
||||
&& self.parse_and_publish(&bytes).is_ok()
|
||||
{
|
||||
*self
|
||||
.syntax_id
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = syntax_id;
|
||||
return;
|
||||
}
|
||||
let Ok(Some(uri)) = simulator.native_capability_uri(SYNTAX_CAPABILITY) else {
|
||||
return;
|
||||
};
|
||||
self.fetch(client, uri, syntax_id, cache, wait);
|
||||
}
|
||||
|
||||
fn fetch(
|
||||
self: &Arc<Self>,
|
||||
client: GridClient,
|
||||
uri: Uri,
|
||||
syntax_id: UUID,
|
||||
cache: PathBuf,
|
||||
wait: bool,
|
||||
) {
|
||||
let inner = Arc::clone(self);
|
||||
let (completed, receiver) = std::sync::mpsc::sync_channel(1);
|
||||
let spawn = std::thread::Builder::new()
|
||||
.name("lsl-syntax-fetch".to_owned())
|
||||
.spawn(move || {
|
||||
let outcome = fetch_bytes(&client, uri).and_then(|bytes| {
|
||||
inner.parse_and_publish(&bytes)?;
|
||||
persist_cache(&cache, &bytes)?;
|
||||
*inner
|
||||
.syntax_id
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = syntax_id;
|
||||
Ok(())
|
||||
});
|
||||
let _ = completed.send(outcome);
|
||||
});
|
||||
if spawn.is_err() {
|
||||
return;
|
||||
}
|
||||
if wait {
|
||||
let _ = receiver.recv_timeout(FETCH_WAIT);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_and_publish(&self, bytes: &[u8]) -> Result<(), Error> {
|
||||
if bytes.is_empty() || bytes.len() > MAX_SYNTAX_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let OSD::Map(root) = OSDParser::deserialize_with_bytes(bytes.to_vec())? else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let parsed = parse_keywords(&root)?;
|
||||
if parsed.is_empty() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
*keyword_store()
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = parsed;
|
||||
let handlers = self
|
||||
.events
|
||||
.handlers
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.iter()
|
||||
.map(|(_, handler)| Arc::clone(handler))
|
||||
.collect::<Vec<_>>();
|
||||
for handler in handlers {
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(())));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_bytes(client: &GridClient, uri: Uri) -> Result<Vec<u8>, Error> {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
let http = client.native_http_caps_client();
|
||||
let (response, bytes) = runtime.block_on(http.get(uri, CancellationToken::default(), None))?;
|
||||
if !response.is_success_status_code() || bytes.is_empty() || bytes.len() > MAX_SYNTAX_BYTES {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn cache_path(client: &GridClient, syntax_id: UUID) -> PathBuf {
|
||||
Path::new(&client.settings_ref().asset_cache.dir).join(format!("keywords_lsl_{syntax_id}.xml"))
|
||||
}
|
||||
|
||||
fn persist_cache(path: &Path, bytes: &[u8]) -> Result<(), Error> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|_| Error::InvalidOperation)?;
|
||||
}
|
||||
std::fs::write(path, bytes).map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
|
||||
fn parse_keywords(
|
||||
root: &HashMap<String, OSD>,
|
||||
) -> Result<HashMap<String, LslSyntaxLslKeyword>, Error> {
|
||||
if root
|
||||
.get(VERSION_KEY)
|
||||
.map(OSD::as_integer)
|
||||
.transpose()?
|
||||
.is_some_and(|version| version != 2)
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut keywords = HashMap::new();
|
||||
let mut groups = root.iter().collect::<Vec<_>>();
|
||||
groups.sort_by(|left, right| left.0.cmp(right.0));
|
||||
for (group, value) in groups {
|
||||
if group == VERSION_KEY {
|
||||
continue;
|
||||
}
|
||||
let OSD::Map(items) = value else {
|
||||
continue;
|
||||
};
|
||||
let Some(category) = category(group) else {
|
||||
continue;
|
||||
};
|
||||
let mut items = items.iter().collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| left.0.cmp(right.0));
|
||||
for (keyword, value) in items {
|
||||
let OSD::Map(attributes) = value else {
|
||||
continue;
|
||||
};
|
||||
let tooltip = tooltip(group, keyword, attributes)?;
|
||||
let value = LslSyntaxLslKeyword {
|
||||
category,
|
||||
keyword: keyword.clone(),
|
||||
tooltip,
|
||||
deprecated: boolean_attribute(attributes, "deprecated")?,
|
||||
god_mode: boolean_attribute(attributes, "god-mode")?,
|
||||
};
|
||||
if keywords.insert(keyword.clone(), value).is_some() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(keywords)
|
||||
}
|
||||
|
||||
fn category(group: &str) -> Option<LslSyntaxLslCategory> {
|
||||
match group {
|
||||
"controls" => Some(LslSyntaxLslCategory::Control),
|
||||
"types" => Some(LslSyntaxLslCategory::Datatype),
|
||||
"constants" => Some(LslSyntaxLslCategory::Constant),
|
||||
"events" => Some(LslSyntaxLslCategory::Event),
|
||||
"functions" => Some(LslSyntaxLslCategory::Function),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn tooltip(group: &str, keyword: &str, attributes: &HashMap<String, OSD>) -> Result<String, Error> {
|
||||
let mut output = attributes
|
||||
.get("tooltip")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
if attributes.contains_key("tooltip") {
|
||||
output.push('\n');
|
||||
}
|
||||
match group {
|
||||
"constants" => {
|
||||
let type_ = string_attribute(attributes, "type")?;
|
||||
let value = string_attribute(attributes, "value")?;
|
||||
output.push_str(&format!(" Type: {type_}-{value}"));
|
||||
}
|
||||
"events" => {
|
||||
output.push_str(&format!(
|
||||
"{keyword} ({})",
|
||||
parse_arguments(attributes.get("arguments"))?
|
||||
));
|
||||
}
|
||||
"functions" => {
|
||||
let return_ = string_attribute(attributes, "return")?;
|
||||
output.push_str(&format!(
|
||||
"{return_} {keyword} ({})\n",
|
||||
parse_arguments(attributes.get("arguments"))?
|
||||
));
|
||||
let energy = attributes
|
||||
.get("energy")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| "0.0".to_owned());
|
||||
output.push_str(&format!("Energy: {energy}"));
|
||||
if let Some(sleep) = attributes.get("sleep") {
|
||||
output.push_str(&format!(", Sleep: {}", sleep.as_string()?));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn parse_arguments(value: Option<&OSD>) -> Result<String, Error> {
|
||||
let Some(OSD::Array(arguments)) = value else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
let mut rendered = Vec::new();
|
||||
for argument in arguments {
|
||||
let OSD::Map(argument) = argument else {
|
||||
continue;
|
||||
};
|
||||
let mut values = argument.iter().collect::<Vec<_>>();
|
||||
values.sort_by(|left, right| left.0.cmp(right.0));
|
||||
for (name, type_) in values {
|
||||
rendered.push(format!("{} {name}", type_.as_string()?));
|
||||
}
|
||||
}
|
||||
Ok(rendered.join(", "))
|
||||
}
|
||||
|
||||
fn string_attribute(attributes: &HashMap<String, OSD>, name: &str) -> Result<String, Error> {
|
||||
attributes
|
||||
.get(name)
|
||||
.map(OSD::as_string)
|
||||
.transpose()
|
||||
.map(Option::unwrap_or_default)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn boolean_attribute(attributes: &HashMap<String, OSD>, name: &str) -> Result<bool, Error> {
|
||||
attributes
|
||||
.get(name)
|
||||
.map(OSD::as_boolean)
|
||||
.transpose()
|
||||
.map(|value| value.unwrap_or(false))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use super::*;
|
||||
|
||||
static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn embedded_catalog_is_complete_and_typed() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
let _syntax = LslSyntax::new_with_constructor().unwrap();
|
||||
let keywords = LslSyntax::keywords().0;
|
||||
assert!(keywords.len() > 500);
|
||||
assert_eq!(
|
||||
keywords.get("llSay").unwrap().category,
|
||||
LslSyntaxLslCategory::Function
|
||||
);
|
||||
assert_eq!(
|
||||
keywords.get("state_entry").unwrap().category,
|
||||
LslSyntaxLslCategory::Event
|
||||
);
|
||||
assert_eq!(
|
||||
keywords.get("integer").unwrap().category,
|
||||
LslSyntaxLslCategory::Datatype
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsed_catalog_replaces_snapshot_and_notifies_subscribers() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
let syntax = LslSyntax::empty();
|
||||
let notifications = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(¬ifications);
|
||||
let _subscription = syntax.subscribe_syntax_changed(Arc::new(move |()| {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
}));
|
||||
let function = OSD::Map(HashMap::from([
|
||||
("return".to_owned(), OSD::String("integer".to_owned())),
|
||||
("energy".to_owned(), OSD::String("10.0".to_owned())),
|
||||
("deprecated".to_owned(), OSD::Boolean(true)),
|
||||
(
|
||||
"arguments".to_owned(),
|
||||
OSD::Array(vec![OSD::Map(HashMap::from([(
|
||||
"message".to_owned(),
|
||||
OSD::String("string".to_owned()),
|
||||
)]))]),
|
||||
),
|
||||
]));
|
||||
let document = OSD::Map(HashMap::from([
|
||||
(VERSION_KEY.to_owned(), OSD::Integer(2)),
|
||||
(
|
||||
"functions".to_owned(),
|
||||
OSD::Map(HashMap::from([("osTest".to_owned(), function)])),
|
||||
),
|
||||
]));
|
||||
let bytes = OSDParser::serialize_llsd_xml_bytes(document).unwrap();
|
||||
syntax.0.parse_and_publish(&bytes).unwrap();
|
||||
|
||||
assert_eq!(notifications.load(Ordering::Relaxed), 1);
|
||||
let keywords = LslSyntax::keywords().0;
|
||||
let keyword = keywords.get("osTest").unwrap();
|
||||
assert!(keyword.deprecated);
|
||||
assert_eq!(keyword.category, LslSyntaxLslCategory::Function);
|
||||
assert!(keyword.tooltip.contains("integer osTest (string message)"));
|
||||
assert!(keyword.tooltip.contains("Energy: 10.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registration_on_opensim_without_syntax_capability_uses_default_catalog() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
let client = GridClient::builder().build().unwrap();
|
||||
let syntax = LslSyntax::new_with_grid_client(client).unwrap();
|
||||
assert!(LslSyntax::keywords().0.contains_key("llOwnerSay"));
|
||||
assert_eq!(
|
||||
syntax
|
||||
.0
|
||||
.subscriptions
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,11 +1,17 @@
|
||||
//! Native LLSD field codecs for the translated Linden message fixtures.
|
||||
|
||||
#![allow(clippy::cast_possible_truncation)] // LLSD reals intentionally narrow to wire f32 fields.
|
||||
#![allow(clippy::too_many_arguments)] // Codec helpers mirror fixed message field sets.
|
||||
#![allow(clippy::type_complexity)] // Parsed tuples preserve the mapped message shape.
|
||||
#![allow(clippy::unnecessary_wraps)] // Handlers share the fallible registry signature.
|
||||
|
||||
use crate::messages::linden::{
|
||||
AgentDropGroupMessage, AgentDropGroupMessageAgentData, AgentGroupDataUpdateMessage,
|
||||
AgentGroupDataUpdateMessageGroupData, AgentGroupDataUpdateMessageNewGroupData,
|
||||
AgentStateUpdateMessage, BulkUpdateInventoryMessage, BulkUpdateInventoryMessageFolderDataInfo,
|
||||
BulkUpdateInventoryMessageItemDataInfo, ChatSessionAcceptInvitation, ChatSessionRequestBlock,
|
||||
ChatSessionRequestMuteUpdate, ChatSessionRequestStartConference, ChatterBoxInvitationMessage,
|
||||
AgentStateUpdateMessage, BaseResourcesInfo, BulkUpdateInventoryMessage,
|
||||
BulkUpdateInventoryMessageFolderDataInfo, BulkUpdateInventoryMessageItemDataInfo,
|
||||
ChatSessionAcceptInvitation, ChatSessionRequestMessage, ChatSessionRequestMuteUpdate,
|
||||
ChatSessionRequestStartConference, ChatterBoxInvitationMessage,
|
||||
ChatterBoxSessionAgentListUpdatesMessage,
|
||||
ChatterBoxSessionAgentListUpdatesMessageAgentUpdatesBlock, ChatterBoxSessionStartReplyMessage,
|
||||
ChatterboxSessionEventReplyMessage, CopyInventoryFromNotecardMessage, CrossedRegionMessage,
|
||||
@@ -13,20 +19,29 @@ use crate::messages::linden::{
|
||||
EnableSimulatorMessage, EnableSimulatorMessageSimulatorInfoBlock,
|
||||
EstablishAgentCommunicationMessage, ForceCloseChatterBoxSessionMessage, GetDisplayNamesMessage,
|
||||
GetObjectCostMessage, GetObjectCostRequest, LandResourcesMessage, LandResourcesRequest,
|
||||
LandStatReplyMessage, LandStatReplyMessageReportDataBlock, MapLayerMessageBase,
|
||||
LandStatReplyMessage, LandStatReplyMessageReportDataBlock, MapLayerMessage,
|
||||
MapLayerReplyVariant, MapLayerReplyVariantLayerData, NavMeshStatusUpdateMessage,
|
||||
ObjectMediaNavigateMessage, ObjectMediaRequest, ObjectMediaResponse, ObjectMediaUpdate,
|
||||
ObjectPhysicsPropertiesMessage, ParcelObjectOwnersReplyMessage,
|
||||
ParcelObjectOwnersReplyMessagePrimOwner, ParcelPropertiesMessage,
|
||||
ParcelPropertiesUpdateMessage, ParcelVoiceInfoRequestMessage, PlacesReplyMessage,
|
||||
PlacesReplyMessageQueryData, ProvisionVoiceAccountRequestMessage, RegionInfoMessage,
|
||||
RemoteParcelRequestReply, RenderMaterialsMessage, RequiredVoiceVersionMessage,
|
||||
ScriptRunningReplyMessage, SearchStatRequestReply, SearchStatRequestRequest,
|
||||
SendPostcardMessage, SetDisplayNameMessage, SetDisplayNameReplyMessage,
|
||||
SimConsoleResponseMessage, TeleportFailedMessage, TeleportFinishMessage,
|
||||
UpdateAgentInformationMessage, UpdateAgentInventoryRequestMessage, UpdateAgentLanguageMessage,
|
||||
UpdateScriptAgentRequestMessage, UpdateScriptTaskUpdateMessage, UploaderRequestComplete,
|
||||
UploaderRequestUpload, ViewerStatsMessage,
|
||||
NewFileAgentInventoryMessage, NewFileAgentInventoryReplyMessage,
|
||||
NewFileAgentInventoryUploadReplyMessage, NewFileAgentInventoryVariablePriceMessage,
|
||||
NewFileAgentInventoryVariablePriceReplyMessage, ObjectMediaMessage, ObjectMediaNavigateMessage,
|
||||
ObjectMediaRequest, ObjectMediaResponse, ObjectMediaUpdate, ObjectPhysicsPropertiesMessage,
|
||||
ParcelObjectOwnersReplyMessage, ParcelObjectOwnersReplyMessagePrimOwner,
|
||||
ParcelPropertiesMessage, ParcelPropertiesUpdateMessage, ParcelVoiceInfoRequestMessage,
|
||||
PlacesReplyMessage, PlacesReplyMessageQueryData, ProvisionVoiceAccountRequestMessage,
|
||||
RegionInfoMessage, RemoteParcelRequestMessage, RemoteParcelRequestReply,
|
||||
RemoteParcelRequestRequest, RenderMaterialsMessage, RequiredVoiceVersionMessage,
|
||||
ScriptRunningReplyMessage, SearchStatRequestMessage, SearchStatRequestReply,
|
||||
SearchStatRequestRequest, SendPostcardMessage, SetDisplayNameMessage,
|
||||
SetDisplayNameReplyMessage, SimConsoleResponseMessage, TeleportFailedMessage,
|
||||
TeleportFinishMessage, UpdateAgentInformationMessage, UpdateAgentInventoryRequestMessage,
|
||||
UpdateAgentLanguageMessage, UpdateGestureAgentInventoryMessage,
|
||||
UpdateNotecardAgentInventoryMessage, UpdateNotecardTaskInventoryMessage,
|
||||
UpdateScriptAgentMessage, UpdateScriptAgentRequestMessage, UpdateScriptTaskMessage,
|
||||
UpdateScriptTaskUpdateMessage, UploadBakedTextureMessage, UploadObjectAssetMessage,
|
||||
UploadObjectAssetMessageObject, UploadObjectAssetMessageObjectExtraParam,
|
||||
UploadObjectAssetMessageObjectFace, UploadScriptTaskMessage, UploaderRequestComplete,
|
||||
UploaderRequestScriptComplete, UploaderRequestUpload, UploaderScriptRequestError,
|
||||
ViewerStatsMessage,
|
||||
};
|
||||
use crate::{
|
||||
AgentDisplayName, Error, GroupPowers, InstantMessageDialog, InstantMessageOnline, LandingType,
|
||||
@@ -36,7 +51,10 @@ use crate::{
|
||||
use flate2::read::ZlibDecoder;
|
||||
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
||||
use libremetaverse_types::compat::Uri;
|
||||
use libremetaverse_types::{AssetType, FolderType, InventoryType, SaleType, UUID, Vector3};
|
||||
use libremetaverse_types::{
|
||||
AssetType, Color4, ExtraParamType, FolderType, InventoryType, Material, Quaternion, SaleType,
|
||||
SculptType, UUID, Vector3,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read as _;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
@@ -54,6 +72,38 @@ pub(crate) trait GeneratedMessage: Sized {
|
||||
fn deserialize_generated(&mut self, map: &OSDMap) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
pub(crate) fn generated_from_osd<T: GeneratedMessage>(data: OSD) -> Result<T, Error> {
|
||||
let OSD::Map(values) = data else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let map = OSDMap::new_with_dictionary(values)?;
|
||||
let mut value = T::new_generated();
|
||||
value.deserialize_generated(&map)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub(crate) fn get_object_cost_handler(
|
||||
map: &OSDMap,
|
||||
) -> Result<Option<Box<dyn crate::interfaces::IMessage>>, Error> {
|
||||
if map.get("object_ids").is_some() {
|
||||
Ok(Some(Box::new(GetObjectCostRequest::new_generated())))
|
||||
} else {
|
||||
Ok(Some(Box::new(GetObjectCostMessage::new_generated())))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn land_resources_handler(
|
||||
map: &OSDMap,
|
||||
) -> Result<Option<Box<dyn crate::interfaces::IMessage>>, Error> {
|
||||
if map.get("parcel_id").is_some() {
|
||||
Ok(Some(Box::new(LandResourcesRequest::new_generated())))
|
||||
} else if map.get("ScriptResourceSummary").is_some() {
|
||||
Ok(Some(Box::new(LandResourcesMessage::new_generated())))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! integer_value {
|
||||
($type:ty, $from:ident, $as:ident) => {
|
||||
impl MessageValue for $type {
|
||||
@@ -232,6 +282,24 @@ impl MessageValue for OSDMap {
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for HashMap<String, i32> {
|
||||
fn message_default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.iter()
|
||||
.map(|(key, value)| Ok((key.clone(), value.to_message_osd()?)))
|
||||
.collect::<Result<HashMap<_, _>, Error>>()
|
||||
.map(OSD::Map)
|
||||
}
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
as_map(value)?
|
||||
.iter()
|
||||
.map(|(key, value)| Ok((key.clone(), i32::from_message_osd(value)?)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for AgentDisplayName {
|
||||
fn message_default() -> Self {
|
||||
Self::new().expect("AgentDisplayName construction is infallible")
|
||||
@@ -813,6 +881,50 @@ fn deserialize_get_object_cost(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_base_resources(value: &BaseResourcesInfo) -> Result<OSDMap, Error> {
|
||||
fn entries(values: &HashMap<String, i32>) -> Result<OSD, Error> {
|
||||
let mut values: Vec<_> = values.iter().collect();
|
||||
values.sort_unstable_by(|left, right| left.0.cmp(right.0));
|
||||
values
|
||||
.into_iter()
|
||||
.map(|(type_, amount)| {
|
||||
Ok(map_osd([
|
||||
("type", type_.to_message_osd()?),
|
||||
("amount", amount.to_message_osd()?),
|
||||
]))
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error>>()
|
||||
.map(OSD::Array)
|
||||
}
|
||||
finish_map([(
|
||||
"summary",
|
||||
map_osd([
|
||||
("available", entries(&value.summary_available)?),
|
||||
("used", entries(&value.summary_used)?),
|
||||
]),
|
||||
)])
|
||||
}
|
||||
|
||||
fn deserialize_base_resources(value: &mut BaseResourcesInfo, map: &OSDMap) -> Result<(), Error> {
|
||||
fn entries(value: &OSD) -> Result<HashMap<String, i32>, Error> {
|
||||
as_array(value)?
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let entry = as_map(entry)?;
|
||||
Ok((
|
||||
String::from_message_osd(required_from(entry, "type")?)?,
|
||||
i32::from_message_osd(required_from(entry, "amount")?)?,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
let summary_value = required(map, "summary")?;
|
||||
let summary = as_map(&summary_value)?;
|
||||
value.summary_available = entries(required_from(summary, "available")?)?;
|
||||
value.summary_used = entries(required_from(summary, "used")?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_nav_mesh_status(value: &NavMeshStatusUpdateMessage) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("region_id", value.region_id.to_message_osd()?),
|
||||
@@ -1172,6 +1284,50 @@ macro_rules! custom_message {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! wrapped_message {
|
||||
($type:ty) => {
|
||||
impl GeneratedMessage for $type {
|
||||
fn new_generated() -> Self {
|
||||
Self {
|
||||
request: MessageValue::message_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_generated(&self) -> Result<OSDMap, Error> {
|
||||
let OSD::Map(values) = self.request.to_message_osd()? else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
OSDMap::new_with_dictionary(values)
|
||||
}
|
||||
|
||||
fn deserialize_generated(&mut self, map: &OSDMap) -> Result<(), Error> {
|
||||
self.request = MessageValue::from_message_osd(&map.copy()?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for $type {
|
||||
fn message_default() -> Self {
|
||||
<Self as GeneratedMessage>::new_generated()
|
||||
}
|
||||
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.serialize_generated()?.copy()
|
||||
}
|
||||
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let map = OSDMap::new_with_dictionary(values.clone())?;
|
||||
let mut output = <Self as GeneratedMessage>::new_generated();
|
||||
output.deserialize_generated(&map)?;
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! osd_entries {
|
||||
($value:expr; $($field:ident => $key:literal),* $(,)?) => {
|
||||
vec![$(($key, $value.$field.to_message_osd()?)),*]
|
||||
@@ -1237,11 +1393,6 @@ custom_message!(AgentGroupDataUpdateMessage {
|
||||
array: [group_data_block, new_group_data_block],
|
||||
bytes: []
|
||||
} => serialize_agent_group, deserialize_agent_group);
|
||||
map_message!(ChatSessionRequestBlock {
|
||||
scalar: [method => "method"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
custom_message!(ChatSessionAcceptInvitation { scalar: [base, session_id], array: [], bytes: [] } => serialize_chat_accept, deserialize_chat_accept, init |value: &mut ChatSessionAcceptInvitation| value.base.method = "accept invitation".into());
|
||||
custom_message!(ChatSessionRequestMuteUpdate { scalar: [base, agent_id, request_key, request_value, session_id], array: [], bytes: [] } => serialize_chat_mute, deserialize_chat_mute, init |value: &mut ChatSessionRequestMuteUpdate| value.base.method = "mute update".into());
|
||||
custom_message!(ChatSessionRequestStartConference { scalar: [base, session_id], array: [agents_block], bytes: [] } => serialize_chat_start, deserialize_chat_start, init |value: &mut ChatSessionRequestStartConference| value.base.method = "start conference".into());
|
||||
@@ -1372,11 +1523,6 @@ custom_message!(LandStatReplyMessage {
|
||||
array: [report_data_blocks],
|
||||
bytes: []
|
||||
} => serialize_land_stat, deserialize_land_stat);
|
||||
map_message!(MapLayerMessageBase {
|
||||
scalar: [flags => "Flags"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
map_message!(MapLayerReplyVariantLayerData {
|
||||
scalar: [
|
||||
bottom => "Bottom",
|
||||
@@ -1582,6 +1728,11 @@ custom_message!(GetObjectCostMessage {
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_get_object_cost, deserialize_get_object_cost);
|
||||
custom_message!(BaseResourcesInfo {
|
||||
scalar: [summary_available, summary_used],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_base_resources, deserialize_base_resources);
|
||||
map_message!(GetObjectCostRequest {
|
||||
scalar: [],
|
||||
array: [object_i_ds => "object_ids"],
|
||||
@@ -1793,6 +1944,229 @@ custom_message!(UploaderRequestComplete {
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_uploader_complete, deserialize_uploader_complete);
|
||||
custom_message!(NewFileAgentInventoryMessage {
|
||||
scalar: [asset_type, description, everyone_mask, folder_id, group_mask, inventory_type, name, next_owner_mask],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_new_file_inventory, deserialize_new_file_inventory);
|
||||
custom_message!(NewFileAgentInventoryVariablePriceMessage {
|
||||
scalar: [asset_type, description, everyone_mask, folder_id, group_mask, inventory_type, name, next_owner_mask],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_new_file_inventory_variable, deserialize_new_file_inventory_variable);
|
||||
map_message!(NewFileAgentInventoryReplyMessage {
|
||||
scalar: [state => "state", uploader => "uploader"],
|
||||
array: [],
|
||||
bytes: []
|
||||
} init |value: &mut NewFileAgentInventoryReplyMessage| value.state = "upload".into());
|
||||
map_message!(NewFileAgentInventoryVariablePriceReplyMessage {
|
||||
scalar: [resource_cost => "resource_cost", rsvp => "rsvp", state => "state", upload_price => "upload_price"],
|
||||
array: [],
|
||||
bytes: []
|
||||
} init |value: &mut NewFileAgentInventoryVariablePriceReplyMessage| value.state = "confirm_upload".into());
|
||||
map_message!(NewFileAgentInventoryUploadReplyMessage {
|
||||
scalar: [
|
||||
new_asset => "new_asset",
|
||||
new_base_mask => "new_base_mask",
|
||||
new_everyone_mask => "new_everyone_mask",
|
||||
new_inventory_item => "new_inventory_item",
|
||||
new_next_owner_mask => "new_next_owner_mask",
|
||||
new_owner_mask => "new_owner_mask",
|
||||
state => "state"
|
||||
],
|
||||
array: [],
|
||||
bytes: []
|
||||
} init |value: &mut NewFileAgentInventoryUploadReplyMessage| value.state = "complete".into());
|
||||
map_message!(UpdateNotecardTaskInventoryMessage {
|
||||
scalar: [item_id => "item_id", task_id => "task_id"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
custom_message!(UploaderRequestScriptComplete {
|
||||
scalar: [asset_id, compiled],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_uploader_script_complete, deserialize_uploader_script_complete);
|
||||
custom_message!(UploaderScriptRequestError {
|
||||
scalar: [asset_id, compiled, error],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_uploader_script_error, deserialize_uploader_script_error);
|
||||
map_message!(RemoteParcelRequestRequest {
|
||||
scalar: [location => "location", region_handle => "region_handle", region_id => "region_id"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
wrapped_message!(ChatSessionRequestMessage);
|
||||
wrapped_message!(MapLayerMessage);
|
||||
wrapped_message!(ObjectMediaMessage);
|
||||
wrapped_message!(RemoteParcelRequestMessage);
|
||||
wrapped_message!(SearchStatRequestMessage);
|
||||
wrapped_message!(UpdateGestureAgentInventoryMessage);
|
||||
wrapped_message!(UpdateNotecardAgentInventoryMessage);
|
||||
wrapped_message!(UpdateScriptAgentMessage);
|
||||
wrapped_message!(UpdateScriptTaskMessage);
|
||||
wrapped_message!(UploadBakedTextureMessage);
|
||||
wrapped_message!(UploadScriptTaskMessage);
|
||||
|
||||
fn serialize_new_file_inventory_fields(
|
||||
asset_type: AssetType,
|
||||
description: &str,
|
||||
everyone_mask: PermissionMask,
|
||||
folder_id: UUID,
|
||||
group_mask: PermissionMask,
|
||||
inventory_type: InventoryType,
|
||||
name: &str,
|
||||
next_owner_mask: PermissionMask,
|
||||
) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("folder_id", folder_id.to_message_osd()?),
|
||||
(
|
||||
"asset_type",
|
||||
libremetaverse_types::Utils::asset_type_to_string(asset_type)?.to_message_osd()?,
|
||||
),
|
||||
(
|
||||
"inventory_type",
|
||||
libremetaverse_types::Utils::inventory_type_to_string(inventory_type)?
|
||||
.to_message_osd()?,
|
||||
),
|
||||
("name", name.to_owned().to_message_osd()?),
|
||||
("description", description.to_owned().to_message_osd()?),
|
||||
("everyone_mask", everyone_mask.to_message_osd()?),
|
||||
("group_mask", group_mask.to_message_osd()?),
|
||||
("next_owner_mask", next_owner_mask.to_message_osd()?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_new_file_inventory_fields(
|
||||
map: &OSDMap,
|
||||
) -> Result<
|
||||
(
|
||||
AssetType,
|
||||
String,
|
||||
PermissionMask,
|
||||
UUID,
|
||||
PermissionMask,
|
||||
InventoryType,
|
||||
String,
|
||||
PermissionMask,
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
Ok((
|
||||
libremetaverse_types::Utils::string_to_asset_type(decode_top(map, "asset_type")?)?,
|
||||
decode_top(map, "description")?,
|
||||
decode_top(map, "everyone_mask")?,
|
||||
decode_top(map, "folder_id")?,
|
||||
decode_top(map, "group_mask")?,
|
||||
libremetaverse_types::Utils::string_to_inventory_type(decode_top(map, "inventory_type")?)?,
|
||||
decode_top(map, "name")?,
|
||||
decode_top(map, "next_owner_mask")?,
|
||||
))
|
||||
}
|
||||
|
||||
fn serialize_new_file_inventory(value: &NewFileAgentInventoryMessage) -> Result<OSDMap, Error> {
|
||||
serialize_new_file_inventory_fields(
|
||||
value.asset_type,
|
||||
&value.description,
|
||||
value.everyone_mask,
|
||||
value.folder_id,
|
||||
value.group_mask,
|
||||
value.inventory_type,
|
||||
&value.name,
|
||||
value.next_owner_mask,
|
||||
)
|
||||
}
|
||||
|
||||
fn deserialize_new_file_inventory(
|
||||
value: &mut NewFileAgentInventoryMessage,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
(
|
||||
value.asset_type,
|
||||
value.description,
|
||||
value.everyone_mask,
|
||||
value.folder_id,
|
||||
value.group_mask,
|
||||
value.inventory_type,
|
||||
value.name,
|
||||
value.next_owner_mask,
|
||||
) = deserialize_new_file_inventory_fields(map)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_new_file_inventory_variable(
|
||||
value: &NewFileAgentInventoryVariablePriceMessage,
|
||||
) -> Result<OSDMap, Error> {
|
||||
serialize_new_file_inventory_fields(
|
||||
value.asset_type,
|
||||
&value.description,
|
||||
value.everyone_mask,
|
||||
value.folder_id,
|
||||
value.group_mask,
|
||||
value.inventory_type,
|
||||
&value.name,
|
||||
value.next_owner_mask,
|
||||
)
|
||||
}
|
||||
|
||||
fn deserialize_new_file_inventory_variable(
|
||||
value: &mut NewFileAgentInventoryVariablePriceMessage,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
(
|
||||
value.asset_type,
|
||||
value.description,
|
||||
value.everyone_mask,
|
||||
value.folder_id,
|
||||
value.group_mask,
|
||||
value.inventory_type,
|
||||
value.name,
|
||||
value.next_owner_mask,
|
||||
) = deserialize_new_file_inventory_fields(map)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_uploader_script_complete(
|
||||
value: &UploaderRequestScriptComplete,
|
||||
) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("state", "complete".to_owned().to_message_osd()?),
|
||||
("new_asset", value.asset_id.to_message_osd()?),
|
||||
("compiled", value.compiled.to_message_osd()?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_uploader_script_complete(
|
||||
value: &mut UploaderRequestScriptComplete,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
value.asset_id = decode_top(map, "new_asset")?;
|
||||
value.compiled = decode_top(map, "compiled")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_uploader_script_error(value: &UploaderScriptRequestError) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("state", "complete".to_owned().to_message_osd()?),
|
||||
("new_asset", value.asset_id.to_message_osd()?),
|
||||
("compiled", value.compiled.to_message_osd()?),
|
||||
("errors", encode_array(std::slice::from_ref(&value.error))?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_uploader_script_error(
|
||||
value: &mut UploaderScriptRequestError,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
value.asset_id = decode_top(map, "new_asset")?;
|
||||
value.compiled = decode_top(map, "compiled")?;
|
||||
value.error = decode_array::<String>(&required(map, "errors")?)?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(Error::Argument)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_agent_group(value: &AgentGroupDataUpdateMessage) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
@@ -2918,6 +3292,329 @@ fn deserialize_viewer_stats(value: &mut ViewerStatsMessage, map: &OSDMap) -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn material_from_i32(value: i32) -> Result<Material, Error> {
|
||||
match value {
|
||||
0 => Ok(Material::Stone),
|
||||
1 => Ok(Material::Metal),
|
||||
2 => Ok(Material::Glass),
|
||||
3 => Ok(Material::Wood),
|
||||
4 => Ok(Material::Flesh),
|
||||
5 => Ok(Material::Plastic),
|
||||
6 => Ok(Material::Rubber),
|
||||
7 => Ok(Material::Light),
|
||||
_ => Err(Error::Argument),
|
||||
}
|
||||
}
|
||||
|
||||
fn sculpt_type_from_i32(value: i32) -> Result<SculptType, Error> {
|
||||
match value {
|
||||
0 => Ok(SculptType::None),
|
||||
1 => Ok(SculptType::Sphere),
|
||||
2 => Ok(SculptType::Torus),
|
||||
3 => Ok(SculptType::Plane),
|
||||
4 => Ok(SculptType::Cylinder),
|
||||
5 => Ok(SculptType::Mesh),
|
||||
64 => Ok(SculptType::Invert),
|
||||
128 => Ok(SculptType::Mirror),
|
||||
_ => Err(Error::Argument),
|
||||
}
|
||||
}
|
||||
|
||||
fn bumpiness_from_i32(value: i32) -> Result<crate::Bumpiness, Error> {
|
||||
use crate::Bumpiness;
|
||||
match value {
|
||||
0 => Ok(Bumpiness::None),
|
||||
1 => Ok(Bumpiness::Brightness),
|
||||
2 => Ok(Bumpiness::Darkness),
|
||||
3 => Ok(Bumpiness::Woodgrain),
|
||||
4 => Ok(Bumpiness::Bark),
|
||||
5 => Ok(Bumpiness::Bricks),
|
||||
6 => Ok(Bumpiness::Checker),
|
||||
7 => Ok(Bumpiness::Concrete),
|
||||
8 => Ok(Bumpiness::Crustytile),
|
||||
9 => Ok(Bumpiness::Cutstone),
|
||||
10 => Ok(Bumpiness::Discs),
|
||||
11 => Ok(Bumpiness::Gravel),
|
||||
12 => Ok(Bumpiness::Petridish),
|
||||
13 => Ok(Bumpiness::Siding),
|
||||
14 => Ok(Bumpiness::Stonetile),
|
||||
15 => Ok(Bumpiness::Stucco),
|
||||
16 => Ok(Bumpiness::Suction),
|
||||
17 => Ok(Bumpiness::Weave),
|
||||
_ => Err(Error::Argument),
|
||||
}
|
||||
}
|
||||
|
||||
impl GeneratedMessage for UploadObjectAssetMessageObjectFace {
|
||||
fn new_generated() -> Self {
|
||||
Self {
|
||||
bump: crate::Bumpiness::None,
|
||||
color: Color4::default(),
|
||||
fullbright: false,
|
||||
glow: 0.0,
|
||||
image_id: UUID::zero(),
|
||||
image_rot: 0.0,
|
||||
media_flags: 0,
|
||||
offset_s: 0.0,
|
||||
offset_t: 0.0,
|
||||
scale_s: 0.0,
|
||||
scale_t: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_generated(&self) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("bump", OSD::Integer(self.bump as i32)),
|
||||
("colors", OSD::from_color4(self.color)?),
|
||||
("fullbright", OSD::Boolean(self.fullbright)),
|
||||
("glow", OSD::Real(f64::from(self.glow))),
|
||||
("imageid", OSD::UUID(self.image_id)),
|
||||
("imagerot", OSD::Real(f64::from(self.image_rot))),
|
||||
("media_flags", OSD::Integer(self.media_flags)),
|
||||
("offsets", OSD::Real(f64::from(self.offset_s))),
|
||||
("offsett", OSD::Real(f64::from(self.offset_t))),
|
||||
("scales", OSD::Real(f64::from(self.scale_s))),
|
||||
("scalet", OSD::Real(f64::from(self.scale_t))),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_generated(&mut self, map: &OSDMap) -> Result<(), Error> {
|
||||
self.bump = bumpiness_from_i32(required(map, "bump")?.as_integer()?)?;
|
||||
self.color = required(map, "colors")?.as_color4()?;
|
||||
self.fullbright = required(map, "fullbright")?.as_boolean()?;
|
||||
self.glow = required(map, "glow")?.as_real()? as f32;
|
||||
self.image_id = required(map, "imageid")?.as_uuid()?;
|
||||
self.image_rot = required(map, "imagerot")?.as_real()? as f32;
|
||||
self.media_flags = required(map, "media_flags")?.as_integer()?;
|
||||
self.offset_s = required(map, "offsets")?.as_real()? as f32;
|
||||
self.offset_t = required(map, "offsett")?.as_real()? as f32;
|
||||
self.scale_s = required(map, "scales")?.as_real()? as f32;
|
||||
self.scale_t = required(map, "scalet")?.as_real()? as f32;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl GeneratedMessage for UploadObjectAssetMessageObjectExtraParam {
|
||||
fn new_generated() -> Self {
|
||||
Self {
|
||||
extra_param_data: Vec::new(),
|
||||
type_: ExtraParamType(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_generated(&self) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("extra_parameter", OSD::Integer(i32::from(self.type_.0))),
|
||||
("param_data", OSD::Binary(self.extra_param_data.clone())),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_generated(&mut self, map: &OSDMap) -> Result<(), Error> {
|
||||
self.type_ = ExtraParamType(
|
||||
u16::try_from(required(map, "extra_parameter")?.as_integer()?)
|
||||
.map_err(|_| Error::Argument)?,
|
||||
);
|
||||
self.extra_param_data = required(map, "param_data")?.as_binary()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_object_default() -> UploadObjectAssetMessageObject {
|
||||
UploadObjectAssetMessageObject {
|
||||
extra_params: Vec::new(),
|
||||
faces: Vec::new(),
|
||||
group_id: UUID::zero(),
|
||||
material: Material::Stone,
|
||||
name: String::new(),
|
||||
path_begin: 0.0,
|
||||
path_curve: 0,
|
||||
path_end: 0.0,
|
||||
position: Vector3::zero(),
|
||||
profile_begin: 0.0,
|
||||
profile_curve: 0,
|
||||
profile_end: 0.0,
|
||||
profile_hollow: 0.0,
|
||||
radius_offset: 0.0,
|
||||
revolutions: 0.0,
|
||||
rotation: Quaternion::identity(),
|
||||
scale: Vector3::zero(),
|
||||
scale_x: 0.0,
|
||||
scale_y: 0.0,
|
||||
sculpt_id: UUID::zero(),
|
||||
sculpt_type: SculptType::None,
|
||||
shear_x: 0.0,
|
||||
shear_y: 0.0,
|
||||
skew: 0.0,
|
||||
taper_x: 0.0,
|
||||
taper_y: 0.0,
|
||||
twist: 0.0,
|
||||
twist_begin: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
impl GeneratedMessage for UploadObjectAssetMessageObject {
|
||||
fn new_generated() -> Self {
|
||||
upload_object_default()
|
||||
}
|
||||
|
||||
fn serialize_generated(&self) -> Result<OSDMap, Error> {
|
||||
let encode = |values: &[&dyn GeneratedMessageObject]| -> Result<OSD, Error> {
|
||||
Ok(OSD::Array(
|
||||
values
|
||||
.iter()
|
||||
.map(|value| value.encode())
|
||||
.collect::<Result<_, _>>()?,
|
||||
))
|
||||
};
|
||||
let extras: Vec<&dyn GeneratedMessageObject> =
|
||||
self.extra_params.iter().map(|v| v as _).collect();
|
||||
let faces: Vec<&dyn GeneratedMessageObject> = self.faces.iter().map(|v| v as _).collect();
|
||||
let shape = map_osd([
|
||||
(
|
||||
"path",
|
||||
map_osd([
|
||||
("begin", OSD::Real(f64::from(self.path_begin))),
|
||||
("curve", OSD::Integer(self.path_curve)),
|
||||
("end", OSD::Real(f64::from(self.path_end))),
|
||||
("radius_offset", OSD::Real(f64::from(self.radius_offset))),
|
||||
("revolutions", OSD::Real(f64::from(self.revolutions))),
|
||||
("scale_x", OSD::Real(f64::from(self.scale_x))),
|
||||
("scale_y", OSD::Real(f64::from(self.scale_y))),
|
||||
("shear_x", OSD::Real(f64::from(self.shear_x))),
|
||||
("shear_y", OSD::Real(f64::from(self.shear_y))),
|
||||
("skew", OSD::Real(f64::from(self.skew))),
|
||||
("taper_x", OSD::Real(f64::from(self.taper_x))),
|
||||
("taper_y", OSD::Real(f64::from(self.taper_y))),
|
||||
("twist", OSD::Real(f64::from(self.twist))),
|
||||
("twist_begin", OSD::Real(f64::from(self.twist_begin))),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"profile",
|
||||
map_osd([
|
||||
("begin", OSD::Real(f64::from(self.profile_begin))),
|
||||
("curve", OSD::Integer(self.profile_curve)),
|
||||
("end", OSD::Real(f64::from(self.profile_end))),
|
||||
("hollow", OSD::Real(f64::from(self.profile_hollow))),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"sculpt",
|
||||
map_osd([
|
||||
("id", OSD::UUID(self.sculpt_id)),
|
||||
("type", OSD::Integer(self.sculpt_type as i32)),
|
||||
]),
|
||||
),
|
||||
]);
|
||||
finish_map([
|
||||
("group-id", OSD::UUID(self.group_id)),
|
||||
("material", OSD::Integer(self.material as i32)),
|
||||
("name", OSD::String(self.name.clone())),
|
||||
("pos", OSD::from_vector3(self.position)?),
|
||||
("rotation", OSD::from_quaternion(self.rotation)?),
|
||||
("scale", OSD::from_vector3(self.scale)?),
|
||||
("extra_parameters", encode(&extras)?),
|
||||
("facelist", encode(&faces)?),
|
||||
("shape", shape),
|
||||
])
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn deserialize_generated(&mut self, map: &OSDMap) -> Result<(), Error> {
|
||||
self.group_id = required(map, "group-id")?.as_uuid()?;
|
||||
self.material = material_from_i32(required(map, "material")?.as_integer()?)?;
|
||||
self.name = required(map, "name")?.as_string()?;
|
||||
self.position = required(map, "pos")?.as_vector3()?;
|
||||
self.rotation = required(map, "rotation")?.as_quaternion()?;
|
||||
self.scale = required(map, "scale")?.as_vector3()?;
|
||||
self.extra_params = decode_array(&required(map, "extra_parameters")?)?;
|
||||
self.faces = decode_array(&required(map, "facelist")?)?;
|
||||
let shape_value = required(map, "shape")?;
|
||||
let shape = as_map(&shape_value)?;
|
||||
let path = as_map(required_from(shape, "path")?)?;
|
||||
macro_rules! real {
|
||||
($field:ident, $key:literal) => {
|
||||
self.$field = required_from(path, $key)?.as_real()? as f32;
|
||||
};
|
||||
}
|
||||
real!(path_begin, "begin");
|
||||
self.path_curve = required_from(path, "curve")?.as_integer()?;
|
||||
real!(path_end, "end");
|
||||
real!(radius_offset, "radius_offset");
|
||||
real!(revolutions, "revolutions");
|
||||
real!(scale_x, "scale_x");
|
||||
real!(scale_y, "scale_y");
|
||||
real!(shear_x, "shear_x");
|
||||
real!(shear_y, "shear_y");
|
||||
real!(skew, "skew");
|
||||
real!(taper_x, "taper_x");
|
||||
real!(taper_y, "taper_y");
|
||||
real!(twist, "twist");
|
||||
real!(twist_begin, "twist_begin");
|
||||
let profile = as_map(required_from(shape, "profile")?)?;
|
||||
self.profile_begin = required_from(profile, "begin")?.as_real()? as f32;
|
||||
self.profile_curve = required_from(profile, "curve")?.as_integer()?;
|
||||
self.profile_end = required_from(profile, "end")?.as_real()? as f32;
|
||||
self.profile_hollow = required_from(profile, "hollow")?.as_real()? as f32;
|
||||
let sculpt = as_map(required_from(shape, "sculpt")?)?;
|
||||
self.sculpt_id = required_from(sculpt, "id")?.as_uuid()?;
|
||||
self.sculpt_type = sculpt_type_from_i32(required_from(sculpt, "type")?.as_integer()?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
trait GeneratedMessageObject {
|
||||
fn encode(&self) -> Result<OSD, Error>;
|
||||
}
|
||||
impl GeneratedMessageObject for UploadObjectAssetMessageObjectFace {
|
||||
fn encode(&self) -> Result<OSD, Error> {
|
||||
self.serialize_generated()?.copy()
|
||||
}
|
||||
}
|
||||
impl GeneratedMessageObject for UploadObjectAssetMessageObjectExtraParam {
|
||||
fn encode(&self) -> Result<OSD, Error> {
|
||||
self.serialize_generated()?.copy()
|
||||
}
|
||||
}
|
||||
|
||||
impl GeneratedMessage for UploadObjectAssetMessage {
|
||||
fn new_generated() -> Self {
|
||||
Self {
|
||||
objects: Vec::new(),
|
||||
}
|
||||
}
|
||||
fn serialize_generated(&self) -> Result<OSDMap, Error> {
|
||||
finish_map([("objects", encode_struct_array(&self.objects)?)])
|
||||
}
|
||||
fn deserialize_generated(&mut self, map: &OSDMap) -> Result<(), Error> {
|
||||
self.objects = decode_array(&required(map, "objects")?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! upload_message_value {
|
||||
($($type:ty),+ $(,)?) => {
|
||||
$(impl MessageValue for $type {
|
||||
fn message_default() -> Self { <Self as GeneratedMessage>::new_generated() }
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> { self.serialize_generated()?.copy() }
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else { return Err(Error::Argument); };
|
||||
let map = OSDMap::new_with_dictionary(values.clone())?;
|
||||
let mut output = <Self as GeneratedMessage>::new_generated();
|
||||
output.deserialize_generated(&map)?;
|
||||
Ok(output)
|
||||
}
|
||||
})+
|
||||
};
|
||||
}
|
||||
|
||||
upload_message_value!(
|
||||
UploadObjectAssetMessage,
|
||||
UploadObjectAssetMessageObject,
|
||||
UploadObjectAssetMessageObjectExtraParam,
|
||||
UploadObjectAssetMessageObjectFace,
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -878,6 +878,7 @@ pub(crate) fn from_dictionary_uri(dict: HashMap<Uri, Uri>) -> Result<OSDMap, Err
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::too_many_lines)] // The exhaustive decoder matrix stays in one test.
|
||||
mod tests {
|
||||
use super::*;
|
||||
use flate2::Compression;
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//! Protocol name/value parsing used by object and avatar updates.
|
||||
|
||||
use crate::{Error, NameValueClassType, NameValueSendtoType, NameValueValueType};
|
||||
use libremetaverse_types::compat::Object;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct NameValue {
|
||||
pub class_: NameValueClassType,
|
||||
pub name: String,
|
||||
pub sendto: NameValueSendtoType,
|
||||
pub type_: NameValueValueType,
|
||||
pub value: Option<Object>,
|
||||
}
|
||||
|
||||
impl NameValue {
|
||||
pub fn new_with_string(data: String) -> Result<Self, Error> {
|
||||
let mut fields = data.splitn(5, char::is_whitespace);
|
||||
let Some(name) = fields.next().filter(|value| !value.is_empty()) else {
|
||||
return Ok(Self::empty());
|
||||
};
|
||||
let type_name = fields.next().unwrap_or_default();
|
||||
let class_name = fields.next().unwrap_or_default();
|
||||
let sendto_name = fields.next().unwrap_or_default();
|
||||
let value_text = fields.next().unwrap_or_default().trim_start();
|
||||
|
||||
let type_ = value_type(type_name);
|
||||
let class_ = class_type(class_name);
|
||||
let sendto = sendto_type(sendto_name);
|
||||
let value = parse_value(type_, value_text);
|
||||
Ok(Self {
|
||||
class_,
|
||||
name: name.to_owned(),
|
||||
sendto,
|
||||
type_,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_string_value_type_class_type_sendto_type_object(
|
||||
name: String,
|
||||
value_type: NameValueValueType,
|
||||
class_type: NameValueClassType,
|
||||
sendto_type: NameValueSendtoType,
|
||||
value: Object,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
class_: class_type,
|
||||
name,
|
||||
sendto: sendto_type,
|
||||
type_: value_type,
|
||||
value: Some(value),
|
||||
})
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
Self {
|
||||
class_: NameValueClassType::Unknown,
|
||||
name: String::new(),
|
||||
sendto: NameValueSendtoType::Unknown,
|
||||
type_: NameValueValueType::Unknown,
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn equals_with_name_value(&self, other: Self) -> bool {
|
||||
self == &other
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn equals_with_object(&self, object: Option<Object>) -> bool {
|
||||
object
|
||||
.as_ref()
|
||||
.and_then(Object::downcast_ref::<Self>)
|
||||
.is_some_and(|other| self == other)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_hash_code(&self) -> i32 {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
self.hash(&mut hasher);
|
||||
hasher.finish() as i32
|
||||
}
|
||||
|
||||
pub fn name_values_to_string(values: Vec<Self>) -> Result<String, Error> {
|
||||
let mut rendered = Vec::new();
|
||||
for value in values {
|
||||
let Some(payload) = value.value.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
rendered.push(format!(
|
||||
"{} {} {} {} {}",
|
||||
value.name,
|
||||
type_string(value.type_).ok_or(Error::IndexOutOfRange)?,
|
||||
class_string(value.class_).ok_or(Error::IndexOutOfRange)?,
|
||||
sendto_string(value.sendto).ok_or(Error::IndexOutOfRange)?,
|
||||
object_string(payload)
|
||||
));
|
||||
}
|
||||
Ok(rendered.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
fn value_type(value: &str) -> NameValueValueType {
|
||||
match value {
|
||||
"STRING" => NameValueValueType::String,
|
||||
"F32" => NameValueValueType::F32,
|
||||
"S32" => NameValueValueType::S32,
|
||||
"VEC3" => NameValueValueType::VEC3,
|
||||
"U32" => NameValueValueType::U32,
|
||||
"ASSET" => NameValueValueType::Asset,
|
||||
"U64" => NameValueValueType::U64,
|
||||
_ => NameValueValueType::String,
|
||||
}
|
||||
}
|
||||
|
||||
fn class_type(value: &str) -> NameValueClassType {
|
||||
match value {
|
||||
"R" => NameValueClassType::ReadOnly,
|
||||
"RW" => NameValueClassType::ReadWrite,
|
||||
"CB" => NameValueClassType::Callback,
|
||||
_ => NameValueClassType::ReadOnly,
|
||||
}
|
||||
}
|
||||
|
||||
fn sendto_type(value: &str) -> NameValueSendtoType {
|
||||
match value {
|
||||
"S" => NameValueSendtoType::Sim,
|
||||
"DS" => NameValueSendtoType::DataSim,
|
||||
"SV" => NameValueSendtoType::SimViewer,
|
||||
"DSV" => NameValueSendtoType::DataSimViewer,
|
||||
_ => NameValueSendtoType::Sim,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_value(type_: NameValueValueType, value: &str) -> Option<Object> {
|
||||
Some(match type_ {
|
||||
NameValueValueType::String | NameValueValueType::Asset => Object::String(value.to_owned()),
|
||||
NameValueValueType::F32 => Object::Real(value.parse::<f32>().unwrap_or_default().into()),
|
||||
NameValueValueType::S32 => Object::Integer(value.parse().unwrap_or_default()),
|
||||
NameValueValueType::U32 => Object::UInteger(value.parse().unwrap_or_default()),
|
||||
NameValueValueType::U64 => Object::ULong(value.parse().unwrap_or_default()),
|
||||
NameValueValueType::VEC3 => Object::Vector3(
|
||||
libremetaverse_types::Vector3::parse(value.to_owned())
|
||||
.unwrap_or_else(|_| libremetaverse_types::Vector3::zero()),
|
||||
),
|
||||
NameValueValueType::Unknown | NameValueValueType::CAMERA => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn type_string(value: NameValueValueType) -> Option<&'static str> {
|
||||
match value {
|
||||
NameValueValueType::String => Some("STRING"),
|
||||
NameValueValueType::F32 => Some("F32"),
|
||||
NameValueValueType::S32 => Some("S32"),
|
||||
NameValueValueType::VEC3 => Some("VEC3"),
|
||||
NameValueValueType::U32 => Some("U32"),
|
||||
NameValueValueType::Asset => Some("ASSET"),
|
||||
NameValueValueType::U64 => Some("U64"),
|
||||
NameValueValueType::Unknown | NameValueValueType::CAMERA => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn class_string(value: NameValueClassType) -> Option<&'static str> {
|
||||
match value {
|
||||
NameValueClassType::ReadOnly => Some("R"),
|
||||
NameValueClassType::ReadWrite => Some("RW"),
|
||||
NameValueClassType::Callback => Some("CB"),
|
||||
NameValueClassType::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sendto_string(value: NameValueSendtoType) -> Option<&'static str> {
|
||||
match value {
|
||||
NameValueSendtoType::Sim => Some("S"),
|
||||
NameValueSendtoType::DataSim => Some("DS"),
|
||||
NameValueSendtoType::SimViewer => Some("SV"),
|
||||
NameValueSendtoType::DataSimViewer => Some("DSV"),
|
||||
NameValueSendtoType::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn object_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::Vector3(value) => value.to_string(),
|
||||
other => format!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_and_round_trips_protocol_name_values() {
|
||||
let value = NameValue::new_with_string("FirstName STRING R S Ada".into()).unwrap();
|
||||
assert_eq!(value.name, "FirstName");
|
||||
assert_eq!(value.type_, NameValueValueType::String);
|
||||
assert_eq!(value.value, Some(Object::String("Ada".into())));
|
||||
assert_eq!(
|
||||
NameValue::name_values_to_string(vec![value]).unwrap(),
|
||||
"FirstName STRING R S Ada"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
//! after the lock is released.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility map.
|
||||
#![allow(clippy::large_enum_variant)] // Commands move through a bounded channel without cloning.
|
||||
#![allow(clippy::needless_pass_by_value)] // Owned mapped parameters preserve the public API.
|
||||
#![allow(clippy::too_many_arguments)] // Login overloads preserve the mapped public API.
|
||||
#![allow(clippy::too_many_lines)] // Packet dispatch keeps the complete protocol switch together.
|
||||
@@ -51,6 +52,8 @@ impl Clone for Packet {
|
||||
has_variable_blocks: self.has_variable_blocks,
|
||||
header: self.header.clone(),
|
||||
type_: self.type_,
|
||||
native_body: self.native_body.clone(),
|
||||
native_payload_length: self.native_payload_length,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1055,6 +1058,7 @@ pub struct SimulatorData {
|
||||
pub terrain_start_height11: f32,
|
||||
pub water_height: f32,
|
||||
pub wind_speeds: RwLock<Option<Vec<Vector2>>>,
|
||||
prey_id: RwLock<UUID>,
|
||||
endpoint: std::net::SocketAddr,
|
||||
connected: AtomicBool,
|
||||
movement_complete: AtomicBool,
|
||||
@@ -1151,6 +1155,14 @@ impl fmt::Debug for Simulator {
|
||||
}
|
||||
|
||||
impl Simulator {
|
||||
pub(crate) fn native_prey_id(&self) -> UUID {
|
||||
*read(&self.data.prey_id)
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_prey_id(&self, value: UUID) {
|
||||
*write(&self.data.prey_id) = value;
|
||||
}
|
||||
|
||||
pub fn native_new(
|
||||
client: GridClient,
|
||||
address: std::net::SocketAddr,
|
||||
@@ -1221,6 +1233,7 @@ impl Simulator {
|
||||
terrain_start_height11: 0.0,
|
||||
water_height: 0.0,
|
||||
wind_speeds: RwLock::new(None),
|
||||
prey_id: RwLock::new(UUID::zero()),
|
||||
endpoint: address,
|
||||
connected: AtomicBool::new(false),
|
||||
movement_complete: AtomicBool::new(false),
|
||||
|
||||
@@ -972,6 +972,12 @@ impl ObjectManagerInner {
|
||||
};
|
||||
|
||||
if construction.p_code == PCode::Avatar {
|
||||
if let Some(client) = self.client.upgrade()
|
||||
&& block.full_id == client.network().native_agent_id()
|
||||
&& let Some(agent) = client.cached_agent_manager_inner()
|
||||
{
|
||||
agent.update_avatar_object_state(block.id, movement.collision_plane);
|
||||
}
|
||||
let is_new = !read(&simulator.objects_avatars).contains_key(&block.id);
|
||||
let avatar = read(&simulator.objects_avatars)
|
||||
.get(&block.id)
|
||||
@@ -1214,6 +1220,12 @@ impl ObjectManagerInner {
|
||||
velocity,
|
||||
};
|
||||
if avatar {
|
||||
if let Some(client) = self.client.upgrade()
|
||||
&& let Some(agent) = client.cached_agent_manager_inner()
|
||||
&& local_id == agent.avatar_local_id()
|
||||
{
|
||||
agent.update_avatar_object_state(local_id, collision_plane);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let Some(mut prim) = read(&simulator.objects_primitives).get(&local_id).cloned() else {
|
||||
|
||||
@@ -1711,9 +1711,6 @@ fn te_offset_float_slice(bytes: &[u8]) -> Result<f32, Error> {
|
||||
bytes.try_into().map_err(|_| Error::IndexOutOfRange)?,
|
||||
)) / 32767.0)
|
||||
}
|
||||
pub(crate) fn te_offset_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
te_offset_float_slice(checked_window(&bytes, pos, 2)?)
|
||||
}
|
||||
pub(crate) fn te_rotation_short(rotation: f32) -> i16 {
|
||||
let two_pi = f64::from(std::f32::consts::TAU);
|
||||
let rotation = f64::from(rotation);
|
||||
@@ -1726,20 +1723,9 @@ fn te_rotation_float_slice(bytes: &[u8]) -> Result<f32, Error> {
|
||||
)) / 32768.0
|
||||
* std::f32::consts::TAU)
|
||||
}
|
||||
pub(crate) fn te_rotation_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
te_rotation_float_slice(checked_window(&bytes, pos, 2)?)
|
||||
}
|
||||
pub(crate) fn te_glow_byte(glow: f32) -> u8 {
|
||||
(glow.clamp(0.0, 1.0) * 255.0) as u8
|
||||
}
|
||||
pub(crate) fn te_glow_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
Ok(f32::from(
|
||||
*checked_window(&bytes, pos, 1)?
|
||||
.first()
|
||||
.ok_or(Error::IndexOutOfRange)?,
|
||||
) / 255.0)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PrimitiveParticleSystem {
|
||||
pub angular_velocity: Vector3,
|
||||
@@ -3193,6 +3179,7 @@ const fn attachment_point_from_u8(value: u8) -> AttachmentPoint {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::float_cmp)] // Texture fixtures use exactly representable values.
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
//! Thread-safe observable key/value storage.
|
||||
|
||||
use crate::{DictionaryEventAction, Error};
|
||||
use libremetaverse_types::compat::{DictionaryEntry, IEnumerator, Object};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
type Callback = Arc<dyn Fn(DictionaryEventAction, DictionaryEntry) + Send + Sync>;
|
||||
|
||||
pub struct ObservableDictionary<TKey, TValue> {
|
||||
values: Mutex<HashMap<TKey, Option<TValue>>>,
|
||||
delegates: Mutex<HashMap<DictionaryEventAction, Vec<Callback>>>,
|
||||
}
|
||||
|
||||
impl<TKey, TValue> ObservableDictionary<TKey, TValue>
|
||||
where
|
||||
TKey: Clone + Eq + Hash + Into<Object>,
|
||||
TValue: Clone + PartialEq + Into<Object>,
|
||||
{
|
||||
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||
Self::new_with_capacity(0)
|
||||
}
|
||||
|
||||
pub fn new_with_int32(capacity: i32) -> Result<Self, Error> {
|
||||
let capacity = usize::try_from(capacity).map_err(|_| Error::Argument)?;
|
||||
Self::new_with_capacity(capacity)
|
||||
}
|
||||
|
||||
fn new_with_capacity(capacity: usize) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
values: Mutex::new(HashMap::with_capacity(capacity)),
|
||||
delegates: Mutex::new(HashMap::new()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add(&self, key: TKey, value: Option<TValue>) -> Result<(), Error> {
|
||||
self.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.insert(key.clone(), value.clone());
|
||||
self.fire(DictionaryEventAction::Add, key, value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_delegate(
|
||||
&self,
|
||||
action: DictionaryEventAction,
|
||||
callback: Callback,
|
||||
) -> Result<(), Error> {
|
||||
self.delegates
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.entry(action)
|
||||
.or_default()
|
||||
.push(callback);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_delegate(
|
||||
&self,
|
||||
action: DictionaryEventAction,
|
||||
callback: Callback,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(callbacks) = self
|
||||
.delegates
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get_mut(&action)
|
||||
{
|
||||
callbacks.retain(|registered| !Arc::ptr_eq(registered, &callback));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear(&self) -> Result<(), Error> {
|
||||
let removed: Vec<_> = self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.drain()
|
||||
.collect();
|
||||
for (key, value) in removed {
|
||||
self.fire(DictionaryEventAction::Remove, key, value);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, key: TKey) -> Result<bool, Error> {
|
||||
Ok(self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.contains_key(&key))
|
||||
}
|
||||
|
||||
pub fn contains_value(&self, value: Option<TValue>) -> Result<bool, Error> {
|
||||
Ok(self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.values()
|
||||
.any(|candidate| candidate == &value))
|
||||
}
|
||||
|
||||
pub fn find(
|
||||
&self,
|
||||
predicate: Box<dyn Fn(&Option<TValue>) -> bool + Send + Sync>,
|
||||
) -> Result<Option<TValue>, Error> {
|
||||
Ok(self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.values()
|
||||
.find(|value| predicate(value))
|
||||
.cloned()
|
||||
.flatten())
|
||||
}
|
||||
|
||||
pub fn find_all_with_predicate(
|
||||
&self,
|
||||
predicate: Box<dyn Fn(&TKey) -> bool + Send + Sync>,
|
||||
) -> Result<Vec<TKey>, Error> {
|
||||
Ok(self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.keys()
|
||||
.filter(|key| predicate(key))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn find_all_with_predicate_6d98ccd2(
|
||||
&self,
|
||||
predicate: Box<dyn Fn(&Option<TValue>) -> bool + Send + Sync>,
|
||||
) -> Result<Vec<Option<TValue>>, Error> {
|
||||
Ok(self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.values()
|
||||
.filter(|value| predicate(value))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn get_enumerator(&self) -> Result<IEnumerator, Error> {
|
||||
Ok(IEnumerator)
|
||||
}
|
||||
|
||||
pub fn remove(&self, key: TKey) -> Result<bool, Error> {
|
||||
let removed = self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.remove(&key);
|
||||
if let Some(value) = removed {
|
||||
self.fire(DictionaryEventAction::Remove, key, value);
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn try_get_value(&self, key: TKey, value: &mut Option<TValue>) -> bool {
|
||||
let found = self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(&key)
|
||||
.cloned();
|
||||
match found {
|
||||
Some(found) => {
|
||||
*value = found;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn count(&self) -> i32 {
|
||||
i32::try_from(
|
||||
self.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.len(),
|
||||
)
|
||||
.unwrap_or(i32::MAX)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn item(&self, key: Option<TKey>) -> Option<TValue> {
|
||||
let key = key?;
|
||||
self.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(&key)
|
||||
.cloned()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn set_item(&mut self, key: Option<TKey>, value: Option<TValue>) {
|
||||
let Some(key) = key else { return };
|
||||
let action = if self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.insert(key.clone(), value.clone())
|
||||
.is_some()
|
||||
{
|
||||
DictionaryEventAction::Change
|
||||
} else {
|
||||
DictionaryEventAction::Add
|
||||
};
|
||||
self.fire(action, key, value);
|
||||
}
|
||||
|
||||
fn fire(&self, action: DictionaryEventAction, key: TKey, value: Option<TValue>) {
|
||||
let callbacks = self
|
||||
.delegates
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(&action)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let entry = DictionaryEntry(key.into(), value.map_or(Object::Undefined, Into::into));
|
||||
for callback in callbacks {
|
||||
callback(action, DictionaryEntry(entry.0.clone(), entry.1.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
//! Runtime representation for the catalogued abstract packet compatibility type.
|
||||
|
||||
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
||||
use libremetaverse_types::Error;
|
||||
|
||||
use crate::interfaces::IByteBufferPool;
|
||||
use crate::packet_catalog::{self, BlockRepetition, PacketType};
|
||||
use crate::packets::{Header, Packet};
|
||||
|
||||
fn body(packet: &Packet) -> Result<&OSDMap, Error> {
|
||||
packet.native_body.as_ref().ok_or(Error::InvalidOperation)
|
||||
}
|
||||
|
||||
fn assign_body(packet: &mut Packet, body: OSDMap) -> Result<(), Error> {
|
||||
let descriptor =
|
||||
packet_catalog::descriptor_by_type(packet.type_).ok_or(Error::InvalidOperation)?;
|
||||
let validated = packet_catalog::build_packet_from_osd(descriptor.name, &body)?
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
packet.native_payload_length = validated.native_payload_length;
|
||||
packet.native_body = Some(body);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn packet_to_osd(packet: &Packet) -> Result<OSDMap, Error> {
|
||||
Ok(body(packet)?.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn packet_from_osd(packet: &mut Packet, osd: OSDMap) -> Result<(), Error> {
|
||||
assign_body(packet, osd)
|
||||
}
|
||||
|
||||
pub(crate) fn get_llsd(packet: &Packet) -> Result<OSD, Error> {
|
||||
let descriptor =
|
||||
packet_catalog::descriptor_by_type(packet.type_).ok_or(Error::InvalidOperation)?;
|
||||
let mut wrapper = std::collections::HashMap::with_capacity(1);
|
||||
wrapper.insert(
|
||||
descriptor.name.to_owned(),
|
||||
OSD::Map(body(packet)?.snapshot()),
|
||||
);
|
||||
Ok(OSD::Map(wrapper))
|
||||
}
|
||||
|
||||
pub(crate) fn from_llsd(osd: OSD) -> Result<Option<Packet>, Error> {
|
||||
let OSD::Map(wrapper) = osd else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut entries = wrapper.into_iter().collect::<Vec<_>>();
|
||||
entries.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
for (name, value) in entries {
|
||||
if let OSD::Map(values) = value {
|
||||
let body = OSDMap::new_with_dictionary(values)?;
|
||||
return packet_catalog::build_packet_from_osd(&name, &body);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) fn from_binary(binary: Vec<u8>) -> Result<Option<Packet>, Error> {
|
||||
from_llsd(OSDParser::deserialize_llsd_binary_with_bytes(binary)?)
|
||||
}
|
||||
|
||||
pub(crate) fn from_xml_string(xml: String) -> Result<Option<Packet>, Error> {
|
||||
from_llsd(OSDParser::deserialize_llsd_xml_with_string(xml)?)
|
||||
}
|
||||
|
||||
pub(crate) fn to_binary(packet: &Packet) -> Result<Vec<u8>, Error> {
|
||||
OSDParser::serialize_llsd_binary_with_osd(get_llsd(packet)?)
|
||||
}
|
||||
|
||||
pub(crate) fn to_xml_string(packet: &Packet) -> Result<String, Error> {
|
||||
OSDParser::serialize_llsd_xml_string(get_llsd(packet)?)
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes(packet: &Packet) -> Result<Vec<u8>, Error> {
|
||||
packet_catalog::encode_packet_body(
|
||||
packet.type_,
|
||||
crate::packet_wire::clone_header(&packet.header),
|
||||
body(packet)?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes_multiple(packet: &Packet) -> Result<Vec<Vec<u8>>, Error> {
|
||||
packet_catalog::encode_packet_body_multiple(
|
||||
packet.type_,
|
||||
crate::packet_wire::clone_header(&packet.header),
|
||||
body(packet)?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes_pooled(
|
||||
packet: &Packet,
|
||||
pool: &dyn IByteBufferPool,
|
||||
size: &mut i32,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let encoded = to_bytes(packet)?;
|
||||
*size = i32::try_from(encoded.len()).map_err(|_| Error::Argument)?;
|
||||
copy_to_pool(pool, &encoded)
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes_multiple_pooled(
|
||||
packet: &Packet,
|
||||
pool: &dyn IByteBufferPool,
|
||||
sizes: &mut Vec<i32>,
|
||||
) -> Result<Vec<Vec<u8>>, Error> {
|
||||
let encoded = to_bytes_multiple(packet)?;
|
||||
sizes.clear();
|
||||
sizes
|
||||
.try_reserve_exact(encoded.len())
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
let mut output = Vec::new();
|
||||
output
|
||||
.try_reserve_exact(encoded.len())
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
for bytes in encoded {
|
||||
sizes.push(i32::try_from(bytes.len()).map_err(|_| Error::Argument)?);
|
||||
output.push(copy_to_pool(pool, &bytes)?);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn copy_to_pool(pool: &dyn IByteBufferPool, encoded: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let minimum = i32::try_from(encoded.len()).map_err(|_| Error::Argument)?;
|
||||
let mut buffer = pool.lease_bytes(minimum)?;
|
||||
if buffer.len() < encoded.len() {
|
||||
buffer
|
||||
.try_reserve_exact(encoded.len() - buffer.len())
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
buffer.resize(encoded.len(), 0);
|
||||
}
|
||||
buffer[..encoded.len()].copy_from_slice(encoded);
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
pub(crate) fn from_bytes_with_header(
|
||||
packet: &mut Packet,
|
||||
header: Header,
|
||||
bytes: &[u8],
|
||||
position: &mut i32,
|
||||
packet_end: &mut i32,
|
||||
) -> Result<(), Error> {
|
||||
let packet_type = packet_catalog::packet_type(header.id, header.frequency);
|
||||
let native_body = packet_catalog::decode_packet_body(
|
||||
packet_type,
|
||||
crate::packet_wire::clone_header(&header),
|
||||
bytes,
|
||||
position,
|
||||
packet_end,
|
||||
)?;
|
||||
assign_decoded(packet, header, packet_type, native_body)
|
||||
}
|
||||
|
||||
pub(crate) fn from_bytes(
|
||||
packet: &mut Packet,
|
||||
bytes: &[u8],
|
||||
position: &mut i32,
|
||||
packet_end: &mut i32,
|
||||
zero_buffer: Option<&mut [u8]>,
|
||||
) -> Result<(), Error> {
|
||||
let header = crate::packet_wire::decode_header(bytes, position, packet_end)?;
|
||||
let packet_type = packet_catalog::packet_type(header.id, header.frequency);
|
||||
let native_body = if header.zerocoded {
|
||||
let buffer = zero_buffer.ok_or(Error::ArgumentNull)?;
|
||||
let decoded_length = crate::packet_wire::zero_decode(
|
||||
Some(bytes),
|
||||
packet_end.checked_add(1).ok_or(Error::Argument)?,
|
||||
Some(buffer),
|
||||
)?;
|
||||
*packet_end = decoded_length.checked_sub(1).ok_or(Error::Argument)?;
|
||||
let end = usize::try_from(decoded_length).map_err(|_| Error::Argument)?;
|
||||
packet_catalog::decode_packet_body(
|
||||
packet_type,
|
||||
crate::packet_wire::clone_header(&header),
|
||||
&buffer[..end],
|
||||
position,
|
||||
packet_end,
|
||||
)?
|
||||
} else {
|
||||
packet_catalog::decode_packet_body(
|
||||
packet_type,
|
||||
crate::packet_wire::clone_header(&header),
|
||||
bytes,
|
||||
position,
|
||||
packet_end,
|
||||
)?
|
||||
};
|
||||
assign_decoded(packet, header, packet_type, native_body)
|
||||
}
|
||||
|
||||
fn assign_decoded(
|
||||
packet: &mut Packet,
|
||||
header: Header,
|
||||
packet_type: PacketType,
|
||||
native_body: OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
let descriptor =
|
||||
packet_catalog::descriptor_by_type(packet_type).ok_or(Error::InvalidOperation)?;
|
||||
packet.has_variable_blocks = descriptor
|
||||
.blocks
|
||||
.iter()
|
||||
.any(|block| matches!(block.repetition, BlockRepetition::Variable));
|
||||
packet.header = header;
|
||||
packet.type_ = packet_type;
|
||||
packet.native_payload_length =
|
||||
packet_catalog::packet_payload_length(packet_type, &native_body)?;
|
||||
packet.native_body = Some(native_body);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn length(packet: &Packet) -> i32 {
|
||||
let header = i32::try_from(crate::packet_wire::header_length(packet.header.frequency))
|
||||
.unwrap_or(i32::MAX);
|
||||
let acks = packet.header.ack_list.as_ref().map_or(0_i32, |acks| {
|
||||
if acks.is_empty() {
|
||||
0
|
||||
} else {
|
||||
i32::try_from(acks.len())
|
||||
.unwrap_or(i32::MAX)
|
||||
.saturating_mul(4)
|
||||
.saturating_add(1)
|
||||
}
|
||||
});
|
||||
header
|
||||
.saturating_add(packet.native_payload_length)
|
||||
.saturating_add(acks)
|
||||
}
|
||||
|
||||
pub(crate) fn block_from_bytes(
|
||||
data: &mut Vec<u8>,
|
||||
bytes: &[u8],
|
||||
position: &mut i32,
|
||||
) -> Result<(), Error> {
|
||||
let start = usize::try_from(*position).map_err(|_| Error::Argument)?;
|
||||
let source = bytes.get(start..).ok_or(Error::IndexOutOfRange)?;
|
||||
data.clear();
|
||||
data.try_reserve_exact(source.len())
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
data.extend_from_slice(source);
|
||||
*position = i32::try_from(bytes.len()).map_err(|_| Error::Argument)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn block_to_bytes(
|
||||
data: &[u8],
|
||||
bytes: &mut [u8],
|
||||
position: &mut i32,
|
||||
) -> Result<(), Error> {
|
||||
let start = usize::try_from(*position).map_err(|_| Error::Argument)?;
|
||||
let end = start.checked_add(data.len()).ok_or(Error::Argument)?;
|
||||
let destination = bytes.get_mut(start..end).ok_or(Error::IndexOutOfRange)?;
|
||||
destination.copy_from_slice(data);
|
||||
*position = i32::try_from(end).map_err(|_| Error::Argument)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn llsd_packet_envelope_round_trips() {
|
||||
let packet = packet_catalog::build_packet(PacketType::CloseCircuit).unwrap();
|
||||
let xml = to_xml_string(&packet).unwrap();
|
||||
let decoded = from_xml_string(xml).unwrap().unwrap();
|
||||
assert_eq!(decoded.type_, PacketType::CloseCircuit);
|
||||
assert_eq!(decoded.packet_to_osd().unwrap().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base_packet_wire_uses_public_header_state() {
|
||||
let mut packet = packet_catalog::build_packet(PacketType::CloseCircuit).unwrap();
|
||||
packet.header.sequence = 0x0102_0304;
|
||||
packet.header.reliable = false;
|
||||
let bytes = to_bytes(&packet).unwrap();
|
||||
assert_eq!(&bytes[1..5], &[1, 2, 3, 4]);
|
||||
assert_eq!(bytes[0] & crate::Helpers::MSG_RELIABLE, 0);
|
||||
assert_eq!(length(&packet), i32::try_from(bytes.len()).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abstract_block_compatibility_storage_checks_bounds() {
|
||||
let mut data = Vec::new();
|
||||
let mut position = 1;
|
||||
block_from_bytes(&mut data, &[9, 8, 7], &mut position).unwrap();
|
||||
assert_eq!(data, [8, 7]);
|
||||
let mut output = [0_u8; 3];
|
||||
position = 1;
|
||||
block_to_bytes(&data, &mut output, &mut position).unwrap();
|
||||
assert_eq!(output, [0, 8, 7]);
|
||||
assert_eq!(position, 3);
|
||||
}
|
||||
}
|
||||
+16009
-498
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,167 @@
|
||||
//! Deterministic, reflection-free packet and compatibility-object diagnostics.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use libremetaverse_structured_data::OSD;
|
||||
use libremetaverse_types::Error;
|
||||
use libremetaverse_types::compat::Object;
|
||||
|
||||
use crate::packets::{Header, Packet};
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn interpret_options(header: &Header) -> String {
|
||||
format!(
|
||||
"[{} {} {} {}]",
|
||||
if header.appended_acks { "Ack" } else { " " },
|
||||
if header.resent { "Res" } else { " " },
|
||||
if header.reliable { "Rel" } else { " " },
|
||||
if header.zerocoded { "Zer" } else { " " },
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn packet_to_string(packet: &Packet) -> Result<String, Error> {
|
||||
let descriptor =
|
||||
crate::packet_catalog::descriptor_by_type(packet.type_).ok_or(Error::InvalidOperation)?;
|
||||
let mut result = String::new();
|
||||
writeln!(
|
||||
result,
|
||||
"Packet Type: {0} http://lib.openmetaverse.co/wiki/{0} http://wiki.secondlife.com/wiki/{0}",
|
||||
descriptor.name,
|
||||
)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
writeln!(result, "[Packet Header]").map_err(|_| Error::InvalidOperation)?;
|
||||
writeln!(result, "Sequence: {}", packet.header.sequence)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
writeln!(result, " Options: {}", interpret_options(&packet.header))
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
writeln!(result, "\n[Packet Payload]").map_err(|_| Error::InvalidOperation)?;
|
||||
let body = packet.packet_to_osd()?;
|
||||
format_osd_map(&body.snapshot(), 0, &mut result)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(crate) fn message_to_string(message: &Object, recurse_level: i32) -> Result<String, Error> {
|
||||
if recurse_level < 0 || recurse_level > 64 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut result = String::new();
|
||||
format_object(
|
||||
message,
|
||||
usize::try_from(recurse_level).map_err(|_| Error::Argument)?,
|
||||
&mut result,
|
||||
)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn format_object(value: &Object, depth: usize, output: &mut String) -> Result<(), Error> {
|
||||
let indent = " ".repeat(depth);
|
||||
match value {
|
||||
Object::Undefined => output.push_str(&format!("{indent}undefined\n")),
|
||||
Object::Boolean(value) => output.push_str(&format!("{indent}{value} [Boolean]\n")),
|
||||
Object::Integer(value) => output.push_str(&format!("{indent}{value} [Int32]\n")),
|
||||
Object::UInteger(value) => output.push_str(&format!("{indent}{value} [UInt32]\n")),
|
||||
Object::Long(value) => output.push_str(&format!("{indent}{value} [Int64]\n")),
|
||||
Object::ULong(value) => output.push_str(&format!("{indent}{value} [UInt64]\n")),
|
||||
Object::Real(value) => output.push_str(&format!("{indent}{value} [Double]\n")),
|
||||
Object::String(value) => output.push_str(&format!("{indent}{value} [String]\n")),
|
||||
Object::Bytes(value) => {
|
||||
output.push_str(&format!("{indent}[Byte[{0}]]\n", value.len()));
|
||||
format_hex(value, depth + 1, output)?;
|
||||
}
|
||||
Object::Array(values) => {
|
||||
output.push_str(&format!("{indent}[Array {0}]\n", values.len()));
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
output.push_str(&format!("{indent} [{index}]\n"));
|
||||
format_object(value, depth + 2, output)?;
|
||||
}
|
||||
}
|
||||
Object::Map(values) => {
|
||||
output.push_str(&format!("{indent}[Map {0}]\n", values.len()));
|
||||
let mut entries = values.iter().collect::<Vec<_>>();
|
||||
entries.sort_by(|left, right| left.0.cmp(right.0));
|
||||
for (key, value) in entries {
|
||||
output.push_str(&format!("{indent} {key}:\n"));
|
||||
format_object(value, depth + 2, output)?;
|
||||
}
|
||||
}
|
||||
other => output.push_str(&format!("{indent}{other:?}\n")),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_osd_map(
|
||||
values: &std::collections::HashMap<String, OSD>,
|
||||
depth: usize,
|
||||
output: &mut String,
|
||||
) -> Result<(), Error> {
|
||||
let mut entries = values.iter().collect::<Vec<_>>();
|
||||
entries.sort_by(|left, right| left.0.cmp(right.0));
|
||||
for (key, value) in entries {
|
||||
writeln!(output, "{}{key}:", " ".repeat(depth)).map_err(|_| Error::InvalidOperation)?;
|
||||
format_osd(value, depth + 1, output)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_osd(value: &OSD, depth: usize, output: &mut String) -> Result<(), Error> {
|
||||
let indent = " ".repeat(depth);
|
||||
match value {
|
||||
OSD::Map(values) => format_osd_map(values, depth, output),
|
||||
OSD::Array(values) => {
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
writeln!(output, "{indent}[{index}]").map_err(|_| Error::InvalidOperation)?;
|
||||
format_osd(value, depth + 1, output)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
OSD::Binary(bytes) => format_hex(bytes, depth, output),
|
||||
other => writeln!(output, "{indent}{other:?}").map_err(|_| Error::InvalidOperation),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_hex(bytes: &[u8], depth: usize, output: &mut String) -> Result<(), Error> {
|
||||
let indent = " ".repeat(depth);
|
||||
for chunk in bytes.chunks(16) {
|
||||
write!(output, "{indent}").map_err(|_| Error::InvalidOperation)?;
|
||||
for byte in chunk {
|
||||
write!(output, "{byte:02X} ").map_err(|_| Error::InvalidOperation)?;
|
||||
}
|
||||
output.push('\n');
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn options_match_reference_layout() {
|
||||
let mut header = crate::packet_catalog::new_header(crate::PacketFrequency::High, 1, false);
|
||||
header.reliable = true;
|
||||
header.resent = true;
|
||||
assert_eq!(interpret_options(&header), "[ Res Rel ]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packet_dump_contains_header_and_sorted_payload() {
|
||||
let packet =
|
||||
crate::packet_catalog::build_packet(crate::packet_catalog::PacketType::CloseCircuit)
|
||||
.unwrap();
|
||||
let dump = packet_to_string(&packet).unwrap();
|
||||
assert!(dump.contains("Packet Type: CloseCircuit"));
|
||||
assert!(dump.contains("[Packet Header]"));
|
||||
assert!(dump.contains("[Packet Payload]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_dump_formats_nested_values_and_bytes() {
|
||||
let value = Object::Map(std::collections::HashMap::from([
|
||||
("z".to_owned(), Object::Bytes(vec![0xab, 0xcd])),
|
||||
("a".to_owned(), Object::Integer(7)),
|
||||
]));
|
||||
let text = message_to_string(&value, 0).unwrap();
|
||||
assert!(text.find("a:").unwrap() < text.find("z:").unwrap());
|
||||
assert!(text.contains("AB CD"));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Bounded primitives shared by the generated UDP packet codecs.
|
||||
|
||||
#![allow(clippy::needless_pass_by_value)] // Header values are small mapped wire records.
|
||||
|
||||
use crate::packets::{Header, Packet};
|
||||
use crate::{Error, Helpers, PacketFrequency};
|
||||
use libremetaverse_types::{Quaternion, UUID, Vector3, Vector3d, Vector4};
|
||||
@@ -377,6 +379,43 @@ pub(crate) fn encode_base_packet(packet: &Packet) -> Result<Vec<u8>, Error> {
|
||||
Ok(writer.into_inner())
|
||||
}
|
||||
|
||||
pub(crate) fn reheader_packet(bytes: &[u8], header: Header) -> Result<Vec<u8>, Error> {
|
||||
let mut position = 0_i32;
|
||||
let mut packet_end = i32::try_from(bytes.len())
|
||||
.map_err(|_| Error::Argument)?
|
||||
.checked_sub(1)
|
||||
.ok_or(Error::Argument)?;
|
||||
let _old_header = decode_header(bytes, &mut position, &mut packet_end)?;
|
||||
let payload_start = usize::try_from(position).map_err(|_| Error::Argument)?;
|
||||
let payload_end = usize::try_from(packet_end.checked_add(1).ok_or(Error::Argument)?)
|
||||
.map_err(|_| Error::Argument)?;
|
||||
let payload = bytes
|
||||
.get(payload_start..payload_end)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
let capacity = header_length(header.frequency)
|
||||
.checked_add(payload.len())
|
||||
.and_then(|length| length.checked_add(ack_length(&header).ok()?))
|
||||
.ok_or(Error::Argument)?;
|
||||
let mut writer = WireWriter::with_capacity(capacity)?;
|
||||
encode_header(&header, &mut writer)?;
|
||||
writer.write_bytes(payload)?;
|
||||
encode_acks(&header, &mut writer)?;
|
||||
Ok(writer.into_inner())
|
||||
}
|
||||
|
||||
pub(crate) fn reheader_packets(packets: &[Vec<u8>], header: Header) -> Result<Vec<Vec<u8>>, Error> {
|
||||
let mut output = Vec::with_capacity(packets.len());
|
||||
for (index, packet) in packets.iter().enumerate() {
|
||||
let mut packet_header = clone_header(&header);
|
||||
if index != 0 {
|
||||
packet_header.appended_acks = false;
|
||||
packet_header.ack_list = None;
|
||||
}
|
||||
output.push(reheader_packet(packet, packet_header)?);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) fn decode_header(
|
||||
bytes: &[u8],
|
||||
position: &mut i32,
|
||||
@@ -650,7 +689,7 @@ pub(crate) fn build_packet_from_bytes(
|
||||
let packet_type = crate::packet_catalog::packet_type(header.id, header.frequency);
|
||||
let descriptor = crate::packet_catalog::descriptor_by_type(packet_type)
|
||||
.ok_or_else(|| parse(usize::try_from(position).unwrap_or(0), "unknown packet ID"))?;
|
||||
if header.zerocoded {
|
||||
let native_body = if header.zerocoded {
|
||||
let decoded_length = zero_decode(
|
||||
Some(packet_buffer),
|
||||
packet_end.checked_add(1).ok_or(Error::Argument)?,
|
||||
@@ -658,22 +697,22 @@ pub(crate) fn build_packet_from_bytes(
|
||||
)?;
|
||||
*packet_end = decoded_length.checked_sub(1).ok_or(Error::Argument)?;
|
||||
let decoded_length = usize::try_from(decoded_length).map_err(|_| Error::Argument)?;
|
||||
crate::packet_catalog::validate_packet_payload(
|
||||
crate::packet_catalog::decode_packet_body(
|
||||
packet_type,
|
||||
clone_header(&header),
|
||||
&zero_buffer[..decoded_length],
|
||||
&mut position,
|
||||
packet_end,
|
||||
)?;
|
||||
)?
|
||||
} else {
|
||||
crate::packet_catalog::validate_packet_payload(
|
||||
crate::packet_catalog::decode_packet_body(
|
||||
packet_type,
|
||||
clone_header(&header),
|
||||
packet_buffer,
|
||||
&mut position,
|
||||
packet_end,
|
||||
)?;
|
||||
}
|
||||
)?
|
||||
};
|
||||
Ok(Packet {
|
||||
has_variable_blocks: descriptor.blocks.iter().any(|block| {
|
||||
matches!(
|
||||
@@ -683,6 +722,11 @@ pub(crate) fn build_packet_from_bytes(
|
||||
}),
|
||||
header,
|
||||
type_: packet_type,
|
||||
native_payload_length: crate::packet_catalog::packet_payload_length(
|
||||
packet_type,
|
||||
&native_body,
|
||||
)?,
|
||||
native_body: Some(native_body),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
use std::f32::consts::PI;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use libremetaverse_types::{Color4, Quaternion, Vector3};
|
||||
|
||||
use crate::{
|
||||
Error, LiveParticle, PrimitiveParticleSystem,
|
||||
PrimitiveParticleSystemParticleDataFlags as DataFlags,
|
||||
PrimitiveParticleSystemParticleFlags as ParticleFlags,
|
||||
PrimitiveParticleSystemSourcePattern as SourcePattern,
|
||||
};
|
||||
|
||||
const MAX_PARTICLES: usize = 4096;
|
||||
|
||||
struct State {
|
||||
source_position: Vector3,
|
||||
source_rotation: Quaternion,
|
||||
target_position: Vector3,
|
||||
wind: Vector3,
|
||||
system_age: f32,
|
||||
burst_timer: f32,
|
||||
active: bool,
|
||||
particles: Vec<LiveParticle>,
|
||||
random: u64,
|
||||
}
|
||||
|
||||
pub struct ParticleSimulator {
|
||||
system: PrimitiveParticleSystem,
|
||||
state: Mutex<State>,
|
||||
}
|
||||
|
||||
impl ParticleSimulator {
|
||||
pub fn new(system: PrimitiveParticleSystem, seed: Option<i32>) -> Result<Self, Error> {
|
||||
let random = match seed.unwrap_or_default() {
|
||||
0 => {
|
||||
let elapsed = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
elapsed.as_secs() ^ u64::from(elapsed.subsec_nanos())
|
||||
}
|
||||
seed => seed as i64 as u64,
|
||||
}
|
||||
.max(1);
|
||||
Ok(Self {
|
||||
system,
|
||||
state: Mutex::new(State {
|
||||
source_position: Vector3::zero(),
|
||||
source_rotation: Quaternion::identity(),
|
||||
target_position: Vector3::zero(),
|
||||
wind: Vector3::zero(),
|
||||
system_age: 0.0,
|
||||
burst_timer: 0.0,
|
||||
active: true,
|
||||
particles: Vec::with_capacity(256),
|
||||
random,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_particles(&self) -> Result<Vec<LiveParticle>, Error> {
|
||||
Ok(self.lock().particles.clone())
|
||||
}
|
||||
|
||||
pub fn tick(&self, dt: f32) -> Result<(), Error> {
|
||||
if !dt.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if dt <= 0.0 {
|
||||
return Ok(());
|
||||
}
|
||||
let mut state = self.lock();
|
||||
state.system_age += dt;
|
||||
if self.system.max_age > 0.0 && state.system_age > self.system.max_age {
|
||||
state.active = false;
|
||||
}
|
||||
|
||||
let flags = self.system.part_data_flags.0;
|
||||
let wind_enabled = flags & DataFlags::WIND.0 != 0;
|
||||
let bounce = flags & DataFlags::BOUNCE.0 != 0;
|
||||
let target_pos = flags & DataFlags::TARGET_POS.0 != 0;
|
||||
let target_linear = flags & DataFlags::TARGET_LINEAR.0 != 0;
|
||||
let interp_color = flags & DataFlags::INTERP_COLOR.0 != 0;
|
||||
let interp_scale = flags & DataFlags::INTERP_SCALE.0 != 0;
|
||||
let wind = state.wind;
|
||||
let source = state.source_position;
|
||||
let target = state.target_position;
|
||||
|
||||
let mut index = state.particles.len();
|
||||
while index > 0 {
|
||||
index -= 1;
|
||||
let particle = &mut state.particles[index];
|
||||
particle.age += dt;
|
||||
if particle.age >= self.system.part_max_age {
|
||||
state.particles.remove(index);
|
||||
continue;
|
||||
}
|
||||
particle.velocity.x += self.system.part_acceleration.x * dt;
|
||||
particle.velocity.y += self.system.part_acceleration.y * dt;
|
||||
particle.velocity.z += self.system.part_acceleration.z * dt;
|
||||
if wind_enabled {
|
||||
particle.velocity.x += (wind.x - particle.velocity.x) * dt * 2.0;
|
||||
particle.velocity.y += (wind.y - particle.velocity.y) * dt * 2.0;
|
||||
particle.velocity.z += (wind.z - particle.velocity.z) * dt * 2.0;
|
||||
}
|
||||
particle.position.x += particle.velocity.x * dt;
|
||||
particle.position.y += particle.velocity.y * dt;
|
||||
particle.position.z += particle.velocity.z * dt;
|
||||
if bounce && particle.position.z < 0.0 {
|
||||
particle.position.z = 0.0;
|
||||
particle.velocity.z *= -0.5;
|
||||
}
|
||||
if target_pos || target_linear {
|
||||
let direction = normalize(Vector3 {
|
||||
x: target.x - source.x - particle.position.x,
|
||||
y: target.y - source.y - particle.position.y,
|
||||
z: target.z - source.z - particle.position.z,
|
||||
});
|
||||
if target_linear {
|
||||
particle.velocity = scale(
|
||||
direction,
|
||||
(self.system.burst_speed_min + self.system.burst_speed_max) * 0.5,
|
||||
);
|
||||
} else {
|
||||
particle.velocity.x += direction.x * dt * 2.0;
|
||||
particle.velocity.y += direction.y * dt * 2.0;
|
||||
particle.velocity.z += direction.z * dt * 2.0;
|
||||
}
|
||||
}
|
||||
let age = if self.system.part_max_age > 0.0 {
|
||||
(particle.age / self.system.part_max_age).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
particle.color = if interp_color {
|
||||
Color4::lerp(
|
||||
self.system.part_start_color,
|
||||
self.system.part_end_color,
|
||||
age,
|
||||
)?
|
||||
} else {
|
||||
self.system.part_start_color
|
||||
};
|
||||
particle.scale_x = if interp_scale {
|
||||
lerp(
|
||||
self.system.part_start_scale_x,
|
||||
self.system.part_end_scale_x,
|
||||
age,
|
||||
)
|
||||
} else {
|
||||
self.system.part_start_scale_x
|
||||
};
|
||||
particle.scale_y = if interp_scale {
|
||||
lerp(
|
||||
self.system.part_start_scale_y,
|
||||
self.system.part_end_scale_y,
|
||||
age,
|
||||
)
|
||||
} else {
|
||||
self.system.part_start_scale_y
|
||||
};
|
||||
particle.glow = lerp(self.system.part_start_glow, self.system.part_end_glow, age);
|
||||
particle.normalized_age = age;
|
||||
}
|
||||
|
||||
if state.active && state.system_age >= self.system.start_age {
|
||||
state.burst_timer += dt;
|
||||
let rate = self.system.burst_rate.max(0.01);
|
||||
while state.burst_timer >= rate {
|
||||
state.burst_timer -= rate;
|
||||
self.emit_burst(&mut state);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.lock().active
|
||||
}
|
||||
|
||||
pub fn source_position(&self) -> Vector3 {
|
||||
self.lock().source_position
|
||||
}
|
||||
|
||||
pub fn set_source_position(&mut self, value: Vector3) {
|
||||
self.lock().source_position = value;
|
||||
}
|
||||
|
||||
pub fn source_rotation(&self) -> Quaternion {
|
||||
self.lock().source_rotation
|
||||
}
|
||||
|
||||
pub fn set_source_rotation(&mut self, value: Quaternion) {
|
||||
self.lock().source_rotation = value;
|
||||
}
|
||||
|
||||
pub fn system_age(&self) -> f32 {
|
||||
self.lock().system_age
|
||||
}
|
||||
|
||||
pub fn target_position(&self) -> Vector3 {
|
||||
self.lock().target_position
|
||||
}
|
||||
|
||||
pub fn set_target_position(&mut self, value: Vector3) {
|
||||
self.lock().target_position = value;
|
||||
}
|
||||
|
||||
pub fn wind(&self) -> Vector3 {
|
||||
self.lock().wind
|
||||
}
|
||||
|
||||
pub fn set_wind(&mut self, value: Vector3) {
|
||||
self.lock().wind = value;
|
||||
}
|
||||
|
||||
fn emit_burst(&self, state: &mut State) {
|
||||
for _ in 0..self.system.burst_part_count {
|
||||
if state.particles.len() >= MAX_PARTICLES {
|
||||
break;
|
||||
}
|
||||
let position = self.initial_position(state);
|
||||
let velocity = self.initial_velocity(state);
|
||||
state.particles.push(LiveParticle {
|
||||
age: 0.0,
|
||||
color: self.system.part_start_color,
|
||||
glow: self.system.part_start_glow,
|
||||
normalized_age: 0.0,
|
||||
position,
|
||||
scale_x: self.system.part_start_scale_x,
|
||||
scale_y: self.system.part_start_scale_y,
|
||||
velocity,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_position(&self, state: &mut State) -> Vector3 {
|
||||
if self.system.burst_radius <= 0.0 {
|
||||
return Vector3::zero();
|
||||
}
|
||||
let radius = random_f32(state) * self.system.burst_radius;
|
||||
let theta = random_f32(state) * 2.0 * PI;
|
||||
let phi = (random_f32(state) - 0.5) * PI;
|
||||
Vector3 {
|
||||
x: radius * phi.cos() * theta.cos(),
|
||||
y: radius * phi.cos() * theta.sin(),
|
||||
z: radius * phi.sin(),
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_velocity(&self, state: &mut State) -> Vector3 {
|
||||
let speed = lerp(
|
||||
self.system.burst_speed_min,
|
||||
self.system.burst_speed_max,
|
||||
random_f32(state),
|
||||
);
|
||||
let mut direction = if self.system.pattern == SourcePattern::DROP {
|
||||
Vector3::zero()
|
||||
} else if self.system.pattern == SourcePattern::EXPLODE {
|
||||
random_unit_sphere(state)
|
||||
} else if self.system.pattern == SourcePattern::ANGLE
|
||||
|| self.system.pattern == SourcePattern::ANGLE_CONE
|
||||
{
|
||||
cone_direction(state, self.system.inner_angle, self.system.outer_angle)
|
||||
} else if self.system.pattern == SourcePattern::ANGLE_CONE_EMPTY {
|
||||
cone_direction(state, self.system.outer_angle, PI)
|
||||
} else {
|
||||
Vector3::unit_z()
|
||||
};
|
||||
if self.system.part_flags & ParticleFlags::OBJECT_RELATIVE.0 != 0
|
||||
&& direction != Vector3::zero()
|
||||
{
|
||||
direction = Vector3::mul_with_vector3_quaternion(direction, state.source_rotation);
|
||||
}
|
||||
scale(direction, speed)
|
||||
}
|
||||
|
||||
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
|
||||
self.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
}
|
||||
|
||||
fn random_f32(state: &mut State) -> f32 {
|
||||
let mut value = state.random;
|
||||
value ^= value << 13;
|
||||
value ^= value >> 7;
|
||||
value ^= value << 17;
|
||||
state.random = value;
|
||||
(value >> 40) as f32 / (1_u32 << 24) as f32
|
||||
}
|
||||
|
||||
fn random_unit_sphere(state: &mut State) -> Vector3 {
|
||||
let theta = random_f32(state) * 2.0 * PI;
|
||||
let phi = (2.0 * random_f32(state) - 1.0).acos();
|
||||
Vector3 {
|
||||
x: phi.sin() * theta.cos(),
|
||||
y: phi.sin() * theta.sin(),
|
||||
z: phi.cos(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cone_direction(state: &mut State, inner: f32, outer: f32) -> Vector3 {
|
||||
let angle = lerp(inner, outer, random_f32(state));
|
||||
let rotation = random_f32(state) * 2.0 * PI;
|
||||
Vector3 {
|
||||
x: angle.sin() * rotation.cos(),
|
||||
y: angle.sin() * rotation.sin(),
|
||||
z: angle.cos(),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize(value: Vector3) -> Vector3 {
|
||||
let length = (value.x * value.x + value.y * value.y + value.z * value.z).sqrt();
|
||||
if length > 0.0 && length.is_finite() {
|
||||
scale(value, length.recip())
|
||||
} else {
|
||||
Vector3::zero()
|
||||
}
|
||||
}
|
||||
|
||||
const fn scale(value: Vector3, amount: f32) -> Vector3 {
|
||||
Vector3 {
|
||||
x: value.x * amount,
|
||||
y: value.y * amount,
|
||||
z: value.z * amount,
|
||||
}
|
||||
}
|
||||
|
||||
fn lerp(start: f32, end: f32, amount: f32) -> f32 {
|
||||
start + (end - start) * amount
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn emits_ages_and_expires_particles_deterministically() {
|
||||
let mut system = PrimitiveParticleSystem::default();
|
||||
system.pattern = SourcePattern::DROP;
|
||||
system.burst_part_count = 2;
|
||||
system.burst_rate = 0.1;
|
||||
system.part_max_age = 0.25;
|
||||
system.part_start_scale_x = 1.0;
|
||||
system.part_start_scale_y = 2.0;
|
||||
let simulator = ParticleSimulator::new(system, Some(7)).unwrap();
|
||||
simulator.tick(0.1).unwrap();
|
||||
assert_eq!(simulator.get_particles().unwrap().len(), 2);
|
||||
simulator.tick(0.1).unwrap();
|
||||
assert_eq!(simulator.get_particles().unwrap().len(), 4);
|
||||
simulator.tick(0.2).unwrap();
|
||||
assert_eq!(simulator.get_particles().unwrap().len(), 6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
//! Physics-driven collision-volume offsets and positional record semantics.
|
||||
|
||||
use crate::Error;
|
||||
use libremetaverse_types::compat::{Object, Vector3};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
const PHYSICS_DRIVEN_PARAMS: [i32; 6] = [1200, 1201, 1204, 1205, 1206, 1207];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PhysicsVolumeMorphsVolumeMorph {
|
||||
bone_name: String,
|
||||
pos_delta: Vector3,
|
||||
}
|
||||
|
||||
impl PhysicsVolumeMorphsVolumeMorph {
|
||||
pub fn new(bone_name: String, pos_delta: Vector3) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
bone_name,
|
||||
pos_delta,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deconstruct(
|
||||
&mut self,
|
||||
bone_name: &mut String,
|
||||
pos_delta: &mut Vector3,
|
||||
) -> Result<(), Error> {
|
||||
*bone_name = self.bone_name.clone();
|
||||
*pos_delta = self.pos_delta;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn equals_with_volume_morph(&self, other: Self) -> bool {
|
||||
*self == other
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn equals_with_object(&self, object: Object) -> bool {
|
||||
object.downcast_ref::<Self>() == Some(self)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_hash_code(&self) -> i32 {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
self.bone_name.hash(&mut hasher);
|
||||
for value in self.pos_delta.0 {
|
||||
value.to_bits().hash(&mut hasher);
|
||||
}
|
||||
hasher.finish() as i32
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::inherent_to_string_shadow_display)]
|
||||
pub fn to_string(&self) -> String {
|
||||
format!(
|
||||
"VolumeMorph {{ BoneName = {}, PosDelta = <{}, {}, {}> }}",
|
||||
self.bone_name, self.pos_delta.0[0], self.pos_delta.0[1], self.pos_delta.0[2]
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn eq(left: Self, right: Self) -> bool {
|
||||
left == right
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn ne(left: Self, right: Self) -> bool {
|
||||
left != right
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn bone_name(&self) -> String {
|
||||
self.bone_name.clone()
|
||||
}
|
||||
|
||||
pub fn set_bone_name(&mut self, value: String) {
|
||||
self.bone_name = value;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn pos_delta(&self) -> Vector3 {
|
||||
self.pos_delta
|
||||
}
|
||||
|
||||
pub fn set_pos_delta(&mut self, value: Vector3) {
|
||||
self.pos_delta = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhysicsVolumeMorphs;
|
||||
|
||||
impl PhysicsVolumeMorphs {
|
||||
pub fn compute_bone_offsets(
|
||||
driven_weights: HashMap<i32, f32>,
|
||||
vp_weights: HashMap<i32, f32>,
|
||||
) -> Result<HashMap<String, Vector3>, Error> {
|
||||
// Preserved for signature parity; the pinned implementation also uses
|
||||
// only the normalized driven output and the visual-param definition.
|
||||
let _ = vp_weights;
|
||||
let mut offsets: HashMap<String, Vector3> = HashMap::new();
|
||||
for (param_id, normalized) in driven_weights {
|
||||
if !PHYSICS_DRIVEN_PARAMS.contains(¶m_id) {
|
||||
continue;
|
||||
}
|
||||
let Some(parameter) = crate::visual_catalog::param_by_id(param_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(morphs) = parameter.volume_morphs.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let local_value =
|
||||
parameter.min_value + (parameter.max_value - parameter.min_value) * normalized;
|
||||
for morph in morphs {
|
||||
let delta = Vector3([
|
||||
morph.position_delta.x * local_value,
|
||||
morph.position_delta.y * local_value,
|
||||
morph.position_delta.z * local_value,
|
||||
]);
|
||||
offsets
|
||||
.entry(morph.bone_name.clone())
|
||||
.and_modify(|current| {
|
||||
for index in 0..3 {
|
||||
current.0[index] += delta.0[index];
|
||||
}
|
||||
})
|
||||
.or_insert(delta);
|
||||
}
|
||||
}
|
||||
Ok(offsets)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn record_properties_equality_and_deconstruction_round_trip() {
|
||||
let delta = Vector3([0.0, 0.0, -0.01]);
|
||||
let mut morph = PhysicsVolumeMorphsVolumeMorph::new("LEFT_PEC".to_owned(), delta).unwrap();
|
||||
let copy = morph.clone();
|
||||
assert!(morph.equals_with_volume_morph(copy.clone()));
|
||||
assert!(morph.equals_with_object(Object::opaque(copy)));
|
||||
let mut name = String::new();
|
||||
let mut output = Vector3::default();
|
||||
morph.deconstruct(&mut name, &mut output).unwrap();
|
||||
assert_eq!(name, "LEFT_PEC");
|
||||
assert_eq!(output, delta);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driven_weight_uses_pinned_visual_param_range_and_morphs() {
|
||||
let offsets =
|
||||
PhysicsVolumeMorphs::compute_bone_offsets(HashMap::from([(1200, 1.0)]), HashMap::new())
|
||||
.unwrap();
|
||||
assert_eq!(offsets["LEFT_PEC"], Vector3([0.0, 0.0, -0.03]));
|
||||
assert_eq!(offsets["RIGHT_PEC"], Vector3([0.0, 0.0, -0.03]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Error types preserved from the upstream public API.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use libremetaverse_types::compat::ExternalError;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
macro_rules! message_error {
|
||||
($name:ident, $fallback:literal) => {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct $name {
|
||||
message: String,
|
||||
inner: Option<ExternalError>,
|
||||
}
|
||||
|
||||
impl $name {
|
||||
fn with_parts(message: String, inner: Option<ExternalError>) -> Self {
|
||||
Self { message, inner }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for $name {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.message.is_empty() {
|
||||
formatter.write_str($fallback)
|
||||
} else {
|
||||
formatter.write_str(&self.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for $name {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.inner
|
||||
.as_ref()
|
||||
.map(|error| error as &(dyn std::error::Error + 'static))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
message_error!(AppearanceManagerException, "appearance operation failed");
|
||||
message_error!(MalformedDataException, "malformed packet data");
|
||||
message_error!(RenderingException, "rendering failed");
|
||||
|
||||
impl AppearanceManagerException {
|
||||
pub fn new(message: String) -> Result<Self, Error> {
|
||||
Ok(Self::with_parts(message, None))
|
||||
}
|
||||
}
|
||||
|
||||
impl MalformedDataException {
|
||||
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||
Ok(Self::with_parts(String::new(), None))
|
||||
}
|
||||
|
||||
pub fn new_with_string(message: String) -> Result<Self, Error> {
|
||||
Ok(Self::with_parts(message, None))
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderingException {
|
||||
pub fn new_with_string(message: String) -> Result<Self, Error> {
|
||||
Ok(Self::with_parts(message, None))
|
||||
}
|
||||
|
||||
pub fn new_with_string_exception(
|
||||
message: String,
|
||||
inner_exception: ExternalError,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self::with_parts(message, Some(inner_exception)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rendering_error_exposes_source() {
|
||||
let error = RenderingException::new_with_string_exception(
|
||||
"mesh failed".to_owned(),
|
||||
ExternalError("bad triangle".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(error.to_string(), "mesh failed");
|
||||
assert_eq!(
|
||||
std::error::Error::source(&error).unwrap().to_string(),
|
||||
"bad triangle"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
//! Region/local coordinate conversions and parent-relative object positions.
|
||||
|
||||
use crate::{Avatar, Error, Primitive, Simulator};
|
||||
use libremetaverse_types::{Vector3, Vector3d};
|
||||
|
||||
pub struct PositionHelper;
|
||||
|
||||
impl PositionHelper {
|
||||
pub fn to_global_position(sim_handle: u64, local_pos: Vector3) -> Result<Vector3d, Error> {
|
||||
let global_x = (sim_handle >> 32) as u32;
|
||||
let global_y = sim_handle as u32;
|
||||
Vector3d::new_with_double_double_double(
|
||||
f64::from(global_x) + f64::from(local_pos.x),
|
||||
f64::from(global_y) + f64::from(local_pos.y),
|
||||
f64::from(local_pos.z),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_local_position(sim_handle: u64, global_pos: Vector3d) -> Result<Vector3, Error> {
|
||||
let global_x = (sim_handle >> 32) as u32;
|
||||
let global_y = sim_handle as u32;
|
||||
Vector3::new_with_single_single_single(
|
||||
(global_pos.x - f64::from(global_x)) as f32,
|
||||
(global_pos.y - f64::from(global_y)) as f32,
|
||||
global_pos.z as f32,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn global_position_with_simulator_vector3(
|
||||
sim: Simulator,
|
||||
local_pos: Vector3,
|
||||
) -> Result<Vector3d, Error> {
|
||||
Self::to_global_position(sim.handle, local_pos)
|
||||
}
|
||||
|
||||
pub fn global_position_with_primitive_simulator(
|
||||
prim: Primitive,
|
||||
current_sim: Simulator,
|
||||
) -> Result<Vector3d, Error> {
|
||||
Self::global_position_with_simulator_vector3(current_sim, prim.position)
|
||||
}
|
||||
|
||||
pub fn region_handle_from_global(global_x: u32, global_y: u32) -> Result<u64, Error> {
|
||||
let x = global_x / Simulator::DEFAULT_REGION_SIZE_X * Simulator::DEFAULT_REGION_SIZE_X;
|
||||
let y = global_y / Simulator::DEFAULT_REGION_SIZE_Y * Simulator::DEFAULT_REGION_SIZE_Y;
|
||||
Ok((u64::from(x) << 32) | u64::from(y))
|
||||
}
|
||||
|
||||
pub fn region_handle_from_global_position(global_pos: Vector3d) -> Result<u64, Error> {
|
||||
Self::region_handle_from_global(global_pos.x as u32, global_pos.y as u32)
|
||||
}
|
||||
|
||||
pub fn get_avatar_position(sim: Simulator, avatar: Avatar) -> Result<Vector3, Error> {
|
||||
if avatar.parent_id == 0 {
|
||||
return Ok(avatar.position);
|
||||
}
|
||||
let objects = sim
|
||||
.objects_primitives
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
Ok(objects
|
||||
.get(&avatar.parent_id)
|
||||
.map_or(avatar.position, |parent| {
|
||||
Vector3::add_with_vector3_vector3_87e25eaf(
|
||||
parent.position,
|
||||
Vector3::mul_with_vector3_quaternion(avatar.position, parent.rotation),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn get_prim_position(sim: Simulator, prim: Primitive) -> Result<Vector3, Error> {
|
||||
if prim.parent_id == 0 {
|
||||
return Ok(prim.position);
|
||||
}
|
||||
let objects = sim
|
||||
.objects_primitives
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
Ok(objects
|
||||
.get(&prim.parent_id)
|
||||
.map_or(prim.position, |parent| {
|
||||
Vector3::add_with_vector3_vector3_87e25eaf(
|
||||
parent.position,
|
||||
Vector3::mul_with_vector3_quaternion(prim.position, parent.rotation),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn format_coordinates(
|
||||
x: Option<Option<i32>>,
|
||||
y: Option<Option<i32>>,
|
||||
z: Option<Option<i32>>,
|
||||
) -> Result<String, Error> {
|
||||
let values: Vec<_> = [x.flatten(), y.flatten(), z.flatten()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|value| value.to_string())
|
||||
.collect();
|
||||
Ok(if values.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" ({})", values.join(","))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn format_region_coordinates(
|
||||
region_name: String,
|
||||
x: Option<Option<i32>>,
|
||||
y: Option<Option<i32>>,
|
||||
z: Option<Option<i32>>,
|
||||
) -> Result<String, Error> {
|
||||
Ok(region_name + &Self::format_coordinates(x, y, z)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn local_global_round_trip_and_region_alignment() {
|
||||
let handle = ((512_u64 * 256) << 32) | (1024_u64 * 256);
|
||||
let local = Vector3::new_with_single_single_single(10.5, 20.25, 5.0).unwrap();
|
||||
let global = PositionHelper::to_global_position(handle, local).unwrap();
|
||||
assert_eq!(
|
||||
PositionHelper::to_local_position(handle, global).unwrap(),
|
||||
local
|
||||
);
|
||||
assert_eq!(
|
||||
PositionHelper::region_handle_from_global(300, 700).unwrap(),
|
||||
(256_u64 << 32) | 512
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_only_present_coordinates() {
|
||||
assert_eq!(
|
||||
PositionHelper::format_coordinates(None, None, None).unwrap(),
|
||||
""
|
||||
);
|
||||
assert_eq!(
|
||||
PositionHelper::format_coordinates(Some(Some(10)), Some(None), Some(Some(30))).unwrap(),
|
||||
" (10,30)"
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,600 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
|
||||
|
||||
use libremetaverse_types::compat::Object;
|
||||
|
||||
use crate::{Error, FieldType, GridClient, PacketFrequency};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MapField {
|
||||
pub count: i32,
|
||||
pub keyword_position: i32,
|
||||
pub name: String,
|
||||
pub type_: FieldType,
|
||||
}
|
||||
|
||||
impl Default for MapField {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
count: 0,
|
||||
keyword_position: 0,
|
||||
name: String::new(),
|
||||
type_: FieldType::U8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MapField {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
pub fn compare_to(&self, obj: Option<Object>) -> Result<i32, Error> {
|
||||
let Some(obj) = obj else {
|
||||
return Ok(1);
|
||||
};
|
||||
let other = obj.downcast_ref::<Self>().ok_or(Error::Argument)?;
|
||||
Ok(self.keyword_position.cmp(&other.keyword_position) as i32)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MapBlock {
|
||||
pub count: i32,
|
||||
pub fields: Vec<MapField>,
|
||||
pub keyword_position: i32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl MapBlock {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
pub fn compare_to(&self, obj: Option<Object>) -> Result<i32, Error> {
|
||||
let Some(obj) = obj else {
|
||||
return Ok(1);
|
||||
};
|
||||
let other = obj.downcast_ref::<Self>().ok_or(Error::Argument)?;
|
||||
Ok(self.keyword_position.cmp(&other.keyword_position) as i32)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MapPacket {
|
||||
pub blocks: Vec<MapBlock>,
|
||||
pub encoded: bool,
|
||||
pub frequency: PacketFrequency,
|
||||
pub id: u16,
|
||||
pub name: String,
|
||||
pub trusted: bool,
|
||||
}
|
||||
|
||||
impl Default for MapPacket {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
blocks: Vec::new(),
|
||||
encoded: false,
|
||||
frequency: PacketFrequency::Low,
|
||||
id: 0,
|
||||
name: String::new(),
|
||||
trusted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MapPacket {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProtocolManager {
|
||||
pub high_maps: Vec<MapPacket>,
|
||||
pub keyword_positions: HashMap<String, i32>,
|
||||
pub low_maps: Vec<MapPacket>,
|
||||
pub medium_maps: Vec<MapPacket>,
|
||||
pub type_sizes: HashMap<FieldType, i32>,
|
||||
}
|
||||
|
||||
impl ProtocolManager {
|
||||
pub fn new(map_file: String, client: GridClient) -> Result<Self, Error> {
|
||||
// The C# instance retains this reference only to enrich parse-error logging.
|
||||
// Rust reports parsing failures to the caller, so no client ownership is needed.
|
||||
drop(client);
|
||||
let mut manager = Self::empty();
|
||||
manager.load_map_file(&map_file)?;
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
let type_sizes = HashMap::from([
|
||||
(FieldType::U8, 1),
|
||||
(FieldType::U16, 2),
|
||||
(FieldType::U32, 4),
|
||||
(FieldType::U64, 8),
|
||||
(FieldType::S8, 1),
|
||||
(FieldType::S16, 2),
|
||||
(FieldType::S32, 4),
|
||||
(FieldType::F32, 4),
|
||||
(FieldType::F64, 8),
|
||||
(FieldType::UUID, 16),
|
||||
(FieldType::BOOL, 1),
|
||||
(FieldType::Vector3, 12),
|
||||
(FieldType::Vector3d, 24),
|
||||
(FieldType::Vector4, 16),
|
||||
(FieldType::Quaternion, 16),
|
||||
(FieldType::IPADDR, 4),
|
||||
(FieldType::IPPORT, 2),
|
||||
(FieldType::Variable, -1),
|
||||
(FieldType::Fixed, -2),
|
||||
]);
|
||||
Self {
|
||||
high_maps: vec![MapPacket::default(); 256],
|
||||
keyword_positions: HashMap::new(),
|
||||
low_maps: vec![MapPacket::default(); 65_536],
|
||||
medium_maps: vec![MapPacket::default(); 256],
|
||||
type_sizes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command_with_bytes(&self, data: Vec<u8>) -> Result<Option<MapPacket>, Error> {
|
||||
let Some(&frequency_byte) = data.get(4) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let result = if frequency_byte != 0xff {
|
||||
self.command_with_u_int16_packet_frequency(
|
||||
u16::from(frequency_byte),
|
||||
PacketFrequency::High,
|
||||
)
|
||||
} else if data.get(5) != Some(&0xff) {
|
||||
let Some(&command) = data.get(5) else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.command_with_u_int16_packet_frequency(u16::from(command), PacketFrequency::Medium)
|
||||
} else {
|
||||
let (Some(&high), Some(&low)) = (data.get(6), data.get(7)) else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.command_with_u_int16_packet_frequency(
|
||||
u16::from_be_bytes([high, low]),
|
||||
PacketFrequency::Low,
|
||||
)
|
||||
};
|
||||
match result {
|
||||
Ok(packet) => Ok(Some(packet)),
|
||||
Err(Error::IndexOutOfRange | Error::InvalidOperation) => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command_with_string(&self, command: String) -> Result<MapPacket, Error> {
|
||||
self.high_maps
|
||||
.iter()
|
||||
.chain(&self.medium_maps)
|
||||
.chain(&self.low_maps)
|
||||
.find(|packet| !packet.name.is_empty() && packet.name == command)
|
||||
.cloned()
|
||||
.ok_or(Error::InvalidOperation)
|
||||
}
|
||||
|
||||
pub fn command_with_u_int16_packet_frequency(
|
||||
&self,
|
||||
command: u16,
|
||||
frequency: PacketFrequency,
|
||||
) -> Result<MapPacket, Error> {
|
||||
let maps = match frequency {
|
||||
PacketFrequency::High => &self.high_maps,
|
||||
PacketFrequency::Medium => &self.medium_maps,
|
||||
PacketFrequency::Low => &self.low_maps,
|
||||
};
|
||||
let packet = maps
|
||||
.get(usize::from(command))
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
if packet.name.is_empty() {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
Ok(packet.clone())
|
||||
}
|
||||
|
||||
pub fn decode_map_file(map_file: String, output_file: String) -> Result<(), Error> {
|
||||
let input = File::open(map_file).map_err(|_| Error::InvalidOperation)?;
|
||||
let output = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(output_file)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
Self::decode(BufReader::new(input), BufWriter::new(output))
|
||||
}
|
||||
|
||||
fn decode(mut input: impl Read, mut output: impl Write) -> Result<(), Error> {
|
||||
let mut key = 0_u8;
|
||||
let mut buffer = [0_u8; 2048];
|
||||
loop {
|
||||
let count = input
|
||||
.read(&mut buffer)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
for byte in &mut buffer[..count] {
|
||||
*byte ^= key;
|
||||
key = key.wrapping_add(43);
|
||||
}
|
||||
output
|
||||
.write_all(&buffer[..count])
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
}
|
||||
output.flush().map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
|
||||
pub fn print_map(&self) -> Result<(), Error> {
|
||||
self.print_one_map(&self.low_maps, "Low ");
|
||||
self.print_one_map(&self.medium_maps, "Medium");
|
||||
self.print_one_map(&self.high_maps, "High ");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_one_map(&self, maps: &[MapPacket], frequency: &str) {
|
||||
for (index, packet) in maps
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, packet)| !packet.name.is_empty())
|
||||
{
|
||||
println!(
|
||||
"{frequency} {index:5} - {} - {} - {}",
|
||||
packet.name,
|
||||
if packet.trusted {
|
||||
"Trusted"
|
||||
} else {
|
||||
"Untrusted"
|
||||
},
|
||||
if packet.encoded {
|
||||
"Unencoded"
|
||||
} else {
|
||||
"Zerocoded"
|
||||
}
|
||||
);
|
||||
for block in &packet.blocks {
|
||||
if block.count == -1 {
|
||||
println!("\t{:4} {} (Variable)", block.keyword_position, block.name);
|
||||
} else {
|
||||
println!(
|
||||
"\t{:4} {} ({})",
|
||||
block.keyword_position, block.name, block.count
|
||||
);
|
||||
}
|
||||
for field in &block.fields {
|
||||
println!(
|
||||
"\t\t{:4} {} ({:?} / {})",
|
||||
field.keyword_position, field.name, field.type_, field.count
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_map_file(&mut self, map_file: &str) -> Result<(), Error> {
|
||||
let input = File::open(map_file).map_err(|_| Error::InvalidOperation)?;
|
||||
self.parse(BufReader::new(input))
|
||||
}
|
||||
|
||||
fn parse(&mut self, input: impl BufRead) -> Result<(), Error> {
|
||||
let mut low = 1_u16;
|
||||
let mut medium = 1_u16;
|
||||
let mut high = 1_u16;
|
||||
let mut in_packet = false;
|
||||
let mut in_block = false;
|
||||
let mut current_packet: Option<(PacketFrequency, usize)> = None;
|
||||
let mut current_block: Option<MapBlock> = None;
|
||||
|
||||
for (line_number, line) in input.lines().enumerate() {
|
||||
let line = line.map_err(|_| Error::Parse {
|
||||
position: line_number,
|
||||
context: "could not read protocol map line",
|
||||
})?;
|
||||
let trimmed = line.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
if !in_packet {
|
||||
if trimmed == "{" {
|
||||
in_packet = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if !in_block {
|
||||
if trimmed == "{" {
|
||||
if current_packet.is_none() {
|
||||
return Err(Self::parse_error(line_number, "block has no packet header"));
|
||||
}
|
||||
in_block = true;
|
||||
} else if trimmed == "}" {
|
||||
if let Some((frequency, index)) = current_packet.take() {
|
||||
self.packet_mut(frequency, index)?
|
||||
.blocks
|
||||
.sort_by_key(|block| block.keyword_position);
|
||||
}
|
||||
in_packet = false;
|
||||
} else if !trimmed.is_empty() && !trimmed.starts_with("//") {
|
||||
current_packet = Some(self.parse_packet_header(
|
||||
&trimmed,
|
||||
line_number,
|
||||
&mut low,
|
||||
&mut medium,
|
||||
&mut high,
|
||||
)?);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if trimmed.starts_with('{') {
|
||||
let tokens: Vec<_> = trimmed.split_whitespace().collect();
|
||||
if tokens.len() < 4 {
|
||||
return Err(Self::parse_error(
|
||||
line_number,
|
||||
"malformed field declaration",
|
||||
));
|
||||
}
|
||||
let type_ = Self::parse_field_type(tokens[2])
|
||||
.ok_or_else(|| Self::parse_error(line_number, "unknown field type"))?;
|
||||
let count = if tokens[3] == "}" {
|
||||
1
|
||||
} else {
|
||||
tokens[3]
|
||||
.parse()
|
||||
.map_err(|_| Self::parse_error(line_number, "invalid field count"))?
|
||||
};
|
||||
let keyword_position = self.keyword_position(tokens[1])?;
|
||||
let block = current_block
|
||||
.as_mut()
|
||||
.ok_or_else(|| Self::parse_error(line_number, "field has no block header"))?;
|
||||
block.fields.push(MapField {
|
||||
count,
|
||||
keyword_position,
|
||||
name: tokens[1].to_owned(),
|
||||
type_,
|
||||
});
|
||||
} else if trimmed == "}" {
|
||||
let mut block = current_block.take().ok_or_else(|| {
|
||||
Self::parse_error(line_number, "block terminator has no header")
|
||||
})?;
|
||||
block.fields.sort_by_key(|field| field.keyword_position);
|
||||
let (frequency, index) = current_packet
|
||||
.ok_or_else(|| Self::parse_error(line_number, "block has no packet"))?;
|
||||
self.packet_mut(frequency, index)?.blocks.push(block);
|
||||
in_block = false;
|
||||
} else if !trimmed.is_empty() && !trimmed.starts_with("//") {
|
||||
if current_block.is_some() {
|
||||
return Err(Self::parse_error(line_number, "block header is incomplete"));
|
||||
}
|
||||
let tokens: Vec<_> = trimmed.split_whitespace().collect();
|
||||
if tokens.len() < 2 {
|
||||
return Err(Self::parse_error(line_number, "malformed block header"));
|
||||
}
|
||||
let count = match tokens[1] {
|
||||
"Single" => 1,
|
||||
"Variable" => -1,
|
||||
"Multiple" => tokens
|
||||
.get(2)
|
||||
.ok_or_else(|| Self::parse_error(line_number, "missing block count"))?
|
||||
.parse()
|
||||
.map_err(|_| Self::parse_error(line_number, "invalid block count"))?,
|
||||
_ => return Err(Self::parse_error(line_number, "unknown block frequency")),
|
||||
};
|
||||
current_block = Some(MapBlock {
|
||||
count,
|
||||
fields: Vec::new(),
|
||||
keyword_position: self.keyword_position(tokens[0])?,
|
||||
name: tokens[0].to_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if in_packet || in_block || current_block.is_some() {
|
||||
return Err(Self::parse_error(
|
||||
0,
|
||||
"unterminated protocol map declaration",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_packet_header(
|
||||
&mut self,
|
||||
line: &str,
|
||||
position: usize,
|
||||
low: &mut u16,
|
||||
medium: &mut u16,
|
||||
high: &mut u16,
|
||||
) -> Result<(PacketFrequency, usize), Error> {
|
||||
let tokens: Vec<_> = line.split_whitespace().collect();
|
||||
if tokens.len() < 4 {
|
||||
return Err(Self::parse_error(position, "malformed packet header"));
|
||||
}
|
||||
self.keyword_position(tokens[0])?;
|
||||
let (frequency, id, trusted_index, encoded_index) = match tokens[1] {
|
||||
"Fixed" => {
|
||||
if tokens.len() < 5 {
|
||||
return Err(Self::parse_error(position, "malformed fixed packet header"));
|
||||
}
|
||||
let raw = tokens[2].strip_prefix("0x").unwrap_or(tokens[2]);
|
||||
let raw = u32::from_str_radix(raw, 16)
|
||||
.map_err(|_| Self::parse_error(position, "invalid fixed packet id"))?;
|
||||
(PacketFrequency::Low, (raw ^ 0xffff_0000) as u16, 3, 4)
|
||||
}
|
||||
"Low" => {
|
||||
let id = *low;
|
||||
*low = low.checked_add(1).ok_or(Error::IndexOutOfRange)?;
|
||||
(PacketFrequency::Low, id, 2, 3)
|
||||
}
|
||||
"Medium" => {
|
||||
let id = *medium;
|
||||
*medium = medium.checked_add(1).ok_or(Error::IndexOutOfRange)?;
|
||||
(PacketFrequency::Medium, id, 2, 3)
|
||||
}
|
||||
"High" => {
|
||||
let id = *high;
|
||||
*high = high.checked_add(1).ok_or(Error::IndexOutOfRange)?;
|
||||
(PacketFrequency::High, id, 2, 3)
|
||||
}
|
||||
_ => return Err(Self::parse_error(position, "unknown packet frequency")),
|
||||
};
|
||||
let packet = MapPacket {
|
||||
blocks: Vec::new(),
|
||||
encoded: tokens.get(encoded_index) == Some(&"Zerocoded"),
|
||||
frequency,
|
||||
id,
|
||||
name: tokens[0].to_owned(),
|
||||
trusted: tokens.get(trusted_index) == Some(&"Trusted"),
|
||||
};
|
||||
let index = usize::from(id);
|
||||
*self.packet_mut(frequency, index)? = packet;
|
||||
Ok((frequency, index))
|
||||
}
|
||||
|
||||
fn packet_mut(
|
||||
&mut self,
|
||||
frequency: PacketFrequency,
|
||||
index: usize,
|
||||
) -> Result<&mut MapPacket, Error> {
|
||||
match frequency {
|
||||
PacketFrequency::High => self.high_maps.get_mut(index),
|
||||
PacketFrequency::Medium => self.medium_maps.get_mut(index),
|
||||
PacketFrequency::Low => self.low_maps.get_mut(index),
|
||||
}
|
||||
.ok_or(Error::IndexOutOfRange)
|
||||
}
|
||||
|
||||
fn keyword_position(&mut self, keyword: &str) -> Result<i32, Error> {
|
||||
if let Some(position) = self.keyword_positions.get(keyword) {
|
||||
return Ok(*position);
|
||||
}
|
||||
let mut hash = keyword
|
||||
.chars()
|
||||
.skip(1)
|
||||
.fold(0_i32, |hash, character| {
|
||||
hash.wrapping_add(character as i32).wrapping_mul(2)
|
||||
})
|
||||
.wrapping_mul(2)
|
||||
& 0x1fff;
|
||||
let start = hash;
|
||||
while self
|
||||
.keyword_positions
|
||||
.values()
|
||||
.any(|position| *position == hash)
|
||||
{
|
||||
hash = (hash + 1) & 0x1fff;
|
||||
if hash == start {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
}
|
||||
self.keyword_positions.insert(keyword.to_owned(), hash);
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
fn parse_field_type(value: &str) -> Option<FieldType> {
|
||||
Some(match value.to_ascii_lowercase().as_str() {
|
||||
"u8" => FieldType::U8,
|
||||
"u16" => FieldType::U16,
|
||||
"u32" => FieldType::U32,
|
||||
"u64" => FieldType::U64,
|
||||
"s8" => FieldType::S8,
|
||||
"s16" => FieldType::S16,
|
||||
"s32" => FieldType::S32,
|
||||
"f32" => FieldType::F32,
|
||||
"f64" => FieldType::F64,
|
||||
"uuid" => FieldType::UUID,
|
||||
"bool" => FieldType::BOOL,
|
||||
"vector3" => FieldType::Vector3,
|
||||
"vector3d" => FieldType::Vector3d,
|
||||
"vector4" => FieldType::Vector4,
|
||||
"quaternion" => FieldType::Quaternion,
|
||||
"ipaddr" => FieldType::IPADDR,
|
||||
"ipport" => FieldType::IPPORT,
|
||||
"variable" => FieldType::Variable,
|
||||
"fixed" => FieldType::Fixed,
|
||||
"single" => FieldType::Single,
|
||||
"multiple" => FieldType::Multiple,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
const fn parse_error(position: usize, context: &'static str) -> Error {
|
||||
Error::Parse { position, context }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
const MAP: &str = r#"
|
||||
// version header
|
||||
{
|
||||
TestHigh High Trusted Zerocoded
|
||||
{
|
||||
AgentData Single
|
||||
{ AgentID UUID }
|
||||
{ SessionID UUID }
|
||||
}
|
||||
}
|
||||
{
|
||||
TestLow Fixed 0xFFFF00F2 Untrusted Unencoded
|
||||
{
|
||||
Data Variable
|
||||
{ Value U32 }
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parses_and_resolves_protocol_commands() {
|
||||
let mut manager = ProtocolManager::empty();
|
||||
manager.parse(Cursor::new(MAP)).unwrap();
|
||||
|
||||
let high = manager.command_with_string("TestHigh".to_owned()).unwrap();
|
||||
assert_eq!(high.frequency, PacketFrequency::High);
|
||||
assert_eq!(high.id, 1);
|
||||
assert!(high.trusted);
|
||||
assert!(high.encoded);
|
||||
assert_eq!(high.blocks[0].fields.len(), 2);
|
||||
|
||||
let low = manager
|
||||
.command_with_bytes(vec![0, 0, 0, 0, 0xff, 0xff, 0, 0xf2])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(low.name, "TestLow");
|
||||
assert_eq!(low.blocks[0].count, -1);
|
||||
assert_eq!(manager.type_sizes[&FieldType::Vector3d], 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xor_decoder_tracks_key_across_chunks() {
|
||||
let input: Vec<_> = (0_u16..4097).map(|value| value as u8).collect();
|
||||
let mut encoded = Vec::new();
|
||||
ProtocolManager::decode(Cursor::new(&input), &mut encoded).unwrap();
|
||||
let mut decoded = Vec::new();
|
||||
ProtocolManager::decode(Cursor::new(encoded), &mut decoded).unwrap();
|
||||
assert_eq!(decoded, input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_short_packet_headers_do_not_panic() {
|
||||
let manager = ProtocolManager::empty();
|
||||
assert!(manager.command_with_bytes(vec![0; 5]).unwrap().is_none());
|
||||
assert!(
|
||||
manager
|
||||
.command_with_bytes(vec![0, 0, 0, 0, 0xff])
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
manager
|
||||
.command_with_bytes(vec![0, 0, 0, 0, 0xff, 0xff, 0])
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
use std::io::Read;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use libremetaverse_types::compat::ReadWrite;
|
||||
|
||||
use crate::Error;
|
||||
use crate::rendering::EndianAwareBinaryReaderSourceFormat as SourceFormat;
|
||||
|
||||
/// A synchronized binary reader whose numeric methods honor the source byte order.
|
||||
pub struct EndianAwareBinaryReader {
|
||||
stream: Mutex<Box<dyn ReadWrite + Send>>,
|
||||
format: SourceFormat,
|
||||
}
|
||||
|
||||
impl EndianAwareBinaryReader {
|
||||
pub fn new_with_stream(stream: Box<dyn ReadWrite + Send>) -> Result<Self, Error> {
|
||||
Self::new_with_stream_source_format(stream, SourceFormat::LittleEndian)
|
||||
}
|
||||
|
||||
pub fn new_with_stream_source_format(
|
||||
stream: Box<dyn ReadWrite + Send>,
|
||||
format: SourceFormat,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
stream: Mutex::new(stream),
|
||||
format,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_exact<const N: usize>(&self) -> Result<[u8; N], Error> {
|
||||
let mut bytes = [0_u8; N];
|
||||
self.stream
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.read_exact(&mut bytes)
|
||||
.map_err(|_| Error::IndexOutOfRange)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn read_double(&self) -> Result<f64, Error> {
|
||||
let bytes = self.read_exact()?;
|
||||
Ok(match self.format {
|
||||
SourceFormat::BigEndian => f64::from_be_bytes(bytes),
|
||||
SourceFormat::LittleEndian => f64::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_int16(&self) -> Result<i16, Error> {
|
||||
let bytes = self.read_exact()?;
|
||||
Ok(match self.format {
|
||||
SourceFormat::BigEndian => i16::from_be_bytes(bytes),
|
||||
SourceFormat::LittleEndian => i16::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_int32(&self) -> Result<i32, Error> {
|
||||
let bytes = self.read_exact()?;
|
||||
Ok(match self.format {
|
||||
SourceFormat::BigEndian => i32::from_be_bytes(bytes),
|
||||
SourceFormat::LittleEndian => i32::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_int64(&self) -> Result<i64, Error> {
|
||||
let bytes = self.read_exact()?;
|
||||
Ok(match self.format {
|
||||
SourceFormat::BigEndian => i64::from_be_bytes(bytes),
|
||||
SourceFormat::LittleEndian => i64::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_single(&self) -> Result<f32, Error> {
|
||||
let bytes = self.read_exact()?;
|
||||
Ok(match self.format {
|
||||
SourceFormat::BigEndian => f32::from_be_bytes(bytes),
|
||||
SourceFormat::LittleEndian => f32::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_u_int32(&self) -> Result<u32, Error> {
|
||||
let bytes = self.read_exact()?;
|
||||
Ok(match self.format {
|
||||
SourceFormat::BigEndian => u32::from_be_bytes(bytes),
|
||||
SourceFormat::LittleEndian => u32::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_string_with_method(&self) -> Result<String, Error> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut stream = self
|
||||
.stream
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
loop {
|
||||
let mut byte = [0_u8; 1];
|
||||
stream
|
||||
.read_exact(&mut byte)
|
||||
.map_err(|_| Error::IndexOutOfRange)?;
|
||||
if byte[0] == 0 {
|
||||
break;
|
||||
}
|
||||
bytes.push(byte[0]);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&bytes).into_owned())
|
||||
}
|
||||
|
||||
pub fn read_string_with_int32(&self, size: i32) -> Result<String, Error> {
|
||||
let size = usize::try_from(size).map_err(|_| Error::Argument)?;
|
||||
let mut bytes = vec![0_u8; size];
|
||||
let mut stream = self
|
||||
.stream
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let mut read = 0;
|
||||
while read < size {
|
||||
let count = stream
|
||||
.read(&mut bytes[read..])
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
read += count;
|
||||
}
|
||||
bytes.truncate(read);
|
||||
Ok(String::from_utf8_lossy(&bytes).trim().to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reads_both_byte_orders_and_strings() {
|
||||
let mut big = Vec::new();
|
||||
big.extend_from_slice(&(-123_i16).to_be_bytes());
|
||||
big.extend_from_slice(&42.5_f32.to_be_bytes());
|
||||
big.extend_from_slice(b"hello\0 padded ");
|
||||
let reader = EndianAwareBinaryReader::new_with_stream_source_format(
|
||||
Box::new(Cursor::new(big)),
|
||||
SourceFormat::BigEndian,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(reader.read_int16().unwrap(), -123);
|
||||
assert_eq!(reader.read_single().unwrap(), 42.5);
|
||||
assert_eq!(reader.read_string_with_method().unwrap(), "hello");
|
||||
assert_eq!(reader.read_string_with_int32(8).unwrap(), "padded");
|
||||
|
||||
let reader = EndianAwareBinaryReader::new_with_stream(Box::new(Cursor::new(
|
||||
0x1234_5678_u32.to_le_bytes().to_vec(),
|
||||
)))
|
||||
.unwrap();
|
||||
assert_eq!(reader.read_u_int32().unwrap(), 0x1234_5678);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_short_numeric_reads() {
|
||||
let reader =
|
||||
EndianAwareBinaryReader::new_with_stream(Box::new(Cursor::new(vec![1, 2]))).unwrap();
|
||||
assert_eq!(reader.read_int32(), Err(Error::IndexOutOfRange));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Cross-platform renderer discovery without CLR assembly loading.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::Error;
|
||||
use crate::rendering::IRendering;
|
||||
|
||||
const BUILTIN_SIMPLE: &str = "builtin:simple";
|
||||
|
||||
pub(crate) fn list_renderers(path: &str) -> Result<Vec<String>, Error> {
|
||||
let directory = Path::new(path);
|
||||
if !directory.is_dir() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut renderers = vec![BUILTIN_SIMPLE.to_owned()];
|
||||
let entries = std::fs::read_dir(directory).map_err(|_| Error::Argument)?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|_| Error::Argument)?;
|
||||
let file_type = entry.file_type().map_err(|_| Error::Argument)?;
|
||||
if !file_type.is_file() {
|
||||
continue;
|
||||
}
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy().to_ascii_lowercase();
|
||||
if is_simple_artifact(&name) {
|
||||
renderers.push(entry.path().to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
renderers.sort();
|
||||
renderers.dedup();
|
||||
Ok(renderers)
|
||||
}
|
||||
|
||||
pub(crate) fn load_renderer(filename: &str) -> Result<Box<dyn IRendering>, Error> {
|
||||
let normalized = Path::new(filename)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or(filename)
|
||||
.to_ascii_lowercase();
|
||||
if filename.eq_ignore_ascii_case(BUILTIN_SIMPLE)
|
||||
|| filename.eq_ignore_ascii_case("simple")
|
||||
|| filename.eq_ignore_ascii_case("LibreMetaverse.Rendering.Simple")
|
||||
|| is_simple_artifact(&normalized)
|
||||
{
|
||||
return Ok(Box::new(
|
||||
crate::builtin_simple_renderer::SimpleRenderer::native_new()?,
|
||||
));
|
||||
}
|
||||
Err(Error::Rendering {
|
||||
source: libremetaverse_types::UUID::zero(),
|
||||
context: "unknown or unsupported renderer",
|
||||
})
|
||||
}
|
||||
|
||||
fn is_simple_artifact(name: &str) -> bool {
|
||||
name.contains("libremetaverse_rendering_simple")
|
||||
|| name.contains("libremetaverse-rendering-simple")
|
||||
|| name.contains("libremetaverse.rendering.simple")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn built_in_renderer_is_discoverable_and_constructible() {
|
||||
let renderers = list_renderers(env!("CARGO_MANIFEST_DIR")).unwrap();
|
||||
assert!(renderers.iter().any(|name| name == BUILTIN_SIMPLE));
|
||||
let renderer = load_renderer(BUILTIN_SIMPLE).unwrap();
|
||||
let mut vertices = Vec::new();
|
||||
renderer
|
||||
.transform_tex_coords(
|
||||
&mut vertices,
|
||||
libremetaverse_types::Vector3::zero(),
|
||||
crate::PrimitiveTextureEntryFace::new(None).unwrap(),
|
||||
libremetaverse_types::Vector3::one(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovery_recognizes_platform_artifact_names() {
|
||||
assert!(is_simple_artifact("liblibremetaverse_rendering_simple.so"));
|
||||
assert!(is_simple_artifact("libremetaverse_rendering_simple.dll"));
|
||||
assert!(is_simple_artifact(
|
||||
"liblibremetaverse_rendering_simple.dylib"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Renderer metadata carried by native renderer registrations.
|
||||
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct RendererNameAttribute(String);
|
||||
|
||||
impl RendererNameAttribute {
|
||||
pub fn new(name: String) -> Result<Self, Error> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Self(name))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RendererNameAttribute {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats the flags the same way .NET formats a valid `FaceMask` value.
|
||||
/// Unknown bit combinations remain numeric instead of inventing flag names.
|
||||
#[must_use]
|
||||
pub fn face_mask_to_string(mask: crate::rendering::FaceMask) -> String {
|
||||
let known = [
|
||||
(crate::rendering::FaceMask::SINGLE.0, "Single"),
|
||||
(crate::rendering::FaceMask::CAP.0, "Cap"),
|
||||
(crate::rendering::FaceMask::END.0, "End"),
|
||||
(crate::rendering::FaceMask::SIDE.0, "Side"),
|
||||
(crate::rendering::FaceMask::INNER.0, "Inner"),
|
||||
(crate::rendering::FaceMask::OUTER.0, "Outer"),
|
||||
(crate::rendering::FaceMask::HOLLOW.0, "Hollow"),
|
||||
(crate::rendering::FaceMask::OPEN.0, "Open"),
|
||||
(crate::rendering::FaceMask::FLAT.0, "Flat"),
|
||||
(crate::rendering::FaceMask::TOP.0, "Top"),
|
||||
(crate::rendering::FaceMask::BOTTOM.0, "Bottom"),
|
||||
];
|
||||
if mask.0 == 0 {
|
||||
return "0".to_owned();
|
||||
}
|
||||
let all_known = known.iter().fold(0, |bits, (flag, _)| bits | flag);
|
||||
if mask.0 & !all_known != 0 {
|
||||
return mask.0.to_string();
|
||||
}
|
||||
known
|
||||
.into_iter()
|
||||
.filter_map(|(flag, name)| (mask.0 & flag != 0).then_some(name))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn renderer_name_rejects_empty_metadata() {
|
||||
assert!(RendererNameAttribute::new(String::new()).is_err());
|
||||
assert_eq!(
|
||||
RendererNameAttribute::new("Simple".to_owned())
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"Simple"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_mask_uses_dotnet_style_flag_names() {
|
||||
let mask = crate::rendering::FaceMask(
|
||||
crate::rendering::FaceMask::CAP.0 | crate::rendering::FaceMask::OUTER.0,
|
||||
);
|
||||
assert_eq!(face_mask_to_string(mask), "Cap, Outer");
|
||||
assert_eq!(face_mask_to_string(crate::rendering::FaceMask(0)), "0");
|
||||
assert_eq!(
|
||||
face_mask_to_string(crate::rendering::FaceMask(1 << 20)),
|
||||
"1048576"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use libremetaverse_types::compat::{CancellationToken, Task};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
pub struct Repeat;
|
||||
|
||||
impl Repeat {
|
||||
pub async fn interval_with_time_span_action_cancellation_token_boolean(
|
||||
poll_interval: Duration,
|
||||
action: Box<dyn Fn() + Send + Sync>,
|
||||
token: CancellationToken,
|
||||
immediately: Option<bool>,
|
||||
) -> Result<(), Error> {
|
||||
if poll_interval.is_zero() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if immediately.unwrap_or(false) {
|
||||
action();
|
||||
}
|
||||
loop {
|
||||
tokio::select! {
|
||||
() = tokio::time::sleep(poll_interval) => action(),
|
||||
() = token.cancelled() => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn interval_with_time_span_func_cancellation_token_boolean(
|
||||
poll_interval: Duration,
|
||||
async_action: Box<dyn Fn() -> Task<()> + Send + Sync>,
|
||||
token: CancellationToken,
|
||||
immediately: Option<bool>,
|
||||
) -> Result<(), Error> {
|
||||
if poll_interval.is_zero() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if immediately.unwrap_or(false) {
|
||||
async_action().await?;
|
||||
}
|
||||
loop {
|
||||
tokio::select! {
|
||||
() = tokio::time::sleep(poll_interval) => async_action().await?,
|
||||
() = token.cancelled() => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use libremetaverse_types::compat::CancellationTokenSource;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn repeats_immediately_and_stops_on_cancellation() {
|
||||
let source = CancellationTokenSource::new();
|
||||
let token = source.token();
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let action_count = Arc::clone(&count);
|
||||
let cancel = source.clone();
|
||||
Repeat::interval_with_time_span_action_cancellation_token_boolean(
|
||||
Duration::from_millis(1),
|
||||
Box::new(move || {
|
||||
if action_count.fetch_add(1, Ordering::Relaxed) >= 2 {
|
||||
cancel.cancel();
|
||||
}
|
||||
}),
|
||||
token,
|
||||
Some(true),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count.load(Ordering::Relaxed), 3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//! Runtime-neutral singleton registration for native `GridClient` composition.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{Error, GridClient, Settings};
|
||||
|
||||
/// Factory stored by service collections for lazy `GridClient` construction.
|
||||
pub type GridClientFactory = Arc<dyn Fn() -> Result<GridClient, Error> + Send + Sync>;
|
||||
|
||||
/// Object-safe service collection contract used by the mapped DI extension.
|
||||
pub trait IServiceCollection: Send {
|
||||
/// Registers the concrete `GridClient` singleton factory.
|
||||
fn add_grid_client_singleton(&mut self, factory: GridClientFactory) -> Result<(), Error>;
|
||||
|
||||
/// Registers `IGridClient` as an alias of the concrete singleton.
|
||||
fn add_i_grid_client_alias(&mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// Small native service collection suitable for applications without a DI crate.
|
||||
#[derive(Default)]
|
||||
pub struct ServiceCollection {
|
||||
client_factory: Option<GridClientFactory>,
|
||||
client: Mutex<Option<GridClient>>,
|
||||
interface_alias: bool,
|
||||
}
|
||||
|
||||
impl ServiceCollection {
|
||||
/// Resolves the lazily-created concrete client singleton.
|
||||
pub fn resolve_grid_client(&self) -> Result<GridClient, Error> {
|
||||
let mut singleton = self
|
||||
.client
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(client) = singleton.as_ref() {
|
||||
return Ok(client.clone());
|
||||
}
|
||||
let factory = self
|
||||
.client_factory
|
||||
.as_ref()
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
let client = factory()?;
|
||||
*singleton = Some(client.clone());
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Resolves the interface registration to the same native singleton.
|
||||
pub fn resolve_i_grid_client(&self) -> Result<GridClient, Error> {
|
||||
if !self.interface_alias {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
self.resolve_grid_client()
|
||||
}
|
||||
}
|
||||
|
||||
impl IServiceCollection for ServiceCollection {
|
||||
fn add_grid_client_singleton(&mut self, factory: GridClientFactory) -> Result<(), Error> {
|
||||
if self.client_factory.is_some() {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
self.client_factory = Some(factory);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_i_grid_client_alias(&mut self) -> Result<(), Error> {
|
||||
if self.client_factory.is_none() || self.interface_alias {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
self.interface_alias = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_grid_client(
|
||||
mut services: Box<dyn IServiceCollection>,
|
||||
configure: Option<Box<dyn Fn(&mut Settings) + Send + Sync>>,
|
||||
) -> Result<Box<dyn IServiceCollection>, Error> {
|
||||
let configure: Option<Arc<dyn Fn(&mut Settings) + Send + Sync>> = configure.map(Arc::from);
|
||||
let factory: GridClientFactory = Arc::new(move || {
|
||||
let mut settings = Settings::default();
|
||||
if let Some(configure) = &configure {
|
||||
configure(&mut settings);
|
||||
}
|
||||
GridClient::builder()
|
||||
.with_settings(settings)
|
||||
.build()
|
||||
.map_err(Into::into)
|
||||
});
|
||||
services.add_grid_client_singleton(factory)?;
|
||||
services.add_i_grid_client_alias()?;
|
||||
Ok(services)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
|
||||
#[derive(Default)]
|
||||
struct RecordingCollection {
|
||||
factory: Arc<Mutex<Option<GridClientFactory>>>,
|
||||
alias_registered: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl IServiceCollection for RecordingCollection {
|
||||
fn add_grid_client_singleton(&mut self, factory: GridClientFactory) -> Result<(), Error> {
|
||||
*self
|
||||
.factory
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(factory);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_i_grid_client_alias(&mut self) -> Result<(), Error> {
|
||||
self.alias_registered.store(true, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registers_one_lazy_client_and_interface_alias() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&calls);
|
||||
let collection = RecordingCollection::default();
|
||||
let recorded_factory = Arc::clone(&collection.factory);
|
||||
let alias_registered = Arc::clone(&collection.alias_registered);
|
||||
let _services = add_grid_client(
|
||||
Box::new(collection),
|
||||
Some(Box::new(move |settings| {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
settings.default_effect_color.r = 0.25;
|
||||
})),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(alias_registered.load(Ordering::Relaxed));
|
||||
assert_eq!(calls.load(Ordering::Relaxed), 0);
|
||||
|
||||
let factory = recorded_factory
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
.expect("GridClient factory must be registered");
|
||||
let client = factory().unwrap();
|
||||
assert_eq!(calls.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(client.settings_ref().default_effect_color.r, 0.25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concrete_collection_resolves_same_runtime() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&calls);
|
||||
let mut services = ServiceCollection::default();
|
||||
services
|
||||
.add_grid_client_singleton(Arc::new(move || {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
GridClient::builder().build().map_err(Into::into)
|
||||
}))
|
||||
.unwrap();
|
||||
services.add_i_grid_client_alias().unwrap();
|
||||
let concrete = services.resolve_grid_client().unwrap();
|
||||
let interface = services.resolve_i_grid_client().unwrap();
|
||||
assert_eq!(concrete.settings_ref(), interface.settings_ref());
|
||||
assert_eq!(calls.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Process-wide simulator cache ownership and inactivity reaping.
|
||||
|
||||
use crate::{Parcel, Primitive, Settings, Simulator, SimulatorDataPool};
|
||||
use libremetaverse_types::Error;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak};
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
const REAPER_INTERVAL: Duration = Duration::from_secs(180);
|
||||
const ACTIVE_TIMESTAMP_SECONDS: u64 = 253_402_300_799;
|
||||
|
||||
fn mutex<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
value
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
struct PoolRecord {
|
||||
active_clients: i32,
|
||||
inactive_since: Option<(Instant, SystemTime)>,
|
||||
prim_cache: HashMap<u32, Primitive>,
|
||||
parcels: HashMap<i32, Parcel>,
|
||||
parcel_map: Vec<i32>,
|
||||
downloading_parcel_map: bool,
|
||||
}
|
||||
|
||||
impl PoolRecord {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
active_clients: 0,
|
||||
inactive_since: None,
|
||||
prim_cache: HashMap::new(),
|
||||
parcels: HashMap::new(),
|
||||
parcel_map: vec![0; 64 * 64],
|
||||
downloading_parcel_map: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(&self, handle: u64) -> SimulatorDataPool {
|
||||
SimulatorDataPool {
|
||||
active_clients: self.active_clients,
|
||||
downloading_parcel_map: self.downloading_parcel_map,
|
||||
handle,
|
||||
inactive_since: self.inactive_since.map_or_else(
|
||||
|| UNIX_EPOCH + Duration::from_secs(ACTIVE_TIMESTAMP_SECONDS),
|
||||
|(_, timestamp)| timestamp,
|
||||
),
|
||||
parcel_map: self.parcel_map.clone(),
|
||||
parcels: std::sync::RwLock::new(self.parcels.clone()),
|
||||
prim_cache: self.prim_cache.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Runtime {
|
||||
records: Mutex<HashMap<u64, PoolRecord>>,
|
||||
worker: Mutex<Option<JoinHandle<()>>>,
|
||||
wake: Condvar,
|
||||
stopped: Mutex<bool>,
|
||||
}
|
||||
|
||||
fn runtime() -> &'static Arc<Runtime> {
|
||||
static RUNTIME: OnceLock<Arc<Runtime>> = OnceLock::new();
|
||||
RUNTIME.get_or_init(|| {
|
||||
Arc::new(Runtime {
|
||||
records: Mutex::new(HashMap::new()),
|
||||
worker: Mutex::new(None),
|
||||
wake: Condvar::new(),
|
||||
stopped: Mutex::new(false),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn reap(runtime: &Runtime) {
|
||||
let timeout = Duration::from_millis(
|
||||
u64::try_from(Settings::simulator_pool_timeout().max(1)).unwrap_or(1),
|
||||
);
|
||||
mutex(&runtime.records).retain(|_, record| {
|
||||
record
|
||||
.inactive_since
|
||||
.is_none_or(|(instant, _)| instant.elapsed() < timeout)
|
||||
});
|
||||
}
|
||||
|
||||
fn ensure_reaper() {
|
||||
let runtime = Arc::clone(runtime());
|
||||
let mut worker = mutex(&runtime.worker);
|
||||
if worker.is_some() {
|
||||
return;
|
||||
}
|
||||
*mutex(&runtime.stopped) = false;
|
||||
let weak: Weak<Runtime> = Arc::downgrade(&runtime);
|
||||
*worker = Some(std::thread::spawn(move || {
|
||||
let Some(runtime) = weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let mut stopped = mutex(&runtime.stopped);
|
||||
loop {
|
||||
let (guard, _) = runtime
|
||||
.wake
|
||||
.wait_timeout(stopped, REAPER_INTERVAL)
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
stopped = guard;
|
||||
if *stopped {
|
||||
return;
|
||||
}
|
||||
drop(stopped);
|
||||
reap(&runtime);
|
||||
stopped = mutex(&runtime.stopped);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub(crate) fn snapshots() -> HashMap<u64, SimulatorDataPool> {
|
||||
reap(runtime());
|
||||
mutex(&runtime().records)
|
||||
.iter()
|
||||
.map(|(handle, record)| (*handle, record.snapshot(*handle)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn get(handle: u64) -> Result<SimulatorDataPool, Error> {
|
||||
let mut records = mutex(&runtime().records);
|
||||
let record = records.entry(handle).or_insert_with(PoolRecord::new);
|
||||
Ok(record.snapshot(handle))
|
||||
}
|
||||
|
||||
pub(crate) fn add(simulator: Simulator) -> Result<(), Error> {
|
||||
ensure_reaper();
|
||||
let mut records = mutex(&runtime().records);
|
||||
let record = records
|
||||
.entry(simulator.handle)
|
||||
.or_insert_with(PoolRecord::new);
|
||||
record.active_clients = record.active_clients.saturating_add(1).max(1);
|
||||
record.inactive_since = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn release(simulator: Simulator) -> Result<(), Error> {
|
||||
let mut records = mutex(&runtime().records);
|
||||
let record = records
|
||||
.entry(simulator.handle)
|
||||
.or_insert_with(PoolRecord::new);
|
||||
record.active_clients = record.active_clients.saturating_sub(1);
|
||||
if record.active_clients <= 0 {
|
||||
record.active_clients = 0;
|
||||
record.inactive_since = Some((Instant::now(), SystemTime::now()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown() -> Result<(), Error> {
|
||||
let runtime = runtime();
|
||||
*mutex(&runtime.stopped) = true;
|
||||
runtime.wake.notify_all();
|
||||
if let Some(worker) = mutex(&runtime.worker).take()
|
||||
&& worker.thread().id() != std::thread::current().id()
|
||||
{
|
||||
worker.join().map_err(|_| Error::InvalidOperation)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
//! Stable built-in viewer sound identifiers from the pinned C# source.
|
||||
|
||||
use libremetaverse_types::UUID;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const SOUNDS: &[(&str, &str)] = &[
|
||||
("BELL_TING", "ed124764-705d-d497-167a-182cd9fa2e6c"),
|
||||
("CLICK", "4c8c3c77-de8d-bde2-b9b8-32635e0fd4a6"),
|
||||
(
|
||||
"HEALTH_REDUCTION_FEMALE",
|
||||
"219c5d93-6c09-31c5-fb3f-c5fe7495c115",
|
||||
),
|
||||
(
|
||||
"HEALTH_REDUCTION_MALE",
|
||||
"e057c244-5768-1056-c37e-1537454eeb62",
|
||||
),
|
||||
("IM_START", "c825dfbc-9827-7e02-6507-3713d18916c1"),
|
||||
(
|
||||
"INSTANT_MESSAGE_NOTIFICATION",
|
||||
"67cc2844-00f3-2b3c-b991-6418d01e1bb7",
|
||||
),
|
||||
("INVALID_OPERATION", "4174f859-0d3d-c517-c424-72923dc21f65"),
|
||||
("KEYBOARD_LOOP", "5e191c7b-8996-9ced-a177-b2ac32bfea06"),
|
||||
(
|
||||
"MONEY_REDUCTION_COINS",
|
||||
"77a018af-098e-c037-51a6-178f05877c6f",
|
||||
),
|
||||
(
|
||||
"MONEY_INCREASE_CASH_REGISTER_BELL",
|
||||
"104974e3-dfda-428b-99ee-b0d4e748d3a3",
|
||||
),
|
||||
("NULL_KEYSTROKE", "2ca849ba-2885-4bc3-90ef-d4987a5b983a"),
|
||||
("OBJECT_COLLISION", "be582e5d-b123-41a2-a150-454c39e961c8"),
|
||||
(
|
||||
"OBJECT_COLLISION_RUBBER",
|
||||
"212b6d1e-8d9c-4986-b3aa-f3c6df8d987d",
|
||||
),
|
||||
(
|
||||
"OBJECT_COLLISION_PLASTIC",
|
||||
"d55c7f3c-e1c3-4ddc-9eff-9ef805d9190e",
|
||||
),
|
||||
(
|
||||
"OBJECT_COLLISION_FLESH",
|
||||
"2d8c6f51-149e-4e23-8413-93a379b42b67",
|
||||
),
|
||||
(
|
||||
"OBJECT_COLLISION_WOOD_SPLINTERING",
|
||||
"6f00669f-15e0-4793-a63e-c03f62fee43a",
|
||||
),
|
||||
(
|
||||
"OBJECT_COLLISION_GLASS_BREAK",
|
||||
"85cda060-b393-48e6-81c8-2cfdfb275351",
|
||||
),
|
||||
(
|
||||
"OBJECT_COLLISION_METAL_CLUNK",
|
||||
"d1375446-1c4d-470b-9135-30132433b678",
|
||||
),
|
||||
(
|
||||
"OBJECT_CREATE_WHOOSH",
|
||||
"3c8fc726-1fd6-862d-fa01-16c5b2568db6",
|
||||
),
|
||||
(
|
||||
"OBJECT_DELETE_SHAKE",
|
||||
"0cb7b00a-4c10-6948-84de-a93c09af2ba9",
|
||||
),
|
||||
("OBJECT_REZ", "f4a0660f-5446-dea2-80b7-6482a082803c"),
|
||||
(
|
||||
"PIE_MENU_APPEAR_DING",
|
||||
"8eaed61f-92ff-6485-de83-4dcc938a478e",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT",
|
||||
"d9f73cf8-17b4-6f7a-1565-7951226c305d",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT1",
|
||||
"f6ba9816-dcaf-f755-7b67-51b31b6233e5",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT2",
|
||||
"7aff2265-d05b-8b72-63c7-dbf96dc2f21f",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT3",
|
||||
"09b2184e-8601-44e2-afbb-ce37434b8ba1",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT4",
|
||||
"bbe4c7fc-7044-b05e-7b89-36924a67593c",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT5",
|
||||
"d166039b-b4f5-c2ec-4911-c85c727b016c",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT6",
|
||||
"242af82b-43c2-9a3b-e108-3b0c7e384981",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT7",
|
||||
"c1f334fb-a5be-8fe7-22b3-29631c21cf0b",
|
||||
),
|
||||
("SNAPSHOT", "3d09f582-3851-c0e0-f5ba-277ac5c73fb4"),
|
||||
(
|
||||
"TELEPORT_TEXTURE_APPLY",
|
||||
"d7a9a565-a013-2a69-797d-5332baa1a947",
|
||||
),
|
||||
("THUNDER", "e95c96a5-293c-bb7a-57ad-ce2e785ad85f"),
|
||||
("WINDOW_CLOSE", "2c346eda-b60c-ab33-1119-b8941916a499"),
|
||||
("WINDOW_OPEN", "c80260ba-41fd-8a46-768a-6bf236360e3a"),
|
||||
("ZIPPER", "6cf2be26-90cb-2669-a599-f5ab7698225f"),
|
||||
];
|
||||
|
||||
pub(crate) fn sound(name: &str) -> UUID {
|
||||
let value = SOUNDS
|
||||
.iter()
|
||||
.find_map(|(candidate, value)| (*candidate == name).then_some(*value))
|
||||
.unwrap_or_else(|| panic!("unknown built-in sound {name}"));
|
||||
UUID::new_with_string(value.to_owned()).expect("built-in sound UUID is valid")
|
||||
}
|
||||
|
||||
pub(crate) fn sounds() -> HashMap<UUID, String> {
|
||||
SOUNDS
|
||||
.iter()
|
||||
.map(|(name, _)| (sound(name), (*name).to_owned()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sound_catalog_is_complete_and_unique() {
|
||||
let catalog = sounds();
|
||||
assert_eq!(catalog.len(), SOUNDS.len());
|
||||
assert_eq!(catalog[&sound("BELL_TING")], "BELL_TING");
|
||||
assert_eq!(catalog[&sound("ZIPPER")], "ZIPPER");
|
||||
}
|
||||
}
|
||||
@@ -417,6 +417,7 @@ impl SoundManagerInner {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::float_cmp)] // Sound fixtures use exactly representable values.
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//! Thread-safe packet/message utilization counters.
|
||||
|
||||
use crate::Error;
|
||||
use crate::stats::Type;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct UtilizationStatisticsStat {
|
||||
type_: Type,
|
||||
tx_count: i64,
|
||||
rx_count: i64,
|
||||
tx_bytes: i64,
|
||||
rx_bytes: i64,
|
||||
}
|
||||
|
||||
impl Default for UtilizationStatisticsStat {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
type_: Type::Packet,
|
||||
tx_count: 0,
|
||||
rx_count: 0,
|
||||
tx_bytes: 0,
|
||||
rx_bytes: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UtilizationStatisticsStat {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn type_(&self) -> Type {
|
||||
self.type_
|
||||
}
|
||||
pub fn set_type_(&mut self, value: Type) {
|
||||
self.type_ = value;
|
||||
}
|
||||
#[must_use]
|
||||
pub fn tx_count(&self) -> i64 {
|
||||
self.tx_count
|
||||
}
|
||||
pub fn set_tx_count(&mut self, value: i64) {
|
||||
self.tx_count = value;
|
||||
}
|
||||
#[must_use]
|
||||
pub fn rx_count(&self) -> i64 {
|
||||
self.rx_count
|
||||
}
|
||||
pub fn set_rx_count(&mut self, value: i64) {
|
||||
self.rx_count = value;
|
||||
}
|
||||
#[must_use]
|
||||
pub fn tx_bytes(&self) -> i64 {
|
||||
self.tx_bytes
|
||||
}
|
||||
pub fn set_tx_bytes(&mut self, value: i64) {
|
||||
self.tx_bytes = value;
|
||||
}
|
||||
#[must_use]
|
||||
pub fn rx_bytes(&self) -> i64 {
|
||||
self.rx_bytes
|
||||
}
|
||||
pub fn set_rx_bytes(&mut self, value: i64) {
|
||||
self.rx_bytes = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct UtilizationStatistics {
|
||||
statistics: Arc<Mutex<HashMap<String, UtilizationStatisticsStat>>>,
|
||||
}
|
||||
|
||||
impl UtilizationStatistics {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
pub fn get_statistics(&self) -> Result<HashMap<String, UtilizationStatisticsStat>, Error> {
|
||||
Ok(self
|
||||
.statistics
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone())
|
||||
}
|
||||
|
||||
pub fn update(&self, key: String, type_: Type, tx_bytes: i64, rx_bytes: i64) {
|
||||
let mut statistics = self
|
||||
.statistics
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let statistic = statistics.entry(key).or_default();
|
||||
statistic.type_ = type_;
|
||||
if rx_bytes > 0 {
|
||||
statistic.rx_count = statistic.rx_count.wrapping_add(1);
|
||||
statistic.rx_bytes = statistic.rx_bytes.wrapping_add(rx_bytes);
|
||||
}
|
||||
if tx_bytes > 0 {
|
||||
statistic.tx_count = statistic.tx_count.wrapping_add(1);
|
||||
statistic.tx_bytes = statistic.tx_bytes.wrapping_add(tx_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn updates_count_only_positive_transfers_and_snapshots_are_independent() {
|
||||
let statistics = UtilizationStatistics::new().unwrap();
|
||||
statistics.update("PacketAck".to_owned(), Type::Packet, 10, 0);
|
||||
statistics.update("PacketAck".to_owned(), Type::Packet, 5, 20);
|
||||
statistics.update("PacketAck".to_owned(), Type::Packet, -1, -1);
|
||||
let snapshot = statistics.get_statistics().unwrap();
|
||||
let stat = &snapshot["PacketAck"];
|
||||
assert_eq!(stat.tx_count(), 2);
|
||||
assert_eq!(stat.tx_bytes(), 15);
|
||||
assert_eq!(stat.rx_count(), 1);
|
||||
assert_eq!(stat.rx_bytes(), 20);
|
||||
assert_eq!(stat.type_(), Type::Packet);
|
||||
}
|
||||
}
|
||||
@@ -379,6 +379,7 @@ impl TerrainManagerInner {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::cast_precision_loss, clippy::float_cmp)] // Bounded terrain fixtures intentionally use f32.
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//! Compatibility facade over the native capability-backed texture downloader.
|
||||
|
||||
use crate::{Error, GridClient};
|
||||
use libremetaverse_types::UUID;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
pub struct TexturePipeline {
|
||||
client: GridClient,
|
||||
running: AtomicBool,
|
||||
}
|
||||
|
||||
impl TexturePipeline {
|
||||
pub fn new(client: GridClient) -> Result<Self, Error> {
|
||||
// Resolve the manager eagerly so construction fails for a disposed or
|
||||
// incompletely initialized client instead of at the first abort.
|
||||
client.native_assets()?;
|
||||
Ok(Self {
|
||||
client,
|
||||
running: AtomicBool::new(false),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn abort_texture_request(&self, texture_id: UUID) -> Result<(), Error> {
|
||||
if texture_id == UUID::zero() {
|
||||
return Ok(());
|
||||
}
|
||||
self.client
|
||||
.native_assets()?
|
||||
.request_image_cancel(texture_id)
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) -> Result<(), Error> {
|
||||
if self.running.swap(false, Ordering::AcqRel) {
|
||||
self.client.native_assets()?.native_cancel_image_requests();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn startup(&self) -> Result<(), Error> {
|
||||
self.client.native_assets()?;
|
||||
if self.client.settings_ref().texture_pipeline.enabled {
|
||||
self.running.store(true, Ordering::Release);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn transfer_count(&self) -> i32 {
|
||||
self.client
|
||||
.native_assets()
|
||||
.map_or(0, |assets| assets.native_image_transfer_count())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TexturePipeline {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.shutdown();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,78 @@
|
||||
//! MessagePack formatter for protocol UUID values.
|
||||
|
||||
use crate::Error;
|
||||
use crate::message_pack::{
|
||||
IMessagePackFormatter, MessagePackReader, MessagePackSerializerOptions, MessagePackWriter,
|
||||
};
|
||||
use libremetaverse_types::UUID;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct UUIDFormatter;
|
||||
|
||||
impl UUIDFormatter {
|
||||
#[must_use]
|
||||
pub const fn instance() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
pub fn serialize(
|
||||
&self,
|
||||
writer: &mut MessagePackWriter,
|
||||
value: UUID,
|
||||
_options: MessagePackSerializerOptions,
|
||||
) -> Result<(), Error> {
|
||||
writer.write_bytes(&value.get_bytes()?)
|
||||
}
|
||||
|
||||
pub fn deserialize(
|
||||
&mut self,
|
||||
reader: &mut MessagePackReader,
|
||||
_options: MessagePackSerializerOptions,
|
||||
) -> Result<UUID, Error> {
|
||||
let bytes = reader.read_bytes()?.ok_or(Error::Argument)?;
|
||||
UUID::from_bytes(bytes, 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl IMessagePackFormatter<UUID> for UUIDFormatter {
|
||||
fn serialize(
|
||||
&self,
|
||||
writer: &mut MessagePackWriter,
|
||||
value: UUID,
|
||||
options: MessagePackSerializerOptions,
|
||||
) -> Result<(), Error> {
|
||||
self.serialize(writer, value, options)
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
reader: &mut MessagePackReader,
|
||||
options: MessagePackSerializerOptions,
|
||||
) -> Result<UUID, Error> {
|
||||
self.deserialize(reader, options)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn uuid_round_trips_as_messagepack_binary() {
|
||||
let expected = UUID::parse("00112233-4455-6677-8899-aabbccddeeff".to_owned()).unwrap();
|
||||
let mut writer = MessagePackWriter::new();
|
||||
UUIDFormatter::instance()
|
||||
.serialize(&mut writer, expected, MessagePackSerializerOptions)
|
||||
.unwrap();
|
||||
assert_eq!(&writer.as_slice()[..2], &[0xc4, 16]);
|
||||
let mut reader = MessagePackReader::new(writer.into_bytes());
|
||||
let actual = UUIDFormatter::instance()
|
||||
.deserialize(&mut reader, MessagePackSerializerOptions)
|
||||
.unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
@@ -23263,6 +23263,9 @@ impl VisualParams {
|
||||
)
|
||||
}
|
||||
}
|
||||
pub(crate) fn param_by_id(id: i32) -> Option<&'static VisualParam> {
|
||||
generated_params().get(&id)
|
||||
}
|
||||
pub(crate) fn decode_visual_params(bytes: &[u8]) -> HashMap<i32, f32> {
|
||||
let params = generated_params();
|
||||
let mut result = HashMap::with_capacity(GROUP0_PARAM_IDS.len());
|
||||
@@ -23313,6 +23316,7 @@ pub(crate) fn decode_visual_params(bytes: &[u8]) -> HashMap<i32, f32> {
|
||||
}
|
||||
pub(crate) fn new_avatar() -> Result<crate::Avatar, crate::Error> {
|
||||
Ok(crate::Avatar {
|
||||
base: crate::Primitive::new_with_constructor()?,
|
||||
animations: Vec::new(),
|
||||
appearance_flags: crate::AppearanceFlags::NONE,
|
||||
appearance_version: 0,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Source SHA-256: 74fa1275cd1f6c2a372af2fefc011a3e764f028a8957291d61f599bcb023a8ff
|
||||
// Source SHA-256: 7b67b72c2ad652626126d0e4eda7e8c26fc94c28f0f915238b2925f80a3d4af2
|
||||
|
||||
#![allow(clippy::float_cmp)] // Wire fixtures use exactly representable values.
|
||||
|
||||
use super::*;
|
||||
use libremetaverse_structured_data::{OSD, OSDMap};
|
||||
use libremetaverse_types::{UUID, Vector3};
|
||||
|
||||
Reference in new issue
Block a user