feat(grid-agent): add embodied behavior controller (#125)
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m42s
CI / required (push) Failing after 2m40s

This commit is contained in:
2026-08-18 00:26:24 +00:00
parent 3a0ead7eb5
commit 058ed10005
11 changed files with 2607 additions and 22 deletions

View File

@@ -107,6 +107,14 @@ pub struct LibremetaverseWorldSnapshotSource {
agent: Arc<libremetaverse::AgentManager>,
}
#[cfg(feature = "live-grid")]
#[derive(Clone)]
pub struct LibremetaverseEmbodimentSink {
client: libremetaverse::GridClient,
agent: Arc<libremetaverse::AgentManager>,
generation: Arc<std::sync::RwLock<Option<u64>>>,
}
#[cfg(feature = "live-grid")]
impl fmt::Debug for LibremetaverseClientOwner {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -130,6 +138,7 @@ pub struct LibremetaverseSessionBackend {
password: crate::config::SecretString,
interaction: Option<crate::interaction::InteractionIngress>,
perception: Option<crate::perception::PerceptionIngress>,
behavior: Option<crate::behavior::BehaviorIngress>,
delivery_generation: Arc<std::sync::RwLock<Option<u64>>>,
}
@@ -187,7 +196,7 @@ impl LibremetaverseClientOwner {
&self,
connection: crate::config::GridConnection,
) -> Result<LibremetaverseSessionBackend, BackendError> {
self.session_backend_inner(connection, None, None)
self.session_backend_inner(connection, None, None, None)
}
/// Creates a supervised live session whose generation owns exactly one
@@ -201,7 +210,7 @@ impl LibremetaverseClientOwner {
connection: crate::config::GridConnection,
ingress: crate::interaction::InteractionIngress,
) -> Result<LibremetaverseSessionBackend, BackendError> {
self.session_backend_inner(connection, Some(ingress), None)
self.session_backend_inner(connection, Some(ingress), None, None)
}
/// Creates a session that generation-fences interaction and perception.
@@ -215,7 +224,27 @@ impl LibremetaverseClientOwner {
interaction: crate::interaction::InteractionIngress,
perception: crate::perception::PerceptionIngress,
) -> Result<LibremetaverseSessionBackend, BackendError> {
self.session_backend_inner(connection, Some(interaction), Some(perception))
self.session_backend_inner(connection, Some(interaction), Some(perception), None)
}
/// Creates a session that generation-fences interaction, perception, and embodiment.
///
/// # Errors
///
/// Rejects a malformed avatar identity before any subscription or login.
pub fn session_backend_with_agent_services(
&self,
connection: crate::config::GridConnection,
interaction: crate::interaction::InteractionIngress,
perception: crate::perception::PerceptionIngress,
behavior: crate::behavior::BehaviorIngress,
) -> Result<LibremetaverseSessionBackend, BackendError> {
self.session_backend_inner(
connection,
Some(interaction),
Some(perception),
Some(behavior),
)
}
fn session_backend_inner(
@@ -223,6 +252,7 @@ impl LibremetaverseClientOwner {
connection: crate::config::GridConnection,
interaction: Option<crate::interaction::InteractionIngress>,
perception: Option<crate::perception::PerceptionIngress>,
behavior: Option<crate::behavior::BehaviorIngress>,
) -> Result<LibremetaverseSessionBackend, BackendError> {
let avatar = connection.avatar_name.trim();
let (first_name, last_name) = avatar
@@ -242,6 +272,7 @@ impl LibremetaverseClientOwner {
password: connection.password,
interaction,
perception,
behavior,
delivery_generation: Arc::clone(&self.delivery_generation),
})
}
@@ -261,6 +292,15 @@ impl LibremetaverseClientOwner {
agent: Arc::clone(&self.agent),
}
}
#[must_use]
pub fn embodiment_sink(&self) -> LibremetaverseEmbodimentSink {
LibremetaverseEmbodimentSink {
client: self.client.clone(),
agent: Arc::clone(&self.agent),
generation: Arc::clone(&self.delivery_generation),
}
}
}
#[cfg(feature = "live-grid")]
@@ -437,6 +477,222 @@ impl crate::perception::WorldSnapshotSource for LibremetaverseWorldSnapshotSourc
}
}
#[cfg(feature = "live-grid")]
impl LibremetaverseEmbodimentSink {
fn ensure_generation(&self, generation: u64) -> Result<(), crate::behavior::BehaviorError> {
let current = *self
.generation
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if current == Some(generation) && self.client.network().native_connected() {
Ok(())
} else {
Err(crate::behavior::BehaviorError::NotReady)
}
}
fn native_pose(
&self,
generation: u64,
) -> Result<crate::behavior::EmbodiedPose, crate::behavior::BehaviorError> {
self.ensure_generation(generation)?;
let simulator = self
.client
.network()
.native_current_sim()
.ok_or(crate::behavior::BehaviorError::NotReady)?;
let position = self.agent.sim_position();
let rotation = self.agent.sim_rotation();
let sin_yaw = 2.0 * f64::from(rotation.w * rotation.z + rotation.x * rotation.y);
let cos_yaw = 1.0 - 2.0 * f64::from(rotation.y * rotation.y + rotation.z * rotation.z);
Ok(crate::behavior::EmbodiedPose {
generation,
region_id: simulator.region_id,
position: native_position(position),
heading_degrees: sin_yaw.atan2(cos_yaw).to_degrees().rem_euclid(360.0),
sitting: self.agent.sitting_on() != 0,
})
}
}
#[cfg(feature = "live-grid")]
#[allow(clippy::cast_possible_truncation)]
impl crate::behavior::EmbodimentSink for LibremetaverseEmbodimentSink {
fn current_pose(
&self,
generation: u64,
cancellation: CancellationToken,
) -> crate::behavior::EmbodimentFuture<'_, crate::behavior::EmbodiedPose> {
Box::pin(async move {
if cancellation.is_cancellation_requested() {
return Err(crate::behavior::BehaviorError::Cancelled);
}
self.native_pose(generation)
})
}
fn resolve_avatar(
&self,
generation: u64,
avatar_id: UUID,
cancellation: CancellationToken,
) -> crate::behavior::EmbodimentFuture<'_, crate::perception::WorldPosition> {
Box::pin(async move {
if cancellation.is_cancellation_requested() {
return Err(crate::behavior::BehaviorError::Cancelled);
}
self.ensure_generation(generation)?;
let simulator = self
.client
.network()
.native_current_sim()
.ok_or(crate::behavior::BehaviorError::NotReady)?;
simulator
.objects_avatars
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.values()
.find(|avatar| avatar.id == avatar_id)
.map(|avatar| native_position(avatar.position))
.ok_or(crate::behavior::BehaviorError::TargetUnavailable)
})
}
fn face_point(
&self,
generation: u64,
point: crate::perception::WorldPosition,
cancellation: CancellationToken,
) -> crate::behavior::EmbodimentFuture<'_, ()> {
Box::pin(async move {
if cancellation.is_cancellation_requested() {
return Err(crate::behavior::BehaviorError::Cancelled);
}
self.ensure_generation(generation)?;
self.agent
.movement
.turn_toward(
libremetaverse_types::Vector3 {
x: point.x as f32,
y: point.y as f32,
z: point.z as f32,
},
Some(true),
)
.map_err(|_| crate::behavior::BehaviorError::NativeOperation)?;
Ok(())
})
}
fn begin_walk(
&self,
generation: u64,
point: crate::perception::WorldPosition,
cancellation: CancellationToken,
) -> crate::behavior::EmbodimentFuture<'_, ()> {
Box::pin(async move {
if cancellation.is_cancellation_requested() {
return Err(crate::behavior::BehaviorError::Cancelled);
}
self.ensure_generation(generation)?;
self.agent
.auto_pilot_local(
point.x.round() as i32,
point.y.round() as i32,
point.z as f32,
)
.map_err(|_| crate::behavior::BehaviorError::NativeOperation)
})
}
fn validate_walk_target(
&self,
generation: u64,
point: crate::perception::WorldPosition,
cancellation: CancellationToken,
) -> crate::behavior::EmbodimentFuture<'_, ()> {
Box::pin(async move {
if cancellation.is_cancellation_requested() {
return Err(crate::behavior::BehaviorError::Cancelled);
}
self.ensure_generation(generation)?;
let simulator = self
.client
.network()
.native_current_sim()
.ok_or(crate::behavior::BehaviorError::NotReady)?;
let parcels = self.client.parcels();
let current = parcels
.get_parcel_local_id(simulator.clone(), self.agent.sim_position())
.map_err(|_| crate::behavior::BehaviorError::NativeOperation)?;
let target = parcels
.get_parcel_local_id(
simulator,
libremetaverse_types::Vector3 {
x: point.x as f32,
y: point.y as f32,
z: point.z as f32,
},
)
.map_err(|_| crate::behavior::BehaviorError::NativeOperation)?;
if current == 0 || current != target {
return Err(crate::behavior::BehaviorError::RegionBoundary);
}
Ok(())
})
}
fn stop(
&self,
generation: u64,
cancellation: CancellationToken,
) -> crate::behavior::EmbodimentFuture<'_, ()> {
Box::pin(async move {
if cancellation.is_cancellation_requested() {
return Err(crate::behavior::BehaviorError::Cancelled);
}
self.ensure_generation(generation)?;
self.agent
.auto_pilot_cancel()
.map(|_| ())
.map_err(|_| crate::behavior::BehaviorError::NativeOperation)
})
}
fn sit(
&self,
generation: u64,
cancellation: CancellationToken,
) -> crate::behavior::EmbodimentFuture<'_, ()> {
Box::pin(async move {
if cancellation.is_cancellation_requested() {
return Err(crate::behavior::BehaviorError::Cancelled);
}
self.ensure_generation(generation)?;
self.agent
.sit()
.map_err(|_| crate::behavior::BehaviorError::NativeOperation)
})
}
fn stand(
&self,
generation: u64,
cancellation: CancellationToken,
) -> crate::behavior::EmbodimentFuture<'_, ()> {
Box::pin(async move {
if cancellation.is_cancellation_requested() {
return Err(crate::behavior::BehaviorError::Cancelled);
}
self.ensure_generation(generation)?;
self.agent
.stand()
.map(|_| ())
.map_err(|_| crate::behavior::BehaviorError::NativeOperation)
})
}
}
#[cfg(feature = "live-grid")]
fn native_position(value: libremetaverse_types::Vector3) -> crate::perception::WorldPosition {
crate::perception::WorldPosition {
@@ -580,6 +836,7 @@ struct LibremetaverseSession {
subscriptions: Vec<libremetaverse_types::compat::Subscription>,
interaction: Option<crate::interaction::InteractionIngress>,
perception: Option<crate::perception::PerceptionIngress>,
behavior: Option<crate::behavior::BehaviorIngress>,
delivery_generation: Arc<std::sync::RwLock<Option<u64>>>,
}
@@ -616,16 +873,20 @@ impl crate::session::GridSession for LibremetaverseSession {
cancellation: CancellationToken,
) -> crate::session::SessionFuture<'static, Result<(), crate::session::SessionFailure>> {
Box::pin(async move {
*self
.delivery_generation
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
if let Some(interaction) = &self.interaction {
let _ = interaction.try_disconnected();
}
if let Some(perception) = &self.perception {
perception.disconnected();
}
if let Some(behavior) = &self.behavior {
behavior.disconnected();
tokio::task::yield_now().await;
}
*self
.delivery_generation
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
self.subscriptions.clear();
self.network
.native_logout_async(Some(cancellation))
@@ -694,6 +955,7 @@ impl crate::session::GridSessionBackend for LibremetaverseSessionBackend {
let disconnected_sender = sender.clone();
let disconnected_interaction = self.interaction.clone();
let disconnected_perception = self.perception.clone();
let disconnected_behavior = self.behavior.clone();
let disconnected_generation = Arc::clone(&self.delivery_generation);
let disconnected = self
.network
@@ -707,6 +969,9 @@ impl crate::session::GridSessionBackend for LibremetaverseSessionBackend {
if let Some(perception) = &disconnected_perception {
perception.disconnected();
}
if let Some(behavior) = &disconnected_behavior {
behavior.disconnected();
}
let kind = match event.reason() {
libremetaverse::NetworkManagerDisconnectType::NetworkTimeout
| libremetaverse::NetworkManagerDisconnectType::ClientInitiated => {
@@ -727,6 +992,7 @@ impl crate::session::GridSessionBackend for LibremetaverseSessionBackend {
let ready_sender = sender.clone();
let ready_interaction = self.interaction.clone();
let ready_perception = self.perception.clone();
let ready_behavior = self.behavior.clone();
let ready_network = self.network.clone();
let ready_agent = Arc::clone(&self.agent);
let ready_generation = Arc::clone(&self.delivery_generation);
@@ -747,18 +1013,27 @@ impl crate::session::GridSessionBackend for LibremetaverseSessionBackend {
{
let _ = perception.connected(generation, simulator.region_id);
}
if let (Some(behavior), Some(simulator)) =
(&ready_behavior, ready_network.native_current_sim())
{
let _ = behavior.connected(generation, simulator.region_id);
}
let _ = ready_sender.try_send(crate::session::SessionSignal::Ready);
}
}));
let mut subscriptions = vec![disconnected, ready];
if let Some(perception) = &self.perception {
let crossing_perception = perception.clone();
let crossing_behavior = self.behavior.clone();
let crossing_network = self.network.clone();
subscriptions.push(self.network.native_subscribe_sim_changed(Arc::new(
move |_| {
if let Some(simulator) = crossing_network.native_current_sim() {
let _ =
crossing_perception.region_changed(generation, simulator.region_id);
if let Some(behavior) = &crossing_behavior {
let _ = behavior.region_changed(generation, simulator.region_id);
}
}
},
)));
@@ -806,6 +1081,11 @@ impl crate::session::GridSessionBackend for LibremetaverseSessionBackend {
{
let _ = perception.connected(generation, simulator.region_id);
}
if let (Some(behavior), Some(simulator)) =
(&self.behavior, self.network.native_current_sim())
{
let _ = behavior.connected(generation, simulator.region_id);
}
let _ = sender.try_send(crate::session::SessionSignal::Ready);
}
let session: Box<dyn crate::session::GridSession> = Box::new(LibremetaverseSession {
@@ -815,6 +1095,7 @@ impl crate::session::GridSessionBackend for LibremetaverseSessionBackend {
subscriptions,
interaction: self.interaction.clone(),
perception: self.perception.clone(),
behavior: self.behavior.clone(),
delivery_generation: Arc::clone(&self.delivery_generation),
});
Ok(session)