Implement native AIS inventory reconciliation (#63)
All checks were successful
Native code generation / deterministic (push) Successful in 13m18s
Imaging and meshing gate / native (push) Successful in 4m12s
Native Rust workspace compile / compile (push) Successful in 4m12s

This commit is contained in:
2026-08-10 06:41:29 +00:00
parent 61d6721287
commit d5c318d280
16 changed files with 1896 additions and 161 deletions

View File

@@ -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,