release 1.0
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
/target
|
||||
/cache
|
||||
*.png
|
||||
.vscode
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Killmail",
|
||||
"killpaper"
|
||||
]
|
||||
}
|
||||
151
Cargo.lock
generated
151
Cargo.lock
generated
@@ -17,6 +17,12 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aliasable"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
||||
|
||||
[[package]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.4"
|
||||
@@ -87,6 +93,26 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
|
||||
dependencies = [
|
||||
"bincode_derive",
|
||||
"serde",
|
||||
"unty",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode_derive"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
|
||||
dependencies = [
|
||||
"virtue",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -256,6 +282,19 @@ dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-graphics-simulator"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31606a4fb7d9d3a79a38d27bc2954cfa98682c8fea4b22c09a442785a80424e"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"embedded-graphics",
|
||||
"image",
|
||||
"ouroboros",
|
||||
"sdl2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-hal"
|
||||
version = "1.0.0"
|
||||
@@ -459,6 +498,12 @@ version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.3.1"
|
||||
@@ -768,8 +813,10 @@ dependencies = [
|
||||
name = "killpaper"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"embedded-graphics",
|
||||
"embedded-graphics-core",
|
||||
"embedded-graphics-simulator",
|
||||
"embedded-hal",
|
||||
"gpiocdev-embedded-hal",
|
||||
"image",
|
||||
@@ -779,6 +826,12 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.175"
|
||||
@@ -948,6 +1001,30 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros"
|
||||
version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59"
|
||||
dependencies = [
|
||||
"aliasable",
|
||||
"ouroboros_macro",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros_macro"
|
||||
version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -1006,6 +1083,19 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2-diagnostics"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"version_check",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.6"
|
||||
@@ -1055,7 +1145,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1240,6 +1330,29 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sdl2"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"sdl2-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sdl2-sys"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.3.0"
|
||||
@@ -1386,6 +1499,12 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -1608,6 +1727,12 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "unty"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.7"
|
||||
@@ -1626,6 +1751,24 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "virtue"
|
||||
version = "0.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
@@ -1842,6 +1985,12 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.0"
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -14,10 +14,12 @@ linux-embedded-hal = "0.4.0"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["http2","rustls-tls-native-roots","json", "blocking", "gzip", "brotli"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.143"
|
||||
bincode = "2.0.1"
|
||||
embedded-graphics-simulator = "0.7.0"
|
||||
|
||||
|
||||
[features]
|
||||
# Remove the linux-dev feature to build the tests on non unix systems
|
||||
default = ["graphics"]
|
||||
graphics = []
|
||||
|
||||
default = ["simulator"] # use simulator by default
|
||||
simulator = []
|
||||
epaper = []
|
||||
paper-png = []
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
BIN
output.png
BIN
output.png
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB |
93
readme.md
Normal file
93
readme.md
Normal file
@@ -0,0 +1,93 @@
|
||||
|
||||
|
||||
# Killpaper
|
||||
|
||||
**Killpaper** is a Rust-based application for displaying EVE Online corporation kills and losses in real-time. It fetches killmails from zKillboard and EVE Online's ESI API, processes detailed kill information, and displays it on either a simulator or an e-paper display.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
* Fetches recent kills and losses for your corporation.
|
||||
* Aggregates detailed information about victims and attackers, including:
|
||||
|
||||
* Character
|
||||
* Corporation
|
||||
* Alliance
|
||||
* Ship
|
||||
* Supports multiple display options:
|
||||
|
||||
* Simulator (for testing and development)
|
||||
* PNG rendering (paper-png feature)
|
||||
* E-Paper display (hardware feature)
|
||||
* Real-time updates via Redis queue integration.
|
||||
* Easy to configure for any corporation.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
* Rust 1.70+
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://git.revwal.de/oliver/killpaper.git
|
||||
cd killpaper
|
||||
```
|
||||
|
||||
Build with Cargo:
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Run the application:
|
||||
|
||||
```bash
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
The display will be selected automatically based on compile-time features:
|
||||
|
||||
* Simulator (default): `--features simulator`
|
||||
* Paper PNG rendering: `--features paper-png`
|
||||
* E-Paper hardware: `--features epaper`
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
cargo run --release --features epaper
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
* Set your corporation ID in `main.rs`:
|
||||
|
||||
```rust
|
||||
let mut app = app::App::new(YOUR_CORP_ID, display);
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Crate Modules
|
||||
|
||||
* `app`: Main application logic for fetching, storing, and drawing kills.
|
||||
* `display`: Display abstraction and implementations (simulator, e-paper).
|
||||
* `killinfo`: Detailed kill information structures and builders.
|
||||
* `model`: Data models for Characters, Corporations, Alliances, Ships, and Killmails.
|
||||
* `services`: API clients for ESI, zKillboard, and Redis queue.
|
||||
* `cache`: Optional caching utilities for faster repeated queries.
|
||||
* `epd`: Optional e-paper display support (feature `epaper`).
|
||||
|
||||
213
src/app.rs
Normal file
213
src/app.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
//! # Main Application Module
|
||||
//!
|
||||
//! The [`App`] struct coordinates all core functionality of the killboard display system.
|
||||
//! It manages:
|
||||
//!
|
||||
//! - **Data sources**
|
||||
//! - [`EsiClient`](crate::services::EsiClient): Fetches detailed killmail info from the EVE ESI API.
|
||||
//! - [`ZkillClient`](crate::services::ZkillClient): Queries historical kill/loss data from zKillboard.
|
||||
//! - [`RedisQClient`](crate::services::RedisQClient): Streams live killmails in near real-time.
|
||||
//!
|
||||
//! - **Killmail processing**
|
||||
//! - [`KillInfoBuilder`](crate::killinfo::KillInfoBuilder) composes rich [`KillInfo`](crate::killinfo::KillInfo)
|
||||
//! objects from raw killmail requests.
|
||||
//!
|
||||
//! - **Display output**
|
||||
//! - Any type implementing the [`KillDisplay`](crate::display::KillDisplay) trait,
|
||||
//! such as [`EPaper`](crate::display::EPaper) or [`ImageSimulator`](crate::display::ImageSimulator).
|
||||
//!
|
||||
//! ## Lifecycle
|
||||
//! 1. Create an [`App`] with [`App::new`].
|
||||
//! 2. Call [`App::init`] to fetch initial kills and losses (past 24h).
|
||||
//! 3. Call [`App::draw`] to render the data on the display.
|
||||
//! 4. Periodically call [`App::pull_update`] to fetch live updates from RedisQ.
|
||||
//!
|
||||
//! ## Example
|
||||
//! ```no_run
|
||||
//! use crate::app::App;
|
||||
//! use crate::display::{KillDisplay, EPaper};
|
||||
//!
|
||||
//! let corp_id = 123456;
|
||||
//! let mut app = App::new(corp_id, EPaper::new());
|
||||
//!
|
||||
//! app.init(); // Load last 24h
|
||||
//! app.draw(); // Draw initial kills/losses
|
||||
//!
|
||||
//! loop {
|
||||
//! if app.pull_update() {
|
||||
//! app.draw(); // Redraw on update
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use crate::display::KillDisplay;
|
||||
use crate::killinfo;
|
||||
use crate::killinfo::KillInfo;
|
||||
use crate::model::killmail::KillmailRequest;
|
||||
use crate::services::{EsiClient, RedisQClient, Target, ZkillClient};
|
||||
|
||||
/// The main application struct tying together data sources, processing, and display.
|
||||
pub struct App<D> {
|
||||
esi: EsiClient,
|
||||
zkill: ZkillClient,
|
||||
redisq: RedisQClient,
|
||||
killbuilder: killinfo::KillInfoBuilder,
|
||||
display: D,
|
||||
corp_id: u32,
|
||||
kills: Vec<KillInfo>,
|
||||
losses: Vec<KillInfo>,
|
||||
}
|
||||
|
||||
/// Number of seconds in a day, used for initial historical queries.
|
||||
const SECONDS_A_DAY: u32 = 60 * 60 * 24;
|
||||
|
||||
impl<D> App<D>
|
||||
where
|
||||
D: KillDisplay,
|
||||
{
|
||||
/// Creates a new [`App`] instance.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `my_corp_id` - The corporation ID to track.
|
||||
/// * `display` - The display backend (e.g. [`EPaper`](crate::display::EPaper)).
|
||||
pub fn new(my_corp_id: u32, display: D) -> Self {
|
||||
let esi = EsiClient::new();
|
||||
let zkill = ZkillClient::new();
|
||||
let mut redisq = RedisQClient::new("todo_generate_random_number".to_string());
|
||||
let mut builder = killinfo::KillInfoBuilder::new();
|
||||
builder.set_corporation_id(my_corp_id);
|
||||
redisq.set_corporation_id(my_corp_id);
|
||||
|
||||
Self {
|
||||
esi,
|
||||
zkill,
|
||||
redisq,
|
||||
killbuilder: builder,
|
||||
display,
|
||||
corp_id: my_corp_id,
|
||||
kills: Vec::new(),
|
||||
losses: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches recent kill requests (past 24 hours) from zKillboard.
|
||||
fn get_initial_kill_requests(&mut self) -> Vec<KillmailRequest> {
|
||||
let response = self
|
||||
.zkill
|
||||
.get_corporation_kills(self.corp_id, SECONDS_A_DAY)
|
||||
.unwrap();
|
||||
|
||||
// Return at most the first 8 elements
|
||||
response.into_iter().take(8).collect()
|
||||
}
|
||||
|
||||
/// Fetches recent loss requests (past 24 hours) from zKillboard.
|
||||
fn get_initial_loss_requests(&mut self) -> Vec<KillmailRequest> {
|
||||
let response = self
|
||||
.zkill
|
||||
.get_corporation_losses(self.corp_id, SECONDS_A_DAY)
|
||||
.unwrap();
|
||||
|
||||
// Return at most the first 8 elements
|
||||
response.into_iter().take(8).collect()
|
||||
}
|
||||
|
||||
/// Resolves the initial set of kills into [`KillInfo`] objects.
|
||||
fn get_initial_kills(&mut self) -> Vec<KillInfo> {
|
||||
let requests = self.get_initial_kill_requests();
|
||||
let killmails = requests
|
||||
.iter()
|
||||
.map(|r| {
|
||||
self.killbuilder
|
||||
.make_kill_info(&self.zkill, &mut self.esi, r)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
killmails
|
||||
}
|
||||
|
||||
/// Resolves the initial set of losses into [`KillInfo`] objects.
|
||||
fn get_initial_losses(&mut self) -> Vec<KillInfo> {
|
||||
let requests = self.get_initial_loss_requests();
|
||||
let killmails = requests
|
||||
.iter()
|
||||
.map(|r| {
|
||||
self.killbuilder
|
||||
.make_kill_info(&self.zkill, &mut self.esi, r)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
killmails
|
||||
}
|
||||
|
||||
/// Initializes the app by fetching and building the initial kills and losses.
|
||||
pub fn init(&mut self) {
|
||||
self.kills = self.get_initial_kills();
|
||||
self.losses = self.get_initial_losses();
|
||||
}
|
||||
|
||||
/// Draws the current kills and losses on the display.
|
||||
///
|
||||
/// - Kills are drawn in a vertical list starting at `x = 0`.
|
||||
/// - Losses are drawn in a vertical list starting at `x = 400`.
|
||||
pub fn draw(&mut self) {
|
||||
self.display.clear(true);
|
||||
|
||||
for (i, k) in self.kills.iter().enumerate() {
|
||||
let ii = i as i32;
|
||||
let y: i32 = ii * 60;
|
||||
|
||||
self.display.draw_kill_info(k, 0, y);
|
||||
}
|
||||
|
||||
for (i, k) in self.losses.iter().enumerate() {
|
||||
let ii = i as i32;
|
||||
let y: i32 = ii * 60;
|
||||
|
||||
self.display.draw_loss_info(k, 400, y);
|
||||
}
|
||||
|
||||
self.display.flush();
|
||||
}
|
||||
|
||||
/// Pulls the next live update from RedisQ and integrates it into the kill/loss list.
|
||||
///
|
||||
/// - If it’s a kill, it is prepended to the kills list.
|
||||
/// - If it’s a loss, it is prepended to the losses list.
|
||||
/// - Each list is capped at 8 entries.
|
||||
///
|
||||
/// Returns `true` if an update was applied, `false` otherwise.
|
||||
pub fn pull_update(&mut self) -> bool {
|
||||
self.display.update();
|
||||
|
||||
if let Some((killrequest, target)) = self.redisq.next() {
|
||||
let killmail = self
|
||||
.killbuilder
|
||||
.make_kill_info(&self.zkill, &mut self.esi, &killrequest)
|
||||
.unwrap();
|
||||
|
||||
match target {
|
||||
Target::Kill => {
|
||||
self.kills.insert(0, killmail); // push to front
|
||||
if self.kills.len() > 8 {
|
||||
// limit length
|
||||
self.kills.pop(); // remove last
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Target::Loss => {
|
||||
self.losses.insert(0, killmail); // push to front
|
||||
if self.losses.len() > 8 {
|
||||
// limit length
|
||||
self.losses.pop(); // remove last
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
84
src/cache/image.rs
vendored
Normal file
84
src/cache/image.rs
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
use image::{ImageError, ImageFormat, RgbImage};
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
///
|
||||
/// A cache for storing and retrieving images by a unique ID.
|
||||
/// Images are stored in a folder
|
||||
///
|
||||
pub struct ImageCache {
|
||||
path: String,
|
||||
collection: BTreeSet<u32>,
|
||||
}
|
||||
|
||||
impl ImageCache {
|
||||
/// Creates a new image cache from the specified directory path.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `path` - The path to the directory where images are stored.
|
||||
pub fn new(path: String) -> Self {
|
||||
let mut collection = BTreeSet::new();
|
||||
let dir_path = Path::new(&path);
|
||||
|
||||
if let Ok(entries) = fs::read_dir(dir_path) {
|
||||
for entry in entries.flatten() {
|
||||
if let Ok(file_type) = entry.file_type() {
|
||||
if file_type.is_file() {
|
||||
if let Some(file_name) = entry.file_name().to_str() {
|
||||
// check if it ends with ".png"
|
||||
if let Some(stem) = file_name.strip_suffix(".png") {
|
||||
// try parse stem into u32
|
||||
if let Ok(id) = stem.parse::<u32>() {
|
||||
collection.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Self { path, collection }
|
||||
}
|
||||
|
||||
/// Constructs the file path for a given image ID.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - The unique identifier of the image.
|
||||
fn file_path(&self, id: u32) -> PathBuf {
|
||||
Path::new(&self.path).join(format!("{:05}.png", id))
|
||||
}
|
||||
|
||||
/// Loads an image by its ID if it exists in the cache.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - The unique identifier of the image to look up.
|
||||
pub fn lookup(&self, id: u32) -> Option<RgbImage> {
|
||||
if !self.collection.contains(&id) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let file_path = self.file_path(id);
|
||||
match image::open(file_path) {
|
||||
Ok(img) => Some(img.to_rgb8()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts an image into the cache and saves it as `{id:05}.png`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - The unique identifier for the image.
|
||||
/// * `image` - A reference to the image to be inserted.
|
||||
pub fn insert(&mut self, id: u32, image: &RgbImage) -> Result<(), ImageError> {
|
||||
let file_path = self.file_path(id);
|
||||
|
||||
// Save the image
|
||||
image.save_with_format(&file_path, ImageFormat::Png)?;
|
||||
|
||||
// Add to collection
|
||||
self.collection.insert(id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
36
src/cache/mod.rs
vendored
Normal file
36
src/cache/mod.rs
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub mod image;
|
||||
|
||||
///
|
||||
/// A simple cache that stores items with a unique ID.
|
||||
///
|
||||
pub struct SimpleCache<T: Clone> {
|
||||
collection: BTreeMap<u32, T>,
|
||||
}
|
||||
|
||||
impl<T: Clone> SimpleCache<T> {
|
||||
/// Create new Empty Cache
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
collection: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a value into the cache with the given ID.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - The unique identifier for the value.
|
||||
/// * `value` - A reference to the value to be inserted.
|
||||
pub fn insert(&mut self, id: u32, value: &T) {
|
||||
self.collection.insert(id, value.clone());
|
||||
}
|
||||
|
||||
/// Looks up a value by its ID and returns it if found.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - The unique identifier of the value to look up.
|
||||
pub fn lookup(&self, id: u32) -> Option<T> {
|
||||
self.collection.get(&id).cloned()
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,11 @@ use embedded_graphics::{
|
||||
text::{Baseline, Text, TextStyleBuilder},
|
||||
};
|
||||
|
||||
use crate::epd::color::Color;
|
||||
use crate::epd::epd7in5_v2::Display7in5;
|
||||
use crate::epd::epd7in5_v2::Epd7in5;
|
||||
use crate::epd::graphics::Display;
|
||||
use crate::epd::traits::*;
|
||||
use crate::{display::KillDisplay, epd::color::Color};
|
||||
|
||||
use linux_embedded_hal::{
|
||||
Delay, SPIError, SpidevDevice,
|
||||
@@ -69,24 +69,27 @@ impl EPaper {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> Result<(), SPIError> {
|
||||
self.epd
|
||||
.update_and_display_frame(&mut self.spi, self.display.buffer(), &mut self.delay)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sleep(&mut self) -> Result<(), SPIError> {
|
||||
self.epd.sleep(&mut self.spi, &mut self.delay)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, on: bool) {
|
||||
impl KillDisplay for EPaper {
|
||||
fn flush(&mut self) {
|
||||
let _ = self.epd.update_and_display_frame(
|
||||
&mut self.spi,
|
||||
self.display.buffer(),
|
||||
&mut self.delay,
|
||||
);
|
||||
}
|
||||
|
||||
fn clear(&mut self, on: bool) {
|
||||
let color = if on { Color::Black } else { Color::White };
|
||||
|
||||
self.display.clear(color).ok();
|
||||
}
|
||||
|
||||
pub fn draw_text(&mut self, x: i32, y: i32, text: &str) {
|
||||
fn draw_text(&mut self, x: i32, y: i32, text: &str) {
|
||||
let style = MonoTextStyleBuilder::new()
|
||||
.font(&embedded_graphics::mono_font::ascii::FONT_8X13)
|
||||
.text_color(Color::White)
|
||||
@@ -99,7 +102,7 @@ impl EPaper {
|
||||
.draw(&mut self.display);
|
||||
}
|
||||
|
||||
pub fn draw_kill_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
fn draw_kill_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
let mut current_y = y;
|
||||
|
||||
// Draw Alliance logo (64x64) if present
|
||||
@@ -174,7 +177,7 @@ impl EPaper {
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn draw_loss_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
fn draw_loss_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
let mut current_y = y;
|
||||
|
||||
// Draw Alliance logo (64x64) if present
|
||||
@@ -10,20 +10,20 @@ use embedded_graphics::{
|
||||
use image::GrayImage;
|
||||
use image::RgbImage;
|
||||
|
||||
use crate::killinfo::KillInfo;
|
||||
use crate::{display::KillDisplay, killinfo::KillInfo};
|
||||
use embedded_graphics::pixelcolor::BinaryColor;
|
||||
use embedded_graphics::pixelcolor::raw::RawU1;
|
||||
|
||||
const WIDTH: usize = 800;
|
||||
const HEIGHT: usize = 480;
|
||||
|
||||
pub struct Display {
|
||||
pub struct ImageSimulator {
|
||||
width: usize,
|
||||
height: usize,
|
||||
buffer: Framebuffer<BinaryColor, RawU1, BigEndian, WIDTH, HEIGHT, { WIDTH * HEIGHT }>,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
impl ImageSimulator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
width: WIDTH,
|
||||
@@ -33,15 +33,6 @@ impl Display {
|
||||
),
|
||||
}
|
||||
}
|
||||
pub fn clear(&mut self, on: bool) {
|
||||
let color = if on {
|
||||
BinaryColor::On
|
||||
} else {
|
||||
BinaryColor::Off
|
||||
};
|
||||
|
||||
self.buffer.clear(color).ok();
|
||||
}
|
||||
|
||||
pub fn draw_rect(&mut self, x: i32, y: i32, w: u32, h: u32, color: u8) {
|
||||
let rect = Rectangle::new(Point::new(x, y), Size::new(w, h));
|
||||
@@ -203,8 +194,10 @@ impl Display {
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flush(&self) {
|
||||
impl KillDisplay for ImageSimulator {
|
||||
fn flush(&mut self) {
|
||||
let buf = self.buffer.data();
|
||||
let img_data: Vec<u8> = buf
|
||||
.iter()
|
||||
@@ -216,6 +209,162 @@ impl Display {
|
||||
img.save("output.png").unwrap();
|
||||
println!("Saved framebuffer to {:?}", "output.png");
|
||||
}
|
||||
|
||||
fn clear(&mut self, on: bool) {
|
||||
let color = if on {
|
||||
BinaryColor::On
|
||||
} else {
|
||||
BinaryColor::Off
|
||||
};
|
||||
|
||||
self.buffer.clear(color).ok();
|
||||
}
|
||||
|
||||
fn draw_text(&mut self, x: i32, y: i32, text: &str) {
|
||||
let bw = BinaryColor::On;
|
||||
let style = MonoTextStyle::new(&FONT_8X13, bw);
|
||||
Text::new(text, Point::new(x, y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn draw_kill_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
let mut current_y = y;
|
||||
|
||||
// Draw Alliance logo (64x64) if present
|
||||
if let Some(alliance) = &kill.victim.alliance {
|
||||
let bw_vec = rgb_to_bw_dithered(&alliance.logo);
|
||||
let logo_raw = ImageRaw::<BinaryColor>::new(&bw_vec, alliance.logo.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x, y))
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Draw Ship icon (64x64) if present
|
||||
if let Some(ship) = &kill.victim.ship {
|
||||
let bw_vec = rgb_to_bw_dithered(&ship.icon);
|
||||
let logo_raw = ImageRaw::<BinaryColor>::new(&bw_vec, ship.icon.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x + 64 + 4, current_y))
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
let text_x = x + 64 + 64 + 8;
|
||||
let style = MonoTextStyle::new(&FONT_8X13, BinaryColor::Off);
|
||||
current_y += 8;
|
||||
|
||||
// Character name + Alliance short
|
||||
let alliance_short = kill
|
||||
.victim
|
||||
.alliance
|
||||
.as_ref()
|
||||
.map(|a| a.short.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_name = kill
|
||||
.victim
|
||||
.character
|
||||
.as_ref()
|
||||
.map(|a| a.name.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_line = format!("{} [{}]", char_name, alliance_short);
|
||||
Text::new(&char_line, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
current_y += 16;
|
||||
|
||||
// Ship name + total value
|
||||
let ship_name = kill
|
||||
.victim
|
||||
.ship
|
||||
.as_ref()
|
||||
.map(|s| s.name.as_str())
|
||||
.unwrap_or("Unknown");
|
||||
let value_line = format!(
|
||||
"{} - {:.2}M ISK",
|
||||
ship_name,
|
||||
kill.total_value / 1_000_000f64
|
||||
);
|
||||
Text::new(&value_line, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
current_y += 16;
|
||||
|
||||
// System name
|
||||
Text::new(&kill.system_name, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn draw_loss_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
let mut current_y = y;
|
||||
|
||||
// Draw Alliance logo (64x64) if present
|
||||
if let Some(character) = &kill.victim.character {
|
||||
let bw_vec = rgb_to_bw_dithered(&character.portrait);
|
||||
let logo_raw = ImageRaw::<BinaryColor>::new(&bw_vec, character.portrait.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x, y))
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Draw Ship icon (64x64) if present
|
||||
if let Some(ship) = &kill.victim.ship {
|
||||
let bw_vec = rgb_to_bw_dithered(&ship.icon);
|
||||
let logo_raw = ImageRaw::<BinaryColor>::new(&bw_vec, ship.icon.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x + 64 + 4, current_y))
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
let text_x = x + 64 + 64 + 8;
|
||||
let style = MonoTextStyle::new(&FONT_8X13, BinaryColor::Off);
|
||||
current_y += 8;
|
||||
|
||||
// Character name + Alliance short
|
||||
let alliance_short = kill
|
||||
.victim
|
||||
.alliance
|
||||
.as_ref()
|
||||
.map(|a| a.short.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_name = kill
|
||||
.victim
|
||||
.character
|
||||
.as_ref()
|
||||
.map(|a| a.name.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_line = format!("{} [{}]", char_name, alliance_short);
|
||||
Text::new(&char_line, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
current_y += 16;
|
||||
|
||||
// Ship name + total value
|
||||
let ship_name = kill
|
||||
.victim
|
||||
.ship
|
||||
.as_ref()
|
||||
.map(|s| s.name.as_str())
|
||||
.unwrap_or("Unknown");
|
||||
let value_line = format!(
|
||||
"{} - {:.2}M ISK",
|
||||
ship_name,
|
||||
kill.total_value / 1_000_000f64
|
||||
);
|
||||
Text::new(&value_line, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
current_y += 16;
|
||||
|
||||
// System name
|
||||
Text::new(&kill.system_name, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb_to_bw_dithered(rgb: &RgbImage) -> Vec<u8> {
|
||||
54
src/display/mod.rs
Normal file
54
src/display/mod.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::killinfo::KillInfo;
|
||||
|
||||
/// A trait for displaying killmail information on various output devices.
|
||||
///
|
||||
/// Implementations may include physical hardware (like e-paper screens)
|
||||
/// or simulated environments (like images for debugging).
|
||||
pub trait KillDisplay {
|
||||
/// Clears the display.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `on` - If `true`, the display is cleared to white; if `false`, to black (if supported).
|
||||
fn clear(&mut self, on: bool);
|
||||
|
||||
/// Draws arbitrary text on the display.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `x` - X-coordinate in pixels.
|
||||
/// * `y` - Y-coordinate in pixels.
|
||||
/// * `text` - The text string to render.
|
||||
fn draw_text(&mut self, x: i32, y: i32, text: &str);
|
||||
|
||||
/// Draws a killmail summary as a "kill" (when the tracked corporation scored the kill).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `kill` - Killmail data to render.
|
||||
/// * `x` - X-coordinate in pixels.
|
||||
/// * `y` - Y-coordinate in pixels.
|
||||
fn draw_kill_info(&mut self, kill: &KillInfo, x: i32, y: i32);
|
||||
|
||||
/// Draws a killmail summary as a "loss" (when the tracked corporation suffered the loss).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `kill` - Killmail data to render.
|
||||
/// * `x` - X-coordinate in pixels.
|
||||
/// * `y` - Y-coordinate in pixels.
|
||||
fn draw_loss_info(&mut self, kill: &KillInfo, x: i32, y: i32);
|
||||
|
||||
/// Finalizes any pending drawing operations and updates the display.
|
||||
fn flush(&mut self);
|
||||
|
||||
// update needed for more interactive displays;
|
||||
fn update(&mut self) {}
|
||||
}
|
||||
|
||||
#[cfg(feature = "epaper")]
|
||||
mod epaper;
|
||||
#[cfg(feature = "epaper")]
|
||||
pub use epaper::EPaper;
|
||||
|
||||
mod image_sim;
|
||||
mod simulator;
|
||||
|
||||
pub use image_sim::ImageSimulator;
|
||||
pub use simulator::Simulator;
|
||||
379
src/display/simulator.rs
Normal file
379
src/display/simulator.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
use embedded_graphics::{
|
||||
image::{Image, ImageRaw},
|
||||
mono_font::{MonoTextStyle, ascii::FONT_8X13},
|
||||
prelude::*,
|
||||
primitives::{PrimitiveStyle, Rectangle, StyledDrawable},
|
||||
text::Text,
|
||||
};
|
||||
use image::RgbImage;
|
||||
|
||||
use crate::{display::KillDisplay, killinfo::KillInfo};
|
||||
use embedded_graphics::pixelcolor::Gray8;
|
||||
|
||||
use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay, Window};
|
||||
|
||||
const WIDTH: usize = 800;
|
||||
const HEIGHT: usize = 480;
|
||||
|
||||
pub struct Simulator {
|
||||
_width: usize,
|
||||
_height: usize,
|
||||
buffer: SimulatorDisplay<Gray8>,
|
||||
window: Window,
|
||||
}
|
||||
|
||||
impl Simulator {
|
||||
pub fn new() -> Self {
|
||||
let display = SimulatorDisplay::<Gray8>::new(Size::new(WIDTH as u32, HEIGHT as u32));
|
||||
|
||||
let output_settings = OutputSettingsBuilder::new()
|
||||
.scale(1)
|
||||
.pixel_spacing(0)
|
||||
.build();
|
||||
|
||||
let window = Window::new("Killpaper", &output_settings);
|
||||
|
||||
Self {
|
||||
_width: WIDTH,
|
||||
_height: HEIGHT,
|
||||
buffer: display,
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_rect(&mut self, x: i32, y: i32, w: u32, h: u32, color: u8) {
|
||||
let rect = Rectangle::new(Point::new(x, y), Size::new(w, h));
|
||||
let style = PrimitiveStyle::with_fill(Gray8::new(color));
|
||||
rect.draw_styled(&style, &mut self.buffer).ok();
|
||||
}
|
||||
|
||||
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: u8) {
|
||||
let style = MonoTextStyle::new(&FONT_8X13, Gray8::new(color));
|
||||
Text::new(text, Point::new(x, y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn draw_kill_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
let mut current_y = y;
|
||||
|
||||
// Draw Alliance logo (64x64) if present
|
||||
if let Some(alliance) = &kill.victim.alliance {
|
||||
let bw_vec = rgbimage_to_gray8(&alliance.logo);
|
||||
let logo_raw = ImageRaw::<Gray8>::new(&bw_vec, alliance.logo.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x, y))
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Draw Ship icon (64x64) if present
|
||||
if let Some(ship) = &kill.victim.ship {
|
||||
let bw_vec = rgbimage_to_gray8(&ship.icon);
|
||||
let logo_raw = ImageRaw::<Gray8>::new(&bw_vec, ship.icon.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x + 64 + 4, current_y))
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
let text_x = x + 64 + 64 + 8;
|
||||
let style = MonoTextStyle::new(&FONT_8X13, Gray8::BLACK);
|
||||
current_y += 8;
|
||||
|
||||
// Character name + Alliance short
|
||||
let alliance_short = kill
|
||||
.victim
|
||||
.alliance
|
||||
.as_ref()
|
||||
.map(|a| a.short.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_name = kill
|
||||
.victim
|
||||
.character
|
||||
.as_ref()
|
||||
.map(|a| a.name.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_line = format!("{} [{}]", char_name, alliance_short);
|
||||
Text::new(&char_line, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
current_y += 16;
|
||||
|
||||
// Ship name + total value
|
||||
let ship_name = kill
|
||||
.victim
|
||||
.ship
|
||||
.as_ref()
|
||||
.map(|s| s.name.as_str())
|
||||
.unwrap_or("Unknown");
|
||||
let value_line = format!(
|
||||
"{} - {:.2}M ISK",
|
||||
ship_name,
|
||||
kill.total_value / 1_000_000f64
|
||||
);
|
||||
Text::new(&value_line, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
current_y += 16;
|
||||
|
||||
// System name
|
||||
Text::new(&kill.system_name, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn draw_loss_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
let mut current_y = y;
|
||||
|
||||
// Draw Alliance logo (64x64) if present
|
||||
if let Some(character) = &kill.victim.character {
|
||||
let bw_vec = rgbimage_to_gray8(&character.portrait);
|
||||
let logo_raw = ImageRaw::<Gray8>::new(&bw_vec, character.portrait.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x, y))
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Draw Ship icon (64x64) if present
|
||||
if let Some(ship) = &kill.victim.ship {
|
||||
let bw_vec = rgbimage_to_gray8(&ship.icon);
|
||||
let logo_raw = ImageRaw::<Gray8>::new(&bw_vec, ship.icon.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x + 64 + 4, current_y))
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
let text_x = x + 64 + 64 + 8;
|
||||
let style = MonoTextStyle::new(&FONT_8X13, Gray8::BLACK);
|
||||
current_y += 8;
|
||||
|
||||
// Character name + Alliance short
|
||||
let alliance_short = kill
|
||||
.victim
|
||||
.alliance
|
||||
.as_ref()
|
||||
.map(|a| a.short.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_name = kill
|
||||
.victim
|
||||
.character
|
||||
.as_ref()
|
||||
.map(|a| a.name.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_line = format!("{} [{}]", char_name, alliance_short);
|
||||
Text::new(&char_line, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
current_y += 16;
|
||||
|
||||
// Ship name + total value
|
||||
let ship_name = kill
|
||||
.victim
|
||||
.ship
|
||||
.as_ref()
|
||||
.map(|s| s.name.as_str())
|
||||
.unwrap_or("Unknown");
|
||||
let value_line = format!(
|
||||
"{} - {:.2}M ISK",
|
||||
ship_name,
|
||||
kill.total_value / 1_000_000f64
|
||||
);
|
||||
Text::new(&value_line, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
current_y += 16;
|
||||
|
||||
// System name
|
||||
Text::new(&kill.system_name, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
impl KillDisplay for Simulator {
|
||||
fn flush(&mut self) {
|
||||
self.window.update(&self.buffer);
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
self.window.update(&self.buffer);
|
||||
|
||||
if self
|
||||
.window
|
||||
.events()
|
||||
.any(|e| e == embedded_graphics_simulator::SimulatorEvent::Quit)
|
||||
{
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self, on: bool) {
|
||||
let color = if on { Gray8::WHITE } else { Gray8::BLACK };
|
||||
|
||||
self.buffer.clear(color).ok();
|
||||
}
|
||||
|
||||
fn draw_text(&mut self, x: i32, y: i32, text: &str) {
|
||||
let bw = Gray8::BLACK;
|
||||
let style = MonoTextStyle::new(&FONT_8X13, bw);
|
||||
Text::new(text, Point::new(x, y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn draw_kill_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
let mut current_y = y;
|
||||
|
||||
// Draw Alliance logo (64x64) if present
|
||||
if let Some(alliance) = &kill.victim.alliance {
|
||||
let bw_vec = rgbimage_to_gray8(&alliance.logo);
|
||||
let logo_raw = ImageRaw::<Gray8>::new(&bw_vec, alliance.logo.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x, y))
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Draw Ship icon (64x64) if present
|
||||
if let Some(ship) = &kill.victim.ship {
|
||||
let bw_vec = rgbimage_to_gray8(&ship.icon);
|
||||
let logo_raw = ImageRaw::<Gray8>::new(&bw_vec, ship.icon.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x + 64 + 4, current_y))
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
let text_x = x + 64 + 64 + 8;
|
||||
let style = MonoTextStyle::new(&FONT_8X13, Gray8::BLACK);
|
||||
current_y += 8;
|
||||
|
||||
// Character name + Alliance short
|
||||
let alliance_short = kill
|
||||
.victim
|
||||
.alliance
|
||||
.as_ref()
|
||||
.map(|a| a.short.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_name = kill
|
||||
.victim
|
||||
.character
|
||||
.as_ref()
|
||||
.map(|a| a.name.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_line = format!("{} [{}]", char_name, alliance_short);
|
||||
Text::new(&char_line, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
current_y += 16;
|
||||
|
||||
// Ship name + total value
|
||||
let ship_name = kill
|
||||
.victim
|
||||
.ship
|
||||
.as_ref()
|
||||
.map(|s| s.name.as_str())
|
||||
.unwrap_or("Unknown");
|
||||
let value_line = format!(
|
||||
"{} - {:.2}M ISK",
|
||||
ship_name,
|
||||
kill.total_value / 1_000_000f64
|
||||
);
|
||||
Text::new(&value_line, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
current_y += 16;
|
||||
|
||||
// System name
|
||||
Text::new(&kill.system_name, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn draw_loss_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
let mut current_y = y;
|
||||
|
||||
// Draw Alliance logo (64x64) if present
|
||||
if let Some(character) = &kill.victim.character {
|
||||
let bw_vec = rgbimage_to_gray8(&character.portrait);
|
||||
let logo_raw = ImageRaw::<Gray8>::new(&bw_vec, character.portrait.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x, y))
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Draw Ship icon (64x64) if present
|
||||
if let Some(ship) = &kill.victim.ship {
|
||||
let bw_vec = rgbimage_to_gray8(&ship.icon);
|
||||
let logo_raw = ImageRaw::<Gray8>::new(&bw_vec, ship.icon.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x + 64 + 4, current_y))
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
let text_x = x + 64 + 64 + 8;
|
||||
let style = MonoTextStyle::new(&FONT_8X13, Gray8::BLACK);
|
||||
current_y += 8;
|
||||
|
||||
// Character name + Alliance short
|
||||
let alliance_short = kill
|
||||
.victim
|
||||
.alliance
|
||||
.as_ref()
|
||||
.map(|a| a.short.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_name = kill
|
||||
.victim
|
||||
.character
|
||||
.as_ref()
|
||||
.map(|a| a.name.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_line = format!("{} [{}]", char_name, alliance_short);
|
||||
Text::new(&char_line, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
current_y += 16;
|
||||
|
||||
// Ship name + total value
|
||||
let ship_name = kill
|
||||
.victim
|
||||
.ship
|
||||
.as_ref()
|
||||
.map(|s| s.name.as_str())
|
||||
.unwrap_or("Unknown");
|
||||
let value_line = format!(
|
||||
"{} - {:.2}M ISK",
|
||||
ship_name,
|
||||
kill.total_value / 1_000_000f64
|
||||
);
|
||||
Text::new(&value_line, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
current_y += 16;
|
||||
|
||||
// System name
|
||||
Text::new(&kill.system_name, Point::new(text_x, current_y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn rgbimage_to_gray8(raw_img: &RgbImage) -> Vec<u8> {
|
||||
let (width, height) = raw_img.dimensions();
|
||||
|
||||
// Convert RGB → grayscale (luma)
|
||||
let mut gray_data = Vec::with_capacity((width * height) as usize);
|
||||
for pixel in raw_img.pixels() {
|
||||
let [r, g, b] = pixel.0;
|
||||
// Standard luminance formula
|
||||
let luma = (0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32) as u8;
|
||||
gray_data.push(luma);
|
||||
}
|
||||
|
||||
return gray_data;
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
//! EPD representation of multicolor with separate buffers
|
||||
//! for each bit makes it hard to properly represent colors here
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
use embedded_graphics::pixelcolor::BinaryColor;
|
||||
#[cfg(feature = "graphics")]
|
||||
use embedded_graphics::pixelcolor::PixelColor;
|
||||
|
||||
/// When trying to parse u8 to one of the color types
|
||||
@@ -133,7 +131,6 @@ impl ColorType for OctColor {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<BinaryColor> for OctColor {
|
||||
fn from(b: BinaryColor) -> OctColor {
|
||||
match b {
|
||||
@@ -143,7 +140,6 @@ impl From<BinaryColor> for OctColor {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<OctColor> for embedded_graphics::pixelcolor::Rgb888 {
|
||||
fn from(b: OctColor) -> Self {
|
||||
let (r, g, b) = b.rgb();
|
||||
@@ -151,7 +147,6 @@ impl From<OctColor> for embedded_graphics::pixelcolor::Rgb888 {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::Rgb888> for OctColor {
|
||||
fn from(p: embedded_graphics::pixelcolor::Rgb888) -> OctColor {
|
||||
use embedded_graphics::prelude::RgbColor;
|
||||
@@ -186,7 +181,6 @@ impl From<embedded_graphics::pixelcolor::Rgb888> for OctColor {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::raw::RawU4> for OctColor {
|
||||
fn from(b: embedded_graphics::pixelcolor::raw::RawU4) -> Self {
|
||||
use embedded_graphics::prelude::RawData;
|
||||
@@ -194,7 +188,6 @@ impl From<embedded_graphics::pixelcolor::raw::RawU4> for OctColor {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl PixelColor for OctColor {
|
||||
type Raw = embedded_graphics::pixelcolor::raw::RawU4;
|
||||
}
|
||||
@@ -291,7 +284,6 @@ impl From<u8> for Color {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::raw::RawU1> for Color {
|
||||
fn from(b: embedded_graphics::pixelcolor::raw::RawU1) -> Self {
|
||||
use embedded_graphics::prelude::RawData;
|
||||
@@ -303,19 +295,16 @@ impl From<embedded_graphics::pixelcolor::raw::RawU1> for Color {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<Color> for embedded_graphics::pixelcolor::raw::RawU1 {
|
||||
fn from(color: Color) -> Self {
|
||||
Self::new(color.get_bit_value())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl PixelColor for Color {
|
||||
type Raw = embedded_graphics::pixelcolor::raw::RawU1;
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<BinaryColor> for Color {
|
||||
fn from(b: BinaryColor) -> Color {
|
||||
match b {
|
||||
@@ -325,7 +314,6 @@ impl From<BinaryColor> for Color {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::Rgb888> for Color {
|
||||
fn from(rgb: embedded_graphics::pixelcolor::Rgb888) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
@@ -344,7 +332,6 @@ impl From<embedded_graphics::pixelcolor::Rgb888> for Color {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<Color> for embedded_graphics::pixelcolor::Rgb888 {
|
||||
fn from(color: Color) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
@@ -355,7 +342,6 @@ impl From<Color> for embedded_graphics::pixelcolor::Rgb888 {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::Rgb565> for Color {
|
||||
fn from(rgb: embedded_graphics::pixelcolor::Rgb565) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
@@ -374,7 +360,6 @@ impl From<embedded_graphics::pixelcolor::Rgb565> for Color {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<Color> for embedded_graphics::pixelcolor::Rgb565 {
|
||||
fn from(color: Color) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
@@ -385,7 +370,6 @@ impl From<Color> for embedded_graphics::pixelcolor::Rgb565 {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::Rgb555> for Color {
|
||||
fn from(rgb: embedded_graphics::pixelcolor::Rgb555) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
@@ -404,7 +388,6 @@ impl From<embedded_graphics::pixelcolor::Rgb555> for Color {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<Color> for embedded_graphics::pixelcolor::Rgb555 {
|
||||
fn from(color: Color) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
@@ -433,7 +416,6 @@ impl TriColor {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::raw::RawU2> for TriColor {
|
||||
fn from(b: embedded_graphics::pixelcolor::raw::RawU2) -> Self {
|
||||
use embedded_graphics::prelude::RawData;
|
||||
@@ -447,12 +429,10 @@ impl From<embedded_graphics::pixelcolor::raw::RawU2> for TriColor {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl PixelColor for TriColor {
|
||||
type Raw = embedded_graphics::pixelcolor::raw::RawU2;
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<BinaryColor> for TriColor {
|
||||
fn from(b: BinaryColor) -> TriColor {
|
||||
match b {
|
||||
@@ -461,7 +441,6 @@ impl From<BinaryColor> for TriColor {
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::Rgb888> for TriColor {
|
||||
fn from(rgb: embedded_graphics::pixelcolor::Rgb888) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
@@ -475,7 +454,6 @@ impl From<embedded_graphics::pixelcolor::Rgb888> for TriColor {
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<TriColor> for embedded_graphics::pixelcolor::Rgb888 {
|
||||
fn from(tri_color: TriColor) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
|
||||
@@ -25,7 +25,6 @@ use self::command::Command;
|
||||
use crate::buffer_len;
|
||||
|
||||
/// Full size buffer for use with the 7in5 v2 EPD
|
||||
#[cfg(feature = "graphics")]
|
||||
pub type Display7in5 = crate::epd::graphics::Display<
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
|
||||
@@ -1,43 +1,76 @@
|
||||
use crate::model::alliance::Alliance;
|
||||
use crate::model::character::Character;
|
||||
use crate::model::corporation::Corporation;
|
||||
use crate::model::killmail::{KillmailAttacker, KillmailRequest};
|
||||
use crate::model::ship::Ship;
|
||||
use crate::services::esi_static::{EsiClient, EsiError};
|
||||
use crate::services::zkill::{ZkillClient, ZkillError};
|
||||
//! # Kill Info Module
|
||||
//!
|
||||
//! This module provides types and functionality to convert raw killmail data
|
||||
//! into structured, rich information about kills and losses in EVE Online.
|
||||
//!
|
||||
//! It leverages both `ZkillClient` (for killmail retrieval) and `EsiClient`
|
||||
//! (for detailed character, corporation, alliance, and ship info).
|
||||
//!
|
||||
//! ## Key Types
|
||||
//! - [`Individual`]: Represents a single participant (victim or attacker) in a killmail.
|
||||
//! - [`KillInfo`]: Aggregates information about a kill, including victim, main attacker, system, and total ISK value.
|
||||
//! - [`KillInfoBuilder`]: Helper to build `KillInfo` instances, prioritizing attackers from a specific corporation.
|
||||
|
||||
use crate::model::Alliance;
|
||||
use crate::model::Character;
|
||||
use crate::model::Corporation;
|
||||
use crate::model::Ship;
|
||||
use crate::model::killmail::{KillmailAttacker, KillmailRequest};
|
||||
use crate::services::{EsiClient, EsiError, ZkillClient, ZkillError};
|
||||
|
||||
/// Represents a participant (victim or attacker) in a killmail.
|
||||
pub struct Individual {
|
||||
/// Optional character info (may be None if unavailable)
|
||||
pub character: Option<Character>,
|
||||
/// Optional alliance info (may be None if unavailable)
|
||||
pub alliance: Option<Alliance>,
|
||||
/// The corporation of the participant (always available)
|
||||
pub corporation: Corporation,
|
||||
/// Optional ship info (may be None if unavailable)
|
||||
pub ship: Option<Ship>,
|
||||
}
|
||||
|
||||
/// Contains full information about a single kill or loss.
|
||||
pub struct KillInfo {
|
||||
/// The victim of the kill
|
||||
pub victim: Individual,
|
||||
/// The primary attacker (may be None if undetermined e.g. NPC)
|
||||
pub main_attacker: Option<Individual>,
|
||||
/// The solar system where the kill occurred
|
||||
pub system_name: String,
|
||||
/// Total value of the destroyed assets
|
||||
pub total_value: f64,
|
||||
}
|
||||
|
||||
/// Builder for [`KillInfo`] objects, allowing prioritization of attackers from a specific corporation.
|
||||
pub struct KillInfoBuilder {
|
||||
corporation_id: u32,
|
||||
}
|
||||
|
||||
impl KillInfoBuilder {
|
||||
/// Creates a new builder with default settings.
|
||||
pub fn new() -> KillInfoBuilder {
|
||||
KillInfoBuilder { corporation_id: 0 }
|
||||
}
|
||||
|
||||
// used to prioritize attacker with this id
|
||||
/// Sets the corporation ID to prioritize when selecting the main attacker.
|
||||
pub fn set_corporation_id(&mut self, id: u32) {
|
||||
self.corporation_id = id;
|
||||
}
|
||||
|
||||
/// Constructs a [`KillInfo`] object from a [`KillmailRequest`] using `ZkillClient` and `EsiClient`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - The `ZkillClient` used to fetch detailed killmail data.
|
||||
/// * `esi` - Mutable reference to `EsiClient` for additional lookups.
|
||||
/// * `request` - The raw killmail request from zKillboard.
|
||||
///
|
||||
/// # Returns
|
||||
/// `Result<KillInfo, ZkillError>` containing the fully resolved kill information.
|
||||
pub fn make_kill_info(
|
||||
&self,
|
||||
client: &ZkillClient,
|
||||
esi: &EsiClient,
|
||||
esi: &mut EsiClient,
|
||||
request: &KillmailRequest,
|
||||
) -> Result<KillInfo, ZkillError> {
|
||||
let total_value = request.value;
|
||||
@@ -79,6 +112,10 @@ impl KillInfoBuilder {
|
||||
})
|
||||
}
|
||||
|
||||
/// Determines the main attacker from a list of killmail attackers.
|
||||
///
|
||||
/// Prefers attackers from the configured corporation; otherwise, chooses
|
||||
/// the attacker with the highest damage done.
|
||||
pub fn find_main_attacker(
|
||||
&self,
|
||||
attacker_list: &Vec<KillmailAttacker>,
|
||||
@@ -104,8 +141,19 @@ impl KillInfoBuilder {
|
||||
}
|
||||
|
||||
impl Individual {
|
||||
/// Builds an [`Individual`] from raw IDs using [`EsiClient`] lookups.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - Mutable reference to `EsiClient` for API queries.
|
||||
/// * `char_id` - Character ID (0 if unavailable)
|
||||
/// * `corp_id` - Corporation ID
|
||||
/// * `alli_id` - Alliance ID (0 if unavailable)
|
||||
/// * `ship_id` - Ship ID (0 if unavailable)
|
||||
///
|
||||
/// # Returns
|
||||
/// `Result<Individual, EsiError>` containing the constructed participant.
|
||||
pub fn build_with_esi(
|
||||
client: &EsiClient,
|
||||
client: &mut EsiClient,
|
||||
char_id: u32,
|
||||
corp_id: u32,
|
||||
alli_id: u32,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
pub mod app;
|
||||
pub mod cache;
|
||||
pub mod display;
|
||||
pub mod epd;
|
||||
pub mod killinfo;
|
||||
pub mod model;
|
||||
|
||||
90
src/main.rs
90
src/main.rs
@@ -1,70 +1,52 @@
|
||||
//! # Main Entry Point
|
||||
//!
|
||||
//! This crate is an EVE Online killboard client and display application.
|
||||
//! It fetches killmails for a corporation using zKillboard and ESI,
|
||||
//! builds detailed kill information, and displays it either on a simulator
|
||||
//! or an e-paper display, depending on compile-time features.
|
||||
//!
|
||||
//! ## Modules
|
||||
//! - `cache`: Utilities for caching data.
|
||||
//! - `display`: Display abstractions and implementations (simulator or e-paper).
|
||||
//! - `killinfo`: Structures and builders for detailed kill information.
|
||||
//! - `model`: Core data models (Character, Corporation, Alliance, Ship, Killmail).
|
||||
//! - `services`: Clients for ESI, zKillboard, and Redis queue.
|
||||
//! - `app`: Main application logic for fetching, storing, and drawing kills.
|
||||
//! - `epd` (optional): E-Paper display support (feature `epaper`).
|
||||
|
||||
pub mod cache;
|
||||
pub mod display;
|
||||
pub mod epaper;
|
||||
pub mod epd;
|
||||
|
||||
pub mod app;
|
||||
pub mod killinfo;
|
||||
pub mod model;
|
||||
pub mod services;
|
||||
|
||||
use crate::killinfo::KillInfo;
|
||||
use crate::services::esi_static::EsiClient;
|
||||
use crate::services::zkill::ZkillClient;
|
||||
#[cfg(feature = "epaper")]
|
||||
pub mod epd;
|
||||
|
||||
pub const fn buffer_len(width: usize, height: usize) -> usize {
|
||||
(width + 7) / 8 * height
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let esi = EsiClient::new();
|
||||
let zkill = ZkillClient::new();
|
||||
fn main() -> () {
|
||||
// Pick the display based on compile-time feature
|
||||
#[cfg(feature = "simulator")]
|
||||
let display = display::Simulator::new();
|
||||
|
||||
let past_seconds = 60 * 60 * 24;
|
||||
let my_corp_id = 98685373;
|
||||
#[cfg(feature = "paper-png")]
|
||||
let display = display::Simulator::new();
|
||||
|
||||
let mut display = display::Display::new();
|
||||
let mut epaper = epaper::EPaper::new().expect("DisplayError");
|
||||
#[cfg(feature = "epaper")]
|
||||
let display = display::EPaper::new();
|
||||
|
||||
display.clear(true);
|
||||
epaper.clear(true);
|
||||
epaper.flush().expect("flush error");
|
||||
let mut app = app::App::new(98685373, display);
|
||||
app.init();
|
||||
app.draw();
|
||||
|
||||
let response = zkill
|
||||
.get_corporation_kills(my_corp_id, past_seconds)
|
||||
.unwrap();
|
||||
let mut builder = killinfo::KillInfoBuilder::new();
|
||||
builder.set_corporation_id(my_corp_id);
|
||||
|
||||
let killmails: Vec<KillInfo> = response
|
||||
.iter()
|
||||
.map(|r| builder.make_kill_info(&zkill, &esi, r).unwrap())
|
||||
.collect();
|
||||
|
||||
for (i, k) in killmails.iter().enumerate() {
|
||||
let ii = i as i32;
|
||||
let y: i32 = ii * 60;
|
||||
|
||||
display.draw_kill_info(k, 0, y);
|
||||
epaper.draw_kill_info(k, 0, y);
|
||||
loop {
|
||||
if app.pull_update() {
|
||||
app.draw();
|
||||
}
|
||||
}
|
||||
|
||||
let response = zkill
|
||||
.get_corporation_losses(my_corp_id, past_seconds)
|
||||
.unwrap();
|
||||
let mut builder = killinfo::KillInfoBuilder::new();
|
||||
builder.set_corporation_id(my_corp_id);
|
||||
|
||||
let killmails: Vec<KillInfo> = response
|
||||
.iter()
|
||||
.map(|r| builder.make_kill_info(&zkill, &esi, r).unwrap())
|
||||
.collect();
|
||||
|
||||
for (i, k) in killmails.iter().enumerate() {
|
||||
let ii = i as i32;
|
||||
let y: i32 = ii * 60;
|
||||
|
||||
display.draw_loss_info(k, 400, y);
|
||||
epaper.draw_loss_info(k, 400, y);
|
||||
}
|
||||
|
||||
display.flush();
|
||||
epaper.flush().expect("flush error");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use image::RgbImage;
|
||||
|
||||
/// Represents an alliance with a unique ID, name, short name and logo.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Alliance {
|
||||
/// The unique ID of the alliance.
|
||||
pub alliance_id: u32,
|
||||
/// The name of the alliance.
|
||||
pub name: String,
|
||||
/// The short name of the alliance.
|
||||
pub short: String,
|
||||
/// The logo image of the alliance in RGB format.
|
||||
pub logo: RgbImage,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use image::RgbImage;
|
||||
|
||||
/// Represents a character in a game or other media.
|
||||
///
|
||||
/// A `Character` has an ID, a name, and a portrait image. The portrait should be a colored image with RGB color channels.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Character {
|
||||
/// The unique identifier for this character.
|
||||
pub character_id: u32,
|
||||
/// The name of the character as a string.
|
||||
pub name: String,
|
||||
/// The portrait image of the character as an RGB image.
|
||||
pub portrait: RgbImage,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
use image::RgbImage;
|
||||
|
||||
/// Represents a corporation with its details.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Corporation {
|
||||
/// The unique identifier of the corporation.
|
||||
pub corporation_id: u32,
|
||||
|
||||
/// The name of the corporation.
|
||||
pub name: String,
|
||||
|
||||
/// A short version or acronym for the corporation's name.
|
||||
pub short: String,
|
||||
|
||||
/// The logo image associated with the corporation, represented as an RGB image.
|
||||
pub logo: RgbImage,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// Represents a killmail request with an ID, hash and value.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KillmailRequest {
|
||||
pub id: u32,
|
||||
@@ -5,6 +6,9 @@ pub struct KillmailRequest {
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
/// Represents a single attacker in a killmail with their character ID,
|
||||
/// corporation ID, alliance ID, ship ID, damage done and whether they
|
||||
/// were the final blow.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct KillmailAttacker {
|
||||
pub character_id: u32,
|
||||
@@ -15,6 +19,8 @@ pub struct KillmailAttacker {
|
||||
pub final_blow: bool,
|
||||
}
|
||||
|
||||
/// Represents the victim of a killmail with their character ID, corporation
|
||||
/// ID, alliance ID and ship ID.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct KillmailVictim {
|
||||
pub character_id: u32,
|
||||
@@ -23,6 +29,8 @@ pub struct KillmailVictim {
|
||||
pub ship_id: u32,
|
||||
}
|
||||
|
||||
/// Represents a killmail with the system ID of where it happened, the victim
|
||||
/// and a list of attackers.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Killmail {
|
||||
pub system_id: u32,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
pub mod alliance;
|
||||
pub mod character;
|
||||
pub mod corporation;
|
||||
mod alliance;
|
||||
mod character;
|
||||
mod corporation;
|
||||
mod ship;
|
||||
|
||||
pub use alliance::Alliance;
|
||||
pub use character::Character;
|
||||
pub use corporation::Corporation;
|
||||
pub use ship::Ship;
|
||||
|
||||
pub mod killmail;
|
||||
pub mod ship;
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
use image::RgbImage;
|
||||
|
||||
/// Represents a ship in a game or simulation.
|
||||
///
|
||||
/// The `Ship` struct contains information about a single ship,
|
||||
/// including its unique identifier (`ship_id`), name, and icon (represented as an RGB image).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ship {
|
||||
/// A unique identifier for the ship.
|
||||
pub ship_id: u32,
|
||||
|
||||
/// The name of the ship.
|
||||
pub name: String,
|
||||
|
||||
/// An icon representing the ship, stored as an RGB image.
|
||||
pub icon: RgbImage,
|
||||
}
|
||||
|
||||
@@ -1,136 +1,56 @@
|
||||
use image::load_from_memory;
|
||||
//! # ESI Static Client
|
||||
//!
|
||||
//! Provides [`EsiClient`], a cached client for retrieving data from the
|
||||
//! [EVE Online ESI API](https://esi.evetech.net/).
|
||||
//!
|
||||
//! Features:
|
||||
//! - Fetch alliance, corporation, character, ship, and system information.
|
||||
//! - Transparent in-memory caching with [`SimpleCache`].
|
||||
//! - On-disk image caching with [`ImageCache`] for logos, portraits, and icons.
|
||||
//!
|
||||
//! Intended for applications that repeatedly request static entity data
|
||||
//! (like killboards, dashboards, or analysis tools).
|
||||
|
||||
use image::{RgbImage, load_from_memory};
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, de::DeserializeOwned};
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::model::alliance::Alliance;
|
||||
use crate::model::character::Character;
|
||||
use crate::model::corporation::Corporation;
|
||||
use crate::model::ship::Ship;
|
||||
use crate::cache::{SimpleCache, image::ImageCache};
|
||||
use crate::model::{Alliance, Character, Corporation, Ship};
|
||||
|
||||
impl EsiClient {
|
||||
pub fn new() -> Self {
|
||||
let client = Client::builder()
|
||||
.user_agent("killpaper-hw,oli1111@web.de")
|
||||
.gzip(true)
|
||||
.brotli(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
/// Structure to parse Alliance info response from json
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct AllianceInfo {
|
||||
name: String,
|
||||
ticker: String,
|
||||
}
|
||||
|
||||
Self {
|
||||
reqwest_client: client,
|
||||
}
|
||||
}
|
||||
/// Structure to parse Corporation info response from json
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct CorporationInfo {
|
||||
name: String,
|
||||
ticker: String,
|
||||
}
|
||||
|
||||
pub fn get_alliance(&self, id: u32) -> Result<Alliance, EsiError> {
|
||||
#[derive(Deserialize)]
|
||||
struct AllianceInfo {
|
||||
name: String,
|
||||
ticker: String,
|
||||
}
|
||||
/// Structure to parse Character info response from json
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct CharacterInfo {
|
||||
name: String,
|
||||
}
|
||||
|
||||
let info_url = format!("https://esi.evetech.net/latest/alliances/{id}/");
|
||||
println!("REQ ESI{}", info_url);
|
||||
let info: AllianceInfo = self.reqwest_client.get(&info_url).send()?.json()?;
|
||||
/// Structure to parse Ship info response from json
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct ShipInfo {
|
||||
name: String,
|
||||
}
|
||||
|
||||
let logo_url = format!("https://images.evetech.net/alliances/{id}/logo?size=64");
|
||||
println!("REQ ESI{}", logo_url);
|
||||
|
||||
let bytes = self.reqwest_client.get(&logo_url).send()?.bytes()?;
|
||||
let img = load_from_memory(&bytes)?.to_rgb8();
|
||||
|
||||
Ok(Alliance {
|
||||
alliance_id: id,
|
||||
name: info.name,
|
||||
short: info.ticker,
|
||||
logo: img,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_corporation(&self, id: u32) -> Result<Corporation, EsiError> {
|
||||
#[derive(Deserialize)]
|
||||
struct CorporationInfo {
|
||||
name: String,
|
||||
ticker: String,
|
||||
}
|
||||
|
||||
let info_url = format!("https://esi.evetech.net/latest/corporations/{id}/");
|
||||
println!("REQ ESI{}", info_url);
|
||||
let info: CorporationInfo = self.reqwest_client.get(&info_url).send()?.json()?;
|
||||
|
||||
let logo_url = format!("https://images.evetech.net/corporations/{id}/logo?size=64");
|
||||
println!("REQ ESI{}", logo_url);
|
||||
let bytes = self.reqwest_client.get(&logo_url).send()?.bytes()?;
|
||||
let img = load_from_memory(&bytes)?.to_rgb8();
|
||||
|
||||
Ok(Corporation {
|
||||
corporation_id: id,
|
||||
name: info.name,
|
||||
short: info.ticker,
|
||||
logo: img,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_character(&self, id: u32) -> Result<Character, EsiError> {
|
||||
#[derive(Deserialize)]
|
||||
struct CharacterInfo {
|
||||
name: String,
|
||||
}
|
||||
|
||||
let info_url = format!("https://esi.evetech.net/latest/characters/{id}/");
|
||||
println!("REQ ESI{}", info_url);
|
||||
|
||||
let info: CharacterInfo = self.reqwest_client.get(&info_url).send()?.json()?;
|
||||
|
||||
let portrait_url = format!("https://images.evetech.net/characters/{id}/portrait?size=64");
|
||||
println!("REQ ESI{}", portrait_url);
|
||||
|
||||
let bytes = self.reqwest_client.get(&portrait_url).send()?.bytes()?;
|
||||
let img = load_from_memory(&bytes)?.to_rgb8();
|
||||
|
||||
Ok(Character {
|
||||
character_id: id,
|
||||
name: info.name,
|
||||
portrait: img,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_ship(&self, id: u32) -> Result<Ship, EsiError> {
|
||||
#[derive(Deserialize)]
|
||||
struct ShipInfo {
|
||||
name: String,
|
||||
}
|
||||
|
||||
let info_url = format!("https://esi.evetech.net/latest/universe/types/{id}/");
|
||||
println!("REQ ESI{}", info_url);
|
||||
|
||||
let info: ShipInfo = self.reqwest_client.get(&info_url).send()?.json()?;
|
||||
|
||||
let icon_url = format!("https://images.evetech.net/types/{id}/icon?size=64");
|
||||
println!("REQ ESI{}", icon_url);
|
||||
|
||||
let bytes = self.reqwest_client.get(&icon_url).send()?.bytes()?;
|
||||
let img = load_from_memory(&bytes)?.to_rgb8();
|
||||
|
||||
Ok(Ship {
|
||||
ship_id: id,
|
||||
name: info.name,
|
||||
icon: img,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_system(&self, id: u32) -> Result<String, EsiError> {
|
||||
#[derive(Deserialize)]
|
||||
struct SystemInfo {
|
||||
name: String,
|
||||
}
|
||||
|
||||
let url = format!("https://esi.evetech.net/latest/universe/systems/{id}/");
|
||||
println!("REQ ESI{}", url);
|
||||
let info: SystemInfo = self.reqwest_client.get(&url).send()?.json()?;
|
||||
|
||||
Ok(info.name)
|
||||
}
|
||||
/// Structure to parse System info response from json
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct SystemInfo {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -168,8 +88,293 @@ impl From<serde_json::Error> for EsiError {
|
||||
}
|
||||
|
||||
impl std::error::Error for EsiError {}
|
||||
|
||||
/// A client for interacting with the [EVE Online ESI API](https://esi.evetech.net/).
|
||||
///
|
||||
/// `EsiClient` provides cached access to alliance, corporation, character,
|
||||
/// ship, and system information, as well as associated images (logos, portraits, icons).
|
||||
/// It uses both in-memory caches (`SimpleCache`) and on-disk image caches (`ImageCache`)
|
||||
/// to minimize redundant API calls and improve performance.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// let mut esi = EsiClient::new();
|
||||
/// let alliance = esi.get_alliance(99000006).unwrap();
|
||||
/// println!("Alliance: {} [{}]", alliance.name, alliance.short);
|
||||
/// ```
|
||||
pub struct EsiClient {
|
||||
pub(crate) reqwest_client: Client,
|
||||
alli_cache: SimpleCache<AllianceInfo>,
|
||||
corp_cache: SimpleCache<CorporationInfo>,
|
||||
char_cache: SimpleCache<CharacterInfo>,
|
||||
ship_cache: SimpleCache<ShipInfo>,
|
||||
system_cache: SimpleCache<SystemInfo>,
|
||||
|
||||
alli_image_cache: ImageCache,
|
||||
corp_image_cache: ImageCache,
|
||||
char_image_cache: ImageCache,
|
||||
ship_image_cache: ImageCache,
|
||||
}
|
||||
|
||||
impl EsiClient {
|
||||
/// Creates a new `EsiClient` instance with default configuration.
|
||||
///
|
||||
/// Initializes the underlying `reqwest::Client` with compression enabled,
|
||||
/// sets a custom user agent, and ensures cache directories for images exist.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if cache directories cannot be created.
|
||||
pub fn new() -> Self {
|
||||
let client = Client::builder()
|
||||
.user_agent("killpaper-hw,oli1111@web.de")
|
||||
.gzip(true)
|
||||
.brotli(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let alli_img_path = "cache/alli";
|
||||
let corp_img_path = "cache/corp";
|
||||
let char_img_path = "cache/char";
|
||||
let ship_img_path = "cache/ship";
|
||||
|
||||
for dir in [alli_img_path, corp_img_path, char_img_path, ship_img_path] {
|
||||
fs::create_dir_all(Path::new(dir)).expect("could not create cache dir");
|
||||
}
|
||||
|
||||
Self {
|
||||
reqwest_client: client,
|
||||
alli_cache: SimpleCache::new(),
|
||||
corp_cache: SimpleCache::new(),
|
||||
char_cache: SimpleCache::new(),
|
||||
ship_cache: SimpleCache::new(),
|
||||
system_cache: SimpleCache::new(),
|
||||
|
||||
alli_image_cache: ImageCache::new(alli_img_path.to_string()),
|
||||
corp_image_cache: ImageCache::new(corp_img_path.to_string()),
|
||||
char_image_cache: ImageCache::new(char_img_path.to_string()),
|
||||
ship_image_cache: ImageCache::new(ship_img_path.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches and caches a JSON resource from ESI.
|
||||
///
|
||||
/// Checks the provided [`SimpleCache`] for the given `id`.
|
||||
/// If not cached, it performs a GET request, deserializes the response,
|
||||
/// stores it in the cache, and returns the result.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`EsiError`] if the request or deserialization fails.
|
||||
fn request_with_cache<T>(
|
||||
id: u32,
|
||||
cache: &mut SimpleCache<T>,
|
||||
client: &Client,
|
||||
request: String,
|
||||
) -> Result<T, EsiError>
|
||||
where
|
||||
T: Clone + DeserializeOwned,
|
||||
{
|
||||
if let Some(value) = cache.lookup(id) {
|
||||
//println!("VALUE hit {}", id);
|
||||
Ok(value)
|
||||
} else {
|
||||
//println!("VALUE miss {} {}",id, request);
|
||||
let info: T = client.get(&request).send()?.json()?;
|
||||
cache.insert(id, &info);
|
||||
Ok(info)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches and caches an image resource from ESI.
|
||||
///
|
||||
/// Checks the provided [`ImageCache`] for the given `id`.
|
||||
/// If not cached, it performs a GET request, decodes the image into `RgbImage`,
|
||||
/// stores it in the cache, and returns the result.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`EsiError`] if the request, decoding, or caching fails.
|
||||
fn request_image_with_cache(
|
||||
id: u32,
|
||||
cache: &mut ImageCache,
|
||||
client: &Client,
|
||||
request: String,
|
||||
) -> Result<RgbImage, EsiError> {
|
||||
if let Some(value) = cache.lookup(id) {
|
||||
//println!("IMG hit {}", id);
|
||||
Ok(value)
|
||||
} else {
|
||||
//println!("IMG miss{} {}",id, request);
|
||||
let bytes = client.get(&request).send()?.bytes()?;
|
||||
let img = load_from_memory(&bytes)?.to_rgb8();
|
||||
cache.insert(id, &img)?;
|
||||
Ok(img)
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves information about an alliance, including its name, ticker, and logo.
|
||||
///
|
||||
/// Uses ESI's alliance information and image endpoints.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - The alliance ID.
|
||||
///
|
||||
/// # Returns
|
||||
/// An [`Alliance`] object containing the alliance details.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`EsiError`] if the request or image retrieval fails.
|
||||
pub fn get_alliance(&mut self, id: u32) -> Result<Alliance, EsiError> {
|
||||
let info_url = format!("https://esi.evetech.net/latest/alliances/{id}/");
|
||||
let info = EsiClient::request_with_cache(
|
||||
id,
|
||||
&mut self.alli_cache,
|
||||
&self.reqwest_client,
|
||||
info_url,
|
||||
)?;
|
||||
|
||||
let logo_url = format!("https://images.evetech.net/alliances/{id}/logo?size=64");
|
||||
let img = EsiClient::request_image_with_cache(
|
||||
id,
|
||||
&mut self.alli_image_cache,
|
||||
&self.reqwest_client,
|
||||
logo_url,
|
||||
)?;
|
||||
|
||||
Ok(Alliance {
|
||||
alliance_id: id,
|
||||
name: info.name,
|
||||
short: info.ticker,
|
||||
logo: img,
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves information about a corporation, including its name, ticker, and logo.
|
||||
///
|
||||
/// Uses ESI's corporation information and image endpoints.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - The corporation ID.
|
||||
///
|
||||
/// # Returns
|
||||
/// A [`Corporation`] object containing the corporation details.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`EsiError`] if the request or image retrieval fails.
|
||||
pub fn get_corporation(&mut self, id: u32) -> Result<Corporation, EsiError> {
|
||||
let info_url = format!("https://esi.evetech.net/latest/corporations/{id}/");
|
||||
let info = EsiClient::request_with_cache(
|
||||
id,
|
||||
&mut self.corp_cache,
|
||||
&self.reqwest_client,
|
||||
info_url,
|
||||
)?;
|
||||
|
||||
let logo_url = format!("https://images.evetech.net/corporations/{id}/logo?size=64");
|
||||
let img = EsiClient::request_image_with_cache(
|
||||
id,
|
||||
&mut self.corp_image_cache,
|
||||
&self.reqwest_client,
|
||||
logo_url,
|
||||
)?;
|
||||
|
||||
Ok(Corporation {
|
||||
corporation_id: id,
|
||||
name: info.name,
|
||||
short: info.ticker,
|
||||
logo: img,
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves information about a character, including its name and portrait.
|
||||
///
|
||||
/// Uses ESI's character information and image endpoints.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - The character ID.
|
||||
///
|
||||
/// # Returns
|
||||
/// A [`Character`] object containing the character details.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`EsiError`] if the request or image retrieval fails.
|
||||
pub fn get_character(&mut self, id: u32) -> Result<Character, EsiError> {
|
||||
let info_url = format!("https://esi.evetech.net/latest/characters/{id}/");
|
||||
let info = EsiClient::request_with_cache(
|
||||
id,
|
||||
&mut self.char_cache,
|
||||
&self.reqwest_client,
|
||||
info_url,
|
||||
)?;
|
||||
|
||||
let portrait_url = format!("https://images.evetech.net/characters/{id}/portrait?size=64");
|
||||
let img = EsiClient::request_image_with_cache(
|
||||
id,
|
||||
&mut self.char_image_cache,
|
||||
&self.reqwest_client,
|
||||
portrait_url,
|
||||
)?;
|
||||
|
||||
Ok(Character {
|
||||
character_id: id,
|
||||
name: info.name,
|
||||
portrait: img,
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves information about a ship type, including its name and icon.
|
||||
///
|
||||
/// Uses ESI's universe type information and image endpoints.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - The ship type ID.
|
||||
///
|
||||
/// # Returns
|
||||
/// A [`Ship`] object containing the ship details.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`EsiError`] if the request or image retrieval fails.
|
||||
pub fn get_ship(&mut self, id: u32) -> Result<Ship, EsiError> {
|
||||
let info_url = format!("https://esi.evetech.net/latest/universe/types/{id}/");
|
||||
let info = EsiClient::request_with_cache(
|
||||
id,
|
||||
&mut self.ship_cache,
|
||||
&self.reqwest_client,
|
||||
info_url,
|
||||
)?;
|
||||
|
||||
let icon_url = format!("https://images.evetech.net/types/{id}/icon?size=64");
|
||||
let img = EsiClient::request_image_with_cache(
|
||||
id,
|
||||
&mut self.ship_image_cache,
|
||||
&self.reqwest_client,
|
||||
icon_url,
|
||||
)?;
|
||||
|
||||
Ok(Ship {
|
||||
ship_id: id,
|
||||
name: info.name,
|
||||
icon: img,
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves the name of a solar system.
|
||||
///
|
||||
/// Uses ESI's universe system endpoint.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - The system ID.
|
||||
///
|
||||
/// # Returns
|
||||
/// The system name as a `String`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`EsiError`] if the request fails.
|
||||
pub fn get_system(&mut self, id: u32) -> Result<String, EsiError> {
|
||||
let url = format!("https://esi.evetech.net/latest/universe/systems/{id}/");
|
||||
let info =
|
||||
EsiClient::request_with_cache(id, &mut self.system_cache, &self.reqwest_client, url)?;
|
||||
|
||||
Ok(info.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -178,7 +383,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_alliance() -> Result<(), EsiError> {
|
||||
let esi = EsiClient::new();
|
||||
let mut esi = EsiClient::new();
|
||||
let alliance = esi.get_alliance(99002685)?;
|
||||
assert_eq!(alliance.alliance_id, 99002685);
|
||||
|
||||
@@ -194,7 +399,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_corporation() -> Result<(), EsiError> {
|
||||
let esi = EsiClient::new();
|
||||
let mut esi = EsiClient::new();
|
||||
let corp = esi.get_corporation(98330748)?;
|
||||
assert_eq!(corp.corporation_id, 98330748);
|
||||
|
||||
@@ -210,7 +415,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_character() -> Result<(), EsiError> {
|
||||
let esi = EsiClient::new();
|
||||
let mut esi = EsiClient::new();
|
||||
let character = esi.get_character(90951867)?;
|
||||
|
||||
assert_eq!(character.character_id, 90951867);
|
||||
@@ -230,7 +435,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_ship() -> Result<(), EsiError> {
|
||||
let esi = EsiClient::new();
|
||||
let mut esi = EsiClient::new();
|
||||
let ship = esi.get_ship(587)?; // Rifter
|
||||
|
||||
assert_eq!(ship.ship_id, 587);
|
||||
@@ -245,7 +450,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_system() -> Result<(), EsiError> {
|
||||
let esi = EsiClient::new();
|
||||
let mut esi = EsiClient::new();
|
||||
let system_name = esi.get_system(30000142)?; // Jita
|
||||
|
||||
assert_eq!(system_name, "Jita");
|
||||
|
||||
@@ -1,2 +1,36 @@
|
||||
pub mod esi_static;
|
||||
pub mod zkill;
|
||||
//! # EVE Online API Clients
|
||||
//!
|
||||
//! This module provides clients for working with EVE Online data sources:
|
||||
//!
|
||||
//! - [`EsiClient`] → Cached access to EVE Swagger Interface (ESI) for alliances, corporations, characters, ships, and systems.
|
||||
//! - [`ZkillClient`] → Fetches kills, losses, and killmail details from [zKillboard](https://zkillboard.com) and ESI.
|
||||
//! - [`RedisQClient`] → Consumes real-time killmail notifications from [zKillboard's RedisQ stream](https://redisq.zkillboard.com).
|
||||
//!
|
||||
//! ## Example
|
||||
//! ```no_run
|
||||
//! use mycrate::api::{EsiClient, ZkillClient, RedisQClient};
|
||||
//!
|
||||
//! // Create an ESI client
|
||||
//! let mut esi = EsiClient::new();
|
||||
//! let corp = esi.get_corporation(123456789).unwrap();
|
||||
//! println!("Corporation: {} [{}]", corp.name, corp.short);
|
||||
//!
|
||||
//! // Create a Zkillboard client
|
||||
//! let zkill = ZkillClient::new();
|
||||
//! let kills = zkill.get_corporation_kills(123456789, 3600).unwrap();
|
||||
//!
|
||||
//! // Create a RedisQ client
|
||||
//! let mut redisq = RedisQClient::new("mychannel".into());
|
||||
//! redisq.set_corporation_id(123456789);
|
||||
//! if let Some((req, target)) = redisq.next() {
|
||||
//! println!("Received killmail {} as {:?}", req.id, target);
|
||||
//! }
|
||||
//! ```
|
||||
mod esi_static;
|
||||
mod redisq;
|
||||
mod zkill;
|
||||
|
||||
pub use esi_static::{EsiClient, EsiError};
|
||||
pub use redisq::RedisQClient;
|
||||
pub use redisq::Target;
|
||||
pub use zkill::{ZkillClient, ZkillError};
|
||||
|
||||
199
src/services/redisq.rs
Normal file
199
src/services/redisq.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
//! # RedisQ Client
|
||||
//!
|
||||
//! Provides [`RedisQClient`], a client for consuming real-time killmail
|
||||
//! notifications from zKillboard's [RedisQ](https://redisq.zkillboard.com) API.
|
||||
//!
|
||||
//! Features:
|
||||
//! - Subscribe to a unique queue/channel for killmail delivery.
|
||||
//! - Optional filtering by corporation ID (classify as Kill or Loss).
|
||||
//! - Blocking long-polling interface for stream consumption.
|
||||
//!
|
||||
//! Useful for live dashboards, bots, or services that react to new kills
|
||||
//! as they happen in EVE Online.
|
||||
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::model::killmail::*;
|
||||
|
||||
/// A simplified killmail structure returned by RedisQ.
|
||||
///
|
||||
/// This is a subset of the official killmail format provided by zKillboard.
|
||||
/// It contains the essential information needed for filtering and processing.
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct QKillmail {
|
||||
#[serde(rename = "solar_system_id")]
|
||||
pub _system_id: u32,
|
||||
#[serde(rename = "killmail_id")]
|
||||
pub killmail_id: u32,
|
||||
pub victim: QKillmailVictim,
|
||||
pub attackers: Vec<QKillmailAttacker>,
|
||||
}
|
||||
|
||||
/// Victim details inside a killmail.
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct QKillmailVictim {
|
||||
pub corporation_id: u32,
|
||||
}
|
||||
|
||||
/// Attacker details inside a killmail.
|
||||
///
|
||||
/// Some attackers may not have a corporation ID (e.g., NPCs).
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct QKillmailAttacker {
|
||||
pub corporation_id: Option<u32>,
|
||||
}
|
||||
|
||||
/// The root JSON wrapper returned by the RedisQ API.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct QKillmailPackage {
|
||||
package: QKillmailWrapper,
|
||||
}
|
||||
|
||||
/// Additional metadata about a kill from zKillboard.
|
||||
///
|
||||
/// Contains the hash (used to fetch the full killmail) and ISK value.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct QZkbInfo {
|
||||
hash: String,
|
||||
#[serde(rename = "totalValue")]
|
||||
total_value: f64,
|
||||
}
|
||||
|
||||
/// Internal wrapper that groups killmail data with zKillboard info.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct QKillmailWrapper {
|
||||
killmail: QKillmail,
|
||||
zkb: QZkbInfo,
|
||||
}
|
||||
|
||||
pub struct RedisQClient {
|
||||
client: Client,
|
||||
filter_corp_id: Option<u32>,
|
||||
channel: String,
|
||||
}
|
||||
|
||||
/// Represents whether a retrieved killmail is a kill or a loss
|
||||
/// relative to the configured corporation filter.
|
||||
pub enum Target {
|
||||
/// A kill where the configured corporation was one of the attackers.
|
||||
Kill,
|
||||
/// A loss where the configured corporation was the victim.
|
||||
Loss,
|
||||
}
|
||||
|
||||
/// Client for consuming killmails from [zKillboard's RedisQ stream](https://redisq.zkillboard.com/listen.php).
|
||||
///
|
||||
/// `RedisQClient` polls the zKillboard RedisQ endpoint and retrieves killmail
|
||||
/// notifications in near real-time. You can optionally filter by corporation ID
|
||||
/// to separate kills from losses.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// let mut client = RedisQClient::new("example_channel".into());
|
||||
/// client.set_corporation_id(123456789);
|
||||
///
|
||||
/// while let Some((km, target)) = client.next() {
|
||||
/// match target {
|
||||
/// Target::Kill => println!("We scored a kill worth {} ISK!", km.value),
|
||||
/// Target::Loss => println!("We suffered a loss: {}", km.id),
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
impl RedisQClient {
|
||||
/// Creates a new [`RedisQClient`] with the given channel name.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `channel` - A RedisQ queue identifier. Each client should have its own unique channel.
|
||||
pub fn new(channel: String) -> Self {
|
||||
let client = Client::builder()
|
||||
.user_agent("killpaper-hw,oli1111@web.de")
|
||||
.gzip(true)
|
||||
.brotli(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
RedisQClient {
|
||||
client,
|
||||
filter_corp_id: None,
|
||||
channel,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the corporation ID to filter killmails against.
|
||||
///
|
||||
/// When set:
|
||||
/// - If the corporation appears among the attackers, the killmail is categorized as [`Target::Kill`].
|
||||
/// - If the corporation is the victim, it is categorized as [`Target::Loss`].
|
||||
/// - If neither, the killmail is ignored.
|
||||
///
|
||||
/// When Not set
|
||||
/// - All killmails are reported ass kills
|
||||
pub fn set_corporation_id(&mut self, id: u32) {
|
||||
self.filter_corp_id = Some(id)
|
||||
}
|
||||
|
||||
/// Retrieves the next killmail from the RedisQ stream.
|
||||
///
|
||||
/// This method performs a blocking HTTP request to the RedisQ endpoint,
|
||||
/// deserializes the response, applies the corporation filter (if set),
|
||||
/// and returns a tuple of:
|
||||
/// - [`KillmailRequest`] (containing ID, hash, and ISK value)
|
||||
/// - [`Target`] (Kill or Loss)
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Some((KillmailRequest, Target))` if a valid killmail was retrieved and passed the filter.
|
||||
/// * `None` if no valid killmail is available or the filter excluded it.
|
||||
///
|
||||
/// # Notes
|
||||
/// - RedisQ is a long-polling API; this call may block until a killmail is available.
|
||||
/// - Malformed or unexpected responses are logged to stdout and skipped.
|
||||
pub fn next(&self) -> Option<(KillmailRequest, Target)> {
|
||||
let url = format!(
|
||||
"https://zkillredisq.stream/listen.php?queueID={}",
|
||||
self.channel
|
||||
);
|
||||
|
||||
if let Ok(resp) = self.client.get(&url).send() {
|
||||
// Read the response as a string first
|
||||
if let Ok(text) = resp.text() {
|
||||
// Deserialize from the same string
|
||||
match serde_json::from_str::<QKillmailPackage>(&text) {
|
||||
Ok(mail) => {
|
||||
let km = mail.package.killmail.clone();
|
||||
|
||||
let target = if let Some(filter_id) = self.filter_corp_id {
|
||||
if km
|
||||
.attackers
|
||||
.iter()
|
||||
.any(|a| a.corporation_id == Some(filter_id))
|
||||
{
|
||||
Target::Kill
|
||||
} else if km.victim.corporation_id == filter_id {
|
||||
Target::Loss
|
||||
} else {
|
||||
// Corp_id filter set but no match
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
Target::Kill
|
||||
};
|
||||
|
||||
let request = KillmailRequest {
|
||||
id: km.killmail_id,
|
||||
hash: mail.package.zkb.hash,
|
||||
value: mail.package.zkb.total_value,
|
||||
};
|
||||
|
||||
return Some((request, target));
|
||||
}
|
||||
Err(err) => {
|
||||
println!("REDISQ_Error: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,46 @@
|
||||
//! # zKillboard Client
|
||||
//!
|
||||
//! Provides [`ZkillClient`], a client for querying [zKillboard](https://zkillboard.com)
|
||||
//! and retrieving killmails, kills, and losses.
|
||||
//!
|
||||
//! Features:
|
||||
//! - Fetch corporation kills or losses within a configurable time window.
|
||||
//! - Retrieve full killmail details (attackers, victim, system) via ESI.
|
||||
//! - Unified error handling through [`ZkillError`].
|
||||
//!
|
||||
//! Useful for historical data queries, killmail processing pipelines,
|
||||
//! or tools that need aggregated kill/loss information.
|
||||
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
|
||||
use crate::model::killmail::*;
|
||||
|
||||
/// API structure to parse zKillboard's kill response JSON.
|
||||
///
|
||||
/// Returned when querying kills or losses from the zKillboard API.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ZkbKill {
|
||||
/// The killmail ID.
|
||||
killmail_id: u32,
|
||||
/// Metadata about the kill from zKillboard.
|
||||
zkb: ZkbInfo,
|
||||
}
|
||||
|
||||
/// Metadata returned by zKillboard for a killmail.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ZkbInfo {
|
||||
/// Hash required to retrieve the full killmail via ESI.
|
||||
hash: String,
|
||||
totalValue: f64,
|
||||
/// Total ISK value of the kill.
|
||||
#[serde(rename = "totalValue")]
|
||||
total_value: f64,
|
||||
}
|
||||
|
||||
pub struct ZkillClient {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
// ESI API deserialization structs
|
||||
/// API structure representing an attacker inside an ESI killmail.
|
||||
///
|
||||
/// Values are optional since some attackers may lack certain fields (e.g., NPCs).
|
||||
#[derive(Deserialize)]
|
||||
struct EsiAttacker {
|
||||
character_id: Option<u32>,
|
||||
@@ -31,6 +51,9 @@ struct EsiAttacker {
|
||||
final_blow: Option<bool>,
|
||||
}
|
||||
|
||||
/// API structure representing the victim inside an ESI killmail.
|
||||
///
|
||||
/// Values are optional since not all fields are guaranteed to be present.
|
||||
#[derive(Deserialize)]
|
||||
struct EsiVictim {
|
||||
character_id: Option<u32>,
|
||||
@@ -39,18 +62,28 @@ struct EsiVictim {
|
||||
ship_type_id: Option<u32>,
|
||||
}
|
||||
|
||||
/// API structure representing the core ESI killmail response.
|
||||
///
|
||||
/// Used to retrieve detailed killmail information from ESI
|
||||
/// after resolving it via zKillboard.
|
||||
#[derive(Deserialize)]
|
||||
struct EsiKillmail {
|
||||
/// The solar system where the kill occurred.
|
||||
solar_system_id: u32,
|
||||
/// List of attackers.
|
||||
attackers: Vec<EsiAttacker>,
|
||||
/// The victim.
|
||||
victim: EsiVictim,
|
||||
}
|
||||
|
||||
// Custom error type
|
||||
/// Error type for zKillboard and ESI API operations.
|
||||
#[derive(Debug)]
|
||||
pub enum ZkillError {
|
||||
/// Wrapper for `reqwest` errors during HTTP requests.
|
||||
Reqwest(reqwest::Error),
|
||||
/// Wrapper for `serde_json` errors during deserialization.
|
||||
Json(serde_json::Error),
|
||||
/// Returned when an entity (e.g., killmail) was not found.
|
||||
NotFound(String),
|
||||
}
|
||||
|
||||
@@ -78,7 +111,30 @@ impl From<serde_json::Error> for ZkillError {
|
||||
}
|
||||
}
|
||||
|
||||
/// A client for interacting with [zKillboard](https://zkillboard.com) and
|
||||
/// fetching killmail information from both zKillboard and EVE ESI.
|
||||
///
|
||||
/// `ZkillClient` can:
|
||||
/// - Retrieve corporation kills or losses within a given time window.
|
||||
/// - Fetch full killmail details (attackers, victim, system) via ESI.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// let client = ZkillClient::new();
|
||||
/// let kills = client.get_corporation_kills(123456789, 3600).unwrap();
|
||||
/// for km in kills {
|
||||
/// let details = client.fetch_killmail_details(&km).unwrap();
|
||||
/// println!("Kill in system {} worth {} ISK", details.system_id, km.value);
|
||||
/// }
|
||||
/// ```
|
||||
pub struct ZkillClient {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl ZkillClient {
|
||||
/// Creates a new [`ZkillClient`] with a configured `reqwest::Client`.
|
||||
///
|
||||
/// The client has compression enabled and a custom user agent set.
|
||||
pub fn new() -> Self {
|
||||
let client = Client::builder()
|
||||
.user_agent("killpaper-hw,oli1111@web.de")
|
||||
@@ -90,6 +146,17 @@ impl ZkillClient {
|
||||
ZkillClient { client }
|
||||
}
|
||||
|
||||
/// Fetches kills for a corporation within a given time window.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `corp_id` - The corporation ID to fetch kills for.
|
||||
/// * `past_seconds` - Look-back window in seconds.
|
||||
///
|
||||
/// # Returns
|
||||
/// A vector of [`KillmailRequest`] containing killmail IDs, hashes, and values.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`ZkillError`] if the request or JSON parsing fails.
|
||||
pub fn get_corporation_kills(
|
||||
&self,
|
||||
corp_id: u32,
|
||||
@@ -108,7 +175,7 @@ impl ZkillClient {
|
||||
.map(|k| KillmailRequest {
|
||||
id: k.killmail_id,
|
||||
hash: k.zkb.hash,
|
||||
value: k.zkb.totalValue,
|
||||
value: k.zkb.total_value,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -133,7 +200,7 @@ impl ZkillClient {
|
||||
.map(|k| KillmailRequest {
|
||||
id: k.killmail_id,
|
||||
hash: k.zkb.hash,
|
||||
value: k.zkb.totalValue,
|
||||
value: k.zkb.total_value,
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user