From 8445e62b9b22d476e36c4973bb0c9751e9129ff2 Mon Sep 17 00:00:00 2001 From: stadust <43299462+stadust@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:52:32 +0000 Subject: [PATCH 1/6] demonlist: Eliminate struct MiniDemon Instead, use MiniDemonWithPlayers everywhere. It's true that a demon will have at most one publisher or verifier, but having a whole new type just to avoid allocating a Vec is a bit much. Signed-off-by: stadust <43299462+stadust@users.noreply.github.com> --- pointercrate-demonlist/src/nationality/get.rs | 14 +++++++------- pointercrate-demonlist/src/nationality/mod.rs | 12 ++---------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/pointercrate-demonlist/src/nationality/get.rs b/pointercrate-demonlist/src/nationality/get.rs index 033c6b57d..c703f0641 100644 --- a/pointercrate-demonlist/src/nationality/get.rs +++ b/pointercrate-demonlist/src/nationality/get.rs @@ -1,7 +1,7 @@ use crate::{ demon::MinimalDemon, error::{DemonlistError, Result}, - nationality::{BestRecord, MiniDemon, MiniDemonWithPlayers, Nationality, NationalityRecord, Subdivision}, + nationality::{BestRecord, MiniDemonWithPlayers, Nationality, NationalityRecord, Subdivision}, }; use futures::stream::StreamExt; use sqlx::{Error, PgConnection}; @@ -171,7 +171,7 @@ pub async fn created_in(nation: &Nationality, connection: &mut PgConnection) -> Ok(creations) } -pub async fn verified_in(nation: &Nationality, connection: &mut PgConnection) -> Result> { +pub async fn verified_in(nation: &Nationality, connection: &mut PgConnection) -> Result> { let mut stream = sqlx::query!( r#"select demons.id as demon, demons.name::text as "demon_name!", demons.position, players.name::text as "player_name!" from demons inner join players on players.id=verifier where nationality=$1"#, nation.iso_country_code).fetch(connection); @@ -180,18 +180,18 @@ pub async fn verified_in(nation: &Nationality, connection: &mut PgConnection) -> while let Some(row) = stream.next().await { let row = row?; - demons.push(MiniDemon { + demons.push(MiniDemonWithPlayers { id: row.demon, demon: row.demon_name, position: row.position, - player: row.player_name, + players: vec![row.player_name], }); } Ok(demons) } -pub async fn published_in(nation: &Nationality, connection: &mut PgConnection) -> Result> { +pub async fn published_in(nation: &Nationality, connection: &mut PgConnection) -> Result> { let mut stream = sqlx::query!( r#"select demons.id as demon, demons.name::text as "demon_name!", demons.position, players.name::text as "player_name!" from demons inner join players on players.id=publisher where nationality=$1"#, nation.iso_country_code).fetch(connection); @@ -200,11 +200,11 @@ pub async fn published_in(nation: &Nationality, connection: &mut PgConnection) - while let Some(row) = stream.next().await { let row = row?; - demons.push(MiniDemon { + demons.push(MiniDemonWithPlayers { id: row.demon, demon: row.demon_name, position: row.position, - player: row.player_name, + players: vec![row.player_name], }); } diff --git a/pointercrate-demonlist/src/nationality/mod.rs b/pointercrate-demonlist/src/nationality/mod.rs index 67a00f17e..b7f8c966d 100644 --- a/pointercrate-demonlist/src/nationality/mod.rs +++ b/pointercrate-demonlist/src/nationality/mod.rs @@ -25,14 +25,6 @@ pub struct BestRecord { players: Vec, } -#[derive(Debug, Serialize, Hash)] -pub struct MiniDemon { - id: i32, - demon: String, - position: i16, - player: String, -} - #[derive(Debug, Serialize, Hash)] pub struct MiniDemonWithPlayers { id: i32, @@ -48,8 +40,8 @@ pub struct NationalityRecord { #[serde(rename = "records")] pub best_records: Vec, pub created: Vec, - pub verified: Vec, - pub published: Vec, + pub verified: Vec, + pub published: Vec, pub unbeaten: Vec, } From c5d579d0ad4142225df392a5c8344308103f2725 Mon Sep 17 00:00:00 2001 From: stadust <43299462+stadust@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:54:43 +0000 Subject: [PATCH 2/6] demonlist: store MinimalDemon in MiniDemonWithPlayer Instead of repeating all the fields of a MinimalDemon, just reuse that struct. This brings the structure of MiniDemonWithPlayers closer to MinimalRecordD, its equivalent from the GET /players/[id] endpoint. Signed-off-by: stadust <43299462+stadust@users.noreply.github.com> --- pointercrate-demonlist/src/nationality/get.rs | 26 ++++++++++++------- pointercrate-demonlist/src/nationality/mod.rs | 4 +-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pointercrate-demonlist/src/nationality/get.rs b/pointercrate-demonlist/src/nationality/get.rs index c703f0641..b188d2b69 100644 --- a/pointercrate-demonlist/src/nationality/get.rs +++ b/pointercrate-demonlist/src/nationality/get.rs @@ -158,11 +158,13 @@ pub async fn created_in(nation: &Nationality, connection: &mut PgConnection) -> let row = row?; match creations.last_mut() { - Some(mini_demon) if mini_demon.demon == row.demon_name => mini_demon.players.push(row.player_name), + Some(mini_demon) if mini_demon.demon.name == row.demon_name => mini_demon.players.push(row.player_name), _ => creations.push(MiniDemonWithPlayers { - id: row.demon, - demon: row.demon_name, - position: row.position, + demon: MinimalDemon { + id: row.demon, + name: row.demon_name, + position: row.position, + }, players: vec![row.player_name], }), } @@ -181,9 +183,11 @@ pub async fn verified_in(nation: &Nationality, connection: &mut PgConnection) -> let row = row?; demons.push(MiniDemonWithPlayers { - id: row.demon, - demon: row.demon_name, - position: row.position, + demon: MinimalDemon { + id: row.demon, + name: row.demon_name, + position: row.position, + }, players: vec![row.player_name], }); } @@ -201,9 +205,11 @@ pub async fn published_in(nation: &Nationality, connection: &mut PgConnection) - let row = row?; demons.push(MiniDemonWithPlayers { - id: row.demon, - demon: row.demon_name, - position: row.position, + demon: MinimalDemon { + id: row.demon, + name: row.demon_name, + position: row.position, + }, players: vec![row.player_name], }); } diff --git a/pointercrate-demonlist/src/nationality/mod.rs b/pointercrate-demonlist/src/nationality/mod.rs index b7f8c966d..f06b1919a 100644 --- a/pointercrate-demonlist/src/nationality/mod.rs +++ b/pointercrate-demonlist/src/nationality/mod.rs @@ -27,9 +27,7 @@ pub struct BestRecord { #[derive(Debug, Serialize, Hash)] pub struct MiniDemonWithPlayers { - id: i32, - demon: String, - position: i16, + demon: MinimalDemon, players: Vec, } From 5935c4daf65e1f47e65300cb4c7682b0caa1fff5 Mon Sep 17 00:00:00 2001 From: stadust <43299462+stadust@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:57:25 +0000 Subject: [PATCH 3/6] demonlist: store MinimalDemon in BestRecord This also brings it closer to MinimalRecordD, its rough equivalent from the FullPlayer object. Signed-off-by: stadust <43299462+stadust@users.noreply.github.com> --- pointercrate-demonlist/src/nationality/get.rs | 10 ++++++---- pointercrate-demonlist/src/nationality/mod.rs | 4 +--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pointercrate-demonlist/src/nationality/get.rs b/pointercrate-demonlist/src/nationality/get.rs index b188d2b69..87328ec6d 100644 --- a/pointercrate-demonlist/src/nationality/get.rs +++ b/pointercrate-demonlist/src/nationality/get.rs @@ -230,11 +230,13 @@ pub async fn best_records_in(nation: &Nationality, connection: &mut PgConnection let row = row?; match records.last_mut() { - Some(record) if record.demon == row.demon_name => record.players.push(row.player_name), + Some(record) if record.demon.name == row.demon_name => record.players.push(row.player_name), _ => records.push(BestRecord { - id: row.demon_id, - demon: row.demon_name, - position: row.position, + demon: MinimalDemon { + id: row.demon_id, + name: row.demon_name, + position: row.position, + }, progress: row.progress, players: vec![row.player_name], }), diff --git a/pointercrate-demonlist/src/nationality/mod.rs b/pointercrate-demonlist/src/nationality/mod.rs index f06b1919a..2098eb32b 100644 --- a/pointercrate-demonlist/src/nationality/mod.rs +++ b/pointercrate-demonlist/src/nationality/mod.rs @@ -18,10 +18,8 @@ pub struct Nationality { #[derive(Debug, Serialize, Hash)] pub struct BestRecord { - id: i32, - demon: String, - position: i16, progress: i16, + demon: MinimalDemon, players: Vec, } From cc834efd2cd792f7a039f38d1885a442745abafc Mon Sep 17 00:00:00 2001 From: stadust <43299462+stadust@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:58:42 +0000 Subject: [PATCH 4/6] demonlist: Flatten `Nationality` object into `NationalityRecord Again, to make these structs be more similar to FullPlayer Signed-off-by: stadust <43299462+stadust@users.noreply.github.com> --- pointercrate-demonlist/src/nationality/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pointercrate-demonlist/src/nationality/mod.rs b/pointercrate-demonlist/src/nationality/mod.rs index 2098eb32b..a7c101c2b 100644 --- a/pointercrate-demonlist/src/nationality/mod.rs +++ b/pointercrate-demonlist/src/nationality/mod.rs @@ -29,8 +29,10 @@ pub struct MiniDemonWithPlayers { players: Vec, } +/// The [`Nationality`] equivalent of [`FullPlayer`], very roughly #[derive(Debug, Hash, Serialize)] pub struct NationalityRecord { + #[serde(flatten)] pub nation: Nationality, #[serde(rename = "records")] From 7983ccfbf689ca313928d5977649a3e27ef17184 Mon Sep 17 00:00:00 2001 From: stadust <43299462+stadust@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:12:24 +0000 Subject: [PATCH 5/6] demonlist(js): Update nation stats viewer to work with new API Signed-off-by: stadust <43299462+stadust@users.noreply.github.com> --- .../static/js/statsviewer/nation.js | 54 ++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/pointercrate-demonlist-pages/static/js/statsviewer/nation.js b/pointercrate-demonlist-pages/static/js/statsviewer/nation.js index 86cf813b7..c41fff0de 100644 --- a/pointercrate-demonlist-pages/static/js/statsviewer/nation.js +++ b/pointercrate-demonlist-pages/static/js/statsviewer/nation.js @@ -23,7 +23,7 @@ class NationStatsViewer extends StatsViewer { let nationData = response.data.data; - this.setName(nationData.nation.nation, nationData.nation); + this.setName(nationData.nation, nationData); let beaten = []; let progress = []; @@ -44,16 +44,12 @@ class NationStatsViewer extends StatsViewer { } else { beaten.push(record); - if (hardest === undefined || record.position < hardest.position) { - hardest = { - name: record.demon, - position: record.position, - id: record.id, - }; + if (hardest === undefined || record.demon.position < hardest.position) { + hardest = record.demon; } - if (record.position > this.list_size) - if (record.position <= this.extended_list_size) ++extended; + if (record.demon.position > this.list_size) + if (record.demon.position <= this.extended_list_size) ++extended; else ++legacy; } } @@ -61,19 +57,15 @@ class NationStatsViewer extends StatsViewer { let amountBeaten = beaten.length - extended - legacy; for (let record of nationData.verified) { - players.add(record.player); - - if (hardest === undefined || record.position < hardest.position) { - hardest = { - name: record.demon, - position: record.position, - id: record.id, - }; + record.players.forEach(players.add, players); + + if (hardest === undefined || record.demon.position < hardest.position) { + hardest = record.demon } - if (!beaten.some((d) => d.id === record.id)) - if (record.position > this.list_size) - if (record.position <= this.extended_list_size) ++extended; + if (!beaten.some((d) => d.demon.id === record.demon.id)) + if (record.demon.position > this.list_size) + if (record.demon.position <= this.extended_list_size) ++extended; else ++legacy; else ++amountBeaten; } @@ -84,9 +76,9 @@ class NationStatsViewer extends StatsViewer { this.setCompletionNumber(amountBeaten, extended, legacy); nationData.unbeaten.sort((r1, r2) => r1.name.localeCompare(r2.name)); - beaten.sort((r1, r2) => r1.demon.localeCompare(r2.demon)); + beaten.sort((r1, r2) => r1.demon.name.localeCompare(r2.demon.name)); progress.sort((r1, r2) => r2.progress - r1.progress); - nationData.created.sort((r1, r2) => r1.demon.localeCompare(r2.demon)); + nationData.created.sort((r1, r2) => r1.demon.name.localeCompare(r2.demon.name)); formatInto( this._unbeaten, @@ -107,8 +99,8 @@ class NationStatsViewer extends StatsViewer { nationData.created.map((creation) => { return this.makeTooltip( this.formatDemon( - { name: creation.demon, position: creation.position }, - "/demonlist/permalink/" + creation.id + "/" + creation.demon, + "/demonlist/permalink/" + creation.demon.id + "/" ), "(Co)created by " + creation.players.length + @@ -124,8 +116,8 @@ class NationStatsViewer extends StatsViewer { nationData.verified.map((verification) => { return this.makeTooltip( this.formatDemon( - { name: verification.demon, position: verification.position }, - "/demonlist/permalink/" + verification.id + "/" + verification.demon, + "/demonlist/permalink/" + verification.demon.id + "/" ), "Verified by: ", verification.player @@ -137,8 +129,8 @@ class NationStatsViewer extends StatsViewer { nationData.published.map((publication) => { return this.makeTooltip( this.formatDemon( - { name: publication.demon, position: publication.position }, - "/demonlist/permalink/" + publication.id + "/" + publication.demon, + "/demonlist/permalink/" + publication.demon.id + "/" ), "Published by: ", publication.player @@ -168,8 +160,8 @@ class NationStatsViewer extends StatsViewer { formatDemonFromRecord(record) { let baseElement = this.formatDemon( - { name: record.demon, position: record.position }, - "/demonlist/permalink/" + record.id + "/" + record.demon, + "/demonlist/permalink/" + record.demon.id + "/" ); if (record.progress !== 100) @@ -197,7 +189,7 @@ $(window).on("load", function () { ); window.statsViewer.initialize(); window.statsViewer.addSelectionListener((selected) => - map.select(selected.nation.country_code) + map.select(selected.country_code) ); map.addSelectionListener((country, _) => { From 649737f5ea8572e9cb516ddb5659430901acc59c Mon Sep 17 00:00:00 2001 From: stadust <43299462+stadust@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:38:19 +0000 Subject: [PATCH 6/6] demonlist(js): default to permalink if no link passed to formatDemon Literally every callsite was either explicitly passing in the permalink, or passing it in if some other link was null (e.g. no verification video). So just move this as a default into the function itself. Signed-off-by: stadust <43299462+stadust@users.noreply.github.com> --- .../static/js/modules/statsviewer.js | 14 ++++------ .../static/js/statsviewer/individual.js | 9 ++----- .../static/js/statsviewer/nation.js | 26 +++++-------------- 3 files changed, 14 insertions(+), 35 deletions(-) diff --git a/pointercrate-demonlist-pages/static/js/modules/statsviewer.js b/pointercrate-demonlist-pages/static/js/modules/statsviewer.js index 19b60bd1f..57799528d 100644 --- a/pointercrate-demonlist-pages/static/js/modules/statsviewer.js +++ b/pointercrate-demonlist-pages/static/js/modules/statsviewer.js @@ -110,7 +110,7 @@ export class StatsViewer extends FilteredPaginator { this._hardest.appendChild( hardest === undefined ? document.createTextNode("None") - : this.formatDemon(hardest, "/demonlist/permalink/" + hardest.id + "/") + : this.formatDemon(hardest) ); } @@ -140,15 +140,11 @@ export class StatsViewer extends FilteredPaginator { element.style.opacity = ".5"; } - if (link) { - let a = document.createElement("a"); - a.href = link; - a.textContent = demon.name; + let a = document.createElement("a"); + a.href = link ?? "/demonlist/permalink/" + demon.id + "/"; + a.textContent = demon.name; - element.appendChild(a); - } else { - element.textContent = demon.name; - } + element.appendChild(a); return element; } diff --git a/pointercrate-demonlist-pages/static/js/statsviewer/individual.js b/pointercrate-demonlist-pages/static/js/statsviewer/individual.js index 9c0caf6c4..86ee9f4b9 100644 --- a/pointercrate-demonlist-pages/static/js/statsviewer/individual.js +++ b/pointercrate-demonlist-pages/static/js/statsviewer/individual.js @@ -82,9 +82,7 @@ class IndividualStatsViewer extends StatsViewer { formatDemonsInto(element, demons) { formatInto( element, - demons.map((demon) => - this.formatDemon(demon, "/demonlist/permalink/" + demon.id + "/") - ) + demons.map((demon) => this.formatDemon(demon)) ); } @@ -92,10 +90,7 @@ class IndividualStatsViewer extends StatsViewer { formatInto( element, records.map((record) => { - let demon = this.formatDemon( - record.demon, - record.video ?? "/demonlist/permalink/" + record.demon.id + "/" - ); + let demon = this.formatDemon(record.demon, record.video); if (record.progress !== 100) { demon.appendChild( document.createTextNode(" (" + record.progress + "%)") diff --git a/pointercrate-demonlist-pages/static/js/statsviewer/nation.js b/pointercrate-demonlist-pages/static/js/statsviewer/nation.js index c41fff0de..efff52b82 100644 --- a/pointercrate-demonlist-pages/static/js/statsviewer/nation.js +++ b/pointercrate-demonlist-pages/static/js/statsviewer/nation.js @@ -83,7 +83,7 @@ class NationStatsViewer extends StatsViewer { formatInto( this._unbeaten, nationData.unbeaten.map((demon) => - this.formatDemon(demon, "/demonlist/permalink/" + demon.id + "/") + this.formatDemon(demon) ) ); formatInto( @@ -98,10 +98,7 @@ class NationStatsViewer extends StatsViewer { this._created, nationData.created.map((creation) => { return this.makeTooltip( - this.formatDemon( - creation.demon, - "/demonlist/permalink/" + creation.demon.id + "/" - ), + this.formatDemon(creation.demon), "(Co)created by " + creation.players.length + " player" + @@ -115,12 +112,9 @@ class NationStatsViewer extends StatsViewer { this._verified, nationData.verified.map((verification) => { return this.makeTooltip( - this.formatDemon( - verification.demon, - "/demonlist/permalink/" + verification.demon.id + "/" - ), + this.formatDemon(verification.demon), "Verified by: ", - verification.player + verification.players.join(", ") ); }) ); @@ -128,12 +122,9 @@ class NationStatsViewer extends StatsViewer { this._published, nationData.published.map((publication) => { return this.makeTooltip( - this.formatDemon( - publication.demon, - "/demonlist/permalink/" + publication.demon.id + "/" - ), + this.formatDemon(publication.demon), "Published by: ", - publication.player + publication.players.join(", ") ); }) ); @@ -159,10 +150,7 @@ class NationStatsViewer extends StatsViewer { } formatDemonFromRecord(record) { - let baseElement = this.formatDemon( - record.demon, - "/demonlist/permalink/" + record.demon.id + "/" - ); + let baseElement = this.formatDemon(record.demon); if (record.progress !== 100) baseElement.appendChild(