From a29f3ba34ceb681ff69f694f0903b0c61c1088a4 Mon Sep 17 00:00:00 2001 From: Pedro Montes Alcalde Date: Mon, 2 Mar 2026 14:58:45 -0300 Subject: [PATCH 1/9] timer: Overhaul the logic and callings (#290) * meow * miao * :3 * aaaaaaa * cheese * bbbbbbbbb * blehhhhhh * miao * :D * nyan * small * splits * last tick fix * maybe * hehe * docstrings * docs change * Fix segfault on no splits * remove asserts * Dont run reset when not running * actually reset this time i swear * avoid reset desync * suggestions * meow * miao * format nyaaaa --- docs/auto-splitters.md | 3 +- src/gui/actions.c | 18 +- src/gui/app_window.c | 46 ++- src/gui/component/clock.c | 11 +- src/gui/component/components.c | 2 +- src/gui/component/components.h | 2 + src/gui/component/detailed-timer.c | 7 +- src/gui/component/pb.c | 5 +- src/gui/component/prev-segment.c | 8 +- src/gui/component/splits.c | 36 ++- src/gui/game.c | 3 - src/gui/game.h | 2 +- src/gui/timer.c | 357 ++++++++++++++--------- src/gui/timer.h | 8 +- src/keybinds/delayed_callbacks.h | 2 +- src/keybinds/delayed_handlers.c | 2 +- src/keybinds/keybinds_callbacks.c | 2 +- src/lasr/auto-splitter.c | 26 +- src/lasr/auto-splitter.h | 6 +- src/lasr/utils.c | 25 +- src/lasr/utils.h | 1 + src/main.c | 2 +- src/timer.c | 445 ++++++++++++++++++----------- src/timer.h | 44 +-- 24 files changed, 665 insertions(+), 398 deletions(-) diff --git a/docs/auto-splitters.md b/docs/auto-splitters.md index eee20d2..fba8860 100644 --- a/docs/auto-splitters.md +++ b/docs/auto-splitters.md @@ -160,7 +160,8 @@ end * To solve that, we only want to split when we enter a loading screen (old is false, current is true), but we also don't want to split on the first loading screen as we have the assumption that the first loading screen is when the run starts. So that's where our loadCount comes in handy, we can just check if we are on the first one and only split when we aren't. ### `isLoading` -Pauses the timer whenever true is being returned. +Marks the timer as "loading"/"paused". When paused, it will start adding time to LT (Loading Time), effectively pausing RTA. +Only has an effect on RTA/LRT (Load Removed Time). Doesnt affect splitter logic at all. * Runs every 1000 / `refreshRate` milliseconds. ```lua process('GameBlaBlaBla.exe') diff --git a/src/gui/actions.c b/src/gui/actions.c index b3f5f72..c08c84a 100644 --- a/src/gui/actions.c +++ b/src/gui/actions.c @@ -1,7 +1,9 @@ #include "src/gui/actions.h" #include "src/gui/app_window.h" #include "src/gui/game.h" +#include "src/gui/timer.h" #include "src/lasr/auto-splitter.h" +#include "src/lasr/utils.h" #include "src/settings/settings.h" #include #include @@ -69,7 +71,7 @@ void open_activated(GSimpleAction* action, } else { win = ls_app_window_new(LS_APP(app)); } - if (is_run_started(win->timer)) { + if (win->timer->running) { GtkWidget* warning = gtk_message_dialog_new( GTK_WINDOW(win), GTK_DIALOG_MODAL, @@ -234,6 +236,9 @@ void close_activated(GSimpleAction* action, } else { win = ls_app_window_new(LS_APP(app)); } + + timer_stop_and_reset(win); + if (win->game && win->timer) { ls_app_window_clear_game(win); } @@ -345,7 +350,7 @@ void open_auto_splitter(GSimpleAction* action, } else { win = ls_app_window_new(LS_APP(app)); } - if (is_run_started(win->timer)) { + if (win->timer->running) { GtkWidget* warning = gtk_message_dialog_new( GTK_WINDOW(win), GTK_DIALOG_MODAL, @@ -391,14 +396,7 @@ void open_auto_splitter(GSimpleAction* action, config_save(); // Restart auto-splitter if it was running - const bool was_asl_enabled = atomic_load(&auto_splitter_enabled); - if (was_asl_enabled) { - atomic_store(&auto_splitter_enabled, false); - while (atomic_load(&auto_splitter_running) && was_asl_enabled) { - // wait, this will be very fast so its ok to just spin - } - atomic_store(&auto_splitter_enabled, true); - } + restart_auto_splitter(); g_free(filename); } diff --git a/src/gui/app_window.c b/src/gui/app_window.c index 4c53899..d25d57a 100644 --- a/src/gui/app_window.c +++ b/src/gui/app_window.c @@ -10,6 +10,9 @@ #include "src/lasr/auto-splitter.h" #include "src/settings/settings.h" #include "src/settings/utils.h" +#include "src/timer.h" +#include +#include #include extern atomic_bool exit_requested; /*!< Set to 1 when LibreSplit is exiting */ @@ -230,7 +233,6 @@ void ls_app_window_destroy(GtkWidget* widget, gpointer data) gboolean ls_app_window_step(gpointer data) { LSAppWindow* win = data; - long long now = ls_time_now(); static int set_cursor; if (win->opts.hide_cursor && !set_cursor) { GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(win)); @@ -240,37 +242,51 @@ gboolean ls_app_window_step(gpointer data) set_cursor = 1; } } + if (win->timer) { - ls_timer_step(win->timer, now); + ls_timer_step(win->timer); + + // printf("RTA: %llu; LT: %llu; LRT: %llu; GT: %llu; GT?: %d\n", + // win->timer->realTime, + // win->timer->loadingTime, + // (win->timer->realTime - win->timer->loadingTime), + // win->timer->gameTime, + // win->timer->usingGameTime); if (atomic_load(&auto_splitter_enabled)) { - if (atomic_load(&call_start) && !win->timer->loading) { - timer_start(win, true); + if (atomic_load(&run_using_game_time_call)) { + win->timer->usingGameTime = atomic_load(&run_using_game_time); + atomic_store(&run_using_game_time_call, false); + } + if (atomic_load(&call_start)) { + timer_start(win); atomic_store(&call_start, 0); } if (atomic_load(&call_split)) { - timer_split(win, true); + timer_split(win); atomic_store(&call_split, 0); } if (atomic_load(&toggle_loading)) { win->timer->loading = !win->timer->loading; - if (win->timer->running && win->timer->loading) { - timer_stop(win); - } else if (win->timer->started && !win->timer->running && !win->timer->loading) { - timer_start(win, true); + + if (win->timer->running) { + if (win->timer->loading) { + timer_pause(win); + } else { + timer_unpause(win); + } } atomic_store(&toggle_loading, 0); } - if (atomic_load(&call_reset)) { - timer_reset(win); - atomic_store(&run_started, false); - atomic_store(&call_reset, 0); - } if (atomic_load(&update_game_time)) { // Update the timer with the game time from auto-splitter - win->timer->time = atomic_load(&game_time_value); + win->timer->gameTime = atomic_load(&game_time_value); atomic_store(&update_game_time, false); } + if (atomic_load(&call_reset)) { + timer_stop_and_reset(win); + atomic_store(&call_reset, 0); + } } } process_delayed_handlers(win); diff --git a/src/gui/component/clock.c b/src/gui/component/clock.c index 12ce0ec..45f3ded 100644 --- a/src/gui/component/clock.c +++ b/src/gui/component/clock.c @@ -106,10 +106,9 @@ static void timer_draw(LSComponent* self_, const ls_game* game, const ls_timer* { LSTimer* self = (LSTimer*)self_; char str[256], millis[256]; - int curr; - curr = timer->curr_split; - if (curr == game->split_count) { + unsigned int curr = timer->curr_split; + if (curr && curr == game->split_count) { --curr; } @@ -118,10 +117,10 @@ static void timer_draw(LSComponent* self_, const ls_game* game, const ls_timer* remove_class(self->time, "losing"); remove_class(self->time, "best-split"); - if (curr == game->split_count) { + if (curr && curr == game->split_count) { curr = game->split_count - 1; } - if (timer->time <= 0) { + if (ls_timer_get_time(timer, true) <= 0) { add_class(self->time, "delay"); } else { if (timer->curr_split == game->split_count @@ -139,7 +138,7 @@ static void timer_draw(LSComponent* self_, const ls_game* game, const ls_timer* } } } - ls_time_millis_string(str, &millis[1], timer->time); + ls_time_millis_string(str, &millis[1], ls_timer_get_time(timer, true)); millis[0] = '.'; gtk_label_set_text(GTK_LABEL(self->time_seconds), str); gtk_label_set_text(GTK_LABEL(self->time_millis), millis); diff --git a/src/gui/component/components.c b/src/gui/component/components.c index c30451d..11f26b3 100644 --- a/src/gui/component/components.c +++ b/src/gui/component/components.c @@ -16,7 +16,7 @@ LSComponent* ls_component_wr_new(void); LSComponentAvailable ls_components[] = { { "title", ls_component_title_new }, { "splits", ls_component_splits_new }, - // { "timer", ls_component_timer_new }, + // { "timer", ls_component_timer_new }, { "detailed-timer", ls_component_detailed_timer_new }, { "prev-segment", ls_component_prev_segment_new }, { "best-sum", ls_component_best_sum_new }, diff --git a/src/gui/component/components.h b/src/gui/component/components.h index 94b4d6c..f8e59f4 100644 --- a/src/gui/component/components.h +++ b/src/gui/component/components.h @@ -28,6 +28,8 @@ typedef struct LSComponentOps { void (*skip)(LSComponent* self, const ls_timer* timer); void (*unsplit)(LSComponent* self, const ls_timer* timer); void (*stop_reset)(LSComponent* self, ls_timer* timer); + void (*pause)(LSComponent* self, ls_timer* timer); + void (*unpause)(LSComponent* self, ls_timer* timer); void (*cancel_run)(LSComponent* self, ls_timer* timer); } LSComponentOps; diff --git a/src/gui/component/detailed-timer.c b/src/gui/component/detailed-timer.c index eb037ba..229ae32 100644 --- a/src/gui/component/detailed-timer.c +++ b/src/gui/component/detailed-timer.c @@ -170,9 +170,8 @@ static void detailed_timer_draw(LSComponent* self_, const ls_game* game, const l char str[256], millis[10] = { 0 }, seg[256], seg_millis[10] = { 0 }; char pb[256] = "PB: "; char best[256] = "Best: "; - int curr; - curr = timer->curr_split; + unsigned int curr = timer->curr_split; if (curr == game->split_count) { --curr; } @@ -185,7 +184,7 @@ static void detailed_timer_draw(LSComponent* self_, const ls_game* game, const l if (curr == game->split_count) { curr = game->split_count - 1; } - if (timer->time <= 0) { + if (ls_timer_get_time(timer, true) <= 0) { add_class(self->time, "delay"); } else { if (timer->curr_split == game->split_count @@ -203,7 +202,7 @@ static void detailed_timer_draw(LSComponent* self_, const ls_game* game, const l } } } - ls_time_millis_string(str, &millis[1], timer->time); + ls_time_millis_string(str, &millis[1], ls_timer_get_time(timer, true)); if (millis[1] != '\0') millis[0] = '.'; gtk_label_set_text(GTK_LABEL(self->time_seconds), str); diff --git a/src/gui/component/pb.c b/src/gui/component/pb.c index 6d29cc8..26c5a26 100644 --- a/src/gui/component/pb.c +++ b/src/gui/component/pb.c @@ -116,7 +116,8 @@ static void pb_draw(LSComponent* self_, const ls_game* game, char str[256]; remove_class(self->personal_best, "time"); gtk_label_set_text(GTK_LABEL(self->personal_best), "-"); - if (timer->curr_split == game->split_count + if (game->split_count + && timer->curr_split == game->split_count && timer->split_times[game->split_count - 1] && (!game->split_times[game->split_count - 1] || (timer->split_times[game->split_count - 1] @@ -125,7 +126,7 @@ static void pb_draw(LSComponent* self_, const ls_game* game, ls_time_string( str, timer->split_times[game->split_count - 1]); gtk_label_set_text(GTK_LABEL(self->personal_best), str); - } else if (game->split_times[game->split_count - 1]) { + } else if (game->split_count && game->split_times[game->split_count - 1]) { add_class(self->personal_best, "time"); ls_time_string( str, game->split_times[game->split_count - 1]); diff --git a/src/gui/component/prev-segment.c b/src/gui/component/prev-segment.c index ee21933..3af0a15 100644 --- a/src/gui/component/prev-segment.c +++ b/src/gui/component/prev-segment.c @@ -117,8 +117,8 @@ static void prev_segment_draw(LSComponent* self_, const ls_game* game, LSPrevSegment* self = (LSPrevSegment*)self_; const char* label; char str[256]; - int prev, curr = timer->curr_split; - if (curr == game->split_count) { + unsigned int prev, curr = timer->curr_split ? timer->curr_split - 1 : 0; + if (game->split_count && curr == game->split_count) { --curr; } @@ -129,7 +129,7 @@ static void prev_segment_draw(LSComponent* self_, const ls_game* game, gtk_label_set_text(GTK_LABEL(self->previous_segment), "-"); label = PREVIOUS_SEGMENT; - if (timer->segment_deltas[curr] > 0) { + if (timer->segment_deltas && timer->segment_deltas[curr] > 0) { // Live segment label = LIVE_SEGMENT; remove_class(self->previous_segment, "best-segment"); @@ -143,7 +143,7 @@ static void prev_segment_draw(LSComponent* self_, const ls_game* game, // Previous segment if (timer->curr_split) { prev = timer->curr_split - 1; - if (timer->segment_deltas[prev]) { + if (timer->segment_deltas && timer->segment_deltas[prev]) { if (timer->split_info[prev] & LS_INFO_BEST_SEGMENT) { add_class(self->previous_segment, "best-segment"); diff --git a/src/gui/component/splits.c b/src/gui/component/splits.c index bcdd5ac..8f974b1 100644 --- a/src/gui/component/splits.c +++ b/src/gui/component/splits.c @@ -11,7 +11,7 @@ */ typedef struct LSSplits { LSComponent base; /*!< The base struct that is extended */ - int split_count; /*!< The number of splits */ + unsigned int split_count; /*!< The number of splits */ GtkWidget* container; /*!< The container for the splits */ GtkWidget* splits; GtkWidget* split_last; @@ -144,7 +144,6 @@ static void splits_show_game(LSComponent* self_, const ls_game* game, { LSSplits* self = (LSSplits*)self_; char str[256]; - int i; self->split_count = game->split_count; self->split_rows = calloc(self->split_count, sizeof(GtkWidget*)); @@ -180,7 +179,7 @@ static void splits_show_game(LSComponent* self_, const ls_game* game, GString* icons_css_src = g_string_new(".split-icon { background-repeat: no-repeat; background-position: center; min-width: 20px; min-height: 20px; background-size: 20px; margin-right: 4px; }"); - for (i = 0; i < self->split_count; ++i) { + for (unsigned int i = 0; i < self->split_count; ++i) { self->split_rows[i] = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); add_class(self->split_rows[i], "split"); gtk_widget_set_hexpand(self->split_rows[i], TRUE); @@ -271,7 +270,8 @@ static void splits_show_game(LSComponent* self_, const ls_game* game, } gtk_widget_show(self->splits); - splits_trailer(self_); + if (self->split_count) + splits_trailer(self_); } /** @@ -310,10 +310,9 @@ static void splits_draw(LSComponent* self_, const ls_game* game, const ls_timer* { LSSplits* self = (LSSplits*)self_; char str[256]; - int i; - for (i = 0; i < self->split_count; ++i) { + for (unsigned int i = 0; i < self->split_count; ++i) { if (i == timer->curr_split - && timer->start_time) { + && timer->started) { add_class(self->split_rows[i], "current-split"); } else { remove_class(self->split_rows[i], "current-split"); @@ -377,7 +376,7 @@ static void splits_draw(LSComponent* self_, const ls_game* game, const ls_timer* if (self->split_count) { int width; int time_width = 0, delta_width = 0; - for (i = 0; i < self->split_count; ++i) { + for (unsigned int i = 0; i < self->split_count; ++i) { width = gtk_widget_get_allocated_width(self->split_deltas[i]); if (width > delta_width) { delta_width = width; @@ -387,7 +386,7 @@ static void splits_draw(LSComponent* self_, const ls_game* game, const ls_timer* time_width = width; } } - for (i = 0; i < self->split_count; ++i) { + for (unsigned int i = 0; i < self->split_count; ++i) { if (delta_width) { gtk_widget_set_size_request( self->split_deltas[i], delta_width, -1); @@ -401,9 +400,16 @@ static void splits_draw(LSComponent* self_, const ls_game* game, const ls_timer* } } - splits_trailer(self_); + if (self->split_count) + splits_trailer(self_); } +/** + * Scrolls to the current split if it's not visible. + * + * @param self_ The splits component itself. + * @param timer The timer instance. + */ static void splits_scroll_to_split(LSComponent* self_, const ls_timer* timer) { LSSplits* self = (LSSplits*)self_; @@ -412,9 +418,13 @@ static void splits_scroll_to_split(LSComponent* self_, const ls_timer* timer) int scroller_h; double curr_scroll; double min_scroll, max_scroll; - int prev = timer->curr_split - 1; - int curr = timer->curr_split; - int next = timer->curr_split + 1; + + if (timer->game->split_count == 0) + return; + + unsigned int prev = timer->curr_split ? timer->curr_split - 1 : 0; + unsigned int curr = timer->curr_split; + unsigned int next = timer->curr_split + 1; if (prev < 0) { prev = 0; } diff --git a/src/gui/game.c b/src/gui/game.c index 7cace7d..56cdffe 100644 --- a/src/gui/game.c +++ b/src/gui/game.c @@ -1,7 +1,6 @@ #include "game.h" #include "src/gui/component/components.h" #include "src/gui/theming.h" -#include "src/lasr/auto-splitter.h" #include "src/settings/definitions.h" extern AppConfig cfg; @@ -15,8 +14,6 @@ void ls_app_window_clear_game(LSAppWindow* win) { GList* l; - atomic_store(&run_finished, false); - gtk_widget_hide(win->box); gtk_widget_show_all(win->welcome_box->box); diff --git a/src/gui/game.h b/src/gui/game.h index 6562b69..9f87ce3 100644 --- a/src/gui/game.h +++ b/src/gui/game.h @@ -5,4 +5,4 @@ void ls_app_window_clear_game(LSAppWindow* win); void ls_app_window_show_game(LSAppWindow* win); void save_game(ls_game* game); -void timer_start(LSAppWindow* win, bool updateComponents); +void timer_start(LSAppWindow* win); diff --git a/src/gui/timer.c b/src/gui/timer.c index 035eace..b6438c3 100644 --- a/src/gui/timer.c +++ b/src/gui/timer.c @@ -1,180 +1,265 @@ #include "timer.h" #include "game.h" #include "src/gui/component/components.h" -#include "src/lasr/auto-splitter.h" +#include "src/lasr/utils.h" #include "src/timer.h" -void timer_reset(LSAppWindow* win) +/** + * Stops the timer if it's running, otherwise resets it. If the timer is reset, the current run will be saved to history if enabled. + * + * @param win The LibreSplit window + */ +void timer_stop_and_reset(LSAppWindow* win) { - if (win->timer) { - GList* l; - if (win->timer->running) { - ls_timer_stop(win->timer); - for (l = win->components; l != NULL; l = l->next) { - LSComponent* component = l->data; - if (component->ops->stop_reset) { - component->ops->stop_reset(component, win->timer); - } - } + if (!win->timer) + return; + + if (win->timer->running) { + ls_timer_stop(win->timer); + } + + if (ls_timer_reset(win->timer)) { + ls_app_window_clear_game(win); + ls_app_window_show_game(win); + save_game(win->game); + } + + for (GList* l = win->components; l != NULL; l = l->next) { + LSComponent* component = l->data; + if (component->ops->stop_reset) { + component->ops->stop_reset(component, win->timer); } + } +} + +/** + * Starts the timer, if it's not already running. If the timer is already running, it does a split. + * + * @param win The LibreSplit window + */ +void timer_start_split(LSAppWindow* win) +{ + if (!win->timer) + return; + + if (!win->timer->started) { // To start again a reset needs to happen + if (ls_timer_start(win->timer)) { + save_game(win->game); + } + } else { + ls_timer_split(win->timer); + } + + for (GList* l = win->components; l != NULL; l = l->next) { + LSComponent* component = l->data; + if (component->ops->start_split) { + component->ops->start_split(component, win->timer); + } + } +} + +/** + * Starts the timer, if it's not already running. If the timer is already running, it does nothing. + * + * @param win The LibreSplit window + */ +void timer_start(LSAppWindow* win) +{ + if (!win->timer) + return; + + if (win->timer->running) + return; // Timer is already running, do nothing + + if (ls_timer_start(win->timer)) { + save_game(win->game); + } + + for (GList* l = win->components; l != NULL; l = l->next) { + LSComponent* component = l->data; + if (component->ops->start_split) { + component->ops->start_split(component, win->timer); + } + } +} + +/** + * Stops the timer if it's running, otherwise resets it. If the timer is reset, the current run will be saved to history if enabled. + * + * @param win The LibreSplit window + */ +void timer_stop_or_reset(LSAppWindow* win) +{ + if (!win->timer) + return; + + if (win->timer->running) { + ls_timer_stop(win->timer); + } else { + // Restart LASR on reset + restart_auto_splitter(); + if (ls_timer_reset(win->timer)) { ls_app_window_clear_game(win); ls_app_window_show_game(win); save_game(win->game); } - for (l = win->components; l != NULL; l = l->next) { - LSComponent* component = l->data; - if (component->ops->stop_reset) { - component->ops->stop_reset(component, win->timer); - } - } - } -} - -void timer_start_split(LSAppWindow* win) -{ - if (win->timer) { - GList* l; - if (!win->timer->running) { - if (ls_timer_start(win->timer)) { - save_game(win->game); - } - } else { - timer_split(win, false); - } - for (l = win->components; l != NULL; l = l->next) { - LSComponent* component = l->data; - if (component->ops->start_split) { - component->ops->start_split(component, win->timer); - } - } - } -} - -void timer_start(LSAppWindow* win, bool updateComponents) -{ - if (win->timer) { - GList* l; - if (!win->timer->running) { - if (ls_timer_start(win->timer)) { - save_game(win->game); - } - if (updateComponents) { - for (l = win->components; l != NULL; l = l->next) { - LSComponent* component = l->data; - if (component->ops->start_split) { - component->ops->start_split(component, win->timer); - } - } - } - } - } -} - -void timer_stop_reset(LSAppWindow* win) -{ - if (win->timer) { - GList* l; - if (is_run_started(win->timer)) { - ls_timer_stop(win->timer); - } else { - const bool was_asl_enabled = atomic_load(&auto_splitter_enabled); - atomic_store(&auto_splitter_enabled, false); - while (atomic_load(&auto_splitter_running) && was_asl_enabled) { - // wait, this will be very fast so its ok to just spin - } - if (was_asl_enabled) - atomic_store(&auto_splitter_enabled, true); - - if (ls_timer_reset(win->timer)) { - ls_app_window_clear_game(win); - ls_app_window_show_game(win); - save_game(win->game); - } - } - for (l = win->components; l != NULL; l = l->next) { - LSComponent* component = l->data; - if (component->ops->stop_reset) { - component->ops->stop_reset(component, win->timer); - } + } + + for (GList* l = win->components; l != NULL; l = l->next) { + LSComponent* component = l->data; + if (component->ops->stop_reset) { + component->ops->stop_reset(component, win->timer); } } } +/** + * @brief Cancels the current run, resetting the timer and game state and saving the cancelled run to history if enabled. + * + * @param win The LibreSplit window + */ void timer_cancel_run(LSAppWindow* win) { - if (win->timer) { - GList* l; - if (ls_timer_cancel(win->timer)) { - ls_app_window_clear_game(win); - ls_app_window_show_game(win); - save_game(win->game); - } - for (l = win->components; l != NULL; l = l->next) { - LSComponent* component = l->data; - if (component->ops->cancel_run) { - component->ops->cancel_run(component, win->timer); - } + + if (!win->timer) + return; + + if (ls_timer_cancel(win->timer)) { + ls_app_window_clear_game(win); + ls_app_window_show_game(win); + save_game(win->game); + } + + for (GList* l = win->components; l != NULL; l = l->next) { + LSComponent* component = l->data; + if (component->ops->cancel_run) { + component->ops->cancel_run(component, win->timer); } } } +/** + * Skips a timer split, filling the skipped split with 0's + * + * @param win The LibreSplit window + */ void timer_skip(LSAppWindow* win) { - if (win->timer) { - GList* l; - ls_timer_skip(win->timer); - for (l = win->components; l != NULL; l = l->next) { - LSComponent* component = l->data; - if (component->ops->skip) { - component->ops->skip(component, win->timer); - } + if (!win->timer) + return; + + ls_timer_skip(win->timer); + for (GList* l = win->components; l != NULL; l = l->next) { + LSComponent* component = l->data; + if (component->ops->skip) { + component->ops->skip(component, win->timer); } } } +/** + * Unsplits the last made split. If the timer is not running or if there are no splits to unsplit, it does nothing. + * + * @param win The LibreSplit window + */ void timer_unsplit(LSAppWindow* win) { - if (win->timer) { - GList* l; - ls_timer_unsplit(win->timer); - for (l = win->components; l != NULL; l = l->next) { - LSComponent* component = l->data; - if (component->ops->unsplit) { - component->ops->unsplit(component, win->timer); - } + if (!win->timer) + return; + + ls_timer_unsplit(win->timer); + + for (GList* l = win->components; l != NULL; l = l->next) { + LSComponent* component = l->data; + if (component->ops->unsplit) { + component->ops->unsplit(component, win->timer); } } } -void timer_split(LSAppWindow* win, bool updateComponents) +/** + * Makes a timer split. If the timer is not running, it does nothing. + * + * @param win The LibreSplit window + */ +void timer_split(LSAppWindow* win) { - if (win->timer) { - GList* l; - ls_timer_split(win->timer); - if (updateComponents) { - for (l = win->components; l != NULL; l = l->next) { - LSComponent* component = l->data; - if (component->ops->start_split) { - component->ops->start_split(component, win->timer); - } - } + if (!win->timer) + return; + + ls_timer_split(win->timer); + + for (GList* l = win->components; l != NULL; l = l->next) { + LSComponent* component = l->data; + if (component->ops->start_split) { + component->ops->start_split(component, win->timer); } } } +/** + * Pauses the timer into a paused/loading state + * + * @param win The LibreSplit window + */ +void timer_pause(LSAppWindow* win) +{ + if (!win->timer) + return; + + if (win->timer->running) { + ls_timer_pause(win->timer); + } + + for (GList* l = win->components; l != NULL; l = l->next) { + LSComponent* component = l->data; + if (component->ops->pause) { + component->ops->pause(component, win->timer); + } + } +} + +/** + * Resumes the timer from a paused/loading state + * + * @param win The LibreSplit window + */ +void timer_unpause(LSAppWindow* win) +{ + if (!win->timer) + return; + + if (win->timer->running) { + ls_timer_unpause(win->timer); + } + + for (GList* l = win->components; l != NULL; l = l->next) { + LSComponent* component = l->data; + if (component->ops->unpause) { + component->ops->unpause(component, win->timer); + } + } +} + +/** + * Stops the timer from ticking + * + * @param win TheLibreSplit window + */ void timer_stop(LSAppWindow* win) { - if (win->timer) { - GList* l; - if (win->timer->running) { - ls_timer_stop(win->timer); - } - for (l = win->components; l != NULL; l = l->next) { - LSComponent* component = l->data; - if (component->ops->stop_reset) { - component->ops->stop_reset(component, win->timer); - } + if (!win->timer) + return; + + if (win->timer->running) { + ls_timer_stop(win->timer); + } + + for (GList* l = win->components; l != NULL; l = l->next) { + LSComponent* component = l->data; + if (component->ops->stop_reset) { + component->ops->stop_reset(component, win->timer); } } } diff --git a/src/gui/timer.h b/src/gui/timer.h index dffa4b9..6dfa19a 100644 --- a/src/gui/timer.h +++ b/src/gui/timer.h @@ -2,10 +2,12 @@ #include "src/gui/app_window.h" -void timer_reset(LSAppWindow* win); +void timer_stop_and_reset(LSAppWindow* win); void timer_start_split(LSAppWindow* win); -void timer_stop_reset(LSAppWindow* win); +void timer_stop_or_reset(LSAppWindow* win); void timer_unsplit(LSAppWindow* win); void timer_skip(LSAppWindow* win); +void timer_pause(LSAppWindow* win); +void timer_unpause(LSAppWindow* win); void timer_stop(LSAppWindow* win); -void timer_split(LSAppWindow* win, bool updateComponents); +void timer_split(LSAppWindow* win); diff --git a/src/keybinds/delayed_callbacks.h b/src/keybinds/delayed_callbacks.h index bd880c0..e7eb008 100644 --- a/src/keybinds/delayed_callbacks.h +++ b/src/keybinds/delayed_callbacks.h @@ -1,6 +1,6 @@ #pragma once #include "src/gui/app_window.h" -extern void timer_stop_reset(LSAppWindow* win); +extern void timer_stop_or_reset(LSAppWindow* win); void process_delayed_handlers(LSAppWindow* win); diff --git a/src/keybinds/delayed_handlers.c b/src/keybinds/delayed_handlers.c index c83836c..c3ccd04 100644 --- a/src/keybinds/delayed_handlers.c +++ b/src/keybinds/delayed_handlers.c @@ -5,7 +5,7 @@ void process_delayed_handlers(LSAppWindow* win) { if (win->delayed_handlers.stop_reset) { - timer_stop_reset(win); + timer_stop_or_reset(win); win->delayed_handlers.stop_reset = false; } } diff --git a/src/keybinds/keybinds_callbacks.c b/src/keybinds/keybinds_callbacks.c index 0d7de21..caf746d 100644 --- a/src/keybinds/keybinds_callbacks.c +++ b/src/keybinds/keybinds_callbacks.c @@ -61,7 +61,7 @@ gboolean ls_app_window_keypress(GtkWidget* widget, if (keybind_match(win->keybinds.start_split, event->key)) { timer_start_split(win); } else if (keybind_match(win->keybinds.stop_reset, event->key)) { - timer_stop_reset(win); + timer_stop_or_reset(win); } else if (keybind_match(win->keybinds.cancel, event->key)) { timer_cancel_run(win); } else if (keybind_match(win->keybinds.unsplit, event->key)) { diff --git a/src/lasr/auto-splitter.c b/src/lasr/auto-splitter.c index c79995d..d18db66 100644 --- a/src/lasr/auto-splitter.c +++ b/src/lasr/auto-splitter.c @@ -40,11 +40,13 @@ int maps_cache_cycles_value = 1; /*!< The number of cycles the cache is active f atomic_bool auto_splitter_enabled = true; /*!< Defines if the auto splitter is enabled */ atomic_bool auto_splitter_running = false; /*!< Defines if the auto splitter is running */ atomic_bool call_start = false; /*!< True if the auto splitter is requesting for a run to start */ -atomic_bool run_started = false; /*!< Defines if a run is started */ -atomic_bool run_finished = false; // Disallows starting the timer again after finishing until reset atomic_bool call_split = false; /*!< True if the auto splitter is requesting to split */ atomic_bool toggle_loading = false; atomic_bool call_reset = false; /*!< True if the auto splitter is requesting a run reset */ +atomic_bool run_using_game_time_call; /*!< True if startup has run and a new value for using game time has been set by the auto splitter */ +atomic_bool run_using_game_time; /*!< True if the auto splitter is requesting to use game time, false for real time */ +atomic_bool run_started = false; /*!< Wheter a run was started or not, same as timer->started but accessible from the auto splitter thread */ +atomic_bool run_running = false; /*!< Wheter we are running or not, same as timer->running but accessible from the auto splitter thread */ bool prev_is_loading; /*!< The previous frame "is_loading" state */ /** @@ -305,6 +307,11 @@ void startup(lua_State* L) lua_getglobal(L, "useGameTime"); if (lua_isboolean(L, -1)) { use_game_time = lua_toboolean(L, -1); + atomic_store(&run_using_game_time, use_game_time); + atomic_store(&run_using_game_time_call, true); + } else { + atomic_store(&run_using_game_time, false); // Default to real time if not specified + atomic_store(&run_using_game_time_call, true); } lua_pop(L, 1); // Remove 'useGameTime' from the stack } @@ -345,9 +352,9 @@ void start(lua_State* L) { bool ret; if (call_va(L, "start", ">b", &ret)) { - atomic_store(&call_start, ret); if (ret) { atomic_store(&run_started, true); + atomic_store(&call_start, true); } } lua_pop(L, 1); // Remove the return value from the stack @@ -401,8 +408,13 @@ void reset(lua_State* L) { bool shouldReset; if (call_va(L, "reset", ">b", &shouldReset)) { - if (shouldReset) + if (shouldReset) { + atomic_store(&call_reset, true); + // Assume these happen instantly to avoid any desync + atomic_store(&run_started, false); + atomic_store(&run_running, false); + } } lua_pop(L, 1); // Remove the return value from the stack } @@ -516,11 +528,11 @@ void run_auto_splitter(void) update(L); } - if (gameTime_exists && use_game_time && atomic_load(&run_started) && !atomic_load(&run_finished)) { + if (gameTime_exists && use_game_time && atomic_load(&run_started) && atomic_load(&run_running)) { gameTime(L); } - if (start_exists && !atomic_load(&run_started) && !atomic_load(&run_finished)) { + if (start_exists && !atomic_load(&run_started) && !atomic_load(&run_running)) { start(L); } @@ -532,7 +544,7 @@ void run_auto_splitter(void) is_loading(L); } - if (reset_exists) { + if (reset_exists && atomic_load(&run_running)) { reset(L); } diff --git a/src/lasr/auto-splitter.h b/src/lasr/auto-splitter.h index 1157590..9ccbadc 100644 --- a/src/lasr/auto-splitter.h +++ b/src/lasr/auto-splitter.h @@ -14,11 +14,13 @@ extern int maps_cache_cycles; extern atomic_bool auto_splitter_enabled; extern atomic_bool auto_splitter_running; extern atomic_bool call_start; -extern atomic_bool run_started; -extern atomic_bool run_finished; extern atomic_bool call_split; extern atomic_bool toggle_loading; extern atomic_bool call_reset; +extern atomic_bool run_using_game_time_call; +extern atomic_bool run_using_game_time; +extern atomic_bool run_started; +extern atomic_bool run_running; extern bool prev_is_loading; /** diff --git a/src/lasr/utils.c b/src/lasr/utils.c index 6a893dc..69354bf 100644 --- a/src/lasr/utils.c +++ b/src/lasr/utils.c @@ -1,11 +1,32 @@ #include "utils.h" -#include "src/lasr/maps/maps.h" +#include "../gui/dialogs.h" +#include "./auto-splitter.h" +#include "./maps/maps.h" #include +#include #include game_process process; +/** + * Restarts the auto splitter by disabling it and re-enabling it again + * + * @return true if the auto splitter was enabled before the restart, false otherwise + */ +bool restart_auto_splitter(void) +{ + const bool was_asl_enabled = atomic_load(&auto_splitter_enabled); + if (was_asl_enabled) { + atomic_store(&auto_splitter_enabled, false); + while (atomic_load(&auto_splitter_running) && was_asl_enabled) { + // wait, this will be very fast so its ok to just spin + } + atomic_store(&auto_splitter_enabled, true); + } + return was_asl_enabled; +} + /** * Gets the base address of a module. * @@ -25,8 +46,6 @@ uintptr_t find_base_address(const char* module) return 0; } -gboolean display_non_capable_mem_read_dialog(void* data); - /** * Prints a memory error to stdout. * diff --git a/src/lasr/utils.h b/src/lasr/utils.h index e4957fd..b074d6c 100644 --- a/src/lasr/utils.h +++ b/src/lasr/utils.h @@ -32,6 +32,7 @@ typedef struct ProcessMap { char name[PATH_MAX]; } ProcessMap; +bool restart_auto_splitter(void); uintptr_t find_base_address(const char* module); bool handle_memory_error(uint32_t err); const char* value_to_c_string(lua_State* L, int index); diff --git a/src/main.c b/src/main.c index ef8d17b..dfcdb46 100644 --- a/src/main.c +++ b/src/main.c @@ -45,7 +45,7 @@ void handle_ctl_command(CTLCommand command) timer_start_split(win); break; case CTL_CMD_STOP_RESET: - timer_stop_reset(win); + timer_stop_or_reset(win); break; case CTL_CMD_CANCEL: timer_cancel_run(win); diff --git a/src/timer.c b/src/timer.c index 79b5a29..c48d8ab 100644 --- a/src/timer.c +++ b/src/timer.c @@ -23,13 +23,33 @@ * * @return The current time, in milliseconds */ -long long ls_time_now(void) +static long long ls_time_now(void) { struct timespec timespec; clock_gettime(CLOCK_MONOTONIC, ×pec); return timespec.tv_sec * 1000000LL + timespec.tv_nsec / 1000; } +/** + * Gets the timer current time, either game time or real time depending on the timer state. + * + * @param timer The timer instance + * @param load_removed Whether to subtract load_removed from RTA time + * @return The current time + */ +inline long long ls_timer_get_time(const ls_timer* timer, bool load_removed) +{ + if (timer->usingGameTime) { + return timer->gameTime; + } + + if (load_removed) { + return timer->realTime - timer->loadingTime; + } + + return timer->realTime; +} + /** * Converts a time string into milliseconds * @@ -194,9 +214,13 @@ void ls_delta_string(char* string, long long time) ls_time_string_format(string, NULL, time, 0, 1, 1); } +/** + * Frees the memory allocated for a game struct. + * + * @param game + */ void ls_game_release(const ls_game* game) { - int i; if (game->path) { free(game->path); } @@ -210,7 +234,7 @@ void ls_game_release(const ls_game* game) free(game->theme_variant); } if (game->split_titles) { - for (i = 0; i < game->split_count; ++i) { + for (unsigned int i = 0; i < game->split_count; ++i) { if (game->split_titles[i]) { free(game->split_titles[i]); free(game->split_icon_paths[i]); @@ -236,7 +260,6 @@ int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg) { int error = 0; ls_game* game; - int i; json_t* json = 0; json_t* ref; json_error_t json_error; @@ -363,7 +386,7 @@ int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg) } game->contains_icons = false; // copy splits - for (i = 0; i < game->split_count; ++i) { + for (unsigned int i = 0; i < game->split_count; ++i) { json_t* split; json_t* split_ref; split = json_array_get(ref, i); @@ -438,8 +461,13 @@ game_create_done: return error; } -void ls_game_update_splits(ls_game* game, - const ls_timer* timer) +/** + * Update the splits of a game based on the current timer. + * + * @param game The game whose splits are to be updated. + * @param timer The timer instance + */ +void ls_game_update_splits(ls_game* game, const ls_timer* timer) { if (timer->curr_split) { int size; @@ -454,7 +482,7 @@ void ls_game_update_splits(ls_game* game, memcpy(game->split_times, timer->split_times, size); } memcpy(game->segment_times, timer->segment_times, size); - for (int i = 0; i < game->split_count; ++i) { + for (unsigned int i = 0; i < game->split_count; ++i) { if (timer->split_times[i] < game->best_splits[i]) { game->best_splits[i] = timer->split_times[i]; } @@ -497,7 +525,6 @@ int ls_game_save(const ls_game* game) char str[256]; json_t* json = json_object(); json_t* splits = json_array(); - int i; if (game->title) { json_object_set_new(json, "title", json_string(game->title)); } @@ -517,7 +544,7 @@ int ls_game_save(const ls_game* game) ls_time_string_serialized(str, game->start_delay); json_object_set_new(json, "start_delay", json_string(str)); } - for (i = 0; i < game->split_count; ++i) { + for (unsigned int i = 0; i < game->split_count; ++i) { json_t* split = json_object(); json_object_set_new(split, "title", json_string(game->split_titles[i])); json_object_set_new(split, "icon", json_string(game->split_icon_paths[i])); @@ -564,12 +591,12 @@ int ls_game_save(const ls_game* game) int ls_run_save(ls_timer* timer, const char* reason) { - if (timer->time == 0) + if (ls_timer_get_time(timer, true) == 0) return 0; int error = 0; char final_time_str[128]; - ls_time_string_serialized(final_time_str, timer->time); + ls_time_string_serialized(final_time_str, ls_timer_get_time(timer, true)); // Root JSON Object json_t* json = json_object(); @@ -590,7 +617,7 @@ int ls_run_save(ls_timer* timer, const char* reason) // Splits Array json_t* splits = json_array(); - for (int i = 0; i < timer->game->split_count; i++) { + for (unsigned int i = 0; i < timer->game->split_count; i++) { json_t* split = json_object(); // Title @@ -651,6 +678,11 @@ int ls_run_save(ls_timer* timer, const char* reason) return error; } +/** + * Frees all the timer information, but not itself + * + * @param timer The timer instance + */ void ls_timer_release(const ls_timer* timer) { if (timer->split_times) { @@ -676,15 +708,25 @@ void ls_timer_release(const ls_timer* timer) } } +/** + * Resets the whole timer back to 0, ready for a new run + * + * @param timer The timer instance + */ static void reset_timer(ls_timer* timer) { - int i; - int size; timer->started = 0; - timer->start_time = 0; + atomic_store(&run_started, false); + timer->running = 0; + atomic_store(&run_running, false); timer->curr_split = 0; - timer->time = -timer->game->start_delay; - size = timer->game->split_count * sizeof(long long); + timer->realTime = -timer->game->start_delay; // Start delay only applies to real time only + timer->gameTime = 0; + timer->usingGameTime = false; + timer->loading = false; + timer->loadingTime = 0; + timer->last_tick = 0; + int size = timer->game->split_count * sizeof(long long); memcpy(timer->split_times, timer->game->split_times, size); memset(timer->split_deltas, 0, size); memcpy(timer->segment_times, timer->game->segment_times, size); @@ -694,7 +736,7 @@ static void reset_timer(ls_timer* timer) size = timer->game->split_count * sizeof(int); memset(timer->split_info, 0, size); timer->sum_of_bests = 0; - for (i = 0; i < timer->game->split_count; ++i) { + for (unsigned int i = 0; i < timer->game->split_count; ++i) { // Check no segments are erroring with LLONG_MAX if (timer->best_segments[i] && timer->best_segments[i] < LLONG_MAX) { timer->sum_of_bests += timer->best_segments[i]; @@ -707,6 +749,13 @@ static void reset_timer(ls_timer* timer) } } +/** + * Creates a timer instance linked to a game instance, allocating necessary memory + * + * @param timer_ptr Apointer to where the allocated timer instance should be stored + * @param game The game instance to link the timer to + * @return Whether the timer creation had an error or not + */ int ls_timer_create(ls_timer** timer_ptr, ls_game* game) { int error = 0; @@ -773,14 +822,22 @@ timer_create_done: return error; } -void ls_timer_step(ls_timer* timer, long long now) +/** + * Executes a timer step, calculating deltas, times, and split infos + * + * @param timer The timer instance + */ +void ls_timer_step(ls_timer* timer) { - timer->now = now; + long long now = ls_time_now(); if (timer->running) { - long long delta = timer->now - timer->start_time; - timer->time += delta; // Accumulate the elapsed time + long long delta = timer->last_tick ? now - timer->last_tick : 0; + timer->realTime += delta; // Accumulate the elapsed time + if (timer->loading) { + timer->loadingTime += delta; // Accumulate loading time if currently loading + } if (timer->curr_split < timer->game->split_count) { - timer->split_times[timer->curr_split] = timer->time; + timer->split_times[timer->curr_split] = timer->usingGameTime ? timer->gameTime : timer->realTime - timer->loadingTime; // calc delta and check it's not an error of LLONG_MAX if (timer->game->split_times[timer->curr_split] && timer->game->split_times[timer->curr_split] < LLONG_MAX) { timer->split_deltas[timer->curr_split] = timer->split_times[timer->curr_split] @@ -823,173 +880,231 @@ void ls_timer_step(ls_timer* timer, long long now) } } } - timer->start_time = now; // Update the start time for the next iteration + timer->last_tick = now; // Update the start time for the next iteration } +/** + * Starts the timer, setting it to running and incrementing attempt count if not already started + * + * @param timer The timer instance + * @return Whether the timer is now running + */ int ls_timer_start(ls_timer* timer) { + // TODO: Allow starting when split_count is 0 for splitless runs, other stuff has to change for this to work (components, timer logic, etc) if (timer->curr_split < timer->game->split_count) { if (!timer->started) { ++*timer->attempt_count; timer->started = 1; + atomic_store(&run_started, true); } - timer->running = 1; + timer->running = true; + atomic_store(&run_running, true); } return timer->running; } +/** + * Performs a split + * + * @param timer The timer instance + * @return The current split index after splitting, 0 if no split happened + */ int ls_timer_split(ls_timer* timer) { - if (timer->time > 0) { - if (timer->curr_split < timer->game->split_count) { - int i; - // check for best split and segment - if (!timer->best_splits[timer->curr_split] - || timer->split_times[timer->curr_split] - < timer->best_splits[timer->curr_split]) { - timer->best_splits[timer->curr_split] = timer->split_times[timer->curr_split]; - timer->split_info[timer->curr_split] - |= LS_INFO_BEST_SPLIT; - } - if (!timer->best_segments[timer->curr_split] - || timer->segment_times[timer->curr_split] - < timer->best_segments[timer->curr_split]) { - timer->best_segments[timer->curr_split] = timer->segment_times[timer->curr_split]; - timer->split_info[timer->curr_split] - |= LS_INFO_BEST_SEGMENT; - } - // update sum of bests + if (ls_timer_get_time(timer, true) <= 0) { + return 0; + } + + if (timer->curr_split >= timer->game->split_count) { + return 0; + } + + // check for best split and segment + if (!timer->best_splits[timer->curr_split] + || timer->split_times[timer->curr_split] + < timer->best_splits[timer->curr_split]) { + timer->best_splits[timer->curr_split] = timer->split_times[timer->curr_split]; + timer->split_info[timer->curr_split] + |= LS_INFO_BEST_SPLIT; + } + if (!timer->best_segments[timer->curr_split] + || timer->segment_times[timer->curr_split] + < timer->best_segments[timer->curr_split]) { + timer->best_segments[timer->curr_split] = timer->segment_times[timer->curr_split]; + timer->split_info[timer->curr_split] + |= LS_INFO_BEST_SEGMENT; + } + // update sum of bests + timer->sum_of_bests = 0; + for (unsigned int i = 0; i < timer->game->split_count; ++i) { + // Check if any best segment is missing/LLONG_MAX + if (timer->best_segments[i] && timer->best_segments[i] < LLONG_MAX) { + timer->sum_of_bests += timer->best_segments[i]; + } else if (timer->game->best_segments[i] && timer->game->best_segments[i] < LLONG_MAX) { + timer->sum_of_bests += timer->game->best_segments[i]; + } else { timer->sum_of_bests = 0; - for (i = 0; i < timer->game->split_count; ++i) { - // Check if any best segment is missing/LLONG_MAX - if (timer->best_segments[i] && timer->best_segments[i] < LLONG_MAX) { - timer->sum_of_bests += timer->best_segments[i]; - } else if (timer->game->best_segments[i] && timer->game->best_segments[i] < LLONG_MAX) { - timer->sum_of_bests += timer->game->best_segments[i]; - } else { - timer->sum_of_bests = 0; - break; - } - } - - ++timer->curr_split; - // stop timer if last split - if (timer->curr_split == timer->game->split_count) { - // Increment finished_count - ++*timer->finished_count; - ls_timer_stop(timer); - atomic_store(&run_finished, true); - ls_game_update_splits((ls_game*)timer->game, timer); - if (cfg.libresplit.save_run_history.value.b) { - ls_run_save(timer, "FINISHED"); - } - } - return timer->curr_split; + break; } } - return 0; -} -int ls_timer_skip(ls_timer* timer) -{ - if (timer->time > 0) { - if (timer->curr_split + 1 == timer->game->split_count) { - // This is the last split, do a normal split instead of skipping - return ls_timer_split(timer); - } - if (timer->curr_split < timer->game->split_count) { - timer->split_times[timer->curr_split] = 0; - timer->split_deltas[timer->curr_split] = 0; - timer->split_info[timer->curr_split] = 0; - timer->segment_times[timer->curr_split] = 0; - timer->segment_deltas[timer->curr_split] = 0; - return ++timer->curr_split; + ++timer->curr_split; + // stop timer if last split + if (timer->curr_split == timer->game->split_count) { + // Increment finished_count + ++*timer->finished_count; + ls_timer_stop(timer); + ls_game_update_splits((ls_game*)timer->game, timer); + if (cfg.libresplit.save_run_history.value.b) { + ls_run_save(timer, "FINISHED"); } } - return 0; -} - -int ls_timer_unsplit(ls_timer* timer) -{ - if (timer->curr_split) { - int i; - int curr = --timer->curr_split; - for (i = curr; i < timer->game->split_count; ++i) { - timer->split_times[i] = timer->game->split_times[i]; - timer->split_deltas[i] = 0; - timer->split_info[i] = 0; - timer->segment_times[i] = timer->game->segment_times[i]; - timer->segment_deltas[i] = 0; - } - if (timer->curr_split + 1 == timer->game->split_count) { - timer->running = 1; - } - return timer->curr_split; - } - return 0; -} - -void ls_timer_stop(ls_timer* timer) -{ - timer->running = 0; - atomic_store(&run_started, false); -} - -int ls_timer_reset(ls_timer* timer) -{ - if (!timer->running) { - if (timer->started && timer->time <= 0) { - return ls_timer_cancel(timer); - } - if (timer->curr_split < timer->game->split_count) { - if (cfg.libresplit.save_run_history.value.b) { - ls_run_save(timer, "RESET"); - } - } - if (ls_timer_has_gold_split(timer)) { - bool user_reset = true; - if (cfg.libresplit.ask_on_gold.value.b) { - user_reset = display_confirm_reset_dialog(); - } - if (user_reset) { - reset_timer(timer); - return 1; - } else { - return 0; - } - } - reset_timer(timer); - return 1; - } - return 0; -} - -int ls_timer_cancel(ls_timer* timer) -{ - if (!timer->running) { - if (timer->started) { - if (*timer->attempt_count > 0) { - --*timer->attempt_count; - } - } - reset_timer(timer); - return 1; - } - return 0; + return timer->curr_split; } /** - * Checks if a run is started, either manually or by - * the auto-splitter + * Skips a split, moving the timer forward one split and setting the split and segment times and deltas to 0 * - * @param timer Pointer to the timer instance (used for RTA) - * - * @returns True if a run is started, false otherwise + * @param timer The timer instance + * @return The current split index after skipping, 0 if no skip happened */ -bool is_run_started(ls_timer* timer) +int ls_timer_skip(ls_timer* timer) { - if (timer == NULL) { - return false; + if (ls_timer_get_time(timer, false) <= 0) + return 0; + + if (timer->curr_split + 1 == timer->game->split_count) { + // This is the last split, do a normal split instead of skipping + return ls_timer_split(timer); } - return timer->running || atomic_load(&run_started); + + if (timer->curr_split >= timer->game->split_count) { + return 0; + } + + timer->split_times[timer->curr_split] = 0; + timer->split_deltas[timer->curr_split] = 0; + timer->split_info[timer->curr_split] = 0; + timer->segment_times[timer->curr_split] = 0; + timer->segment_deltas[timer->curr_split] = 0; + return ++timer->curr_split; +} + +/** + * Unsplits the last split, moving the timer back one split and resetting the split and segment times and deltas to the game times + * + * @param timer The timer instance + * @return The current split index after unsplitting, the same or 0 if no unsplit happened + */ +int ls_timer_unsplit(ls_timer* timer) +{ + if (timer->curr_split == 0) { + return 0; + } + + unsigned int curr = --timer->curr_split; + for (unsigned int i = curr; i < timer->game->split_count; ++i) { + timer->split_times[i] = timer->game->split_times[i]; + timer->split_deltas[i] = 0; + timer->split_info[i] = 0; + timer->segment_times[i] = timer->game->segment_times[i]; + timer->segment_deltas[i] = 0; + } + if (timer->curr_split + 1 == timer->game->split_count) { + timer->running = true; + atomic_store(&run_running, true); + } + return timer->curr_split; +} + +/** + * Marks the timer as loading, incrementing loading time in step until unpaused + * + * @param timer The timer instance + */ +void ls_timer_pause(ls_timer* timer) +{ + timer->loading = 1; +} + +/** + * Marks the timer as not loading, not incrementing loading time + * + * @param timer The timer instance + */ +void ls_timer_unpause(ls_timer* timer) +{ + timer->loading = 0; +} + +/** + * Stops the timer from ticking + * + * @param timer The timer instance + */ +void ls_timer_stop(ls_timer* timer) +{ + timer->running = false; + atomic_store(&run_running, false); +} + +/** + * Resets all the timer and splits back to 0 + * + * Also saves run + * + * @param timer The timer instance + * @return Whether the reset was successful, will fail if the timer is currently running/reset cancelled + */ +int ls_timer_reset(ls_timer* timer) +{ + // Disallow resets while running + if (timer->running) + return 0; + + if (timer->started && ls_timer_get_time(timer, true) <= 0) { + return ls_timer_cancel(timer); + } + + if (timer->curr_split < timer->game->split_count) { + if (cfg.libresplit.save_run_history.value.b) { + ls_run_save(timer, "RESET"); + } + } + + // Warn if the reset will lose a gold split, and allow the user to cancel the reset if they want to keep it + if (ls_timer_has_gold_split(timer)) { + bool user_reset = true; + if (cfg.libresplit.ask_on_gold.value.b) { + user_reset = display_confirm_reset_dialog(); + } + if (!user_reset) { + return 0; + } + } + + reset_timer(timer); + return 1; +} + +/** + * Cancels the current run, ignoring attempt and resetting timer + * + * @param timer The timer instance + * @return Whether the cancel was successful, will fail if the timer is currently running + */ +int ls_timer_cancel(ls_timer* timer) +{ + // Disallow resets while running + if (timer->running) + return 0; + + if (timer->started) { + if (*timer->attempt_count > 0) { + --*timer->attempt_count; + } + } + reset_timer(timer); + return 1; } diff --git a/src/timer.h b/src/timer.h index 5477e71..52fc3b5 100644 --- a/src/timer.h +++ b/src/timer.h @@ -4,10 +4,10 @@ #include #include -#define LS_INFO_BEHIND_TIME (1) -#define LS_INFO_LOSING_TIME (2) -#define LS_INFO_BEST_SPLIT (4) -#define LS_INFO_BEST_SEGMENT (8) +#define LS_INFO_BEHIND_TIME (1 << 0) +#define LS_INFO_LOSING_TIME (1 << 1) +#define LS_INFO_BEST_SPLIT (1 << 2) +#define LS_INFO_BEST_SEGMENT (1 << 3) // Gold split extern AppConfig cfg; @@ -25,23 +25,28 @@ typedef struct ls_game { char** split_titles; char** split_icon_paths; // null if no icons bool contains_icons; - int split_count; + unsigned int split_count; long long* split_times; long long* segment_times; long long* best_splits; long long* best_segments; } ls_game; +/** + * @brief Timer structure for managing game and time. + * Timer structure, it includes RTA, gametime, loading time, splits, deltas, and other relevant information for tracking the progress of a run. + */ typedef struct ls_timer { - int started; - int running; - int loading; - int curr_split; - long long now; - long long start_time; - long long time; - long long sum_of_bests; - long long world_record; + bool usingGameTime; /*!< Splitter is using game time instead of real time. Only to be used internally */ + long long gameTime; /*!< The current game time only usable in LASR. Only to be used internally */ + long long realTime; /*!< Real time. Starts when run start and pauses while loading. Only to be used internally */ + int loading; /*!< Currently loading? used for knowing if loadingTime should tick or not. Only to be used internally */ + long long loadingTime; /*!< Time spent loading, used to subtract from real time when trying to get Load-Removed Time. Only to be used internally */ + int started; /*!< Wether the run has started, either by LASR or manually, keeps being set to true after run finished */ + bool running; /*!< Whether the runner is currently running. If this is false and started is true then the run finished. Mainly used to check if some actions are valid to perform (splits, pause, etc) */ + unsigned int curr_split; /*!< Index of the current split, 0 for first split */ + long long sum_of_bests; /*!< Sum of best segments */ + long long world_record; /*!< World record time */ long long* split_times; long long* split_deltas; long long* segment_times; @@ -50,13 +55,14 @@ typedef struct ls_timer { long long* best_splits; long long* best_segments; const ls_game* game; + long long last_tick; // This NEEDS to be here for resetting int* attempt_count; int* finished_count; } ls_timer; extern atomic_bool run_started; -long long ls_time_now(void); +long long ls_timer_get_time(const ls_timer* timer, bool load_removed); long long ls_time_value(const char* string); @@ -86,7 +92,7 @@ void ls_timer_release(const ls_timer* timer); int ls_timer_start(ls_timer* timer); -void ls_timer_step(ls_timer* timer, long long now); +void ls_timer_step(ls_timer* timer); int ls_timer_split(ls_timer* timer); @@ -94,10 +100,12 @@ int ls_timer_skip(ls_timer* timer); int ls_timer_unsplit(ls_timer* timer); +void ls_timer_pause(ls_timer* timer); + +void ls_timer_unpause(ls_timer* timer); + void ls_timer_stop(ls_timer* timer); int ls_timer_reset(ls_timer* timer); int ls_timer_cancel(ls_timer* timer); - -bool is_run_started(ls_timer* timer); From c4fd163e5457321a9c0dd57d8465abc5c7d441f6 Mon Sep 17 00:00:00 2001 From: Daniele Penazzo Date: Tue, 10 Mar 2026 19:48:38 +0100 Subject: [PATCH 2/9] Fix various memory issues (#334) * Properly free the splits icon paths on game release This fixes a small memory leak when you load/unload/reload splits with icons * Correctly free stuff in the splits component too * Fix two segfaults when timer is not loaded A check is missing for the presence of timer, so if you right click -> close and then try to open a split or auto splitter, libresplit segfaults. * Correctly free welcome_box on closing * Make sure that malloc() in gui_settings actually works In low-memory conditions the malloc may fail with undefined behaviour * Revert connection of destroy signal The widget is freed just before LibreSplit closes already. I thought it was a disconnected signal handler. * Autoimporter mon amour (non) * Actually this one isn't needed anymore --- src/gui/actions.c | 4 ++-- src/gui/component/splits.c | 38 ++++++++++++++++++++++++++------------ src/gui/settings_dialog.c | 7 ++++++- src/timer.c | 3 +++ 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/gui/actions.c b/src/gui/actions.c index c08c84a..73453ae 100644 --- a/src/gui/actions.c +++ b/src/gui/actions.c @@ -71,7 +71,7 @@ void open_activated(GSimpleAction* action, } else { win = ls_app_window_new(LS_APP(app)); } - if (win->timer->running) { + if (win->timer && win->timer->running) { GtkWidget* warning = gtk_message_dialog_new( GTK_WINDOW(win), GTK_DIALOG_MODAL, @@ -350,7 +350,7 @@ void open_auto_splitter(GSimpleAction* action, } else { win = ls_app_window_new(LS_APP(app)); } - if (win->timer->running) { + if (win->timer && win->timer->running) { GtkWidget* warning = gtk_message_dialog_new( GTK_WINDOW(win), GTK_DIALOG_MODAL, diff --git a/src/gui/component/splits.c b/src/gui/component/splits.c index 8f974b1..3bbc2db 100644 --- a/src/gui/component/splits.c +++ b/src/gui/component/splits.c @@ -27,6 +27,26 @@ typedef struct LSSplits { } LSSplits; extern LSComponentOps ls_splits_operations; +void free_all(LSSplits* self_) +{ + LSSplits* self = (LSSplits*)self_; + if (self->split_rows) { + free(self->split_rows); + } + if (self->split_titles) { + free(self->split_titles); + } + if (self->split_icons) { + free(self->split_icons); + } + if (self->split_deltas) { + free(self->split_deltas); + } + if (self->split_times) { + free(self->split_times); + } +} + /** * Constructor */ @@ -152,28 +172,25 @@ static void splits_show_game(LSComponent* self_, const ls_game* game, self->split_titles = calloc(self->split_count, sizeof(GtkWidget*)); if (!self->split_titles) { - free(self->split_rows); + free_all(self); return; } self->split_icons = calloc(self->split_count, sizeof(GtkWidget*)); - if (!self->split_titles) { - free(self->split_rows); + if (!self->split_icons) { + free_all(self); return; } self->split_deltas = calloc(self->split_count, sizeof(GtkWidget*)); if (!self->split_deltas) { - free(self->split_rows); - free(self->split_titles); + free_all(self); return; } self->split_times = calloc(self->split_count, sizeof(GtkWidget*)); if (!self->split_times) { - free(self->split_rows); - free(self->split_titles); - free(self->split_deltas); + free_all(self); return; } @@ -291,10 +308,7 @@ static void splits_clear_game(LSComponent* self_) self->split_rows[i]); } gtk_adjustment_set_value(self->split_adjust, 0); - free(self->split_rows); - free(self->split_titles); - free(self->split_deltas); - free(self->split_times); + free_all(self); self->split_count = 0; } diff --git a/src/gui/settings_dialog.c b/src/gui/settings_dialog.c index 1962c5d..af33e06 100644 --- a/src/gui/settings_dialog.c +++ b/src/gui/settings_dialog.c @@ -9,7 +9,7 @@ #include #include -static LSGuiSetting* gui_settings; +static LSGuiSetting* gui_settings = NULL; /** * Takes the application config and counts how many settings are available. @@ -46,6 +46,7 @@ static gboolean on_help_window_delete(GtkWidget* widget, GdkEvent* event, gpoint { gtk_widget_destroy(widget); free(gui_settings); + gui_settings = NULL; return TRUE; } @@ -163,6 +164,10 @@ static void build_settings_dialog(GtkApplication* app, gpointer data) { int settings_number = enumerate_settings(cfg); gui_settings = malloc(settings_number * sizeof(LSGuiSetting)); + if (gui_settings == NULL) { + printf("Cannot allocate memory for the settings GUI."); + return; + } GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "LibreSplit Settings"); diff --git a/src/timer.c b/src/timer.c index c48d8ab..7fefa60 100644 --- a/src/timer.c +++ b/src/timer.c @@ -245,6 +245,9 @@ void ls_game_release(const ls_game* game) if (game->split_times) { free(game->split_times); } + if (game->split_icon_paths) { + free(game->split_icon_paths); + } if (game->segment_times) { free(game->segment_times); } From 0ec65fdee55e2f80edcb7258a8c2750e44cd4920 Mon Sep 17 00:00:00 2001 From: Daniele Penazzo Date: Tue, 10 Mar 2026 19:49:17 +0100 Subject: [PATCH 3/9] Added customized logging library (#317) * Added asynchronous logging library Did some rudimentary testing, might need more for safety. Fixes #316 at least in part * Included missing stdbool include * Added console logging (in addition to file logging) * Force a file flush at every write This might be useful if we want to analyze the cause of a crash, if we don't flush the file, one to several log lines might be missing. * Added default log settings for release and debug versions * Added LOG_*F formattable logging functions * Allow the main thread to close the logger Or LS will keep on logging from a detached thread. * Empty the logger queue on thread exit. This should remove the possibility of losing messages on exit. * Add timestamps to logs and avoid double newlines * Put the log file in XDG_DATA_DIR/libresplit * Default LogLevel at Warns + Errors Debug LogLevel stays at "everything" * Bunch'o'statics * Move prctl to the beginning of the logging thread code --- meson.build | 4 ++ src/gui/app_window.c | 2 + src/logging.c | 151 +++++++++++++++++++++++++++++++++++++++++++ src/logging.h | 85 ++++++++++++++++++++++++ src/main.c | 6 ++ src/settings/utils.c | 27 +++++++- src/settings/utils.h | 3 +- 7 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 src/logging.c create mode 100644 src/logging.h diff --git a/meson.build b/meson.build index 99d2236..8194528 100644 --- a/meson.build +++ b/meson.build @@ -10,6 +10,9 @@ add_project_arguments('-DAPP_VERSION="@0@"'.format(meson.project_version()), lan if get_option('buildtype') == 'debug' add_project_arguments('-DDEBUG', language: 'c') + add_project_arguments('-DLOG_LEVEL=0', language: 'c') +else + add_project_arguments('-DLOG_LEVEL=2', language: 'c') endif cc = meson.get_compiler('c') @@ -31,6 +34,7 @@ libresplit_sources = files( 'src/server.c', 'src/shared.c', 'src/timer.c', + 'src/logging.c', # Settings 'src/settings/definitions.c', diff --git a/src/gui/app_window.c b/src/gui/app_window.c index d25d57a..62bee91 100644 --- a/src/gui/app_window.c +++ b/src/gui/app_window.c @@ -8,6 +8,7 @@ #include "src/keybinds/delayed_callbacks.h" #include "src/keybinds/keybinds_callbacks.h" #include "src/lasr/auto-splitter.h" +#include "src/logging.h" #include "src/settings/settings.h" #include "src/settings/utils.h" #include "src/timer.h" @@ -210,6 +211,7 @@ void ls_app_window_destroy(GtkWidget* widget, gpointer data) } atomic_store(&auto_splitter_enabled, 0); atomic_store(&exit_requested, 1); + close_logger(); // Close any other open application windows (settings, dialogs, etc.) GApplication* app = g_application_get_default(); if (app) { diff --git a/src/logging.c b/src/logging.c new file mode 100644 index 0000000..e4dc987 --- /dev/null +++ b/src/logging.c @@ -0,0 +1,151 @@ +/** \file logging.c + * Logger Rollster + * + * Asynchronous Logging Library for LibreSplit based on threads, circular queues, + * hopes and dreams. + */ +#include "logging.h" +#include "settings/utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/*! The log queue, used as buffer */ +static LogQueue logQueue; +/*! Atomic bool used to keep the thread active, might be used for clean closing in future */ +static atomic_bool logging_active; + +/** + * Initializes the log queue, ready to receive messages + */ +void initLogQueue(void) +{ + logQueue.head = 0; + logQueue.tail = 0; + pthread_mutex_init(&logQueue.lock, NULL); + pthread_cond_init(&logQueue.cond, NULL); + logging_active = 1; +} + +/** + * Underlying function to all the LOG_* macros + * + * Works as a producer. + * + * @param[in] fmt The message to print in the log or the format string + */ +void logMessage(const char* fmt, ...) +{ + // Lock the mutex for writing + pthread_mutex_lock(&logQueue.lock); + // If the queue is full, wait (bottleneck) + while ((logQueue.tail + 1) % LOG_QUEUE_SIZE == logQueue.head) { + pthread_cond_wait(&logQueue.cond, &logQueue.lock); + } + // Create a timestamp for the log + char timestamp[64]; + time_t current_time = time(NULL); + struct tm* t = localtime(¤t_time); + strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", t); + // There is space in the queue, add a new message + va_list args; + va_start(args, fmt); + // Put the timestamp first... + snprintf(logQueue.message_queue[logQueue.tail], LOG_STR_LEN, "%s | ", timestamp); + // The remaining space is for the message + vsnprintf(logQueue.message_queue[logQueue.tail] + strlen(timestamp) + 1, LOG_STR_LEN - sizeof(timestamp) - 1, fmt, args); + va_end(args); + logQueue.tail = (logQueue.tail + 1) % LOG_QUEUE_SIZE; + + // Signal a possibly waiting logging thread + pthread_cond_signal(&logQueue.cond); + // Unlock the mutex + pthread_mutex_unlock(&logQueue.lock); +} + +/** + * Pops a message from the log queue, writing it into console + * and the log file. Just a utility. + * + * @param logfile The File pointer to write into + */ +static void pop_message(FILE* logfile) +{ + // Remove a message from the queue + // We don't empty the whole queue to avoid being a bottleneck for the + // addition of new messages. + // Log to console + printf("%s", logQueue.message_queue[logQueue.head]); + // Log to file + fprintf(logfile, "%s", logQueue.message_queue[logQueue.head]); + // Flush the file immediately to disk, in case something crashes + fflush(logfile); + logQueue.head = (logQueue.head + 1) % LOG_QUEUE_SIZE; +} + +/** + * The logging thread, writes the queued messages in the log. + * + * Works as a consumer + * + * @param arg Unused. + */ +void* loggingThread(void* arg) +{ + prctl(PR_SET_NAME, "LS Logger", 0, 0, 0); + char data_path[PATH_MAX]; + get_libresplit_data_folder_path(data_path); + strcat(data_path, "/libresplit.log"); + FILE* logfile = fopen(data_path, "a"); + if (!logfile) { + perror("Failed to open log file"); + return NULL; + } + while (atomic_load(&logging_active)) { + // Lock the mutex for reading + pthread_mutex_lock(&logQueue.lock); + // If the queue is empty, wait + while (logQueue.head == logQueue.tail) { + pthread_cond_wait(&logQueue.cond, &logQueue.lock); + // We got signalled by the main thread to close up + if (!atomic_load(&logging_active)) { + break; + } + } + pop_message(logfile); + // Unlock the mutex + pthread_mutex_unlock(&logQueue.lock); + } + // We're closing the logger, empty the remaining logs... + while (logQueue.head != logQueue.tail) { + pop_message(logfile); + } + // ... and close the logfile + fclose(logfile); + return 0; +} + +/** + * Function to close the logger thread. + * + * This is needed because while we're closing, we might be waiting for + * the queue to fill up. If that's the case, we signal the thread to continue + * after setting logging_active to false. + */ +void close_logger() +{ + atomic_store(&logging_active, 0); + LOG_DEBUG("Shutting down logger thread...") + // Signal the logging thread to continue, so to hit the closing condition, + // in case it is waiting for the log queue to fill. If it isn't, the signal + // should be ignored. + pthread_cond_signal(&logQueue.cond); +} diff --git a/src/logging.h b/src/logging.h new file mode 100644 index 0000000..c2fa912 --- /dev/null +++ b/src/logging.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include + +#define LOG_QUEUE_SIZE 100 +#define LOG_STR_LEN 512 + +extern atomic_bool exit_requested; + +/** \brief The Log Buffer + * + * Allows to memorize messages in a circular queue for the + * logging thread to consume + */ +typedef struct LogQueue { + char message_queue[LOG_QUEUE_SIZE][LOG_STR_LEN]; /*!< The read circular queue */ + int head; /*!< Index of the head of the queue */ + int tail; /*!< Index of the tail of the queue */ + pthread_mutex_t lock; /*!< Lock to avoid race conditions */ + pthread_cond_t cond; /*!< Condition to signal between the logMessage function and the logging thread */ +} LogQueue; + +void initLogQueue(void); + +void logMessage(const char* fmt, ...); +void close_logger(); + +void* loggingThread(void* arg); + +#define LOG_LEVEL_DEBUG 0 +#define LOG_LEVEL_INFO 1 +#define LOG_LEVEL_WARN 2 +#define LOG_LEVEL_ERR 3 + +#if !defined(LOG_LEVEL) +#define LOG_LEVEL LOG_LEVEL_ERR +#endif + +#define LOG__XSTR(x) #x +#define LOG__STR(x) LOG__XSTR(x) + +#define LOG_STRING(file, line, level, message) \ + file ": " line " | " level " - " message "\n" + +#define LOG(T, message) \ + { \ + logMessage(LOG_STRING(__FILE__, LOG__STR(__LINE__), #T, message)); \ + } + +#define LOGF(T, fmt, ...) \ + { \ + logMessage(LOG_STRING(__FILE__, LOG__STR(__LINE__), #T, fmt), __VA_ARGS__); \ + } + +#if LOG_LEVEL == LOG_LEVEL_DEBUG +#define LOG_DEBUG(message) LOG([Debug], message); +#define LOG_DEBUGF(fmt, ...) LOGF([Debug], fmt, __VA_ARGS__); +#else +#define LOG_DEBUG(fmt, ...) +#define LOG_DEBUGF(fmt, ...) +#endif + +#if LOG_LEVEL <= LOG_LEVEL_INFO +#define LOG_INFO(message) LOG([Info], message); +#define LOG_INFOF(fmt, ...) LOGF([Info], fmt, __VA_ARGS__); +#else +#define LOG_INFO(message) +#endif + +#if LOG_LEVEL <= LOG_LEVEL_WARN +#define LOG_WARN(message) LOG([Warn], message); +#define LOG_WARNF(fmt, ...) LOGF([Warn], fmt, __VA_ARGS__); +#else +#define LOG_WARN(message) +#define LOG_WARNF(message) +#endif + +#if LOG_LEVEL <= LOG_LEVEL_ERR +#define LOG_ERR(message) LOG([ERR], message); +#define LOG_ERRF(fmt, ...) LOGF([ERR], fmt, __VA_ARGS__); +#else +#define LOG_ERR(message) +#define LOG_ERRF(message) +#endif diff --git a/src/main.c b/src/main.c index dfcdb46..53adeaa 100644 --- a/src/main.c +++ b/src/main.c @@ -2,6 +2,7 @@ #include "gui/timer.h" #include "keybinds/keybinds_callbacks.h" #include "lasr/auto-splitter.h" +#include "logging.h" #include "server.h" #include "settings/utils.h" #include "shared.h" @@ -88,6 +89,7 @@ static void* ls_auto_splitter(void* arg) int main(int argc, char* argv[]) { + initLogQueue(); check_directories(); g_app = ls_app_new(); @@ -97,10 +99,14 @@ int main(int argc, char* argv[]) pthread_t t2; // Control server thread pthread_create(&t2, NULL, &ls_ctl_server, NULL); + pthread_t t3; // Logging Thread + pthread_create(&t3, NULL, &loggingThread, NULL); + g_application_run(G_APPLICATION(g_app), argc, argv); pthread_join(t1, NULL); pthread_join(t2, NULL); + pthread_join(t3, NULL); return 0; } diff --git a/src/settings/utils.c b/src/settings/utils.c index 7f0157c..d4d151f 100644 --- a/src/settings/utils.c +++ b/src/settings/utils.c @@ -37,6 +37,25 @@ static void mkdir_p(const char* dir, __mode_t permissions) mkdir(tmp, permissions); } +/** + * Copies the user's livesplit data path in a given string. + * + * @param out_path The string to copy the data path into. + */ +void get_libresplit_data_folder_path(char* out_path) +{ + struct passwd* pw = getpwuid(getuid()); + char* XDG_DATA_HOME = getenv("XDG_DATA_HOME"); + char* base_dir = strcat(pw->pw_dir, "/.local/share/libresplit"); + if (XDG_DATA_HOME != NULL) { + char config_dir[PATH_MAX] = { 0 }; + strcpy(config_dir, XDG_DATA_HOME); + strcat(config_dir, "/libresplit"); + strcpy(base_dir, config_dir); + } + strcpy(out_path, base_dir); +} + /** * Copies the user's livesplit configuration path in a given string. * @@ -67,6 +86,9 @@ void check_directories(void) char libresplit_directory[PATH_MAX] = { 0 }; get_libresplit_folder_path(libresplit_directory); + char libresplit_data_directory[PATH_MAX] = { 0 }; + get_libresplit_data_folder_path(libresplit_data_directory); + char auto_splitters_directory[PATH_MAX]; char themes_directory[PATH_MAX]; char splits_directory[PATH_MAX]; @@ -84,7 +106,10 @@ void check_directories(void) strcpy(runs_directory, libresplit_directory); strcat(runs_directory, "/runs"); - // Make the libresplit directory if it doesn't exist + // Make the libresplit data directory if it doesn't exist + mkdir_p(libresplit_data_directory, 0755); + + // Make the libresplit config directory if it doesn't exist mkdir_p(libresplit_directory, 0755); // Make the autosplitters directory if it doesn't exist diff --git a/src/settings/utils.h b/src/settings/utils.h index b0b358c..19e2381 100644 --- a/src/settings/utils.h +++ b/src/settings/utils.h @@ -1,4 +1,5 @@ #pragma once +void get_libresplit_data_folder_path(char* out_path); void get_libresplit_folder_path(char* out_path); -void check_directories(void); \ No newline at end of file +void check_directories(void); From cf7399b7fb4f3ec2f32348f739fa75b2945b30ed Mon Sep 17 00:00:00 2001 From: Pedro Montes Alcalde Date: Tue, 10 Mar 2026 15:53:05 -0300 Subject: [PATCH 4/9] timer: Fix game & timer leaks (#330) --- src/gui/app_window.c | 2 + src/timer.c | 164 ++++++++++++++++++++++++------------------- src/timer.h | 6 +- 3 files changed, 96 insertions(+), 76 deletions(-) diff --git a/src/gui/app_window.c b/src/gui/app_window.c index 62bee91..32bd990 100644 --- a/src/gui/app_window.c +++ b/src/gui/app_window.c @@ -205,9 +205,11 @@ void ls_app_window_destroy(GtkWidget* widget, gpointer data) LSAppWindow* win = (LSAppWindow*)widget; if (win->timer) { ls_timer_release(win->timer); + win->timer = 0; } if (win->game) { ls_game_release(win->game); + win->game = 0; } atomic_store(&auto_splitter_enabled, 0); atomic_store(&exit_requested, 1); diff --git a/src/timer.c b/src/timer.c index 7fefa60..b549dbb 100644 --- a/src/timer.c +++ b/src/timer.c @@ -215,69 +215,72 @@ void ls_delta_string(char* string, long long time) } /** - * Frees the memory allocated for a game struct. + * Frees the memory allocated for a game struct and sets all its pointers to NULL. * * @param game */ -void ls_game_release(const ls_game* game) +void ls_game_release(ls_game* game) { - if (game->path) { - free(game->path); - } if (game->title) { free(game->title); + game->title = 0; } if (game->theme) { free(game->theme); + game->theme = 0; } if (game->theme_variant) { free(game->theme_variant); + game->theme_variant = 0; } if (game->split_titles) { for (unsigned int i = 0; i < game->split_count; ++i) { if (game->split_titles[i]) { free(game->split_titles[i]); - free(game->split_icon_paths[i]); + game->split_titles[i] = 0; } } free(game->split_titles); + game->split_titles = 0; } if (game->split_times) { free(game->split_times); + game->split_times = 0; } if (game->split_icon_paths) { free(game->split_icon_paths); } if (game->segment_times) { free(game->segment_times); + game->segment_times = 0; } if (game->best_splits) { free(game->best_splits); + game->best_splits = 0; } if (game->best_segments) { free(game->best_segments); + game->best_segments = 0; } + + free(game); } int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg) { int error = 0; - ls_game* game; json_t* json = 0; json_t* ref; json_error_t json_error; // allocate game - game = calloc(1, sizeof(ls_game)); + ls_game* game = calloc(1, sizeof(ls_game)); if (!game) { error = 1; - goto game_create_done; + goto game_create_error; } // copy path to file - game->path = strdup(path); - if (!game->path) { - error = 1; - goto game_create_done; - } + strncpy(game->path, path, PATH_MAX - 1); + game->path[PATH_MAX - 1] = '\0'; // load json json = json_load_file(game->path, 0, &json_error); if (!json) { @@ -285,7 +288,7 @@ int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg) size_t msg_len = snprintf(NULL, 0, "%s (%d:%d)", json_error.text, json_error.line, json_error.column); *error_msg = calloc(msg_len + 1, sizeof(char)); sprintf(*error_msg, "%s (%d:%d)", json_error.text, json_error.line, json_error.column); - goto game_create_done; + goto game_create_error; } // copy title ref = json_object_get(json, "title"); @@ -293,7 +296,7 @@ int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg) game->title = strdup(json_string_value(ref)); if (!game->title) { error = 1; - goto game_create_done; + goto game_create_error; } } // copy theme @@ -302,7 +305,7 @@ int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg) game->theme = strdup(json_string_value(ref)); if (!game->theme) { error = 1; - goto game_create_done; + goto game_create_error; } } // copy theme variant @@ -311,7 +314,7 @@ int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg) game->theme_variant = strdup(json_string_value(ref)); if (!game->theme_variant) { error = 1; - goto game_create_done; + goto game_create_error; } } // get attempt count @@ -350,42 +353,40 @@ int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg) ref = json_object_get(json, "splits"); if (ref) { game->split_count = json_array_size(ref); + + int split_count = game->split_count + 1; // +1 for the final split to end cursor on + // allocate titles - game->split_titles = calloc(game->split_count, - sizeof(char*)); + game->split_titles = calloc(split_count, sizeof(char*)); if (!game->split_titles) { error = 1; - goto game_create_done; + goto game_create_error; } // allocate splits - game->split_times = calloc(game->split_count, - sizeof(long long)); + game->split_times = calloc(split_count, sizeof(long long)); if (!game->split_times) { error = 1; - goto game_create_done; + goto game_create_error; } - game->split_icon_paths = calloc(game->split_count, sizeof(char*)); + game->split_icon_paths = calloc(split_count, sizeof(char*)); if (!game->split_icon_paths) { error = 1; - goto game_create_done; + goto game_create_error; } - game->segment_times = calloc(game->split_count, - sizeof(long long)); + game->segment_times = calloc(split_count, sizeof(long long)); if (!game->segment_times) { error = 1; - goto game_create_done; + goto game_create_error; } - game->best_splits = calloc(game->split_count, - sizeof(long long)); + game->best_splits = calloc(split_count, sizeof(long long)); if (!game->best_splits) { error = 1; - goto game_create_done; + goto game_create_error; } - game->best_segments = calloc(game->split_count, - sizeof(long long)); + game->best_segments = calloc(split_count, sizeof(long long)); if (!game->best_segments) { error = 1; - goto game_create_done; + goto game_create_error; } game->contains_icons = false; // copy splits @@ -399,7 +400,7 @@ int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg) json_string_value(split_ref)); if (!game->split_titles[i]) { error = 1; - goto game_create_done; + goto game_create_error; } } @@ -408,7 +409,7 @@ int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg) game->split_icon_paths[i] = strdup(json_string_value(split_ref)); if (!game->split_icon_paths[i]) { error = 1; - goto game_create_done; + goto game_create_error; } game->contains_icons = true; } @@ -452,16 +453,27 @@ int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg) } } } -game_create_done: - if (!error) { - *game_ptr = game; - } else if (game) { - ls_game_release(game); - } +game_create_error: if (json) { json_decref(json); } - return error; + + if (error) { + if (game) { + ls_game_release(game); + game = 0; + } + return error; + } + + // Free all of the old game's data before replacing the pointer + if (*game_ptr) { + ls_game_release(*game_ptr); + *game_ptr = 0; + } + *game_ptr = game; + + return 0; } /** @@ -686,7 +698,7 @@ int ls_run_save(ls_timer* timer, const char* reason) * * @param timer The timer instance */ -void ls_timer_release(const ls_timer* timer) +void ls_timer_release(ls_timer* timer) { if (timer->split_times) { free(timer->split_times); @@ -709,6 +721,8 @@ void ls_timer_release(const ls_timer* timer) if (timer->best_segments) { free(timer->best_segments); } + + free(timer); } /** @@ -767,62 +781,66 @@ int ls_timer_create(ls_timer** timer_ptr, ls_game* game) timer = calloc(1, sizeof(ls_timer)); if (!timer) { error = 1; - goto timer_create_done; + goto timer_create_error; } timer->game = game; timer->attempt_count = &game->attempt_count; timer->finished_count = &game->finished_count; // alloc splits - timer->split_times = calloc(timer->game->split_count, - sizeof(long long)); + int split_count = timer->game->split_count + 1; // +1 for the last invisible "split" that exists to signify no split + + timer->split_times = calloc(split_count, sizeof(long long)); if (!timer->split_times) { error = 1; - goto timer_create_done; + goto timer_create_error; } - timer->split_deltas = calloc(timer->game->split_count, - sizeof(long long)); + timer->split_deltas = calloc(split_count, sizeof(long long)); if (!timer->split_deltas) { error = 1; - goto timer_create_done; + goto timer_create_error; } - timer->segment_times = calloc(timer->game->split_count, - sizeof(long long)); + timer->segment_times = calloc(split_count, sizeof(long long)); if (!timer->segment_times) { error = 1; - goto timer_create_done; + goto timer_create_error; } - timer->segment_deltas = calloc(timer->game->split_count, - sizeof(long long)); + timer->segment_deltas = calloc(split_count, sizeof(long long)); if (!timer->segment_deltas) { error = 1; - goto timer_create_done; + goto timer_create_error; } - timer->best_splits = calloc(timer->game->split_count, - sizeof(long long)); + timer->best_splits = calloc(split_count, sizeof(long long)); if (!timer->best_splits) { error = 1; - goto timer_create_done; + goto timer_create_error; } - timer->best_segments = calloc(timer->game->split_count, - sizeof(long long)); + timer->best_segments = calloc(split_count, sizeof(long long)); if (!timer->best_segments) { error = 1; - goto timer_create_done; + goto timer_create_error; } - timer->split_info = calloc(timer->game->split_count, - sizeof(int)); + timer->split_info = calloc(split_count, sizeof(int)); if (!timer->split_info) { error = 1; - goto timer_create_done; + goto timer_create_error; } reset_timer(timer); -timer_create_done: - if (!error) { - *timer_ptr = timer; - } else if (timer) { - ls_timer_release(timer); +timer_create_error: + if (error) { + if (timer) { + ls_timer_release(timer); + timer = 0; + } + return error; } - return error; + + // Free old timer before replacing the pointer + if (*timer_ptr) { + ls_timer_release(*timer_ptr); + *timer_ptr = 0; + } + *timer_ptr = timer; + return 0; } /** diff --git a/src/timer.h b/src/timer.h index 52fc3b5..6caac6a 100644 --- a/src/timer.h +++ b/src/timer.h @@ -12,7 +12,7 @@ extern AppConfig cfg; typedef struct ls_game { - char* path; + char path[PATH_MAX]; char* title; char* theme; char* theme_variant; @@ -84,11 +84,11 @@ bool ls_timer_has_gold_split(const ls_timer* timer); int ls_game_save(const ls_game* game); -void ls_game_release(const ls_game* game); +void ls_game_release(ls_game* game); int ls_timer_create(ls_timer** timer_ptr, ls_game* game); -void ls_timer_release(const ls_timer* timer); +void ls_timer_release(ls_timer* timer); int ls_timer_start(ls_timer* timer); From 3c07a9a25f357e388e0126bcd4940795dab231d9 Mon Sep 17 00:00:00 2001 From: Daniele Penazzo Date: Sat, 28 Mar 2026 00:17:42 +0100 Subject: [PATCH 5/9] Make sure dialogs at least try to find a parent window (#343) * Make sure dialogs at least try to find a parent window Given when they're viewed, the only active window should be the main window from LibreSplit. If push comes to shove, we can change the code to iterate through all windows until we find one that is "marked" as the main window. Fixes #261 * Autoimporter stuff --- src/gui/dialogs.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/gui/dialogs.c b/src/gui/dialogs.c index 6498433..bf68e94 100644 --- a/src/gui/dialogs.c +++ b/src/gui/dialogs.c @@ -6,6 +6,7 @@ * @return False, to remove the function from the queue. */ #include "src/lasr/auto-splitter.h" +#include #include #include #include @@ -29,8 +30,13 @@ static void dialog_response_cb(GtkWidget* dialog, gint response_id, gpointer use gboolean display_non_capable_mem_read_dialog(gpointer data) { atomic_store(&auto_splitter_enabled, 0); + GtkApplication* app = GTK_APPLICATION(g_application_get_default()); + GtkWindow* win = NULL; + if (app != NULL) { + win = gtk_application_get_active_window(app); + } GtkWidget* dialog = gtk_message_dialog_new( - GTK_WINDOW(NULL), + GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_NONE, @@ -59,8 +65,13 @@ gboolean display_non_capable_mem_read_dialog(gpointer data) bool display_confirm_reset_dialog(void) { + GtkApplication* app = GTK_APPLICATION(g_application_get_default()); + GtkWindow* win = NULL; + if (app != NULL) { + win = gtk_application_get_active_window(app); + } GtkWidget* dialog = gtk_message_dialog_new( - GTK_WINDOW(NULL), + GTK_WINDOW(win), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO, From 8a288e9eab85bcc698435c789cd711a6e900ca9d Mon Sep 17 00:00:00 2001 From: Pedro Montes Alcalde Date: Sat, 28 Mar 2026 18:01:34 -0300 Subject: [PATCH 6/9] actions: Fix possible nullptr dereference when opening a file dialog (#349) --- src/gui/actions.c | 36 +++++++++++++++++++++++------------- src/settings/settings.h | 6 +++++- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/gui/actions.c b/src/gui/actions.c index 73453ae..7125634 100644 --- a/src/gui/actions.c +++ b/src/gui/actions.c @@ -110,15 +110,20 @@ void open_activated(GSimpleAction* action, res = gtk_dialog_run(GTK_DIALOG(dialog)); if (res == GTK_RESPONSE_ACCEPT) { - char* filename; GtkFileChooser* chooser = GTK_FILE_CHOOSER(dialog); char last_folder[PATH_MAX]; - filename = gtk_file_chooser_get_filename(chooser); - strcpy(last_folder, gtk_file_chooser_get_current_folder(chooser)); - CFG_SET_STR(cfg.history.last_split_folder.value.s, last_folder); - ls_app_window_open(win, filename); - CFG_SET_STR(cfg.history.split_file.value.s, filename); - g_free(filename); + char* filename = gtk_file_chooser_get_filename(chooser); + const char* current_folder = gtk_file_chooser_get_current_folder(chooser); + if (current_folder) { + strncpy(last_folder, current_folder, sizeof(last_folder) - 1); + last_folder[sizeof(last_folder) - 1] = '\0'; + CFG_SET_STR(cfg.history.last_split_folder.value.s, last_folder); + } + if (filename) { + ls_app_window_open(win, filename); + CFG_SET_STR(cfg.history.split_file.value.s, filename); + g_free(filename); + } } if (!win->game || !win->timer) { gtk_widget_show_all(win->welcome_box->box); @@ -389,16 +394,21 @@ void open_auto_splitter(GSimpleAction* action, GtkFileChooser* chooser = GTK_FILE_CHOOSER(dialog); char* filename = gtk_file_chooser_get_filename(chooser); char last_folder[PATH_MAX]; - strcpy(last_folder, gtk_file_chooser_get_current_folder(chooser)); - CFG_SET_STR(cfg.history.last_auto_splitter_folder.value.s, last_folder); - CFG_SET_STR(cfg.history.auto_splitter_file.value.s, filename); - strcpy(auto_splitter_file, filename); + const char* current_folder = gtk_file_chooser_get_current_folder(chooser); + if (current_folder) { + strncpy(last_folder, current_folder, sizeof(last_folder) - 1); + last_folder[sizeof(last_folder) - 1] = '\0'; + CFG_SET_STR(cfg.history.last_auto_splitter_folder.value.s, last_folder); + } + if (filename) { + CFG_SET_STR(cfg.history.auto_splitter_file.value.s, filename); + strcpy(auto_splitter_file, filename); + g_free(filename); + } config_save(); // Restart auto-splitter if it was running restart_auto_splitter(); - - g_free(filename); } gtk_widget_destroy(dialog); } diff --git a/src/settings/settings.h b/src/settings/settings.h index b188a43..2aa3fe6 100644 --- a/src/settings/settings.h +++ b/src/settings/settings.h @@ -8,7 +8,11 @@ extern AppConfig cfg; -#define CFG_SET_STR(a, b) strncpy(a, b, sizeof(a) - 1); +#define CFG_SET_STR(a, b) \ + do { \ + strncpy(a, b, sizeof(a) - 1); \ + a[sizeof(a) - 1] = '\0'; \ + } while (0) bool config_init(void); bool config_save(void); From 3dbf7614876ca140cb3ed1fbf4330b4f7e54e562 Mon Sep 17 00:00:00 2001 From: Daniele Penazzo Date: Sun, 29 Mar 2026 00:25:04 +0100 Subject: [PATCH 7/9] Address Scan build Warnings (#344) * Add guard in maps.c to avoid NULL-dereferencing * Remove dead-store in bind.c It's assigned in the for loop anyway and never read outside of it. * Remove dead-store in theming.c We assign the error flag and then never read from it again. * Remove dead-store in prev-segment.c It's assigned in the if statement 3 lines below, and never read outside of it. * Move GSList declaration inside for loop * Address GCC's complaints about missing va_end()s * Address possible failure in callocating error_msg --- src/gui/component/prev-segment.c | 1 - src/gui/theming.c | 1 - src/keybinds/bind.c | 6 ++---- src/lasr/auto-splitter.c | 6 ++++++ src/lasr/maps/maps.c | 10 +++++++--- src/timer.c | 6 ++++++ 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/gui/component/prev-segment.c b/src/gui/component/prev-segment.c index 3af0a15..beaa910 100644 --- a/src/gui/component/prev-segment.c +++ b/src/gui/component/prev-segment.c @@ -139,7 +139,6 @@ static void prev_segment_draw(LSComponent* self_, const ls_game* game, ls_delta_string(str, timer->segment_deltas[curr]); gtk_label_set_text(GTK_LABEL(self->previous_segment), str); } else if (curr) { - prev = timer->curr_split - 1; // Previous segment if (timer->curr_split) { prev = timer->curr_split - 1; diff --git a/src/gui/theming.c b/src/gui/theming.c index b838c91..3243e73 100644 --- a/src/gui/theming.c +++ b/src/gui/theming.c @@ -150,7 +150,6 @@ void ls_app_load_theme_with_fallback(LSAppWindow* win, const char* name, const c (gssize)fallback_css_data_len(), &gerror); if (gerror != NULL) { g_printerr("Error loading default theme CSS: %s\n", gerror->message); - error = true; g_error_free(gerror); gerror = NULL; } diff --git a/src/keybinds/bind.c b/src/keybinds/bind.c index 5e0fb4a..8f0034e 100644 --- a/src/keybinds/bind.c +++ b/src/keybinds/bind.c @@ -548,9 +548,8 @@ keybinder_bind_full(const char* keystring, */ void keybinder_unbind(const char* keystring, KeybinderHandler handler) { - GSList* iter; - for (iter = bindings; iter != NULL; iter = iter->next) { + for (GSList* iter = bindings; iter != NULL; iter = iter->next) { struct Binding* binding = iter->data; if (strcmp(keystring, binding->keystring) != 0 || handler != binding->handler) @@ -581,9 +580,8 @@ void keybinder_unbind(const char* keystring, KeybinderHandler handler) */ void keybinder_unbind_all(const char* keystring) { - GSList* iter = bindings; - for (iter = bindings; iter != NULL; iter = iter->next) { + for (GSList* iter = bindings; iter != NULL; iter = iter->next) { struct Binding* binding = iter->data; if (strcmp(keystring, binding->keystring) != 0) { diff --git a/src/lasr/auto-splitter.c b/src/lasr/auto-splitter.c index d18db66..673f693 100644 --- a/src/lasr/auto-splitter.c +++ b/src/lasr/auto-splitter.c @@ -209,6 +209,7 @@ bool call_va(lua_State* L, const char* func, const char* sig, ...) default: printf("invalid option (%c)\n", *(sig - 1)); + va_end(vl); return false; } if (*(sig - 1) == '>') @@ -235,6 +236,7 @@ bool call_va(lua_State* L, const char* func, const char* sig, ...) case 'd': /* double result */ if (!lua_isnumber(L, nres)) { printf("function '%s' wrong result type, expected double\n", func); + va_end(vl); return false; } *va_arg(vl, double*) = lua_tonumber(L, nres); @@ -243,6 +245,7 @@ bool call_va(lua_State* L, const char* func, const char* sig, ...) case 'i': /* int result */ if (!lua_isnumber(L, nres)) { printf("function '%s' wrong result type, expected int\n", func); + va_end(vl); return false; } *va_arg(vl, int*) = lua_tointeger(L, nres); @@ -251,6 +254,7 @@ bool call_va(lua_State* L, const char* func, const char* sig, ...) case 's': /* string result */ if (!lua_isstring(L, nres)) { printf("function '%s' wrong result type, expected string\n", func); + va_end(vl); return false; } *va_arg(vl, const char**) = lua_tostring(L, nres); @@ -259,6 +263,7 @@ bool call_va(lua_State* L, const char* func, const char* sig, ...) case 'b': if (!lua_isboolean(L, nres)) { printf("function '%s' wrong result type, expected boolean\n", func); + va_end(vl); return false; } *va_arg(vl, bool*) = lua_toboolean(L, nres); @@ -266,6 +271,7 @@ bool call_va(lua_State* L, const char* func, const char* sig, ...) default: printf("invalid option (%c)\n", *(sig - 1)); + va_end(vl); return false; } nres++; diff --git a/src/lasr/maps/maps.c b/src/lasr/maps/maps.c index 374aa6c..35bc826 100644 --- a/src/lasr/maps/maps.c +++ b/src/lasr/maps/maps.c @@ -39,10 +39,14 @@ static void append_entry(ProcessMap e) new_block->used = 0; new_block->next = NULL; - if (!head) + if (!head) { head = new_block; - else - current->next = new_block; + } else { + if (current) { + // Guard to avoid null-dereferencing + current->next = new_block; + } + } current = new_block; } diff --git a/src/timer.c b/src/timer.c index b549dbb..3bfa94a 100644 --- a/src/timer.c +++ b/src/timer.c @@ -4,6 +4,7 @@ */ #include "timer.h" #include "gui/dialogs.h" +#include "logging.h" #include "settings/utils.h" #include "lasr/auto-splitter.h" @@ -287,6 +288,11 @@ int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg) error = 1; size_t msg_len = snprintf(NULL, 0, "%s (%d:%d)", json_error.text, json_error.line, json_error.column); *error_msg = calloc(msg_len + 1, sizeof(char)); + if (*error_msg == NULL) { + LOG_ERR("Cannot allocate memory for error message"); + error = 1; + goto game_create_error; + } sprintf(*error_msg, "%s (%d:%d)", json_error.text, json_error.line, json_error.column); goto game_create_error; } From 3a4f3e05638ff7b3dab3daaf54daced2f068b01d Mon Sep 17 00:00:00 2001 From: Daniele Penazzo Date: Sun, 29 Mar 2026 23:29:33 +0200 Subject: [PATCH 8/9] Save context menu pointer in window (#351) Previously the menu was regenerated and the old pointer never freed, leading to a memory leak. Fixes #350 --- src/gui/app_window.c | 1 + src/gui/app_window.h | 1 + src/gui/context_menu.c | 86 ++++++++++++++++++++++-------------------- 3 files changed, 47 insertions(+), 41 deletions(-) diff --git a/src/gui/app_window.c b/src/gui/app_window.c index 32bd990..b151428 100644 --- a/src/gui/app_window.c +++ b/src/gui/app_window.c @@ -326,6 +326,7 @@ static void ls_app_window_init(LSAppWindow* win) win->display = gdk_display_get_default(); win->style = NULL; + win->context_menu = NULL; // make data path win->data_path[0] = '\0'; diff --git a/src/gui/app_window.h b/src/gui/app_window.h index 0c4f50c..69211fc 100644 --- a/src/gui/app_window.h +++ b/src/gui/app_window.h @@ -46,6 +46,7 @@ typedef struct _LSAppWindow { GtkWidget* container; LSWelcomeBox* welcome_box; GtkWidget* box; + GtkWidget* context_menu; /*!< The context menu */ GList* components; GtkWidget* footer; GtkCssProvider* reset_style; /*!< The "reset rules" provider, will remove desktop theme rules */ diff --git a/src/gui/context_menu.c b/src/gui/context_menu.c index 5c04b61..b22a7d6 100644 --- a/src/gui/context_menu.c +++ b/src/gui/context_menu.c @@ -25,50 +25,54 @@ gboolean button_right_click(GtkWidget* widget, GdkEventButton* event, gpointer a } else { win = ls_app_window_new(LS_APP(app)); } - GtkWidget* menu = gtk_menu_new(); - GtkWidget* menu_open_splits = gtk_menu_item_new_with_label("Open Splits"); - GtkWidget* menu_save_splits = gtk_menu_item_new_with_label("Save Splits"); - GtkWidget* menu_open_auto_splitter = gtk_menu_item_new_with_label("Open Auto Splitter"); - GtkWidget* menu_enable_auto_splitter = gtk_check_menu_item_new_with_label("Enable Auto Splitter"); - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menu_enable_auto_splitter), atomic_load(&auto_splitter_enabled)); - GtkWidget* menu_enable_win_on_top = gtk_check_menu_item_new_with_label("Always on Top"); - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menu_enable_win_on_top), win->opts.win_on_top); - GtkWidget* menu_reload = gtk_menu_item_new_with_label("Reload"); - GtkWidget* menu_close = gtk_menu_item_new_with_label("Close"); - GtkWidget* menu_settings = gtk_menu_item_new_with_label("Settings"); - GtkWidget* menu_about = gtk_menu_item_new_with_label("About and help"); - GtkWidget* menu_quit = gtk_menu_item_new_with_label("Quit"); + if (win->context_menu == NULL) { + GtkWidget* menu = gtk_menu_new(); + GtkWidget* menu_open_splits = gtk_menu_item_new_with_label("Open Splits"); + GtkWidget* menu_save_splits = gtk_menu_item_new_with_label("Save Splits"); + GtkWidget* menu_open_auto_splitter = gtk_menu_item_new_with_label("Open Auto Splitter"); + GtkWidget* menu_enable_auto_splitter = gtk_check_menu_item_new_with_label("Enable Auto Splitter"); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menu_enable_auto_splitter), atomic_load(&auto_splitter_enabled)); + GtkWidget* menu_enable_win_on_top = gtk_check_menu_item_new_with_label("Always on Top"); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menu_enable_win_on_top), win->opts.win_on_top); + GtkWidget* menu_reload = gtk_menu_item_new_with_label("Reload"); + GtkWidget* menu_close = gtk_menu_item_new_with_label("Close"); + GtkWidget* menu_settings = gtk_menu_item_new_with_label("Settings"); + GtkWidget* menu_about = gtk_menu_item_new_with_label("About and help"); + GtkWidget* menu_quit = gtk_menu_item_new_with_label("Quit"); - // Add the menu items to the menu - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_open_splits); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_save_splits); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_open_auto_splitter); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_enable_auto_splitter); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_reload); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_close); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_enable_win_on_top); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_settings); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_about); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_quit); + // Add the menu items to the menu + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_open_splits); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_save_splits); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_open_auto_splitter); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_enable_auto_splitter); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_reload); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_close); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_enable_win_on_top); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_settings); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_about); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_quit); - // Attach the callback functions to the menu items - g_signal_connect(menu_open_splits, "activate", G_CALLBACK(open_activated), app); - g_signal_connect(menu_save_splits, "activate", G_CALLBACK(save_activated), app); - g_signal_connect(menu_open_auto_splitter, "activate", G_CALLBACK(open_auto_splitter), app); - g_signal_connect(menu_enable_auto_splitter, "toggled", G_CALLBACK(toggle_auto_splitter), NULL); - g_signal_connect(menu_enable_win_on_top, "toggled", G_CALLBACK(menu_toggle_win_on_top), app); - g_signal_connect(menu_reload, "activate", G_CALLBACK(reload_activated), app); - g_signal_connect(menu_close, "activate", G_CALLBACK(close_activated), app); - g_signal_connect(menu_settings, "activate", G_CALLBACK(show_settings_dialog), app); - g_signal_connect(menu_about, "activate", G_CALLBACK(show_help_dialog), app); - g_signal_connect(menu_quit, "activate", G_CALLBACK(quit_activated), app); + // Attach the callback functions to the menu items + g_signal_connect(menu_open_splits, "activate", G_CALLBACK(open_activated), app); + g_signal_connect(menu_save_splits, "activate", G_CALLBACK(save_activated), app); + g_signal_connect(menu_open_auto_splitter, "activate", G_CALLBACK(open_auto_splitter), app); + g_signal_connect(menu_enable_auto_splitter, "toggled", G_CALLBACK(toggle_auto_splitter), NULL); + g_signal_connect(menu_enable_win_on_top, "toggled", G_CALLBACK(menu_toggle_win_on_top), app); + g_signal_connect(menu_reload, "activate", G_CALLBACK(reload_activated), app); + g_signal_connect(menu_close, "activate", G_CALLBACK(close_activated), app); + g_signal_connect(menu_settings, "activate", G_CALLBACK(show_settings_dialog), app); + g_signal_connect(menu_about, "activate", G_CALLBACK(show_help_dialog), app); + g_signal_connect(menu_quit, "activate", G_CALLBACK(quit_activated), app); - gtk_widget_show_all(menu); - gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)event); + win->context_menu = menu; + } + + gtk_widget_show_all(win->context_menu); + gtk_menu_popup_at_pointer(GTK_MENU(win->context_menu), (GdkEvent*)event); return TRUE; } return FALSE; From 2c18bcbf5e300fceff9ecdc72ed5000fcd1e1f9b Mon Sep 17 00:00:00 2001 From: Jack Tench <79285604+JackTench@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:46:14 +0100 Subject: [PATCH 9/9] Ensure dialog windows can only have one instance open (#356) * Convert help dialog to singleton * Convert settings dialog to singleton --- src/gui/help_dialog.c | 10 ++++++++++ src/gui/settings_dialog.c | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/gui/help_dialog.c b/src/gui/help_dialog.c index 4baf321..14bee49 100644 --- a/src/gui/help_dialog.c +++ b/src/gui/help_dialog.c @@ -3,15 +3,25 @@ #include #include +static GtkWidget* help_window_singleton = NULL; + static gboolean on_help_window_delete(GtkWidget* widget, GdkEvent* event, gpointer user_data) { + help_window_singleton = NULL; gtk_widget_destroy(widget); return TRUE; } static void build_help_dialog(GtkApplication* app, gpointer data) { + // Show already open window if another one is called. + if (help_window_singleton) { + gtk_window_present(GTK_WINDOW(help_window_singleton)); + return; + } + GtkWidget* window = gtk_application_window_new(app); + help_window_singleton = window; gtk_window_set_title(GTK_WINDOW(window), "About LibreSplit"); gtk_window_set_default_size(GTK_WINDOW(window), 200, 320); gtk_window_set_resizable(GTK_WINDOW(window), FALSE); diff --git a/src/gui/settings_dialog.c b/src/gui/settings_dialog.c index af33e06..ca380afe 100644 --- a/src/gui/settings_dialog.c +++ b/src/gui/settings_dialog.c @@ -11,6 +11,8 @@ static LSGuiSetting* gui_settings = NULL; +static GtkWidget* settings_window_singleton = NULL; + /** * Takes the application config and counts how many settings are available. * @@ -44,6 +46,7 @@ static size_t enumerate_settings(AppConfig cfg) */ static gboolean on_help_window_delete(GtkWidget* widget, GdkEvent* event, gpointer user_data) { + settings_window_singleton = NULL; gtk_widget_destroy(widget); free(gui_settings); gui_settings = NULL; @@ -162,6 +165,12 @@ static void set_widget_defaults(GtkWidget* obj) static void build_settings_dialog(GtkApplication* app, gpointer data) { + // Show already open window if another one is called. + if (settings_window_singleton) { + gtk_window_present(GTK_WINDOW(settings_window_singleton)); + return; + } + int settings_number = enumerate_settings(cfg); gui_settings = malloc(settings_number * sizeof(LSGuiSetting)); if (gui_settings == NULL) { @@ -170,6 +179,7 @@ static void build_settings_dialog(GtkApplication* app, gpointer data) } GtkWidget* window = gtk_application_window_new(app); + settings_window_singleton = window; gtk_window_set_title(GTK_WINDOW(window), "LibreSplit Settings"); gtk_window_set_default_size(GTK_WINDOW(window), 500, 500); gtk_window_set_resizable(GTK_WINDOW(window), FALSE);