mirror of
https://github.com/adamaq01/mikado.git
synced 2024-11-23 22:20:56 +01:00
feat: implement cloud link scores hijack to display Tachi PBs instead
This commit is contained in:
parent
99de91cc43
commit
dfba8af45b
@ -27,3 +27,8 @@ url = "2.3"
|
||||
either = { version = "1.8", features = ["serde"] }
|
||||
num_enum = "0.6"
|
||||
chrono = "0.4"
|
||||
kbinxml = { git = "https://github.com/mbilker/kbinxml-rs.git", version = "3.1.0" }
|
||||
psmap = { git = "https://github.com/mbilker/kbinxml-rs.git", version = "2.0.0" }
|
||||
psmap_derive = { git = "https://github.com/mbilker/kbinxml-rs.git", version = "2.0.0" }
|
||||
bytes = "1.4"
|
||||
dynfmt = { version = "0.1", default-features = false, features = ["curly"] }
|
||||
|
11
mikado.toml
11
mikado.toml
@ -3,21 +3,26 @@
|
||||
enable = true
|
||||
# Whether the hook should export your class (skill level) or not
|
||||
export_class = true
|
||||
# Whether the hook should should inject your Tachi PBs in place of Cloud PBs
|
||||
inject_cloud_pbs = true
|
||||
# Timeout for web requests, in milliseconds
|
||||
timeout = 3000
|
||||
|
||||
[cards]
|
||||
# Card numbers that should be whitelisted
|
||||
# If this is empty, all cards will be whitelisted
|
||||
# E000 format, should be in single quotes and separated by commas
|
||||
# Example: whitelist = ['E000000000', 'E000000001']
|
||||
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'
|
||||
|
||||
# Tachi pbs endpoint
|
||||
pbs = '/api/v1/users/{}/games/sdvx/Single/pbs/all'
|
||||
# Your Tachi API key
|
||||
api_key = 'your-key-here'
|
||||
|
27
src/cloudlink/ext.rs
Normal file
27
src/cloudlink/ext.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use crate::types::cloudlink::{Chart, Score};
|
||||
use kbinxml::{Node, Value, ValueArray};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) trait HashMapExt {
|
||||
fn to_properties(self) -> Vec<Node>;
|
||||
}
|
||||
|
||||
impl HashMapExt for HashMap<Chart, Score> {
|
||||
fn to_properties(self) -> Vec<Node> {
|
||||
self.into_iter()
|
||||
.map(|(chart, score)| {
|
||||
let mut property = score.to_property();
|
||||
property[0] = chart.song_id;
|
||||
property[1] = chart.difficulty as u32;
|
||||
|
||||
Node::with_nodes(
|
||||
"info",
|
||||
vec![Node::with_value(
|
||||
"param",
|
||||
Value::Array(ValueArray::U32(property)),
|
||||
)],
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
130
src/cloudlink/mod.rs
Normal file
130
src/cloudlink/mod.rs
Normal file
@ -0,0 +1,130 @@
|
||||
mod ext;
|
||||
|
||||
use crate::types::cloudlink::{Chart, Score};
|
||||
use crate::{helpers, TACHI_PBS_URL};
|
||||
use anyhow::Result;
|
||||
use dynfmt::Format;
|
||||
use ext::HashMapExt;
|
||||
use kbinxml::{CompressionType, EncodingType, Node, Options, Value, ValueArray};
|
||||
use log::info;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn build_response_base(scores: Vec<Node>) -> Node {
|
||||
Node::with_nodes(
|
||||
"response",
|
||||
vec![Node::with_nodes(
|
||||
"game",
|
||||
vec![Node::with_nodes("music", scores)],
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn process_pbs(user: &str, music: &Node, encoding: EncodingType) -> Result<Vec<u8>> {
|
||||
let url = dynfmt::SimpleCurlyFormat.format(TACHI_PBS_URL.as_str(), [user])?;
|
||||
let response: serde_json::Value = helpers::request_tachi("GET", url, None::<()>)?;
|
||||
let body = response["body"].as_object().ok_or(anyhow::anyhow!(
|
||||
"Could not parse response body from Tachi PBs API"
|
||||
))?;
|
||||
let pbs = body["pbs"]
|
||||
.as_array()
|
||||
.ok_or(anyhow::anyhow!("Could not parse PBs from Tachi PBs API"))?;
|
||||
let charts = body["charts"]
|
||||
.as_array()
|
||||
.ok_or(anyhow::anyhow!("Could not parse charts from Tachi PBs API"))?;
|
||||
let charts = charts
|
||||
.iter()
|
||||
.map(|chart| {
|
||||
let chart_id = chart["chartID"].as_str().ok_or(anyhow::anyhow!(
|
||||
"Could not parse chart ID from Tachi PBs API"
|
||||
))?;
|
||||
let song_id = chart["data"]["inGameID"].as_u64().ok_or(anyhow::anyhow!(
|
||||
"Could not parse ingame ID from Tachi PBs API"
|
||||
))? as u32;
|
||||
let difficulty = match chart["difficulty"].as_str().ok_or(anyhow::anyhow!(
|
||||
"Could not parse difficulty from Tachi PBs API"
|
||||
))? {
|
||||
"NOV" => 0,
|
||||
"ADV" => 1,
|
||||
"EXH" => 2,
|
||||
"MXM" => 4,
|
||||
_ => 3,
|
||||
};
|
||||
Ok((
|
||||
chart_id,
|
||||
Chart {
|
||||
song_id,
|
||||
difficulty,
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect::<Result<HashMap<&str, Chart>>>()?;
|
||||
|
||||
let mut scores = HashMap::with_capacity(music.children().len() + pbs.len());
|
||||
for pb in music.children() {
|
||||
let score = pb
|
||||
.children()
|
||||
.first()
|
||||
.ok_or(anyhow::anyhow!("Could not find param node"))?;
|
||||
if let Value::Array(ValueArray::U32(value)) = score
|
||||
.value()
|
||||
.ok_or(anyhow::anyhow!("Could not find value in param node"))?
|
||||
{
|
||||
let song_id = value[0];
|
||||
let difficulty = value[1] as u8;
|
||||
let chart = Chart {
|
||||
song_id,
|
||||
difficulty,
|
||||
};
|
||||
let score = Score::from_property(value.as_slice().try_into()?);
|
||||
scores.insert(chart, score);
|
||||
}
|
||||
}
|
||||
|
||||
for pb in pbs {
|
||||
let chart_id = pb["chartID"].as_str().ok_or(anyhow::anyhow!(
|
||||
"Could not parse chart ID from Tachi PBs API"
|
||||
))?;
|
||||
let chart = charts
|
||||
.get(chart_id)
|
||||
.ok_or(anyhow::anyhow!("Could not find chart"))?;
|
||||
let score = pb["scoreData"]["score"].as_u64().ok_or(anyhow::anyhow!(
|
||||
"Could not parse PB score from Tachi PBs API"
|
||||
))?;
|
||||
let lamp = pb["scoreData"]["enumIndexes"]["lamp"]
|
||||
.as_u64()
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"Could not parse PB lamp from Tachi PBs API"
|
||||
))?
|
||||
+ 1;
|
||||
let grade = pb["scoreData"]["enumIndexes"]["grade"]
|
||||
.as_u64()
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"Could not parse PB grade from Tachi PBs API"
|
||||
))?
|
||||
+ 1;
|
||||
|
||||
let entry = scores.entry(*chart);
|
||||
match entry {
|
||||
Entry::Occupied(mut entry) => {
|
||||
let base_score = entry.get_mut();
|
||||
*base_score.cloud_score_mut() = score as u32;
|
||||
*base_score.cloud_clear_mut() = lamp as u32;
|
||||
*base_score.cloud_grade_mut() = grade as u32;
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let score = Score::from_cloud(score as u32, lamp as u8, grade as u8);
|
||||
entry.insert(score);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let response = build_response_base(scores.to_properties());
|
||||
let bytes = kbinxml::to_binary_with_options(
|
||||
Options::new(CompressionType::Uncompressed, encoding),
|
||||
&response,
|
||||
)?;
|
||||
info!("Successfully injected Tachi PBs as Cloud scores");
|
||||
|
||||
Ok(bytes)
|
||||
}
|
@ -26,12 +26,27 @@ impl Configuration {
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct GeneralConfiguration {
|
||||
#[serde(default = "default_true")]
|
||||
pub enable: bool,
|
||||
#[serde(default)]
|
||||
pub export_class: bool,
|
||||
#[serde(default)]
|
||||
pub inject_cloud_pbs: bool,
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout: u64,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_timeout() -> u64 {
|
||||
3000
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct CardsConfiguration {
|
||||
#[serde(default)]
|
||||
pub whitelist: Vec<String>,
|
||||
}
|
||||
|
||||
@ -40,5 +55,6 @@ pub struct TachiConfiguration {
|
||||
pub base_url: String,
|
||||
pub status: String,
|
||||
pub import: String,
|
||||
pub pbs: String,
|
||||
pub api_key: String,
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
mod cloudlink;
|
||||
mod configuration;
|
||||
mod handlers;
|
||||
mod helpers;
|
||||
|
148
src/mikado.rs
148
src/mikado.rs
@ -7,7 +7,8 @@ use crate::sys::{
|
||||
use crate::types::game::Property;
|
||||
use crate::{helpers, CONFIGURATION, TACHI_STATUS_URL};
|
||||
use anyhow::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use bytes::Bytes;
|
||||
use kbinxml::{CompressionType, Node, Options, Value};
|
||||
use log::{debug, error, info};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
|
||||
@ -30,6 +31,11 @@ pub fn hook_init() -> Result<()> {
|
||||
// Initializing function detours
|
||||
crochet::enable!(property_destroy_hook)
|
||||
.map_err(|err| anyhow::anyhow!("Could not enable function detour: {:#}", err))?;
|
||||
if CONFIGURATION.general.inject_cloud_pbs {
|
||||
debug!("PBs injection enabled");
|
||||
crochet::enable!(property_mem_read_hook)
|
||||
.map_err(|err| anyhow::anyhow!("Could not enable function detour: {:#}", err))?;
|
||||
}
|
||||
|
||||
info!("Hook successfully initialized");
|
||||
|
||||
@ -46,9 +52,138 @@ pub fn hook_release() -> Result<()> {
|
||||
.map_err(|err| anyhow::anyhow!("Could not disable function detour: {:#}", err))?;
|
||||
}
|
||||
|
||||
if crochet::is_enabled!(property_mem_read_hook) {
|
||||
crochet::disable!(property_mem_read_hook)
|
||||
.map_err(|err| anyhow::anyhow!("Could not disable function detour: {:#}", err))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
static LOAD: AtomicBool = AtomicBool::new(false);
|
||||
static LOAD_M: AtomicBool = AtomicBool::new(false);
|
||||
static COMMON: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[crochet::hook("avs2-core.dll", "XCgsqzn00000b7")]
|
||||
pub unsafe fn property_mem_read_hook(
|
||||
ptr: *const (),
|
||||
something: i32,
|
||||
flags: i32,
|
||||
data: *const u8,
|
||||
size: u32,
|
||||
) -> *const () {
|
||||
let load = LOAD.load(Ordering::SeqCst);
|
||||
let load_m = LOAD_M.load(Ordering::SeqCst);
|
||||
let common = COMMON.load(Ordering::SeqCst);
|
||||
if !load && !load_m && !common {
|
||||
return call_original!(ptr, something, flags, data, size);
|
||||
}
|
||||
|
||||
let bytes = std::slice::from_raw_parts(ptr as *const u8, something as usize).to_vec();
|
||||
match property_mem_read_hook_wrapped(bytes, load, load_m, common) {
|
||||
Some(Ok(response)) => {
|
||||
call_original!(
|
||||
response.as_ptr() as *const (),
|
||||
response.len() as i32,
|
||||
flags,
|
||||
data,
|
||||
size
|
||||
)
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
error!(
|
||||
"Error while processing an important e-amusement response node: {:#}",
|
||||
err
|
||||
);
|
||||
call_original!(ptr, something, flags, data, size)
|
||||
}
|
||||
None => call_original!(ptr, something, flags, data, size),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::manual_map)]
|
||||
pub unsafe fn property_mem_read_hook_wrapped(
|
||||
response: Vec<u8>,
|
||||
load: bool,
|
||||
load_m: bool,
|
||||
common: bool,
|
||||
) -> Option<Result<Vec<u8>>> {
|
||||
let (mut root, encoding) = kbinxml::from_bytes(Bytes::from(response))
|
||||
.and_then(|(node, encoding)| node.as_node().map(|node| (node, encoding)))
|
||||
.ok()?;
|
||||
|
||||
if common
|
||||
.then(|| root.pointer(&["game", "event"]))
|
||||
.flatten()
|
||||
.is_some()
|
||||
{
|
||||
Some((|| {
|
||||
let events = root
|
||||
.pointer_mut(&["game", "event"])
|
||||
.expect("Could not find events node");
|
||||
|
||||
events.children_mut().retain(|info| {
|
||||
if let Some(Value::String(event_id)) = info
|
||||
.pointer(&["event_id"])
|
||||
.and_then(|event_id| event_id.value())
|
||||
{
|
||||
event_id != "CLOUD_LINK_ENABLE"
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
events.children_mut().push(Node::with_nodes(
|
||||
"info",
|
||||
vec![Node::with_value(
|
||||
"event_id",
|
||||
Value::String("CLOUD_LINK_ENABLE".to_string()),
|
||||
)],
|
||||
));
|
||||
let response = kbinxml::to_binary_with_options(
|
||||
Options::new(CompressionType::Uncompressed, encoding),
|
||||
&root,
|
||||
)?;
|
||||
COMMON.store(false, Ordering::Relaxed);
|
||||
|
||||
Ok(response)
|
||||
})())
|
||||
} else if load
|
||||
.then(|| root.pointer(&["game", "code"]))
|
||||
.flatten()
|
||||
.is_some()
|
||||
{
|
||||
Some((|| {
|
||||
let game = root
|
||||
.pointer_mut(&["game"])
|
||||
.expect("Could not find game node");
|
||||
game.children_mut().retain(|node| {
|
||||
return node.key() != "cloud";
|
||||
});
|
||||
game.children_mut().push(Node::with_nodes(
|
||||
"cloud",
|
||||
vec![Node::with_value("relation", Value::S8(1))],
|
||||
));
|
||||
let response = kbinxml::to_binary_with_options(
|
||||
Options::new(CompressionType::Uncompressed, encoding),
|
||||
&root,
|
||||
)?;
|
||||
LOAD.store(false, Ordering::Relaxed);
|
||||
|
||||
Ok(response)
|
||||
})())
|
||||
} else if let Some(music) = load_m.then(|| root.pointer(&["game", "music"])).flatten() {
|
||||
Some((|| {
|
||||
let user = USER.load(Ordering::SeqCst).to_string();
|
||||
let response = crate::cloudlink::process_pbs(user.as_str(), music, encoding)?;
|
||||
LOAD_M.store(false, Ordering::Relaxed);
|
||||
|
||||
Ok(response)
|
||||
})())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[crochet::hook("avs2-core.dll", "XCgsqzn0000091")]
|
||||
pub unsafe fn property_destroy_hook(property: *mut ()) -> i32 {
|
||||
if property.is_null() {
|
||||
@ -102,6 +237,17 @@ pub unsafe fn property_destroy_hook(property: *mut ()) -> i32 {
|
||||
result.unwrap().replace('\0', "")
|
||||
};
|
||||
debug!("Intercepted Game Method: {}", method);
|
||||
|
||||
if CONFIGURATION.general.inject_cloud_pbs {
|
||||
if method == "sv6_load_m" {
|
||||
LOAD_M.store(true, Ordering::Relaxed);
|
||||
} else if method == "sv6_common" {
|
||||
COMMON.store(true, Ordering::Relaxed);
|
||||
} else if method == "sv6_load" {
|
||||
LOAD.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
if method != "sv6_save_m" && (!CONFIGURATION.general.export_class || method != "sv6_save") {
|
||||
return call_original!(property);
|
||||
}
|
||||
|
43
src/types/cloudlink.rs
Normal file
43
src/types/cloudlink.rs
Normal file
@ -0,0 +1,43 @@
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
|
||||
pub struct Chart {
|
||||
pub song_id: u32,
|
||||
pub difficulty: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Score {
|
||||
property: [u32; 21],
|
||||
}
|
||||
|
||||
impl Score {
|
||||
pub fn from_cloud(score: u32, clear: u8, grade: u8) -> Self {
|
||||
let mut ret = Self::default();
|
||||
ret.property[17] = score;
|
||||
ret.property[18] = clear as u32;
|
||||
ret.property[19] = grade as u32;
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn from_property(property: &[u32; 21]) -> Self {
|
||||
Self {
|
||||
property: *property,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cloud_score_mut(&mut self) -> &mut u32 {
|
||||
&mut self.property[17]
|
||||
}
|
||||
|
||||
pub fn cloud_clear_mut(&mut self) -> &mut u32 {
|
||||
&mut self.property[18]
|
||||
}
|
||||
|
||||
pub fn cloud_grade_mut(&mut self) -> &mut u32 {
|
||||
&mut self.property[19]
|
||||
}
|
||||
|
||||
pub fn to_property(self) -> Vec<u32> {
|
||||
self.property.to_vec()
|
||||
}
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
pub mod cloudlink;
|
||||
pub mod game;
|
||||
pub mod tachi;
|
||||
|
Loading…
Reference in New Issue
Block a user