Fix PR 3 review findings [skip ci]

This commit is contained in:
SCOPEDD committed 2026-09-29 23:21:14 -04:00
1 parent 590178e41a
commit 454620f21e
21 files changed
+522 -160

No files matched your search

+3 -1
View File
@@ -496,6 +496,7 @@ pub async fn server_market_buy(
.bind(&payload.buyer_uuid)
.execute(&mut *tx)
.await?;
let new_balance = buyer_bal - price;
// Pay seller
sqlx::query(
@@ -543,6 +544,7 @@ pub async fn server_market_buy(
"item_data": item_data,
"item_name": item_name,
"amount": amount,
"price": price
"price": price,
"new_balance": new_balance
})).await
}
-1
View File
@@ -1047,7 +1047,6 @@ pub async fn server_unclaim_chunk(
)
.bind(&guild_id)
.bind(&payload.uuid)
.bind(&server.instance_id)
.fetch_optional(&state.db)
.await?;
+59 -1
View File
@@ -14,7 +14,65 @@ pub fn current_daily_key() -> String {
}
pub fn current_weekly_key() -> String {
chrono::Utc::now().format("%Y-W%V").to_string()
chrono::Utc::now().format("%G-W%V").to_string()
}
fn action_matches(target: &str, action: &str) -> bool {
let Some(target) = target.strip_prefix("action:") else { return false };
let (target_action, target_dimension) = target.split_once('@').map_or((target, None), |(a, d)| (a, Some(d)));
let (actual_action, actual_dimension) = action.split_once('@').map_or((action, None), |(a, d)| (a, Some(d)));
if target_dimension.is_some_and(|d| Some(d) != actual_dimension) { return false; }
let Some((target_kind, materials)) = target_action.split_once(':') else { return false };
let Some((actual_kind, material)) = actual_action.split_once(':') else { return false };
if target_kind != actual_kind { return false; }
materials.split('|').any(|pattern| {
if pattern == "*" { true }
else if let Some(suffix) = pattern.strip_prefix('*') { material.ends_with(suffix) }
else if let Some(prefix) = pattern.strip_suffix('*') { material.starts_with(prefix) }
else { material == pattern }
})
}
#[cfg(test)]
mod action_tests {
use super::action_matches;
#[test]
fn only_matching_materials_and_dimensions_advance_quests() {
assert!(action_matches("action:block_broken:DIAMOND_ORE|DEEPSLATE_DIAMOND_ORE", "block_broken:DIAMOND_ORE@minecraft:overworld"));
assert!(!action_matches("action:block_broken:DIAMOND_ORE", "block_broken:STONE@minecraft:overworld"));
assert!(action_matches("action:block_placed:*_PLANKS", "block_placed:OAK_PLANKS@minecraft:overworld"));
assert!(action_matches("action:block_broken:*@minecraft:the_nether", "block_broken:NETHERRACK@minecraft:the_nether"));
assert!(!action_matches("action:block_broken:*@minecraft:the_nether", "block_broken:STONE@minecraft:overworld"));
}
}
/// Advance quests that require a particular block, mob, or dimension rather
/// than a generic stat counter.
pub async fn advance_action_quests(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, uuid: &str, action: &str, count: i64, now: &str,
) -> AppResult<()> {
if count <= 0 { return Ok(()); }
let rows: Vec<(String, String, String, i64)> = sqlx::query_as(
"SELECT id, period, target_stat, target_count FROM quests WHERE enabled = 1 AND target_stat LIKE 'action:%'",
).fetch_all(&mut **tx).await?;
let daily = current_daily_key();
let weekly = current_weekly_key();
for (id, period, target, required) in rows {
if !action_matches(&target, action) { continue; }
let key = if period == "weekly" { &weekly } else { &daily };
sqlx::query(
"INSERT INTO user_quests (user_uuid, quest_id, period_key, progress, completed, claimed, updated_at)
VALUES (?, ?, ?, ?, ?, 0, ?)
ON CONFLICT (user_uuid, quest_id, period_key) DO UPDATE SET
progress = progress + excluded.progress,
completed = CASE WHEN progress + excluded.progress >= ? THEN 1 ELSE completed END,
updated_at = excluded.updated_at",
)
.bind(uuid).bind(&id).bind(key).bind(count).bind(count >= required).bind(now).bind(required)
.execute(&mut **tx).await?;
}
Ok(())
}
/// Get all active daily and weekly quests for the authenticated user with live progress.
+35 -3
View File
@@ -392,6 +392,17 @@ pub struct Sync {
pub async fn sync(GameServer(server): GameServer, State(state): State<AppState>, Json(s): Json<Sync>) -> AppResult<Json<Value>> {
let at_now = now();
let mut tx = state.db.begin().await?;
let leveling_on = s.features.as_ref().and_then(|f| f.leveling_enabled).unwrap_or(true);
let mut xp_before = std::collections::HashMap::new();
if !leveling_on {
for uuid in s.stats.iter().filter_map(|d| dashed(&d.uuid))
.chain(s.events.iter().filter_map(|e| e.uuid.as_deref().and_then(dashed))) {
if xp_before.contains_key(&uuid) { continue; }
let xp: i64 = sqlx::query_scalar("SELECT global_xp FROM user_levels WHERE uuid = ?")
.bind(&uuid).fetch_optional(&mut *tx).await?.unwrap_or(0);
xp_before.insert(uuid, xp);
}
}
// Record the receipt in the same transaction as the deltas. A response
// lost after commit can then be retried without counting activity twice.
@@ -476,6 +487,7 @@ pub async fn sync(GameServer(server): GameServer, State(state): State<AppState>,
}
let achievements_on = s.features.as_ref().and_then(|f| f.achievements_enabled).unwrap_or(true);
let quests_on = s.features.as_ref().and_then(|f| f.quests_enabled).unwrap_or(true);
for e in s.events.iter().filter(|_| fresh).take(MAX_EVENTS_PER_SYNC) {
let kind = clip(&e.kind, 24).to_ascii_lowercase();
@@ -494,8 +506,18 @@ pub async fn sync(GameServer(server): GameServer, State(state): State<AppState>,
.bind(&kind)
.bind(e.detail.as_deref().map(|d| clip(d, 256)))
.bind(at)
.execute(&mut *tx)
.await?;
.execute(&mut *tx)
.await?;
if quests_on && kind == "action" {
if let (Some(uuid), Some(detail)) = (e.uuid.as_deref().and_then(dashed), e.detail.as_deref()) {
if let Some((action, count)) = detail.rsplit_once(" +") {
if let Ok(count) = count.parse::<i64>() {
crate::routes::quests::advance_action_quests(&mut tx, &uuid, action, count.clamp(0, 1_000_000), &at_now).await?;
}
}
}
}
// Check if event unlocks an achievement (if achievements enabled)
if achievements_on {
@@ -523,7 +545,17 @@ pub async fn sync(GameServer(server): GameServer, State(state): State<AppState>,
let mut rewarded = std::collections::HashSet::new();
for d in &s.stats { if let Some(uuid) = dashed(&d.uuid) { rewarded.insert(uuid); } }
for e in &s.events { if let Some(uuid) = e.uuid.as_deref().and_then(dashed) { rewarded.insert(uuid); } }
for uuid in rewarded { crate::routes::leveling::grant_rewards(&mut tx, &uuid, &at_now).await?; }
if leveling_on {
for uuid in rewarded { crate::routes::leveling::grant_rewards(&mut tx, &uuid, &at_now).await?; }
} else {
// Achievement unlocks remain enabled, but their trigger must not grant
// progression on a server whose leveling module is disabled.
for (uuid, xp) in xp_before {
let level = crate::routes::leveling::level_from_xp(xp).0;
sqlx::query("UPDATE user_levels SET global_xp = ?, global_level = ? WHERE uuid = ?")
.bind(xp).bind(level).bind(&uuid).execute(&mut *tx).await?;
}
}
tx.commit().await?;
// Occasional housekeeping.
+30 -9
View File
@@ -1,6 +1,6 @@
//! Friends, Direct Messages, Game Invites, Player Profiles, and Social Feed.
use crate::auth::AuthUser;
use crate::auth::{AuthUser, MaybeUser};
use crate::error::{AppError, AppResult};
use crate::state::AppState;
use axum::extract::{Path, Query, State};
@@ -456,6 +456,7 @@ pub async fn respond_game_invite(
// ---------------------------------------------------------------------------
pub async fn get_player_profile(
MaybeUser(viewer): MaybeUser,
Path(uuid): Path<String>,
State(state): State<AppState>,
) -> AppResult<Json<UserProfileView>> {
@@ -497,23 +498,27 @@ pub async fn get_player_profile(
.await?;
// Recent posts
let post_rows: Vec<(i64, String, String, String, Option<String>, i64, String)> = sqlx::query_as(
"SELECT id, user_uuid, author_name, content, image_url, likes_count, created_at
FROM user_posts WHERE user_uuid = ? ORDER BY id DESC LIMIT 10",
let post_rows: Vec<(i64, String, String, String, Option<String>, i64, bool, String)> = sqlx::query_as(
"SELECT p.id, p.user_uuid, p.author_name, p.content, p.image_url, p.likes_count,
EXISTS(SELECT 1 FROM user_post_likes l WHERE l.post_id = p.id AND l.user_uuid = ?),
p.created_at
FROM user_posts p WHERE p.user_uuid = ? ORDER BY p.id DESC LIMIT 10",
)
.bind(viewer.as_ref().map(|user| user.uuid.as_str()).unwrap_or(""))
.bind(&puuid)
.fetch_all(&state.db)
.await?;
let posts = post_rows
.into_iter()
.map(|(id, u_uuid, aname, content, img, likes, cat)| UserPost {
.map(|(id, u_uuid, aname, content, img, likes, liked_by_me, cat)| UserPost {
id,
user_uuid: u_uuid,
author_name: aname,
content,
image_url: img,
likes_count: likes,
liked_by_me,
created_at: cat,
})
.collect();
@@ -634,6 +639,7 @@ pub async fn create_user_post(
content: content.to_string(),
image_url: payload.image_url,
likes_count: 0,
liked_by_me: false,
created_at: now,
}))
}
@@ -653,16 +659,31 @@ pub async fn delete_user_post(
}
pub async fn like_user_post(
_auth: AuthUser,
auth: AuthUser,
Path(post_id): Path<i64>,
State(state): State<AppState>,
) -> AppResult<Json<Value>> {
sqlx::query("UPDATE user_posts SET likes_count = likes_count + 1 WHERE id = ?")
let mut tx = state.db.begin().await?;
let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM user_posts WHERE id = ?)")
.bind(post_id)
.execute(&state.db)
.fetch_one(&mut *tx)
.await?;
if !exists { return Err(AppError::not_found("Post not found")); }
let added = sqlx::query("INSERT OR IGNORE INTO user_post_likes (post_id, user_uuid) VALUES (?, ?)")
.bind(post_id)
.bind(&auth.uuid)
.execute(&mut *tx)
.await?
.rows_affected() > 0;
if added {
sqlx::query("UPDATE user_posts SET likes_count = likes_count + 1 WHERE id = ?")
.bind(post_id)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(Json(serde_json::json!({ "ok": true })))
Ok(Json(serde_json::json!({ "ok": true, "liked": added })))
}
// ---------------------------------------------------------------------------