mirror of
https://github.com/adamaq01/mikado.git
synced 2024-11-27 15:40:49 +01:00
feat: release first version
This commit is contained in:
commit
ca59382377
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
/Cargo.lock
|
||||||
|
/.idea
|
32
Cargo.toml
Normal file
32
Cargo.toml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
[package]
|
||||||
|
name = "mikado"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Adam Thibert <adamthibert01@gmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true # Automatically strip symbols from the binary.
|
||||||
|
lto = true # Enable link-time optimization.
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
winapi = { version = "0.3", features = ["minwindef", "windef", "winuser", "libloaderapi", "processthreadsapi", "winbase", "consoleapi"] }
|
||||||
|
crochet = "0.2"
|
||||||
|
log = "0.4"
|
||||||
|
env_logger = "0.10"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
confy = "0.5"
|
||||||
|
anyhow = "1.0"
|
||||||
|
lazy_static = "1.4"
|
||||||
|
ureq = { version = "2.6", features = ["json"] }
|
||||||
|
url = "2.3"
|
||||||
|
either = { version = "1.8", features = ["serde"] }
|
||||||
|
num_enum = "0.6"
|
||||||
|
chrono = "0.4"
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Adam Thibert
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
20
README.md
Normal file
20
README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Mikado
|
||||||
|
|
||||||
|
Little SDVX hook to submit your scores to a Tachi instance while you are playing.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
- Download the latest release from the [releases page](https://github.com/adamaq01/mikado/releases/latest)
|
||||||
|
- Extract the zip file into your SDVX installation folder
|
||||||
|
- When you start the game, inject the DLL into the process
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- The configuration file will be created in the same folder as the DLL at startup if it doesn't already exist
|
||||||
|
- You can configure some options (like the Tachi URL) by editing the `mikado.toml` file
|
||||||
|
- If you are using Spicetools, you can add the `-k mikado.dll` option or specify the DLL in the configuration tool to
|
||||||
|
automatically inject it at startup
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
23
mikado.toml
Normal file
23
mikado.toml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[general]
|
||||||
|
# Set to 'false' to disable the hook
|
||||||
|
enable = true
|
||||||
|
# Whether the hook should export your class (skill level) or not
|
||||||
|
export_class = true
|
||||||
|
|
||||||
|
[cards]
|
||||||
|
# Card numbers that should be whitelisted
|
||||||
|
# If this is empty, all cards will be whitelisted
|
||||||
|
whitelist = []
|
||||||
|
|
||||||
|
[tachi]
|
||||||
|
# Tachi instance base URL
|
||||||
|
base_url = 'https://kamaitachi.xyz/'
|
||||||
|
|
||||||
|
# Tachi status endpoint
|
||||||
|
status = '/api/v1/status'
|
||||||
|
|
||||||
|
# Tachi score import endpoint
|
||||||
|
import = '/ir/direct-manual/import'
|
||||||
|
|
||||||
|
# Your Tachi API key
|
||||||
|
api_key = 'your-key-here'
|
44
src/configuration.rs
Normal file
44
src/configuration.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Configuration {
|
||||||
|
pub general: GeneralConfiguration,
|
||||||
|
pub cards: CardsConfiguration,
|
||||||
|
pub tachi: TachiConfiguration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Configuration {
|
||||||
|
pub fn load() -> Result<Self> {
|
||||||
|
if !Path::new("mikado.toml").exists() {
|
||||||
|
File::create("mikado.toml")
|
||||||
|
.and_then(|mut file| file.write_all(include_bytes!("../mikado.toml")))
|
||||||
|
.map_err(|err| anyhow::anyhow!("Could not create default config file: {}", err))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
confy::load_path("mikado.toml")
|
||||||
|
.map_err(|err| anyhow::anyhow!("Could not load config: {}", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct GeneralConfiguration {
|
||||||
|
pub enable: bool,
|
||||||
|
pub export_class: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct CardsConfiguration {
|
||||||
|
pub whitelist: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct TachiConfiguration {
|
||||||
|
pub base_url: String,
|
||||||
|
pub status: String,
|
||||||
|
pub import: String,
|
||||||
|
pub api_key: String,
|
||||||
|
}
|
34
src/lib.rs
Normal file
34
src/lib.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
mod configuration;
|
||||||
|
mod log;
|
||||||
|
mod mikado;
|
||||||
|
mod save;
|
||||||
|
mod scores;
|
||||||
|
mod sys;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
use crate::mikado::{hook_init, hook_release};
|
||||||
|
use ::log::error;
|
||||||
|
use winapi::shared::minwindef::{BOOL, DWORD, HINSTANCE, LPVOID, TRUE};
|
||||||
|
use winapi::um::consoleapi::AllocConsole;
|
||||||
|
use winapi::um::winnt::{DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH};
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[allow(non_snake_case, unused_variables)]
|
||||||
|
extern "system" fn DllMain(dll_module: HINSTANCE, call_reason: DWORD, reserved: LPVOID) -> BOOL {
|
||||||
|
match call_reason {
|
||||||
|
DLL_PROCESS_ATTACH => {
|
||||||
|
unsafe { AllocConsole() };
|
||||||
|
if let Err(err) = hook_init() {
|
||||||
|
error!("{:#}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DLL_PROCESS_DETACH => {
|
||||||
|
if let Err(err) = hook_release() {
|
||||||
|
error!("{:#}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
TRUE
|
||||||
|
}
|
71
src/log.rs
Normal file
71
src/log.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Logger {
|
||||||
|
file: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Logger {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
file: File::create("mikado.log").unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for Logger {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
// Ignore the result of the write to stdout, since it's not really important
|
||||||
|
let _ = std::io::stdout().write(buf);
|
||||||
|
self.file.write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
// Ignore the result of the write to stdout, since it's not really important
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
self.file.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Padded<T> {
|
||||||
|
pub(crate) value: T,
|
||||||
|
pub(crate) width: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: fmt::Display> fmt::Display for Padded<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{: <width$}", self.value, width = self.width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) static MAX_MODULE_WIDTH: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
|
pub(crate) fn max_target_width(target: &str) -> usize {
|
||||||
|
let max_width = MAX_MODULE_WIDTH.load(Ordering::Relaxed);
|
||||||
|
if max_width < target.len() {
|
||||||
|
MAX_MODULE_WIDTH.store(target.len(), Ordering::Relaxed);
|
||||||
|
target.len()
|
||||||
|
} else {
|
||||||
|
max_width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn colored_level(
|
||||||
|
style: &mut env_logger::fmt::Style,
|
||||||
|
level: log::Level,
|
||||||
|
) -> env_logger::fmt::StyledValue<&'static str> {
|
||||||
|
match level {
|
||||||
|
log::Level::Trace => style
|
||||||
|
.set_color(env_logger::fmt::Color::Magenta)
|
||||||
|
.value("TRACE"),
|
||||||
|
log::Level::Debug => style.set_color(env_logger::fmt::Color::Blue).value("DEBUG"),
|
||||||
|
log::Level::Info => style.set_color(env_logger::fmt::Color::Green).value("INFO"),
|
||||||
|
log::Level::Warn => style
|
||||||
|
.set_color(env_logger::fmt::Color::Yellow)
|
||||||
|
.value("WARN"),
|
||||||
|
log::Level::Error => style.set_color(env_logger::fmt::Color::Red).value("ERROR"),
|
||||||
|
}
|
||||||
|
}
|
233
src/mikado.rs
Normal file
233
src/mikado.rs
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
use crate::configuration::Configuration;
|
||||||
|
use crate::log::Logger;
|
||||||
|
use crate::save::process_course;
|
||||||
|
use crate::scores::process_scores;
|
||||||
|
use crate::sys::{
|
||||||
|
property_clear_error, property_mem_write, property_node_name, property_node_refer,
|
||||||
|
property_query_size, property_search, property_set_flag, NodeType,
|
||||||
|
};
|
||||||
|
use crate::types::game::Property;
|
||||||
|
use crate::types::tachi::Import;
|
||||||
|
use anyhow::Result;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use log::{debug, error, info};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref CONFIGURATION: Configuration = {
|
||||||
|
let result = Configuration::load();
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("{:#}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.unwrap()
|
||||||
|
};
|
||||||
|
pub static ref TACHI_STATUS_URL: Url = {
|
||||||
|
let result = Url::parse(&CONFIGURATION.tachi.base_url)
|
||||||
|
.and_then(|url| url.join(&CONFIGURATION.tachi.status));
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("Could not parse Tachi status URL: {:#}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.unwrap()
|
||||||
|
};
|
||||||
|
pub static ref TACHI_IMPORT_URL: Url = {
|
||||||
|
let result = Url::parse(&CONFIGURATION.tachi.base_url)
|
||||||
|
.and_then(|url| url.join(&CONFIGURATION.tachi.import));
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("Could not parse Tachi import URL: {:#}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.unwrap()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_import(import: Import) -> Result<()> {
|
||||||
|
debug!("Trying to import to Tachi: {:#?}", import);
|
||||||
|
let authorization = format!("Bearer {}", CONFIGURATION.tachi.api_key);
|
||||||
|
let response = ureq::post(TACHI_IMPORT_URL.as_str())
|
||||||
|
.set("Authorization", authorization.as_str())
|
||||||
|
.send_json(import)
|
||||||
|
.map_err(|err| anyhow::anyhow!("Could not reach Tachi API: {:#}", err))?;
|
||||||
|
debug!("Tachi API status response: {:#?}", response.into_string()?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hook_init() -> Result<()> {
|
||||||
|
if !CONFIGURATION.general.enable {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuring logger
|
||||||
|
env_logger::builder()
|
||||||
|
.filter_level(log::LevelFilter::Info)
|
||||||
|
.parse_default_env()
|
||||||
|
.target(env_logger::Target::Pipe(Box::new(Logger::new())))
|
||||||
|
.format(|f, record| {
|
||||||
|
use crate::log::{colored_level, max_target_width, Padded};
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let target = record.target();
|
||||||
|
let max_width = max_target_width(target);
|
||||||
|
|
||||||
|
let mut style = f.style();
|
||||||
|
let level = colored_level(&mut style, record.level());
|
||||||
|
|
||||||
|
let mut style = f.style();
|
||||||
|
let target = style.set_bold(true).value(Padded {
|
||||||
|
value: target,
|
||||||
|
width: max_width,
|
||||||
|
});
|
||||||
|
|
||||||
|
let time = chrono::Local::now().format("%d/%m/%Y %H:%M:%S");
|
||||||
|
|
||||||
|
writeln!(f, "[{}] {} {} -> {}", time, level, target, record.args())
|
||||||
|
})
|
||||||
|
.init();
|
||||||
|
|
||||||
|
// Trying to reach Tachi API
|
||||||
|
debug!("Trying to reach Tachi API at {}", TACHI_STATUS_URL.as_str());
|
||||||
|
let authorization = format!("Bearer {}", CONFIGURATION.tachi.api_key);
|
||||||
|
let response = ureq::get(TACHI_STATUS_URL.as_str())
|
||||||
|
.set("Authorization", authorization.as_str())
|
||||||
|
.call()
|
||||||
|
.map_err(|err| anyhow::anyhow!("Could not reach Tachi API: {:#}", err))?;
|
||||||
|
debug!("Tachi API status response: {:#?}", response.into_string()?);
|
||||||
|
info!("Tachi API successfully reached");
|
||||||
|
|
||||||
|
// Initializing function detours
|
||||||
|
crochet::enable!(property_destroy_hook)
|
||||||
|
.map_err(|err| anyhow::anyhow!("Could not enable function detour: {:#}", err))?;
|
||||||
|
|
||||||
|
info!("Hook successfully initialized");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hook_release() -> Result<()> {
|
||||||
|
if !CONFIGURATION.general.enable {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
crochet::disable!(property_destroy_hook)
|
||||||
|
.map_err(|err| anyhow::anyhow!("Could not disable function detour: {:#}", err))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[crochet::hook("avs2-core.dll", "XCgsqzn0000091")]
|
||||||
|
pub unsafe fn property_destroy_hook(property: *mut ()) -> i32 {
|
||||||
|
if property.is_null() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let game_node = property_search(property, std::ptr::null(), b"/call/game\0".as_ptr());
|
||||||
|
if game_node.is_null() {
|
||||||
|
property_clear_error(property);
|
||||||
|
return call_original!(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = [0u8; 256];
|
||||||
|
let result = property_node_name(game_node, buffer.as_mut_ptr(), buffer.len() as u32);
|
||||||
|
if result < 0 {
|
||||||
|
return call_original!(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = {
|
||||||
|
let result = std::str::from_utf8(&buffer[0..4]);
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("Could not convert buffer to string: {:#}", err);
|
||||||
|
return call_original!(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.unwrap()
|
||||||
|
};
|
||||||
|
if name != "game" {
|
||||||
|
return call_original!(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = property_node_refer(
|
||||||
|
property,
|
||||||
|
game_node,
|
||||||
|
b"method@\0".as_ptr(),
|
||||||
|
NodeType::NodeAttr,
|
||||||
|
buffer.as_mut_ptr() as *mut (),
|
||||||
|
256,
|
||||||
|
);
|
||||||
|
if result < 0 {
|
||||||
|
return call_original!(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
let method = {
|
||||||
|
let result = std::str::from_utf8(&buffer[0..11]);
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("Could not convert buffer to string: {:#}", err);
|
||||||
|
return call_original!(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.unwrap().replace('\0', "")
|
||||||
|
};
|
||||||
|
debug!("Intercepted Game Method: {}", method);
|
||||||
|
if method != "sv6_save_m" && (!CONFIGURATION.general.export_class || method != "sv6_save") {
|
||||||
|
return call_original!(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
property_set_flag(property, 0x800, 0x008);
|
||||||
|
|
||||||
|
let size = property_query_size(property);
|
||||||
|
if size < 0 {
|
||||||
|
property_set_flag(property, 0x008, 0x800);
|
||||||
|
return call_original!(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = vec![0u8; size as usize];
|
||||||
|
let result = property_mem_write(property, buffer.as_ptr() as *mut u8, buffer.len() as u32);
|
||||||
|
property_set_flag(property, 0x008, 0x800);
|
||||||
|
if result < 0 {
|
||||||
|
return call_original!(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read buf to string
|
||||||
|
let property_str = {
|
||||||
|
let result = std::str::from_utf8(&buffer);
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("Could not convert buffer to string: {:#}", err);
|
||||||
|
return call_original!(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("Processing property: {}", property_str);
|
||||||
|
if let Err(err) = match method.as_str() {
|
||||||
|
"sv6_save_m" => serde_json::from_str::<Property>(property_str)
|
||||||
|
.map_err(|err| anyhow::anyhow!("Could not parse property: {:#}", err))
|
||||||
|
.and_then(|prop| {
|
||||||
|
process_scores(
|
||||||
|
prop.call
|
||||||
|
.game
|
||||||
|
.left()
|
||||||
|
.ok_or(anyhow::anyhow!("Could not process scores property"))?,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
"sv6_save" => serde_json::from_str::<Property>(property_str)
|
||||||
|
.map_err(|err| anyhow::anyhow!("Could not parse property: {:#}", err))
|
||||||
|
.and_then(|prop| {
|
||||||
|
process_course(
|
||||||
|
prop.call
|
||||||
|
.game
|
||||||
|
.right()
|
||||||
|
.ok_or(anyhow::anyhow!("Could not process course property"))?,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
_ => unreachable!(),
|
||||||
|
} {
|
||||||
|
error!("{:#}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
call_original!(property)
|
||||||
|
}
|
26
src/save.rs
Normal file
26
src/save.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use crate::mikado::{send_import, CONFIGURATION};
|
||||||
|
use crate::types::game::GameSave;
|
||||||
|
use crate::types::tachi::{Import, ImportClasses, SkillLevel};
|
||||||
|
use anyhow::Result;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
pub fn process_course(course: GameSave) -> Result<()> {
|
||||||
|
let card = course.ref_id;
|
||||||
|
if !CONFIGURATION.cards.whitelist.is_empty() && !CONFIGURATION.cards.whitelist.contains(&card) {
|
||||||
|
info!("Card {} is not whitelisted, skipping class update", card);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let import = Import {
|
||||||
|
meta: Default::default(),
|
||||||
|
classes: Some(ImportClasses {
|
||||||
|
dan: SkillLevel::from(course.skill_level),
|
||||||
|
}),
|
||||||
|
scores: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
send_import(import)?;
|
||||||
|
info!("Successfully updated class for card {}", card);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
59
src/scores.rs
Normal file
59
src/scores.rs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
use crate::mikado::{send_import, CONFIGURATION};
|
||||||
|
use crate::types::game::GameScores;
|
||||||
|
use crate::types::tachi::{Difficulty, HitMeta, Import, ImportScore, Judgements, TachiLamp};
|
||||||
|
use anyhow::Result;
|
||||||
|
use either::Either;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
pub fn process_scores(scores: GameScores) -> Result<()> {
|
||||||
|
let card = scores.ref_id;
|
||||||
|
if !CONFIGURATION.cards.whitelist.is_empty() && !CONFIGURATION.cards.whitelist.contains(&card) {
|
||||||
|
info!("Card {} is not whitelisted, skipping score(s)", card);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let tracks = match scores.tracks {
|
||||||
|
Either::Left(track) => vec![track],
|
||||||
|
Either::Right(tracks) => tracks,
|
||||||
|
};
|
||||||
|
|
||||||
|
let time_achieved = std::time::UNIX_EPOCH
|
||||||
|
.elapsed()
|
||||||
|
.map(|duration| duration.as_millis())
|
||||||
|
.map_err(|err| anyhow::anyhow!("Could not get time from System {:#}", err))?;
|
||||||
|
|
||||||
|
let scores = tracks
|
||||||
|
.into_iter()
|
||||||
|
.map(|track| ImportScore {
|
||||||
|
score: track.score,
|
||||||
|
lamp: TachiLamp::from(track.clear_type),
|
||||||
|
match_type: "sdvxInGameID".to_string(),
|
||||||
|
identifier: track.music_id.to_string(),
|
||||||
|
difficulty: Difficulty::from(track.music_type),
|
||||||
|
time_achieved,
|
||||||
|
judgements: Judgements {
|
||||||
|
critical: track.critical,
|
||||||
|
near: track.near,
|
||||||
|
miss: track.error,
|
||||||
|
},
|
||||||
|
hit_meta: HitMeta {
|
||||||
|
fast: track.judge[0],
|
||||||
|
slow: track.judge[6],
|
||||||
|
max_combo: track.max_chain,
|
||||||
|
ex_score: track.ex_score,
|
||||||
|
gauge: track.effective_rate as f32 / 100.0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let import = Import {
|
||||||
|
meta: Default::default(),
|
||||||
|
classes: None,
|
||||||
|
scores,
|
||||||
|
};
|
||||||
|
|
||||||
|
send_import(import)?;
|
||||||
|
info!("Successfully imported score(s) for card {}", card);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
96
src/sys.rs
Normal file
96
src/sys.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
#[crochet::load("avs2-core.dll")]
|
||||||
|
extern "C" {
|
||||||
|
#[symbol("XCgsqzn0000091")]
|
||||||
|
pub fn property_destroy(property: *mut ()) -> i32;
|
||||||
|
#[symbol("XCgsqzn000009a")]
|
||||||
|
pub fn property_set_flag(property: *mut (), set_flags: u32, clear_flags: u32) -> u32;
|
||||||
|
#[symbol("XCgsqzn000009d")]
|
||||||
|
pub fn property_clear_error(property: *mut ()) -> *mut ();
|
||||||
|
#[symbol("XCgsqzn000009f")]
|
||||||
|
pub fn property_query_size(property: *const ()) -> i32;
|
||||||
|
#[symbol("XCgsqzn00000a1")]
|
||||||
|
pub fn property_search(property: *const (), node: *const (), path: *const u8) -> *mut ();
|
||||||
|
#[symbol("XCgsqzn00000a7")]
|
||||||
|
pub fn property_node_name(node: *const (), buffer: *mut u8, size: u32) -> i32;
|
||||||
|
#[symbol("XCgsqzn00000ab")]
|
||||||
|
pub fn property_node_read(
|
||||||
|
node: *const (),
|
||||||
|
node_type: NodeType,
|
||||||
|
data: *mut (),
|
||||||
|
size: u32,
|
||||||
|
) -> i32;
|
||||||
|
#[symbol("XCgsqzn00000af")]
|
||||||
|
pub fn property_node_refer(
|
||||||
|
property: *const (),
|
||||||
|
node: *const (),
|
||||||
|
path: *const u8,
|
||||||
|
node_type: NodeType,
|
||||||
|
data: *mut (),
|
||||||
|
size: u32,
|
||||||
|
) -> i32;
|
||||||
|
#[symbol("XCgsqzn00000b8")]
|
||||||
|
pub fn property_mem_write(property: *mut (), data: *mut u8, size: u32) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum NodeType {
|
||||||
|
NodeNode = 1,
|
||||||
|
NodeS8 = 2,
|
||||||
|
NodeU8 = 3,
|
||||||
|
NodeS16 = 4,
|
||||||
|
NodeU16 = 5,
|
||||||
|
NodeS32 = 6,
|
||||||
|
NodeU32 = 7,
|
||||||
|
NodeS64 = 8,
|
||||||
|
NodeU64 = 9,
|
||||||
|
NodeBin = 10,
|
||||||
|
NodeStr = 11,
|
||||||
|
NodeIp4 = 12,
|
||||||
|
NodeTime = 13,
|
||||||
|
NodeFloat = 14,
|
||||||
|
NodeDouble = 15,
|
||||||
|
Node2s8 = 16,
|
||||||
|
Node2u8 = 17,
|
||||||
|
Node2s16 = 18,
|
||||||
|
Node2u16 = 19,
|
||||||
|
Node2s32 = 20,
|
||||||
|
Node2u32 = 21,
|
||||||
|
Node2s64 = 22,
|
||||||
|
Node2u64 = 23,
|
||||||
|
Node2f = 24,
|
||||||
|
Node2d = 25,
|
||||||
|
Node3s8 = 26,
|
||||||
|
Node3u8 = 27,
|
||||||
|
Node3s16 = 28,
|
||||||
|
Node3u16 = 29,
|
||||||
|
Node3s32 = 30,
|
||||||
|
Node3u32 = 31,
|
||||||
|
Node3s64 = 32,
|
||||||
|
Node3u64 = 33,
|
||||||
|
Node3f = 34,
|
||||||
|
Node3d = 35,
|
||||||
|
Node4s8 = 36,
|
||||||
|
Node4u8 = 37,
|
||||||
|
Node4s16 = 38,
|
||||||
|
Node4u16 = 39,
|
||||||
|
Node4s32 = 40,
|
||||||
|
Node4u32 = 41,
|
||||||
|
Node4s64 = 42,
|
||||||
|
Node4u64 = 43,
|
||||||
|
Node4f = 44,
|
||||||
|
Node4d = 45,
|
||||||
|
NodeAttr = 46,
|
||||||
|
NodeAttrAndNode = 47,
|
||||||
|
NodeVs8 = 48,
|
||||||
|
NodeVu8 = 49,
|
||||||
|
NodeVs16 = 50,
|
||||||
|
NodeVu16 = 51,
|
||||||
|
NodeBool = 52,
|
||||||
|
Node2b = 53,
|
||||||
|
Node3b = 54,
|
||||||
|
Node4b = 55,
|
||||||
|
NodeVb = 56,
|
||||||
|
}
|
45
src/types/game.rs
Normal file
45
src/types/game.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use either::Either;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Property {
|
||||||
|
pub call: CallStruct,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CallStruct {
|
||||||
|
#[serde(with = "either::serde_untagged")]
|
||||||
|
pub game: Either<GameScores, GameSave>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GameScores {
|
||||||
|
#[serde(rename = "refid")]
|
||||||
|
pub ref_id: String,
|
||||||
|
#[serde(with = "either::serde_untagged", rename = "track")]
|
||||||
|
pub tracks: Either<Track, Vec<Track>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Track {
|
||||||
|
pub music_id: u32,
|
||||||
|
pub music_type: u32,
|
||||||
|
pub score: u32,
|
||||||
|
#[serde(rename = "exscore")]
|
||||||
|
pub ex_score: u32,
|
||||||
|
pub clear_type: u32,
|
||||||
|
pub max_chain: u32,
|
||||||
|
pub critical: u32,
|
||||||
|
pub near: u32,
|
||||||
|
pub error: u32,
|
||||||
|
pub effective_rate: u32,
|
||||||
|
pub gauge_type: u32,
|
||||||
|
pub judge: [u32; 7],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct GameSave {
|
||||||
|
#[serde(rename = "refid")]
|
||||||
|
pub ref_id: String,
|
||||||
|
pub skill_level: u32,
|
||||||
|
}
|
2
src/types/mod.rs
Normal file
2
src/types/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod game;
|
||||||
|
pub mod tachi;
|
128
src/types/tachi.rs
Normal file
128
src/types/tachi.rs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
use num_enum::FromPrimitive;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Import {
|
||||||
|
pub meta: ImportMeta,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub classes: Option<ImportClasses>,
|
||||||
|
pub scores: Vec<ImportScore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ImportMeta {
|
||||||
|
pub game: String,
|
||||||
|
#[serde(rename = "playtype")]
|
||||||
|
pub play_type: String,
|
||||||
|
pub service: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ImportMeta {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
game: "sdvx".to_string(),
|
||||||
|
play_type: "Single".to_string(),
|
||||||
|
service: "Mikado".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ImportClasses {
|
||||||
|
pub dan: SkillLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, FromPrimitive, Serialize, Deserialize)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum SkillLevel {
|
||||||
|
#[num_enum(default)]
|
||||||
|
#[serde(rename = "DAN_1")]
|
||||||
|
First = 1,
|
||||||
|
#[serde(rename = "DAN_2")]
|
||||||
|
Second = 2,
|
||||||
|
#[serde(rename = "DAN_3")]
|
||||||
|
Third = 3,
|
||||||
|
#[serde(rename = "DAN_4")]
|
||||||
|
Fourth = 4,
|
||||||
|
#[serde(rename = "DAN_5")]
|
||||||
|
Fifth = 5,
|
||||||
|
#[serde(rename = "DAN_6")]
|
||||||
|
Sixth = 6,
|
||||||
|
#[serde(rename = "DAN_7")]
|
||||||
|
Seventh = 7,
|
||||||
|
#[serde(rename = "DAN_8")]
|
||||||
|
Eighth = 8,
|
||||||
|
#[serde(rename = "DAN_9")]
|
||||||
|
Ninth = 9,
|
||||||
|
#[serde(rename = "DAN_10")]
|
||||||
|
Tenth = 10,
|
||||||
|
#[serde(rename = "DAN_11")]
|
||||||
|
Eleventh = 11,
|
||||||
|
#[serde(rename = "INF")]
|
||||||
|
Infinite = 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ImportScore {
|
||||||
|
pub score: u32,
|
||||||
|
pub lamp: TachiLamp,
|
||||||
|
#[serde(rename = "matchType")]
|
||||||
|
pub match_type: String,
|
||||||
|
pub identifier: String,
|
||||||
|
pub difficulty: Difficulty,
|
||||||
|
#[serde(rename = "timeAchieved")]
|
||||||
|
pub time_achieved: u128,
|
||||||
|
pub judgements: Judgements,
|
||||||
|
#[serde(rename = "hitMeta")]
|
||||||
|
pub hit_meta: HitMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, FromPrimitive, Serialize, Deserialize)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum TachiLamp {
|
||||||
|
#[num_enum(default)]
|
||||||
|
#[serde(rename = "FAILED")]
|
||||||
|
Failed = 1,
|
||||||
|
#[serde(rename = "CLEAR")]
|
||||||
|
Clear = 2,
|
||||||
|
#[serde(rename = "EXCESSIVE CLEAR")]
|
||||||
|
ExcessiveClear = 3,
|
||||||
|
#[serde(rename = "ULTIMATE CHAIN")]
|
||||||
|
UltimateChain = 4,
|
||||||
|
#[serde(rename = "PERFECT ULTIMATE CHAIN")]
|
||||||
|
PerfectUltimateChain = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, FromPrimitive, Serialize, Deserialize)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum Difficulty {
|
||||||
|
#[num_enum(default)]
|
||||||
|
#[serde(rename = "NOV")]
|
||||||
|
Novice = 0,
|
||||||
|
#[serde(rename = "ADV")]
|
||||||
|
Advanced = 1,
|
||||||
|
#[serde(rename = "EXH")]
|
||||||
|
Exhaust = 2,
|
||||||
|
#[serde(rename = "ANY_INF")]
|
||||||
|
AnyInfinite = 3,
|
||||||
|
#[serde(rename = "MXM")]
|
||||||
|
Maximum = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Judgements {
|
||||||
|
pub critical: u32,
|
||||||
|
pub near: u32,
|
||||||
|
pub miss: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct HitMeta {
|
||||||
|
pub fast: u32,
|
||||||
|
pub slow: u32,
|
||||||
|
#[serde(rename = "maxCombo")]
|
||||||
|
pub max_combo: u32,
|
||||||
|
#[serde(rename = "exScore")]
|
||||||
|
pub ex_score: u32,
|
||||||
|
pub gauge: f32,
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user