inital working setup
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Killmail",
|
||||
"killpaper"
|
||||
]
|
||||
}
|
||||
2999
Cargo.lock
generated
Normal file
2999
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "killpaper"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
d = "0.0.1"
|
||||
embedded-graphics = "0.8.1"
|
||||
image = { version = "0.25.6", default-features = false, features = ["jpeg", "png"] }
|
||||
reqwest = { version = "0.12", features = ["json", "blocking", "gzip", "brotli"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.143"
|
||||
BIN
killmails_display.png
Normal file
BIN
killmails_display.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
BIN
output.png
Normal file
BIN
output.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
116
src/display.rs
Normal file
116
src/display.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use embedded_graphics::{
|
||||
framebuffer::Framebuffer,
|
||||
mono_font::{MonoTextStyle, ascii::FONT_8X13},
|
||||
pixelcolor::{
|
||||
Gray8,
|
||||
raw::{LittleEndian, RawU8},
|
||||
},
|
||||
prelude::*,
|
||||
primitives::{PrimitiveStyle, Rectangle, StyledDrawable},
|
||||
text::Text,
|
||||
image::{Image, ImageRaw},
|
||||
};
|
||||
use image::RgbImage;
|
||||
use image::{GrayImage};
|
||||
|
||||
|
||||
|
||||
use crate::killinfo::KillInfo;
|
||||
|
||||
const WIDTH: usize = 800;
|
||||
const HEIGHT: usize = 480;
|
||||
|
||||
pub struct Display {
|
||||
width: usize,
|
||||
height: usize,
|
||||
buffer: Framebuffer<Gray8, RawU8, LittleEndian, WIDTH, HEIGHT, { WIDTH * HEIGHT }>,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
width : WIDTH,
|
||||
height : HEIGHT,
|
||||
buffer:
|
||||
Framebuffer::<Gray8, RawU8, LittleEndian, WIDTH, HEIGHT, { WIDTH * HEIGHT }>::new(),
|
||||
}
|
||||
}
|
||||
|
||||
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 gray_vec = rgb_to_gray_vec(&alliance.logo);
|
||||
let logo_raw = ImageRaw::<Gray8>::new(&gray_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 gray_vec = rgb_to_gray_vec(&ship.icon);
|
||||
let logo_raw = ImageRaw::<Gray8>::new(&gray_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::new(255));
|
||||
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_line = format!("{} [{}]", kill.victim.character.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 flush(&self) {
|
||||
|
||||
let buf = self.buffer.data();
|
||||
let img = GrayImage::from_raw(self.width as u32, self.height as u32, buf.iter().map(|p| *p).collect()).unwrap();
|
||||
img.save("output.png").unwrap();
|
||||
println!("Saved framebuffer to {:?}", "output.png");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
fn rgb_to_gray_vec(rgb: &RgbImage) -> Vec<u8> {
|
||||
rgb.pixels()
|
||||
.map(|p| {
|
||||
let [r, g, b] = p.0;
|
||||
(0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32) as u8
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
138
src/killinfo.rs
Normal file
138
src/killinfo.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
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};
|
||||
|
||||
pub struct Individual {
|
||||
pub character: Character,
|
||||
pub alliance: Option<Alliance>,
|
||||
pub corporation: Corporation,
|
||||
pub ship: Option<Ship>,
|
||||
}
|
||||
|
||||
pub struct KillInfo {
|
||||
pub victim: Individual,
|
||||
pub main_attacker: Option<Individual>,
|
||||
pub system_name: String,
|
||||
pub total_value: f64,
|
||||
}
|
||||
|
||||
pub struct KillInfoBuilder {
|
||||
corporation_id: u32,
|
||||
}
|
||||
|
||||
impl KillInfoBuilder {
|
||||
pub fn new() -> KillInfoBuilder {
|
||||
KillInfoBuilder { corporation_id: 0 }
|
||||
}
|
||||
|
||||
// used to prioritize attacker with this id
|
||||
pub fn set_corporation_id(&mut self, id: u32) {
|
||||
self.corporation_id = id;
|
||||
}
|
||||
|
||||
pub fn make_kill_info(
|
||||
&self,
|
||||
client: &ZkillClient,
|
||||
esi: &EsiClient,
|
||||
request: &KillmailRequest,
|
||||
) -> Result<KillInfo, ZkillError> {
|
||||
let total_value = request.value;
|
||||
let details = client.fetch_killmail_details(request)?;
|
||||
let attacker_list = details.attacker;
|
||||
let main_attacker: Option<Individual> =
|
||||
self.find_main_attacker(&attacker_list).and_then(|a| {
|
||||
// Try to build the Individual. If it fails, return None
|
||||
match Individual::build_with_esi(
|
||||
esi,
|
||||
a.character_id,
|
||||
a.corporation_id,
|
||||
a.alliance_id,
|
||||
a.ship_id,
|
||||
) {
|
||||
Ok(ind) => Some(ind),
|
||||
Err(_) => None, // ignore errors here
|
||||
}
|
||||
});
|
||||
|
||||
let victim = Individual::build_with_esi(
|
||||
esi,
|
||||
details.victim.character_id,
|
||||
details.victim.corporation_id,
|
||||
details.victim.alliance_id,
|
||||
details.victim.ship_id,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let system_name = esi
|
||||
.get_system(details.system_id)
|
||||
.unwrap_or("error".to_string());
|
||||
|
||||
Ok(KillInfo {
|
||||
victim,
|
||||
main_attacker,
|
||||
system_name,
|
||||
total_value,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_main_attacker(
|
||||
&self,
|
||||
attacker_list: &Vec<KillmailAttacker>,
|
||||
) -> Option<KillmailAttacker> {
|
||||
if attacker_list.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Try to find attackers from our corporation
|
||||
let mut filtered: Vec<&KillmailAttacker> = attacker_list
|
||||
.iter()
|
||||
.filter(|a| a.corporation_id == self.corporation_id)
|
||||
.collect();
|
||||
|
||||
// If none, fallback to all attackers
|
||||
if filtered.is_empty() {
|
||||
filtered = attacker_list.iter().collect();
|
||||
}
|
||||
|
||||
// Find attacker with max damage
|
||||
filtered.into_iter().max_by_key(|a| a.damage_done).copied() // copy the KillmailAttacker out of reference
|
||||
}
|
||||
}
|
||||
|
||||
impl Individual {
|
||||
pub fn build_with_esi(
|
||||
client: &EsiClient,
|
||||
char_id: u32,
|
||||
corp_id: u32,
|
||||
alli_id: u32,
|
||||
ship_id: u32,
|
||||
) -> Result<Individual, EsiError> {
|
||||
let character = client.get_character(char_id)?;
|
||||
let corporation = client.get_corporation(corp_id)?;
|
||||
|
||||
// Only try fetching alliance if alli_id != 0, else None
|
||||
let alliance = if alli_id != 0 {
|
||||
client.get_alliance(alli_id).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Only try fetching ship if ship_id != 0, else None
|
||||
let ship = if ship_id != 0 {
|
||||
client.get_ship(ship_id).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Individual {
|
||||
character,
|
||||
alliance,
|
||||
corporation,
|
||||
ship,
|
||||
})
|
||||
}
|
||||
}
|
||||
3
src/lib.rs
Normal file
3
src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod killinfo;
|
||||
pub mod model;
|
||||
pub mod services;
|
||||
122
src/main.rs
Normal file
122
src/main.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use embedded_graphics::{
|
||||
mono_font::{MonoTextStyle, ascii::FONT_6X10},
|
||||
pixelcolor::Gray8,
|
||||
};
|
||||
use image::{GrayImage, Pixel, RgbImage};
|
||||
|
||||
pub mod display;
|
||||
pub mod killinfo;
|
||||
pub mod model;
|
||||
pub mod services;
|
||||
use crate::killinfo::KillInfo;
|
||||
use crate::services::esi_static::EsiClient;
|
||||
use crate::services::zkill::ZkillClient;
|
||||
|
||||
fn rgb_to_gray8(img: &RgbImage) -> GrayImage {
|
||||
let mut gray = GrayImage::new(img.width(), img.height());
|
||||
for (x, y, pixel) in img.enumerate_pixels() {
|
||||
let luma = pixel.to_luma()[0]; // simple conversion
|
||||
gray.put_pixel(x, y, image::Luma([luma]));
|
||||
}
|
||||
gray
|
||||
}
|
||||
|
||||
// Example function to render kill info on a display buffer
|
||||
fn render_killmails(killmails: &[KillInfo], buffer: &mut GrayImage, x_off: i64) {
|
||||
let mut y_offset = 0;
|
||||
|
||||
for k in killmails {
|
||||
// Draw victim portrait
|
||||
|
||||
if let Some(alli) = &k.victim.alliance {
|
||||
let logo = &alli.logo;
|
||||
let gray_logo = rgb_to_gray8(logo);
|
||||
// copy portrait into buffer at (0, y_offset)
|
||||
image::imageops::overlay(buffer, &gray_logo, x_off + 0, y_offset as i64);
|
||||
}
|
||||
|
||||
if let Some(ship) = &k.victim.ship {
|
||||
let icon = &ship.icon;
|
||||
let gray_icon = rgb_to_gray8(icon);
|
||||
// copy portrait into buffer at (0, y_offset)
|
||||
image::imageops::overlay(buffer, &gray_icon, x_off + 64, y_offset as i64);
|
||||
}
|
||||
|
||||
// Draw victim name
|
||||
//let text_style = MonoTextStyle::new(&FONT_6X10, Gray8::new(255));
|
||||
// You can use embedded_graphics Text or a real framebuffer draw function
|
||||
// Example with embedded_graphics (requires DrawTarget)
|
||||
// Text::new(&k.victim.character.name, Point::new(50, y_offset as i32), text_style)
|
||||
// .draw(display)?;
|
||||
|
||||
// Draw system name and value
|
||||
println!(
|
||||
"{} {:.2}M ISK in {}",
|
||||
k.victim.character.name,
|
||||
k.total_value / 1_000_000.0,
|
||||
k.system_name
|
||||
);
|
||||
|
||||
// Advance y offset
|
||||
y_offset += 60; // adjust spacing
|
||||
if y_offset >= buffer.height() as usize {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Simulated 800x480 grayscale display buffer
|
||||
let mut buffer = GrayImage::new(800, 480);
|
||||
|
||||
let esi = EsiClient::new();
|
||||
let zkill = ZkillClient::new();
|
||||
|
||||
let past_seconds = 60 * 60 * 24;
|
||||
let my_corp_id = 98685373;
|
||||
|
||||
let mut display = display::Display::new();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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_kill_info(k, 400, y);
|
||||
}
|
||||
|
||||
display.flush();;
|
||||
|
||||
// Save buffer for debugging
|
||||
buffer.save("killmails_display.png").unwrap();
|
||||
}
|
||||
9
src/model/alliance.rs
Normal file
9
src/model/alliance.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use image::RgbImage;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Alliance {
|
||||
pub alliance_id: u32,
|
||||
pub name: String,
|
||||
pub short: String,
|
||||
pub logo: RgbImage,
|
||||
}
|
||||
8
src/model/character.rs
Normal file
8
src/model/character.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use image::RgbImage;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Character {
|
||||
pub character_id: u32,
|
||||
pub name: String,
|
||||
pub portrait: RgbImage,
|
||||
}
|
||||
9
src/model/corporation.rs
Normal file
9
src/model/corporation.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use image::RgbImage;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Corporation {
|
||||
pub corporation_id: u32,
|
||||
pub name: String,
|
||||
pub short: String,
|
||||
pub logo: RgbImage,
|
||||
}
|
||||
31
src/model/killmail.rs
Normal file
31
src/model/killmail.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KillmailRequest {
|
||||
pub id: u32,
|
||||
pub hash: String,
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct KillmailAttacker {
|
||||
pub character_id: u32,
|
||||
pub corporation_id: u32,
|
||||
pub alliance_id: u32,
|
||||
pub ship_id: u32,
|
||||
pub damage_done: u32,
|
||||
pub final_blow: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct KillmailVictim {
|
||||
pub character_id: u32,
|
||||
pub corporation_id: u32,
|
||||
pub alliance_id: u32,
|
||||
pub ship_id: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Killmail {
|
||||
pub system_id: u32,
|
||||
pub victim: KillmailVictim,
|
||||
pub attacker: Vec<KillmailAttacker>,
|
||||
}
|
||||
5
src/model/mod.rs
Normal file
5
src/model/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod alliance;
|
||||
pub mod character;
|
||||
pub mod corporation;
|
||||
pub mod killmail;
|
||||
pub mod ship;
|
||||
8
src/model/ship.rs
Normal file
8
src/model/ship.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use image::RgbImage;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ship {
|
||||
pub ship_id: u32,
|
||||
pub name: String,
|
||||
pub icon: RgbImage,
|
||||
}
|
||||
256
src/services/esi_static.rs
Normal file
256
src/services/esi_static.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
use image::load_from_memory;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
|
||||
use crate::model::alliance::Alliance;
|
||||
use crate::model::character::Character;
|
||||
use crate::model::corporation::Corporation;
|
||||
use crate::model::ship::Ship;
|
||||
|
||||
impl EsiClient {
|
||||
pub fn new() -> Self {
|
||||
let client = Client::builder()
|
||||
.user_agent("killpaper-hw,oli1111@web.de")
|
||||
.gzip(true)
|
||||
.brotli(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
reqwest_client: client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_alliance(&self, id: u32) -> Result<Alliance, EsiError> {
|
||||
#[derive(Deserialize)]
|
||||
struct AllianceInfo {
|
||||
name: String,
|
||||
ticker: 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()?;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EsiError {
|
||||
Http(reqwest::Error),
|
||||
Image(image::ImageError),
|
||||
Json(serde_json::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for EsiError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
EsiError::Http(e) => write!(f, "HTTP request error: {}", e),
|
||||
EsiError::Image(e) => write!(f, "Image decoding error: {}", e),
|
||||
EsiError::Json(e) => write!(f, "JSON parsing error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Conversions so we can use `?`
|
||||
impl From<reqwest::Error> for EsiError {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
EsiError::Http(e)
|
||||
}
|
||||
}
|
||||
impl From<image::ImageError> for EsiError {
|
||||
fn from(e: image::ImageError) -> Self {
|
||||
EsiError::Image(e)
|
||||
}
|
||||
}
|
||||
impl From<serde_json::Error> for EsiError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
EsiError::Json(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for EsiError {}
|
||||
pub struct EsiClient {
|
||||
pub(crate) reqwest_client: Client,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_alliance() -> Result<(), EsiError> {
|
||||
let esi = EsiClient::new();
|
||||
let alliance = esi.get_alliance(99002685)?;
|
||||
assert_eq!(alliance.alliance_id, 99002685);
|
||||
|
||||
println!("Alliance name: {:?}", alliance.name);
|
||||
println!("Alliance ticker: {:?}", alliance.short);
|
||||
println!("image {} {}", alliance.logo.width(), alliance.logo.height());
|
||||
|
||||
assert!(alliance.logo.width() > 0);
|
||||
assert!(alliance.logo.height() > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_corporation() -> Result<(), EsiError> {
|
||||
let esi = EsiClient::new();
|
||||
let corp = esi.get_corporation(98330748)?;
|
||||
assert_eq!(corp.corporation_id, 98330748);
|
||||
|
||||
println!("Corporation name: {:?}", corp.name);
|
||||
println!("Corporation ticker: {:?}", corp.short);
|
||||
println!("image {} {}", corp.logo.width(), corp.logo.height());
|
||||
|
||||
assert!(corp.logo.width() > 0);
|
||||
assert!(corp.logo.height() > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_character() -> Result<(), EsiError> {
|
||||
let esi = EsiClient::new();
|
||||
let character = esi.get_character(90951867)?;
|
||||
|
||||
assert_eq!(character.character_id, 90951867);
|
||||
|
||||
println!("Character name: {:?}", character.name);
|
||||
println!(
|
||||
"image {} {}",
|
||||
character.portrait.width(),
|
||||
character.portrait.height()
|
||||
);
|
||||
|
||||
assert!(character.portrait.width() > 0);
|
||||
assert!(character.portrait.height() > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_ship() -> Result<(), EsiError> {
|
||||
let esi = EsiClient::new();
|
||||
let ship = esi.get_ship(587)?; // Rifter
|
||||
|
||||
assert_eq!(ship.ship_id, 587);
|
||||
println!("Ship: {}", ship.name);
|
||||
println!("image {} {}", ship.icon.width(), ship.icon.height());
|
||||
|
||||
assert!(ship.icon.width() > 0);
|
||||
assert!(ship.icon.height() > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_system() -> Result<(), EsiError> {
|
||||
let esi = EsiClient::new();
|
||||
let system_name = esi.get_system(30000142)?; // Jita
|
||||
|
||||
assert_eq!(system_name, "Jita");
|
||||
println!("System name: {}", system_name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
2
src/services/mod.rs
Normal file
2
src/services/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod esi_static;
|
||||
pub mod zkill;
|
||||
214
src/services/zkill.rs
Normal file
214
src/services/zkill.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
|
||||
use crate::model::killmail::*;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ZkbKill {
|
||||
killmail_id: u32,
|
||||
zkb: ZkbInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ZkbInfo {
|
||||
hash: String,
|
||||
totalValue: f64,
|
||||
}
|
||||
|
||||
pub struct ZkillClient {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
// ESI API deserialization structs
|
||||
#[derive(Deserialize)]
|
||||
struct EsiAttacker {
|
||||
character_id: Option<u32>,
|
||||
corporation_id: Option<u32>,
|
||||
alliance_id: Option<u32>,
|
||||
ship_type_id: Option<u32>,
|
||||
damage_done: Option<u32>,
|
||||
final_blow: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EsiVictim {
|
||||
character_id: Option<u32>,
|
||||
corporation_id: Option<u32>,
|
||||
alliance_id: Option<u32>,
|
||||
ship_type_id: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EsiKillmail {
|
||||
solar_system_id: u32,
|
||||
attackers: Vec<EsiAttacker>,
|
||||
victim: EsiVictim,
|
||||
}
|
||||
|
||||
// Custom error type
|
||||
#[derive(Debug)]
|
||||
pub enum ZkillError {
|
||||
Reqwest(reqwest::Error),
|
||||
Json(serde_json::Error),
|
||||
NotFound(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for ZkillError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ZkillError::Reqwest(e) => write!(f, "Request failed: {}", e),
|
||||
ZkillError::Json(e) => write!(f, "JSON parsing failed: {}", e),
|
||||
ZkillError::NotFound(msg) => write!(f, "Not found: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ZkillError {}
|
||||
|
||||
impl From<reqwest::Error> for ZkillError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
ZkillError::Reqwest(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ZkillError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ZkillError::Json(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl ZkillClient {
|
||||
pub fn new() -> Self {
|
||||
let client = Client::builder()
|
||||
.user_agent("killpaper-hw,oli1111@web.de")
|
||||
.gzip(true)
|
||||
.brotli(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
ZkillClient { client }
|
||||
}
|
||||
|
||||
pub fn get_corporation_kills(
|
||||
&self,
|
||||
corp_id: u32,
|
||||
past_seconds: u32,
|
||||
) -> Result<Vec<KillmailRequest>, ZkillError> {
|
||||
let url = format!(
|
||||
"https://zkillboard.com/api/kills/corporationID/{}/pastSeconds/{}/",
|
||||
corp_id, past_seconds
|
||||
);
|
||||
println!("REQ ZKILL {url}");
|
||||
let response = self.client.get(&url).send()?.text()?;
|
||||
let kills: Vec<ZkbKill> = serde_json::from_str(&response)?;
|
||||
|
||||
let result = kills
|
||||
.into_iter()
|
||||
.map(|k| KillmailRequest {
|
||||
id: k.killmail_id,
|
||||
hash: k.zkb.hash,
|
||||
value: k.zkb.totalValue,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn get_corporation_losses(
|
||||
&self,
|
||||
corp_id: u32,
|
||||
past_seconds: u32,
|
||||
) -> Result<Vec<KillmailRequest>, ZkillError> {
|
||||
let url = format!(
|
||||
"https://zkillboard.com/api/losses/corporationID/{}/pastSeconds/{}/",
|
||||
corp_id, past_seconds
|
||||
);
|
||||
println!("REQ ZKILL {url}");
|
||||
let response = self.client.get(&url).send()?.text()?;
|
||||
let kills: Vec<ZkbKill> = serde_json::from_str(&response)?;
|
||||
|
||||
let result = kills
|
||||
.into_iter()
|
||||
.map(|k| KillmailRequest {
|
||||
id: k.killmail_id,
|
||||
hash: k.zkb.hash,
|
||||
value: k.zkb.totalValue,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn fetch_killmail_details(&self, req: &KillmailRequest) -> Result<Killmail, ZkillError> {
|
||||
let url = format!(
|
||||
"https://esi.evetech.net/latest/killmails/{}/{}/",
|
||||
req.id, req.hash
|
||||
);
|
||||
println!("REQ ZKILL {url}");
|
||||
let esi_killmail: EsiKillmail = self.client.get(&url).send()?.json()?;
|
||||
|
||||
let attackers = esi_killmail
|
||||
.attackers
|
||||
.into_iter()
|
||||
.map(|a| KillmailAttacker {
|
||||
character_id: a.character_id.unwrap_or(0),
|
||||
corporation_id: a.corporation_id.unwrap_or(0),
|
||||
alliance_id: a.alliance_id.unwrap_or(0),
|
||||
ship_id: a.ship_type_id.unwrap_or(0),
|
||||
damage_done: a.damage_done.unwrap_or(0),
|
||||
final_blow: a.final_blow.unwrap_or(false),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let victim = KillmailVictim {
|
||||
character_id: esi_killmail.victim.character_id.unwrap_or(0),
|
||||
corporation_id: esi_killmail.victim.corporation_id.unwrap_or(0),
|
||||
alliance_id: esi_killmail.victim.alliance_id.unwrap_or(0),
|
||||
ship_id: esi_killmail.victim.ship_type_id.unwrap_or(0),
|
||||
};
|
||||
|
||||
Ok(Killmail {
|
||||
system_id: esi_killmail.solar_system_id,
|
||||
victim,
|
||||
attacker: attackers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_corporation_kills() {
|
||||
let zkill = ZkillClient::new();
|
||||
let kills = zkill.get_corporation_kills(98330748, 86400).unwrap();
|
||||
|
||||
println!("Found {} kills", kills.len());
|
||||
for k in kills.iter().take(5) {
|
||||
println!("Kill ID: {}, Hash: {}, Value: {}", k.id, k.hash, k.value);
|
||||
assert!(k.value > 0.0);
|
||||
assert!(!k.hash.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_killmail_details() {
|
||||
let zkill = ZkillClient::new();
|
||||
// Replace with a valid killmail ID & hash for testing
|
||||
let req = KillmailRequest {
|
||||
id: 129572369,
|
||||
hash: "5c95b74245f3fa762dcad7f2dee73581ba885195".to_string(),
|
||||
value: 52895174.48,
|
||||
};
|
||||
let result = zkill.fetch_killmail_details(&req);
|
||||
match result {
|
||||
Ok(kill) => {
|
||||
println!("Kill system ID: {}", kill.system_id);
|
||||
assert!(kill.attacker.len() > 0 || kill.victim.ship_id > 0);
|
||||
}
|
||||
Err(e) => println!("Error fetching killmail: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user