Reset TFS database without removing accounts, players and guilds

SQL queries to reset database to ‘server start’ without removing accounts, players and guilds. All players are set to level 30 with valid hp, mana and cap for their vocations.

UPDATE `players`
SET
`health` = 185 + 22 * 5,
`healthmax` = 185 + 22 * 5,
`mana` =  35 + 22 * 30,
`manamax` = 35 + 22 * 30,
`cap` = 470 + 22 * 10,
`level` = 30,
`experience` = 368300,
`maglevel` = 30,
`soul` = 100,
`manaspent` = 0,
`lookaddons` = 0,
`posx` = 0,
`posy` = 0,
`posz` = 0, 
`conditions` = '',
`skull` = 0,
`skulltime` = 0,
`blessings` = 0,
`offlinetraining_time` = 43200,
`offlinetraining_skill` = -1,
`stamina` = 2520,
`skill_fist` = 10,
`skill_club` = 10,
`skill_sword` = 10,
`skill_axe` = 10,
`skill_dist` = 10,
`skill_shielding` = 10,
`skill_fishing` = 10,
`skill_fist_tries` = 0,
`skill_club_tries` = 0,
`skill_sword_tries` = 0,
`skill_axe_tries` = 0,
`skill_dist_tries` = 0,
`skill_shielding_tries` = 0,
`skill_fishing_tries` = 0
WHERE `vocation` =  1 OR `vocation`= 5  OR `vocation`= 2 OR `vocation`= 6;

UPDATE `players`
SET
`health` = 185 + 22 * 10,
`healthmax` = 185 + 22 * 10,
`mana` =  35 + 22 * 15,
`manamax` = 35 + 22 * 15,
`cap` = 470 + 22 * 20,
`level` = 30,
`experience` = 368300,
`maglevel` = 5,
`soul` = 100,
`manaspent` = 0,
`lookaddons` = 0,
`posx` = 0,
`posy` = 0,
`posz` = 0, 
`conditions` = '',
`skull` = 0,
`skulltime` = 0,
`blessings` = 0,
`offlinetraining_time` = 43200,
`offlinetraining_skill` = -1,
`stamina` = 2520,
`skill_fist` = 10,
`skill_club` = 10,
`skill_sword` = 10,
`skill_axe` = 10,
`skill_dist` = 30,
`skill_shielding` = 10,
`skill_fishing` = 10,
`skill_fist_tries` = 0,
`skill_club_tries` = 0,
`skill_sword_tries` = 0,
`skill_axe_tries` = 0,
`skill_dist_tries` = 0,
`skill_shielding_tries` = 0,
`skill_fishing_tries` = 0
WHERE `vocation` =  3 OR `vocation`= 7;

UPDATE `players`
SET
`health` = 185 + 22 * 15,
`healthmax` = 185 + 22 * 15,
`mana` =  35 + 22 * 5,
`manamax` = 35 + 22 * 5,
`cap` = 470 + 22 * 25,
`level` = 30,
`experience` = 368300,
`maglevel` = 2,
`soul` = 100,
`manaspent` = 0,
`lookaddons` = 0,
`posx` = 0,
`posy` = 0,
`posz` = 0, 
`conditions` = '',
`skull` = 0,
`skulltime` = 0,
`blessings` = 0,
`offlinetraining_time` = 43200,
`offlinetraining_skill` = -1,
`stamina` = 2520,
`skill_fist` = 10,
`skill_club` = 30,
`skill_sword` = 30,
`skill_axe` = 30,
`skill_dist` = 10,
`skill_shielding` = 10,
`skill_fishing` = 10,
`skill_fist_tries` = 0,
`skill_club_tries` = 0,
`skill_sword_tries` = 0,
`skill_axe_tries` = 0,
`skill_dist_tries` = 0,
`skill_shielding_tries` = 0,
`skill_fishing_tries` = 0
WHERE `vocation` =  4 OR `vocation`= 8;

DELETE FROM `account_ban_history`;
DELETE FROM `account_bans`;
DELETE FROM `house_lists`;
DELETE FROM `houses`;
DELETE FROM `market_history`;
DELETE FROM `market_offers`;
DELETE FROM `player_deaths`;
DELETE FROM `player_depotitems`;
DELETE FROM `player_inboxitems`;
DELETE FROM `player_items`;
DELETE FROM `player_spells`;
DELETE FROM `player_storage`;
DELETE FROM `players_online`;
DELETE FROM `tile_store`;

OTC Module – Debug UI

When you create new module, you always got problems with OTUI design. Why X element is on that position? What am I clicking?

I can’t solve all these problems, but my module can help you debug your UI without milion prints to console.

With this module you can easily view tree of elements on given position (under mouse cursor):

It also works before login:

modules/client_ui_debug/client_ui_debug.otmod

Module
  name: client_ui_debug
  description: Draws tree of widgets on mouse position
  author: Gesior.pl
  reloadable: false
  sandboxed: true
  scripts: [ client_ui_debug ]
  @onLoad: init()
  @onUnload: terminate()

modules/client_ui_debug/client_ui_debug.otui

HighlightWidget < Panel
  id: highlightWidget
  background-color: #AA000099
  focusable: false
  phantom: true

Panel
  id: clientUiDebug
  anchors.top: parent.top
  anchors.left: parent.left
  anchors.right: parent.right
  height: 20
  margin-top: 1
  focusable: false

  UILabel
    id: clientUiDebugLabel
    color: #FF7777
    background-color: #00000099
    anchors.fill: parent
    text-align: left
    text-auto-resize: false
    padding: 2
    font: verdana-11px-antialised

modules/client_ui_debug/client_ui_debug.lua

local clientUiDebug
local clientUiDebugLabel
local clientUiDebugHighlightWidget

function onClientUiDebuggerMouseMove(mouseBindWidget, mousePos, mouseMove)
    local widgets = rootWidget:recursiveGetChildrenByPos(mousePos)

    local smallestWidget
    for _, widget in pairs(widgets) do
        if (widget:getId() ~= 'highlightWidget' and widget:getId() ~= 'toolTip') then
            if (not smallestWidget or
                    (widget:getSize().width <= smallestWidget:getSize().width and widget:getSize().height <= smallestWidget:getSize().height)
            ) then
                smallestWidget = widget
            end
        end
    end
    if smallestWidget then
        clientUiDebugHighlightWidget:setPosition(smallestWidget:getPosition())
        clientUiDebugHighlightWidget:setSize(smallestWidget:getSize())
        clientUiDebugHighlightWidget:raise()
    end

    local widgetNames = {}
    for wi = #widgets, 1, -1 do
        local widget = widgets[wi]
        if (widget:getId() ~= 'highlightWidget') then
            table.insert(widgetNames, widget:getClassName() .. '#' .. widget:getId())
        end
    end
    clientUiDebugLabel:setText(table.concat(widgetNames, " -> "))
end

function init()
    connect(rootWidget, {
        onMouseMove = onClientUiDebuggerMouseMove,
    })
    clientUiDebug = g_ui.displayUI('client_ui_debug')
    clientUiDebugLabel = clientUiDebug:getChildById('clientUiDebugLabel')
    clientUiDebugHighlightWidget = g_ui.createWidget('HighlightWidget', rootWidget)
end

function terminate()
    disconnect(rootWidget, {
        onMouseMove = onClientUiDebuggerMouseMove,
    })
    clientUiDebug:destroy()
    clientUiDebugHighlightWidget:destroy()
end

at end of modules list in modules/client/client.otmod add:

- client_ui_debug

items.xml sorter – by id/fromid

Simple PHP code to sort file items.xml by id/fromid attribute

<?php
$path_to_file = 'data/items/items.xml';
$xml = simplexml_load_string(file_get_contents($path_to_file));
$trees = $xml->xpath('/items/item');

function sort_trees($t1, $t2)
{
    $a = isset($t1['id']) ? $t1['id'] : $t1['fromid'];
    $b = isset($t2['id']) ? $t2['id'] : $t2['fromid'];
    return $a - $b;
}

usort($trees, 'sort_trees');

foreach ($trees as $tree) {
    echo '<item ';
    foreach ($tree->attributes() as $k => $v) {
        echo $k . '="' . $v . '" ';
    }
    if (count($tree->attribute) > 0) {
        echo '>' . PHP_EOL;
        foreach ($tree->attribute as $k => $v) {
            echo '<attribute key="' . $v->attributes()['key'] . '" value="' . htmlspecialchars($v->attributes()['value'], ENT_XML1 | ENT_QUOTES, 'UTF-8') . '" />' . PHP_EOL;
        }
        echo '</item>' . PHP_EOL;
    } else {
        echo '/>' . PHP_EOL;
    }
}

[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