use std::{collections::HashSet, path::PathBuf};
use crate::errors::InventoryError;
use benthic_protocol::objects::{GeneratorObject, MinimalObjectUpdate};
use glam::{Quat, Vec3};
use metaverse_messages::{
udp::object::object_update::ExtraParams,
utils::{object_types::ObjectType, texture_entry::TextureEntry},
};
use sqlx::{Row, SqlitePool};
use uuid::Uuid;
fn vec3_from_row(
row: &sqlx::sqlite::SqliteRow,
x: &str,
y: &str,
z: &str,
) -> Result<Vec3, sqlx::Error> {
Ok(Vec3::new(
row.try_get::<f32, &str>(x)?,
row.try_get::<f32, &str>(y)?,
row.try_get::<f32, &str>(z)?,
))
}
fn quat_from_row(
row: &sqlx::sqlite::SqliteRow,
x: &str,
y: &str,
z: &str,
w: &str,
) -> Result<Quat, sqlx::Error> {
Ok(Quat::from_xyzw(
row.try_get::<f32, &str>(x)?,
row.try_get::<f32, &str>(y)?,
row.try_get::<f32, &str>(z)?,
row.try_get::<f32, &str>(w)?,
))
}
pub async fn sqlite_check_cache(
pool: &SqlitePool,
id: u32,
crc: u32,
region_id: String,
) -> Result<(Uuid, PathBuf, Option<PathBuf>, GeneratorObject), InventoryError> {
let row = sqlx::query(
r#"
SELECT
asset_id,
glb,
full_id,
id,
parent,
scale_x, scale_y, scale_z,
rot_x, rot_y, rot_z, rot_w,
pos_x, pos_y, pos_z
FROM object_updates
WHERE id = ?
AND crc = ?
AND region_id = ?
"#,
)
.bind(id)
.bind(crc)
.bind(region_id)
.fetch_optional(pool)
.await?
.ok_or_else(|| InventoryError::CacheMiss("Object not found in cache".to_string()))?;
let asset_id: String = row.try_get("asset_id")?;
let asset_id = Uuid::parse_str(&asset_id)?;
let glb: Option<String> = row.try_get("glb")?;
let glb_path = glb.as_ref().map(PathBuf::from);
let generator = GeneratorObject {
full_id: {
let id_str: String = row.try_get("full_id")?;
Uuid::parse_str(&id_str)?
},
local_id: row.try_get("id")?,
parent_id: row.try_get("parent")?,
position: vec3_from_row(&row, "pos_x", "pos_y", "pos_z")?,
scale: vec3_from_row(&row, "scale_x", "scale_y", "scale_z")?,
rotation: quat_from_row(&row, "rot_x", "rot_y", "rot_z", "rot_w")?,
};
Ok((
asset_id,
glb_path.clone().unwrap_or_default(),
glb_path,
generator,
))
}
pub async fn sqlite_update_object_json_path(
pool: &SqlitePool,
full_id: Uuid,
asset_id: Uuid,
json_path: &str,
) -> Result<(), InventoryError> {
sqlx::query(
r#"
UPDATE object_updates
SET asset_id = ?,
json = ?
WHERE full_id = ?
"#,
)
.bind(asset_id.to_string())
.bind(json_path)
.bind(full_id.to_string())
.execute(pool)
.await?;
Ok(())
}
pub async fn sqlite_update_object_glb_path(
pool: &SqlitePool,
full_id: Uuid,
glb_path: &str,
) -> Result<(), InventoryError> {
sqlx::query(
r#"
UPDATE object_updates
SET glb = ?
WHERE full_id = ?
"#,
)
.bind(glb_path)
.bind(full_id.to_string())
.execute(pool)
.await?;
Ok(())
}
pub async fn sqlite_insert_object_update(
pool: &SqlitePool,
object: MinimalObjectUpdate<ExtraParams, TextureEntry, ObjectType>,
) -> Result<(), InventoryError> {
let parent_id = object.parent_id.unwrap_or(0);
let rotation = object.rotation.normalize();
sqlx::query(
r#"
INSERT INTO object_updates (
id, full_id, crc, region_id, parent, pcode,
pos_x, pos_y, pos_z,
rot_x, rot_y, rot_z, rot_w,
scale_x, scale_y, scale_z
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(full_id) DO UPDATE SET
id = excluded.id,
parent = excluded.parent,
crc = excluded.crc,
region_id = excluded.region_id,
pcode = excluded.pcode,
pos_x = excluded.pos_x,
pos_y = excluded.pos_y,
pos_z = excluded.pos_z,
rot_x = excluded.rot_x,
rot_y = excluded.rot_y,
rot_z = excluded.rot_z,
rot_w = excluded.rot_w,
scale_x = excluded.scale_x,
scale_y = excluded.scale_y,
scale_z = excluded.scale_z
"#,
)
.bind(object.local_id as i64)
.bind(object.full_id.to_string())
.bind(object.crc)
.bind(object.region_id)
.bind(parent_id as i64)
.bind(object.object_type.to_string())
.bind(object.position.x)
.bind(object.position.y)
.bind(object.position.z)
.bind(rotation.x)
.bind(rotation.y)
.bind(rotation.z)
.bind(rotation.w)
.bind(object.scale.x)
.bind(object.scale.y)
.bind(object.scale.z)
.execute(pool)
.await?;
Ok(())
}
pub async fn sqlite_get_parent(
pool: &SqlitePool,
object_id: u32,
) -> Result<Option<u32>, InventoryError> {
let row = sqlx::query("SELECT parent FROM object_updates WHERE id = ?")
.bind(object_id as i64)
.fetch_one(pool)
.await?;
Ok(row.try_get("parent")?)
}
pub async fn sqlite_get_object_scale_rotation_position(
pool: &SqlitePool,
object_id: u32,
) -> Result<(Vec3, Quat, Vec3), InventoryError> {
let row = sqlx::query(
r#"
SELECT
pos_x, pos_y, pos_z,
rot_x, rot_y, rot_z, rot_w,
scale_x, scale_y, scale_z
FROM object_updates
WHERE id = ?
"#,
)
.bind(object_id as i64)
.fetch_one(pool)
.await?;
let position = vec3_from_row(&row, "pos_x", "pos_y", "pos_z")?;
let scale = vec3_from_row(&row, "scale_x", "scale_y", "scale_z")?;
let rotation = quat_from_row(&row, "rot_x", "rot_y", "rot_z", "rot_w")?;
Ok((scale, rotation, position))
}
pub async fn set_object_transform_by_id(
pool: &SqlitePool,
object_id: u32,
position: Vec3,
rotation: Quat,
scale: Vec3,
) -> Result<(), InventoryError> {
let rotation = rotation.normalize();
sqlx::query(
r#"
UPDATE object_updates
SET
pos_x = ?,
pos_y = ?,
pos_z = ?,
rot_x = ?,
rot_y = ?,
rot_z = ?,
rot_w = ?,
scale_x = ?,
scale_y = ?,
scale_z = ?
WHERE id = ?
"#,
)
.bind(position.x)
.bind(position.y)
.bind(position.z)
.bind(rotation.x)
.bind(rotation.y)
.bind(rotation.z)
.bind(rotation.w)
.bind(scale.x)
.bind(scale.y)
.bind(scale.z)
.bind(object_id as i64)
.execute(pool)
.await?;
Ok(())
}
pub async fn get_missing_object_updates(
pool: &SqlitePool,
ids: &[Uuid],
) -> Result<Vec<Uuid>, sqlx::Error> {
if ids.is_empty() {
return Ok(Vec::new());
}
let mut query = sqlx::QueryBuilder::<sqlx::Sqlite>::new(
"SELECT full_id FROM object_updates WHERE full_id IN (",
);
let mut separated = query.separated(", ");
for id in ids {
separated.push_bind(id.to_string());
}
separated.push_unseparated(")");
let rows = query.build().fetch_all(pool).await?;
let existing_ids: HashSet<Uuid> = rows
.iter()
.filter_map(|row| row.try_get::<String, _>("full_id").ok())
.filter_map(|id| Uuid::parse_str(&id).ok())
.collect();
Ok(ids
.iter()
.copied()
.filter(|id| !existing_ids.contains(id))
.collect())
}