diff --git a/src/configuration.rs b/src/configuration.rs index ec75d24..9370987 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -29,8 +29,13 @@ impl Configuration { pub struct GeneralConfiguration { #[serde(default = "default_true")] pub enable: bool, - #[serde(default)] + + #[serde(default = "default_true")] pub export_class: bool, + + #[serde(default = "default_false")] + pub fail_over_lamp: bool, + #[serde(default = "default_timeout")] pub timeout: u64, } @@ -39,6 +44,10 @@ fn default_true() -> bool { true } +fn default_false() -> bool { + false +} + fn default_timeout() -> u64 { 3000 } diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/helpers.rs b/src/helpers.rs index dccdb74..5327b64 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -129,7 +129,6 @@ pub fn read_hinternet_url(handle: HINTERNET) -> Result { Err(anyhow!("Could not get URL from HINTERNET handle: {ec}")) } -/// Read all bytes of a slice into a buffer. pub fn read_slice(buf: *const u8, len: usize) -> Result> { let mut slice = unsafe { std::slice::from_raw_parts(buf, len) }; let mut ret = Vec::with_capacity(len); diff --git a/src/saekawa.rs b/src/saekawa.rs index ee57050..58112df 100644 --- a/src/saekawa.rs +++ b/src/saekawa.rs @@ -20,7 +20,7 @@ use crate::{ }, types::{ game::UpsertUserAllRequest, - tachi::{ClassEmblem, Import, ImportClasses, ImportScore}, + tachi::{ClassEmblem, Difficulty, Import, ImportClasses, ImportScore}, }, CONFIGURATION, TACHI_IMPORT_URL, TACHI_STATUS_URL, UPSERT_USER_ALL_API_ENCRYPTED, }; @@ -41,6 +41,16 @@ pub fn hook_init() -> Result<()> { .as_u64() .ok_or(anyhow::anyhow!("Couldn't parse user from Tachi response"))?; + let mut permissions = resp["body"]["permissions"] + .as_array() + .ok_or(anyhow!("Couldn't parse permissions from Tachi response"))? + .into_iter() + .filter_map(|v| v.as_str()); + + if permissions.all(|v| v != "submit_score") { + return Err(anyhow!("API key has insufficient permission. The permission submit_score must be set.")); + } + info!("Logged in to Tachi with userID {user_id}"); let winhttpwritedata = unsafe { @@ -50,12 +60,13 @@ pub fn hook_init() -> Result<()> { }; unsafe { - DetourWriteData.initialize(winhttpwritedata, winhttpwritedata_hook)?; - - DetourWriteData.enable()?; + DetourWriteData + .initialize(winhttpwritedata, winhttpwritedata_hook)? + .enable()?; }; info!("Hook successfully initialized"); + Ok(()) } @@ -191,11 +202,13 @@ fn winhttpwritedata_hook( .user_playlog_list .into_iter() .filter_map(|playlog| { - if let Ok(score) = ImportScore::try_from(playlog) { - if score.difficulty.as_str() == "WORLD'S END" { - return None; - } - Some(score) + let result = + ImportScore::try_from_playlog(playlog, CONFIGURATION.general.fail_over_lamp); + if result + .as_ref() + .is_ok_and(|v| v.difficulty != Difficulty::WorldsEnd) + { + result.ok() } else { None } diff --git a/src/types/tachi.rs b/src/types/tachi.rs index aa55c71..d40d5e7 100644 --- a/src/types/tachi.rs +++ b/src/types/tachi.rs @@ -1,4 +1,4 @@ -use anyhow::anyhow; +use anyhow::Result; use chrono::{FixedOffset, NaiveDateTime, TimeZone}; use num_enum::TryFromPrimitive; use serde::{Deserialize, Serialize}; @@ -68,7 +68,7 @@ pub struct ImportScore { pub lamp: TachiLamp, pub match_type: String, pub identifier: String, - pub difficulty: String, + pub difficulty: Difficulty, pub time_achieved: u128, pub judgements: Judgements, pub optional: OptionalMetrics, @@ -93,6 +93,28 @@ pub enum TachiLamp { AllJusticeCritical = 4, } +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, TryFromPrimitive)] +#[repr(u32)] +pub enum Difficulty { + #[serde(rename = "BASIC")] + Basic = 0, + + #[serde(rename = "ADVANCED")] + Advanced = 1, + + #[serde(rename = "EXPERT")] + Expert = 2, + + #[serde(rename = "MASTER")] + Master = 3, + + #[serde(rename = "ULTIMA")] + Ultima = 4, + + #[serde(rename = "WORLD'S END")] + WorldsEnd = 5, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Judgements { pub jcrit: u32, @@ -107,11 +129,11 @@ pub struct OptionalMetrics { pub max_combo: u32, } -impl TryFrom for ImportScore { - type Error = anyhow::Error; - - fn try_from(p: UserPlaylog) -> Result { - let lamp = if p.is_all_justice { +impl ImportScore { + pub fn try_from_playlog(p: UserPlaylog, fail_over_lamp: bool) -> Result { + let lamp = if !p.is_clear && fail_over_lamp { + TachiLamp::Failed + } else if p.is_all_justice { if p.judge_justice + p.judge_attack + p.judge_guilty == 0 { TachiLamp::AllJusticeCritical } else { @@ -133,23 +155,11 @@ impl TryFrom for ImportScore { }; let rom_major_version = p.rom_version.split('.').next().unwrap_or("2"); - let difficulty = match p.level { - 0 => "BASIC", - 1 => "ADVANCED", - 2 => "EXPERT", - 3 => "MASTER", - 4 => if rom_major_version == "2" { - "ULTIMA" - } else { - "WORLD'S END" - }, - 5 => if rom_major_version == "2" { - "WORLD'S END" - } else { - return Err(anyhow!("difficulty index '5' should not be possible on rom_version {rom_major_version}.")); - }, - _ => return Err(anyhow!("unknown difficulty index {level} on major version {rom_major_version}", level=p.level)), - }.to_string(); + let difficulty = if rom_major_version == "1" && p.level == 4 { + Difficulty::WorldsEnd + } else { + Difficulty::try_from(p.level)? + }; let datetime = NaiveDateTime::parse_from_str(&p.user_play_date, "%Y-%m-%d %H:%M:%S")?; let jst_offset =