array( 'title' => t('Access achievements'), ), 'earn achievements' => array( 'title' => t('Earn achievements'), ), 'administer achievements' => array( 'title' => t('Administer achievements'), ), ); } /** * Implements hook_menu(). */ function achievements_menu() { $items['achievements/leaderboard'] = array( 'access arguments' => array('access achievements'), 'description' => 'View the site-wide achievements leaderboard.', 'file' => 'achievements.pages.inc', 'page callback' => 'achievements_leaderboard_totals', 'title' => 'Leaderboard', ); $items['achievements/leaderboard/%achievements'] = array( 'access arguments' => array('access achievements'), 'description' => "View a specific achievement's leaderboard.", 'file' => 'achievements.pages.inc', 'page callback' => 'achievements_leaderboard_for', 'page arguments' => array(2), 'title' => 'Per-achievement leaderboard', 'type' => MENU_CALLBACK, ); $items['user/%user/achievements'] = array( 'access arguments' => array('access achievements'), 'description' => "View a specific user's leaderboard.", 'file' => 'achievements.pages.inc', 'page callback' => 'achievements_user_page', 'page arguments' => array(1), 'title' => 'Achievements', 'type' => MENU_LOCAL_TASK, ); $items['admin/config/people/achievements'] = array( 'access arguments' => array('administer achievements'), 'description' => 'Configure the achievements system.', 'file' => 'achievements.admin.inc', 'page callback' => 'drupal_get_form', 'page arguments' => array('achievements_settings'), 'title' => 'Achievements', ); $items['achievements/autocomplete'] = array( 'access arguments' => array('access achievements'), 'file' => 'achievements.pages.inc', 'page callback' => 'achievements_autocomplete', 'title' => 'Achievement title autocomplete', 'type' => MENU_CALLBACK, ); return $items; } /** * Implements hook_theme(). */ function achievements_theme() { return array( 'achievement' => array( 'variables' => array('achievement' => NULL, 'unlock' => NULL), 'template' => 'achievement', ), 'achievement_notification' => array( 'variables' => array('achievement' => NULL, 'unlock' => NULL), 'template' => 'achievement-notification', ), ); } /** * Process variables for achievement.tpl.php. */ function template_preprocess_achievement(&$variables) { achievement_template_shared_variables($variables); } /** * Process variables for achievement-notification.tpl.php. */ function template_preprocess_achievement_notification(&$variables) { achievement_template_shared_variables($variables); $variables['classes_array'][] = 'element-hidden'; } /** * Standard variables used in our achievement templates. * * All our achievement templates send in $achievement and $unlock, but display * some or all of the data in different ways. This is a centralized collection * of the various helper $variables needed for theme display. */ function achievement_template_shared_variables(&$variables) { $variables['state'] = isset($variables['unlock']) ? 'unlocked' : 'locked'; if (isset($variables['achievement']['hidden']) && !achievements_unlocked_already($variables['achievement']['id'])) { $variables['achievement']['points'] = t('???'); // IIiII haveEeeA aa seecFRrit and I'll NEEVvaaha hTellLL.. $variables['achievement']['title'] = t('Hidden achievement'); // unless, of course, you PayPal me bribes. $variables['achievement']['description'] = t('Continue playing to unlock this hidden achievement.'); $variables['state'] = 'hidden'; } $variables['classes_array'][] = 'achievement-' . $variables['state']; $variables['classes_array'][] = 'ui-corner-all'; // if jquery UI is enabled, adds rounded rects. $variables['achievement_url'] = url('achievements/leaderboard/' . $variables['achievement']['id']); $variables['unlocked_date'] = isset($variables['unlock']['timestamp']) ? format_date($variables['unlock']['timestamp'], 'custom', 'Y/m/d') : ''; $variables['unlocked_rank'] = isset($variables['unlock']['rank']) ? t('Rank #@rank', array('@rank' => $variables['unlock']['rank'])) : ''; // set the per-achievement image or admin default. $default = drupal_get_path('module', 'achievements') . '/images/default-' . $variables['state'] . '-70.jpg'; $variables['image'] = isset($variables['achievement']['images'][$variables['state']]) ? theme('image', array('path' => $variables['achievement']['images'][$variables['state']])) : theme('image', array('path' => variable_get('achievements_image_' . $variables['state'], $default))); $variables['image'] = l($variables['image'], 'achievements/leaderboard/' . $variables['achievement']['id'], array('html' => TRUE)); } /** * Implements hook_block_info(). */ function achievements_block_info() { return array( 'achievements_leaderboard' => array( 'info' => t('Achievements leaderboard'), 'cache' => DRUPAL_CACHE_GLOBAL, ), ); } /** * Implements hook_block_view(). */ function achievements_block_view($delta = '') { if ($delta == 'achievements_leaderboard') { include_once(drupal_get_path('module', 'achievements') . '/achievements.pages.inc'); return array( // stupid file that I have to include. WHERE"S MY FUNCTION REGSITERYR. 'content' => achievements_leaderboard_totals(TRUE, variable_get('achievements_leaderboard_block_count_top', 5)), 'subject' => t('Leaderboard'), ); } } /** * Implements hook_block_configure(). */ function achievements_block_configure($delta = '') { if ($delta == 'achievements_leaderboard') { $form['achievements_rankings'] = array( '#title' => t('Leaderboard ranks'), '#type' => 'fieldset', ); $form['achievements_rankings']['achievements_leaderboard_block_count_top'] = array( '#type' => 'select', '#title' => t('Number of top ranks'), '#default_value' => variable_get('achievements_leaderboard_block_count_top', 5), '#options' => drupal_map_assoc(range(0, 30)), ); return $form; } } /** * Implements hook_block_save(). */ function achievements_block_save($delta = '', $edit = array()) { if ($delta == 'achievements_leaderboard') { // bleh, this is wasteful code that should be automated. variable_set('achievements_leaderboard_block_count_top', $edit['achievements_leaderboard_block_count_top']); } } /** * Load information about our achievements. * * @param $achievement_id * The (optional) achievement this request applies against. * @param $grouped * Whether to return the achievements list flattened (FALSE, the default) * or grouped into achievement-defined categories. If TRUE, but there is no * group specified for an achievement, it'll be stored in a "-none-" array * intended to simplify display code. Not compatible with $achievement_id. * @param $reset * Forces a refresh of the cached achievement data. * * @return $achievements * An array of all achievements, or just the one passed. */ function achievements_load($achievement_id = NULL, $grouped = FALSE, $reset = FALSE) { $achievements = &drupal_static(__FUNCTION__); if (!isset($achievements) || $reset) { if (!$reset && $cache = cache_get('achievements_info')) { $achievements = $cache->data; } else { $result = module_invoke_all('achievements_info'); // determine if we're looking at an achievement or group and create our // master $achievements array. we store the achievements in two separate // forms: one with a tree (for display purposes) and one flattened (for // lookup purposes). the flattened index is referenced so that we save // space in the final serialized blob that cache_set() sends. foreach ($result as $key => $value) { if (isset($value['achievements']) && is_array($value['achievements'])) { $achievements['grouped'][$key] = $value; // copy the whole shebang into realz. foreach ($achievements['grouped'][$key]['achievements'] as $id => $achievement) { $achievements['grouped'][$key]['achievements'][$id]['id'] = $id; $achievements['grouped'][$key]['achievements'][$id]['group_id'] = $key; $achievements['grouped'][$key]['achievements'][$id]['group_title'] = $value['title']; $achievements['flat'][$id] = &$achievements['grouped'][$key]['achievements'][$id]; } } else { $value['id'] = $key; $value['group_id'] = '-none-'; $value['group_title'] = NULL; // moo. $achievements['grouped']['-none-']['achievements'][$key] = $value; $achievements['flat'][$key] = &$achievements['grouped']['-none-']['achievements'][$key]; } } if (isset($achievements['grouped']['-none-'])) { $achievements['grouped']['-none-']['title'] = t('Miscellany'); } cache_set('achievements_info', $achievements, 'cache', CACHE_TEMPORARY); } } if ($achievement_id) { // all my majesty and brilliance, and you just want one result? /me weeps. return isset($achievements['flat'][$achievement_id]) ? $achievements['flat'][$achievement_id] : FALSE; } // return the whole shebang in groups or a flattened lookup bucket. return $grouped ? $achievements['grouped'] : $achievements['flat']; } /** * Returns all, or per-user, achievement totals. * * @param $count * The number of top-ranking users to return (defaults to 50). * @param $current_user * Whether to include the current user's stats in the list, even if they're * not in the top $count. This is just a friendly "how you compare" feature * ("I'm rank 52; nearly in the top 50. woot!"). Defaults to TRUE. * * @return $totals * An array of totals information for the top $count users. */ function achievements_totals($count = 50, $current_user = TRUE) { $query = db_select('achievement_totals', 'at'); $query->join('users', 'u', 'u.uid = at.uid'); $query->fields('at', array('uid', 'points', 'unlocks', 'timestamp'))->fields('u', array('name')); $query->orderBy('at.points', 'DESC')->orderBy('at.timestamp'); // @todo DESC/ASC doesn't index. $achievers = $query->range(0, $count)->execute()->fetchAllAssoc('uid'); $rank = 1; // add the ranking. foreach ($achievers as $achiever) { $achiever->rank = $rank++; } // if the current logged in user isn't in our top $count, add 'em if they have achievements. if ($current_user && user_is_logged_in() && !isset($achievers[$GLOBALS['user']->uid]) && achievements_totals_user('all')) { $achievers[$GLOBALS['user']->uid] = achievements_totals_user('all'); } return $achievers; } /** * Returns a specific user's achievement totals. * * @param $type * 'points', 'unlocks', 'rank', or 'all' (defaults to 'points'). * @param $uid * The user to return achievement info for (defaults to current user). * * @return $integer or $object * The value of the passed $type. If $type is 'all', the full object. */ function achievements_totals_user($type = 'points', $uid = NULL) { list($uid, $access) = achievements_user_is_achiever($uid); // we don't check for access as this is info grubbing only. $achievers = &drupal_static(__FUNCTION__); if (!isset($achievers[$uid])) { $query = db_select('achievement_totals', 'at'); $query->join('users', 'u', 'u.uid = at.uid'); $query->fields('at', array('uid', 'points', 'unlocks', 'timestamp'))->fields('u', array('name')); $achievers[$uid] = $query->condition('at.uid', $uid)->execute()->fetchObject(); if ($achievers[$uid]) { // only keep going if they've unlocked something. // to find the user's rank: count all the users with greater points, add // all the users with equal points but earlier timestamps, and then add 1. $better_points = db_select('achievement_totals')->condition('points', $achievers[$uid]->points, '>')->countQuery()->execute()->fetchField(); $earlier_times = db_select('achievement_totals')->condition('points', $achievers[$uid]->points)->condition('timestamp', $achievers[$uid]->timestamp, '<')->countQuery()->execute()->fetchField(); $achievers[$uid]->rank = $better_points + $earlier_times + 1; } } return $type == 'all' ? $achievers[$uid] : (isset($achievers[$uid]->$type) ? $achievers[$uid]->$type : 0); } /** * Logs a user as having unlocked an achievement. * * @param $achievement_id * The achievement this request applies against. * @param $uid * The user to unlock an achievement for (defaults to current user). */ function achievements_unlocked($achievement_id, $uid = NULL) { list($uid, $access) = achievements_user_is_achiever($uid); if (!$access) { return; } // i know you want it, but... // grab information about the achievement. $achievement = achievements_load($achievement_id); if (!isset($achievement)) { // hrm... try a cache refresh? $achievement = achievements_load($achievement_id, FALSE, TRUE); } if (isset($achievement) && !achievements_unlocked_already($achievement_id, $uid)) { $last_rank = db_select('achievement_unlocks', 'au')->fields('au', array('rank')) // not. exciting. at. all. ->condition('achievement_id', $achievement_id)->orderBy('rank', 'DESC')->range(0, 1)->execute()->fetchField(); db_insert('achievement_unlocks') ->fields(array( 'achievement_id' => $achievement_id, 'uid' => $uid, 'rank' => $last_rank ? $last_rank + 1 : 1, 'timestamp' => REQUEST_TIME, 'seen' => 0, )) ->execute(); db_merge('achievement_totals') ->key(array('uid' => $uid)) ->fields(array( 'points' => $achievement['points'], 'unlocks' => 1, // OMG CONGRATS 'timestamp' => REQUEST_TIME, )) ->expression('points', 'points + :points', array(':points' => $achievement['points'])) ->expression('unlocks', 'unlocks + :increment', array(':increment' => 1)) ->execute(); watchdog('achievements', t('Unlocked: %achievement (+@points).'), array('%achievement' => $achievement['title'], '@points' => $achievement['points']), WATCHDOG_NOTICE, l(t('view'), 'user/' . $uid . '/achievements')); // nothing fancy. } } /** * Determine if a user has already unlocked an achievement. * * @param $achievement_id * The achievement this request applies against. * @param $uid * The user this request applies against (defaults to current user). * * @return NULL or $unlocked * $unlocked is an array containing rank and timestamp. */ function achievements_unlocked_already($achievement_id, $uid = NULL) { list($uid, $access) = achievements_user_is_achiever($uid); if (!$access) { return; } // i can't let you in, y'know? $unlock = db_select('achievement_unlocks', 'au')->fields('au', array('rank', 'timestamp')) ->condition('achievement_id', $achievement_id)->condition('uid', $uid)->execute()->fetchAssoc(); return isset($unlock) ? $unlock : NULL; } /** * Retrieve data needed by an achievement. * * @param $achievement_id * An identifier for the achievement whose data is being collected. * @param $uid * The user this stored data applies to (defaults to current user). * * @return $data * The data stored for this achievement and user (unserialized). */ function achievements_storage_get($achievement_id = NULL, $uid = NULL) { list($uid, $access) = achievements_user_is_achiever($uid); if (!$access) { return; } // it's not that I don't want to... return unserialize(db_select('achievement_storage')->fields('achievement_storage', array('data')) ->condition('achievement_id', $achievement_id)->condition('uid', $uid)->execute()->fetchField()); } /** * Save data needed by an achievement. * * @param $achievement_id * An identifier for the achievement whose data is being collected. * @param $uid * The user this stored data applies to (defaults to current user). * @param $data * The data being saved (of any type; serialization occurs). */ function achievements_storage_set($achievement_id = NULL, $data = NULL, $uid = NULL) { list($uid, $access) = achievements_user_is_achiever($uid); if (!$access) { return; } // I... I'M IN LOVE WITH MORBUS OK?!!? db_merge('achievement_storage') ->key(array('uid' => $uid, 'achievement_id' => $achievement_id)) ->fields(array('data' => serialize($data))) // it's hot in here. ->execute(); // i hate all DBTNG syntax. NEVAH STANDARNDIZEE?Ee1 } /** * Determine if a user is able to earn achievements. * * This is a general helper around the core achievements functions and allows * us to default to the global user if a $uid is not passed, but also check * permissions against a user who is not the global user. This allows us to * a) define roles of users that can not earn achievements and b) manually * unlock achievements for a non-current user. * * @param $uid * The user to check for "earn achievements" (defaults to current user). * * @return $results * An array with values of: * - $uid is the determined user (default: the global user). * - $access is a TRUE or FALSE as returned by user_access(). */ function achievements_user_is_achiever($uid = NULL) { if (!isset($uid) || $uid == $GLOBALS['user']->uid) { return array($GLOBALS['user']->uid, user_access('earn achievements')); } else { return array($uid, user_access('earn achievements', user_load($uid))); } } /** * Implements hook_user_cancel(). */ function achievements_user_cancel($edit, $account, $method) { achievements_user_delete($account); // no stats for non-players. } /** * Implements hook_user_delete(). */ function achievements_user_delete($account) { db_delete('achievement_totals')->condition('uid', $account->uid)->execute(); db_delete('achievement_unlocks')->condition('uid', $account->uid)->execute(); db_delete('achievement_storage')->condition('uid', $account->uid)->execute(); } /** * Implements hook_page_alter(). */ function achievements_page_alter(&$page) { if (achievements_user_is_achiever()) { $unlocks = db_select('achievement_unlocks', 'au')->fields('au', array('achievement_id', 'rank', 'timestamp')) ->condition('uid', $GLOBALS['user']->uid)->condition('seen', 0)->orderBy('timestamp')->execute()->fetchAllAssoc('achievement_id'); // if unseen unlocks are available, load in our JS libraries, // display our achievement notification, and flag 'em as seen. if (count($unlocks)) { drupal_add_library('system', 'ui.dialog'); drupal_add_library('system', 'effects.fade'); drupal_add_js(drupal_get_path('module', 'achievements') . '/achievements.js'); foreach ($unlocks as $unlock) { $achievement = achievements_load($unlock->achievement_id); $page['page_bottom']['achievements'][$unlock->achievement_id] = array( '#theme' => 'achievement_notification', '#achievement' => $achievement, '#unlock' => (array) $unlock, ); } db_update('achievement_unlocks')->fields(array('seen' => 1)) ->condition('uid', $GLOBALS['user']->uid)->condition('seen', 0)->execute(); } } }