use std::sync::LazyLock;
use arenabuddy_core::{
cards::CardsDatabase,
models::{Deck, MTGAMatch, MTGAMatchBuilder, MatchResult, MatchResultBuilder, Mulligan},
replay::MatchReplay,
};
use chrono::{DateTime, Utc};
use include_dir::{Dir, include_dir};
use indoc::indoc;
use rusqlite::{Connection, Params as RusqliteParams, Result as RusqliteResult, Transaction};
use rusqlite_migration::Migrations;
use tracing::{debug, error, info};
use crate::{MatchDBError, Result, Storage};
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
static MIGRATIONS: LazyLock<Migrations<'static>> = LazyLock::new(|| {
Migrations::from_directory(&MIGRATIONS_DIR).unwrap_or(Migrations::new(Vec::new()))
});
#[derive(Debug)]
pub struct MatchDB {
pub conn: Connection,
pub cards_database: CardsDatabase,
}
impl MatchDB {
pub fn new(conn: Connection, cards_database: CardsDatabase) -> Self {
Self {
conn,
cards_database,
}
}
pub fn init(&mut self) -> Result<()> {
MIGRATIONS.to_latest(&mut self.conn)?;
Ok(())
}
pub fn execute(&mut self, query: &str, params: impl RusqliteParams) -> RusqliteResult<usize> {
self.conn.execute(query, params)
}
fn insert_match(mtga_match: &MTGAMatch, tx: &Transaction) -> Result<()> {
let params = (
mtga_match.id(),
mtga_match.controller_seat_id(),
mtga_match.controller_player_name(),
mtga_match.opponent_player_name(),
mtga_match.created_at(),
);
let sql = indoc! {r"INSERT INTO matches
(id, controller_seat_id, controller_player_name, opponent_player_name, created_at)
VALUES (?1, ?2, ?3, ?4, ?5) ON CONFLICT(id) DO NOTHING"};
tx.execute(sql, params)?;
Ok(())
}
fn insert_deck(match_id: &str, deck: &Deck, tx: &Transaction) -> Result<()> {
let deck_string = serde_json::to_string(deck.mainboard())?;
let sideboard_string = serde_json::to_string(deck.sideboard())?;
tx.execute(indoc! {r"INSERT INTO decks
(match_id, game_number, deck_cards, sideboard_cards)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT (match_id, game_number)
DO UPDATE SET deck_cards = excluded.deck_cards, sideboard_cards = excluded.sideboard_cards"},
(match_id, deck.game_number(), deck_string, sideboard_string)
)?;
Ok(())
}
fn insert_mulligan_info(mulligan_info: &Mulligan, tx: &Transaction) -> Result<()> {
tx.execute(
indoc!{r"INSERT INTO mulligans (match_id, game_number, number_to_keep, hand, play_draw, opponent_identity, decision)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
ON CONFLICT (match_id, game_number, number_to_keep)
DO UPDATE SET hand = excluded.hand, play_draw = excluded.play_draw, opponent_identity = excluded.opponent_identity, decision = excluded.decision"},
(
mulligan_info.match_id(),
mulligan_info.game_number(),
mulligan_info.number_to_keep(),
mulligan_info.hand(),
mulligan_info.play_draw(),
mulligan_info.opponent_identity(),
mulligan_info.decision(),
),
)?;
Ok(())
}
fn insert_match_result(match_result: &MatchResult, tx: &Transaction) -> Result<()> {
let params = (
match_result.match_id(),
match_result.game_number(),
match_result.winning_team_id(),
match_result.result_scope(),
);
let sql = indoc! {r"INSERT INTO match_results (match_id, game_number, winning_team_id, result_scope)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT (match_id, game_number)
DO UPDATE SET winning_team_id = excluded.winning_team_id, result_scope = excluded.result_scope"};
tx.execute(sql, params)?;
Ok(())
}
pub fn get_match_results(&mut self, match_id: &str) -> Result<Vec<MatchResult>> {
let mut stmt = self
.conn
.prepare("SELECT game_number, winning_team_id, result_scope FROM match_results WHERE match_id = ?1 AND game_number > 0")?;
let results = stmt
.query_map([match_id], |row| {
let game_number: i32 = row.get(0)?;
let winning_team_id: i32 = row.get(1)?;
let result_scope: String = row.get(2)?;
Ok(MatchResult::new(
match_id.to_string(),
game_number,
winning_team_id,
result_scope,
))
})?
.collect::<rusqlite::Result<Vec<MatchResult>>>()?;
Ok(results)
}
pub fn get_decklists(&mut self, match_id: &str) -> Result<Vec<Deck>> {
let mut stmt = self.conn.prepare(
"SELECT game_number, deck_cards, sideboard_cards FROM decks WHERE match_id = ?1",
)?;
let deck = stmt
.query_map([match_id], |row| {
let game_number: i32 = row.get(0)?;
let deck_cards: String = row.get(1)?;
let sideboard_cards: String = row.get(2)?;
Ok(Deck::from_raw(
"Found Deck".to_string(),
game_number,
&deck_cards,
&sideboard_cards,
))
})?
.collect::<rusqlite::Result<Vec<Deck>>>()?;
Ok(deck)
}
pub fn get_mulligans(&mut self, match_id: &str) -> Result<Vec<Mulligan>> {
let mut stmt = self
.conn
.prepare("SELECT game_number, number_to_keep, hand, play_draw, opponent_identity, decision FROM mulligans WHERE match_id = ?1")?;
let mulligans = stmt
.query_map([match_id], |row| {
let game_number: i32 = row.get(0)?;
let number_to_keep: i32 = row.get(1)?;
let hand: String = row.get(2)?;
let play_draw: String = row.get(3)?;
let opponent_identity: String = row.get(4)?;
let decision: String = row.get(5)?;
Ok(Mulligan::new(
match_id.to_string(),
game_number,
number_to_keep,
hand,
play_draw,
opponent_identity,
decision,
))
})?
.collect::<rusqlite::Result<Vec<Mulligan>>>()?;
Ok(mulligans)
}
pub fn get_matches(&mut self) -> Result<Vec<MTGAMatch>> {
let mut statement = self.conn.prepare("SELECT id, controller_seat_id, controller_player_name, opponent_player_name, created_at FROM matches")?;
let matches = statement
.query_map([], |row| {
let id: String = row.get(0)?;
let controller_seat_id: i32 = row.get(1)?;
let controller_player_name: String = row.get(2)?;
let opponent_player_name: String = row.get(3)?;
let created_at: Option<DateTime<Utc>> = row.get(4)?;
Ok(MTGAMatch::new_with_timestamp(
id,
controller_seat_id,
controller_player_name,
opponent_player_name,
created_at.unwrap_or_default(),
))
})?
.collect::<RusqliteResult<Vec<MTGAMatch>>>()?;
Ok(matches)
}
pub fn get_match(&mut self, match_id: &str) -> Result<(MTGAMatch, Option<MatchResult>)> {
let mut statement = self.conn.prepare(indoc! {r#"
SELECT
m.id, m.controller_player_name, m.opponent_player_name, mr.winning_team_id, m.controller_seat_id, m.created_at
FROM matches m JOIN match_results mr ON m.id = mr.match_id
WHERE m.id = ?1 AND mr.result_scope = "MatchScope_Match" LIMIT 1
"#}
)?;
info!("Getting match details for match_id: {}", match_id);
Ok(statement
.query_row([&match_id], |row| {
let id: String = row.get(0)?;
let controller_player_name: String = row.get(1)?;
let opponent_player_name: String = row.get(2)?;
let winning_team_id: i32 = row.get(3)?;
let controller_seat_id: i32 = row.get(4)?;
let created_at: DateTime<Utc> = row.get(5)?;
Ok((
MTGAMatch::new_with_timestamp(
id.clone(),
controller_seat_id,
controller_player_name,
opponent_player_name,
created_at,
),
Some(MatchResult::new_match_result(id, winning_team_id)),
))
})
.unwrap_or_else(|e| {
error!("Error getting match details: {:?}", e);
(MTGAMatch::default(), None)
}))
}
}
impl Storage for MatchDB {
fn write(&mut self, match_replay: &MatchReplay) -> crate::Result<()> {
info!("Writing match replay to database");
let controller_seat_id = match_replay.get_controller_seat_id();
let match_id = &match_replay.match_id;
let (controller_name, opponent_name) = match_replay.get_player_names(controller_seat_id)?;
let event_start = match_replay.match_start_time().unwrap_or(Utc::now());
let mtga_match = MTGAMatchBuilder::default()
.id(match_id.to_string())
.controller_seat_id(controller_seat_id)
.controller_player_name(controller_name)
.opponent_player_name(opponent_name)
.created_at(event_start)
.build()?;
let tx = self.conn.transaction().map_err(MatchDBError::from)?;
Self::insert_match(&mtga_match, &tx)?;
match_replay
.get_decklists()?
.iter()
.try_for_each(|deck| Self::insert_deck(&match_replay.match_id, deck, &tx))?;
let mulligan_infos = match_replay.get_mulligan_infos(&self.cards_database)?;
mulligan_infos
.iter()
.try_for_each(|mulligan_info| Self::insert_mulligan_info(mulligan_info, &tx))?;
let match_results = match_replay.get_match_results()?;
debug!("{:?}", match_results);
for (i, result) in match_results.result_list.iter().enumerate() {
let game_number = if result.scope == "MatchScope_Game" {
i32::try_from(i + 1).unwrap_or(0)
} else {
0
};
let match_result = MatchResultBuilder::default()
.match_id(match_id.to_string())
.game_number(game_number)
.winning_team_id(result.winning_team_id)
.result_scope(result.scope.clone())
.build()?;
Self::insert_match_result(&match_result, &tx)?;
}
tx.commit().map_err(MatchDBError::from)?;
Ok(())
}
}