1<?php require_once 'engine/init.php';
2znote_csrf_protect_public_post();
3protect_page();
4theme_open();
5// Convert a seconds integer value into days, hours, minutes and seconds string.
6function toDuration($is) {
7 $duration['day'] = $is / (24 * 60 * 60);
8 if (($duration['day'] - (int)$duration['day']) > 0)
9 $duration['hour'] = ($duration['day'] - (int)$duration['day']) * 24;
10 if (isset($duration['hour'])) {
11 if (($duration['hour'] - (int)$duration['hour']) > 0)
12 $duration['minute'] = ($duration['hour'] - (int)$duration['hour']) * 60;
13 if (isset($duration['minute'])) {
14 if (($duration['minute'] - (int)$duration['minute']) > 0)
15 $duration['second'] = ($duration['minute'] - (int)$duration['minute']) * 60;
16 }
17 }
18 $tmp = array();
19 foreach ($duration as $type => $value) {
20 if ($value >= 1) {
21 $pluralType = ((int)$value === 1) ? $type : $type . 's';
22 if ($type !== 'second') $tmp[] = (int)$value . " $pluralType";
23 else $tmp[] = (int)$value . " $pluralType";
24 }
25 }
26 return implode(', ', $tmp);
27}
28?>
29<?php view('auction_header'); ?>
30<?php
31// Import from config:
32$auction = $config['shop_auction'];
33$loadOutfits = ($config['show_outfits']['highscores']) ? true : false;
34$this_account_id = (int)$session_user_id;
35$is_admin = is_admin($user_data);
36
37// If character auction is enabled in config.php
38if ($auction['characterAuction']) {
39
40 if (znote_server_adapter()->normalizedEngine() !== 'TFS_10') {
41 view('auction_wrong_engine');
42 theme_close();
43 die();
44 }
45 if ((int)$auction['storage_account_id'] === (int)$this_account_id) {
46 view('auction_storage_error');
47 theme_close();
48 die();
49 }
50 $step = $auction['step'];
51 $step_duration = $auction['step_duration'];
52 $actions = array(
53 'list', // list all available players in auction
54 'view', // view a specific player
55 'create', // select which character to add and initial price
56 'add', // add character to list
57 'bid', // Bid or buy a specific player
58 'refund', // Refund a player you added back to your account
59 'claim' // Claim a character you won through purchase or bid
60 );
61
62 // Default action is list, but $_GET or $_POST will override it.
63 $action = 'list';
64 // Load selected string from actions array based on input, strict whitelist validation
65 if (isset( $_GET['action']) && in_array( $_GET['action'], $actions)) {
66 $action = $actions[array_search( $_GET['action'], $actions, true)];
67 }
68 if (isset($_POST['action']) && in_array($_POST['action'], $actions)) {
69 $action = $actions[array_search($_POST['action'], $actions, true)];
70 }
71
72 // Passive check to see if bid period has expired and someone won a deal
73 $time = time();
74 db()->transaction(function ($db) use ($time) {
75 $expired_auctions = $db->fetchAll("
76 SELECT
77 `id`,
78 `original_account_id`,
79 (`bid`+`deposit`) as `points`
80 FROM `znote_auction_player`
81 WHERE `sold` = 0
82 AND `time_end` < ?
83 AND `bidder_account_id` > 0
84 FOR UPDATE
85 ", [$time]);
86
87 if ($expired_auctions !== false) {
88 foreach ($expired_auctions as $a) {
89 $db->execute("UPDATE `znote_auction_player` SET `sold` = 1 WHERE `id` = ?;", [$a['id']]);
90 // Transfer points to seller account
91 $db->execute("
92 UPDATE `znote_accounts`
93 SET `points` = `points` + ?
94 WHERE `account_id` = ?;
95 ", [$a['points'], $a['original_account_id']]);
96 }
97 }
98
99 return true;
100 });
101 // end passive check
102
103 // If we bid or buy a character
104 // silently continues to list if buy, back to view if bid
105 if ($action === 'bid') {
106 //data_dump($_POST, false, "Bid or buying:");
107 $zaid = (isset($_POST['zaid']) && (int)$_POST['zaid'] > 0) ? (int)$_POST['zaid'] : false;
108 $price = (isset($_POST['price']) && (int)$_POST['price'] > 0) ? (int)$_POST['price'] : false;
109
110 $action = 'list';
111 if ($zaid !== false && $price !== false) {
112 // The whole read-check-write sequence is locked in one transaction,
113 // so two concurrent bids on the same character (or on the same
114 // buyer's balance) cannot both succeed off a stale points/bid read.
115 $bidOutcome = db()->transaction(function ($db) use ($zaid, $price, $this_account_id, $step, $step_duration) {
116 // The account of the buyer, if he can afford what he is trying to pay
117 $account = $db->fetchOne("
118 SELECT
119 `a`.`id`,
120 `za`.`points`
121 FROM `accounts` a
122 INNER JOIN `znote_accounts` za
123 ON `a`.`id` = `za`.`account_id`
124 WHERE `a`.`id` = ?
125 AND `za`.`points` >= ?
126 LIMIT 1 FOR UPDATE;
127 ", [$this_account_id, $price]);
128 //data_dump($account, false, "Buyer account:");
129
130 // The character to buy, presuming it isn't sold, buyer isn't the owner, buyer can afford it
131 if ($account === false) {
132 return false;
133 }
134
135 $character = $db->fetchOne("
136 SELECT
137 `za`.`id` AS `zaid`,
138 `za`.`player_id`,
139 `za`.`original_account_id`,
140 `za`.`bidder_account_id`,
141 `za`.`time_begin`,
142 `za`.`time_end`,
143 `za`.`price`,
144 `za`.`bid`,
145 `za`.`deposit`,
146 `za`.`sold`
147 FROM `znote_auction_player` za
148 WHERE `za`.`id` = ?
149 AND `za`.`sold` = 0
150 AND `za`.`original_account_id` != ?
151 AND `za`.`price` <= ?
152 AND `za`.`bid` + ? <= ?
153 LIMIT 1 FOR UPDATE
154 ", [$zaid, $this_account_id, $price, $step, $price]);
155 //data_dump($character, false, "Character to buy:");
156
157 if ($character === false) {
158 return false;
159 }
160
161 // If auction already have a previous bidder, refund him his points
162 if ($character['bid'] > 0 && $character['bidder_account_id'] > 0) {
163 $db->execute("
164 UPDATE `znote_accounts`
165 SET `points` = `points` + ?
166 WHERE `account_id` = ?
167 LIMIT 1;
168 ", [$character['bid'], $character['bidder_account_id']]);
169 // If previous bidder is not you, increase bidding period by 1 hour
170 // (Extending bid war to give bidding competitor a chance to retaliate)
171 if ((int)$character['bidder_account_id'] !== (int)$account['id']) {
172 $db->execute("
173 UPDATE `znote_auction_player`
174 SET `time_end` = `time_end` + ?
175 WHERE `id` = ?
176 LIMIT 1;
177 ", [$step_duration, $character['zaid']]);
178 }
179 }
180 // Remove points from buyer
181 $db->execute("
182 UPDATE `znote_accounts`
183 SET `points` = `points` - ?
184 WHERE `account_id` = ?
185 LIMIT 1;
186 ", [$price, $account['id']]);
187 // Update auction, and set new bidder data
188 $now = time();
189 $db->execute("
190 UPDATE `znote_auction_player`
191 SET
192 `bidder_account_id` = ?,
193 `bid` = ?,
194 `sold` = CASE WHEN ? >= `time_end` THEN 1 ELSE 0 END
195 WHERE `id` = ?
196 LIMIT 1;
197 ", [$account['id'], $price, $now, $character['zaid']]);
198 // If character is sold, give points to seller
199 if ($now >= $character['time_end']) {
200 $db->execute("
201 UPDATE `znote_accounts`
202 SET `points` = `points` + ?
203 WHERE `account_id` = ?
204 LIMIT 1;
205 ", [$character['deposit'] + $price, $character['original_account_id']]);
206 return 'sold';
207 }
208 // If character is not sold, this is a bidding war, we want to send user back to view.
209 return 'bidding';
210 // Note: Transferring character to the new account etc happens later in $action = 'claim'
211 });
212
213 if ($bidOutcome === 'bidding') {
214 $action = 'view';
215 }
216 }
217 }
218
219 // See a specific character in auction,
220 // silently fallback to list if he doesn't exist or is already sold
221 if ($action === 'view') { // View a character in the auction
222 if (!isset($zaid)) {
223 $zaid = (isset($_GET['zaid']) && (int)$_GET['zaid'] > 0) ? (int)$_GET['zaid'] : false;
224 }
225 if ($zaid !== false) {
226 // Retrieve basic character information
227 $character = db()->fetchOne("
228 SELECT
229 `za`.`id` AS `zaid`,
230 `za`.`player_id`,
231 `za`.`original_account_id`,
232 `za`.`bidder_account_id`,
233 `za`.`time_begin`,
234 `za`.`time_end`,
235 CASE WHEN `za`.`price` > `za`.`bid`
236 THEN `za`.`price`
237 ELSE `za`.`bid` + ?
238 END AS `price`,
239 CASE WHEN `za`.`original_account_id` = ?
240 THEN 1
241 ELSE 0
242 END AS `own`,
243 CASE WHEN `za`.`original_account_id` = ?
244 THEN `p`.`name`
245 ELSE ''
246 END AS `name`,
247 CASE WHEN `za`.`original_account_id` = ?
248 THEN `za`.`bid`
249 ELSE 0
250 END AS `bid`,
251 CASE WHEN `za`.`original_account_id` = ?
252 THEN `za`.`deposit`
253 ELSE 0
254 END AS `deposit`,
255 `p`.`vocation`,
256 `p`.`level`,
257 `p`.`sex`,
258 `p`.`balance`,
259 `p`.`lookbody` AS `body`,
260 `p`.`lookfeet` AS `feet`,
261 `p`.`lookhead` AS `head`,
262 `p`.`looklegs` AS `legs`,
263 `p`.`looktype` AS `type`,
264 `p`.`lookaddons` AS `addons`,
265 `p`.`maglevel` AS `magic`,
266 `p`.`skill_fist` AS `fist`,
267 `p`.`skill_club` AS `club`,
268 `p`.`skill_sword` AS `sword`,
269 `p`.`skill_axe` AS `axe`,
270 `p`.`skill_dist` AS `dist`,
271 `p`.`skill_shielding` AS `shielding`,
272 `p`.`skill_fishing` AS `fishing`
273 FROM `znote_auction_player` za
274 INNER JOIN `players` p
275 ON `za`.`player_id` = `p`.`id`
276 WHERE `za`.`id` = ?
277 AND `za`.`sold` = 0
278 LIMIT 1;
279 ", [$step, $this_account_id, $this_account_id, $this_account_id, $this_account_id, $zaid]);
280 //data_dump($character, false, "Character info");
281
282 if (is_array($character) && !empty($character)) {
283 // If the end of the bid is in the future, the bid is currently ongoing
284 $bidding_period = ((int)$character['time_end']+1 > time()) ? true : false;
285 $player_items = db()->fetchAll("
286 SELECT `itemtype`, SUM(`count`) AS `count`
287 FROM `player_items`
288 WHERE `player_id` = ?
289 GROUP BY `itemtype`
290 ORDER BY MIN(`pid`) ASC
291 ", [$character['player_id']]);
292 $depot_items = db()->fetchAll("
293 SELECT `itemtype`, SUM(`count`) AS `count`
294 FROM `player_depotitems`
295 WHERE `player_id` = ?
296 GROUP BY `itemtype`
297 ORDER BY MIN(`pid`) ASC
298 ", [$character['player_id']]);
299 $account = db()->fetchOne("
300 SELECT `points`
301 FROM `znote_accounts`
302 WHERE `account_id` = ?
303 AND `points` >= ?
304 LIMIT 1;
305 ", [$this_account_id, $character['price']]);
306
307 $items = getItemList();
308
309 view('auction_view', [
310 'character' => $character,
311 'account' => $account,
312 'bidding_period' => $bidding_period,
313 'loadOutfits' => $loadOutfits,
314 'step' => $step,
315 'this_account_id' => $this_account_id,
316 'player_items' => $player_items,
317 'depot_items' => $depot_items,
318 'items' => $items,
319 ]);
320 } else {
321 $action = 'list';
322 }
323 }
324 }
325
326 // If we are adding a character to the list
327 // silently continues to list
328 if ($action === 'add') {
329 $pid = (isset($_POST['pid']) && (int)$_POST['pid'] > 0) ? (int)$_POST['pid'] : false;
330 $cost = (isset($_POST['cost']) && (int)$_POST['cost'] > 0) ? (int)$_POST['cost'] : false;
331 $deposit = (int)$cost * ($auction['deposit'] / 100);
332 $password = SHA1($_POST['password']);
333
334 // Verify values
335 $status = false;
336 $account = false;
337 if ($pid > 0 && $cost >= $auction['lowestPrice']) {
338 $account = db()->fetchOne("
339 SELECT `a`.`id`, `a`.`password`, `za`.`points`
340 FROM `accounts` a
341 INNER JOIN `znote_accounts` za
342 ON `a`.`id` = `za`.`account_id`
343 WHERE `a`.`id` = ?
344 AND `a`.`password` = ?
345 AND `za`.`points` >= ?
346 LIMIT 1
347 ;", [$this_account_id, $password, $deposit]);
348 if (isset($account['password']) && $account['password'] === $password) {
349 // Check if player exist, is offline and not already in auction
350 // And is not a tutor or a GM+.
351 $player = db()->fetchOne("
352 SELECT `p`.`id`, `p`.`name`,
353 CASE
354 WHEN `po`.`player_id` IS NULL
355 THEN 0
356 ELSE 1
357 END AS `online`,
358 CASE
359 WHEN `za`.`player_id` IS NULL
360 THEN 0
361 ELSE 1
362 END AS `alreadyInAuction`
363 FROM `players` p
364 LEFT JOIN `players_online` po
365 ON `p`.`id` = `po`.`player_id`
366 LEFT JOIN `znote_auction_player` za
367 ON `p`.`id` = `za`.`player_id`
368 AND `p`.`account_id` = `za`.`original_account_id`
369 AND `za`.`claimed` = 0
370 WHERE `p`.`id` = ?
371 AND `p`.`account_id` = ?
372 AND `p`.`group_id` = 1
373 LIMIT 1
374 ;", [$pid, $this_account_id]);
375 // Verify storage account ID exist
376 $storage_account = db()->fetchOne("
377 SELECT `id`
378 FROM `accounts`
379 WHERE `id` = ?
380 LIMIT 1;
381 ", [$auction['storage_account_id']]);
382 if ($storage_account === false) {
383 data_dump($auction, false, "Configured storage_account_id in config.php does not exist!");
384 } else {
385 if (isset($player['online']) && $player['online'] == 0) {
386 if (isset($player['alreadyInAuction']) && $player['alreadyInAuction'] == 0) {
387 $status = true;
388 }
389 }
390 }
391 }
392 }
393 if ($status) {
394 $time_begin = time();
395 $time_end = $time_begin + ($auction['biddingDuration']);
396 // Re-check the balance under lock and spend it with a relative
397 // update, so two concurrent listings from the same account cannot
398 // both go through on a stale points read.
399 db()->transaction(function ($db) use ($pid, $this_account_id, $time_begin, $time_end, $cost, $deposit, $auction, $account) {
400 $current = $db->fetchOne("SELECT `points` FROM `znote_accounts` WHERE `account_id` = ? LIMIT 1 FOR UPDATE;", [$account['id']]);
401 if (!is_array($current) || (int)$current['points'] < $deposit) {
402 return false;
403 }
404
405 // Insert row to znote_auction_player
406 $db->execute("
407 INSERT INTO `znote_auction_player` (
408 `player_id`,
409 `original_account_id`,
410 `bidder_account_id`,
411 `time_begin`,
412 `time_end`,
413 `price`,
414 `bid`,
415 `deposit`,
416 `sold`,
417 `claimed`
418 ) VALUES (?, ?, 0, ?, ?, ?, 0, ?, 0, 0);
419 ", [$pid, $this_account_id, $time_begin, $time_end, $cost, $deposit]);
420 // Move player to storage account
421 $db->execute("
422 UPDATE `players`
423 SET `account_id` = ?
424 WHERE `id` = ?
425 LIMIT 1;
426 ", [$auction['storage_account_id'], $pid]);
427 // Hide character from public character list (in pidprofile.php)
428 $db->execute("
429 UPDATE `znote_players`
430 SET `hide_char` = 1
431 WHERE `player_id` = ?
432 LIMIT 1;
433 ", [$pid]);
434 // Remove deposit from account
435 $db->execute("
436 UPDATE `znote_accounts`
437 SET `points` = `points` - ?
438 WHERE `account_id` = ?
439 LIMIT 1;
440 ", [$deposit, $account['id']]);
441
442 return true;
443 });
444 }
445 $action = 'list';
446 }
447
448 // If we are refunding a player back to its original owner
449 // silently continues to list
450 if ($action === 'refund') {
451 $zaid = (isset($_POST['zaid']) && (int)$_POST['zaid'] > 0) ? (int)$_POST['zaid'] : false;
452 //data_dump($_POST, false, "POST");
453 if ($zaid !== false) {
454 $time = time();
455 // Re-verify the same conditions under lock right before writing,
456 // so two concurrent refund/claim/bid requests for the same
457 // character cannot both act on it.
458 db()->transaction(function ($db) use ($zaid, $this_account_id, $time) {
459 // If original account is the one trying to get it back,
460 // and bidding period is over,
461 // and its not labeled as sold
462 // and nobody has bid on it
463 $character = $db->fetchOne("
464 SELECT `player_id`
465 FROM `znote_auction_player`
466 WHERE `id` = ?
467 AND `original_account_id` = ?
468 AND `time_end` <= ?
469 AND `bidder_account_id` = 0
470 AND `bid` = 0
471 AND `sold` = 0
472 LIMIT 1
473 FOR UPDATE
474 ", [$zaid, $this_account_id, $time]);
475 //data_dump($character, false, "Character");
476 if ($character === false) {
477 return false;
478 }
479
480 // Move character to buyer account and give it a new name
481 $db->execute("
482 UPDATE `players`
483 SET `account_id` = ?
484 WHERE `id` = ?
485 LIMIT 1;
486 ", [$this_account_id, $character['player_id']]);
487 // Set label to sold
488 $db->execute("
489 UPDATE `znote_auction_player`
490 SET `sold` = 1
491 WHERE `id` = ?
492 LIMIT 1;
493 ", [$zaid]);
494 // Show character in public character list (in characterprofile.php)
495 $db->execute("
496 UPDATE `znote_players`
497 SET `hide_char` = 0
498 WHERE `player_id` = ?
499 LIMIT 1;
500 ", [$character['player_id']]);
501
502 return true;
503 });
504 }
505 $action = 'list';
506 }
507
508 // If we are claiming a character
509 // If validation fails then explain why, but then head over to list regardless of status
510 if ($action === 'claim') {
511 $zaid = (isset($_POST['zaid']) && (int)$_POST['zaid'] > 0) ? (int)$_POST['zaid'] : false;
512 $name = (isset($_POST['name']) && !empty($_POST['name'])) ? getValue($_POST['name'] ?? null) : false;
513 $errors = array();
514 //data_dump($_POST, $name, "Post data:");
515 if ($zaid === false) {
516 $errors[] = t('auc.not_found');
517 }
518 if ((int)$auction['storage_account_id'] === $this_account_id) {
519 $errors[] = t('auc.storage_account2');
520 if ($is_admin) {
521 $errors[] = "ADMIN: The storage account in config.php should not be the same as the admin account.";
522 }
523 }
524 if ($name === false) {
525 $errors[] = t('auc.name_required');
526 } else {
527 // begin name validation
528 $name = validate_name($name);
529 if (user_character_exist($name) !== false) {
530 $errors[] = t('acc.name_taken');
531 }
532 if (!preg_match("/^[a-zA-Z_ ]+$/", $name)) {
533 $errors[] = t('acc.name_letters');
534 }
535 if (strlen($name) < $config['minL'] || strlen($name) > $config['maxL']) {
536 $errors[] = t('acc.name_length', ['min' => $config['minL'], 'max' => $config['maxL']]);
537 }
538 // name restriction
539 $resname = explode(" ", $name);
540 foreach($resname as $res) {
541 if(in_array(strtolower($res), $config['invalidNameTags'])) {
542 $errors[] = t('reg.restricted_word2');
543 }
544 else if(strlen($res) == 1) {
545 $errors[] = t('reg.words_too_short2');
546 }
547 }
548 $name = format_character_name($name);
549 // end name validation
550 if (empty($errors)) {
551 // Make sure you have access to claim this zaid character.
552 // And that you haven't already claimed it.
553 // And that the character isn't online...
554 // Re-verified under lock right before writing, so a concurrent
555 // claim/bid on the same auction row cannot race this one.
556 $claimed = db()->transaction(function ($db) use ($zaid, $this_account_id, $name) {
557 $character = $db->fetchOne("
558 SELECT
559 `za`.`id` AS `zaid`,
560 `za`.`player_id`,
561 `p`.`account_id`
562 FROM `znote_auction_player` za
563 INNER JOIN `players` p
564 ON `za`.`player_id` = `p`.`id`
565 LEFT JOIN `players_online` po
566 ON `p`.`id` = `po`.`player_id`
567 WHERE `za`.`id` = ?
568 AND `za`.`sold` = 1
569 AND `p`.`account_id` != ?
570 AND `za`.`bidder_account_id` = ?
571 AND `po`.`player_id` IS NULL
572 FOR UPDATE
573 ", [$zaid, $this_account_id, $this_account_id]);
574 //data_dump($character, false, "Character");
575 if ($character === false) {
576 return false;
577 }
578
579 // Set character to claimed
580 $db->execute("
581 UPDATE `znote_auction_player`
582 SET `claimed` = 1
583 WHERE `id` = ?
584 ", [$character['zaid']]);
585 // Move character to buyer account and give it a new name
586 $db->execute("
587 UPDATE `players`
588 SET `name` = ?,
589 `account_id` = ?
590 WHERE `id` = ?
591 LIMIT 1;
592 ", [$name, $this_account_id, $character['player_id']]);
593 // Show character in public character list (in characterprofile.php)
594 $db->execute("
595 UPDATE `znote_players`
596 SET `hide_char` = 0
597 WHERE `player_id` = ?
598 LIMIT 1;
599 ", [$character['player_id']]);
600 // Remove character from other players VIP lists
601 $db->execute("
602 DELETE FROM `account_viplist`
603 WHERE `player_id` = ?
604 ", [$character['player_id']]);
605 // Remove the character deathlist
606 $db->execute("
607 DELETE FROM `player_deaths`
608 WHERE `player_id` = ?
609 ", [$character['player_id']]);
610
611 return true;
612 });
613
614 if (!$claimed) {
615 $errors[] = "You either don't have access to claim this character, or you have already claimed it, or this character isn't sold yet, or we were unable to find this auction order.";
616 if ($is_admin) {
617 $errors[] = "ADMIN: ... Or character is online.";
618 }
619 }
620 }
621 }
622 if (!empty($errors)) {
623 view('auction_claim_errors', ['errors' => $errors]);
624 }
625 $action = 'list';
626 }
627
628 // List characters currently in the auction
629 if ($action === 'list') {
630 // If this account have successfully bought or won an auction
631 // Intercept the list action and let the user do claim actions
632 $pending = db()->fetchAll("
633 SELECT
634 `za`.`id` AS `zaid`,
635 CASE WHEN `za`.`price` > `za`.`bid`
636 THEN `za`.`price`
637 ELSE `za`.`bid`
638 END AS `price`,
639 `za`.`time_begin`,
640 `za`.`time_end`,
641 `p`.`vocation`,
642 `p`.`level`,
643 `p`.`lookbody` AS `body`,
644 `p`.`lookfeet` AS `feet`,
645 `p`.`lookhead` AS `head`,
646 `p`.`looklegs` AS `legs`,
647 `p`.`looktype` AS `type`,
648 `p`.`lookaddons` AS `addons`
649 FROM `znote_auction_player` za
650 INNER JOIN `players` p
651 ON `za`.`player_id` = `p`.`id`
652 WHERE `p`.`account_id` = ?
653 AND `za`.`claimed` = 0
654 AND `za`.`sold` = 1
655 AND `za`.`bidder_account_id` = ?
656 ORDER BY `p`.`level` desc
657 ", [$auction['storage_account_id'], $this_account_id]);
658 //data_dump($pending, false, "Pending characters:");
659
660 // --- Filters, search and sort -------------------------------------
661 $filterVoc = (isset($_GET['voc']) && (int)$_GET['voc'] > 0) ? (int)$_GET['voc'] : 0;
662 $filterLevelMin = (isset($_GET['level_min']) && (int)$_GET['level_min'] > 0) ? (int)$_GET['level_min'] : 0;
663 $filterLevelMax = (isset($_GET['level_max']) && (int)$_GET['level_max'] > 0) ? (int)$_GET['level_max'] : 0;
664 $search = trim((string)($_GET['q'] ?? ''));
665 $items = getItemList();
666 $sortOptions = array('level_desc', 'level_asc', 'price_desc', 'price_asc', 'ending_soon');
667 $sort = (isset($_GET['sort']) && in_array($_GET['sort'], $sortOptions, true)) ? $_GET['sort'] : 'level_desc';
668 $sortSql = array(
669 'level_desc' => '`p`.`level` DESC',
670 'level_asc' => '`p`.`level` ASC',
671 'price_desc' => '`price` DESC',
672 'price_asc' => '`price` ASC',
673 'ending_soon' => '`za`.`time_end` ASC',
674 )[$sort];
675
676 $where = array('`p`.`account_id` = ?', '`za`.`sold` = 0');
677 $params = array($auction['storage_account_id']);
678
679 if ($filterVoc > 0) {
680 $where[] = '`p`.`vocation` = ?';
681 $params[] = $filterVoc;
682 }
683 if ($filterLevelMin > 0) {
684 $where[] = '`p`.`level` >= ?';
685 $params[] = $filterLevelMin;
686 }
687 if ($filterLevelMax > 0) {
688 $where[] = '`p`.`level` <= ?';
689 $params[] = $filterLevelMax;
690 }
691 if ($search !== '') {
692 // Matches either the character name, or an item name resolved to
693 // itemtype ids first ($items is already loaded above for the
694 // item icons below, so this costs no extra file read).
695 $matchingIds = array();
696 $needle = strtolower($search);
697 if (is_array($items)) {
698 foreach ($items as $id => $name) {
699 if (strpos(strtolower($name), $needle) !== false) {
700 $matchingIds[] = (int)$id;
701 }
702 }
703 }
704 if ($matchingIds) {
705 $placeholders = implode(',', array_fill(0, count($matchingIds), '?'));
706 $where[] = '(`p`.`name` LIKE ? OR `za`.`player_id` IN ('
707 . "SELECT `player_id` FROM `player_items` WHERE `itemtype` IN ($placeholders)"
708 . ' UNION '
709 . "SELECT `player_id` FROM `player_depotitems` WHERE `itemtype` IN ($placeholders)"
710 . '))';
711 $params[] = '%' . $search . '%';
712 foreach ($matchingIds as $id) $params[] = $id;
713 foreach ($matchingIds as $id) $params[] = $id;
714 } else {
715 $where[] = '`p`.`name` LIKE ?';
716 $params[] = '%' . $search . '%';
717 }
718 }
719
720 $whereSql = implode(' AND ', $where);
721
722 $auctionPerPage = 15;
723 $page = (isset($_GET['page']) && (int)$_GET['page'] > 0) ? (int)$_GET['page'] : 1;
724
725 $totalRow = db()->fetchOne("
726 SELECT COUNT(*) AS `c`
727 FROM `znote_auction_player` za
728 INNER JOIN `players` p ON `za`.`player_id` = `p`.`id`
729 WHERE {$whereSql};
730 ", $params);
731 $total = ($totalRow !== false) ? (int)$totalRow['c'] : 0;
732 $pageCount = max(1, (int)ceil($total / $auctionPerPage));
733 $page = min($page, $pageCount);
734 $offset = ($page - 1) * $auctionPerPage;
735
736 // Show the list
737 $listParams = array_merge(array($step), $params, array($offset, $auctionPerPage));
738 $characters = db()->fetchAll("
739 SELECT
740 `za`.`id` AS `zaid`,
741 CASE WHEN `za`.`price` > `za`.`bid`
742 THEN `za`.`price`
743 ELSE `za`.`bid` + ?
744 END AS `price`,
745 `za`.`time_begin`,
746 `za`.`time_end`,
747 `p`.`id` AS `player_id`,
748 `p`.`vocation`,
749 `p`.`level`,
750 `p`.`sex`,
751 `p`.`lookbody` AS `body`,
752 `p`.`lookfeet` AS `feet`,
753 `p`.`lookhead` AS `head`,
754 `p`.`looklegs` AS `legs`,
755 `p`.`looktype` AS `type`,
756 `p`.`lookaddons` AS `addons`
757 FROM `znote_auction_player` za
758 INNER JOIN `players` p
759 ON `za`.`player_id` = `p`.`id`
760 WHERE {$whereSql}
761 ORDER BY {$sortSql}
762 LIMIT ?, ?;
763 ", $listParams);
764 //data_dump($characters, false, "List characters");
765
766 // --- Highlight items per listed character --------------------------
767 // A handful of "notable" items per card: the equipped weapon/shield
768 // (pid 5/6, TFS equip slots) plus the two largest depot stacks (a
769 // simple proxy for "stacked valuables" like the reference page's
770 // ammo piles). Capped at 4 icons to match the reference layout.
771 $highlightItems = array();
772 if (is_array($characters) && $characters) {
773 $playerIds = array_column($characters, 'player_id');
774 $idPlaceholders = implode(',', array_fill(0, count($playerIds), '?'));
775
776 $equipRows = db()->fetchAll("
777 SELECT `player_id`, `itemtype`, `count`
778 FROM `player_items`
779 WHERE `player_id` IN ($idPlaceholders)
780 AND `pid` IN (5, 6);
781 ", $playerIds);
782 if (is_array($equipRows)) {
783 foreach ($equipRows as $row) {
784 $pid = (int)$row['player_id'];
785 if (!isset($highlightItems[$pid])) $highlightItems[$pid] = array();
786 if (count($highlightItems[$pid]) < 4) {
787 $highlightItems[$pid][] = array('itemtype' => (int)$row['itemtype'], 'count' => (int)$row['count']);
788 }
789 }
790 }
791
792 $depotRows = db()->fetchAll("
793 SELECT `player_id`, `itemtype`, SUM(`count`) AS `count`
794 FROM `player_depotitems`
795 WHERE `player_id` IN ($idPlaceholders)
796 GROUP BY `player_id`, `itemtype`
797 ORDER BY `count` DESC;
798 ", $playerIds);
799 if (is_array($depotRows)) {
800 foreach ($depotRows as $row) {
801 $pid = (int)$row['player_id'];
802 if (!isset($highlightItems[$pid])) $highlightItems[$pid] = array();
803 if (count($highlightItems[$pid]) < 4) {
804 $highlightItems[$pid][] = array('itemtype' => (int)$row['itemtype'], 'count' => (int)$row['count']);
805 }
806 }
807 }
808 }
809
810 view('auction_list', [
811 'pending' => $pending,
812 'characters' => $characters,
813 'loadOutfits' => $loadOutfits,
814 'is_admin' => $is_admin,
815 'highlightItems' => $highlightItems,
816 'items' => $items,
817 'filterVoc' => $filterVoc,
818 'filterLevelMin' => $filterLevelMin,
819 'filterLevelMax' => $filterLevelMax,
820 'search' => $search,
821 'sort' => $sort,
822 'page' => $page,
823 'pageCount' => $pageCount,
824 'total' => $total,
825 ]);
826
827 } elseif ($action === 'create') { // Add player to auction view
828 $minToCreate = (int)ceil(($auction['lowestPrice'] / 100) * $auction['deposit']);
829 $own_characters = db()->fetchAll("
830 SELECT
831 `p`.`id`,
832 `p`.`name`,
833 `p`.`level`,
834 `p`.`vocation`,
835 `a`.`points`
836 FROM `players` p
837 INNER JOIN `znote_accounts` a
838 ON `p`.`account_id` = `a`.`account_id`
839 LEFT JOIN `znote_auction_player` za
840 ON `p`.`id` = `za`.`player_id`
841 AND `p`.`account_id` = `za`.`original_account_id`
842 AND `za`.`claimed` = 0
843 LEFT JOIN `players_online` po
844 ON `p`.`id` = `po`.`player_id`
845 WHERE `p`.`account_id` = ?
846 AND `za`.`player_id` IS NULL
847 AND `po`.`player_id` IS NULL
848 AND `p`.`level` >= ?
849 AND `a`.`points` >= ?
850 ;", [$this_account_id, $auction['lowestLevel'], $minToCreate]);
851 //data_dump($own_characters, false, "own_chars");
852
853 $max = (is_array($own_characters) && !empty($own_characters))
854 ? ($own_characters[0]['points'] / $auction['deposit']) * 100
855 : 0;
856
857 view('auction_create', [
858 'own_characters' => $own_characters,
859 'auction' => $auction,
860 'minToCreate' => $minToCreate,
861 'max' => $max,
862 ]);
863 }
864} else {
865 view('auction_disabled');
866}
867theme_close(); ?>
868