Skip to content
achievements.module 26.7 KiB
Newer Older
Morbus Iff's avatar
Morbus Iff committed
<?php

/**
 * @file
 * Unlock achievements and earn points based on milestones.
 */

/**
 * Implements hook_permission().
Morbus Iff's avatar
Morbus Iff committed
 */
function achievements_permission() {
  return array(
    'access achievements' => array(
      'title' => t('Access achievements'),
    ),
    'earn achievements' => array(
      'title' => t('Earn achievements'),
    ),
    'administer achievements' => array(
      'title' => t('Administer achievements'),
    ),
  );
}
Morbus Iff's avatar
Morbus Iff committed

/**
 * Implements hook_menu().
Morbus Iff's avatar
Morbus Iff committed
 */
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(
Morbus Iff's avatar
Morbus Iff committed
    'access arguments'  => array('administer achievements'),
    'description'       => 'Configure the achievements system.',
    'file'              => 'achievements.admin.inc',
Morbus Iff's avatar
Morbus Iff committed
    '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,
  );
Morbus Iff's avatar
Morbus Iff committed

  return $items;
}

/**
 * Implements hook_theme().
Morbus Iff's avatar
Morbus Iff committed
 */
function achievements_theme() {
  $path = drupal_get_path('module', 'achievements') . '/templates';

  return array(
    'achievement' => array(
      'variables'       => array('achievement' => NULL, 'unlock' => NULL),
      'template'        => 'achievement',
    'achievement_notification' => array(
      'variables'       => array('achievement' => NULL, 'unlock' => NULL),
      'template'        => 'achievement-notification',
      'path'            => $path,
    ),
    'achievement_latest_unlock' => array(
      'variables'       => array('achievement' => NULL, 'unlock' => NULL),
      'template'        => 'achievement-latest-unlock',
      'path'            => $path,
    'achievement_user_stats' => array(
      'variables'       => array('stats' => NULL),
    ),
    'achievement_groups_wrapper' => array(
      'render element'  => 'element',
    ),
    'achievement_group_wrapper' => array(
      'render element'  => 'element',
    ),
Morbus Iff's avatar
Morbus Iff committed
}

/**
 * Process variables for achievement.tpl.php.
 */
function template_preprocess_achievement(&$variables) {
  achievements_template_shared_variables($variables);
}

/**
 * Process variables for achievement-notification.tpl.php.
 */
function template_preprocess_achievement_notification(&$variables) {
  achievements_template_shared_variables($variables);
  $variables['classes_array'][] = 'element-hidden';
}

/**
 * Process variables for achievement-latest-unlock.tpl.php.
 */
function template_preprocess_achievement_latest_unlock(&$variables) {
  achievements_template_shared_variables($variables);
}

/**
 * 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 achievements_template_shared_variables(&$variables) {
  $variables['state'] = isset($variables['unlock']) ? 'unlocked' : 'locked';
  $variables['classes_array'][] = 'achievement-' . $variables['state'];
  $variables['classes_array'][] = 'ui-corner-all'; // add rounded rects for tabs.
  $variables['achievement_url'] = url('achievements/leaderboard/' . $variables['achievement']['id']);
  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';
  }

  // set the displayed image to a hidden-or-not default-or-not determined value. sheesh.
  $default = drupal_get_path('module', 'achievements') . '/images/default-' . $variables['state'] . '-70.jpg';
  $variables['image_path'] = isset($variables['achievement']['images'][$variables['state']])
    ? $variables['achievement']['images'][$variables['state']] // user-defined image, yay!
    : variable_get('achievements_image_' . $variables['state'], $default);

  $variables['image'] = array(
    '#theme' => 'image_formatter',
    '#item' => array(
      'uri'   => $variables['image_path'],
      'alt'   => $variables['achievement']['title'],
      'title' => $variables['achievement']['title'],
    ),
    '#path' => array(
      'path'    => 'achievements/leaderboard/' . $variables['achievement']['id'],
      'options' => array('html' => TRUE),
    ),
  );

  $variables['achievement_title'] = array(
    '#type'   => 'link',
    '#title'  => $variables['achievement']['title'],
    '#href'   => 'achievements/leaderboard/' . $variables['achievement']['id'],
  $variables['achievement_points'] = array(
    '#markup' => t('@points points', array('@points' => $variables['achievement']['points'])),
  );
  $variables['unlocked_date'] = array(
    '#markup' => isset($variables['unlock']['timestamp'])
      ? format_date($variables['unlock']['timestamp'], 'custom', 'Y/m/d')
      : '',
  );
  $variables['unlocked_rank'] = array(
    '#markup' => isset($variables['unlock']['rank'])
      ? t('Rank #@rank', array('@rank' => $variables['unlock']['rank']))
      : '',
  );
Morbus Iff's avatar
Morbus Iff committed
/**
 * Implements hook_block_info().
Morbus Iff's avatar
Morbus Iff committed
 */
function achievements_block_info() {
Morbus Iff's avatar
Morbus Iff committed
  return array(
    'achievements_leaderboard' => array(
      'info'  => t('Achievements leaderboard'),
      'cache' => DRUPAL_NO_CACHE,
 * Implements hook_block_view().
Morbus Iff's avatar
Morbus Iff committed
 */
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_block(),
      'subject' => t('Leaderboard'),
/**
 * Implements hook_block_configure().
 */
function achievements_block_configure($delta = '') {
  if ($delta == 'achievements_leaderboard') {
    $form['achievements_rankings'] = array(
      '#description'        => t('Enabling the "relative leaderboard" will show the current user\'s position and, optionally, a number of ranks before and after that position. For example, if the current user is ranked 12th and you configure 3 nearby ranks, the relative leaderboard will show ranks 9 through 15. The relative leaderboard will only show if the logged-in user does not appear in the displayed top ranks.'),
      '#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)),
    );
    $form['achievements_rankings']['achievements_leaderboard_block_relative'] = array(
      '#type'           => 'radios',
      '#title'          => t('Relative leaderboard display'),
      '#default_value'  => variable_get('achievements_leaderboard_block_relative', 'nearby_ranks'),
      '#options'        => array(
        'disabled'      => t('Don\'t show the relative leaderboard'),
        'user_only'     => t('Show only the current user'),
        'nearby_ranks'  => t('Show the current user and nearby ranks'),
      ),
    );
    $form['achievements_rankings']['achievements_leaderboard_block_relative_nearby_ranks'] = array(
      '#type'           => 'select',
      '#title'          => t('Number of nearby ranks to display'),
      '#default_value'  => variable_get('achievements_leaderboard_block_relative_nearby_ranks', 1),
      '#options'        => drupal_map_assoc(range(1, 10)),
    );

    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']);
    variable_set('achievements_leaderboard_block_relative', $edit['achievements_leaderboard_block_relative']);
    variable_set('achievements_leaderboard_block_relative_nearby_ranks', $edit['achievements_leaderboard_block_relative_nearby_ranks']);
Morbus Iff's avatar
Morbus Iff committed
/**
 * 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
Morbus Iff's avatar
Morbus Iff committed
 *   Forces a refresh of the cached achievement data.
Morbus Iff's avatar
Morbus Iff committed
 * @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__);
Morbus Iff's avatar
Morbus Iff committed

  if (!isset($achievements) || $reset) {
    if (!$reset && $cache = cache_get('achievements_info')) {
      $achievements = $cache->data;
Morbus Iff's avatar
Morbus Iff committed
    }
    else {
      $achievements = array('flat' => array(), 'grouped' => array());
      $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['grouped'][$key]['achievements'][$id]['points'] = isset($achievement['points']) ? $achievement['points'] : 0;
            $achievements['flat'][$id] = &$achievements['grouped'][$key]['achievements'][$id];
          }
        }
        else {
          $value['id'] = $key;
          $value['group_id'] = '-none-';
          $value['group_title'] = NULL; // moo.
          $value['points'] = isset($value['points']) ? $value['points'] : 0;
          $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);
Morbus Iff's avatar
Morbus Iff committed
    }
  }

  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'];
Morbus Iff's avatar
Morbus Iff committed
}

/**
 * Returns a user-centric leaderboard.
Morbus Iff's avatar
Morbus Iff committed
 *
 * @param $nearby
 *   How many nearby ranks/users should be returned (defaults to 0).
 * @param $uid
 *   The user to return achievement info for (defaults to current user).
 *   Either an empty array (for users who have yet to unlock anything) or
 *   or an array of user(s) and their achievement statistics, keyed to uid.
Morbus Iff's avatar
Morbus Iff committed
 */
function achievements_totals_user($nearby = 0, $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__);
Morbus Iff's avatar
Morbus Iff committed
  if (!isset($achievers[$uid])) {
    $query = db_select('achievement_totals', 'at');
    $query->addTag('achievement_totals_user')->join('users', 'u', 'u.uid = at.uid');
    $query->fields('at', array('uid', 'points', 'unlocks', 'timestamp', 'achievement_id'))->fields('u', array('name'));
    $achievers[$uid] = $query->condition('at.uid', $uid)->execute()->fetchObject();

    if (isset($achievers[$uid]->points)) { // only 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;
    }
Morbus Iff's avatar
Morbus Iff committed
  }

  // we need to get some nearby users, so use our global leaderboard query to
  // get a range based on the $uid's current rank and how many $nearby users.
  if (isset($achievers[$uid]->points) && $nearby) {
    $starting_rank = max(1, $achievers[$uid]->rank - $nearby);

    // build the magical rank-ranging query.
    $query = db_select('achievement_totals', 'at');
    $query->addTag('achievement_totals_user_nearby')->join('users', 'u', 'u.uid = at.uid');
    $query->fields('at', array('uid', 'points', 'unlocks', 'timestamp', 'achievement_id'))->fields('u', array('name'));
    $query->orderBy('at.points', 'DESC')->orderBy('at.timestamp'); // @bug DESC/ASC doesn't index. at all. curses.
    $offset = max(0, $achievers[$uid]->rank - $nearby - 1); // max() ensures no negative numbers in our lookup.
    $limit  = $achievers[$uid]->rank - $offset + $nearby; // always get +$nearby from current rank.
    $achievers = $query->range($offset, $limit)->execute()->fetchAllAssoc('uid');

    foreach ($achievers as $achiever) {
      $achiever->rank = $starting_rank++;
    }
  }

  return isset($achievers[$uid]->points) ? $achievers : array();
Morbus Iff's avatar
Morbus Iff committed
}

/**
 * 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).
Morbus Iff's avatar
Morbus Iff committed
 */
function achievements_unlocked($achievement_id, $uid = NULL) {
  list($uid, $access) = achievements_user_is_achiever($uid);
  if (!$access) { return; } // i know you want it, but...
Morbus Iff's avatar
Morbus Iff committed

  // 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);
Morbus Iff's avatar
Morbus Iff committed

  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,
      ))
      ->execute();

    db_merge('achievement_totals')
      ->key(array('uid' => $uid))
      ->fields(array(
        'points'          => $achievement['points'],
        'unlocks'         => 1, // OMG CONGRATS
        'timestamp'       => REQUEST_TIME,
        'achievement_id'  => $achievement_id,
      ))
      ->expression('points', 'points + :points', array(':points' => $achievement['points']))
      ->expression('unlocks', 'unlocks + :increment', array(':increment' => 1))
      ->execute();
    // update the unlocked_already static cache so that it returns accurate
    // unlocks for the rest of the page load. we could have just reset the
    // cache entirely but that would cost us additional database queries
    // based on the number of unique unlocks in this page load.
    $unlocks = &drupal_static('achievements_unlocked_already');
    $unlocks[$uid][$achievement_id] = array(
      'achievement_id'  => $achievement_id,
      'rank'            => $last_rank ? $last_rank + 1 : 1,
      'timestamp'       => REQUEST_TIME,
    );

    module_invoke_all('achievements_unlocked', $achievement, $uid);

    watchdog('achievements', 'Unlocked: %achievement (+@points).',
      array('%achievement' => $achievement['title'], '@points' => $achievement['points']),
      WATCHDOG_NOTICE, l(t('view'), 'user/' . $uid . '/achievements')); // nothing fancy.
Morbus Iff's avatar
Morbus Iff committed
  }
}

/**
 * Return data about a user's unlocked achievements.
Morbus Iff's avatar
Morbus Iff committed
 *
 * @param $achievement_id
 *   A specific achievement to check the unlock status of.
Morbus Iff's avatar
Morbus Iff committed
 * @param $uid
 *   The user this request applies against (defaults to current user).
 * @return NULL or $unlocked or $unlocks
 *   One of the following, based on the passed parameters:
 *   - If the $uid has not unlocked $achievement_id, return NULL.
 *   - If $achievement_id is unlocked, return an array of rank and timestamp.
 *   - If no $achievement_id is passed, an array of all $uid's unlocks.
Morbus Iff's avatar
Morbus Iff committed
 */
function achievements_unlocked_already($achievement_id = NULL, $uid = NULL) {
  list($uid, $access) = achievements_user_is_achiever($uid);
  if (!$access) { return; } // i can't let you in, y'know?
  $unlocks = &drupal_static(__FUNCTION__);
Morbus Iff's avatar
Morbus Iff committed

  if (!isset($unlocks[$uid])) {
    // we grab all unlocks and cache per page load. it's better than lots of per-achievement lookups.
    $unlocks[$uid] = db_select('achievement_unlocks', 'au')->fields('au', array('achievement_id', 'rank', 'timestamp'))
      ->condition('uid', $uid)->execute()->fetchAllAssoc('achievement_id', PDO::FETCH_ASSOC); // INSERT FORTUNE, LONELINESS.
Morbus Iff's avatar
Morbus Iff committed

  if (isset($achievement_id)) { // return data about a specific unlock if requested.
    return isset($unlocks[$uid][$achievement_id]) ? $unlocks[$uid][$achievement_id] : NULL;
  }
  else { // all of 'em.
    return $unlocks[$uid];
  }
Morbus Iff's avatar
Morbus Iff committed
}

/**
 * Relocks (or "takes away") an achievement from a user.
 *
 * @param $achievement_id
 *   The achievement this request applies against.
 * @param $uid
 *   The user to relock an achievement for (defaults to current user).
 */
function achievements_locked($achievement_id, $uid = NULL) {
  list($uid, $access) = achievements_user_is_achiever($uid);
  if (!$access) { return; } // i have congestion and sniffles.

  // only remove the achievement if the user has unlocked it.
  if (achievements_unlocked_already($achievement_id, $uid)) {
    $achievement = achievements_load($achievement_id); // we need the full thing so we know how many points to take away.
    db_delete('achievement_unlocks')->condition('achievement_id', $achievement['id'])->condition('uid', $uid)->execute();
    db_update('achievement_totals') // remove from unlocks and subtract points from the current totals.
      ->fields(array('uid' => $uid, 'timestamp' => REQUEST_TIME))
      ->expression('points', 'points - :points', array(':points' => $achievement['points']))
      ->expression('unlocks', 'unlocks - :decrement', array(':decrement' => 1))
      ->condition('uid', $uid)->execute();

    // remove any storage associated with this achievement.
    achievements_storage_del($achievement['id'], $uid);
    module_invoke_all('achievements_locked', $achievement, $uid);
  }
}

Morbus Iff's avatar
Morbus Iff committed
/**
 * 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).
Morbus Iff's avatar
Morbus Iff committed
 * @return $data
 *   The data stored for this achievement and user (unserialized).
Morbus Iff's avatar
Morbus Iff committed
 */
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...
Morbus Iff's avatar
Morbus Iff committed

  return unserialize(db_select('achievement_storage')->fields('achievement_storage', array('data'))
    ->condition('achievement_id', $achievement_id)->condition('uid', $uid)->execute()->fetchField());
Morbus Iff's avatar
Morbus Iff committed
}

/**
 * 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).
Morbus Iff's avatar
Morbus Iff committed
 * @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
Morbus Iff's avatar
Morbus Iff committed
}

/**
 * Delete data stored 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).
 */
function achievements_storage_del($achievement_id = NULL, $uid = NULL) {
  list($uid, $access) = achievements_user_is_achiever($uid);
  if (!$access) { return; } // WE'RE GONNA HAVE A MEELLION BABIES.

  $achievement = achievements_load($achievement_id);
  $storage = isset($achievement['storage']) ? $achievement['storage'] : $achievement['id'];
  db_delete('achievement_storage')->condition('achievement_id', $storage)->condition('uid', $uid)->execute();
}

/**
 * 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 determined by user_access() and hooks.
 */
function achievements_user_is_achiever($uid = NULL) {
  $is_achiever = &drupal_static(__FUNCTION__);
  $uid = isset($uid) ? $uid : $GLOBALS['user']->uid;

  if (!isset($is_achiever[$uid])) {
    if ($uid == $GLOBALS['user']->uid) { // if $uid is current user, check normally.
      $is_achiever[$uid] = array($GLOBALS['user']->uid, user_access('earn achievements'));
    }
    else { // if it's not the global user, we need to load them fully, then check.
      $is_achiever[$uid] = array($uid, user_access('earn achievements', user_load($uid)));
    }

    // Let other modules decide if this user can earn achievements. Hook
    // results take precedence over the standard user_access() because we
    // treat code prowess as stronger rationale than simplistic UI clicking.
    $module_access = module_invoke_all('achievements_access_earn', $uid);

    if (in_array(TRUE, $module_access, TRUE)) {
      $is_achiever[$uid] = array($uid, TRUE);
    }
    elseif (in_array(FALSE, $module_access, TRUE)) {
      $is_achiever[$uid] = array($uid, FALSE);
    }
Morbus Iff's avatar
Morbus Iff committed
/**
 * Implements hook_user_cancel().
Morbus Iff's avatar
Morbus Iff committed
 */
function achievements_user_cancel($edit, $account, $method) {
  achievements_user_delete($account); // no stats for non-players.
Morbus Iff's avatar
Morbus Iff committed
}

/**
 * Implements hook_user_delete().
Morbus Iff's avatar
Morbus Iff committed
 */
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();
Morbus Iff's avatar
Morbus Iff committed
}

/**
 * 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')) // find everything we've yet to see.
      ->condition('uid', $GLOBALS['user']->uid)->condition('seen', 0)->orderBy('timestamp')->execute()->fetchAllAssoc('achievement_id', PDO::FETCH_ASSOC);

    // 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,
        );
      }

      db_update('achievement_unlocks')->fields(array('seen' => 1))
        ->condition('uid', $GLOBALS['user']->uid)->condition('seen', 0)->execute();
    }
  }
}