Skip to content
achievements.module 12.6 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.',
Morbus Iff's avatar
Morbus Iff committed
    'file'              => 'achievements.pages.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,
  );
Morbus Iff's avatar
Morbus Iff committed

  return $items;
}

/**
 * Implements hook_theme().
Morbus Iff's avatar
Morbus Iff committed
 */
function achievements_theme() {
  return array(
    'achievement' => array(
      'variables'       => array('achievement' => NULL, 'unlock' => NULL),
      'template'        => 'achievement',
    ),
  );
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_CACHE_GLOBAL,
Morbus Iff's avatar
Morbus Iff committed
    ),
  );
}

/**
 * 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.
      'subject' => t('Leaderboard'),
      'content' => achievements_leaderboard_totals(TRUE),
Morbus Iff's avatar
Morbus Iff committed
    );
  }
}

/**
 * Load information about our achievements.
 *
 * @param $achievement_id
 *   Optional; the achievement this request applies against.
 * @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, $reset = FALSE) {
  $achievements = &drupal_static(__FUNCTION__);
Morbus Iff's avatar
Morbus Iff committed

  if (!isset($achievements) || $reset) {
    if (!$reset && $cache = cache_get('achievement_info')) {
      $achievements = $cache->data;
Morbus Iff's avatar
Morbus Iff committed
    }
    else {
      $achievements = module_invoke_all('achievements_info');
      cache_set('achievement_info', $achievements, 'cache', CACHE_TEMPORARY);
      // no magically-useful way to say "on file change". le sigh.
Morbus Iff's avatar
Morbus Iff committed
    }
  }

  return $achievement_id
    ? (isset($achievements[$achievement_id]) ? $achievements[$achievement_id] : FALSE)
    : $achievements; // return FALSE to stop bum URLs (via the menu %loader callback).
Morbus Iff's avatar
Morbus Iff committed
}

/**
 * Returns all, or per user, achievement totals.
Morbus Iff's avatar
Morbus Iff committed
 *
 * @param $count
 *   Defaults to 50; the number of top-ranking users to return.
 * @param $current_user
 *   Defaults to TRUE; 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!").
 * @return $totals
 *   An array of totals information for the top $count users.
Morbus Iff's avatar
Morbus Iff committed
 */
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');
Morbus Iff's avatar
Morbus Iff committed

  $rank = 1; // add the ranking.
  foreach ($achievers as $achiever) {
    $achiever->rank = $rank++;
  }

  // if the current user isn't in our top $count, find 'em.
  if ($current_user && user_is_logged_in() && !isset($achievers[$GLOBALS['user']->uid])) {
    $achievers[$GLOBALS['user']->uid] = achievements_totals_user('all');
Morbus Iff's avatar
Morbus Iff committed
  }

Morbus Iff's avatar
Morbus Iff committed
}

/**
 * Returns a specific user's achievement totals.
Morbus Iff's avatar
Morbus Iff committed
 *
 * @param $type
 *   Defaults to 'points'; one of 'points', 'unlocks', 'rank', or 'all'.
 * @param $uid
 *   Defaults to current user; the user to return achievement info for.
 *
 * @return $integer or $object
 *   The value of the passed $type. If $type is 'all', the full object.
Morbus Iff's avatar
Morbus Iff committed
 */
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__);
Morbus Iff's avatar
Morbus Iff committed
  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();

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

  return $type == 'all' ? $achievers[$uid] : ($achievers[$uid]->$type ? $achievers[$uid]->$type : 0);
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
 *   Defaults to current user; the user to unlock an achievement for.
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, 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,
      ))
      ->expression('points', 'points + :points', array(':points' => $achievement['points']))
      ->expression('unlocks', 'unlocks + :increment', array(':increment' => 1))
      ->execute();

    if ($uid == $GLOBALS['user']->uid) { // only show the unlock if $uid is our viewer.
Morbus Iff's avatar
Morbus Iff committed
      drupal_set_message(t('<strong>Achievement unlocked:</strong> @achievement (+@number). !view.',
        array('@achievement' => $achievement['title'], '@number' => $achievement['points'],
          '!view' => l(t('View your achievements'), 'user/' . $uid . '/achievements'))));
Morbus Iff's avatar
Morbus Iff committed
    }
  }
}

/**
 * Determine if a user has already unlocked an achievement.
 *
 * @param $achievement_id
 *   The achievement this request applies against.
 * @param $uid
 *   Defaults to current user; the user this request applies against.
 * @return NULL or $unlocked
 *   $unlocked is an array containing rank and timestamp.
Morbus Iff's avatar
Morbus Iff committed
 */
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?
Morbus Iff's avatar
Morbus Iff committed

  $unlock = db_select('achievement_unlocks', 'au')->fields('au', array('rank', 'timestamp'))
    ->condition('achievement_id', $achievement_id)->condition('uid', $uid)->execute()->fetchAssoc();
Morbus Iff's avatar
Morbus Iff committed

  return isset($unlock) ? $unlock : NULL;
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
 *   Defaults to current user; the user this stored data applies to.
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
 *   Defaults to current user; the user this stored data applies to.
 * @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
}

/**
 * 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
 *   Defaults to current user; the user to check for "earn achievements".
 *
 * @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)));
  }
}

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
}