add the ability to make props non-droppable

This commit is contained in:
Evan Carroll 2026-01-13 17:46:56 -06:00
parent ea3b444d71
commit 845d64c981
5 changed files with 82 additions and 14 deletions

View file

@ -614,6 +614,7 @@ pub struct InventoryItem {
pub layer: Option<AvatarLayer>, pub layer: Option<AvatarLayer>,
pub is_transferable: bool, pub is_transferable: bool,
pub is_portable: bool, pub is_portable: bool,
pub is_droppable: bool,
pub origin: PropOrigin, pub origin: PropOrigin,
pub acquired_at: DateTime<Utc>, pub acquired_at: DateTime<Utc>,
} }
@ -663,6 +664,7 @@ pub struct ServerProp {
pub is_unique: bool, pub is_unique: bool,
pub is_transferable: bool, pub is_transferable: bool,
pub is_portable: bool, pub is_portable: bool,
pub is_droppable: bool,
pub is_active: bool, pub is_active: bool,
pub available_from: Option<DateTime<Utc>>, pub available_from: Option<DateTime<Utc>>,
pub available_until: Option<DateTime<Utc>>, pub available_until: Option<DateTime<Utc>>,

View file

@ -20,6 +20,7 @@ pub async fn list_user_inventory<'e>(
layer, layer,
is_transferable, is_transferable,
is_portable, is_portable,
is_droppable,
origin, origin,
acquired_at acquired_at
FROM props.inventory FROM props.inventory
@ -35,16 +36,30 @@ pub async fn list_user_inventory<'e>(
} }
/// Drop a prop (remove from inventory). /// Drop a prop (remove from inventory).
/// Returns an error if the prop is non-droppable (essential prop).
pub async fn drop_inventory_item<'e>( pub async fn drop_inventory_item<'e>(
executor: impl PgExecutor<'e>, executor: impl PgExecutor<'e>,
user_id: Uuid, user_id: Uuid,
item_id: Uuid, item_id: Uuid,
) -> Result<(), AppError> { ) -> Result<(), AppError> {
let result = sqlx::query( // Use a CTE to check existence/droppability and delete in a single query
// Returns: (existed, was_droppable, was_deleted)
let result: Option<(bool, bool, bool)> = sqlx::query_as(
r#" r#"
DELETE FROM props.inventory WITH item_info AS (
SELECT id, is_droppable
FROM props.inventory
WHERE id = $1 AND user_id = $2 WHERE id = $1 AND user_id = $2
),
deleted AS (
DELETE FROM props.inventory
WHERE id = $1 AND user_id = $2 AND is_droppable = true
RETURNING id RETURNING id
)
SELECT
EXISTS(SELECT 1 FROM item_info) AS existed,
COALESCE((SELECT is_droppable FROM item_info), false) AS was_droppable,
EXISTS(SELECT 1 FROM deleted) AS was_deleted
"#, "#,
) )
.bind(item_id) .bind(item_id)
@ -52,11 +67,27 @@ pub async fn drop_inventory_item<'e>(
.fetch_optional(executor) .fetch_optional(executor)
.await?; .await?;
if result.is_none() { match result {
Some((false, _, _)) | None => {
return Err(AppError::NotFound( return Err(AppError::NotFound(
"Inventory item not found or not owned by user".to_string(), "Inventory item not found or not owned by user".to_string(),
)); ));
} }
Some((true, false, _)) => {
return Err(AppError::Forbidden(
"This prop cannot be dropped - it is an essential prop".to_string(),
));
}
Some((true, true, true)) => {
// Successfully deleted
}
Some((true, true, false)) => {
// Should not happen - item existed, was droppable, but wasn't deleted
return Err(AppError::Internal(
"Unexpected error dropping inventory item".to_string(),
));
}
}
Ok(()) Ok(())
} }

View file

@ -46,14 +46,40 @@ pub async fn list_channel_loose_props<'e>(
/// ///
/// Deletes from inventory and inserts into loose_props with 30-minute expiry. /// Deletes from inventory and inserts into loose_props with 30-minute expiry.
/// Returns the created loose prop. /// Returns the created loose prop.
pub async fn drop_prop_to_canvas<'e>( /// Returns an error if the prop is non-droppable (essential prop).
executor: impl PgExecutor<'e>, pub async fn drop_prop_to_canvas(
pool: &sqlx::PgPool,
inventory_item_id: Uuid, inventory_item_id: Uuid,
user_id: Uuid, user_id: Uuid,
channel_id: Uuid, channel_id: Uuid,
position_x: f64, position_x: f64,
position_y: f64, position_y: f64,
) -> Result<LooseProp, AppError> { ) -> Result<LooseProp, AppError> {
// First check if the item exists and is droppable
let item_check: Option<(bool,)> = sqlx::query_as(
r#"SELECT is_droppable FROM props.inventory WHERE id = $1 AND user_id = $2"#,
)
.bind(inventory_item_id)
.bind(user_id)
.fetch_optional(pool)
.await?;
match item_check {
None => {
return Err(AppError::NotFound(
"Inventory item not found or not owned by user".to_string(),
));
}
Some((false,)) => {
return Err(AppError::Forbidden(
"This prop cannot be dropped - it is an essential prop".to_string(),
));
}
Some((true,)) => {
// Item is droppable, proceed with the drop operation
}
}
// Use a CTE to delete from inventory and insert to loose_props in one query // Use a CTE to delete from inventory and insert to loose_props in one query
let prop = sqlx::query_as::<_, LooseProp>( let prop = sqlx::query_as::<_, LooseProp>(
r#" r#"
@ -111,10 +137,10 @@ pub async fn drop_prop_to_canvas<'e>(
.bind(channel_id) .bind(channel_id)
.bind(position_x as f32) .bind(position_x as f32)
.bind(position_y as f32) .bind(position_y as f32)
.fetch_optional(executor) .fetch_optional(pool)
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
AppError::NotFound("Inventory item not found or not owned by user".to_string()) AppError::Internal("Unexpected error dropping prop to canvas".to_string())
})?; })?;
Ok(prop) Ok(prop)
@ -144,6 +170,7 @@ pub async fn pick_up_loose_prop<'e>(
COALESCE(sp.default_layer, rp.default_layer) as layer, COALESCE(sp.default_layer, rp.default_layer) as layer,
COALESCE(sp.is_transferable, rp.is_transferable) as is_transferable, COALESCE(sp.is_transferable, rp.is_transferable) as is_transferable,
COALESCE(sp.is_portable, true) as is_portable, COALESCE(sp.is_portable, true) as is_portable,
COALESCE(sp.is_droppable, rp.is_droppable, true) as is_droppable,
dp.server_prop_id, dp.server_prop_id,
dp.realm_prop_id dp.realm_prop_id
FROM deleted_prop dp FROM deleted_prop dp
@ -161,6 +188,7 @@ pub async fn pick_up_loose_prop<'e>(
origin, origin,
is_transferable, is_transferable,
is_portable, is_portable,
is_droppable,
provenance, provenance,
acquired_at acquired_at
) )
@ -174,10 +202,11 @@ pub async fn pick_up_loose_prop<'e>(
'server_library'::props.prop_origin, 'server_library'::props.prop_origin,
COALESCE(si.is_transferable, true), COALESCE(si.is_transferable, true),
COALESCE(si.is_portable, true), COALESCE(si.is_portable, true),
COALESCE(si.is_droppable, true),
'[]'::jsonb, '[]'::jsonb,
now() now()
FROM source_info si FROM source_info si
RETURNING id, prop_name, prop_asset_path, layer, is_transferable, is_portable, acquired_at RETURNING id, prop_name, prop_asset_path, layer, is_transferable, is_portable, is_droppable, acquired_at
) )
SELECT SELECT
ii.id, ii.id,
@ -186,6 +215,7 @@ pub async fn pick_up_loose_prop<'e>(
ii.layer, ii.layer,
ii.is_transferable, ii.is_transferable,
ii.is_portable, ii.is_portable,
ii.is_droppable,
'server_library'::props.prop_origin as origin, 'server_library'::props.prop_origin as origin,
ii.acquired_at ii.acquired_at
FROM inserted_item ii FROM inserted_item ii

View file

@ -51,6 +51,7 @@ pub async fn get_server_prop_by_id<'e>(
is_unique, is_unique,
is_transferable, is_transferable,
is_portable, is_portable,
is_droppable,
is_active, is_active,
available_from, available_from,
available_until, available_until,
@ -139,6 +140,7 @@ pub async fn create_server_prop<'e>(
is_unique, is_unique,
is_transferable, is_transferable,
is_portable, is_portable,
is_droppable,
is_active, is_active,
available_from, available_from,
available_until, available_until,

View file

@ -285,6 +285,9 @@ async fn handle_socket(
// needs user_id match anyway) // needs user_id match anyway)
drop(conn); drop(conn);
// Clone pool for use in the receive task (needed for multi-query operations)
let recv_pool = pool.clone();
// Spawn task to handle incoming messages from client // Spawn task to handle incoming messages from client
let recv_task = tokio::spawn(async move { let recv_task = tokio::spawn(async move {
while let Some(Ok(msg)) = receiver.next().await { while let Some(Ok(msg)) = receiver.next().await {
@ -401,7 +404,7 @@ async fn handle_socket(
let pos_y = member.position_y + offset_y; let pos_y = member.position_y + offset_y;
match loose_props::drop_prop_to_canvas( match loose_props::drop_prop_to_canvas(
&mut *recv_conn, &recv_pool,
inventory_item_id, inventory_item_id,
user_id, user_id,
channel_id, channel_id,