Compare commits
10 Commits
2019c66c23
...
48e7c5c5b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48e7c5c5b7 | ||
|
|
fa21804cc3 | ||
|
|
ecfed6b739 | ||
|
|
be220291f6 | ||
|
|
e01cbd0a51 | ||
|
|
898a889a53 | ||
|
|
7e50a627f6 | ||
|
|
e8843540a3 | ||
|
|
101833d0d8 | ||
|
|
1d6b6555b1 |
49
Cargo.lock
generated
49
Cargo.lock
generated
@@ -38,6 +38,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"colors-transform",
|
"colors-transform",
|
||||||
"csdemo",
|
"csdemo",
|
||||||
|
"divan",
|
||||||
"image",
|
"image",
|
||||||
"phf",
|
"phf",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
@@ -554,6 +555,7 @@ dependencies = [
|
|||||||
"anstyle",
|
"anstyle",
|
||||||
"clap_lex",
|
"clap_lex",
|
||||||
"strsim",
|
"strsim",
|
||||||
|
"terminal_size",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -606,6 +608,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "condtype"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "config"
|
name = "config"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
@@ -927,6 +935,31 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "divan"
|
||||||
|
version = "0.1.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e05d17bd4ff1c1e7998ed4623d2efd91f72f1e24141ac33aac9377974270e1f"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"clap",
|
||||||
|
"condtype",
|
||||||
|
"divan-macros",
|
||||||
|
"libc",
|
||||||
|
"regex-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "divan-macros"
|
||||||
|
version = "0.1.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b4464d46ce68bfc7cb76389248c7c254def7baca8bece0693b02b83842c4c88"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dlv-list"
|
name = "dlv-list"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -2933,6 +2966,12 @@ dependencies = [
|
|||||||
"regex-syntax 0.8.5",
|
"regex-syntax 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-lite"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.6.29"
|
version = "0.6.29"
|
||||||
@@ -3709,6 +3748,16 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminal_size"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef"
|
||||||
|
dependencies = [
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.66"
|
version = "1.0.66"
|
||||||
|
|||||||
@@ -17,3 +17,9 @@ phf = { version = "0.11" }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { version = "1.4" }
|
pretty_assertions = { version = "1.4" }
|
||||||
tracing-test = { version = "0.2", features = ["no-env-filter"] }
|
tracing-test = { version = "0.2", features = ["no-env-filter"] }
|
||||||
|
|
||||||
|
divan = "0.1.15"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "analysis"
|
||||||
|
harness = false
|
||||||
|
|||||||
35
analysis/benches/analysis.rs
Normal file
35
analysis/benches/analysis.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
fn main() {
|
||||||
|
divan::main();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[divan::bench(args = ["dust2.dem", "inferno.dem", "nuke.dem"])]
|
||||||
|
fn heatmap(bencher: divan::Bencher, file: &str) {
|
||||||
|
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../testfiles/")
|
||||||
|
.join(file);
|
||||||
|
let data = std::fs::read(path).unwrap();
|
||||||
|
|
||||||
|
let config = analysis::heatmap::Config { cell_size: 2.0 };
|
||||||
|
|
||||||
|
bencher.bench(|| analysis::heatmap::parse(divan::black_box(&config), divan::black_box(&data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[divan::bench(args = ["dust2.dem", "inferno.dem", "nuke.dem"])]
|
||||||
|
fn perround(bencher: divan::Bencher, file: &str) {
|
||||||
|
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../testfiles/")
|
||||||
|
.join(file);
|
||||||
|
let data = std::fs::read(path).unwrap();
|
||||||
|
|
||||||
|
bencher.bench(|| analysis::perround::parse(divan::black_box(&data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[divan::bench(args = ["dust2.dem", "inferno.dem", "nuke.dem"])]
|
||||||
|
fn endofgame(bencher: divan::Bencher, file: &str) {
|
||||||
|
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../testfiles/")
|
||||||
|
.join(file);
|
||||||
|
let data = std::fs::read(path).unwrap();
|
||||||
|
|
||||||
|
bencher.bench(|| analysis::endofgame::parse(divan::black_box(&data)));
|
||||||
|
}
|
||||||
52
analysis/src/head_to_head.rs
Normal file
52
analysis/src/head_to_head.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct Output {
|
||||||
|
pub players: HashMap<csdemo::UserId, csdemo::parser::Player>,
|
||||||
|
pub head_to_head: HashMap<csdemo::UserId, HashMap<csdemo::UserId, usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> Result<Output, ()> {
|
||||||
|
let tmp = csdemo::Container::parse(buf).map_err(|e| ())?;
|
||||||
|
|
||||||
|
let output = csdemo::lazyparser::LazyParser::new(tmp);
|
||||||
|
|
||||||
|
let players = output.player_info();
|
||||||
|
|
||||||
|
let mut head_to_head = HashMap::new();
|
||||||
|
|
||||||
|
for event in output.events().filter_map(|e| e.ok()) {
|
||||||
|
let event = match event {
|
||||||
|
csdemo::DemoEvent::GameEvent(ge) => *ge,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
csdemo::game_event::GameEvent::PlayerDeath(death) => {
|
||||||
|
let (attacker_player, attacker) = match death.attacker.and_then(|u| players.get(&u).zip(Some(u))) {
|
||||||
|
Some(a) => a,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (died_player, died) = match death.userid.and_then(|u| players.get(&u).zip(Some(u))) {
|
||||||
|
Some(d) => d,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if attacker_player.team == died_player.team {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attacker_entry: &mut HashMap<_, _> = head_to_head.entry(attacker).or_default();
|
||||||
|
let died_killed: &mut usize = attacker_entry.entry(died).or_default();
|
||||||
|
*died_killed += 1;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Output {
|
||||||
|
players,
|
||||||
|
head_to_head,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod endofgame;
|
pub mod endofgame;
|
||||||
pub mod heatmap;
|
pub mod heatmap;
|
||||||
pub mod perround;
|
pub mod perround;
|
||||||
|
pub mod head_to_head;
|
||||||
|
|||||||
@@ -60,7 +60,16 @@ pub struct Round {
|
|||||||
pub enum RoundEvent {
|
pub enum RoundEvent {
|
||||||
BombPlanted,
|
BombPlanted,
|
||||||
BombDefused,
|
BombDefused,
|
||||||
Kill { attacker: u64, died: u64 },
|
Kill {
|
||||||
|
attacker: u64,
|
||||||
|
died: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
weapon: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
headshot: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
noscope: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -138,7 +147,7 @@ pub fn parse(buf: &[u8]) -> Result<PerRound, ()> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let event = match ge.as_ref() {
|
let event = match *ge {
|
||||||
csdemo::game_event::GameEvent::BombPlanted(planted) => RoundEvent::BombPlanted,
|
csdemo::game_event::GameEvent::BombPlanted(planted) => RoundEvent::BombPlanted,
|
||||||
csdemo::game_event::GameEvent::BombDefused(defused) => RoundEvent::BombDefused,
|
csdemo::game_event::GameEvent::BombDefused(defused) => RoundEvent::BombDefused,
|
||||||
csdemo::game_event::GameEvent::PlayerDeath(death) => {
|
csdemo::game_event::GameEvent::PlayerDeath(death) => {
|
||||||
@@ -157,6 +166,9 @@ pub fn parse(buf: &[u8]) -> Result<PerRound, ()> {
|
|||||||
RoundEvent::Kill {
|
RoundEvent::Kill {
|
||||||
attacker: attacker_player.xuid,
|
attacker: attacker_player.xuid,
|
||||||
died: died_player.xuid,
|
died: died_player.xuid,
|
||||||
|
weapon: death.weapon,
|
||||||
|
noscope: death.noscope.unwrap_or(false),
|
||||||
|
headshot: death.headshot.unwrap_or(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => continue,
|
_ => continue,
|
||||||
|
|||||||
40
analysis/tests/head_to_head.rs
Normal file
40
analysis/tests/head_to_head.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use analysis::head_to_head;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "Testing"]
|
||||||
|
fn head_to_head_nuke() {
|
||||||
|
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../testfiles/nuke.dem");
|
||||||
|
dbg!(path);
|
||||||
|
let input_bytes = std::fs::read(path).unwrap();
|
||||||
|
|
||||||
|
let result = head_to_head::parse(&input_bytes).unwrap();
|
||||||
|
|
||||||
|
let expected = head_to_head::Output {
|
||||||
|
players: [(csdemo::UserId(0), csdemo::parser::Player {
|
||||||
|
xuid: 0,
|
||||||
|
name: "".to_owned(),
|
||||||
|
team: 0,
|
||||||
|
color: 0,
|
||||||
|
})].into_iter().collect(),
|
||||||
|
head_to_head: [
|
||||||
|
(csdemo::UserId(0), HashMap::new()),
|
||||||
|
(csdemo::UserId(1), HashMap::new()),
|
||||||
|
(csdemo::UserId(2), HashMap::new()),
|
||||||
|
(csdemo::UserId(3), HashMap::new()),
|
||||||
|
(csdemo::UserId(4), HashMap::new()),
|
||||||
|
(csdemo::UserId(5), HashMap::new()),
|
||||||
|
(csdemo::UserId(6), HashMap::new()),
|
||||||
|
(csdemo::UserId(7), HashMap::new()),
|
||||||
|
(csdemo::UserId(8), HashMap::new()),
|
||||||
|
(csdemo::UserId(9), HashMap::new()),
|
||||||
|
].into_iter().collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
dbg!(&expected, &result);
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ use diesel_async::RunQueryDsl;
|
|||||||
pub mod base;
|
pub mod base;
|
||||||
pub mod heatmap;
|
pub mod heatmap;
|
||||||
pub mod perround;
|
pub mod perround;
|
||||||
|
pub mod head_to_head;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum AnalysisData {
|
pub enum AnalysisData {
|
||||||
@@ -23,8 +24,8 @@ impl AnalysisInput {
|
|||||||
steamid: String,
|
steamid: String,
|
||||||
demoid: String,
|
demoid: String,
|
||||||
storage: &dyn crate::storage::DemoStorage,
|
storage: &dyn crate::storage::DemoStorage,
|
||||||
) -> Result<Self, ()> {
|
) -> Result<Self, String> {
|
||||||
let data = storage.load(steamid.clone(), demoid.clone()).await.unwrap();
|
let data = storage.load(steamid.clone(), demoid.clone()).await?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
steamid,
|
steamid,
|
||||||
@@ -61,12 +62,13 @@ pub trait Analysis {
|
|||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static ANALYSIS_METHODS: std::sync::LazyLock<[std::sync::Arc<dyn Analysis + Send + Sync>; 3]> =
|
pub static ANALYSIS_METHODS: std::sync::LazyLock<[std::sync::Arc<dyn Analysis + Send + Sync>; 4]> =
|
||||||
std::sync::LazyLock::new(|| {
|
std::sync::LazyLock::new(|| {
|
||||||
[
|
[
|
||||||
std::sync::Arc::new(base::BaseAnalysis::new()),
|
std::sync::Arc::new(base::BaseAnalysis::new()),
|
||||||
std::sync::Arc::new(heatmap::HeatmapAnalysis::new()),
|
std::sync::Arc::new(heatmap::HeatmapAnalysis::new()),
|
||||||
std::sync::Arc::new(perround::PerRoundAnalysis::new()),
|
std::sync::Arc::new(perround::PerRoundAnalysis::new()),
|
||||||
|
std::sync::Arc::new(head_to_head::HeadToHeadAnalysis::new()),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,6 +113,8 @@ where
|
|||||||
let result = db_con
|
let result = db_con
|
||||||
.build_transaction()
|
.build_transaction()
|
||||||
.run::<_, TaskError<AE>, _>(|conn| {
|
.run::<_, TaskError<AE>, _>(|conn| {
|
||||||
|
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let mut results: Vec<crate::models::AnalysisTask> = query.load(conn).await?;
|
let mut results: Vec<crate::models::AnalysisTask> = query.load(conn).await?;
|
||||||
let task = match results.pop() {
|
let task = match results.pop() {
|
||||||
@@ -118,21 +122,28 @@ where
|
|||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let input = AnalysisInput::load(
|
let delete_query =
|
||||||
task.steam_id.clone(),
|
diesel::dsl::delete(crate::schema::analysis_queue::dsl::analysis_queue)
|
||||||
task.demo_id.clone(),
|
.filter(crate::schema::analysis_queue::dsl::demo_id.eq(task.demo_id.clone()))
|
||||||
|
.filter(crate::schema::analysis_queue::dsl::steam_id.eq(task.steam_id.clone()));
|
||||||
|
|
||||||
|
let input = match AnalysisInput::load(
|
||||||
|
task.steam_id,
|
||||||
|
task.demo_id,
|
||||||
storage.as_ref(),
|
storage.as_ref(),
|
||||||
)
|
)
|
||||||
.await
|
.await {
|
||||||
.unwrap();
|
Ok(i) => i,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Loading Analysis Input: {:?}", e);
|
||||||
|
delete_query.execute(conn).await?;
|
||||||
|
return Ok(Some(()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let tmp = action(input, &mut *conn);
|
let tmp = action(input, &mut *conn);
|
||||||
tmp.await.map_err(|e| TaskError::RunningAction(e))?;
|
tmp.await.map_err(|e| TaskError::RunningAction(e))?;
|
||||||
|
|
||||||
let delete_query =
|
|
||||||
diesel::dsl::delete(crate::schema::analysis_queue::dsl::analysis_queue)
|
|
||||||
.filter(crate::schema::analysis_queue::dsl::demo_id.eq(task.demo_id))
|
|
||||||
.filter(crate::schema::analysis_queue::dsl::steam_id.eq(task.steam_id));
|
|
||||||
delete_query.execute(conn).await?;
|
delete_query.execute(conn).await?;
|
||||||
|
|
||||||
Ok(Some(()))
|
Ok(Some(()))
|
||||||
|
|||||||
72
backend/src/analysis/head_to_head.rs
Normal file
72
backend/src/analysis/head_to_head.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub struct HeadToHeadAnalysis {}
|
||||||
|
|
||||||
|
impl HeadToHeadAnalysis {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Analysis for HeadToHeadAnalysis {
|
||||||
|
#[tracing::instrument(name = "HeadToHead", skip(self, input))]
|
||||||
|
fn analyse(
|
||||||
|
&self,
|
||||||
|
input: AnalysisInput,
|
||||||
|
) -> Result<
|
||||||
|
Box<
|
||||||
|
dyn FnOnce(
|
||||||
|
&mut diesel_async::pg::AsyncPgConnection,
|
||||||
|
) -> core::pin::Pin<
|
||||||
|
Box<
|
||||||
|
(dyn core::future::Future<Output = Result<(), diesel::result::Error>>
|
||||||
|
+ Send
|
||||||
|
+ '_),
|
||||||
|
>,
|
||||||
|
> + Send,
|
||||||
|
>,
|
||||||
|
(),
|
||||||
|
> {
|
||||||
|
tracing::info!("Performing Head-to-Head analysis");
|
||||||
|
|
||||||
|
let result = analysis::head_to_head::parse(input.data()).inspect_err(|e| {
|
||||||
|
tracing::error!("{:?}", e);
|
||||||
|
}).map_err(|e| ())?;
|
||||||
|
|
||||||
|
let values_to_insert: Vec<_> = result.head_to_head.into_iter().flat_map(|(user_id, enemies)| {
|
||||||
|
enemies.into_iter().map(move |(enemy, kills)| (user_id, enemy, kills))
|
||||||
|
}).filter_map(|(user_id, enemy_id, kills)| {
|
||||||
|
let player = result.players.get(&user_id)?;
|
||||||
|
let enemy = result.players.get(&enemy_id)?;
|
||||||
|
|
||||||
|
Some(crate::models::DemoHeadToHead {
|
||||||
|
demo_id: input.demoid.clone(),
|
||||||
|
player: player.xuid.to_string(),
|
||||||
|
enemy: enemy.xuid.to_string(),
|
||||||
|
kills: kills as i16,
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Ok(Box::new(move |connection| {
|
||||||
|
// TODO
|
||||||
|
// Construct the actual queries
|
||||||
|
|
||||||
|
let query = diesel::insert_into(crate::schema::demo_head_to_head::dsl::demo_head_to_head)
|
||||||
|
.values(values_to_insert)
|
||||||
|
.on_conflict((
|
||||||
|
crate::schema::demo_head_to_head::dsl::demo_id,
|
||||||
|
crate::schema::demo_head_to_head::dsl::player,
|
||||||
|
crate::schema::demo_head_to_head::dsl::enemy,
|
||||||
|
))
|
||||||
|
.do_update().set(crate::schema::demo_head_to_head::kills.eq(diesel::upsert::excluded(crate::schema::demo_head_to_head::kills)));
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
// TODO
|
||||||
|
// Execute queries
|
||||||
|
query.execute(connection).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,16 @@ impl Analysis for PerRoundAnalysis {
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let query = diesel::dsl::insert_into(crate::schema::demo_round::dsl::demo_round)
|
let query = diesel::dsl::insert_into(crate::schema::demo_round::dsl::demo_round)
|
||||||
.values(&values)
|
.values(&values)
|
||||||
.on_conflict_do_nothing();
|
.on_conflict((
|
||||||
|
crate::schema::demo_round::dsl::demo_id,
|
||||||
|
crate::schema::demo_round::dsl::round_number,
|
||||||
|
))
|
||||||
|
.do_update()
|
||||||
|
.set(
|
||||||
|
crate::schema::demo_round::dsl::events.eq(diesel::upsert::excluded(
|
||||||
|
crate::schema::demo_round::dsl::events,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
query.execute(connection).await?;
|
query.execute(connection).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub fn router(storage: Box<dyn crate::storage::DemoStorage>) -> axum::Router {
|
|||||||
.route("/:id/analysis/scoreboard", axum::routing::get(scoreboard))
|
.route("/:id/analysis/scoreboard", axum::routing::get(scoreboard))
|
||||||
.route("/:id/analysis/perround", axum::routing::get(perround))
|
.route("/:id/analysis/perround", axum::routing::get(perround))
|
||||||
.route("/:id/analysis/heatmap", axum::routing::get(heatmap))
|
.route("/:id/analysis/heatmap", axum::routing::get(heatmap))
|
||||||
|
.route("/:id/analysis/headtohead", axum::routing::get(head_to_head))
|
||||||
.with_state(Arc::new(DemoState { storage }))
|
.with_state(Arc::new(DemoState { storage }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,12 +45,22 @@ async fn list(
|
|||||||
crate::schema::demo_teams::table
|
crate::schema::demo_teams::table
|
||||||
.on(crate::schema::demos::dsl::demo_id.eq(crate::schema::demo_teams::dsl::demo_id)),
|
.on(crate::schema::demos::dsl::demo_id.eq(crate::schema::demo_teams::dsl::demo_id)),
|
||||||
)
|
)
|
||||||
|
.inner_join(
|
||||||
|
crate::schema::demo_players::table
|
||||||
|
.on(crate::schema::demos::dsl::demo_id
|
||||||
|
.eq(crate::schema::demo_players::dsl::demo_id)),
|
||||||
|
)
|
||||||
.select((
|
.select((
|
||||||
crate::models::Demo::as_select(),
|
crate::models::Demo::as_select(),
|
||||||
crate::models::DemoInfo::as_select(),
|
crate::models::DemoInfo::as_select(),
|
||||||
crate::models::DemoTeam::as_select(),
|
crate::models::DemoTeam::as_select(),
|
||||||
|
crate::models::DemoPlayer::as_select(),
|
||||||
))
|
))
|
||||||
.filter(crate::schema::demos::dsl::steam_id.eq(steam_id.to_string()));
|
.filter(
|
||||||
|
crate::schema::demos::dsl::steam_id
|
||||||
|
.eq(steam_id.to_string())
|
||||||
|
.and(crate::schema::demo_players::dsl::steam_id.eq(steam_id.to_string())),
|
||||||
|
);
|
||||||
let pending_query = crate::schema::demos::dsl::demos
|
let pending_query = crate::schema::demos::dsl::demos
|
||||||
.inner_join(crate::schema::processing_status::table.on(
|
.inner_join(crate::schema::processing_status::table.on(
|
||||||
crate::schema::demos::dsl::demo_id.eq(crate::schema::processing_status::dsl::demo_id),
|
crate::schema::demos::dsl::demo_id.eq(crate::schema::processing_status::dsl::demo_id),
|
||||||
@@ -72,6 +83,7 @@ async fn list(
|
|||||||
crate::models::Demo,
|
crate::models::Demo,
|
||||||
crate::models::DemoInfo,
|
crate::models::DemoInfo,
|
||||||
crate::models::DemoTeam,
|
crate::models::DemoTeam,
|
||||||
|
crate::models::DemoPlayer,
|
||||||
)> = done_query.load(con).await?;
|
)> = done_query.load(con).await?;
|
||||||
|
|
||||||
let pending_results: Vec<(crate::models::Demo)> = pending_query.load(con).await?;
|
let pending_results: Vec<(crate::models::Demo)> = pending_query.load(con).await?;
|
||||||
@@ -83,7 +95,7 @@ async fn list(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut demos = std::collections::HashMap::new();
|
let mut demos = std::collections::HashMap::new();
|
||||||
for (demo, info, team) in results.into_iter() {
|
for (demo, info, team, player) in results.into_iter() {
|
||||||
let entry = demos
|
let entry = demos
|
||||||
.entry(demo.demo_id.clone())
|
.entry(demo.demo_id.clone())
|
||||||
.or_insert(common::BaseDemoInfo {
|
.or_insert(common::BaseDemoInfo {
|
||||||
@@ -92,6 +104,7 @@ async fn list(
|
|||||||
uploaded_at: demo.uploaded_at,
|
uploaded_at: demo.uploaded_at,
|
||||||
team2_score: 0,
|
team2_score: 0,
|
||||||
team3_score: 0,
|
team3_score: 0,
|
||||||
|
player_team: player.team,
|
||||||
});
|
});
|
||||||
|
|
||||||
if team.team == 2 {
|
if team.team == 2 {
|
||||||
@@ -287,15 +300,31 @@ async fn scoreboard(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.filter(crate::schema::demo_players::dsl::demo_id.eq(demo_id));
|
.filter(crate::schema::demo_players::dsl::demo_id.eq(demo_id.clone()));
|
||||||
|
|
||||||
|
let team_query = crate::schema::demo_teams::dsl::demo_teams
|
||||||
|
.filter(crate::schema::demo_teams::dsl::demo_id.eq(demo_id));
|
||||||
|
|
||||||
let mut db_con = crate::db_connection().await;
|
let mut db_con = crate::db_connection().await;
|
||||||
|
|
||||||
let response: Vec<(crate::models::DemoPlayer, crate::models::DemoPlayerStats)> =
|
let db_result = db_con
|
||||||
match query.load(&mut db_con).await {
|
.build_transaction()
|
||||||
|
.read_only()
|
||||||
|
.run::<_, diesel::result::Error, _>(|con| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let players: Vec<(crate::models::DemoPlayer, crate::models::DemoPlayerStats)> =
|
||||||
|
query.load(con).await?;
|
||||||
|
let teams: Vec<crate::models::DemoTeam> = team_query.load(con).await?;
|
||||||
|
|
||||||
|
Ok((players, teams))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (response, team_response) = match db_result {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Querying DB: {:?}", e);
|
tracing::error!("Querying DB {:?}", e);
|
||||||
return Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
return Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -307,9 +336,16 @@ async fn scoreboard(
|
|||||||
|
|
||||||
let mut teams = std::collections::BTreeMap::new();
|
let mut teams = std::collections::BTreeMap::new();
|
||||||
for (player, stats) in response {
|
for (player, stats) in response {
|
||||||
let team = teams.entry(player.team as u32).or_insert(Vec::new());
|
let team =
|
||||||
|
teams
|
||||||
|
.entry(player.team as u32)
|
||||||
|
.or_insert(common::demo_analysis::ScoreBoardTeam {
|
||||||
|
number: player.team as u32,
|
||||||
|
players: Vec::new(),
|
||||||
|
score: 0,
|
||||||
|
});
|
||||||
|
|
||||||
team.push(common::demo_analysis::ScoreBoardPlayer {
|
team.players.push(common::demo_analysis::ScoreBoardPlayer {
|
||||||
name: player.name,
|
name: player.name,
|
||||||
kills: stats.kills as usize,
|
kills: stats.kills as usize,
|
||||||
deaths: stats.deaths as usize,
|
deaths: stats.deaths as usize,
|
||||||
@@ -318,8 +354,15 @@ async fn scoreboard(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for team in team_response {
|
||||||
|
let number = team.team as u32;
|
||||||
|
if let Some(entry) = teams.get_mut(&number) {
|
||||||
|
entry.score = team.end_score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(axum::Json(common::demo_analysis::ScoreBoard {
|
Ok(axum::Json(common::demo_analysis::ScoreBoard {
|
||||||
teams: teams.into_iter().collect::<Vec<_>>(),
|
teams: teams.into_values().collect::<Vec<_>>(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +506,13 @@ async fn perround(
|
|||||||
analysis::perround::RoundEvent::BombDefused => {
|
analysis::perround::RoundEvent::BombDefused => {
|
||||||
common::demo_analysis::RoundEvent::BombDefused
|
common::demo_analysis::RoundEvent::BombDefused
|
||||||
}
|
}
|
||||||
analysis::perround::RoundEvent::Kill { attacker, died } => {
|
analysis::perround::RoundEvent::Kill {
|
||||||
|
attacker,
|
||||||
|
died,
|
||||||
|
weapon,
|
||||||
|
noscope,
|
||||||
|
headshot,
|
||||||
|
} => {
|
||||||
let attacker_name = players
|
let attacker_name = players
|
||||||
.iter()
|
.iter()
|
||||||
.find(|p| p.steam_id == attacker.to_string())
|
.find(|p| p.steam_id == attacker.to_string())
|
||||||
@@ -478,6 +527,9 @@ async fn perround(
|
|||||||
common::demo_analysis::RoundEvent::Killed {
|
common::demo_analysis::RoundEvent::Killed {
|
||||||
attacker: attacker_name,
|
attacker: attacker_name,
|
||||||
died: died_name,
|
died: died_name,
|
||||||
|
weapon,
|
||||||
|
headshot,
|
||||||
|
noscope,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -505,6 +557,49 @@ async fn perround(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(session))]
|
||||||
|
async fn head_to_head(
|
||||||
|
session: UserSession,
|
||||||
|
Path(demo_id): Path<String>,
|
||||||
|
) -> Result<axum::response::Json<common::demo_analysis::HeadToHead>, axum::http::StatusCode> {
|
||||||
|
let mut db_con = crate::db_connection().await;
|
||||||
|
|
||||||
|
let head_to_head_query = crate::schema::demo_head_to_head::dsl::demo_head_to_head
|
||||||
|
.filter(crate::schema::demo_head_to_head::dsl::demo_id.eq(demo_id.clone()));
|
||||||
|
|
||||||
|
let player_query = crate::schema::demo_players::dsl::demo_players
|
||||||
|
.filter(crate::schema::demo_players::dsl::demo_id.eq(demo_id));
|
||||||
|
|
||||||
|
let (players, head_to_head_entries) = db_con.build_transaction().read_only().run(|connection| Box::pin(async move {
|
||||||
|
let head_to_head_entries: Vec<crate::models::DemoHeadToHead> = head_to_head_query.load(connection).await?;
|
||||||
|
let players: Vec<crate::models::DemoPlayer> = player_query.load(connection).await?;
|
||||||
|
|
||||||
|
Ok::<_, diesel::result::Error>((players, head_to_head_entries))
|
||||||
|
})).await.map_err(|e| {
|
||||||
|
tracing::error!("Querying DB: {:?}", e);
|
||||||
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let (mut row_team, mut column_team): (Vec<_>, Vec<_>) = players.into_iter().partition(|p| p.team == 2);
|
||||||
|
row_team.sort_unstable_by_key(|p| p.color);
|
||||||
|
column_team.sort_unstable_by_key(|p| p.color);
|
||||||
|
|
||||||
|
let results: Vec<_> = row_team.iter().map(|row_player| {
|
||||||
|
column_team.iter().map(|column_player| {
|
||||||
|
let row_kills = head_to_head_entries.iter().find(|entry| entry.player == row_player.steam_id && entry.enemy == column_player.steam_id);
|
||||||
|
let column_kills = head_to_head_entries.iter().find(|entry| entry.player == column_player.steam_id && entry.enemy == row_player.steam_id);
|
||||||
|
|
||||||
|
(row_kills.map(|k| k.kills).unwrap_or(0), column_kills.map(|k| k.kills).unwrap_or(0))
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Ok(axum::Json(common::demo_analysis::HeadToHead {
|
||||||
|
row_players: row_team.into_iter().map(|p| p.name).collect(),
|
||||||
|
column_players: column_team.into_iter().map(|p| p.name).collect(),
|
||||||
|
entries: results,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
// The corresponding values for each map can be found using the Source2 Viewer and opening the
|
// The corresponding values for each map can be found using the Source2 Viewer and opening the
|
||||||
// files in 'game/csgo/pak01_dir.vpk' and then 'resource/overviews/{map}.txt'
|
// files in 'game/csgo/pak01_dir.vpk' and then 'resource/overviews/{map}.txt'
|
||||||
static MINIMAP_COORDINATES: phf::Map<&str, MiniMapDefinition> = phf::phf_map! {
|
static MINIMAP_COORDINATES: phf::Map<&str, MiniMapDefinition> = phf::phf_map! {
|
||||||
|
|||||||
36
backend/src/gc.rs
Normal file
36
backend/src/gc.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(storage))]
|
||||||
|
pub async fn run_gc(storage: &mut dyn crate::storage::DemoStorage) -> Result<(), ()> {
|
||||||
|
let stored_demos = match storage.list_demos().await {
|
||||||
|
Ok(ds) => ds,
|
||||||
|
Err(e) => return Err(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("Found {} demos in storage", stored_demos.len());
|
||||||
|
|
||||||
|
let mut db_con = crate::db_connection().await;
|
||||||
|
|
||||||
|
let db_res = db_con
|
||||||
|
.build_transaction()
|
||||||
|
.run(move |con| {
|
||||||
|
Box::pin(async move {
|
||||||
|
for demo in stored_demos {
|
||||||
|
let query = crate::schema::demos::dsl::demos
|
||||||
|
.filter(crate::schema::demos::dsl::demo_id.eq(&demo)).select(crate::models::Demo::as_select());
|
||||||
|
|
||||||
|
let matching: Vec<crate::models::Demo> = query.load(con).await?;
|
||||||
|
|
||||||
|
if matching.is_empty() {
|
||||||
|
tracing::debug!("Should delete old demo {:?}", demo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), diesel::result::Error>(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
db_res.map_err(|e| ())
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ pub mod diesel_sessionstore;
|
|||||||
|
|
||||||
pub mod analysis;
|
pub mod analysis;
|
||||||
|
|
||||||
|
mod gc;
|
||||||
|
|
||||||
pub async fn db_connection() -> diesel_async::AsyncPgConnection {
|
pub async fn db_connection() -> diesel_async::AsyncPgConnection {
|
||||||
use diesel_async::AsyncConnection;
|
use diesel_async::AsyncConnection;
|
||||||
|
|
||||||
@@ -132,3 +134,16 @@ pub async fn run_analysis(storage: Box<dyn crate::storage::DemoStorage>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(storage))]
|
||||||
|
pub async fn run_garbage_collection(mut storage: Box<dyn crate::storage::DemoStorage>) {
|
||||||
|
loop {
|
||||||
|
tracing::info!("Running Garbage Collection");
|
||||||
|
|
||||||
|
if let Err(e) = gc::run_gc(storage.as_mut()).await {
|
||||||
|
tracing::error!("Running GC {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(15 * 60)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ struct CliArgs {
|
|||||||
|
|
||||||
#[clap(long = "analysis", default_value_t = true)]
|
#[clap(long = "analysis", default_value_t = true)]
|
||||||
analysis: bool,
|
analysis: bool,
|
||||||
|
|
||||||
|
#[clap(long = "gc", default_value_t = true)]
|
||||||
|
garbage_collection: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main(flavor = "multi_thread")]
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
@@ -93,7 +96,12 @@ async fn main() {
|
|||||||
if args.analysis {
|
if args.analysis {
|
||||||
tracing::info!("Enabled Analysis module");
|
tracing::info!("Enabled Analysis module");
|
||||||
|
|
||||||
component_set.spawn(backend::run_analysis(storage));
|
component_set.spawn(backend::run_analysis(storage.duplicate()));
|
||||||
|
}
|
||||||
|
if args.garbage_collection {
|
||||||
|
tracing::info!("Enabled Garbage-Collection module");
|
||||||
|
|
||||||
|
component_set.spawn(backend::run_garbage_collection(storage));
|
||||||
}
|
}
|
||||||
tracing::info!("Started modules");
|
tracing::info!("Started modules");
|
||||||
|
|
||||||
|
|||||||
@@ -120,3 +120,13 @@ pub struct DemoRound {
|
|||||||
pub win_reason: String,
|
pub win_reason: String,
|
||||||
pub events: serde_json::Value,
|
pub events: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Queryable, Selectable, Insertable, Debug)]
|
||||||
|
#[diesel(table_name = crate::schema::demo_head_to_head)]
|
||||||
|
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||||
|
pub struct DemoHeadToHead {
|
||||||
|
pub demo_id: String,
|
||||||
|
pub player: String,
|
||||||
|
pub enemy: String,
|
||||||
|
pub kills: i16,
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
demo_head_to_head (demo_id, player, enemy) {
|
||||||
|
demo_id -> Text,
|
||||||
|
player -> Text,
|
||||||
|
enemy -> Text,
|
||||||
|
kills -> Int2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
demo_heatmaps (demo_id, steam_id, team) {
|
demo_heatmaps (demo_id, steam_id, team) {
|
||||||
demo_id -> Text,
|
demo_id -> Text,
|
||||||
@@ -97,6 +106,7 @@ diesel::table! {
|
|||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
analysis_queue,
|
analysis_queue,
|
||||||
|
demo_head_to_head,
|
||||||
demo_heatmaps,
|
demo_heatmaps,
|
||||||
demo_info,
|
demo_info,
|
||||||
demo_player_stats,
|
demo_player_stats,
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ pub trait DemoStorage: Send + Sync {
|
|||||||
) -> futures::future::BoxFuture<'f, Result<crate::analysis::AnalysisData, String>>
|
) -> futures::future::BoxFuture<'f, Result<crate::analysis::AnalysisData, String>>
|
||||||
where
|
where
|
||||||
'own: 'f;
|
'own: 'f;
|
||||||
|
|
||||||
|
fn list_demos<'own, 'f>(&'own self) -> futures::future::BoxFuture<'f, Result<Vec<String>, String>>
|
||||||
|
where 'own: 'f;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FileStorage {
|
pub struct FileStorage {
|
||||||
@@ -107,6 +110,14 @@ impl DemoStorage for FileStorage {
|
|||||||
}
|
}
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_demos<'own, 'f>(&'own self) -> futures::future::BoxFuture<'f, Result<Vec<String>, String>>
|
||||||
|
where 'own: 'f {
|
||||||
|
async move {
|
||||||
|
// TODO
|
||||||
|
Ok(Vec::new())
|
||||||
|
}.boxed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct S3Storage {
|
pub struct S3Storage {
|
||||||
@@ -154,12 +165,10 @@ impl DemoStorage for S3Storage {
|
|||||||
let body_reader = tokio_util::io::StreamReader::new(body_with_io_error);
|
let body_reader = tokio_util::io::StreamReader::new(body_with_io_error);
|
||||||
futures::pin_mut!(body_reader);
|
futures::pin_mut!(body_reader);
|
||||||
|
|
||||||
self.bucket.list(String::new(), None).await.unwrap();
|
|
||||||
|
|
||||||
self.bucket
|
self.bucket
|
||||||
.put_object_stream(&mut body_reader, path)
|
.put_object_stream(&mut body_reader, path)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.map_err(|e| format!("Uploading Stream to bucket: {:?}", e))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -178,7 +187,7 @@ impl DemoStorage for S3Storage {
|
|||||||
let path = std::path::PathBuf::new().join(user_id).join(demo_id);
|
let path = std::path::PathBuf::new().join(user_id).join(demo_id);
|
||||||
let path = path.to_str().unwrap();
|
let path = path.to_str().unwrap();
|
||||||
|
|
||||||
let resp = self.bucket.get_object(path).await.unwrap();
|
let resp = self.bucket.get_object(path).await.map_err(|e| format!("Loading from Bucket: {:?}", e))?;
|
||||||
|
|
||||||
Ok(crate::analysis::AnalysisData::Preloaded(
|
Ok(crate::analysis::AnalysisData::Preloaded(
|
||||||
resp.to_vec().into(),
|
resp.to_vec().into(),
|
||||||
@@ -186,4 +195,20 @@ impl DemoStorage for S3Storage {
|
|||||||
}
|
}
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_demos<'own, 'f>(&'own self) -> futures::future::BoxFuture<'f, Result<Vec<String>, String>>
|
||||||
|
where 'own: 'f {
|
||||||
|
async move {
|
||||||
|
let listed = match self.bucket.list("".to_string(), None).await {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => return Err(format!("{:?}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(listed.into_iter().flat_map(|rs| {
|
||||||
|
rs.contents.into_iter().map(|entry| {
|
||||||
|
entry.key
|
||||||
|
})
|
||||||
|
}).collect())
|
||||||
|
}.boxed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct ScoreBoard {
|
pub struct ScoreBoard {
|
||||||
pub teams: Vec<(u32, Vec<ScoreBoardPlayer>)>,
|
pub teams: Vec<ScoreBoardTeam>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct ScoreBoardTeam {
|
||||||
|
pub number: u32,
|
||||||
|
pub score: i16,
|
||||||
|
pub players: Vec<ScoreBoardPlayer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -67,5 +74,18 @@ pub enum RoundWinReason {
|
|||||||
pub enum RoundEvent {
|
pub enum RoundEvent {
|
||||||
BombPlanted,
|
BombPlanted,
|
||||||
BombDefused,
|
BombDefused,
|
||||||
Killed { attacker: String, died: String },
|
Killed {
|
||||||
|
attacker: String,
|
||||||
|
died: String,
|
||||||
|
weapon: Option<String>,
|
||||||
|
noscope: bool,
|
||||||
|
headshot: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct HeadToHead {
|
||||||
|
pub row_players: Vec<String>,
|
||||||
|
pub column_players: Vec<String>,
|
||||||
|
pub entries: Vec<Vec<(i16, i16)>>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub struct BaseDemoInfo {
|
|||||||
pub uploaded_at: chrono::naive::NaiveDateTime,
|
pub uploaded_at: chrono::naive::NaiveDateTime,
|
||||||
pub team2_score: i16,
|
pub team2_score: i16,
|
||||||
pub team3_score: i16,
|
pub team3_score: i16,
|
||||||
|
pub player_team: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
|||||||
26
frontend/colors.css
Normal file
26
frontend/colors.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
* {
|
||||||
|
/** Generated using https://colorffy.com/dark-theme-generator?colors=40f7f4-121212 */
|
||||||
|
/** Dark theme primary colors */
|
||||||
|
--color-primary-a0: #40f7f4;
|
||||||
|
--color-primary-a10: #66f8f5;
|
||||||
|
--color-primary-a20: #81faf6;
|
||||||
|
--color-primary-a30: #98fbf8;
|
||||||
|
--color-primary-a40: #acfcf9;
|
||||||
|
--color-primary-a50: #befdfa;
|
||||||
|
|
||||||
|
/** Dark theme surface colors */
|
||||||
|
--color-surface-a0: #121212;
|
||||||
|
--color-surface-a10: #282828;
|
||||||
|
--color-surface-a20: #3f3f3f;
|
||||||
|
--color-surface-a30: #575757;
|
||||||
|
--color-surface-a40: #717171;
|
||||||
|
--color-surface-a50: #8b8b8b;
|
||||||
|
|
||||||
|
/** Dark theme mixed surface colors */
|
||||||
|
--color-surface-mixed-a0: #1b2525;
|
||||||
|
--color-surface-mixed-a10: #303939;
|
||||||
|
--color-surface-mixed-a20: #474f4f;
|
||||||
|
--color-surface-mixed-a30: #5e6666;
|
||||||
|
--color-surface-mixed-a40: #777e7d;
|
||||||
|
--color-surface-mixed-a50: #919696;
|
||||||
|
}
|
||||||
@@ -3,9 +3,17 @@
|
|||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="/main.css">
|
<link rel="stylesheet" href="/main.css">
|
||||||
|
|
||||||
|
<link data-trunk rel="css" href="colors.css">
|
||||||
<link data-trunk rel="copy-dir" href="static/"/>
|
<link data-trunk rel="copy-dir" href="static/"/>
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: var(--color-surface-a0);
|
||||||
|
color: var(--color-primary-a50);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -42,27 +42,8 @@ pub fn demo() -> impl leptos::IntoView {
|
|||||||
None => String::new(),
|
None => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let selected_tab = move || {
|
|
||||||
let loc = leptos_router::use_location();
|
|
||||||
let loc_path = loc.pathname.get();
|
|
||||||
let trailing = loc_path.split('/').last();
|
|
||||||
trailing.unwrap_or("/").to_owned()
|
|
||||||
};
|
|
||||||
|
|
||||||
let style = stylers::style! {
|
let style = stylers::style! {
|
||||||
"Demo",
|
"Demo",
|
||||||
.analysis_bar {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto auto auto;
|
|
||||||
column-gap: 20px;
|
|
||||||
|
|
||||||
background-color: #2d2d2d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.analysis_selector {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
@@ -73,19 +54,93 @@ pub fn demo() -> impl leptos::IntoView {
|
|||||||
.current {
|
.current {
|
||||||
background-color: #5d5d5d;
|
background-color: #5d5d5d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.demo_heading {
|
||||||
|
display: grid;
|
||||||
|
margin: 2vh 0px;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {class = style,
|
view! {class = style,
|
||||||
|
<div class="demo_heading">
|
||||||
<h2>Demo - { id } - { map }</h2>
|
<h2>Demo - { id } - { map }</h2>
|
||||||
|
<button on:click=move |_| rerun_analysis.dispatch(()) style="display: inline-block;">Rerun Analysis</button>
|
||||||
<button on:click=move |_| rerun_analysis.dispatch(())>Rerun analysis</button>
|
|
||||||
<div class="analysis_bar">
|
|
||||||
<div class="analysis_selector" class:current=move || selected_tab() == "scoreboard"><A href="scoreboard"><span>Scoreboard</span></A></div>
|
|
||||||
<div class="analysis_selector" class:current=move || selected_tab() == "perround"><A href="perround"><span>Per Round</span></A></div>
|
|
||||||
<div class="analysis_selector" class:current=move || selected_tab() == "heatmaps"><A href="heatmaps"><span>Heatmaps</span></A></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TabBar prefix=move || format!("/demo/{}/", id()) parts=&[("scoreboard", "Scoreboard"), ("perround", "Per Round"), ("heatmaps", "Heatmaps")] />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Outlet/>
|
<Outlet/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[leptos::component]
|
||||||
|
pub fn tab_bar<P>(
|
||||||
|
prefix: P,
|
||||||
|
parts: &'static [(&'static str, &'static str)],
|
||||||
|
) -> impl leptos::IntoView
|
||||||
|
where
|
||||||
|
P: Fn() -> String + Copy + 'static,
|
||||||
|
{
|
||||||
|
let selected_tab = move || {
|
||||||
|
let prefix = prefix();
|
||||||
|
let loc = leptos_router::use_location();
|
||||||
|
let loc_path = loc.pathname.get();
|
||||||
|
let trailing = loc_path
|
||||||
|
.strip_prefix(&prefix)
|
||||||
|
.unwrap_or(&loc_path)
|
||||||
|
.split('/')
|
||||||
|
.filter(|l| !l.is_empty())
|
||||||
|
.next();
|
||||||
|
trailing
|
||||||
|
.or(parts.first().map(|p| p.0))
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = stylers::style! {
|
||||||
|
"TabBar",
|
||||||
|
.analysis_bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--rows), auto);
|
||||||
|
column-gap: 20px;
|
||||||
|
|
||||||
|
background-color: var(--color-surface-a10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis_selector {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
padding: 1vh 1vw;
|
||||||
|
color: #d5d5d5;
|
||||||
|
background-color: var(--color-surface-a20);
|
||||||
|
}
|
||||||
|
.current {
|
||||||
|
background-color: var(--color-surface-a30);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let tabs = move || {
|
||||||
|
parts.into_iter().map(|(routename, name)| {
|
||||||
|
view! {class=style,
|
||||||
|
<div class="analysis_selector" class:current=move || selected_tab() == routename.to_string()>
|
||||||
|
<A href=routename.to_string()>
|
||||||
|
<span>{ name.to_string() }</span>
|
||||||
|
</A>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {class = style,
|
||||||
|
<div class="analysis_bar" style=format!("--rows: {}", parts.len())>
|
||||||
|
{ tabs }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ pub fn per_round() -> impl leptos::IntoView {
|
|||||||
.round_overview {
|
.round_overview {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
|
|
||||||
|
margin-top: 2vh;
|
||||||
|
|
||||||
width: 90vw;
|
width: 90vw;
|
||||||
grid-template-columns: auto repeat(12, 1fr) 5px repeat(12, 1fr) 5px repeat(3, 1fr) 5px repeat(3, 1fr);
|
grid-template-columns: auto repeat(12, 1fr) 5px repeat(12, 1fr) 5px repeat(3, 1fr) 5px repeat(3, 1fr);
|
||||||
grid-template-rows: repeat(3, auto);
|
grid-template-rows: repeat(3, auto);
|
||||||
@@ -79,24 +81,33 @@ pub fn per_round() -> impl leptos::IntoView {
|
|||||||
|
|
||||||
match (current_round, teams) {
|
match (current_round, teams) {
|
||||||
(Some(round), Some(teams)) => {
|
(Some(round), Some(teams)) => {
|
||||||
round.events.iter().map(|event| {
|
round.events.into_iter().map(|event| {
|
||||||
match event {
|
match event {
|
||||||
common::demo_analysis::RoundEvent::BombPlanted => view! { <li>Bomb has been planted</li> }.into_view(),
|
common::demo_analysis::RoundEvent::BombPlanted => view! { <li>Bomb has been planted</li> }.into_view(),
|
||||||
common::demo_analysis::RoundEvent::BombDefused => view! { <li>Bomb has been defused</li> }.into_view(),
|
common::demo_analysis::RoundEvent::BombDefused => view! { <li>Bomb has been defused</li> }.into_view(),
|
||||||
common::demo_analysis::RoundEvent::Killed { attacker, died } => {
|
common::demo_analysis::RoundEvent::Killed { attacker, died, weapon, headshot, noscope } => {
|
||||||
let mut attacker_t = teams.iter().find(|t| t.players.contains(attacker)).map(|t| t.name == "TERRORIST").unwrap_or(false);
|
let mut attacker_t = teams.iter().find(|t| t.players.contains(&attacker)).map(|t| t.name == "TERRORIST").unwrap_or(false);
|
||||||
let mut died_t = teams.iter().find(|t| t.players.contains(died)).map(|t| t.name == "TERRORIST").unwrap_or(false);
|
let mut died_t = teams.iter().find(|t| t.players.contains(&died)).map(|t| t.name == "TERRORIST").unwrap_or(false);
|
||||||
|
|
||||||
if (12..27).contains(&round_index) {
|
if (12..27).contains(&round_index) {
|
||||||
attacker_t = !attacker_t;
|
attacker_t = !attacker_t;
|
||||||
died_t = !died_t;
|
died_t = !died_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let weapon_display = move || {
|
||||||
|
let parts = weapon.as_ref().into_iter().map(|w| w.as_str())
|
||||||
|
.chain(headshot.then_some("Headshot").into_iter())
|
||||||
|
.chain(noscope.then_some("Noscope").into_iter());
|
||||||
|
|
||||||
|
format!("(using {})", parts.collect::<Vec<_>>().join(","))
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
class=style,
|
class=style,
|
||||||
<li>{"'"}
|
<li>
|
||||||
<span class:t_player=move || attacker_t class:ct_player=move || !attacker_t>{ attacker }</span>{"'"} killed {"'"}
|
{"'"}<span class:t_player=move || attacker_t class:ct_player=move || !attacker_t>{ attacker }</span>{"'"}
|
||||||
<span class:t_player=move || died_t class:ct_player=move || !died_t>{ died }</span>{"'"}
|
killed { weapon_display }
|
||||||
|
{"'"}<span class:t_player=move || died_t class:ct_player=move || !died_t>{ died }</span>{"'"}
|
||||||
</li>
|
</li>
|
||||||
}.into_view()
|
}.into_view()
|
||||||
},
|
},
|
||||||
@@ -192,8 +203,6 @@ pub fn per_round() -> impl leptos::IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
class=style,
|
class=style,
|
||||||
<h3>Per Round</h3>
|
|
||||||
|
|
||||||
<div class="round_overview">
|
<div class="round_overview">
|
||||||
{ team_names }
|
{ team_names }
|
||||||
{ round_overview }
|
{ round_overview }
|
||||||
|
|||||||
@@ -1,46 +1,20 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
|
use leptos_router::Outlet;
|
||||||
|
|
||||||
|
pub mod general;
|
||||||
|
pub mod headtohead;
|
||||||
|
|
||||||
|
use crate::demo::TabBar;
|
||||||
|
|
||||||
#[leptos::component]
|
#[leptos::component]
|
||||||
pub fn scoreboard() -> impl leptos::IntoView {
|
pub fn scoreboard() -> impl leptos::IntoView {
|
||||||
use leptos::Suspense;
|
let params = leptos_router::use_params_map();
|
||||||
|
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
|
||||||
let scoreboard_resource =
|
|
||||||
create_resource(leptos_router::use_params_map(), |params| async move {
|
|
||||||
let id = params.get("id").unwrap();
|
|
||||||
|
|
||||||
let res =
|
|
||||||
reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/scoreboard", id))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
res.json::<common::demo_analysis::ScoreBoard>()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
let (ordering, set_ordering) = create_signal::<orderings::Ordering>(orderings::DAMAGE);
|
|
||||||
|
|
||||||
let scoreboards = move || {
|
|
||||||
scoreboard_resource
|
|
||||||
.get()
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|v| v.teams.into_iter())
|
|
||||||
.map(|(team, players)| {
|
|
||||||
view! {
|
|
||||||
<TeamScoreboard value=players team_name=format!("Team {}", team) />
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<h2>Scoreboard</h2>
|
<TabBar prefix=move || format!("/demo/{}/scoreboard", id()) parts=&[("general", "General"), ("headtohead", "Head-to-Head")] />
|
||||||
|
|
||||||
<Suspense
|
<Outlet />
|
||||||
fallback=move || view! { <p>Loading Scoreboard data</p> }
|
|
||||||
>
|
|
||||||
{ scoreboards }
|
|
||||||
</Suspense>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +77,7 @@ fn team_scoreboard(
|
|||||||
let style = stylers::style! {
|
let style = stylers::style! {
|
||||||
"Team-Scoreboard",
|
"Team-Scoreboard",
|
||||||
tr:nth-child(even) {
|
tr:nth-child(even) {
|
||||||
background-color: #dddddd;
|
background-color: var(--color-surface-a10);
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
|
|||||||
45
frontend/src/demo/scoreboard/general.rs
Normal file
45
frontend/src/demo/scoreboard/general.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use leptos::*;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[leptos::component]
|
||||||
|
pub fn general() -> impl leptos::IntoView {
|
||||||
|
use leptos::Suspense;
|
||||||
|
|
||||||
|
let scoreboard_resource =
|
||||||
|
create_resource(leptos_router::use_params_map(), |params| async move {
|
||||||
|
let id = params.get("id").unwrap();
|
||||||
|
|
||||||
|
let res =
|
||||||
|
reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/scoreboard", id))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
res.json::<common::demo_analysis::ScoreBoard>()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
let (ordering, set_ordering) = create_signal::<orderings::Ordering>(orderings::DAMAGE);
|
||||||
|
|
||||||
|
let scoreboards = move || {
|
||||||
|
scoreboard_resource
|
||||||
|
.get()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|v| v.teams.into_iter())
|
||||||
|
.map(|team| {
|
||||||
|
view! {
|
||||||
|
<TeamScoreboard value=team.players team_name=format!("Team {} - {}", team.number, team.score) />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Suspense
|
||||||
|
fallback=move || view! { <p>Loading Scoreboard data</p> }
|
||||||
|
>
|
||||||
|
{ scoreboards }
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
}
|
||||||
98
frontend/src/demo/scoreboard/headtohead.rs
Normal file
98
frontend/src/demo/scoreboard/headtohead.rs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
use leptos::*;
|
||||||
|
use leptos::Suspense;
|
||||||
|
|
||||||
|
#[leptos::component]
|
||||||
|
pub fn head_to_head() -> impl leptos::IntoView {
|
||||||
|
let head_to_head_resource =
|
||||||
|
create_resource(leptos_router::use_params_map(), |params| async move {
|
||||||
|
let id = params.get("id").unwrap();
|
||||||
|
|
||||||
|
let res =
|
||||||
|
reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/headtohead", id))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
res.json::<common::demo_analysis::HeadToHead>()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
let style = stylers::style! {
|
||||||
|
"Head-to-Head-root",
|
||||||
|
div {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let matrix_view = move || head_to_head_resource.get().map(|r| view! {
|
||||||
|
<Matrix data=r />
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
class=style,
|
||||||
|
<Suspense fallback=move || view! { <p>Loading Head-to-Head data...</p> }>
|
||||||
|
<div>
|
||||||
|
{ matrix_view }
|
||||||
|
</div>
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[leptos::component]
|
||||||
|
fn matrix(data: common::demo_analysis::HeadToHead) -> impl leptos::IntoView {
|
||||||
|
let row_player_view = move || {
|
||||||
|
data.row_players.iter().enumerate().map(|(idx, name)| {
|
||||||
|
view! {
|
||||||
|
<span style=format!("grid-row: {}; grid-column: 1;", idx + 2)> {name} </span>
|
||||||
|
}
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let column_player_view = move || {
|
||||||
|
data.column_players.iter().enumerate().map(|(idx, name)| {
|
||||||
|
view! {
|
||||||
|
<span style=format!("grid-row: 1; grid-column: {}; text-align: right", idx + 2)> {name} </span>
|
||||||
|
}
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = stylers::style! {
|
||||||
|
"Head-to-Head-Matrix-Cell",
|
||||||
|
.cell {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
}
|
||||||
|
.cell_back {
|
||||||
|
grid-row: 1/3;
|
||||||
|
grid-column: 1/3;
|
||||||
|
|
||||||
|
background-color: var(--color-surface-a0);
|
||||||
|
background-image: linear-gradient(to right top, var(--color-surface-a0) 50%, var(--color-surface-a10) 50%);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry_view = move || {
|
||||||
|
data.entries.iter().enumerate().flat_map(|(row, row_data)| {
|
||||||
|
row_data.iter().enumerate().map(move |(column, &(row_kills, column_kills))| {
|
||||||
|
let entry = move || view! {
|
||||||
|
<div style="grid-row: 2; grid-column: 1; text-align: center;">{ row_kills }</div>
|
||||||
|
<div style="grid-row: 1; grid-column: 2; text-align: center;">{ column_kills }</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
class = style,
|
||||||
|
<div class="cell" style=format!("grid-row: {}; grid-column: {};", row + 2, column + 2)>
|
||||||
|
<div class="cell_back"></div>
|
||||||
|
{ entry }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
{ row_player_view }
|
||||||
|
{ column_player_view }
|
||||||
|
{ entry_view }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,7 @@ use leptos::*;
|
|||||||
#[leptos::component]
|
#[leptos::component]
|
||||||
pub fn homepage(get_notification: ReadSignal<u8>) -> impl leptos::IntoView {
|
pub fn homepage(get_notification: ReadSignal<u8>) -> impl leptos::IntoView {
|
||||||
let demo_data = create_resource(
|
let demo_data = create_resource(
|
||||||
move || {
|
move || get_notification.get(),
|
||||||
get_notification.get()
|
|
||||||
},
|
|
||||||
|_| async move {
|
|_| async move {
|
||||||
let res = reqwasm::http::Request::get("/api/demos/list")
|
let res = reqwasm::http::Request::get("/api/demos/list")
|
||||||
.send()
|
.send()
|
||||||
@@ -16,11 +14,24 @@ pub fn homepage(get_notification: ReadSignal<u8>) -> impl leptos::IntoView {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let pending_display = move || {
|
||||||
|
demo_data
|
||||||
|
.get()
|
||||||
|
.map(|d| d.pending)
|
||||||
|
.filter(|p| p.len() > 0)
|
||||||
|
.map(|pending| {
|
||||||
|
view! {
|
||||||
|
<p>{pending.len()} demos are pending/waiting for analysis</p>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<h2>Demos</h2>
|
<h2>Demos</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{ pending_display }
|
||||||
<DemoList demos=demo_data />
|
<DemoList demos=demo_data />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -34,18 +45,23 @@ fn demo_list(
|
|||||||
"DemoList",
|
"DemoList",
|
||||||
.list {
|
.list {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
grid-template-columns: auto auto auto;
|
grid-template-columns: auto auto auto;
|
||||||
row-gap: 1ch;
|
row-gap: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headers {
|
||||||
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
class=style,
|
class=style,
|
||||||
<div class="list">
|
<div class="list">
|
||||||
<span>Score</span>
|
<span class="headers">Score</span>
|
||||||
<span>Date</span>
|
<span class="headers">Date</span>
|
||||||
<span>Map</span>
|
<span class="headers">Map</span>
|
||||||
|
|
||||||
{ move || demos.get().map(|d| d.done).unwrap_or_default().into_iter().enumerate().map(|(i, demo)| view! { <DemoListEntry demo idx=i+1 /> }).collect::<Vec<_>>() }
|
{ move || demos.get().map(|d| d.done).unwrap_or_default().into_iter().enumerate().map(|(i, demo)| view! { <DemoListEntry demo idx=i+1 /> }).collect::<Vec<_>>() }
|
||||||
</div>
|
</div>
|
||||||
@@ -59,12 +75,14 @@ fn demo_list_entry(demo: common::BaseDemoInfo, idx: usize) -> impl leptos::IntoV
|
|||||||
.entry {
|
.entry {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
border: solid #030303aa 1px;
|
|
||||||
|
|
||||||
grid-column: 1 / 4;
|
grid-column: 1 / 4;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
.background_entry {
|
||||||
|
background-color: var(--color-surface-a20);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.score, .map {
|
.score, .map {
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
@@ -90,14 +108,37 @@ fn demo_list_entry(demo: common::BaseDemoInfo, idx: usize) -> impl leptos::IntoV
|
|||||||
grid-template-columns: auto;
|
grid-template-columns: auto;
|
||||||
grid-template-rows: auto auto;
|
grid-template-rows: auto auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.won {
|
||||||
|
color: #00aa00;
|
||||||
|
}
|
||||||
|
.lost {
|
||||||
|
color: #aa0000;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let (player_score, enemy_score) = if demo.player_team == 2 {
|
||||||
|
(demo.team2_score, demo.team3_score)
|
||||||
|
} else {
|
||||||
|
(demo.team3_score, demo.team2_score)
|
||||||
|
};
|
||||||
|
let won = move || player_score > enemy_score;
|
||||||
|
let lost = move || enemy_score > player_score;
|
||||||
|
let tie = move || player_score == enemy_score;
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
class=style,
|
class=style,
|
||||||
<span class="score" style=format!("grid-row: {};", idx + 1)>{demo.team2_score}:{demo.team3_score}</span>
|
<span class="entry background_entry" style=format!("grid-row: {};", idx + 1)></span>
|
||||||
|
<span
|
||||||
|
class="score"
|
||||||
|
style=format!("grid-row: {};", idx + 1)
|
||||||
|
class:won=won
|
||||||
|
class:lost=lost
|
||||||
|
class:tie=tie
|
||||||
|
>{demo.team2_score}:{demo.team3_score}</span>
|
||||||
<div class="date" style=format!("grid-row: {};", idx + 1)>
|
<div class="date" style=format!("grid-row: {};", idx + 1)>
|
||||||
<span>{demo.uploaded_at.format("%Y-%m-%d").to_string()}</span>
|
<span>{demo.uploaded_at.format("%Y-%m-%d").to_string()}</span>
|
||||||
<span>{demo.uploaded_at.format("%H-%M-%S").to_string()}</span>
|
<span>{demo.uploaded_at.format("%H:%M:%S").to_string()}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="map" style=format!("grid-row: {};", idx + 1)>{demo.map}</span>
|
<span class="map" style=format!("grid-row: {};", idx + 1)>{demo.map}</span>
|
||||||
<a class="entry" href=format!("demo/{}/scoreboard", demo.id) style=format!("grid-row: {};", idx + 1)></a>
|
<a class="entry" href=format!("demo/{}/scoreboard", demo.id) style=format!("grid-row: {};", idx + 1)></a>
|
||||||
|
|||||||
@@ -47,11 +47,73 @@ pub fn upload_demo(
|
|||||||
.shown {
|
.shown {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.lds-ellipsis,
|
||||||
|
.lds-ellipsis div {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.lds-ellipsis {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 60px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
.lds-ellipsis div {
|
||||||
|
position: absolute;
|
||||||
|
top: 7px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||||
|
}
|
||||||
|
.lds-ellipsis div:nth-child(1) {
|
||||||
|
left: 8px;
|
||||||
|
animation: lds-ellipsis1 0.6s infinite;
|
||||||
|
}
|
||||||
|
.lds-ellipsis div:nth-child(2) {
|
||||||
|
left: 8px;
|
||||||
|
animation: lds-ellipsis2 0.6s infinite;
|
||||||
|
}
|
||||||
|
.lds-ellipsis div:nth-child(3) {
|
||||||
|
left: 28px;
|
||||||
|
animation: lds-ellipsis2 0.6s infinite;
|
||||||
|
}
|
||||||
|
.lds-ellipsis div:nth-child(4) {
|
||||||
|
left: 48px;
|
||||||
|
animation: lds-ellipsis3 0.6s infinite;
|
||||||
|
}
|
||||||
|
@keyframes lds-ellipsis1 {
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes lds-ellipsis3 {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes lds-ellipsis2 {
|
||||||
|
0% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(20px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let uploading = RwSignal::new(false);
|
let uploading = RwSignal::new(false);
|
||||||
|
|
||||||
let handle_resp: std::rc::Rc<dyn Fn(&leptos::web_sys::Response)> = std::rc::Rc::new(move |resp: &leptos::web_sys::Response| {
|
let handle_resp: std::rc::Rc<dyn Fn(&leptos::web_sys::Response)> =
|
||||||
|
std::rc::Rc::new(move |resp: &leptos::web_sys::Response| {
|
||||||
if resp.status() != 200 {
|
if resp.status() != 200 {
|
||||||
// TODO
|
// TODO
|
||||||
// Display error somehow
|
// Display error somehow
|
||||||
@@ -86,10 +148,13 @@ pub fn upload_demo(
|
|||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p
|
<div
|
||||||
class:shown=move || uploading.get()
|
class:shown=move || uploading.get()
|
||||||
class:hidden=move || !uploading.get()
|
class:hidden=move || !uploading.get()
|
||||||
>Uploading...</p>
|
>
|
||||||
|
<span>Uploading</span>
|
||||||
|
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ fn main() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" view=move || view! { <Homepage get_notification=get_reload_demos /> } />
|
<Route path="/" view=move || view! { <Homepage get_notification=get_reload_demos /> } />
|
||||||
<Route path="/demo/:id" view=Demo>
|
<Route path="/demo/:id" view=Demo>
|
||||||
|
<Route path="scoreboard" view=frontend::demo::scoreboard::Scoreboard>
|
||||||
|
<Route path="general" view=frontend::demo::scoreboard::general::General />
|
||||||
|
<Route path="headtohead" view=frontend::demo::scoreboard::headtohead::HeadToHead />
|
||||||
|
<Route path="" view=frontend::demo::scoreboard::general::General />
|
||||||
|
</Route>
|
||||||
<Route path="perround" view=frontend::demo::perround::PerRound />
|
<Route path="perround" view=frontend::demo::perround::PerRound />
|
||||||
<Route path="scoreboard" view=frontend::demo::scoreboard::Scoreboard />
|
|
||||||
<Route path="heatmaps" view=frontend::demo::heatmap::Heatmaps />
|
<Route path="heatmaps" view=frontend::demo::heatmap::Heatmaps />
|
||||||
<Route path="" view=frontend::demo::scoreboard::Scoreboard />
|
<Route path="" view=frontend::demo::scoreboard::Scoreboard />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ fn steam_login(height: &'static str, width: &'static str) -> impl leptos::IntoVi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
tmp
|
||||||
{ tmp }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[leptos::component]
|
#[leptos::component]
|
||||||
@@ -45,8 +43,9 @@ pub fn top_bar(update_demo_visible: WriteSignal<DemoUploadStatus>) -> impl lepto
|
|||||||
height: 4vh;
|
height: 4vh;
|
||||||
padding-top: 0.5vh;
|
padding-top: 0.5vh;
|
||||||
padding-bottom: 0.5vh;
|
padding-bottom: 0.5vh;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
background-color: #28282f;
|
background-color: var(--color-surface-a10);
|
||||||
color: #d5d5d5;
|
color: #d5d5d5;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -60,7 +59,7 @@ pub fn top_bar(update_demo_visible: WriteSignal<DemoUploadStatus>) -> impl lepto
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
color: #d5d5d5;
|
color: var(--color-primary-a50);
|
||||||
width: 15vw;
|
width: 15vw;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
|||||||
2
migrations/2024-11-04-220702_head-to-head/down.sql
Normal file
2
migrations/2024-11-04-220702_head-to-head/down.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP TABLE demo_head_to_head;
|
||||||
8
migrations/2024-11-04-220702_head-to-head/up.sql
Normal file
8
migrations/2024-11-04-220702_head-to-head/up.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
CREATE TABLE IF NOT EXISTS demo_head_to_head (
|
||||||
|
demo_id TEXT NOT NULL,
|
||||||
|
player TEXT NOT NULL,
|
||||||
|
enemy TEXT NOT NULL,
|
||||||
|
kills int2 NOT NULL,
|
||||||
|
PRIMARY KEY (demo_id, player, enemy)
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user