Centralize world and viewport game loops
Some checks failed
CI / rust-skia (Rust only) (push) Has been cancelled
CI / required (push) Has been cancelled

This commit is contained in:
2026-08-23 13:42:01 +02:00
parent cb9233c59c
commit 33d1cf6827
18 changed files with 2541 additions and 287 deletions

View File

@@ -0,0 +1,16 @@
[package]
name = "metacrate-grid-world"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Bounded OpenSim and Second Life event stream and persistent world state"
publish = false
[dependencies]
libremetaverse = { version = "0.0.1", path = "../libremetaverse", default-features = false }
libremetaverse-types = { version = "0.0.1", path = "../libremetaverse-types" }
[lints]
workspace = true

View File

@@ -0,0 +1,767 @@
//! Bounded OpenSim/Second Life event consumption and persistent world state.
#![allow(clippy::missing_errors_doc)]
use libremetaverse::{Avatar, GridClient, Primitive, Simulator};
use libremetaverse_types::{UUID, compat::Subscription};
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
mpsc::{self, Receiver, RecvError, SyncSender, TryRecvError},
},
thread,
time::Duration,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RegionDescriptor {
pub id: UUID,
pub handle: u64,
pub name: String,
pub size: (u32, u32),
}
impl RegionDescriptor {
#[must_use]
pub fn from_simulator(simulator: &Simulator) -> Self {
Self {
id: simulator.region_id,
handle: simulator.handle,
name: simulator.name.clone(),
size: simulator.region_size(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum PrimitiveChange {
Full,
Movement,
}
pub enum WorldEvent {
RegionChanged(Option<RegionDescriptor>),
PrimitiveUpdated {
region: RegionDescriptor,
primitive: Arc<Primitive>,
change: PrimitiveChange,
},
AvatarUpdated {
region: RegionDescriptor,
avatar: Arc<Avatar>,
},
AvatarMoved {
region: RegionDescriptor,
local_id: u32,
position: libremetaverse_types::Vector3,
rotation: libremetaverse_types::Quaternion,
velocity: libremetaverse_types::Vector3,
},
ObjectsRemoved {
region: RegionDescriptor,
local_ids: Vec<u32>,
},
TerrainUpdated {
region: RegionDescriptor,
x: i32,
y: i32,
patch_size: i32,
height_map: Vec<f32>,
},
VisibilitySnapshot {
region: RegionDescriptor,
primitives: Vec<Arc<Primitive>>,
avatars: Vec<Arc<Avatar>>,
terrain: Vec<((i32, i32), TerrainPatch)>,
},
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum DirtyWorldItem {
Region,
Primitive(u32, PrimitiveChange),
Avatar(u32),
Terrain(i32, i32),
}
#[derive(Clone, Debug, PartialEq)]
pub struct TerrainPatch {
pub patch_size: i32,
pub height_map: Arc<[f32]>,
}
#[derive(Clone)]
pub struct WorldSnapshot {
pub observed_unix_millis: u64,
pub region: Option<RegionDescriptor>,
pub primitives: Arc<HashMap<u32, Arc<Primitive>>>,
pub avatars: Arc<HashMap<u32, Arc<Avatar>>>,
pub terrain: Arc<BTreeMap<(i32, i32), TerrainPatch>>,
}
#[derive(Default)]
pub struct WorldState {
region: Option<RegionDescriptor>,
primitives: HashMap<u32, Arc<Primitive>>,
avatars: HashMap<u32, Arc<Avatar>>,
terrain: BTreeMap<(i32, i32), TerrainPatch>,
dirty: BTreeSet<DirtyWorldItem>,
}
impl WorldState {
pub fn apply(&mut self, event: WorldEvent) {
match event {
WorldEvent::RegionChanged(region) => self.change_region(region),
WorldEvent::PrimitiveUpdated {
region,
primitive,
change,
} => {
self.ensure_region(region);
let local_id = primitive.local_id;
self.primitives.insert(local_id, primitive);
self.dirty.retain(
|item| !matches!(item, DirtyWorldItem::Primitive(id, _) if *id == local_id),
);
self.dirty
.insert(DirtyWorldItem::Primitive(local_id, change));
}
WorldEvent::AvatarUpdated { region, avatar } => {
self.ensure_region(region);
let local_id = avatar.local_id;
self.avatars.insert(local_id, avatar);
self.dirty.insert(DirtyWorldItem::Avatar(local_id));
}
WorldEvent::AvatarMoved {
region,
local_id,
position,
rotation,
velocity,
} => {
self.ensure_region(region);
if let Some(avatar) = self.avatars.get_mut(&local_id) {
let avatar = Arc::make_mut(avatar);
avatar.position = position;
avatar.rotation = rotation;
avatar.velocity = velocity;
self.dirty.insert(DirtyWorldItem::Avatar(local_id));
}
}
WorldEvent::ObjectsRemoved { region, local_ids } => {
self.ensure_region(region);
for local_id in local_ids {
self.primitives.remove(&local_id);
self.avatars.remove(&local_id);
self.dirty
.insert(DirtyWorldItem::Primitive(local_id, PrimitiveChange::Full));
}
}
WorldEvent::TerrainUpdated {
region,
x,
y,
patch_size,
height_map,
} => {
self.ensure_region(region);
self.terrain.insert(
(x, y),
TerrainPatch {
patch_size,
height_map: height_map.into(),
},
);
self.dirty.insert(DirtyWorldItem::Terrain(x, y));
}
WorldEvent::VisibilitySnapshot {
region,
primitives,
avatars,
terrain,
} => {
self.region = Some(region);
self.primitives = primitives
.into_iter()
.map(|primitive| (primitive.local_id, primitive))
.collect();
self.avatars = avatars
.into_iter()
.map(|avatar| (avatar.local_id, avatar))
.collect();
self.terrain = terrain.into_iter().collect();
self.dirty.clear();
self.dirty.insert(DirtyWorldItem::Region);
}
}
}
fn ensure_region(&mut self, region: RegionDescriptor) {
if self.region.as_ref().map(|value| value.handle) != Some(region.handle) {
self.change_region(Some(region));
}
}
fn change_region(&mut self, region: Option<RegionDescriptor>) {
self.region = region;
self.primitives.clear();
self.avatars.clear();
self.terrain.clear();
self.dirty.clear();
self.dirty.insert(DirtyWorldItem::Region);
}
#[must_use]
pub fn region(&self) -> Option<&RegionDescriptor> {
self.region.as_ref()
}
#[must_use]
pub fn primitive(&self, local_id: u32) -> Option<Arc<Primitive>> {
self.primitives.get(&local_id).cloned()
}
#[must_use]
pub fn avatar(&self, local_id: u32) -> Option<Arc<Avatar>> {
self.avatars.get(&local_id).cloned()
}
#[must_use]
pub fn terrain(&self, x: i32, y: i32) -> Option<&TerrainPatch> {
self.terrain.get(&(x, y))
}
pub fn take_dirty(&mut self) -> Vec<DirtyWorldItem> {
std::mem::take(&mut self.dirty).into_iter().collect()
}
#[must_use]
pub fn snapshot(&self) -> WorldSnapshot {
WorldSnapshot {
observed_unix_millis: unix_millis_now(),
region: self.region.clone(),
primitives: Arc::new(self.primitives.clone()),
avatars: Arc::new(self.avatars.clone()),
terrain: Arc::new(self.terrain.clone()),
}
}
}
fn unix_millis_now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |duration| {
duration.as_millis().try_into().unwrap_or(u64::MAX)
})
}
#[derive(Clone)]
pub struct WorldEventSender {
sender: SyncSender<WorldEvent>,
dropped: Arc<AtomicU64>,
}
impl WorldEventSender {
pub fn send(&self, event: WorldEvent) {
if self.sender.try_send(event).is_err() {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
}
#[must_use]
pub fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
}
pub struct WorldEventReceiver {
receiver: Receiver<WorldEvent>,
dropped: Arc<AtomicU64>,
}
#[derive(Clone)]
pub struct WorldEventMetrics {
dropped: Arc<AtomicU64>,
}
impl WorldEventMetrics {
#[must_use]
pub fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
}
#[must_use]
pub fn world_event_channel(capacity: usize) -> Option<(WorldEventSender, WorldEventReceiver)> {
if capacity == 0 {
return None;
}
let (sender, receiver) = mpsc::sync_channel(capacity);
let dropped = Arc::new(AtomicU64::new(0));
Some((
WorldEventSender {
sender,
dropped: Arc::clone(&dropped),
},
WorldEventReceiver { receiver, dropped },
))
}
impl WorldEventReceiver {
pub fn recv(&self) -> Result<WorldEvent, RecvError> {
self.receiver.recv()
}
pub fn try_recv(&self) -> Result<WorldEvent, TryRecvError> {
self.receiver.try_recv()
}
#[must_use]
pub fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
#[must_use]
pub fn metrics(&self) -> WorldEventMetrics {
WorldEventMetrics {
dropped: Arc::clone(&self.dropped),
}
}
}
pub struct LibreMetaverseWorldEvents {
_subscriptions: Vec<Subscription>,
stopped: Arc<AtomicBool>,
monitor: Option<thread::JoinHandle<()>>,
}
impl Drop for LibreMetaverseWorldEvents {
fn drop(&mut self) {
self.stopped.store(true, Ordering::Release);
if let Some(monitor) = self.monitor.take() {
monitor.thread().unpark();
let _ = monitor.join();
}
}
}
impl LibreMetaverseWorldEvents {
#[must_use]
pub fn subscribe(client: &GridClient, capacity: usize) -> Option<(Self, WorldEventReceiver)> {
Self::subscribe_inner(client, None, capacity)
}
#[must_use]
pub fn subscribe_visible(
client: &GridClient,
agent: Arc<libremetaverse::AgentManager>,
max_distance_meters: u32,
capacity: usize,
) -> Option<(Self, WorldEventReceiver)> {
let maximum = f32::from(u16::try_from(max_distance_meters).ok()?);
(maximum > 0.0).then_some(())?;
Self::subscribe_inner(client, Some((agent, maximum)), capacity)
}
#[allow(clippy::too_many_lines)] // One subscription owner keeps all native event lifetimes together.
fn subscribe_inner(
client: &GridClient,
view: Option<(Arc<libremetaverse::AgentManager>, f32)>,
capacity: usize,
) -> Option<(Self, WorldEventReceiver)> {
if capacity == 0 {
return None;
}
let (ingress, receiver) = world_event_channel(capacity)?;
let objects = client.objects();
let terrain = client.terrain();
let network = client.network();
let monitor_view = view.clone();
let monitor_ingress = ingress.clone();
let mut subscriptions = Vec::with_capacity(6);
let event_ingress = ingress.clone();
let event_view = view.clone();
subscriptions.push(objects.subscribe_object_update(Arc::new(move |event| {
let simulator = event.simulator();
let primitive = event.prim();
let Some(primitive) = visible_primitive(&simulator, primitive, event_view.as_ref())
else {
return;
};
event_ingress.send(WorldEvent::PrimitiveUpdated {
region: RegionDescriptor::from_simulator(&simulator),
primitive: Arc::new(primitive),
change: PrimitiveChange::Full,
});
})));
let event_ingress = ingress.clone();
let event_view = view.clone();
subscriptions.push(
objects.subscribe_terse_object_update(Arc::new(move |event| {
let simulator = event.simulator();
let update = event.update();
if update.avatar {
if visible_position(update.position, event_view.as_ref()) {
event_ingress.send(WorldEvent::AvatarMoved {
region: RegionDescriptor::from_simulator(&simulator),
local_id: update.local_id,
position: update.position,
rotation: update.rotation,
velocity: update.velocity,
});
} else {
event_ingress.send(WorldEvent::ObjectsRemoved {
region: RegionDescriptor::from_simulator(&simulator),
local_ids: vec![update.local_id],
});
}
return;
}
let mut primitive = event.prim();
primitive.position = update.position;
primitive.rotation = update.rotation;
primitive.velocity = update.velocity;
primitive.acceleration = update.acceleration;
primitive.angular_velocity = update.angular_velocity;
let local_id = primitive.local_id;
let Some(primitive) = visible_primitive(&simulator, primitive, event_view.as_ref())
else {
event_ingress.send(WorldEvent::ObjectsRemoved {
region: RegionDescriptor::from_simulator(&simulator),
local_ids: vec![local_id],
});
return;
};
event_ingress.send(WorldEvent::PrimitiveUpdated {
region: RegionDescriptor::from_simulator(&simulator),
primitive: Arc::new(primitive),
change: PrimitiveChange::Movement,
});
})),
);
let event_ingress = ingress.clone();
let event_view = view.clone();
subscriptions.push(objects.subscribe_avatar_update(Arc::new(move |event| {
let simulator = event.simulator();
let avatar = event.avatar();
if !visible_position(avatar.position, event_view.as_ref()) {
return;
}
event_ingress.send(WorldEvent::AvatarUpdated {
region: RegionDescriptor::from_simulator(&simulator),
avatar: Arc::new(avatar),
});
})));
let event_ingress = ingress.clone();
subscriptions.push(objects.subscribe_kill_object(Arc::new(move |event| {
let simulator = event.simulator();
event_ingress.send(WorldEvent::ObjectsRemoved {
region: RegionDescriptor::from_simulator(&simulator),
local_ids: vec![event.object_local_id()],
});
})));
let event_ingress = ingress.clone();
let event_view = view;
subscriptions.push(
terrain.subscribe_land_patch_received(Arc::new(move |event| {
let simulator = event.simulator();
if !visible_terrain_patch(
event.x(),
event.y(),
event.patch_size(),
event_view.as_ref(),
) {
return;
}
event_ingress.send(WorldEvent::TerrainUpdated {
region: RegionDescriptor::from_simulator(&simulator),
x: event.x(),
y: event.y(),
patch_size: event.patch_size(),
height_map: event.height_map(),
});
})),
);
let event_ingress = ingress;
let callback_client = client.clone();
subscriptions.push(network.subscribe_sim_changed(Arc::new(move |_| {
event_ingress.send(WorldEvent::RegionChanged(
callback_client
.network()
.current_sim()
.as_ref()
.map(RegionDescriptor::from_simulator),
));
})));
let stopped = Arc::new(AtomicBool::new(false));
let monitor = if let Some((agent, maximum)) = monitor_view {
let monitor_client = client.clone();
let monitor_stopped = Arc::clone(&stopped);
Some(
thread::Builder::new()
.name("metacrate-world-visibility".into())
.spawn(move || {
let mut last = None;
while !monitor_stopped.load(Ordering::Acquire) {
if let Some(simulator) = monitor_client.network().current_sim() {
let position = agent.sim_position();
let changed = last.is_none_or(
|(handle, previous): (u64, libremetaverse_types::Vector3)| {
let dx = position.x - previous.x;
let dy = position.y - previous.y;
let dz = position.z - previous.z;
handle != simulator.handle
|| dx * dx + dy * dy + dz * dz >= 16.0
},
);
if changed {
monitor_ingress
.send(visibility_snapshot(&simulator, &agent, maximum));
last = Some((simulator.handle, position));
}
}
thread::park_timeout(Duration::from_millis(250));
}
})
.ok()?,
)
} else {
None
};
Some((
Self {
_subscriptions: subscriptions,
stopped,
monitor,
},
receiver,
))
}
}
fn visibility_snapshot(
simulator: &Simulator,
agent: &libremetaverse::AgentManager,
maximum: f32,
) -> WorldEvent {
let center = agent.sim_position();
let primitives = simulator
.objects_primitives
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let avatars = simulator
.objects_avatars
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let visible_primitives = primitives
.values()
.filter_map(|primitive| resolved_primitive(primitive.clone(), &primitives, &avatars))
.filter(|primitive| position_in_radius(primitive.position, center, maximum))
.map(Arc::new)
.collect();
let visible_avatars = avatars
.values()
.filter(|avatar| position_in_radius(avatar.position, center, maximum))
.cloned()
.map(Arc::new)
.collect();
drop(primitives);
drop(avatars);
let terrain = simulator
.terrain
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.filter(|patch| terrain_patch_in_radius(patch.x, patch.y, 16, center, maximum))
.map(|patch| {
(
(patch.x, patch.y),
TerrainPatch {
patch_size: 16,
height_map: Arc::from(patch.data.clone()),
},
)
})
.collect();
WorldEvent::VisibilitySnapshot {
region: RegionDescriptor::from_simulator(simulator),
primitives: visible_primitives,
avatars: visible_avatars,
terrain,
}
}
fn position_in_radius(
position: libremetaverse_types::Vector3,
center: libremetaverse_types::Vector3,
maximum: f32,
) -> bool {
let dx = position.x - center.x;
let dy = position.y - center.y;
let dz = position.z - center.z;
dx * dx + dy * dy + dz * dz <= maximum * maximum
}
fn visible_position(
position: libremetaverse_types::Vector3,
view: Option<&(Arc<libremetaverse::AgentManager>, f32)>,
) -> bool {
view.is_none_or(|(agent, maximum)| {
let center = agent.sim_position();
position_in_radius(position, center, *maximum)
})
}
fn visible_terrain_patch(
x: i32,
y: i32,
patch_size: i32,
view: Option<&(Arc<libremetaverse::AgentManager>, f32)>,
) -> bool {
let Some((agent, maximum)) = view else {
return true;
};
terrain_patch_in_radius(x, y, patch_size, agent.sim_position(), *maximum)
}
fn terrain_patch_in_radius(
x: i32,
y: i32,
patch_size: i32,
center: libremetaverse_types::Vector3,
maximum: f32,
) -> bool {
let (Ok(x), Ok(y), Ok(patch_size)) = (
i16::try_from(x),
i16::try_from(y),
i16::try_from(patch_size),
) else {
return false;
};
let (Some(patch_x), Some(patch_y)) = (x.checked_mul(patch_size), y.checked_mul(patch_size))
else {
return false;
};
let patch_x = f32::from(patch_x) + f32::from(patch_size) * 0.5;
let patch_y = f32::from(patch_y) + f32::from(patch_size) * 0.5;
let dx = patch_x - center.x;
let dy = patch_y - center.y;
dx * dx + dy * dy <= maximum * maximum
}
fn visible_primitive(
simulator: &Simulator,
mut primitive: Primitive,
view: Option<&(Arc<libremetaverse::AgentManager>, f32)>,
) -> Option<Primitive> {
if view.is_none() {
return Some(primitive);
}
let primitives = simulator
.objects_primitives
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let avatars = simulator
.objects_avatars
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
primitive = resolved_primitive(primitive, &primitives, &avatars)?;
visible_position(primitive.position, view).then_some(primitive)
}
fn resolved_primitive(
mut primitive: Primitive,
primitives: &HashMap<u32, Primitive>,
avatars: &HashMap<u32, Avatar>,
) -> Option<Primitive> {
let mut parent_id = primitive.parent_id;
for _ in 0..32 {
if parent_id == 0 {
primitive.parent_id = 0;
return Some(primitive);
}
if let Some(parent) = primitives.get(&parent_id) {
primitive.position = libremetaverse_types::Vector3::add_with_vector3_vector3(
parent.position,
libremetaverse_types::Vector3::mul_with_vector3_quaternion(
primitive.position,
parent.rotation,
),
)
.ok()?;
primitive.rotation = libremetaverse_types::Quaternion::mul_with_quaternion_quaternion(
parent.rotation,
primitive.rotation,
);
parent_id = parent.parent_id;
continue;
}
let avatar = avatars.get(&parent_id)?;
primitive.position = libremetaverse_types::Vector3::add_with_vector3_vector3(
avatar.position,
libremetaverse_types::Vector3::mul_with_vector3_quaternion(
primitive.position,
avatar.rotation,
),
)
.ok()?;
primitive.rotation = libremetaverse_types::Quaternion::mul_with_quaternion_quaternion(
avatar.rotation,
primitive.rotation,
);
primitive.parent_id = 0;
primitive.is_attachment = true;
return Some(primitive);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn region_changes_clear_state_and_dirty_changes_coalesce() {
let first = RegionDescriptor {
id: UUID::new_with_string("00000000-0000-0000-0000-000000000001".into()).unwrap(),
handle: 1,
name: "First".into(),
size: (256, 256),
};
let second = RegionDescriptor {
id: UUID::new_with_string("00000000-0000-0000-0000-000000000002".into()).unwrap(),
handle: 2,
name: "Second".into(),
size: (1024, 1024),
};
let mut world = WorldState::default();
world.apply(WorldEvent::RegionChanged(Some(first)));
world.apply(WorldEvent::TerrainUpdated {
region: second.clone(),
x: 0,
y: 0,
patch_size: 1,
height_map: vec![3.0],
});
assert_eq!(world.region(), Some(&second));
assert_eq!(world.terrain(0, 0).unwrap().height_map.as_ref(), &[3.0]);
assert_eq!(
world.take_dirty(),
vec![DirtyWorldItem::Region, DirtyWorldItem::Terrain(0, 0)]
);
assert!(world.take_dirty().is_empty());
world.apply(WorldEvent::VisibilitySnapshot {
region: second,
primitives: Vec::new(),
avatars: Vec::new(),
terrain: Vec::new(),
});
assert!(world.terrain(0, 0).is_none());
assert_eq!(world.take_dirty(), vec![DirtyWorldItem::Region]);
}
}