normalized database

This commit is contained in:
Robin Appelman 2020-06-17 23:33:10 +02:00
commit d6c622fcda
8 changed files with 677 additions and 591 deletions

View file

@ -1,12 +1,17 @@
use serde::Deserialize;
#[derive(Debug, Clone, Copy, sqlx::Type, Deserialize, Eq, PartialEq)]
#[sqlx(rename = "team")]
#[sqlx(rename_all = "lowercase")]
pub enum TeamId {
Blue,
Red,
}
#[derive(Debug, Clone, Copy, sqlx::Type, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
#[sqlx(rename_all = "lowercase")]
#[sqlx(rename = "class_type")]
pub enum Class {
Scout,
Soldier,
@ -19,13 +24,20 @@ pub enum Class {
Spy,
}
#[derive(Debug, Clone, Copy, sqlx::Type, Deserialize, Eq, PartialEq)]
#[derive(Debug, Clone, Copy, sqlx::Type, Eq, PartialEq)]
#[sqlx(rename = "game_mode")]
pub enum GameMode {
#[sqlx(rename = "ultiduo")]
UltiDuo,
#[sqlx(rename = "4v4")]
Fours,
#[sqlx(rename = "6v6")]
Sixes,
#[sqlx(rename = "7v7")]
Sevens,
#[sqlx(rename = "9v9")]
Highlander,
#[sqlx(rename = "other")]
Other,
}
@ -39,6 +51,7 @@ pub enum EventType {
#[derive(Debug, Clone, Copy, sqlx::Type, Deserialize, Hash, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
#[sqlx(rename_all = "lowercase")]
pub enum Medigun {
Medigun,
KritzKrieg,
@ -52,221 +65,21 @@ impl Default for Medigun {
}
}
#[derive(Debug, Clone, Copy, sqlx::Type, Deserialize, Hash, Eq, PartialEq)]
// #[sqlx(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum Weapon {
Sniperrifle,
TauntSniper,
SydneySleeper,
TheWinger,
HotHand,
DeflectRocket,
ScoutSword,
VoodooPin,
Degreaser,
Shortstop,
RobotArm,
TfPumpkinBomb,
Kunai,
Wrench,
NecroSmasher,
Headtaker,
ProtoSyringe,
EternalReward,
EurekaEffect,
DragonsFuryBonus,
GrapplingHook,
PepPistol,
Guillotine,
LavaAxe,
WranglerKill,
PanicAttack,
LooseCannon,
Thirddegree,
TauntPyro,
IronBomber,
PersianPersuader,
Amputator,
TfProjectilePipe,
AwperHand,
Demokatana,
LooseCannonReflect,
ObjMinisentry,
BackScratcher,
Pomson,
TheClassic,
Sandman,
Axtinguisher,
ObjSentrygun2,
Jar,
Samrevolver,
DeflectFlare,
LochNLoad,
TfProjectileRocket,
Annihilator,
RocketpackStomp,
UllapoolCaberExplosion,
Minigun,
DeflectPromode,
LibertyLauncher,
Nessieclub,
QuickiebombLauncher,
CowMangler,
WrapAssassin,
Blackbox,
CandyCane,
RocketlauncherDirecthit,
AiFlamethrower,
SodaPopper,
FrontierJustice,
RescueRangerReflect,
WarriorSpirit,
Widowmaker,
QuakeRl,
TfProjectileArrow,
Sledgehammer,
DeflectArrow,
MarketGardener,
UniquePickaxeEscape,
PistolScout,
BostonBasher,
SpellbookBats,
Holymackerel,
Club,
Ball,
Fireaxe,
Backburner,
EvictionNotice,
HolidayPunch,
TauntSoldier,
ShotgunPrimary,
NonnonviolentProtest,
ForceANature,
Paintrain,
FreedomStaff,
ShotgunHwg,
LongHeatmaker,
ShotgunSoldier,
Knife,
Batsaber,
Mailbox,
CompoundBow,
UllapoolCaber,
Ambassador,
PlayerPenetration,
Powerjack,
CrusadersCrossbow,
ScotlandShard,
WrenchJag,
Scattergun,
Unknown,
Shahanshah,
Pistol,
SpellbookBoss,
Smg,
DragonsFury,
Revolver,
Player,
TheMaul,
Skullbat,
HamShank,
SolemnVow,
IronCurtain,
Bonesaw,
DumpsterDevice,
Bushwacka,
Builder,
BreadBite,
SouthernHospitality,
Tribalkukri,
TheCapper,
Fists,
DisciplinaryAction,
TfProjectileFlare,
BleedKill,
BlackRose,
Letranger,
Tomislav,
Atomizer,
BackScatter,
PepBrawlerblaster,
TfProjectilePipeRemote,
Battleaxe,
DeflectFlareDetonator,
TauntMedic,
Telefrag,
StickybombDefender,
SplendidScreen,
Claidheamohmor,
Airstrike,
RighteousBison,
GlovesRunningUrgently,
Sword,
Mantreads,
DeflectSticky,
Enforcer,
ScorchShot,
ProRifle,
SpyCicle,
Bat,
SharpDresser,
SpellbookLightning,
TideTurner,
ShotgunPyro,
LavaBat,
TauntHeavy,
Bottle,
UniquePickaxe,
Phlogistinator,
CrossingGuard,
GigerCounter,
ChargedSmg,
SyringegunMedic,
Gloves,
BazaarBargain,
SpellbookMirv,
BigEarner,
Battleneedle,
Warfan,
ObjSentrygun,
Manmelter,
FamilyBusiness,
ReserveShooter,
ShortCircuit,
Flaregun,
SpellbookFireball,
World,
JarMilk,
Flamethrower,
ShootingStar,
TriggerHurt,
Blutsauger,
TauntSpy,
TfProjectileEnergyBall,
JarGas,
TfProjectileMechanicalarmorb,
ObjSentrygun3,
Diamondback,
Shovel,
BrassBeast,
LooseCannonImpact,
Demoshield,
PrinnyMachete,
Machina,
RocketlauncherFireball,
StickyResistance,
Detonator,
TfProjectileSentryrocket,
UnarmedCombat,
SpellbookSkeleton,
Ubersaw,
Maxgun,
RobotArmComboKill,
RescueRanger,
Apocofists,
Natascha,
ProSmg,
SteelFists,
Fryingpan,
#[derive(Debug, Clone, Copy, sqlx::Type, Eq, PartialEq)]
#[sqlx(rename_all = "lowercase")]
#[sqlx(rename = "map_type")]
pub enum MapType {
Stopwatch,
Cp,
KOTH,
CTF,
UltiDuo,
BBall,
Other,
}
impl Default for MapType {
fn default() -> Self {
MapType::Other
}
}

257
src/database.rs Normal file
View file

@ -0,0 +1,257 @@
use crate::data::{Class, GameMode, MapType, Medigun, TeamId};
use crate::normalized::NormalizedLog;
use crate::raw::Event;
use chrono::{DateTime, Utc};
use sqlx::PgPool;
use std::collections::HashMap;
use steamid_ng::SteamID;
pub async fn store_log(pool: &PgPool, id: u32, log: &NormalizedLog) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO logs(id, red_score, blue_score, length, game_mode, map, type, date)\
VALUES($1, $2, $3, $4, $5, $6, $7, $8)",
id as i32,
log.teams.red.score as i32,
log.teams.blue.score as i32,
log.info.total_length as i32,
log.game_mode() as GameMode,
log.info.map,
log.info.map_type() as MapType,
log.info.date() as DateTime<Utc>
)
.execute(pool)
.await?;
for (num, round) in log.rounds.iter().enumerate() {
let round_id: i32 = sqlx::query!(
r#"INSERT INTO rounds(
round, log_id, length, winner, first_cap, red_score, blue_score,
red_kills, blue_kills, red_dmg, blue_dmg, red_ubers, blue_ubers
)
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
RETURNING id"#,
num as i32,
id as i32,
round.length as i32,
round.winner as TeamId,
round.first_cap as TeamId,
round.team.red.score as i32,
round.team.blue.score as i32,
round.team.red.kills as i32,
round.team.blue.kills as i32,
round.team.red.dmg as i32,
round.team.blue.dmg as i32,
round.team.red.charges as i32,
round.team.blue.charges as i32,
)
.fetch_one(pool)
.await?
.id;
for event in &round.events {
match event {
Event::PointCap { time, team, point } => {
sqlx::query!(
"INSERT INTO events_point_cap(round_id, time, team, point)\
VALUES($1, $2, $3, $4)",
round_id,
*time as i32,
*team as TeamId,
*point as i32,
)
.execute(pool)
.await?;
}
Event::RoundWin { time, team } => {
sqlx::query!(
"INSERT INTO events_round_win(round_id, time, team)\
VALUES($1, $2, $3)",
round_id,
*time as i32,
*team as TeamId,
)
.execute(pool)
.await?;
}
Event::MedicDeath {
time,
team,
steamid,
killer,
} => {
sqlx::query!(
"INSERT INTO events_medic_death(round_id, time, team, steam_id, killer)\
VALUES($1, $2, $3, $4, $5)",
round_id,
*time as i32,
*team as TeamId,
u64::from(*steamid) as i64,
u64::from(*killer) as i64,
)
.execute(pool)
.await?;
}
Event::Drop {
time,
steamid,
team,
} => {
sqlx::query!(
"INSERT INTO events_drop(round_id, time, team, steam_id)\
VALUES($1, $2, $3, $4)",
round_id,
*time as i32,
*team as TeamId,
u64::from(*steamid) as i64,
)
.execute(pool)
.await?;
}
Event::Charge {
medigun,
time,
steamid,
team,
} => {
sqlx::query!(
"INSERT INTO events_charge(round_id, time, team, medigun, steam_id)\
VALUES($1, $2, $3, $4, $5)",
round_id,
*time as i32,
*team as TeamId,
*medigun as Medigun,
u64::from(*steamid) as i64,
)
.execute(pool)
.await?;
}
}
}
}
let mut heals_received: HashMap<SteamID, u32> = HashMap::new();
for heal_map in log.heal_spread.values() {
for (steam_id, heals) in heal_map {
heals_received
.entry(*steam_id)
.and_modify(|received| *received += heals)
.or_insert(*heals);
}
}
for (steam_id, player) in &log.players {
let kills = log.class_kills.get(steam_id).cloned().unwrap_or_default();
let player_id: i32 = sqlx::query!(
"INSERT INTO players (\
log_id, steam_id, name, kills, deaths, assists,\
suicides, dmg, damage_taken, ubers, medigun_ubers,\
kritzkrieg_ubers, quickfix_ubers, vacinator_ubers,\
drops, medkits, medkits_hp, backstabs, headshots,\
heal, heals_received,\
scout_kills, soldier_kills, pyro_kills, demoman_kills,\
heavy_kills, engineer_kills, medic_kills, sniper_kills, spy_kills
)\
VALUES(\
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,\
$11, $12, $13, $14, $15, $16, $17, $18, $19, $20,\
$21, $22, $23, $24, $25, $26, $27, $28, $29, $30\
)\
RETURNING id",
id as i32,
u64::from(*steam_id) as i64,
log.names.get(steam_id).cloned().unwrap_or_default(),
player.kills as i32,
player.deaths as i32,
player.assists as i32,
player.suicides as i32,
player.dmg as i32,
player.dt_real as i32,
player.ubers as i32,
player
.ubertypes
.get(&Medigun::Medigun)
.copied()
.unwrap_or_default() as i32,
player
.ubertypes
.get(&Medigun::KritzKrieg)
.copied()
.unwrap_or_default() as i32,
player
.ubertypes
.get(&Medigun::QuickFix)
.copied()
.unwrap_or_default() as i32,
player
.ubertypes
.get(&Medigun::Vacinator)
.copied()
.unwrap_or_default() as i32,
player.drops as i32,
player.medkits as i32,
player.medkits_hp as i32,
player.backstabs as i32,
player.headshots as i32,
player.heal as i32,
heals_received.get(steam_id).copied().unwrap_or_default() as i32,
kills.scout as i32,
kills.soldier as i32,
kills.pyro as i32,
kills.demoman as i32,
kills.heavyweapons as i32,
kills.engineer as i32,
kills.medic as i32,
kills.sniper as i32,
kills.spy as i32
)
.fetch_one(pool)
.await?
.id;
for class in &player.class_stats {
let class_stat_id: i32 = sqlx::query!(
"INSERT INTO class_stats(player_id, type, time, kills, deaths, assists, dmg)\
VALUES($1, $2, $3, $4, $5, $6, $7)\
RETURNING id",
player_id,
class.class as Class,
class.total_time as i32,
class.kills as i32,
class.deaths as i32,
class.assists as i32,
class.dmg as i32,
)
.fetch_one(pool)
.await?
.id;
for (weapon, stats) in &class.weapon {
sqlx::query!(
"INSERT INTO player_weapon_stats(class_stat_id, weapon, kills, shots, hits, dmg)\
VALUES($1, $2, $3, $4, $5, $6)",
class_stat_id as i32,
*weapon,
stats.kills as i32,
stats.shots as i32,
stats.hits as i32,
stats.dmg as i32,
)
.execute(pool)
.await?;
}
}
}
Ok(())
}
// macro_rules! insert_fields {
// ($table:ident, {
// $($($field:ident => $value:expr),)+
// }) => {
// sqlx::query!(
// concat!("INSERT INTO ", stringify!($table), "(", stringify!$(field)) ") VALUES ()"\
// VALUES($1, $2, $3, $4, $5)",
// )
// };
// }

View file

@ -1,7 +1,23 @@
mod data;
mod raw;
mod database;
mod normalized;
mod raw;
fn main() {
println!("Hello, world!");
use crate::database::store_log;
use crate::normalized::NormalizedLog;
use main_error::MainError;
use sqlx::PgPool;
use std::fs;
#[tokio::main]
async fn main() -> Result<(), MainError> {
let database_url = dotenv::var("DATABASE_URL")?;
let content = fs::read_to_string("tests/data/2522305.json").unwrap();
let parsed: NormalizedLog = serde_json::from_str(&content).unwrap();
let pool = PgPool::builder().max_size(2).build(&database_url).await?;
dbg!(store_log(&pool, 2522305, &parsed).await)?;
Ok(())
}

View file

@ -1,8 +1,10 @@
pub use crate::data::TeamId;
use crate::data::{GameMode, MapType};
use crate::raw::RawLog;
pub use crate::raw::{
ChatMessage, ClassNumbers, Event, Player, RoundPlayer, Team, Teams, Uploader,
};
use chrono::{DateTime, NaiveDateTime, Utc};
use serde::Deserialize;
use std::collections::HashMap;
use steamid_ng::SteamID;
@ -24,6 +26,22 @@ pub struct NormalizedLog {
pub info: Info,
}
impl NormalizedLog {
pub fn game_mode(&self) -> GameMode {
if self.info.map_type() == MapType::UltiDuo {
return GameMode::UltiDuo;
}
match self.players.len() {
7..=9 => GameMode::Fours,
11..=13 => GameMode::Sixes,
14 => GameMode::Sevens,
17..=19 => GameMode::Highlander,
_ => GameMode::Other,
}
}
}
#[derive(Debug, Clone)]
pub struct Info {
pub map: String,
@ -49,6 +67,30 @@ pub struct Info {
pub uploader: Uploader,
}
impl Info {
pub fn map_type(&self) -> MapType {
if map_is_stopwatch(&self.map) {
MapType::Stopwatch
} else if self.map.starts_with("cp") {
MapType::Cp
} else if self.map.starts_with("koth") {
MapType::KOTH
} else if self.map.starts_with("ctf") {
MapType::CTF
} else if self.map.starts_with("ultiduo") {
MapType::UltiDuo
} else if self.map.starts_with("bball") {
MapType::BBall
} else {
MapType::Other
}
}
pub fn date(&self) -> DateTime<Utc> {
DateTime::from_utc(NaiveDateTime::from_timestamp(self.date as i64, 0), Utc)
}
}
#[derive(Debug, Clone)]
pub struct Round {
pub start_time: u64,

View file

@ -1,4 +1,4 @@
use crate::data::{Class, Medigun, TeamId, Weapon};
use crate::data::{Class, Medigun, TeamId};
use serde::export::TryFrom;
use serde::Deserialize;
use std::collections::HashMap;
@ -54,6 +54,7 @@ pub struct Team {
#[derive(Debug, Clone, Deserialize)]
pub struct Player {
pub class_stats: Vec<ClassStat>,
pub team: TeamId,
pub kills: u16,
pub deaths: u16,
@ -106,20 +107,64 @@ pub struct ClassStat {
#[serde(rename = "type")]
pub class: Class,
pub kills: u16,
pub assists: u16,
pub deaths: u16,
pub dmg: u32,
pub total_time: u32,
pub weapon: HashMap<Weapon, WeaponStat>,
#[serde(default)]
pub weapon: HashMap<String, WeaponStat>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum RawWeaponStats {
Kills(u32),
Stats {
kills: u32,
dmg: u32,
avg_dmg: f32,
shots: u32,
hits: u32,
},
}
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawWeaponStats")]
pub struct WeaponStat {
pub kills: u32,
pub dmg: u32,
pub avg_dmg: u32,
pub avg_dmg: f32,
pub shots: u32,
pub hits: u32,
}
impl From<RawWeaponStats> for WeaponStat {
fn from(raw: RawWeaponStats) -> Self {
match raw {
RawWeaponStats::Kills(kills) => WeaponStat {
kills,
dmg: 0,
avg_dmg: 0.0,
shots: 0,
hits: 0,
},
RawWeaponStats::Stats {
kills,
dmg,
avg_dmg,
shots,
hits,
} => WeaponStat {
kills,
dmg,
avg_dmg,
shots,
hits,
},
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Round {
#[serde(default)]
@ -187,7 +232,7 @@ impl Event {
}
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ClassNumbers {
#[serde(default)]
pub scout: u8,