1: <?php
2: /**
3: * This file is part of GameQ.
4: *
5: * GameQ is free software; you can redistribute it and/or modify
6: * it under the terms of the GNU Lesser General Public License as published by
7: * the Free Software Foundation; either version 3 of the License, or
8: * (at your option) any later version.
9: *
10: * GameQ is distributed in the hope that it will be useful,
11: * but WITHOUT ANY WARRANTY; without even the implied warranty of
12: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13: * GNU Lesser General Public License for more details.
14: *
15: * You should have received a copy of the GNU Lesser General Public License
16: * along with this program. If not, see <http://www.gnu.org/licenses/>.
17: */
18:
19: namespace GameQ\Protocols;
20:
21: use GameQ\Buffer;
22: use GameQ\Exception\Protocol as Exception;
23: use GameQ\Helpers\Str;
24: use GameQ\Protocol;
25: use GameQ\Result;
26: use GameQ\Server;
27:
28: /**
29: * San Andreas Multiplayer Protocol Class (samp)
30: *
31: * Note:
32: * Player information will not be returned if player count is over 256
33: *
34: * @author Austin Bischoff <austin@codebeard.com>
35: */
36: class Samp extends Protocol
37: {
38: /**
39: * Array of packets we want to look up.
40: * Each key should correspond to a defined method in this or a parent class
41: *
42: * @var array
43: */
44: protected $packets = [
45: self::PACKET_STATUS => "SAMP%si",
46: self::PACKET_PLAYERS => "SAMP%sd",
47: self::PACKET_RULES => "SAMP%sr",
48: ];
49:
50: /**
51: * Use the response flag to figure out what method to run
52: *
53: * @var array
54: */
55: protected $responses = [
56: "\x69" => "processStatus", // i
57: "\x64" => "processPlayers", // d
58: "\x72" => "processRules", // r
59: ];
60:
61: /**
62: * The query protocol used to make the call
63: *
64: * @var string
65: */
66: protected $protocol = 'samp';
67:
68: /**
69: * String name of this protocol class
70: *
71: * @var string
72: */
73: protected $name = 'samp';
74:
75: /**
76: * Longer string name of this protocol class
77: *
78: * @var string
79: */
80: protected $name_long = "San Andreas Multiplayer";
81:
82: /**
83: * Holds the calculated server code that is passed when querying for information
84: *
85: * @var string
86: */
87: protected $server_code = null;
88:
89: /**
90: * The client join link
91: *
92: * @var string
93: */
94: protected $join_link = "samp://%s:%d/";
95:
96: /**
97: * Normalize settings for this protocol
98: *
99: * @var array
100: */
101: protected $normalize = [
102: // General
103: 'general' => [
104: // target => source
105: 'dedicated' => 'dedicated',
106: 'hostname' => ['hostname', 'servername'],
107: 'mapname' => 'mapname',
108: 'maxplayers' => 'max_players',
109: 'numplayers' => 'num_players',
110: 'password' => 'password',
111: ],
112: // Individual
113: 'player' => [
114: 'name' => 'name',
115: 'score' => 'score',
116: 'ping' => 'ping',
117: ],
118: ];
119:
120: /**
121: * Handle some work before sending the packets out to the server
122: *
123: * @param \GameQ\Server $server
124: */
125: public function beforeSend(Server $server)
126: {
127: // Build the server code
128: $this->server_code = implode('', array_map('chr', explode('.', $server->ip()))) .
129: pack("S", $server->portClient());
130:
131: // Loop over the packets and update them
132: foreach ($this->packets as $packetType => $packet) {
133: // Fill out the packet with the server info
134: $this->packets[$packetType] = sprintf($packet, $this->server_code);
135: }
136: }
137:
138: /**
139: * Process the response
140: *
141: * @return array
142: * @throws \GameQ\Exception\Protocol
143: */
144: public function processResponse()
145: {
146: // Results that will be returned
147: $results = [];
148:
149: // Get the length of the server code so we can figure out how much to read later
150: $serverCodeLength = strlen($this->server_code);
151:
152: // We need to pre-sort these for split packets so we can do extra work where needed
153: foreach ($this->packets_response as $response) {
154: // Make new buffer
155: $buffer = new Buffer($response);
156:
157: // Check the header, should be SAMP
158: if (($header = $buffer->read(4)) !== 'SAMP') {
159: throw new Exception(__METHOD__ . " header response '{$header}' is not valid");
160: }
161:
162: // Check to make sure the server response code matches what we sent
163: if ($buffer->read($serverCodeLength) !== $this->server_code) {
164: throw new Exception(__METHOD__ . " code check failed.");
165: }
166:
167: // Figure out what packet response this is for
168: $response_type = $buffer->read(1);
169:
170: // Figure out which packet response this is
171: if (!array_key_exists($response_type, $this->responses)) {
172: throw new Exception(__METHOD__ . " response type '{$response_type}' is not valid");
173: }
174:
175: // Now we need to call the proper method
176: $results = array_merge(
177: $results,
178: call_user_func_array([$this, $this->responses[$response_type]], [$buffer])
179: );
180:
181: unset($buffer);
182: }
183:
184: return $results;
185: }
186:
187: // Internal methods
188:
189: /**
190: * Handles processing the server status data
191: *
192: * @param \GameQ\Buffer $buffer
193: * @return array
194: * @throws \GameQ\Exception\Protocol
195: */
196: protected function processStatus(Buffer $buffer)
197: {
198: // Set the result to a new result instance
199: $result = new Result();
200:
201: // Always dedicated
202: $result->add('dedicated', 1);
203:
204: // Pull out the server information
205: $result->add('password', $buffer->readInt8());
206: $result->add('num_players', $buffer->readInt16());
207: $result->add('max_players', $buffer->readInt16());
208:
209: // These are read differently for these last 3
210: $result->add('servername', Str::isoToUtf8($buffer->read($buffer->readInt32())));
211: $result->add('gametype', $buffer->read($buffer->readInt32()));
212: $result->add('language', $buffer->read($buffer->readInt32()));
213:
214: unset($buffer);
215:
216: return $result->fetch();
217: }
218:
219: /**
220: * Handles processing the player data into a usable format
221: *
222: * @param \GameQ\Buffer $buffer
223: * @return array
224: * @throws \GameQ\Exception\Protocol
225: */
226: protected function processPlayers(Buffer $buffer)
227: {
228: // Set the result to a new result instance
229: $result = new Result();
230:
231: // Number of players
232: $result->add('num_players', $buffer->readInt16());
233:
234: // Run until we run out of buffer
235: while ($buffer->getLength()) {
236: $result->addPlayer('id', $buffer->readInt8());
237: $result->addPlayer('name', Str::isoToUtf8($buffer->readPascalString()));
238: $result->addPlayer('score', $buffer->readInt32());
239: $result->addPlayer('ping', $buffer->readInt32());
240: }
241:
242: unset($buffer);
243:
244: return $result->fetch();
245: }
246:
247: /**
248: * Handles processing the rules data into a usable format
249: *
250: * @param \GameQ\Buffer $buffer
251: * @return array
252: * @throws \GameQ\Exception\Protocol
253: */
254: protected function processRules(Buffer $buffer)
255: {
256: // Set the result to a new result instance
257: $result = new Result();
258:
259: // Number of rules
260: $result->add('num_rules', $buffer->readInt16());
261:
262: // Run until we run out of buffer
263: while ($buffer->getLength()) {
264: $result->add($buffer->readPascalString(), $buffer->readPascalString());
265: }
266:
267: unset($buffer);
268:
269: return $result->fetch();
270: }
271: }
272: