Statistics CGI program and hacky display code.

This is what is running on stats.wesnoth.org.
This commit is contained in:
Rusty Russell 2007-12-26 07:13:19 +00:00
parent 2c35486873
commit b1a0af242c
36 changed files with 2633 additions and 0 deletions

19
utils/stats/Makefile Normal file
View file

@ -0,0 +1,19 @@
CFLAGS=-Wall -g -Wunused-parameter -W -Wmissing-declarations
LDFLAGS=-lsqlite3 -lm
all: upload.cgi graph db2wml
upload.cgi: upload.cgi.o utils.o sqlite3_database.o
graph: graph.o utils.o sqlite3_database.o
db2wml: db2wml.o utils.o sqlite3_database.o
clean:
rm -f graph upload.cgi *.o
# Testsuite: .input* are fed into database, then .test run, compared with .result.
check: upload.cgi
@./run-tests.sh
clean-db: upload.cgi
rm -f wesnoth-uploads.db
./wesnoth-upload.cgi --initialize

View file

@ -0,0 +1,14 @@
# Shell routine to check QUERY_STRING args.
# Check vars are safe before we set them.
for f in `echo "$QUERY_STRING" | tr '&' ' '`; do
case "$f" in
W_*)
if echo "$f" | grep -qv '^[A-Za-z0-9_]*=[A-Za-z0-9_+.,-]*$'; then exit 1; fi
;;
*)
exit 1
;;
esac
done
eval `echo "$QUERY_STRING" | tr '&' ' '`

View file

@ -0,0 +1,47 @@
#! /bin/sh
GRAPH=/home/rusty/wesnoth/graph
DATABASE=/home/rusty/wesnoth/wesnoth-uploads.db
. check_args.sh
draw()
{
case "$W_TYPE" in
gold)
$GRAPH campaign_view gold 'start_turn = 0' $W_CAMPAIGN $W_DIFF $W_VERSION "" `echo $W_SCENARIOS | tr , ' '`;;
loss_abort)
$GRAPH campaign_view 'count(*)' 'result != 1' $W_CAMPAIGN $W_DIFF $W_VERSION "" `echo $W_SCENARIOS | tr , ' '`;;
wins)
$GRAPH campaign_view 'count(*)' 'result = 1' $W_CAMPAIGN $W_DIFF $W_VERSION "" `echo $W_SCENARIOS | tr , ' '`;;
win_turn)
$GRAPH campaign_view 'end_turn*100/(num_turns+1)' 'result = 1' $W_CAMPAIGN $W_DIFF $W_VERSION "" `echo $W_SCENARIOS | tr , ' '`;;
level)
$GRAPH units_view 'sum(count)' "start_turn = 0 AND level = $W_LEVEL" $W_CAMPAIGN $W_DIFF $W_VERSION "GROUP BY game" `echo $W_SCENARIOS | tr , ' '`;;
time)
$GRAPH campaign_view 'sum(time / 60)' 'time > 60' $W_CAMPAIGN $W_DIFF $W_VERSION "GROUP BY player" `echo $W_SCENARIOS | tr , ' '`;;
player)
$GRAPH --progress $W_CAMPAIGN $W_DIFF $W_VERSION $W_PLAYER `echo $W_SCENARIOS | tr , ' '`;;
esac
}
if [ -n "$W_PNG" ]; then
echo "Content-type: image/png"
echo
TMPFILE=`mktemp`
trap "rm -f $TMPFILE" 0
draw > $TMPFILE
# Hack: find viewBox.
X_Y=`sed -n 's/.*viewBox="-25 -25 \([0-9]*\) \([0-9]*\)".*/\1,\2/p' < $TMPFILE`
X=`echo $X_Y | cut -d, -f1`
Y=`echo $X_Y | cut -d, -f2`
if [ $Y -gt 160 ]; then
# Scale back to 160 high.
X=`echo "scale=2; $X * (160 / $Y)" | bc | cut -d. -f1`
Y=160
fi
rsvg -w$X -h$Y $TMPFILE /dev/fd/1
else
echo "Content-type: image/svg+xml"
echo
draw
fi

View file

@ -0,0 +1,40 @@
<div class="visualClear"></div>
</div>
</div> <!-- end content -->
<div id="footer">
<div class="visualClear"></div>
<div class="portlet" id="p-personal">
<ul>
<li id="pt-login">
<a href="http://www.wesnoth.org/mw/index.php?title=Special:Userlogin&amp;returnto=About">Create an account or log in</a>
</li>
</ul>
</div>
<div class="portlet" id="p-fixed">
<ul>
<li><a href="http://www.wesnoth.org/wiki/HomePage">Home</a></li>
<li><a href="http://www.wesnoth.org/wiki/Special:Recentchanges">Recent changes</a></li>
</ul>
</div>
<div class="visualClear"></div>
<div id="note">
<p>Copyright &copy; 2003-2005 The Battle for Wesnoth</p>
</div>
</div> <!-- end footer -->
</div> <!-- end main -->
</div> <!-- end global -->
</body>
</html>

View file

@ -0,0 +1,45 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="KEYWORDS" content="Stats" />
<meta name="robots" content="index,follow" />
<link rel="shortcut icon" href="http://www.wesnoth.org/favicon.ico" />
<link rel="stylesheet" type="text/css" media="print" href="http://www.wesnoth.org/mw/skins/common/commonPrint.css" />
<link rel="shortcut icon" type="image/png" href="http://www.wesnoth.org/mw/skins/glamdrol/ico.png" />
<style type="text/css" media="screen,projection">/*<![CDATA[*/ @import "http://www.wesnoth.org/mw/skins/glamdrol/main.css"; /*]]>*/</style>
<script type="text/javascript" src="http://www.wesnoth.org/mw/index.php?title=-&amp;action=raw&amp;gen=js"></script><script type="text/javascript" src="http://www.wesnoth.org/mw/skins/common/wikibits.js"></script>
<title>Statistics - Gathered from "Help Wesnoth" Volunteers</title>
</head>
<body class="ns-0">
<div id="global">
<div id="header">
<div id="logo">
<a href="http://www.wesnoth.org/"><img alt="Wesnoth logo" src="http://www.wesnoth.org/mw/skins/glamdrol/wesnoth-logo.jpg" /></a>
</div>
</div>
<div id="nav">
<ul>
<li><a href="http://www.wesnoth.org/">Home</a></li>
<li><a href="http://www.wesnoth.org/wiki/Play">Play</a></li>
<li><a href="http://www.wesnoth.org/wiki/Create">Create</a></li>
<li><a href="http://www.wesnoth.org/forum/">Forums</a></li>
<li><a href="http://www.wesnoth.org/wiki/Support">Support</a></li>
<li><a href="http://www.wesnoth.org/wiki/Project">Project</a></li>
<li><a href="http://www.wesnoth.org/wiki/About">About</a></li>
</ul>
</div>
<div id="main">
<div id="content">
<a name="top" id="contentTop"></a>

413
utils/stats/cgi-bin/index.cgi Executable file
View file

@ -0,0 +1,413 @@
#! /bin/sh
DATABASE=/home/rusty/wesnoth/wesnoth-uploads.db
URL="http://ozlabs.org/~rusty/stats.wesnoth.org/query-wesnoth.cgi"
print_header()
{
echo "<h1 class=\"firstHeading\">$1</h1>"
echo '<div id="bodyContent">'
echo '<h3 id="siteSub">From Wesnoth</h3>'
echo '<div id="contentSub"></div>'
}
get_scenario_list()
{
# Before 1.1.2, the versions weren't generally numbered. Now they
# should all be.
case $1 in
1.1-svn|1.1+svn|1.1.1|1.1.1+svn)
case $2 in
CAMPAIGN_HEIR_TO_THE_THRONE)
echo The_Elves_Besieged Blackwater_Port The_Isle_of_Anduin The_Bay_of_Pearls Muff_Malals_Peninsula Isle_of_the_Damned The_Siege_of_Elensefar Crossroads The_Princess_of_Wesnoth The_Valley_of_Death-The_Princesss_Revenge Gryphon_Mountain The_Ford_of_Abez Northern_Winter Mountain_Pass The_Dwarven_Doors Plunging_into_the_Darkness The_Lost_General Hasty_Alliance Scepter A_Choice_Must_Be_Made Snow_Plains Swamp_Of_Dread North_Elves Elven_Council valley_statue return_to_wesnoth trial_clans battle_for_wesnoth
;;
CAMPAIGN_TWO_BROTHERS)
# These are numbered!
/usr/bin/sqlite3 $DATABASE "select DISTINCT scenario FROM campaign_view WHERE campaign = '$2' AND version = '$1';" | sort -n
;;
*)
/usr/bin/sqlite3 $DATABASE "select DISTINCT scenario FROM campaign_view WHERE campaign = '$2' AND version = '$1';"
;;
esac
;;
*)
/usr/bin/sqlite3 $DATABASE "select DISTINCT scenario FROM campaign_view WHERE campaign = '$2' AND version = '$1';" | sort -n
;;
esac
}
# Returns sets of up to 10 comma-separated scenarios, given a scenario list.
chop_scenarios()
{
if [ $# = 1 ]; then
echo $1
return
fi
while [ $# -gt 8 ]; do
echo $1,$2,$3,$4,$5,$6,$7,$8
# Note that this deliberately repeats one.
shift 7
done
if [ $# -gt 1 ]; then
echo $@ | tr ' ' ','
fi
}
colorize()
{
sed -e 's,>aborted<,><font color=\"#ece000\">aborted</font><,g' -e 's,>victory<,><font color=\"green\">victory</font><,g' -e 's,>defeat<,><font color=\"#ff0d00\">defeat</font><,g'
}
# add_href field extra
add_href()
{
sed "s,^<TR><TD>\([^<]*\),<TR><TD><a href=\"?$1=\1\$2_VERSION\">\1,"
}
is_official()
{
case $1 in
1.1-svn)
case $2 in
CAMPAIGN_EASTERN_INVASION|CAMPAIGN_HEIR_TO_THE_THRONE|CAMPAIGN_SON_OF_THE_BLACK_EYE|CAMPAIGN_THE_RISE_OF_WESNOTH|CAMPAIGN_THE_DARK_HORDES|TUTORIAL)
return 0
;;
esac
return 1
;;
# Two brothers added.
1.1+svn|1.1.1)
case $2 in
CAMPAIGN_EASTERN_INVASION|CAMPAIGN_HEIR_TO_THE_THRONE|CAMPAIGN_SON_OF_THE_BLACK_EYE|CAMPAIGN_THE_RISE_OF_WESNOTH|CAMPAIGN_THE_DARK_HORDES|CAMPAIGN_TWO_BROTHERS|TUTORIAL)
return 0
;;
esac
return 1
;;
# 1.1.1+svn: Under the Burning Suns and South Guard added
# Son of Black Eye and Dark Hordes removed
*)
case $2 in
CAMPAIGN_DESERT|CAMPAIGN_THE_SOUTH_GUARD|CAMPAIGN_EASTERN_INVASION|CAMPAIGN_HEIR_TO_THE_THRONE|CAMPAIGN_THE_RISE_OF_WESNOTH|CAMPAIGN_TWO_BROTHERS|TUTORIAL)
return 0
;;
esac
return 1
;;
esac
}
print_main()
{
VERSIONS=`/usr/bin/sqlite3 $DATABASE "SELECT name FROM version_names;"`
# By default, use latest non-svn version.
if [ -z "$W_VERSION" ]; then
W_VERSION=`echo "$VERSIONS" | grep -v svn | tail -1`
fi
print_header "Wesnoth Statistics for $W_VERSION"
sep="Other versions available: "
for ver in $VERSIONS; do
if [ "$ver" != "$W_VERSION" ]; then
echo "$sep <a href=\"?W_VERSION=$ver\">$ver</a> "
sep="|"
fi
done
echo '<h2>Wesnoth Official Campaigns</h2>'
echo '<table border="1"><tr>'
echo '<th>Campaign Name</th>'
echo '<th>Difficulty</th>'
echo '<th>Games Uploaded</th>'
echo '</tr>'
/usr/bin/sqlite3 $DATABASE "SELECT campaign,difficulty,count(*) FROM campaign_view WHERE version='$W_VERSION' GROUP BY campaign,difficulty ORDER BY campaign,count(*) DESC;" | while IFS="|" read campaign diff count; do
if is_official $W_VERSION "$campaign"; then
echo "<TR><TD><a href=\"?W_CAMPAIGN=$campaign&W_DIFF=$diff&W_VERSION=$W_VERSION\">$campaign</a></TD><TD>$diff</TD><TD>$count</TD></TR>"
fi
done
echo '</table>'
echo '<h2>Wesnoth Unofficial Campaigns</h2>'
echo '<table border="1"><tr>'
echo '<th>Campaign Name</th>'
echo '<th>Difficulty</th>'
echo '<th>Games Uploaded</th>'
echo '</tr>'
/usr/bin/sqlite3 $DATABASE "SELECT campaign,difficulty,count(*) FROM campaign_view WHERE version='$W_VERSION' GROUP BY campaign,difficulty ORDER BY campaign,count(*) DESC;" | while IFS="|" read campaign diff count; do
if ! is_official $W_VERSION "$campaign"; then
echo "<TR><TD><a href=\"?W_CAMPAIGN=$campaign&W_DIFF=$diff&W_VERSION=$W_VERSION\">$campaign</a></TD><TD>$diff</TD><TD>$count</TD></TR>"
fi
done
echo '</table>'
echo "<h2><a href=\"?W_PLAYERS=1&W_VERSION=$W_VERSION\">List of Wesnoth Players</a></h2>"
}
print_players()
{
print_header "Wesnoth Players for $W_VERSION"
echo '<h2>Wesnoth Players</h2>'
echo '<table border="1"><tr>'
echo '<th>Player ID</th>'
echo '<th>Games uploaded</th>'
echo '<th>Last upload</th>'
echo '</tr>'
/usr/bin/sqlite3 -html $DATABASE "SELECT DISTINCT campaign_view.player,game_count.games_received-1,game_count.last_upload FROM campaign_view,players,game_count WHERE campaign_view.version = '$W_VERSION' AND game_count.player_ref = players.rowid AND players.id = campaign_view.player;" | sed "s,^<TR><TD>\([^<]*\),<TR><TD><a href=\"?W_PLAYER=\1\&W_VERSION=$W_VERSION\">\1,"
echo '</table>'
}
add_graph()
{
echo "<object type=\"image/svg+xml\" data=\"draw_graph.cgi?$1\" NAME=\"$2\" width=\"100%\"><img src=\"draw_graph.cgi?$1&W_PNG=1\"></object>"
}
# Graph out all campaigns a player played (unless specific requested, from graph)
do_graph_player()
{
echo '(<font color="green">Green</font> means victory, <font color="#ff0d00">red</font> means defeat, <font color="#ece000">yellow</font> means quit. Bars indicate start/finish turn).<br>'
if [ -n "$W_CAMPAIGN" ]; then
EXTRA="AND campaign='$W_CAMPAIGN'"
fi
for campaign_diff in `/usr/bin/sqlite3 $DATABASE "SELECT DISTINCT campaign,difficulty FROM campaign_view WHERE player='$W_PLAYER' AND version='$W_VERSION' $EXTRA;"`; do
campaign=`echo "$campaign_diff" | cut -d\| -f1`
diff=`echo "$campaign_diff" | cut -d\| -f2`
SCENARIOS=$(chop_scenarios $(/usr/bin/sqlite3 $DATABASE "SELECT DISTINCT scenario FROM campaign_view WHERE player='$W_PLAYER' AND version='$W_VERSION' AND difficulty='$diff' AND campaign='$campaign';") )
echo "Campaign $campaign ($diff):"
for scen in $SCENARIOS; do
add_graph "W_SCENARIOS=$scen&W_VERSION=$W_VERSION&W_CAMPAIGN=$campaign&W_DIFF=$diff&W_PLAYER=$W_PLAYER&W_TYPE=player" "Player progress"
done
done
echo "<h2><a href=\"?W_PLAYERP=$W_PLAYER&W_VERSION=$W_VERSION\">Statistics in detail</a></h2>"
}
graph_player()
{
print_header "Wesnoth Player $W_PLAYER"
do_graph_player
}
# Print out all scenarios a player played.
print_player()
{
print_header "Wesnoth Player $W_PLAYER"
echo '<table border="1"><tr>'
echo '<th>ID</th>'
echo '<th>Campaign Name</th>'
echo '<th>Scenario</th>'
echo '<th>Difficulty</th>'
echo '</tr>'
i=1
/usr/bin/sqlite3 $DATABASE "SELECT DISTINCT campaign,scenario,difficulty FROM campaign_view WHERE player='$W_PLAYERP' AND version='$W_VERSION';" | while IFS='|' read campaign scenario diff; do
echo "<TR><TD><a href=\"?W_PLAYERSCENARIO=$i&W_PLAYER=$W_PLAYERP&W_CAMPAIGN=$campaign&W_SCENARIO=$scenario&W_DIFF=$diff&W_VERSION=$W_VERSION\">$i</TD><TD>$campaign</TD><TD>$scenario</TD><TD>$diff</TD></TR>"
i=$(($i + 1))
done
echo '</table>'
}
# "Type" "Name" scenarios...
add_scenario_graphs()
{
ASG_TYPE=$1
ASG_NAME=$2
shift 2
for scen; do
add_graph "W_SCENARIOS=$scen&W_VERSION=$W_VERSION&W_DIFF=$W_DIFF&W_CAMPAIGN=$W_CAMPAIGN&W_TYPE=$ASG_TYPE" "$ASG_NAME"
done
}
# Graph the campaign on a given campaign_names.rowid
graph_campaign()
{
print_header "Wesnoth $W_CAMPAIGN ($W_DIFF)"
SCENARIOS=$(chop_scenarios $(get_scenario_list $W_VERSION $W_CAMPAIGN) )
echo "<h2>Gold at start of game</h2>"
add_scenario_graphs "gold" "Gold" $SCENARIOS
echo "<h2>Losses/quits</h2>"
add_scenario_graphs "loss_abort" "Losses and quits" $SCENARIOS
echo "<h2>Wins</h2>"
add_scenario_graphs "wins" "Victories" $SCENARIOS
echo "<h2>Percent turns used on victory</h2>"
add_scenario_graphs "win_turn" "Percent turns used" $SCENARIOS
echo "<h2>Total minutes per player</h2>"
add_scenario_graphs "time" "Minutes spent" $SCENARIOS
for level in 1 2 3; do
echo "<h2>Count of Level $level units at start of game</h2>"
add_scenario_graphs "level&W_LEVEL=$level" "Level $level units" $SCENARIOS
done
echo "<h2><a href=\"?W_CAMPAIGNP=$W_CAMPAIGN&W_DIFF=$W_DIFF&W_VERSION=$W_VERSION\">Statistics in detail</a></h2>"
}
# Print out details on a given campaign
print_campaign()
{
print_header "Wesnoth $W_CAMPAIGN ($W_DIFF)"
echo '<table border="1"><tr>'
echo '<th>Scenario</th>'
echo '</tr>'
/usr/bin/sqlite3 -html $DATABASE "SELECT DISTINCT scenario FROM campaign_view WHERE campaign = '$W_CAMPAIGNP' AND difficulty = '$W_DIFF' AND version = '$W_VERSION';"| sed "s,^<TR><TD>\([^<]*\),<TR><TD><a href=\"?W_SCENARIO=\1\&W_CAMPAIGN=$W_CAMPAIGNP\&W_VERSION=$W_VERSION\&W_DIFF=$W_DIFF\">\1,"
echo '</table>'
}
# Print out details on a given scenario
print_scenario()
{
print_header "Wesnoth Scenario $W_SCENARIO ($W_CAMPAIGN $W_DIFF)"
echo '<table border="1"><tr>'
echo '<th>Game ID</th>'
echo '<th>Player</th>'
echo '<th>Start turn</th>'
echo '<th>Start gold</th>'
echo '<th>Time taken (sec)</th>'
echo '<th>Result</th>'
echo '<th>End turn</th>'
echo '<th>Num turns</th>'
echo '</tr>'
/usr/bin/sqlite3 -html $DATABASE "SELECT game,player,start_turn,gold,time,CASE result WHEN 0 THEN 'aborted' WHEN 1 THEN 'victory' ELSE 'defeat' END, end_turn, num_turns FROM campaign_view WHERE scenario = '$W_SCENARIO' AND campaign = '$W_CAMPAIGN' AND difficulty = '$W_DIFF';" | sed "s,^<TR><TD>\([^<]*\),<TR><TD><a href=\"?W_GAME=\1\">\1," | colorize
echo '</table>'
}
# Print out details on a given scenario.
print_player_scenario()
{
print_header "Wesnoth Player $W_PLAYER playing $W_SCENARIO ($W_CAMPAIGN $W_DIFF)"
echo "Number of turns in scenario: "
# Ideally, only returns one number...
/usr/bin/sqlite3 $DATABASE "SELECT DISTINCT num_turns from campaign_view WHERE scenario='$W_SCENARIO' AND campaign='$W_CAMPAIGN' AND player='$W_PLAYER' AND difficulty='$W_DIFF' AND version='$W_VERSION';"
echo '<table border="1"><tr>'
echo '<th>ID</th>'
echo '<th>Start turn</th>'
echo '<th>Start gold</th>'
echo '<th>Time taken (sec)</th>'
echo '<th>End turn</th>'
echo '<th>Result</th>'
echo '</tr>'
/usr/bin/sqlite3 -html $DATABASE "SELECT game,start_turn,gold,time,end_turn,CASE result WHEN 0 THEN 'aborted' WHEN 1 THEN 'victory' ELSE 'defeat' END FROM campaign_view WHERE version='$W_VERSION' AND player='$W_PLAYER' AND scenario='$W_SCENARIO' AND difficulty='$W_DIFF' AND campaign='$W_CAMPAIGN' ORDER BY game;" | sed "s,^<TR><TD>\([^<]*\),<TR><TD><a href=\"?W_GAME=\1\">\1," | colorize
echo '</table>'
}
# Print out details and units for a given game.
print_game()
{
print_header "Details of game $W_GAME"
echo '<table border="1"><tr>'
/usr/bin/sqlite3 $DATABASE "SELECT campaign,difficulty,scenario,player,version,gold,start_turn,end_turn,num_turns,time,result FROM campaign_view WHERE game='$W_GAME';" | (IFS="|" read cam diff scen player ver gold st et nt time result
echo "Campaign: $cam<br>"
echo "Difficulty: $diff<br>"
echo "Scenario: $scen<br>"
echo "Player: $player<br>"
echo "Version: $ver<br>"
echo "Start gold: $gold<br>"
echo "Start turn: $st<br>"
echo "End turn: $et<br>"
echo "Num turns: $nt<br>"
echo "Time taken (sec): $time<br>"
echo "Result:"
case $result in
0) echo "<font color=\"#ece000\">aborted</font>";;
1) echo "<font color=\"green\">victory</font>";;
2) echo "<font color=\"#ff0d00\">defeat</font>";;
esac
echo "<br>")
echo "<h2>Important Unit Stats (at Start of Game)</h2>"
echo '<table border="1"><tr>'
echo '<th>Name</th>'
echo '<th>Level</th>'
echo '<th>Experience</th>'
echo '</tr>'
/usr/bin/sqlite3 -html $DATABASE "SELECT unit_names.name,special_units.level,special_units.experience FROM special_units,unit_names WHERE special_units.game_ref=$W_GAME AND unit_names.rowid=special_units.unit_name_ref;"
echo '</table>'
echo "<h2>Wesnoth Unit Summary (at Start of Game)</h2>"
echo '<table border="1"><tr>'
echo '<th>Type</th>'
echo '<th>Level</th>'
echo '<th>Number</th>'
echo '</tr>'
/usr/bin/sqlite3 -html $DATABASE "SELECT unit_types.name,unit_types.level,unit_tallies.count FROM unit_types,unit_tallies WHERE unit_tallies.game_ref=$W_GAME AND unit_types.rowid=unit_tallies.unit_type_ref ORDER BY unit_types.level DESC,unit_types.name;"
echo '</table>'
}
# Simple access page which is given in the Help Wesnoth dialog
my_page()
{
VERSIONS=`/usr/bin/sqlite3 $DATABASE "SELECT DISTINCT version FROM campaign_view WHERE player=$W_P;"`
# By default, use latest version for this player
if [ -z "$W_VERSION" ]; then
W_VERSION=`echo "$VERSIONS" | tail -1`
fi
print_header "Wesnoth Page for Player $W_PLAYER"
echo "Versions played: "
sep=""
for ver in $VERSIONS; do
if [ "$ver" = "$W_VERSION" ]; then
echo "$sep <u>$W_VERSION</u>"
else
echo "$sep <a href=\"?W_P=$W_P&W_VERSION=$ver\">$ver</a> "
fi
sep="|"
done
W_PLAYER=$W_P do_graph_player
}
echo "Content-type: text/html"
echo
cat /home/rusty/public_html/stats.wesnoth.org/header.html
# We accept a simple numeric arg for player id.
if echo "$QUERY_STRING" | grep -q '^[0-9][0-9]*$'; then
QUERY_STRING="W_P=$QUERY_STRING"
fi
. check_args.sh
case "$QUERY_STRING" in
W_PLAYER=*)
graph_player
;;
W_PLAYERP=*)
print_player
;;
W_CAMPAIGN=*)
graph_campaign
;;
W_CAMPAIGNP=*)
print_campaign
;;
W_SCENARIO=*)
print_scenario
;;
W_PLAYERSCENARIO=*)
print_player_scenario
;;
W_GAME=*)
print_game
;;
W_P=*)
my_page
;;
W_PLAYERS=*)
print_players
;;
*)
print_main
;;
esac
echo '<div id="lastmod"> This page based generated from a database, which was last modified '`date -u -r $DATABASE`'.</div>'
cat /home/rusty/public_html/stats.wesnoth.org/footer.html

30
utils/stats/database.h Normal file
View file

@ -0,0 +1,30 @@
/* Simple SQL-style database ops. Currently implemented for sqlite3. */
#ifndef _UPLOAD_ANALYSIS_DATABASE_H
#define _UPLOAD_ANALYSIS_DATABASE_H
#include <stdbool.h>
/* Returns handle to the database.. */
void *db_open(const char *file);
/* Runs query (SELECT). Fills in columns. */
struct db_query
{
unsigned int num_rows;
char ***rows;
};
struct db_query *db_query(void *h, const char *query);
/* Runs command (CREATE TABLE/INSERT) */
void db_command(void *h, const char *command);
/* Starts transaction. Doesn't need to nest. */
void db_transaction_start(void *h);
/* Finishes transaction, or rolls it back and caller needs to start again. */
bool db_transaction_finish(void *h);
/* Closes database (only called when everything OK). */
void db_close(void *h);
#endif /* _UPLOAD_ANALYSIS_DATABASE_H */

121
utils/stats/db2wml.c Normal file
View file

@ -0,0 +1,121 @@
/* Create WML upload file for a given game. */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "database.h"
#include "utils.h"
#define DATABASE_FILE "/home/rusty/wesnoth/wesnoth-uploads.db"
int log_fd = STDERR_FILENO;
int main(int argc, char *argv[])
{
void *h;
char *q, *filename;
unsigned int i;
int level;
struct db_query *query;
if (argc != 3)
barf("Usage: %s <database> <games.rowid>\n", argv[0]);
h = db_open(argv[1]);
/*
campaign="CAMPAIGN_HEIR_TO_THE_THRONE"
difficulty="NORMAL"
scenario="Mountain_Pass"
gold="549"
time="2052"
num_turns="25"
start_turn="10"
version="1.1-svn"
*/
q = aprintf("SELECT campaign_names.name,difficulty_names.name,scenario_names.name,games.gold,games.start_time,games.start_turn,version_names.name,games.result,games.end_time,games.end_gold,games.game_number,players.id FROM players,campaign_names,difficulty_names,scenario_names,campaigns,scenarios,games,version_names WHERE games.rowid='%s' AND games.scenario_ref = scenarios.rowid AND scenarios.campaign_ref = campaigns.rowid AND campaigns.difficulty_name_ref = difficulty_names.rowid AND campaigns.campaign_name_ref = campaign_names.rowid AND games.version_name_ref = version_names.rowid AND campaigns.player_ref = players.rowid AND scenario_names.rowid = scenarios.scenario_name_ref;", argv[2]);
query = db_query(h, q);
if (atoi(query->rows[0][8]) - atoi(query->rows[0][4]) < 100) {
fprintf(stderr, "Only lasted from '%s' to '%s'\n",
query->rows[0][4], query->rows[0][8]);
exit(0);
}
filename = aprintf("%08i", atoi(query->rows[0][10]));
dup2(open(filename, O_WRONLY|O_TRUNC|O_CREAT, 0666), STDOUT_FILENO);
printf("format_version=\"1\"\n"
"version=\"%s\"\n"
"id=\"%s\"\n",
query->rows[0][6],
query->rows[0][11]);
printf("[game]\n"
"\tcampaign=\"%s\"\n"
"\tdifficulty=\"%s\"\n"
"\tscenario=\"%s\"\n"
"\tgold=\"%s\"\n"
"\ttime=\"%s\"\n"
"\tstart_turn=\"%s\"\n",
query->rows[0][0],
query->rows[0][1],
query->rows[0][2],
query->rows[0][3],
query->rows[0][4],
query->rows[0][5]);
if (streq(query->rows[0][7], "0")) {
printf("\t[quit]\n"
"\t\ttime=\"%s\"\n"
"\t[/quit]\n",
query->rows[0][8]);
} else if (streq(query->rows[0][7], "1")) {
printf("\t[victory]\n"
"\t\ttime=\"%s\"\n"
"\t\tgold=\"%s\"\n"
"\t[/victory]\n",
query->rows[0][8],
query->rows[0][9]);
} else {
printf("\t[defeat]\n"
"\t\ttime=\"%s\"\n"
"\t[/defeat]\n",
query->rows[0][8]);
}
q = aprintf("SELECT unit_names.name,special_units.level,special_units.experience FROM unit_names,special_units WHERE special_units.game_ref = '%s' AND unit_names.rowid = special_units.unit_name_ref;", argv[2]);
query = db_query(h, q);
for (i = 0; i < query->num_rows; i++)
printf("\t[special-unit]\n"
"\t\tname=\"%s\"\n"
"\t\tlevel=\"%s\"\n"
"\t\texperience=\"%s\"\n"
"\t[/special-unit]\n",
query->rows[i][0], query->rows[i][1],query->rows[i][2]);
level = -1;
printf("\t[units-by-level]\n");
q = aprintf("SELECT unit_types.level,unit_types.name,unit_tallies.count FROM unit_tallies,unit_types WHERE unit_tallies.game_ref = '%s' AND unit_types.rowid = unit_tallies.unit_type_ref ORDER BY unit_types.level ASC;", argv[2]);
query = db_query(h, q);
for (i = 0; i < query->num_rows; i++) {
if (atoi(query->rows[i][0]) != level) {
if (i != 0)
printf("\t\t[/%i]\n", level);
level = atoi(query->rows[i][0]);
printf("\t\t[%i]\n", level);
}
printf("\t\t\t[%s]\n"
"\t\t\t\tcount=\"%s\"\n"
"\t\t\t[/%s]\n",
query->rows[i][1], query->rows[i][2],query->rows[i][1]);
}
if (i != 0)
printf("\t\t[/%i]\n", level);
printf("\t[/units-by-level]\n"
"[/game]\n");
return 0;
}

287
utils/stats/graph.c Normal file
View file

@ -0,0 +1,287 @@
/* Query database to produce a graph of a campaign as SVG. */
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include "utils.h"
#include "database.h"
#define DATABASE_FILE "/home/rusty/wesnoth/wesnoth-uploads.db"
#define HEIGHT 200
#define WIDTH 130 /* For each entry */
#define PROGRESS_WIDTH 150
#define BORDER 25
#define BAR_HEIGHT 10
int log_fd = STDERR_FILENO;
static void print_circle(unsigned int x, unsigned int val,
unsigned int repeats,
const char *player,
const char *campaign, const char *ver)
{
/* Some queries (eg. # wins) not associated with particular player. */
if (player)
printf("<a xlink:href=\"index.cgi"
"?W_PLAYER=%s&amp;W_CAMPAIGN=%s&amp;W_VERSION=%s\""
" xlink:title=\"Player %s\" target=\"_blank\">",
player, campaign, ver, player);
printf("<circle cx=\"%u\" cy=\"%u\" r=\"%f\" fill=\"red\"/>",
x, HEIGHT - val, sqrt(repeats)*2);
if (player)
printf("</a>");
printf("\n");
}
static void plot(struct db_query *query, unsigned int x, float scale,
const char *campaign, const char *ver)
{
unsigned int i, prev = 0;
for (i = 0; i < query->num_rows; i++) {
if (streq(query->rows[i][0], query->rows[prev][0]))
continue;
print_circle(x, atoi(query->rows[prev][0])*scale, i-prev,
query->rows[prev][1], campaign, ver);
prev = i;
}
if (i != 0)
print_circle(x, atoi(query->rows[prev][0])*scale, i-prev,
query->rows[prev][1], campaign, ver);
}
static void draw_graph(const char *view, const char *answer,
const char *select_cond, const char *campaign,
const char *diff, const char *ver,
const char *extra,
int argc, char *argv[])
{
void *h;
char *querystr;
struct db_query *query[argc];
int i, minimum = 0, maximum = 0;
unsigned int j, span, grad;
float scale;
h = db_open(DATABASE_FILE);
for (i = 0; i < argc; i++) {
querystr = aprintf("SELECT %s,player FROM %s WHERE %s AND version='%s' AND difficulty='%s' AND scenario='%s' AND campaign='%s' %s;",
answer, view, select_cond, ver, diff,
argv[i], campaign, extra);
query[i] = db_query(h, querystr);
for (j = 0; j < query[i]->num_rows; j++) {
maximum = max(atoi(query[i]->rows[j][0]), maximum);
minimum = min(atoi(query[i]->rows[j][0]), minimum);
}
#if 0
printf("%s: ", argv[i]);
for (j = 0; j < query[i]->num_rows; j++)
printf("%s ", query[i]->rows[j][0]);
printf("\n");
#endif
}
/* Header and axes. */
printf("<?xml version=\"1.0\" standalone=\"no\"?>\n"
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 20001102//EN\" \"svg-20001102.dtd\">"
"<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"-%u -%u %u %u\" >"
"<path fill=\"none\" stroke=\"black\" d=\"M 0 %u l 0 -%u m -15 25 l 15 -25 l 15 25\"/>"
"<path fill=\"none\" stroke=\"black\" d=\"M 0 %u l %i 0 m -25 -15 l 25 15 l -25 15\"/>\n",
BORDER, BORDER, argc*WIDTH+BORDER*2, HEIGHT+BORDER*2,
HEIGHT, HEIGHT, HEIGHT, argc*WIDTH);
/* Print names of scenarios and marks along x axis. */
for (i = 0; i < argc; i++) {
char *name = strndup(argv[i], 15);
while (strchr(name, '_'))
*strchr(name, '_') = ' ';
printf("<text text-anchor=\"middle\" text-align=\"center\" font-size=\"16\" fill=\"black\" x=\"%u\" y=\"%u\">%s</text>\n"
"<line stroke=\"black\" x1=\"%u\" y1=\"%u\" x2=\"%u\" y2=\"%u\"/>\n",
i*WIDTH+WIDTH/2, HEIGHT+20, name,
i*WIDTH+WIDTH/2, HEIGHT, i*WIDTH+WIDTH/2, HEIGHT+5);
}
span = maximum - minimum;
/* We want a Y size of HEIGHT. */
scale = (float)HEIGHT / span;
if (span > 200)
grad = 100;
else if (span > 20)
grad = 10;
else
grad = 1;
/* Print gradiations up y axis. */
for (i = (minimum+grad-1)/grad*grad; i < maximum; i += grad)
printf("<text fill=\"black\" x=\"-25\" y=\"%f\">%u</text>\n"
"<line stroke=\"black\" x1=\"-5\" y1=\"%f\" x2=\"0\" y2=\"%f\"/>\n",
HEIGHT - scale*i, i, (span-i)*scale, (span-i)*scale);
/* Now plot results. We compine multiples into bigger circles. */
for (i = 0; i < argc; i++)
plot(query[i], i*WIDTH+WIDTH/2, scale, campaign, ver);
/* Draw average */
printf("<path fill=\"none\" stroke=\"gray\" d=\"");
for (i = 0; i < argc; i++) {
unsigned int sum = 0;
float avg;
for (j = 0; j < query[i]->num_rows; j++)
sum += atoi(query[i]->rows[j][0]);
if (query[i]->num_rows == 0)
avg = 0;
else
avg = (float)sum / query[i]->num_rows;
printf("%s %u %f ", i == 0 ? "M" : "L",
i*WIDTH+WIDTH/2, HEIGHT - avg*scale);
}
printf("\"/>\n");
/* Print 0 line if not at bottom of graph. */
if (minimum != 0)
printf("<line stroke=\"red\" x1=\"%u\" y1=\"%f\" x2=\"%u\" y2=\"%f\"/>\n",
0, maximum*scale, argc*WIDTH, maximum*scale);
printf("</svg>\n");
}
static void draw_one(struct db_query *query, unsigned height,
unsigned int argc, char **argv)
{
unsigned int i, j;
/* Axes */
printf("<path fill=\"none\" stroke=\"black\" d=\"M 0 %u l 0 -%u m -15 25 l 15 -25 l 15 25\"/>"
"<path fill=\"none\" stroke=\"black\" d=\"M 0 %u l %i 0 m -25 -15 l 25 15 l -25 15\"/>\n",
height, height, height, argc*PROGRESS_WIDTH);
/* Print names of scenarios along x axis, with lines. */
for (i = 0; i < argc; i++) {
char *name = strndup(argv[i], 15);
while (strchr(name, '_'))
*strchr(name, '_') = ' ';
printf("<text text-anchor=\"middle\" text-align=\"center\" font-size=\"16\" fill=\"black\" x=\"%u\" y=\"%u\">%s</text>\n"
"<line stroke=\"black\" x1=\"%u\" y1=\"%u\" x2=\"%u\" y2=\"%u\"/>\n",
i*PROGRESS_WIDTH+PROGRESS_WIDTH/2, height+20, name,
i*PROGRESS_WIDTH+PROGRESS_WIDTH, 0,
i*PROGRESS_WIDTH+PROGRESS_WIDTH, height+5);
}
/* Draw each game. */
for (i = 0; i < query->num_rows; i++) {
int start, end, num_turns;
/* Find which scenario was played. */
for (j = 0; j < argc; j++) {
if (streq(query->rows[i][0], argv[j]))
break;
}
if (j == argc)
continue;
/* Bar goes from start to end turn. */
start = atoi(query->rows[i][1]);
end = atoi(query->rows[i][2]);
/* Old data has no end 8( */
if (end < start)
end = start;
/* We can end one turn *after* last turn (timeout). */
num_turns = atoi(query->rows[i][3])+1;
start /= (float)num_turns/PROGRESS_WIDTH;
end /= (float)num_turns/PROGRESS_WIDTH;
/* Link this to the specific game stats. */
printf("<a xlink:href=\"index.cgi?W_GAME=%s\""
" xlink:title=\"Game %s\" target=\"_blank\">",
query->rows[i][5], query->rows[i][5]);
printf("<rect x=\"%u\" y=\"%u\" width=\"%u\" height=\"%u\""
" stroke=\"black\" fill=\"%s\"/></a>\n",
PROGRESS_WIDTH*j+start, height-BAR_HEIGHT,
end-start?:1, BAR_HEIGHT,
streq(query->rows[i][4], "1") ? "green"
: streq(query->rows[i][4], "2") ? "#ff0d00"
: "#ece000");
height -= BAR_HEIGHT;
}
}
/* Draw the progress of this player. */
static void draw_progress_graph(const char *campaign, const char *diff,
const char *ver, const char *player,
unsigned int argc, char *argv[])
{
void *h;
char *querystr;
struct db_query *query;
unsigned int height, i, j;
h = db_open(DATABASE_FILE);
querystr = aprintf("SELECT scenario,start_turn,end_turn,num_turns,result,game FROM campaign_view WHERE campaign='%s' AND difficulty='%s' AND version='%s' AND player='%s' ORDER BY game;",
campaign, diff, ver, player);
query = db_query(h, querystr);
/* Remove scenarios not played by player. */
for (i = 0; i < argc; i++) {
for (j = 0; j < query->num_rows; j++) {
if (streq(query->rows[j][0], argv[i]))
break;
}
if (j == query->num_rows) {
delete_arr(argv, argc, i, 1);
argc--;
i--;
}
}
height = 0;
/* For each one we're going to draw, add BAR_HEIGHT to height */
for (i = 0; i < argc; i++) {
for (j = 0; j < query->num_rows; j++) {
if (streq(query->rows[j][0], argv[i]))
height += BAR_HEIGHT;
}
}
/* Header. */
printf("<?xml version=\"1.0\" standalone=\"no\"?>\n"
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 20001102//EN\" \"svg-20001102.dtd\">"
"<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"-%u -%u %u %u\" preserveAspectRatio=\"none\">\n",
BORDER, BORDER, argc*PROGRESS_WIDTH+BORDER*2, height+BORDER*2);
draw_one(query, height, argc, argv);
printf("</svg>\n");
}
static void usage(const char *name)
{
barf("Usage: %s <view> <answer> <select-cond> <campaign> <difficulty> <version> <extra-args> <scenario>...\n"
" OR:\n"
" %s --progress <campaign> <difficulty> <version> <scenario_name>...",
name, name);
}
/* FIXME: Allow multiple queries plotted with different colors? */
int main(int argc, char *argv[])
{
if (argv[1] && streq(argv[1], "--progress")) {
if (argc < 7)
usage(argv[0]);
draw_progress_graph(argv[2], argv[3], argv[4], argv[5],
argc-6, argv+6);
} else {
if (argc < 9)
usage(argv[0]);
draw_graph(argv[1], argv[2], argv[3], argv[4], argv[5],
argv[6], argv[7], argc - 8, argv + 8);
}
return 0;
}

62
utils/stats/run-tests.sh Executable file
View file

@ -0,0 +1,62 @@
#! /bin/sh
set -e
TESTING_DATABASE=/tmp/wesnoth-uploads.db
TESTING_LOGFILE=/tmp/wesnoth-upload-log.
export TESTING_DATABASE
export TESTING_LOGFILE
for f in testsuite/*.input; do
echo $f
rm -f $TESTING_DATABASE $TESTING_LOGFILE*
./upload.cgi --initialize
echo 'INSERT INTO bad_serial VALUES ("BADSERIAL");' | sqlite3 $TESTING_DATABASE
for i in $f*; do
case $i in *~) :;; *)
if ./upload.cgi < $i > /dev/null; then :
else echo ERROR:; cat ${TESTING_LOGFILE}0; exit 1
fi;;
esac
done
BASE=`echo $f | sed 's/\.input$//'`
[ x"`sqlite3 $TESTING_DATABASE < $BASE.test`" = x"`cat $BASE.output`" ]
done
echo -n Parallel test
rm -f $TESTING_DATABASE $TESTING_LOGFILE*
./upload.cgi --initialize
parallel_test()
{
while [ -f $1 ]; do sleep 0; done
if ./upload.cgi < $2 > /dev/null; then
echo $2 succeeded >> $3
echo -n .
else
echo $2 failed >> $3
fi
}
STARTFILE=`mktemp`
OUTPUTFILE=`mktemp`
trap "rm -f $OUTPUTFILE $STARTFILE" EXIT
COUNT=0
for i in testsuite/*.parallel; do
COUNT=$(($COUNT + 1))
parallel_test $STARTFILE $i $OUTPUTFILE &
done
rm $STARTFILE
while [ `wc -l < $OUTPUTFILE` -lt $COUNT ]; do
sleep 1
done
echo
if grep failed $OUTPUTFILE; then
exit 1
fi
[ x"`sqlite3 $TESTING_DATABASE < testsuite/parallel-test`" = x"`cat testsuite/parallel-output`" ]
echo Succeeded.

View file

@ -0,0 +1,102 @@
/* SQLite3 database backend. */
#include <stdlib.h>
#include <unistd.h>
#include <sqlite3.h>
#include "database.h"
#include "utils.h"
/* sqlite3_busy_timeout sleeps for a *second*. What a piece of shit. */
static int busy(void *unused __attribute__((unused)), int count)
{
usleep(50000);
/* If we've been stuck for 1000 iterations (at least 50
* seconds), give up. */
return (count < 1000);
}
void *db_open(const char *file)
{
sqlite3 *handle;
int err = sqlite3_open(file, &handle);
if (err != SQLITE_OK)
barf("Error %i from sqlite3_open of db '%s'\n", err, file);
sqlite3_busy_handler(handle, busy, NULL);
return handle;
}
static int query_cb(void *data, int num, char**vals,
char**names __attribute__((unused)))
{
int i;
struct db_query *query = data;
query->rows = realloc_array(query->rows, query->num_rows+1);
query->rows[query->num_rows] = new_array(char *, num);
for (i = 0; i < num; i++) {
/* We don't count rows with NULL results
* (eg. count(*),player where count turns out to be
* zero. */
if (!vals[i])
return 0;
query->rows[query->num_rows][i] = strdup(vals[i]);
}
query->num_rows++;
return 0;
}
/* Runs query (SELECT). Fails if > 1 row returned. Fills in columns. */
struct db_query *db_query(void *h, const char *query)
{
struct db_query *ret = new(struct db_query);
char *err;
ret->rows = NULL;
ret->num_rows = 0;
if (sqlite3_exec(h, query, query_cb, ret, &err) != SQLITE_OK)
barf("Failed sqlite3 query '%s': %s", query, err);
return ret;
}
/* Runs command (CREATE TABLE/INSERT) */
void db_command(void *h, const char *command)
{
char *err;
if (sqlite3_exec(h, command, NULL, NULL, &err) != SQLITE_OK)
barf("Failed sqlite3 command '%s': %s", command, err);
}
/* Starts transaction. Doesn't need to nest. */
void db_transaction_start(void *h)
{
char *err;
if (sqlite3_exec(h, "BEGIN EXCLUSIVE TRANSACTION", NULL, NULL, &err)!=SQLITE_OK)
barf("Starting sqlite3 transaction: %s\n", err);
}
/* Finishes transaction, or rolls it back and caller needs to start again. */
bool db_transaction_finish(void *h)
{
switch (sqlite3_exec(h, "COMMIT TRANSACTION;", NULL, NULL, NULL)) {
case SQLITE_OK:
return true;
case SQLITE_BUSY:
if (sqlite3_exec(h, "ROLLBACK TRANSACTION;", NULL, NULL, NULL)
!= SQLITE_OK)
barf("Ending sqlite3 busy rollback failed");
return false;
default:
barf("Strange sqlite3 error return from COMMIT");
}
}
/* Closes database (only called when everything OK). */
void db_close(void *h)
{
sqlite3_close(h);
}

View file

@ -0,0 +1,28 @@
format_version="1"
id="587613382338525"
serial="BADSERIAL"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,3 @@
/* Returns NEXT game number. */
SELECT games_received FROM game_count;

View file

@ -0,0 +1,28 @@
format_version="1"
id="587613382338525"
serial="115726044800000149"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1,28 @@
format_version="1"
id="587613382338525"
serial="115726044800000149"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1 @@
2

View file

@ -0,0 +1,3 @@
/* Returns NEXT game number. */
SELECT games_received FROM game_count;

View file

@ -0,0 +1,28 @@
format_version="1"
id="5876133823385251"
serial="1157260448000001491"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1,27 @@
format_version="1"
id="587613382338525"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1,27 @@
format_version="1"
id="587613382338525"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1 @@
3

View file

@ -0,0 +1,3 @@
/* Returns NEXT game number. */
SELECT games_received FROM game_count;

View file

@ -0,0 +1,28 @@
format_version="1"
id="5876133823385252"
serial="1157260448000001492"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1,28 @@
format_version="1"
id="5876133823385253"
serial="1157260448000001493"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1,28 @@
format_version="1"
id="5876133823385254"
serial="1157260448000001494"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1,28 @@
format_version="1"
id="5876133823385255"
serial="1157260448000001495"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1,28 @@
format_version="1"
id="5876133823385256"
serial="1157260448000001496"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1,28 @@
format_version="1"
id="5876133823385257"
serial="1157260448000001497"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1,28 @@
format_version="1"
id="5876133823385258"
serial="1157260448000001498"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1,28 @@
format_version="1"
id="5876133823385259"
serial="1157260448000001499"
version="1.1.9+svn"
[game]
campaign="CAMPAIGN_TEST"
difficulty="NORMAL"
gold="1000"
num_turns="100"
scenario="Test"
time="41"
[special-unit]
experience="0"
level="1"
name="Side1"
[/special-unit]
[units-by-level]
[1]
[Elvish Scout]
count="1"
[/Elvish Scout]
[/1]
[/units-by-level]
[quit]
end_turn="12"
time="1598"
[/quit]
[/game]

View file

@ -0,0 +1,18 @@
1157260448000001491
1157260448000001492
1157260448000001493
1157260448000001494
1157260448000001495
1157260448000001496
1157260448000001497
1157260448000001498
1157260448000001499
5876133823385251
5876133823385252
5876133823385253
5876133823385254
5876133823385255
5876133823385256
5876133823385257
5876133823385258
5876133823385259

View file

@ -0,0 +1,3 @@
/* Check all the serial numbers are in there */
select * from serial order by id;
select * from players order by id;

823
utils/stats/upload.cgi.c Normal file
View file

@ -0,0 +1,823 @@
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdbool.h>
#include "database.h"
#include "utils.h"
#define MAX_LOGS 10
int log_fd = STDERR_FILENO;
static const char *database_file(void)
{
return getenv("TESTING_DATABASE") ?:
"/home/rusty/wesnoth/wesnoth-uploads.db";
}
static const char *logfile_prefix(void)
{
return getenv("TESTING_LOGFILE") ?:
"/home/rusty/wesnoth/wesnoth-upload-log.";
}
/* We open a log file, and delete if if all goes well. We
* keep up a max of MAX_LOGS files, to avoid filling disk. */
static void maybe_log_to_file(char **filename)
{
char name[strlen(logfile_prefix()) + sizeof(__stringify(MAX_LOGS))];
unsigned int i;
for (i = 0; i < MAX_LOGS; i++) {
struct stat st;
sprintf(name, "%s%u", logfile_prefix(), i);
if (lstat(name, &st) != 0) {
log_fd = open(name, O_WRONLY|O_CREAT|O_EXCL, 0640);
if (log_fd >= 0) {
*filename = strdup(name);
return;
}
}
}
*filename = NULL;
log_fd = open("/dev/null", O_WRONLY);
if (log_fd < 0)
barf_perror("Could not open /dev/null for logging");
}
struct wml
{
const char *name;
unsigned int num_keys;
const char **keyval;
unsigned int num_children;
struct wml **child;
};
static struct wml *new_wml(const char *name)
{
struct wml *wml = new(struct wml);
wml->name = name;
wml->num_keys = wml->num_children = 0;
wml->keyval = NULL;
wml->child = NULL;
return wml;
}
/* Deliberately simple parser (doesn't handle comments, for example).
* We want canonical forms so we can enter into database.
*/
static char *get_line(char **string)
{
char *start, *first_quote = NULL;
/* Ignore whitespace. */
while (isspace(**string))
(*string)++;
if (**string == '\0')
return NULL;
start = *string;
while (**string != '\n') {
switch (**string) {
case '\0':
barf("Unexpected end of input");
case '+':
if (!first_quote)
barf("Cannot handle '+'");
break;
case '"':
if (!first_quote)
first_quote = *string;
else {
/* Only accept close of string then \n */
if ((*string)[1] != '\n')
barf("String must end of end of line");
}
break;
default:
break;
}
(*string)++;
}
if (first_quote) {
/* Trim both quotes. */
if ((*string)[-1] != '"')
barf("Incomplete string");
(*string)[-1] = '\0';
memmove(first_quote, first_quote+1, strlen(first_quote));
} else
**string = '\0';
(*string)++;
return start;
}
static void wml_add(struct wml *wml, const char *line)
{
wml->keyval = realloc_array(wml->keyval, wml->num_keys+1);
wml->keyval[wml->num_keys++] = line;
}
static void wml_add_child(struct wml *wml, struct wml *child)
{
wml->child = realloc_array(wml->child, wml->num_children+1);
wml->child[wml->num_children++] = child;
}
#if 0 /* Unused */
static void wml_replace(struct wml *wml, const char *key, const char *value)
{
unsigned int i, len = strlen(key);
for (i = 0; i < wml->num_keys; i++) {
if (memcmp(wml->keyval[i], key, len) == 0
&& wml->keyval[i][len] == '=') {
wml->keyval[i] = aprintf("%s=%s", key, value);
return;
}
}
barf("Could not find key '%s' to replace", key);
}
#endif
static struct wml *parse_data(char **string, const char *name)
{
char *line;
char end[2 + strlen(name ? name : "") + 1 + 1];
struct wml *wml = new_wml(name);
if (name)
sprintf(end, "[/%s]", name);
else
end[0] = '\0';
for (;;) {
line = get_line(string);
if (!line) {
/* Only the top element can terminate this way. */
if (!name)
return wml;
barf("Unexpected end of file during [%s]", name);
}
/* The end? */
if (streq(line, end))
return wml;
/* Child? */
if (*line == '[') {
struct wml *child;
if (line[1] == '/' || !strends(line, "]"))
barf("Malformed line '%s' during [%s]",
line, name);
line[strlen(line) - 1] = '\0';
child = parse_data(string, line+1);
wml_add_child(wml, child);
} else {
char *eq = strchr(line, '=');
if (!eq)
barf("Expected '=' in '%s'", line);
wml_add(wml, line);
}
}
}
static bool write_all(int fd, const void *data, unsigned int len)
{
while (len) {
int done;
done = write(fd, data, len);
if (done < 0 && errno == EINTR)
continue;
if (done <= 0)
return false;
data += done;
len -= done;
}
return true;
}
/* Simple parser for Wesnoth Markup Language. */
static struct wml *parse(int fd)
{
unsigned long size;
char *data = grab_input(fd, &size);
logp("Content-length: %lu\n", size);
if (!write_all(log_fd, data, size))
barf("Could not write input to log");
/* No NULs please */
if (strlen(data) != size)
barf("Contained embedded NUL chars");
logp("=== END INPUT ===");
return parse_data(&data, NULL);
}
/* For debygging */
void dump(const struct wml *wml, unsigned int level);
void dump(const struct wml *wml, unsigned int level)
{
unsigned int i;
char indent[level+1];
memset(indent, '\t', level);
indent[level] = '\0';
for (i = 0; i < wml->num_keys; i++)
printf("%s%s\n", indent, wml->keyval[i]);
for (i = 0; i < wml->num_children; i++) {
printf("%s[%s]\n", indent, wml->child[i]->name);
dump(wml->child[i], level+1);
}
}
#define INTEGER 1
#define TEXT 2
#define NAME_REFERENCE 3
#define UNIQUE_TEXT 4
#define UNIQUE_INTEGER 5
/* We don't put common strings into tables, but instead use a reference into
* a table of names. */
static void __attribute__((sentinel))
create_table(void *h, const char *tablename, ...)
{
va_list ap;
const char *name;
char sep = '(';
char *cmd = aprintf("CREATE TABLE \"%s\" ", tablename);
va_start(ap, tablename);
while ((name = va_arg(ap, const char *)) != NULL) {
switch (va_arg(ap, int)) {
case INTEGER:
cmd = aprintf_add(cmd, "%c%s INTEGER", sep, name);
break;
case UNIQUE_INTEGER:
cmd = aprintf_add(cmd, "%c%s INTEGER UNIQUE",
sep, name);
break;
case TEXT:
cmd = aprintf_add(cmd, "%c%s TEXT", sep, name);
break;
case UNIQUE_TEXT:
cmd = aprintf_add(cmd, "%c%s TEXT UNIQUE",
sep, name);
break;
case NAME_REFERENCE: {
char tblname[strlen(name)+1];
cmd = aprintf_add(cmd, "%c%s INTEGER", sep, name);
memcpy(tblname, name, strlen(name) - strlen("_ref"));
tblname[strlen(name) - strlen("_ref")] = '\0';
strcat(tblname, "s");
create_table(h, tblname, "name", UNIQUE_TEXT, NULL);
break;
}
default:
barf("Unexpected type in create_table");
}
sep = ',';
}
va_end(ap);
cmd = aprintf_add(cmd, ");");
db_command(h, cmd);
}
static void create_tables(void *h)
{
do {
db_transaction_start(h);
create_table(h, "players",
"id", UNIQUE_TEXT,
NULL);
create_table(h, "game_count",
"player_ref", UNIQUE_INTEGER,
"games_received", INTEGER,
"last_upload", TEXT,
NULL);
create_table(h, "campaigns",
"player_ref", INTEGER,
"campaign_name_ref", NAME_REFERENCE,
"difficulty_name_ref", NAME_REFERENCE,
NULL);
create_table(h, "scenarios",
"campaign_ref", INTEGER,
"scenario_name_ref", NAME_REFERENCE,
NULL);
create_table(h, "games",
"scenario_ref", INTEGER,
"start_turn", INTEGER,
"gold", INTEGER,
"start_time", INTEGER,
"version_name_ref", NAME_REFERENCE,
"game_number", INTEGER,
/* 0 = unknown/quit, 1 = victory, 2 = defeat */
"result", INTEGER,
"end_time", INTEGER,
"end_turn", INTEGER,
"num_turns", INTEGER,
/* This is only set on victory. */
"end_gold", INTEGER,
NULL);
create_table(h, "special_units",
"game_ref", INTEGER,
"unit_name_ref", NAME_REFERENCE,
"level", INTEGER,
"experience", INTEGER,
NULL);
create_table(h, "unit_types",
"name", TEXT,
"level", INTEGER,
NULL);
create_table(h, "unit_tallies",
"game_ref", INTEGER,
"unit_type_ref", INTEGER,
"count", INTEGER,
NULL);
create_table(h, "serial",
"id", UNIQUE_TEXT,
NULL);
create_table(h, "bad_serial",
"id", UNIQUE_TEXT,
NULL);
db_command(h, "CREATE VIEW campaign_view AS SELECT games.rowid AS game,games.end_time-games.start_time AS time,scenario_names.name AS scenario,games.result AS result,games.start_turn AS start_turn,games.end_turn AS end_turn,games.num_turns AS num_turns,games.gold AS gold,players.id AS player,campaign_names.name AS campaign,difficulty_names.name AS difficulty,version_names.name AS version FROM games INNER JOIN scenarios ON games.scenario_ref = scenarios.rowid INNER JOIN campaigns ON scenarios.campaign_ref = campaigns.rowid INNER JOIN difficulty_names ON campaigns.difficulty_name_ref = difficulty_names.rowid INNER JOIN version_names ON games.version_name_ref = version_names.rowid INNER JOIN scenario_names ON scenarios.scenario_name_ref = scenario_names.rowid INNER JOIN campaign_names ON campaigns.campaign_name_ref = campaign_names.rowid INNER JOIN players ON players.rowid = campaigns.player_ref;");
db_command(h, "CREATE VIEW units_view AS SELECT scenario_names.name AS scenario,games.rowid AS game,unit_tallies.count AS count,unit_types.level AS level,players.id AS player,campaign_names.name AS campaign,difficulty_names.name AS difficulty,version_names.name AS version, games.start_turn AS start_turn FROM games INNER JOIN scenarios ON games.scenario_ref = scenarios.rowid INNER JOIN campaigns ON scenarios.campaign_ref = campaigns.rowid INNER JOIN difficulty_names ON campaigns.difficulty_name_ref = difficulty_names.rowid INNER JOIN version_names ON games.version_name_ref = version_names.rowid INNER JOIN unit_tallies ON games.rowid = unit_tallies.game_ref INNER JOIN unit_types ON unit_types.rowid = unit_tallies.unit_type_ref INNER JOIN scenario_names ON scenarios.scenario_name_ref = scenario_names.rowid INNER JOIN campaign_names ON campaigns.campaign_name_ref = campaign_names.rowid INNER JOIN players ON players.rowid = campaigns.player_ref;");
db_command(h, "CREATE UNIQUE INDEX unit_tallies_idx ON unit_tallies (game_ref, unit_type_ref, count);");
} while (!db_transaction_finish(h));
}
static const char *get_maybe(const struct wml *wml, const char *key)
{
unsigned int i, len = strlen(key);
for (i = 0; i < wml->num_keys; i++) {
if (memcmp(wml->keyval[i], key, len) == 0
&& wml->keyval[i][len] == '=')
return wml->keyval[i] + len + 1;
}
return NULL;
}
/* Sanity check that this really is an int. */
static const char *is_int(const char *val)
{
char *endp;
strtoul(val, &endp, 10);
if (*endp || endp == val)
barf("Value '%s' is not a valid integer", val);
return val;
}
static const char *get(const struct wml *wml, const char *key)
{
const char *ret = get_maybe(wml, key);
if (ret)
return ret;
if (!wml->name)
barf("Did not find toplevel key '%s'", key);
barf("Did not find key '%s' in [%s]", key, wml->name);
}
static struct wml *get_child(const struct wml *wml, const char *key)
{
unsigned int i;
for (i = 0; i < wml->num_children; i++) {
if (streq(wml->child[i]->name, key))
return wml->child[i];
}
return NULL;
}
/* Returns NULL or array of columns. */
static char **db_select(void *h, const char *table, const char *key,
const char *val, ...)
{
char *cmd;
const char *col;
char sep = ' ';
va_list ap;
struct db_query *query;
cmd = aprintf("SELECT");
va_start(ap, val);
while ((col = va_arg(ap, const char *)) != NULL) {
cmd = aprintf_add(cmd, "%c\"%s\"", sep, col);
sep = ',';
}
va_end(ap);
cmd = aprintf_add(cmd, " FROM \"%s\" WHERE \"%s\" = \"%s\";",
table, key, val);
query = db_query(h, cmd);
if (query->num_rows > 1)
barf_perror("Query '%s' returned %i rows",
cmd, query->num_rows);
return query->num_rows ? query->rows[0] : NULL;
}
static char *get_name_ref(void *h, const char *table, const char *name)
{
char **answer;
answer = db_select(h, table, "name", name, "ROWID", NULL);
if (!answer) {
char *cmd;
cmd = aprintf("INSERT INTO \"%s\" VALUES(\"%s\");",table,name);
db_command(h, cmd);
answer = db_select(h, table, "name", name, "ROWID", NULL);
if (!answer)
barf("Cannot find name '%s' after insert in table %s",
name, table);
}
return answer[0];
}
static char *get_unit_type_ref(void *h, const char *name, const char *level)
{
struct db_query *q;
char *query, *cmd;
query = aprintf("SELECT ROWID FROM \"unit_types\" where"
" \"name\" = \"%s\" AND \"level\" = \"%s\";",
name, level);
q = db_query(h, query);
if (q->num_rows)
return q->rows[0][0];
cmd = aprintf("INSERT INTO \"unit_types\" VALUES(\"%s\",\"%s\");",
name, level);
db_command(h, cmd);
q = db_query(h, query);
if (q->num_rows != 1)
barf("Cannot find unit_type '%s/%s' after insert",
name, level);
return q->rows[0][0];
}
/* Return ROWID of this entry (key, value) pairs. */
static char *make_ref(void *h, bool might_exist, const char *table, ...)
{
struct db_query *q;
char *query, *cmd;
const char *key, *val;
const char *sep = "";
va_list ap;
query = aprintf("SELECT ROWID FROM \"%s\" WHERE ", table);
va_start(ap, table);
while ((key = va_arg(ap, const char *)) != NULL) {
val = va_arg(ap, const char *);
query = aprintf_add(query, "%s\"%s\" = \"%s\"",
sep, key, val);
sep = " AND ";
}
va_end(ap);
query = aprintf_add(query, ";");
if (might_exist) {
/* Try looking for it first? */
q = db_query(h, query);
if (q->num_rows)
return q->rows[0][0];
}
/* Didn't find one, so make one. */
cmd = aprintf("INSERT INTO \"%s\" (", table);
sep = "";
va_start(ap, table);
while ((key = va_arg(ap, const char *)) != NULL) {
val = va_arg(ap, const char *);
cmd = aprintf_add(cmd, "%s\"%s\"", sep, key);
sep = ",";
}
va_end(ap);
cmd = aprintf_add(cmd, ") VALUES (");
sep = "";
va_start(ap, table);
while ((key = va_arg(ap, const char *)) != NULL) {
val = va_arg(ap, const char *);
cmd = aprintf_add(cmd, "%s\"%s\"", sep, val);
sep = ",";
}
va_end(ap);
cmd = aprintf_add(cmd, ");");
db_command(h, cmd);
/* FIXME: doesn't insert return the ROWID? */
q = db_query(h, query);
if (q->num_rows != 1)
barf("Entry '%s' did not exist after '%s'", query, cmd);
return q->rows[0][0];
}
/* [5]
[Elder Mage]
count="1"
[/Elder Mage]
[/5]
*/
static void add_tally_level(void *h, const char *game_ref, struct wml *level)
{
unsigned int i;
for (i = 0; i < level->num_children; i++) {
char *unit_type;
unit_type = get_unit_type_ref(h, level->child[i]->name,
is_int(level->name));
make_ref(h, false, "unit_tallies",
"game_ref", game_ref,
"unit_type_ref", unit_type,
"count", is_int(get(level->child[i],"count")),
NULL);
}
}
/*
[units-by-level]
[2] ...
[5] ...
[/units-by-level]
*/
static void add_tally(void *h, const char *game_ref, struct wml *tally)
{
unsigned int i;
if (!tally)
barf("No units-by-level entry");
for (i = 0; i < tally->num_children; i++)
add_tally_level(h, game_ref, tally->child[i]);
}
/* [special-unit]
experience="22"
level="3"
name="Konrad"
[/special-unit]
*/
static void add_special(void *h, const char *game_ref, struct wml *special)
{
char *name;
name = get_name_ref(h, "unit_names", get(special, "name"));
make_ref(h, false, "special_units",
"game_ref", game_ref,
"unit_name_ref", name,
"level", get(special, "level"),
"experience", get(special, "experience"),
NULL);
}
/* [game]
campaign="CAMPAIGN_HEIR_TO_THE_THRONE"
difficulty="NORMAL"
gold="549"
scenario="Mountain_Pass"
time="2052"
start_turn="10"
version="1.1-svn"
[special-unit]...
[units-by-level]...
One of:
[victory]
gold="749"
time="3351"
end_turn="19"
[/victory]
OR:
[defeat]
time="5003"
end_turn="19"
[/defeat]
OR:
[quit]
time="5003"
end_turn="19"
[/quit]
*/
static void add_game(void *h,
const char *player_ref,
const char *version_ref,
unsigned int gamenum,
struct wml *game)
{
struct wml *result;
const char *campaign, *difficulty, *scenario;
char *campaign_ref, *scenario_ref, *game_ref;
const char *gold, *start_time, *start_turn, *game_number, *result_num,
*end_time, *end_turn, *end_gold, *num_turns;
unsigned int i;
/* Get reference numbers for strings. */
campaign = get_name_ref(h, "campaign_names", get(game,"campaign"));
difficulty = get_name_ref(h,"difficulty_names",get(game,"difficulty"));
scenario = get_name_ref(h, "scenario_names", get(game,"scenario"));
/* Get reference for campaign entry. */
campaign_ref = make_ref(h, true, "campaigns",
"player_ref", player_ref,
"campaign_name_ref", campaign,
"difficulty_name_ref", difficulty, NULL);
/* Get reference for scenario. */
scenario_ref = make_ref(h, true, "scenarios",
"campaign_ref", campaign_ref,
"scenario_name_ref", scenario, NULL);
gold = is_int(get(game, "gold"));
start_time = is_int(get(game, "time"));
num_turns = is_int(get(game, "num_turns"));
/* We can tell between save at turn 1, and at end of last scenario.
* This matters: a save at turn 1 means maps is not random.
*/
start_turn = get_maybe(game, "start_turn");
if (!start_turn)
start_turn = "0";
is_int(start_turn);
game_number = aprintf("%i", gamenum);
if ((result = get_child(game, "victory")) != NULL) {
result_num = "1";
end_time = is_int(get(result, "time"));
end_turn = is_int(get(result, "end_turn"));
end_gold = is_int(get(result, "gold"));
} else if ((result = get_child(game, "defeat")) != NULL) {
result_num = "2";
end_time = is_int(get(result, "time"));
end_turn = is_int(get(result, "end_turn"));
end_gold = "0";
} else if ((result = get_child(game, "quit")) != NULL) {
result_num = "0";
end_time = is_int(get(result, "time"));
end_turn = is_int(get(result, "end_turn"));
end_gold = "0";
} else
barf("No victory, defeat or quit!");
/* Make entry for this game. */
game_ref = make_ref(h, false, "games",
"scenario_ref", scenario_ref,
"start_turn", start_turn,
"gold", gold,
"start_time", start_time,
"version_name_ref", version_ref,
"game_number", game_number,
"result", result_num,
"end_time", end_time,
"end_turn", end_turn,
"end_gold", end_gold,
"num_turns", num_turns,
NULL);
/* Make entry for each special-unit. */
for (i = 0; i < game->num_children; i++) {
if (streq(game->child[i]->name, "special-unit"))
add_special(h, game_ref, game->child[i]);
}
add_tally(h, game_ref, get_child(game, "units-by-level"));
}
static const char *find_or_create_player(void *h,
const char *id,
unsigned int *game_number)
{
char **answer, *cmd;
answer = db_select(h, "players", "id", id, "ROWID", NULL);
if (!answer) {
cmd = aprintf("INSERT INTO \"players\" VALUES(\"%s\");", id);
db_command(h, cmd);
answer = db_select(h, "players", "id", id, "ROWID", NULL);
*game_number = 1;
} else {
char **game;
game = db_select(h, "game_count", "player_ref", answer[0],
"games_received", NULL);
if (!game)
barf("No game_count entry for %s", answer[0]);
*game_number = atoi(game[0]);
}
return answer[0];
}
static void update_player_games(void *h,
const char *player_ref, unsigned int num)
{
char *cmd;
cmd = aprintf("INSERT OR REPLACE INTO \"game_count\""
" VALUES(\"%s\",%u,CURRENT_DATE);", player_ref, num);
db_command(h, cmd);
}
static void add_to_database(void *h, struct wml *wml)
{
const char *version_ref, *id, *serial;
const char *player_ref;
unsigned int i, games;
serial = get_maybe(wml, "serial");
id = get(wml, "id");
do {
db_transaction_start(h);
if (serial) {
/* We already have it. */
if (db_select(h, "serial", "id", serial, "ROWID",NULL))
return;
/* Manually-maintained list of known-bad files. */
if (db_select(h, "bad_serial", "id", serial, "ROWID",
NULL))
return;
make_ref(h, false, "serial", "id", serial, NULL);
}
version_ref = get_name_ref(h, "version_names",
get(wml, "version"));
player_ref = find_or_create_player(h, id, &games);
for (i = 0; i < wml->num_children; i++) {
if (!streq(wml->child[i]->name, "game"))
barf("Unexpected toplevel element [%s]",
wml->child[i]->name);
add_game(h, player_ref, version_ref, games + i,
wml->child[i]);
}
update_player_games(h, player_ref, games + i);
} while (!db_transaction_finish(h));
}
/* Convert game to latest version. */
static struct wml *convert_game(struct wml *game, const char *version)
{
return game;
}
/* Convert wml to the latest version. */
static struct wml *convert_wml(struct wml *wml, const char *version)
{
unsigned int i;
for (i = 0; i < wml->num_children; i++)
if (streq(wml->child[i]->name, "game"))
wml->child[i] = convert_game(wml->child[i], version);
return wml;
}
static void receive(int fd)
{
struct wml *wml;
char *logfile;
void *handle;
maybe_log_to_file(&logfile);
handle = db_open(database_file());
wml = parse(fd);
wml = convert_wml(wml, get(wml, "format_version"));
add_to_database(handle, wml);
/* We need this for a valid reply. */
printf("Content-type: text/plain\n\n");
if (logfile)
unlink(logfile);
db_close(handle);
}
/* For testing, takes a whole pile of files as input. */
int main(int argc, char *argv[])
{
if (argc == 2 && streq(argv[1], "--initialize")) {
create_tables(db_open(database_file()));
exit(0);
}
nice(10);
if (argc == 1)
receive(STDIN_FILENO);
else {
int i;
for (i = 1; i < argc; i++)
receive(open(argv[i], O_RDONLY));
}
return 0;
}

135
utils/stats/utils.c Normal file
View file

@ -0,0 +1,135 @@
/* Random util functions, mainly taken from stdrusty.h */
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <assert.h>
#include "utils.h"
void *_realloc_array(void *ptr, size_t size, size_t num)
{
if (num >= SIZE_MAX/size)
return NULL;
return realloc_nofail(ptr, size * num);
}
void *realloc_nofail(void *ptr, size_t size)
{
ptr = realloc(ptr, size);
if (ptr)
return ptr;
barf("realloc of %zu failed", size);
}
void *malloc_nofail(size_t size)
{
void *ptr = malloc(size);
if (ptr)
return ptr;
barf("malloc of %zu failed", size);
}
/* Avoid stupid sprintf-style non-value return. */
char *aprintf(const char *fmt, ...)
{
char *ret;
va_list arglist;
va_start(arglist, fmt);
vasprintf(&ret, fmt, arglist);
va_end(arglist);
return ret;
}
char *aprintf_add(char *s, const char *fmt, ...)
{
int oldlen, len;
va_list arglist;
va_start(arglist, fmt);
len = vsnprintf(NULL, 0, fmt, arglist);
va_end(arglist);
oldlen = (s ? strlen(s) : 0);
s = realloc(s, oldlen + len + 1);
va_start(arglist, fmt);
vsnprintf(s + oldlen, len + 1, fmt, arglist);
va_end(arglist);
return s;
}
void logpv(const char *fmt, va_list arglist)
{
char *str;
vasprintf(&str, fmt, arglist);
write(log_fd, str, strlen(str));
}
void logp(const char *fmt, ...)
{
va_list arglist;
va_start(arglist, fmt);
logpv(fmt, arglist);
write(log_fd, "\n", 1);
va_end(arglist);
}
void barf(const char *fmt, ...)
{
va_list arglist;
write(log_fd, "FATAL: ", strlen("FATAL: "));
va_start(arglist, fmt);
logpv(fmt, arglist);
write(log_fd, "\n", 1);
va_end(arglist);
exit(1);
}
void barf_perror(const char *fmt, ...)
{
int err = errno;
va_list arglist;
fprintf(stderr, "FATAL: ");
va_start(arglist, fmt);
logpv(fmt, arglist);
va_end(arglist);
logp(": %s", strerror(err));
exit(1);
}
/* This is all squid allows in a POST by default anyway. */
#define MAXIMUM_LEN 40000
/* This version adds one byte (for nul term) */
void *grab_input(int fd, unsigned long *size)
{
int ret;
void *buffer;
buffer = malloc(MAXIMUM_LEN+1);
*size = 0;
while ((ret = read(fd, buffer + *size, MAXIMUM_LEN - *size)) > 0) {
*size += ret;
if (*size == MAXIMUM_LEN)
barf("too much input");
}
if (ret < 0)
barf_perror("read failure");
((char *)buffer)[*size] = '\0';
return buffer;
}
void _delete_arr(void *p, unsigned len, unsigned off, unsigned num, size_t s)
{
assert(off + num <= len);
memmove(p + off*s, p + (off+num)*s, (len - (off+num))*s);
}

72
utils/stats/utils.h Normal file
View file

@ -0,0 +1,72 @@
/* Random util functions, mainly taken from stdrusty.h */
#ifndef _UPLOAD_ANALYSIS_UTILS_H
#define _UPLOAD_ANALYSIS_UTILS_H
#include <stdarg.h>
#include <stdbool.h>
#include <string.h>
/* Is A == B ? */
#define streq(a,b) (strcmp((a),(b)) == 0)
/* Does A start with B ? */
#define strstarts(a,b) (strncmp((a),(b),strlen(b)) == 0)
/* Does A end in B ? */
static inline bool strends(const char *a, const char *b)
{
if (strlen(a) < strlen(b))
return false;
return streq(a + strlen(a) - strlen(b), b);
}
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
#define ___stringify(x) #x
#define __stringify(x) ___stringify(x)
/* Convenient wrappers for malloc and realloc. Use them. */
#define new(type) ((type *)malloc_nofail(sizeof(type)))
#define new_array(type, num) realloc_array((type *)0, (num))
#define realloc_array(ptr, num) ((__typeof__(ptr))_realloc_array((ptr), sizeof((*ptr)), (num)))
void *malloc_nofail(size_t size);
void *realloc_nofail(void *ptr, size_t size);
void *_realloc_array(void *ptr, size_t size, size_t num);
/* Avoid stupid sprintf-style */
char *aprintf(const char *fmt, ...) __attribute__((format(printf,1,2)));
char *aprintf_add(char *s, const char *fmt, ...)
__attribute__((format(printf,2,3)));
void logpv(const char *fmt, va_list arglist);
void logp(const char *fmt, ...) __attribute__((format(printf,1,2)));
void barf(const char *fmt, ...) __attribute__((noreturn, format(printf,1,2)));
void barf_perror(const char *fmt, ...)
__attribute__((noreturn, format(printf,1,2)));
void *grab_input(int fd, unsigned long *size);
extern int log_fd;
/* from the Linux Kernel:
* min()/max() macros that also do
* strict type-checking.. See the
* "unnecessary" pointer comparison.
*/
#define min(x,y) ({ \
typeof(x) _x = (x); \
typeof(y) _y = (y); \
(void) (&_x == &_y); \
_x < _y ? _x : _y; })
#define max(x,y) ({ \
typeof(x) _x = (x); \
typeof(y) _y = (y); \
(void) (&_x == &_y); \
_x > _y ? _x : _y; })
/* Delete (by memmove) elements of an array */
#define delete_arr(p, len, off, num) \
_delete_arr((p), (len), (off), (num), sizeof(*p))
void _delete_arr(void *p, unsigned len, unsigned off, unsigned num, size_t s);
#endif /* _UPLOAD_ANALYSIS_UTILS_H */