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

This commit is contained in:
hugo committed 2026-08-12 14:44:28 +00:00
1 parent dceb394378
commit c9a1170a27
140 files changed
+82175 -27179

No files matched your search

+1
View File
@@ -20,6 +20,7 @@ flate2 = "1.1.2"
futures-channel = "0.3.31"
futures-util = "0.3.31"
libremetaverse-imaging = { version = "0.0.1", path = "../libremetaverse-imaging" }
libremetaverse-prim-mesher = { version = "0.0.1", path = "../libremetaverse-prim-mesher" }
libremetaverse-structured-data = { version = "0.0.1", path = "../libremetaverse-structured-data" }
libremetaverse-types = { version = "0.0.1", path = "../libremetaverse-types" }
mac_address2 = "2.0.2"
@@ -0,0 +1,833 @@
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
+679 -7
View File
@@ -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};
+14 -1
View File
@@ -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();
+7
View File
@@ -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;
+152 -1
View File
@@ -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 &parameters {
if let Some(value) = asset.params.get_mut(&parameter.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 &parameters {
if let Some(current) = asset.params.get_mut(&parameter.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();
+336 -3
View File
@@ -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('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
}
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");
}
}
+15 -1
View File
@@ -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,
+21 -14
View File
@@ -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]
+774
View File
@@ -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('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
}
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());
}
}
+229
View File
@@ -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)?,
),
)
}
+284
View File
@@ -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);
}
}
+1
View File
@@ -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};
+1
View File
@@ -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),
};
+144
View File
@@ -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()
}
+104
View File
@@ -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);
}
}
+53 -10
View File
@@ -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());
}
}
+120
View File
@@ -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);
}
}
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()
+350
View File
@@ -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);
}
}
+4 -3
View File
@@ -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"
);
}
}
+504 -2
View File
@@ -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());
+243 -6
View File
@@ -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.
+723
View File
@@ -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");
}
}
+655
View File
@@ -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();
}
}
+1 -1
View File
@@ -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);
+532
View File
@@ -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(&notifications);
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
+724 -27
View File
@@ -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;
+215
View File
@@ -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 {
+1 -14
View File
@@ -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()));
}
}
}
+292
View File
@@ -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);
}
}
File diff suppressed because it is too large. Load diff
+167
View File
@@ -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"));
}
}
+50 -6
View File
@@ -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(&param_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]));
}
}
+98
View File
@@ -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"
);
}
}
+81
View File
@@ -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(())
}
+140
View File
@@ -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};
@@ -0,0 +1,57 @@
//! Behavioral checks for immutable event-argument mappings generated from the
//! pinned C# constructor assignments.
#![allow(clippy::float_cmp)] // Generated constructor fixtures retain exact values.
use libremetaverse::{MeanCollisionEventArgs, MeanCollisionType, ScriptDialogEventArgs};
use libremetaverse_types::UUID;
use std::time::{Duration, UNIX_EPOCH};
fn id(value: u64) -> UUID {
UUID::new_with_u_int64(value).expect("UUID")
}
#[test]
fn script_dialog_preserves_constructor_values_and_aliases() {
let image_id = id(1);
let object_id = id(2);
let owner_id = id(3);
let dialog = ScriptDialogEventArgs::new(
"Choose".to_owned(),
"OpenSim object".to_owned(),
image_id,
object_id,
"Grid".to_owned(),
"Resident".to_owned(),
-42,
vec!["Yes".to_owned(), "No".to_owned()],
owner_id,
)
.expect("ScriptDialogEventArgs");
assert_eq!(dialog.message(), "Choose");
assert_eq!(dialog.object_name(), "OpenSim object");
assert_eq!(dialog.image_id(), image_id);
assert_eq!(dialog.object_id(), object_id);
assert_eq!(dialog.first_name(), "Grid");
assert_eq!(dialog.last_name(), "Resident");
assert_eq!(dialog.channel(), -42);
assert_eq!(dialog.button_labels(), ["Yes", "No"]);
assert_eq!(dialog.owner_id(), owner_id);
}
#[test]
fn mean_collision_maps_perp_to_aggressor() {
let aggressor = id(10);
let victim = id(11);
let time = UNIX_EPOCH + Duration::from_secs(1234);
let collision =
MeanCollisionEventArgs::new(MeanCollisionType::Bump, aggressor, victim, 2.5, time)
.expect("MeanCollisionEventArgs");
assert_eq!(collision.type_(), MeanCollisionType::Bump);
assert_eq!(collision.aggressor(), aggressor);
assert_eq!(collision.victim(), victim);
assert_eq!(collision.magnitude(), 2.5);
assert_eq!(collision.time(), time);
}
+15 -2
View File
@@ -1,3 +1,8 @@
#![allow(clippy::await_holding_lock)] // The process-wide guard serializes socket integration tests.
#![allow(clippy::float_cmp)] // Wire fixtures use exactly representable values.
#![allow(clippy::items_after_statements)] // Local constants stay next to their concurrency scenario.
#![allow(clippy::too_many_lines)] // End-to-end fixtures keep each protocol exchange contiguous.
use libremetaverse::http::DownloadManager;
use libremetaverse::interfaces::IMessage;
use libremetaverse::messages::linden::{
@@ -14,7 +19,7 @@ use libremetaverse::{
InventoryItem, LoginState, LoginStatus, NetworkManager, NetworkManagerDisconnectType,
NetworkManagerLoginResponseCallback, PacketEventDictionary, Simulator,
};
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
use libremetaverse_types::compat::{
CancellationTokenSource, EventHandler, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
};
@@ -287,7 +292,15 @@ fn abort_and_callback_unregistration_preserve_reference_noop_behavior() {
}
struct TestCapsMessage;
impl IMessage for TestCapsMessage {}
impl IMessage for TestCapsMessage {
fn deserialize(&mut self, _map: OSDMap) -> Result<(), Error> {
Ok(())
}
fn serialize(&self) -> Result<OSDMap, Error> {
OSDMap::new_with_constructor()
}
}
#[test]
fn caps_callbacks_are_ordered_and_subscription_guards_unregister() {