Skip to content

Commit

Permalink
add rc_client_can_pause function (#292)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jamiras authored Dec 2, 2023
1 parent 8afec6c commit 64fcb9e
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 0 deletions.
8 changes: 8 additions & 0 deletions include/rc_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
65 changes: 65 additions & 0 deletions src/rc_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/rc_client_external.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/rc_client_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
111 changes: 111 additions & 0 deletions test/test_rc_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----- */

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions test/test_rc_client_external.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 */
Expand Down

0 comments on commit 64fcb9e

Please sign in to comment.