diff --git a/documentation/changeLog.txt b/documentation/changeLog.txt index 902c95497..63bcd06aa 100644 --- a/documentation/changeLog.txt +++ b/documentation/changeLog.txt @@ -4,6 +4,30 @@ http://onehouronelife.com/updateLog.php +Server Fixes + +--DIE babies don't affect genetic score of their ancestors (but affect + themselves). + + + +Version 245 2019-June-28 + +--Support for new genetic fitness score across lives, ranking, leaderboard, + and genetic history display inside client. + + + + +Server Fixes + +--Whoops... totally forgot to update killEmotionIndex.ini server setting. + Murder Mouth should work now. + + + + + Version 243 2019-June-21 --Better randomization (less repetative) of multi-sound sets by shuffling diff --git a/documentation/html/footer.php b/documentation/html/footer.php index f177ff0c0..66ea06c09 100644 --- a/documentation/html/footer.php +++ b/documentation/html/footer.php @@ -32,12 +32,15 @@ - - - - - - + + + + + + + + +
[Home][Buy][Food Stats][Fail Stats][Artwork][Credits][Home][Buy][Wiki][Food Stats][Fail Stats][Artwork][Credits]
diff --git a/documentation/html/header.php b/documentation/html/header.php index 78b5221c2..2f1eb2b0c 100644 --- a/documentation/html/header.php +++ b/documentation/html/header.php @@ -37,12 +37,12 @@ [Buy] [News] [Family Trees] + [Leaderboard] [Photos] [Update Log] [Polls] [Forums] [Tech Tree] - [Wiki] diff --git a/fitnessServer/index.php b/fitnessServer/index.php new file mode 100644 index 000000000..894c903a6 --- /dev/null +++ b/fitnessServer/index.php @@ -0,0 +1,72 @@ + + + + + + + + + + +
+ +
+ + + +
+ +
+ Yubikey:
+ + + + + + + +
+
+Server-provided Pepper: +
+ + +
+hmac_sha1 of password with pepper as key:
+ + +
+ + + \ No newline at end of file diff --git a/fitnessServer/passwordHashUtility.php b/fitnessServer/passwordHashUtility.php new file mode 100644 index 000000000..34ff48f0c --- /dev/null +++ b/fitnessServer/passwordHashUtility.php @@ -0,0 +1,28 @@ + +
+ + + + diff --git a/fitnessServer/protocol.txt b/fitnessServer/protocol.txt new file mode 100644 index 000000000..e063c06a8 --- /dev/null +++ b/fitnessServer/protocol.txt @@ -0,0 +1,172 @@ + + + + +server.php +?action=get_client_sequence_number +&email=[email address] + +Return: +sequence number +OK + +Gets next valid sequence number associated with email, for client requests. +Note that even if email is unknown to server, 0 will be returned so that first +request can be submitted. + + + +server.php +?action=get_server_sequence_number +&server_name=[full server name] + +Return: +sequence number +OK + +Gets next valid sequence number associated with server name, for server +requests. Note that even if server is unknown to server, 0 will be returned +so that first request can be submitted. + + + + +server.php +?action=report_death +&email=[email address] +&name=[character name] +&display_id=[object id] +&self_rel_name=[word for You] +&ancestor_list=[list] +&server_name=[name] +&sequence_number=[int] +&hash_value=[hash value] + +Return: +OK +-or- +DENIED + +Used by game servers to indicate that a given email finished a life and died + +DENIED is returned if there are no life tokens left. + + +hash_value is computed on both ends with: + +HMAC_SHA1( $shared_secret, $sequence_number ) + + +Where $shared_secret is a secret string known to both the fitnessServer and +the game servers that have permission to spend tokens. + +If sequence number is <= previously used sequence number for this server, +request will be rejected. + +Anscestor list is in following format: + +email Relation_Name,email Relation_Name,... + +(obviously, this list will be URL-encoded) + +Relation names like Great Granddaughter must have spaces replaced by _, like: +Great_Granddaughter + + +NOTE: in case of Eve or baby-suicide, Ancestor list will be blank + + + + + +server.php +?action=get_score +&email=[email address] +&server_name=[name] +&sequence_number=[int] +&hash_value=[hash value] + +Return: +score +OK +-or- +DENIED + +Used by game servers to request a given user's current score. + +DENIED is returned if the email isn't known + + + + + + + +==== +These calls are called by game clients +==== + + +server.php +?action=get_client_score +&email=[email address] +&sequence_number=[int] +&hash_value=[hash value] + +Return: +leaderboard_name +score +rank +OK +-or- +DENIED + +Used by clients to request score information. + +DENIED is returned if the email isn't known or hash check fails + + + +hash_value is computed on both ends with: + +HMAC_SHA1( $ticket_id, $string_to_hash ) + +Where $ticket_id has hyphens removed and is all uppercase. + + + + +server.php +?action=get_client_score_details +&email=[email address] +&sequence_number=[int] +&hash_value=[hash value] + +Return: +leaderboard_name +score +rank +name,relation,display_id,died_sec_ago,age,old_score,new_score +name,relation,display_id,died_sec_ago,age,old_score,new_score +name,relation,display_id,died_sec_ago,age,old_score,new_score +.... +name,relation,display_id,died_sec_ago,age,old_score,new_score +OK +-or- +DENIED + +Used by clients to request detailed score information. + +DENIED is returned if the email isn't known or hash check fails + +Same results as get_client_score, followed by list of recent selves and +offspring that contributed to the score + + + + +hash_value is computed on both ends with: + +HMAC_SHA1( $ticket_id, $string_to_hash ) + +Where $ticket_id has hyphens removed and is all uppercase. diff --git a/fitnessServer/server.php b/fitnessServer/server.php new file mode 100644 index 000000000..b99c2635c --- /dev/null +++ b/fitnessServer/server.php @@ -0,0 +1,2001 @@ + +Fitness Server Web-based setup + + +
+ +
+ +
"; + +$setup_footer = " +
+
+
+"; + + + + + + +// ensure that magic quotes are OFF +// we hand-filter all _REQUEST data with regexs before submitting it to the DB +if( get_magic_quotes_gpc() ) { + // force magic quotes to be removed + $_GET = array_map( 'fs_stripslashes_deep', $_GET ); + $_POST = array_map( 'fs_stripslashes_deep', $_POST ); + $_REQUEST = array_map( 'fs_stripslashes_deep', $_REQUEST ); + $_COOKIE = array_map( 'fs_stripslashes_deep', $_COOKIE ); + } + + + +// Check that the referrer header is this page, or kill the connection. +// Used to block XSRF attacks on state-changing functions. +// (To prevent it from being dangerous to surf other sites while you are +// logged in as admin.) +// Thanks Chris Cowan. +function fs_checkReferrer() { + global $fullServerURL; + + if( !isset($_SERVER['HTTP_REFERER']) || + strpos($_SERVER['HTTP_REFERER'], $fullServerURL) !== 0 ) { + + die( "Bad referrer header" ); + } + } + + + + +// all calls need to connect to DB, so do it once here +fs_connectToDatabase(); + +// close connection down below (before function declarations) + + +// testing: +//sleep( 5 ); + + +// general processing whenver server.php is accessed directly + + + + +// grab POST/GET variables +$action = fs_requestFilter( "action", "/[A-Z_]+/i" ); + +$debug = fs_requestFilter( "debug", "/[01]/" ); + +$remoteIP = ""; +if( isset( $_SERVER[ "REMOTE_ADDR" ] ) ) { + $remoteIP = $_SERVER[ "REMOTE_ADDR" ]; + } + + + + +if( $action == "version" ) { + global $fs_version; + echo "$fs_version"; + } +else if( $action == "get_client_sequence_number" ) { + fs_getClientSequenceNumber(); + } +else if( $action == "get_server_sequence_number" ) { + fs_getServerSequenceNumber(); + } +else if( $action == "report_death" ) { + fs_reportDeath(); + } +else if( $action == "get_score" ) { + fs_getScore(); + } +else if( $action == "get_client_score" ) { + fs_getClientScore(); + } +else if( $action == "get_client_score_details" ) { + fs_getClientScoreDetails(); + } +else if( $action == "show_log" ) { + fs_showLog(); + } +else if( $action == "clear_log" ) { + fs_clearLog(); + } +else if( $action == "show_data" ) { + fs_showData(); + } +else if( $action == "show_detail" ) { + fs_showDetail(); + } +else if( $action == "logout" ) { + fs_logout(); + } +else if( $action == "show_leaderboard" ) { + fs_showLeaderboard(); + } +else if( $action == "fs_setup" ) { + global $setup_header, $setup_footer; + echo $setup_header; + + echo "

Fitness Server Web-based Setup

"; + + echo "Creating tables:
"; + + echo "
+
+ +
"; + + fs_setupDatabase(); + + echo "


"; + + echo $setup_footer; + } +else if( preg_match( "/server\.php/", $_SERVER[ "SCRIPT_NAME" ] ) ) { + // server.php has been called without an action parameter + + // the preg_match ensures that server.php was called directly and + // not just included by another script + + // quick (and incomplete) test to see if we should show instructions + global $tableNamePrefix; + + // check if our tables exist + $exists = + fs_doesTableExist( $tableNamePrefix . "users" ) && + fs_doesTableExist( $tableNamePrefix . "lives" ) && + fs_doesTableExist( $tableNamePrefix . "offspring" ) && + fs_doesTableExist( $tableNamePrefix . "first_names" ) && + fs_doesTableExist( $tableNamePrefix . "last_names" ) && + fs_doesTableExist( $tableNamePrefix . "log" ); + + + if( $exists ) { + echo "Fitness Server database setup and ready"; + } + else { + // start the setup procedure + + global $setup_header, $setup_footer; + echo $setup_header; + + echo "

Fitness Server Web-based Setup

"; + + echo "Fitness Server will walk you through a " . + "brief setup process.

"; + + echo "Step 1: ". + "". + "create the database tables"; + + echo $setup_footer; + } + } + + + +// done processing +// only function declarations below + +fs_closeDatabase(); + + + + +function fs_populateNameTable( $inTableName, $inFileName ) { + + if( $file = fopen( $inFileName, "r" ) ) { + + $firstLine = true; + + $query = "INSERT INTO $inTableName ( name ) VALUES "; + + while( !feof( $file ) ) { + $line = trim( fgets( $file) ); + + if( $line == "" ) { + continue; + } + + if( ! $firstLine ) { + $query = $query . ","; + } + + $query = $query . " ( '$line' )"; + + $firstLine = false; + } + + fclose( $file ); + + $query = $query . ";"; + + $result = fs_queryDatabase( $query ); + } + else { + echo "(Failed to populate, couldn't open file $inFileName)
"; + } + } + + + + +/** + * Creates the database tables needed by seedBlogs. + */ +function fs_setupDatabase() { + global $tableNamePrefix; + + $tableName = $tableNamePrefix . "log"; + if( ! fs_doesTableExist( $tableName ) ) { + + // this table contains general info about the server + // use INNODB engine so table can be locked + $query = + "CREATE TABLE $tableName(" . + "entry TEXT NOT NULL, ". + "entry_time DATETIME NOT NULL, ". + "index( entry_time ) );"; + + $result = fs_queryDatabase( $query ); + + echo "$tableName table created
"; + } + else { + echo "$tableName table already exists
"; + } + + + + $tableName = $tableNamePrefix . "servers"; + if( ! fs_doesTableExist( $tableName ) ) { + + $query = + "CREATE TABLE $tableName(" . + "id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT," . + // example: server1.onehouronelife.com + "name VARCHAR(254) NOT NULL," . + "UNIQUE KEY( name ),". + // for use with server requests + "sequence_number INT NOT NULL );"; + + $result = fs_queryDatabase( $query ); + + echo "$tableName table created
"; + } + else { + echo "$tableName table already exists
"; + } + + + + + $tableName = $tableNamePrefix . "users"; + if( ! fs_doesTableExist( $tableName ) ) { + + $query = + "CREATE TABLE $tableName(" . + "id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT," . + "email VARCHAR(254) NOT NULL," . + "UNIQUE KEY( email )," . + "leaderboard_name varchar(254) NOT NULL,". + "lives_affecting_score INT UNSIGNED NOT NULL,". + "score FLOAT NOT NULL," . + "index( score )," . + "last_action_time DATETIME NOT NULL,". + "index( last_action_time, score ),". + // for use with client connections + "client_sequence_number INT NOT NULL );"; + + $result = fs_queryDatabase( $query ); + + echo "$tableName table created
"; + } + else { + echo "$tableName table already exists
"; + } + + + + // holds common info about each life, referenced from offspring table + $tableName = $tableNamePrefix . "lives"; + if( ! fs_doesTableExist( $tableName ) ) { + + $query = + "CREATE TABLE $tableName(" . + "id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT," . + "name VARCHAR(254) NOT NULL,". + // age at time of death in years + "age FLOAT UNSIGNED NOT NULL,". + "display_id INT UNSIGNED NOT NULL );"; + + $result = fs_queryDatabase( $query ); + + echo "$tableName table created
"; + } + else { + echo "$tableName table already exists
"; + } + + + $tableName = $tableNamePrefix . "offspring"; + if( ! fs_doesTableExist( $tableName ) ) { + + $query = + "CREATE TABLE $tableName(" . + "player_id INT UNSIGNED NOT NULL," . + "life_id INT UNSIGNED NOT NULL,". + "PRIMARY KEY( player_id, life_id ),". + // this will be You for self + // or Granddaughter, etc. + "relation_name VARCHAR(254) NOT NULL,". + // player_id's score before this offspring + "old_score FLOAT UNSIGNED NOT NULL,". + // player_id's score after this offspring + "new_score FLOAT UNSIGNED NOT NULL,". + "death_time DATETIME NOT NULL,". + // for fast filtering/sorting of most-recent 20 + // offspring for a given player + "index( player_id, death_time ) );"; + + $result = fs_queryDatabase( $query ); + + echo "$tableName table created
"; + } + else { + echo "$tableName table already exists
"; + } + + + + $tableName = $tableNamePrefix . "first_names"; + if( ! fs_doesTableExist( $tableName ) ) { + + $query = + "CREATE TABLE $tableName(" . + "id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT," . + "name VARCHAR(245) NOT NULL );"; + + $result = fs_queryDatabase( $query ); + + echo "$tableName table created
"; + + fs_populateNameTable( $tableName, "firstNames.txt" ); + } + else { + echo "$tableName table already exists
"; + } + + + + $tableName = $tableNamePrefix . "last_names"; + if( ! fs_doesTableExist( $tableName ) ) { + + $query = + "CREATE TABLE $tableName(" . + "id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT," . + "name VARCHAR(245) NOT NULL );"; + + $result = fs_queryDatabase( $query ); + + echo "$tableName table created
"; + + fs_populateNameTable( $tableName, "lastNames.txt" ); + } + else { + echo "$tableName table already exists
"; + } + + + } + + + +function fs_showLog() { + fs_checkPassword( "show_log" ); + + echo "[Main]


"; + + global $tableNamePrefix; + + $query = "SELECT * FROM $tableNamePrefix"."log ". + "ORDER BY entry_time DESC;"; + $result = fs_queryDatabase( $query ); + + $numRows = mysqli_num_rows( $result ); + + + + echo "". + "Clear log"; + + echo "
"; + + echo "$numRows log entries:


\n"; + + + for( $i=0; $i<$numRows; $i++ ) { + $time = fs_mysqli_result( $result, $i, "entry_time" ); + $entry = htmlspecialchars( fs_mysqli_result( $result, $i, "entry" ) ); + + echo "$time:
$entry
\n"; + } + } + + + +function fs_clearLog() { + fs_checkPassword( "clear_log" ); + + echo "[Main]


"; + + global $tableNamePrefix; + + $query = "DELETE FROM $tableNamePrefix"."log;"; + $result = fs_queryDatabase( $query ); + + if( $result ) { + echo "Log cleared."; + } + else { + echo "DELETE operation failed?"; + } + } + + + + + + + + + + + + + + + + + + + + +function fs_logout() { + fs_checkReferrer(); + + fs_clearPasswordCookie(); + + echo "Logged out"; + } + + + + +function fs_showData( $checkPassword = true ) { + // these are global so they work in embeded function call below + global $skip, $search, $order_by; + + if( $checkPassword ) { + fs_checkPassword( "show_data" ); + } + + global $tableNamePrefix, $remoteIP; + + + echo "". + "". + "". + "
[Main][Logout]



"; + + + + + $skip = fs_requestFilter( "skip", "/[0-9]+/", 0 ); + + global $usersPerPage; + + $search = fs_requestFilter( "search", "/[A-Z0-9_@. \-]+/i" ); + + $order_by = fs_requestFilter( "order_by", "/[A-Z_]+/i", + "id" ); + + $keywordClause = ""; + $searchDisplay = ""; + + if( $search != "" ) { + + + $keywordClause = "WHERE ( email LIKE '%$search%' " . + "OR id LIKE '%$search%' ) "; + + $searchDisplay = " matching $search"; + } + + + + + // first, count results + $query = "SELECT COUNT(*) FROM $tableNamePrefix". + "users $keywordClause;"; + + $result = fs_queryDatabase( $query ); + $totalRecords = fs_mysqli_result( $result, 0, 0 ); + + + $orderDir = "DESC"; + + if( $order_by == "email" ) { + $orderDir = "ASC"; + } + + + $query = "SELECT * ". + "FROM $tableNamePrefix"."users $keywordClause". + "ORDER BY $order_by $orderDir ". + "LIMIT $skip, $usersPerPage;"; + $result = fs_queryDatabase( $query ); + + $numRows = mysqli_num_rows( $result ); + + $startSkip = $skip + 1; + + $endSkip = $startSkip + $usersPerPage - 1; + + if( $endSkip > $totalRecords ) { + $endSkip = $totalRecords; + } + + + + // form for searching users and resetting tokens +?> +
+ + + +
+ + + + + + + +
+
+\n"; + + + $nextSkip = $skip + $usersPerPage; + + $prevSkip = $skip - $usersPerPage; + + if( $prevSkip >= 0 ) { + echo "[". + "Previous Page] "; + } + if( $nextSkip < $totalRecords ) { + echo "[". + "Next Page]"; + } + + echo "

"; + + echo "\n"; + + function orderLink( $inOrderBy, $inLinkText ) { + global $skip, $search, $order_by; + if( $inOrderBy == $order_by ) { + // already displaying this order, don't show link + return "$inLinkText"; + } + + // else show a link to switch to this order + return "$inLinkText"; + } + + + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + + + for( $i=0; $i<$numRows; $i++ ) { + $id = fs_mysqli_result( $result, $i, "id" ); + $email = fs_mysqli_result( $result, $i, "email" ); + $leaderboard_name = fs_mysqli_result( $result, $i, "leaderboard_name" ); + $last_action_time = fs_mysqli_result( $result, $i, "last_action_time" ); + $score = fs_mysqli_result( $result, $i, "score" ); + $lives_affecting_score = + fs_mysqli_result( $result, $i, "lives_affecting_score" ); + + $encodedEmail = urlencode( $email ); + + + echo "\n"; + + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + } + echo "
".orderLink( "id", "ID" )."".orderLink( "email", "Email" )."".orderLink( "leaderboard_name", "Leaderbord Name" )."".orderLink( "last_action_time", "Last Action" )."".orderLink( "score", "Score" )."".orderLink( "lives_affecting_score", "Lives Counted" )."
$id". + "". + "$email$leaderboard_name$last_action_time$score$lives_affecting_score
"; + + + echo "
"; + + echo "". + "Show log"; + echo "
"; + echo "Generated for $remoteIP\n"; + + } + + + + + + + + +function fs_showDetail( $checkPassword = true ) { + if( $checkPassword ) { + fs_checkPassword( "show_detail" ); + } + + echo "[Main]


"; + + global $tableNamePrefix; + + + $email = fs_requestFilter( "email", "/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+/i" ); + + $query = "SELECT id ". + "FROM $tableNamePrefix"."users ". + "WHERE email = '$email';"; + $result = fs_queryDatabase( $query ); + + $id = fs_mysqli_result( $result, 0, "id" ); + + $query = "SELECT name, age, relation_name, ". + "old_score, new_score, death_time ". + "FROM $tableNamePrefix"."offspring AS offspring ". + "INNER JOIN $tableNamePrefix"."lives AS lives ". + "ON offspring.life_id = lives.id ". + "WHERE offspring.player_id = $id ORDER BY offspring.death_time DESC ". + "LIMIT 20"; + + + $result = fs_queryDatabase( $query ); + + echo "
"; + + echo "ID: $id

"; + echo "Email: $email

"; + echo "
"; + + $numRows = mysqli_num_rows( $result ); + + echo ""; + for( $i=0; $i<$numRows; $i++ ) { + $name = fs_mysqli_result( $result, $i, "name" ); + $age = fs_mysqli_result( $result, $i, "age" ); + $relation_name = fs_mysqli_result( $result, $i, "relation_name" ); + $old_score = fs_mysqli_result( $result, $i, "old_score" ); + $new_score = fs_mysqli_result( $result, $i, "new_score" ); + $death_time = fs_mysqli_result( $result, $i, "death_time" ); + + $delta = $new_score - $old_score; + + $deltaString; + + if( $delta < 0 ) { + $deltaString = " - " . abs( $delta ); + } + else { + $deltaString = " + " . $delta; + } + + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + echo "
$name$age years old$relation_name$old_score$deltaString$new_score$death_time
"; + } + + + + +function fs_showLeaderboard() { + + global $tableNamePrefix; + + global $header, $footer; + + eval( $header ); + + echo "
"; + + $query = "SELECT leaderboard_name, score ". + "FROM $tableNamePrefix"."users ". + "WHERE last_action_time > DATE_SUB( NOW(), INTERVAL 48 HOUR )". + "ORDER BY score DESC limit 1000;"; + + $result = fs_queryDatabase( $query ); + + $numRows = mysqli_num_rows( $result ); + + echo ""; + + for( $i=0; $i<$numRows; $i++ ) { + $place = $i + 1; + + $name = fs_mysqli_result( $result, $i, "leaderboard_name" ); + $score = fs_mysqli_result( $result, $i, "score" ); + + echo ""; + } + echo "
$place.$name$score
"; + + + echo "
"; + + eval( $footer ); + } + + + + + + + + +function fs_getClientSequenceNumber() { + global $tableNamePrefix; + + + $email = fs_requestFilter( "email", "/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+/i", "" ); + + if( $email == "" ) { + fs_log( "getClientSequenceNumber denied for bad email" ); + + echo "DENIED"; + return; + } + + + $seq = fs_getClientSequenceNumberForEmail( $email ); + + echo "$seq\n"."OK"; + } + + + +// assumes already-filtered, valid email +// returns 0 if not found +function fs_getClientSequenceNumberForEmail( $inEmail ) { + global $tableNamePrefix; + + $query = "SELECT client_sequence_number FROM $tableNamePrefix"."users ". + "WHERE email = '$inEmail';"; + $result = fs_queryDatabase( $query ); + + $numRows = mysqli_num_rows( $result ); + + if( $numRows < 1 ) { + return 0; + } + else { + return fs_mysqli_result( $result, 0, "client_sequence_number" ); + } + } + + + + + +function fs_getServerSequenceNumber() { + global $tableNamePrefix; + + + $name = fs_requestFilter( "server_name", "/[A-Z0-9.\-]+/i", "" ); + + if( $name == "" ) { + fs_log( "getServerSequenceNumber denied for bad name" ); + + echo "DENIED"; + return; + } + + + $seq = fs_getServerSequenceNumberForName( $name ); + + echo "$seq\n"."OK"; + } + + + +// assumes already-filtered, valid name +// returns 0 if not found +function fs_getServerSequenceNumberForName( $inName ) { + global $tableNamePrefix; + + $query = "SELECT sequence_number FROM $tableNamePrefix"."servers ". + "WHERE name = '$inName';"; + $result = fs_queryDatabase( $query ); + + $numRows = mysqli_num_rows( $result ); + + if( $numRows < 1 ) { + return 0; + } + else { + return fs_mysqli_result( $result, 0, "sequence_number" ); + } + } + + + + + + +function fs_checkServerSeqHash( $name ) { + global $sharedGameServerSecret; + + + global $action; + + + $sequence_number = fs_requestFilter( "sequence_number", "/[0-9]+/i", "0" ); + + $hash_value = fs_requestFilter( "hash_value", "/[A-F0-9]+/i", "" ); + + $hash_value = strtoupper( $hash_value ); + + + if( $name == "" ) { + + fs_log( "checkServerSeqHash denied for bad server name" ); + + echo "DENIED"; + die(); + } + + $trueSeq = fs_getServerSequenceNumberForName( $name ); + + if( $trueSeq > $sequence_number ) { + fs_log( "checkServerSeqHash denied for stale sequence number" ); + + echo "DENIED"; + die(); + } + + $computedHashValue = + strtoupper( fs_hmac_sha1( $sharedGameServerSecret, $sequence_number ) ); + + if( $computedHashValue != $hash_value ) { + // fs_log( "curse denied for bad hash value" ); + + echo "DENIED"; + die(); + } + + return $trueSeq; + } + + + + +function fs_checkClientSeqHash( $email ) { + global $sharedGameServerSecret; + + + global $action; + + + $sequence_number = fs_requestFilter( "sequence_number", "/[0-9]+/i", "0" ); + + $hash_value = fs_requestFilter( "hash_value", "/[A-F0-9]+/i", "" ); + + $hash_value = strtoupper( $hash_value ); + + + if( $email == "" ) { + + fs_log( "checkClientSeqHash denied for bad email" ); + + echo "DENIED"; + die(); + } + + $trueSeq = fs_getClientSequenceNumberForEmail( $email ); + + if( $trueSeq > $sequence_number ) { + fs_log( "checkClientSeqHash denied for stale sequence number" ); + + echo "DENIED"; + die(); + } + + $correct = false; + + $encodedEmail = urlencode( $email ); + + + global $ticketServerURL; + $url = "$ticketServerURL". + "?action=check_ticket_hash". + "&email=$encodedEmail". + "&hash_value=$hash_value". + "&string_to_hash=$sequence_number"; + + + $result = trim( file_get_contents( $url ) ); + + if( $result == "VALID" ) { + $correct = true; + } + + + if( ! $correct ) { + fs_log( "checkClientSeqHash denied, hash check failed" ); + + echo "DENIED"; + die(); + } + + + return $trueSeq; + } + + + + +function fs_pickLeaderboardName( $inEmail ) { + global $tableNamePrefix; + + $query = "SELECT COUNT(*) FROM $tableNamePrefix"."first_names ;"; + + $result = fs_queryDatabase( $query ); + $firstCount = fs_mysqli_result( $result, 0, 0 ); + + $query = "SELECT COUNT(*) FROM $tableNamePrefix"."last_names ;"; + + $result = fs_queryDatabase( $query ); + $lastCount = fs_mysqli_result( $result, 0, 0 ); + + + // include secret in hash, so people with code can't match + // names to emails + global $sharedGameServerSecret; + + $emailHash = sha1( $inEmail . $sharedGameServerSecret ); + + $seedA = hexdec( substr( $emailHash, 0, 8 ) ); + $seedB = hexdec( substr( $emailHash, 8, 8 ) ); + + fs_log( "Seeds: $seedA, $seedB" ); + + mt_srand( $seedA ); + + $firstPick = mt_rand( 1, $firstCount ); + + mt_srand( $seedB ); + $lastPick = mt_rand( 1, $lastCount ); + + + $query = "SELECT name FROM $tableNamePrefix"."first_names ". + "WHERE id = $firstPick;"; + + $result = fs_queryDatabase( $query ); + $firstName = fs_mysqli_result( $result, 0, 0 ); + + + $query = "SELECT name FROM $tableNamePrefix"."last_names ". + "WHERE id = $lastPick;"; + + $result = fs_queryDatabase( $query ); + $lastName = fs_mysqli_result( $result, 0, 0 ); + + $query = "SELECT COUNT(*) FROM $tableNamePrefix"."last_names ;"; + + + return ucwords( strtolower( "$firstName $lastName" ) ); + } + + + + +// log a death that will affect the score of $inEmail +function fs_logDeath( $inEmail, $life_id, $inRelName, $inAge ) { + global $tableNamePrefix; + + $query = "SELECT COUNT(*) FROM $tableNamePrefix"."users ". + "WHERE email = '$inEmail';"; + + $result = fs_queryDatabase( $query ); + $count = fs_mysqli_result( $result, 0, 0 ); + + if( $count == 0 ) { + fs_addUserRecord( $inEmail ); + } + + $query = "SELECT id, score FROM $tableNamePrefix"."users ". + "WHERE email = '$inEmail';"; + + $result = fs_queryDatabase( $query ); + $player_id = fs_mysqli_result( $result, 0, "id" ); + $old_score = fs_mysqli_result( $result, 0, "score" ); + + + // score update + global $formulaR, $formulaK; + + + $delta = $inAge - $old_score; + + if( $formulaR != 1 ) { + $delta = pow( $delta, $formulaR ); + } + $delta /= $formulaK; + + $new_score = $old_score + $delta; + + + + $query = "INSERT into $tableNamePrefix"."offspring ". + "SET player_id = $player_id, life_id = $life_id, ". + "relation_name = '$inRelName', old_score = $old_score,". + "new_score = $new_score, death_time = CURRENT_TIMESTAMP;"; + + fs_queryDatabase( $query ); + + + + $query = "UPDATE $tableNamePrefix"."users ". + "SET lives_affecting_score = lives_affecting_score + 1, ". + "score = $new_score, last_action_time = CURRENT_TIMESTAMP ". + "WHERE email = '$inEmail';"; + + fs_queryDatabase( $query ); + + } + + + + +function fs_checkAndUpdateServerSeqNumber() { + global $tableNamePrefix; + + $server_name = fs_requestFilter( "server_name", "/[A-Z0-9.\-]+/i", "" ); + + $trueSeq = fs_checkServerSeqHash( $server_name ); + + + // no locking is done here, because action is asynchronous anyway + + if( $trueSeq == 0 ) { + // no record exists, add one + $query = "INSERT INTO $tableNamePrefix". "servers SET " . + "name = '$server_name', ". + "sequence_number = 1 ". + "ON DUPLICATE KEY UPDATE sequence_number = sequence_number + 1;"; + fs_queryDatabase( $query ); + } + else { + $query = "UPDATE $tableNamePrefix". "servers SET " . + "sequence_number = sequence_number + 1 ". + "WHERE name = '$server_name';"; + fs_queryDatabase( $query ); + } + } + + + +function fs_checkAndUpdateClientSeqNumber() { + global $tableNamePrefix; + + $email = fs_requestFilter( "email", "/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+/i", "" ); + + $trueSeq = fs_checkClientSeqHash( $email ); + + + // no locking is done here, because action is asynchronous anyway + + if( $trueSeq == 0 ) { + // no record exists, add one + fs_addUserRecord( $email ); + } + else { + $query = "UPDATE $tableNamePrefix". "users SET " . + "client_sequence_number = client_sequence_number + 1 ". + "WHERE email = '$email';"; + fs_queryDatabase( $query ); + } + } + + + + +function fs_addUserRecord( $inEmail ) { + $leaderboard_name = fs_pickLeaderboardName( $inEmail ); + + global $tableNamePrefix; + + $query = "INSERT INTO $tableNamePrefix"."users ". + "SET email = '$inEmail', leaderboard_name = '$leaderboard_name', ". + "lives_affecting_score = 0, score=0, ". + "last_action_time=CURRENT_TIMESTAMP, client_sequence_number=1;"; + + fs_queryDatabase( $query ); + } + + + + +function fs_reportDeath() { + fs_checkAndUpdateServerSeqNumber(); + + + global $tableNamePrefix; + + + $email = fs_requestFilter( "email", "/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+/i", "" ); + + if( $email == "" ) { + echo "DENIED"; + die(); + } + + $age = fs_requestFilter( "age", "/[0-9.]+/i", "0" ); + $display_id = fs_requestFilter( "display_id", "/[0-9]+/i", "0" ); + + $name = fs_requestFilter( "name", "/[A-Z ]+/i", "" ); + + $name = ucwords( strtolower( $name ) ); + + $name = preg_replace( '/ /', '_', $name ); + + + $query = "INSERT INTO $tableNamePrefix". "lives SET " . + "name = '$name', ". + "age = $age, ". + "display_id = $display_id;"; + fs_queryDatabase( $query ); + + + global $fs_mysqlLink; + $life_id = mysqli_insert_id( $fs_mysqlLink ); + + $self_rel_name = fs_requestFilter( "self_rel_name", "/[A-Z ]+/i", "You" ); + + + + + // log effect of own death + fs_logDeath( $email, $life_id, $self_rel_name, $age ); + + + $ancestor_list = ""; + if( isset( $_REQUEST[ "ancestor_list" ] ) ) { + $ancestor_list = $_REQUEST[ "ancestor_list" ]; + } + + if( $ancestor_list != "" ) { + + $listParts = explode( ",", $ancestor_list ); + + foreach( $listParts as $part ) { + + list( $ancestorEmail, $relName ) = explode( " ", $part, 2 ); + + fs_logDeath( $ancestorEmail, $life_id, $relName, $age ); + } + } + + echo "OK"; + } + + + + + +function fs_getScore() { + fs_checkAndUpdateServerSeqNumber(); + + global $tableNamePrefix; + + + $email = fs_requestFilter( "email", "/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+/i", "" ); + + if( $email == "" ) { + echo "DENIED"; + die(); + } + + $query = "SELECT score FROM $tableNamePrefix"."users ". + "WHERE email = '$email';"; + + $result = fs_queryDatabase( $query ); + + $score = 0; + + if( mysqli_num_rows( $result ) > 0 ) { + $score = fs_mysqli_result( $result, 0, "score" ); + } + + echo "$score\nOK"; + } + + + +function fs_outputBasicScore( $inEmail ) { + global $tableNamePrefix; + + $query = "SELECT leaderboard_name, score, ". + "TIMESTAMPDIFF( SECOND, last_action_time, CURRENT_TIMESTAMP ) ". + " as sec_passed ". + "FROM $tableNamePrefix"."users ". + "WHERE email = '$inEmail';"; + + $result = fs_queryDatabase( $query ); + + if( mysqli_num_rows( $result ) > 0 ) { + $score = fs_mysqli_result( $result, 0, "score" ); + $leaderboard_name = fs_mysqli_result( $result, 0, "leaderboard_name" ); + $sec_passed = fs_mysqli_result( $result, 0, "sec_passed" ); + + $leaderboard_name = preg_replace( '/ /', '_', $leaderboard_name ); + + echo "$leaderboard_name\n$score\n"; + + // compute rank + + $rank = 0; + + if( $sec_passed < 3600 * 48 ) { + + $query = + "SELECT count(*) FROM $tableNamePrefix"."users ". + "WHERE last_action_time > ". + "DATE_SUB( NOW(), INTERVAL 48 HOUR ) ". + "AND score > $score;"; + + $result = fs_queryDatabase( $query ); + + $rank = 1 + fs_mysqli_result( $result, 0, 0 ); + } + echo "$rank\n"; + } + } + + + + +function fs_getClientScore() { + fs_checkAndUpdateClientSeqNumber(); + + global $tableNamePrefix; + + + $email = fs_requestFilter( "email", "/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+/i", "" ); + + if( $email == "" ) { + echo "DENIED"; + die(); + } + + fs_outputBasicScore( $email ); + + echo "OK"; + } + + + +function fs_getClientScoreDetails() { + fs_checkAndUpdateClientSeqNumber(); + + global $tableNamePrefix; + + + $email = fs_requestFilter( "email", "/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+/i", "" ); + + if( $email == "" ) { + echo "DENIED"; + die(); + } + + fs_outputBasicScore( $email ); + + // now offspring that contribute to score + + + $query = "SELECT id ". + "FROM $tableNamePrefix"."users ". + "WHERE email = '$email';"; + $result = fs_queryDatabase( $query ); + + $id = fs_mysqli_result( $result, 0, "id" ); + + $query = "SELECT name, age, display_id, relation_name, ". + "old_score, new_score, ". + "TIMESTAMPDIFF( SECOND, death_time, CURRENT_TIMESTAMP ) ". + " as died_sec_ago ". + "FROM $tableNamePrefix"."offspring AS offspring ". + "INNER JOIN $tableNamePrefix"."lives AS lives ". + "ON offspring.life_id = lives.id ". + "WHERE offspring.player_id = $id ORDER BY offspring.death_time DESC ". + "LIMIT 20"; + + + $result = fs_queryDatabase( $query ); + + $numRows = mysqli_num_rows( $result ); + + for( $i=0; $i<$numRows; $i++ ) { + $name = fs_mysqli_result( $result, $i, "name" ); + $age = fs_mysqli_result( $result, $i, "age" ); + $display_id = fs_mysqli_result( $result, $i, "display_id" ); + $relation_name = fs_mysqli_result( $result, $i, "relation_name" ); + $old_score = fs_mysqli_result( $result, $i, "old_score" ); + $new_score = fs_mysqli_result( $result, $i, "new_score" ); + $died_sec_ago = fs_mysqli_result( $result, $i, "died_sec_ago" ); + // one per line + // name,relation,display_id,died_sec_ago,age,old_score,new_score + + echo "$name,$relation_name,$display_id,". + "$died_sec_ago,$age,$old_score,$new_score\n"; + } + + + echo "OK"; + } + + + + + + +$fs_mysqlLink; + + +// general-purpose functions down here, many copied from seedBlogs + +/** + * Connects to the database according to the database variables. + */ +function fs_connectToDatabase() { + global $databaseServer, + $databaseUsername, $databasePassword, $databaseName, + $fs_mysqlLink; + + + $fs_mysqlLink = + mysqli_connect( $databaseServer, $databaseUsername, $databasePassword ) + or fs_operationError( "Could not connect to database server: " . + mysqli_error( $fs_mysqlLink ) ); + + mysqli_select_db( $fs_mysqlLink, $databaseName ) + or fs_operationError( "Could not select $databaseName database: " . + mysqli_error( $fs_mysqlLink ) ); + } + + + +/** + * Closes the database connection. + */ +function fs_closeDatabase() { + global $fs_mysqlLink; + + mysqli_close( $fs_mysqlLink ); + } + + +/** + * Returns human-readable summary of a timespan. + * Examples: 10.5 hours + * 34 minutes + * 45 seconds + */ +function fs_secondsToTimeSummary( $inSeconds ) { + if( $inSeconds < 120 ) { + if( $inSeconds == 1 ) { + return "$inSeconds second"; + } + return "$inSeconds seconds"; + } + else if( $inSeconds < 3600 ) { + $min = number_format( $inSeconds / 60, 0 ); + return "$min minutes"; + } + else { + $hours = number_format( $inSeconds / 3600, 1 ); + return "$hours hours"; + } + } + + +/** + * Returns human-readable summary of a distance back in time. + * Examples: 10 hours + * 34 minutes + * 45 seconds + * 19 days + * 3 months + * 2.5 years + */ +function fs_secondsToAgeSummary( $inSeconds ) { + if( $inSeconds < 120 ) { + if( $inSeconds == 1 ) { + return "$inSeconds second"; + } + return "$inSeconds seconds"; + } + else if( $inSeconds < 3600 * 2 ) { + $min = number_format( $inSeconds / 60, 0 ); + return "$min minutes"; + } + else if( $inSeconds < 24 * 3600 * 2 ) { + $hours = number_format( $inSeconds / 3600, 0 ); + return "$hours hours"; + } + else if( $inSeconds < 24 * 3600 * 60 ) { + $days = number_format( $inSeconds / ( 3600 * 24 ), 0 ); + return "$days days"; + } + else if( $inSeconds < 24 * 3600 * 365 * 2 ) { + // average number of days per month + // based on 400 year calendar cycle + // we skip a leap year every 100 years unless the year is divisible by 4 + $months = number_format( $inSeconds / ( 3600 * 24 * 30.436875 ), 0 ); + return "$months months"; + } + else { + // same logic behind computing average length of a year + $years = number_format( $inSeconds / ( 3600 * 24 * 365.2425 ), 1 ); + return "$years years"; + } + } + + + +/** + * Queries the database, and dies with an error message on failure. + * + * @param $inQueryString the SQL query string. + * + * @return a result handle that can be passed to other mysql functions. + */ +function fs_queryDatabase( $inQueryString ) { + global $fs_mysqlLink; + + if( gettype( $fs_mysqlLink ) != "resource" ) { + // not a valid mysql link? + fs_connectToDatabase(); + } + + $result = mysqli_query( $fs_mysqlLink, $inQueryString ); + + if( $result == FALSE ) { + + $errorNumber = mysqli_errno( $fs_mysqlLink ); + + // server lost or gone? + if( $errorNumber == 2006 || + $errorNumber == 2013 || + // access denied? + $errorNumber == 1044 || + $errorNumber == 1045 || + // no db selected? + $errorNumber == 1046 ) { + + // connect again? + fs_closeDatabase(); + fs_connectToDatabase(); + + $result = mysqli_query( $fs_mysqlLink, $inQueryString ) + or fs_operationError( + "Database query failed:
$inQueryString

" . + mysqli_error( $fs_mysqlLink ) ); + } + else { + // some other error (we're still connected, so we can + // add log messages to database + fs_fatalError( "Database query failed:
$inQueryString

" . + mysqli_error( $fs_mysqlLink ) ); + } + } + + return $result; + } + + + +/** + * Replacement for the old mysql_result function. + */ +function fs_mysqli_result( $result, $number, $field=0 ) { + mysqli_data_seek( $result, $number ); + $row = mysqli_fetch_array( $result ); + return $row[ $field ]; + } + + + +/** + * Checks whether a table exists in the currently-connected database. + * + * @param $inTableName the name of the table to look for. + * + * @return 1 if the table exists, or 0 if not. + */ +function fs_doesTableExist( $inTableName ) { + // check if our table exists + $tableExists = 0; + + $query = "SHOW TABLES"; + $result = fs_queryDatabase( $query ); + + $numRows = mysqli_num_rows( $result ); + + + for( $i=0; $i<$numRows && ! $tableExists; $i++ ) { + + $tableName = fs_mysqli_result( $result, $i, 0 ); + + if( $tableName == $inTableName ) { + $tableExists = 1; + } + } + return $tableExists; + } + + + +function fs_log( $message ) { + global $enableLog, $tableNamePrefix, $fs_mysqlLink; + + if( $enableLog ) { + $slashedMessage = mysqli_real_escape_string( $fs_mysqlLink, $message ); + + $query = "INSERT INTO $tableNamePrefix"."log VALUES ( " . + "'$slashedMessage', CURRENT_TIMESTAMP );"; + $result = fs_queryDatabase( $query ); + } + } + + + +/** + * Displays the error page and dies. + * + * @param $message the error message to display on the error page. + */ +function fs_fatalError( $message ) { + //global $errorMessage; + + // set the variable that is displayed inside error.php + //$errorMessage = $message; + + //include_once( "error.php" ); + + // for now, just print error message + $logMessage = "Fatal error: $message"; + + echo( $logMessage ); + + fs_log( $logMessage ); + + die(); + } + + + +/** + * Displays the operation error message and dies. + * + * @param $message the error message to display. + */ +function fs_operationError( $message ) { + + // for now, just print error message + echo( "ERROR: $message" ); + die(); + } + + +/** + * Recursively applies the addslashes function to arrays of arrays. + * This effectively forces magic_quote escaping behavior, eliminating + * a slew of possible database security issues. + * + * @inValue the value or array to addslashes to. + * + * @return the value or array with slashes added. + */ +function fs_addslashes_deep( $inValue ) { + return + ( is_array( $inValue ) + ? array_map( 'fs_addslashes_deep', $inValue ) + : addslashes( $inValue ) ); + } + + + +/** + * Recursively applies the stripslashes function to arrays of arrays. + * This effectively disables magic_quote escaping behavior. + * + * @inValue the value or array to stripslashes from. + * + * @return the value or array with slashes removed. + */ +function fs_stripslashes_deep( $inValue ) { + return + ( is_array( $inValue ) + ? array_map( 'fs_stripslashes_deep', $inValue ) + : stripslashes( $inValue ) ); + } + + + +/** + * Filters a $_REQUEST variable using a regex match. + * + * Returns "" (or specified default value) if there is no match. + */ +function fs_requestFilter( $inRequestVariable, $inRegex, $inDefault = "" ) { + if( ! isset( $_REQUEST[ $inRequestVariable ] ) ) { + return $inDefault; + } + + return fs_filter( $_REQUEST[ $inRequestVariable ], $inRegex, $inDefault ); + } + + +/** + * Filters a value using a regex match. + * + * Returns "" (or specified default value) if there is no match. + */ +function fs_filter( $inValue, $inRegex, $inDefault = "" ) { + + $numMatches = preg_match( $inRegex, + $inValue, $matches ); + + if( $numMatches != 1 ) { + return $inDefault; + } + + return $matches[0]; + } + + + +// this function checks the password directly from a request variable +// or via hash from a cookie. +// +// It then sets a new cookie for the next request. +// +// This avoids storing the password itself in the cookie, so a stale cookie +// (cached by a browser) can't be used to figure out the password and log in +// later. +function fs_checkPassword( $inFunctionName ) { + $password = ""; + $password_hash = ""; + + $badCookie = false; + + + global $accessPasswords, $tableNamePrefix, $remoteIP, $enableYubikey, + $passwordHashingPepper; + + $cookieName = $tableNamePrefix . "cookie_password_hash"; + + $passwordSent = false; + + if( isset( $_REQUEST[ "passwordHMAC" ] ) ) { + $passwordSent = true; + + // already hashed client-side on login form + // hash again, because hash client sends us is not stored in + // our settings file + $password = fs_hmac_sha1( $passwordHashingPepper, + $_REQUEST[ "passwordHMAC" ] ); + + + // generate a new hash cookie from this password + $newSalt = time(); + $newHash = md5( $newSalt . $password ); + + $password_hash = $newSalt . "_" . $newHash; + } + else if( isset( $_COOKIE[ $cookieName ] ) ) { + fs_checkReferrer(); + $password_hash = $_COOKIE[ $cookieName ]; + + // check that it's a good hash + + $hashParts = preg_split( "/_/", $password_hash ); + + // default, to show in log message on failure + // gets replaced if cookie contains a good hash + $password = "(bad cookie: $password_hash)"; + + $badCookie = true; + + if( count( $hashParts ) == 2 ) { + + $salt = $hashParts[0]; + $hash = $hashParts[1]; + + foreach( $accessPasswords as $truePassword ) { + $trueHash = md5( $salt . $truePassword ); + + if( $trueHash == $hash ) { + $password = $truePassword; + $badCookie = false; + } + } + + } + } + else { + // no request variable, no cookie + // cookie probably expired + $badCookie = true; + $password_hash = "(no cookie. expired?)"; + } + + + + if( ! in_array( $password, $accessPasswords ) ) { + + if( ! $badCookie ) { + + echo "Incorrect password."; + + fs_log( "Failed $inFunctionName access with password: ". + "$password" ); + } + else { + echo "Session expired."; + + fs_log( "Failed $inFunctionName access with bad cookie: ". + "$password_hash" ); + } + + die(); + } + else { + + if( $passwordSent && $enableYubikey ) { + global $yubikeyIDs, $yubicoClientID, $yubicoSecretKey, + $passwordHashingPepper; + + $yubikey = $_REQUEST[ "yubikey" ]; + + $index = array_search( $password, $accessPasswords ); + $yubikeyIDList = preg_split( "/:/", $yubikeyIDs[ $index ] ); + + $providedID = substr( $yubikey, 0, 12 ); + + if( ! in_array( $providedID, $yubikeyIDList ) ) { + echo "Provided Yubikey does not match ID for this password."; + die(); + } + + + $nonce = fs_hmac_sha1( $passwordHashingPepper, uniqid() ); + + $callURL = + "https://api2.yubico.com/wsapi/2.0/verify?id=$yubicoClientID". + "&otp=$yubikey&nonce=$nonce"; + + $result = trim( file_get_contents( $callURL ) ); + + $resultLines = preg_split( "/\s+/", $result ); + + sort( $resultLines ); + + $resultPairs = array(); + + $messageToSignParts = array(); + + foreach( $resultLines as $line ) { + // careful here, because = is used in base-64 encoding + // replace first = in a line (the key/value separator) + // with # + + $lineToParse = preg_replace( '/=/', '#', $line, 1 ); + + // now split on # instead of = + $parts = preg_split( "/#/", $lineToParse ); + + $resultPairs[$parts[0]] = $parts[1]; + + if( $parts[0] != "h" ) { + // include all but signature in message to sign + $messageToSignParts[] = $line; + } + } + $messageToSign = implode( "&", $messageToSignParts ); + + $trueSig = + base64_encode( + hash_hmac( 'sha1', + $messageToSign, + // need to pass in raw key + base64_decode( $yubicoSecretKey ), + true) ); + + if( $trueSig != $resultPairs["h"] ) { + echo "Yubikey authentication failed.
"; + echo "Bad signature from authentication server
"; + die(); + } + + $status = $resultPairs["status"]; + if( $status != "OK" ) { + echo "Yubikey authentication failed: $status"; + die(); + } + + } + + // set cookie again, renewing it, expires in 24 hours + $expireTime = time() + 60 * 60 * 24; + + setcookie( $cookieName, $password_hash, $expireTime, "/" ); + } + } + + + + +function fs_clearPasswordCookie() { + global $tableNamePrefix; + + $cookieName = $tableNamePrefix . "cookie_password_hash"; + + // expire 24 hours ago (to avoid timezone issues) + $expireTime = time() - 60 * 60 * 24; + + setcookie( $cookieName, "", $expireTime, "/" ); + } + + + + + + + + + +function fs_hmac_sha1( $inKey, $inData ) { + return hash_hmac( "sha1", + $inData, $inKey ); + } + + +function fs_hmac_sha1_raw( $inKey, $inData ) { + return hash_hmac( "sha1", + $inData, $inKey, true ); + } + + + + + + +// decodes a ASCII hex string into an array of 0s and 1s +function fs_hexDecodeToBitString( $inHexString ) { + $digits = str_split( $inHexString ); + + $bitString = ""; + + foreach( $digits as $digit ) { + $index = hexdec( $digit ); + + $binDigitString = decbin( $index ); + + // pad with 0s + $binDigitString = + substr( "0000", 0, 4 - strlen( $binDigitString ) ) . + $binDigitString; + + $bitString = $bitString . $binDigitString; + } + + return $bitString; + } + + + + +?> diff --git a/fitnessServer/settings.php b/fitnessServer/settings.php new file mode 100644 index 000000000..55e13f735 --- /dev/null +++ b/fitnessServer/settings.php @@ -0,0 +1,132 @@ + \ No newline at end of file diff --git a/fitnessServer/sha1.js b/fitnessServer/sha1.js new file mode 100644 index 000000000..491981c0f --- /dev/null +++ b/fitnessServer/sha1.js @@ -0,0 +1,25 @@ +/* + A JavaScript implementation of the SHA family of hashes, as + defined in FIPS PUB 180-4 and FIPS PUB 202, as well as the corresponding + HMAC implementation as defined in FIPS PUB 198a + + Copyright Brian Turek 2008-2016 + Distributed under the BSD License + See http://caligatio.github.com/jsSHA/ for more information + + Several functions taken from Paul Johnston +*/ +'use strict';(function(G){function t(e,a,d){var g=0,c=[],b=0,f,k,l,h,m,w,n,y,p=!1,q=[],t=[],v,u=!1;d=d||{};f=d.encoding||"UTF8";v=d.numRounds||1;l=z(a,f);if(v!==parseInt(v,10)||1>v)throw Error("numRounds must a integer >= 1");if("SHA-1"===e)m=512,w=A,n=H,h=160,y=function(a){return a.slice()};else throw Error("Chosen SHA variant is not supported");k=x(e);this.setHMACKey=function(a,b,c){var d;if(!0===p)throw Error("HMAC key already set");if(!0===u)throw Error("Cannot set HMAC key after calling update"); +f=(c||{}).encoding||"UTF8";b=z(b,f)(a);a=b.binLen;b=b.value;d=m>>>3;c=d/4-1;if(da/8){for(;b.length<=c;)b.push(0);b[c]&=4294967040}for(a=0;a<=c;a+=1)q[a]=b[a]^909522486,t[a]=b[a]^1549556828;k=w(q,k);g=m;p=!0};this.update=function(a){var d,e,f,h=0,n=m>>>5;d=l(a,c,b);a=d.binLen;e=d.value;d=a>>>5;for(f=0;f>>5);b=a%m;u=!0};this.getHash=function(a,d){var f,l,m,r;if(!0=== +p)throw Error("Cannot call getHash after setting HMAC key");m=B(d);switch(a){case "HEX":f=function(a){return C(a,h,m)};break;case "B64":f=function(a){return D(a,h,m)};break;case "BYTES":f=function(a){return E(a,h)};break;case "ARRAYBUFFER":try{l=new ArrayBuffer(0)}catch(I){throw Error("ARRAYBUFFER not supported by this environment");}f=function(a){return F(a,h)};break;default:throw Error("format must be HEX, B64, BYTES, or ARRAYBUFFER");}r=n(c.slice(),b,g,y(k),h);for(l=1;l>>3;if(0!==g%2)throw Error("String of HEX type must be in byte increments");for(c=0;c>>1)+l;for(f=k>>>2;a.length<=f;)a.push(0);a[f]|=b<<8*(3-k%4)}return{value:a,binLen:4*g+d}}function K(e,a,d){var g=[],c,b,f,k,g=a||[0];d=d||0;b=d>>>3;for(c=0;c>>2,g.length<=f&&g.push(0),g[f]|=a<<8*(3-k%4);return{value:g,binLen:8*e.length+d}}function L(e,a,d){var g=[],c=0,b,f,k,l,h,m,g=a||[0];d=d||0;a=d>>>3;if(-1===e.search(/^[a-zA-Z0-9=+\/]+$/))throw Error("Invalid character in base-64 string");f=e.indexOf("=");e=e.replace(/\=/g,"");if(-1!==f&&f *lines = tokenizeString( readyResult ); + + // skip already-parsed header and OK at end + for( int i=3; i< lines->size()-1; i++ ) { + + char *line = lines->getElementDirect( i ); + + // Eve_Jones,You,353,4495,14.7841,1.32155,2.6678 + + int numParts; + char **parts = split( line, ",", &numParts ); + + if( numParts == 7 ) { + + OffspringRecord r; + char *nameWorking = stringToUpperCase( parts[0] ); + char *relationWorking = stringToUpperCase( parts[1] ); + + char found; + r.name = replaceAll( nameWorking, "_", " ", &found ); + delete [] nameWorking; + + shortenLongString( r.name ); + + r.relationName = + replaceAll( relationWorking, "_", " ", &found ); + delete [] relationWorking; + + shortenLongString( r.relationName ); + + + sscanf( parts[2], "%d", &( r.displayID ) ); + sscanf( parts[3], "%d", &( r.diedSecAgo ) ); + sscanf( parts[4], "%lf", &( r.age ) ); + sscanf( parts[5], "%lf", &( r.oldScore ) ); + sscanf( parts[6], "%lf", &( r.newScore ) ); + recentOffspring.push_back( r ); + } + + for( int j=0; jdeallocateStringElements(); + delete lines; + } + } + + } + + + + +const char *getRankSuffix() { + + const char *rankSuffix = "TH"; + + switch( rank % 10 ) { + case 1: + rankSuffix = "ST"; + break; + case 2: + rankSuffix = "ND"; + break; + case 3: + rankSuffix = "RD"; + break; + default: + rankSuffix = "TH"; + break; + } + + return rankSuffix; + } + + + +// These draw nothing if latest data (after last trigger) not ready yet + +void drawFitnessScore( doublePair inPos, char inMoreDigits ) { + if( !useFitnessServer ) { + return; + } + + if( score != -1 ) { + + const char *rankSuffix = getRankSuffix(); + + char *scoreString; + + if( inMoreDigits ) { + scoreString = autoSprintf( "%0.2f", score ); + } + else { + scoreString = autoSprintf( "%0.1f", score ); + } + + char *message; + + if( rank != 0 ) { + message = + autoSprintf( translate( "scoreMessage" ), + scoreString, rank, rankSuffix ); + } + else { + // no rank + message = + autoSprintf( translate( "scoreMessageNoRank" ), + scoreString ); + } + + delete [] scoreString; + + drawMessage( message, inPos ); + + delete [] message; + } + else { + stepActiveRequest(); + } + } + + + + +static void drawFadeRect( doublePair inBottomLeft, doublePair inTopRight, + FloatColor inBottomColor, FloatColor inTopColor ) { + + double vert[8] = + { inBottomLeft.x, inBottomLeft.y, + inBottomLeft.x, inTopRight.y, + inTopRight.x, inTopRight.y, + inTopRight.x, inBottomLeft.y }; + + float vertColor[16] = + { inBottomColor.r, inBottomColor.g, inBottomColor.b, inBottomColor.a, + inTopColor.r, inTopColor.g, inTopColor.b, inTopColor.a, + inTopColor.r, inTopColor.g, inTopColor.b, inTopColor.a, + inBottomColor.r, inBottomColor.g, inBottomColor.b, inBottomColor.a }; + + drawQuads( 1, vert, vertColor ); + } + + + + + +static void drawFace( doublePair inPos, int inDisplayID, double inAge, + int inDrawIndex ) { + + ObjectRecord *faceO = getObject( inDisplayID ); + + startAddingToStencil( true, true ); + + setDrawColor( 1, 1, 1, 1 ); + drawSquare( inPos, 28 ); + + startDrawingThroughStencil( false ); + + + // use grass background for all + if( 0 < groundSpritesArraySize && + groundSprites[0] != NULL ) { + + if( inDrawIndex > groundSprites[0]->numTilesHigh - 1 ) { + inDrawIndex = inDrawIndex % groundSprites[2]->numTilesHigh; + } + + SpriteHandle h = groundSprites[0]->tiles[inDrawIndex][0]; + + drawSprite( h, inPos ); + } + + + int headIndex = getHeadIndex( faceO, inAge ); + + doublePair headPos = faceO->spritePos[ headIndex ]; + + + int frontFootIndex = getFrontFootIndex( faceO, inAge ); + + doublePair frontFootPos = + faceO->spritePos[ frontFootIndex ]; + + + int bodyIndex = getBodyIndex( faceO, inAge ); + + doublePair bodyPos = faceO->spritePos[ bodyIndex ]; + + + doublePair framePos = + add( add( faceO->spritePos[ headIndex ], + getAgeHeadOffset( inAge, headPos, + bodyPos, + frontFootPos ) ), + getAgeBodyOffset( inAge, bodyPos ) ); + + // move face down + inPos = sub( inPos, framePos ); + + inPos.y -= 16; + + drawObject( faceO, 2, + inPos, + 0, false, false, + inAge, + 0, + false, + false, + getEmptyClothingSet() ); + + stopStencil(); + } + + + +void drawFitnessScoreDetails( doublePair inPos, int inSkip ) { + if( !useFitnessServer ) { + return; + } + + + + if( score != -1 ) { + doublePair namePos = inPos; + namePos.x -= 140; + namePos.y += 8; + + + if( rank > 0 ) { + // hide leaderboard message if unranked + + const char *rankSuffix = getRankSuffix(); + + char *leaderboardString = + autoSprintf( translate( "leaderboardMessage" ), + rank, rankSuffix, leaderboardName ); + + drawMessage( leaderboardString, namePos ); + delete [] leaderboardString; + } + + + //drawFitnessScore( scorePos, true ); + + + inPos.y -= 85; + + doublePair titlePos = inPos; + + titlePos.x -= 480; + + mainFont->drawString( + translate( "geneticHistoryTitle" ), titlePos, alignLeft ); + + titlePos = inPos; + + // 7 extra, to line up with fixed with number column + titlePos.x += 480 + 7; + + + mainFont->drawString( + translate( "fitnessTitle" ), titlePos, alignRight ); + + + inPos.y -= 65; + + doublePair startPos = inPos; + + FloatColor bgColor = { 0.2, 0.2, 0.2, 1.0 }; + FloatColor bgColorAlt = { 0.1, 0.1, 0.1, 1.0 }; + + if( inSkip % 2 == 1 ) { + FloatColor temp = bgColor; + bgColor = bgColorAlt; + bgColorAlt = temp; + } + + int indLimit = recentOffspring.size(); + + if( indLimit - inSkip > 8 ) { + indLimit = inSkip + 8; + } + + for( int i=inSkip; idrawString( r.name, pos, alignLeft ); + + pos.y -= 32; + + mainFont->drawString( r.relationName, pos, alignLeft ); + + pos.x = inPos.x; + + int yearsAgo = r.diedSecAgo / 60; + + char *diedAgoString; + + if( yearsAgo == 1 ) { + diedAgoString = autoSprintf( "%s %d %s", + translate( "died" ), + yearsAgo, + translate( "yearAgo" ) ); + } + else if( yearsAgo < 1000 ) { + diedAgoString = autoSprintf( "%s %d %s", + translate( "died" ), + yearsAgo, + translate( "yearsAgo" ) ); + } + else if( yearsAgo < 10000 ) { + diedAgoString = autoSprintf( "%s %d %s", + translate( "died" ), + yearsAgo / 100, + translate( "centuriesAgo" ) ); + } + else { + diedAgoString = autoSprintf( "%s %d %s", + translate( "died" ), + yearsAgo / 1000, + translate( "millenniaAgo" ) ); + } + + + + mainFont->drawString( diedAgoString, pos, alignLeft ); + delete [] diedAgoString; + + pos.y += 32; + + char *ageString = autoSprintf( "%s %0.1f", + translate( "age" ), + r.age ); + + mainFont->drawString( ageString, pos, alignLeft ); + + delete [] ageString; + + pos.x = inPos.x + 350; + + double scoreDelt = r.newScore - r.oldScore; + + char *deltString; + + if( scoreDelt > 0 ) { + setDrawColor( 0.0, 1.0, 0.0, 1.0 ); + + deltString = autoSprintf( "+%0.2f", scoreDelt ); + } + else { + setDrawColor( 1.0, 0.0, 0.0, 1.0 ); + + deltString = autoSprintf( "%0.2f", scoreDelt ); + } + + numbersFontFixed->drawString( deltString, pos, alignRight ); + + delete [] deltString; + + pos.x = inPos.x + 480; + + setDrawColor( 1, 1, 1, 1 ); + + char *scoreString = autoSprintf( "%0.2f", r.newScore ); + + numbersFontFixed->drawString( scoreString, pos, alignRight ); + + + delete [] scoreString; + + + //swap + FloatColor temp = bgColor; + bgColor = bgColorAlt; + bgColorAlt = temp; + + inPos.y -= 80; + } + + + if( recentOffspring.size() - inSkip > 6 ) { + // off bottom + + // instant fade-in + bottomShadingFade = 1; + } + else { + if( bottomShadingFade > 0 ) { + bottomShadingFade -= 0.1; + if( bottomShadingFade < 0 ) { + bottomShadingFade = 0; + } + } + } + + + if( bottomShadingFade ) { + + doublePair bottom = { startPos.x - 560, startPos.y - 510 }; + doublePair top = { startPos.x + 500, startPos.y - 400 }; + + FloatColor bottomColor = { 0, 0, 0, bottomShadingFade }; + FloatColor topColor = { 0, 0, 0, 0 }; + + drawFadeRect( bottom, top, + bottomColor, topColor ); + } + + + if( inSkip > 0 ) { + // off top too + if( topShadingFade < 1 ) { + topShadingFade += 0.1; + if( topShadingFade > 1 ) { + topShadingFade = 1; + } + } + } + else { + if( topShadingFade > 0 ) { + topShadingFade -= 0.1; + if( topShadingFade < 0 ) { + topShadingFade = 0; + } + } + } + + if( topShadingFade ) { + + doublePair bottom = { startPos.x - 560, startPos.y - 70 }; + doublePair top = { startPos.x + 500, startPos.y + 40 }; + + FloatColor bottomColor = { 0, 0, 0, 0 }; + FloatColor topColor = { 0, 0, 0, topShadingFade }; + + drawFadeRect( bottom, top, + bottomColor, topColor ); + } + + + } + else { + stepActiveRequest(); + } + } + + + + +int getMaxFitnessListSkip() { + return recentOffspring.size() - 6; + } + + + +char canFitnessScroll() { + if( recentOffspring.size() > 6 ) { + return true; + } + return false; + } + + + + +char isFitnessScoreReady() { + if( score != -1 ) { + return true; + } + return false; + } + + diff --git a/gameSource/fitnessScore.h b/gameSource/fitnessScore.h new file mode 100644 index 000000000..076097c3b --- /dev/null +++ b/gameSource/fitnessScore.h @@ -0,0 +1,33 @@ +#include "minorGems/game/doublePair.h" + + +void initFitnessScore(); + +void freeFitnessScore(); + + +void triggerFitnessScoreUpdate(); + +void triggerFitnessScoreDetailsUpdate(); + + +// false if either have been triggered and result not ready yet +char isFitnessScoreReady(); + + +// These draw nothing if latest data (after last trigger) not ready yet + +void drawFitnessScore( doublePair inPos, char inMoreDigits = false ); + + +// inSkip controls paging through list +void drawFitnessScoreDetails( doublePair inPos, int inSkip ); + +int getMaxFitnessListSkip(); + +char canFitnessScroll(); + + + +// returns true if using +char usingFitnessServer(); diff --git a/gameSource/game.cpp b/gameSource/game.cpp index c669cdb6a..c15f24cde 100644 --- a/gameSource/game.cpp +++ b/gameSource/game.cpp @@ -1,4 +1,4 @@ -int versionNumber = 243; +int versionNumber = 245; int dataVersionNumber = 0; int binVersionNumber = versionNumber; @@ -76,6 +76,7 @@ CustomRandomSource randSource( 34957197 ); #include "emotion.h" #include "photos.h" #include "lifeTokens.h" +#include "fitnessScore.h" #include "FinalMessagePage.h" @@ -89,6 +90,7 @@ CustomRandomSource randSource( 34957197 ); #include "ReviewPage.h" #include "TwinPage.h" #include "PollPage.h" +#include "GeneticHistoryPage.h" //#include "TestPage.h" #include "ServerActionPage.h" @@ -144,6 +146,7 @@ SettingsPage *settingsPage; ReviewPage *reviewPage; TwinPage *twinPage; PollPage *pollPage; +GeneticHistoryPage *geneticHistoryPage; //TestPage *testPage = NULL; @@ -650,7 +653,9 @@ void initFrameDrawer( int inWidth, int inHeight, int inTargetFrameRate, pollPage = new PollPage( reviewURL ); delete [] reviewURL; - + + geneticHistoryPage = new GeneticHistoryPage(); + // 0 music headroom needed, because we fade sounds before playing music setVolumeScaling( 10, 0 ); @@ -738,6 +743,7 @@ void freeFrameDrawer() { delete reviewPage; delete twinPage; delete pollPage; + delete geneticHistoryPage; //if( testPage != NULL ) { // delete testPage; @@ -764,7 +770,7 @@ void freeFrameDrawer() { freePhotos(); freeLifeTokens(); - + freeFitnessScore(); if( reflectorURL != NULL ) { delete [] reflectorURL; @@ -1620,7 +1626,8 @@ void drawFrame( char inUpdate ) { initPhotos(); initLifeTokens(); - + initFitnessScore(); + initMusicPlayer(); setMusicLoudness( musicLoudness ); @@ -1686,6 +1693,10 @@ void drawFrame( char inUpdate ) { currentGamePage = pollPage; currentGamePage->base_makeActive( true ); } + else if( existingAccountPage->checkSignal( "genes" ) ) { + currentGamePage = geneticHistoryPage; + currentGamePage->base_makeActive( true ); + } else if( existingAccountPage->checkSignal( "settings" ) ) { currentGamePage = settingsPage; currentGamePage->base_makeActive( true ); @@ -2052,6 +2063,17 @@ void drawFrame( char inUpdate ) { currentGamePage->base_makeActive( true ); } } + else if( currentGamePage == geneticHistoryPage ) { + if( geneticHistoryPage->checkSignal( "done" ) ) { + if( !isHardToQuitMode() ) { + currentGamePage = existingAccountPage; + } + else { + currentGamePage = rebirthChoicePage; + } + currentGamePage->base_makeActive( true ); + } + } else if( currentGamePage == rebirthChoicePage ) { if( rebirthChoicePage->checkSignal( "reborn" ) ) { // get server address again from scratch, in case @@ -2073,6 +2095,10 @@ void drawFrame( char inUpdate ) { currentGamePage = existingAccountPage; currentGamePage->base_makeActive( true ); } + else if( rebirthChoicePage->checkSignal( "genes" ) ) { + currentGamePage = geneticHistoryPage; + currentGamePage->base_makeActive( true ); + } else if( rebirthChoicePage->checkSignal( "quit" ) ) { quitGame(); } diff --git a/gameSource/languages/English.txt b/gameSource/languages/English.txt index 51eb9fc14..1fb1271fa 100644 --- a/gameSource/languages/English.txt +++ b/gameSource/languages/English.txt @@ -44,6 +44,8 @@ atSignTip "INSERT '@' SIGN (INTL KEYBOARDS)" loginButton "LOGIN" friendsButton "FRIENDS" familyTrees "FAMILY TREES" +genesButton "GENES" +geneticHistoryButton "GENETIC HISTORY" clearAccount "CLEAR ACCOUNT" settingsButton "SETTINGS" @@ -192,6 +194,29 @@ minuteSingular "MINUTE" tokenTimeMessage "NEW LIFE IN: " +scoreMessage "GENETIC FITNESS:##%s (%d%s PLACE)" + +scoreMessageNoRank "GENETIC FITNESS:##%s" + + + +refreshButton "REFRESH" + +age "AGE" + +leaderboardMessage "LEADERBOARD NAME (%d%s PLACE):##%s" + +leaderboard "LEADERBOARD" + +scrollTip "SCROLL##WITH##ARROW##KEYS" + +geneticHistoryTitle "YOUR RECENT GENETIC HISTORY:" + +fitnessTitle "FITNESS" + + + + customServerMesssage "CUSTOM SERVER: %s : %d" connecting "CONNECTING" @@ -328,6 +353,11 @@ unknownPerson "UNKNOWN PERSON" yearAgo "YEAR AGO" yearsAgo "YEARS AGO" +centuriesAgo "CENTURIES AGO" + +millenniaAgo "MILLENNIA AGO" + + died "DIED" zero "ZERO" diff --git a/gameSource/makeFileList b/gameSource/makeFileList index d3998cabd..3eda68bbb 100644 --- a/gameSource/makeFileList +++ b/gameSource/makeFileList @@ -71,6 +71,9 @@ PickableStatics.cpp \ photos.cpp \ lifeTokens.cpp \ PollPage.cpp \ +fitnessScore.cpp \ +GeneticHistoryPage.cpp \ + GAME_GRAPHICS = \ diff --git a/gameSource/settings/fitnessServerURL.ini b/gameSource/settings/fitnessServerURL.ini new file mode 100644 index 000000000..beb6de451 --- /dev/null +++ b/gameSource/settings/fitnessServerURL.ini @@ -0,0 +1 @@ +http://onehouronelife.com/fitnessServer/server.php \ No newline at end of file diff --git a/gameSource/settings/useFitnessServer.ini b/gameSource/settings/useFitnessServer.ini new file mode 100644 index 000000000..56a6051ca --- /dev/null +++ b/gameSource/settings/useFitnessServer.ini @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/lineageServer/server.php b/lineageServer/server.php index cdc246613..31480be8f 100644 --- a/lineageServer/server.php +++ b/lineageServer/server.php @@ -1544,12 +1544,15 @@ function ls_frontPage() { $string_to_hash = ls_requestFilter( "string_to_hash", "/[A-Z0-9]+/i", "0" ); + + $encodedEmail = urlencode( $emailFilter ); + $correct = false; global $ticketServerURL; $url = "$ticketServerURL". "?action=check_ticket_hash". - "&email=$emailFilter". + "&email=$encodedEmail". "&hash_value=$ticket_hash". "&string_to_hash=$string_to_hash"; diff --git a/server/fitnessScore.cpp b/server/fitnessScore.cpp new file mode 100644 index 000000000..3abaea529 --- /dev/null +++ b/server/fitnessScore.cpp @@ -0,0 +1,400 @@ +#include "fitnessScore.h" + + +#include "minorGems/util/SettingsManager.h" + +#include "minorGems/network/web/WebRequest.h" +#include "minorGems/network/web/URLUtils.h" + +#include "minorGems/crypto/hashes/sha1.h" + + +typedef struct FitnessFitnessOpRequest { + char *email; + // separated by &, should end with & too + const char *extraParams; + char isScoreRequest; + float scoreResult; + WebRequest *seqW; + WebRequest *mainW; + } FitnessOpRequest; + +static SimpleVector scoreRequests; + +static SimpleVector deathRequests; + +static char *serverID = NULL; + + + +void initFitnessScore() { + serverID = SettingsManager::getStringSetting( "serverID", "testServer" ); + } + + + +static void freeRequest( FitnessOpRequest *inR ) { + + if( inR->extraParams != NULL ) { + delete [] inR->extraParams; + inR->extraParams = NULL; + } + + if( inR->email != NULL ) { + delete [] inR->email; + inR->email = NULL; + } + if( inR->seqW != NULL ) { + delete inR->seqW; + inR->seqW = NULL; + } + if( inR->mainW != NULL ) { + delete inR->mainW; + inR->mainW = NULL; + } + + } + + + +void freeFitnessScore() { + for( int i=0; imainW != NULL ) { + + int result = r->mainW->step(); + + if( result == 0 ) { + return 0; + } + else if( result == -1 ) { + // error + // maybe server is down + // let player through anyway + + freeRequest( r ); + return 1; + } + else if( result == 1 ) { + // got result + // make sure op is permitted + + int returnVal = 1; + + char *text = r->mainW->getResult(); + + if( strstr( text, "DENIED" ) != NULL ) { + returnVal = -1; + } + else if( r->isScoreRequest ) { + sscanf( text, "%f", &( r->scoreResult ) ); + } + delete [] text; + + freeRequest( r ); + return returnVal; + } + + return 0; + } + else if( r->seqW != NULL ) { + // still waiting for result from sequence request + + int result = r->seqW->step(); + + if( result == 0 ) { + return 0; + } + else if( result == -1 ) { + // error + // maybe server is down + // let player through anyway + + freeRequest( r ); + + return 1; + } + else if( result == 1 ) { + // got seq result + + int sequenceNumber = 0; + + char *text = r->seqW->getResult(); + + sscanf( text, "%d\nOK", &sequenceNumber ); + + delete [] text; + + + char *sharedSecret = + SettingsManager::getStringSetting( + "fitnessServerSharedSecret", + "secret_phrase" ); + + char *seqString = autoSprintf( "%d", sequenceNumber ); + + char *hash = hmac_sha1( sharedSecret, seqString ); + + delete [] seqString; + + delete [] sharedSecret; + + char *encodedEmail = URLUtils::urlEncode( r->email ); + + char *serverURL = + SettingsManager::getStringSetting( + "fitnessServerURL", "" ); + + + char *url = autoSprintf( + "%s?%s" + "&email=%s" + "&server_name=%s" + "&sequence_number=%d" + "&hash_value=%s", + serverURL, + r->extraParams, + encodedEmail, + serverID, + sequenceNumber, + hash ); + + delete [] encodedEmail; + delete [] hash; + delete [] serverURL; + + r->mainW = new WebRequest( "GET", url, NULL ); + printf( "Starting new web request for %s\n", url ); + + delete [] url; + } + + return 0; + } + else { + // need to send seq request + char *serverURL = + SettingsManager::getStringSetting( + "fitnessServerURL", "" ); + + char *encodedName = URLUtils::urlEncode( serverID ); + + char *url = autoSprintf( + "%s?action=get_server_sequence_number" + "&server_name=%s", + serverURL, + encodedName ); + + delete [] encodedName; + delete [] serverURL; + + r->seqW = new WebRequest( "GET", url, NULL ); + printf( "Starting new web request for %s\n", url ); + + delete [] url; + + return 0; + } + } + + + + +void stepFitnessScore() { + // only need to step death requests here, because no one is checking + // them + + // score requests are checked over and over (and thus stepped) + // with repeated calls to getFitnessScore + + + for( int i=0; iemail, inEmail ) == 0 ) { + // match + + int result = stepOpRequest( r ); + + if( result == 1 ) { + *outScore = r->scoreResult; + } + + + if( result != 0 ) { + // done, deleted already + scoreRequests.deleteElement( i ); + } + return result; + } + } + + + + // didn't find a match in existing requests + + if( SettingsManager::getIntSetting( "useFitnessServer", 0 ) == 0 || + SettingsManager::getIntSetting( "remoteReport", 0 ) == 0 ) { + // everyone has fitness 0 (default + *outScore = 0; + return 1; + } + + + // else using server, start a new spend request + + FitnessOpRequest r; + + r.email = stringDuplicate( inEmail ); + r.extraParams = stringDuplicate( "action=get_score&" ); + r.isScoreRequest = true; + + r.seqW = NULL; + r.mainW = NULL; + + scoreRequests.push_back( r ); + + return 0; + } + + + +void logFitnessDeath( int inNumLivePlayers, + char *inEmail, char *inName, int inDisplayID, + double inAge, + SimpleVector *inAncestorEmails, + SimpleVector *inAncestorRelNames ) { + + if( SettingsManager::getIntSetting( "useFitnessServer", 0 ) == 0 || + SettingsManager::getIntSetting( "remoteReport", 0 ) == 0 ) { + // not using server + return; + } + + if( inNumLivePlayers < + SettingsManager::getIntSetting( "minActivePlayersForFitness", 15 ) ) { + // not enough players for this to count + return; + } + + + + + // else using server, start a new death request + + FitnessOpRequest r; + + r.email = stringDuplicate( inEmail ); + + + if( inName == NULL ) { + inName = (char*)"NAMELESS"; + } + + char *encodedName = URLUtils::urlEncode( inName ); + + + SimpleVector workingList; + + int num = inAncestorEmails->size(); + + for( int i=0; igetElementDirect( i ) ); + + workingList.appendElementString( " " ); + + char *relName = inAncestorRelNames->getElementDirect( i ); + + char found; + char *relNameNoSpace = + replaceAll( relName, " ", "_", &found ); + + workingList.appendElementString( relNameNoSpace ); + + if( i < num-1 ) { + workingList.appendElementString( "," ); + } + + delete [] relNameNoSpace; + } + + + char *ancestorList = workingList.getElementString(); + + char *encodedList = URLUtils::urlEncode( ancestorList ); + + delete [] ancestorList; + + + r.extraParams = + autoSprintf( + "action=report_death&" + "name=%s&" + "display_id=%d&" + "self_rel_name=You&" + "age=%f&" + "ancestor_list=%s", + encodedName, + inDisplayID, + inAge, + encodedList ); + + delete [] encodedName; + delete [] encodedList; + + r.isScoreRequest = false; + + r.seqW = NULL; + r.mainW = NULL; + + deathRequests.push_back( r ); + } + diff --git a/server/fitnessScore.h b/server/fitnessScore.h new file mode 100644 index 000000000..1584302b6 --- /dev/null +++ b/server/fitnessScore.h @@ -0,0 +1,24 @@ +#include "minorGems/util/SimpleVector.h" + + +void initFitnessScore(); + + +void freeFitnessScore(); + +void stepFitnessScore(); + + +// return value: +// 0 still pending +// -1 DENIED +// 1 score ready (and returned in outScore) +// outScore NOT modified if result not ready +int getFitnessScore( char *inEmail, float *outScore ); + +// all string params copied internally +void logFitnessDeath( int inNumLivePlayers, + char *inEmail, char *inName, int inDisplayID, + double inAge, + SimpleVector *inAncestorEmails, + SimpleVector *inAncestorRelNames ); diff --git a/server/lifeTokens.cpp b/server/lifeTokens.cpp index 76a8a7d27..5ccee4025 100644 --- a/server/lifeTokens.cpp +++ b/server/lifeTokens.cpp @@ -253,7 +253,8 @@ int spendLifeToken( char *inEmail ) { // didn't find a match in existing requests - if( SettingsManager::getIntSetting( "useLifeTokenServer", 0 ) == 0 ) { + if( SettingsManager::getIntSetting( "useLifeTokenServer", 0 ) == 0 || + SettingsManager::getIntSetting( "remoteReport", 0 ) == 0 ) { // not using server, allow all return 1; } @@ -277,7 +278,8 @@ int spendLifeToken( char *inEmail ) { void refundLifeToken( char *inEmail ) { - if( SettingsManager::getIntSetting( "useLifeTokenServer", 0 ) == 0 ) { + if( SettingsManager::getIntSetting( "useLifeTokenServer", 0 ) == 0 || + SettingsManager::getIntSetting( "remoteReport", 0 ) == 0 ) { // not using server return; } diff --git a/server/makeFileList b/server/makeFileList index bb7ac517e..e8fa27ef5 100644 --- a/server/makeFileList +++ b/server/makeFileList @@ -55,6 +55,7 @@ objectSurvey.cpp \ language.cpp \ familySkipList.cpp \ lifeTokens.cpp \ +fitnessScore.cpp \ diff --git a/server/server.cpp b/server/server.cpp index 9a816fd5a..6f5d85d36 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -52,6 +52,8 @@ #include "language.h" #include "familySkipList.h" #include "lifeTokens.h" +#include "fitnessScore.h" + #include "minorGems/util/random/JenkinsRandomSource.h" @@ -223,6 +225,10 @@ typedef struct LiveObject { int id; + // -1 if unknown + float fitnessScore; + + // object ID used to visually represent this player int displayID; @@ -259,6 +265,10 @@ typedef struct LiveObject { SimpleVector *lineage; + SimpleVector *ancestorEmails; + SimpleVector *ancestorRelNames; + + // id of Eve that started this line int lineageEveID; @@ -1274,6 +1284,13 @@ void quitCleanup() { delete nextPlayer->lineage; + nextPlayer->ancestorEmails->deallocateStringElements(); + delete nextPlayer->ancestorEmails; + + nextPlayer->ancestorRelNames->deallocateStringElements(); + delete nextPlayer->ancestorRelNames; + + if( nextPlayer->name != NULL ) { delete [] nextPlayer->name; } @@ -1333,6 +1350,8 @@ void quitCleanup() { freeLifeTokens(); + freeFitnessScore(); + freeLifeLog(); freeFoodLog(); @@ -2154,6 +2173,8 @@ double computeAge( LiveObject *inPlayer ) { setDeathReason( inPlayer, "age" ); inPlayer->error = true; + + age = forceDeathAge; } return age; } @@ -2227,7 +2248,22 @@ int computeFoodCapacity( LiveObject *inPlayer ) { cap = 4; } - returnVal = cap; + int lostBars = 20 - cap; + + if( lostBars > 0 && inPlayer->fitnessScore > 0 ) { + + // consider effect of fitness on reducing lost bars + + // for now, let's make it quadratic + double maxLostBars = + 16 - 16 * pow( inPlayer->fitnessScore / 60.0, 2 ); + + if( lostBars > maxLostBars ) { + lostBars = maxLostBars; + } + } + + returnVal = 20 - lostBars; } return ceil( returnVal * inPlayer->foodCapModifier ); @@ -4972,6 +5008,18 @@ int processLoggedInPlayer( char inAllowReconnect, newObject.id = nextID; nextID++; + + newObject.fitnessScore = -1; + + int fitResult = getFitnessScore( inEmail, &newObject.fitnessScore ); + + if( fitResult == -1 ) { + // failed right away + // stop asking now + newObject.fitnessScore = 0; + } + + SettingsManager::setSetting( "nextPlayerID", (int)nextID ); @@ -5925,6 +5973,111 @@ int processLoggedInPlayer( char inAllowReconnect, newObject.heldOriginY = newObject.yd; newObject.actionTarget = newObject.birthPos; + + + + newObject.ancestorEmails = new SimpleVector(); + newObject.ancestorRelNames = new SimpleVector(); + + for( int j=0; jerror ) { + continue; + } + + // a living other player + + if( ! getFemale( otherPlayer ) ) { + + // check if his mother is an ancestor + // (then he's an uncle + if( otherPlayer->parentID > 0 ) { + + // look at lineage above parent + // don't count brothers, only uncles + for( int i=1; isize(); i++ ) { + + if( newObject.lineage->getElementDirect( i ) == + otherPlayer->parentID ) { + + newObject.ancestorEmails->push_back( + stringDuplicate( otherPlayer->email ) ); + + // i tells us how many greats + SimpleVector workingName; + + for( int g=2; g<=i; g++ ) { + workingName.appendElementString( "Great_" ); + } + if( ! getFemale( &newObject ) ) { + workingName.appendElementString( "Nephew" ); + } + else { + workingName.appendElementString( "Niece" ); + } + + newObject.ancestorRelNames->push_back( + workingName.getElementString() ); + + break; + } + } + } + } + else { + // females, look for direct ancestry + + for( int i=0; isize(); i++ ) { + + if( newObject.lineage->getElementDirect( i ) == + otherPlayer->id ) { + + newObject.ancestorEmails->push_back( + stringDuplicate( otherPlayer->email ) ); + + // i tells us how many greats and grands + SimpleVector workingName; + + for( int g=1; g<=i; g++ ) { + if( g == i ) { + workingName.appendElementString( "Grand" ); + } + else { + workingName.appendElementString( "Great_" ); + } + } + + + if( i != 0 ) { + if( ! getFemale( &newObject ) ) { + workingName.appendElementString( "son" ); + } + else { + workingName.appendElementString( "daughter" ); + } + } + else { + // no "Grand" + if( ! getFemale( &newObject ) ) { + workingName.appendElementString( "Son" ); + } + else { + workingName.appendElementString( "Daughter" ); + } + } + + + newObject.ancestorRelNames->push_back( + workingName.getElementString() ); + + break; + } + } + } + } + + @@ -8653,6 +8806,39 @@ void getLineageLineForPlayer( LiveObject *inPlayer, +void logFitnessDeath( LiveObject *nextPlayer ) { + + // log this death for fitness purposes, + // for both tutorial and non + + SimpleVector emptyAncestorEmails; + SimpleVector emptyAncestorRelNames; + + + SimpleVector *ancestorEmails = nextPlayer->ancestorEmails; + SimpleVector *ancestorRelNames = nextPlayer->ancestorRelNames; + + + if( nextPlayer->suicide ) { + // don't let this suicide death affect scores of any ancestors + ancestorEmails = &emptyAncestorEmails; + ancestorRelNames = &emptyAncestorRelNames; + } + + + + logFitnessDeath( players.size(), + nextPlayer->email, + nextPlayer->name, nextPlayer->displayID, + computeAge( nextPlayer ), + ancestorEmails, + ancestorRelNames ); + } + + + + + int main() { if( checkReadOnly() ) { @@ -8798,6 +8984,8 @@ int main() { initLifeTokens(); + initFitnessScore(); + initLifeLog(); //initBackup(); @@ -9057,7 +9245,8 @@ int main() { stepCurseServerRequests(); stepLifeTokens(); - + stepFitnessScore(); + stepMapLongTermCulling( players.size() ); } @@ -10090,6 +10279,21 @@ int main() { continue; } + + if( nextPlayer->fitnessScore == -1 ) { + // see if result ready yet + int fitResult = + getFitnessScore( nextPlayer->email, + &nextPlayer->fitnessScore ); + + if( fitResult == -1 ) { + // failed + // stop asking now + nextPlayer->fitnessScore = 0; + } + } + + double curCrossTime = Time::getCurrentTime(); char checkCrossing = true; @@ -14217,7 +14421,12 @@ int main() { nextPlayer->name, nextPlayer->lastSay, male ); + + + // both tutorial and non-tutorial players + logFitnessDeath( nextPlayer ); + // don't use age here, because it unfairly gives Eve // +14 years that she didn't actually live // use true played years instead @@ -17765,6 +17974,13 @@ int main() { delete nextPlayer->lineage; + nextPlayer->ancestorEmails->deallocateStringElements(); + delete nextPlayer->ancestorEmails; + + nextPlayer->ancestorRelNames->deallocateStringElements(); + delete nextPlayer->ancestorRelNames; + + if( nextPlayer->name != NULL ) { delete [] nextPlayer->name; } diff --git a/server/settings/fitnessServerSharedSecret.ini b/server/settings/fitnessServerSharedSecret.ini new file mode 100644 index 000000000..4d0c4e989 --- /dev/null +++ b/server/settings/fitnessServerSharedSecret.ini @@ -0,0 +1 @@ +secret_phrase \ No newline at end of file diff --git a/server/settings/fitnessServerURL.ini b/server/settings/fitnessServerURL.ini new file mode 100644 index 000000000..beb6de451 --- /dev/null +++ b/server/settings/fitnessServerURL.ini @@ -0,0 +1 @@ +http://onehouronelife.com/fitnessServer/server.php \ No newline at end of file diff --git a/server/settings/killEmotionIndex.ini b/server/settings/killEmotionIndex.ini index d8263ee98..19c7bdba7 100644 --- a/server/settings/killEmotionIndex.ini +++ b/server/settings/killEmotionIndex.ini @@ -1 +1 @@ -2 \ No newline at end of file +16 \ No newline at end of file diff --git a/server/settings/minActivePlayersForFitness.ini b/server/settings/minActivePlayersForFitness.ini new file mode 100644 index 000000000..3f10ffe7a --- /dev/null +++ b/server/settings/minActivePlayersForFitness.ini @@ -0,0 +1 @@ +15 \ No newline at end of file diff --git a/server/settings/useFitnessServer.ini b/server/settings/useFitnessServer.ini new file mode 100644 index 000000000..56a6051ca --- /dev/null +++ b/server/settings/useFitnessServer.ini @@ -0,0 +1 @@ +1 \ No newline at end of file