446 lines
12 KiB
PHP
446 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* This file is part of GameQ.
|
|
*
|
|
* GameQ is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* GameQ 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 Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/**
|
|
* GameSpy3 Protocol Class
|
|
*
|
|
* This class is used as the basis for all game servers
|
|
* that use the GameSpy3 protocol for querying
|
|
* server status.
|
|
*
|
|
* Note: UT3 and Crysis2 have known issues with GSv3 responses
|
|
*
|
|
* @author Austin Bischoff <austin@codebeard.com>
|
|
*/
|
|
class GameQ_Protocols_Gamespy3 extends GameQ_Protocols
|
|
{
|
|
/**
|
|
* Set the packet mode to linear
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $packet_mode = self::PACKET_MODE_LINEAR;
|
|
|
|
/**
|
|
* Array of packets we want to look up.
|
|
* Each key should correspond to a defined method in this or a parent class
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $packets = array(
|
|
self::PACKET_CHALLENGE => "\xFE\xFD\x09\x10\x20\x30\x40",
|
|
self::PACKET_ALL => "\xFE\xFD\x00\x10\x20\x30\x40%s\xFF\xFF\xFF\x01",
|
|
);
|
|
|
|
/**
|
|
* Methods to be run when processing the response(s)
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $process_methods = array(
|
|
"process_all",
|
|
);
|
|
|
|
/**
|
|
* Default port for this server type
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $port = 1; // Default port, used if not set when instanced
|
|
|
|
/**
|
|
* The protocol being used
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $protocol = 'gamespy3';
|
|
|
|
/**
|
|
* String name of this protocol class
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $name = 'gamespy3';
|
|
|
|
/**
|
|
* Longer string name of this protocol class
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $name_long = "Gamespy3";
|
|
|
|
/**
|
|
* Parse the challenge response and apply it to all the packet types
|
|
* that require it.
|
|
*
|
|
* @see GameQ_Protocols_Core::parseChallengeAndApply()
|
|
*/
|
|
protected function parseChallengeAndApply()
|
|
{
|
|
// Pull out the challenge
|
|
$challenge = substr(preg_replace( "/[^0-9\-]/si", "", $this->challenge_buffer->getBuffer()), 1);
|
|
|
|
$challenge_result = sprintf(
|
|
"%c%c%c%c",
|
|
( $challenge >> 24 ),
|
|
( $challenge >> 16 ),
|
|
( $challenge >> 8 ),
|
|
( $challenge >> 0 )
|
|
);
|
|
|
|
// Apply the challenge and return
|
|
return $this->challengeApply($challenge_result);
|
|
}
|
|
|
|
/*
|
|
* Internal methods
|
|
*/
|
|
protected function preProcess_all($packets)
|
|
{
|
|
$return = array();
|
|
|
|
// Get packet index, remove header
|
|
foreach ($packets as $index => $packet)
|
|
{
|
|
// Make new buffer
|
|
$buf = new GameQ_Buffer($packet);
|
|
|
|
// Skip the header
|
|
$buf->skip(14);
|
|
|
|
// Get the current packet and make a new index in the array
|
|
$return[$buf->readInt16()] = $buf->getBuffer();
|
|
}
|
|
|
|
unset($buf);
|
|
|
|
// Sort packets, reset index
|
|
ksort($return);
|
|
|
|
// Grab just the values
|
|
$return = array_values($return);
|
|
|
|
// Compare last var of current packet with first var of next packet
|
|
// On a partial match, remove last var from current packet,
|
|
// variable header from next packet
|
|
for ($i = 0, $x = count($return); $i < $x - 1; $i++)
|
|
{
|
|
// First packet
|
|
$fst = substr($return[$i], 0, -1);
|
|
|
|
// Second packet
|
|
$snd = $return[$i+1];
|
|
|
|
// Get last variable from first packet
|
|
$fstvar = substr($fst, strrpos($fst, "\x00")+1);
|
|
|
|
// Get first variable from last packet
|
|
$snd = substr($snd, strpos($snd, "\x00")+2);
|
|
$sndvar = substr($snd, 0, strpos($snd, "\x00"));
|
|
|
|
// Check if fstvar is a substring of sndvar
|
|
// If so, remove it from the first string
|
|
if (strpos($sndvar, $fstvar) !== false)
|
|
{
|
|
$return[$i] = preg_replace("#(\\x00[^\\x00]+\\x00)$#", "\x00", $return[$i]);
|
|
}
|
|
}
|
|
|
|
// Now let's loop the return and remove any dupe prefixes
|
|
for($x = 1; $x < count($return); $x++)
|
|
{
|
|
$buf = new GameQ_Buffer($return[$x]);
|
|
|
|
$prefix = $buf->readString();
|
|
|
|
// Check to see if the return before has the same prefix present
|
|
if($prefix != null && strstr($return[($x-1)], $prefix))
|
|
{
|
|
// Update the return by removing the prefix plus 2 chars
|
|
$return[$x] = substr(str_replace($prefix, '', $return[$x]), 2);
|
|
}
|
|
|
|
unset($buf);
|
|
}
|
|
|
|
unset($x, $i, $snd, $sndvar, $fst, $fstvar);
|
|
|
|
// Implode into a string and return
|
|
return implode("", $return);
|
|
}
|
|
|
|
protected function process_all()
|
|
{
|
|
// Make sure we have a valid response
|
|
if(!$this->hasValidResponse(self::PACKET_ALL))
|
|
{
|
|
return array();
|
|
}
|
|
|
|
// Set the result to a new result instance
|
|
$result = new GameQ_Result();
|
|
|
|
// Parse the response
|
|
$data = $this->preProcess_all($this->packets_response[self::PACKET_ALL]);
|
|
|
|
// Create a new buffer
|
|
$buf = new GameQ_Buffer($data);
|
|
|
|
// We go until we hit an empty key
|
|
while($buf->getLength())
|
|
{
|
|
$key = $buf->readString();
|
|
|
|
if (strlen($key) == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
$result->add($key, $buf->readString());
|
|
}
|
|
|
|
// Now we need to offload to parse the remaining data, player and team information
|
|
$this->parsePlayerTeamInfo($buf, $result);
|
|
|
|
// Return the result
|
|
return $result->fetch();
|
|
}
|
|
|
|
protected function delete_result(&$result, $array)
|
|
{
|
|
foreach($array as $key)
|
|
{
|
|
unset($result[$key]);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
protected function move_result(&$result, $old, $new)
|
|
{
|
|
if (isset($result[$old]))
|
|
{
|
|
$result[$new] = $result[$old];
|
|
unset($result[$old]);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Parse the player and team information but do it smartly. Update to the old parseSub method.
|
|
*
|
|
* @param GameQ_Buffer $buf
|
|
* @param GameQ_Result $result
|
|
*/
|
|
protected function parsePlayerTeamInfo(GameQ_Buffer &$buf, GameQ_Result &$result)
|
|
{
|
|
/*
|
|
* Explode the data into groups. First is player, next is team (item_t)
|
|
*
|
|
* Each group should be as follows:
|
|
*
|
|
* [0] => item_
|
|
* [1] => information for item_
|
|
* ...
|
|
*/
|
|
$data = explode("\x00\x00", $buf->getBuffer());
|
|
|
|
// By default item_group is blank, this will be set for each loop thru the data
|
|
$item_group = '';
|
|
|
|
// By default the item_type is blank, this will be set on each loop
|
|
$item_type = '';
|
|
|
|
// Loop through all of the $data for information and pull it out into the result
|
|
for($x=0; $x < count($data)-1; $x++)
|
|
{
|
|
// Pull out the item
|
|
$item = $data[$x];
|
|
|
|
// If this is an empty item, move on
|
|
if($item == '' || $item == "\x00")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* Left as reference:
|
|
*
|
|
* Each block of player_ and team_t have preceeding junk chars
|
|
*
|
|
* player_ is actually \x01player_
|
|
* team_t is actually \x00\x02team_t
|
|
*
|
|
* Probably a by-product of the change to exploding the data from the original.
|
|
*
|
|
* For now we just strip out these characters
|
|
*/
|
|
|
|
// Check to see if $item has a _ at the end, this is player info
|
|
if(substr($item, -1) == '_')
|
|
{
|
|
// Set the item group
|
|
$item_group = 'players';
|
|
|
|
// Set the item type, rip off any trailing stuff and bad chars
|
|
$item_type = rtrim(str_replace("\x01", '', $item), '_');
|
|
}
|
|
// Check to see if $item has a _t at the end, this is team info
|
|
elseif(substr($item, -2) == '_t')
|
|
{
|
|
// Set the item group
|
|
$item_group = 'teams';
|
|
|
|
// Set the item type, rip off any trailing stuff and bad chars
|
|
$item_type = rtrim(str_replace(array("\x00", "\x02"), '', $item), '_t');
|
|
}
|
|
// We can assume it is data belonging to a previously defined item
|
|
else
|
|
{
|
|
// Make a temp buffer so we have easier access to the data
|
|
$buf_temp = new GameQ_Buffer($item);
|
|
|
|
// Get the values
|
|
while ($buf_temp->getLength())
|
|
{
|
|
// No value so break the loop, end of string
|
|
if (($val = $buf_temp->readString()) === '')
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Add the value to the proper item in the correct group
|
|
$result->addSub($item_group, $item_type, trim($val));
|
|
}
|
|
|
|
// Unset out buffer
|
|
unset($buf_temp);
|
|
}
|
|
}
|
|
|
|
// Free up some memory
|
|
unset($data, $item, $item_group, $item_type, $val);
|
|
}
|
|
|
|
/**
|
|
* Parse the player and team info
|
|
*
|
|
* @param GameQ_Buffer $buf
|
|
* @param GameQ_Result $result
|
|
* @throws GameQ_ProtocolsException
|
|
* @return boolean
|
|
*/
|
|
protected function parsePlayerTeamInfoNew(GameQ_Buffer &$buf, GameQ_Result &$result)
|
|
{
|
|
/**
|
|
* Player info is always first, team info (if defined) is second.
|
|
*
|
|
* Reference:
|
|
*
|
|
* Player info is preceeded by a hex code of \x01
|
|
* Team info is preceeded by a hex code of \x02
|
|
*/
|
|
|
|
// Check the header to make sure the player data is proper
|
|
if($buf->read(1) != "\x01")
|
|
{
|
|
//throw new GameQ_ProtocolsException("First character in player buffer != '\x01'");
|
|
return FALSE;
|
|
}
|
|
|
|
// Offload the player parsing
|
|
$this->parseSubInfo('players', $buf->readString("\x00\x00\x00"), $result);
|
|
|
|
// Check to make sure we have team information
|
|
if($buf->getLength() >= 6)
|
|
{
|
|
// Burn chars
|
|
$buf->skip(2);
|
|
|
|
// Check the header to make sure the data is proper
|
|
if($buf->read(1) != "\x02")
|
|
{
|
|
//throw new GameQ_ProtocolsException("First character in team buffer != '\x02'");
|
|
return FALSE;
|
|
}
|
|
|
|
// Offload the team parsing
|
|
$this->parseSubInfo('teams', $buf->readString("\x00\x00\x00"), $result);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Parse the sub-item information for players and teams
|
|
*
|
|
* @param string $section
|
|
* @param string $data
|
|
* @param GameQ_Result $result
|
|
* @return boolean
|
|
*/
|
|
protected function parseSubInfo($section, $data, GameQ_Result &$result)
|
|
{
|
|
/*
|
|
* Explode the items so we can iterate easier
|
|
*
|
|
* Items should split up as follows:
|
|
*
|
|
* [0] => item_
|
|
* [1] => data for item_
|
|
* [2] => item2_
|
|
* [3] => data for item2_
|
|
* ...
|
|
*/
|
|
$items = explode("\x00\x00", $data);
|
|
|
|
print_r($items);
|
|
|
|
// Loop through all of the items
|
|
for($x = 0; $x < count($items); $x += 2)
|
|
{
|
|
// $x is always the key for the item (i.e. player_, ping_, team_, score_, etc...)
|
|
$item_type = rtrim($items[$x], '_,_t');
|
|
|
|
// $x+1 is always the data for the above item
|
|
// Make a temp buffer so we have easier access to the data
|
|
$buf_temp = new GameQ_Buffer($items[$x+1]);
|
|
|
|
// Get the values
|
|
while ($buf_temp->getLength())
|
|
{
|
|
// No value so break the loop, end of string
|
|
if (($val = $buf_temp->readString()) === '')
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Add the value to the proper item in the correct group
|
|
$result->addSub($section, $item_type, trim($val));
|
|
}
|
|
|
|
// Unset out buffer
|
|
unset($buf_temp, $val);
|
|
}
|
|
|
|
unset($x, $items, $item_type);
|
|
|
|
return TRUE;
|
|
}
|
|
}
|