cleanup: make the modeling of props better
This commit is contained in:
parent
475d1ef90a
commit
4f0f88504a
2 changed files with 139 additions and 19 deletions
|
|
@ -822,6 +822,50 @@ impl std::fmt::Display for PropOrigin {
|
|||
}
|
||||
}
|
||||
|
||||
/// Source of a prop, combining origin type and ID.
|
||||
///
|
||||
/// This replaces the pattern of two nullable UUID columns (server_prop_id, realm_prop_id)
|
||||
/// with a single enum that encodes both the source type and ID.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PropSource {
|
||||
/// Prop from the server-wide library (server.props)
|
||||
Server(Uuid),
|
||||
/// Prop from a realm-specific library (realm.props)
|
||||
Realm(Uuid),
|
||||
/// Prop uploaded by a user (auth.uploads) - future use
|
||||
Upload(Uuid),
|
||||
}
|
||||
|
||||
impl PropSource {
|
||||
/// Extract the UUID from any variant.
|
||||
pub fn id(&self) -> Uuid {
|
||||
match self {
|
||||
PropSource::Server(id) | PropSource::Realm(id) | PropSource::Upload(id) => *id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PropSource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PropSource::Server(id) => write!(f, "server:{}", id),
|
||||
PropSource::Realm(id) => write!(f, "realm:{}", id),
|
||||
PropSource::Upload(id) => write!(f, "upload:{}", id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PropSource> for PropOrigin {
|
||||
fn from(source: &PropSource) -> Self {
|
||||
match source {
|
||||
PropSource::Server(_) => PropOrigin::ServerLibrary,
|
||||
PropSource::Realm(_) => PropOrigin::RealmLibrary,
|
||||
PropSource::Upload(_) => PropOrigin::UserUpload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An inventory item (user-owned prop).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
|
|
@ -840,6 +884,19 @@ pub struct InventoryItem {
|
|||
pub acquired_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl InventoryItem {
|
||||
/// Get the prop source combining `prop_id` and `origin` into a `PropSource`.
|
||||
///
|
||||
/// Returns `None` if `prop_id` is `None` (shouldn't happen in practice).
|
||||
pub fn prop_source(&self) -> Option<PropSource> {
|
||||
self.prop_id.map(|id| match self.origin {
|
||||
PropOrigin::ServerLibrary => PropSource::Server(id),
|
||||
PropOrigin::RealmLibrary => PropSource::Realm(id),
|
||||
PropOrigin::UserUpload => PropSource::Upload(id),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Response for inventory list.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InventoryResponse {
|
||||
|
|
@ -882,16 +939,69 @@ pub struct PropAcquisitionListResponse {
|
|||
pub props: Vec<PropAcquisitionInfo>,
|
||||
}
|
||||
|
||||
/// A prop dropped in a channel, available for pickup.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct LooseProp {
|
||||
/// Intermediate row type for database queries returning loose props.
|
||||
///
|
||||
/// This maps directly to database columns (with nullable server_prop_id/realm_prop_id)
|
||||
/// and is converted to `LooseProp` with a `PropSource` enum.
|
||||
#[cfg(feature = "ssr")]
|
||||
#[derive(Debug, Clone, sqlx::FromRow)]
|
||||
pub struct LoosePropRow {
|
||||
pub id: Uuid,
|
||||
pub channel_id: Uuid,
|
||||
pub server_prop_id: Option<Uuid>,
|
||||
pub realm_prop_id: Option<Uuid>,
|
||||
pub position_x: f64,
|
||||
pub position_y: f64,
|
||||
pub scale: f32,
|
||||
pub dropped_by: Option<Uuid>,
|
||||
pub expires_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub prop_name: String,
|
||||
pub prop_asset_path: String,
|
||||
pub is_locked: bool,
|
||||
pub locked_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl From<LoosePropRow> for LooseProp {
|
||||
fn from(row: LoosePropRow) -> Self {
|
||||
let source = if let Some(id) = row.server_prop_id {
|
||||
PropSource::Server(id)
|
||||
} else if let Some(id) = row.realm_prop_id {
|
||||
PropSource::Realm(id)
|
||||
} else {
|
||||
// Database CHECK constraint ensures exactly one is set,
|
||||
// but we need a fallback. This should never happen.
|
||||
panic!("LoosePropRow has neither server_prop_id nor realm_prop_id set")
|
||||
};
|
||||
|
||||
LooseProp {
|
||||
id: row.id,
|
||||
channel_id: row.channel_id,
|
||||
source,
|
||||
position_x: row.position_x,
|
||||
position_y: row.position_y,
|
||||
scale: row.scale,
|
||||
dropped_by: row.dropped_by,
|
||||
expires_at: row.expires_at,
|
||||
created_at: row.created_at,
|
||||
prop_name: row.prop_name,
|
||||
prop_asset_path: row.prop_asset_path,
|
||||
is_locked: row.is_locked,
|
||||
locked_by: row.locked_by,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A prop dropped in a channel, available for pickup.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LooseProp {
|
||||
pub id: Uuid,
|
||||
pub channel_id: Uuid,
|
||||
/// The source of this prop (server library, realm library, or user upload).
|
||||
pub source: PropSource,
|
||||
pub position_x: f64,
|
||||
pub position_y: f64,
|
||||
/// Scale factor (0.1 - 10.0) inherited from prop definition at drop time.
|
||||
pub scale: f32,
|
||||
pub dropped_by: Option<Uuid>,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
use sqlx::PgExecutor;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{InventoryItem, LooseProp};
|
||||
use crate::models::{InventoryItem, LooseProp, LoosePropRow, PropSource};
|
||||
use chattyness_error::{AppError, OptionExt};
|
||||
|
||||
/// Ensure an instance exists for a scene.
|
||||
|
|
@ -37,7 +37,7 @@ pub async fn list_channel_loose_props<'e>(
|
|||
executor: impl PgExecutor<'e>,
|
||||
channel_id: Uuid,
|
||||
) -> Result<Vec<LooseProp>, AppError> {
|
||||
let props = sqlx::query_as::<_, LooseProp>(
|
||||
let rows = sqlx::query_as::<_, LoosePropRow>(
|
||||
r#"
|
||||
SELECT
|
||||
lp.id,
|
||||
|
|
@ -66,7 +66,7 @@ pub async fn list_channel_loose_props<'e>(
|
|||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(props)
|
||||
Ok(rows.into_iter().map(LooseProp::from).collect())
|
||||
}
|
||||
|
||||
/// Drop a prop from inventory to the canvas.
|
||||
|
|
@ -224,12 +224,22 @@ pub async fn drop_prop_to_canvas<'e>(
|
|||
Some(prop_name),
|
||||
Some(prop_asset_path),
|
||||
)) => {
|
||||
// Construct PropSource from the nullable columns
|
||||
let source = if let Some(sid) = server_prop_id {
|
||||
PropSource::Server(sid)
|
||||
} else if let Some(rid) = realm_prop_id {
|
||||
PropSource::Realm(rid)
|
||||
} else {
|
||||
return Err(AppError::Internal(
|
||||
"Dropped prop has neither server_prop_id nor realm_prop_id".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
// Success! Convert f32 positions to f64.
|
||||
Ok(LooseProp {
|
||||
id,
|
||||
channel_id,
|
||||
server_prop_id,
|
||||
realm_prop_id,
|
||||
source,
|
||||
position_x: position_x.into(),
|
||||
position_y: position_y.into(),
|
||||
scale,
|
||||
|
|
@ -352,7 +362,7 @@ pub async fn update_loose_prop_scale<'e>(
|
|||
));
|
||||
}
|
||||
|
||||
let prop = sqlx::query_as::<_, LooseProp>(
|
||||
let row = sqlx::query_as::<_, LoosePropRow>(
|
||||
r#"
|
||||
WITH updated AS (
|
||||
UPDATE scene.loose_props
|
||||
|
|
@ -399,7 +409,7 @@ pub async fn update_loose_prop_scale<'e>(
|
|||
.await?
|
||||
.or_not_found("Loose prop (may have expired)")?;
|
||||
|
||||
Ok(prop)
|
||||
Ok(LooseProp::from(row))
|
||||
}
|
||||
|
||||
/// Get a loose prop by ID.
|
||||
|
|
@ -407,7 +417,7 @@ pub async fn get_loose_prop_by_id<'e>(
|
|||
executor: impl PgExecutor<'e>,
|
||||
loose_prop_id: Uuid,
|
||||
) -> Result<Option<LooseProp>, AppError> {
|
||||
let prop = sqlx::query_as::<_, LooseProp>(
|
||||
let row = sqlx::query_as::<_, LoosePropRow>(
|
||||
r#"
|
||||
SELECT
|
||||
lp.id,
|
||||
|
|
@ -435,7 +445,7 @@ pub async fn get_loose_prop_by_id<'e>(
|
|||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(prop)
|
||||
Ok(row.map(LooseProp::from))
|
||||
}
|
||||
|
||||
/// Move a loose prop to a new position.
|
||||
|
|
@ -445,7 +455,7 @@ pub async fn move_loose_prop<'e>(
|
|||
x: f64,
|
||||
y: f64,
|
||||
) -> Result<LooseProp, AppError> {
|
||||
let prop = sqlx::query_as::<_, LooseProp>(
|
||||
let row = sqlx::query_as::<_, LoosePropRow>(
|
||||
r#"
|
||||
WITH updated AS (
|
||||
UPDATE scene.loose_props
|
||||
|
|
@ -493,7 +503,7 @@ pub async fn move_loose_prop<'e>(
|
|||
.await?
|
||||
.or_not_found("Loose prop (may have expired)")?;
|
||||
|
||||
Ok(prop)
|
||||
Ok(LooseProp::from(row))
|
||||
}
|
||||
|
||||
/// Lock a loose prop (moderator only).
|
||||
|
|
@ -502,7 +512,7 @@ pub async fn lock_loose_prop<'e>(
|
|||
loose_prop_id: Uuid,
|
||||
locked_by: Uuid,
|
||||
) -> Result<LooseProp, AppError> {
|
||||
let prop = sqlx::query_as::<_, LooseProp>(
|
||||
let row = sqlx::query_as::<_, LoosePropRow>(
|
||||
r#"
|
||||
WITH updated AS (
|
||||
UPDATE scene.loose_props
|
||||
|
|
@ -549,7 +559,7 @@ pub async fn lock_loose_prop<'e>(
|
|||
.await?
|
||||
.or_not_found("Loose prop (may have expired)")?;
|
||||
|
||||
Ok(prop)
|
||||
Ok(LooseProp::from(row))
|
||||
}
|
||||
|
||||
/// Unlock a loose prop (moderator only).
|
||||
|
|
@ -557,7 +567,7 @@ pub async fn unlock_loose_prop<'e>(
|
|||
executor: impl PgExecutor<'e>,
|
||||
loose_prop_id: Uuid,
|
||||
) -> Result<LooseProp, AppError> {
|
||||
let prop = sqlx::query_as::<_, LooseProp>(
|
||||
let row = sqlx::query_as::<_, LoosePropRow>(
|
||||
r#"
|
||||
WITH updated AS (
|
||||
UPDATE scene.loose_props
|
||||
|
|
@ -603,7 +613,7 @@ pub async fn unlock_loose_prop<'e>(
|
|||
.await?
|
||||
.or_not_found("Loose prop (may have expired)")?;
|
||||
|
||||
Ok(prop)
|
||||
Ok(LooseProp::from(row))
|
||||
}
|
||||
|
||||
/// Delete expired loose props.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue