mirror of
https://git.ely.my.id/Tenshi/Hi3-Patcher
synced 2026-11-04 21:46:48 +01:00
Initial Commit
This commit is contained in:
commit
737780597b
51 files changed
+8675
No files matched your search
+329
@@ -0,0 +1,329 @@
|
||||
//! Non-interactive command-line interface, intended for scripting and for
|
||||
//! driving the patcher from another application.
|
||||
//!
|
||||
//! Contract: the process exit code is authoritative — `0` success, `1` a
|
||||
//! patch/runtime error, `2` a usage error. The whole command line is
|
||||
//! validated before any patch work starts. With `--json`, the final line of
|
||||
//! stdout is a single JSON object (`{"ok":true,"command":...,"output":...}`
|
||||
//! or `{"ok":false,"error":...}`), usage errors included; human progress is
|
||||
//! printed before it.
|
||||
use crate::patch::{
|
||||
ace,
|
||||
dispatch::{Target, android, windows},
|
||||
hoyokprotect, protection, rsa,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use std::{collections::HashMap, path::PathBuf, process::ExitCode};
|
||||
|
||||
const USAGE: &str = "\
|
||||
Honkai Impact 3rd patcher
|
||||
|
||||
Usage:
|
||||
Hi3-Patcher Launch the interactive menu
|
||||
Hi3-Patcher <command> [options]
|
||||
|
||||
Commands:
|
||||
dispatch Redirect dispatch & SDK URLs to a server
|
||||
--platform <windows|android> --input <path>
|
||||
--server <host[:port]> [--https]
|
||||
rsa Replace the passport RSA public key
|
||||
--platform <windows|android> --input <path> [--key <base64>]
|
||||
ace Remove ACE from a Windows client (<= 8.9); requires a capture
|
||||
--input <BH3.exe|folder> [--yes]
|
||||
hoyok Disable HoYoKProtect in a Windows client (9.0+)
|
||||
--input <BH3.exe|folder>
|
||||
protection Remove Android protection and re-sign the APK
|
||||
--input <app.apk> [--variant <ano|tss|auto>]
|
||||
|
||||
Options:
|
||||
--json Emit a single JSON result object as the last stdout line
|
||||
-h, --help, -V, --version
|
||||
|
||||
Windows --input accepts the game folder or its BH3.exe.
|
||||
rsa --key defaults to the PemukulPaku public key when omitted.";
|
||||
|
||||
/// Options each command accepts: `(value options, switches)`. Every command
|
||||
/// also accepts `--json`.
|
||||
fn accepted(command: &str) -> Option<(&'static [&'static str], &'static [&'static str])> {
|
||||
Some(match command {
|
||||
"dispatch" => (&["platform", "input", "server"], &["https"]),
|
||||
"rsa" => (&["platform", "input", "key"], &[]),
|
||||
"ace" => (&["input"], &["yes"]),
|
||||
"hoyok" => (&["input"], &[]),
|
||||
"protection" => (&["input", "variant"], &[]),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
struct Parsed {
|
||||
values: HashMap<String, String>,
|
||||
switches: Vec<String>,
|
||||
}
|
||||
|
||||
/// Split `--name value`, `--name=value` and `--switch` tokens, rejecting
|
||||
/// anything the command does not accept. Errors are usage errors.
|
||||
fn parse(command: &str, rest: &[String]) -> Result<Parsed, String> {
|
||||
let (options, flags) =
|
||||
accepted(command).ok_or_else(|| format!("unknown command: {command}"))?;
|
||||
let mut values = HashMap::new();
|
||||
let mut switches: Vec<String> = Vec::new();
|
||||
let mut it = rest.iter();
|
||||
while let Some(token) = it.next() {
|
||||
let Some(name) = token.strip_prefix("--") else {
|
||||
return Err(format!("unexpected argument: {token}"));
|
||||
};
|
||||
let (name, inline) = match name.split_once('=') {
|
||||
Some((name, value)) => (name, Some(value)),
|
||||
None => (name, None),
|
||||
};
|
||||
if name == "json" || flags.contains(&name) {
|
||||
if inline.is_some() {
|
||||
return Err(format!("--{name} does not take a value"));
|
||||
}
|
||||
if switches.iter().any(|s| s == name) {
|
||||
return Err(format!("--{name} given more than once"));
|
||||
}
|
||||
switches.push(name.to_owned());
|
||||
} else if options.contains(&name) {
|
||||
let value = match inline {
|
||||
Some(value) => value.to_owned(),
|
||||
None => it
|
||||
.next()
|
||||
.filter(|value| !value.starts_with("--"))
|
||||
.ok_or_else(|| format!("--{name} needs a value"))?
|
||||
.clone(),
|
||||
};
|
||||
if values.insert(name.to_owned(), value).is_some() {
|
||||
return Err(format!("--{name} given more than once"));
|
||||
}
|
||||
} else {
|
||||
return Err(format!("{command} does not accept --{name}"));
|
||||
}
|
||||
}
|
||||
Ok(Parsed { values, switches })
|
||||
}
|
||||
|
||||
impl Parsed {
|
||||
fn required(&self, name: &str) -> Result<&str, String> {
|
||||
match self.values.get(name).map(String::as_str) {
|
||||
None | Some("") => Err(format!("--{name} is required")),
|
||||
Some(value) => Ok(value),
|
||||
}
|
||||
}
|
||||
fn has(&self, name: &str) -> bool {
|
||||
self.switches.iter().any(|s| s == name)
|
||||
}
|
||||
}
|
||||
|
||||
enum Platform {
|
||||
Windows,
|
||||
Android,
|
||||
}
|
||||
|
||||
enum Variant {
|
||||
Auto,
|
||||
Ano,
|
||||
Tss,
|
||||
}
|
||||
|
||||
/// A fully validated command, ready to run.
|
||||
enum Job {
|
||||
Dispatch { platform: Platform, input: PathBuf, target: Target },
|
||||
Rsa { platform: Platform, input: PathBuf, key: String },
|
||||
Ace { input: PathBuf, yes: bool },
|
||||
Hoyok { input: PathBuf },
|
||||
Protection { input: PathBuf, variant: Variant },
|
||||
}
|
||||
|
||||
fn parse_platform(args: &Parsed) -> Result<Platform, String> {
|
||||
match args.required("platform")? {
|
||||
"windows" | "win" => Ok(Platform::Windows),
|
||||
"android" => Ok(Platform::Android),
|
||||
other => Err(format!("unknown platform: {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve `--input`; a Windows input may be the game folder or its BH3.exe.
|
||||
fn input_path(args: &Parsed, platform: &Platform) -> Result<PathBuf, String> {
|
||||
let path = PathBuf::from(args.required("input")?);
|
||||
Ok(match platform {
|
||||
Platform::Windows if path.is_dir() => path.join("BH3.exe"),
|
||||
_ => path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check a parsed command line per command. Errors are usage errors.
|
||||
fn job(command: &str, args: &Parsed) -> Result<Job, String> {
|
||||
Ok(match command {
|
||||
"dispatch" => {
|
||||
let platform = parse_platform(args)?;
|
||||
let input = input_path(args, &platform)?;
|
||||
let target = Target::new(args.required("server")?, args.has("https"))
|
||||
.map_err(|error| format!("--server: {error:#}"))?;
|
||||
Job::Dispatch { platform, input, target }
|
||||
}
|
||||
"rsa" => {
|
||||
let platform = parse_platform(args)?;
|
||||
let input = input_path(args, &platform)?;
|
||||
let key = args.values.get("key").cloned().unwrap_or_default();
|
||||
rsa::public_body(&key).map_err(|error| format!("--key: {error:#}"))?;
|
||||
Job::Rsa { platform, input, key }
|
||||
}
|
||||
"ace" => Job::Ace {
|
||||
input: input_path(args, &Platform::Windows)?,
|
||||
yes: args.has("yes"),
|
||||
},
|
||||
"hoyok" => Job::Hoyok {
|
||||
input: input_path(args, &Platform::Windows)?,
|
||||
},
|
||||
"protection" => Job::Protection {
|
||||
input: input_path(args, &Platform::Android)?,
|
||||
variant: match args.values.get("variant").map(String::as_str) {
|
||||
None | Some("auto") => Variant::Auto,
|
||||
Some("ano") => Variant::Ano,
|
||||
Some("tss") => Variant::Tss,
|
||||
Some(other) => return Err(format!("unknown variant: {other}")),
|
||||
},
|
||||
},
|
||||
other => return Err(format!("unknown command: {other}")),
|
||||
})
|
||||
}
|
||||
|
||||
/// Run a validated job. Returns the output path for Android patches.
|
||||
fn execute(job: Job) -> Result<Option<PathBuf>> {
|
||||
Ok(match job {
|
||||
Job::Dispatch { platform: Platform::Windows, input, target } => {
|
||||
windows::patch(&input, &target)?;
|
||||
None
|
||||
}
|
||||
Job::Dispatch { platform: Platform::Android, input, target } => {
|
||||
Some(android::patch(&input, &target)?)
|
||||
}
|
||||
Job::Rsa { platform: Platform::Windows, input, key } => {
|
||||
rsa::patch_windows(&input, &key)?;
|
||||
None
|
||||
}
|
||||
Job::Rsa { platform: Platform::Android, input, key } => {
|
||||
Some(rsa::patch_android(&input, &key)?)
|
||||
}
|
||||
Job::Ace { input, yes } => {
|
||||
ace::patch(&input, |_| Ok(yes))?;
|
||||
None
|
||||
}
|
||||
Job::Hoyok { input } => {
|
||||
hoyokprotect::patch(&input)?;
|
||||
None
|
||||
}
|
||||
Job::Protection { input, variant } => Some(match variant {
|
||||
Variant::Auto => protection::auto(&input)?,
|
||||
Variant::Ano => protection::anosdk(&input)?,
|
||||
Variant::Tss => protection::mobile_ace_tss(&input)?,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn report(command: &str, result: Result<Option<PathBuf>>, json: bool) -> ExitCode {
|
||||
match result {
|
||||
Ok(output) => {
|
||||
let out = output.as_ref().map(|p| p.display().to_string());
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({"ok": true, "command": command, "output": out})
|
||||
);
|
||||
} else if let Some(path) = out {
|
||||
println!("Output: {path}");
|
||||
}
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(error) => {
|
||||
let message = format!("{error:#}");
|
||||
if json {
|
||||
println!("{}", serde_json::json!({"ok": false, "error": message}));
|
||||
} else {
|
||||
eprintln!("error: {message}");
|
||||
}
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn usage_error(message: &str, json: bool) -> ExitCode {
|
||||
if json {
|
||||
println!("{}", serde_json::json!({"ok": false, "error": message}));
|
||||
} else {
|
||||
eprintln!("error: {message}\n\n{USAGE}");
|
||||
}
|
||||
ExitCode::from(2)
|
||||
}
|
||||
|
||||
/// Entry point for a non-empty argument list (the command and its options).
|
||||
pub fn run(argv: &[String]) -> ExitCode {
|
||||
let command = argv[0].as_str();
|
||||
let rest = &argv[1..];
|
||||
if matches!(command, "-h" | "--help" | "help")
|
||||
|| rest.iter().any(|a| a == "-h" || a == "--help")
|
||||
{
|
||||
println!("{USAGE}");
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
if matches!(command, "-V" | "--version" | "version") {
|
||||
println!("Hi3-Patcher {}", env!("CARGO_PKG_VERSION"));
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
let json = rest.iter().any(|a| a == "--json");
|
||||
match parse(command, rest).and_then(|args| job(command, &args)) {
|
||||
Ok(job) => report(command, execute(job), json),
|
||||
Err(message) => usage_error(&message, json),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Validate a command line the way `run` does, without executing it.
|
||||
fn check(line: &[&str]) -> Result<Job, String> {
|
||||
let argv: Vec<String> = line.iter().map(|s| s.to_string()).collect();
|
||||
parse(&argv[0], &argv[1..]).and_then(|args| job(&argv[0], &args))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_command_lines() {
|
||||
let cases: &[&[&str]] = &[
|
||||
&["dispatch", "--platform", "windows", "--input", "x", "--server", "1.1.1.1"],
|
||||
&["dispatch", "--platform=android", "--input=a.apk", "--server=h:3000", "--https", "--json"],
|
||||
&["rsa", "--platform", "win", "--input", "x"],
|
||||
&["rsa", "--platform", "windows", "--input", "x", "--key", ""],
|
||||
&["ace", "--input", "x", "--yes"],
|
||||
&["hoyok", "--input", "x"],
|
||||
&["protection", "--input", "a.apk", "--variant", "tss"],
|
||||
];
|
||||
for line in cases {
|
||||
assert!(check(line).is_ok(), "rejected: {line:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_usage_errors() {
|
||||
let cases: &[&[&str]] = &[
|
||||
&["bogus"],
|
||||
&["dispatch", "--platform", "windows", "--input", "x"],
|
||||
&["dispatch", "--platform", "plan9", "--input", "x", "--server", "h"],
|
||||
&["dispatch", "--platform", "windows", "--input", "x", "--server", "h/p"],
|
||||
&["dispatch", "stray"],
|
||||
&["dispatch", "--input"],
|
||||
&["dispatch", "--input", "--json"],
|
||||
&["dispatch", "--platform", "windows", "--input", "x", "--server", "h", "--sever", "h"],
|
||||
&["rsa", "--platform", "windows", "--input", "x", "--key", "abc"],
|
||||
&["ace", "--input", "x", "--https"],
|
||||
&["ace", "--input", "a", "--input", "b"],
|
||||
&["ace", "--input", "x", "--yes=1"],
|
||||
&["hoyok", "--input", ""],
|
||||
&["protection", "--input", "a.apk", "--variant", "x"],
|
||||
];
|
||||
for line in cases {
|
||||
assert!(check(line).is_err(), "accepted: {line:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
//! Interactive terminal front end: arrow-key menus and path prompts.
|
||||
use crate::{
|
||||
android::toolchain,
|
||||
patch::{
|
||||
ace,
|
||||
dispatch::{Target, android, windows},
|
||||
hoyokprotect, protection, rsa,
|
||||
},
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show, position},
|
||||
event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
|
||||
execute,
|
||||
style::{Color, Print, ResetColor, SetForegroundColor},
|
||||
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
struct Screen;
|
||||
impl Drop for Screen {
|
||||
fn drop(&mut self) {
|
||||
let _ = terminal::disable_raw_mode();
|
||||
let _ = execute!(io::stdout(), Show, LeaveAlternateScreen);
|
||||
}
|
||||
}
|
||||
|
||||
struct RawInput;
|
||||
impl Drop for RawInput {
|
||||
fn drop(&mut self) {
|
||||
let _ = terminal::disable_raw_mode();
|
||||
let _ = execute!(io::stdout(), Show);
|
||||
}
|
||||
}
|
||||
|
||||
fn screen(title: &str) -> Result<()> {
|
||||
execute!(io::stdout(), MoveTo(0, 0), Clear(ClearType::All))?;
|
||||
print!("Honkai Impact 3 - Patcher\r\n");
|
||||
print!("{:>53}\r\n", concat!("Ver: ", env!("CARGO_PKG_VERSION")));
|
||||
print!("=====================================================\r\n{title}\r\n\r\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Returns the activated shortcut; arrows only change the highlight.
|
||||
fn selection(key: KeyEvent, selected: &mut usize, options: &[(char, &str)]) -> Option<char> {
|
||||
if key.kind == KeyEventKind::Release {
|
||||
return None;
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Up => *selected = (*selected + options.len() - 1) % options.len(),
|
||||
KeyCode::Down => *selected = (*selected + 1) % options.len(),
|
||||
KeyCode::Enter if key.kind == KeyEventKind::Press => return Some(options[*selected].0),
|
||||
KeyCode::Esc => return Some('0'),
|
||||
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Some('0'),
|
||||
KeyCode::Char(c) if key.kind == KeyEventKind::Press && options.iter().any(|o| o.0 == c) => {
|
||||
return Some(c);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn menu(title: &str, options: &[(char, &str)]) -> Result<char> {
|
||||
terminal::enable_raw_mode()?;
|
||||
let _raw = RawInput;
|
||||
execute!(io::stdout(), Hide)?;
|
||||
let mut selected = 0;
|
||||
loop {
|
||||
screen(title)?;
|
||||
io::stdout().flush()?;
|
||||
let (_, first_row) = position()?;
|
||||
for (index, (key, label)) in options.iter().enumerate() {
|
||||
print!("{} {key}. {label}\r\n", if index == selected { ">" } else { " " });
|
||||
}
|
||||
io::stdout().flush()?;
|
||||
loop {
|
||||
match event::read()? {
|
||||
Event::Key(key) => {
|
||||
let previous = selected;
|
||||
if let Some(choice) = selection(key, &mut selected, options) {
|
||||
return Ok(choice);
|
||||
}
|
||||
if previous != selected {
|
||||
execute!(
|
||||
io::stdout(),
|
||||
MoveTo(0, first_row + previous as u16),
|
||||
Print(" "),
|
||||
MoveTo(0, first_row + selected as u16),
|
||||
Print(">")
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Event::Resize(..) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt(label: &str) -> Result<String> {
|
||||
print!("{label}");
|
||||
io::stdout().flush()?;
|
||||
let mut value = String::new();
|
||||
if io::stdin().read_line(&mut value)? == 0 {
|
||||
bail!("input closed");
|
||||
}
|
||||
Ok(value.trim().trim_matches('"').trim_matches('\'').to_owned())
|
||||
}
|
||||
|
||||
fn prompt_path(label: &str) -> Result<Option<PathBuf>> {
|
||||
print!("{label}");
|
||||
io::stdout().flush()?;
|
||||
let (column, row) = position()?;
|
||||
terminal::enable_raw_mode()?;
|
||||
let _raw = RawInput;
|
||||
let mut value = String::new();
|
||||
loop {
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind != KeyEventKind::Release => match key.code {
|
||||
KeyCode::Esc => return Ok(None),
|
||||
KeyCode::Enter => {
|
||||
print!("\r\n");
|
||||
return Ok(Some(PathBuf::from(
|
||||
value.trim().trim_matches('"').trim_matches('\''),
|
||||
)));
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
value.pop();
|
||||
}
|
||||
KeyCode::Char(c)
|
||||
if !key.modifiers.contains(KeyModifiers::CONTROL)
|
||||
|| key.modifiers.contains(KeyModifiers::ALT) =>
|
||||
{
|
||||
value.push(c);
|
||||
}
|
||||
_ => continue,
|
||||
},
|
||||
Event::Paste(text) => value.extend(text.chars().filter(|c| !matches!(c, '\r' | '\n'))),
|
||||
_ => continue,
|
||||
}
|
||||
execute!(
|
||||
io::stdout(),
|
||||
MoveTo(column, row),
|
||||
Clear(ClearType::FromCursorDown),
|
||||
Print(&value)
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_rsa_key() -> Result<String> {
|
||||
println!("\nNote: Press Enter for Default used by PemukulPaku");
|
||||
print!("Public key: ");
|
||||
io::stdout().flush()?;
|
||||
let (column, row) = position()?;
|
||||
terminal::enable_raw_mode()?;
|
||||
let _raw = RawInput;
|
||||
let mut value = String::new();
|
||||
loop {
|
||||
execute!(io::stdout(), MoveTo(column, row), Clear(ClearType::FromCursorDown))?;
|
||||
if value.is_empty() {
|
||||
execute!(
|
||||
io::stdout(),
|
||||
SetForegroundColor(Color::DarkGrey),
|
||||
Print(rsa::DEFAULT_BODY),
|
||||
ResetColor,
|
||||
MoveTo(column, row)
|
||||
)?;
|
||||
} else {
|
||||
print!("{value}");
|
||||
}
|
||||
io::stdout().flush()?;
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind != KeyEventKind::Release => match key.code {
|
||||
KeyCode::Enter => {
|
||||
execute!(io::stdout(), MoveTo(column, row), Clear(ClearType::FromCursorDown))?;
|
||||
if value.is_empty() {
|
||||
execute!(
|
||||
io::stdout(),
|
||||
SetForegroundColor(Color::DarkGrey),
|
||||
Print("(default)"),
|
||||
ResetColor
|
||||
)?;
|
||||
} else {
|
||||
print!("{value}");
|
||||
}
|
||||
print!("\r\n");
|
||||
return Ok(value);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
value.pop();
|
||||
}
|
||||
KeyCode::Char(c)
|
||||
if !key.modifiers.contains(KeyModifiers::CONTROL)
|
||||
|| key.modifiers.contains(KeyModifiers::ALT) =>
|
||||
{
|
||||
value.push(c);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Paste(text) => value.extend(text.chars().filter(|c| !c.is_whitespace())),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_bh3_exe() -> Result<Option<PathBuf>> {
|
||||
println!("Enter full Path to the Game folder.");
|
||||
println!("Example: D:\\example\\some folder\\BH3_vX.X.0_becb21cb288f\n");
|
||||
let Some(path) = prompt_path("Full path to BH3: ")? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(if path.is_dir() {
|
||||
path.join("BH3.exe")
|
||||
} else {
|
||||
path
|
||||
}))
|
||||
}
|
||||
|
||||
fn dispatch_patch(platform: char) -> Result<()> {
|
||||
let platform_name = if platform == '1' { "Windows" } else { "Android" };
|
||||
let section = format!("Platform > {platform_name} > Dispatch & SDK Patch");
|
||||
let https = match menu(
|
||||
&format!(
|
||||
"{section} / Protocol\r\n\r\nPatch Client to redirect to a different IP or Domain.\r\nNote: Normally it's HTTPS, but pemu uses HTTP for simpler setup\r\nso the client does not need a Cert Patch."
|
||||
),
|
||||
&[('1', "HTTP (default)"), ('2', "HTTPS"), ('0', "Back")],
|
||||
)? {
|
||||
'1' => false,
|
||||
'2' => true,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
screen(&format!("{section} / Address"))?;
|
||||
println!("Patch Client to redirect to a different IP or Domain.\n");
|
||||
println!("Example: 127.0.0.1 or example.com");
|
||||
let target = Target::new(&prompt("Address: ")?, https)?;
|
||||
screen(&format!("{section} / Client"))?;
|
||||
let input = if platform == '1' {
|
||||
prompt_bh3_exe()?
|
||||
} else {
|
||||
prompt_path("Full path to APK: ")?
|
||||
};
|
||||
let Some(input) = input else { return Ok(()) };
|
||||
screen(&format!("{section} / Patching"))?;
|
||||
if platform == '1' {
|
||||
windows::patch(&input, &target)?;
|
||||
} else {
|
||||
android::patch(&input, &target)?;
|
||||
}
|
||||
prompt("\nPress Enter to return...")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_android() -> Result<bool> {
|
||||
if toolchain::build_tools().is_err() {
|
||||
if menu(
|
||||
"Platform > Android > Setup\r\n\r\nThis patch requires Android SDK Build-Tools 35.0.0 (~60 MB). Download them?",
|
||||
&[('1', "Yes"), ('2', "No")],
|
||||
)? != '1'
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
screen("Platform > Android > Setup / Downloading Build-Tools")?;
|
||||
toolchain::install_build_tools(&toolchain::local_build_tools()?)?;
|
||||
}
|
||||
if toolchain::check_java().is_err() {
|
||||
if menu(
|
||||
"Platform > Android > Setup\r\n\r\nThis patch requires Java 17+. Download portable Java (~190 MB)?",
|
||||
&[('1', "Yes"), ('2', "No")],
|
||||
)? != '1'
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
screen("Platform > Android > Setup / Downloading Java")?;
|
||||
toolchain::install_java(&toolchain::local_java()?)?;
|
||||
toolchain::check_java()?;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn patch_options(platform: char) -> &'static [(char, &'static str)] {
|
||||
if platform == '1' {
|
||||
&[
|
||||
('1', "Dispatch & SDK Patch"),
|
||||
('2', "RSA Patch"),
|
||||
('3', "ACE Patch"),
|
||||
('4', "HoYoKProtect Patch"),
|
||||
('0', "Back"),
|
||||
]
|
||||
} else {
|
||||
&[
|
||||
('1', "Dispatch & SDK Patch"),
|
||||
('2', "RSA Patch"),
|
||||
('3', "Mobile ACE/TSS Patch (CN)"),
|
||||
('4', "AnoSDK Patch (SEA, GLB, JP, KR, TW)"),
|
||||
('0', "Back"),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
let _screen = Screen;
|
||||
execute!(io::stdout(), EnterAlternateScreen)?;
|
||||
loop {
|
||||
let platform = menu(
|
||||
"Platform:\r\n\r\nSelect the Client Platform you want to Patch.",
|
||||
&[
|
||||
('1', "Windows"),
|
||||
('2', "Android"),
|
||||
('3', "iOS (not implemented yet)"),
|
||||
('0', "Exit"),
|
||||
],
|
||||
)?;
|
||||
if platform == '0' {
|
||||
return Ok(());
|
||||
}
|
||||
if platform == '3' {
|
||||
continue;
|
||||
}
|
||||
let platform_path = if platform == '1' {
|
||||
"Platform > Windows"
|
||||
} else {
|
||||
"Platform > Android"
|
||||
};
|
||||
if platform == '2' {
|
||||
match setup_android() {
|
||||
Ok(true) => {}
|
||||
Ok(false) => continue,
|
||||
Err(error) => {
|
||||
screen("Platform > Android > Setup / Error")?;
|
||||
println!("{error:#}");
|
||||
prompt("\nPress Enter to return...")?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
loop {
|
||||
match menu(platform_path, patch_options(platform))? {
|
||||
'0' => break,
|
||||
'1' => {
|
||||
if let Err(error) = dispatch_patch(platform) {
|
||||
println!("[x] {error:#}");
|
||||
prompt("\nPress Enter to return...")?;
|
||||
}
|
||||
}
|
||||
'2' => {
|
||||
screen(&format!("{platform_path} > RSA Patch"))?;
|
||||
println!("Replaces the RSA Public Key used by Ma-Passport");
|
||||
println!(
|
||||
"Note: if you use a different RSA Public key make sure it matches the PemukulPaku's appsettings.json Private Key"
|
||||
);
|
||||
if platform == '2' {
|
||||
println!("Creates a separate signed APK. The original stays unchanged.");
|
||||
}
|
||||
let input = if platform == '1' {
|
||||
prompt_bh3_exe()?
|
||||
} else {
|
||||
prompt_path("Full path to APK: ")?
|
||||
};
|
||||
let Some(input) = input else { continue };
|
||||
let key = prompt_rsa_key()?;
|
||||
let result = if platform == '1' {
|
||||
rsa::patch_windows(&input, &key)
|
||||
} else {
|
||||
rsa::patch_android(&input, &key).map(|_| ())
|
||||
};
|
||||
if let Err(error) = result {
|
||||
println!("[x] {error:#}");
|
||||
}
|
||||
prompt("\nPress Enter to return...")?;
|
||||
}
|
||||
'3' => {
|
||||
if platform == '2' {
|
||||
screen(&format!("{platform_path} > Mobile ACE/TSS Patch"))?;
|
||||
println!("Removes client Protection and creates a separate signed APK.");
|
||||
let Some(input) = prompt_path("Full path to APK: ")? else {
|
||||
continue;
|
||||
};
|
||||
if let Err(error) = protection::mobile_ace_tss(&input) {
|
||||
println!("[x] {error:#}");
|
||||
}
|
||||
} else {
|
||||
screen(&format!("{platform_path} > ACE Patch"))?;
|
||||
println!("Removes ACE from the Game Client.");
|
||||
println!("Note: Only for clients 8.9 and below, 9.0+ clients use HoYoKProtect.");
|
||||
let Some(input) = prompt_bh3_exe()? else {
|
||||
continue;
|
||||
};
|
||||
if let Err(error) = ace::patch(&input, |_| {
|
||||
let choice = menu(
|
||||
&format!(
|
||||
"{platform_path} > ACE Patch\r\n\r\nThis patch will start the client to capture initialization."
|
||||
),
|
||||
&[('1', "Yes"), ('2', "No")],
|
||||
)?;
|
||||
screen(&format!("{platform_path} > ACE Patch"))?;
|
||||
Ok(choice == '1')
|
||||
}) {
|
||||
println!("[x] {error:#}");
|
||||
}
|
||||
}
|
||||
prompt("\nPress Enter to return...")?;
|
||||
}
|
||||
'4' => {
|
||||
if platform == '2' {
|
||||
screen(&format!("{platform_path} > AnoSDK Patch"))?;
|
||||
println!("Removes client Protection and creates a separate signed APK.");
|
||||
let Some(input) = prompt_path("Full path to APK: ")? else {
|
||||
continue;
|
||||
};
|
||||
if let Err(error) = protection::anosdk(&input) {
|
||||
println!("[x] {error:#}");
|
||||
}
|
||||
} else {
|
||||
screen(&format!("{platform_path} > HoYoKProtect Patch"))?;
|
||||
println!("Removes HoYoKProtect from the Game Client.");
|
||||
let Some(input) = prompt_bh3_exe()? else {
|
||||
continue;
|
||||
};
|
||||
if let Err(error) = hoyokprotect::patch(&input) {
|
||||
println!("[x] {error:#}");
|
||||
}
|
||||
}
|
||||
prompt("\nPress Enter to return...")?;
|
||||
}
|
||||
_ => {
|
||||
screen("Not implemented yet. No files changed.")?;
|
||||
prompt("Press Enter to return...")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//! Front ends: an interactive terminal menu and an argument-driven interface.
|
||||
//! With no arguments the interactive menu runs; otherwise the first argument
|
||||
//! selects a command (see [`args`]).
|
||||
pub mod args;
|
||||
pub mod interactive;
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
pub fn run() -> ExitCode {
|
||||
let argv: Vec<String> = std::env::args().skip(1).collect();
|
||||
if argv.is_empty() {
|
||||
if let Err(error) = interactive::run() {
|
||||
eprintln!("error: {error:#}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
ExitCode::SUCCESS
|
||||
} else {
|
||||
args::run(&argv)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user