Implement native AIS inventory reconciliation (#63)
This commit is contained in:
@@ -855,6 +855,35 @@ enum InventoryValue {
|
||||
}
|
||||
|
||||
impl InventoryValue {
|
||||
fn from_item_typed(item: InventoryItem) -> Self {
|
||||
macro_rules! typed {
|
||||
($kind:ty, $variant:ident) => {{
|
||||
let mut value = <$kind>::new(item.base.uuid)
|
||||
.expect("known inventory item constructor cannot fail");
|
||||
value.base = item;
|
||||
Self::$variant(value)
|
||||
}};
|
||||
}
|
||||
match item.inventory_type {
|
||||
InventoryType::TEXTURE => typed!(InventoryTexture, Texture),
|
||||
InventoryType::SOUND => typed!(InventorySound, Sound),
|
||||
InventoryType::CALLING_CARD => typed!(InventoryCallingCard, CallingCard),
|
||||
InventoryType::LANDMARK => typed!(InventoryLandmark, Landmark),
|
||||
InventoryType::OBJECT => typed!(InventoryObject, Object),
|
||||
InventoryType::NOTECARD => typed!(InventoryNotecard, Notecard),
|
||||
InventoryType::CATEGORY => typed!(InventoryCategory, Category),
|
||||
InventoryType::LSL => typed!(InventoryLSL, Lsl),
|
||||
InventoryType::SNAPSHOT => typed!(InventorySnapshot, Snapshot),
|
||||
InventoryType::ATTACHMENT => typed!(InventoryAttachment, Attachment),
|
||||
InventoryType::WEARABLE => typed!(InventoryWearable, Wearable),
|
||||
InventoryType::ANIMATION => typed!(InventoryAnimation, Animation),
|
||||
InventoryType::GESTURE => typed!(InventoryGesture, Gesture),
|
||||
InventoryType::SETTINGS => typed!(InventorySettings, Settings),
|
||||
InventoryType::MATERIAL => typed!(InventoryMaterial, Material),
|
||||
_ => Self::Item(item),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_class(value: &dyn InventoryObjectClass) -> Option<Self> {
|
||||
macro_rules! downcast {
|
||||
($type:ty, $variant:ident) => {
|
||||
@@ -1472,6 +1501,161 @@ impl Inventory {
|
||||
.and_then(|value| value.item().cloned())
|
||||
}
|
||||
|
||||
/// Atomically applies one fully parsed AIS response. A detached candidate
|
||||
/// graph is built first, so validation or rebuilding failures cannot expose
|
||||
/// a partially reconciled inventory to readers.
|
||||
#[allow(clippy::too_many_lines)] // One lock-free candidate build followed by one state swap.
|
||||
pub(crate) fn native_apply_ais_batch(
|
||||
&self,
|
||||
folders: Vec<InventoryFolder>,
|
||||
items: Vec<InventoryItem>,
|
||||
removed_items: &[UUID],
|
||||
removed_categories: &[UUID],
|
||||
broken_links: &[UUID],
|
||||
versions: &HashMap<UUID, i32>,
|
||||
) -> Result<(), Error> {
|
||||
let mut incoming = HashSet::new();
|
||||
for uuid in folders
|
||||
.iter()
|
||||
.map(|folder| folder.base.uuid)
|
||||
.chain(items.iter().map(|item| item.base.uuid))
|
||||
{
|
||||
if uuid == UUID::zero() || !incoming.insert(uuid) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
}
|
||||
if incoming.len() > MAX_CACHE_RECORDS || versions.keys().any(|uuid| *uuid == UUID::zero()) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut touched = incoming.clone();
|
||||
touched.extend(versions.keys().copied());
|
||||
|
||||
let (mut candidate, old_values) = {
|
||||
let state = read(&self.inner.state);
|
||||
let old_values: HashMap<_, _> = state
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|(uuid, node)| node.value().map(|value| (*uuid, value)))
|
||||
.collect();
|
||||
let mut candidate = InventoryState {
|
||||
root: state.root,
|
||||
library_root: state.library_root,
|
||||
..InventoryState::default()
|
||||
};
|
||||
for (uuid, value) in &old_values {
|
||||
candidate
|
||||
.items
|
||||
.insert(*uuid, InventoryNode::from_value(value.clone()));
|
||||
}
|
||||
(candidate, old_values)
|
||||
};
|
||||
|
||||
for folder in folders {
|
||||
candidate.items.insert(
|
||||
folder.base.uuid,
|
||||
InventoryNode::from_value(InventoryValue::Folder(folder)),
|
||||
);
|
||||
}
|
||||
for item in items {
|
||||
let uuid = item.base.uuid;
|
||||
let value = InventoryValue::from_item_typed(item);
|
||||
candidate
|
||||
.items
|
||||
.insert(uuid, InventoryNode::from_value(value));
|
||||
}
|
||||
for (uuid, version) in versions {
|
||||
if let Some(node) = candidate.items.get(uuid) {
|
||||
let Some(mut value) = node.value() else {
|
||||
return Err(Error::InvalidOperation);
|
||||
};
|
||||
let Some(folder) = value.folder_mut() else {
|
||||
return Err(Error::InvalidOperation);
|
||||
};
|
||||
folder.version = *version;
|
||||
node.set_value(value);
|
||||
}
|
||||
}
|
||||
|
||||
let direct_removals: HashSet<_> = removed_items
|
||||
.iter()
|
||||
.chain(broken_links)
|
||||
.copied()
|
||||
.filter(|uuid| *uuid != UUID::zero())
|
||||
.collect();
|
||||
for uuid in direct_removals {
|
||||
candidate.items.remove(&uuid);
|
||||
}
|
||||
rebuild_indexes_and_counts(&mut candidate);
|
||||
for category in removed_categories
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|uuid| *uuid != UUID::zero())
|
||||
{
|
||||
let mut pending = vec![category];
|
||||
let mut descendants = HashSet::new();
|
||||
while let Some(uuid) = pending.pop() {
|
||||
if descendants.len() >= MAX_CACHE_RECORDS || !descendants.insert(uuid) {
|
||||
continue;
|
||||
}
|
||||
if let Some(node) = candidate.items.get(&uuid) {
|
||||
pending.extend(read(&node.inner).children.keys().copied());
|
||||
}
|
||||
}
|
||||
for uuid in descendants {
|
||||
candidate.items.remove(&uuid);
|
||||
}
|
||||
if candidate.root == Some(category) {
|
||||
candidate.root = None;
|
||||
}
|
||||
if candidate.library_root == Some(category) {
|
||||
candidate.library_root = None;
|
||||
}
|
||||
}
|
||||
if candidate.items.len() > MAX_CACHE_RECORDS {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
rebuild_indexes_and_counts(&mut candidate);
|
||||
candidate.root_node = candidate
|
||||
.root
|
||||
.and_then(|uuid| candidate.items.get(&uuid).cloned());
|
||||
candidate.library_root_node = candidate
|
||||
.library_root
|
||||
.and_then(|uuid| candidate.items.get(&uuid).cloned());
|
||||
|
||||
let new_values: HashMap<_, _> = candidate
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|(uuid, node)| node.value().map(|value| (*uuid, value)))
|
||||
.collect();
|
||||
*write(&self.inner.state) = candidate;
|
||||
|
||||
for (uuid, old) in &old_values {
|
||||
match new_values.get(uuid) {
|
||||
None => self
|
||||
.inner
|
||||
.removed
|
||||
.emit(InventoryObjectRemovedEventArgs::from_value(old.clone())),
|
||||
Some(new) if touched.contains(uuid) => {
|
||||
self.inner
|
||||
.updated
|
||||
.emit(InventoryObjectUpdatedEventArgs::from_values(
|
||||
old.clone(),
|
||||
new.clone(),
|
||||
));
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
for (uuid, new) in new_values {
|
||||
if !old_values.contains_key(&uuid) {
|
||||
self.inner
|
||||
.added
|
||||
.emit(InventoryObjectAddedEventArgs::from_value(new));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn try_get_value_with_uuid_inventory_base(
|
||||
&self,
|
||||
uuid: UUID,
|
||||
|
||||
Reference in New Issue
Block a user