This commit is contained in:
41666 2023-06-09 04:42:23 -04:00
commit 83ad349f30
16 changed files with 3428 additions and 0 deletions

23
.github/metagame/ci.yaml vendored Normal file
View file

@ -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 }}/metagame:latest"; else echo ""; fi)
docker buildx build . \
-t ghcr.io/${{ github.repository }}/metagame:${{ github.sha }} $TAG_LATEST_IF_MASTER \
--push \
--cache-to type=gha,scope=$GITHUB_REF_NAME-metagame \
--cache-from type=gha,scope=$GITHUB_REF_NAME-metagame

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1695
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

21
Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "metagame"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1.28", features = ["full"] }
axum = { version = "0.6", features = ["json", "macros"] }
reqwest = { version = "0.11", features = ["json"] }
openssl = { version = "0.10", features = ["vendored"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sled = { version = "0.34" }
tower-http = { version = "0.4", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
chrono = { version = "0.4", features = ["serde"] }
lazy_static = "1.4"
serde-aux = "4"

21
Dockerfile Normal file
View file

@ -0,0 +1,21 @@
FROM rust:1.69.0-bullseye as rust-base
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl clang
ARG MOLD_VERSION=1.11.0
RUN curl -sSL https://github.com/rui314/mold/releases/download/v${MOLD_VERSION}/mold-${MOLD_VERSION}-x86_64-linux.tar.gz | tar xzv && \
mv mold-${MOLD_VERSION}-x86_64-linux/bin/mold /mold && \
rm -rf mold-${MOLD_VERSION}-x86_64-linux
ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=clang
ENV RUSTFLAGS="-C link-arg=-fuse-ld=/mold"
FROM rust-base as builder
COPY . .
ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=clang
ENV RUSTFLAGS="-C link-arg=-fuse-ld=/mold"
RUN cargo build --release --bin metagame
FROM debian:bullseye-slim as runtime
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/metagame /app
ENTRYPOINT ["/app"]

1
hack/metagame-gen/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1247
hack/metagame-gen/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
[package]
name = "metagame-gen"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde-aux = { version = "4" }
tokio = { version = "1", features = ["full"] }

View file

@ -0,0 +1,43 @@
use serde::Deserialize;
use serde_aux::prelude::*;
#[derive(Deserialize)]
struct MetagameEventResponse {
metagame_event_list: Vec<MetagameEvent>,
}
#[derive(Deserialize)]
struct MetagameEvent {
metagame_event_id: String,
#[serde(rename = "type", deserialize_with = "deserialize_number_from_string")]
event_type: i32,
}
#[tokio::main]
async fn main() {
let response = reqwest::get(
"https://census.daybreakgames.com/s:ps2livepublic/get/ps2/metagame_event?c:limit=1000",
)
.await
.unwrap();
let metagame_events: MetagameEventResponse = response.json().await.unwrap();
let template = format!("// GENERATED CODE. DO NOT EDIT MANUALLY. Run `cd hack/metagame-gen; cargo run` to generate.
pub fn alert_type(metagame_event_id: i32) -> String {{
match metagame_event_id {{
{} => \"air\".to_string(),
{} => \"sudden_death\".to_string(),
{} | _ => \"conquest\".to_string(),
}}
}}",
metagame_events.metagame_event_list.iter().filter(|e| e.event_type == 10).map(|e| e.metagame_event_id.clone()).collect::<Vec<String>>().join(" | "),
metagame_events.metagame_event_list.iter().filter(|e| e.event_type == 6).map(|e| e.metagame_event_id.clone()).collect::<Vec<String>>().join(" | "),
metagame_events.metagame_event_list.iter().filter(|e| e.event_type == 9).map(|e| e.metagame_event_id.clone()).collect::<Vec<String>>().join(" | "),
);
std::fs::write("../../src/alert_types.rs", template).unwrap();
println!("Generated alert_types.rs");
}

16
src/alert_types.rs Normal file
View file

@ -0,0 +1,16 @@
// GENERATED CODE. DO NOT EDIT MANUALLY. Run `cd hack/metagame-gen; cargo run` to generate.
pub fn alert_type(metagame_event_id: i32) -> String {
match metagame_event_id {
167 | 168 | 172 | 173 | 174 | 175 | 194 | 195 | 196 | 197 | 204 | 206 | 207 | 216 | 217
| 218 | 219 | 220 | 221 | 225 | 228 | 229 | 230 | 231 | 232 | 235 => "air".to_string(),
106 | 198 | 199 | 200 | 201 | 233 | 236 | 237 | 238 | 239 | 240 | 241 => {
"sudden_death".to_string()
}
147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 176 | 177 | 178
| 179 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 208 | 209 | 210 | 211 | 212
| 213 | 214 | 215 | 222 | 223 | 224 | 226 | 227 | 248 | 249 | 250 | _ => {
"conquest".to_string()
}
}
}

84
src/alerts.rs Normal file
View file

@ -0,0 +1,84 @@
use crate::{alert_types::alert_type, misc};
use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use serde_aux::prelude::*;
use std::collections::HashMap;
#[derive(Serialize)]
pub struct Alert {
pub id: i32,
pub zone: i32,
pub end_time: Option<DateTime<Utc>>,
pub start_time: Option<DateTime<Utc>>,
pub alert_type: String,
pub ps2alerts: String,
}
pub async fn get_alerts(world_id: i32) -> Result<Vec<Alert>, ()> {
let response = reqwest::get(format!(
"https://census.daybreakgames.com/s:{}/get/{}/world_event/?world_id={}&type=METAGAME&c:limit=10",
misc::service_id(),
misc::platform(world_id),
world_id
))
.await
.unwrap();
let world_events: WorldEventResponse = match response.json().await {
Ok(world_events) => world_events,
Err(_) => return Err(()),
};
let mut alerts: HashMap<i32, Alert> = HashMap::new();
for world_event in world_events.world_event_list {
let alert = alerts.entry(world_event.id).or_insert(Alert {
id: world_event.metagame_event_id,
zone: world_event.zone_id,
end_time: None,
start_time: None,
alert_type: alert_type(world_event.metagame_event_id),
ps2alerts: format!(
"https://ps2alerts.com/alert/{}-{}",
world_id, world_event.id
),
});
if world_event.metagame_event_state_name == "started" {
alert.start_time = Utc.timestamp_opt(world_event.timestamp as i64, 0).single();
} else if world_event.metagame_event_state_name == "ended" {
alert.end_time = Utc.timestamp_opt(world_event.timestamp as i64, 0).single();
}
}
let mut active_alerts: Vec<Alert> = vec![];
for (_, alert) in alerts {
if alert.end_time.is_none() {
active_alerts.push(alert);
}
}
Ok(active_alerts)
}
#[derive(Deserialize)]
struct WorldEventResponse {
world_event_list: Vec<WorldEvent>,
}
#[derive(Deserialize)]
struct WorldEvent {
#[serde(
rename = "instance_id",
deserialize_with = "deserialize_number_from_string"
)]
id: i32,
#[serde(deserialize_with = "deserialize_number_from_string")]
metagame_event_id: i32,
metagame_event_state_name: String,
#[serde(deserialize_with = "deserialize_number_from_string")]
timestamp: i64,
#[serde(deserialize_with = "deserialize_number_from_string")]
zone_id: i32,
}

51
src/html/index.html Normal file
View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<meta charset="utf-8" />
<title>Aggregate PlanetSide 2 Population API</title>
<meta name="description" content="Multi-source population API" />
<style>
body {
font-family: sans-serif;
margin: 0;
padding: 0;
background-color: black;
color: white;
}
main {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
flex-direction: column;
font-weight: lighter;
text-align: center;
}
.big-header {
font-size: 2rem;
}
.api-list {
margin-top: 2rem;
}
.api-item {
display: block;
background-color: #334;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
margin: 0.5rem;
text-decoration: none;
color: white;
font-weight: bold;
}
</style>
<main>
<div class="big-header">
low fuss metagame API. get current continents open and alerts.
</div>
<div class="api-list">
<a class="api-item" href="/1">GET /{worldID} ▶️</a>
<a class="api-item" href="/all">GET /all ▶️</a>
</div>
<p>Results are cached for 3 minutes. Requesting faster is a dumb idea.</p>
</main>

47
src/main.rs Normal file
View file

@ -0,0 +1,47 @@
#![feature(slice_group_by)]
use axum::{extract::Path, response::Html, routing::get, Json, Router};
use std::{env, net::SocketAddr};
use types::{FactionPercents, World, Zone};
use zones::get_zone_states;
mod alert_types;
mod alerts;
mod misc;
mod types;
mod zones;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.layer(tower_http::trace::TraceLayer::new_for_http())
.route("/:world", get(get_world))
.route("/", get(root));
let addr = SocketAddr::from((
[127, 0, 0, 1],
env::var("PORT")
.unwrap_or("8076".to_string())
.parse()
.unwrap(),
));
tracing::debug!("listening on http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn root() -> Html<&'static str> {
Html(include_str!("./html/index.html"))
}
#[axum::debug_handler]
pub async fn get_world(Path(world): Path<i32>) -> Json<World> {
Json(World {
id: world,
zones: get_zone_states(world).await.unwrap(),
})
}

18
src/misc.rs Normal file
View file

@ -0,0 +1,18 @@
use lazy_static::lazy_static;
use std::env;
lazy_static! {
pub static ref SERVICE_ID: String = env::var("SERVICE_ID").unwrap();
}
pub fn service_id() -> String {
SERVICE_ID.clone()
}
pub fn platform(world_id: i32) -> String {
match world_id {
1000 => "ps2ps4us".to_string(),
2000 => "ps2ps4eu".to_string(),
_ => "ps2".to_string(),
}
}

33
src/types.rs Normal file
View file

@ -0,0 +1,33 @@
use serde::Serialize;
#[derive(Serialize)]
pub struct World {
pub id: i32,
pub zones: Vec<Zone>,
}
#[derive(Serialize)]
pub struct Zone {
pub id: i32,
pub locked: bool,
pub alert: Option<Alert>,
pub faction_control: FactionPercents,
}
#[derive(Serialize)]
pub struct Alert {
pub id: i32,
#[serde(rename = "type")]
pub alert_type: String,
pub start: i64,
pub end: i64,
pub score: FactionPercents,
pub ps2alerts: String,
}
#[derive(Serialize)]
pub struct FactionPercents {
pub vs: f32,
pub nc: f32,
pub tr: f32,
}

115
src/zones.rs Normal file
View file

@ -0,0 +1,115 @@
use crate::{
misc,
types::{FactionPercents, Zone},
};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use serde_aux::prelude::*;
use std::collections::HashMap;
lazy_static! {
pub static ref ZONE_REGIONS: HashMap<i32, Vec<i32>> = HashMap::from([
(2, vec![2201, 2202, 2203]),
(4, vec![4230, 4240, 4250]),
(6, vec![6001, 6002, 6003]),
(8, vec![18029, 18030, 18062]),
(344, vec![18303, 18304, 18305]),
]);
}
pub async fn get_zone_states(world_id: i32) -> Result<Vec<Zone>, ()> {
let response = reqwest::get(format!(
"https://census.daybreakgames.com/s:{}/get/{}/map/?world_id={}&zone_ids=2,4,6,8,344",
misc::service_id(),
misc::platform(world_id),
world_id,
))
.await
.unwrap();
let map: MapResponse = match response.json().await {
Ok(map) => map,
Err(_) => return Err(()),
};
let mut zones: Vec<Zone> = Vec::new();
for map_zone in map.map_list {
let warpgate_zone_filter = ZONE_REGIONS.get(&map_zone.zone_id).unwrap();
let warpgate_factions = map_zone
.regions
.row
.iter()
.filter(|r| warpgate_zone_filter.contains(&r.row_data.region_id))
.map(|r| r.row_data.faction_id)
.collect::<Vec<i32>>();
let zone = Zone {
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),
alert: None,
};
zones.push(zone);
}
Ok(zones)
}
fn calculate_faction_percents(regions: &Vec<MapRegionRowData>) -> FactionPercents {
let groups = regions.group_by(|a, b| a.row_data.faction_id == b.row_data.faction_id);
let mut faction_percents = FactionPercents {
vs: 0.0,
nc: 0.0,
tr: 0.0,
};
for faction in groups {
faction.
}
faction_percents
}
#[derive(Deserialize)]
struct MapResponse {
map_list: Vec<MapZone>,
}
#[derive(Deserialize)]
struct MapZone {
#[serde(rename = "ZoneId", deserialize_with = "deserialize_number_from_string")]
zone_id: i32,
#[serde(rename = "Regions")]
regions: MapRegionRow,
}
#[derive(Deserialize)]
struct MapRegionRow {
#[serde(rename = "Row")]
row: Vec<MapRegionRowData>,
}
#[derive(Deserialize)]
struct MapRegionRowData {
#[serde(rename = "RowData")]
row_data: MapRegion,
}
#[derive(Deserialize)]
struct MapRegion {
#[serde(
rename = "RegionId",
deserialize_with = "deserialize_number_from_string"
)]
region_id: i32,
#[serde(
rename = "FactionId",
deserialize_with = "deserialize_number_from_string"
)]
faction_id: i32,
}