From 64fcb9ea3cf990e65343057ace9271ff3b77428e Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Sat, 2 Dec 2023 08:22:48 -0700 Subject: [PATCH] add rc_client_can_pause function (#292) --- include/rc_client.h | 8 +++ src/rc_client.c | 65 +++++++++++++++++++ src/rc_client_external.h | 2 + src/rc_client_internal.h | 3 + test/test_rc_client.c | 111 +++++++++++++++++++++++++++++++++ test/test_rc_client_external.c | 25 ++++++++ 6 files changed, 214 insertions(+) diff --git a/include/rc_client.h b/include/rc_client.h index da0c40bd..00a3f6ed 100644 --- a/include/rc_client.h +++ b/include/rc_client.h @@ -629,6 +629,14 @@ void rc_client_do_frame(rc_client_t* client); */ void rc_client_idle(rc_client_t* client); +/** + * Determines if a sufficient amount of frames have been processed since the last call to rc_client_can_pause. + * Should not be called unless the client is trying to pause. + * If false is returned, and frames_remaining is not NULL, frames_remaining will be set to the number of frames + * still required before pause is allowed, which can be converted to a time in seconds for displaying to the user. + */ +int rc_client_can_pause(rc_client_t* client, uint32_t* frames_remaining); + /** * Informs the runtime that the emulator has been reset. Will reset all achievements and leaderboards * to their initial state (includes hiding indicators/trackers). diff --git a/src/rc_client.c b/src/rc_client.c index b2a6463f..aa0becd8 100644 --- a/src/rc_client.c +++ b/src/rc_client.c @@ -23,6 +23,9 @@ #define RC_CLIENT_UNKNOWN_GAME_ID (uint32_t)-1 #define RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS (10 * 60) /* ten minutes */ +#define RC_MINIMUM_UNPAUSED_FRAMES 20 +#define RC_PAUSE_DECAY_MULTIPLIER 4 + enum { RC_CLIENT_ASYNC_NOT_ABORTED = 0, RC_CLIENT_ASYNC_ABORTED = 1, @@ -91,6 +94,7 @@ rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, return NULL; client->state.hardcore = 1; + client->state.required_unpaused_frames = RC_MINIMUM_UNPAUSED_FRAMES; client->callbacks.read_memory = read_memory_function; client->callbacks.server_call = server_call_function; @@ -4892,6 +4896,24 @@ void rc_client_do_frame(rc_client_t* client) rc_client_raise_pending_events(client, client->game); } + /* we've processed a frame. if there's a pause delay in effect, process it */ + if (client->state.unpaused_frame_decay > 0) { + client->state.unpaused_frame_decay--; + + if (client->state.unpaused_frame_decay == 0 && + client->state.required_unpaused_frames > RC_MINIMUM_UNPAUSED_FRAMES) { + /* the full decay has elapsed and a penalty still exists. + * lower the penalty and reset the decay counter */ + client->state.required_unpaused_frames >>= 1; + + if (client->state.required_unpaused_frames <= RC_MINIMUM_UNPAUSED_FRAMES) + client->state.required_unpaused_frames = RC_MINIMUM_UNPAUSED_FRAMES; + + client->state.unpaused_frame_decay = + client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1) - 1; + } + } + rc_client_idle(client); } @@ -5072,6 +5094,49 @@ void rc_client_reset(rc_client_t* client) rc_client_raise_pending_events(client, client->game); } +int rc_client_can_pause(rc_client_t* client, uint32_t* frames_remaining) +{ +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->can_pause) + return client->state.external_client->can_pause(frames_remaining); +#endif + + if (frames_remaining) + *frames_remaining = 0; + + /* pause is always allowed in softcore */ + if (!rc_client_get_hardcore_enabled(client)) + return 1; + + /* a full decay means we haven't processed any frames since the last time this was called. */ + if (client->state.unpaused_frame_decay == client->state.required_unpaused_frames * RC_PAUSE_DECAY_MULTIPLIER) + return 1; + + /* if less than RC_MINIMUM_UNPAUSED_FRAMES have been processed, don't allow the pause */ + if (client->state.unpaused_frame_decay > client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1)) { + if (frames_remaining) { + *frames_remaining = client->state.unpaused_frame_decay - + client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1); + } + return 0; + } + + /* we're going to allow the emulator to pause. calculate how many frames are needed before the next + * pause will be allowed. */ + + if (client->state.unpaused_frame_decay > 0) { + /* The user has paused within the decay window. Require a longer + * run of unpaused frames before allowing the next pause */ + if (client->state.required_unpaused_frames < 5 * 60) /* don't make delay longer then 5 seconds */ + client->state.required_unpaused_frames += RC_MINIMUM_UNPAUSED_FRAMES; + } + + /* require multiple unpaused_frames windows to decay the penalty */ + client->state.unpaused_frame_decay = client->state.required_unpaused_frames * RC_PAUSE_DECAY_MULTIPLIER; + + return 1; +} + size_t rc_client_progress_size(rc_client_t* client) { size_t result; diff --git a/src/rc_client_external.h b/src/rc_client_external.h index 32258ff3..62e23d91 100644 --- a/src/rc_client_external.h +++ b/src/rc_client_external.h @@ -14,6 +14,7 @@ typedef void (*rc_client_external_enable_logging_func_t)(rc_client_t* client, in typedef void (*rc_client_external_set_event_handler_func_t)(rc_client_t* client, rc_client_event_handler_t handler); typedef void (*rc_client_external_set_read_memory_func_t)(rc_client_t* client, rc_client_read_memory_func_t handler); typedef void (*rc_client_external_set_get_time_millisecs_func_t)(rc_client_t* client, rc_get_time_millisecs_func_t handler); +typedef int (*rc_client_external_can_pause_func_t)(uint32_t* frames_remaining); typedef void (*rc_client_external_set_int_func_t)(int value); typedef int (*rc_client_external_get_int_func_t)(void); @@ -116,6 +117,7 @@ typedef struct rc_client_external_t rc_client_external_action_func_t do_frame; rc_client_external_action_func_t idle; rc_client_external_get_int_func_t is_processing_required; + rc_client_external_can_pause_func_t can_pause; rc_client_external_action_func_t reset; rc_client_external_progress_size_func_t progress_size; diff --git a/src/rc_client_internal.h b/src/rc_client_internal.h index 9c545477..98ff25f8 100644 --- a/src/rc_client_internal.h +++ b/src/rc_client_internal.h @@ -317,6 +317,9 @@ typedef struct rc_client_state_t { rc_client_raintegration_t* raintegration; #endif + uint16_t unpaused_frame_decay; + uint16_t required_unpaused_frames; + uint8_t hardcore; uint8_t encore_mode; uint8_t spectator_mode; diff --git a/test/test_rc_client.c b/test/test_rc_client.c index 618b060b..528a971b 100644 --- a/test/test_rc_client.c +++ b/test/test_rc_client.c @@ -1148,6 +1148,25 @@ static void test_get_user_game_summary_with_unsupported_unlocks(void) rc_client_destroy(g_client); } +static void test_get_user_game_summary_no_achievements(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_empty, no_unlocks); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); + + ASSERT_NUM_EQUALS(summary.points_core, 0); + ASSERT_NUM_EQUALS(summary.points_unlocked, 0); + + rc_client_destroy(g_client); +} /* ----- load game ----- */ @@ -7340,6 +7359,94 @@ static void test_reset_hides_widgets(void) rc_client_destroy(g_client); } +/* ----- pause ----- */ + +static void test_can_pause(void) +{ + uint16_t frames_needed, frames_needed2, frames_needed3, frames_needed4; + uint32_t frames_remaining; + int i; + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + + rc_client_do_frame(g_client); + + /* first pause should always be allowed */ + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + frames_needed = g_client->state.unpaused_frame_decay; + + /* if no frames have been processed, the client is still paused, so pause is allowed */ + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + ASSERT_NUM_EQUALS(g_client->state.unpaused_frame_decay, frames_needed); + + /* do a few frames (not enough to allow pause) - pause should still not be allowed */ + for (i = 0; i < 10; i++) + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 0); + ASSERT_NUM_EQUALS(frames_remaining, 10); + ASSERT_NUM_EQUALS(g_client->state.unpaused_frame_decay, frames_needed - 10); + + /* do enough frames to allow pause, but not clear out the decay value. + * pause should be allowed, and the decay value should be reset to a higher value. */ + for (i = 0; i < 20; i++) + rc_client_do_frame(g_client); + + ASSERT_NUM_GREATER(g_client->state.unpaused_frame_decay, 0); + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + frames_needed2 = g_client->state.unpaused_frame_decay; + ASSERT_NUM_GREATER(frames_needed2, frames_needed); + + /* do enough frames to allow pause before - should not allow pause now */ + for (i = 0; i < 25; i++) + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 0); + ASSERT_NUM_EQUALS(frames_remaining, 15); + ASSERT_NUM_EQUALS(g_client->state.unpaused_frame_decay, frames_needed2 - 25); + + /* do enough frames to allow pause, but not clear out the decay value. + * pause should be allowed, and the decay value should be reset to an even higher value. */ + for (i = 0; i < 35; i++) + rc_client_do_frame(g_client); + + ASSERT_NUM_GREATER(g_client->state.unpaused_frame_decay, 0); + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + frames_needed3 = g_client->state.unpaused_frame_decay; + ASSERT_NUM_GREATER(frames_needed3, frames_needed2); + + /* completely clear out the decay. decay value should drop, but not all the way. */ + for (i = 0; i < frames_needed3; i++) + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + frames_needed4 = g_client->state.unpaused_frame_decay; + ASSERT_NUM_LESS(frames_needed4, frames_needed3); + ASSERT_NUM_GREATER(frames_needed4, frames_needed); + + /* completely clear out the decay. decay value should drop back to default + * have to do this twice to get through the decayed cycles */ + for (i = 0; i < frames_needed4 * 2; i++) + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + ASSERT_NUM_EQUALS(g_client->state.unpaused_frame_decay, frames_needed); + + /* disable hardcore. pause should be allowed immediately */ + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + + rc_client_destroy(g_client); +} + /* ----- progress ----- */ static void test_deserialize_progress_updates_widgets(void) @@ -8061,6 +8168,7 @@ void test_client(void) { TEST(test_get_user_game_summary_encore_mode); TEST(test_get_user_game_summary_with_unsupported_and_unofficial); TEST(test_get_user_game_summary_with_unsupported_unlocks); + TEST(test_get_user_game_summary_no_achievements); /* load game */ TEST(test_load_game_required_fields); @@ -8201,6 +8309,9 @@ void test_client(void) { /* reset */ TEST(test_reset_hides_widgets); + /* pause */ + TEST(test_can_pause); + /* deserialize_progress */ TEST(test_deserialize_progress_updates_widgets); TEST(test_deserialize_progress_null); diff --git a/test/test_rc_client_external.c b/test/test_rc_client_external.c index a99fefa2..92cb6068 100644 --- a/test/test_rc_client_external.c +++ b/test/test_rc_client_external.c @@ -1058,6 +1058,30 @@ static void rc_client_external_reset(void) g_external_event = "reset"; } +static int rc_client_external_can_pause(uint32_t* frames_remaining) +{ + *frames_remaining = g_external_int ? 0 : 10; + + return g_external_int; +} + +static void test_can_pause(void) +{ + uint32_t frames_remaining; + g_client = mock_client_with_external(); + g_client->state.external_client->can_pause = rc_client_external_can_pause; + + g_external_int = 0; + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 0); + ASSERT_NUM_EQUALS(frames_remaining, 10); + + g_external_int = 1; + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + + rc_client_destroy(g_client); +} + static void test_reset(void) { g_client = mock_client_with_external(); @@ -1186,6 +1210,7 @@ void test_client_external(void) { TEST(test_is_processing_required); TEST(test_do_frame); TEST(test_idle); + TEST(test_can_pause); TEST(test_reset); /* progress */