[TFS 1.x+] Generate big depot

LUA code to generate backpack full of backpacks:

function fillContainer(container, level, innerContainerCount)
    if level > 0 then
        for i = 1, innerContainerCount do
            local innerContainer = Game.createItem(2000)
            container:addItemEx(innerContainer)
            fillContainer(innerContainer, level - 1, innerContainerCount)
        end
    else
        for i = 1, 20 do
            local item = Game.createItem(i % 2 == 0 and 2376 or 2400)
            container:addItemEx(item)
        end
    end
end

function generateBigContainer(player, innerContainerCount, deepness)
    local item = player:getTile():addItem(2000)
    fillContainer(item, deepness, innerContainerCount)
    return item
end

Code to call it:

!lua generateBigContainer(player, 20, 2)

Requires !lua debug:

https://otland.net/threads/debug-lua-scripts-with-talkaction.112027/

https://otland.net/threads/tfs-1-x-luajit-debug-lua-scripts-with-talkaction.260910/

https://otland.net/threads/tfs-1-3-lua-5-2-debug-lua-scripts-with-talkaction.266086/

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());
}

OTS Restarter script

restart.sh with console logs with timestamp and binary file copy (for GDB)

#!/bin/bash
ulimit -c unlimited
while true; do

 cp kasteria bins/kasteria_`date '+%Y-%m-%d_%H-%M'`_`crc32 kasteria`
 stdbuf -o 0 ./kasteria 2>&1 | ts '%Y-%m-%d_%H-%M-%.S' | tee -a 'console/console_'`date '+%Y-%m-%d'`'.log';

 echo START SLEEP FOR 3 SECONDS, PRESS CTRL+C TO TURN OFF RESTARTER
 sleep 3
 echo END SLEEP

done

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;
}

Autowalk/cavebot for OTClient

Someone asked me about script that will make character run to given position and after reaching it run to another. I made it for free, so I decided to publish it as it may be interesting for others.

It’s not ready-to-run module, but I think it’s very close. For tests I pasted parts of it to ‘battle’ module and it worked.

This code requires ‘BOT_PROTECTION’ disabled in OTClient.

local walkEvent = nil
local positionsList = {
    { x = 32608, y = 32131, z = 7 },
    { x = 32601, y = 32131, z = 7 },
    { x = 32601, y = 32138, z = 7 },
    { x = 32608, y = 32138, z = 7 },
}

local isAttacking = 0
local isFollowing = 0
local currentTargetPositionId = 1

local autowalkTargetPosition = positionsList[currentTargetPositionId]

function init()
    connect(LocalPlayer, {
        onPositionChange = onCreaturePositionChange
    })

    connect(g_game, {
        onAttackingCreatureChange = onAttack,
        onFollowingCreatureChange = onFollow,
        onDisappear = onCreatureDisappear
    })
end

function terminate()
    disconnect(LocalPlayer, {
        onPositionChange = onCreaturePositionChange
    })

    disconnect(g_game, {
        onAttackingCreatureChange = onAttack,
        onFollowingCreatureChange = onFollow,
        onDisappear = onCreatureDisappear
    })
end

function walkToTarget()
    if not g_game.isOnline() then
        walkEvent = scheduleEvent(walkToTarget, 500)
        return
    end

    if g_game.getLocalPlayer():getStepTicksLeft() > 0 then
        -- wait until walk animation finish
        walkEvent = scheduleEvent(walkToTarget, g_game.getLocalPlayer():getStepTicksLeft())
        return
    end

    if isAttacking or isFollowing or not autowalkTargetPosition then
        -- do not walk with selected attack/follow target or when there is no target position
        walkEvent = scheduleEvent(walkToTarget, 100)
        return
    end

    -- fast search path on minimap (known tiles)
    steps, result = g_map.findPath(g_game.getLocalPlayer():getPosition(), autowalkTargetPosition, 5000, 0)
    if result == PathFindResults.Ok then
        g_game.walk(steps[1], true)
    elseif result == PathFindResults.Position then
        -- on target position
        currentTargetPositionId = currentTargetPositionId + 1
        autowalkTargetPosition = positionsList[(currentTargetPositionId % #positionsList) + 1]
    else
        -- slow search path on minimap, if not found, start 'scanning' map
        steps, result = g_map.findPath(g_game.getLocalPlayer():getPosition(), autowalkTargetPosition, 25000, 1)
        if result == PathFindResults.Ok then
            g_game.walk(steps[1], true)
        end
    end

    -- limit steps to 10 per second (100 ms between steps)
    walkEvent = scheduleEvent(walkToTarget, math.max(100, g_game.getLocalPlayer():getStepTicksLeft()))
end

function onCreaturePositionChange(creature, newPos, oldPos)
    if not walkEvent then
        -- start automoving 1 second after logging into game
        walkEvent = scheduleEvent(walkToTarget, 1000)
    end
end

function onAttack(creature)
    isAttacking = creature and creature:getId() or 0
end

function onFollow(creature)
    isFollowing = creature and creature:getId() or 0
end

function onCreatureDisappear(creature)
    if isAttacking == creature:getId() then
        isAttacking = 0
    end
    if isFollowing == creature:getId() then
        isFollowing = 0
    end
end

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

Linux Mint 19 – PHP/MariaDB dev env v1

apt install htop screen screenie mc nano tcpdump vim mariadb-server mariadb-client sshfs filezilla dbeaver-ce postgresql wireshark

Hasło do konta 'root' MariaDB ustawić na 'root'.

mysql
UPDATE mysql.user SET plugin = '', host = '%';
flush privileges;
SET PASSWORD FOR 'root'@'%' = PASSWORD('root');
flush privileges;

add-apt-repository ppa:ondrej/php
add-apt-repository ppa:ondrej/apache2
apt update

apt install apache2

apt install php5.6 php5.6-fpm php5.6-bcmath php5.6-bz2 php5.6-calendar php5.6-cli php5.6-common php5.6-ctype php5.6-curl php5.6-dba php5.6-dev php5.6-dom php5.6-exif php5.6-fileinfo php5.6-gd php5.6-gettext php5.6-gmp php5.6-iconv php5.6-imagick php5.6-intl php5.6-json php5.6-ldap php5.6-mailparse php5.6-mbstring php5.6-mongodb php5.6-mysql php5.6-mysqli php5.6-mysqlnd php5.6-oauth php5.6-odbc php5.6-opcache php5.6-pdo php5.6-pdo-dblib php5.6-pdo-mysql php5.6-pdo-odbc php5.6-pdo-pgsql php5.6-pdo-sqlite php5.6-pgsql php5.6-phar php5.6-phpdbg php5.6-posix php5.6-radius php5.6-readline php5.6-redis php5.6-simplexml php5.6-smbclient php5.6-soap php5.6-sockets php5.6-solr php5.6-sqlite3 php5.6-ssh2 php5.6-sybase php5.6-tokenizer php5.6-uploadprogress php5.6-uuid php5.6-xdebug php5.6-xml php5.6-xmlreader php5.6-xmlrpc php5.6-xmlwriter php5.6-yaml php5.6-zip

apt install php7.0 php7.0-fpm php7.0-bcmath php7.0-bz2 php7.0-calendar php7.0-cli php7.0-common php7.0-ctype php7.0-curl php7.0-dba php7.0-dev php7.0-dom php7.0-exif php7.0-fileinfo php7.0-gd php7.0-gettext php7.0-gmp php7.0-http php7.0-iconv php7.0-imagick php7.0-intl php7.0-json php7.0-ldap php7.0-mailparse php7.0-mbstring php7.0-mongodb php7.0-mysql php7.0-mysqli php7.0-mysqlnd php7.0-oauth php7.0-odbc php7.0-opcache php7.0-pdo php7.0-pdo-dblib php7.0-pdo-mysql php7.0-pdo-odbc php7.0-pdo-pgsql php7.0-pdo-sqlite php7.0-pgsql php7.0-phar php7.0-phpdbg php7.0-posix php7.0-radius php7.0-readline php7.0-redis php7.0-simplexml php7.0-smbclient php7.0-soap php7.0-sockets php7.0-solr php7.0-sqlite3 php7.0-ssh2 php7.0-sybase php7.0-tokenizer php7.0-uploadprogress php7.0-uuid php7.0-xdebug php7.0-xml php7.0-xmlreader php7.0-xmlrpc php7.0-xmlwriter php7.0-yaml php7.0-zip

apt install php7.1 php7.1-fpm php7.1-bcmath php7.1-bz2 php7.1-calendar php7.1-cli php7.1-common php7.1-ctype php7.1-curl php7.1-dba php7.1-dev php7.1-dom php7.1-exif php7.1-fileinfo php7.1-gd php7.1-gettext php7.1-gmp php7.1-http php7.1-iconv php7.1-imagick php7.1-intl php7.1-json php7.1-ldap php7.1-mailparse php7.1-mbstring php7.1-mongodb php7.1-mysql php7.1-mysqli php7.1-mysqlnd php7.1-oauth php7.1-odbc php7.1-opcache php7.1-pdo php7.1-pdo-dblib php7.1-pdo-mysql php7.1-pdo-odbc php7.1-pdo-pgsql php7.1-pdo-sqlite php7.1-pgsql php7.1-phar php7.1-phpdbg php7.1-posix php7.1-radius php7.1-readline php7.1-redis php7.1-simplexml php7.1-smbclient php7.1-soap php7.1-sockets php7.1-solr php7.1-sqlite3 php7.1-ssh2 php7.1-sybase php7.1-tokenizer php7.1-uploadprogress php7.1-uuid php7.1-xdebug php7.1-xml php7.1-xmlreader php7.1-xmlrpc php7.1-xmlwriter php7.1-yaml php7.1-zip

apt install php7.2 php7.2-fpm php7.2-bcmath php7.2-bz2 php7.2-calendar php7.2-cli php7.2-common php7.2-ctype php7.2-curl php7.2-dba php7.2-dev php7.2-dom php7.2-exif php7.2-fileinfo php7.2-gd php7.2-gettext php7.2-gmp php7.2-http php7.2-iconv php7.2-imagick php7.2-intl php7.2-json php7.2-ldap php7.2-mailparse php7.2-mbstring php7.2-mongodb php7.2-mysql php7.2-mysqli php7.2-mysqlnd php7.2-oauth php7.2-odbc php7.2-opcache php7.2-pdo php7.2-pdo-dblib php7.2-pdo-mysql php7.2-pdo-odbc php7.2-pdo-pgsql php7.2-pdo-sqlite php7.2-pgsql php7.2-phar php7.2-phpdbg php7.2-posix php7.2-radius php7.2-readline php7.2-redis php7.2-simplexml php7.2-smbclient php7.2-soap php7.2-sockets php7.2-solr php7.2-sqlite3 php7.2-ssh2 php7.2-sybase php7.2-tokenizer php7.2-uploadprogress php7.2-uuid php7.2-xdebug php7.2-xml php7.2-xmlreader php7.2-xmlrpc php7.2-xmlwriter php7.2-yaml php7.2-zip

apt install php7.3 php7.3-fpm php7.3-bcmath php7.3-bz2 php7.3-calendar php7.3-cli php7.3-common php7.3-ctype php7.3-curl php7.3-dba php7.3-dev php7.3-dom php7.3-exif php7.3-fileinfo php7.3-gd php7.3-gettext php7.3-gmp php7.3-http php7.3-iconv php7.3-imagick php7.3-intl php7.3-json php7.3-ldap php7.3-mailparse php7.3-mbstring php7.3-mongodb php7.3-mysql php7.3-mysqli php7.3-mysqlnd php7.3-oauth php7.3-odbc php7.3-opcache php7.3-pdo php7.3-pdo-dblib php7.3-pdo-mysql php7.3-pdo-odbc php7.3-pdo-pgsql php7.3-pdo-sqlite php7.3-pgsql php7.3-phar php7.3-phpdbg php7.3-posix php7.3-radius php7.3-readline php7.3-redis php7.3-simplexml php7.3-smbclient php7.3-soap php7.3-sockets php7.3-solr php7.3-sqlite3 php7.3-ssh2 php7.3-sybase php7.3-tokenizer php7.3-uploadprogress php7.3-uuid php7.3-xdebug php7.3-xml php7.3-xmlreader php7.3-xmlrpc php7.3-xmlwriter php7.3-yaml php7.3-zip


wyłączyć 'raphf' w PHP 5.6 (crashuje PHPa, a jest zaciągane razem z którąś z paczek)
rm /etc/php/5.6/cli/conf.d/20-raphf.ini /etc/php/5.6/fpm/conf.d/20-raphf.ini /etc/php/5.6/phpdbg/conf.d/20-raphf.ini
wyłączyć 'propro' i 'http', mają zależności do 'raphf' w PHP 5.6



a2enmod proxy
a2enmod proxy_fcgi

zmienić konfig PHPa:

/etc/php/5.6/fpm/php.ini
/etc/php/7.0/fpm/php.ini
/etc/php/7.1/fpm/php.ini
/etc/php/7.2/fpm/php.ini
/etc/php/7.3/fpm/php.ini

upload_max_filesize = 128M
post_max_size = 256M
memory_limit = 1024M
max_execution_time = 0

systemctl restart php5.6-fpm php7.0-fpm php7.1-fpm php7.2-fpm php7.3-fpm

a2enconf php5.6-fpm php7.0-fpm php7.1-fpm php7.2-fpm php7.3-fpm

// konfig domen domyslny wywalic, dodac konfig dla domen php**.sbg.best

<VirtualHost *:80>
        ServerName php56.sbg.best 

        ServerAdmin webmaster@php56.sbg.best
        DocumentRoot /home/jskalski/p/

        <FilesMatch \.php$>
                SetHandler "proxy:unix:/var/run/php/php5.6-fpm.sock|fcgi://localhost/"
        </FilesMatch>

        ErrorLog ${APACHE_LOG_DIR}/error_56.log
        CustomLog ${APACHE_LOG_DIR}/access_56.log combined
</VirtualHost>



systemctl restart apache2



Eloth start scripts for GMs

ROOK

Create fast monster spawn

Spawn “Snake” every second (1000 ms) for 5 seconds:

!lua function fms(m, p, c) doSummonCreature(m, p, true, true) if (c > 1) then addEvent(fms, 1000, m, p, c-1) end end fms("Spider", getCreaturePosition(cid), 5)

Mass spawn around GOD:

!lua for i = 1, 20 do doSummonCreature("Spider", getCreaturePosition(cid), true, true) end

Spawn temple NPC

For FACC towns:

/s The Oracle

For PACC towns:

/s The Gatekeeper

MAIN

Open Desert Quest

Create teleports that allow 20+ levels do quest solo without items:

!lua doCreateTeleport(1387, Position(32672, 32102, 8), Position(32647, 32092, 7)) doCreateTeleport(1387, Position(32672, 32070, 8), Position(32673, 32089, 8))

Generate big depot

LUA code to generate backpack full of backpacks:

function fill(container, level)
	for i = 1, 20 do
		local innerContainer = doAddContainerItem(container, 2000)
		if level > 0 then
			fill(innerContainer, level - 1)
		end
	end
end
container = doCreateItem(2000, getThingPos(cid)) fill(container, 2)

One-liner for !lua debug talkaction:

function fill(c, l) for i = 1, 20 do ic = doAddContainerItem(c, 2000) if l > 0 then fill(ic, l -1) end end end c = doCreateItem(2000, getThingPos(cid)) fill(c, 2)

Then put it in parcel and send to depot.