Code: Server Query Classes |
Code: Server Query Classes |
Apr 5 2010, 12:06 PM
Post
#1
|
|
Security and Projects Group: Clan Dogsbody Posts: 4,687 Thank(s): 1098 Points: 2,440 Joined: 31-August 07 From: A Magical Place, with toys in the million, all under one roof Member No.: 1 |
Below is the most updated class file for querying and executing commands against a BC2 server. Am using this as a basic for database population, thought some people (dan maybe) might find it useful for development
CODE <?php
/** * * <b>official threads:</b><br /> * http://www.bfbc2admin.com/phpbb/viewtopic.php?f=9&t=220&start=0 <b>(english)</b><br /> * http://www.fpsadmin.com/forum/showthread.php?t=20045 <b>(english)</b><br /> * <br /> * <b>project page:</b><br /> * http://www.jlnx.de/BC2Conn/<br /> * <br /> * <b>API:</b><br /> * http://www.jlnx.de/BC2Conn/API/ * * @author JLNNN <JLN@hush.ai> * @version 1.41b */ class BC2Conn { private $_serverIP = null; private $_serverRconQueryPort = null; private $_clientSequenceNr = 0; private $_sock = null; private $_connection = false; /** * @param String * @param Integer * * @return boolean true = connection successful, false = connection failed */ function __construct($serverIP, $serverRconQueryPort) { if ($this->_serverIP == null) { $this->_serverIP = $serverIP; $this->_serverRconQueryPort = $serverRconQueryPort; $this->_connection = $this->_openConnection(); } return $this->_connection; } /** * @return void */ function __destruct() { if ($this->_connection) { $this->_closeConnection(); $this->_connection = false; } } /* required methods for communicating with the gameserver */ private function _encodeClientRequest($data) { $packet = $this->_encodePacket(false, false, $this->_clientSequenceNr, $data); $this->_clientSequenceNr++; return $packet; } private function _encodeHeader($isFromServer, $isResponse, $sequence) { $header = $sequence & 0x3fffffff; if ($isFromServer) { $header += 0x80000000; } if ($isResponse) { $header += 0x40000000; } return pack('I', $header); } private function _decodeHeader($data) { $header = unpack('I', $data); return array ( $header & 0x80000000, $header & 0x40000000, $header & 0x3fffffff ); } private function _encodeInt32($size) { return pack('I', $size); } private function _decodeInt32($data) { $decode = unpack('I', $data); return $decode[1]; } private function _encodeWords($words) { $size = 0; $encodedWords = ''; foreach ($words as $word) { $strWord = $word; $encodedWords .= $this->_encodeInt32(strlen($strWord)); $encodedWords .= $strWord; $encodedWords .= "\x00"; $size += strlen($strWord) + 5; } return array ( $size, $encodedWords ); } private function _decodeWords($size, $data) { $numWords = $this->_decodeInt32($data); $offset = 0; while ($offset < $size) { $wordLen = $this->_decodeInt32(substr($data, $offset, 4)); $word = substr($data, $offset +4, $wordLen); $words[] = $word; $offset += $wordLen +5; } return $words; } private function _encodePacket($isFromServer, $isResponse, $sequence, $data) { $data = explode(' ', $data); if ($data[0] == "admin.yell") { $dataCount = count($data) - 1; $adminYell = array ( $data[0] ); $adminYell[1] = ""; foreach ($data as $key => $value) { if ($key != 0 && $key != $dataCount -1 && $key != $dataCount) { $adminYell[1] .= $value . " "; } } $adminYell[1] = trim($adminYell[1]); $adminYell[2] = $data[$dataCount -1]; $adminYell[3] = $data[$dataCount]; if (strtolower($adminYell[3]) != "all") { $adminYell[4] = $adminYell[3]; $adminYell[3] = "player"; } $data = $adminYell; } else { if ($data[0] == "vars.serverDescription" && isset ($data[1])) { $dataCount = count($data) - 1; $serverDesc = array ( $data[0] ); $serverDesc[1] = ""; foreach ($data as $key => $value) { if ($key != 0) { $serverDesc[1] .= $value . " "; } } $serverDesc[1] = trim($serverDesc[1]); $data = $serverDesc; } else if ($data[0] == "admin.kickPlayer" && isset ($data[1])) { $dataCount = count($data) - 1; $kickPlayer = array ( $data[0] ); $kickPlayer[1] = ""; foreach ($data as $key => $value) { if ($key != 0) { $kickPlayer[1] .= $value . " "; } } $kickPlayer[1] = trim($kickPlayer[1]); $data = $kickPlayer; } else if ($data[0] == "admin.banPlayer" && isset ($data[1])) { $dataCount = count($data) - 1; $banPlayer = array ( $data[0] ); $banPlayer[1] = ""; foreach ($data as $key => $value) { if ($key != 0 && $key != $dataCount) { $banPlayer[1] .= $value . " "; } } $banPlayer[1] = trim($banPlayer[1]); // trim ending whitespace $banPlayer[2] = $data[$dataCount]; $data = $banPlayer; } } $encodedHeader = $this->_encodeHeader($isFromServer, $isResponse, $sequence); $encodedNumWords = $this->_encodeInt32(count($data)); list ($wordsSize, $encodedWords) = $this->_encodeWords($data); $encodedSize = $this->_encodeInt32($wordsSize +12); return $encodedHeader . $encodedSize . $encodedNumWords . $encodedWords; } private function _decodePacket($data) { list ($isFromServer, $isResponse, $sequence) = $this->_decodeHeader($data); $wordsSize = $this->_decodeInt32(substr($data, 4, 4)) - 12; $words = $this->_decodeWords($wordsSize, substr($data, 12)); return array ( $isFromServer, $isResponse, $sequence, $words ); } private function _hex_str($hex) { $string = ''; for ($i = 0; $i < strlen($hex) - 1; $i += 2) { $string .= chr(hexdec($hex[$i] . $hex[$i +1])); } return $string; } private function _bool2String($boolean) { $onOrOff = ""; if ($boolean) { $onOrOff = "true"; } else { $onOrOff = "false"; } return $onOrOff; } private function _array2String($array, $key = 1) { return $array[$key]; } /* internal methods */ private function _openConnection() { $this->_sock = fsockopen("tcp://" . $this->_serverIP, $this->_serverRconQueryPort); if ($this->_sock != false) { socket_set_timeout($this->_sock, 0, 500000); return true; } return false; } private function _closeConnection() { $this->_clientRequest("quit"); fclose($this->_sock); } private function _clientRequest($clientRequest) { fwrite($this->_sock, $this->_encodeClientRequest($clientRequest)); list ($isFromServer, $isResponse, $sequence, $requestAnswer) = $this->_decodePacket(fread($this->_sock, 4096)); return $requestAnswer; } private function adminRequestGamePassword() { return $this->_clientRequest("vars.gamePassword"); } /* login & logout */ /** * plain text login to gameserver * * @param String * * @return array */ function loginInsecure($rconPassword) { return $this->_clientRequest("login.plainText " . $rconPassword); } /** * salted hash login to gameserver * * @param String * * @return array */ function loginSecure($rconPassword) { $salt = $this->_clientRequest("login.hashed"); $hashedPW = $this->_hex_str($salt[1]) . $rconPassword; $saltedHashedPW = strtoupper(md5($hashedPW)); return $this->_clientRequest("login.hashed " . $saltedHashedPW); } /** * logging out * * @return array */ function logout() { return $this->_clientRequest("logout"); } /* some replacements */ /** * returns the name of the given map<br /><br /> * example: getMapName("Levels/MP_002"); * * @param String * * @return String name of the given map */ function getMapName($mapURI) { $mapNamesXML = simplexml_load_file("mapNames.xml"); for ($i = 0; $i <= (count($mapNamesXML->map) - 1); $i++) { if ($mapURI == $mapNamesXML->map[$i]->attributes()->uri) { $mapName = $mapNamesXML->map[$i]->attributes()->name; } } return $mapName; } /** * returns the name of the given playmode<br /><br /> * example: getPlaymodeName("RUSH"); * * @param String * * @return String name of the given playmode */ function getPlaymodeName($playmodeURI) { $playModesXML = simplexml_load_file("playModes.xml"); for ($i = 0; $i <= (count($playModesXML->playmode) - 1); $i++) { if ($playmodeURI == $playModesXML->playmode[$i]->attributes()->uri) { $playmodeName = $playModesXML->playmode[$i]->attributes()->name; } } return $playmodeName; } /** * returns the name of the given squad<br /><br /> * example: getSquadName(1); * * @param Integer * * @return String */ function getSquadName($squadID) { $squadNamesXML = simplexml_load_file("squadNames.xml"); if ($squadID == 24) { $squadName = $squadNamesXML->squad[8]->attributes()->name; } else { for ($i = 0; $i < (count($squadNamesXML->squad) - 1); $i++) { if ($squadID == $squadNamesXML->squad[$i]->attributes()->id) { $squadName = $squadNamesXML->squad[$i]->attributes()->name; } } } return $squadName; } /** * gets the teamname of a given map, playmode and squad * * @param String * @param String * @param Integer * * @return String */ function getTeamName($mapURI, $playmodeURI, $squadID) { $teamNameXML = simplexml_load_file("teamNames.xml"); $teamName = "TeamNameNotFoundError"; if ($mapURI == "Levels/MP_012GR") { // xml case 'Levels/MP_012GR' if (count($teamNameXML->teamName[1]->playMode[0]) - 1 >= $squadID) { $teamName = $teamNameXML->teamName[1]->playMode[0]->team[$squadID]->name; } } else if ($mapURI != "Levels/MP_012GR" && $playmodeURI == "SQDM") { // xml case 'SQDM' if (count($teamNameXML->teamName[2]->playMode[0]) - 1 >= $squadID) { $teamName = $teamNameXML->teamName[2]->playMode[0]->team[$squadID]->name; } } else { // xml case 'default' $playModes = count($teamNameXML->teamName[0]) - 1; if (count($teamNameXML->teamName[0]) - 1 >= $squadID) { for ($i = 0; $i < $playModes; $i++) { if ($teamNameXML->teamName[0]->playMode[$i]->attributes()->uri == $playmodeURI) { $teamName = $teamNameXML->teamName[0]->playMode[$i]->team[$squadID]->name; } } } } return $teamName; } /* server information */ /** * returns the server ip as a string * * @return String */ function getServerIP() { return $this->_serverIP; } /** * returns the server information as an array * * @return array */ function getServerInfo() { return $this->_clientRequest("serverInfo"); } /** * returns the server name as a string * * @return String */ function getServerName() { $serverInfo = $this->getServerInfo(); return $serverInfo[1]; } /** * returns the current players on server as a string * * @return String */ function getCurrentPlayers() { $serverInfo = $this->getServerInfo(); return $serverInfo[2]; } /** * returns the max amount of players allowed on server as a string * * @return String */ function getMaxPlayers() { $serverInfo = $this->getServerInfo(); return $serverInfo[3]; } /** * returns the current playmode as a string * * @return String */ function getCurrentPlaymode() { $serverInfo = $this->getServerInfo(); return $serverInfo[4]; } /** * returns the current playmod (human readable) as a string * * @return String */ function getCurrentPlaymodeName() { $serverInfo = $this->getServerInfo(); return $this->getPlaymodeName($serverInfo[4]); } /** * returns the current map as a string * * @return String */ function getCurrentMap() { $serverInfo = $this->getServerInfo(); return $serverInfo[5]; } /** * returns the current map (human readable) as a string * * @return String */ function getCurrentMapName() { $serverInfo = $this->getServerInfo(); return $this->getMapName($serverInfo[5]); } /** * returns the server version as an array * * @return array */ function getVersion() { $serverVersion = $this->_clientRequest("version"); return $serverVersion; } /* admin server information */ /** * returns the gamepassword as a string * * @return String */ function adminGetGamePassword() { $gamePassword = $this->adminRequestGamePassword(); return $gamePassword[1]; } /** * gets the information about a given player (or all) * * @param String (optional) * * @return array */ function adminGetPlayerList($playerName = "all") { return $this->_clientRequest("admin.listPlayers " . $playerName); } /** * returns all commands available on the server - requires login * * @return array */ function adminGetAllCommands() { return $this->_clientRequest("help"); } /** * returns true/false, if server events are enabled in this connection or * not * * @return array */ function getEventsEnabledStatus() { return $this->_clientRequest("eventsEnabled"); } /** * sets the server events on/off in this connection * * @return array */ //function setEventsEnabledStatus($boolean) { // return $this->_clientRequest("eventsEnabled " . // $this->_bool2String($boolean)); //} /* admin commands */ /** * sends an admin-yell message to the specified player * * TODO: Need fix for sending messages to squads / team * * @param String * @param String (optional) - if not set, message will be sent to all * players * @param Integer (optional) - amount of time the message will be displayed, * must be 1-59999 * * @return array */ function adminYellMessage($text, $playerName = "all", $durationInMS = 59999) { return $this->_clientRequest("admin.yell " . $text . " " . $durationInMS . " " . $playerName); } /** * runs the next level on maplist * * @return array */ function adminRunNextLevel() { return $this->_clientRequest("admin.runNextLevel"); } /** * @see getCurrentMap(); * * @return array */ //function adminGetCurrentMapName() { // return $this->_clientRequest("admin.currentLevel"); //} /** * sets the next level to play<br /><br /> * ##QA: Not working. * * @param String * * @return array */ //function adminSetNextLevel($mapURI) { // return $this->_clientRequest("admin.nextLevel " . $mapURI); //} /** * restarts the current level * * @return array */ function adminRestartMap() { return $this->_clientRequest("admin.restartMap"); } /** * sets a new playlist<br /><br /> * example: adminSetPlaylist("SQDM"); * * @param String * * @return array */ function adminSetPlaylist($playmodeURI) { return $this->_clientRequest("admin.setPlaylist " . $playmodeURI); } /** * returns all available playmodes on server * * @return array */ function adminGetPlaylists() { return $this->_clientRequest("admin.getPlaylists"); } /** * returns current playmode on server * * @see getCurrentPlaymode(); * * @return array */ function adminGetPlaylist() { return $this->_clientRequest("admin.getPlaylist"); } /** * loads the maplist (needed for adminGetMaplist()) * * @see adminGetMapList() * * @return array */ function adminLoadMaplist() { return $this->_clientRequest("mapList.load"); } /** * saves the current maplist to file * * @return array */ function adminSaveMaplist() { return $this->_clientRequest("mapList.save"); } /** * reads the maplist from map file * * @return array */ function adminGetMaplist() { $this->adminLoadMaplist(); return $this->_clientRequest("mapList.list"); } /** * clears the map file * * @return array */ function adminClearMaplist() { return $this->_clientRequest("mapList.clear"); } /** * removes a given map from maplist * * @param String * * @return array */ function adminRemoveMapFromList($mapURI) { return $this->_clientRequest("mapList.remove " . $mapURI); } /** * appends a given map to maplist * * @param String * * @return array */ function adminAddMapToList($mapURI) { return $this->_clientRequest("mapList.append " . $mapURI); } /* admin server settings */ /** * sets a new password on the gameserver.<br /><br /> * to clear the password, use adminSetGamePassword(""); * * @param String * * @return array */ function adminSetGamePassword($serverPassword) { return $this->_clientRequest("vars.gamePassword " . $serverPassword); } /** * sets punkbuster on/off * * @param boolean * * @return array */ function adminVarSetPunkbuster($boolean) { return $this->_clientRequest("vars.punkBuster " . $this->_bool2String($boolean)); } /** * gets true/false, if punkbuster is enabled or not * * @return String */ function adminVarGetPunkbuster() { return $this->_array2String($this->_clientRequest("vars.punkBuster")); } /** * sets the admin (rcon) password<br /> * [ haven't tested this ] * * @param String * * @return array */ //function adminVarSetAdminPassword($string) { // return $this->_clientRequest("vars.adminPassword " . // $this->_bool2String($boolean)); //} /** * gets the admin (rcon) password<br /> * [ i only get the result: PasswordRetrievalNotAllowed ] * * @return array */ //function adminVarGetAdminPassword() { // return $this->_clientRequest("vars.adminPassword"); //} /** * sets hardcore mode on/off * * @param boolean * * @return array */ function adminVarSetHardcore($boolean) { return $this->_clientRequest("vars.hardCore " . $this->_bool2String($boolean)); } /** * gets true/false, if hardcore mode is enabled or not * * @return String */ function adminVarGetHardcore() { return $this->_array2String($this->_clientRequest("vars.hardCore")); } /** * sets ranked server on/off * * @param boolean * * @return array */ function adminVarSetRanked($boolean) { return $this->_clientRequest("vars.ranked " . $this->_bool2String($boolean)); } /** * gets value of ranked server settings * * @return String */ function adminVarGetRanked() { return $this->_array2String($this->_clientRequest("vars.ranked")); } /** * sets the max rank limit players are allowed to join<br /><br /> * ##QA: Says 'OK' but still allow higher ranked players to join. * * @param Integer * * @return array */ function adminVarSetRankLimit($integer) { return $this->_clientRequest("vars.rankLimit " . $integer); } /** * gets rank limit set on server * * @return String */ function adminVarGetRankLimit() { return $this->_array2String($this->_clientRequest("vars.rankLimit")); } /** * sets teambalance on/off * * @param boolean * * @return array */ function adminVarSetTeambalance($boolean) { return $this->_clientRequest("vars.teamBalance " . $this->_bool2String($boolean)); } /** * gets true/false, if teambalance is enabled or not * * @return String */ function adminVarGetTeambalance() { return $this->_array2String($this->_clientRequest("vars.teamBalance")); } /** * sets friendly fire on/off * * @param boolean * * @return array */ function adminVarSetFriendlyFire($boolean) { return $this->_clientRequest("vars.friendlyFire " . $this->_bool2String($boolean)); } /** * gets true/false, if friendly fire is enabled or not * * @return String */ function adminVarGetFriendlyFire() { return $this->_array2String($this->_clientRequest("vars.friendlyFire")); } /** * sets the banner url to given address * * @param String * * @return array */ function adminVarSetBannerURL($string) { return $this->_clientRequest("vars.bannerUrl " . $string); } /** * gets the banner url * * @return String */ function adminVarGetBannerURL() { return $this->_array2String($this->_clientRequest("vars.bannerUrl")); } /** * sets the server description to given text * * @param String * * @return array */ function adminVarSetServerDescription($string) { return $this->_clientRequest("vars.serverDescription " . $string); } /** * gets the server description * * @return String */ function adminVarGetServerDescription() { return $this->_array2String($this->_clientRequest("vars.serverDescription")); } /** * sets killcam on/off * * @param boolean * * @return array */ function adminVarSetKillCam($boolean) { return $this->_clientRequest("vars.killCam " . $this->_bool2String($boolean)); } /** * gets true/false, if killcam is enabled or not * * @return String */ function adminVarGetKillCam() { return $this->_array2String($this->_clientRequest("vars.killCam")); } /** * sets minimap on/off * * @param boolean * * @return array */ function adminVarSetMiniMap($boolean) { return $this->_clientRequest("vars.miniMap " . $this->_bool2String($boolean)); } /** * gets true/false, if minimap is enabled or not * * @return String */ function adminVarGetMiniMap() { return $this->_array2String($this->_clientRequest("vars.miniMap")); } /** * sets crosshair on/off * * @param boolean * * @return array */ function adminVarSetCrosshair($boolean) { return $this->_clientRequest("vars.crossHair " . $this->_bool2String($boolean)); } /** * gets true/false, if crosshair is enabled or not * * @return String */ function adminVarGetCrosshair() { return $this->_array2String($this->_clientRequest("vars.crossHair")); } /** * sets 3d spotting on maps on/off * * @param boolean * * @return array */ function adminVarSet3dSpotting($boolean) { return $this->_clientRequest("vars.3dSpotting " . $this->_bool2String($boolean)); } /** * gets true/false, if 3d spotting is enabled or not * * @return String */ function adminVarGet3dSpotting() { return $this->_array2String($this->_clientRequest("vars.3dSpotting")); } /** * sets minimap spotting on/off * * @param boolean * * @return array */ function adminVarSetMiniMapSpotting($boolean) { return $this->_clientRequest("vars.miniMapSpotting " . $this->_bool2String($boolean)); } /** * gets true/false, if minimap spotting is enabled or not * * @return String */ function adminVarGetMiniMapSpotting() { return $this->_array2String($this->_clientRequest("vars.miniMapSpotting")); } /** * sets the 3rd person vehicle cam on/off<br /><br /> * ##QA: Works but is bugged. If you change the setting and someone is in * a vehicle in 3rd person view when at end of round, that player will be * stuck in 3rd person view even though the setting should only allow 1st * person view. * * @param boolean * * @return array */ function adminVarSet3rdPersonVehiCam($boolean) { return $this->_clientRequest("vars.thirdPersonVehicleCameras " . $this->_bool2String($boolean)); } /** * gets true/false, if 3rd person vehicle cam is enabled or not * * @return String */ function adminVarGet3rdPersonVehiCam() { return $this->_array2String($this->_clientRequest("vars.thirdPersonVehicleCameras")); } /** * gets maplist of maps supported by given playmode<br /><br /> * ##QA: Does not give maps names. * * @param String * * @return array */ //function adminGetSupportedMaps($playlist) { // return $this->_clientRequest("admin.supportedMaps " . $playlist); //} /** * kicks a specified player by playername * * @param String * * @return array */ function adminKickPlayer($playerName) { return $this->_clientRequest("admin.kickPlayer " . $playerName); } /** * bans a specified player by playername for a given range of time.<br /> * range of time can be: perm = permanent, round = current round<br /> * if no range is given, the player will be banned for the current round<br /> * [I wasn't able to ban a player for xx seconds - maybe DICE made a mistake?] * * @param String * @param String (optional) - if not set, given player will be banned permanently * * @return array */ function adminBanPlayer($playerName, $timerange = "perm") { return $this->_clientRequest("admin.banPlayer " . $playerName . " " . $timerange); } /** * bans a specified player by given playerip * range of time can be: perm = permanent, round = current round<br /> * if no range is given, the player will be banned for the current round<br /> * [I wasn't able to ban an ip for xx seconds - maybe DICE made a mistake?] * * @param String * @param String (optional) - if not set, ip will be banned permanently * * @return array */ function adminBanIP($playerIP, $timerange = "perm") { return $this->_clientRequest("admin.banIP " . $playerIP . " " . $timerange); } /** * unbans a player by playername * * @param String * * @return array */ function adminUnbanPlayer($playerName) { return $this->_clientRequest("admin.unbanPlayer " . $playerName); } /** * unbans a player by playerip * * @param String * * @return array */ function adminUnbanIP($playerIP) { return $this->_clientRequest("admin.unbanIP " . $playerIP); } /** * clears all bans from playername banlist * * @return array */ function adminClearPlayerBanList() { return $this->_clientRequest("admin.clearPlayerBanList"); } /** * clears all bans from playerip banlist * * @return array */ function adminClearIPBanList() { return $this->_clientRequest("admin.clearIPBanList"); } /** * lists all bans from playername banlist<br /> * [Only worked sometimes for me - ?]<br /><br /> * ##QA: The list is currently a single, long string in a very ugly format. * * @return array */ function adminListPlayerBans() { return $this->_clientRequest("admin.listPlayerBans"); } /** * lists all bans from playerip banlist<br /> * [Only worked sometimes for me - ?]<br /><br /> * ##QA: The list is currently a single, long string in a very ugly format. * * @return array */ function adminListIPBans() { return $this->_clientRequest("admin.listIPBans"); } /** * loads the file containing all reserved slots<br /> * [I don't know if this method is useful..] * * @return array */ function adminReservedSlotsLoad() { return $this->_clientRequest("reservedSlots.load"); } /** * saves made changes to reserved slots file * * @return array */ function adminReservedSlotsSave() { return $this->_clientRequest("reservedSlots.save"); } /** * adds a player by given playername to reserved slots file * * @param String * * @return array */ function adminReservedSlotsAddPlayer($playerName) { return $this->_clientRequest("reservedSlots.addPlayer " . $playerName); } /** * removes a player by given playername from reserved slots file * * @param String * * @return array */ function adminReservedSlotsRemovePlayer($playerName) { return $this->_clientRequest("reservedSlots.removePlayer " . $playerName); } /** * clears the file containing all reserved slots * * @return array */ function adminReservedSlotsClear() { return $this->_clientRequest("reservedSlots.clear"); } /** * lists all playernames in reserved slots file * * @return array */ function adminReservedSlotsList() { return $this->_clientRequest("reservedSlots.list"); } } ?> -------------------- |
|
|
Jun 14 2021, 07:56 AM
Post
#21
|
|
Knight Lieutenant Group: Trusted non-SM Posts: 921 Thank(s): 524 Points: 921 Joined: 9-March 16 From: Bulgaria Member No.: 4,846 |
You should take this more seriously! People are relying on you and yet you let this happen. I cannot tell you how disappointed I am right now.
|
|
|
The Following 2 Users Say Thank You to Sir Robin (not so brave) For This Useful Post: hardworkingcrackwhore, Marsche |
Lo-Fi Version | Time is now: 21st November 2024 - 09:46 PM |