├── .gitignore ├── Makefile ├── README.md ├── calc.c ├── calc.h ├── common.c ├── common.h ├── config.c ├── config.h ├── draw.c ├── draw.h ├── io.c ├── io.h ├── main.c ├── splitters ├── sar_split.c ├── splitters.tar.gz └── splitters │ └── sar_split ├── timer.c ├── timer.h └── vdict.h /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | adrift 3 | splitters/*.o 4 | splitters/sar_split 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .POSIX: 2 | .PHONY: all clean splitters 3 | 4 | CFLAGS := -Wall -Werror $(shell pkg-config --cflags vtk) -D_POSIX_C_SOURCE=200809L 5 | LDFLAGS := $(shell pkg-config --libs vtk) -lpthread 6 | 7 | SPLITTER_FLAGS := -D_POSIX_C_SOURCE=200809L 8 | 9 | HDRS := $(wildcard *.h) 10 | 11 | all: adrift splitters 12 | 13 | clean: 14 | rm -f adrift *.o splitters/sar_split 15 | 16 | splitters: splitters/sar_split 17 | 18 | adrift: main.o draw.o common.o io.o calc.o timer.o config.o 19 | $(CC) -o $@ $^ $(LDFLAGS) 20 | 21 | splitters/%: splitters/%.c 22 | $(CC) -o $@ $^ $(SPLITTER_FLAGS) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # adrift 2 | 3 | A simple, lightweight speedrun timer for Linux. 4 | 5 | ## Building 6 | 7 | adrift is built using GNU Make. After installing its dependencies, 8 | simply run `make` in the source tree to build the `adrift` binary. This 9 | binary is standalone and can be installed to an appropriate location. 10 | 11 | ### Dependencies 12 | 13 | adrift depends on [vtk](https://github.com/vktec/vtk) for its GUI. 14 | 15 | ## Usage 16 | 17 | adrift [directory] 18 | 19 | adrift will look for and store all configuaration files, run 20 | information, etc in the given directory, or, if none was given, the 21 | current working directory. All files used by adrift are UTF-8 and use LF 22 | line endings. 23 | 24 | When adrift starts, a file named `splits` wil be read, which contains 25 | the split names, each on their own line. Subsplits can be created by 26 | indenting splits with tabs as follows: 27 | 28 | Chapter 1 29 | Map 1 30 | Map 2 31 | Chapter 2 32 | Map 3 33 | Map 4 34 | 35 | adrift will also execute the file named `splitter`. This should be an 36 | executable file which outputs a rift data stream on stdout for splitting 37 | (see the Autosplitting section below). 38 | 39 | ## Configuration 40 | 41 | When it starts, adrift will attempt to read a file named `config`. Each 42 | line of this file is a string key, followed by any amount of whitespace 43 | (excluding newlines), and a corresponding string value. A keys prefixed 44 | with `col_` configures a color: its value should be a color code in the 45 | form `RRGGBB` or `RRGGBBAA`, optionally with a leading `#` (the alpha 46 | component is assumed to be `FF` if not given). The following 47 | configuration keys currently exist: 48 | 49 | - `game` 50 | - `category` 51 | - `col_background` 52 | - `col_text` 53 | - `col_timer` 54 | - `col_timer_ahead` 55 | - `col_timer_behind` 56 | - `col_active_split` 57 | - `col_split_gold` 58 | - `col_split_ahead` 59 | - `col_split_behind` 60 | - `split_time_width` 61 | - `window_width` 62 | - `window_height` 63 | 64 | ## Autosplitting 65 | 66 | Autosplitters are communicated with via the [rift 67 | protocol](https://github.com/vktec/rift/blob/master/protocol.md). Any 68 | rift-compliant autosplitter should work with adrift. 69 | 70 | Included in the repo is an autosplitter which interfaces with 71 | [SAR](https://github.com/Blenderiste09/SourceAutoRecord). This splitter 72 | requires ptrace privileges to work, as it must read Portal 2's memory. 73 | These can be given by running the following command as root: 74 | 75 | setcap cap_sys_ptrace=eip ./splitter 76 | -------------------------------------------------------------------------------- /calc.c: -------------------------------------------------------------------------------- 1 | #include "calc.h" 2 | 3 | static uint64_t _sob(struct split *splits, size_t nsplits) { 4 | uint64_t sum = 0; 5 | 6 | for (size_t i = 0; i < nsplits; ++i) { 7 | uint64_t best; 8 | if (splits[i].is_group) { 9 | best = _sob(splits[i].group.splits, splits[i].group.nsplits); 10 | } else { 11 | best = splits[i].split.times.best; 12 | } 13 | 14 | if (best == UINT64_MAX) { 15 | return UINT64_MAX; 16 | } 17 | 18 | sum += best; 19 | } 20 | 21 | return sum; 22 | } 23 | 24 | uint64_t calc_sum_of_best(struct state *s) { 25 | return _sob(s->splits, s->nsplits); 26 | } 27 | 28 | static uint64_t _bpt(struct state *s, struct split *splits, size_t nsplits) { 29 | uint64_t sum = 0; 30 | 31 | for (size_t i = 0; i < nsplits; ++i) { 32 | uint64_t best; 33 | if (splits[i].is_group) { 34 | best = _bpt(s, splits[i].group.splits, splits[i].group.nsplits); 35 | } else if (splits[i].split.id == s->active_split) { 36 | best = splits[i].split.times.best; 37 | if (s->split_time > best) best = s->split_time; 38 | } else if (splits[i].split.times.cur != UINT64_MAX) { 39 | uint64_t prev = splits[i].split.id == 0 ? 0 : get_split_by_id(s, splits[i].split.id - 1)->split.times.cur; 40 | best = splits[i].split.times.cur - prev; 41 | } else { 42 | best = splits[i].split.times.best; 43 | } 44 | 45 | if (best == UINT64_MAX) { 46 | return UINT64_MAX; 47 | } 48 | 49 | sum += best; 50 | } 51 | 52 | return sum; 53 | } 54 | 55 | uint64_t calc_best_possible_time(struct state *s) { 56 | return _bpt(s, s->splits, s->nsplits); 57 | } 58 | -------------------------------------------------------------------------------- /calc.h: -------------------------------------------------------------------------------- 1 | #ifndef CALC_H 2 | #define CALC_H 3 | 4 | #include "common.h" 5 | 6 | uint64_t calc_sum_of_best(struct state *s); 7 | uint64_t calc_best_possible_time(struct state *s); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /common.c: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | #include 3 | 4 | static struct split *_get_split_by_id(struct split *splits, size_t nsplits, unsigned id) { 5 | for (size_t i = 0; i < nsplits; ++i) { 6 | if (splits[i].is_group) { 7 | struct split *sp = _get_split_by_id(splits[i].group.splits, splits[i].group.nsplits, id); 8 | if (sp) return sp; 9 | } else if (splits[i].split.id == id) { 10 | return &splits[i]; 11 | } 12 | } 13 | 14 | return NULL; 15 | } 16 | 17 | struct split *get_split_by_id(struct state *s, unsigned id) { 18 | return _get_split_by_id(s->splits, s->nsplits, id); 19 | } 20 | 21 | static struct split *_get_final_split(struct split *splits, size_t nsplits) { 22 | struct split *sp = &splits[nsplits - 1]; 23 | if (sp->is_group) { 24 | return _get_final_split(sp->group.splits, sp->group.nsplits); 25 | } else { 26 | return sp; 27 | } 28 | } 29 | 30 | int get_split_id(struct split *sp) { 31 | sp = _get_final_split(sp, 1); 32 | return sp->split.id; 33 | } 34 | 35 | struct split *get_final_split(struct state *s) { 36 | return _get_final_split(s->splits, s->nsplits); 37 | } 38 | 39 | struct times get_split_times(struct split *sp) { 40 | while (sp->is_group) { 41 | sp = &sp->group.splits[sp->group.nsplits - 1]; 42 | } 43 | 44 | return sp->split.times; 45 | } 46 | 47 | uint64_t get_comparison(struct state *s, struct times t) { 48 | return t.pb; 49 | } 50 | 51 | void free_splits(struct split *splits, size_t nsplits) { 52 | for (size_t i = 0; i < nsplits; ++i) { 53 | free(splits[i].name); 54 | if (splits[i].is_group) { 55 | free_splits(splits[i].group.splits, splits[i].group.nsplits); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /common.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMON_H 2 | #define COMMON_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "config.h" 11 | 12 | enum widget_type { 13 | WIDGET_GAME_NAME, 14 | WIDGET_CATEGORY_NAME, 15 | WIDGET_TIMER, 16 | WIDGET_SPLIT_TIMER, 17 | WIDGET_SPLITS, 18 | WIDGET_SUM_OF_BEST, 19 | WIDGET_BEST_POSSIBLE_TIME, 20 | }; 21 | 22 | struct times { 23 | // UINT64_MAX if not present 24 | 25 | // Cumulative 26 | uint64_t cur; 27 | uint64_t pb; 28 | 29 | // Per-split 30 | uint64_t best; 31 | bool golded_this_run; 32 | }; 33 | 34 | struct split { 35 | char *name; 36 | bool is_group; 37 | 38 | union { 39 | struct { 40 | size_t nsplits; 41 | struct split *splits; 42 | bool expanded; 43 | } group; 44 | 45 | struct { 46 | struct times times; 47 | int id; 48 | } split; 49 | }; 50 | }; 51 | 52 | struct state { 53 | vtk_window win; 54 | cairo_t *cr; 55 | 56 | const char *game_name; 57 | const char *category_name; 58 | 59 | size_t nwidgets; 60 | enum widget_type *widgets; 61 | 62 | size_t nsplits; 63 | struct split *splits; 64 | 65 | int active_split; 66 | 67 | uint64_t timer; 68 | uint64_t split_time; 69 | 70 | time_t run_started; 71 | 72 | struct cfgdict *cfg; 73 | }; 74 | 75 | struct split *get_split_by_id(struct state *s, unsigned id); 76 | int get_split_id(struct split *sp); 77 | struct split *get_final_split(struct state *s); 78 | struct times get_split_times(struct split *sp); 79 | uint64_t get_comparison(struct state *s, struct times t); 80 | void free_splits(struct split *splits, size_t nsplits); 81 | 82 | #endif 83 | -------------------------------------------------------------------------------- /config.c: -------------------------------------------------------------------------------- 1 | #define VDICT_IMPL 2 | 3 | #include "config.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | bool config_get_color(struct cfgdict *cfg, const char *k, float *r, float *g, float *b, float *a) { 10 | char *v; 11 | if (!cfgdict_get(cfg, (char *)k, &v)) { 12 | return false; 13 | } 14 | 15 | if (v[0] == '#') ++v; 16 | 17 | size_t len = strlen(v); 18 | int ri, gi, bi, ai = 255; 19 | int p; 20 | 21 | if (len == 6 && sscanf(v, "%2x%2x%2x%n", &ri, &gi, &bi, &p) == 3 && p == 6) { 22 | goto done; 23 | } 24 | 25 | if (len == 8 && sscanf(v, "%2x%2x%2x%2x%n", &ri, &gi, &bi, &ai, &p) == 4 && p == 8) { 26 | goto done; 27 | } 28 | 29 | return false; 30 | 31 | done: 32 | *r = ri / 255.0f; 33 | *g = gi / 255.0f; 34 | *b = bi / 255.0f; 35 | *a = ai / 255.0f; 36 | return true; 37 | } 38 | 39 | long config_get_int(struct cfgdict *cfg, const char *k, long def) { 40 | const char *str = config_get_str(cfg, k, NULL); 41 | if (!str) return def; 42 | while (isspace(*str)) ++str; 43 | char *end; 44 | long val = strtol(str, &end, 10); 45 | while (isspace(*end)) ++end; 46 | return *end ? def : val; 47 | } 48 | 49 | const char *config_get_str(struct cfgdict *cfg, const char *k, const char *def) { 50 | char *ret = (char *)def; 51 | cfgdict_get(cfg, (char *)k, &ret); 52 | return ret; 53 | } 54 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | 4 | #include 5 | 6 | #define VDICT_NAME cfgdict 7 | #define VDICT_KEY char * 8 | #define VDICT_VAL char * 9 | #define VDICT_HASH vdict_hash_string 10 | #define VDICT_EQUAL vdict_eq_string 11 | #include "vdict.h" 12 | 13 | bool config_get_color(struct cfgdict *cfg, const char *k, float *r, float *g, float *b, float *a); 14 | long config_get_int(struct cfgdict *cfg, const char *k, long def); 15 | const char *config_get_str(struct cfgdict *cfg, const char *k, const char *def); 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /draw.c: -------------------------------------------------------------------------------- 1 | #include "draw.h" 2 | #include "common.h" 3 | #include "calc.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define TEXT_PAD 3 11 | 12 | static void set_color_cfg(struct state *s, const char *k, float r, float g, float b, float a) { 13 | config_get_color(s->cfg, k, &r, &g, &b, &a); 14 | cairo_set_source_rgba(s->cr, r, g, b, a); 15 | } 16 | 17 | // To stop the timer position doing weird things, we assume the width of 18 | // digits is constant 19 | int get_approx_time_width(struct state *s, const char *str) { 20 | size_t len = strlen(str) + 1; 21 | char *buf = malloc(len); 22 | for (size_t i = 0; i < len; ++i) { 23 | char c = str[i]; 24 | if (c >= '0' && c <= '9') { 25 | buf[i] = '0'; 26 | } else { 27 | buf[i] = c; 28 | } 29 | } 30 | cairo_text_extents_t ext; 31 | cairo_text_extents(s->cr, buf, &ext); 32 | free(buf); 33 | return ext.width; 34 | } 35 | 36 | const char *format_time(uint64_t total, char prefix, int prec) { 37 | if (total == UINT64_MAX) return "-"; 38 | 39 | static char buf[64]; 40 | 41 | // The logic for calculating frac is pretty simple but it's still 42 | // easier to just switch on the precision 43 | uint64_t frac; 44 | switch (prec) { 45 | case 0: 46 | frac = 0; 47 | break; 48 | case 1: 49 | frac = (total / 100000) % 10; 50 | break; 51 | case 2: 52 | frac = (total / 10000) % 100; 53 | break; 54 | case 3: 55 | frac = (total / 1000) % 1000; 56 | break; 57 | case 4: 58 | frac = (total / 100) % 10000; 59 | break; 60 | case 5: 61 | frac = (total / 10) % 100000; 62 | break; 63 | case 6: 64 | default: 65 | frac = total % 1000000; 66 | break; 67 | } 68 | 69 | total /= 1000000; 70 | uint64_t secs = total % 60; 71 | total /= 60; 72 | uint64_t mins = total % 60; 73 | total /= 60; 74 | uint64_t hrs = total; 75 | 76 | char *ptr = buf; 77 | size_t bufsz = sizeof buf; 78 | if (prefix) { 79 | buf[0] = prefix; 80 | ++ptr; 81 | --bufsz; 82 | } 83 | 84 | if (hrs) { 85 | snprintf(ptr, bufsz, "%"PRIu64":%02"PRIu64":%02"PRIu64".%0*"PRIu64, hrs, mins, secs, prec, frac); 86 | } else if (mins) { 87 | snprintf(ptr, bufsz, "%"PRIu64":%02"PRIu64".%0*"PRIu64, mins, secs, prec, frac); 88 | } else { 89 | snprintf(ptr, bufsz, "%"PRIu64".%0*"PRIu64, secs, prec, frac); 90 | } 91 | 92 | return buf; 93 | } 94 | 95 | enum align { 96 | ALIGN_LEFT, 97 | ALIGN_CENTER, 98 | ALIGN_RIGHT, 99 | ALIGN_RIGHT_TIME, 100 | }; 101 | 102 | int get_font_height(struct state *s) { 103 | cairo_font_extents_t ext; 104 | cairo_font_extents(s->cr, &ext); 105 | return ext.ascent + ext.descent; 106 | } 107 | 108 | void draw_text(struct state *s, const char *str, int w, int h, int *y, bool update_y, enum align align, int off) { 109 | int width; 110 | if (align == ALIGN_RIGHT_TIME) { 111 | width = get_approx_time_width(s, str); 112 | } else { 113 | cairo_text_extents_t ext; 114 | cairo_text_extents(s->cr, str, &ext); 115 | width = ext.width; 116 | } 117 | cairo_font_extents_t fext; 118 | cairo_font_extents(s->cr, &fext); 119 | int old = *y; 120 | *y += TEXT_PAD + fext.ascent; 121 | switch (align) { 122 | case ALIGN_LEFT: 123 | cairo_move_to(s->cr, off + TEXT_PAD, *y); 124 | break; 125 | case ALIGN_CENTER: 126 | cairo_move_to(s->cr, off + (w - off - width) / 2, *y); 127 | break; 128 | case ALIGN_RIGHT: 129 | case ALIGN_RIGHT_TIME: 130 | cairo_move_to(s->cr, w - off - TEXT_PAD - width, *y); 131 | break; 132 | } 133 | cairo_show_text(s->cr, str); 134 | *y += TEXT_PAD + fext.descent; 135 | if (!update_y) *y = old; 136 | } 137 | 138 | void draw_splits(struct state *s, int w, int h, int *y, int off, struct split *splits, size_t nsplits) { 139 | cairo_set_font_size(s->cr, 16.0f); 140 | for (size_t i = 0; i < nsplits; ++i) { 141 | struct times times = get_split_times(&splits[i]); 142 | uint64_t comparison = get_comparison(s, times); 143 | uint64_t cur = times.cur; 144 | 145 | bool active = !splits[i].is_group && splits[i].split.id == s->active_split; 146 | 147 | if (active) { 148 | set_color_cfg(s, "col_active_split", 0.3, 0.5, 0.8, 1.0); 149 | cairo_rectangle(s->cr, 0, *y, w, get_font_height(s) + 2 * TEXT_PAD); 150 | cairo_fill(s->cr); 151 | } 152 | 153 | // For the active split, we want to display the delta as soon as it goes over gold 154 | if (active && (s->split_time > times.best)) { 155 | cur = s->timer; 156 | } 157 | 158 | // If there's an active comparison *and* a current split time, find 159 | // the delta 160 | const char *delta = "-"; 161 | if (cur == UINT64_MAX && comparison == UINT64_MAX) { 162 | delta = ""; 163 | } else if (cur != UINT64_MAX && comparison != UINT64_MAX) { 164 | if (cur < comparison) delta = format_time(comparison - cur, '-', 2); 165 | else delta = format_time(cur - comparison, '+', 2); 166 | } 167 | 168 | if (times.golded_this_run && !splits[i].is_group) { 169 | set_color_cfg(s, "col_split_gold", 1.0, 0.9, 0.3, 1.0); 170 | } else if (cur != UINT64_MAX && comparison != UINT64_MAX) { 171 | if (cur < comparison) { 172 | set_color_cfg(s, "col_split_ahead", 0.2, 1.0, 0.2, 1.0); 173 | } else { 174 | set_color_cfg(s, "col_split_behind", 1.0, 0.2, 0.2, 1.0); 175 | } 176 | } else { 177 | set_color_cfg(s, "col_text", 1.0, 1.0, 1.0, 1.0); 178 | } 179 | 180 | draw_text(s, delta, w, h, y, false, ALIGN_RIGHT_TIME, config_get_int(s->cfg, "split_time_width", 100)); 181 | 182 | // For splits before active, draw the time obtained 183 | // For splits after, draw the comparison 184 | set_color_cfg(s, "col_text", 1.0, 1.0, 1.0, 1.0); 185 | if (cur == UINT64_MAX || active) { 186 | draw_text(s, format_time(comparison, 0, 2), w, h, y, false, ALIGN_RIGHT, 0); 187 | } else { 188 | draw_text(s, format_time(cur, 0, 2), w, h, y, false, ALIGN_RIGHT, 0); 189 | } 190 | 191 | set_color_cfg(s, "col_text", 1.0, 1.0, 1.0, 1.0); 192 | draw_text(s, splits[i].name, w, h, y, true, ALIGN_LEFT, off); 193 | 194 | if (splits[i].is_group && splits[i].group.expanded) { 195 | draw_splits(s, w, h, y, off + 20, splits[i].group.splits, splits[i].group.nsplits); 196 | } 197 | } 198 | } 199 | 200 | void draw_widget(struct state *s, enum widget_type t, int w, int h, int *y) { 201 | switch (t) { 202 | case WIDGET_GAME_NAME: 203 | set_color_cfg(s, "col_text", 1.0, 1.0, 1.0, 1.0); 204 | cairo_set_font_size(s->cr, 23.0f); 205 | draw_text(s, s->game_name, w, h, y, true, ALIGN_CENTER, 0); 206 | break; 207 | case WIDGET_CATEGORY_NAME: 208 | set_color_cfg(s, "col_text", 1.0, 1.0, 1.0, 1.0); 209 | cairo_set_font_size(s->cr, 16.0f); 210 | draw_text(s, s->category_name, w, h, y, true, ALIGN_CENTER, 0); 211 | break; 212 | case WIDGET_TIMER: 213 | set_color_cfg(s, "col_timer", 1.0, 1.0, 1.0, 1.0); 214 | if (s->active_split != -1) { 215 | struct times times = get_split_times(get_split_by_id(s, s->active_split)); 216 | uint64_t comparison = get_comparison(s, times); 217 | if (s->timer < comparison) { 218 | set_color_cfg(s, "col_timer_ahead", 1.0, 1.0, 1.0, 1.0); 219 | } else { 220 | set_color_cfg(s, "col_timer_behind", 1.0, 1.0, 1.0, 1.0); 221 | } 222 | } 223 | cairo_set_font_size(s->cr, 26.0f); 224 | draw_text(s, format_time(s->timer, 0, 3), w, h, y, true, ALIGN_RIGHT_TIME, 0); 225 | break; 226 | case WIDGET_SPLIT_TIMER: 227 | set_color_cfg(s, "col_timer", 1.0, 1.0, 1.0, 1.0); 228 | if (s->active_split != -1) { 229 | struct times times = get_split_times(get_split_by_id(s, s->active_split)); 230 | uint64_t comparison = get_comparison(s, times); 231 | 232 | uint64_t prev_comparison = 0; 233 | if (s->active_split > 0) { 234 | struct times prev_times = get_split_times(get_split_by_id(s, s->active_split-1)); 235 | prev_comparison = get_comparison(s, prev_times); 236 | } 237 | 238 | if (s->split_time < comparison - prev_comparison) { 239 | set_color_cfg(s, "col_timer_ahead", 1.0, 1.0, 1.0, 1.0); 240 | } else { 241 | set_color_cfg(s, "col_timer_behind", 1.0, 1.0, 1.0, 1.0); 242 | } 243 | } 244 | cairo_set_font_size(s->cr, 24.0f); 245 | draw_text(s, format_time(s->split_time, 0, 3), w, h, y, true, ALIGN_RIGHT_TIME, 0); 246 | break; 247 | case WIDGET_SPLITS: 248 | draw_splits(s, w, h, y, 0, s->splits, s->nsplits); 249 | break; 250 | case WIDGET_SUM_OF_BEST: 251 | set_color_cfg(s, "col_text", 1.0, 1.0, 1.0, 1.0); 252 | cairo_set_font_size(s->cr, 17.0f); 253 | draw_text(s, "Sum of best:", w, h, y, false, ALIGN_LEFT, 0); 254 | draw_text(s, format_time(calc_sum_of_best(s), 0, 3), w, h, y, true, ALIGN_RIGHT, 0); 255 | break; 256 | case WIDGET_BEST_POSSIBLE_TIME: 257 | set_color_cfg(s, "col_text", 1.0, 1.0, 1.0, 1.0); 258 | cairo_set_font_size(s->cr, 17.0f); 259 | draw_text(s, "Best possible time:", w, h, y, false, ALIGN_LEFT, 0); 260 | draw_text(s, format_time(calc_best_possible_time(s), 0, 3), w, h, y, true, ALIGN_RIGHT, 0); 261 | break; 262 | } 263 | } 264 | 265 | void draw_handler(vtk_event ev, void *u) { 266 | struct state *s = u; 267 | 268 | int w, h; 269 | vtk_window_get_size(s->win, &w, &h); 270 | 271 | cairo_rectangle(s->cr, 0, 0, w, h); 272 | set_color_cfg(s, "col_background", 0.0, 0.0, 0.0, 0.0); 273 | cairo_fill(s->cr); 274 | 275 | int y = 0; 276 | 277 | for (size_t i = 0; i < s->nwidgets; ++i) { 278 | draw_widget(s, s->widgets[i], w, h, &y); 279 | } 280 | } 281 | 282 | -------------------------------------------------------------------------------- /draw.h: -------------------------------------------------------------------------------- 1 | #ifndef DRAW_H 2 | #define DRAW_H 3 | 4 | #include 5 | 6 | void draw_handler(vtk_event ev, void *u); 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /io.c: -------------------------------------------------------------------------------- 1 | #include "io.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | static inline size_t _count_tabs(char *line) { 9 | size_t i = 0; 10 | while (line[i] == '\t') ++i; 11 | return i; 12 | }; 13 | 14 | static ssize_t _read_splits(FILE *f, unsigned *id, unsigned level, struct split **out, char **trailing_line) { 15 | size_t splits_alloc = 8; 16 | size_t nsplits = 0; 17 | struct split *splits = malloc(splits_alloc * sizeof splits[0]); 18 | 19 | char *line = NULL; 20 | 21 | while (true) { 22 | if (!line) { 23 | size_t n = 0; 24 | if (getline(&line, &n, f) == -1) { 25 | free(line); 26 | *trailing_line = NULL; 27 | break; 28 | } 29 | } 30 | 31 | if (!strcmp(line, "\n")) { 32 | line = NULL; 33 | continue; 34 | } 35 | 36 | size_t ntabs = _count_tabs(line); 37 | 38 | if (ntabs != level) { 39 | *trailing_line = line; 40 | break; 41 | } 42 | 43 | if (splits_alloc == nsplits) { 44 | splits_alloc *= 2; 45 | splits = realloc(splits, splits_alloc * sizeof splits[0]); 46 | } 47 | 48 | size_t len = strlen(line + ntabs); 49 | char *name = malloc(len); 50 | strncpy(name, line + ntabs, len - 1); 51 | name[len - 1] = 0; 52 | 53 | char *next = NULL; 54 | 55 | splits[nsplits].name = name; 56 | 57 | struct split *subsplits; 58 | ssize_t nsubsplits = _read_splits(f, id, level + 1, &subsplits, &next); 59 | if (nsubsplits < 1) { 60 | // No subsplits, so it's an actual split 61 | splits[nsplits].is_group = false; 62 | splits[nsplits].split.id = (*id)++; 63 | splits[nsplits].split.times = (struct times){ 64 | .cur = UINT64_MAX, 65 | .pb = UINT64_MAX, 66 | .best = UINT64_MAX, 67 | .golded_this_run = false, 68 | }; 69 | } else { 70 | // We have subsplits, so it's a group 71 | splits[nsplits].is_group = true; 72 | splits[nsplits].group.expanded = false; 73 | splits[nsplits].group.nsplits = nsubsplits; 74 | splits[nsplits].group.splits = subsplits; 75 | } 76 | 77 | free(line); 78 | 79 | line = next; 80 | ++nsplits; 81 | } 82 | 83 | if (nsplits == 0) { 84 | free(splits); 85 | } 86 | 87 | *out = splits; 88 | return nsplits; 89 | } 90 | 91 | ssize_t read_splits_file(const char *path, struct split **out) { 92 | FILE *f = fopen(path, "r"); 93 | 94 | if (!f) { 95 | return -1; 96 | } 97 | 98 | unsigned id = 0; 99 | 100 | char *trailing; 101 | ssize_t nsplits = _read_splits(f, &id, 0, out, &trailing); 102 | 103 | if (nsplits != -1 && trailing) { 104 | fprintf(stderr, "trailing line in splits file: %s", trailing); // We don't print a newline as the trailing line includes one 105 | free(trailing); 106 | free_splits(*out, nsplits); 107 | return -1; 108 | } 109 | 110 | fclose(f); 111 | 112 | return nsplits; 113 | } 114 | 115 | static bool _read_times(FILE *f, size_t off, struct split *splits, size_t nsplits) { 116 | for (size_t i = 0; i < nsplits; ++i) { 117 | if (splits[i].is_group) { 118 | if (!_read_times(f, off, splits[i].group.splits, splits[i].group.nsplits)) { 119 | return false; 120 | } 121 | } else { 122 | uint64_t val; 123 | int dummy = -1; 124 | if (fscanf(f, "-\n%n", &dummy) == 0 && dummy != -1) { 125 | *(uint64_t *)((char *)&splits[i].split.times + off) = UINT64_MAX; 126 | } else if (fscanf(f, "%"PRIu64"\n", &val) == 1) { 127 | *(uint64_t *)((char *)&splits[i].split.times + off) = val; 128 | } else { 129 | return false; 130 | } 131 | } 132 | } 133 | 134 | return true; 135 | } 136 | 137 | static void _clear_times(size_t off, struct split *splits, size_t nsplits) { 138 | for (size_t i = 0; i < nsplits; ++i) { 139 | if (splits[i].is_group) { 140 | _clear_times(off, splits[i].group.splits, splits[i].group.nsplits); 141 | } else { 142 | *(uint64_t *)((char *)&splits[i].split.times + off) = UINT64_MAX; 143 | } 144 | } 145 | } 146 | 147 | bool read_times(struct split *splits, size_t nsplits, const char *path, size_t off) { 148 | FILE *f = fopen(path, "r"); 149 | 150 | if (!f) { 151 | return false; 152 | } 153 | 154 | bool success = _read_times(f, off, splits, nsplits); 155 | 156 | // TODO: enforce eof 157 | 158 | fclose(f); 159 | 160 | if (!success) { 161 | _clear_times(off, splits, nsplits); 162 | } 163 | 164 | return success; 165 | } 166 | 167 | bool _save_times(FILE *f, size_t off, struct split *splits, size_t nsplits) { 168 | for (size_t i = 0; i < nsplits; ++i) { 169 | if (splits[i].is_group) { 170 | _save_times(f, off, splits[i].group.splits, splits[i].group.nsplits); 171 | } else { 172 | uint64_t time = *(uint64_t *)((char *)&splits[i].split.times + off); 173 | if (time == UINT64_MAX) { 174 | if (fputs("-\n", f) == EOF) { 175 | return false; 176 | } 177 | } else { 178 | if (fprintf(f, "%"PRIu64"\n", time) == EOF) { 179 | return false; 180 | } 181 | } 182 | } 183 | } 184 | 185 | return true; 186 | } 187 | 188 | bool save_times(struct split *splits, size_t nsplits, const char *path, size_t off) { 189 | FILE *f = fopen(path, "w"); 190 | 191 | if (!f) { 192 | return false; 193 | } 194 | 195 | bool success = _save_times(f, off, splits, nsplits); 196 | 197 | fclose(f); 198 | 199 | return success; 200 | } 201 | 202 | // TODO: this leaks memory, might be worth not doing that 203 | bool read_config(const char *path, struct cfgdict *cfg) { 204 | FILE *f = fopen(path, "r"); 205 | 206 | if (!f) { 207 | return false; 208 | } 209 | 210 | while (true) { 211 | size_t allocd = 0; 212 | char *line = NULL; 213 | size_t n = getline(&line, &allocd, f); 214 | 215 | if (n == -1) { 216 | break; 217 | } 218 | 219 | // strip trailing whitespace 220 | { 221 | char *end = line; 222 | while (*end) ++end; 223 | --end; 224 | while (isspace(*end)) --end; 225 | end[1] = 0; 226 | } 227 | 228 | if (n > 0) { 229 | int p = -1; 230 | char *k; 231 | int matched = sscanf(line, "%ms %n", &k, &p); 232 | if (matched != 1 || p == -1) { 233 | if (matched >= 1) free(k); 234 | // TODO: clear cfg 235 | fclose(f); 236 | return false; 237 | } 238 | char *v = strdup(line + p); 239 | int ret = cfgdict_put(cfg, k, v); 240 | if (ret == -1) { 241 | free(k); 242 | free(v); 243 | // TODO: clear cfg 244 | fclose(f); 245 | return false; 246 | } else if (ret == 1) { 247 | fprintf(stderr, "Warning: duplicate config key %s\n", k); 248 | } 249 | } 250 | } 251 | 252 | fclose(f); 253 | 254 | return true; 255 | } 256 | -------------------------------------------------------------------------------- /io.h: -------------------------------------------------------------------------------- 1 | #ifndef IO_H 2 | #define IO_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "common.h" 8 | #include "config.h" 9 | 10 | ssize_t read_splits_file(const char *path, struct split **out); 11 | bool read_times(struct split *splits, size_t nsplits, const char *path, size_t off); 12 | bool save_times(struct split *splits, size_t nsplits, const char *path, size_t off); 13 | bool read_config(const char *path, struct cfgdict *cfg); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "draw.h" 13 | #include "common.h" 14 | #include "io.h" 15 | #include "timer.h" 16 | 17 | static atomic_bool _g_should_exit; 18 | 19 | static vtk_window _g_win; 20 | 21 | static void int_handler(int signal) { 22 | vtk_window_close(_g_win); 23 | } 24 | 25 | static void close_handler(vtk_event ev, void *u) { 26 | vtk_window_close(_g_win); 27 | } 28 | 29 | static int input_main(void *u) { 30 | struct state *s = u; 31 | 32 | int pipefd[2]; 33 | if (pipe(pipefd) == -1) { 34 | fputs("Failed to create pipe\n", stderr); 35 | exit(1); 36 | } 37 | 38 | pid_t pid = 0; 39 | 40 | pid = fork(); 41 | if (pid == 0) { 42 | // Child 43 | close(pipefd[0]); 44 | dup2(pipefd[1], STDOUT_FILENO); 45 | execlp("./splitter", "./splitter", NULL); 46 | fputs("Failed to exec splitter\n", stderr); 47 | exit(1); 48 | } else if (pid == -1) { 49 | fputs("Failed to fork\n", stderr); 50 | exit(1); 51 | } 52 | 53 | // Parent 54 | 55 | close(pipefd[1]); 56 | 57 | int fl = fcntl(pipefd[0], F_GETFL); 58 | if (fl == -1) fl = 0; 59 | fcntl(pipefd[0], F_SETFL, fl | O_NONBLOCK); 60 | 61 | FILE *f = fdopen(pipefd[0], "r"); 62 | 63 | struct pollfd fds[] = { 64 | { pipefd[0], POLLIN }, 65 | }; 66 | 67 | while (!_g_should_exit) { 68 | // Use poll rather than getline directly; that way, we can routinely 69 | // check if we should exit 70 | if (poll(fds, sizeof fds / sizeof fds[0], 500) > 0) { 71 | char *line = NULL; 72 | size_t n = 0; 73 | if (getline(&line, &n, f) == -1) { 74 | free(line); 75 | break; 76 | } 77 | line[strlen(line) - 1] = 0; // Remove newline 78 | timer_parse(s, line); 79 | free(line); 80 | vtk_window_trigger_update(s->win); 81 | } 82 | } 83 | 84 | close(pipefd[0]); 85 | 86 | if (pid) { 87 | kill(pid, SIGINT); 88 | } 89 | 90 | return 0; 91 | } 92 | 93 | void update_handler(vtk_event ev, void *u) { 94 | struct state *s = u; 95 | vtk_window_redraw(s->win); 96 | } 97 | 98 | int main(int argc, char **argv) { 99 | if (argc > 2 || (argc == 2 && !strcmp(argv[1], "-h"))) { 100 | fprintf(stderr, "Usage: %s [path]\n", argv[0]); 101 | return argc > 2; 102 | } 103 | 104 | if (argc == 2) { 105 | if (chdir(argv[1])) { 106 | fprintf(stderr, "Failed to chdir to %s\n", argv[1]); 107 | return 1; 108 | } 109 | } 110 | 111 | enum widget_type widgets[] = { 112 | WIDGET_GAME_NAME, 113 | WIDGET_CATEGORY_NAME, 114 | WIDGET_SUM_OF_BEST, 115 | WIDGET_BEST_POSSIBLE_TIME, 116 | WIDGET_TIMER, 117 | WIDGET_SPLIT_TIMER, 118 | WIDGET_SPLITS, 119 | }; 120 | 121 | struct split *splits; 122 | ssize_t nsplits = read_splits_file("splits", &splits); 123 | 124 | if (nsplits == -1) { 125 | return 1; 126 | } 127 | 128 | if (!read_times(splits, nsplits, "pb", offsetof(struct times, pb))) { 129 | fputs("Warning: could not read PB\n", stderr); 130 | } 131 | 132 | if (!read_times(splits, nsplits, "golds", offsetof(struct times, best))) { 133 | fputs("Warning: could not read golds\n", stderr); 134 | } 135 | 136 | struct cfgdict *cfg = cfgdict_new(); 137 | if (!read_config("config", cfg)) { 138 | fputs("Warning: could not read config\n", stderr); 139 | } 140 | 141 | int err; 142 | 143 | vtk vtk; 144 | err = vtk_new(&vtk); 145 | if (err) { 146 | fprintf(stderr, "Error initializing vtk: %s\n", vtk_strerr(err)); 147 | return 1; 148 | } 149 | 150 | vtk_window win; 151 | err = vtk_window_new(&win, vtk, "Adrift", 0, 0, config_get_int(cfg, "window_width", 350), config_get_int(cfg, "window_height", 650)); 152 | if (err) { 153 | fprintf(stderr, "Error initializing vtk window: %s\n", vtk_strerr(err)); 154 | vtk_destroy(vtk); 155 | return 1; 156 | } 157 | 158 | cairo_t *cr = vtk_window_get_cairo(win); 159 | 160 | struct state s = { 161 | .win = win, 162 | .cr = cr, 163 | 164 | .game_name = config_get_str(cfg, "game", "Portal 2"), 165 | .category_name = config_get_str(cfg, "category", "Inbounds NoSLA"), 166 | 167 | .nwidgets = sizeof widgets / sizeof widgets[0], 168 | .widgets = widgets, 169 | 170 | .nsplits = nsplits, 171 | .splits = splits, 172 | 173 | .active_split = -1, 174 | 175 | .timer = 0, 176 | .split_time = 0, 177 | 178 | .cfg = cfg, 179 | }; 180 | 181 | _g_win = win; 182 | 183 | thrd_t inp_thrd; 184 | if (thrd_create(&inp_thrd, &input_main, &s) != thrd_success) { 185 | fputs("Error creating thread\n", stderr); 186 | vtk_window_destroy(win); 187 | vtk_destroy(vtk); 188 | return 1; 189 | } 190 | 191 | vtk_window_set_event_handler(win, VTK_EV_DRAW, draw_handler, &s); 192 | vtk_window_set_event_handler(win, VTK_EV_CLOSE, close_handler, &s); 193 | vtk_window_set_event_handler(win, VTK_EV_UPDATE, update_handler, &s); 194 | 195 | signal(SIGINT, &int_handler); 196 | 197 | vtk_window_mainloop(win); 198 | 199 | vtk_window_destroy(win); 200 | vtk_destroy(vtk); 201 | 202 | _g_should_exit = true; 203 | 204 | thrd_join(inp_thrd, NULL); 205 | 206 | save_times(splits, nsplits, "golds", offsetof(struct times, best)); 207 | 208 | cfgdict_free(cfg); 209 | 210 | return 0; 211 | } 212 | -------------------------------------------------------------------------------- /splitters/sar_split.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #define _DEFAULT_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | /* 23 | * 16 byte "SAR_TIMER_START\0" 24 | * int total 25 | * float interval_per_tick 26 | * int action 27 | * 14 byte end "SAR_TIMER_END\0" 28 | */ 29 | 30 | struct addr_range { 31 | struct addr_range *next; 32 | void *start; 33 | size_t len; 34 | }; 35 | 36 | /* Read and parse /proc//maps and return a linked list of all the 37 | * address ranges in the program. If heap_only is true, only return 38 | * address ranges labelled '[heap]'. */ 39 | static struct addr_range *get_ranges(pid_t pid, bool heap_only) { 40 | if (pid <= 0) return NULL; 41 | 42 | char pathbuf[32]; // sufficient for pid INT_MAX 43 | snprintf(pathbuf, sizeof pathbuf, "/proc/%d/maps", pid); 44 | 45 | FILE *f = fopen(pathbuf, "r"); 46 | if (!f) return NULL; 47 | 48 | struct addr_range *lst = NULL; 49 | 50 | char *line = NULL; 51 | size_t len = 0; 52 | while (getline(&line, &len, f) > 0) { 53 | uint64_t start, end; 54 | int name_start; 55 | if (sscanf(line, "%lx-%lx %*7s %*x %*u:%*u %*u %n", &start, &end, &name_start) != 2) continue; 56 | 57 | if (heap_only) { 58 | if (strcmp("[heap]\n", line + name_start)) continue; 59 | } 60 | 61 | struct addr_range *tmp = malloc(sizeof *tmp); 62 | tmp->next = lst; 63 | tmp->start = (void *)start; 64 | tmp->len = end - start; 65 | lst = tmp; 66 | } 67 | 68 | free(line); 69 | fclose(f); 70 | 71 | return lst; 72 | } 73 | 74 | /* Try to find the SAR timer structure in the memory of the given 75 | * process, and return the address in its memory where the timer was 76 | * found, or NULL if it could not be found. */ 77 | static void *scan_for_timer(pid_t pid) { 78 | for (struct addr_range *r = get_ranges(pid, true); r; r = r->next) { 79 | fprintf(stderr, "[LOG] --- Scan range %lx-%lx ---\n", (uintptr_t)r->start, (uintptr_t)r->start + r->len); 80 | 81 | void *buf = malloc(r->len); 82 | 83 | if (!buf) { 84 | fprintf(stderr, "[WARN] Failed to allocate buffer of size %lx. Skipping address range\n", r->len); 85 | continue; 86 | } 87 | 88 | struct iovec local = {buf, r->len}, remote = {r->start, r->len}; 89 | 90 | size_t len_read = syscall(SYS_process_vm_readv, pid, &local, 1, &remote, 1, 0); 91 | 92 | if (len_read == -1) { 93 | fprintf(stderr, "[WARN] Failed to read address range with errno %d. Skipping\n", errno); 94 | free(buf); 95 | continue; 96 | } 97 | 98 | if (len_read != r->len) { 99 | fprintf(stderr, "[WARN] Failed to read full address range; range had size %lx, but only read %lx. Skipping\n", r->len, len_read); 100 | free(buf); 101 | continue; 102 | } 103 | 104 | for (size_t i = 0; i <= r->len - 42; ++i) { 105 | if (strcmp("SAR_TIMER_START", buf + i) != 0) continue; 106 | if (strcmp("SAR_TIMER_END", buf + i + 28) != 0) continue; 107 | fputs("[LOG] Found SAR timer location!\n", stderr); 108 | free(buf); 109 | return (char *)r->start + i; 110 | } 111 | 112 | free(buf); 113 | } 114 | 115 | return NULL; 116 | } 117 | 118 | enum timer_action { 119 | NOTHING, 120 | START, 121 | RESTART, 122 | SPLIT, 123 | END, 124 | RESET, 125 | PAUSE, 126 | RESUME, 127 | }; 128 | 129 | struct timer_info { 130 | int total; 131 | float ipt; 132 | enum timer_action action; 133 | }; 134 | 135 | /* Poll the SAR timer that exists at the given address for the given 136 | * process, and put the retrieved information in *info. Returns 0 on 137 | * success, any other value on failure. */ 138 | static int poll_timer(pid_t pid, void *addr, struct timer_info *info) { 139 | struct iovec local = {info, sizeof *info}, remote = {(char *)addr + 16, sizeof *info}; 140 | size_t len_read = syscall(SYS_process_vm_readv, pid, &local, 1, &remote, 1, 0); 141 | 142 | if (len_read != sizeof *info) { 143 | return 1; 144 | } 145 | 146 | return 0; 147 | } 148 | 149 | static pid_t find_process(void) { 150 | DIR *d = opendir("/proc"); 151 | if (!d) return -1; 152 | 153 | fputs("[LOG] Enumerating processes...\n", stderr); 154 | 155 | struct dirent *de; 156 | while ((de = readdir(d))) { 157 | pid_t pid = atoi(de->d_name); 158 | if (pid > 0) { 159 | char pathbuf[32]; // sufficient for pid INT_MAX 160 | snprintf(pathbuf, sizeof pathbuf, "/proc/%d/cmdline", pid); 161 | 162 | FILE *f = fopen(pathbuf, "r"); 163 | if (!f) continue; 164 | 165 | char *cmdline = NULL; 166 | size_t cmdlen = 0; 167 | if (getline(&cmdline, &cmdlen, f) > 0) { 168 | char *base = basename(cmdline); 169 | if (!strcmp(base, "portal2_linux")) { 170 | fputs("[LOG] Found portal2_linux process!\n", stderr); 171 | free(cmdline); 172 | fclose(f); 173 | closedir(d); 174 | return pid; 175 | } 176 | } 177 | 178 | free(cmdline); 179 | fclose(f); 180 | } 181 | } 182 | 183 | closedir(d); 184 | return -1; 185 | } 186 | 187 | struct state { 188 | pid_t pid; 189 | void *addr; 190 | enum timer_action last_action; 191 | }; 192 | 193 | struct state *splitter_init(int fd, bool initial_connect) { 194 | pid_t pid = find_process(); 195 | 196 | if (pid < 0) { 197 | fputs("[ERR] Could not find portal2_linux process\n", stderr); 198 | return NULL; 199 | } 200 | 201 | fprintf(stderr, "[LOG] Using process %d\n", pid); 202 | 203 | void *addr = scan_for_timer(pid); 204 | 205 | if (!addr) { 206 | fputs("[ERR] Could not find timer in memory! Is SAR loaded?\n", stderr); 207 | return NULL; 208 | } 209 | 210 | struct state *s = malloc(sizeof *s); 211 | s->pid = pid; 212 | s->addr = addr; 213 | s->last_action = NOTHING; 214 | 215 | fputs("[LOG] Initialization completed!\n", stderr); 216 | 217 | // Reset to the current time 218 | 219 | struct timer_info info; 220 | 221 | if (poll_timer(s->pid, s->addr, &info)) { 222 | fputs("[ERR] Failed to poll timer!\n", stderr); 223 | return NULL; 224 | } 225 | 226 | if (initial_connect) { 227 | uint64_t usec = (double)info.ipt * (double)info.total * 1e6; 228 | dprintf(fd, "%lu RESET\n", usec); 229 | } 230 | 231 | return s; 232 | } 233 | 234 | int splitter_update(int fd, struct state *st) { 235 | struct timer_info info; 236 | 237 | if (poll_timer(st->pid, st->addr, &info)) { 238 | fputs("[ERR] Failed to poll timer!\n", stderr); 239 | return 1; 240 | } 241 | 242 | uint64_t usec = (double)info.ipt * (double)info.total * 1e6; 243 | 244 | enum timer_action new_act = info.action != st->last_action ? info.action : NOTHING; 245 | st->last_action = info.action; 246 | 247 | switch (new_act) { 248 | case START: 249 | dprintf(fd, "0 BEGIN\n"); 250 | dprintf(fd, "%lu\n", usec); 251 | break; 252 | case SPLIT: 253 | case END: 254 | dprintf(fd, "%lu SPLIT\n", usec); 255 | break; 256 | case RESET: 257 | dprintf(fd, "%lu RESET\n", usec); 258 | break; 259 | case RESTART: 260 | dprintf(fd, "%lu RESET\n", usec); 261 | dprintf(fd, "0 BEGIN\n"); 262 | dprintf(fd, "%lu\n", usec); 263 | break; 264 | default: 265 | dprintf(fd, "%lu\n", usec); 266 | break; 267 | } 268 | 269 | return 0; 270 | } 271 | 272 | int fd; 273 | char *fifo_path; 274 | 275 | void cleanup(int sig) { 276 | if (fifo_path) { 277 | close(fd); 278 | unlink(fifo_path); 279 | } 280 | exit(0); 281 | } 282 | 283 | int usage(char *argv0) { 284 | fprintf(stderr, "Usage: %s [fifo path]\n", argv0); 285 | return 1; 286 | } 287 | 288 | int main(int argc, char **argv) { 289 | if (argc > 2) { 290 | return usage(argv[0]); 291 | } 292 | 293 | if (argc == 2 && !strcmp(argv[1], "-h")) { 294 | return usage(argv[0]); 295 | } 296 | 297 | fd = STDOUT_FILENO; 298 | fifo_path = argc == 2 ? argv[1] : NULL; 299 | 300 | if (fifo_path) { 301 | if (mkfifo(fifo_path, 0644) == -1) { 302 | fprintf(stderr, "[ERR] Failed to create FIFO '%s': error %d\n", fifo_path, errno); 303 | return 1; 304 | } 305 | 306 | fd = open(fifo_path, O_WRONLY); 307 | if (fd == -1) { 308 | fprintf(stderr, "[ERR] Failed to open '%s': error %d\n", fifo_path, errno); 309 | unlink(fifo_path); 310 | return 1; 311 | } 312 | } 313 | 314 | struct sigaction act = { 315 | .sa_handler = cleanup, 316 | }; 317 | sigaction(SIGINT, &act, NULL); 318 | 319 | struct state *st; 320 | bool last_failed = false; 321 | bool initial_connect = true; 322 | 323 | while (true) { 324 | do { 325 | st = splitter_init(fd, initial_connect); 326 | if (!st) sleep(2); 327 | } while (!st); 328 | 329 | initial_connect = false; 330 | 331 | if (st == NULL) { 332 | if (fifo_path) unlink(fifo_path); 333 | return 1; 334 | } 335 | 336 | while (true) { 337 | if (splitter_update(fd, st)) { 338 | if (last_failed) { 339 | if (fifo_path) { 340 | close(fd); 341 | unlink(fifo_path); 342 | } 343 | return 1; 344 | } else { 345 | last_failed = true; 346 | break; // re-init 347 | } 348 | } 349 | 350 | last_failed = false; 351 | 352 | struct timespec sleep_tv = { 353 | .tv_sec = 0, 354 | .tv_nsec = 15000000, // 15ms 355 | }; 356 | 357 | nanosleep(&sleep_tv, NULL); 358 | } 359 | } 360 | 361 | return 0; 362 | } 363 | -------------------------------------------------------------------------------- /splitters/splitters.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlugg/adrift/76d2961307bfacf11030a3ce0c4fa8542f00c773/splitters/splitters.tar.gz -------------------------------------------------------------------------------- /splitters/splitters/sar_split: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlugg/adrift/76d2961307bfacf11030a3ce0c4fa8542f00c773/splitters/splitters/sar_split -------------------------------------------------------------------------------- /timer.c: -------------------------------------------------------------------------------- 1 | #include "timer.h" 2 | #include "io.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define RUNS_DIR "runs" 11 | // Microseconds you have to beat gold by for it to actually register - prevents rounding issues 12 | #define GOLD_EPSILON 10 13 | 14 | static bool _update_expanded(int active_split, struct split *splits, size_t nsplits) { 15 | bool expand = false; 16 | 17 | for (size_t i = 0; i < nsplits; ++i) { 18 | if (splits[i].is_group) { 19 | bool e = _update_expanded(active_split, splits[i].group.splits, splits[i].group.nsplits); 20 | splits[i].group.expanded = e; 21 | expand = expand || e; 22 | } else { 23 | expand = expand || active_split == splits[i].split.id; 24 | } 25 | } 26 | 27 | return expand; 28 | } 29 | 30 | static void _commit_pb(struct split *splits, size_t nsplits) { 31 | for (size_t i = 0; i < nsplits; ++i) { 32 | if (splits[i].is_group) { 33 | _commit_pb(splits[i].group.splits, splits[i].group.nsplits); 34 | } else { 35 | splits[i].split.times.pb = splits[i].split.times.cur; 36 | } 37 | } 38 | } 39 | 40 | static void _run_finish(struct state *s) { 41 | struct split *final = get_final_split(s); 42 | mkdir(RUNS_DIR, 0777); 43 | char run_name[64]; 44 | strftime(run_name, sizeof run_name, RUNS_DIR "/%Y-%m-%d_%H.%M.%S", localtime(&s->run_started)); 45 | save_times(s->splits, s->nsplits, run_name, offsetof(struct times, cur)); 46 | if (final->split.times.cur < final->split.times.pb) { 47 | unlink("pb"); 48 | symlink(run_name, "pb"); 49 | } 50 | } 51 | 52 | static void _clear_cur(struct split *splits, size_t nsplits) { 53 | for (size_t i = 0; i < nsplits; ++i) { 54 | if (splits[i].is_group) { 55 | _clear_cur(splits[i].group.splits, splits[i].group.nsplits); 56 | } else { 57 | splits[i].split.times.cur = UINT64_MAX; 58 | splits[i].split.times.golded_this_run = false; 59 | } 60 | } 61 | } 62 | 63 | void update_expanded(struct state *s) { 64 | _update_expanded(s->active_split, s->splits, s->nsplits); 65 | } 66 | 67 | void timer_begin(struct state *s) { 68 | s->active_split = 0; 69 | s->run_started = time(NULL); 70 | update_expanded(s); 71 | } 72 | 73 | void timer_reset(struct state *s) { 74 | if (s->active_split == -1) { 75 | struct split *final = get_final_split(s); 76 | if (final->split.times.cur < final->split.times.pb) { 77 | _commit_pb(s->splits, s->nsplits); 78 | } 79 | } 80 | _clear_cur(s->splits, s->nsplits); 81 | s->active_split = -1; 82 | update_expanded(s); 83 | } 84 | 85 | void timer_split(struct state *s) { 86 | if (s->active_split == -1) return; 87 | 88 | struct split *sp = get_split_by_id(s, s->active_split); 89 | sp->split.times.cur = s->timer; 90 | 91 | if (s->split_time < sp->split.times.best - GOLD_EPSILON) { 92 | sp->split.times.best = s->split_time; 93 | sp->split.times.golded_this_run = true; 94 | save_times(s->splits, s->nsplits, "golds", offsetof(struct times, best)); 95 | } 96 | 97 | if (sp == get_final_split(s)) { 98 | s->active_split = -1; 99 | _run_finish(s); 100 | } else { 101 | s->active_split++; 102 | } 103 | 104 | update_expanded(s); 105 | } 106 | 107 | static void update_time(struct state *s, uint64_t time) { 108 | uint64_t prev = 0; 109 | if (s->active_split > 0) { 110 | prev = get_split_by_id(s, s->active_split - 1)->split.times.cur; 111 | } 112 | 113 | s->timer = time; 114 | s->split_time = time - prev; 115 | } 116 | 117 | void timer_parse(struct state *s, const char *str) { 118 | char *end; 119 | long us = strtol(str, &end, 10); 120 | 121 | if (end == str) { 122 | goto err; 123 | } 124 | 125 | bool updated = false; 126 | 127 | if (end[0] == ' ') { 128 | ++end; 129 | if (!strcmp(end, "BEGIN")) { 130 | timer_reset(s); 131 | update_time(s, us); 132 | timer_begin(s); 133 | updated = true; 134 | } else if (!strcmp(end, "RESET")) { 135 | timer_reset(s); 136 | update_time(s, us); 137 | updated = true; 138 | } else if (!strcmp(end, "SPLIT")) { 139 | if (s->active_split != -1) { 140 | update_time(s, us); 141 | timer_split(s); 142 | updated = true; 143 | } 144 | } else if (end[0] != '\0') { 145 | goto err; 146 | } 147 | } else if (end[0] != '\0') { 148 | goto err; 149 | } 150 | 151 | if (!updated && s->active_split != -1) { 152 | update_time(s, us); 153 | } 154 | 155 | return; 156 | 157 | err: 158 | fprintf(stderr, "Warning: bad splitter data! Got line '%s'\n", str); 159 | return; 160 | } 161 | -------------------------------------------------------------------------------- /timer.h: -------------------------------------------------------------------------------- 1 | #ifndef TIMER_H 2 | #define TIMER_H 3 | 4 | #include "common.h" 5 | 6 | void timer_begin(struct state *s); 7 | void timer_reset(struct state *s); 8 | void timer_split(struct state *s); 9 | void timer_parse(struct state *s, const char *str); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /vdict.h: -------------------------------------------------------------------------------- 1 | /* vdict.h 2 | * 3 | * A generic, ordered dictionary type inspired by Python's dict. 4 | */ 5 | 6 | /* 7 | * This is free and unencumbered software released into the public domain. 8 | * 9 | * Anyone is free to copy, modify, publish, use, compile, sell, or 10 | * distribute this software, either in source code form or as a compiled 11 | * binary, for any purpose, commercial or non-commercial, and by any 12 | * means. 13 | * 14 | * In jurisdictions that recognize copyright laws, the author or authors 15 | * of this software dedicate any and all copyright interest in the 16 | * software to the public domain. We make this dedication for the benefit 17 | * of the public at large and to the detriment of our heirs and 18 | * successors. We intend this dedication to be an overt act of 19 | * relinquishment in perpetuity of all present and future rights to this 20 | * software under copyright law. 21 | * 22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 24 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 25 | * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 26 | * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 27 | * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | * OTHER DEALINGS IN THE SOFTWARE. 29 | * 30 | * For more information, please refer to 31 | */ 32 | 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | #ifndef VDICT_NAME 39 | #error "VDICT_NAME undefined. This is used as the struct name and as the function prefix" 40 | #endif 41 | #ifndef VDICT_KEY 42 | #error "VDICT_KEY undefined. This is used as the key type" 43 | #endif 44 | #ifndef VDICT_VAL 45 | #error "VDICT_VAL undefined. This is used as the value type" 46 | #endif 47 | #ifndef VDICT_HASH 48 | #error "VDICT_HASH undefined. This is used as the key hash function (try vdict_hash_int or vdict_hash_string)" 49 | #endif 50 | #ifndef VDICT_EQUAL 51 | #error "VDICT_EQUAL undefined. This is used to compare keys for equality (try vdict_eq_int or vdict_eq_string)" 52 | #endif 53 | #ifndef VDICT_LINK 54 | #define VDICT_LINK 55 | #endif 56 | 57 | #ifndef _vdict_COMMON 58 | #define _vdict_COMMON 59 | 60 | // Hash functions {{{ 61 | static inline uint32_t vdict_hash_int(uint32_t x) { 62 | // From https://stackoverflow.com/a/12996028 63 | x = ((x >> 16) ^ x) * 0x45d9f3b; 64 | x = ((x >> 16) ^ x) * 0x45d9f3b; 65 | x = (x >> 16) ^ x; 66 | return x; 67 | } 68 | 69 | static inline uint32_t vdict_hash_string(const char *s) { 70 | // djb2a 71 | uintmax_t hash = 5381; 72 | while (*s) hash = hash*33 ^ *s++; 73 | return hash; 74 | } 75 | // }}} 76 | 77 | // Equality functions {{{ 78 | static inline _Bool vdict_eq_int(uint32_t a, uint32_t b) { 79 | return a == b; 80 | } 81 | 82 | static inline _Bool vdict_eq_string(const char *a, const char *b) { 83 | return !strcmp(a, b); 84 | } 85 | // }}} 86 | 87 | #define _vdict_SPLAT_(a, b, c, d, e, ...) a##b##c##d##e 88 | #define _vdict_SPLAT(...) _vdict_SPLAT_(__VA_ARGS__,,,) 89 | 90 | #define _vdict_intern(name) _vdict_SPLAT(_, VDICT_NAME, _, name) 91 | #define _vdict_extern(name) _vdict_SPLAT(VDICT_NAME, _, name) 92 | #define _vdict VDICT_NAME 93 | #define _vdict_entry _vdict_intern(entry) 94 | 95 | #endif 96 | 97 | struct _vdict; 98 | 99 | // Create a new dictionary 100 | VDICT_LINK struct _vdict *_vdict_extern(new)(void); 101 | 102 | // Delete a dictionary 103 | VDICT_LINK void _vdict_extern(free)(struct _vdict *d); 104 | 105 | // Insert a key/value pair into a dictionary 106 | // Returns 1 if the key was already in the dictionary, 0 if it was not, and -1 if out-of-memory 107 | VDICT_LINK int _vdict_extern(put)(struct _vdict *d, VDICT_KEY k, VDICT_VAL v); 108 | 109 | // Get the value of a key 110 | // Returns 1 if the key was found, 0 otherwise 111 | // If v is not NULL and the key was found, *v is set to the value 112 | VDICT_LINK _Bool _vdict_extern(get)(struct _vdict *d, VDICT_KEY k, VDICT_VAL *v); 113 | 114 | // Delete a key/value pair 115 | // Returns 1 if the key was found, 0 otherwise 116 | // If v is not NULL and the key was found, *v is set to the value before the entry is deleted 117 | VDICT_LINK _Bool _vdict_extern(del)(struct _vdict *d, VDICT_KEY, VDICT_VAL *v); 118 | 119 | #ifdef VDICT_IMPL 120 | #undef VDICT_IMPL 121 | 122 | struct _vdict_entry { 123 | uint32_t hash; 124 | _Bool removed; 125 | 126 | VDICT_KEY k; 127 | VDICT_VAL v; 128 | }; 129 | 130 | struct _vdict { 131 | // Total number of entries 132 | uint32_t n_entry; 133 | // log_2 of number of allocated entries 134 | uint32_t ecap_e; 135 | // log_2 of number of allocated indices in `map` 136 | uint32_t mcap_e; 137 | 138 | // Entries referenced by indices in `map` 139 | struct _vdict_entry *ent; 140 | // The actual hash table. Stores indices into entries, 1-indexed, or 0 for empty cell 141 | uint32_t *map; 142 | }; 143 | 144 | // Hash a key, returning an in-bounds value for the specified dict 145 | static inline uint32_t _vdict_intern(hash)(struct _vdict *d, VDICT_KEY k) { 146 | return (VDICT_HASH(k)) >> (32 - d->mcap_e); 147 | } 148 | 149 | // Wrap an index to be in-bounds for the specified dict 150 | static inline uint32_t _vdict_intern(wrap)(struct _vdict *d, uint32_t i) { 151 | return i & ((1 << d->mcap_e) - 1); 152 | } 153 | 154 | // Get the entry of a hash table index 155 | static inline struct _vdict_entry *_vdict_intern(entry)(struct _vdict *d, uint32_t i) { 156 | return d->ent + d->map[i] - 1; 157 | } 158 | 159 | // Return 1 if the entry of the given hash table index exists and has a value, else 0 160 | static inline _Bool _vdict_intern(exists)(struct _vdict *d, uint32_t i) { 161 | return d->map[i] && !_vdict_intern(entry)(d, i)->removed; 162 | } 163 | 164 | // Find the hash table index of a key 165 | static uint32_t _vdict_intern(index)(struct _vdict *d, VDICT_KEY k, uint32_t h) { 166 | uint32_t i = h; 167 | for (;;) { 168 | if (!d->map[i]) return i; 169 | struct _vdict_entry *ent = _vdict_intern(entry)(d, i); 170 | 171 | if (!ent->removed && ent->hash == h && VDICT_EQUAL(ent->k, k)) { 172 | return i; 173 | } 174 | 175 | i = _vdict_intern(wrap)(d, i + 1); 176 | } 177 | } 178 | 179 | // Create a dict 180 | VDICT_LINK struct _vdict *_vdict_extern(new)(void) { 181 | struct _vdict *d = malloc(sizeof *d); 182 | d->n_entry = 0; 183 | 184 | d->ecap_e = 4; 185 | d->ent = malloc((1 << d->ecap_e) * sizeof *d->ent); 186 | d->mcap_e = 5; 187 | d->map = calloc((1 << d->mcap_e), sizeof *d->map); 188 | 189 | return d; 190 | } 191 | 192 | // Delete a dict 193 | VDICT_LINK void _vdict_extern(free)(struct _vdict *d) { 194 | free(d->ent); 195 | free(d->map); 196 | free(d); 197 | } 198 | 199 | // Put a k/v pair, rehashing if load factor >=50% 200 | VDICT_LINK int _vdict_extern(put)(struct _vdict *d, VDICT_KEY k, VDICT_VAL v) { 201 | if (2 * d->n_entry >= 1 << d->mcap_e) { 202 | uint32_t *map = d->map; 203 | d->map = calloc(1 << ++d->mcap_e, sizeof *d->map); 204 | if (!d->map) { 205 | d->map = map; 206 | d->mcap_e--; 207 | return -1; 208 | } 209 | 210 | uint32_t geti = 0, puti = 0; 211 | while (geti < d->n_entry) { 212 | struct _vdict_entry ent = d->ent[geti++]; 213 | if (!ent.removed) { 214 | ent.hash = _vdict_intern(hash)(d, ent.k); 215 | if (puti != geti) { 216 | d->ent[puti] = ent; 217 | } 218 | puti++; 219 | 220 | uint32_t i = _vdict_intern(index)(d, ent.k, ent.hash); 221 | d->map[i] = puti; // Increment is before this, because indices are 1-indexed 222 | } 223 | } 224 | 225 | free(map); 226 | } 227 | 228 | uint32_t h = _vdict_intern(hash)(d, k); 229 | uint32_t i = _vdict_intern(index)(d, k, h); 230 | 231 | int ret; 232 | if (_vdict_intern(exists)(d, i)) { 233 | ret = 1; // Already in dict 234 | } else { 235 | ret = 0; // Added to dict 236 | d->map[i] = ++d->n_entry; 237 | 238 | // Grow entry array if needed 239 | if (d->n_entry >= (1 << d->ecap_e)) { 240 | struct _vdict_entry *ent = d->ent; 241 | d->ent = realloc(d->ent, (1 << ++d->ecap_e) * sizeof *d->ent); 242 | if (!d->ent) { 243 | d->ent = ent; 244 | d->ecap_e--; 245 | return -1; 246 | } 247 | } 248 | } 249 | 250 | *_vdict_intern(entry)(d, i) = (struct _vdict_entry){h, 0, k, v}; 251 | return ret; 252 | } 253 | 254 | VDICT_LINK _Bool _vdict_extern(get)(struct _vdict *d, VDICT_KEY k, VDICT_VAL *v) { 255 | uint32_t h = _vdict_intern(hash)(d, k); 256 | uint32_t i = _vdict_intern(index)(d, k, h); 257 | 258 | if (!_vdict_intern(exists)(d, i)) return 0; 259 | 260 | if (v) *v = _vdict_intern(entry)(d, i)->v; 261 | return 1; 262 | } 263 | 264 | VDICT_LINK _Bool _vdict_extern(del)(struct _vdict *d, VDICT_KEY k, VDICT_VAL *v) { 265 | uint32_t h = _vdict_intern(hash)(d, k); 266 | uint32_t i = _vdict_intern(index)(d, k, h); 267 | 268 | if (!_vdict_intern(exists)(d, i)) return 0; 269 | 270 | struct _vdict_entry *ent = _vdict_intern(entry)(d, i); 271 | if (v) *v = ent->v; 272 | ent->removed = 1; 273 | 274 | return 1; 275 | } 276 | 277 | #endif 278 | 279 | #undef VDICT_LINK 280 | #undef VDICT_EQUAL 281 | #undef VDICT_HASH 282 | #undef VDICT_KEY 283 | #undef VDICT_VAL 284 | #undef VDICT_NAME 285 | --------------------------------------------------------------------------------