Implement native AgentManager services (#59)
All checks were successful
Native code generation / deterministic (push) Successful in 12m0s
Imaging and meshing gate / native (push) Successful in 4m3s
Native Rust workspace compile / compile (push) Successful in 4m4s

This commit is contained in:
2026-08-09 22:07:51 +00:00
parent c8b2317e13
commit 9c9d5b91f1
16 changed files with 7464 additions and 1531 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -256,6 +256,12 @@ impl Caps {
Ok(read(&self.inner.caps).get(&capability).cloned())
}
pub(crate) fn seed_request_finished(&self) -> bool {
mutex(&self.inner.seed_task)
.as_ref()
.is_none_or(|task| task.handle.is_finished())
}
pub fn disconnect(&self, immediate: bool) -> Result<(), Error> {
self.inner.disconnect(immediate);
Ok(())

View File

@@ -117,6 +117,7 @@ struct ClientRuntime {
http_caps_client: Mutex<crate::caps_http::HttpCapsClient>,
caps_rate_limiter: Mutex<crate::caps_http::CapsRateLimiter>,
network_manager: Mutex<std::sync::Weak<crate::network_manager::NetworkManagerInner>>,
agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>,
shutdown_complete: Condvar,
shutdown_wait: Mutex<()>,
}
@@ -144,6 +145,7 @@ impl ClientRuntime {
http_caps_client: Mutex::new(http_caps_client),
caps_rate_limiter: Mutex::new(caps_rate_limiter),
network_manager: Mutex::new(std::sync::Weak::new()),
agent_manager: Mutex::new(std::sync::Weak::new()),
shutdown_complete: Condvar::new(),
shutdown_wait: Mutex::new(()),
}
@@ -242,11 +244,24 @@ impl ClientRuntime {
/// Construction is side-effect free: it creates no runtime, task, socket, or
/// HTTP request. Network-facing services are attached explicitly and are owned
/// until ordered shutdown or drop.
#[derive(Clone)]
pub struct GridClient {
pub(crate) settings: Settings,
pub(crate) time_provider: TimeProvider,
runtime: Arc<ClientRuntime>,
network_manager: Option<crate::NetworkManager>,
agent_manager: Option<Box<crate::agent_manager::AgentManager>>,
}
impl Clone for GridClient {
fn clone(&self) -> Self {
Self {
settings: self.settings.clone(),
time_provider: self.time_provider.clone(),
runtime: Arc::clone(&self.runtime),
network_manager: self.network_manager.clone(),
agent_manager: None,
}
}
}
#[cfg(test)]
@@ -353,6 +368,9 @@ impl GridClient {
}
pub(crate) fn native_network(&self) -> Result<crate::NetworkManager, crate::Error> {
if let Some(manager) = &self.network_manager {
return Ok(manager.clone());
}
let mut cached = self
.runtime
.network_manager
@@ -369,12 +387,44 @@ impl GridClient {
}
#[allow(clippy::needless_pass_by_value)] // The mapped C# property setter owns its value.
pub(crate) fn native_set_network(&self, value: crate::NetworkManager) {
pub(crate) fn native_set_network(&mut self, value: crate::NetworkManager) {
*self
.runtime
.network_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = value.native_inner_weak();
self.network_manager = Some(value);
}
pub(crate) fn native_self(&mut self) -> &mut crate::agent_manager::AgentManager {
if self.agent_manager.is_none() {
let client = Arc::new(self.clone());
let manager = crate::agent_manager::AgentManager::native_new(Some(client))
.unwrap_or_else(|_| panic!("failed to construct AgentManager"));
self.agent_manager = Some(Box::new(manager));
}
self.agent_manager.as_deref_mut().expect("agent manager")
}
pub(crate) fn cached_agent_manager_inner(
&self,
) -> Option<Arc<crate::agent_manager::AgentManagerInner>> {
self.runtime
.agent_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.upgrade()
}
pub(crate) fn cache_agent_manager_inner(
&self,
inner: &Arc<crate::agent_manager::AgentManagerInner>,
) {
*self
.runtime
.agent_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Arc::downgrade(inner);
}
pub(crate) fn native_set_caps_rate_limiter(
@@ -416,7 +466,7 @@ impl fmt::Debug for GridClient {
.field("lifecycle_state", &self.lifecycle_state())
.field("services", &service_names)
.field("time_provider", &self.time_provider)
.finish()
.finish_non_exhaustive()
}
}
@@ -527,6 +577,8 @@ impl GridClientBuilder {
http_caps_client,
caps_rate_limiter,
)),
network_manager: None,
agent_manager: None,
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,375 @@
//! Native gesture asset parsing and step types.
#![allow(clippy::inherent_to_string)] // Public names mirror C# ToString mappings.
#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility API.
use crate::{Error, assets::GestureStepType};
use libremetaverse_types::{AssetType, UUID};
use std::fmt::Write as _;
#[derive(Clone, Debug, PartialEq)]
pub enum GestureStep {
Animation(GestureStepAnimation),
Sound(GestureStepSound),
Chat(GestureStepChat),
Wait(GestureStepWait),
EOF(GestureStepEOF),
}
impl GestureStep {
#[must_use]
pub const fn gesture_step_type(&self) -> GestureStepType {
match self {
Self::Animation(_) => GestureStepType::Animation,
Self::Sound(_) => GestureStepType::Sound,
Self::Chat(_) => GestureStepType::Chat,
Self::Wait(_) => GestureStepType::Wait,
Self::EOF(_) => GestureStepType::EOF,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GestureStepAnimation {
pub animation_start: bool,
pub id: UUID,
pub name: String,
}
impl GestureStepAnimation {
pub fn new() -> Result<Self, Error> {
Ok(Self {
animation_start: true,
..Self::default()
})
}
#[must_use]
pub fn to_string(&self) -> String {
format!(
"{} animation: {}",
if self.animation_start {
"Start"
} else {
"Stop"
},
self.name
)
}
#[must_use]
pub const fn gesture_step_type(&self) -> GestureStepType {
GestureStepType::Animation
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GestureStepSound {
pub id: UUID,
pub name: String,
}
impl GestureStepSound {
pub fn new() -> Result<Self, Error> {
Ok(Self::default())
}
#[must_use]
pub fn to_string(&self) -> String {
format!("Sound: {}", self.name)
}
#[must_use]
pub const fn gesture_step_type(&self) -> GestureStepType {
GestureStepType::Sound
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GestureStepChat {
pub text: String,
}
impl GestureStepChat {
pub fn new() -> Result<Self, Error> {
Ok(Self::default())
}
#[must_use]
pub fn to_string(&self) -> String {
format!("Chat: {}", self.text)
}
#[must_use]
pub const fn gesture_step_type(&self) -> GestureStepType {
GestureStepType::Chat
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GestureStepWait {
pub wait_for_animation: bool,
pub wait_for_time: bool,
pub wait_time: f32,
}
impl GestureStepWait {
pub const fn new() -> Result<Self, Error> {
Ok(Self {
wait_for_animation: false,
wait_for_time: false,
wait_time: 0.0,
})
}
#[must_use]
pub fn to_string(&self) -> String {
let mut result = String::from("-- Wait for: ");
if self.wait_for_animation {
result.push_str("(animations to finish) ");
}
if self.wait_for_time {
let _ = write!(result, "(time {:.1}s)", self.wait_time);
}
result
}
#[must_use]
pub const fn gesture_step_type(&self) -> GestureStepType {
GestureStepType::Wait
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GestureStepEOF;
impl GestureStepEOF {
pub const fn new() -> Result<Self, Error> {
Ok(Self)
}
#[must_use]
pub fn to_string(&self) -> String {
"End of gesture sequence".into()
}
#[must_use]
pub const fn gesture_step_type(&self) -> GestureStepType {
GestureStepType::EOF
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct AssetGesture {
pub replace_with: String,
pub sequence: Vec<GestureStep>,
pub trigger: String,
pub trigger_key: u8,
pub trigger_key_mask: u32,
asset_id: UUID,
asset_data: Vec<u8>,
}
impl AssetGesture {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self::default())
}
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
Ok(Self {
asset_id,
asset_data,
..Self::default()
})
}
#[must_use]
pub const fn asset_type(&self) -> AssetType {
AssetType::Gesture
}
#[must_use]
pub const fn asset_id(&self) -> UUID {
self.asset_id
}
#[must_use]
pub fn asset_data(&self) -> &[u8] {
&self.asset_data
}
pub fn decode(&mut self) -> Result<bool, Error> {
Ok(self.decode_inner().is_ok())
}
fn decode_inner(&mut self) -> Result<(), Error> {
let text = String::from_utf8(self.asset_data.clone()).map_err(|_| Error::Argument)?;
let mut lines = text.split('\n');
if parse_line::<i32>(&mut lines)? != 2 {
return Err(Error::Argument);
}
self.trigger_key = parse_line(&mut lines)?;
self.trigger_key_mask = parse_line(&mut lines)?;
next_line(&mut lines)?.clone_into(&mut self.trigger);
next_line(&mut lines)?.clone_into(&mut self.replace_with);
let count = parse_line::<i32>(&mut lines)?;
if count < 0 {
return Err(Error::Argument);
}
let mut sequence = Vec::with_capacity(usize::try_from(count).map_err(|_| Error::Argument)?);
for _ in 0..count {
match parse_line::<i32>(&mut lines)? {
0 => {
let name = next_line(&mut lines)?.to_owned();
let id = UUID::new_with_string(
next_line(&mut lines)?.trim_end_matches('\r').into(),
)?;
let flags = parse_line::<i32>(&mut lines)?;
sequence.push(GestureStep::Animation(GestureStepAnimation {
animation_start: flags == 0,
id,
name,
}));
}
1 => {
let name = next_line(&mut lines)?.replace('\r', "");
let id = UUID::new_with_string(
next_line(&mut lines)?.trim_end_matches('\r').into(),
)?;
let _flags = parse_line::<i32>(&mut lines)?;
sequence.push(GestureStep::Sound(GestureStepSound { id, name }));
}
2 => {
let text = next_line(&mut lines)?.to_owned();
let _flags = parse_line::<i32>(&mut lines)?;
sequence.push(GestureStep::Chat(GestureStepChat { text }));
}
3 => {
let wait_time = parse_line::<f32>(&mut lines)?;
let flags = parse_line::<i32>(&mut lines)?;
sequence.push(GestureStep::Wait(GestureStepWait {
wait_for_animation: flags & 0x02 != 0,
wait_for_time: flags & 0x01 != 0,
wait_time,
}));
}
4 => {
sequence.push(GestureStep::EOF(GestureStepEOF));
break;
}
// The reference ignores unknown step discriminants.
_ => {}
}
}
self.sequence = sequence;
Ok(())
}
pub fn encode(&mut self) -> Result<(), Error> {
let mut text = format!(
"2\n{}\n{}\n{}\n{}\n{}\n",
self.trigger_key,
self.trigger_key_mask,
self.trigger,
self.replace_with,
self.sequence.len()
);
for step in &self.sequence {
match step {
GestureStep::Animation(step) => {
let _ = write!(
text,
"0\n{}\n{}\n{}\n",
step.name,
step.id,
i32::from(!step.animation_start)
);
}
GestureStep::Sound(step) => {
let _ = write!(text, "1\n{}\n{}\n0\n", step.name, step.id);
}
GestureStep::Chat(step) => {
let _ = write!(text, "2\n{}\n0\n", step.text);
}
GestureStep::Wait(step) => {
let flags =
i32::from(step.wait_for_time) | (i32::from(step.wait_for_animation) << 1);
let _ = write!(text, "3\n{:.6}\n{}\n", step.wait_time, flags);
}
GestureStep::EOF(_) => {
text.push_str("4\n");
break;
}
}
}
self.asset_data = text.into_bytes();
Ok(())
}
}
fn next_line<'a>(lines: &mut impl Iterator<Item = &'a str>) -> Result<&'a str, Error> {
lines.next().ok_or(Error::Argument)
}
fn parse_line<'a, T: std::str::FromStr>(
lines: &mut impl Iterator<Item = &'a str>,
) -> Result<T, Error> {
next_line(lines)?
.trim_end_matches('\r')
.parse()
.map_err(|_| Error::Argument)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gesture_v2_roundtrip_preserves_all_step_payloads() {
let animation = UUID::random().unwrap();
let sound = UUID::random().unwrap();
let mut gesture = AssetGesture::new_with_constructor().unwrap();
gesture.trigger_key = 7;
gesture.trigger_key_mask = 3;
gesture.trigger = "/wave".into();
gesture.replace_with = "waves".into();
gesture.sequence = vec![
GestureStep::Chat(GestureStepChat {
text: "/5 hello".into(),
}),
GestureStep::Animation(GestureStepAnimation {
animation_start: true,
id: animation,
name: "Wave".into(),
}),
GestureStep::Sound(GestureStepSound {
id: sound,
name: "Bell".into(),
}),
GestureStep::Wait(GestureStepWait {
wait_for_animation: true,
wait_for_time: true,
wait_time: 1.25,
}),
GestureStep::EOF(GestureStepEOF),
];
gesture.encode().unwrap();
let mut decoded =
AssetGesture::new_with_uuid_bytes(UUID::random().unwrap(), gesture.asset_data.clone())
.unwrap();
assert!(decoded.decode().unwrap());
assert_eq!(decoded.trigger_key, gesture.trigger_key);
assert_eq!(decoded.trigger_key_mask, gesture.trigger_key_mask);
assert_eq!(decoded.trigger, gesture.trigger);
assert_eq!(decoded.replace_with, gesture.replace_with);
assert_eq!(decoded.sequence, gesture.sequence);
}
#[test]
fn malformed_gesture_returns_false_without_partial_sequence() {
let mut gesture =
AssetGesture::new_with_uuid_bytes(UUID::zero(), b"1\n0\n".to_vec()).unwrap();
assert!(!gesture.decode().unwrap());
assert!(gesture.sequence.is_empty());
}
}

View File

@@ -4,12 +4,15 @@ extern crate self as libremetaverse;
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
mod attention_catalog;
mod agent_manager;
mod agent_messages;
mod bit_pack;
mod caps;
mod caps_http;
mod client_core;
mod download_manager;
mod event_queue;
mod gesture;
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
mod foliage_catalog;
mod generated;
@@ -154,18 +157,6 @@ impl InventoryAISClient {
}
}
#[allow(dead_code)]
impl AgentManager {
fn extract_experience_permission(
_map: libremetaverse_structured_data::OSDMap,
_experience_id: libremetaverse_types::UUID,
) -> Result<String, Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.AgentManager.ExtractExperiencePermission(LibreMetaverse.StructuredData.OSDMap,LibreMetaverse.UUID)",
)
}
}
#[allow(dead_code, clippy::unused_self)]
impl EstateTools {
fn estate_owner_message_handler(

View File

@@ -295,6 +295,13 @@ pub struct PacketReceivedEventArgs {
simulator: Simulator,
}
#[derive(Clone)]
pub(crate) struct RawPacketReceivedEventArgs {
pub packet_type: PacketType,
pub data: Vec<u8>,
pub simulator: Simulator,
}
impl PacketReceivedEventArgs {
pub fn new(packet: Packet, simulator: Simulator) -> Result<Self, Error> {
Ok(Self { packet, simulator })
@@ -1201,6 +1208,12 @@ impl Simulator {
Arc::clone(&self.data)
}
pub(crate) fn native_caps(&self) -> Option<Caps> {
read(&self.data.caps_state)
.clone()
.map(|inner| Caps::native_from_inner(inner, self.native_clone_without_caps()))
}
pub(crate) fn native_from_weak(data: &Weak<SimulatorData>) -> Option<Self> {
data.upgrade().map(|data| Self { caps: None, data })
}
@@ -1775,6 +1788,7 @@ struct NetworkEvents {
logged_out: EventRegistry<LoggedOutEventArgs>,
login_progress: EventRegistry<LoginProgressEventArgs>,
packet_sent: EventRegistry<PacketSentEventArgs>,
raw_packet_received: EventRegistry<RawPacketReceivedEventArgs>,
sim_changed: EventRegistry<SimChangedEventArgs>,
sim_connected: EventRegistry<SimConnectedEventArgs>,
sim_connecting: EventRegistry<SimConnectingEventArgs>,
@@ -1870,6 +1884,15 @@ impl NetworkManagerInner {
continue;
};
inner.process_internal_packet(&packet, &simulator, raw_data.as_deref());
if let Some(data) = raw_data {
inner.events.raw_packet_received.emit_with(|| {
RawPacketReceivedEventArgs {
packet_type: packet.type_,
data: data.clone(),
simulator: simulator.clone(),
}
});
}
let _ =
inner
.packet_events
@@ -2378,6 +2401,7 @@ impl Drop for NetworkManagerInner {
}
/// Native implementation of C# `NetworkManager`.
#[derive(Clone)]
pub struct NetworkManager {
pub login_response_data: Option<LoginResponseData>,
pub simulators: SimulatorCollection,
@@ -2408,6 +2432,14 @@ impl NetworkManager {
Arc::downgrade(&self.inner)
}
pub(crate) fn native_agent_id(&self) -> UUID {
*read(&self.inner.agent_id)
}
pub(crate) fn native_session_id(&self) -> UUID {
*read(&self.inner.session_id)
}
pub fn native_new(client: GridClient) -> Result<Self, Error> {
let simulators = SimulatorCollection::default();
let packet_events = PacketEventDictionary::new(client.clone())?;
@@ -3176,6 +3208,13 @@ impl NetworkManager {
.subscribe(packet_type, handler, is_async)
}
pub(crate) fn subscribe_raw_packet(
&self,
handler: EventHandler<RawPacketReceivedEventArgs>,
) -> Subscription {
self.inner.events.raw_packet_received.subscribe(handler)
}
pub fn subscribe_caps(
&self,
caps_event: String,