.
*
* AOChat, a PHP class for talking with the Anarchy Online chat servers.
* It requires the sockets extension (to connect to the chat server..)
* from PHP 4.2.0+ and either the GMP or BCMath extension (for generating
* and calculating the login keys) to work.
*
* A disassembly of the official java chat client[1] for Anarchy Online
* and Slicer's AO::Chat perl module[2] were used as a reference for this
* class.
*
* [1]:
* [2]:
*
* Updates to this class can be found from the following web site:
* http://auno.org/dev/aochat.html
*
**************************************************************************
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA
*
*/
if((float)phpversion() < 5.0)
{
die("AOChat class needs PHP version >= 5.0.0 to work.\n");
}
if(!extension_loaded("sockets"))
{
die("AOChat class needs the Sockets extension to work.\n");
}
if(!extension_loaded("gmp") &&
!extension_loaded("bcmath") &&
!extension_loaded("aokex"))
{
die("AOChat class needs either AOkex, GMP or BCMath extension to work.\n");
}
set_time_limit(0);
ini_set("html_errors", 0);
/* Packet type definitions - so we won't have to use the number IDs
* .. I did not distinct between server and client message types, as
* they are mostly the same for same type packets, but maybe it should
* have been done anyway.. // auno - 2004/mar/26
*/
define('AOCP_LOGIN_SEED', 0);
define('AOCP_LOGIN_REQUEST', 2);
define('AOCP_LOGIN_SELECT', 3);
define('AOCP_LOGIN_OK', 5);
define('AOCP_LOGIN_ERROR', 6);
define('AOCP_LOGIN_CHARLIST', 7);
define('AOCP_CLIENT_UNKNOWN', 10);
define('AOCP_CLIENT_NAME', 20);
define('AOCP_CLIENT_LOOKUP', 21);
define('AOCP_MSG_PRIVATE', 30);
define('AOCP_MSG_VICINITY', 34);
define('AOCP_MSG_VICINITYA', 35);
define('AOCP_MSG_SYSTEM', 36);
define('AOCP_CHAT_NOTICE', 37);
define('AOCP_BUDDY_ADD', 40);
define('AOCP_BUDDY_REMOVE', 41);
define('AOCP_ONLINE_SET', 42);
define('AOCP_PRIVGRP_INVITE', 50);
define('AOCP_PRIVGRP_KICK', 51);
define('AOCP_PRIVGRP_JOIN', 52);
define('AOCP_PRIVGRP_PART', 53);
define('AOCP_PRIVGRP_KICKALL', 54);
define('AOCP_PRIVGRP_CLIJOIN', 55);
define('AOCP_PRIVGRP_CLIPART', 56);
define('AOCP_PRIVGRP_MESSAGE', 57);
define('AOCP_PRIVGRP_REFUSE', 58);
define('AOCP_GROUP_ANNOUNCE', 60);
define('AOCP_GROUP_PART', 61);
define('AOCP_GROUP_DATA_SET', 64);
define('AOCP_GROUP_MESSAGE', 65);
define('AOCP_GROUP_CM_SET', 66);
define('AOCP_CLIENTMODE_GET', 70);
define('AOCP_CLIENTMODE_SET', 71);
define('AOCP_PING', 100);
define('AOCP_FORWARD', 110);
define('AOCP_CC', 120);
define('AOCP_ADM_MUX_INFO', 1100);
define('AOCP_GROUP_JOIN', AOCP_GROUP_ANNOUNCE); /* compat */
define('AOC_GROUP_NOWRITE', 0x00000002);
define('AOC_GROUP_NOASIAN', 0x00000020);
define('AOC_GROUP_MUTE', 0x01010000);
define('AOC_GROUP_LOG', 0x02020000);
define('AOC_BUDDY_KNOWN', 0x01);
define('AOC_BUDDY_ONLINE', 0x02);
define('AOC_FLOOD_LIMIT', 7);
define('AOC_FLOOD_INC', 2);
define('AOC_PRIORITY_HIGH', 1000);
define('AOC_PRIORITY_MED', 500);
define('AOC_PRIORITY_LOW', 100);
define('AOEM_UNKNOWN', 0xFF);
define('AOEM_ORG_JOIN', 0x10);
define('AOEM_ORG_KICK', 0x11);
define('AOEM_ORG_LEAVE', 0x12);
define('AOEM_ORG_DISBAND', 0x13);
define('AOEM_ORG_FORM', 0x14);
define('AOEM_ORG_VOTE', 0x15);
define('AOEM_NW_ATTACK', 0x20);
define('AOEM_NW_ABANDON', 0x21);
define('AOEM_AI_CLOAK', 0x30);
define('AOEM_AI_RADAR', 0x31);
define('AOEM_AI_ATTACK', 0x32);
define('AOEM_AI_REMOVE_INIT', 0x33);
define('AOEM_AI_REMOVE', 0x34);
define('AOEM_AI_HQ_REMOVE_INIT', 0x35);
define('AOEM_AI_HQ_REMOVE', 0x36);
class AOChat
{
var $state, $debug, $id, $gid, $chars, $char, $grp, $buddies;
var $socket, $last_packet, $last_ping, $callback, $cbargs;
var $serverseed, $tellqueue;
/* Initialization */
function AOChat($cb, $args = NULL)
{
$this->callback = $cb;
$this->cbargs = $args;
$this->disconnect();
}
function disconnect()
{
if(is_resource($this->socket))
socket_close($this->socket);
$this->socket = NULL;
$this->serverseed = NULL;
$this->chars = NULL;
$this->char = NULL;
$this->last_packet = 0;
$this->last_ping = 0;
$this->state = "connect";
$this->id = array();
$this->gid = array();
$this->grp = array();
$this->chars = array();
$this->buddies = array();
$this->tellqueue = NULL;
$this->groupqueue = NULL;
}
/* Network stuff */
function connect($server = "chat2.d1.funcom.com", $port = 7012)
{
if($this->state !== "connect")
die("AOChat: not expecting connect.\n");
$s = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if(!is_resource($s)) /* this is fatal */
die("Could not create socket.\n");
$this->socket = $s;
$this->state = "auth";
if(@socket_connect($s, $server, $port) === false)
{
trigger_error("Could not connect to the AO Chat server ($server:$port): ".
socket_strerror(socket_last_error($s)), E_USER_WARNING);
$this->disconnect();
return false;
}
$packet = $this->get_packet();
if(!is_object($packet) || $packet->type != AOCP_LOGIN_SEED)
{
trigger_error("Received invalid greeting packet from AO Chat server.", E_USER_WARNING);
$this->disconnect();
return false;
}
$this->tellqueue = new AOChatQueue(array($this, 'dispatch_tell'), AOC_FLOOD_LIMIT, AOC_FLOOD_INC);
$this->groupqueue = new AOChatQueue(array($this, 'dispatch_groupmsg'), AOC_FLOOD_LIMIT, AOC_FLOOD_INC);
return $s;
}
function iteration()
{
$now = time();
if($this->tellqueue !== NULL)
$this->tellqueue->run();
if($this->groupqueue !== NULL)
$this->groupqueue->run();
if(($now-$this->last_packet) > 60)
if(($now-$this->last_ping) > 60)
$this->send_ping();
}
function wait_for_packet($time = 1)
{
$this->iteration();
$sec = (int)$time;
if(is_float($time))
$usec = (int)($time * 1000000 % 1000000);
else
$usec = 0;
if(!socket_select($a = array($this->socket), $b = null, $c = null, $sec, $usec))
return NULL;
else
return $this->get_packet();
}
function read_data($len)
{
$data = "";
$rlen = $len;
while($rlen > 0)
{
if(($tmp = socket_read($this->socket, $rlen)) === false)
{
printf("Read error: %s\n", socket_strerror(socket_last_error($this->socket)));
$this->disconnect();
return "";
}
if($tmp == "")
{
echo("Read error: EOF\n");
$this->disconnect();
return "";
}
$data .= $tmp;
$rlen -= strlen($tmp);
}
return $data;
}
function get_packet()
{
$head = $this->read_data(4);
if(strlen($head) != 4)
return false;
list(, $type, $len) = unpack("n2", $head);
$data = $this->read_data($len);
if(is_resource($this->debug))
{
fwrite($this->debug, "<<<<<\n");
fwrite($this->debug, $head);
fwrite($this->debug, $data);
fwrite($this->debug, "\n=====\n");
}
$packet = new AOChatPacket("in", $type, $data);
switch($type)
{
case AOCP_LOGIN_SEED :
$this->serverseed = $packet->args[0];
break;
case AOCP_CLIENT_NAME :
case AOCP_CLIENT_LOOKUP :
list($id, $name) = $packet->args;
$id = "" . $id;
$name = ucfirst(strtolower($name));
$this->id[$id] = $name;
$this->id[$name] = $id;
break;
case AOCP_GROUP_ANNOUNCE :
list($gid, $name, $status) = $packet->args;
$this->grp[$gid] = $status;
$this->gid[$gid] = $name;
$this->gid[strtolower($name)] = $gid;
break;
case AOCP_GROUP_MESSAGE :
/* Hack to support extended messages */
if($packet->args[1] === 0 && substr($packet->args[2], 0, 2) == "~&")
{
$em = new AOExtMsg($packet->args[2]);
if($em->type != AOEM_UNKNOWN)
{
$packet->args[2] = $em->text;
$packet->args[] = $em;
}
}
break;
case AOCP_BUDDY_ADD :
list($bid, $bonline, $btype) = $packet->args;
$this->buddies[$bid] = ($bonline ? AOC_BUDDY_ONLINE : 0)|
(ord($btype) ? AOC_BUDDY_KNOWN : 0);
break;
case AOCP_BUDDY_REMOVE :
unset($this->buddies[$packet->args[0]]);
break;
}
$this->last_packet = time();
if(is_callable($this->callback))
{
call_user_func($this->callback, $packet->type, $packet->args, $this->cbargs);
}
return $packet;
}
function send_packet($packet)
{
$data = pack("n2", $packet->type, strlen($packet->data)) . $packet->data;
if(is_resource($this->debug))
{
fwrite($this->debug, ">>>>>\n");
fwrite($this->debug, $data);
fwrite($this->debug, "\n=====\n");
}
socket_write($this->socket, $data, strlen($data));
return true;
}
/* Login functions */
function authenticate($username, $password)
{
if($this->state != "auth")
die("AOChat: not expecting authentication.\n");
if(extension_loaded("aokex"))
$key = aokex_login_key($this->serverseed, $username, $password);
else
$key = $this->generate_login_key($this->serverseed, $username, $password);
$pak = new AOChatPacket("out", AOCP_LOGIN_REQUEST, array(0, $username, $key));
$this->send_packet($pak);
$packet = $this->get_packet();
if($packet->type != AOCP_LOGIN_CHARLIST)
{
return false;
}
for($i=0;$iargs[0]);$i++)
{
$this->chars[] = array(
"id" => $packet->args[0][$i],
"name" => ucfirst(strtolower($packet->args[1][$i])),
"level" => $packet->args[2][$i],
"online" => $packet->args[3][$i]);
}
$this->username = $username;
$this->state = "login";
return $this->chars;
}
function login($char)
{
if($this->state != "login")
die("AOChat: not expecting login.\n");
if(is_int($char))
{
$field = "id";
}
else if(is_string($char))
{
$field = "name";
$char = ucfirst(strtolower($char));
}
if(!is_array($char))
{
if(empty($field))
{
return false;
}
else
{
foreach($this->chars as $e)
{
if($e[$field] == $char)
{
$char = $e;
break;
}
}
}
}
if(!is_array($char))
{
die("AOChat: no valid character to login.\n");
}
$pq = new AOChatPacket("out", AOCP_LOGIN_SELECT, $char["id"]);
$this->send_packet($pq);
$pr = $this->get_packet();
if($pr->type != AOCP_LOGIN_OK)
{
return false;
}
$this->char = $char;
$this->state = "ok";
return true;
}
/* User and group lookup functions */
function lookup_user($u)
{
$u = ucfirst(strtolower($u));
if(isset($this->id[$u]))
return $this->id[$u];
$this->send_packet(new AOChatPacket("out", AOCP_CLIENT_LOOKUP, $u));
for($i=0; $i<100 && !isset($this->id[$u]); $i++)
$this->get_packet();
return isset($this->id[$u]) ? $this->id[$u] : false;
}
function get_uid($user)
{
if(!($uid = (int)$user))
if(!($uid = (int)$this->lookup_user($user)))
return false;
if($uid <= 0 || $uid == 0xffffffff)
return false;
return $uid;
}
function get_uname($user)
{
if(!($uid = (int)$user))
return $user;
else
return $this->lookup_user($uid);
}
function lookup_group($arg, $type=0)
{
if($type && ($is_gid = (strlen($arg) === 5 && (ord($arg[0])&~0x80) < 0x10)))
return $arg;
if(!$is_gid)
$arg = strtolower($arg);
return isset($this->gid[$arg]) ? $this->gid[$arg] : false;
}
function get_gid($g)
{
return $this->lookup_group($g, 1);
}
function get_gname($g)
{
if(($gid = $this->lookup_group($g, 1)) === false)
return false;
return $this->gid[$gid];
}
/* Sending various packets */
function send_ping()
{
$this->last_ping = time();
return $this->send_packet(new AOChatPacket("out", AOCP_PING, "AOChat.php"));
}
function dispatch_tell($tgt, $msg)
{
if(($uid = $this->get_uid($tgt)) === false)
return false;
return $this->send_packet(new AOChatPacket("out", AOCP_MSG_PRIVATE,
array($uid, $msg, "\0")));
}
function send_tell($user, $msg, $blob = "\0")
{
$this->tellqueue->push(AOC_PRIORITY_MED, $user, $msg);
return true;
}
/* General chat groups */
function dispatch_groupmsg($group, $msg)
{
if(($gid = $this->get_gid($group)) === false)
return false;
return $this->send_packet(new AOChatPacket("out", AOCP_GROUP_MESSAGE,
array($gid, $msg, "\0")));
}
function send_group($group, $msg, $blob = "\0")
{
$this->groupqueue->push(AOC_PRIORITY_MED, $group, $msg);
return true;
}
function group_join($group)
{
if(($gid = $this->get_gid($group)) === false)
return false;
return $this->send_packet(new AOChatPacket("out", AOCP_GROUP_DATA_SET,
array($gid, $this->grp[$gid] & ~AOC_GROUP_MUTE, "\0")));
}
function group_leave($group)
{
if(($gid = $this->get_gid($group)) === false)
return false;
return $this->send_packet(new AOChatPacket("out", AOCP_GROUP_DATA_SET,
array($gid, $this->grp[$gid] | AOC_GROUP_MUTE, "\0")));
}
function group_status($group)
{
if(($gid = $this->get_gid($group)) === false)
return false;
return $this->grp[$gid];
}
/* Private chat groups */
function send_privgroup($group, $msg, $blob = "\0")
{
if(($gid = $this->get_uid($group)) === false)
return false;
return $this->send_packet(new AOChatPacket("out", AOCP_PRIVGRP_MESSAGE,
array($gid, $msg, $blob)));
}
function privategroup_join($group)
{
if(($gid = $this->get_uid($group)) === false)
return false;
return $this->send_packet(new AOChatPacket("out", AOCP_PRIVGRP_JOIN, $gid));
}
function join_privgroup($group) /* Deprecated - 2004/Mar/26 - auno@auno.org */
{
return $this->privategroup_join($group);
}
function privategroup_invite($user)
{
if(($uid = $this->get_uid($user)) === false)
return false;
return $this->send_packet(new AOChatPacket("out", AOCP_PRIVGRP_INVITE, $uid));
}
function privategroup_kick($user)
{
if(($uid = $this->get_uid($user)) === false)
return false;
return $this->send_packet(new AOChatPacket("out", AOCP_PRIVGRP_KICK, $uid));
}
function privategroup_kick_all()
{
return $this->send_packet(new AOChatPacket("out", AOCP_PRIVGRP_KICKALL, ""));
}
/* Buddies */
function buddy_add($user, $type="\1")
{
if(($uid = $this->get_uid($user)) === false)
return false;
if($uid === $this->char['id'])
return false;
return $this->send_packet(new AOChatPacket("out", AOCP_BUDDY_ADD,
array($uid, $type)));
}
function buddy_remove($user)
{
if(($uid = $this->get_uid($user)) === false)
return false;
return $this->send_packet(new AOChatPacket("out", AOCP_BUDDY_REMOVE, $uid));
}
function buddy_remove_unknown()
{
return $this->send_packet(new AOChatPacket("out", AOCP_CC,
array(array("rembuddy", "?"))));
}
function buddy_exists($who)
{
if(($uid = $this->get_uid($who)) === false)
return false;
return (int)$this->buddies[$uid];
}
function buddy_online($who)
{
return ($this->buddy_exists($who) & AOC_BUDDY_ONLINE) ? true : false;
}
/* Login key generation and encryption */
function get_random_hex_key($bits)
{
$str = "";
do
$str .= sprintf('%02x', mt_rand(0, 0xff));
while(($bits -= 8) > 0);
return $str;
}
function bighexdec($x)
{
if(substr($x, 0, 2) != "0x")
return $x;
$r = "0";
for($p = $q = strlen($x) - 1; $p >= 2; $p--)
{
$r = bcadd($r, bcmul(hexdec($x[$p]), bcpow(16, $q - $p)));
}
return $r;
}
function bigdechex($x)
{
$r = "";
while($x != "0")
{
$r = dechex(bcmod($x, 16)) . $r;
$x = bcdiv($x, 16);
}
return $r;
}
function bcmath_powm($base, $exp, $mod)
{
$base = $this->bighexdec($base);
$exp = $this->bighexdec($exp);
$mod = $this->bighexdec($mod);
if(function_exists("bcpowmod")) /* PHP5 finally has this */
{
$r = bcpowmod($base, $exp, $mod);
return $this->bigdechex($r);
}
$r = 1;
$p = $base;
while(true)
{
if(bcmod($exp, 2))
{
$r = bcmod(bcmul($p, $r), $mod);
$exp = bcsub($exp, "1");
if(bccomp($exp, "0") == 0)
{
return $this->bigdechex($r);
}
}
$exp = bcdiv($exp, 2);
$p = bcmod(bcmul($p, $p), $mod);
}
}
/* This is 'half' Diffie-Hellman key exchange.
* 'Half' as in we already have the server's key ($dhY)
* $dhN is a prime and $dhG is generator for it.
*
* http://en.wikipedia.org/wiki/Diffie-Hellman_key_exchange
*/
function generate_login_key($servkey, $username, $password)
{
$dhY = "0x9c32cc23d559ca90fc31be72df817d0e124769e809f936bc14360ff4bed758f260a0d596584eacbbc2b88bdd410416163e11dbf62173393fbc0c6fefb2d855f1a03dec8e9f105bbad91b3437d8eb73fe2f44159597aa4053cf788d2f9d7012fb8d7c4ce3876f7d6cd5d0c31754f4cd96166708641958de54a6def5657b9f2e92";
$dhN = "0xeca2e8c85d863dcdc26a429a71a9815ad052f6139669dd659f98ae159d313d13c6bf2838e10a69b6478b64a24bd054ba8248e8fa778703b418408249440b2c1edd28853e240d8a7e49540b76d120d3b1ad2878b1b99490eb4a2a5e84caa8a91cecbdb1aa7c816e8be343246f80c637abc653b893fd91686cf8d32d6cfe5f2a6f";
$dhG = "0x5";
$dhx = "0x".$this->get_random_hex_key(256);
if(extension_loaded("gmp"))
{
$dhN = gmp_init($dhN);
$dhX = gmp_strval(gmp_powm($dhG, $dhx, $dhN), 16);
$dhK = gmp_strval(gmp_powm($dhY, $dhx, $dhN), 16);
}
else if(extension_loaded("bcmath"))
{
$dhX = $this->bcmath_powm($dhG, $dhx, $dhN);
$dhK = $this->bcmath_powm($dhY, $dhx, $dhN);
}
else
{
die("generate_login_key(): no idea how to powm...\n");
}
$str = sprintf("%s|%s|%s", $username, $servkey, $password);
if(strlen($dhK) < 32)
$dhK = str_repeat("0", 32-strlen($dhK)) . $dhK;
else
$dhK = substr($dhK, 0, 32);
$prefix = pack("H16", $this->get_random_hex_key(64));
$length = 8 + 4 + strlen($str); /* prefix, int, ... */
$pad = str_repeat(" ", (8 - $length % 8) % 8);
$strlen = pack("N", strlen($str));
$plain = $prefix . $strlen . $str . $pad;
$crypted = $this->aochat_crypt($dhK, $plain);
return $dhX . "-" . $crypted;
}
function aochat_crypt($key, $str)
{
if(strlen($key) != 32 || strlen($str) % 8 != 0)
{
return false;
}
$cycle = array(0, 0);
$result = array(0, 0);
$ret = "";
$keyarr = unpack("V*", pack("H*", $key));
$dataarr = unpack("V*", $str);
for($i=1; $i<=sizeof($dataarr); $i+=2)
{
$cycle[0] = $dataarr[$i] ^ $result[0];
$cycle[1] = $dataarr[$i+1] ^ $result[1];
$result = $this->aochat_tea_encrypt($cycle, $keyarr);
$ret .= array_pop(unpack("H*", pack("V*", $result[0], $result[1])));
}
return $ret;
}
/* TEA encryption
* http://en.wikipedia.org/wiki/Tiny_Encryption_Algorithm
*/
function aochat_tea_encrypt($cycle, $key)
{
$a = $cycle[0];
$b = $cycle[1];
$sum = 0;
$delta = (int)0x9e3779b9;
$i = 32;
while($i--)
{
$sum = (int)($sum + $delta);
$a += (($b << 4 & 0xfffffff0) + $key[1]) ^ ($b + $sum) ^ (($b >> 5 & 0x7ffffff) + $key[2]);
$b += (($a << 4 & 0xfffffff0) + $key[3]) ^ ($a + $sum) ^ (($a >> 5 & 0x7ffffff) + $key[4]);
}
return array($a, $b);
}
}
/* The AOChatPacket class - turning packets into binary blobs and
* binary blobs into packets.
*
* Data types:
* I - 32 bit integer: uint32_t
* S - 8 bit string array: uint16_t length, char str[length]
* G - 40 bit binary data: unsigned char data[5]
* i - integer array: uint16_t count, uint32_t[count]
* s - string array: uint16_t count, aochat_str_t[count]
*
* D - 'data', we have relabeled all 'D' type fields to 'S'
* M - mapping [see t.class in ao_nosign.jar] - unsupported
*
*/
class AOChatPacket
{
private static $packet_map = array(
"in" => array(
AOCP_LOGIN_SEED => array("name"=>"Login Seed", "args"=>"S"),
AOCP_LOGIN_OK => array("name"=>"Login Result OK", "args"=>""),
AOCP_LOGIN_ERROR => array("name"=>"Login Result Error", "args"=>"S"),
AOCP_LOGIN_CHARLIST => array("name"=>"Login CharacterList", "args"=>"isii"),
AOCP_CLIENT_UNKNOWN => array("name"=>"Client Unknown", "args"=>"I"),
AOCP_CLIENT_NAME => array("name"=>"Client Name", "args"=>"IS"),
AOCP_CLIENT_LOOKUP => array("name"=>"Lookup Result", "args"=>"IS"),
AOCP_MSG_PRIVATE => array("name"=>"Message Private", "args"=>"ISS"),
AOCP_MSG_VICINITY => array("name"=>"Message Vicinity", "args"=>"ISS"),
AOCP_MSG_VICINITYA => array("name"=>"Message Anon Vicinity", "args"=>"SSS"),
AOCP_MSG_SYSTEM => array("name"=>"Message System", "args"=>"S"),
AOCP_CHAT_NOTICE => array("name"=>"Chat Notice", "args"=>"IIIS"),
AOCP_BUDDY_ADD => array("name"=>"Buddy Added", "args"=>"IIS"),
AOCP_BUDDY_REMOVE => array("name"=>"Buddy Removed", "args"=>"I"),
AOCP_PRIVGRP_INVITE => array("name"=>"Privategroup Invited", "args"=>"I"),
AOCP_PRIVGRP_KICK => array("name"=>"Privategroup Kicked", "args"=>"I"),
AOCP_PRIVGRP_PART => array("name"=>"Privategroup Part", "args"=>"I"),
AOCP_PRIVGRP_CLIJOIN => array("name"=>"Privategroup Client Join", "args"=>"II"),
AOCP_PRIVGRP_CLIPART => array("name"=>"Privategroup Client Part", "args"=>"II"),
AOCP_PRIVGRP_MESSAGE => array("name"=>"Privategroup Message", "args"=>"IISS"),
AOCP_PRIVGRP_REFUSE => array("name"=>"Privategroup Refuse Invite", "args"=>"II"),
AOCP_GROUP_ANNOUNCE => array("name"=>"Group Announce", "args"=>"GSIS"),
AOCP_GROUP_PART => array("name"=>"Group Part", "args"=>"G"),
AOCP_GROUP_MESSAGE => array("name"=>"Group Message", "args"=>"GISS"),
AOCP_PING => array("name"=>"Pong", "args"=>"S"),
AOCP_FORWARD => array("name"=>"Forward", "args"=>"IM"),
AOCP_ADM_MUX_INFO => array("name"=>"Adm Mux Info", "args"=>"iii"),
),
"out" => array(
AOCP_LOGIN_REQUEST => array("name"=>"Login Response GetCharLst", "args"=>"ISS"),
AOCP_LOGIN_SELECT => array("name"=>"Login Select Character", "args"=>"I"),
AOCP_CLIENT_LOOKUP => array("name"=>"Name Lookup", "args"=>"S"),
AOCP_MSG_PRIVATE => array("name"=>"Message Private", "args"=>"ISS"),
AOCP_BUDDY_ADD => array("name"=>"Buddy Add", "args"=>"IS"),
AOCP_BUDDY_REMOVE => array("name"=>"Buddy Remove", "args"=>"I"),
AOCP_ONLINE_SET => array("name"=>"Onlinestatus Set", "args"=>"I"),
AOCP_PRIVGRP_INVITE => array("name"=>"Privategroup Invite", "args"=>"I"),
AOCP_PRIVGRP_KICK => array("name"=>"Privategroup Kick", "args"=>"I"),
AOCP_PRIVGRP_JOIN => array("name"=>"Privategroup Join", "args"=>"I"),
AOCP_PRIVGRP_PART => array("name"=>"Privategroup Part", "args"=>"I"),
AOCP_PRIVGRP_KICKALL => array("name"=>"Privategroup Kickall", "args"=>""),
AOCP_PRIVGRP_MESSAGE => array("name"=>"Privategroup Message", "args"=>"ISS"),
AOCP_GROUP_DATA_SET => array("name"=>"Group Data Set", "args"=>"GIS"),
AOCP_GROUP_MESSAGE => array("name"=>"Group Message", "args"=>"GSS"),
AOCP_GROUP_CM_SET => array("name"=>"Group Clientmode Set", "args"=>"GIIII"),
AOCP_CLIENTMODE_GET => array("name"=>"Clientmode Get", "args"=>"IG"),
AOCP_CLIENTMODE_SET => array("name"=>"Clientmode Set", "args"=>"IIII"),
AOCP_PING => array("name"=>"Ping", "args"=>"S"),
AOCP_CC => array("name"=>"CC", "args"=>"s"),
),
);
function AOChatPacket($dir, $type, $data)
{
$this->args = array();
$this->type = $type;
$this->dir = $dir;
$pmap = self::$packet_map[$dir][$type];
if(!$pmap)
{
echo "Unsupported packet type (". $dir . ", " . $type . ")\n";
return false;
}
if($dir == "in")
{
if(!is_string($data))
{
echo "Incorrect argument for incoming packet, expecting a string.\n";
return false;
}
for($i=0; $iargs[] = $res;
}
}
else
{
if(!is_array($data))
{
$args = array($data);
}
else
{
$args = $data;
}
$data = "";
for($i=0; $idata = $data;
}
return true;
}
}
/* New "extended" messages, parser and abstraction.
* These were introduced in 16.1. The messages use postscript
* base85 encoding (not ipv6 / rfc 1924 base85). They also use
* some custom encoding and references to further confuse things.
*
* Messages start with the magic marker ~& and end with ~
* Messages begin with two base85 encoded numbers that define
* the category and instance of the message. After that there
* are an category/instance defined amount of variables which
* are prefixed by the variable type. A base85 encoded number
* takes 5 bytes. Variable types:
*
* s: string, first byte is the length of the string
* i: signed integer (b85)
* u: unsigned integer (b85)
* f: float (b85)
* R: reference, b85 category and instance
* F: recursive encoding
* ~: end of message
*
* Message categories:
* 501 : More org messages
* 0xad0ae9b : Organization leave because of alignment change
* s(Char)
* 506 : NW messages
* 0x0c299d4 : Tower attack
* R(Faction), s(Org), s(Char),
* R(Faction), s(Org),
* s(Zone), i(Zone-X), i(Zone-Y)
* 0x8cac524 : Area abandoned
* R(Faction), s(Org), s(Zone)
* 508 : Org messages
* 0x04e87e7 : Character joined the organization
* s(Char)
* 0x2360067 : Character was kicked
* s(Kicker), s(Kicked)
* 0x2bd9377 : Character has left
* s(Char)
* 0x8487156 : Change of governing form
* s(Char), s(Form)
* 0x88cc2e7 : Organization disbanded
* s(Char)
* 0xc477095 : Vote begins
* s(Vote text), u(Minutes), s(Choices)
* 1001 : AI messages
* 0x01 : Cloak
* s(Char), s(Cloak status)
* 0x02 : Radar alert
* 0x03 : Alien attack
* s(Zone)
* 0x04 : Org HQ removed
* s(Char), s(Zone)
* 0x05 : Building removal initiated
* s(Char), R(House type), s(Zone)
* 0x06 : Building removed
* s(Char), R(House type), s(Zone)
* 0x07 : Org HQ remove initiated
* s(Char), s(Zone)
*
* Reference categories:
* 509 : House types (?)
* 0x00 : Normal House
* 2005 : Faction
* 0x00 : Neutral
* 0x01 : Clan
* 0x02 : Omni
*
*/
class AOExtMsg
{
private static $msg_cat = array(
501 => array(0xad0ae9b => array(AOEM_ORG_LEAVE,
"{NAME} has left the organization because of alignment change.",
"s{NAME}"),
),
506 => array(0x0c299d4 => array(AOEM_NW_ATTACK,
"{ATT_NAME} ({ATT_ORG}, {ATT_SIDE}) attacked {DEF_ORG} ({DEF_SIDE}) in {ZONE} at {X}, {Y}.",
"R{ATT_SIDE}/s{ATT_ORG}/s{ATT_NAME}/R{DEF_SIDE}/s{DEF_ORG}/s{ZONE}/i{X}/i{Y}"),
0x8cac524 => array(AOEM_NW_ABANDON,
"{ORG} ({SIDE}) abandoned their base in {ZONE}.",
"R{SIDE}/s{ORG}/s{ZONE}"),
),
508 => array(0x04e87e7 => array(AOEM_ORG_JOIN,
"{NAME} has joined the organization.",
"s{NAME}"),
0x2360067 => array(AOEM_ORG_KICK,
"{KICKER} kicked {NAME} from the organization.",
"s{KICKER}/s{NAME}"),
0x2bd9377 => array(AOEM_ORG_LEAVE,
"{NAME} has left the organization.",
"s{NAME}"),
0x8487156 => array(AOEM_ORG_FORM,
"{NAME} changed the organization governing form to {FORM}.",
"s{NAME}/s{FORM}"),
0x88cc2e7 => array(AOEM_ORG_DISBAND,
"{NAME} has disbanded the organization.",
"s{NAME}"),
0xc477095 => array(AOEM_ORG_VOTE,
"Voting notice: {SUBJECT}\nCandidates: {CHOICES}\nDuration: {DURATION} minutes",
"s{SUBJECT}/u{MINUTES}/s{CHOICES}"),
),
1001 => array(0x01 => array(AOEM_AI_CLOAK,
"{NAME} turned the cloaking device in your city {STATUS}.",
"s{NAME}/s{STATUS}"),
0x02 => array(AOEM_AI_RADAR,
"Your radar station is picking up alien activity in the area surrounding your city.",
""),
0x03 => array(AOEM_AI_ATTACK,
"Your city in {ZONE} has been targeted by hostile forces.",
"s{ZONE}"),
0x04 => array(AOEM_AI_HQ_REMOVE,
"{NAME} removed the organization headquarters in {ZONE}.",
"s{NAME}/s{ZONE}"),
0x05 => array(AOEM_AI_REMOVE_INIT,
"{NAME} initiated removal of a {TYPE} in {ZONE}.",
"s{NAME}/R{TYPE}/s{ZONE}"),
0x06 => array(AOEM_AI_REMOVE,
"{NAME} removed a {TYPE} in {ZONE}.",
"s{NAME}/R{TYPE}/s{ZONE}"),
0x07 => array(AOEM_AI_HQ_REMOVE_INIT,
"{NAME} initiated removal of the organization headquarters in {ZONE}.",
"s{NAME}/s{ZONE}"),
),
);
private static $ref_cat = array(
509 => array(0x00 => "Normal House"),
2005 => array(0x00 => "Neutral",
0x01 => "Clan",
0x02 => "Omni"),
);
public $type, $text, $args;
function AOExtMsg($str=NULL)
{
$this->type = AOEM_UNKNOWN;
if(!empty($str))
$this->read($str);
}
function arg($n)
{
$key = "{".strtoupper($n)."}";
if(isset($this->args[$key]))
return $this->args[$key];
return NULL;
}
function read($msg)
{
if(substr($msg, 0, 2) !== "~&")
return false;
$msg = substr($msg, 2);
$category = $this->b85g($msg);
$instance = $this->b85g($msg);
if(!isset(self::$msg_cat[$category]) || !isset(self::$msg_cat[$category][$instance]))
return false;
$typ = self::$msg_cat[$category][$instance][0];
$fmt = self::$msg_cat[$category][$instance][1];
$enc = self::$msg_cat[$category][$instance][2];
$args = array();
foreach(split("/", $enc) as $eone)
{
$ename = substr($eone, 1);
$msg = substr($msg, 1); // skip the data type id
switch($eone[0])
{
case "s":
$len = ord($msg[0])-1;
$str = substr($msg, 1, $len);
$msg = substr($msg, $len +1);
$args[$ename] = $str;
break;
case "i":
case "u":
$num = $this->b85g($msg);
$args[$ename] = $num;
break;
case "R":
$cat = $this->b85g($msg);
$ins = $this->b85g($msg);
if(!isset(self::$ref_cat[$cat]) || !isset(self::$ref_cat[$cat][$ins]))
$str = "Unknown ($cat, $ins)";
else
$str = self::$ref_cat[$cat][$ins];
$args[$ename] = $str;
break;
}
}
$str = strtr($fmt, $args);
$this->type = $typ;
$this->text = $str;
$this->args = $args;
}
function b85g(&$str)
{
$n = 0;
for($i=0; $i<5; $i++)
$n = $n*85 + ord($str[$i])-33;
$str = substr($str, 5);
return $n;
}
}
/* Prioritized chat message queue. */
class AOChatQueue
{
var $dfunc, $queue, $qsize;
var $point, $limit, $inc;
function AOChatQueue($cb, $limit, $inc)
{
$this->dfunc = $cb;
$this->limit = $limit;
$this->inc = $inc;
$this->point = 0;
$this->queue = array();
$this->qsize = 0;
}
function push($priority)
{
$args = array_slice(func_get_args(), 1);
$now = time();
if($this->point <= ($now+$this->limit))
{
call_user_func_array($this->dfunc, $args);
$this->point = (($this->point<$now) ? $now : $this->point)+$this->inc;
return 1;
}
if(isset($this->queue[$priority]))
{
$this->queue[$priority][] = $args;
}
else
{
$this->queue[$priority] = array($args);
krsort($this->queue);
}
$this->qsize ++;
return 2;
}
function run()
{
if($this->qsize === 0)
return 0;
$now = time();
if($this->point < $now)
$this->point = $now;
else if($this->point > ($now + $this->limit))
return 0;
$processed = 0;
foreach(array_keys($this->queue) as $priority)
{
for(;;)
{
$item = array_shift($this->queue[$priority]);
if($item === NULL)
{
unset($this->queue[$priority]);
break;
}
call_user_func_array($this->dfunc, $item);
$this->point += $this->inc;
$processed ++;
if($this->point > ($now + $this->limit))
{
break(2);
}
}
}
$this->qsize -= $processed;
return $processed;
}
}
?>