Fix formatting and implement the end of game score display
This commit is contained in:
@@ -107,6 +107,13 @@ pub struct AnalysisInput {
|
|||||||
pub struct BaseInfo {
|
pub struct BaseInfo {
|
||||||
pub map: String,
|
pub map: String,
|
||||||
pub players: Vec<(BasePlayerInfo, BasePlayerStats)>,
|
pub players: Vec<(BasePlayerInfo, BasePlayerStats)>,
|
||||||
|
pub teams: Vec<(u32, BaseTeamInfo)>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BaseTeamInfo {
|
||||||
|
pub score: usize,
|
||||||
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@@ -10,8 +10,25 @@ impl BaseAnalysis {
|
|||||||
|
|
||||||
impl Analysis for BaseAnalysis {
|
impl Analysis for BaseAnalysis {
|
||||||
#[tracing::instrument(name = "Base", skip(self, input))]
|
#[tracing::instrument(name = "Base", 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 Base analysis");
|
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 Base analysis");
|
||||||
|
|
||||||
let file = std::fs::File::open(&input.path).unwrap();
|
let file = std::fs::File::open(&input.path).unwrap();
|
||||||
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).unwrap() };
|
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).unwrap() };
|
||||||
|
|
||||||
@@ -19,20 +36,33 @@ impl Analysis for BaseAnalysis {
|
|||||||
|
|
||||||
let base_result = BaseInfo {
|
let base_result = BaseInfo {
|
||||||
map: result.map,
|
map: result.map,
|
||||||
players: result.players.into_iter().map(|(info, stats)| {
|
players: result
|
||||||
(BasePlayerInfo {
|
.players
|
||||||
name: info.name,
|
.into_iter()
|
||||||
steam_id: info.steam_id,
|
.map(|(info, stats)| {
|
||||||
team: info.team,
|
(
|
||||||
ingame_id: info.ingame_id,
|
BasePlayerInfo {
|
||||||
color: info.color,
|
name: info.name,
|
||||||
}, BasePlayerStats {
|
steam_id: info.steam_id,
|
||||||
kills: stats.kills,
|
team: info.team,
|
||||||
assists: stats.assists,
|
ingame_id: info.ingame_id,
|
||||||
damage: stats.damage,
|
color: info.color,
|
||||||
deaths: stats.deaths,
|
},
|
||||||
|
BasePlayerStats {
|
||||||
|
kills: stats.kills,
|
||||||
|
assists: stats.assists,
|
||||||
|
damage: stats.damage,
|
||||||
|
deaths: stats.deaths,
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}).collect()
|
.collect(),
|
||||||
|
teams: result.teams.into_iter().map(|(numb, val)| {
|
||||||
|
(numb, BaseTeamInfo {
|
||||||
|
name: val.name,
|
||||||
|
score: val.score,
|
||||||
|
})
|
||||||
|
}).collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (player_info, player_stats): (Vec<_>, Vec<_>) = base_result
|
let (player_info, player_stats): (Vec<_>, Vec<_>) = base_result
|
||||||
@@ -64,48 +94,68 @@ impl Analysis for BaseAnalysis {
|
|||||||
map: base_result.map,
|
map: base_result.map,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let demo_teams: Vec<crate::models::DemoTeam> = base_result.teams.into_iter().map(|(numb, info)| {
|
||||||
|
crate::models::DemoTeam {
|
||||||
|
demo_id: input.demoid.clone(),
|
||||||
|
team: numb as i16,
|
||||||
|
end_score: info.score as i16,
|
||||||
|
start_name: info.name,
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
Ok(Box::new(move |connection| {
|
Ok(Box::new(move |connection| {
|
||||||
let store_demo_info_query =
|
let store_demo_info_query =
|
||||||
diesel::dsl::insert_into(crate::schema::demo_info::dsl::demo_info)
|
diesel::dsl::insert_into(crate::schema::demo_info::dsl::demo_info)
|
||||||
.values(demo_info)
|
.values(demo_info)
|
||||||
.on_conflict(crate::schema::demo_info::dsl::demo_id)
|
.on_conflict(crate::schema::demo_info::dsl::demo_id)
|
||||||
.do_update()
|
.do_update()
|
||||||
.set(
|
.set(
|
||||||
crate::schema::demo_info::dsl::map
|
crate::schema::demo_info::dsl::map
|
||||||
.eq(diesel::upsert::excluded(crate::schema::demo_info::dsl::map)),
|
.eq(diesel::upsert::excluded(crate::schema::demo_info::dsl::map)),
|
||||||
);
|
);
|
||||||
let store_demo_players_query =
|
let store_demo_players_query =
|
||||||
diesel::dsl::insert_into(crate::schema::demo_players::dsl::demo_players)
|
diesel::dsl::insert_into(crate::schema::demo_players::dsl::demo_players)
|
||||||
.values(player_info)
|
.values(player_info)
|
||||||
.on_conflict_do_nothing();
|
.on_conflict_do_nothing();
|
||||||
|
|
||||||
let store_demo_player_stats_query =
|
let store_demo_player_stats_query =
|
||||||
diesel::dsl::insert_into(crate::schema::demo_player_stats::dsl::demo_player_stats)
|
diesel::dsl::insert_into(crate::schema::demo_player_stats::dsl::demo_player_stats)
|
||||||
.values(player_stats)
|
.values(player_stats)
|
||||||
.on_conflict((
|
.on_conflict((
|
||||||
crate::schema::demo_player_stats::dsl::demo_id,
|
crate::schema::demo_player_stats::dsl::demo_id,
|
||||||
crate::schema::demo_player_stats::dsl::steam_id,
|
crate::schema::demo_player_stats::dsl::steam_id,
|
||||||
))
|
))
|
||||||
.do_update()
|
.do_update()
|
||||||
.set((
|
.set((
|
||||||
crate::schema::demo_player_stats::dsl::deaths.eq(diesel::upsert::excluded(
|
crate::schema::demo_player_stats::dsl::deaths.eq(diesel::upsert::excluded(
|
||||||
crate::schema::demo_player_stats::dsl::deaths,
|
crate::schema::demo_player_stats::dsl::deaths,
|
||||||
)),
|
)),
|
||||||
crate::schema::demo_player_stats::dsl::kills.eq(diesel::upsert::excluded(
|
crate::schema::demo_player_stats::dsl::kills.eq(diesel::upsert::excluded(
|
||||||
crate::schema::demo_player_stats::dsl::kills,
|
crate::schema::demo_player_stats::dsl::kills,
|
||||||
)),
|
)),
|
||||||
crate::schema::demo_player_stats::dsl::assists.eq(diesel::upsert::excluded(
|
crate::schema::demo_player_stats::dsl::assists.eq(
|
||||||
crate::schema::demo_player_stats::dsl::assists,
|
diesel::upsert::excluded(
|
||||||
)),
|
crate::schema::demo_player_stats::dsl::assists,
|
||||||
crate::schema::demo_player_stats::dsl::damage.eq(diesel::upsert::excluded(
|
),
|
||||||
crate::schema::demo_player_stats::dsl::damage,
|
),
|
||||||
)),
|
crate::schema::demo_player_stats::dsl::damage.eq(diesel::upsert::excluded(
|
||||||
|
crate::schema::demo_player_stats::dsl::damage,
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
|
||||||
|
let store_demo_teams = diesel::dsl::insert_into(crate::schema::demo_teams::dsl::demo_teams)
|
||||||
|
.values(demo_teams).on_conflict((crate::schema::demo_teams::dsl::demo_id, crate::schema::demo_teams::dsl::team))
|
||||||
|
.do_update()
|
||||||
|
.set((
|
||||||
|
crate::schema::demo_teams::dsl::start_name.eq(diesel::upsert::excluded(crate::schema::demo_teams::dsl::start_name)),
|
||||||
|
crate::schema::demo_teams::dsl::end_score.eq(diesel::upsert::excluded(crate::schema::demo_teams::dsl::end_score)),
|
||||||
));
|
));
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
store_demo_info_query.execute(connection).await?;
|
store_demo_info_query.execute(connection).await?;
|
||||||
store_demo_players_query.execute(connection).await?;
|
store_demo_players_query.execute(connection).await?;
|
||||||
store_demo_player_stats_query.execute(connection).await?;
|
store_demo_player_stats_query.execute(connection).await?;
|
||||||
|
store_demo_teams.execute(connection).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,47 +10,77 @@ impl HeatmapAnalysis {
|
|||||||
|
|
||||||
impl Analysis for HeatmapAnalysis {
|
impl Analysis for HeatmapAnalysis {
|
||||||
#[tracing::instrument(name = "Heatmap", skip(self, input))]
|
#[tracing::instrument(name = "Heatmap", 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>, ()> {
|
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!("Generating HEATMAPs");
|
tracing::info!("Generating HEATMAPs");
|
||||||
|
|
||||||
let file = std::fs::File::open(&input.path).unwrap();
|
let file = std::fs::File::open(&input.path).unwrap();
|
||||||
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).unwrap() };
|
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).unwrap() };
|
||||||
|
|
||||||
let config = analysis::heatmap::Config {
|
let config = analysis::heatmap::Config { cell_size: 5.0 };
|
||||||
cell_size: 5.0,
|
let result = analysis::heatmap::parse(&config, &mmap).unwrap();
|
||||||
};
|
|
||||||
let result = analysis::heatmap::parse(&config, &mmap).unwrap();
|
|
||||||
|
|
||||||
tracing::info!("Got {} Entity-Heatmaps", result.player_heatmaps.len());
|
tracing::info!("Got {} Entity-Heatmaps", result.player_heatmaps.len());
|
||||||
let heatmap_result: Vec<_> = result.player_heatmaps.into_iter().filter_map(|((userid, team), heatmap)| {
|
let heatmap_result: Vec<_> = result
|
||||||
let player = match result.player_info.get(&userid) {
|
.player_heatmaps
|
||||||
Some(p) => p,
|
.into_iter()
|
||||||
None => {
|
.filter_map(|((userid, team), heatmap)| {
|
||||||
tracing::warn!("Could not find player: {:?}", userid);
|
let player = match result.player_info.get(&userid) {
|
||||||
return None;
|
Some(p) => p,
|
||||||
}
|
None => {
|
||||||
};
|
tracing::warn!("Could not find player: {:?}", userid);
|
||||||
|
return None;
|
||||||
Some(((player.xuid.to_string(), team), heatmap))
|
}
|
||||||
}).collect();
|
};
|
||||||
|
|
||||||
let player_heatmaps: Vec<_> = heatmap_result.into_iter().map(|((player, team), heatmap)| {
|
Some(((player.xuid.to_string(), team), heatmap))
|
||||||
tracing::trace!("HeatMap for Player: {:?} in Team {:?}", player, team);
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
crate::models::DemoPlayerHeatmap {
|
let player_heatmaps: Vec<_> = heatmap_result
|
||||||
demo_id: input.demoid.clone(),
|
.into_iter()
|
||||||
steam_id: player,
|
.map(|((player, team), heatmap)| {
|
||||||
team,
|
tracing::trace!("HeatMap for Player: {:?} in Team {:?}", player, team);
|
||||||
data: serde_json::to_string(&heatmap).unwrap(),
|
|
||||||
}
|
crate::models::DemoPlayerHeatmap {
|
||||||
}).collect();
|
demo_id: input.demoid.clone(),
|
||||||
|
steam_id: player,
|
||||||
|
team,
|
||||||
|
data: serde_json::to_string(&heatmap).unwrap(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(Box::new(move |connection| {
|
Ok(Box::new(move |connection| {
|
||||||
let store_demo_player_heatmaps_query = diesel::dsl::insert_into(crate::schema::demo_heatmaps::dsl::demo_heatmaps)
|
let store_demo_player_heatmaps_query =
|
||||||
.values(player_heatmaps)
|
diesel::dsl::insert_into(crate::schema::demo_heatmaps::dsl::demo_heatmaps)
|
||||||
.on_conflict((crate::schema::demo_heatmaps::dsl::demo_id, crate::schema::demo_heatmaps::dsl::steam_id, crate::schema::demo_heatmaps::dsl::team))
|
.values(player_heatmaps)
|
||||||
.do_update()
|
.on_conflict((
|
||||||
.set(crate::schema::demo_heatmaps::dsl::data.eq(diesel::upsert::excluded(crate::schema::demo_heatmaps::dsl::data)));
|
crate::schema::demo_heatmaps::dsl::demo_id,
|
||||||
|
crate::schema::demo_heatmaps::dsl::steam_id,
|
||||||
|
crate::schema::demo_heatmaps::dsl::team,
|
||||||
|
))
|
||||||
|
.do_update()
|
||||||
|
.set(
|
||||||
|
crate::schema::demo_heatmaps::dsl::data.eq(diesel::upsert::excluded(
|
||||||
|
crate::schema::demo_heatmaps::dsl::data,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
store_demo_player_heatmaps_query.execute(connection).await?;
|
store_demo_player_heatmaps_query.execute(connection).await?;
|
||||||
|
|||||||
@@ -10,22 +10,41 @@ impl PerRoundAnalysis {
|
|||||||
|
|
||||||
impl Analysis for PerRoundAnalysis {
|
impl Analysis for PerRoundAnalysis {
|
||||||
#[tracing::instrument(name = "PerRoundAnalysis", skip(self, input))]
|
#[tracing::instrument(name = "PerRoundAnalysis", 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>, ()> {
|
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,
|
||||||
|
>,
|
||||||
|
(),
|
||||||
|
> {
|
||||||
let file = std::fs::File::open(&input.path).unwrap();
|
let file = std::fs::File::open(&input.path).unwrap();
|
||||||
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).unwrap() };
|
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).unwrap() };
|
||||||
|
|
||||||
let result = analysis::perround::parse(&mmap).unwrap();
|
let result = analysis::perround::parse(&mmap).unwrap();
|
||||||
|
|
||||||
let values: Vec<crate::models::DemoRound> = result.rounds.into_iter().enumerate().map(|(i, r)| {
|
let values: Vec<crate::models::DemoRound> = result
|
||||||
crate::models::DemoRound {
|
.rounds
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, r)| crate::models::DemoRound {
|
||||||
demo_id: input.demoid.clone(),
|
demo_id: input.demoid.clone(),
|
||||||
round_number: i as i16,
|
round_number: i as i16,
|
||||||
start_tick: r.start as i64,
|
start_tick: r.start as i64,
|
||||||
end_tick: r.end as i64,
|
end_tick: r.end as i64,
|
||||||
win_reason: serde_json::to_string(&r.winreason).unwrap(),
|
win_reason: serde_json::to_string(&r.winreason).unwrap(),
|
||||||
events: serde_json::to_value(&r.events).unwrap(),
|
events: serde_json::to_value(&r.events).unwrap(),
|
||||||
}
|
})
|
||||||
}).collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Box::new(move |connection| {
|
Ok(Box::new(move |connection| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
|||||||
@@ -41,28 +41,46 @@ async fn list(
|
|||||||
tracing::info!("SteamID: {:?}", steam_id);
|
tracing::info!("SteamID: {:?}", steam_id);
|
||||||
|
|
||||||
let query = crate::schema::demos::dsl::demos
|
let query = crate::schema::demos::dsl::demos
|
||||||
.inner_join(crate::schema::demo_info::table.on(
|
.inner_join(
|
||||||
crate::schema::demos::dsl::demo_id.eq(crate::schema::demo_info::dsl::demo_id),
|
crate::schema::demo_info::table
|
||||||
))
|
.on(crate::schema::demos::dsl::demo_id.eq(crate::schema::demo_info::dsl::demo_id)),
|
||||||
|
)
|
||||||
|
.inner_join(
|
||||||
|
crate::schema::demo_teams::table
|
||||||
|
.on(crate::schema::demos::dsl::demo_id.eq(crate::schema::demo_teams::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(),
|
||||||
))
|
))
|
||||||
.filter(crate::schema::demos::dsl::steam_id.eq(steam_id.to_string()));
|
.filter(crate::schema::demos::dsl::steam_id.eq(steam_id.to_string()));
|
||||||
let results: Vec<(crate::models::Demo, crate::models::DemoInfo)> =
|
let results: Vec<(crate::models::Demo, crate::models::DemoInfo, crate::models::DemoTeam)> =
|
||||||
query.load(&mut crate::db_connection().await).await.unwrap();
|
query.load(&mut crate::db_connection().await).await.unwrap();
|
||||||
|
|
||||||
|
let mut demos = std::collections::HashMap::new();
|
||||||
|
for (demo, info, team) in results.into_iter() {
|
||||||
|
let entry = demos.entry(demo.demo_id.clone()).or_insert(common::BaseDemoInfo {
|
||||||
|
id: demo.demo_id,
|
||||||
|
map: info.map,
|
||||||
|
team2_score: 0,
|
||||||
|
team3_score: 0,
|
||||||
|
});
|
||||||
|
|
||||||
Ok(axum::response::Json(
|
if team.team == 2 {
|
||||||
results
|
entry.team2_score = team.end_score;
|
||||||
.into_iter()
|
} else if team.team == 3 {
|
||||||
.map(|(demo, info)| {
|
entry.team3_score = team.end_score;
|
||||||
common::BaseDemoInfo {
|
} else {
|
||||||
id: demo.demo_id,
|
tracing::warn!("Unknown Team: {:?}", team);
|
||||||
map: info.map,
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
// TODO
|
||||||
))
|
// Sort this
|
||||||
|
let mut output = demos.into_values().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(axum::response::Json(output))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, session, form))]
|
#[tracing::instrument(skip(state, session, form))]
|
||||||
@@ -82,7 +100,10 @@ async fn upload(
|
|||||||
Some(c) => c,
|
Some(c) => c,
|
||||||
None => {
|
None => {
|
||||||
tracing::error!("Getting File content from request");
|
tracing::error!("Getting File content from request");
|
||||||
return Err((axum::http::StatusCode::BAD_REQUEST, "Failed to get file-content from upload"));
|
return Err((
|
||||||
|
axum::http::StatusCode::BAD_REQUEST,
|
||||||
|
"Failed to get file-content from upload",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,7 +111,7 @@ async fn upload(
|
|||||||
let demo_id = raw_demo_id.to_string();
|
let demo_id = raw_demo_id.to_string();
|
||||||
|
|
||||||
tracing::debug!(?demo_id, "Upload Size: {:?}", file_content.len());
|
tracing::debug!(?demo_id, "Upload Size: {:?}", file_content.len());
|
||||||
|
|
||||||
let user_folder = std::path::Path::new(&state.upload_folder).join(format!("{}/", steam_id));
|
let user_folder = std::path::Path::new(&state.upload_folder).join(format!("{}/", steam_id));
|
||||||
if !tokio::fs::try_exists(&user_folder).await.unwrap_or(false) {
|
if !tokio::fs::try_exists(&user_folder).await.unwrap_or(false) {
|
||||||
tokio::fs::create_dir_all(&user_folder).await.unwrap();
|
tokio::fs::create_dir_all(&user_folder).await.unwrap();
|
||||||
@@ -106,30 +127,36 @@ async fn upload(
|
|||||||
|
|
||||||
// Turn all of this into a single transaction
|
// Turn all of this into a single transaction
|
||||||
|
|
||||||
let db_trans_result = db_con.build_transaction().run(|c| {
|
let db_trans_result = db_con
|
||||||
Box::pin(async move {
|
.build_transaction()
|
||||||
let query =
|
.run(|c| {
|
||||||
diesel::dsl::insert_into(crate::schema::demos::dsl::demos).values(crate::models::NewDemo {
|
Box::pin(async move {
|
||||||
demo_id: demo_id.clone(),
|
let query = diesel::dsl::insert_into(crate::schema::demos::dsl::demos).values(
|
||||||
steam_id: steam_id.to_string(),
|
crate::models::NewDemo {
|
||||||
});
|
demo_id: demo_id.clone(),
|
||||||
query.execute(c).await?;
|
steam_id: steam_id.to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
query.execute(c).await?;
|
||||||
|
|
||||||
let queue_query = diesel::dsl::insert_into(crate::schema::analysis_queue::dsl::analysis_queue)
|
let queue_query =
|
||||||
.values(crate::models::AddAnalysisTask {
|
diesel::dsl::insert_into(crate::schema::analysis_queue::dsl::analysis_queue)
|
||||||
demo_id: demo_id.clone(),
|
.values(crate::models::AddAnalysisTask {
|
||||||
steam_id: steam_id.to_string(),
|
demo_id: demo_id.clone(),
|
||||||
});
|
steam_id: steam_id.to_string(),
|
||||||
queue_query.execute(c).await?;
|
});
|
||||||
|
queue_query.execute(c).await?;
|
||||||
|
|
||||||
let processing_query =
|
let processing_query = diesel::dsl::insert_into(
|
||||||
diesel::dsl::insert_into(crate::schema::processing_status::dsl::processing_status)
|
crate::schema::processing_status::dsl::processing_status,
|
||||||
.values(crate::models::ProcessingStatus { demo_id, info: 0 });
|
)
|
||||||
processing_query.execute(c).await?;
|
.values(crate::models::ProcessingStatus { demo_id, info: 0 });
|
||||||
|
processing_query.execute(c).await?;
|
||||||
|
|
||||||
Ok::<(), diesel::result::Error>(())
|
Ok::<(), diesel::result::Error>(())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}).await;
|
.await;
|
||||||
|
|
||||||
if let Err(e) = db_trans_result {
|
if let Err(e) = db_trans_result {
|
||||||
tracing::error!("Inserting data into db: {:?}", e);
|
tracing::error!("Inserting data into db: {:?}", e);
|
||||||
@@ -267,12 +294,14 @@ async fn scoreboard(
|
|||||||
async fn heatmap(
|
async fn heatmap(
|
||||||
session: UserSession,
|
session: UserSession,
|
||||||
Path(demo_id): Path<String>,
|
Path(demo_id): Path<String>,
|
||||||
) -> Result<axum::response::Json<Vec<common::demo_analysis::PlayerHeatmap>>, axum::http::StatusCode> {
|
) -> Result<axum::response::Json<Vec<common::demo_analysis::PlayerHeatmap>>, axum::http::StatusCode>
|
||||||
|
{
|
||||||
use base64::prelude::Engine;
|
use base64::prelude::Engine;
|
||||||
|
|
||||||
let mut db_con = crate::db_connection().await;
|
let mut db_con = crate::db_connection().await;
|
||||||
|
|
||||||
let demo_info_query = crate::schema::demo_info::dsl::demo_info.filter(crate::schema::demo_info::dsl::demo_id.eq(demo_id.clone()));
|
let demo_info_query = crate::schema::demo_info::dsl::demo_info
|
||||||
|
.filter(crate::schema::demo_info::dsl::demo_id.eq(demo_id.clone()));
|
||||||
let demo_info: crate::models::DemoInfo = match demo_info_query.first(&mut db_con).await {
|
let demo_info: crate::models::DemoInfo = match demo_info_query.first(&mut db_con).await {
|
||||||
Ok(i) => i,
|
Ok(i) => i,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -282,18 +311,26 @@ async fn heatmap(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let query = crate::schema::demo_players::dsl::demo_players
|
let query = crate::schema::demo_players::dsl::demo_players
|
||||||
.inner_join(crate::schema::demo_heatmaps::dsl::demo_heatmaps.on(
|
.inner_join(
|
||||||
crate::schema::demo_players::dsl::steam_id.eq(crate::schema::demo_heatmaps::dsl::steam_id)
|
crate::schema::demo_heatmaps::dsl::demo_heatmaps.on(
|
||||||
.and(crate::schema::demo_players::dsl::demo_id.eq(crate::schema::demo_heatmaps::dsl::demo_id))
|
crate::schema::demo_players::dsl::steam_id
|
||||||
)).filter(crate::schema::demo_players::dsl::demo_id.eq(demo_id));
|
.eq(crate::schema::demo_heatmaps::dsl::steam_id)
|
||||||
|
.and(
|
||||||
|
crate::schema::demo_players::dsl::demo_id
|
||||||
|
.eq(crate::schema::demo_heatmaps::dsl::demo_id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter(crate::schema::demo_players::dsl::demo_id.eq(demo_id));
|
||||||
|
|
||||||
let result: Vec<(crate::models::DemoPlayer, crate::models::DemoPlayerHeatmap)> = match query.load(&mut db_con).await {
|
let result: Vec<(crate::models::DemoPlayer, crate::models::DemoPlayerHeatmap)> =
|
||||||
Ok(d) => d,
|
match query.load(&mut db_con).await {
|
||||||
Err(e) => {
|
Ok(d) => d,
|
||||||
tracing::error!("Querying DB: {:?}", e);
|
Err(e) => {
|
||||||
return Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
tracing::error!("Querying DB: {:?}", e);
|
||||||
}
|
return Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let demo_map = &demo_info.map;
|
let demo_map = &demo_info.map;
|
||||||
let minimap_coords = match MINIMAP_COORDINATES.get(demo_map) {
|
let minimap_coords = match MINIMAP_COORDINATES.get(demo_map) {
|
||||||
@@ -304,21 +341,30 @@ async fn heatmap(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let data: Vec<common::demo_analysis::PlayerHeatmap> = result.into_iter().map(|(player, heatmap)| {
|
let data: Vec<common::demo_analysis::PlayerHeatmap> = result
|
||||||
let team = heatmap.team.clone();
|
.into_iter()
|
||||||
let mut heatmap: analysis::heatmap::HeatMap = serde_json::from_str(&heatmap.data).unwrap();
|
.map(|(player, heatmap)| {
|
||||||
heatmap.fit(minimap_coords.x_coord(0.0)..minimap_coords.x_coord(1024.0), minimap_coords.y_coord(1024.0)..minimap_coords.y_coord(0.0));
|
let team = heatmap.team.clone();
|
||||||
let h_image = heatmap.as_image();
|
let mut heatmap: analysis::heatmap::HeatMap =
|
||||||
|
serde_json::from_str(&heatmap.data).unwrap();
|
||||||
|
heatmap.fit(
|
||||||
|
minimap_coords.x_coord(0.0)..minimap_coords.x_coord(1024.0),
|
||||||
|
minimap_coords.y_coord(1024.0)..minimap_coords.y_coord(0.0),
|
||||||
|
);
|
||||||
|
let h_image = heatmap.as_image();
|
||||||
|
|
||||||
let mut buffer = std::io::Cursor::new(Vec::new());
|
let mut buffer = std::io::Cursor::new(Vec::new());
|
||||||
h_image.write_to(&mut buffer, image::ImageFormat::Png).unwrap();
|
h_image
|
||||||
|
.write_to(&mut buffer, image::ImageFormat::Png)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
common::demo_analysis::PlayerHeatmap {
|
common::demo_analysis::PlayerHeatmap {
|
||||||
name: player.name,
|
name: player.name,
|
||||||
team,
|
team,
|
||||||
png_data: base64::prelude::BASE64_STANDARD.encode(buffer.into_inner()),
|
png_data: base64::prelude::BASE64_STANDARD.encode(buffer.into_inner()),
|
||||||
}
|
}
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(axum::Json(data))
|
Ok(axum::Json(data))
|
||||||
}
|
}
|
||||||
@@ -328,23 +374,38 @@ async fn perround(
|
|||||||
session: UserSession,
|
session: UserSession,
|
||||||
Path(demo_id): Path<String>,
|
Path(demo_id): Path<String>,
|
||||||
) -> Result<axum::response::Json<Vec<common::demo_analysis::DemoRound>>, axum::http::StatusCode> {
|
) -> Result<axum::response::Json<Vec<common::demo_analysis::DemoRound>>, axum::http::StatusCode> {
|
||||||
let rounds_query = crate::schema::demo_round::dsl::demo_round.filter(crate::schema::demo_round::dsl::demo_id.eq(demo_id.clone()));
|
let rounds_query = crate::schema::demo_round::dsl::demo_round
|
||||||
let round_players_query = crate::schema::demo_players::dsl::demo_players.filter(crate::schema::demo_players::dsl::demo_id.eq(demo_id));
|
.filter(crate::schema::demo_round::dsl::demo_id.eq(demo_id.clone()));
|
||||||
|
let round_players_query = crate::schema::demo_players::dsl::demo_players
|
||||||
|
.filter(crate::schema::demo_players::dsl::demo_id.eq(demo_id));
|
||||||
|
|
||||||
let mut db_con = crate::db_connection().await;
|
let mut db_con = crate::db_connection().await;
|
||||||
|
|
||||||
let raw_rounds: Vec<crate::models::DemoRound> = rounds_query.load(&mut db_con).await.unwrap();
|
let raw_rounds: Vec<crate::models::DemoRound> = rounds_query.load(&mut db_con).await.unwrap();
|
||||||
let players: Vec<crate::models::DemoPlayer> = round_players_query.load(&mut db_con).await.unwrap();
|
let players: Vec<crate::models::DemoPlayer> =
|
||||||
|
round_players_query.load(&mut db_con).await.unwrap();
|
||||||
|
|
||||||
let mut result = Vec::with_capacity(raw_rounds.len());
|
let mut result = Vec::with_capacity(raw_rounds.len());
|
||||||
for raw_round in raw_rounds.into_iter() {
|
for raw_round in raw_rounds.into_iter() {
|
||||||
let reason = match serde_json::from_str(&raw_round.win_reason) {
|
let reason = match serde_json::from_str(&raw_round.win_reason) {
|
||||||
Ok(analysis::perround::WinReason::StillInProgress) => common::demo_analysis::RoundWinReason::StillInProgress,
|
Ok(analysis::perround::WinReason::StillInProgress) => {
|
||||||
Ok(analysis::perround::WinReason::TKilled) => common::demo_analysis::RoundWinReason::TKilled,
|
common::demo_analysis::RoundWinReason::StillInProgress
|
||||||
Ok(analysis::perround::WinReason::CTKilled) => common::demo_analysis::RoundWinReason::CTKilled,
|
}
|
||||||
Ok(analysis::perround::WinReason::BombDefused) => common::demo_analysis::RoundWinReason::BombDefused,
|
Ok(analysis::perround::WinReason::TKilled) => {
|
||||||
Ok(analysis::perround::WinReason::BombExploded) => common::demo_analysis::RoundWinReason::BombExploded,
|
common::demo_analysis::RoundWinReason::TKilled
|
||||||
Ok(analysis::perround::WinReason::TimeRanOut) => common::demo_analysis::RoundWinReason::TimeRanOut,
|
}
|
||||||
|
Ok(analysis::perround::WinReason::CTKilled) => {
|
||||||
|
common::demo_analysis::RoundWinReason::CTKilled
|
||||||
|
}
|
||||||
|
Ok(analysis::perround::WinReason::BombDefused) => {
|
||||||
|
common::demo_analysis::RoundWinReason::BombDefused
|
||||||
|
}
|
||||||
|
Ok(analysis::perround::WinReason::BombExploded) => {
|
||||||
|
common::demo_analysis::RoundWinReason::BombExploded
|
||||||
|
}
|
||||||
|
Ok(analysis::perround::WinReason::TimeRanOut) => {
|
||||||
|
common::demo_analysis::RoundWinReason::TimeRanOut
|
||||||
|
}
|
||||||
Ok(other) => {
|
Ok(other) => {
|
||||||
tracing::error!("Unknown Mapping {:?}", other);
|
tracing::error!("Unknown Mapping {:?}", other);
|
||||||
return Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
return Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
@@ -355,27 +416,38 @@ async fn perround(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let parsed_events: Vec<analysis::perround::RoundEvent> = serde_json::from_value(raw_round.events).unwrap();
|
let parsed_events: Vec<analysis::perround::RoundEvent> =
|
||||||
let events: Vec<_> = parsed_events.into_iter().map(|event| {
|
serde_json::from_value(raw_round.events).unwrap();
|
||||||
match event {
|
let events: Vec<_> = parsed_events
|
||||||
analysis::perround::RoundEvent::BombPlanted => common::demo_analysis::RoundEvent::BombPlanted,
|
.into_iter()
|
||||||
analysis::perround::RoundEvent::BombDefused => common::demo_analysis::RoundEvent::BombDefused,
|
.map(|event| match event {
|
||||||
|
analysis::perround::RoundEvent::BombPlanted => {
|
||||||
|
common::demo_analysis::RoundEvent::BombPlanted
|
||||||
|
}
|
||||||
|
analysis::perround::RoundEvent::BombDefused => {
|
||||||
|
common::demo_analysis::RoundEvent::BombDefused
|
||||||
|
}
|
||||||
analysis::perround::RoundEvent::Kill { attacker, died } => {
|
analysis::perround::RoundEvent::Kill { attacker, died } => {
|
||||||
let attacker_name = players.iter().find(|p| p.steam_id == attacker.to_string()).map(|p| p.name.clone()).unwrap();
|
let attacker_name = players
|
||||||
let died_name = players.iter().find(|p| p.steam_id == died.to_string()).map(|p| p.name.clone()).unwrap();
|
.iter()
|
||||||
|
.find(|p| p.steam_id == attacker.to_string())
|
||||||
|
.map(|p| p.name.clone())
|
||||||
|
.unwrap();
|
||||||
|
let died_name = players
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.steam_id == died.to_string())
|
||||||
|
.map(|p| p.name.clone())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
common::demo_analysis::RoundEvent::Killed {
|
common::demo_analysis::RoundEvent::Killed {
|
||||||
attacker: attacker_name,
|
attacker: attacker_name,
|
||||||
died: died_name,
|
died: died_name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}).collect();
|
.collect();
|
||||||
|
|
||||||
result.push(common::demo_analysis::DemoRound {
|
result.push(common::demo_analysis::DemoRound { reason, events });
|
||||||
reason,
|
|
||||||
events,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(axum::Json(result))
|
Ok(axum::Json(result))
|
||||||
|
|||||||
@@ -76,12 +76,12 @@ pub async fn run_api(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.layer(session_layer)
|
.layer(session_layer)
|
||||||
.nest_service(
|
.nest_service("/", tower_http::services::ServeDir::new(serve_dir));
|
||||||
"/",
|
|
||||||
tower_http::services::ServeDir::new(serve_dir),
|
|
||||||
);
|
|
||||||
|
|
||||||
let listen_addr = std::net::SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), 3000);
|
let listen_addr = std::net::SocketAddr::new(
|
||||||
|
std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)),
|
||||||
|
3000,
|
||||||
|
);
|
||||||
tracing::info!("Listening on Addr: {:?}", listen_addr);
|
tracing::info!("Listening on Addr: {:?}", listen_addr);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(listen_addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(listen_addr).await.unwrap();
|
||||||
@@ -110,17 +110,18 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
|
|||||||
let mut store_result_fns = Vec::new();
|
let mut store_result_fns = Vec::new();
|
||||||
for analysis in analysis::ANALYSIS_METHODS.iter().map(|a| a.clone()) {
|
for analysis in analysis::ANALYSIS_METHODS.iter().map(|a| a.clone()) {
|
||||||
let input = input.clone();
|
let input = input.clone();
|
||||||
let store_result = match tokio::task::spawn_blocking(move || analysis.analyse(input)).await {
|
let store_result =
|
||||||
Ok(Ok(r)) => r,
|
match tokio::task::spawn_blocking(move || analysis.analyse(input)).await {
|
||||||
Ok(Err(e)) => {
|
Ok(Ok(r)) => r,
|
||||||
tracing::error!("Analysis failed: {:?}", e);
|
Ok(Err(e)) => {
|
||||||
continue;
|
tracing::error!("Analysis failed: {:?}", e);
|
||||||
}
|
continue;
|
||||||
Err(e) => {
|
}
|
||||||
tracing::error!("Joining Task: {:?}", e);
|
Err(e) => {
|
||||||
continue;
|
tracing::error!("Joining Task: {:?}", e);
|
||||||
}
|
continue;
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
store_result_fns.push(store_result);
|
store_result_fns.push(store_result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ async fn main() {
|
|||||||
let registry = tracing_subscriber::Registry::default()
|
let registry = tracing_subscriber::Registry::default()
|
||||||
.with(tracing_subscriber::fmt::layer())
|
.with(tracing_subscriber::fmt::layer())
|
||||||
.with(tracing_subscriber::filter::filter_fn(|meta| {
|
.with(tracing_subscriber::filter::filter_fn(|meta| {
|
||||||
meta.target().contains("backend")
|
meta.target().contains("backend") || meta.target().contains("analysis")
|
||||||
|| meta.target().contains("analysis")
|
|
||||||
}));
|
}));
|
||||||
tracing::subscriber::set_global_default(registry).unwrap();
|
tracing::subscriber::set_global_default(registry).unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ pub struct NewDemo {
|
|||||||
pub struct Demo {
|
pub struct Demo {
|
||||||
pub steam_id: String,
|
pub steam_id: String,
|
||||||
pub demo_id: String,
|
pub demo_id: String,
|
||||||
pub uploaded_at: diesel::data_types::PgTimestamp
|
pub uploaded_at: diesel::data_types::PgTimestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable, Insertable, Debug)]
|
#[derive(Queryable, Selectable, Insertable, Debug)]
|
||||||
@@ -65,6 +65,16 @@ pub struct DemoPlayerStats {
|
|||||||
pub assists: i16,
|
pub assists: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Queryable, Selectable, Insertable, Debug)]
|
||||||
|
#[diesel(table_name = crate::schema::demo_teams)]
|
||||||
|
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||||
|
pub struct DemoTeam {
|
||||||
|
pub demo_id: String,
|
||||||
|
pub team: i16,
|
||||||
|
pub end_score: i16,
|
||||||
|
pub start_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable, Insertable, Debug)]
|
#[derive(Queryable, Selectable, Insertable, Debug)]
|
||||||
#[diesel(table_name = crate::schema::processing_status)]
|
#[diesel(table_name = crate::schema::processing_status)]
|
||||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||||
|
|||||||
@@ -56,6 +56,15 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
demo_teams (demo_id, team) {
|
||||||
|
demo_id -> Text,
|
||||||
|
team -> Int2,
|
||||||
|
end_score -> Int2,
|
||||||
|
start_name -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
demos (steam_id, demo_id) {
|
demos (steam_id, demo_id) {
|
||||||
steam_id -> Text,
|
steam_id -> Text,
|
||||||
@@ -79,13 +88,6 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
user_demos (steam_id, demo_id) {
|
|
||||||
steam_id -> Text,
|
|
||||||
demo_id -> Text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
users (steamid) {
|
users (steamid) {
|
||||||
steamid -> Text,
|
steamid -> Text,
|
||||||
@@ -100,9 +102,9 @@ diesel::allow_tables_to_appear_in_same_query!(
|
|||||||
demo_player_stats,
|
demo_player_stats,
|
||||||
demo_players,
|
demo_players,
|
||||||
demo_round,
|
demo_round,
|
||||||
|
demo_teams,
|
||||||
demos,
|
demos,
|
||||||
processing_status,
|
processing_status,
|
||||||
sessions,
|
sessions,
|
||||||
user_demos,
|
|
||||||
users,
|
users,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
pub struct BaseDemoInfo {
|
pub struct BaseDemoInfo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub map: String,
|
pub map: String,
|
||||||
|
pub team2_score: i16,
|
||||||
|
pub team3_score: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub fn demo_list_entry(demo: common::BaseDemoInfo) -> impl leptos::IntoView {
|
|||||||
view! {
|
view! {
|
||||||
<li>
|
<li>
|
||||||
<A href=format!("demo/{}/scoreboard", demo.id)>
|
<A href=format!("demo/{}/scoreboard", demo.id)>
|
||||||
<span>{demo.map} - {demo.id}</span>
|
<span>{demo.map} - {demo.id} - {demo.team2_score}:{demo.team3_score}</span>
|
||||||
</A>
|
</A>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
DROP TABLE processing_status;
|
DROP TABLE processing_status;
|
||||||
DROP TABLE demo_player_stats;
|
DROP TABLE demo_player_stats;
|
||||||
DROP TABLE demo_players;
|
DROP TABLE demo_players;
|
||||||
|
DROP TABLE demo_teams;
|
||||||
DROP TABLE demo_info;
|
DROP TABLE demo_info;
|
||||||
|
|||||||
@@ -27,3 +27,11 @@ CREATE TABLE IF NOT EXISTS demo_player_stats (
|
|||||||
assists int2 NOT NULL,
|
assists int2 NOT NULL,
|
||||||
PRIMARY KEY (demo_id, steam_id)
|
PRIMARY KEY (demo_id, steam_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS demo_teams (
|
||||||
|
demo_id TEXT NOT NULL,
|
||||||
|
team int2 NOT NULL,
|
||||||
|
end_score int2 NOT NULL,
|
||||||
|
start_name TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (demo_id, team)
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user