Complete first release candidate audit (#107)
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
This commit is contained in:
1 parent
dceb394378
commit
c9a1170a27
140 files changed
+82175
-27179
No files matched your search
@@ -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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
|
||||
@@ -17,12 +17,13 @@ use crate::messages::linden::{
|
||||
};
|
||||
use crate::{
|
||||
AgentAccessEventArgs, AgentDataReplyEventArgs, AgentExperiencesEventArgs,
|
||||
AgentPreferencesEventArgs, AnimationsChangedEventArgs, AvatarRenderInfoEventArgs,
|
||||
BalanceEventArgs, ChatEventArgs, ChatSessionMemberAddedEventArgs,
|
||||
AgentPreferencesEventArgs, AlertMessageEventArgs, AnimationsChangedEventArgs,
|
||||
AvatarRenderInfoEventArgs, BalanceEventArgs, ChatEventArgs, ChatSessionMemberAddedEventArgs,
|
||||
ChatSessionMemberLeftEventArgs, ExperienceInfoEventArgs, ExperiencePreferencesEventArgs,
|
||||
GroupChatJoinedEventArgs, MoneyBalanceReplyEventArgs, NavMeshStatusUpdateEventArgs,
|
||||
ProductInfoEventArgs, RegionExperiencesEventArgs, SetDisplayNameReplyEventArgs,
|
||||
ViewerBenefitsEventArgs,
|
||||
GroupChatJoinedEventArgs, LoadUrlEventArgs, MeanCollisionEventArgs, MoneyBalanceReplyEventArgs,
|
||||
NavMeshStatusUpdateEventArgs, ProductInfoEventArgs, RegionExperiencesEventArgs,
|
||||
ScriptControlEventArgs, ScriptDialogEventArgs, ScriptQuestionEventArgs,
|
||||
ScriptSensorReplyEventArgs, SetDisplayNameReplyEventArgs, ViewerBenefitsEventArgs,
|
||||
};
|
||||
use crate::{
|
||||
AgentManagerAgentMovement, ChatSessionMember, ChatType, EffectType, Error, GridClient,
|
||||
@@ -31,11 +32,11 @@ use crate::{
|
||||
};
|
||||
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
||||
use libremetaverse_types::compat::{EventHandler, Subscription, Uri};
|
||||
use libremetaverse_types::{Color4, UUID, Vector3, Vector3d};
|
||||
use libremetaverse_types::{Color4, UUID, Vector3, Vector3d, Vector4};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fmt;
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock, RwLock};
|
||||
use std::time::{Duration, Instant, UNIX_EPOCH};
|
||||
|
||||
@@ -317,8 +318,17 @@ pub(crate) struct AgentManagerInner {
|
||||
disposed: AtomicBool,
|
||||
agent_id: RwLock<UUID>,
|
||||
session_id: RwLock<UUID>,
|
||||
secure_session_id: RwLock<UUID>,
|
||||
first_name: RwLock<String>,
|
||||
last_name: RwLock<String>,
|
||||
start_location: RwLock<String>,
|
||||
look_at: RwLock<Vector3>,
|
||||
home_region_handle: RwLock<u64>,
|
||||
home_look_at: RwLock<Vector3>,
|
||||
local_id: RwLock<u32>,
|
||||
collision_plane: RwLock<Vector4>,
|
||||
health: RwLock<f32>,
|
||||
height_width_generation: AtomicU32,
|
||||
active_group: RwLock<UUID>,
|
||||
active_group_powers: RwLock<crate::GroupPowers>,
|
||||
agent_access: RwLock<String>,
|
||||
@@ -344,6 +354,7 @@ pub(crate) struct AgentManagerInner {
|
||||
network: Mutex<Option<crate::NetworkManager>>,
|
||||
raw_packet_subscription: Mutex<Option<Subscription>>,
|
||||
caps_subscriptions: Mutex<Vec<Subscription>>,
|
||||
login_callback: Mutex<Option<crate::NetworkManagerLoginResponseCallback>>,
|
||||
movement: OnceLock<Arc<crate::agent_movement::AgentMovementRuntime>>,
|
||||
chat_from_simulator: EventRegistry<ChatEventArgs>,
|
||||
money_balance: EventRegistry<BalanceEventArgs>,
|
||||
@@ -362,6 +373,13 @@ pub(crate) struct AgentManagerInner {
|
||||
set_display_name_reply: EventRegistry<SetDisplayNameReplyEventArgs>,
|
||||
nav_mesh_status_update: EventRegistry<NavMeshStatusUpdateEventArgs>,
|
||||
mute_list_updated: EventRegistry<()>,
|
||||
alert_message: EventRegistry<AlertMessageEventArgs>,
|
||||
load_url: EventRegistry<LoadUrlEventArgs>,
|
||||
mean_collision: EventRegistry<MeanCollisionEventArgs>,
|
||||
script_control_change: EventRegistry<ScriptControlEventArgs>,
|
||||
script_dialog: EventRegistry<ScriptDialogEventArgs>,
|
||||
script_question: EventRegistry<ScriptQuestionEventArgs>,
|
||||
script_sensor_reply: EventRegistry<ScriptSensorReplyEventArgs>,
|
||||
}
|
||||
|
||||
impl Default for AgentManagerInner {
|
||||
@@ -370,8 +388,17 @@ impl Default for AgentManagerInner {
|
||||
disposed: AtomicBool::new(false),
|
||||
agent_id: RwLock::new(UUID::zero()),
|
||||
session_id: RwLock::new(UUID::zero()),
|
||||
secure_session_id: RwLock::new(UUID::zero()),
|
||||
first_name: RwLock::new(String::new()),
|
||||
last_name: RwLock::new(String::new()),
|
||||
start_location: RwLock::new(String::new()),
|
||||
look_at: RwLock::new(Vector3::zero()),
|
||||
home_region_handle: RwLock::new(0),
|
||||
home_look_at: RwLock::new(Vector3::zero()),
|
||||
local_id: RwLock::new(0),
|
||||
collision_plane: RwLock::new(Vector4::zero()),
|
||||
health: RwLock::new(0.0),
|
||||
height_width_generation: AtomicU32::new(0),
|
||||
active_group: RwLock::new(UUID::zero()),
|
||||
active_group_powers: RwLock::new(crate::GroupPowers(0)),
|
||||
agent_access: RwLock::new(String::new()),
|
||||
@@ -397,6 +424,7 @@ impl Default for AgentManagerInner {
|
||||
network: Mutex::new(None),
|
||||
raw_packet_subscription: Mutex::new(None),
|
||||
caps_subscriptions: Mutex::new(Vec::new()),
|
||||
login_callback: Mutex::new(None),
|
||||
movement: OnceLock::new(),
|
||||
chat_from_simulator: EventRegistry::default(),
|
||||
money_balance: EventRegistry::default(),
|
||||
@@ -415,6 +443,13 @@ impl Default for AgentManagerInner {
|
||||
set_display_name_reply: EventRegistry::default(),
|
||||
nav_mesh_status_update: EventRegistry::default(),
|
||||
mute_list_updated: EventRegistry::default(),
|
||||
alert_message: EventRegistry::default(),
|
||||
load_url: EventRegistry::default(),
|
||||
mean_collision: EventRegistry::default(),
|
||||
script_control_change: EventRegistry::default(),
|
||||
script_dialog: EventRegistry::default(),
|
||||
script_question: EventRegistry::default(),
|
||||
script_sensor_reply: EventRegistry::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,6 +479,31 @@ impl AgentManagerInner {
|
||||
crate::packets::PacketType::AvatarAnimation => {
|
||||
self.handle_avatar_animation_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::AlertMessage => {
|
||||
self.handle_alert_message_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::AgentAlertMessage => {
|
||||
self.handle_agent_alert_message_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::HealthMessage => {
|
||||
self.handle_health_message_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::LoadURL => self.handle_load_url_packet(&event.data),
|
||||
crate::packets::PacketType::MeanCollisionAlert => {
|
||||
self.handle_mean_collision_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::ScriptControlChange => {
|
||||
self.handle_script_control_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::ScriptDialog => {
|
||||
self.handle_script_dialog_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::ScriptQuestion => {
|
||||
self.handle_script_question_packet(&event.data, event.simulator)
|
||||
}
|
||||
crate::packets::PacketType::ScriptSensorReply => {
|
||||
self.handle_script_sensor_reply_packet(&event.data)
|
||||
}
|
||||
crate::packets::PacketType::MuteListUpdate => {
|
||||
self.handle_mute_list_update_packet(&event.data, event.simulator)
|
||||
}
|
||||
@@ -458,6 +518,164 @@ impl AgentManagerInner {
|
||||
let _ = result;
|
||||
}
|
||||
|
||||
fn handle_alert_message_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::AlertMessagePacket>(data)?;
|
||||
let (notification_id, extra_params) =
|
||||
packet
|
||||
.alert_info
|
||||
.first()
|
||||
.map_or(Ok::<_, Error>((None, None)), |info| {
|
||||
let extra_params = if info.extra_params.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match OSDParser::deserialize_with_bytes(info.extra_params.clone())? {
|
||||
OSD::Map(map) => Some(OSDMap::new_with_dictionary(map)?),
|
||||
_ => return Err(Error::InvalidOperation),
|
||||
}
|
||||
};
|
||||
Ok((Some(wire_string(info.message.clone())), extra_params))
|
||||
})?;
|
||||
self.alert_message.emit(AlertMessageEventArgs::new(
|
||||
wire_string(packet.alert_data.message),
|
||||
notification_id,
|
||||
extra_params,
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_agent_alert_message_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::AgentAlertMessagePacket>(data)?;
|
||||
self.alert_message.emit(AlertMessageEventArgs::new(
|
||||
wire_string(packet.alert_data.message),
|
||||
None,
|
||||
None,
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_health_message_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::HealthMessagePacket>(data)?;
|
||||
*self
|
||||
.health
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = packet.health_data.health;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_load_url_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::LoadURLPacket>(data)?;
|
||||
let data = packet.data;
|
||||
self.load_url.emit(LoadUrlEventArgs::new(
|
||||
wire_string(data.object_name),
|
||||
data.object_id,
|
||||
data.owner_id,
|
||||
data.owner_is_group,
|
||||
wire_string(data.message),
|
||||
wire_string(data.url),
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_mean_collision_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::MeanCollisionAlertPacket>(data)?;
|
||||
for collision in packet.mean_collision {
|
||||
let type_ = match collision.type_ {
|
||||
0 => crate::MeanCollisionType::None,
|
||||
1 => crate::MeanCollisionType::Bump,
|
||||
2 => crate::MeanCollisionType::LLPushObject,
|
||||
3 => crate::MeanCollisionType::SelectedObjectCollide,
|
||||
4 => crate::MeanCollisionType::ScriptedObjectCollide,
|
||||
5 => crate::MeanCollisionType::PhysicalObjectCollide,
|
||||
_ => return Err(Error::Argument),
|
||||
};
|
||||
self.mean_collision.emit(MeanCollisionEventArgs::new(
|
||||
type_,
|
||||
collision.perp,
|
||||
collision.victim,
|
||||
collision.mag,
|
||||
UNIX_EPOCH + Duration::from_secs(u64::from(collision.time)),
|
||||
)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_script_control_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::ScriptControlChangePacket>(data)?;
|
||||
for change in packet.data {
|
||||
self.script_control_change.emit(ScriptControlEventArgs::new(
|
||||
crate::ScriptControlChange(change.controls),
|
||||
change.pass_to_agent,
|
||||
change.take_controls,
|
||||
)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_script_dialog_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::ScriptDialogPacket>(data)?;
|
||||
let owner_id = packet
|
||||
.owner_data
|
||||
.first()
|
||||
.map_or(UUID::zero(), |owner| owner.owner_id);
|
||||
let buttons = packet
|
||||
.buttons
|
||||
.into_iter()
|
||||
.map(|button| wire_string(button.button_label))
|
||||
.collect();
|
||||
let dialog = packet.data;
|
||||
self.script_dialog.emit(ScriptDialogEventArgs::new(
|
||||
wire_string(dialog.message),
|
||||
wire_string(dialog.object_name),
|
||||
dialog.image_id,
|
||||
dialog.object_id,
|
||||
wire_string(dialog.first_name),
|
||||
wire_string(dialog.last_name),
|
||||
dialog.chat_channel,
|
||||
buttons,
|
||||
owner_id,
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_script_question_packet(
|
||||
&self,
|
||||
data: &[u8],
|
||||
simulator: Simulator,
|
||||
) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::ScriptQuestionPacket>(data)?;
|
||||
let question = packet.data;
|
||||
self.script_question.emit(ScriptQuestionEventArgs::new(
|
||||
simulator,
|
||||
question.task_id,
|
||||
question.item_id,
|
||||
wire_string(question.object_name),
|
||||
wire_string(question.object_owner),
|
||||
crate::ScriptPermission(question.questions),
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_script_sensor_reply_packet(&self, data: &[u8]) -> Result<(), Error> {
|
||||
let packet = decode_raw_packet::<crate::packets::ScriptSensorReplyPacket>(data)?;
|
||||
let requester_id = packet.requester.source_id;
|
||||
for sensed in packet.sensed_data {
|
||||
self.script_sensor_reply
|
||||
.emit(ScriptSensorReplyEventArgs::new(
|
||||
requester_id,
|
||||
sensed.group_id,
|
||||
wire_string(sensed.name),
|
||||
sensed.object_id,
|
||||
sensed.owner_id,
|
||||
sensed.position,
|
||||
sensed.range,
|
||||
sensed.rotation,
|
||||
crate::ScriptSensorTypeFlags(sensed.type_),
|
||||
sensed.velocity,
|
||||
)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_mute_list_update_packet(
|
||||
&self,
|
||||
data: &[u8],
|
||||
@@ -747,6 +965,81 @@ impl AgentManagerInner {
|
||||
)
|
||||
}
|
||||
|
||||
fn apply_login_response(&self, response: &crate::LoginResponseData) {
|
||||
*self
|
||||
.agent_id
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.agent_id();
|
||||
*self
|
||||
.session_id
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.session_id();
|
||||
*self
|
||||
.secure_session_id
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.secure_session_id();
|
||||
*self
|
||||
.first_name
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.first_name();
|
||||
*self
|
||||
.last_name
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.last_name();
|
||||
*self
|
||||
.start_location
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.start_location();
|
||||
*self
|
||||
.agent_access
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.agent_access();
|
||||
let look_at = response.look_at();
|
||||
*self
|
||||
.look_at
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = look_at;
|
||||
let home = response.home();
|
||||
*self
|
||||
.home_region_handle
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = home.region_handle;
|
||||
*self
|
||||
.home_look_at
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = home.look_at;
|
||||
*self
|
||||
.benefits
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.account_level_benefits();
|
||||
*self
|
||||
.active_gestures
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = response.gestures();
|
||||
if let Some(movement) = self.movement.get() {
|
||||
movement.set_home_position(home.position);
|
||||
let _ = movement.set_login_look_at(look_at);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_avatar_object_state(&self, local_id: u32, collision_plane: Vector4) {
|
||||
*self
|
||||
.local_id
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = local_id;
|
||||
*self
|
||||
.collision_plane
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = collision_plane;
|
||||
}
|
||||
|
||||
pub(crate) fn avatar_local_id(&self) -> u32 {
|
||||
*self
|
||||
.local_id
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn handle_caps_event(
|
||||
self: &Arc<Self>,
|
||||
client: Arc<GridClient>,
|
||||
@@ -1044,6 +1337,39 @@ impl AgentManager {
|
||||
Arc::downgrade(&inner),
|
||||
)
|
||||
}));
|
||||
{
|
||||
let mut callback = mutex(&inner.login_callback);
|
||||
if callback.is_none() {
|
||||
let weak = Arc::downgrade(&inner);
|
||||
let registered = crate::NetworkManagerLoginResponseCallback::from_handler(
|
||||
move |success, redirect, _message, _reason, response| {
|
||||
if success
|
||||
&& !redirect
|
||||
&& let (Some(inner), Some(response)) = (weak.upgrade(), response)
|
||||
{
|
||||
inner.apply_login_response(&response);
|
||||
}
|
||||
},
|
||||
);
|
||||
network.native_register_login_response_callback(
|
||||
registered.clone(),
|
||||
Some(vec![
|
||||
"agent_id".into(),
|
||||
"session_id".into(),
|
||||
"secure_session_id".into(),
|
||||
"first_name".into(),
|
||||
"last_name".into(),
|
||||
"start_location".into(),
|
||||
"agent_access".into(),
|
||||
"look_at".into(),
|
||||
"home".into(),
|
||||
"gestures".into(),
|
||||
"account_level_benefits".into(),
|
||||
]),
|
||||
)?;
|
||||
*callback = Some(registered);
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
agent_state_status: None,
|
||||
group_chat_sessions: Arc::clone(&inner.group_chat_sessions),
|
||||
@@ -1076,12 +1402,66 @@ impl AgentManager {
|
||||
self.inner.animations_changed.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_alert_message(
|
||||
&self,
|
||||
handler: EventHandler<AlertMessageEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.alert_message.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_load_url(
|
||||
&self,
|
||||
handler: EventHandler<LoadUrlEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.load_url.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_mean_collision(
|
||||
&self,
|
||||
handler: EventHandler<MeanCollisionEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.mean_collision.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_script_control_change(
|
||||
&self,
|
||||
handler: EventHandler<ScriptControlEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.script_control_change.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_script_dialog(
|
||||
&self,
|
||||
handler: EventHandler<ScriptDialogEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.script_dialog.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_script_question(
|
||||
&self,
|
||||
handler: EventHandler<ScriptQuestionEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.script_question.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_script_sensor_reply(
|
||||
&self,
|
||||
handler: EventHandler<ScriptSensorReplyEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.script_sensor_reply.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_dispose(&self) -> Result<(), Error> {
|
||||
if self.inner.disposed.swap(true, Ordering::AcqRel) {
|
||||
return Ok(());
|
||||
}
|
||||
mutex(&self.inner.raw_packet_subscription).take();
|
||||
mutex(&self.inner.caps_subscriptions).clear();
|
||||
if let Some(callback) = mutex(&self.inner.login_callback).take()
|
||||
&& let Some(network) = mutex(&self.inner.network).as_ref()
|
||||
{
|
||||
network.native_unregister_login_response_callback(&callback)?;
|
||||
}
|
||||
if let Some(movement) = self.inner.movement.get() {
|
||||
movement.stop();
|
||||
}
|
||||
@@ -1388,6 +1768,297 @@ impl AgentManager {
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
pub(crate) fn native_secure_session_id(&self) -> UUID {
|
||||
*self
|
||||
.inner
|
||||
.secure_session_id
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_secure_session_id(&self, value: UUID) {
|
||||
*self
|
||||
.inner
|
||||
.secure_session_id
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
pub(crate) fn native_start_location(&self) -> String {
|
||||
self.inner
|
||||
.start_location
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_start_location(&self, value: String) {
|
||||
*self
|
||||
.inner
|
||||
.start_location
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
pub(crate) fn native_look_at(&self) -> Vector3 {
|
||||
*self
|
||||
.inner
|
||||
.look_at
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_look_at(&self, value: Vector3) {
|
||||
*self
|
||||
.inner
|
||||
.look_at
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
let _ = self
|
||||
.inner
|
||||
.movement
|
||||
.get()
|
||||
.map(|movement| movement.set_login_look_at(value));
|
||||
}
|
||||
|
||||
pub(crate) fn native_home_region_handle(&self) -> u64 {
|
||||
*self
|
||||
.inner
|
||||
.home_region_handle
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_home_look_at(&self) -> Vector3 {
|
||||
*self
|
||||
.inner
|
||||
.home_look_at
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_local_id(&self) -> u32 {
|
||||
*self
|
||||
.inner
|
||||
.local_id
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_collision_plane(&self) -> Vector4 {
|
||||
*self
|
||||
.inner
|
||||
.collision_plane
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_health(&self) -> f32 {
|
||||
*self
|
||||
.inner
|
||||
.health
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_health(&self, value: f32) {
|
||||
*self
|
||||
.inner
|
||||
.health
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn native_grab(
|
||||
&self,
|
||||
object_local_id: u32,
|
||||
grab_offset: Vector3,
|
||||
uv_coord: Vector3,
|
||||
st_coord: Vector3,
|
||||
face_index: i32,
|
||||
position: Vector3,
|
||||
normal: Vector3,
|
||||
binormal: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::ObjectGrabPacket::new_with_constructor()?;
|
||||
packet.agent_data.agent_id = self.native_agent_id();
|
||||
packet.agent_data.session_id = self.native_session_id();
|
||||
packet.object_data.local_id = object_local_id;
|
||||
packet.object_data.grab_offset = grab_offset;
|
||||
let mut surface = crate::packets::ObjectGrabPacketSurfaceInfoBlock::new_with_constructor()?;
|
||||
surface.uv_coord = uv_coord;
|
||||
surface.st_coord = st_coord;
|
||||
surface.face_index = face_index;
|
||||
surface.position = position;
|
||||
surface.normal = normal;
|
||||
surface.binormal = binormal;
|
||||
packet.surface_info = vec![surface];
|
||||
self.send_current_packet(
|
||||
crate::packets::PacketType::ObjectGrab,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn native_grab_update(
|
||||
&self,
|
||||
object_id: UUID,
|
||||
grab_position: Vector3,
|
||||
grab_offset: Vector3,
|
||||
uv_coord: Vector3,
|
||||
st_coord: Vector3,
|
||||
face_index: i32,
|
||||
position: Vector3,
|
||||
normal: Vector3,
|
||||
binormal: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::ObjectGrabUpdatePacket::new_with_constructor()?;
|
||||
packet.agent_data.agent_id = self.native_agent_id();
|
||||
packet.agent_data.session_id = self.native_session_id();
|
||||
packet.object_data.object_id = object_id;
|
||||
packet.object_data.grab_offset_initial = grab_offset;
|
||||
packet.object_data.grab_position = grab_position;
|
||||
packet.object_data.time_since_last = 0;
|
||||
let mut surface =
|
||||
crate::packets::ObjectGrabUpdatePacketSurfaceInfoBlock::new_with_constructor()?;
|
||||
surface.uv_coord = uv_coord;
|
||||
surface.st_coord = st_coord;
|
||||
surface.face_index = face_index;
|
||||
surface.position = position;
|
||||
surface.normal = normal;
|
||||
surface.binormal = binormal;
|
||||
packet.surface_info = vec![surface];
|
||||
self.send_current_packet(
|
||||
crate::packets::PacketType::ObjectGrabUpdate,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn native_de_grab(
|
||||
&self,
|
||||
object_local_id: u32,
|
||||
uv_coord: Vector3,
|
||||
st_coord: Vector3,
|
||||
face_index: i32,
|
||||
position: Vector3,
|
||||
normal: Vector3,
|
||||
binormal: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::ObjectDeGrabPacket::new_with_constructor()?;
|
||||
packet.agent_data.agent_id = self.native_agent_id();
|
||||
packet.agent_data.session_id = self.native_session_id();
|
||||
packet.object_data.local_id = object_local_id;
|
||||
let mut surface =
|
||||
crate::packets::ObjectDeGrabPacketSurfaceInfoBlock::new_with_constructor()?;
|
||||
surface.uv_coord = uv_coord;
|
||||
surface.st_coord = st_coord;
|
||||
surface.face_index = face_index;
|
||||
surface.position = position;
|
||||
surface.normal = normal;
|
||||
surface.binormal = binormal;
|
||||
packet.surface_info = vec![surface];
|
||||
self.send_current_packet(
|
||||
crate::packets::PacketType::ObjectDeGrab,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
fn send_current_packet(
|
||||
&self,
|
||||
packet_type: crate::packets::PacketType,
|
||||
data: Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let simulator = self
|
||||
.client
|
||||
.network()
|
||||
.native_current_sim()
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
send_simulator_packet(&simulator, packet_type, data)
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_height_width(&self, height: u16, width: u16) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::AgentHeightWidthPacket::new_with_constructor()?;
|
||||
packet.agent_data.agent_id = self.native_agent_id();
|
||||
packet.agent_data.session_id = self.native_session_id();
|
||||
packet.agent_data.circuit_code = self.client.network().native_circuit_code();
|
||||
packet.height_width_block.height = height;
|
||||
packet.height_width_block.width = width;
|
||||
packet.height_width_block.gen_counter = self
|
||||
.inner
|
||||
.height_width_generation
|
||||
.fetch_add(1, Ordering::AcqRel);
|
||||
self.send_current_packet(
|
||||
crate::packets::PacketType::AgentHeightWidth,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_home(&self) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::SetStartLocationRequestPacket::new_with_constructor()?;
|
||||
packet.agent_data.agent_id = self.native_agent_id();
|
||||
packet.agent_data.session_id = self.native_session_id();
|
||||
packet.start_location_data.location_pos = self.native_movement_runtime().sim_position();
|
||||
packet.start_location_data.location_id = 1;
|
||||
packet.start_location_data.sim_name = Vec::new();
|
||||
packet.start_location_data.location_look_at =
|
||||
self.native_movement_runtime().camera_at_axis();
|
||||
self.send_current_packet(
|
||||
crate::packets::PacketType::SetStartLocationRequest,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn native_script_question_reply(
|
||||
&self,
|
||||
simulator: Simulator,
|
||||
item_id: UUID,
|
||||
task_id: UUID,
|
||||
permissions: crate::ScriptPermission,
|
||||
) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::ScriptAnswerYesPacket::new_with_constructor()?;
|
||||
packet.agent_data.agent_id = self.native_agent_id();
|
||||
packet.agent_data.session_id = self.native_session_id();
|
||||
packet.data.item_id = item_id;
|
||||
packet.data.task_id = task_id;
|
||||
packet.data.questions = permissions.0;
|
||||
send_simulator_packet(
|
||||
&simulator,
|
||||
crate::packets::PacketType::ScriptAnswerYes,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn native_request_script_sensor(
|
||||
&self,
|
||||
name: String,
|
||||
search_id: UUID,
|
||||
type_: crate::ScriptSensorTypeFlags,
|
||||
range: f32,
|
||||
arc: f32,
|
||||
request_id: UUID,
|
||||
simulator: Simulator,
|
||||
) -> Result<(), Error> {
|
||||
let mut packet = crate::packets::ScriptSensorRequestPacket::new_with_constructor()?;
|
||||
packet.requester.arc = arc;
|
||||
packet.requester.range = range;
|
||||
packet.requester.region_handle = simulator.handle;
|
||||
packet.requester.request_id = request_id;
|
||||
packet.requester.search_dir = libremetaverse_types::Quaternion::identity();
|
||||
packet.requester.search_id = search_id;
|
||||
packet.requester.search_name = name.into_bytes();
|
||||
packet.requester.search_pos = Vector3::zero();
|
||||
packet.requester.search_regions = 0;
|
||||
packet.requester.source_id = self.native_agent_id();
|
||||
packet.requester.type_ = type_.0;
|
||||
send_simulator_packet(
|
||||
&simulator,
|
||||
crate::packets::PacketType::ScriptSensorRequest,
|
||||
packet.to_bytes_with_method()?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn native_last_name(&self) -> String {
|
||||
self.inner
|
||||
.last_name
|
||||
@@ -4064,6 +4735,7 @@ fn osd_uuid_array_contains(map: &OSDMap, key: &str, id: UUID) -> Result<bool, Er
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::too_many_lines)] // Protocol scenarios remain readable as contiguous exchanges.
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
@@ -2388,6 +2388,18 @@ impl AgentMovementRuntime {
|
||||
read(&self.kinematics).home_position
|
||||
}
|
||||
|
||||
pub(crate) fn set_home_position(&self, value: Vector3) {
|
||||
write(&self.kinematics).home_position = value;
|
||||
}
|
||||
|
||||
pub(crate) fn set_login_look_at(&self, value: Vector3) -> Result<(), Error> {
|
||||
self.camera(128.0).look_direction_with_vector3(value)
|
||||
}
|
||||
|
||||
pub(crate) fn camera_at_axis(&self) -> Vector3 {
|
||||
self.camera(128.0).at_axis()
|
||||
}
|
||||
|
||||
pub(crate) fn last_position_update(&self) -> SystemTime {
|
||||
read(&self.kinematics).last_position_update
|
||||
}
|
||||
@@ -2853,6 +2865,7 @@ impl AgentManager {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::float_cmp)] // Wire fixtures use exactly representable values.
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::packets::{
|
||||
@@ -3431,7 +3444,7 @@ mod tests {
|
||||
let (prediction_sender, prediction_receiver) = mpsc::channel();
|
||||
let _prediction =
|
||||
manager.native_subscribe_region_crossing_predicted(Arc::new(move |event| {
|
||||
prediction_sender.send(event).unwrap()
|
||||
prediction_sender.send(event).unwrap();
|
||||
}));
|
||||
runtime.update_multi_simulator_state(&simulator);
|
||||
let prediction = prediction_receiver.try_recv().unwrap();
|
||||
|
||||
@@ -177,6 +177,13 @@ pub(crate) fn builtin_animations() -> std::collections::HashMap<UUID, String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn builtin_animation(name: &str) -> UUID {
|
||||
builtin_animations()
|
||||
.into_iter()
|
||||
.find_map(|(id, candidate)| (candidate == name).then_some(id))
|
||||
.unwrap_or_else(|| panic!("unknown built-in animation {name}"))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BinBVHJointKey {
|
||||
pub key_element: Vector3,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Exact Rust translations of LibreMetaverse.Tests/BakerTests.cs.
|
||||
// Source SHA-256: 7727140f0fbf2c77ed7a6f3f468798d8f7ba96c5d9889246a2bccd2d4bd8def2
|
||||
|
||||
#![allow(clippy::similar_names)] // Baker/baked terminology matches the API under test.
|
||||
|
||||
use super::*;
|
||||
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
|
||||
use libremetaverse_types::Color4;
|
||||
|
||||
@@ -159,6 +159,7 @@ pub(crate) struct AppearanceManagerInner {
|
||||
appearance_cancel: Mutex<Option<CancellationTokenSource>>,
|
||||
appearance_gate: Arc<tokio::sync::Semaphore>,
|
||||
events: AppearanceEvents,
|
||||
login_callback: Mutex<Option<crate::NetworkManagerLoginResponseCallback>>,
|
||||
network_subscriptions: Mutex<Vec<Subscription>>,
|
||||
}
|
||||
|
||||
@@ -232,11 +233,35 @@ fn handle_appearance_packet(
|
||||
match packet_type {
|
||||
PacketType::AgentWearablesUpdate => handle_wearables_update(inner, data),
|
||||
PacketType::AgentCachedTextureResponse => handle_cached_bakes(inner, data),
|
||||
PacketType::AvatarAppearance => handle_avatar_appearance(inner, data),
|
||||
PacketType::RebakeAvatarTextures => handle_rebake_request(inner, data),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_avatar_appearance(inner: &Arc<AppearanceManagerInner>, data: Vec<u8>) {
|
||||
let mut offset = 0;
|
||||
let Ok(packet) =
|
||||
crate::packets::AvatarAppearancePacket::new_with_bytes_int32(data, &mut offset)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = inner.client.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Ok(network) = client.native_network() else {
|
||||
return;
|
||||
};
|
||||
if packet.sender.id != network.native_agent_id() {
|
||||
return;
|
||||
}
|
||||
if let Some(appearance) = packet.appearance_data.first() {
|
||||
inner
|
||||
.last_cof_version
|
||||
.fetch_max(appearance.cof_version, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_wearables_update(inner: &Arc<AppearanceManagerInner>, data: Vec<u8>) {
|
||||
let mut offset = 0;
|
||||
let Ok(packet) = AgentWearablesUpdatePacket::new_with_bytes_int32(data, &mut offset) else {
|
||||
@@ -371,9 +396,31 @@ impl AppearanceManager {
|
||||
appearance_cancel: Mutex::new(None),
|
||||
appearance_gate: Arc::new(tokio::sync::Semaphore::new(1)),
|
||||
events: AppearanceEvents::default(),
|
||||
login_callback: Mutex::new(None),
|
||||
network_subscriptions: Mutex::new(Vec::new()),
|
||||
});
|
||||
let weak = Arc::downgrade(&inner);
|
||||
let login_callback = crate::NetworkManagerLoginResponseCallback::from_handler(
|
||||
move |success, redirect, _message, _reason, response| {
|
||||
if success
|
||||
&& !redirect
|
||||
&& let (Some(inner), Some(response)) = (weak.upgrade(), response)
|
||||
{
|
||||
inner.last_cof_version.fetch_max(
|
||||
i32::try_from(response.cof_version()).unwrap_or(i32::MAX),
|
||||
Ordering::AcqRel,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
client
|
||||
.native_network()?
|
||||
.native_register_login_response_callback(
|
||||
login_callback.clone(),
|
||||
Some(vec!["cof_version".to_owned()]),
|
||||
)?;
|
||||
*mutex(&inner.login_callback) = Some(login_callback);
|
||||
let weak = Arc::downgrade(&inner);
|
||||
let subscription = client
|
||||
.native_network()?
|
||||
.subscribe_raw_packet(Arc::new(move |event| {
|
||||
@@ -463,6 +510,12 @@ impl AppearanceManager {
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)] // The mapped Dispose API is fallible.
|
||||
pub(crate) fn native_dispose(&self) -> Result<(), Error> {
|
||||
if let Some(callback) = mutex(&self.inner.login_callback).take()
|
||||
&& let Some(client) = self.inner.client.upgrade()
|
||||
&& let Ok(network) = client.native_network()
|
||||
{
|
||||
network.native_unregister_login_response_callback(&callback)?;
|
||||
}
|
||||
self.inner.disposed.store(true, Ordering::Release);
|
||||
write(&self.inner.wearables).clear();
|
||||
self.inner
|
||||
@@ -1049,6 +1102,70 @@ impl AppearanceManager {
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn native_apply_archetype(
|
||||
&self,
|
||||
archetype: crate::GenepoolArchetype,
|
||||
) -> Result<(), Error> {
|
||||
let parameters = archetype.params();
|
||||
{
|
||||
let mut assets = write(&self.inner.wearable_assets);
|
||||
for asset in assets.values_mut() {
|
||||
for parameter in ¶meters {
|
||||
if let Some(value) = asset.params.get_mut(¶meter.id()) {
|
||||
*value = parameter.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.inner
|
||||
.server_visual_parameters
|
||||
.store(false, Ordering::Release);
|
||||
self.native_request_set_appearance(true).await
|
||||
}
|
||||
|
||||
pub(crate) async fn native_blend_to_archetype(
|
||||
&self,
|
||||
archetype: crate::GenepoolArchetype,
|
||||
amount: f32,
|
||||
) -> Result<(), Error> {
|
||||
if !amount.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if amount <= 0.0 {
|
||||
return Ok(());
|
||||
}
|
||||
let amount = amount.min(1.0);
|
||||
let parameters = archetype.params();
|
||||
{
|
||||
let mut assets = write(&self.inner.wearable_assets);
|
||||
for asset in assets.values_mut() {
|
||||
for parameter in ¶meters {
|
||||
if let Some(current) = asset.params.get_mut(¶meter.id()) {
|
||||
*current += amount * (parameter.value() - *current);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.inner
|
||||
.server_visual_parameters
|
||||
.store(false, Ordering::Release);
|
||||
self.native_request_set_appearance(true).await
|
||||
}
|
||||
|
||||
pub(crate) async fn native_randomize_appearance(
|
||||
&self,
|
||||
random: Option<libremetaverse_types::compat::RandomSource>,
|
||||
) -> Result<(), Error> {
|
||||
let archetypes = crate::Genepool::archetypes();
|
||||
let maximum = i32::try_from(archetypes.len()).map_err(|_| Error::IndexOutOfRange)?;
|
||||
let index = random.unwrap_or_default().next(maximum)?;
|
||||
let archetype = archetypes
|
||||
.into_iter()
|
||||
.nth(usize::try_from(index).map_err(|_| Error::IndexOutOfRange)?)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
self.native_apply_archetype(archetype).await
|
||||
}
|
||||
|
||||
pub(crate) fn native_server_baking_region(&self) -> Result<bool, Error> {
|
||||
let protocols = self
|
||||
.client()?
|
||||
@@ -2303,7 +2420,7 @@ mod tests {
|
||||
let delivered_handler = Arc::clone(&delivered);
|
||||
let releasing = Arc::clone(&released);
|
||||
let healthy = registry.subscribe(Arc::new(move |value| {
|
||||
let _keep_alive = &releasing;
|
||||
std::hint::black_box(&releasing);
|
||||
delivered_handler.fetch_add(value as u64, Ordering::AcqRel);
|
||||
}));
|
||||
drop(released);
|
||||
@@ -2555,6 +2672,40 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_avatar_appearance_advances_cof_version_monotonically() {
|
||||
let client = GridClient::new().unwrap();
|
||||
let manager = client.appearance();
|
||||
let mut packet = crate::packets::AvatarAppearancePacket::new_with_constructor().unwrap();
|
||||
packet.sender.id = client.network().native_agent_id();
|
||||
let mut appearance =
|
||||
crate::packets::AvatarAppearancePacketAppearanceDataBlock::new_with_constructor()
|
||||
.unwrap();
|
||||
appearance.cof_version = 17;
|
||||
packet.appearance_data.push(appearance);
|
||||
|
||||
handle_appearance_packet(
|
||||
&manager.inner,
|
||||
PacketType::AvatarAppearance,
|
||||
packet.to_bytes_with_method().unwrap(),
|
||||
);
|
||||
assert_eq!(manager.native_last_cof_version(), 17);
|
||||
|
||||
let mut older = crate::packets::AvatarAppearancePacket::new_with_constructor().unwrap();
|
||||
older.sender.id = client.network().native_agent_id();
|
||||
let mut older_data =
|
||||
crate::packets::AvatarAppearancePacketAppearanceDataBlock::new_with_constructor()
|
||||
.unwrap();
|
||||
older_data.cof_version = 3;
|
||||
older.appearance_data.push(older_data);
|
||||
handle_appearance_packet(
|
||||
&manager.inner,
|
||||
PacketType::AvatarAppearance,
|
||||
older.to_bytes_with_method().unwrap(),
|
||||
);
|
||||
assert_eq!(manager.native_last_cof_version(), 17);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn failed_appearance_request_emits_failure_and_empty_cache_request_does_not_wait() {
|
||||
let client = GridClient::new().unwrap();
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
// inputs intentionally and RegionSettings intentionally exposes boolean fields.
|
||||
#![allow(clippy::pedantic)]
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use base64::Engine as _;
|
||||
use flate2::{Compression, GzBuilder, read::GzDecoder};
|
||||
use libremetaverse_types::compat::ReadWrite;
|
||||
use libremetaverse_types::compat::{IDictionary, ReadWrite, XmlTextWriter};
|
||||
use libremetaverse_types::{AssetType, Error, UUID};
|
||||
|
||||
use crate::assets::TarArchiveReaderTarEntryType;
|
||||
@@ -305,6 +308,34 @@ pub struct TarArchiveWriter {
|
||||
state: Mutex<WriterState>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AssetsArchiver {
|
||||
assets: std::collections::HashMap<UUID, crate::assets::Asset>,
|
||||
}
|
||||
|
||||
impl AssetsArchiver {
|
||||
pub fn new(assets: IDictionary<UUID, crate::assets::Asset>) -> Result<Self, Error> {
|
||||
if assets.0.len() > MAX_ENTRIES {
|
||||
return Err(archive_error("asset count limit"));
|
||||
}
|
||||
Ok(Self { assets: assets.0 })
|
||||
}
|
||||
|
||||
pub fn archive(&self, archive: TarArchiveWriter) -> Result<(), Error> {
|
||||
let mut assets = self.assets.iter().collect::<Vec<_>>();
|
||||
assets.sort_by_key(|(id, _)| id.to_string());
|
||||
for (id, asset) in assets {
|
||||
asset.encode()?;
|
||||
let extension = asset_extension(asset.asset_type()).unwrap_or_default();
|
||||
archive.write_file_with_string_bytes(
|
||||
format!("assets/{id}{extension}"),
|
||||
asset.asset_data.clone(),
|
||||
)?;
|
||||
}
|
||||
archive.close()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OarFileAssetLoadedCallback(
|
||||
std::sync::Arc<dyn Fn(crate::assets::Asset, i64, i64) + Send + Sync>,
|
||||
@@ -833,6 +864,227 @@ fn prepare_output_directory(path: &str) -> Result<PathBuf, Error> {
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
fn reset_output_directory(path: &str) -> Result<PathBuf, Error> {
|
||||
let root = PathBuf::from(path);
|
||||
if root.exists() {
|
||||
let metadata = fs::symlink_metadata(&root).map_err(io_error)?;
|
||||
if metadata.file_type().is_symlink() || !metadata.is_dir() {
|
||||
return Err(archive_error("archive output root"));
|
||||
}
|
||||
fs::remove_dir_all(&root).map_err(io_error)?;
|
||||
}
|
||||
fs::create_dir_all(&root).map_err(io_error)?;
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
pub(crate) fn load_objects(
|
||||
object_data: Vec<u8>,
|
||||
object_callback: OarFileSceneObjectLoadedCallback,
|
||||
bytes_read: i64,
|
||||
total_bytes: i64,
|
||||
) -> Result<(), Error> {
|
||||
if object_data.len() > MAX_ENTRY_BYTES {
|
||||
return Err(archive_error("object XML size limit"));
|
||||
}
|
||||
let xml = std::str::from_utf8(&object_data).map_err(|_| archive_error("object XML UTF-8"))?;
|
||||
let document = roxmltree::Document::parse(xml).map_err(|_| archive_error("object XML"))?;
|
||||
let root = document.root_element();
|
||||
let nodes = if root.tag_name().name() == "scene" {
|
||||
root.children()
|
||||
.filter(roxmltree::Node::is_element)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![root]
|
||||
};
|
||||
if nodes.len() > MAX_ENTRIES {
|
||||
return Err(archive_error("object count limit"));
|
||||
}
|
||||
for node in nodes {
|
||||
let range = node.range();
|
||||
let linkset = crate::assets::AssetPrim::new_with_string(xml[range].to_owned())?;
|
||||
object_callback.invoke(linkset, bytes_read, total_bytes)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn sog_to_xml2(
|
||||
mut writer: XmlTextWriter,
|
||||
prim: crate::assets::AssetPrim,
|
||||
) -> Result<(), Error> {
|
||||
let xml = prim.encode_xml()?;
|
||||
writer.0.push_str(
|
||||
xml.strip_prefix("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
|
||||
.unwrap_or(&xml),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn xml_escape(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
fn push_element(output: &mut String, name: &str, value: impl std::fmt::Display) {
|
||||
let _ = write!(
|
||||
output,
|
||||
"<{name}>{}</{name}>",
|
||||
xml_escape(&value.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
fn push_vector(output: &mut String, name: &str, value: libremetaverse_types::Vector3) {
|
||||
let _ = write!(
|
||||
output,
|
||||
"<{name}><X>{}</X><Y>{}</Y><Z>{}</Z></{name}>",
|
||||
value.x, value.y, value.z
|
||||
);
|
||||
}
|
||||
|
||||
fn parcel_xml(parcel: &crate::Parcel, global_id: UUID) -> String {
|
||||
let mut output = String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?><LandData>");
|
||||
push_element(&mut output, "Area", parcel.area);
|
||||
push_element(&mut output, "AuctionID", parcel.auction_id);
|
||||
push_element(&mut output, "AuthBuyerID", parcel.auth_buyer_id);
|
||||
push_element(&mut output, "Category", parcel.category as i8);
|
||||
push_element(
|
||||
&mut output,
|
||||
"ClaimDate",
|
||||
parcel
|
||||
.claim_date
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs(),
|
||||
);
|
||||
push_element(&mut output, "ClaimPrice", parcel.claim_price);
|
||||
push_element(&mut output, "GlobalID", global_id);
|
||||
push_element(&mut output, "GroupID", parcel.group_id);
|
||||
push_element(&mut output, "IsGroupOwned", parcel.is_group_owned);
|
||||
push_element(
|
||||
&mut output,
|
||||
"Bitmap",
|
||||
base64::engine::general_purpose::STANDARD.encode(&parcel.bitmap),
|
||||
);
|
||||
push_element(&mut output, "Description", &parcel.desc);
|
||||
push_element(&mut output, "Flags", parcel.flags.0);
|
||||
push_element(&mut output, "LandingType", parcel.landing as u8);
|
||||
push_element(&mut output, "Name", &parcel.name);
|
||||
push_element(&mut output, "Status", parcel.status as i8);
|
||||
push_element(&mut output, "LocalID", parcel.local_id);
|
||||
push_element(
|
||||
&mut output,
|
||||
"MediaAutoScale",
|
||||
u8::from(parcel.media.media_auto_scale),
|
||||
);
|
||||
push_element(&mut output, "MediaID", parcel.media.media_id);
|
||||
push_element(&mut output, "MediaURL", &parcel.media.media_url);
|
||||
push_element(&mut output, "MusicURL", &parcel.music_url);
|
||||
push_element(&mut output, "OwnerID", parcel.owner_id);
|
||||
output.push_str("<ParcelAccessList>");
|
||||
for entry in parcel
|
||||
.access_black_list
|
||||
.iter()
|
||||
.chain(&parcel.access_white_list)
|
||||
{
|
||||
output.push_str("<ParcelAccessEntry>");
|
||||
push_element(&mut output, "AgentID", entry.agent_id);
|
||||
push_element(
|
||||
&mut output,
|
||||
"Time",
|
||||
entry
|
||||
.time
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs(),
|
||||
);
|
||||
push_element(&mut output, "AccessList", entry.flags.0);
|
||||
output.push_str("</ParcelAccessEntry>");
|
||||
}
|
||||
output.push_str("</ParcelAccessList>");
|
||||
push_element(&mut output, "PassHours", parcel.pass_hours);
|
||||
push_element(&mut output, "PassPrice", parcel.pass_price);
|
||||
push_element(&mut output, "SalePrice", parcel.sale_price);
|
||||
push_element(&mut output, "SnapshotID", parcel.snapshot_id);
|
||||
push_vector(&mut output, "UserLocation", parcel.user_location);
|
||||
push_vector(&mut output, "UserLookAt", parcel.user_look_at);
|
||||
push_element(&mut output, "Dwell", parcel.dwell);
|
||||
push_element(&mut output, "OtherCleanTime", parcel.other_clean_time);
|
||||
output.push_str("</LandData>");
|
||||
output
|
||||
}
|
||||
|
||||
pub(crate) fn save_parcels(sim: crate::Simulator, parcel_path: String) -> Result<(), Error> {
|
||||
let root = reset_output_directory(&parcel_path)?;
|
||||
let mut parcels = sim
|
||||
.native_parcels_snapshot()
|
||||
.into_values()
|
||||
.collect::<Vec<_>>();
|
||||
parcels.sort_by_key(|parcel| parcel.local_id);
|
||||
if parcels.len() > MAX_ENTRIES {
|
||||
return Err(archive_error("parcel count limit"));
|
||||
}
|
||||
for parcel in parcels {
|
||||
let global_id = UUID::random()?;
|
||||
fs::write(
|
||||
root.join(format!("{global_id}.xml")),
|
||||
parcel_xml(&parcel, global_id),
|
||||
)
|
||||
.map_err(io_error)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_prim_textures(prim: &crate::prim_object::PrimObject, textures: &mut Vec<UUID>) {
|
||||
if let Some(entry) = &prim.textures {
|
||||
if let Some(default) = &entry.default_texture {
|
||||
textures.push(default.texture_id());
|
||||
}
|
||||
textures.extend(
|
||||
entry
|
||||
.face_textures
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(crate::PrimitiveTextureEntryFace::texture_id),
|
||||
);
|
||||
}
|
||||
textures.push(prim.sculpt.texture);
|
||||
textures.push(prim.particles.texture_id);
|
||||
}
|
||||
|
||||
pub(crate) async fn save_prims(
|
||||
manager: crate::AssetManager,
|
||||
prims: Vec<crate::assets::AssetPrim>,
|
||||
prims_path: String,
|
||||
assets_path: String,
|
||||
) -> Result<(), Error> {
|
||||
if prims.len() > MAX_ENTRIES {
|
||||
return Err(archive_error("linkset count limit"));
|
||||
}
|
||||
let root = reset_output_directory(&prims_path)?;
|
||||
let mut textures = Vec::new();
|
||||
for prim in prims {
|
||||
let Some(parent) = &prim.parent else {
|
||||
continue;
|
||||
};
|
||||
fs::write(
|
||||
root.join(format!("Primitive_{}.xml", parent.id)),
|
||||
prim.encode_xml()?,
|
||||
)
|
||||
.map_err(io_error)?;
|
||||
collect_prim_textures(parent, &mut textures);
|
||||
for child in &prim.children {
|
||||
collect_prim_textures(child, &mut textures);
|
||||
}
|
||||
}
|
||||
textures.retain(|id| *id != UUID::zero());
|
||||
textures.sort_by_key(UUID::to_string);
|
||||
textures.dedup();
|
||||
save_assets(manager, AssetType::Texture, textures, assets_path).await
|
||||
}
|
||||
|
||||
fn write_asset(root: &Path, id: UUID, extension: &str, data: &[u8]) -> Result<(), Error> {
|
||||
if data.len() > MAX_ENTRY_BYTES {
|
||||
return Err(archive_error("asset size limit"));
|
||||
@@ -1201,10 +1453,42 @@ pub(crate) fn save_region_settings(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
BLOCK, MAX_ENTRY_BYTES, MAX_PATH_DEPTH, TarArchiveReader, padded_octal,
|
||||
AssetsArchiver, BLOCK, MAX_ENTRY_BYTES, MAX_PATH_DEPTH, OarFileSceneObjectLoadedCallback,
|
||||
TarArchiveReader, TarArchiveWriter, load_objects, padded_octal, sog_to_xml2,
|
||||
validate_archive_path,
|
||||
};
|
||||
use crate::assets::TarArchiveReaderTarEntryType;
|
||||
use crate::assets::{Asset, AssetPrim, TarArchiveReaderTarEntryType};
|
||||
use crate::prim_object::PrimObject;
|
||||
use libremetaverse_types::compat::{IDictionary, XmlTextWriter};
|
||||
use libremetaverse_types::{AssetType, UUID};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct SharedCursor(Arc<Mutex<Cursor<Vec<u8>>>>);
|
||||
|
||||
impl Read for SharedCursor {
|
||||
fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.0.lock().expect("cursor").read(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for SharedCursor {
|
||||
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
|
||||
self.0.lock().expect("cursor").write(buffer)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.0.lock().expect("cursor").flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for SharedCursor {
|
||||
fn seek(&mut self, position: SeekFrom) -> std::io::Result<u64> {
|
||||
self.0.lock().expect("cursor").seek(position)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_absolute_parent_and_windows_style_archive_paths() {
|
||||
@@ -1258,4 +1542,53 @@ mod tests {
|
||||
let mut kind = TarArchiveReaderTarEntryType::TYPE_UNKNOWN;
|
||||
assert!(reader.read_entry(&mut path, &mut kind).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assets_archiver_writes_encoded_assets_and_closes_the_tar() {
|
||||
let id = UUID::new_with_u_int64(42).expect("asset id");
|
||||
let asset = Asset::native_new(AssetType::Texture, id, vec![1, 2, 3]).expect("asset");
|
||||
let archiver = AssetsArchiver::new(IDictionary(HashMap::from([(id, asset)])))
|
||||
.expect("assets archiver");
|
||||
let shared = SharedCursor::default();
|
||||
let writer = TarArchiveWriter::new(Box::new(shared.clone())).expect("tar writer");
|
||||
archiver.archive(writer).expect("archive assets");
|
||||
|
||||
let bytes = shared.0.lock().expect("cursor").get_ref().clone();
|
||||
let reader = TarArchiveReader::new(Box::new(Cursor::new(bytes))).expect("tar reader");
|
||||
let mut path = String::new();
|
||||
let mut kind = TarArchiveReaderTarEntryType::TYPE_UNKNOWN;
|
||||
assert_eq!(
|
||||
reader.read_entry(&mut path, &mut kind).expect("tar entry"),
|
||||
Some(vec![1, 2, 3])
|
||||
);
|
||||
assert_eq!(path, format!("assets/{id}_texture.jp2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_loader_handles_scene_wrappers_and_xml2_writer_accepts_linksets() {
|
||||
let mut parent = PrimObject::new().expect("parent");
|
||||
parent.id = UUID::new_with_u_int64(7).expect("parent id");
|
||||
parent.name = "archive fixture".into();
|
||||
let prim = AssetPrim::new_with_prim_object_list(parent, Vec::new()).expect("linkset");
|
||||
let encoded = prim.encode_xml().expect("linkset XML");
|
||||
let group = encoded
|
||||
.strip_prefix("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
|
||||
.expect("XML declaration");
|
||||
let scene = format!("<scene>{group}</scene>");
|
||||
let loaded = Arc::new(Mutex::new(Vec::new()));
|
||||
let observed = Arc::clone(&loaded);
|
||||
let callback = OarFileSceneObjectLoadedCallback::from_callback(move |prim, read, total| {
|
||||
observed.lock().expect("loaded").push((prim, read, total));
|
||||
});
|
||||
|
||||
load_objects(scene.into_bytes(), callback, 12, 24).expect("load objects");
|
||||
let loaded = loaded.lock().expect("loaded");
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert_eq!(
|
||||
loaded[0].0.parent.as_ref().expect("parent").name,
|
||||
"archive fixture"
|
||||
);
|
||||
assert_eq!((loaded[0].1, loaded[0].2), (12, 24));
|
||||
sog_to_xml2(XmlTextWriter(String::new()), loaded[0].0.clone()).expect("write XML2");
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ mod event_slot_tests {
|
||||
let delivered_handler = Arc::clone(&delivered);
|
||||
let releasing = Arc::clone(&released);
|
||||
let healthy = slot.subscribe(Arc::new(move |value| {
|
||||
let _keep_alive = &releasing;
|
||||
std::hint::black_box(&releasing);
|
||||
delivered_handler.fetch_add(value as u64, Ordering::AcqRel);
|
||||
}));
|
||||
drop(released);
|
||||
@@ -1046,6 +1046,20 @@ impl AssetManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_image_transfer_count(&self) -> i32 {
|
||||
i32::try_from(mutex(&self.inner.image_cancellations).len()).unwrap_or(i32::MAX)
|
||||
}
|
||||
|
||||
pub(crate) fn native_cancel_image_requests(&self) {
|
||||
let requests = mutex(&self.inner.image_cancellations)
|
||||
.drain()
|
||||
.map(|(_, source)| source)
|
||||
.collect::<Vec<_>>();
|
||||
for source in requests {
|
||||
source.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_server_baked_image(
|
||||
&self,
|
||||
avatar_id: UUID,
|
||||
|
||||
@@ -465,28 +465,35 @@ impl AssetMesh {
|
||||
})
|
||||
}
|
||||
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||
let raw = RawAsset::with_data(AssetType::Mesh, asset_id, asset_data.clone())?;
|
||||
let mesh_data = match OSDParser::deserialize_llsd_binary_with_bytes(asset_data)? {
|
||||
OSD::Map(map) => OSDMap::new_with_dictionary(map)?,
|
||||
_ => return Err(Error::Argument),
|
||||
};
|
||||
Ok(Self { raw, mesh_data })
|
||||
Ok(Self {
|
||||
raw: RawAsset::with_data(AssetType::Mesh, asset_id, asset_data)?,
|
||||
mesh_data: OSDMap::new_with_constructor()?,
|
||||
})
|
||||
}
|
||||
pub fn decode(&self) -> Result<bool, Error> {
|
||||
Ok(matches!(
|
||||
OSDParser::deserialize_llsd_binary_with_bytes(self.raw.bytes()),
|
||||
Ok(OSD::Map(_))
|
||||
))
|
||||
self.mesh_data.clear()?;
|
||||
let Ok(decoded) = crate::mesh_asset::decode_asset_map(&self.raw.bytes(), UUID::zero())
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
for entry in decoded.snapshot() {
|
||||
self.mesh_data.add_with_key_value_pair(entry)?;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
pub fn encode(&self) -> Result<(), Error> {
|
||||
self.raw
|
||||
.replace(OSDParser::serialize_llsd_binary_with_osd(OSD::Map(
|
||||
self.mesh_data.snapshot(),
|
||||
))?)
|
||||
// LLMesh encoding from arbitrary decoded domains is intentionally not
|
||||
// defined by the pinned API. Preserve the original asset bytes, just
|
||||
// as the reference implementation's sealed no-op encoder does.
|
||||
Ok(())
|
||||
}
|
||||
pub const fn asset_type(&self) -> AssetType {
|
||||
AssetType::Mesh
|
||||
}
|
||||
|
||||
pub(crate) fn native_bytes(&self) -> Vec<u8> {
|
||||
self.raw.bytes()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AssetTexture {
|
||||
|
||||
@@ -32,7 +32,9 @@ fn asset_models_retain_type_and_reject_malformed_meshes() {
|
||||
.unwrap();
|
||||
assert_eq!(asset.asset_type(), AssetType::Sound);
|
||||
assert!(asset.decode().unwrap());
|
||||
assert!(AssetMesh::new_with_uuid_bytes(UUID::random().unwrap(), b"not-llsd".to_vec()).is_err());
|
||||
let malformed =
|
||||
AssetMesh::new_with_uuid_bytes(UUID::random().unwrap(), b"not-llsd".to_vec()).unwrap();
|
||||
assert!(!malformed.decode().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,774 @@
|
||||
//! OpenSim-compatible XML2 linkset assets.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
#![allow(clippy::too_many_lines)]
|
||||
#![allow(clippy::format_push_string)]
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use base64::Engine as _;
|
||||
use libremetaverse_types::compat::XmlTextReader;
|
||||
use libremetaverse_types::{AssetType, Color4, InventoryType, Quaternion, UUID, Vector3};
|
||||
use roxmltree::Node;
|
||||
|
||||
use crate::prim_object::{PrimObject, PrimObjectInventoryBlockItemBlock, asset_type};
|
||||
use crate::{Error, Primitive, PrimitiveTextureEntry};
|
||||
|
||||
const MAX_XML_BYTES: usize = 64 * 1024 * 1024;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AssetPrim {
|
||||
pub children: Vec<PrimObject>,
|
||||
pub parent: Option<PrimObject>,
|
||||
asset_data: Vec<u8>,
|
||||
asset_id: UUID,
|
||||
}
|
||||
|
||||
fn escape(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
fn element(output: &mut String, name: &str, value: impl std::fmt::Display) {
|
||||
let _ = write!(output, "<{name}>{}</{name}>", escape(&value.to_string()));
|
||||
}
|
||||
|
||||
fn uuid_element(output: &mut String, name: &str, value: UUID) {
|
||||
let _ = write!(output, "<{name}><UUID>{value}</UUID></{name}>");
|
||||
}
|
||||
|
||||
fn vector_element(output: &mut String, name: &str, value: Vector3) {
|
||||
let _ = write!(
|
||||
output,
|
||||
"<{name}><X>{}</X><Y>{}</Y><Z>{}</Z></{name}>",
|
||||
value.x, value.y, value.z
|
||||
);
|
||||
}
|
||||
|
||||
fn quaternion_element(output: &mut String, name: &str, value: Quaternion) {
|
||||
let _ = write!(
|
||||
output,
|
||||
"<{name}><X>{}</X><Y>{}</Y><Z>{}</Z><W>{}</W></{name}>",
|
||||
value.x, value.y, value.z, value.w
|
||||
);
|
||||
}
|
||||
|
||||
fn color_element(output: &mut String, name: &str, value: Color4) {
|
||||
let _ = write!(
|
||||
output,
|
||||
"<{name}><R>{}</R><G>{}</G><B>{}</B><A>{}</A></{name}>",
|
||||
value.r, value.g, value.b, value.a
|
||||
);
|
||||
}
|
||||
|
||||
fn unix_seconds(value: SystemTime) -> u64 {
|
||||
value
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or(Duration::ZERO)
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn child<'a, 'input>(node: Node<'a, 'input>, name: &str) -> Option<Node<'a, 'input>> {
|
||||
node.children()
|
||||
.find(|candidate| candidate.is_element() && candidate.tag_name().name() == name)
|
||||
}
|
||||
|
||||
fn text(node: Node<'_, '_>, name: &str) -> Option<String> {
|
||||
child(node, name)
|
||||
.and_then(|value| value.text())
|
||||
.map(str::trim)
|
||||
.map(str::to_owned)
|
||||
}
|
||||
|
||||
fn parsed<T>(node: Node<'_, '_>, name: &str, default: T) -> Result<T, Error>
|
||||
where
|
||||
T: FromStr,
|
||||
{
|
||||
text(node, name).map_or(Ok(default), |value| {
|
||||
value.parse().map_err(|_| Error::Argument)
|
||||
})
|
||||
}
|
||||
|
||||
fn boolean(node: Node<'_, '_>, name: &str, default: bool) -> Result<bool, Error> {
|
||||
text(node, name).map_or(Ok(default), |value| {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"true" | "1" => Ok(true),
|
||||
"false" | "0" => Ok(false),
|
||||
_ => Err(Error::Argument),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn uuid(node: Node<'_, '_>, name: &str) -> Result<UUID, Error> {
|
||||
let Some(container) = child(node, name) else {
|
||||
return Ok(UUID::zero());
|
||||
};
|
||||
let value = container
|
||||
.descendants()
|
||||
.find(|candidate| {
|
||||
candidate.is_element() && matches!(candidate.tag_name().name(), "UUID" | "Guid")
|
||||
})
|
||||
.and_then(|candidate| candidate.text())
|
||||
.or_else(|| container.text())
|
||||
.map(str::trim)
|
||||
.unwrap_or_default();
|
||||
if value.is_empty() {
|
||||
Ok(UUID::zero())
|
||||
} else {
|
||||
UUID::new_with_string(value.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn vector(node: Node<'_, '_>, name: &str) -> Result<Vector3, Error> {
|
||||
let Some(value) = child(node, name) else {
|
||||
return Ok(Vector3::zero());
|
||||
};
|
||||
Ok(Vector3 {
|
||||
x: parsed(value, "X", 0.0_f32)?,
|
||||
y: parsed(value, "Y", 0.0_f32)?,
|
||||
z: parsed(value, "Z", 0.0_f32)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn quaternion(node: Node<'_, '_>, name: &str) -> Result<Quaternion, Error> {
|
||||
let Some(value) = child(node, name) else {
|
||||
return Ok(Quaternion::identity());
|
||||
};
|
||||
Quaternion::new_with_single_single_single_single(
|
||||
parsed(value, "X", 0.0_f32)?,
|
||||
parsed(value, "Y", 0.0_f32)?,
|
||||
parsed(value, "Z", 0.0_f32)?,
|
||||
parsed(value, "W", 1.0_f32)?,
|
||||
)
|
||||
}
|
||||
|
||||
fn color(node: Node<'_, '_>, name: &str) -> Result<Color4, Error> {
|
||||
let Some(value) = child(node, name) else {
|
||||
return Ok(Color4::default());
|
||||
};
|
||||
Ok(Color4 {
|
||||
r: parsed(value, "R", 0.0_f32)?.clamp(0.0, 1.0),
|
||||
g: parsed(value, "G", 0.0_f32)?.clamp(0.0, 1.0),
|
||||
b: parsed(value, "B", 0.0_f32)?.clamp(0.0, 1.0),
|
||||
a: parsed(value, "A", 0.0_f32)?.clamp(0.0, 1.0),
|
||||
})
|
||||
}
|
||||
|
||||
fn write_inventory(output: &mut String, prim: &PrimObject) {
|
||||
output.push_str("<TaskInventory>");
|
||||
for item in &prim.inventory.items {
|
||||
output.push_str("<TaskInventoryItem>");
|
||||
uuid_element(output, "AssetID", item.asset_id);
|
||||
element(output, "BasePermissions", item.perms_base);
|
||||
element(output, "CreationDate", unix_seconds(item.creation_date));
|
||||
uuid_element(output, "CreatorID", item.creator_id);
|
||||
element(output, "Description", &item.description);
|
||||
element(output, "EveryonePermissions", item.perms_everyone);
|
||||
element(output, "Flags", item.flags);
|
||||
uuid_element(output, "GroupID", item.group_id);
|
||||
element(output, "GroupPermissions", item.perms_group);
|
||||
element(output, "InvType", item.inv_type.raw());
|
||||
uuid_element(output, "ItemID", item.id);
|
||||
uuid_element(output, "OldItemID", UUID::zero());
|
||||
uuid_element(output, "LastOwnerID", item.last_owner_id);
|
||||
element(output, "Name", &item.name);
|
||||
element(output, "NextPermissions", item.perms_next_owner);
|
||||
uuid_element(output, "OwnerID", item.owner_id);
|
||||
element(output, "CurrentPermissions", item.perms_owner);
|
||||
uuid_element(output, "ParentID", prim.id);
|
||||
uuid_element(output, "ParentPartID", prim.id);
|
||||
uuid_element(output, "PermsGranter", item.perms_granter_id);
|
||||
element(output, "PermsMask", item.perms_base);
|
||||
element(output, "Type", item.type_ as i8);
|
||||
element(output, "OwnerChanged", false);
|
||||
output.push_str("</TaskInventoryItem>");
|
||||
}
|
||||
output.push_str("</TaskInventory>");
|
||||
}
|
||||
|
||||
fn read_inventory(node: Node<'_, '_>) -> Result<Vec<PrimObjectInventoryBlockItemBlock>, Error> {
|
||||
let Some(inventory) = child(node, "TaskInventory") else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
inventory
|
||||
.children()
|
||||
.filter(|entry| entry.is_element() && entry.tag_name().name() == "TaskInventoryItem")
|
||||
.map(|entry| {
|
||||
let inv_type = parsed(entry, "InvType", -1_i32)?;
|
||||
Ok(PrimObjectInventoryBlockItemBlock {
|
||||
asset_id: uuid(entry, "AssetID")?,
|
||||
creation_date: UNIX_EPOCH
|
||||
.checked_add(Duration::from_secs(parsed(entry, "CreationDate", 0_u64)?))
|
||||
.ok_or(Error::Argument)?,
|
||||
creator_id: uuid(entry, "CreatorID")?,
|
||||
description: text(entry, "Description").unwrap_or_default(),
|
||||
flags: parsed(entry, "Flags", 0_i32)?,
|
||||
group_id: uuid(entry, "GroupID")?,
|
||||
id: uuid(entry, "ItemID")?,
|
||||
inv_type: InventoryType::from_raw(
|
||||
i8::try_from(inv_type).map_err(|_| Error::Argument)?,
|
||||
),
|
||||
last_owner_id: uuid(entry, "LastOwnerID")?,
|
||||
name: text(entry, "Name").unwrap_or_default(),
|
||||
owner_id: uuid(entry, "OwnerID")?,
|
||||
perms_base: parsed(entry, "BasePermissions", 0_u32)?,
|
||||
perms_everyone: parsed(entry, "EveryonePermissions", 0_u32)?,
|
||||
perms_granter_id: uuid(entry, "PermsGranter")?,
|
||||
perms_group: parsed(entry, "GroupPermissions", 0_u32)?,
|
||||
perms_next_owner: parsed(entry, "NextPermissions", 0_u32)?,
|
||||
perms_owner: parsed(entry, "CurrentPermissions", 0_u32)?,
|
||||
type_: asset_type(parsed(entry, "Type", -1_i32)?)?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn write_prim(
|
||||
output: &mut String,
|
||||
prim: &PrimObject,
|
||||
parent: Option<&PrimObject>,
|
||||
) -> Result<(), Error> {
|
||||
output.push_str("<SceneObjectPart xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">");
|
||||
element(output, "AllowedDrop", prim.allowed_drop);
|
||||
uuid_element(output, "CreatorID", prim.creator_id);
|
||||
uuid_element(output, "FolderID", prim.folder_id);
|
||||
element(output, "InventorySerial", prim.inventory.serial);
|
||||
write_inventory(output, prim);
|
||||
let mut flags = 0_u32;
|
||||
for (enabled, flag) in [
|
||||
(prim.use_physics, libremetaverse_types::PrimFlags::PHYSICS),
|
||||
(prim.phantom, libremetaverse_types::PrimFlags::PHANTOM),
|
||||
(
|
||||
prim.die_at_edge,
|
||||
libremetaverse_types::PrimFlags::DIE_AT_EDGE,
|
||||
),
|
||||
(
|
||||
prim.return_at_edge,
|
||||
libremetaverse_types::PrimFlags::RETURN_AT_EDGE,
|
||||
),
|
||||
(prim.temporary, libremetaverse_types::PrimFlags::TEMPORARY),
|
||||
(prim.sandbox, libremetaverse_types::PrimFlags::SANDBOX),
|
||||
(
|
||||
prim.allowed_drop,
|
||||
libremetaverse_types::PrimFlags::ALLOW_INVENTORY_DROP,
|
||||
),
|
||||
] {
|
||||
if enabled {
|
||||
flags |= flag.0;
|
||||
}
|
||||
}
|
||||
element(output, "ObjectFlags", flags);
|
||||
uuid_element(output, "UUID", prim.id);
|
||||
element(output, "LocalId", prim.local_id);
|
||||
element(output, "Name", &prim.name);
|
||||
element(output, "Material", prim.material);
|
||||
element(output, "PassTouches", prim.pass_touches);
|
||||
element(output, "RegionHandle", prim.region_handle);
|
||||
element(output, "ScriptAccessPin", prim.remote_script_access_pin);
|
||||
vector_element(
|
||||
output,
|
||||
"GroupPosition",
|
||||
parent.map_or(prim.position, |root| root.position),
|
||||
);
|
||||
vector_element(
|
||||
output,
|
||||
"OffsetPosition",
|
||||
if parent.is_some() {
|
||||
prim.position
|
||||
} else {
|
||||
Vector3::zero()
|
||||
},
|
||||
);
|
||||
quaternion_element(output, "RotationOffset", prim.rotation);
|
||||
vector_element(output, "Velocity", prim.velocity);
|
||||
vector_element(output, "RotationalVelocity", Vector3::zero());
|
||||
vector_element(output, "AngularVelocity", prim.angular_velocity);
|
||||
vector_element(output, "Acceleration", prim.acceleration);
|
||||
element(output, "Description", &prim.description);
|
||||
color_element(output, "Color", prim.text_color);
|
||||
element(output, "Text", &prim.text);
|
||||
element(output, "SitName", &prim.sit_name);
|
||||
element(output, "TouchName", &prim.touch_name);
|
||||
element(output, "LinkNum", prim.link_number);
|
||||
element(output, "ClickAction", prim.click_action);
|
||||
output.push_str("<Shape>");
|
||||
element(
|
||||
output,
|
||||
"PathBegin",
|
||||
Primitive::pack_begin_cut(prim.shape.path_begin)?,
|
||||
);
|
||||
element(output, "PathCurve", prim.shape.path_curve);
|
||||
element(
|
||||
output,
|
||||
"PathEnd",
|
||||
Primitive::pack_end_cut(prim.shape.path_end)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathRadiusOffset",
|
||||
Primitive::pack_path_twist(prim.shape.path_radius_offset)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathRevolutions",
|
||||
Primitive::pack_path_revolutions(prim.shape.path_revolutions)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathScaleX",
|
||||
Primitive::pack_path_scale(prim.shape.path_scale_x)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathScaleY",
|
||||
Primitive::pack_path_scale(prim.shape.path_scale_y)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathShearX",
|
||||
Primitive::pack_path_shear(prim.shape.path_shear_x)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathShearY",
|
||||
Primitive::pack_path_shear(prim.shape.path_shear_y)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathSkew",
|
||||
Primitive::pack_path_twist(prim.shape.path_skew)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathTaperX",
|
||||
Primitive::pack_path_taper(prim.shape.path_taper_x)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathTaperY",
|
||||
Primitive::pack_path_taper(prim.shape.path_taper_y)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathTwist",
|
||||
Primitive::pack_path_twist(prim.shape.path_twist)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"PathTwistBegin",
|
||||
Primitive::pack_path_twist(prim.shape.path_twist_begin)?,
|
||||
);
|
||||
element(output, "PCode", prim.p_code);
|
||||
element(
|
||||
output,
|
||||
"ProfileBegin",
|
||||
Primitive::pack_begin_cut(prim.shape.profile_begin)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"ProfileEnd",
|
||||
Primitive::pack_end_cut(prim.shape.profile_end)?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"ProfileHollow",
|
||||
Primitive::pack_profile_hollow(prim.shape.profile_hollow)?,
|
||||
);
|
||||
vector_element(output, "Scale", prim.scale);
|
||||
element(output, "State", prim.state);
|
||||
element(output, "ProfileShape", prim.shape.profile_curve & 0x0f);
|
||||
element(output, "HollowShape", prim.shape.profile_curve & 0xf0);
|
||||
element(output, "ProfileCurve", prim.shape.profile_curve);
|
||||
let texture_bytes = prim
|
||||
.textures
|
||||
.as_ref()
|
||||
.map(PrimitiveTextureEntry::get_bytes)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
element(
|
||||
output,
|
||||
"TextureEntry",
|
||||
base64::engine::general_purpose::STANDARD.encode(texture_bytes),
|
||||
);
|
||||
output.push_str("<ExtraParams></ExtraParams>");
|
||||
uuid_element(output, "SculptTexture", prim.sculpt.texture);
|
||||
element(output, "SculptType", prim.sculpt.type_);
|
||||
output.push_str("<SculptData></SculptData>");
|
||||
element(output, "FlexiSoftness", prim.flexible.softness);
|
||||
element(output, "FlexiTension", prim.flexible.tension);
|
||||
element(output, "FlexiDrag", prim.flexible.drag);
|
||||
element(output, "FlexiGravity", prim.flexible.gravity);
|
||||
element(output, "FlexiWind", prim.flexible.wind);
|
||||
element(output, "FlexiForceX", prim.flexible.force.x);
|
||||
element(output, "FlexiForceY", prim.flexible.force.y);
|
||||
element(output, "FlexiForceZ", prim.flexible.force.z);
|
||||
element(output, "LightColorR", prim.light.color.r);
|
||||
element(output, "LightColorG", prim.light.color.g);
|
||||
element(output, "LightColorB", prim.light.color.b);
|
||||
element(output, "LightColorA", prim.light.color.a);
|
||||
element(output, "LightRadius", prim.light.radius);
|
||||
element(output, "LightCutoff", prim.light.cutoff);
|
||||
element(output, "LightFalloff", prim.light.falloff);
|
||||
element(output, "LightIntensity", prim.light.intensity);
|
||||
element(
|
||||
output,
|
||||
"FlexiEntry",
|
||||
prim.flexible != crate::prim_object::PrimObjectFlexibleBlock::new()?,
|
||||
);
|
||||
element(
|
||||
output,
|
||||
"LightEntry",
|
||||
prim.light != crate::prim_object::PrimObjectLightBlock::new()?,
|
||||
);
|
||||
element(output, "SculptEntry", prim.sculpt.texture != UUID::zero());
|
||||
output.push_str("</Shape>");
|
||||
vector_element(output, "Scale", prim.scale);
|
||||
element(output, "UpdateFlag", 0);
|
||||
quaternion_element(output, "SitTargetOrientation", prim.sit_rotation);
|
||||
vector_element(output, "SitTargetPosition", prim.sit_offset);
|
||||
vector_element(output, "SitTargetPositionLL", prim.sit_offset);
|
||||
quaternion_element(output, "SitTargetOrientationLL", prim.sit_rotation);
|
||||
element(output, "ParentID", prim.parent_id);
|
||||
element(output, "CreationDate", unix_seconds(prim.creation_date));
|
||||
element(output, "RezDate", unix_seconds(prim.rez_date));
|
||||
element(output, "Category", 0);
|
||||
element(output, "SalePrice", prim.sale_price);
|
||||
element(output, "ObjectSaleType", prim.sale_type);
|
||||
element(output, "OwnershipCost", 0);
|
||||
uuid_element(output, "GroupID", prim.group_id);
|
||||
uuid_element(output, "OwnerID", prim.owner_id);
|
||||
uuid_element(output, "LastOwnerID", prim.last_owner_id);
|
||||
element(output, "BaseMask", prim.perms_base);
|
||||
element(output, "OwnerMask", prim.perms_owner);
|
||||
element(output, "GroupMask", prim.perms_group);
|
||||
element(output, "EveryoneMask", prim.perms_everyone);
|
||||
element(output, "NextOwnerMask", prim.perms_next_owner);
|
||||
element(output, "Flags", "None");
|
||||
uuid_element(output, "SitTargetAvatar", UUID::zero());
|
||||
uuid_element(output, "CollisionSound", prim.collision_sound);
|
||||
element(output, "CollisionSoundVolume", prim.collision_sound_volume);
|
||||
element(
|
||||
output,
|
||||
"ScriptState",
|
||||
base64::engine::general_purpose::STANDARD.encode(&prim.script_state),
|
||||
);
|
||||
output.push_str("</SceneObjectPart>");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_prim(node: Node<'_, '_>) -> Result<PrimObject, Error> {
|
||||
let mut prim = PrimObject::new()?;
|
||||
prim.allowed_drop = boolean(node, "AllowedDrop", true)?;
|
||||
prim.creator_id = uuid(node, "CreatorID")?;
|
||||
prim.folder_id = uuid(node, "FolderID")?;
|
||||
prim.inventory.serial = parsed(node, "InventorySerial", 0_i32)?;
|
||||
prim.inventory.items = read_inventory(node)?;
|
||||
let flags = parsed(node, "ObjectFlags", 0_u32)?;
|
||||
prim.use_physics = flags & libremetaverse_types::PrimFlags::PHYSICS.0 != 0;
|
||||
prim.phantom = flags & libremetaverse_types::PrimFlags::PHANTOM.0 != 0;
|
||||
prim.die_at_edge = flags & libremetaverse_types::PrimFlags::DIE_AT_EDGE.0 != 0;
|
||||
prim.return_at_edge = flags & libremetaverse_types::PrimFlags::RETURN_AT_EDGE.0 != 0;
|
||||
prim.temporary = flags & libremetaverse_types::PrimFlags::TEMPORARY.0 != 0;
|
||||
prim.sandbox = flags & libremetaverse_types::PrimFlags::SANDBOX.0 != 0;
|
||||
prim.allowed_drop |= flags & libremetaverse_types::PrimFlags::ALLOW_INVENTORY_DROP.0 != 0;
|
||||
prim.id = uuid(node, "UUID")?;
|
||||
prim.local_id = parsed(node, "LocalId", 0_u32)?;
|
||||
prim.name = text(node, "Name").unwrap_or_default();
|
||||
prim.material = parsed(node, "Material", 0_i32)?;
|
||||
prim.pass_touches = boolean(node, "PassTouches", false)?;
|
||||
prim.region_handle = parsed(node, "RegionHandle", 0_u64)?;
|
||||
prim.remote_script_access_pin = parsed(node, "ScriptAccessPin", 0_i32)?;
|
||||
let group_position = vector(node, "GroupPosition")?;
|
||||
let offset_position = vector(node, "OffsetPosition")?;
|
||||
prim.rotation = quaternion(node, "RotationOffset")?;
|
||||
prim.velocity = vector(node, "Velocity")?;
|
||||
prim.angular_velocity = vector(node, "AngularVelocity")?;
|
||||
prim.acceleration = vector(node, "Acceleration")?;
|
||||
prim.description = text(node, "Description").unwrap_or_default();
|
||||
prim.text_color = color(node, "Color")?;
|
||||
prim.text = text(node, "Text").unwrap_or_default();
|
||||
prim.sit_name = text(node, "SitName").unwrap_or_default();
|
||||
prim.touch_name = text(node, "TouchName").unwrap_or_default();
|
||||
prim.link_number = parsed(node, "LinkNum", 0_i32)?;
|
||||
prim.click_action = parsed(node, "ClickAction", 0_i32)?;
|
||||
if let Some(shape) = child(node, "Shape") {
|
||||
prim.shape.path_begin = Primitive::unpack_begin_cut(parsed(shape, "PathBegin", 0_u16)?)?;
|
||||
prim.shape.path_curve = parsed(shape, "PathCurve", 0_i32)?;
|
||||
prim.shape.path_end = Primitive::unpack_end_cut(parsed(shape, "PathEnd", 0_u16)?)?;
|
||||
prim.shape.path_radius_offset =
|
||||
Primitive::unpack_path_twist(parsed(shape, "PathRadiusOffset", 0_i8)?)?;
|
||||
prim.shape.path_revolutions =
|
||||
Primitive::unpack_path_revolutions(parsed(shape, "PathRevolutions", 0_u8)?)?;
|
||||
prim.shape.path_scale_x = Primitive::unpack_path_scale(parsed(shape, "PathScaleX", 0_u8)?)?;
|
||||
prim.shape.path_scale_y = Primitive::unpack_path_scale(parsed(shape, "PathScaleY", 0_u8)?)?;
|
||||
prim.shape.path_shear_x = Primitive::unpack_path_shear(parsed(shape, "PathShearX", 0_i8)?)?;
|
||||
prim.shape.path_shear_y = Primitive::unpack_path_shear(parsed(shape, "PathShearY", 0_i8)?)?;
|
||||
prim.shape.path_skew = Primitive::unpack_path_twist(parsed(shape, "PathSkew", 0_i8)?)?;
|
||||
prim.shape.path_taper_x = Primitive::unpack_path_taper(parsed(shape, "PathTaperX", 0_i8)?)?;
|
||||
prim.shape.path_taper_y = Primitive::unpack_path_taper(parsed(shape, "PathTaperY", 0_i8)?)?;
|
||||
prim.shape.path_twist = Primitive::unpack_path_twist(parsed(shape, "PathTwist", 0_i8)?)?;
|
||||
prim.shape.path_twist_begin =
|
||||
Primitive::unpack_path_twist(parsed(shape, "PathTwistBegin", 0_i8)?)?;
|
||||
prim.p_code = parsed(shape, "PCode", 0_i32)?;
|
||||
prim.shape.profile_begin =
|
||||
Primitive::unpack_begin_cut(parsed(shape, "ProfileBegin", 0_u16)?)?;
|
||||
prim.shape.profile_end = Primitive::unpack_end_cut(parsed(shape, "ProfileEnd", 0_u16)?)?;
|
||||
prim.shape.profile_hollow =
|
||||
Primitive::unpack_profile_hollow(parsed(shape, "ProfileHollow", 0_u16)?)?;
|
||||
prim.scale = vector(shape, "Scale")?;
|
||||
prim.state = parsed(shape, "State", 0_i32)?;
|
||||
prim.shape.profile_curve = parsed(shape, "ProfileCurve", 0_i32)?;
|
||||
if let Some(encoded) = text(shape, "TextureEntry") {
|
||||
let bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(encoded)
|
||||
.map_err(|_| Error::Argument)?;
|
||||
if !bytes.is_empty() {
|
||||
prim.textures = Some(PrimitiveTextureEntry::new_with_bytes_int32_int32(
|
||||
bytes.clone(),
|
||||
0,
|
||||
i32::try_from(bytes.len()).map_err(|_| Error::Argument)?,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
prim.sculpt.texture = uuid(shape, "SculptTexture")?;
|
||||
prim.sculpt.type_ = parsed(shape, "SculptType", 0_i32)?;
|
||||
prim.flexible.softness = parsed(shape, "FlexiSoftness", 0_i32)?;
|
||||
prim.flexible.tension = parsed(shape, "FlexiTension", 0.0_f32)?;
|
||||
prim.flexible.drag = parsed(shape, "FlexiDrag", 0.0_f32)?;
|
||||
prim.flexible.gravity = parsed(shape, "FlexiGravity", 0.0_f32)?;
|
||||
prim.flexible.wind = parsed(shape, "FlexiWind", 0.0_f32)?;
|
||||
prim.flexible.force = Vector3 {
|
||||
x: parsed(shape, "FlexiForceX", 0.0_f32)?,
|
||||
y: parsed(shape, "FlexiForceY", 0.0_f32)?,
|
||||
z: parsed(shape, "FlexiForceZ", 0.0_f32)?,
|
||||
};
|
||||
prim.light.color = Color4 {
|
||||
r: parsed(shape, "LightColorR", 0.0_f32)?,
|
||||
g: parsed(shape, "LightColorG", 0.0_f32)?,
|
||||
b: parsed(shape, "LightColorB", 0.0_f32)?,
|
||||
a: parsed(shape, "LightColorA", 0.0_f32)?,
|
||||
};
|
||||
prim.light.radius = parsed(shape, "LightRadius", 0.0_f32)?;
|
||||
prim.light.cutoff = parsed(shape, "LightCutoff", 0.0_f32)?;
|
||||
prim.light.falloff = parsed(shape, "LightFalloff", 0.0_f32)?;
|
||||
prim.light.intensity = parsed(shape, "LightIntensity", 0.0_f32)?;
|
||||
}
|
||||
prim.scale = child(node, "Scale").map_or(Ok(prim.scale), |_| vector(node, "Scale"))?;
|
||||
prim.sit_offset = vector(node, "SitTargetPositionLL")?;
|
||||
prim.sit_rotation = quaternion(node, "SitTargetOrientationLL")?;
|
||||
prim.parent_id = parsed(node, "ParentID", 0_u32)?;
|
||||
prim.position = if prim.parent_id == 0 {
|
||||
group_position
|
||||
} else {
|
||||
offset_position
|
||||
};
|
||||
prim.creation_date = UNIX_EPOCH
|
||||
.checked_add(Duration::from_secs(parsed(node, "CreationDate", 0_u64)?))
|
||||
.ok_or(Error::Argument)?;
|
||||
prim.rez_date = UNIX_EPOCH
|
||||
.checked_add(Duration::from_secs(parsed(node, "RezDate", 0_u64)?))
|
||||
.ok_or(Error::Argument)?;
|
||||
prim.sale_price = parsed(node, "SalePrice", 0_i32)?;
|
||||
prim.sale_type = parsed(node, "ObjectSaleType", 0_i32)?;
|
||||
prim.group_id = uuid(node, "GroupID")?;
|
||||
prim.owner_id = uuid(node, "OwnerID")?;
|
||||
prim.last_owner_id = uuid(node, "LastOwnerID")?;
|
||||
prim.perms_base = parsed(node, "BaseMask", 0_u32)?;
|
||||
prim.perms_owner = parsed(node, "OwnerMask", 0_u32)?;
|
||||
prim.perms_group = parsed(node, "GroupMask", 0_u32)?;
|
||||
prim.perms_everyone = parsed(node, "EveryoneMask", 0_u32)?;
|
||||
prim.perms_next_owner = parsed(node, "NextOwnerMask", 0_u32)?;
|
||||
prim.collision_sound = uuid(node, "CollisionSound")?;
|
||||
prim.collision_sound_volume = parsed(node, "CollisionSoundVolume", 0.0_f32)?;
|
||||
if let Some(encoded) = text(node, "ScriptState") {
|
||||
prim.script_state = base64::engine::general_purpose::STANDARD
|
||||
.decode(encoded)
|
||||
.map_err(|_| Error::Argument)?;
|
||||
}
|
||||
Ok(prim)
|
||||
}
|
||||
|
||||
impl AssetPrim {
|
||||
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
children: Vec::new(),
|
||||
parent: None,
|
||||
asset_data: Vec::new(),
|
||||
asset_id: UUID::zero(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_prim_object_list(
|
||||
parent: PrimObject,
|
||||
children: Vec<PrimObject>,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
asset_id: parent.id,
|
||||
children,
|
||||
parent: Some(parent),
|
||||
asset_data: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
||||
if asset_data.len() > MAX_XML_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Self {
|
||||
children: Vec::new(),
|
||||
parent: None,
|
||||
asset_data,
|
||||
asset_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_string(xml_data: String) -> Result<Self, Error> {
|
||||
let mut value = Self::new_with_uuid_bytes(UUID::zero(), xml_data.as_bytes().to_vec())?;
|
||||
if !value.decode_xml(xml_data)? {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub const fn asset_type(&self) -> AssetType {
|
||||
AssetType::Object
|
||||
}
|
||||
pub const fn asset_id(&self) -> UUID {
|
||||
self.asset_id
|
||||
}
|
||||
pub fn native_bytes(&self) -> &[u8] {
|
||||
&self.asset_data
|
||||
}
|
||||
|
||||
pub fn encode(&mut self) -> Result<(), Error> {
|
||||
self.asset_data = self.encode_xml()?.into_bytes();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decode(&mut self) -> Result<bool, Error> {
|
||||
let Ok(xml) = String::from_utf8(self.asset_data.clone()) else {
|
||||
return Ok(false);
|
||||
};
|
||||
match self.decode_xml(xml) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_xml(&self) -> Result<String, Error> {
|
||||
let mut output =
|
||||
String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?><SceneObjectGroup>");
|
||||
if let Some(parent) = &self.parent {
|
||||
write_prim(&mut output, parent, None)?;
|
||||
}
|
||||
output.push_str("<OtherParts>");
|
||||
for child in &self.children {
|
||||
write_prim(&mut output, child, self.parent.as_ref())?;
|
||||
}
|
||||
output.push_str("</OtherParts></SceneObjectGroup>");
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn decode_xml(&mut self, xml_data: String) -> Result<bool, Error> {
|
||||
if xml_data.len() > MAX_XML_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let document = roxmltree::Document::parse(&xml_data).map_err(|_| Error::Argument)?;
|
||||
let root = document.root_element();
|
||||
if root.tag_name().name() != "SceneObjectGroup" {
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(parent_node) = root
|
||||
.children()
|
||||
.find(|node| node.is_element() && node.tag_name().name() == "SceneObjectPart")
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let parent = read_prim(parent_node)?;
|
||||
let children = child(root, "OtherParts")
|
||||
.into_iter()
|
||||
.flat_map(|container| container.children())
|
||||
.filter(|node| node.is_element() && node.tag_name().name() == "SceneObjectPart")
|
||||
.map(read_prim)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
if self.asset_id == UUID::zero() {
|
||||
self.asset_id = parent.id;
|
||||
}
|
||||
self.parent = Some(parent);
|
||||
self.children = children;
|
||||
self.asset_data = xml_data.into_bytes();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn load_prim(reader: XmlTextReader) -> Result<PrimObject, Error> {
|
||||
if reader.0.len() > MAX_XML_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let document = roxmltree::Document::parse(&reader.0).map_err(|_| Error::Argument)?;
|
||||
let node = document
|
||||
.descendants()
|
||||
.find(|node| node.is_element() && node.tag_name().name() == "SceneObjectPart")
|
||||
.ok_or(Error::Argument)?;
|
||||
read_prim(node)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn xml2_round_trip_preserves_linkset_and_inventory() {
|
||||
let mut root = PrimObject::new().unwrap();
|
||||
root.id = UUID::new_with_string("7bd3f7e3-2afc-44aa-a495-b741e4189f61".into()).unwrap();
|
||||
root.name = "OpenSim root & <fixture>".into();
|
||||
root.position = Vector3 {
|
||||
x: 128.0,
|
||||
y: 64.0,
|
||||
z: 25.0,
|
||||
};
|
||||
root.p_code = 9;
|
||||
root.shape.path_end = 1.0;
|
||||
root.shape.path_scale_x = 1.0;
|
||||
root.shape.path_scale_y = 1.0;
|
||||
root.shape.path_revolutions = 1.0;
|
||||
root.shape.profile_end = 1.0;
|
||||
root.script_state = vec![1, 2, 3];
|
||||
let mut item = PrimObjectInventoryBlockItemBlock::new().unwrap();
|
||||
item.name = "script".into();
|
||||
item.type_ = AssetType::LSLText;
|
||||
root.inventory.items.push(item);
|
||||
let mut child = root.clone();
|
||||
child.id = UUID::new_with_string("ff6863c7-7acb-4e81-a28b-f0f162f06455".into()).unwrap();
|
||||
child.name = "child".into();
|
||||
child.parent_id = root.local_id.max(1);
|
||||
child.position = Vector3 {
|
||||
x: 1.0,
|
||||
y: 2.0,
|
||||
z: 3.0,
|
||||
};
|
||||
let asset =
|
||||
AssetPrim::new_with_prim_object_list(root.clone(), vec![child.clone()]).unwrap();
|
||||
let xml = asset.encode_xml().unwrap();
|
||||
let decoded = AssetPrim::new_with_string(xml).unwrap();
|
||||
assert_eq!(decoded.parent.as_ref().unwrap().name, root.name);
|
||||
assert_eq!(decoded.children.len(), 1);
|
||||
assert_eq!(decoded.children[0].position, child.position);
|
||||
assert_eq!(decoded.children[0].script_state, child.script_state);
|
||||
assert_eq!(decoded.asset_type(), AssetType::Object);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_or_non_group_xml_is_rejected_without_partial_state() {
|
||||
let mut asset = AssetPrim::new_with_constructor().unwrap();
|
||||
assert!(!asset.decode_xml("<not-a-group/>".into()).unwrap());
|
||||
assert!(asset.decode_xml("<SceneObjectGroup>".into()).is_err());
|
||||
assert!(asset.parent.is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
//! Avatar inheritance, naming, and LLSD persistence.
|
||||
|
||||
use crate::{
|
||||
AgentManagerControlFlags, Animation, AppearanceFlags, AvatarAttachment, AvatarAvatarProperties,
|
||||
AvatarInterests, AvatarStatistics, Error, NameValue, NameValueValueType, Primitive,
|
||||
};
|
||||
use libremetaverse_structured_data::OSD;
|
||||
use libremetaverse_types::{UUID, Vector3};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) fn statistics_get_osd(value: &AvatarStatistics) -> Result<OSD, Error> {
|
||||
Ok(OSD::Map(HashMap::from([
|
||||
(
|
||||
"behavior_positive".into(),
|
||||
OSD::Integer(value.behavior_positive),
|
||||
),
|
||||
(
|
||||
"behavior_negative".into(),
|
||||
OSD::Integer(value.behavior_negative),
|
||||
),
|
||||
(
|
||||
"appearance_positive".into(),
|
||||
OSD::Integer(value.appearance_positive),
|
||||
),
|
||||
(
|
||||
"appearance_negative".into(),
|
||||
OSD::Integer(value.appearance_negative),
|
||||
),
|
||||
(
|
||||
"buildings_positive".into(),
|
||||
OSD::Integer(value.building_positive),
|
||||
),
|
||||
(
|
||||
"buildings_negative".into(),
|
||||
OSD::Integer(value.building_negative),
|
||||
),
|
||||
("given_positive".into(), OSD::Integer(value.given_positive)),
|
||||
("given_negative".into(), OSD::Integer(value.given_negative)),
|
||||
])))
|
||||
}
|
||||
|
||||
pub(crate) fn statistics_from_osd(value: OSD) -> Result<AvatarStatistics, Error> {
|
||||
let OSD::Map(map) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let integer = |key: &str| map.get(key).ok_or(Error::Argument)?.as_integer();
|
||||
Ok(AvatarStatistics {
|
||||
appearance_negative: integer("appearance_negative")?,
|
||||
appearance_positive: integer("appearance_positive")?,
|
||||
// Retain the pinned C# decoder's observable assignment behavior.
|
||||
behavior_negative: 0,
|
||||
behavior_positive: integer("behavior_positive")?,
|
||||
building_negative: integer("buildings_negative")?,
|
||||
building_positive: integer("buildings_positive")?,
|
||||
given_negative: integer("given_negative")?,
|
||||
given_positive: integer("given_positive")?,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Avatar {
|
||||
pub base: Primitive,
|
||||
pub animations: Vec<Animation>,
|
||||
pub appearance_flags: AppearanceFlags,
|
||||
pub appearance_version: u8,
|
||||
pub attachments: Vec<AvatarAttachment>,
|
||||
pub cof_version: i32,
|
||||
pub control_flags: AgentManagerControlFlags,
|
||||
pub groups: Vec<UUID>,
|
||||
pub hover_height: Vector3,
|
||||
pub profile_interests: AvatarInterests,
|
||||
pub profile_properties: AvatarAvatarProperties,
|
||||
pub profile_statistics: AvatarStatistics,
|
||||
pub visual_parameters: Vec<u8>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Avatar {
|
||||
type Target = Primitive;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.base
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Avatar {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.base
|
||||
}
|
||||
}
|
||||
|
||||
impl Avatar {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
crate::visual_catalog::new_avatar()
|
||||
}
|
||||
|
||||
pub fn decode_visual_params(&self) -> Result<HashMap<i32, f32>, Error> {
|
||||
Ok(crate::visual_catalog::decode_visual_params(
|
||||
&self.visual_parameters,
|
||||
))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn first_name(&self) -> String {
|
||||
self.name_value("FirstName")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn last_name(&self) -> String {
|
||||
self.name_value("LastName")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn name(&self) -> String {
|
||||
let first = self.first_name();
|
||||
let last = self.last_name();
|
||||
if first.is_empty() || last.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{first} {last}")
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn group_name(&self) -> String {
|
||||
self.name_value("Title")
|
||||
}
|
||||
|
||||
fn name_value(&self, name: &str) -> String {
|
||||
self.base
|
||||
.name_values
|
||||
.as_ref()
|
||||
.and_then(|values| {
|
||||
values
|
||||
.iter()
|
||||
.find(|value| value.name == name && value.type_ == NameValueValueType::String)
|
||||
})
|
||||
.and_then(|value| value.value.as_ref())
|
||||
.and_then(|value| match value {
|
||||
libremetaverse_types::compat::Object::String(value) => Some(value.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_osd(&self) -> Result<OSD, Error> {
|
||||
let OSD::Map(mut map) = self.base.get_osd()? else {
|
||||
return Err(Error::InvalidOperation);
|
||||
};
|
||||
map.insert(
|
||||
"groups".into(),
|
||||
OSD::Array(self.groups.iter().copied().map(OSD::UUID).collect()),
|
||||
);
|
||||
map.insert(
|
||||
"profile_statistics".into(),
|
||||
self.profile_statistics.get_osd()?,
|
||||
);
|
||||
map.insert(
|
||||
"profile_properties".into(),
|
||||
self.profile_properties.get_osd()?,
|
||||
);
|
||||
map.insert("profile_interest".into(), self.profile_interests.get_osd()?);
|
||||
map.insert("control_flags".into(), OSD::Integer(self.control_flags.0));
|
||||
map.insert(
|
||||
"visual_parameters".into(),
|
||||
OSD::Array(
|
||||
self.visual_parameters
|
||||
.iter()
|
||||
.map(|value| OSD::Integer(i32::from(*value)))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
map.insert("first_name".into(), OSD::String(self.first_name()));
|
||||
map.insert("last_name".into(), OSD::String(self.last_name()));
|
||||
map.insert("group_name".into(), OSD::String(self.group_name()));
|
||||
Ok(OSD::Map(map))
|
||||
}
|
||||
|
||||
pub fn from_osd(osd: OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(map) = &osd else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let mut avatar = Self::new()?;
|
||||
avatar.base = Primitive::from_osd(osd.clone())?;
|
||||
avatar.groups = osd_array(map.get("groups"))
|
||||
.iter()
|
||||
.filter_map(|value| value.as_uuid().ok())
|
||||
.collect();
|
||||
if let Some(value) = map.get("profile_statistics") {
|
||||
avatar.profile_statistics = AvatarStatistics::from_osd(value.clone())?;
|
||||
}
|
||||
if let Some(value) = map.get("profile_properties") {
|
||||
avatar.profile_properties = AvatarAvatarProperties::from_osd(value.clone())?;
|
||||
}
|
||||
if let Some(value) = map.get("profile_interest") {
|
||||
avatar.profile_interests = AvatarInterests::from_osd(value.clone())?;
|
||||
}
|
||||
avatar.control_flags =
|
||||
AgentManagerControlFlags(map.get("control_flags").map_or(Ok(0), OSD::as_integer)?);
|
||||
avatar.visual_parameters = osd_array(map.get("visual_parameters"))
|
||||
.iter()
|
||||
.map(|value| value.as_integer().map(|number| number.clamp(0, 255) as u8))
|
||||
.collect::<Result<_, _>>()?;
|
||||
avatar.base.name_values = Some(vec![
|
||||
string_name_value("FirstName", map.get("first_name"))?,
|
||||
string_name_value("LastName", map.get("last_name"))?,
|
||||
string_name_value("Title", map.get("group_name"))?,
|
||||
]);
|
||||
Ok(avatar)
|
||||
}
|
||||
}
|
||||
|
||||
fn osd_array(value: Option<&OSD>) -> &[OSD] {
|
||||
match value {
|
||||
Some(OSD::Array(values)) => values,
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn string_name_value(name: &str, value: Option<&OSD>) -> Result<NameValue, Error> {
|
||||
NameValue::new_with_string_value_type_class_type_sendto_type_object(
|
||||
name.into(),
|
||||
NameValueValueType::String,
|
||||
crate::NameValueClassType::ReadOnly,
|
||||
crate::NameValueSendtoType::Sim,
|
||||
libremetaverse_types::compat::Object::String(
|
||||
value.map_or(Ok(String::new()), OSD::as_string)?,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
//! Native spring-mass-damper simulation for avatar physics wearables.
|
||||
|
||||
use crate::Error;
|
||||
use libremetaverse_types::compat::Vector3;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
type BoneProvider = Arc<dyn Fn(String) -> Vector3 + Send + Sync>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Channel {
|
||||
driver: i32,
|
||||
driven: &'static [i32],
|
||||
direction: [f32; 3],
|
||||
params: [i32; 7],
|
||||
position: f32,
|
||||
velocity: f32,
|
||||
joint_velocity: f32,
|
||||
joint_acceleration: f32,
|
||||
last_time: f32,
|
||||
world_position: [f32; 3],
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
fn new(driver: i32, driven: &'static [i32], direction: [f32; 3], params: [i32; 7]) -> Self {
|
||||
Self {
|
||||
driver,
|
||||
driven,
|
||||
direction,
|
||||
params,
|
||||
position: 0.5,
|
||||
velocity: 0.0,
|
||||
joint_velocity: 0.0,
|
||||
joint_acceleration: 0.0,
|
||||
last_time: -1.0,
|
||||
world_position: [0.0; 3],
|
||||
}
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.position = 0.5;
|
||||
self.velocity = 0.0;
|
||||
self.joint_velocity = 0.0;
|
||||
self.joint_acceleration = 0.0;
|
||||
self.last_time = -1.0;
|
||||
self.world_position = [0.0; 3];
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
channels: Vec<Channel>,
|
||||
weights: HashMap<i32, f32>,
|
||||
provider: Option<BoneProvider>,
|
||||
}
|
||||
|
||||
/// Stateful simulator matching the viewer's six LLPhysicsMotion channels.
|
||||
pub struct AvatarPhysicsSimulator {
|
||||
state: Mutex<State>,
|
||||
}
|
||||
|
||||
impl AvatarPhysicsSimulator {
|
||||
pub(crate) fn native_new() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
state: Mutex::new(State {
|
||||
channels: vec![
|
||||
Channel::new(
|
||||
1100,
|
||||
&[1200],
|
||||
[0.0, 0.0, -1.0],
|
||||
[10000, 10001, 10002, 10006, 10003, 10004, 10005],
|
||||
),
|
||||
Channel::new(
|
||||
1101,
|
||||
&[1201],
|
||||
[-1.0, 0.0, 0.0],
|
||||
[10000, 10001, 10002, 10010, 10007, 10008, 10009],
|
||||
),
|
||||
Channel::new(
|
||||
1105,
|
||||
&[1207],
|
||||
[0.0, -1.0, 0.0],
|
||||
[10000, 10001, 10002, 10032, 10029, 10030, 10031],
|
||||
),
|
||||
Channel::new(
|
||||
1103,
|
||||
&[1205],
|
||||
[0.0, 0.0, -1.0],
|
||||
[10018, 10019, 10020, 10024, 10021, 10022, 10023],
|
||||
),
|
||||
Channel::new(
|
||||
1104,
|
||||
&[1206],
|
||||
[0.0, -1.0, 0.0],
|
||||
[10018, 10019, 10020, 10028, 10025, 10026, 10027],
|
||||
),
|
||||
Channel::new(
|
||||
1102,
|
||||
&[1202, 1203, 1204],
|
||||
[0.0, 0.0, -1.0],
|
||||
[10011, 10012, 10013, 10017, 10014, 10015, 10016],
|
||||
),
|
||||
],
|
||||
weights: HashMap::new(),
|
||||
provider: None,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_wearable_params(
|
||||
&self,
|
||||
values: HashMap<i32, f32>,
|
||||
) -> Result<(), Error> {
|
||||
let mut state = self
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
state.weights = values;
|
||||
state.channels.iter_mut().for_each(Channel::reset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_bone_position_provider(
|
||||
&self,
|
||||
provider: Box<dyn Fn(String) -> Vector3 + Send + Sync>,
|
||||
) -> Result<(), Error> {
|
||||
self.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.provider = Some(provider.into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_tick(&self, time: f32) -> Result<HashMap<i32, f32>, Error> {
|
||||
if !time.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut state = self
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let weights = state.weights.clone();
|
||||
let provider = state.provider.clone();
|
||||
let mut output = HashMap::new();
|
||||
for channel in &mut state.channels {
|
||||
let position = step(channel, time, &weights, provider.as_ref());
|
||||
for id in channel.driven {
|
||||
output.insert(*id, position);
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
fn param(weights: &HashMap<i32, f32>, id: i32, fallback: f32) -> f32 {
|
||||
weights
|
||||
.get(&id)
|
||||
.copied()
|
||||
.or_else(|| crate::visual_catalog::param_by_id(id).map(|p| p.default_value))
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
|
||||
fn driver_position(channel: &Channel, weights: &HashMap<i32, f32>) -> f32 {
|
||||
let Some(definition) = crate::visual_catalog::param_by_id(channel.driver) else {
|
||||
return 0.5;
|
||||
};
|
||||
let range = definition.max_value - definition.min_value;
|
||||
if range.abs() < 1.0e-6 {
|
||||
return 0.0;
|
||||
}
|
||||
((weights
|
||||
.get(&channel.driver)
|
||||
.copied()
|
||||
.unwrap_or(definition.default_value)
|
||||
- definition.min_value)
|
||||
/ range)
|
||||
.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
fn dot(left: [f32; 3], right: [f32; 3]) -> f32 {
|
||||
left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
|
||||
}
|
||||
fn subtract(left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
|
||||
[left[0] - right[0], left[1] - right[1], left[2] - right[2]]
|
||||
}
|
||||
fn vector(value: Vector3) -> [f32; 3] {
|
||||
value.0
|
||||
}
|
||||
|
||||
fn step(
|
||||
channel: &mut Channel,
|
||||
time: f32,
|
||||
weights: &HashMap<i32, f32>,
|
||||
provider: Option<&BoneProvider>,
|
||||
) -> f32 {
|
||||
let joint_name = if matches!(channel.driver, 1100 | 1101 | 1105) {
|
||||
"mChest"
|
||||
} else {
|
||||
"mPelvis"
|
||||
};
|
||||
let joint = provider.map_or(channel.world_position, |get| vector(get(joint_name.into())));
|
||||
let target = driver_position(channel, weights);
|
||||
if channel.last_time < 0.0 {
|
||||
channel.last_time = time;
|
||||
channel.position = target;
|
||||
channel.world_position = joint;
|
||||
return target;
|
||||
}
|
||||
let dt = time - channel.last_time;
|
||||
if dt <= 0.0 || dt > 1.0 {
|
||||
channel.last_time = time;
|
||||
return channel.position.clamp(0.0, 1.0);
|
||||
}
|
||||
let [
|
||||
mass_id,
|
||||
gravity_id,
|
||||
drag_id,
|
||||
damping_id,
|
||||
max_id,
|
||||
spring_id,
|
||||
gain_id,
|
||||
] = channel.params;
|
||||
let mass = param(weights, mass_id, 0.1).max(1.0e-6);
|
||||
let gravity = param(weights, gravity_id, 0.0);
|
||||
let drag = param(weights, drag_id, 1.0);
|
||||
let damping = param(weights, damping_id, 0.2);
|
||||
let max_effect = param(weights, max_id, 0.0);
|
||||
let spring = param(weights, spring_id, 10.0);
|
||||
let gain = param(weights, gain_id, 10.0);
|
||||
let scaled_dt = dt * 30.0;
|
||||
let joint_velocity =
|
||||
dot(subtract(joint, channel.world_position), channel.direction) * 100.0 / scaled_dt;
|
||||
let acceleration = ((joint_velocity - channel.joint_velocity) / scaled_dt) / 3.0
|
||||
+ channel.joint_acceleration * (2.0 / 3.0);
|
||||
if max_effect == 0.0 && channel.position.clamp(0.0, 1.0) == target {
|
||||
channel.last_time = time;
|
||||
channel.world_position = joint;
|
||||
channel.joint_velocity = joint_velocity;
|
||||
channel.joint_acceleration = acceleration;
|
||||
return target;
|
||||
}
|
||||
let steps = (dt / 0.05) as u32 + 1;
|
||||
let sub_dt = dt / steps as f32;
|
||||
let mut position = channel.position.clamp(0.0, 1.0);
|
||||
let mut velocity = channel.velocity;
|
||||
for _ in 0..steps {
|
||||
let force = gain * acceleration * mass
|
||||
+ dot([0.0, 0.0, 1.0], channel.direction) * gravity * mass
|
||||
- (position - target) * spring
|
||||
- damping * velocity
|
||||
+ 0.5 * drag * joint_velocity * joint_velocity * joint_velocity.signum();
|
||||
velocity = (velocity + force / mass * sub_dt).clamp(-100.0, 100.0);
|
||||
position += velocity * sub_dt;
|
||||
if (position < 0.0 && velocity < 0.0) || (position > 1.0 && velocity > 0.0) {
|
||||
velocity = 0.0;
|
||||
}
|
||||
}
|
||||
if !position.is_finite() || !velocity.is_finite() {
|
||||
position = 0.0;
|
||||
velocity = 0.0;
|
||||
channel.joint_velocity = 0.0;
|
||||
channel.joint_acceleration = 0.0;
|
||||
}
|
||||
channel.last_time = time;
|
||||
channel.position = position;
|
||||
channel.velocity = velocity;
|
||||
channel.joint_velocity = joint_velocity;
|
||||
channel.joint_acceleration = acceleration;
|
||||
channel.world_position = joint;
|
||||
position.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn exposes_all_driven_channels_and_resets_to_driver_weights() {
|
||||
let simulator = AvatarPhysicsSimulator::native_new().unwrap();
|
||||
simulator
|
||||
.native_set_wearable_params(HashMap::from([(1100, 0.25)]))
|
||||
.unwrap();
|
||||
let output = simulator.native_tick(1.0).unwrap();
|
||||
assert_eq!(output.len(), 8);
|
||||
assert!((0.0..=1.0).contains(&output[&1200]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
//! Deterministic reference renderer built on the checked native prim mesher.
|
||||
|
||||
#![allow(clippy::cast_possible_truncation)] // Checked C# float-to-int twist conversion.
|
||||
#![allow(clippy::missing_errors_doc)] // Public signatures are fixed by the API mapping.
|
||||
#![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped surface.
|
||||
#![allow(clippy::needless_pass_by_value)] // Mapped value parameters are owned.
|
||||
#![allow(clippy::unnecessary_wraps)] // The mapped constructor is fallible in the C# API.
|
||||
#![allow(clippy::unused_self)] // IRendering exposes these operations as instance methods.
|
||||
|
||||
use libremetaverse::rendering::{
|
||||
DetailLevel, Face, FaceMask, FacetedMesh, IRendering, SimpleMesh, Vertex,
|
||||
};
|
||||
use libremetaverse::{MappingType, Primitive, PrimitiveTextureEntryFace};
|
||||
use libremetaverse_imaging::ManagedImage;
|
||||
use libremetaverse_prim_mesher::{
|
||||
PathType, PrimMesh, SculptMesh, SculptMeshSculptType, VertexIndexer,
|
||||
};
|
||||
use libremetaverse_types::compat::Object;
|
||||
use libremetaverse_types::{
|
||||
Error, HoleType, PathCurve, ProfileCurve, SculptType, Vector2, Vector3,
|
||||
};
|
||||
|
||||
const MAX_FACE_VERTICES: usize = 65_536;
|
||||
const MAX_TOTAL_VERTICES: usize = MAX_FACE_VERTICES;
|
||||
const MAX_TOTAL_INDICES: usize = MAX_TOTAL_VERTICES * 6;
|
||||
|
||||
/// Small, deterministic renderer for prim and sculpt geometry.
|
||||
pub(crate) struct SimpleRenderer;
|
||||
|
||||
impl SimpleRenderer {
|
||||
pub(crate) fn native_new() -> Result<Self, Error> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
pub(crate) fn native_generate_simple_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
lod: DetailLevel,
|
||||
) -> Result<SimpleMesh, Error> {
|
||||
let source = prim.id;
|
||||
let faceted = self.native_generate_faceted_mesh(prim, lod)?;
|
||||
flatten_faces(faceted.faces, source)
|
||||
}
|
||||
|
||||
pub(crate) fn native_generate_simple_sculpt_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
sculpt_texture: ManagedImage,
|
||||
lod: DetailLevel,
|
||||
) -> Result<SimpleMesh, Error> {
|
||||
let source = prim.id;
|
||||
let faceted = self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)?;
|
||||
flatten_faces(faceted.faces, source)
|
||||
}
|
||||
|
||||
pub(crate) fn native_generate_faceted_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
lod: DetailLevel,
|
||||
) -> Result<FacetedMesh, Error> {
|
||||
let source = prim.id;
|
||||
let mesh = generate_prim_mesh(&prim, lod, source)?;
|
||||
let indexer = mesh
|
||||
.get_vertex_indexer()
|
||||
.map_err(|_| render_error(source, "index primitive viewer faces"))?
|
||||
.ok_or_else(|| render_error(source, "primitive viewer faces are unavailable"))?;
|
||||
let faces = indexed_faces(&prim, indexer, source)?;
|
||||
Ok(FacetedMesh {
|
||||
faces,
|
||||
skin_data: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_generate_faceted_sculpt_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
sculpt_texture: ManagedImage,
|
||||
lod: DetailLevel,
|
||||
) -> Result<FacetedMesh, Error> {
|
||||
let source = prim.id;
|
||||
let sculpt = prim
|
||||
.sculpt
|
||||
.as_ref()
|
||||
.ok_or_else(|| render_error(source, "primitive has no sculpt metadata"))?;
|
||||
let sculpt_type = match sculpt.type_() {
|
||||
SculptType::Cylinder => SculptMeshSculptType::Cylinder,
|
||||
SculptType::Plane => SculptMeshSculptType::Plane,
|
||||
SculptType::Sphere => SculptMeshSculptType::Sphere,
|
||||
SculptType::Torus => SculptMeshSculptType::Torus,
|
||||
SculptType::Mesh => {
|
||||
return Err(render_error(
|
||||
source,
|
||||
"mesh sculpt requires a decoded mesh asset",
|
||||
));
|
||||
}
|
||||
SculptType::None | SculptType::Invert | SculptType::Mirror => {
|
||||
return Err(render_error(source, "unsupported sculpt topology"));
|
||||
}
|
||||
};
|
||||
let mesher_lod = match lod {
|
||||
DetailLevel::Highest | DetailLevel::High => 32,
|
||||
DetailLevel::Medium => 16,
|
||||
DetailLevel::Low => 8,
|
||||
};
|
||||
let mesh = SculptMesh::new_with_managed_image_sculpt_type_int32_boolean_boolean_boolean(
|
||||
sculpt_texture,
|
||||
sculpt_type,
|
||||
mesher_lod,
|
||||
true,
|
||||
sculpt.mirror(),
|
||||
sculpt.invert(),
|
||||
)
|
||||
.map_err(|_| render_error(source, "generate bounded sculpt geometry"))?;
|
||||
if mesh.coords.len() > MAX_FACE_VERTICES
|
||||
|| mesh.coords.len() != mesh.normals.len()
|
||||
|| mesh.coords.len() != mesh.uvs.len()
|
||||
{
|
||||
return Err(render_error(source, "invalid sculpt vertex domains"));
|
||||
}
|
||||
let vertices = mesh
|
||||
.coords
|
||||
.iter()
|
||||
.zip(&mesh.normals)
|
||||
.zip(&mesh.uvs)
|
||||
.map(|((position, normal), uv)| {
|
||||
checked_vertex(
|
||||
Vector3 {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
z: position.z,
|
||||
},
|
||||
Vector3 {
|
||||
x: normal.x,
|
||||
y: normal.y,
|
||||
z: normal.z,
|
||||
},
|
||||
Vector2 { x: uv.u, y: uv.v },
|
||||
source,
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut indices = Vec::with_capacity(
|
||||
mesh.faces
|
||||
.len()
|
||||
.checked_mul(3)
|
||||
.ok_or_else(|| render_error(source, "sculpt index count overflow"))?,
|
||||
);
|
||||
for face in mesh.faces {
|
||||
push_triangle(
|
||||
&mut indices,
|
||||
face.v1,
|
||||
face.v2,
|
||||
face.v3,
|
||||
vertices.len(),
|
||||
source,
|
||||
)?;
|
||||
}
|
||||
let texture = texture_for_face(&prim, 0, source)?;
|
||||
let face = make_face(0, vertices, indices, texture, source)?;
|
||||
Ok(FacetedMesh {
|
||||
faces: vec![face],
|
||||
skin_data: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_transform_tex_coords(
|
||||
&self,
|
||||
vertices: &mut Vec<Vertex>,
|
||||
center: Vector3,
|
||||
te_face: PrimitiveTextureEntryFace,
|
||||
prim_scale: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
if !finite3(center)
|
||||
|| !finite3(prim_scale)
|
||||
|| ![
|
||||
te_face.repeat_u(),
|
||||
te_face.repeat_v(),
|
||||
te_face.offset_u(),
|
||||
te_face.offset_v(),
|
||||
te_face.rotation(),
|
||||
]
|
||||
.into_iter()
|
||||
.all(f32::is_finite)
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let cosine = te_face.rotation().cos();
|
||||
let sine = te_face.rotation().sin();
|
||||
for vertex in vertices {
|
||||
if !finite3(vertex.position) || !finite3(vertex.normal) || !finite2(vertex.tex_coord) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if te_face.tex_map_type() == MappingType::Planar {
|
||||
let mut binormal;
|
||||
let normal_x = vertex.normal.x;
|
||||
if (-0.5..0.5).contains(&normal_x) {
|
||||
binormal = Vector3::unit_x();
|
||||
if vertex.normal.y > 0.0 {
|
||||
binormal.x = -1.0;
|
||||
}
|
||||
} else {
|
||||
binormal = Vector3::unit_y();
|
||||
if normal_x < 0.0 {
|
||||
binormal.y = -1.0;
|
||||
}
|
||||
}
|
||||
let tangent = cross(binormal, vertex.normal);
|
||||
let scaled = Vector3 {
|
||||
x: vertex.position.x * prim_scale.x,
|
||||
y: vertex.position.y * prim_scale.y,
|
||||
z: vertex.position.z * prim_scale.z,
|
||||
};
|
||||
vertex.tex_coord.x = 0.5 + dot(binormal, scaled) * 2.0;
|
||||
vertex.tex_coord.y = 0.5 - dot(tangent, scaled) * 2.0;
|
||||
}
|
||||
let x = vertex.tex_coord.x - 0.5;
|
||||
let y = vertex.tex_coord.y - 0.5;
|
||||
vertex.tex_coord.x =
|
||||
(x * cosine + y * sine) * te_face.repeat_u() + te_face.offset_u() + 0.5;
|
||||
vertex.tex_coord.y =
|
||||
(-x * sine + y * cosine) * te_face.repeat_v() + te_face.offset_v() + 0.5;
|
||||
if !finite2(vertex.tex_coord) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl IRendering for SimpleRenderer {
|
||||
fn generate_faceted_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
lod: DetailLevel,
|
||||
) -> Result<Option<FacetedMesh>, Error> {
|
||||
self.native_generate_faceted_mesh(prim, lod).map(Some)
|
||||
}
|
||||
|
||||
fn generate_faceted_sculpt_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
sculpt_texture: ManagedImage,
|
||||
lod: DetailLevel,
|
||||
) -> Result<Option<FacetedMesh>, Error> {
|
||||
self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn generate_simple_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
lod: DetailLevel,
|
||||
) -> Result<Option<SimpleMesh>, Error> {
|
||||
self.native_generate_simple_mesh(prim, lod).map(Some)
|
||||
}
|
||||
|
||||
fn generate_simple_sculpt_mesh(
|
||||
&self,
|
||||
prim: Primitive,
|
||||
sculpt_texture: ManagedImage,
|
||||
lod: DetailLevel,
|
||||
) -> Result<Option<SimpleMesh>, Error> {
|
||||
self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn transform_tex_coords(
|
||||
&self,
|
||||
vertices: &mut Vec<Vertex>,
|
||||
center: Vector3,
|
||||
te_face: PrimitiveTextureEntryFace,
|
||||
prim_scale: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
self.native_transform_tex_coords(vertices, center, te_face, prim_scale)
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_prim_mesh(
|
||||
prim: &Primitive,
|
||||
lod: DetailLevel,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<PrimMesh, Error> {
|
||||
let data = &prim.prim_data;
|
||||
let parameters = [
|
||||
data.profile_begin,
|
||||
data.profile_end,
|
||||
data.profile_hollow,
|
||||
data.path_scale_x,
|
||||
data.path_scale_y,
|
||||
data.path_begin,
|
||||
data.path_end,
|
||||
data.path_shear_x,
|
||||
data.path_shear_y,
|
||||
data.path_radius_offset,
|
||||
data.path_revolutions,
|
||||
data.path_skew,
|
||||
data.path_taper_x,
|
||||
data.path_taper_y,
|
||||
data.path_twist_begin,
|
||||
data.path_twist,
|
||||
];
|
||||
if !parameters.into_iter().all(f32::is_finite) {
|
||||
return Err(render_error(
|
||||
source,
|
||||
"non-finite primitive construction data",
|
||||
));
|
||||
}
|
||||
let (mut sides, mut profile_begin, mut profile_end, sphere_mode) =
|
||||
match data.profile_curve_with_property() {
|
||||
ProfileCurve::Circle => (lod_sides(lod), data.profile_begin, data.profile_end, false),
|
||||
ProfileCurve::EqualTriangle
|
||||
| ProfileCurve::IsoTriangle
|
||||
| ProfileCurve::RightTriangle => (3, data.profile_begin, data.profile_end, false),
|
||||
ProfileCurve::HalfCircle => (
|
||||
lod_sides(lod),
|
||||
data.profile_begin.mul_add(0.5, 0.5),
|
||||
data.profile_end.mul_add(0.5, 0.5),
|
||||
true,
|
||||
),
|
||||
ProfileCurve::Square => (4, data.profile_begin, data.profile_end, false),
|
||||
};
|
||||
if sides < 3 {
|
||||
sides = 3;
|
||||
}
|
||||
profile_begin = profile_begin.clamp(0.0, 1.0);
|
||||
profile_end = profile_end.clamp(0.0, 1.0);
|
||||
let hollow_sides = if data.profile_hole() == HoleType::Circle {
|
||||
lod_sides(lod)
|
||||
} else if data.profile_hole() == HoleType::Triangle {
|
||||
3
|
||||
} else if data.profile_hole() == HoleType::Same {
|
||||
sides
|
||||
} else {
|
||||
4
|
||||
};
|
||||
let mut mesh = PrimMesh::new(
|
||||
sides,
|
||||
profile_begin,
|
||||
profile_end,
|
||||
data.profile_hollow,
|
||||
hollow_sides,
|
||||
)
|
||||
.map_err(|_| render_error(source, "initialize primitive profile"))?;
|
||||
mesh.viewer_mode = true;
|
||||
mesh.sphere_mode = sphere_mode;
|
||||
mesh.hole_size_x = data.path_scale_x;
|
||||
mesh.hole_size_y = data.path_scale_y;
|
||||
mesh.path_cut_begin = data.path_begin;
|
||||
mesh.path_cut_end = data.path_end;
|
||||
mesh.top_shear_x = data.path_shear_x;
|
||||
mesh.top_shear_y = data.path_shear_y;
|
||||
mesh.radius = data.path_radius_offset;
|
||||
mesh.revolutions = data.path_revolutions;
|
||||
mesh.skew = data.path_skew;
|
||||
mesh.steps_per_revolution = lod_sides(lod);
|
||||
let path_type = if matches!(data.path_curve, PathCurve::Line | PathCurve::Flexible) {
|
||||
mesh.taper_x = 1.0 - data.path_scale_x;
|
||||
mesh.taper_y = 1.0 - data.path_scale_y;
|
||||
mesh.twist_begin = checked_twist(data.path_twist_begin, 180.0, source)?;
|
||||
mesh.twist_end = checked_twist(data.path_twist, 180.0, source)?;
|
||||
PathType::Linear
|
||||
} else {
|
||||
mesh.taper_x = data.path_taper_x;
|
||||
mesh.taper_y = data.path_taper_y;
|
||||
mesh.twist_begin = checked_twist(data.path_twist_begin, 360.0, source)?;
|
||||
mesh.twist_end = checked_twist(data.path_twist, 360.0, source)?;
|
||||
PathType::Circular
|
||||
};
|
||||
mesh.extrude(path_type)
|
||||
.map_err(|_| render_error(source, "extrude primitive geometry"))?;
|
||||
if mesh.coords.len() > MAX_TOTAL_VERTICES || mesh.viewer_faces.len() > MAX_TOTAL_INDICES / 3 {
|
||||
return Err(render_error(
|
||||
source,
|
||||
"primitive geometry exceeds renderer bounds",
|
||||
));
|
||||
}
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
fn indexed_faces(
|
||||
prim: &Primitive,
|
||||
indexer: VertexIndexer,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<Vec<Face>, Error> {
|
||||
let count = usize::try_from(indexer.num_prim_faces)
|
||||
.map_err(|_| render_error(source, "invalid primitive face count"))?;
|
||||
if count != indexer.viewer_vertices.len() || count != indexer.viewer_polygons.len() {
|
||||
return Err(render_error(source, "misaligned primitive face domains"));
|
||||
}
|
||||
let mut total_vertices = 0usize;
|
||||
let mut total_indices = 0usize;
|
||||
let mut faces = Vec::with_capacity(count);
|
||||
for face_index in 0..count {
|
||||
let source_vertices = &indexer.viewer_vertices[face_index];
|
||||
let polygons = indexer.viewer_polygons[face_index]
|
||||
.as_ref()
|
||||
.ok_or_else(|| render_error(source, "primitive face has no polygon domain"))?;
|
||||
if source_vertices.is_empty() {
|
||||
if polygons.is_empty() {
|
||||
continue;
|
||||
}
|
||||
return Err(render_error(
|
||||
source,
|
||||
"primitive face polygons have no vertex domain",
|
||||
));
|
||||
}
|
||||
if source_vertices.len() > MAX_FACE_VERTICES {
|
||||
return Err(render_error(
|
||||
source,
|
||||
"primitive face exceeds 16-bit vertex range",
|
||||
));
|
||||
}
|
||||
total_vertices = total_vertices
|
||||
.checked_add(source_vertices.len())
|
||||
.ok_or_else(|| render_error(source, "primitive vertex count overflow"))?;
|
||||
if total_vertices > MAX_TOTAL_VERTICES {
|
||||
return Err(render_error(source, "primitive vertex budget exceeded"));
|
||||
}
|
||||
let vertices = source_vertices
|
||||
.iter()
|
||||
.map(|vertex| {
|
||||
checked_vertex(
|
||||
Vector3 {
|
||||
x: vertex.v.x,
|
||||
y: vertex.v.y,
|
||||
z: vertex.v.z,
|
||||
},
|
||||
Vector3 {
|
||||
x: vertex.n.x,
|
||||
y: vertex.n.y,
|
||||
z: vertex.n.z,
|
||||
},
|
||||
Vector2 {
|
||||
x: vertex.uv.u,
|
||||
y: 1.0 - vertex.uv.v,
|
||||
},
|
||||
source,
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut indices = Vec::with_capacity(
|
||||
polygons
|
||||
.len()
|
||||
.checked_mul(3)
|
||||
.ok_or_else(|| render_error(source, "primitive index count overflow"))?,
|
||||
);
|
||||
for polygon in polygons {
|
||||
if polygon.v1 == polygon.v2 || polygon.v1 == polygon.v3 || polygon.v2 == polygon.v3 {
|
||||
continue;
|
||||
}
|
||||
push_triangle(
|
||||
&mut indices,
|
||||
polygon.v1,
|
||||
polygon.v2,
|
||||
polygon.v3,
|
||||
vertices.len(),
|
||||
source,
|
||||
)?;
|
||||
}
|
||||
total_indices = total_indices
|
||||
.checked_add(indices.len())
|
||||
.ok_or_else(|| render_error(source, "primitive index count overflow"))?;
|
||||
if total_indices > MAX_TOTAL_INDICES {
|
||||
return Err(render_error(source, "primitive index budget exceeded"));
|
||||
}
|
||||
let texture = texture_for_face(prim, face_index, source)?;
|
||||
faces.push(make_face(
|
||||
i32::try_from(face_index)
|
||||
.map_err(|_| render_error(source, "primitive face id overflow"))?,
|
||||
vertices,
|
||||
indices,
|
||||
texture,
|
||||
source,
|
||||
)?);
|
||||
}
|
||||
Ok(faces)
|
||||
}
|
||||
|
||||
fn flatten_faces(
|
||||
faces: Vec<Face>,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<SimpleMesh, Error> {
|
||||
let mut vertices = Vec::new();
|
||||
let mut indices = Vec::new();
|
||||
for face in faces {
|
||||
let base = vertices.len();
|
||||
let combined = base
|
||||
.checked_add(face.vertices.len())
|
||||
.ok_or_else(|| render_error(source, "simple mesh vertex count overflow"))?;
|
||||
if combined > MAX_TOTAL_VERTICES {
|
||||
return Err(render_error(
|
||||
source,
|
||||
"simple mesh exceeds 16-bit vertex range",
|
||||
));
|
||||
}
|
||||
for index in face.indices {
|
||||
let global = base
|
||||
.checked_add(usize::from(index))
|
||||
.ok_or_else(|| render_error(source, "simple mesh index overflow"))?;
|
||||
if global >= combined {
|
||||
return Err(render_error(source, "simple mesh index is out of range"));
|
||||
}
|
||||
indices.push(
|
||||
u16::try_from(global)
|
||||
.map_err(|_| render_error(source, "simple mesh index exceeds 16 bits"))?,
|
||||
);
|
||||
}
|
||||
vertices.extend(face.vertices);
|
||||
}
|
||||
if indices.len() > MAX_TOTAL_INDICES {
|
||||
return Err(render_error(source, "simple mesh index budget exceeded"));
|
||||
}
|
||||
Ok(SimpleMesh { indices, vertices })
|
||||
}
|
||||
|
||||
fn texture_for_face(
|
||||
prim: &Primitive,
|
||||
index: usize,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<PrimitiveTextureEntryFace, Error> {
|
||||
let Some(textures) = prim.textures.as_ref() else {
|
||||
return Ok(PrimitiveTextureEntryFace::default());
|
||||
};
|
||||
let index =
|
||||
u32::try_from(index).map_err(|_| render_error(source, "texture face index overflow"))?;
|
||||
textures
|
||||
.get_face(index)
|
||||
.map_err(|_| render_error(source, "texture face index is out of range"))
|
||||
.map(|face| face.cloned().unwrap_or_default())
|
||||
}
|
||||
|
||||
fn make_face(
|
||||
id: i32,
|
||||
vertices: Vec<Vertex>,
|
||||
indices: Vec<u16>,
|
||||
texture_face: PrimitiveTextureEntryFace,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<Face, Error> {
|
||||
let first = vertices
|
||||
.first()
|
||||
.ok_or_else(|| render_error(source, "render face has no vertices"))?;
|
||||
let mut min = first.position;
|
||||
let mut max = first.position;
|
||||
for vertex in &vertices[1..] {
|
||||
min.x = min.x.min(vertex.position.x);
|
||||
min.y = min.y.min(vertex.position.y);
|
||||
min.z = min.z.min(vertex.position.z);
|
||||
max.x = max.x.max(vertex.position.x);
|
||||
max.y = max.y.max(vertex.position.y);
|
||||
max.z = max.z.max(vertex.position.z);
|
||||
}
|
||||
let center = Vector3 {
|
||||
x: (min.x + max.x) * 0.5,
|
||||
y: (min.y + max.y) * 0.5,
|
||||
z: (min.z + max.z) * 0.5,
|
||||
};
|
||||
Ok(Face {
|
||||
begin_s: 0,
|
||||
begin_t: 0,
|
||||
center,
|
||||
edge: Vec::new(),
|
||||
id,
|
||||
indices,
|
||||
mask: FaceMask::SINGLE,
|
||||
max_extent: max,
|
||||
min_extent: min,
|
||||
normalized_scale: Vector3 {
|
||||
x: max.x - min.x,
|
||||
y: max.y - min.y,
|
||||
z: max.z - min.z,
|
||||
},
|
||||
num_s: 0,
|
||||
num_t: 0,
|
||||
tex_coords1: None,
|
||||
texture_face,
|
||||
user_data: Object::Undefined,
|
||||
vertices,
|
||||
weights: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn checked_vertex(
|
||||
position: Vector3,
|
||||
normal: Vector3,
|
||||
tex_coord: Vector2,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<Vertex, Error> {
|
||||
if !finite3(position) || !finite3(normal) || !finite2(tex_coord) {
|
||||
return Err(render_error(
|
||||
source,
|
||||
"renderer produced a non-finite vertex",
|
||||
));
|
||||
}
|
||||
Ok(Vertex {
|
||||
position,
|
||||
normal,
|
||||
tex_coord,
|
||||
})
|
||||
}
|
||||
|
||||
fn push_triangle(
|
||||
indices: &mut Vec<u16>,
|
||||
v1: i32,
|
||||
v2: i32,
|
||||
v3: i32,
|
||||
vertex_count: usize,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<(), Error> {
|
||||
for index in [v1, v2, v3] {
|
||||
let index =
|
||||
usize::try_from(index).map_err(|_| render_error(source, "negative renderer index"))?;
|
||||
if index >= vertex_count {
|
||||
return Err(render_error(source, "renderer index is out of range"));
|
||||
}
|
||||
indices.push(
|
||||
u16::try_from(index)
|
||||
.map_err(|_| render_error(source, "renderer index exceeds 16 bits"))?,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn checked_twist(
|
||||
value: f32,
|
||||
multiplier: f32,
|
||||
source: libremetaverse_types::UUID,
|
||||
) -> Result<i32, Error> {
|
||||
let degrees = value * multiplier;
|
||||
if !degrees.is_finite() || degrees.abs() > 360_000.0 {
|
||||
return Err(render_error(source, "primitive twist is out of range"));
|
||||
}
|
||||
Ok(degrees as i32)
|
||||
}
|
||||
|
||||
const fn lod_sides(lod: DetailLevel) -> i32 {
|
||||
match lod {
|
||||
DetailLevel::Low => 6,
|
||||
DetailLevel::Medium => 12,
|
||||
DetailLevel::High | DetailLevel::Highest => 24,
|
||||
}
|
||||
}
|
||||
|
||||
const fn render_error(source: libremetaverse_types::UUID, context: &'static str) -> Error {
|
||||
Error::Rendering { source, context }
|
||||
}
|
||||
|
||||
const fn finite2(value: Vector2) -> bool {
|
||||
value.x.is_finite() && value.y.is_finite()
|
||||
}
|
||||
|
||||
const fn finite3(value: Vector3) -> bool {
|
||||
value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
|
||||
}
|
||||
|
||||
const fn dot(left: Vector3, right: Vector3) -> f32 {
|
||||
left.x * right.x + left.y * right.y + left.z * right.z
|
||||
}
|
||||
|
||||
const fn cross(left: Vector3, right: Vector3) -> Vector3 {
|
||||
Vector3 {
|
||||
x: left.y * right.z - left.z * right.y,
|
||||
y: left.z * right.x - left.x * right.z,
|
||||
z: left.x * right.y - left.y * right.x,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
//! Native callback and legacy asynchronous-delegate adapters.
|
||||
|
||||
use std::any::Any;
|
||||
use std::fmt;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
||||
use libremetaverse_types::compat::{DictionaryEntry, Object};
|
||||
|
||||
use crate::{DictionaryEventAction, Error};
|
||||
|
||||
/// Completion object returned by mapped delegate `BeginInvoke` methods.
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncInvocation {
|
||||
state: Arc<AsyncInvocationState>,
|
||||
async_state: Object,
|
||||
}
|
||||
|
||||
struct AsyncInvocationState {
|
||||
result: Mutex<Option<Result<(), Error>>>,
|
||||
completed: Condvar,
|
||||
}
|
||||
|
||||
impl fmt::Debug for AsyncInvocation {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("AsyncInvocation")
|
||||
.field("async_state", &self.async_state)
|
||||
.field("completed", &self.is_completed())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncInvocation {
|
||||
pub(crate) fn pending(async_state: Object) -> Self {
|
||||
Self {
|
||||
state: Arc::new(AsyncInvocationState {
|
||||
result: Mutex::new(None),
|
||||
completed: Condvar::new(),
|
||||
}),
|
||||
async_state,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn complete(&self, result: Result<(), Error>) {
|
||||
*self
|
||||
.state
|
||||
.result
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(result);
|
||||
self.state.completed.notify_all();
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_completed(&self) -> bool {
|
||||
self.state
|
||||
.result
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn async_state(&self) -> &Object {
|
||||
&self.async_state
|
||||
}
|
||||
|
||||
pub(crate) fn wait(&self) -> Result<(), Error> {
|
||||
let result = self
|
||||
.state
|
||||
.result
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let result = self
|
||||
.state
|
||||
.completed
|
||||
.wait_while(result, |result| result.is_none())
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
result.expect("completion predicate guarantees a result")
|
||||
}
|
||||
}
|
||||
|
||||
type DictionaryHandler = Arc<dyn Fn(DictionaryEventAction, DictionaryEntry) + Send + Sync>;
|
||||
|
||||
/// Opaque target accepted by the metadata-compatible delegate constructor.
|
||||
pub struct DictionaryCallbackTarget(pub DictionaryHandler);
|
||||
|
||||
impl fmt::Debug for DictionaryCallbackTarget {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("DictionaryCallbackTarget(..)")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DictionaryChangeCallback(DictionaryHandler);
|
||||
|
||||
impl DictionaryChangeCallback {
|
||||
pub fn from_callback(
|
||||
callback: impl Fn(DictionaryEventAction, DictionaryEntry) + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Self(Arc::new(callback))
|
||||
}
|
||||
|
||||
pub fn new(object: Object, method: isize) -> Result<Self, Error> {
|
||||
if method != 0 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
object
|
||||
.downcast_arc::<DictionaryCallbackTarget>()
|
||||
.map(|target| Self(Arc::clone(&target.0)))
|
||||
.ok_or(Error::Argument)
|
||||
}
|
||||
|
||||
pub fn begin_invoke(
|
||||
&self,
|
||||
action: DictionaryEventAction,
|
||||
entry: DictionaryEntry,
|
||||
callback: Box<dyn Fn(&dyn Any) + Send + Sync>,
|
||||
object: Object,
|
||||
) -> Result<Box<dyn Any + Send + Sync>, Error> {
|
||||
let invocation = AsyncInvocation::pending(object);
|
||||
let worker_invocation = invocation.clone();
|
||||
let handler = Arc::clone(&self.0);
|
||||
std::thread::Builder::new()
|
||||
.name("dictionary-callback".to_owned())
|
||||
.spawn(move || {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
handler(action, entry);
|
||||
}))
|
||||
.map_err(|_| Error::InvalidOperation);
|
||||
worker_invocation.complete(result);
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
callback(&worker_invocation);
|
||||
}));
|
||||
})
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
Ok(Box::new(invocation))
|
||||
}
|
||||
|
||||
pub fn end_invoke(&self, result: Box<dyn Any + Send + Sync>) -> Result<(), Error> {
|
||||
result
|
||||
.downcast::<AsyncInvocation>()
|
||||
.map_err(|_| Error::Argument)?
|
||||
.wait()
|
||||
}
|
||||
|
||||
pub fn invoke(
|
||||
&self,
|
||||
action: DictionaryEventAction,
|
||||
entry: DictionaryEntry,
|
||||
) -> Result<(), Error> {
|
||||
(self.0)(action, entry);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[test]
|
||||
fn asynchronous_delegate_can_be_observed_and_ended() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&calls);
|
||||
let callback = DictionaryChangeCallback::from_callback(move |_, _| {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
});
|
||||
let result = callback
|
||||
.begin_invoke(
|
||||
DictionaryEventAction::Add,
|
||||
DictionaryEntry(Object::Integer(1), Object::Integer(2)),
|
||||
Box::new(|_| {}),
|
||||
Object::Undefined,
|
||||
)
|
||||
.unwrap();
|
||||
callback.end_invoke(result).unwrap();
|
||||
assert_eq!(calls.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
}
|
||||
@@ -614,6 +614,7 @@ const ALL_CAPABILITIES: &[&str] = &[
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::too_many_lines)] // The seed exchange is one contiguous protocol scenario.
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{GridClient, HttpCapsClient};
|
||||
|
||||
@@ -1152,6 +1152,7 @@ impl HttpCapsClient {
|
||||
body: payload,
|
||||
};
|
||||
let response = tokio::select! {
|
||||
biased;
|
||||
response = handler.send(request, cancellation_token.clone()) => response,
|
||||
() = cancellation_token.cancelled() => return Err(Error::Cancelled),
|
||||
};
|
||||
|
||||
@@ -117,6 +117,8 @@ struct ClientRuntime {
|
||||
agent_throttle_sender: Mutex<Option<Arc<dyn crate::udp_transport::AgentThrottleSender>>>,
|
||||
http_caps_client: Mutex<crate::caps_http::HttpCapsClient>,
|
||||
caps_rate_limiter: Mutex<crate::caps_http::CapsRateLimiter>,
|
||||
utilization_statistics: Mutex<crate::stats::UtilizationStatistics>,
|
||||
throttle: Mutex<crate::AgentThrottle>,
|
||||
network_manager: Mutex<std::sync::Weak<crate::network_manager::NetworkManagerInner>>,
|
||||
agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>,
|
||||
object_manager: Mutex<Option<Arc<crate::object_manager::ObjectManagerInner>>>,
|
||||
@@ -169,6 +171,11 @@ impl ClientRuntime {
|
||||
agent_throttle_sender: Mutex::new(None),
|
||||
http_caps_client: Mutex::new(http_caps_client),
|
||||
caps_rate_limiter: Mutex::new(caps_rate_limiter),
|
||||
utilization_statistics: Mutex::new(
|
||||
crate::stats::UtilizationStatistics::new()
|
||||
.expect("utilization statistics construction is infallible"),
|
||||
),
|
||||
throttle: Mutex::new(crate::AgentThrottle::default()),
|
||||
network_manager: Mutex::new(std::sync::Weak::new()),
|
||||
agent_manager: Mutex::new(std::sync::Weak::new()),
|
||||
object_manager: Mutex::new(None),
|
||||
@@ -651,6 +658,10 @@ impl GridClient {
|
||||
self.agent_manager.as_deref_mut().expect("agent manager")
|
||||
}
|
||||
|
||||
pub(crate) fn native_agent_manager(&self) -> Result<crate::AgentManager, crate::Error> {
|
||||
crate::agent_manager::AgentManager::native_new(Some(Arc::new(self.clone())))
|
||||
}
|
||||
|
||||
pub(crate) fn cached_agent_manager_inner(
|
||||
&self,
|
||||
) -> Option<Arc<crate::agent_manager::AgentManagerInner>> {
|
||||
@@ -1142,6 +1153,38 @@ impl GridClient {
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
pub(crate) fn native_stats(&self) -> crate::stats::UtilizationStatistics {
|
||||
self.runtime
|
||||
.utilization_statistics
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_stats(&self, value: crate::stats::UtilizationStatistics) {
|
||||
*self
|
||||
.runtime
|
||||
.utilization_statistics
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
pub(crate) fn native_throttle(&self) -> crate::AgentThrottle {
|
||||
self.runtime
|
||||
.throttle
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_throttle(&self, value: crate::AgentThrottle) {
|
||||
*self
|
||||
.runtime
|
||||
.throttle
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&self) -> Result<(), ClientCoreError> {
|
||||
self.runtime.shutdown()
|
||||
}
|
||||
@@ -1181,14 +1224,115 @@ impl Drop for GridClient {
|
||||
}
|
||||
|
||||
impl crate::IGridClient for GridClient {
|
||||
fn ais_client(&self) -> crate::InventoryAISClient {
|
||||
self.native_ais_client()
|
||||
.unwrap_or_else(|_| panic!("failed to construct InventoryAISClient"))
|
||||
}
|
||||
|
||||
fn appearance(&self) -> crate::AppearanceManager {
|
||||
self.native_appearance()
|
||||
.unwrap_or_else(|_| panic!("failed to construct AppearanceManager"))
|
||||
}
|
||||
|
||||
fn assets(&self) -> crate::AssetManager {
|
||||
self.native_assets()
|
||||
.unwrap_or_else(|_| panic!("failed to construct AssetManager"))
|
||||
}
|
||||
|
||||
fn avatars(&self) -> crate::AvatarManager {
|
||||
self.native_avatars()
|
||||
.unwrap_or_else(|_| panic!("failed to construct AvatarManager"))
|
||||
}
|
||||
|
||||
fn directory(&self) -> crate::DirectoryManager {
|
||||
self.native_directory()
|
||||
.unwrap_or_else(|_| panic!("failed to construct DirectoryManager"))
|
||||
}
|
||||
|
||||
fn environment(&self) -> crate::EnvironmentManager {
|
||||
self.native_environment()
|
||||
.unwrap_or_else(|_| panic!("failed to construct EnvironmentManager"))
|
||||
}
|
||||
|
||||
fn estate(&self) -> crate::EstateTools {
|
||||
self.native_estate()
|
||||
.unwrap_or_else(|_| panic!("failed to construct EstateTools"))
|
||||
}
|
||||
|
||||
fn friends(&self) -> crate::FriendsManager {
|
||||
self.native_friends()
|
||||
.unwrap_or_else(|_| panic!("failed to construct FriendsManager"))
|
||||
}
|
||||
|
||||
fn grid(&self) -> crate::GridManager {
|
||||
self.native_grid()
|
||||
.unwrap_or_else(|_| panic!("failed to construct GridManager"))
|
||||
}
|
||||
|
||||
fn groups(&self) -> crate::GroupManager {
|
||||
self.native_groups()
|
||||
}
|
||||
|
||||
fn http_caps_client(&self) -> crate::caps_http::HttpCapsClient {
|
||||
self.native_http_caps_client()
|
||||
}
|
||||
|
||||
fn interest_list(&self) -> crate::InterestListManager {
|
||||
self.native_interest_list()
|
||||
.unwrap_or_else(|_| panic!("failed to construct InterestListManager"))
|
||||
}
|
||||
|
||||
fn inventory(&self) -> crate::InventoryManager {
|
||||
self.native_inventory()
|
||||
.unwrap_or_else(|_| panic!("failed to construct InventoryManager"))
|
||||
}
|
||||
|
||||
fn marketplace(&self) -> crate::marketplace::MarketplaceManager {
|
||||
self.native_marketplace()
|
||||
}
|
||||
|
||||
fn network(&self) -> crate::NetworkManager {
|
||||
self.native_network()
|
||||
.unwrap_or_else(|_| panic!("failed to construct NetworkManager"))
|
||||
}
|
||||
|
||||
fn objects(&self) -> crate::ObjectManager {
|
||||
self.native_objects()
|
||||
.unwrap_or_else(|_| panic!("failed to construct ObjectManager"))
|
||||
}
|
||||
|
||||
fn parcels(&self) -> crate::ParcelManager {
|
||||
self.native_parcels()
|
||||
.unwrap_or_else(|_| panic!("failed to construct ParcelManager"))
|
||||
}
|
||||
|
||||
fn self_(&self) -> crate::AgentManager {
|
||||
self.native_agent_manager()
|
||||
.unwrap_or_else(|_| panic!("failed to construct AgentManager"))
|
||||
}
|
||||
|
||||
fn settings(&self) -> Settings {
|
||||
self.settings.clone()
|
||||
}
|
||||
|
||||
fn sound(&self) -> crate::SoundManager {
|
||||
self.native_sound()
|
||||
.unwrap_or_else(|_| panic!("failed to construct SoundManager"))
|
||||
}
|
||||
|
||||
fn stats(&self) -> crate::stats::UtilizationStatistics {
|
||||
self.native_stats()
|
||||
}
|
||||
|
||||
fn terrain(&self) -> crate::TerrainManager {
|
||||
self.native_terrain()
|
||||
.unwrap_or_else(|_| panic!("failed to construct TerrainManager"))
|
||||
}
|
||||
|
||||
fn throttle(&self) -> crate::AgentThrottle {
|
||||
self.native_throttle()
|
||||
}
|
||||
|
||||
fn time_provider(&self) -> TimeProvider {
|
||||
self.time_provider.clone()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//! Value conversion helpers used by the generated `COLLADA 1.4` data model.
|
||||
|
||||
fn regex_whitespace_split(value: &str) -> Vec<&str> {
|
||||
let mut values = Vec::new();
|
||||
let mut start = 0;
|
||||
let mut whitespace = false;
|
||||
for (index, character) in value.char_indices() {
|
||||
if character.is_whitespace() {
|
||||
if !whitespace {
|
||||
values.push(&value[start..index]);
|
||||
whitespace = true;
|
||||
}
|
||||
} else if whitespace {
|
||||
start = index;
|
||||
whitespace = false;
|
||||
}
|
||||
}
|
||||
if whitespace {
|
||||
values.push("");
|
||||
} else {
|
||||
values.push(&value[start..]);
|
||||
}
|
||||
values
|
||||
}
|
||||
|
||||
pub(crate) fn parse_bools(value: &str) -> Vec<bool> {
|
||||
regex_whitespace_split(value)
|
||||
.into_iter()
|
||||
.map(|value| value == "true")
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_doubles(value: &str) -> Vec<f64> {
|
||||
regex_whitespace_split(value)
|
||||
.into_iter()
|
||||
.map(|value| value.parse().unwrap_or_default())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_longs(value: &str) -> Vec<i64> {
|
||||
regex_whitespace_split(value)
|
||||
.into_iter()
|
||||
.map(|value| value.parse().unwrap_or_default())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_names(value: &str) -> Vec<String> {
|
||||
regex_whitespace_split(value)
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn format_bools(values: &[bool]) -> String {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| if *value { "true" } else { "false" })
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
pub(crate) fn format_values<T: ToString>(values: &[T]) -> String {
|
||||
values
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::import_export::collada14::{AssetUnit, Node, NodeType};
|
||||
|
||||
#[test]
|
||||
fn whitespace_split_matches_regex_split_boundaries() {
|
||||
assert_eq!(parse_names(""), vec![""]);
|
||||
assert_eq!(parse_names(" a\t b "), vec!["", "a", "b", ""]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_numeric_elements_follow_try_parse_defaults() {
|
||||
assert_eq!(parse_doubles("1 nope 2.5"), vec![1.0, 0.0, 2.5]);
|
||||
assert_eq!(parse_longs("1 nope -2"), vec![1, 0, -2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_schema_properties_round_trip_owned_values() {
|
||||
let mut unit = AssetUnit::new().unwrap();
|
||||
assert_eq!(unit.meter(), 1.0);
|
||||
assert_eq!(unit.name(), "meter");
|
||||
unit.set_meter(0.3048);
|
||||
unit.set_name("foot".into());
|
||||
assert_eq!(unit.meter(), 0.3048);
|
||||
assert_eq!(unit.name(), "foot");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_schema_constructors_preserve_pinned_defaults() {
|
||||
let node = Node::new().unwrap();
|
||||
assert_eq!(node.type_(), NodeType::NODE);
|
||||
assert!(node.items().is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
//! Mutable orthonormal coordinate frame matching the pinned grid math behavior.
|
||||
|
||||
use libremetaverse_types::{Error, Matrix4, Quaternion, Vector3};
|
||||
use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct FrameState {
|
||||
origin: Vector3,
|
||||
x_axis: Vector3,
|
||||
y_axis: Vector3,
|
||||
z_axis: Vector3,
|
||||
}
|
||||
|
||||
/// Origin plus forward, left, and up axes used by agent camera/movement code.
|
||||
#[derive(Debug)]
|
||||
pub struct CoordinateFrame {
|
||||
state: RwLock<FrameState>,
|
||||
}
|
||||
|
||||
impl Clone for CoordinateFrame {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
state: RwLock::new(*read(&self.state)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read<T>(value: &RwLock<T>) -> RwLockReadGuard<'_, T> {
|
||||
value
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn write<T>(value: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
|
||||
value
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn finite(value: Vector3) -> bool {
|
||||
value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
|
||||
}
|
||||
|
||||
fn frame_finite(value: FrameState) -> bool {
|
||||
finite(value.origin) && finite(value.x_axis) && finite(value.y_axis) && finite(value.z_axis)
|
||||
}
|
||||
|
||||
fn orthonormalize(value: &mut FrameState) -> Result<(), Error> {
|
||||
value.x_axis = Vector3::normalize(value.x_axis)?;
|
||||
let projection = Vector3::multiply_with_vector3_single(
|
||||
value.x_axis,
|
||||
Vector3::dot(value.x_axis, value.y_axis)?,
|
||||
)?;
|
||||
value.y_axis = Vector3::normalize(Vector3::subtract(value.y_axis, projection)?)?;
|
||||
value.z_axis = Vector3::cross(value.x_axis, value.y_axis)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl CoordinateFrame {
|
||||
pub fn x_axis_with_field() -> Vector3 {
|
||||
Vector3::unit_x()
|
||||
}
|
||||
|
||||
pub fn y_axis_with_field() -> Vector3 {
|
||||
Vector3::unit_y()
|
||||
}
|
||||
|
||||
pub fn z_axis_with_field() -> Vector3 {
|
||||
Vector3::unit_z()
|
||||
}
|
||||
|
||||
pub fn new_with_vector3(origin: Vector3) -> Result<Self, Error> {
|
||||
Self::from_state(FrameState {
|
||||
origin,
|
||||
x_axis: Self::x_axis_with_field(),
|
||||
y_axis: Self::y_axis_with_field(),
|
||||
z_axis: Self::z_axis_with_field(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_vector3_matrix4(origin: Vector3, rotation: Matrix4) -> Result<Self, Error> {
|
||||
Self::from_state(FrameState {
|
||||
origin,
|
||||
x_axis: rotation.at_axis(),
|
||||
y_axis: rotation.left_axis(),
|
||||
z_axis: rotation.up_axis(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_vector3_quaternion(
|
||||
origin: Vector3,
|
||||
rotation: Quaternion,
|
||||
) -> Result<Self, Error> {
|
||||
Self::new_with_vector3_matrix4(origin, Matrix4::create_from_quaternion(rotation)?)
|
||||
}
|
||||
|
||||
pub fn new_with_vector3_vector3(origin: Vector3, direction: Vector3) -> Result<Self, Error> {
|
||||
let frame = Self::new_with_vector3(origin)?;
|
||||
frame.look_direction_with_vector3(direction)?;
|
||||
if !frame_finite(*read(&frame.state)) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
pub fn new_with_vector3_vector3_vector3_vector3(
|
||||
origin: Vector3,
|
||||
x_axis: Vector3,
|
||||
y_axis: Vector3,
|
||||
z_axis: Vector3,
|
||||
) -> Result<Self, Error> {
|
||||
Self::from_state(FrameState {
|
||||
origin,
|
||||
x_axis,
|
||||
y_axis,
|
||||
z_axis,
|
||||
})
|
||||
}
|
||||
|
||||
fn from_state(state: FrameState) -> Result<Self, Error> {
|
||||
if !frame_finite(state) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Self {
|
||||
state: RwLock::new(state),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn look_at_with_vector3_vector3(
|
||||
&self,
|
||||
origin: Vector3,
|
||||
target: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
self.look_at_with_vector3_vector3_vector3(origin, target, Self::z_axis_with_field())
|
||||
}
|
||||
|
||||
pub fn look_at_with_vector3_vector3_vector3(
|
||||
&self,
|
||||
origin: Vector3,
|
||||
target: Vector3,
|
||||
up_direction: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
if !finite(origin) || !finite(target) || !finite(up_direction) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let at = Vector3::normalize(Vector3::subtract(target, origin)?)?;
|
||||
let mut state = write(&self.state);
|
||||
state.origin = origin;
|
||||
set_look_direction(&mut state, at, up_direction)
|
||||
}
|
||||
|
||||
pub fn look_direction_with_vector3(&self, at: Vector3) -> Result<(), Error> {
|
||||
self.look_direction_with_vector3_vector3(at, Self::z_axis_with_field())
|
||||
}
|
||||
|
||||
pub fn look_direction_with_vector3_vector3(
|
||||
&self,
|
||||
at: Vector3,
|
||||
up_direction: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
if !finite(at) || !finite(up_direction) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
set_look_direction(&mut write(&self.state), at, up_direction)
|
||||
}
|
||||
|
||||
pub fn look_direction_with_double(&self, heading: f64) -> Result<(), Error> {
|
||||
if !heading.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut state = write(&self.state);
|
||||
state.y_axis = Vector3 {
|
||||
x: heading.cos() as f32,
|
||||
y: heading.sin() as f32,
|
||||
z: state.y_axis.z,
|
||||
};
|
||||
state.x_axis = Vector3 {
|
||||
x: -heading.sin() as f32,
|
||||
y: heading.cos() as f32,
|
||||
z: state.x_axis.z,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pitch(&self, angle: f32) -> Result<(), Error> {
|
||||
let axis = read(&self.state).y_axis;
|
||||
self.rotate_with_quaternion(Quaternion::create_from_axis_angle_with_vector3_single(
|
||||
axis, angle,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn reset_axes(&self) -> Result<(), Error> {
|
||||
let mut state = write(&self.state);
|
||||
state.x_axis = Self::x_axis_with_field();
|
||||
state.y_axis = Self::y_axis_with_field();
|
||||
state.z_axis = Self::z_axis_with_field();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn roll(&self, angle: f32) -> Result<(), Error> {
|
||||
let axis = read(&self.state).x_axis;
|
||||
self.rotate_with_quaternion(Quaternion::create_from_axis_angle_with_vector3_single(
|
||||
axis, angle,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn rotate_with_matrix4(&self, matrix: Matrix4) -> Result<(), Error> {
|
||||
let mut state = write(&self.state);
|
||||
state.x_axis = Vector3::transform(state.x_axis, matrix)?;
|
||||
state.y_axis = Vector3::transform(state.y_axis, matrix)?;
|
||||
orthonormalize(&mut state)?;
|
||||
if !frame_finite(*state) {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rotate_with_quaternion(&self, rotation: Quaternion) -> Result<(), Error> {
|
||||
self.rotate_with_matrix4(Matrix4::create_from_quaternion(rotation)?)
|
||||
}
|
||||
|
||||
pub fn rotate_with_single_vector3(
|
||||
&self,
|
||||
angle: f32,
|
||||
rotation_axis: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
self.rotate_with_quaternion(Quaternion::create_from_axis_angle_with_vector3_single(
|
||||
rotation_axis,
|
||||
angle,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn yaw(&self, angle: f32) -> Result<(), Error> {
|
||||
let axis = read(&self.state).z_axis;
|
||||
self.rotate_with_quaternion(Quaternion::create_from_axis_angle_with_vector3_single(
|
||||
axis, angle,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> Vector3 {
|
||||
read(&self.state).origin
|
||||
}
|
||||
|
||||
pub fn set_origin(&mut self, value: Vector3) {
|
||||
assert!(finite(value), "non-finite CoordinateFrame origin");
|
||||
write(&self.state).origin = value;
|
||||
}
|
||||
|
||||
pub fn x_axis_with_property(&self) -> Vector3 {
|
||||
read(&self.state).x_axis
|
||||
}
|
||||
|
||||
pub fn set_x_axis_with_property(&mut self, value: Vector3) {
|
||||
assert!(finite(value), "non-finite CoordinateFrame X axis");
|
||||
write(&self.state).x_axis = value;
|
||||
}
|
||||
|
||||
pub fn y_axis_with_property(&self) -> Vector3 {
|
||||
read(&self.state).y_axis
|
||||
}
|
||||
|
||||
pub fn set_y_axis_with_property(&mut self, value: Vector3) {
|
||||
assert!(finite(value), "non-finite CoordinateFrame Y axis");
|
||||
write(&self.state).y_axis = value;
|
||||
}
|
||||
|
||||
pub fn z_axis_with_property(&self) -> Vector3 {
|
||||
read(&self.state).z_axis
|
||||
}
|
||||
|
||||
pub fn set_z_axis_with_property(&mut self, value: Vector3) {
|
||||
assert!(finite(value), "non-finite CoordinateFrame Z axis");
|
||||
write(&self.state).z_axis = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_look_direction(
|
||||
state: &mut FrameState,
|
||||
mut at: Vector3,
|
||||
up_direction: Vector3,
|
||||
) -> Result<(), Error> {
|
||||
let mut left = Vector3::cross(up_direction, at)?;
|
||||
if left == Vector3::zero() {
|
||||
at.x += 0.01;
|
||||
at = Vector3::normalize(at)?;
|
||||
left = Vector3::cross(up_direction, at)?;
|
||||
}
|
||||
state.x_axis = at;
|
||||
state.y_axis = Vector3::normalize(left)?;
|
||||
state.z_axis = Vector3::cross(at, state.y_axis)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn origin_constructor_uses_grid_axes() {
|
||||
let frame = CoordinateFrame::new_with_vector3(Vector3::zero()).unwrap();
|
||||
assert_eq!(frame.x_axis_with_property(), Vector3::unit_x());
|
||||
assert_eq!(frame.y_axis_with_property(), Vector3::unit_y());
|
||||
assert_eq!(frame.z_axis_with_property(), Vector3::unit_z());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn look_direction_handles_parallel_up_without_zero_left_axis() {
|
||||
let frame = CoordinateFrame::new_with_vector3(Vector3::zero()).unwrap();
|
||||
frame
|
||||
.look_direction_with_vector3_vector3(Vector3::unit_z(), Vector3::unit_z())
|
||||
.unwrap();
|
||||
assert_ne!(frame.y_axis_with_property(), Vector3::zero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructor_rejects_non_finite_input() {
|
||||
assert!(matches!(
|
||||
CoordinateFrame::new_with_vector3(Vector3 {
|
||||
x: f32::NAN,
|
||||
y: 0.0,
|
||||
z: 0.0,
|
||||
}),
|
||||
Err(Error::Argument)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotations_keep_axes_orthonormal() {
|
||||
let frame = CoordinateFrame::new_with_vector3(Vector3::zero()).unwrap();
|
||||
frame.yaw(0.75).unwrap();
|
||||
let x = frame.x_axis_with_property();
|
||||
let y = frame.y_axis_with_property();
|
||||
let z = frame.z_axis_with_property();
|
||||
assert!((Vector3::dot(x, y).unwrap()).abs() < 1.0e-5);
|
||||
assert!((Vector3::dot(x, z).unwrap()).abs() < 1.0e-5);
|
||||
assert!((Vector3::dot(y, z).unwrap()).abs() < 1.0e-5);
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,12 @@ struct LinkRequest {
|
||||
|
||||
trait OutfitBackend: Send + Sync {
|
||||
fn store(&self) -> Option<Inventory>;
|
||||
fn folder_contents(
|
||||
&self,
|
||||
folder_id: UUID,
|
||||
owner_id: UUID,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> BackendFuture<'_, Vec<Box<dyn InventoryObjectClass>>>;
|
||||
fn attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem>;
|
||||
fn attachment_point(&self, item: UUID) -> Option<AttachmentPoint>;
|
||||
fn create_links(
|
||||
@@ -204,6 +210,28 @@ impl OutfitBackend for ClientOutfitBackend {
|
||||
.and_then(|manager| manager.native_store())
|
||||
}
|
||||
|
||||
fn folder_contents(
|
||||
&self,
|
||||
folder_id: UUID,
|
||||
owner_id: UUID,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> BackendFuture<'_, Vec<Box<dyn InventoryObjectClass>>> {
|
||||
Box::pin(async move {
|
||||
self.client()?
|
||||
.native_inventory()?
|
||||
.native_folder_contents(
|
||||
folder_id,
|
||||
owner_id,
|
||||
true,
|
||||
true,
|
||||
crate::InventorySortOrder::BY_NAME,
|
||||
cancellation_token,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem> {
|
||||
self.client
|
||||
.upgrade()
|
||||
@@ -487,7 +515,6 @@ impl CurrentOutfitFolder {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary.
|
||||
pub(crate) async fn native_get_current_outfit_links(
|
||||
&self,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
@@ -496,16 +523,20 @@ impl CurrentOutfitFolder {
|
||||
let Some(cof) = self.native_cof() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let contents = self
|
||||
.backend
|
||||
.folder_contents(
|
||||
cof.base.uuid(),
|
||||
cof.base.owner_id(),
|
||||
cancellation_token.clone(),
|
||||
)
|
||||
.await?;
|
||||
let mut links = HashMap::new();
|
||||
if let Some(store) = self.backend.store()
|
||||
&& let Ok(contents) = store.get_contents_with_uuid(cof.base.uuid())
|
||||
{
|
||||
for value in contents {
|
||||
if let Some(item) = inventory_item(value.as_ref())
|
||||
&& item.is_link()?
|
||||
{
|
||||
links.insert(item.base.uuid(), item);
|
||||
}
|
||||
for value in contents {
|
||||
if let Some(item) = inventory_item(value.as_ref())
|
||||
&& item.is_link()?
|
||||
{
|
||||
links.insert(item.base.uuid(), item);
|
||||
}
|
||||
}
|
||||
for (id, item) in &read(&self.state).shadow_links {
|
||||
@@ -1203,6 +1234,18 @@ mod tests {
|
||||
None
|
||||
}
|
||||
|
||||
fn folder_contents(
|
||||
&self,
|
||||
_folder_id: UUID,
|
||||
_owner_id: UUID,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> BackendFuture<'_, Vec<Box<dyn InventoryObjectClass>>> {
|
||||
Box::pin(async move {
|
||||
check_cancelled(cancellation_token.as_ref())?;
|
||||
Ok(Vec::new())
|
||||
})
|
||||
}
|
||||
|
||||
fn attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem> {
|
||||
read(&self.attachments)
|
||||
.iter()
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
//! Cross-platform resource cleanup helpers.
|
||||
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use libremetaverse_types::compat::{CancellationTokenSource, Close, ExternalError, Task, Thread};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
type ErrorLogger = Box<dyn Fn(String, Option<ExternalError>) + Send + Sync>;
|
||||
|
||||
fn panic_error(payload: Box<dyn std::any::Any + Send>) -> ExternalError {
|
||||
if let Some(message) = payload.downcast_ref::<&str>() {
|
||||
ExternalError((*message).to_owned())
|
||||
} else if let Some(message) = payload.downcast_ref::<String>() {
|
||||
ExternalError(message.clone())
|
||||
} else {
|
||||
ExternalError("action panicked with a non-string payload".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn report(logger: &Option<ErrorLogger>, message: impl Into<String>, error: Option<ExternalError>) {
|
||||
if let Some(logger) = logger {
|
||||
logger(message.into(), error);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helpers that contain cleanup failures and report them through a caller-owned
|
||||
/// logger, matching the upstream disposal contract.
|
||||
pub struct DisposalHelper;
|
||||
|
||||
impl DisposalHelper {
|
||||
pub fn safe_action(
|
||||
action: Box<dyn Fn() + Send + Sync>,
|
||||
action_name: Option<String>,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<(), Error> {
|
||||
if let Err(payload) = catch_unwind(AssertUnwindSafe(action)) {
|
||||
let message = action_name.filter(|name| !name.is_empty()).map_or_else(
|
||||
|| "Error executing action".to_owned(),
|
||||
|name| format!("Error executing {name}"),
|
||||
);
|
||||
report(&logger, message, Some(panic_error(payload)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn safe_cancel_and_dispose(
|
||||
mut source: Option<CancellationTokenSource>,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<(), Error> {
|
||||
let Some(source) = source.as_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
source.cancel();
|
||||
if let Err(error) = source.close() {
|
||||
report(
|
||||
&logger,
|
||||
"Error disposing CancellationTokenSource",
|
||||
Some(error),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn safe_dispose(
|
||||
mut resource: Box<dyn Close>,
|
||||
resource_name: Option<String>,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<(), Error> {
|
||||
if let Err(error) = resource.close() {
|
||||
let message = resource_name.filter(|name| !name.is_empty()).map_or_else(
|
||||
|| "Error disposing resource".to_owned(),
|
||||
|name| format!("Error disposing {name}"),
|
||||
);
|
||||
report(&logger, message, Some(error));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn safe_dispose_all(
|
||||
resources: Box<dyn Iterator<Item = Box<dyn Close>>>,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<(), Error> {
|
||||
for mut resource in resources {
|
||||
if let Err(error) = resource.close() {
|
||||
report(&logger, "Error disposing resource", Some(error));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn safe_dispose_clear<T: Close>(
|
||||
mut collection: Vec<T>,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<(), Error> {
|
||||
for resource in &mut collection {
|
||||
if let Err(error) = resource.close() {
|
||||
report(&logger, "Error disposing resource", Some(error));
|
||||
}
|
||||
}
|
||||
collection.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn safe_join_thread(
|
||||
thread: Thread,
|
||||
timeout: Duration,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<bool, Error> {
|
||||
if !thread.is_alive() {
|
||||
return Ok(true);
|
||||
}
|
||||
match thread.join_timeout(timeout) {
|
||||
Ok(true) => Ok(true),
|
||||
Ok(false) => {
|
||||
report(
|
||||
&logger,
|
||||
format!(
|
||||
"Thread {} did not exit in time",
|
||||
thread.name().unwrap_or("unnamed")
|
||||
),
|
||||
None,
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
Err(error) => {
|
||||
report(
|
||||
&logger,
|
||||
format!(
|
||||
"Error waiting for thread {}",
|
||||
thread.name().unwrap_or("unnamed")
|
||||
),
|
||||
Some(error),
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn safe_wait_task_with_task_time_span_action(
|
||||
task: Task<()>,
|
||||
timeout: Duration,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<bool, Error> {
|
||||
match task.wait_timeout(timeout) {
|
||||
Ok(Some(())) => Ok(true),
|
||||
Ok(None) => {
|
||||
report(&logger, "Task did not complete in time", None);
|
||||
Ok(false)
|
||||
}
|
||||
Err(error) => {
|
||||
report(
|
||||
&logger,
|
||||
"Error waiting for task",
|
||||
Some(ExternalError(error.to_string())),
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn safe_wait_task_with_task_time_span_action_b4795938(
|
||||
task: Task<()>,
|
||||
timeout: Duration,
|
||||
logger: Option<ErrorLogger>,
|
||||
) -> Result<bool, Error> {
|
||||
// `Task` is executor-neutral; its bounded driver parks only this worker
|
||||
// thread and is woken by the underlying future when progress is ready.
|
||||
Self::safe_wait_task_with_task_time_span_action(task, timeout, logger)
|
||||
}
|
||||
|
||||
pub async fn using_with_func_func<TDisposable: Close, TResult>(
|
||||
factory: Box<dyn Fn() -> TDisposable + Send + Sync>,
|
||||
action: Box<dyn Fn(&mut TDisposable) -> Task<Option<TResult>> + Send + Sync>,
|
||||
) -> Result<Option<TResult>, Error> {
|
||||
let mut guard = CloseGuard::new(factory());
|
||||
let result = action(guard.resource_mut()).await;
|
||||
guard.close()?;
|
||||
result
|
||||
}
|
||||
|
||||
pub fn using_with_func_func_3fbb9598<TDisposable: Close, TResult>(
|
||||
factory: Box<dyn Fn() -> TDisposable + Send + Sync>,
|
||||
action: Box<dyn Fn(&mut TDisposable) -> Option<TResult> + Send + Sync>,
|
||||
) -> Result<Option<TResult>, Error> {
|
||||
let mut guard = CloseGuard::new(factory());
|
||||
let result = action(guard.resource_mut());
|
||||
guard.close()?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
struct CloseGuard<T: Close> {
|
||||
resource: Option<T>,
|
||||
}
|
||||
|
||||
impl<T: Close> CloseGuard<T> {
|
||||
fn new(resource: T) -> Self {
|
||||
Self {
|
||||
resource: Some(resource),
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_mut(&mut self) -> &mut T {
|
||||
self.resource
|
||||
.as_mut()
|
||||
.expect("resource remains until close")
|
||||
}
|
||||
|
||||
fn close(&mut self) -> Result<(), Error> {
|
||||
let mut resource = self.resource.take().ok_or(Error::InvalidOperation)?;
|
||||
resource.close().map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Close> Drop for CloseGuard<T> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(resource) = self.resource.as_mut() {
|
||||
let _ = resource.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs one callback exactly once when explicitly disposed or dropped.
|
||||
pub struct DisposalHelperDisposalGuard {
|
||||
on_dispose: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
|
||||
}
|
||||
|
||||
impl DisposalHelperDisposalGuard {
|
||||
pub fn new(on_dispose: Box<dyn Fn() + Send + Sync>) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
on_dispose: Mutex::new(Some(on_dispose)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dispose(&self) -> Result<(), Error> {
|
||||
let callback = self
|
||||
.on_dispose
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.take();
|
||||
let Some(callback) = callback else {
|
||||
return Ok(());
|
||||
};
|
||||
catch_unwind(AssertUnwindSafe(callback)).map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
}
|
||||
|
||||
impl Close for DisposalHelperDisposalGuard {
|
||||
fn close(&mut self) -> Result<(), ExternalError> {
|
||||
self.dispose()
|
||||
.map_err(|error| ExternalError(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DisposalHelperDisposalGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Arc, atomic::AtomicUsize};
|
||||
|
||||
struct CountedClose(Arc<AtomicUsize>);
|
||||
|
||||
impl Close for CountedClose {
|
||||
fn close(&mut self) -> Result<(), ExternalError> {
|
||||
self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disposal_guard_runs_callback_once() {
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&count);
|
||||
let guard = DisposalHelperDisposalGuard::new(Box::new(move || {
|
||||
observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}))
|
||||
.unwrap();
|
||||
guard.dispose().unwrap();
|
||||
guard.dispose().unwrap();
|
||||
drop(guard);
|
||||
assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn using_closes_after_action() {
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let factory_count = Arc::clone(&count);
|
||||
let result = DisposalHelper::using_with_func_func_3fbb9598(
|
||||
Box::new(move || CountedClose(factory_count.clone())),
|
||||
Box::new(|_| Some(42)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, Some(42));
|
||||
assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_thread_join_reports_timeout_then_completion() {
|
||||
let thread = Thread::spawn(Some("disposal-test".to_owned()), || {
|
||||
std::thread::sleep(Duration::from_millis(30));
|
||||
})
|
||||
.unwrap();
|
||||
assert!(
|
||||
!DisposalHelper::safe_join_thread(thread.clone(), Duration::from_millis(1), None,)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(DisposalHelper::safe_join_thread(thread, Duration::from_secs(1), None,).unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
use libremetaverse_structured_data::{OSD, OSDMap};
|
||||
|
||||
use crate::Error;
|
||||
use crate::message_codec::MessageValue;
|
||||
|
||||
fn empty_map() -> OSDMap {
|
||||
OSDMap::new_with_constructor().expect("empty OSD map construction is infallible")
|
||||
}
|
||||
|
||||
fn map_value(map: &OSDMap, key: &str) -> Option<OSD> {
|
||||
map.get(key)
|
||||
}
|
||||
|
||||
/// Payload-preserving representation of the C# abstract upload response block.
|
||||
pub struct AssetUploaderBlock {
|
||||
pub state: String,
|
||||
payload: OSDMap,
|
||||
}
|
||||
|
||||
impl AssetUploaderBlock {
|
||||
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
|
||||
self.state = map_value(&map, "state")
|
||||
.map(|value| value.as_string())
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
self.payload = map;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<OSDMap, Error> {
|
||||
if self.payload.count() == 0 {
|
||||
let map = empty_map();
|
||||
if !self.state.is_empty() {
|
||||
map.add_with_string_osd("state".into(), OSD::from_string(self.state.clone())?)?;
|
||||
}
|
||||
Ok(map)
|
||||
} else {
|
||||
Ok(self.payload.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for AssetUploaderBlock {
|
||||
fn message_default() -> Self {
|
||||
Self {
|
||||
state: String::new(),
|
||||
payload: empty_map(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.serialize()?.copy()
|
||||
}
|
||||
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let map = OSDMap::new_with_dictionary(values.clone())?;
|
||||
let mut block = Self::message_default();
|
||||
block.deserialize(map)?;
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChatSessionRequestBlock {
|
||||
pub method: String,
|
||||
payload: OSDMap,
|
||||
}
|
||||
|
||||
impl ChatSessionRequestBlock {
|
||||
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
|
||||
self.method = map_value(&map, "method")
|
||||
.ok_or(Error::Argument)?
|
||||
.as_string()?;
|
||||
self.payload = map;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<OSDMap, Error> {
|
||||
if self.payload.count() == 0 {
|
||||
let map = empty_map();
|
||||
map.add_with_string_osd("method".into(), OSD::from_string(self.method.clone())?)?;
|
||||
Ok(map)
|
||||
} else {
|
||||
Ok(self.payload.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for ChatSessionRequestBlock {
|
||||
fn message_default() -> Self {
|
||||
Self {
|
||||
method: String::new(),
|
||||
payload: empty_map(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.serialize()?.copy()
|
||||
}
|
||||
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let mut block = Self::message_default();
|
||||
block.deserialize(OSDMap::new_with_dictionary(values.clone())?)?;
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MapLayerMessageBase {
|
||||
pub flags: i32,
|
||||
payload: OSDMap,
|
||||
}
|
||||
|
||||
impl MapLayerMessageBase {
|
||||
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
|
||||
self.flags = if let Some(value) = map.get("Flags") {
|
||||
value.as_integer()?
|
||||
} else if let Some(OSD::Map(agent)) = map.get("AgentData") {
|
||||
agent.get("Flags").ok_or(Error::Argument)?.as_integer()?
|
||||
} else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
self.payload = map;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<OSDMap, Error> {
|
||||
if self.payload.count() == 0 {
|
||||
let map = empty_map();
|
||||
map.add_with_string_osd("Flags".into(), OSD::from_integer_with_int32(self.flags)?)?;
|
||||
Ok(map)
|
||||
} else {
|
||||
Ok(self.payload.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for MapLayerMessageBase {
|
||||
fn message_default() -> Self {
|
||||
Self {
|
||||
flags: 0,
|
||||
payload: empty_map(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.serialize()?.copy()
|
||||
}
|
||||
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let mut block = Self::message_default();
|
||||
block.deserialize(OSDMap::new_with_dictionary(values.clone())?)?;
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MapLayerRequestVariant {
|
||||
pub flags: i32,
|
||||
}
|
||||
|
||||
impl MapLayerRequestVariant {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self { flags: 0 })
|
||||
}
|
||||
|
||||
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
|
||||
self.flags = map.get("Flags").ok_or(Error::Argument)?.as_integer()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<OSDMap, Error> {
|
||||
let map = empty_map();
|
||||
map.add_with_string_osd("Flags".into(), OSD::from_integer_with_int32(self.flags)?)?;
|
||||
Ok(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for MapLayerRequestVariant {
|
||||
fn message_default() -> Self {
|
||||
Self { flags: 0 }
|
||||
}
|
||||
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.serialize()?.copy()
|
||||
}
|
||||
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let mut block = Self::message_default();
|
||||
block.deserialize(OSDMap::new_with_dictionary(values.clone())?)?;
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! opaque_block {
|
||||
($name:ident) => {
|
||||
pub struct $name {
|
||||
payload: OSDMap,
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
|
||||
self.payload = map;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<OSDMap, Error> {
|
||||
Ok(self.payload.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for $name {
|
||||
fn message_default() -> Self {
|
||||
Self {
|
||||
payload: empty_map(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.serialize()?.copy()
|
||||
}
|
||||
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
Ok(Self {
|
||||
payload: OSDMap::new_with_dictionary(values.clone())?,
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
opaque_block!(ObjectMediaBlock);
|
||||
opaque_block!(RemoteParcelRequestBlock);
|
||||
opaque_block!(SearchStatRequestBlock);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dynamic_upload_block_preserves_unmodeled_fields() {
|
||||
let map = empty_map();
|
||||
map.add_with_string_osd("state".into(), OSD::from_string("complete".into()).unwrap())
|
||||
.unwrap();
|
||||
map.add_with_string_osd(
|
||||
"new_asset".into(),
|
||||
OSD::from_string("asset".into()).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut block = AssetUploaderBlock::message_default();
|
||||
block.deserialize(map).unwrap();
|
||||
assert_eq!(block.state, "complete");
|
||||
assert_eq!(
|
||||
block.serialize().unwrap().get("new_asset"),
|
||||
Some(OSD::String("asset".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_layer_base_reads_both_request_and_reply_shapes() {
|
||||
let request = empty_map();
|
||||
request
|
||||
.add_with_string_osd("Flags".into(), OSD::from_integer_with_int32(7).unwrap())
|
||||
.unwrap();
|
||||
let mut block = MapLayerMessageBase::message_default();
|
||||
block.deserialize(request).unwrap();
|
||||
assert_eq!(block.flags, 7);
|
||||
|
||||
let reply = empty_map();
|
||||
reply
|
||||
.add_with_string_osd(
|
||||
"AgentData".into(),
|
||||
OSD::Map(std::collections::HashMap::from([(
|
||||
"Flags".into(),
|
||||
OSD::Integer(9),
|
||||
)])),
|
||||
)
|
||||
.unwrap();
|
||||
block.deserialize(reply).unwrap();
|
||||
assert_eq!(block.flags, 9);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
//! Temporary event subscriptions with timeout and cancellation support.
|
||||
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::time::Duration;
|
||||
|
||||
use libremetaverse_types::compat::{
|
||||
CancellationToken, Close, EventHandler, ExternalError, TaskCompletionSource,
|
||||
};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
fn timeout_duration(timeout_ms: i32) -> Result<Option<Duration>, Error> {
|
||||
match timeout_ms {
|
||||
-1 => Ok(None),
|
||||
value if value >= 0 => Ok(Some(Duration::from_millis(value as u64))),
|
||||
_ => Err(Error::Argument),
|
||||
}
|
||||
}
|
||||
|
||||
/// Event wait helpers matching the upstream subscribe/unsubscribe lifecycle.
|
||||
pub struct EventSubscriptionHelper;
|
||||
|
||||
impl EventSubscriptionHelper {
|
||||
pub fn wait_for_condition<TEventArgs: 'static>(
|
||||
subscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
unsubscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
condition: Box<dyn Fn(&Option<TEventArgs>) -> bool + Send + Sync>,
|
||||
timeout_ms: i32,
|
||||
) -> Result<(), Error> {
|
||||
let timeout = timeout_duration(timeout_ms)?;
|
||||
let (sender, receiver) = mpsc::sync_channel(1);
|
||||
let handler: EventHandler<Option<TEventArgs>> = Arc::new(move |event| {
|
||||
if condition(&event) {
|
||||
let _ = sender.try_send(());
|
||||
}
|
||||
});
|
||||
subscribe(Arc::clone(&handler));
|
||||
let subscription = EventSubscription::from_registered(unsubscribe, handler);
|
||||
match timeout {
|
||||
Some(timeout) => {
|
||||
let _ = receiver.recv_timeout(timeout);
|
||||
}
|
||||
None => {
|
||||
let _ = receiver.recv();
|
||||
}
|
||||
}
|
||||
subscription.dispose()
|
||||
}
|
||||
|
||||
pub async fn wait_for_event_with_action_action_func_func_int32_cancellation_token_t_result<
|
||||
TEventArgs: 'static,
|
||||
TResult,
|
||||
>(
|
||||
subscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
unsubscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
filter: Box<dyn Fn(&Option<TEventArgs>) -> bool + Send + Sync>,
|
||||
result_selector: Box<dyn Fn(&Option<TEventArgs>) -> Option<TResult> + Send + Sync>,
|
||||
timeout_ms: i32,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
default_value: Option<TResult>,
|
||||
) -> Result<Option<TResult>, Error>
|
||||
where
|
||||
TResult: Send + 'static,
|
||||
{
|
||||
let timeout = timeout_duration(timeout_ms)?;
|
||||
let completion = TaskCompletionSource::new();
|
||||
let event_completion = completion.clone();
|
||||
let handler: EventHandler<Option<TEventArgs>> = Arc::new(move |event| {
|
||||
if filter(&event) {
|
||||
let _ = event_completion.try_set_result(result_selector(&event));
|
||||
}
|
||||
});
|
||||
subscribe(Arc::clone(&handler));
|
||||
let subscription = EventSubscription::from_registered(unsubscribe, handler);
|
||||
|
||||
let cancellation = cancellation_token.map(|token| {
|
||||
let completion = completion.clone();
|
||||
token.register_callback(Arc::new(move || {
|
||||
let _ = completion.try_set_cancelled();
|
||||
}))
|
||||
});
|
||||
if let Some(timeout) = timeout {
|
||||
let timeout_completion = completion.clone();
|
||||
if std::thread::Builder::new()
|
||||
.name("event-wait-timeout".to_owned())
|
||||
.spawn(move || {
|
||||
std::thread::sleep(timeout);
|
||||
let _ = timeout_completion.try_set_result(default_value);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
subscription.dispose()?;
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
}
|
||||
|
||||
let result = completion.future().await;
|
||||
drop(cancellation);
|
||||
subscription.dispose()?;
|
||||
result
|
||||
}
|
||||
|
||||
pub fn wait_for_event_with_action_action_func_func_int32_t_result<
|
||||
TEventArgs: 'static,
|
||||
TResult,
|
||||
>(
|
||||
subscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
unsubscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
filter: Box<dyn Fn(&Option<TEventArgs>) -> bool + Send + Sync>,
|
||||
result_selector: Box<dyn Fn(&Option<TEventArgs>) -> Option<TResult> + Send + Sync>,
|
||||
timeout_ms: i32,
|
||||
default_value: Option<TResult>,
|
||||
) -> Result<Option<TResult>, Error>
|
||||
where
|
||||
TResult: Send + 'static,
|
||||
{
|
||||
let timeout = timeout_duration(timeout_ms)?;
|
||||
let (sender, receiver) = mpsc::sync_channel(1);
|
||||
let handler: EventHandler<Option<TEventArgs>> = Arc::new(move |event| {
|
||||
if filter(&event) {
|
||||
let _ = sender.try_send(result_selector(&event));
|
||||
}
|
||||
});
|
||||
subscribe(Arc::clone(&handler));
|
||||
let subscription = EventSubscription::from_registered(unsubscribe, handler);
|
||||
let result = match timeout {
|
||||
Some(timeout) => receiver.recv_timeout(timeout).unwrap_or(default_value),
|
||||
None => receiver.recv().unwrap_or(default_value),
|
||||
};
|
||||
subscription.dispose()?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Exactly-once unsubscribe guard.
|
||||
pub struct EventSubscription<TEventArgs> {
|
||||
registered: Mutex<RegisteredEvent<TEventArgs>>,
|
||||
}
|
||||
|
||||
struct RegisteredEvent<TEventArgs> {
|
||||
unsubscribe: Option<Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>>,
|
||||
handler: Option<EventHandler<Option<TEventArgs>>>,
|
||||
}
|
||||
|
||||
impl<TEventArgs> EventSubscription<TEventArgs> {
|
||||
pub fn new(
|
||||
subscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
unsubscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
handler: EventHandler<Option<TEventArgs>>,
|
||||
) -> Result<Self, Error> {
|
||||
subscribe(Arc::clone(&handler));
|
||||
Ok(Self::from_registered(unsubscribe, handler))
|
||||
}
|
||||
|
||||
fn from_registered(
|
||||
unsubscribe: Box<dyn Fn(EventHandler<Option<TEventArgs>>) + Send + Sync>,
|
||||
handler: EventHandler<Option<TEventArgs>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
registered: Mutex::new(RegisteredEvent {
|
||||
unsubscribe: Some(unsubscribe),
|
||||
handler: Some(handler),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispose(&self) -> Result<(), Error> {
|
||||
let (unsubscribe, handler) = {
|
||||
let mut registered = self
|
||||
.registered
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
(registered.unsubscribe.take(), registered.handler.take())
|
||||
};
|
||||
if let (Some(unsubscribe), Some(handler)) = (unsubscribe, handler) {
|
||||
unsubscribe(handler);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<TEventArgs> Close for EventSubscription<TEventArgs> {
|
||||
fn close(&mut self) -> Result<(), ExternalError> {
|
||||
self.dispose()
|
||||
.map_err(|error| ExternalError(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<TEventArgs> Drop for EventSubscription<TEventArgs> {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[test]
|
||||
fn subscription_unsubscribes_exactly_once() {
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&count);
|
||||
let subscription = EventSubscription::<i32>::new(
|
||||
Box::new(|_| {}),
|
||||
Box::new(move |_| {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
}),
|
||||
Arc::new(|_| {}),
|
||||
)
|
||||
.unwrap();
|
||||
subscription.dispose().unwrap();
|
||||
subscription.dispose().unwrap();
|
||||
drop(subscription);
|
||||
assert_eq!(count.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synchronous_wait_selects_matching_event_and_unsubscribes() {
|
||||
let slot = Arc::new(Mutex::new(None::<EventHandler<Option<i32>>>));
|
||||
let subscribe_slot = Arc::clone(&slot);
|
||||
let unsubscribe_slot = Arc::clone(&slot);
|
||||
let emitter_slot = Arc::clone(&slot);
|
||||
let emitter = std::thread::spawn(move || {
|
||||
while emitter_slot.lock().unwrap().is_none() {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
let handler = emitter_slot.lock().unwrap().as_ref().unwrap().clone();
|
||||
handler(Some(7));
|
||||
});
|
||||
let result =
|
||||
EventSubscriptionHelper::wait_for_event_with_action_action_func_func_int32_t_result(
|
||||
Box::new(move |handler| *subscribe_slot.lock().unwrap() = Some(handler)),
|
||||
Box::new(move |_| *unsubscribe_slot.lock().unwrap() = None),
|
||||
Box::new(|event| event == &Some(7)),
|
||||
Box::new(|event| *event),
|
||||
1_000,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
emitter.join().unwrap();
|
||||
assert_eq!(result, Some(7));
|
||||
assert!(slot.lock().unwrap().is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Deterministic, cross-platform filename and two-name helpers.
|
||||
|
||||
use crate::Error;
|
||||
use libremetaverse_types::compat::Utf16CodeUnit;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub struct FileHelper;
|
||||
|
||||
impl FileHelper {
|
||||
pub fn safe_file_name(original: String) -> Result<String, Error> {
|
||||
Ok(replace_utf16(
|
||||
&original,
|
||||
is_invalid_file_name,
|
||||
u16::from(b'_'),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn safe_dir_name(original: String) -> Result<String, Error> {
|
||||
Ok(replace_utf16(&original, is_invalid_path, u16::from(b'_')))
|
||||
}
|
||||
|
||||
pub fn sanitize(
|
||||
input: String,
|
||||
invalid_chars: Vec<Utf16CodeUnit>,
|
||||
replacement: Option<Utf16CodeUnit>,
|
||||
) -> Result<String, Error> {
|
||||
let invalid: HashSet<u16> = invalid_chars.into_iter().map(|unit| unit.0).collect();
|
||||
Ok(replace_utf16(
|
||||
&input,
|
||||
|unit| invalid.contains(&unit),
|
||||
replacement.unwrap_or(Utf16CodeUnit(u16::from(b'_'))).0,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn try_parse_two_names(
|
||||
input: String,
|
||||
first: &mut Option<String>,
|
||||
last: &mut Option<String>,
|
||||
) -> bool {
|
||||
*first = None;
|
||||
*last = None;
|
||||
let mut names = input.split_whitespace();
|
||||
let Some(first_name) = names.next() else {
|
||||
return false;
|
||||
};
|
||||
let Some(last_name) = names.next() else {
|
||||
return false;
|
||||
};
|
||||
if names.next().is_some() {
|
||||
return false;
|
||||
}
|
||||
*first = Some(first_name.to_owned());
|
||||
*last = Some(last_name.to_owned());
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_utf16(input: &str, invalid: impl Fn(u16) -> bool, replacement: u16) -> String {
|
||||
let units: Vec<u16> = input
|
||||
.encode_utf16()
|
||||
.map(|unit| if invalid(unit) { replacement } else { unit })
|
||||
.collect();
|
||||
String::from_utf16_lossy(&units)
|
||||
}
|
||||
|
||||
fn is_invalid_file_name(unit: u16) -> bool {
|
||||
unit <= 31 || matches!(unit, 34 | 42 | 47 | 58 | 60 | 62 | 63 | 92 | 124)
|
||||
}
|
||||
|
||||
fn is_invalid_path(unit: u16) -> bool {
|
||||
unit <= 31 || matches!(unit, 34 | 42 | 58 | 60 | 62 | 63 | 124)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn safe_names_use_the_portable_invalid_character_union() {
|
||||
assert_eq!(
|
||||
FileHelper::safe_file_name("a:b/c\\d?.txt".to_owned()).unwrap(),
|
||||
"a_b_c_d_.txt"
|
||||
);
|
||||
assert_eq!(
|
||||
FileHelper::safe_dir_name("root/child:name".to_owned()).unwrap(),
|
||||
"root/child_name"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_operates_on_csharp_utf16_code_units() {
|
||||
let result = FileHelper::sanitize(
|
||||
"a:b🙂".to_owned(),
|
||||
vec![Utf16CodeUnit(u16::from(b':'))],
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, "a_b🙂");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exactly_two_unicode_whitespace_delimited_names_are_accepted() {
|
||||
let mut first = Some("stale".to_owned());
|
||||
let mut last = Some("stale".to_owned());
|
||||
assert!(FileHelper::try_parse_two_names(
|
||||
" Ada\u{2003}Lovelace ".to_owned(),
|
||||
&mut first,
|
||||
&mut last,
|
||||
));
|
||||
assert_eq!(first.as_deref(), Some("Ada"));
|
||||
assert_eq!(last.as_deref(), Some("Lovelace"));
|
||||
assert!(!FileHelper::try_parse_two_names(
|
||||
"Ada Byron Lovelace".to_owned(),
|
||||
&mut first,
|
||||
&mut last,
|
||||
));
|
||||
assert_eq!(first, None);
|
||||
assert_eq!(last, None);
|
||||
}
|
||||
}
|
||||
+34108
-19390
File diff suppressed because it is too large.
Load diff
@@ -877,6 +877,11 @@ impl GridManagerInner {
|
||||
&event.data,
|
||||
&mut pos,
|
||||
) {
|
||||
if let Ok(prey_index) = usize::try_from(packet.index.prey)
|
||||
&& let Some(prey) = packet.agent_data.get(prey_index)
|
||||
{
|
||||
event.simulator.native_set_prey_id(prey.agent_id);
|
||||
}
|
||||
let positions: HashMap<_, _> = packet
|
||||
.agent_data
|
||||
.into_iter()
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
//! Native implementations of the pinned static helper surface.
|
||||
|
||||
use crate::{Error, Primitive, Simulator};
|
||||
use flate2::Compression;
|
||||
use flate2::read::ZlibDecoder;
|
||||
use flate2::write::ZlibEncoder;
|
||||
use libremetaverse_structured_data::{OSD, OSDParser};
|
||||
use libremetaverse_types::UUID;
|
||||
use libremetaverse_types::compat::{ExternalError, Object, ReadWrite};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const MAX_DECOMPRESSED_OSD: u64 = 128 * 1024 * 1024;
|
||||
|
||||
pub struct Helpers;
|
||||
|
||||
impl Helpers {
|
||||
pub const MSG_APPENDED_ACKS: u8 = 0x10;
|
||||
pub const MSG_RESENT: u8 = 0x20;
|
||||
pub const MSG_RELIABLE: u8 = 0x40;
|
||||
pub const MSG_ZEROCODED: u8 = 0x80;
|
||||
|
||||
pub fn copy_stream(
|
||||
mut input: Box<dyn ReadWrite + Send>,
|
||||
mut output: Box<dyn ReadWrite + Send>,
|
||||
) -> Result<(), Error> {
|
||||
std::io::copy(&mut input, &mut output).map_err(|_| Error::InvalidOperation)?;
|
||||
output.flush().map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
|
||||
pub fn decompress_osd(mesh_bytes: Vec<u8>) -> Result<OSD, Error> {
|
||||
Self::decode_zlib(&mesh_bytes)
|
||||
}
|
||||
|
||||
pub fn float_to_terse_string(val: f32) -> Result<String, Error> {
|
||||
if val == 0.0 {
|
||||
return Ok(".00".to_owned());
|
||||
}
|
||||
if val.is_nan() {
|
||||
return Ok("NaN".to_owned());
|
||||
}
|
||||
if val == f32::INFINITY {
|
||||
return Ok("Infinity".to_owned());
|
||||
}
|
||||
if val == f32::NEG_INFINITY {
|
||||
return Ok("-Infinity".to_owned());
|
||||
}
|
||||
let mut value = format!("{val:.2}");
|
||||
while value.ends_with('0') {
|
||||
value.pop();
|
||||
}
|
||||
if value.ends_with('.') {
|
||||
value.pop();
|
||||
} else if value.starts_with("-0") {
|
||||
value.remove(1);
|
||||
} else if value.starts_with('0') {
|
||||
value.remove(0);
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn get_resource_stream_with_string(
|
||||
resource_name: String,
|
||||
) -> Result<Option<Box<dyn ReadWrite + Send>>, Error> {
|
||||
Self::get_resource_stream_with_string_string(resource_name, "openmetaverse_data".to_owned())
|
||||
}
|
||||
|
||||
pub fn get_resource_stream_with_string_string(
|
||||
resource_name: String,
|
||||
search_path: String,
|
||||
) -> Result<Option<Box<dyn ReadWrite + Send>>, Error> {
|
||||
let base = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|path| path.parent().map(std::path::Path::to_path_buf))
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
let filename = base.join(search_path).join(resource_name);
|
||||
match std::fs::OpenOptions::new().read(true).open(filename) {
|
||||
Ok(file) => Ok(Some(Box::new(file))),
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn global_pos_to_region_handle(
|
||||
global_x: f32,
|
||||
global_y: f32,
|
||||
local_x: &mut f32,
|
||||
local_y: &mut f32,
|
||||
) -> Result<u64, Error> {
|
||||
let x =
|
||||
(global_x as u32 / Simulator::DEFAULT_REGION_SIZE_X) * Simulator::DEFAULT_REGION_SIZE_X;
|
||||
let y =
|
||||
(global_y as u32 / Simulator::DEFAULT_REGION_SIZE_Y) * Simulator::DEFAULT_REGION_SIZE_Y;
|
||||
*local_x = global_x - x as f32;
|
||||
*local_y = global_y - y as f32;
|
||||
Ok((u64::from(x) << 32) | u64::from(y))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn inventory_crc(
|
||||
creation_date: i32,
|
||||
sale_type: u8,
|
||||
inv_type: i8,
|
||||
type_: i8,
|
||||
mut asset_id: UUID,
|
||||
mut group_id: UUID,
|
||||
sale_price: i32,
|
||||
mut owner_id: UUID,
|
||||
mut creator_id: UUID,
|
||||
mut item_id: UUID,
|
||||
mut folder_id: UUID,
|
||||
everyone_mask: u32,
|
||||
flags: u32,
|
||||
next_owner_mask: u32,
|
||||
group_mask: u32,
|
||||
owner_mask: u32,
|
||||
) -> Result<u32, Error> {
|
||||
let values = [
|
||||
asset_id.crc()?,
|
||||
folder_id.crc()?,
|
||||
item_id.crc()?,
|
||||
creator_id.crc()?,
|
||||
owner_id.crc()?,
|
||||
group_id.crc()?,
|
||||
owner_mask,
|
||||
next_owner_mask,
|
||||
everyone_mask,
|
||||
group_mask,
|
||||
flags,
|
||||
inv_type as u32,
|
||||
type_ as u32,
|
||||
creation_date as u32,
|
||||
sale_price as u32,
|
||||
u32::from(sale_type).wrapping_mul(0x0707_3096),
|
||||
];
|
||||
Ok(values.into_iter().fold(0_u32, u32::wrapping_add))
|
||||
}
|
||||
|
||||
pub fn osd_to_prim_list(osd: OSD) -> Result<Vec<Primitive>, Error> {
|
||||
let OSD::Map(map) = osd else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
map.into_iter()
|
||||
.map(|(local_id, value)| {
|
||||
let mut primitive = Primitive::from_osd(value)?;
|
||||
primitive.local_id = local_id.parse().map_err(|_| Error::Argument)?;
|
||||
Ok(primitive)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn prim_list_to_osd(prims: Vec<Primitive>) -> Result<OSD, Error> {
|
||||
let mut map = HashMap::with_capacity(prims.len());
|
||||
for primitive in prims {
|
||||
map.insert(primitive.local_id.to_string(), primitive.get_osd()?);
|
||||
}
|
||||
Ok(OSD::Map(map))
|
||||
}
|
||||
|
||||
pub fn safe_action(
|
||||
action: Box<dyn Fn() + Send + Sync>,
|
||||
action_name: Option<String>,
|
||||
logger: Option<Box<dyn Fn(String, Option<ExternalError>) + Send + Sync>>,
|
||||
) -> Result<(), Error> {
|
||||
if let Err(payload) = catch_unwind(AssertUnwindSafe(action)) {
|
||||
if let Some(logger) = logger {
|
||||
let label = action_name.filter(|name| !name.is_empty());
|
||||
let message = label.map_or_else(
|
||||
|| "Error executing action".to_owned(),
|
||||
|name| format!("Error executing {name}"),
|
||||
);
|
||||
let detail = payload.downcast_ref::<&str>().map_or_else(
|
||||
|| {
|
||||
payload
|
||||
.downcast_ref::<String>()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "panic".to_owned())
|
||||
},
|
||||
|value| (*value).to_owned(),
|
||||
);
|
||||
logger(message, Some(ExternalError(detail)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn split_by(
|
||||
str: String,
|
||||
chunk_length: i32,
|
||||
) -> Result<Box<dyn Iterator<Item = String>>, Error> {
|
||||
if str.is_empty() || chunk_length < 1 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let chunk_length = usize::try_from(chunk_length).map_err(|_| Error::Argument)?;
|
||||
let units: Vec<u16> = str.encode_utf16().collect();
|
||||
let chunks: Vec<String> = units
|
||||
.chunks(chunk_length)
|
||||
.map(String::from_utf16_lossy)
|
||||
.collect();
|
||||
Ok(Box::new(chunks.into_iter()))
|
||||
}
|
||||
|
||||
pub fn struct_to_string(value: Object) -> Result<String, Error> {
|
||||
if let Object::Map(fields) = value {
|
||||
let mut fields: Vec<_> = fields.into_iter().collect();
|
||||
fields.sort_unstable_by(|left, right| left.0.cmp(&right.0));
|
||||
Ok(fields
|
||||
.into_iter()
|
||||
.map(|(name, value)| format!("{name}: {value:?}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "))
|
||||
} else {
|
||||
Ok(format!("{value:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn te_glow_byte(glow: f32) -> Result<u8, Error> {
|
||||
Ok((glow * 255.0) as u8)
|
||||
}
|
||||
|
||||
pub fn te_glow_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
Ok(f32::from(*bytes.get(index(pos)?).ok_or(Error::IndexOutOfRange)?) / 255.0)
|
||||
}
|
||||
|
||||
pub fn te_offset_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
let pos = index(pos)?;
|
||||
let pair: [u8; 2] = bytes
|
||||
.get(pos..pos.checked_add(2).ok_or(Error::IndexOutOfRange)?)
|
||||
.ok_or(Error::IndexOutOfRange)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::IndexOutOfRange)?;
|
||||
Ok(f32::from(i16::from_le_bytes(pair)) / 32767.0)
|
||||
}
|
||||
|
||||
pub fn te_offset_short(offset: f32) -> Result<i16, Error> {
|
||||
Ok((offset.clamp(-1.0, 1.0) * 32767.0).round_ties_even() as i16)
|
||||
}
|
||||
|
||||
pub fn te_rotation_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
let pos = index(pos)?;
|
||||
let pair: [u8; 2] = bytes
|
||||
.get(pos..pos.checked_add(2).ok_or(Error::IndexOutOfRange)?)
|
||||
.ok_or(Error::IndexOutOfRange)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::IndexOutOfRange)?;
|
||||
Ok(f32::from(u16::from_le_bytes(pair)) / 32768.0 * std::f32::consts::TAU)
|
||||
}
|
||||
|
||||
pub fn te_rotation_short(rotation: f32) -> Result<i16, Error> {
|
||||
let remainder =
|
||||
rotation - (rotation / std::f32::consts::TAU).round_ties_even() * std::f32::consts::TAU;
|
||||
Ok((remainder / std::f32::consts::TAU * 32768.0 + 0.5).round_ties_even() as i16)
|
||||
}
|
||||
|
||||
pub fn z_compress_osd(data: OSD) -> Result<Vec<u8>, Error> {
|
||||
let binary = OSDParser::serialize_llsd_binary_with_osd_boolean(data, false)?;
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
|
||||
encoder
|
||||
.write_all(&binary)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
encoder.finish().map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
|
||||
pub fn z_decompress_osd(data: Vec<u8>) -> Result<OSD, Error> {
|
||||
Self::decode_zlib(&data)
|
||||
}
|
||||
|
||||
pub fn zero_decode(
|
||||
src: Option<&[u8]>,
|
||||
srclen: i32,
|
||||
dest: Option<&mut [u8]>,
|
||||
) -> Result<i32, Error> {
|
||||
crate::packet_wire::zero_decode(src, srclen, dest)
|
||||
}
|
||||
|
||||
pub fn zero_encode(
|
||||
src: Option<&[u8]>,
|
||||
srclen: i32,
|
||||
dest: Option<&mut [u8]>,
|
||||
) -> Result<i32, Error> {
|
||||
crate::packet_wire::zero_encode(src, srclen, dest)
|
||||
}
|
||||
|
||||
fn decode_zlib(data: &[u8]) -> Result<OSD, Error> {
|
||||
let decoder = ZlibDecoder::new(data);
|
||||
let mut limited = decoder.take(MAX_DECOMPRESSED_OSD + 1);
|
||||
let mut decoded = Vec::new();
|
||||
limited
|
||||
.read_to_end(&mut decoded)
|
||||
.map_err(|_| Error::Parse {
|
||||
position: 0,
|
||||
context: "invalid zlib-compressed LLSD",
|
||||
})?;
|
||||
if decoded.len() as u64 > MAX_DECOMPRESSED_OSD {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
OSDParser::deserialize_llsd_binary_with_bytes(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
fn index(value: i32) -> Result<usize, Error> {
|
||||
usize::try_from(value).map_err(|_| Error::IndexOutOfRange)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn terse_float_and_texture_entry_conversions_match_pinned_examples() {
|
||||
assert_eq!(Helpers::float_to_terse_string(0.0).unwrap(), ".00");
|
||||
assert_eq!(Helpers::float_to_terse_string(0.5).unwrap(), ".5");
|
||||
assert_eq!(Helpers::float_to_terse_string(-0.25).unwrap(), "-.25");
|
||||
for offset in [-1.0, -0.25, 0.0, 0.75, 1.0] {
|
||||
let encoded = Helpers::te_offset_short(offset).unwrap();
|
||||
let decoded = Helpers::te_offset_float(encoded.to_le_bytes().to_vec(), 0).unwrap();
|
||||
assert!((decoded - offset).abs() <= 1.0 / 32767.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compressed_llsd_round_trips_and_rejects_non_zlib_input() {
|
||||
let value = OSD::Map(HashMap::from([
|
||||
("grid".to_owned(), OSD::String("OpenSim".to_owned())),
|
||||
("count".to_owned(), OSD::Integer(3)),
|
||||
]));
|
||||
let compressed = Helpers::z_compress_osd(value.clone()).unwrap();
|
||||
assert_eq!(Helpers::z_decompress_osd(compressed).unwrap(), value);
|
||||
assert!(Helpers::z_decompress_osd(vec![1, 2, 3]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_by_counts_utf16_units() {
|
||||
let chunks: Vec<_> = Helpers::split_by("ab🙂cd".to_owned(), 2).unwrap().collect();
|
||||
assert_eq!(chunks, ["ab", "🙂", "cd"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_handle_returns_local_coordinates() {
|
||||
let mut local_x = 0.0;
|
||||
let mut local_y = 0.0;
|
||||
let handle =
|
||||
Helpers::global_pos_to_region_handle(300.5, 600.25, &mut local_x, &mut local_y)
|
||||
.unwrap();
|
||||
assert_eq!(handle, (256_u64 << 32) | 512);
|
||||
assert_eq!(local_x, 44.5);
|
||||
assert_eq!(local_y, 88.25);
|
||||
}
|
||||
}
|
||||
@@ -295,9 +295,10 @@ impl InventoryAISClient {
|
||||
{
|
||||
Ok((success, _, _, _)) => Ok(success),
|
||||
Err(Error::Cancelled) => Err(Error::Cancelled),
|
||||
Err(_error) => {
|
||||
#[cfg(test)]
|
||||
eprintln!("AIS mutation failed: {_error:?}");
|
||||
Err(error) => {
|
||||
if cfg!(test) {
|
||||
eprintln!("AIS mutation failed: {error:?}");
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//! Typed inventory operation failure.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use libremetaverse_types::compat::ExternalError;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct InventoryException {
|
||||
message: String,
|
||||
inner: Option<ExternalError>,
|
||||
}
|
||||
|
||||
impl InventoryException {
|
||||
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
message: String::new(),
|
||||
inner: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_string(message: String) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
message,
|
||||
inner: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_string_exception(
|
||||
message: String,
|
||||
inner_exception: ExternalError,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
message,
|
||||
inner: Some(inner_exception),
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for InventoryException {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.message.is_empty() {
|
||||
formatter.write_str("inventory operation failed")
|
||||
} else {
|
||||
formatter.write_str(&self.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for InventoryException {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.inner
|
||||
.as_ref()
|
||||
.map(|error| error as &(dyn std::error::Error + 'static))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn preserves_message_and_inner_error() {
|
||||
let error = InventoryException::new_with_string_exception(
|
||||
"missing item".to_owned(),
|
||||
ExternalError("not found".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(error.to_string(), "missing item");
|
||||
assert_eq!(
|
||||
std::error::Error::source(&error).unwrap().to_string(),
|
||||
"not found"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
#![allow(clippy::missing_errors_doc)] // Public signatures mirror the C# API.
|
||||
#![allow(clippy::must_use_candidate)]
|
||||
#![allow(clippy::needless_pass_by_value)]
|
||||
#![allow(clippy::too_many_arguments)] // Capability calls preserve mapped request fields.
|
||||
#![allow(clippy::too_many_lines)] // Protocol parsers intentionally keep each wire state machine together.
|
||||
#![allow(clippy::unnecessary_wraps)] // Mapped C# methods require Result-shaped Rust APIs.
|
||||
|
||||
@@ -1657,7 +1658,7 @@ impl InventoryManager {
|
||||
}
|
||||
|
||||
pub(crate) fn native_no_results() -> Task<Vec<InventoryBase>> {
|
||||
Task(std::marker::PhantomData)
|
||||
Task::ready(Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) fn native_create_or_retrieve_inventory_item(
|
||||
@@ -4426,6 +4427,507 @@ fn parse_task_inventory_values(
|
||||
}
|
||||
|
||||
impl InventoryManager {
|
||||
async fn post_capability_osd(
|
||||
&self,
|
||||
uri: Uri,
|
||||
body: OSD,
|
||||
token: CancellationToken,
|
||||
) -> Result<OSD, Error> {
|
||||
token.throw_if_cancellation_requested()?;
|
||||
let payload = OSDParser::serialize_llsd_xml_bytes(body)?;
|
||||
let (response, bytes) = self
|
||||
.client()?
|
||||
.native_http_caps_client()
|
||||
.post_with_uri_string_bytes_cancellation_token_i_progress(
|
||||
uri,
|
||||
"application/llsd+xml".into(),
|
||||
payload,
|
||||
token,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
if !response.is_success_status_code() {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
OSDParser::deserialize_with_bytes(bytes)
|
||||
}
|
||||
|
||||
async fn post_asset_bytes_osd(
|
||||
&self,
|
||||
uri: Uri,
|
||||
data: Vec<u8>,
|
||||
token: CancellationToken,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<OSD, Error> {
|
||||
token.throw_if_cancellation_requested()?;
|
||||
if data.is_empty() || data.len() > crate::asset_models::MAX_ASSET_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let (response, bytes) = self
|
||||
.client()?
|
||||
.native_http_caps_client()
|
||||
.post_with_uri_string_bytes_cancellation_token_i_progress(
|
||||
uri,
|
||||
"application/octet-stream".into(),
|
||||
data,
|
||||
token,
|
||||
progress,
|
||||
)
|
||||
.await?;
|
||||
if !response.is_success_status_code() {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
OSDParser::deserialize_with_bytes(bytes)
|
||||
}
|
||||
|
||||
async fn begin_inventory_capability(
|
||||
&self,
|
||||
capability: &str,
|
||||
query: HashMap<String, OSD>,
|
||||
token: &CancellationToken,
|
||||
) -> Result<OSD, Error> {
|
||||
let simulator = self
|
||||
.client()?
|
||||
.native_network()?
|
||||
.current_sim()
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
let uri = self
|
||||
.wait_for_capability(&simulator, capability, token)
|
||||
.await?
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
self.post_capability_osd(uri, OSD::Map(query), token.clone())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn finish_inventory_upload(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
item_id: Option<UUID>,
|
||||
mut result: OSD,
|
||||
token: CancellationToken,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<((bool, String, UUID, UUID), OSD), Error> {
|
||||
let mut progress = progress;
|
||||
for _ in 0..3 {
|
||||
let OSD::Map(contents) = &result else {
|
||||
return Ok((
|
||||
(false, "invalid_response".into(), UUID::zero(), UUID::zero()),
|
||||
result,
|
||||
));
|
||||
};
|
||||
let state = contents
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
match state.as_str() {
|
||||
"upload" => {
|
||||
let uploader = contents
|
||||
.get("uploader")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uri()?
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
result = self
|
||||
.post_asset_bytes_osd(
|
||||
uploader,
|
||||
data.clone(),
|
||||
token.clone(),
|
||||
progress.take(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
"complete" => {
|
||||
let resolved_item = match item_id {
|
||||
Some(id) => id,
|
||||
None => contents
|
||||
.get("new_inventory_item")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?,
|
||||
};
|
||||
let asset_id = contents
|
||||
.get("new_asset")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?;
|
||||
let owner = self.client()?.native_agent_manager()?.native_agent_id();
|
||||
let _ = self.native_request_fetch_inventory(
|
||||
HashMap::from([(resolved_item, owner)]),
|
||||
Some(token),
|
||||
);
|
||||
return Ok(((true, String::new(), resolved_item, asset_id), result));
|
||||
}
|
||||
_ => {
|
||||
return Ok(((false, state, UUID::zero(), UUID::zero()), result));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((
|
||||
(
|
||||
false,
|
||||
"Upload state machine exceeded maximum iterations".into(),
|
||||
UUID::zero(),
|
||||
UUID::zero(),
|
||||
),
|
||||
result,
|
||||
))
|
||||
}
|
||||
|
||||
fn new_asset_query(
|
||||
&self,
|
||||
name: String,
|
||||
description: String,
|
||||
asset_type: AssetType,
|
||||
inventory_type: InventoryType,
|
||||
folder_id: UUID,
|
||||
permissions: &Permissions,
|
||||
include_expected_cost: bool,
|
||||
) -> Result<HashMap<String, OSD>, Error> {
|
||||
let mut query = HashMap::from([
|
||||
("folder_id".into(), OSD::UUID(folder_id)),
|
||||
(
|
||||
"asset_type".into(),
|
||||
OSD::String(Utils::asset_type_to_string(asset_type)?),
|
||||
),
|
||||
(
|
||||
"inventory_type".into(),
|
||||
OSD::String(Utils::inventory_type_to_string(inventory_type)?),
|
||||
),
|
||||
("name".into(), OSD::String(name)),
|
||||
("description".into(), OSD::String(description)),
|
||||
(
|
||||
"everyone_mask".into(),
|
||||
OSD::Integer(permissions.everyone_mask.0.cast_signed()),
|
||||
),
|
||||
(
|
||||
"group_mask".into(),
|
||||
OSD::Integer(permissions.group_mask.0.cast_signed()),
|
||||
),
|
||||
(
|
||||
"next_owner_mask".into(),
|
||||
OSD::Integer(permissions.next_owner_mask.0.cast_signed()),
|
||||
),
|
||||
]);
|
||||
if include_expected_cost {
|
||||
query.insert(
|
||||
"expected_upload_cost".into(),
|
||||
OSD::Integer(self.client()?.settings_ref().upload_cost()),
|
||||
);
|
||||
}
|
||||
Ok(query)
|
||||
}
|
||||
|
||||
pub(crate) async fn native_request_create_item_from_asset(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
name: String,
|
||||
description: String,
|
||||
asset_type: AssetType,
|
||||
inventory_type: InventoryType,
|
||||
folder_id: UUID,
|
||||
permissions: Permissions,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<(bool, String, UUID, UUID), Error> {
|
||||
let token = cancellation_token.unwrap_or_default();
|
||||
let query = self.new_asset_query(
|
||||
name,
|
||||
description,
|
||||
asset_type,
|
||||
inventory_type,
|
||||
folder_id,
|
||||
&permissions,
|
||||
true,
|
||||
)?;
|
||||
let initial = self
|
||||
.begin_inventory_capability("NewFileAgentInventory", query, &token)
|
||||
.await?;
|
||||
self.finish_inventory_upload(data, None, initial, token, progress)
|
||||
.await
|
||||
.map(|(result, _)| result)
|
||||
}
|
||||
|
||||
pub(crate) async fn native_create_item_from_asset(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
name: String,
|
||||
description: String,
|
||||
asset_type: AssetType,
|
||||
inventory_type: InventoryType,
|
||||
folder_id: UUID,
|
||||
permissions: Permissions,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<crate::InventoryManagerCreateItemFromAssetResult, Error> {
|
||||
let mut output = crate::InventoryManagerCreateItemFromAssetResult::default();
|
||||
match self
|
||||
.native_request_create_item_from_asset(
|
||||
data,
|
||||
name,
|
||||
description,
|
||||
asset_type,
|
||||
inventory_type,
|
||||
folder_id,
|
||||
permissions,
|
||||
cancellation_token,
|
||||
progress,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((success, status, item_id, asset_id)) => {
|
||||
output.set_success(success);
|
||||
output.set_status(Some(status));
|
||||
output.set_item_id(item_id);
|
||||
output.set_asset_id(asset_id);
|
||||
}
|
||||
Err(error) => {
|
||||
let status = error.to_string();
|
||||
output.set_status(Some(status.clone()));
|
||||
output.set_error(Some(ExternalError(status)));
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) fn native_can_upload_large_textures(&self) -> bool {
|
||||
let Ok(client) = self.client() else {
|
||||
return false;
|
||||
};
|
||||
let premium = client
|
||||
.native_agent_manager()
|
||||
.is_ok_and(|manager| manager.native_benefits().premium_access() > 0);
|
||||
premium
|
||||
&& client
|
||||
.native_network()
|
||||
.ok()
|
||||
.and_then(|network| network.current_sim())
|
||||
.and_then(|simulator| simulator.native_caps())
|
||||
.and_then(|caps| {
|
||||
caps.capability_uri("NewFileAgentInventoryVariablePrice".into())
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub(crate) async fn native_create_item_from_asset_variable_price(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
name: String,
|
||||
description: String,
|
||||
asset_type: AssetType,
|
||||
inventory_type: InventoryType,
|
||||
folder_id: UUID,
|
||||
permissions: Permissions,
|
||||
confirm_cost: Option<Box<dyn Fn(i32) -> bool + Send + Sync>>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<crate::InventoryManagerCreateItemFromAssetResult, Error> {
|
||||
let mut output = crate::InventoryManagerCreateItemFromAssetResult::default();
|
||||
if !self.native_can_upload_large_textures() {
|
||||
let status = "membership_or_capability_required".to_owned();
|
||||
output.set_status(Some(status.clone()));
|
||||
output.set_error(Some(ExternalError(status)));
|
||||
return Ok(output);
|
||||
}
|
||||
let token = cancellation_token.unwrap_or_default();
|
||||
let query = self.new_asset_query(
|
||||
name,
|
||||
description,
|
||||
asset_type,
|
||||
inventory_type,
|
||||
folder_id,
|
||||
&permissions,
|
||||
false,
|
||||
)?;
|
||||
let initial = self
|
||||
.begin_inventory_capability("NewFileAgentInventoryVariablePrice", query, &token)
|
||||
.await?;
|
||||
output.set_raw_result(Some(initial.clone()));
|
||||
let OSD::Map(contents) = initial else {
|
||||
output.set_status(Some("invalid_response".into()));
|
||||
return Ok(output);
|
||||
};
|
||||
let state = contents
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
if state != "confirm_upload" {
|
||||
output.set_status(Some(format!("unexpected_state:{state}")));
|
||||
return Ok(output);
|
||||
}
|
||||
let price = contents
|
||||
.get("upload_price")
|
||||
.map(OSD::as_integer)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
if confirm_cost.as_ref().is_some_and(|confirm| !confirm(price)) {
|
||||
let status = "cost_rejected".to_owned();
|
||||
output.set_status(Some(status.clone()));
|
||||
output.set_error(Some(ExternalError(status)));
|
||||
return Ok(output);
|
||||
}
|
||||
let rsvp = contents
|
||||
.get("rsvp")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uri()?
|
||||
.filter(|uri| uri.0 != "about:blank")
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
let uploaded = self
|
||||
.post_asset_bytes_osd(rsvp, data, token.clone(), progress)
|
||||
.await?;
|
||||
output.set_raw_result(Some(uploaded.clone()));
|
||||
let OSD::Map(uploaded) = uploaded else {
|
||||
output.set_status(Some("invalid_upload_response".into()));
|
||||
return Ok(output);
|
||||
};
|
||||
let state = uploaded
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
output.set_status(Some(state.clone()));
|
||||
if state == "complete" {
|
||||
let item_id = uploaded
|
||||
.get("new_inventory_item")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?;
|
||||
let asset_id = uploaded
|
||||
.get("new_asset")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?;
|
||||
output.set_item_id(item_id);
|
||||
output.set_asset_id(asset_id);
|
||||
output.set_success(true);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) async fn native_upload_inventory_asset(
|
||||
&self,
|
||||
capability: &str,
|
||||
data: Vec<u8>,
|
||||
item_id: UUID,
|
||||
task_id: Option<UUID>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<(bool, String, UUID, UUID), Error> {
|
||||
let token = cancellation_token.unwrap_or_default();
|
||||
let mut query = HashMap::from([("item_id".into(), OSD::UUID(item_id))]);
|
||||
if let Some(task_id) = task_id {
|
||||
query.insert("task_id".into(), OSD::UUID(task_id));
|
||||
}
|
||||
let initial = self
|
||||
.begin_inventory_capability(capability, query, &token)
|
||||
.await?;
|
||||
self.finish_inventory_upload(data, Some(item_id), initial, token, progress)
|
||||
.await
|
||||
.map(|(result, _)| result)
|
||||
}
|
||||
|
||||
pub(crate) async fn native_upload_script(
|
||||
&self,
|
||||
capability: &str,
|
||||
data: Vec<u8>,
|
||||
item_id: UUID,
|
||||
task_id: Option<UUID>,
|
||||
mono: bool,
|
||||
running: Option<bool>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<(bool, String, bool, Option<Vec<String>>, UUID, UUID), Error> {
|
||||
let token = cancellation_token.unwrap_or_default();
|
||||
let mut progress = progress;
|
||||
let mut query = HashMap::from([
|
||||
("item_id".into(), OSD::UUID(item_id)),
|
||||
(
|
||||
"target".into(),
|
||||
OSD::String(if mono { "mono" } else { "lsl2" }.into()),
|
||||
),
|
||||
]);
|
||||
if let Some(task_id) = task_id {
|
||||
query.insert("task_id".into(), OSD::UUID(task_id));
|
||||
}
|
||||
if let Some(running) = running {
|
||||
query.insert("is_script_running".into(), OSD::Boolean(running));
|
||||
}
|
||||
let mut result = self
|
||||
.begin_inventory_capability(capability, query, &token)
|
||||
.await?;
|
||||
for _ in 0..3 {
|
||||
let OSD::Map(contents) = &result else {
|
||||
return Ok((
|
||||
false,
|
||||
"invalid_response".into(),
|
||||
false,
|
||||
None,
|
||||
UUID::zero(),
|
||||
UUID::zero(),
|
||||
));
|
||||
};
|
||||
let state = contents
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
if state == "upload" {
|
||||
let uploader = contents
|
||||
.get("uploader")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uri()?
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
result = self
|
||||
.post_asset_bytes_osd(uploader, data.clone(), token.clone(), progress.take())
|
||||
.await?;
|
||||
continue;
|
||||
}
|
||||
if state == "complete" {
|
||||
let asset_id = contents
|
||||
.get("new_asset")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?;
|
||||
let compiled = contents
|
||||
.get("compiled")
|
||||
.map(OSD::as_boolean)
|
||||
.transpose()?
|
||||
.unwrap_or(false);
|
||||
let errors = match contents.get("errors") {
|
||||
Some(OSD::Array(values)) => Some(
|
||||
values
|
||||
.iter()
|
||||
.map(OSD::as_string)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
return Ok((true, state, compiled, errors, item_id, asset_id));
|
||||
}
|
||||
return Ok((false, state, false, None, UUID::zero(), UUID::zero()));
|
||||
}
|
||||
Ok((
|
||||
false,
|
||||
"Upload state machine exceeded maximum iterations".into(),
|
||||
false,
|
||||
None,
|
||||
UUID::zero(),
|
||||
UUID::zero(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn native_upload_thumbnail(
|
||||
&self,
|
||||
inventory_id: UUID,
|
||||
@@ -5030,7 +5532,7 @@ mod tests {
|
||||
.expect("network")
|
||||
.dispatch_caps_event("ScriptRunningReply", &reply, simulator);
|
||||
|
||||
let event = observed.lock().expect("observed").clone().expect("event");
|
||||
let event = (*observed.lock().expect("observed")).expect("event");
|
||||
assert_eq!(event.object_id(), object_id);
|
||||
assert_eq!(event.script_id(), script_id);
|
||||
assert!(event.is_mono());
|
||||
|
||||
@@ -24,10 +24,16 @@ mod asset_cache;
|
||||
mod asset_manager;
|
||||
mod asset_material;
|
||||
mod asset_models;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod asset_prim;
|
||||
mod avatar_manager;
|
||||
mod avatar_rig;
|
||||
mod baking_texture_provider;
|
||||
mod bit_pack;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod builtin_simple_renderer;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod callback_runtime;
|
||||
mod caps;
|
||||
mod caps_http;
|
||||
mod client_core;
|
||||
@@ -36,13 +42,21 @@ mod download_manager;
|
||||
mod environment_manager;
|
||||
mod estate_tools;
|
||||
mod event_queue;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod event_subscription;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod file_helper;
|
||||
mod friends_manager;
|
||||
mod gesture;
|
||||
mod gltf;
|
||||
mod group_manager;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod helpers;
|
||||
mod initial_outfit;
|
||||
mod inventory;
|
||||
mod inventory_ais;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod inventory_exception;
|
||||
mod inventory_manager;
|
||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||
mod foliage_catalog;
|
||||
@@ -50,24 +64,64 @@ mod generated;
|
||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||
mod genepool_catalog;
|
||||
mod j2k;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod linden_mesh;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod logger;
|
||||
mod login;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod lsl_syntax;
|
||||
#[path = "marketplace.rs"]
|
||||
mod marketplace_runtime;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod mesh_asset;
|
||||
mod message_codec;
|
||||
mod message_decoder;
|
||||
mod model_workflow;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod name_value;
|
||||
mod network_manager;
|
||||
mod object_manager;
|
||||
mod object_material;
|
||||
mod object_model;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod observable_dictionary;
|
||||
#[rustfmt::skip]
|
||||
pub mod packet_catalog;
|
||||
mod directory_manager;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod disposal_helper;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod dynamic_messages;
|
||||
mod grid_manager;
|
||||
pub use grid_manager::MapItemData;
|
||||
mod interest_list;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod packet_base;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod packet_decoder;
|
||||
mod packet_wire;
|
||||
mod parcel_manager;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod particle_simulator;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod port_errors;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod position_helper;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod prim_object;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod protocol_manager;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod rendering_binary_reader;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod rendering_loader;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod rendering_metadata;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod repeat;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod service_collection;
|
||||
mod sim_stats;
|
||||
mod skeleton;
|
||||
mod slurl;
|
||||
@@ -80,8 +134,14 @@ mod terrain_manager;
|
||||
mod transfers;
|
||||
mod udp_transport;
|
||||
mod user_report;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod uuid_formatter;
|
||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||
mod visual_catalog;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod avatar_model;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod avatar_physics;
|
||||
|
||||
#[cfg(test)]
|
||||
mod appearance_baker_semantics;
|
||||
@@ -190,6 +250,7 @@ impl imaging::Baker {
|
||||
}
|
||||
}
|
||||
|
||||
pub use callback_runtime::{AsyncInvocation, DictionaryCallbackTarget};
|
||||
pub use caps_http::CapsHttpLimits;
|
||||
pub use client_core::{
|
||||
ClientCoreError, ClientLifecycleState, ClientService, GridClientBuilder, ShutdownPhase,
|
||||
@@ -199,6 +260,7 @@ pub use libremetaverse_imaging as imaging_abstractions;
|
||||
pub use libremetaverse_structured_data as structured_data;
|
||||
pub use libremetaverse_types as types;
|
||||
pub use libremetaverse_types::Error;
|
||||
pub use logger::LoggerCallbackTarget;
|
||||
pub use network_manager::SimulatorCollection;
|
||||
pub use udp_transport::{
|
||||
AgentThrottleSender, UdpPacketHandler, UdpThrottleCategory, UdpTransportConfig,
|
||||
@@ -213,6 +275,23 @@ pub(crate) fn is_offline_fixture_uri(uri: &libremetaverse_types::compat::Uri) ->
|
||||
.is_some_and(|host| host == "invalid" || host.ends_with(".invalid"))
|
||||
}
|
||||
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod collada;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod coordinate_frame;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod physics_volume_morphs;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod simulator_data_pool;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod sound_catalog;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod statistics_runtime;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod texture_pipeline;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod threading_runtime;
|
||||
|
||||
/// Rust object-safe view of the C# `InventoryBase` inheritance hierarchy.
|
||||
pub trait InventoryObjectClass: std::any::Any {
|
||||
/// Returns the concrete value for exact C# runtime-type checks.
|
||||
@@ -320,19 +399,177 @@ impl InventoryItemClass for InventoryTexture {}
|
||||
impl InventoryItemClass for InventoryWearable {}
|
||||
|
||||
/// Project-owned boundary types for external `MessagePack` signatures.
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the external API boundary.
|
||||
pub mod message_pack {
|
||||
pub trait IMessagePackFormatter<T = ()> {}
|
||||
pub struct MessagePackReader;
|
||||
use libremetaverse_types::Error;
|
||||
|
||||
pub trait IMessagePackFormatter<T = ()> {
|
||||
fn serialize(
|
||||
&self,
|
||||
writer: &mut MessagePackWriter,
|
||||
value: T,
|
||||
options: MessagePackSerializerOptions,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
reader: &mut MessagePackReader,
|
||||
options: MessagePackSerializerOptions,
|
||||
) -> Result<T, Error>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MessagePackReader {
|
||||
bytes: Vec<u8>,
|
||||
position: usize,
|
||||
}
|
||||
|
||||
impl MessagePackReader {
|
||||
#[must_use]
|
||||
pub fn new(bytes: Vec<u8>) -> Self {
|
||||
Self { bytes, position: 0 }
|
||||
}
|
||||
|
||||
pub fn read_bytes(&mut self) -> Result<Option<Vec<u8>>, Error> {
|
||||
let marker = *self
|
||||
.bytes
|
||||
.get(self.position)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
self.position += 1;
|
||||
if marker == 0xc0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let length = match marker {
|
||||
0xc4 => {
|
||||
let length = *self
|
||||
.bytes
|
||||
.get(self.position)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
self.position += 1;
|
||||
usize::from(length)
|
||||
}
|
||||
0xc5 => {
|
||||
let length = self
|
||||
.bytes
|
||||
.get(self.position..self.position + 2)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
self.position += 2;
|
||||
usize::from(u16::from_be_bytes([length[0], length[1]]))
|
||||
}
|
||||
0xc6 => {
|
||||
let length = self
|
||||
.bytes
|
||||
.get(self.position..self.position + 4)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
self.position += 4;
|
||||
usize::try_from(u32::from_be_bytes([
|
||||
length[0], length[1], length[2], length[3],
|
||||
]))
|
||||
.map_err(|_| Error::IndexOutOfRange)?
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::Parse {
|
||||
position: self.position - 1,
|
||||
context: "MessagePack binary marker",
|
||||
});
|
||||
}
|
||||
};
|
||||
let end = self
|
||||
.position
|
||||
.checked_add(length)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
let value = self
|
||||
.bytes
|
||||
.get(self.position..end)
|
||||
.ok_or(Error::IndexOutOfRange)?
|
||||
.to_vec();
|
||||
self.position = end;
|
||||
Ok(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct MessagePackSerializerOptions;
|
||||
pub struct MessagePackWriter;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MessagePackWriter {
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl MessagePackWriter {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn write_bytes(&mut self, value: &[u8]) -> Result<(), Error> {
|
||||
match value.len() {
|
||||
length if u8::try_from(length).is_ok() => {
|
||||
self.bytes.push(0xc4);
|
||||
self.bytes.push(length as u8);
|
||||
}
|
||||
length if u16::try_from(length).is_ok() => {
|
||||
self.bytes.push(0xc5);
|
||||
self.bytes.extend_from_slice(&(length as u16).to_be_bytes());
|
||||
}
|
||||
length => {
|
||||
self.bytes.push(0xc6);
|
||||
let length = u32::try_from(length).map_err(|_| Error::Argument)?;
|
||||
self.bytes.extend_from_slice(&length.to_be_bytes());
|
||||
}
|
||||
}
|
||||
self.bytes.extend_from_slice(value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.bytes
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
self.bytes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Project-owned boundary types for external logging signatures.
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the external API boundary.
|
||||
pub mod logging {
|
||||
pub trait ILoggerFactory {}
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
use libremetaverse_types::compat::{Close, ExternalError, Object};
|
||||
|
||||
pub trait ILogger: Send + Sync {
|
||||
fn is_enabled(&self, level: LogLevel) -> bool;
|
||||
fn log(
|
||||
&self,
|
||||
level: LogLevel,
|
||||
message: &Object,
|
||||
exception: Option<&ExternalError>,
|
||||
client_name: Option<&str>,
|
||||
);
|
||||
fn begin_scope(&self, state: Object) -> Box<dyn Close>;
|
||||
}
|
||||
|
||||
pub trait ILoggerFactory: Send + Sync {
|
||||
fn create_logger(&self, name: &str) -> Arc<dyn ILogger>;
|
||||
fn shutdown(&self) -> Result<(), ExternalError>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct LogLevel(pub i32);
|
||||
|
||||
impl LogLevel {
|
||||
pub const TRACE: Self = Self(0);
|
||||
pub const DEBUG: Self = Self(1);
|
||||
pub const INFORMATION: Self = Self(2);
|
||||
pub const WARNING: Self = Self(3);
|
||||
pub const ERROR: Self = Self(4);
|
||||
pub const CRITICAL: Self = Self(5);
|
||||
pub const NONE: Self = Self(6);
|
||||
}
|
||||
}
|
||||
|
||||
/// Project-owned boundary types for external networking signatures.
|
||||
@@ -345,7 +582,7 @@ pub mod net {
|
||||
|
||||
/// Project-owned boundary types for external service-collection signatures.
|
||||
pub mod services {
|
||||
pub trait IServiceCollection {}
|
||||
pub use crate::service_collection::{IServiceCollection, ServiceCollection};
|
||||
}
|
||||
|
||||
/// Common behavior for program shims while the network stack is unimplemented.
|
||||
|
||||
@@ -0,0 +1,723 @@
|
||||
//! Validated reader for Linden Lab's legacy `.llm` avatar mesh format.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use libremetaverse_types::compat::{Object, SortedList};
|
||||
use libremetaverse_types::{Error, Vector2, Vector3};
|
||||
|
||||
use crate::rendering::{
|
||||
LindenMeshFace, LindenMeshMorph, LindenMeshMorphVertex, LindenMeshSkinWeightElement,
|
||||
LindenMeshVertex, LindenMeshVertexRemap, LindenSkeleton,
|
||||
};
|
||||
|
||||
const MESH_HEADER: &str = "Linden Binary Mesh 1.0";
|
||||
const MORPH_FOOTER: &str = "End Morphs";
|
||||
const MAX_MESH_BYTES: usize = 64 * 1024 * 1024;
|
||||
const MAX_MORPHS: usize = 16_384;
|
||||
const MORPH_VERTEX_BYTES: usize = 48;
|
||||
|
||||
/// Complete native state for a full Linden avatar mesh.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LindenMesh {
|
||||
pub min_pixel_width: f32,
|
||||
pub morphs: Vec<LindenMeshMorph>,
|
||||
pub skin_weights: Vec<LindenMeshSkinWeightElement>,
|
||||
name: String,
|
||||
header: String,
|
||||
has_weights: bool,
|
||||
has_detail_tex_coords: bool,
|
||||
position: Vector3,
|
||||
rotation_angles: Vector3,
|
||||
scale: Vector3,
|
||||
num_vertices: u16,
|
||||
vertices: Vec<LindenMeshVertex>,
|
||||
num_faces: u16,
|
||||
faces: Vec<LindenMeshFace>,
|
||||
num_skin_joints: u16,
|
||||
skin_joints: Vec<String>,
|
||||
num_remaps: i32,
|
||||
vertex_remaps: Vec<LindenMeshVertexRemap>,
|
||||
lod_meshes: SortedList<i32, Object>,
|
||||
skeleton: Option<LindenSkeleton>,
|
||||
}
|
||||
|
||||
/// Complete native state for a reference-only Linden LOD mesh.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ReferenceMesh {
|
||||
pub min_pixel_width: f32,
|
||||
pub header: String,
|
||||
pub has_weights: bool,
|
||||
pub has_detail_tex_coords: bool,
|
||||
pub position: Vector3,
|
||||
pub rotation_angles: Vector3,
|
||||
pub rotation_order: u8,
|
||||
pub scale: Vector3,
|
||||
pub num_faces: u16,
|
||||
pub faces: Vec<LindenMeshFace>,
|
||||
}
|
||||
|
||||
impl LindenMesh {
|
||||
pub(crate) fn native_new(
|
||||
name: String,
|
||||
skeleton: Option<LindenSkeleton>,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
min_pixel_width: 0.0,
|
||||
morphs: Vec::new(),
|
||||
skin_weights: Vec::new(),
|
||||
name,
|
||||
header: String::new(),
|
||||
has_weights: false,
|
||||
has_detail_tex_coords: false,
|
||||
position: Vector3::zero(),
|
||||
rotation_angles: Vector3::zero(),
|
||||
scale: Vector3::zero(),
|
||||
num_vertices: 0,
|
||||
vertices: Vec::new(),
|
||||
num_faces: 0,
|
||||
faces: Vec::new(),
|
||||
num_skin_joints: 0,
|
||||
skin_joints: Vec::new(),
|
||||
num_remaps: 0,
|
||||
vertex_remaps: Vec::new(),
|
||||
lod_meshes: SortedList(BTreeMap::new()),
|
||||
skeleton: Some(skeleton.unwrap_or(LindenSkeleton::load_with_method()?)),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_load_mesh(&mut self, filename: &str) -> Result<(), Error> {
|
||||
let bytes = read_mesh_file(filename)?;
|
||||
let parsed = ParsedFullMesh::parse(&bytes)?;
|
||||
|
||||
self.header = parsed.header;
|
||||
self.has_weights = parsed.has_weights;
|
||||
self.has_detail_tex_coords = parsed.has_detail_tex_coords;
|
||||
self.position = parsed.position;
|
||||
self.rotation_angles = parsed.rotation_angles;
|
||||
self.scale = parsed.scale;
|
||||
self.num_vertices = parsed.num_vertices;
|
||||
self.vertices = parsed.vertices;
|
||||
self.num_faces = parsed.num_faces;
|
||||
self.faces = parsed.faces;
|
||||
self.num_skin_joints = parsed.num_skin_joints;
|
||||
self.skin_joints = parsed.skin_joints;
|
||||
self.morphs = parsed.morphs;
|
||||
self.num_remaps = parsed.num_remaps;
|
||||
self.vertex_remaps = parsed.vertex_remaps;
|
||||
self.expand_skin_weights()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_load_lod_mesh(
|
||||
&mut self,
|
||||
level: i32,
|
||||
filename: &str,
|
||||
) -> Result<Object, Error> {
|
||||
let object = if is_eye_lod(filename) {
|
||||
let mut mesh = Self::native_new(String::new(), None)?;
|
||||
mesh.native_load_mesh(filename)?;
|
||||
Object::opaque(mesh)
|
||||
} else {
|
||||
let mut mesh = ReferenceMesh::native_new()?;
|
||||
mesh.native_load_mesh(filename)?;
|
||||
Object::opaque(mesh)
|
||||
};
|
||||
self.lod_meshes.0.insert(level, object.clone());
|
||||
Ok(object)
|
||||
}
|
||||
|
||||
pub(crate) fn native_load_reference_mesh(
|
||||
&mut self,
|
||||
lod_level: i32,
|
||||
filename: &str,
|
||||
) -> Result<ReferenceMesh, Error> {
|
||||
if is_eye_lod(filename) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut mesh = ReferenceMesh::native_new()?;
|
||||
mesh.native_load_mesh(filename)?;
|
||||
self.lod_meshes
|
||||
.0
|
||||
.insert(lod_level, Object::opaque(mesh.clone()));
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
fn expand_skin_weights(&mut self) -> Result<(), Error> {
|
||||
self.skin_weights.clear();
|
||||
let Some(skeleton) = &self.skeleton else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut joints =
|
||||
skeleton.build_expanded_joint_list(Box::new(self.skin_joints.clone().into_iter()))?;
|
||||
if joints.is_empty() {
|
||||
match self.name.as_str() {
|
||||
"eyeBallLeftMesh" => joints.extend(["mEyeLeft".to_owned(), "mSkull".to_owned()]),
|
||||
"eyeBallRightMesh" => {
|
||||
joints.extend(["mEyeRight".to_owned(), "mSkull".to_owned()]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if joints.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
self.skin_weights.reserve(self.vertices.len());
|
||||
for vertex in &self.vertices {
|
||||
let raw_index = vertex.weight.floor() as i32;
|
||||
let bone_index = raw_index - 1;
|
||||
let blend = (vertex.weight - raw_index as f32).clamp(0.0, 1.0);
|
||||
let valid_pair = usize::try_from(bone_index)
|
||||
.ok()
|
||||
.filter(|index| index.saturating_add(1) < joints.len());
|
||||
let weight = if let Some(index) = valid_pair {
|
||||
LindenMeshSkinWeightElement {
|
||||
bone1: joints[index].clone(),
|
||||
bone2: joints[index + 1].clone(),
|
||||
weight1: 1.0 - blend,
|
||||
weight2: blend,
|
||||
}
|
||||
} else {
|
||||
let nearest = bone_index.clamp(0, joints.len().saturating_sub(1) as i32) as usize;
|
||||
LindenMeshSkinWeightElement {
|
||||
bone1: joints[nearest].clone(),
|
||||
bone2: "mPelvis".to_owned(),
|
||||
weight1: 1.0,
|
||||
weight2: 0.0,
|
||||
}
|
||||
};
|
||||
self.skin_weights.push(weight);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_faces(&self) -> Vec<LindenMeshFace> {
|
||||
self.faces.clone()
|
||||
}
|
||||
pub(crate) fn native_set_faces(&mut self, value: Vec<LindenMeshFace>) {
|
||||
self.num_faces = value.len().try_into().unwrap_or(u16::MAX);
|
||||
self.faces = value;
|
||||
}
|
||||
pub(crate) fn native_has_detail_tex_coords(&self) -> bool {
|
||||
self.has_detail_tex_coords
|
||||
}
|
||||
pub(crate) fn native_set_has_detail_tex_coords(&mut self, value: bool) {
|
||||
self.has_detail_tex_coords = value;
|
||||
}
|
||||
pub(crate) fn native_has_weights(&self) -> bool {
|
||||
self.has_weights
|
||||
}
|
||||
pub(crate) fn native_set_has_weights(&mut self, value: bool) {
|
||||
self.has_weights = value;
|
||||
}
|
||||
pub(crate) fn native_header(&self) -> String {
|
||||
self.header.clone()
|
||||
}
|
||||
pub(crate) fn native_set_header(&mut self, value: String) {
|
||||
self.header = value;
|
||||
}
|
||||
pub(crate) fn native_lod_meshes(&self) -> SortedList<i32, Object> {
|
||||
self.lod_meshes.clone()
|
||||
}
|
||||
pub(crate) fn native_set_lod_meshes(&mut self, value: SortedList<i32, Object>) {
|
||||
self.lod_meshes = value;
|
||||
}
|
||||
pub(crate) fn native_name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
pub(crate) fn native_set_name(&mut self, value: String) {
|
||||
self.name = value;
|
||||
}
|
||||
pub(crate) fn native_num_faces(&self) -> u16 {
|
||||
self.num_faces
|
||||
}
|
||||
pub(crate) fn native_set_num_faces(&mut self, value: u16) {
|
||||
self.num_faces = value;
|
||||
}
|
||||
pub(crate) fn native_num_remaps(&self) -> i32 {
|
||||
self.num_remaps
|
||||
}
|
||||
pub(crate) fn native_set_num_remaps(&mut self, value: i32) {
|
||||
self.num_remaps = value;
|
||||
}
|
||||
pub(crate) fn native_num_skin_joints(&self) -> u16 {
|
||||
self.num_skin_joints
|
||||
}
|
||||
pub(crate) fn native_set_num_skin_joints(&mut self, value: u16) {
|
||||
self.num_skin_joints = value;
|
||||
}
|
||||
pub(crate) fn native_num_vertices(&self) -> u16 {
|
||||
self.num_vertices
|
||||
}
|
||||
pub(crate) fn native_set_num_vertices(&mut self, value: u16) {
|
||||
self.num_vertices = value;
|
||||
}
|
||||
pub(crate) fn native_position(&self) -> Vector3 {
|
||||
self.position
|
||||
}
|
||||
pub(crate) fn native_set_position(&mut self, value: Vector3) {
|
||||
self.position = value;
|
||||
}
|
||||
pub(crate) fn native_rotation_angles(&self) -> Vector3 {
|
||||
self.rotation_angles
|
||||
}
|
||||
pub(crate) fn native_set_rotation_angles(&mut self, value: Vector3) {
|
||||
self.rotation_angles = value;
|
||||
}
|
||||
pub(crate) fn native_scale(&self) -> Vector3 {
|
||||
self.scale
|
||||
}
|
||||
pub(crate) fn native_set_scale(&mut self, value: Vector3) {
|
||||
self.scale = value;
|
||||
}
|
||||
pub(crate) fn native_skeleton(&self) -> Option<LindenSkeleton> {
|
||||
self.skeleton.clone()
|
||||
}
|
||||
pub(crate) fn native_set_skeleton(&mut self, value: Option<LindenSkeleton>) {
|
||||
self.skeleton = value;
|
||||
}
|
||||
pub(crate) fn native_skin_joints(&self) -> Vec<String> {
|
||||
self.skin_joints.clone()
|
||||
}
|
||||
pub(crate) fn native_set_skin_joints(&mut self, value: Vec<String>) {
|
||||
self.num_skin_joints = value.len().try_into().unwrap_or(u16::MAX);
|
||||
self.skin_joints = value;
|
||||
}
|
||||
pub(crate) fn native_vertex_remaps(&self) -> Vec<LindenMeshVertexRemap> {
|
||||
self.vertex_remaps.clone()
|
||||
}
|
||||
pub(crate) fn native_set_vertex_remaps(&mut self, value: Vec<LindenMeshVertexRemap>) {
|
||||
self.num_remaps = value.len().try_into().unwrap_or(i32::MAX);
|
||||
self.vertex_remaps = value;
|
||||
}
|
||||
pub(crate) fn native_vertices(&self) -> Vec<LindenMeshVertex> {
|
||||
self.vertices.clone()
|
||||
}
|
||||
pub(crate) fn native_set_vertices(&mut self, value: Vec<LindenMeshVertex>) {
|
||||
self.num_vertices = value.len().try_into().unwrap_or(u16::MAX);
|
||||
self.vertices = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl ReferenceMesh {
|
||||
pub(crate) fn native_new() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
min_pixel_width: 0.0,
|
||||
header: String::new(),
|
||||
has_weights: false,
|
||||
has_detail_tex_coords: false,
|
||||
position: Vector3::zero(),
|
||||
rotation_angles: Vector3::zero(),
|
||||
rotation_order: 0,
|
||||
scale: Vector3::zero(),
|
||||
num_faces: 0,
|
||||
faces: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_load_mesh(&mut self, filename: &str) -> Result<(), Error> {
|
||||
let bytes = read_mesh_file(filename)?;
|
||||
let mut reader = Reader::new(&bytes);
|
||||
self.header = reader.fixed_string(24, "reference mesh header")?;
|
||||
validate_header(&self.header)?;
|
||||
self.has_weights = reader.byte("reference weights flag")? != 0;
|
||||
self.has_detail_tex_coords = reader.byte("reference detail flag")? != 0;
|
||||
self.position = reader.vector3("reference position")?;
|
||||
self.rotation_angles = reader.vector3("reference rotation")?;
|
||||
self.rotation_order = reader.byte("reference rotation order")?;
|
||||
self.scale = reader.vector3("reference scale")?;
|
||||
self.num_faces = reader.u16("reference face count")?;
|
||||
self.faces = read_faces(&mut reader, self.num_faces)?;
|
||||
reader.finish("reference mesh trailing bytes")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ParsedFullMesh {
|
||||
header: String,
|
||||
has_weights: bool,
|
||||
has_detail_tex_coords: bool,
|
||||
position: Vector3,
|
||||
rotation_angles: Vector3,
|
||||
scale: Vector3,
|
||||
num_vertices: u16,
|
||||
vertices: Vec<LindenMeshVertex>,
|
||||
num_faces: u16,
|
||||
faces: Vec<LindenMeshFace>,
|
||||
num_skin_joints: u16,
|
||||
skin_joints: Vec<String>,
|
||||
morphs: Vec<LindenMeshMorph>,
|
||||
num_remaps: i32,
|
||||
vertex_remaps: Vec<LindenMeshVertexRemap>,
|
||||
}
|
||||
|
||||
impl ParsedFullMesh {
|
||||
fn parse(bytes: &[u8]) -> Result<Self, Error> {
|
||||
let mut reader = Reader::new(bytes);
|
||||
let header = reader.fixed_string(24, "mesh header")?;
|
||||
validate_header(&header)?;
|
||||
let has_weights = reader.byte("weights flag")? != 0;
|
||||
let has_detail_tex_coords = reader.byte("detail flag")? != 0;
|
||||
let position = reader.vector3("position")?;
|
||||
let rotation_angles = reader.vector3("rotation")?;
|
||||
let _rotation_order = reader.byte("rotation order")?;
|
||||
let scale = reader.vector3("scale")?;
|
||||
let num_vertices = reader.u16("vertex count")?;
|
||||
let count = usize::from(num_vertices);
|
||||
let mut vertices = vec![empty_vertex(); count];
|
||||
for vertex in &mut vertices {
|
||||
vertex.coord = reader.vector3("vertex coordinates")?;
|
||||
}
|
||||
for vertex in &mut vertices {
|
||||
vertex.normal = reader.vector3("vertex normals")?;
|
||||
}
|
||||
for vertex in &mut vertices {
|
||||
vertex.bi_normal = reader.vector3("vertex binormals")?;
|
||||
}
|
||||
for vertex in &mut vertices {
|
||||
vertex.tex_coord = reader.vector2("vertex texture coordinates")?;
|
||||
}
|
||||
if has_detail_tex_coords {
|
||||
for vertex in &mut vertices {
|
||||
vertex.detail_tex_coord = reader.vector2("vertex detail texture coordinates")?;
|
||||
}
|
||||
}
|
||||
if has_weights {
|
||||
for vertex in &mut vertices {
|
||||
vertex.weight = reader.f32("vertex weight")?;
|
||||
if !vertex.weight.is_finite() {
|
||||
return Err(parse_error(reader.position(), "non-finite vertex weight"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let num_faces = reader.u16("face count")?;
|
||||
let faces = read_faces(&mut reader, num_faces)?;
|
||||
let (num_skin_joints, skin_joints) = if has_weights {
|
||||
let count = reader.u16("skin joint count")?;
|
||||
let mut joints = Vec::with_capacity(usize::from(count));
|
||||
for _ in 0..count {
|
||||
joints.push(reader.fixed_string(64, "skin joint name")?);
|
||||
}
|
||||
(count, joints)
|
||||
} else {
|
||||
(0, Vec::new())
|
||||
};
|
||||
let mut morphs = Vec::new();
|
||||
loop {
|
||||
let name = reader.fixed_string(64, "morph name")?;
|
||||
if name == MORPH_FOOTER {
|
||||
break;
|
||||
}
|
||||
if morphs.len() >= MAX_MORPHS {
|
||||
return Err(parse_error(reader.position(), "morph count exceeds limit"));
|
||||
}
|
||||
let signed_count = reader.i32("morph vertex count")?;
|
||||
let morph_count = usize::try_from(signed_count)
|
||||
.map_err(|_| parse_error(reader.position(), "negative morph vertex count"))?;
|
||||
reader.require(
|
||||
morph_count
|
||||
.checked_mul(MORPH_VERTEX_BYTES)
|
||||
.ok_or_else(|| parse_error(reader.position(), "morph size overflow"))?,
|
||||
"morph vertices",
|
||||
)?;
|
||||
let mut morph_vertices = Vec::with_capacity(morph_count);
|
||||
for _ in 0..morph_count {
|
||||
morph_vertices.push(LindenMeshMorphVertex {
|
||||
vertex_index: reader.u32("morph vertex index")?,
|
||||
coord: reader.vector3("morph coordinates")?,
|
||||
normal: reader.vector3("morph normal")?,
|
||||
bi_normal: reader.vector3("morph binormal")?,
|
||||
tex_coord: reader.vector2("morph texture coordinates")?,
|
||||
});
|
||||
}
|
||||
morphs.push(LindenMeshMorph {
|
||||
name,
|
||||
num_vertices: signed_count,
|
||||
vertices: morph_vertices,
|
||||
});
|
||||
}
|
||||
let (num_remaps, vertex_remaps) = if reader.remaining() == 0 {
|
||||
(0, Vec::new())
|
||||
} else {
|
||||
let signed_count = reader.i32("vertex remap count")?;
|
||||
let count = usize::try_from(signed_count)
|
||||
.map_err(|_| parse_error(reader.position(), "negative vertex remap count"))?;
|
||||
reader.require(
|
||||
count
|
||||
.checked_mul(8)
|
||||
.ok_or_else(|| parse_error(reader.position(), "vertex remap size overflow"))?,
|
||||
"vertex remaps",
|
||||
)?;
|
||||
let mut remaps = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
remaps.push(LindenMeshVertexRemap {
|
||||
remap_source: reader.i32("remap source")?,
|
||||
remap_destination: reader.i32("remap destination")?,
|
||||
});
|
||||
}
|
||||
(signed_count, remaps)
|
||||
};
|
||||
reader.finish("mesh trailing bytes")?;
|
||||
Ok(Self {
|
||||
header,
|
||||
has_weights,
|
||||
has_detail_tex_coords,
|
||||
position,
|
||||
rotation_angles,
|
||||
scale,
|
||||
num_vertices,
|
||||
vertices,
|
||||
num_faces,
|
||||
faces,
|
||||
num_skin_joints,
|
||||
skin_joints,
|
||||
morphs,
|
||||
num_remaps,
|
||||
vertex_remaps,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_vertex() -> LindenMeshVertex {
|
||||
LindenMeshVertex {
|
||||
bi_normal: Vector3::zero(),
|
||||
coord: Vector3::zero(),
|
||||
detail_tex_coord: Vector2::zero(),
|
||||
normal: Vector3::zero(),
|
||||
tex_coord: Vector2::zero(),
|
||||
weight: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_faces(reader: &mut Reader<'_>, count: u16) -> Result<Vec<LindenMeshFace>, Error> {
|
||||
reader.require(usize::from(count) * 6, "face indices")?;
|
||||
let mut faces = Vec::with_capacity(usize::from(count));
|
||||
for _ in 0..count {
|
||||
faces.push(LindenMeshFace {
|
||||
indices: vec![
|
||||
reader.i16("face index")?,
|
||||
reader.i16("face index")?,
|
||||
reader.i16("face index")?,
|
||||
],
|
||||
});
|
||||
}
|
||||
Ok(faces)
|
||||
}
|
||||
|
||||
fn read_mesh_file(filename: &str) -> Result<Vec<u8>, Error> {
|
||||
let metadata = std::fs::metadata(filename).map_err(|_| Error::Argument)?;
|
||||
let length = usize::try_from(metadata.len()).map_err(|_| Error::Argument)?;
|
||||
if length > MAX_MESH_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let bytes = std::fs::read(filename).map_err(|_| Error::Argument)?;
|
||||
if bytes.len() > MAX_MESH_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn is_eye_lod(filename: &str) -> bool {
|
||||
Path::new(filename)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
== Some("avatar_eye_1.llm")
|
||||
}
|
||||
|
||||
fn validate_header(header: &str) -> Result<(), Error> {
|
||||
if header == MESH_HEADER {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(parse_error(0, "unrecognized Linden mesh header"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn trim_at_nul(value: &str) -> String {
|
||||
value
|
||||
.split_once('\0')
|
||||
.map_or(value, |(prefix, _)| prefix)
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
struct Reader<'a> {
|
||||
bytes: &'a [u8],
|
||||
position: usize,
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
fn new(bytes: &'a [u8]) -> Self {
|
||||
Self { bytes, position: 0 }
|
||||
}
|
||||
fn position(&self) -> usize {
|
||||
self.position
|
||||
}
|
||||
fn remaining(&self) -> usize {
|
||||
self.bytes.len().saturating_sub(self.position)
|
||||
}
|
||||
fn require(&self, count: usize, context: &'static str) -> Result<(), Error> {
|
||||
if count <= self.remaining() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(parse_error(self.position, context))
|
||||
}
|
||||
}
|
||||
fn take(&mut self, count: usize, context: &'static str) -> Result<&'a [u8], Error> {
|
||||
self.require(count, context)?;
|
||||
let start = self.position;
|
||||
self.position += count;
|
||||
Ok(&self.bytes[start..self.position])
|
||||
}
|
||||
fn byte(&mut self, context: &'static str) -> Result<u8, Error> {
|
||||
Ok(self.take(1, context)?[0])
|
||||
}
|
||||
fn i16(&mut self, context: &'static str) -> Result<i16, Error> {
|
||||
let bytes: [u8; 2] = self
|
||||
.take(2, context)?
|
||||
.try_into()
|
||||
.map_err(|_| parse_error(self.position, context))?;
|
||||
Ok(i16::from_le_bytes(bytes))
|
||||
}
|
||||
fn u16(&mut self, context: &'static str) -> Result<u16, Error> {
|
||||
let bytes: [u8; 2] = self
|
||||
.take(2, context)?
|
||||
.try_into()
|
||||
.map_err(|_| parse_error(self.position, context))?;
|
||||
Ok(u16::from_le_bytes(bytes))
|
||||
}
|
||||
fn i32(&mut self, context: &'static str) -> Result<i32, Error> {
|
||||
let bytes: [u8; 4] = self
|
||||
.take(4, context)?
|
||||
.try_into()
|
||||
.map_err(|_| parse_error(self.position, context))?;
|
||||
Ok(i32::from_le_bytes(bytes))
|
||||
}
|
||||
fn u32(&mut self, context: &'static str) -> Result<u32, Error> {
|
||||
let bytes: [u8; 4] = self
|
||||
.take(4, context)?
|
||||
.try_into()
|
||||
.map_err(|_| parse_error(self.position, context))?;
|
||||
Ok(u32::from_le_bytes(bytes))
|
||||
}
|
||||
fn f32(&mut self, context: &'static str) -> Result<f32, Error> {
|
||||
let bytes: [u8; 4] = self
|
||||
.take(4, context)?
|
||||
.try_into()
|
||||
.map_err(|_| parse_error(self.position, context))?;
|
||||
let value = f32::from_le_bytes(bytes);
|
||||
if value.is_finite() {
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(parse_error(self.position, context))
|
||||
}
|
||||
}
|
||||
fn vector2(&mut self, context: &'static str) -> Result<Vector2, Error> {
|
||||
Ok(Vector2 {
|
||||
x: self.f32(context)?,
|
||||
y: self.f32(context)?,
|
||||
})
|
||||
}
|
||||
fn vector3(&mut self, context: &'static str) -> Result<Vector3, Error> {
|
||||
Ok(Vector3 {
|
||||
x: self.f32(context)?,
|
||||
y: self.f32(context)?,
|
||||
z: self.f32(context)?,
|
||||
})
|
||||
}
|
||||
fn fixed_string(&mut self, count: usize, context: &'static str) -> Result<String, Error> {
|
||||
Ok(trim_at_nul(&String::from_utf8_lossy(
|
||||
self.take(count, context)?,
|
||||
)))
|
||||
}
|
||||
fn finish(&self, context: &'static str) -> Result<(), Error> {
|
||||
if self.remaining() == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(parse_error(self.position, context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn parse_error(position: usize, context: &'static str) -> Error {
|
||||
Error::Parse { position, context }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn base_header(weights: bool, detail: bool) -> Vec<u8> {
|
||||
let mut bytes = [0_u8; 24].to_vec();
|
||||
bytes[..MESH_HEADER.len()].copy_from_slice(MESH_HEADER.as_bytes());
|
||||
bytes.push(u8::from(weights));
|
||||
bytes.push(u8::from(detail));
|
||||
for value in [1.0_f32, 2.0, 3.0, 0.1, 0.2, 0.3] {
|
||||
bytes.extend(value.to_le_bytes());
|
||||
}
|
||||
bytes.push(7);
|
||||
for value in [4.0_f32, 5.0, 6.0] {
|
||||
bytes.extend(value.to_le_bytes());
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_full_mesh_arrays_morphs_and_remaps() {
|
||||
let mut bytes = base_header(false, true);
|
||||
bytes.extend(1_u16.to_le_bytes());
|
||||
for value in [
|
||||
1.0_f32, 2.0, 3.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.25, 0.75, 0.5, 0.5,
|
||||
] {
|
||||
bytes.extend(value.to_le_bytes());
|
||||
}
|
||||
bytes.extend(1_u16.to_le_bytes());
|
||||
for index in [0_i16, 0, 0] {
|
||||
bytes.extend(index.to_le_bytes());
|
||||
}
|
||||
let mut morph_name = [0_u8; 64];
|
||||
morph_name[..5].copy_from_slice(b"smile");
|
||||
bytes.extend(morph_name);
|
||||
bytes.extend(1_i32.to_le_bytes());
|
||||
bytes.extend(0_u32.to_le_bytes());
|
||||
for value in [0.1_f32; 11] {
|
||||
bytes.extend(value.to_le_bytes());
|
||||
}
|
||||
let mut footer = [0_u8; 64];
|
||||
footer[..MORPH_FOOTER.len()].copy_from_slice(MORPH_FOOTER.as_bytes());
|
||||
bytes.extend(footer);
|
||||
bytes.extend(1_i32.to_le_bytes());
|
||||
bytes.extend(3_i32.to_le_bytes());
|
||||
bytes.extend(4_i32.to_le_bytes());
|
||||
let mesh = ParsedFullMesh::parse(&bytes).unwrap();
|
||||
assert_eq!(mesh.vertices.len(), 1);
|
||||
assert_eq!(mesh.faces[0].indices, vec![0, 0, 0]);
|
||||
assert_eq!(mesh.morphs[0].name, "smile");
|
||||
assert_eq!(mesh.vertex_remaps[0].remap_destination, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_bad_header_truncation_negative_counts_and_trailing_data() {
|
||||
assert!(ParsedFullMesh::parse(b"bad").is_err());
|
||||
let mut negative = base_header(false, false);
|
||||
negative.extend(0_u16.to_le_bytes());
|
||||
negative.extend(0_u16.to_le_bytes());
|
||||
let mut name = [0_u8; 64];
|
||||
name[0] = b'x';
|
||||
negative.extend(name);
|
||||
negative.extend((-1_i32).to_le_bytes());
|
||||
assert!(ParsedFullMesh::parse(&negative).is_err());
|
||||
let mut valid = base_header(false, false);
|
||||
valid.extend(0_u16.to_le_bytes());
|
||||
valid.extend(0_u16.to_le_bytes());
|
||||
let mut footer = [0_u8; 64];
|
||||
footer[..MORPH_FOOTER.len()].copy_from_slice(MORPH_FOOTER.as_bytes());
|
||||
valid.extend(footer);
|
||||
valid.push(1);
|
||||
assert!(ParsedFullMesh::parse(&valid).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_stops_at_first_nul() {
|
||||
assert_eq!(trim_at_nul("abc\0def\0"), "abc");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
//! Process-wide logging facade and callback compatibility layer.
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fmt;
|
||||
use std::sync::{
|
||||
Arc, Mutex, OnceLock,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
};
|
||||
use std::thread::ThreadId;
|
||||
use std::time::Duration;
|
||||
|
||||
use libremetaverse_types::compat::{
|
||||
CancellationToken, Close, ExternalError, Object, Subscription, Task, TaskCompletionSource,
|
||||
};
|
||||
|
||||
use crate::callback_runtime::AsyncInvocation;
|
||||
use crate::logging::{ILogger, ILoggerFactory, LogLevel};
|
||||
use crate::{Error, GridClient, Settings, Simulator};
|
||||
|
||||
struct LoggerState {
|
||||
factory: Option<Arc<dyn ILoggerFactory>>,
|
||||
logger: Option<Arc<dyn ILogger>>,
|
||||
callbacks: BTreeMap<u64, LoggerLogCallback>,
|
||||
next_callback: u64,
|
||||
}
|
||||
|
||||
impl Default for LoggerState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
factory: None,
|
||||
logger: None,
|
||||
callbacks: BTreeMap::new(),
|
||||
next_callback: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn state() -> &'static Mutex<LoggerState> {
|
||||
static STATE: OnceLock<Mutex<LoggerState>> = OnceLock::new();
|
||||
STATE.get_or_init(|| Mutex::new(LoggerState::default()))
|
||||
}
|
||||
|
||||
fn ensure_initialized() -> Arc<dyn ILogger> {
|
||||
let mut state = state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(logger) = state.logger.as_ref() {
|
||||
return Arc::clone(logger);
|
||||
}
|
||||
let factory: Arc<dyn ILoggerFactory> = Arc::new(ConsoleLoggerFactory::default());
|
||||
let logger = factory.create_logger("LibreMetaverse.Logger");
|
||||
state.factory = Some(factory);
|
||||
state.logger = Some(Arc::clone(&logger));
|
||||
logger
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ConsoleLoggerFactory {
|
||||
closed: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl ILoggerFactory for ConsoleLoggerFactory {
|
||||
fn create_logger(&self, name: &str) -> Arc<dyn ILogger> {
|
||||
Arc::new(ConsoleLogger {
|
||||
name: name.to_owned(),
|
||||
closed: Arc::clone(&self.closed),
|
||||
scopes: Arc::new(Mutex::new(HashMap::new())),
|
||||
next_scope: Arc::new(AtomicU64::new(1)),
|
||||
})
|
||||
}
|
||||
|
||||
fn shutdown(&self) -> Result<(), ExternalError> {
|
||||
self.closed.store(true, Ordering::Release);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
type ScopeMap = HashMap<ThreadId, Vec<(u64, Object)>>;
|
||||
|
||||
struct ConsoleLogger {
|
||||
name: String,
|
||||
closed: Arc<AtomicBool>,
|
||||
scopes: Arc<Mutex<ScopeMap>>,
|
||||
next_scope: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl ILogger for ConsoleLogger {
|
||||
fn is_enabled(&self, level: LogLevel) -> bool {
|
||||
!self.closed.load(Ordering::Acquire) && level != LogLevel::NONE && level >= LogLevel::DEBUG
|
||||
}
|
||||
|
||||
fn log(
|
||||
&self,
|
||||
level: LogLevel,
|
||||
message: &Object,
|
||||
exception: Option<&ExternalError>,
|
||||
client_name: Option<&str>,
|
||||
) {
|
||||
if !self.is_enabled(level) {
|
||||
return;
|
||||
}
|
||||
let scopes = self
|
||||
.scopes
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(&std::thread::current().id())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let scope_text = scopes
|
||||
.iter()
|
||||
.map(|(_, state)| format!("{state:?}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let level_name = match level {
|
||||
LogLevel::TRACE => "TRACE",
|
||||
LogLevel::DEBUG => "DEBUG",
|
||||
LogLevel::INFORMATION => "INFO",
|
||||
LogLevel::WARNING => "WARN",
|
||||
LogLevel::ERROR => "ERROR",
|
||||
LogLevel::CRITICAL => "CRITICAL",
|
||||
_ => "LOG",
|
||||
};
|
||||
let client = client_name
|
||||
.filter(|name| !name.is_empty())
|
||||
.map_or(String::new(), |name| format!(" [{name}]"));
|
||||
let scope = if scope_text.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" [{scope_text}]")
|
||||
};
|
||||
let exception = exception.map_or(String::new(), |error| format!(": {}", error.0));
|
||||
eprintln!(
|
||||
"[{level_name}] [{}]{client}{scope} {message:?}{exception}",
|
||||
self.name
|
||||
);
|
||||
}
|
||||
|
||||
fn begin_scope(&self, state: Object) -> Box<dyn Close> {
|
||||
let thread_id = std::thread::current().id();
|
||||
let id = self.next_scope.fetch_add(1, Ordering::Relaxed);
|
||||
self.scopes
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.entry(thread_id)
|
||||
.or_default()
|
||||
.push((id, state));
|
||||
Box::new(ConsoleScope {
|
||||
scopes: Arc::clone(&self.scopes),
|
||||
thread_id,
|
||||
id: Some(id),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsoleScope {
|
||||
scopes: Arc<Mutex<ScopeMap>>,
|
||||
thread_id: ThreadId,
|
||||
id: Option<u64>,
|
||||
}
|
||||
|
||||
impl Close for ConsoleScope {
|
||||
fn close(&mut self) -> Result<(), ExternalError> {
|
||||
let Some(id) = self.id.take() else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut scopes = self
|
||||
.scopes
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(thread_scopes) = scopes.get_mut(&self.thread_id) {
|
||||
thread_scopes.retain(|(scope_id, _)| *scope_id != id);
|
||||
if thread_scopes.is_empty() {
|
||||
scopes.remove(&self.thread_id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConsoleScope {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.close();
|
||||
}
|
||||
}
|
||||
|
||||
struct NoopScope;
|
||||
|
||||
impl Close for NoopScope {
|
||||
fn close(&mut self) -> Result<(), ExternalError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
type LogHandler = Arc<dyn Fn(Object, LogLevel) + Send + Sync>;
|
||||
|
||||
pub struct LoggerCallbackTarget(pub LogHandler);
|
||||
|
||||
impl fmt::Debug for LoggerCallbackTarget {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("LoggerCallbackTarget(..)")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LoggerLogCallback(LogHandler);
|
||||
|
||||
impl LoggerLogCallback {
|
||||
pub fn from_callback(callback: impl Fn(Object, LogLevel) + Send + Sync + 'static) -> Self {
|
||||
Self(Arc::new(callback))
|
||||
}
|
||||
|
||||
pub fn new(object: Object, method: isize) -> Result<Self, Error> {
|
||||
if method != 0 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
object
|
||||
.downcast_arc::<LoggerCallbackTarget>()
|
||||
.map(|target| Self(Arc::clone(&target.0)))
|
||||
.ok_or(Error::Argument)
|
||||
}
|
||||
|
||||
pub fn begin_invoke(
|
||||
&self,
|
||||
message: Object,
|
||||
level: LogLevel,
|
||||
callback: Box<dyn Fn(&dyn Any) + Send + Sync>,
|
||||
object: Object,
|
||||
) -> Result<Box<dyn Any + Send + Sync>, Error> {
|
||||
let invocation = AsyncInvocation::pending(object);
|
||||
let worker_invocation = invocation.clone();
|
||||
let handler = Arc::clone(&self.0);
|
||||
std::thread::Builder::new()
|
||||
.name("logger-callback".to_owned())
|
||||
.spawn(move || {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
handler(message, level);
|
||||
}))
|
||||
.map_err(|_| Error::InvalidOperation);
|
||||
worker_invocation.complete(result);
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
callback(&worker_invocation);
|
||||
}));
|
||||
})
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
Ok(Box::new(invocation))
|
||||
}
|
||||
|
||||
pub fn end_invoke(&self, result: Box<dyn Any + Send + Sync>) -> Result<(), Error> {
|
||||
result
|
||||
.downcast::<AsyncInvocation>()
|
||||
.map_err(|_| Error::Argument)?
|
||||
.wait()
|
||||
}
|
||||
|
||||
pub fn invoke(&self, message: Object, level: LogLevel) -> Result<(), Error> {
|
||||
(self.0)(message, level);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Logger;
|
||||
|
||||
impl Logger {
|
||||
pub fn subscribe_on_log_message(handler: Option<LoggerLogCallback>) -> Subscription {
|
||||
let Some(handler) = handler else {
|
||||
return Subscription::detached();
|
||||
};
|
||||
let id = {
|
||||
let mut state = state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let id = state.next_callback;
|
||||
state.next_callback = state.next_callback.wrapping_add(1).max(1);
|
||||
state.callbacks.insert(id, handler);
|
||||
id
|
||||
};
|
||||
Subscription::new(move || {
|
||||
state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.callbacks
|
||||
.remove(&id);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn begin_scope(state_value: Object) -> Result<Box<dyn Close>, Error> {
|
||||
let logger = state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.logger
|
||||
.clone();
|
||||
Ok(logger.map_or_else(
|
||||
|| Box::new(NoopScope) as Box<dyn Close>,
|
||||
|logger| logger.begin_scope(state_value),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn begin_client_scope(mut client: GridClient) -> Result<Box<dyn Close>, Error> {
|
||||
let log_names = client.settings().logging.log_names;
|
||||
if !log_names {
|
||||
return Ok(Box::new(NoopScope));
|
||||
}
|
||||
let name = client.self_().name();
|
||||
if name.is_empty() {
|
||||
return Ok(Box::new(NoopScope));
|
||||
}
|
||||
Self::begin_scope(Object::Map(HashMap::from([(
|
||||
"Client".to_owned(),
|
||||
Object::String(name),
|
||||
)])))
|
||||
}
|
||||
|
||||
pub fn begin_region_scope(simulator: Simulator) -> Result<Box<dyn Close>, Error> {
|
||||
let name = if simulator.name.is_empty() {
|
||||
simulator.handle.to_string()
|
||||
} else {
|
||||
simulator.name.clone()
|
||||
};
|
||||
Self::begin_scope(Object::Map(HashMap::from([(
|
||||
"Region".to_owned(),
|
||||
Object::String(name),
|
||||
)])))
|
||||
}
|
||||
|
||||
pub fn create_default_console_logger_factory() -> Result<Box<dyn ILoggerFactory>, Error> {
|
||||
Ok(Box::new(ConsoleLoggerFactory::default()))
|
||||
}
|
||||
|
||||
pub fn set_logger_factory(factory: Box<dyn ILoggerFactory>, name: String) -> Result<(), Error> {
|
||||
let factory: Arc<dyn ILoggerFactory> = Arc::from(factory);
|
||||
let logger = factory.create_logger(&name);
|
||||
let previous = {
|
||||
let mut state = state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let previous = state.factory.replace(Arc::clone(&factory));
|
||||
state.logger = Some(logger);
|
||||
previous
|
||||
};
|
||||
if let Some(previous) = previous {
|
||||
let _ = previous.shutdown();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn shutdown_with_method() -> Result<(), Error> {
|
||||
let factory = {
|
||||
let mut state = state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
state.logger = None;
|
||||
state.factory.take()
|
||||
};
|
||||
if let Some(factory) = factory {
|
||||
let _ = factory.shutdown();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown_with_nullable_cancellation_token(
|
||||
timeout: Option<Option<Duration>>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<bool, Error> {
|
||||
let factory = {
|
||||
let mut state = state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
state.logger = None;
|
||||
state.factory.take()
|
||||
};
|
||||
let Some(factory) = factory else {
|
||||
return Ok(true);
|
||||
};
|
||||
let completion = TaskCompletionSource::new();
|
||||
let shutdown_completion = completion.clone();
|
||||
if std::thread::Builder::new()
|
||||
.name("logger-shutdown".to_owned())
|
||||
.spawn(move || {
|
||||
let _ = shutdown_completion.try_set_result(factory.shutdown().is_ok());
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
let cancellation = cancellation_token.map(|token| {
|
||||
let completion = completion.clone();
|
||||
token.register_callback(Arc::new(move || {
|
||||
let _ = completion.try_set_result(false);
|
||||
}))
|
||||
});
|
||||
if let Some(timeout) = timeout.flatten() {
|
||||
let timeout_completion = completion.clone();
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("logger-shutdown-timeout".to_owned())
|
||||
.spawn(move || {
|
||||
std::thread::sleep(timeout);
|
||||
let _ = timeout_completion.try_set_result(false);
|
||||
});
|
||||
}
|
||||
let result = completion.future().await;
|
||||
drop(cancellation);
|
||||
result
|
||||
}
|
||||
|
||||
fn callbacks() -> Vec<LoggerLogCallback> {
|
||||
state()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.callbacks
|
||||
.values()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn raise(message: &Object, level: LogLevel) {
|
||||
for callback in Self::callbacks() {
|
||||
let message = message.clone();
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("logger-event".to_owned())
|
||||
.spawn(move || {
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let _ = callback.invoke(message, level);
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn client_name(mut client: Option<GridClient>) -> Option<String> {
|
||||
let client = client.as_mut()?;
|
||||
if !client.settings().logging.log_names {
|
||||
return None;
|
||||
}
|
||||
let name = client.self_().name();
|
||||
(!name.is_empty()).then_some(name)
|
||||
}
|
||||
|
||||
fn log_to_sink(
|
||||
message: &Object,
|
||||
level: LogLevel,
|
||||
exception: Option<&ExternalError>,
|
||||
client_name: Option<&str>,
|
||||
) {
|
||||
let logger = ensure_initialized();
|
||||
if logger.is_enabled(level) {
|
||||
logger.log(level, message, exception, client_name);
|
||||
}
|
||||
}
|
||||
|
||||
fn log(
|
||||
message: Object,
|
||||
level: LogLevel,
|
||||
client: Option<GridClient>,
|
||||
exception: Option<ExternalError>,
|
||||
) -> Result<(), Error> {
|
||||
Self::raise(&message, level);
|
||||
let configured = Settings::log_level();
|
||||
if configured == LogLevel::NONE || configured > level {
|
||||
return Ok(());
|
||||
}
|
||||
let client_name = Self::client_name(client);
|
||||
Self::log_to_sink(&message, level, exception.as_ref(), client_name.as_deref());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn log_named(
|
||||
message: Object,
|
||||
level: LogLevel,
|
||||
exception: Option<ExternalError>,
|
||||
client_name: String,
|
||||
) -> Result<(), Error> {
|
||||
Self::log_to_sink(&message, level, exception.as_ref(), Some(&client_name));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_enabled(level: LogLevel) -> Result<bool, Error> {
|
||||
Ok(ensure_initialized().is_enabled(level))
|
||||
}
|
||||
|
||||
pub fn log_with_object_log_level_grid_client(
|
||||
message: Object,
|
||||
level: LogLevel,
|
||||
client: Option<GridClient>,
|
||||
) -> Result<(), Error> {
|
||||
Self::log(message, level, client, None)
|
||||
}
|
||||
|
||||
pub fn log_with_object_log_level_grid_client_exception(
|
||||
message: Object,
|
||||
level: LogLevel,
|
||||
client: Option<GridClient>,
|
||||
exception: Option<ExternalError>,
|
||||
) -> Result<(), Error> {
|
||||
Self::log(message, level, client, exception)
|
||||
}
|
||||
|
||||
pub fn log_with_object_log_level_exception(
|
||||
message: Object,
|
||||
level: LogLevel,
|
||||
exception: ExternalError,
|
||||
) -> Result<(), Error> {
|
||||
Self::log(message, level, None, Some(exception))
|
||||
}
|
||||
|
||||
pub fn debug_log(message: Object, client: Option<GridClient>) -> Result<(), Error> {
|
||||
let level = Settings::log_level();
|
||||
if level > LogLevel::DEBUG {
|
||||
return Ok(());
|
||||
}
|
||||
Self::log(message, level, client, None)
|
||||
}
|
||||
|
||||
pub async fn use_scope(
|
||||
state: Object,
|
||||
action: Box<dyn Fn() -> Task<()> + Send + Sync>,
|
||||
) -> Result<(), Error> {
|
||||
let mut scope = Self::begin_scope(state)?;
|
||||
let result = action().await;
|
||||
let close = scope.close().map_err(|_| Error::InvalidOperation);
|
||||
result.and(close)
|
||||
}
|
||||
|
||||
pub async fn use_client_scope(
|
||||
client: GridClient,
|
||||
action: Box<dyn Fn() -> Task<()> + Send + Sync>,
|
||||
) -> Result<(), Error> {
|
||||
let mut scope = Self::begin_client_scope(client)?;
|
||||
let result = action().await;
|
||||
let close = scope.close().map_err(|_| Error::InvalidOperation);
|
||||
result.and(close)
|
||||
}
|
||||
|
||||
pub async fn use_region_scope(
|
||||
simulator: Simulator,
|
||||
action: Box<dyn Fn() -> Task<()> + Send + Sync>,
|
||||
) -> Result<(), Error> {
|
||||
let mut scope = Self::begin_region_scope(simulator)?;
|
||||
let result = action().await;
|
||||
let close = scope.close().map_err(|_| Error::InvalidOperation);
|
||||
result.and(close)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! level_methods {
|
||||
($client:ident, $client_error:ident, $named:ident, $named_error:ident, $level:expr) => {
|
||||
impl Logger {
|
||||
pub fn $client(message: Object, client: Option<GridClient>) -> Result<(), Error> {
|
||||
Self::log(message, $level, client, None)
|
||||
}
|
||||
|
||||
pub fn $client_error(
|
||||
message: Object,
|
||||
exception: ExternalError,
|
||||
client: Option<GridClient>,
|
||||
) -> Result<(), Error> {
|
||||
Self::log(message, $level, client, Some(exception))
|
||||
}
|
||||
|
||||
pub fn $named(message: Object, client_name: String) -> Result<(), Error> {
|
||||
Self::log_named(message, $level, None, client_name)
|
||||
}
|
||||
|
||||
pub fn $named_error(
|
||||
message: Object,
|
||||
exception: ExternalError,
|
||||
client_name: String,
|
||||
) -> Result<(), Error> {
|
||||
Self::log_named(message, $level, Some(exception), client_name)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
level_methods!(
|
||||
trace_with_object_grid_client,
|
||||
trace_with_object_exception_grid_client,
|
||||
trace_with_object_string,
|
||||
trace_with_object_exception_string,
|
||||
LogLevel::TRACE
|
||||
);
|
||||
level_methods!(
|
||||
debug_with_object_grid_client,
|
||||
debug_with_object_exception_grid_client,
|
||||
debug_with_object_string,
|
||||
debug_with_object_exception_string,
|
||||
LogLevel::DEBUG
|
||||
);
|
||||
level_methods!(
|
||||
info_with_object_grid_client,
|
||||
info_with_object_exception_grid_client,
|
||||
info_with_object_string,
|
||||
info_with_object_exception_string,
|
||||
LogLevel::INFORMATION
|
||||
);
|
||||
level_methods!(
|
||||
warn_with_object_grid_client,
|
||||
warn_with_object_exception_grid_client,
|
||||
warn_with_object_string,
|
||||
warn_with_object_exception_string,
|
||||
LogLevel::WARNING
|
||||
);
|
||||
level_methods!(
|
||||
error_with_object_grid_client,
|
||||
error_with_object_exception_grid_client,
|
||||
error_with_object_string,
|
||||
error_with_object_exception_string,
|
||||
LogLevel::ERROR
|
||||
);
|
||||
level_methods!(
|
||||
critical_with_object_grid_client,
|
||||
critical_with_object_exception_grid_client,
|
||||
critical_with_object_string,
|
||||
critical_with_object_exception_string,
|
||||
LogLevel::CRITICAL
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
|
||||
#[test]
|
||||
fn event_subscription_delivers_and_unregisters() {
|
||||
Logger::shutdown_with_method().unwrap();
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&calls);
|
||||
let subscription = Logger::subscribe_on_log_message(Some(
|
||||
LoggerLogCallback::from_callback(move |_, _| {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
}),
|
||||
));
|
||||
Logger::info_with_object_grid_client(Object::String("one".to_owned()), None).unwrap();
|
||||
for _ in 0..100 {
|
||||
if calls.load(Ordering::Relaxed) == 1 {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
assert_eq!(calls.load(Ordering::Relaxed), 1);
|
||||
drop(subscription);
|
||||
Logger::info_with_object_grid_client(Object::String("two".to_owned()), None).unwrap();
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
assert_eq!(calls.load(Ordering::Relaxed), 1);
|
||||
Logger::shutdown_with_method().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scope_close_is_idempotent() {
|
||||
let _ = ensure_initialized();
|
||||
let mut scope = Logger::begin_scope(Object::String("test".to_owned())).unwrap();
|
||||
scope.close().unwrap();
|
||||
scope.close().unwrap();
|
||||
Logger::shutdown_with_method().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1634,7 +1634,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let result = response.parse_with_osd_map(map).unwrap();
|
||||
assert!(result.success());
|
||||
assert_eq!(response.home().region_handle, (1000_u64 << 32) | 2000);
|
||||
assert_eq!(response.home().region_handle, (1000_u64 << 32) | 0x07d0);
|
||||
assert_eq!(response.inventory_skeleton().len(), 1);
|
||||
let inventory = response.inventory_skeleton();
|
||||
assert_eq!(inventory[0].base.uuid(), folder);
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
//! Runtime LSL keyword catalog with OpenSim capability refresh support.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)] // Public signatures mirror the compatibility map.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::Engine as _;
|
||||
use flate2::read::GzDecoder;
|
||||
use libremetaverse_structured_data::{OSD, OSDParser};
|
||||
use libremetaverse_types::UUID;
|
||||
use libremetaverse_types::compat::{
|
||||
CancellationToken, EventHandler, FrozenDictionary, Subscription, Uri,
|
||||
};
|
||||
|
||||
use crate::{Error, GridClient, LslSyntaxLslCategory, LslSyntaxLslKeyword, Simulator};
|
||||
|
||||
const VERSION_KEY: &str = "llsd-lsl-syntax-version";
|
||||
const SYNTAX_FEATURE: &str = "LSLSyntaxId";
|
||||
const SYNTAX_CAPABILITY: &str = "LSLSyntax";
|
||||
const MAX_SYNTAX_BYTES: usize = 8 * 1024 * 1024;
|
||||
const FETCH_WAIT: Duration = Duration::from_secs(20);
|
||||
const DEFAULT_SYNTAX_GZIP_BASE64: &str =
|
||||
include_str!("../resources/keywords_lsl_default.xml.gz.b64");
|
||||
|
||||
static KEYWORDS: OnceLock<RwLock<HashMap<String, LslSyntaxLslKeyword>>> = OnceLock::new();
|
||||
|
||||
fn keyword_store() -> &'static RwLock<HashMap<String, LslSyntaxLslKeyword>> {
|
||||
KEYWORDS.get_or_init(|| RwLock::new(HashMap::new()))
|
||||
}
|
||||
|
||||
struct SyntaxEvents {
|
||||
next_id: AtomicU64,
|
||||
handlers: Mutex<Vec<(u64, EventHandler<()>)>>,
|
||||
}
|
||||
|
||||
impl Default for SyntaxEvents {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
next_id: AtomicU64::new(1),
|
||||
handlers: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LslSyntaxInner {
|
||||
events: Arc<SyntaxEvents>,
|
||||
client: Mutex<Option<GridClient>>,
|
||||
syntax_id: Mutex<UUID>,
|
||||
subscriptions: Mutex<Vec<Subscription>>,
|
||||
}
|
||||
|
||||
/// Process-wide LSL keyword catalog and per-registration change notification.
|
||||
#[derive(Clone)]
|
||||
pub struct LslSyntax(Arc<LslSyntaxInner>);
|
||||
|
||||
impl LslSyntax {
|
||||
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||
let syntax = Self::empty();
|
||||
syntax.load_default()?;
|
||||
Ok(syntax)
|
||||
}
|
||||
|
||||
pub fn new_with_grid_client(client: GridClient) -> Result<Self, Error> {
|
||||
let syntax = Self::empty();
|
||||
syntax.register(client)?;
|
||||
Ok(syntax)
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
Self(Arc::new(LslSyntaxInner {
|
||||
events: Arc::new(SyntaxEvents::default()),
|
||||
client: Mutex::new(None),
|
||||
syntax_id: Mutex::new(UUID::zero()),
|
||||
subscriptions: Mutex::new(Vec::new()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn subscribe_syntax_changed(&self, handler: EventHandler<()>) -> Subscription {
|
||||
let id = self.0.events.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
self.0
|
||||
.events
|
||||
.handlers
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push((id, handler));
|
||||
let events = Arc::downgrade(&self.0.events);
|
||||
Subscription::new(move || {
|
||||
if let Some(events) = events.upgrade() {
|
||||
events
|
||||
.handlers
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.retain(|(candidate, _)| *candidate != id);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn keywords() -> FrozenDictionary<String, LslSyntaxLslKeyword> {
|
||||
FrozenDictionary(
|
||||
keyword_store()
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn register(&self, client: GridClient) -> Result<(), Error> {
|
||||
*self
|
||||
.0
|
||||
.client
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(client.clone());
|
||||
|
||||
// Always establish a useful catalog, including on OpenSim grids that
|
||||
// do not advertise the optional LSLSyntax capability.
|
||||
self.load_default()?;
|
||||
|
||||
let inner = Arc::clone(&self.0);
|
||||
let network = client.native_network()?;
|
||||
let client_for_change = client.clone();
|
||||
let changed = network.native_subscribe_sim_changed(Arc::new(move |_| {
|
||||
if let Ok(network) = client_for_change.native_network()
|
||||
&& let Some(simulator) = network.native_current_sim()
|
||||
{
|
||||
LslSyntaxInner::observe_simulator(&inner, simulator, false);
|
||||
}
|
||||
}));
|
||||
self.retain_subscription(changed);
|
||||
|
||||
if let Some(simulator) = network.native_current_sim() {
|
||||
LslSyntaxInner::observe_simulator(&self.0, simulator, true);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn retain_subscription(&self, subscription: Subscription) {
|
||||
self.0
|
||||
.subscriptions
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push(subscription);
|
||||
}
|
||||
|
||||
fn load_default(&self) -> Result<(), Error> {
|
||||
let compact = DEFAULT_SYNTAX_GZIP_BASE64
|
||||
.split_ascii_whitespace()
|
||||
.collect::<String>();
|
||||
let compressed = base64::engine::general_purpose::STANDARD
|
||||
.decode(compact)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
let mut decoder = GzDecoder::new(compressed.as_slice());
|
||||
let mut bytes = Vec::new();
|
||||
decoder
|
||||
.by_ref()
|
||||
.take((MAX_SYNTAX_BYTES + 1) as u64)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
if bytes.len() > MAX_SYNTAX_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
self.0.parse_and_publish(&bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl LslSyntaxInner {
|
||||
fn observe_simulator(this: &Arc<Self>, simulator: Simulator, wait: bool) {
|
||||
if let Some(caps) = simulator.native_caps() {
|
||||
let weak = Arc::downgrade(this);
|
||||
let subscription = caps.subscribe_capabilities_received(Some(Arc::new(move |args| {
|
||||
if let Some(inner) = weak.upgrade() {
|
||||
inner.refresh(args.simulator(), false);
|
||||
}
|
||||
})));
|
||||
this.subscriptions
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push(subscription);
|
||||
}
|
||||
this.refresh(simulator, wait);
|
||||
}
|
||||
|
||||
fn refresh(self: &Arc<Self>, simulator: Simulator, wait: bool) {
|
||||
let syntax_id = match simulator.features.get(SYNTAX_FEATURE.to_owned()) {
|
||||
Ok(Some(value)) => match value.as_uuid() {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
},
|
||||
_ => return,
|
||||
};
|
||||
if *self
|
||||
.syntax_id
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
== syntax_id
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(client) = self
|
||||
.client
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let cache = cache_path(&client, syntax_id);
|
||||
if let Ok(bytes) = std::fs::read(&cache)
|
||||
&& self.parse_and_publish(&bytes).is_ok()
|
||||
{
|
||||
*self
|
||||
.syntax_id
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = syntax_id;
|
||||
return;
|
||||
}
|
||||
let Ok(Some(uri)) = simulator.native_capability_uri(SYNTAX_CAPABILITY) else {
|
||||
return;
|
||||
};
|
||||
self.fetch(client, uri, syntax_id, cache, wait);
|
||||
}
|
||||
|
||||
fn fetch(
|
||||
self: &Arc<Self>,
|
||||
client: GridClient,
|
||||
uri: Uri,
|
||||
syntax_id: UUID,
|
||||
cache: PathBuf,
|
||||
wait: bool,
|
||||
) {
|
||||
let inner = Arc::clone(self);
|
||||
let (completed, receiver) = std::sync::mpsc::sync_channel(1);
|
||||
let spawn = std::thread::Builder::new()
|
||||
.name("lsl-syntax-fetch".to_owned())
|
||||
.spawn(move || {
|
||||
let outcome = fetch_bytes(&client, uri).and_then(|bytes| {
|
||||
inner.parse_and_publish(&bytes)?;
|
||||
persist_cache(&cache, &bytes)?;
|
||||
*inner
|
||||
.syntax_id
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = syntax_id;
|
||||
Ok(())
|
||||
});
|
||||
let _ = completed.send(outcome);
|
||||
});
|
||||
if spawn.is_err() {
|
||||
return;
|
||||
}
|
||||
if wait {
|
||||
let _ = receiver.recv_timeout(FETCH_WAIT);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_and_publish(&self, bytes: &[u8]) -> Result<(), Error> {
|
||||
if bytes.is_empty() || bytes.len() > MAX_SYNTAX_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let OSD::Map(root) = OSDParser::deserialize_with_bytes(bytes.to_vec())? else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let parsed = parse_keywords(&root)?;
|
||||
if parsed.is_empty() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
*keyword_store()
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = parsed;
|
||||
let handlers = self
|
||||
.events
|
||||
.handlers
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.iter()
|
||||
.map(|(_, handler)| Arc::clone(handler))
|
||||
.collect::<Vec<_>>();
|
||||
for handler in handlers {
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(())));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_bytes(client: &GridClient, uri: Uri) -> Result<Vec<u8>, Error> {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
let http = client.native_http_caps_client();
|
||||
let (response, bytes) = runtime.block_on(http.get(uri, CancellationToken::default(), None))?;
|
||||
if !response.is_success_status_code() || bytes.is_empty() || bytes.len() > MAX_SYNTAX_BYTES {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn cache_path(client: &GridClient, syntax_id: UUID) -> PathBuf {
|
||||
Path::new(&client.settings_ref().asset_cache.dir).join(format!("keywords_lsl_{syntax_id}.xml"))
|
||||
}
|
||||
|
||||
fn persist_cache(path: &Path, bytes: &[u8]) -> Result<(), Error> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|_| Error::InvalidOperation)?;
|
||||
}
|
||||
std::fs::write(path, bytes).map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
|
||||
fn parse_keywords(
|
||||
root: &HashMap<String, OSD>,
|
||||
) -> Result<HashMap<String, LslSyntaxLslKeyword>, Error> {
|
||||
if root
|
||||
.get(VERSION_KEY)
|
||||
.map(OSD::as_integer)
|
||||
.transpose()?
|
||||
.is_some_and(|version| version != 2)
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut keywords = HashMap::new();
|
||||
let mut groups = root.iter().collect::<Vec<_>>();
|
||||
groups.sort_by(|left, right| left.0.cmp(right.0));
|
||||
for (group, value) in groups {
|
||||
if group == VERSION_KEY {
|
||||
continue;
|
||||
}
|
||||
let OSD::Map(items) = value else {
|
||||
continue;
|
||||
};
|
||||
let Some(category) = category(group) else {
|
||||
continue;
|
||||
};
|
||||
let mut items = items.iter().collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| left.0.cmp(right.0));
|
||||
for (keyword, value) in items {
|
||||
let OSD::Map(attributes) = value else {
|
||||
continue;
|
||||
};
|
||||
let tooltip = tooltip(group, keyword, attributes)?;
|
||||
let value = LslSyntaxLslKeyword {
|
||||
category,
|
||||
keyword: keyword.clone(),
|
||||
tooltip,
|
||||
deprecated: boolean_attribute(attributes, "deprecated")?,
|
||||
god_mode: boolean_attribute(attributes, "god-mode")?,
|
||||
};
|
||||
if keywords.insert(keyword.clone(), value).is_some() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(keywords)
|
||||
}
|
||||
|
||||
fn category(group: &str) -> Option<LslSyntaxLslCategory> {
|
||||
match group {
|
||||
"controls" => Some(LslSyntaxLslCategory::Control),
|
||||
"types" => Some(LslSyntaxLslCategory::Datatype),
|
||||
"constants" => Some(LslSyntaxLslCategory::Constant),
|
||||
"events" => Some(LslSyntaxLslCategory::Event),
|
||||
"functions" => Some(LslSyntaxLslCategory::Function),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn tooltip(group: &str, keyword: &str, attributes: &HashMap<String, OSD>) -> Result<String, Error> {
|
||||
let mut output = attributes
|
||||
.get("tooltip")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
if attributes.contains_key("tooltip") {
|
||||
output.push('\n');
|
||||
}
|
||||
match group {
|
||||
"constants" => {
|
||||
let type_ = string_attribute(attributes, "type")?;
|
||||
let value = string_attribute(attributes, "value")?;
|
||||
output.push_str(&format!(" Type: {type_}-{value}"));
|
||||
}
|
||||
"events" => {
|
||||
output.push_str(&format!(
|
||||
"{keyword} ({})",
|
||||
parse_arguments(attributes.get("arguments"))?
|
||||
));
|
||||
}
|
||||
"functions" => {
|
||||
let return_ = string_attribute(attributes, "return")?;
|
||||
output.push_str(&format!(
|
||||
"{return_} {keyword} ({})\n",
|
||||
parse_arguments(attributes.get("arguments"))?
|
||||
));
|
||||
let energy = attributes
|
||||
.get("energy")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| "0.0".to_owned());
|
||||
output.push_str(&format!("Energy: {energy}"));
|
||||
if let Some(sleep) = attributes.get("sleep") {
|
||||
output.push_str(&format!(", Sleep: {}", sleep.as_string()?));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn parse_arguments(value: Option<&OSD>) -> Result<String, Error> {
|
||||
let Some(OSD::Array(arguments)) = value else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
let mut rendered = Vec::new();
|
||||
for argument in arguments {
|
||||
let OSD::Map(argument) = argument else {
|
||||
continue;
|
||||
};
|
||||
let mut values = argument.iter().collect::<Vec<_>>();
|
||||
values.sort_by(|left, right| left.0.cmp(right.0));
|
||||
for (name, type_) in values {
|
||||
rendered.push(format!("{} {name}", type_.as_string()?));
|
||||
}
|
||||
}
|
||||
Ok(rendered.join(", "))
|
||||
}
|
||||
|
||||
fn string_attribute(attributes: &HashMap<String, OSD>, name: &str) -> Result<String, Error> {
|
||||
attributes
|
||||
.get(name)
|
||||
.map(OSD::as_string)
|
||||
.transpose()
|
||||
.map(Option::unwrap_or_default)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn boolean_attribute(attributes: &HashMap<String, OSD>, name: &str) -> Result<bool, Error> {
|
||||
attributes
|
||||
.get(name)
|
||||
.map(OSD::as_boolean)
|
||||
.transpose()
|
||||
.map(|value| value.unwrap_or(false))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use super::*;
|
||||
|
||||
static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn embedded_catalog_is_complete_and_typed() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
let _syntax = LslSyntax::new_with_constructor().unwrap();
|
||||
let keywords = LslSyntax::keywords().0;
|
||||
assert!(keywords.len() > 500);
|
||||
assert_eq!(
|
||||
keywords.get("llSay").unwrap().category,
|
||||
LslSyntaxLslCategory::Function
|
||||
);
|
||||
assert_eq!(
|
||||
keywords.get("state_entry").unwrap().category,
|
||||
LslSyntaxLslCategory::Event
|
||||
);
|
||||
assert_eq!(
|
||||
keywords.get("integer").unwrap().category,
|
||||
LslSyntaxLslCategory::Datatype
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsed_catalog_replaces_snapshot_and_notifies_subscribers() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
let syntax = LslSyntax::empty();
|
||||
let notifications = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(¬ifications);
|
||||
let _subscription = syntax.subscribe_syntax_changed(Arc::new(move |()| {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
}));
|
||||
let function = OSD::Map(HashMap::from([
|
||||
("return".to_owned(), OSD::String("integer".to_owned())),
|
||||
("energy".to_owned(), OSD::String("10.0".to_owned())),
|
||||
("deprecated".to_owned(), OSD::Boolean(true)),
|
||||
(
|
||||
"arguments".to_owned(),
|
||||
OSD::Array(vec![OSD::Map(HashMap::from([(
|
||||
"message".to_owned(),
|
||||
OSD::String("string".to_owned()),
|
||||
)]))]),
|
||||
),
|
||||
]));
|
||||
let document = OSD::Map(HashMap::from([
|
||||
(VERSION_KEY.to_owned(), OSD::Integer(2)),
|
||||
(
|
||||
"functions".to_owned(),
|
||||
OSD::Map(HashMap::from([("osTest".to_owned(), function)])),
|
||||
),
|
||||
]));
|
||||
let bytes = OSDParser::serialize_llsd_xml_bytes(document).unwrap();
|
||||
syntax.0.parse_and_publish(&bytes).unwrap();
|
||||
|
||||
assert_eq!(notifications.load(Ordering::Relaxed), 1);
|
||||
let keywords = LslSyntax::keywords().0;
|
||||
let keyword = keywords.get("osTest").unwrap();
|
||||
assert!(keyword.deprecated);
|
||||
assert_eq!(keyword.category, LslSyntaxLslCategory::Function);
|
||||
assert!(keyword.tooltip.contains("integer osTest (string message)"));
|
||||
assert!(keyword.tooltip.contains("Energy: 10.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registration_on_opensim_without_syntax_capability_uses_default_catalog() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
let client = GridClient::builder().build().unwrap();
|
||||
let syntax = LslSyntax::new_with_grid_client(client).unwrap();
|
||||
assert!(LslSyntax::keywords().0.contains_key("llOwnerSay"));
|
||||
assert_eq!(
|
||||
syntax
|
||||
.0
|
||||
.subscriptions
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,11 +1,17 @@
|
||||
//! Native LLSD field codecs for the translated Linden message fixtures.
|
||||
|
||||
#![allow(clippy::cast_possible_truncation)] // LLSD reals intentionally narrow to wire f32 fields.
|
||||
#![allow(clippy::too_many_arguments)] // Codec helpers mirror fixed message field sets.
|
||||
#![allow(clippy::type_complexity)] // Parsed tuples preserve the mapped message shape.
|
||||
#![allow(clippy::unnecessary_wraps)] // Handlers share the fallible registry signature.
|
||||
|
||||
use crate::messages::linden::{
|
||||
AgentDropGroupMessage, AgentDropGroupMessageAgentData, AgentGroupDataUpdateMessage,
|
||||
AgentGroupDataUpdateMessageGroupData, AgentGroupDataUpdateMessageNewGroupData,
|
||||
AgentStateUpdateMessage, BulkUpdateInventoryMessage, BulkUpdateInventoryMessageFolderDataInfo,
|
||||
BulkUpdateInventoryMessageItemDataInfo, ChatSessionAcceptInvitation, ChatSessionRequestBlock,
|
||||
ChatSessionRequestMuteUpdate, ChatSessionRequestStartConference, ChatterBoxInvitationMessage,
|
||||
AgentStateUpdateMessage, BaseResourcesInfo, BulkUpdateInventoryMessage,
|
||||
BulkUpdateInventoryMessageFolderDataInfo, BulkUpdateInventoryMessageItemDataInfo,
|
||||
ChatSessionAcceptInvitation, ChatSessionRequestMessage, ChatSessionRequestMuteUpdate,
|
||||
ChatSessionRequestStartConference, ChatterBoxInvitationMessage,
|
||||
ChatterBoxSessionAgentListUpdatesMessage,
|
||||
ChatterBoxSessionAgentListUpdatesMessageAgentUpdatesBlock, ChatterBoxSessionStartReplyMessage,
|
||||
ChatterboxSessionEventReplyMessage, CopyInventoryFromNotecardMessage, CrossedRegionMessage,
|
||||
@@ -13,20 +19,29 @@ use crate::messages::linden::{
|
||||
EnableSimulatorMessage, EnableSimulatorMessageSimulatorInfoBlock,
|
||||
EstablishAgentCommunicationMessage, ForceCloseChatterBoxSessionMessage, GetDisplayNamesMessage,
|
||||
GetObjectCostMessage, GetObjectCostRequest, LandResourcesMessage, LandResourcesRequest,
|
||||
LandStatReplyMessage, LandStatReplyMessageReportDataBlock, MapLayerMessageBase,
|
||||
LandStatReplyMessage, LandStatReplyMessageReportDataBlock, MapLayerMessage,
|
||||
MapLayerReplyVariant, MapLayerReplyVariantLayerData, NavMeshStatusUpdateMessage,
|
||||
ObjectMediaNavigateMessage, ObjectMediaRequest, ObjectMediaResponse, ObjectMediaUpdate,
|
||||
ObjectPhysicsPropertiesMessage, ParcelObjectOwnersReplyMessage,
|
||||
ParcelObjectOwnersReplyMessagePrimOwner, ParcelPropertiesMessage,
|
||||
ParcelPropertiesUpdateMessage, ParcelVoiceInfoRequestMessage, PlacesReplyMessage,
|
||||
PlacesReplyMessageQueryData, ProvisionVoiceAccountRequestMessage, RegionInfoMessage,
|
||||
RemoteParcelRequestReply, RenderMaterialsMessage, RequiredVoiceVersionMessage,
|
||||
ScriptRunningReplyMessage, SearchStatRequestReply, SearchStatRequestRequest,
|
||||
SendPostcardMessage, SetDisplayNameMessage, SetDisplayNameReplyMessage,
|
||||
SimConsoleResponseMessage, TeleportFailedMessage, TeleportFinishMessage,
|
||||
UpdateAgentInformationMessage, UpdateAgentInventoryRequestMessage, UpdateAgentLanguageMessage,
|
||||
UpdateScriptAgentRequestMessage, UpdateScriptTaskUpdateMessage, UploaderRequestComplete,
|
||||
UploaderRequestUpload, ViewerStatsMessage,
|
||||
NewFileAgentInventoryMessage, NewFileAgentInventoryReplyMessage,
|
||||
NewFileAgentInventoryUploadReplyMessage, NewFileAgentInventoryVariablePriceMessage,
|
||||
NewFileAgentInventoryVariablePriceReplyMessage, ObjectMediaMessage, ObjectMediaNavigateMessage,
|
||||
ObjectMediaRequest, ObjectMediaResponse, ObjectMediaUpdate, ObjectPhysicsPropertiesMessage,
|
||||
ParcelObjectOwnersReplyMessage, ParcelObjectOwnersReplyMessagePrimOwner,
|
||||
ParcelPropertiesMessage, ParcelPropertiesUpdateMessage, ParcelVoiceInfoRequestMessage,
|
||||
PlacesReplyMessage, PlacesReplyMessageQueryData, ProvisionVoiceAccountRequestMessage,
|
||||
RegionInfoMessage, RemoteParcelRequestMessage, RemoteParcelRequestReply,
|
||||
RemoteParcelRequestRequest, RenderMaterialsMessage, RequiredVoiceVersionMessage,
|
||||
ScriptRunningReplyMessage, SearchStatRequestMessage, SearchStatRequestReply,
|
||||
SearchStatRequestRequest, SendPostcardMessage, SetDisplayNameMessage,
|
||||
SetDisplayNameReplyMessage, SimConsoleResponseMessage, TeleportFailedMessage,
|
||||
TeleportFinishMessage, UpdateAgentInformationMessage, UpdateAgentInventoryRequestMessage,
|
||||
UpdateAgentLanguageMessage, UpdateGestureAgentInventoryMessage,
|
||||
UpdateNotecardAgentInventoryMessage, UpdateNotecardTaskInventoryMessage,
|
||||
UpdateScriptAgentMessage, UpdateScriptAgentRequestMessage, UpdateScriptTaskMessage,
|
||||
UpdateScriptTaskUpdateMessage, UploadBakedTextureMessage, UploadObjectAssetMessage,
|
||||
UploadObjectAssetMessageObject, UploadObjectAssetMessageObjectExtraParam,
|
||||
UploadObjectAssetMessageObjectFace, UploadScriptTaskMessage, UploaderRequestComplete,
|
||||
UploaderRequestScriptComplete, UploaderRequestUpload, UploaderScriptRequestError,
|
||||
ViewerStatsMessage,
|
||||
};
|
||||
use crate::{
|
||||
AgentDisplayName, Error, GroupPowers, InstantMessageDialog, InstantMessageOnline, LandingType,
|
||||
@@ -36,7 +51,10 @@ use crate::{
|
||||
use flate2::read::ZlibDecoder;
|
||||
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
||||
use libremetaverse_types::compat::Uri;
|
||||
use libremetaverse_types::{AssetType, FolderType, InventoryType, SaleType, UUID, Vector3};
|
||||
use libremetaverse_types::{
|
||||
AssetType, Color4, ExtraParamType, FolderType, InventoryType, Material, Quaternion, SaleType,
|
||||
SculptType, UUID, Vector3,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read as _;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
@@ -54,6 +72,38 @@ pub(crate) trait GeneratedMessage: Sized {
|
||||
fn deserialize_generated(&mut self, map: &OSDMap) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
pub(crate) fn generated_from_osd<T: GeneratedMessage>(data: OSD) -> Result<T, Error> {
|
||||
let OSD::Map(values) = data else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let map = OSDMap::new_with_dictionary(values)?;
|
||||
let mut value = T::new_generated();
|
||||
value.deserialize_generated(&map)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub(crate) fn get_object_cost_handler(
|
||||
map: &OSDMap,
|
||||
) -> Result<Option<Box<dyn crate::interfaces::IMessage>>, Error> {
|
||||
if map.get("object_ids").is_some() {
|
||||
Ok(Some(Box::new(GetObjectCostRequest::new_generated())))
|
||||
} else {
|
||||
Ok(Some(Box::new(GetObjectCostMessage::new_generated())))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn land_resources_handler(
|
||||
map: &OSDMap,
|
||||
) -> Result<Option<Box<dyn crate::interfaces::IMessage>>, Error> {
|
||||
if map.get("parcel_id").is_some() {
|
||||
Ok(Some(Box::new(LandResourcesRequest::new_generated())))
|
||||
} else if map.get("ScriptResourceSummary").is_some() {
|
||||
Ok(Some(Box::new(LandResourcesMessage::new_generated())))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! integer_value {
|
||||
($type:ty, $from:ident, $as:ident) => {
|
||||
impl MessageValue for $type {
|
||||
@@ -232,6 +282,24 @@ impl MessageValue for OSDMap {
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for HashMap<String, i32> {
|
||||
fn message_default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.iter()
|
||||
.map(|(key, value)| Ok((key.clone(), value.to_message_osd()?)))
|
||||
.collect::<Result<HashMap<_, _>, Error>>()
|
||||
.map(OSD::Map)
|
||||
}
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
as_map(value)?
|
||||
.iter()
|
||||
.map(|(key, value)| Ok((key.clone(), i32::from_message_osd(value)?)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for AgentDisplayName {
|
||||
fn message_default() -> Self {
|
||||
Self::new().expect("AgentDisplayName construction is infallible")
|
||||
@@ -813,6 +881,50 @@ fn deserialize_get_object_cost(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_base_resources(value: &BaseResourcesInfo) -> Result<OSDMap, Error> {
|
||||
fn entries(values: &HashMap<String, i32>) -> Result<OSD, Error> {
|
||||
let mut values: Vec<_> = values.iter().collect();
|
||||
values.sort_unstable_by(|left, right| left.0.cmp(right.0));
|
||||
values
|
||||
.into_iter()
|
||||
.map(|(type_, amount)| {
|
||||
Ok(map_osd([
|
||||
("type", type_.to_message_osd()?),
|
||||
("amount", amount.to_message_osd()?),
|
||||
]))
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error>>()
|
||||
.map(OSD::Array)
|
||||
}
|
||||
finish_map([(
|
||||
"summary",
|
||||
map_osd([
|
||||
("available", entries(&value.summary_available)?),
|
||||
("used", entries(&value.summary_used)?),
|
||||
]),
|
||||
)])
|
||||
}
|
||||
|
||||
fn deserialize_base_resources(value: &mut BaseResourcesInfo, map: &OSDMap) -> Result<(), Error> {
|
||||
fn entries(value: &OSD) -> Result<HashMap<String, i32>, Error> {
|
||||
as_array(value)?
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let entry = as_map(entry)?;
|
||||
Ok((
|
||||
String::from_message_osd(required_from(entry, "type")?)?,
|
||||
i32::from_message_osd(required_from(entry, "amount")?)?,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
let summary_value = required(map, "summary")?;
|
||||
let summary = as_map(&summary_value)?;
|
||||
value.summary_available = entries(required_from(summary, "available")?)?;
|
||||
value.summary_used = entries(required_from(summary, "used")?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_nav_mesh_status(value: &NavMeshStatusUpdateMessage) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("region_id", value.region_id.to_message_osd()?),
|
||||
@@ -1172,6 +1284,50 @@ macro_rules! custom_message {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! wrapped_message {
|
||||
($type:ty) => {
|
||||
impl GeneratedMessage for $type {
|
||||
fn new_generated() -> Self {
|
||||
Self {
|
||||
request: MessageValue::message_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_generated(&self) -> Result<OSDMap, Error> {
|
||||
let OSD::Map(values) = self.request.to_message_osd()? else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
OSDMap::new_with_dictionary(values)
|
||||
}
|
||||
|
||||
fn deserialize_generated(&mut self, map: &OSDMap) -> Result<(), Error> {
|
||||
self.request = MessageValue::from_message_osd(&map.copy()?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageValue for $type {
|
||||
fn message_default() -> Self {
|
||||
<Self as GeneratedMessage>::new_generated()
|
||||
}
|
||||
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> {
|
||||
self.serialize_generated()?.copy()
|
||||
}
|
||||
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let map = OSDMap::new_with_dictionary(values.clone())?;
|
||||
let mut output = <Self as GeneratedMessage>::new_generated();
|
||||
output.deserialize_generated(&map)?;
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! osd_entries {
|
||||
($value:expr; $($field:ident => $key:literal),* $(,)?) => {
|
||||
vec![$(($key, $value.$field.to_message_osd()?)),*]
|
||||
@@ -1237,11 +1393,6 @@ custom_message!(AgentGroupDataUpdateMessage {
|
||||
array: [group_data_block, new_group_data_block],
|
||||
bytes: []
|
||||
} => serialize_agent_group, deserialize_agent_group);
|
||||
map_message!(ChatSessionRequestBlock {
|
||||
scalar: [method => "method"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
custom_message!(ChatSessionAcceptInvitation { scalar: [base, session_id], array: [], bytes: [] } => serialize_chat_accept, deserialize_chat_accept, init |value: &mut ChatSessionAcceptInvitation| value.base.method = "accept invitation".into());
|
||||
custom_message!(ChatSessionRequestMuteUpdate { scalar: [base, agent_id, request_key, request_value, session_id], array: [], bytes: [] } => serialize_chat_mute, deserialize_chat_mute, init |value: &mut ChatSessionRequestMuteUpdate| value.base.method = "mute update".into());
|
||||
custom_message!(ChatSessionRequestStartConference { scalar: [base, session_id], array: [agents_block], bytes: [] } => serialize_chat_start, deserialize_chat_start, init |value: &mut ChatSessionRequestStartConference| value.base.method = "start conference".into());
|
||||
@@ -1372,11 +1523,6 @@ custom_message!(LandStatReplyMessage {
|
||||
array: [report_data_blocks],
|
||||
bytes: []
|
||||
} => serialize_land_stat, deserialize_land_stat);
|
||||
map_message!(MapLayerMessageBase {
|
||||
scalar: [flags => "Flags"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
map_message!(MapLayerReplyVariantLayerData {
|
||||
scalar: [
|
||||
bottom => "Bottom",
|
||||
@@ -1582,6 +1728,11 @@ custom_message!(GetObjectCostMessage {
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_get_object_cost, deserialize_get_object_cost);
|
||||
custom_message!(BaseResourcesInfo {
|
||||
scalar: [summary_available, summary_used],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_base_resources, deserialize_base_resources);
|
||||
map_message!(GetObjectCostRequest {
|
||||
scalar: [],
|
||||
array: [object_i_ds => "object_ids"],
|
||||
@@ -1793,6 +1944,229 @@ custom_message!(UploaderRequestComplete {
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_uploader_complete, deserialize_uploader_complete);
|
||||
custom_message!(NewFileAgentInventoryMessage {
|
||||
scalar: [asset_type, description, everyone_mask, folder_id, group_mask, inventory_type, name, next_owner_mask],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_new_file_inventory, deserialize_new_file_inventory);
|
||||
custom_message!(NewFileAgentInventoryVariablePriceMessage {
|
||||
scalar: [asset_type, description, everyone_mask, folder_id, group_mask, inventory_type, name, next_owner_mask],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_new_file_inventory_variable, deserialize_new_file_inventory_variable);
|
||||
map_message!(NewFileAgentInventoryReplyMessage {
|
||||
scalar: [state => "state", uploader => "uploader"],
|
||||
array: [],
|
||||
bytes: []
|
||||
} init |value: &mut NewFileAgentInventoryReplyMessage| value.state = "upload".into());
|
||||
map_message!(NewFileAgentInventoryVariablePriceReplyMessage {
|
||||
scalar: [resource_cost => "resource_cost", rsvp => "rsvp", state => "state", upload_price => "upload_price"],
|
||||
array: [],
|
||||
bytes: []
|
||||
} init |value: &mut NewFileAgentInventoryVariablePriceReplyMessage| value.state = "confirm_upload".into());
|
||||
map_message!(NewFileAgentInventoryUploadReplyMessage {
|
||||
scalar: [
|
||||
new_asset => "new_asset",
|
||||
new_base_mask => "new_base_mask",
|
||||
new_everyone_mask => "new_everyone_mask",
|
||||
new_inventory_item => "new_inventory_item",
|
||||
new_next_owner_mask => "new_next_owner_mask",
|
||||
new_owner_mask => "new_owner_mask",
|
||||
state => "state"
|
||||
],
|
||||
array: [],
|
||||
bytes: []
|
||||
} init |value: &mut NewFileAgentInventoryUploadReplyMessage| value.state = "complete".into());
|
||||
map_message!(UpdateNotecardTaskInventoryMessage {
|
||||
scalar: [item_id => "item_id", task_id => "task_id"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
custom_message!(UploaderRequestScriptComplete {
|
||||
scalar: [asset_id, compiled],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_uploader_script_complete, deserialize_uploader_script_complete);
|
||||
custom_message!(UploaderScriptRequestError {
|
||||
scalar: [asset_id, compiled, error],
|
||||
array: [],
|
||||
bytes: []
|
||||
} => serialize_uploader_script_error, deserialize_uploader_script_error);
|
||||
map_message!(RemoteParcelRequestRequest {
|
||||
scalar: [location => "location", region_handle => "region_handle", region_id => "region_id"],
|
||||
array: [],
|
||||
bytes: []
|
||||
});
|
||||
wrapped_message!(ChatSessionRequestMessage);
|
||||
wrapped_message!(MapLayerMessage);
|
||||
wrapped_message!(ObjectMediaMessage);
|
||||
wrapped_message!(RemoteParcelRequestMessage);
|
||||
wrapped_message!(SearchStatRequestMessage);
|
||||
wrapped_message!(UpdateGestureAgentInventoryMessage);
|
||||
wrapped_message!(UpdateNotecardAgentInventoryMessage);
|
||||
wrapped_message!(UpdateScriptAgentMessage);
|
||||
wrapped_message!(UpdateScriptTaskMessage);
|
||||
wrapped_message!(UploadBakedTextureMessage);
|
||||
wrapped_message!(UploadScriptTaskMessage);
|
||||
|
||||
fn serialize_new_file_inventory_fields(
|
||||
asset_type: AssetType,
|
||||
description: &str,
|
||||
everyone_mask: PermissionMask,
|
||||
folder_id: UUID,
|
||||
group_mask: PermissionMask,
|
||||
inventory_type: InventoryType,
|
||||
name: &str,
|
||||
next_owner_mask: PermissionMask,
|
||||
) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("folder_id", folder_id.to_message_osd()?),
|
||||
(
|
||||
"asset_type",
|
||||
libremetaverse_types::Utils::asset_type_to_string(asset_type)?.to_message_osd()?,
|
||||
),
|
||||
(
|
||||
"inventory_type",
|
||||
libremetaverse_types::Utils::inventory_type_to_string(inventory_type)?
|
||||
.to_message_osd()?,
|
||||
),
|
||||
("name", name.to_owned().to_message_osd()?),
|
||||
("description", description.to_owned().to_message_osd()?),
|
||||
("everyone_mask", everyone_mask.to_message_osd()?),
|
||||
("group_mask", group_mask.to_message_osd()?),
|
||||
("next_owner_mask", next_owner_mask.to_message_osd()?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_new_file_inventory_fields(
|
||||
map: &OSDMap,
|
||||
) -> Result<
|
||||
(
|
||||
AssetType,
|
||||
String,
|
||||
PermissionMask,
|
||||
UUID,
|
||||
PermissionMask,
|
||||
InventoryType,
|
||||
String,
|
||||
PermissionMask,
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
Ok((
|
||||
libremetaverse_types::Utils::string_to_asset_type(decode_top(map, "asset_type")?)?,
|
||||
decode_top(map, "description")?,
|
||||
decode_top(map, "everyone_mask")?,
|
||||
decode_top(map, "folder_id")?,
|
||||
decode_top(map, "group_mask")?,
|
||||
libremetaverse_types::Utils::string_to_inventory_type(decode_top(map, "inventory_type")?)?,
|
||||
decode_top(map, "name")?,
|
||||
decode_top(map, "next_owner_mask")?,
|
||||
))
|
||||
}
|
||||
|
||||
fn serialize_new_file_inventory(value: &NewFileAgentInventoryMessage) -> Result<OSDMap, Error> {
|
||||
serialize_new_file_inventory_fields(
|
||||
value.asset_type,
|
||||
&value.description,
|
||||
value.everyone_mask,
|
||||
value.folder_id,
|
||||
value.group_mask,
|
||||
value.inventory_type,
|
||||
&value.name,
|
||||
value.next_owner_mask,
|
||||
)
|
||||
}
|
||||
|
||||
fn deserialize_new_file_inventory(
|
||||
value: &mut NewFileAgentInventoryMessage,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
(
|
||||
value.asset_type,
|
||||
value.description,
|
||||
value.everyone_mask,
|
||||
value.folder_id,
|
||||
value.group_mask,
|
||||
value.inventory_type,
|
||||
value.name,
|
||||
value.next_owner_mask,
|
||||
) = deserialize_new_file_inventory_fields(map)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_new_file_inventory_variable(
|
||||
value: &NewFileAgentInventoryVariablePriceMessage,
|
||||
) -> Result<OSDMap, Error> {
|
||||
serialize_new_file_inventory_fields(
|
||||
value.asset_type,
|
||||
&value.description,
|
||||
value.everyone_mask,
|
||||
value.folder_id,
|
||||
value.group_mask,
|
||||
value.inventory_type,
|
||||
&value.name,
|
||||
value.next_owner_mask,
|
||||
)
|
||||
}
|
||||
|
||||
fn deserialize_new_file_inventory_variable(
|
||||
value: &mut NewFileAgentInventoryVariablePriceMessage,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
(
|
||||
value.asset_type,
|
||||
value.description,
|
||||
value.everyone_mask,
|
||||
value.folder_id,
|
||||
value.group_mask,
|
||||
value.inventory_type,
|
||||
value.name,
|
||||
value.next_owner_mask,
|
||||
) = deserialize_new_file_inventory_fields(map)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_uploader_script_complete(
|
||||
value: &UploaderRequestScriptComplete,
|
||||
) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("state", "complete".to_owned().to_message_osd()?),
|
||||
("new_asset", value.asset_id.to_message_osd()?),
|
||||
("compiled", value.compiled.to_message_osd()?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_uploader_script_complete(
|
||||
value: &mut UploaderRequestScriptComplete,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
value.asset_id = decode_top(map, "new_asset")?;
|
||||
value.compiled = decode_top(map, "compiled")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_uploader_script_error(value: &UploaderScriptRequestError) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("state", "complete".to_owned().to_message_osd()?),
|
||||
("new_asset", value.asset_id.to_message_osd()?),
|
||||
("compiled", value.compiled.to_message_osd()?),
|
||||
("errors", encode_array(std::slice::from_ref(&value.error))?),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_uploader_script_error(
|
||||
value: &mut UploaderScriptRequestError,
|
||||
map: &OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
value.asset_id = decode_top(map, "new_asset")?;
|
||||
value.compiled = decode_top(map, "compiled")?;
|
||||
value.error = decode_array::<String>(&required(map, "errors")?)?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(Error::Argument)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_agent_group(value: &AgentGroupDataUpdateMessage) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
@@ -2918,6 +3292,329 @@ fn deserialize_viewer_stats(value: &mut ViewerStatsMessage, map: &OSDMap) -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn material_from_i32(value: i32) -> Result<Material, Error> {
|
||||
match value {
|
||||
0 => Ok(Material::Stone),
|
||||
1 => Ok(Material::Metal),
|
||||
2 => Ok(Material::Glass),
|
||||
3 => Ok(Material::Wood),
|
||||
4 => Ok(Material::Flesh),
|
||||
5 => Ok(Material::Plastic),
|
||||
6 => Ok(Material::Rubber),
|
||||
7 => Ok(Material::Light),
|
||||
_ => Err(Error::Argument),
|
||||
}
|
||||
}
|
||||
|
||||
fn sculpt_type_from_i32(value: i32) -> Result<SculptType, Error> {
|
||||
match value {
|
||||
0 => Ok(SculptType::None),
|
||||
1 => Ok(SculptType::Sphere),
|
||||
2 => Ok(SculptType::Torus),
|
||||
3 => Ok(SculptType::Plane),
|
||||
4 => Ok(SculptType::Cylinder),
|
||||
5 => Ok(SculptType::Mesh),
|
||||
64 => Ok(SculptType::Invert),
|
||||
128 => Ok(SculptType::Mirror),
|
||||
_ => Err(Error::Argument),
|
||||
}
|
||||
}
|
||||
|
||||
fn bumpiness_from_i32(value: i32) -> Result<crate::Bumpiness, Error> {
|
||||
use crate::Bumpiness;
|
||||
match value {
|
||||
0 => Ok(Bumpiness::None),
|
||||
1 => Ok(Bumpiness::Brightness),
|
||||
2 => Ok(Bumpiness::Darkness),
|
||||
3 => Ok(Bumpiness::Woodgrain),
|
||||
4 => Ok(Bumpiness::Bark),
|
||||
5 => Ok(Bumpiness::Bricks),
|
||||
6 => Ok(Bumpiness::Checker),
|
||||
7 => Ok(Bumpiness::Concrete),
|
||||
8 => Ok(Bumpiness::Crustytile),
|
||||
9 => Ok(Bumpiness::Cutstone),
|
||||
10 => Ok(Bumpiness::Discs),
|
||||
11 => Ok(Bumpiness::Gravel),
|
||||
12 => Ok(Bumpiness::Petridish),
|
||||
13 => Ok(Bumpiness::Siding),
|
||||
14 => Ok(Bumpiness::Stonetile),
|
||||
15 => Ok(Bumpiness::Stucco),
|
||||
16 => Ok(Bumpiness::Suction),
|
||||
17 => Ok(Bumpiness::Weave),
|
||||
_ => Err(Error::Argument),
|
||||
}
|
||||
}
|
||||
|
||||
impl GeneratedMessage for UploadObjectAssetMessageObjectFace {
|
||||
fn new_generated() -> Self {
|
||||
Self {
|
||||
bump: crate::Bumpiness::None,
|
||||
color: Color4::default(),
|
||||
fullbright: false,
|
||||
glow: 0.0,
|
||||
image_id: UUID::zero(),
|
||||
image_rot: 0.0,
|
||||
media_flags: 0,
|
||||
offset_s: 0.0,
|
||||
offset_t: 0.0,
|
||||
scale_s: 0.0,
|
||||
scale_t: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_generated(&self) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("bump", OSD::Integer(self.bump as i32)),
|
||||
("colors", OSD::from_color4(self.color)?),
|
||||
("fullbright", OSD::Boolean(self.fullbright)),
|
||||
("glow", OSD::Real(f64::from(self.glow))),
|
||||
("imageid", OSD::UUID(self.image_id)),
|
||||
("imagerot", OSD::Real(f64::from(self.image_rot))),
|
||||
("media_flags", OSD::Integer(self.media_flags)),
|
||||
("offsets", OSD::Real(f64::from(self.offset_s))),
|
||||
("offsett", OSD::Real(f64::from(self.offset_t))),
|
||||
("scales", OSD::Real(f64::from(self.scale_s))),
|
||||
("scalet", OSD::Real(f64::from(self.scale_t))),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_generated(&mut self, map: &OSDMap) -> Result<(), Error> {
|
||||
self.bump = bumpiness_from_i32(required(map, "bump")?.as_integer()?)?;
|
||||
self.color = required(map, "colors")?.as_color4()?;
|
||||
self.fullbright = required(map, "fullbright")?.as_boolean()?;
|
||||
self.glow = required(map, "glow")?.as_real()? as f32;
|
||||
self.image_id = required(map, "imageid")?.as_uuid()?;
|
||||
self.image_rot = required(map, "imagerot")?.as_real()? as f32;
|
||||
self.media_flags = required(map, "media_flags")?.as_integer()?;
|
||||
self.offset_s = required(map, "offsets")?.as_real()? as f32;
|
||||
self.offset_t = required(map, "offsett")?.as_real()? as f32;
|
||||
self.scale_s = required(map, "scales")?.as_real()? as f32;
|
||||
self.scale_t = required(map, "scalet")?.as_real()? as f32;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl GeneratedMessage for UploadObjectAssetMessageObjectExtraParam {
|
||||
fn new_generated() -> Self {
|
||||
Self {
|
||||
extra_param_data: Vec::new(),
|
||||
type_: ExtraParamType(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_generated(&self) -> Result<OSDMap, Error> {
|
||||
finish_map([
|
||||
("extra_parameter", OSD::Integer(i32::from(self.type_.0))),
|
||||
("param_data", OSD::Binary(self.extra_param_data.clone())),
|
||||
])
|
||||
}
|
||||
|
||||
fn deserialize_generated(&mut self, map: &OSDMap) -> Result<(), Error> {
|
||||
self.type_ = ExtraParamType(
|
||||
u16::try_from(required(map, "extra_parameter")?.as_integer()?)
|
||||
.map_err(|_| Error::Argument)?,
|
||||
);
|
||||
self.extra_param_data = required(map, "param_data")?.as_binary()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_object_default() -> UploadObjectAssetMessageObject {
|
||||
UploadObjectAssetMessageObject {
|
||||
extra_params: Vec::new(),
|
||||
faces: Vec::new(),
|
||||
group_id: UUID::zero(),
|
||||
material: Material::Stone,
|
||||
name: String::new(),
|
||||
path_begin: 0.0,
|
||||
path_curve: 0,
|
||||
path_end: 0.0,
|
||||
position: Vector3::zero(),
|
||||
profile_begin: 0.0,
|
||||
profile_curve: 0,
|
||||
profile_end: 0.0,
|
||||
profile_hollow: 0.0,
|
||||
radius_offset: 0.0,
|
||||
revolutions: 0.0,
|
||||
rotation: Quaternion::identity(),
|
||||
scale: Vector3::zero(),
|
||||
scale_x: 0.0,
|
||||
scale_y: 0.0,
|
||||
sculpt_id: UUID::zero(),
|
||||
sculpt_type: SculptType::None,
|
||||
shear_x: 0.0,
|
||||
shear_y: 0.0,
|
||||
skew: 0.0,
|
||||
taper_x: 0.0,
|
||||
taper_y: 0.0,
|
||||
twist: 0.0,
|
||||
twist_begin: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
impl GeneratedMessage for UploadObjectAssetMessageObject {
|
||||
fn new_generated() -> Self {
|
||||
upload_object_default()
|
||||
}
|
||||
|
||||
fn serialize_generated(&self) -> Result<OSDMap, Error> {
|
||||
let encode = |values: &[&dyn GeneratedMessageObject]| -> Result<OSD, Error> {
|
||||
Ok(OSD::Array(
|
||||
values
|
||||
.iter()
|
||||
.map(|value| value.encode())
|
||||
.collect::<Result<_, _>>()?,
|
||||
))
|
||||
};
|
||||
let extras: Vec<&dyn GeneratedMessageObject> =
|
||||
self.extra_params.iter().map(|v| v as _).collect();
|
||||
let faces: Vec<&dyn GeneratedMessageObject> = self.faces.iter().map(|v| v as _).collect();
|
||||
let shape = map_osd([
|
||||
(
|
||||
"path",
|
||||
map_osd([
|
||||
("begin", OSD::Real(f64::from(self.path_begin))),
|
||||
("curve", OSD::Integer(self.path_curve)),
|
||||
("end", OSD::Real(f64::from(self.path_end))),
|
||||
("radius_offset", OSD::Real(f64::from(self.radius_offset))),
|
||||
("revolutions", OSD::Real(f64::from(self.revolutions))),
|
||||
("scale_x", OSD::Real(f64::from(self.scale_x))),
|
||||
("scale_y", OSD::Real(f64::from(self.scale_y))),
|
||||
("shear_x", OSD::Real(f64::from(self.shear_x))),
|
||||
("shear_y", OSD::Real(f64::from(self.shear_y))),
|
||||
("skew", OSD::Real(f64::from(self.skew))),
|
||||
("taper_x", OSD::Real(f64::from(self.taper_x))),
|
||||
("taper_y", OSD::Real(f64::from(self.taper_y))),
|
||||
("twist", OSD::Real(f64::from(self.twist))),
|
||||
("twist_begin", OSD::Real(f64::from(self.twist_begin))),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"profile",
|
||||
map_osd([
|
||||
("begin", OSD::Real(f64::from(self.profile_begin))),
|
||||
("curve", OSD::Integer(self.profile_curve)),
|
||||
("end", OSD::Real(f64::from(self.profile_end))),
|
||||
("hollow", OSD::Real(f64::from(self.profile_hollow))),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"sculpt",
|
||||
map_osd([
|
||||
("id", OSD::UUID(self.sculpt_id)),
|
||||
("type", OSD::Integer(self.sculpt_type as i32)),
|
||||
]),
|
||||
),
|
||||
]);
|
||||
finish_map([
|
||||
("group-id", OSD::UUID(self.group_id)),
|
||||
("material", OSD::Integer(self.material as i32)),
|
||||
("name", OSD::String(self.name.clone())),
|
||||
("pos", OSD::from_vector3(self.position)?),
|
||||
("rotation", OSD::from_quaternion(self.rotation)?),
|
||||
("scale", OSD::from_vector3(self.scale)?),
|
||||
("extra_parameters", encode(&extras)?),
|
||||
("facelist", encode(&faces)?),
|
||||
("shape", shape),
|
||||
])
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn deserialize_generated(&mut self, map: &OSDMap) -> Result<(), Error> {
|
||||
self.group_id = required(map, "group-id")?.as_uuid()?;
|
||||
self.material = material_from_i32(required(map, "material")?.as_integer()?)?;
|
||||
self.name = required(map, "name")?.as_string()?;
|
||||
self.position = required(map, "pos")?.as_vector3()?;
|
||||
self.rotation = required(map, "rotation")?.as_quaternion()?;
|
||||
self.scale = required(map, "scale")?.as_vector3()?;
|
||||
self.extra_params = decode_array(&required(map, "extra_parameters")?)?;
|
||||
self.faces = decode_array(&required(map, "facelist")?)?;
|
||||
let shape_value = required(map, "shape")?;
|
||||
let shape = as_map(&shape_value)?;
|
||||
let path = as_map(required_from(shape, "path")?)?;
|
||||
macro_rules! real {
|
||||
($field:ident, $key:literal) => {
|
||||
self.$field = required_from(path, $key)?.as_real()? as f32;
|
||||
};
|
||||
}
|
||||
real!(path_begin, "begin");
|
||||
self.path_curve = required_from(path, "curve")?.as_integer()?;
|
||||
real!(path_end, "end");
|
||||
real!(radius_offset, "radius_offset");
|
||||
real!(revolutions, "revolutions");
|
||||
real!(scale_x, "scale_x");
|
||||
real!(scale_y, "scale_y");
|
||||
real!(shear_x, "shear_x");
|
||||
real!(shear_y, "shear_y");
|
||||
real!(skew, "skew");
|
||||
real!(taper_x, "taper_x");
|
||||
real!(taper_y, "taper_y");
|
||||
real!(twist, "twist");
|
||||
real!(twist_begin, "twist_begin");
|
||||
let profile = as_map(required_from(shape, "profile")?)?;
|
||||
self.profile_begin = required_from(profile, "begin")?.as_real()? as f32;
|
||||
self.profile_curve = required_from(profile, "curve")?.as_integer()?;
|
||||
self.profile_end = required_from(profile, "end")?.as_real()? as f32;
|
||||
self.profile_hollow = required_from(profile, "hollow")?.as_real()? as f32;
|
||||
let sculpt = as_map(required_from(shape, "sculpt")?)?;
|
||||
self.sculpt_id = required_from(sculpt, "id")?.as_uuid()?;
|
||||
self.sculpt_type = sculpt_type_from_i32(required_from(sculpt, "type")?.as_integer()?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
trait GeneratedMessageObject {
|
||||
fn encode(&self) -> Result<OSD, Error>;
|
||||
}
|
||||
impl GeneratedMessageObject for UploadObjectAssetMessageObjectFace {
|
||||
fn encode(&self) -> Result<OSD, Error> {
|
||||
self.serialize_generated()?.copy()
|
||||
}
|
||||
}
|
||||
impl GeneratedMessageObject for UploadObjectAssetMessageObjectExtraParam {
|
||||
fn encode(&self) -> Result<OSD, Error> {
|
||||
self.serialize_generated()?.copy()
|
||||
}
|
||||
}
|
||||
|
||||
impl GeneratedMessage for UploadObjectAssetMessage {
|
||||
fn new_generated() -> Self {
|
||||
Self {
|
||||
objects: Vec::new(),
|
||||
}
|
||||
}
|
||||
fn serialize_generated(&self) -> Result<OSDMap, Error> {
|
||||
finish_map([("objects", encode_struct_array(&self.objects)?)])
|
||||
}
|
||||
fn deserialize_generated(&mut self, map: &OSDMap) -> Result<(), Error> {
|
||||
self.objects = decode_array(&required(map, "objects")?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! upload_message_value {
|
||||
($($type:ty),+ $(,)?) => {
|
||||
$(impl MessageValue for $type {
|
||||
fn message_default() -> Self { <Self as GeneratedMessage>::new_generated() }
|
||||
fn to_message_osd(&self) -> Result<OSD, Error> { self.serialize_generated()?.copy() }
|
||||
fn from_message_osd(value: &OSD) -> Result<Self, Error> {
|
||||
let OSD::Map(values) = value else { return Err(Error::Argument); };
|
||||
let map = OSDMap::new_with_dictionary(values.clone())?;
|
||||
let mut output = <Self as GeneratedMessage>::new_generated();
|
||||
output.deserialize_generated(&map)?;
|
||||
Ok(output)
|
||||
}
|
||||
})+
|
||||
};
|
||||
}
|
||||
|
||||
upload_message_value!(
|
||||
UploadObjectAssetMessage,
|
||||
UploadObjectAssetMessageObject,
|
||||
UploadObjectAssetMessageObjectExtraParam,
|
||||
UploadObjectAssetMessageObjectFace,
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -878,6 +878,7 @@ pub(crate) fn from_dictionary_uri(dict: HashMap<Uri, Uri>) -> Result<OSDMap, Err
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::too_many_lines)] // The exhaustive decoder matrix stays in one test.
|
||||
mod tests {
|
||||
use super::*;
|
||||
use flate2::Compression;
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//! Protocol name/value parsing used by object and avatar updates.
|
||||
|
||||
use crate::{Error, NameValueClassType, NameValueSendtoType, NameValueValueType};
|
||||
use libremetaverse_types::compat::Object;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct NameValue {
|
||||
pub class_: NameValueClassType,
|
||||
pub name: String,
|
||||
pub sendto: NameValueSendtoType,
|
||||
pub type_: NameValueValueType,
|
||||
pub value: Option<Object>,
|
||||
}
|
||||
|
||||
impl NameValue {
|
||||
pub fn new_with_string(data: String) -> Result<Self, Error> {
|
||||
let mut fields = data.splitn(5, char::is_whitespace);
|
||||
let Some(name) = fields.next().filter(|value| !value.is_empty()) else {
|
||||
return Ok(Self::empty());
|
||||
};
|
||||
let type_name = fields.next().unwrap_or_default();
|
||||
let class_name = fields.next().unwrap_or_default();
|
||||
let sendto_name = fields.next().unwrap_or_default();
|
||||
let value_text = fields.next().unwrap_or_default().trim_start();
|
||||
|
||||
let type_ = value_type(type_name);
|
||||
let class_ = class_type(class_name);
|
||||
let sendto = sendto_type(sendto_name);
|
||||
let value = parse_value(type_, value_text);
|
||||
Ok(Self {
|
||||
class_,
|
||||
name: name.to_owned(),
|
||||
sendto,
|
||||
type_,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_string_value_type_class_type_sendto_type_object(
|
||||
name: String,
|
||||
value_type: NameValueValueType,
|
||||
class_type: NameValueClassType,
|
||||
sendto_type: NameValueSendtoType,
|
||||
value: Object,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
class_: class_type,
|
||||
name,
|
||||
sendto: sendto_type,
|
||||
type_: value_type,
|
||||
value: Some(value),
|
||||
})
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
Self {
|
||||
class_: NameValueClassType::Unknown,
|
||||
name: String::new(),
|
||||
sendto: NameValueSendtoType::Unknown,
|
||||
type_: NameValueValueType::Unknown,
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn equals_with_name_value(&self, other: Self) -> bool {
|
||||
self == &other
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn equals_with_object(&self, object: Option<Object>) -> bool {
|
||||
object
|
||||
.as_ref()
|
||||
.and_then(Object::downcast_ref::<Self>)
|
||||
.is_some_and(|other| self == other)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_hash_code(&self) -> i32 {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
self.hash(&mut hasher);
|
||||
hasher.finish() as i32
|
||||
}
|
||||
|
||||
pub fn name_values_to_string(values: Vec<Self>) -> Result<String, Error> {
|
||||
let mut rendered = Vec::new();
|
||||
for value in values {
|
||||
let Some(payload) = value.value.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
rendered.push(format!(
|
||||
"{} {} {} {} {}",
|
||||
value.name,
|
||||
type_string(value.type_).ok_or(Error::IndexOutOfRange)?,
|
||||
class_string(value.class_).ok_or(Error::IndexOutOfRange)?,
|
||||
sendto_string(value.sendto).ok_or(Error::IndexOutOfRange)?,
|
||||
object_string(payload)
|
||||
));
|
||||
}
|
||||
Ok(rendered.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
fn value_type(value: &str) -> NameValueValueType {
|
||||
match value {
|
||||
"STRING" => NameValueValueType::String,
|
||||
"F32" => NameValueValueType::F32,
|
||||
"S32" => NameValueValueType::S32,
|
||||
"VEC3" => NameValueValueType::VEC3,
|
||||
"U32" => NameValueValueType::U32,
|
||||
"ASSET" => NameValueValueType::Asset,
|
||||
"U64" => NameValueValueType::U64,
|
||||
_ => NameValueValueType::String,
|
||||
}
|
||||
}
|
||||
|
||||
fn class_type(value: &str) -> NameValueClassType {
|
||||
match value {
|
||||
"R" => NameValueClassType::ReadOnly,
|
||||
"RW" => NameValueClassType::ReadWrite,
|
||||
"CB" => NameValueClassType::Callback,
|
||||
_ => NameValueClassType::ReadOnly,
|
||||
}
|
||||
}
|
||||
|
||||
fn sendto_type(value: &str) -> NameValueSendtoType {
|
||||
match value {
|
||||
"S" => NameValueSendtoType::Sim,
|
||||
"DS" => NameValueSendtoType::DataSim,
|
||||
"SV" => NameValueSendtoType::SimViewer,
|
||||
"DSV" => NameValueSendtoType::DataSimViewer,
|
||||
_ => NameValueSendtoType::Sim,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_value(type_: NameValueValueType, value: &str) -> Option<Object> {
|
||||
Some(match type_ {
|
||||
NameValueValueType::String | NameValueValueType::Asset => Object::String(value.to_owned()),
|
||||
NameValueValueType::F32 => Object::Real(value.parse::<f32>().unwrap_or_default().into()),
|
||||
NameValueValueType::S32 => Object::Integer(value.parse().unwrap_or_default()),
|
||||
NameValueValueType::U32 => Object::UInteger(value.parse().unwrap_or_default()),
|
||||
NameValueValueType::U64 => Object::ULong(value.parse().unwrap_or_default()),
|
||||
NameValueValueType::VEC3 => Object::Vector3(
|
||||
libremetaverse_types::Vector3::parse(value.to_owned())
|
||||
.unwrap_or_else(|_| libremetaverse_types::Vector3::zero()),
|
||||
),
|
||||
NameValueValueType::Unknown | NameValueValueType::CAMERA => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn type_string(value: NameValueValueType) -> Option<&'static str> {
|
||||
match value {
|
||||
NameValueValueType::String => Some("STRING"),
|
||||
NameValueValueType::F32 => Some("F32"),
|
||||
NameValueValueType::S32 => Some("S32"),
|
||||
NameValueValueType::VEC3 => Some("VEC3"),
|
||||
NameValueValueType::U32 => Some("U32"),
|
||||
NameValueValueType::Asset => Some("ASSET"),
|
||||
NameValueValueType::U64 => Some("U64"),
|
||||
NameValueValueType::Unknown | NameValueValueType::CAMERA => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn class_string(value: NameValueClassType) -> Option<&'static str> {
|
||||
match value {
|
||||
NameValueClassType::ReadOnly => Some("R"),
|
||||
NameValueClassType::ReadWrite => Some("RW"),
|
||||
NameValueClassType::Callback => Some("CB"),
|
||||
NameValueClassType::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sendto_string(value: NameValueSendtoType) -> Option<&'static str> {
|
||||
match value {
|
||||
NameValueSendtoType::Sim => Some("S"),
|
||||
NameValueSendtoType::DataSim => Some("DS"),
|
||||
NameValueSendtoType::SimViewer => Some("SV"),
|
||||
NameValueSendtoType::DataSimViewer => Some("DSV"),
|
||||
NameValueSendtoType::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn object_string(value: &Object) -> String {
|
||||
match value {
|
||||
Object::Undefined => String::new(),
|
||||
Object::Boolean(value) => value.to_string(),
|
||||
Object::Integer(value) => value.to_string(),
|
||||
Object::UInteger(value) => value.to_string(),
|
||||
Object::Long(value) => value.to_string(),
|
||||
Object::ULong(value) => value.to_string(),
|
||||
Object::Real(value) => value.to_string(),
|
||||
Object::String(value) => value.clone(),
|
||||
Object::UUID(value) => value.to_string(),
|
||||
Object::Vector3(value) => value.to_string(),
|
||||
other => format!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_and_round_trips_protocol_name_values() {
|
||||
let value = NameValue::new_with_string("FirstName STRING R S Ada".into()).unwrap();
|
||||
assert_eq!(value.name, "FirstName");
|
||||
assert_eq!(value.type_, NameValueValueType::String);
|
||||
assert_eq!(value.value, Some(Object::String("Ada".into())));
|
||||
assert_eq!(
|
||||
NameValue::name_values_to_string(vec![value]).unwrap(),
|
||||
"FirstName STRING R S Ada"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
//! after the lock is released.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility map.
|
||||
#![allow(clippy::large_enum_variant)] // Commands move through a bounded channel without cloning.
|
||||
#![allow(clippy::needless_pass_by_value)] // Owned mapped parameters preserve the public API.
|
||||
#![allow(clippy::too_many_arguments)] // Login overloads preserve the mapped public API.
|
||||
#![allow(clippy::too_many_lines)] // Packet dispatch keeps the complete protocol switch together.
|
||||
@@ -51,6 +52,8 @@ impl Clone for Packet {
|
||||
has_variable_blocks: self.has_variable_blocks,
|
||||
header: self.header.clone(),
|
||||
type_: self.type_,
|
||||
native_body: self.native_body.clone(),
|
||||
native_payload_length: self.native_payload_length,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1055,6 +1058,7 @@ pub struct SimulatorData {
|
||||
pub terrain_start_height11: f32,
|
||||
pub water_height: f32,
|
||||
pub wind_speeds: RwLock<Option<Vec<Vector2>>>,
|
||||
prey_id: RwLock<UUID>,
|
||||
endpoint: std::net::SocketAddr,
|
||||
connected: AtomicBool,
|
||||
movement_complete: AtomicBool,
|
||||
@@ -1151,6 +1155,14 @@ impl fmt::Debug for Simulator {
|
||||
}
|
||||
|
||||
impl Simulator {
|
||||
pub(crate) fn native_prey_id(&self) -> UUID {
|
||||
*read(&self.data.prey_id)
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_prey_id(&self, value: UUID) {
|
||||
*write(&self.data.prey_id) = value;
|
||||
}
|
||||
|
||||
pub fn native_new(
|
||||
client: GridClient,
|
||||
address: std::net::SocketAddr,
|
||||
@@ -1221,6 +1233,7 @@ impl Simulator {
|
||||
terrain_start_height11: 0.0,
|
||||
water_height: 0.0,
|
||||
wind_speeds: RwLock::new(None),
|
||||
prey_id: RwLock::new(UUID::zero()),
|
||||
endpoint: address,
|
||||
connected: AtomicBool::new(false),
|
||||
movement_complete: AtomicBool::new(false),
|
||||
|
||||
@@ -972,6 +972,12 @@ impl ObjectManagerInner {
|
||||
};
|
||||
|
||||
if construction.p_code == PCode::Avatar {
|
||||
if let Some(client) = self.client.upgrade()
|
||||
&& block.full_id == client.network().native_agent_id()
|
||||
&& let Some(agent) = client.cached_agent_manager_inner()
|
||||
{
|
||||
agent.update_avatar_object_state(block.id, movement.collision_plane);
|
||||
}
|
||||
let is_new = !read(&simulator.objects_avatars).contains_key(&block.id);
|
||||
let avatar = read(&simulator.objects_avatars)
|
||||
.get(&block.id)
|
||||
@@ -1214,6 +1220,12 @@ impl ObjectManagerInner {
|
||||
velocity,
|
||||
};
|
||||
if avatar {
|
||||
if let Some(client) = self.client.upgrade()
|
||||
&& let Some(agent) = client.cached_agent_manager_inner()
|
||||
&& local_id == agent.avatar_local_id()
|
||||
{
|
||||
agent.update_avatar_object_state(local_id, collision_plane);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let Some(mut prim) = read(&simulator.objects_primitives).get(&local_id).cloned() else {
|
||||
|
||||
@@ -1711,9 +1711,6 @@ fn te_offset_float_slice(bytes: &[u8]) -> Result<f32, Error> {
|
||||
bytes.try_into().map_err(|_| Error::IndexOutOfRange)?,
|
||||
)) / 32767.0)
|
||||
}
|
||||
pub(crate) fn te_offset_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
te_offset_float_slice(checked_window(&bytes, pos, 2)?)
|
||||
}
|
||||
pub(crate) fn te_rotation_short(rotation: f32) -> i16 {
|
||||
let two_pi = f64::from(std::f32::consts::TAU);
|
||||
let rotation = f64::from(rotation);
|
||||
@@ -1726,20 +1723,9 @@ fn te_rotation_float_slice(bytes: &[u8]) -> Result<f32, Error> {
|
||||
)) / 32768.0
|
||||
* std::f32::consts::TAU)
|
||||
}
|
||||
pub(crate) fn te_rotation_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
te_rotation_float_slice(checked_window(&bytes, pos, 2)?)
|
||||
}
|
||||
pub(crate) fn te_glow_byte(glow: f32) -> u8 {
|
||||
(glow.clamp(0.0, 1.0) * 255.0) as u8
|
||||
}
|
||||
pub(crate) fn te_glow_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
Ok(f32::from(
|
||||
*checked_window(&bytes, pos, 1)?
|
||||
.first()
|
||||
.ok_or(Error::IndexOutOfRange)?,
|
||||
) / 255.0)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PrimitiveParticleSystem {
|
||||
pub angular_velocity: Vector3,
|
||||
@@ -3193,6 +3179,7 @@ const fn attachment_point_from_u8(value: u8) -> AttachmentPoint {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::float_cmp)] // Texture fixtures use exactly representable values.
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
//! Thread-safe observable key/value storage.
|
||||
|
||||
use crate::{DictionaryEventAction, Error};
|
||||
use libremetaverse_types::compat::{DictionaryEntry, IEnumerator, Object};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
type Callback = Arc<dyn Fn(DictionaryEventAction, DictionaryEntry) + Send + Sync>;
|
||||
|
||||
pub struct ObservableDictionary<TKey, TValue> {
|
||||
values: Mutex<HashMap<TKey, Option<TValue>>>,
|
||||
delegates: Mutex<HashMap<DictionaryEventAction, Vec<Callback>>>,
|
||||
}
|
||||
|
||||
impl<TKey, TValue> ObservableDictionary<TKey, TValue>
|
||||
where
|
||||
TKey: Clone + Eq + Hash + Into<Object>,
|
||||
TValue: Clone + PartialEq + Into<Object>,
|
||||
{
|
||||
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||
Self::new_with_capacity(0)
|
||||
}
|
||||
|
||||
pub fn new_with_int32(capacity: i32) -> Result<Self, Error> {
|
||||
let capacity = usize::try_from(capacity).map_err(|_| Error::Argument)?;
|
||||
Self::new_with_capacity(capacity)
|
||||
}
|
||||
|
||||
fn new_with_capacity(capacity: usize) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
values: Mutex::new(HashMap::with_capacity(capacity)),
|
||||
delegates: Mutex::new(HashMap::new()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add(&self, key: TKey, value: Option<TValue>) -> Result<(), Error> {
|
||||
self.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.insert(key.clone(), value.clone());
|
||||
self.fire(DictionaryEventAction::Add, key, value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_delegate(
|
||||
&self,
|
||||
action: DictionaryEventAction,
|
||||
callback: Callback,
|
||||
) -> Result<(), Error> {
|
||||
self.delegates
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.entry(action)
|
||||
.or_default()
|
||||
.push(callback);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_delegate(
|
||||
&self,
|
||||
action: DictionaryEventAction,
|
||||
callback: Callback,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(callbacks) = self
|
||||
.delegates
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get_mut(&action)
|
||||
{
|
||||
callbacks.retain(|registered| !Arc::ptr_eq(registered, &callback));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear(&self) -> Result<(), Error> {
|
||||
let removed: Vec<_> = self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.drain()
|
||||
.collect();
|
||||
for (key, value) in removed {
|
||||
self.fire(DictionaryEventAction::Remove, key, value);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, key: TKey) -> Result<bool, Error> {
|
||||
Ok(self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.contains_key(&key))
|
||||
}
|
||||
|
||||
pub fn contains_value(&self, value: Option<TValue>) -> Result<bool, Error> {
|
||||
Ok(self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.values()
|
||||
.any(|candidate| candidate == &value))
|
||||
}
|
||||
|
||||
pub fn find(
|
||||
&self,
|
||||
predicate: Box<dyn Fn(&Option<TValue>) -> bool + Send + Sync>,
|
||||
) -> Result<Option<TValue>, Error> {
|
||||
Ok(self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.values()
|
||||
.find(|value| predicate(value))
|
||||
.cloned()
|
||||
.flatten())
|
||||
}
|
||||
|
||||
pub fn find_all_with_predicate(
|
||||
&self,
|
||||
predicate: Box<dyn Fn(&TKey) -> bool + Send + Sync>,
|
||||
) -> Result<Vec<TKey>, Error> {
|
||||
Ok(self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.keys()
|
||||
.filter(|key| predicate(key))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn find_all_with_predicate_6d98ccd2(
|
||||
&self,
|
||||
predicate: Box<dyn Fn(&Option<TValue>) -> bool + Send + Sync>,
|
||||
) -> Result<Vec<Option<TValue>>, Error> {
|
||||
Ok(self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.values()
|
||||
.filter(|value| predicate(value))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn get_enumerator(&self) -> Result<IEnumerator, Error> {
|
||||
Ok(IEnumerator)
|
||||
}
|
||||
|
||||
pub fn remove(&self, key: TKey) -> Result<bool, Error> {
|
||||
let removed = self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.remove(&key);
|
||||
if let Some(value) = removed {
|
||||
self.fire(DictionaryEventAction::Remove, key, value);
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn try_get_value(&self, key: TKey, value: &mut Option<TValue>) -> bool {
|
||||
let found = self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(&key)
|
||||
.cloned();
|
||||
match found {
|
||||
Some(found) => {
|
||||
*value = found;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn count(&self) -> i32 {
|
||||
i32::try_from(
|
||||
self.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.len(),
|
||||
)
|
||||
.unwrap_or(i32::MAX)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn item(&self, key: Option<TKey>) -> Option<TValue> {
|
||||
let key = key?;
|
||||
self.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(&key)
|
||||
.cloned()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn set_item(&mut self, key: Option<TKey>, value: Option<TValue>) {
|
||||
let Some(key) = key else { return };
|
||||
let action = if self
|
||||
.values
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.insert(key.clone(), value.clone())
|
||||
.is_some()
|
||||
{
|
||||
DictionaryEventAction::Change
|
||||
} else {
|
||||
DictionaryEventAction::Add
|
||||
};
|
||||
self.fire(action, key, value);
|
||||
}
|
||||
|
||||
fn fire(&self, action: DictionaryEventAction, key: TKey, value: Option<TValue>) {
|
||||
let callbacks = self
|
||||
.delegates
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(&action)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let entry = DictionaryEntry(key.into(), value.map_or(Object::Undefined, Into::into));
|
||||
for callback in callbacks {
|
||||
callback(action, DictionaryEntry(entry.0.clone(), entry.1.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
//! Runtime representation for the catalogued abstract packet compatibility type.
|
||||
|
||||
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
||||
use libremetaverse_types::Error;
|
||||
|
||||
use crate::interfaces::IByteBufferPool;
|
||||
use crate::packet_catalog::{self, BlockRepetition, PacketType};
|
||||
use crate::packets::{Header, Packet};
|
||||
|
||||
fn body(packet: &Packet) -> Result<&OSDMap, Error> {
|
||||
packet.native_body.as_ref().ok_or(Error::InvalidOperation)
|
||||
}
|
||||
|
||||
fn assign_body(packet: &mut Packet, body: OSDMap) -> Result<(), Error> {
|
||||
let descriptor =
|
||||
packet_catalog::descriptor_by_type(packet.type_).ok_or(Error::InvalidOperation)?;
|
||||
let validated = packet_catalog::build_packet_from_osd(descriptor.name, &body)?
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
packet.native_payload_length = validated.native_payload_length;
|
||||
packet.native_body = Some(body);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn packet_to_osd(packet: &Packet) -> Result<OSDMap, Error> {
|
||||
Ok(body(packet)?.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn packet_from_osd(packet: &mut Packet, osd: OSDMap) -> Result<(), Error> {
|
||||
assign_body(packet, osd)
|
||||
}
|
||||
|
||||
pub(crate) fn get_llsd(packet: &Packet) -> Result<OSD, Error> {
|
||||
let descriptor =
|
||||
packet_catalog::descriptor_by_type(packet.type_).ok_or(Error::InvalidOperation)?;
|
||||
let mut wrapper = std::collections::HashMap::with_capacity(1);
|
||||
wrapper.insert(
|
||||
descriptor.name.to_owned(),
|
||||
OSD::Map(body(packet)?.snapshot()),
|
||||
);
|
||||
Ok(OSD::Map(wrapper))
|
||||
}
|
||||
|
||||
pub(crate) fn from_llsd(osd: OSD) -> Result<Option<Packet>, Error> {
|
||||
let OSD::Map(wrapper) = osd else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut entries = wrapper.into_iter().collect::<Vec<_>>();
|
||||
entries.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
for (name, value) in entries {
|
||||
if let OSD::Map(values) = value {
|
||||
let body = OSDMap::new_with_dictionary(values)?;
|
||||
return packet_catalog::build_packet_from_osd(&name, &body);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) fn from_binary(binary: Vec<u8>) -> Result<Option<Packet>, Error> {
|
||||
from_llsd(OSDParser::deserialize_llsd_binary_with_bytes(binary)?)
|
||||
}
|
||||
|
||||
pub(crate) fn from_xml_string(xml: String) -> Result<Option<Packet>, Error> {
|
||||
from_llsd(OSDParser::deserialize_llsd_xml_with_string(xml)?)
|
||||
}
|
||||
|
||||
pub(crate) fn to_binary(packet: &Packet) -> Result<Vec<u8>, Error> {
|
||||
OSDParser::serialize_llsd_binary_with_osd(get_llsd(packet)?)
|
||||
}
|
||||
|
||||
pub(crate) fn to_xml_string(packet: &Packet) -> Result<String, Error> {
|
||||
OSDParser::serialize_llsd_xml_string(get_llsd(packet)?)
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes(packet: &Packet) -> Result<Vec<u8>, Error> {
|
||||
packet_catalog::encode_packet_body(
|
||||
packet.type_,
|
||||
crate::packet_wire::clone_header(&packet.header),
|
||||
body(packet)?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes_multiple(packet: &Packet) -> Result<Vec<Vec<u8>>, Error> {
|
||||
packet_catalog::encode_packet_body_multiple(
|
||||
packet.type_,
|
||||
crate::packet_wire::clone_header(&packet.header),
|
||||
body(packet)?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes_pooled(
|
||||
packet: &Packet,
|
||||
pool: &dyn IByteBufferPool,
|
||||
size: &mut i32,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let encoded = to_bytes(packet)?;
|
||||
*size = i32::try_from(encoded.len()).map_err(|_| Error::Argument)?;
|
||||
copy_to_pool(pool, &encoded)
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes_multiple_pooled(
|
||||
packet: &Packet,
|
||||
pool: &dyn IByteBufferPool,
|
||||
sizes: &mut Vec<i32>,
|
||||
) -> Result<Vec<Vec<u8>>, Error> {
|
||||
let encoded = to_bytes_multiple(packet)?;
|
||||
sizes.clear();
|
||||
sizes
|
||||
.try_reserve_exact(encoded.len())
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
let mut output = Vec::new();
|
||||
output
|
||||
.try_reserve_exact(encoded.len())
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
for bytes in encoded {
|
||||
sizes.push(i32::try_from(bytes.len()).map_err(|_| Error::Argument)?);
|
||||
output.push(copy_to_pool(pool, &bytes)?);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn copy_to_pool(pool: &dyn IByteBufferPool, encoded: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let minimum = i32::try_from(encoded.len()).map_err(|_| Error::Argument)?;
|
||||
let mut buffer = pool.lease_bytes(minimum)?;
|
||||
if buffer.len() < encoded.len() {
|
||||
buffer
|
||||
.try_reserve_exact(encoded.len() - buffer.len())
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
buffer.resize(encoded.len(), 0);
|
||||
}
|
||||
buffer[..encoded.len()].copy_from_slice(encoded);
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
pub(crate) fn from_bytes_with_header(
|
||||
packet: &mut Packet,
|
||||
header: Header,
|
||||
bytes: &[u8],
|
||||
position: &mut i32,
|
||||
packet_end: &mut i32,
|
||||
) -> Result<(), Error> {
|
||||
let packet_type = packet_catalog::packet_type(header.id, header.frequency);
|
||||
let native_body = packet_catalog::decode_packet_body(
|
||||
packet_type,
|
||||
crate::packet_wire::clone_header(&header),
|
||||
bytes,
|
||||
position,
|
||||
packet_end,
|
||||
)?;
|
||||
assign_decoded(packet, header, packet_type, native_body)
|
||||
}
|
||||
|
||||
pub(crate) fn from_bytes(
|
||||
packet: &mut Packet,
|
||||
bytes: &[u8],
|
||||
position: &mut i32,
|
||||
packet_end: &mut i32,
|
||||
zero_buffer: Option<&mut [u8]>,
|
||||
) -> Result<(), Error> {
|
||||
let header = crate::packet_wire::decode_header(bytes, position, packet_end)?;
|
||||
let packet_type = packet_catalog::packet_type(header.id, header.frequency);
|
||||
let native_body = if header.zerocoded {
|
||||
let buffer = zero_buffer.ok_or(Error::ArgumentNull)?;
|
||||
let decoded_length = crate::packet_wire::zero_decode(
|
||||
Some(bytes),
|
||||
packet_end.checked_add(1).ok_or(Error::Argument)?,
|
||||
Some(buffer),
|
||||
)?;
|
||||
*packet_end = decoded_length.checked_sub(1).ok_or(Error::Argument)?;
|
||||
let end = usize::try_from(decoded_length).map_err(|_| Error::Argument)?;
|
||||
packet_catalog::decode_packet_body(
|
||||
packet_type,
|
||||
crate::packet_wire::clone_header(&header),
|
||||
&buffer[..end],
|
||||
position,
|
||||
packet_end,
|
||||
)?
|
||||
} else {
|
||||
packet_catalog::decode_packet_body(
|
||||
packet_type,
|
||||
crate::packet_wire::clone_header(&header),
|
||||
bytes,
|
||||
position,
|
||||
packet_end,
|
||||
)?
|
||||
};
|
||||
assign_decoded(packet, header, packet_type, native_body)
|
||||
}
|
||||
|
||||
fn assign_decoded(
|
||||
packet: &mut Packet,
|
||||
header: Header,
|
||||
packet_type: PacketType,
|
||||
native_body: OSDMap,
|
||||
) -> Result<(), Error> {
|
||||
let descriptor =
|
||||
packet_catalog::descriptor_by_type(packet_type).ok_or(Error::InvalidOperation)?;
|
||||
packet.has_variable_blocks = descriptor
|
||||
.blocks
|
||||
.iter()
|
||||
.any(|block| matches!(block.repetition, BlockRepetition::Variable));
|
||||
packet.header = header;
|
||||
packet.type_ = packet_type;
|
||||
packet.native_payload_length =
|
||||
packet_catalog::packet_payload_length(packet_type, &native_body)?;
|
||||
packet.native_body = Some(native_body);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn length(packet: &Packet) -> i32 {
|
||||
let header = i32::try_from(crate::packet_wire::header_length(packet.header.frequency))
|
||||
.unwrap_or(i32::MAX);
|
||||
let acks = packet.header.ack_list.as_ref().map_or(0_i32, |acks| {
|
||||
if acks.is_empty() {
|
||||
0
|
||||
} else {
|
||||
i32::try_from(acks.len())
|
||||
.unwrap_or(i32::MAX)
|
||||
.saturating_mul(4)
|
||||
.saturating_add(1)
|
||||
}
|
||||
});
|
||||
header
|
||||
.saturating_add(packet.native_payload_length)
|
||||
.saturating_add(acks)
|
||||
}
|
||||
|
||||
pub(crate) fn block_from_bytes(
|
||||
data: &mut Vec<u8>,
|
||||
bytes: &[u8],
|
||||
position: &mut i32,
|
||||
) -> Result<(), Error> {
|
||||
let start = usize::try_from(*position).map_err(|_| Error::Argument)?;
|
||||
let source = bytes.get(start..).ok_or(Error::IndexOutOfRange)?;
|
||||
data.clear();
|
||||
data.try_reserve_exact(source.len())
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
data.extend_from_slice(source);
|
||||
*position = i32::try_from(bytes.len()).map_err(|_| Error::Argument)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn block_to_bytes(
|
||||
data: &[u8],
|
||||
bytes: &mut [u8],
|
||||
position: &mut i32,
|
||||
) -> Result<(), Error> {
|
||||
let start = usize::try_from(*position).map_err(|_| Error::Argument)?;
|
||||
let end = start.checked_add(data.len()).ok_or(Error::Argument)?;
|
||||
let destination = bytes.get_mut(start..end).ok_or(Error::IndexOutOfRange)?;
|
||||
destination.copy_from_slice(data);
|
||||
*position = i32::try_from(end).map_err(|_| Error::Argument)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn llsd_packet_envelope_round_trips() {
|
||||
let packet = packet_catalog::build_packet(PacketType::CloseCircuit).unwrap();
|
||||
let xml = to_xml_string(&packet).unwrap();
|
||||
let decoded = from_xml_string(xml).unwrap().unwrap();
|
||||
assert_eq!(decoded.type_, PacketType::CloseCircuit);
|
||||
assert_eq!(decoded.packet_to_osd().unwrap().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base_packet_wire_uses_public_header_state() {
|
||||
let mut packet = packet_catalog::build_packet(PacketType::CloseCircuit).unwrap();
|
||||
packet.header.sequence = 0x0102_0304;
|
||||
packet.header.reliable = false;
|
||||
let bytes = to_bytes(&packet).unwrap();
|
||||
assert_eq!(&bytes[1..5], &[1, 2, 3, 4]);
|
||||
assert_eq!(bytes[0] & crate::Helpers::MSG_RELIABLE, 0);
|
||||
assert_eq!(length(&packet), i32::try_from(bytes.len()).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abstract_block_compatibility_storage_checks_bounds() {
|
||||
let mut data = Vec::new();
|
||||
let mut position = 1;
|
||||
block_from_bytes(&mut data, &[9, 8, 7], &mut position).unwrap();
|
||||
assert_eq!(data, [8, 7]);
|
||||
let mut output = [0_u8; 3];
|
||||
position = 1;
|
||||
block_to_bytes(&data, &mut output, &mut position).unwrap();
|
||||
assert_eq!(output, [0, 8, 7]);
|
||||
assert_eq!(position, 3);
|
||||
}
|
||||
}
|
||||
+16009
-498
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,167 @@
|
||||
//! Deterministic, reflection-free packet and compatibility-object diagnostics.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use libremetaverse_structured_data::OSD;
|
||||
use libremetaverse_types::Error;
|
||||
use libremetaverse_types::compat::Object;
|
||||
|
||||
use crate::packets::{Header, Packet};
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn interpret_options(header: &Header) -> String {
|
||||
format!(
|
||||
"[{} {} {} {}]",
|
||||
if header.appended_acks { "Ack" } else { " " },
|
||||
if header.resent { "Res" } else { " " },
|
||||
if header.reliable { "Rel" } else { " " },
|
||||
if header.zerocoded { "Zer" } else { " " },
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn packet_to_string(packet: &Packet) -> Result<String, Error> {
|
||||
let descriptor =
|
||||
crate::packet_catalog::descriptor_by_type(packet.type_).ok_or(Error::InvalidOperation)?;
|
||||
let mut result = String::new();
|
||||
writeln!(
|
||||
result,
|
||||
"Packet Type: {0} http://lib.openmetaverse.co/wiki/{0} http://wiki.secondlife.com/wiki/{0}",
|
||||
descriptor.name,
|
||||
)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
writeln!(result, "[Packet Header]").map_err(|_| Error::InvalidOperation)?;
|
||||
writeln!(result, "Sequence: {}", packet.header.sequence)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
writeln!(result, " Options: {}", interpret_options(&packet.header))
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
writeln!(result, "\n[Packet Payload]").map_err(|_| Error::InvalidOperation)?;
|
||||
let body = packet.packet_to_osd()?;
|
||||
format_osd_map(&body.snapshot(), 0, &mut result)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(crate) fn message_to_string(message: &Object, recurse_level: i32) -> Result<String, Error> {
|
||||
if recurse_level < 0 || recurse_level > 64 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut result = String::new();
|
||||
format_object(
|
||||
message,
|
||||
usize::try_from(recurse_level).map_err(|_| Error::Argument)?,
|
||||
&mut result,
|
||||
)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn format_object(value: &Object, depth: usize, output: &mut String) -> Result<(), Error> {
|
||||
let indent = " ".repeat(depth);
|
||||
match value {
|
||||
Object::Undefined => output.push_str(&format!("{indent}undefined\n")),
|
||||
Object::Boolean(value) => output.push_str(&format!("{indent}{value} [Boolean]\n")),
|
||||
Object::Integer(value) => output.push_str(&format!("{indent}{value} [Int32]\n")),
|
||||
Object::UInteger(value) => output.push_str(&format!("{indent}{value} [UInt32]\n")),
|
||||
Object::Long(value) => output.push_str(&format!("{indent}{value} [Int64]\n")),
|
||||
Object::ULong(value) => output.push_str(&format!("{indent}{value} [UInt64]\n")),
|
||||
Object::Real(value) => output.push_str(&format!("{indent}{value} [Double]\n")),
|
||||
Object::String(value) => output.push_str(&format!("{indent}{value} [String]\n")),
|
||||
Object::Bytes(value) => {
|
||||
output.push_str(&format!("{indent}[Byte[{0}]]\n", value.len()));
|
||||
format_hex(value, depth + 1, output)?;
|
||||
}
|
||||
Object::Array(values) => {
|
||||
output.push_str(&format!("{indent}[Array {0}]\n", values.len()));
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
output.push_str(&format!("{indent} [{index}]\n"));
|
||||
format_object(value, depth + 2, output)?;
|
||||
}
|
||||
}
|
||||
Object::Map(values) => {
|
||||
output.push_str(&format!("{indent}[Map {0}]\n", values.len()));
|
||||
let mut entries = values.iter().collect::<Vec<_>>();
|
||||
entries.sort_by(|left, right| left.0.cmp(right.0));
|
||||
for (key, value) in entries {
|
||||
output.push_str(&format!("{indent} {key}:\n"));
|
||||
format_object(value, depth + 2, output)?;
|
||||
}
|
||||
}
|
||||
other => output.push_str(&format!("{indent}{other:?}\n")),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_osd_map(
|
||||
values: &std::collections::HashMap<String, OSD>,
|
||||
depth: usize,
|
||||
output: &mut String,
|
||||
) -> Result<(), Error> {
|
||||
let mut entries = values.iter().collect::<Vec<_>>();
|
||||
entries.sort_by(|left, right| left.0.cmp(right.0));
|
||||
for (key, value) in entries {
|
||||
writeln!(output, "{}{key}:", " ".repeat(depth)).map_err(|_| Error::InvalidOperation)?;
|
||||
format_osd(value, depth + 1, output)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_osd(value: &OSD, depth: usize, output: &mut String) -> Result<(), Error> {
|
||||
let indent = " ".repeat(depth);
|
||||
match value {
|
||||
OSD::Map(values) => format_osd_map(values, depth, output),
|
||||
OSD::Array(values) => {
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
writeln!(output, "{indent}[{index}]").map_err(|_| Error::InvalidOperation)?;
|
||||
format_osd(value, depth + 1, output)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
OSD::Binary(bytes) => format_hex(bytes, depth, output),
|
||||
other => writeln!(output, "{indent}{other:?}").map_err(|_| Error::InvalidOperation),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_hex(bytes: &[u8], depth: usize, output: &mut String) -> Result<(), Error> {
|
||||
let indent = " ".repeat(depth);
|
||||
for chunk in bytes.chunks(16) {
|
||||
write!(output, "{indent}").map_err(|_| Error::InvalidOperation)?;
|
||||
for byte in chunk {
|
||||
write!(output, "{byte:02X} ").map_err(|_| Error::InvalidOperation)?;
|
||||
}
|
||||
output.push('\n');
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn options_match_reference_layout() {
|
||||
let mut header = crate::packet_catalog::new_header(crate::PacketFrequency::High, 1, false);
|
||||
header.reliable = true;
|
||||
header.resent = true;
|
||||
assert_eq!(interpret_options(&header), "[ Res Rel ]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packet_dump_contains_header_and_sorted_payload() {
|
||||
let packet =
|
||||
crate::packet_catalog::build_packet(crate::packet_catalog::PacketType::CloseCircuit)
|
||||
.unwrap();
|
||||
let dump = packet_to_string(&packet).unwrap();
|
||||
assert!(dump.contains("Packet Type: CloseCircuit"));
|
||||
assert!(dump.contains("[Packet Header]"));
|
||||
assert!(dump.contains("[Packet Payload]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_dump_formats_nested_values_and_bytes() {
|
||||
let value = Object::Map(std::collections::HashMap::from([
|
||||
("z".to_owned(), Object::Bytes(vec![0xab, 0xcd])),
|
||||
("a".to_owned(), Object::Integer(7)),
|
||||
]));
|
||||
let text = message_to_string(&value, 0).unwrap();
|
||||
assert!(text.find("a:").unwrap() < text.find("z:").unwrap());
|
||||
assert!(text.contains("AB CD"));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Bounded primitives shared by the generated UDP packet codecs.
|
||||
|
||||
#![allow(clippy::needless_pass_by_value)] // Header values are small mapped wire records.
|
||||
|
||||
use crate::packets::{Header, Packet};
|
||||
use crate::{Error, Helpers, PacketFrequency};
|
||||
use libremetaverse_types::{Quaternion, UUID, Vector3, Vector3d, Vector4};
|
||||
@@ -377,6 +379,43 @@ pub(crate) fn encode_base_packet(packet: &Packet) -> Result<Vec<u8>, Error> {
|
||||
Ok(writer.into_inner())
|
||||
}
|
||||
|
||||
pub(crate) fn reheader_packet(bytes: &[u8], header: Header) -> Result<Vec<u8>, Error> {
|
||||
let mut position = 0_i32;
|
||||
let mut packet_end = i32::try_from(bytes.len())
|
||||
.map_err(|_| Error::Argument)?
|
||||
.checked_sub(1)
|
||||
.ok_or(Error::Argument)?;
|
||||
let _old_header = decode_header(bytes, &mut position, &mut packet_end)?;
|
||||
let payload_start = usize::try_from(position).map_err(|_| Error::Argument)?;
|
||||
let payload_end = usize::try_from(packet_end.checked_add(1).ok_or(Error::Argument)?)
|
||||
.map_err(|_| Error::Argument)?;
|
||||
let payload = bytes
|
||||
.get(payload_start..payload_end)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
let capacity = header_length(header.frequency)
|
||||
.checked_add(payload.len())
|
||||
.and_then(|length| length.checked_add(ack_length(&header).ok()?))
|
||||
.ok_or(Error::Argument)?;
|
||||
let mut writer = WireWriter::with_capacity(capacity)?;
|
||||
encode_header(&header, &mut writer)?;
|
||||
writer.write_bytes(payload)?;
|
||||
encode_acks(&header, &mut writer)?;
|
||||
Ok(writer.into_inner())
|
||||
}
|
||||
|
||||
pub(crate) fn reheader_packets(packets: &[Vec<u8>], header: Header) -> Result<Vec<Vec<u8>>, Error> {
|
||||
let mut output = Vec::with_capacity(packets.len());
|
||||
for (index, packet) in packets.iter().enumerate() {
|
||||
let mut packet_header = clone_header(&header);
|
||||
if index != 0 {
|
||||
packet_header.appended_acks = false;
|
||||
packet_header.ack_list = None;
|
||||
}
|
||||
output.push(reheader_packet(packet, packet_header)?);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) fn decode_header(
|
||||
bytes: &[u8],
|
||||
position: &mut i32,
|
||||
@@ -650,7 +689,7 @@ pub(crate) fn build_packet_from_bytes(
|
||||
let packet_type = crate::packet_catalog::packet_type(header.id, header.frequency);
|
||||
let descriptor = crate::packet_catalog::descriptor_by_type(packet_type)
|
||||
.ok_or_else(|| parse(usize::try_from(position).unwrap_or(0), "unknown packet ID"))?;
|
||||
if header.zerocoded {
|
||||
let native_body = if header.zerocoded {
|
||||
let decoded_length = zero_decode(
|
||||
Some(packet_buffer),
|
||||
packet_end.checked_add(1).ok_or(Error::Argument)?,
|
||||
@@ -658,22 +697,22 @@ pub(crate) fn build_packet_from_bytes(
|
||||
)?;
|
||||
*packet_end = decoded_length.checked_sub(1).ok_or(Error::Argument)?;
|
||||
let decoded_length = usize::try_from(decoded_length).map_err(|_| Error::Argument)?;
|
||||
crate::packet_catalog::validate_packet_payload(
|
||||
crate::packet_catalog::decode_packet_body(
|
||||
packet_type,
|
||||
clone_header(&header),
|
||||
&zero_buffer[..decoded_length],
|
||||
&mut position,
|
||||
packet_end,
|
||||
)?;
|
||||
)?
|
||||
} else {
|
||||
crate::packet_catalog::validate_packet_payload(
|
||||
crate::packet_catalog::decode_packet_body(
|
||||
packet_type,
|
||||
clone_header(&header),
|
||||
packet_buffer,
|
||||
&mut position,
|
||||
packet_end,
|
||||
)?;
|
||||
}
|
||||
)?
|
||||
};
|
||||
Ok(Packet {
|
||||
has_variable_blocks: descriptor.blocks.iter().any(|block| {
|
||||
matches!(
|
||||
@@ -683,6 +722,11 @@ pub(crate) fn build_packet_from_bytes(
|
||||
}),
|
||||
header,
|
||||
type_: packet_type,
|
||||
native_payload_length: crate::packet_catalog::packet_payload_length(
|
||||
packet_type,
|
||||
&native_body,
|
||||
)?,
|
||||
native_body: Some(native_body),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
use std::f32::consts::PI;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use libremetaverse_types::{Color4, Quaternion, Vector3};
|
||||
|
||||
use crate::{
|
||||
Error, LiveParticle, PrimitiveParticleSystem,
|
||||
PrimitiveParticleSystemParticleDataFlags as DataFlags,
|
||||
PrimitiveParticleSystemParticleFlags as ParticleFlags,
|
||||
PrimitiveParticleSystemSourcePattern as SourcePattern,
|
||||
};
|
||||
|
||||
const MAX_PARTICLES: usize = 4096;
|
||||
|
||||
struct State {
|
||||
source_position: Vector3,
|
||||
source_rotation: Quaternion,
|
||||
target_position: Vector3,
|
||||
wind: Vector3,
|
||||
system_age: f32,
|
||||
burst_timer: f32,
|
||||
active: bool,
|
||||
particles: Vec<LiveParticle>,
|
||||
random: u64,
|
||||
}
|
||||
|
||||
pub struct ParticleSimulator {
|
||||
system: PrimitiveParticleSystem,
|
||||
state: Mutex<State>,
|
||||
}
|
||||
|
||||
impl ParticleSimulator {
|
||||
pub fn new(system: PrimitiveParticleSystem, seed: Option<i32>) -> Result<Self, Error> {
|
||||
let random = match seed.unwrap_or_default() {
|
||||
0 => {
|
||||
let elapsed = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
elapsed.as_secs() ^ u64::from(elapsed.subsec_nanos())
|
||||
}
|
||||
seed => seed as i64 as u64,
|
||||
}
|
||||
.max(1);
|
||||
Ok(Self {
|
||||
system,
|
||||
state: Mutex::new(State {
|
||||
source_position: Vector3::zero(),
|
||||
source_rotation: Quaternion::identity(),
|
||||
target_position: Vector3::zero(),
|
||||
wind: Vector3::zero(),
|
||||
system_age: 0.0,
|
||||
burst_timer: 0.0,
|
||||
active: true,
|
||||
particles: Vec::with_capacity(256),
|
||||
random,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_particles(&self) -> Result<Vec<LiveParticle>, Error> {
|
||||
Ok(self.lock().particles.clone())
|
||||
}
|
||||
|
||||
pub fn tick(&self, dt: f32) -> Result<(), Error> {
|
||||
if !dt.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if dt <= 0.0 {
|
||||
return Ok(());
|
||||
}
|
||||
let mut state = self.lock();
|
||||
state.system_age += dt;
|
||||
if self.system.max_age > 0.0 && state.system_age > self.system.max_age {
|
||||
state.active = false;
|
||||
}
|
||||
|
||||
let flags = self.system.part_data_flags.0;
|
||||
let wind_enabled = flags & DataFlags::WIND.0 != 0;
|
||||
let bounce = flags & DataFlags::BOUNCE.0 != 0;
|
||||
let target_pos = flags & DataFlags::TARGET_POS.0 != 0;
|
||||
let target_linear = flags & DataFlags::TARGET_LINEAR.0 != 0;
|
||||
let interp_color = flags & DataFlags::INTERP_COLOR.0 != 0;
|
||||
let interp_scale = flags & DataFlags::INTERP_SCALE.0 != 0;
|
||||
let wind = state.wind;
|
||||
let source = state.source_position;
|
||||
let target = state.target_position;
|
||||
|
||||
let mut index = state.particles.len();
|
||||
while index > 0 {
|
||||
index -= 1;
|
||||
let particle = &mut state.particles[index];
|
||||
particle.age += dt;
|
||||
if particle.age >= self.system.part_max_age {
|
||||
state.particles.remove(index);
|
||||
continue;
|
||||
}
|
||||
particle.velocity.x += self.system.part_acceleration.x * dt;
|
||||
particle.velocity.y += self.system.part_acceleration.y * dt;
|
||||
particle.velocity.z += self.system.part_acceleration.z * dt;
|
||||
if wind_enabled {
|
||||
particle.velocity.x += (wind.x - particle.velocity.x) * dt * 2.0;
|
||||
particle.velocity.y += (wind.y - particle.velocity.y) * dt * 2.0;
|
||||
particle.velocity.z += (wind.z - particle.velocity.z) * dt * 2.0;
|
||||
}
|
||||
particle.position.x += particle.velocity.x * dt;
|
||||
particle.position.y += particle.velocity.y * dt;
|
||||
particle.position.z += particle.velocity.z * dt;
|
||||
if bounce && particle.position.z < 0.0 {
|
||||
particle.position.z = 0.0;
|
||||
particle.velocity.z *= -0.5;
|
||||
}
|
||||
if target_pos || target_linear {
|
||||
let direction = normalize(Vector3 {
|
||||
x: target.x - source.x - particle.position.x,
|
||||
y: target.y - source.y - particle.position.y,
|
||||
z: target.z - source.z - particle.position.z,
|
||||
});
|
||||
if target_linear {
|
||||
particle.velocity = scale(
|
||||
direction,
|
||||
(self.system.burst_speed_min + self.system.burst_speed_max) * 0.5,
|
||||
);
|
||||
} else {
|
||||
particle.velocity.x += direction.x * dt * 2.0;
|
||||
particle.velocity.y += direction.y * dt * 2.0;
|
||||
particle.velocity.z += direction.z * dt * 2.0;
|
||||
}
|
||||
}
|
||||
let age = if self.system.part_max_age > 0.0 {
|
||||
(particle.age / self.system.part_max_age).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
particle.color = if interp_color {
|
||||
Color4::lerp(
|
||||
self.system.part_start_color,
|
||||
self.system.part_end_color,
|
||||
age,
|
||||
)?
|
||||
} else {
|
||||
self.system.part_start_color
|
||||
};
|
||||
particle.scale_x = if interp_scale {
|
||||
lerp(
|
||||
self.system.part_start_scale_x,
|
||||
self.system.part_end_scale_x,
|
||||
age,
|
||||
)
|
||||
} else {
|
||||
self.system.part_start_scale_x
|
||||
};
|
||||
particle.scale_y = if interp_scale {
|
||||
lerp(
|
||||
self.system.part_start_scale_y,
|
||||
self.system.part_end_scale_y,
|
||||
age,
|
||||
)
|
||||
} else {
|
||||
self.system.part_start_scale_y
|
||||
};
|
||||
particle.glow = lerp(self.system.part_start_glow, self.system.part_end_glow, age);
|
||||
particle.normalized_age = age;
|
||||
}
|
||||
|
||||
if state.active && state.system_age >= self.system.start_age {
|
||||
state.burst_timer += dt;
|
||||
let rate = self.system.burst_rate.max(0.01);
|
||||
while state.burst_timer >= rate {
|
||||
state.burst_timer -= rate;
|
||||
self.emit_burst(&mut state);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.lock().active
|
||||
}
|
||||
|
||||
pub fn source_position(&self) -> Vector3 {
|
||||
self.lock().source_position
|
||||
}
|
||||
|
||||
pub fn set_source_position(&mut self, value: Vector3) {
|
||||
self.lock().source_position = value;
|
||||
}
|
||||
|
||||
pub fn source_rotation(&self) -> Quaternion {
|
||||
self.lock().source_rotation
|
||||
}
|
||||
|
||||
pub fn set_source_rotation(&mut self, value: Quaternion) {
|
||||
self.lock().source_rotation = value;
|
||||
}
|
||||
|
||||
pub fn system_age(&self) -> f32 {
|
||||
self.lock().system_age
|
||||
}
|
||||
|
||||
pub fn target_position(&self) -> Vector3 {
|
||||
self.lock().target_position
|
||||
}
|
||||
|
||||
pub fn set_target_position(&mut self, value: Vector3) {
|
||||
self.lock().target_position = value;
|
||||
}
|
||||
|
||||
pub fn wind(&self) -> Vector3 {
|
||||
self.lock().wind
|
||||
}
|
||||
|
||||
pub fn set_wind(&mut self, value: Vector3) {
|
||||
self.lock().wind = value;
|
||||
}
|
||||
|
||||
fn emit_burst(&self, state: &mut State) {
|
||||
for _ in 0..self.system.burst_part_count {
|
||||
if state.particles.len() >= MAX_PARTICLES {
|
||||
break;
|
||||
}
|
||||
let position = self.initial_position(state);
|
||||
let velocity = self.initial_velocity(state);
|
||||
state.particles.push(LiveParticle {
|
||||
age: 0.0,
|
||||
color: self.system.part_start_color,
|
||||
glow: self.system.part_start_glow,
|
||||
normalized_age: 0.0,
|
||||
position,
|
||||
scale_x: self.system.part_start_scale_x,
|
||||
scale_y: self.system.part_start_scale_y,
|
||||
velocity,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_position(&self, state: &mut State) -> Vector3 {
|
||||
if self.system.burst_radius <= 0.0 {
|
||||
return Vector3::zero();
|
||||
}
|
||||
let radius = random_f32(state) * self.system.burst_radius;
|
||||
let theta = random_f32(state) * 2.0 * PI;
|
||||
let phi = (random_f32(state) - 0.5) * PI;
|
||||
Vector3 {
|
||||
x: radius * phi.cos() * theta.cos(),
|
||||
y: radius * phi.cos() * theta.sin(),
|
||||
z: radius * phi.sin(),
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_velocity(&self, state: &mut State) -> Vector3 {
|
||||
let speed = lerp(
|
||||
self.system.burst_speed_min,
|
||||
self.system.burst_speed_max,
|
||||
random_f32(state),
|
||||
);
|
||||
let mut direction = if self.system.pattern == SourcePattern::DROP {
|
||||
Vector3::zero()
|
||||
} else if self.system.pattern == SourcePattern::EXPLODE {
|
||||
random_unit_sphere(state)
|
||||
} else if self.system.pattern == SourcePattern::ANGLE
|
||||
|| self.system.pattern == SourcePattern::ANGLE_CONE
|
||||
{
|
||||
cone_direction(state, self.system.inner_angle, self.system.outer_angle)
|
||||
} else if self.system.pattern == SourcePattern::ANGLE_CONE_EMPTY {
|
||||
cone_direction(state, self.system.outer_angle, PI)
|
||||
} else {
|
||||
Vector3::unit_z()
|
||||
};
|
||||
if self.system.part_flags & ParticleFlags::OBJECT_RELATIVE.0 != 0
|
||||
&& direction != Vector3::zero()
|
||||
{
|
||||
direction = Vector3::mul_with_vector3_quaternion(direction, state.source_rotation);
|
||||
}
|
||||
scale(direction, speed)
|
||||
}
|
||||
|
||||
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
|
||||
self.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
}
|
||||
|
||||
fn random_f32(state: &mut State) -> f32 {
|
||||
let mut value = state.random;
|
||||
value ^= value << 13;
|
||||
value ^= value >> 7;
|
||||
value ^= value << 17;
|
||||
state.random = value;
|
||||
(value >> 40) as f32 / (1_u32 << 24) as f32
|
||||
}
|
||||
|
||||
fn random_unit_sphere(state: &mut State) -> Vector3 {
|
||||
let theta = random_f32(state) * 2.0 * PI;
|
||||
let phi = (2.0 * random_f32(state) - 1.0).acos();
|
||||
Vector3 {
|
||||
x: phi.sin() * theta.cos(),
|
||||
y: phi.sin() * theta.sin(),
|
||||
z: phi.cos(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cone_direction(state: &mut State, inner: f32, outer: f32) -> Vector3 {
|
||||
let angle = lerp(inner, outer, random_f32(state));
|
||||
let rotation = random_f32(state) * 2.0 * PI;
|
||||
Vector3 {
|
||||
x: angle.sin() * rotation.cos(),
|
||||
y: angle.sin() * rotation.sin(),
|
||||
z: angle.cos(),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize(value: Vector3) -> Vector3 {
|
||||
let length = (value.x * value.x + value.y * value.y + value.z * value.z).sqrt();
|
||||
if length > 0.0 && length.is_finite() {
|
||||
scale(value, length.recip())
|
||||
} else {
|
||||
Vector3::zero()
|
||||
}
|
||||
}
|
||||
|
||||
const fn scale(value: Vector3, amount: f32) -> Vector3 {
|
||||
Vector3 {
|
||||
x: value.x * amount,
|
||||
y: value.y * amount,
|
||||
z: value.z * amount,
|
||||
}
|
||||
}
|
||||
|
||||
fn lerp(start: f32, end: f32, amount: f32) -> f32 {
|
||||
start + (end - start) * amount
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn emits_ages_and_expires_particles_deterministically() {
|
||||
let mut system = PrimitiveParticleSystem::default();
|
||||
system.pattern = SourcePattern::DROP;
|
||||
system.burst_part_count = 2;
|
||||
system.burst_rate = 0.1;
|
||||
system.part_max_age = 0.25;
|
||||
system.part_start_scale_x = 1.0;
|
||||
system.part_start_scale_y = 2.0;
|
||||
let simulator = ParticleSimulator::new(system, Some(7)).unwrap();
|
||||
simulator.tick(0.1).unwrap();
|
||||
assert_eq!(simulator.get_particles().unwrap().len(), 2);
|
||||
simulator.tick(0.1).unwrap();
|
||||
assert_eq!(simulator.get_particles().unwrap().len(), 4);
|
||||
simulator.tick(0.2).unwrap();
|
||||
assert_eq!(simulator.get_particles().unwrap().len(), 6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
//! Physics-driven collision-volume offsets and positional record semantics.
|
||||
|
||||
use crate::Error;
|
||||
use libremetaverse_types::compat::{Object, Vector3};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
const PHYSICS_DRIVEN_PARAMS: [i32; 6] = [1200, 1201, 1204, 1205, 1206, 1207];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PhysicsVolumeMorphsVolumeMorph {
|
||||
bone_name: String,
|
||||
pos_delta: Vector3,
|
||||
}
|
||||
|
||||
impl PhysicsVolumeMorphsVolumeMorph {
|
||||
pub fn new(bone_name: String, pos_delta: Vector3) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
bone_name,
|
||||
pos_delta,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deconstruct(
|
||||
&mut self,
|
||||
bone_name: &mut String,
|
||||
pos_delta: &mut Vector3,
|
||||
) -> Result<(), Error> {
|
||||
*bone_name = self.bone_name.clone();
|
||||
*pos_delta = self.pos_delta;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn equals_with_volume_morph(&self, other: Self) -> bool {
|
||||
*self == other
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn equals_with_object(&self, object: Object) -> bool {
|
||||
object.downcast_ref::<Self>() == Some(self)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_hash_code(&self) -> i32 {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
self.bone_name.hash(&mut hasher);
|
||||
for value in self.pos_delta.0 {
|
||||
value.to_bits().hash(&mut hasher);
|
||||
}
|
||||
hasher.finish() as i32
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::inherent_to_string_shadow_display)]
|
||||
pub fn to_string(&self) -> String {
|
||||
format!(
|
||||
"VolumeMorph {{ BoneName = {}, PosDelta = <{}, {}, {}> }}",
|
||||
self.bone_name, self.pos_delta.0[0], self.pos_delta.0[1], self.pos_delta.0[2]
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn eq(left: Self, right: Self) -> bool {
|
||||
left == right
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn ne(left: Self, right: Self) -> bool {
|
||||
left != right
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn bone_name(&self) -> String {
|
||||
self.bone_name.clone()
|
||||
}
|
||||
|
||||
pub fn set_bone_name(&mut self, value: String) {
|
||||
self.bone_name = value;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn pos_delta(&self) -> Vector3 {
|
||||
self.pos_delta
|
||||
}
|
||||
|
||||
pub fn set_pos_delta(&mut self, value: Vector3) {
|
||||
self.pos_delta = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhysicsVolumeMorphs;
|
||||
|
||||
impl PhysicsVolumeMorphs {
|
||||
pub fn compute_bone_offsets(
|
||||
driven_weights: HashMap<i32, f32>,
|
||||
vp_weights: HashMap<i32, f32>,
|
||||
) -> Result<HashMap<String, Vector3>, Error> {
|
||||
// Preserved for signature parity; the pinned implementation also uses
|
||||
// only the normalized driven output and the visual-param definition.
|
||||
let _ = vp_weights;
|
||||
let mut offsets: HashMap<String, Vector3> = HashMap::new();
|
||||
for (param_id, normalized) in driven_weights {
|
||||
if !PHYSICS_DRIVEN_PARAMS.contains(¶m_id) {
|
||||
continue;
|
||||
}
|
||||
let Some(parameter) = crate::visual_catalog::param_by_id(param_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(morphs) = parameter.volume_morphs.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let local_value =
|
||||
parameter.min_value + (parameter.max_value - parameter.min_value) * normalized;
|
||||
for morph in morphs {
|
||||
let delta = Vector3([
|
||||
morph.position_delta.x * local_value,
|
||||
morph.position_delta.y * local_value,
|
||||
morph.position_delta.z * local_value,
|
||||
]);
|
||||
offsets
|
||||
.entry(morph.bone_name.clone())
|
||||
.and_modify(|current| {
|
||||
for index in 0..3 {
|
||||
current.0[index] += delta.0[index];
|
||||
}
|
||||
})
|
||||
.or_insert(delta);
|
||||
}
|
||||
}
|
||||
Ok(offsets)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn record_properties_equality_and_deconstruction_round_trip() {
|
||||
let delta = Vector3([0.0, 0.0, -0.01]);
|
||||
let mut morph = PhysicsVolumeMorphsVolumeMorph::new("LEFT_PEC".to_owned(), delta).unwrap();
|
||||
let copy = morph.clone();
|
||||
assert!(morph.equals_with_volume_morph(copy.clone()));
|
||||
assert!(morph.equals_with_object(Object::opaque(copy)));
|
||||
let mut name = String::new();
|
||||
let mut output = Vector3::default();
|
||||
morph.deconstruct(&mut name, &mut output).unwrap();
|
||||
assert_eq!(name, "LEFT_PEC");
|
||||
assert_eq!(output, delta);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driven_weight_uses_pinned_visual_param_range_and_morphs() {
|
||||
let offsets =
|
||||
PhysicsVolumeMorphs::compute_bone_offsets(HashMap::from([(1200, 1.0)]), HashMap::new())
|
||||
.unwrap();
|
||||
assert_eq!(offsets["LEFT_PEC"], Vector3([0.0, 0.0, -0.03]));
|
||||
assert_eq!(offsets["RIGHT_PEC"], Vector3([0.0, 0.0, -0.03]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Error types preserved from the upstream public API.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use libremetaverse_types::compat::ExternalError;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
macro_rules! message_error {
|
||||
($name:ident, $fallback:literal) => {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct $name {
|
||||
message: String,
|
||||
inner: Option<ExternalError>,
|
||||
}
|
||||
|
||||
impl $name {
|
||||
fn with_parts(message: String, inner: Option<ExternalError>) -> Self {
|
||||
Self { message, inner }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for $name {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.message.is_empty() {
|
||||
formatter.write_str($fallback)
|
||||
} else {
|
||||
formatter.write_str(&self.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for $name {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.inner
|
||||
.as_ref()
|
||||
.map(|error| error as &(dyn std::error::Error + 'static))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
message_error!(AppearanceManagerException, "appearance operation failed");
|
||||
message_error!(MalformedDataException, "malformed packet data");
|
||||
message_error!(RenderingException, "rendering failed");
|
||||
|
||||
impl AppearanceManagerException {
|
||||
pub fn new(message: String) -> Result<Self, Error> {
|
||||
Ok(Self::with_parts(message, None))
|
||||
}
|
||||
}
|
||||
|
||||
impl MalformedDataException {
|
||||
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||
Ok(Self::with_parts(String::new(), None))
|
||||
}
|
||||
|
||||
pub fn new_with_string(message: String) -> Result<Self, Error> {
|
||||
Ok(Self::with_parts(message, None))
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderingException {
|
||||
pub fn new_with_string(message: String) -> Result<Self, Error> {
|
||||
Ok(Self::with_parts(message, None))
|
||||
}
|
||||
|
||||
pub fn new_with_string_exception(
|
||||
message: String,
|
||||
inner_exception: ExternalError,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self::with_parts(message, Some(inner_exception)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rendering_error_exposes_source() {
|
||||
let error = RenderingException::new_with_string_exception(
|
||||
"mesh failed".to_owned(),
|
||||
ExternalError("bad triangle".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(error.to_string(), "mesh failed");
|
||||
assert_eq!(
|
||||
std::error::Error::source(&error).unwrap().to_string(),
|
||||
"bad triangle"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
//! Region/local coordinate conversions and parent-relative object positions.
|
||||
|
||||
use crate::{Avatar, Error, Primitive, Simulator};
|
||||
use libremetaverse_types::{Vector3, Vector3d};
|
||||
|
||||
pub struct PositionHelper;
|
||||
|
||||
impl PositionHelper {
|
||||
pub fn to_global_position(sim_handle: u64, local_pos: Vector3) -> Result<Vector3d, Error> {
|
||||
let global_x = (sim_handle >> 32) as u32;
|
||||
let global_y = sim_handle as u32;
|
||||
Vector3d::new_with_double_double_double(
|
||||
f64::from(global_x) + f64::from(local_pos.x),
|
||||
f64::from(global_y) + f64::from(local_pos.y),
|
||||
f64::from(local_pos.z),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_local_position(sim_handle: u64, global_pos: Vector3d) -> Result<Vector3, Error> {
|
||||
let global_x = (sim_handle >> 32) as u32;
|
||||
let global_y = sim_handle as u32;
|
||||
Vector3::new_with_single_single_single(
|
||||
(global_pos.x - f64::from(global_x)) as f32,
|
||||
(global_pos.y - f64::from(global_y)) as f32,
|
||||
global_pos.z as f32,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn global_position_with_simulator_vector3(
|
||||
sim: Simulator,
|
||||
local_pos: Vector3,
|
||||
) -> Result<Vector3d, Error> {
|
||||
Self::to_global_position(sim.handle, local_pos)
|
||||
}
|
||||
|
||||
pub fn global_position_with_primitive_simulator(
|
||||
prim: Primitive,
|
||||
current_sim: Simulator,
|
||||
) -> Result<Vector3d, Error> {
|
||||
Self::global_position_with_simulator_vector3(current_sim, prim.position)
|
||||
}
|
||||
|
||||
pub fn region_handle_from_global(global_x: u32, global_y: u32) -> Result<u64, Error> {
|
||||
let x = global_x / Simulator::DEFAULT_REGION_SIZE_X * Simulator::DEFAULT_REGION_SIZE_X;
|
||||
let y = global_y / Simulator::DEFAULT_REGION_SIZE_Y * Simulator::DEFAULT_REGION_SIZE_Y;
|
||||
Ok((u64::from(x) << 32) | u64::from(y))
|
||||
}
|
||||
|
||||
pub fn region_handle_from_global_position(global_pos: Vector3d) -> Result<u64, Error> {
|
||||
Self::region_handle_from_global(global_pos.x as u32, global_pos.y as u32)
|
||||
}
|
||||
|
||||
pub fn get_avatar_position(sim: Simulator, avatar: Avatar) -> Result<Vector3, Error> {
|
||||
if avatar.parent_id == 0 {
|
||||
return Ok(avatar.position);
|
||||
}
|
||||
let objects = sim
|
||||
.objects_primitives
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
Ok(objects
|
||||
.get(&avatar.parent_id)
|
||||
.map_or(avatar.position, |parent| {
|
||||
Vector3::add_with_vector3_vector3_87e25eaf(
|
||||
parent.position,
|
||||
Vector3::mul_with_vector3_quaternion(avatar.position, parent.rotation),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn get_prim_position(sim: Simulator, prim: Primitive) -> Result<Vector3, Error> {
|
||||
if prim.parent_id == 0 {
|
||||
return Ok(prim.position);
|
||||
}
|
||||
let objects = sim
|
||||
.objects_primitives
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
Ok(objects
|
||||
.get(&prim.parent_id)
|
||||
.map_or(prim.position, |parent| {
|
||||
Vector3::add_with_vector3_vector3_87e25eaf(
|
||||
parent.position,
|
||||
Vector3::mul_with_vector3_quaternion(prim.position, parent.rotation),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn format_coordinates(
|
||||
x: Option<Option<i32>>,
|
||||
y: Option<Option<i32>>,
|
||||
z: Option<Option<i32>>,
|
||||
) -> Result<String, Error> {
|
||||
let values: Vec<_> = [x.flatten(), y.flatten(), z.flatten()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|value| value.to_string())
|
||||
.collect();
|
||||
Ok(if values.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" ({})", values.join(","))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn format_region_coordinates(
|
||||
region_name: String,
|
||||
x: Option<Option<i32>>,
|
||||
y: Option<Option<i32>>,
|
||||
z: Option<Option<i32>>,
|
||||
) -> Result<String, Error> {
|
||||
Ok(region_name + &Self::format_coordinates(x, y, z)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn local_global_round_trip_and_region_alignment() {
|
||||
let handle = ((512_u64 * 256) << 32) | (1024_u64 * 256);
|
||||
let local = Vector3::new_with_single_single_single(10.5, 20.25, 5.0).unwrap();
|
||||
let global = PositionHelper::to_global_position(handle, local).unwrap();
|
||||
assert_eq!(
|
||||
PositionHelper::to_local_position(handle, global).unwrap(),
|
||||
local
|
||||
);
|
||||
assert_eq!(
|
||||
PositionHelper::region_handle_from_global(300, 700).unwrap(),
|
||||
(256_u64 << 32) | 512
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_only_present_coordinates() {
|
||||
assert_eq!(
|
||||
PositionHelper::format_coordinates(None, None, None).unwrap(),
|
||||
""
|
||||
);
|
||||
assert_eq!(
|
||||
PositionHelper::format_coordinates(Some(Some(10)), Some(None), Some(Some(30))).unwrap(),
|
||||
" (10,30)"
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,600 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
|
||||
|
||||
use libremetaverse_types::compat::Object;
|
||||
|
||||
use crate::{Error, FieldType, GridClient, PacketFrequency};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MapField {
|
||||
pub count: i32,
|
||||
pub keyword_position: i32,
|
||||
pub name: String,
|
||||
pub type_: FieldType,
|
||||
}
|
||||
|
||||
impl Default for MapField {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
count: 0,
|
||||
keyword_position: 0,
|
||||
name: String::new(),
|
||||
type_: FieldType::U8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MapField {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
pub fn compare_to(&self, obj: Option<Object>) -> Result<i32, Error> {
|
||||
let Some(obj) = obj else {
|
||||
return Ok(1);
|
||||
};
|
||||
let other = obj.downcast_ref::<Self>().ok_or(Error::Argument)?;
|
||||
Ok(self.keyword_position.cmp(&other.keyword_position) as i32)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MapBlock {
|
||||
pub count: i32,
|
||||
pub fields: Vec<MapField>,
|
||||
pub keyword_position: i32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl MapBlock {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
pub fn compare_to(&self, obj: Option<Object>) -> Result<i32, Error> {
|
||||
let Some(obj) = obj else {
|
||||
return Ok(1);
|
||||
};
|
||||
let other = obj.downcast_ref::<Self>().ok_or(Error::Argument)?;
|
||||
Ok(self.keyword_position.cmp(&other.keyword_position) as i32)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MapPacket {
|
||||
pub blocks: Vec<MapBlock>,
|
||||
pub encoded: bool,
|
||||
pub frequency: PacketFrequency,
|
||||
pub id: u16,
|
||||
pub name: String,
|
||||
pub trusted: bool,
|
||||
}
|
||||
|
||||
impl Default for MapPacket {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
blocks: Vec::new(),
|
||||
encoded: false,
|
||||
frequency: PacketFrequency::Low,
|
||||
id: 0,
|
||||
name: String::new(),
|
||||
trusted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MapPacket {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProtocolManager {
|
||||
pub high_maps: Vec<MapPacket>,
|
||||
pub keyword_positions: HashMap<String, i32>,
|
||||
pub low_maps: Vec<MapPacket>,
|
||||
pub medium_maps: Vec<MapPacket>,
|
||||
pub type_sizes: HashMap<FieldType, i32>,
|
||||
}
|
||||
|
||||
impl ProtocolManager {
|
||||
pub fn new(map_file: String, client: GridClient) -> Result<Self, Error> {
|
||||
// The C# instance retains this reference only to enrich parse-error logging.
|
||||
// Rust reports parsing failures to the caller, so no client ownership is needed.
|
||||
drop(client);
|
||||
let mut manager = Self::empty();
|
||||
manager.load_map_file(&map_file)?;
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
let type_sizes = HashMap::from([
|
||||
(FieldType::U8, 1),
|
||||
(FieldType::U16, 2),
|
||||
(FieldType::U32, 4),
|
||||
(FieldType::U64, 8),
|
||||
(FieldType::S8, 1),
|
||||
(FieldType::S16, 2),
|
||||
(FieldType::S32, 4),
|
||||
(FieldType::F32, 4),
|
||||
(FieldType::F64, 8),
|
||||
(FieldType::UUID, 16),
|
||||
(FieldType::BOOL, 1),
|
||||
(FieldType::Vector3, 12),
|
||||
(FieldType::Vector3d, 24),
|
||||
(FieldType::Vector4, 16),
|
||||
(FieldType::Quaternion, 16),
|
||||
(FieldType::IPADDR, 4),
|
||||
(FieldType::IPPORT, 2),
|
||||
(FieldType::Variable, -1),
|
||||
(FieldType::Fixed, -2),
|
||||
]);
|
||||
Self {
|
||||
high_maps: vec![MapPacket::default(); 256],
|
||||
keyword_positions: HashMap::new(),
|
||||
low_maps: vec![MapPacket::default(); 65_536],
|
||||
medium_maps: vec![MapPacket::default(); 256],
|
||||
type_sizes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command_with_bytes(&self, data: Vec<u8>) -> Result<Option<MapPacket>, Error> {
|
||||
let Some(&frequency_byte) = data.get(4) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let result = if frequency_byte != 0xff {
|
||||
self.command_with_u_int16_packet_frequency(
|
||||
u16::from(frequency_byte),
|
||||
PacketFrequency::High,
|
||||
)
|
||||
} else if data.get(5) != Some(&0xff) {
|
||||
let Some(&command) = data.get(5) else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.command_with_u_int16_packet_frequency(u16::from(command), PacketFrequency::Medium)
|
||||
} else {
|
||||
let (Some(&high), Some(&low)) = (data.get(6), data.get(7)) else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.command_with_u_int16_packet_frequency(
|
||||
u16::from_be_bytes([high, low]),
|
||||
PacketFrequency::Low,
|
||||
)
|
||||
};
|
||||
match result {
|
||||
Ok(packet) => Ok(Some(packet)),
|
||||
Err(Error::IndexOutOfRange | Error::InvalidOperation) => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command_with_string(&self, command: String) -> Result<MapPacket, Error> {
|
||||
self.high_maps
|
||||
.iter()
|
||||
.chain(&self.medium_maps)
|
||||
.chain(&self.low_maps)
|
||||
.find(|packet| !packet.name.is_empty() && packet.name == command)
|
||||
.cloned()
|
||||
.ok_or(Error::InvalidOperation)
|
||||
}
|
||||
|
||||
pub fn command_with_u_int16_packet_frequency(
|
||||
&self,
|
||||
command: u16,
|
||||
frequency: PacketFrequency,
|
||||
) -> Result<MapPacket, Error> {
|
||||
let maps = match frequency {
|
||||
PacketFrequency::High => &self.high_maps,
|
||||
PacketFrequency::Medium => &self.medium_maps,
|
||||
PacketFrequency::Low => &self.low_maps,
|
||||
};
|
||||
let packet = maps
|
||||
.get(usize::from(command))
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
if packet.name.is_empty() {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
Ok(packet.clone())
|
||||
}
|
||||
|
||||
pub fn decode_map_file(map_file: String, output_file: String) -> Result<(), Error> {
|
||||
let input = File::open(map_file).map_err(|_| Error::InvalidOperation)?;
|
||||
let output = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(output_file)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
Self::decode(BufReader::new(input), BufWriter::new(output))
|
||||
}
|
||||
|
||||
fn decode(mut input: impl Read, mut output: impl Write) -> Result<(), Error> {
|
||||
let mut key = 0_u8;
|
||||
let mut buffer = [0_u8; 2048];
|
||||
loop {
|
||||
let count = input
|
||||
.read(&mut buffer)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
for byte in &mut buffer[..count] {
|
||||
*byte ^= key;
|
||||
key = key.wrapping_add(43);
|
||||
}
|
||||
output
|
||||
.write_all(&buffer[..count])
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
}
|
||||
output.flush().map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
|
||||
pub fn print_map(&self) -> Result<(), Error> {
|
||||
self.print_one_map(&self.low_maps, "Low ");
|
||||
self.print_one_map(&self.medium_maps, "Medium");
|
||||
self.print_one_map(&self.high_maps, "High ");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_one_map(&self, maps: &[MapPacket], frequency: &str) {
|
||||
for (index, packet) in maps
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, packet)| !packet.name.is_empty())
|
||||
{
|
||||
println!(
|
||||
"{frequency} {index:5} - {} - {} - {}",
|
||||
packet.name,
|
||||
if packet.trusted {
|
||||
"Trusted"
|
||||
} else {
|
||||
"Untrusted"
|
||||
},
|
||||
if packet.encoded {
|
||||
"Unencoded"
|
||||
} else {
|
||||
"Zerocoded"
|
||||
}
|
||||
);
|
||||
for block in &packet.blocks {
|
||||
if block.count == -1 {
|
||||
println!("\t{:4} {} (Variable)", block.keyword_position, block.name);
|
||||
} else {
|
||||
println!(
|
||||
"\t{:4} {} ({})",
|
||||
block.keyword_position, block.name, block.count
|
||||
);
|
||||
}
|
||||
for field in &block.fields {
|
||||
println!(
|
||||
"\t\t{:4} {} ({:?} / {})",
|
||||
field.keyword_position, field.name, field.type_, field.count
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_map_file(&mut self, map_file: &str) -> Result<(), Error> {
|
||||
let input = File::open(map_file).map_err(|_| Error::InvalidOperation)?;
|
||||
self.parse(BufReader::new(input))
|
||||
}
|
||||
|
||||
fn parse(&mut self, input: impl BufRead) -> Result<(), Error> {
|
||||
let mut low = 1_u16;
|
||||
let mut medium = 1_u16;
|
||||
let mut high = 1_u16;
|
||||
let mut in_packet = false;
|
||||
let mut in_block = false;
|
||||
let mut current_packet: Option<(PacketFrequency, usize)> = None;
|
||||
let mut current_block: Option<MapBlock> = None;
|
||||
|
||||
for (line_number, line) in input.lines().enumerate() {
|
||||
let line = line.map_err(|_| Error::Parse {
|
||||
position: line_number,
|
||||
context: "could not read protocol map line",
|
||||
})?;
|
||||
let trimmed = line.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
if !in_packet {
|
||||
if trimmed == "{" {
|
||||
in_packet = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if !in_block {
|
||||
if trimmed == "{" {
|
||||
if current_packet.is_none() {
|
||||
return Err(Self::parse_error(line_number, "block has no packet header"));
|
||||
}
|
||||
in_block = true;
|
||||
} else if trimmed == "}" {
|
||||
if let Some((frequency, index)) = current_packet.take() {
|
||||
self.packet_mut(frequency, index)?
|
||||
.blocks
|
||||
.sort_by_key(|block| block.keyword_position);
|
||||
}
|
||||
in_packet = false;
|
||||
} else if !trimmed.is_empty() && !trimmed.starts_with("//") {
|
||||
current_packet = Some(self.parse_packet_header(
|
||||
&trimmed,
|
||||
line_number,
|
||||
&mut low,
|
||||
&mut medium,
|
||||
&mut high,
|
||||
)?);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if trimmed.starts_with('{') {
|
||||
let tokens: Vec<_> = trimmed.split_whitespace().collect();
|
||||
if tokens.len() < 4 {
|
||||
return Err(Self::parse_error(
|
||||
line_number,
|
||||
"malformed field declaration",
|
||||
));
|
||||
}
|
||||
let type_ = Self::parse_field_type(tokens[2])
|
||||
.ok_or_else(|| Self::parse_error(line_number, "unknown field type"))?;
|
||||
let count = if tokens[3] == "}" {
|
||||
1
|
||||
} else {
|
||||
tokens[3]
|
||||
.parse()
|
||||
.map_err(|_| Self::parse_error(line_number, "invalid field count"))?
|
||||
};
|
||||
let keyword_position = self.keyword_position(tokens[1])?;
|
||||
let block = current_block
|
||||
.as_mut()
|
||||
.ok_or_else(|| Self::parse_error(line_number, "field has no block header"))?;
|
||||
block.fields.push(MapField {
|
||||
count,
|
||||
keyword_position,
|
||||
name: tokens[1].to_owned(),
|
||||
type_,
|
||||
});
|
||||
} else if trimmed == "}" {
|
||||
let mut block = current_block.take().ok_or_else(|| {
|
||||
Self::parse_error(line_number, "block terminator has no header")
|
||||
})?;
|
||||
block.fields.sort_by_key(|field| field.keyword_position);
|
||||
let (frequency, index) = current_packet
|
||||
.ok_or_else(|| Self::parse_error(line_number, "block has no packet"))?;
|
||||
self.packet_mut(frequency, index)?.blocks.push(block);
|
||||
in_block = false;
|
||||
} else if !trimmed.is_empty() && !trimmed.starts_with("//") {
|
||||
if current_block.is_some() {
|
||||
return Err(Self::parse_error(line_number, "block header is incomplete"));
|
||||
}
|
||||
let tokens: Vec<_> = trimmed.split_whitespace().collect();
|
||||
if tokens.len() < 2 {
|
||||
return Err(Self::parse_error(line_number, "malformed block header"));
|
||||
}
|
||||
let count = match tokens[1] {
|
||||
"Single" => 1,
|
||||
"Variable" => -1,
|
||||
"Multiple" => tokens
|
||||
.get(2)
|
||||
.ok_or_else(|| Self::parse_error(line_number, "missing block count"))?
|
||||
.parse()
|
||||
.map_err(|_| Self::parse_error(line_number, "invalid block count"))?,
|
||||
_ => return Err(Self::parse_error(line_number, "unknown block frequency")),
|
||||
};
|
||||
current_block = Some(MapBlock {
|
||||
count,
|
||||
fields: Vec::new(),
|
||||
keyword_position: self.keyword_position(tokens[0])?,
|
||||
name: tokens[0].to_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if in_packet || in_block || current_block.is_some() {
|
||||
return Err(Self::parse_error(
|
||||
0,
|
||||
"unterminated protocol map declaration",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_packet_header(
|
||||
&mut self,
|
||||
line: &str,
|
||||
position: usize,
|
||||
low: &mut u16,
|
||||
medium: &mut u16,
|
||||
high: &mut u16,
|
||||
) -> Result<(PacketFrequency, usize), Error> {
|
||||
let tokens: Vec<_> = line.split_whitespace().collect();
|
||||
if tokens.len() < 4 {
|
||||
return Err(Self::parse_error(position, "malformed packet header"));
|
||||
}
|
||||
self.keyword_position(tokens[0])?;
|
||||
let (frequency, id, trusted_index, encoded_index) = match tokens[1] {
|
||||
"Fixed" => {
|
||||
if tokens.len() < 5 {
|
||||
return Err(Self::parse_error(position, "malformed fixed packet header"));
|
||||
}
|
||||
let raw = tokens[2].strip_prefix("0x").unwrap_or(tokens[2]);
|
||||
let raw = u32::from_str_radix(raw, 16)
|
||||
.map_err(|_| Self::parse_error(position, "invalid fixed packet id"))?;
|
||||
(PacketFrequency::Low, (raw ^ 0xffff_0000) as u16, 3, 4)
|
||||
}
|
||||
"Low" => {
|
||||
let id = *low;
|
||||
*low = low.checked_add(1).ok_or(Error::IndexOutOfRange)?;
|
||||
(PacketFrequency::Low, id, 2, 3)
|
||||
}
|
||||
"Medium" => {
|
||||
let id = *medium;
|
||||
*medium = medium.checked_add(1).ok_or(Error::IndexOutOfRange)?;
|
||||
(PacketFrequency::Medium, id, 2, 3)
|
||||
}
|
||||
"High" => {
|
||||
let id = *high;
|
||||
*high = high.checked_add(1).ok_or(Error::IndexOutOfRange)?;
|
||||
(PacketFrequency::High, id, 2, 3)
|
||||
}
|
||||
_ => return Err(Self::parse_error(position, "unknown packet frequency")),
|
||||
};
|
||||
let packet = MapPacket {
|
||||
blocks: Vec::new(),
|
||||
encoded: tokens.get(encoded_index) == Some(&"Zerocoded"),
|
||||
frequency,
|
||||
id,
|
||||
name: tokens[0].to_owned(),
|
||||
trusted: tokens.get(trusted_index) == Some(&"Trusted"),
|
||||
};
|
||||
let index = usize::from(id);
|
||||
*self.packet_mut(frequency, index)? = packet;
|
||||
Ok((frequency, index))
|
||||
}
|
||||
|
||||
fn packet_mut(
|
||||
&mut self,
|
||||
frequency: PacketFrequency,
|
||||
index: usize,
|
||||
) -> Result<&mut MapPacket, Error> {
|
||||
match frequency {
|
||||
PacketFrequency::High => self.high_maps.get_mut(index),
|
||||
PacketFrequency::Medium => self.medium_maps.get_mut(index),
|
||||
PacketFrequency::Low => self.low_maps.get_mut(index),
|
||||
}
|
||||
.ok_or(Error::IndexOutOfRange)
|
||||
}
|
||||
|
||||
fn keyword_position(&mut self, keyword: &str) -> Result<i32, Error> {
|
||||
if let Some(position) = self.keyword_positions.get(keyword) {
|
||||
return Ok(*position);
|
||||
}
|
||||
let mut hash = keyword
|
||||
.chars()
|
||||
.skip(1)
|
||||
.fold(0_i32, |hash, character| {
|
||||
hash.wrapping_add(character as i32).wrapping_mul(2)
|
||||
})
|
||||
.wrapping_mul(2)
|
||||
& 0x1fff;
|
||||
let start = hash;
|
||||
while self
|
||||
.keyword_positions
|
||||
.values()
|
||||
.any(|position| *position == hash)
|
||||
{
|
||||
hash = (hash + 1) & 0x1fff;
|
||||
if hash == start {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
}
|
||||
self.keyword_positions.insert(keyword.to_owned(), hash);
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
fn parse_field_type(value: &str) -> Option<FieldType> {
|
||||
Some(match value.to_ascii_lowercase().as_str() {
|
||||
"u8" => FieldType::U8,
|
||||
"u16" => FieldType::U16,
|
||||
"u32" => FieldType::U32,
|
||||
"u64" => FieldType::U64,
|
||||
"s8" => FieldType::S8,
|
||||
"s16" => FieldType::S16,
|
||||
"s32" => FieldType::S32,
|
||||
"f32" => FieldType::F32,
|
||||
"f64" => FieldType::F64,
|
||||
"uuid" => FieldType::UUID,
|
||||
"bool" => FieldType::BOOL,
|
||||
"vector3" => FieldType::Vector3,
|
||||
"vector3d" => FieldType::Vector3d,
|
||||
"vector4" => FieldType::Vector4,
|
||||
"quaternion" => FieldType::Quaternion,
|
||||
"ipaddr" => FieldType::IPADDR,
|
||||
"ipport" => FieldType::IPPORT,
|
||||
"variable" => FieldType::Variable,
|
||||
"fixed" => FieldType::Fixed,
|
||||
"single" => FieldType::Single,
|
||||
"multiple" => FieldType::Multiple,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
const fn parse_error(position: usize, context: &'static str) -> Error {
|
||||
Error::Parse { position, context }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
const MAP: &str = r#"
|
||||
// version header
|
||||
{
|
||||
TestHigh High Trusted Zerocoded
|
||||
{
|
||||
AgentData Single
|
||||
{ AgentID UUID }
|
||||
{ SessionID UUID }
|
||||
}
|
||||
}
|
||||
{
|
||||
TestLow Fixed 0xFFFF00F2 Untrusted Unencoded
|
||||
{
|
||||
Data Variable
|
||||
{ Value U32 }
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parses_and_resolves_protocol_commands() {
|
||||
let mut manager = ProtocolManager::empty();
|
||||
manager.parse(Cursor::new(MAP)).unwrap();
|
||||
|
||||
let high = manager.command_with_string("TestHigh".to_owned()).unwrap();
|
||||
assert_eq!(high.frequency, PacketFrequency::High);
|
||||
assert_eq!(high.id, 1);
|
||||
assert!(high.trusted);
|
||||
assert!(high.encoded);
|
||||
assert_eq!(high.blocks[0].fields.len(), 2);
|
||||
|
||||
let low = manager
|
||||
.command_with_bytes(vec![0, 0, 0, 0, 0xff, 0xff, 0, 0xf2])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(low.name, "TestLow");
|
||||
assert_eq!(low.blocks[0].count, -1);
|
||||
assert_eq!(manager.type_sizes[&FieldType::Vector3d], 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xor_decoder_tracks_key_across_chunks() {
|
||||
let input: Vec<_> = (0_u16..4097).map(|value| value as u8).collect();
|
||||
let mut encoded = Vec::new();
|
||||
ProtocolManager::decode(Cursor::new(&input), &mut encoded).unwrap();
|
||||
let mut decoded = Vec::new();
|
||||
ProtocolManager::decode(Cursor::new(encoded), &mut decoded).unwrap();
|
||||
assert_eq!(decoded, input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_short_packet_headers_do_not_panic() {
|
||||
let manager = ProtocolManager::empty();
|
||||
assert!(manager.command_with_bytes(vec![0; 5]).unwrap().is_none());
|
||||
assert!(
|
||||
manager
|
||||
.command_with_bytes(vec![0, 0, 0, 0, 0xff])
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
manager
|
||||
.command_with_bytes(vec![0, 0, 0, 0, 0xff, 0xff, 0])
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
use std::io::Read;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use libremetaverse_types::compat::ReadWrite;
|
||||
|
||||
use crate::Error;
|
||||
use crate::rendering::EndianAwareBinaryReaderSourceFormat as SourceFormat;
|
||||
|
||||
/// A synchronized binary reader whose numeric methods honor the source byte order.
|
||||
pub struct EndianAwareBinaryReader {
|
||||
stream: Mutex<Box<dyn ReadWrite + Send>>,
|
||||
format: SourceFormat,
|
||||
}
|
||||
|
||||
impl EndianAwareBinaryReader {
|
||||
pub fn new_with_stream(stream: Box<dyn ReadWrite + Send>) -> Result<Self, Error> {
|
||||
Self::new_with_stream_source_format(stream, SourceFormat::LittleEndian)
|
||||
}
|
||||
|
||||
pub fn new_with_stream_source_format(
|
||||
stream: Box<dyn ReadWrite + Send>,
|
||||
format: SourceFormat,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
stream: Mutex::new(stream),
|
||||
format,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_exact<const N: usize>(&self) -> Result<[u8; N], Error> {
|
||||
let mut bytes = [0_u8; N];
|
||||
self.stream
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.read_exact(&mut bytes)
|
||||
.map_err(|_| Error::IndexOutOfRange)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn read_double(&self) -> Result<f64, Error> {
|
||||
let bytes = self.read_exact()?;
|
||||
Ok(match self.format {
|
||||
SourceFormat::BigEndian => f64::from_be_bytes(bytes),
|
||||
SourceFormat::LittleEndian => f64::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_int16(&self) -> Result<i16, Error> {
|
||||
let bytes = self.read_exact()?;
|
||||
Ok(match self.format {
|
||||
SourceFormat::BigEndian => i16::from_be_bytes(bytes),
|
||||
SourceFormat::LittleEndian => i16::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_int32(&self) -> Result<i32, Error> {
|
||||
let bytes = self.read_exact()?;
|
||||
Ok(match self.format {
|
||||
SourceFormat::BigEndian => i32::from_be_bytes(bytes),
|
||||
SourceFormat::LittleEndian => i32::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_int64(&self) -> Result<i64, Error> {
|
||||
let bytes = self.read_exact()?;
|
||||
Ok(match self.format {
|
||||
SourceFormat::BigEndian => i64::from_be_bytes(bytes),
|
||||
SourceFormat::LittleEndian => i64::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_single(&self) -> Result<f32, Error> {
|
||||
let bytes = self.read_exact()?;
|
||||
Ok(match self.format {
|
||||
SourceFormat::BigEndian => f32::from_be_bytes(bytes),
|
||||
SourceFormat::LittleEndian => f32::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_u_int32(&self) -> Result<u32, Error> {
|
||||
let bytes = self.read_exact()?;
|
||||
Ok(match self.format {
|
||||
SourceFormat::BigEndian => u32::from_be_bytes(bytes),
|
||||
SourceFormat::LittleEndian => u32::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_string_with_method(&self) -> Result<String, Error> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut stream = self
|
||||
.stream
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
loop {
|
||||
let mut byte = [0_u8; 1];
|
||||
stream
|
||||
.read_exact(&mut byte)
|
||||
.map_err(|_| Error::IndexOutOfRange)?;
|
||||
if byte[0] == 0 {
|
||||
break;
|
||||
}
|
||||
bytes.push(byte[0]);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&bytes).into_owned())
|
||||
}
|
||||
|
||||
pub fn read_string_with_int32(&self, size: i32) -> Result<String, Error> {
|
||||
let size = usize::try_from(size).map_err(|_| Error::Argument)?;
|
||||
let mut bytes = vec![0_u8; size];
|
||||
let mut stream = self
|
||||
.stream
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let mut read = 0;
|
||||
while read < size {
|
||||
let count = stream
|
||||
.read(&mut bytes[read..])
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
read += count;
|
||||
}
|
||||
bytes.truncate(read);
|
||||
Ok(String::from_utf8_lossy(&bytes).trim().to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reads_both_byte_orders_and_strings() {
|
||||
let mut big = Vec::new();
|
||||
big.extend_from_slice(&(-123_i16).to_be_bytes());
|
||||
big.extend_from_slice(&42.5_f32.to_be_bytes());
|
||||
big.extend_from_slice(b"hello\0 padded ");
|
||||
let reader = EndianAwareBinaryReader::new_with_stream_source_format(
|
||||
Box::new(Cursor::new(big)),
|
||||
SourceFormat::BigEndian,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(reader.read_int16().unwrap(), -123);
|
||||
assert_eq!(reader.read_single().unwrap(), 42.5);
|
||||
assert_eq!(reader.read_string_with_method().unwrap(), "hello");
|
||||
assert_eq!(reader.read_string_with_int32(8).unwrap(), "padded");
|
||||
|
||||
let reader = EndianAwareBinaryReader::new_with_stream(Box::new(Cursor::new(
|
||||
0x1234_5678_u32.to_le_bytes().to_vec(),
|
||||
)))
|
||||
.unwrap();
|
||||
assert_eq!(reader.read_u_int32().unwrap(), 0x1234_5678);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_short_numeric_reads() {
|
||||
let reader =
|
||||
EndianAwareBinaryReader::new_with_stream(Box::new(Cursor::new(vec![1, 2]))).unwrap();
|
||||
assert_eq!(reader.read_int32(), Err(Error::IndexOutOfRange));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Cross-platform renderer discovery without CLR assembly loading.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::Error;
|
||||
use crate::rendering::IRendering;
|
||||
|
||||
const BUILTIN_SIMPLE: &str = "builtin:simple";
|
||||
|
||||
pub(crate) fn list_renderers(path: &str) -> Result<Vec<String>, Error> {
|
||||
let directory = Path::new(path);
|
||||
if !directory.is_dir() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut renderers = vec![BUILTIN_SIMPLE.to_owned()];
|
||||
let entries = std::fs::read_dir(directory).map_err(|_| Error::Argument)?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|_| Error::Argument)?;
|
||||
let file_type = entry.file_type().map_err(|_| Error::Argument)?;
|
||||
if !file_type.is_file() {
|
||||
continue;
|
||||
}
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy().to_ascii_lowercase();
|
||||
if is_simple_artifact(&name) {
|
||||
renderers.push(entry.path().to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
renderers.sort();
|
||||
renderers.dedup();
|
||||
Ok(renderers)
|
||||
}
|
||||
|
||||
pub(crate) fn load_renderer(filename: &str) -> Result<Box<dyn IRendering>, Error> {
|
||||
let normalized = Path::new(filename)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or(filename)
|
||||
.to_ascii_lowercase();
|
||||
if filename.eq_ignore_ascii_case(BUILTIN_SIMPLE)
|
||||
|| filename.eq_ignore_ascii_case("simple")
|
||||
|| filename.eq_ignore_ascii_case("LibreMetaverse.Rendering.Simple")
|
||||
|| is_simple_artifact(&normalized)
|
||||
{
|
||||
return Ok(Box::new(
|
||||
crate::builtin_simple_renderer::SimpleRenderer::native_new()?,
|
||||
));
|
||||
}
|
||||
Err(Error::Rendering {
|
||||
source: libremetaverse_types::UUID::zero(),
|
||||
context: "unknown or unsupported renderer",
|
||||
})
|
||||
}
|
||||
|
||||
fn is_simple_artifact(name: &str) -> bool {
|
||||
name.contains("libremetaverse_rendering_simple")
|
||||
|| name.contains("libremetaverse-rendering-simple")
|
||||
|| name.contains("libremetaverse.rendering.simple")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn built_in_renderer_is_discoverable_and_constructible() {
|
||||
let renderers = list_renderers(env!("CARGO_MANIFEST_DIR")).unwrap();
|
||||
assert!(renderers.iter().any(|name| name == BUILTIN_SIMPLE));
|
||||
let renderer = load_renderer(BUILTIN_SIMPLE).unwrap();
|
||||
let mut vertices = Vec::new();
|
||||
renderer
|
||||
.transform_tex_coords(
|
||||
&mut vertices,
|
||||
libremetaverse_types::Vector3::zero(),
|
||||
crate::PrimitiveTextureEntryFace::new(None).unwrap(),
|
||||
libremetaverse_types::Vector3::one(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovery_recognizes_platform_artifact_names() {
|
||||
assert!(is_simple_artifact("liblibremetaverse_rendering_simple.so"));
|
||||
assert!(is_simple_artifact("libremetaverse_rendering_simple.dll"));
|
||||
assert!(is_simple_artifact(
|
||||
"liblibremetaverse_rendering_simple.dylib"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Renderer metadata carried by native renderer registrations.
|
||||
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct RendererNameAttribute(String);
|
||||
|
||||
impl RendererNameAttribute {
|
||||
pub fn new(name: String) -> Result<Self, Error> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Self(name))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RendererNameAttribute {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats the flags the same way .NET formats a valid `FaceMask` value.
|
||||
/// Unknown bit combinations remain numeric instead of inventing flag names.
|
||||
#[must_use]
|
||||
pub fn face_mask_to_string(mask: crate::rendering::FaceMask) -> String {
|
||||
let known = [
|
||||
(crate::rendering::FaceMask::SINGLE.0, "Single"),
|
||||
(crate::rendering::FaceMask::CAP.0, "Cap"),
|
||||
(crate::rendering::FaceMask::END.0, "End"),
|
||||
(crate::rendering::FaceMask::SIDE.0, "Side"),
|
||||
(crate::rendering::FaceMask::INNER.0, "Inner"),
|
||||
(crate::rendering::FaceMask::OUTER.0, "Outer"),
|
||||
(crate::rendering::FaceMask::HOLLOW.0, "Hollow"),
|
||||
(crate::rendering::FaceMask::OPEN.0, "Open"),
|
||||
(crate::rendering::FaceMask::FLAT.0, "Flat"),
|
||||
(crate::rendering::FaceMask::TOP.0, "Top"),
|
||||
(crate::rendering::FaceMask::BOTTOM.0, "Bottom"),
|
||||
];
|
||||
if mask.0 == 0 {
|
||||
return "0".to_owned();
|
||||
}
|
||||
let all_known = known.iter().fold(0, |bits, (flag, _)| bits | flag);
|
||||
if mask.0 & !all_known != 0 {
|
||||
return mask.0.to_string();
|
||||
}
|
||||
known
|
||||
.into_iter()
|
||||
.filter_map(|(flag, name)| (mask.0 & flag != 0).then_some(name))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn renderer_name_rejects_empty_metadata() {
|
||||
assert!(RendererNameAttribute::new(String::new()).is_err());
|
||||
assert_eq!(
|
||||
RendererNameAttribute::new("Simple".to_owned())
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"Simple"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_mask_uses_dotnet_style_flag_names() {
|
||||
let mask = crate::rendering::FaceMask(
|
||||
crate::rendering::FaceMask::CAP.0 | crate::rendering::FaceMask::OUTER.0,
|
||||
);
|
||||
assert_eq!(face_mask_to_string(mask), "Cap, Outer");
|
||||
assert_eq!(face_mask_to_string(crate::rendering::FaceMask(0)), "0");
|
||||
assert_eq!(
|
||||
face_mask_to_string(crate::rendering::FaceMask(1 << 20)),
|
||||
"1048576"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use libremetaverse_types::compat::{CancellationToken, Task};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
pub struct Repeat;
|
||||
|
||||
impl Repeat {
|
||||
pub async fn interval_with_time_span_action_cancellation_token_boolean(
|
||||
poll_interval: Duration,
|
||||
action: Box<dyn Fn() + Send + Sync>,
|
||||
token: CancellationToken,
|
||||
immediately: Option<bool>,
|
||||
) -> Result<(), Error> {
|
||||
if poll_interval.is_zero() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if immediately.unwrap_or(false) {
|
||||
action();
|
||||
}
|
||||
loop {
|
||||
tokio::select! {
|
||||
() = tokio::time::sleep(poll_interval) => action(),
|
||||
() = token.cancelled() => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn interval_with_time_span_func_cancellation_token_boolean(
|
||||
poll_interval: Duration,
|
||||
async_action: Box<dyn Fn() -> Task<()> + Send + Sync>,
|
||||
token: CancellationToken,
|
||||
immediately: Option<bool>,
|
||||
) -> Result<(), Error> {
|
||||
if poll_interval.is_zero() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if immediately.unwrap_or(false) {
|
||||
async_action().await?;
|
||||
}
|
||||
loop {
|
||||
tokio::select! {
|
||||
() = tokio::time::sleep(poll_interval) => async_action().await?,
|
||||
() = token.cancelled() => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use libremetaverse_types::compat::CancellationTokenSource;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn repeats_immediately_and_stops_on_cancellation() {
|
||||
let source = CancellationTokenSource::new();
|
||||
let token = source.token();
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let action_count = Arc::clone(&count);
|
||||
let cancel = source.clone();
|
||||
Repeat::interval_with_time_span_action_cancellation_token_boolean(
|
||||
Duration::from_millis(1),
|
||||
Box::new(move || {
|
||||
if action_count.fetch_add(1, Ordering::Relaxed) >= 2 {
|
||||
cancel.cancel();
|
||||
}
|
||||
}),
|
||||
token,
|
||||
Some(true),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count.load(Ordering::Relaxed), 3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//! Runtime-neutral singleton registration for native `GridClient` composition.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{Error, GridClient, Settings};
|
||||
|
||||
/// Factory stored by service collections for lazy `GridClient` construction.
|
||||
pub type GridClientFactory = Arc<dyn Fn() -> Result<GridClient, Error> + Send + Sync>;
|
||||
|
||||
/// Object-safe service collection contract used by the mapped DI extension.
|
||||
pub trait IServiceCollection: Send {
|
||||
/// Registers the concrete `GridClient` singleton factory.
|
||||
fn add_grid_client_singleton(&mut self, factory: GridClientFactory) -> Result<(), Error>;
|
||||
|
||||
/// Registers `IGridClient` as an alias of the concrete singleton.
|
||||
fn add_i_grid_client_alias(&mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// Small native service collection suitable for applications without a DI crate.
|
||||
#[derive(Default)]
|
||||
pub struct ServiceCollection {
|
||||
client_factory: Option<GridClientFactory>,
|
||||
client: Mutex<Option<GridClient>>,
|
||||
interface_alias: bool,
|
||||
}
|
||||
|
||||
impl ServiceCollection {
|
||||
/// Resolves the lazily-created concrete client singleton.
|
||||
pub fn resolve_grid_client(&self) -> Result<GridClient, Error> {
|
||||
let mut singleton = self
|
||||
.client
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(client) = singleton.as_ref() {
|
||||
return Ok(client.clone());
|
||||
}
|
||||
let factory = self
|
||||
.client_factory
|
||||
.as_ref()
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
let client = factory()?;
|
||||
*singleton = Some(client.clone());
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Resolves the interface registration to the same native singleton.
|
||||
pub fn resolve_i_grid_client(&self) -> Result<GridClient, Error> {
|
||||
if !self.interface_alias {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
self.resolve_grid_client()
|
||||
}
|
||||
}
|
||||
|
||||
impl IServiceCollection for ServiceCollection {
|
||||
fn add_grid_client_singleton(&mut self, factory: GridClientFactory) -> Result<(), Error> {
|
||||
if self.client_factory.is_some() {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
self.client_factory = Some(factory);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_i_grid_client_alias(&mut self) -> Result<(), Error> {
|
||||
if self.client_factory.is_none() || self.interface_alias {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
self.interface_alias = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_grid_client(
|
||||
mut services: Box<dyn IServiceCollection>,
|
||||
configure: Option<Box<dyn Fn(&mut Settings) + Send + Sync>>,
|
||||
) -> Result<Box<dyn IServiceCollection>, Error> {
|
||||
let configure: Option<Arc<dyn Fn(&mut Settings) + Send + Sync>> = configure.map(Arc::from);
|
||||
let factory: GridClientFactory = Arc::new(move || {
|
||||
let mut settings = Settings::default();
|
||||
if let Some(configure) = &configure {
|
||||
configure(&mut settings);
|
||||
}
|
||||
GridClient::builder()
|
||||
.with_settings(settings)
|
||||
.build()
|
||||
.map_err(Into::into)
|
||||
});
|
||||
services.add_grid_client_singleton(factory)?;
|
||||
services.add_i_grid_client_alias()?;
|
||||
Ok(services)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
|
||||
#[derive(Default)]
|
||||
struct RecordingCollection {
|
||||
factory: Arc<Mutex<Option<GridClientFactory>>>,
|
||||
alias_registered: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl IServiceCollection for RecordingCollection {
|
||||
fn add_grid_client_singleton(&mut self, factory: GridClientFactory) -> Result<(), Error> {
|
||||
*self
|
||||
.factory
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(factory);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_i_grid_client_alias(&mut self) -> Result<(), Error> {
|
||||
self.alias_registered.store(true, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registers_one_lazy_client_and_interface_alias() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&calls);
|
||||
let collection = RecordingCollection::default();
|
||||
let recorded_factory = Arc::clone(&collection.factory);
|
||||
let alias_registered = Arc::clone(&collection.alias_registered);
|
||||
let _services = add_grid_client(
|
||||
Box::new(collection),
|
||||
Some(Box::new(move |settings| {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
settings.default_effect_color.r = 0.25;
|
||||
})),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(alias_registered.load(Ordering::Relaxed));
|
||||
assert_eq!(calls.load(Ordering::Relaxed), 0);
|
||||
|
||||
let factory = recorded_factory
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
.expect("GridClient factory must be registered");
|
||||
let client = factory().unwrap();
|
||||
assert_eq!(calls.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(client.settings_ref().default_effect_color.r, 0.25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concrete_collection_resolves_same_runtime() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&calls);
|
||||
let mut services = ServiceCollection::default();
|
||||
services
|
||||
.add_grid_client_singleton(Arc::new(move || {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
GridClient::builder().build().map_err(Into::into)
|
||||
}))
|
||||
.unwrap();
|
||||
services.add_i_grid_client_alias().unwrap();
|
||||
let concrete = services.resolve_grid_client().unwrap();
|
||||
let interface = services.resolve_i_grid_client().unwrap();
|
||||
assert_eq!(concrete.settings_ref(), interface.settings_ref());
|
||||
assert_eq!(calls.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Process-wide simulator cache ownership and inactivity reaping.
|
||||
|
||||
use crate::{Parcel, Primitive, Settings, Simulator, SimulatorDataPool};
|
||||
use libremetaverse_types::Error;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak};
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
const REAPER_INTERVAL: Duration = Duration::from_secs(180);
|
||||
const ACTIVE_TIMESTAMP_SECONDS: u64 = 253_402_300_799;
|
||||
|
||||
fn mutex<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
value
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
struct PoolRecord {
|
||||
active_clients: i32,
|
||||
inactive_since: Option<(Instant, SystemTime)>,
|
||||
prim_cache: HashMap<u32, Primitive>,
|
||||
parcels: HashMap<i32, Parcel>,
|
||||
parcel_map: Vec<i32>,
|
||||
downloading_parcel_map: bool,
|
||||
}
|
||||
|
||||
impl PoolRecord {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
active_clients: 0,
|
||||
inactive_since: None,
|
||||
prim_cache: HashMap::new(),
|
||||
parcels: HashMap::new(),
|
||||
parcel_map: vec![0; 64 * 64],
|
||||
downloading_parcel_map: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(&self, handle: u64) -> SimulatorDataPool {
|
||||
SimulatorDataPool {
|
||||
active_clients: self.active_clients,
|
||||
downloading_parcel_map: self.downloading_parcel_map,
|
||||
handle,
|
||||
inactive_since: self.inactive_since.map_or_else(
|
||||
|| UNIX_EPOCH + Duration::from_secs(ACTIVE_TIMESTAMP_SECONDS),
|
||||
|(_, timestamp)| timestamp,
|
||||
),
|
||||
parcel_map: self.parcel_map.clone(),
|
||||
parcels: std::sync::RwLock::new(self.parcels.clone()),
|
||||
prim_cache: self.prim_cache.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Runtime {
|
||||
records: Mutex<HashMap<u64, PoolRecord>>,
|
||||
worker: Mutex<Option<JoinHandle<()>>>,
|
||||
wake: Condvar,
|
||||
stopped: Mutex<bool>,
|
||||
}
|
||||
|
||||
fn runtime() -> &'static Arc<Runtime> {
|
||||
static RUNTIME: OnceLock<Arc<Runtime>> = OnceLock::new();
|
||||
RUNTIME.get_or_init(|| {
|
||||
Arc::new(Runtime {
|
||||
records: Mutex::new(HashMap::new()),
|
||||
worker: Mutex::new(None),
|
||||
wake: Condvar::new(),
|
||||
stopped: Mutex::new(false),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn reap(runtime: &Runtime) {
|
||||
let timeout = Duration::from_millis(
|
||||
u64::try_from(Settings::simulator_pool_timeout().max(1)).unwrap_or(1),
|
||||
);
|
||||
mutex(&runtime.records).retain(|_, record| {
|
||||
record
|
||||
.inactive_since
|
||||
.is_none_or(|(instant, _)| instant.elapsed() < timeout)
|
||||
});
|
||||
}
|
||||
|
||||
fn ensure_reaper() {
|
||||
let runtime = Arc::clone(runtime());
|
||||
let mut worker = mutex(&runtime.worker);
|
||||
if worker.is_some() {
|
||||
return;
|
||||
}
|
||||
*mutex(&runtime.stopped) = false;
|
||||
let weak: Weak<Runtime> = Arc::downgrade(&runtime);
|
||||
*worker = Some(std::thread::spawn(move || {
|
||||
let Some(runtime) = weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let mut stopped = mutex(&runtime.stopped);
|
||||
loop {
|
||||
let (guard, _) = runtime
|
||||
.wake
|
||||
.wait_timeout(stopped, REAPER_INTERVAL)
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
stopped = guard;
|
||||
if *stopped {
|
||||
return;
|
||||
}
|
||||
drop(stopped);
|
||||
reap(&runtime);
|
||||
stopped = mutex(&runtime.stopped);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub(crate) fn snapshots() -> HashMap<u64, SimulatorDataPool> {
|
||||
reap(runtime());
|
||||
mutex(&runtime().records)
|
||||
.iter()
|
||||
.map(|(handle, record)| (*handle, record.snapshot(*handle)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn get(handle: u64) -> Result<SimulatorDataPool, Error> {
|
||||
let mut records = mutex(&runtime().records);
|
||||
let record = records.entry(handle).or_insert_with(PoolRecord::new);
|
||||
Ok(record.snapshot(handle))
|
||||
}
|
||||
|
||||
pub(crate) fn add(simulator: Simulator) -> Result<(), Error> {
|
||||
ensure_reaper();
|
||||
let mut records = mutex(&runtime().records);
|
||||
let record = records
|
||||
.entry(simulator.handle)
|
||||
.or_insert_with(PoolRecord::new);
|
||||
record.active_clients = record.active_clients.saturating_add(1).max(1);
|
||||
record.inactive_since = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn release(simulator: Simulator) -> Result<(), Error> {
|
||||
let mut records = mutex(&runtime().records);
|
||||
let record = records
|
||||
.entry(simulator.handle)
|
||||
.or_insert_with(PoolRecord::new);
|
||||
record.active_clients = record.active_clients.saturating_sub(1);
|
||||
if record.active_clients <= 0 {
|
||||
record.active_clients = 0;
|
||||
record.inactive_since = Some((Instant::now(), SystemTime::now()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown() -> Result<(), Error> {
|
||||
let runtime = runtime();
|
||||
*mutex(&runtime.stopped) = true;
|
||||
runtime.wake.notify_all();
|
||||
if let Some(worker) = mutex(&runtime.worker).take()
|
||||
&& worker.thread().id() != std::thread::current().id()
|
||||
{
|
||||
worker.join().map_err(|_| Error::InvalidOperation)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
//! Stable built-in viewer sound identifiers from the pinned C# source.
|
||||
|
||||
use libremetaverse_types::UUID;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const SOUNDS: &[(&str, &str)] = &[
|
||||
("BELL_TING", "ed124764-705d-d497-167a-182cd9fa2e6c"),
|
||||
("CLICK", "4c8c3c77-de8d-bde2-b9b8-32635e0fd4a6"),
|
||||
(
|
||||
"HEALTH_REDUCTION_FEMALE",
|
||||
"219c5d93-6c09-31c5-fb3f-c5fe7495c115",
|
||||
),
|
||||
(
|
||||
"HEALTH_REDUCTION_MALE",
|
||||
"e057c244-5768-1056-c37e-1537454eeb62",
|
||||
),
|
||||
("IM_START", "c825dfbc-9827-7e02-6507-3713d18916c1"),
|
||||
(
|
||||
"INSTANT_MESSAGE_NOTIFICATION",
|
||||
"67cc2844-00f3-2b3c-b991-6418d01e1bb7",
|
||||
),
|
||||
("INVALID_OPERATION", "4174f859-0d3d-c517-c424-72923dc21f65"),
|
||||
("KEYBOARD_LOOP", "5e191c7b-8996-9ced-a177-b2ac32bfea06"),
|
||||
(
|
||||
"MONEY_REDUCTION_COINS",
|
||||
"77a018af-098e-c037-51a6-178f05877c6f",
|
||||
),
|
||||
(
|
||||
"MONEY_INCREASE_CASH_REGISTER_BELL",
|
||||
"104974e3-dfda-428b-99ee-b0d4e748d3a3",
|
||||
),
|
||||
("NULL_KEYSTROKE", "2ca849ba-2885-4bc3-90ef-d4987a5b983a"),
|
||||
("OBJECT_COLLISION", "be582e5d-b123-41a2-a150-454c39e961c8"),
|
||||
(
|
||||
"OBJECT_COLLISION_RUBBER",
|
||||
"212b6d1e-8d9c-4986-b3aa-f3c6df8d987d",
|
||||
),
|
||||
(
|
||||
"OBJECT_COLLISION_PLASTIC",
|
||||
"d55c7f3c-e1c3-4ddc-9eff-9ef805d9190e",
|
||||
),
|
||||
(
|
||||
"OBJECT_COLLISION_FLESH",
|
||||
"2d8c6f51-149e-4e23-8413-93a379b42b67",
|
||||
),
|
||||
(
|
||||
"OBJECT_COLLISION_WOOD_SPLINTERING",
|
||||
"6f00669f-15e0-4793-a63e-c03f62fee43a",
|
||||
),
|
||||
(
|
||||
"OBJECT_COLLISION_GLASS_BREAK",
|
||||
"85cda060-b393-48e6-81c8-2cfdfb275351",
|
||||
),
|
||||
(
|
||||
"OBJECT_COLLISION_METAL_CLUNK",
|
||||
"d1375446-1c4d-470b-9135-30132433b678",
|
||||
),
|
||||
(
|
||||
"OBJECT_CREATE_WHOOSH",
|
||||
"3c8fc726-1fd6-862d-fa01-16c5b2568db6",
|
||||
),
|
||||
(
|
||||
"OBJECT_DELETE_SHAKE",
|
||||
"0cb7b00a-4c10-6948-84de-a93c09af2ba9",
|
||||
),
|
||||
("OBJECT_REZ", "f4a0660f-5446-dea2-80b7-6482a082803c"),
|
||||
(
|
||||
"PIE_MENU_APPEAR_DING",
|
||||
"8eaed61f-92ff-6485-de83-4dcc938a478e",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT",
|
||||
"d9f73cf8-17b4-6f7a-1565-7951226c305d",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT1",
|
||||
"f6ba9816-dcaf-f755-7b67-51b31b6233e5",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT2",
|
||||
"7aff2265-d05b-8b72-63c7-dbf96dc2f21f",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT3",
|
||||
"09b2184e-8601-44e2-afbb-ce37434b8ba1",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT4",
|
||||
"bbe4c7fc-7044-b05e-7b89-36924a67593c",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT5",
|
||||
"d166039b-b4f5-c2ec-4911-c85c727b016c",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT6",
|
||||
"242af82b-43c2-9a3b-e108-3b0c7e384981",
|
||||
),
|
||||
(
|
||||
"PIE_MENU_SLICE_HIGHLIGHT7",
|
||||
"c1f334fb-a5be-8fe7-22b3-29631c21cf0b",
|
||||
),
|
||||
("SNAPSHOT", "3d09f582-3851-c0e0-f5ba-277ac5c73fb4"),
|
||||
(
|
||||
"TELEPORT_TEXTURE_APPLY",
|
||||
"d7a9a565-a013-2a69-797d-5332baa1a947",
|
||||
),
|
||||
("THUNDER", "e95c96a5-293c-bb7a-57ad-ce2e785ad85f"),
|
||||
("WINDOW_CLOSE", "2c346eda-b60c-ab33-1119-b8941916a499"),
|
||||
("WINDOW_OPEN", "c80260ba-41fd-8a46-768a-6bf236360e3a"),
|
||||
("ZIPPER", "6cf2be26-90cb-2669-a599-f5ab7698225f"),
|
||||
];
|
||||
|
||||
pub(crate) fn sound(name: &str) -> UUID {
|
||||
let value = SOUNDS
|
||||
.iter()
|
||||
.find_map(|(candidate, value)| (*candidate == name).then_some(*value))
|
||||
.unwrap_or_else(|| panic!("unknown built-in sound {name}"));
|
||||
UUID::new_with_string(value.to_owned()).expect("built-in sound UUID is valid")
|
||||
}
|
||||
|
||||
pub(crate) fn sounds() -> HashMap<UUID, String> {
|
||||
SOUNDS
|
||||
.iter()
|
||||
.map(|(name, _)| (sound(name), (*name).to_owned()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sound_catalog_is_complete_and_unique() {
|
||||
let catalog = sounds();
|
||||
assert_eq!(catalog.len(), SOUNDS.len());
|
||||
assert_eq!(catalog[&sound("BELL_TING")], "BELL_TING");
|
||||
assert_eq!(catalog[&sound("ZIPPER")], "ZIPPER");
|
||||
}
|
||||
}
|
||||
@@ -417,6 +417,7 @@ impl SoundManagerInner {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::float_cmp)] // Sound fixtures use exactly representable values.
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//! Thread-safe packet/message utilization counters.
|
||||
|
||||
use crate::Error;
|
||||
use crate::stats::Type;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct UtilizationStatisticsStat {
|
||||
type_: Type,
|
||||
tx_count: i64,
|
||||
rx_count: i64,
|
||||
tx_bytes: i64,
|
||||
rx_bytes: i64,
|
||||
}
|
||||
|
||||
impl Default for UtilizationStatisticsStat {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
type_: Type::Packet,
|
||||
tx_count: 0,
|
||||
rx_count: 0,
|
||||
tx_bytes: 0,
|
||||
rx_bytes: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UtilizationStatisticsStat {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn type_(&self) -> Type {
|
||||
self.type_
|
||||
}
|
||||
pub fn set_type_(&mut self, value: Type) {
|
||||
self.type_ = value;
|
||||
}
|
||||
#[must_use]
|
||||
pub fn tx_count(&self) -> i64 {
|
||||
self.tx_count
|
||||
}
|
||||
pub fn set_tx_count(&mut self, value: i64) {
|
||||
self.tx_count = value;
|
||||
}
|
||||
#[must_use]
|
||||
pub fn rx_count(&self) -> i64 {
|
||||
self.rx_count
|
||||
}
|
||||
pub fn set_rx_count(&mut self, value: i64) {
|
||||
self.rx_count = value;
|
||||
}
|
||||
#[must_use]
|
||||
pub fn tx_bytes(&self) -> i64 {
|
||||
self.tx_bytes
|
||||
}
|
||||
pub fn set_tx_bytes(&mut self, value: i64) {
|
||||
self.tx_bytes = value;
|
||||
}
|
||||
#[must_use]
|
||||
pub fn rx_bytes(&self) -> i64 {
|
||||
self.rx_bytes
|
||||
}
|
||||
pub fn set_rx_bytes(&mut self, value: i64) {
|
||||
self.rx_bytes = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct UtilizationStatistics {
|
||||
statistics: Arc<Mutex<HashMap<String, UtilizationStatisticsStat>>>,
|
||||
}
|
||||
|
||||
impl UtilizationStatistics {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
pub fn get_statistics(&self) -> Result<HashMap<String, UtilizationStatisticsStat>, Error> {
|
||||
Ok(self
|
||||
.statistics
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone())
|
||||
}
|
||||
|
||||
pub fn update(&self, key: String, type_: Type, tx_bytes: i64, rx_bytes: i64) {
|
||||
let mut statistics = self
|
||||
.statistics
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let statistic = statistics.entry(key).or_default();
|
||||
statistic.type_ = type_;
|
||||
if rx_bytes > 0 {
|
||||
statistic.rx_count = statistic.rx_count.wrapping_add(1);
|
||||
statistic.rx_bytes = statistic.rx_bytes.wrapping_add(rx_bytes);
|
||||
}
|
||||
if tx_bytes > 0 {
|
||||
statistic.tx_count = statistic.tx_count.wrapping_add(1);
|
||||
statistic.tx_bytes = statistic.tx_bytes.wrapping_add(tx_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn updates_count_only_positive_transfers_and_snapshots_are_independent() {
|
||||
let statistics = UtilizationStatistics::new().unwrap();
|
||||
statistics.update("PacketAck".to_owned(), Type::Packet, 10, 0);
|
||||
statistics.update("PacketAck".to_owned(), Type::Packet, 5, 20);
|
||||
statistics.update("PacketAck".to_owned(), Type::Packet, -1, -1);
|
||||
let snapshot = statistics.get_statistics().unwrap();
|
||||
let stat = &snapshot["PacketAck"];
|
||||
assert_eq!(stat.tx_count(), 2);
|
||||
assert_eq!(stat.tx_bytes(), 15);
|
||||
assert_eq!(stat.rx_count(), 1);
|
||||
assert_eq!(stat.rx_bytes(), 20);
|
||||
assert_eq!(stat.type_(), Type::Packet);
|
||||
}
|
||||
}
|
||||
@@ -379,6 +379,7 @@ impl TerrainManagerInner {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::cast_precision_loss, clippy::float_cmp)] // Bounded terrain fixtures intentionally use f32.
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//! Compatibility facade over the native capability-backed texture downloader.
|
||||
|
||||
use crate::{Error, GridClient};
|
||||
use libremetaverse_types::UUID;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
pub struct TexturePipeline {
|
||||
client: GridClient,
|
||||
running: AtomicBool,
|
||||
}
|
||||
|
||||
impl TexturePipeline {
|
||||
pub fn new(client: GridClient) -> Result<Self, Error> {
|
||||
// Resolve the manager eagerly so construction fails for a disposed or
|
||||
// incompletely initialized client instead of at the first abort.
|
||||
client.native_assets()?;
|
||||
Ok(Self {
|
||||
client,
|
||||
running: AtomicBool::new(false),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn abort_texture_request(&self, texture_id: UUID) -> Result<(), Error> {
|
||||
if texture_id == UUID::zero() {
|
||||
return Ok(());
|
||||
}
|
||||
self.client
|
||||
.native_assets()?
|
||||
.request_image_cancel(texture_id)
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) -> Result<(), Error> {
|
||||
if self.running.swap(false, Ordering::AcqRel) {
|
||||
self.client.native_assets()?.native_cancel_image_requests();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn startup(&self) -> Result<(), Error> {
|
||||
self.client.native_assets()?;
|
||||
if self.client.settings_ref().texture_pipeline.enabled {
|
||||
self.running.store(true, Ordering::Release);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn transfer_count(&self) -> i32 {
|
||||
self.client
|
||||
.native_assets()
|
||||
.map_or(0, |assets| assets.native_image_transfer_count())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TexturePipeline {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.shutdown();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,78 @@
|
||||
//! MessagePack formatter for protocol UUID values.
|
||||
|
||||
use crate::Error;
|
||||
use crate::message_pack::{
|
||||
IMessagePackFormatter, MessagePackReader, MessagePackSerializerOptions, MessagePackWriter,
|
||||
};
|
||||
use libremetaverse_types::UUID;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct UUIDFormatter;
|
||||
|
||||
impl UUIDFormatter {
|
||||
#[must_use]
|
||||
pub const fn instance() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
pub fn serialize(
|
||||
&self,
|
||||
writer: &mut MessagePackWriter,
|
||||
value: UUID,
|
||||
_options: MessagePackSerializerOptions,
|
||||
) -> Result<(), Error> {
|
||||
writer.write_bytes(&value.get_bytes()?)
|
||||
}
|
||||
|
||||
pub fn deserialize(
|
||||
&mut self,
|
||||
reader: &mut MessagePackReader,
|
||||
_options: MessagePackSerializerOptions,
|
||||
) -> Result<UUID, Error> {
|
||||
let bytes = reader.read_bytes()?.ok_or(Error::Argument)?;
|
||||
UUID::from_bytes(bytes, 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl IMessagePackFormatter<UUID> for UUIDFormatter {
|
||||
fn serialize(
|
||||
&self,
|
||||
writer: &mut MessagePackWriter,
|
||||
value: UUID,
|
||||
options: MessagePackSerializerOptions,
|
||||
) -> Result<(), Error> {
|
||||
self.serialize(writer, value, options)
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
reader: &mut MessagePackReader,
|
||||
options: MessagePackSerializerOptions,
|
||||
) -> Result<UUID, Error> {
|
||||
self.deserialize(reader, options)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn uuid_round_trips_as_messagepack_binary() {
|
||||
let expected = UUID::parse("00112233-4455-6677-8899-aabbccddeeff".to_owned()).unwrap();
|
||||
let mut writer = MessagePackWriter::new();
|
||||
UUIDFormatter::instance()
|
||||
.serialize(&mut writer, expected, MessagePackSerializerOptions)
|
||||
.unwrap();
|
||||
assert_eq!(&writer.as_slice()[..2], &[0xc4, 16]);
|
||||
let mut reader = MessagePackReader::new(writer.into_bytes());
|
||||
let actual = UUIDFormatter::instance()
|
||||
.deserialize(&mut reader, MessagePackSerializerOptions)
|
||||
.unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
@@ -23263,6 +23263,9 @@ impl VisualParams {
|
||||
)
|
||||
}
|
||||
}
|
||||
pub(crate) fn param_by_id(id: i32) -> Option<&'static VisualParam> {
|
||||
generated_params().get(&id)
|
||||
}
|
||||
pub(crate) fn decode_visual_params(bytes: &[u8]) -> HashMap<i32, f32> {
|
||||
let params = generated_params();
|
||||
let mut result = HashMap::with_capacity(GROUP0_PARAM_IDS.len());
|
||||
@@ -23313,6 +23316,7 @@ pub(crate) fn decode_visual_params(bytes: &[u8]) -> HashMap<i32, f32> {
|
||||
}
|
||||
pub(crate) fn new_avatar() -> Result<crate::Avatar, crate::Error> {
|
||||
Ok(crate::Avatar {
|
||||
base: crate::Primitive::new_with_constructor()?,
|
||||
animations: Vec::new(),
|
||||
appearance_flags: crate::AppearanceFlags::NONE,
|
||||
appearance_version: 0,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Source SHA-256: 74fa1275cd1f6c2a372af2fefc011a3e764f028a8957291d61f599bcb023a8ff
|
||||
// Source SHA-256: 7b67b72c2ad652626126d0e4eda7e8c26fc94c28f0f915238b2925f80a3d4af2
|
||||
|
||||
#![allow(clippy::float_cmp)] // Wire fixtures use exactly representable values.
|
||||
|
||||
use super::*;
|
||||
use libremetaverse_structured_data::{OSD, OSDMap};
|
||||
use libremetaverse_types::{UUID, Vector3};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user