13 Commits

Author SHA1 Message Date
amizing25 62a934011b feat: add gitea prebuilt actions
Prebuilt release / prebuilt (push) Successful in 1m42s
2026-07-16 22:59:49 +07:00
amizing25 b559c3b433 chore: remove mhypbase 2026-07-16 22:45:38 +07:00
amizing25 8ed8c7c443 feat: update pattern 2026-07-16 16:56:23 +07:00
amizing25 1ec60097a6 feat: add AccountPlatNative patch for CN SDK auth 2026-06-03 18:47:17 +07:00
amizing25 aec2a1e575 feat(launcher): build executable to require administrator by default
Add rustc-link-arg directives in build.rs to embed a UAC manifest:
- /MANIFEST:EMBED
- /MANIFESTUAC:level='requireAdministrator'
2025-08-22 06:39:39 +07:00
amizing25 7ab400507e feat(launcher): warn if the launcher is not run as admin 2025-08-22 06:08:34 +07:00
amizing25 d66398b9b8 fix: fixed client damaged error 2025-08-22 06:04:37 +07:00
amizing25 ee7fa339fb refactor: minor changes
- use VA directly on interceptor instead of RVA
2025-08-22 05:53:53 +07:00
amizing25 cc2f1865fc fix: update set_dither pattern 2025-05-27 10:54:20 +07:00
amizing25 0c87252394 fix censorship patch 2025-05-23 08:23:35 +07:00
amizing25 61928b9269 feat: Update patterns 2025-05-22 18:42:02 +07:00
amizing25 3f6933c096 feat: add RSA patch for AccountRSAKey 2025-03-16 15:14:45 +07:00
amizing25 bc651397e6 fix: remove hkcheck as it's not needed (yet?) 2025-03-15 05:48:53 +07:00
25 changed files with 680 additions and 186 deletions
+99
View File
@@ -0,0 +1,99 @@
name: Prebuilt release
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
releases: write
jobs:
prebuilt:
runs-on: ubuntu-latest
env:
CARGO_TERM_COLOR: always
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install nightly Rust
uses: dtolnay/rust-toolchain@nightly
with:
targets: x86_64-pc-windows-gnu
- name: Install MinGW and packaging tools
run: |
sudo apt-get update
sudo apt-get install -y mingw-w64 zip unzip jq
- name: Build release binaries
env:
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
run: cargo build --release --target x86_64-pc-windows-gnu -p ext -p launcher
- name: Package prebuilt release
run: |
set -euo pipefail
rm -rf dist
mkdir -p dist
cp target/x86_64-pc-windows-gnu/release/hkrpg.dll dist/
cp target/x86_64-pc-windows-gnu/release/launcher.exe dist/
test -f dist/hkrpg.dll
test -f dist/launcher.exe
(
cd dist
zip -9 -q ../hkrpg.zip hkrpg.dll launcher.exe
)
unzip -l hkrpg.zip
- name: Publish prebuilt release
env:
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_SHA: ${{ gitea.sha }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -euo pipefail
api="${GITEA_API_URL}"
repo="${GITEA_REPOSITORY}"
tag="prebuilt"
auth=(-H "Authorization: token ${GITEA_TOKEN}" -H "Accept: application/json")
release_status="$(curl -sS "${auth[@]}" -o release.json -w '%{http_code}' \
"${api}/repos/${repo}/releases/tags/${tag}" || true)"
case "${release_status}" in
200)
;;
404)
curl -fsS -X POST "${auth[@]}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${tag}\",\"target_commitish\":\"${GITEA_SHA}\",\"name\":\"${tag}\",\"body\":\"Automated prebuilt binaries for ${GITEA_SHA}\",\"draft\":false,\"prerelease\":false}" \
"${api}/repos/${repo}/releases" > release.json
;;
*)
cat release.json
exit 1
;;
esac
release_id="$(jq -er '.id' release.json)"
assets="$(curl -fsS "${auth[@]}" \
"${api}/repos/${repo}/releases/${release_id}/assets")"
while read -r asset_id; do
test -z "${asset_id}" || curl -fsS -X DELETE "${auth[@]}" \
"${api}/repos/${repo}/releases/${release_id}/assets/${asset_id}"
done < <(printf '%s' "${assets}" | jq -r '.[] | select(.name == "hkrpg.zip") | .id')
curl -fsS -X POST "${auth[@]}" \
-F "attachment=@hkrpg.zip;type=application/zip" \
"${api}/repos/${repo}/releases/${release_id}/assets?name=hkrpg.zip"
Generated
+84 -4
View File
@@ -17,6 +17,32 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "dashmap"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]] [[package]]
name = "ext" name = "ext"
version = "0.1.0" version = "0.1.0"
@@ -25,10 +51,17 @@ dependencies = [
"windows", "windows",
] ]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]] [[package]]
name = "hkrpg" name = "hkrpg"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"dashmap",
"ilhook", "ilhook",
"patternscan", "patternscan",
"windows", "windows",
@@ -77,6 +110,15 @@ version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@@ -84,11 +126,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "mhypbase" name = "once_cell"
version = "0.1.0" version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [ dependencies = [
"hkrpg", "cfg-if",
"windows", "libc",
"redox_syscall",
"smallvec",
"windows-link",
] ]
[[package]] [[package]]
@@ -115,6 +168,15 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.1"
@@ -144,6 +206,18 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.100" version = "2.0.100"
@@ -201,6 +275,12 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.1.2" version = "0.1.2"
+8 -1
View File
@@ -1,11 +1,17 @@
[workspace] [workspace]
members = ["hkrpg", "launcher", "mhypbase", "ext"] members = ["hkrpg", "launcher", "ext"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.0"
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true
[workspace.dependencies] [workspace.dependencies]
windows = { version = "0.54.0", features = [ windows = { version = "0.54.0", features = [
"Win32_Foundation", "Win32_Foundation",
@@ -22,6 +28,7 @@ windows = { version = "0.54.0", features = [
] } ] }
ilhook = "2.1.1" ilhook = "2.1.1"
patternscan = "1.2.0" patternscan = "1.2.0"
dashmap = "6.1.0"
# Local crates # Local crates
hkrpg = { path = "hkrpg/" } hkrpg = { path = "hkrpg/" }
+6 -3
View File
@@ -1,8 +1,11 @@
redirect HTTP requests & remove censorship! features:
- redirect HTTP requests
- remove censorship
- replace AccountRSAKey into custom public key (by default, compatible with [hoyo-sdk by xeondev](https://git.xeondev.com/reversedrooms/hoyo-sdk))
currently, this has only been tested on CNBETAWin3.1.53 and may require an update for future versions. currently, this has only been tested on CNBETAWin4.4.51 and may require an update for future versions.
note: if you plan to use `mhypbase.dll`, please make a copy of the original first in case something breaks. Otherwise, simply copy `hkrpg.dll` and `launcher.exe` into the game folder and run `launcher.exe` as an administrator. note: simply copy `hkrpg.dll` and `launcher.exe` into the game folder and run `launcher.exe` as an administrator.
Sources: Sources:
+1
View File
@@ -7,3 +7,4 @@ version.workspace = true
windows.workspace = true windows.workspace = true
ilhook.workspace = true ilhook.workspace = true
patternscan.workspace = true patternscan.workspace = true
dashmap.workspace = true
+9
View File
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudOyZJDKXjYQdUS+wN7d
lKBP2XkhkDO/sx6pttn719o+rJ8y8XOO3rftSoRasvw1nzidDQqEnHl1Aw5+a90D
OQvlgBaIda3npqVmi4X2sEzswR7EH5BOJizx7LAxixcoby3jOQathdpl44wQ04XR
+SIkukvxcL1jTepfuc0Pp+DD+4MU5O4/38b9v+ryizU+1CdwzukV27rkMvOtA6ok
Is6Vkb5ubcAC/W3vCIP5gv+QRdYB6ihzLhs5WI/9Ex2PrLpdpxr7gClvEFxkHcLy
LCvSfp9pvt/tnsYsk1tct0HKEr8tlaJ6t1zEGEvFQZRpBsahl/lhpT15vyM8iyiD
DwIDAQAB
-----END PUBLIC KEY-----
+1
View File
@@ -0,0 +1 @@
<RSAKeyValue><Exponent>AQAB</Exponent><Modulus>hEegnKISgDas5VTuRBUlixB+bvmPvXKa3kVO22UEZjPGMUFLmIl3DhH+dsZo7qJn/GfJCUkP1FA0MJ5Bj8PX8IatLJKIJ9dMCNdnAlkXTlMg86QQAhHZN83vP4swj5ILcrGNKl3YAZ49fvzo7nheuTt0/40f0HkHdNa1dUHECBs=</Modulus></RSAKeyValue>
+47 -54
View File
@@ -2,25 +2,22 @@ use std::sync::{LazyLock, OnceLock};
use windows::{Win32::System::LibraryLoader::GetModuleHandleA, core::s}; use windows::{Win32::System::LibraryLoader::GetModuleHandleA, core::s};
use crate::util::{scan_il2cpp_section, scan_unity_player_section}; use crate::util::scan_il2cpp_section;
const PTR_TO_STRING_ANSI: &str = "E8 ? ? ? ? 48 ? ? 48 85 C0 75 ? 48 8D 4C 24"; const IL2CPP_STRING_NEW_LEN: &str =
const MAKE_INITIAL_URL: &str = "55 41 56 56 57 53 48 83 EC ? 48 8D 6C 24 ? 48 C7 45 ? ? ? ? ? 48 89 D6 48 89 CF E8 ? ? ? ? 84 C0"; // TODO "E8 ? ? ? ? EB ? 31 C0 48 89 06 48 8B 47 ? 48 89 46 ? F2 0F 10 47";
const SET_ELEVATION_DITHER: &str = "E9 ? ? ? ? 0F 28 74 24 ? 48 83 C4 ? 5B 5F 5E 41 5E 41 5F C3 31 F6"; // TODO const MAKE_INITIAL_URL: &str =
const SET_DISTANCE_DITHER: &str = "E8 ? ? ? ? 49 8B 46 ? 48 85 C0 0F 84 ? ? ? ? 48 8B 4D"; // TODO "55 41 56 56 57 53 48 81 EC 90 00 00 00 48 8D AC 24 ? ? ? ? 48 C7 45 ? ? ? ? ? 48 85 C9 74";
const SET_DITHER_ALPHA: &str = "56 57 48 83 EC ? 0F 29 74 24 ? 44 89 C6 0F 28 F1 48 89 CF 80 3D ? ? ? ? ? 75 ? 80 7F"; // TODO const SET_DITHER: &str = "E8 ? ? ? ? 84 C0 75 ? C7 43";
const SET_DITHER_ALPHA_ANIM: &str = "56 57 55 53 48 83 EC ? 44 0F 29 44 24 ? 0F 29 7C 24 ? 0F 29 74 24 ? 44 0F 28 C3 0F 28 F2 0F 28 F9"; // TODO const SDK_PUBLIC_KEY_LITERAL: &str =
const HK_CHECK1: &str = "55 41 56 56 57 53 48 81 EC 00 01 00 00 48 8D AC 24 80 00 00 00 C7 45 7C 00 00 00 00"; "48 8B 0D ? ? ? ? 4C 89 E2 E8 ? ? ? ? 48 89 C6 48 8B 0D ? ? ? ? E8 ? ? ? ? 48 89 C7 48 8B 0D";
const HK_CHECK2: &str = "55 41 57 41 56 41 55 41 54 56 57 53 48 81 EC B8 02 00 00";
#[derive(Default)] #[derive(Default)]
pub struct RVAConfig { pub struct RVAConfig {
pub ptr_to_string_ansi: usize, pub il2cpp_string_new_len: usize,
pub make_initial_url: usize, pub make_initial_url: usize,
pub set_elevation_dither: usize, pub set_dither: usize,
pub set_distance_dither: usize, pub sdk_public_key: usize,
pub set_dither_alpha: usize,
pub set_dither_alpha_anim: usize,
pub hk_check1: usize, pub hk_check1: usize,
pub hk_check2: usize, pub hk_check2: usize,
} }
@@ -34,22 +31,32 @@ pub fn rva_config() -> &'static mut RVAConfig {
pub static GAME_ASSEMBLY_BASE: LazyLock<usize> = pub static GAME_ASSEMBLY_BASE: LazyLock<usize> =
LazyLock::new(|| unsafe { GetModuleHandleA(s!("GameAssembly.dll")).unwrap().0 as usize }); LazyLock::new(|| unsafe { GetModuleHandleA(s!("GameAssembly.dll")).unwrap().0 as usize });
pub static UNITY_PLAYER_BASE: LazyLock<usize> = // pub static UNITY_PLAYER_BASE: LazyLock<usize> =
LazyLock::new(|| unsafe { GetModuleHandleA(s!("UnityPlayer.dll")).unwrap().0 as usize }); // LazyLock::new(|| unsafe { GetModuleHandleA(s!("UnityPlayer.dll")).unwrap().0 as usize });
macro_rules! set_rva { macro_rules! set_rva {
($config:ident, $field:ident, $scan_fn:ident, $rva_pat:expr, $fallback:expr) => { ($base:ident, $config:ident, $field:ident, $scan_fn:ident, $rva_pat:expr, $fallback:expr) => {
if let Some(addr) = unsafe { $scan_fn($rva_pat) } { if $rva_pat == "" {
$config.$field = addr - *GAME_ASSEMBLY_BASE; $config.$field = *$base + $fallback;
println!( println!(
"[hkrpg::addr::set_rva] Found relative address for {} -> 0x{:X}", "[hkrpg::addr::set_rva] Using fallback relative address for {} [{}] -> 0x{:X}",
stringify!($field), stringify!($field),
$config.$field stringify!($base),
$config.$field - *$base
);
} else if let Some(addr) = unsafe { $scan_fn($rva_pat) } {
$config.$field = addr;
println!(
"[hkrpg::addr::set_rva] Found relative address for {} [{}] -> 0x{:X}",
stringify!($field),
stringify!($base),
$config.$field - *$base
); );
} else { } else {
eprintln!( eprintln!(
"[hkrpg::addr::set_rva] Failed to find pattern for {} using {}", "[hkrpg::addr::set_rva] Failed to find pattern for {} [{}] using {}",
stringify!($field), stringify!($field),
stringify!($base),
stringify!($scan_fn) stringify!($scan_fn)
); );
@@ -61,17 +68,19 @@ macro_rules! set_rva {
pub unsafe fn init_rvas() { pub unsafe fn init_rvas() {
let config = rva_config(); let config = rva_config();
// ptr_to_string_ansi // il2cpp_string_new_len
set_rva!( set_rva!(
GAME_ASSEMBLY_BASE,
config, config,
ptr_to_string_ansi, il2cpp_string_new_len,
scan_il2cpp_section, scan_il2cpp_section,
PTR_TO_STRING_ANSI, IL2CPP_STRING_NEW_LEN,
0x0 0x0
); );
// make_initial_url // make_initial_url
set_rva!( set_rva!(
GAME_ASSEMBLY_BASE,
config, config,
make_initial_url, make_initial_url,
scan_il2cpp_section, scan_il2cpp_section,
@@ -79,39 +88,23 @@ pub unsafe fn init_rvas() {
0x0 0x0
); );
// set_elevation_dither // set_dither
set_rva!( set_rva!(
GAME_ASSEMBLY_BASE,
config, config,
set_elevation_dither, set_dither,
scan_il2cpp_section, scan_il2cpp_section,
SET_ELEVATION_DITHER, SET_DITHER,
0x0
);
// set_distance_dither
set_rva!(
config,
set_distance_dither,
scan_il2cpp_section,
SET_DISTANCE_DITHER,
0x0
);
// set_dither_alpha
set_rva!(
config,
set_dither_alpha,
scan_il2cpp_section,
SET_DITHER_ALPHA,
0x0
);
// set_dither_alpha_anim
set_rva!(
config,
set_dither_alpha_anim,
scan_il2cpp_section,
SET_DITHER_ALPHA_ANIM,
0x0 0x0
); );
set_rva!(config, hk_check1, scan_unity_player_section, HK_CHECK1, 0x0); // sdk_public_key_literal
set_rva!(config, hk_check2, scan_unity_player_section, HK_CHECK2, 0x0); set_rva!(
GAME_ASSEMBLY_BASE,
config,
sdk_public_key,
scan_il2cpp_section,
SDK_PUBLIC_KEY_LITERAL,
0x0
)
} }
+2 -2
View File
@@ -1,6 +1,6 @@
use std::{ffi::CString, fmt::Display}; use std::{ffi::CString, fmt::Display};
use crate::addr::{GAME_ASSEMBLY_BASE, rva_config}; use crate::addr::rva_config;
#[repr(transparent)] #[repr(transparent)]
pub struct Il2cppString(usize); pub struct Il2cppString(usize);
@@ -15,7 +15,7 @@ impl Il2cppString {
pub fn new(string: &str) -> Self { pub fn new(string: &str) -> Self {
let func = unsafe { let func = unsafe {
std::mem::transmute::<usize, fn(*const u8, usize) -> usize>( std::mem::transmute::<usize, fn(*const u8, usize) -> usize>(
*GAME_ASSEMBLY_BASE + rva_config().ptr_to_string_ansi, rva_config().il2cpp_string_new_len,
) )
}; };
let len = string.len(); let len = string.len();
+4 -1
View File
@@ -2,13 +2,16 @@ use ilhook::x64::{
CallbackOption, HookFlags, HookPoint, HookType, Hooker, JmpBackRoutine, RetnRoutine, CallbackOption, HookFlags, HookPoint, HookType, Hooker, JmpBackRoutine, RetnRoutine,
}; };
#[derive(Default)]
pub struct Interceptor { pub struct Interceptor {
hooks: Vec<HookPoint>, hooks: Vec<HookPoint>,
} }
type Result<T> = std::result::Result<T, ilhook::HookError>; type Result<T> = std::result::Result<T, ilhook::HookError>;
impl Interceptor { impl Interceptor {
pub const fn new() -> Self {
Interceptor { hooks: Vec::new() }
}
pub fn attach(&mut self, addr: usize, routine: JmpBackRoutine) -> Result<()> { pub fn attach(&mut self, addr: usize, routine: JmpBackRoutine) -> Result<()> {
let hooker = Hooker::new( let hooker = Hooker::new(
addr, addr,
+10 -2
View File
@@ -1,9 +1,10 @@
#![feature(str_from_utf16_endian, once_cell_get_mut)] #![feature(once_cell_get_mut)]
use std::{thread, time::Duration}; use std::{thread, time::Duration};
use modules::{ use modules::{
HkrpgModuleManager, censorship_patch::CensorshipPatch, hk_check::HkCheck, network::Network, HkrpgModuleManager, apn::ApnPatch, censorship_patch::CensorshipPatch, crypto::Crypto,
hk_check::HkCheck, misc::Misc, network::Network,
}; };
use windows::{ use windows::{
Win32::System::{Console, LibraryLoader::GetModuleHandleA}, Win32::System::{Console, LibraryLoader::GetModuleHandleA},
@@ -26,11 +27,18 @@ pub fn main() {
thread::sleep(Duration::from_millis(200)); thread::sleep(Duration::from_millis(200));
} }
let mut mm1 = HkrpgModuleManager::default();
mm1.add::<Misc>();
mm1.init()
.expect("[hkrpg::main] failed to initialize module (Misc)");
addr::init_rvas(); addr::init_rvas();
let mut module_manager = HkrpgModuleManager::default(); let mut module_manager = HkrpgModuleManager::default();
module_manager.add::<ApnPatch>();
module_manager.add::<HkCheck>(); module_manager.add::<HkCheck>();
module_manager.add::<Network>(); module_manager.add::<Network>();
module_manager.add::<Crypto>();
module_manager.add::<CensorshipPatch>(); module_manager.add::<CensorshipPatch>();
module_manager module_manager
.init() .init()
+160
View File
@@ -0,0 +1,160 @@
use dashmap::DashMap;
use ilhook::x64::Registers;
use patternscan::scan_first_match;
use std::{
borrow::Cow,
ffi::{CStr, CString},
path::Path,
sync::{LazyLock, OnceLock},
};
use windows::{
Win32::System::{
LibraryLoader::{GetModuleHandleW, LoadLibraryW},
ProcessStatus::{GetModuleInformation, MODULEINFO},
Threading::GetCurrentProcess,
},
core::{PCWSTR, w},
};
use crate::modules::{HkrpgModule, HkrpgModuleContext};
pub struct ApnPatch;
impl HkrpgModule for HkrpgModuleContext<ApnPatch> {
unsafe fn init(&mut self) -> Result<(), ilhook::HookError> {
if !load_apn_dll() {
return Ok(());
};
let base;
loop {
if let Ok(module) = unsafe { GetModuleHandleW(w!("AccountPlatNative.dll")) } {
base = module.0 as usize;
println!("AccountPlatNative.dll: 0x{base:X}");
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
let Some(string_alloc_rva) = scan_apn("E8 ? ? ? ? 41 89 BF") else {
println!("[AccountPlatNative] failed to scan string_alloc rva. patch disabled");
return Ok(());
};
self.interceptor
.attach(base + string_alloc_rva, on_string_alloc)
.expect("Failed to hook AccountPlatNative string alloc");
println!("[AccountPlatNative] patch enabled");
Ok(())
}
}
extern "win64" fn on_string_alloc(reg: *mut Registers, _: usize) {
const SDK_PUBLIC_KEY: &str = include_str!("../../sdk_public_key.pem");
static STRING_CACHE_MAP: LazyLock<DashMap<Cow<'static, str>, CString>> =
LazyLock::new(DashMap::new);
unsafe {
let content = CStr::from_ptr((*reg).rdx as *const i8).to_string_lossy();
let size = (*reg).r8;
if let Some(cstring) = STRING_CACHE_MAP.get(&content) {
(*reg).rdx = cstring.as_ptr() as u64;
(*reg).r8 = cstring.count_bytes() as u64;
return;
}
// URL string alloc
if content.starts_with("https://") && content.contains(".mihoyo.com") {
let mut new_url = String::from("http://127.0.0.1:21000");
content.split('/').skip(3).for_each(|s| {
new_url.push('/');
new_url.push_str(s);
});
let cstring = STRING_CACHE_MAP
.entry(content.clone())
.or_insert_with(|| CString::new(&*new_url).expect("Failed to create CString"));
(*reg).rdx = cstring.as_ptr() as u64;
(*reg).r8 = new_url.len() as u64;
println!("[AccountPlatNative] url {content} replaced to {new_url}");
}
// RSA public key string alloc
else if size == 268 && content.starts_with("-----BEGIN PUBLIC KEY-----") {
let cstring = STRING_CACHE_MAP
.entry(content.clone())
.or_insert_with(|| CString::new(SDK_PUBLIC_KEY).expect("Failed to create CString"));
(*reg).rdx = cstring.as_ptr() as u64;
(*reg).r8 = SDK_PUBLIC_KEY.len() as u64;
println!("[AccountPlatNative] sdk public key replaced! ");
}
}
}
fn load_apn_dll() -> bool {
let dll_path = "./StarRail_Data/Plugins/x86_64/AccountPlatNative.dll";
if !Path::new(dll_path).exists() {
return false;
}
let wide_dll_path: Vec<u16> = dll_path.encode_utf16().chain(std::iter::once(0)).collect();
let ptr = wide_dll_path.as_ptr();
if let Err(err) = unsafe { LoadLibraryW(PCWSTR::from_raw(ptr)) } {
println!("[AccountPlatNative] Failed to inject {dll_path}. Error: {err:#?}",);
};
true
}
pub fn scan_apn(pat: &str) -> Option<usize> {
let mut slice = apn_slice();
scan_first_match(&mut slice, pat).unwrap().map(|address| {
let slice = apn_slice();
match slice.get(address) {
// jmp sub_xxxxxxx
Some(&0xE8) => {
let offset =
i32::from_le_bytes(slice[address + 1..address + 5].try_into().unwrap());
address + 5 + offset as usize
}
// mov REGISTER, [rip + offset] (0x48 0x8B 0x0D XXXXXXXX)
Some(&0x48) if slice.get(address + 1) == Some(&0x8B) => {
let offset =
i32::from_le_bytes(slice[address + 3..address + 7].try_into().unwrap());
address + 7 + offset as usize
}
_ => address,
}
})
}
fn apn_slice() -> &'static [u8] {
static SLICE: OnceLock<&[u8]> = OnceLock::new();
unsafe {
SLICE.get_or_init(|| {
let module = GetModuleHandleW(w!("AccountPlatNative.dll")).unwrap();
let mut module_info = MODULEINFO {
lpBaseOfDll: std::ptr::null_mut(),
SizeOfImage: 0,
EntryPoint: std::ptr::null_mut(),
};
GetModuleInformation(
GetCurrentProcess(),
module,
&mut module_info,
std::mem::size_of::<MODULEINFO>() as u32,
)
.unwrap();
std::slice::from_raw_parts(
module.0 as *const u8,
module_info.SizeOfImage.try_into().unwrap(),
)
})
}
}
+2 -9
View File
@@ -11,7 +11,7 @@ macro_rules! replace {
$( $(
if $config.$field != 0 { if $config.$field != 0 {
$self.interceptor.replace( $self.interceptor.replace(
$self.base.wrapping_add($config.$field), $config.$field,
CensorshipPatch::on_set_dither, CensorshipPatch::on_set_dither,
)?; )?;
} else { } else {
@@ -24,14 +24,7 @@ macro_rules! replace {
impl HkrpgModule for HkrpgModuleContext<CensorshipPatch> { impl HkrpgModule for HkrpgModuleContext<CensorshipPatch> {
unsafe fn init(&mut self) -> Result<(), ilhook::HookError> { unsafe fn init(&mut self) -> Result<(), ilhook::HookError> {
let config = rva_config(); let config = rva_config();
replace!( replace!(self, config, set_dither);
self,
config,
set_distance_dither,
set_elevation_dither,
set_dither_alpha,
set_dither_alpha_anim
);
Ok(()) Ok(())
} }
} }
+23
View File
@@ -0,0 +1,23 @@
use crate::{addr::rva_config, il2cpp_string::Il2cppString};
use super::{HkrpgModule, HkrpgModuleContext};
pub struct Crypto;
const ACCOUNT_RSA_KEY_REPLACEMENT: &str = include_str!("../../sdk_public_key.xml");
impl HkrpgModule for HkrpgModuleContext<Crypto> {
unsafe fn init(&mut self) -> Result<(), ilhook::HookError> {
let config = rva_config();
if config.sdk_public_key != 0 {
unsafe {
*(config.sdk_public_key as *mut Il2cppString) =
Il2cppString::new(ACCOUNT_RSA_KEY_REPLACEMENT)
}
println!("[crypto::init] AccountRSAKey replaced")
} else {
println!("[crypto::init] pattern is outdated! disabling AccountRSAKey replacement")
}
Ok(())
}
}
+5 -8
View File
@@ -10,14 +10,11 @@ impl HkrpgModule for HkrpgModuleContext<HkCheck> {
unsafe fn init(&mut self) -> Result<(), ilhook::HookError> { unsafe fn init(&mut self) -> Result<(), ilhook::HookError> {
let config = rva_config(); let config = rva_config();
if config.hk_check1 != 0 && config.hk_check2 != 0 { if config.hk_check1 != 0 && config.hk_check2 != 0 {
self.interceptor.replace( self.interceptor
self.base.wrapping_add(config.hk_check1), .replace(config.hk_check1, HkCheck::replacement)?;
HkCheck::replacement, self.interceptor
)?; .replace(config.hk_check2, HkCheck::replacement)?;
self.interceptor.replace( println!("[hk_check::init] hk_check bypassed")
self.base.wrapping_add(config.hk_check2),
HkCheck::replacement,
)?;
} }
Ok(()) Ok(())
} }
+39
View File
@@ -0,0 +1,39 @@
use crate::modules::{HkrpgModule, HkrpgModuleContext};
use ilhook::x64::Registers;
use std::ffi::CStr;
use windows::{
Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress},
core::s,
};
pub struct Misc;
impl HkrpgModule for HkrpgModuleContext<Misc> {
unsafe fn init(&mut self) -> Result<(), ilhook::HookError> {
unsafe {
let ws32 = GetModuleHandleA(s!("Ws2_32.dll")).unwrap();
let get_addr_info = GetProcAddress(ws32, s!("getaddrinfo")).unwrap();
self.interceptor
.attach(get_addr_info as usize, Misc::on_get_addr_info)?;
println!("[misc::init] initialized")
}
Ok(())
}
}
impl Misc {
pub unsafe extern "win64" fn on_get_addr_info(reg: *mut Registers, _: usize) {
unsafe {
let host = CStr::from_ptr((*reg).rcx as *const i8).to_string_lossy();
if host.contains("globaldp-")
&& (host.contains("bhsr.com") || host.contains("starrails.com"))
{
println!("[*] [on_get_addr_info] {host} -> 0.0.0.0");
std::ptr::copy_nonoverlapping(c"0.0.0.0".as_ptr(), (*reg).rcx as *mut i8, 9);
}
}
}
}
+6 -3
View File
@@ -2,12 +2,15 @@ use std::marker::PhantomData;
use crate::{addr, interceptor::Interceptor}; use crate::{addr, interceptor::Interceptor};
pub mod apn;
pub mod censorship_patch; pub mod censorship_patch;
pub mod crypto;
pub mod hk_check; pub mod hk_check;
pub mod misc;
pub mod network; pub mod network;
pub struct HkrpgModuleContext<T> { pub struct HkrpgModuleContext<T> {
base: usize, _base: usize,
interceptor: Interceptor, interceptor: Interceptor,
_module_type: PhantomData<T>, _module_type: PhantomData<T>,
} }
@@ -15,8 +18,8 @@ pub struct HkrpgModuleContext<T> {
impl<T> HkrpgModuleContext<T> { impl<T> HkrpgModuleContext<T> {
fn new(base: usize) -> Self { fn new(base: usize) -> Self {
Self { Self {
base, _base: base,
interceptor: Interceptor::default(), interceptor: Interceptor::new(),
_module_type: PhantomData, _module_type: PhantomData,
} }
} }
+6 -7
View File
@@ -9,11 +9,10 @@ pub struct Network;
impl HkrpgModule for HkrpgModuleContext<Network> { impl HkrpgModule for HkrpgModuleContext<Network> {
unsafe fn init(&mut self) -> Result<(), ilhook::HookError> { unsafe fn init(&mut self) -> Result<(), ilhook::HookError> {
let config = rva_config(); let config = rva_config();
if config.make_initial_url != 0 && config.ptr_to_string_ansi != 0 { if config.make_initial_url != 0 && config.il2cpp_string_new_len != 0 {
self.interceptor.attach( self.interceptor
self.base.wrapping_add(config.make_initial_url), .attach(config.make_initial_url, Network::on_make_initial_url)?;
Network::on_make_initial_url, println!("[network::init] network patch enabled")
)?;
} else { } else {
println!("[network::init] pattern is outdated! disabling http redirection") println!("[network::init] pattern is outdated! disabling http redirection")
} }
@@ -23,7 +22,7 @@ impl HkrpgModule for HkrpgModuleContext<Network> {
impl Network { impl Network {
const SDK_URL: &str = "http://127.0.0.1:21000"; const SDK_URL: &str = "http://127.0.0.1:21000";
// const DISPATCH_URL: &str = "http://127.0.0.1:21000"; const DISPATCH_URL: &str = "http://127.0.0.1:21000";
const REDIRECT_SDK: bool = true; const REDIRECT_SDK: bool = true;
const REDIRECT_DISPATCH: bool = true; const REDIRECT_DISPATCH: bool = true;
@@ -39,7 +38,7 @@ impl Network {
s if (s.contains("bhsr.com") || s.contains("starrails.com")) s if (s.contains("bhsr.com") || s.contains("starrails.com"))
&& Self::REDIRECT_DISPATCH => && Self::REDIRECT_DISPATCH =>
{ {
Self::SDK_URL.to_string() Self::DISPATCH_URL.to_string()
} }
s => { s => {
println!("Leaving request as-is: {s}"); println!("Leaving request as-is: {s}");
+68 -39
View File
@@ -10,7 +10,7 @@ use windows::{
core::s, core::s,
}; };
use crate::addr::{GAME_ASSEMBLY_BASE, UNITY_PLAYER_BASE}; use crate::addr::GAME_ASSEMBLY_BASE;
#[allow(static_mut_refs)] #[allow(static_mut_refs)]
unsafe fn game_assembly_slice() -> &'static [u8] { unsafe fn game_assembly_slice() -> &'static [u8] {
@@ -38,49 +38,78 @@ unsafe fn game_assembly_slice() -> &'static [u8] {
} }
} }
#[allow(static_mut_refs)]
unsafe fn unity_player_slice() -> &'static [u8] {
static mut SLICE: OnceCell<&[u8]> = OnceCell::new();
unsafe {
SLICE.get_or_init(|| {
let module = GetModuleHandleA(s!("UnityPlayer.dll")).unwrap();
let mut module_info = MODULEINFO {
lpBaseOfDll: std::ptr::null_mut(),
SizeOfImage: 0,
EntryPoint: std::ptr::null_mut(),
};
GetModuleInformation(
GetCurrentProcess(),
module,
&mut module_info,
std::mem::size_of::<MODULEINFO>() as u32,
)
.unwrap();
std::slice::from_raw_parts(
module.0 as *const u8,
module_info.SizeOfImage.try_into().unwrap(),
)
})
}
}
pub unsafe fn scan_il2cpp_section(pat: &str) -> Option<usize> { pub unsafe fn scan_il2cpp_section(pat: &str) -> Option<usize> {
let mut slice = unsafe { game_assembly_slice() }; let mut slice = unsafe { game_assembly_slice() };
scan_first_match(&mut slice, pat).unwrap().map(|address| { scan_first_match(&mut slice, pat).unwrap().map(|address| {
let slice = unsafe { game_assembly_slice() }; let slice = unsafe { game_assembly_slice() };
let instruction = &slice[address..address + 1][0]; match slice.get(address) {
if *instruction == 0xE8 { // jmp sub_xxxxxxx
let offset = i32::from_le_bytes((&slice[address + 1..address + 1 + 4]).try_into().unwrap()); Some(&0xE8) => {
let pointer = offset as usize + 5 + address; let offset =
return GAME_ASSEMBLY_BASE.wrapping_add(pointer); i32::from_le_bytes(slice[address + 1..address + 5].try_into().unwrap());
GAME_ASSEMBLY_BASE.wrapping_add(address + 5 + offset as usize)
}
// mov rcx, [rip + offset] (0x48 0x8B 0x0D XXXXXXXX)
Some(&0x48)
if slice.get(address + 1) == Some(&0x8B)
&& slice.get(address + 2) == Some(&0x0D) =>
{
let offset =
i32::from_le_bytes(slice[address + 3..address + 7].try_into().unwrap());
GAME_ASSEMBLY_BASE.wrapping_add(address + 7 + offset as usize)
}
_ => GAME_ASSEMBLY_BASE.wrapping_add(address),
} }
GAME_ASSEMBLY_BASE.wrapping_add(address)
}) })
} }
pub unsafe fn scan_unity_player_section(pat: &str) -> Option<usize> { // #[allow(static_mut_refs)]
let mut slice = unsafe { unity_player_slice() }; // unsafe fn unity_player_slice() -> &'static [u8] {
scan_first_match(&mut slice, pat) // static mut SLICE: OnceCell<&[u8]> = OnceCell::new();
.unwrap() // unsafe {
.map(|loc| UNITY_PLAYER_BASE.wrapping_add(loc)) // SLICE.get_or_init(|| {
} // let module = GetModuleHandleA(s!("UnityPlayer.dll")).unwrap();
// let mut module_info = MODULEINFO {
// lpBaseOfDll: std::ptr::null_mut(),
// SizeOfImage: 0,
// EntryPoint: std::ptr::null_mut(),
// };
// GetModuleInformation(
// GetCurrentProcess(),
// module,
// &mut module_info,
// std::mem::size_of::<MODULEINFO>() as u32,
// )
// .unwrap();
// std::slice::from_raw_parts(
// module.0 as *const u8,
// module_info.SizeOfImage.try_into().unwrap(),
// )
// })
// }
// }
// pub unsafe fn scan_unity_player_section(pat: &str) -> Option<usize> {
// let mut slice = unsafe { unity_player_slice() };
// scan_first_match(&mut slice, pat).unwrap().map(|address| {
// let slice = unsafe { unity_player_slice() };
// match slice.get(address) {
// // jmp sub_xxxxxxx
// Some(&0xE8) => {
// let offset =
// i32::from_le_bytes(slice[address + 1..address + 5].try_into().unwrap());
// UNITY_PLAYER_BASE.wrapping_add(address + 5 + offset as usize)
// }
// // mov rcx, [rip + offset] (0x48 0x8B 0x0D XXXXXXXX)
// Some(&0x48)
// if slice.get(address + 1) == Some(&0x8B)
// && slice.get(address + 2) == Some(&0x0D) =>
// {
// let offset =
// i32::from_le_bytes(slice[address + 3..address + 7].try_into().unwrap());
// UNITY_PLAYER_BASE.wrapping_add(address + 7 + offset as usize)
// }
// _ => UNITY_PLAYER_BASE.wrapping_add(address),
// }
// })
// }
+54
View File
@@ -0,0 +1,54 @@
use std::{env, fs, path::PathBuf, process::Command};
fn main() {
match env::var("CARGO_CFG_TARGET_ENV").as_deref() {
Ok("msvc") => {
println!("cargo:rustc-link-arg-bin=launcher=/MANIFEST:EMBED");
println!("cargo:rustc-link-arg-bin=launcher=/MANIFESTUAC:level='requireAdministrator'");
}
Ok("gnu") => {
let Some(out_dir) = env::var_os("OUT_DIR") else {
return;
};
let out_dir = PathBuf::from(out_dir);
let manifest = out_dir.join("launcher.manifest");
let resource = out_dir.join("launcher.rc");
let object = out_dir.join("launcher-manifest.o");
if fs::write(&manifest, MANIFEST).is_err() || fs::write(&resource, RESOURCE).is_err() {
return;
}
let Ok(status) = Command::new("x86_64-w64-mingw32-windres")
.current_dir(&out_dir)
.args(["launcher.rc", "-O", "coff", "-o"])
.arg(&object)
.status()
else {
return;
};
if !status.success() {
return;
}
println!("cargo:rustc-link-arg-bin=launcher={}", object.display());
}
_ => {}
}
}
const RESOURCE: &str = "1 24 \"launcher.manifest\"\n";
const MANIFEST: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
"#;
+45 -3
View File
@@ -4,16 +4,17 @@ use std::ffi::CString;
use std::ptr::null_mut; use std::ptr::null_mut;
use windows::Win32::Foundation::{CloseHandle, GetLastError, HANDLE}; use windows::Win32::Foundation::{CloseHandle, GetLastError, HANDLE};
use windows::Win32::Security::{GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation};
use windows::Win32::System::Diagnostics::Debug::WriteProcessMemory; use windows::Win32::System::Diagnostics::Debug::WriteProcessMemory;
use windows::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress}; use windows::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
use windows::Win32::System::Memory::{ use windows::Win32::System::Memory::{
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE, VirtualAllocEx, VirtualFreeEx, MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE, VirtualAllocEx, VirtualFreeEx,
}; };
use windows::Win32::System::Threading::{ use windows::Win32::System::Threading::{
CREATE_SUSPENDED, CreateProcessA, CreateRemoteThread, PROCESS_INFORMATION, ResumeThread, CREATE_SUSPENDED, CreateProcessA, CreateRemoteThread, OpenProcessToken, PROCESS_INFORMATION,
STARTUPINFOA, WaitForSingleObject, ResumeThread, STARTUPINFOA, WaitForSingleObject,
}; };
use windows::core::{PSTR, s}; use windows::core::{Error, PSTR, s};
fn inject_standard(h_target: HANDLE, dll_path: &str) -> bool { fn inject_standard(h_target: HANDLE, dll_path: &str) -> bool {
unsafe { unsafe {
@@ -70,7 +71,48 @@ fn inject_standard(h_target: HANDLE, dll_path: &str) -> bool {
} }
} }
fn is_running_as_admin() -> Result<bool, Error> {
unsafe {
let mut token_handle = HANDLE::default();
let current_process = windows::Win32::System::Threading::GetCurrentProcess();
if OpenProcessToken(current_process, TOKEN_QUERY, &mut token_handle).is_err() {
return Err(windows::core::Error::from_win32());
}
let mut elevation = TOKEN_ELEVATION::default();
let mut size = std::mem::size_of::<TOKEN_ELEVATION>() as u32;
let success = GetTokenInformation(
token_handle,
TokenElevation,
Some(&mut elevation as *mut _ as *mut _),
size,
&mut size,
);
let _ = CloseHandle(token_handle);
if success.is_ok() {
Ok(elevation.TokenIsElevated != 0)
} else {
Err(windows::core::Error::from_win32())
}
}
}
fn wait_exit() {
println!("Press any key to exit...");
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
}
fn main() { fn main() {
if !is_running_as_admin().unwrap_or_default() {
println!("launcher need to be launched as admin");
wait_exit();
}
let current_dir = std::env::current_dir().unwrap(); let current_dir = std::env::current_dir().unwrap();
let dll_path = current_dir.join("hkrpg.dll"); let dll_path = current_dir.join("hkrpg.dll");
if !dll_path.is_file() { if !dll_path.is_file() {
-12
View File
@@ -1,12 +0,0 @@
[package]
name = "mhypbase"
edition = "2024"
version.workspace = true
[lib]
name = "mhypbase"
crate-type = ["cdylib"]
[dependencies]
windows.workspace = true
hkrpg.workspace = true
-7
View File
@@ -1,7 +0,0 @@
fn main() {
println!(
"cargo:rustc-link-search=native={}",
std::env::var("OUT_DIR").unwrap()
);
println!("cargo:rustc-link-arg=/DEF:mhypbase/mhypbase.def");
}
-3
View File
@@ -1,3 +0,0 @@
LIBRARY MHYPBASE
EXPORTS
Initialize @1
-27
View File
@@ -1,27 +0,0 @@
use windows::Win32::{
Foundation::HINSTANCE,
System::{
SystemServices::DLL_PROCESS_ATTACH,
Threading::{GetCurrentThread, TerminateThread},
},
};
#[unsafe(no_mangle)]
#[allow(non_snake_case, unused_variables)]
extern "cdecl" fn Initialize() -> bool {
std::thread::sleep(std::time::Duration::from_secs(2));
unsafe {
TerminateThread(GetCurrentThread(), 0).unwrap();
}
false
}
#[allow(non_snake_case)]
#[unsafe(no_mangle)]
unsafe extern "system" fn DllMain(_: HINSTANCE, call_reason: u32, _: *mut ()) {
if call_reason == DLL_PROCESS_ATTACH {
std::thread::spawn(hkrpg::main);
}
}