mirror of
https://github.com/adamaq01/mikado.git
synced 2024-11-23 22:20:56 +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