Merge branch 'main' into moredocs

This commit is contained in:
Penaz 2026-04-09 22:15:46 +02:00
commit b7891cddf4
38 changed files with 1200 additions and 554 deletions

View file

@ -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')

View file

@ -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',

View file

@ -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 <gtk/gtk.h>
#include <sys/stat.h>
@ -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);
}

View file

@ -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 <stdatomic.h>
#include <stdio.h>
#include <sys/stat.h>
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';

View file

@ -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 */

View file

@ -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);

View file

@ -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 },

View file

@ -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;

View file

@ -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);

View file

@ -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]);

View file

@ -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");

View file

@ -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;
}

View file

@ -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;

View file

@ -6,6 +6,7 @@
* @return False, to remove the function from the queue.
*/
#include "src/lasr/auto-splitter.h"
#include <gio/gio.h>
#include <glib.h>
#include <gtk/gtk.h>
#include <stdatomic.h>
@ -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,

View file

@ -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);

View file

@ -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);

View file

@ -3,6 +3,8 @@
#include <gtk/gtk.h>
#include <stdio.h>
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);

View file

@ -9,7 +9,9 @@
#include <gtk/gtk.h>
#include <stdio.h>
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);

View file

@ -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;
}

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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) {

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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)) {

View file

@ -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);
}

View file

@ -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;
/**

View file

@ -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;
}

View file

@ -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 <glib.h>
#include <stdatomic.h>
#include <stdio.h>
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.
*

View file

@ -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);

151
src/logging.c Normal file
View file

@ -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 <linux/limits.h>
#include <linux/prctl.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/prctl.h>
#include <time.h>
/*! 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(&current_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);
}

85
src/logging.h Normal file
View file

@ -0,0 +1,85 @@
#pragma once
#include <pthread.h>
#include <stdatomic.h>
#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

View file

@ -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;
}

View file

@ -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);

View file

@ -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

View file

@ -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);
void check_directories(void);

View file

@ -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, &timespec);
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;
}

View file

@ -4,15 +4,15 @@
#include <stdatomic.h>
#include <stdbool.h>
#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);