Centralize world and viewport game loops
This commit is contained in:
767
crates/metacrate-grid-world/src/lib.rs
Normal file
767
crates/metacrate-grid-world/src/lib.rs
Normal 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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user