Skip to content

Commit 519ba36

Browse files
authored
feat: Map medal times (#93)
* feat: Map medal times * Add medal times SQL column bound to maps table with migration * Fill them with the `/map/insert` game API service, that updates them when available, like the `cps_number` field * Add comment to explain medal times check
1 parent 75b8a3d commit 519ba36

File tree

5 files changed

+192
-9
lines changed

5 files changed

+192
-9
lines changed

crates/entity/src/entities/maps.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ pub struct Model {
2323
///
2424
/// This was created for future features.
2525
pub linked_map: Option<u32>,
26+
27+
/// The bronze time of the map.
28+
pub bronze_time: Option<i32>,
29+
/// The silver time of the map.
30+
pub silver_time: Option<i32>,
31+
/// The gold time of the map.
32+
pub gold_time: Option<i32>,
33+
/// The author time of the map.
34+
pub author_time: Option<i32>,
2635
}
2736

2837
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

crates/game_api/src/http/map.rs

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use entity::{maps, player_rating, players, rating, rating_kind};
1212
use futures::{StreamExt, future::try_join_all};
1313
use records_lib::Database;
1414
use sea_orm::{
15-
ActiveValue::Set, ColumnTrait as _, EntityTrait as _, FromQueryResult, PaginatorTrait,
16-
QueryFilter, QuerySelect, prelude::Expr, sea_query::Func,
15+
ActiveModelTrait as _, ActiveValue::Set, ColumnTrait as _, EntityTrait as _, FromQueryResult,
16+
PaginatorTrait, QueryFilter, QuerySelect, prelude::Expr, sea_query::Func,
1717
};
1818
use serde::{Deserialize, Serialize};
1919

@@ -29,13 +29,43 @@ pub fn map_scope() -> Scope {
2929
.route("/reset_ratings", web::post().to(reset_ratings))
3030
}
3131

32+
#[derive(Deserialize)]
33+
#[cfg_attr(test, derive(Serialize))]
34+
struct MedalTimes {
35+
bronze_time: i32,
36+
silver_time: i32,
37+
gold_time: i32,
38+
author_time: i32,
39+
}
40+
3241
#[derive(Deserialize)]
3342
#[cfg_attr(test, derive(Serialize))]
3443
struct UpdateMapBody {
3544
name: String,
3645
map_uid: String,
3746
cps_number: u32,
3847
author: PlayerInfoNetBody,
48+
medal_times: Option<MedalTimes>,
49+
}
50+
51+
fn update_active_model_medal_times(
52+
active_model: &mut maps::ActiveModel,
53+
medal_times: Option<MedalTimes>,
54+
) {
55+
// If the request body contains medal times, it doesn't mean that the map has medal times saved.
56+
// This is because ManiaScript can't encode null values when serializing to JSON. So it encodes
57+
// 0 for integers by default.
58+
if let Some(medal_times) = medal_times
59+
&& medal_times.author_time > 0
60+
&& medal_times.bronze_time > medal_times.silver_time
61+
&& medal_times.silver_time > medal_times.gold_time
62+
&& medal_times.gold_time > medal_times.author_time
63+
{
64+
active_model.bronze_time = Set(Some(medal_times.bronze_time));
65+
active_model.silver_time = Set(Some(medal_times.silver_time));
66+
active_model.gold_time = Set(Some(medal_times.gold_time));
67+
active_model.author_time = Set(Some(medal_times.author_time));
68+
}
3969
}
4070

4171
async fn insert(
@@ -46,24 +76,42 @@ async fn insert(
4676
let map = records_lib::map::get_map_from_uid(&conn, &body.map_uid).await?;
4777

4878
if let Some(map) = map {
49-
if map.cps_number.is_none() {
50-
let map = maps::ActiveModel {
51-
cps_number: Set(Some(body.cps_number)),
52-
..From::from(map)
53-
};
54-
maps::Entity::update(map).exec(&conn).await.with_api_err()?;
79+
let is_map_medals_empty = map.bronze_time.is_none()
80+
&& map.silver_time.is_none()
81+
&& map.gold_time.is_none()
82+
&& map.author_time.is_none();
83+
84+
let is_map_cps_number_empty = map.cps_number.is_none();
85+
86+
let mut updated_map = maps::ActiveModel::from(map);
87+
88+
if is_map_cps_number_empty {
89+
updated_map.cps_number = Set(Some(body.cps_number));
90+
}
91+
92+
if is_map_medals_empty {
93+
update_active_model_medal_times(&mut updated_map, body.medal_times);
94+
}
95+
96+
if updated_map.is_changed() {
97+
maps::Entity::update(updated_map)
98+
.exec(&conn)
99+
.await
100+
.with_api_err()?;
55101
}
56102
} else {
57103
let player = player::get_or_insert(&conn, &body.author).await?;
58104

59-
let new_map = maps::ActiveModel {
105+
let mut new_map = maps::ActiveModel {
60106
game_id: Set(body.map_uid),
61107
player_id: Set(player.id),
62108
name: Set(body.name),
63109
cps_number: Set(Some(body.cps_number)),
64110
..Default::default()
65111
};
66112

113+
update_active_model_medal_times(&mut new_map, body.medal_times);
114+
67115
maps::Entity::insert(new_map)
68116
.exec(&conn)
69117
.await

crates/game_api/tests/map_update.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@ struct Request {
1919
author: MapAuthor,
2020
}
2121

22+
#[derive(serde::Serialize)]
23+
struct MedalTimes {
24+
bronze_time: i32,
25+
silver_time: i32,
26+
gold_time: i32,
27+
author_time: i32,
28+
}
29+
30+
#[derive(serde::Serialize)]
31+
struct RequestWithMedalTimes {
32+
name: String,
33+
map_uid: String,
34+
cps_number: u32,
35+
author: MapAuthor,
36+
medal_times: MedalTimes,
37+
}
38+
2239
#[tokio::test]
2340
async fn insert_map_existing_player() -> anyhow::Result<()> {
2441
let player = players::ActiveModel {
@@ -166,3 +183,66 @@ async fn update_map_cps_number() -> anyhow::Result<()> {
166183
})
167184
.await
168185
}
186+
187+
#[tokio::test]
188+
async fn update_map_medal_times() -> anyhow::Result<()> {
189+
let player = players::ActiveModel {
190+
id: Set(1),
191+
login: Set("player_login".to_owned()),
192+
name: Set("player_name".to_owned()),
193+
role: Set(0),
194+
..Default::default()
195+
};
196+
197+
let map = maps::ActiveModel {
198+
id: Set(1),
199+
game_id: Set("map_uid".to_owned()),
200+
name: Set("map_name".to_owned()),
201+
player_id: Set(1),
202+
..Default::default()
203+
};
204+
205+
base::with_db(async |db| {
206+
players::Entity::insert(player).exec(&db.sql_conn).await?;
207+
maps::Entity::insert(map).exec(&db.sql_conn).await?;
208+
209+
let app = base::get_app(db.clone()).await;
210+
211+
let req = test::TestRequest::post()
212+
.uri("/map/insert")
213+
.set_json(RequestWithMedalTimes {
214+
map_uid: "map_uid".to_owned(),
215+
name: Default::default(),
216+
cps_number: 5,
217+
author: Default::default(),
218+
medal_times: MedalTimes {
219+
bronze_time: 5000,
220+
silver_time: 4000,
221+
gold_time: 3000,
222+
author_time: 2000,
223+
},
224+
})
225+
.to_request();
226+
227+
let res = test::call_service(&app, req).await;
228+
assert_eq!(res.status(), 200);
229+
230+
let map = maps::Entity::find_by_id(1u32)
231+
.one(&db.sql_conn)
232+
.await?
233+
.unwrap_or_else(|| panic!("Map should exist in database"));
234+
235+
assert_eq!(map.game_id, "map_uid");
236+
assert_eq!(map.name, "map_name");
237+
assert_eq!(map.player_id, 1);
238+
assert_eq!(map.cps_number, Some(5));
239+
240+
assert_eq!(map.bronze_time, Some(5000));
241+
assert_eq!(map.silver_time, Some(4000));
242+
assert_eq!(map.gold_time, Some(3000));
243+
assert_eq!(map.author_time, Some(2000));
244+
245+
anyhow::Ok(())
246+
})
247+
.await
248+
}

crates/migration/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod m20250918_132016_add_event_edition_maps_source;
66
mod m20250918_135135_add_event_edition_maps_thumbnail_source;
77
mod m20250918_215914_add_event_edition_maps_availability;
88
mod m20250918_222203_add_event_edition_maps_disability;
9+
mod m20251004_214656_add_maps_medal_times;
910

1011
use sea_orm_migration::prelude::*;
1112

@@ -25,6 +26,7 @@ impl MigratorTrait for Migrator {
2526
Box::new(m20250918_135135_add_event_edition_maps_thumbnail_source::Migration),
2627
Box::new(m20250918_215914_add_event_edition_maps_availability::Migration),
2728
Box::new(m20250918_222203_add_event_edition_maps_disability::Migration),
29+
Box::new(m20251004_214656_add_maps_medal_times::Migration),
2830
]
2931
}
3032
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use sea_orm_migration::prelude::*;
2+
3+
#[derive(DeriveMigrationName)]
4+
pub struct Migration;
5+
6+
#[async_trait::async_trait]
7+
impl MigrationTrait for Migration {
8+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9+
manager
10+
.alter_table(
11+
Table::alter()
12+
.table(Maps::Table)
13+
.add_column(ColumnDef::new(Maps::BronzeTime).null().integer().take())
14+
.add_column(ColumnDef::new(Maps::SilverTime).null().integer().take())
15+
.add_column(ColumnDef::new(Maps::GoldTime).null().integer().take())
16+
.add_column(ColumnDef::new(Maps::AuthorTime).null().integer().take())
17+
.take(),
18+
)
19+
.await
20+
}
21+
22+
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
23+
manager
24+
.alter_table(
25+
Table::alter()
26+
.table(Maps::Table)
27+
.drop_column(Maps::BronzeTime)
28+
.drop_column(Maps::SilverTime)
29+
.drop_column(Maps::GoldTime)
30+
.drop_column(Maps::AuthorTime)
31+
.take(),
32+
)
33+
.await
34+
}
35+
}
36+
37+
#[derive(DeriveIden)]
38+
enum Maps {
39+
Table,
40+
BronzeTime,
41+
SilverTime,
42+
GoldTime,
43+
AuthorTime,
44+
}

0 commit comments

Comments
 (0)