[OTS] Check monsters spawn time in spawn.xml

This script reads spawn.xml file and list monsters sorted by spawn time. You can easily detect monsters with too low or too high spawn times.

<?php

$spawnsFile = 'ots-spawn.xml';

function element_attributes($element_name, $xml)
{
    if ($xml == false) {
        return false;
    }
    $found = preg_match(
        '#<' . $element_name .
        '\s+([^>]+(?:"|\'))\s?/?>#',
        $xml,
        $matches
    );
    if ($found == 1) {
        $attribute_array = array();
        $attribute_string = $matches[1];
        $found = preg_match_all(
            '#([^\s=]+)\s*=\s*(\'[^<\']*\'|"[^<"]*")#',
            $attribute_string,
            $matches,
            PREG_SET_ORDER
        );
        if ($found != 0) {
            foreach ($matches as $attribute) {
                $attribute_array[$attribute[1]] =
                    substr($attribute[2], 1, -1);
            }
            return $attribute_array;
        }
    }
    return false;
}

$monsters = [];

foreach (file($spawnsFile) as $lineNo => $line) {
    $line = trim($line);
    if (substr($line, 0, 8) === '<monster') {
        $monsterAttributes = element_attributes('monster', $line);
        if (is_array($monsterAttributes)) {
            if (isset($monsterAttributes['name']) && isset($monsterAttributes['spawntime'])) {
                $name = $monsterAttributes['name'];
                $spawnTime = $monsterAttributes['spawntime'];

                $monsters[] = [
                    'line' => $lineNo,
                    'name' => $monsterAttributes['name'],
                    'spawnTime' => (int)$monsterAttributes['spawntime'],
                ];
            }
        }
    }
}

usort($monsters, function ($a, $b) {
    return $a['spawnTime'] < $b['spawnTime'];
});

foreach ($monsters as $monster) {
    echo $monster['line'] . ',' . $monster['name'] . ',' . $monster['spawnTime'] . PHP_EOL;
}

PHP file, directory, soft link and hard link information

What PHP functions file_exists, is_file, is_dir and is_link return for file, directory, hard link and soft link?

<?php

/*
touch file
mkdir dir
ln file file_hard_link
ln -s file file_soft_link
ln -s dir dir_soft_link
touch dir/file_in_dir
echo 1 > file
cat file file_hard_link file_soft_link
*/

$tests = [
    'file',
    'dir',
    'file_hard_link',
    'file_soft_link',
    'dir_soft_link',
    'dir/file_in_dir',
    'dir_soft_link/file_in_dir'
];

foreach ($tests as $test) {
    echo 'name: ', str_pad($test, 25) . ' ';
    echo 'file_exists: ', (int)file_exists($test) . ' ';
    echo 'is_file: ', (int)is_file($test)  . ' ';
    echo 'is_dir: ', (int)is_dir($test) . ' ';
    echo 'is_link: ', (int)is_link($test) . PHP_EOL;
}

Result:

name: file                      file_exists: 1 is_file: 1 is_dir: 0 is_link: 0
name: dir                       file_exists: 1 is_file: 0 is_dir: 1 is_link: 0
name: file_hard_link            file_exists: 1 is_file: 1 is_dir: 0 is_link: 0
name: file_soft_link            file_exists: 1 is_file: 1 is_dir: 0 is_link: 1
name: dir_soft_link             file_exists: 1 is_file: 0 is_dir: 1 is_link: 1
name: dir/file_in_dir           file_exists: 1 is_file: 1 is_dir: 0 is_link: 0
name: dir_soft_link/file_in_dir file_exists: 1 is_file: 1 is_dir: 0 is_link: 0

[PHP] Read SI formatted numbers as floats

Simple function to convert numbers from SI notation to float:
– decimal point separator – dot or comma
– thousands separator – space

In this case used to load money value from strings in different formats.

Examples:
##
# ###
# ###, #
# ###.#
#,###
12 -> 12
1 234 -> 1234
1 234,5 -> 1234.5
1 234.5 -> 1234.5
1,234 -> 1.234

<?php
$validTests = [
    '1 234',
    '-1 234',
    '- 1 234',
    '1 234.00',
    '-1 234.00',
    '1 234,00',
    '-1 234,00',
    '1234',
    '-1234',
    '1234.00',
    '-1234.00',
    '1234,00',
    '-1234,00',
    '1 234,00000000000',
    '1234,00000000000',
    '1 234,34545654565',
    '-1 234,34545654565',
    '1234,34545654565',
    '-1234,34545654565',
    '1 234 567',
    '1 234 567.00343',
    '1 234 567,34565',
    '1.234',
];

$invalidTests = [
    '-1 $',
    '1,234.00',
    '1.234,00',
    '-1.234,00',
    '-1.234.567',
    '-1.234.567,00',
    '-1,234,567.00',
];

function getAmountValue($money)
{
    $value = trim($money);
    $value = str_replace(' ', '', $value);

    $invalidCharacters = preg_replace('/([0-9\.,-])/i', '', $value);
    if (!empty($invalidCharacters)) {
        throw new InvalidArgumentException('Money value string "' . $value . '" contains invalid characters: "' . $invalidCharacters . '"!');
    }

    $separatorsCount = mb_substr_count($value, '.') + mb_substr_count($value, ',');
    if ($separatorsCount > 1) {
        throw new InvalidArgumentException('Money value string "' . $value . '" contains more than 1 separator!');
    }
    $value = str_replace(',', '.', $value);

    return (float)$value;
}

foreach ($validTests as $money) {
    try {
        $value = getAmountValue($money);
    } catch (Exception $exception) {
        $value = $exception->getMessage();
    }
    echo var_export($money, true) . ' = ' . var_export($value, true) . PHP_EOL;
}

echo '--- INVALID ---' . PHP_EOL;

foreach ($invalidTests as $money) {
    try {
        $value = getAmountValue($money);
    } catch (Exception $exception) {
        $value = $exception->getMessage();
    }
    echo var_export($money, true) . ' = ' . var_export($value, true) . PHP_EOL;
}

Gesior2012 – block using monster names as new player name

In acc. maker in file system/load.compat.php find function check_name_new_char and above return true add:

    if (stripos(",The Queen of Banshee,Rook,Orc,",
            ',' . $name . ',') !== false) {
        return false;
    }

Line where you need to paste it: https://github.com/gesior/Gesior2012/blob/268f477369c5c4a8ba88084224142941cba15654/system/load.compat.php#L142

List of monsters of your OTS you can generate using http://halp.skalski.pro/2020/05/21/ots-generating-list-of-monster-names/

Set/change PHP version command [Ubuntu/Debian]

Command to change PHP version in system and Apache2 with multiple PHP versions installed (from https://launchpad.net/~ondrej/+archive/ubuntu/php )

#!/bin/sh

if [ -z "$1" ]
then
        echo 'PHP version required'
else
        if test -f "/usr/bin/php"$1; then
                sudo a2dismod php7.0 > /dev/null
                sudo a2dismod php7.1 > /dev/null
                sudo a2dismod php7.2 > /dev/null
                sudo a2dismod php7.3 > /dev/null
                sudo a2dismod php7.4 > /dev/null
                sudo a2enmod php$1 > /dev/null
                systemctl restart apache2

                sudo ln -fs /usr/bin/php$1 /etc/alternatives/php
                sudo ln -fs /usr/bin/php-config$1 /etc/alternatives/php-config
                sudo ln -fs /usr/bin/phpdbg$1 /etc/alternatives/phpdbg
                sudo ln -fs /usr/bin/phpize$1 /etc/alternatives/phpize
                sudo ln -fs /usr/bin/phar$1 /etc/alternatives/phar
                sudo ln -fs /usr/bin/phar.phar$1 /etc/alternatives/phar.phar
                echo 'Changed PHP version to: '$1
        else
                echo 'Wrong PHP version: '$1
        fi
fi

PHP TibiaCam file reader

Simple reader for uncompressed TibiaCam files (binary files, up to 60MB).

It’s also example of fast reading binary data from file and handling it as stream.

<?php

class BinaryData
{
    /** @var resource */
    private $data;
    /** @var int */
    private $length;

    /**
     * BinaryData constructor.
     * @param string $data
     */
    public function __construct($data)
    {
        $this->data = fopen('php://memory', 'wb+');
        fwrite($this->data, $data);
        $this->length = ftell($this->data);
        rewind($this->data);
    }

    /**
     * @return int
     */
    public function getLength()
    {
        return $this->length;
    }

    /**
     * @return bool
     */
    public function isEof()
    {
        return ftell($this->data) == $this->length;
    }

    /**
     * @param int $length
     */
    public function skip($length)
    {
        fseek($this->data, $length, SEEK_CUR);
    }

    /**
     * @return int
     */
    public function getU8()
    {
        return unpack('C', fread($this->data, 1))[1];
    }

    /**
     * @return int
     */
    public function getU16()
    {
        return unpack('v', fread($this->data, 2))[1];
    }

    /**
     * @return int
     */
    public function getU32()
    {
        return unpack('V', fread($this->data, 4))[1];
    }

    /**
     * @return int
     */
    public function getU64()
    {
        return unpack('P', fread($this->data, 8))[1];
    }

    /**
     * @param int $length
     * @return string
     */
    public function getString($length = -1)
    {
        if ($length == -1) {
            $length = $this->getU16();
        }

        return fread($this->data, $length);
    }

}

class NetworkPacket extends BinaryData
{
    /**
     * 0x01 = packets sent to player
     * 0x03 = packets received from player
     * 0x05 = refresh packets for fast cam rewinding
     */
    const TYPE_OUT = 0x01;
    const TYPE_IN = 0x03;
    const TYPE_REFRESH = 0x05;

    /** @var int */
    private $type;

    /**
     * NetworkPacket constructor.
     * @param string $data
     */
    public function __construct($data)
    {
        parent::__construct($data);
        $this->type = $this->getU8();
    }

    /**
     * @return int
     */
    public function getType()
    {
        return $this->type;
    }

}

class Cam extends BinaryData
{
    /**
     * Cam constructor.
     * @param string $filePath path to cam file
     */
    public function __construct($filePath)
    {
        parent::__construct(file_get_contents($filePath));
    }

    /**
     * @return array
     */
    public function readTibiaCamAttributes()
    {
        $this->skip(8); // text: TIBIACAM

        $camVersion = $this->getU8();
        $playerId = $this->getU32();
        $playerName = $this->getString();
        $protocolVersion = $this->getU16();
        $timeCreated = $this->getU64();

        return [$camVersion, $playerId, $playerName, $protocolVersion, $timeCreated];
    }

}

$fileName = 'nowy.cam.ready';
$cam = new Cam($fileName);
$camAttributes = $cam->readTibiaCamAttributes();
var_dump($camAttributes);

while (!$cam->isEof()) {
    $packetTime = $cam->getU64();
    var_dump($packetTime);
    $packet = new NetworkPacket($cam->getString());
    var_dump($packet->getType());
    var_dump($packet->getLength());
}

Simple PHP router

Apache .htaccess (redirect all requests to not existing files/directories to index.php):

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php [QSA,L]

PHP router file. Check if route is on list of available routes and include file by name of route. Some parts of route can be variables: ‘user/$name’. Variables can have default values: ‘users/$order=id’, default value can be empty string: ‘user/$name=’

<?php
var_dump((string)$_SERVER['REDIRECT_URL']);
$urlParts = explode('/', trim((string)$_SERVER['REDIRECT_URL'], '/'));

$routes = [
    'login',
    'logout',
    'user/$name',
    'users/list/$type=level/$page=2/$vocation=all',
];

function openRoute($pathParts, $vars)
{
    $file = 'pages/' . implode('_', $pathParts) . '.php';

    ob_start();
    extract($vars);
    $main_content = '';
    include($file);
    $main_content .= ob_get_clean();

    return $main_content;
}

$routeFound = false;

foreach ($routes as $route) {
    $routeParts = explode('/', $route);
    $vars = [];
    $pathParts = [];

    if (count($routeParts) >= count($urlParts)) {
        foreach ($routeParts as $partId => $routePart) {
            if ($routePart[0] === '$') {
                $optionalVar = strpos($routePart, '=') !== false;

                if ($optionalVar) {
                    list($varName, $defaultValue) = explode('=', $routePart);
                    $varName = substr($varName, 1);
                } else {
                    $varName = substr($routePart, 1);
                }

                if (isset($urlParts[$partId])) {
                    $vars[$varName] = $urlParts[$partId];
                } elseif ($optionalVar) {
                    $vars[$varName] = $defaultValue;
                } else {
                    continue 2;
                }
            } elseif (isset($urlParts[$partId]) && $routePart === $urlParts[$partId]) {
                $pathParts[] = $routePart;
            } else {
                continue 2;
            }
        }

        var_dump('found: ', $route, $vars, $pathParts);
        $main_content = openRoute($pathParts, $vars);
        $routeFound = true;

        break;
    }
}

if (!$routeFound) {
    http_response_code(404);
    echo '404 Not found';
    exit;
}

Fast configure multiple PHP versions php.ini

Script to configure PHP variables for development environment.

<?php

$phpDir = '/etc/php';

$backupPath = __DIR__ . DIRECTORY_SEPARATOR . 'config_backup.' . date('Y-m-d_H-i-s') . '.zip';
echo 'Backup config file: ' . $backupPath . PHP_EOL;
system('zip -r ' . $backupPath . ' ' . $phpDir . ' > /dev/null');
chmod($backupPath, 0777);

$values = [
	'upload_max_filesize' => '256M',
	'post_max_size' => '256M',
	'max_file_uploads' => 100,
	'memory_limit' => '1024M',
	'max_execution_time' => 15,
	'error_reporting' => 'E_ALL & ~E_DEPRECATED & ~E_STRICT',
	'display_errors' => 'Off',
	'log_errors' => 'On',
	'xdebug.remote_enable' => '1',
];
setPhpConfigValues($values, $phpDir, ['5.6', '7.0', '7.1', '7.2', '7.3'], ['apache2', 'fpm']);

$values = [
	'memory_limit' => -1,
	'max_execution_time' => 0,
	'error_reporting' => 'E_ALL & ~E_DEPRECATED & ~E_STRICT',
	'display_errors' => 'On',
	'log_errors' => 'Off',
];
setPhpConfigValues($values, $phpDir, ['5.6', '7.0', '7.1', '7.2', '7.3'], ['cli']);

system('systemctl restart apache2');
foreach(['5.6', '7.0', '7.1', '7.2', '7.3'] as $version) {
	system('systemctl restart php' . $version . '-fpm');
}

function setPhpConfigValues($values, $phpDir, $phpVersions, $phpTypes)
{
	foreach($phpVersions as $phpVersion) {
		foreach($phpTypes as $phpType) {
			$filePath = $phpDir . DIRECTORY_SEPARATOR . $phpVersion . DIRECTORY_SEPARATOR . $phpType . DIRECTORY_SEPARATOR . 'php.ini';
			if (file_exists($filePath)) {
				if (is_readable($filePath)) {
					if (is_writable($filePath)) {
						$lines = file($filePath);
						echo 'FILE: ' . $filePath . PHP_EOL;
						foreach($values as $searchKey => $newValue) {
							foreach($lines as $line => $text) {
								if (substr_count($text, '=') > 0) {
									list($lineKey, $lineValue) = explode('=', $text, 2);

									if (strpos($lineKey, $searchKey) !== false) {
										if (trim($lineValue) != $newValue) {
											echo 'Change key "' . $searchKey . '" value from "' . trim($lineValue) . '" to "' . $newValue . '"' . PHP_EOL;
											$lines[$line] = $searchKey . ' = ' . $newValue . PHP_EOL;
										}
										continue 2;
									}
								}
							}

							echo 'Add key "' . $searchKey . '" with value"' . $newValue . '"' . PHP_EOL;
							$lines[] = $searchKey . ' = ' . $newValue . PHP_EOL;
						}
						file_put_contents($filePath, implode('', $lines));
						echo 'SAVE FILE: ' . $filePath . PHP_EOL;
					} else {
						echo 'Config file not writable: ' . $filePath . PHP_EOL;
					}
				} else {
					echo 'Config file not readable: ' . $filePath . PHP_EOL;
				}
			} else {
				echo 'Config file not found: ' . $filePath . PHP_EOL;
			}
		}
	}
}

PHP function isIpInRanges($ip, $ranges)

Function that checks if given IP is within one of given IP ranges.

<?php
/**
 * Checks if given IP is in one of IP ranges
 * @param string|int $ip in format x.x.x.x or unsigned int
 * @param string[] $ranges in format IP/NETMASK
 * @return bool
 */
function isIpInRanges($ip, $ranges)
{
    if (is_numeric($ip))
        $ip_dec = $ip;
    else
        $ip_dec = ip2long($ip);

    foreach ($ranges as $range) {
        if (strpos($range, '/') === false)
            $range .= '/32';

        list($range, $netmask) = explode('/', $range, 2);
        $x = explode('.', $range);
        while (count($x) < 4) $x[] = '0';
        $range = sprintf("%u.%u.%u.%u", $x[0], $x[1], $x[2], $x[3]);
        $range_dec = ip2long($range);

        $wildcard_dec = pow(2, (32 - $netmask)) - 1;
        $netmask_dec = ~$wildcard_dec;

        if (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec))
            return true;
    }

    return false;
}

// TESTS
var_dump(isIpInRanges('127.0.0.1', [])); // false
var_dump(isIpInRanges('127.0.0.1', ['127.0.0.1'])); // true
var_dump(isIpInRanges('127.0.0.1', ['127.0.0.0/32'])); // false
var_dump(isIpInRanges('127.0.0.1', ['127.0.0.0/24'])); // true
var_dump(isIpInRanges('1.1.1.1', ['1.0.0.0/16', '1.1.0.0/16'])); // true
var_dump(isIpInRanges('192.168.11.11', ['192.168.11/24'])); // true
var_dump(isIpInRanges('192.168.11.11', ['192.168.11/22'])); // true
var_dump(isIpInRanges('192.168.11.11', ['192.168/29'])); // false

var_dump(isIpInRanges(3000000000, ['178.208.94.0'])); // true
var_dump(isIpInRanges(3000000000, ['178/8'])); // true