Statistics CGI program and hacky display code.
This is what is running on stats.wesnoth.org.
This commit is contained in:
parent
2c35486873
commit
b1a0af242c
36 changed files with 2633 additions and 0 deletions
19
utils/stats/Makefile
Normal file
19
utils/stats/Makefile
Normal 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
|
14
utils/stats/cgi-bin/check_args.sh
Normal file
14
utils/stats/cgi-bin/check_args.sh
Normal 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 '&' ' '`
|
47
utils/stats/cgi-bin/draw_graph.cgi
Executable file
47
utils/stats/cgi-bin/draw_graph.cgi
Executable 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
|
40
utils/stats/cgi-bin/footer.html
Normal file
40
utils/stats/cgi-bin/footer.html
Normal 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&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 © 2003-2005 The Battle for Wesnoth</p>
|
||||
</div>
|
||||
|
||||
</div> <!-- end footer -->
|
||||
|
||||
</div> <!-- end main -->
|
||||
|
||||
</div> <!-- end global -->
|
||||
|
||||
</body>
|
||||
</html>
|
45
utils/stats/cgi-bin/header.html
Normal file
45
utils/stats/cgi-bin/header.html
Normal 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=-&action=raw&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
413
utils/stats/cgi-bin/index.cgi
Executable 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
30
utils/stats/database.h
Normal 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
121
utils/stats/db2wml.c
Normal 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
287
utils/stats/graph.c
Normal 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&W_CAMPAIGN=%s&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
62
utils/stats/run-tests.sh
Executable 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.
|
102
utils/stats/sqlite3_database.c
Normal file
102
utils/stats/sqlite3_database.c
Normal 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);
|
||||
}
|
||||
|
28
utils/stats/testsuite/1-bad-serial.input
Normal file
28
utils/stats/testsuite/1-bad-serial.input
Normal 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]
|
1
utils/stats/testsuite/1-bad-serial.output
Normal file
1
utils/stats/testsuite/1-bad-serial.output
Normal file
|
@ -0,0 +1 @@
|
|||
|
3
utils/stats/testsuite/1-bad-serial.test
Normal file
3
utils/stats/testsuite/1-bad-serial.test
Normal file
|
@ -0,0 +1,3 @@
|
|||
/* Returns NEXT game number. */
|
||||
|
||||
SELECT games_received FROM game_count;
|
28
utils/stats/testsuite/1-simple-serial.input
Normal file
28
utils/stats/testsuite/1-simple-serial.input
Normal 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]
|
28
utils/stats/testsuite/1-simple-serial.input2
Normal file
28
utils/stats/testsuite/1-simple-serial.input2
Normal 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]
|
1
utils/stats/testsuite/1-simple-serial.output
Normal file
1
utils/stats/testsuite/1-simple-serial.output
Normal file
|
@ -0,0 +1 @@
|
|||
2
|
3
utils/stats/testsuite/1-simple-serial.test
Normal file
3
utils/stats/testsuite/1-simple-serial.test
Normal file
|
@ -0,0 +1,3 @@
|
|||
/* Returns NEXT game number. */
|
||||
|
||||
SELECT games_received FROM game_count;
|
28
utils/stats/testsuite/1.parallel
Normal file
28
utils/stats/testsuite/1.parallel
Normal 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]
|
27
utils/stats/testsuite/2-no-serial.input
Normal file
27
utils/stats/testsuite/2-no-serial.input
Normal 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]
|
27
utils/stats/testsuite/2-no-serial.input2
Normal file
27
utils/stats/testsuite/2-no-serial.input2
Normal 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]
|
1
utils/stats/testsuite/2-no-serial.output
Normal file
1
utils/stats/testsuite/2-no-serial.output
Normal file
|
@ -0,0 +1 @@
|
|||
3
|
3
utils/stats/testsuite/2-no-serial.test
Normal file
3
utils/stats/testsuite/2-no-serial.test
Normal file
|
@ -0,0 +1,3 @@
|
|||
/* Returns NEXT game number. */
|
||||
|
||||
SELECT games_received FROM game_count;
|
28
utils/stats/testsuite/2.parallel
Normal file
28
utils/stats/testsuite/2.parallel
Normal 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]
|
28
utils/stats/testsuite/3.parallel
Normal file
28
utils/stats/testsuite/3.parallel
Normal 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]
|
28
utils/stats/testsuite/4.parallel
Normal file
28
utils/stats/testsuite/4.parallel
Normal 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]
|
28
utils/stats/testsuite/5.parallel
Normal file
28
utils/stats/testsuite/5.parallel
Normal 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]
|
28
utils/stats/testsuite/6.parallel
Normal file
28
utils/stats/testsuite/6.parallel
Normal 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]
|
28
utils/stats/testsuite/7.parallel
Normal file
28
utils/stats/testsuite/7.parallel
Normal 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]
|
28
utils/stats/testsuite/8.parallel
Normal file
28
utils/stats/testsuite/8.parallel
Normal 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]
|
28
utils/stats/testsuite/9.parallel
Normal file
28
utils/stats/testsuite/9.parallel
Normal 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]
|
18
utils/stats/testsuite/parallel-output
Normal file
18
utils/stats/testsuite/parallel-output
Normal file
|
@ -0,0 +1,18 @@
|
|||
1157260448000001491
|
||||
1157260448000001492
|
||||
1157260448000001493
|
||||
1157260448000001494
|
||||
1157260448000001495
|
||||
1157260448000001496
|
||||
1157260448000001497
|
||||
1157260448000001498
|
||||
1157260448000001499
|
||||
5876133823385251
|
||||
5876133823385252
|
||||
5876133823385253
|
||||
5876133823385254
|
||||
5876133823385255
|
||||
5876133823385256
|
||||
5876133823385257
|
||||
5876133823385258
|
||||
5876133823385259
|
3
utils/stats/testsuite/parallel-test
Normal file
3
utils/stats/testsuite/parallel-test
Normal 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
823
utils/stats/upload.cgi.c
Normal 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
135
utils/stats/utils.c
Normal 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
72
utils/stats/utils.h
Normal 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 */
|
Loading…
Add table
Reference in a new issue