Compare commits

...

5 Commits

Author SHA1 Message Date
38fc8210ab merge branch 'params', squashed 2024-10-31 06:16:39 -07:00
d549f73a43 corrected typo (of velocity) 2024-10-27 18:12:59 -07:00
4d1816e0ce merged branch 'speed-records', squashed 2024-10-18 21:21:02 -07:00
ded4e29594 merge branch 'import script', squashed 2024-10-13 12:38:11 -07:00
f17c6a8f68 merge staticgen, squashed 2024-10-12 11:15:11 -07:00
16 changed files with 722 additions and 69 deletions

5
.gitignore vendored
View File

@ -1,3 +1,4 @@
*.exe *.exe
*.o
*.o *.txt
*.log

View File

@ -1,9 +1,21 @@
CFLAGS= -Ofast -I"include" CFLAGS= -Ofast -I"include"
BIN=cts BIN=cts
all: main all: colors4python staticgen
main: main.o # 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)
# 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) gcc colors.o dbquery.o main.o -lsqlite3 -o $(BIN)
main.o: dbquery.o src/main.c main.o: dbquery.o src/main.c
@ -19,4 +31,4 @@ testcolor: src/colors.c src/tcolor.c
gcc src/colors.c src/tcolor.c -o tcolor -I"includes" -g gcc src/colors.c src/tcolor.c -o tcolor -I"includes" -g
clean: clean:
rm *.o *.log $(BIN) rm *.o

View File

@ -1,32 +1,67 @@
C CGI Xonotic DeFrag # xdfcgi
-----------------
A CGI program written in C to display data related to Race CTS leaderboards of Xonotic servers. A common gateway inferface (CGI) program written in C to display Race CTS leaderboards of Xonotic servers. It can also be a static page generator.
## Requirements ## Requirements
* SQLite3 sqlite-devel python3 python-sqlite
## Compiling
`make` 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).
sqlite3 my-new.db
sqlite > .read schema.sql
python scripts/import-from-xon.py my-new.db ~/.xonotic/data/data/server.db
## Usage: (CGI) Queries
## Web Server Queries
* `(none)` * `(none)`
- Query file: `queries/mranks.sql` - file: `queries/mranks.sql`
- Requests the maplist of the server and related data. - 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]` * `?map=[map name]`
- Query file: `queries/mleaderboard-ojoin.sql` - file: `queries/mleaderboard-ojoin.sql`
- Requests the leaderboard of the map. - Requests the leaderboard of the map.
* `?player=[clientid]` * `?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. - 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 files `allmaps.py`, `output/leaderboard.css`, `overview.html`, `map.html` produce the output.
Before executing `allmaps.py`, copy and modify the templates.
cp templates/overview.html .
cp templates/map.html .
`allmaps.py` outputs an html file for all distinct maps in the database. The leaderboards for each map (equivalent to `?map=[map name]`) are in `output/maps/`.
## Game Versions Used Under: ## Game Versions Used Under:
* Xonotic 0.8.1
* Xonotic 0.8.2 * Xonotic 0.8.1
* Xonotic 0.8.2
## Compilers ## Compilers
* gcc (GCC) 10.2.1
* MinGW, GCC 4.7.1 * MinGW, GCC 4.7.1
__________________ __________________
This program uses an sqlite3 database file created from `~/.xonotic/data/data/server.db`. The script `scripts/import-from-xon.py` is based on `https://git.teknik.io/antares/xonotic-py-sqlite3-defrag2db` by [Antares](https://antares.neocities.org/).
The database may be built using [xonotic-py-sqlite3-defrag2db](https://git.teknik.io/antares/xonotic-py-sqlite3-defrag2db).

View File

@ -14,12 +14,18 @@ void hsl2rgb(struct Rgb *, const struct Hls const *);
void rgb2hsl(struct Hls *, const struct Rgb 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*); void print_plname(const char*);
static char* append_to_str(char *, const char *);
char* colorize_name(char *, char * const);
#endif #endif

17
output/leaderboard.css Normal file
View File

@ -0,0 +1,17 @@
footer {
text-align:center;
}
table, th, td {
border: 1px solid grey;
border-collapse: collapse;
}
table {
width:100%;
}
th, td {
width:auto;
text-align:center;
word-wrap: break-word;
margin: 1em 1em 1em 1em;
}

View File

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

View File

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

View File

@ -5,4 +5,4 @@ where Cts_ranks.mapid = Cts_times.mapid
and cryptokey = idvalue and cryptokey = idvalue
and idrank = 1 and idrank = 1
group by Cts_ranks.mapid group by Cts_ranks.mapid
order by max(trank); order by count(trank) DESC;

53
queries/schema.sql Normal file
View File

@ -0,0 +1,53 @@
DROP TABLE IF EXISTS Cts_times;
CREATE TABLE Cts_times(
mapid TEXT,
gametype TEXT,
trank INT,
tvalue INT,
PRIMARY KEY (mapid, gametype, trank),
FOREIGN KEY (mapid, gametype, trank) REFERENCES Cts_ranks(mapid, gametype, idrank)
);
DROP TABLE IF EXISTS Cts_ranks;
CREATE TABLE Cts_ranks(
mapid TEXT,
gametype TEXT,
idrank INT,
idvalue TEXT,
PRIMARY KEY (mapid, gametype, idrank)
);
DROP TABLE IF EXISTS Id2alias;
CREATE TABLE Id2alias(
rtype TEXT,
cryptokey TEXT,
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
-- // player improved his existing record, only have to iterate on ranks between new and old recs
-- for (i = prevpos; i > newpos; --i)
-- {
-- db_put(ServerProgsDB, strcat(map, rr, "time", ftos(i)), ftos(race_readTime(map, i - 1)));
-- db_put(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(i)), race_readUID(map, i - 1));
-- }
-- ....
-- // store new time itself
-- db_put(ServerProgsDB, strcat(map, rr, "time", ftos(i)), ftos(race_readTime(map, i - 1)));
-- db_put(ServerProgsDB, strcat(map, rr, "crypto_idfp", ftos(i)), race_readUID(map, i - 1));
-- 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.

100
scripts/allmaps.py Normal file
View File

@ -0,0 +1,100 @@
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 = []
con = sql.connect(database)
with con:
cursor = con.cursor()
try:
cursor.execute("select distinct mapid from Cts_times;")
output = cursor.fetchall()
except sql.Error:
print("Shit is fucked.")
return output
# if there is no query then it outputs the index file.
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 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()
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()
# for each map generate an html file.
for game_map in maps:
# game_map is a tuple obj.
map_name = game_map[0]
query = {"QUERY_STRING" : ("map=%s" % 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=run_cgi(query),
speed=sentence
)
)
# fout.write(template % (title, map_name, table))
return True
if __name__ == "__main__":
success = False
try:
success = main()
except FileNotFoundError:
traceback.print_exc()
print("\n\t The script probably didn't find the page templates needed to generate a page. You can copy minimal working examples from the repository at templates/.")
if success:
print("allmaps.py - Generated pages for all maps.")
pass

255
scripts/import-from-xon.py Normal file
View File

@ -0,0 +1,255 @@
import re, argparse
import sqlite3 as sql
import logging
import logging.handlers
from os.path import isfile, exists
from urllib.parse import unquote
import sys, traceback
#------------------------------------------------+
# get_list_from_server_txt
#------------------------------------------------+
# Rows in the game server database are
# occasionally concatenated into one line.
# To simplify, they are unmerged.
#
# The final result is every row in the game server
# database with its own index in a list.
#------------------------------------------------+
def get_list_from_server_txt(filename):
def unmerge_rows(line, char, x):
chunks = line.split(char)
newrows = [char.join(chunks[:x]), char.join(chunks[x:])]
# need to prefix each row with a char.
# only the last row will be missing it.
newrows[-1] = char + newrows[-1]
if newrows[-1].count(char) > (x - 1):
newrows += unmerge_rows(newrows.pop(), char, x)
return newrows
rows = []
with open(filename, 'r') as f:
# server database has a lot of newlines to ignore
rows = [line for line in f if line != "\n"]
output = []
n = 3
backslash = '\\'
for row in rows:
# The first and last column is prefixed with a backslash.
# So multiple rows on one line should be split at the 3rd backslash.
if row.count(backslash) > (n - 1):
unmerged = unmerge_rows(row, backslash, n)
for u in unmerged:
output.append(u)
else:
output.append(row)
return output
def init_logging(folder, base_file_name="dbimport-%s.log"):
if not exists(folder):
return False
filename = "%s/%s" % (folder, base_file_name)
i = 0
while exists(filename % i):
i += 1
filename = filename % i
f = open(filename, mode='a', encoding='utf-8')
logging.basicConfig(stream=f, level=logging.DEBUG)
return filename
#------------------------------------------------+
# uid2namefix
#------------------------------------------------+
# Unlike other rows,
# the separator character, '/' is part of the value of the second column.
# so an ordinary match for '/' or '\' can not be done like the other types of rows.
# example from game server db:
# \/uid2name/Mnumg2Yh/yxNFDTqGI+YyhlM7QDI0fpEmAaBJ8cI5dU=\Tuxxy
# it should become:
# ["uid2name", "Mnumg2Yh/yxNFDTqGI+YyhlM7QDI0fpEmAaBJ8cI5dU=", "Tuxxy"]
def uid2namefix(row):
# quick fix
# replace first and last occurrence of backslash
# this results in [,/uid2name/cryptoid_fp, name]
e = re.sub(r'^([^\\]*)\\|\\(?=[^\\]*$)', ',', row)
# replace first two occurence of forward slash
# this results in [,,uid2name,cryptoid_fp, name]
ee = e.replace('/', ',', 2)
# split on comma
# but start from index 2 because the first commas are left over
# c is now a list of strings.
# ["uid2name", <crypto_idfp value>, <player name value>]
c = ee[2:].split(',')
c[2] = unquote(c[2])
c[2] = c[2].strip('\n')
return c
# O(n) and organize cts related data into list of rows.
def filters(db):
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:
ti.append(uid2namefix(d))
else:
# regex:
# find substrings that do not contain backslash, forwardslash, or newline.
e = re.findall(r'[^\\/\n]+', d)
if d.find("cts100record/time") != -1:
e[rank_index] = int(e[rank_index].replace("time", ""))
tt.append(e)
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)
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
def insert_to_database(d, s):
def insert(c, q, d):
for x in d:
# possible to do executemany
# but want to be able to catch the problematic rows
# as it is iterated through.
# and proceed with adding OK rows.
try:
c.execute(q, x)
except sql.ProgrammingError as e:
print(e)
print(x)
return
con = sql.connect(d)
with con:
csr = con.cursor()
try:
times, \
ranks, \
ids, \
speed, \
speed_ids = filters(get_list_from_server_txt(s))
if times:
insert(csr, "INSERT OR REPLACE INTO Cts_times VALUES(?, ?, ?, ?)", times)
logging.info('\n'.join(y for y in [str(x) for x in times]))
if ranks:
insert(csr, "INSERT OR REPLACE INTO Cts_ranks VALUES(?, ?, ?, ?)", ranks)
logging.info('\n'.join(y for y in [str(x) for x in ranks]))
if ids:
insert(csr, "INSERT OR REPLACE INTO Id2alias VALUES(?, ?, ?)", ids)
logging.info('\n'.join(y for y in [str(x) for x in ids]))
if speed:
insert(csr, "INSERT OR REPLACE INTO Speed VALUES(?, ?)", speed)
if speed_ids:
insert(csr, "INSERT OR REPLACE INTO Fastest_players VALUES(?, ?)", speed_ids)
except sql.Error:
logging.exception("sql error encountered in function 'i'")
if con:
con.rollback()
def write_query(out_file, data):
if exists(out_file):
print("stopped: output file already exists", file=sys.stderr)
return False
times, \
ranks, \
ids, \
speed, \
speed_ids = filters(get_list_from_server_txt(data))
with open(out_file, 'w', encoding='utf-8') as file_handle:
for t in times:
file_handle.write("INSERT OR REPLACE INTO Cts_times VALUES(\'%s\', \'%s\', %s, %s);\n" % tuple(t))
for r in ranks:
file_handle.write("INSERT OR REPLACE INTO Cts_ranks VALUES(\'%s\', \'%s\', %s, \'%s\');\n" % tuple(r))
for i in ids:
file_handle.write("INSERT OR REPLACE INTO Id2alias VALUES(\'%s\', \'%s\', \'%s\');\n" % tuple(i))
return True
# Test whether repeat rows are added.
def check_duplicates(database, data):
c = sql.connect(database)
p = True
with c:
cs = c.cursor()
try:
logging.info("Inserting into database (1/2)")
insert_to_database(database, data)
logging.info("Querying (1/2)")
cs.execute("SELECT * FROM Cts_times")
a = cs.fetchall()
cs.execute("SELECT * FROM Cts_ranks")
b = cs.fetchall()
cs.execute("SELECT * FROM Id2alias")
c = cs.fetchall()
logging.info("Inserting into database (2/2)")
insert_to_database(database, data)
logging.info("Querying (2/2)")
cs.execute("SELECT * FROM Cts_times")
x = cs.fetchall()
cs.execute("SELECT * FROM Cts_ranks")
y = cs.fetchall()
cs.execute("SELECT * FROM Id2alias")
z = cs.fetchall()
if len(a) != len(x):
logging.error("Issue with Cts_times")
p = False
if len(b) != len(y):
logging.error("Issue with Cts_ranks")
p = False
if len(c) != len(z):
logging.error("Issue with Id2alias")
p = False
if p:
logging.info("Database ok - no repeat rows added.")
except sql.Error:
logging.exception("encountered sql error in function 'duplicate test'.")
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument('dest',
help="destination, a sqlite3 database (or query file, if given -q flag)")
ap.add_argument('src',
help="source, should be data generated by a Xonotic server")
ap.add_argument('-q', '--export-query',
action='store_true',
help="write query file (as opposed to executing / inserting rows into database)")
ap.add_argument('-t', '--test',
action='store_true',
help="test database for duplicates")
ap.add_argument('-l', '--log-dir',
type=str,
help="set folder to store log files")
args = ap.parse_args()
log_dir = args.log_dir or "logs"
log_file = init_logging(log_dir)
if log_file:
print("writing log to folder '%s'," % log_dir, log_file, file=sys.stderr)
else:
print("exited: logging not initialized (folder '%s' does not exist)" % log_dir, file=sys.stderr)
exit()
try:
if args.test:
check_duplicates(args.dest, args.src)
if args.export_query:
write_query(args.dest, args.src)
else:
insert_to_database(args.dest, args.src)
except FileNotFoundError:
traceback.print_exc()
print("\n\t exited: no input file to work with.", file=sys.stderr)

View File

@ -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) { switch(d) {
case 0: case 0:
printf("<span style='color:rgb(128,128,128)'>"); return "<span style='color:rgb(128,128,128)'>";
break;
case 1: case 1:
printf("<span style='color:rgb(255,0,0)'>"); return "<span style='color:rgb(255,0,0)'>";
break;
case 2: case 2:
printf("<span style='color:rgb(51,255,0)'>"); return "<span style='color:rgb(51,255,0)'>";
break;
case 3: case 3:
printf("<span style='color:rgb(255,255,0)'>"); return "<span style='color:rgb(255,255,0)'>";
break;
case 4: case 4:
printf("<span style='color:rgb(51,102,255)'>"); return "<span style='color:rgb(51,102,255)'>";
break;
case 5: case 5:
printf("<span style='color:rgb(51,255,255)'>"); return "<span style='color:rgb(51,255,255)'>";
break;
case 6: case 6:
printf("<span style='color:rgb(255,51,102)'>"); return "<span style='color:rgb(255,51,102)'>";
break;
case 7: case 7:
printf("<span style='color:rgb(255,255,255)'>"); return "<span style='color:rgb(255,255,255)'>";
break;
case 8: case 8:
printf("<span style='color:rgb(153,153,153)'>"); return "<span style='color:rgb(153,153,153)'>";
break;
case 9: case 9:
printf("<span style='color:rgb(128,128,128)'>"); return "<span style='color:rgb(128,128,128)'>";
break;
} }
} }
static void hexspan(const char *str) { static void hexspan(char *buf, int bufsize, const char *str) {
// length of ...
// "<span style=\"color:rgb(%d,%d,%d)\">"
// where each %d ranges from 0 to 255
// char buf[40];
const char h1[2] = {str[0], '\0'}; const char h1[2] = {str[0], '\0'};
const char h2[2] = {str[1], '\0'}; const char h2[2] = {str[1], '\0'};
const char h3[2] = {str[2], '\0'}; const char h3[2] = {str[2], '\0'};
@ -131,17 +125,21 @@ static void hexspan(const char *str) {
nhls.l = MIN_CONTRAST; nhls.l = MIN_CONTRAST;
hsl2rgb(&nrgb, &nhls); hsl2rgb(&nrgb, &nhls);
} }
printf("<span style=\"color:rgb(%d,%d,%d)\">", nrgb.r, nrgb.g, nrgb.b); int wrote = snprintf(
buf, bufsize,
"<span style=\"color:rgb(%d,%d,%d)\">",
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 *token = strtok(str, "^");
char c; char c;
printf("<TD>");
while (token) { while (token) {
c = token[0]; c = token[0];
if (isdigit(c)) { if (isdigit(c)) {
decspan(c - '0'); printf( decspan(c - '0') );
if (strlen(token) > 1) { if (strlen(token) > 1) {
printf("%s", token + 1); printf("%s", token + 1);
} }
@ -150,7 +148,9 @@ static void b(char * const str) {
(isxdigit(token[1]) && (isxdigit(token[1]) &&
isxdigit(token[2]) && isxdigit(token[2]) &&
isxdigit(token[3]))) { isxdigit(token[3]))) {
hexspan(token + 1); //exclude x char tag[TAG_LEN];
hexspan(tag, TAG_LEN, token + 1);
printf( tag ); //exclude x
if (strlen(token) > 4){ if (strlen(token) > 4){
printf("%s", token + 4); printf("%s", token + 4);
} }
@ -160,7 +160,16 @@ static void b(char * const str) {
} }
token = strtok(NULL, "^"); token = strtok(NULL, "^");
} }
printf("</TD>"); }
static void sanitize(char *user_name) {
if (user_name == NULL) {
return;
}
char *pos = user_name;
while (pos = strstr(pos, "^^")) {
strcpy(pos, (pos + 1));
}
} }
void print_plname(const char* str) { void print_plname(const char* str) {
@ -170,11 +179,85 @@ void print_plname(const char* str) {
char *copy; char *copy;
copy = calloc(strlen(str) + 1, sizeof(char)); copy = calloc(strlen(str) + 1, sizeof(char));
strcpy(copy, str); strcpy(copy, str);
char *pos = copy; sanitize(copy);
while (pos = strstr(pos, "^^")) { colorize_noalloc(copy);
strcpy(pos, (pos + 1)); fflush(stdout);
}
b(copy);
free(copy); free(copy);
} }
static char* append_to_str(char *dest, const char *src) {
if (dest == NULL || src == NULL) {
fprintf(stderr, "append_to_str(): warning - received null ptr" );
return NULL;
}
size_t new_len = strlen(dest) + strlen(src) + 1;
char *new_str = realloc(dest, new_len);
if (new_str != NULL) {
strcat(new_str, src);
}
return new_str;
}
// the most colorful names are the longest
// names with 8 colors can go to 400 chars
char* colorize_name(char *buf, /*int bufsize,*/ char * const str) {
char *token = strtok(str, "^");
char c;
// unsigned int i = 0;
while (token) {
c = token[0];
if (isdigit(c)) {
// printf("%i : %s\n", i, buf);;
buf = append_to_str(buf, decspan(c - '0') );
if (strlen(token) > 1) {
buf = append_to_str(buf, token + 1);
}
buf = append_to_str(buf, "</span>");
} else if ((c == 'x' && strlen(token) > 3) &&
(isxdigit(token[1]) &&
isxdigit(token[2]) &&
isxdigit(token[3]))) {
char tag[TAG_LEN];
hexspan(tag, TAG_LEN, token + 1); //exclude x
buf = append_to_str(buf, tag );
if (strlen(token) > 4){
buf = append_to_str(buf, token + 4);
}
buf = append_to_str(buf, "</span>");
} else {
buf = append_to_str(buf, token);
}
token = strtok(NULL, "^");
// i++;
}
return buf;
}
/* test:
./colors ^9[^1S^9]^x469Kom^0ier^7
./colors ^9[^1S^9]^^x469Kom^0ier^7
*/
#ifdef COLORS4PYTHON
int main(int argc, const char **argv) {
if (argc < 1) {
return -1;
}
char *colored = (char*)calloc(strlen(argv[1]) + 1, sizeof(char));
char *player_name = (char*)calloc(strlen(argv[1]) + 1, sizeof(char));
strcpy(player_name, argv[1]);
sanitize(player_name);
colored = colorize_name(colored, /*sizeof(colored),*/ player_name);
fprintf(stdout, "%s\n", colored);
// clean up
fflush(stdout);
free(colored);
free(player_name);
return 0;
}
#endif

View File

@ -5,9 +5,10 @@
#include <sqlite3.h> #include <sqlite3.h>
#include "colors.h" #include "colors.h"
#define QOVERVIEW 'o' #define QOVERVIEW 'o' // default case - see get_filename()
#define QRPLAYER 'p' #define QRPLAYER 'p' // ?player=
#define QMLEADERBOARD 'm' #define QMLEADERBOARD 'm' // ?map=
#define QFASTEST 'f'
static inline char *get_filename(char * const c) { static inline char *get_filename(char * const c) {
char *qout = "queries/mranks.sql"; char *qout = "queries/mranks.sql";
@ -22,6 +23,9 @@ static inline char *get_filename(char * const c) {
case QRPLAYER: case QRPLAYER:
qout = "queries/rplayers.sql"; qout = "queries/rplayers.sql";
break; break;
case QFASTEST:
qout = "queries/fastest-players.sql";
break;
} }
} }
return qout; return qout;
@ -31,7 +35,7 @@ static inline void print_tblheader(const char *c) {
char *labels; char *labels;
switch (*c) { switch (*c) {
default: default:
labels = "<TABLE class='center'>\ labels = "<TABLE class='leaderboard'>\
<TH class='tablename' COLSPAN='4'> <H3><BR>Map List</H3> </TH>\ <TH class='tablename' COLSPAN='4'> <H3><BR>Map List</H3> </TH>\
<TR>\ <TR>\
<TH class='columnname'>Name</TH>\ <TH class='columnname'>Name</TH>\
@ -41,7 +45,7 @@ static inline void print_tblheader(const char *c) {
</TR>"; </TR>";
break; break;
case QMLEADERBOARD: case QMLEADERBOARD:
labels = "<TABLE class='center'>\ labels = "<TABLE class='leaderboard'>\
<TH class='tablename' COLSPAN='3'><H3><BR>Leaderboard</H3></TH>\ <TH class='tablename' COLSPAN='3'><H3><BR>Leaderboard</H3></TH>\
<TR>\ <TR>\
<TH class='columnname'>Rank</TH>\ <TH class='columnname'>Rank</TH>\
@ -50,7 +54,7 @@ static inline void print_tblheader(const char *c) {
</TR>"; </TR>";
break; break;
case QRPLAYER: case QRPLAYER:
labels = "<TABLE class='center'>\ labels = "<TABLE class='leaderboard'>\
<TH class='tablename' COLSPAN='3'> <H3><BR>Ranks</H3> </TH>\ <TH class='tablename' COLSPAN='3'> <H3><BR>Ranks</H3> </TH>\
<TR>\ <TR>\
<TH class='columnname'>Name</TH>\ <TH class='columnname'>Name</TH>\
@ -58,6 +62,15 @@ static inline void print_tblheader(const char *c) {
<TH class='columnname'>Rank</TH>\ <TH class='columnname'>Rank</TH>\
</TR>"; </TR>";
break; break;
case QFASTEST:
labels = "<table class='leaderboard'>\
<th class='tablename' COLSPAN='4'> <H3><BR>Map List</H3> </th>\
<tr>\
<th class='columnname'>Name</th>\
<th class='columnname'>Records</th>\
<th class='columnname'>Highest Velocity (qu/s)</th>\
<th class='columnname'>Held By</th>\
</tr>";
} }
printf("%s", labels); printf("%s", labels);
} }
@ -84,9 +97,11 @@ static void print_time(const unsigned char *strcs) {
static void qresult(sqlite3_stmt * const sp, const char *c) { static void qresult(sqlite3_stmt * const sp, const char *c) {
#define ISPLAYERNAME(x, y) (y == 1 && *x == QMLEADERBOARD) || \ #define ISPLAYERNAME(x, y) (y == 1 && *x == QMLEADERBOARD) || \
(y == 3 && *x == QOVERVIEW)|| \ (y == 3 && *x == QOVERVIEW)|| \
(y == 0 && *x == QRPLAYER) (y == 0 && *x == QRPLAYER) || \
(y == 3 && *x == QFASTEST)
#define ISMAPNAME(x, y) (y == 0 && *x == QOVERVIEW) ||\ #define ISMAPNAME(x, y) (y == 0 && *x == QOVERVIEW) ||\
(y == 1 && *x == QRPLAYER) (y == 1 && *x == QRPLAYER) || \
(y == 0 && *x == QFASTEST)
int e; int e;
unsigned int i; unsigned int i;
const unsigned int cc = sqlite3_column_count(sp); const unsigned int cc = sqlite3_column_count(sp);
@ -96,16 +111,24 @@ static void qresult(sqlite3_stmt * const sp, const char *c) {
for (i = 0; i < cc; ++i) { for (i = 0; i < cc; ++i) {
unsigned const char * const field = sqlite3_column_text(sp, i); unsigned const char * const field = sqlite3_column_text(sp, i);
if (ISPLAYERNAME(c, i)) { if (ISPLAYERNAME(c, i)) {
printf("<TD>");
print_plname(field); print_plname(field);
printf("</TD>");
} else if (ISMAPNAME(c, i)) { } else if (ISMAPNAME(c, i)) {
#ifdef STATICGEN
printf("<TD><a href='./maps/%s.html'>%s</a></TD>", field, field);
#else
printf("<TD><a href='/cgi/cts?map=%s'>%s</a></TD>", field, field); printf("<TD><a href='/cgi/cts?map=%s'>%s</a></TD>", field, field);
#endif
} else if (i == 2 && (*c == QMLEADERBOARD || *c == QOVERVIEW)) { } else if (i == 2 && (*c == QMLEADERBOARD || *c == QOVERVIEW)) {
print_time(field); print_time(field);
} else if (i == 2 && *c == QFASTEST) { // velocity
printf("<TD>%.2f</TD>", atof(field) );
} else { } else {
printf("<TD>%s</TD>", field); printf("<TD>%s</TD>", field);
} }
} }
printf("</TR>"); printf("</TR>\n");
} }
printf("</TABLE>"); printf("</TABLE>");
} }

View File

@ -8,10 +8,7 @@ void html(void) {
<link rel=\"stylesheet\" type=\"text/css\" href=\"page.css\">\ <link rel=\"stylesheet\" type=\"text/css\" href=\"page.css\">\
<title>/v/ - Xonotic</title>\ <title>/v/ - Xonotic</title>\
<p class='hidden'>:-) / nice one<br></p>"; <p class='hidden'>:-) / nice one<br></p>";
const char *html_bot = "<br><br><p classname='footer'>Pages under construction.<br>\ const char *html_bot = "<br><br>\
Service may sporadically become unavailable.<br>\
In-game database is not directly synced with this web server.\
</p>\
</body></html>"; </body></html>";
const char *html_mid = "<br>\ const char *html_mid = "<br>\
<H2>hi / good luck and have fun.<br><br>Available Pages</H2>\ <H2>hi / good luck and have fun.<br><br>Available Pages</H2>\
@ -29,7 +26,18 @@ void html(void) {
printf("%s", html_bot); printf("%s", html_bot);
} }
// use with template
void templated(void) {
const char *qstr = getenv("QUERY_STRING");
getquery(qstr);
return;
}
int main(void) { int main(void) {
#ifdef STATICGEN
templated();
#else
html(); html();
#endif
return 0; return 0;
} }

24
templates/map.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title} - Leaderboards - Minimal Working Example</title>
<link href="/style.css" rel="stylesheet" type="text/css" media="all">
<link href="../leaderboard.css" rel="stylesheet" type="text/css" media="all">
</head>
<body>
<!-- map name -->
<h1>{map_name}</h1>
<!-- code generated table goes here -->
{table}
<p>{speed}</p>
<footer>
<p>Page generated using <a href="https://notabug.org/scuti/xdfcgi">xdfcgi</a> by <a href="https://scuti.neocities.org/">scuti</a></p>
</footer>
</body>
</html>

19
templates/overview.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CTS Leaderboards - Minimal Working Example</title>
<link href="/style.css" rel="stylesheet" type="text/css" media="all">
<link href="./leaderboard.css" rel="stylesheet" type="text/css" media="all">
</head>
<body>
<!-- code generated table goes here -->
%s
<footer>
<p>Page generated using <a href="https://notabug.org/scuti/xdfcgi">xdfcgi</a> by <a href="https://scuti.neocities.org/">scuti</a></p>
</footer>
</body>
</html>