diff -Naur beta5//scripts/config.inc forums//scripts/config.inc
--- beta5//scripts/config.inc	2011-02-05 10:09:57.904335002 +0100
+++ forums//scripts/config.inc	2011-03-12 15:47:09.431300051 +0100
@@ -39,7 +39,7 @@
 	"widgetURL"	=> "http://www.legacyworlds.com/downloads/LegacyWorlds-Dashboard-latest.zip",
 
 	// Version numbers to make us feel good
-	"v_engine"	=> "0.85a",
+	"v_engine"	=> "0.86",
 	"v_game"	=> "Beta 5",
 	"v_rev"		=> "2218",
 
diff -Naur beta5//scripts/game/beta5/actions/getForums.inc forums//scripts/game/beta5/actions/getForums.inc
--- beta5//scripts/game/beta5/actions/getForums.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/game/beta5/actions/getForums.inc	2011-02-05 10:10:03.244335002 +0100
@@ -0,0 +1,84 @@
+<?
+
+/** This action returns the structure of the forums a player has access to.
+ *
+ * \parameter $player The player identifier for whom to get the forums' structure
+ *
+ * \returns null on error or a fl_container instance that contains the forums' structure.
+ */
+
+class beta5_getForums {
+
+	private static $names = array(
+		'G'	=> array(
+				'en'	=> array('General forums', "LegacyWorlds' public forums"),
+				'fr'	=> array('Forums généraux', "Les forums publics de LegacyWorlds")
+			),
+		'U'	=> array(
+				'en'	=> array('Player forums', "Forums belonging to other LegacyWorlds players"),
+				'fr'	=> array('Forums des joueurs', "Forums appartenant à d'autres joueurs de LegacyWorlds")
+			)
+	);
+
+	public function __construct($game) {
+		$this->game = $game;
+		$this->db = $this->game->getDBAccess();
+
+		// Load the forums' access libraries
+		$this->gForums = $this->game->getLib('main/gforums');
+		$this->uForums = $this->game->getLib('main/uforums');
+		$this->aForums = $this->game->getLib('beta5/aforums');
+
+		// Load the container class
+		loader::needClasses('main/forums', 'fl_container');
+	}
+
+	public function run( $player ) {
+		// Get the player's user ID
+		$q = $this->db->query("SELECT userid FROM player WHERE id = $1", (int) $player);
+		if (!( $q && dbCount($q) == 1)) {
+			return null;
+		}
+		list($user) = dbFetchArray($q);
+
+		// Get the language string for the user
+		// FIXME: that thing would not work inside anything *but* a web session
+		$lang = getLanguage();
+
+		// Get the list of general, user-specific and alliance forums
+		$gForums = $this->gForums->call('getStructure', $user);
+		$aForums = $this->aForums->call('getStructure', $user);
+		$uForums = $this->uForums->call('getStructure', $user);
+
+		// Generate the container for the whole thing
+		$forums = new fl_container('/', '', '');
+
+		// Generate the container for general forums
+		$gContainer = new fl_container('G', self::$names['G'][$lang][0], self::$names['G'][$lang][1]);
+		foreach ($gForums as $gc) {
+			$gContainer->addCategory($gc);
+		}
+		$forums->addCategory($gContainer);
+
+		// Add the alliance forums
+		if (count($aForums) == 1) {
+			$forums->addCategory($aForums[0]);
+		}
+
+		// Add the player's own forums
+		$forums->addCategory(array_shift($uForums));
+
+		// Add the other players' forums
+		if (count($uForums)) {
+			$uContainer = new fl_container('U', self::$names['U'][$lang][0], self::$names['U'][$lang][1]);
+			foreach ($uForums as $uc) {
+				$uContainer->addCategory($uc);
+			}
+			$forums->addCategory($uContainer);
+		}
+
+		return $forums;
+	}
+}
+
+?>
diff -Naur beta5//scripts/game/beta5/actions.inc forums//scripts/game/beta5/actions.inc
--- beta5//scripts/game/beta5/actions.inc	2011-02-05 10:09:57.784335002 +0100
+++ forums//scripts/game/beta5/actions.inc	2011-03-12 14:56:24.591300053 +0100
@@ -29,6 +29,10 @@
 	var	$fleetDepartures = array();
 	var	$ePower = array();
 
+	var	$index = array(
+			'getForums'
+		);
+
 
 	function actions_beta5($game) {
 		$this->game	= $game;
diff -Naur beta5//scripts/game/beta5/aforums/library.inc forums//scripts/game/beta5/aforums/library.inc
--- beta5//scripts/game/beta5/aforums/library.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/game/beta5/aforums/library.inc	2011-02-05 10:10:03.334335002 +0100
@@ -0,0 +1,256 @@
+<?php
+
+/** Beta 5 alliance forums management library */
+
+class beta5_aforums_library {
+
+	private static $allianceAdmins = array();
+	private static $rAccess = array();
+
+	var $index = array( );
+
+	public function __construct($lib) {
+		$this->lib	= $lib;
+		$this->game	= $this->lib->game;
+		$this->version	= $this->game->version;
+		$this->db	= $this->game->db;
+
+		$this->fLib	= $this->game->getLib('main/forums');
+		loader::needClasses('main/forums', array('fl_category', 'fl_forum'));
+
+		$this->getPlayerQ = $this->db->prepare(
+			"SELECT id FROM player WHERE userid = $1 AND (quit IS NULL OR quit > UNIX_TIMESTAMP(NOW()))",
+			array("user"));
+		$this->getAllianceCatQ = $this->db->prepare(
+			"SELECT a.f_category FROM player p, alliance a "
+				. "WHERE a.id = p.alliance AND p.alliance IS NOT NULL AND p.a_status='IN ' AND p.id = $1",
+			array("player") );
+		$this->getAllianceQ = $this->db->prepare(
+			"SELECT a.tag, a.name FROM alliance a WHERE a.f_category = $1",
+			array("category") );
+		$this->getCatForumsQ = $this->db->prepare(
+			"SELECT id FROM forums.t_forum WHERE category = $1 ORDER BY f_order", array("category") );
+		$this->getForumPrivQ = $this->db->prepare(
+			"SELECT * FROM get_aforums_privs( $1, $2 )", array("player", "forum") );
+		$this->getForumQ = $this->db->prepare(
+			"SELECT * FROM alliance_forum WHERE forum = $1", array("forum") );
+		$this->getForumRanksQ = $this->db->prepare(
+			"SELECT rank, is_mod FROM al_rank_forum WHERE forum = $1", array("forum") );
+		$this->getRankNameQ = $this->db->prepare(
+			"SELECT CASE name IS NULL WHEN TRUE THEN '-' ELSE name END FROM alliance_grade "
+				. "WHERE id = $1",
+			array("rank") );
+	}
+
+	public function getStructure( $user ) {
+		$cats = array();
+
+		$q = $this->getPlayerQ->execute($user);
+		if ($q && dbCount($q) == 1) {
+			list($player) = dbFetchArray($q);
+
+			$q = $this->getAllianceCatQ->execute($player);
+			if ($q && dbCount($q)) {
+				list($catId) = dbFetchArray($q);
+				$cat = $this->fLib->call('getCategory', $catId, $user);
+				if (! is_null($cat)) {
+					array_push($cats, $cat);
+				}
+			}
+		}
+
+		return $cats;
+	}
+
+	public function getCategory( $catId , $user ) {
+		$q = $this->getAllianceQ->execute($catId);
+		if (! ($q && dbCount($q) == 1)) {
+			return null;
+		}
+		list($tag, $name) = dbFetchArray($q);
+
+		$title = "[$tag] $name";
+		$description = "These are the forums for the alliance you are a member of, $name.";
+
+		$cat = new fl_category( $this->game, $user, $catId, $title, $description );
+
+		$q = $this->getCatForumsQ->execute($catId);
+		while ($r = dbFetchArray($q)) {
+			$forum = $this->fLib->call('getForum', $r[0], $user);
+			if ($forum->canView() && ($forum->isAdmin() || ! $forum->isDeleted())) {
+				$cat->addForum( $forum );
+			}
+		}
+
+		return $cat;
+	}
+
+
+	public function getForum( $forumId, $user ) {
+		$q = $this->getPlayerQ->execute($user);
+		if (!($q && dbCount($q) == 1)) {
+			return null;
+		}
+		list($player) = dbFetchArray($q);
+
+		$q = $this->getForumPrivQ->execute($player, $forumId);
+		if (! ($q && dbCount($q) == 1)) {
+			return null;
+		}
+		$privs = dbFetchHash($q);
+
+		try {
+			$forum = new fl_forum( $this->game, $forumId, $user );
+		} catch (Exception $e) {
+			return null;
+		}
+
+		$forum->setAdmin( $privs['is_admin'] == 't' );
+		$forum->setMod( $privs['is_mod'] == 't' );
+		$forum->setCreateTopic( $privs['can_create'] == 't' );
+		$forum->setCreatePoll( $privs['can_poll'] == 't' );
+		$forum->setPost( $privs['can_post'] == 't' );
+		$forum->setView( $privs['can_view'] == 't' );
+
+		return $forum;
+	}
+
+	public function getAdmins($forum) {
+		$q = $this->getForumQ->execute($forum);
+		if (!($q && dbCount($q) == 1)) {
+			return null;
+		}
+		$r = dbFetchHash($q);
+		$alliance = $r['alliance'];
+
+		if (! is_null(self::$allianceAdmins[$alliance])) {
+			return self::$allianceAdmins[$alliance];
+		}
+
+		if (is_null($this->alliance)) {
+			$this->alliance = $this->game->getLib('beta5/alliance');
+		}
+
+		$l = array_keys($this->alliance->call('getRanks', $alliance));
+		$admin = array();
+		foreach	($l as $rId) {
+			$pr = $this->alliance->call('getRankPrivileges', $rId);
+			if (! $pr['forum_admin']) {
+				continue;
+			}
+			array_push($admin, $rId);
+		}
+
+		return (self::$allianceAdmins[$alliance] = $admin);
+	}
+
+	public function getModerators($forum) {
+		$rl = $this->getRankAccess($forum);
+		$rv = array();
+		foreach ($rl as $rId => $isMod) {
+			if ($isMod) {
+				array_push($rv, $rId);
+			}
+		}
+		return $rv;
+	}
+
+	public function getUsers($forum) {
+		$rl = $this->getRankAccess($forum);
+		$rv = array();
+		foreach ($rl as $rId => $isMod) {
+			if (! $isMod) {
+				array_push($rv, $rId);
+			}
+		}
+		return $rv;
+	}
+
+
+	private function getRankAccess($forum) {
+		if (! is_null(self::$rAccess[$forum])) {
+			return self::$rAccess[$forum];
+		}
+
+		$rv = array();
+		$q = $this->getForumRanksQ->execute($forum);
+
+		if ($q && dbCount($q)) {
+			while ($r = dbFetchHash($q)) {
+				$rv[$r['rank']] = ($r['is_mod'] == 't');
+			}
+		}
+
+		return (self::$rAccess[$forum] = $rv);
+	}
+
+
+	public function aclIdToName($id) {
+		$q = $this->getRankNameQ->execute($id);
+		if (!($q && dbCount($q) == 1)) {
+			return null;
+		}
+		list($rv) = dbFetchArray($q);
+		return $rv;
+	}
+
+
+
+	public function getUserPrivileges($forum) {
+		$q = $this->getForumQ->execute($forum);
+		if (!($q && dbCount($q) == 1)) {
+			return null;
+		}
+		$r = dbFetchHash($q);
+		return $r['access_mode'];
+	}
+
+
+	public function create($player, $user, $alliance, $order, $name, $description, $accessMode) {
+		$q = $this->db->query("SELECT create_alliance_forum($1, $2, $3, $4, $5, $6)",
+			$player, $alliance, $order, $name, $description, $accessMode);
+		if (!($q && dbCount($q))) {
+			return -6;
+		}
+		list($rv) = dbFetchArray($q);
+
+		if ($rv > 0) {
+			$forum = $this->fLib->call('getForum', $rv, $user);
+			if (is_null($forum)) {
+				return -6;
+			}
+			$forum->getCategory()->insertNewForum($forum);
+			$rv = $forum;
+		} else {
+			$rv = -$rv;
+		}
+
+		return $rv;
+	}
+
+	public function modifyForum($forum, $player, $name, $description, $accessMode) {
+		$this->db->query("SELECT modify_alliance_forum($1,$2,$3,$4,$5)",
+			$player, $forum,  $name, $description, $accessMode);
+		if (!($q && dbCount($q))) {
+			return -5;
+		}
+		list($rv) = dbFetchArray($q);
+		return $rv;
+	}
+
+	public function clearForumACL($forum) {
+		$this->db->query("DELETE FROM al_rank_forum WHERE forum = $1", $forum);
+	}
+
+	public function addForumModerator( $forum, $id ) {
+		$this->db->query("INSERT INTO al_rank_forum (rank, forum, is_mod) VALUES ($1, $2, TRUE)",
+			$id, $forum);
+	}
+
+	public function addForumUser( $forum, $id ) {
+		$this->db->query("INSERT INTO al_rank_forum (rank, forum, is_mod) VALUES ($1, $2, FALSE)",
+			$id, $forum);
+	}
+}
+
+?>
diff -Naur beta5//scripts/game/beta5/forums/topic/library.inc forums//scripts/game/beta5/forums/topic/library.inc
--- beta5//scripts/game/beta5/forums/topic/library.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/game/beta5/forums/topic/library.inc	2011-02-05 10:10:03.334335002 +0100
@@ -0,0 +1,330 @@
+<?
+
+class beta5_forums_topic_library {
+
+	private $pHandler;
+	private	$lib;
+	private $game;
+	private $topic = null;
+	private $deleted = false;
+
+	public function __construct($lib) {
+		$this->lib	= $lib;
+		$this->game	= $this->lib->game;
+	}
+
+	public function initialize($pageHandler) {
+		$this->pHandler = $pageHandler;
+	}
+
+	public function forumCommand($player, $commandArgs) {
+
+		// Read the arguments
+		if (strstr($commandArgs, "#") !== FALSE) {
+			list( $topicIdS, $hexId ) = explode('#', $commandArgs);
+			if (strlen($hexId) != 32 || preg_match('/[^A-Z0-9]/i', $hexId)) {
+				$hexId = md5(uniqid(rand()));
+			}
+
+			$topicId = (int) $topicIdS;
+			$data = $this->pHandler->getSessionData('topic_' . $hexId);
+			if (is_null($data) || $topicId != $data['topic']) {
+				$hexId = md5(uniqid(rand()));
+				$firstTime = true;
+			} else {
+				$firstTime = false;
+			}
+		} else {
+			$topicId = (int) $commandArgs;
+			$hexId = md5(uniqid(rand()));
+			$firstTime = true;
+		}
+
+		// Get the player's user ID
+		$pinf = $this->game->getLib('beta5/player')->call('get', $player);
+		$user = $pinf['uid'];
+
+		if ($firstTime) {
+			$this->game->action('getForums', $player);
+			$data = $this->initTopicData($user, $topicId);
+			$this->makeDataString($data, $hexId);
+		} else {
+			$this->updateTopicData($topicId, $data);
+		}
+		$this->pHandler->setSessionData('topic_' . $hexId, $data);
+		$this->data = $data;
+
+		if ($data['exists']) {
+			$rv = array("V#F#{$data['forum']}", "vTopic", "$topicId#$hexId");
+		} else {
+			$rv = array("", "vTopic", "$topicId#$hexId");
+		}
+		return $rv;
+	}
+
+
+	/* This method initialises the stored session data for the specified topic ID.
+	 */
+	private function initTopicData($user, $topicId) {
+		$data = array(
+			"viewTime"	=> time(),
+			"topic"		=> $topicId,
+			"user"		=> $user
+		);
+
+		// Check whether the topic actually exists
+		$topic = $this->game->getLib('main/forums')->call('getTopic', $topicId, $user);
+		if (is_null($topic)) {
+			$data['exists'] = false;
+			return $data;
+		}
+
+		// Check whether the topic is available
+		$forum = $this->forum = $topic->getForum();
+		if (is_null($forum) || ! $forum->canView()) {
+			$data['exists'] = false;
+			return $data;
+		}
+
+		// The topic exists
+		$data['exists'] = true;
+		$data['forum'] = $forum->getId();
+		$data['title'] = $topic->getTitle();
+
+		// Has it been deleted?
+		if ($topic->isDeleted()) {
+			$data['deleted'] = true;
+			$data['deletedAt'] = $topic->getDeletionTime();
+			$data['deletedBy'] = $topic->getDeletionMod();
+			return $data;
+		}
+
+		// Get additional data
+		$users = array();
+		$data['lastChange'] = $topic->getLastChange();
+		$data['lastRead'] = $topic->getLastRead();
+		$data['isLocked'] = $topic->isLocked();
+		$data['isMod'] = $forum->isMod();
+		$data['canPost'] = $forum->canPost();
+
+		// Get the topic's posts
+		$data['postOrder'] = array();
+
+		// -> linear, newest first
+		$list = $topic->getPostList(false, false);
+		$data['postOrder']['ln'] = array();
+		foreach ($list as $p) {
+			array_push($data['postOrder']['ln'], $p->getId());
+		}
+
+		// -> linear, oldest first
+		$list = $topic->getPostList(false, true);
+		$data['postOrder']['lo'] = array();
+		foreach ($list as $p) {
+			array_push($data['postOrder']['lo'], $p->getId());
+		}
+
+		// -> threaded, newest first
+		$list = $topic->getPostList(true, false);
+		$data['postOrder']['tn'] = array();
+		foreach ($list as $p) {
+			array_push($data['postOrder']['tn'], $p->getId());
+		}
+
+		// -> threaded, oldest first
+		$list = $topic->getPostList(true, true);
+		$data['postOrder']['to'] = array();
+		foreach ($list as $p) {
+			array_push($data['postOrder']['to'], $p->getId());
+		}
+
+		// -> post data
+		$data['posts'] = array();
+		foreach ($list as $p) {
+			// post_id # depth # author_id # posted_at # is_unread # lc_time # lc_author
+			// post_title
+			$pStr = $p->getId() . "#" . $p->getDepth() . "#" . $p->getPostedBy()
+				. "#" . $p->getPostedAt() . "#"  . ($p->isUnread() ? 1 : 0)
+				. "#" . $p->getLastChange() . "#" . $p->getLastChangeAuthor()
+				. "\n" . utf8entities($p->getTitle());
+			array_push($data['posts'], $pStr);
+			if (!in_array($p->getPostedBy(), $users)) {
+				array_push($users, $p->getPostedBy());
+			}
+			if (!(is_null($p->getLastChangeAuthor()) || in_array($p->getLastChangeAuthor(), $users))) {
+				array_push($users, $p->getLastChangeAuthor());
+			}
+		}
+
+		// Get poll data
+		$poll = $topic->getPoll();
+		$data['hasPoll'] = ! is_null($poll);
+		if ($data['hasPoll']) {
+			// FIXME: get poll data
+		}
+
+		// Get user names
+		$data['users'] = $this->dumpNames($users);
+
+		return $data;
+	}
+
+	private function dumpNames($ids) {
+		$accLib = $this->game->getLib('main/account');
+
+		$output = array();
+		array_push($output, count($ids));
+		foreach ($ids as $id) {
+			array_push($output, "$id#" . utf8entities($accLib->call('getUserName', $id)));
+		}
+		return $output;
+	}
+
+	/* This method generates the data string to be sent the first time a topic
+	 * page is loaded.
+	 */
+	private function makeDataString(&$data, $hexId) {
+		$result = array(
+			"{$data['topic']}#$hexId"
+		);
+		if ($data['exists']) {
+			// Fetch the path to the topic
+			$parents = array();
+			$obj = $this->forum;
+			do {
+				array_push($parents, array(
+					$obj->getId(), utf8entities($obj->getTitle())
+				));
+				$obj = $obj->getParent();
+			} while (! is_null($obj));
+
+			if ($data['deleted']) {
+				// If the topic has been deleted
+				array_push($result, "DELETED#{$data['deletedAt']}#" . count($parents));
+				array_push($result, utf8entities($data['title']));
+				// FIXME: fetch moderator's name
+//				array_push($result, $this->game->getLib('
+
+				// Dump the path
+				foreach (array_reverse($parents) as $p) {
+					array_push($result, join('#', $p));
+				}
+
+			} else {
+				// If the topic is available
+				array_push($result, "TOPIC#" . count($parents));
+				array_push($result, utf8entities($data['title']));
+				array_push($result, "{$data['user']}#" . ($data['isMod'] ? 1 : 0)
+					. "#" . ($data['canPost'] ? 1 : 0) . "#" . ($data['hasPoll'] ? 1 : 0));
+
+				// Dump the path
+				foreach (array_reverse($parents) as $p) {
+					array_push($result, join('#', $p));
+				}
+
+				// Post orders
+				array_push($result, join('#', $data['postOrder']['ln']));
+				array_push($result, join('#', $data['postOrder']['lo']));
+				array_push($result, join('#', $data['postOrder']['tn']));
+				array_push($result, join('#', $data['postOrder']['to']));
+
+				// Posts
+				foreach ($data['posts'] as $post) {
+					array_push($result, $post);
+				}
+
+				// Users
+				foreach ($data['users'] as $user) {
+					array_push($result, $user);
+				}
+
+				// Get the viewing options
+				array_push($result, join('#', $this->getOptions($data['topic'])));
+			}
+		} else {
+			array_push($result, "MEH");
+		}
+
+		$data['initString'] = join("\n", $result);
+	}
+
+	/** This method checks for updates on the topic */
+	private function updateTopicData($topicId, &$data) {
+		$this->topic = $topic = $this->game->getLib('main/forums')->call('getTopic', $topicId, $user);
+		if (is_null($topic)) {
+			$data['stillExists'] = false;
+			return $data;
+		}
+
+		// Check whether the topic is available
+		$forum = $this->forum = $topic->getForum();
+		if (is_null($forum) || ! $forum->canView()) {
+			$data['stillExists'] = false;
+			return $data;
+		}
+
+		// The topic exists
+		$data['stillExists'] = true;
+
+		// Has it been deleted?
+		if ($topic->isDeleted()) {
+			$data['nowDeleted'] = true;
+			$data['deletedAt'] = $topic->getDeletionTime();
+			$data['deletedBy'] = $topic->getDeletionMod();
+			return $data;
+		}
+
+		// Topic hasn't been deleted, check for updates
+		$data['lastChange'] = $topic->getLastChange();
+	}
+
+	public function getData() {
+		if (is_null($this->data['updateString'])) {
+			return $this->data['initString'];
+		}
+	}
+
+
+	private function getOptions( $id ) {
+		if (! is_null($this->forum)) {
+			$isMod = $this->forum->isMod();
+		} else {
+			$isMod = false;
+		}
+
+		// perPage = posts / page, default 50, possible values 25 50 75 100
+		$perPage = prefs::get("main/T#PP#$id", prefs::get("main/T#PP", 50));
+		// vDeleted = view deleted posts, default "no"
+		$vDeleted = $isMod ? prefs::get("main/T#VD#$id", prefs::get("main/T#VD", 0)) : 0;
+		// threaded = enable threaded mode, default "yes"
+		$threaded = prefs::get("main/T#TV#$id", prefs::get("main/T#TV", 1));
+		// order = show oldest posts first, default "no"
+		$order = prefs::get("main/T#PO#$id", prefs::get("main/T#PO", 0));
+		// openPosts = posts open by default, default 1, possible values 0: none, 1: new only, 2: all
+		$openPosts = prefs::get("main/T#OP#$id", prefs::get("main/T#OP", 1));
+
+		return array($perPage, $vDeleted, $threaded, $order, $openPosts);
+	}
+
+
+	public function getPostContents($postId) {
+		$postId = (int) $postId;
+		if (! ($this->data['exists'] && $this->data['stillExists'])) {
+			return "-#$postId";
+		}
+
+		$post = $this->topic->getPostById($postId);
+		if (is_null($post)) {
+			return "-#$postId";
+		}
+
+		$contents = $this->game->getLib('main/forums')->call('substitute',
+			$post->getContents($this->data['viewTime']),
+			$post->codeEnabled($this->data['viewTime'])
+		);
+
+		return "+#$postId\n$contents";
+	}
+}
+
+?>
diff -Naur beta5//scripts/game/beta5/forums/view/library.inc forums//scripts/game/beta5/forums/view/library.inc
--- beta5//scripts/game/beta5/forums/view/library.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/game/beta5/forums/view/library.inc	2011-02-05 10:10:03.334335002 +0100
@@ -0,0 +1,405 @@
+<?
+
+class beta5_forums_view_library {
+
+	private $pHandler;
+	private $lib;
+	private $game;
+	private $fStructure;
+	private $displayObject;
+
+	public function __construct($lib) {
+		$this->lib	= $lib;
+		$this->game	= $this->lib->game;
+	}
+
+	public function initialize($pageHandler) {
+		$this->pHandler = $pageHandler;
+	}
+
+	public function forumCommand($player, $commandArgs) {
+		// Examine the arguments
+		$args = explode('#', $commandArgs);
+		if (count($args) != 2 || ! in_array($args[0], array('F', 'C'))) {
+			$args = array('C', '/');
+		}
+
+		// Try to find the requested object
+		$this->fStructure = $this->game->action('getForums', $player);
+		if ($args[0] == 'C') {
+			$this->displayObject = $this->fStructure->findCategory($args[1]);
+		} else {
+			$this->displayObject = $this->fStructure->findForum($args[1]);
+			if (! is_null($this->displayObject) && (
+					($this->displayObject->isDeleted() && ! $this->displayObject->isAdmin())
+						|| ! $this->displayObject->canView()) ) {
+				$this->displayObject = null;
+			}
+		}
+		if (is_null($this->displayObject)) {
+			$this->displayObject = $this->fStructure;
+			$args = array('C', '/');
+		}
+
+		$view = join('#', $args);
+		return array("V#$view", $args[0] == 'C' ? 'vCat' : 'vForum', $view);
+	}
+
+	public function getData($oldMD5 = null) {
+		if (is_null($this->displayObject)) {
+			$r = null;
+		} else {
+			if ($this->displayObject instanceof fl_forum) {
+				$r = $this->getForumData();
+			} else {
+				$r = $this->getCategoryData();
+			}
+			$md5 = md5(serialize($r));
+			if ($md5 != $oldMD5) {
+				array_unshift($r, $md5);
+				$r = join("\n", $r);
+			} else {
+				$r = '-';
+			}
+		}
+		return $r;
+	}
+
+	public function categoryRead($category) {
+		if (is_null($this->displayObject)) {
+			return;
+		}
+
+		$cat = $this->displayObject->findCategory((int) $category);
+		if (is_null($cat) || ! ($cat instanceof fl_category)) {
+			return;
+		}
+		$cat->markRead();
+	}
+
+	public function forumRead() {
+		if (is_null($this->displayObject) || ! $this->displayObject instanceof fl_forum ) {
+			return;
+		}
+
+		$this->displayObject->markRead();
+	}
+
+	public function restoreTopics($topicList) {
+		if (is_null($this->displayObject) || ! $this->displayObject instanceof fl_forum
+				|| ! $this->displayObject->isMod() ) {
+			return;
+		}
+
+		foreach ($topicList as $topicId) {
+			$topic = $this->displayObject->findTopic($topicId);
+			if (! is_null($topic) && $topic->isInForum($this->displayObject)) {
+				$topic->restore();
+			}
+		}
+		$this->displayObject->refresh();
+	}
+
+	public function deleteTopics($topicList) {
+		if (is_null($this->displayObject) || ! $this->displayObject instanceof fl_forum
+				|| ! $this->displayObject->isMod() ) {
+			return;
+		}
+
+		foreach ($topicList as $topicId) {
+			$topic = $this->displayObject->findTopic($topicId);
+			if (!is_null($topic) && $topic->isInForum($this->displayObject)) {
+				$topic->delete();
+			}
+		}
+		$this->displayObject->refresh();
+	}
+
+	public function changeTopicsLevel($topicList, $change) {
+		$change = (int) $change;
+		if (is_null($this->displayObject) || ! $this->displayObject instanceof fl_forum
+				|| ! $this->displayObject->isMod() || $change != -1 && $change != 1 ) {
+			return;
+		}
+
+		foreach ($topicList as $topicId) {
+			$topic = $this->displayObject->findTopic($topicId);
+			if (!is_null($topic) && $topic->isInForum($this->displayObject)) {
+				$topic->setStickyLevel($topic->getStickyLevel() + $change);
+			}
+		}
+		$this->displayObject->refresh();
+	}
+
+	public function setTopicsLevel($topicList, $level) {
+		$level = (int) $level;
+		if (is_null($this->displayObject) || ! $this->displayObject instanceof fl_forum
+				|| ! $this->displayObject->isMod() || $level < 0 || $level > 10) {
+			return;
+		}
+
+		foreach ($topicList as $topicId) {
+			$topic = $this->displayObject->findTopic($topicId);
+			if (!is_null($topic) && $topic->isInForum($this->displayObject)) {
+				$topic->setStickyLevel($level);
+			}
+		}
+		$this->displayObject->refresh();
+	}
+
+	public function setTopicsLock($topicList, $lock) {
+		if (is_null($this->displayObject) || ! $this->displayObject instanceof fl_forum
+				|| ! $this->displayObject->isMod()) {
+			return;
+		}
+
+		foreach ($topicList as $topicId) {
+			$topic = $this->displayObject->findTopic($topicId);
+			if (!is_null($topic) && $topic->isInForum($this->displayObject)) {
+				$topic->setLock($lock);
+			}
+		}
+	}
+
+	public function moveTopics($topicList, $destId) {
+		if (is_null($this->displayObject) || ! $this->displayObject instanceof fl_forum
+				|| ! $this->displayObject->isMod() || $destId == $this->displayObject->getId()) {
+			return;
+		}
+
+		$destination = $this->fStructure->findForum((int) $destId);
+		if (is_null($destination) || ! $destination->isMod()) {
+			return;
+		}
+
+		foreach ($topicList as $topicId) {
+			$topic = $this->displayObject->findTopic($topicId);
+			if (! is_null($topic) && $topic->isInForum($this->displayObject)) {
+				$topic->moveTo($destination);
+			}
+		}
+		$this->displayObject->refresh();
+		$destination->refresh();
+	}
+
+
+	private function dumpNames(&$output, $ids) {
+		$accLib = $this->game->getLib('main/account');
+
+		array_push($output, count($ids));
+		foreach ($ids as $id) {
+			array_push($output, "$id#" . utf8entities($accLib->call('getUserName', $id)));
+		}
+	}
+
+	private function getCategoryData() {
+		$output = array(
+			"C#" . $this->displayObject->getId()
+		);
+		$namesNeeded = array();
+
+		$this->dumpCategory( &$output, &$namesNeeded, $this->displayObject );
+		$this->dumpNames( &$output, $namesNeeded );
+
+		return $output;
+	}
+
+	private function dumpCategory( &$output, &$namesNeeded, $object ) {
+		if ( $object instanceof fl_container ) {
+			$contents = $object->getCategories();
+			$type = 'C';
+		} else {
+			$contents = array();
+			foreach ($object->getForums() as $forum) {
+				if ($forum->canView() && ($forum->isAdmin() || ! $forum->isDeleted())) {
+					array_push($contents, $forum);
+				}
+			}
+			$type = 'F';
+		}
+
+		$fullDesc = explode("\n", $object->getDescription());
+		$hasUnread = ($object->getUnread() > 0) ? 1 : 0;
+		array_push( $output, $object->getId() . "#$type#$hasUnread#"
+			. count($contents) . "#" . count($fullDesc) );
+		if ($type == 'F') {
+			array_push( $output, $object->getTypeName(getLanguage()) ); // FIXME getLanguage() ...
+		}
+		array_push( $output, utf8entities($object->getTitle()) );
+		foreach ($fullDesc as $dLine ) {
+			array_push( $output, utf8entities($dLine) );
+		}
+
+		if ( $object instanceof fl_container ) {
+			foreach ($contents as $cat) {
+				$this->dumpCategory( &$output, &$namesNeeded, $cat );
+			}
+		} else {
+			foreach ($contents as $forum) {
+				$this->dumpCatForum( &$output, &$namesNeeded, $forum );
+			}
+		}
+	}
+
+	private function dumpCatForum( &$output, &$namesNeeded, $forum ) {
+		$fullDesc = explode("\n", $forum->getDescription());
+
+		$deleted = $forum->isDeleted() ? '1' : '0';
+		$deletedAt = $forum->deletedAt();
+		$deletedBy = $forum->deletedBy();
+		if (! (is_null($deletedBy) || in_array($deletedBy, $namesNeeded))) {
+			array_push($namesNeeded, $deletedBy);
+		}
+
+		$topics = $forum->getTopics();
+		$posts = is_null($lp = $forum->getLastPost()) ? 0 : $forum->getPosts();
+		$unread = ($forum->getUnread() > 0 ? 1 : 0);
+
+		array_push( $output, $forum->getId() . "#" . count($fullDesc)
+			. "#$deleted#$deletedAt#$deletedBy#$topics#$posts#$unread" );
+
+		if ($posts != 0 && !is_null($lp)) {
+			$moment = $lp->getLastChange();
+			$author = $lp->getLastChangeAuthor();
+			if (! in_array($author, $namesNeeded) ) {
+				array_push($namesNeeded, $author);
+			}
+			array_push( $output, "$author#$moment" );
+		}
+
+		array_push( $output, utf8entities($forum->getTitle()) );
+		foreach ($fullDesc as $dLine ) {
+			array_push( $output, utf8entities($dLine) );
+		}
+	}
+
+	private function getForumData() {
+		$output = array(
+			"F#" . $this->displayObject->getId()
+		);
+		$namesNeeded = array();
+		$fob = $this->displayObject;
+		$id = $fob->getId();
+
+		$perPage = prefs::get("main/F#PP#$id", prefs::get("main/F#PP", 10));
+		$vDeleted = $fob->isMod() ? prefs::get("main/F#VD#$id", prefs::get("main/F#VD", 0)) : 0;
+
+		$rTopics = $fob->getTopicList();
+		$topics = array();
+		foreach ($rTopics as $topic) {
+			if ( ! ($topic->isInForum($fob) || !is_null($topic->getForum()) && $topic->getForum()->canView()) 
+				|| ($topic->isDeleted() && ! $vDeleted) ) {
+				continue;
+			}
+			array_push($topics, $topic);
+		}
+
+		$nTopics = count($topics);
+		$hasUnread = ($fob->getUnread() > 0) ? 1 : 0;
+		$isMod = $fob->isMod() ? 1 : 0;
+		$canPost = $fob->canCreateTopic() ? 1 : 0;
+		$fDesc = explode("\n", $fob->getDescription());
+		$admins = $fob->getAdministrators(true);
+		$mods = $fob->getModerators(true);
+		$users = $fob->getUsers(true);
+
+		if ($fob->isMod()) {
+			$fMoveTo = array();
+			$list = $this->fStructure->findModForums();
+			foreach ($list as $forum) {
+				$fId = $forum->getId();
+				if ($fId == $id) {
+					continue;
+				}
+				$fName = array($forum->getParent()->getTitle(), $forum->getTitle());
+				array_push($fMoveTo, "$fId#" . utf8entities(join(' > ', $fName)));
+			}
+		}
+
+		$parents = array();
+		$obj = $fob->getParent();
+		do {
+			array_push($parents, array(
+				$obj->getId(), utf8entities($obj->getTitle())
+			));
+			$obj = $obj->getParent();
+		} while (! is_null($obj));
+
+		array_push($output, "$id#$nTopics#$hasUnread#$isMod#$canPost#$perPage#$vDeleted#" . count($fDesc)
+				. "#" . count($parents) . "#" . count($admins) . "#" . count($mods)
+				. "#" . count($users) . ($isMod ? ("#" . count($fMoveTo)) : ""));
+		array_push($output, utf8entities($fob->getTitle()));
+
+		// Dump description
+		foreach ($fDesc as $dLine) {
+			array_push($output, utf8entities($dLine));
+		}
+		// Parent categories
+		foreach (array_reverse($parents) as $p) {
+			array_push($output, join('#', $p));
+		}
+		// Administrators
+		foreach ($admins as $name) {
+			array_push($output, utf8entities($name));
+		}
+		// Moderators
+		foreach ($mods as $name) {
+			array_push($output, utf8entities($name));
+		}
+		// Users
+		foreach ($users as $name) {
+			array_push($output, utf8entities($name));
+		}
+		// List of forums one can move a topic to
+		if ($isMod) {
+			foreach ($fMoveTo as $l) {
+				array_push($output, $l);
+			}
+		}
+
+		foreach ($topics as $topic) {
+			if ($topic->isDeleted() && ! $vDeleted) {
+				continue;
+			}
+
+			$tid = $topic->getId();
+			$movedTo = $topic->isInForum($fob) ? '' : $topic->getForum()->getId();
+			$unread = ($topic->getLastRead() < $topic->getLastChange()) ? 1 : 0;
+			$sticky = $topic->getStickyLevel();
+			$tReplies = $topic->getPosts() - 1;
+
+			$fpTime = $topic->getPosted();
+			$fpAuth = $topic->getAuthor();
+			if (! (is_null($fpAuth) || in_array($fpAuth, $namesNeeded))) {
+				array_push($namesNeeded, $fpAuth);
+			}
+			$lcTime = $topic->getLastChange();
+			$lcAuth = $topic->getLastChangeAuthor();
+			if (! (is_null($lcAuth) || in_array($lcAuth, $namesNeeded))) {
+				array_push($namesNeeded, $lcAuth);
+			}
+
+			$isLocked = $topic->isLocked() ? 1 : 0;
+			$hasPoll = is_null($topic->getPoll()) ? 0 : 1;
+
+			$isDeleted = $topic->isDeleted() ? 1 : 0;
+			$deletedAt = $topic->getDeletionTime();
+			$deletedBy = $topic->getDeletionMod();
+			if (! (is_null($deletedBy) || in_array($deletedBy, $namesNeeded))) {
+				array_push($namesNeeded, $deletedBy);
+			}
+
+			array_push($output, "$tid#$movedTo#$unread#$sticky#$tReplies#$fpTime#$fpAuth#$lcTime#$lcAuth"
+				. "#$isLocked#$hasPoll#$isDeleted#$deletedAt#$deletedBy");
+			array_push($output, utf8entities($topic->getTitle()));
+			if ($movedTo != '') {
+				array_push($output, utf8entities($topic->getForum()->getTitle()));
+			}
+		}
+
+		$this->dumpNames( &$output, $namesNeeded );
+		return $output;
+	}
+}
+
+?>
diff -Naur beta5//scripts/game/main/forums/fl_category.inc forums//scripts/game/main/forums/fl_category.inc
--- beta5//scripts/game/main/forums/fl_category.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/game/main/forums/fl_category.inc	2011-02-05 10:10:03.434335002 +0100
@@ -0,0 +1,270 @@
+<?php
+
+/* Category management class */
+
+class fl_category {
+
+	private static $getLibQuery = null;
+	private static $getNameQuery = null;
+
+	private $game;
+	private $mainLib = null;
+	private $hdlLib = null;
+
+	private $parent = null;
+	private $user;
+	private $id;
+	private $title;
+	private $description;
+
+	private $forums = array();
+	private $oForums = array();
+	private $unread = null;
+	private $topics = null;
+	private $posts = null;
+
+	public function __construct( $game, $user, $id, $title, $description = null ) {
+		self::initQueries();
+
+		$this->game = $game;
+		$this->db = $this->game->getDBAccess();
+
+		$this->user = $user;
+		$this->id = $id;
+		$this->title = $title;
+		$this->description = $description;
+	}
+
+	private static function initQueries() {
+		if (!is_null(self::$getLibQuery)) {
+			return;
+		}
+
+		$db = config::getMainInterface()->getDBAccess();
+		self::$getLibQuery = $db->prepare(
+			"SELECT t.id AS id,t.lib_path AS path "
+				.  "FROM forums.category_type t, forums.category c "
+				. "WHERE c.id = $1 AND t.id = c.acl_lib",
+			array("id") );
+		self::$getNameQuery = $db->prepare(
+			"SELECT name FROM forums.cat_type_text "
+				. "WHERE id = $1 AND lang = $2",
+			array("id", "lang") );
+	}
+
+	public function findCategory( $catId ) {
+		if ($this->getId() == $catId) {
+			return $this;
+		}
+		return null;
+	}
+
+
+	// ----------------------------------------------------------------------
+	// READING CATEGORY PROPERTIES
+	// ----------------------------------------------------------------------
+
+	public function getId() {
+		return $this->id;
+	}
+
+	public function getTitle() {
+		return $this->title;
+	}
+
+	public function getDescription() {
+		return $this->description;
+	}
+
+	public function getLibrary() {
+		if (is_null($this->hdlLib)) {
+			$q = self::$getLibQuery->execute(array("id" => $this->id));
+			if (! ($q && dbCount($q) == 1)) {
+				return null;
+			}
+
+			$r = dbFetchHash($q);
+			$r['library'] = $this->game->getLib($r['path']);
+			$r['text'] = array();
+			$this->hdlLib = $r;
+		}
+		return $this->hdlLib['library'];
+	}
+
+	public function getTypeName($lang) {
+		if (is_null($this->hdlLib)) {
+			if (is_null($this->getLibrary())) {
+				return null;
+			}
+		}
+
+		if (! isset($this->hdlLib['text'][$lang]) ) {
+			$q = self::$getNameQuery->execute(array(
+				"id"	=> $this->hdlLib['id'],
+				"lang"	=> $lang
+			));
+			if (! ($q || dbCount($q) == 1)) {
+				return null;
+			}
+			list($this->hdlLib['text'][$lang]) = dbFetchArray($q);
+		}
+
+		return $this->hdlLib['text'][$lang];
+	}
+
+	public function getUnread( ) {
+		if (!is_null($this->unread)) {
+			return $this->unread;
+		}
+
+		$total = 0;
+		foreach ($this->forums as $f) {
+			if ( $f->canView() && ! $f->isDeleted() ) {
+				$total += $f->getUnread();
+			}
+		}
+		return ($this->unread = $total);
+	}
+
+	public function getTopics( ) {
+		if (!is_null($this->topics)) {
+			return $this->topics;
+		}
+
+		$total = 0;
+		foreach ($this->forums as $f) {
+			if ( $f->canView() && ! $f->isDeleted() ) {
+				$total += $f->getTopics();
+			}
+		}
+		return ($this->topics = $total);
+	}
+
+	public function getPosts( ) {
+		if (!is_null($this->posts)) {
+			return $this->posts;
+		}
+
+		$total = 0;
+		foreach ($this->forums as $f) {
+			if ( $f->canView() && ! $f->isDeleted() ) {
+				$total += $f->getPosts();
+			}
+		}
+		return ($this->posts = $total);
+	}
+
+	public function setParent( $category ) {
+		$this->parent = $category;
+	}
+
+	public function getParent() {
+		return $this->parent;
+	}
+
+
+	// ----------------------------------------------------------------------
+	// FORUM ACCESS
+	// ----------------------------------------------------------------------
+
+	public function getForums() {
+		return $this->oForums;
+	}
+
+	public function findModForums() {
+		$r = array();
+		foreach ($this->oForums as $f) {
+			if ($f->isMod()) {
+				array_push($r, $f);
+			}
+		}
+		return $r;
+	}
+
+	public function addForum( $forum ) {
+		if (is_null($forum)) {
+			return;
+		}
+		$this->forums[(string) $forum->getId()] = $forum;
+		array_push($this->oForums, $forum);
+		$forum->setParent($this);
+	}
+
+	public function insertNewForum($forum) {
+		if (isset($this->forums[$id])) {
+			return;
+		}
+
+		$this->forums[$forum->getId()] = $forum;
+		$forum->setParent($this);
+		$order = array();
+		for ($i = 0; $i < count($this->oForums); $i ++) {
+			if ($i == $forum->getOrder()) {
+				array_push($order, $forum);
+			}
+			if ($i >= $forum->getOrder()) {
+				$this->oForums[$i]->increaseOrder();
+			}
+			array_push($order, $this->oForums[$i]);
+		}
+		if ($i == $forum->getOrder()) {
+			array_push($order, $forum);
+		}
+		$this->oForums = $order;
+	}
+
+	public function findForum( $id ) {
+		return isset($this->forums[$id]) ? $this->forums[$id] : null;
+	}
+
+	public function moveForum( $id, $moveUp ) {
+		if (!isset($this->forums[$id])) {
+			return false;
+		}
+
+		$qs = "SELECT forums.move_" . ($moveUp ? 'up' : 'down') . '($1)';
+		$q = $this->db->query($qs, $id);
+		if (! ($q && dbCount($q))) {
+			return false;
+		}
+		list($rc) = dbFetchArray($q);
+
+		if ($rc == 't') {
+			$n = count($this->oForums);
+			for ($i = 0; $i < $n; $i ++) {
+				if ($this->oForums[$i]->getId() == $id) {
+					if ($moveUp) {
+						$rv = $this->oForums[$i - 1];
+						$this->oForums[$i - 1] = $this->oForums[$i];
+						$this->oForums[$i] = $rv;
+					} else {
+						$rv = $this->oForums[$i + 1];
+						$this->oForums[$i + 1] = $this->oForums[$i];
+						$this->oForums[$i] = $rv;
+					}
+					break;
+				}
+			}
+		} else {
+			$rv = false;
+		}
+
+		return $rv;
+	}
+
+	public function markRead() {
+		foreach ($this->oForums as $forum) {
+			$forum->markRead();
+		}
+	}
+
+	public function refresh() {
+		$this->unread = $this->topics = $this->posts = null;
+		if ($this->parent) {
+			$this->parent->refresh();
+		}
+	}
+}
+
+
+?>
diff -Naur beta5//scripts/game/main/forums/fl_container.inc forums//scripts/game/main/forums/fl_container.inc
--- beta5//scripts/game/main/forums/fl_container.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/game/main/forums/fl_container.inc	2011-02-05 10:10:03.434335002 +0100
@@ -0,0 +1,116 @@
+<?php
+
+/* Class that represents a pseudo-category that contains sub-categories */
+
+class fl_container {
+
+	private $categories = array();
+	private $parent = null;
+	private $id;
+	private $title;
+	private $description;
+
+	private $topics = null;
+	private $unread = null;
+
+	public function __construct( $id, $title, $description ) {
+		$this->id = $id;
+		$this->title = $title;
+		$this->description = $description;
+	}
+
+	public function addCategory( $category ) {
+		if (! is_null($category)) {
+			array_push($this->categories, $category);
+			$category->setParent( $this );
+		}
+	}
+
+	public function setParent( $category ) {
+		$this->parent = $category;
+	}
+
+	public function getParent() {
+		return $this->parent;
+	}
+
+	public function getCategories() {
+		return $this->categories;
+	}
+
+	public function getId() {
+		return $this->id;
+	}
+
+	public function getTitle() {
+		return $this->title;
+	}
+
+	public function getDescription() {
+		return $this->description;
+	}
+
+	public function getUnread( ) {
+		if (!is_null($this->unread)) {
+			return $this->unread;
+		}
+
+		$total = 0;
+		foreach ($this->categories as $c) {
+			$total += $c->getUnread();
+		}
+		return ($this->unread = $total);
+	}
+
+	public function getTopics( ) {
+		if (!is_null($this->topics)) {
+			return $this->topics;
+		}
+
+		$total = 0;
+		foreach ($this->categories as $c) {
+			$total += $c->getTopics();
+		}
+		return ($this->topics = $total);
+	}
+
+	public function findCategory( $catId ) {
+		if ($this->getId() == $catId) {
+			return $this;
+		}
+		foreach ($this->categories as $cat) {
+			$x = $cat->findCategory( $catId );
+			if (! is_null($x)) {
+				return $x;
+			}
+		}
+		return null;
+	}
+
+	public function findForum( $fId ) {
+		foreach ($this->categories as $cat) {
+			$x = $cat->findForum( $fId );
+			if (! is_null($x)) {
+				return $x;
+			}
+		}
+		return null;
+	}
+
+	public function findModForums() {
+		$r = array();
+		foreach ($this->categories as $cat) {
+			$r = array_merge($r, $cat->findModForums());
+		}
+		return $r;
+	}
+
+	public function refresh() {
+		$this->unread = $this->topics = null;
+		if ($this->parent) {
+			$this->parent->refresh();
+		}
+	}
+}
+
+?>
diff -Naur beta5//scripts/game/main/forums/fl_forum.inc forums//scripts/game/main/forums/fl_forum.inc
--- beta5//scripts/game/main/forums/fl_forum.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/game/main/forums/fl_forum.inc	2011-02-05 10:10:03.434335002 +0100
@@ -0,0 +1,533 @@
+<?php
+
+/* Forum handling class */
+
+class fl_forum {
+
+	private static $queries = null;
+
+	private $game;
+	private $db;
+	private $mainLib = null;
+	private $parent = null;
+
+	private $user;
+	private $record;
+	private $privs;
+
+	private $unread = null;
+	private $topicList = null;
+
+	private $acl = array(
+			"admins"	=> array(),
+			"mods"		=> array(),
+			"users"		=> array()
+		);
+
+	public function __construct($game, $id, $user) {
+		self::initQueries();
+
+		$this->game = $game;
+		$this->db = $this->game->getDBAccess();
+		$this->user = $user;
+
+		$q = self::$queries['getInfo']->execute($id);
+		if (! ($q && dbCount($q) == 1)) {
+			throw new Exception("Unable to read forum record");
+		}
+		$this->record = dbFetchHash($q);
+
+		$this->privs = array(
+			'adm'	=> false,
+			'mod'	=> false,
+			'ntop'	=> false,
+			'poll'	=> false,
+			'post'	=> false,
+			'view'	=> false
+		);
+	}
+
+
+	private static function initQueries() {
+		if (is_array(self::$queries)) {
+			return;
+		}
+
+		$db = config::getMainInterface()->getDBAccess();
+
+		self::$queries['getInfo'] = $db->prepare(
+			"SELECT * FROM forums.forum WHERE id = $1",
+			array("id") );
+		self::$queries['getRead'] = $db->prepare(
+			"SELECT forums.get_read_topics( $1 , $2 )",
+			array("forum", "user") );
+		self::$queries['getCurrentSig'] = $db->prepare(
+			"SELECT id FROM forums.get_signature( $1, UNIX_TIMESTAMP(NOW()) )",
+			array("user") );
+		self::$queries['createTopic'] = $db->prepare(
+			"SELECT forums.create_topic($1, $2, $3, $4, $5, $6, $7, $8)",
+			array("forum", "user", "sticky", "title", "contents", "code", "smileys", "sig") );
+		self::$queries['killTopic'] = $db->prepare(
+			"DELETE FROM forums.t_topic WHERE id = $1",
+			array("topic") );
+		self::$queries['getTopics'] = $db->prepare(
+			"SELECT * FROM forums.topic WHERE forum = $1 OR moved_from = $1",
+			array("forum") );
+	}
+
+
+	// ----------------------------------------------------------------------
+	// READING FORUM PROPERTIES
+	// ----------------------------------------------------------------------
+
+	public function getId() {
+		return $this->record['id'];
+	}
+
+	public function getCategory() {
+		if (is_null($this->mainLib)) {
+			$this->mainLib = $this->game->getLib('main/forums');
+		}
+		return $this->mainLib->call('getCategory', $this->record['category'], $this->user);
+	}
+
+	public function getOrder() {
+		return $this->record['f_order'];
+	}
+
+	public function getTitle() {
+		return $this->record['title'];
+	}
+
+	public function getDescription() {
+		return $this->record['description'];
+	}
+
+	public function isDeleted() {
+		return !is_null($this->record['deleted']);
+	}
+
+	public function deletedAt() {
+		return $this->isDeleted() ? $this->record['deleted'] : null;
+	}
+
+	public function deletedBy() {
+		return $this->isDeleted() ? $this->record['deleted_by'] : null;
+	}
+
+	public function getTopics() {
+		return $this->record['topics'];
+	}
+
+	public function getPosts() {
+		return $this->record['posts'];
+	}
+
+	public function getUnread() {
+		if (is_null($this->unread)) {
+			$q = self::$queries['getRead']->execute($this->record['id'], $this->user);
+			if (! ($q && dbCount($q) == 1)) {
+				return 0;
+			}
+			list($r) = dbFetchArray($q);
+			$this->unread = $this->record['topics'] - $r;
+		}
+		return $this->unread;
+	}
+
+	public function isAdmin() {
+		return $this->privs['adm'];
+	}
+
+	public function isMod() {
+		return $this->privs['mod'];
+	}
+
+	public function canCreateTopic() {
+		return $this->privs['ntop'];
+	}
+
+	public function canCreatePoll() {
+		return $this->privs['poll'];
+	}
+
+	public function canPost() {
+		return $this->privs['post'];
+	}
+
+	public function canView() {
+		return $this->privs['view'];
+	}
+
+	public function getLastPost() {
+		$q = $this->db->query("SELECT * FROM forums.get_last_post($1)", $this->record['id']);
+		if (! ($q && dbCount($q))) {
+			return null;
+		}
+
+		if (! $this->mainLib) {
+			$this->mainLib = $this->game->getLib('main/forums');
+		}
+		return $this->mainLib->call('getPost', dbFetchHash($q), $this->user);
+	}
+
+	public function setParent( $category ) {
+		$this->parent = $category;
+	}
+
+	public function getParent() {
+		return $this->parent;
+	}
+
+
+	// ----------------------------------------------------------------------
+	// FORUM'S ACCESS LIST
+	// ----------------------------------------------------------------------
+
+	private function getAcl($type, $libFunc, $iWantNames) {
+		// Check if we read that already
+		$aKey = $iWantNames ? 'names' : 'ids';
+		if (array_key_exists($aKey, $this->acl[$type])) {
+			return $this->acl[$type][$aKey];
+		}
+
+		// Get the library and extract the ACL IDs
+		$catLib = $this->getCategory()->getLibrary();
+		if (! array_key_exists('ids', $this->acl[$type])) {
+			$this->acl[$type]['ids'] = $catLib->call($libFunc, $this->record['id']);
+		}
+
+		// If we were requesting the IDs, leave
+		if ($aKey == 'ids') {
+			return $this->acl[$type]['ids'];
+		}
+
+		// If the IDs list is null, names are null as well
+		if (is_null($this->acl[$type]['ids'])) {
+			$this->acl[$type]['names'] = null;
+			return $this->acl[$type]['names'];
+		}
+
+		// Convert the identifiers to names and return
+		$this->acl[$type]['names'] = array();
+		foreach ($this->acl[$type]['ids'] as $id) {
+			$this->acl[$type]['names'][$id] = $catLib->call('aclIdToName', $id);
+		}
+		return $this->acl[$type]['names'];
+	}
+
+	public function getAdministrators($iWantNames = false) {
+		return $this->getAcl("admins", "getAdmins", $iWantNames);
+	}
+
+	public function getModerators($iWantNames = false) {
+		return $this->getAcl("mods", "getModerators", $iWantNames);
+	}
+
+	public function getUsers($iWantNames = false) {
+		return $this->getAcl("users", "getUsers", $iWantNames);
+	}
+
+	public function clearACL() {
+		$this->getCategory()->getLibrary()->call('clearForumACL', $this->record['id']);
+		$this->acl = array(
+			'users' => array(),
+			'mods' => array(),
+			'admins' => array()
+		);
+	}
+
+	public function addModerator($id) {
+		$this->getCategory()->getLibrary()->call('addForumModerator', $this->record['id'], $id);
+		$this->acl['mods'] = array();
+	}
+
+	public function addUser($id) {
+		$this->getCategory()->getLibrary()->call('addForumUser', $this->record['id'], $id);
+		$this->acl['users'] = array();
+	}
+
+
+	// ----------------------------------------------------------------------
+	// SETTING FORUM PROPERTIES
+	// ----------------------------------------------------------------------
+
+	public function setAdmin($value) {
+		$this->privs['adm'] = $value;
+	}
+
+	public function setMod($value) {
+		$this->privs['mod'] = $value;
+	}
+
+	public function setCreateTopic($value) {
+		$this->privs['ntop'] = $value;
+	}
+
+	public function setCreatePoll($value) {
+		$this->privs['poll'] = $value;
+	}
+
+	public function setPost($value) {
+		$this->privs['post'] = $value;
+	}
+
+	public function setView($value) {
+		$this->privs['view'] = $value;
+	}
+
+	public function increaseOrder() {
+		$this->record['f_order'] ++;
+	}
+
+
+	// ----------------------------------------------------------------------
+	// INTERNAL METHODS FOR CHECKS AND FORMATTING
+	// ----------------------------------------------------------------------
+
+	private function checkPostContents($title, $contents) {
+		if (strlen($title) < 2) {
+			$e = 1;
+		} elseif (strlen($title) > 100) {
+			$e = 2;
+		} elseif (strlen($contents) < 3) {
+			$e = 3;
+		} else {
+			$e = 0;
+		}
+		return $e;
+	}
+
+
+	private function reformatContents($contents) {
+		// Max. line breaks
+		$maxNL = 500;
+		// Max. characters without break
+		$maxNC = 100;
+
+		$ot = $contents;
+		$nt = "";
+		$nl = 0;
+
+		while ($ot != '' && $nl < $maxNL) {
+			$p = strpos($ot, '\n');
+			if ($p !== false && $p < $maxNC) {
+				$nt .= substr($ot, 0, $p+1);
+				$ot = substr($ot, $p+1);
+			} else if (strlen($ot) < $maxNC) {
+				$nt .= $ot;
+				$ot = "";
+			} else {
+				$s = substr($ot, 0, $maxNC);
+				$p = strrpos($s, ' ');
+				$ot = substr($ot, $maxNC);
+				$nt .= $s;
+				if ($p === false) {
+					$nt .= "\n";
+				}
+			}
+			$nl ++;
+		}
+
+		if ($nl >= $maxNL) {
+			return null;
+		}
+		return $nt;
+	}
+
+
+
+	// ----------------------------------------------------------------------
+	// OPERATIONS
+	// ----------------------------------------------------------------------
+
+	/* This method creates a new topic in the forum. It returns the
+	 * corresponding fl_topic instance or a numeric error code.
+	 *
+	 * Error codes:
+	 *  -1  - Permission denied (topic creation)
+	 *  -2  - Permission denied (poll creation)
+	 *  -3  - Title too short
+	 *  -4  - Title too long
+	 *  -5  - Contents too short
+	 *  -6  - Contents too long
+	 *  -7  - Permission denied (sticky)
+	 *  -8  - Database access error
+	 *  -9  - Poll title too short
+	 * -10  - Poll title too long
+	 * -11  - Not enough poll options
+	 * -12  - Too many poll options
+	 */
+	public function createTopic(
+		$title, $contents, $stickyLevel,
+		$enableCode, $enableSmileys, $enableSignature,
+		$poll = null
+	) {
+		// Check posting privileges
+		if ( ! $this->canCreateTopic() ) {
+			return -1;
+		}
+		if ( ! (is_null($poll) || $this->canCreatePoll()) ) {
+			return -2;
+		}
+
+		// Check the title and contents
+		$title = trim($title);
+		$contents = trim($contents);
+		$pc = $this->checkPostContents($title, $contents);
+		if ($pc) {
+			return - $pc - 2;
+		}
+
+		// Reformat the post's contents
+		$contents = $this->reformatContents($contents);
+		if (is_null($contents)) {
+			return -6;
+		}
+
+		// Check the sticky level
+		$stickyLevel = (int) $stickyLevel;
+		if ( $stickyLevel < 0 || $stickyLevel > 10 || ($stickyLevel > 0 && ! $this->isMod()) ) {
+			return -7;
+		}
+
+		// Check the poll's contents
+		if (! is_null($poll)) {
+			$pc = $poll->checkData();
+			if ($pc != 0) {
+				return - $pc - 8;
+			}
+		}
+
+		// Get the user's current signature if needed
+		if ($enableSignature) {
+			$q = self::$queries['getCurrentSig']->execute($this->user);
+			if ( ! ($q && dbCount($q)) ) {
+				return -8;
+			}
+
+			list($sig) = dbFetchArray($q);
+		} else {
+			$sig = null;
+		}
+
+		// Add the topic
+		$q = self::$queries['createTopic']->execute(array(
+			"forum"		=> $this->record['id'],
+			"user"		=> $this->user,
+			"sticky"	=> $stickyLevel,
+			"title"		=> $title,
+			"contents"	=> $contents,
+			"code"		=> dbBool($enableCode),
+			"smileys"	=> dbBool($enableSmileys),
+			"sig"		=> $sig
+		));
+		if (! ($q && dbCount($q)) ) {
+			return -8;
+		}
+		list($tid) = dbFetchArray($q);
+		if (is_null($tid)) {
+			return -8;
+		}
+
+		// Insert poll if needed
+		if (! (is_null($poll) || $poll->insertIntoDB( $this->game, $this->user, $tid ) ) ) {
+			self::$queries['killTopic']->execute($tid);
+			return -8;
+		}
+
+		if (! $this->mainLib) {
+			$this->mainLib = $this->game->getLib('main/forums');
+		}
+		$topic = $this->mainLib->call('getTopic', $tid, $this->user);
+
+		$this->refresh();
+		return $topic;
+	}
+
+
+	public function getTopicList() {
+		if (is_null($this->topicList)) {
+			$q = self::$queries['getTopics']->execute($this->record['id']);
+			if (! $q) {
+				return null;
+			}
+
+			if (is_null($this->mainLib)) {
+				$this->mainLib = $this->game->getLib('main/forums');
+			}
+			$this->topicList = array();
+			while ($r = dbFetchHash($q)) {
+				$t = $this->mainLib->call('getTopic', $r, $this->user);
+				$this->topicList[$t->getId()] = $t;
+			}
+		}
+
+		return $this->topicList;
+	}
+
+	public function findTopic( $id ) {
+		if (is_null($this->topicList)) {
+			$this->getTopicList();
+		}
+		return array_key_exists($id, $this->topicList) ? $this->topicList[$id] : null;
+	}
+
+	/* Deletes the forum */
+	function delete() {
+		if ($this->isDeleted()) {
+			return;
+		}
+
+		$this->db->query("SELECT forums.delete_forum($1, $2)", $this->record['id'], $this->user);
+		
+		$q = self::$queries['getInfo']->execute($this->record['id']);
+		if ($q && dbCount($q) == 1) {
+			$this->record = dbFetchHash($q);
+		}
+	}
+
+	/* Restore the forum */
+	function restore() {
+		if (!$this->isDeleted()) {
+			return;
+		}
+
+		$this->db->query("SELECT forums.restore_forum($1)", $this->record['id']);
+		
+		$q = self::$queries['getInfo']->execute($this->record['id']);
+		if ($q && dbCount($q) == 1) {
+			$this->record = dbFetchHash($q);
+		}
+	}
+
+	/* Moves the forum either up or down */
+	function move($moveUp) {
+		$rv = $this->getCategory()->moveForum($this->record['id'], $moveUp);
+		if ($rv !== false) {
+			$this->record['f_order'] += ($moveUp ? -1 : 1);
+			$rv->record['f_order'] += ($moveUp ? 1 : -1);
+		}
+	}
+
+	/* Mark all topics as read */
+	function markRead() {
+		$this->db->query("SELECT forums.mark_forum_read($1, $2)", $this->record['id'], $this->user);
+	}
+
+	/* Forces the object to be re-read from the DB */
+	function refresh() {
+		$q = self::$queries['getInfo']->execute($this->record['id']);
+		if (! ($q && dbCount($q) == 1)) {
+			return;
+		}
+		$this->record = dbFetchHash($q);
+		$this->unread = $this->topics = $this->topicList = null;
+
+		if ($this->parent) {
+			$this->parent->refresh();
+		}
+	}
+}
+
+?>
diff -Naur beta5//scripts/game/main/forums/fl_poll.inc forums//scripts/game/main/forums/fl_poll.inc
--- beta5//scripts/game/main/forums/fl_poll.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/game/main/forums/fl_poll.inc	2011-02-05 10:10:03.434335002 +0100
@@ -0,0 +1,373 @@
+<?php
+
+
+/* Forum poll management class */
+
+class fl_poll {
+
+	private $user;
+	private $game;
+	private $db;
+
+	private $fromDB;
+	private $nbVotesQ;
+	private $checked;
+
+	private $record;
+	private $options = array();
+
+	public function __construct() {
+		$args = func_get_args();
+
+		if (count($args) == 1) {
+			// Manual initialisation
+			$this->fromDB = false;
+			$this->checked = false;
+			$this->record = array(
+				'title'	=> preg_replace('/\s+/', ' ', trim($args[0]))
+			);
+		} else {
+			// Load from database
+			$this->fromDB = true;
+			$this->checked = true;
+			$this->game = $args[0];
+			$this->db = $this->game->getDBAccess();
+			$this->mainLib = $this->game->getLib('main/forums');
+			$this->user = $args[1];
+			$id = $args[2];
+
+			// Fetch the poll's data
+			$q = $this->db->query("SELECT * FROM forums.poll WHERE topic = $1 FOR UPDATE", $id);
+			if (! ($q && dbCount($q) == 1)) {
+				throw new Exception("No poll found for topic #$id");
+			}
+			$this->record = dbFetchHash($q);
+
+			// Fetch the poll options
+			$q = $this->db->query("SELECT * FROM forums.poll_option "
+				. "WHERE poll = $1 ORDER BY po_order "
+				. "FOR UPDATE", $id);
+			if (! ($q && dbCount($q) > 1)) {
+				throw new Exception("Poll options not found for topic #$id");
+			}
+			while ($r = dbFetchHash($q)) {
+				array_push($this->options, $r);
+			}
+
+			$this->prepareNbVotesQuery();
+		}
+	}
+
+
+	private function prepareNbVotesQuery() {
+		$this->nbVotesQ = $this->db->prepare(
+			"SELECT COUNT(*) FROM forums.poll_vote WHERE vote = $1", array("option") );
+	}
+
+
+	// ----------------------------------------------------------------------
+	// READING POLL PROPERTIES
+	// ----------------------------------------------------------------------
+
+	public function getTopic() {
+		if (! $this->fromDB) {
+			return null;
+		}
+		return $this->mainLib->call('getTopic', $this->record['topic'] );
+	}
+
+	public function getTitle() {
+		return $this->record['title'];
+	}
+
+	public function isClosed() {
+		return $this->fromDB && ($this->record['closed'] == 't');
+	}
+
+	public function isDeleted() {
+		return $this->fromDB && !is_null($this->record['deleted']);
+	}
+
+	public function getDeletionTime() {
+		return $this->fromDB ? $this->record['deleted'] : null;
+	}
+
+	public function getDeletionMod() {
+		return $this->fromDB ? $this->record['deleted_by'] : null;
+	}
+
+
+	// ----------------------------------------------------------------------
+	// POLL CREATION
+	// ----------------------------------------------------------------------
+
+	/* Checks the poll's data before inserting it in the database.
+	 * Returns null if the poll is already inside the database, 0 if the poll
+	 * is ok, or one of the following error codes:
+	 *  1 - Title too short
+	 *  2 - Title too long
+	 *  3 - Not enough options
+	 *  4 - Too many options
+	 */
+	public function checkData() {
+		if ($this->fromDB || $this->checked) {
+			return null;
+		}
+
+		$l = strlen($this->record['title']);
+		if ($l < 3) {
+			return 1;
+		} elseif ($l > 64) {
+			return 2;
+		}
+
+		$c = count($this->options);
+		if ($c < 2) {
+			return 3;
+		} elseif ($c > 20) {
+			return 4;
+		}
+
+		$this->checked = true;
+		return 0;
+	}
+
+	/* Insert the poll's data into the database. */
+	public function insertIntoDB($game, $user, $topicId) {
+		if ($this->fromDB || ! $this->checked) {
+			return false;
+		}
+		$db = $game->getDBAccess();
+
+		// Insert the poll
+		$q = $db->query("SELECT * FROM forums.create_poll($1, $2)", $topicId, $this->record['title']);
+		if (! ($q && dbCount($q) == 1)) {
+			return false;
+		}
+
+		$rec = dbFetchHash($q);
+		if (is_null($rec['topic'])) {
+			return false;
+		}
+
+		// Insert the options
+		$pollQuery = $db->prepare("SELECT * FROM forums.create_option($1, $2, $3)",
+			array("poll", "order", "title") );
+		try {
+			$nOpts = array();
+			for ($i = 0; $i < count($this->options); $i ++) {
+				$q = $pollQuery->execute(array(
+					"poll"	=> $topicId,
+					"order"	=> $i,
+					"title"	=> $this->options[$i]['title']
+				));
+				if (! ($q && dbCount($q) == 1)) {
+					throw new Exception('abort');
+				}
+
+				$orec = dbFetchHash($q);
+				if (is_null($orec['id'])) {
+					throw new Exception('abort');
+				}
+
+				array_push($nOpts, $orec);
+			}
+			$pollQuery->destroy();
+		} catch (Exception $e) {
+			$pollQuery->destroy();
+			$db->query("DELETE FROM forums.poll WHERE topic = $1", $topicId);
+			return false;
+		}
+
+		// Finalize
+		$this->record = $rec;
+		$this->options = $nOpts;
+		$this->game = $game;
+		$this->db = $db;
+		$this->user = $user;
+		$this->mainLib = $this->game->getLib('main/forums');
+		$this->prepareNbVotesQuery();
+		$this->fromDB = true;
+		return true;
+	}
+
+
+	// ----------------------------------------------------------------------
+	// OPTIONS
+	// ----------------------------------------------------------------------
+
+	/* Adds a new option at the end of the list.
+	 * Returns 0 on success or one of the following error codes:
+	 *  -1 - Title too short
+	 *  -2 - Title too long
+	 *  -3 - Too many options
+	 *  -4 - Database error
+	 *  -5 - Adding options after the poll has been checked is not permitted
+	 */
+	public function appendOption($title) {
+		$title = preg_replace('/\s+/', ' ', trim($title));
+		$l = strlen($title);
+		if ($l < 2) {
+			return -1;
+		} elseif ($l > 64) {
+			return -2;
+		}
+
+		if ($this->fromDB) {
+			$c = count($this->options);
+			if ($c == 20) {
+				return -3;
+			}
+
+			$q = $this->db->query("SELECT * FROM forums.create_option($1, $2, $3)",
+				$this->record['topic'], $c, $title);
+			if (! ($q && dbCount($q) == 1)) {
+				return -4;
+			}
+			array_push($this->options, dbFetchHash($q));
+		} else if (!$this->checked) {
+			array_push($this->options, array(
+				'title'	=> $title
+			));
+		} else {
+			return -5;
+		}
+		return 0;
+	}
+
+	/* Removes the poll option with the specified index. */
+	public function removeOption( $order ) {
+		$order = (int) $order;
+		if ($order < 0 || $order >= count($this->options)) {
+			return false;
+		}
+
+		if ($this->fromDB) {
+			$q = $this->db->query("SELECT forums.delete_option( $1, $2 )", $this->record['topic'], $order);
+			if (! ($q && dbCount($q) == 1)) {
+				return false;
+			}
+			list($ok) = dbFetchArray($q);
+			if ($ok != 't') {
+				return false;
+			}
+
+			for ($i = $order + 1; $i < count($this->options); $i ++) {
+				$this->options[$i]['po_order'] --;
+			}
+		}
+		array_splice($this->options, $order, 1);
+		return true;
+	}
+
+	/* Get the options */
+	public function getOptions() {
+		$opts = $this->options;
+		if (! $this->fromDB) {
+			for ($i = 0; $i < count($opts); $i++) {
+				$opts[$i]['po_order'] = $i;
+			}
+		}
+		return $opts;
+	}
+
+	/* Move an option up in the list of options */
+	public function moveUp( $order ) {
+		if ($order <= 0 || $order >= count($this->options)) {
+			return false;
+		}
+
+		if ($this->fromDB) {
+			$q = $this->db->query("SELECT forums.move_opt_up( $1, $2 )", $this->record['topic'], $order);
+			if (! ($q && dbCount($q) == 1)) {
+				return false;
+			}
+			list($ok) = dbFetchArray($q);
+			if ($ok != 't') {
+				return false;
+			}
+
+			$this->options[$order]['po_order'] --;
+			$this->options[$order - 1]['po_order'] ++;
+		}
+
+		$tmp = $this->options[$order];
+		$this->options[$order] = $this->options[$order - 1];
+		$this->options[$order - 1] = $tmp;
+		return true;
+	}
+
+	/* Move an option down in the list of options */
+	public function moveDown( $order ) {
+		if ($order < 0 || $order >= count($this->options) - 1) {
+			return false;
+		}
+
+		if ($this->fromDB) {
+			$q = $this->db->query("SELECT forums.move_opt_down($1,$2)", $this->record['topic'], $order);
+			if (! ($q && dbCount($q) == 1)) {
+				return false;
+			}
+			list($ok) = dbFetchArray($q);
+			if ($ok != 't') {
+				return false;
+			}
+
+			$this->options[$order]['po_order'] ++;
+			$this->options[$order + 1]['po_order'] --;
+		}
+
+		$tmp = $this->options[$order];
+		$this->options[$order] = $this->options[$order + 1];
+		$this->options[$order + 1] = $tmp;
+		return true;
+	}
+
+	/* Vote */
+	public function setVote( $order ) {
+		if (! $this->fromDB || $order < 0 || $order >= count($this->options)) {
+			return false;
+		}
+
+		$q = $this->db->query("SELECT forums.set_vote($1,$2,$3)", $this->user, $this->record['topic'],
+			$this->options[$order]['id']);
+		if (! $q) {
+			return false;
+		}
+		return true;
+	}
+
+	/* Returns the order of the option the user is voting for */
+	public function getVote() {
+		if (! $this->fromDB) {
+			return null;
+		}
+
+		$q = $this->db->query("SELECT o.po_order FROM forums.poll_option o, forums.poll_vote v "
+					. " WHERE o.id = v.vote AND o.poll = $1 AND v.account = $2",
+				$this->record['topic'], $this->user);
+		if (!($q && dbCount($q) == 1)) {
+			return null;
+		}
+		list($order) = dbFetchArray($q);
+		return $order;
+	}
+
+	/* Returns the amount of votes for an option */
+	public function getNbVotes( $order ) {
+		if (! $this->fromDB || $order < 0 || $order >= count($this->options)) {
+			return 0;
+		}
+
+		$q = $this->nbVotesQ->execute($this->options[$order]['id']);
+		if (!($q && dbCount($q) == 1)) {
+			return 0;
+		}
+
+		list($votes) = dbFetchArray($q);
+		return $votes;
+	}
+}
+
+
+?>
diff -Naur beta5//scripts/game/main/forums/fl_post.inc forums//scripts/game/main/forums/fl_post.inc
--- beta5//scripts/game/main/forums/fl_post.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/game/main/forums/fl_post.inc	2011-02-05 10:10:03.434335002 +0100
@@ -0,0 +1,219 @@
+<?php
+
+
+class fl_post {
+
+	private static $getQ		= null;
+	private static $historyQ	= null;
+	private static $repliesQ	= null;
+
+	private $game;
+	private $mainLib;
+	private $db;
+	private $user;
+
+	private $record;
+	private $history = null;
+	private $replies = null;
+
+
+	public function __construct($game, $user, $data) {
+		self::initQueries();
+
+		$this->game = $game;
+		$this->user = $user;
+		$this->db = $this->game->getDBAccess();
+		$this->mainLib = $this->game->getLib('main/forums');
+
+		if (is_array($data)) {
+			$this->record = $data;
+		} else {
+			$data = (int) $data;
+			$row = self::$getQ->execute($data);
+			if (! ($row && dbCount($row) == 1)) {
+				throw new Exception("Post #$data not found");
+			}
+			$this->record = dbFetchHash($row);
+		}
+	}
+
+
+	static private function initQueries() {
+		if (! is_null(self::$getQ)) {
+			return;
+		}
+
+		$db = config::getMainInterface()->getDBAccess();
+		self::$getQ = $db->prepare( "SELECT * FROM forums.post WHERE id = $1", array("id") );
+		self::$historyQ = $db->prepare( "SELECT * FROM forums.post_text WHERE post = $1 ORDER BY moment",
+			array( "post" ));
+		self::$repliesQ = $db->prepare( "SELECT id FROM forums.post WHERE reply_to = $1 ORDER BY post_moment",
+			array( "post" ));
+	}
+
+
+	// ----------------------------------------------------------------------
+	// READING POST PROPERTIES
+	// ----------------------------------------------------------------------
+
+	public function getId() {
+		return $this->record['id'];
+	}
+
+
+	public function getForum() {
+		return $this->mainLib->call('getForum', $this->record['forum'], $this->user);
+	}
+
+	public function getTopic() {
+		return $this->mainLib->call('getTopic', $this->record['topic'], $this->user);
+	}
+
+
+	public function getParent() {
+		return is_null($this->record['reply_to']) ? null
+			: $this->mainLib->call('getPost', $this->record['reply_to'], $this->user);
+	}
+
+	public function getDepth() {
+		return $this->record['depth'];
+	}
+
+
+	public function getPostedAt() {
+		return $this->record['post_moment'];
+	}
+
+	public function getPostedBy() {
+		return $this->record['author'];
+	}
+
+	public function getLastChange() {
+		return $this->record['last_change'];
+	}
+
+	public function getLastChangeAuthor() {
+		return $this->record['last_author'];
+	}
+
+	public function isUnread() {
+		$topic = $this->getTopic();
+		if (is_null($topic)) {
+			return false;
+		}
+
+		return $topic->getLastRead() < $this->record['last_change'];
+	}
+
+
+	public function isDeleted() {
+		return ! is_null($this->record['deleted']);
+	}
+
+	public function getDeletionTime() {
+		return $this->record['deleted'];
+	}
+
+	public function getDeletionMod() {
+		return $this->record['deleted_by'];
+	}
+
+
+	public function getTitle() {
+		return $this->record['title'];
+	}
+	
+	public function getContents($time = null) {
+		if (is_null($time)) {
+			$v = $this->record['contents'];
+		} else {
+			$hist = $this->getFullHistory();
+			$v = '';
+			foreach ($hist as $he) {
+				if ($he['moment'] > $time) {
+					break;
+				}
+				$v = $he['contents'];
+			}
+		}
+		return $v;
+	}
+
+	public function codeEnabled($time = null) {
+		if (is_null($time)) {
+			$v = $this->record['enable_code'];
+		} else {
+			$hist = $this->getFullHistory();
+			$v = false;
+			foreach ($hist as $he) {
+				if ($he['moment'] > $time) {
+					break;
+				}
+				$v = $he['enable_code'];
+			}
+		}
+		return ($v == 't');
+	}
+
+	public function smileysEnabled($time = null) {
+		if (is_null($time)) {
+			$v = $this->record['enable_smileys'];
+		} else {
+			$hist = $this->getFullHistory();
+			$v = false;
+			foreach ($hist as $he) {
+				if ($he['moment'] > $time) {
+					break;
+				}
+				$v = $he['enable_smileys'];
+			}
+		}
+		return ($v == 't');
+	}
+
+
+	public function getSignature() {
+		throw new Exception("FIXME: getSignature() is not implemented");
+	}
+
+	/* This function returns a post's full history */
+	public function getFullHistory() {
+		if (is_null( $this->history )) {
+			$result = self::$historyQ->execute($this->record['id']);
+			if (! ($result && dbCount($result))) {
+				return null;
+			}
+			$history = array();
+			while ($row = dbFetchHash($result)) {
+				array_push($history, $row);
+			}
+			$this->history = $history;
+		}
+		return $this->history;
+	}
+
+	/* This function returns the replies to a post */
+	public function getReplies() {
+		if (is_null( $this->replies )) {
+			$result = self::$repliesQ->execute($this->record['id']);
+			if (! $result) {
+				return null;
+			}
+			$this->replies = array();
+			while ($row = dbFetchArray($result)) {
+				$reply = $this->mainLib->call('getPost', $row[0], $this->user);
+				if (! is_null($reply)) {
+					array_push($this->replies, $reply);
+				}
+			}
+		}
+		return $this->replies;
+	}
+
+	/* This function deletes a post if it is not the top-level post */
+	public function delete() {
+	}
+}
+
+
+?>
diff -Naur beta5//scripts/game/main/forums/fl_topic.inc forums//scripts/game/main/forums/fl_topic.inc
--- beta5//scripts/game/main/forums/fl_topic.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/game/main/forums/fl_topic.inc	2011-02-05 10:10:03.434335002 +0100
@@ -0,0 +1,339 @@
+<?php
+
+/* Topic handling class */
+
+class fl_topic {
+
+	private static $getQuery = null;
+	private static $restoreQuery = null;
+	private static $deleteQuery = null;
+	private static $stickyLevelQuery = null;
+	private static $setLockQuery = null;
+	private static $moveQuery = null;
+	private static $postsQuery = null;
+
+	private $game;
+	private $db;
+	private $mainLib = null;
+
+	private $user;
+	private $record;
+	private $lastRead = null;
+	private $allPosts = null;
+
+	public function __construct($game, $user, $data) {
+		self::initQueries();
+
+		$this->game = $game;
+		$this->db = $this->game->getDBAccess();
+		$this->user = $user;
+
+		if (is_array($data)) {
+			$this->record = $data;
+		} else {
+			$data = (int) $data;
+			$q = self::$getQuery->execute($data);
+			if (! ($q && dbCount($q) == 1)) {
+				throw new Exception("Topic #$data not found");
+			}
+			$this->record = dbFetchHash($q);
+		}
+	}
+
+	static private function initQueries() {
+		if (is_null(self::$getQuery)) {
+			$db = config::getMainInterface()->getDBAccess();
+			self::$getQuery = $db->prepare(
+				"SELECT * FROM forums.topic WHERE id = $1",
+				array("id") );
+			self::$restoreQuery = $db->prepare( "SELECT forums.restore_post( $1 )", array("id") );
+			self::$deleteQuery = $db->prepare( "SELECT forums.delete_post( $1 , $2 )",
+				array("id", "user") );
+			self::$stickyLevelQuery = $db->prepare( "UPDATE forums.t_topic SET sticky_level = $2 "
+				. "WHERE id = $1", array("id", "level") );
+			self::$setLockQuery = $db->prepare( "UPDATE forums.t_topic SET locked = $2 WHERE id = $1",
+				array("id", "locked") );
+			self::$moveQuery = $db->prepare( "SELECT forums.move_topic( $1, $2, $3)",
+				array("topic", "destination", "user") );
+			self::$postsQuery = $db->prepare( "SELECT * FROM forums.post WHERE topic = $1",
+				array("topic") );
+		}
+	}
+
+
+	// ----------------------------------------------------------------------
+	// READING TOPIC PROPERTIES
+	// ----------------------------------------------------------------------
+
+	public function getId() {
+		return $this->record['id'];
+	}
+
+	public function getForum() {
+		if (is_null($this->mainLib)) {
+			$this->mainLib = $this->game->getLib('main/forums');
+		}
+		return $this->mainLib->call('getForum', $this->record['forum'], $this->user);
+	}
+
+	public function getMovedFrom() {
+		if (is_null($this->record['moved_from'])) {
+			return null;
+		}
+
+		if (is_null($this->mainLib)) {
+			$this->mainLib = $this->game->getLib('main/forums');
+		}
+		return $this->mainLib->call('getForum', $this->record['moved_from'], $this->user);
+	}
+
+	public function isInForum($forum) {
+		return ($forum->getId() == $this->record['forum']);
+	}
+
+	public function getPoll() {
+		if (is_null($this->mainLib)) {
+			$this->mainLib = $this->game->getLib('main/forums');
+		}
+		return $this->mainLib->call('getPoll', $this->record['id'], $this->user);
+	}
+
+	public function getStickyLevel() {
+		return $this->record['sticky_level'];
+	}
+
+	public function getTitle() {
+		return $this->record['title'];
+	}
+
+	public function getPosted() {
+		return $this->record['fp_moment'];
+	}
+
+	public function getAuthor() {
+		return $this->record['fp_author'];
+	}
+
+	public function getLastChange() {
+		return $this->record['lc_moment'];
+	}
+
+	public function getLastChangeAuthor() {
+		return $this->record['lc_author'];
+	}
+
+	public function isDeleted() {
+		return ! is_null($this->record['deleted']);
+	}
+
+	public function getDeletionTime() {
+		return $this->record['deleted'];
+	}
+
+	public function getDeletionMod() {
+		return $this->record['deleted_by'];
+	}
+
+	public function isLocked() {
+		return ($this->record['locked'] == 't');
+	}
+
+	public function getLastRead() {
+		if (is_null($this->lastRead)) {
+			$q = $this->db->query("SELECT read_at FROM forums.topic_read WHERE topic = $1 AND read_by = $2",
+				$this->record['id'], $this->user);
+			if (!($q && dbCount($q) == 1)) {
+				return 0;
+			}
+
+			list($this->lastRead) = dbFetchArray($q);
+		}
+
+		return $this->lastRead;
+	}
+
+
+	// ----------------------------------------------------------------------
+	// MANAGEMENT FUNCTIONS
+	// ----------------------------------------------------------------------
+
+	public function restore() {
+		if (! $this->isDeleted() ) {
+			return;
+		}
+		self::$restoreQuery->execute( $this->record['first_post'] );
+		$q = self::$getQuery->execute( $this->record['id'] );
+		if (! ($q && dbCount($q))) {
+			return;
+		}
+		$this->record = dbFetchHash($q);
+	}
+
+	public function delete() {
+		if ($this->isDeleted() ) {
+			return;
+		}
+		self::$deleteQuery->execute( $this->record['first_post'], $this->user );
+		$q = self::$getQuery->execute( $this->record['id'] );
+		if (! ($q && dbCount($q))) {
+			return;
+		}
+		$this->record = dbFetchHash($q);
+	}
+
+	public function setStickyLevel( $level ) {
+		if ($this->isDeleted() ) {
+			return;
+		}
+		if ($level < 0 || $level > 10) {
+			return;
+		}
+		self::$stickyLevelQuery->execute( $this->record['id'], $level );
+		$this->record['sticky_level'] = $level;
+	}
+
+	public function setLock( $locked ) {
+		if ($this->isDeleted() ) {
+			return;
+		}
+		self::$setLockQuery->execute( $this->record['id'], dbBool($locked) );
+		$this->record['locked'] = $locked ? 't' : 'f';
+	}
+
+	public function moveTo( $forum ) {
+		if ($this->isDeleted() ) {
+			return;
+		}
+		self::$moveQuery->execute($this->record['id'], $forum->getId(), $this->user);
+		$q = self::$getQuery->execute( $this->record['id'] );
+		if (! ($q && dbCount($q))) {
+			return;
+		}
+		$this->record = dbFetchHash($q);
+	}
+
+
+	// ----------------------------------------------------------------------
+	// ACCESSING THE POSTS
+	// ----------------------------------------------------------------------
+
+	public function getPosts() {
+		return $this->record['posts'];
+	}
+
+	public function getFirstPost() {
+		if (is_null($this->mainLib)) {
+			$this->mainLib = $this->game->getLib('main/forums');
+		}
+		return $this->mainLib->call('getPost', $this->record['first_post'], $this->user);
+	}
+
+	private function loadPosts() {
+		if (is_null($this->allPosts)) {
+			$q = self::$postsQuery->execute($this->record['id']);
+			if (! ($q && dbCount($q))) {
+				return null;
+			}
+
+			$this->allPosts = array();
+			while ($r = dbFetchHash($q)) {
+				$this->allPosts[$r['id']] = $this->mainLib->call('getPost', $r, $this->user);
+			}
+		}
+	}
+
+	public function getPostList($isThreaded, $oldFirst) {
+		$this->loadPosts();
+
+		if ($isThreaded) {
+			$result = $this->threadedSort($this->getFirstPost()->getReplies(), $oldFirst);
+			if (count($result)) {
+				array_shift($result);
+			}
+			array_unshift($result, $this->getFirstPost());
+		} else {
+			// If we are not in threaded mode, generate the list
+			// of reverse post <=> time associations and sort it
+			// in the appropriate direction
+			$timeList = array();
+			$result = array();
+
+			foreach ($this->allPosts as $id => $p) {
+				$time = (int) $p->getPostedAt();
+				if (! is_array($timeList[$time])) {
+					$timeList[$time] = array();
+				}
+				array_push($timeList[$time], $p);
+			}
+
+			$times = array_keys($timeList);
+			if ($oldFirst) {
+				sort($times);
+			} else {
+				rsort($times);
+			}
+
+			foreach ($times as $t) {
+				foreach ($timeList[$t] as $p) {
+					array_push($result, $p);
+				}
+			}
+		}
+
+		return $result;
+	}
+
+	private function threadedSort($posts, $oldFirst) {
+		if (! count($posts)) {
+			return array();
+		}
+
+		$timeList = array();
+		$replyList = array();
+
+		foreach ($posts as $post) {
+			$postReplies = $this->threadedSort($post->getReplies(), $oldFirst);
+
+			if (count($postReplies)) {
+				$time = array_shift($postReplies);
+				$replyList[$post->getId()] = $postReplies;
+			} else {
+				$time = $post->getPostedAt();
+				$replyList[$post->getId()] = array();
+			}
+
+			$time = (int) $time;
+			if (! is_array($timeList[$time])) {
+				$timeList[$time] = array();
+			}
+			array_push($timeList[$time], $post);
+		}
+
+		$times = array_keys($timeList);
+		if ($oldFirst) {
+			sort($times);
+		} else {
+			rsort($times);
+		}
+
+		$result = array($times[0]);
+		foreach ($times as $t) {
+			foreach ($timeList[$t] as $p) {
+				array_push($result, $p);
+				foreach ($replyList[$p->getId()] as $r) {
+					array_push($result, $r);
+				}
+			}
+		}
+
+		return $result;
+	}
+
+	public function getPostById($id) {
+		$this->loadPosts();
+		return array_key_exists($id, $this->allPosts) ? $this->allPosts[$id] : null;
+	}
+}
+
+
+?>
diff -Naur beta5//scripts/game/main/forums/library/substitute.inc forums//scripts/game/main/forums/library/substitute.inc
--- beta5//scripts/game/main/forums/library/substitute.inc	2011-02-05 10:09:57.844335002 +0100
+++ forums//scripts/game/main/forums/library/substitute.inc	2011-02-05 10:10:03.424335002 +0100
@@ -1,59 +1,73 @@
 <?php
 
 class main_forums_substitute {
-	var $code	= null;
-	var $smiley	= null;
+	private static $code	= array(
+			array(
+				"re"	=> '\[b\](.*?)\[\/b\]',
+				"rt"	=> '<b>$1</b>'
+			), array(
+				"re"	=> '\[u\](.*?)\[\/u\]',
+				"rt"	=> '<u>$1</u>'
+			), array(
+				"re"	=> '\[i\](.*?)\[\/i\]',
+				"rt"	=> '<i>$1</i>'
+			), array(
+				"re"	=> '\[sep(arator)?\]',
+				"rt"	=> '<hr/>'
+			), array(
+				"re"	=> '\[item\](.*?)\[\/item\]',
+				"rt"	=> '<ul style="margin: 0px"><li>$1</li></ul>'
+			), array(
+				"re"	=> '\[quote\](.*?)\[\/quote\]',
+				"rt"	=> '<blockquote style="margin: 10px 5px; padding: 5px; border: 1px solid #5F5F5F; background-color: #2F2F2F">$1</blockquote>',
+				"rep"	=> true
+			), array(
+				"re"	=> '\[quote=([^\]]+)\](.*?)\[\/quote\]',
+				"rt"	=> '<blockquote style="margin: 10px 5px; padding: 5px; border: 1px solid #5F5F5F; background-color: #2F2F2F"><b>$1</b> said:<br/>$2</blockquote>',
+				"rep"	=> true
+			), array(
+				"re"	=> '\[link=(http[^\]]+)\](.+?)\[\/link\]',
+				"rt"	=> '<a href="$1" target="_blank">$2</a>'
+			), array(
+				"re"	=> '\[code\](.*?)\[\/code\]',
+				"rt"	=> '<pre>$1</pre>'
+			)
+		);
 
 	function main_forums_substitute($lib) {
 		$this->lib	= $lib;
 		$this->db	= $this->lib->game->db;
 	}
 
-	function run($text, $ec, $es) {
+	function run($text, $ec) {
 		$src = array('/\\n/','/\\r/'); $dst = array("<br/>",'');
+		$rsSrc = array(); $rsDst = array();
 		$text = utf8entities($text, ENT_NOQUOTES);
-		$ec = ($ec == 't');
-		$es = ($es == 't');
 
 		if ($ec) {
-			if (is_array($this->code)) {
-				foreach	($this->code as $s => $d) {
-					array_push($src, '/'.$s.'/i');
-					array_push($dst, $d);
-				}
-			} else {
-				$this->codes = array();
-				$q = $this->db->query("SELECT * FROM f_code");
-				while ($r = dbFetchArray($q)) {
-					$this->code[$r[0]] = $r[1];
-					array_push($src, '/'.$r[0].'/i');
-					array_push($dst, $r[1]);
+			foreach	(self::$code as $c) {
+				if ($c['rep']) {
+					array_push($rsSrc, "/{$c['re']}/i");
+					array_push($rsDst, $c['rt']);
+				} else {
+					array_push($src, "/{$c['re']}/i");
+					array_push($dst, $c['rt']);
 				}
 			}
 		}
-		if ($es) {
-			if (is_array($this->smiley)) {
-				foreach	($this->smiley as $s => $d) {
-					array_push($src, '/'.$s.'/i');
-					array_push($dst, $d);
-				}
-			} else {
-				$this->smiley = array();
-				$q = $this->db->query("SELECT * FROM f_smiley");
-				while ($r = dbFetchArray($q)) {
-					$fn = getStatic("main/pics/smiles/icon_".$r[1].".gif");
-					if (is_null($fn)) {
-						continue;
-					}
-					$code = "<img src='$fn' alt='[S]' />";
-					$this->smiley[$r[0]] = $code;
-					array_push($src, '/'.$r[0].'/i');
-					array_push($dst, $code);
+
+		if (count($rsSrc)) {
+			$i = 0;
+			foreach ($rsSrc as $re) {
+				while (preg_match($re, $text)) {
+					$text = preg_replace($re, $rsDst[$i], $text);
 				}
+				$i ++;
 			}
 		}
 
-		return preg_replace($src, $dst, $text);
+		$rv = preg_replace($src, $dst, $text);
+		return $rv;
 	}
 }
 
diff -Naur beta5//scripts/game/main/forums/library.inc forums//scripts/game/main/forums/library.inc
--- beta5//scripts/game/main/forums/library.inc	2011-02-05 10:09:57.844335002 +0100
+++ forums//scripts/game/main/forums/library.inc	2011-02-05 10:10:03.434335002 +0100
@@ -1,71 +1,152 @@
 <?php
 
 class main_forums_library {
-	var $index	= array(
-		'deletePost',
-		'deleteTopic',
-		'edit',
-		'get',
-		'getAdministrator',
-		'getCategories',
-		'getCategory',
-		'getForums',
-		'getModerator',
-		'getPost',
-		'getPosts',
-		'getTopic',
-		'getTopics',
-		'move',
-		'newTopic',
-		'reply',
-		'signature',
-		'substitute',
-		'updateLast',
+	private $libraries = array();
+	private $categories = array();
+	private $forums = array();
+	private $topics = array();
+	private $polls = array();
+	private $posts = array();
+
+	private $getForumLib;
+	private $getCategoryLib;
+
+	public $index	= array(
+		'substitute'
 	);
 
-	function main_forums_library($lib) {
+	public function __construct($lib) {
 		$this->lib	= $lib;
-		$this->db	= $this->lib->game->db;
-	}
+		$this->game	= $this->lib->game;
+		$this->db	= $this->game->getDBAccess();
 
-	function getVersionCategory($ver) {
-		$q = $this->db->query("SELECT id,description FROM f_category WHERE title='!$ver!'");
-		return dbFetchHash($q);
-	}
+		$this->getForumLib = $this->db->prepare(
+			"SELECT l.lib_path FROM forums.t_forum f, forums.category c, forums.category_type l "
+				. "WHERE f.id = $1 AND c.id = f.category AND l.id = c.acl_lib",
+			array("id") );
+		$this->getCategoryLib = $this->db->prepare(
+			"SELECT l.lib_path FROM forums.category c, forums.category_type l "
+				. "WHERE l.id = c.acl_lib AND c.id = $1",
+			array("id") );
+	}
+
+	public function getCategory($id, $user) {
+		$idx = "$id;$user";
+
+		if (! isset($this->categories[$idx])) {
+			$q = $this->getCategoryLib->execute(array("id" => $id));
+			if (! ($q && dbCount($q) == 1)) {
+				return null;
+			}
+			list($lib) = dbFetchArray($q);
+
+			if (! isset($this->libraries[$lib]) ) {
+				$this->libraries[$lib] = $this->game->getLib($lib);
+			}
+			$cat = $this->libraries[$lib]->call('getCategory', $id, $user);
+
+			if (is_null($cat)) {
+				return null;
+			}
+			$this->categories[$idx] = $cat;
+		}
 
-	function isRead($topic, $player) {
-		$q = $this->db->query("SELECT * FROM f_read WHERE topic=$topic AND reader=$player");
-		return $q && dbCount($q);
+		return $this->categories[$idx];
 	}
 
-	function markRead($topic, $player) {
-		if ($this->isRead($topic,$player)) {
-			return false;
+	public function getForum($id, $user) {
+		$idx = "$id;$user";
+
+		if (! isset($this->forums[$idx])) {
+			$q = $this->getForumLib->execute(array("id" => $id));
+			if (! ($q && dbCount($q) == 1)) {
+				return null;
+			}
+			list($lib) = dbFetchArray($q);
+
+			if (! isset($this->libraries[$lib]) ) {
+				$this->libraries[$lib] = $this->game->getLib($lib);
+			}
+			$f = $this->libraries[$lib]->call('getForum', $id, $user);
+
+			if (is_null($f)) {
+				return null;
+			}
+			$this->forums[$idx] = $f;
 		}
-		$this->db->query("DELETE FROM f_read WHERE topic=$topic AND reader=$player");
-		$this->db->query("INSERT INTO f_read(topic,reader)VALUES($topic,$player)");
-		return true;
+
+		return $this->forums[$idx];
 	}
 
-	function markUnread($topic, $player) {
-		$this->db->query("DELETE FROM f_read WHERE topic=$topic AND reader<>$player");
+	public function getTopic($data, $user) {
+		if (is_array($data)) {
+			$id = $data['id'];
+		} else {
+			$id = $data;
+		}
+		$idx = "$id;$user";
+
+		if (! isset($this->topics[$idx])) {
+			loader::needClasses('main/forums', 'fl_topic');
+			try {
+				$topic = new fl_topic($this->game, $user, $data);
+			} catch (Exception $e) {
+				logText("main/forums::getTopic: failed to fetch topic #$id (user #$user)", LOG_WARNING);
+				return null;
+			}
+			$this->topics[$idx] = $topic;
+		}
+
+		return $this->topics[$idx];
 	}
 
-	// Get the amount of unread topics in a forum
-	function getRead($fid, $uid) {
-		$q = $this->db->query("SELECT COUNT(*) FROM f_read r,f_topic t WHERE t.id=r.topic AND t.forum=$fid AND r.reader=$uid AND t.deleted IS NULL");
-		list($nr) = dbFetchArray($q);
-		return $nr;
+	public function getPoll($id, $user) {
+		$idx = "$id;$user";
+
+		if (! isset($this->polls[$idx])) {
+			loader::needClasses('main/forums', 'fl_poll');
+			try {
+				$poll = new fl_poll($this->game, $user, $id);
+			} catch (Exception $e) {
+				return null;
+			}
+			$this->polls[$idx] = $poll;
+		}
+
+		return $this->polls[$idx];
 	}
 
-	function switchSticky($forum, $topic) {
-		$this->db->query("UPDATE f_topic SET sticky=NOT sticky WHERE id=$topic AND forum=$forum AND deleted IS NULL");
+	public function getPost($data, $user) {
+		if (is_array($data)) {
+			$id = $data['id'];
+		} else {
+			$id = $data;
+		}
+		$idx = "$id;$user";
+
+		if (! isset($this->posts[$idx])) {
+			loader::needClasses('main/forums', 'fl_post');
+			try {
+				$post = new fl_post($this->game, $user, $data);
+			} catch (Exception $e) {
+				logText("main/forums::getPost: failed to fetch post #$id (user #$user)", LOG_WARN);
+				return null;
+			}
+			$this->posts[$idx] = $post;
+		}
+
+		return $this->posts[$idx];
 	}
 
-	function markForumRead($fid, $uid) {
-		$q = $this->db->query("SELECT id FROM f_topic WHERE forum=$fid AND deleted IS NULL");
-		while ($r = dbFetchArray($q)) {
-			$this->markRead($r[0], $uid);
+	public function setOptions($pid, $forum, $perPage, $viewDeleted) {
+		if ($forum == -1) {
+			prefs::remove('main/F#PP#%');
+			prefs::remove('main/F#VD#%');
+			prefs::set('main/F#PP', $perPage);
+			prefs::set('main/F#VD', $viewDeleted ? 1 : 0);
+		} else {
+			prefs::set("main/F#PP#$forum", $perPage);
+			prefs::set("main/F#VD#$forum", $viewDeleted ? 1 : 0);
 		}
 	}
 }
diff -Naur beta5//scripts/game/main/gforums/library.inc forums//scripts/game/main/gforums/library.inc
--- beta5//scripts/game/main/gforums/library.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/game/main/gforums/library.inc	2011-02-05 10:10:03.384335002 +0100
@@ -0,0 +1,165 @@
+<?php
+
+/* General forums management library */
+
+class main_gforums_library {
+
+	var $index = array( );
+
+	function __construct($lib) {
+		$this->lib	= $lib;
+		$this->game	= $this->lib->game;
+		$this->version	= $this->game->version;
+		$this->db	= $this->game->db;
+
+		$this->fLib	= $this->game->getLib('main/forums');
+		loader::needClasses('main/forums', array('fl_category', 'fl_forum'));
+
+		$this->getCatQuery = $this->db->prepare(
+			"SELECT t_string,t_is_game FROM main.gf_category WHERE category = $1", array("id") );
+		$this->getCatForumsQuery = $this->db->prepare(
+			"SELECT id FROM forums.t_forum WHERE category = $1 ORDER BY f_order", array("category") );
+		$this->getForumPrivQuery = $this->db->prepare(
+			"SELECT * FROM main.get_gforums_privs( $1, $2 )", array("user", "forum") );
+		$this->getUserNameQ = $this->db->prepare( "SELECT name FROM account WHERE id = $1", array("user") );
+	}
+
+	function getStructure( $user ) {
+		$cats = array();
+
+		$q = $this->db->query("SELECT * FROM main.get_gf_categories($1, $2)",
+			$this->version->id, $this->game->name);
+		while ($r = dbFetchArray($q)) {
+			$cat = $this->fLib->call('getCategory', $r[0], $user);
+			if (! is_null($cat)) {
+				array_push($cats, $cat);
+			}
+		}
+
+		return $cats;
+	}
+
+	function getCategory( $catId , $user ) {
+		$q = $this->getCatQuery->execute($catId);
+		if (! ($q && dbCount($q) == 1)) {
+			return null;
+		}
+		list($str, $ig) = dbFetchArray($q);
+
+		if (is_null($str)) {
+			$title = 'Legacy Worlds forums';
+			$description = 'These forums are common to all versions of Legacy Worlds after Beta 5.';
+		} else if ($ig == 'f') {
+			$txt = config::$config->versions[$str]->text;
+			$title = "Legacy Worlds {$txt}";
+			$description = "These forums are common to all games using the Legacy Worlds "
+				. "{$txt} base code.";
+		} else {
+			$txt = config::$config->games[$str]->text;
+			$title = $txt;
+			$description = "These are the forums for the {$txt} game.";
+		}
+
+		$cat = new fl_category( $this->game, $user, $catId, $title, $description );
+
+		$q = $this->getCatForumsQuery->execute($catId);
+		while ($r = dbFetchArray($q)) {
+			$forum = $this->fLib->call('getForum', $r[0], $user);
+			if ($forum->canView() && ($forum->isAdmin() || ! $forum->isDeleted())) {
+				$cat->addForum( $forum );
+			}
+		}
+
+		return $cat;
+	}
+
+
+	function getForum( $forumId, $user ) {
+		$q = $this->getForumPrivQuery->execute($user, $forumId);
+		if (! ($q && dbCount($q) == 1)) {
+			return null;
+		}
+		$privs = dbFetchHash($q);
+
+		try {
+			$forum = new fl_forum( $this->game, $forumId, $user );
+		} catch (Exception $e) {
+			return null;
+		}
+
+		$forum->setAdmin( $privs['is_admin'] == 't' );
+		$forum->setMod( $privs['is_mod'] == 't' );
+		$forum->setCreateTopic( $privs['can_create'] == 't' );
+		$forum->setCreatePoll( $privs['can_poll'] == 't' );
+		$forum->setPost( $privs['can_post'] == 't' );
+		$forum->setView( $privs['can_view'] == 't' );
+
+		return $forum;
+	}
+
+
+	public function getAdmins($forum) {
+		$q = $this->db->query("SELECT category FROM forums.t_forum WHERE id = $1", $forum);
+		if (! ($q && dbCount($q) == 1)) {
+			return array();
+		}
+		list($cat) = dbFetchArray($q);
+
+		$admins = array();
+
+		$q = $this->db->query("SELECT account FROM gf_admin WHERE category = $1 OR category IS NULL", $cat);
+		if (!$q) {
+			return $admins;
+		}
+		while ($r = dbFetchArray($q)) {
+			array_push($admins, $r[0]);
+		}
+		return $admins;
+	}
+
+
+	public function getModerators($forum) {
+		$q = $this->db->query("SELECT category FROM forums.t_forum WHERE id = $1", $forum);
+		if (! ($q && dbCount($q) == 1)) {
+			return array();
+		}
+		list($cat) = dbFetchArray($q);
+
+		$mods = array();
+
+		$q = $this->db->query("SELECT account FROM gf_cat_moderator "
+			. "WHERE category = $1 OR category IS NULL", $cat);
+		if (!$q) {
+			return $mods;
+		}
+		while ($r = dbFetchArray($q)) {
+			array_push($mods, $r[0]);
+		}
+
+		$q = $this->db->query("SELECT account FROM gf_forum_moderator WHERE forum = $1", $forum);
+		if (!$q) {
+			while ($r = dbFetchArray($q)) {
+				array_push($mods, $r[0]);
+			}
+		}
+
+		return $mods;
+	}
+
+
+	public function getUsers($forum) {
+		return array();
+	}
+
+
+	public function aclIdToName($id) {
+		$q = $this->getUserNameQ->execute($id);
+		if (!($q && dbCount($q) == 1)) {
+			return null;
+		}
+		list($rv) = dbFetchArray($q);
+		return $rv;
+	}
+}
+
+?>
diff -Naur beta5//scripts/game/main/uforums/library.inc forums//scripts/game/main/uforums/library.inc
--- beta5//scripts/game/main/uforums/library.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/game/main/uforums/library.inc	2011-02-05 10:10:03.374335002 +0100
@@ -0,0 +1,193 @@
+<?php
+
+/** User forums management library
+ *
+ * This library handles permissions for user forums.
+ */
+
+class main_uforums_library {
+
+	var $index = array( );
+
+	function __construct($lib) {
+		$this->lib	= $lib;
+		$this->game	= $this->lib->game;
+		$this->db	= $this->game->getDBAccess();
+
+		$this->account	= $this->game->getLib('main/account');
+		$this->fLib	= $this->game->getLib('main/forums');
+		loader::needClasses('main/forums', array('fl_category', 'fl_forum'));
+
+		$this->getOwnCatQ = $this->db->prepare(
+			"SELECT main.uf_get_category($1)", array("user") );
+		$this->getOtherCatsQ = $this->db->prepare(
+			"SELECT DISTINCT category FROM forums.t_forum WHERE id IN ("
+				. "SELECT forum FROM main.uf_subscription WHERE account = $1)",
+			array("user") );
+		$this->getCatOwnerQ = $this->db->prepare(
+			"SELECT account FROM main.user_category WHERE category = $1", array("category") );
+		$this->getOwnForumsQ = $this->db->prepare(
+			"SELECT id FROM forums.t_forum WHERE category = $1 ORDER BY f_order", array("category") );
+		$this->getOtherForumsQ = $this->db->prepare(
+			"SELECT id FROM forums.t_forum"
+				. " WHERE category = $1 "
+				.   " AND id IN (SELECT forum FROM main.uf_subscription WHERE account = $2)"
+				. " ORDER BY f_order",
+			array("category", "user") );
+		$this->getForumPrivQ = $this->db->prepare(
+			"SELECT main.uf_get_user_access( $2, $1 )", array("forum", "user") );
+		$this->getUserNameQ = $this->db->prepare( "SELECT name FROM account WHERE id = $1", array("user") );
+	}
+
+	function getStructure( $user ) {
+		$cats = array();
+
+		$q = $this->getOwnCatQ->execute($user);
+		if ( $q && dbCount($q) == 1) {
+			list($id) = dbFetchArray($q);
+			$cat = $this->fLib->call('getCategory', $id, $user);
+			if (! is_null($cat)) {
+				array_push($cats, $cat);
+			}
+		}
+
+		$q = $this->getOtherCatsQ->execute($user);
+		while ($r = dbFetchArray($q)) {
+			$cat = $this->fLib->call('getCategory', $r[0], $user);
+			if (! is_null($cat)) {
+				array_push($cats, $cat);
+			}
+		}
+
+		return $cats;
+	}
+
+	function getCategory( $id, $user ) {
+		$q = $this->getCatOwnerQ->execute($id);
+		if (! ($q && dbCount($q) == 1) ) {
+			return null;
+		}
+		list($account) = dbFetchArray($q);
+
+		if ($account == $user) {
+			$title = "My forums";
+			$description = "These are your own forums; do with them as you please.";
+		} else {
+			$uname = $this->account->call('getUserName', $account);
+			$title =  "$uname's forums";
+			$description = "These are the personnal forums of player $uname.";
+		}
+
+		$cat = new fl_category( $this->game, $user, $id, $title, $description );
+
+		if ($account == $user) {
+			$q = $this->getOwnForumsQ->execute($id);
+			if (! $q ) {
+				return null;
+			}
+		} else {
+			$q = $this->getOtherForumsQ->execute($id, $user);
+			if (! ($q && dbCount($q) ) ) {
+				return null;
+			}
+		}
+		while ($r = dbFetchArray($q)) {
+			$forum = $this->fLib->call('getForum', $r[0], $user);
+			if ($forum->canView() && ($forum->isAdmin() || ! $forum->isDeleted())) {
+				$cat->addForum( $forum );
+			}
+		}
+
+		return $cat;
+	}
+
+	function getForum( $forumId, $user) {
+		$q = $this->getForumPrivQ->execute($forumId, $user);
+		if (! ($q && dbCount($q) == 1)) {
+			return null;
+		}
+		list($accessMode) = dbFetchArray($q);
+		if (is_null($accessMode)) {
+			return null;
+		}
+
+		try {
+			$forum = new fl_forum( $this->game, $forumId, $user );
+		} catch (Exception $e) {
+			return null;
+		}
+
+		$forum->setAdmin( $accessMode == 'A' );
+		$forum->setMod( in_array($accessMode, array('M', 'A')) );
+		$forum->setCreatePoll( in_array($accessMode, array('L', 'M', 'A')) );
+		$forum->setCreateTopic( in_array($accessMode, array('T', 'L', 'M', 'A')) );
+		$forum->setPost( $accessMode != 'R' );
+		$forum->setView( true );
+
+		return $forum;
+	}
+
+
+	public function getAdmins($forum) {
+		$q = $this->db->query("SELECT uc.account FROM user_category uc, forums.t_forum f "
+			. "WHERE f.id = $1 AND uc.category = f.category", $forum);
+		if (!($q && dbCount($q) == 1)) {
+			return array();
+		}
+		list($admin) = dbFetchArray($q);
+
+		return array($admin);
+	}
+
+
+	public function getModerators($forum) {
+		$q = $this->db->query("SELECT account FROM uf_subscription WHERE access_mode = 'M' AND forum = $1",
+			$forum);
+		if (!$q) {
+			return array();
+		}
+		$mods = array();
+		while ($r = dbFetchArray($q)) {
+			array_push($mods, $r[0]);
+		}
+
+		return $mods;
+	}
+
+
+	public function getUsers($forum) {
+		$q = $this->db->query("SELECT uf_get_access_mode($1)", $forum);
+		if (!($q && dbCount($q))) {
+			return array();
+		}
+		list($mode) = dbFetchArray($q);
+		if ($mode == 'P') {
+			return array();
+		}
+
+		$q = $this->db->query("SELECT account FROM uf_subscription WHERE access_mode <> 'M' AND forum = $1",
+			$forum);
+		if (!$q) {
+			return array();
+		}
+		$users = array();
+		while ($r = dbFetchArray($q)) {
+			array_push($users, $r[0]);
+		}
+
+		return $users;
+	}
+
+
+	public function aclIdToName($id) {
+		$q = $this->getUserNameQ->execute($id);
+		if (!($q && dbCount($q) == 1)) {
+			return null;
+		}
+		list($rv) = dbFetchArray($q);
+		return $rv;
+	}
+}
+
+
+?>
diff -Naur beta5//scripts/lib/classloader.inc forums//scripts/lib/classloader.inc
--- beta5//scripts/lib/classloader.inc	2011-02-05 10:09:57.434335002 +0100
+++ forums//scripts/lib/classloader.inc	2011-03-12 14:56:22.471300053 +0100
@@ -20,6 +20,17 @@
 
 		array_push(loader::$loadedClasses, $className);
 	}
+
+	static function needClasses($basePath, $classes) {
+		$path = config::$main['scriptdir'] . "/game/$basePath/";
+		if (!is_array($classes)) {
+			$classes = array($classes);
+		}
+
+		foreach ($classes as $cn) {
+			self::load($path . $cn . ".inc", $cn);
+		}
+	}
 }
 
 ?>
diff -Naur beta5//scripts/lib/config.inc forums//scripts/lib/config.inc
--- beta5//scripts/lib/config.inc	2011-02-05 10:09:57.434335002 +0100
+++ forums//scripts/lib/config.inc	2011-03-12 14:56:24.031300053 +0100
@@ -123,6 +123,10 @@
 	static function getParam($name) {
 		return config::$config->games['main']->params[$name];
 	}
+
+	static function getMainInterface() {
+		return config::$config->games['main'];
+	}
 }
 
 config::load();
diff -Naur beta5//scripts/lib/db_accessor.inc forums//scripts/lib/db_accessor.inc
--- beta5//scripts/lib/db_accessor.inc	2011-02-05 10:09:57.434335002 +0100
+++ forums//scripts/lib/db_accessor.inc	2011-03-12 15:30:19.021300053 +0100
@@ -32,9 +32,15 @@
 		$this->db->disableExceptions();
 	}
 
-	public function query($q) {
+	public function query() {
 		$this->setNamespace();
-		return $this->db->query($q);
+		$args = func_get_args();
+		return call_user_func_array(array($this->db, "query"), $args);
+	}
+
+	public function prepare($q, $params) {
+		$this->setNamespace();
+		return db_query::getQuery($this, $q, $params);
 	}
 
 	public function begin() {
@@ -45,6 +51,14 @@
 		return $this->db->end($rb);
 	}
 
+	public function getNamespace() {
+		return $this->namespace;
+	}
+
+	public function getDatabase() {
+		return $this->db;
+	}
+
 	public function needsReset() {
 		return $this->db->needsReset();
 	}
diff -Naur beta5//scripts/lib/db_connection.inc forums//scripts/lib/db_connection.inc
--- beta5//scripts/lib/db_connection.inc	2011-02-05 10:09:57.434335002 +0100
+++ forums//scripts/lib/db_connection.inc	2011-03-12 15:39:08.891300050 +0100
@@ -229,17 +229,22 @@
 		return true;
 	}
 
-	public function query($query) {
-		if (!$this->inTrans) {
-			$this->fail("query executed outside of transaction", $query);
-			return null;
+	public function query() {
+		if (func_num_args() == 1) {
+			$query = func_get_arg(0);
+			if (!$this->checkQuery($query)) {
+				return null;
+			}
+			$r = @pg_query($this->conn, $query);
+		} else {
+			$args = func_get_args();
+			$query = array_shift($args);
+			if (!$this->checkQuery($query)) {
+				return null;
+			}
+			$r = call_user_func("pg_query_params", $query, $args);
 		}
 
-		$this->queries ++;
-		if ($this->trace) {
-			l::trace("EXECUTE: $query");
-		}
-		$r = @pg_query($this->conn, $query);
 		if (!$r) {
 			$cStat = pg_connection_status($this->conn);
 			$error = pg_last_error($this->conn);
@@ -264,48 +269,23 @@
 			}
 		} elseif (preg_match('/^\s*insert\s+into ("?\w+"?)/i', $query, $match)) {
 			pg_free_result($r);
-
-			$tn = $match[1];
-			if ($tn{0} == '"') {
-				$tn = str_replace('"', '', $tn);
-			} else {
-				$tn = strtolower($tn);
-			}
-
-			$r2 = @pg_query("SELECT last_inserted('$tn')");
-			if ($r2) {
-				$rv = pg_fetch_row($r2);
-				if (is_null($rv[0])) {
-					$r = true;
-				} else {
-					$r = $rv[0];
-				}
-				pg_free_result($r2);
-			} elseif (preg_match('/deadlock/i', $error)) {
-				l::error("SQL: deadlock detected");
-				if ($this->useExceptions) {
-					throw new db_deadlock_exception($error);
-				}
-				$this->error = true;
-				return;
-			} else {
-				$cStat = pg_connection_status($this->conn);
-				$error = pg_last_error($this->conn);
-				if ($cStat == PGSQL_CONNECTION_BAD) {
-					l::notice("SQL: connection is gone while fetching last ID");
-					$this->__needsReset = true;
-					if ($this->useExceptions) {
-						throw new db_srv_exception($error);
-					}
-				} else {
-					$this->fail("failed to fetch last ID: $error", "SELECT last_inserted('$tn')");
-				}
-				$r = null;
-			}
+			$r = $this->getLastInserted($match[1]);
 		}
 		return $r;
 	}
 
+	private function checkQuery($query) {
+		if (!$this->inTrans) {
+			$this->fail("SQL: query executed outside of transaction", $query);
+			return false;
+		}
+		if ($this->trace) {
+			l::trace("EXECUTE: $query");
+		}
+		$this->queries ++;
+		return true;
+	}
+
 	public function getGameAccess($prefix) {
 		if (is_null($this->accessors[$prefix])) {
 			$this->accessors[$prefix] = new db_accessor($this, $prefix);
@@ -313,6 +293,50 @@
 		return $this->accessors[$prefix];
 	}
 
+	public function getConnection() {
+		return $this->conn;
+	}
+
+	private function getLastInserted($tn) {
+		if ($tn{0} == '"') {
+			$tn = str_replace('"', '', $tn);
+		} else {
+			$tn = strtolower($tn);
+		}
+
+		$r2 = @pg_query("SELECT last_inserted('$tn')");
+		if ($r2) {
+			$rv = pg_fetch_row($r2);
+			if (is_null($rv[0])) {
+				$r = true;
+			} else {
+				$r = $rv[0];
+			}
+			pg_free_result($r2);
+		} elseif (preg_match('/deadlock/i', $error)) {
+			l::error("SQL: deadlock detected");
+			if ($this->useExceptions) {
+				throw new db_deadlock_exception($error);
+			}
+			$this->error = true;
+			$r = null;
+		} else {
+			$cStat = pg_connection_status($this->conn);
+			$error = pg_last_error($this->conn);
+			if ($cStat == PGSQL_CONNECTION_BAD) {
+				l::notice("SQL: connection is gone while fetching last ID");
+				$this->__needsReset = true;
+				if ($this->useExceptions) {
+					throw new db_srv_exception($error);
+				}
+			} else {
+				$this->fail("failed to fetch last ID: $error", "SELECT last_inserted('$tn')");
+			}
+			$r = null;
+		}
+		return $r;
+	}
+
 	public function needsReset() {
 		return $this->__needsReset;
 	}
diff -Naur beta5//scripts/lib/db_query.inc forums//scripts/lib/db_query.inc
--- beta5//scripts/lib/db_query.inc	1970-01-01 01:00:00.000000000 +0100
+++ forums//scripts/lib/db_query.inc	2011-02-05 10:10:03.074335002 +0100
@@ -0,0 +1,132 @@
+<?php
+
+class db_query {
+
+	private static	$queries	= array();
+
+	private $counter = 1;
+	private $table = null;
+	private $name;
+	private $accessor;
+	private $database;
+	private $connection;
+	private $query;
+	private $parameters;
+	private $resource;
+
+	private function __construct($name, $accessor, $query, $pNames) {
+		$this->accessor = $accessor;
+		$this->query = $query;
+		$this->parameters = $pNames;
+		$this->name = $name;
+
+		$this->database = $this->accessor->getDatabase();
+		$this->connection = $this->database->getConnection();
+
+		$this->accessor->useNamespace();
+		$r = @pg_prepare($this->connection, $this->name, $query);
+
+		if (! $r) {
+			$this->accessor->getDatabase()->fail(
+				"SQL: Could not prepare query: " . pg_last_error($this->connection),
+				$this->query );
+			throw new Exception('failed');
+		}
+
+		if (preg_match('/^\s*insert\s+into ("?\w+"?)/i', $this->query, $match)) {
+			$this->table = $match[1];
+		}
+	}
+
+
+	public function execute() {
+		if ($this->counter == 0) {
+			$this->database->fail(
+				"SQL: tried to execute de-allocated query",
+				$this->query );
+			return null;
+		}
+		if (!$this->database->checkQuery($this->query)) {
+			return null;
+		}
+
+		$args = func_get_args();
+		if (count($args) == 1 && is_array($args[0])) {
+			$params = $args[0];
+			$p = array();
+			for ($i = 0; $i < count($this->parameters); $i ++) {
+				if (!array_key_exists($this->parameters[$i], $params)) {
+					$this->database->fail(
+						"SQL: missing parameter '{$this->parameters[$i]}' for prepared query",
+						$this->query );
+					return null;
+				}
+				$p[$i] = $params[$this->parameters[$i]];
+			}
+		} elseif (count($args) == count($this->parameters)) {
+			$p = $args;
+		} else {
+			$this->database->fail( "SQL: invalid parameters for prepared query", $this->query );
+			return null;
+		}
+
+		$this->accessor->useNamespace();
+		$r = @pg_execute($this->connection, $this->name, $p);
+
+		if (! $r) {
+			$this->database->fail(
+				"SQL: could not execute prepared query: " . pg_last_error($this->connection),
+				$this->query );
+			return null;
+		}
+
+		if (!is_null($this->table)) {
+			pg_free_result($r);
+			$r = $this->database->getLastInserted($this->table);
+		}
+
+		return $r;
+	}
+
+	public function destroy($force = false) {
+		if ($this->counter == 0) {
+			return;
+		}
+
+		if ($force) {
+			$this->counter = 0;
+		} else {
+			$this->counter --;
+		}
+
+		if ($this->counter == 0) {
+			$this->accessor->useNamespace();
+			$r = $this->database->query("DEALLOCATE {$this->name}");
+			self::$queries[$this->name] = null;
+		}
+	}
+
+
+	public static function getQuery($accessor, $query, $pNames) {
+		$data = array(
+			"n"	=> $accessor->getNamespace(),
+			"q"	=> $query,
+			"p"	=> $pNames
+		);
+		$name = "q_" . strtolower(md5(serialize($data)));
+
+		if (is_null(self::$queries[$name])) {
+			try {
+				self::$queries[$name] = new db_query($name, $accessor, $query, $pNames);
+			} catch (Exception $e) {
+				return null;
+			}
+		} else {
+			self::$queries[$name]->counter ++;
+		}
+		return self::$queries[$name];
+	}
+
+}
+
+?>
diff -Naur beta5//scripts/lib/library.inc forums//scripts/lib/library.inc
--- beta5//scripts/lib/library.inc	2011-02-05 10:09:57.434335002 +0100
+++ forums//scripts/lib/library.inc	2011-03-12 14:56:22.481300053 +0100
@@ -66,10 +66,28 @@
 				$fcn = $this->loadClass($this->functions[$function][1]);
 				$this->functions[$function][2] = new $fcn($this);
 			}
-			$rv = call_user_func_array(array($this->functions[$function][2], 'run'), $args);
+			try {
+				$rv = call_user_func_array(array($this->functions[$function][2], 'run'), $args);
+			} catch (Exception $e) {
+				fatalError(32, array(
+					"Function call '{$this->name}::$function' on game '{$this->game->name}' "
+						. "failed with exception:",
+					$e->getMessage()
+				));
+			}
 		} else {
 			// Call the function instance's method
-			$rv = call_user_func_array(array($this->mainClass, $this->functions[$function][1]), $args);
+			try {
+				$rv = call_user_func_array(
+					array($this->mainClass, $this->functions[$function][1]), $args
+				);
+			} catch (Exception $e) {
+				fatalError(32, array(
+					"Function call '{$this->name}::$function' on game '{$this->game->name}' "
+						. "failed with exception:",
+					$e->getMessage()
+				));
+			}
 		}
 
 		return $rv;
diff -Naur beta5//scripts/lib/prefs.inc forums//scripts/lib/prefs.inc
--- beta5//scripts/lib/prefs.inc	2011-02-05 10:09:57.434335002 +0100
+++ forums//scripts/lib/prefs.inc	2011-02-05 10:10:03.074335002 +0100
@@ -107,6 +107,19 @@
 		return $v;
 	}
 
+	static function remove($path) {
+		list($version, $name) = explode('/', $path);
+		$qs = "DELETE FROM user_preferences WHERE account={$_SESSION['userid']} AND version = '$version' AND id";
+		if (strstr($name, '%') === FALSE) {
+			$q = dbQuery("$qs = '$name'");
+		} else {
+			$q = dbQuery("$qs LIKE '$name'");
+		}
+		prefs::$prefs = null;
+		prefs::load();
+
+	}
+
 }
 
 ?>
diff -Naur beta5//scripts/site/beta5/handlers/alliance.inc forums//scripts/site/beta5/handlers/alliance.inc
--- beta5//scripts/site/beta5/handlers/alliance.inc	2011-02-05 10:09:57.204335002 +0100
+++ forums//scripts/site/beta5/handlers/alliance.inc	2011-03-12 15:16:23.151300053 +0100
@@ -18,8 +18,8 @@
 				// Pending requests
 				"getPending", "acceptRequests", "rejectRequests",
 				// Forums
-				"getForums", "newForum", "changeForum", "delForum",
-				"moveForum", "getForumAcl",
+				"getForums", "newForum", "changeForum",
+				"delForum", "restoreForum", "moveForum",
 				// Ranks
 				"getRanks", "newRank", "changeRank", "delRank"
 			),
@@ -802,190 +802,316 @@
 	// ALLIANCE FORUMS MANAGEMENT
 	//-------------------------------------------
 
-	function doGetForums($aid) {
-		$afl = gameAction('getAllianceForums', $aid);
-		$s = "";
-		foreach	($afl as $id => $afd)
-		{
-			if	($s != "")
-				$s .= "\n";
-			$s .= "$id#" . $afd['order'] . "#" . ($afd['user_post'] ? 1 : 0) . "#" . $afd['title'];
-			if	($afd['description'] != '')
-			{
-				$dll = split("\n", $afd['description']);
-				foreach	($dll as $dl)
-					$s .= "\n+#$dl";
+	private function forumsGetLibraries() {
+		// Get the libraries
+		$this->player = input::$game->getLib('beta5/player');
+		$this->alliance = input::$game->getLib('beta5/alliance');
+		$this->forums = input::$game->getLib('main/forums');
+		$this->aForums = input::$game->getLib('beta5/aforums');
+	}
+
+	private function doGetForums($aid, $uid) {
+		$out = array();
+		$players = array();
+
+		// Get the category and output the amount of forums
+		$cat = array_shift($this->aForums->call('getStructure', $uid));
+		array_push($out, count($cat->getForums()));
+		$admin = null;
+
+		foreach ($cat->getForums() as $forum) {
+			// Get general information regarding the forum
+			$fid = $forum->getId();
+			$userPrivs = $this->aForums->call('getUserPrivileges', $fid);
+			$topics = $forum->getTopics();
+			$name = $forum->getTitle();
+
+			// Get information about the forum's deletion
+			$deleted = $forum->isDeleted() ? '1' : '0';
+			$deletedAt = $forum->deletedAt();
+			$deletedBy = $forum->deletedBy();
+			if (! (is_null($deletedBy) || in_array($deletedBy, $players))) {
+				array_push($players, $deletedBy);
+			}
+
+			// Output general information
+			array_push($out, "$fid#$deleted#$deletedBy#$deletedAt#$userPrivs#$topics#$name");
+
+			// Get the rank-specific access privileges for that forum
+			array_push($out, join('#', $forum->getUsers()));
+			array_push($out, join('#', $forum->getModerators()));
+			$admins = $forum->getAdministrators();
+
+			// Get the description
+			$description = $forum->getDescription();
+			if (is_null($description) || $description == '') {
+				array_push($out, "0");
+			} else {
+				$dList = explode("\n", $description);
+				array_push($out, count($dList));
+				foreach ($dList as $dLine) {
+					array_push($out, $dLine);
+				}
 			}
 		}
-		return	$s;
+
+		// Get the rank names
+		$ranks = $this->alliance->call('getRanks', $aid);
+		array_push($out, count($ranks));
+		foreach ($ranks as $rId => $rName) {
+			array_push($out, "$rId#" . utf8entities($rName));
+		}
+
+		// List the ranks having administrator privileges
+		if (is_null($admins)) {
+			$admins = array();
+			foreach ($ranks as $rId => $rName) {
+				$rkp = $this->alliance->call('getRankPrivileges', $rId);
+				if ($rkp['forum_admin']) {
+					array_push($admins, $rId);
+				}
+			}
+		}
+		array_push($out, join('#', $admins));
+
+		// Get the player names (for forums that have been deleted)
+		array_push($out, count($players));
+		foreach ($players as $pid) {
+			array_push($out, "$pid#" . utf8entities($this->player->call('getName', $pid, true)));
+		}
+
+		return join("\n", $out);
 	}
 
-	function getForums() {
+	public function getForums() {
 		$pid = $_SESSION[game::sessName()]['player'];
-		$p = gameAction('getPlayerInfo', $pid);
+		$this->forumsGetLibraries();
+
+		// Check if the player's in an alliance
+		$p = $this->player->call('get', $pid);
 		if (is_null($p['aid'])) {
 			return	"ERR#0";
 		}
+
+		// Check if the player is a forums administrator
 		$aid = $p['aid'];
-		$pr = gameAction('getAlliancePrivileges', $pid);
+		$pr = $this->alliance->call('getPrivileges', $pid);
 		if (!$pr['forum_admin']) {
 			return	"ERR#4";
 		}
-		$_SESSION[game::sessName()]['alliance_page'] = 'FAdmin';
 
-		return	$this->doGetForums($aid);
+		$_SESSION[game::sessName()]['alliance_page'] = 'FAdmin';
+		return $this->doGetForums($aid, $p['uid']);
 	}
 
-	function newForum($name, $userPost, $after, $description, $acl) {
-		if (gameAction('isOnVacation', $_SESSION[game::sessName()]['player'])) {
-			return "ERR#200";
+	private function setForumPrivileges( $forum, $alliance, $acl ) {
+		$rl = $this->alliance->call('getRanks', $alliance);
+		$acla = explode('#', $acl);
+		$forum->clearACL();
+
+		foreach	($acla as $as) {
+			list($rank, $level) = explode('!', $as);
+			$level --;
+			if (is_null($rl[$rank]) || ($level != 0 && $level != 1)) {
+				continue;
+			}
+
+			if ($level) {
+				$forum->addModerator($rank);
+			} else {
+				$forum->addUser($rank);
+			}
 		}
+	}
 
+
+	public function newForum($name, $accessMode, $after, $description, $acl) {
+		$this->forumsGetLibraries();
 		$pid = $_SESSION[game::sessName()]['player'];
-		$p = gameAction('getPlayerInfo', $pid);
-		if	(is_null($p['aid']))
-			return	"ERR#0";
-		$aid = $p['aid'];
-		$pr = gameAction('getAlliancePrivileges', $pid);
-		if	(!$pr['forum_admin'])
-			return	"ERR#4";
+		$p = $this->player->call('get', $pid);
 
-		$afl = gameAction('getAllianceForums', $aid);
-		if (count($afl) >= 30) {
-			return	"ERR#5";
+		if (is_null($p['aid'])) {
+			return "ERR#0\n0";
 		}
 
-		$name = preg_replace('/\s+/', ' ', trim($name));
-		if	($name == "" || strlen($name) < 4)
-			return	"ERR#1";
-		foreach	($afl as $fid => $fd)
-			if	($fd['title'] == $name)
-				return	"ERR#2";
-
-		if	($after != "-1" && is_null($afl[$after]))
-			return	"ERR#6";
-
-		$description = trim($description);
-		gameAction('newAllianceForum', $aid, $name, ($userPost == 1), $after, $description);
-
-		$afl = gameAction('getAllianceForums', $aid);
-		$mId = false;
-		foreach ($afl as $fid => $fd)
-			if	($fd['title'] == $name)
-			{
-				$mId = $fid;
-				break;
+		if ($this->player->call('isOnVacation', $pid)) {
+			$err = 200;
+		} else {
+			$name = preg_replace('/\s+/', ' ', trim($name));
+			$description = trim($description);
+			$cat = array_shift($this->aForums->call('getStructure', $p['uid']));
+			$after = (int) $after;
+
+			if ($name == "" || strlen($name) < 4) {
+				$err = 1;
+			} elseif ($after != -1 && is_null($cat->findForum($after))) {
+				$err = 6;
+			} else {
+				if ($after == -1) {
+					$order = 0;
+				} else {
+					$order = $cat->findForum($after)->getOrder() + 1;
+				}
+
+				$rv = $this->aForums->call('create',
+					$pid, $p['uid'], $p['aid'], $order,
+					$name, $description, $accessMode
+				);
+				if (is_object($rv)) {
+					$this->setForumPrivileges( $rv, $p['aid'], $acl );
+					$err = null;
+				} else {
+					logText("Alliance forum creation returned $rv");
+					switch ($rv) :
+						case 1: $err = 0; break;
+						case 2: $err = 4; break;
+						case 3: $err = 5; break;
+						case 4: $err = 2; break;
+						default: $err = 7; break;
+					endswitch;
+				}
 			}
-		if (!$mId) {
-			return	"ERR#7";
 		}
 
-		$rl = gameAction('getAllianceRanks', $aid);
-		$fread = $fmod = array();
-		$acla = explode('#', $acl);
-		foreach	($acla as $as)
-		{
-			list($rank,$level) = explode('!', $as);
-			$level --;
-			if	(is_null($rl[$rank]) || ($level != 0 && $level != 1))
-				continue;
-			if	($level)
-				array_push($fmod, $rank);
-			else
-				array_push($fread, $rank);
+		$out = $this->doGetForums($p['aid'], $p['uid']);
+		if (!is_null($err)) {
+			$out = "ERR#$err\n$out";
 		}
-		gameAction('setForumAccess', $mId, $fread, $fmod);
-
-		return	$this->doGetForums($aid);
+		return $out;
 	}
 
-	function changeForum($id, $name, $userPost, $description, $acl) {
-		if (gameAction('isOnVacation', $_SESSION[game::sessName()]['player'])) {
-			return "ERR#200";
-		}
-
+	public function changeForum($id, $name, $accessMode, $description, $acl) {
+		$this->forumsGetLibraries();
 		$pid = $_SESSION[game::sessName()]['player'];
-		$p = gameAction('getPlayerInfo', $pid);
-		if	(is_null($p['aid']))
-			return	"ERR#0";
-		$aid = $p['aid'];
-		$pr = gameAction('getAlliancePrivileges', $pid);
-		if	(!$pr['forum_admin'])
-			return	"ERR#1";
-
-		$afl = gameAction('getAllianceForums', $aid);
-		if	(is_null($afl[$id]))
-			return	"ERR#3";
+		$p = $this->player->call('get', $pid);
 
-		$name = preg_replace('/\s+/', ' ', trim($name));
-		if	($name == "" || strlen($name) < 4)
-			return	"ERR#1";
-		foreach	($afl as $fid => $fd)
-			if	($fid != $id && $fd['name'] == $name)
-				return	"ERR#2";
-
-		$description = trim($description);
-		gameAction('modifyAllianceForum', $id, $name, ($userPost == 1), $description);
+		if (is_null($p['aid'])) {
+			return "ERR#0\n0";
+		}
 
-		$rl = gameAction('getAllianceRanks', $aid);
-		$fread = $fmod = array();
-		$acla = explode('#', $acl);
-		foreach	($acla as $as)
-		{
-			list($rank,$level) = explode('!', $as);
-			$level --;
-			if	(is_null($rl[$rank]) || ($level != 0 && $level != 1))
-				continue;
-			if	($level)
-				array_push($fmod, $rank);
-			else
-				array_push($fread, $rank);
+		if ($this->player->call('isOnVacation', $pid)) {
+			$err = 200;
+		} else {
+			$id = (int) $id;
+			$name = preg_replace('/\s+/', ' ', trim($name));
+			$description = trim($description);
+
+			$rv = $this->aForums->call('modifyForum', $id, $pid, $name, $description, $accessMode);
+			if ($rv > 0) {
+				switch ($rv) :
+					case -1: $err = 7; break;
+					case -2: $err = 0; break;
+					case -3: $err = 4; break;
+					case -4: $err = 8; break;
+					case -5: $err = 7; break;
+				endswitch;
+			} else {
+				$err = null;
+				$this->setForumPrivileges( $this->forums->call('getForum', $id, $p['uid']),
+					$p['aid'], $acl );
+			}
 		}
-		gameAction('setForumAccess', $id, $fread, $fmod);
 
-		return	$this->doGetForums($aid);
+		$out = $this->doGetForums($p['aid'], $p['uid']);
+		if (!is_null($err)) {
+			$out = "ERR#$err\n$out";
+		}
+		return $out;
 	}
 
 	function delForum($id) {
-		if (gameAction('isOnVacation', $_SESSION[game::sessName()]['player'])) {
-			return "ERR#200";
+		$this->forumsGetLibraries();
+		$pid = $_SESSION[game::sessName()]['player'];
+		$p = $this->player->call('get', $pid);
+
+		if (is_null($p['aid'])) {
+			$err = 0;
+			$out = "0";
+		} else {
+			if ($this->player->call('isOnVacation', $pid)) {
+				$err = 200;
+			} else {
+				$f = $this->forums->call('getForum', (int) $id, $p['uid']);
+				if (is_null($f) || $f->isDeleted()) {
+					$err = 8;
+				} elseif (! $f->isAdmin()) {
+					$err = 4;
+				} else {
+					$f->delete();
+					$err = null;
+				}
+			}
+			$out = $this->doGetForums($p['aid'], $p['uid']);
 		}
 
+		if (!is_null($err)) {
+			$out = "ERR#$err\n$out";
+		}
+		return $out;
+	}
+
+	function restoreForum($id) {
+		$this->forumsGetLibraries();
 		$pid = $_SESSION[game::sessName()]['player'];
-		$p = gameAction('getPlayerInfo', $pid);
-		if	(is_null($p['aid']))
-			return	"ERR#0";
-		$aid = $p['aid'];
-		$pr = gameAction('getAlliancePrivileges', $pid);
-		if	(!$pr['forum_admin'])
-			return	"ERR#1";
+		$p = $this->player->call('get', $pid);
 
-		$afl = gameAction('getAllianceForums', $aid);
-		if (is_null($afl[$id])) {
-			return "ERR#8";
+		if (is_null($p['aid'])) {
+			$err = 0;
+			$out = "0";
+		} else {
+			if ($this->player->call('isOnVacation', $pid)) {
+				$err = 200;
+			} else {
+				$f = $this->forums->call('getForum', (int) $id, $p['uid']);
+				if (is_null($f)) {
+					$err = 9;
+				} elseif (! $f->isAdmin()) {
+					$err = 4;
+				} elseif (! $f->isDeleted()) {
+					$err = 9;
+				} else {
+					$f->restore();
+					$err = null;
+				}
+			}
+			$out = $this->doGetForums($p['aid'], $p['uid']);
 		}
-			gameAction('deleteAllianceForum', $id);
-		return	$this->doGetForums($aid);
-	}
 
-	function moveForum($id, $up) {
-		if (gameAction('isOnVacation', $_SESSION[game::sessName()]['player'])) {
-			return "ERR#200";
+		if (!is_null($err)) {
+			$out = "ERR#$err\n$out";
 		}
+		return $out;
+	}
 
+	function moveForum($id, $up) {
+		$this->forumsGetLibraries();
 		$pid = $_SESSION[game::sessName()]['player'];
-		$p = gameAction('getPlayerInfo', $pid);
-		if	(is_null($p['aid']))
-			return	"ERR#0";
-		$aid = $p['aid'];
-		$pr = gameAction('getAlliancePrivileges', $pid);
-		if	(!$pr['forum_admin'])
-			return	"ERR#1";
+		$p = $this->player->call('get', $pid);
+
+		if (is_null($p['aid'])) {
+			$err = 0;
+			$out = "0";
+		} else {
+			if ($this->player->call('isOnVacation', $pid)) {
+				$err = 200;
+			} else {
+				$f = $this->forums->call('getForum', (int) $id, $p['uid']);
+				if (is_null($f) || $f->isDeleted()) {
+					$err = 8;
+				} elseif (! $f->isAdmin()) {
+					$err = 4;
+				} else {
+					$f->move($up == '1');
+					$err = null;
+				}
+			}
+			$out = $this->doGetForums($p['aid'], $p['uid']);
+		}
 
-		$afl = gameAction('getAllianceForums', $aid);
-		if	(!is_null($afl[$id]))
-			gameAction('moveAllianceForum', $id, ($up == "1"));
-		return	$this->doGetForums($aid);
+		if (!is_null($err)) {
+			$out = "ERR#$err\n$out";
+		}
+		return $out;
 	}
 
 	function getForumAcl($id) {
diff -Naur beta5//scripts/site/beta5/handlers/comms.inc forums//scripts/site/beta5/handlers/comms.inc
--- beta5//scripts/site/beta5/handlers/comms.inc	2011-02-05 10:09:57.204335002 +0100
+++ forums//scripts/site/beta5/handlers/comms.inc	2011-03-12 15:21:17.911300054 +0100
@@ -8,6 +8,50 @@
 			'init'	=> "makeCommsTooltips();\ninitPage();"
 		);
 
+	/** This method dumps containers, categories and forums to the output data.
+	 * The dump's format is the following:
+	 *
+	 *  type # id # nElements # topics # unread
+	 *  name
+	 *
+	 * With:
+	 *  "type" being either "C" for categories and containers or "F" for forums
+	 *  "id" being the object's identifier
+	 *  "nElements" is the number of sub-elements the object contains
+	 *  "topics" and "unread" are the amount of topics and unread topics
+	 *  "name" is the object's title
+	 *
+	 * \parameter $result a reference to the output data
+	 * \parameter $container the object to be dumped
+	 */
+	private function dumpForums( &$result, $container ) {
+		$res = array();
+
+		if ( $container instanceof fl_forum ) {
+			array_push($res, 'F');
+			$contents = array();
+		} else {
+			array_push($res, 'C');
+			if ( $container instanceof fl_category ) {
+				$contents = $container->getForums();
+			} else {
+				$contents = $container->getCategories();
+			}
+		}
+
+		array_push( $res, $container->getId() );
+		array_push( $res, count($contents) );
+		array_push( $res, $container->getTopics() );
+		array_push( $res, $container->getUnread() );
+
+		array_push( $result, join('#', $res) );
+		array_push( $result, utf8entities($container->getTitle()));
+
+		foreach ($contents as $c) {
+			$this->dumpForums( &$result, $c );
+		}
+	}
+
 	public function getCommsData() {
 		// Get the data
 		$data = $this->game->action('getCommsOverview', $_SESSION[game::sessName()]['player']);
@@ -16,33 +60,20 @@
 		$result = array();
 		array_push($result, count($data['folders']['CUS']) . "#" . count($data['forums']['general'])
 			. "#" . count($data['forums']['alliance']));
-
-		// Messages in default folders
-		$dFld = array('IN', 'INT', 'OUT');
-		foreach ($dFld as $f) {
-			array_push($result, join('#', $data['folders'][$f]));
-		}
-
-		// Custom folders
 		foreach ($data['folders']['CUS'] as $id => $folder) {
 			$folder[2] = utf8entities($folder[2]);
 			array_unshift($folder, $id);
 			array_push($result, join('#', $folder));
 		}
 
+
 		// Forums
-		foreach ($data['forums']['general'] as $cat) {
-			array_push($result, "{$cat['id']}#{$cat['type']}#" . count($cat['forums'])
-				. "#" . utf8entities($cat['title']));
-
-			foreach	($cat['forums'] as $f) {
-				$f[3] = utf8entities($f[3]);
-				array_push($result, join('#', $f));
-			}
-		}
-		foreach ($data['forums']['alliance'] as $f) {
-			$f[3] = utf8entities($f[3]);
-			array_push($result, join('#', $f));
+		$forums = gameAction('getForums', $pid);
+		if (is_null($forums)) {
+			array_push($result, "C#/#0#0#0");
+			array_push($result, "");
+		} else {
+			$this->dumpForums( &$result, $forums );
 		}
 
 		return	join("\n", $result);
diff -Naur beta5//scripts/site/beta5/handlers/diplomacy.inc forums//scripts/site/beta5/handlers/diplomacy.inc
--- beta5//scripts/site/beta5/handlers/diplomacy.inc	2011-02-05 10:09:57.204335002 +0100
+++ forums//scripts/site/beta5/handlers/diplomacy.inc	2011-02-05 10:10:02.884335002 +0100
@@ -1,7 +1,12 @@
 <?php
 
-class	page_handler
-{
+class page_handler {
+
+	private $rkLib;
+	private $player;
+	private $alliance;
+	private $aForums;
+
 	var	$needsAuth = true;
 	var	$ajax = array(
 			'func'	=> array('getInformation'),
@@ -9,9 +14,6 @@
 		);
 
 	function getAllianceRanking($tag) {
-		if (! $this->rkLib) {
-			$this->rkLib = input::$game->getLib('main/rankings');
-		}
 		$rt = $this->rkLib->call('getType', "a_general");
 		$r = $this->rkLib->call('get', $rt, $tag);
 		if (!$r) {
@@ -20,27 +22,33 @@
 		return	$r;
 	}
 
-	function	getInformation()
-	{
+	function getInformation() {
 		$out = array();
 		$pid = $_SESSION[game::sessName()]['player'];
-		$pinf = gameAction('getPlayerInfo', $pid);
-		if	(!is_null($pinf['arid']))
-		{
-			$ainf = gameAction('getAllianceInfo', $pinf['arid']);
+
+		// Get the libraries
+		$this->rkLib = input::$game->getLib('main/rankings');
+		$this->player = input::$game->getLib('beta5/player');
+		$this->alliance = input::$game->getLib('beta5/alliance');
+
+		$pinf = $this->player->call('get', $pid);
+
+		// Player has requested to join an alliance
+		if (!is_null($pinf['arid'])) {
+			$ainf = $this->alliance->call('get', $pinf['arid']);
 			$s  = "1#" . $ainf['nplanets'] . '#' . $ainf['avgx'] . '#' . $ainf['avgy'];
 			list($points,$ranking) = $this->getAllianceRanking($ainf['tag']);
 			$s .= "#$ranking#$points";
 			array_push($out, $s);
 			array_push($out, utf8entities($pinf['alliance_req']));
 			array_push($out, utf8entities($pinf['aname']));
-			$alinf = gameAction('getPlayerName', $ainf['leader']);
+			$alinf = $this->player->call('getName', $ainf['leader']);
 			array_push($out, utf8entities($alinf));
-		}
-		elseif	(!is_null($pinf['aid']))
-		{
-			$ainf = gameAction('getAllianceInfo', $pinf['aid']);
-			$pr = gameAction('getAlliancePrivileges', $pid);
+
+		// Player is a member of an alliance
+		} elseif (!is_null($pinf['aid'])) {
+			$ainf = $this->alliance->call('get', $pinf['aid']);
+			$pr = $this->alliance->call('getPrivileges', $pid);
 
 			$s  = "2#" . $ainf['nplanets'] . '#' . $ainf['avgx'] . '#' . $ainf['avgy'];
 			list($points,$ranking) = $this->getAllianceRanking($ainf['tag']);
@@ -49,31 +57,36 @@
 			array_push($out, $s);
 			array_push($out, utf8entities($pinf['alliance']));
 			array_push($out, utf8entities($pinf['aname']));
-			if	(!$pr['is_leader'])
-			{
-				array_push($out, utf8entities(gameAction('getPlayerName', $ainf['leader'])));
-				if	(is_null($pinf['a_grade']))
+
+			// Get the player's rank if he isn't the leader
+			if (!$pr['is_leader']) {
+				array_push($out, utf8entities($this->player->call('getName', $ainf['leader'])));
+				if (is_null($pinf['a_grade'])) {
 					array_push($out, "-");
-				else
-				{
-					$rkl = gameAction('getAllianceRanks', $pinf['aid']);
+				} else {
+					$rkl = $this->alliance->call('getRanks', $pinf['aid']);
 					array_push($out, $rkl[$pinf['a_grade']]);
 				}
 			}
 
-			$fl = gameAction('getAllianceForumsComplete', $pinf['aid']);
-			foreach	($fl as $fd)
-			{
-				$fid = $fd['id'];
-				if	(!(in_array($fid, $pr['f_read']) || in_array($fid, $pr['f_mod'])))
+			// Get the list of alliance forums
+			$aForums = input::$game->getLib('beta5/aforums');
+			$cat = array_shift($aForums->call('getStructure', $pinf['uid']));
+			foreach ($cat->getForums() as $forum) {
+				if ($forum->isDeleted()) {
 					continue;
-				$tot = $fd['topics'];
-				$unread = $tot - gameAction('getReadTopics', $fid, $pid);
-				array_push($out, "$fid#$tot#$unread#".utf8entities($fd['title']));
+				}
+				$fid = $forum->getId();
+				$tot = $forum->getTopics();
+				$unread = $forum->getUnread();
+				$name = utf8entities($forum->getTitle());
+				array_push($out, "$fid#$tot#$unread#$name");
 			}
-		}
-		else
+
+		// Player is not a member of any alliance and has not requested to join one
+		} else {
 			array_push($out, "0");
+		}
 
 		$pm = gameAction('getAllMessages', $pid, 'IN');
 		$pmn = gameAction('getNewMessages', $pid, 'IN');
diff -Naur beta5//scripts/site/beta5/handlers/forums.inc forums//scripts/site/beta5/handlers/forums.inc
--- beta5//scripts/site/beta5/handlers/forums.inc	2011-02-05 10:09:57.204335002 +0100
+++ forums//scripts/site/beta5/handlers/forums.inc	2011-03-12 14:56:16.341300049 +0100
@@ -1,9 +1,7 @@
 <?php
 
-class	page_handler
-{
-	var	$needsAuth = true;
-
+class page_handler {
+/*
 	function	getForum($ctype, $fid, &$cats)
 	{
 		$forum = null;
@@ -850,8 +848,9 @@
 				$gfl[$fid] = $ctype;
 		}
 
-		$txt = '%' . preg_replace('/\*/', '%', addslashes($_SESSION[game::sessName()]['forumsearch']['text'])) . '%';
-		$pg = $_SESSION[game::sessName()]['forumsearch']['page'];
+*/
+//		$txt = '%' . preg_replace('/\*/', '%', addslashes($_SESSION[game::sessName()]['forumsearch']['text'])) . '%';
+/*		$pg = $_SESSION[game::sessName()]['forumsearch']['page'];
 		$mpp = $_SESSION[game::sessName()]['forumsearch']['perpage'];
 		$c = $_SESSION[game::sessName()]['forumsearch']['whole'];
 		$sfs = array('moment','title');
@@ -915,7 +914,10 @@
 				$fl[$cat['type'] . "#" . $f['id']] = array($f['title'],$cat['id'],$cat['title']);
 		}
 
-		$txt = '%' . preg_replace('/\*/', '%', addslashes($_SESSION[game::sessName()]['forumsearch']['text'])) . '%';
+*/
+//		$txt = '%' . preg_replace('/\*/', '%', addslashes($_SESSION[game::sessName()]['forumsearch']['text'])) . '%';
+
+/*
 		$pg = $_SESSION[game::sessName()]['forumsearch']['page'];
 		$mpp = $_SESSION[game::sessName()]['forumsearch']['perpage'];
 		$c = $_SESSION[game::sessName()]['forumsearch']['whole'];
@@ -1067,6 +1069,474 @@
 		}
 		$this->output = "forums";
 	}
+*/
+	/* Timeout in seconds for the forums' session data */
+	const SESSION_TIMEOUT	= 60;
+
+	/* AJAX engine data */
+	public $needsAuth	= true;
+	public $ajax		= array(
+		"init" => "main = new ForumsLayout();",
+		"func" => array(
+			// Menu functions
+			"showMenu", "hideMenu", "getMenu",
+			"menuOpen", "menuClose",
+			// Category / forum view functions
+			"getView", "categoryRead", "forumOptions", "forumRead",
+			"restoreTopics", "deleteTopics", "changeTopicsLevel",
+			"setTopicsLevel", "setTopicsLock", "moveTopics",
+			// Topic view functions
+			"getTopic", "loadPostContents"
+		));
+
+	/* Method that loads the required libraries */
+	private function loadLibraries() {
+		$this->player = input::$game->getLib('beta5/player');
+
+		$this->forums = input::$game->getLib('main/forums');
+		$this->gForums = input::$game->getLib('main/gforums');
+		$this->uForums = input::$game->getLib('main/uforums');
+		$this->aForums = input::$game->getLib('beta5/aforums');
+	}
+
+
+/***********************************************************************************************************************/
+/** SESSION CODE *******************************************************************************************************/
+/***********************************************************************************************************************/
+
+	/* This method reads the menu's status from the session */
+	private function getMenuSession() {
+		if (! array_key_exists( 'forums_menu', $_SESSION[game::sessName()] )) {
+			$_SESSION[game::sessName()]['forums_menu'] = array();
+		}
+		return $_SESSION[game::sessName()]['forums_menu'];
+	}
+
+	/* This method stores the menu's status */
+	private function storeMenuSession( $value ) {
+		$_SESSION[game::sessName()]['forums_menu'] = $value;
+	}
+
+	/* This method initialises the main forums session and removes old data */
+	private function initMainSession() {
+
+		if (is_null( $_SESSION[game::sessName()]['forums_data'] )) {
+			$_SESSION[game::sessName()]['forums_data'] = array();
+			return;
+		}
+
+		// Clean up old data
+		$now = time();
+		$nSession = array();
+		foreach ($_SESSION[game::sessName()]['forums_data'] as $key => $data) {
+			if (!is_array( $data )) {
+				continue;
+			}
+			if ($data['ts'] - $now < self::SESSION_TIMEOUT) {
+				$nSession[$key] = $data;
+			}
+		}
+		$_SESSION[game::sessName()]['forums_data'] = $nSession;
+	}
+
+	/* This method is used by specific handlers to read from the session */
+	public function getSessionData($key) {
+		$this->initMainSession();
+		if (is_array($_SESSION[game::sessName()]['forums_data'][$key])
+				&& array_key_exists( 'data', $_SESSION[game::sessName()]['forums_data'][$key])) {
+			$_SESSION[game::sessName()]['forums_data'][$key]['ts'] = time();
+			return $_SESSION[game::sessName()]['forums_data'][$key]['data'];
+		}
+		return null;
+	}
+
+	/* This method is used by specific handlers to write into the session */
+	public function setSessionData($key, $data) {
+		$this->initMainSession();
+		$_SESSION[game::sessName()]['forums_data'][$key] = array(
+			"ts"	=> time(),
+			"data"	=> $data
+		);
+	}
+
+
+/***********************************************************************************************************************/
+/** MENU HANDLER *******************************************************************************************************/
+/***********************************************************************************************************************/
+
+	/* Returns a boolean indicating whether the menu should be displayed */
+	private function getMenuVisibility() {
+		if (! array_key_exists( 'display_forums_menu', $_SESSION[game::sessName()] )) {
+			$_SESSION[game::sessName()]['display_forums_menu'] = true;
+		}
+		return $_SESSION[game::sessName()]['display_forums_menu'];
+	}
+
+	/* Sets the value for the menu's visibility */
+	private function setMenuVisibility( $value ) {
+		$_SESSION[game::sessName()]['display_forums_menu'] = (boolean) $value;
+	}
+
+	/* This method generates the output for a menu entry */
+	private function makeMenuEntry( &$output, $command, $isLeaf, $isOpen,
+			$displayType, $text, $id = '', $unread = 0 ) {
+
+		// The first line indicates the type of menu entry, as well as the display type,
+		// amount of unread topics and, if the entry is a submenu, whether it's open or not
+		$txt = $isLeaf ? "L" : "N";
+		$txt .= "#$displayType#$unread";
+		if (! $isLeaf) {
+			$txt .= "#" . ($isOpen ? "1" : "0") . "#$id";
+		}
+		array_push( $output, $txt );
+
+		// The two next lines contain the command and text / command ID
+		array_push( $output, $command );
+		array_push( $output, $text );
+	}
+
+	/* This method outputs the "separator" indicator */
+	private function makeMenuSeparator( &$output ) {
+		array_push( $output, "S" );
+	}
+
+	/* This method outputs the "end of menu" indicator */
+	private function makeEndOfMenu( &$output ) {
+		array_push( $output, "E" );
+	}
+
+	/* This method generates the menu's data */
+	private function dumpMenu( &$output, $structure ) {
+
+		// Handle the root of the tree
+		if ($structure instanceof fl_container && $structure->getId() == '/') {
+			// This is the root node
+			$this->makeMenuEntry( &$output, "V#C#/", false, true, "C", "overview", '/' );
+
+			// Add "Latest messages" entry
+			$this->makeMenuEntry( &$output, "L#C#/", true, false, "C", "latest" );
+			// Add the "Search forums" entry
+			$this->makeMenuEntry( &$output, "SF", true, false, "C", "search" );
+
+			// Add the moderation tools submenu
+			// FIXME
+
+			// Add the submenus
+			$this->makeMenuSeparator( &$output );
+			foreach ($structure->getCategories() as $cat) {
+				$this->dumpMenu( &$output, $cat, $menuData );
+			}
+
+			$this->makeEndOfMenu( &$output );
+			return;
+		}
+
+		// Handle containers
+		if ($structure instanceof fl_container) {
+			$id = $structure->getId();
+			$menuData = $this->getMenuSession();
+
+			// Create the node
+			$this->makeMenuEntry( &$output, "V#C#$id", false, $menuData[$id],
+				"T", $structure->getTitle(), $id, $structure->getUnread());
+
+			// Return if the menu's closed
+			if (! $menuData[$id]) {
+				return;
+			}
+
+			// Add "Latest messages" entry
+			$this->makeMenuEntry( &$output, "L#C#$id", true, false, "C", "latest" );
+
+			// Special stuff to be added here
+			// FIXME
+
+			// Add the submenus
+			$catList = $structure->getCategories();
+			if (count($catList)) {
+				$this->makeMenuSeparator( &$output );
+				foreach ($catList as $cat) {
+					$this->dumpMenu( &$output, $cat, $menuData );
+				}
+			}
+
+			$this->makeEndOfMenu( &$output );
+			return;
+		}
+
+		// Standard categories
+		if ($structure instanceof fl_category) {
+			$id = $structure->getId();
+			$menuData = $this->getMenuSession();
+
+			// Create the node
+			$this->makeMenuEntry( &$output, "V#C#$id", false, $menuData[$id],
+				"T", $structure->getTitle(), $id, $structure->getUnread());
+
+			// Return if the menu's closed
+			if (! $menuData[$id]) {
+				return;
+			}
+
+			// Add "Latest messages" entry
+			$this->makeMenuEntry( &$output, "L#C#$id", true, false, "C", "latest" );
+
+			// Special stuff to be added here
+			// FIXME
+
+			// Add the forums
+			$fList = $structure->getForums();
+			if (count($fList)) {
+				$this->makeMenuSeparator( &$output );
+				foreach ($fList as $f) {
+					if ( ! $f->canView() || $f->isDeleted() ) {
+						continue;
+					}
+					$this->makeMenuEntry( &$output, "V#F#" . $f->getId(),
+						true, false, "T", $f->getTitle(), '', $f->getUnread() );
+				}
+			}
+
+			$this->makeEndOfMenu( &$output );
+			return;
+		}
+	}
+
+	/* AJAX method that shows the menu */
+	public function showMenu() {
+		$this->setMenuVisibility(true);
+		return $this->getMenu();
+	}
+
+	/* AJAX method that hides the menu */
+	public function hideMenu() {
+		$this->setMenuVisibility(false);
+	}
+
+	/* AJAX method that returns the forums' menu */
+	public function getMenu($previousMD5 = null) {
+		$pid = $_SESSION[game::sessName()]['player'];
+		$this->loadLibraries();
+
+		// Get the forums' structure
+		$structure = input::$game->action('getForums', $pid);
+
+		// Dump the whole thing
+		$result = array();
+		$this->dumpMenu( &$result, $structure );
+
+		// Check the MD5 sum
+		$md5 = md5(serialize($result));
+		if ($md5 !== $previousMD5) {
+			array_unshift($result, $md5);
+			return join("\n", $result);
+		}
+
+		return "-";
+	}
+
+	/* AJAX method that opens a path in the menu */
+	public function menuOpen($toID) {
+		$intID = (int) $toID;
+		if ( in_array($toID, array('G', 'U', 'MT')) || (string) $intID == $toID ) {
+			if ( (string) $intID == $toID ) {
+				$toID = (string) $intID;
+			}
+			$menuData = $this->getMenuSession();
+			$menuData[$toID] = true;
+			$this->storeMenuSession($menuData);
+		}
+
+		return $this->getMenu();
+	}
+
+	/* AJAX method that closes a path in the menu */
+	public function menuClose($toID) {
+		$intID = (int) $toID;
+		if ( in_array($toID, array('G', 'U', 'MT')) || (string) $intID == $toID ) {
+			if ( (string) $intID == $toID ) {
+				$toID = (string) $intID;
+			}
+			$menuData = $this->getMenuSession();
+			$menuData[$toID] = false;
+			$this->storeMenuSession($menuData);
+		}
+
+		return $this->getMenu();
+	}
+
+
+/***********************************************************************************************************************/
+/** CATEGORY / FORUM VIEW **********************************************************************************************/
+/***********************************************************************************************************************/
+
+	/* AJAX method used to refresh a view page */
+	public function getView($whichView, $md5) {
+		$handler = input::$game->getLib('beta5/forums/view');
+		$pid = $_SESSION[game::sessName()]['player'];
+		$handler->call('forumCommand', $pid, $whichView);
+		return $handler->call('getData', $md5);
+	}
+
+	/* AJAX method that marks all of a category's forums as read */
+	public function categoryRead($category, $whichView) {
+		$handler = input::$game->getLib('beta5/forums/view');
+		$pid = $_SESSION[game::sessName()]['player'];
+		$handler->call('forumCommand', $pid, $whichView);
+		$handler->call('categoryRead', $category);
+		return $handler->call('getData', $md5);
+	}
+
+	/* AJAX method that set forums options */
+	public function forumOptions($forum, $toAll, $perPage, $viewDeleted, $md5) {
+		$forum = (int) $forum;
+		$toAll = ($toAll == '1');
+		$perPage = ($perPage > 0 && $perPage % 10 == 0 && $perPage / 10 < 6) ? (int)$perPage : 20;
+		$viewDeleted = $viewDeleted;
+		$pid = $_SESSION[game::sessName()]['player'];
+
+		$this->loadLibraries();
+		$this->forums->call('setOptions', $pid, $toAll ? -1 : $forum, $perPage, $viewDeleted);
+
+		$handler = input::$game->getLib('beta5/forums/view');
+		$handler->call('forumCommand', $pid, "F#$forum");
+		return $handler->call('getData', $md5);
+	}
+
+	/* AJAX method that set forums options */
+	public function forumRead($forum) {
+		$pid = $_SESSION[game::sessName()]['player'];
+		$handler = input::$game->getLib('beta5/forums/view');
+		$handler->call('forumCommand', $pid, "F#$forum");
+		$handler->call('forumRead');
+		return $handler->call('getData');
+	}
+
+	/* AJAX method that restores a set of topics */
+	public function restoreTopics($forum, $topics) {
+		$pid = $_SESSION[game::sessName()]['player'];
+		$handler = input::$game->getLib('beta5/forums/view');
+		$handler->call('forumCommand', $pid, "F#$forum");
+		$handler->call('restoreTopics', explode('#', $topics));
+		return $handler->call('getData');
+	}
+
+	/* AJAX method that deletes a set of topics */
+	public function deleteTopics($forum, $topics) {
+		$pid = $_SESSION[game::sessName()]['player'];
+		$handler = input::$game->getLib('beta5/forums/view');
+		$handler->call('forumCommand', $pid, "F#$forum");
+		$handler->call('deleteTopics', explode('#', $topics));
+		return $handler->call('getData');
+	}
+
+	/* AJAX method that changes the sticky level for a set of topics */
+	public function changeTopicsLevel($forum, $topics, $change) {
+		$pid = $_SESSION[game::sessName()]['player'];
+		$handler = input::$game->getLib('beta5/forums/view');
+		$handler->call('forumCommand', $pid, "F#$forum");
+		$handler->call('changeTopicsLevel', explode('#', $topics), $change);
+		return $handler->call('getData');
+	}
+
+	/* AJAX method that sets the sticky level for a set of topics */
+	public function setTopicsLevel($forum, $topics, $level) {
+		$pid = $_SESSION[game::sessName()]['player'];
+		$handler = input::$game->getLib('beta5/forums/view');
+		$handler->call('forumCommand', $pid, "F#$forum");
+		$handler->call('setTopicsLevel', explode('#', $topics), $level);
+		return $handler->call('getData');
+	}
+
+	/* AJAX method that sets the lock for a set of topics */
+	public function setTopicsLock($forum, $topics, $lock) {
+		$lock = ($lock == '1');
+		$pid = $_SESSION[game::sessName()]['player'];
+		$handler = input::$game->getLib('beta5/forums/view');
+		$handler->call('forumCommand', $pid, "F#$forum");
+		$handler->call('setTopicsLock', explode('#', $topics), $lock);
+		return $handler->call('getData');
+	}
+
+	/* AJAX method that moves a set of topics to another forum */
+	public function moveTopics($forum, $topics, $destination) {
+		$pid = $_SESSION[game::sessName()]['player'];
+		$handler = input::$game->getLib('beta5/forums/view');
+		$handler->call('forumCommand', $pid, "F#$forum");
+		$handler->call('moveTopics', explode('#', $topics), $destination);
+		return $handler->call('getData');
+	}
+
+
+/***********************************************************************************************************************/
+/** TOPIC VIEW *********************************************************************************************************/
+/***********************************************************************************************************************/
+
+	/* AJAX method that returns a post's contents */
+	public function loadPostContents($topic, $md5, $postId) {
+		$pid = $_SESSION[game::sessName()]['player'];
+		$handler = input::$game->getLib('beta5/forums/topic');
+		$handler->call('initialize', $this);
+		$handler->call('forumCommand', $pid, "$topic#$md5");
+		return $handler->call('getPostContents', $postId);
+	}
+
+
+/***********************************************************************************************************************/
+/** PAGE HANDLER *******************************************************************************************************/
+/***********************************************************************************************************************/
+
+	/* The main page handler */
+	public function handle($input) {
+
+		// Get the command
+		$cmd = $input['cmd'];
+		if ($cmd == 'o' || $cmd == '') {
+			$cmd = 'V#C#/';
+		}
+
+		// Check the command
+		if ($cmd == 'SF') {
+			// Search forums
+			$args = array(); // FIXME
+		} elseif ($cmd == 'xx') {
+			// FIXME: other 'simple' commands
+		} else {
+			$rCmd = $cmd{0};
+			$args = substr($cmd, 2);
+			if ($rCmd == 'V') {
+				// View command
+				$handler = input::$game->getLib('beta5/forums/view');
+			} elseif ($rCmd == 'T') {
+				// View topic command
+				$handler = input::$game->getLib('beta5/forums/topic');
+			}
+		}
+
+		// Execute the command
+		$handler->call('initialize', $this);
+		list($mCurrent, $pageType, $pageData) = $handler->call( 'forumCommand',
+			$_SESSION[game::sessName()]['player'], $args );
+
+		$this->data = array(
+			"needData"	=> input::$IE,
+			"showMenu"	=> $this->getMenuVisibility(),
+			"menuCurrent"	=> $mCurrent,
+			"pageType"	=> $pageType
+		);
+		if (input::$IE) {
+			$this->data['menuContents'] = '';
+			$this->data['pageData'] = $pageData;
+		} else {
+			if ($this->data['showMenu']) {
+				$this->data['menuContents'] = $this->getMenu();
+			}
+			$this->data['pageData'] = $handler->call('getData');
+		}
+
+		$this->output = "forums";
+	}
+
 }
 
 ?>
diff -Naur beta5//scripts/site/beta5/output/comms.en.inc forums//scripts/site/beta5/output/comms.en.inc
--- beta5//scripts/site/beta5/output/comms.en.inc	2011-02-05 10:09:57.294335002 +0100
+++ forums//scripts/site/beta5/output/comms.en.inc	2011-03-12 14:56:19.761300052 +0100
@@ -15,9 +15,7 @@
     </p>
  </td><td style="width:45%">
   <h1>Forums</h1>
-   <h2>General forums</h2>
-    <p id="gforums"></p>
-   <div id="aforums"></div>
+   <div id="forums"></div>
  </td><td>
   <a href="manual?p=communications_page">Help</a>
  </td>
diff -Naur beta5//scripts/site/beta5/output/forums.en.inc forums//scripts/site/beta5/output/forums.en.inc
--- beta5//scripts/site/beta5/output/forums.en.inc	2011-02-05 10:09:57.294335002 +0100
+++ forums//scripts/site/beta5/output/forums.en.inc	2011-02-05 10:10:02.974335002 +0100
@@ -1,5 +1,6 @@
 <?
 
+/*
 function	drawForumsMenu($mode,$fList)
 {
 ?>
@@ -68,5 +69,19 @@
 $sp = $args['sp'];
 $args = $args['d'];
 include("forums/en/$sp.inc");
+*/
 
 ?>
+<div style="display: none; visibility: hidden;">
+ <div id="f-params"><?=$args['showMenu'] ? 1 : 0?>#<?=$args['needData'] ? 1 : 0?>#<?=$args['pageType']?></div>
+ <div id="f-menu-current"><?=$args['menuCurrent']?></div>
+<?php
+if (! $args['needData']) {
+?>
+ <div id="f-menu-init"><?=$args['menuContents']?></div>
+ <div id="f-page-init"><?=$args['pageData']?></div>
+<?php
+}
+?>
+</div>
+<div id="f-contents">&nbsp;</div>
diff -Naur beta5//scripts/ticks.php forums//scripts/ticks.php
--- beta5//scripts/ticks.php	2011-02-05 10:09:57.904335002 +0100
+++ forums//scripts/ticks.php	2011-03-12 15:13:57.671300053 +0100
@@ -20,8 +20,8 @@
 $__loader = array(
 	'log', 'classloader',
 	'version', 'game', 'tick', 'config',
-	'db_connection', 'db_accessor', 'db_copy', 'db',
-	'pcheck', 'library', 'tick_manager',
+	'db_connection', 'db_accessor', 'db_copy' , 'db_query',
+	'db', 'pcheck' , 'library', 'tick_manager',
 );
 require_once("loader.inc");
 
diff -Naur beta5//site/index.php forums//site/index.php
--- beta5//site/index.php	2011-02-05 10:09:57.164335002 +0100
+++ forums//site/index.php	2011-02-05 10:10:02.844335002 +0100
@@ -4,8 +4,8 @@
 $__loader = array(
 	'log', 'classloader',
 	'version', 'game', 'tick', 'config',
-	'db_connection', 'db_accessor', 'db',
-	'library', 'actions', 'data_tree',
+	'db_connection', 'db_accessor', 'db_query',
+	'db', 'library', 'actions', 'data_tree',
 	'input', 'ajax', 'handler', 'engine',
 	'resource', 'tracking', 'session',
 	'account', 'prefs', 'output'
diff -Naur beta5//site/static/beta5/js/pg_alliance-en.js forums//site/static/beta5/js/pg_alliance-en.js
--- beta5//site/static/beta5/js/pg_alliance-en.js	2011-02-05 10:09:56.434335002 +0100
+++ forums//site/static/beta5/js/pg_alliance-en.js	2011-03-12 15:12:38.321300050 +0100
@@ -1037,54 +1037,105 @@
 // FORUMS ADMINISTRATION PAGE
 //--------------------------------------------------
 
-function        drawForumList()
-{
+function drawForumList() {
 	var     str = '<table cellspacing="0" cellpadding="0">';
 	str += '<tr><td class="div2" id="fattl"><h1>Alliance Forums</h1><td id="crforum">&nbsp;</td></tr>';
 	str += '<tr><td colspan="2" id="falist">&nbsp;</td></tr></table>';
 	document.getElementById('alpmain').innerHTML = str;
-	if      (faForums.length < 30)
-		drawCreateForumLink();
-	if      (faForums.length == 0)
+
+	if (faForums.length < 30) {
+		document.getElementById('crforum').innerHTML = '<a href="#" ' + alltt[120]
+			+ ' onClick="createForum();return false">Create a forum</a>';
+	}
+
+	if (faForums.length == 0) {
 		drawTextNoForums();
-	else
+	} else {
 		drawRealForumList();
+	}
 }
 
-function	drawCreateForumLink()
-{
-	var	str = '<a href="#" ' + alltt[120] + ' onClick="createForum();return false">';
-	str += 'Create a forum</a>';
-	document.getElementById('crforum').innerHTML = str;
-}
-
-function	drawTextNoForums()
-{
+function drawTextNoForums() {
 	document.getElementById('falist').innerHTML = '<p>No forums have been defined for the alliance.</p>';
 }
 
-function	drawRealForumList()
-{
-	var	i, str = '<table class="list" cellspacing="0" cellpadding="0" id="fatbl">';
+function drawRealForumList() {
+	var i, str = '<table class="list" cellspacing="0" cellpadding="0" id="fatbl">';
+
+	var getFRankText = function (id) {
+		var r = faRanks['r' + id];
+		return r == '-' ? '<i>Default member</i>' : r;
+	}
+
+	str += '<tr><th class="faname">Name &amp; description</th><th class="fauserpost">User access mode</th></tr>';
+	for (i = 0; i < faForums.length; i++) {
+		str += '<tr><td class="faname"><b><u>'
+			+ faForums[i].name.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
+			+ '</u></b> (';
+		if (faForums[i].isDeleted) {
+			str += 'deleted by <b>' + faPlayers['p' + faForums[i].deletedBy] + '</b> at <b>'
+				+ formatDate(faForums[i].deletedAt) + '</b> - <a href="#" onClick="restoreForum('
+				+ faForums[i].id + ');return false">Restore</a>';
+		} else {
+			str += '<a ' + alltt[130] + ' href="#" onClick="editForum(' + faForums[i].id
+				+ ');return false">Edit</a> - ';
+			if (i > 0) {
+				str += '<a ' + alltt[131] + ' href="#" onClick="moveForum(' + faForums[i].id
+					+ ',true);return false">Move up</a> - ';
+			}
+			if (i < faForums.length - 1) {
+				str += '<a ' + alltt[132] + ' href="#" onClick="moveForum(' + faForums[i].id
+					+ ',false);return false">Move down</a> - ';
+			}
+			str += '<a ' + alltt[133] + ' href="#" onClick="deleteForum(' + faForums[i].id
+				+ ');return false">Delete</a>';
+		}
+		str += ")";
+
+		if (faForums[i].description != '' || faForums[i].users.length || faForums[i].mods.length) {
+
+			var nbr = false;
+			str += '<br/><p>';
+
+			if (faForums[i].description != '') {
+				str += faForums[i].description.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g,"<br/>");
+				nbr = true;
+			}
+
+			if (faForums[i].users.length) {
+				str += (nbr ? '<br/>' : '') + '<b>Users:</b> ';
+				for (var ri in faForums[i].users) {
+					str += (ri > 0 ? ' - ' : '') + getFRankText(faForums[i].users[ri]);
+				}
+				nbr = true;
+			}
+
+			if (faForums[i].mods.length) {
+				str += (nbr ? '<br/>' : '') + '<b>Moderators:</b> ';
+				for (var ri in faForums[i].mods) {
+					str += (ri > 0 ? ' - ' : '') + getFRankText(faForums[i].mods[ri]);
+				}
+				nbr = true;
+			}
+
+			str += '</p>';
+		}
 
-	str += '<tr><th class="faname">Name &amp; description</th><th class="fauserpost">New threads</th></tr>';
-	for	(i=0;i<faForums.length;i++)
-	{
-		str += '<tr>';
-		str += '<td class="faname"><b><u>' + faForums[i].name.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</u></b> (';
-		str += '<a ' + alltt[130] + ' href="#" onClick="editForum(' + faForums[i].id + ');return false">Edit</a> - ';
-		if	(i > 0)
-			str += '<a ' + alltt[131] + ' href="#" onClick="moveForum(' + faForums[i].id + ',true);return false">Move up</a> - ';
-		if	(i < faForums.length - 1)
-			str += '<a ' + alltt[132] + ' href="#" onClick="moveForum(' + faForums[i].id + ',false);return false">Move down</a> - ';
-		str += '<a ' + alltt[133] + ' href="#" onClick="deleteForum(' + faForums[i].id + ');return false">Delete</a>)';
-		if	(faForums[i].description != '')
-			str += '<br/><p>' + faForums[i].description.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g,"<br/>") + '</p>';
 		str += '</td><td class="fauserpost">';
-		if	(faForums[i].userPost)
-			str += 'Everyone';
-		else
-			str += 'Moderators only';
+		switch (faForums[i].accessMode) {
+		case 'L':
+			str += 'Full access';
+			break;
+		case 'T':
+			str += 'No polls';
+			break;
+		case 'P':
+			str += 'Replies only';
+			break;
+		case 'M':
+			str += 'Read only';
+			break;
+		}
 		str += '</td></tr>';
 	}
 
@@ -1092,85 +1143,96 @@
 	document.getElementById('falist').innerHTML = str;
 }
 
-function	cheatAlert()
-{
-	alert('Possible cheating detected.');
-}
-
-function	confirmDeleteForum(name)
-{
-	var	str = 'You are about to delete the following forum:\n' + name + '\n';
-	str += 'The forum\'s topics will be lost and you will not be able to recover them.\nPlease confirm.';
+function confirmDeleteForum(forum) {
+	var str = 'You are about to delete the following forum:\n   ' + forum.name + '\n';
+	if (forum.topics > 0) {
+		str += 'The forum contains '
+			+ (forum.topics > 1 ? (formatNumber(forum.topics.toString()) + ' topics') : 'a single topic')
+			+ '.\n';
+	}
+	str += 'Please confirm.';
 	return	confirm(str);
 }
 
-function	alertMaximumFCount()
-{
+function alertMaximumFCount() {
 	alert('The alliance has reached its maximum possible count of forums.\nYou will be taken back to the list.');
 }
 
-function	drawForumEditor()
-{
+function drawForumEditor() {
 	document.getElementById('falist').innerHTML = '&nbsp;';
 
 	var	str;
-	if	(faEditing.id)
-	{
+	if (faEditing.id) {
 		f = forumById(faEditing.id);
 		str = "'" + f.name.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + "' forum";
-	}
-	else
+	} else {
 		str = "Create a forum";
+	}
 	document.getElementById('fattl').innerHTML = '<h1>' + str + '</h1>';
 
-	str  = '<table cellspacing="0" cellpadding="0" id="feditor">';
-	str += '<tr><th class="edheader">Forum name:</th><td><input ' + alltt[140] + ' type="text" name="fedit" id="fname" size="48" maxlength="64" value="';
-	str += faEditing.name.replace(/"/g, '&quot;') + '" onKeyUp="faEditing.name=value.replace(/&lt;/g, \'<\').replace(/&gt;/g, \'>\').replace(/&amp;/g, \'&\');';
-	str += 'updateFEditor()" onChange="faEditing.name=value;updateFEditor()" />';
+	str  = '<table cellspacing="0" cellpadding="0" id="feditor">'
+		+ '<tr><th class="edheader">Forum name:</th><td><input ' + alltt[140]
+		+ ' type="text" name="fedit" id="fname" size="48" maxlength="64"'
+		+ ' onKeyUp="faEditing.name=value;updateFEditor()" onChange="faEditing.name=value;updateFEditor()" />'
+		+ '</td></tr>'
+		+ '<tr><th class="edheader">User access mode:</th><td>';
+
+	var aModes = {
+		M: 'Read only',
+		P: 'Replies only',
+		T: 'Topic creation',
+		L: 'Complete access'
+	};
+	for (var i in aModes) {
+		str += '<input type="radio" name="fam" id="f-am-' + i + '" value="' + i
+			+ '" onClick="faEditing.accessMode=value;updateFEditor()"'
+			+ (faEditing.accessMode == i ? ' checked="checked"' : '')
+			+ ' /><label for="f-am-' + i + '">' + aModes[i] + '</label> ';
+	}
 	str += '</td></tr>';
-	str += '<tr><th class="edheader">New threads:</th><td>';
-	str += '<label><input ' + alltt[141] + ' type="radio" name="nt" value="1" onClick="faEditing.userPost=true;updateFEditor()" ';
-	if	(faEditing.userPost)
-		str += 'checked="checked" ';
-	str += '/> Everyone</label> <label><input ' + alltt[142] + ' type="radio" name="nt" value="0" onClick="faEditing.userPost=false;updateFEditor()" ';
-	if	(!faEditing.userPost)
-		str += 'checked="checked" ';
-	str += '/> Moderators only</label></td></tr>';
-	if	(!faEditing.id)
-		str += '<tr><th class="edheader">Initial position:</th><td  ' + alltt[143] + ' id="faeipos">&nbsp;</td></tr>';
-	str += '<tr><th class="edheader">Description:</th><td><textarea ' + alltt[144] + ' name="fdesc" onKeyUp="faEditing.description=';
-	str += 'value.replace(/&lt;/g, \'<\').replace(/&gt;/g, \'>\').replace(/&amp;/g, \'&\');updateFEditor()"';
-	str += ' onChange="faEditing.description=value;updateFEditor()"';
-	str += ' rows="10" cols="48">' + faEditing.description.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</textarea></td></tr>';
-	str += '<td id="febut" colspan="2"><input type="button" name="feok" value="Ok" id="feok" onClick="forumEditOk();return false" ' + alltt[145] + ' />';
-	str += '&nbsp;<input type="button" name="feccl" value="Cancel" onClick="forumEditCancel();return false"  ' + alltt[146] + ' /></td></tr>';
-	str += '<tr><td colspan="2"><h3>Forum access</h3></td></tr>';
-	str += '<tr><td colspan="2" id="faeaccess"><p>(Loading access list data ...)</p></td></tr>';
 
-	str += '</table>';
+	if (!faEditing.id) {
+		str += '<tr><th class="edheader">Initial position:</th><td  '
+			+ alltt[143] + ' id="faeipos">&nbsp;</td></tr>';
+	}
+
+	str += '<tr><th class="edheader">Description:</th><td><textarea ' + alltt[144]
+		+ ' name="fdesc" id="fdesc" onKeyUp="faEditing.description=value;updateFEditor()"'
+		+ ' onChange="faEditing.description=value;updateFEditor()"'
+		+ ' rows="10" cols="48"></textarea></td></tr>'
+		+ '<tr><td id="febut" colspan="2"><input type="button" name="feok" value="Ok" id="feok" '
+		+ 'onClick="forumEditOk();return false" ' + alltt[145] + ' />'
+		+ '&nbsp;<input type="button" name="feccl" value="Cancel" onClick="forumEditCancel();return false" '
+		+ alltt[146] + ' /></td></tr>'
+		+ '<tr><td colspan="2"><h3>Forum access</h3></td></tr>'
+		+ '<tr><td colspan="2" id="faeaccess">&nbsp;</td></tr>'
+		+ '</table>';
 	document.getElementById('falist').innerHTML = str;
+
+	document.getElementById('fname').value = faEditing.name;
+	document.getElementById('fdesc').value = faEditing.description;
 	document.getElementById('feok').disabled = true;
-	if	(!faEditing.id)
+	if (!faEditing.id) {
 		updateFPosSelector();
+	}
+	drawFAccessManager();
 }
 
-function	updateFPosSelector()
-{
-	var	i, str = '<select name="fanewpos" onChange="faNewPos=this.options[this.selectedIndex].value">';
-	str += '<option value="-1">At the beginning</option>';
-	for	(i=0;i<faForums.length;i++)
-	{
+function updateFPosSelector() {
+	var i, str = '<select name="fanewpos" onChange="faNewPos=this.options[this.selectedIndex].value">'
+			+ '<option value="-1">At the beginning</option>';
+	for (i = 0; i < faForums.length; i ++) {
 		str += '<option value="' + faForums[i].id + '"';
-		if	(faNewPos == faForums[i].id)
+		if (faNewPos == faForums[i].id) {
 			str += ' selected="selected"';
+		}
 		str += '>After ' + faForums[i].name + '</option>';
 	}
 	str += '</select>';
 	document.getElementById('faeipos').innerHTML = str;
 }
 
-function	drawFAccessManager()
-{
+function drawFAccessManager() {
 	var	lnp = new Array(), lmd = new Array(), lrd = new Array(), ml, i, sc=0;
 	for	(i=0;i<faAccess.length;i++)
 	{
@@ -1268,6 +1330,8 @@
 		case 6: str += 'Probable bug: the forum to insert after wasn\'t transmitted.'; break;
 		case 7: str += 'A server error has occured.'; break;
 		case 8: str += 'This forum has already been deleted.'; break;
+		case 9: str += 'This forum has been cleansed and can no longer be recovered.'; break;
+		case 10: str += 'This forum has already been restored.'; break;
 		case 200: str += "You can\'t modify an alliance\'s forums while in vacation mode."; break;
 		default: str += 'An unknown error has happened.'; break;
 	}
diff -Naur beta5//site/static/beta5/js/pg_alliance.js forums//site/static/beta5/js/pg_alliance.js
--- beta5//site/static/beta5/js/pg_alliance.js	2011-02-05 10:09:56.434335002 +0100
+++ forums//site/static/beta5/js/pg_alliance.js	2011-03-12 14:56:07.421300050 +0100
@@ -80,6 +80,9 @@
 var	faOriACL;
 var	faNewPos;
 var	faAccess;
+var	faRanks;
+var	faAdmins;
+var	faPlayers;
 
 var	raRanks = new Array();
 var	raForums;
@@ -1239,11 +1242,9 @@
 
 // Forums administation
 
-function	switchToFAdminPage()
-{
+function switchToFAdminPage() {
 	clearUpdate();
-	if	(alPrivileges[5] != 1)
-	{
+	if (alPrivileges[5] != 1) {
 		switchToMainPage();
 		return;
 	}
@@ -1255,36 +1256,97 @@
 	x_getForums(forumsReceived);
 }
 
-function	AllianceForum(id,order,userPost,name,description)
-{
-	this.id			= id;
-	this.order		= order;
-	this.userPost		= userPost;
-	this.name		= name;
-	this.description	= description;
+function AllianceForum() {
+	this.id			= null;
+	this.accessMode		= 'T';
+	this.isDeleted		= false;
+	this.deletedAt		= null;
+	this.deletedBy		= null;
+	this.name		= '';
+	this.description	= '';
+	this.topics		= 0;
+	this.users		= new Array();
+	this.mods		= new Array();
+
+	this.parse = function (lines) {
+		var line = lines.shift().split('#');
+
+		// General information
+		this.id		= line.shift();
+		this.isDeleted	= (line.shift() == '1');
+		this.deletedBy	= line.shift();
+		this.deletedAt	= line.shift();
+		this.accessMode	= line.shift();
+		this.topics	= parseInt(line.shift(), 10);
+		this.name	= line.join('#');
+
+		// Users
+		line = lines.shift();
+		if (line != '') {
+			this.users = line.split('#');
+		}
+
+		// Moderators
+		line = lines.shift();
+		if (line != '') {
+			this.mods = line.split('#');
+		}
+
+		// Description
+		var dLines = parseInt(lines.shift(), 10);
+		for (var i = 0; i < dLines; i ++) {
+			this.description += (i > 0 ? "\n" : "") + lines.shift();
+		}
+
+		return this;
+	};
+
+	this.copyFrom = function (ori) {
+		this.id			= ori.id;
+		this.isDeleted		= ori.isDeleted;
+		this.deletedBy		= ori.deletedBy;
+		this.deletedAt		= ori.deletedAt;
+		this.accessMode		= ori.accessMode;
+		this.topics		= ori.topics;
+		this.name		= ori.name;
+		this.description	= ori.description
+
+		// Users
+		for (var i in ori.users) {
+			this.users.push(ori.users[i]);
+		}
+
+		// Moderators
+		for (var i in ori.mods) {
+			this.mods.push(ori.users[i]);
+		}
+
+		return this;
+	};
 }
 
-function	ForumACL(id,priv,name)
-{
+
+function ForumACL(id, priv, name) {
 	this.id		= id;
 	this.priv	= priv;
 	this.name	= name;
 	this.selected	= false;
 }
 
+
 function forumsReceived(data) {
 	if (amPage != 'FAdmin') {
 		return;
 	}
 
-	faForums = new Array();
 	if (data.indexOf("ERR#") == 0) {
-		alertForum(parseInt((data.split('#'))[1], 10));
-		puTimer = setTimeout('x_getForums(forumsReceived)', 180000);
-		return;
-	} else if (data != "") {
-		parseForumList(data);
+		var lines = data.split('\n');
+		var el = lines.shift();
+		data = lines.join('\n');
+
+		alertForum(parseInt((el.split('#'))[1], 10));
 	}
+	parseForumList(data);
 
 	if (!faEditing) {
 		drawForumList();
@@ -1292,15 +1354,19 @@
 		alertMaximumFCount();
 		forumEditCancel();
 	} else if (!faEditing.id) {
-		if	(faNewPos != -1 && !forumById(faNewPos))
+		if (faNewPos != -1 && !forumById(faNewPos)) {
 			faNewPos = -1;
+		}
 		updateFPosSelector();
 	} else if (faEditing.id) {
-		var	f = forumById(faEditing.id);
-		if (!f) {
+		var f = forumById(faEditing.id);
+		if (!f || f.isDeleted) {
 			alertForumDeleted();
 			forumEditCancel();
-		} else if (faOriginal.name != f.name || faOriginal.userPost != f.userPost || faOriginal.description != f.description) {
+		} else if (faOriginal.name != f.name
+			|| faOriginal.accessMode != f.accessMode
+			|| faOriginal.description != f.description) {
+
 			alertForumChanged();
 			updateFEditor();
 		} else {
@@ -1310,111 +1376,119 @@
 	puTimer = setTimeout('x_getForums(forumsReceived)', 180000);
 }
 
-function	parseForumList(data)
-{
-	var	dl = data.split('\n');
-	var	st = 0, i = 0, cf = 0;
-	var	a;
 
-	while	(i<dl.length)
-	{
-		if	(st == 0)
-		{
-			a = dl[i].split('#');
-			faForums[cf] = new AllianceForum(a.shift(),a.shift(),(a.shift()=="1"),a.join('#'),'');
-			st = 1;
-			i ++;
-		}
-		else if	(st == 1)
-		{
-			if	(dl[i].indexOf('+#') == 0)
-			{
-				a = dl[i].split('#');
-				a.shift();
-				a = a.join('#');
-				if	(faForums[cf].description != '')
-					faForums[cf].description += '\n';
-				faForums[cf].description += a;
-				i++;
-			}
-			else
-			{
-				cf++;
-				st = 0;
-			}
-		}
+function parseForumList(data) {
+	var lines = data.split('\n');
+	var fCount = parseInt(lines.shift(), 10);
+	var i;
+
+	faForums = new Array();
+	for (i = 0; i < fCount; i ++) {
+		faForums.push( new AllianceForum().parse(lines) );
+	}
+
+	var rCount = parseInt(lines.shift(), 10);
+	faRanks = { };
+	for (i = 0; i < rCount; i ++) {
+		var rLine = lines.shift().split('#');
+		faRanks['r' + rLine.shift()] = rLine.join('#');
 	}
 
-	faForums.sort(new Function('a','b','return (a.order > b.order ? 1 : -1)'));
+	i = lines.shift();
+	faAdmins = (i == '') ? (new Array()) : i.split('#');
+
+	var pCount = parseInt(lines.shift(), 10);
+	faPlayers = { };
+	for (i = 0; i < pCount; i ++) {
+		var rLine = lines.shift().split('#');
+		faPlayers['p' + rLine.shift()] = rLine.join('#');
+	}
 }
 
-function	moveForum(id,up)
-{
+
+function moveForum(id, up) {
 	x_moveForum(id, up ? 1 : 0, forumsReceived);
 }
 
-function	forumById(id)
-{
-	var	i;
-	for	(i=0;i<faForums.length&&faForums[i].id!=id;i++)
-		;
-	return	(i==faForums.length) ? false : faForums[i];
+function forumById(id) {
+	var i;
+	for (i = 0 ; i < faForums.length && faForums[i].id != id; i ++) ;
+	return (i==faForums.length) ? false : faForums[i];
 }
 
-function	deleteForum(id)
-{
-	var	f = forumById(id);
-	if	(!(f && confirmDeleteForum(f.name)))
+function deleteForum(id) {
+	var f = forumById(id);
+	if (!(f && confirmDeleteForum(f))) {
 		return;
+	}
 	x_delForum(id, forumsReceived);
 }
 
-function	createForum()
-{
-	faEditing = new AllianceForum(false,false,true,'','');
-	if	(faForums.length)
+function restoreForum(id) {
+	var f = forumById(id);
+	if (!f) {
+		return;
+	}
+	x_restoreForum(id, forumsReceived);
+}
+
+function createForum() {
+	faEditing = new AllianceForum();
+	if (faForums.length) {
 		faNewPos = faForums[faForums.length - 1].id;
-	else
+	} else {
 		faNewPos = -1;
+	}
 	displayForumEditor();
 }
 
-function	editForum(id)
-{
-	var	f = forumById(id);
-	if	(!f)
+function editForum(id) {
+	var f = forumById(id);
+	if (!f) {
 		return;
-	faEditing = new AllianceForum(id,f.order,f.userPost,f.name,f.description);
-	faOriginal = new AllianceForum(id,f.order,f.userPost,f.name,f.description);
+	}
+	faEditing = new AllianceForum().copyFrom(f);
+	faOriginal = f;
 	displayForumEditor();
 }
 
-function	displayForumEditor()
-{
+function displayForumEditor() {
 	document.getElementById('crforum').innerHTML = '';
+	computeForumACL();
 	drawForumEditor();
-	x_getForumAcl(faEditing.id ? faEditing.id : '', forumAclReceived);
 }
 
-function	forumAclReceived(data)
-{
+function computeForumACL() {
+	var feUsers, feMods, feAdmins;
+	feUsers = '#' + faEditing.users.join('##') + '#';
+	feMods = '#' + faEditing.mods.join('##') + '#';
+	feAdmins = '#' + faAdmins.join('##') + '#';
+
 	faAccess = new Array();
-	if	(data != "")
-	{
-		var	i, l = data.split('\n');
-		for	(i=0;i<l.length;i++)
-		{
-			var	a = l[i].split('#');
-			faAccess.push(new ForumACL(a.shift(), a.shift(), a.join('#')));
+	for (var i in faRanks) {
+		var rId = i.substr(1, i.length - 1);
+		var rn = faRanks[i];
+		var pr;
+
+		if (feUsers.indexOf('#' + rId + '#') != -1) {
+			pr = 1;
+		} else if (feMods.indexOf('#' + rId + '#') != -1) {
+			pr = 2;
+		} else if (feAdmins.indexOf('#' + rId + '#') != -1) {
+			pr = 3;
+		} else {
+			pr = 0;
 		}
-		faAccess.sort(new Function('a','b','return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1'));
+		faAccess.push(new ForumACL(rId, pr, rn));
 	}
-	if	(faEditing.id)
+	faAccess.sort(new Function('a','b','return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1'));
+
+	if (faEditing.id) {
 		faOriACL = makeForumACLString();
-	drawFAccessManager();
-	updateFEditor();
+	}
 }
 
+
 function	setFAccessLevel(level)
 {
 	var	i,cc=0;
@@ -1432,33 +1506,36 @@
 	}
 }
 
-function	updateFEditor()
-{
-	var	ok, i;
+function updateFEditor() {
+	var ok, i;
 	ok = (faEditing.name.length >= 4);
-	if	(faEditing.id)
+	if (faEditing.id) {
 		ok = ok && (
 			faEditing.name != faOriginal.name || faEditing.description != faOriginal.description
-			|| faEditing.userPost != faOriginal.userPost || faOriACL != makeForumACLString()
+			|| faEditing.accessMode != faOriginal.accessMode || faOriACL != makeForumACLString()
 		);
+	}
 	document.getElementById('feok').disabled = !ok;
 }
 
-function	makeForumACLString()
-{
-	var	a = new Array(), i;
-	for	(i=0;i<faAccess.length;i++)
-		if	(faAccess[i].priv != 3 && faAccess[i].priv != 0)
+function makeForumACLString() {
+	var a = new Array(), i;
+	for (i = 0; i < faAccess.length; i ++) {
+		if (faAccess[i].priv != 3 && faAccess[i].priv != 0) {
 			a.push(faAccess[i].id + '!' + faAccess[i].priv);
-	return	a.join('#');
+		}
+	}
+	return a.join('#');
 }
 
-function	forumEditOk()
-{
-	if	(faEditing.id)
-		x_changeForum(faEditing.id, faEditing.name, faEditing.userPost ? 1 : 0, faEditing.description, makeForumACLString(), forumEditCallback);
-	else
-		x_newForum(faEditing.name, faEditing.userPost ? 1 : 0, faNewPos, faEditing.description, makeForumACLString(), forumEditCallback);
+function forumEditOk() {
+	if (faEditing.id) {
+		x_changeForum( faEditing.id, faEditing.name, faEditing.accessMode, faEditing.description,
+			makeForumACLString(), forumEditCallback );
+	} else {
+		x_newForum( faEditing.name, faEditing.accessMode, faNewPos, faEditing.description,
+			makeForumACLString(), forumEditCallback );
+	}
 }
 
 function	forumEditCancel()
diff -Naur beta5//site/static/beta5/js/pg_comms-en.js forums//site/static/beta5/js/pg_comms-en.js
--- beta5//site/static/beta5/js/pg_comms-en.js	2011-02-05 10:09:56.434335002 +0100
+++ forums//site/static/beta5/js/pg_comms-en.js	2011-02-05 10:10:02.214335002 +0100
@@ -28,13 +28,15 @@
 	return	str;
 }
 
-function	makeTopicsText(tot, n)
-{
-	if	(tot == 0)
-		return	"empty forum";
-	var	str = '<b>' + formatNumber(tot) + '</b> topic' + (tot > 1 ? 's' : '');
-	if	(n == 0)
-		return	str;
-	str += ' (<b>' + formatNumber(n) + '</b> unread)';
-	return	str;
+function makeTopicsText(tot, n) {
+	if (tot == 0) {
+		return "empty forum";
+	}
+
+	var str = '<b>' + formatNumber(tot.toString()) + '</b> topic' + (tot > 1 ? 's' : '');
+	if (n == 0) {
+		return str;
+	}
+	str += ' (<b>' + formatNumber(n.toString()) + '</b> unread)';
+	return str;
 }
diff -Naur beta5//site/static/beta5/js/pg_comms.js forums//site/static/beta5/js/pg_comms.js
--- beta5//site/static/beta5/js/pg_comms.js	2011-02-05 10:09:56.434335002 +0100
+++ forums//site/static/beta5/js/pg_comms.js	2011-03-12 15:09:43.961300049 +0100
@@ -12,20 +12,40 @@
 	this.name	= name;
 }
 
-function	Category(id, type, name)
-{
-	this.id		= id;
-	this.type	= type;
-	this.name	= name;
-	this.forums	= new Array();
-}
 
-function	Forum(id, nTopics, nUnread, name)
-{
-	this.id		= id;
-	this.nTopics	= nTopics;
-	this.nUnread	= nUnread;
-	this.name	= name;
+
+function ForumsEntity(inputData) {
+	var iLine = inputData.shift().split('#');
+	var nElements;
+
+	this.type	= iLine.shift();
+	this.id		= iLine.shift();
+	nElements	= parseInt(iLine.shift(), 10);
+	this.topics	= parseInt(iLine.shift(), 10);
+	this.unread	= parseInt(iLine.shift(), 10);
+	this.name	= inputData.shift();
+	this.contents	= new Array();
+	this.output	= function (depth) {
+		var str = '';
+
+		if (this.id != '/') {
+			for (var i = 0; i < depth; i++) {
+				str += '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
+			}
+			str += '<a href="forums?cmd=V%23' + this.type + '%23' + this.id + '">' + this.name + '</a>: '
+				+ makeTopicsText(this.topics, this.unread);
+		}
+
+		for (var i = 0; i < this.contents.length; i ++) {
+			str += '<br/>' + this.contents[i].output(depth + 1);
+		}
+
+		return str;
+	};
+
+	for (var i = 0; i < nElements; i ++) {
+		this.contents.push( new ForumsEntity(inputData) );
+	}
 }
 
 
@@ -35,110 +55,55 @@
 }
 
 
-function	commsDataReceived(data)
-{
-	var	i, l = data.split('\n');
-	var	a = l.shift().split('#');
-	var	nCustom = parseInt(a[0],10), nGenCats = parseInt(a[1],10), nAForums = parseInt(a[2],10);
+function commsDataReceived(data) {
+	var i, a, nCustom;
+	var l = data.split('\n');
 
 	// Default folders
 	dFolders = new Array();
-	for	(i=0;i<3;i++)
-	{
+	for (i=0;i<3;i++) {
 		a = l.shift().split('#');
 		dFolders.push(new Folder('', a[0], a[1], ''));
 	}
 
 	// Custom folders
+	nCustom = parseInt(l.shift(), 10);
 	cFolders = new Array();
-	for	(i=0;i<nCustom;i++)
-	{
+	for (i=0;i<nCustom;i++) {
 		a = l.shift().split('#');
 		cFolders.push(new Folder(a.shift(), a.shift(), a.shift(), a.join('#')));
 	}
 
 	// General categories & forums
-	genForums = new Array();
-	for	(i=0;i<nGenCats;i++)
-	{
-		a = l.shift().split('#');
-
-		var	j,c,id,tp,nForums;
-		id = a.shift(); tp = a.shift();
-		nForums = parseInt(a.shift(), 10);
-		c = new Category(id, tp, a.join('#'));
-
-		for	(j=0;j<nForums;j++)
-		{
-			a = l.shift().split('#');
-			c.forums.push(new Forum(a.shift(),a.shift(),a.shift(),a.join('#')));
-		}
-
-		genForums.push(c);
-	}
-
-	// Alliance forums
-	aForums = new Array();
-	for	(i=0;i<nAForums;i++)
-	{
-		a = l.shift().split('#');
-		aForums.push(new Forum(a.shift(),a.shift(),a.shift(),a.join('#')));
-	}
+	forums = new ForumsEntity(l);
 
 	drawCommsPage();
 	setTimeout('x_getCommsData(commsDataReceived)', 30000);
 }
 
 
-function	drawCommsPage()
-{
-	var	i, j, a;
+function drawCommsPage() {
+	var i, j, a;
 
 	// Default folders
-	for	(i=0;i<3;i++)
+	for (i=0;i<3;i++) {
 		document.getElementById('msg' + i).innerHTML = makeMessagesText(dFolders[i].tMsg, dFolders[i].nMsg);
+	}
 
 	// Custom folders
-	if	(cFolders.length == 0)
+	if (cFolders.length == 0) {
 		document.getElementById('cflist').innerHTML = noCustomFolders;
-	else
-	{
+	} else {
 		a = new Array();
-		for	(i=0;i<cFolders.length;i++)
-		{
-			var	s = '<a href="message?a=f&f=C&cf=' + cFolders[i].id;
-			s += '" ' + comtt[0] + ' >' + cFolders[i].name + '</a>: ' + makeMessagesText(cFolders[i].tMsg, cFolders[i].nMsg);
+		for (i=0;i<cFolders.length;i++) {
+			var s = '<a href="message?a=f&f=C&cf=' + cFolders[i].id;
+			s += '" ' + comtt[0] + ' >' + cFolders[i].name + '</a>: '
+				+ makeMessagesText(cFolders[i].tMsg, cFolders[i].nMsg);
 			a.push(s);
 		}
 		document.getElementById('cflist').innerHTML = a.join('<br/>') + '<br/>';
 	}
 
-	// General forums
-	a = new Array();
-	for	(i=0;i<genForums.length;i++)
-	{with(genForums[i]){
-		var	s = '<a href="forums?cmd=C%23G%23' + id + '" ' + comtt[1] + ' >' + name + '</a>';
-		for	(j=0;j<forums.length;j++)
-		{
-			s += '<br/>&nbsp;&nbsp;-&nbsp;<a href="forums?cmd=F%23' + type + '%23' + forums[j].id + '" ' + comtt[2] + ' >' + forums[j].name + '</a>: ';
-			s += makeTopicsText(forums[j].nTopics, forums[j].nUnread);
-		}
-		a.push(s);
-	}}
-	document.getElementById('gforums').innerHTML = a.join('<br/><br/>');
-
-	// Alliance forums
-	if	(aForums.length == 0)
-		document.getElementById('aforums').innerHTML = '&nbsp;';
-	else
-	{
-		a = new Array();
-		for	(j=0;j<aForums.length;j++)
-		{
-			s  = '&nbsp;&nbsp;-&nbsp;<a href="forums?cmd=F%23A%23' + aForums[j].id + '" ' + comtt[3] + ' >' + aForums[j].name + '</a>: ';
-			s += makeTopicsText(aForums[j].nTopics, aForums[j].nUnread);
-			a.push(s);
-		}
-		document.getElementById('aforums').innerHTML = '<h2>' + allianceForums + '</h2><p>' + a.join('<br/>') + '</p>';
-	}
+	// Forums
+	document.getElementById('forums').innerHTML = '<p>' + forums.output(-1) + '</p>';
 }
diff -Naur beta5//site/static/beta5/js/pg_forums-en.js forums//site/static/beta5/js/pg_forums-en.js
--- beta5//site/static/beta5/js/pg_forums-en.js	2011-02-05 10:09:56.434335002 +0100
+++ forums//site/static/beta5/js/pg_forums-en.js	2011-02-05 10:10:02.214335002 +0100
@@ -1,53 +1,98 @@
-function	confirmDelete()
-{
-	var	i = countSelected();
-	if	(i == 0)
-	{
-		alert('Please select the topic(s) you want to delete.');
-		return	false;
-	}
-	return	confirm('Please confirm you want to delete ' + (i > 1 ? ('these ' + i + ' topics') : 'this topic') + '.');
-}
-
-function	confirmSticky()
-{
-	var	i = countSelected();
-	if	(i == 0)
-	{
-		alert('Please select the topic(s) you want to switch to/from sticky.');
-		return	false;
-	}
-	return	confirm('Please confirm you want to switch ' + (i > 1 ? ('these ' + i + ' topics') : 'this topic') + ' to/from sticky.');
-}
-
-function	confirmMove()
-{
-	var	i = countSelected();
-	if	(i == 0)
-	{
-		alert('Please select the topic(s) you want to move.');
-		return	false;
-	}
-
-	var	e = document.getElementById('mdest');
-	if	(e.options[e.selectedIndex].value == '')
-	{
-		alert('Please select the forum to which the topic'+(i>1?'s':'')+' must be moved.');
-		return	false;
-	}
-
-	return	confirm(
-			'Please confirm you want to move the selected topic' + (i>1?'s':'') + '\nto the "'
-			+ e.options[e.selectedIndex].text + '" forum.'
-		);
-}
-
-function	confirmDTopic()
-{
-	return	confirm('Deleting this post will delete the whole topic. Please confirm.');
-}
-
-function	confirmDPost()
-{
-	return	confirm('Please confirm you want to delete this post.');
-}
+/* Text for menu commands */
+MenuItem.commandText = {
+	overview:	"Overview",
+	latest:		"Latest messages",
+	search:		"Search forums"
+};
+
+/* Overview pseudo-category title & description */
+CategoryView.ovTitle = "Forums overview";
+CategoryView.ovDescription = "This page gives you a global view of all of the forums you have access to.";
+
+/* Text for layout-level commands */
+ForumsLayout.menuText = ['S','h','o','w','&nbsp;','m','e','n','u'];
+ForumsLayout.hideMenu = "Hide";
+ForumsLayout.menuTitle = "Forums";
+
+/* Text for the category view */
+CategoryView.empty = 'There are no forums in this category.';
+CategoryView.headers = {
+	name: 'Forum name',
+	topics: 'Topics',
+	posts: 'Posts',
+	lastPost: 'Last modification'
+};
+CategoryView.forumIcon = {
+	read: "This forums's topics have been read",
+	unread: "Some topics in this forum haven't been read"
+};
+CategoryView.deletedAt = "Forum deleted at ";
+CategoryView.by = " by ";
+CategoryView.noPosts = 'Empty forum';
+CategoryView.markRead = 'Mark forums as read';
+
+/* Text for the forum view */
+ForumView.markRead = 'Mark topics as read';
+ForumView.adminsHdr = 'Administrators';
+ForumView.modsHdr = 'Moderators';
+ForumView.usersHdr = 'Users';
+ForumView.empty = 'There are no topics in this forum.';
+ForumView.details = ' More details ... ';
+ForumView.showDetails = 'Show details';
+ForumView.hideDetails = 'Hide details';
+ForumView.displayOptions = 'Display options';
+ForumView.newTopic = 'New topic';
+ForumView.modTools = 'Moderation tools';
+ForumView.previousPage = 'Previous page';
+ForumView.nextPage = 'Next page';
+ForumView.pageSelHdr = 'Jump to page ';
+ForumView.headers = {
+	topic: 'Topic',
+	replies: 'Replies',
+	fPost: 'First post',
+	lPost: 'Last update'
+};
+ForumView.movedTo = 'Moved to ';
+ForumView.applyTo = 'Apply to ';
+ForumView.thisForum = 'this forum';
+ForumView.allForums = 'all forums';
+ForumView.perPage = 'Topics / page:';
+ForumView.ok = 'Ok';
+ForumView.cancel = 'Cancel';
+ForumView.displayDeleted = 'Display deleted topics:';
+ForumView.deletedAt = 'Topic deleted at ';
+ForumView.hideModTools = 'Hide moderation tools';
+ForumView.pleaseSelect = 'Please select at least one topic.';
+ForumView.deletedTopics = 'Deleted topics: ';
+ForumView.restoreTopics = 'restore';
+ForumView.selectedTopics = 'Selected topics: ';
+ForumView.deleteTopics = 'delete';
+ForumView.stickyLevel = 'sticky level: ';
+ForumView.decreaseStickyLevel = 'decrease';
+ForumView.increaseStickyLevel = 'increase';
+ForumView.setTo = 'set to ';
+ForumView.normalPost = 'not sticky';
+ForumView.lock = 'lock';
+ForumView.unlock = 'unlock';
+ForumView.moveTo = 'Move topics to ';
+
+
+/* Topic view: topic not found */
+TopicView.notFound = {
+	title: "Topic not found",
+	text: "The topic you were looking for is unavailable, either because it doesn't exist anymore or "
+		+ "because you don't have access to the forum it is in."
+};
+/* Topic view: deleted topic */
+TopicView.deleted = {
+	header: "This topic was deleted at ",
+	by: " by "
+};
+/* Topic view: show / hide post contents */
+TopicView.close = "Hide post contents";
+TopicView.open = "Show post contents";
+/* Topic view, misc text */
+TopicView.posted = "Posted ";
+TopicView.loading = "Loading, please wait ...";
+TopicView.loadError = "An error occurred while loading this post :-(";
+TopicView.edited = "Edited at ";
diff -Naur beta5//site/static/beta5/js/pg_forums.js forums//site/static/beta5/js/pg_forums.js
--- beta5//site/static/beta5/js/pg_forums.js	2011-02-05 10:09:56.434335002 +0100
+++ forums//site/static/beta5/js/pg_forums.js	2011-02-05 10:10:02.204335002 +0100
@@ -1,10 +1,1607 @@
-function	countSelected()
-{
-	var	n = 0, i = 0, e;
-	while	(e = document.getElementById('msel' + i))
-	{
-		n += e.checked ? 1 : 0;
-		i++;
+var pageContents;
+var useGIFs;
+
+
+MenuItem = function (lines) {
+
+	// Parse the data
+	var fields = lines.shift().split('#');
+
+	this.isNode	= (fields.shift() == 'N');
+	this.isCommand	= (fields.shift() == 'C');
+	this.unread	= parseInt(fields.shift(), 10);
+	this.isOpen	= this.isNode ? (fields.shift() == '1') : false;
+	this.id		= this.isNode ? fields.shift() : null;
+	this.cmdLink	= lines.shift();
+	this.text	= lines.shift();
+	this.entries	= (this.isNode && this.isOpen) ? (new Array()) : null;
+
+	if (this.isCommand) {
+		this.text = MenuItem.commandText[this.text];
 	}
-	return	n;
-}
+
+	// Add menu entries
+	while (this.isNode && this.isOpen && lines[0] != 'E') {
+		if (lines[0] == 'S') {
+			// Separators
+			this.entries.push(null);
+			lines.shift();
+		} else {
+			// Entries
+			this.entries.push(new MenuItem(lines));
+		}
+	}
+	if (this.isNode && this.isOpen) {
+		lines.shift();
+	}
+
+	this.draw = function (output, depth) {
+		var rd = (typeof depth == 'undefined') ? -1 : depth;
+
+		var cst = ' style="margin: 0px; padding: 0px; border-width: 0px; vertical-align: middle"';
+		var tst = ' style="width: 100%; margin: 0px; padding: 0px; border-width: 0px"';
+		var smallCell = '<td style="width: 16px; margin: 0px; padding: 0px; border-width: 0px">';
+		var str = '<tr><td' + cst + '><table' + tst + '><tr>';
+
+		if (depth > 0) {
+			for (var i = 0; i < depth; i ++) {
+				str += smallCell + '&nbsp;</td>';
+			}
+		}
+
+		// 'Open'/'Close' command for submenus or empty cell
+		str += smallCell;
+		if (depth > -1 && this.isNode) {
+			if (this.isOpen) {
+				str += '<a href="#" onClick="main.menuClose(\'' + this.id + '\'); return false">-</a>';
+			} else {
+				str += '<a href="#" onClick="main.menuOpen(\'' + this.id + '\'); return false">+</a>';
+			}
+		} else {
+			str += '&nbsp;';
+		}
+		str += '</td>';
+
+		// Menu entry's text
+		str += '<td' + cst + '><a href="?cmd=' + this.cmdLink.replace(/#/g, '%23')
+			+ '" style="text-decoration:none;font-style: normal">'
+			+ (MenuItem.current == this.cmdLink ? '<em><u>' : '')
+			+ ((this.unread > 0 && ! (this.isNode && this.isOpen)) ? '<b>' : '')
+			+ this.text
+			+ ((this.unread > 0 && ! (this.isNode && this.isOpen)) ? (' (' + this.unread + ')</b>') : '')
+			+ (MenuItem.current == this.cmdLink ? '</u></em>' : '')
+			+ '</a></td></tr></table></td></tr>';
+
+		// Output this line
+		output.push(str);
+
+		// Submenus
+		var sepPrev = false;
+		for (var i in this.entries) {
+			var e = this.entries[i];
+			if (e) {
+				if (e.isNode && e.isOpen && !sepPrev) {
+					output.push('<tr><td' + cst + '>&nbsp;</td></tr>');
+				}
+				e.draw(output, rd + 1);
+				if (e.isNode && e.isOpen) {
+					output.push('<tr><td' + cst + '>&nbsp;</td></tr>');
+					sepPrev = true;
+				} else {
+					sepPrev = false;
+				}
+			} else if (!sepPrev) {
+				output.push('<tr><td' + cst + '>&nbsp;</td></tr>');
+				sepPrev = true;
+			}
+		}
+	};
+};
+
+
+function parseNames( lines ) {
+	var line, cnt;
+	var res = { };
+
+	cnt = parseInt( lines.shift(), 10 );
+	for (var i = 0; i < cnt; i ++) {
+		line = lines.shift().split('#');
+		res[line[0]] = line[1];
+	}
+
+	return res;
+};
+
+
+TopicView = function () {
+
+	var me = this;
+	var tst = ' style="width: 100%; margin: 0px; padding: 0px; border-width: 0px"';
+	var cst = ' style="margin: 0px; padding: 0px; border-width: 0px; vertical-align: middle';
+
+	var pageUpdater = null;
+	var pageMD5 = null;
+	var topicId = null;
+	var locked = false;
+	var parents = [];
+	var posts = [];
+	var pOrder = {
+		ln: null,
+		lo: null,
+		tn: null,
+		to: null
+	};
+	var opts = {
+		perPage: 50,
+		vDeleted: false,
+		threaded: false,
+		order: false,
+		openPosts: 1
+	};
+	var title;
+	var topicStatus;
+	var currentUser;
+	var isMod;
+	var canPost;
+	var hasPoll;
+	var users;
+	var nPages;
+	var cPage;
+	var queue;
+
+	var parseInitialContents, displayNotFound, displayDeleted, displayTopicLayout, drawPageChanger,
+		displayPage, initLoaderQueue, loadPost, postLoaded;
+
+	parseInitialContents = function (data) {
+		var line = data.shift();
+
+		if (line == 'MEH') {
+			displayNotFound();
+			return false;
+		}
+
+		if (line.indexOf('DELETED#') == 0) {
+			var deletedAt, nParents;
+			line = line.split('#');
+			deletedAt = line[1];
+			nParents = parseInt(line[2], 10);
+			displayDeleted(data, deletedAt, nParents);
+			return false;
+		}
+
+		line = line.split('#');
+		var nParents = parseInt(line[1], 10);
+
+		title = data.shift();
+		line = data.shift().split('#');
+		currentUser = line.shift();
+		isMod = (line.shift() == 1);
+		canPost = (line.shift() == 1);
+		hasPoll = (line.shift() == 1);
+
+		for (var i = 0; i < nParents; i ++) {
+			line = data.shift().split('#');
+			parents.push({
+				id: line.shift(),
+				name: line.join('')
+			});
+		}
+
+		pOrder.ln = data.shift().split('#');
+		pOrder.lo = data.shift().split('#');
+		pOrder.tn = data.shift().split('#');
+		pOrder.to = data.shift().split('#');
+
+		for (var i = 0; i < pOrder.ln.length; i ++) {
+			var p = { };
+			line = data.shift().split('#');
+			p.id = line.shift();
+			p.depth = parseInt(line.shift(), 10);
+			p.author = line.shift();
+			p.postedAt = line.shift();
+			p.unread = (line.shift() == 1);
+			p.lcTime = line.shift();
+			p.lcAuthor = line.shift();
+			p.title = data.shift();
+			p.loading = false;
+			posts[p.id] = p;
+		}
+
+		users = parseNames( data );
+
+		line = data.shift().split('#');
+		opts.perPage = parseInt(line.shift(), 10);
+		opts.vDeleted = (line.shift() == 1);
+		opts.threaded = (line.shift() == 1);
+		opts.order = (line.shift() == 1);
+		opts.openPosts = parseInt(line.shift(), 10);
+
+		for (var i in posts) {
+			switch (opts.openPosts) {
+				case 0:	posts[i].open = false;
+					break;
+				case 1: posts[i].open = posts[i].unread;
+					break;
+				case 2: posts[i].open = true;
+					break;
+			}
+		}
+
+		initLoaderQueue();
+
+		var m = pOrder.ln.length % opts.perPage;
+		nPages = (pOrder.ln.length - m) / opts.perPage + (m ? 1 : 0);
+		cPage = 0;
+		displayTopicLayout();
+	};
+
+	displayNotFound = function () {
+		document.getElementById('f-page').innerHTML = '<h1>' + TopicView.notFound.title + '</h1><p>'
+			+ TopicView.notFound.text + '</p>';
+	};
+
+	displayDeleted = function (data, deletedAt, nParents) {
+		var str = '<div style="padding: 0px 0px 5px 5px">'
+			+ '<h1 style="padding: 0px">' + data.shift() + '</h1>';
+		for (var i = 0; i < nParents; i ++) {
+			var l = data.shift().split('#');
+			var pid = l.shift();
+
+			str += (i > 0 ? ' &gt; ' : '') + '<a href="?cmd=V%23' + (i == nParents - 1 ? 'F' : 'C')
+				+ '%23' + pid + '">' + (pid == '/' ? ForumsLayout.menuTitle : l.join('')) + '</a>';
+		}
+		str += '</div><p>' + TopicView.deleted.header + '<b>' + formatDate(deletedAt)
+			+ '</b>' + TopicView.deleted.by + '<b>'
+			+ 'some guy (FIXME)' + '</b>.</p>';
+		document.getElementById('f-page').innerHTML = str;
+	};
+
+	displayTopicLayout = function () {
+		var str = '<div style="padding: 0px 0px 0px 5px"><h1 style="padding: 0px">'
+			+ title + '</h1>';
+		for (var i in parents) {
+			str += (i > 0 ? ' &gt; ' : '') + '<a href="?cmd=V%23' + (i == parents.length - 1 ? 'F' : 'C')
+				+ '%23' + parents[i].id + '">'
+				+ (parents[i].id == '/' ? ForumsLayout.menuTitle : parents[i].name) + '</a>';
+		}
+
+		str += '<table style="width: 100%; margin: 10px 0px 0px 0px; padding: 0px; border-width: 0px"><tr>';
+		str += drawPageChanger();
+
+		// FIXME: display tools
+
+		if (hasPoll) {
+			// FIXME: display poll
+		}
+
+		str += '<tr><td colspan="3"' + cst + '" id="topics-display">&nbsp;</td></tr>';
+		str += drawPageChanger();
+		str += '</table>';
+
+		document.getElementById('f-page').innerHTML = str;
+		displayPage();
+	};
+
+	drawPageChanger = function () {
+		if (nPages > 1) {
+			var st = 'border-width: 0px; margin: 0px; vertical-align: middle; ';
+			var str = '<tr><td style="' + st + 'padding: 5px 0px 0px 0px; text-align:left; width: 33%">'
+				+ (cPage > 0 ? '<a href="#" onClick="pageContents.prevPage(); return false">' : '')
+				+ '&lt;-- ' + ForumView.previousPage + (cPage > 0 ? '</a>' : '')
+				+ '</td><td style="' + st + 'padding: 5px 0px 0px 0px; text-align:center">'
+				+ '<form style="' + st + 'padding: 0px; ">' + ForumView.pageSelHdr
+				+ '<select onChange="pageContents.jumpToPage(options[selectedIndex].value)">';
+			for (var i = 0; i < nPages; i ++) {
+				str += '<option ' + (i == cPage ? 'selected="selected" ' : '') + 'value="'
+					+ i + '">' + (i + 1) + '</option>';
+			}
+			str += '</select></form></td><td style="' + st
+				+ '; padding: 5px 0px 0px 0px; text-align: right; width: 33%">'
+				+ ((cPage<nPages-1) ? '<a href="#" onClick="pageContents.nextPage(); return false">' : '')
+				+ ForumView.nextPage + '--&gt;' + ((cPage<nPages-1) ? '</a>' : '') + '</td></tr>';
+			return str;
+		}
+		return '';
+	};
+
+	displayPage = function () {
+		var low = cPage * opts.perPage;
+		var high = Math.min(low + opts.perPage, pOrder.ln.length);
+		var str = '\n\n<table' + tst + '>';
+		var order;
+
+		if (opts.threaded) {
+			order = opts.order ? pOrder.to : pOrder.tn;
+		} else {
+			order = opts.order ? pOrder.lo : pOrder.ln;
+		}
+
+		for (var i = low; i < high; i ++) {
+			var post = posts[order[i]];
+			str += '\n<tr><td' + cst + '"><table' + tst + '><tr>';
+			if (opts.threaded && post.depth > 0) {
+				var sz = post.depth * 10;
+				str += '<td style="width: ' + sz + 'px; padding: 0px; margin: 0px">&nbsp;</td>';
+			}
+			str += '<td style="padding: 0px; margin: 0px" id="tp-post-' + post.id
+				+ '">&nbsp</td></tr></table></td></tr>';
+		}
+		document.getElementById('topics-display').innerHTML = str + '\n</table>';
+
+		displayPosts();
+	};
+
+	displayPosts = function () {
+		for (var i in posts) {
+			var post = posts[i];
+			var el = document.getElementById("tp-post-" + post.id);
+			if (el) {
+				el.innerHTML = displayPost(post);
+			}
+		}
+	};
+
+	displayPost = function (post) {
+		var color = post.unread ? '#FFFFFF' : '#5F5F5F';
+		var margin = post.open ? (post.depth == 0 ? '0px 0px 10px 0px' : '10px 0px') : '0px';
+		var onClick = ' onClick="pageContents.togglePost(' + post.id + ')"';
+		var str = '<table style="width: 100%; margin: ' + margin + '; padding: 0px;border-collapse: '
+			+ 'collapse; border: 1px solid ' + color + '"><tr><td rowspan="2" style="'
+			+ 'width: 32px; border: 1px solid ' + color + '; padding: 0px;'
+			+ 'vertical-align: middle"' + onClick + '><img src="' + staticurl
+			+ '/beta5/pics/post_' + (post.open ? 'o' : 'c') + (post.unread ? 'n' : 'o')
+			+ '.gif" alt="' + (post.open ? TopicView.close : TopicView.open) + '" /></td>'
+			+ '<th style="padding: 1px; text-align: left; border: 1px solid ' + color + '"' + onClick + '>'
+			+ post.title + '</th></tr><tr><td style="padding: 1px; border: 1px solid ' + color + '"'
+			+ onClick + '>' + TopicView.posted + '<b>' + formatDate(post.postedAt) + '</b>'
+			+ CategoryView.by + '<b>' + users[post.author] + '</b></td></tr>';
+
+		if (post.open) {
+			str += '<tr><td style="padding: 10px 5px; border: 1px solid ' + color + '" colspan="2"'
+				+ ' id="post-contents-' + post.id + '">'
+				+ (post.loaded ? post.contents :
+					('<p style="padding: 0px 5px"><i>' + TopicView.loading + '</i></p>'))
+				+ '</td></tr>';
+			if (post.lcTime != post.postedAt) {
+				str += '<tr><td style="padding: 1px 5px; border: 1px solid ' + color
+					+ '; text-align: right" colspan="2"><i>' + TopicView.edited
+					+ formatDate(post.lcTime) + CategoryView.by + '<b>'
+					+ users[post.lcAuthor] + '</b></i></td></tr>';
+			}
+		}
+			
+		return str + '</tr></table>';
+	};
+
+	initLoaderQueue = function () {
+		var order;
+
+		if (opts.threaded) {
+			order = opts.order ? pOrder.to : pOrder.tn;
+		} else {
+			order = opts.order ? pOrder.lo : pOrder.ln;
+		}
+
+		var openPosts = [], fpPosts = [], rest = [];
+		for (var i in order) {
+			var post = posts[order[i]];
+			if (post.open) {
+				openPosts.push(post);
+			} else if (i < opts.perPage) {
+				fpPosts.push(post);
+			} else {
+				rest.push(post);
+			}
+		}
+
+		queue = openPosts.concat(fpPosts.concat(rest));
+		loadPost();
+	};
+
+	postLoaded = function (data) {
+		if (queue.length == 0) {
+			return;
+		}
+
+		if (data.indexOf('-#') == 0) {
+			data = data.split('#');
+			if (queue[0].id != data[1]) {
+				loadPost();
+				return;
+			}
+			queue[0].loading = false;
+			queue[0].contents = '<p style="padding: 10px"><i>' + TopicView.loadError + '</i></p>';
+		} else {
+			data = data.split('\n');
+			var l = data.shift().split('#');
+			if (queue[0].id != l[1]) {
+				loadPost();
+				return;
+			}
+			queue[0].loading = false;
+			queue[0].contents = data.join('\n');
+		}
+		queue[0].loaded = true;
+
+		var el = document.getElementById("tp-post-" + queue[0].id);
+		if (el) {
+			el.innerHTML = displayPost(queue[0]);
+		}
+
+		queue.shift();
+		loadPost();
+	};
+
+	loadPost = function () {
+		if (queue.length == 0 || queue[0].loading) {
+			return;
+		}
+
+		queue[0].loading = true;
+		x_loadPostContents(topicId, pageMD5, queue[0].id, function (data) {
+			postLoaded(data);
+		});
+	};
+
+	this.parse = function (data) {
+		if (data.indexOf('\n') == -1) {
+			// IE, first update
+			data = data.split('#');
+			topicId = data.shift();
+			pageMD5 = data.shift();
+			this.update();
+			return;
+		}
+
+		data = data.split('\n');
+		var l = data.shift().split('#');
+		topicId = l.shift();
+		pageMD5 = l.shift();
+		if (parseInitialContents(data)) {
+			pageUpdater = window.setTimeout("pageContents.update()", 10000);
+		}
+		locked = false;
+	};
+
+	this.update = function () {
+		if (locked) {
+			return;
+		}
+		pageUpdater = null;
+		locked = true;
+		x_getTopic(topicId, pageMD5, function (data) { pageContents.parse(data); });
+	};
+
+	this.togglePost = function (postId) {
+		var post = posts[postId];
+		post.open = ! post.open;
+		var el = document.getElementById("tp-post-" + post.id);
+		if (el) {
+			el.innerHTML = displayPost(post);
+		}
+	};
+
+	this.setFieldValues = function () { };
+};
+
+
+ForumView = function () {
+	var me = this;
+
+	var pageUpdater = null;
+	var locked = false;
+	var pageMD5 = null;
+	var viewId = null;
+	var players = [];
+
+	var nPages = 0;
+	var cPage = -1;
+	var perPage = 10;
+
+	var topics = [];
+	var isMod = false;
+	var canPost = false;
+	var vDeleted = false;
+	var unread = false;
+	var admins = null;
+	var mods = null;
+	var users = null;
+	var hasMoveTargets = false;
+	var moveTargets = {};
+	var description = '';
+	var title = '';
+	var parents = null;
+
+	var showDetails = false;
+	var showModTools = false;
+	var modSelAll = false;
+	var showOptions = false;
+	var options = { };
+	var modTools = { };
+	var selTopicsStr = '';
+
+	var mainParser, parseTopic, displayContents, drawControls, drawModTools, setFieldValues, getSelectedTopics;
+
+	mainParser = function (lines) {
+		var line = lines.shift().split('#');
+		var nt, ndl, npe, na, nm, nu, nmt;
+
+		forumId		= line.shift();
+		nt		= parseInt(line.shift(), 10);
+		unread		= (line.shift() == '1');
+		isMod		= (line.shift() == '1');
+		canPost		= (line.shift() == '1');
+		perPage		= parseInt(line.shift(), 10);
+		vDeleted	= (line.shift() == '1');
+		ndl		= parseInt(line.shift(), 10);
+		npe		= parseInt(line.shift(), 10);
+		na		= parseInt(line.shift(), 10);
+		nm		= parseInt(line.shift(), 10);
+		nu		= parseInt(line.shift(), 10);
+		nmt		= isMod ? parseInt(line.shift(), 10) : 0;
+		hasMoveTargets	= (nmt > 0);
+		title		= lines.shift();
+
+		var dc = new Array();
+		for (var i = 0; i < ndl; i ++) {
+			dc.push(lines.shift());
+		}
+		description = dc.join('<br/>');
+
+		parents = new Array();
+		for (var i = 0; i < npe; i ++) {
+			line = lines.shift().split('#');
+			parents.push({
+				id: line.shift(),
+				name: line.join('#')
+			});
+		}
+
+		admins = new Array();
+		for (var i = 0; i < na; i ++) {
+			admins.push(lines.shift());
+		}
+		mods = new Array();
+		for (var i = 0; i < nm; i ++) {
+			mods.push(lines.shift());
+		}
+		users = new Array();
+		for (var i = 0; i < nu; i ++) {
+			users.push(lines.shift());
+		}
+
+		moveTargets = {};
+		for (var i = 0; i < nmt; i ++) {
+			line = lines.shift().split('#');
+			var ln = line.shift();
+			moveTargets[ln] = line.join('#');
+		}
+
+		selTopicsStr = '';
+		for (var i in topics) {
+			if (topics[i].selected) {
+				selTopicsStr += '#' + topics[i].id + '#';
+			}
+		}
+
+		topics = new Array();
+		for (var i = 0; i < nt; i ++) {
+			topics.push(parseTopic(lines));
+		}
+
+		if (nt == 0) {
+			cPage = -1;
+		} else {
+			nPages = Math.ceil(nt / perPage);
+			if (cPage < 0) {
+				cPage = 0;
+			} else if (cPage >= nPages) {
+				cPage = nPages - 1;
+			}
+		}
+	};
+
+	parseTopic = function (lines) {
+		var topic = { };
+		var line = lines.shift().split('#');
+
+		topic.id	= line.shift();
+		topic.movedTo	= line.shift();
+		topic.unread	= (line.shift() == '1');
+		topic.sticky	= parseInt(line.shift(), 10);
+		topic.nReplies	= line.shift();
+		topic.fpTime	= line.shift();
+		topic.fpAuthor	= line.shift();
+		topic.lcTime	= line.shift();
+		topic.lcAuthor	= line.shift();
+		topic.isLocked	= (line.shift() == '1');
+		topic.hasPoll	= (line.shift() == '1');
+		topic.isDeleted	= (line.shift() == '1');
+		topic.deletedAt	= line.shift();
+		topic.deletedBy	= line.shift();
+		topic.title	= lines.shift();
+		topic.mToForum	= (topic.movedTo == '') ? null : lines.shift();
+		topic.selected	= (selTopicsStr.indexOf( '#' + topic.id + '#') != -1);
+
+		return topic;
+	};
+
+	getSelectedTopics = function (deleted) {
+		var rv = new Array();
+		for (var i = cPage * perPage; i < topics.length && i < (cPage + 1) * perPage; i ++) {
+			if (topics[i].movedTo == '' && topics[i].selected && topics[i].isDeleted == deleted) {
+				rv.push(topics[i].id);
+			}
+		}
+		return rv;
+	};
+
+	drawControls = function () {
+		// Main controls
+		var str = '<tr><td style="padding: 10px 0px; border-width: 0px">'
+			+ '<table style="width: 100%; padding: 0px 10px; border-width: 0px; margin: 0px">'
+			+ '<tr><td style="padding: 0px; border-width: 0px; margin: 0px; text-align:center" colspan="3">'
+			+ '<div id="f-std-ctrl" style="width: 100%; text-align: center'
+			+ ((showModTools || showOptions) ? '; display: none' : '') + '">'
+			+ '<a href="#" onClick="pageContents.displayOptions();return false">'
+			+ ForumView.displayOptions + '</a>';
+
+		if (canPost) {
+			str += ' - <a href="?cmd=C%23' + forumId + '">' + ForumView.newTopic + '</a>';
+		}
+		if (isMod) {
+			str += ' - <a href="#" onClick="pageContents.displayModTools();return false">'
+				+ ForumView.modTools + '</a>';
+		}
+
+		// Display options
+		str += '</div><fieldset id="f-options" style="margin: 0px; padding:0px'
+			+ (showOptions ? '; display: block' : '; display: none') + '">'
+			+ '<legend>' + ForumView.displayOptions + '</legend><form style="padding: 0px; margin: 0px">'
+			+ '<table style="width: 100%"><tr><td style="padding: 0px 5px; margin: 0px; '
+			+ 'width: 33%; text-align: right">' + ForumView.applyTo
+			+ '</td><td style="padding: 0px; margin: 0px; text-align:left">'
+			+' <input type="radio" name="f-apply-to" '
+			+ 'value="0" id="f-apply-to-this" onClick="pageContents.setOption(\'toAll\', 0)" />'
+			+ '<label for="f-apply-to-this">' + ForumView.thisForum
+			+ '</label> <input type="radio" name="f-apply-to" value="1" id="f-apply-to-all" '
+			+ 'onClick="pageContents.setOption(\'toAll\', 1)" />'
+			+ '<label for="f-apply-to-all">' + ForumView.allForums + '</label></td>'
+			+ '<td style="width: 33%; padding: 0px 5px; margin: 0px; text-align:right">'
+			+ '<a href="#" onClick="pageContents.optionsOk(); return false">' + ForumView.ok
+			+ '</a></td></tr><tr><td style="width: 33%; padding: 0px 5px; margin: 0px; text-align:right">'
+			+ ForumView.perPage + '</td><td style="padding: 0px; margin: 0px; text-align:left">'
+			+ '<select id="f-per-page" '
+			+ 'onChange="pageContents.setOption(\'pp\', options[selectedIndex].value)">';
+		for (var i = 10; i < 60; i += 10) {
+			str += '<option value="' + i + '">' + i + '</option>';
+		}
+		str += '</select></td><td style="width: 33%; padding: 0px 5px; margin: 0px; text-align:right">'
+			+ '<a href="#" onClick="pageContents.optionsCancel(); return false">' + ForumView.cancel
+			+ '</a></td></tr>';
+		if (isMod) {
+			str += '<tr><td style="padding: 0px 5px; margin: 0px; text-align: right"><label for="f-disp-del">'
+				+ ForumView.displayDeleted + '</label></td><td style="padding: 0px; margin: 0px; '
+				+ 'text-align:left" colspan="2"><input type="checkbox" name="f-disp-del"'
+				+ ' id="f-disp-del" value="1" onClick="pageContents.setOption(\'dd\', checked ? 1 : 0)" '
+				+ '/></td></tr>';
+		}
+		str += '</table></form></fieldset>';
+
+		// Moderation tools
+		if (isMod) {
+			str += '<fieldset id="f-mod-tools" style="margin: 0px; padding: 5px 0px; text-align: center'
+				+ (showModTools ? '; display: block' : '; display: none') + '">'
+				+ '<legend>' + ForumView.modTools + '</legend>'
+				+ '<form style="padding: 0px; margin: 0px" id="f-mod-tools-contents">'
+				+ '</form></fieldset>';
+		}
+		str += '</td></tr>';
+
+		// Page control
+		if (topics.length && nPages > 1) {
+			var st = 'border-width: 0px; margin: 0px; vertical-align: middle; ';
+			str += '<tr><td style="' + st + 'padding: 5px 0px 0px 0px; text-align:left; width: 33%">'
+				+ (cPage > 0 ? '<a href="#" onClick="pageContents.prevPage(); return false">' : '')
+				+ '&lt;-- ' + ForumView.previousPage + (cPage > 0 ? '</a>' : '')
+				+ '</td><td style="' + st + 'padding: 5px 0px 0px 0px; text-align:center">'
+				+ '<form style="' + st + 'padding: 0px; ">' + ForumView.pageSelHdr
+				+ '<select onChange="pageContents.jumpToPage(options[selectedIndex].value)">';
+			for (var i = 0; i < nPages; i ++) {
+				str += '<option ' + (i == cPage ? 'selected="selected" ' : '') + 'value="'
+					+ i + '">' + (i + 1) + '</option>';
+			}
+			str += '</select></form></td><td style="' + st
+				+ '; padding: 5px 0px 0px 0px; text-align: right; width: 33%">'
+				+ ((cPage<nPages-1) ? '<a href="#" onClick="pageContents.nextPage(); return false">' : '')
+				+ ForumView.nextPage + '--&gt;' + ((cPage<nPages-1) ? '</a>' : '') + '</td></tr>';
+		}
+
+		str += '</table>';
+		return str;
+	};
+
+	displayContents = function () {
+		// Draw the header
+		var str = '<div style="padding: 0px 0px 0px 5px">';
+		var hasDetails = (description != '' || admins.length || mods.length || users.length);
+		if (unread || hasDetails) {
+			// "Mark as read" link
+			str += '<div style="float: right; text-align: right">';
+			if (hasDetails) {
+				str += '<a href="#" onClick="pageContents.toggleDetails();'
+					+ 'return false" id="f-toggle-details">'
+					+ (showDetails ? ForumView.hideDetails : ForumView.showDetails)
+					+ '</a>' + (unread ? ' - ' : '');
+			}
+			if (unread) {
+				str += '<a href="#" onClick="pageContents.markRead(); return false">'
+					+ ForumView.markRead + '</a>';
+			}
+			str += '</div>';
+		}
+		// Title and parent categories
+		str += '<h1 style="padding: 0px">' + title + '</h1>';
+		for (var i in parents) {
+			str += (i > 0 ? ' &gt; ' : '') + '<a href="?cmd=V%23C%23' + parents[i].id + '">'
+				+ (parents[i].id == '/' ? ForumsLayout.menuTitle : parents[i].name)
+				+ '</a>';
+		}
+		str += '</div>';
+		// Details (description & ACL)
+		if (hasDetails) {
+			var needsBR = false;
+			str += '<fieldset id="f-details" style="margin: 20px 20px'
+				+ (showDetails ? '' : '; display: none')
+				+ '"><legend>' + ForumView.details + '</legend>';
+
+			if (description != '') {
+				str += description;
+				needsBR = true;
+			}
+
+			if (admins.length) {
+				str += (needsBR ? '<br/>' : '') + '<b>' + ForumView.adminsHdr + '</b>: '
+					+ admins.join(' - ');
+				needsBR = true;
+			}
+
+			if (mods.length) {
+				str += (needsBR ? '<br/>' : '') + '<b>' + ForumView.modsHdr + '</b>: '
+					+ mods.join(' - ');
+				needsBR = true;
+			}
+
+			if (users.length) {
+				str += (needsBR ? '<br/>' : '') + '<b>' + ForumView.usersHdr + '</b>: '
+					+ users.join(' - ');
+			}
+
+			str += '</fieldset>';
+		}
+		str += '<table style="width: 100%; margin: 20px 0px; padding: 0px; border-style:none">';
+
+		str += drawControls();
+		if (topics.length) {
+			// Topics list
+			str += '<tr><td style="padding: 0px; margin: 0px"><form style="padding:0px;margin:0px;'
+				+ 'width:100%"><table style="width: 100%; padding: 0px; margin: 0px; border-collapse: '
+				+ 'collapse"><tr><td style="width: 20px;text-align:center;vertical-align:middle">'
+				+ '<input type="checkbox" id="mt-sel-all" name="mt-sel-all" value="1" '
+				+ (modSelAll ? 'checked="checked" ' : '') + (showModTools ? '' : 'style="display: none"')
+				+ ' onClick="pageContents.toggleSelAll(); return true" />'
+				+ '</td><td style="width: 32px">&nbsp;</td>'
+				+ '<th style="text-align: left; padding: 0px 0px 0px 5px">' + ForumView.headers.topic
+				+ '</th><th style="width: 8%">' + ForumView.headers.replies
+				+ '</th><th style="width: 25%">' + ForumView.headers.fPost
+				+ '</th><th style="width: 25%">' + ForumView.headers.lPost + '</th></tr>';
+
+			for (var i = cPage * perPage; i < topics.length && i < (cPage + 1) * perPage; i ++) {
+				str += '\n<tr style="border-width: 1px 0px; border-style: solid; border-color: white"'
+					+ '><td style="width: 20px; text-align: center; padding: 0px; vertical-align:'
+					+ 'middle"><input type="checkbox" id="mt-sel-' + topics[i].id + '" value="1"'
+					+ (topics[i].selected ? ' checked="checked"' : '')
+					+ (showModTools ? '' : ' style="display: none"')
+					+ ' onClick="pageContents.toggleTopic(' + i + '); return true" />'
+					+ '</td><td style="width: 32px; text-align: center; padding: 0px">'
+					+ '<img src="' + staticurl + '/beta5/pics/topic_s' + topics[i].sticky
+					+ '_' + (topics[i].unread ? 'un' : '') + 'read.gif" style="border-width: 0px" />'
+					+ '</td><td style="padding: 0px 0px 0px 5px">'
+					+ ((topics[i].isLocked || topics[i].hasPoll) ? '<div style="float: right">' : '')
+					+ (topics[i].isLocked ? ('<img src="' + staticurl + '/beta5/pics/topic_locked.'
+						+ (useGIFs ? 'gif' : 'png')
+						+ '" style="border-width: 0px; float: left" />') : '')
+					+ (topics[i].hasPoll ? ('<img src="' + staticurl + '/beta5/pics/topic_poll.'
+						+ (useGIFs ? 'gif' : 'png') + '" style="border-width: 0px" />') : '')
+					+ ((topics[i].isLocked || topics[i].hasPoll) ? '</div>' : '') + '<div>'
+					+ (topics[i].isDeleted ? '' : ('<a href="?cmd=T%23' + topics[i].id + '">'))
+					+ topics[i].title + (topics[i].isDeleted ? '' : '</a>')
+					+ (topics[i].movedTo != '' ? ('<br/>&nbsp;-&gt;&nbsp;' + ForumView.movedTo
+						+ '<a href="?cmd=V%23F%23' + topics[i].movedTo + '">'
+						+ topics[i].mToForum + '</a>') : '')
+					+ '</div></td>';
+				if (topics[i].isDeleted) {
+					str += '<td colspan="3" style="text-align: center; vertical-align: middle">'
+						+ ForumView.deletedAt + formatDate(topics[i].deletedAt) + CategoryView.by
+						+ '<b>' + players[topics[i].deletedBy] + '</b></td>';
+				} else {
+					str += '<td style="text-align:center; vertical-align: middle">'
+						+ formatNumber(topics[i].nReplies) + '</td>'
+						+ '<td style="text-align:center;padding: 0px 2px;vertical-align:middle">'
+						+ formatDate(topics[i].fpTime) + CategoryView.by + '<b>'
+						+ players[topics[i].fpAuthor] + '</b></td>'
+						+ '<td style="text-align:center;padding: 0px 2px;vertical-align:middle">'
+						+ formatDate(topics[i].lcTime) + CategoryView.by + '<b>'
+						+ players[topics[i].lcAuthor] + '</b></td>';
+				}
+				str += '</tr>';
+			}
+
+			str += '</table>';
+		} else {
+			str += '<tr><td style="padding: 0px; height: 48px; border-style: solid; border-color: white; '
+				+ 'border-width: 1px 0px; text-align: center; vertical-align: middle">'
+				+ ForumView.empty + '</td></tr>';
+		}
+		str += '</table>';
+		
+		document.getElementById('f-page').innerHTML = str;
+		setFieldValues();
+	};
+
+	hideOptions = function () {
+		var e = document.getElementById('f-options');
+		if (!e) {
+			return;
+		}
+		showOptions = false;
+		e.style.display = 'none';
+		document.getElementById('f-std-ctrl').style.display = 'block';
+	};
+
+	drawModTools = function () {
+		var cnt = 0, sel = 0, dSel = 0, nSel = 0, hasLocked = false, hasUnlocked = false,
+			minStL = 11, maxStL = -1;
+		if (cPage != -1) {
+			for (var i = cPage * perPage; i < topics.length && i < (cPage + 1) * perPage; i ++) {
+				if (topics[i].movedTo != '') {
+					continue;
+				}
+				document.getElementById('mt-sel-' + topics[i].id).checked = topics[i].selected;
+				if (topics[i].selected) {
+					if (topics[i].isDeleted) {
+						dSel ++;
+					} else {
+						nSel ++;
+						hasLocked = hasLocked || topics[i].isLocked;
+						hasUnlocked = hasUnlocked || ! topics[i].isLocked;
+					}
+					minStL = (minStL > topics[i].sticky) ? topics[i].sticky : minStL;
+					maxStL = (maxStL < topics[i].sticky) ? topics[i].sticky : maxStL;
+					sel ++;
+				}
+				cnt ++;
+			}
+			document.getElementById('mt-sel-all').checked = modSelAll = (sel == cnt);
+		}
+
+		var str = '<a href="#" onClick="pageContents.hideModTools(); return false">' + ForumView.hideModTools
+			+ '</a><br/>';
+		if (!(dSel || nSel)) {
+			str += '<br/>' + ForumView.pleaseSelect;
+		} else {
+			if (nSel) {
+				str += '<br/>' + ForumView.selectedTopics
+					+ '<a href="#" onClick="pageContents.deleteSelected(); return false">'
+					+ ForumView.deleteTopics + '</a> - ' + ForumView.stickyLevel;
+
+				// Sticky level management
+				if (maxStL < 10) {
+					str += '<a href="#" onClick="pageContents.increaseSticky(); return false">'
+					+ ForumView.increaseStickyLevel + '</a> / ';
+				}
+				if (minStL > 0) {
+					str += '<a href="#" onClick="pageContents.decreaseSticky(); return false">'
+					+ ForumView.decreaseStickyLevel + '</a> / ';
+				}
+
+				str += ForumView.setTo + '<select id="mt-set-level" onChange="pageContents.setSticky('
+					+ 'options[selectedIndex].value)"><option value="">-------</option>'
+
+				var stText = [ForumView.normalPost, 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII',
+					'IX', 'X'];
+				for (var i = 0; i < 11; i ++) {
+					str += '<option value="' + i + '">' + stText[i] + '</option>';
+				}
+				str += '</select>';
+
+				// Lock / Unlock
+				if (hasLocked) {
+					str += ' - <a href="#" onClick="pageContents.unlockTopics(); return false">'
+						+ ForumView.unlock + '</a>';
+				}
+				if (hasUnlocked) {
+					str += ' - <a href="#" onClick="pageContents.lockTopics(); return false">'
+						+ ForumView.lock + '</a>';
+				}
+
+				// Move to
+				if (hasMoveTargets) {
+					str += '<br/>' + ForumView.moveTo + '<select id="mt-move-to" onChange="'
+						+ 'pageContents.moveTo(options[selectedIndex].value)">'
+						+ '<option value="">-------</option>';
+					for (var i in moveTargets) {
+						str += '<option value="' + i + '">' + moveTargets[i] + '</option>';
+					}
+					str += '</select>';
+				}
+			}
+			if (dSel) {
+				str += '<br/>' + ForumView.deletedTopics
+					+ '<a href="#" onClick="pageContents.restoreSelected(); return false">'
+					+ ForumView.restoreTopics + '</a>';
+			}
+		}
+
+		document.getElementById('f-mod-tools-contents').innerHTML = str;
+	};
+
+	setFieldValues = function () {
+		var e = document.getElementById('f-options');
+		if (!e) {
+			return;
+		}
+
+		// Options block
+		document.getElementById('f-apply-to-this').checked = (options.toAll == 0);
+		document.getElementById('f-apply-to-all').checked = (options.toAll == 1);
+		document.getElementById('f-per-page').selectedIndex = (options.pp / 10) - 1;
+
+		// Moderation tools
+		if (! isMod) {
+			return;
+		}
+		document.getElementById('f-disp-del').checked = (options.dd == 1);
+
+		if (cPage != -1) {
+			for (var i = cPage * perPage; i < topics.length && i < (cPage + 1) * perPage; i ++) {
+				document.getElementById('mt-sel-' + topics[i].id).style.display =
+					((showModTools && topics[i].movedTo == '') ? 'block' : 'none');
+			}
+			document.getElementById('mt-sel-all').style.display = (showModTools ? 'block' : 'none');
+		}
+
+		drawModTools();
+	};
+
+	// Page update: parser & updater
+	this.parse = function (data) {
+		if (data != '-') {
+			if (data.indexOf('\n') == -1) {
+				// IE, first update
+				viewId = data;
+				this.update();
+				return;
+			}
+
+			var lines = data.split('\n');
+			pageMD5 = lines.shift();
+			viewId = lines.shift();
+
+			mainParser(lines);
+			players = parseNames(lines);
+
+			displayContents();
+		}
+
+		pageUpdater = window.setTimeout("pageContents.update()", 10000);
+		locked = false;
+	};
+	this.update = function () {
+		if (locked) {
+			return;
+		}
+		pageUpdater = null;
+		locked = true;
+		x_getView(viewId, pageMD5, function (data) { pageContents.parse(data); });
+	};
+	this.setFieldValues = function () { setFieldValues(); };
+
+	// Displaying details
+	this.toggleDetails = function () {
+		var e = document.getElementById('f-details');
+		if (!e) {
+			return;
+		}
+		showDetails = ! showDetails;
+		document.getElementById('f-toggle-details').innerHTML
+			= (showDetails ? ForumView.hideDetails : ForumView.showDetails)
+		e.style.display = showDetails ? 'block' : 'none';
+	};
+
+	// "Display options" box management
+	this.displayOptions = function () {
+		var e = document.getElementById('f-options');
+		if (!e) {
+			return;
+		}
+		options = {
+			toAll: 0,
+			pp: perPage,
+			dd: (isMod && vDeleted) ? 1 : 0
+		};
+		setFieldValues();
+		showOptions = true;
+		e.style.display = 'block';
+		document.getElementById('f-std-ctrl').style.display = 'none';
+	};
+	this.optionsOk = function () {
+		hideOptions();
+		if (locked) {
+			window.setTimeout("pageContents.optionsOk()", 250);
+			return;
+		}
+		locked = true;
+		if (pageUpdater) {
+			window.clearTimeout(pageUpdater);
+			pageUpdater = null;
+		}
+		x_forumOptions(forumId, options.toAll, options.pp, options.dd, pageMD5, function (data) {
+			pageContents.parse(data); });
+	};
+	this.optionsCancel = function () { hideOptions(); };
+	this.setOption = function (name, value) { options[name] = value; };
+
+	// Moderation tools
+	this.displayModTools = function () {
+		var e = document.getElementById('f-mod-tools');
+		if (!e) {
+			return;
+		}
+		showModTools = true;
+		setFieldValues();
+		e.style.display = 'block';
+		document.getElementById('f-std-ctrl').style.display = 'none';
+	};
+	this.hideModTools = function () {
+		var e = document.getElementById('f-mod-tools');
+		if (!e) {
+			return;
+		}
+		showModTools = false;
+		setFieldValues();
+		e.style.display = 'none';
+		document.getElementById('f-std-ctrl').style.display = 'block';
+	};
+	this.toggleTopic = function (index) {
+		topics[index].selected = !topics[index].selected;
+		drawModTools();
+	};
+	this.toggleSelAll = function () {
+		modSelAll = !modSelAll;
+		for (var i = cPage * perPage; i < topics.length && i < (cPage + 1) * perPage; i ++) {
+			if (topics[i].movedTo != '') {
+				continue;
+			}
+			topics[i].selected = modSelAll;
+		}
+		drawModTools();
+	};
+	this.restoreSelected = function () {
+		if (locked) {
+			return;
+		}
+		locked = true;
+		if (pageUpdater) {
+			window.clearTimeout(pageUpdater);
+			pageUpdater = null;
+		}
+
+		var tlist = getSelectedTopics(true);
+		x_restoreTopics(forumId, tlist.join('#'), function (data) { pageContents.parse(data); });
+	};
+	this.deleteSelected = function () {
+		if (locked) {
+			return;
+		}
+		locked = true;
+		if (pageUpdater) {
+			window.clearTimeout(pageUpdater);
+			pageUpdater = null;
+		}
+
+		var tlist = getSelectedTopics(false);
+		x_deleteTopics(forumId, tlist.join('#'), function (data) { pageContents.parse(data); });
+	};
+	this.increaseSticky = function () {
+		if (locked) {
+			return;
+		}
+		locked = true;
+		if (pageUpdater) {
+			window.clearTimeout(pageUpdater);
+			pageUpdater = null;
+		}
+
+		var tlist = getSelectedTopics(false);
+		x_changeTopicsLevel(forumId, tlist.join('#'), 1, function (data) { pageContents.parse(data); });
+	};
+	this.decreaseSticky = function () {
+		if (locked) {
+			return;
+		}
+		locked = true;
+		if (pageUpdater) {
+			window.clearTimeout(pageUpdater);
+			pageUpdater = null;
+		}
+
+		var tlist = getSelectedTopics(false);
+		x_changeTopicsLevel(forumId, tlist.join('#'), -1, function (data) { pageContents.parse(data); });
+	};
+	this.setSticky = function (value) {
+		var e = document.getElementById('mt-set-level');
+		if (e) { e.disabled = true; }
+		e = document.getElementById('mt-set-level'); if (e) { e.disabled = true; }
+		e = document.getElementById('mt-move-to'); if (e) { e.disabled = true; }
+
+		if (locked) {
+			window.setTimeout("pageContents.setSticky(" + value + ")", 250);
+			return;
+		}
+		locked = true;
+		if (pageUpdater) {
+			window.clearTimeout(pageUpdater);
+			pageUpdater = null;
+		}
+
+		var tlist = getSelectedTopics(false);
+		x_setTopicsLevel(forumId, tlist.join('#'), value, function (data) { pageContents.parse(data); });
+	};
+	this.lockTopics = function () {
+		if (locked) {
+			return;
+		}
+		locked = true;
+		if (pageUpdater) {
+			window.clearTimeout(pageUpdater);
+			pageUpdater = null;
+		}
+
+		var tlist = getSelectedTopics(false);
+		x_setTopicsLock(forumId, tlist.join('#'), 1, function (data) { pageContents.parse(data); });
+	};
+	this.unlockTopics = function () {
+		if (locked) {
+			return;
+		}
+		locked = true;
+		if (pageUpdater) {
+			window.clearTimeout(pageUpdater);
+			pageUpdater = null;
+		}
+
+		var tlist = getSelectedTopics(false);
+		x_setTopicsLock(forumId, tlist.join('#'), 0, function (data) { pageContents.parse(data); });
+	};
+	this.moveTo = function (dest) {
+		e = document.getElementById('mt-set-level'); if (e) { e.disabled = true; }
+		e = document.getElementById('mt-move-to'); if (e) { e.disabled = true; }
+
+		if (locked) {
+			window.setTimeout("pageContents.moveTo(" + dest + ")", 250);
+			return;
+		}
+		locked = true;
+		if (pageUpdater) {
+			window.clearTimeout(pageUpdater);
+			pageUpdater = null;
+		}
+
+		var tlist = getSelectedTopics(false);
+		x_moveTopics(forumId, tlist.join('#'), dest, function (data) { pageContents.parse(data); });
+	};
+
+	// Page management
+	this.nextPage = function () { this.jumpToPage(cPage + 1); };
+	this.prevPage = function () { this.jumpToPage(cPage - 1); };
+	this.jumpToPage = function (value) {
+		cPage = parseInt(value, 10);
+		displayContents();
+	};
+
+	// Mark topics as read
+	this.markRead = function () {
+		if (locked) {
+			window.setTimeout("pageContents.markRead()", 250);
+			return;
+		}
+		locked = true;
+		if (pageUpdater) {
+			window.clearTimeout(pageUpdater);
+			pageUpdater = null;
+		}
+		x_forumRead(forumId, function (data) { pageContents.parse(data); });
+	};
+};
+
+
+CategoryView = function () {
+
+	var me = this;
+	var tst = ' style="width: 100%; margin: 0px; padding: 0px; border-width: 0px"';
+	var cst = ' style="margin: 0px; padding: 0px; border-width: 0px; vertical-align: middle"';
+
+	var pageUpdater = null;
+	var pageMD5 = null;
+	var viewId = null;
+	var contents = null;
+	var locked = false;
+	var players = [];
+
+	var parseContents, parseForum, displayContents, displayForums;
+
+	parseContents = function (lines) {
+		var line = lines.shift().split('#');
+		var obj = {};
+		var cc, dc;
+
+		obj.id = line.shift();
+		obj.isCategory = (line.shift() == 'F');
+		obj.hasUnread = (line.shift() == '1');
+		cc = parseInt(line.shift(), 10);
+		dc = parseInt(line.shift(), 10);
+
+		if (obj.isCategory) {
+			obj.typeName = lines.shift();
+		}
+		obj.title = lines.shift();
+
+		var da = new Array();
+		for (var i = 0; i < dc; i ++) {
+			da.push(lines.shift());
+		}
+		obj.description = da.join('<br/>');
+
+		if (obj.id == '/') {
+			obj.title = CategoryView.ovTitle;
+			obj.description = CategoryView.ovDescription;
+		}
+
+		obj.contents = new Array();
+		for (var i = 0; i < cc; i ++) {
+			obj.contents.push( obj.isCategory ? parseForum(lines) : parseContents(lines) );
+		}
+
+		return obj;
+	};
+	parseForum = function (lines) {
+		var line = lines.shift().split('#');
+		var obj = { };
+		var dc;
+
+		obj.id = line.shift();
+		dc = parseInt(line.shift(), 10);
+		obj.isDeleted = (line.shift() == '1');
+		obj.deletedAt = line.shift();
+		obj.deletedBy = line.shift();
+		obj.topics = line.shift();
+		obj.posts = line.shift();
+		obj.isUnread = (line.shift() == '1');
+
+		if (parseInt(obj.posts, 10) > 0) {
+			line = lines.shift().split('#');
+			obj.lastAuthor = line.shift();
+			obj.lastTimestamp = line.shift();
+		}
+
+		obj.title = lines.shift();
+		var desc = new Array();
+		for (var i = 0; i < dc; i ++) {
+			desc.push(lines.shift());
+		}
+		obj.description = desc.join('<br/>');
+
+		return obj;
+	};
+	displayContents = function (obj, depth) {
+		var d = (typeof depth == 'undefined') ? 0 : depth;
+		var mw = d * 8;
+		var htag = 'h' + (d + 1);
+
+		var str = '<table style="width: 100%; margin: 5px 0px 2px 0px; padding: 0px; border-width: 0px"><tr>';
+		if (d > 0) {
+			str += '<td style="width: ' + mw + 'px; margin: 0px; padding: 0px; border-width: 0px;">'
+				+ '&nbsp;</td>';
+		}
+
+		var rText = '';
+		if (obj.isCategory) {
+			rText = '<div style="float: right; text-align: right">' + obj.typeName;
+			if (obj.hasUnread) {
+				rText += '<br/><a href="#" onClick="pageContents.markRead(\''
+					+ obj.id + '\'); return false">' + CategoryView.markRead + '</a>';
+			}
+			rText += '</div>';
+		}
+
+		str += '<td' + cst + '>' + rText + '<div><' + htag + '>' + obj.title + '</' + htag + '>'
+			+ (obj.description != '' ? ('<p>' + obj.description + '</p>') : '')
+			+ '</div></td></tr></table>';
+
+		if (! obj.isCategory) {
+			for (var i in obj.contents) {
+				str += displayContents(obj.contents[i], d + 1);
+			}
+		} else {
+			str += displayForums(obj);
+		}
+
+		return str;
+	};
+	displayForums = function (obj) {
+		if (obj.contents.length == 0) {
+			return '<p>' + CategoryView.empty + '</p>';
+		}
+
+		var str = '<table style="border-collapse: collapse; width: 100%; margin: 0px 0px 20px 0px; padding: 0px; '
+			+ 'border-style: none; border-color: white; border-width: 0px"><tr>'
+			+ '<td style="width: 32px">&nbsp;</td><th style="text-align: left">'
+			+ CategoryView.headers.name + '</th><th style="width: 10%">' + CategoryView.headers.topics
+			+ '</th><th style="width: 10%">' + CategoryView.headers.posts
+			+ '</th><th style="width: 25%">' + CategoryView.headers.lastPost
+			+ '</th></tr>';
+
+		for (var i in obj.contents) {
+			var forum = obj.contents[i];
+			str += '<tr><td style="height: 32px"><img src="' + staticurl
+				+ '/beta5/pics/forum_' + (forum.isUnread ? 'un' : '') + 'read.'
+				+ (useGIFs ? 'gif' : 'png') + '" alt="'
+				+ CategoryView.forumIcon[forum.isUnread ? 'unread' : 'read']
+				+ '" style="border-style:none;border-width: 0px" /></td>'
+				+ '<td style="vertical-align: middle; padding: 1px 0px 2px 0px"><a href="?cmd=V%23F%23'
+				+ forum.id + '">' + forum.title + '</a>'
+				+ (forum.description != '' ? ('<br/>' + forum.description) : '') + '</td>';
+			if (forum.isDeleted) {
+				str += '<td style="vertical-align: middle; text-align: center" colspan="3">'
+					+ CategoryView.deletedAt + '<b>' + formatDate(forum.deletedAt) + '</b>'
+					+ CategoryView.by + '<b>' + players[forum.deletedBy] + '</b></td>';
+			} else {
+				str += '<td style="vertical-align: middle; text-align: center">'
+					+ formatNumber(forum.topics)
+					+ '</td><td style="vertical-align: middle; text-align: center">'
+					+ formatNumber(forum.posts)
+					+ '</td><td style="vertical-align: middle; text-align: center">';
+				if (forum.posts != 0) {
+					str += formatDate(forum.lastTimestamp) + '<br/>' + CategoryView.by
+						+ '<b>' + players[forum.lastAuthor] + '</b>';
+				} else {
+					str += CategoryView.noPosts;
+				}
+				str += '</td>';
+			}
+			str += '</tr>';
+		}
+
+		str += '</table>';
+		return str;
+	};
+
+	this.parse = function (data) {
+		if (data != '-') {
+			if (data.indexOf('\n') == -1) {
+				// IE, first update
+				viewId = data;
+				this.update();
+				return;
+			}
+
+			var lines = data.split('\n');
+			pageMD5 = lines.shift();
+			viewId = lines.shift();
+
+			contents = parseContents(lines);
+			players = parseNames(lines);
+
+			document.getElementById('f-page').innerHTML = displayContents(contents);
+		}
+
+		pageUpdater = window.setTimeout("pageContents.update()", 10000);
+		locked = false;
+	};
+
+	this.update = function () {
+		if (locked) {
+			return;
+		}
+		pageUpdater = null;
+		locked = true;
+		x_getView(viewId, pageMD5, function (data) { pageContents.parse(data); });
+	};
+
+	this.markRead = function (id) {
+		if (locked) {
+			window.setTimeout("pageContents.markRead('" + id + "')", 250);
+			return;
+		}
+		locked = true;
+		if (pageUpdater) {
+			window.clearTimeout(pageUpdater);
+			pageUpdater = null;
+		}
+		x_categoryRead(id, viewId, function (data) { pageContents.parse(data); });
+	};
+
+	this.setFieldValues = function () { };
+};
+
+
+
+ForumsLayout = function () {
+
+	var init = document.getElementById('f-params').innerHTML.split('#');
+	var me = this;
+
+	// Read the page's parameters
+	var menuVisible	= (init.shift() == '1');
+	var needData	= (init.shift() == '1');
+	var pageType	= init.shift();
+
+	// Read the current menu entry
+	MenuItem.current = document.getElementById('f-menu-current').innerHTML;
+
+	// Initialize other private variables here
+	var menuMD5		= '';
+	var menuTree		= null;
+	var menuUpdater		= null;
+	var menuFDisabled	= false;
+	var page		= null;
+
+	// Initialize private methods here
+	var parseMenuData, drawLayout, drawMenu;
+	parseMenuData = function(data) {
+		menuUpdater = null;
+		if (! menuVisible) {
+			menuFDisabled = false;
+			return;
+		}
+
+		var lines = data.split('\n');
+		var line = lines.shift();
+
+		if (line != '-') {
+			menuMD5 = line;
+			menuTree = new MenuItem(lines);
+			drawMenu();
+		}
+		menuFDisabled = false;
+		menuUpdater = window.setTimeout("main.updateMenu()", 5000);
+	};
+	drawMenu = function () {
+		if (! menuTree) {
+			menuVisible = false;
+			drawLayout();
+			return;
+		}
+
+		var tst = ' style="width: 100%; margin: 0px; padding: 0px; border-width: 0px"';
+		var cst = ' style="margin: 0px; padding: 0px; border-width: 0px; vertical-align: middle"';
+		var str = '<table' + tst + '><tr><td' + tst + '><table' + tst + '><tr><td ' + cst
+			+ '<h1>' + ForumsLayout.menuTitle + '</h1></td><td' + cst + '><a href="#" onClick="'
+			+ 'main.hideMenu();return false">' + ForumsLayout.hideMenu
+			+ '</a></td></tr></table></td></tr>' + '<tr><td' + tst + '>&nbsp;</td></tr>';
+
+		var x = new Array();
+		menuTree.draw(x);
+
+		str += x.join('') + '</table>';
+		document.getElementById('f-menu').innerHTML = str;
+	};
+	drawLayout = function () {
+		var mContents;
+		if (document.getElementById('f-page')) {
+			mContents = document.getElementById('f-page').innerHTML;
+		} else {
+			mContents = '&nbsp;';
+		}
+
+		var mainMargin;
+		var str = '<div id="f-menu" style="float: left; margin: 0px 5px; padding: 1px 10px 20px 5px; '
+			+ 'border-color: white; border-style: solid; border-width: 0px 1px 0px 0px';
+		if (menuVisible) {
+			str += '; width: 300px"><i>(loading menu)</i>';
+			mainMargin = 326;
+		} else {
+			str += ';text-align:center; width: 16px"><a href="#" onClick="main.showMenu();return false">'
+				+ ForumsLayout.menuText.join('<br/>') + '</a>';
+			mainMargin = 42;
+		}
+		str += '</div><div id="f-page" style="margin: 1px 5px 10px ' + mainMargin + 'px">&nbsp;</div>';
+		document.getElementById('f-contents').innerHTML = str;
+		document.getElementById('f-page').innerHTML = mContents;
+		if (mContents != '&nbsp;') {
+			pageContents.setFieldValues();
+		}
+	};
+
+	// Public methods
+	this.updateMenu = function() {
+		menuFDisabled = true;
+		x_getMenu(menuMD5, parseMenuData);
+	};
+	this.hideMenu = function() {
+		if (menuFDisabled || !menuVisible) {
+			if (menuFDisabled) {
+				window.setTimeout("main.hideMenu()", 250);
+			}
+			return;
+		}
+		if (menuUpdater) {
+			window.clearTimeout(menuUpdater);
+		}
+		menuUpdater = menuMD5 = menuTree = null;
+		menuVisible = false;
+		x_hideMenu(function () {});
+		drawLayout();
+	};
+	this.showMenu = function() {
+		if (menuFDisabled || menuVisible) {
+			if (menuFDisabled) {
+				window.setTimeout("main.showMenu()", 250);
+			}
+			return;
+		}
+		menuFDisabled = true;
+		menuVisible = true;
+		drawLayout();
+		x_showMenu(parseMenuData);
+	};
+	this.menuOpen = function(id) {
+		if (menuFDisabled || !menuVisible) {
+			if (menuFDisabled) {
+				window.setTimeout("main.menuOpen('" + id + "')", 250);
+			}
+			return;
+		}
+		menuFDisabled = true;
+		if (menuUpdater) {
+			window.clearTimeout(menuUpdater);
+			menuUpdater = null;
+		}
+		x_menuOpen(id, parseMenuData);
+	};
+	this.menuClose = function(id) {
+		if (menuFDisabled || !menuVisible) {
+			if (menuFDisabled) {
+				window.setTimeout("main.menuClose('" + id + "')", 250);
+			}
+			return;
+		}
+		menuFDisabled = true;
+		if (menuUpdater) {
+			window.clearTimeout(menuUpdater);
+			menuUpdater = null;
+		}
+		x_menuClose(id, parseMenuData);
+	};
+
+	useGIFs = needData;
+	pageContents = new ForumsLayout.pages[pageType];
+	drawLayout();
+	if (needData) {
+		if (menuVisible) {
+			x_getMenu(parseMenuData);
+		}
+	} else {
+		if (menuVisible) {
+			parseMenuData(document.getElementById('f-menu-init').innerHTML);
+		}
+	}
+	pageContents.parse(document.getElementById('f-page-init').innerHTML);
+};
+
+ForumsLayout.pages = {
+	vCat: CategoryView,
+	vForum: ForumView,
+	vTopic: TopicView
+};
diff -Naur beta5//site/static/beta5/js/pg_overview-en.js forums//site/static/beta5/js/pg_overview-en.js
--- beta5//site/static/beta5/js/pg_overview-en.js	2011-02-05 10:09:56.434335002 +0100
+++ forums//site/static/beta5/js/pg_overview-en.js	2011-03-12 15:10:45.561300049 +0100
@@ -84,26 +84,7 @@
 	str += '<h2>Fleets</h2><p>Total fleet power: <b>'+formatNumber(flOverview[0])+'</b></p>';
 	str += '<h2>Money</h2><p>Daily Profit: <b>&euro;'+formatNumber(moOverview[2])+'</b></p></td>';
 
-	str += '<td colspan="2"><h2>Forums</h2><p>';
-	var	i,j,k,a=new Array();
-	for	(i=0;i<genForums.length;i++)
-	{with(genForums[i]){
-		k = 0;
-		for	(j=0;j<forums.length;j++)
-			k += parseInt(forums[j].nUnread,10);
-		j = (k==0? "no unread topics" : (k + ' unread topic' + (k>1 ? 's' : '')));
-		a.push('<a href="forums?cmd=C%23G%23'+id+'" ' + ovett[4] + ' >'+name+'</a>: ' + j);
-	}}
-
-	if	(aForums.length)
-	{
-		k = 0;
-		for	(j=0;j<aForums.length;j++)
-			k += parseInt(aForums[j].nUnread,10);
-		j = (k==0? "no unread topics" : (k + ' unread topic' + (k>1 ? 's' : '')));
-		a.push('<a href="forums?cmd=C%23A%23'+allianceId+'" ' + ovett[5] + ' >Alliance forums</a>: ' + j);
-	}
-	str += a.join('<br/>') + '</p>';
+	str += '<td colspan="2"><h2>Forums</h2><p>' + forums.output(-1, false) + '</p>';
 
 	str += '<h2>Planets</h2><p><b>'+formatNumber(unOverview[0])+'</b> planets</p>';
 	str += '<h2>Next ticks</h2><p id="ticks"> </p>';
@@ -188,30 +169,7 @@
 	str += 'Daily Profit: <b>&euro;'+formatNumber(moOverview[2])+'</b><br/>';
 	str += '<a href="money" ' + ovett[15] + ' >More details...</a></p></td>';
 
-	str += '<td colspan="2"><h2>Forums</h2><p>';
-	var	j,a=new Array(),s;
-	for	(i=0;i<genForums.length;i++)
-	{with(genForums[i]){
-		s = '<b>' + name + '</b> (<a href="forums?cmd=C%23G%23'+id+'" ' + ovett[16] + '>view</a>)';
-		for	(j=0;j<forums.length;j++)
-		{
-			s += '<br/>&nbsp;&nbsp;-&nbsp;<a href="forums?cmd=F%23' + type + '%23' + forums[j].id + '" ' + ovett[17] + '>' + forums[j].name + '</a>: ';
-			s += makeTopicsText(forums[j].nTopics, forums[j].nUnread);
-		}
-		a.push(s);
-	}}
-
-	if	(aForums.length)
-	{
-		s = '<b>Alliance Forums</b> (<a href="forums?cmd=C%23A%23'+allianceId+'" ' + ovett[18] + ' >view</a>)';
-		for	(j=0;j<aForums.length;j++)
-		{
-			s += '<br/>&nbsp;&nbsp;-&nbsp;<a href="forums?cmd=F%23A%23' + aForums[j].id + '" ' + ovett[17] + ' >' + aForums[j].name + '</a>: ';
-			s += makeTopicsText(aForums[j].nTopics, aForums[j].nUnread);
-		}
-		a.push(s);
-	}
-	str += a.join('<br/><br/>') + '</p>';
+	str += '<td colspan="2"><h2>Forums</h2><p>' + forums.output(-1, true) + '</p>';
 
 	str += '<h2>Universe</h2><p><b>'+formatNumber(unOverview[0])+'</b> planets';// (<b>';
 	str += /*formatNumber(unOverview[2]) + '</b> at the same prot. level)*/'<br/><b>';
@@ -240,6 +198,40 @@
 }
 
 
+function __forumsOutput(depth, complete) {
+	var str = '';
+
+	if (this.id != '/') {
+		for (var i = 0; i < depth; i++) {
+			str += '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
+		}
+		str += '<a href="forums?cmd=V%23' + this.type + '%23' + this.id + '">' + this.name + '</a>: ';
+		if (complete) {
+			str += '<b>' + this.topics + '</b> topic' + (this.unread > 1 ? 's' : '');
+			if (this.unread > 0) {
+				str += ' (<b>' + this.unread + '</b> unread)';
+			}
+		} else {
+			if (this.unread == 0) {
+				str += 'no unread topics';
+			} else {
+				str += '<b>' + this.unread + '</b> unread topic' + (this.unread > 1 ? 's' : '');
+			}
+		}
+	}
+
+	for (var i = 0; i < this.contents.length; i ++) {
+		if (this.contents[i].type == 'F' && ! complete) {
+			continue;
+		}
+
+		str += '<br/>' + this.contents[i].output(depth + 1, complete);
+	}
+
+	return str;
+}
+
+
 function confirmBreakProtection() {
 	return confirm('You are about to break away from Peacekeeper protection.\n'
 		+ 'Anyone will be able to attack your planets afterwards.\n'
diff -Naur beta5//site/static/beta5/js/pg_overview.js forums//site/static/beta5/js/pg_overview.js
--- beta5//site/static/beta5/js/pg_overview.js	2011-02-05 10:09:56.434335002 +0100
+++ forums//site/static/beta5/js/pg_overview.js	2011-03-12 15:08:00.801300049 +0100
@@ -1,5 +1,5 @@
 var	dFolders, cFolders;
-var	genForums, aForums, allianceId;
+var	forums;
 var	plOverview, flOverview, moOverview, nResearch;
 var	unOverview,stDiff,ticks,tUpdate,rankings;
 var	complete, protection, updateTimer;
@@ -110,24 +110,25 @@
 	this.name	= name;
 }
 
-function	Category(id, type, name)
-{
-	this.id		= id;
-	this.type	= type;
-	this.name	= name;
-	this.forums	= new Array();
-}
+function ForumsEntity(inputData) {
+	var iLine = inputData.shift().split('#');
+	var nElements;
+
+	this.type	= iLine.shift();
+	this.id		= iLine.shift();
+	nElements	= parseInt(iLine.shift(), 10);
+	this.topics	= parseInt(iLine.shift(), 10);
+	this.unread	= parseInt(iLine.shift(), 10);
+	this.name	= inputData.shift();
+	this.contents	= new Array();
+	this.output	= __forumsOutput;
 
-function	Forum(id, nTopics, nUnread, name)
-{
-	this.id		= id;
-	this.nTopics	= nTopics;
-	this.nUnread	= nUnread;
-	this.name	= name;
+	for (var i = 0; i < nElements; i ++) {
+		this.contents.push( new ForumsEntity(inputData) );
+	}
 }
 
 
-
 function initPage() {
 	overviewReceived(document.getElementById('init-data').value);
 }
@@ -140,55 +141,26 @@
 }
 
 
-function	parseComms(l)
-{
-	var	i, a = l.shift().split('#');
-	var	nCustom = parseInt(a[0],10), nGenCats = parseInt(a[1],10), nAForums = parseInt(a[2],10);
-	allianceId = a[3];
+function parseComms(l) {
+	var i, a, nCustom;
 
 	// Default folders
 	dFolders = new Array();
-	for	(i=0;i<3;i++)
-	{
+	for (i=0;i<3;i++) {
 		a = l.shift().split('#');
 		dFolders.push(new Folder('', a[0], a[1], ''));
 	}
 
 	// Custom folders
+	nCustom = parseInt(l.shift(), 10);
 	cFolders = new Array();
-	for	(i=0;i<nCustom;i++)
-	{
+	for (i=0;i<nCustom;i++) {
 		a = l.shift().split('#');
 		cFolders.push(new Folder(a.shift(), a.shift(), a.shift(), a.join('#')));
 	}
 
 	// General categories & forums
-	genForums = new Array();
-	for	(i=0;i<nGenCats;i++)
-	{
-		a = l.shift().split('#');
-
-		var	j,c,id,tp,nForums;
-		id = a.shift(); tp = a.shift();
-		nForums = parseInt(a.shift(), 10);
-		c = new Category(id, tp, a.join('#'));
-
-		for	(j=0;j<nForums;j++)
-		{
-			a = l.shift().split('#');
-			c.forums.push(new Forum(a.shift(),a.shift(),a.shift(),a.join('#')));
-		}
-
-		genForums.push(c);
-	}
-
-	// Alliance forums
-	aForums = new Array();
-	for	(i=0;i<nAForums;i++)
-	{
-		a = l.shift().split('#');
-		aForums.push(new Forum(a.shift(),a.shift(),a.shift(),a.join('#')));
-	}
+	forums = new ForumsEntity(l);
 }
 
 
diff -Naur beta5//site/static/beta5/pics/forum_read.gif forums//site/static/beta5/pics/forum_read.gif
--- beta5//site/static/beta5/pics/forum_read.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/forum_read.gif	2011-02-05 10:10:02.694335002 +0100
@@ -0,0 +1,5 @@
+GIF89a � ��I�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+��,���� � ������������������������&9"-�3>�>=�<:8867�3=$�>?=;9741*'# 9�&�0�
+6�!$'3,�81223��3458@������,>#0�9FEEDCCBA@���	19J��%���t�R�B(����B�	(H��@�S�X���
.^�����
9��I]�!9���
<�a�5Ej�0B�$H��'H�d"R�F�tԘ�Ŋ!8d�Hd��/.�#�
+�0X�сF$X�E�!Ĉ&N�@�"�
+MkJ�L��eD���;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/forum_read.png forums//site/static/beta5/pics/forum_read.png
--- beta5//site/static/beta5/pics/forum_read.png	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/forum_read.png	2011-02-05 10:10:02.694335002 +0100
@@ -0,0 +1,3 @@
+�PNG
+
+���
IHDR��� ��� ���szz����bKGD�������������	pHYs����������tIME�		0)No޸���IDATX���k$E��U�]�����fr]�����"ě��G,(^��]�y��%�E<�E<+� ����$fe��d�����!�Nd���KW��T=���<U�q�rXӊ�n�nY����h4�0�o�ۿw�]��~MJ�X��?�m)%J)�RH)q��!ĽV��ݴ�䴢��Z���$����G�[RʗVWW?����Ƙs�,)���,�,�!�����x��DQ����(�5��}�
��˲&�TYEQ��)EQ��9EQ`���lz@
�� �cz�^��z�.�U��8::boo�<�Ͻm�e���J)��7s�0|,�@A��iz�oZ����bP�߲�s-8[�,!��Z�.#�T�A��������Bw���e���x�?᳙U0�y���<��N��N����666��Z�E�U��s���^ZZ�iccß��{�q��b�+�(�.^�b��j}~�Y^^��1���x��1p[)��������܄xƘ'ƕ�1�yNY���@>���m��DZ��`~�B)����T��4M)��(�`&�I������)%6 �$1�������>P��ͫ����7o�3�����8FJ�ֺ|��џc��ٶ�_nܸٶ=��e	 �0�ΕH��t�7��,�����ӓ��{�1�0�ի���i-��IB@��#��ܧ�0�F�K'���RJ���ֺ\__�$���β,�Ak��8��Zc�����3s`ss��(�nGQD�$��y��_v��f����u]��¶mc�6B�s��^J�
�;3���^]YYy=�cvvvH�4�r]��n���}�	�W�j[>�J�����Y���`@�e����0$�2�8�y�h4b4M���֭[�xo9��<`vww1Ơ�&�"�j�Z6��ߟh�bV_�J�qB����|������� ���6�R��p���q��~0�<`۶����^�����[���<�������?j�g}Ͷ����IEND�B`�
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/forum_unread.gif forums//site/static/beta5/pics/forum_unread.gif
--- beta5//site/static/beta5/pics/forum_unread.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/forum_unread.gif	2011-02-05 10:10:02.694335002 +0100
@@ -0,0 +1,3 @@
+GIF89a � ������������������������ ��"��"��#��'��*��2��*��+��3��+��5��6��/��8��0��4��:��4��5��>��7��?��=��8��>��;��B��<��=��>��E��A��B��H��B��C��D��L��E��G��F��H��I��O��K��L��M��O��P��R��U��U��V��}��X��X��W��X��Y��Z�҃�Ն��]��^�ս��u��^��`�Շ����`��n�׍�؈��d�ג��g�ؚ��e�ח��e�ם�ע��l����ڤ��j�چ�ب�ۧ�ح��k�ز��l��n��o��p�ڸ�݂��u�߽��q�܁��x�݄��t�߇�݂��y�������x��y��y�߅�������|��|���������~���ߕ����ߘ���������ߝ���ߠ�ߥ�������ߨ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#J�x�WI�T�+�^�n�5�T(L�*H�+����񪵪Ӥ=g�0��B%<��"�N�#Bd���A�dZXJGn���РC�5z��(cӪ]˶-Am�A�#�4hΚ1S��1_�^�Z��O+ S`ʤ�)R�2X��qj!D|HH�)Ll�0��@1|Sǔ���ȡ�Ǐ F�.i┊�����Pd �,Z��a#"M��Q��@�h=�$V���g͖\%+�KV�H�F@�g���,�3'��"9h�0q���!l���Q�
+(���T@�tCQ���@a�`�RT��\�A�k(܉(���	�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/forum_unread.png forums//site/static/beta5/pics/forum_unread.png
--- beta5//site/static/beta5/pics/forum_unread.png	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/forum_unread.png	2011-02-05 10:10:02.694335002 +0100
@@ -0,0 +1,8 @@
+�PNG
+
+���
IHDR��� ��� ���szz����bKGD�������������	pHYs����������tIME�		0*�f���IDATX���kU�?w~��lv7��ش��m�4mQ�}4hѪ���G+jAD���`)>E}�C}����Z��XmK�j�ִI!M7;;sg�w����M_TȁÝ9̜���������mؿl�SPM1�aM�/ý�$����#�_QSX��p��~[��	���̓��g1_a�Ůc�v���+Wz��_z��N��,qq�C�5l�	�����H�D�J@��$�	�R��%�E`0{���^��*8&Ȉ��[r�]�E
Hc�I�#�Ch.�lA�A��j��
+0�����4�p�{&M�f���x�?@��V����h�R��'���Q���?���2�V�����
+�=}��6��c����F�8�-O�#��W���t6�T薜�)%'?==���i��	��������>�n����m�X'� ���������!��1I�rJ�}��:�O��W��<�Na�l�ps`����v���3#v|��",�b��C�A�kZ��1� nn�g�~��6�w��R_��@�"�!*M�����n��T<�Eh�,�x%�W@s��� 
+!\�8ӄ$�T�j�6�\�@�ӧ��X=��`1�]�4s�z2�h�'��֍�$�":>j��jUAw�W�
+F�i����t!6�K��C�	�	����̱uz�SK��/�*B��GkGX^(��_���l4/f:P�du�4����)I�-��7��cg۹^��NE�fuˆ���|O*������Q�ΰ��A��[��n�~,;�'��k@}`��8|��*�$A�8��RM1Lm�Qr�{�[@00l�,00-�����rf�w�/ �y�|��;��q�_������G��*�y��e�))CMO���^�0�ԭ��_-�Lh���(U!���4E�DS/�u;�jd:��q!PYo��Шb�;�eFu|x{��[P�QO��H=Y�jW
+T6z�{�Z����~�(�7�rC;d�.f�ps�nD�'aiw���6��v�(m�b���-س�ց��;���Z@�.���������������IEND�B`�
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/post_cn.gif forums//site/static/beta5/pics/post_cn.gif
--- beta5//site/static/beta5/pics/post_cn.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/post_cn.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,9 @@
+GIF89a � ����"�$), �-!�0$3%�8*�:,�;-<.>.�?/�?0�@1�A2�C4E4�F5�G6�G7�H8�I9J9L:�K:M;�N<�O=�P>�Q?�R@S@TA�UB�UBVC�WD�XE�YF�ZG�[H\H^I�]I_J�^J`K�_KaL�`LbM�fK�cN�dO�ePfQjOgQiR�hRlQ�iSnR�kT�oS�pS�rU�sVnXpYuXr[vYs\t]w^�{]�v^	y`~`|c�a{b�h�j
�l�j�k�m�l�n�m�n�o	�o�p�q�r�q�t�s�u�v�v�w�w�x�y �z�{�|�|$�}�~��'����(��!��)��"��*��#��+��$��,��-��.��.��5��/��0��1��2��2��3��3��4��4��5��<��6��7��8��9��:��A��B��C��D��E��F��G��H��I��J��K��Q��W��R��X��S��T��U��V��W��X��_��`��a��b��h«cìd¬jĭeíkŮfįlŰmƱnDzoɳp̷{лѼ�ӿ�����‹�Ì�č�Ŕ�Ȗ�ɗ�ʘ�˙�̚�͛�͡�΢�ϣ�Х�Ҧ�Ԯ���������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�C�Y"J���!�,W�����8 ф�h1K@�����9rڨS���;��i�;y����F���
+����t#C��81���6e��"g�E�-��	�K�p�CG�Q�Y�h�K�R4b���DO?w^����'N�4%���@;j��"��;E�B�ʔgQ�@M��A�?~��pL�)�
+e
+*R�<=)��� >���K4���J�
+UnS�:5R�`�����<y�g�)�����ʔ�4d�ȡÆd?r�H�D�q9^
+v���NpA!���
6�:�`�B�w�-��@0A�B	4��X
9��^}đAR��,��b��%���
+;ư�
p��ْD
d0��t(B	*�@B�'�@C~���fW䢆8 �8��4xHB4`�SH8��R^*�pB�*�A	!t�a,��+tE#���,���
++��
L�!Tq�$�4R�r�E0�0s�1��L/�����\1I%�,�H�d� �,�L2��2J80A�Q�\cRFU�I��,#L/Z8�A�^�	(sI2���+̨z.O�@�c�ҙ'�Tr�iq�1���F���(����%t\�Я���*�qJ,�xF��>�E#����(����$
;l�@VhaFZ�|��+�l���;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/post_co.gif forums//site/static/beta5/pics/post_co.gif
--- beta5//site/static/beta5/pics/post_co.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/post_co.gif	2011-02-05 10:10:02.714335002 +0100
@@ -0,0 +1,6 @@
+GIF89a � ���...666:::===>>>???@@@BBBDDDEEEHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmppprrrssstttuuuvvvwwwxxxzzz|||}}}~~~���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\���Ç� b�H�"D���#bA@�T�Be��(O�0QR�GG�<�dђ�+ULBy�d	 /����K.[hB�d
+�&L�	����1b„�˓%`,�"�J%S�`b�L�2dƀI�BD	2��5(B!hԤ�ƌ�XA�I��*��8ȣ��6�٬Qs�@�&���l�"T��u8o�0A�����-^���TI�Q�>G��11�8�B/j4���I��X�hW�ĉ�'O����0�D�,Z���d
+�&i�Ȳ'�
 H�pA��$�֜
+Ɛ�OA[�Q�0�@X���	����
�)H\��
+0��~`�� �0	$�`c	6Tj#zFT��X�A�B"4��VLaĂ`�!A�`��t�A���_���WP1eAZ�!D�Y@�)��l�e8\�Ua����x<���3�`�`�AZt�E�}t�"�R� ��c� �TЁ_�Յ�M�&�4��"�RH nаbh��h��Y�PU�8�����4��=��F�zq�����#�$R�0`���mp+����*bW2��o�G�eD�C<���|���H�q��D��!a!��u�op!�j
QiR4]��=�G4�l�G�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/post_on.gif forums//site/static/beta5/pics/post_on.gif
--- beta5//site/static/beta5/pics/post_on.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/post_on.gif	2011-02-05 10:10:02.684335002 +0100
@@ -0,0 +1,11 @@
+GIF89a � ����)�)& ( �, �2%�5(7)�9+�:,�;-<.>.�?/�?0�@1�A2�C4E4�F5�G6�G7�H8�I9J9L:�M;�N<�O=�Q?�R@TA�TAUB�VC�WD�XE�\C�YF�ZG�[H\H^I�_J�`K�aL�bM�cN�gL�dO�cO	ePfQjOhRlQ�nR�oS�lU�pS�qT�lVrU�sVpYqZqZ
xZ�wZv]�t]w^�{]�v^	u^w_
+za�~`{b{b}d}d�f~ef�i�l�j�m�k�n�n�o	�p
+�q�q�r�s�r�t�t�u�u�v�w�w�x�y�y �z�{�|�|$�}�~�,�},�~&��'����!��)��"��*��#��+��$��,��-��-��.��/��0��0��1��1��2��2��9��3��3��:��4��4��;��5��5��<��6��6��=��7��7��>��8��?��9��@��@��:��A��G��B��C��D��E��F��G��H��I��J��P��K��Q��L��R��S��T��U��V��W��Y��_��`��f��a��b«cìd¬jįlŰmƱnDzoʴq˵r̶s̷{ι}Ϻ~�č�Ŕ�̚�̠�͡�ϣ�Х�Ҧ�ӧ�Ԯ���������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\��Çr)c�1�d�� 1p����d9p���±#�4��$D =z�i�FL˅W�T�$)R�Du�H(��:mҀ���J P��f�T�Ƃ���sRN�	��1e��(R�ڼX𠮍B����U�g>�JE�Ԟ�>����ȱ'�d9���X�\ifc�ƒ(�t��͓*���"�V�X�a�A@!��H�d�P=y���bPϭ[��עǀ��6$��)Q�@zPz�x���]�x��
+��Bt����� @}��A;��#a����3� lPB	 L�J)���4E�@[D"�0�(��2�q@�P��,2�
+)��Fy�A\}�c
+B@`��	-� � M��2<�[T�(�(�A2�WB#���I@a0����qƊYA�(���P��X�:�	��U�_����@�Ap�B�-�0�<�P��(B�b�2ƈ������%�5��[ ��#����A[��L��@A
+�p”1��bt��$x�I�8S�P��
+X�iD��2
+'�R�AX�"�4�X*k&��CC�a�+��҉%j�6��H�1�R��	0���+���J)��!,��Bs,��P��R�*��q�BV��3��I�t���-���
+Vt�l䒌0z��-��r"b�l2�bD�#�����L�WTA��H'��@�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/post_oo.gif forums//site/static/beta5/pics/post_oo.gif
--- beta5//site/static/beta5/pics/post_oo.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/post_oo.gif	2011-02-05 10:10:02.684335002 +0100
@@ -0,0 +1,7 @@
+GIF89a � ���---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\�P�Ç&|H�2cĄ�Q"A)T޼q�f��41��(Q
+�9r��qC��3e2^����;v���$Ȗ5hR��³��=z���C���
Vpa��E1[��R�?}���R�C���ꕌ�*b?Z$HP�@�xh���J�i��̘0yJ1D�P�B��$pqXĈ&t�,��K�R�N���0D�(aʝ:���Y�G�	_�E��(RL�s��L6i.�V$I�H��aP�D�,�����c4ez7�D��*U��i�C/���&ʓ0l$�[$�T��|p����1�� k�A
?��To�dbx��8���1�P�cA��
N@��%�p��@�	=���70a�D�0%��I$ �"���
+(�pC:��ÖD�!�l�G&Z\��	5�0�2�p�<���Fh�y�I��'\�Y)<�&;��Ip��ι�g*���������a�uı�X��"ʤ
+���5��c��Z��&Y��2�f��C�I���~��d���*J$`��F"�Hzp����(\X��
+D4�F#� bmZ�zl���ID���<�"��b�Rp�
+&u��%�@�o����
{$��Wl�?�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_locked.gif forums//site/static/beta5/pics/topic_locked.gif
--- beta5//site/static/beta5/pics/topic_locked.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_locked.gif	2011-02-05 10:10:02.694335002 +0100
@@ -0,0 +1,5 @@
+GIF89a � �������			
+
+
+


   !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#J��ԫV�(�$�<y�t5�(p7~��mK�� ����K�hѧj�filT�'�����C�R�es�9��EJ�|y)��$�����RS6DQs���2�)��5�׬]����%��'F;�S���߿�a�H)۷È���BЪI�L9�/Ux �A欳g��t�2��`�R�&F��,W�D��G�/`�|�҅�,W�R}�����_�v�E+�+V�N��4�]�v�5+�T�H5��T�a�N�L���ӦL�*I�KD+_�x�҅�-[h�EO�@ҁ&���&�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_locked.png forums//site/static/beta5/pics/topic_locked.png
--- beta5//site/static/beta5/pics/topic_locked.png	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_locked.png	2011-02-05 10:10:02.684335002 +0100
@@ -0,0 +1,6 @@
+�PNG
+
+���
IHDR��� ��� ����s����bKGD����̿���	pHYs����������tIME�	
+m�)X���IDATH��=hQ�{^��%�$�=J!H
+E1�,R�[Y��D�F��hP����B�@�4)�tz"\�xz1���swo?޳09��Ke�y�¾���7�v������]C;u`ZZ�K鱹x������{�%iK[~����OO�ۣڜ�<�����Fg�{�h0�]�[�ش
+�?����Z��܉�a&N�\`���f߭���w�Z�8G�.�Y/��2�Hm�M���D/E>,�x��),,�?��������<elBj�R�T�C��G@(����"&�WF'D��S�L~��?pe�����0X�dN/>	�4��S��jx�L9%���,?>n<�:Cퟀ��,�MW�-����U���X�.X���f%�����:&V��	���H�ǘxDT������	Hjx��f���A�D��	��6	�$����쑢���)�[�n`;ž1r�EXᗻ�E��͗�tC�lt�]{�׌H��nzZ���G�q���o���j�����IEND�B`�
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_poll.gif forums//site/static/beta5/pics/topic_poll.gif
--- beta5//site/static/beta5/pics/topic_poll.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_poll.gif	2011-02-05 10:10:02.694335002 +0100
@@ -0,0 +1,9 @@
+GIF89a � ��m����			
+
+
+


   !!!###$$$&&&---///111222666777888;;;<<<===>>>@@@AAABBBCCCEEEFFFGGGHHHIIIJJJLLLMMMNNNPPPRRRSSSTTTUUUWWWYYY]]]___```aaabbbcccdddeeekkklllmmmnnnooorrrssswwwzzz���������������������������������������������������������������������������������������������������������������������������������������������������!�
+��,���� � ����������������������
+����;NM?/��Ilh[D	��HjfYB��GidWA������ŏ'PSOK(���Đ
+-ejd_-	J�ΐ	*agb],���ۏA^f]+���
+pP�q��=[AD�#B���߼���\xQ����̰�
#"�8��ე�	/6P�E�7$��"�$:F4m�����?EAo��	S���PrܡcB�*4P`�挖!�fRX�F��P^$�`dL*3>2 �
+�@8p`"�)���A����+V���8�p���˘3k6�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_poll.png forums//site/static/beta5/pics/topic_poll.png
--- beta5//site/static/beta5/pics/topic_poll.png	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_poll.png	2011-02-05 10:10:02.684335002 +0100
@@ -0,0 +1,7 @@
+�PNG
+
+���
IHDR��� ��� ���szz����bKGD�������������	pHYs����������tIME�	s(	���IDATX��Ok$U��uuWu�PiB4 H�����!!�,EA�0�����r����E�	ѕ&n� Ջ����zﺘ�&��L'�a\��n���{���^���!��j����z�T*��"���1�o�܏�^�{U�wK��0���c��9���k��EQ�i�f�K��:s��GD�0|��������\^^��lR���^��V�����?��o���W�(���_��Ͽ����;;;lll���b|&"�\�knIJU�(
+)�k�8��P���GQ���oc�"�"RXk��Uu�=eY�����L&��XZ�V5��"�����uqq�ި�^J �"�F=��KKK�ooo��  �2�8����n�����1�O���}�s�������z�4M������"2��u{���Q�`0��*�Z�,�(
+ʲ�,�&��f
+w��e�7y��wvv����v���}�:[{A��A�|/"ov:����<���-�s�D�i	�߆a����BX���5���$I���*��2o�۵Z��f����k��k-�����}VVV�T*����Hc��N���}F�	�%쨛M�CD&��j��:^��d�V�y�VD&�c?c�V��w���Q�V���y��Xq]���@���(��`c�eI�$���G���q�`0 M�qLT�ި��i	|
+<>����s<ϛH�k-��m���9���)�~k-�n�N�CY�5U}��2���	�1���*sss�*eYr�R�1�4MI��7৻��;���?�|�r�������IEND�B`�
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s0_read.gif forums//site/static/beta5/pics/topic_s0_read.gif
--- beta5//site/static/beta5/pics/topic_s0_read.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s0_read.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,4 @@
+GIF89a � ��x����'''(((***---000111222555666777<<<>>>???CCCDDDGGGIIIJJJKKKLLLMMMNNNRRRSSSTTTUUUWWW^^^```bbbcccdddfffkkklllmmmnnnqqqrrrtttzzz}}}������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+��,���� � �������������������/=CB?:73-,)('$!
+�1SUQLJGCD850+)&$���7SOOJHE>>�,,� �	�3XWV�UUSSMMKKDDAA<841�-�7vuupsp�tq޼��
�7kά!��	C9�ܩs�NE��X��oN�8n�Aa�ƓQ��XG�6rƜ���&F�-ᄔ�F��6m���F
�5>�E9�N�7r��dӰ�!K3�#�
9hֈ�b�֬���i�R�3�f�|!q5�ĭ]��[��٥i׶}w��v+^�s��3eΜ!3&��k�xP�iҠ1C��-t���r��6���[f0ZFb��9oԴL7͚ڦO�&���|�ܴA�!2fƈs��eC�%���
�q)����jC*�=}#���`(���K��Y`V
+TP�P8�DHa�D��C;��Åꀁ!h�� 6��$"`@ ������Ɍ4�h�8�H �;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s0_unread.gif forums//site/static/beta5/pics/topic_s0_unread.gif
--- beta5//site/static/beta5/pics/topic_s0_unread.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s0_unread.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,2 @@
+GIF89a � ��v�'#($*&-(0+1,2-506172<6>8?9C<!D="G@#IB$JC%KD%LE&NF'RJ)SK)TL*UM*WO+^U/`W0bY1cZ1d[2f\3ka5lb6mc6nd7qf8rg9ti:zo=}q>s?�t@�vA�wA�xB�zC�{D�|D�}E�~E��F��G��G��H��H��I��I��J��J��K��L��M��N��O��O��P��P��Q��R��T��U��V��W��W��X��X��Y��Y��Z��[��[��_ñaɶdнh��i��j��l��l��m��p��q��q��r��s��t��t��u��u��v��v��w��w��x��x��y��y��z��z��{��{��|��|��}��}��~��������������������������������!�
+��,���� � �������������������-;A@=851+*'&%"	�/QSOJHEAB63.)'$"���5QMMHFC<<�**���1VUT�SSQQKKIIBB??:62/�+�5tssnqn�roڴ��f��6iʤs�	C7�ԙS�NE��X��/Μ7l��1a�ƓQ��8��5p”���&F�-݄��F��6m�̹��4>�EGΜ6p��T�p�!K3�{��8f҈�b�֬���Y�
2�d�tq5�ĭ]��[��٥i׶}w��t+^�s-�2cʔ�K�k�x��gΘ!#F��,t���2��5���;&�/XBR�r�8mд<�L�ڦO�&���|�ج1��1d€�R��eC�%��u��q)���EjC(�=m#]��^({��eK��W`V�	RH�N0��FAB���9�Å�`�!`�� 2Ѐ$P�����b��Ɍ4�h�8�H �;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s10_read.gif forums//site/static/beta5/pics/topic_s10_read.gif
--- beta5//site/static/beta5/pics/topic_s10_read.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s10_read.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,12 @@
+GIF89a � ������
+
+
+'''(((***---000111222555666777<<<===>>>???CCCDDDGGGIIIJJJKKKLLLNNNOOOPPPQQQRRRSSSTTTUUUVVVWWW^^^```bbbcccdddfffjjjkkklllmmmnnnpppqqqrrrtttzzz}}}���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*��d�'I���
2\�H��Ç4X��� �2h�t�re
+�#D~��QC�
+#<lؐ�B˂Fʀ�K�&Mp��ѳ�
+%<�԰���B֨IC
�2e�x�…
+�(Q��.�0Z��P"C}*T��A�
+�Hϟ:5"i�hQ#F�)S^����D�B���"�SoV��"D�!�c#��ۚ1�64 ���n���N���=��@���@@�Fn ��h�lA�g�2���Y�0�� D~ɯ#� ��4�P��b�k@����u�0^y��������{��}�w�*�0��8`���Ȉ$���"�W��G}��n��m{�q@r ��|衇nXP��F�SB���0�y�aG1�k�(�H!�������H*I���_�� ���y�aGu�h���6�{�"��2�eo$iP|-BX���8�2�Qs@
+)6t�g�AF_p�9WXaP0��I,�k�I�`&����`@��<��
,�,
+8��Hd���f�-B�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s10_unread.gif forums//site/static/beta5/pics/topic_s10_unread.gif
--- beta5//site/static/beta5/pics/topic_s10_unread.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s10_unread.gif	2011-02-05 10:10:02.694335002 +0100
@@ -0,0 +1,12 @@
+GIF89a � ��������
+	
+		
+

'#($*&-(0+1,2-506172<6=7>8?9C<!D="G@#IB$JC%KD%LE&NF'OG'PH(QI(RJ)SK)TL*UM*VN+WO+^U/`W0bY1cZ1d[2f\3j`5ka5lb6mc6nd7pe8qf8rg9ti:zo=}q>s?�t@�vA�wA�xB�zC�{D�|D�}E�~E��F��G��G��H��H��I��I��J��J��K��L��M��M��N��O��O��P��P��Q��R��T��T��U��V��W��W��X��X��Y��Y��Z��Z��[��[��]��_ñaɶdнh��i��j��l��l��m��m��n��o��p��q��q��r��s��t��t��u��u��v��v��w��w��x��x��y��y��z��z��{��{��|��|��}��}��~��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*��d�'I���
2\�H��Ç4X��� �2h�t�re
+�#D~��QC�
+#<lؐ�B˂Fʀ�K�&Mp��ѳ�
+%<�԰���B֨IC
�2e�x�…
+�(Q��.�0Z��P"C}*T��A�
+�Hϟ:5"i�hQ#F�)S^����D�B���"�SoV��"D�!�c#��ۚ1�64 ���n���N���=��@���@@�Fn ��h�lA�g�2���Y�0�� D~ɯ#� ��4�P��b�k@����u�0^y��������{��}�w�*�0��8`���Ȉ$���"�W��G}��n��m{�q@r ��|衇nXP��F�SB���0�y�aG1�k�(�H!�������H*I���_�� ���y�aGu�h���6�{�"��2�eo$iP|-BX���8�2�Qs@
+)6t�g�AF_p�9WXaP0��I,�k�I�`&����`@��<��
,�,
+8��Hd���f�-B�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s1_read.gif forums//site/static/beta5/pics/topic_s1_read.gif
--- beta5//site/static/beta5/pics/topic_s1_read.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s1_read.gif	2011-02-05 10:10:02.684335002 +0100
@@ -0,0 +1,3 @@
+GIF89a � ��w����'''(((***---000111222555666777<<<>>>???CCCDDDGGGIIIJJJKKKLLLNNNRRRSSSTTTUUUWWW^^^```bbbcccdddfffkkklllmmmnnnqqqrrrtttzzz}}}���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+��,���� � ���������������������.<BA>962,+('&# 
+�0RTPKIFBC74/*(%#���6RNNIGD==�++��	�2WVU�TTRRLLJJCC@@;730�,�6uttoro���q��6oܨ1�f/%�c���:)R����>9tഉS愡SnTI$�8o��c"�ʛ1�|32N��h�L	�@E�0٤Y�(���ʙC�M:2�<$a(T�EC�i�&�5h�p-��k�8�qؼL��L2^Ft�j',��eϦ]K�-Էq�ֽ�א�:�#K��V�2f̌FK�7�U|St�V4hΔ3���Z�l)nQ7Z��%#�˗,"�`IgN7i^���F
�֮aR���>~m؜Y��̘2b�|1�ų!�u,�f�ݻ�����嵡���ɾ&��/�}�E\�G�r�S<�M(��FQ�?���9��a�9\`�d0��
8���P�� ���(��H��<��#"���;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s1_unread.gif forums//site/static/beta5/pics/topic_s1_unread.gif
--- beta5//site/static/beta5/pics/topic_s1_unread.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s1_unread.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,3 @@
+GIF89a � ��w����'#($*&-(0+1,2-506172<6>8?9C<!D="G@#IB$JC%KD%LE&NF'RJ)SK)TL*UM*WO+^U/`W0bY1cZ1d[2f\3ka5lb6mc6nd7qf8rg9ti:zo=}q>s?�t@�vA�wA�xB�zC�{D�|D�}E�~E��F��G��G��H��H��I��I��J��J��K��L��M��N��O��O��P��P��Q��R��T��U��V��W��W��X��X��Y��Y��Z��[��[��_ñaɶdнh��i��j��l��l��m��p��q��q��r��s��t��t��u��u��v��v��w��w��x��x��y��y��z��z��{��{��|��|��}��}��~�����������������������������!�
+��,���� � ���������������������.<BA>962,+('&# 
+�0RTPKIFBC74/*(%#���6RNNIGD==�++��	�2WVU�TTRRLLJJCC@@;730�,�6uttoro���q��6oܨ1�f/%�c���:)R����>9tഉS愡SnTI$�8o��c"�ʛ1�|32N��h�L	�@E�0٤Y�(���ʙC�M:2�<$a(T�EC�i�&�5h�p-��k�8�qؼL��L2^Ft�j',��eϦ]K�-Էq�ֽ�א�:�#K��V�2f̌FK�7�U|St�V4hΔ3���Z�l)nQ7Z��%#�˗,"�`IgN7i^���F
�֮aR���>~m؜Y��̘2b�|1�ų!�u,�f�ݻ�����嵡���ɾ&��/�}�E\�G�r�S<�M(��FQ�?���9��a�9\`�d0��
8���P�� ���(��H��<��#"���;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s2_read.gif forums//site/static/beta5/pics/topic_s2_read.gif
--- beta5//site/static/beta5/pics/topic_s2_read.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s2_read.gif	2011-02-05 10:10:02.694335002 +0100
@@ -0,0 +1,7 @@
+GIF89a � ��w����'''(((***---000111222555666777<<<>>>???CCCDDDGGGIIIJJJKKKLLLNNNRRRSSSTTTUUUWWW^^^```bbbcccdddfffkkklllmmmnnnqqqrrrtttzzz}}}���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+��,���� � ���������������������.<BA>962,+('&# 
+�0RTPKIFBC74/*(%#���6RNNIGD==�++��	�2WVU�TTRRLLJJCC@@;730�,�6uttoro���q��6oܨ1�f/%�c���:)R����>9tഉS愡SnTI$�8o��c"�ʛ1�|32N��h���Q� a�I�F
PdC�=J�M:2�<$a(FԢ���i�&�5h�p-�uh�}�EټL��L2^Ft�
+ ,��eϦ]K�-ηt�έ{7�!u"K�,��X3d̘#���o�
+�jQ4�ϔ3���Z�l)'���f���K�V��3'��4/��E�fwkװ	���o�6lάQxfL1a����ِl�:�e���]�ax{���P
+~U�`_����_x��͇h��0�OD�DJ q�E1�<�C;t�a�AL`b,�"0��8@�1
+0�����<��H �;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s2_unread.gif forums//site/static/beta5/pics/topic_s2_unread.gif
--- beta5//site/static/beta5/pics/topic_s2_unread.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s2_unread.gif	2011-02-05 10:10:02.694335002 +0100
@@ -0,0 +1,7 @@
+GIF89a � ��w����'#($*&-(0+1,2-506172<6>8?9C<!D="G@#IB$JC%KD%LE&NF'RJ)SK)TL*UM*WO+^U/`W0bY1cZ1d[2f\3ka5lb6mc6nd7qf8rg9ti:zo=}q>s?�t@�vA�wA�xB�zC�{D�|D�}E�~E��F��G��G��H��H��I��I��J��J��K��L��M��N��O��O��P��P��Q��R��T��U��V��W��W��X��X��Y��Y��Z��[��[��_ñaɶdнh��i��j��l��l��m��p��q��q��r��s��t��t��u��u��v��v��w��w��x��x��y��y��z��z��{��{��|��|��}��}��~�����������������������������!�
+��,���� � ���������������������.<BA>962,+('&# 
+�0RTPKIFBC74/*(%#���6RNNIGD==�++��	�2WVU�TTRRLLJJCC@@;730�,�6uttoro���q��6oܨ1�f/%�c���:)R����>9tഉS愡SnTI$�8o��c"�ʛ1�|32N��h���Q� a�I�F
PdC�=J�M:2�<$a(FԢ���i�&�5h�p-�uh�}�EټL��L2^Ft�
+ ,��eϦ]K�-ηt�έ{7�!u"K�,��X3d̘#���o�
+�jQ4�ϔ3���Z�l)'���f���K�V��3'��4/��E�fwkװ	���o�6lάQxfL1a����ِl�:�e���]�ax{���P
+~U�`_����_x��͇h��0�OD�DJ q�E1�<�C;t�a�AL`b,�"0��8@�1
+0�����<��H �;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s3_read.gif forums//site/static/beta5/pics/topic_s3_read.gif
--- beta5//site/static/beta5/pics/topic_s3_read.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s3_read.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,3 @@
+GIF89a � ��w����'''(((***---000111222555666777<<<>>>???CCCDDDGGGIIIJJJKKKLLLNNNRRRSSSTTTUUUWWW^^^```bbbcccdddfffkkklllmmmnnnqqqrrrtttzzz}}}���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+��,���� � ���������������������.<BA>962,+('&# 
+�0RTPKIFBC74/*(%#���6RNNIGD==�++��	�2WVU�TTRRLLJJCC@@;730�,�6uttoro���q��6oܨ1�f/%�c���:)R����>9tഉS愡SnTI$�8o��c"�ʛ1�|32N��h���"ъDa�I�F
PdC��!*gNR:2�<$a(FT��B]猚�^��z��T��q�$M��L2^Ft��V,��fѪ%�V�ۤr�ҵ�Wo!u"K�Ct_R6Dɘ13F-!��I9웱k��9Sf̘/Zjq�b�nR�KF̗/YDZ��Μ8nҼDc��ޯa��B#�}�ڰ9�F�1eĄ�b�hC�)�H���w;���eKlC)��!�}��_t��]pQ_}X�V�	SL�DO4�GQDC��:�Ç�p�!d��(6��,"`@ ��0�� Ɏ<��㏈�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s3_unread.gif forums//site/static/beta5/pics/topic_s3_unread.gif
--- beta5//site/static/beta5/pics/topic_s3_unread.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s3_unread.gif	2011-02-05 10:10:02.684335002 +0100
@@ -0,0 +1,3 @@
+GIF89a � ��w����'#($*&-(0+1,2-506172<6>8?9C<!D="G@#IB$JC%KD%LE&NF'RJ)SK)TL*UM*WO+^U/`W0bY1cZ1d[2f\3ka5lb6mc6nd7qf8rg9ti:zo=}q>s?�t@�vA�wA�xB�zC�{D�|D�}E�~E��F��G��G��H��H��I��I��J��J��K��L��M��N��O��O��P��P��Q��R��T��U��V��W��W��X��X��Y��Y��Z��[��[��_ñaɶdнh��i��j��l��l��m��p��q��q��r��s��t��t��u��u��v��v��w��w��x��x��y��y��z��z��{��{��|��|��}��}��~�����������������������������!�
+��,���� � ���������������������.<BA>962,+('&# 
+�0RTPKIFBC74/*(%#���6RNNIGD==�++��	�2WVU�TTRRLLJJCC@@;730�,�6uttoro���q��6oܨ1�f/%�c���:)R����>9tഉS愡SnTI$�8o��c"�ʛ1�|32N��h���"ъDa�I�F
PdC��!*gNR:2�<$a(FT��B]猚�^��z��T��q�$M��L2^Ft��V,��fѪ%�V�ۤr�ҵ�Wo!u"K�Ct_R6Dɘ13F-!��I9웱k��9Sf̘/Zjq�b�nR�KF̗/YDZ��Μ8nҼDc��ޯa��B#�}�ڰ9�F�1eĄ�b�hC�)�H���w;���eKlC)��!�}��_t��]pQ_}X�V�	SL�DO4�GQDC��:�Ç�p�!d��(6��,"`@ ��0�� Ɏ<��㏈�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s4_read.gif forums//site/static/beta5/pics/topic_s4_read.gif
--- beta5//site/static/beta5/pics/topic_s4_read.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s4_read.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,5 @@
+GIF89a � ������			'''(((***+++,,,---000111222555666777999;;;<<<>>>???CCCDDDFFFGGGIIIJJJKKKLLLNNNRRRSSSTTTUUUWWW^^^```bbbcccdddfffkkklllmmmnnnqqqrrrtttzzz}}}������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*�a�I$C��A#�,R� 1���2P����/`�\�"�ɓ >r̈ႅ�:hА�B˂@�h�Re
+�#GpҠ��	!:�̠���<Ȍ	C/^�`�b�ɓ%K�顣n�
�$��>}����=}�ࡃ'�6.
+!DH�A�)S���^@����3���SoV-��?}����"��ۚ1��3���}����Y*����;y��G:�8�#���y��0��:�4���3g��4u��g3�`���)G���O��q�s���G��d���G|��G�}��G� F��! �"X�
���� ����	4(t��g�`�
��@_|4A�L�Q�q���&d�m���z��m(m�a�
+��� ������A�xl��M4�f|���zԑ�bu�1o�A9�$e�
��yX��op�L$_��g4؆�m����NZ�����[t�E<VP1�QD�FA�E��� t�"p`lL��,p�D{@�p�e����v�P@�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s4_unread.gif forums//site/static/beta5/pics/topic_s4_unread.gif
--- beta5//site/static/beta5/pics/topic_s4_unread.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s4_unread.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,6 @@
+GIF89a � ������	
+'#($*&+',(-(0+1,2-50617293;5<6>8?9C<!D="F?#G@#IB$JC%KD%LE&NF'RJ)SK)TL*UM*WO+^U/`W0bY1cZ1d[2f\3ka5lb6mc6nd7qf8rg9ti:zo=}q>s?�t@�vA�wA�xB�yB�zC�{D�|D�}E�~E��F��G��G��H��H��I��I��J��J��K��L��M��M��N��N��O��O��P��P��Q��R��T��U��V��W��W��X��X��Y��Y��Z��[��[��_ñaɶdλgнh��i��j��l��l��m��o��p��q��q��r��s��t��t��u��u��v��v��w��w��x��x��y��y��z��z��{��{��|��|��}��}��~�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*�a�I$C��A#�,R� 1���2P����/`�\�"�ɓ >r̈ႅ�:hА�B˂@�h�Re
+�#GpҠ��	!:�̠���<Ȍ	C/^�`�b�ɓ%K�顣n�
�$��>}����=}�ࡃ'�6.
+!DH�A�)S���^@����3���SoV-��?}����"��ۚ1��3���}����Y*����;y��G:�8�#���y��0��:�4���3g��4u��g3�`���)G���O��q�s���G��d���G|��G�}��G� F��! �"X�
���� ����	4(t��g�`�
��@_|4A�L�Q�q���&d�m���z��m(m�a�
+��� ������A�xl��M4�f|���zԑ�bu�1o�A9�$e�
��yX��op�L$_��g4؆�m����NZ�����[t�E<VP1�QD�FA�E��� t�"p`lL��,p�D{@�p�e����v�P@�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s5_read.gif forums//site/static/beta5/pics/topic_s5_read.gif
--- beta5//site/static/beta5/pics/topic_s5_read.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s5_read.gif	2011-02-05 10:10:02.684335002 +0100
@@ -0,0 +1,5 @@
+GIF89a � ������			'''(((***+++,,,---000111222555666777999:::<<<>>>???CCCDDDFFFGGGIIIJJJKKKLLLNNNRRRSSSTTTUUUWWW^^^```bbbcccdddfffkkklllmmmnnnqqqrrrtttzzz}}}������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*�a�	$C��A#�,R� 1���2P�����._�X���� >r̈ႅ�:hА�B˂@�d�BEʓ#GpҠ��	!:�̠���<ĄC�˗.]�\�Rʼn%J�顣n�
�
+��>}�ٳ�o�<|�ܙsN�5.
+4(� A�)S����?����#���SoVt ?|��y�"��ۚ1��3ڏ�}�����V�$����;���`�?"���H6��-�����:���@�:w�	_p����Z@�k;v�!Gk� �p�@�`��z��|�dnp@��X�A7b�(f���h��q�1o�a�
+$޶We��@uI�p���d�m��A�z�Q���l���A4� ~�a�kuX�Vyd�͠_{�Gt�p��ḻƍ-I�n���H�W�q���W �͉���A#k�������cA0x�\h�ESH6P8��F1D��:� �[������P��`�D�Vk���"�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s5_unread.gif forums//site/static/beta5/pics/topic_s5_unread.gif
--- beta5//site/static/beta5/pics/topic_s5_unread.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s5_unread.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,6 @@
+GIF89a � ������	
+'#($*&+',(-(0+1,2-50617293:4<6>8?9C<!D="F?#G@#IB$JC%KD%LE&NF'RJ)SK)TL*UM*WO+^U/`W0bY1cZ1d[2f\3ka5lb6mc6nd7qf8rg9ti:zo=}q>s?�t@�vA�wA�xB�yB�zC�{D�|D�}E�~E��F��G��G��H��H��I��I��J��J��K��L��M��N��N��O��O��P��P��Q��R��T��U��V��W��W��X��X��Y��Y��Z��[��[��_ñaɶdнh��i��j��k��l��l��m��o��p��q��q��r��s��t��t��u��u��v��v��w��w��x��x��y��y��z��z��{��{��|��|��}��}��~��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*�a�	$C��A#�,R� 1���2P�����._�X���� >r̈ႅ�:hА�B˂@�d�BEʓ#GpҠ��	!:�̠���<ĄC�˗.]�\�Rʼn%J�顣n�
�
+��>}�ٳ�o�<|�ܙsN�5.
+4(� A�)S����?����#���SoVt ?|��y�"��ۚ1��3ڏ�}�����V�$����;���`�?"���H6��-�����:���@�:w�	_p����Z@�k;v�!Gk� �p�@�`��z��|�dnp@��X�A7b�(f���h��q�1o�a�
+$޶We��@uI�p���d�m��A�z�Q���l���A4� ~�a�kuX�Vyd�͠_{�Gt�p��ḻƍ-I�n���H�W�q���W �͉���A#k�������cA0x�\h�ESH6P8��F1D��:� �[������P��`�D�Vk���"�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s6_read.gif forums//site/static/beta5/pics/topic_s6_read.gif
--- beta5//site/static/beta5/pics/topic_s6_read.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s6_read.gif	2011-02-05 10:10:02.694335002 +0100
@@ -0,0 +1,8 @@
+GIF89a � ������			'''(((***+++,,,---...000111222555666777999;;;<<<>>>???CCCDDDFFFGGGHHHIIIJJJKKKLLLNNNRRRSSSTTTUUUVVVWWW^^^```bbbcccdddfffkkklllmmmnnnqqqrrrtttzzz}}}���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*ԑ䉓%F���q��/X�8a�4T���`0b�d�B�	"Axؠ�E
+!>p���B˂C�p�r�J%Jp޸�sE
+#>�����?ʐCV�0`�h��
+�&M�ң.��4�O ?}�����=~�䩓GΝ61)ThP!B�)S����@���Dg�A!�SoV=� @~���##��ۚ1��3���F�`��-�� 6=y�#���	�%���|�l=�a��q[
�z
+���
 ����F|A�%��R
+�	���x�A�m�0nH��A!�����|yԷ�mr@�F�x`��@Ȋ,�����A��Q�q��F)޶��0��1gr��
+��m���t�Q$�s��n��A7�6� ���kwxGZ*�d�٠_{���v衘r�n��Ǝ=I�n����#[������ �ݩ�����m�������cA3�F_t�;VTA�R@�DIa��j��@	k�$�@���P��`�D�f����"�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s6_unread.gif forums//site/static/beta5/pics/topic_s6_unread.gif
--- beta5//site/static/beta5/pics/topic_s6_unread.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s6_unread.gif	2011-02-05 10:10:02.694335002 +0100
@@ -0,0 +1,9 @@
+GIF89a � ������	
+'#($*&+',(-(.)0+1,2-50617293;5<6>8?9C<!D="F?#G@#HA$IB$JC%KD%LE&NF'RJ)SK)TL*UM*VN+WO+^U/`W0bY1cZ1d[2f\3ka5lb6mc6nd7qf8rg9ti:zo=}q>s?�t@�vA�wA�xB�yB�zC�{D�|D�}E�~E��F��G��G��H��H��I��I��J��J��K��L��M��N��O��O��P��P��Q��Q��R��T��U��V��W��W��X��X��Y��Y��Z��[��[��_ñaɶdнh��i��j��l��l��m��o��p��q��q��r��s��t��t��u��u��v��v��w��w��x��x��y��y��z��z��{��{��|��|��}��}��~��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*ԑ䉓%F���q��/X�8a�4T���`0b�d�B�	"Axؠ�E
+!>p���B˂C�p�r�J%Jp޸�sE
+#>�����?ʐCV�0`�h��
+�&M�ң.��4�O ?}�����=~�䩓GΝ61)ThP!B�)S����@���Dg�A!�SoV=� @~���##��ۚ1��3���F�`��-�� 6=y�#���	�%���|�l=�a��q[
�z
+���
 ����F|A�%��R
+�	���x�A�m�0nH��A!�����|yԷ�mr@�F�x`��@Ȋ,�����A��Q�q��F)޶��0��1gr��
+��m���t�Q$�s��n��A7�6� ���kwxGZ*�d�٠_{���v衘r�n��Ǝ=I�n����#[������ �ݩ�����m�������cA3�F_t�;VTA�R@�DIa��j��@	k�$�@���P��`�D�f����"�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s7_read.gif forums//site/static/beta5/pics/topic_s7_read.gif
--- beta5//site/static/beta5/pics/topic_s7_read.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s7_read.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,4 @@
+GIF89a � ������			'''(((***+++,,,---...000111222555666777999<<<>>>???CCCDDDGGGHHHIIIJJJKKKLLLNNNRRRSSSTTTUUUVVVWWW^^^```bbbcccdddfffkkklllmmmnnnqqqrrrtttzzz}}}���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*�q�	�$D���QCF-T�(A���0P�����._�X���!?tА��	:hА�B˂A�d�BE�$Hp֨�3�	!:�Ġ��=ĄC�˗.]�\�Rʼn�%K�񱣮�
�
+��>}�ٳ�o�<|�ܙsN�5/4(� A�)S����?����#'�A �SoVt ?|��y#��ۚ1��3ڏ��F���f,���5s;x��G�:��G����?����y�� ��h��3���g1׃����k�<�Z�(�<��5�r��-����d0���|�p��9� F��Q`�	.�`A8�������v�q0�m���A8�& sP|�au�e�H�
���u�Ass���l�A��Z �����Ց`w��ƒL:9
��_z�A���o���k�h���6~�G�
+���k�ѤA3�ay�1G�q��#k��F����cA1x�@\h�ESHP8��GA���J� �$[�4�@���P��`�D�v����"�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s7_unread.gif forums//site/static/beta5/pics/topic_s7_unread.gif
--- beta5//site/static/beta5/pics/topic_s7_unread.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s7_unread.gif	2011-02-05 10:10:02.684335002 +0100
@@ -0,0 +1,5 @@
+GIF89a � ������	
+'#($*&+',(-(.)0+1,2-50617293<6>8?9C<!D="G@#HA$IB$JC%KD%LE&NF'RJ)SK)TL*UM*VN+WO+^U/`W0bY1cZ1d[2f\3ka5lb6mc6nd7qf8rg9ti:zo=}q>s?�t@�vA�wA�xB�yB�zC�{D�|D�}E�~E��F��G��G��H��H��I��I��J��J��K��L��M��N��O��O��P��P��Q��R��T��U��V��W��W��X��X��Y��Y��Z��[��[��_ñaɶdнh��i��j��l��l��m��n��o��p��q��q��r��s��t��t��u��u��v��v��w��w��x��x��y��y��z��z��{��{��|��|��}��}��~��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*�q�	�$D���QCF-T�(A���0P�����._�X���!?tА��	:hА�B˂A�d�BE�$Hp֨�3�	!:�Ġ��=ĄC�˗.]�\�Rʼn�%K�񱣮�
�
+��>}�ٳ�o�<|�ܙsN�5/4(� A�)S����?����#'�A �SoVt ?|��y#��ۚ1��3ڏ��F���f,���5s;x��G�:��G����?����y�� ��h��3���g1׃����k�<�Z�(�<��5�r��-����d0���|�p��9� F��Q`�	.�`A8�������v�q0�m���A8�& sP|�au�e�H�
���u�Ass���l�A��Z �����Ց`w��ƒL:9
��_z�A���o���k�h���6~�G�
+���k�ѤA3�ay�1G�q��#k��F����cA1x�@\h�ESHP8��GA���J� �$[�4�@���P��`�D�v����"�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s8_read.gif forums//site/static/beta5/pics/topic_s8_read.gif
--- beta5//site/static/beta5/pics/topic_s8_read.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s8_read.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,7 @@
+GIF89a � ������			'''(((***+++,,,---...000111222555666777999<<<>>>???CCCDDDGGGHHHIIIJJJKKKLLLNNNRRRSSSTTTUUUVVVWWW^^^```bbbcccdddfffkkklllmmmnnnqqqrrrtttzzz}}}���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*�q�I%D���QCF-T�(A���0P����/`�\�"��!?tА��	:hА�B˂A�h�Re
+$Hp֨�3�	!:�Ġ��=ƈ	C/^�`�b��&L�񱣮�
�$��>}����=}�ࡃ'�6/!DH�A�)S���^@����3'�A �SoV-��?}���#��ۚ1��3���B� ��,���2s������P�$4���c9�@�����؅A�����P���x�s<xؘ/����Q�P��}����r���y�@�`�@@�y ��~G����sd��
+����0Ȍ�����8^�z0'���+ĨY� Ee}D�sv��\m��`A7h���Ȋ���r0�Fe�`P
�	�|��s�ar�Ae�WD�f|���zԑ�bu�1o�AD�%e�
֡y��`���gXi�|	B�y�A�mp��Mj�
+*E�_l��YXA�RD�KqDD�l�D�`
+"����X0��4��
+��xk�Hd�覫.B�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s8_unread.gif forums//site/static/beta5/pics/topic_s8_unread.gif
--- beta5//site/static/beta5/pics/topic_s8_unread.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s8_unread.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,8 @@
+GIF89a � ������	
+'#($*&+',(-(.)0+1,2-50617293<6>8?9C<!D="G@#HA$IB$JC%KD%LE&NF'RJ)SK)TL*UM*VN+WO+^U/`W0bY1cZ1d[2f\3ka5lb6mc6nd7qf8rg9ti:zo=}q>s?�t@�vA�wA�xB�yB�zC�{D�|D�}E�~E��F��G��G��H��H��I��I��J��J��K��K��L��M��N��O��O��P��P��Q��R��T��U��V��W��W��X��X��Y��Y��Z��[��[��_ñaɶdнh��i��j��l��l��m��o��p��p��q��q��r��s��t��t��u��u��v��v��w��w��x��x��y��y��z��z��{��{��|��|��}��}��~�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*�q�I%D���QCF-T�(A���0P����/`�\�"��!?tА��	:hА�B˂A�h�Re
+$Hp֨�3�	!:�Ġ��=ƈ	C/^�`�b��&L�񱣮�
�$��>}����=}�ࡃ'�6/!DH�A�)S���^@����3'�A �SoV-��?}���#��ۚ1��3���B� ��,���2s������P�$4���c9�@�����؅A�����P���x�s<xؘ/����Q�P��}����r���y�@�`�@@�y ��~G����sd��
+����0Ȍ�����8^�z0'���+ĨY� Ee}D�sv��\m��`A7h���Ȋ���r0�Fe�`P
�	�|��s�ar�Ae�WD�f|���zԑ�bu�1o�AD�%e�
֡y��`���gXi�|	B�y�A�mp��Mj�
+*E�_l��YXA�RD�KqDD�l�D�`
+"����X0��4��
+��xk�Hd�覫.B�;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s9_read.gif forums//site/static/beta5/pics/topic_s9_read.gif
--- beta5//site/static/beta5/pics/topic_s9_read.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s9_read.gif	2011-02-05 10:10:02.684335002 +0100
@@ -0,0 +1,10 @@
+GIF89a � ������
+
+
+'''(((***---000111222555666777;;;<<<===>>>???CCCDDDGGGIIIJJJKKKLLLNNNPPPQQQRRRSSSTTTUUUWWW^^^___```bbbcccdddfffkkklllmmmnnnoooqqqrrrtttzzz}}}������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*�d�'H�١ㆍ1\�@q�
6\����2g�t�re
+#C|�A#Ɗ#>p���B˂EȀ�%K&Lp��ѳ�
+%>�����AԤAC��2d�x�…
+(P����0Z��P"C}*T��A�
+�Hϟ:4i�hQ#F�)S^����D�B�džA"�SoV��"D�!�S#��ۚ1�64 ������	�J*'r�� @�G:�x��(�3��ق��0�:�2�,���"?���_��m���@�߀���u�@�y���qm�^|��w�}�]f�)�@�h`A=0"∌���0H�@�Gz�q�/��m%�� n`�|衇n|H�-B"#�E!�5�{�a�m�`��-�"�����H�G"��@9h��^|2��(�|䁇}�A�AL�W�`"��ƈ��u���A8�av
+��v�hGt�i�k�X�
f��a|��:Y\a�T<��I �Į�"A�A,�`��b�A�F��4���(��, ѵ�f����;
\ No newline at end of file
diff -Naur beta5//site/static/beta5/pics/topic_s9_unread.gif forums//site/static/beta5/pics/topic_s9_unread.gif
--- beta5//site/static/beta5/pics/topic_s9_unread.gif	1970-01-01 01:00:00.000000000 +0100
+++ forums//site/static/beta5/pics/topic_s9_unread.gif	2011-02-05 10:10:02.704335002 +0100
@@ -0,0 +1,10 @@
+GIF89a � ��������
+	
+		
+

'#($*&-(0+1,2-506172;5<6=7>8?9C<!D="G@#IB$JC%KD%LE&NF'PH(QI(RJ)SK)TL*UM*WO+^U/_V/`W0bY1cZ1d[2f\3ka5lb6mc6nd7oe7qf8rg9ti:zo=}q>s?�t@�vA�wA�xB�zC�{D�|D�}E�~E��F��G��G��H��H��I��I��J��J��K��K��L��M��N��N��O��O��P��P��Q��R��S��T��U��V��W��W��X��X��Y��Y��Z��[��[��\��_ñaɶdнh��i��j��k��l��l��m��m��n��p��p��q��q��r��s��t��t��u��u��v��v��w��w��x��x��y��y��z��z��{��{��|��|��}��}��~��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������!�
+���,���� � �����	H����*\Ȱ�Ç#*�d�'H�١ㆍ1\�@q�
6\����2g�t�re
+#C|�A#Ɗ#>p���B˂EȀ�%K&Lp��ѳ�
+%>�����AԤAC��2d�x�…
+(P����0Z��P"C}*T��A�
+�Hϟ:4i�hQ#F�)S^����D�B�džA"�SoV��"D�!�S#��ۚ1�64 ������	�J*'r�� @�G:�x��(�3��ق��0�:�2�,���"?���_��m���@�߀���u�@�y���qm�^|��w�}�]f�)�@�h`A=0"∌���0H�@�Gz�q�/��m%�� n`�|衇n|H�-B"#�E!�5�{�a�m�`��-�"�����H�G"��@9h��^|2��(�|䁇}�A�AL�W�`"��ƈ��u���A8�av
+��v�hGt�i�k�X�
f��a|��:Y\a�T<��I �Į�"A�A,�`��b�A�F��4���(��, ѵ�f����;
\ No newline at end of file
diff -Naur beta5//sql/13-main-forums.sql forums//sql/13-main-forums.sql
--- beta5//sql/13-main-forums.sql	2011-02-05 10:09:56.244335002 +0100
+++ forums//sql/13-main-forums.sql	1970-01-01 01:00:00.000000000 +0100
@@ -1,149 +0,0 @@
--- LegacyWorlds Beta 5
--- PostgreSQL database scripts
---
--- 13-main-forums.sql
---
--- Tables for the forums
---
--- Copyright(C) 2004-2007, DeepClone Development
--- --------------------------------------------------------
-
-
-
--- Connect to the database
-\c legacyworlds legacyworlds_admin
-
-
-
--- 
--- Forum categories
---
-CREATE TABLE main.f_category (
-	id		SERIAL		NOT NULL		PRIMARY KEY,
-	corder		INT		NOT NULL		UNIQUE CHECK(corder >= 0),
-	title		VARCHAR(64)	NOT NULL		UNIQUE,
-	description	TEXT
-);
-
-GRANT SELECT,INSERT ON TABLE main.f_category TO legacyworlds;
-GRANT SELECT,UPDATE ON main.f_category_id_seq TO legacyworlds;
-
-
---
--- Forums
---
-CREATE TABLE main.f_forum (
-	id		SERIAL		NOT NULL		PRIMARY KEY,
-	category	INT		NOT NULL		REFERENCES main.f_category (id) ON DELETE CASCADE,
-	forder		INT		NOT NULL DEFAULT 0	CHECK(forder >= 0),
-	title		VARCHAR(64)	NOT NULL,
-	description	TEXT,
-	topics		INT		NOT NULL		CHECK(topics >= 0),
-	posts		INT		NOT NULL		CHECK(posts >= 0),
-	last_post	BIGINT		NULL,
-	user_post	BOOLEAN		NOT NULL DEFAULT TRUE,
-	admin_only	BOOLEAN		NOT NULL DEFAULT FALSE
-);
-
-CREATE UNIQUE INDEX forum_unique ON main.f_forum (category, forder);
-CREATE INDEX forum_last_post ON main.f_forum (last_post);
-
-GRANT SELECT,UPDATE,INSERT ON TABLE main.f_forum TO legacyworlds;
-GRANT SELECT,UPDATE ON main.f_forum_id_seq TO legacyworlds;
-
-
---
--- Topics
---
-CREATE TABLE main.f_topic (
-	id		BIGSERIAL	NOT NULL		PRIMARY KEY,
-	forum		INT		NOT NULL		REFERENCES main.f_forum (id) ON DELETE CASCADE,
-	first_post	BIGINT		NOT NULL,
-	last_post	BIGINT		NULL,
-	sticky		BOOLEAN		NOT NULL DEFAULT FALSE,
-	deleted		INT		NULL
-);
-
-CREATE INDEX topic_forum ON main.f_topic (forum);
-CREATE INDEX topic_fpost ON main.f_topic (first_post);
-CREATE INDEX topic_lpost ON main.f_topic (last_post);
-
-GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE main.f_topic TO legacyworlds;
-GRANT SELECT,UPDATE ON main.f_topic_id_seq TO legacyworlds;
-
-
---
--- Posts
---
-CREATE TABLE main.f_post (
-	id		BIGSERIAL			PRIMARY KEY,
-	forum		INT		NOT NULL	REFERENCES main.f_forum (id) ON DELETE CASCADE,
-	topic		BIGINT		NULL		REFERENCES main.f_topic (id) ON DELETE CASCADE,
-	author		BIGINT		NOT NULL	REFERENCES main.account (id),
-	reply_to	BIGINT		NULL		REFERENCES main.f_post (id) ON DELETE SET NULL,
-	moment		INT		NOT NULL	DEFAULT INT4(EXTRACT(EPOCH FROM NOW())),
-	title		VARCHAR(100)	NOT NULL,
-	contents	TEXT		NOT NULL,
-	enable_code	BOOLEAN		NOT NULL	DEFAULT TRUE,
-	enable_smileys	BOOLEAN		NOT NULL	DEFAULT TRUE,
-	edited		INT		NULL,
-	edited_by	BIGINT		NULL		REFERENCES main.account (id),
-	deleted		INT		NULL
-);
-
-CREATE INDEX post_forum ON main.f_post (forum);
-CREATE INDEX post_topic ON main.f_post (topic);
-CREATE INDEX post_author ON main.f_post (author);
-CREATE INDEX post_editor ON main.f_post (edited_by);
-CREATE INDEX post_reply ON main.f_post (reply_to);
-
-ALTER TABLE main.f_forum ADD FOREIGN KEY (last_post) REFERENCES main.f_post (id) ON DELETE SET NULL;
-ALTER TABLE main.f_topic ADD FOREIGN KEY (first_post) REFERENCES main.f_post (id) ON DELETE CASCADE;
-ALTER TABLE main.f_topic ADD FOREIGN KEY (last_post) REFERENCES main.f_post (id) ON DELETE SET NULL;
-
-GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE main.f_post TO legacyworlds;
-GRANT SELECT,UPDATE ON main.f_post_id_seq TO legacyworlds;
-
-
--- 
--- Read topics
--- 
-CREATE TABLE main.f_read (
-	reader	BIGINT	NOT NULL	REFERENCES main.account (id),
-	topic	BIGINT	NOT NULL	REFERENCES main.f_topic (id) ON DELETE CASCADE,
-	PRIMARY KEY (reader, topic)
-);
-
-GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE main.f_read TO legacyworlds;
-
-
---
--- Smileys and forum codes
---
-CREATE TABLE main.f_smiley (
-	smiley	VARCHAR(20) NOT NULL PRIMARY KEY,
-	file	VARCHAR(20) NOT NULL
-);
-CREATE TABLE main.f_code (
-	p_reg_exp	VARCHAR(40) NOT NULL PRIMARY KEY,
-	replacement	VARCHAR(80) NOT NULL
-);
-GRANT SELECT ON main.f_smiley TO legacyworlds;
-GRANT SELECT ON main.f_code TO legacyworlds;
-
-
---
--- Admins, mods, losers
--- Not everything is useful in the current version so meh.
---
-CREATE TABLE main.f_admin (
-	"user"		BIGINT	NOT NULL	REFERENCES main.account (id) PRIMARY KEY,
-	category	INT	NULL		REFERENCES main.f_category (id) ON DELETE CASCADE
-);
-GRANT SELECT,INSERT,UPDATE,DELETE ON main.f_admin TO legacyworlds;
-
-CREATE TABLE main.f_moderator (
-	"user"		BIGINT	NOT NULL	REFERENCES main.account (id) PRIMARY KEY,
-	forum		INT	NULL		REFERENCES main.f_forum (id) ON DELETE CASCADE
-);
-GRANT SELECT,INSERT,UPDATE,DELETE ON main.f_moderator TO legacyworlds;
diff -Naur beta5//sql/14-main-forums.sql forums//sql/14-main-forums.sql
--- beta5//sql/14-main-forums.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/14-main-forums.sql	2011-02-05 10:10:01.774335002 +0100
@@ -0,0 +1,227 @@
+-- LegacyWorlds Beta 5
+-- PostgreSQL database scripts
+--
+-- 14-main-forums.sql
+--
+-- Execute the generic forums installation script,
+-- then add tables for the general forums
+--
+-- Copyright(C) 2004-2007, DeepClone Development
+-- --------------------------------------------------------
+
+
+
+-- Install the generic forums
+\i forums/FORUMS.sql
+
+
+--
+-- Create category type for general forums
+--
+SELECT forums.add_category_type( 'main/gforums', 'en', 'General forums' );
+-- SELECT forums.add_category_type( 'main/gforums', 'fr', 'Forums généraux ' );
+
+
+
+--
+-- The general forums' categories
+--
+CREATE TABLE main.gf_category (
+	category	BIGINT				PRIMARY KEY REFERENCES forums.category (id) ON DELETE CASCADE,
+	t_string	VARCHAR(15)	NULL,
+	t_is_game	BOOLEAN		NULL,
+	UNIQUE(t_string, t_is_game)
+);
+
+GRANT SELECT,INSERT,DELETE ON main.gf_category TO legacyworlds;
+
+
+
+--
+-- The general forums' access list
+--
+-- Access type:
+--  - MO: moderator- (and admin-) only access, not visible to most users
+--  - MP: moderator- (and admin-) only posting, standard users can't create new topics
+--  - UP: users can view the forum and create topics, but can't create polls; this is the default
+--  - UL: users can view the forum, create topics and create polls
+--
+CREATE TABLE main.gf_forum (
+	forum		BIGINT				PRIMARY KEY REFERENCES forums.t_forum (id) ON DELETE CASCADE,
+	access_type	CHAR(2)	NOT NULL DEFAULT 'UP'	CHECK( access_type IN ('MO', 'MP', 'UP', 'UL') )
+);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON main.gf_forum TO legacyworlds;
+
+-- Trigger function & definition for main.gf_forum
+CREATE OR REPLACE FUNCTION main.trgf_gf_forum_check () RETURNS TRIGGER AS $$
+BEGIN
+	PERFORM g.category FROM main.gf_category g, forums.forum f
+		WHERE f.id = NEW.forum AND g.category = f.category;
+	IF NOT FOUND THEN
+		RAISE EXCEPTION 'Forum #% is not a general forum', NEW.forum; 
+	END IF;
+	RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER trg_gf_forum_check BEFORE INSERT OR UPDATE ON main.gf_forum
+	FOR EACH ROW EXECUTE PROCEDURE main.trgf_gf_forum_check();
+
+
+--
+-- Bans on the general forums
+--
+CREATE TABLE main.gf_ban (
+	account		BIGINT				PRIMARY KEY REFERENCES main.account (id) ON DELETE CASCADE,
+	until		INT		NOT NULL
+);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON main.gf_ban TO legacyworlds;
+
+
+
+--
+-- General forums administrators
+--
+-- List of people who can create, remove or modify general forums.
+-- An admin is either a general admin (all GF categories) or the admin for a specific category
+--
+CREATE TABLE main.gf_admin (
+	account		BIGINT		NOT NULL	REFERENCES main.account (id) ON DELETE CASCADE,
+	category	BIGINT		NULL		REFERENCES main.gf_category (category) ON DELETE CASCADE,
+	UNIQUE( account, category )
+);
+
+GRANT SELECT,INSERT,DELETE ON main.gf_admin TO legacyworlds;
+
+-- Trigger function & definition that makes sure that you're either a general admin or a category-specific admin,
+-- not both.
+CREATE OR REPLACE FUNCTION main.trgf_gf_admin_check () RETURNS TRIGGER AS $$
+BEGIN
+	IF NEW.category IS NULL THEN
+		PERFORM * FROM main.gf_admin WHERE account = NEW.account AND category IS NOT NULL;
+		IF FOUND THEN
+			RAISE EXCEPTION 'User #% is already a cat-specific admin', NEW.account;
+		END IF;
+		DELETE FROM main.gf_cat_moderator WHERE account = NEW.account;
+		DELETE FROM main.gf_forum_moderator WHERE account = NEW.account;
+	ELSE
+		PERFORM * FROM main.gf_category WHERE category = NEW.category;
+		IF NOT FOUND THEN
+			RAISE EXCEPTION 'Category #% is not a general category', NEW.category;
+		END IF;
+		PERFORM * FROM main.gf_admin WHERE account = NEW.account AND category IS NULL;
+		IF FOUND THEN
+			RAISE EXCEPTION 'User #% is already a general admin', NEW.account;
+		END IF;
+		DELETE FROM main.gf_cat_moderator WHERE account = NEW.account AND category = NEW.category;
+		DELETE FROM main.gf_forum_moderator WHERE account = NEW.account AND forum IN (
+			SELECT id FROM forums.forum WHERE category = NEW.category
+		);
+	END IF;
+	RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER trg_gf_admin_check BEFORE INSERT ON main.gf_admin
+	FOR EACH ROW EXECUTE PROCEDURE main.trgf_gf_admin_check();
+
+
+
+
+--
+-- General forums moderators, by category
+--
+-- List of people who can moderate whole categories of the general forums.
+-- These people should not already be listed in the admin list for the category.
+--
+CREATE TABLE main.gf_cat_moderator (
+	account		BIGINT		NOT NULL	REFERENCES main.account (id) ON DELETE CASCADE,
+	category	BIGINT		NULL		REFERENCES main.gf_category (category) ON DELETE CASCADE,
+	UNIQUE( account, category )
+);
+
+GRANT SELECT,INSERT,DELETE ON main.gf_cat_moderator TO legacyworlds;
+
+-- Trigger function & definition that makes sure that you're either a general mod or a category-specific mod,
+-- and that you're not an admin.
+CREATE OR REPLACE FUNCTION main.trgf_gf_cmod_check () RETURNS TRIGGER AS $$
+BEGIN
+	IF NEW.category IS NULL THEN
+		PERFORM * FROM main.gf_cat_moderator WHERE account = NEW.account AND category IS NOT NULL;
+		IF FOUND THEN
+			RAISE EXCEPTION 'User #% is already a cat-specific moderator', NEW.account;
+		END IF;
+		PERFORM * FROM main.gf_admin  WHERE account = NEW.account AND category IS NULL;
+		IF FOUND THEN
+			RAISE EXCEPTION 'User #% is already a general administrator', NEW.account;
+		END IF;
+		DELETE FROM main.gf_forum_moderator WHERE account = NEW.account;
+	ELSE
+		PERFORM * FROM main.gf_category WHERE category = NEW.category;
+		IF NOT FOUND THEN
+			RAISE EXCEPTION 'Category #% is not a general category', NEW.category;
+		END IF;
+		PERFORM * FROM main.gf_cat_moderator WHERE account = NEW.account AND category IS NULL;
+		IF FOUND THEN
+			RAISE EXCEPTION 'User #% is already a general moderator', NEW.account;
+		END IF;
+		PERFORM * FROM main.gf_admin
+			WHERE account = NEW.account
+			  AND (category = NEW.category OR category IS NULL);
+		IF FOUND THEN
+			RAISE EXCEPTION 'User #% is already an administrator for category', NEW.account;
+		END IF;
+		DELETE FROM main.gf_forum_moderator WHERE account = NEW.account AND forum IN (
+			SELECT id FROM forums.forum WHERE category = NEW.category
+		);
+	END IF;
+	RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER trg_gf_cmod_check BEFORE INSERT ON main.gf_cat_moderator
+	FOR EACH ROW EXECUTE PROCEDURE main.trgf_gf_cmod_check();
+
+
+
+
+--
+-- General forums moderators, by forum
+--
+-- List of people who can moderate specific forums.
+--
+CREATE TABLE main.gf_forum_moderator (
+	account		BIGINT		NOT NULL	REFERENCES main.account (id) ON DELETE CASCADE,
+	forum		BIGINT		NOT NULL	REFERENCES forums.t_forum (id) ON DELETE CASCADE,
+	UNIQUE( account, forum )
+	-- FIXME: add constraint to make sure that if someone's a mod for the forum's category or an admin
+	-- for the category in question, that someone can't be added as a forum-specific moderator
+);
+
+GRANT SELECT,INSERT,DELETE ON main.gf_forum_moderator TO legacyworlds;
+
+-- Trigger function & definition that makes sure that you're not a forums' category mod or admin.
+CREATE OR REPLACE FUNCTION main.trgf_gf_fmod_check() RETURNS TRIGGER AS $$
+DECLARE
+	cid BIGINT;
+BEGIN
+	SELECT INTO cid g.category FROM main.gf_category g, forums.forum f
+		WHERE f.id = NEW.forum AND g.category = f.category;
+	IF NOT FOUND THEN
+		RAISE EXCEPTION 'Forum #% is not a general forum', NEW.forum; 
+	END IF;
+
+	PERFORM * FROM main.gf_cat_moderator WHERE account = NEW.account AND (category IS NULL OR category = cid);
+	IF FOUND THEN
+		RAISE EXCEPTION 'User #% is already a moderator for forum #%', NEW.account, NEW.forum;
+	END IF;
+
+	PERFORM * FROM main.gf_admin WHERE account = NEW.account AND (category IS NULL OR category = cid);
+	IF FOUND THEN
+		RAISE EXCEPTION 'User #% is already an administrator for forum #%', NEW.account, NEW.forum;
+	END IF;
+
+	RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER trg_gf_fmod_check BEFORE INSERT ON main.gf_forum_moderator
+	FOR EACH ROW EXECUTE PROCEDURE main.trgf_gf_fmod_check();
diff -Naur beta5//sql/15-main-gf-functions.sql forums//sql/15-main-gf-functions.sql
--- beta5//sql/15-main-gf-functions.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/15-main-gf-functions.sql	2011-02-05 10:10:01.774335002 +0100
@@ -0,0 +1,205 @@
+-- LegacyWorlds Beta 5
+-- PostgreSQL database scripts
+--
+-- 15-main-gf-functions.sql
+--
+-- Creates the general forums access functions
+--
+-- Copyright(C) 2004-2007, DeepClone Development
+-- --------------------------------------------------------
+
+
+
+--
+-- main.init_general_forums()
+--
+-- This function initialises the general forums by creating
+-- a category for the main GF category.
+
+CREATE OR REPLACE FUNCTION main.init_general_forums() RETURNS BIGINT AS $$
+DECLARE
+	cid BIGINT;
+BEGIN
+	SELECT INTO cid * FROM main.gf_category WHERE t_string IS NULL;
+	IF FOUND THEN
+		RETURN cid;
+	END IF;
+
+	SELECT INTO cid forums.make_category( 'main/gforums' );
+	INSERT INTO main.gf_category (category, t_string, t_is_game )
+		VALUES (cid, NULL, NULL);
+
+	RETURN cid;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- main.init_version_forums( version )
+--
+-- This function initialises the general forums category
+-- for a specific version of LW
+
+CREATE OR REPLACE FUNCTION main.init_version_forums( v TEXT ) RETURNS BIGINT AS $$
+DECLARE
+	cid BIGINT;
+BEGIN
+	SELECT INTO cid * FROM main.gf_category WHERE t_string = v AND NOT t_is_game;
+	IF FOUND THEN
+		RETURN cid;
+	END IF;
+
+	SELECT INTO cid forums.make_category( 'main/gforums' );
+	INSERT INTO main.gf_category (category, t_string, t_is_game )
+		VALUES (cid, v, FALSE);
+
+	RETURN cid;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- main.init_game_forums( game )
+--
+-- This function initialises the general forums category
+-- for a specific LW game
+
+CREATE OR REPLACE FUNCTION main.init_game_forums( g TEXT ) RETURNS BIGINT AS $$
+DECLARE
+	cid BIGINT;
+BEGIN
+	SELECT INTO cid * FROM main.gf_category WHERE t_string = g AND t_is_game;
+	IF FOUND THEN
+		RETURN cid;
+	END IF;
+
+	SELECT INTO cid forums.make_category( 'main/gforums' );
+	INSERT INTO main.gf_category (category, t_string, t_is_game )
+		VALUES (cid, g, TRUE);
+
+	RETURN cid;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- main.get_gf_categories( version , game )
+--
+-- Returns the list of all available general forum categories for a version and game
+
+CREATE OR REPLACE FUNCTION main.get_gf_categories( ver TEXT, game TEXT ) RETURNS SETOF BIGINT AS $$
+		SELECT category FROM main.gf_category
+			WHERE t_string IS NULL
+			   OR (t_string = $1 AND NOT t_is_game)
+			   OR (t_string = $2 AND t_is_game)
+		     ORDER BY category;
+$$ LANGUAGE SQL;
+
+
+--
+-- main.get_gf_list( version , game )
+--
+-- Returns the list of all available general forums for a version and game
+
+CREATE OR REPLACE FUNCTION main.get_gf_list( ver TEXT, game TEXT ) RETURNS SETOF BIGINT AS $$
+	SELECT id FROM forums.t_forum
+		WHERE category IN ( SELECT * FROM main.get_gf_categories( $1, $2 ) )
+	     ORDER BY category, f_order;
+$$ LANGUAGE SQL;
+
+
+--
+-- main.get_gforums_privs( user , forum )
+--
+-- Returns the set of privileges an user has over a specific general forum
+
+CREATE OR REPLACE FUNCTION main.get_gforums_privs( aid BIGINT, fid BIGINT, OUT can_view BOOLEAN, OUT can_post BOOLEAN, OUT can_create BOOLEAN, OUT can_poll BOOLEAN, OUT is_mod BOOLEAN, OUT is_admin BOOLEAN) AS $$
+DECLARE
+	cid BIGINT;
+	r TEXT;
+BEGIN
+	-- Get the category's ID
+	SELECT INTO cid category FROM forums.t_forum WHERE id = fid;
+	IF NOT FOUND THEN
+		is_admin := FALSE;
+		is_mod := FALSE;
+		can_view := FALSE;
+		can_post := FALSE;
+		can_create := FALSE;
+		can_poll := FALSE;
+		RETURN;
+	END IF;
+
+	-- Check if this is a general category
+	PERFORM * FROM main.gf_category WHERE category = cid;
+	IF NOT FOUND THEN
+		RETURN;
+	END IF;
+
+	-- Fetch category admin details
+	PERFORM * FROM main.gf_admin WHERE (category IS NULL OR category = cid) AND account = aid;
+	IF FOUND THEN
+		can_view := TRUE;
+		can_post := TRUE;
+		can_create := TRUE;
+		can_poll := TRUE;
+		is_mod := TRUE;
+		is_admin := TRUE;
+		RETURN;
+	END IF;
+	is_admin := FALSE;
+
+	-- Fetch category mod details
+	PERFORM * FROM main.gf_cat_moderator WHERE (category IS NULL OR category = cid) AND account = aid;
+	IF FOUND THEN
+		can_view := TRUE;
+		can_post := TRUE;
+		can_create := TRUE;
+		can_poll := TRUE;
+		is_mod := TRUE;
+		RETURN;
+	END IF;
+
+	-- Fetch forum mod details
+	PERFORM * FROM main.gf_forum_moderator WHERE forum = fid AND account = aid;
+	IF FOUND THEN
+		can_view := TRUE;
+		can_post := TRUE;
+		can_create := TRUE;
+		can_poll := TRUE;
+		is_mod := TRUE;
+		RETURN;
+	END IF;
+	is_mod := false;
+
+	-- Check forum access
+	SELECT INTO r access_type FROM main.gf_forum WHERE forum = fid;
+	IF NOT FOUND THEN
+		r := 'UP';
+	END IF;
+
+	-- Check for mod-only forum
+	IF r = 'MO' THEN
+		can_view := FALSE;
+		can_post := FALSE;
+		can_create := FALSE;
+		can_poll := FALSE;
+		RETURN;
+	END IF;
+	can_view := TRUE;
+
+	-- Check for bans
+	PERFORM * FROM main.gf_ban WHERE account = aid;
+	IF FOUND THEN
+		can_post := FALSE;
+		can_create := FALSE;
+		can_poll := FALSE;
+		RETURN;
+	END IF;
+
+	-- Set can_post, can_create and can_poll depending on the access type
+	can_post := TRUE;
+	can_create := (r = 'UP' OR r = 'UL');
+	can_poll := (r = 'UL');
+END;
+$$ LANGUAGE plpgsql;
diff -Naur beta5//sql/15-main-uforums.sql forums//sql/15-main-uforums.sql
--- beta5//sql/15-main-uforums.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/15-main-uforums.sql	2011-02-05 10:10:01.774335002 +0100
@@ -0,0 +1,332 @@
+-- LegacyWorlds Beta 5
+-- PostgreSQL database scripts
+--
+-- 15-main-uforums.sql
+--
+-- Add tables and functions to manage the user forums
+--
+-- Copyright(C) 2004-2007, DeepClone Development
+-- --------------------------------------------------------
+
+
+--
+-- Access types for forums:
+--  - 'P': public
+--  - 'W': password
+--  - 'I': invite-only
+--
+-- User access flag:
+--  - 'M': moderator
+--  - 'L': can start topics, post and create polls
+--  - 'T': can start topics and post
+--  - 'P': can post
+--  - 'R': can read
+
+
+--
+-- Create category type
+--
+SELECT forums.add_category_type( 'main/uforums', 'en', 'User forums' );
+
+
+--
+-- main.uf_get_access_mode( forum )
+--
+-- Returns the access mode for a specific user forum
+
+CREATE OR REPLACE FUNCTION main.uf_get_access_mode( fid BIGINT ) RETURNS TEXT AS $$
+DECLARE
+	cid BIGINT;
+	am TEXT;
+BEGIN
+	-- Check if the forum exists and is an user forum; if it is, get the access mode
+	SELECT INTO am access_mode FROM main.user_forum WHERE forum = fid;
+	IF NOT FOUND THEN
+		RETURN NULL;
+	END IF;
+
+	IF am IS NULL THEN
+		-- Forum uses defaults, get its category
+		SELECT INTO cid category FROM forums.t_forum WHERE id = fid;
+		SELECT INTO am default_access FROM main.user_category;
+	END IF;
+
+	RETURN am;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- main.uf_get_user_access( user, forum )
+--
+-- Returns the user access mode for a specific user and forum combination
+
+CREATE OR REPLACE FUNCTION main.uf_get_user_access( aid BIGINT, fid BIGINT ) RETURNS TEXT AS $$
+DECLARE
+	fua TEXT;
+	r RECORD;
+BEGIN
+	-- Read the category's data
+	SELECT INTO r c.account AS fo, c.user_access AS am FROM main.user_category c, forums.t_forum f
+		WHERE c.category = f.category AND f.id = fid;
+
+	-- Try reading the subscription value
+	SELECT INTO fua access_mode FROM main.uf_subscription WHERE account = aid;
+	IF NOT FOUND THEN
+		-- No subscription - check if we're the owner
+		IF r.fo = aid THEN
+			RETURN 'A';
+		END IF;
+		RETURN NULL;
+	END IF;
+
+	-- We got a subscription value
+	IF fua IS NOT NULL THEN
+		RETURN fua;
+	END IF;
+
+	-- Check if the forum exists and get its default user access
+	SELECT INTO fua user_access FROM main.user_forum WHERE forum = fid;
+	IF NOT FOUND THEN
+		RETURN NULL;
+	END IF;
+
+	-- If we had a default user access mode, return it
+	IF fua IS NOT NULL THEN
+		RETURN fua;
+	END IF;
+
+	-- Return the category's default access mode
+	RETURN r.am;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- main.uf_get_category( user )
+--
+-- Gets the ID of the user's forums category. If it doesn't exist, create it.
+
+CREATE OR REPLACE FUNCTION main.uf_get_category( aid BIGINT ) RETURNS BIGINT AS $$
+DECLARE
+	cid BIGINT;
+BEGIN
+	SELECT INTO cid category FROM main.user_category WHERE account = aid;
+	IF NOT FOUND THEN
+		cid := forums.make_category( 'main/uforums' );
+		INSERT INTO main.user_category (category, account) VALUES (cid, aid);
+	END IF;
+	RETURN cid;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- main.uf_create_forum( user, forum_order, forum_title, forum_description, user_access, access_mode, password )
+--
+-- Creates an user forum.
+-- Return values:
+--	any positive value: identifier of the new forum
+--	-1: the user has 20 forums already
+--	-2: invalid access modes
+--	-3: a forum with the same name already exists
+--	-4: failure in the generic forums code
+
+CREATE OR REPLACE FUNCTION main.uf_create_forum( aid BIGINT, fo INT, ttl TEXT, dsc TEXT, ua TEXT, am TEXT, pass TEXT) RETURNS BIGINT AS $$
+DECLARE
+	cid BIGINT;
+	fid BIGINT;
+	n INT;
+	cam TEXT;
+BEGIN
+	-- Get the user's category and check if it's possible to create the user's new forum
+	cid := main.uf_get_category( aid );
+	SELECT INTO n COUNT(*) FROM forums.t_forum WHERE category = cid AND deleted IS NULL;
+	IF n = 20 THEN
+		RETURN -1;
+	END IF;
+
+	-- Check the access mode
+	SELECT INTO cam default_access FROM main.user_category WHERE category = cid;
+	IF (am = 'W' AND pass IS NULL) OR (am IS NULL AND cam = 'W' AND pass IS NULL) THEN
+		RETURN -2;
+	END IF;
+
+	-- Check the forum's title
+	PERFORM * FROM forums.t_forum WHERE category = cid AND title = ttl;
+	IF FOUND THEN
+		RETURN -3;
+	END IF;
+
+	-- Create the forum
+	fid := forums.make_forum( cid, fo, ttl, dsc );
+	IF fid IS NULL THEN
+		RETURN -4;
+	END IF;
+	IF (am = 'W' OR (am IS NULL AND cam = 'W')) THEN
+		INSERT INTO main.user_forum (forum, access_mode, password, user_access) VALUES ( fid, am, pass, ua );
+	ELSE
+		INSERT INTO main.user_forum (forum, access_mode, user_access) VALUES ( fid, am, ua );
+	END IF;
+
+	RETURN fid;
+END;
+$$ LANGUAGE plpgsql;
+
+
+
+
+--
+-- Category / user mapping
+--
+-- If the account gets disabled (QUIT, INAC), we need to mark the forums for deletion.
+-- If the account gets re-enabled (STD), we need to restore the forums.
+-- If the account gets kicked, we need to delete the forums immediately.
+
+CREATE TABLE main.user_category (
+	account		BIGINT				PRIMARY KEY REFERENCES main.account (id) ON DELETE CASCADE,
+	category	BIGINT	NOT NULL		UNIQUE REFERENCES forums.category (id) ON DELETE CASCADE,
+	default_access	CHAR(1)	NOT NULL DEFAULT 'I'	CHECK( default_access IN ('P', 'I') ),
+	user_access	CHAR(1)	NOT NULL DEFAULT 'T'	CHECK( user_access IN ('M', 'L', 'T', 'P', 'R') )
+);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON main.user_category TO legacyworlds;
+
+-- Trigger function & definition for the account table that allows handling the users' categories.
+CREATE OR REPLACE FUNCTION main.trgf_account_user_forums() RETURNS TRIGGER AS $$
+DECLARE
+	cid BIGINT;
+BEGIN
+	-- No status change, proceed
+	IF NEW.status = OLD.status THEN
+		RETURN NEW;
+	END IF;
+
+	-- Get the user's category
+	SELECT INTO cid category FROM main.user_category WHERE account = NEW.id;
+	IF NOT FOUND THEN
+		RETURN NEW;
+	END IF;
+
+	-- Check the new status
+	IF NEW.status = 'INAC' OR NEW.status = 'QUIT' THEN
+		PERFORM forums.delete_forums( cid, NEW.id );
+	ELSIF NEW.status = 'STD' THEN
+		PERFORM forums.restore_forums( cid );
+	ELSIF NEW.status = 'KICKED' THEN
+		DELETE FROM forums.category WHERE id = cid;
+	END IF;
+
+	RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER trg_account_user_forums BEFORE UPDATE ON main.account
+	FOR EACH ROW EXECUTE PROCEDURE main.trgf_account_user_forums();
+
+
+
+--
+-- User forums
+--
+
+CREATE TABLE main.user_forum (
+	forum		BIGINT		PRIMARY KEY		REFERENCES forums.t_forum (id) ON DELETE CASCADE,
+	access_mode	CHAR(1)		DEFAULT NULL		CHECK( access_mode IS NULL
+									OR access_mode IN ('P', 'W', 'I') ),
+	password	VARCHAR(64)	DEFAULT NULL,
+	user_access	CHAR(1)		DEFAULT NULL		CHECK( user_access IS NULL
+									OR user_access IN ('M', 'L', 'T', 'P', 'R') ),
+	CHECK( (main.uf_get_access_mode(forum) = 'W' AND password IS NOT NULL)
+		OR (main.uf_get_access_mode(forum) <> 'W' AND password IS NULL) )
+);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON main.user_forum TO legacyworlds;
+
+
+--
+-- Subscriptions
+--
+-- An user can't subscribe to one of his own forums.
+--
+
+CREATE TABLE main.uf_subscription (
+	forum		BIGINT		NOT NULL		REFERENCES main.user_forum (forum) ON DELETE CASCADE,
+	account		BIGINT		NOT NULL		REFERENCES main.account (id) ON DELETE CASCADE,
+	access_mode	CHAR(1)		DEFAULT NULL		CHECK( access_mode IS NULL
+									OR access_mode IN ('M', 'L', 'T', 'P', 'R') ),
+	PRIMARY KEY( forum, account )
+);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON main.uf_subscription TO legacyworlds;
+
+-- Trigger function & definition
+CREATE OR REPLACE FUNCTION main.trgf_uf_subscription_check() RETURNS TRIGGER AS $$
+DECLARE
+	fo BIGINT;
+BEGIN
+	IF TG_OP = 'INSERT' THEN
+		SELECT INTO fo account FROM main.user_category c, forums.t_forum f
+			WHERE c.category = f.category AND f.id = NEW.forum;
+		IF FOUND AND fo = NEW.account THEN
+			RETURN NULL;
+		END IF;
+	ELSIF TG_OP = 'UPDATE' THEN
+		NEW.forum = OLD.forum;
+		NEW.account = OLD.account;
+	END IF;
+	RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER trg_uf_subscription_check BEFORE INSERT OR UPDATE ON main.uf_subscription
+	FOR EACH ROW EXECUTE PROCEDURE main.trgf_uf_subscription_check();
+
+
+--
+-- Invites
+--
+-- An user can only be invited to an invite-only user forum.
+-- An user can be invited if he's not already a subscriber to the forum.
+-- An user can't be invited to one of his own forums.
+--
+
+CREATE TABLE main.uf_invite (
+	forum		BIGINT		NOT NULL		REFERENCES main.user_forum (forum) ON DELETE CASCADE,
+	account		BIGINT		NOT NULL		REFERENCES main.account (id) ON DELETE CASCADE,
+	sent_on		INT		NOT NULL DEFAULT UNIX_TIMESTAMP( NOW() ),
+	PRIMARY KEY( forum, account )
+);
+
+GRANT SELECT,INSERT,DELETE ON main.uf_invite TO legacyworlds;
+
+-- Trigger function & definition
+CREATE OR REPLACE FUNCTION main.trgf_uf_invite_check() RETURNS TRIGGER AS $$
+DECLARE
+	m TEXT;
+	fo BIGINT;
+BEGIN
+	-- Check the forum's access mode
+	m := main.uf_get_access_mode( NEW.forum );
+	IF m IS NULL OR m <> 'I' THEN
+		RETURN NULL;
+	END IF;
+	-- Check for a subscription by the user
+	PERFORM * FROM main.uf_subscription WHERE forum = NEW.forum AND account = NEW.account;
+	IF FOUND THEN
+		RETURN NULL;
+	END IF;
+	-- Check for an existing invite for the user
+	PERFORM * FROM main.uf_invite WHERE forum = NEW.forum AND account = NEW.account;
+	IF FOUND THEN
+		RETURN NULL;
+	END IF;
+	-- Check if the user is actually the forum's owner
+	SELECT INTO fo account FROM main.user_category c, forums.t_forum f
+		WHERE c.category = f.category AND f.id = NEW.forum;
+	IF FOUND AND fo = NEW.account THEN
+		RETURN NULL;
+	END IF;
+	RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER trg_uf_invite_check BEFORE INSERT ON main.uf_invite
+	FOR EACH ROW EXECUTE PROCEDURE main.trgf_uf_invite_check();
diff -Naur beta5//sql/18-main-functions.sql forums//sql/18-main-functions.sql
--- beta5//sql/18-main-functions.sql	2011-02-05 10:09:56.244335002 +0100
+++ forums//sql/18-main-functions.sql	2011-02-05 10:10:01.774335002 +0100
@@ -27,8 +27,13 @@
 --
 -- Function that registers a game
 --
-CREATE OR REPLACE FUNCTION main.register_game (version TEXT, game_name TEXT) RETURNS VOID AS $$
+CREATE OR REPLACE FUNCTION main.register_game (ver TEXT, gn TEXT) RETURNS VOID AS $$
+BEGIN
 	INSERT INTO main.ranking_game (ranking, game)
-		SELECT id, $2 FROM main.ranking_def
-			WHERE version = $1;
-$$ LANGUAGE SQL;
+		SELECT id, gn FROM main.ranking_def
+			WHERE version = ver;
+	PERFORM main.init_general_forums();
+	PERFORM main.init_version_forums( ver );
+	PERFORM main.init_game_forums( gn );
+END;
+$$ LANGUAGE plpgsql;
diff -Naur beta5//sql/19-main-values.sql forums//sql/19-main-values.sql
--- beta5//sql/19-main-values.sql	2011-02-05 10:09:56.244335002 +0100
+++ forums//sql/19-main-values.sql	2011-03-12 15:05:03.511300054 +0100
@@ -77,49 +77,6 @@
 \.
 
 
-COPY main.f_smiley FROM STDIN;
-:-?\\)	smile
-[:;]-?p	razz
-:-?D	lol
-[:;]-&gt;	biggrin
-;-?\\)	wink
-;-?D	mrgreen
-:-?\\(	sad
-:evil:	evil
-:smile:	smile
-:happy:	smile
-:wink:	wink
-:sad:	sad
-:unhappy:	sad
-:'\\(	cry
-:cry:	cry
-:crying:	cry
-:grin:	biggrin
-:lol:	lol
-:tongue:	razz
-:rofl:	mrgreen
-[:;]-?\\|	neutral
-:neutral:	neutral
-\.
-
-
-COPY main.f_code FROM STDIN;
-\\[b\\](.*?)\\[\\/b\\]	<b>$1</b>
-\\[u\\](.*?)\\[\\/u\\]	<u>$1</u>
-\\[i\\](.*?)\\[\\/i\\]	<i>$1</i>
-\\[sep(arator)?\\]	<hr/>
-\\[item\\](.*?)\\[\\/item\\]	<ul><li>$1</li></ul>
-\\[quote\\](.*?)\\[\\/quote\\]	<blockquote class="quote">$1</blockquote>
-\\[quote=([^\\]]+)\\](.*?)\\[\\/quote\\]	<blockquote class="quote"><b>$1</b> said:<br/>$2</blockquote>
-\\[link=(http[^\\]]+)\\](.+?)\\[\\/link\\]	<a href="$1" target="_blank">$2</a>
-\\[code\\](.*?)\\[\\/code\\]	<pre>$1</pre>
-<\\/li><\\/ul>\\s*(<br\\/>\\s*)*<ul><li>	</li><li>
-\\[manual\\](.*?)\\[\\/manual\\]	<a href="manual" target="_blank">$1</a>
-\\[manual=(\\w+)(#\\w+)?\\](.*?)\\[\\/manual\\]	<a href="manual?p=$1$2" target="_blank">$3</a>
-\\[topic=(\\d+)\\](.*?)\\[\\/topic\\]	<a href="forums?cmd=T%23G%23$1" target="_blank">$2</a>
-\.
-
-
 -- Connect to the database in USER mode
 \c legacyworlds legacyworlds
 
diff -Naur beta5//sql/beta5/00-beta5.sql forums//sql/beta5/00-beta5.sql
--- beta5//sql/beta5/00-beta5.sql	2011-02-05 10:09:56.184335002 +0100
+++ forums//sql/beta5/00-beta5.sql	2011-02-05 10:10:01.744335002 +0100
@@ -44,3 +44,6 @@
 	'for a long term estimate of the bets players'' accomplishments.');
 SELECT add_ranking_description('beta5', 'p_idr', 'en', 'Inflicted Damage Ranking',
 	'This ranking represents the amount of damage a player has inflicted on other players'' fleets.');
+
+
+SELECT forums.add_category_type( 'beta5/aforums', 'en', 'Alliance forums' );
diff -Naur beta5//sql/beta5/structure/01-alliance.sql forums//sql/beta5/structure/01-alliance.sql
--- beta5//sql/beta5/structure/01-alliance.sql	2011-02-05 10:09:56.174335002 +0100
+++ forums//sql/beta5/structure/01-alliance.sql	2011-03-12 14:59:24.651300052 +0100
@@ -20,12 +20,15 @@
 	successor	BIGINT				REFERENCES player (id),
 	democracy	BOOLEAN		NOT NULL	DEFAULT FALSE,
 	default_grade	BIGINT		NOT NULL,
+	f_category	BIGINT		NOT NULL DEFAULT forums.make_category('beta5/forums')
+							REFERENCES forums.category (id),
 	enable_tt	CHAR(1)		NOT NULL	DEFAULT 'N' CHECK(enable_tt IN ('N', 'S', 'R'))
 );
 
 CREATE INDEX alliance_leader ON alliance (leader);
 CREATE INDEX alliance_successor ON alliance (successor);
 CREATE INDEX alliance_def_grade ON alliance (default_grade);
+CREATE INDEX alliance_f_category ON alliance (f_category);
 
 GRANT SELECT,INSERT,UPDATE,DELETE ON alliance TO legacyworlds;
 GRANT SELECT,UPDATE ON alliance_id_seq TO legacyworlds;
diff -Naur beta5//sql/beta5/structure/02-alliance-forums.sql forums//sql/beta5/structure/02-alliance-forums.sql
--- beta5//sql/beta5/structure/02-alliance-forums.sql	2011-02-05 10:09:56.174335002 +0100
+++ forums//sql/beta5/structure/02-alliance-forums.sql	2011-03-12 15:02:40.271300051 +0100
@@ -10,91 +10,268 @@
 -- --------------------------------------------------------
 
 
-CREATE TABLE af_forum (
-	id		SERIAL					PRIMARY KEY,
-	alliance	INT		NOT NULL		REFERENCES alliance(id) ON DELETE CASCADE,
-	forder		INT		NOT NULL		CHECK(forder >= 0),
-	title		VARCHAR(64)	NOT NULL,
-	description	TEXT,
-	topics		INT		NOT NULL DEFAULT 0	CHECK(topics >= 0),
-	posts		INT		NOT NULL DEFAULT 0	CHECK(posts >= 0),
-	last_post	BIGINT,
-	user_post	BOOLEAN		NOT NULL DEFAULT TRUE,
-	UNIQUE(alliance, forder),
-	UNIQUE(alliance, title)
-);
-
-CREATE INDEX af_forum_last_post ON af_forum (last_post);
-
-GRANT SELECT,INSERT,UPDATE,DELETE ON af_forum TO legacyworlds;
-GRANT SELECT,UPDATE ON af_forum_id_seq TO legacyworlds;
-
-
-
-CREATE TABLE af_topic (
-	id		BIGSERIAL	NOT NULL	PRIMARY KEY,
-	forum		INT		NOT NULL	REFERENCES af_forum (id) ON DELETE CASCADE,
-	first_post	BIGINT		NOT NULL,
-	last_post	BIGINT,
-	sticky		BOOLEAN		NOT NULL	DEFAULT FALSE
-);
+--
+-- Access types for alliance forums
+--
+-- - M: only mods can post, create topics and polls
+-- - P: only mods can create topics and polls, users can post
+-- - T: only mods can create polls, users can create topics and post
+-- - L: users can create polls or topics and post
+--
 
-CREATE INDEX af_topic_forum ON af_topic (forum);
-CREATE INDEX af_topic_first_post ON af_topic (first_post);
-CREATE INDEX af_topic_last_post ON af_topic (last_post);
-
-GRANT SELECT,INSERT,UPDATE,DELETE ON af_topic TO legacyworlds;
-GRANT SELECT,UPDATE ON af_topic_id_seq TO legacyworlds;
 
 
+--
+-- Alliance forums
+--
 
-CREATE TABLE af_post (
-	id		BIGSERIAL	NOT NULL	PRIMARY KEY,
-	forum		INT		NOT NULL	REFERENCES af_forum (id) ON DELETE CASCADE,
-	topic		BIGINT				REFERENCES af_topic (id) ON DELETE CASCADE,
-	author		BIGINT		NOT NULL	REFERENCES player (id),
-	reply_to	BIGINT				REFERENCES af_post (id) ON DELETE NO ACTION,
-	moment		INT		NOT NULL	DEFAULT INT4(EXTRACT(EPOCH FROM NOW())),
-	title		VARCHAR(100)	NOT NULL,
-	contents	TEXT		NOT NULL,
-	enable_code	BOOLEAN		NOT NULL	DEFAULT TRUE,
-	enable_smileys	BOOLEAN		NOT NULL	DEFAULT TRUE,
-	edited		INT,
-	edited_by	BIGINT				REFERENCES player(id)
+CREATE TABLE alliance_forum (
+	forum		BIGINT		PRIMARY KEY		REFERENCES forums.t_forum (id) ON DELETE CASCADE,
+	alliance	INT		NOT NULL		REFERENCES alliance(id) ON DELETE CASCADE,
+	access_mode	CHAR(1)		NOT NULL DEFAULT('T')	CHECK( access_mode IN ('M', 'P', 'T', 'L') ),
+	UNIQUE( alliance, forum )
 );
 
-CREATE INDEX af_post_forum ON af_post (forum);
-CREATE INDEX af_post_topic ON af_post (topic);
-CREATE INDEX af_post_author ON af_post (author);
-CREATE INDEX af_post_reply_to ON af_post (reply_to);
-CREATE INDEX af_post_edited_by ON af_post (edited_by);
-
-ALTER TABLE af_forum ADD FOREIGN KEY (last_post) REFERENCES af_post (id) ON DELETE SET NULL;
-ALTER TABLE af_topic ADD FOREIGN KEY (first_post) REFERENCES af_post (id) ON DELETE CASCADE;
-ALTER TABLE af_topic ADD FOREIGN KEY (last_post) REFERENCES af_post (id) ON DELETE SET NULL;
+GRANT SELECT, INSERT, UPDATE ON alliance_forum TO legacyworlds;
 
-GRANT SELECT,INSERT,UPDATE,DELETE ON af_post TO legacyworlds;
-GRANT SELECT,UPDATE ON af_post_id_seq TO legacyworlds;
 
 
+--
+-- Alliance ranks / forum access
+--
 
-CREATE TABLE af_read (
-	reader		BIGINT		NOT NULL	REFERENCES player (id),
-	topic		BIGINT		NOT NULL	REFERENCES af_topic (id) ON DELETE CASCADE,
-	PRIMARY KEY (reader, topic)
+CREATE TABLE al_rank_forum (
+	rank		BIGINT		NOT NULL	REFERENCES alliance_grade (id) ON DELETE CASCADE,
+	forum		INT		NOT NULL	REFERENCES alliance_forum (forum) ON DELETE CASCADE,
+	is_mod		BOOLEAN		NOT NULL	DEFAULT FALSE,
+	PRIMARY KEY ( rank, forum )
 );
 
-CREATE INDEX af_read_topic ON af_read (topic);
-GRANT SELECT,INSERT,DELETE ON af_read TO legacyworlds;
-
+CREATE INDEX al_rank_forum_forum ON al_rank_forum (forum);
+GRANT SELECT,INSERT,DELETE,UPDATE ON al_rank_forum TO legacyworlds;
 
 
-CREATE TABLE algr_forums (
-	grade		BIGINT		NOT NULL	REFERENCES alliance_grade (id) ON DELETE CASCADE,
-	forum		INT		NOT NULL	REFERENCES af_forum (id) ON DELETE CASCADE,
-	is_mod		BOOLEAN		NOT NULL	DEFAULT FALSE,
-	PRIMARY KEY (grade, forum)
-);
-
-CREATE INDEX algr_forums_forum ON algr_forums (forum);
-GRANT SELECT,INSERT,DELETE,UPDATE ON algr_forums TO legacyworlds;
+--
+-- create_alliance_forum( player, alliance, order, title, description, access_mode )
+--
+-- Creates a new forum in an alliance. Return values:
+--	any positive value: identifier of the new forum
+--	-1: alliance not found
+--	-2: the player doesn't have the necessary privileges to create the forum
+--	-3: the alliance already has 30 forums
+--	-4: a forum with the same title already exists
+--	-5: invalid access mode
+--	-6: failure to create the generic forum
+
+CREATE OR REPLACE FUNCTION create_alliance_forum( pid BIGINT, aid BIGINT, fo INT, ttl TEXT, dsc TEXT, am TEXT) RETURNS BIGINT AS $$
+DECLARE
+	arec RECORD;
+	prec RECORD;
+	rid BIGINT;
+	rrec RECORD;
+	nf BIGINT;
+BEGIN
+	-- Check the access mode
+	IF am NOT IN ('M','P','T','L') THEN
+		RETURN -5;
+	END IF;
+
+	-- Get the alliance's record
+	SELECT INTO arec default_grade, f_category, leader FROM alliance WHERE id = aid;
+	IF NOT FOUND THEN
+		RETURN -1;
+	END IF;
+
+	-- Get the player's record
+	SELECT INTO prec alliance, a_grade FROM player WHERE id = pid AND (quit IS NULL OR quit > UNIX_TIMESTAMP(NOW()))
+		AND alliance = aid AND a_status = 'IN ';
+	IF NOT FOUND THEN
+		RETURN -2;
+	END IF;
+
+	-- Get the player's rank
+	rid := CASE prec.a_grade IS NULL
+		WHEN TRUE THEN arec.default_grade
+		ELSE prec.a_grade
+	END;
+	SELECT INTO rrec forum_admin FROM alliance_grade WHERE id = rid;
+	IF NOT FOUND THEN
+		RETURN -2;
+	END IF;
+
+	-- Check if the player has access
+	IF NOT (pid = arec.leader OR rrec.forum_admin) THEN
+		RETURN -2;
+	END IF;
+
+	-- Check the amount of forums the alliance has
+	SELECT INTO nf COUNT(*) FROM alliance_forum WHERE alliance = aid;
+	IF nf >= 30 THEN
+		RETURN -3;
+	END IF;
+
+	-- Check the forum's title
+	PERFORM * FROM forums.t_forum WHERE category = arec.f_category AND title = ttl;
+	IF FOUND THEN
+		RETURN -4;
+	END IF;
+
+	-- Create the forum
+	nf := forums.make_forum( arec.f_category, fo, ttl, dsc );
+	IF nf IS NULL THEN
+		RETURN -6;
+	END IF;
+	INSERT INTO alliance_forum (forum, alliance, access_mode) VALUES (nf, aid, am);
+	RETURN nf;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- modify_alliance_forum( player, forum, title, description, access_mode )
+--
+-- This function tries to modify an existing alliance forum. Return values:
+-- 	 0	success
+--	-1	invalid access mode
+--	-2	the player is not in the alliance
+--	-3	the player is not a forum administrator for the alliance
+--	-4	forum not found
+
+CREATE OR REPLACE FUNCTION modify_alliance_forum( pid BIGINT, fid BIGINT, ttl TEXT, dsc TEXT, am TEXT ) RETURNS INT AS $$
+DECLARE
+	prec RECORD;
+	arec RECORD;
+	rid BIGINT;
+BEGIN
+	-- Check the access mode
+	IF am NOT IN ('M','P','T','L') THEN
+		RETURN -1;
+	END IF;
+
+	-- Get the player's alliance and rank
+	SELECT INTO prec alliance, a_grade FROM player WHERE id = pid AND (quit IS NULL OR quit > UNIX_TIMESTAMP(NOW()))
+		AND alliance IS NOT NULL AND a_status = 'IN ';
+	IF NOT FOUND THEN
+		RETURN -2;
+	END IF;
+
+	-- Check the player's privileges
+	SELECT INTO arec default_grade, leader FROM alliance WHERE id = prec.alliance;
+	IF (arec.leader <> pid) THEN
+		rid := CASE prec.a_grade IS NULL
+			WHEN TRUE THEN arec.default_grade
+			ELSE prec.a_grade
+		END;
+		PERFORM id FROM alliance_grade WHERE id = rid AND forum_admin;
+		IF NOT FOUND THEN
+			RETURN -3;
+		END IF;
+	END IF;
+
+	-- Make sure the forum exists and hasn't been deleted
+	PERFORM f.id,af.alliance FROM forums.t_forum f, alliance_forum af
+		WHERE af.alliance = prec.alliance AND f.id = af.forum AND f.deleted IS NULL;
+	IF NOT FOUND THEN
+		RETURN -4;
+	END IF;
+
+	-- Update both the forum and the access table
+	UPDATE forums.t_forum SET title = ttl, description = dsc WHERE id = fid;
+	UPDATE alliance_forum SET access_mode = am WHERE forum = fid;
+	RETURN 0;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- get_aforums_privs( player, forum )
+--
+-- Returns the set of privileges a player has over a specific alliance forum
+
+CREATE OR REPLACE FUNCTION get_aforums_privs( pid BIGINT, fid BIGINT, OUT can_view BOOLEAN, OUT can_post BOOLEAN, OUT can_create BOOLEAN, OUT can_poll BOOLEAN, OUT is_mod BOOLEAN, OUT is_admin BOOLEAN) AS $$
+DECLARE
+	cid BIGINT;
+	rid BIGINT;
+	arec RECORD;
+	b BOOLEAN;
+BEGIN
+	-- Initialise all privileges to FALSE
+	is_admin := FALSE;
+	is_mod := FALSE;
+	can_view := FALSE;
+	can_post := FALSE;
+	can_create := FALSE;
+	can_poll := FALSE;
+
+	-- Checks whether it's an alliance forum and get the associated data
+	SELECT INTO arec a.id, a.leader, a.default_grade, af.access_mode
+		 FROM alliance a, alliance_forum af
+		WHERE af.forum = fid AND a.id = af.alliance;
+	IF NOT FOUND THEN
+		RETURN;
+	END IF;
+
+	-- If the player is the leader, set all privileges to TRUE and return
+	IF arec.leader = pid THEN
+		is_admin := TRUE;
+		is_mod := TRUE;
+		can_view := TRUE;
+		can_post := TRUE;
+		can_create := TRUE;
+		can_poll := TRUE;
+		RETURN;
+	END IF;
+
+	-- Checks whether the player is an actual member of the alliance and get his rank
+	SELECT INTO rid a_grade FROM player
+		WHERE id = pid AND (quit IS NULL OR quit > UNIX_TIMESTAMP(NOW()))
+		  AND alliance = arec.id AND a_status = 'IN ';
+	IF NOT FOUND THEN
+		RETURN;
+	ELSIF rid IS NULL THEN
+		rid := arec.default_grade;
+	END IF;
+
+	-- Checks whether the player is a forums admin for the alliance
+	SELECT INTO b forum_admin FROM alliance_grade WHERE id = rid;
+	IF NOT FOUND THEN
+		RETURN;
+	ELSIF b THEN
+		is_admin := TRUE;
+		is_mod := TRUE;
+		can_view := TRUE;
+		can_post := TRUE;
+		can_create := TRUE;
+		can_poll := TRUE;
+		RETURN;
+	END IF;
+
+	-- Get the access level for this forum/rank combination
+	SELECT INTO b is_mod FROM al_rank_forum WHERE rank = rid AND forum = fid;
+	IF NOT FOUND THEN
+		RETURN;
+	ELSIF b THEN
+		is_mod := TRUE;
+		can_view := TRUE;
+		can_post := TRUE;
+		can_create := TRUE;
+		can_poll := TRUE;
+	END IF;
+
+	-- We can view; can we post?
+	can_view := TRUE;
+	IF arec.access_mode = 'M' THEN
+		RETURN;
+	END IF;
+
+	-- We can post; can we create topics?
+	can_post := TRUE;
+	IF arec.access_mode = 'P' THEN
+		RETURN;
+	END IF;
+
+	-- We can create topics; can we create polls?
+	can_create := TRUE;
+	can_poll := (arec.access_mode = 'L');
+END;
+$$ LANGUAGE plpgsql;
diff -Naur beta5//sql/forums/00-schema.sql forums//sql/forums/00-schema.sql
--- beta5//sql/forums/00-schema.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/forums/00-schema.sql	2011-02-05 10:10:01.764335002 +0100
@@ -0,0 +1,18 @@
+-- LegacyWorlds Beta 5
+-- PostgreSQL database scripts
+--
+-- forums/00-schema.sql
+--
+-- Initialises the schema for the forums
+--
+-- Copyright(C) 2004-2007, DeepClone Development
+-- --------------------------------------------------------
+
+
+-- Connect to the database
+\c legacyworlds legacyworlds_admin
+
+-- Create the forums schema
+CREATE SCHEMA forums;
+GRANT USAGE ON SCHEMA forums TO legacyworlds;
+
diff -Naur beta5//sql/forums/01-forums.sql forums//sql/forums/01-forums.sql
--- beta5//sql/forums/01-forums.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/forums/01-forums.sql	2011-02-05 10:10:01.764335002 +0100
@@ -0,0 +1,77 @@
+-- LegacyWorlds Beta 5
+-- PostgreSQL database scripts
+--
+-- forums/01-forums.sql
+--
+-- Categories, forums, topics and posts
+--
+-- Copyright(C) 2004-2007, DeepClone Development
+-- --------------------------------------------------------
+
+
+
+--
+-- Create the table of forum category types
+--
+CREATE TABLE forums.category_type (
+	id		SERIAL			PRIMARY KEY,
+	lib_path	VARCHAR(24)		NOT NULL UNIQUE
+);
+
+GRANT SELECT ON forums.category_type TO legacyworlds;
+SELECT register_serial_table('forums', 'category_type');
+
+
+
+--
+-- Create the table of translations for category types
+--
+CREATE TABLE forums.cat_type_text (
+	id		INT		NOT NULL	REFERENCES forums.category_type (id) ON DELETE CASCADE,
+	lang		VARCHAR(4)	NOT NULL	REFERENCES main.lang (txt) ON DELETE CASCADE,
+	name		VARCHAR(32)	NOT NULL,
+	PRIMARY KEY( id, lang )
+);
+
+CREATE INDEX i_forums_cat_type_lang ON forums.cat_type_text (lang);
+
+GRANT SELECT ON forums.cat_type_text TO legacyworlds;
+
+
+--
+-- Create the category table to store forum groups
+--
+CREATE TABLE forums.category (
+	id		BIGSERIAL			PRIMARY KEY,
+	acl_lib		INT		NOT NULL	REFERENCES forums.category_type (id) ON DELETE CASCADE
+);
+
+CREATE INDEX i_forums_category_acl_lib ON forums.category (acl_lib);
+
+GRANT SELECT,INSERT,DELETE ON forums.category TO legacyworlds;
+GRANT SELECT,UPDATE ON forums.category_id_seq TO legacyworlds;
+
+SELECT register_serial_table('forums', 'category');
+
+
+--
+-- Create the forums table
+--
+CREATE TABLE forums.t_forum (
+	id		BIGSERIAL			PRIMARY KEY,
+	category	BIGINT		NOT NULL	REFERENCES forums.category (id) ON DELETE CASCADE,
+	f_order		INT		NOT NULL	CHECK( f_order >= 0 ),
+	title		VARCHAR(64)	NOT NULL,
+	description	TEXT,
+	deleted		INT		NULL,
+	deleted_by	BIGINT		NULL		REFERENCES main.account (id) ON DELETE SET NULL,
+	UNIQUE( category, f_order ),
+	UNIQUE( category, title )
+);
+
+CREATE INDEX i_forum_deleted_by ON forums.t_forum (deleted_by);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON forums.t_forum TO legacyworlds;
+GRANT SELECT,UPDATE ON forums.t_forum_id_seq TO legacyworlds;
+
+SELECT register_serial_table('forums', 't_forum');
diff -Naur beta5//sql/forums/01-signatures.sql forums//sql/forums/01-signatures.sql
--- beta5//sql/forums/01-signatures.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/forums/01-signatures.sql	2011-02-05 10:10:01.764335002 +0100
@@ -0,0 +1,31 @@
+-- LegacyWorlds Beta 5
+-- PostgreSQL database scripts
+--
+-- forums/01-signatures.sql
+--
+-- Signature storage table
+--
+-- Copyright(C) 2004-2007, DeepClone Development
+-- --------------------------------------------------------
+
+
+--
+-- Create the signature storage table
+--
+CREATE TABLE forums.signature (
+	id		BIGSERIAL		PRIMARY KEY,
+	account		BIGINT	NOT NULL	REFERENCES main.account(id) ON DELETE CASCADE,
+	sig_set		INT	NOT NULL	DEFAULT UNIX_TIMESTAMP(NOW()),
+	sig_unset	INT,
+	signature	TEXT	NOT NULL,
+	enable_code	BOOLEAN	NOT NULL,
+	enable_smileys	BOOLEAN	NOT NULL,
+	CHECK( sig_unset IS NULL OR sig_set < sig_unset )
+);
+
+CREATE UNIQUE INDEX i_forums_signature ON forums.signature (account, sig_set);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON forums.signature TO legacyworlds;
+GRANT SELECT,UPDATE ON forums.signature_id_seq TO legacyworlds;
+
+SELECT register_serial_table('forums', 'signature');
diff -Naur beta5//sql/forums/02-topics.sql forums//sql/forums/02-topics.sql
--- beta5//sql/forums/02-topics.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/forums/02-topics.sql	2011-02-05 10:10:01.764335002 +0100
@@ -0,0 +1,92 @@
+-- LegacyWorlds Beta 5
+-- PostgreSQL database scripts
+--
+-- forums/02-topics.sql
+--
+-- Topics, posts and post texts
+--
+-- Copyright(C) 2004-2007, DeepClone Development
+-- --------------------------------------------------------
+
+
+--
+-- Create the topics table
+--
+CREATE TABLE forums.t_topic (
+	id		BIGSERIAL				PRIMARY KEY,
+	forum		BIGINT		NOT NULL		REFERENCES forums.t_forum (id) ON DELETE CASCADE,
+	sticky_level	INT2		NOT NULL DEFAULT 0	CHECK( sticky_level >= 0 AND sticky_level <= 10 ),
+	moved_from	BIGINT		NULL			REFERENCES forums.t_forum (id) ON DELETE SET NULL,
+	deleted		INT		NULL,
+	deleted_by	BIGINT		NULL			REFERENCES main.account (id) ON DELETE CASCADE,
+	locked		BOOLEAN		NOT NULL DEFAULT FALSE
+);
+
+CREATE INDEX i_topic_forum ON forums.t_topic (forum);
+CREATE INDEX i_topic_moved_from ON forums.t_topic (moved_from);
+CREATE INDEX i_topic_deleted_by ON forums.t_topic (deleted_by);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON forums.t_topic TO legacyworlds;
+GRANT SELECT,UPDATE ON forums.t_topic_id_seq TO legacyworlds;
+
+SELECT register_serial_table('forums', 't_topic');
+
+
+--
+-- Create the post table
+--
+
+CREATE TABLE forums.t_post (
+	id		BIGSERIAL			PRIMARY KEY,
+	topic		BIGINT		NOT NULL	REFERENCES forums.t_topic (id) ON DELETE CASCADE,
+	depth		INT		NOT NULL DEFAULT 0,
+	reply_to	BIGINT				REFERENCES forums.t_post (id),
+	signature	BIGINT				REFERENCES forums.signature (id) ON DELETE SET NULL,
+	deleted		INT,
+	deleted_by	BIGINT				REFERENCES main.account (id) ON DELETE CASCADE
+);
+
+CREATE INDEX i_post_topic ON forums.t_post (topic);
+CREATE INDEX i_post_reply_to ON forums.t_post (reply_to);
+CREATE INDEX i_post_signature ON forums.t_post (signature);
+CREATE INDEX i_deleted_by ON forums.t_post (deleted_by);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON forums.t_post TO legacyworlds;
+GRANT SELECT,UPDATE ON forums.t_post_id_seq TO legacyworlds;
+
+SELECT register_serial_table('forums', 't_post');
+
+
+--
+-- Create the table to store post texts
+--
+
+CREATE TABLE forums.post_text (
+	post		BIGINT		NOT NULL	REFERENCES forums.t_post (id) ON DELETE CASCADE,
+	moment		INT		NOT NULL DEFAULT UNIX_TIMESTAMP(NOW()),
+	author		BIGINT		NOT NULL	REFERENCES main.account (id) ON DELETE CASCADE,
+	title		VARCHAR(100)	NOT NULL,
+	contents	TEXT		NOT NULL,
+	enable_code	BOOLEAN		NOT NULL,
+	enable_smileys	BOOLEAN		NOT NULL,
+	PRIMARY KEY( post, moment )
+);
+
+CREATE INDEX i_post_text_author ON forums.post_text (author);
+
+GRANT SELECT,INSERT,DELETE ON forums.post_text TO legacyworlds;
+
+
+--
+-- Create a table that stores the last time an user read some topic
+--
+CREATE TABLE forums.topic_read (
+	topic		BIGINT		NOT NULL	REFERENCES forums.t_topic (id) ON DELETE CASCADE,
+	read_by		BIGINT		NOT NULL	REFERENCES main.account (id) ON DELETE CASCADE,
+	read_at		INT		NOT NULL DEFAULT UNIX_TIMESTAMP(NOW()),
+	PRIMARY KEY( topic, read_by )
+);
+
+CREATE INDEX i_topic_read_by ON forums.topic_read (read_by);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON forums.topic_read TO legacyworlds;
diff -Naur beta5//sql/forums/03-polls.sql forums//sql/forums/03-polls.sql
--- beta5//sql/forums/03-polls.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/forums/03-polls.sql	2011-02-05 10:10:01.764335002 +0100
@@ -0,0 +1,55 @@
+-- LegacyWorlds Beta 5
+-- PostgreSQL database scripts
+--
+-- forums/03-polls.sql
+--
+-- Forum polls, poll options and votes
+--
+-- Copyright(C) 2004-2007, DeepClone Development
+-- --------------------------------------------------------
+
+
+--
+-- Create the polls table
+--
+CREATE TABLE forums.poll (
+	topic		BIGINT				PRIMARY KEY REFERENCES forums.t_topic (id) ON DELETE CASCADE,
+	title		VARCHAR(64)	NOT NULL,
+	closed		BOOLEAN		NOT NULL DEFAULT FALSE,
+	deleted		INT		NULL,
+	deleted_by	BIGINT		NULL		REFERENCES main.account(id) ON DELETE SET NULL
+);
+
+CREATE INDEX i_poll_deleted_by ON forums.poll (deleted_by);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON forums.poll TO legacyworlds;
+
+
+--
+-- Table for a poll's options
+--
+CREATE TABLE forums.poll_option (
+	id		BIGSERIAL			PRIMARY KEY,
+	poll		BIGINT		NOT NULL	REFERENCES forums.t_topic (id) ON DELETE CASCADE,
+	po_order	INT		NOT NULL	CHECK( po_order >= 0 ),
+	title		VARCHAR(64)	NOT NULL,
+	UNIQUE( poll, po_order ),
+	UNIQUE( poll, title )
+);
+
+GRANT SELECT,INSERT,UPDATE,DELETE ON forums.poll_option TO legacyworlds;
+GRANT SELECT,UPDATE ON forums.poll_option_id_seq TO legacyworlds;
+
+SELECT register_serial_table('forums', 'poll_option');
+
+
+--
+-- Table for a poll's votes
+--
+CREATE TABLE forums.poll_vote (
+	vote		BIGINT		NOT NULL	REFERENCES forums.poll_option (id) ON DELETE CASCADE,
+	account		BIGINT		NOT NULL	REFERENCES main.account (id) ON DELETE CASCADE,
+	PRIMARY KEY( vote, account )
+);
+
+GRANT SELECT,INSERT,DELETE ON forums.poll_vote TO legacyworlds;
diff -Naur beta5//sql/forums/10-views.sql forums//sql/forums/10-views.sql
--- beta5//sql/forums/10-views.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/forums/10-views.sql	2011-02-05 10:10:01.764335002 +0100
@@ -0,0 +1,73 @@
+-- LegacyWorlds Beta 5
+-- PostgreSQL database scripts
+--
+-- forums/10-views.sql
+--
+-- Defines a few views corresponding to some common
+-- queries
+--
+-- Copyright(C) 2004-2007, DeepClone Development
+-- --------------------------------------------------------
+
+
+--
+-- View for individual posts
+--
+
+CREATE VIEW forums.post
+	 AS SELECT p.id AS id, t.forum AS forum, p.topic AS topic, t1.author AS author,
+	 	   p.reply_to AS reply_to, p.signature AS signature, p.deleted AS deleted,
+		   p.deleted_by AS deleted_by, p.depth AS depth, t1.moment AS post_moment,
+		   t2.moment AS last_change, t2.author AS last_author,
+		   t2.contents AS contents, t2.title AS title,
+		   t2.enable_code AS enable_code, t2.enable_smileys AS enable_smileys
+	      FROM forums.t_post p, forums.t_topic t, forums.post_text t1, forums.post_text t2
+	     WHERE t1.post = p.id AND t1.moment = (SELECT MIN(moment) FROM forums.post_text WHERE post = p.id)
+	       AND t2.post = p.id AND t2.moment = (SELECT MAX(moment) FROM forums.post_text WHERE post = p.id)
+	       AND t.id = p.topic;
+
+GRANT SELECT ON forums.post TO legacyworlds;
+
+
+--
+-- View for forum topics
+--
+
+CREATE VIEW forums.topic_fp_lp
+      AS SELECT t.id, t.forum, MIN(p.post_moment) AS fp_moment,
+		MAX(p.last_change) AS lc_moment, COUNT(p.*) AS posts
+	   FROM forums.t_topic t
+      LEFT JOIN forums.post p ON (p.topic = t.id AND (
+			(t.deleted IS NULL AND p.deleted IS NULL)
+		     OR (t.deleted IS NOT NULL AND p.deleted = t.deleted) ))
+       GROUP BY t.id, t.forum, t.deleted;
+
+
+CREATE VIEW forums.topic
+      AS SELECT t.*, tt.posts AS posts,
+		fp.title, fp.id AS first_post, fp.author AS fp_author, fp.post_moment AS fp_moment,
+		lp.author AS lc_author, lp.last_change AS lc_moment
+	   FROM forums.t_topic t
+      LEFT JOIN forums.topic_fp_lp tt ON (t.id = tt.id)
+      LEFT JOIN forums.post fp ON (fp.topic = t.id AND fp.post_moment = tt.fp_moment)
+      LEFT JOIN forums.post lp ON (lp.topic = t.id AND lp.last_change = tt.lc_moment)
+       ORDER BY sticky_level DESC, lc_moment DESC;
+
+GRANT SELECT ON forums.topic TO legacyworlds;
+
+
+--
+-- View for forums
+--
+
+CREATE VIEW forums.forum
+	 AS SELECT f.id AS id, f.category AS category, f.f_order AS f_order,
+	 	   f.title AS title, f.description AS description,
+		   f.deleted AS deleted, f.deleted_by AS deleted_by,
+		   (SELECT COUNT(*) FROM forums.t_topic WHERE forum = f.id AND deleted IS NULL) AS topics,
+		   (SELECT COUNT(*) FROM forums.post WHERE forum = f.id AND deleted IS NULL) AS posts
+	      FROM forums.t_forum f
+	  ORDER BY category, f_order;
+
+GRANT SELECT ON forums.forum TO legacyworlds;
+
diff -Naur beta5//sql/forums/11-access-functions.sql forums//sql/forums/11-access-functions.sql
--- beta5//sql/forums/11-access-functions.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/forums/11-access-functions.sql	2011-02-05 10:10:01.764335002 +0100
@@ -0,0 +1,869 @@
+-- LegacyWorlds Beta 5
+-- PostgreSQL database scripts
+--
+-- forums/11-access-functions.sql
+--
+-- Functions to access the forums and topics
+--
+-- Copyright(C) 2004-2007, DeepClone Development
+-- --------------------------------------------------------
+
+
+-- --------------------------------------------------------
+-- LIST OF ALL REQUIRED FUNCTIONS
+-- --------------------------------------------------------
+--
+-- Functions listed with a '*' have been implemented.
+-- Functions listed with a '!' need modifications.
+--
+--
+-- GENERAL CLEAN-UP
+--
+--  forum_cleanup() *
+--     Causes a general clean-up by deleting old stuff
+--     that had been marked for deletion earlier.
+--
+--
+-- SIGNATURE MANAGEMENT
+--
+--  set_signature( account, set_all, new_signature, enable_code, enable_smileys ) *
+--     Sets a new signature for the account
+--
+--  get_signature( account, timestamp ) *
+--     Returns the complete data for the account's
+--     signature at the specified timestamp.
+--
+--
+-- CATEGORIES AND FORUMS MANAGEMENT
+--
+--  add_category_type( library_path , language , description ) *
+--     Creates a category type with the specified library as
+--     its handler if it doesn't exist, then add the description.
+--
+--  make_category( access_library ) *
+--     Creates a category and returns its ID
+--
+--  make_forum( category, f_order, title, description ) *
+--     Creates a forum and returns its ID
+--
+--  move_up( forum ) *
+--     Moves a forum up in the list of forums in a category
+--
+--  move_down( forum ) *
+--     Moves a forum down in the list of forums in a category
+--
+--  delete_forum( forum , administrator ) *
+--     Delete a forum and its contents
+--
+--  restore_forum( forum ) *
+--     Restores a deleted forum to its previous state
+--
+--  delete_forums( category, administrator )
+--     Deletes all forums in a category
+--
+--  restore_forums( category )
+--     Restore all forums in a category
+--
+--  get_last_post( forum ) *
+--     Returns the data from the last post in the specified forum
+--
+--  get_read_topics( forum , user ) *
+--     Returns the amount of topics an user has read since they
+--     were last updated
+--
+--  mark_forum_read( forum, read_by ) *
+--     Marks all of a forum's topics as read by some user
+--
+--
+-- TOPICS AND POSTS MANAGEMENT
+--
+--  mark_topic_read( topic, read_by ) *
+--     Marks a topic as read by some user
+--
+--  create_topic( forum, author, sticky_level, title, contents, enable_code, enable_smileys, sig ) *
+--     Creates a new topic in a forum and returns its
+--     ID
+--
+--  move_topic( topic, forum, user ) *
+--     Moves a topic from a forum to another
+--
+--  add_reply( reply_to, author, title, contents, enable_code, enable_smileys, signature ) *
+--     Posts a reply to a post
+--
+--  edit_post( post, author, contents, enable_code, enable_smileys, change_signature, signature ) *
+--     Modifies a post
+--
+--  delete_post( post, moderator ) *
+--     Marks a post as deleted; delete the topic if the
+--     post is the topic's "main" post
+--
+--  restore_post( post ) *
+--     Restores a deleted post or, if that post was the
+--     first of a topic, restore the whole topic
+--
+--
+-- FORUM POLLS
+--
+--  create_poll( topic, title ) *
+--     Adds a new poll to the database and returns the
+--     new row
+--
+--  create_option( poll, order, title ) *
+--     Adds a poll option at the specified order and
+--     returns the new row
+--
+--  delete_option( poll, order ) *
+--     Removes a poll option
+--
+--  move_opt_up( poll, order ) *
+--     Moves a poll option up in the list and returns a
+--     boolean to indicate success or failure
+--
+--  move_opt_down( poll, order ) *
+--     Moves a poll option down in the list and returns a
+--     boolean to indicate success or failure
+--
+--  set_vote( user, poll, option) *
+--     Sets the vote on a forum poll
+--
+
+
+
+
+-- --------------------------------------------------------
+-- GENERAL CLEAN-UP
+-- --------------------------------------------------------
+
+--
+-- forum_cleanup()
+--
+-- Causes a general clean-up by deleting old stuff that had been marked for deletion earlier.
+
+CREATE OR REPLACE FUNCTION forums.forum_cleanup() RETURNS VOID AS $$
+DECLARE
+	crec RECORD;
+	frec RECORD;
+	fod INT;
+	nts INT;
+BEGIN
+	-- Start by locking the tables
+	LOCK TABLE forums.category, forums.t_forum, forums.t_topic, forums.t_post IN ACCESS EXCLUSIVE MODE;
+
+	-- Now get the categories containing forums to be deleted
+	nts := UNIX_TIMESTAMP( NOW() ) - 28 * 24 * 3600;
+	FOR crec IN SELECT DISTINCT category FROM forums.t_forum
+		WHERE deleted IS NOT NULL AND deleted <= nts
+	LOOP
+		-- For each category, go through the list of forums
+		fod := 0;
+		FOR frec IN SELECT id,deleted FROM forums.t_forum WHERE category = crec.category
+		LOOP
+			-- This forum is to be deleted; remove it
+			IF (frec.deleted IS NOT NULL AND frec.deleted <= nts) THEN
+				DELETE FROM forums.t_forum WHERE id = frec.id;
+				fod := fod + 1;
+
+			-- This forum is not to be deleted, however its order must be fixed
+			ELSIF (fod > 1) THEN
+				UPDATE forums.t_forum SET f_order = f_order - fod WHERE id = frec.id;
+			END IF;
+		END LOOP;
+	END LOOP;
+
+	-- Delete topics and posts that were not part of forums to be deleted
+	DELETE FROM forums.t_topic WHERE deleted IS NOT NULL AND deleted <= nts;
+	DELETE FROM forums.t_post WHERE deleted IS NOT NULL AND deleted <= nts;
+END;
+$$ LANGUAGE plpgsql;
+
+
+
+
+-- --------------------------------------------------------
+-- SIGNATURE MANAGEMENT
+-- --------------------------------------------------------
+
+--
+-- forums.set_signature( account, set_all, new_signature, enable_code, enable_smileys )
+--
+-- Sets a new signature for the account
+
+CREATE OR REPLACE FUNCTION forums.set_signature ( a BIGINT, s BOOLEAN, t TEXT, ec BOOLEAN, es BOOLEAN ) RETURNS VOID AS $$
+DECLARE
+	sid	BIGINT;
+BEGIN
+	-- Mark the previous signature as "ended"
+	UPDATE forums.signature
+		  SET sig_unset = UNIX_TIMESTAMP(NOW()) - 1
+		WHERE account = a AND sig_unset IS NULL;
+
+	-- If there *is* a new signature, insert it
+	IF NOT(t IS NULL OR t = '') THEN
+		INSERT INTO forums.signature (account, signature, enable_code, enable_smileys)
+			VALUES ( a, t, ec, es );
+		sid := last_inserted('signature');
+	ELSE
+		sid := NULL;
+	END IF;
+
+	-- Change all of the posts' signatures to the new one and delete
+	-- the old signatures if needed.
+	IF s THEN
+		UPDATE forums.t_post
+			  SET signature = sid
+			WHERE author = a;
+		IF sid IS NOT NULL THEN
+			DELETE FROM forums.signature
+				WHERE account = a AND id <> sid;
+		ELSE
+			DELETE FROM forums.signature
+				WHERE account = a;
+		END IF;
+	END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.get_signature( account, timestamp )
+--
+-- Returns the complete data for the account's signature at the specified timestamp.
+
+CREATE OR REPLACE FUNCTION forums.get_signature ( a BIGINT, ts INT ) RETURNS forums.signature AS $$
+	SELECT * FROM forums.signature
+		WHERE account = $1 AND sig_set <= $2
+		  AND (sig_unset IS NULL OR sig_unset >= $2);
+$$ LANGUAGE SQL;
+
+
+
+
+-- --------------------------------------------------------
+-- FORUMS AND CATEGORIES MANAGEMENT
+-- --------------------------------------------------------
+
+--
+-- forums.add_category_type( library_path , language , description )
+--
+-- Creates a category type with the specified library as its handler if it doesn't exist, then add the description.
+
+CREATE OR REPLACE FUNCTION forums.add_category_type ( lp TEXT, lg TEXT, dsc TEXT ) RETURNS VOID AS $$
+DECLARE
+	ct INT;
+BEGIN
+	SELECT INTO ct id FROM forums.category_type WHERE lib_path = lp;
+	IF NOT FOUND THEN
+		INSERT INTO forums.category_type (lib_path) VALUES (lp);
+		ct := last_inserted( 'category_type' );
+	END IF;
+
+	INSERT INTO forums.cat_type_text (id, lang, name) VALUES (ct, lg, dsc);
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.make_category( access_library )
+--
+-- Creates a category and returns its ID
+
+CREATE OR REPLACE FUNCTION forums.make_category ( al TEXT ) RETURNS BIGINT AS $$
+DECLARE
+	ct INT;
+BEGIN
+	SELECT INTO ct id FROM forums.category_type WHERE lib_path = al;
+	IF NOT FOUND THEN
+		RETURN NULL;
+	END IF;
+
+	INSERT INTO forums.category (acl_lib) VALUES ( ct );
+	RETURN last_inserted( 'category' ); 
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.make_forum( category, f_order, title, description )
+--
+-- Creates a forum and returns its ID
+
+CREATE OR REPLACE FUNCTION forums.make_forum( cid BIGINT, fo INT, tt TEXT, dsc TEXT )
+		RETURNS BIGINT AS $$
+DECLARE
+	rec RECORD;
+	fid BIGINT;
+	rfo INT;
+BEGIN
+	IF fo IS NULL THEN
+		SELECT INTO rfo MAX(f_order) + 1 FROM forums.t_forum WHERE category = cid;
+	ELSE
+		rfo := fo;
+		FOR rec IN SELECT id FROM forums.t_forum
+			WHERE category = cid AND f_order >= rfo
+		     ORDER BY f_order DESC
+		          FOR UPDATE
+		LOOP
+			UPDATE forums.t_forum SET f_order = f_order + 1 WHERE id = rec.id;
+		END LOOP;
+	END IF;
+
+	INSERT INTO forums.t_forum (category, f_order, title, description)
+		VALUES (cid, rfo, tt, dsc);
+	fid := last_inserted( 't_forum' );
+	RETURN fid;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.move_up( forum )
+--
+-- Moves a forum up in the list of forums in a category
+
+CREATE OR REPLACE FUNCTION forums.move_up ( fid BIGINT ) RETURNS BOOLEAN AS $$
+DECLARE
+	cid BIGINT;
+	fo INT;
+	mfo INT;
+BEGIN
+	SELECT INTO fo COUNT(*) FROM forums.t_forum WHERE id = fid;
+	IF fo = 0 THEN
+		RETURN FALSE;
+	END IF;
+	SELECT INTO cid category FROM forums.t_forum WHERE id = fid;
+
+	PERFORM * FROM forums.t_forum WHERE category = cid FOR UPDATE;
+	SELECT INTO fo f_order FROM forums.t_forum WHERE id = fid;
+	IF fo = 0 THEN
+		RETURN FALSE;
+	END IF;
+
+	SELECT INTO mfo MAX(f_order) + 1 FROM forums.t_forum
+	          WHERE category = (SELECT category FROM forums.t_forum WHERE id = fid);
+	
+	UPDATE forums.t_forum SET f_order = mfo WHERE id = fid;
+	UPDATE forums.t_forum SET f_order = fo WHERE category = cid AND f_order = fo - 1;
+	UPDATE forums.t_forum SET f_order = fo - 1 WHERE id = fid;
+
+	RETURN TRUE;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.move_down( forum )
+--
+-- Moves a forum down in the list of forums in a category
+
+CREATE OR REPLACE FUNCTION forums.move_down ( fid BIGINT ) RETURNS BOOLEAN AS $$
+DECLARE
+	cid BIGINT;
+	fo INT;
+	mfo INT;
+BEGIN
+	SELECT INTO fo COUNT(*) FROM forums.t_forum WHERE id = fid;
+	IF fo = 0 THEN
+		RETURN FALSE;
+	END IF;
+	SELECT INTO cid category FROM forums.t_forum WHERE id = fid;
+
+	PERFORM * FROM forums.t_forum WHERE category = cid FOR UPDATE;
+	SELECT INTO fo f_order FROM forums.t_forum WHERE id = fid;
+	SELECT INTO mfo MAX(f_order) FROM forums.t_forum
+	          WHERE category = (SELECT category FROM forums.t_forum WHERE id = fid);
+	IF fo = mfo THEN
+		RETURN FALSE;
+	END IF;
+	
+	UPDATE forums.t_forum SET f_order = mfo + 1 WHERE id = fid;
+	UPDATE forums.t_forum SET f_order = fo WHERE category = cid AND f_order = fo + 1;
+	UPDATE forums.t_forum SET f_order = fo + 1 WHERE id = fid;
+
+	RETURN TRUE;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.delete_forum( forum , administrator )
+--
+-- Delete a forum and its contents
+
+CREATE OR REPLACE FUNCTION forums.delete_forum( fid BIGINT, aid BIGINT ) RETURNS VOID AS $$
+DECLARE
+	cts INT;
+BEGIN
+	-- Check if the forum exists and hasn't been deleted
+	PERFORM * FROM forums.t_forum WHERE id = fid AND deleted IS NULL;
+	IF NOT FOUND THEN
+		RETURN;
+	END IF;
+
+	-- Marks the forum and its contents as deleted
+	cts := UNIX_TIMESTAMP( NOW () );
+	UPDATE forums.t_forum SET deleted = cts, deleted_by = aid WHERE id = fid;
+	-- FIXME Argh! f_order! headache!
+	UPDATE forums.t_post SET deleted = cts, deleted_by = aid
+		WHERE topic IN (SELECT id FROM forums.t_topic WHERE forum = fid AND deleted IS NULL)
+		  AND deleted IS NULL;
+	UPDATE forums.t_topic SET deleted = cts, deleted_by = aid
+		WHERE forum = fid AND deleted IS NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.restore_forum( forum )
+--
+-- Restores a deleted forum to its previous state
+
+CREATE OR REPLACE FUNCTION forums.restore_forum( fid BIGINT ) RETURNS VOID AS $$
+DECLARE
+	ts INT;
+BEGIN
+	SELECT INTO ts deleted FROM forums.t_forum
+		WHERE id = fid AND deleted IS NOT NULL;
+	IF NOT FOUND THEN
+		RETURN;
+	END IF;
+
+	-- Marks the forum and its contents as deleted
+	UPDATE forums.t_forum SET deleted = NULL, deleted_by = NULL WHERE id = fid;
+	-- FIXME Argh! f_order! headache!
+	UPDATE forums.t_post SET deleted = NULL, deleted_by = NULL
+		WHERE topic IN (SELECT id FROM forums.t_topic WHERE forum = fid AND deleted = ts)
+		  AND deleted = ts;
+	UPDATE forums.t_topic SET deleted = NULL, deleted_by = NULL
+		WHERE forum = fid AND deleted = ts;
+END;
+$$ LANGUAGE plpgsql;
+
+--
+-- forums.delete_forums( category, administrator )
+--
+-- Deletes all forums in a category
+
+CREATE OR REPLACE FUNCTION forums.delete_forums( cid BIGINT, aid BIGINT ) RETURNS VOID AS $$
+DECLARE
+	f RECORD;
+BEGIN
+	FOR f IN SELECT id FROM forums.t_forum WHERE category = cid AND deleted IS NULL
+	LOOP
+		PERFORM forums.delete_forum( f.id, aid );
+	END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.restore_forums( category )
+--
+-- Restore all forums in a category
+
+CREATE OR REPLACE FUNCTION forums.restore_forums( cid BIGINT ) RETURNS VOID AS $$
+DECLARE
+	f RECORD;
+BEGIN
+	FOR f IN SELECT id FROM forums.t_forum WHERE category = cid AND deleted IS NOT NULL
+	LOOP
+		PERFORM forums.restore_forum( f.id );
+	END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.get_last_post( forum )
+--
+-- Returns the data from the last post in the specified forum
+
+CREATE OR REPLACE FUNCTION forums.get_last_post( fid BIGINT ) RETURNS forums.post AS $$
+	SELECT * FROM forums.post
+		WHERE forum = $1 AND deleted IS NULL
+		  AND last_change = (SELECT MAX(last_change) FROM forums.post WHERE forum = $1 AND deleted IS NULL);
+$$ LANGUAGE SQL;
+
+
+--
+-- forums.get_read_topics( forum , user )
+--
+-- Returns the amount of topics an user has read since they were last updated
+
+CREATE OR REPLACE FUNCTION forums.get_read_topics( fid BIGINT, aid BIGINT ) RETURNS BIGINT AS $$
+	SELECT COUNT(DISTINCT p.topic)
+			   FROM forums.post p
+		      LEFT JOIN forums.topic_read r ON (p.topic = r.topic AND p.last_change <= r.read_at)
+			  WHERE p.forum = $1 AND r.read_by = $2 AND p.deleted IS NULL;
+$$ LANGUAGE SQL;
+
+
+--
+-- forums.mark_forum_read( forum, read_by )
+--
+-- Marks all of a forum's topics as read by some user
+
+CREATE OR REPLACE FUNCTION forums.mark_forum_read( fid BIGINT, aid BIGINT ) RETURNS VOID AS $$ 
+DECLARE
+	rec RECORD;
+BEGIN
+	FOR rec IN SELECT id FROM forums.t_topic WHERE deleted IS NULL AND forum = fid FOR UPDATE
+	LOOP
+		PERFORM forums.mark_topic_read( rec.id, aid );
+	END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+
+
+-- --------------------------------------------------------
+-- TOPICS AND POSTS MANAGEMENT
+-- --------------------------------------------------------
+
+--
+-- forums.mark_topic_read( topic, read_by )
+--
+-- Marks a topic as read by some user
+
+CREATE OR REPLACE FUNCTION forums.mark_topic_read ( tid BIGINT, rid BIGINT ) RETURNS VOID AS $$
+DECLARE
+	rr RECORD;
+BEGIN
+	SELECT INTO rr * FROM forums.topic_read WHERE topic = tid AND read_by = rid FOR UPDATE;
+	IF NOT FOUND THEN
+		INSERT INTO forums.topic_read (topic, read_by) VALUES (tid, rid);
+	ELSE
+		UPDATE forums.topic_read SET read_at = UNIX_TIMESTAMP(NOW())
+			WHERE topic = tid AND read_by = rid;
+	END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+
+
+--
+-- forums.create_topic( forum, author, sticky_level, title, contents, enable_code, enable_smileys, sig )
+--
+-- Creates a new topic in a forum and returns its ID
+
+CREATE OR REPLACE FUNCTION forums.create_topic(
+			fid BIGINT, aid BIGINT, sl INT,
+			ttl TEXT, c TEXT, ec BOOLEAN,
+			es BOOLEAN, sig BIGINT )
+		RETURNS BIGINT AS $$
+DECLARE
+	tid BIGINT;
+	pid BIGINT;
+BEGIN
+	INSERT INTO forums.t_topic (forum, sticky_level) VALUES (fid, sl);
+	SELECT INTO tid last_inserted('t_topic');
+
+	INSERT INTO forums.t_post (topic, signature) VALUES (tid, sig);
+	SELECT INTO pid last_inserted('t_post');
+
+	INSERT INTO forums.post_text (post, author, title, contents, enable_code, enable_smileys)
+		VALUES (pid, aid, ttl, c, ec, es);
+	INSERT INTO forums.topic_read (topic, read_by) VALUES (tid, aid);
+
+	RETURN tid;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.move_topic( topic, forum, user ) *
+--
+-- Moves a topic from a forum to another
+
+CREATE OR REPLACE FUNCTION forums.move_topic( tid BIGINT, did BIGINT, aid BIGINT ) RETURNS VOID AS $$
+DECLARE
+	cfid BIGINT;
+BEGIN
+	SELECT INTO cfid forum FROM forums.t_topic WHERE id = tid;
+	IF NOT FOUND THEN
+		RETURN;
+	END IF;
+
+	UPDATE forums.t_topic SET moved_from = cfid, forum = did WHERE id = tid;
+	DELETE FROM forums.topic_read WHERE topic = tid;
+	PERFORM forums.mark_topic_read( tid, aid );
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.add_reply( reply_to, author, title, contents, enable_code, enable_smileys, signature )
+--
+-- Posts a reply to a post
+
+CREATE OR REPLACE FUNCTION forums.add_reply (
+			rid BIGINT, aid BIGINT, ttl TEXT,
+			c TEXT, ec BOOLEAN, es BOOLEAN,
+			sig BIGINT )
+		RETURNS BIGINT AS $$
+DECLARE
+	rr RECORD;
+	pid BIGINT;
+BEGIN
+	SELECT INTO rr * FROM forums.t_post WHERE id = rid AND deleted IS NULL;
+	IF NOT FOUND THEN
+		RETURN -1;
+	END IF;
+
+	INSERT INTO forums.t_post (topic, reply_to, signature, depth)
+		VALUES (rr.topic, rid, sig, rr.depth + 1);
+	SELECT INTO pid last_inserted('t_post');
+
+	INSERT INTO forums.post_text (post, author, title, contents, enable_code, enable_smileys)
+		VALUES (pid, aid, ttl, c, ec, es);
+
+	PERFORM forums.mark_topic_read( rr.topic, aid );
+
+	RETURN pid;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.edit_post( post, author, contents, enable_code, enable_smileys, change_signature, signature )
+--
+-- Modifies a post
+
+CREATE OR REPLACE FUNCTION forums.edit_post (
+			pid BIGINT, aid BIGINT, ttl TEXT,
+			c TEXT, ec BOOLEAN, es BOOLEAN,
+			chsig BOOLEAN, sig BIGINT )
+		RETURNS VOID AS $$
+DECLARE
+	tid BIGINT;
+BEGIN
+	PERFORM * FROM forums.t_post WHERE id = pid AND deleted IS NULL;
+	IF NOT FOUND THEN
+		RETURN;
+	END IF;
+
+	INSERT INTO forums.post_text (post, author, title, contents, enable_code, enable_smileys)
+		VALUES (pid, aid, ttl, c, ec, es);
+	IF chsig THEN
+		UPDATE forums.t_post SET signature = sig WHERE id = pid;
+	END IF;
+
+	SELECT INTO tid topic FROM forums.t_post WHERE id = pid;
+	PERFORM forums.mark_topic_read( tid, aid );
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.delete_post( post, moderator )
+--
+-- Marks a post as deleted; delete the topic if the post is the topic's "main" post
+
+CREATE OR REPLACE FUNCTION forums.delete_post ( pid BIGINT, mid BIGINT ) RETURNS VOID AS $$
+DECLARE
+	ppid	BIGINT;
+	cts	INT;
+BEGIN
+	SELECT INTO ppid reply_to FROM forums.t_post WHERE id = pid AND deleted IS NULL;
+	IF NOT FOUND THEN
+		RETURN;
+	END IF;
+
+	cts := UNIX_TIMESTAMP(NOW());
+	IF ppid IS NULL THEN
+		-- Delete the topic
+		SELECT INTO ppid topic FROM forums.t_post WHERE id = pid;
+		UPDATE forums.t_topic SET deleted = cts, deleted_by = mid WHERE id = ppid;
+		UPDATE forums.poll SET deleted = cts, deleted_by = mid WHERE topic = ppid;
+		UPDATE forums.t_post SET deleted = cts, deleted_by = mid
+			WHERE topic = ppid AND deleted IS NULL;
+	ELSE
+		-- Delete the post and reparent replies;
+		UPDATE forums.t_post SET deleted = cts, deleted_by = mid WHERE id = pid;
+		UPDATE forums.t_post SET reply_to = ppid, depth = depth - 1 WHERE reply_to = pid;
+	END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.restore_post( post )
+--
+-- Restores a deleted post or, if that post was the first of a topic, restore the whole topic
+
+CREATE OR REPLACE FUNCTION forums.restore_post ( pid BIGINT ) RETURNS VOID AS $$
+DECLARE
+	r	RECORD;
+BEGIN
+	SELECT INTO r reply_to, deleted, topic FROM forums.t_post
+		WHERE id = pid AND deleted IS NOT NULL;
+	IF NOT FOUND THEN
+		RETURN;
+	END IF;
+
+	IF r.reply_to IS NULL THEN
+		-- Restore the topic
+		UPDATE forums.t_post SET deleted = NULL, deleted_by = NULL
+			WHERE topic = r.topic AND deleted = r.deleted;
+		UPDATE forums.t_topic SET deleted = NULL, deleted_by = NULL
+			WHERE id = r.topic;
+		UPDATE forums.poll SET deleted = NULL, deleted_by = NULL
+			WHERE topic = r.topic AND deleted = r.deleted;
+	ELSE
+		-- Restore the post
+		UPDATE forums.t_post SET deleted = NULL, deleted_by = NULL
+			WHERE id = pid;
+	END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+
+
+
+-- --------------------------------------------------------
+-- FORUM POLLS
+-- --------------------------------------------------------
+
+--
+-- forums.create_poll( topic, title )
+--
+-- Adds a new poll to the database and returns the new row
+
+CREATE OR REPLACE FUNCTION forums.create_poll( t_id BIGINT, ttl TEXT ) RETURNS forums.poll AS $$
+DECLARE
+	rv forums.poll;
+BEGIN
+	INSERT INTO forums.poll (topic, title) VALUES (t_id, ttl);
+	SELECT INTO rv * FROM forums.poll WHERE topic = t_id;
+	RETURN rv;
+EXCEPTION WHEN unique_violation OR foreign_key_violation THEN
+	rv.topic := NULL;
+	rv.title := NULL;
+	RETURN rv;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.create_option( poll, order, title )
+--
+-- Adds a poll option at the specified order and returns the new row
+
+CREATE OR REPLACE FUNCTION forums.create_option( p_id BIGINT, oo INT, ttl TEXT ) RETURNS forums.poll_option AS $$
+DECLARE
+	rv forums.poll_option;
+	rec RECORD;
+	noid BIGINT;
+BEGIN
+	FOR rec IN SELECT * FROM forums.poll_option
+			WHERE poll = p_id AND po_order >= oo
+		     ORDER BY po_order DESC
+			      FOR UPDATE
+	LOOP
+		UPDATE forums.poll_option SET po_order = po_order + 1 WHERE id = rec.id;
+	END LOOP;
+
+	INSERT INTO forums.poll_option (poll, po_order, title) VALUES (p_id, oo, ttl);
+	SELECT INTO noid last_inserted('poll_option');
+
+	SELECT INTO rv * FROM forums.poll_option WHERE id = noid;
+	RETURN rv;
+
+EXCEPTION WHEN foreign_key_violation OR unique_violation THEN
+	rv.id := NULL;
+	rv.poll := NULL;
+	RETURN rv;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.delete_option( poll, order )
+--
+-- Removes a poll option
+
+CREATE OR REPLACE FUNCTION forums.delete_option( p_id BIGINT, oo INT ) RETURNS VOID AS $$
+DECLARE
+	rec RECORD;
+BEGIN
+	PERFORM * FROM forums.poll_option WHERE poll = p_id AND po_order > oo FOR UPDATE;
+	DELETE FROM forums.poll_option WHERE poll = p_id AND po_order = oo;
+	FOR rec IN SELECT id FROM forums.poll_option WHERE poll = p_id AND po_order > oo ORDER BY po_order ASC
+	LOOP
+		UPDATE forums.poll_option SET po_order = po_order - 1 WHERE id = rec.id;
+	END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.move_opt_up( poll, order )
+--
+-- Moves a poll option up in the list and returns a boolean to indicate success or failure
+
+CREATE OR REPLACE FUNCTION forums.move_opt_up( p_id BIGINT, oo INT ) RETURNS BOOLEAN AS $$
+DECLARE
+	c BIGINT;
+BEGIN
+	IF oo = 0 THEN
+		RETURN FALSE;
+	END IF;
+
+	SELECT INTO c COUNT(*) FROM forums.poll_option WHERE poll = p_id;
+	IF NOT FOUND OR oo >= c THEN
+		RETURN FALSE;
+	END IF;
+
+	UPDATE forums.poll_option SET po_order = c WHERE poll = p_id AND po_order = oo - 1;
+	UPDATE forums.poll_option SET po_order = po_order - 1 WHERE poll = p_id AND po_order = oo;
+	UPDATE forums.poll_option SET po_order = oo WHERE poll = p_id AND po_order = c;
+	RETURN TRUE;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.move_opt_down( poll, order )
+--
+-- Moves a poll option down in the list and returns a boolean to indicate success or failure
+
+CREATE OR REPLACE FUNCTION forums.move_opt_down( p_id BIGINT, oo INT ) RETURNS BOOLEAN AS $$
+DECLARE
+	c BIGINT;
+BEGIN
+	SELECT INTO c COUNT(*) FROM forums.poll_option WHERE poll = p_id;
+	IF NOT FOUND OR oo >= c - 1 THEN
+		RETURN FALSE;
+	END IF;
+
+	UPDATE forums.poll_option SET po_order = c WHERE poll = p_id AND po_order = oo + 1;
+	UPDATE forums.poll_option SET po_order = po_order + 1 WHERE poll = p_id AND po_order = oo;
+	UPDATE forums.poll_option SET po_order = oo WHERE poll = p_id AND po_order = c;
+	RETURN TRUE;
+END;
+$$ LANGUAGE plpgsql;
+
+
+--
+-- forums.set_vote( user, poll, option )
+--
+-- Sets the vote on a forum poll
+
+CREATE OR REPLACE FUNCTION forums.set_vote( u_id BIGINT, p_id BIGINT, o_id BIGINT ) RETURNS VOID AS $$
+DECLARE
+	p BIGINT;
+BEGIN
+	-- Delete the previous vote
+	DELETE FROM forums.poll_vote
+		WHERE account = u_id
+		  AND vote IN (SELECT id FROM forums.poll_option WHERE poll = p_id);
+
+	IF o_id IS NOT NULL THEN
+		-- Add the new vote if that is possible
+		SELECT INTO p poll FROM forums.poll_option WHERE id = o_id;
+		IF NOT FOUND OR p <> p_id THEN
+			RETURN;
+		END IF;
+		INSERT INTO forums.poll_vote (vote, account) VALUES (o_id, u_id);
+	END IF;
+END;
+$$ LANGUAGE plpgsql;
diff -Naur beta5//sql/forums/FORUMS.sql forums//sql/forums/FORUMS.sql
--- beta5//sql/forums/FORUMS.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/forums/FORUMS.sql	2011-02-05 10:10:01.764335002 +0100
@@ -0,0 +1,19 @@
+-- LegacyWorlds Beta 5
+-- PostgreSQL database scripts
+--
+-- forums/FORUMS.sql
+--
+-- Install the forums' generic tables and code
+--
+-- Copyright(C) 2004-2007, DeepClone Development
+-- --------------------------------------------------------
+
+
+-- Execute the forums' installation scripts
+\i forums/00-schema.sql
+\i forums/01-forums.sql
+\i forums/01-signatures.sql
+\i forums/02-topics.sql
+\i forums/03-polls.sql
+\i forums/10-views.sql
+\i forums/11-access-functions.sql
diff -Naur beta5//sql/INSTALL.sql forums//sql/INSTALL.sql
--- beta5//sql/INSTALL.sql	2011-02-05 10:09:56.244335002 +0100
+++ forums//sql/INSTALL.sql	2011-03-12 15:03:56.721300053 +0100
@@ -10,16 +10,20 @@
 
 \i 00-init.sql
 \i 01-inheritance.sql
+
 \i 10-main.sql
 \i 11-main-enums.sql
 \i 12-main-tables.sql
 \i 13-main-donations.sql
-\i 13-main-forums.sql
+-- \i 13-main-forums.sql
 \i 13-main-links.sql
 \i 13-main-manual.sql
 \i 13-main-proxy.sql
+\i 14-main-forums.sql
+\i 15-main-gf-functions.sql
 \i 18-main-functions.sql
 \i 19-main-values.sql
+
 \i 25-ctf-maps.sql
 \i 25-predefined-alliances.sql
 \i 30-beta5.sql
diff -Naur beta5//sql/tools/destroy_alliance_forums.sql forums//sql/tools/destroy_alliance_forums.sql
--- beta5//sql/tools/destroy_alliance_forums.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/tools/destroy_alliance_forums.sql	2011-02-05 10:10:01.714335002 +0100
@@ -0,0 +1,11 @@
+ALTER TABLE alliance DROP COLUMN f_category;
+
+-- grep 'CREATE TABLE' beta5/structure/02-alliance-forums.sql | sed -e 's/CREATE/DROP/' -e 's/ ($/ CASCADE;/' >>tools/destroy_alliance_forums.sql
+-- grep 'CREATE OR REPLACE FUNCTION' beta5/structure/02-alliance-forums.sql | sed -e 's/CREATE OR REPLACE/DROP/' -e 's/).*$/) CASCADE;/' >> tools/destroy_alliance_forums.sql
+
+DROP TABLE alliance_forum CASCADE;
+DROP TABLE al_rank_forum CASCADE;
+
+DROP FUNCTION create_alliance_forum( pid BIGINT, aid BIGINT, fo INT, ttl TEXT, dsc TEXT, am TEXT) CASCADE;
+DROP FUNCTION modify_alliance_forum( pid BIGINT, fid BIGINT, ttl TEXT, dsc TEXT, am TEXT ) CASCADE;
+DROP FUNCTION get_aforums_privs( pid BIGINT, fid BIGINT, OUT can_view BOOLEAN, OUT can_post BOOLEAN, OUT can_create BOOLEAN, OUT can_poll BOOLEAN, OUT is_mod BOOLEAN, OUT is_admin BOOLEAN) CASCADE;
diff -Naur beta5//sql/tools/destroy_forums.sql forums//sql/tools/destroy_forums.sql
--- beta5//sql/tools/destroy_forums.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/tools/destroy_forums.sql	2011-02-05 10:10:01.714335002 +0100
@@ -0,0 +1,39 @@
+DROP SCHEMA forums CASCADE;
+
+SET search_path="b5",public;
+\i tools/destroy_alliance_forums.sql
+
+SET search_path="b5m0",public;
+\i tools/destroy_alliance_forums.sql
+
+SET search_path=public;
+-- grep 'CREATE TABLE' 1[45]*.sql | awk -F: '{print $2}' | sed -e 's/CREATE/DROP/' -e 's/ ($/ CASCADE;/' >>tools/destroy_forums.sql
+-- grep 'CREATE OR REPLACE FUNCTION' 1[45]*.sql | awk -F: '{print $2}' | sed -e 's/CREATE OR REPLACE/DROP/' -e 's/).*$/) CASCADE;/' >> tools/destroy_forums.sql
+
+DROP TABLE main.gf_category CASCADE;
+DROP TABLE main.gf_forum CASCADE;
+DROP TABLE main.gf_ban CASCADE;
+DROP TABLE main.gf_admin CASCADE;
+DROP TABLE main.gf_cat_moderator CASCADE;
+DROP TABLE main.gf_forum_moderator CASCADE;
+DROP TABLE main.user_category CASCADE;
+DROP TABLE main.user_forum CASCADE;
+DROP TABLE main.uf_subscription CASCADE;
+DROP TABLE main.uf_invite CASCADE;
+DROP FUNCTION main.trgf_gf_forum_check () CASCADE;
+DROP FUNCTION main.trgf_gf_admin_check () CASCADE;
+DROP FUNCTION main.trgf_gf_cmod_check () CASCADE;
+DROP FUNCTION main.trgf_gf_fmod_check() CASCADE;
+DROP FUNCTION main.init_general_forums() CASCADE;
+DROP FUNCTION main.init_version_forums( v TEXT ) CASCADE;
+DROP FUNCTION main.init_game_forums( g TEXT ) CASCADE;
+DROP FUNCTION main.get_gf_categories( ver TEXT, game TEXT ) CASCADE;
+DROP FUNCTION main.get_gf_list( ver TEXT, game TEXT ) CASCADE;
+DROP FUNCTION main.get_gforums_privs( aid BIGINT, fid BIGINT, OUT can_view BOOLEAN, OUT can_post BOOLEAN, OUT can_create BOOLEAN, OUT can_poll BOOLEAN, OUT is_mod BOOLEAN, OUT is_admin BOOLEAN) CASCADE;
+DROP FUNCTION main.uf_get_access_mode( fid BIGINT ) CASCADE;
+DROP FUNCTION main.uf_get_user_access( aid BIGINT, fid BIGINT ) CASCADE;
+DROP FUNCTION main.uf_get_category( aid BIGINT ) CASCADE;
+DROP FUNCTION main.uf_create_forum( aid BIGINT, fo INT, ttl TEXT, dsc TEXT, ua TEXT, am TEXT, pass TEXT) CASCADE;
+DROP FUNCTION main.trgf_account_user_forums() CASCADE;
+DROP FUNCTION main.trgf_uf_subscription_check() CASCADE;
+DROP FUNCTION main.trgf_uf_invite_check() CASCADE;
diff -Naur beta5//sql/tools/make_alliance_forums.sql forums//sql/tools/make_alliance_forums.sql
--- beta5//sql/tools/make_alliance_forums.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/tools/make_alliance_forums.sql	2011-02-05 10:10:01.714335002 +0100
@@ -0,0 +1,115 @@
+ALTER TABLE alliance ADD COLUMN f_category BIGINT NOT NULL DEFAULT forums.make_category('beta5/aforums') REFERENCES forums.category (id);
+CREATE INDEX alliance_f_category ON alliance (f_category);
+\i beta5/structure/02-alliance-forums.sql
+
+
+
+CREATE OR REPLACE FUNCTION get_player_uid( pid BIGINT ) RETURNS BIGINT AS $$
+DECLARE
+	i BIGINT;
+BEGIN
+	SELECT INTO i userid FROM player WHERE id = pid;
+	RETURN i;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION upgrade_alf_replies(opid BIGINT, npid BIGINT) RETURNS VOID AS $$
+DECLARE
+	rep RECORD;
+	nrid BIGINT;
+BEGIN
+	-- For each reply
+	FOR rep IN SELECT * FROM af_post WHERE reply_to = opid
+	LOOP
+		-- Post the reply
+		SELECT INTO nrid forums.add_reply( npid, get_player_uid(rep.author), rep.title, rep.contents,
+			rep.enable_code, rep.enable_smileys, NULL );
+		UPDATE forums.post_text SET moment = rep.moment WHERE post = nrid;
+
+		-- Check for edited post
+		IF rep.edited IS NOT NULL THEN
+			INSERT INTO forums.post_text (post, moment, author, title, contents, enable_code, enable_smileys)
+				VALUES (nrid, rep.edited, get_player_uid(rep.edited_by), rep.title, rep.contents,
+					rep.enable_code, rep.enable_smileys);
+		END IF;
+
+		-- Handle replies
+		PERFORM upgrade_alf_replies( rep.id, nrid );
+	END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION upgrade_alf_contents(ofid INT, nfid BIGINT) RETURNS VOID AS $$
+DECLARE
+	top RECORD;
+	fpost RECORD;
+	ntid BIGINT;
+	fpid BIGINT;
+BEGIN
+	RAISE NOTICE 'Importing alliance forum #% (as #%)', ofid, nfid;
+
+	-- For each topic
+	FOR top IN SELECT * FROM af_topic WHERE forum = ofid
+	LOOP
+		-- Create it
+		SELECT INTO fpost * FROM af_post WHERE id = top.first_post;
+		SELECT INTO ntid forums.create_topic(
+			nfid, get_player_uid(fpost.author), CASE top.sticky WHEN true THEN 10 ELSE 0 END,
+			fpost.title, fpost.contents, fpost.enable_code, fpost.enable_smileys, NULL
+		);
+
+		-- Get the first post and fix its timestamp
+		SELECT INTO fpid id FROM forums.t_post WHERE topic = ntid;
+		UPDATE forums.post_text SET moment = fpost.moment WHERE post = fpid;
+
+		-- Add an entry if it has been edited
+		IF fpost.edited IS NOT NULL THEN
+			INSERT INTO forums.post_text (post, moment, author, title, contents, enable_code, enable_smileys)
+				VALUES (fpid, fpost.edited, get_player_uid(fpost.edited_by), fpost.title, fpost.contents,
+					fpost.enable_code, fpost.enable_smileys);
+		END IF;
+
+		PERFORM upgrade_alf_replies( fpost.id, fpid );
+	END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION upgrade_alliance( aid INT, cid BIGINT ) RETURNS VOID AS $$
+DECLARE
+	af RECORD;
+	arf RECORD;
+	nfid BIGINT;
+BEGIN
+	FOR af IN SELECT * FROM af_forum WHERE alliance = aid
+	LOOP
+		SELECT INTO nfid forums.make_forum( cid, af.forder, af.title, af.description );
+		INSERT INTO alliance_forum VALUES ( nfid, aid, CASE af.user_post WHEN TRUE THEN 'T' ELSE 'P' END);
+		FOR arf IN SELECT * FROM algr_forums WHERE forum = af.id
+		LOOP
+			INSERT INTO al_rank_forum VALUES (arf.grade, nfid, arf.is_mod);
+		END LOOP;
+		PERFORM upgrade_alf_contents( af.id, nfid );
+	END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION upgrade_alliance_forums() RETURNS VOID AS $$
+DECLARE
+	al RECORD;
+BEGIN
+	FOR al IN SELECT * FROM alliance
+	LOOP
+		RAISE NOTICE 'Upgrading forums for alliance #% [%]', al.id, al.tag;
+		PERFORM upgrade_alliance( al.id, al.f_category );
+	END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT upgrade_alliance_forums();
+
+DROP FUNCTION upgrade_alliance_forums() ;
+DROP FUNCTION upgrade_alliance( INT, BIGINT ) ;
+DROP FUNCTION upgrade_alf_contents( INT, BIGINT) ;
+DROP FUNCTION get_player_uid( BIGINT ) ;
diff -Naur beta5//sql/tools/make_forums.sql forums//sql/tools/make_forums.sql
--- beta5//sql/tools/make_forums.sql	1970-01-01 01:00:00.000000000 +0100
+++ forums//sql/tools/make_forums.sql	2011-02-05 10:10:01.714335002 +0100
@@ -0,0 +1,168 @@
+\i 14-main-forums.sql
+\i 15-main-gf-functions.sql
+\i 15-main-uforums.sql
+
+SELECT forums.add_category_type( 'beta5/aforums', 'en', 'Alliance forums' );
+
+
+CREATE OR REPLACE FUNCTION upgrade_gen_replies(opid BIGINT, npid BIGINT) RETURNS VOID AS $$
+DECLARE
+	rep RECORD;
+	nrid BIGINT;
+BEGIN
+	-- For each reply
+	FOR rep IN SELECT * FROM main.f_post WHERE reply_to = opid AND deleted IS NULL
+	LOOP
+		-- Post the reply
+		SELECT INTO nrid forums.add_reply( npid, rep.author, rep.title, rep.contents,
+			rep.enable_code, rep.enable_smileys, NULL );
+		UPDATE forums.post_text SET moment = rep.moment WHERE post = nrid;
+
+		-- Check for edited post
+		IF rep.edited IS NOT NULL THEN
+			INSERT INTO forums.post_text (post, moment, author, title, contents, enable_code, enable_smileys)
+				VALUES (nrid, rep.edited, rep.edited_by, rep.title, rep.contents,
+					rep.enable_code, rep.enable_smileys);
+		END IF;
+
+		-- Handle replies
+		PERFORM upgrade_gen_replies( rep.id, nrid );
+	END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION upgrade_gen_forum(ofid INT, nfid BIGINT) RETURNS VOID AS $$
+DECLARE
+	top RECORD;
+	fpost RECORD;
+	ntid BIGINT;
+	fpid BIGINT;
+BEGIN
+	RAISE NOTICE 'Importing general forum #% (as #%)', ofid, nfid;
+
+	-- For each topic
+	FOR top IN SELECT * FROM main.f_topic WHERE forum = ofid AND deleted IS NULL
+	LOOP
+		-- Create it
+		SELECT INTO fpost * FROM main.f_post WHERE id = top.first_post;
+		SELECT INTO ntid forums.create_topic(
+			nfid, fpost.author, CASE top.sticky WHEN true THEN 10 ELSE 0 END,
+			fpost.title, fpost.contents, fpost.enable_code, fpost.enable_smileys, NULL
+		);
+
+		-- Get the first post and fix its timestamp
+		SELECT INTO fpid id FROM forums.t_post WHERE topic = ntid;
+		UPDATE forums.post_text SET moment = fpost.moment WHERE post = fpid;
+
+		-- Add an entry if it has been edited
+		IF fpost.edited IS NOT NULL THEN
+			INSERT INTO forums.post_text (post, moment, author, title, contents, enable_code, enable_smileys)
+				VALUES (fpid, fpost.edited, fpost.edited_by, fpost.title, fpost.contents,
+					fpost.enable_code, fpost.enable_smileys);
+		END IF;
+
+		PERFORM upgrade_gen_replies( fpost.id, fpid );
+	END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION upgrade_forums() RETURNS VOID AS $$
+DECLARE
+	mcid	BIGINT;
+	cid_v	BIGINT;
+	cid_b5	BIGINT;
+	cid_m5	BIGINT;
+	fid	BIGINT;
+BEGIN
+	-- Main forums
+	SELECT INTO mcid main.init_general_forums();
+	-- Announcements
+	SELECT INTO fid forums.make_forum( mcid, 0, 'Announcements',
+		'Updates on the game''s progress, new versions, etc...');
+	INSERT INTO main.gf_forum VALUES ( fid, 'MP' );
+	PERFORM upgrade_gen_forum( 1, fid );
+	-- The Pub
+	SELECT INTO fid forums.make_forum( mcid, 1, 'The Pub', 'Discuss whatever''s on your mind in here.');
+	INSERT INTO main.gf_forum VALUES ( fid, 'UL' );
+	PERFORM upgrade_gen_forum( 3, fid );
+	-- M&A
+	SELECT INTO fid forums.make_forum( mcid, 2, 'Malcontents and Anarchists',
+		'Forum in which public executions take place.');
+	INSERT INTO main.gf_forum VALUES ( fid, 'MP' );
+	PERFORM upgrade_gen_forum( 4, fid );
+	-- Admin policies
+	SELECT INTO fid forums.make_forum( mcid, 3, '[ADMIN] Main moderators/admins board',
+		'Forum only available to Legacy Worlds administrators and moderators.');
+	INSERT INTO main.gf_forum VALUES ( fid, 'MO' );
+	PERFORM upgrade_gen_forum( 12, fid );
+
+
+	-- Beta 5 *version* forums
+	SELECT INTO cid_v main.init_version_forums('beta5');
+	-- New / Improved features
+	SELECT INTO fid forums.make_forum( cid_v, 0, 'New / Improved features',
+		'Your ideas about how to improve the game and our response to these ideas.' );
+	PERFORM upgrade_gen_forum( 2, fid);
+	-- Bugs and Problems
+	SELECT INTO fid forums.make_forum( cid_v, 1, 'Bugs and Problems',
+		'Whine about what you think is wrong with Beta 5 in this forum.' );
+	PERFORM upgrade_gen_forum( 7, fid);
+	-- Help
+	SELECT INTO fid forums.make_forum( cid_v, 2, 'Help', 'Ask the staff and other players for advice.' );
+	PERFORM upgrade_gen_forum( 8, fid);
+
+
+	-- Beta 5 *game* forums
+	SELECT INTO cid_b5 main.init_game_forums('beta5');
+	-- General Discussion
+	SELECT INTO fid forums.make_forum( cid_b5, 0, 'General Discussion',
+		'Discuss what''s going on in the Beta 5 universe.' );
+	PERFORM upgrade_gen_forum( 5, fid );
+	-- Alliance Recruitment
+	SELECT INTO fid forums.make_forum( cid_b5, 1, 'Alliance Recruitment',
+		'Advertise for your alliance and recruit new members through this forum.' );
+	PERFORM upgrade_gen_forum( 6, fid );
+	-- Marketplace Advertisement
+	SELECT INTO fid forums.make_forum( cid_b5, 2, 'Marketplace Advertisement',
+		'Advertise for items you''ve put on sale in the marketplace or technologies you''re willing to provide through diplomacy.' );
+	PERFORM upgrade_gen_forum( 9, fid );
+
+
+	-- Beta 5 *match* forums
+	SELECT INTO cid_m5 main.init_game_forums('b5match');
+	-- General Discussion
+	SELECT INTO fid forums.make_forum( cid_m5, 0, 'General Discussion',
+		'Discuss what''s going on in the Beta 5 match.' );
+	PERFORM upgrade_gen_forum( 11, fid );
+	-- Alliance Recruitment
+	SELECT INTO fid forums.make_forum( cid_m5, 1, 'Alliance Recruitment',
+		'Advertise for your alliance and recruit new members through this forum.' );
+	-- Marketplace Advertisement
+	SELECT INTO fid forums.make_forum( cid_m5, 2, 'Marketplace Advertisement',
+		'Advertise for items you''ve put on sale in the marketplace or technologies you''re willing to provide through diplomacy.' );
+
+
+	-- Add admins and mods
+	INSERT INTO main.gf_admin (account) VALUES (1);
+	INSERT INTO main.gf_admin (account) VALUES (2);
+	INSERT INTO main.gf_admin (account) VALUES (3);
+	INSERT INTO main.gf_admin (account) VALUES (4);
+	INSERT INTO main.gf_cat_moderator (account) VALUES (7);
+	INSERT INTO main.gf_cat_moderator (account) VALUES (8);
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT upgrade_forums();
+
+DROP FUNCTION upgrade_forums() ;
+DROP FUNCTION upgrade_gen_forum(ofid INT, nfid BIGINT) ;
+DROP FUNCTION upgrade_gen_replies(BIGINT, BIGINT) ;
+
+SET search_path=b5,main,public;
+\i tools/make_alliance_forums.sql
+SET search_path=b5m0,main,public;
+\i tools/make_alliance_forums.sql
+SET search_path=public;
+
+VACUUM ANALYZE;