diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1d74ece --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +target +.git \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..1dedcaa --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,23 @@ +name: "CI" + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - run: docker buildx create --use --driver=docker-container + - run: | + TAG_LATEST_IF_MASTER=$(if [ "$GITHUB_REF_NAME" = "main" ]; then echo "-t ghcr.io/${{ github.repository }}/agg-population:latest"; else echo ""; fi) + docker buildx build . \ + -t ghcr.io/${{ github.repository }}/agg-population:${{ github.sha }} $TAG_LATEST_IF_MASTER \ + --push \ + --cache-to type=gha,scope=$GITHUB_REF_NAME-agg-population \ + --cache-from type=gha,scope=$GITHUB_REF_NAME-agg-population diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..78218b7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.tabSize": 4 +} diff --git a/Cargo.lock b/Cargo.lock index ce4bce8..aaca994 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,6 +102,15 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -604,6 +613,7 @@ name = "metagame" version = "0.1.0" dependencies = [ "axum", + "bincode", "chrono", "lazy_static", "openssl", diff --git a/Cargo.toml b/Cargo.toml index 5f39c28..9536454 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,5 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } chrono = { version = "0.4", features = ["serde"] } lazy_static = "1.4" -serde-aux = "4" \ No newline at end of file +serde-aux = "4" +bincode = "1.3" diff --git a/nomad/metagame.nomad.hcl b/nomad/metagame.nomad.hcl new file mode 100644 index 0000000..3135421 --- /dev/null +++ b/nomad/metagame.nomad.hcl @@ -0,0 +1,27 @@ +job "metagame" { + type = "service" + + update { + max_parallel = 1 + stagger = "10s" + } + + group "api" { + count = 1 + + network { + port "http" { + static = 8067 + } + } + + task "api" { + driver = "docker" + + config { + image = "ghcr.io/genudine/metagame/metagame:latest" + ports = ["http"] + } + } + } +} \ No newline at end of file diff --git a/src/alerts.rs b/src/alerts.rs index 6273da5..7c42bb8 100644 --- a/src/alerts.rs +++ b/src/alerts.rs @@ -1,19 +1,13 @@ -use crate::{alert_types::alert_type, misc}; -use chrono::{DateTime, TimeZone, Utc}; -use serde::{Deserialize, Serialize}; +use crate::{ + alert_types::alert_type, + misc, + types::{Alert, FactionPercents}, +}; +use chrono::{TimeZone, Utc}; +use serde::Deserialize; use serde_aux::prelude::*; use std::collections::HashMap; -#[derive(Serialize)] -pub struct Alert { - pub id: i32, - pub zone: i32, - pub end_time: Option>, - pub start_time: Option>, - pub alert_type: String, - pub ps2alerts: String, -} - pub async fn get_alerts(world_id: i32) -> Result, ()> { let response = reqwest::get(format!( "https://census.daybreakgames.com/s:{}/get/{}/world_event/?world_id={}&type=METAGAME&c:limit=10", @@ -42,6 +36,11 @@ pub async fn get_alerts(world_id: i32) -> Result, ()> { "https://ps2alerts.com/alert/{}-{}", world_id, world_event.id ), + percentages: FactionPercents { + nc: world_event.faction_nc, + tr: world_event.faction_tr, + vs: world_event.faction_vs, + }, }); if world_event.metagame_event_state_name == "started" { @@ -81,4 +80,10 @@ struct WorldEvent { timestamp: i64, #[serde(deserialize_with = "deserialize_number_from_string")] zone_id: i32, + #[serde(deserialize_with = "deserialize_number_from_string")] + faction_nc: f32, + #[serde(deserialize_with = "deserialize_number_from_string")] + faction_tr: f32, + #[serde(deserialize_with = "deserialize_number_from_string")] + faction_vs: f32, } diff --git a/src/main.rs b/src/main.rs index 5eccd7a..7ac1fdf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,16 @@ #![feature(slice_group_by)] -use axum::{extract::Path, response::Html, routing::get, Json, Router}; +use alerts::get_alerts; +use axum::{ + extract::{Path, State}, + response::Html, + routing::get, + Json, Router, +}; use std::{env, net::SocketAddr}; -use types::{FactionPercents, World, Zone}; +use tokio::task::JoinSet; +use tracing::Level; +use types::{World, Zone}; use zones::get_zone_states; mod alert_types; @@ -13,12 +21,16 @@ mod zones; #[tokio::main] async fn main() { - tracing_subscriber::fmt::init(); + tracing_subscriber::fmt() + .with_max_level(Level::DEBUG) + .init(); let app = Router::new() .layer(tower_http::trace::TraceLayer::new_for_http()) - .route("/:world", get(get_world)) - .route("/", get(root)); + .route("/:world", get(get_one_world)) + // .route("/all", get(get_all_worlds)) + .route("/", get(root)) + .with_state(sled::open("/tmp/metagame").expect("open")); let addr = SocketAddr::from(( [127, 0, 0, 1], @@ -38,10 +50,79 @@ async fn root() -> Html<&'static str> { Html(include_str!("./html/index.html")) } -#[axum::debug_handler] -pub async fn get_world(Path(world): Path) -> Json { - Json(World { - id: world, - zones: get_zone_states(world).await.unwrap(), - }) +pub async fn get_one_world(State(db): State, Path(world): Path) -> Json { + Json(get_world(db, world).await) +} + +pub async fn get_all_worlds(State(db): State) -> Json> { + let mut set = JoinSet::new(); + let mut worlds = vec![World::default(); 8]; + + for world in vec![1, 10, 13, 17, 19, 40, 1000, 2000] { + set.spawn(get_world(db.clone(), world)); + } + + let mut i = 0; + while let Some(response) = set.join_next().await { + worlds[i] = response.unwrap_or_default(); + i += 1; + } + + Json(worlds) +} + +pub async fn get_world(db: sled::Db, world: i32) -> World { + match world_from_cache(db.clone(), world) { + Ok(response) => return response, + _ => {} + } + + let alerts = get_alerts(world).await.unwrap(); + let zones = get_zone_states(world).await.unwrap(); + + let converged_zones: Vec = zones + .into_iter() + .map(|zone| { + let mut zone = zone; + let alert = alerts.iter().find(|alert| alert.zone == zone.id); + + zone.alert = alert.cloned(); + + zone + }) + .collect(); + + let response = World { + id: world, + zones: converged_zones, + cached_at: chrono::Utc::now(), + }; + + world_to_cache(db, world, &response); + + response +} + +fn world_from_cache(db: sled::Db, world: i32) -> Result { + let key = format!("world:{}", world); + let value = match db.get(key) { + Ok(Some(value)) => value, + _ => return Err(()), + }; + + match bincode::deserialize::(&value) { + Ok(response) => { + if response.cached_at + chrono::Duration::minutes(3) < chrono::Utc::now() { + return Err(()); + } + Ok(response) + } + _ => Err(()), + } +} + +fn world_to_cache(db: sled::Db, world: i32, response: &World) { + let key = format!("world:{}", world); + let value = bincode::serialize(response).unwrap(); + db.insert(key, value).unwrap(); } diff --git a/src/types.rs b/src/types.rs index d82c062..28e3545 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,31 +1,33 @@ -use serde::Serialize; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; -#[derive(Serialize)] +#[derive(Deserialize, Serialize, Clone, Default)] pub struct World { pub id: i32, pub zones: Vec, + pub cached_at: DateTime, } -#[derive(Serialize)] +#[derive(Deserialize, Serialize, Clone, Default)] pub struct Zone { pub id: i32, pub locked: bool, pub alert: Option, - pub faction_control: FactionPercents, + pub territory: FactionPercents, } -#[derive(Serialize)] +#[derive(Deserialize, Serialize, Clone, Default)] pub struct Alert { pub id: i32, - #[serde(rename = "type")] + pub zone: i32, + pub end_time: Option>, + pub start_time: Option>, pub alert_type: String, - pub start: i64, - pub end: i64, - pub score: FactionPercents, pub ps2alerts: String, + pub percentages: FactionPercents, } -#[derive(Serialize)] +#[derive(Deserialize, Serialize, Clone, Default)] pub struct FactionPercents { pub vs: f32, pub nc: f32, diff --git a/src/zones.rs b/src/zones.rs index 18f8d31..fb658d2 100644 --- a/src/zones.rs +++ b/src/zones.rs @@ -3,7 +3,7 @@ use crate::{ types::{FactionPercents, Zone}, }; use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use serde_aux::prelude::*; use std::collections::HashMap; @@ -48,7 +48,7 @@ pub async fn get_zone_states(world_id: i32) -> Result, ()> { id: map_zone.zone_id, locked: warpgate_factions[0] == warpgate_factions[1] && warpgate_factions[1] == warpgate_factions[2], - faction_control: calculate_faction_percents(&map_zone.regions.row), + territory: calculate_faction_percents(&map_zone.regions.row), alert: None, }; @@ -61,17 +61,36 @@ pub async fn get_zone_states(world_id: i32) -> Result, ()> { fn calculate_faction_percents(regions: &Vec) -> FactionPercents { let groups = regions.group_by(|a, b| a.row_data.faction_id == b.row_data.faction_id); - let mut faction_percents = FactionPercents { + struct FactionTotals { + vs: f32, + nc: f32, + tr: f32, + } + + let mut faction_totals = FactionTotals { vs: 0.0, nc: 0.0, tr: 0.0, }; - for faction in groups { - faction. + for row in groups { + let faction_id = row[0].row_data.faction_id; + + match faction_id { + 1 => faction_totals.vs += 1.0, + 2 => faction_totals.nc += 1.0, + 3 => faction_totals.tr += 1.0, + _ => (), + } } - faction_percents + let total = faction_totals.vs + faction_totals.nc + faction_totals.tr; + + FactionPercents { + vs: faction_totals.vs / total * 100.0, + nc: faction_totals.nc / total * 100.0, + tr: faction_totals.tr / total * 100.0, + } } #[derive(Deserialize)]