├── src ├── config.h.in ├── server.h ├── settings.h ├── memory.h ├── shared.c ├── shared.h ├── signature.h ├── auto-splitter.h ├── process.h ├── component │ ├── components.c │ ├── components.h │ ├── best-sum.c │ ├── wr.c │ ├── pb.c │ ├── title.c │ ├── clock.c │ ├── prev-segment.c │ ├── detailed-timer.c │ └── splits.c ├── bind.h ├── timer.h ├── main.css ├── ctl.c ├── server.c ├── settings.c ├── process.c ├── signature.c ├── memory.c ├── auto-splitter.c └── bind.c ├── .clang-format ├── docs ├── images │ └── gtk_debugger.png ├── troubleshooting.md ├── split-files.md ├── settings-keybinds.md ├── themes.md └── auto-splitters.md ├── assets ├── icons │ ├── libresplit-1024.png │ ├── libresplit-128.png │ ├── libresplit-16.png │ ├── libresplit-22.png │ ├── libresplit-24.png │ ├── libresplit-256.png │ ├── libresplit-32.png │ ├── libresplit-36.png │ ├── libresplit-48.png │ ├── libresplit-512.png │ ├── libresplit-64.png │ ├── libresplit-72.png │ └── libresplit-96.png ├── libresplit.desktop ├── appimage.sh ├── default_settings.json └── libresplit.svg ├── .gitignore ├── .github └── workflows │ ├── publish-wiki.yml │ ├── format.yml │ └── build.yml ├── meson.build └── README.md /src/config.h.in: -------------------------------------------------------------------------------- 1 | #define DEFAULT_CONFIG_PATH "@DEFAULT_CONFIG_PATH@" 2 | -------------------------------------------------------------------------------- /src/server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void* ls_ctl_server(void* arg); 4 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: WebKit 3 | IndentCaseBlocks: true 4 | IndentCaseLabels: true 5 | -------------------------------------------------------------------------------- /docs/images/gtk_debugger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/docs/images/gtk_debugger.png -------------------------------------------------------------------------------- /assets/icons/libresplit-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-1024.png -------------------------------------------------------------------------------- /assets/icons/libresplit-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-128.png -------------------------------------------------------------------------------- /assets/icons/libresplit-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-16.png -------------------------------------------------------------------------------- /assets/icons/libresplit-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-22.png -------------------------------------------------------------------------------- /assets/icons/libresplit-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-24.png -------------------------------------------------------------------------------- /assets/icons/libresplit-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-256.png -------------------------------------------------------------------------------- /assets/icons/libresplit-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-32.png -------------------------------------------------------------------------------- /assets/icons/libresplit-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-36.png -------------------------------------------------------------------------------- /assets/icons/libresplit-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-48.png -------------------------------------------------------------------------------- /assets/icons/libresplit-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-512.png -------------------------------------------------------------------------------- /assets/icons/libresplit-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-64.png -------------------------------------------------------------------------------- /assets/icons/libresplit-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-72.png -------------------------------------------------------------------------------- /assets/icons/libresplit-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wins1ey/LibreSplit/HEAD/assets/icons/libresplit-96.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .vscode/ 3 | .cache/ 4 | compile_commands.json 5 | 6 | # AppImage stuff 7 | AppDir/ 8 | *.AppImage -------------------------------------------------------------------------------- /assets/libresplit.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=LibreSplit 3 | Exec=libresplit 4 | Icon=libresplit 5 | Type=Application 6 | Categories=GTK;GNOME;Utility; 7 | -------------------------------------------------------------------------------- /assets/appimage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# -eq 0 ]; then 4 | exec "$APPDIR/usr/bin/libresplit" 5 | else 6 | exec "$APPDIR/usr/bin/libresplit-ctl" "$@" 7 | fi 8 | -------------------------------------------------------------------------------- /src/settings.h: -------------------------------------------------------------------------------- 1 | #ifndef __SETTINGS_H__ 2 | #define __SETTINGS_H__ 3 | 4 | #include 5 | 6 | void get_libresplit_folder_path(char* out_path); 7 | void ls_update_setting(const char* section, const char* setting, json_t* value); 8 | json_t* get_setting_value(const char* section, const char* setting); 9 | 10 | #endif /* __SETTINGS_H__ */ 11 | -------------------------------------------------------------------------------- /src/memory.h: -------------------------------------------------------------------------------- 1 | #ifndef __MEMORY_H__ 2 | #define __MEMORY_H__ 3 | 4 | #include "lua.h" 5 | #include 6 | #include 7 | #include 8 | 9 | ssize_t process_vm_readv(int pid, struct iovec* mem_local, int liovcnt, struct iovec* mem_remote, int riovcnt, int flags); 10 | 11 | int read_address(lua_State* L); 12 | 13 | int get_base_address(lua_State* L); 14 | 15 | int size_of(lua_State* L); 16 | 17 | #endif /* __MEMORY_H__ */ 18 | -------------------------------------------------------------------------------- /src/shared.c: -------------------------------------------------------------------------------- 1 | #include "shared.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | void getXDGruntimeDir(char* buffer, size_t size) 9 | { 10 | const char* xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); 11 | buffer[0] = '\0'; 12 | if (xdg_runtime_dir) { 13 | strncpy(buffer, xdg_runtime_dir, size - 1); 14 | buffer[size - 1] = '\0'; 15 | return; 16 | } 17 | 18 | const int uid = getuid(); 19 | snprintf(buffer, size, "/run/user/%d", uid); 20 | } 21 | -------------------------------------------------------------------------------- /src/shared.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #define LIBRESPLIT_SOCK_NAME "libresplit.sock" 7 | 8 | void getXDGruntimeDir(char* buffer, size_t size); 9 | 10 | typedef enum : uint8_t { 11 | CTL_CMD_START_SPLIT, 12 | CTL_CMD_STOP_RESET, 13 | CTL_CMD_CANCEL, 14 | CTL_CMD_UNSPLIT, 15 | CTL_CMD_SKIP, 16 | CTL_CMD_EXIT, 17 | } CTLCommand; 18 | 19 | typedef struct __attribute__((__packed__)) CTLMessage { 20 | uint32_t length; // Size of message in bytes 21 | uint8_t message[]; 22 | } CTLMessage; 23 | -------------------------------------------------------------------------------- /.github/workflows/publish-wiki.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [master, main] 4 | paths: 5 | - docs/** 6 | - .github/workflows/publish-wiki.yml 7 | 8 | concurrency: 9 | group: publish-wiki 10 | cancel-in-progress: true 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | publish-wiki: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: Andrew-Chen-Wang/github-wiki-action@v5 20 | with: 21 | path: docs/ 22 | -------------------------------------------------------------------------------- /src/signature.h: -------------------------------------------------------------------------------- 1 | #ifndef __SIGNATURE_H__ 2 | #define __SIGNATURE_H__ 3 | 4 | #include "src/process.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | ProcessMap* get_memory_regions(pid_t pid, int* count); 12 | bool match_pattern(const uint8_t* data, const uint16_t* pattern, size_t pattern_size); 13 | uint16_t* convert_signature(const char* signature, size_t* pattern_size); 14 | bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t size); 15 | int perform_sig_scan(lua_State* L); 16 | 17 | #endif /* __SIGNATURE_H__ */ 18 | -------------------------------------------------------------------------------- /src/auto-splitter.h: -------------------------------------------------------------------------------- 1 | #ifndef __AUTO_SPLITTER_H__ 2 | #define __AUTO_SPLITTER_H__ 3 | 4 | #include 5 | #include 6 | 7 | extern atomic_bool auto_splitter_enabled; 8 | extern atomic_bool auto_splitter_running; 9 | extern atomic_bool call_start; 10 | extern atomic_bool run_started; 11 | extern atomic_bool run_finished; 12 | extern atomic_bool call_split; 13 | extern atomic_bool toggle_loading; 14 | extern atomic_bool call_reset; 15 | extern atomic_bool update_game_time; 16 | extern atomic_llong game_time_value; 17 | extern char auto_splitter_file[PATH_MAX]; 18 | extern int maps_cache_cycles_value; 19 | 20 | void check_directories(); 21 | void run_auto_splitter(); 22 | 23 | #endif /* __AUTO_SPLITTER_H__ */ 24 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | jobs: 4 | format-check: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - name: Checkout repository 9 | uses: actions/checkout@v4 10 | 11 | - name: Format files 12 | run: | 13 | clang-format -i $(find src -name '*.c' -or -name '*.h') 14 | 15 | - name: Check for changes 16 | run: | 17 | changed_files=$(git diff --name-only) 18 | if [ -n "$changed_files" ]; then 19 | echo "The following files require formatting:" 20 | echo "$changed_files" 21 | echo "Run 'make format' and commit the changes." 22 | exit 1 23 | fi 24 | -------------------------------------------------------------------------------- /assets/default_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "libresplit": { 3 | "start_decorated": false, 4 | "start_on_top": true, 5 | "hide_cursor": false, 6 | "auto_splitter_enabled": true, 7 | "global_hotkeys": false, 8 | "theme": "standard", 9 | "theme_variant": "" 10 | }, 11 | "keybinds": { 12 | "start_split": "space", 13 | "stop_reset": "BackSpace", 14 | "cancel": "Delete", 15 | "unsplit": "Page_Up", 16 | "skip_split": "Page_Down", 17 | "toggle_decorations": "Control_R", 18 | "toggle_win_on_top": "k" 19 | }, 20 | "history":{ 21 | "split_file": "", 22 | "last_split_folder": "", 23 | "last_auto_splitter_folder": "", 24 | "auto_splitter_file": "" 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/process.h: -------------------------------------------------------------------------------- 1 | #ifndef __PROCESS_H__ 2 | #define __PROCESS_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | struct game_process { 11 | const char* name; 12 | int pid; 13 | uintptr_t base_address; 14 | uintptr_t dll_address; 15 | }; 16 | typedef struct game_process game_process; 17 | 18 | typedef struct ProcessMap { 19 | uintptr_t start; 20 | uintptr_t end; 21 | uintptr_t size; 22 | char name[PATH_MAX]; 23 | } ProcessMap; 24 | extern uint32_t p_maps_cache_size; 25 | 26 | uintptr_t find_base_address(const char* module); 27 | int process_exists(); 28 | int find_process_id(lua_State* L); 29 | int getPid(lua_State* L); 30 | bool parseMapsLine(const char* line, ProcessMap* map); 31 | int lua_get_module_size(lua_State* L); 32 | 33 | #endif /* __PROCESS_H__ */ 34 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | ## LibreSplit was unable to read memory from the target process. 2 | * This is because in linux, a process cannot read the memory of another process that are unrelated 3 | * This fix should ONLY be used if you REALLY want to run the linux native version of a game with a linux native auto splitter 4 | * To fix this: **Run the game/program trough stam** 5 | * If the above doesnt work for some reason, keep reading 6 | ### THIS WORKAROUND IS A HUGE SECURITY RISK, SO PLEASE ONLY DO IT IF ABSOLUTELY NECESSARY. 7 | #### You should give such permission only to programs you fully trust: A vulnerability in a program with such permission could give full system-wide access to malicious actors 8 | * Run `sudo setcap cap_sys_ptrace+ep /path/to/libresplit` 9 | * Replace `path/to/libresplit` with the actual path of the libresplit binary 10 | * To revert back this capability run: 11 | * `sudo setcap -r /path/to/libresplit` 12 | * Replace `path/to/libresplit` with the actual path of the libresplit binary 13 | -------------------------------------------------------------------------------- /src/component/components.c: -------------------------------------------------------------------------------- 1 | #include "components.h" 2 | 3 | LSComponent* ls_component_title_new(); 4 | LSComponent* ls_component_splits_new(); 5 | LSComponent* ls_component_timer_new(); 6 | LSComponent* ls_component_detailed_timer_new(); 7 | LSComponent* ls_component_prev_segment_new(); 8 | LSComponent* ls_component_best_sum_new(); 9 | LSComponent* ls_component_pb_new(); 10 | LSComponent* ls_component_wr_new(); 11 | 12 | LSComponentAvailable ls_components[] = { 13 | { "title", ls_component_title_new }, 14 | { "splits", ls_component_splits_new }, 15 | // { "timer", ls_component_timer_new }, 16 | { "detailed-timer", ls_component_detailed_timer_new }, 17 | { "prev-segment", ls_component_prev_segment_new }, 18 | { "best-sum", ls_component_best_sum_new }, 19 | { "pb", ls_component_pb_new }, 20 | { "wr", ls_component_wr_new }, 21 | { NULL, NULL } 22 | }; 23 | 24 | void add_class(GtkWidget* widget, const char* class) 25 | { 26 | gtk_style_context_add_class(gtk_widget_get_style_context(widget), class); 27 | } 28 | 29 | void remove_class(GtkWidget* widget, const char* class) 30 | { 31 | gtk_style_context_remove_class(gtk_widget_get_style_context(widget), class); 32 | } 33 | -------------------------------------------------------------------------------- /src/component/components.h: -------------------------------------------------------------------------------- 1 | #ifndef __COMPONENTS_H__ 2 | #define __COMPONENTS_H__ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "../timer.h" 10 | 11 | typedef struct _LSComponent LSComponent; 12 | typedef struct _LSComponentOps LSComponentOps; 13 | typedef struct _LSComponentAvailable LSComponentAvailable; 14 | 15 | struct _LSComponent { 16 | LSComponentOps* ops; 17 | }; 18 | 19 | struct _LSComponentOps { 20 | void (*delete)(LSComponent* self); 21 | GtkWidget* (*widget)(LSComponent* self); 22 | 23 | void (*resize)(LSComponent* self, int win_width, int win_height); 24 | void (*show_game)(LSComponent* self, const ls_game* game, const ls_timer* timer); 25 | void (*clear_game)(LSComponent* self); 26 | void (*draw)(LSComponent* self, const ls_game* game, const ls_timer* timer); 27 | 28 | void (*start_split)(LSComponent* self, const ls_timer* timer); 29 | void (*skip)(LSComponent* self, const ls_timer* timer); 30 | void (*unsplit)(LSComponent* self, const ls_timer* timer); 31 | void (*stop_reset)(LSComponent* self, ls_timer* timer); 32 | void (*cancel_run)(LSComponent* self, ls_timer* timer); 33 | }; 34 | 35 | struct _LSComponentAvailable { 36 | char* name; 37 | LSComponent* (*new)(); 38 | }; 39 | 40 | // A NULL-terminated array of all available components 41 | extern LSComponentAvailable ls_components[]; 42 | 43 | // Utility functions 44 | void add_class(GtkWidget* widget, const char* class); 45 | 46 | void remove_class(GtkWidget* widget, const char* class); 47 | 48 | #endif /* __COMPONENTS_H__ */ 49 | -------------------------------------------------------------------------------- /src/bind.h: -------------------------------------------------------------------------------- 1 | /* bind.h 2 | * Copyright (C) 2008 Alex Graveley 3 | * Copyright (C) 2010 Ulrik Sverdrup 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining 6 | * a copy of this software and associated documentation files (the 7 | * "Software"), to deal in the Software without restriction, including 8 | * without limitation the rights to use, copy, modify, merge, publish, 9 | * distribute, sublicense, and/or sell copies of the Software, and to 10 | * permit persons to whom the Software is furnished to do so, subject to 11 | * the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | #ifndef __BIND_H__ 25 | #define __BIND_H__ 26 | 27 | #include 28 | 29 | G_BEGIN_DECLS 30 | 31 | typedef void (*KeybinderHandler)(const char* keystring, void* user_data); 32 | 33 | void keybinder_init(void); 34 | 35 | gboolean keybinder_bind(const char* keystring, 36 | KeybinderHandler handler, 37 | void* user_data); 38 | 39 | gboolean 40 | keybinder_bind_full(const char* keystring, 41 | KeybinderHandler handler, 42 | void* user_data, 43 | GDestroyNotify notify); 44 | 45 | void keybinder_unbind(const char* keystring, 46 | KeybinderHandler handler); 47 | 48 | void keybinder_unbind_all(const char* keystring); 49 | 50 | guint32 keybinder_get_current_event_time(void); 51 | 52 | G_END_DECLS 53 | 54 | #endif /* __BIND__ */ 55 | -------------------------------------------------------------------------------- /docs/split-files.md: -------------------------------------------------------------------------------- 1 | # Split Files 2 | 3 | Split files are stores as well-formed JSON and **must** contain one main object. 4 | 5 | You can use splits located in [the resource repository](https://github.com/LibreSplit/LibreSplit-resources/tree/main/splits) to start creating your own split files and place them however you want. 6 | 7 | ## Main Object 8 | 9 | | Key | Value | 10 | | --------------- | --------------------------------------- | 11 | | `title` | Title string at top of window | 12 | | `attempt_count` | Number of attempts | 13 | | `start_delay` | Non-negative delay until timer starts | 14 | | `world_record` | Best known time | 15 | | `splits` | Array of [split objects](#split-object) | 16 | | `theme` | Window theme | 17 | | `theme_variant` | Window theme variant | 18 | | `width` | Window width | 19 | | `height` | Window height | 20 | 21 | Most of the above keys are optional. 22 | 23 | ## Split Object 24 | 25 | | Key | Value | 26 | | -------------- | ---------------------- | 27 | | `title` | Split title | 28 | | `icon` | Icon file path or url | 29 | | `time` | Split time | 30 | | `best_time` | Your best split time | 31 | | `best_segment` | Your best segment time | 32 | 33 | Times are strings in `HH:MM:SS.mmmmmm` format. 34 | 35 | Icons can be either a local file path (preferably absolute) or a URL. Note that only GTK-supported image formats will work. For example, `.svg` and `.webp` won't. 36 | 37 | ## Example 38 | 39 | Here is a quick example of how a simple split file would look: 40 | 41 | ```json 42 | { 43 | "title": "School - Homework%", 44 | "attempt_count": 55, 45 | "splits": [ 46 | { 47 | "title": "Maths", 48 | "time": "05:12:55.123456", 49 | "best_time": "05:12:55.123456", 50 | "best_segment": "05:12:55.123456", 51 | }, 52 | { 53 | "title": "Science", 54 | "time": "02:23:35.123456", 55 | "best_time": "01:15:35.789520", 56 | "best_segment": "01:15:35.789520", 57 | } 58 | ], 59 | "width": 250, 60 | "height": 500 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /src/timer.h: -------------------------------------------------------------------------------- 1 | #ifndef __TIMER_H__ 2 | #define __TIMER_H__ 3 | 4 | #define LS_INFO_BEHIND_TIME (1) 5 | #define LS_INFO_LOSING_TIME (2) 6 | #define LS_INFO_BEST_SPLIT (4) 7 | #define LS_INFO_BEST_SEGMENT (8) 8 | 9 | struct ls_game { 10 | char* path; 11 | char* title; 12 | char* theme; 13 | char* theme_variant; 14 | int attempt_count; 15 | int finished_count; 16 | int width; 17 | int height; 18 | long long world_record; 19 | long long start_delay; 20 | char** split_titles; 21 | char** split_icon_paths; // null if no icons 22 | bool contains_icons; 23 | int split_count; 24 | long long* split_times; 25 | long long* segment_times; 26 | long long* best_splits; 27 | long long* best_segments; 28 | }; 29 | typedef struct ls_game ls_game; 30 | 31 | struct ls_timer { 32 | int started; 33 | int running; 34 | int loading; 35 | int curr_split; 36 | long long now; 37 | long long start_time; 38 | long long time; 39 | long long sum_of_bests; 40 | long long world_record; 41 | long long* split_times; 42 | long long* split_deltas; 43 | long long* segment_times; 44 | long long* segment_deltas; 45 | int* split_info; 46 | long long* best_splits; 47 | long long* best_segments; 48 | const ls_game* game; 49 | int* attempt_count; 50 | int* finished_count; 51 | }; 52 | typedef struct ls_timer ls_timer; 53 | 54 | long long ls_time_now(void); 55 | 56 | long long ls_time_value(const char* string); 57 | 58 | void ls_time_string(char* string, long long time); 59 | 60 | void ls_time_millis_string(char* seconds, char* millis, long long time); 61 | 62 | void ls_split_string(char* string, long long time, int compact); 63 | 64 | void ls_delta_string(char* string, long long time); 65 | 66 | int ls_game_create(ls_game** game_ptr, const char* path, char** error_msg); 67 | 68 | void ls_game_update_splits(ls_game* game, const ls_timer* timer); 69 | 70 | void ls_game_update_bests(const ls_game* game, const ls_timer* timer); 71 | 72 | int ls_game_save(const ls_game* game); 73 | 74 | void ls_game_release(const ls_game* game); 75 | 76 | int ls_timer_create(ls_timer** timer_ptr, ls_game* game); 77 | 78 | void ls_timer_release(const ls_timer* timer); 79 | 80 | int ls_timer_start(ls_timer* timer); 81 | 82 | void ls_timer_step(ls_timer* timer, long long now); 83 | 84 | int ls_timer_split(ls_timer* timer); 85 | 86 | int ls_timer_skip(ls_timer* timer); 87 | 88 | int ls_timer_unsplit(ls_timer* timer); 89 | 90 | void ls_timer_stop(ls_timer* timer); 91 | 92 | int ls_timer_reset(ls_timer* timer); 93 | 94 | int ls_timer_cancel(ls_timer* timer); 95 | 96 | #endif /* __TIMER_H__ */ 97 | -------------------------------------------------------------------------------- /src/component/best-sum.c: -------------------------------------------------------------------------------- 1 | #include "components.h" 2 | 3 | typedef struct _LSBestSum { 4 | LSComponent base; 5 | GtkWidget* container; 6 | GtkWidget* sum_of_bests; 7 | } LSBestSum; 8 | extern LSComponentOps ls_best_sum_operations; 9 | 10 | #define SUM_OF_BEST_SEGMENTS "Sum of best segments" 11 | 12 | LSComponent* ls_component_best_sum_new() 13 | { 14 | LSBestSum* self; 15 | GtkWidget* label; 16 | 17 | self = malloc(sizeof(LSBestSum)); 18 | if (!self) { 19 | return NULL; 20 | } 21 | self->base.ops = &ls_best_sum_operations; 22 | 23 | self->container = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 24 | add_class(self->container, "footer"); /* hack */ 25 | add_class(self->container, "sum-of-bests-container"); 26 | gtk_widget_show(self->container); 27 | 28 | label = gtk_label_new(SUM_OF_BEST_SEGMENTS); 29 | add_class(label, "sum-of-bests-label"); 30 | gtk_widget_set_halign(label, GTK_ALIGN_START); 31 | gtk_widget_set_hexpand(label, TRUE); 32 | gtk_container_add(GTK_CONTAINER(self->container), label); 33 | gtk_widget_show(label); 34 | 35 | self->sum_of_bests = gtk_label_new(NULL); 36 | add_class(self->sum_of_bests, "sum-of-bests"); 37 | gtk_widget_set_halign(self->sum_of_bests, GTK_ALIGN_END); 38 | gtk_container_add(GTK_CONTAINER(self->container), self->sum_of_bests); 39 | gtk_widget_show(self->sum_of_bests); 40 | 41 | return (LSComponent*)self; 42 | } 43 | 44 | static void best_sum_delete(LSComponent* self) 45 | { 46 | free(self); 47 | } 48 | 49 | static GtkWidget* best_sum_widget(LSComponent* self) 50 | { 51 | return ((LSBestSum*)self)->container; 52 | } 53 | 54 | static void best_sum_show_game(LSComponent* self_, 55 | const ls_game* game, const ls_timer* timer) 56 | { 57 | LSBestSum* self = (LSBestSum*)self_; 58 | char str[256]; 59 | if (game->split_count && timer->sum_of_bests) { 60 | ls_time_string(str, timer->sum_of_bests); 61 | gtk_label_set_text(GTK_LABEL(self->sum_of_bests), str); 62 | } 63 | } 64 | 65 | static void best_sum_clear_game(LSComponent* self_) 66 | { 67 | LSBestSum* self = (LSBestSum*)self_; 68 | gtk_label_set_text(GTK_LABEL(self->sum_of_bests), ""); 69 | } 70 | 71 | static void best_sum_draw(LSComponent* self_, const ls_game* game, 72 | const ls_timer* timer) 73 | { 74 | LSBestSum* self = (LSBestSum*)self_; 75 | char str[256]; 76 | remove_class(self->sum_of_bests, "time"); 77 | gtk_label_set_text(GTK_LABEL(self->sum_of_bests), "-"); 78 | if (timer->sum_of_bests) { 79 | add_class(self->sum_of_bests, "time"); 80 | ls_time_string(str, timer->sum_of_bests); 81 | gtk_label_set_text(GTK_LABEL(self->sum_of_bests), str); 82 | } 83 | } 84 | 85 | LSComponentOps ls_best_sum_operations = { 86 | .delete = best_sum_delete, 87 | .widget = best_sum_widget, 88 | .show_game = best_sum_show_game, 89 | .clear_game = best_sum_clear_game, 90 | .draw = best_sum_draw 91 | }; 92 | -------------------------------------------------------------------------------- /src/component/wr.c: -------------------------------------------------------------------------------- 1 | #include "components.h" 2 | 3 | typedef struct _LSWr { 4 | LSComponent base; 5 | GtkWidget* container; 6 | GtkWidget* world_record_label; 7 | GtkWidget* world_record; 8 | } LSWr; 9 | extern LSComponentOps ls_wr_operations; 10 | 11 | #define WORLD_RECORD "World record" 12 | 13 | LSComponent* ls_component_wr_new() 14 | { 15 | LSWr* self; 16 | 17 | self = malloc(sizeof(LSWr)); 18 | if (!self) { 19 | return NULL; 20 | } 21 | self->base.ops = &ls_wr_operations; 22 | 23 | self->container = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 24 | add_class(self->container, "footer"); /* hack */ 25 | add_class(self->container, "world-record-container"); 26 | gtk_widget_show(self->container); 27 | 28 | self->world_record_label = gtk_label_new(WORLD_RECORD); 29 | add_class(self->world_record_label, "world-record-label"); 30 | gtk_container_add(GTK_CONTAINER(self->container), self->world_record_label); 31 | 32 | self->world_record = gtk_label_new(NULL); 33 | add_class(self->world_record, "world-record"); 34 | add_class(self->world_record, "time"); 35 | gtk_widget_set_halign(self->world_record, GTK_ALIGN_END); 36 | gtk_container_add(GTK_CONTAINER(self->container), self->world_record); 37 | 38 | return (LSComponent*)self; 39 | } 40 | 41 | static void wr_delete(LSComponent* self) 42 | { 43 | free(self); 44 | } 45 | 46 | static GtkWidget* wr_widget(LSComponent* self) 47 | { 48 | return ((LSWr*)self)->container; 49 | } 50 | 51 | static void wr_show_game(LSComponent* self_, 52 | const ls_game* game, const ls_timer* timer) 53 | { 54 | LSWr* self = (LSWr*)self_; 55 | gtk_widget_set_halign(self->world_record_label, GTK_ALIGN_START); 56 | gtk_widget_set_hexpand(self->world_record_label, TRUE); 57 | if (game->world_record) { 58 | char str[256]; 59 | ls_time_string(str, game->world_record); 60 | gtk_label_set_text(GTK_LABEL(self->world_record), str); 61 | gtk_widget_show(self->world_record); 62 | gtk_widget_show(self->world_record_label); 63 | } 64 | } 65 | 66 | static void wr_clear_game(LSComponent* self_) 67 | { 68 | LSWr* self = (LSWr*)self_; 69 | gtk_widget_hide(self->world_record_label); 70 | gtk_widget_hide(self->world_record); 71 | } 72 | 73 | static void wr_draw(LSComponent* self_, const ls_game* game, 74 | const ls_timer* timer) 75 | { 76 | LSWr* self = (LSWr*)self_; 77 | char str[256]; 78 | if (timer->curr_split == game->split_count 79 | && game->world_record) { 80 | if (timer->split_times[game->split_count - 1] 81 | && timer->split_times[game->split_count - 1] 82 | < game->world_record) { 83 | ls_time_string(str, timer->split_times[game->split_count - 1]); 84 | } else { 85 | ls_time_string(str, game->world_record); 86 | } 87 | gtk_label_set_text(GTK_LABEL(self->world_record), str); 88 | } 89 | } 90 | 91 | LSComponentOps ls_wr_operations = { 92 | .delete = wr_delete, 93 | .widget = wr_widget, 94 | .show_game = wr_show_game, 95 | .clear_game = wr_clear_game, 96 | .draw = wr_draw 97 | }; 98 | -------------------------------------------------------------------------------- /docs/settings-keybinds.md: -------------------------------------------------------------------------------- 1 | # Settings and keybinds 2 | 3 | LibreSplit uses JSON as its way to keep track of your preferences. 4 | 5 | ## The settings file 6 | 7 | Settings are saved in the `settings.json` file inside your `XDG_CONFIG_HOME` folder (it will usually be situated in `~/.config/libresplit/settings.json`). 8 | 9 | The settings file is divided in 3 main parts: 10 | 11 | - **libresplit:** Which contains the general settings; 12 | - **keybinds:** Which contains your key bindings; 13 | - **history:** Which tracks the last splits, auto splitters, split folder and auto splitters folder you opened. 14 | 15 | ### General settings 16 | 17 | Under the `libresplit` section, you will find the following settings: 18 | 19 | | Setting | Type | Description | Default | 20 | | ----------------- | ------- | ---------------------------------- | -------------- | 21 | | `start_decorated` | Boolean | Start with window decorations | `false` | 22 | | `start_on_top` | Boolean | Start with window as always on top | `true` | 23 | | `hide_cursor` | Boolean | Hide cursor in window | `false` | 24 | | `global_hotkeys` | Boolean | Enables global hotkeys | `false` | 25 | | `start_on_top` | Boolean | Start with window as always on top | `false` | 26 | | `theme` | String | Default theme name | `'standard'` | 27 | | `theme_variant` | String | Default theme variant | `''` | 28 | 29 | ### Keybind settings 30 | 31 | Under the `keybind` section, you will find the following key bindings: 32 | 33 | | Keybind | Type | Description | Default | 34 | | ---------------------------- | ------ | ------------------------------------------- | --------------------- | 35 | | `start_split` | String | Start/split keybind | space | 36 | | `stop_reset` | String | Stop/Reset keybind | Backspace | 37 | | `cancel` | String | Cancel keybind | Delete | 38 | | `unsplit` | String | Unsplit keybind | Page_Up | 39 | | `skip_split` | String | Skip split keybind | Page_Down | 40 | | `toggle_decorations` | String | Toggle window decorations keybind | Control_R | 41 | | `toggle_win_on_top` | String | Toggle window "Always on top" state keybind | <Control>k | 42 | 43 | Keybind strings must be parsable by the [gtk_accelerator_parse](https://docs.gtk.org/gtk4/func.accelerator_parse.html). 44 | 45 | See the [complete list of keynames](https://github.com/GNOME/gtk/blob/main/gdk/keynames.txt) for `gdk`. Modifiers are enclosed in angular brackets <>: ``, ``, ``, ``, ``, ``. Note that you should use `a` instead of `-a` or similar. 46 | 47 | ## Modifying the default values 48 | 49 | You can edit the settings by directly changing the `settings.json` file inside of LibreSplit's configuration directory. 50 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | .window { 2 | background: #222; 3 | padding: 0; 4 | margin: 0; 5 | font-family: Roboto, Droid Sans, Open Sans, Ubuntu, Cantarell, Arial, FreeSans, Liberation Sans; 6 | } 7 | .header { 8 | background: linear-gradient(#555,#222); 9 | font-weight: 900; 10 | text-shadow: 0px 2px 1px rgba(0,0,0,0.33); 11 | box-shadow: 0px 2px 6px rgba(0,0,0,0.5); 12 | border-bottom: 1px solid rgba(255,255,255,0.07); 13 | } 14 | .title { 15 | padding-top: 4px; 16 | padding-bottom: 4px; 17 | padding-left: 8px; 18 | font-size: 11pt; 19 | } 20 | .attempt-count { 21 | color: #2196F3; 22 | font-size: 9pt; 23 | padding: 8px; 24 | padding-top: 7px; 25 | } 26 | .time { 27 | color: #fff; 28 | font-weight: 900; 29 | } 30 | .delta { 31 | color: #fff; 32 | } 33 | .timer { 34 | text-shadow: 0px 2px 1px rgba(0,0,0,0.33); 35 | background: linear-gradient(#000,#333); 36 | box-shadow: 0px 2px 6px rgba(0,0,0,0.5); 37 | transition: .25s; 38 | } 39 | .timer-seconds { 40 | font-size: 32pt; 41 | } 42 | .timer-millis { 43 | padding-bottom: 6px; 44 | font-weight: 100; 45 | padding-right: 8px; 46 | } 47 | 48 | .segment-seconds { 49 | font-size: 28px; 50 | } 51 | 52 | .segment-millis { 53 | font-size: 22px; 54 | } 55 | 56 | .delay { 57 | opacity: 0.33; 58 | transition: .25s; 59 | } 60 | .splits, .split-last { 61 | font-weight: 300; 62 | font-size: 10pt; 63 | border: 0; 64 | } 65 | .split { 66 | border-bottom: 1px solid rgba(255,255,255,0.05); 67 | } 68 | .current-split { 69 | background-color: #37474F; 70 | } 71 | .split>.delta, .split-last>.delta { 72 | color: #4CAF50; 73 | } 74 | .split>.behind, .split-last>.behind { 75 | color: #F44336; 76 | } 77 | .split>.best-segment, .split-last>.best-segment { 78 | color: #FFC107; 79 | animation: bestseg-blink; 80 | animation-duration: 2s; 81 | animation-iteration-count: infinite; 82 | animation-direction: alternate; 83 | 84 | } 85 | .split-title { 86 | padding-top: 4px; 87 | padding-left: 8px; 88 | padding-bottom: 4px; 89 | } 90 | .split-time { 91 | padding-right: 8px; 92 | } 93 | .split-last { 94 | border: 0; 95 | opacity: 0.5; 96 | } 97 | .timer { 98 | background: linear-gradient(#4CAF50,#1B5E20); 99 | border: 0; 100 | } 101 | .timer.delay { 102 | background: linear-gradient(#9E9E9E,#444); 103 | } 104 | .timer.behind { 105 | background: linear-gradient(#F44336,#B71C1C); 106 | } 107 | .footer { 108 | font-size: 8pt; 109 | } 110 | .prev-segment-label, .prev-segment { 111 | padding-top: 8px; 112 | } 113 | .prev-segment-label, .sum-of-bests-label, .personal-best-label, .world-record-label { 114 | padding-left: 8px; 115 | } 116 | .prev-segment, .sum-of-bests, .personal-best, .world-record { 117 | padding-right: 8px; 118 | } 119 | .personal-best, .personal-best-label, .world-record, .world-record-label { 120 | padding-bottom: 8px; 121 | } 122 | .footer>.delta { 123 | color: #4CAF50; 124 | } 125 | .footer>.behind { 126 | color: #F44336; 127 | } 128 | .footer>.best-segment, .footer>.best-segment { 129 | color: #FFC107; 130 | } 131 | .world-record { 132 | color: #2196F3; 133 | } 134 | 135 | @keyframes bestseg-blink { 136 | from { 137 | color: #FFC107; 138 | } to { 139 | color: #FFE082; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'libresplit', 3 | 'c', 4 | 'cpp', 5 | default_options: ['c_std=gnu23', 'cpp_std=c++20'], 6 | meson_version: '>=1.1', 7 | ) 8 | 9 | if get_option('buildtype') == 'debug' 10 | add_project_arguments('-DDEBUG', language: 'cpp') 11 | endif 12 | 13 | threads = dependency('threads') 14 | gtk = dependency('gtk+-3.0') 15 | luajit = dependency('luajit') 16 | x11 = dependency('x11') 17 | jansson = dependency('jansson') 18 | 19 | configure_file( 20 | input: 'src/config.h.in', 21 | output: 'config.h', 22 | configuration: { 23 | 'DEFAULT_CONFIG_PATH': get_option('prefix') / get_option('datadir') / meson.project_name() / 'default_settings.json', 24 | }, 25 | ) 26 | 27 | executable( 28 | 'libresplit', 29 | [ 30 | 'src/main.c', 31 | 'src/shared.c', 32 | 'src/server.c', 33 | 'src/auto-splitter.c', 34 | 'src/bind.c', 35 | 'src/memory.c', 36 | 'src/process.c', 37 | 'src/settings.c', 38 | 'src/signature.c', 39 | 'src/timer.c', 40 | 'src/component/best-sum.c', 41 | 'src/component/clock.c', 42 | 'src/component/components.c', 43 | 'src/component/pb.c', 44 | 'src/component/prev-segment.c', 45 | 'src/component/splits.c', 46 | 'src/component/title.c', 47 | 'src/component/wr.c', 48 | 'src/component/detailed-timer.c', 49 | ], 50 | dependencies: [threads, gtk, luajit, x11, jansson], 51 | c_args: [ 52 | '-DPREFIX="' + get_option('prefix') + '"', 53 | '-DDATADIR="' + get_option('datadir') + '"', 54 | '-Werror', 55 | ], 56 | install: true, 57 | ) 58 | 59 | executable( 60 | 'libresplit-ctl', 61 | 'src/ctl.c', 62 | 'src/shared.c', 63 | install: true, 64 | ) 65 | 66 | message('prefix: ' + get_option('prefix')) # /usr/local by default 67 | message('datadir: ' + get_option('datadir')) # share by default 68 | message('buildtype: ' + get_option('buildtype')) 69 | 70 | install_data( 71 | 'assets/libresplit.desktop', 72 | install_dir: get_option('prefix') / get_option('datadir') / 'applications', 73 | ) 74 | install_data( 75 | 'assets/default_settings.json', 76 | install_dir: get_option('prefix') / get_option('datadir') / meson.project_name(), 77 | ) 78 | install_data( 79 | 'README.md', 80 | install_dir: get_option('prefix') / get_option('datadir') / 'doc' / meson.project_name(), 81 | ) 82 | install_data( 83 | 'LICENSE', 84 | install_dir: get_option('prefix') / get_option('datadir') / 'licenses' / meson.project_name(), 85 | ) 86 | 87 | # TODO: better install dir for this to make desktop and gtk icons work on debug builds/uninstalled tests 88 | foreach size : [16, 22, 24, 32, 36, 48, 64, 72, 96, 128, 256, 512, 1024] 89 | install_data( 90 | 'assets/icons/libresplit-@0@.png'.format(size), 91 | install_dir: get_option('prefix') / get_option('datadir') / 'icons/hicolor/@0@x@0@/apps/'.format(size), 92 | rename: 'libresplit.png', 93 | install_mode: 'rw-r--r--', 94 | ) 95 | endforeach 96 | 97 | gnome = import('gnome') 98 | #gnome.post_install(update_desktop_database: true, gtk_update_icon_cache: true) 99 | gnome.post_install(gtk_update_icon_cache: true) 100 | -------------------------------------------------------------------------------- /src/component/pb.c: -------------------------------------------------------------------------------- 1 | #include "components.h" 2 | 3 | typedef struct _LSPb { 4 | LSComponent base; 5 | GtkWidget* container; 6 | GtkWidget* personal_best; 7 | } LSPb; 8 | extern LSComponentOps ls_pb_operations; 9 | 10 | #define PERSONAL_BEST "Personal best" 11 | 12 | LSComponent* ls_component_pb_new() 13 | { 14 | LSPb* self; 15 | GtkWidget* label; 16 | 17 | self = malloc(sizeof(LSPb)); 18 | if (!self) { 19 | return NULL; 20 | } 21 | self->base.ops = &ls_pb_operations; 22 | 23 | self->container = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 24 | add_class(self->container, "footer"); /* hack */ 25 | add_class(self->container, "personal-best-container"); 26 | gtk_widget_show(self->container); 27 | 28 | label = gtk_label_new(PERSONAL_BEST); 29 | add_class(label, "personal-best-label"); 30 | gtk_widget_set_halign(label, GTK_ALIGN_START); 31 | gtk_widget_set_hexpand(label, TRUE); 32 | gtk_container_add(GTK_CONTAINER(self->container), label); 33 | gtk_widget_show(label); 34 | 35 | self->personal_best = gtk_label_new(NULL); 36 | add_class(self->personal_best, "personal-best"); 37 | gtk_widget_set_halign(self->personal_best, GTK_ALIGN_END); 38 | gtk_container_add(GTK_CONTAINER(self->container), self->personal_best); 39 | gtk_widget_show(self->personal_best); 40 | 41 | return (LSComponent*)self; 42 | } 43 | 44 | static void pb_delete(LSComponent* self) 45 | { 46 | free(self); 47 | } 48 | 49 | static GtkWidget* pb_widget(LSComponent* self) 50 | { 51 | return ((LSPb*)self)->container; 52 | } 53 | 54 | static void pb_show_game(LSComponent* self_, 55 | const ls_game* game, const ls_timer* timer) 56 | { 57 | LSPb* self = (LSPb*)self_; 58 | char str[256]; 59 | if (game->split_count && game->split_times[game->split_count - 1]) { 60 | ls_time_string( 61 | str, game->split_times[game->split_count - 1]); 62 | gtk_label_set_text(GTK_LABEL(self->personal_best), str); 63 | } 64 | } 65 | 66 | static void pb_clear_game(LSComponent* self_) 67 | { 68 | LSPb* self = (LSPb*)self_; 69 | gtk_label_set_text(GTK_LABEL(self->personal_best), ""); 70 | } 71 | 72 | static void pb_draw(LSComponent* self_, const ls_game* game, 73 | const ls_timer* timer) 74 | { 75 | LSPb* self = (LSPb*)self_; 76 | char str[256]; 77 | remove_class(self->personal_best, "time"); 78 | gtk_label_set_text(GTK_LABEL(self->personal_best), "-"); 79 | if (timer->curr_split == game->split_count 80 | && timer->split_times[game->split_count - 1] 81 | && (!game->split_times[game->split_count - 1] 82 | || (timer->split_times[game->split_count - 1] 83 | < game->split_times[game->split_count - 1]))) { 84 | add_class(self->personal_best, "time"); 85 | ls_time_string( 86 | str, timer->split_times[game->split_count - 1]); 87 | gtk_label_set_text(GTK_LABEL(self->personal_best), str); 88 | } else if (game->split_times[game->split_count - 1]) { 89 | add_class(self->personal_best, "time"); 90 | ls_time_string( 91 | str, game->split_times[game->split_count - 1]); 92 | gtk_label_set_text(GTK_LABEL(self->personal_best), str); 93 | } 94 | } 95 | 96 | LSComponentOps ls_pb_operations = { 97 | .delete = pb_delete, 98 | .widget = pb_widget, 99 | .show_game = pb_show_game, 100 | .clear_game = pb_clear_game, 101 | .draw = pb_draw 102 | }; 103 | -------------------------------------------------------------------------------- /src/component/title.c: -------------------------------------------------------------------------------- 1 | #include "components.h" 2 | 3 | typedef struct _LSTitle { 4 | LSComponent base; 5 | GtkWidget* header; 6 | GtkWidget* title; 7 | GtkWidget* attempt_count; 8 | GtkWidget* finished_count; 9 | } LSTitle; 10 | extern LSComponentOps ls_title_operations; // defined at the end of the file 11 | 12 | LSComponent* ls_component_title_new() 13 | { 14 | LSTitle* self; 15 | 16 | self = malloc(sizeof(LSTitle)); 17 | if (!self) { 18 | return NULL; 19 | } 20 | self->base.ops = &ls_title_operations; 21 | 22 | self->header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 23 | add_class(self->header, "header"); 24 | gtk_widget_show(self->header); 25 | 26 | self->title = gtk_label_new(NULL); 27 | add_class(self->title, "title"); 28 | gtk_label_set_justify(GTK_LABEL(self->title), GTK_JUSTIFY_CENTER); 29 | gtk_label_set_line_wrap(GTK_LABEL(self->title), TRUE); 30 | gtk_widget_set_hexpand(self->title, TRUE); 31 | gtk_container_add(GTK_CONTAINER(self->header), self->title); 32 | 33 | self->attempt_count = gtk_label_new(NULL); 34 | add_class(self->attempt_count, "attempt-count"); 35 | gtk_widget_set_margin_start(self->attempt_count, 8); 36 | gtk_widget_set_valign(self->attempt_count, GTK_ALIGN_START); 37 | gtk_container_add(GTK_CONTAINER(self->header), self->attempt_count); 38 | gtk_widget_show(self->attempt_count); 39 | 40 | self->finished_count = gtk_label_new(NULL); 41 | add_class(self->finished_count, "finished_count"); 42 | gtk_widget_set_margin_start(self->finished_count, 8); 43 | gtk_widget_set_valign(self->finished_count, GTK_ALIGN_START); 44 | gtk_container_add(GTK_CONTAINER(self->header), self->finished_count); 45 | gtk_widget_show(self->finished_count); 46 | 47 | return (LSComponent*)self; 48 | } 49 | 50 | static void title_delete(LSComponent* self) 51 | { 52 | free(self); 53 | } 54 | 55 | static GtkWidget* title_widget(LSComponent* self) 56 | { 57 | return ((LSTitle*)self)->header; 58 | } 59 | 60 | static void title_resize(LSComponent* self_, int win_width, int win_height) 61 | { 62 | GdkRectangle rect; 63 | int attempt_count_width; 64 | int finished_count_width; 65 | int title_width; 66 | LSTitle* self = (LSTitle*)self_; 67 | 68 | gtk_widget_hide(self->title); 69 | gtk_widget_get_allocation(self->attempt_count, &rect); 70 | attempt_count_width = rect.width; 71 | gtk_widget_get_allocation(self->finished_count, &rect); 72 | finished_count_width = rect.width; 73 | title_width = win_width - (attempt_count_width + finished_count_width); 74 | rect.width = title_width; 75 | gtk_widget_show(self->title); 76 | gtk_widget_set_allocation(self->title, &rect); 77 | } 78 | 79 | static void title_show_game(LSComponent* self_, const ls_game* game, 80 | const ls_timer* timer) 81 | { 82 | char str[64]; 83 | LSTitle* self = (LSTitle*)self_; 84 | gtk_label_set_text(GTK_LABEL(self->title), game->title); 85 | sprintf(str, "#%d", game->attempt_count); 86 | gtk_label_set_text(GTK_LABEL(self->attempt_count), str); 87 | } 88 | 89 | static void title_draw(LSComponent* self_, const ls_game* game, const ls_timer* timer) 90 | { 91 | char attempt_str[64]; 92 | char finished_str[64]; 93 | char combi_str[64]; 94 | LSTitle* self = (LSTitle*)self_; 95 | sprintf(attempt_str, "%d", game->attempt_count); 96 | sprintf(finished_str, "#%d", game->finished_count); 97 | strcpy(combi_str, finished_str); 98 | strcat(combi_str, "/"); 99 | strcat(combi_str, attempt_str); 100 | gtk_label_set_text(GTK_LABEL(self->attempt_count), combi_str); 101 | } 102 | 103 | LSComponentOps ls_title_operations = { 104 | .delete = title_delete, 105 | .widget = title_widget, 106 | .resize = title_resize, 107 | .show_game = title_show_game, 108 | .draw = title_draw 109 | }; 110 | -------------------------------------------------------------------------------- /src/ctl.c: -------------------------------------------------------------------------------- 1 | #include "shared.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | void print_help() 14 | { 15 | printf("Available commands:\n"); 16 | printf(" startorsplit - Start the timer/Split if timer is running\n"); 17 | printf(" stoporreset - Stop the timer/Reset the timer if its stopped\n"); 18 | printf(" cancel - Cancel the run\n"); 19 | printf(" unsplit - Unsplit the timer\n"); 20 | printf(" skipsplit - Skip the current split\n"); 21 | printf(" exit - Closes LibreSplit\n"); 22 | printf(" help - Show this help message\n"); 23 | } 24 | 25 | bool sendToLibreSplit(const CTLCommand cmd) 26 | { 27 | char runtime_dir[PATH_MAX - 17]; 28 | getXDGruntimeDir(runtime_dir, sizeof(runtime_dir)); 29 | if (strlen(runtime_dir) == 0) { 30 | fprintf(stderr, "Failed to get LibreSplit socket path.\n"); 31 | return false; 32 | } 33 | 34 | char socket_path[PATH_MAX]; 35 | snprintf(socket_path, PATH_MAX, "%s/%s", runtime_dir, LIBRESPLIT_SOCK_NAME); 36 | 37 | int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); 38 | if (sockfd == -1) { 39 | perror("Failed to create socket"); 40 | return false; 41 | } 42 | 43 | struct sockaddr_un addr = { 0 }; 44 | memset(&addr, 0, sizeof(struct sockaddr_un)); 45 | addr.sun_family = AF_UNIX; 46 | strcpy(addr.sun_path, socket_path); 47 | 48 | if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { 49 | perror("Failed to connect to LibreSplit socket"); 50 | close(sockfd); 51 | return false; 52 | } 53 | 54 | // We can send arbitrarily long messages, but for now we only need to send a single byte 55 | // Could be useful for future extensions though 56 | CTLMessage* ctl_msg = (CTLMessage*)malloc(sizeof(CTLMessage) + sizeof(cmd)); 57 | ctl_msg->length = htonl(sizeof(cmd)); 58 | memcpy(ctl_msg->message, &cmd, sizeof(cmd)); 59 | 60 | int written = write(sockfd, ctl_msg, sizeof(CTLMessage) + sizeof(cmd)); 61 | if (written != sizeof(CTLMessage) + sizeof(cmd)) { 62 | fprintf(stderr, "Failed to send command to LibreSplit.\n"); 63 | close(sockfd); 64 | free(ctl_msg); 65 | return false; 66 | } 67 | free(ctl_msg); 68 | close(sockfd); 69 | 70 | return true; 71 | } 72 | 73 | int main(int argc, char* argv[]) 74 | { 75 | if (argc != 2) { 76 | fprintf(stderr, "Error: This program accepts exactly 1 argument.\n"); 77 | fprintf(stderr, "Try 'help' for a list of commands.\n"); 78 | return 1; 79 | } 80 | 81 | const char* cmd = argv[1]; 82 | 83 | if (strcmp(cmd, "help") == 0) { 84 | print_help(); 85 | return 0; 86 | } 87 | 88 | bool success = false; 89 | 90 | if (strcmp(cmd, "startorsplit") == 0) { 91 | success = sendToLibreSplit(CTL_CMD_START_SPLIT); 92 | } else if (strcmp(cmd, "stoporreset") == 0) { 93 | success = sendToLibreSplit(CTL_CMD_STOP_RESET); 94 | } else if (strcmp(cmd, "cancel") == 0) { 95 | success = sendToLibreSplit(CTL_CMD_CANCEL); 96 | } else if (strcmp(cmd, "unsplit") == 0) { 97 | success = sendToLibreSplit(CTL_CMD_UNSPLIT); 98 | } else if (strcmp(cmd, "skipsplit") == 0) { 99 | success = sendToLibreSplit(CTL_CMD_SKIP); 100 | } else if (strcmp(cmd, "exit") == 0) { 101 | success = sendToLibreSplit(CTL_CMD_EXIT); 102 | } else { 103 | fprintf(stderr, "Unknown command: %s\n", cmd); 104 | fprintf(stderr, "Try 'help' for a list of valid commands.\n"); 105 | return 1; 106 | } 107 | 108 | if (!success) { 109 | return 1; 110 | } 111 | 112 | return 0; 113 | } -------------------------------------------------------------------------------- /src/component/clock.c: -------------------------------------------------------------------------------- 1 | #include "components.h" 2 | 3 | typedef struct _LSTimer { 4 | LSComponent base; 5 | GtkWidget* time; 6 | GtkWidget* time_seconds; 7 | GtkWidget* time_millis; 8 | } LSTimer; 9 | extern LSComponentOps ls_timer_operations; 10 | 11 | LSComponent* ls_component_timer_new() 12 | { 13 | LSTimer* self; 14 | GtkWidget* spacer; 15 | 16 | self = malloc(sizeof(LSTimer)); 17 | if (!self) { 18 | return NULL; 19 | } 20 | self->base.ops = &ls_timer_operations; 21 | 22 | self->time = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 23 | add_class(self->time, "timer"); 24 | add_class(self->time, "time"); 25 | gtk_widget_show(self->time); 26 | 27 | spacer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 28 | gtk_widget_set_hexpand(spacer, TRUE); 29 | gtk_container_add(GTK_CONTAINER(self->time), spacer); 30 | gtk_widget_show(spacer); 31 | 32 | self->time_seconds = gtk_label_new(NULL); 33 | add_class(self->time_seconds, "timer-seconds"); 34 | gtk_widget_set_valign(self->time_seconds, GTK_ALIGN_BASELINE); 35 | gtk_container_add(GTK_CONTAINER(self->time), self->time_seconds); 36 | gtk_widget_show(self->time_seconds); 37 | 38 | spacer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 39 | gtk_widget_set_valign(spacer, GTK_ALIGN_END); 40 | gtk_container_add(GTK_CONTAINER(self->time), spacer); 41 | gtk_widget_show(spacer); 42 | 43 | self->time_millis = gtk_label_new(NULL); 44 | add_class(self->time_millis, "timer-millis"); 45 | gtk_widget_set_valign(self->time_millis, GTK_ALIGN_BASELINE); 46 | gtk_container_add(GTK_CONTAINER(spacer), self->time_millis); 47 | gtk_widget_show(self->time_millis); 48 | 49 | return (LSComponent*)self; 50 | } 51 | 52 | // Avoid collision with timer_delete of time.h 53 | static void ls_timer_delete(LSComponent* self) 54 | { 55 | free(self); 56 | } 57 | 58 | static GtkWidget* timer_widget(LSComponent* self) 59 | { 60 | return ((LSTimer*)self)->time; 61 | } 62 | 63 | static void timer_clear_game(LSComponent* self_) 64 | { 65 | LSTimer* self = (LSTimer*)self_; 66 | gtk_label_set_text(GTK_LABEL(self->time_seconds), ""); 67 | gtk_label_set_text(GTK_LABEL(self->time_millis), ""); 68 | remove_class(self->time, "behind"); 69 | remove_class(self->time, "losing"); 70 | } 71 | 72 | static void timer_draw(LSComponent* self_, const ls_game* game, const ls_timer* timer) 73 | { 74 | LSTimer* self = (LSTimer*)self_; 75 | char str[256], millis[256]; 76 | int curr; 77 | 78 | curr = timer->curr_split; 79 | if (curr == game->split_count) { 80 | --curr; 81 | } 82 | 83 | remove_class(self->time, "delay"); 84 | remove_class(self->time, "behind"); 85 | remove_class(self->time, "losing"); 86 | remove_class(self->time, "best-split"); 87 | 88 | if (curr == game->split_count) { 89 | curr = game->split_count - 1; 90 | } 91 | if (timer->time <= 0) { 92 | add_class(self->time, "delay"); 93 | } else { 94 | if (timer->curr_split == game->split_count 95 | && timer->split_info[curr] 96 | & LS_INFO_BEST_SPLIT) { 97 | add_class(self->time, "best-split"); 98 | } else { 99 | if (timer->split_info[curr] 100 | & LS_INFO_BEHIND_TIME) { 101 | add_class(self->time, "behind"); 102 | } 103 | if (timer->split_info[curr] 104 | & LS_INFO_LOSING_TIME) { 105 | add_class(self->time, "losing"); 106 | } 107 | } 108 | } 109 | ls_time_millis_string(str, &millis[1], timer->time); 110 | millis[0] = '.'; 111 | gtk_label_set_text(GTK_LABEL(self->time_seconds), str); 112 | gtk_label_set_text(GTK_LABEL(self->time_millis), millis); 113 | } 114 | 115 | LSComponentOps ls_timer_operations = { 116 | .delete = ls_timer_delete, 117 | .widget = timer_widget, 118 | .clear_game = timer_clear_game, 119 | .draw = timer_draw 120 | }; 121 | -------------------------------------------------------------------------------- /src/component/prev-segment.c: -------------------------------------------------------------------------------- 1 | #include "components.h" 2 | 3 | typedef struct _LSPrevSegment { 4 | LSComponent base; 5 | GtkWidget* container; 6 | GtkWidget* previous_segment_label; 7 | GtkWidget* previous_segment; 8 | } LSPrevSegment; 9 | extern LSComponentOps ls_prev_segment_operations; 10 | 11 | #define PREVIOUS_SEGMENT "Previous segment" 12 | #define LIVE_SEGMENT "Live segment" 13 | 14 | LSComponent* ls_component_prev_segment_new() 15 | { 16 | LSPrevSegment* self; 17 | 18 | self = malloc(sizeof(LSPrevSegment)); 19 | if (!self) { 20 | return NULL; 21 | } 22 | self->base.ops = &ls_prev_segment_operations; 23 | 24 | self->container = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 25 | add_class(self->container, "footer"); 26 | add_class(self->container, "prev-segment-container"); 27 | gtk_widget_show(self->container); 28 | 29 | self->previous_segment_label = gtk_label_new(PREVIOUS_SEGMENT); 30 | add_class(self->previous_segment_label, "prev-segment-label"); 31 | gtk_widget_set_halign(self->previous_segment_label, 32 | GTK_ALIGN_START); 33 | gtk_widget_set_hexpand(self->previous_segment_label, TRUE); 34 | gtk_container_add(GTK_CONTAINER(self->container), 35 | self->previous_segment_label); 36 | gtk_widget_show(self->previous_segment_label); 37 | 38 | self->previous_segment = gtk_label_new(NULL); 39 | add_class(self->previous_segment, "prev-segment"); 40 | gtk_widget_set_halign(self->previous_segment, GTK_ALIGN_END); 41 | gtk_container_add(GTK_CONTAINER(self->container), self->previous_segment); 42 | gtk_widget_show(self->previous_segment); 43 | 44 | return (LSComponent*)self; 45 | } 46 | 47 | static void prev_segment_delete(LSComponent* self) 48 | { 49 | free(self); 50 | } 51 | 52 | static GtkWidget* prev_segment_widget(LSComponent* self) 53 | { 54 | return ((LSPrevSegment*)self)->container; 55 | } 56 | 57 | static void prev_segment_show_game(LSComponent* self_, 58 | const ls_game* game, const ls_timer* timer) 59 | { 60 | LSPrevSegment* self = (LSPrevSegment*)self_; 61 | remove_class(self->previous_segment, "behind"); 62 | remove_class(self->previous_segment, "losing"); 63 | remove_class(self->previous_segment, "best-segment"); 64 | } 65 | 66 | static void prev_segment_clear_game(LSComponent* self_) 67 | { 68 | LSPrevSegment* self = (LSPrevSegment*)self_; 69 | gtk_label_set_text(GTK_LABEL(self->previous_segment_label), 70 | PREVIOUS_SEGMENT); 71 | gtk_label_set_text(GTK_LABEL(self->previous_segment), ""); 72 | } 73 | 74 | static void prev_segment_draw(LSComponent* self_, const ls_game* game, 75 | const ls_timer* timer) 76 | { 77 | LSPrevSegment* self = (LSPrevSegment*)self_; 78 | const char* label; 79 | char str[256]; 80 | int prev, curr = timer->curr_split; 81 | if (curr == game->split_count) { 82 | --curr; 83 | } 84 | 85 | remove_class(self->previous_segment, "best-segment"); 86 | remove_class(self->previous_segment, "behind"); 87 | remove_class(self->previous_segment, "losing"); 88 | remove_class(self->previous_segment, "delta"); 89 | gtk_label_set_text(GTK_LABEL(self->previous_segment), "-"); 90 | 91 | label = PREVIOUS_SEGMENT; 92 | if (timer->segment_deltas[curr] > 0) { 93 | // Live segment 94 | label = LIVE_SEGMENT; 95 | remove_class(self->previous_segment, "best-segment"); 96 | add_class(self->previous_segment, "behind"); 97 | add_class(self->previous_segment, "losing"); 98 | add_class(self->previous_segment, "delta"); 99 | ls_delta_string(str, timer->segment_deltas[curr]); 100 | gtk_label_set_text(GTK_LABEL(self->previous_segment), str); 101 | } else if (curr) { 102 | prev = timer->curr_split - 1; 103 | // Previous segment 104 | if (timer->curr_split) { 105 | prev = timer->curr_split - 1; 106 | if (timer->segment_deltas[prev]) { 107 | if (timer->split_info[prev] 108 | & LS_INFO_BEST_SEGMENT) { 109 | add_class(self->previous_segment, "best-segment"); 110 | } else if (timer->segment_deltas[prev] > 0) { 111 | add_class(self->previous_segment, "behind"); 112 | add_class(self->previous_segment, "losing"); 113 | } 114 | add_class(self->previous_segment, "delta"); 115 | ls_delta_string(str, timer->segment_deltas[prev]); 116 | gtk_label_set_text(GTK_LABEL(self->previous_segment), str); 117 | } 118 | } 119 | } 120 | gtk_label_set_text(GTK_LABEL(self->previous_segment_label), label); 121 | } 122 | 123 | LSComponentOps ls_prev_segment_operations = { 124 | .delete = prev_segment_delete, 125 | .widget = prev_segment_widget, 126 | .show_game = prev_segment_show_game, 127 | .clear_game = prev_segment_clear_game, 128 | .draw = prev_segment_draw 129 | }; 130 | -------------------------------------------------------------------------------- /src/server.c: -------------------------------------------------------------------------------- 1 | #include "shared.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | extern atomic_bool exit_requested; 15 | 16 | // Structure to pass command data to main thread 17 | typedef struct { 18 | CTLCommand command; 19 | } CommandData; 20 | 21 | // External functions from main.c to handle commands 22 | extern void handle_ctl_command(CTLCommand command); 23 | 24 | // Command execution function that runs on the main thread 25 | static gboolean execute_command_on_main_thread(gpointer data) 26 | { 27 | CommandData* cmd_data = (CommandData*)data; 28 | 29 | // Call the main.c function to handle the command 30 | handle_ctl_command(cmd_data->command); 31 | 32 | g_free(cmd_data); 33 | return FALSE; // Remove from idle queue 34 | } 35 | 36 | // Forward declarations for command handlers 37 | 38 | int receive_message(int sockfd, CTLMessage** out) 39 | { 40 | 41 | // Read header (blocking) 42 | uint32_t len = 0; 43 | ssize_t n = read(sockfd, &len, sizeof(len)); 44 | if (n == 0) 45 | return 1; // client closed connection immediately 46 | len = ntohl(len); 47 | if (n != sizeof(len)) 48 | return -1; 49 | 50 | CTLMessage* msg = malloc(sizeof(CTLMessage) + len); 51 | msg->length = len; 52 | 53 | size_t received = 0; 54 | while (received < len) { 55 | n = read(sockfd, msg->message + received, len - received); 56 | if (n <= 0) { 57 | free(msg); 58 | return -1; 59 | } 60 | received += n; 61 | } 62 | 63 | *out = msg; 64 | return 0; 65 | } 66 | 67 | void* ls_ctl_server(void* arg) 68 | { 69 | char runtime_dir[PATH_MAX - 17]; 70 | getXDGruntimeDir(runtime_dir, sizeof(runtime_dir)); 71 | if (strlen(runtime_dir) == 0) { 72 | fprintf(stderr, "Failed to get LibreSplit socket path.\n"); 73 | return 0; 74 | } 75 | 76 | char socket_path[PATH_MAX]; 77 | snprintf(socket_path, PATH_MAX, "%s/%s", runtime_dir, LIBRESPLIT_SOCK_NAME); 78 | 79 | int server_fd = socket(AF_UNIX, SOCK_STREAM, 0); 80 | if (server_fd < 0) { 81 | perror("Failed to create socket"); 82 | return 0; 83 | } 84 | 85 | struct sockaddr_un addr; 86 | memset(&addr, 0, sizeof(addr)); 87 | addr.sun_family = AF_UNIX; 88 | strcpy(addr.sun_path, socket_path); 89 | 90 | unlink(socket_path); 91 | 92 | if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { 93 | perror("Failed to bind socket"); 94 | close(server_fd); 95 | return 0; 96 | } 97 | 98 | if (listen(server_fd, 20) < 0) { // backlog for many short connections 99 | perror("Failed to listen on socket"); 100 | close(server_fd); 101 | return 0; 102 | } 103 | 104 | while (1) { 105 | fd_set fds; 106 | FD_ZERO(&fds); 107 | FD_SET(server_fd, &fds); 108 | 109 | struct timeval tv; 110 | tv.tv_sec = 0; 111 | tv.tv_usec = 50 * 1000; // 50ms 112 | 113 | int ret = select(server_fd + 1, &fds, NULL, NULL, &tv); 114 | 115 | if (ret < 0) { 116 | perror("select failed"); 117 | continue; 118 | } 119 | 120 | if (ret == 0) { // Timeout 121 | if (atomic_load(&exit_requested)) { 122 | break; 123 | } 124 | } 125 | 126 | // If we reach here, the listening socket is readable 127 | if (FD_ISSET(server_fd, &fds)) { 128 | 129 | int client_fd = accept(server_fd, NULL, NULL); 130 | if (client_fd < 0) { 131 | perror("Failed to accept client connection"); 132 | continue; // do not stop the server on accept errors 133 | } 134 | 135 | CTLMessage* msg = NULL; 136 | int result = receive_message(client_fd, &msg); 137 | 138 | if (result == 0) { 139 | if (msg->length == sizeof(CTLCommand)) { 140 | CTLCommand command = *(CTLCommand*)msg->message; 141 | CommandData* cmd_data = g_malloc(sizeof(CommandData)); 142 | cmd_data->command = command; 143 | 144 | // Queue command execution on main thread 145 | g_idle_add(execute_command_on_main_thread, cmd_data); 146 | } else { 147 | printf("Invalid message length: %u (expected %zu)\n", msg->length, sizeof(CTLCommand)); 148 | } 149 | 150 | free(msg); 151 | } else { 152 | printf("Client closed without sending a full message.\n"); 153 | } 154 | 155 | close(client_fd); 156 | } 157 | } 158 | 159 | close(server_fd); 160 | unlink(socket_path); 161 | 162 | return 0; 163 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | jobs: 4 | tests: 5 | runs-on: ubuntu-24.04 6 | name: Build 7 | steps: 8 | - name: "Checkout" 9 | uses: actions/checkout@v4 10 | with: 11 | fetch-depth: 0 12 | 13 | - name: "Install dependencies" 14 | run: | 15 | sudo add-apt-repository ppa:puni070/gcc-noble -y 16 | sudo apt -y update 17 | sudo apt -y install gcc-15 libgtk-3-dev libx11-dev libjansson-dev libluajit-5.1-dev 18 | sudo pip3 install meson 19 | sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 150 20 | sudo update-alternatives --set gcc /usr/bin/gcc-15 21 | 22 | - name: "Configure" 23 | run: meson setup build -Dbuildtype=release -Dprefix=/usr 24 | 25 | - name: "Build" 26 | run: meson compile -C build 27 | 28 | - name: Set Build number 29 | shell: bash 30 | run: echo "build_number=$(git rev-list HEAD --count)" >> $GITHUB_ENV 31 | 32 | - name: Compute git short sha 33 | shell: bash 34 | run: echo "git_short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 35 | 36 | - name: "Bundle extra files" 37 | run: | 38 | mkdir release 39 | cp -r assets release/ 40 | cp -r build/libresplit release/ 41 | cp -r build/libresplit-ctl release/ 42 | cp README.md release/ 43 | cp LICENSE release/ 44 | 45 | - name: "Upload artifacts" 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: LibreSplit-${{ env.build_number }}-${{ env.git_short_sha }} 49 | path: release/ 50 | 51 | appimage: 52 | runs-on: ubuntu-24.04 53 | name: Build AppImage 54 | needs: tests 55 | steps: 56 | - name: "Checkout" 57 | uses: actions/checkout@v4 58 | with: 59 | fetch-depth: 0 60 | 61 | - name: "Download build artifacts" 62 | uses: actions/download-artifact@v4 63 | with: 64 | pattern: LibreSplit-* 65 | merge-multiple: true 66 | 67 | - name: "Install AppImage dependencies" 68 | run: | 69 | sudo apt -y update 70 | sudo apt -y install desktop-file-utils libluajit-5.1-dev 71 | 72 | - name: Set Build number 73 | shell: bash 74 | run: echo "build_number=$(git rev-list HEAD --count)" >> $GITHUB_ENV 75 | 76 | - name: Compute git short sha 77 | shell: bash 78 | run: echo "git_short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 79 | 80 | - name: "Setup AppDir structure" 81 | run: | 82 | mkdir -p AppDir/usr/bin 83 | mkdir -p AppDir/usr/share/applications 84 | mkdir -p AppDir/usr/share/libresplit 85 | mkdir -p AppDir/usr/share/pixmaps 86 | mkdir -p AppDir/usr/share/doc/libresplit 87 | mkdir -p AppDir/usr/share/licenses/libresplit 88 | 89 | cp libresplit AppDir/usr/bin/ 90 | cp libresplit-ctl AppDir/usr/bin/ 91 | chmod +x AppDir/usr/bin/libresplit 92 | chmod +x AppDir/usr/bin/libresplit-ctl 93 | 94 | cp assets/default_settings.json AppDir/usr/share/libresplit/ 95 | cp assets/libresplit.desktop AppDir/usr/share/applications/ 96 | 97 | cp assets/icons/libresplit-256.png AppDir/usr/share/pixmaps/libresplit.png 98 | 99 | cp LICENSE AppDir/usr/share/licenses/libresplit/ 100 | 101 | # Create AppImage-compatible desktop entry at root 102 | cp assets/libresplit.desktop AppDir/ 103 | sed -i 's/^Exec=.*/Exec=AppRun/' AppDir/libresplit.desktop 104 | cp assets/appimage.sh AppDir/AppRun 105 | chmod +x AppDir/AppRun 106 | cp assets/icons/libresplit-256.png AppDir/libresplit.png 107 | 108 | - name: "Download linuxdeploy" 109 | run: | 110 | wget -O linuxdeploy-x86_64.AppImage https://github.com/linuxdeploy/linuxdeploy/releases/latest/download/linuxdeploy-x86_64.AppImage 111 | chmod +x linuxdeploy-x86_64.AppImage 112 | 113 | - name: "Create AppImage" 114 | run: | 115 | ./linuxdeploy-x86_64.AppImage --appdir AppDir --desktop-file AppDir/libresplit.desktop --icon-file AppDir/libresplit.png --output appimage 116 | 117 | - name: "Upload AppImage" 118 | uses: actions/upload-artifact@v4 119 | with: 120 | path: LibreSplit-x86_64.AppImage 121 | name: LibreSplit-${{ env.build_number }}-${{ env.git_short_sha }}-x86_64.AppImage 122 | -------------------------------------------------------------------------------- /src/settings.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include "settings.h" 13 | 14 | /** 15 | * Gets the default config path, considering APPDIR if set. 16 | * 17 | * @param out_path A buffer to store the resulting path, which may be prepended with APPDIR if set for AppImages 18 | */ 19 | void get_default_config_path(char* out_path) 20 | { 21 | out_path[0] = '\0'; 22 | if (getenv("APPDIR")) { 23 | strcat(out_path, getenv("APPDIR")); 24 | } 25 | strcat(out_path, DEFAULT_CONFIG_PATH); 26 | } 27 | 28 | void copy_default_config(const char* dest_path) 29 | { 30 | char default_config_path[PATH_MAX]; 31 | get_default_config_path(default_config_path); 32 | FILE* src = fopen(default_config_path, "r"); 33 | FILE* dest = fopen(dest_path, "w"); 34 | 35 | if (!src || !dest) { 36 | perror("Failed to open source or destination config file for copying"); 37 | // Only one of the two files might have failed to open 38 | if (src != NULL) { 39 | fclose(src); 40 | } 41 | if (dest != NULL) { 42 | fclose(dest); 43 | } 44 | return; 45 | } 46 | 47 | char ch; 48 | while ((ch = fgetc(src)) != EOF) { 49 | fputc(ch, dest); 50 | } 51 | 52 | fclose(src); 53 | fclose(dest); 54 | } 55 | 56 | void get_libresplit_folder_path(char* out_path) 57 | { 58 | struct passwd* pw = getpwuid(getuid()); 59 | char* XDG_CONFIG_HOME = getenv("XDG_CONFIG_HOME"); 60 | char* base_dir = strcat(pw->pw_dir, "/.config/libresplit"); 61 | if (XDG_CONFIG_HOME != NULL) { 62 | char config_dir[PATH_MAX] = { 0 }; 63 | strcpy(config_dir, XDG_CONFIG_HOME); 64 | strcat(config_dir, "/libresplit"); 65 | strcpy(base_dir, config_dir); 66 | } 67 | strcpy(out_path, base_dir); 68 | } 69 | 70 | void ls_update_setting(const char* section, const char* setting, json_t* value) 71 | { 72 | char settings_path[PATH_MAX]; 73 | get_libresplit_folder_path(settings_path); 74 | strcat(settings_path, "/settings.json"); 75 | 76 | // Load existing settings 77 | json_t* root = NULL; 78 | FILE* file = fopen(settings_path, "r"); 79 | if (file) { 80 | json_error_t error; 81 | root = json_loadf(file, 0, &error); 82 | fclose(file); 83 | if (!root) { 84 | printf("Failed to load settings: %s\n", error.text); 85 | return; 86 | } 87 | } else { 88 | // If file doesn't exist, create a new settings object 89 | root = json_object(); 90 | } 91 | 92 | // Update specific setting 93 | json_t* ls_obj = json_object_get(root, section); 94 | if (!ls_obj) { 95 | ls_obj = json_object(); 96 | json_object_set(root, section, ls_obj); 97 | } 98 | json_object_set_new(ls_obj, setting, value); 99 | 100 | // Save updated settings back to the file 101 | FILE* output_file = fopen(settings_path, "w"); 102 | if (output_file) { 103 | json_dumpf(root, output_file, JSON_INDENT(4)); 104 | fclose(output_file); 105 | } else { 106 | printf("Failed to save settings to %s\n", settings_path); 107 | } 108 | 109 | json_decref(root); 110 | } 111 | 112 | json_t* _load_from_json(const char* settings_path) 113 | { 114 | FILE* file = fopen(settings_path, "r"); 115 | if (file) { 116 | json_error_t error; 117 | json_t* root = json_loadf(file, 0, &error); 118 | fclose(file); 119 | if (!root) { 120 | printf("Failed to load settings from path %s: %s\n", settings_path, error.text); 121 | return NULL; 122 | } 123 | return root; 124 | } else { 125 | printf("Failed to open settings file: %s\n", settings_path); 126 | return NULL; 127 | } 128 | } 129 | 130 | json_t* _load_default_settings() 131 | { 132 | 133 | char default_config_path[PATH_MAX]; 134 | get_default_config_path(default_config_path); 135 | return _load_from_json(default_config_path); 136 | } 137 | 138 | json_t* _load_user_settings() 139 | { 140 | char settings_path[PATH_MAX]; 141 | get_libresplit_folder_path(settings_path); 142 | strcat(settings_path, "/settings.json"); 143 | 144 | struct stat st = { 0 }; 145 | 146 | if (stat(settings_path, &st) == -1) { 147 | printf("Cannot find user settings file, copying default one\n"); 148 | copy_default_config(settings_path); 149 | } 150 | 151 | return _load_from_json(settings_path); 152 | } 153 | 154 | json_t* load_settings() 155 | { 156 | json_t* settings = _load_default_settings(); 157 | json_t* user_settings = _load_user_settings(); 158 | // If there are no user settings, load only the defaults 159 | if (user_settings == NULL) { 160 | return settings ? settings : NULL; 161 | } 162 | // If, for some reason, the defaults cannot be loaded, load the user settings only 163 | if (settings == NULL) { 164 | return user_settings ? user_settings : NULL; 165 | } 166 | // If both can be loaded, overlay settings with user_settings, to allow 167 | // for further updates down the line during development 168 | int merged = json_object_update_recursive(settings, user_settings); 169 | if (merged == 0) { 170 | // Merge successful, return the updated settings 171 | return settings; 172 | } 173 | printf("Failed to merge settings"); 174 | // If the merge fails, return nothing 175 | return NULL; 176 | } 177 | 178 | json_t* get_setting_value(const char* section, const char* setting) 179 | { 180 | json_t* root = load_settings(); 181 | if (!root) { 182 | return NULL; 183 | } 184 | 185 | json_t* section_obj = json_object_get(root, section); 186 | if (!section_obj) { 187 | printf("Section '%s' not found\n", section); 188 | json_decref(root); 189 | return NULL; 190 | } 191 | 192 | json_t* value = json_object_get(section_obj, setting); 193 | if (!value) { 194 | printf("Setting '%s' not found in section '%s'\n", setting, section); 195 | json_decref(root); 196 | return NULL; 197 | } 198 | 199 | // Increment the reference count before returning 200 | json_incref(value); 201 | 202 | // Release the root object 203 | json_decref(root); 204 | 205 | return value; 206 | } 207 | -------------------------------------------------------------------------------- /docs/themes.md: -------------------------------------------------------------------------------- 1 | # Themes 2 | 3 | LibreSplit can be customized using themes, which are made of CSS stylesheets. 4 | 5 | ## Changing the theme 6 | 7 | You can set the global theme by changing the `theme` value in your `settings.json` configuration file, usually you'll find it in `~/.config/libresplit/`. 8 | 9 | Also each split JSON file can apply their own themes by specifying a `theme` key in the main object, see [the split files documentation](split-files.md) for more information. 10 | 11 | ## Creating your own theme 12 | 13 | 1. Create a CSS stylesheet with your desired styles. 14 | 2. Place the stylesheet under the `~/.config/libresplit/themes//.css`directory where `name` is the name of your theme. If you have your `XDG_CONFIG_HOME` env var pointing somewhere else, you may need to change the directory accordingly. 15 | 3. Theme variants should follow the pattern `-.css`. 16 | 17 | See the [GtkCssProvider documentation](https://docs.gtk.org/gtk3/css-properties.html) for a list of supported CSS properties. Note that you can also modify the default font-family. 18 | 19 | | LibreSplit CSS classes | Explanation Where needed | 20 | | ----------------------------- | ----------------------------------------------- | 21 | | `.window` | The entire LibreSplit window | 22 | | `.header` | The header, containing title and attempt counters | 23 | | `.title` | | 24 | | `.attempt-count` | | 25 | | `.time` | | 26 | | `.delta` | | 27 | | `.time` | | 28 | | `.timer` | | 29 | | `.timer-container` | Container for both the detailed and normal timer | 30 | | `.detailed-timer` | Container for the `segment-pb` and `segment-best` classes | 31 | | `.timer-seconds` | | 32 | | `.timer-millis` | | 33 | | `.delay` | Timer not running/in negative time | 34 | | `.splits` | Container of the splits | 35 | | `.split` | The splits themselves | 36 | | `.current-split` | | 37 | | `.split-title` | | 38 | | `.split-icon` | | 39 | | `.split-time` | | 40 | | `.split-delta` | Comparison time in the split | 41 | | `.split-last` | The last split, if its not yet scrolled down to | 42 | | `.done` | | 43 | | `.behind` | Behind the PB but gaining time | 44 | | `.losing` | Ahead of PB but losing time | 45 | | `.behind.losing` | (class combination) Behind PB and losing time | 46 | | `.best-segment` | | 47 | | `.best-split` | | 48 | | `.footer` | A generic footer | 49 | | `.sum-of-bests-container` | The container for the "sum of bests" label and timer | 50 | | `.personal-best-container` | The container for the "personal best" label and timer | 51 | | `.prev-segment-container` | The container for the "previous segment" label and timer | 52 | | `.world-record-container` | The container for the "world record" label and timer | 53 | | `.prev-segment-label` | | 54 | | `.prev-segment` | | 55 | | `.segment` | | 56 | | `.segment-best` | | 57 | | `.segment-pb` | | 58 | | `.segment-seconds` | | 59 | | `.segment-millis` | | 60 | | `.sum-of-bests-label` | | 61 | | `.sum-of-bests` | | 62 | | `.split-icon` | | 63 | | `.personal-best-label` | | 64 | | `.personal-best` | | 65 | | `.world-record-label` | | 66 | | `.world-record` | | 67 | 68 | If a split has a `title` key, its UI element receives a class name derived from its title. 69 | 70 | Specifically, the title is lowercase and all non-alphanumeric characters are replaced with hyphens, and the result is concatenated with `split-title-`. 71 | 72 | For instance, if your split is titled "First split", it can be styled by targeting the CSS class `.split-title-first-split`. 73 | 74 | A more complex example: if your split is named "Space Station (Part 1)", the CSS class will be `.split-title-space-station--part-1-` (because the parentheses will become hyphens). 75 | 76 | ## FAQ 77 | 78 | ### How do I hide a section of LibreSplit? 79 | 80 | GTK does not have a built-in way of hiding pieces of the interface, but you can hide most items by setting the font-size to zero. For instance: 81 | 82 | ```css 83 | .segment-pb, .segment-best{ 84 | font-size: 0; 85 | } 86 | ``` 87 | 88 | Will hide the PB and Best sections. 89 | 90 | ### Is there anything to help me develop themes out there? 91 | 92 | In fact, there is! 93 | 94 | If you run LibreSplit from a terminal like this: 95 | 96 | ```sh 97 | GTK_DEBUG=interactive libresplit 98 | ``` 99 | 100 | LiveSplit will be started together with another window: the interactive GTK debugger. Like the one you see below: 101 | 102 | ![The GTK Debug window](./images/gtk_debugger.png) 103 | 104 | Make sure that both LibreSplit and this window are visible, because when you click on one row in the GTK debugger (`Objects` tab), the corresponding section in LiveSplit will flash 3 times, letting you know what you selected. 105 | 106 | Once you found what you want to edit, take a note of its `style class` (See [Creating your own theme](#creating-your-own-theme) for a list) and head to the `CSS` tab: there you can edit in real time LiveSplit's aspect. These edits are temporary, but they can help you developing your own CSS theme. 107 | 108 | Once you're done developing your theme, feel free to share it with the community! 109 | -------------------------------------------------------------------------------- /src/process.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include "auto-splitter.h" 13 | #include "lua.h" 14 | #include "process.h" 15 | 16 | struct game_process process; 17 | #define MAPS_CACHE_MAX_SIZE 32 18 | ProcessMap p_maps_cache[MAPS_CACHE_MAX_SIZE]; 19 | uint32_t p_maps_cache_size = 0; 20 | 21 | void execute_command(const char* command, char* output) 22 | { 23 | char buffer[4096]; 24 | FILE* pipe = popen(command, "r"); 25 | if (!pipe) { 26 | fprintf(stderr, "Error executing command: %s\n", command); 27 | exit(1); 28 | } 29 | 30 | while (fgets(buffer, 128, pipe) != NULL) { 31 | strcat(output, buffer); 32 | } 33 | 34 | pclose(pipe); 35 | } 36 | 37 | /* 38 | Gets the base address of a module 39 | if `module` equals to a nullptr, the main process is used, else it will search for the base addr of the specified module 40 | */ 41 | uintptr_t find_base_address(const char* module) 42 | { 43 | const char* module_to_grep = module == 0 ? process.name : module; 44 | 45 | for (int32_t i = 0; i < p_maps_cache_size; i++) { 46 | const char* name = p_maps_cache[i].name; 47 | if (strstr(name, module_to_grep) != NULL) { 48 | return p_maps_cache[i].start; 49 | } 50 | } 51 | 52 | char path[22]; // 22 is the maximum length the path can be (strlen("/proc/4294967296/maps")) 53 | 54 | snprintf(path, sizeof(path), "/proc/%d/maps", process.pid); 55 | 56 | FILE* f = fopen(path, "r"); 57 | 58 | if (f) { 59 | char current_line[PATH_MAX + 100]; 60 | while (fgets(current_line, sizeof(current_line), f) != NULL) { 61 | if (strstr(current_line, module_to_grep) == NULL) 62 | continue; 63 | fclose(f); 64 | uintptr_t addr_start = strtoull(current_line, NULL, 16); 65 | if (maps_cache_cycles_value != 0 && p_maps_cache_size < MAPS_CACHE_MAX_SIZE) { 66 | ProcessMap map; 67 | if (parseMapsLine(current_line, &map)) { 68 | p_maps_cache[p_maps_cache_size] = map; 69 | p_maps_cache_size++; 70 | } 71 | } 72 | return addr_start; 73 | } 74 | fclose(f); 75 | } 76 | printf("Couldn't find base address\n"); 77 | return 0; 78 | } 79 | 80 | uint64_t get_module_size(const char* module) 81 | { 82 | const char* module_to_grep = module == 0 ? process.name : module; 83 | 84 | // Check the cache 85 | for (int32_t i = 0; i < p_maps_cache_size; i++) { 86 | const char* name = p_maps_cache[i].name; 87 | if (strstr(name, module_to_grep) != NULL) { 88 | return p_maps_cache[i].size; 89 | } 90 | } 91 | 92 | // If not in cache, read the maps 93 | char path[22]; // 22 is the maximum length the path can be (strlen("/proc/4294967296/maps")) 94 | 95 | snprintf(path, sizeof(path), "/proc/%d/maps", process.pid); 96 | 97 | FILE* f = fopen(path, "r"); 98 | 99 | if (f) { 100 | char current_line[PATH_MAX + 100]; 101 | while (fgets(current_line, sizeof(current_line), f) != NULL) { 102 | if (strstr(current_line, module_to_grep) == NULL) 103 | continue; 104 | fclose(f); 105 | ProcessMap map; 106 | bool parsed = parseMapsLine(current_line, &map); 107 | if (maps_cache_cycles_value != 0 && p_maps_cache_size < MAPS_CACHE_MAX_SIZE) { 108 | if (parsed) { 109 | p_maps_cache[p_maps_cache_size] = map; 110 | p_maps_cache_size++; 111 | } 112 | } 113 | return map.size; 114 | } 115 | fclose(f); 116 | } 117 | printf("Couldn't find the module\n"); 118 | return 0; 119 | } 120 | 121 | /** 122 | * The lua getModuleSize() function. 123 | * 124 | * FIXME: Size is taken as an unsigned long, which may 125 | * ^ exceed the size of lua_Integer 126 | * 127 | * @param L The lua state 128 | */ 129 | int lua_get_module_size(lua_State* L) 130 | { 131 | if (lua_gettop(L) == 0) { 132 | // Called without argument 133 | uint64_t size = get_module_size(NULL); 134 | lua_pushinteger(L, (lua_Integer)size); 135 | return 1; 136 | } 137 | if (lua_isnil(L, 1)) { 138 | // Called with "nil" as argument 139 | uint64_t size = get_module_size(NULL); 140 | lua_pushinteger(L, (lua_Integer)size); 141 | return 1; 142 | } 143 | if (!lua_isstring(L, 1)) { 144 | // Called with invalid non-string parameter 145 | printf("[getModuleSize] Module name must be a string or nil (for the main module)"); 146 | lua_pushnil(L); 147 | return 1; 148 | } 149 | // Called with a module name (string) 150 | const char* module_name = lua_tostring(L, 1); 151 | uint64_t size = get_module_size(module_name); 152 | lua_pushinteger(L, (lua_Integer)size); 153 | return 1; 154 | } 155 | 156 | void stock_process_id(const char* pid_command) 157 | { 158 | char pid_output[PATH_MAX + 100]; 159 | pid_output[0] = '\0'; 160 | 161 | while (atomic_load(&auto_splitter_enabled)) { 162 | execute_command(pid_command, pid_output); 163 | process.pid = strtoul(pid_output, NULL, 10); 164 | if (process.pid) { 165 | size_t newlinePos = strcspn(pid_output, "\n"); 166 | if (newlinePos != strlen(pid_output) - 1 && pid_output[0] != '\0') { 167 | printf("Multiple PID's found for process: %s\n", process.name); 168 | } 169 | break; 170 | } else { 171 | printf("%s isn't running.\n", process.name); 172 | usleep(100000); // Sleep for 100ms 173 | } 174 | } 175 | 176 | printf("Process: %s\n", process.name); 177 | printf("PID: %u\n", process.pid); 178 | process.base_address = find_base_address(NULL); 179 | process.dll_address = process.base_address; 180 | } 181 | 182 | int find_process_id(lua_State* L) 183 | { 184 | printf("\033[2J\033[1;1H"); // Clear the console 185 | 186 | process.name = lua_tostring(L, 1); 187 | const char* sort = lua_tostring(L, 2); 188 | char sortCmd[16] = ""; 189 | 190 | if (!sort) { 191 | sort = "first"; 192 | } else { 193 | if (strcmp(sort, "first") != 0 && strcmp(sort, "last") != 0) { 194 | printf("[process] Invalid sort argument '%s'. Use 'first' or 'last'. Falling back to first\n", sort); 195 | sort = "first"; 196 | } 197 | } 198 | 199 | if (strcmp(sort, "first") == 0) { 200 | sortCmd[0] = '\0'; // No sorting 201 | } 202 | if (strcmp(sort, "last") == 0) { 203 | strcpy(sortCmd, " | sort -r"); // Reverse the sorting to get latest PID 204 | } 205 | 206 | char command[256]; 207 | snprintf(command, sizeof(command), "pgrep \"%.*s\"%s", (int)strnlen(process.name, 15), process.name, sortCmd); 208 | 209 | stock_process_id(command); 210 | 211 | return 0; 212 | } 213 | 214 | int getPid(lua_State* L) 215 | { 216 | lua_pushinteger(L, process.pid); 217 | return 1; 218 | } 219 | 220 | int process_exists() 221 | { 222 | int result = kill(process.pid, 0); 223 | return result == 0; 224 | } 225 | 226 | bool parseMapsLine(const char* line, ProcessMap* map) 227 | { 228 | uintptr_t end; 229 | uint64_t size; 230 | char mode[8]; 231 | unsigned long offset; 232 | unsigned int major_id, minor_id, node_id; 233 | 234 | // Thank you kernel source code 235 | int sscanf_res = sscanf(line, "%lx-%lx %7s %lx %u:%u %u %s", &map->start, 236 | &end, mode, &offset, &major_id, 237 | &minor_id, &node_id, map->name); 238 | if (!sscanf_res) 239 | return false; 240 | 241 | // Calculate the map size 242 | size = end - map->start; 243 | map->size = size; 244 | return true; 245 | } 246 | -------------------------------------------------------------------------------- /src/component/detailed-timer.c: -------------------------------------------------------------------------------- 1 | #include "components.h" 2 | 3 | typedef struct _LSDetailedTimer { 4 | LSComponent base; 5 | GtkWidget* detailed_timer; 6 | GtkWidget* detailed_info; 7 | GtkWidget* segment_pb; 8 | GtkWidget* segment_best; 9 | GtkWidget* detailed_time; 10 | GtkWidget* time; 11 | GtkWidget* time_seconds; 12 | GtkWidget* time_millis; 13 | GtkWidget* segment; 14 | GtkWidget* segment_seconds; 15 | GtkWidget* segment_millis; 16 | } LSDetailedTimer; 17 | extern LSComponentOps ls_detailed_timer_operations; 18 | 19 | LSComponent* ls_component_detailed_timer_new() 20 | { 21 | LSDetailedTimer* self; 22 | GtkWidget* spacer; 23 | 24 | self = malloc(sizeof(LSDetailedTimer)); 25 | if (!self) 26 | return NULL; 27 | self->base.ops = &ls_detailed_timer_operations; 28 | // 29 | self->detailed_timer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 30 | gtk_widget_show(self->detailed_timer); 31 | add_class(self->detailed_timer, "timer-container"); 32 | 33 | self->detailed_info = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 34 | gtk_box_pack_start(GTK_BOX(self->detailed_timer), self->detailed_info, FALSE, FALSE, 0); 35 | gtk_widget_show(self->detailed_info); 36 | add_class(self->detailed_info, "detailed-timer"); 37 | 38 | self->segment_best = gtk_label_new(NULL); 39 | add_class(self->segment_best, "segment-best"); 40 | gtk_box_pack_end(GTK_BOX(self->detailed_info), self->segment_best, FALSE, FALSE, 0); 41 | gtk_widget_show(self->segment_best); 42 | 43 | self->segment_pb = gtk_label_new(NULL); 44 | add_class(self->segment_pb, "segment-pb"); 45 | gtk_box_pack_end(GTK_BOX(self->detailed_info), self->segment_pb, FALSE, FALSE, 0); 46 | gtk_widget_show(self->segment_pb); 47 | // 48 | self->detailed_time = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 49 | add_class(self->detailed_time, "timer"); 50 | gtk_box_pack_end(GTK_BOX(self->detailed_timer), self->detailed_time, TRUE, TRUE, 0); 51 | gtk_widget_show(self->detailed_time); 52 | 53 | self->time = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 54 | add_class(self->time, "timer"); 55 | add_class(self->time, "time"); 56 | gtk_box_pack_start(GTK_BOX(self->detailed_time), self->time, FALSE, FALSE, 0); 57 | gtk_widget_show(self->time); 58 | 59 | spacer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 60 | gtk_widget_set_hexpand(spacer, TRUE); 61 | gtk_container_add(GTK_CONTAINER(self->time), spacer); 62 | gtk_widget_show(spacer); 63 | 64 | self->time_seconds = gtk_label_new(NULL); 65 | add_class(self->time_seconds, "timer-seconds"); 66 | gtk_widget_set_valign(self->time_seconds, GTK_ALIGN_BASELINE); 67 | gtk_container_add(GTK_CONTAINER(self->time), self->time_seconds); 68 | gtk_widget_show(self->time_seconds); 69 | 70 | spacer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 71 | gtk_widget_set_valign(spacer, GTK_ALIGN_END); 72 | gtk_container_add(GTK_CONTAINER(self->time), spacer); 73 | gtk_widget_show(spacer); 74 | 75 | self->time_millis = gtk_label_new(NULL); 76 | add_class(self->time_millis, "timer-millis"); 77 | gtk_widget_set_valign(self->time_millis, GTK_ALIGN_BASELINE); 78 | gtk_container_add(GTK_CONTAINER(spacer), self->time_millis); 79 | gtk_widget_show(self->time_millis); 80 | 81 | self->segment = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 82 | add_class(self->segment, "segment"); 83 | gtk_box_pack_start(GTK_BOX(self->detailed_time), self->segment, FALSE, FALSE, 0); 84 | gtk_widget_show(self->segment); 85 | 86 | spacer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 87 | gtk_widget_set_hexpand(spacer, TRUE); 88 | gtk_container_add(GTK_CONTAINER(self->segment), spacer); 89 | gtk_widget_show(spacer); 90 | 91 | self->segment_seconds = gtk_label_new(NULL); 92 | add_class(self->segment_seconds, "segment-seconds"); 93 | gtk_widget_set_valign(self->segment_seconds, GTK_ALIGN_BASELINE); 94 | gtk_container_add(GTK_CONTAINER(self->segment), self->segment_seconds); 95 | gtk_widget_show(self->segment_seconds); 96 | 97 | spacer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 98 | gtk_widget_set_valign(spacer, GTK_ALIGN_END); 99 | gtk_container_add(GTK_CONTAINER(self->segment), spacer); 100 | gtk_widget_show(spacer); 101 | 102 | self->segment_millis = gtk_label_new(NULL); 103 | add_class(self->segment_millis, "segment-millis"); 104 | gtk_widget_set_valign(self->segment_millis, GTK_ALIGN_BASELINE); 105 | gtk_container_add(GTK_CONTAINER(self->segment), self->segment_millis); 106 | gtk_widget_show(self->segment_millis); 107 | 108 | return (LSComponent*)self; 109 | } 110 | 111 | // Avoid collision with timer_delete of time.h 112 | static void ls_detailed_timer_delete(LSComponent* self) 113 | { 114 | free(self); 115 | } 116 | 117 | static GtkWidget* detailed_timer_widget(LSComponent* self) 118 | { 119 | return ((LSDetailedTimer*)self)->detailed_timer; 120 | } 121 | 122 | static void detailed_timer_clear_game(LSComponent* self_) 123 | { 124 | LSDetailedTimer* self = (LSDetailedTimer*)self_; 125 | gtk_label_set_text(GTK_LABEL(self->time_seconds), ""); 126 | gtk_label_set_text(GTK_LABEL(self->time_millis), ""); 127 | gtk_label_set_text(GTK_LABEL(self->segment_seconds), ""); 128 | gtk_label_set_text(GTK_LABEL(self->segment_millis), ""); 129 | 130 | remove_class(self->time, "behind"); 131 | remove_class(self->time, "losing"); 132 | } 133 | 134 | static void detailed_timer_draw(LSComponent* self_, const ls_game* game, const ls_timer* timer) 135 | { 136 | LSDetailedTimer* self = (LSDetailedTimer*)self_; 137 | char str[256], millis[256], seg[256], seg_millis[256]; 138 | char pb[256] = "PB: "; 139 | char best[256] = "Best: "; 140 | int curr; 141 | 142 | curr = timer->curr_split; 143 | if (curr == game->split_count) { 144 | --curr; 145 | } 146 | 147 | remove_class(self->time, "delay"); 148 | remove_class(self->time, "behind"); 149 | remove_class(self->time, "losing"); 150 | remove_class(self->time, "best-split"); 151 | 152 | if (curr == game->split_count) { 153 | curr = game->split_count - 1; 154 | } 155 | if (timer->time <= 0) { 156 | add_class(self->time, "delay"); 157 | } else { 158 | if (timer->curr_split == game->split_count 159 | && timer->split_info[curr] 160 | & LS_INFO_BEST_SPLIT) { 161 | add_class(self->time, "best-split"); 162 | } else { 163 | if (timer->split_info[curr] 164 | & LS_INFO_BEHIND_TIME) { 165 | add_class(self->time, "behind"); 166 | } 167 | if (timer->split_info[curr] 168 | & LS_INFO_LOSING_TIME) { 169 | add_class(self->time, "losing"); 170 | } 171 | } 172 | } 173 | ls_time_millis_string(str, &millis[1], timer->time); 174 | millis[0] = '.'; 175 | gtk_label_set_text(GTK_LABEL(self->time_seconds), str); 176 | gtk_label_set_text(GTK_LABEL(self->time_millis), millis); 177 | 178 | if (timer->curr_split == 0) { 179 | gtk_label_set_text(GTK_LABEL(self->segment_seconds), str); 180 | gtk_label_set_text(GTK_LABEL(self->segment_millis), millis); 181 | } else { 182 | ls_time_millis_string(seg, &seg_millis[1], timer->segment_times[timer->curr_split]); 183 | seg_millis[0] = '.'; 184 | gtk_label_set_text(GTK_LABEL(self->segment_seconds), seg); 185 | gtk_label_set_text(GTK_LABEL(self->segment_millis), seg_millis); 186 | } 187 | 188 | ls_time_string(&pb[6], game->segment_times[timer->curr_split]); 189 | gtk_label_set_text(GTK_LABEL(self->segment_pb), pb); 190 | 191 | ls_time_string(&best[6], game->best_segments[timer->curr_split]); 192 | gtk_label_set_text(GTK_LABEL(self->segment_best), best); 193 | } 194 | 195 | LSComponentOps ls_detailed_timer_operations = { 196 | .delete = ls_detailed_timer_delete, 197 | .widget = detailed_timer_widget, 198 | .clear_game = detailed_timer_clear_game, 199 | .draw = detailed_timer_draw 200 | }; 201 | -------------------------------------------------------------------------------- /src/signature.c: -------------------------------------------------------------------------------- 1 | #include "signature.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "lua.h" 13 | #include "memory.h" 14 | #include "process.h" 15 | 16 | #include 17 | 18 | extern game_process process; 19 | 20 | // Error handling macro 21 | #define HANDLE_ERROR(msg) \ 22 | do { \ 23 | perror(msg); \ 24 | return NULL; \ 25 | } while (0) 26 | 27 | // Error logging function 28 | void log_error(const char* format, ...) 29 | { 30 | va_list args; 31 | va_start(args, format); 32 | fprintf(stderr, "Error in sig_scan: "); 33 | vfprintf(stderr, format, args); 34 | fprintf(stderr, "\n"); 35 | va_end(args); 36 | } 37 | 38 | /** 39 | * Gets all the memory regions of a certain PID 40 | * 41 | * @param pid The ID of the process to get the memory regions of 42 | * @param count A pointer to a counter onto where to store the number of regions 43 | * 44 | * @return A dinamically allocated array of ProcessMap that have been found 45 | */ 46 | ProcessMap* get_memory_regions(pid_t pid, int* count) 47 | { 48 | char maps_path[256]; 49 | if (snprintf(maps_path, sizeof(maps_path), "/proc/%d/maps", pid) < 0) { 50 | HANDLE_ERROR("Failed to create maps path"); 51 | } 52 | 53 | FILE* maps_file = fopen(maps_path, "r"); 54 | if (!maps_file) { 55 | HANDLE_ERROR("Failed to open maps file"); 56 | } 57 | 58 | ProcessMap* regions = NULL; 59 | int capacity = 0; 60 | *count = 0; 61 | 62 | char line[256]; 63 | while (fgets(line, sizeof(line), maps_file)) { 64 | if (*count >= capacity) { 65 | capacity = capacity == 0 ? 10 : capacity * 2; 66 | ProcessMap* temp = realloc(regions, capacity * sizeof(ProcessMap)); 67 | if (!temp) { 68 | free(regions); 69 | fclose(maps_file); 70 | HANDLE_ERROR("Failed to allocate memory for regions"); 71 | } 72 | regions = temp; 73 | } 74 | 75 | uintptr_t start, end; 76 | if (sscanf(line, "%" SCNxPTR "-%" SCNxPTR, &start, &end) != 2) { 77 | continue; // Skip lines that don't match the expected format 78 | } 79 | regions[*count].start = start; 80 | regions[*count].end = end; 81 | (*count)++; 82 | } 83 | 84 | fclose(maps_file); 85 | return regions; 86 | } 87 | 88 | /** 89 | * Matches a pattern with an array of bytes. 90 | * 91 | * @param data The data to compare the pattern against. 92 | * @param pattern The pattern to test for. 93 | * @param pattern_size The length of the pattern. 94 | * 95 | * @return True if the pattern matches the data, false otherwise 96 | */ 97 | bool match_pattern(const uint8_t* data, const uint16_t* pattern, size_t pattern_size) 98 | { 99 | for (size_t i = 0; i < pattern_size; ++i) { 100 | uint8_t byte = pattern[i] & 0xFF; 101 | bool ignore = (pattern[i] >> 8) & 0x1; 102 | if (!ignore && data[i] != byte) { 103 | return false; 104 | } 105 | } 106 | return true; 107 | } 108 | 109 | /** 110 | * Converts an IDA-like signature into a pattern to be used in LibreSplit. 111 | * Supports the '??' string to ignore certain bytes in the comparison. 112 | * 113 | * @param signature A string containing the signature to convert. 114 | * @param pattern_size A pointer onto where to save the size of the pattern. 115 | * 116 | * @return A pattern to be used with the LibreSplit signature scan functions. 117 | */ 118 | uint16_t* convert_signature(const char* signature, size_t* pattern_size) 119 | { 120 | char* signature_copy = strdup(signature); 121 | if (!signature_copy) { 122 | return NULL; 123 | } 124 | 125 | char* token = strtok(signature_copy, " "); 126 | size_t size = 0; 127 | size_t capacity = 10; 128 | uint16_t* pattern = (uint16_t*)malloc(capacity * sizeof(uint16_t)); 129 | if (!pattern) { 130 | free(signature_copy); 131 | return NULL; 132 | } 133 | 134 | while (token != NULL) { 135 | if (size >= capacity) { 136 | capacity *= 2; 137 | uint16_t* temp = (uint16_t*)realloc(pattern, capacity * sizeof(uint16_t)); 138 | if (!temp) { 139 | free(pattern); 140 | free(signature_copy); 141 | return NULL; 142 | } 143 | pattern = temp; 144 | } 145 | 146 | if (strstr(token, "?") != NULL) { 147 | // Treats a half-byte mask as a full-byte mask (0? => ?? or ?F=> ??) 148 | pattern[size] = 0xFF00; // Set the upper byte to 1 to indicate ignoring this byte 149 | } else { 150 | pattern[size] = strtol(token, NULL, 16); 151 | } 152 | size++; 153 | token = strtok(NULL, " "); 154 | } 155 | 156 | free(signature_copy); 157 | *pattern_size = size; 158 | return pattern; 159 | } 160 | 161 | bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t size) 162 | { 163 | struct iovec local_iov = { buffer, size }; 164 | struct iovec remote_iov = { (void*)address, size }; 165 | ssize_t nread = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0); 166 | 167 | return nread == (ssize_t)size; 168 | } 169 | 170 | /** 171 | * Performs the Lua Auto Splitter sig_scan function, pushing onto the Lua stack the result. 172 | * 173 | * If a pattern is found, it will be offset by the process base_address, allowing the result to 174 | * be used directly in readAddress, without any module definition. 175 | * 176 | * Using readAddress with a module name and an address coming from sig_scan is not supported and 177 | * may result in out-of-process reads or other unforeseen consequences. 178 | * 179 | * @param L The lua state. 180 | * 181 | * @return Always 1 182 | */ 183 | int perform_sig_scan(lua_State* L) 184 | { 185 | if (lua_gettop(L) != 2) { 186 | log_error("Invalid number of arguments: expected 2 (signature, offset)"); 187 | lua_pushnil(L); 188 | return 1; 189 | } 190 | 191 | if (!lua_isstring(L, 1) || !lua_isnumber(L, 2)) { 192 | log_error("Invalid argument types: expected (string, number)"); 193 | lua_pushnil(L); 194 | return 1; 195 | } 196 | 197 | pid_t p_pid = process.pid; 198 | const char* signature = lua_tostring(L, 1); 199 | intptr_t offset = lua_tointeger(L, 2); 200 | 201 | // Validate signature string 202 | if (strlen(signature) == 0) { 203 | log_error("Signature string cannot be empty"); 204 | lua_pushnil(L); 205 | return 1; 206 | } 207 | 208 | size_t pattern_length; 209 | uint16_t* pattern = convert_signature(signature, &pattern_length); 210 | if (!pattern) { 211 | log_error("Failed to convert signature"); 212 | lua_pushnil(L); 213 | return 1; 214 | } 215 | 216 | int regions_count = 0; 217 | ProcessMap* regions = get_memory_regions(p_pid, ®ions_count); 218 | if (!regions) { 219 | free(pattern); 220 | log_error("Failed to get memory regions"); 221 | lua_pushnil(L); 222 | return 1; 223 | } 224 | 225 | for (int i = 0; i < regions_count; i++) { 226 | ProcessMap region = regions[i]; 227 | ssize_t region_size = region.end - region.start; 228 | uint8_t* buffer = malloc(region_size); 229 | if (!buffer) { 230 | free(pattern); 231 | free(regions); 232 | log_error("Failed to allocate memory for region buffer"); 233 | lua_pushnil(L); 234 | return 1; 235 | } 236 | 237 | if (!validate_process_memory(p_pid, region.start, buffer, region_size)) { 238 | free(buffer); 239 | continue; // Continue to next region 240 | } 241 | 242 | for (size_t j = 0; j <= region_size - pattern_length; ++j) { 243 | if (match_pattern(buffer + j, pattern, pattern_length)) { 244 | // The resulting address is the start of the region 245 | // plus the index of the first byte that matches 246 | // plus the user-set offset, minus the process's base_address 247 | // or a subsequent memory read will read the wrong address or 248 | // go out of memory (due to commit 2b4417f offsetting memory reads) 249 | // So this result might be negative if the main module happens to be after 250 | // the found signature. This should be corrected by readAddress. 251 | intptr_t result = (region.start + j + offset) - process.base_address; 252 | 253 | free(buffer); 254 | free(pattern); 255 | free(regions); 256 | 257 | lua_pushnumber(L, result); 258 | return 1; 259 | } 260 | } 261 | 262 | free(buffer); 263 | } 264 | 265 | free(pattern); 266 | free(regions); 267 | 268 | // No match found 269 | log_error("No match found for the given signature"); 270 | lua_pushnil(L); 271 | return 1; 272 | } 273 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

LibreSplit

5 | 6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

21 | 22 |

23 | 24 | 25 |

26 | 27 | ## About 28 | 29 | LibreSplit is a speedrun timer based on [urn](https://github.com/3snowp7im/urn) that adds support for Lua-based auto splitters that are easy to port from ASL. 30 | 31 |

32 | 33 | 34 |

35 | 36 | ### If you are looking for the public repository of splits, auto splitters and themes. They are located [here](https://github.com/LibreSplit/LibreSplit-resources) 37 | 38 | ## Features 39 | 40 | - **Split Tracking and Timing:** Accurately track and time your speedruns with ease. 41 | - **Auto Splitter Support:** Utilize Lua-based auto splitters to automate split timing based on in-game events. 42 | - **Customizable Themes:** Customize your timer's appearance by creating and applying custom themes. 43 | - **Flexible Configuration:** Configure keybindings and various settings to suit your preferences. 44 | - **Icon support for splits.** 45 | - **Always on Top support.** 46 | - **Support for in-game time.** 47 | 48 | ---- 49 | 50 | ## Installation 51 | 52 | - Arch-based Distros 53 | - `yay libresplit-git` 54 | - `paru libresplit-git` 55 | 56 | See the [libresplit-git](https://aur.archlinux.org/packages/libresplit-git) package on the Arch User Repository (AUR). 57 | 58 | - NixOS 59 | 60 | See the [libresplit](https://search.nixos.org/packages?channel=25.05&show=libresplit&query=libresplit) package, courtesy of [@fgaz](https://github.com/fgaz). 61 | 62 | ## Building 63 | 64 | LibreSplit requires the following dependencies on your system to compile: 65 | 66 | - `git` 67 | - `meson` 68 | - `libgtk+-3.0` 69 | - `x11` 70 | - `libjansson` 71 | - `luajit` 72 | 73 | Install the required dependencies: 74 | 75 | - Debian-based systems 76 | 77 | ```sh 78 | sudo apt update 79 | sudo apt install build-essential libgtk-3-dev libjansson-dev meson libluajit-5.1-dev cmake 80 | ``` 81 | 82 | - Arch-based systems 83 | 84 | ```sh 85 | sudo pacman -Sy 86 | sudo pacman -S gtk3 jansson luajit git meson 87 | ``` 88 | 89 | Clone the project: 90 | 91 | ```sh 92 | git clone https://github.com/LibreSplit/LibreSplit 93 | cd LibreSplit 94 | ``` 95 | 96 | Now compile and install: 97 | 98 | ```sh 99 | meson setup build -Dbuildtype=release 100 | meson compile -C build 101 | meson install -C build 102 | ``` 103 | 104 | All done! Now you can start the desktop **LibreSplit** or run `/usr/local/bin/libresplit`. 105 | 106 | --- 107 | 108 | ## Usage 109 | 110 | When you start **LibreSplit** for the first time, it will create the `libresplit` directory in your config directory (it will usually be `~/.config/libresplit`). Such directory will contain: 111 | 112 | - Splits. 113 | - Auto Splitters. 114 | - Themes. 115 | 116 | All 3 directories will start empty, so you may want to download the [resource repository](https://github.com/LibreSplit/LibreSplit-resources/) first and clone it in `~/.config/libresplit/` before starting LibreSplit for the first time. 117 | 118 | A file dialog will then appear, asking you to select a Split JSON file (see [Split files](#split-files)). 119 | 120 | Initially the window is undecorated. You can toggle window decorations by pressing the `Right Control` key. 121 | 122 | ### Default Keybinds 123 | 124 | The timer is controlled with the following keys 125 | (note that their action **depends on the state of the timer**): 126 | 127 | | Key | Timer is Stopped | Timer is running | 128 | | --------------------- | ------------------ | ------------------ | 129 | | Spacebar | Start timer | Split | 130 | | Backspace | Reset timer | Stop timer | 131 | | Delete | Cancel | - | 132 | 133 | Cancel will **reset the timer** and **decrement the attempt counter**. A run that is reset before the start delay is automatically cancelled. 134 | 135 | If you forget to split, or accidentally split twice, you can manually change the current split: 136 | 137 | | Key | Action | 138 | | -------------------- | ---------- | 139 | | Page Up | Unsplit | 140 | | Page Down | Skip Split | 141 | 142 | ### Colors 143 | 144 | The color of a time or delta has a special meaning. 145 | 146 | | Color | Meaning | 147 | | ------------- | ---------------------------------------- | 148 | | Dark red | Behind splits in PB and losing time | 149 | | Light red | Behind splits in PB and gaining time | 150 | | Dark green | Ahead of splits in PB and gaining time | 151 | | Light green | Ahead of splits in PB and losing time | 152 | | Blue | Best split time in any run | 153 | | Gold | Best segment time in any run | 154 | 155 | --- 156 | 157 | ## Settings and Keybinds 158 | 159 | If you want to change the default settings or keybinds, you can check the [Settings and Keybinds documentation](docs/settings-keybinds.md) 160 | 161 | --- 162 | 163 | ## Auto Splitters 164 | 165 | LibreSplit supports auto splitters written in Lua to automate split timing based on in-game events. 166 | 167 | Feel free to make your own, Documentation can be found [here](docs/auto-splitters.md) 168 | 169 | --- 170 | 171 | ## Split Files 172 | 173 | Split files in LibreSplit are stored as well-formed JSON. 174 | 175 | Check the [Split Files documentation](docs/split-files.md) for more information. 176 | 177 | --- 178 | 179 | ## Themes 180 | 181 | You can customize LibreSplit to your liking using themes. 182 | 183 | For more information, check the [Themes documentation](docs/themes.md). 184 | 185 | --- 186 | 187 | ## FAQ 188 | 189 | - **How do I resize the application window?** 190 | 191 | You can edit the `width` and `height` properties in the [Split JSON File](docs/split-files.md) 192 | 193 | - **How do I change the default keybinds?** 194 | 195 | You can change the keybinds by editing your `settings.json` file (usually inside the `~/.config/libresplit` folder). 196 | 197 | See [Settings and Keybinds](docs/settings-keybinds.md) for some examples and more information. 198 | 199 | - **How do I make the keybinds global?** 200 | 201 | You can set the `global_hotkeys` property as `true` by editing your `settings.json` file. 202 | 203 | Wayland users experienced crashes when enabled `global_hotkeys`, so this settings is ignored for Wayland desktops. 204 | 205 | See [Settings and Keybinds](docs/settings-keybinds.md) for more information and workarounds for Wayland. 206 | 207 | - **Can I modify LibreSplit's appearance?** 208 | 209 | Yes. You can [create your own theme](docs/themes.md) or [download themes online](https://github.com/LibreSplit/LibreSplit-resources/tree/main/themes). 210 | 211 | - **How can I make my own split file?** 212 | 213 | You can use any existing JSON split file as an example from our [resource repository](https://github.com/LibreSplit/LibreSplit-resources/tree/main/splits) and refer to the [Split Files Documentation](docs/split-files.md) for more information. 214 | 215 | You can place the split files wherever you prefer, just open them when starting LibreSplit. 216 | 217 | - **Can I define custom icons for my splits?** 218 | 219 | Yes! You can use local files or web urls. See the `icon` key in the [split object](docs/split-files.md#split-object). 220 | 221 | The default icon size is 20x20px, but you can change it like so: 222 | 223 | ```css 224 | .split-icon { 225 | min-width: 24px; 226 | min-height: 24px; 227 | background-size: 24px; 228 | } 229 | ``` 230 | 231 | - **Can I contribute?** 232 | 233 | Absolutely! 234 | 235 | You can contribute in many ways: 236 | 237 | - By making [pull requests](https://github.com/LibreSplit/LibreSplit/pulls). 238 | - By creating new themes, split files or auto splitters and add them to our [resource repository](https://github.com/LibreSplit/LibreSplit-resources/). 239 | - By [reporting issues](https://github.com/LibreSplit/LibreSplit/pulls). 240 | - By sending us suggestions, feature requests, improve the documentation and more. Feel free to join our [discord server](https://discord.gg/qbzD7MBjyw) to follow LibreSplit's development up close. 241 | 242 | --- 243 | 244 | ## Uninstall LibreSplit 245 | 246 | You can uninstall LibreSplit using your package manager or, if you built it manually, by running 247 | 248 | ```sh 249 | cd build 250 | sudo ninja uninstall 251 | ``` 252 | -------------------------------------------------------------------------------- /src/memory.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "glib.h" 12 | #include "lua.h" 13 | #include "memory.h" 14 | #include "process.h" 15 | 16 | bool memory_error; 17 | extern game_process process; 18 | gboolean display_non_capable_mem_read_dialog(void* data); 19 | 20 | #define READ_MEMORY_FUNCTION(value_type) \ 21 | value_type read_memory_##value_type(uint64_t mem_address, int32_t* err) \ 22 | { \ 23 | value_type value; \ 24 | \ 25 | struct iovec mem_local; \ 26 | struct iovec mem_remote; \ 27 | \ 28 | mem_local.iov_base = &value; \ 29 | mem_local.iov_len = sizeof(value); \ 30 | mem_remote.iov_len = sizeof(value); \ 31 | mem_remote.iov_base = (void*)(uintptr_t)mem_address; \ 32 | \ 33 | ssize_t mem_n_read = process_vm_readv(process.pid, &mem_local, 1, &mem_remote, 1, 0); \ 34 | if (mem_n_read == -1) { \ 35 | *err = (int32_t)errno; \ 36 | memory_error = true; \ 37 | } else if (mem_n_read != (ssize_t)mem_remote.iov_len) { \ 38 | printf("Error reading process memory: short read of %ld bytes\n", (long)mem_n_read); \ 39 | exit(1); \ 40 | } \ 41 | \ 42 | return value; \ 43 | } 44 | 45 | READ_MEMORY_FUNCTION(int8_t) 46 | READ_MEMORY_FUNCTION(uint8_t) 47 | READ_MEMORY_FUNCTION(int16_t) 48 | READ_MEMORY_FUNCTION(uint16_t) 49 | READ_MEMORY_FUNCTION(int32_t) 50 | READ_MEMORY_FUNCTION(uint32_t) 51 | READ_MEMORY_FUNCTION(int64_t) 52 | READ_MEMORY_FUNCTION(uint64_t) 53 | READ_MEMORY_FUNCTION(float) 54 | READ_MEMORY_FUNCTION(double) 55 | READ_MEMORY_FUNCTION(bool) 56 | 57 | char* read_memory_string(uint64_t mem_address, int buffer_size, int32_t* err) 58 | { 59 | char* buffer = (char*)malloc(buffer_size); 60 | if (buffer == NULL) { 61 | // Handle memory allocation failure 62 | return NULL; 63 | } 64 | 65 | struct iovec mem_local; 66 | struct iovec mem_remote; 67 | 68 | mem_local.iov_base = buffer; 69 | mem_local.iov_len = buffer_size; 70 | mem_remote.iov_len = buffer_size; 71 | mem_remote.iov_base = (void*)(uintptr_t)mem_address; 72 | 73 | ssize_t mem_n_read = process_vm_readv(process.pid, &mem_local, 1, &mem_remote, 1, 0); 74 | if (mem_n_read == -1) { 75 | buffer[0] = '\0'; 76 | *err = (int32_t)errno; 77 | memory_error = true; 78 | } else if (mem_n_read != (ssize_t)mem_remote.iov_len) { 79 | printf("Error reading process memory: short read of %ld bytes\n", (long)mem_n_read); 80 | exit(1); 81 | } 82 | 83 | return buffer; 84 | } 85 | 86 | /* 87 | Prints the according error to stdout 88 | True if the error was printed 89 | False if the error is unknown 90 | */ 91 | bool handle_memory_error(uint32_t err) 92 | { 93 | static bool shownDialog = false; 94 | if (err == 0) 95 | return false; 96 | switch (err) { 97 | case EFAULT: 98 | printf("[readAddress] EFAULT: Invalid memory space/address\n"); 99 | break; 100 | case EINVAL: 101 | printf("[readAddress] EINVAL: An error ocurred while reading memory\n"); 102 | break; 103 | case ENOMEM: 104 | printf("[readAddress] ENOMEM: Please get more memory\n"); 105 | break; 106 | case EPERM: 107 | printf("[readAddress] EPERM: Permission denied\n"); 108 | 109 | if (!shownDialog) { 110 | shownDialog = true; 111 | g_idle_add(display_non_capable_mem_read_dialog, NULL); 112 | } 113 | 114 | break; 115 | case ESRCH: 116 | printf("[readAddress] ESRCH: No process with specified PID exists\n"); 117 | break; 118 | } 119 | return true; 120 | } 121 | 122 | /** 123 | * The Lua "getBaseAddress" Auto Splitter function. 124 | * 125 | * Takes an optional module name and returns its base address. 126 | * If the argument passed is absent or nil, defaults to the main module. 127 | * 128 | * @param L The Lua State 129 | */ 130 | int get_base_address(lua_State* L) 131 | { 132 | uintptr_t address; 133 | if (lua_gettop(L) == 0 || lua_isnil(L, 1)) { 134 | // No arguments passed or first argument is nil, search for process base address 135 | address = find_base_address(NULL); 136 | lua_pushnumber(L, address); 137 | return 1; 138 | } 139 | if (lua_isstring(L, 1)) { 140 | // Module name passed, search for its base address 141 | const char* module_name = lua_tostring(L, 1); 142 | address = find_base_address(module_name); 143 | lua_pushnumber(L, address); 144 | return 1; 145 | } 146 | printf("Cannot search for base address: module name must be a string or nil (for main module)"); 147 | return 0; 148 | } 149 | 150 | /** 151 | * The "sizeOf" Lua AutoSplitter Runtime function 152 | * 153 | * Takes the type and returns the size it occupies, in bytes. 154 | * 155 | * @param L The Lua State 156 | */ 157 | int size_of(lua_State* L) 158 | { 159 | if (!lua_isstring(L, 1)) { 160 | printf("The first argument must be a string defining the type to size"); 161 | return 0; 162 | } 163 | const char* type_to_size = lua_tostring(L, 1); 164 | int size_of_type = 0; 165 | if (strcmp(type_to_size, "sbyte") == 0) { 166 | size_of_type = sizeof(int8_t); 167 | } else if (strcmp(type_to_size, "byte") == 0) { 168 | size_of_type = sizeof(uint8_t); 169 | } else if (strcmp(type_to_size, "short") == 0) { 170 | size_of_type = sizeof(int16_t); 171 | } else if (strcmp(type_to_size, "ushort") == 0) { 172 | size_of_type = sizeof(uint16_t); 173 | } else if (strcmp(type_to_size, "int") == 0) { 174 | size_of_type = sizeof(int32_t); 175 | } else if (strcmp(type_to_size, "uint") == 0) { 176 | size_of_type = sizeof(uint32_t); 177 | } else if (strcmp(type_to_size, "long") == 0) { 178 | size_of_type = sizeof(int64_t); 179 | } else if (strcmp(type_to_size, "ulong") == 0) { 180 | size_of_type = sizeof(uint64_t); 181 | } else if (strcmp(type_to_size, "float") == 0) { 182 | size_of_type = sizeof(float); 183 | } else if (strcmp(type_to_size, "double") == 0) { 184 | size_of_type = sizeof(double); 185 | } else if (strcmp(type_to_size, "bool") == 0) { 186 | size_of_type = sizeof(bool); 187 | } else if (strstr(type_to_size, "string") != NULL) { 188 | int buffer_size = atoi(type_to_size + 6); 189 | if (buffer_size < 2) { 190 | printf("Invalid string size, please read documentation"); 191 | return 0; 192 | } 193 | size_of_type = sizeof(char) * buffer_size; 194 | } else if (strstr(type_to_size, "byte")) { 195 | int array_size = atoi(type_to_size + 4); 196 | if (array_size < 1) { 197 | printf("Invalid byte array size, please read documentation"); 198 | return 0; 199 | } 200 | size_of_type = sizeof(uint8_t) * array_size; 201 | } else { 202 | // Error handling 203 | printf("Cannot find size of type %s", type_to_size); 204 | lua_pushnil(L); 205 | return 1; 206 | } 207 | lua_pushinteger(L, size_of_type); 208 | return 1; 209 | } 210 | 211 | int read_address(lua_State* L) 212 | { 213 | memory_error = false; 214 | uint64_t address; 215 | const char* value_type = lua_tostring(L, 1); 216 | int i; 217 | 218 | if (lua_isnil(L, 2)) { 219 | // The address is NULL, this will bring a segfault if left alone 220 | printf("[readAddress] The address argument cannot be nil. Check your auto splitter code.\n"); 221 | lua_pushnil(L); 222 | return 1; 223 | } 224 | 225 | if (lua_isnumber(L, 2)) { 226 | address = process.base_address + lua_tointeger(L, 2); 227 | i = 3; 228 | } else { 229 | const char* module = lua_tostring(L, 2); 230 | if (strcmp(process.name, module) != 0) { 231 | process.dll_address = find_base_address(module); 232 | } 233 | address = process.dll_address + lua_tointeger(L, 3); 234 | i = 4; 235 | } 236 | 237 | int error = 0; 238 | 239 | for (; i <= lua_gettop(L); i++) { 240 | if (address <= UINT32_MAX) { 241 | address = read_memory_uint32_t((uint64_t)address, &error); 242 | if (memory_error) 243 | break; 244 | } else { 245 | address = read_memory_uint64_t(address, &error); 246 | if (memory_error) 247 | break; 248 | } 249 | address += lua_tointeger(L, i); 250 | } 251 | 252 | if (strcmp(value_type, "sbyte") == 0) { 253 | int8_t value = read_memory_int8_t(address, &error); 254 | lua_pushinteger(L, value); 255 | } else if (strcmp(value_type, "byte") == 0) { 256 | uint8_t value = read_memory_uint8_t(address, &error); 257 | lua_pushinteger(L, value); 258 | } else if (strcmp(value_type, "short") == 0) { 259 | int16_t value = read_memory_int16_t(address, &error); 260 | lua_pushinteger(L, value); 261 | } else if (strcmp(value_type, "ushort") == 0) { 262 | uint16_t value = read_memory_uint16_t(address, &error); 263 | lua_pushinteger(L, value); 264 | } else if (strcmp(value_type, "int") == 0) { 265 | int32_t value = read_memory_int32_t(address, &error); 266 | lua_pushinteger(L, value); 267 | } else if (strcmp(value_type, "uint") == 0) { 268 | uint32_t value = read_memory_uint32_t(address, &error); 269 | lua_pushinteger(L, value); 270 | } else if (strcmp(value_type, "long") == 0) { 271 | // TODO: Fix 64 bit numbers, luajit 5.1 doesnt support 64 bit numbers natively 272 | int64_t value = read_memory_int64_t(address, &error); 273 | lua_pushinteger(L, value); 274 | } else if (strcmp(value_type, "ulong") == 0) { 275 | // TODO: Fix 64 bit numbers, luajit 5.1 doesnt support 64 bit numbers natively 276 | uint64_t value = read_memory_uint64_t(address, &error); 277 | lua_pushinteger(L, value); 278 | } else if (strcmp(value_type, "float") == 0) { 279 | float value = read_memory_float(address, &error); 280 | lua_pushnumber(L, value); 281 | } else if (strcmp(value_type, "double") == 0) { 282 | double value = read_memory_double(address, &error); 283 | lua_pushnumber(L, value); 284 | } else if (strcmp(value_type, "bool") == 0) { 285 | bool value = read_memory_bool(address, &error); 286 | lua_pushboolean(L, value ? 1 : 0); 287 | } else if (strstr(value_type, "string") != NULL) { 288 | int buffer_size = atoi(value_type + 6); 289 | if (buffer_size < 2) { 290 | printf("[readAddress] Invalid string size, please read documentation"); 291 | exit(1); 292 | } 293 | char* value = read_memory_string(address, buffer_size, &error); 294 | lua_pushstring(L, value != NULL ? value : ""); 295 | free(value); 296 | } else if (strstr(value_type, "byte")) { 297 | int array_size = atoi(value_type + 4); 298 | if (array_size < 1) { 299 | printf("[readAddress] Invalid byte array size, please read documentation"); 300 | exit(1); 301 | } 302 | uint8_t* results = malloc(array_size * sizeof(uint8_t)); 303 | for (int j = 0; j < array_size; j++) { 304 | uint8_t value = read_memory_uint8_t(address + j, &error); 305 | if (memory_error) 306 | break; 307 | results[j] = value; 308 | } 309 | 310 | // Now that we have the results, push them to Lua table 311 | // This is because if the read_memory fails midway, we don't want to push partial data 312 | // And also want to avoid pushing the fallback result as part of the table 313 | if (!memory_error) { 314 | lua_createtable(L, array_size, 0); 315 | for (int j = 0; j < array_size; j++) { 316 | uint8_t value = results[j]; 317 | lua_pushinteger(L, value); 318 | lua_rawseti(L, -2, j + 1); 319 | } 320 | } 321 | free(results); 322 | } else { 323 | printf("[readAddress] Invalid value type: %s\n", value_type); 324 | exit(1); 325 | } 326 | 327 | if (memory_error) { 328 | lua_pushnil(L); 329 | handle_memory_error(error); 330 | } 331 | 332 | return 1; 333 | } 334 | -------------------------------------------------------------------------------- /src/auto-splitter.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "auto-splitter.h" 16 | #include "lua.h" 17 | #include "memory.h" 18 | #include "process.h" 19 | #include "settings.h" 20 | #include "signature.h" 21 | 22 | char auto_splitter_file[PATH_MAX]; 23 | int refresh_rate = 60; 24 | bool use_game_time = false; 25 | atomic_bool update_game_time = false; 26 | atomic_llong game_time_value = 0; 27 | int maps_cache_cycles = 1; // 0=off, 1=current cycle, +1=multiple cycles 28 | int maps_cache_cycles_value = 1; // same as `maps_cache_cycles` but this one represents the current value that changes on each cycle rather than the reference from the script 29 | atomic_bool auto_splitter_enabled = true; 30 | atomic_bool auto_splitter_running = false; 31 | atomic_bool call_start = false; 32 | atomic_bool run_started = false; 33 | atomic_bool run_finished = false; // Disallows starting the timer again after finishing until reset 34 | atomic_bool call_split = false; 35 | atomic_bool toggle_loading = false; 36 | atomic_bool call_reset = false; 37 | bool prev_is_loading; 38 | 39 | static const char* disabled_functions[] = { 40 | "collectgarbage", 41 | "dofile", 42 | "getmetatable", 43 | "setmetatable", 44 | "getfenv", 45 | "setfenv", 46 | "load", 47 | "loadfile", 48 | "loadstring", 49 | "rawequal", 50 | "rawget", 51 | "rawset", 52 | "module", 53 | "require", 54 | "newproxy", 55 | }; 56 | 57 | extern game_process process; 58 | 59 | // I have no idea how this works 60 | // https://stackoverflow.com/a/2336245 61 | static void mkdir_p(const char* dir, __mode_t permissions) 62 | { 63 | char tmp[256] = { 0 }; 64 | char* p = NULL; 65 | size_t len; 66 | 67 | snprintf(tmp, sizeof(tmp), "%s", dir); 68 | len = strlen(tmp); 69 | if (tmp[len - 1] == '/') 70 | tmp[len - 1] = 0; 71 | for (p = tmp + 1; *p; p++) 72 | if (*p == '/') { 73 | *p = 0; 74 | mkdir(tmp, permissions); 75 | *p = '/'; 76 | } 77 | mkdir(tmp, permissions); 78 | } 79 | 80 | void check_directories() 81 | { 82 | char libresplit_directory[PATH_MAX] = { 0 }; 83 | get_libresplit_folder_path(libresplit_directory); 84 | 85 | char auto_splitters_directory[PATH_MAX]; 86 | char themes_directory[PATH_MAX]; 87 | char splits_directory[PATH_MAX]; 88 | 89 | strcpy(auto_splitters_directory, libresplit_directory); 90 | strcat(auto_splitters_directory, "/auto-splitters"); 91 | 92 | strcpy(themes_directory, libresplit_directory); 93 | strcat(themes_directory, "/themes"); 94 | 95 | strcpy(splits_directory, libresplit_directory); 96 | strcat(splits_directory, "/splits"); 97 | 98 | // Make the libresplit directory if it doesn't exist 99 | mkdir_p(libresplit_directory, 0755); 100 | 101 | // Make the autosplitters directory if it doesn't exist 102 | if (mkdir(auto_splitters_directory, 0755) == -1) { 103 | // Directory already exists or there was an error 104 | } 105 | 106 | // Make the themes directory if it doesn't exist 107 | if (mkdir(themes_directory, 0755) == -1) { 108 | // Directory already exists or there was an error 109 | } 110 | 111 | // Make the splits directory if it doesn't exist 112 | if (mkdir(splits_directory, 0755) == -1) { 113 | // Directory already exists or there was an error 114 | } 115 | } 116 | 117 | static const luaL_Reg lj_lib_load[] = { 118 | { "", luaopen_base }, 119 | { LUA_STRLIBNAME, luaopen_string }, 120 | { LUA_MATHLIBNAME, luaopen_math }, 121 | { LUA_BITLIBNAME, luaopen_bit }, 122 | { LUA_JITLIBNAME, luaopen_jit }, 123 | { NULL, NULL } 124 | }; 125 | 126 | LUALIB_API void luaL_openlibs(lua_State* L) 127 | { 128 | const luaL_Reg* lib; 129 | for (lib = lj_lib_load; lib->func; lib++) { 130 | lua_pushcfunction(L, lib->func); 131 | lua_pushstring(L, lib->name); 132 | lua_call(L, 1, 0); 133 | } 134 | } 135 | 136 | void disable_functions(lua_State* L, const char** functions) 137 | { 138 | for (int i = 0; functions[i] != NULL; i++) { 139 | lua_pushnil(L); 140 | lua_setglobal(L, functions[i]); 141 | } 142 | } 143 | 144 | /* 145 | Generic function to call lua functions 146 | Signatures are something like `disb>s` 147 | 1. d = double 148 | 2. i = int 149 | 3. s = string 150 | 4. b = boolean 151 | 5. > = return separator 152 | 153 | Example: `call_va("functionName", "dd>d", x, y, &z);` 154 | */ 155 | bool call_va(lua_State* L, const char* func, const char* sig, ...) 156 | { 157 | va_list vl; 158 | int narg, nres; /* number of arguments and results */ 159 | 160 | va_start(vl, sig); 161 | lua_getglobal(L, func); /* get function */ 162 | 163 | /* push arguments */ 164 | narg = 0; 165 | while (*sig) { /* push arguments */ 166 | switch (*sig++) { 167 | case 'd': /* double argument */ 168 | lua_pushnumber(L, va_arg(vl, double)); 169 | break; 170 | 171 | case 'i': /* int argument */ 172 | lua_pushnumber(L, va_arg(vl, int)); 173 | break; 174 | 175 | case 's': /* string argument */ 176 | lua_pushstring(L, va_arg(vl, char*)); 177 | break; 178 | 179 | case 'b': 180 | lua_pushboolean(L, va_arg(vl, int)); 181 | break; 182 | 183 | case '>': 184 | break; 185 | 186 | default: 187 | printf("invalid option (%c)\n", *(sig - 1)); 188 | return false; 189 | } 190 | if (*(sig - 1) == '>') 191 | break; 192 | narg++; 193 | luaL_checkstack(L, 1, "too many arguments"); 194 | } 195 | 196 | /* do the call */ 197 | nres = strlen(sig); /* number of expected results */ 198 | if (lua_pcall(L, narg, nres, 0) != LUA_OK) { 199 | const char* err = lua_tostring(L, -1); 200 | printf("error running function '%s': %s\n", func, err); 201 | return false; 202 | } 203 | 204 | /* retrieve results */ 205 | nres = -nres; /* stack index of first result */ 206 | /* check if there's a return value */ 207 | if (!lua_isnil(L, nres)) { 208 | while (*sig) { /* get results */ 209 | switch (*sig++) { 210 | case 'd': /* double result */ 211 | if (!lua_isnumber(L, nres)) { 212 | printf("function '%s' wrong result type, expected double\n", func); 213 | return false; 214 | } 215 | *va_arg(vl, double*) = lua_tonumber(L, nres); 216 | break; 217 | 218 | case 'i': /* int result */ 219 | if (!lua_isnumber(L, nres)) { 220 | printf("function '%s' wrong result type, expected int\n", func); 221 | return false; 222 | } 223 | *va_arg(vl, int*) = (int)lua_tonumber(L, nres); 224 | break; 225 | 226 | case 's': /* string result */ 227 | if (!lua_isstring(L, nres)) { 228 | printf("function '%s' wrong result type, expected string\n", func); 229 | return false; 230 | } 231 | *va_arg(vl, const char**) = lua_tostring(L, nres); 232 | break; 233 | 234 | case 'b': 235 | if (!lua_isboolean(L, nres)) { 236 | printf("function '%s' wrong result type, expected boolean\n", func); 237 | return false; 238 | } 239 | *va_arg(vl, bool*) = lua_toboolean(L, nres); 240 | break; 241 | 242 | default: 243 | printf("invalid option (%c)\n", *(sig - 1)); 244 | return false; 245 | } 246 | nres++; 247 | } 248 | } else { 249 | va_end(vl); 250 | return false; 251 | } 252 | va_end(vl); 253 | return true; 254 | } 255 | 256 | void startup(lua_State* L) 257 | { 258 | lua_getglobal(L, "startup"); 259 | lua_pcall(L, 0, 0, 0); 260 | 261 | lua_getglobal(L, "refreshRate"); 262 | if (lua_isnumber(L, -1)) { 263 | refresh_rate = lua_tointeger(L, -1); 264 | } 265 | lua_pop(L, 1); // Remove 'refreshRate' from the stack 266 | 267 | lua_getglobal(L, "mapsCacheCycles"); 268 | if (lua_isnumber(L, -1)) { 269 | maps_cache_cycles = lua_tointeger(L, -1); 270 | maps_cache_cycles_value = maps_cache_cycles; 271 | } 272 | lua_pop(L, 1); // Remove 'mapsCacheCycles' from the stack 273 | 274 | lua_getglobal(L, "useGameTime"); 275 | if (lua_isboolean(L, -1)) { 276 | use_game_time = lua_toboolean(L, -1); 277 | } 278 | lua_pop(L, 1); // Remove 'useGameTime' from the stack 279 | } 280 | 281 | void state(lua_State* L) 282 | { 283 | call_va(L, "state", ""); 284 | } 285 | 286 | void update(lua_State* L) 287 | { 288 | call_va(L, "update", ""); 289 | } 290 | 291 | void start(lua_State* L) 292 | { 293 | bool ret; 294 | if (call_va(L, "start", ">b", &ret)) { 295 | atomic_store(&call_start, ret); 296 | if (ret) { 297 | atomic_store(&run_started, true); 298 | } 299 | } 300 | lua_pop(L, 1); // Remove the return value from the stack 301 | } 302 | 303 | void split(lua_State* L) 304 | { 305 | bool ret; 306 | if (call_va(L, "split", ">b", &ret)) { 307 | atomic_store(&call_split, ret); 308 | } 309 | lua_pop(L, 1); // Remove the return value from the stack 310 | } 311 | 312 | void is_loading(lua_State* L) 313 | { 314 | bool loading; 315 | if (call_va(L, "isLoading", ">b", &loading)) { 316 | if (loading != prev_is_loading) { 317 | atomic_store(&toggle_loading, true); 318 | prev_is_loading = !prev_is_loading; 319 | } 320 | } 321 | lua_pop(L, 1); // Remove the return value from the stack 322 | } 323 | 324 | void reset(lua_State* L) 325 | { 326 | bool shouldReset; 327 | if (call_va(L, "reset", ">b", &shouldReset)) { 328 | if (shouldReset) 329 | atomic_store(&call_reset, true); 330 | } 331 | lua_pop(L, 1); // Remove the return value from the stack 332 | } 333 | 334 | void gameTime(lua_State* L) 335 | { 336 | int gameTime; 337 | if (call_va(L, "gameTime", ">i", &gameTime)) { 338 | // Convert gameTime from milliseconds to the expected time format and update the timer 339 | atomic_store(&game_time_value, (long long)gameTime * 1000); 340 | atomic_store(&update_game_time, true); 341 | } 342 | lua_pop(L, 1); // Remove the return value from the stack 343 | } 344 | 345 | void run_auto_splitter() 346 | { 347 | lua_State* L = luaL_newstate(); 348 | luaL_openlibs(L); 349 | disable_functions(L, disabled_functions); 350 | lua_pushcfunction(L, find_process_id); 351 | lua_setglobal(L, "process"); 352 | lua_pushcfunction(L, get_base_address); 353 | lua_setglobal(L, "getBaseAddress"); 354 | lua_pushcfunction(L, read_address); 355 | lua_setglobal(L, "readAddress"); 356 | lua_pushcfunction(L, size_of); 357 | lua_setglobal(L, "sizeOf"); 358 | lua_pushcfunction(L, perform_sig_scan); 359 | lua_setglobal(L, "sig_scan"); 360 | lua_pushcfunction(L, getPid); 361 | lua_setglobal(L, "getPID"); 362 | lua_pushcfunction(L, lua_get_module_size); 363 | lua_setglobal(L, "getModuleSize"); 364 | 365 | char current_file[PATH_MAX]; 366 | strcpy(current_file, auto_splitter_file); 367 | 368 | // Load the Lua file 369 | if (luaL_loadfile(L, auto_splitter_file) != LUA_OK) { 370 | // Error loading the file 371 | const char* error_msg = lua_tostring(L, -1); 372 | lua_pop(L, 1); // Remove the error message from the stack 373 | fprintf(stderr, "Lua syntax error: %s\n", error_msg); 374 | lua_close(L); 375 | atomic_store(&auto_splitter_enabled, false); 376 | return; 377 | } 378 | 379 | // Execute the Lua file 380 | if (lua_pcall(L, 0, LUA_MULTRET, 0) != LUA_OK) { 381 | // Error executing the file 382 | const char* error_msg = lua_tostring(L, -1); 383 | lua_pop(L, 1); // Remove the error message from the stack 384 | fprintf(stderr, "Lua runtime error: %s\n", error_msg); 385 | lua_close(L); 386 | atomic_store(&auto_splitter_enabled, false); 387 | return; 388 | } 389 | 390 | lua_getglobal(L, "state"); 391 | bool state_exists = lua_isfunction(L, -1); 392 | lua_pop(L, 1); // Remove 'state' from the stack 393 | 394 | lua_getglobal(L, "start"); 395 | bool start_exists = lua_isfunction(L, -1); 396 | lua_pop(L, 1); // Remove 'start' from the stack 397 | 398 | lua_getglobal(L, "split"); 399 | bool split_exists = lua_isfunction(L, -1); 400 | lua_pop(L, 1); // Remove 'split' from the stack 401 | 402 | lua_getglobal(L, "isLoading"); 403 | bool is_loading_exists = lua_isfunction(L, -1); 404 | lua_pop(L, 1); // Remove 'isLoading' from the stack 405 | 406 | lua_getglobal(L, "startup"); 407 | bool startup_exists = lua_isfunction(L, -1); 408 | lua_pop(L, 1); // Remove 'startup' from the stack 409 | 410 | lua_getglobal(L, "reset"); 411 | bool reset_exists = lua_isfunction(L, -1); 412 | lua_pop(L, 1); // Remove 'reset' from the stack 413 | 414 | lua_getglobal(L, "update"); 415 | bool update_exists = lua_isfunction(L, -1); 416 | lua_pop(L, 1); // Remove 'update' from the stack 417 | 418 | lua_getglobal(L, "gameTime"); 419 | bool gameTime_exists = lua_isfunction(L, -1); 420 | lua_pop(L, 1); // Remove 'gameTime' from the stack 421 | 422 | if (startup_exists) { 423 | startup(L); 424 | } 425 | 426 | printf("Refresh rate: %d\n", refresh_rate); 427 | int rate = 1000000 / refresh_rate; 428 | 429 | while (1) { 430 | struct timespec clock_start; 431 | clock_gettime(CLOCK_MONOTONIC, &clock_start); 432 | 433 | if (!atomic_load(&auto_splitter_enabled) || strcmp(current_file, auto_splitter_file) != 0 || !process_exists() || process.pid == 0) { 434 | break; 435 | } 436 | 437 | if (state_exists) { 438 | state(L); 439 | } 440 | 441 | if (update_exists) { 442 | update(L); 443 | } 444 | 445 | if (gameTime_exists && use_game_time && atomic_load(&run_started) && !atomic_load(&run_finished)) { 446 | gameTime(L); 447 | } 448 | 449 | if (start_exists && !atomic_load(&run_started) && !atomic_load(&run_finished)) { 450 | start(L); 451 | } 452 | 453 | if (split_exists && atomic_load(&run_started)) { 454 | split(L); 455 | } 456 | 457 | if (is_loading_exists) { 458 | is_loading(L); 459 | } 460 | 461 | if (reset_exists) { 462 | reset(L); 463 | } 464 | 465 | // Clear the memory maps cache if needed 466 | maps_cache_cycles_value--; 467 | if (maps_cache_cycles_value < 1) { 468 | p_maps_cache_size = 0; // We dont need to "empty" the list as the elements after index 0 are considered invalid 469 | maps_cache_cycles_value = maps_cache_cycles; 470 | // printf("Cleared maps cache\n"); 471 | } 472 | 473 | struct timespec clock_end; 474 | clock_gettime(CLOCK_MONOTONIC, &clock_end); 475 | long long duration = (clock_end.tv_sec - clock_start.tv_sec) * 1000000 + (clock_end.tv_nsec - clock_start.tv_nsec) / 1000; 476 | // printf("duration: %llu\n", duration); 477 | if (duration < rate) { 478 | usleep(rate - duration); 479 | } 480 | } 481 | 482 | lua_close(L); 483 | } 484 | -------------------------------------------------------------------------------- /src/component/splits.c: -------------------------------------------------------------------------------- 1 | #include "components.h" 2 | #include 3 | 4 | typedef struct _LSSplits { 5 | LSComponent base; 6 | int split_count; 7 | GtkWidget* container; 8 | GtkWidget* splits; 9 | GtkWidget* split_last; 10 | GtkAdjustment* split_adjust; 11 | GtkWidget* split_scroller; 12 | GtkWidget* split_viewport; 13 | GtkWidget** split_rows; 14 | GtkWidget** split_titles; 15 | GtkWidget** split_icons; 16 | GtkWidget** split_deltas; 17 | GtkWidget** split_times; 18 | GtkCssProvider* icons_css_provider; 19 | } LSSplits; 20 | extern LSComponentOps ls_splits_operations; 21 | 22 | LSComponent* ls_component_splits_new() 23 | { 24 | LSSplits* self; 25 | 26 | self = malloc(sizeof(LSSplits)); 27 | if (!self) { 28 | return NULL; 29 | } 30 | self->base.ops = &ls_splits_operations; 31 | 32 | self->split_adjust = gtk_adjustment_new(0., 0., 0., 0., 0., 0.); 33 | 34 | self->split_scroller = gtk_scrolled_window_new(NULL, self->split_adjust); 35 | gtk_widget_set_vexpand(self->split_scroller, TRUE); 36 | gtk_widget_set_hexpand(self->split_scroller, TRUE); 37 | gtk_widget_show(self->split_scroller); 38 | gtk_widget_add_events(self->split_scroller, GDK_SCROLL_MASK); 39 | 40 | self->split_viewport = gtk_viewport_new(NULL, NULL); 41 | gtk_container_add(GTK_CONTAINER(self->split_scroller), 42 | self->split_viewport); 43 | gtk_widget_show(self->split_viewport); 44 | 45 | self->splits = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 46 | add_class(self->splits, "splits"); 47 | gtk_widget_set_hexpand(self->splits, TRUE); 48 | gtk_container_add(GTK_CONTAINER(self->split_viewport), self->splits); 49 | gtk_widget_show(self->splits); 50 | 51 | self->icons_css_provider = NULL; 52 | 53 | self->split_last = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 54 | add_class(self->split_last, "split-last"); 55 | gtk_widget_set_hexpand(self->split_last, TRUE); 56 | gtk_widget_show(self->split_last); 57 | 58 | self->container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 59 | gtk_container_add(GTK_CONTAINER(self->container), self->split_scroller); 60 | gtk_container_add(GTK_CONTAINER(self->container), self->split_last); 61 | gtk_widget_show(self->container); 62 | return (LSComponent*)self; 63 | } 64 | 65 | static void splits_delete(LSComponent* self) 66 | { 67 | free(self); 68 | } 69 | 70 | static GtkWidget* splits_widget(LSComponent* self) 71 | { 72 | return ((LSSplits*)self)->container; 73 | } 74 | 75 | static void splits_trailer(LSComponent* self_) 76 | { 77 | LSSplits* self = (LSSplits*)self_; 78 | int height, split_h, last = self->split_count - 1; 79 | double curr_scroll = gtk_adjustment_get_value(self->split_adjust); 80 | double scroll_max = gtk_adjustment_get_upper(self->split_adjust); 81 | double page_size = gtk_adjustment_get_page_size(self->split_adjust); 82 | g_object_ref(self->split_rows[last]); 83 | split_h = gtk_widget_get_allocated_height(self->split_titles[last]); 84 | height = gtk_widget_get_allocated_height(self->splits); 85 | if (gtk_widget_get_parent(self->split_rows[last]) == self->splits) { 86 | if (curr_scroll + page_size < scroll_max) { 87 | // move last split to split_last 88 | gtk_container_remove(GTK_CONTAINER(self->splits), 89 | self->split_rows[last]); 90 | gtk_container_add(GTK_CONTAINER(self->split_last), 91 | self->split_rows[last]); 92 | gtk_widget_show(self->split_last); 93 | } 94 | } else { 95 | if (curr_scroll + page_size == scroll_max) { 96 | // move last split to split box 97 | gtk_container_remove(GTK_CONTAINER(self->split_last), 98 | self->split_rows[last]); 99 | gtk_container_add(GTK_CONTAINER(self->splits), 100 | self->split_rows[last]); 101 | gtk_adjustment_set_upper(self->split_adjust, 102 | scroll_max + height); 103 | gtk_adjustment_set_value(self->split_adjust, 104 | curr_scroll + split_h); 105 | gtk_widget_hide(self->split_last); 106 | } 107 | } 108 | g_object_unref(self->split_rows[last]); 109 | } 110 | 111 | static void splits_show_game(LSComponent* self_, const ls_game* game, 112 | const ls_timer* timer) 113 | { 114 | LSSplits* self = (LSSplits*)self_; 115 | char str[256]; 116 | int i; 117 | self->split_count = game->split_count; 118 | 119 | self->split_rows = calloc(self->split_count, sizeof(GtkWidget*)); 120 | if (!self->split_rows) 121 | return; 122 | 123 | self->split_titles = calloc(self->split_count, sizeof(GtkWidget*)); 124 | if (!self->split_titles) { 125 | free(self->split_rows); 126 | return; 127 | } 128 | 129 | self->split_icons = calloc(self->split_count, sizeof(GtkWidget*)); 130 | if (!self->split_titles) { 131 | free(self->split_rows); 132 | return; 133 | } 134 | 135 | self->split_deltas = calloc(self->split_count, sizeof(GtkWidget*)); 136 | if (!self->split_deltas) { 137 | free(self->split_rows); 138 | free(self->split_titles); 139 | return; 140 | } 141 | 142 | self->split_times = calloc(self->split_count, sizeof(GtkWidget*)); 143 | if (!self->split_times) { 144 | free(self->split_rows); 145 | free(self->split_titles); 146 | free(self->split_deltas); 147 | return; 148 | } 149 | 150 | 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; }"); 151 | 152 | for (i = 0; i < self->split_count; ++i) { 153 | self->split_rows[i] = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 154 | add_class(self->split_rows[i], "split"); 155 | gtk_widget_set_hexpand(self->split_rows[i], TRUE); 156 | gtk_container_add(GTK_CONTAINER(self->splits), 157 | self->split_rows[i]); 158 | 159 | self->split_titles[i] = gtk_label_new(game->split_titles[i]); 160 | add_class(self->split_titles[i], "split-title"); 161 | gtk_widget_set_halign(self->split_titles[i], GTK_ALIGN_START); 162 | gtk_widget_set_hexpand(self->split_titles[i], TRUE); 163 | 164 | if (game->split_titles[i] 165 | && strlen(game->split_titles[i])) { 166 | char* c = &str[12]; 167 | strcpy(str, "split-title-"); 168 | strcpy(c, game->split_titles[i]); 169 | do { 170 | if (!isalnum(*c)) { 171 | *c = '-'; 172 | } else { 173 | *c = tolower(*c); 174 | } 175 | } while (*++c != '\0'); 176 | { 177 | add_class(self->split_rows[i], str); 178 | } 179 | } 180 | 181 | if (game->contains_icons) { 182 | if (game->split_icon_paths[i]) { 183 | // g_string_append_printf(icons_css_src, ".split:nth-child(%d) .split-icon { background-image: url('%s'); }", i+1, game->split_icon_paths[i]); 184 | g_string_append_printf( 185 | icons_css_src, 186 | ".%s .split-icon { background-image: url('%s'); }", 187 | str, game->split_icon_paths[i]); 188 | } 189 | self->split_icons[i] = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 190 | add_class(self->split_icons[i], "split-icon"); 191 | // set size but allow to dinamically change it from css with min-width and min-height 192 | gtk_widget_set_size_request(self->split_icons[i], 20, 20); 193 | gtk_container_add(GTK_CONTAINER(self->split_rows[i]), self->split_icons[i]); 194 | gtk_widget_show(self->split_icons[i]); 195 | } 196 | gtk_container_add(GTK_CONTAINER(self->split_rows[i]), self->split_titles[i]); 197 | 198 | self->split_deltas[i] = gtk_label_new(NULL); 199 | add_class(self->split_deltas[i], "split-delta"); 200 | gtk_widget_set_size_request(self->split_deltas[i], 1, -1); 201 | gtk_container_add(GTK_CONTAINER(self->split_rows[i]), 202 | self->split_deltas[i]); 203 | 204 | self->split_times[i] = gtk_label_new(NULL); 205 | add_class(self->split_times[i], "split-time"); 206 | gtk_widget_set_halign(self->split_times[i], GTK_ALIGN_END); 207 | gtk_container_add(GTK_CONTAINER(self->split_rows[i]), 208 | self->split_times[i]); 209 | 210 | if (game->split_times[i]) { 211 | ls_split_string(str, game->split_times[i], 0); 212 | gtk_label_set_text(GTK_LABEL(self->split_times[i]), str); 213 | } 214 | 215 | gtk_widget_show_all(self->split_rows[i]); 216 | } 217 | 218 | if (self->icons_css_provider) { 219 | // remove old css provider 220 | gtk_style_context_remove_provider_for_screen( 221 | gdk_screen_get_default(), 222 | GTK_STYLE_PROVIDER(self->icons_css_provider)); 223 | g_object_unref(self->icons_css_provider); 224 | self->icons_css_provider = NULL; 225 | } 226 | 227 | if (icons_css_src->len > 0) { 228 | self->icons_css_provider = gtk_css_provider_new(); 229 | gtk_css_provider_load_from_data( 230 | self->icons_css_provider, 231 | icons_css_src->str, 232 | icons_css_src->len, 233 | NULL); 234 | // add new css provider 235 | gtk_style_context_add_provider_for_screen( 236 | gdk_screen_get_default(), 237 | GTK_STYLE_PROVIDER(self->icons_css_provider), 238 | GTK_STYLE_PROVIDER_PRIORITY_USER); 239 | g_string_free(icons_css_src, TRUE); 240 | } 241 | 242 | gtk_widget_show(self->splits); 243 | splits_trailer(self_); 244 | } 245 | 246 | static void splits_clear_game(LSComponent* self_) 247 | { 248 | LSSplits* self = (LSSplits*)self_; 249 | int i; 250 | gtk_widget_hide(self->splits); 251 | gtk_widget_hide(self->split_last); 252 | for (i = self->split_count - 1; i >= 0; --i) { 253 | gtk_container_remove( 254 | GTK_CONTAINER(gtk_widget_get_parent(self->split_rows[i])), 255 | self->split_rows[i]); 256 | } 257 | gtk_adjustment_set_value(self->split_adjust, 0); 258 | free(self->split_rows); 259 | free(self->split_titles); 260 | free(self->split_deltas); 261 | free(self->split_times); 262 | self->split_count = 0; 263 | } 264 | 265 | #define SHOW_DELTA_THRESHOLD (-30 * 1000000LL) 266 | static void splits_draw(LSComponent* self_, const ls_game* game, const ls_timer* timer) 267 | { 268 | LSSplits* self = (LSSplits*)self_; 269 | char str[256]; 270 | int i; 271 | for (i = 0; i < self->split_count; ++i) { 272 | if (i == timer->curr_split 273 | && timer->start_time) { 274 | add_class(self->split_rows[i], "current-split"); 275 | } else { 276 | remove_class(self->split_rows[i], "current-split"); 277 | } 278 | 279 | remove_class(self->split_times[i], "time"); 280 | remove_class(self->split_times[i], "done"); 281 | 282 | // Set split_times label to - 283 | gtk_label_set_text(GTK_LABEL(self->split_times[i]), "-"); 284 | 285 | if (i < timer->curr_split) { 286 | add_class(self->split_times[i], "done"); 287 | if (timer->split_times[i]) { 288 | add_class(self->split_times[i], "time"); 289 | ls_split_string(str, timer->split_times[i], 0); 290 | gtk_label_set_text(GTK_LABEL(self->split_times[i]), str); 291 | } 292 | } else if (game->split_times[i]) { 293 | add_class(self->split_times[i], "time"); 294 | ls_split_string(str, game->split_times[i], 0); 295 | gtk_label_set_text(GTK_LABEL(self->split_times[i]), str); 296 | } 297 | 298 | remove_class(self->split_deltas[i], "best-split"); 299 | remove_class(self->split_deltas[i], "best-segment"); 300 | remove_class(self->split_deltas[i], "behind"); 301 | remove_class(self->split_deltas[i], "losing"); 302 | remove_class(self->split_deltas[i], "delta"); 303 | gtk_label_set_text(GTK_LABEL(self->split_deltas[i]), ""); 304 | if (i < timer->curr_split 305 | || timer->split_deltas[i] >= SHOW_DELTA_THRESHOLD) { 306 | if (timer->split_info[i] & LS_INFO_BEST_SPLIT) { 307 | add_class(self->split_deltas[i], "best-split"); 308 | } 309 | if (timer->split_info[i] & LS_INFO_BEST_SEGMENT) { 310 | add_class(self->split_deltas[i], "best-segment"); 311 | } 312 | if (timer->split_info[i] & LS_INFO_BEHIND_TIME) { 313 | add_class(self->split_deltas[i], "behind"); 314 | if (timer->split_info[i] 315 | & LS_INFO_LOSING_TIME) { 316 | add_class(self->split_deltas[i], "losing"); 317 | } 318 | } else { 319 | remove_class(self->split_deltas[i], "behind"); 320 | if (timer->split_info[i] 321 | & LS_INFO_LOSING_TIME) { 322 | add_class(self->split_deltas[i], "losing"); 323 | } 324 | } 325 | if (timer->split_deltas[i]) { 326 | add_class(self->split_deltas[i], "delta"); 327 | ls_delta_string(str, timer->split_deltas[i]); 328 | gtk_label_set_text(GTK_LABEL(self->split_deltas[i]), str); 329 | } 330 | } 331 | } 332 | 333 | // keep split sizes in sync 334 | if (self->split_count) { 335 | int width; 336 | int time_width = 0, delta_width = 0; 337 | for (i = 0; i < self->split_count; ++i) { 338 | width = gtk_widget_get_allocated_width(self->split_deltas[i]); 339 | if (width > delta_width) { 340 | delta_width = width; 341 | } 342 | width = gtk_widget_get_allocated_width(self->split_times[i]); 343 | if (width > time_width) { 344 | time_width = width; 345 | } 346 | } 347 | for (i = 0; i < self->split_count; ++i) { 348 | if (delta_width) { 349 | gtk_widget_set_size_request( 350 | self->split_deltas[i], delta_width, -1); 351 | } 352 | if (time_width) { 353 | width = gtk_widget_get_allocated_width( 354 | self->split_times[i]); 355 | gtk_widget_set_margin_start(self->split_times[i], 356 | /*WINDOW_PAD*/ 8 * 2 + (time_width - width)); 357 | } 358 | } 359 | } 360 | 361 | splits_trailer(self_); 362 | } 363 | 364 | static void splits_scroll_to_split(LSComponent* self_, const ls_timer* timer) 365 | { 366 | LSSplits* self = (LSSplits*)self_; 367 | int split_x, split_y; 368 | int split_h; 369 | int scroller_h; 370 | double curr_scroll; 371 | double min_scroll, max_scroll; 372 | int prev = timer->curr_split - 1; 373 | int curr = timer->curr_split; 374 | int next = timer->curr_split + 1; 375 | if (prev < 0) { 376 | prev = 0; 377 | } 378 | if (curr >= self->split_count) { 379 | curr = self->split_count - 1; 380 | } 381 | if (next >= self->split_count) { 382 | next = self->split_count - 1; 383 | } 384 | curr_scroll = gtk_adjustment_get_value(self->split_adjust); 385 | gtk_widget_translate_coordinates( 386 | self->split_titles[prev], 387 | self->split_viewport, 388 | 0, 0, &split_x, &split_y); 389 | scroller_h = gtk_widget_get_allocated_height(self->split_scroller); 390 | split_h = gtk_widget_get_allocated_height(self->split_titles[prev]); 391 | if (curr != next && curr != prev) { 392 | split_h += gtk_widget_get_allocated_height(self->split_titles[curr]); 393 | } 394 | if (next != prev) { 395 | int h = gtk_widget_get_allocated_height(self->split_titles[next]); 396 | if (split_h + h < scroller_h) { 397 | split_h += h; 398 | } 399 | } 400 | min_scroll = split_y + curr_scroll - scroller_h + split_h; 401 | max_scroll = split_y + curr_scroll; 402 | if (curr_scroll > max_scroll) { 403 | gtk_adjustment_set_value(self->split_adjust, max_scroll); 404 | } else if (curr_scroll < min_scroll) { 405 | gtk_adjustment_set_value(self->split_adjust, min_scroll); 406 | } 407 | } 408 | 409 | void splits_start_split(LSComponent* self, const ls_timer* timer) 410 | { 411 | splits_scroll_to_split(self, timer); 412 | } 413 | 414 | void splits_skip(LSComponent* self, const ls_timer* timer) 415 | { 416 | splits_scroll_to_split(self, timer); 417 | } 418 | 419 | void splits_unsplit(LSComponent* self, const ls_timer* timer) 420 | { 421 | splits_scroll_to_split(self, timer); 422 | } 423 | 424 | LSComponentOps ls_splits_operations = { 425 | .delete = splits_delete, 426 | .widget = splits_widget, 427 | .show_game = splits_show_game, 428 | .clear_game = splits_clear_game, 429 | .draw = splits_draw, 430 | .start_split = splits_start_split, 431 | .skip = splits_skip, 432 | .unsplit = splits_unsplit 433 | }; 434 | -------------------------------------------------------------------------------- /assets/libresplit.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 13 | 15 | 19 | 24 | 25 | 32 | 36 | 41 | 42 | 49 | 53 | 58 | 59 | 68 | 75 | 79 | 84 | 85 | 93 | 94 | 100 | 106 | 113 | 120 | 127 | 130 | 138 | 147 | 156 | 166 | 170 | 175 | 179 | 182 | 185 | 188 | 192 | 195 | 198 | 199 | 200 | 205 | 209 | 212 | 215 | 218 | 222 | 225 | 228 | 229 | 230 | 235 | 239 | 242 | 245 | 248 | 252 | 255 | 258 | 259 | 260 | 265 | 269 | 272 | 275 | 278 | 282 | 285 | 288 | 289 | 290 | 293 | 294 | 295 | 302 | 309 | 315 | 320 | 325 | 330 | 331 | -------------------------------------------------------------------------------- /src/bind.c: -------------------------------------------------------------------------------- 1 | /* bind.c 2 | * Copyright (C) 2008 Alex Graveley 3 | * Copyright (C) 2010 Ulrik Sverdrup 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining 6 | * a copy of this software and associated documentation files (the 7 | * "Software"), to deal in the Software without restriction, including 8 | * without limitation the rights to use, copy, modify, merge, publish, 9 | * distribute, sublicense, and/or sell copies of the Software, and to 10 | * permit persons to whom the Software is furnished to do so, subject to 11 | * the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | #include 25 | #include 26 | #include 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | #include "bind.h" 35 | 36 | /* Uncomment the next line to print a debug trace. */ 37 | /* #define DEBUG */ 38 | 39 | #ifdef DEBUG 40 | #define TRACE(x) x 41 | #else 42 | #define TRACE(x) \ 43 | do { \ 44 | } while (FALSE); 45 | #endif 46 | 47 | #define MODIFIERS_ERROR ((GdkModifierType)(-1)) 48 | #define MODIFIERS_NONE 0 49 | 50 | /* Group to use: Which of configured keyboard Layouts 51 | * Since grabbing a key blocks its use, we can't grab the corresponding 52 | * (physical) keys for alternative layouts. 53 | * 54 | * Because of this, we interpret all keys relative to the default 55 | * keyboard layout. 56 | * 57 | * For example, if you bind "w", the physical W key will respond to 58 | * the bound key, even if you switch to a keyboard layout where the W key 59 | * types a different letter. 60 | */ 61 | #define WE_ONLY_USE_ONE_GROUP 0 62 | 63 | struct Binding { 64 | KeybinderHandler handler; 65 | void* user_data; 66 | char* keystring; 67 | GDestroyNotify notify; 68 | /* GDK "distilled" values */ 69 | guint keyval; 70 | GdkModifierType modifiers; 71 | }; 72 | 73 | static GSList* bindings = NULL; 74 | static guint32 last_event_time = 0; 75 | static gboolean processing_event = FALSE; 76 | 77 | /* Return the modifier mask that needs to be pressed to produce key in the 78 | * given group (keyboard layout) and level ("shift level"). 79 | */ 80 | static GdkModifierType 81 | FinallyGetModifiersForKeycode(XkbDescPtr xkb, 82 | KeyCode key, 83 | uint group, 84 | uint level) 85 | { 86 | int nKeyGroups; 87 | int effectiveGroup; 88 | XkbKeyTypeRec* type; 89 | int k; 90 | 91 | nKeyGroups = XkbKeyNumGroups(xkb, key); 92 | if ((!XkbKeycodeInRange(xkb, key)) || (nKeyGroups == 0)) { 93 | return MODIFIERS_ERROR; 94 | } 95 | 96 | /* Taken from GDK's MyEnhancedXkbTranslateKeyCode */ 97 | /* find the offset of the effective group */ 98 | effectiveGroup = group; 99 | if (effectiveGroup >= nKeyGroups) { 100 | unsigned groupInfo = XkbKeyGroupInfo(xkb, key); 101 | switch (XkbOutOfRangeGroupAction(groupInfo)) { 102 | default: 103 | effectiveGroup %= nKeyGroups; 104 | break; 105 | case XkbClampIntoRange: 106 | effectiveGroup = nKeyGroups - 1; 107 | break; 108 | case XkbRedirectIntoRange: 109 | effectiveGroup = XkbOutOfRangeGroupNumber(groupInfo); 110 | if (effectiveGroup >= nKeyGroups) 111 | effectiveGroup = 0; 112 | break; 113 | } 114 | } 115 | type = XkbKeyKeyType(xkb, key, effectiveGroup); 116 | for (k = 0; k < type->map_count; k++) { 117 | if (type->map[k].active && type->map[k].level == level) { 118 | if (type->preserve) { 119 | return (type->map[k].mods.mask & ~type->preserve[k].mask); 120 | } else { 121 | return type->map[k].mods.mask; 122 | } 123 | } 124 | } 125 | return MODIFIERS_NONE; 126 | } 127 | 128 | /* Grab or ungrab the keycode+modifiers combination, first plainly, and then 129 | * including each ignorable modifier in turn. 130 | */ 131 | static gboolean 132 | grab_ungrab_with_ignorable_modifiers(GdkWindow* rootwin, 133 | uint keycode, 134 | uint modifiers, 135 | gboolean grab) 136 | { 137 | guint i; 138 | gboolean success = FALSE; 139 | GdkDisplay* display = gdk_display_get_default(); 140 | 141 | /* Ignorable modifiers */ 142 | guint mod_masks[] = { 143 | 0, /* modifier only */ 144 | GDK_MOD2_MASK, 145 | GDK_LOCK_MASK, 146 | GDK_MOD2_MASK | GDK_LOCK_MASK, 147 | }; 148 | 149 | gdk_x11_display_error_trap_push(display); 150 | 151 | for (i = 0; i < G_N_ELEMENTS(mod_masks); i++) { 152 | if (grab) { 153 | XGrabKey(GDK_WINDOW_XDISPLAY(rootwin), 154 | keycode, 155 | modifiers | mod_masks[i], 156 | GDK_WINDOW_XID(rootwin), 157 | True, 158 | GrabModeSync, 159 | GrabModeSync); 160 | } else { 161 | XUngrabKey(GDK_WINDOW_XDISPLAY(rootwin), 162 | keycode, 163 | modifiers | mod_masks[i], 164 | GDK_WINDOW_XID(rootwin)); 165 | } 166 | } 167 | gdk_display_flush(display); 168 | if (gdk_x11_display_error_trap_pop(display)) { 169 | TRACE(g_warning("Failed grab/ungrab")); 170 | if (grab) { 171 | /* On error, immediately release keys again */ 172 | grab_ungrab_with_ignorable_modifiers(rootwin, 173 | keycode, 174 | modifiers, 175 | FALSE); 176 | } 177 | } else { 178 | success = TRUE; 179 | } 180 | return success; 181 | } 182 | 183 | /* Grab or ungrab then keyval and modifiers combination, grabbing all key 184 | * combinations yielding the same key values. 185 | * Includes ignorable modifiers using grab_ungrab_with_ignorable_modifiers. 186 | */ 187 | static gboolean 188 | grab_ungrab(GdkWindow* rootwin, 189 | uint keyval, 190 | uint modifiers, 191 | gboolean grab) 192 | { 193 | int k; 194 | GdkKeymap* map; 195 | GdkKeymapKey* keys; 196 | gint n_keys; 197 | GdkModifierType add_modifiers; 198 | XkbDescPtr xmap; 199 | gboolean success = FALSE; 200 | 201 | xmap = XkbGetMap(GDK_WINDOW_XDISPLAY(rootwin), 202 | XkbAllClientInfoMask, 203 | XkbUseCoreKbd); 204 | 205 | GdkDisplay* display = gdk_display_get_default(); 206 | map = gdk_keymap_get_for_display(display); 207 | gdk_keymap_get_entries_for_keyval(map, keyval, &keys, &n_keys); 208 | 209 | if (n_keys == 0) 210 | return FALSE; 211 | 212 | for (k = 0; k < n_keys; k++) { 213 | /* NOTE: We only bind for the first group, 214 | * so regardless of current keyboard layout, it will 215 | * grab the key from the default Layout. 216 | */ 217 | if (keys[k].group != WE_ONLY_USE_ONE_GROUP) { 218 | continue; 219 | } 220 | 221 | add_modifiers = FinallyGetModifiersForKeycode(xmap, 222 | keys[k].keycode, 223 | keys[k].group, 224 | keys[k].level); 225 | 226 | if (add_modifiers == MODIFIERS_ERROR) { 227 | continue; 228 | } 229 | TRACE(g_print("grab/ungrab keycode: %d, lev: %d, grp: %d, ", 230 | keys[k].keycode, keys[k].level, keys[k].group)); 231 | TRACE(g_print("modifiers: 0x%x (consumed: 0x%x)\n", 232 | add_modifiers | modifiers, add_modifiers)); 233 | if (grab_ungrab_with_ignorable_modifiers(rootwin, 234 | keys[k].keycode, 235 | add_modifiers | modifiers, 236 | grab)) { 237 | 238 | success = TRUE; 239 | } else { 240 | /* When grabbing, break on error */ 241 | if (grab && !success) { 242 | break; 243 | } 244 | } 245 | } 246 | g_free(keys); 247 | XkbFreeClientMap(xmap, 0, TRUE); 248 | 249 | return success; 250 | } 251 | 252 | static gboolean 253 | keyvalues_equal(guint kv1, guint kv2) 254 | { 255 | return kv1 == kv2; 256 | } 257 | 258 | /* Compare modifier set equality, 259 | * while accepting overloaded modifiers (MOD1 and META together) 260 | */ 261 | static gboolean 262 | modifiers_equal(GdkModifierType mf1, GdkModifierType mf2) 263 | { 264 | GdkModifierType ignored = 0; 265 | 266 | /* Accept MOD1 + META as MOD1 */ 267 | if (mf1 & mf2 & GDK_MOD1_MASK) { 268 | ignored |= GDK_META_MASK; 269 | } 270 | /* Accept SUPER + HYPER as SUPER */ 271 | if (mf1 & mf2 & GDK_SUPER_MASK) { 272 | ignored |= GDK_HYPER_MASK; 273 | } 274 | if ((mf1 & ~ignored) == (mf2 & ~ignored)) { 275 | return TRUE; 276 | } 277 | return FALSE; 278 | } 279 | 280 | static gboolean 281 | do_grab_key(struct Binding* binding) 282 | { 283 | gboolean success; 284 | GdkWindow* rootwin = gdk_get_default_root_window(); 285 | GdkDisplay* display = gdk_display_get_default(); 286 | GdkKeymap* keymap = gdk_keymap_get_for_display(display); 287 | 288 | GdkModifierType modifiers; 289 | guint keysym = 0; 290 | 291 | if (keymap == NULL || rootwin == NULL) { 292 | return FALSE; 293 | } 294 | 295 | gtk_accelerator_parse(binding->keystring, &keysym, &modifiers); 296 | 297 | if (keysym == 0) { 298 | return FALSE; 299 | } 300 | 301 | binding->keyval = keysym; 302 | binding->modifiers = modifiers; 303 | TRACE(g_print("Grabbing keyval: %d, vmodifiers: 0x%x, name: %s\n", 304 | keysym, modifiers, binding->keystring)); 305 | 306 | /* Map virtual modifiers to non-virtual modifiers */ 307 | gdk_keymap_map_virtual_modifiers(keymap, &modifiers); 308 | 309 | if (modifiers == binding->modifiers && (GDK_SUPER_MASK | GDK_HYPER_MASK | GDK_META_MASK) & modifiers) { 310 | g_warning("Failed to map virtual modifiers"); 311 | return FALSE; 312 | } 313 | 314 | success = grab_ungrab(rootwin, keysym, modifiers, TRUE /* grab */); 315 | 316 | if (!success) { 317 | g_warning("Binding '%s' failed!", binding->keystring); 318 | } 319 | 320 | return success; 321 | } 322 | 323 | static gboolean 324 | do_ungrab_key(struct Binding* binding) 325 | { 326 | GdkDisplay* display = gdk_display_get_default(); 327 | GdkKeymap* keymap = gdk_keymap_get_for_display(display); 328 | GdkWindow* rootwin = gdk_get_default_root_window(); 329 | GdkModifierType modifiers; 330 | 331 | if (keymap == NULL || rootwin == NULL) { 332 | return FALSE; 333 | } 334 | 335 | TRACE(g_print("Ungrabbing keyval: %d, vmodifiers: 0x%x, name: %s\n", 336 | binding->keyval, binding->modifiers, binding->keystring)); 337 | 338 | /* Map virtual modifiers to non-virtual modifiers */ 339 | modifiers = binding->modifiers; 340 | gdk_keymap_map_virtual_modifiers(keymap, &modifiers); 341 | 342 | grab_ungrab(rootwin, binding->keyval, modifiers, FALSE /* ungrab */); 343 | return TRUE; 344 | } 345 | 346 | static GdkFilterReturn 347 | filter_func(GdkXEvent* gdk_xevent, GdkEvent* event, gpointer data) 348 | { 349 | XEvent* xevent = (XEvent*)gdk_xevent; 350 | GdkDisplay* display = gdk_display_get_default(); 351 | GdkKeymap* keymap = gdk_keymap_get_for_display(display); 352 | guint keyval; 353 | GdkModifierType consumed, modifiers; 354 | guint mod_mask = gtk_accelerator_get_default_mod_mask(); 355 | GSList* iter; 356 | GdkWindow* rootwin = data; 357 | 358 | //(void) event; 359 | (void)data; 360 | 361 | switch (xevent->type) { 362 | case KeyPress: 363 | modifiers = xevent->xkey.state; 364 | 365 | TRACE(g_print("Got KeyPress keycode: %d, modifiers: 0x%x\n", 366 | xevent->xkey.keycode, 367 | xevent->xkey.state)); 368 | 369 | gdk_keymap_translate_keyboard_state( 370 | keymap, 371 | xevent->xkey.keycode, 372 | modifiers, 373 | /* See top comment why we don't use this here: 374 | XkbGroupForCoreState (xevent->xkey.state) 375 | */ 376 | WE_ONLY_USE_ONE_GROUP, 377 | &keyval, NULL, NULL, &consumed); 378 | 379 | /* Map non-virtual to virtual modifiers */ 380 | modifiers &= ~consumed; 381 | gdk_keymap_add_virtual_modifiers(keymap, &modifiers); 382 | modifiers &= mod_mask; 383 | 384 | TRACE(g_print("Translated keyval: %d, vmodifiers: 0x%x, name: %s\n", 385 | keyval, modifiers, 386 | gtk_accelerator_name(keyval, modifiers))); 387 | 388 | /* 389 | * Set the last event time for use when showing 390 | * windows to avoid anti-focus-stealing code. 391 | */ 392 | processing_event = TRUE; 393 | last_event_time = xevent->xkey.time; 394 | 395 | iter = bindings; 396 | while (iter != NULL) { 397 | /* NOTE: ``iter`` might be removed from the list 398 | * in the callback. 399 | */ 400 | struct Binding* binding = iter->data; 401 | iter = iter->next; 402 | 403 | if (keyvalues_equal(binding->keyval, keyval) && modifiers_equal(binding->modifiers, modifiers)) { 404 | TRACE(g_print("Calling handler for '%s'...\n", 405 | binding->keystring)); 406 | 407 | (binding->handler)(binding->keystring, 408 | binding->user_data); 409 | } 410 | } 411 | 412 | processing_event = FALSE; 413 | break; 414 | case KeyRelease: 415 | TRACE(g_print("Got KeyRelease! \n")); 416 | break; 417 | } 418 | XAllowEvents(GDK_WINDOW_XDISPLAY(rootwin), 419 | ReplayKeyboard, xevent->xkey.time); 420 | XFlush(GDK_WINDOW_XDISPLAY(rootwin)); 421 | 422 | return GDK_FILTER_CONTINUE; 423 | } 424 | 425 | static void 426 | keymap_changed(GdkKeymap* map) 427 | { 428 | GSList* iter; 429 | 430 | (void)map; 431 | 432 | TRACE(g_print("Keymap changed! Regrabbing keys...")); 433 | 434 | for (iter = bindings; iter != NULL; iter = iter->next) { 435 | struct Binding* binding = iter->data; 436 | do_ungrab_key(binding); 437 | } 438 | 439 | for (iter = bindings; iter != NULL; iter = iter->next) { 440 | struct Binding* binding = iter->data; 441 | do_grab_key(binding); 442 | } 443 | } 444 | 445 | /** 446 | * keybinder_init: 447 | * 448 | * Initialize the keybinder library. 449 | * 450 | * This function must be called after initializing GTK, before calling any 451 | * other function in the library. Can only be called once. 452 | */ 453 | void keybinder_init() 454 | { 455 | GdkDisplay* display = gdk_display_get_default(); 456 | GdkKeymap* keymap = gdk_keymap_get_for_display(display); 457 | GdkWindow* rootwin = gdk_get_default_root_window(); 458 | 459 | gdk_window_add_filter(rootwin, filter_func, rootwin); 460 | 461 | /* Workaround: Make sure modmap is up to date 462 | * There is possibly a bug in GTK+ where virtual modifiers are not 463 | * mapped because the modmap is not updated. The following function 464 | * updates it. 465 | */ 466 | (void)gdk_keymap_have_bidi_layouts(keymap); 467 | 468 | g_signal_connect(keymap, 469 | "keys_changed", 470 | G_CALLBACK(keymap_changed), 471 | NULL); 472 | } 473 | 474 | /** 475 | * keybinder_bind: (skip) 476 | * @keystring: an accelerator description (gtk_accelerator_parse() format) 477 | * @handler: callback function 478 | * @user_data: data to pass to @handler 479 | * 480 | * Grab a key combination globally and register a callback to be called each 481 | * time the key combination is pressed. 482 | * 483 | * This function is excluded from introspected bindings and is replaced by 484 | * keybinder_bind_full. 485 | * 486 | * Returns: %TRUE if the accelerator could be grabbed 487 | */ 488 | gboolean 489 | keybinder_bind(const char* keystring, 490 | KeybinderHandler handler, 491 | void* user_data) 492 | { 493 | return keybinder_bind_full(keystring, handler, user_data, NULL); 494 | } 495 | 496 | /** 497 | * keybinder_bind_full: 498 | * @keystring: an accelerator description (gtk_accelerator_parse() format) 499 | * @handler: (scope notified): callback function 500 | * @user_data: (closure) (allow-none): data to pass to @handler 501 | * @notify: (allow-none): called when @handler is unregistered 502 | * 503 | * Grab a key combination globally and register a callback to be called each 504 | * time the key combination is pressed. 505 | * 506 | * Rename to: keybinder_bind 507 | * 508 | * Since: 0.3.0 509 | * 510 | * Returns: %TRUE if the accelerator could be grabbed 511 | */ 512 | gboolean 513 | keybinder_bind_full(const char* keystring, 514 | KeybinderHandler handler, 515 | void* user_data, 516 | GDestroyNotify notify) 517 | { 518 | struct Binding* binding; 519 | gboolean success; 520 | 521 | binding = g_new0(struct Binding, 1); 522 | binding->keystring = g_strdup(keystring); 523 | binding->handler = handler; 524 | binding->user_data = user_data; 525 | binding->notify = notify; 526 | 527 | /* Sets the binding's keycode and modifiers */ 528 | success = do_grab_key(binding); 529 | 530 | if (success) { 531 | bindings = g_slist_prepend(bindings, binding); 532 | } else { 533 | g_free(binding->keystring); 534 | g_free(binding); 535 | } 536 | return success; 537 | } 538 | 539 | /** 540 | * keybinder_unbind: (skip) 541 | * @keystring: an accelerator description (gtk_accelerator_parse() format) 542 | * @handler: callback function 543 | * 544 | * Unregister a specific previously bound callback for this keystring. 545 | * 546 | * This function is excluded from introspected bindings and is replaced by 547 | * keybinder_unbind_all. 548 | */ 549 | void keybinder_unbind(const char* keystring, KeybinderHandler handler) 550 | { 551 | GSList* iter; 552 | 553 | for (iter = bindings; iter != NULL; iter = iter->next) { 554 | struct Binding* binding = iter->data; 555 | 556 | if (strcmp(keystring, binding->keystring) != 0 || handler != binding->handler) 557 | continue; 558 | 559 | do_ungrab_key(binding); 560 | bindings = g_slist_remove(bindings, binding); 561 | 562 | TRACE(g_print("unbind, notify: %p\n", binding->notify)); 563 | if (binding->notify) { 564 | binding->notify(binding->user_data); 565 | } 566 | g_free(binding->keystring); 567 | g_free(binding); 568 | break; 569 | } 570 | } 571 | 572 | /** 573 | * keybinder_unbind_all: 574 | * @keystring: an accelerator description (gtk_accelerator_parse() format) 575 | * 576 | * Unregister all previously bound callbacks for this keystring. 577 | * 578 | * Rename to: keybinder_unbind 579 | * 580 | * Since: 0.3.0 581 | */ 582 | void keybinder_unbind_all(const char* keystring) 583 | { 584 | GSList* iter = bindings; 585 | 586 | for (iter = bindings; iter != NULL; iter = iter->next) { 587 | struct Binding* binding = iter->data; 588 | 589 | if (strcmp(keystring, binding->keystring) != 0) { 590 | continue; 591 | } 592 | 593 | do_ungrab_key(binding); 594 | bindings = g_slist_remove(bindings, binding); 595 | 596 | TRACE(g_print("unbind_all, notify: %p\n", binding->notify)); 597 | if (binding->notify) { 598 | binding->notify(binding->user_data); 599 | } 600 | g_free(binding->keystring); 601 | g_free(binding); 602 | 603 | /* re-start scan from head of new list */ 604 | iter = bindings; 605 | if (!iter) 606 | break; 607 | } 608 | } 609 | 610 | /** 611 | * keybinder_get_current_event_time: 612 | * 613 | * Returns: the current event timestamp 614 | */ 615 | guint32 616 | keybinder_get_current_event_time(void) 617 | { 618 | if (processing_event) { 619 | return last_event_time; 620 | } else { 621 | return GDK_CURRENT_TIME; 622 | } 623 | } 624 | -------------------------------------------------------------------------------- /docs/auto-splitters.md: -------------------------------------------------------------------------------- 1 | # Auto Splitters 2 | 3 | * Auto splitters are scripts that automate the process of timing your run in a game by making splits automatically, starting, resetting, pausing, etc. 4 | 5 | # How do they work? 6 | 7 | * These work by reading into game's memory and determining when the timer should do something, like make a split. 8 | 9 | * LibreSplit's autosplitting system works in a very similar way to LiveSplit's. The main difference is that LibreSplit uses Lua instead of C#. There are also some key differences: 10 | * Runs an entire Lua system instead of only supporting specifically named C# blocks. 11 | * This means you can run external functions outside of the ones LibreSplit executes. 12 | * Support for the entire Lua language, including the importing of libraries for tasks such as performance monitoring. 13 | 14 | # How to make LibreSplit auto splitters 15 | 16 | * It's somewhat easy if you know what you are doing or are porting an already existing one. 17 | 18 | * First in the lua script goes a `process` function call with the name of the games process: 19 | 20 | ```lua 21 | process('GameBlaBlaBla.exe') 22 | ``` 23 | * With this line, LibreSplit will repeatedly attempt to find this process and will not continue script execution until it is found. 24 | 25 | * Next we have to define the basic functions. Not all are required and the ones that are required may change depending on the game or end goal, like if loading screens are included or not. 26 | * The order at which these run is the same as they are documented below. 27 | 28 | ### `startup` 29 | The purpose of this function is to specify how many times LibreSplit checks memory values and executes functions each second, the default is 60Hz. Usually, 60Hz is fine and this function can remain undefined. However, it's there if you need it. Its also useful to change other configuration about the script. 30 | ```lua 31 | process('GameBlaBlaBla.exe') 32 | 33 | function startup() 34 | refreshRate = 120 35 | useGameTime = true 36 | end 37 | ``` 38 | 39 | ### `state` 40 | The main purpose of this function is to assign memory values to Lua variables. 41 | * Runs every 1000 / `refreshRate` milliseconds and when the script is enabled/loaded. 42 | 43 | ```lua 44 | process('GameBlaBlaBla.exe') 45 | 46 | local isLoading = false; 47 | 48 | function startup() 49 | refreshRate = 120 50 | end 51 | 52 | function state() 53 | isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 54 | end 55 | ``` 56 | 57 | * You may have noticed that we're assigning this `isLoading` variable with the result of the function `readAddress`. This function is part of LibreSplit's Lua context and its purpose is to read memory values. It's explained in detail at the bottom of this document. 58 | 59 | ### `update` 60 | The purpose of this function is to update local variables. 61 | * Runs every 1000 / `refreshRate` milliseconds. 62 | ```lua 63 | process('GameBlaBlaBla.exe') 64 | 65 | local current = {isLoading = false}; 66 | local old = {isLoading = false}; 67 | local loadCount = 0 68 | 69 | function startup() 70 | refreshRate = 120 71 | end 72 | 73 | function state() 74 | old.isLoading = current.isLoading; 75 | 76 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 77 | end 78 | 79 | function update() 80 | if not current.isLoading and old.isLoading then 81 | loadCount = loadCount + 1; 82 | end 83 | end 84 | ``` 85 | * We now have 3 variables, one represents the current state while the other the old state of isLoading, we also have loadCount getting updated in the `update` function which will store how many times we've entered the loading screen 86 | 87 | ### `start` 88 | This tells LibreSplit when to start the timer.\ 89 | _Note: LibreSplit will ignore any start calls if the timer is running._ 90 | * Runs every 1000 / `refreshRate` milliseconds. 91 | ```lua 92 | process('GameBlaBlaBla.exe') 93 | 94 | local current = {isLoading = false}; 95 | local old = {isLoading = false}; 96 | local loadCount = 0 97 | 98 | function startup() 99 | refreshRate = 120 100 | end 101 | 102 | function state() 103 | old.isLoading = current.isLoading; 104 | 105 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 106 | end 107 | 108 | function update() 109 | if not current.isLoading and old.isLoading then 110 | loadCount = loadCount + 1; 111 | end 112 | end 113 | 114 | function start() 115 | return current.isLoading 116 | end 117 | ``` 118 | 119 | ### `split` 120 | Tells LibreSplit to execute a split whenever it gets a true return. 121 | * Runs every 1000 / `refreshRate` milliseconds. 122 | ```lua 123 | process('GameBlaBlaBla.exe') 124 | 125 | local current = {isLoading = false}; 126 | local old = {isLoading = false}; 127 | local loadCount = 0 128 | 129 | function startup() 130 | refreshRate = 120 131 | end 132 | 133 | function state() 134 | old.isLoading = current.isLoading; 135 | 136 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 137 | end 138 | 139 | function update() 140 | if not current.isLoading and old.isLoading then 141 | loadCount = loadCount + 1; 142 | end 143 | end 144 | 145 | function start() 146 | return current.isLoading 147 | end 148 | 149 | function split() 150 | local shouldSplit = false; 151 | if current.isLoading and not old.isLoading then 152 | loadCount = loadCount + 1; 153 | shouldSplit = loadCount > 1; 154 | end 155 | 156 | return shouldSplit; 157 | end 158 | ``` 159 | * Whoa lots of code, why didnt we just return if we are currently in a loading screen like in start? Because if we do, we will do multiple splits a second, the function runs multiple times and it would do lots of unwanted splits. 160 | * 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. 161 | 162 | ### `isLoading` 163 | Pauses the timer whenever true is being returned. 164 | * Runs every 1000 / `refreshRate` milliseconds. 165 | ```lua 166 | process('GameBlaBlaBla.exe') 167 | 168 | local current = {isLoading = false, scene = ""}; 169 | local old = {isLoading = false, scene = ""}; 170 | local loadCount = 0 171 | 172 | function startup() 173 | refreshRate = 120 174 | end 175 | 176 | function state() 177 | old.isLoading = current.isLoading; 178 | old.scene = current.scene; 179 | 180 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 181 | current.scene = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xBB, 0xEE, 0x55, 0xDD, 0xBA, 0x6A); 182 | end 183 | 184 | function update() 185 | if not current.isLoading and old.isLoading then 186 | loadCount = loadCount + 1; 187 | end 188 | end 189 | 190 | function start() 191 | return current.isLoading 192 | end 193 | 194 | function split() 195 | local shouldSplit = false; 196 | if current.isLoading and not old.isLoading then 197 | loadCount = loadCount + 1; 198 | shouldSplit = loadCount > 1; 199 | end 200 | 201 | return shouldSplit; 202 | end 203 | 204 | function isLoading() 205 | return current.isLoading 206 | end 207 | ``` 208 | * Pretty self explanatory, since we want to return whenever we are currently in a loading screen, we can just send our current isLoading status, same as start. 209 | 210 | # `reset` 211 | Instantly resets the timer. Use with caution. 212 | * Runs every 1000 / `refreshRate` milliseconds. 213 | ```lua 214 | process('GameBlaBlaBla.exe') 215 | 216 | local current = {isLoading = false}; 217 | local old = {isLoading = false}; 218 | local loadCount = 0 219 | local didReset = false 220 | 221 | function startup() 222 | refreshRate = 120 223 | end 224 | 225 | function state() 226 | old.isLoading = current.isLoading; 227 | 228 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 229 | end 230 | 231 | function update() 232 | if not current.isLoading and old.isLoading then 233 | loadCount = loadCount + 1; 234 | end 235 | end 236 | 237 | function start() 238 | return current.isLoading 239 | end 240 | 241 | function split() 242 | local shouldSplit = false; 243 | if current.isLoading and not old.isLoading then 244 | loadCount = loadCount + 1; 245 | shouldSplit = loadCount > 1; 246 | end 247 | 248 | return shouldSplit; 249 | end 250 | 251 | function isLoading() 252 | return current.isLoading 253 | end 254 | 255 | function reset() 256 | if not old.scene == "MenuScene" and current.scene == "MenuScene" then 257 | return true 258 | end 259 | return false 260 | end 261 | ``` 262 | * In this example we are checking for the scene, of course, the address is completely arbitrary and doesnt mean anything for this example. Specifically we are checking if we are entering the MenuScene scene. 263 | 264 | # `gameTime` 265 | ### **When using `gameTime`, `isLoading` has to ALWAYS return true** 266 | Function that is used to set the current timer time when `useGameTime` is `true` (`false` by default) 267 | * The return value of this function should be the current time in milliseconds 268 | * Runs every 1000 / `refreshRate` milliseconds. 269 | ```lua 270 | process('GameBlaBlaBla.exe') 271 | 272 | local current = {isLoading = false}; 273 | local old = {isLoading = false}; 274 | local loadCount = 0 275 | local didReset = false 276 | local IGT = 0 277 | 278 | function startup() 279 | refreshRate = 120 280 | end 281 | 282 | function state() 283 | old.isLoading = current.isLoading; 284 | 285 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 286 | IGT = readAddress("int", "UnityPlayer.dll", 0x019B4878, ...); 287 | end 288 | 289 | function update() 290 | if not current.isLoading and old.isLoading then 291 | loadCount = loadCount + 1; 292 | end 293 | end 294 | 295 | function start() 296 | return current.isLoading 297 | end 298 | 299 | function split() 300 | local shouldSplit = false; 301 | if current.isLoading and not old.isLoading then 302 | loadCount = loadCount + 1; 303 | shouldSplit = loadCount > 1; 304 | end 305 | 306 | return shouldSplit; 307 | end 308 | 309 | function isLoading() 310 | return true 311 | end 312 | 313 | function reset() 314 | if not old.scene == "MenuScene" and current.scene == "MenuScene" then 315 | return true 316 | end 317 | return false 318 | end 319 | 320 | function gameTime() 321 | return IGT -- Assuming IGT is the current time in milliseconds tracked by the game 322 | end 323 | ``` 324 | * In this example we added `IGT`, which is the variable in which the game keeps track of how long you've played for by some way or another, later this IGT variable is used as a return value to the `gameTime` function. Also the `useGameTime` is set to true to be able to use this feature 325 | 326 | 327 | ## readAddress 328 | * `readAddress` is the second function that LibreSplit defines for us and its globally available, its job is to read the memory value of a specified address. 329 | * The first value defines what kind of value we will read: 330 | 1. `sbyte`: signed 8 bit integer 331 | 2. `byte`: unsigned 8 bit integer 332 | 3. `short`: signed 16 bit integer 333 | 4. `ushort`: unsigned 16 bit integer 334 | 5. `int`: signed 32 bit integer 335 | 6. `uint`: unsigned 32 bit integer 336 | 7. `long`: signed 64 bit integer 337 | 8. `ulong`: unsigned 64 bit integer 338 | 9. `float`: 32 bit floating point number 339 | 10. `double`: 64 bit floating point number 340 | 11. `bool`: Boolean (true or false) 341 | 12. `stringX`, A string of characters. Its usage is different compared the rest, you type "stringX" where the X is how long the string can be plus 1, this is to allocate the NULL terminator which defines when the string ends, for example, if the longest possible string to return is "cheese", you would define it as "string7". Setting X lower can result in the string terminating incorrectly and getting an incorrect result, setting it higher doesnt have any difference (aside from wasting memory). 342 | 13. `byteX`: An array of bytes, functions the same as `stringX`, but it reads bytes instead, the result is given in the form of an "array", also known as just a table that you can access with indexes, like `result[10]` will give you the 10th byte of whatever array you read 343 | 344 | * The second argument can be 2 things, a string or a number. 345 | * If its a number: The value in that memory address of the main process will be used. 346 | * If its a string: It will find the corresponding map of that string, for example "UnityPlayer.dll", This means that instead of reading the memory of the main map of the process (main binary .exe), it will instead read the memory of UnityPlayer.dll's memory space. 347 | * Next you have to add another argument, this will be the offset at which to read from from the perspective of the base address of the module, meaning if the module is mapped to 0x1000 to 0xFFFF and you put 0x0100 in the offset, it will read the value in the address 0x1010. 348 | 349 | * The rest of arguments are memory offsets or pointer paths. 350 | * A Pointer Path is a list of Offsets + a Base Address. The auto splitter reads the value at the base address and interprets the value as yet another address. It adds the first offset to this address and reads the value of the calculated address. It does this over and over until there are no more offsets. At that point, it has found the value it was searching for. This resembles the way objects are stored in memory. Every object has a clearly defined layout where each variable has a consistent offset within the object, so you basically follow these variables from object to object. 351 | 352 | * Cheat Engine is a tool that allows you to easily find Addresses and Pointer Paths for those Addresses, so you don't need to debug the game to figure out the structure of the memory. 353 | 354 | ## sig_scan 355 | 356 | `sig_scan` performs a signature/pattern scan using the provided IDA-style byte array and an integer offset, It returns a numeric representation of the found address. 357 | 358 | Example: 359 | 360 | `signature = sig_scan("89 5C 24 ?? 89 44 24 ?? 74 ?? 48 8D 15", 4)` 361 | 362 | Returns: 363 | 364 | `5387832857` 365 | 366 | (Which is the decimal representation of the address `0x14123ce19`) 367 | 368 | ### Notes 369 | 370 | * `sig_scan` may require LibreSplit to have advanced memory-reading permissions, check the [troubleshooting guide](./troubleshooting.md) to see how to enable it. If such permissions are not given, LibreSplit may not be able to find some signatures. 371 | * Lua automatically handles the conversion of hexadecimal strings to numbers, so parsing/casting it manually is not required. You can use the result of `sig_scan` directly into `readAddress`. 372 | * Until the address is found, `sig_scan` returns a `nil` value. 373 | * Signature scanning is an expensive action. So in most cases, we recommend avoiding scanning for a signature all the time, but using a variable as a "guard", this way as soon as `sig_scan` returns a valid value, the auto splitter will skip the expensive signature scanning. 374 | 375 | Mini example script with the game SPRAWL: 376 | ```lua 377 | process('Sprawl-Win64-Shipping.exe') 378 | 379 | local featuretest = nil 380 | 381 | function state() 382 | -- If our "guard variable" is nil, we didn't find an address yet... 383 | -- If our "guard variable" is not nil, we already found the memory address in a previous loop, 384 | -- so we skip further signature scanning 385 | if featuretest == nil then 386 | -- so we perform the signature scan to find the initial address 387 | featuretest = sig_scan("89 5C 24 ?? 89 44 24 ?? 74 ?? 48 8D 15", 4) 388 | -- Print a message to warn the user 389 | print("Signature scan did not find the address.") 390 | end 391 | -- When sig_scan returns a valid value, our guard variable will not be nil anymore, 392 | -- so we can continue with the rest of the auto splitter code 393 | if featuretest ~= nil then 394 | -- Read an integer value from the found address 395 | local readValue = readAddress('int', featuretest) 396 | print("Feature test address: ", featuretest) 397 | print("Read value: ", readValue) 398 | end 399 | end 400 | ``` 401 | 402 | **Attention:** The `sig_scan` function will return an address that is automatically offset with the process base address, so it is ready to use with the `readAddress` function **without a module name**. Using `readAddress` with a module name is not supported and using a module name might result in wrong or out-of-process reads. 403 | 404 | ## getPID 405 | * Returns the current PID 406 | 407 | # Experimental stuff 408 | ## `mapsCacheCycles` 409 | 410 | * When a `readAddress` that uses a memory map the biggest bottleneck is reading every line of `/proc/pid/maps` and checking if that line is the corresponding module. This option allows you to set for how many cycles the cache of that file should be used. The cache is global so it gets reset every x number of cycles. 411 | * `0`: Disabled completely 412 | * `1` (default): Enabled for the current cycle 413 | * `2`: Enabled for the current cycle and the next one 414 | * `3`: Enabled for the current cycle and the 2 next ones 415 | * You get the idea 416 | 417 | ### Performance 418 | * Every uncached map finding takes around 1ms (depends a lot on your RAM and CPU) 419 | * Every cached map finding takes around 100us 420 | 421 | * Mainly useful for lots of `readAddress`-es and the game has an uncapped game state update rate, where literally every millisecond matters 422 | 423 | ### Example 424 | ```lua 425 | function startup() 426 | refreshRate = 60; 427 | mapsCacheCycles = 1; 428 | end 429 | 430 | -- Assume all this readAddresses are different, 431 | -- Instead of taking near 10ms it will instead take 1-2ms, because only this cycle is cached and the first readAddress is a cache miss, if the mapsCacheCycles were higher than 1 then a cycle could take less than half a millisecond 432 | function state() 433 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 434 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 435 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 436 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 437 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 438 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 439 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 440 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 441 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 442 | current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); 443 | end 444 | 445 | ``` 446 | 447 | ## `getBaseAddress` 448 | Returns the base address of a given Module. If called without arguments, or with the only accepted argument as `nil`, it will return the base address of the main module. 449 | 450 | This can be useful for manual pointer manipulation, if required by the auto splitter. 451 | 452 | Usage: 453 | 454 | ``` 455 | -- This gets the base address of the main process 456 | local main_process_base_address = getbaseaddress() 457 | -- This also gets the base address of the main process 458 | local main_process_base_address = getbaseaddress(nil) 459 | -- This gets the base address of another module 460 | local module_base_address = getbaseaddress("UnityPlayer.dll") 461 | ``` 462 | 463 | ## `sizeOf` 464 | Returns the size of a given type. Uses the same type names as [readAddress](#readAddress) and will automatically size arrays and strings according to their length too. 465 | 466 | ``` 467 | -- Get the size of a 32 bit integer 468 | local int_size = sizeOf("int") 469 | -- Get the size of a 20-character string 470 | local str_size = sizeOf("string20") 471 | -- Get the size of a 25-byte array 472 | local array_size = sizeOf("byte25") 473 | ``` 474 | 475 | **Warning:** As it is now, `sizeOf` returns the size in bytes. This may not be what is needed to properly work with pointers and may see some changes in the future for better integration with the rest of the Auto-Splitter Runtime. 476 | 477 | ## getModuleSize 478 | 479 | Given a certain module name (or nothing/nil), returns the size of the module. 480 | 481 | ```lua 482 | local main_module_size = getModuleSize(); 483 | local main_module_size_2 = getModuleSize(nil); 484 | local other_module_size = getModuleSize("other_module"); 485 | ``` 486 | --------------------------------------------------------------------------------