Compare commits

...

3 Commits

Author SHA1 Message Date
60d3b50568 survival gauge 2023-08-28 22:14:00 +02:00
867474df82 hispeed_auto longest bpm 2023-08-28 22:14:00 +02:00
5b72856a42 hispeed_auto 2023-08-28 22:14:00 +02:00
3 changed files with 500 additions and 0 deletions

View File

@ -43,6 +43,17 @@
<!-- Display offset adjust value on score result screen (requires hidden_is_offset, won't be sent over network) -->
<show_offset __type="bool">0</show_offset>
<!-- Hi-speed -->
<!-- Auto set hi-speed to match previously set BPM (0: off, 1: target higher bpm on soflan, 2: target lower bpm on soflan, 3: target longest bpm on soflan) -->
<hispeed_auto __type="u8">0</hispeed_auto>
<!-- Default target BPM, 0 to disable (requires hispeed_auto) -->
<!-- Note: target is still updated when manually changing hi-speed (except soflan and "?" charts) -->
<hispeed_default_bpm __type="u16">0</hispeed_default_bpm>
<!-- Gauge options -->
<!-- Turn hard gauge into survival gauge -->
<survival_gauge __type="bool">0</survival_gauge>
<!-- Result screen display patches -->
<!-- Display details on result screen by default (no need to press yellow button) -->
<show_details __type="bool">0</show_details>

View File

@ -6,6 +6,7 @@
struct popnhax_config {
bool practice_mode;
bool hidden_is_offset;
bool survival_gauge;
bool show_offset;
bool show_fast_slow;
bool show_details;
@ -43,6 +44,8 @@ struct popnhax_config {
uint8_t debounce;
bool enhanced_polling_stats;
int8_t enhanced_polling_priority;
uint8_t hispeed_auto;
uint16_t hispeed_default_bpm;
};
#endif

View File

@ -90,6 +90,8 @@ struct popnhax_config config = {};
PSMAP_BEGIN(config_psmap, static)
PSMAP_MEMBER_REQ(PSMAP_PROPERTY_TYPE_BOOL, struct popnhax_config, hidden_is_offset,
"/popnhax/hidden_is_offset")
PSMAP_MEMBER_REQ(PSMAP_PROPERTY_TYPE_BOOL, struct popnhax_config, survival_gauge,
"/popnhax/survival_gauge")
PSMAP_MEMBER_REQ(PSMAP_PROPERTY_TYPE_BOOL, struct popnhax_config, show_fast_slow,
"/popnhax/show_fast_slow")
PSMAP_MEMBER_REQ(PSMAP_PROPERTY_TYPE_BOOL, struct popnhax_config, show_details,
@ -164,6 +166,10 @@ PSMAP_MEMBER_REQ(PSMAP_PROPERTY_TYPE_BOOL, struct popnhax_config, enhanced_polli
"/popnhax/enhanced_polling_stats")
PSMAP_MEMBER_REQ(PSMAP_PROPERTY_TYPE_S8, struct popnhax_config, enhanced_polling_priority,
"/popnhax/enhanced_polling_priority")
PSMAP_MEMBER_REQ(PSMAP_PROPERTY_TYPE_U8, struct popnhax_config, hispeed_auto,
"/popnhax/hispeed_auto")
PSMAP_MEMBER_REQ(PSMAP_PROPERTY_TYPE_U16, struct popnhax_config, hispeed_default_bpm,
"/popnhax/hispeed_default_bpm")
PSMAP_END
enum BufferIndexes {
@ -3974,6 +3980,476 @@ static bool patch_enhanced_polling_stats()
return true;
}
/*
* auto hi-speed
*/
bool g_longest_bpm_old_chart = false;
bool g_bypass_hispeed = false; //bypass target update for mystery bpm and soflan songs
bool g_mystery_bpm = 0;
uint32_t g_hispeed = 0;
uint32_t g_hispeed_addr = 0;
uint32_t g_target_bpm = 0;
uint16_t *g_base_bpm_ptr = 0; //will point to g_low_bpm or g_hi_bpm according to mode
uint16_t g_low_bpm = 0;
uint16_t g_hi_bpm = 0;
uint16_t g_longest_bpm = 0;
unsigned char *g_chart_addr = 0;
typedef struct chart_chunk_s {
uint32_t timestamp;
uint16_t operation;
uint16_t data;
uint32_t duration;
} chart_chunk_t;
#define CHART_OP_BPM 0x0445
#define CHART_OP_END 0x0645
typedef struct bpm_list_s {
uint16_t bpm;
uint32_t duration;
struct bpm_list_s *next;
} bpm_list_t;
static uint32_t add_duration(bpm_list_t *list, uint16_t bpm, uint32_t duration)
{
if (list->bpm == 0)
{
list->bpm = bpm;
list->duration = duration;
return duration;
}
while (list->next != NULL)
{
if (list->bpm == bpm)
{
list->duration += duration;
return list->duration;
}
list = list->next;
}
//new entry
bpm_list_t *entry = (bpm_list_t *)malloc(sizeof(bpm_list_t));
entry->bpm = bpm;
entry->duration = duration;
entry->next = NULL;
list->next = entry;
return duration;
}
static void destroy_list(bpm_list_t *list)
{
while (list != NULL)
{
bpm_list_t *tmp = list;
list = list->next;
free(tmp);
}
}
/* the goal is to set g_longest_bpm to the most used bpm from the chart present in memory at address g_chart_addr
*/
void compute_longest_bpm(){
chart_chunk_t* chunk = NULL;
unsigned char *chart_ptr = g_chart_addr;
int chunk_size = g_longest_bpm_old_chart? 8 : 12;
uint32_t prev_timestamp = 0x64; //game adds 0x64 to every timestamp of a chart upon loading
uint16_t prev_bpm = 0;
bpm_list_t *list = (bpm_list_t *)malloc(sizeof(bpm_list_t));
list->bpm = 0;
list->duration = 0;
list->next = NULL;
uint32_t max_duration = 0;
uint16_t longest_bpm = 0;
do {
chunk = (chart_chunk_t*) chart_ptr;
if ( chunk->operation == CHART_OP_BPM || chunk->operation == CHART_OP_END )
{
if (prev_bpm)
{
uint32_t bpm_dur = add_duration(list, prev_bpm, chunk->timestamp - prev_timestamp); //will add to existing entry or create a new one if not present
if (bpm_dur > max_duration)
{
max_duration = bpm_dur;
longest_bpm = prev_bpm;
}
}
prev_bpm = chunk->data;
prev_timestamp = chunk->timestamp;
}
chart_ptr += chunk_size;
} while ( chunk->operation != CHART_OP_END );
destroy_list(list);
g_longest_bpm = longest_bpm;
}
void (*real_set_hispeed)();
void hook_set_hispeed()
{
g_bypass_hispeed = false;
__asm("mov %0, eax\n":"=r"(g_hispeed_addr): :);
real_set_hispeed();
}
void (*real_retrieve_chart_addr)();
void hook_retrieve_chart_addr()
{
__asm("push eax\n");
__asm("push ecx\n");
__asm("push edx\n");
__asm("mov %0, esi\n":"=a"(g_chart_addr): :);
__asm("call %0\n"::"r"(compute_longest_bpm));
__asm("pop edx\n");
__asm("pop ecx\n");
__asm("pop eax\n");
real_retrieve_chart_addr();
}
void (*real_retrieve_chart_addr_old)();
void hook_retrieve_chart_addr_old()
{
__asm("push eax\n");
__asm("push ecx\n");
__asm("push edx\n");
__asm("mov %0, edi\n":"=a"(g_chart_addr): :);
__asm("call %0\n"::"r"(compute_longest_bpm));
__asm("pop edx\n");
__asm("pop ecx\n");
__asm("pop eax\n");
real_retrieve_chart_addr_old();
}
/*
* This is called a lot of times: when arriving on option select, and when changing/navigating *any* option
* I'm hooking here to set hi-speed to the target BPM
*/
double g_hispeed_double = 0;
void (*real_read_hispeed)();
void hook_read_hispeed()
{
__asm("push eax\n");
__asm("push ecx\n");
__asm("push edx\n");
__asm("mov %0, word ptr [ebp+0xA1A]\n":"=a"(g_low_bpm): :);
__asm("mov %0, word ptr [ebp+0xA1C]\n":"=a"(g_hi_bpm): :);
__asm("mov %0, byte ptr [ebp+0xA1E]\n":"=a"(g_mystery_bpm): :);
if ( g_bypass_hispeed || g_target_bpm == 0 ) //bypass for mystery BPM and soflan songs (to avoid hi-speed being locked since target won't change)
{
__asm("jmp leave_read_hispeed\n");
}
g_hispeed_double = (double)g_target_bpm / (double)(*g_base_bpm_ptr/10.0);
g_hispeed = (uint32_t)(g_hispeed_double+0.5); //rounding to nearest
if (g_hispeed > 0x64) g_hispeed = 0x0A;
if (g_hispeed < 0x0A) g_hispeed = 0x64;
__asm("and edi, 0xFFFF0000\n"); //keep existing popkun and hidden status values
__asm("or edi, dword ptr[%0]\n"::"m"(g_hispeed)); //fix hispeed initial display on option screen
__asm("mov eax, dword ptr[%0]\n"::"m"(g_hispeed_addr));
__asm("mov dword ptr[eax], edi\n");
__asm("leave_read_hispeed:\n");
__asm("pop edx\n");
__asm("pop ecx\n");
__asm("pop eax\n");
real_read_hispeed();
}
void (*real_increase_hispeed)();
void hook_increase_hispeed()
{
__asm("push eax\n");
__asm("push edx\n");
__asm("push ecx\n");
if ( g_mystery_bpm || g_low_bpm != g_hi_bpm )
{
g_bypass_hispeed = true;
__asm("jmp leave_increase_hispeed\n");
}
//increase hispeed
__asm("mov ecx, dword ptr[edi]\n");
__asm("inc ecx\n");
__asm("cmp ecx, 0x65\n");
__asm("jb skip_hispeed_rollover_high\n");
__asm("mov ecx, 0x0A\n");
__asm("skip_hispeed_rollover_high:\n");
//compute resulting bpm using exact same formula as game (base bpm in eax, multiplier in ecx)
__asm("movsx eax, %0\n"::"m"(g_hi_bpm));
__asm("cwde\n");
__asm("movsx ecx,cx\n");
__asm("imul ecx,eax\n");
__asm("mov eax, 0x66666667\n");
__asm("imul ecx\n");
__asm("sar edx,2\n");
__asm("mov eax,edx\n");
__asm("shr eax,0x1F\n");
__asm("add eax,edx\n");
__asm("mov %0, eax\n":"=m"(g_target_bpm): :);
__asm("leave_increase_hispeed:\n");
__asm("pop ecx\n");
__asm("pop edx\n");
__asm("pop eax\n");
real_increase_hispeed();
}
void (*real_decrease_hispeed)();
void hook_decrease_hispeed()
{
__asm("push eax\n");
__asm("push edx\n");
__asm("push ecx\n");
if ( g_mystery_bpm || g_low_bpm != g_hi_bpm )
{
g_bypass_hispeed = true;
__asm("jmp leave_decrease_hispeed\n");
}
//decrease hispeed
__asm("mov ecx, dword ptr[edi]\n");
__asm("dec ecx\n");
__asm("cmp ecx, 0x0A\n");
__asm("jge skip_hispeed_rollover_low\n");
__asm("mov ecx, 0x64\n");
__asm("skip_hispeed_rollover_low:\n");
//compute resulting bpm using exact same formula as game (base bpm in eax, multiplier in ecx)
__asm("movsx eax, %0\n"::"m"(g_hi_bpm));
__asm("cwde\n");
__asm("movsx ecx,cx\n");
__asm("imul ecx,eax\n");
__asm("mov eax, 0x66666667\n");
__asm("imul ecx\n");
__asm("sar edx,2\n");
__asm("mov eax,edx\n");
__asm("shr eax,0x1F\n");
__asm("add eax,edx\n");
__asm("mov %0, eax\n":"=m"(g_target_bpm): :);
__asm("leave_decrease_hispeed:\n");
__asm("pop ecx\n");
__asm("pop edx\n");
__asm("pop eax\n");
real_decrease_hispeed();
}
bool patch_hispeed_auto(uint8_t mode, uint16_t default_bpm)
{
DWORD dllSize = 0;
char *data = getDllData(g_game_dll_fn, &dllSize);
g_base_bpm_ptr = &g_hi_bpm;
if (mode == 2)
g_base_bpm_ptr = &g_low_bpm;
else if (mode == 3)
g_base_bpm_ptr = &g_longest_bpm;
g_target_bpm = default_bpm;
/* retrieve hi-speed address */
{
int64_t pattern_offset = search(data, dllSize, "\x66\x89\x0C\x07\x0F\xB6\x45\x04", 8, 0);
if (pattern_offset == -1) {
LOG("popnhax: auto hi-speed: cannot find hi-speed address\n");
return false;
}
uint64_t patch_addr = (int64_t)data + pattern_offset + 0x04;
MH_CreateHook((LPVOID)(patch_addr), (LPVOID)hook_set_hispeed,
(void **)&real_set_hispeed);
}
/* write new hispeed according to target bpm */
{
int64_t pattern_offset = search(data, dllSize, "\x98\x50\x66\x8B\x85\x1A\x0A\x00\x00\x8B\xCF", 11, 0);
if (pattern_offset == -1) {
LOG("popnhax: auto hi-speed: cannot find hi-speed apply address\n");
return false;
}
uint64_t patch_addr = (int64_t)data + pattern_offset - 0x07;
MH_CreateHook((LPVOID)(patch_addr), (LPVOID)hook_read_hispeed,
(void **)&real_read_hispeed);
}
/* update target bpm on hispeed increase */
{
int64_t pattern_offset = search(data, dllSize, "\x66\xFF\x07\x0F\xB7\x07\x66\x83\xF8\x64", 10, 0);
if (pattern_offset == -1) {
LOG("popnhax: auto hi-speed: cannot find hi-speed increase\n");
return false;
}
uint64_t patch_addr = (int64_t)data + pattern_offset;
MH_CreateHook((LPVOID)(patch_addr), (LPVOID)hook_increase_hispeed,
(void **)&real_increase_hispeed);
}
/* update target bpm on hispeed decrease */
{
int64_t pattern_offset = search(data, dllSize, "\x66\xFF\x0F\x0F\xB7\x07\x66\x83\xF8\x0A", 10, 0);
if (pattern_offset == -1) {
LOG("popnhax: auto hi-speed: cannot find hi-speed decrease\n");
return false;
}
uint64_t patch_addr = (int64_t)data + pattern_offset;
MH_CreateHook((LPVOID)(patch_addr), (LPVOID)hook_decrease_hispeed,
(void **)&real_decrease_hispeed);
}
/* compute longest bpm for mode 3 */
if (mode == 3)
{
int64_t pattern_offset = search(data, dllSize, "\x00\x00\x72\x05\xB9\xFF", 6, 0);
if (pattern_offset == -1) {
LOG("popnhax: auto hi-speed: cannot find chart address\n");
return false;
}
/* detect if usaneko+ */
uint64_t patch_addr = (int64_t)data + pattern_offset + 0x0F;
uint8_t check_byte = *((uint8_t *)(patch_addr + 1));
if (check_byte == 0x04)
{
patch_addr += 9;
g_longest_bpm_old_chart = true;
LOG("popnhax: auto hi-speed: old game version\n");
MH_CreateHook((LPVOID)(patch_addr), (LPVOID)hook_retrieve_chart_addr_old,
(void **)&real_retrieve_chart_addr_old);
}
else
{
patch_addr += 11;
MH_CreateHook((LPVOID)(patch_addr), (LPVOID)hook_retrieve_chart_addr,
(void **)&real_retrieve_chart_addr);
}
}
LOG("popnhax: auto hi-speed enabled");
if (g_target_bpm != 0)
LOG(" with default %hubpm", g_target_bpm);
if (mode == 2)
LOG(" (lower bpm target)");
else if (mode == 3)
LOG(" (longest bpm target)");
else
LOG(" (higher bpm target)");
LOG("\n");
return true;
}
/* HARD GAUGE SURVIVAL*/
uint8_t g_hard_gauge_selected = false;
void (*real_survival_flag_hard_gauge)();
void hook_survival_flag_hard_gauge()
{
__asm("cmp bl, 0\n");
__asm("jne no_hard_gauge\n");
g_hard_gauge_selected = false;
__asm("cmp cl, 2\n");
__asm("jne no_hard_gauge\n");
g_hard_gauge_selected = true;
__asm("no_hard_gauge:\n");
real_survival_flag_hard_gauge();
}
void (*real_survival_flag_hard_gauge_old)();
void hook_survival_flag_hard_gauge_old()
{
__asm("cmp bl, 0\n");
__asm("jne no_hard_gauge_old\n");
g_hard_gauge_selected = false;
__asm("cmp dl, 2\n");
__asm("jne no_hard_gauge_old\n");
g_hard_gauge_selected = true;
__asm("no_hard_gauge_old:\n");
real_survival_flag_hard_gauge_old();
}
void (*real_check_survival_gauge)();
void hook_check_survival_gauge()
{
if ( g_hard_gauge_selected )
{
__asm("mov al, 1\n");
__asm("ret\n");
}
real_check_survival_gauge();
}
bool patch_hard_gauge_survival()
{
DWORD dllSize = 0;
char *data = getDllData(g_game_dll_fn, &dllSize);
/* change is_survival_gauge function behavior */
{
int64_t pattern_offset = search(data, dllSize, "\x33\xC9\x83\xF8\x04\x0F\x94\xC1\x8A\xC1", 10, 0);
if (pattern_offset == -1) {
LOG("popnhax: survival gauge: cannot find survival gauge check function\n");
return false;
}
uint64_t patch_addr = (int64_t)data + pattern_offset + 0x02;
MH_CreateHook((LPVOID)(patch_addr), (LPVOID)hook_check_survival_gauge,
(void **)&real_check_survival_gauge);
}
/* hook commit option to flag hard gauge being selected */
{
/* find option commit function (unilab) */
int64_t pattern_offset = search(data, dllSize, "\x89\x48\x0C\x8B\x56\x10\x89\x50\x10\x66\x8B\x4E\x14\x66\x89\x48\x14\x5B\xC3\xCC", 20, 0);
if (pattern_offset == -1) {
/* wasn't found, look for older function */
int64_t first_loc = search(data, dllSize, "\x0F\xB6\xC3\x03\xCF\x8D", 6, 0);
if (first_loc == -1) {
LOG("popnhax: survival gauge: cannot find option commit function (1)\n");
return false;
}
pattern_offset = search(data, 0x50, "\x89\x50\x0C", 3, first_loc);
if (pattern_offset == -1) {
LOG("popnhax: survival gauge: cannot find option commit function (2)\n");
return false;
}
uint64_t patch_addr = (int64_t)data + pattern_offset;
MH_CreateHook((LPVOID)(patch_addr), (LPVOID)hook_survival_flag_hard_gauge_old,
(void **)&real_survival_flag_hard_gauge_old);
}
else
{
uint64_t patch_addr = (int64_t)data + pattern_offset;
MH_CreateHook((LPVOID)(patch_addr), (LPVOID)hook_survival_flag_hard_gauge,
(void **)&real_survival_flag_hard_gauge);
}
}
LOG("popnhax: hard gauge is survival gauge\n");
return true;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
@ -4167,6 +4643,10 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserv
patch_hd_resolution(config.force_hd_resolution);
}
if (config.survival_gauge) {
patch_hard_gauge_survival();
}
if (config.hidden_is_offset){
patch_hidden_is_offset();
if (config.show_offset){
@ -4255,6 +4735,12 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserv
patch_enhanced_polling_stats();
}
}
if (config.hispeed_auto)
{
patch_hispeed_auto(config.hispeed_auto, config.hispeed_default_bpm);
}
#if DEBUG == 1
patch_get_time();
#endif