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/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/actions.c b/src/gui/actions.c index b3f5f72..7125634 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 && win->timer->running) { GtkWidget* warning = gtk_message_dialog_new( GTK_WINDOW(win), GTK_DIALOG_MODAL, @@ -108,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); @@ -234,6 +241,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 +355,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 && win->timer->running) { GtkWidget* warning = gtk_message_dialog_new( GTK_WINDOW(win), GTK_DIALOG_MODAL, @@ -384,23 +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 - 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); - } - - g_free(filename); + restart_auto_splitter(); } gtk_widget_destroy(dialog); } diff --git a/src/gui/app_window.c b/src/gui/app_window.c index 47b07fc..934d022 100644 --- a/src/gui/app_window.c +++ b/src/gui/app_window.c @@ -8,8 +8,12 @@ #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" +#include +#include #include extern atomic_bool exit_requested; /*!< Set to 1 when LibreSplit is exiting */ @@ -227,12 +231,15 @@ 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); + close_logger(); // Close any other open application windows (settings, dialogs, etc.) GApplication* app = g_application_get_default(); if (app) { @@ -256,7 +263,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)); @@ -266,37 +272,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); @@ -332,6 +352,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/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..beaa910 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"); @@ -139,11 +139,10 @@ 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; - 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 607bd86..8348b11 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 */ /*! The GTKBox containing all the split rows (except the last one, most of the time.) * The last split is added to this container when the scrollbox is scrolled all the way down. */ @@ -29,6 +29,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 */ @@ -146,7 +166,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*)); @@ -155,34 +174,31 @@ 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; } 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); @@ -273,7 +289,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_); } /** @@ -293,10 +310,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; } @@ -312,10 +326,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"); @@ -379,7 +392,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; @@ -389,7 +402,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); @@ -403,9 +416,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_; @@ -414,9 +434,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/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; diff --git a/src/gui/dialogs.c b/src/gui/dialogs.c index 2605490..0b96acd 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 @@ -37,8 +38,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, @@ -73,8 +79,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, 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/help_dialog.c b/src/gui/help_dialog.c index 3058dbd..ac8dd73 100644 --- a/src/gui/help_dialog.c +++ b/src/gui/help_dialog.c @@ -3,6 +3,8 @@ #include #include +static GtkWidget* help_window_singleton = NULL; + /** * Destructor for the help dialog window. * @@ -14,6 +16,7 @@ */ static gboolean on_help_window_delete(GtkWidget* widget, GdkEvent* event, gpointer user_data) { + help_window_singleton = NULL; gtk_widget_destroy(widget); return TRUE; } @@ -26,7 +29,14 @@ static gboolean on_help_window_delete(GtkWidget* widget, GdkEvent* event, gpoint */ 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 9b5bf27..6a9ad63 100644 --- a/src/gui/settings_dialog.c +++ b/src/gui/settings_dialog.c @@ -9,7 +9,9 @@ #include #include -static LSGuiSetting* gui_settings; +static LSGuiSetting* gui_settings = NULL; + +static GtkWidget* settings_window_singleton = NULL; /** * Takes the application config and counts how many settings are available. @@ -44,8 +46,10 @@ 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; return TRUE; } @@ -178,10 +182,21 @@ 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) { + printf("Cannot allocate memory for the settings GUI."); + return; + } 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); diff --git a/src/gui/theming.c b/src/gui/theming.c index f018a95..cf470b9 100644 --- a/src/gui/theming.c +++ b/src/gui/theming.c @@ -158,7 +158,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/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/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/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 7c96719..38fd2f1 100644 --- a/src/keybinds/delayed_handlers.c +++ b/src/keybinds/delayed_handlers.c @@ -15,7 +15,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..673f693 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 */ /** @@ -207,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) == '>') @@ -233,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); @@ -241,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); @@ -249,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); @@ -257,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); @@ -264,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++; @@ -305,6 +313,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 +358,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 +414,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 +534,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 +550,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/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/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/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 ef8d17b..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" @@ -45,7 +46,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); @@ -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/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); 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); diff --git a/src/timer.c b/src/timer.c index 79b5a29..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" @@ -23,13 +24,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,72 +215,86 @@ void ls_delta_string(char* string, long long time) ls_time_string_format(string, NULL, time, 0, 1, 1); } -void ls_game_release(const ls_game* game) +/** + * Frees the memory allocated for a game struct and sets all its pointers to NULL. + * + * @param game + */ +void ls_game_release(ls_game* game) { - int i; - 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 (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]); + 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; - int i; 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) { 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_done; + goto game_create_error; } // copy title ref = json_object_get(json, "title"); @@ -267,7 +302,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 @@ -276,7 +311,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 @@ -285,7 +320,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 @@ -324,46 +359,44 @@ 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 - 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); @@ -373,7 +406,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; } } @@ -382,7 +415,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; } @@ -426,20 +459,36 @@ 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; } -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 +503,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 +546,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 +565,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 +612,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 +638,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,7 +699,12 @@ int ls_run_save(ls_timer* timer, const char* reason) return error; } -void ls_timer_release(const ls_timer* timer) +/** + * Frees all the timer information, but not itself + * + * @param timer The timer instance + */ +void ls_timer_release(ls_timer* timer) { if (timer->split_times) { free(timer->split_times); @@ -674,17 +727,29 @@ void ls_timer_release(const ls_timer* timer) if (timer->best_segments) { free(timer->best_segments); } + + free(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 +759,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 +772,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; @@ -715,72 +787,84 @@ 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; } -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 +907,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..6caac6a 100644 --- a/src/timer.h +++ b/src/timer.h @@ -4,15 +4,15 @@ #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; typedef struct ls_game { - char* path; + char path[PATH_MAX]; char* title; char* theme; char* theme_variant; @@ -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); @@ -78,15 +84,15 @@ 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); -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);