<?php
/* vim: set noexpandtab tabstop=2 softtabstop=2 shiftwidth=2: */

/**
 * Checkpoints plugin.
 * Provides checkpoints/finish tracking, and displays checkpoint passages
 * via automatic pop-ups (useful only in Rounds/Team/Cup modes).
 * Manages the CP panel during playing, retiring & spectating,
 * as well as the Cup mode's warm-up phase.  Disabled in Stunts mode.
 * Created by Xymph
 *
 * Dependencies: used by plugin.dedimania.php & plugin.localdatabase.php
 */

Aseco::registerEvent('onPlayerConnect', 'addplayer_cp');
Aseco::registerEvent('onPlayerDisconnect', 'removeplayer_cp');
Aseco::registerEvent('onBeginMap', 'reset_checkp');
Aseco::registerEvent('onBeginRound', 'clear_curr_cp');
Aseco::registerEvent('onEndMap', 'disable_checkp');
Aseco::registerEvent('onRestartMap', 'restart_checkp');
Aseco::registerEvent('onCheckpoint', 'store_checkp');
Aseco::registerEvent('onPlayerFinish1', 'store_finish');  // use pre event before local/Dedimania record processsing
Aseco::registerEvent('onPlayerInfoChanged', 'spec_togglecp');

Aseco::addChatCommand('cps', 'Sets local record checkpoints tracking');
Aseco::addChatCommand('cpsspec', 'Shows checkpoints of spectated player');
Aseco::addChatCommand('sectms', 'Displays all local records\' sector times');

global $checkpoints, $checkpoint_tests;
$checkpoints = array();
$checkpoint_tests = false;  // after reload no tests until end race

class Checkpoints {
	var $loclrec;
	var $dedirec;
	var $best_time;
	var $best_fin;
	var $best_cps;
	var $curr_fin;
	var $curr_cps;
	var $speccers;

	// init empty checkpoints
	function Checkpoints() {
		$this->loclrec = -1;  // -1 = off, 0 = own/last rec, 1-max = rec #1-max
		$this->dedirec = -1;  // -1 = off, 0 = own/last rec, 1-30 = rec #1-30
		$this->best_time = 0;
		$this->best_fin = PHP_INT_MAX;
		$this->curr_fin = PHP_INT_MAX;
		$this->best_cps = array();
		$this->curr_cps = array();
		$this->speccers = array();
	}
}  // class Checkpoints

function chat_cps($aseco, $command) {
	global $checkpoints;

	$player = $command['author'];
	$login = $player->login;

	// check for relay server
	if ($aseco->server->isrelay) {
		$message = formatText($aseco->getChatMessage('NOTONRELAY'));
		$aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login);
		return;
	}

	if ($aseco->settings['display_checkpoints']) {
		// set local checkpoints tracking
		$param = $command['params'];
		if (strtolower($param) == 'off') {
			$checkpoints[$login]->loclrec = -1;
			$checkpoints[$login]->dedirec = -1;
			$message = '{#server}> Local checkpoints tracking: {#highlite}OFF';
		}
		elseif ($param == '') {
			$checkpoints[$login]->loclrec = 0;
			$checkpoints[$login]->dedirec = -1;
			$message = '{#server}> Local checkpoints tracking: {#highlite}ON {#server}(your own or the last record)';
		}
		elseif (is_numeric($param) && $param > 0 && $param <= $aseco->server->records->max) {
			$checkpoints[$login]->loclrec = intval($param);
			$checkpoints[$login]->dedirec = -1;
			$message = '{#server}> Local checkpoints tracking record: {#highlite}' . $checkpoints[$login]->loclrec;
		}
		else {
			$message = '{#server}> {#error}No such Local record {#highlite}$i ' . $param;
		}

		// handle checkpoints panel
		if ($checkpoints[$login]->loclrec == -1) {
			// disable CP panel
			if ($aseco->settings['enable_cpsspec'] && !empty($checkpoints[$login]->speccers))
				cpspanel_off($aseco, $login . ',' . implode(',', $checkpoints[$login]->speccers));
			else
				cpspanel_off($aseco, $login);
		} else {
			// enable CP panel unless spectator, Stunts mode, or warm-up
			if (!$player->isspectator && $aseco->server->gameinfo->mode != Gameinfo::STNT && !$aseco->warmup_phase) {
				if ($aseco->settings['enable_cpsspec'] && !empty($checkpoints[$login]->speccers))
					display_cpspanel($aseco, $login . ',' . implode(',', $checkpoints[$login]->speccers), 0, '$00f -.--');
				else
					display_cpspanel($aseco, $login, 0, '$00f -.--');
			}
		}
	} else {
		$message = '{#server}> {#error}Checkpoints tracking permanently disabled by server';
	}
	$aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login);
}  // chat_cps

function chat_cpsspec($aseco, $command) {
	global $checkpoints;

	$player = $command['author'];
	$login = $player->login;

	// check for relay server
	if ($aseco->server->isrelay) {
		$message = formatText($aseco->getChatMessage('NOTONRELAY'));
		$aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login);
		return;
	}

	if ($aseco->settings['enable_cpsspec']) {
		// toggle cpsspec setting
		if ($player->speclogin != '') {
			// if subscribed, unsubscribe first
			if ($player->speclogin != ',' && isset($checkpoints[$player->speclogin])) {
				if (($i = array_search($login, $checkpoints[$player->speclogin]->speccers)) !== false)
					unset($checkpoints[$player->speclogin]->speccers[$i]);
			}
			$player->speclogin = '';
			cpspanel_off($aseco, $login);
		} else {
			// if spectator, subscribe
			$aseco->client->query('GetPlayerInfo', $login, 1);
			$info = $aseco->client->getResponse();
			$targetid = floor($info['SpectatorStatus'] / 10000);
			// check for player or free camera
			if ($info['SpectatorStatus'] == 0 || $targetid == 255) {
				$player->speclogin = ',';  // no target
			} else {
				// find login for target
				foreach ($aseco->server->players->player_list as $pl) {
					if ($pl->pid == $targetid) {
						$player->speclogin = $pl->login;
						// subscribe to this player
						if (!in_array($login, $checkpoints[$player->speclogin]->speccers))
							$checkpoints[$player->speclogin]->speccers[] = $login;
						break;
					}
				}
			}
		}

		// show chat message
		$message = '{#server}> Spectated player checkpoints tracking ';
		if ($player->speclogin != '')
			$message .= 'enabled';
		else
			$message .= 'disabled';
		$aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login);
	} else {
		$message = $aseco->getChatMessage('NO_CPSSPEC');
		$aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login);
	}
}  // chat_cpsspec


function chat_sectms($aseco, $command) {

	$player = $command['author'];
	$login = $player->login;

	// check for relay server
	if ($aseco->server->isrelay) {
		$message = formatText($aseco->getChatMessage('NOTONRELAY'));
		$aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login);
		return;
	}

	if (!$total = $aseco->server->records->count()) {
		$aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No records found!'), $login);
		return;
	}

	// find sector count from first record with sector times
	$cpscnt = '?';
	for ($i = 0; $i < $total; $i++) {
		$cur_record = $aseco->server->records->getRecord($i);
		if (!empty($cur_record->checks)) {
			$cpscnt = count($cur_record->checks);
			break;
		}
	}

	// display ManiaLink window
	$head = 'Current TOP ' . $aseco->server->records->max . ' Local Sector Times (' . $cpscnt . '):';
	$cpsmax = 12;
	// compute widths
	$width = 0.1 + 0.18 + min($cpscnt, $cpsmax) * 0.1 + ($cpscnt > $cpsmax ? 0.06 : 0.0);
	if ($width < 1.0) $width = 1.0;
	$widths = array($width, 0.1, 0.18);
	for ($i = 0; $i < min($cpscnt, $cpsmax); $i++)
		$widths[] = 0.1; // cp
	if ($cpscnt > $cpsmax)
		$widths[] = 0.06;

	$msg = array();
	$lines = 0;
	$player->msgs = array();
	$player->msgs[0] = array(1, $head, $widths, array('BgRaceScore2', 'Podium'));

	// create list of records
	for ($i = 0; $i < $total; $i++) {
		$cur_record = $aseco->server->records->getRecord($i);
		$line = array();
		$line[] = str_pad($i+1, 2, '0', STR_PAD_LEFT) . '.';
		$line[] = ($cur_record->new ? '{#black}' : '') .
		          ($aseco->server->gameinfo->mode == Gameinfo::STNT ?
		           $cur_record->score : formatTime($cur_record->score));
		// append up to $cpsmax sector times
		if (!empty($cur_record->checks)) {
			$j = 1;
			$pr = 0;
			foreach ($cur_record->checks as $cp) {
				$line[] = '$n' . formatTime($cp - $pr);
				$pr = $cp;
				if (++$j > $cpsmax) {
					if ($cpscnt > $cpsmax) $line[] = '+';
					break;
				}
			}
		}
		$msg[] = $line;
		if (++$lines > 14) {
			$player->msgs[] = $msg;
			$lines = 0;
			$msg = array();
		}
	}
	// add if last batch exists
	if (!empty($msg))
		$player->msgs[] = $msg;

	// display ManiaLink message
	display_manialink_multi($player);
}  // chat_sectms


// called @ onPlayerConnect
function addplayer_cp($aseco, $player) {
	global $checkpoints;

	$login = $player->login;

	$checkpoints[$login] = new Checkpoints();
	// set first lap reference in Laps mode
	if ($aseco->server->gameinfo->mode == Gameinfo::LAPS)
		$checkpoints[$login]->curr_fin = 0;
	if ($aseco->settings['display_checkpoints']) {
		// set personal or default CPs
		if ($cps = ldb_getCPs($aseco, $login)) {
			$checkpoints[$login]->loclrec = $cps['cps'];
			$checkpoints[$login]->dedirec = $cps['dedicps'];
		} else {
			if ($aseco->settings['auto_enable_cps'])
				$checkpoints[$login]->loclrec = 0;
			if ($aseco->settings['auto_enable_dedicps'])
				$checkpoints[$login]->dedirec = 0;
		}
	}
}  // addplayer_cp

// called @ onPlayerDisconnect
function removeplayer_cp($aseco, $player) {
	global $checkpoints;

	$login = $player->login;

	ldb_setCPs($aseco, $login,
	           $checkpoints[$login]->loclrec, $checkpoints[$login]->dedirec);

	// free up memory
	unset($checkpoints[$login]);
}  // removeplayer_cp

// called @ onBeginMap
function reset_checkp($aseco, $map) {
	global $checkpoints, $laps_cpcount;

	// clear all checkpoints
	foreach ($checkpoints as $login => $cp) {
		$checkpoints[$login]->best_cps = array();
		$checkpoints[$login]->curr_cps = array();
		$checkpoints[$login]->best_fin = PHP_INT_MAX;
		if ($aseco->server->gameinfo->mode == Gameinfo::LAPS)
			$checkpoints[$login]->curr_fin = 0;
		else
			$checkpoints[$login]->curr_fin = PHP_INT_MAX;
	}

	// set local checkpoint references
	if ($aseco->settings['display_checkpoints']) {
		foreach ($checkpoints as $login => $cp) {
			$lrec = $checkpoints[$login]->loclrec - 1;

			// check for specific record
			if ($lrec+1 > 0) {
				// if specific record unavailable, use last one
				if ($lrec > $aseco->server->records->count() - 1)
					$lrec = $aseco->server->records->count() - 1;
				$curr = $aseco->server->records->getRecord($lrec);
				// check for valid checkpoints
				if (!empty($curr->checks) && $curr->score == end($curr->checks)) {
					$checkpoints[$login]->best_fin = $curr->score;
					$checkpoints[$login]->best_cps = $curr->checks;
				}
			}
			elseif ($lrec+1 == 0) {
				// search for own/last record
				$lrec = 0;
				while ($lrec < $aseco->server->records->count()) {
					$curr = $aseco->server->records->getRecord($lrec++);
					if ($curr->player->login == $login)
						break;
				}
				// check for valid checkpoints
				if (!empty($curr->checks) && $curr->score == end($curr->checks)) {
					$checkpoints[$login]->best_fin = $curr->score;
					$checkpoints[$login]->best_cps = $curr->checks;
				}
			}  // else -1
		}
	}

	// CP count only for Laps mode
	$laps_cpcount = $map->nbchecks;
}  // reset_checkp

// called @ onBeginRound
function clear_curr_cp($aseco) {
	global $checkpoints;

	// if Stunts mode or warm-up, bail out immediately
	if ($aseco->server->gameinfo->mode == Gameinfo::STNT || $aseco->warmup_phase) return;

	// clear current checkpoints
	foreach ($checkpoints as $login => $cp) {
		$checkpoints[$login]->curr_cps = array();
		// set first lap reference in Laps mode, otherwise max time
		if ($aseco->server->gameinfo->mode == Gameinfo::LAPS)
			$checkpoints[$login]->curr_fin = 0;
		else
			$checkpoints[$login]->curr_fin = PHP_INT_MAX;

		// reset CP panel unless spectator
		if ($checkpoints[$login]->loclrec != -1) {
			$player = $aseco->server->players->getPlayer($login);
			if (!$player->isspectator) {
				if ($aseco->settings['enable_cpsspec'] && !empty($checkpoints[$login]->speccers))
					display_cpspanel($aseco, $login . ',' . implode(',', $checkpoints[$login]->speccers), 0, '$00f -.--');
				else
					display_cpspanel($aseco, $login, 0, '$00f -.--');
			}
		}
	}
}  // clear_curr_cp

// called @ onEndMap
function disable_checkp($aseco, $data) {
	global $checkpoint_tests;

	// disable CP panels at end of map
	allcpspanels_off($aseco);

	$checkpoint_tests = true;  // now commence cheat tests
}  // disable_checkp

// called @ onRestartMap
function restart_checkp($aseco, $data) {
	global $checkpoints, $checkpoint_tests;

	// clear current checkpoints
	foreach ($checkpoints as $login => $cp) {
		$checkpoints[$login]->curr_cps = array();
		// set first lap reference in Laps mode, otherwise max time
		if ($aseco->server->gameinfo->mode == Gameinfo::LAPS)
			$checkpoints[$login]->curr_fin = 0;
		else
			$checkpoints[$login]->curr_fin = PHP_INT_MAX;
	}

	$checkpoint_tests = true;  // now commence cheat tests
}  // restart_checkp

// called @ onCheckpoint
// [0]=PlayerUid, [1]=Login, [2]=TimeOrScore, [3]=CurLap, [4]=CheckpointIndex
function store_checkp($aseco, $checkpt) {
	global $rasp, $checkpoints, $laps_cpcount, $checkpoint_tests,
	       $feature_stats;  // from rasp.settings.php

	// if Stunts mode, bail out immediately
	// no checkpoints during warm-up, so no need to check that
	if ($aseco->server->gameinfo->mode == Gameinfo::STNT) return;

	// if undefined login, bail out too
	$login = $checkpt[1];
	if (!isset($checkpoints[$login])) return;

	// check for Laps mode
	if ($aseco->server->gameinfo->mode != Gameinfo::LAPS) {

		// reset for next run in TimeAttack mode
		if ($aseco->server->gameinfo->mode == Gameinfo::TA && $checkpt[4] == 0)
			$checkpoints[$login]->curr_cps = array();

		// check for cheated checkpoints:
		// non-positive time, wrong index, or time less than preceding one
		if ($checkpt[2] <= 0 || $checkpt[4] != count($checkpoints[$login]->curr_cps) ||
		    ($checkpt[4] > 0 && $checkpt[2] < end($checkpoints[$login]->curr_cps))) {
			if ($checkpoint_tests) {
				$aseco->processCheater($login, $checkpoints[$login]->curr_cps, $checkpt, -1);
				return;
			}
		}

		// store current checkpoint
		$checkpoints[$login]->curr_cps[$checkpt[4]] = $checkpt[2];

		// check if displaying for this player, and for best checkpoints
		if ($checkpoints[$login]->loclrec != -1 &&
		    isset($checkpoints[$login]->best_cps[$checkpt[4]])) {

			// check whether not last one (Finish)
			$check = $checkpt[4] + 1;
			$diff = $checkpoints[$login]->curr_cps[$checkpt[4]] -
			        $checkpoints[$login]->best_cps[$checkpt[4]];
			// check for improvement
			if ($diff < 0) {
				$diff = abs($diff);
				$sign = '$00f-';  // blue
			} elseif ($diff == 0) {
				$sign = '$00f';  // blue
			} else {  // $diff > 0
				$sign = '$f00+';  // red
			}
			$sec = floor($diff/1000);
			$ths = $diff - ($sec * 1000);

			// check for Finish checkpoint
			if ($check == count($checkpoints[$login]->best_cps))
				$check = 'F';
			// update CP panel
			if ($aseco->settings['enable_cpsspec'] && !empty($checkpoints[$login]->speccers))
				display_cpspanel($aseco, $login . ',' . implode(',', $checkpoints[$login]->speccers), $check,
				                 $sign . sprintf('%d.%03d', $sec, $ths));
			else
				display_cpspanel($aseco, $login, $check,
				                 $sign . sprintf('%d.%03d', $sec, $ths));
		}

	} else {  // Laps

		// check for cheated checkpoints:
		// non-positive time, negative index
		if ($checkpt[2] <= 0 || $checkpt[4] < 0) {
			if ($checkpoint_tests) {
				$aseco->processCheater($login, $checkpoints[$login]->curr_cps, $checkpt, -1);
				return;
			}
		}

		// get relative CP in this lap
		$relcheck = $checkpt[4] % $laps_cpcount;

		// check for cheated checkpoints:
		// wrong index, time not more than reference, relative time less than preceding one
		if ($relcheck != count($checkpoints[$login]->curr_cps) ||
		    $checkpt[2] < $checkpoints[$login]->curr_fin ||
		    ($relcheck > 0 && $checkpt[2] - $checkpoints[$login]->curr_fin < end($checkpoints[$login]->curr_cps))) {
			if ($checkpoint_tests) {
				$aseco->processCheater($login, $checkpoints[$login]->curr_cps, $checkpt, -1);
				return;
			}
		}

		// store current checkpoint for current lap, relative to reference
		$checkpoints[$login]->curr_cps[$relcheck] = $checkpt[2] - $checkpoints[$login]->curr_fin;

		// check for a completed lap
		if ($checkpt[3] * $laps_cpcount != $checkpt[4] + 1) {

			// check if displaying for this player, and for best checkpoints
			if ($checkpoints[$login]->loclrec != -1 &&
			    isset($checkpoints[$login]->best_cps[$relcheck])) {

				// check for improvement
				$diff = $checkpoints[$login]->curr_cps[$relcheck] - $checkpoints[$login]->best_cps[$relcheck];
				if ($diff < 0) {
					$diff = abs($diff);
					$sign = '$00f-';  // blue
				} elseif ($diff == 0) {
					$sign = '$00f';  // blue
				} else {  // $diff > 0
					$sign = '$f00+';  // red
				}
				$sec = floor($diff/1000);
				$ths = $diff - ($sec * 1000);

				// update CP panel
				if ($aseco->settings['enable_cpsspec'] && !empty($checkpoints[$login]->speccers))
					display_cpspanel($aseco, $login . ',' . implode(',', $checkpoints[$login]->speccers), $relcheck + 1,
					                 $sign . sprintf('%d.%03d', $sec, $ths));
				else
					display_cpspanel($aseco, $login, $relcheck + 1,
					                 $sign . sprintf('%d.%03d', $sec, $ths));
			}

		} else {  // completed lap

			// store current lap finish as reference for next lap
			$checkpoints[$login]->curr_fin = $checkpt[2];

			// build a record object with the current lap information
			$finish_item = new Record();
			$finish_item->player = $aseco->server->players->getPlayer($login);
			$finish_item->score = $checkpoints[$login]->curr_cps[$relcheck];
			$finish_item->date = strftime('%Y-%m-%d %H:%M:%S');
			$finish_item->map = clone $aseco->server->map;
			unset($finish_item->map->gbx);  // reduce memory usage
			unset($finish_item->map->mx);

			// store current lap
			if ($feature_stats) {
				$rasp->insertTime($finish_item, implode(',', $checkpoints[$login]->curr_cps));
			}

			// process for local and Dedimania records
			$finish_item->new = true;  // set lap 'Finish' flag
			ldb_playerFinish($aseco, $finish_item);
			$finish_item->new = true;  // ditto
			if (function_exists('dedimania_playerfinish'))
				dedimania_playerfinish($aseco, $finish_item);

			// check for new best lap
			$diff = $checkpoints[$login]->curr_cps[$relcheck] - $checkpoints[$login]->best_fin;
			if ($diff < 0) {
				// store new best lap
				$checkpoints[$login]->best_fin = $checkpoints[$login]->curr_cps[$relcheck];
				$checkpoints[$login]->best_cps = $checkpoints[$login]->curr_cps;
				// store timestamp for sorting in case of equal bests
				$checkpoints[$login]->best_time = microtime(true);
			}

			// check if displaying for this player, and not first lap
			if ($checkpoints[$login]->loclrec != -1 && $checkpt[4] + 1 >= $laps_cpcount) {
				// check for improvement
				if ($diff < 0) {
					$diff = abs($diff);
					$sign = '$00f-';  // blue
				} elseif ($diff == 0) {
					$sign = '$00f';  // blue
				} else {  // $diff > 0
					$sign = '$f00+';  // red
				}
				$sec = floor($diff/1000);
				$ths = $diff - ($sec * 1000);

				// indicate Lap Finish checkpoint
				$relcheck = 'L';
				// update CP panel
				if ($aseco->settings['enable_cpsspec'] && !empty($checkpoints[$login]->speccers))
					display_cpspanel($aseco, $login . ',' . implode(',', $checkpoints[$login]->speccers), $relcheck,
					                 $sign . sprintf('%d.%03d', $sec, $ths));
				else
					display_cpspanel($aseco, $login, $relcheck,
					                 $sign . sprintf('%d.%03d', $sec, $ths));
			}

			// reset for next lap
			$checkpoints[$login]->curr_cps = array();
		}
	}
}  // store_checkp

// called @ onPlayerFinish1
function store_finish($aseco, $finish_item) {
	global $checkpoints, $checkpoint_tests;

	// if Laps or Stunts mode, bail out immediately
	// no finishes during warm-up, so no need to check that
	if ($aseco->server->gameinfo->mode == Gameinfo::LAPS ||
	    $aseco->server->gameinfo->mode == Gameinfo::STNT) return;

	$login = $finish_item->player->login;
	// in case of CP order problem
	sort($checkpoints[$login]->curr_cps);

	// check for actual finish
	if ($finish_item->score > 0) {
		// compute number of checkpoints (incl. multilaps except in TA mode)
		$reqchecks = $finish_item->map->nbchecks;
		if ($aseco->server->gameinfo->mode != Gameinfo::TA && $finish_item->map->laprace) {
			if ($finish_item->map->forcedlaps != 0)
				$reqchecks *= $finish_item->map->forcedlaps;
			else
				$reqchecks *= $finish_item->map->nblaps;
		}

		// check for required number of checkpoints
		if ($reqchecks != count($checkpoints[$login]->curr_cps)) {
			if ($checkpoint_tests) {
				trigger_error('CPs for ' . $login . ' required: ' . $reqchecks . '  present: ' . count($checkpoints[$login]->curr_cps) .
				              ' - ' . implode(',', $checkpoints[$login]->curr_cps), E_USER_WARNING);
			}
			// reset to prevent local/Dedimania records
			$finish_item->score = 0;
		// check for finish equal last checkpoint
		} elseif ($finish_item->score == end($checkpoints[$login]->curr_cps)) {
			$checkpoints[$login]->curr_fin = $finish_item->score;

		// check for improvement
			if ($checkpoints[$login]->curr_fin < $checkpoints[$login]->best_fin) {
				$checkpoints[$login]->best_fin = $checkpoints[$login]->curr_fin;
				$checkpoints[$login]->best_cps = $checkpoints[$login]->curr_cps;
				// store timestamp for sorting in case of equal bests
				$checkpoints[$login]->best_time = microtime(true);
			}
		} else {
			if ($checkpoint_tests) {
				$aseco->processCheater($login, $checkpoints[$login]->curr_cps, false, $finish_item->score);
				// reset to prevent local/Dedimania records
				$finish_item->score = 0;
			}
		}
	}
	// check for player retire in TimeAttack mode
	elseif ($aseco->server->gameinfo->mode == Gameinfo::TA && $finish_item->score == 0 && $checkpoints[$login]->loclrec != -1) {
		// reset CP panel
		if ($aseco->settings['enable_cpsspec'] && !empty($checkpoints[$login]->speccers))
			display_cpspanel($aseco, $login . ',' . implode(',', $checkpoints[$login]->speccers), 0, '$00f -.--');
		else
			display_cpspanel($aseco, $login, 0, '$00f -.--');
	}
}  // store_finish

// called @ onPlayerInfoChanged
function spec_togglecp($aseco, $playerinfo) {
	global $checkpoints;

	// if Stunts mode or warm-up, bail out immediately
	if ($aseco->server->gameinfo->mode == Gameinfo::STNT || $aseco->warmup_phase) return;

	$login = $playerinfo['Login'];
	$player = $aseco->server->players->getPlayer($login);
	// if no real spectator status change, bail out immediately
	if ($player->prevstatus == $player->isspectator) return;

	// check if CPS active
	if (isset($checkpoints[$login]) && $checkpoints[$login]->loclrec != -1) {
		// check spectator status
		if ($playerinfo['SpectatorStatus'] != 0) {
			// disable CP panel
			if ($aseco->settings['enable_cpsspec'] && !empty($checkpoints[$login]->speccers))
				cpspanel_off($aseco, $login . ',' . implode(',', $checkpoints[$login]->speccers));
			else
				cpspanel_off($aseco, $login);
		} else {
			// enable CP panel
			if ($aseco->settings['enable_cpsspec'] && !empty($checkpoints[$login]->speccers))
				display_cpspanel($aseco, $login . ',' . implode(',', $checkpoints[$login]->speccers), 0, '$00f -.--');
			else
				display_cpspanel($aseco, $login, 0, '$00f -.--');
		}
	}

	// check for spectated player update
	if ($aseco->settings['enable_cpsspec']) {
		// check if /cpsspec enabled
		if ($player->speclogin != '') {
			$targetid = floor($playerinfo['SpectatorStatus'] / 10000);

			// check for player status or free camera
			if ($playerinfo['SpectatorStatus'] == 0 || $targetid == 255) {
				// if subscribed, unsubscribe first
				if ($player->speclogin != ',' && isset($checkpoints[$player->speclogin])) {
					if (($i = array_search($login, $checkpoints[$player->speclogin]->speccers)) !== false)
						unset($checkpoints[$player->speclogin]->speccers[$i]);
				}
				$player->speclogin = ',';  // no target
			} else {
				// ignore stray self-spectating PlayerInfoChanged events
				if ($player->pid != $targetid) {
					// find login for target
					foreach ($aseco->server->players->player_list as $pl) {
						if ($pl->pid == $targetid) {
							$player->speclogin = $pl->login;
							// subscribe to this player
							if (!in_array($login, $checkpoints[$player->speclogin]->speccers))
								$checkpoints[$player->speclogin]->speccers[] = $login;
							break;
						}
					}
				}
			}
		}
	}
}  // spec_togglecp
?>
