player.php

main 434 lines · 10.5 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2
3class Player {
4
5 protected $_playerdata = array(
6 'id' => null,
7 'name' => null,
8 'world_id' => null,
9 'group_id' => null,
10 'account_id' => null,
11 'level' => null,
12 'vocation' => null,
13 'health' => null,
14 'healthmax' => null,
15 'experience' => null,
16 'lookbody' => null,
17 'lookfeet' => null,
18 'lookhead' => null,
19 'looklegs' => null,
20 'looktype' => null,
21 'lookaddons' => null,
22 'maglevel' => null,
23 'mana' => null,
24 'manamax' => null,
25 'manaspent' => null,
26 'soul' => null,
27 'town_id' => null,
28 'posx' => null,
29 'posy' => null,
30 'posz' => null,
31 'conditions' => null,
32 'cap' => null,
33 'sex' => null,
34 'lastlogin' => null,
35 'lastip' => null,
36 'save' => null,
37 'skull' => null,
38 'skulltime' => null,
39 'rank_id' => null,
40 'guildnick' => null,
41 'lastlogout' => null,
42 'blessings' => null,
43 'balance' => null,
44 'stamina' => null,
45 'direction' => null,
46 'loss_experience' => null,
47 'loss_mana' => null,
48 'loss_skills' => null,
49 'loss_containers' => null,
50 'loss_items' => null,
51 'premend' => null,
52 'online' => null,
53 'marriage' => null,
54 'promotion' => null,
55 'deleted' => null,
56 'description' => null,
57 'onlinetime' => null,
58 'deletion' => null,
59 'offlinetraining_time' => null,
60 'offlinetraining_skill' => null,
61 'skill_fist' => null,
62 'skill_fist_tries' => null,
63 'skill_club' => null,
64 'skill_club_tries' => null,
65 'skill_sword' => null,
66 'skill_sword_tries' => null,
67 'skill_axe' => null,
68 'skill_axe_tries' => null,
69 'skill_dist' => null,
70 'skill_dist_tries' => null,
71 'skill_shielding' => null,
72 'skill_shielding_tries' => null,
73 'skill_fishing' => null,
74 'skill_fishing_tries' => null,
75 );
76 protected $_znotedata = array(
77 'comment' => null,
78 'created' => null,
79 'hide_char' => null,
80 );
81 protected $_name_id = false;
82 protected $_querylog = array();
83 protected $_errors = array();
84
85 public function __construct(string|int|array $name_id_array, array|string|false $fields = false, bool $query = true) {
86
87 if (!is_array($name_id_array)) {
88 $this->_name_id = $name_id_array;
89 }
90
91 if ($name_id_array !== false) {
92
93 if (is_string($name_id_array) || is_int($name_id_array)) {
94 if ($query) {
95 $this->update($this->mysql_select($name_id_array, $fields));
96 }
97 return;
98 }
99
100 if (is_array($name_id_array)) {
101 if (isset($name_id_array['id'])) {
102 $this->_name_id = $name_id_array['id'];
103 } elseif (isset($name_id_array['name'])) {
104 $this->_name_id = $name_id_array['name'];
105 }
106
107 $this->update($name_id_array);
108 return;
109 }
110 }
111
112 throw new InvalidArgumentException(
113 'Player constructor expects string|int|array'
114 );
115 }
116
117 /**
118 * Return all player data, or the fields specified in param $fields.
119 *
120 * @param array $fields
121 * @access public
122 * @return mixed (array 'field' => 'value', or false (bool))
123 **/
124 public function fetch($fields = false) {
125 if (is_string($fields)) {
126 $fields = [$fields];
127 }
128 if ($fields !== false && !is_array($fields)) {
129 return false;
130 }
131
132 // Return all data that is not null.
133 if (!$fields) {
134 $returndata = array();
135 foreach ($this->_playerdata as $field => $value) {
136 if (!is_null($value)) $returndata[$field] = $value;
137 }
138 foreach ($this->_znotedata as $field => $value) {
139 if (!is_null($value)) $returndata[$field] = $value;
140 }
141 return $returndata;
142
143 } else {
144 // The return array
145 $returndata = array();
146
147 // Array containing null fields, we need to fetch these from db later on.
148 $missingValues = array();
149
150 // Populate the two above arrays
151 foreach ($fields as $field) {
152
153 if (array_key_exists($field, $this->_playerdata)) {
154 if (is_null($this->_playerdata[$field])) $missingValues[] = $field;
155 else $returndata[$field] = $this->_playerdata[$field];
156
157 } elseif (array_key_exists($field, $this->_znotedata)) {
158 if (is_null($this->_znotedata[$field])) $missingValues[] = $field;
159 else $returndata[$field] = $this->_znotedata[$field];
160 }
161 }
162
163 // See if we are missing any values
164 if (!empty($missingValues)) {
165 // Query for this data
166 $data = $this->mysql_select($this->_name_id, $missingValues);
167 // Update this object
168 $this->update($data);
169 foreach ($data as $field => $value) {
170 $returndata[$field] = $value;
171 }
172 }
173 return $returndata;
174 }
175 return false;
176 }
177
178 /**
179 * Update player data.
180 *
181 * @param array $fields
182 * @access public
183 * @return mixed (array, boolean)
184 **/
185 public function update(array $data): bool {
186 if (is_array($data) && !empty($data)) {
187 foreach ($data as $field => $value) {
188
189 if (array_key_exists($field, $this->_playerdata)) {
190 $this->_playerdata[$field] = $value;
191
192 } elseif (array_key_exists($field, $this->_znotedata)) {
193 $this->_znotedata[$field] = $value;
194 }
195 }
196 return true;
197 }
198 return false;
199 }
200
201 public function getErrors() {
202 return (!empty($this->_errors)) ? $this->_errors : false;
203 }
204 public function dumpErrors() {
205 if ($this->getErrors() !== false)
206 data_dump($this->getErrors(), false, "Errors detected in player class:");
207 }
208
209 /**
210 * Select player data from mysql.
211 *
212 * @param mixed (int, string) $name_id, array $fields
213 * @access private
214 * @return mixed (array, boolean)
215 **/
216 private function mysql_select($name_id, $fields = false) {
217 $table = 'players';
218 $znote_table = 'znote_players';
219 $znote_fields = array();
220
221 // Dynamic fields logic
222 switch (gettype($fields)) {
223 case 'boolean':
224 $field_elements = '*';
225 $znote_fields = array('comment', 'created', 'hide_char');
226 break;
227
228 case 'string':
229 $fields = array($fields);
230
231 case 'array':
232 // Get rid of fields related to znote_
233 foreach ($fields as $key => $field) {
234 if (!array_key_exists($field, $this->_playerdata)) {
235 $znote_fields[] = $field;
236 unset($fields[$key]);
237 }
238 }
239
240 //Since we use for loop later, we need to reindex the array if we unset something.
241 if (!empty($znote_fields)) $fields = array_values($fields);
242
243 // Add 'id' field if its not already there.
244 if (!in_array('id', $fields)) $fields[] = 'id';
245
246 // Loop through every field and generate the sql string
247 $allowedFields = array_keys($this->_playerdata + $this->_znotedata);
248
249 $safeFields = [];
250
251 foreach ($fields as $field) {
252 if (!in_array($field, $allowedFields, true)) {
253 continue;
254 }
255 $safeFields[] = "`$field`";
256 }
257 if (empty($safeFields)) {
258 return false;
259 }
260 $field_elements = implode(', ', $safeFields);
261 break;
262 }
263
264 // Value logic
265 $params = [];
266 if (is_int($name_id)) {
267 $name_id = (int)$name_id;
268 $where = "`id` = ?";
269 $params[] = $name_id;
270 } else {
271 $name_id = getValue($name_id);
272 if ($name_id === false) {
273 return false;
274 }
275 $where = "`name` = ?";
276 $params[] = $name_id;
277 }
278
279 $query = "SELECT {$field_elements} FROM `{$table}` WHERE {$where} LIMIT 1;";
280
281 // Log query to player object
282 $this->_querylog[] = $query;
283 // Fetch from players table
284 $data = db()->fetchOne($query, $params);
285 if ($data === false) {
286 return false;
287 }
288
289 unset($data['conditions']);
290
291 // Fetch from znote_players table if neccesary
292 if (!empty($znote_fields)) {
293 // Only allow known znote_players columns - the caller-supplied
294 // field list is not otherwise validated before reaching here.
295 $znoteAllowed = array_keys($this->_znotedata);
296 $safeZnoteFields = [];
297 foreach ($znote_fields as $zf) {
298 if (in_array($zf, $znoteAllowed, true)) {
299 $safeZnoteFields[] = "`{$zf}`";
300 }
301 }
302
303 if (!empty($safeZnoteFields)) {
304 $field_elements = implode(', ', $safeZnoteFields);
305 $query = "SELECT {$field_elements} FROM `{$znote_table}` WHERE `player_id` = ? LIMIT 1;";
306 $this->_querylog[] = $query;
307 $zdata = db()->fetchOne($query, [$data['id']]);
308 if (is_array($zdata)) {
309 foreach ($zdata as $field => $value) $data[$field] = $value;
310 }
311 }
312 }
313 return $data;
314 }
315
316 /**
317 * Create player.
318 *
319 * @param none
320 * @access public
321 * @return bool $status
322 **/
323 public function create(int $accountId): bool {
324 // Player already exists
325 if (!is_null($this->_playerdata['id'])) {
326 $this->_errors[] = 'Player already exists.';
327 return false;
328 }
329
330 // 🔐 SECURE POST ACCESS
331 $name = $_POST['name'] ?? null;
332 $vocation = $_POST['selected_vocation'] ?? null;
333 $town = $_POST['selected_town'] ?? null;
334 $gender = $_POST['selected_gender'] ?? null;
335
336 if (!$name || !$vocation || !$town || $gender === null) {
337 $this->_errors[] = 'Missing character creation data.';
338 return false;
339 }
340
341 // Format & validate name
342 $name = format_character_name($name);
343 $name = validate_name($name);
344 $name = sanitize($name);
345
346 if ($name === false) {
347 $this->_errors[] = 'Invalid character name.';
348 return false;
349 }
350
351 // Check name exists
352 $exist = db()->fetchOne(
353 "SELECT `id` FROM `players` WHERE `name` = ? LIMIT 1;", [$name]
354 );
355 if ($exist !== false) {
356 $this->_errors[] = "Character name already exists.";
357 return false;
358 }
359
360 $config = fullConfig();
361
362 // Validate vocation
363 if (!in_array((int)$vocation, $config['available_vocations'], true)) {
364 $this->_errors[] = 'Invalid vocation.';
365 }
366
367 // Validate town
368 if (!in_array((int)$town, $config['available_towns'], true)) {
369 $this->_errors[] = 'Invalid town.';
370 }
371
372 // Validate gender
373 if (!in_array((int)$gender, [0, 1], true)) {
374 $this->_errors[] = 'Invalid gender.';
375 }
376
377 // Stop if errors
378 if (!empty($this->_errors)) {
379 return false;
380 }
381
382 // Character count
383 $char_count = user_character_list_count($accountId);
384 if ($char_count >= $config['max_characters']) {
385 $this->_errors[] = 'Maximum characters reached.';
386 return false;
387 }
388
389 // Prepare insert data
390 $character_data = [
391 'name' => $name,
392 'account_id' => $accountId,
393 'vocation' => (int)$vocation,
394 'town_id' => (int)$town,
395 'sex' => (int)$gender,
396 'lastip' => getIPLong(),
397 'created' => time()
398 ];
399
400 array_walk($character_data, 'array_sanitize');
401
402 // Outfit
403 $character_data['looktype'] = (
404 $gender == 1
405 ? $config['maleOutfitId']
406 : $config['femaleOutfitId']
407 );
408
409 // INSERT PLAYER
410 db()->execute(
411 "INSERT INTO `players`
412 (`name`,`account_id`,`vocation`,`town_id`,`sex`,`lastip`,`created`,`looktype`)
413 VALUES
414 (?, ?, ?, ?, ?, " . sqlIpWrite($character_data['lastip']) . ", ?, ?)",
415 [
416 $character_data['name'],
417 $character_data['account_id'],
418 $character_data['vocation'],
419 $character_data['town_id'],
420 $character_data['sex'],
421 $character_data['created'],
422 $character_data['looktype'],
423 ]
424 );
425 return true;
426 }
427}
428
429/*
430$this->_file = $file . self::EXT;
431$this->setExpiration(config('cache_lifespan'));
432$this->_lifespan = $span;
433*/
434
Top