init
This commit is contained in:
commit
83ad349f30
16 changed files with 3428 additions and 0 deletions
23
.github/metagame/ci.yaml
vendored
Normal file
23
.github/metagame/ci.yaml
vendored
Normal 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
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1695
Cargo.lock
generated
Normal file
1695
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal 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
21
Dockerfile
Normal 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
1
hack/metagame-gen/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1247
hack/metagame-gen/Cargo.lock
generated
Normal file
1247
hack/metagame-gen/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
12
hack/metagame-gen/Cargo.toml
Normal file
12
hack/metagame-gen/Cargo.toml
Normal 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"] }
|
43
hack/metagame-gen/src/main.rs
Normal file
43
hack/metagame-gen/src/main.rs
Normal 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
16
src/alert_types.rs
Normal 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
84
src/alerts.rs
Normal 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
51
src/html/index.html
Normal 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
47
src/main.rs
Normal 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
18
src/misc.rs
Normal 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
33
src/types.rs
Normal 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
115
src/zones.rs
Normal 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,
|
||||
}
|
Loading…
Add table
Reference in a new issue