• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!
  • 2026 staff recruitment is open! Check it out and consider applying!

Active/upcoming Otclient.

Elac

New Member
Joined
Apr 10, 2013
Messages
10
Reaction score
2
Location
Sweden
Hello! Hope everyone is doing fine!
Kind of new to this tibia open building, perfect when the kids is going to bed in the evening! so bear with me..

I am using Canary with OTclient Redemption and server working all fine but my problem is the client don't show Active/upcoming scheduler. I got for example:

LUA:
        "name": "Double Exp",
            "startdate": "01/26/2026",
            "enddate": "01/27/2026",
            "ingame": {
                "exprate": 200
            },
            "description": "Double experience when hunting monsters.",
            "colors": {
                "colordark": "#002d00",
                "colorlight": "#004400"
            },
            "details": {
                "displaypriority": 6,
                "isseasonal": 1,
                "specialevent": 1
            }
        }

The server is giving me: Active EventScheduler: Double Exp but the client is only showing boosted Creature/boss.

Inside data/XML/events.xml I got this:

Code:
<?xml version="1.0" encoding="UTF-8"?>
<events>
    <event name="Otservbr example 1" startdate="11/03/2020" enddate="12/30/2023" script="example.lua" >
        <ingame exprate="100" lootrate="100" bosslootrate="100" spawnrate="100" skillrate="100" />
        <description description="Otserver br example 1 description double exp and a half, double loot !chance!, regular spawn and double skill" />
        <colors colordark="#235c00" colorlight="#2d7400" />
        <details displaypriority="6" isseasonal="0" specialevent="0" />
    </event>
    <event name="Otservbr example 2" startdate="2/2/2022" enddate="12/31/2023" script="" >
        <ingame exprate="100" lootrate="100" bosslootrate="100" spawnrate="100" skillrate="100" />
        <description description="Otserver br example 2 description 50% less exp, triple loot !chance!, 50% faster spawn and regular skill" />
        <colors colordark="#735D10" colorlight="#8B6D05" />
        <details displaypriority="6" isseasonal="0" specialevent="0" />
    </event>
</events>




Anyone know how to fix this and why boosted creature/boss is only showing?

Thanks alot! (Beginner)

Login.php:

LUA:
<?php

use MyAAC\Models\BoostedCreature;
use MyAAC\Models\PlayerOnline;
use MyAAC\Models\Account;
use MyAAC\Models\Player;
use MyAAC\RateLimit;

require_once 'common.php';
require_once SYSTEM . 'functions.php';
require_once SYSTEM . 'init.php';
require_once SYSTEM . 'status.php';

# error function
function sendError($message, $code = 3){
    $ret = [];
    $ret['errorCode'] = $code;
    $ret['errorMessage'] = $message;
    die(json_encode($ret));
}

# event schedule function
function parseEvent($table1, $date, $table2)
{
    if ($table1) {
        if ($date) {
            if ($table2) {
                $date = $table1->getAttribute('startdate');
                return date_create("{$date}")->format('U');
            } else {
                $date = $table1->getAttribute('enddate');
                return date_create("{$date}")->format('U');
            }
        } else {
            foreach($table1 as $attr) {
                if ($attr) {
                    return $attr->getAttribute($table2);
                }
            }
        }
    }
    return 'error';
}

$request = json_decode(file_get_contents('php://input'));
$action = $request->type ?? '';

/** @var OTS_Base_DB $db */
/** @var array $config */

switch ($action) {
    case 'cacheinfo':
        $playersonline = PlayerOnline::count();
        die(json_encode([
            'playersonline' => $playersonline,
            'twitchstreams' => 0,
            'twitchviewer' => 0,
            'gamingyoutubestreams' => 0,
            'gamingyoutubeviewer' => 0
        ]));

    case 'eventschedule':
        $eventlist = [];
        $file_path = config('server_path') . 'data/XML/events.xml';
        if (!file_exists($file_path)) {
            die(json_encode([]));
        }
        $xml = new DOMDocument;
        $xml->load($file_path);
        $tmplist = [];
        $tableevent = $xml->getElementsByTagName('event');

        foreach ($tableevent as $event) {
            if ($event) { $tmplist = [
            'colorlight' => parseEvent($event->getElementsByTagName('colors'), false, 'colorlight'),
            'colordark' => parseEvent($event->getElementsByTagName('colors'), false, 'colordark'),
            'description' => parseEvent($event->getElementsByTagName('description'), false, 'description'),
            'displaypriority' => intval(parseEvent($event->getElementsByTagName('details'), false, 'displaypriority')),
            'enddate' => intval(parseEvent($event, true, false)),
            'isseasonal' => getBoolean(intval(parseEvent($event->getElementsByTagName('details'), false, 'isseasonal'))),
            'name' => $event->getAttribute('name'),
            'startdate' => intval(parseEvent($event, true, true)),
            'specialevent' => intval(parseEvent($event->getElementsByTagName('details'), false, 'specialevent'))
                ];
            $eventlist[] = $tmplist; } }
        die(json_encode(['eventlist' => $eventlist, 'lastupdatetimestamp' => time()]));

    case 'boostedcreature':
        $clientVersion = (int)setting('core.client');

        // 13.40 and up
        if ($clientVersion >= 1340) {
            $creatureBoost = $db->query("SELECT * FROM " . $db->tableName('boosted_creature'))->fetchAll();
            $bossBoost     = $db->query("SELECT * FROM " . $db->tableName('boosted_boss'))->fetchAll();
            die(json_encode([
                'boostedcreature' => true,
                'creatureraceid'  => intval($creatureBoost[0]['raceid']),
                'bossraceid'      => intval($bossBoost[0]['raceid'])
            ]));
        }

        // lower clients
        $boostedCreature = BoostedCreature::first();
        die(json_encode([
            'boostedcreature' => true,
            'raceid' => $boostedCreature->raceid
        ]));

    case 'login':

        $port = $config['lua']['gameProtocolPort'];

        // default world info
        $world = [
            'id' => 0,
            'name' => $config['lua']['serverName'],
            'externaladdress' => $config['lua']['ip'],
            'externalport' => $port,
            'externaladdressprotected' => $config['lua']['ip'],
            'externalportprotected' => $port,
            'externaladdressunprotected' => $config['lua']['ip'],
            'externalportunprotected' => $port,
            'previewstate' => 0,
            'location' => 'BRA', // BRA, EUR, USA
            'anticheatprotection' => false,
            'pvptype' => array_search($config['lua']['worldType'], ['pvp', 'no-pvp', 'pvp-enforced']),
            'istournamentworld' => false,
            'restrictedstore' => false,
            'currenttournamentphase' => 2
        ];

        $characters = [];

        $inputEmail = $request->email ?? false;
        $inputAccountName = $request->accountname ?? false;
        $inputToken = $request->token ?? false;

        $account = Account::query();
        if ($inputEmail != false) { // login by email
            $account->where('email', $inputEmail);
        }
        else if($inputAccountName != false) { // login by account name
            $account->where('name', $inputAccountName);
        }

        $account = $account->first();

        $ip = get_browser_real_ip();
        $limiter = new RateLimit('failed_logins', setting('core.account_login_attempts_limit'), setting('core.account_login_ban_time'));
        $limiter->enabled = setting('core.account_login_ipban_protection');
        $limiter->load();

        $ban_msg = 'A wrong account, password or secret has been entered ' . setting('core.account_login_attempts_limit') . ' times in a row. You are unable to log into your account for the next ' . setting('core.account_login_ban_time') . ' minutes. Please wait.';
        if (!$account) {
            $limiter->increment($ip);
            if ($limiter->exceeded($ip)) {
                sendError($ban_msg);
            }

            sendError(($inputEmail != false ? 'Email' : 'Account name') . ' or password is not correct.');
        }

        $current_password = encrypt((USE_ACCOUNT_SALT ? $account->salt : '') . $request->password);
        if (!$account || $account->password != $current_password) {
            $limiter->increment($ip);
            if ($limiter->exceeded($ip)) {
                sendError($ban_msg);
            }

            sendError(($inputEmail != false ? 'Email' : 'Account name') . ' or password is not correct.');
        }

        $accountHasSecret = false;
        if (fieldExist('secret', 'accounts')) {
            $accountSecret = $account->secret;
            if ($accountSecret != null && $accountSecret != '') {
                $accountHasSecret = true;
                if ($inputToken === false) {
                    $limiter->increment($ip);
                    if ($limiter->exceeded($ip)) {
                        sendError($ban_msg);
                    }
                    sendError('Submit a valid two-factor authentication token.', 6);
                } else {
                    require_once LIBS . 'rfc6238.php';
                    if (TokenAuth6238::verify($accountSecret, $inputToken) !== true) {
                        $limiter->increment($ip);
                        if ($limiter->exceeded($ip)) {
                            sendError($ban_msg);
                        }

                        sendError('Two-factor authentication failed, token is wrong.', 6);
                    }
                }
            }
        }

        $limiter->reset($ip);
        if (setting('core.account_mail_verify') && $account->email_verified !== 1) {
            sendError('You need to verify your account, enter in our site and resend verify e-mail!');
        }

        // common columns
        $columns = 'id, name, level, sex, vocation, looktype, lookhead, lookbody, looklegs, lookfeet, lookaddons';

        if (fieldExist('isreward', 'accounts')) {
            $columns .= ', isreward';
        }

        if (fieldExist('istutorial', 'accounts')) {
            $columns .= ', istutorial';
        }

        $players = Player::where('account_id', $account->id)->notDeleted()->selectRaw($columns)->get();
        if($players && $players->count()) {
            $highestLevelId = $players->sortByDesc('experience')->first()->getKey();

            foreach ($players as $player) {
                $characters[] = create_char($player, $highestLevelId);
            }
        }

        /*
         * not needed anymore?
        if (fieldExist('premdays', 'accounts') && fieldExist('lastday', 'accounts')) {
            $save = false;
            $timeNow = time();
            $premDays = $account->premdays;
            $lastDay = $account->lastday;
            $lastLogin = $lastDay;

            if ($premDays != 0 && $premDays != PHP_INT_MAX) {
                if ($lastDay == 0) {
                    $lastDay = $timeNow;
                    $save = true;
                } else {
                    $days = (int)(($timeNow - $lastDay) / 86400);
                    if ($days > 0) {
                        if ($days >= $premDays) {
                            $premDays = 0;
                            $lastDay = 0;
                        } else {
                            $premDays -= $days;
                            $reminder = ($timeNow - $lastDay) % 86400;
                            $lastDay = $timeNow - $reminder;
                        }

                        $save = true;
                    }
                }
            } else if ($lastDay != 0) {
                $lastDay = 0;
                $save = true;
            }
            if ($save) {
                $account->premdays = $premDays;
                $account->lastday = $lastDay;
                $account->save();
            }
        }
        */

        $worlds = [$world];
        $playdata = compact('worlds', 'characters');

        $sessionKey = ($inputEmail !== false) ? $inputEmail : $inputAccountName; // email or account name
        $sessionKey .= "\n" . $request->password; // password
        if (!fieldExist('istutorial', 'players')) {
            $sessionKey .= "\n";
        }
        $sessionKey .= ($accountHasSecret && strlen($accountSecret) > 5) ? $inputToken : '';

        // this is workaround to distinguish between TFS 1.x and otservbr
        // TFS 1.x requires the number in session key
        // otservbr requires just login and password
        // so we check for istutorial field which is present in otservbr, and not in TFS
        if (!fieldExist('istutorial', 'players')) {
            $sessionKey .= "\n".floor(time() / 30);
        }

        $session = [
            'sessionkey' => $sessionKey,
            'lastlogintime' => 0,
            'ispremium' => $account->is_premium,
            'premiumuntil' => ($account->premium_days) > 0 ? (time() + ($account->premium_days * 86400)) : 0,
            'status' => 'active', // active, frozen or suspended
            'returnernotification' => false,
            'showrewardnews' => true,
            'isreturner' => true,
            'fpstracking' => false,
            'optiontracking' => false,
            'tournamentticketpurchasestate' => 0,
            'emailcoderequest' => false
        ];
        die(json_encode(compact('session', 'playdata')));

    default:
        sendError("Unrecognized event {$action}.");
    break;
}

function create_char($player, $highestLevelId) {
    return [
        'worldid' => 0,
        'name' => $player->name,
        'ismale' => $player->sex === 1,
        'tutorial' => isset($player->istutorial) && $player->istutorial,
        'level' => $player->level,
        'vocation' => $player->vocation_name,
        'outfitid' => $player->looktype,
        'headcolor' => $player->lookhead,
        'torsocolor' => $player->lookbody,
        'legscolor' => $player->looklegs,
        'detailcolor' => $player->lookfeet,
        'addonsflags' => $player->lookaddons,
        'ishidden' => $player->is_deleted,
        'istournamentparticipant' => false,
        'ismaincharacter' => $highestLevelId === $player->getKey(),
        'dailyrewardstate' => $player->isreward ?? 0,
        'remainingdailytournamentplaytime' => 0
    ];
}

 
May be a stupid question, but did you try to change in events.xml

startdate="2/2/2022" enddate="12/31/2023"

To current year/month?

The login already sends that information, so it should work.
 
May be a stupid question, but did you try to change in events.xml

startdate="2/2/2022" enddate="12/31/2023"

To current year/month?

The login already sends that information, so it should work.
Kind of weird this:
startdate="27/1/2026" enddate="12/30/2027"
Console: [2026-27-01 18:50:54.789] [warning] EventsScheduler::loadScheduleEventFromXml - Event 'Otservbr example 1' end time is before start time
 
Event 'Otservbr example 1' end time is before start time
Most of date manipulation libraries interpret 27th month of 2026 as 3rd month of 2028, so end time is after start time:
 
startdate="27/1/2026" enddate="12/30/2027"
This in data/XML/events.xml should be replaced with (month/day/year format):
XML:
startdate="1/27/2026" enddate="12/30/2027"
MyAAC login.php will automatically generate JSON from the Canary .xml configuration file.
It may require few minutes to update in client (and of course client restart), as Tibia 12+ caches a lot of data to do not overload server with requests.
 
Back
Top