diff --git a/Makefile b/Makefile index 3f21dfe..2d90086 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,21 @@ CFLAGS= -Ofast -I"include" BIN=cts -all: main +all: colors4python staticgen +# build for static page generator staticgen: colors.o src/dbquery.c echo "\nCompiling executable as static page generator\n" gcc -c src/dbquery.c $(CFLAGS) -DSTATICGEN gcc -c src/main.c $(CFLAGS) -DSTATICGEN gcc colors.o dbquery.o main.o -lsqlite3 -o $(BIN) -main: main.o +# used by python script to colorize names (html) +colors4python: + gcc $(CFLAGS) src/colors.c -o colors -DCOLORS4PYTHON + +# build for cgi +cgi: main.o gcc colors.o dbquery.o main.o -lsqlite3 -o $(BIN) main.o: dbquery.o src/main.c diff --git a/README.md b/README.md index f003d3e..9521a0b 100644 --- a/README.md +++ b/README.md @@ -8,39 +8,44 @@ A common gateway inferface (CGI) program written in C to display Race CTS leader The first is only needed for compilation of the C program. The latter two are only for the auxiliary script `allmaps.py`. ## Compiling -`make` makes a CGI program. +`make` makes a static page generator. -`make staticgen` makes a static page generator. +`make cgi` makes a CGI program. ## Usage: Import data from Xonotic -This program uses an sqlite3 database file created from `~/.xonotic/data/data/server.db` (text). +This program uses an sqlite3 database file created from `~/.xonotic/data/data/server.db` (text). sqlite3 my-new.db sqlite > .read schema.sql python scripts/import-from-xon.py my-new.db ~/.xonotic/data/data/server.db -## Usage: CGI Query Strings -The program queries the database `db/cts.db` (`./src/dbquery.c`, function `static bool executequery`) +## Usage: (CGI) Queries * `(none)` - - Query file: `queries/mranks.sql` - - Requests the map list of the server and related data. + - file: `queries/mranks.sql` + - Requests the map list of the server, the best times scored per map and by which player. + +* `?fastest-players` + - file: `queries/fastest-players.sql` + - Requests the map list of the server, the highest velocities attained per map and by which player. * `?map=[map name]` - - Query file: `queries/mleaderboard-ojoin.sql` + - file: `queries/mleaderboard-ojoin.sql` - Requests the leaderboard of the map. * `?player=[clientid]` - - Query file: `queries/rplayers.sql` + - file: `queries/rplayers.sql` - Requests a player's ranks for all maps leaderboards s/he is present on. +`queries/fastest-player-of-map.sql` is used exclusively by the python script `scripts/allmaps.py`. + ## Usage: Static Page Generation python scripts/allmaps.py -The CGI program is still invoked in static generation. The files `allmaps.py`, `output/leaderboard.css`, `overview.html`, `map.html` produce the output. +The files `allmaps.py`, `output/leaderboard.css`, `overview.html`, `map.html` produce the output. Before executing `allmaps.py`, copy and modify the templates. diff --git a/include/colors.h b/include/colors.h index f50513b..1d7cf71 100644 --- a/include/colors.h +++ b/include/colors.h @@ -14,12 +14,18 @@ void hsl2rgb(struct Rgb *, const struct Hls const *); void rgb2hsl(struct Hls *, const struct Rgb const *); -static void decspan(const int); +static const char *decspan(const int); -static void hexspan(const char *); +static void hexspan(char *, int, const char *); -static void b(char * const); +static void colorize_noalloc(char * const); + +static void sanitize(char *); void print_plname(const char*); +static char* append_to_str(char *, const char *); + +char* colorize_name(char *, char * const); + #endif diff --git a/queries/fastest-player-of-map.sql b/queries/fastest-player-of-map.sql new file mode 100644 index 0000000..3ce14db --- /dev/null +++ b/queries/fastest-player-of-map.sql @@ -0,0 +1,5 @@ +select speed, ifnull(alias, 'Unregistered Player') +from Speed, Fastest_players +left join Id2alias + on idvalue = cryptokey +where Speed.mapid = Fastest_players. mapid and Speed.mapid = ? diff --git a/queries/fastest-players.sql b/queries/fastest-players.sql new file mode 100644 index 0000000..a73b70a --- /dev/null +++ b/queries/fastest-players.sql @@ -0,0 +1,12 @@ +select Speed.mapid, max(trank), speed, ifnull(alias, 'Unregistered Player') +from Speed, Fastest_players, Cts_times +left join Id2alias + on Fastest_players.idvalue = cryptokey +where Speed.mapid = Fastest_players.mapid + and Cts_times.mapid = Speed.mapid + and tvalue != 0 +group by Cts_times.mapid +order by count(trank) DESC; + +-- if condition tvalue != 0 is not present +-- database will return that maps have 99 records diff --git a/queries/schema.sql b/queries/schema.sql index 5ffa249..448fa3a 100644 --- a/queries/schema.sql +++ b/queries/schema.sql @@ -22,6 +22,18 @@ CREATE TABLE Id2alias( alias TEXT, PRIMARY KEY (cryptokey) ); +drop table if exists Speed; +create table Speed( + mapid text, + speed float, + primary key (mapid) +); +drop table if exists Fastest_players; +create table Fastest_players ( + mapid text, + idvalue text, + primary key (mapid) +); -- These table fields are unaltered. -- Exerpts from source/qcsrc/race.qc @@ -38,4 +50,4 @@ CREATE TABLE Id2alias( -- re: foreign key from & to Cts_ranks & Id2alias. -- An ranked unregistered player will have a row in Cts_ranks, but will not have a row in Id2alias. --- A registered player may have a row in Id2alias, but may not necessary have a rank. \ No newline at end of file +-- A registered player may have a row in Id2alias, but may not necessary have a rank. diff --git a/scripts/allmaps.py b/scripts/allmaps.py index b184cd1..bb202f6 100644 --- a/scripts/allmaps.py +++ b/scripts/allmaps.py @@ -1,6 +1,13 @@ import sqlite3 as sql import subprocess, traceback +# import contextlib +# +import sys, io, os +# import ctypes +# colors = ctypes.CDLL('./colors.so') +# colors.colorize_name.argtypes = (ctypes.char_p, ctypes.int, ctypes.char_p) + # get all maps in database def getmaps(database): output = [] @@ -15,26 +22,50 @@ def getmaps(database): return output # if there is no query then it outputs the index file. -def getcontent(query=None): +def run_cgi(query=None): cmd = [("./cts")] proc = subprocess.Popen(cmd, env=query, stdout=subprocess.PIPE, shell=True) # communicate returns 'bytes' class with function 'decode' return proc.communicate()[0].decode('utf-8') -def renderindex(template): - # no env variable - table = getcontent() - filename = "./output/index.html" - with open(filename, 'w+') as fout: - fout.write(template % (table)) - fout.close - pass +def run_colors(player_name): + ret = player_name + result = subprocess.run(['./colors', player_name], capture_output=True, text=True) + if result.returncode == 0: + ret = result.stdout + return ret + +def get_speed_record(database, map_id): + message = "{name} traveled the fastest at {speed} qu/s." + query = str() + result = [] + with open("queries/fastest-player-of-map.sql") as f: + query = f.read() + # q = query.replace('?', map_id) + # print(q) + with sql.connect(database) as con: + cursor = con.cursor() + try: + cursor.execute(query, (map_id,)) + result = cursor.fetchall() + except sql.Error: + pass + player_name = result[0][1] + colored = (run_colors(player_name)).strip() + velocity = round(result[0][0], 2) + return message.format(name=colored, speed=velocity) + def main(): template = "" with open("overview.html", 'r') as fin: template = fin.read() - renderindex(template) + with open("output/index.html", 'w') as fout: + fout.write(template % run_cgi()) + # use same template for fastest-players + query = {"QUERY_STRING" : "fastest-players"} + with open("output/fastest-players.html", 'w') as fout: + fout.write(template % run_cgi(query)) maps = getmaps("db/cts.db") with open("map.html", 'r') as fin: template = fin.read() @@ -43,14 +74,16 @@ def main(): # game_map is a tuple obj. map_name = game_map[0] query = {"QUERY_STRING" : ("map=%s" % map_name)} - table = getcontent(query) - filename = ("./output/maps/%s.html" % map_name) + filename = ("output/maps/%s.html" % map_name) + sentence = get_speed_record("db/cts.db", map_name) with open(filename, 'w+') as fout: title = map_name fout.write(template.format( title=title, map_name=map_name, - table=table) + table=run_cgi(query), + speed=sentence + ) ) # fout.write(template % (title, map_name, table)) return True diff --git a/scripts/import-from-xon.py b/scripts/import-from-xon.py index 7a758a3..3ad57cb 100644 --- a/scripts/import-from-xon.py +++ b/scripts/import-from-xon.py @@ -85,9 +85,13 @@ def uid2namefix(row): # O(n) and organize cts related data into list of rows. def filters(db): - tt = [] - tr = [] - ti = [] + tt = [] # time (seconds) + tr = [] # ranks + ti = [] # id + # xonotic only stores one player per map + # for speed records (fastest player only) + s = [] # speed + sid = [] # speed id rank_index = 2 for d in db: if d.find("uid2name") != -1: @@ -99,15 +103,20 @@ def filters(db): if d.find("cts100record/time") != -1: e[rank_index] = int(e[rank_index].replace("time", "")) tt.append(e) - if d.find("cts100record/crypto_idfp") != -1: + elif d.find("cts100record/crypto_idfp") != -1: e[3] = unquote(e[3]) e[rank_index] = int(e[rank_index].replace("crypto_idfp", "")) tr.append(e) - if d.find("cts100record/speed") != -1: - # print(d) - # speed records - not implemented - pass - return tt, tr, ti + elif d.find("cts100record/speed/speed") != -1: +# example: +# ['zeel-omnitek', 'cts100record', 'speed', 'speed', '1584.598511'] +# --- note, index 1, 2, 3 are unneeded + s.append([ e[0], unquote(e[-1]) ]) + elif d.find("cts100record/speed/crypto_idfp") != -1: +# example: +# ['minideck_cts_v4r4', 'cts100record', 'speed', 'crypto_idfp', 'duHTyaSGpdTk7oebwPFoo899xPoTwP9bja4DUjCjTLo%3D'] + sid.append([ e[0], unquote(e[-1]) ]) + return tt, tr, ti, s, sid #------------------------------------------------+ # Functions: Database Creation @@ -133,7 +142,7 @@ def i(d, s): with con: csr = con.cursor() try: - times, ranks, ids = filters(get_list_from_server_txt(s)) + times, ranks, ids, speed, speed_ids = filters(get_list_from_server_txt(s)) if times: inserttodb(csr, "INSERT OR REPLACE INTO Cts_times VALUES(?, ?, ?, ?)", times) logging.info('\n'.join(y for y in [str(x) for x in times])) @@ -143,6 +152,10 @@ def i(d, s): if ids: inserttodb(csr, "INSERT OR REPLACE INTO Id2alias VALUES(?, ?, ?)", ids) logging.info('\n'.join(y for y in [str(x) for x in ids])) + if speed: + inserttodb(csr, "INSERT OR REPLACE INTO Speed VALUES(?, ?)", speed) + if speed_ids: + inserttodb(csr, "INSERT OR REPLACE INTO Fastest_players VALUES(?, ?)", speed_ids) except sql.Error: logging.exception("sql error encountered in function 'i'") if con: @@ -151,7 +164,7 @@ def i(d, s): # 'insert' new data into a file i.e sql query file def f(d, s): with open(d, 'w', encoding='utf-8') as h: - times, ranks, ids = filters(get_list_from_server_txt(s)) + times, ranks, ids, speed, speed_ids = filters(get_list_from_server_txt(s)) for t in times: h.write("INSERT OR REPLACE INTO Cts_times VALUES(%s, %s, %s, %s)\n" % tuple(t)) pass diff --git a/src/colors.c b/src/colors.c index 54efd1b..625cb08 100644 --- a/src/colors.c +++ b/src/colors.c @@ -80,42 +80,36 @@ void rgb2hsl(struct Hls *dest, const struct Rgb const *src) { } } -static void decspan(const int d) { +static const char* decspan(const int d) { switch(d) { case 0: - printf(""); - break; + return ""; case 1: - printf(""); - break; + return ""; case 2: - printf(""); - break; + return ""; case 3: - printf(""); - break; + return ""; case 4: - printf(""); - break; + return ""; case 5: - printf(""); - break; + return ""; case 6: - printf(""); - break; + return ""; case 7: - printf(""); - break; + return ""; case 8: - printf(""); - break; + return ""; case 9: - printf(""); - break; + return ""; } } -static void hexspan(const char *str) { +static void hexspan(char *buf, int bufsize, const char *str) { + // length of ... + // "" + // where each %d ranges from 0 to 255 + // char buf[40]; const char h1[2] = {str[0], '\0'}; const char h2[2] = {str[1], '\0'}; const char h3[2] = {str[2], '\0'}; @@ -131,17 +125,21 @@ static void hexspan(const char *str) { nhls.l = MIN_CONTRAST; hsl2rgb(&nrgb, &nhls); } - printf("", nrgb.r, nrgb.g, nrgb.b); + int wrote = snprintf( + buf, bufsize, + "", + nrgb.r, nrgb.g, nrgb.b); + // output = buf; } -static void b(char * const str) { +#define TAG_LEN 40 +static void colorize_noalloc(char * const str) { char *token = strtok(str, "^"); char c; - printf("
| \
+ |||
---|---|---|---|
Name | \ +Records | \ +Highest Velocty (qu/s) | \ +Held By | \ +"); print_plname(field); + printf(" | "); } else if (ISMAPNAME(c, i)) { #ifdef STATICGEN printf("%s | ", field, field); @@ -105,11 +122,13 @@ static void qresult(sqlite3_stmt * const sp, const char *c) { #endif } else if (i == 2 && (*c == QMLEADERBOARD || *c == QOVERVIEW)) { print_time(field); + } else if (i == 2 && *c == QFASTEST) { // velocity + printf("%.2f | ", atof(field) ); } else { printf("%s | ", field); } } - printf(""); + printf("\n"); } printf("
{speed}
+