diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c3930f3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +OTClient is made available under the MIT License + +Copyright (c) 2010-2017 OTClient + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index b82dd7b..bda1734 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,42 @@ -# otclientv8 -Free version of an alternative tibia client for otserv +# OTClientV8 + +Preview version of OTClientV8, it's v0.95 beta, version v1.0 with all tutorial will be released soon +It's based on https://github.com/edubart/otclient. It's not backward compatible + +## Quick Start + +Open `init.lua` and edit: + +``` +-- CONFIG +APP_NAME = "otclientv8" -- important, change it, it's name for config dir and files in appdata +APP_VERSION = 1337 -- client version for updater and login to indentify outdated client + +-- If you don't use updater or other service, set it to updater = "" +Services = { + website = "http://otclient.ovh", -- currently not used + updater = "http://otclient.ovh/api/updater.php", + news = "http://otclient.ovh/api/news.php", + stats = "", + crash = "http://otclient.ovh/api/crash.php", + feedback = "http://otclient.ovh/api/feedback.php" +} + +-- Servers accept http login url or ip:port:version +Servers = { + OTClientV8 = "http://otclient.ovh/api/login.php", + OTClientV8proxy = "http://otclient.ovh/api/login.php?proxy=1", + OTClientV8c = "otclient.ovh:7171:1099" +} +ALLOW_CUSTOM_SERVERS = true -- if true it will show option ANOTHER on server list +-- CONFIG END +``` + +Also remember to add your sprite and data file to data/things + +That's it, you're ready to use OTClientV8. +Soon I will add tutorial how to activate extra features (there are a lot of them) + +DirectX version requires 3 dlls: libEGL.dll libGLESv2.dll d3dcompiler_46.dll + +If it can't start (missing dlls) then user need to install visual studio 2019 redistributable x86: https://aka.ms/vs/16/release/vc_redist.x86.exe diff --git a/api/crash.php b/api/crash.php new file mode 100644 index 0000000..fc84d0b --- /dev/null +++ b/api/crash.php @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/api/feedback.php b/api/feedback.php new file mode 100644 index 0000000..a63bdc4 --- /dev/null +++ b/api/feedback.php @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/api/image.png b/api/image.png new file mode 100644 index 0000000..fa2242e Binary files /dev/null and b/api/image.png differ diff --git a/api/login.php b/api/login.php new file mode 100644 index 0000000..a229a0c --- /dev/null +++ b/api/login.php @@ -0,0 +1,182 @@ + (filename, md5 checksum in hex) +// $things = null; things can be null if you want to use default values, but then enable auto download of sprites and dat in updater + +/* //for 860 +$things = array( + "sprites" => array("$version/Tibia.spr", "3db8c0098d34ca3d9a8ec29d40ef1b7b"), + "data" => array("$version/Tibia.dat", "85785b5d67b4c111f780a74895c85c75") +); +*/ + +// for 1099 +$things = array( + "sprites" => array("$version/Tibia.spr", "63d38646597649a55a8be463d6c0fb49"), + "data" => array("$version/Tibia.dat", "ae7157cfff42f14583d6363e77044df7") +); + +$customProtocol = nil; // if not nil it will replace client version in protocolgame, may be used to detect outdated client + +// executes modules.client_options.setOption(option, value, true) +$settings = array( + +); + +// it's from src/client/const.h, executes g_game.enableFeature/g_game.disableFeature +$features = array( + 22 => true, // GameFormatCreatureName + 25 => true, // GameExtendedClientPing + 30 => true, // GameChangeMapAwareRange +// 56 => true, // GameSpritesAlphaChannel + 80 => true, + 90 => true, // GameNewWalking +// 91 => true, // GameSmootherWalking + 95 => true, // GameBot + 97 => true, // light +); + +$rsa = "1091201329673994292788609605089955415282375029027981291234687579" . + "3726629149257644633073969600111060390723088861007265581882535850" . + "3429057592827629436413108566029093628212635953836686562675849720" . + "6207862794310902180176810615217550567108238764764442605581471797" . + "07119674283982419152118103759076030616683978566631413"; + +// proxies (it's custom feature, not available for free) +$proxies = array( + array( + "localPort" => 7172, + "host" => "51.158.184.57", + "port" => 7162, + "priority" => 0 + ), + array( + "localPort" => 7172, + "host" => "54.39.190.20", + "port" => 7162, + "priority" => 0 + ), + array( + "localPort" => 7172, + "host" => "51.83.226.109", + "port" => 7162, + "priority" => 0, + ), + array( + "localPort" => 7172, + "host" => "35.247.201.100", + "port" => 443, + "priority" => 0 + ) +); + +// config end + +$data = file_get_contents("php://input"); + +$data = json_decode($data); +if(empty($data)) { + http_response_code(400); +} + +if($data->quick == 1) { + // under development + http_response_code(404); + die(); +} + +$conn = new mysqli($dbserver, $username, $password, $dbname); +if ($conn->connect_error) { + die("SQL connection failed: " . $conn->connect_error); +} + +$account = $data->account; +if($encryption == "sha1") + $password = sha1($data->password); +else if($encryption == "md5") + $password = md5($data->password); +else + $password = $data->password; + +$token = $data->token; + +$account = preg_replace("/[^A-Za-z0-9 ._-]/", '', $account); +$password = preg_replace("/[^A-Za-z0-9 ._-]/", '', $password); +$token = preg_replace("/[^A-Za-z0-9 ._-]/", '', $token); +$ip = preg_replace("/[^A-Za-z0-9 ._-]/", '', $_SERVER['REMOTE_ADDR']); + +if($maxLogins != null && $maxLogins > 0) { + $result = $conn->query("select count(*) as `attempts` from `login_attmpts` where `ip` = '".$ip."' and `date` > NOW() - INTERVAL ".$blockTime." SECOND"); + $result = $result->fetch_assoc(); + if($result['attempts'] > $maxLogins) { + die(json_encode(array("error" => "Too many login attempts, please wait ".$blockTime." seconds."))); + } + $conn->query("INSERT INTO `login_attmpts` (`acc`, `ip`, `date`) VALUES ('".$conn->real_escape_string($account)."', '".$ip."', NOW())"); +} + +$result = $conn->query("select * from accounts where `name` = '".$conn->real_escape_string($account)."' and `password` = '".$conn->real_escape_string($password)."'"); +if ($result->num_rows != 1) { + die(json_encode(array("error" => "Invalid account/password"))); +} +$acc = $result->fetch_assoc(); + +$session = "".$data->account."\n".$data->password."\n$token\n".time(); + +if($serverIp != "proxy" && $serverIp != "0.0.0.0") { + $proxies = null; +} + +$response = array( + "error" => "", + "rsa" => $rsa, + "version" => $version, + "things" => $things, + "customProtocol" => $customProtocol, + "session" => $session, + "characters" => array(), + "account" => array(), + "settings" => $settings, + "features" => $features, + "proxies" => $proxies +); + +$response["account"]["status"] = 0; // 0=ok, 1=frozen, 2=supsended +$response["account"]["subStatus"] = 1; // 0=free, 1=premium +$response["account"]["premDays"] = 65535; + +$characters = $conn->query("select * from `players` where `account_id` = '".$acc['id']."'"); +if ($characters->num_rows == 0) { + die(json_encode(array("error" => "Account doesn't have any characters"))); +} + +while($character = $characters->fetch_assoc()) { + $response["characters"][] = array( + "name" => $character['name'], + "worldName" => $serverName, + "worldIp" => $serverIp, + "worldPort" => $serverPort + // if you are good enough and can code it in lua, you can add outfit, level, vocation, whatever you want here + ); +} + +echo json_encode($response); +?> \ No newline at end of file diff --git a/api/newlogin.php b/api/newlogin.php new file mode 100644 index 0000000..55bcea3 --- /dev/null +++ b/api/newlogin.php @@ -0,0 +1,28 @@ + $qrcode, + "code" => $code, + "status" => "waiting" +); + +echo json_encode($data); +?> \ No newline at end of file diff --git a/api/news.php b/api/news.php new file mode 100644 index 0000000..f11c943 --- /dev/null +++ b/api/news.php @@ -0,0 +1,35 @@ + "TEST SERVERS", "text" => "OTCLIENTV8 Accs:\nacc1/acc\nacc2/acc\nacc3/acc"); + +$news[] = array("title" => "First title", + "text" => "This is example of lua g_http api. Those news are from http://otclient.ovh/news.php + \nRequest was for language '".$lang."', however, there's only english version of this, don't have time to create more versions"); +$news[] = array("title" => "Random joke", "text" => $jokes[array_rand($jokes)]); +$news[] = array("title" => "Image test", "image" => base64_encode(file_get_contents("image.png"))); + +echo json_encode($news); + +?> \ No newline at end of file diff --git a/api/phpqrcode.php b/api/phpqrcode.php new file mode 100644 index 0000000..80adb9d --- /dev/null +++ b/api/phpqrcode.php @@ -0,0 +1,3312 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + + +/* + * Version: 1.1.4 + * Build: 2010100721 + */ + + + +//---- qrconst.php ----------------------------- + + + + + +/* + * PHP QR Code encoder + * + * Common constants + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + // Encoding modes + + define('QR_MODE_NUL', -1); + define('QR_MODE_NUM', 0); + define('QR_MODE_AN', 1); + define('QR_MODE_8', 2); + define('QR_MODE_KANJI', 3); + define('QR_MODE_STRUCTURE', 4); + + // Levels of error correction. + + define('QR_ECLEVEL_L', 0); + define('QR_ECLEVEL_M', 1); + define('QR_ECLEVEL_Q', 2); + define('QR_ECLEVEL_H', 3); + + // Supported output formats + + define('QR_FORMAT_TEXT', 0); + define('QR_FORMAT_PNG', 1); + + class qrstr { + public static function set(&$srctab, $x, $y, $repl, $replLen = false) { + $srctab[$y] = substr_replace($srctab[$y], ($replLen !== false)?substr($repl,0,$replLen):$repl, $x, ($replLen !== false)?$replLen:strlen($repl)); + } + } + + + +//---- merged_config.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Config file, tuned-up for merged verion + */ + + define('QR_CACHEABLE', false); // use cache - more disk reads but less CPU power, masks and format templates are stored there + define('QR_CACHE_DIR', false); // used when QR_CACHEABLE === true + define('QR_LOG_DIR', false); // default error logs dir + + define('QR_FIND_BEST_MASK', true); // if true, estimates best mask (spec. default, but extremally slow; set to false to significant performance boost but (propably) worst quality code + define('QR_FIND_FROM_RANDOM', 2); // if false, checks all masks available, otherwise value tells count of masks need to be checked, mask id are got randomly + define('QR_DEFAULT_MASK', 2); // when QR_FIND_BEST_MASK === false + + define('QR_PNG_MAXIMUM_SIZE', 1024); // maximum allowed png image width (in pixels), tune to make sure GD and PHP can handle such big images + + + + +//---- qrtools.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Toolset, handy and debug utilites. + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRtools { + + //---------------------------------------------------------------------- + public static function binarize($frame) + { + $len = count($frame); + foreach ($frame as &$frameLine) { + + for($i=0; $i<$len; $i++) { + $frameLine[$i] = (ord($frameLine[$i])&1)?'1':'0'; + } + } + + return $frame; + } + + //---------------------------------------------------------------------- + public static function tcpdfBarcodeArray($code, $mode = 'QR,L', $tcPdfVersion = '4.5.037') + { + $barcode_array = array(); + + if (!is_array($mode)) + $mode = explode(',', $mode); + + $eccLevel = 'L'; + + if (count($mode) > 1) { + $eccLevel = $mode[1]; + } + + $qrTab = QRcode::text($code, false, $eccLevel); + $size = count($qrTab); + + $barcode_array['num_rows'] = $size; + $barcode_array['num_cols'] = $size; + $barcode_array['bcode'] = array(); + + foreach ($qrTab as $line) { + $arrAdd = array(); + foreach(str_split($line) as $char) + $arrAdd[] = ($char=='1')?1:0; + $barcode_array['bcode'][] = $arrAdd; + } + + return $barcode_array; + } + + //---------------------------------------------------------------------- + public static function clearCache() + { + self::$frames = array(); + } + + //---------------------------------------------------------------------- + public static function buildCache() + { + QRtools::markTime('before_build_cache'); + + $mask = new QRmask(); + for ($a=1; $a <= QRSPEC_VERSION_MAX; $a++) { + $frame = QRspec::newFrame($a); + if (QR_IMAGE) { + $fileName = QR_CACHE_DIR.'frame_'.$a.'.png'; + QRimage::png(self::binarize($frame), $fileName, 1, 0); + } + + $width = count($frame); + $bitMask = array_fill(0, $width, array_fill(0, $width, 0)); + for ($maskNo=0; $maskNo<8; $maskNo++) + $mask->makeMaskNo($maskNo, $width, $frame, $bitMask, true); + } + + QRtools::markTime('after_build_cache'); + } + + //---------------------------------------------------------------------- + public static function log($outfile, $err) + { + if (QR_LOG_DIR !== false) { + if ($err != '') { + if ($outfile !== false) { + file_put_contents(QR_LOG_DIR.basename($outfile).'-errors.txt', date('Y-m-d H:i:s').': '.$err, FILE_APPEND); + } else { + file_put_contents(QR_LOG_DIR.'errors.txt', date('Y-m-d H:i:s').': '.$err, FILE_APPEND); + } + } + } + } + + //---------------------------------------------------------------------- + public static function dumpMask($frame) + { + $width = count($frame); + for($y=0;$y<$width;$y++) { + for($x=0;$x<$width;$x++) { + echo ord($frame[$y][$x]).','; + } + } + } + + //---------------------------------------------------------------------- + public static function markTime($markerId) + { + list($usec, $sec) = explode(" ", microtime()); + $time = ((float)$usec + (float)$sec); + + if (!isset($GLOBALS['qr_time_bench'])) + $GLOBALS['qr_time_bench'] = array(); + + $GLOBALS['qr_time_bench'][$markerId] = $time; + } + + //---------------------------------------------------------------------- + public static function timeBenchmark() + { + self::markTime('finish'); + + $lastTime = 0; + $startTime = 0; + $p = 0; + + echo ' + + '; + + foreach($GLOBALS['qr_time_bench'] as $markerId=>$thisTime) { + if ($p > 0) { + echo ''; + } else { + $startTime = $thisTime; + } + + $p++; + $lastTime = $thisTime; + } + + echo ' + + +
BENCHMARK
till '.$markerId.': '.number_format($thisTime-$lastTime, 6).'s
TOTAL: '.number_format($lastTime-$startTime, 6).'s
'; + } + + } + + //########################################################################## + + QRtools::markTime('start'); + + + + +//---- qrspec.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * QR Code specifications + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * The following data / specifications are taken from + * "Two dimensional symbol -- QR-code -- Basic Specification" (JIS X0510:2004) + * or + * "Automatic identification and data capture techniques -- + * QR Code 2005 bar code symbology specification" (ISO/IEC 18004:2006) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('QRSPEC_VERSION_MAX', 40); + define('QRSPEC_WIDTH_MAX', 177); + + define('QRCAP_WIDTH', 0); + define('QRCAP_WORDS', 1); + define('QRCAP_REMINDER', 2); + define('QRCAP_EC', 3); + + class QRspec { + + public static $capacity = array( + array( 0, 0, 0, array( 0, 0, 0, 0)), + array( 21, 26, 0, array( 7, 10, 13, 17)), // 1 + array( 25, 44, 7, array( 10, 16, 22, 28)), + array( 29, 70, 7, array( 15, 26, 36, 44)), + array( 33, 100, 7, array( 20, 36, 52, 64)), + array( 37, 134, 7, array( 26, 48, 72, 88)), // 5 + array( 41, 172, 7, array( 36, 64, 96, 112)), + array( 45, 196, 0, array( 40, 72, 108, 130)), + array( 49, 242, 0, array( 48, 88, 132, 156)), + array( 53, 292, 0, array( 60, 110, 160, 192)), + array( 57, 346, 0, array( 72, 130, 192, 224)), //10 + array( 61, 404, 0, array( 80, 150, 224, 264)), + array( 65, 466, 0, array( 96, 176, 260, 308)), + array( 69, 532, 0, array( 104, 198, 288, 352)), + array( 73, 581, 3, array( 120, 216, 320, 384)), + array( 77, 655, 3, array( 132, 240, 360, 432)), //15 + array( 81, 733, 3, array( 144, 280, 408, 480)), + array( 85, 815, 3, array( 168, 308, 448, 532)), + array( 89, 901, 3, array( 180, 338, 504, 588)), + array( 93, 991, 3, array( 196, 364, 546, 650)), + array( 97, 1085, 3, array( 224, 416, 600, 700)), //20 + array(101, 1156, 4, array( 224, 442, 644, 750)), + array(105, 1258, 4, array( 252, 476, 690, 816)), + array(109, 1364, 4, array( 270, 504, 750, 900)), + array(113, 1474, 4, array( 300, 560, 810, 960)), + array(117, 1588, 4, array( 312, 588, 870, 1050)), //25 + array(121, 1706, 4, array( 336, 644, 952, 1110)), + array(125, 1828, 4, array( 360, 700, 1020, 1200)), + array(129, 1921, 3, array( 390, 728, 1050, 1260)), + array(133, 2051, 3, array( 420, 784, 1140, 1350)), + array(137, 2185, 3, array( 450, 812, 1200, 1440)), //30 + array(141, 2323, 3, array( 480, 868, 1290, 1530)), + array(145, 2465, 3, array( 510, 924, 1350, 1620)), + array(149, 2611, 3, array( 540, 980, 1440, 1710)), + array(153, 2761, 3, array( 570, 1036, 1530, 1800)), + array(157, 2876, 0, array( 570, 1064, 1590, 1890)), //35 + array(161, 3034, 0, array( 600, 1120, 1680, 1980)), + array(165, 3196, 0, array( 630, 1204, 1770, 2100)), + array(169, 3362, 0, array( 660, 1260, 1860, 2220)), + array(173, 3532, 0, array( 720, 1316, 1950, 2310)), + array(177, 3706, 0, array( 750, 1372, 2040, 2430)) //40 + ); + + //---------------------------------------------------------------------- + public static function getDataLength($version, $level) + { + return self::$capacity[$version][QRCAP_WORDS] - self::$capacity[$version][QRCAP_EC][$level]; + } + + //---------------------------------------------------------------------- + public static function getECCLength($version, $level) + { + return self::$capacity[$version][QRCAP_EC][$level]; + } + + //---------------------------------------------------------------------- + public static function getWidth($version) + { + return self::$capacity[$version][QRCAP_WIDTH]; + } + + //---------------------------------------------------------------------- + public static function getRemainder($version) + { + return self::$capacity[$version][QRCAP_REMINDER]; + } + + //---------------------------------------------------------------------- + public static function getMinimumVersion($size, $level) + { + + for($i=1; $i<= QRSPEC_VERSION_MAX; $i++) { + $words = self::$capacity[$i][QRCAP_WORDS] - self::$capacity[$i][QRCAP_EC][$level]; + if($words >= $size) + return $i; + } + + return -1; + } + + //###################################################################### + + public static $lengthTableBits = array( + array(10, 12, 14), + array( 9, 11, 13), + array( 8, 16, 16), + array( 8, 10, 12) + ); + + //---------------------------------------------------------------------- + public static function lengthIndicator($mode, $version) + { + if ($mode == QR_MODE_STRUCTURE) + return 0; + + if ($version <= 9) { + $l = 0; + } else if ($version <= 26) { + $l = 1; + } else { + $l = 2; + } + + return self::$lengthTableBits[$mode][$l]; + } + + //---------------------------------------------------------------------- + public static function maximumWords($mode, $version) + { + if($mode == QR_MODE_STRUCTURE) + return 3; + + if($version <= 9) { + $l = 0; + } else if($version <= 26) { + $l = 1; + } else { + $l = 2; + } + + $bits = self::$lengthTableBits[$mode][$l]; + $words = (1 << $bits) - 1; + + if($mode == QR_MODE_KANJI) { + $words *= 2; // the number of bytes is required + } + + return $words; + } + + // Error correction code ----------------------------------------------- + // Table of the error correction code (Reed-Solomon block) + // See Table 12-16 (pp.30-36), JIS X0510:2004. + + public static $eccTable = array( + array(array( 0, 0), array( 0, 0), array( 0, 0), array( 0, 0)), + array(array( 1, 0), array( 1, 0), array( 1, 0), array( 1, 0)), // 1 + array(array( 1, 0), array( 1, 0), array( 1, 0), array( 1, 0)), + array(array( 1, 0), array( 1, 0), array( 2, 0), array( 2, 0)), + array(array( 1, 0), array( 2, 0), array( 2, 0), array( 4, 0)), + array(array( 1, 0), array( 2, 0), array( 2, 2), array( 2, 2)), // 5 + array(array( 2, 0), array( 4, 0), array( 4, 0), array( 4, 0)), + array(array( 2, 0), array( 4, 0), array( 2, 4), array( 4, 1)), + array(array( 2, 0), array( 2, 2), array( 4, 2), array( 4, 2)), + array(array( 2, 0), array( 3, 2), array( 4, 4), array( 4, 4)), + array(array( 2, 2), array( 4, 1), array( 6, 2), array( 6, 2)), //10 + array(array( 4, 0), array( 1, 4), array( 4, 4), array( 3, 8)), + array(array( 2, 2), array( 6, 2), array( 4, 6), array( 7, 4)), + array(array( 4, 0), array( 8, 1), array( 8, 4), array(12, 4)), + array(array( 3, 1), array( 4, 5), array(11, 5), array(11, 5)), + array(array( 5, 1), array( 5, 5), array( 5, 7), array(11, 7)), //15 + array(array( 5, 1), array( 7, 3), array(15, 2), array( 3, 13)), + array(array( 1, 5), array(10, 1), array( 1, 15), array( 2, 17)), + array(array( 5, 1), array( 9, 4), array(17, 1), array( 2, 19)), + array(array( 3, 4), array( 3, 11), array(17, 4), array( 9, 16)), + array(array( 3, 5), array( 3, 13), array(15, 5), array(15, 10)), //20 + array(array( 4, 4), array(17, 0), array(17, 6), array(19, 6)), + array(array( 2, 7), array(17, 0), array( 7, 16), array(34, 0)), + array(array( 4, 5), array( 4, 14), array(11, 14), array(16, 14)), + array(array( 6, 4), array( 6, 14), array(11, 16), array(30, 2)), + array(array( 8, 4), array( 8, 13), array( 7, 22), array(22, 13)), //25 + array(array(10, 2), array(19, 4), array(28, 6), array(33, 4)), + array(array( 8, 4), array(22, 3), array( 8, 26), array(12, 28)), + array(array( 3, 10), array( 3, 23), array( 4, 31), array(11, 31)), + array(array( 7, 7), array(21, 7), array( 1, 37), array(19, 26)), + array(array( 5, 10), array(19, 10), array(15, 25), array(23, 25)), //30 + array(array(13, 3), array( 2, 29), array(42, 1), array(23, 28)), + array(array(17, 0), array(10, 23), array(10, 35), array(19, 35)), + array(array(17, 1), array(14, 21), array(29, 19), array(11, 46)), + array(array(13, 6), array(14, 23), array(44, 7), array(59, 1)), + array(array(12, 7), array(12, 26), array(39, 14), array(22, 41)), //35 + array(array( 6, 14), array( 6, 34), array(46, 10), array( 2, 64)), + array(array(17, 4), array(29, 14), array(49, 10), array(24, 46)), + array(array( 4, 18), array(13, 32), array(48, 14), array(42, 32)), + array(array(20, 4), array(40, 7), array(43, 22), array(10, 67)), + array(array(19, 6), array(18, 31), array(34, 34), array(20, 61)),//40 + ); + + //---------------------------------------------------------------------- + // CACHEABLE!!! + + public static function getEccSpec($version, $level, array &$spec) + { + if (count($spec) < 5) { + $spec = array(0,0,0,0,0); + } + + $b1 = self::$eccTable[$version][$level][0]; + $b2 = self::$eccTable[$version][$level][1]; + $data = self::getDataLength($version, $level); + $ecc = self::getECCLength($version, $level); + + if($b2 == 0) { + $spec[0] = $b1; + $spec[1] = (int)($data / $b1); + $spec[2] = (int)($ecc / $b1); + $spec[3] = 0; + $spec[4] = 0; + } else { + $spec[0] = $b1; + $spec[1] = (int)($data / ($b1 + $b2)); + $spec[2] = (int)($ecc / ($b1 + $b2)); + $spec[3] = $b2; + $spec[4] = $spec[1] + 1; + } + } + + // Alignment pattern --------------------------------------------------- + + // Positions of alignment patterns. + // This array includes only the second and the third position of the + // alignment patterns. Rest of them can be calculated from the distance + // between them. + + // See Table 1 in Appendix E (pp.71) of JIS X0510:2004. + + public static $alignmentPattern = array( + array( 0, 0), + array( 0, 0), array(18, 0), array(22, 0), array(26, 0), array(30, 0), // 1- 5 + array(34, 0), array(22, 38), array(24, 42), array(26, 46), array(28, 50), // 6-10 + array(30, 54), array(32, 58), array(34, 62), array(26, 46), array(26, 48), //11-15 + array(26, 50), array(30, 54), array(30, 56), array(30, 58), array(34, 62), //16-20 + array(28, 50), array(26, 50), array(30, 54), array(28, 54), array(32, 58), //21-25 + array(30, 58), array(34, 62), array(26, 50), array(30, 54), array(26, 52), //26-30 + array(30, 56), array(34, 60), array(30, 58), array(34, 62), array(30, 54), //31-35 + array(24, 50), array(28, 54), array(32, 58), array(26, 54), array(30, 58), //35-40 + ); + + + /** -------------------------------------------------------------------- + * Put an alignment marker. + * @param frame + * @param width + * @param ox,oy center coordinate of the pattern + */ + public static function putAlignmentMarker(array &$frame, $ox, $oy) + { + $finder = array( + "\xa1\xa1\xa1\xa1\xa1", + "\xa1\xa0\xa0\xa0\xa1", + "\xa1\xa0\xa1\xa0\xa1", + "\xa1\xa0\xa0\xa0\xa1", + "\xa1\xa1\xa1\xa1\xa1" + ); + + $yStart = $oy-2; + $xStart = $ox-2; + + for($y=0; $y<5; $y++) { + QRstr::set($frame, $xStart, $yStart+$y, $finder[$y]); + } + } + + //---------------------------------------------------------------------- + public static function putAlignmentPattern($version, &$frame, $width) + { + if($version < 2) + return; + + $d = self::$alignmentPattern[$version][1] - self::$alignmentPattern[$version][0]; + if($d < 0) { + $w = 2; + } else { + $w = (int)(($width - self::$alignmentPattern[$version][0]) / $d + 2); + } + + if($w * $w - 3 == 1) { + $x = self::$alignmentPattern[$version][0]; + $y = self::$alignmentPattern[$version][0]; + self::putAlignmentMarker($frame, $x, $y); + return; + } + + $cx = self::$alignmentPattern[$version][0]; + for($x=1; $x<$w - 1; $x++) { + self::putAlignmentMarker($frame, 6, $cx); + self::putAlignmentMarker($frame, $cx, 6); + $cx += $d; + } + + $cy = self::$alignmentPattern[$version][0]; + for($y=0; $y<$w-1; $y++) { + $cx = self::$alignmentPattern[$version][0]; + for($x=0; $x<$w-1; $x++) { + self::putAlignmentMarker($frame, $cx, $cy); + $cx += $d; + } + $cy += $d; + } + } + + // Version information pattern ----------------------------------------- + + // Version information pattern (BCH coded). + // See Table 1 in Appendix D (pp.68) of JIS X0510:2004. + + // size: [QRSPEC_VERSION_MAX - 6] + + public static $versionPattern = array( + 0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d, + 0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9, + 0x177ec, 0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75, + 0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64, + 0x27541, 0x28c69 + ); + + //---------------------------------------------------------------------- + public static function getVersionPattern($version) + { + if($version < 7 || $version > QRSPEC_VERSION_MAX) + return 0; + + return self::$versionPattern[$version -7]; + } + + // Format information -------------------------------------------------- + // See calcFormatInfo in tests/test_qrspec.c (orginal qrencode c lib) + + public static $formatInfo = array( + array(0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976), + array(0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0), + array(0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed), + array(0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b) + ); + + public static function getFormatInfo($mask, $level) + { + if($mask < 0 || $mask > 7) + return 0; + + if($level < 0 || $level > 3) + return 0; + + return self::$formatInfo[$level][$mask]; + } + + // Frame --------------------------------------------------------------- + // Cache of initial frames. + + public static $frames = array(); + + /** -------------------------------------------------------------------- + * Put a finder pattern. + * @param frame + * @param width + * @param ox,oy upper-left coordinate of the pattern + */ + public static function putFinderPattern(&$frame, $ox, $oy) + { + $finder = array( + "\xc1\xc1\xc1\xc1\xc1\xc1\xc1", + "\xc1\xc0\xc0\xc0\xc0\xc0\xc1", + "\xc1\xc0\xc1\xc1\xc1\xc0\xc1", + "\xc1\xc0\xc1\xc1\xc1\xc0\xc1", + "\xc1\xc0\xc1\xc1\xc1\xc0\xc1", + "\xc1\xc0\xc0\xc0\xc0\xc0\xc1", + "\xc1\xc1\xc1\xc1\xc1\xc1\xc1" + ); + + for($y=0; $y<7; $y++) { + QRstr::set($frame, $ox, $oy+$y, $finder[$y]); + } + } + + //---------------------------------------------------------------------- + public static function createFrame($version) + { + $width = self::$capacity[$version][QRCAP_WIDTH]; + $frameLine = str_repeat ("\0", $width); + $frame = array_fill(0, $width, $frameLine); + + // Finder pattern + self::putFinderPattern($frame, 0, 0); + self::putFinderPattern($frame, $width - 7, 0); + self::putFinderPattern($frame, 0, $width - 7); + + // Separator + $yOffset = $width - 7; + + for($y=0; $y<7; $y++) { + $frame[$y][7] = "\xc0"; + $frame[$y][$width - 8] = "\xc0"; + $frame[$yOffset][7] = "\xc0"; + $yOffset++; + } + + $setPattern = str_repeat("\xc0", 8); + + QRstr::set($frame, 0, 7, $setPattern); + QRstr::set($frame, $width-8, 7, $setPattern); + QRstr::set($frame, 0, $width - 8, $setPattern); + + // Format info + $setPattern = str_repeat("\x84", 9); + QRstr::set($frame, 0, 8, $setPattern); + QRstr::set($frame, $width - 8, 8, $setPattern, 8); + + $yOffset = $width - 8; + + for($y=0; $y<8; $y++,$yOffset++) { + $frame[$y][8] = "\x84"; + $frame[$yOffset][8] = "\x84"; + } + + // Timing pattern + + for($i=1; $i<$width-15; $i++) { + $frame[6][7+$i] = chr(0x90 | ($i & 1)); + $frame[7+$i][6] = chr(0x90 | ($i & 1)); + } + + // Alignment pattern + self::putAlignmentPattern($version, $frame, $width); + + // Version information + if($version >= 7) { + $vinf = self::getVersionPattern($version); + + $v = $vinf; + + for($x=0; $x<6; $x++) { + for($y=0; $y<3; $y++) { + $frame[($width - 11)+$y][$x] = chr(0x88 | ($v & 1)); + $v = $v >> 1; + } + } + + $v = $vinf; + for($y=0; $y<6; $y++) { + for($x=0; $x<3; $x++) { + $frame[$y][$x+($width - 11)] = chr(0x88 | ($v & 1)); + $v = $v >> 1; + } + } + } + + // and a little bit... + $frame[$width - 8][8] = "\x81"; + + return $frame; + } + + //---------------------------------------------------------------------- + public static function debug($frame, $binary_mode = false) + { + if ($binary_mode) { + + foreach ($frame as &$frameLine) { + $frameLine = join('  ', explode('0', $frameLine)); + $frameLine = join('██', explode('1', $frameLine)); + } + + ?> + +


        '; + echo join("
        ", $frame); + echo '






'; + + } else { + + foreach ($frame as &$frameLine) { + $frameLine = join(' ', explode("\xc0", $frameLine)); + $frameLine = join('', explode("\xc1", $frameLine)); + $frameLine = join(' ', explode("\xa0", $frameLine)); + $frameLine = join('', explode("\xa1", $frameLine)); + $frameLine = join('', explode("\x84", $frameLine)); //format 0 + $frameLine = join('', explode("\x85", $frameLine)); //format 1 + $frameLine = join('', explode("\x81", $frameLine)); //special bit + $frameLine = join(' ', explode("\x90", $frameLine)); //clock 0 + $frameLine = join('', explode("\x91", $frameLine)); //clock 1 + $frameLine = join(' ', explode("\x88", $frameLine)); //version + $frameLine = join('', explode("\x89", $frameLine)); //version + $frameLine = join('♦', explode("\x01", $frameLine)); + $frameLine = join('⋅', explode("\0", $frameLine)); + } + + ?> + + "; + echo join("
", $frame); + echo "
"; + + } + } + + //---------------------------------------------------------------------- + public static function serial($frame) + { + return gzcompress(join("\n", $frame), 9); + } + + //---------------------------------------------------------------------- + public static function unserial($code) + { + return explode("\n", gzuncompress($code)); + } + + //---------------------------------------------------------------------- + public static function newFrame($version) + { + if($version < 1 || $version > QRSPEC_VERSION_MAX) + return null; + + if(!isset(self::$frames[$version])) { + + $fileName = QR_CACHE_DIR.'frame_'.$version.'.dat'; + + if (QR_CACHEABLE) { + if (file_exists($fileName)) { + self::$frames[$version] = self::unserial(file_get_contents($fileName)); + } else { + self::$frames[$version] = self::createFrame($version); + file_put_contents($fileName, self::serial(self::$frames[$version])); + } + } else { + self::$frames[$version] = self::createFrame($version); + } + } + + if(is_null(self::$frames[$version])) + return null; + + return self::$frames[$version]; + } + + //---------------------------------------------------------------------- + public static function rsBlockNum($spec) { return $spec[0] + $spec[3]; } + public static function rsBlockNum1($spec) { return $spec[0]; } + public static function rsDataCodes1($spec) { return $spec[1]; } + public static function rsEccCodes1($spec) { return $spec[2]; } + public static function rsBlockNum2($spec) { return $spec[3]; } + public static function rsDataCodes2($spec) { return $spec[4]; } + public static function rsEccCodes2($spec) { return $spec[2]; } + public static function rsDataLength($spec) { return ($spec[0] * $spec[1]) + ($spec[3] * $spec[4]); } + public static function rsEccLength($spec) { return ($spec[0] + $spec[3]) * $spec[2]; } + + } + + + +//---- qrimage.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Image output of code using GD2 + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('QR_IMAGE', true); + + class QRimage { + + //---------------------------------------------------------------------- + public static function png($frame, $filename = false, $pixelPerPoint = 4, $outerFrame = 4,$saveandprint=FALSE) + { + $image = self::image($frame, $pixelPerPoint, $outerFrame); + + if ($filename === false) { + Header("Content-type: image/png"); + ImagePng($image); + } else { + if($saveandprint===TRUE){ + ImagePng($image, $filename); + header("Content-type: image/png"); + ImagePng($image); + }else{ + ImagePng($image, $filename); + } + } + + ImageDestroy($image); + } + + //---------------------------------------------------------------------- + public static function jpg($frame, $filename = false, $pixelPerPoint = 8, $outerFrame = 4, $q = 85) + { + $image = self::image($frame, $pixelPerPoint, $outerFrame); + + if ($filename === false) { + Header("Content-type: image/jpeg"); + ImageJpeg($image, null, $q); + } else { + ImageJpeg($image, $filename, $q); + } + + ImageDestroy($image); + } + + //---------------------------------------------------------------------- + private static function image($frame, $pixelPerPoint = 4, $outerFrame = 4) + { + $h = count($frame); + $w = strlen($frame[0]); + + $imgW = $w + 2*$outerFrame; + $imgH = $h + 2*$outerFrame; + + $base_image =ImageCreate($imgW, $imgH); + + $col[0] = ImageColorAllocate($base_image,255,255,255); + $col[1] = ImageColorAllocate($base_image,0,0,0); + + imagefill($base_image, 0, 0, $col[0]); + + for($y=0; $y<$h; $y++) { + for($x=0; $x<$w; $x++) { + if ($frame[$y][$x] == '1') { + ImageSetPixel($base_image,$x+$outerFrame,$y+$outerFrame,$col[1]); + } + } + } + + $target_image =ImageCreate($imgW * $pixelPerPoint, $imgH * $pixelPerPoint); + ImageCopyResized($target_image, $base_image, 0, 0, 0, 0, $imgW * $pixelPerPoint, $imgH * $pixelPerPoint, $imgW, $imgH); + ImageDestroy($base_image); + + return $target_image; + } + } + + + +//---- qrinput.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Input encoding class + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('STRUCTURE_HEADER_BITS', 20); + define('MAX_STRUCTURED_SYMBOLS', 16); + + class QRinputItem { + + public $mode; + public $size; + public $data; + public $bstream; + + public function __construct($mode, $size, $data, $bstream = null) + { + $setData = array_slice($data, 0, $size); + + if (count($setData) < $size) { + $setData = array_merge($setData, array_fill(0,$size-count($setData),0)); + } + + if(!QRinput::check($mode, $size, $setData)) { + throw new Exception('Error m:'.$mode.',s:'.$size.',d:'.join(',',$setData)); + return null; + } + + $this->mode = $mode; + $this->size = $size; + $this->data = $setData; + $this->bstream = $bstream; + } + + //---------------------------------------------------------------------- + public function encodeModeNum($version) + { + try { + + $words = (int)($this->size / 3); + $bs = new QRbitstream(); + + $val = 0x1; + $bs->appendNum(4, $val); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_NUM, $version), $this->size); + + for($i=0; $i<$words; $i++) { + $val = (ord($this->data[$i*3 ]) - ord('0')) * 100; + $val += (ord($this->data[$i*3+1]) - ord('0')) * 10; + $val += (ord($this->data[$i*3+2]) - ord('0')); + $bs->appendNum(10, $val); + } + + if($this->size - $words * 3 == 1) { + $val = ord($this->data[$words*3]) - ord('0'); + $bs->appendNum(4, $val); + } else if($this->size - $words * 3 == 2) { + $val = (ord($this->data[$words*3 ]) - ord('0')) * 10; + $val += (ord($this->data[$words*3+1]) - ord('0')); + $bs->appendNum(7, $val); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeModeAn($version) + { + try { + $words = (int)($this->size / 2); + $bs = new QRbitstream(); + + $bs->appendNum(4, 0x02); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_AN, $version), $this->size); + + for($i=0; $i<$words; $i++) { + $val = (int)QRinput::lookAnTable(ord($this->data[$i*2 ])) * 45; + $val += (int)QRinput::lookAnTable(ord($this->data[$i*2+1])); + + $bs->appendNum(11, $val); + } + + if($this->size & 1) { + $val = QRinput::lookAnTable(ord($this->data[$words * 2])); + $bs->appendNum(6, $val); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeMode8($version) + { + try { + $bs = new QRbitstream(); + + $bs->appendNum(4, 0x4); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_8, $version), $this->size); + + for($i=0; $i<$this->size; $i++) { + $bs->appendNum(8, ord($this->data[$i])); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeModeKanji($version) + { + try { + + $bs = new QRbitrtream(); + + $bs->appendNum(4, 0x8); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_KANJI, $version), (int)($this->size / 2)); + + for($i=0; $i<$this->size; $i+=2) { + $val = (ord($this->data[$i]) << 8) | ord($this->data[$i+1]); + if($val <= 0x9ffc) { + $val -= 0x8140; + } else { + $val -= 0xc140; + } + + $h = ($val >> 8) * 0xc0; + $val = ($val & 0xff) + $h; + + $bs->appendNum(13, $val); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeModeStructure() + { + try { + $bs = new QRbitstream(); + + $bs->appendNum(4, 0x03); + $bs->appendNum(4, ord($this->data[1]) - 1); + $bs->appendNum(4, ord($this->data[0]) - 1); + $bs->appendNum(8, ord($this->data[2])); + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function estimateBitStreamSizeOfEntry($version) + { + $bits = 0; + + if($version == 0) + $version = 1; + + switch($this->mode) { + case QR_MODE_NUM: $bits = QRinput::estimateBitsModeNum($this->size); break; + case QR_MODE_AN: $bits = QRinput::estimateBitsModeAn($this->size); break; + case QR_MODE_8: $bits = QRinput::estimateBitsMode8($this->size); break; + case QR_MODE_KANJI: $bits = QRinput::estimateBitsModeKanji($this->size);break; + case QR_MODE_STRUCTURE: return STRUCTURE_HEADER_BITS; + default: + return 0; + } + + $l = QRspec::lengthIndicator($this->mode, $version); + $m = 1 << $l; + $num = (int)(($this->size + $m - 1) / $m); + + $bits += $num * (4 + $l); + + return $bits; + } + + //---------------------------------------------------------------------- + public function encodeBitStream($version) + { + try { + + unset($this->bstream); + $words = QRspec::maximumWords($this->mode, $version); + + if($this->size > $words) { + + $st1 = new QRinputItem($this->mode, $words, $this->data); + $st2 = new QRinputItem($this->mode, $this->size - $words, array_slice($this->data, $words)); + + $st1->encodeBitStream($version); + $st2->encodeBitStream($version); + + $this->bstream = new QRbitstream(); + $this->bstream->append($st1->bstream); + $this->bstream->append($st2->bstream); + + unset($st1); + unset($st2); + + } else { + + $ret = 0; + + switch($this->mode) { + case QR_MODE_NUM: $ret = $this->encodeModeNum($version); break; + case QR_MODE_AN: $ret = $this->encodeModeAn($version); break; + case QR_MODE_8: $ret = $this->encodeMode8($version); break; + case QR_MODE_KANJI: $ret = $this->encodeModeKanji($version);break; + case QR_MODE_STRUCTURE: $ret = $this->encodeModeStructure(); break; + + default: + break; + } + + if($ret < 0) + return -1; + } + + return $this->bstream->size(); + + } catch (Exception $e) { + return -1; + } + } + }; + + //########################################################################## + + class QRinput { + + public $items; + + private $version; + private $level; + + //---------------------------------------------------------------------- + public function __construct($version = 0, $level = QR_ECLEVEL_L) + { + if ($version < 0 || $version > QRSPEC_VERSION_MAX || $level > QR_ECLEVEL_H) { + throw new Exception('Invalid version no'); + return NULL; + } + + $this->version = $version; + $this->level = $level; + } + + //---------------------------------------------------------------------- + public function getVersion() + { + return $this->version; + } + + //---------------------------------------------------------------------- + public function setVersion($version) + { + if($version < 0 || $version > QRSPEC_VERSION_MAX) { + throw new Exception('Invalid version no'); + return -1; + } + + $this->version = $version; + + return 0; + } + + //---------------------------------------------------------------------- + public function getErrorCorrectionLevel() + { + return $this->level; + } + + //---------------------------------------------------------------------- + public function setErrorCorrectionLevel($level) + { + if($level > QR_ECLEVEL_H) { + throw new Exception('Invalid ECLEVEL'); + return -1; + } + + $this->level = $level; + + return 0; + } + + //---------------------------------------------------------------------- + public function appendEntry(QRinputItem $entry) + { + $this->items[] = $entry; + } + + //---------------------------------------------------------------------- + public function append($mode, $size, $data) + { + try { + $entry = new QRinputItem($mode, $size, $data); + $this->items[] = $entry; + return 0; + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + + public function insertStructuredAppendHeader($size, $index, $parity) + { + if( $size > MAX_STRUCTURED_SYMBOLS ) { + throw new Exception('insertStructuredAppendHeader wrong size'); + } + + if( $index <= 0 || $index > MAX_STRUCTURED_SYMBOLS ) { + throw new Exception('insertStructuredAppendHeader wrong index'); + } + + $buf = array($size, $index, $parity); + + try { + $entry = new QRinputItem(QR_MODE_STRUCTURE, 3, buf); + array_unshift($this->items, $entry); + return 0; + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function calcParity() + { + $parity = 0; + + foreach($this->items as $item) { + if($item->mode != QR_MODE_STRUCTURE) { + for($i=$item->size-1; $i>=0; $i--) { + $parity ^= $item->data[$i]; + } + } + } + + return $parity; + } + + //---------------------------------------------------------------------- + public static function checkModeNum($size, $data) + { + for($i=0; $i<$size; $i++) { + if((ord($data[$i]) < ord('0')) || (ord($data[$i]) > ord('9'))){ + return false; + } + } + + return true; + } + + //---------------------------------------------------------------------- + public static function estimateBitsModeNum($size) + { + $w = (int)$size / 3; + $bits = $w * 10; + + switch($size - $w * 3) { + case 1: + $bits += 4; + break; + case 2: + $bits += 7; + break; + default: + break; + } + + return $bits; + } + + //---------------------------------------------------------------------- + public static $anTable = array( + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + ); + + //---------------------------------------------------------------------- + public static function lookAnTable($c) + { + return (($c > 127)?-1:self::$anTable[$c]); + } + + //---------------------------------------------------------------------- + public static function checkModeAn($size, $data) + { + for($i=0; $i<$size; $i++) { + if (self::lookAnTable(ord($data[$i])) == -1) { + return false; + } + } + + return true; + } + + //---------------------------------------------------------------------- + public static function estimateBitsModeAn($size) + { + $w = (int)($size / 2); + $bits = $w * 11; + + if($size & 1) { + $bits += 6; + } + + return $bits; + } + + //---------------------------------------------------------------------- + public static function estimateBitsMode8($size) + { + return $size * 8; + } + + //---------------------------------------------------------------------- + public function estimateBitsModeKanji($size) + { + return (int)(($size / 2) * 13); + } + + //---------------------------------------------------------------------- + public static function checkModeKanji($size, $data) + { + if($size & 1) + return false; + + for($i=0; $i<$size; $i+=2) { + $val = (ord($data[$i]) << 8) | ord($data[$i+1]); + if( $val < 0x8140 + || ($val > 0x9ffc && $val < 0xe040) + || $val > 0xebbf) { + return false; + } + } + + return true; + } + + /*********************************************************************** + * Validation + **********************************************************************/ + + public static function check($mode, $size, $data) + { + if($size <= 0) + return false; + + switch($mode) { + case QR_MODE_NUM: return self::checkModeNum($size, $data); break; + case QR_MODE_AN: return self::checkModeAn($size, $data); break; + case QR_MODE_KANJI: return self::checkModeKanji($size, $data); break; + case QR_MODE_8: return true; break; + case QR_MODE_STRUCTURE: return true; break; + + default: + break; + } + + return false; + } + + + //---------------------------------------------------------------------- + public function estimateBitStreamSize($version) + { + $bits = 0; + + foreach($this->items as $item) { + $bits += $item->estimateBitStreamSizeOfEntry($version); + } + + return $bits; + } + + //---------------------------------------------------------------------- + public function estimateVersion() + { + $version = 0; + $prev = 0; + do { + $prev = $version; + $bits = $this->estimateBitStreamSize($prev); + $version = QRspec::getMinimumVersion((int)(($bits + 7) / 8), $this->level); + if ($version < 0) { + return -1; + } + } while ($version > $prev); + + return $version; + } + + //---------------------------------------------------------------------- + public static function lengthOfCode($mode, $version, $bits) + { + $payload = $bits - 4 - QRspec::lengthIndicator($mode, $version); + switch($mode) { + case QR_MODE_NUM: + $chunks = (int)($payload / 10); + $remain = $payload - $chunks * 10; + $size = $chunks * 3; + if($remain >= 7) { + $size += 2; + } else if($remain >= 4) { + $size += 1; + } + break; + case QR_MODE_AN: + $chunks = (int)($payload / 11); + $remain = $payload - $chunks * 11; + $size = $chunks * 2; + if($remain >= 6) + $size++; + break; + case QR_MODE_8: + $size = (int)($payload / 8); + break; + case QR_MODE_KANJI: + $size = (int)(($payload / 13) * 2); + break; + case QR_MODE_STRUCTURE: + $size = (int)($payload / 8); + break; + default: + $size = 0; + break; + } + + $maxsize = QRspec::maximumWords($mode, $version); + if($size < 0) $size = 0; + if($size > $maxsize) $size = $maxsize; + + return $size; + } + + //---------------------------------------------------------------------- + public function createBitStream() + { + $total = 0; + + foreach($this->items as $item) { + $bits = $item->encodeBitStream($this->version); + + if($bits < 0) + return -1; + + $total += $bits; + } + + return $total; + } + + //---------------------------------------------------------------------- + public function convertData() + { + $ver = $this->estimateVersion(); + if($ver > $this->getVersion()) { + $this->setVersion($ver); + } + + for(;;) { + $bits = $this->createBitStream(); + + if($bits < 0) + return -1; + + $ver = QRspec::getMinimumVersion((int)(($bits + 7) / 8), $this->level); + if($ver < 0) { + throw new Exception('WRONG VERSION'); + return -1; + } else if($ver > $this->getVersion()) { + $this->setVersion($ver); + } else { + break; + } + } + + return 0; + } + + //---------------------------------------------------------------------- + public function appendPaddingBit(&$bstream) + { + $bits = $bstream->size(); + $maxwords = QRspec::getDataLength($this->version, $this->level); + $maxbits = $maxwords * 8; + + if ($maxbits == $bits) { + return 0; + } + + if ($maxbits - $bits < 5) { + return $bstream->appendNum($maxbits - $bits, 0); + } + + $bits += 4; + $words = (int)(($bits + 7) / 8); + + $padding = new QRbitstream(); + $ret = $padding->appendNum($words * 8 - $bits + 4, 0); + + if($ret < 0) + return $ret; + + $padlen = $maxwords - $words; + + if($padlen > 0) { + + $padbuf = array(); + for($i=0; $i<$padlen; $i++) { + $padbuf[$i] = ($i&1)?0x11:0xec; + } + + $ret = $padding->appendBytes($padlen, $padbuf); + + if($ret < 0) + return $ret; + + } + + $ret = $bstream->append($padding); + + return $ret; + } + + //---------------------------------------------------------------------- + public function mergeBitStream() + { + if($this->convertData() < 0) { + return null; + } + + $bstream = new QRbitstream(); + + foreach($this->items as $item) { + $ret = $bstream->append($item->bstream); + if($ret < 0) { + return null; + } + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public function getBitStream() + { + + $bstream = $this->mergeBitStream(); + + if($bstream == null) { + return null; + } + + $ret = $this->appendPaddingBit($bstream); + if($ret < 0) { + return null; + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public function getByteStream() + { + $bstream = $this->getBitStream(); + if($bstream == null) { + return null; + } + + return $bstream->toByte(); + } + } + + + + + + +//---- qrbitstream.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Bitstream class + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRbitstream { + + public $data = array(); + + //---------------------------------------------------------------------- + public function size() + { + return count($this->data); + } + + //---------------------------------------------------------------------- + public function allocate($setLength) + { + $this->data = array_fill(0, $setLength, 0); + return 0; + } + + //---------------------------------------------------------------------- + public static function newFromNum($bits, $num) + { + $bstream = new QRbitstream(); + $bstream->allocate($bits); + + $mask = 1 << ($bits - 1); + for($i=0; $i<$bits; $i++) { + if($num & $mask) { + $bstream->data[$i] = 1; + } else { + $bstream->data[$i] = 0; + } + $mask = $mask >> 1; + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public static function newFromBytes($size, $data) + { + $bstream = new QRbitstream(); + $bstream->allocate($size * 8); + $p=0; + + for($i=0; $i<$size; $i++) { + $mask = 0x80; + for($j=0; $j<8; $j++) { + if($data[$i] & $mask) { + $bstream->data[$p] = 1; + } else { + $bstream->data[$p] = 0; + } + $p++; + $mask = $mask >> 1; + } + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public function append(QRbitstream $arg) + { + if (is_null($arg)) { + return -1; + } + + if($arg->size() == 0) { + return 0; + } + + if($this->size() == 0) { + $this->data = $arg->data; + return 0; + } + + $this->data = array_values(array_merge($this->data, $arg->data)); + + return 0; + } + + //---------------------------------------------------------------------- + public function appendNum($bits, $num) + { + if ($bits == 0) + return 0; + + $b = QRbitstream::newFromNum($bits, $num); + + if(is_null($b)) + return -1; + + $ret = $this->append($b); + unset($b); + + return $ret; + } + + //---------------------------------------------------------------------- + public function appendBytes($size, $data) + { + if ($size == 0) + return 0; + + $b = QRbitstream::newFromBytes($size, $data); + + if(is_null($b)) + return -1; + + $ret = $this->append($b); + unset($b); + + return $ret; + } + + //---------------------------------------------------------------------- + public function toByte() + { + + $size = $this->size(); + + if($size == 0) { + return array(); + } + + $data = array_fill(0, (int)(($size + 7) / 8), 0); + $bytes = (int)($size / 8); + + $p = 0; + + for($i=0; $i<$bytes; $i++) { + $v = 0; + for($j=0; $j<8; $j++) { + $v = $v << 1; + $v |= $this->data[$p]; + $p++; + } + $data[$i] = $v; + } + + if($size & 7) { + $v = 0; + for($j=0; $j<($size & 7); $j++) { + $v = $v << 1; + $v |= $this->data[$p]; + $p++; + } + $data[$bytes] = $v; + } + + return $data; + } + + } + + + + +//---- qrsplit.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Input splitting classes + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * The following data / specifications are taken from + * "Two dimensional symbol -- QR-code -- Basic Specification" (JIS X0510:2004) + * or + * "Automatic identification and data capture techniques -- + * QR Code 2005 bar code symbology specification" (ISO/IEC 18004:2006) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + class QRsplit { + + public $dataStr = ''; + public $input; + public $modeHint; + + //---------------------------------------------------------------------- + public function __construct($dataStr, $input, $modeHint) + { + $this->dataStr = $dataStr; + $this->input = $input; + $this->modeHint = $modeHint; + } + + //---------------------------------------------------------------------- + public static function isdigitat($str, $pos) + { + if ($pos >= strlen($str)) + return false; + + return ((ord($str[$pos]) >= ord('0'))&&(ord($str[$pos]) <= ord('9'))); + } + + //---------------------------------------------------------------------- + public static function isalnumat($str, $pos) + { + if ($pos >= strlen($str)) + return false; + + return (QRinput::lookAnTable(ord($str[$pos])) >= 0); + } + + //---------------------------------------------------------------------- + public function identifyMode($pos) + { + if ($pos >= strlen($this->dataStr)) + return QR_MODE_NUL; + + $c = $this->dataStr[$pos]; + + if(self::isdigitat($this->dataStr, $pos)) { + return QR_MODE_NUM; + } else if(self::isalnumat($this->dataStr, $pos)) { + return QR_MODE_AN; + } else if($this->modeHint == QR_MODE_KANJI) { + + if ($pos+1 < strlen($this->dataStr)) + { + $d = $this->dataStr[$pos+1]; + $word = (ord($c) << 8) | ord($d); + if(($word >= 0x8140 && $word <= 0x9ffc) || ($word >= 0xe040 && $word <= 0xebbf)) { + return QR_MODE_KANJI; + } + } + } + + return QR_MODE_8; + } + + //---------------------------------------------------------------------- + public function eatNum() + { + $ln = QRspec::lengthIndicator(QR_MODE_NUM, $this->input->getVersion()); + + $p = 0; + while(self::isdigitat($this->dataStr, $p)) { + $p++; + } + + $run = $p; + $mode = $this->identifyMode($p); + + if($mode == QR_MODE_8) { + $dif = QRinput::estimateBitsModeNum($run) + 4 + $ln + + QRinput::estimateBitsMode8(1) // + 4 + l8 + - QRinput::estimateBitsMode8($run + 1); // - 4 - l8 + if($dif > 0) { + return $this->eat8(); + } + } + if($mode == QR_MODE_AN) { + $dif = QRinput::estimateBitsModeNum($run) + 4 + $ln + + QRinput::estimateBitsModeAn(1) // + 4 + la + - QRinput::estimateBitsModeAn($run + 1);// - 4 - la + if($dif > 0) { + return $this->eatAn(); + } + } + + $ret = $this->input->append(QR_MODE_NUM, $run, str_split($this->dataStr)); + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function eatAn() + { + $la = QRspec::lengthIndicator(QR_MODE_AN, $this->input->getVersion()); + $ln = QRspec::lengthIndicator(QR_MODE_NUM, $this->input->getVersion()); + + $p = 0; + + while(self::isalnumat($this->dataStr, $p)) { + if(self::isdigitat($this->dataStr, $p)) { + $q = $p; + while(self::isdigitat($this->dataStr, $q)) { + $q++; + } + + $dif = QRinput::estimateBitsModeAn($p) // + 4 + la + + QRinput::estimateBitsModeNum($q - $p) + 4 + $ln + - QRinput::estimateBitsModeAn($q); // - 4 - la + + if($dif < 0) { + break; + } else { + $p = $q; + } + } else { + $p++; + } + } + + $run = $p; + + if(!self::isalnumat($this->dataStr, $p)) { + $dif = QRinput::estimateBitsModeAn($run) + 4 + $la + + QRinput::estimateBitsMode8(1) // + 4 + l8 + - QRinput::estimateBitsMode8($run + 1); // - 4 - l8 + if($dif > 0) { + return $this->eat8(); + } + } + + $ret = $this->input->append(QR_MODE_AN, $run, str_split($this->dataStr)); + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function eatKanji() + { + $p = 0; + + while($this->identifyMode($p) == QR_MODE_KANJI) { + $p += 2; + } + + $ret = $this->input->append(QR_MODE_KANJI, $p, str_split($this->dataStr)); + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function eat8() + { + $la = QRspec::lengthIndicator(QR_MODE_AN, $this->input->getVersion()); + $ln = QRspec::lengthIndicator(QR_MODE_NUM, $this->input->getVersion()); + + $p = 1; + $dataStrLen = strlen($this->dataStr); + + while($p < $dataStrLen) { + + $mode = $this->identifyMode($p); + if($mode == QR_MODE_KANJI) { + break; + } + if($mode == QR_MODE_NUM) { + $q = $p; + while(self::isdigitat($this->dataStr, $q)) { + $q++; + } + $dif = QRinput::estimateBitsMode8($p) // + 4 + l8 + + QRinput::estimateBitsModeNum($q - $p) + 4 + $ln + - QRinput::estimateBitsMode8($q); // - 4 - l8 + if($dif < 0) { + break; + } else { + $p = $q; + } + } else if($mode == QR_MODE_AN) { + $q = $p; + while(self::isalnumat($this->dataStr, $q)) { + $q++; + } + $dif = QRinput::estimateBitsMode8($p) // + 4 + l8 + + QRinput::estimateBitsModeAn($q - $p) + 4 + $la + - QRinput::estimateBitsMode8($q); // - 4 - l8 + if($dif < 0) { + break; + } else { + $p = $q; + } + } else { + $p++; + } + } + + $run = $p; + $ret = $this->input->append(QR_MODE_8, $run, str_split($this->dataStr)); + + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function splitString() + { + while (strlen($this->dataStr) > 0) + { + if($this->dataStr == '') + return 0; + + $mode = $this->identifyMode(0); + + switch ($mode) { + case QR_MODE_NUM: $length = $this->eatNum(); break; + case QR_MODE_AN: $length = $this->eatAn(); break; + case QR_MODE_KANJI: + if ($hint == QR_MODE_KANJI) + $length = $this->eatKanji(); + else $length = $this->eat8(); + break; + default: $length = $this->eat8(); break; + + } + + if($length == 0) return 0; + if($length < 0) return -1; + + $this->dataStr = substr($this->dataStr, $length); + } + } + + //---------------------------------------------------------------------- + public function toUpper() + { + $stringLen = strlen($this->dataStr); + $p = 0; + + while ($p<$stringLen) { + $mode = self::identifyMode(substr($this->dataStr, $p), $this->modeHint); + if($mode == QR_MODE_KANJI) { + $p += 2; + } else { + if (ord($this->dataStr[$p]) >= ord('a') && ord($this->dataStr[$p]) <= ord('z')) { + $this->dataStr[$p] = chr(ord($this->dataStr[$p]) - 32); + } + $p++; + } + } + + return $this->dataStr; + } + + //---------------------------------------------------------------------- + public static function splitStringToQRinput($string, QRinput $input, $modeHint, $casesensitive = true) + { + if(is_null($string) || $string == '\0' || $string == '') { + throw new Exception('empty string!!!'); + } + + $split = new QRsplit($string, $input, $modeHint); + + if(!$casesensitive) + $split->toUpper(); + + return $split->splitString(); + } + } + + + +//---- qrrscode.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Reed-Solomon error correction support + * + * Copyright (C) 2002, 2003, 2004, 2006 Phil Karn, KA9Q + * (libfec is released under the GNU Lesser General Public License.) + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRrsItem { + + public $mm; // Bits per symbol + public $nn; // Symbols per block (= (1<= $this->nn) { + $x -= $this->nn; + $x = ($x >> $this->mm) + ($x & $this->nn); + } + + return $x; + } + + //---------------------------------------------------------------------- + public static function init_rs_char($symsize, $gfpoly, $fcr, $prim, $nroots, $pad) + { + // Common code for intializing a Reed-Solomon control block (char or int symbols) + // Copyright 2004 Phil Karn, KA9Q + // May be used under the terms of the GNU Lesser General Public License (LGPL) + + $rs = null; + + // Check parameter ranges + if($symsize < 0 || $symsize > 8) return $rs; + if($fcr < 0 || $fcr >= (1<<$symsize)) return $rs; + if($prim <= 0 || $prim >= (1<<$symsize)) return $rs; + if($nroots < 0 || $nroots >= (1<<$symsize)) return $rs; // Can't have more roots than symbol values! + if($pad < 0 || $pad >= ((1<<$symsize) -1 - $nroots)) return $rs; // Too much padding + + $rs = new QRrsItem(); + $rs->mm = $symsize; + $rs->nn = (1<<$symsize)-1; + $rs->pad = $pad; + + $rs->alpha_to = array_fill(0, $rs->nn+1, 0); + $rs->index_of = array_fill(0, $rs->nn+1, 0); + + // PHP style macro replacement ;) + $NN =& $rs->nn; + $A0 =& $NN; + + // Generate Galois field lookup tables + $rs->index_of[0] = $A0; // log(zero) = -inf + $rs->alpha_to[$A0] = 0; // alpha**-inf = 0 + $sr = 1; + + for($i=0; $i<$rs->nn; $i++) { + $rs->index_of[$sr] = $i; + $rs->alpha_to[$i] = $sr; + $sr <<= 1; + if($sr & (1<<$symsize)) { + $sr ^= $gfpoly; + } + $sr &= $rs->nn; + } + + if($sr != 1){ + // field generator polynomial is not primitive! + $rs = NULL; + return $rs; + } + + /* Form RS code generator polynomial from its roots */ + $rs->genpoly = array_fill(0, $nroots+1, 0); + + $rs->fcr = $fcr; + $rs->prim = $prim; + $rs->nroots = $nroots; + $rs->gfpoly = $gfpoly; + + /* Find prim-th root of 1, used in decoding */ + for($iprim=1;($iprim % $prim) != 0;$iprim += $rs->nn) + ; // intentional empty-body loop! + + $rs->iprim = (int)($iprim / $prim); + $rs->genpoly[0] = 1; + + for ($i = 0,$root=$fcr*$prim; $i < $nroots; $i++, $root += $prim) { + $rs->genpoly[$i+1] = 1; + + // Multiply rs->genpoly[] by @**(root + x) + for ($j = $i; $j > 0; $j--) { + if ($rs->genpoly[$j] != 0) { + $rs->genpoly[$j] = $rs->genpoly[$j-1] ^ $rs->alpha_to[$rs->modnn($rs->index_of[$rs->genpoly[$j]] + $root)]; + } else { + $rs->genpoly[$j] = $rs->genpoly[$j-1]; + } + } + // rs->genpoly[0] can never be zero + $rs->genpoly[0] = $rs->alpha_to[$rs->modnn($rs->index_of[$rs->genpoly[0]] + $root)]; + } + + // convert rs->genpoly[] to index form for quicker encoding + for ($i = 0; $i <= $nroots; $i++) + $rs->genpoly[$i] = $rs->index_of[$rs->genpoly[$i]]; + + return $rs; + } + + //---------------------------------------------------------------------- + public function encode_rs_char($data, &$parity) + { + $MM =& $this->mm; + $NN =& $this->nn; + $ALPHA_TO =& $this->alpha_to; + $INDEX_OF =& $this->index_of; + $GENPOLY =& $this->genpoly; + $NROOTS =& $this->nroots; + $FCR =& $this->fcr; + $PRIM =& $this->prim; + $IPRIM =& $this->iprim; + $PAD =& $this->pad; + $A0 =& $NN; + + $parity = array_fill(0, $NROOTS, 0); + + for($i=0; $i< ($NN-$NROOTS-$PAD); $i++) { + + $feedback = $INDEX_OF[$data[$i] ^ $parity[0]]; + if($feedback != $A0) { + // feedback term is non-zero + + // This line is unnecessary when GENPOLY[NROOTS] is unity, as it must + // always be for the polynomials constructed by init_rs() + $feedback = $this->modnn($NN - $GENPOLY[$NROOTS] + $feedback); + + for($j=1;$j<$NROOTS;$j++) { + $parity[$j] ^= $ALPHA_TO[$this->modnn($feedback + $GENPOLY[$NROOTS-$j])]; + } + } + + // Shift + array_shift($parity); + if($feedback != $A0) { + array_push($parity, $ALPHA_TO[$this->modnn($feedback + $GENPOLY[0])]); + } else { + array_push($parity, 0); + } + } + } + } + + //########################################################################## + + class QRrs { + + public static $items = array(); + + //---------------------------------------------------------------------- + public static function init_rs($symsize, $gfpoly, $fcr, $prim, $nroots, $pad) + { + foreach(self::$items as $rs) { + if($rs->pad != $pad) continue; + if($rs->nroots != $nroots) continue; + if($rs->mm != $symsize) continue; + if($rs->gfpoly != $gfpoly) continue; + if($rs->fcr != $fcr) continue; + if($rs->prim != $prim) continue; + + return $rs; + } + + $rs = QRrsItem::init_rs_char($symsize, $gfpoly, $fcr, $prim, $nroots, $pad); + array_unshift(self::$items, $rs); + + return $rs; + } + } + + + +//---- qrmask.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Masking + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('N1', 3); + define('N2', 3); + define('N3', 40); + define('N4', 10); + + class QRmask { + + public $runLength = array(); + + //---------------------------------------------------------------------- + public function __construct() + { + $this->runLength = array_fill(0, QRSPEC_WIDTH_MAX + 1, 0); + } + + //---------------------------------------------------------------------- + public function writeFormatInformation($width, &$frame, $mask, $level) + { + $blacks = 0; + $format = QRspec::getFormatInfo($mask, $level); + + for($i=0; $i<8; $i++) { + if($format & 1) { + $blacks += 2; + $v = 0x85; + } else { + $v = 0x84; + } + + $frame[8][$width - 1 - $i] = chr($v); + if($i < 6) { + $frame[$i][8] = chr($v); + } else { + $frame[$i + 1][8] = chr($v); + } + $format = $format >> 1; + } + + for($i=0; $i<7; $i++) { + if($format & 1) { + $blacks += 2; + $v = 0x85; + } else { + $v = 0x84; + } + + $frame[$width - 7 + $i][8] = chr($v); + if($i == 0) { + $frame[8][7] = chr($v); + } else { + $frame[8][6 - $i] = chr($v); + } + + $format = $format >> 1; + } + + return $blacks; + } + + //---------------------------------------------------------------------- + public function mask0($x, $y) { return ($x+$y)&1; } + public function mask1($x, $y) { return ($y&1); } + public function mask2($x, $y) { return ($x%3); } + public function mask3($x, $y) { return ($x+$y)%3; } + public function mask4($x, $y) { return (((int)($y/2))+((int)($x/3)))&1; } + public function mask5($x, $y) { return (($x*$y)&1)+($x*$y)%3; } + public function mask6($x, $y) { return ((($x*$y)&1)+($x*$y)%3)&1; } + public function mask7($x, $y) { return ((($x*$y)%3)+(($x+$y)&1))&1; } + + //---------------------------------------------------------------------- + private function generateMaskNo($maskNo, $width, $frame) + { + $bitMask = array_fill(0, $width, array_fill(0, $width, 0)); + + for($y=0; $y<$width; $y++) { + for($x=0; $x<$width; $x++) { + if(ord($frame[$y][$x]) & 0x80) { + $bitMask[$y][$x] = 0; + } else { + $maskFunc = call_user_func(array($this, 'mask'.$maskNo), $x, $y); + $bitMask[$y][$x] = ($maskFunc == 0)?1:0; + } + + } + } + + return $bitMask; + } + + //---------------------------------------------------------------------- + public static function serial($bitFrame) + { + $codeArr = array(); + + foreach ($bitFrame as $line) + $codeArr[] = join('', $line); + + return gzcompress(join("\n", $codeArr), 9); + } + + //---------------------------------------------------------------------- + public static function unserial($code) + { + $codeArr = array(); + + $codeLines = explode("\n", gzuncompress($code)); + foreach ($codeLines as $line) + $codeArr[] = str_split($line); + + return $codeArr; + } + + //---------------------------------------------------------------------- + public function makeMaskNo($maskNo, $width, $s, &$d, $maskGenOnly = false) + { + $b = 0; + $bitMask = array(); + + $fileName = QR_CACHE_DIR.'mask_'.$maskNo.DIRECTORY_SEPARATOR.'mask_'.$width.'_'.$maskNo.'.dat'; + + if (QR_CACHEABLE) { + if (file_exists($fileName)) { + $bitMask = self::unserial(file_get_contents($fileName)); + } else { + $bitMask = $this->generateMaskNo($maskNo, $width, $s, $d); + if (!file_exists(QR_CACHE_DIR.'mask_'.$maskNo)) + mkdir(QR_CACHE_DIR.'mask_'.$maskNo); + file_put_contents($fileName, self::serial($bitMask)); + } + } else { + $bitMask = $this->generateMaskNo($maskNo, $width, $s, $d); + } + + if ($maskGenOnly) + return; + + $d = $s; + + for($y=0; $y<$width; $y++) { + for($x=0; $x<$width; $x++) { + if($bitMask[$y][$x] == 1) { + $d[$y][$x] = chr(ord($s[$y][$x]) ^ (int)$bitMask[$y][$x]); + } + $b += (int)(ord($d[$y][$x]) & 1); + } + } + + return $b; + } + + //---------------------------------------------------------------------- + public function makeMask($width, $frame, $maskNo, $level) + { + $masked = array_fill(0, $width, str_repeat("\0", $width)); + $this->makeMaskNo($maskNo, $width, $frame, $masked); + $this->writeFormatInformation($width, $masked, $maskNo, $level); + + return $masked; + } + + //---------------------------------------------------------------------- + public function calcN1N3($length) + { + $demerit = 0; + + for($i=0; $i<$length; $i++) { + + if($this->runLength[$i] >= 5) { + $demerit += (N1 + ($this->runLength[$i] - 5)); + } + if($i & 1) { + if(($i >= 3) && ($i < ($length-2)) && ($this->runLength[$i] % 3 == 0)) { + $fact = (int)($this->runLength[$i] / 3); + if(($this->runLength[$i-2] == $fact) && + ($this->runLength[$i-1] == $fact) && + ($this->runLength[$i+1] == $fact) && + ($this->runLength[$i+2] == $fact)) { + if(($this->runLength[$i-3] < 0) || ($this->runLength[$i-3] >= (4 * $fact))) { + $demerit += N3; + } else if((($i+3) >= $length) || ($this->runLength[$i+3] >= (4 * $fact))) { + $demerit += N3; + } + } + } + } + } + return $demerit; + } + + //---------------------------------------------------------------------- + public function evaluateSymbol($width, $frame) + { + $head = 0; + $demerit = 0; + + for($y=0; $y<$width; $y++) { + $head = 0; + $this->runLength[0] = 1; + + $frameY = $frame[$y]; + + if ($y>0) + $frameYM = $frame[$y-1]; + + for($x=0; $x<$width; $x++) { + if(($x > 0) && ($y > 0)) { + $b22 = ord($frameY[$x]) & ord($frameY[$x-1]) & ord($frameYM[$x]) & ord($frameYM[$x-1]); + $w22 = ord($frameY[$x]) | ord($frameY[$x-1]) | ord($frameYM[$x]) | ord($frameYM[$x-1]); + + if(($b22 | ($w22 ^ 1))&1) { + $demerit += N2; + } + } + if(($x == 0) && (ord($frameY[$x]) & 1)) { + $this->runLength[0] = -1; + $head = 1; + $this->runLength[$head] = 1; + } else if($x > 0) { + if((ord($frameY[$x]) ^ ord($frameY[$x-1])) & 1) { + $head++; + $this->runLength[$head] = 1; + } else { + $this->runLength[$head]++; + } + } + } + + $demerit += $this->calcN1N3($head+1); + } + + for($x=0; $x<$width; $x++) { + $head = 0; + $this->runLength[0] = 1; + + for($y=0; $y<$width; $y++) { + if($y == 0 && (ord($frame[$y][$x]) & 1)) { + $this->runLength[0] = -1; + $head = 1; + $this->runLength[$head] = 1; + } else if($y > 0) { + if((ord($frame[$y][$x]) ^ ord($frame[$y-1][$x])) & 1) { + $head++; + $this->runLength[$head] = 1; + } else { + $this->runLength[$head]++; + } + } + } + + $demerit += $this->calcN1N3($head+1); + } + + return $demerit; + } + + + //---------------------------------------------------------------------- + public function mask($width, $frame, $level) + { + $minDemerit = PHP_INT_MAX; + $bestMaskNum = 0; + $bestMask = array(); + + $checked_masks = array(0,1,2,3,4,5,6,7); + + if (QR_FIND_FROM_RANDOM !== false) { + + $howManuOut = 8-(QR_FIND_FROM_RANDOM % 9); + for ($i = 0; $i < $howManuOut; $i++) { + $remPos = rand (0, count($checked_masks)-1); + unset($checked_masks[$remPos]); + $checked_masks = array_values($checked_masks); + } + + } + + $bestMask = $frame; + + foreach($checked_masks as $i) { + $mask = array_fill(0, $width, str_repeat("\0", $width)); + + $demerit = 0; + $blacks = 0; + $blacks = $this->makeMaskNo($i, $width, $frame, $mask); + $blacks += $this->writeFormatInformation($width, $mask, $i, $level); + $blacks = (int)(100 * $blacks / ($width * $width)); + $demerit = (int)((int)(abs($blacks - 50) / 5) * N4); + $demerit += $this->evaluateSymbol($width, $mask); + + if($demerit < $minDemerit) { + $minDemerit = $demerit; + $bestMask = $mask; + $bestMaskNum = $i; + } + } + + return $bestMask; + } + + //---------------------------------------------------------------------- + } + + + + +//---- qrencode.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Main encoder classes. + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRrsblock { + public $dataLength; + public $data = array(); + public $eccLength; + public $ecc = array(); + + public function __construct($dl, $data, $el, &$ecc, QRrsItem $rs) + { + $rs->encode_rs_char($data, $ecc); + + $this->dataLength = $dl; + $this->data = $data; + $this->eccLength = $el; + $this->ecc = $ecc; + } + }; + + //########################################################################## + + class QRrawcode { + public $version; + public $datacode = array(); + public $ecccode = array(); + public $blocks; + public $rsblocks = array(); //of RSblock + public $count; + public $dataLength; + public $eccLength; + public $b1; + + //---------------------------------------------------------------------- + public function __construct(QRinput $input) + { + $spec = array(0,0,0,0,0); + + $this->datacode = $input->getByteStream(); + if(is_null($this->datacode)) { + throw new Exception('null imput string'); + } + + QRspec::getEccSpec($input->getVersion(), $input->getErrorCorrectionLevel(), $spec); + + $this->version = $input->getVersion(); + $this->b1 = QRspec::rsBlockNum1($spec); + $this->dataLength = QRspec::rsDataLength($spec); + $this->eccLength = QRspec::rsEccLength($spec); + $this->ecccode = array_fill(0, $this->eccLength, 0); + $this->blocks = QRspec::rsBlockNum($spec); + + $ret = $this->init($spec); + if($ret < 0) { + throw new Exception('block alloc error'); + return null; + } + + $this->count = 0; + } + + //---------------------------------------------------------------------- + public function init(array $spec) + { + $dl = QRspec::rsDataCodes1($spec); + $el = QRspec::rsEccCodes1($spec); + $rs = QRrs::init_rs(8, 0x11d, 0, 1, $el, 255 - $dl - $el); + + + $blockNo = 0; + $dataPos = 0; + $eccPos = 0; + for($i=0; $iecccode,$eccPos); + $this->rsblocks[$blockNo] = new QRrsblock($dl, array_slice($this->datacode, $dataPos), $el, $ecc, $rs); + $this->ecccode = array_merge(array_slice($this->ecccode,0, $eccPos), $ecc); + + $dataPos += $dl; + $eccPos += $el; + $blockNo++; + } + + if(QRspec::rsBlockNum2($spec) == 0) + return 0; + + $dl = QRspec::rsDataCodes2($spec); + $el = QRspec::rsEccCodes2($spec); + $rs = QRrs::init_rs(8, 0x11d, 0, 1, $el, 255 - $dl - $el); + + if($rs == NULL) return -1; + + for($i=0; $iecccode,$eccPos); + $this->rsblocks[$blockNo] = new QRrsblock($dl, array_slice($this->datacode, $dataPos), $el, $ecc, $rs); + $this->ecccode = array_merge(array_slice($this->ecccode,0, $eccPos), $ecc); + + $dataPos += $dl; + $eccPos += $el; + $blockNo++; + } + + return 0; + } + + //---------------------------------------------------------------------- + public function getCode() + { + $ret; + + if($this->count < $this->dataLength) { + $row = $this->count % $this->blocks; + $col = $this->count / $this->blocks; + if($col >= $this->rsblocks[0]->dataLength) { + $row += $this->b1; + } + $ret = $this->rsblocks[$row]->data[$col]; + } else if($this->count < $this->dataLength + $this->eccLength) { + $row = ($this->count - $this->dataLength) % $this->blocks; + $col = ($this->count - $this->dataLength) / $this->blocks; + $ret = $this->rsblocks[$row]->ecc[$col]; + } else { + return 0; + } + $this->count++; + + return $ret; + } + } + + //########################################################################## + + class QRcode { + + public $version; + public $width; + public $data; + + //---------------------------------------------------------------------- + public function encodeMask(QRinput $input, $mask) + { + if($input->getVersion() < 0 || $input->getVersion() > QRSPEC_VERSION_MAX) { + throw new Exception('wrong version'); + } + if($input->getErrorCorrectionLevel() > QR_ECLEVEL_H) { + throw new Exception('wrong level'); + } + + $raw = new QRrawcode($input); + + QRtools::markTime('after_raw'); + + $version = $raw->version; + $width = QRspec::getWidth($version); + $frame = QRspec::newFrame($version); + + $filler = new FrameFiller($width, $frame); + if(is_null($filler)) { + return NULL; + } + + // inteleaved data and ecc codes + for($i=0; $i<$raw->dataLength + $raw->eccLength; $i++) { + $code = $raw->getCode(); + $bit = 0x80; + for($j=0; $j<8; $j++) { + $addr = $filler->next(); + $filler->setFrameAt($addr, 0x02 | (($bit & $code) != 0)); + $bit = $bit >> 1; + } + } + + QRtools::markTime('after_filler'); + + unset($raw); + + // remainder bits + $j = QRspec::getRemainder($version); + for($i=0; $i<$j; $i++) { + $addr = $filler->next(); + $filler->setFrameAt($addr, 0x02); + } + + $frame = $filler->frame; + unset($filler); + + + // masking + $maskObj = new QRmask(); + if($mask < 0) { + + if (QR_FIND_BEST_MASK) { + $masked = $maskObj->mask($width, $frame, $input->getErrorCorrectionLevel()); + } else { + $masked = $maskObj->makeMask($width, $frame, (intval(QR_DEFAULT_MASK) % 8), $input->getErrorCorrectionLevel()); + } + } else { + $masked = $maskObj->makeMask($width, $frame, $mask, $input->getErrorCorrectionLevel()); + } + + if($masked == NULL) { + return NULL; + } + + QRtools::markTime('after_mask'); + + $this->version = $version; + $this->width = $width; + $this->data = $masked; + + return $this; + } + + //---------------------------------------------------------------------- + public function encodeInput(QRinput $input) + { + return $this->encodeMask($input, -1); + } + + //---------------------------------------------------------------------- + public function encodeString8bit($string, $version, $level) + { + if(string == NULL) { + throw new Exception('empty string!'); + return NULL; + } + + $input = new QRinput($version, $level); + if($input == NULL) return NULL; + + $ret = $input->append($input, QR_MODE_8, strlen($string), str_split($string)); + if($ret < 0) { + unset($input); + return NULL; + } + return $this->encodeInput($input); + } + + //---------------------------------------------------------------------- + public function encodeString($string, $version, $level, $hint, $casesensitive) + { + + if($hint != QR_MODE_8 && $hint != QR_MODE_KANJI) { + throw new Exception('bad hint'); + return NULL; + } + + $input = new QRinput($version, $level); + if($input == NULL) return NULL; + + $ret = QRsplit::splitStringToQRinput($string, $input, $hint, $casesensitive); + if($ret < 0) { + return NULL; + } + + return $this->encodeInput($input); + } + + //---------------------------------------------------------------------- + public static function png($text, $outfile = false, $level = QR_ECLEVEL_L, $size = 3, $margin = 4, $saveandprint=false) + { + $enc = QRencode::factory($level, $size, $margin); + return $enc->encodePNG($text, $outfile, $saveandprint=false); + } + + //---------------------------------------------------------------------- + public static function text($text, $outfile = false, $level = QR_ECLEVEL_L, $size = 3, $margin = 4) + { + $enc = QRencode::factory($level, $size, $margin); + return $enc->encode($text, $outfile); + } + + //---------------------------------------------------------------------- + public static function raw($text, $outfile = false, $level = QR_ECLEVEL_L, $size = 3, $margin = 4) + { + $enc = QRencode::factory($level, $size, $margin); + return $enc->encodeRAW($text, $outfile); + } + } + + //########################################################################## + + class FrameFiller { + + public $width; + public $frame; + public $x; + public $y; + public $dir; + public $bit; + + //---------------------------------------------------------------------- + public function __construct($width, &$frame) + { + $this->width = $width; + $this->frame = $frame; + $this->x = $width - 1; + $this->y = $width - 1; + $this->dir = -1; + $this->bit = -1; + } + + //---------------------------------------------------------------------- + public function setFrameAt($at, $val) + { + $this->frame[$at['y']][$at['x']] = chr($val); + } + + //---------------------------------------------------------------------- + public function getFrameAt($at) + { + return ord($this->frame[$at['y']][$at['x']]); + } + + //---------------------------------------------------------------------- + public function next() + { + do { + + if($this->bit == -1) { + $this->bit = 0; + return array('x'=>$this->x, 'y'=>$this->y); + } + + $x = $this->x; + $y = $this->y; + $w = $this->width; + + if($this->bit == 0) { + $x--; + $this->bit++; + } else { + $x++; + $y += $this->dir; + $this->bit--; + } + + if($this->dir < 0) { + if($y < 0) { + $y = 0; + $x -= 2; + $this->dir = 1; + if($x == 6) { + $x--; + $y = 9; + } + } + } else { + if($y == $w) { + $y = $w - 1; + $x -= 2; + $this->dir = -1; + if($x == 6) { + $x--; + $y -= 8; + } + } + } + if($x < 0 || $y < 0) return null; + + $this->x = $x; + $this->y = $y; + + } while(ord($this->frame[$y][$x]) & 0x80); + + return array('x'=>$x, 'y'=>$y); + } + + } ; + + //########################################################################## + + class QRencode { + + public $casesensitive = true; + public $eightbit = false; + + public $version = 0; + public $size = 3; + public $margin = 4; + + public $structured = 0; // not supported yet + + public $level = QR_ECLEVEL_L; + public $hint = QR_MODE_8; + + //---------------------------------------------------------------------- + public static function factory($level = QR_ECLEVEL_L, $size = 3, $margin = 4) + { + $enc = new QRencode(); + $enc->size = $size; + $enc->margin = $margin; + + switch ($level.'') { + case '0': + case '1': + case '2': + case '3': + $enc->level = $level; + break; + case 'l': + case 'L': + $enc->level = QR_ECLEVEL_L; + break; + case 'm': + case 'M': + $enc->level = QR_ECLEVEL_M; + break; + case 'q': + case 'Q': + $enc->level = QR_ECLEVEL_Q; + break; + case 'h': + case 'H': + $enc->level = QR_ECLEVEL_H; + break; + } + + return $enc; + } + + //---------------------------------------------------------------------- + public function encodeRAW($intext, $outfile = false) + { + $code = new QRcode(); + + if($this->eightbit) { + $code->encodeString8bit($intext, $this->version, $this->level); + } else { + $code->encodeString($intext, $this->version, $this->level, $this->hint, $this->casesensitive); + } + + return $code->data; + } + + //---------------------------------------------------------------------- + public function encode($intext, $outfile = false) + { + $code = new QRcode(); + + if($this->eightbit) { + $code->encodeString8bit($intext, $this->version, $this->level); + } else { + $code->encodeString($intext, $this->version, $this->level, $this->hint, $this->casesensitive); + } + + QRtools::markTime('after_encode'); + + if ($outfile!== false) { + file_put_contents($outfile, join("\n", QRtools::binarize($code->data))); + } else { + return QRtools::binarize($code->data); + } + } + + //---------------------------------------------------------------------- + public function encodePNG($intext, $outfile = false,$saveandprint=false) + { + try { + + ob_start(); + $tab = $this->encode($intext); + $err = ob_get_contents(); + ob_end_clean(); + + if ($err != '') + QRtools::log($outfile, $err); + + $maxSize = (int)(QR_PNG_MAXIMUM_SIZE / (count($tab)+2*$this->margin)); + + QRimage::png($tab, $outfile, min(max(1, $this->size), $maxSize), $this->margin,$saveandprint); + + } catch (Exception $e) { + + QRtools::log($outfile, $e->getMessage()); + + } + } + } + + diff --git a/api/stats.php b/api/stats.php new file mode 100644 index 0000000..48859a1 --- /dev/null +++ b/api/stats.php @@ -0,0 +1,13 @@ +uid) { + die(); +} + +if($json->uid) { + file_put_contents("stats/".($json->uid).".log", "\n".$data."\n", FILE_APPEND); +} + +echo "OK"; +?> \ No newline at end of file diff --git a/api/updater.php b/api/updater.php new file mode 100644 index 0000000..01a411d --- /dev/null +++ b/api/updater.php @@ -0,0 +1,96 @@ +platform; + $version = $data->version; // currently not used +} + +if($platform == "WIN32-WGL") { // opengl + $binary_path = "/otclient_gl.exe"; + $checksums_file = "checksums_gl.txt"; +} else if($platform == "WIN32-EGL") { // dx + $binary_path = "/otclient_dx.exe"; + $checksums_file = "checksums_dx.txt"; +} else { + $binary_path = ""; + $checksums_file = "checksums.txt"; +} + +$data_dir = "/var/www/otclient/files"; +$things_dir = "/data/things"; // files from that dir won't be downloaded automaticly, you can set it to null to download everything automaticly (useful if you have only 1 version of data/sprites) +$files_url = "http://otclient.ovh/files"; +$update_checksum_interval = 60; // caling updater 100x/s would lag disc, we need to cache it +$main_files_and_dirs = array("data", "modules", "init.lua"); // used to ignore other files/dirs in data_dir + +// CONFIG END + +$data = array("url" => $files_url, "files" => array(), "things" => array(), "binary" => $binary_path); + +function getDirFiles($dir, &$results = array()){ + $files = scandir($dir); + + foreach($files as $key => $value){ + $path = realpath($dir.DIRECTORY_SEPARATOR.$value); + if(!is_dir($path)) { + $results[] = $path; + } else if($value != "." && $value != "..") { + getDirFiles($path, $results); + } + } + return $results; +} + +function updateChecksums() { + global $data_dir; + global $main_files_and_dirs; + global $binary_path; + global $checksums_file; + global $data; + global $things_dir; + + $ret = array(); + $data_dir_realpath = realpath($data_dir); + $files = getDirFiles($data_dir); + foreach($files as $file) { + $relative_path = str_replace($data_dir_realpath, "", $file); + $ps = explode("/", $relative_path); + if($relative_path == $binary_path || (count($ps) >= 2 && in_array($ps[1], $main_files_and_dirs))) + $ret[$relative_path] = md5_file($file); + } + foreach($ret as $file => $checksum) { + if($things_dir != null && !empty($things_dir) && strpos($file, $things_dir) === 0) { + $data["things"][$file] = $checksum; + } else { + $data["files"][$file] = $checksum; + } + } + $ret = json_encode($data); + if(file_put_contents($checksums_file, $ret) === FALSE) { + echo "Can't create checksum file (try to set correct chmod) ". - $checksums_file; + exit(); + } + return $ret; +} + +if (function_exists('sem_get')) { + $semaphore = sem_get(18237192837, 1, 0666, 1); + if(!$semaphore) + { + echo "Failed to get semaphore - sem_get().\n"; + exit(); + } + + sem_acquire($semaphore); +} +$ft = filemtime($checksums_file); +if($ft === false || $ft + $update_checksum_interval < time()) { + echo updateChecksums(); +} else { + echo file_get_contents($checksums_file); +} +if (function_exists('sem_get')) { + sem_release($semaphore); +} +?> \ No newline at end of file diff --git a/d3dcompiler_46.dll b/d3dcompiler_46.dll new file mode 100644 index 0000000..c706328 Binary files /dev/null and b/d3dcompiler_46.dll differ diff --git a/data/cursors/cursors.otml b/data/cursors/cursors.otml new file mode 100644 index 0000000..36c1f39 --- /dev/null +++ b/data/cursors/cursors.otml @@ -0,0 +1,13 @@ +Cursors + target: + image: targetcursor + hot-spot: 9 9 + horizontal: + image: horizontalcursor + hot-spot: 9 4 + vertical: + image: verticalcursor + hot-spot: 4 9 + text: + image: textcursor + hot-spot: 4 9 diff --git a/data/cursors/horizontalcursor.png b/data/cursors/horizontalcursor.png new file mode 100644 index 0000000..6ba0f27 Binary files /dev/null and b/data/cursors/horizontalcursor.png differ diff --git a/data/cursors/targetcursor.png b/data/cursors/targetcursor.png new file mode 100644 index 0000000..3ce607b Binary files /dev/null and b/data/cursors/targetcursor.png differ diff --git a/data/cursors/textcursor.png b/data/cursors/textcursor.png new file mode 100644 index 0000000..abcd823 Binary files /dev/null and b/data/cursors/textcursor.png differ diff --git a/data/cursors/verticalcursor.png b/data/cursors/verticalcursor.png new file mode 100644 index 0000000..1f31d1c Binary files /dev/null and b/data/cursors/verticalcursor.png differ diff --git a/data/fonts/cipsoftFont.otfont b/data/fonts/cipsoftFont.otfont new file mode 100644 index 0000000..1e9e2fe --- /dev/null +++ b/data/fonts/cipsoftFont.otfont @@ -0,0 +1,7 @@ +Font + name: cipsoftFont + texture: cipsoftFont + height: 8 + glyph-size: 8 8 + space-width: 2 + default: true diff --git a/data/fonts/cipsoftFont.png b/data/fonts/cipsoftFont.png new file mode 100644 index 0000000..65efe25 Binary files /dev/null and b/data/fonts/cipsoftFont.png differ diff --git a/data/fonts/sans-bold-16px.otfont b/data/fonts/sans-bold-16px.otfont new file mode 100644 index 0000000..bdf5cf4 --- /dev/null +++ b/data/fonts/sans-bold-16px.otfont @@ -0,0 +1,6 @@ +Font + name: sans-bold-16px + texture: sans-bold-16px_cp1252 + height: 20 + glyph-size: 24 24 + space-width: 3 diff --git a/data/fonts/sans-bold-16px_cp1252.png b/data/fonts/sans-bold-16px_cp1252.png new file mode 100644 index 0000000..eef9474 Binary files /dev/null and b/data/fonts/sans-bold-16px_cp1252.png differ diff --git a/data/fonts/terminus-10px.otfont b/data/fonts/terminus-10px.otfont new file mode 100644 index 0000000..957da0d --- /dev/null +++ b/data/fonts/terminus-10px.otfont @@ -0,0 +1,8 @@ +Font + name: terminus-10px + texture: terminus-10px + height: 12 + y-offset: 0 + glyph-size: 16 16 + fixed-glyph-width: 6 + space-width: 6 diff --git a/data/fonts/terminus-10px.png b/data/fonts/terminus-10px.png new file mode 100644 index 0000000..4e8f500 Binary files /dev/null and b/data/fonts/terminus-10px.png differ diff --git a/data/fonts/terminus-14px-bold.otfont b/data/fonts/terminus-14px-bold.otfont new file mode 100644 index 0000000..0814f4a --- /dev/null +++ b/data/fonts/terminus-14px-bold.otfont @@ -0,0 +1,8 @@ +Font + name: terminus-14px-bold + texture: terminus-14px-bold + height: 16 + y-offset: 2 + glyph-size: 16 16 + fixed-glyph-width: 8 + space-width: 8 diff --git a/data/fonts/terminus-14px-bold.png b/data/fonts/terminus-14px-bold.png new file mode 100644 index 0000000..99962a5 Binary files /dev/null and b/data/fonts/terminus-14px-bold.png differ diff --git a/data/fonts/verdana-11px-antialised.otfont b/data/fonts/verdana-11px-antialised.otfont new file mode 100644 index 0000000..c6cac89 --- /dev/null +++ b/data/fonts/verdana-11px-antialised.otfont @@ -0,0 +1,7 @@ +Font + name: verdana-11px-antialised + texture: verdana-11px-antialised_cp1252 + height: 14 + glyph-size: 16 16 + space-width: 4 + default: true diff --git a/data/fonts/verdana-11px-antialised_cp1250.png b/data/fonts/verdana-11px-antialised_cp1250.png new file mode 100644 index 0000000..ba9e6cb Binary files /dev/null and b/data/fonts/verdana-11px-antialised_cp1250.png differ diff --git a/data/fonts/verdana-11px-antialised_cp1252.png b/data/fonts/verdana-11px-antialised_cp1252.png new file mode 100644 index 0000000..c01e218 Binary files /dev/null and b/data/fonts/verdana-11px-antialised_cp1252.png differ diff --git a/data/fonts/verdana-11px-monochrome.otfont b/data/fonts/verdana-11px-monochrome.otfont new file mode 100644 index 0000000..e39cf0b --- /dev/null +++ b/data/fonts/verdana-11px-monochrome.otfont @@ -0,0 +1,6 @@ +Font + name: verdana-11px-monochrome + texture: verdana-11px-monochrome_cp1252 + height: 14 + glyph-size: 16 16 + space-width: 3 diff --git a/data/fonts/verdana-11px-monochrome_cp1250.png b/data/fonts/verdana-11px-monochrome_cp1250.png new file mode 100644 index 0000000..11002ef Binary files /dev/null and b/data/fonts/verdana-11px-monochrome_cp1250.png differ diff --git a/data/fonts/verdana-11px-monochrome_cp1252.png b/data/fonts/verdana-11px-monochrome_cp1252.png new file mode 100644 index 0000000..d053ca5 Binary files /dev/null and b/data/fonts/verdana-11px-monochrome_cp1252.png differ diff --git a/data/fonts/verdana-11px-rounded.otfont b/data/fonts/verdana-11px-rounded.otfont new file mode 100644 index 0000000..9c18e23 --- /dev/null +++ b/data/fonts/verdana-11px-rounded.otfont @@ -0,0 +1,8 @@ +Font + name: verdana-11px-rounded + texture: verdana-11px-rounded_cp1252 + height: 16 + glyph-size: 16 16 + y-offset: -2 + spacing: -1 -3 + space-width: 4 diff --git a/data/fonts/verdana-11px-rounded_cp1250.png b/data/fonts/verdana-11px-rounded_cp1250.png new file mode 100644 index 0000000..bcf4638 Binary files /dev/null and b/data/fonts/verdana-11px-rounded_cp1250.png differ diff --git a/data/fonts/verdana-11px-rounded_cp1252.png b/data/fonts/verdana-11px-rounded_cp1252.png new file mode 100644 index 0000000..0ff0322 Binary files /dev/null and b/data/fonts/verdana-11px-rounded_cp1252.png differ diff --git a/data/images/background.png b/data/images/background.png new file mode 100644 index 0000000..042269e Binary files /dev/null and b/data/images/background.png differ diff --git a/data/images/clienticon.png b/data/images/clienticon.png new file mode 100644 index 0000000..4200fb5 Binary files /dev/null and b/data/images/clienticon.png differ diff --git a/data/images/crosshair/default.png b/data/images/crosshair/default.png new file mode 100644 index 0000000..1715d95 Binary files /dev/null and b/data/images/crosshair/default.png differ diff --git a/data/images/crosshair/full.png b/data/images/crosshair/full.png new file mode 100644 index 0000000..b5c2173 Binary files /dev/null and b/data/images/crosshair/full.png differ diff --git a/data/images/flags/de.png b/data/images/flags/de.png new file mode 100644 index 0000000..103b04d Binary files /dev/null and b/data/images/flags/de.png differ diff --git a/data/images/flags/en.png b/data/images/flags/en.png new file mode 100644 index 0000000..f36a688 Binary files /dev/null and b/data/images/flags/en.png differ diff --git a/data/images/flags/es.png b/data/images/flags/es.png new file mode 100644 index 0000000..fda8ccc Binary files /dev/null and b/data/images/flags/es.png differ diff --git a/data/images/flags/pl.png b/data/images/flags/pl.png new file mode 100644 index 0000000..52795d0 Binary files /dev/null and b/data/images/flags/pl.png differ diff --git a/data/images/flags/pt.png b/data/images/flags/pt.png new file mode 100644 index 0000000..b0c7a22 Binary files /dev/null and b/data/images/flags/pt.png differ diff --git a/data/images/flags/sv.png b/data/images/flags/sv.png new file mode 100644 index 0000000..60932e2 Binary files /dev/null and b/data/images/flags/sv.png differ diff --git a/data/images/game/battle/battle_monsters.png b/data/images/game/battle/battle_monsters.png new file mode 100644 index 0000000..9c01361 Binary files /dev/null and b/data/images/game/battle/battle_monsters.png differ diff --git a/data/images/game/battle/battle_npcs.png b/data/images/game/battle/battle_npcs.png new file mode 100644 index 0000000..d19635d Binary files /dev/null and b/data/images/game/battle/battle_npcs.png differ diff --git a/data/images/game/battle/battle_party.png b/data/images/game/battle/battle_party.png new file mode 100644 index 0000000..77fd67e Binary files /dev/null and b/data/images/game/battle/battle_party.png differ diff --git a/data/images/game/battle/battle_players.png b/data/images/game/battle/battle_players.png new file mode 100644 index 0000000..84c74b9 Binary files /dev/null and b/data/images/game/battle/battle_players.png differ diff --git a/data/images/game/battle/battle_skulls.png b/data/images/game/battle/battle_skulls.png new file mode 100644 index 0000000..3f3a2ec Binary files /dev/null and b/data/images/game/battle/battle_skulls.png differ diff --git a/data/images/game/circle/left_empty.png b/data/images/game/circle/left_empty.png new file mode 100644 index 0000000..7868b9f Binary files /dev/null and b/data/images/game/circle/left_empty.png differ diff --git a/data/images/game/circle/left_full.png b/data/images/game/circle/left_full.png new file mode 100644 index 0000000..dd9ecf4 Binary files /dev/null and b/data/images/game/circle/left_full.png differ diff --git a/data/images/game/circle/right_empty.png b/data/images/game/circle/right_empty.png new file mode 100644 index 0000000..a20de6a Binary files /dev/null and b/data/images/game/circle/right_empty.png differ diff --git a/data/images/game/circle/right_full.png b/data/images/game/circle/right_full.png new file mode 100644 index 0000000..8d866d2 Binary files /dev/null and b/data/images/game/circle/right_full.png differ diff --git a/data/images/game/combatmodes/chasemode.png b/data/images/game/combatmodes/chasemode.png new file mode 100644 index 0000000..f3ef705 Binary files /dev/null and b/data/images/game/combatmodes/chasemode.png differ diff --git a/data/images/game/combatmodes/fightbalanced.png b/data/images/game/combatmodes/fightbalanced.png new file mode 100644 index 0000000..3113538 Binary files /dev/null and b/data/images/game/combatmodes/fightbalanced.png differ diff --git a/data/images/game/combatmodes/fightdefensive.png b/data/images/game/combatmodes/fightdefensive.png new file mode 100644 index 0000000..3829a21 Binary files /dev/null and b/data/images/game/combatmodes/fightdefensive.png differ diff --git a/data/images/game/combatmodes/fightoffensive.png b/data/images/game/combatmodes/fightoffensive.png new file mode 100644 index 0000000..2fb6e79 Binary files /dev/null and b/data/images/game/combatmodes/fightoffensive.png differ diff --git a/data/images/game/combatmodes/mount.png b/data/images/game/combatmodes/mount.png new file mode 100644 index 0000000..879646a Binary files /dev/null and b/data/images/game/combatmodes/mount.png differ diff --git a/data/images/game/combatmodes/redfistmode.png b/data/images/game/combatmodes/redfistmode.png new file mode 100644 index 0000000..e197210 Binary files /dev/null and b/data/images/game/combatmodes/redfistmode.png differ diff --git a/data/images/game/combatmodes/safefight.png b/data/images/game/combatmodes/safefight.png new file mode 100644 index 0000000..2117067 Binary files /dev/null and b/data/images/game/combatmodes/safefight.png differ diff --git a/data/images/game/combatmodes/whitedovemode.png b/data/images/game/combatmodes/whitedovemode.png new file mode 100644 index 0000000..29d4e0c Binary files /dev/null and b/data/images/game/combatmodes/whitedovemode.png differ diff --git a/data/images/game/combatmodes/whitehandmode.png b/data/images/game/combatmodes/whitehandmode.png new file mode 100644 index 0000000..c010d82 Binary files /dev/null and b/data/images/game/combatmodes/whitehandmode.png differ diff --git a/data/images/game/combatmodes/yellowhandmode.png b/data/images/game/combatmodes/yellowhandmode.png new file mode 100644 index 0000000..13a0b02 Binary files /dev/null and b/data/images/game/combatmodes/yellowhandmode.png differ diff --git a/data/images/game/console/channels.png b/data/images/game/console/channels.png new file mode 100644 index 0000000..885ba22 Binary files /dev/null and b/data/images/game/console/channels.png differ diff --git a/data/images/game/console/clearchannel.png b/data/images/game/console/clearchannel.png new file mode 100644 index 0000000..201bd82 Binary files /dev/null and b/data/images/game/console/clearchannel.png differ diff --git a/data/images/game/console/closechannel.png b/data/images/game/console/closechannel.png new file mode 100644 index 0000000..0b130d4 Binary files /dev/null and b/data/images/game/console/closechannel.png differ diff --git a/data/images/game/console/ignore.png b/data/images/game/console/ignore.png new file mode 100644 index 0000000..8ed2df6 Binary files /dev/null and b/data/images/game/console/ignore.png differ diff --git a/data/images/game/console/leftarrow.png b/data/images/game/console/leftarrow.png new file mode 100644 index 0000000..7e065f5 Binary files /dev/null and b/data/images/game/console/leftarrow.png differ diff --git a/data/images/game/console/rightarrow.png b/data/images/game/console/rightarrow.png new file mode 100644 index 0000000..4c51e9f Binary files /dev/null and b/data/images/game/console/rightarrow.png differ diff --git a/data/images/game/console/say.png b/data/images/game/console/say.png new file mode 100644 index 0000000..82ffb5f Binary files /dev/null and b/data/images/game/console/say.png differ diff --git a/data/images/game/console/whisper.png b/data/images/game/console/whisper.png new file mode 100644 index 0000000..440ac16 Binary files /dev/null and b/data/images/game/console/whisper.png differ diff --git a/data/images/game/console/yell.png b/data/images/game/console/yell.png new file mode 100644 index 0000000..398bc28 Binary files /dev/null and b/data/images/game/console/yell.png differ diff --git a/data/images/game/creaturetype/summon_other.png b/data/images/game/creaturetype/summon_other.png new file mode 100644 index 0000000..6f6fa60 Binary files /dev/null and b/data/images/game/creaturetype/summon_other.png differ diff --git a/data/images/game/creaturetype/summon_own.png b/data/images/game/creaturetype/summon_own.png new file mode 100644 index 0000000..0abcb94 Binary files /dev/null and b/data/images/game/creaturetype/summon_own.png differ diff --git a/data/images/game/dangerous.png b/data/images/game/dangerous.png new file mode 100644 index 0000000..c927c67 Binary files /dev/null and b/data/images/game/dangerous.png differ diff --git a/data/images/game/emblems/emblem_blue.png b/data/images/game/emblems/emblem_blue.png new file mode 100644 index 0000000..a018e3d Binary files /dev/null and b/data/images/game/emblems/emblem_blue.png differ diff --git a/data/images/game/emblems/emblem_green.png b/data/images/game/emblems/emblem_green.png new file mode 100644 index 0000000..e5ead37 Binary files /dev/null and b/data/images/game/emblems/emblem_green.png differ diff --git a/data/images/game/emblems/emblem_member.png b/data/images/game/emblems/emblem_member.png new file mode 100644 index 0000000..7af0ad9 Binary files /dev/null and b/data/images/game/emblems/emblem_member.png differ diff --git a/data/images/game/emblems/emblem_other.png b/data/images/game/emblems/emblem_other.png new file mode 100644 index 0000000..2b2d5ad Binary files /dev/null and b/data/images/game/emblems/emblem_other.png differ diff --git a/data/images/game/emblems/emblem_red.png b/data/images/game/emblems/emblem_red.png new file mode 100644 index 0000000..94d712a Binary files /dev/null and b/data/images/game/emblems/emblem_red.png differ diff --git a/data/images/game/minimap/cross.png b/data/images/game/minimap/cross.png new file mode 100644 index 0000000..fa8a7ad Binary files /dev/null and b/data/images/game/minimap/cross.png differ diff --git a/data/images/game/minimap/flag0.png b/data/images/game/minimap/flag0.png new file mode 100644 index 0000000..1b80e29 Binary files /dev/null and b/data/images/game/minimap/flag0.png differ diff --git a/data/images/game/minimap/flag1.png b/data/images/game/minimap/flag1.png new file mode 100644 index 0000000..560bf79 Binary files /dev/null and b/data/images/game/minimap/flag1.png differ diff --git a/data/images/game/minimap/flag10.png b/data/images/game/minimap/flag10.png new file mode 100644 index 0000000..7cba49e Binary files /dev/null and b/data/images/game/minimap/flag10.png differ diff --git a/data/images/game/minimap/flag11.png b/data/images/game/minimap/flag11.png new file mode 100644 index 0000000..688175c Binary files /dev/null and b/data/images/game/minimap/flag11.png differ diff --git a/data/images/game/minimap/flag12.png b/data/images/game/minimap/flag12.png new file mode 100644 index 0000000..63ce6bf Binary files /dev/null and b/data/images/game/minimap/flag12.png differ diff --git a/data/images/game/minimap/flag13.png b/data/images/game/minimap/flag13.png new file mode 100644 index 0000000..1ffb377 Binary files /dev/null and b/data/images/game/minimap/flag13.png differ diff --git a/data/images/game/minimap/flag14.png b/data/images/game/minimap/flag14.png new file mode 100644 index 0000000..c9d5182 Binary files /dev/null and b/data/images/game/minimap/flag14.png differ diff --git a/data/images/game/minimap/flag15.png b/data/images/game/minimap/flag15.png new file mode 100644 index 0000000..fc9d692 Binary files /dev/null and b/data/images/game/minimap/flag15.png differ diff --git a/data/images/game/minimap/flag16.png b/data/images/game/minimap/flag16.png new file mode 100644 index 0000000..da226e1 Binary files /dev/null and b/data/images/game/minimap/flag16.png differ diff --git a/data/images/game/minimap/flag17.png b/data/images/game/minimap/flag17.png new file mode 100644 index 0000000..4000460 Binary files /dev/null and b/data/images/game/minimap/flag17.png differ diff --git a/data/images/game/minimap/flag18.png b/data/images/game/minimap/flag18.png new file mode 100644 index 0000000..7a625e7 Binary files /dev/null and b/data/images/game/minimap/flag18.png differ diff --git a/data/images/game/minimap/flag19.png b/data/images/game/minimap/flag19.png new file mode 100644 index 0000000..8f46613 Binary files /dev/null and b/data/images/game/minimap/flag19.png differ diff --git a/data/images/game/minimap/flag2.png b/data/images/game/minimap/flag2.png new file mode 100644 index 0000000..226eb55 Binary files /dev/null and b/data/images/game/minimap/flag2.png differ diff --git a/data/images/game/minimap/flag3.png b/data/images/game/minimap/flag3.png new file mode 100644 index 0000000..1d7f8ef Binary files /dev/null and b/data/images/game/minimap/flag3.png differ diff --git a/data/images/game/minimap/flag4.png b/data/images/game/minimap/flag4.png new file mode 100644 index 0000000..3f53f9d Binary files /dev/null and b/data/images/game/minimap/flag4.png differ diff --git a/data/images/game/minimap/flag5.png b/data/images/game/minimap/flag5.png new file mode 100644 index 0000000..8badc69 Binary files /dev/null and b/data/images/game/minimap/flag5.png differ diff --git a/data/images/game/minimap/flag6.png b/data/images/game/minimap/flag6.png new file mode 100644 index 0000000..8b71310 Binary files /dev/null and b/data/images/game/minimap/flag6.png differ diff --git a/data/images/game/minimap/flag7.png b/data/images/game/minimap/flag7.png new file mode 100644 index 0000000..866d079 Binary files /dev/null and b/data/images/game/minimap/flag7.png differ diff --git a/data/images/game/minimap/flag8.png b/data/images/game/minimap/flag8.png new file mode 100644 index 0000000..3c98633 Binary files /dev/null and b/data/images/game/minimap/flag8.png differ diff --git a/data/images/game/minimap/flag9.png b/data/images/game/minimap/flag9.png new file mode 100644 index 0000000..7a625e7 Binary files /dev/null and b/data/images/game/minimap/flag9.png differ diff --git a/data/images/game/minimap/flagcheckbox.png b/data/images/game/minimap/flagcheckbox.png new file mode 100644 index 0000000..b479c40 Binary files /dev/null and b/data/images/game/minimap/flagcheckbox.png differ diff --git a/data/images/game/minimap/floor_down.png b/data/images/game/minimap/floor_down.png new file mode 100644 index 0000000..b508610 Binary files /dev/null and b/data/images/game/minimap/floor_down.png differ diff --git a/data/images/game/minimap/floor_up.png b/data/images/game/minimap/floor_up.png new file mode 100644 index 0000000..804aa9e Binary files /dev/null and b/data/images/game/minimap/floor_up.png differ diff --git a/data/images/game/minimap/zoom_in.png b/data/images/game/minimap/zoom_in.png new file mode 100644 index 0000000..cca4c63 Binary files /dev/null and b/data/images/game/minimap/zoom_in.png differ diff --git a/data/images/game/minimap/zoom_out.png b/data/images/game/minimap/zoom_out.png new file mode 100644 index 0000000..6a56dd4 Binary files /dev/null and b/data/images/game/minimap/zoom_out.png differ diff --git a/data/images/game/npcicons/icon_chat.png b/data/images/game/npcicons/icon_chat.png new file mode 100644 index 0000000..b0da15e Binary files /dev/null and b/data/images/game/npcicons/icon_chat.png differ diff --git a/data/images/game/npcicons/icon_quest.png b/data/images/game/npcicons/icon_quest.png new file mode 100644 index 0000000..dcc8860 Binary files /dev/null and b/data/images/game/npcicons/icon_quest.png differ diff --git a/data/images/game/npcicons/icon_trade.png b/data/images/game/npcicons/icon_trade.png new file mode 100644 index 0000000..87db9f0 Binary files /dev/null and b/data/images/game/npcicons/icon_trade.png differ diff --git a/data/images/game/npcicons/icon_tradequest.png b/data/images/game/npcicons/icon_tradequest.png new file mode 100644 index 0000000..7f90a6c Binary files /dev/null and b/data/images/game/npcicons/icon_tradequest.png differ diff --git a/data/images/game/shields/shield_blue.png b/data/images/game/shields/shield_blue.png new file mode 100644 index 0000000..2d05797 Binary files /dev/null and b/data/images/game/shields/shield_blue.png differ diff --git a/data/images/game/shields/shield_blue_not_shared.png b/data/images/game/shields/shield_blue_not_shared.png new file mode 100644 index 0000000..6bd6a78 Binary files /dev/null and b/data/images/game/shields/shield_blue_not_shared.png differ diff --git a/data/images/game/shields/shield_blue_shared.png b/data/images/game/shields/shield_blue_shared.png new file mode 100644 index 0000000..4cdc2b7 Binary files /dev/null and b/data/images/game/shields/shield_blue_shared.png differ diff --git a/data/images/game/shields/shield_blue_white.png b/data/images/game/shields/shield_blue_white.png new file mode 100644 index 0000000..f1aa8fe Binary files /dev/null and b/data/images/game/shields/shield_blue_white.png differ diff --git a/data/images/game/shields/shield_gray.png b/data/images/game/shields/shield_gray.png new file mode 100644 index 0000000..aa4689e Binary files /dev/null and b/data/images/game/shields/shield_gray.png differ diff --git a/data/images/game/shields/shield_yellow.png b/data/images/game/shields/shield_yellow.png new file mode 100644 index 0000000..eaee81c Binary files /dev/null and b/data/images/game/shields/shield_yellow.png differ diff --git a/data/images/game/shields/shield_yellow_not_shared.png b/data/images/game/shields/shield_yellow_not_shared.png new file mode 100644 index 0000000..85b0b30 Binary files /dev/null and b/data/images/game/shields/shield_yellow_not_shared.png differ diff --git a/data/images/game/shields/shield_yellow_shared.png b/data/images/game/shields/shield_yellow_shared.png new file mode 100644 index 0000000..196c4fd Binary files /dev/null and b/data/images/game/shields/shield_yellow_shared.png differ diff --git a/data/images/game/shields/shield_yellow_white.png b/data/images/game/shields/shield_yellow_white.png new file mode 100644 index 0000000..7dc9899 Binary files /dev/null and b/data/images/game/shields/shield_yellow_white.png differ diff --git a/data/images/game/skull_socket.png b/data/images/game/skull_socket.png new file mode 100644 index 0000000..a2d30e2 Binary files /dev/null and b/data/images/game/skull_socket.png differ diff --git a/data/images/game/skulls/skull_black.png b/data/images/game/skulls/skull_black.png new file mode 100644 index 0000000..8d3ddc0 Binary files /dev/null and b/data/images/game/skulls/skull_black.png differ diff --git a/data/images/game/skulls/skull_green.png b/data/images/game/skulls/skull_green.png new file mode 100644 index 0000000..382461f Binary files /dev/null and b/data/images/game/skulls/skull_green.png differ diff --git a/data/images/game/skulls/skull_orange.png b/data/images/game/skulls/skull_orange.png new file mode 100644 index 0000000..0c906c1 Binary files /dev/null and b/data/images/game/skulls/skull_orange.png differ diff --git a/data/images/game/skulls/skull_red.png b/data/images/game/skulls/skull_red.png new file mode 100644 index 0000000..67245fa Binary files /dev/null and b/data/images/game/skulls/skull_red.png differ diff --git a/data/images/game/skulls/skull_white.png b/data/images/game/skulls/skull_white.png new file mode 100644 index 0000000..e2c3d55 Binary files /dev/null and b/data/images/game/skulls/skull_white.png differ diff --git a/data/images/game/skulls/skull_yellow.png b/data/images/game/skulls/skull_yellow.png new file mode 100644 index 0000000..2994f8e Binary files /dev/null and b/data/images/game/skulls/skull_yellow.png differ diff --git a/data/images/game/slots/ammo-blessed.png b/data/images/game/slots/ammo-blessed.png new file mode 100644 index 0000000..bed31d7 Binary files /dev/null and b/data/images/game/slots/ammo-blessed.png differ diff --git a/data/images/game/slots/ammo.png b/data/images/game/slots/ammo.png new file mode 100644 index 0000000..345415f Binary files /dev/null and b/data/images/game/slots/ammo.png differ diff --git a/data/images/game/slots/back-blessed.png b/data/images/game/slots/back-blessed.png new file mode 100644 index 0000000..b369e6e Binary files /dev/null and b/data/images/game/slots/back-blessed.png differ diff --git a/data/images/game/slots/back.png b/data/images/game/slots/back.png new file mode 100644 index 0000000..dc874a7 Binary files /dev/null and b/data/images/game/slots/back.png differ diff --git a/data/images/game/slots/body-blessed.png b/data/images/game/slots/body-blessed.png new file mode 100644 index 0000000..caa01ba Binary files /dev/null and b/data/images/game/slots/body-blessed.png differ diff --git a/data/images/game/slots/body.png b/data/images/game/slots/body.png new file mode 100644 index 0000000..78dcbe2 Binary files /dev/null and b/data/images/game/slots/body.png differ diff --git a/data/images/game/slots/feet-blessed.png b/data/images/game/slots/feet-blessed.png new file mode 100644 index 0000000..29c011f Binary files /dev/null and b/data/images/game/slots/feet-blessed.png differ diff --git a/data/images/game/slots/feet.png b/data/images/game/slots/feet.png new file mode 100644 index 0000000..4bdfd5f Binary files /dev/null and b/data/images/game/slots/feet.png differ diff --git a/data/images/game/slots/finger-blessed.png b/data/images/game/slots/finger-blessed.png new file mode 100644 index 0000000..575f34d Binary files /dev/null and b/data/images/game/slots/finger-blessed.png differ diff --git a/data/images/game/slots/finger.png b/data/images/game/slots/finger.png new file mode 100644 index 0000000..61dec1e Binary files /dev/null and b/data/images/game/slots/finger.png differ diff --git a/data/images/game/slots/head-blessed.png b/data/images/game/slots/head-blessed.png new file mode 100644 index 0000000..418f4f6 Binary files /dev/null and b/data/images/game/slots/head-blessed.png differ diff --git a/data/images/game/slots/head.png b/data/images/game/slots/head.png new file mode 100644 index 0000000..f2f3782 Binary files /dev/null and b/data/images/game/slots/head.png differ diff --git a/data/images/game/slots/left-hand-blessed.png b/data/images/game/slots/left-hand-blessed.png new file mode 100644 index 0000000..6140de8 Binary files /dev/null and b/data/images/game/slots/left-hand-blessed.png differ diff --git a/data/images/game/slots/left-hand.png b/data/images/game/slots/left-hand.png new file mode 100644 index 0000000..7ca84cf Binary files /dev/null and b/data/images/game/slots/left-hand.png differ diff --git a/data/images/game/slots/legs-blessed.png b/data/images/game/slots/legs-blessed.png new file mode 100644 index 0000000..759f13b Binary files /dev/null and b/data/images/game/slots/legs-blessed.png differ diff --git a/data/images/game/slots/legs.png b/data/images/game/slots/legs.png new file mode 100644 index 0000000..145121a Binary files /dev/null and b/data/images/game/slots/legs.png differ diff --git a/data/images/game/slots/neck-blessed.png b/data/images/game/slots/neck-blessed.png new file mode 100644 index 0000000..e3b6875 Binary files /dev/null and b/data/images/game/slots/neck-blessed.png differ diff --git a/data/images/game/slots/neck.png b/data/images/game/slots/neck.png new file mode 100644 index 0000000..94a4e55 Binary files /dev/null and b/data/images/game/slots/neck.png differ diff --git a/data/images/game/slots/purse.png b/data/images/game/slots/purse.png new file mode 100644 index 0000000..229e9ed Binary files /dev/null and b/data/images/game/slots/purse.png differ diff --git a/data/images/game/slots/right-hand-blessed.png b/data/images/game/slots/right-hand-blessed.png new file mode 100644 index 0000000..4a6bd9b Binary files /dev/null and b/data/images/game/slots/right-hand-blessed.png differ diff --git a/data/images/game/slots/right-hand.png b/data/images/game/slots/right-hand.png new file mode 100644 index 0000000..0aa355b Binary files /dev/null and b/data/images/game/slots/right-hand.png differ diff --git a/data/images/game/slots/soulcap.png b/data/images/game/slots/soulcap.png new file mode 100644 index 0000000..174d0df Binary files /dev/null and b/data/images/game/slots/soulcap.png differ diff --git a/data/images/game/spells/cooldowns.png b/data/images/game/spells/cooldowns.png new file mode 100644 index 0000000..a8c9d64 Binary files /dev/null and b/data/images/game/spells/cooldowns.png differ diff --git a/data/images/game/spells/defaultspells.png b/data/images/game/spells/defaultspells.png new file mode 100644 index 0000000..d1926f3 Binary files /dev/null and b/data/images/game/spells/defaultspells.png differ diff --git a/data/images/game/states/bleeding.png b/data/images/game/states/bleeding.png new file mode 100644 index 0000000..024ee7e Binary files /dev/null and b/data/images/game/states/bleeding.png differ diff --git a/data/images/game/states/burning.png b/data/images/game/states/burning.png new file mode 100644 index 0000000..9d503ca Binary files /dev/null and b/data/images/game/states/burning.png differ diff --git a/data/images/game/states/cursed.png b/data/images/game/states/cursed.png new file mode 100644 index 0000000..6171bd9 Binary files /dev/null and b/data/images/game/states/cursed.png differ diff --git a/data/images/game/states/dazzled.png b/data/images/game/states/dazzled.png new file mode 100644 index 0000000..01e42ac Binary files /dev/null and b/data/images/game/states/dazzled.png differ diff --git a/data/images/game/states/drowning.png b/data/images/game/states/drowning.png new file mode 100644 index 0000000..88c4dad Binary files /dev/null and b/data/images/game/states/drowning.png differ diff --git a/data/images/game/states/drunk.png b/data/images/game/states/drunk.png new file mode 100644 index 0000000..e83af44 Binary files /dev/null and b/data/images/game/states/drunk.png differ diff --git a/data/images/game/states/electrified.png b/data/images/game/states/electrified.png new file mode 100644 index 0000000..38e67a8 Binary files /dev/null and b/data/images/game/states/electrified.png differ diff --git a/data/images/game/states/freezing.png b/data/images/game/states/freezing.png new file mode 100644 index 0000000..04acfb0 Binary files /dev/null and b/data/images/game/states/freezing.png differ diff --git a/data/images/game/states/haste.png b/data/images/game/states/haste.png new file mode 100644 index 0000000..9f39a96 Binary files /dev/null and b/data/images/game/states/haste.png differ diff --git a/data/images/game/states/hungry.png b/data/images/game/states/hungry.png new file mode 100644 index 0000000..829e191 Binary files /dev/null and b/data/images/game/states/hungry.png differ diff --git a/data/images/game/states/logout_block.png b/data/images/game/states/logout_block.png new file mode 100644 index 0000000..4244dfe Binary files /dev/null and b/data/images/game/states/logout_block.png differ diff --git a/data/images/game/states/magic_shield.png b/data/images/game/states/magic_shield.png new file mode 100644 index 0000000..4286a01 Binary files /dev/null and b/data/images/game/states/magic_shield.png differ diff --git a/data/images/game/states/poisoned.png b/data/images/game/states/poisoned.png new file mode 100644 index 0000000..3aae9cc Binary files /dev/null and b/data/images/game/states/poisoned.png differ diff --git a/data/images/game/states/protection_zone.png b/data/images/game/states/protection_zone.png new file mode 100644 index 0000000..741f4df Binary files /dev/null and b/data/images/game/states/protection_zone.png differ diff --git a/data/images/game/states/protection_zone_block.png b/data/images/game/states/protection_zone_block.png new file mode 100644 index 0000000..47bcade Binary files /dev/null and b/data/images/game/states/protection_zone_block.png differ diff --git a/data/images/game/states/slowed.png b/data/images/game/states/slowed.png new file mode 100644 index 0000000..b1ab240 Binary files /dev/null and b/data/images/game/states/slowed.png differ diff --git a/data/images/game/states/strengthened.png b/data/images/game/states/strengthened.png new file mode 100644 index 0000000..29e827d Binary files /dev/null and b/data/images/game/states/strengthened.png differ diff --git a/data/images/game/viplist/icons.png b/data/images/game/viplist/icons.png new file mode 100644 index 0000000..e0d67cc Binary files /dev/null and b/data/images/game/viplist/icons.png differ diff --git a/data/images/game/viplist/vipcheckbox.png b/data/images/game/viplist/vipcheckbox.png new file mode 100644 index 0000000..b479c40 Binary files /dev/null and b/data/images/game/viplist/vipcheckbox.png differ diff --git a/data/images/optionstab/audio.png b/data/images/optionstab/audio.png new file mode 100644 index 0000000..e3aee23 Binary files /dev/null and b/data/images/optionstab/audio.png differ diff --git a/data/images/optionstab/console.png b/data/images/optionstab/console.png new file mode 100644 index 0000000..d9ce1db Binary files /dev/null and b/data/images/optionstab/console.png differ diff --git a/data/images/optionstab/extras.png b/data/images/optionstab/extras.png new file mode 100644 index 0000000..ace46d8 Binary files /dev/null and b/data/images/optionstab/extras.png differ diff --git a/data/images/optionstab/game.png b/data/images/optionstab/game.png new file mode 100644 index 0000000..40892e4 Binary files /dev/null and b/data/images/optionstab/game.png differ diff --git a/data/images/optionstab/graphics.png b/data/images/optionstab/graphics.png new file mode 100644 index 0000000..ace46d8 Binary files /dev/null and b/data/images/optionstab/graphics.png differ diff --git a/data/images/topbuttons/audio.png b/data/images/topbuttons/audio.png new file mode 100644 index 0000000..7bac9e9 Binary files /dev/null and b/data/images/topbuttons/audio.png differ diff --git a/data/images/topbuttons/audio_mute.png b/data/images/topbuttons/audio_mute.png new file mode 100644 index 0000000..a7dda9a Binary files /dev/null and b/data/images/topbuttons/audio_mute.png differ diff --git a/data/images/topbuttons/battle.png b/data/images/topbuttons/battle.png new file mode 100644 index 0000000..ed67347 Binary files /dev/null and b/data/images/topbuttons/battle.png differ diff --git a/data/images/topbuttons/bot.png b/data/images/topbuttons/bot.png new file mode 100644 index 0000000..8dc7d11 Binary files /dev/null and b/data/images/topbuttons/bot.png differ diff --git a/data/images/topbuttons/combatcontrols.png b/data/images/topbuttons/combatcontrols.png new file mode 100644 index 0000000..f39523e Binary files /dev/null and b/data/images/topbuttons/combatcontrols.png differ diff --git a/data/images/topbuttons/cooldowns.png b/data/images/topbuttons/cooldowns.png new file mode 100644 index 0000000..0e66ccf Binary files /dev/null and b/data/images/topbuttons/cooldowns.png differ diff --git a/data/images/topbuttons/debug.png b/data/images/topbuttons/debug.png new file mode 100644 index 0000000..9768e4c Binary files /dev/null and b/data/images/topbuttons/debug.png differ diff --git a/data/images/topbuttons/healthinfo.png b/data/images/topbuttons/healthinfo.png new file mode 100644 index 0000000..c398bf6 Binary files /dev/null and b/data/images/topbuttons/healthinfo.png differ diff --git a/data/images/topbuttons/hotkeys.png b/data/images/topbuttons/hotkeys.png new file mode 100644 index 0000000..9f0c25e Binary files /dev/null and b/data/images/topbuttons/hotkeys.png differ diff --git a/data/images/topbuttons/inventory.png b/data/images/topbuttons/inventory.png new file mode 100644 index 0000000..27876d9 Binary files /dev/null and b/data/images/topbuttons/inventory.png differ diff --git a/data/images/topbuttons/login.png b/data/images/topbuttons/login.png new file mode 100644 index 0000000..55ec697 Binary files /dev/null and b/data/images/topbuttons/login.png differ diff --git a/data/images/topbuttons/logout.png b/data/images/topbuttons/logout.png new file mode 100644 index 0000000..91e2354 Binary files /dev/null and b/data/images/topbuttons/logout.png differ diff --git a/data/images/topbuttons/minimap.png b/data/images/topbuttons/minimap.png new file mode 100644 index 0000000..8ec6efe Binary files /dev/null and b/data/images/topbuttons/minimap.png differ diff --git a/data/images/topbuttons/modulemanager.png b/data/images/topbuttons/modulemanager.png new file mode 100644 index 0000000..8089991 Binary files /dev/null and b/data/images/topbuttons/modulemanager.png differ diff --git a/data/images/topbuttons/motd.png b/data/images/topbuttons/motd.png new file mode 100644 index 0000000..01cb280 Binary files /dev/null and b/data/images/topbuttons/motd.png differ diff --git a/data/images/topbuttons/options.png b/data/images/topbuttons/options.png new file mode 100644 index 0000000..3a8aaf7 Binary files /dev/null and b/data/images/topbuttons/options.png differ diff --git a/data/images/topbuttons/particles.png b/data/images/topbuttons/particles.png new file mode 100644 index 0000000..a52cc24 Binary files /dev/null and b/data/images/topbuttons/particles.png differ diff --git a/data/images/topbuttons/questlog.png b/data/images/topbuttons/questlog.png new file mode 100644 index 0000000..0ad6ea4 Binary files /dev/null and b/data/images/topbuttons/questlog.png differ diff --git a/data/images/topbuttons/shop.png b/data/images/topbuttons/shop.png new file mode 100644 index 0000000..21d6980 Binary files /dev/null and b/data/images/topbuttons/shop.png differ diff --git a/data/images/topbuttons/skills.png b/data/images/topbuttons/skills.png new file mode 100644 index 0000000..52deb10 Binary files /dev/null and b/data/images/topbuttons/skills.png differ diff --git a/data/images/topbuttons/spelllist.png b/data/images/topbuttons/spelllist.png new file mode 100644 index 0000000..e1c01cc Binary files /dev/null and b/data/images/topbuttons/spelllist.png differ diff --git a/data/images/topbuttons/terminal.png b/data/images/topbuttons/terminal.png new file mode 100644 index 0000000..768c2e9 Binary files /dev/null and b/data/images/topbuttons/terminal.png differ diff --git a/data/images/topbuttons/unjustifiedpoints.png b/data/images/topbuttons/unjustifiedpoints.png new file mode 100644 index 0000000..67245fa Binary files /dev/null and b/data/images/topbuttons/unjustifiedpoints.png differ diff --git a/data/images/topbuttons/viplist.png b/data/images/topbuttons/viplist.png new file mode 100644 index 0000000..d0e69e7 Binary files /dev/null and b/data/images/topbuttons/viplist.png differ diff --git a/data/images/ui/android.png b/data/images/ui/android.png new file mode 100644 index 0000000..6d14f24 Binary files /dev/null and b/data/images/ui/android.png differ diff --git a/data/images/ui/arrow_horizontal.png b/data/images/ui/arrow_horizontal.png new file mode 100644 index 0000000..a0ec72c Binary files /dev/null and b/data/images/ui/arrow_horizontal.png differ diff --git a/data/images/ui/arrow_vertical.png b/data/images/ui/arrow_vertical.png new file mode 100644 index 0000000..d48aba3 Binary files /dev/null and b/data/images/ui/arrow_vertical.png differ diff --git a/data/images/ui/button.png b/data/images/ui/button.png new file mode 100644 index 0000000..103f617 Binary files /dev/null and b/data/images/ui/button.png differ diff --git a/data/images/ui/button_popupmenu.png b/data/images/ui/button_popupmenu.png new file mode 100644 index 0000000..103f617 Binary files /dev/null and b/data/images/ui/button_popupmenu.png differ diff --git a/data/images/ui/button_rounded.png b/data/images/ui/button_rounded.png new file mode 100644 index 0000000..eebfa2f Binary files /dev/null and b/data/images/ui/button_rounded.png differ diff --git a/data/images/ui/button_square.png b/data/images/ui/button_square.png new file mode 100644 index 0000000..103f617 Binary files /dev/null and b/data/images/ui/button_square.png differ diff --git a/data/images/ui/button_top.png b/data/images/ui/button_top.png new file mode 100644 index 0000000..73703a0 Binary files /dev/null and b/data/images/ui/button_top.png differ diff --git a/data/images/ui/button_top_blink.png b/data/images/ui/button_top_blink.png new file mode 100644 index 0000000..9a66927 Binary files /dev/null and b/data/images/ui/button_top_blink.png differ diff --git a/data/images/ui/button_topgame.png b/data/images/ui/button_topgame.png new file mode 100644 index 0000000..c6f115f Binary files /dev/null and b/data/images/ui/button_topgame.png differ diff --git a/data/images/ui/checkbox.png b/data/images/ui/checkbox.png new file mode 100644 index 0000000..e3aa5e1 Binary files /dev/null and b/data/images/ui/checkbox.png differ diff --git a/data/images/ui/colorbox.png b/data/images/ui/colorbox.png new file mode 100644 index 0000000..9d2ef7f Binary files /dev/null and b/data/images/ui/colorbox.png differ diff --git a/data/images/ui/combobox.png b/data/images/ui/combobox.png new file mode 100644 index 0000000..8ef29ab Binary files /dev/null and b/data/images/ui/combobox.png differ diff --git a/data/images/ui/combobox_rounded.png b/data/images/ui/combobox_rounded.png new file mode 100644 index 0000000..3c00702 Binary files /dev/null and b/data/images/ui/combobox_rounded.png differ diff --git a/data/images/ui/combobox_square.png b/data/images/ui/combobox_square.png new file mode 100644 index 0000000..8ef29ab Binary files /dev/null and b/data/images/ui/combobox_square.png differ diff --git a/data/images/ui/icon_add.png b/data/images/ui/icon_add.png new file mode 100644 index 0000000..f820a0b Binary files /dev/null and b/data/images/ui/icon_add.png differ diff --git a/data/images/ui/ios.png b/data/images/ui/ios.png new file mode 100644 index 0000000..6c0ecb4 Binary files /dev/null and b/data/images/ui/ios.png differ diff --git a/data/images/ui/item-blessed.png b/data/images/ui/item-blessed.png new file mode 100644 index 0000000..41db9d9 Binary files /dev/null and b/data/images/ui/item-blessed.png differ diff --git a/data/images/ui/item.png b/data/images/ui/item.png new file mode 100644 index 0000000..4230ace Binary files /dev/null and b/data/images/ui/item.png differ diff --git a/data/images/ui/menubox.png b/data/images/ui/menubox.png new file mode 100644 index 0000000..307e5fd Binary files /dev/null and b/data/images/ui/menubox.png differ diff --git a/data/images/ui/miniwindow.png b/data/images/ui/miniwindow.png new file mode 100644 index 0000000..e258304 Binary files /dev/null and b/data/images/ui/miniwindow.png differ diff --git a/data/images/ui/miniwindow_buttons.png b/data/images/ui/miniwindow_buttons.png new file mode 100644 index 0000000..8bf5271 Binary files /dev/null and b/data/images/ui/miniwindow_buttons.png differ diff --git a/data/images/ui/panel_bottom.png b/data/images/ui/panel_bottom.png new file mode 100644 index 0000000..b56a0c9 Binary files /dev/null and b/data/images/ui/panel_bottom.png differ diff --git a/data/images/ui/panel_container.png b/data/images/ui/panel_container.png new file mode 100644 index 0000000..984672e Binary files /dev/null and b/data/images/ui/panel_container.png differ diff --git a/data/images/ui/panel_content.png b/data/images/ui/panel_content.png new file mode 100644 index 0000000..b56a0c9 Binary files /dev/null and b/data/images/ui/panel_content.png differ diff --git a/data/images/ui/panel_flat.png b/data/images/ui/panel_flat.png new file mode 100644 index 0000000..d32beb7 Binary files /dev/null and b/data/images/ui/panel_flat.png differ diff --git a/data/images/ui/panel_lightflat.png b/data/images/ui/panel_lightflat.png new file mode 100644 index 0000000..d32beb7 Binary files /dev/null and b/data/images/ui/panel_lightflat.png differ diff --git a/data/images/ui/panel_map.png b/data/images/ui/panel_map.png new file mode 100644 index 0000000..b56a0c9 Binary files /dev/null and b/data/images/ui/panel_map.png differ diff --git a/data/images/ui/panel_side.png b/data/images/ui/panel_side.png new file mode 100644 index 0000000..b56a0c9 Binary files /dev/null and b/data/images/ui/panel_side.png differ diff --git a/data/images/ui/panel_top.png b/data/images/ui/panel_top.png new file mode 100644 index 0000000..649874a Binary files /dev/null and b/data/images/ui/panel_top.png differ diff --git a/data/images/ui/progressbar.png b/data/images/ui/progressbar.png new file mode 100644 index 0000000..d139932 Binary files /dev/null and b/data/images/ui/progressbar.png differ diff --git a/data/images/ui/scrollbar.png b/data/images/ui/scrollbar.png new file mode 100644 index 0000000..f565ecd Binary files /dev/null and b/data/images/ui/scrollbar.png differ diff --git a/data/images/ui/separator_horizontal.png b/data/images/ui/separator_horizontal.png new file mode 100644 index 0000000..40484e3 Binary files /dev/null and b/data/images/ui/separator_horizontal.png differ diff --git a/data/images/ui/separator_vertical.png b/data/images/ui/separator_vertical.png new file mode 100644 index 0000000..ca3b5d3 Binary files /dev/null and b/data/images/ui/separator_vertical.png differ diff --git a/data/images/ui/spinbox.png b/data/images/ui/spinbox.png new file mode 100644 index 0000000..8029090 Binary files /dev/null and b/data/images/ui/spinbox.png differ diff --git a/data/images/ui/spinbox_down.png b/data/images/ui/spinbox_down.png new file mode 100644 index 0000000..0f99b8e Binary files /dev/null and b/data/images/ui/spinbox_down.png differ diff --git a/data/images/ui/spinbox_up.png b/data/images/ui/spinbox_up.png new file mode 100644 index 0000000..8432b2d Binary files /dev/null and b/data/images/ui/spinbox_up.png differ diff --git a/data/images/ui/tabbutton_rounded.png b/data/images/ui/tabbutton_rounded.png new file mode 100644 index 0000000..eebfa2f Binary files /dev/null and b/data/images/ui/tabbutton_rounded.png differ diff --git a/data/images/ui/tabbutton_square.png b/data/images/ui/tabbutton_square.png new file mode 100644 index 0000000..1e1c583 Binary files /dev/null and b/data/images/ui/tabbutton_square.png differ diff --git a/data/images/ui/textedit.png b/data/images/ui/textedit.png new file mode 100644 index 0000000..96062ef Binary files /dev/null and b/data/images/ui/textedit.png differ diff --git a/data/images/ui/window.png b/data/images/ui/window.png new file mode 100644 index 0000000..0b3f3c8 Binary files /dev/null and b/data/images/ui/window.png differ diff --git a/data/images/ui/window_headless.png b/data/images/ui/window_headless.png new file mode 100644 index 0000000..d74ec1d Binary files /dev/null and b/data/images/ui/window_headless.png differ diff --git a/data/locales/de.lua b/data/locales/de.lua new file mode 100644 index 0000000..9637d66 --- /dev/null +++ b/data/locales/de.lua @@ -0,0 +1,377 @@ + +locale = { + name = "de", + charset = "cp1252", + languageName = "Deutsch", + + formatNumbers = true, + decimalSeperator = ',', + thousandsSeperator = ' ', + + translation = { + ["1a) Offensive Name"] = false, + ["1b) Invalid Name Format"] = false, + ["1c) Unsuitable Name"] = false, + ["1d) Name Inciting Rule Violation"] = false, + ["2a) Offensive Statement"] = false, + ["2b) Spamming"] = false, + ["2c) Illegal Advertising"] = false, + ["2d) Off-Topic Public Statement"] = false, + ["2e) Non-English Public Statement"] = false, + ["2f) Inciting Rule Violation"] = false, + ["3a) Bug Abuse"] = false, + ["3b) Game Weakness Abuse"] = false, + ["3c) Using Unofficial Software to Play"] = false, + ["3d) Hacking"] = false, + ["3e) Multi-Clienting"] = false, + ["3f) Account Trading or Sharing"] = false, + ["4a) Threatening Gamemaster"] = false, + ["4b) Pretending to Have Influence on Rule Enforcement"] = false, + ["4c) False Report to Gamemaster"] = false, + ["Accept"] = "Annehmen", + ["Account name"] = "Benutzername", + ["Account Status:"] = false, + ["Action:"] = false, + ["Add"] = "Hinzufügen", + ["Add new VIP"] = "Neuen Freund hinzufügen", + ["Addon 1"] = "Addon 1", + ["Addon 2"] = "Addon 2", + ["Addon 3"] = "Addon 3", + ["Add to VIP list"] = "Zur VIP Liste hinzufügen", + ["Adjust volume"] = "Lautstärke regeln", + ["Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nSimply click on Ok to resume your journeys!"] = false, + ["All"] = false, + ["All modules and scripts were reloaded."] = "Alle Module wurden neu geladen", + ["Allow auto chase override"] = false, + ["Also known as dash in tibia community, recommended\nfor playing characters with high speed"] = false, + ["Ambient light: %s%%"] = false, + ["Amount:"] = "Menge:", + ["Amount"] = "Menge", + ["Anonymous"] = "Anonym", + ["Are you sure you want to logout?"] = "Sind Sie sicher das du das Spiel verlassen willst?", + ["Attack"] = "Angreifen", + ["Author"] = "Autor", + ["Autoload"] = "Automatisch", + ["Autoload priority"] = "Ladepriorität", + ["Auto login"] = "Automatisch einloggen", + ["Auto login selected character on next charlist load"] = "Automatisches einloggen des ausgewählten Charakters", + ["Axe Fighting"] = "Axtkampf", + ["Balance:"] = "Guthaben:", + ["Banishment"] = false, + ["Banishment + Final Warning"] = false, + ["Battle"] = "Kampf", + ["Browse"] = false, + ["Bug report sent."] = "Bugreport würde versendet.", + ["Button Assign"] = "Button Assign", + ["Buy"] = "Kaufen", + ["Buy Now"] = "Jetzt kaufen", + ["Buy Offers"] = "Angebot", + ["Buy with backpack"] = "Im Backpack kaufen", + ["Cancel"] = "Abbrechen", + ["Cannot login while already in game."] = "Sie können sich nicht einloggen während Sie im Spiel sind.", + ["Cap"] = false, + ["Capacity"] = "Belastbarkeit", + ["Center"] = false, + ["Channels"] = false, + ["Character List"] = "Charakter Liste", + ["Classic control"] = "Klassische Steuerung", + ["Clear current message window"] = "Chatverlauf leeren", + ["Clear Messages"] = false, + ["Clear object"] = "Objekt leeren", + ["Client needs update."] = "Der Client muss geupdated werden.", + ["Close"] = "Schließen", + ["Close this channel"] = "Diesen Channel schließen", + ["Club Fighting"] = "Keulenkampf", + ["Combat Controls"] = "Kampfsteuerungen", + ["Comment:"] = "Kommentar:", + ["Connecting to game server..."] = "Verbindung zum Spielserver wird aufgebaut...", + ["Connecting to login server..."] = "Verbindung zum Loginserver wird aufgebaut...", + ["Console"] = false, + ["Cooldowns"] = false, + ["Copy message"] = "Nachricht kopieren", + ["Copy name"] = "Namen kopieren", + ["Copy Name"] = "Namen kopieren", + ["Create Map Mark"] = false, + ["Create mark"] = false, + ["Create New Offer"] = "Neues Angebot erstellen", + ["Create Offer"] = "Angebot erstellen", + ["Current hotkeys:"] = "Aktuelle Hotkeys", + ["Current hotkey to add: %s"] = "Hotkeys zum hinzufügen: %s", + ["Current Offers"] = "Aktuelle Angebote", + ["Default"] = "Standart", + ["Delete mark"] = false, + ["Description:"] = false, + ["Description"] = "Beschreibung", + ["Destructive Behaviour"] = false, + ["Detail"] = "Details", + ["Details"] = false, + ["Disable Shared Experience"] = "Expteilung deaktivieren", + ["Dismount"] = false, + ["Display connection speed to the server (milliseconds)"] = false, + ["Distance Fighting"] = "Fernkampf", + ["Don't stretch/shrink Game Window"] = false, + ["Edit hotkey text:"] = "Hotkeytext bearbeiten:", + ["Edit List"] = "Liste bearbeiten", + ["Edit Text"] = "Text bearbeiten", + ["Enable music"] = "Musik einschalten", + ["Enable Shared Experience"] = "Expteilung aktivieren", + ["Enable smart walking"] = false, + ["Enable vertical synchronization"] = "'Vertical Synchronization' aktivieren", + ["Enable walk booster"] = false, + ["Enter Game"] = "Dem Spiel beitreten", + ["Enter one name per line."] = "Gib einen Namen pro Zeile ein.", + ["Enter with your account again to update your client."] = false, + ["Error"] = "Error", + ["Error"] = "Error", + ["Excessive Unjustified Player Killing"] = false, + ["Exclude from private chat"] = "Aus dem Privatgespräch ausschließen", + ["Exit"] = false, + ["Experience"] = "Erfahrung", + ["Filter list to match your level"] = false, + ["Filter list to match your vocation"] = false, + ["Find:"] = false, + ["Fishing"] = "Fischen", + ["Fist Fighting"] = "Faustkampf", + ["Follow"] = "Verfolgen", + ["Force Exit"] = false, + ["For Your Information"] = false, + ["Free Account"] = false, + ["Fullscreen"] = "Vollbild", + ["Game"] = false, + ["Game framerate limit: %s"] = false, + ["Graphics"] = "Grafik", + ["Graphics card driver not detected"] = false, + ["Graphics Engine:"] = "Grafikengine:", + ["Head"] = "Kopf", + ["Healing"] = false, + ["Health Info"] = false, + ["Health Information"] = false, + ["Hide monsters"] = "Monster ausblenden", + ["Hide non-skull players"] = "Spieler ohne Skull ausblenden", + ["Hide Npcs"] = "NPCs ausblenden", + ["Hide Offline"] = false, + ["Hide party members"] = "Partymitglieder ausblenden", + ["Hide players"] = "Spieler ausblenden", + ["Hide spells for higher exp. levels"] = false, + ["Hide spells for other vocations"] = false, + ["Hit Points"] = "Lebenspunkte", + ["Hold left mouse button to navigate\nScroll mouse middle button to zoom\nRight mouse button to create map marks"] = false, + ["Hotkeys"] = false, + ["If you shut down the program, your character might stay in the game.\nClick on 'Logout' to ensure that you character leaves the game properly.\nClick on 'Exit' if you want to exit the program without logging out your character."] = "Wenn du das Programm schließt kann es sein, dass dein Charakter im Spiel verweilt.nKlicke 'Logout' um sicherzustellen, dass dein Charakter das Spiel wirklich verlässt.\nKlicke 'Exit' wenn du das Programm beenden willst on deinen Charakter auszuloggen.", + ["Ignore"] = false, + ["Ignore capacity"] = "Belastbarkeit ignorieren", + ["Ignored players:"] = false, + ["Ignore equipped"] = "Equipment ignorieren", + ["Ignore List"] = false, + ["Ignore players"] = false, + ["Ignore Private Messages"] = false, + ["Ignore Yelling"] = false, + ["Interface framerate limit: %s"] = false, + ["Inventory"] = "Inventar", + ["Invite to Party"] = "Zur Party einladen", + ["Invite to private chat"] = "Zum Privatchat einladen", + ["IP Address Banishment"] = false, + ["Item Offers"] = false, + ["It is empty."] = false, + ["Join %s's Party"] = "%ss Party beitreten", + ["Leave Party"] = "Party verlassen", + ["Level"] = "Stufe", + ["Lifetime Premium Account"] = false, + ["Limits FPS to 60"] = "FPS auf 60 begrenzen", + ["List of items that you're able to buy"] = "Liste der Items die du kaufen kannst", + ["List of items that you're able to sell"] = "Liste der Items die du verkaufen kannst", + ["Load"] = "Laden", + ["Logging out..."] = "Ausloggen...", + ["Login"] = "Einloggen", + ["Login Error"] = false, + ["Login Error"] = false, + ["Logout"] = "Ausloggen", + ["Look"] = "Ansehen", + ["Magic Level"] = "Magie Level", + ["Make sure that your client uses\nthe correct game protocol version"] = "Vergewissere dich, dass der Client das richtige Protokoll verwendet.", + ["Mana"] = "Mana", + ["Manage hotkeys:"] = "Hotkeys verwalten:", + ["Market"] = "Markt", + ["Market Offers"] = "Marktangebot", + ["Message of the day"] = "Nachricht des Tages", + ["Message to "] = "Nachricht an ", + ["Message to %s"] = "Nachricht an %s", + ["Minimap"] = "Minimap", + ["Module Manager"] = "Module verwalten", + ["Module name"] = "Modulname", + ["Mount"] = false, + ["Move Stackable Item"] = false, + ["Move up"] = false, + ["My Offers"] = "Mein Angebot", + ["Name:"] = "Name:", + ["Name Report"] = false, + ["Name Report + Banishment"] = false, + ["Name Report + Banishment + Final Warning"] = false, + ["No"] = false, + ["No graphics card detected, everything will be drawn using the CPU,\nthus the performance will be really bad.\nPlease update your graphics driver to have a better performance."] = false, + ["No item selected."] = "Keine Items ausgewählt", + ["No Mount"] = "Kein Reittier", + ["No Outfit"] = "Kein Outfit", + ["No statement has been selected."] = false, + ["Notation"] = false, + ["NPC Trade"] = "NPC Handel", + ["Offer History"] = "Angebotsverlauf", + ["Offers"] = "Angebote", + ["Offer Type:"] = "Angebotstyp:", + ["Offline Training"] = false, + ["Ok"] = false, + ["on %s.\n"] = false, + ["Open"] = "Öffnen", + ["Open a private message channel:"] = "Privatchannel öffnen:", + ["Open charlist automatically when starting client"] = false, + ["Open in new window"] = "Im neuen Fenster öffnen", + ["Open new channel"] = "Neuen Channel öffnen", + ["Options"] = "Optionen", + ["Overview"] = false, + ["Pass Leadership to %s"] = "%s zum Anführer ernennen", + ["Password"] = "Passwort", + ["Piece Price:"] = "Stückpreis", + ["Please enter a character name:"] = "Bitte gib einen Charakternamen an:", + ["Please, press the key you wish to add onto your hotkeys manager"] = "Bitte die gewünschte Taste drücken", + ["Please Select"] = false, + ["Please use this dialog to only report bugs. Do not report rule violations here!"] = false, + ["Please wait"] = "Warte bitte", + ["Port"] = "Port", + ["Position:"] = false, + ["Position: %i %i %i"] = false, + ["Premium Account (%s) days left"] = false, + ["Price:"] = "Preis", + ["Primary"] = "Primär", + ["Protocol"] = "Protokoll", + ["Quest Log"] = false, + ["Randomize"] = false, + ["Randomize characters outfit"] = "Zufälliges Outfit", + ["Reason:"] = "Grund:", + ["Refresh"] = "Aktualisieren", + ["Refresh Offers"] = false, + ["Regeneration Time"] = false, + ["Reject"] = "Ablehnen", + ["Reload All"] = "Alle neu laden", + ["Remember account and password when starts client"] = false, + ["Remember password"] = "Passwort speichern", + ["Remove"] = "Entfernen", + ["Remove %s"] = "%s entfernen", + ["Report Bug"] = false, + ["Reserved for more functionality later."] = false, + ["Reset Market"] = false, + ["Revoke %s's Invitation"] = "%ss Einladung zurückziehen", + ["Rotate"] = "Rotieren", + ["Rule Violation"] = false, + ["Save"] = false, + ["Save Messages"] = false, + ["Search:"] = "Suchen:", + ["Search all items"] = false, + ["Secondary"] = "Sekundär", + ["Select object"] = "Objekt auswählen", + ["Select Outfit"] = "Outfit auswählen", + ["Select your language"] = false, + ["Sell"] = "Verkaufen", + ["Sell Now"] = "Jetzt verkaufen", + ["Sell Offers"] = "Verkaufsangebote", + ["Send"] = "Versenden", + ["Send automatically"] = "Automatisch versenden", + ["Send Message"] = false, + ["Server"] = "Server", + ["Server Log"] = false, + ["Set Outfit"] = "Outfit ändern", + ["Shielding"] = "Verteidigung", + ["Show all items"] = "Alle Items anzeigen", + ["Show connection ping"] = false, + ["Show Depot Only"] = false, + ["Show event messages in console"] = "Event Nachrichten in der Konsole anzeigen", + ["Show frame rate"] = "FPS Rate anzeigen", + ["Show info messages in console"] = "Informations Nachrichten in der Konsole anzeigen", + ["Show left panel"] = false, + ["Show levels in console"] = "Level in der Konsole anzeigen", + ["Show Offline"] = false, + ["Show private messages in console"] = "Privatnachrichten in der Konsole anzeigen", + ["Show private messages on screen"] = "Privatenachrichten auf dem Bildschirm anzeigen", + ["Show Server Messages"] = false, + ["Show status messages in console"] = "Status Nachrichten in der Konsole anzeigen", + ["Show Text"] = "Text anzeigen", + ["Show timestamps in console"] = "Zeit in der Konsole anzeigen", + ["Show your depot items only"] = "Nur Depotitems anzeigen", + ["Skills"] = "Fähigkeiten", + ["Soul"] = false, + ["Soul Points"] = false, + ["Special"] = false, + ["Speed"] = false, + ["Spell Cooldowns"] = false, + ["Spell List"] = false, + ["Stamina"] = "Ausdauer", + ["Statement:"] = false, + ["Statement Report"] = false, + ["Statistics"] = "Statistiken", + ["Stop Attack"] = "Angriff abbrechen", + ["Stop Follow"] = "Verfolgen abbrechen", + ["Support"] = false, + ["%s: (use object)"] = "%s: (Objekt benutzen)", + ["%s: (use object on target)"] = "%s: (Objekt auf Ziel benutzen)", + ["%s: (use object on yourself)"] = false, + ["%s: (use object with crosshair)"] = false, + ["Sword Fighting"] = "Schwertkampf", + ["Terminal"] = "Terminal", + ["There is no way."] = "Es gibt keinen Weg dagin.", + ["Title"] = false, + ["Total Price:"] = "Gesamtpreis:", + ["Trade"] = "Handel", + ["Trade with ..."] = "Handeln mit ...", + ["Trying to reconnect in %s seconds."] = "Versuch neu zu verbinden in %s Sekunden.", + ["Unable to load dat file, please place a valid dat in '%s'"] = false, + ["Unable to load spr file, please place a valid spr in '%s'"] = false, + ["Unable to logout."] = "Es ist nicht möglich auszuloggen.", + ["Unignore"] = false, + ["Unload"] = false, + ["Update needed"] = false, + ["Use"] = "Benutzen", + ["Use on target"] = "Auf Ziel benutzen", + ["Use on yourself"] = false, + ["Use with ..."] = "Benutzen mit ...", + ["Version"] = "Version", + ["VIP List"] = "VIP Liste", + ["Voc."] = false, + ["Vocation"] = false, + ["Waiting List"] = "Warteliste", + ["Website"] = false, + ["Weight:"] = "Gewicht:", + ["Will detect when to use diagonal step based on the\nkeys you are pressing"] = false, + ["With crosshair"] = false, + ["Yes"] = false, + ["You are bleeding"] = "Du blutest", + ["You are burning"] = "Du brennst", + ["You are cursed"] = "Du bist verflucht", + ["You are dazzled"] = "Du bist geblendet", + ["You are dead."] = "Du bist tot.", + ["You are dead"] = "Du bist tot", + ["You are drowning"] = "Du ertrinkst", + ["You are drunk"] = "Du bist betrunken", + ["You are electrified"] = "Du bist elektrifiziert", + ["You are freezing"] = "Du bist am Erfrieren", + ["You are hasted"] = "Du bist am Eilen", + ["You are hungry"] = "Du bist hungrig", + ["You are paralysed"] = "Du bist paralysiert", + ["You are poisoned"] = "Du bist vergiftet", + ["You are protected by a magic shield"] = "Du wirst von einem magischen Schild beschützt", + ["You are strengthened"] = "Du bist gestärkt", + ["You are within a protection zone"] = "Du befindest dich in einer Schutzzone", + ["You can enter new text."] = "Du kannst einen neuen Text eingeben", + ["You have %s percent"] = "Du hast %d Prozent", + ["You have %s percent to go"] = "Dir fehlen %d Prozent", + ["You may not logout during a fight"] = "Du kannst nicht mitten im Kampf ausloggen", + ["You may not logout or enter a protection zone"] = "Du kannst nicht ausloggen oder eine Schutzzone betreten", + ["You must enter a comment."] = "Du musst einen Kommentar eingeben.", + ["You must enter a valid server address and port."] = "Du musst eine gültige Serveradresse und einen gültigen Port eingeben", + ["You must select a character to login!"] = "Du musst einen Charakter auswählen!", + ["Your Capacity:"] = "Deine Belastbarkeit:", + ["You read the following, written by \n%s\n"] = "Du liest das Folgende, geschrieben von \n%s\n", + ["You read the following, written on \n%s.\n"] = false, + ["Your Money:"] = "Dein Geld:", + } +} + +modules.client_locales.installLocale(locale) diff --git a/data/locales/en.lua b/data/locales/en.lua new file mode 100644 index 0000000..5508110 --- /dev/null +++ b/data/locales/en.lua @@ -0,0 +1,14 @@ +locale = { + name = "en", + charset = "cp1252", + languageName = "English", + + formatNumbers = true, + decimalSeperator = '.', + thousandsSeperator = ',', + + -- translations are not needed because everything is already in english + translation = {} +} + +modules.client_locales.installLocale(locale) diff --git a/data/locales/es.lua b/data/locales/es.lua new file mode 100644 index 0000000..0e67deb --- /dev/null +++ b/data/locales/es.lua @@ -0,0 +1,382 @@ +-- special thanks for Shaday, who made these translations +--Dominique120 edits: I made some statements to sound more formal and appropriate as well as correcting a few words that were not translated. I also added a few notes for future translators to keep in mind. + + +locale = { + name = "es", + charset = "cp1252", + languageName = "Espańol", + + formatNumbers = true, + decimalSeperator = ',', + thousandsSeperator = '.', + + translation = { + ["1a) Offensive Name"] = "1a) Nombre ofensivo", + ["1b) Invalid Name Format"] = "1b) Formato invalido para nombre", + ["1c) Unsuitable Name"] = "1c) Nombre no adecuado", + ["1d) Name Inciting Rule Violation"] = "1d) Nombre que incita una violación al reglamento", + ["2a) Offensive Statement"] = "2a) Comentario ofensivo", + ["2b) Spamming"] = "2b) Spamming", + ["2c) Illegal Advertising"] = "2c) Publicidad ilícita", + ["2d) Off-Topic Public Statement"] = "2d) Publicación fuera de lugar", + ["2e) Non-English Public Statement"] = "2e) Publicación fuera del ingles", + ["2f) Inciting Rule Violation"] = "2f) Incitar a una violación al reglamento", + ["3a) Bug Abuse"] = "3a) Abuso de error", + ["3b) Game Weakness Abuse"] = "3b) Abuso de debilidad del juego", + ["3c) Using Unofficial Software to Play"] = "3c) Usando software ilegal para jugar", + ["3d) Hacking"] = "3d) Hackeo", + ["3e) Multi-Clienting"] = "3e) Uso de múltiples clientes", + ["3f) Account Trading or Sharing"] = "3f) Intercambio de cuenta", + ["4a) Threatening Gamemaster"] = "4a) Amenzar a un Gamemaster", + ["4b) Pretending to Have Influence on Rule Enforcement"] = "4b) Pretender tener influencia en una parte del reglamento", + ["4c) False Report to Gamemaster"] = "4c) Reporte falso a un Gamemaster", + ["Accept"] = "Aceptar", + ["Account name"] = "Nombre de cuenta", + ["Account Status:"] = "Estado de cuenta:", + ["Action:"] = "Acción:", + ["Add"] = "Ańadir", + ["Add new VIP"] = "Ańadir nuevo VIP", + ["Addon 1"] = "Addon 1", + ["Addon 2"] = "Addon 2", + ["Addon 3"] = "Addon 3", + ["Add to VIP list"] = "Ańadir a lista VIP", + ["Adjust volume"] = "Ajustar volumen", + ["Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nSimply click on Ok to resume your journeys!"] = "ˇAy! Aventurero valiente, has conocido un triste destino. \nPero no se desespere, porque los dioses te llevarán de vuelta \na este mundo a cambio de un pequeńo sacrificio \n\nSimplemente haga clic en Aceptar para continuar con sus viajes!", + ["All"] = "Todo", + ["All modules and scripts were reloaded."] = "Todos los módulos y scripts se vuelven a cargar.", + ["Allow auto chase override"] = "Permitur auto persecución override", + ["Also known as dash in tibia community, recommended\nfor playing characters with high speed"] = "Conocido por la comunidad de tibia como dash.\nRecomenada para jugadores de alto nivel.", + ["Ambient light: %s%%"] = "Ambiente de luz: %s%%", + ["Amount:"] = "Cantidad:", + ["Amount"] = "Cantidad", + ["Anonymous"] = "Anónimo", + ["Are you sure you want to logout?"] = "żEstas seguro de que deseas salir?", + ["Attack"] = "Atacar", + ["Author"] = "Autor", + ["Autoload"] = "Auto carga", + ["Autoload priority"] = "Auto carga prioritaria", + ["Auto login"] = "Auto ingresar", + ["Auto login selected character on next charlist load"] = "Ingresar la siguiente vez que aparece el charlist con el personaje seleccionado", + ["Axe Fighting"] = "Combate con hacha", + ["Balance:"] = "Saldo:", + ["Banishment"] = "Banishment", + ["Banishment + Final Warning"] = "Banishment + Final Warning", + ["Battle"] = "Batalla", + ["Browse"] = "Navegar", + ["Bug report sent."] = "Reporte de error enviado.", + ["Button Assign"] = "Botón asignado", + ["Buy"] = "Compra", + ["Buy Now"] = "Compra ahora", + ["Buy Offers"] = "Comprar oferta", + ["Buy with backpack"] = "Comprar con mochila", + ["Cancel"] = "Cancelar", + ["Cannot login while already in game."] = "No se puede iniciar sesión, mientras que estés en el juego.", + ["Cap"] = "Cap", + ["Capacity"] = "Capacidad", + ["Center"] = "Centrar", + ["Channels"] = "Canales", + ["Character List"] = "Lista de carácter", + ["Classic control"] = "Controles Clásicos", + ["Clear current message window"] = "Limpiar mensaje actual en ventana", + ["Clear Messages"] = "Limpiar mensaje", + ["Clear object"] = "Limpiar objeto", + ["Client needs update."] = "El cliente necesita una actualización.", + ["Close"] = "Cerrar", + ["Close this channel"] = "Cerrar este canal", + ["Club Fighting"] = "Combate con mazo", + ["Combat Controls"] = "Controles de combate", + ["Comment:"] = "Comentario:", + ["Connecting to game server..."] = "Conectando a servidor game...", + ["Connecting to login server..."] = "Conectando a servidor login...", + ["Console"] = "Consola", + ["Cooldowns"] = "Descansos", + ["Copy message"] = "Copiar mensaje", + ["Copy name"] = "Copiar nombre", + ["Copy Name"] = "Copiar nombre", + ["Create Map Mark"] = "Crear marca en mapa", + ["Create mark"] = "Crear marca", + ["Create New Offer"] = "Crear nueva oferta", + ["Create Offer"] = "Crear oferta", + ["Current hotkeys:"] = "Actuales hotkeys:", + ["Current hotkey to add: %s"] = "Actuales hotkeys a agregar: %s", + ["Current Offers"] = "Oferta actual", + ["Default"] = "Predeterminado", + ["Delete mark"] = "Borrar Marca", + ["Description:"] = "Descripción:", + ["Description"] = "Descripción", + ["Destructive Behaviour"] = "Comportamiento destructivo", + ["Detail"] = "Detalle", + ["Details"] = "Detalles", + ["Disable Shared Experience"] = "Desactivar experiencia compartida", + ["Dismount"] = "Desmontar", + ["Display connection speed to the server (milliseconds)"] = "Mostrar velocidad de conexión en el servidor (millisegundos)", + ["Distance Fighting"] = "Combate a distancia", + ["Don\'t stretch/shrink Game Window"] = "No estirar ni reducir el tamańo de ventana", + ["Edit hotkey text:"] = "Editar texto de hotkey:", + ["Edit List"] = "Editar lista", + ["Edit Text"] = "Editar texto", + ["Enable music"] = "Habilitar música", + ["Enable Shared Experience"] = "Habilitar experiencia compartida", + ["Enable smart walking"] = "Habilitar caminado inteligente", + ["Enable vertical synchronization"] = "Habilitar sincronización vertical", + ["Enable walk booster"] = "Habilitar caminado turbo", + ["Enter Game"] = "Entrar al juego", + ["Enter one name per line."] = "Introducir un nombre por linea.", + ["Enter with your account again to update your client."] = "Ingrese con su cuenta nuevamente para actualizar el cliente.", + ["Error"] = "Error", + ["Error"] = "Error", + ["Excessive Unjustified Player Killing"] = "Asesinato excesivo injustificado de jugadores", + ["Exclude from private chat"] = "Ejecutar desde un canal privado", + ["Exit"] = "Salir", + ["Experience"] = "Experiencia", + ["Filter list to match your level"] = "Lista de filtros que coincida con el nivel", + ["Filter list to match your vocation"] = "Lista de filtros que coincida con el vocación", + ["Find:"] = "Encontrar:", + ["Fishing"] = "Pesca", + ["Fist Fighting"] = "Combate con puńos", + ["Follow"] = "Seguir", + ["Force Exit"] = "Forzar salida", + ["For Your Information"] = "Para tu información", + ["Free Account"] = "Cuenta gratis", + ["Fullscreen"] = "Pantalla completa", + ["Game"] = "Juego", + ["Game framerate limit: %s"] = "Limite de cuadros por segundo en el juego: %s", + ["Graphics"] = "Gráficos", + ["Graphics card driver not detected"] = "Controlador de tarjeta gráfica de video no detectado", + ["Graphics Engine:"] = "Motor Gráfico:", + ["Head"] = "Cabeza", + ["Healing"] = "Curación", + ["Health Info"] = "HP Info",--This can be better + ["Health Information"] = "HP Información",--This can be better + ["Hide monsters"] = "Ocultar monstruos", + ["Hide non-skull players"] = "Ocultar jugadores sin skull", + ["Hide Npcs"] = "Ocultar NPCs", + ["Hide Offline"] = "Ocultar fuera de linea", + ["Hide party members"] = "Ocultar miembros del party", + ["Hide players"] = "Ocultar players", + ["Hide spells for higher exp. levels"] = "Ocultar hechizos para niveles mas altos que tu experiencia.", + ["Hide spells for other vocations"] = "Ocultar hechizos que sean para otra vocación", + ["Hit Points"] = "Puntos de vida", + ["Hold left mouse button to navigate\nScroll mouse middle button to zoom\nRight mouse button to create map marks"] = "Mantenga presionado el botón derecho del ratón para navegar\nDezplaze la rueda central del ratón para ampliar\nbotón derecho del mouse para crear marcas del mapa", + ["Hotkeys"] = "Hotkeys", + ["If you shut down the program, your character might stay in the game.\nClick on 'Logout' to ensure that you character leaves the game properly.\nClick on 'Exit' if you want to exit the program without logging out your character."] = "Si se cierra el programa, tu personaje puede permanecer en el juego.\nHaga clic en 'Salir' para asegurarse de que personaje deja el juego correctamente.\nHaga click en 'Salir' si desea salir del programa sin tener que salir de tu personaje.", + ["Ignore"] = "Ignorar", + ["Ignore capacity"] = "Ignorar Capacidad", + ["Ignored players:"] = "Jugadores ignorados:", + ["Ignore equipped"] = "Ignorar lo equipado", + ["Ignore List"] = "Ignorar lista", + ["Ignore players"] = "Ignorar jugadores", + ["Ignore Private Messages"] = "Ignorar mensajes privados", + ["Ignore Yelling"] = "Ignorar gritos", + ["Interface framerate limit: %s"] = "Interface de cuadros por segundo: %s", + ["Inventory"] = "Inventario", + ["Invite to Party"] = "Ivitar al party", + ["Invite to private chat"] = "Invitar a canal privado", + ["IP Address Banishment"] = "Banishment - Dirección IP", + ["Item Offers"] = "Ofertas de objetos", + ["It is empty."] = "Está vació.", + ["Join %s\'s Party"] = "Unirse al party de %s ", + ["Leave Party"] = "Dejar el party", + ["Level"] = "Nivel", + ["Lifetime Premium Account"] = "Tiempo de Premium Account infinito", + ["Limits FPS to 60"] = "Limites FPS a 60", + ["List of items that you're able to buy"] = "Lista de objetos que puedes de comprar", + ["List of items that you're able to sell"] = "Lista de objetos que puedes de vender", + ["Load"] = "Cargar", + ["Logging out..."] = "Cerrando sesión...", + ["Login"] = "Ingresar", + ["Login Error"] = "Error de ingreso", + ["Login Error"] = "Error de ingreso", + ["Logout"] = "Salir", + ["Look"] = "Mirar", + ["Magic Level"] = "Nivel mágico", + ["Make sure that your client uses\nthe correct game protocol version"] = "Asegúrese de que el cliente este utilizando\nes el versión del protocolo adecuado", + ["Mana"] = "Mana", + ["Manage hotkeys:"] = "Administrador de hotkeys:", + ["Market"] = "Mercado", + ["Market Offers"] = "Ofertas en mercado", + ["Message of the day"] = "Mensaje del día", + ["Message to "] = "Mensaje a", + ["Message to %s"] = "Mensaje a %s", + ["Minimap"] = "Minimapa", + ["Module Manager"] = "Administrador de módulos", + ["Module name"] = "Nombre del modulo", + ["Mount"] = "Montar", --Unique name? + ["Move Stackable Item"] = "Mover objeto apilable", + ["Move up"] = "Mover arriba", + ["My Offers"] = "Mis ofertas", + ["Name:"] = "Nombre:", + ["Name Report"] = "Name Report", + ["Name Report + Banishment"] = "Name Report + Banishment", + ["Name Report + Banishment + Final Warning"] = "Name Report + Banishment + Final Warning", + ["No"] = "No", + ["No graphics card detected, everything will be drawn using the CPU,\nthus the performance will be really bad.\nPlease update your graphics driver to have a better performance."] = "No se ha detectado una tarjeta gráfica y todo sera procesado por tu procesador,\npor lo tanto el rendimiento va a ser muy malo.\nPor favor, actualice su controlador de gráficos para tener un rendimiento optimo.", + ["No item selected."] = "No hay elemento seleccionado.", + ["No Mount"] = "No montura", + ["No Outfit"] = "No outfit", + ["No statement has been selected."] = "No hay comentario seleccionado.", + ["Notation"] = "Notation", + ["NPC Trade"] = "Intercambio con NPC", + ["Offer History"] = "Historial de oferta", + ["Offers"] = "Ofertas", + ["Offer Type:"] = "Tipo de oferta:", + ["Offline Training"] = "Entrenamiento offLine", + ["Ok"] = "OK", + ["on %s.\n"] = "en %s.\n", + ["Open"] = "Abierto", + ["Open a private message channel:"] = "Abrir mensaje en canal privado:", + ["Open charlist automatically when starting client"] = "Abrir lista de jugadores automáticamente al iniciar el cliente", + ["Open in new window"] = "Abrir en nueva ventana", + ["Open new channel"] = "Abrir nuevo canal", + ["Options"] = "Opciones", + ["Overview"] = "Descripción", + ["Pass Leadership to %s"] = "Pasar liderazgo a %s", + ["Password"] = "Contraseńa", + ["Piece Price:"] = "Precio por pieza:", + ["Please enter a character name:"] = "Por favor ingresar nombre del jugador:", + ["Please, press the key you wish to add onto your hotkeys manager"] = "Por favor, presiona la tecla que desees para que sea registrada en\nel administrador de hotkeys", + ["Please Select"] = "Por favor seleccione", + ["Please use this dialog to only report bugs. Do not report rule violations here!"] = "Por favor usa este diálogo solo para reportar errores.\nˇNo reportar violaciones del reglamento aquí!", + ["Please wait"] = "Por favor espere", + ["Port"] = "Puerto", + ["Position:"] = "Posición:", + ["Position: %i %i %i"] = "Posición: %i %i %i", + ["Premium Account (%s) days left"] = "Tienes (%s) días de Premium Account restantes", + ["Price:"] = "Precio:", + ["Primary"] = "Primario", + ["Protocol"] = "Protocolo", + ["Quest Log"] = "Quest Log", --Unique name + ["Randomize"] = "Combinar", + ["Randomize characters outfit"] = "Combinar vestimenta del jugador", + ["Reason:"] = "Razón:", + ["Refresh"] = "Refrescar", + ["Refresh Offers"] = "Refrescar ofertas", + ["Regeneration Time"] = "Tiempo de regeneración", + ["Reject"] = "Rechazar", + ["Reload All"] = "Cargar todo de nuevo", + ["Remember account and password when starts client"] = "Recordar cuenta y contraseńa al iniciar el cliente", + ["Remember password"] = "Recordar contraseńa", + ["Remove"] = "Remover", + ["Remove %s"] = "Remover %s", + ["Report Bug"] = "Reportar error", + ["Reserved for more functionality later."] = "Reservado para una función futura.", + ["Reset Market"] = "Reiniciar mercado", + ["Revoke %s\'s Invitation"] = "Anular %s\'s invitación", + ["Rotate"] = "Rotar", + ["Rule Violation"] = "Violación del reglamento", + ["Save"] = "Guardar", + ["Save Messages"] = "Guardar mensaje", + ["Search:"] = "Buscar:", + ["Search all items"] = "Buscar todos los objetos", + ["Secondary"] = "Secundario", + ["Select object"] = "Seleccionar objeto", + ["Select Outfit"] = "Seleccionar outfit", + ["Select your language"] = "Selectionar tu lenguaje", + ["Sell"] = "Vender", + ["Sell Now"] = "Vender ahora", + ["Sell Offers"] = "Ofertas de venta", + ["Send"] = "Enviar", + ["Send automatically"] = "Enviar automáticamente", + ["Send Message"] = "Enviar mensaje", + ["Server"] = "Server", --Unique name + ["Server Log"] = "Server Log", --Unique name + ["Set Outfit"] = "Escoger vestimenta", + ["Shielding"] = "Escudo", + ["Show all items"] = "Mostrar todos los objetos", + ["Show connection ping"] = "Mostrar ping de conexión", + ["Show Depot Only"] = "Mostrar solo el Depot", + ["Show event messages in console"] = "Mostrar mensajes de evento en consola", + ["Show frame rate"] = "Mostrar información de cuadros por secundo", + ["Show info messages in console"] = "Mostrar mensajes de información en consola", + ["Show left panel"] = "Mostrar panel izquierdo", + ["Show levels in console"] = "Mostrar niveles en consola", + ["Show Offline"] = "Mostrar Desconectados", + ["Show private messages in console"] = "Mostrar mensajes privados en consola", + ["Show private messages on screen"] = "Mostrar mensajes privados en pantalla", + ["Show Server Messages"] = "Mostrar mensajes del servidor", + ["Show status messages in console"] = "Mostrar mensajes de estado en consola", + ["Show Text"] = "Mostrar texto", + ["Show timestamps in console"] = "Mostrar marcas de tiempo en consola", + ["Show your depot items only"] = "Mostrar solo tus objetos en depot", + ["Skills"] = "Habilidades", + ["Soul"] = "Soul", + ["Soul Points"] = "Puntos de Soul", --I'm leaving these as is because its a unique name, if you want to change it it can be "Alma" or "Espíritu" + ["Special"] = "Especial", + ["Speed"] = "Velocidad", + ["Spell Cooldowns"] = "Spells Cooldowns", --Could be "Tiempo de recarga para los hechizos". + ["Spell List"] = "Lista de hechizos", + ["Stamina"] = "Resistencia", + ["Statement:"] = "Comentario:", + ["Statement Report"] = "Statement Report", --Could be "reporte del comentario" + ["Statistics"] = "Estadísticas", + ["Stop Attack"] = "Detener ataque", + ["Stop Follow"] = "Detener persecución", + ["Support"] = "Soporte", + ["%s: (use object)"] = "%s: (usar objeto)", + ["%s: (use object on target)"] = "%s: (usar objeto en un objetivo)", + ["%s: (use object on yourself)"] = "%s: (usar objeto en mi mismo)", + ["%s: (use object with crosshair)"] = "%s: (usar objeto con punto de mira)", + ["Sword Fighting"] = "Combate de espada", + ["Terminal"] = "Terminal", + ["There is no way."] = "No hay ninguna manera.", + ["Title"] = "Titulo", + ["Total Price:"] = "Total total:", + ["Trade"] = "Intercambio", + ["Trade with ..."] = "Intercambiar con ...", + ["Trying to reconnect in %s seconds."] = "", + ["Unable to load dat file, please place a valid dat in '%s'"] = "No se puede cargar el archivo dat, por favor coloque un dat válido en '%s'", + ["Unable to load spr file, please place a valid spr in '%s'"] = "No se puede cargar el archivo spr, por favor coloque un spr válido en '%s'", + ["Unable to logout."] = "No se puede cerrar sesión-", + ["Unignore"] = "Dejar de ignorar", + ["Unload"] = "No cargado", + ["Update needed"] = "Es necesario actualizar", + ["Use"] = "Uso", + ["Use on target"] = "Usar en un objetivo", + ["Use on yourself"] = "Usar en mi mismo", + ["Use with ..."] = "Usar en ...", + ["Version"] = "Versión", + ["VIP List"] = "Lista Vip", + ["Voc."] = "Voc.", + ["Vocation"] = "Vocación", + ["Waiting List"] = "Lista de espera", + ["Website"] = "Sitio Web", + ["Weight:"] = "Peso:", + ["Will detect when to use diagonal step based on the\nkeys you are pressing"] = "Detectara cuando se camina en diagonal usando las flechas", + ["With crosshair"] = "Con punto de mira", + ["Yes"] = "Si", + ["You are bleeding"] = "Te estas desangrando", + ["You are burning"] = "Te estas quemando", + ["You are cursed"] = "Tu estas maldecido", + ["You are dazzled"] = "Tu estas deslumbrado", + ["You are dead."] = "Tu estas muerto.", + ["You are dead"] = "Tu estas muerto", + ["You are drowning"] = "Te estas ahogando", + ["You are drunk"] = "Tu estas ebrio", + ["You are electrified"] = "Tu estas electrocutado", + ["You are freezing"] = "Te estas congelando", + ["You are hasted"] = "Tu estas rápido", --I dont know what is the best way to translate this so I'm leaving it as I found it. + ["You are hungry"] = "Tu estas hambriento", + ["You are paralysed"] = "Tu estas paralizado", + ["You are poisoned"] = "Tu estas envenedado", + ["You are protected by a magic shield"] = "Tu estas protegido por un escudo mágico", + ["You are strengthened"] = "Tu estas reforzado", + ["You are within a protection zone"] = "Tu estas dentro de una zona de protección", + ["You can enter new text."] = "Tu puedes ingresar un texto nuevo.", + ["You have %s percent"] = "Tu tienes %s por ciento", + ["You have %s percent to go"] = "Tu tienes %s por ciento para ir", + ["You may not logout during a fight"] = "No puedes salir durante una pelea", + ["You may not logout or enter a protection zone"] = "No puedes salir o entrar en una zona de protección", + ["You must enter a comment."] = "Debes ingresar un comentario.", + ["You must enter a valid server address and port."] = "Debes ingresar una dirección válida de servidor y el puerto.", + ["You must select a character to login!"] = "ˇDebes seleccionar un personaje para ingresar!", + ["Your Capacity:"] = "Tu capacidad:", + ["You read the following, written by \n%s\n"] = "Lees lo siguiente, escrito por \n%s\n", + ["You read the following, written on \n%s.\n"] = "Lees lo siguiente, escrito en \n%s\n", + ["Your Money:"] = "Tu dinero:", + ["Change language"] = "Cambiar idioma", + ["Don't stretch or shrink Game Window"] = "No estirar o encoger Ventana de Juego" + } +} + +modules.client_locales.installLocale(locale) \ No newline at end of file diff --git a/data/locales/pl.lua b/data/locales/pl.lua new file mode 100644 index 0000000..a7c3813 --- /dev/null +++ b/data/locales/pl.lua @@ -0,0 +1,419 @@ +locale = { + name = "pl", + languageName = "Polski", + + formatNumbers = true, + decimalSeperator = '.', + thousandsSeperator = ' ', + + translation = { + ["1a) Offensive Name"] = "1a) Obrazliwe Imie", + ["1b) Invalid Name Format"] = "1b) Niepoprawny Format Imienia", + ["1c) Unsuitable Name"] = "1c) Nieodpowiednie Imie", + ["1d) Name Inciting Rule Violation"] = "1d) Imie Nawolujace Do Lamania Regulaminu", + ["2a) Offensive Statement"] = "2a) Obrazliwa wypowiedz", + ["2b) Spamming"] = "2b) Spamowanie", + ["2c) Illegal Advertising"] = "2c) Nielegalne Reklamy", + ["2d) Off-Topic Public Statement"] = "2d) Publiczne Wypowiadanie Sie Nie Na Temat", + ["2e) Non-English Public Statement"] = "2e) Publiczne wypowiadanie Sie W Jezyku Innym Niz Angielski", + ["2f) Inciting Rule Violation"] = "2f) Nawolywanie Do Lamania Regulaminu ", + ["3a) Bug Abuse"] = "3a) Wykorzystywanie Bledow", + ["3b) Game Weakness Abuse"] = "3b) Wykorzystanie Slabosci Gry", + ["3c) Using Unofficial Software to Play"] = "3c) Gra Przy Uzyciu Nieoficjalnego Oprogramowania", + ["3d) Hacking"] = "3d) Wlamywanie Sie", + ["3e) Multi-Clienting"] = "3e) Uzycie Multi-Klienta", + ["3f) Account Trading or Sharing"] = "3f) Handel Lub Udostepnianie Kont", + ["4a) Threatening Gamemaster"] = "4a) Grozby Pod Adresem Mistrza Gry", + ["4b) Pretending to Have Influence on Rule Enforcement"] = "4b) Udawanie Wplywu na Ustanawianie Regul Gry", + ["4c) False Report to Gamemaster"] = "4c) Wyslanie Falszywego Raportu Mistrzowi Gry", + ["Accept"] = "Akceptuj", + ["Account name"] = "Numer konta", + ["Account Status:"] = "Status Konta:", + ["Action:"] = "Akcja:", + ["Add"] = "Dodaj", + ["Add new VIP"] = "Nowy VIP", + ["Addon 1"] = "Addon 1", + ["Addon 2"] = "Addon 2", + ["Addon 3"] = "Addon 3", + ["Add to VIP list"] = "Dodaj do VIPow", + ["Adjust volume"] = "Zmien glosnosc", + ["Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nSimply click on Ok to resume your journeys!"] = false, + ["All"] = "Wszystkie", + ["All modules and scripts were reloaded."] = "Wszystkie moduly ", + ["Allow auto chase override"] = false, + ["Also known as dash in tibia community, recommended\nfor playing characters with high speed"] = false, + ["Ambient light: %s%%"] = "Swiatlo tla: %s%%", + ["Amount:"] = "Ilosc:", + ["Amount"] = "Ilosc", + ["Anonymous"] = "Anonimowy", + ["Are you sure you want to logout?"] = "Czy jestes pewien, ze sie chcesz wylogowac?", + ["Attack"] = "Atak", + ["Author"] = "Autor", + ["Autoload"] = "Autoladowanie", + ["Autoload priority"] = "Priorytet autoladowania", + ["Auto login"] = "Loguj automatycznie", + ["Auto login selected character on next charlist load"] = "Automatycznie zaloguj wybrana postac podczas kolejnego ladowaia listy postaci", + ["Axe Fighting"] = "Walka toporem", + ["Balance:"] = false, + ["Banishment"] = false, + ["Banishment + Final Warning"] = false, + ["Battle"] = "Bitwa", + ["Browse"] = "Przegladaj", + ["Bug report sent."] = "Raport o bledzie zostal wyslany.", + ["Button Assign"] = "Przypisanie Klawisza", + ["Buy"] = "Kup", + ["Buy Now"] = "Kup Teraz", + ["Buy Offers"] = "Oferty Kupna", + ["Buy with backpack"] = "Kupuj z plecakami", + ["Cancel"] = "Anuluj", + ["Cannot login while already in game."] = "Nie mozna zalogowac gdy juz w grze", + ["Cap"] = "Ladownosc", + ["Capacity"] = "Ladownosc", + ["Center"] = "Wysrodkuj", + ["Channels"] = "Kanaly", + ["Character List"] = "Lista postaci", + ["Classic control"] = "Klasyczne sterowaie", + ["Clear current message window"] = "Wyczysc bierzace okno", + ["Clear Messages"] = "Wyczysc wiadomosci", + ["Clear object"] = "Wyczysc obiekt", + ["Client needs update."] = "Klient wymaga aktalizacji", + ["Close"] = "Zamknij", + ["Close this channel"] = "Zamknij kanal", + ["Club Fighting"] = "Walka obuchem", + ["Combat Controls"] = "Kontrola walki", + ["Comment:"] = "Komentarz:", + ["Connecting to game server..."] = "Laczenie z serwerem gry...", + ["Connecting to login server..."] = "Laczenie z serwerem logowania...", + ["Console"] = "Konsola", + ["Cooldowns"] = "Czasy odnowienia", + ["Copy message"] = "Kopiuj wiadomosc", + ["Copy name"] = "Kopiuj imie", + ["Copy Name"] = "Kopiuj Imie", + ["Create Map Mark"] = "Utworz Znacznik na Mapie", + ["Create mark"] = "Utworz znacznik", + ["Create New Offer"] = "Utworz Nowa Oferte", + ["Create Offer"] = "Utworz Oferte", + ["Current hotkeys:"] = "Aktualny hotkey:", + ["Current hotkey to add: %s"] = "Aktualny hotkey do dodania: %s", + ["Current Offers"] = "Obecne Oferty", + ["Default"] = "Domyslny", + ["Delete mark"] = "Usun znacznik", + ["Description:"] = "Opis:", + ["Description"] = "Opis", + ["Destructive Behaviour"] = "Destrukcyjne Zachowanie", + ["Detail"] = "Szczegoly", + ["Details"] = "Szczegoly", + ["Disable Shared Experience"] = "Wylacz Dzielenie Doswiadczenia", + ["Dismount"] = "Zejdz z wierzchowca", + ["Display connection speed to the server (milliseconds)"] = "Wyswietl ping do serwera (ms)", + ["Distance Fighting"] = "Walka na odleglosc", + ["Don't stretch/shrink Game Window"] = "Nie rozszerzaj/zwezaj Okna Gry", + ["Edit hotkey text:"] = "Edytuj tresc hotkeya:", + ["Edit List"] = "Lista Edycji", + ["Edit Text"] = "Edytuj tekst", + ["Enable music"] = "Odtwarzaj muzyke", + ["Enable Shared Experience"] = "Wlacz dzielenie doswiadczenia", + ["Enable smart walking"] = "Wlacz inteligentne chodzenie", + ["Enable vertical synchronization"] = "Wlacz synchronizacje pionowa", + ["Enable walk booster"] = false, + ["Enter Game"] = "Wejdz do gry", + ["Enter one name per line."] = "Wprowadz jedno imie na linie", + ["Enter with your account again to update your client."] = "Zaloguj sie ponownie by zaktualizowac klienta", + ["Error"] = "Blad", + ["Excessive Unjustified Player Killing"] = "Nadmierne Nieusprawiedliwione Zabijanie Graczy", + ["Exclude from private chat"] = "Wyrzuc w prywatnej konwersacji", + ["Exit"] = "Wyjdz", + ["Experience"] = "Doswiadczenie", + ["Filter list to match your level"] = "Wyswietl tylko odpowiednie dla mojego poziomu", + ["Filter list to match your vocation"] = "Wyswietl tylko odpowiednie dla mojej klasy", + ["Find:"] = "Szukaj:", + ["Fishing"] = "Wedkarstwo", + ["Fist Fighting"] = "Walka wrecz", + ["Follow"] = "Podazaj", + ["Force Exit"] = "Wymus Zamkniecie", + ["For Your Information"] = "Dla twojej informacji", + ["Free Account"] = "Darmowe Konto", + ["Fullscreen"] = "Pelen ekran", + ["Game"] = "Gra", + ["Game framerate limit: %s"] = "Limit FPS: %s", + ["Graphics"] = "Grafika", + ["Graphics card driver not detected"] = "Nie wykryto karty graficznej", + ["Graphics Engine:"] = "Silnik graficzny:", + ["Head"] = "Glowa", + ["Healing"] = "Leczenie", + ["Health Info"] = "Info o zyciu", + ["Health Information"] = "Informacje o zyciu", + ["Hide monsters"] = "Ukryj potwory", + ["Hide non-skull players"] = "Ukryj graczy bez skulla", + ["Hide Npcs"] = "Ukryj NPCe", + ["Hide Offline"] = "Ukryj Niedostepnych", + ["Hide party members"] = "Ukryj czlonkow druzyny", + ["Hide players"] = "Ukryj graczy", + ["Hide spells for higher exp. levels"] = "Ukryj zaklecia wyzszych poziomow postaci", + ["Hide spells for other vocations"] = "Ukryj zaklecia innych klas", + ["Hit Points"] = "Punkty uderzen", + ["Hold left mouse button to navigate\nScroll mouse middle button to zoom\nRight mouse button to create map marks"] = false, + ["Hotkeys"] = "Hotkeye", + ["If you shut down the program, your character might stay in the game.\nClick on 'Logout' to ensure that you character leaves the game properly.\nClick on 'Exit' if you want to exit the program without logging out your character."] = false, + ["Ignore"] = "Ignoruj", + ["Ignore capacity"] = "Ignoruj pojemnosc", + ["Ignored players:"] = "Ignorowani gracze:", + ["Ignore equipped"] = "Ignoruj ekwipunek", + ["Ignore List"] = "Lista Ignorowanych", + ["Ignore players"] = "Ignoruj graczy", + ["Ignore Private Messages"] = "Ignoruj Prywatne Wiadomosci", + ["Ignore Yelling"] = "Ignoruj Krzyki", + ["Interface framerate limit: %s"] = "Limit FPS interfejsu: %s", + ["Inventory"] = "Ekwipunek", + ["Invite to Party"] = "Zapros do druzyny", + ["Invite to private chat"] = "Zapros do prywatnej konwersacji", + ["IP Address Banishment"] = "Blokada adresu IP", + ["Item Offers"] = "Oferty Przedmiotow", + ["It is empty."] = "To jest puste.", + ["Join %s's Party"] = "Dolacz do druzyny gracza %s", + ["Leave Party"] = "Opusc druzyne", + ["Level"] = "Poziom", + ["Lifetime Premium Account"] = "Konto Premium na Stale", + ["Limits FPS to 60"] = "Ogranicz FPS do 60", + ["List of items that you're able to buy"] = "Lista przedmiotow, ktore mozesz kupic", + ["List of items that you're able to sell"] = "Lista przedmiotow, ktore mozesz sprzedac", + ["Load"] = "Wczytaj", + ["Logging out..."] = "Wylogowuje...", + ["Login"] = "Zaloguj", + ["Login Error"] = "Blad Logowania", + ["Login Error"] = "Blad Logowania", + ["Logout"] = "Wyloguj", + ["Look"] = "Spojrz", + ["Magic Level"] = "Poziom Magiczny", + ["Make sure that your client uses\nthe correct game protocol version"] = "Upewnij sie, ze twoj klient\nuzywa wlasciwego protokolu gry.", + ["Mana"] = "Mana", + ["Manage hotkeys:"] = "Zarzadzaj hotkeyami:", + ["Market"] = false, + ["Market Offers"] = "Oferty", + ["Message of the day"] = "Wiadomosc dnia", + ["Message to "] = "Wiadomosc do ", + ["Message to %s"] = "Wiadomosc do %s", + ["Minimap"] = "Minimapa", + ["Module Manager"] = "Menedzer modulow", + ["Module name"] = "Nazwa modulu", + ["Mount"] = "Wierzchowiec", + ["Move Stackable Item"] = "Przenies przedmiot", + ["Move up"] = "Przenies wyzej", + ["My Offers"] = "Moje Oferty", + ["Name:"] = "Nazwa:", + ["Name Report"] = false, + ["Name Report + Banishment"] = false, + ["Name Report + Banishment + Final Warning"] = false, + ["No"] = "Nie", + ["No graphics card detected, everything will be drawn using the CPU,\nthus the performance will be really bad.\nPlease update your graphics driver to have a better performance."] = false, + ["No item selected."] = "Nie wybrano przedmiotu.", + ["No Mount"] = "Brak wierzchowca", + ["No Outfit"] = "Brak stroju", + ["No statement has been selected."] = false, + ["Notation"] = false, + ["NPC Trade"] = "Handel NPC", + ["Offer History"] = "Historia Ofert", + ["Offers"] = "Oferty", + ["Offer Type:"] = "Typ Oferty:", + ["Offline Training"] = "Trening Offline", + ["Ok"] = "Ok", + ["on %s.\n"] = false, + ["Open"] = "Otworz", + ["Open a private message channel:"] = "Otworz prywatny kanal:", + ["Open charlist automatically when starting client"] = "Automatycznie otworz liste postaci przy starcie gry", + ["Open in new window"] = "Otworz w nowym oknie", + ["Open new channel"] = "Otworz nowy kanal", + ["Options"] = "Opcje", + ["Overview"] = "Podsumowanie", + ["Pass Leadership to %s"] = "Oddaj przywodztwo %s", + ["Password"] = "Haslo", + ["Piece Price:"] = "Cena jednego przedmiotu", + ["Please enter a character name:"] = "Podaj nazwe postaci:", + ["Please, press the key you wish to add onto your hotkeys manager"] = "Nacisnij klawisz, ktory chcesz dodac do menedzera skrotow klawiszowych", + ["Please Select"] = "Prosze wybrac", + ["Please use this dialog to only report bugs. Do not report rule violations here!"] = "Zglaszaj tylko bledy gry, nie lamanie zasad", + ["Please wait"] = "Prosze czekac", + ["Port"] = "Port", + ["Position:"] = "Pozycja:", + ["Position: %i %i %i"] = "Pozycja: %i %i %i", + ["Premium Account (%s) days left"] = "Konto Premium (%s) dni", + ["Price:"] = "Cena:", + ["Primary"] = "Podstawowy", + ["Protocol"] = "Protokol", + ["Quest Log"] = "Dziennik Misji", + ["Randomize"] = "Losuj", + ["Randomize characters outfit"] = "Ustaw losowy wyglad", + ["Reason:"] = "Powod:", + ["Refresh"] = "Odswiez", + ["Refresh Offers"] = "Odswiez Oferty", + ["Regeneration Time"] = "Czas Regeneracji", + ["Reject"] = "Odrzuc", + ["Reload All"] = "Zaladuj ponownie wszystko", + ["Remember account and password when starts client"] = "Zapamietaj identyfikator konta oraz haslo", + ["Remember password"] = "Zapamietaj haslo", + ["Remove"] = "Usun", + ["Remove %s"] = "Usun %s", + ["Report Bug"] = "Zglos Blad", + ["Reserved for more functionality later."] = "Zarezerowane dla przyszlych funkcjonalnosci.", + ["Reset Market"] = "Zaladuj market ponownie", + ["Revoke %s's Invitation"] = "Odmow na zaproszenie gracza %s", + ["Rotate"] = "Obroc", + ["Rule Violation"] = "Zlamanie Regul", + ["Save"] = "Zapisz", + ["Save Messages"] = "Zapisz Wiadomosci", + ["Search:"] = "Szukaj:", + ["Search all items"] = "Znajdz wszystkie przedmioty", + ["Secondary"] = "Drugorzedny", + ["Select object"] = "Wybierz obiekt", + ["Select Outfit"] = "Wybierz outfit", + ["Select your language"] = "Wybierz jezyk", + ["Sell"] = "Sprzedaj", + ["Sell Now"] = "Sprzedaj Teraz", + ["Sell Offers"] = "Oferty Sprzedazy", + ["Send"] = "Wyslij", + ["Send automatically"] = "Wyslij automatycznie", + ["Send Message"] = "Wyslij Wiadomosc", + ["Server"] = "Serwer", + ["Server Log"] = "Log Serwera", + ["Set Outfit"] = "Ustaw outfit", + ["Shielding"] = "Obrona tarcza", + ["Show all items"] = "Pokaz wszystkie przedmioty", + ["Show connection ping"] = "Wyswietl ping", + ["Show Depot Only"] = "Pokaz tylko przedmioty z depozytu", + ["Show event messages in console"] = "Pokaz wydarzenia w konsoli", + ["Show frame rate"] = "Pokaz FPS", + ["Show info messages in console"] = "Pokaz informacje w konsoli", + ["Show left panel"] = "Pokaz lewy panel", + ["Show levels in console"] = "Pokaz poziomy w konsoli", + ["Show Offline"] = "Pokaz Niedostepnych", + ["Show private messages in console"] = "Pokaz prywatne wiadomosci w konsoli", + ["Show private messages on screen"] = "Pokaz prywatne wiadomosci na ekranie", + ["Show Server Messages"] = "Pokaz Wiadomosci Serwera", + ["Show status messages in console"] = "Pokaz status w konsoli", + ["Show Text"] = "Pokaz Tekst", + ["Show timestamps in console"] = "Pokaz znaczniki czasu w konsoli", + ["Show your depot items only"] = "Pokaz tylko przedmioty z depozytu", + ["Skills"] = "Umiejetnosci", + ["Soul"] = "Dusze", + ["Soul Points"] = "Punktey Duszy", + ["Special"] = "Specialne", + ["Speed"] = "Predkosc", + ["Spell Cooldowns"] = "Czas odnowienia czaru", + ["Spell List"] = "Lista Zaklec", + ["Stamina"] = "Wytrzymalosc", + ["Statement:"] = "Wyrazenie", + ["Statement Report"] = "Reportuj wyrazenie", + ["Statistics"] = "Statystki", + ["Stop Attack"] = "Anuluj atak", + ["Stop Follow"] = "Przestan podazac", + ["Support"] = "Wsparcie", + ["%s: (use object)"] = "%s: (uzyj obiekt)", + ["%s: (use object on target)"] = "%s: (uzyj obiektu na celu)", + ["%s: (use object on yourself)"] = "%s: (uzyj obiektu na sobie)", + ["%s: (use object with crosshair)"] = "%s: (uzyj obiektu z celownikiem)", + ["Sword Fighting"] = "Atak mieczem", + ["Terminal"] = "Terminal", + ["There is no way."] = "Nie ma drogi.", + ["Title"] = "Tytul", + ["Total Price"] = "Cena calosci", + ["Trade"] = "Handel", + ["Trade with ..."] = "Handluj z ...", + ["Trying to reconnect in %s seconds."] = "Ponowna proba laczenia za %s sekund.", + ["Unable to load dat file, please place a valid dat in '%s'"] = "Nie mozna zaladowac pliku .dat z '%s'", + ["Unable to load spr file, please place a valid spr in '%s'"] = "Nie mozna zaladowac pliku .spr z '%s'", + ["Unable to logout."] = "Nie mozna sie wylogowac.", + ["Unignore"] = "Anuluj Ignorowanie", + ["Unload"] = "Wylacz", + ["Update needed"] = "Wymagana aktualizacja", + ["Use"] = "Uzyj", + ["Use on target"] = "Uzyj na celu", + ["Use on yourself"] = "Uzyj na sobie", + ["Use with ..."] = "Uzyj z ...", + ["Version"] = "Wersja", + ["VIP List"] = "Lista VIP", + ["Voc."] = "Profesja", + ["Vocation"] = "Profesja", + ["Waiting List"] = "Lista Oczekujacych", + ["Website"] = "Strona:", + ["Weight:"] = "Waga:", + ["Will detect when to use diagonal step based on the\nkeys you are pressing"] = false, + ["With crosshair"] = "Z celownikiem", + ["Yes"] = "Tak", + ["You are bleeding"] = "Krwawisz", + ["You are burning"] = "Palisz sie", + ["You are cursed"] = "Jestes przeklety", + ["You are dazzled"] = "Jestes oslepiony", + ["You are dead."] = "Jestes martwy.", + ["You are dead"] = "Jestes martwy", + ["You are drowning"] = "Topisz sie", + ["You are drunk"] = "Jestes pijany", + ["You are electrified"] = "Jestes porazony pradem", + ["You are freezing"] = "Zamarzasz", + ["You are hasted"] = "Masz zwiekszona predkosc ruchu", + ["You are hungry"] = "Jestes glodny", + ["You are paralysed"] = "Jestes sparalizowany", + ["You are poisoned"] = "Jestes zatruty", + ["You are protected by a magic shield"] = "Jestes chroniony magiczna tarcza", + ["You are strengthened"] = "Jestes wzmocniony", + ["You are within a protection zone"] = "Jestes w strefie ochronnej", + ["You can enter new text."] = "Mozesz wprowadzic nowy tekst.", + ["You have %s percent"] = "Masz %s procent", + ["You have %s percent to go"] = "Brakuje Ci %s procent", + ["You may not logout during a fight"] = "Nie mozesz sie wylogowac w trakcie walki", + ["You may not logout or enter a protection zone"] = "Nie mozesz sie wylogowac ani wejsc do strefy ochronnej", + ["You must enter a comment."] = "Prosze wprowadzic komentarz", + ["You must enter a valid server address and port."] = "Prosze wprowadzic poprawny adres i port.", + ["You must select a character to login!"] = "Musisz wybrac postac aby sie zalogowac!", + ["Your Capacity:"] = "Twoja Ladownosc:", + ["You read the following, written by \n%s\n"] = false, + ["You read the following, written on \n%s.\n"] = false, + ["Your Money:"] = "Twoje pieniadze", + ["Enable dash walking"] = "Wlacz szybsze chodzenie (dash walking)", + ["Will boost your walk on high speed characters"] = "Przyspieszy poruszanie sie postaci o wysokiej predkosci", + ["Display creature names"] = "Wyswietlaj nazwy potworow", + ["Display creature health bars"] = "Wyswietlaj paski zycia potworow", + ["Display text messages"] = "Wyswietlaj wiadomosci tekstowe", + ["Change language"] = "Zmien jezyk", + ["Enable lights"] = "Wlacz oswietlenie", + ["Enable audio"] = "Wlacz dzwiek", + ["Enable music sound"] = "Wlacz muzyke", + ["Music volume: %d"] = "Glosnosc muzyki: %d", + ["Audio"] = "Dzwiek", + ["Server List"] = "Lista serwerow", + ["Server list"] = "Lista serwerow", + ["Client Version"] = "Wersja klienta", + ["Add new server"] = "Dodaj nowy serwer", + ["Select"] = "Wybierz", + ["New Server"] = "Nowy serwer", + ["Host"] = false, + ["Reset All"] = "Ustaw domyslne", + ["Disable chat mode, allow to walk using ASDW"] = "Zablokuj tryb rozmow, zezwol na poruszanie sie za pomoca klawiszy ADSW", + ["Name"] = "Imie", + ["Price"] = "Cena", + ["Your Money"] = "Twoje fundusze", + ["Weight"] = "Waga", + ["Your Capacity"] = "Twoj udzwig", + ["Search"] = "Szukaj", + ["Sell All"] = "Sprzedaj wszystko", + ["Statement"] = "Stanowisko", + ["Reason"] = "Powod", + ["Action"] = "Akcja", + ["Comment"] = "Komentarz", + ["Balance"] = "Stan konta", + ["Offer Type"] = "Typ oferty", + ["Piece Price"] = "Cena jednego", + ["Find"] = "Szukaj", + ["Formula"] = "Formula", + ["Group"] = "Groupa", + ["Type"] = "Typ", + ["Cooldown"] = "Czas odnowienia", + ["Premium"] = false, + ["Any"] = "Dowolny", + ["Sorcerer"] = "Czarodziej", + ["Druid"] = false, + ["Paladin"] = "Paladyn", + ["Knight"] = "Rycerz" + } +} + +modules.client_locales.installLocale(locale) diff --git a/data/locales/pt.lua b/data/locales/pt.lua new file mode 100644 index 0000000..e5dc3b0 --- /dev/null +++ b/data/locales/pt.lua @@ -0,0 +1,413 @@ +locale = { + name = "pt", + charset = "cp1252", + languageName = "Portuguęs", + + formatNumbers = true, + decimalSeperator = ',', + thousandsSeperator = '.', + + -- As traduçőes devem vir sempre em ordem alfabética. + translation = { + ["%d of experience per hour"] = "%d de experięncia por hora", + ["%s of experience left"] = "%s de experięncia faltando", + ["%s: (use object on target)"] = "%s: (usar objeto no alvo)", + ["%s: (use object on yourself)"] = "%s: (usar objeto em si)", + ["%s: (use object with crosshair)"] = "%s: (usar objeto com mira)", + ["%s: (use object)"] = "%s: (usar objeto)", + ["1a) Offensive Name"] = "1a) Nome ofensivo", + ["1b) Invalid Name Format"] = "1b) Nome com formato inválido", + ["1c) Unsuitable Name"] = "1c) Nome năo adequado", + ["1d) Name Inciting Rule Violation"] = "1d) Nome estimulando violaçăo de regra", + ["2a) Offensive Statement"] = "2a) Afirmaçăo ofensiva", + ["2b) Spamming"] = "2b) Spamming", + ["2c) Illegal Advertising"] = "2c) Anúncio ilegal", + ["2d) Off-Topic Public Statement"] = "2d) Afirmaçăo pública fora de contexto", + ["2e) Non-English Public Statement"] = "2e) Afirmaçăo pública em lingua năo inglesa", + ["2f) Inciting Rule Violation"] = "2f) Estimulando violaçăo de regra", + ["3a) Bug Abuse"] = "3a) Abuso de falhas", + ["3b) Game Weakness Abuse"] = "3b) Abuso de falhas no jogo", + ["3c) Using Unofficial Software to Play"] = "3c) Uso de programas ilegais para jogar", + ["3d) Hacking"] = "3d) Hacking", + ["3e) Multi-Clienting"] = "3e) Uso de mais de um cliente para jogar", + ["3f) Account Trading or Sharing"] = "3f) Troca de contas ou compartilhamento", + ["4a) Threatening Gamemaster"] = "4a) Ameaçar Gamemaster", + ["4b) Pretending to Have Influence on Rule Enforcement"] = "4b) Fingir ter influencia sobre a execuçăo de regras", + ["4c) False Report to Gamemaster"] = "4c) Relatório falso para Gamemaster", + ["Accept"] = "Aceitar", + ["Account name"] = "Nome da conta", + ["Account Status"] = "Estado da Conta", + ["Action"] = "Açăo", + ["Add new server"] = "Adicionar novo servidor", + ["Add new VIP"] = "Adicionar nova VIP", + ["Add to VIP list"] = "Adicionar a lista VIP", + ["Add"] = "Adicionar", + ["Addon 1"] = "Addon 1", + ["Addon 2"] = "Addon 2", + ["Addon 3"] = "Addon 3", + ["Adjust volume"] = "Ajustar volume", + ["Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nSimply click on Ok to resume your journeys!"] = false, + ["All modules and scripts were reloaded."] = "Todos módulos e scripts foram recarregados.", + ["All"] = "Todos", + ["Allow auto chase override"] = "Permitir sobrescrever o modo de perseguiçăo", + ["Also known as dash in tibia community, recommended\nfor playing characters with high speed"] = "Também conhecido como dash na comunidade tibiana, recomendado\npara jogar com personagem que possuam velocidade alta", + ["Ambient light: %s%%"] = "Luz ambiente: %s%%", + ["Amount"] = "Quantidade", + ["Anonymous"] = "Anônimo", + ["Any"] = "Qualquer", + ["Are you sure you want to logout?"] = "Vocę tem certeza que quer sair?", + ["Attack"] = "Atacar", + ["Audio"] = "Áudio", + ["Author"] = "Autor", + ["Auto login selected character on next charlist load"] = "Entrar automaticamente com o personagem quando reabrir a lista de personagens", + ["Auto login"] = "Entrar automaticamente", + ["Autoload priority"] = "Prioridade de carregamento", + ["Autoload"] = "Carregar automaticamente", + ["Axe Fighting"] = "Combate com Machado", + ["Balance"] = "Saldo", + ["Banishment + Final Warning"] = "Banimento + Aviso final", + ["Banishment"] = "Banimento", + ["Battle"] = "Batalha", + ["Browse"] = "Navegar", + ["Bug report sent."] = "Reporte de bug enviado.", + ["Button Assign"] = "Selecionar botăo", + ["Buy Now"] = "Comparar agora", + ["Buy Offers"] = "Ofertas de compra", + ["Buy with backpack"] = "Comprar com mochila", + ["Buy"] = "Comprar", + ["Cancel"] = "Cancelar", + ["Cannot login while already in game."] = "Năo é possivel logar enquanto já estiver jogando.", + ["Cap"] = "Cap", + ["Capacity"] = "Capacidade", + ["Center"] = "Centro", + ["Change language"] = "Trocar língua", + ["Channels"] = "Canais", + ["Character List"] = "Lista de personagens", + ["Classic control"] = "Controle clássico", + ["Clear current message window"] = "Apagar mensagens", + ["Clear Messages"] = "Limpar mensagens", + ["Clear object"] = "Limpar objeto", + ["Client needs update."] = "O client do jogo precisa ser atualizado", + ["Close this channel"] = "Fechar esse canal", + ["Close"] = "Fechar", + ["Club Fighting"] = "Combate com Porrete", + ["Combat Controls"] = "Controles de combate", + ["Comment"] = "Comentário", + ["Connecting to game server..."] = "Conectando no servidor do jogo...", + ["Connecting to login server..."] = "Conectando no servidor de autenticaçăo...", + ["Connection Error"] = "Erro de Conexăo", + ["Console"] = "Console", + ["Cooldown"] = "Cooldown", + ["Cooldowns"] = "Cooldowns", + ["Copy message"] = "Copiar mensagem", + ["Copy name"] = "Copiar nome", + ["Copy Name"] = "Copiar Nome", + ["Create Map Mark"] = "Criar marca no mapa", + ["Create mark"] = "Criar marca", + ["Create New Offer"] = "Criar nova oferta", + ["Create Offer"] = "Criar oferta", + ["Current hotkey to add: %s"] = "Atalho atual para adicionar: %s", + ["Current hotkeys:"] = "Atalhos atuais", + ["Current Offers"] = "Ofertas atuais", + ["Default"] = "Padrăo", + ["Delete mark"] = "Deletar marca", + ["Description"] = "Descriçăo", + ["Description:"] = "Descriçăo", + ["Destructive Behaviour"] = "Comportamento destrutivo", + ["Detail"] = "Detalhe", + ["Details"] = "Detalhes", + ["Disable Shared Experience"] = "Desativar experięncia compartilhada", + ["Dismount"] = "Desmontar", + ["Display connection speed to the server (milliseconds)"] = "Exibir a velocidade de conexăo com o servidor (milisegundos)", + ["Display creature health bars"] = "Exibir barras de vida das criaturas", + ["Display creature names"] = "Exibir nomes das criaturas", + ["Display text messages"] = "Exibir mensagens de texto", + ["Distance Fighting"] = "Combate a Distância", + ["Don't stretch or shrink Game Window"] = "Năo esticar ou contrair a janela do game", + ["Druid"] = "Druid", + ["Edit hotkey text:"] = "Editar texto do atalho", + ["Edit List"] = "Editar lista", + ["Edit Text"] = "Editar Texto", + ["Enable audio"] = "Ativar áudio", + ["Enable dash walking"] = "Ativar andar rápido", + ["Enable lights"] = "Ativar luzes", + ["Enable music sound"] = "Ativar música", + ["Enable music"] = "Ativar musica", + ["Enable shader effects"] = "Ativar efeitos shader", + ["Enable Shared Experience"] = "Ativar experięncia compartilhada", + ["Enable smart walking"] = "Ativar andar inteligente", + ["Enable vertical synchronization"] = "Ativar sincronizaçăo vertical", + ["Enable walk booster"] = "Ativar andar intensificado", + ["Enter Game"] = "Entrar no jogo", + ["Enter one name per line."] = "Entre somente um nome por linha.", + ["Enter with your account again to update your client."] = "Entre com sua conta denovo para atualizar o client.", + ["Error"] = "Erro", + ["Excessive Unjustified Player Killing"] = "Assassinato em excesso, sem justificativa, de jogadores", + ["Exclude from private chat"] = "Excluir do canal privado", + ["Exit"] = "Sair", + ["Experience"] = "Experięncia", + ["Filter list to match your level"] = "Filtrar a lista para o seu level", + ["Filter list to match your vocation"] = "Filtrar a lista para a sua vocaçăo", + ["Find"] = "Procurar", + ["Fishing"] = "Pesca", + ["Fist Fighting"] = "Porrada", + ["Follow"] = "Seguir", + ["For Your Information"] = "Para sua informaçăo", + ["Force Exit"] = "Forçar Saida", + ["Formula"] = "Fórmula", + ["Free Account"] = "Conta Grátis", + ["Fullscreen"] = "Tela cheia", + ["Game framerate limit: %s"] = "Limite da taxa de quadros do jogo: %s", + ["Game"] = "Jogo", + ["Graphics card driver not detected"] = "Driver da placa de vídeo năo detectado", + ["Graphics Engine:"] = "Motor Gráfico:", + ["Graphics"] = "Gráficos", + ["Group"] = "Grupo", + ["Head"] = "Cabeça", + ["Healing"] = "Curando", + ["Health Info"] = "Barra de Vida", + ["Health Information"] = "Informaçăo de vida", + ["Hide monsters"] = "Esconder montros", + ["Hide non-skull players"] = "Esconder jogadores sem caveira", + ["Hide Npcs"] = "Esconder NPCs", + ["Hide Offline"] = "Esconder Offline", + ["Hide party members"] = "Esconder membros do grupo", + ["Hide players"] = "Esconder jogadores", + ["Hide spells for higher exp. levels"] = "Esconder feitiços de nível maior", + ["Hide spells for other vocations"] = "Esconder feitiços de outras vocaçőes", + ["Hit Points"] = "Pontos de Vida", + ["Hold left mouse button to navigate\nScroll mouse middle button to zoom\nRight mouse button to create map marks"] = "Segure o botăo esquerdo para navegar\nGire o botăo do centro do mouse para ampliar\nClique com o botăo direito do mouse para criar marcas", + ["Host"] = "Host", + ["Hotkeys"] = "Atalhos", + ["If you shut down the program, your character might stay in the game.\nClick on 'Logout' to ensure that you character leaves the game properly.\nClick on 'Exit' if you want to exit the program without logging out your character."] = "Se vocę desligar o programa o seu personagem pode continuar no jogo.\nClique em 'Sair' para assegurar que seu personagem saia do jogo adequadamente.\nClique em 'Forçar Saida' para fechar o programa sem desconectar seu personagem.", + ["Ignore capacity"] = "Ignorar capacidade", + ["Ignore equipped"] = "Ignorar equipado", + ["Ignore List"] = "Lista de Ignorados", + ["Ignore players"] = "Jogadores ignorados", + ["Ignore Private Messages"] = "Ignorar mensagens privadas", + ["Ignore Yelling"] = "Ignorar gritos", + ["Ignore"] = "Ignorar", + ["Ignored players:"] = "Joadores ignorados:", + ["Interface framerate limit: %s"] = "Limite da taxa de quadros da interface: %s", + ["Inventory"] = "Inventário", + ["Invite to Party"] = "Convidar para o grupo", + ["Invite to private chat"] = "Convidar para o canal privado", + ["IP Address Banishment"] = "Banimento de endereço IP", + ["It is empty."] = "Está vazio.", + ["Item Offers"] = "Ofertas de items", + ["Join %s's Party"] = "Entrar na party do %s", + ["Knight"] = "Knight", + ["Leave Party"] = "Sair do grupo", + ["Level"] = "Nível", + ["Lifetime Premium Account"] = "Conta Premium para a vida toda.", + ["Limits FPS to 60"] = "Limita o FPS para 60", + ["List of items that you're able to buy"] = "Lista de itens que vocę pode comprar", + ["List of items that you're able to sell"] = "Lista de itens que vocę pode vender", + ["Load"] = "Carregar", + ["Logging out..."] = "Saindo...", + ["Login Error"] = "Erro de Autenticaçăo", + ["Login"] = "Entrar", + ["Logout"] = "Sair", + ["Look"] = "Olhar", + ["Magic Level"] = "Nível Mágico", + ["Make sure that your client uses\nthe correct game protocol version"] = "Tenha certeza que o seu cliente use\no mesmo protocolo do servidor do jogo", + ["Mana"] = "Mana", + ["Manage hotkeys:"] = "Configurar atalhos:", + ["Market Offers"] = "Ofertas do mercado", + ["Market"] = "Mercado", + ["Message of the day"] = "Mensagem do dia", + ["Message to "] = "Mensagem para ", + ["Message to %s"] = "Mandar mensagem para %s", + ["Minimap"] = "Minimapa", + ["Module Manager"] = "Gerenciador de Módulos", + ["Module name"] = "Nome do módulo", + ["Mount"] = "Montar", + ["Move Stackable Item"] = "Mover item contável", + ["Move up"] = "Mover para cima", + ["Music volume: %d"] = "Volume da música: %d", + ["My Offers"] = "Minhas ofertas", + ["Name Report + Banishment + Final Warning"] = "Reportar Nome + Banimento + Aviso Final", + ["Name Report + Banishment"] = "Reportar Nome + Banimento", + ["Name Report"] = "Reportar Nome", + ["Name"] = "Nome", + ["New Server"] = "Novo Servidor", + ["Next level in %d hours and %d minutes"] = "Próximo nível em %d horas e %d minutos", + ["No graphics card detected, everything will be drawn using the CPU,\nthus the performance will be really bad.\nPlease update your graphics driver to have a better performance."] = false, + ["No item selected."] = "Nenhum item selecionado", + ["No Mount"] = "Sem montaria", + ["No Outfit"] = "Sem roupa", + ["No statement has been selected."] = "Nenhuma afirmaçăo foi selecionada.", + ["No"] = "Năo", + ["Notation"] = "Notaçăo", + ["NPC Trade"] = "Troca com NPC", + ["Offer History"] = "Histórico de ofertas", + ["Offer Type"] = "Tipo de oferta", + ["Offers"] = "Ofertas", + ["Offline Training"] = "Treino Offline", + ["Ok"] = "Ok", + ["on %s.\n"] = "em %s.\n", + ["Open a private message channel:"] = "Abrir um canal privado:", + ["Open charlist automatically when starting client"] = "Abrir lista de personagens ao iniciar o cliente", + ["Open in new window"] = "Abrir em nova janela", + ["Open new channel"] = "Abrir novo canal", + ["Open"] = "Abrir", + ["Options"] = "Opçőes", + ["Overview"] = "Visăo geral", + ["Paladin"] = "Paladin", + ["Pass Leadership to %s"] = "Passar liderança para %s", + ["Password"] = "Senha", + ["Piece Price"] = "Preço por peça", + ["Please enter a character name:"] = "Por favor, entre com o nome do personagem:", + ["Please Select"] = "Por favor, selecione algo", + ["Please use this dialog to only report bugs. Do not report rule violations here!"] = "Por favor, use este campo apenas para reportar defeitos. Năo reporte violaçăo de regras aqui!", + ["Please wait"] = "Por favor, espere", + ["Please, press the key you wish to add onto your hotkeys manager"] = "Por favor, pressione a tecla que vocę deseja\nadicionar no gerenciador de atalhos", + ["Port"] = "Porta", + ["Position"] = "Posiçăo", + ["Position: %i %i %i"] = "Posiçăo: %i %i %i", + ["Premium Account (%s) days left"] = "Conta Premium (%s) dias faltando", + ["Premium"] = "Premium", + ["Price"] = "Preço", + ["Primary"] = "Primário", + ["Protocol"] = "Protocolo", + ["Quest Log"] = "Registro de Quest", + ["Randomize characters outfit"] = "Gerar roupa aleatória", + ["Randomize"] = "Embaralhar", + ["Reason"] = "Motivo", + ["Refresh Offers"] = "Atualizar Ofertas", + ["Refresh"] = "Atualizar", + ["Regeneration Time"] = "Tempo de Regeneraçăo", + ["Reject"] = "Rejeitar", + ["Reload All"] = "Recarregar Todos", + ["Remember account and password when starts client"] = "Lembrar conta e senha quando iniciar o cliente", + ["Remember password"] = "Lembrar senha", + ["Remove %s"] = "Remover %s", + ["Remove"] = "Remover", + ["Report Bug"] = "Reportar defeito", + ["Reserved for more functionality later."] = "Reservado para futura maior funcionalidade.", + ["Reset All"] = "Resetar Todos", + ["Reset Market"] = "Resetar Mercado", + ["Revoke %s's Invitation"] = "Năo aceitar o convite do %s", + ["Rotate"] = "Girar", + ["Rule Violation"] = "Violaçăo de regra", + ["Save Messages"] = "Salvar Mensagens", + ["Save"] = "Salvar", + ["Search all items"] = "Procurar todos os items", + ["Search"] = "Procurar", + ["Secondary"] = "Secundário", + ["Select object"] = "Selecionar objeto", + ["Select Outfit"] = "Selecionar Roupa", + ["Select your language"] = "Selecione sua língua", + ["Select"] = "Selecionar", + ["Sell All"] = "Vender Todos", + ["Sell Now"] = "Vender agora", + ["Sell Offers"] = "Ofertas de venda", + ["Sell"] = "Vender", + ["Send automatically"] = "Enviar automaticamente", + ["Send Message"] = "Enviar Mensagem", + ["Send"] = "Enviar", + ["Server List"] = "Lista de Servidores", + ["Server list"] = "Lista de servidores", + ["Server Log"] = "Registro do servidor", + ["Server"] = "Servidor", + ["Set Outfit"] = "Escolher Roupa", + ["Shielding"] = "Defesa", + ["Show all items"] = "Exibir todos os itens", + ["Show connection ping"] = "Mostrar latęncia de conexăo", + ["Show Depot Only"] = "Mostrar somente o depósito", + ["Show event messages in console"] = "Exibir mensagens de eventos no console", + ["Show frame rate"] = "Exibir FPS", + ["Show info messages in console"] = "Exibir mensagens informativas no console", + ["Show left panel"] = "Mostrar barra lateral esquerda", + ["Show levels in console"] = "Exibir níveis no console", + ["Show Offline"] = "Mostrar Offline", + ["Show private messages in console"] = "Exibir mensagens privadas no console", + ["Show private messages on screen"] = "Exibir mensagens na tela", + ["Show Server Messages"] = "Mostrar Mensagens do Servidor", + ["Show status messages in console"] = "Exibir mensagens de estado no console", + ["Show Text"] = "Mostrar texto", + ["Show timestamps in console"] = "Exibir o horário no console", + ["Show your depot items only"] = "Mostrar os itens somentedo depósito", + ["Skills"] = "Habilidades", + ["Sorcerer"] = "Sorcerer", + ["Soul Points"] = "Pontos de Alma", + ["Soul"] = "Alma", + ["Special"] = "Especial", + ["Speed"] = "Velocidade", + ["Spell Cooldowns"] = "", + ["Spell List"] = "Lista de Feitiços", + ["Stamina"] = "Vigor", + ["Statement Report"] = "Afirmar Relato", + ["Statement"] = "Afirmaçăo", + ["Statistics"] = "Estatísticas", + ["Stop Attack"] = "Parar de Atacar", + ["Stop Follow"] = "Parar de Seguir", + ["Support"] = "Suporte", + ["Sword Fighting"] = "Combate com Espada", + ["Terminal"] = "Terminal", + ["There is no way."] = "Năo há rota", + ["Title"] = "Título", + ["Total Price"] = "Preço total", + ["Trade with ..."] = "Trocar com ...", + ["Trade"] = "Trocar", + ["Trying to reconnect in %s seconds."] = "Tentando reconectar em %s segundos.", + ["Type"] = "Tipo", + ["Unable to establish a connection. (err: %d)"] = "Năo foi possível estabilizar a conexă. (err: %d)", + ["Unable to load dat file, please place a valid dat in '%s'"] = "Năo foi possível carregar o arquivo DAT, por favor coloque um arquivo válido em %s", + ["Unable to load spr file, please place a valid spr in '%s'"] = "Năo foi possível carregar o arquivo SPR, por favor coloque um arquivo válido em %s", + ["Unable to logout."] = "Năo é possivel sair", + ["Unignore"] = "Designorar", + ["Unload"] = "Descarregar", + ["Update needed"] = "Atualizaçăo necessária", + ["Use on target"] = "Usar no alvo", + ["Use on yourself"] = "Usar em si", + ["Use with ..."] = "Usar com ...", + ["Use"] = "Usar", + ["Version"] = "Versăo", + ["VIP List"] = "Lista VIP", + ["Voc."] = "Voc.", + ["Vocation"] = "Vocaçăo", + ["Waiting List"] = "Lista de espera", + ["Website"] = "Website", + ["Weight"] = "Peso", + ["Will boost your walk on high speed characters"] = "Irá melhorar o andar de persnagens rápidos", + ["Will detect when to use diagonal step based on the\nkeys you are pressing"] = "Detectar quando usar o passo diagonal\nbaseado nas teclas pressionadas", + ["With crosshair"] = "Com mira", + ["Yes"] = "Sim", + ["You are bleeding"] = "Vocę está sangrando", + ["You are burning"] = "Vocę está queimando", + ["You are cursed"] = "Vocę está amaldiçoado", + ["You are dazzled"] = "Vocę está deslumbrado", + ["You are dead"] = "Vocę está morto", + ["You are dead."] = "Vocę está morto.", + ["You are drowning"] = "Vocę está se afogando", + ["You are drunk"] = "Vocę está bębado", + ["You are electrified"] = "Vocę está eletrificado", + ["You are freezing"] = "Vocę está congelando", + ["You are hasted"] = "Vocę está com pressa", + ["You are hungry"] = "Vocę está faminto", + ["You are paralysed"] = "Vocę está paralizado", + ["You are poisoned"] = "Vocę está envenenado", + ["You are protected by a magic shield"] = "Vocę está protegido com um escudo mágico", + ["You are strengthened"] = "Vocę está reforçado", + ["You are within a protection zone"] = "Vocę está dentro de uma zona de proteçăo", + ["You can enter new text."] = "Vocę pode entrar com um novo texto.", + ["You have %d%% to advance to level %d."] = "Vocę tem %d%% para avançar para o nível %d.", + ["You have %s percent to go"] = "Vocę tem %s porcento para avançar", + ["You have %s percent"] = "Vocę tem %s porcento", + ["You may not logout during a fight"] = "Vocę năo pode sair durante um combate", + ["You may not logout or enter a protection zone"] = "Vocę năo pode sair ou entrar em uma zona de proteçăo", + ["You must enter a comment."] = "Vocę precisa entrar com um comentário", + ["You must enter a valid server address and port."] = "Vocę precisa colocar um endereço e uma porta do servidor válidos.", + ["You must select a character to login!"] = "Vocę deve selecionar um personagem para entrar!", + ["You read the following, written by \n%s\n"] = "Vocę lę o seguinte, escrito por \n%s\n", + ["You read the following, written on \n%s.\n"] = "Vocę lę o seguinte, escrito em \n%s.\n", + ["Your Capacity"] = "Sua capacidade", + ["Your character health is %d out of %d."] = "A vida do seu personagem é %d de %d.", + ["Your character mana is %d out of %d."] = "A mana do seu personagem é %d de %d.", + ["Your connection has been lost. (err: %d)"] = "A sua conexăo foi perdida. (err: %d)", + ["Your Money"] = "Seu dinheiro", + } +} + +modules.client_locales.installLocale(locale) diff --git a/data/locales/sv.lua b/data/locales/sv.lua new file mode 100644 index 0000000..d6263f4 --- /dev/null +++ b/data/locales/sv.lua @@ -0,0 +1,378 @@ +-- thanks cometangel, who made these translations + +locale = { + name = "sv", + charset = "cp1252", + languageName = "Svenska", + + formatNumbers = true, + decimalSeperator = ',', + thousandsSeperator = ' ', + + translation = { + ["1a) Offensive Name"] = "1a) Offensivt Namn", + ["1b) Invalid Name Format"] = "1b) Ogiltigt Namnformat", + ["1c) Unsuitable Name"] = "1c) Opassande Namn", + ["1d) Name Inciting Rule Violation"] = "1d) Namn anstiftar regelbrott.", + ["2a) Offensive Statement"] = "2a) Offensivt Uttryck", + ["2b) Spamming"] = "2b) Spammning", + ["2c) Illegal Advertising"] = "2c) Olaglig Reklamföring", + ["2d) Off-Topic Public Statement"] = "2d) Icke-Ämneförhĺllande publiskt uttryck", + ["2e) Non-English Public Statement"] = "2e) Icke-Engelskt publiskt uttryck", + ["2f) Inciting Rule Violation"] = "2f) Antyder regelbrytande", + ["3a) Bug Abuse"] = "3a) Missbrukande av bugg", + ["3b) Game Weakness Abuse"] = "3b) Spelsvaghetsmissbruk", + ["3c) Using Unofficial Software to Play"] = "3c) Använder Icke-officiel mjukvara för att spela", + ["3d) Hacking"] = "3d) Hackar", + ["3e) Multi-Clienting"] = "3e) Multi-klient", + ["3f) Account Trading or Sharing"] = "3f) Kontohandel", + ["4a) Threatening Gamemaster"] = "4a) Hotar gamemaster", + ["4b) Pretending to Have Influence on Rule Enforcement"] = "4b) Lĺtsas ha inflytande pĺ Regelsystem", + ["4c) False Report to Gamemaster"] = "4c) Falsk rapport till gamemaster", + ["Accept"] = "Acceptera", + ["Account name"] = "Konto namn", + ["Account Status:"] = false, + ["Action:"] = "Handling:", + ["Add"] = "Lägg till", + ["Add new VIP"] = "Ny VIP", + ["Addon 1"] = "Tillägg 1", + ["Addon 2"] = "Tillägg 2", + ["Addon 3"] = "Tillägg 3", + ["Add to VIP list"] = "Lägg till pĺ VIP Listan", + ["Adjust volume"] = "Justera Volym", + ["Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nSimply click on Ok to resume your journeys!"] = false, + ["All"] = false, + ["All modules and scripts were reloaded."] = "Alla moduler och skript laddades om", + ["Allow auto chase override"] = "Tillĺt Jaktstyrning", + ["Also known as dash in tibia community, recommended\nfor playing characters with high speed"] = false, + ["Ambient light: %s%%"] = false, + ["Amount:"] = "Antal:", + ["Amount"] = "Antal", + ["Anonymous"] = "Anonym", + ["Are you sure you want to logout?"] = "Är du säker att du vill logga ut?", + ["Attack"] = "Attackera", + ["Author"] = "Översättare", + ["Autoload"] = "Automatisk Laddning", + ["Autoload priority"] = "Laddningsprioritet", + ["Auto login"] = "Autoinloggning", + ["Auto login selected character on next charlist load"] = "Logga in Näst laddad karaktär automatisk nästa gĺng karaktärlistan laddar", + ["Axe Fighting"] = "Yx Stridande", + ["Balance:"] = "Balans:", + ["Banishment"] = "Bannlysning", + ["Banishment + Final Warning"] = "Bannlysning + Sista varning", + ["Battle"] = "Strid", + ["Browse"] = "Bläddra", + ["Bug report sent."] = "Buggrapport Skickad.", + ["Button Assign"] = "Assignera Knapp", + ["Buy"] = "Köp", + ["Buy Now"] = "Köp Nu", + ["Buy Offers"] = "Köp Offerter", + ["Buy with backpack"] = "Köp med ryggsäck", + ["Cancel"] = "Avbryt", + ["Cannot login while already in game."] = "Kan ej logga in medan du redan är i spelet.", + ["Cap"] = "Kap", + ["Capacity"] = "Kapacitet", + ["Center"] = "Centrera", + ["Channels"] = "Kanaler", + ["Character List"] = "Karaktär lista", + ["Classic control"] = "Klassisk kontroll", + ["Clear current message window"] = "Rensa nuvarande meddelanderuta", + ["Clear Messages"] = false, + ["Clear object"] = "Rensa objekt", + ["Client needs update."] = "Klienten behöver uppdateras.", + ["Close"] = "Stäng", + ["Close this channel"] = "Stäng Denna Kanal", + ["Club Fighting"] = "Klubb Stridande", + ["Combat Controls"] = "Krigs Kontroller", + ["Comment:"] = "Kommentar:", + ["Connecting to game server..."] = "Kopplar upp till spelserver...", + ["Connecting to login server..."] = "Kopplar upp till autentiseringserver...", + ["Console"] = false, + ["Cooldowns"] = false, + ["Copy message"] = "Kopiera meddelande", + ["Copy name"] = "Kopiera namn", + ["Copy Name"] = "Kopiera Namn", + ["Create Map Mark"] = false, + ["Create mark"] = false, + ["Create New Offer"] = "Skapa ny offert.", + ["Create Offer"] = "Skapa Offert", + ["Current hotkeys:"] = "Aktuella snabbtangenter", + ["Current hotkey to add: %s"] = "Ny Snabbtangent: %s", + ["Current Offers"] = "Nuvarande Offerter", + ["Default"] = "Standard", + ["Delete mark"] = false, + ["Description:"] = false, + ["Description"] = "Beskrivning", + ["Destructive Behaviour"] = "Destruktivt beteende", + ["Detail"] = "Detalj", + ["Details"] = "Detaljer", + ["Disable Shared Experience"] = "Avaktivera delad erfarenhet", + ["Dismount"] = false, + ["Display connection speed to the server (milliseconds)"] = false, + ["Distance Fighting"] = "Distans Stridande", + ["Don't stretch/shrink Game Window"] = false, + ["Edit hotkey text:"] = "Ändra Snabbtangent:", + ["Edit List"] = "Ändra Lista", + ["Edit Text"] = "Ändra text", + ["Enable music"] = "Aktivera musik", + ["Enable Shared Experience"] = "Aktivera delad erfarenhet", + ["Enable smart walking"] = false, + ["Enable vertical synchronization"] = "Aktivera vertikal synkronisering", + ["Enable walk booster"] = false, + ["Enter Game"] = "Gĺ in i Spelet", + ["Enter one name per line."] = "Skriv ett namn per linje.", + ["Enter with your account again to update your client."] = false, + ["Error"] = "Fel", + ["Error"] = "Fel", + ["Excessive Unjustified Player Killing"] = "Överdrivet oberättigat dödande av spelare", + ["Exclude from private chat"] = "Exkludera frĺn privat chat", + ["Exit"] = "Avsluta", + ["Experience"] = "Erfarenhet", + ["Filter list to match your level"] = "Filtrera efter nivĺ", + ["Filter list to match your vocation"] = "Filtrera efter kallelse", + ["Find:"] = false, + ["Fishing"] = "Fiske", + ["Fist Fighting"] = "Hand Stridande", + ["Follow"] = "Följ", + ["Force Exit"] = false, + ["For Your Information"] = false, + ["Free Account"] = false, + ["Fullscreen"] = "Helskärm", + ["Game"] = false, + ["Game framerate limit: %s"] = "Spelets FPS gräns: %s", + ["Graphics"] = "Grafik", + ["Graphics card driver not detected"] = false, + ["Graphics Engine:"] = "Grafikmotor:", + ["Head"] = "Huvud", + ["Healing"] = false, + ["Health Info"] = "Livsinfo", + ["Health Information"] = "Livsinformation", + ["Hide monsters"] = "Göm Monster", + ["Hide non-skull players"] = "Göm icke-skullad spelare", + ["Hide Npcs"] = "Göm NPCs", + ["Hide Offline"] = false, + ["Hide party members"] = "Göm gruppmedlemmar", + ["Hide players"] = "Göm spelare", + ["Hide spells for higher exp. levels"] = false, + ["Hide spells for other vocations"] = false, + ["Hit Points"] = "Livspoäng", + ["Hold left mouse button to navigate\nScroll mouse middle button to zoom\nRight mouse button to create map marks"] = false, + ["Hotkeys"] = "Snabbtangenter", + ["If you shut down the program, your character might stay in the game.\nClick on 'Logout' to ensure that you character leaves the game properly.\nClick on 'Exit' if you want to exit the program without logging out your character."] = "Om du stänger av programmet kan din karaktär stanna i spelet.\nKlicka pĺ 'Logga ut' för att säkerställa att din karaktär lämnar spelet korrekt.\nKlicka pĺ 'Avsluta' om du vill avsluta programmet utan att logga ut din karaktär.", + ["Ignore"] = false, + ["Ignore capacity"] = "Ignorera kapacitet", + ["Ignored players:"] = false, + ["Ignore equipped"] = "Ignorera utrustning", + ["Ignore List"] = false, + ["Ignore players"] = false, + ["Ignore Private Messages"] = false, + ["Ignore Yelling"] = false, + ["Interface framerate limit: %s"] = "Gränssnitt FPS gräns: %s", + ["Inventory"] = "Utrustning", + ["Invite to Party"] = "Bjud till grupp", + ["Invite to private chat"] = "Bjud in i privat chat", + ["IP Address Banishment"] = "Bannlysning av IP", + ["Item Offers"] = "Objekt offert", + ["It is empty."] = false, + ["Join %s's Party"] = "Gĺ med i %s's Grupp", + ["Leave Party"] = "Lämna Grupp", + ["Level"] = "Nivĺ", + ["Lifetime Premium Account"] = false, + ["Limits FPS to 60"] = "Stoppa FPS vid 60", + ["List of items that you're able to buy"] = "Lista av saker du kan köpa", + ["List of items that you're able to sell"] = "Lista av saker du kan sälja", + ["Load"] = "Ladda", + ["Logging out..."] = "Loggar ut...", + ["Login"] = "Logga in", + ["Login Error"] = "Autentifikations fel", + ["Login Error"] = "Autentifikations fel", + ["Logout"] = "Logga ut", + ["Look"] = "Kolla", + ["Magic Level"] = "Magisk Nivĺ", + ["Make sure that your client uses\nthe correct game protocol version"] = "Var säker pĺ att din client\n andvänder rätt protokol version", + ["Mana"] = "Mana", + ["Manage hotkeys:"] = "Ändra snabbtangenten:", + ["Market"] = "Marknad", + ["Market Offers"] = "Marknadsofferter", + ["Message of the day"] = "Dagens meddelande", + ["Message to "] = "Meddelande till ", + ["Message to %s"] = "Meddelande till %s", + ["Minimap"] = "Minikarta", + ["Module Manager"] = "Modul Manager", + ["Module name"] = "Modul namn", + ["Mount"] = false, + ["Move Stackable Item"] = "Flytta stapelbart föremĺl", + ["Move up"] = "Flytta upp", + ["My Offers"] = "Mina offerter", + ["Name:"] = "Namn:", + ["Name Report"] = "Namn Rapport", + ["Name Report + Banishment"] = "Namn rapport + Bannlysning", + ["Name Report + Banishment + Final Warning"] = "Namn rapport + Bannlysning + Sista varning", + ["No"] = "Nej", + ["No graphics card detected, everything will be drawn using the CPU,\nthus the performance will be really bad.\nPlease update your graphics driver to have a better performance."] = false, + ["No item selected."] = "Ingen sak vald.", + ["No Mount"] = "Ingen Mount", + ["No Outfit"] = "Ingen Utstyrsel", + ["No statement has been selected."] = "Inget pĺstĺende är valt.", + ["Notation"] = "Notering", + ["NPC Trade"] = "Handel NPC", + ["Offer History"] = "Offert Historia", + ["Offers"] = "Offerter", + ["Offer Type:"] = "Offert typ:", + ["Offline Training"] = false, + ["Ok"] = "Ok", + ["on %s.\n"] = "pĺ %s.\n", + ["Open"] = "Öppna", + ["Open a private message channel:"] = "Öppna en privat meddelandekanal:", + ["Open charlist automatically when starting client"] = false, + ["Open in new window"] = "Öppna i nytt fönster", + ["Open new channel"] = "Öppna ny kanal", + ["Options"] = "Inställningar", + ["Overview"] = false, + ["Pass Leadership to %s"] = "Ge ledarskap till %s", + ["Password"] = "Lösenord", + ["Piece Price:"] = "Per Styck:", + ["Please enter a character name:"] = "Skriv in ett karaktärsnamn:", + ["Please, press the key you wish to add onto your hotkeys manager"] = "Tryck pĺ knappen som du\nvill lägga till som snabbtangent", + ["Please Select"] = "Välj", + ["Please use this dialog to only report bugs. Do not report rule violations here!"] = "Använd den här dialogrutan endast för att rapportera buggar. Rapportera inte regelbrott här!", + ["Please wait"] = "Var God Vänta", + ["Port"] = "Port", + ["Position:"] = false, + ["Position: %i %i %i"] = false, + ["Premium Account (%s) days left"] = false, + ["Price:"] = "Pris", + ["Primary"] = "Primär", + ["Protocol"] = "Protokoll", + ["Quest Log"] = "Uppdragslog", + ["Randomize"] = "Slumpa", + ["Randomize characters outfit"] = "Slumpa karaktärs utstyrsel", + ["Reason:"] = "Anledning:", + ["Refresh"] = "Uppdatera", + ["Refresh Offers"] = false, + ["Regeneration Time"] = false, + ["Reject"] = "Avvisa", + ["Reload All"] = "Ladda om allt", + ["Remember account and password when starts client"] = false, + ["Remember password"] = "Kom ihĺg lösenord", + ["Remove"] = "Ta bort", + ["Remove %s"] = "Ta bort %s", + ["Report Bug"] = "Rapportera Bugg", + ["Reserved for more functionality later."] = false, + ["Reset Market"] = false, + ["Revoke %s's Invitation"] = "Annulera %s's Inbjudan", + ["Rotate"] = "Rotera", + ["Rule Violation"] = "Regel Brott", + ["Save"] = false, + ["Save Messages"] = false, + ["Search:"] = "Sök:", + ["Search all items"] = false, + ["Secondary"] = "Sekundär", + ["Select object"] = "Välj Objekt", + ["Select Outfit"] = "Välj Utstyrsel", + ["Select your language"] = false, + ["Sell"] = "Sälj", + ["Sell Now"] = "Sälj Nu", + ["Sell Offers"] = "Sälj Offerter", + ["Send"] = "Skicka", + ["Send automatically"] = "Skicka automatiskt", + ["Send Message"] = false, + ["Server"] = "Server", + ["Server Log"] = "Server Log", + ["Set Outfit"] = "Bestäm Utstyrsel", + ["Shielding"] = "Sköld", + ["Show all items"] = "Visa alla saker", + ["Show connection ping"] = false, + ["Show Depot Only"] = "Visa bara förrĺd", + ["Show event messages in console"] = "Visa event meddelanden i konsol", + ["Show frame rate"] = "Visa FPS", + ["Show info messages in console"] = "Visa info meddelanden i konsol", + ["Show left panel"] = "Visa vänster panel", + ["Show levels in console"] = "Visa nivĺer i konsol", + ["Show Offline"] = false, + ["Show private messages in console"] = "Visa privata meddelanden i konsol", + ["Show private messages on screen"] = "Visa privata meddelanden pĺ skärmen", + ["Show Server Messages"] = false, + ["Show status messages in console"] = "Visa statusmeddelanden i konsol", + ["Show Text"] = "Visa Text", + ["Show timestamps in console"] = "Visa tidstämpel i konsol", + ["Show your depot items only"] = "Visa mitt förrĺd endast", + ["Skills"] = "Förmĺgor", + ["Soul"] = "Själ", + ["Soul Points"] = "Själpoäng", + ["Special"] = false, + ["Speed"] = false, + ["Spell Cooldowns"] = false, + ["Spell List"] = false, + ["Stamina"] = "Uthĺllighet", + ["Statement:"] = "Pĺstĺende:", + ["Statement Report"] = "Pĺstĺenderapport", + ["Statistics"] = "Statistik", + ["Stop Attack"] = "Sluta Attackera", + ["Stop Follow"] = "Sluta Följa", + ["Support"] = false, + ["%s: (use object)"] = "%s: (Använd objekt)", + ["%s: (use object on target)"] = "%s: (Använd objekt pĺ mĺl)", + ["%s: (use object on yourself)"] = "%s: (Använd objekt pĺ mig)", + ["%s: (use object with crosshair)"] = "%s: (Använd objekt med sikte)", + ["Sword Fighting"] = "Svärd Stridning", + ["Terminal"] = "Terminal", + ["There is no way."] = "Det finns ingen väg.", + ["Title"] = false, + ["Total Price:"] = "Totalt Pris:", + ["Trade"] = "Handel", + ["Trade with ..."] = "Handla med ...", + ["Trying to reconnect in %s seconds."] = "Försöker koppla upp igen om %s sekunder.", + ["Unable to load dat file, please place a valid dat in '%s'"] = "kan ej ladda dat filen, lägg en giltig dat fil i '%s'", + ["Unable to load spr file, please place a valid spr in '%s'"] = "kan ej ladda spr filen, lägg en giltig spr fil i '%s'", + ["Unable to logout."] = "Kan ej logga ut.", + ["Unignore"] = false, + ["Unload"] = "Avladda", + ["Update needed"] = false, + ["Use"] = "Använd", + ["Use on target"] = "Använd pĺ mĺl", + ["Use on yourself"] = "Använd pĺ mig", + ["Use with ..."] = "Använd med ...", + ["Version"] = "Version", + ["VIP List"] = "VIP Lista", + ["Voc."] = "Kallelse", + ["Vocation"] = false, + ["Waiting List"] = "Kölista", + ["Website"] = "Websida", + ["Weight:"] = "Vikt:", + ["Will detect when to use diagonal step based on the\nkeys you are pressing"] = false, + ["With crosshair"] = "Med sikte", + ["Yes"] = "Ja", + ["You are bleeding"] = "Du Blöder", + ["You are burning"] = "Du brinner", + ["You are cursed"] = "Du är fördömd", + ["You are dazzled"] = "Du är chockad", + ["You are dead."] = "Du är död.", + ["You are dead"] = "Du är död", + ["You are drowning"] = "Du drunknar", + ["You are drunk"] = "Du är full.", + ["You are electrified"] = "Du är elektrifierad", + ["You are freezing"] = "Du Fryser", + ["You are hasted"] = "Du är i hast", + ["You are hungry"] = "Du är hungrig", + ["You are paralysed"] = "Du är paralyserad", + ["You are poisoned"] = "Du är förgiftad", + ["You are protected by a magic shield"] = "Du är skyddad av en magisk sköld", + ["You are strengthened"] = "Du är förstärkt", + ["You are within a protection zone"] = "Du är inom en skyddszon", + ["You can enter new text."] = "Du kan skriva i ny text.", + ["You have %s percent"] = "Du har %s procent", + ["You have %s percent to go"] = "Du har %s procent kvar", + ["You may not logout during a fight"] = "Du kan ej logga ut i strid", + ["You may not logout or enter a protection zone"] = "Du kan ej logga ut eller gĺ in i en skyddszon", + ["You must enter a comment."] = "Du mĺste skriva en kommentar", + ["You must enter a valid server address and port."] = "Du mĺste fylla i en giltig server adress och port", + ["You must select a character to login!"] = "Du mĺste välja en karaktär för att logga in!", + ["Your Capacity:"] = "Din Kapacitet:", + ["You read the following, written by \n%s\n"] = "Du läser följande, Skrivet av \n%s\n", + ["You read the following, written on \n%s.\n"] = false, + ["Your Money:"] = "Dina Pengar:", + } +} + +modules.client_locales.installLocale(locale) \ No newline at end of file diff --git a/data/particles/particle.png b/data/particles/particle.png new file mode 100644 index 0000000..044d629 Binary files /dev/null and b/data/particles/particle.png differ diff --git a/data/particles/particles.otps b/data/particles/particles.otps new file mode 100644 index 0000000..8424488 --- /dev/null +++ b/data/particles/particles.otps @@ -0,0 +1,36 @@ +Particle + name: groupcooldown_particle + + duration: 0.4 + min-position-radius: 0 + max-position-radius: 32 + min-position-angle: 0 + max-position-angle: 360 + velocity: 10 + min-velocity-angle: 0 + max-velocity-angle: 360 + colors: #ffffff00 #ffffffff #fff13000 + colors-stops: 0 0.1 1 + size: 1 1 + texture: /particles/particle + composition-mode: normal + +Effect + name: groupcooldown-effect + description: Effect for group cooldowns in the cooldown module + + System + position: 0 0 + + Emitter + position: 0 0 + delay: 0.06 + duration: 0.2 + burst-rate: 350 + burst-count: 50 + particle-type: groupcooldown_particle + + AttractionAffector + position: 0 0 + acceleration: 1000 + diff --git a/data/styles/10-buttons.otui b/data/styles/10-buttons.otui new file mode 100644 index 0000000..bf872db --- /dev/null +++ b/data/styles/10-buttons.otui @@ -0,0 +1,87 @@ +Button < UIButton + font: verdana-11px-antialised + color: #dfdfdfff + size: 106 23 + text-offset: 0 0 + image-source: /images/ui/button + image-color: #dfdfdf + image-clip: 0 0 22 23 + image-border: 3 + padding: 5 10 5 10 + opacity: 1.0 + + $hover !disabled: + image-clip: 0 23 22 23 + + $pressed: + image-clip: 0 46 22 23 + text-offset: 1 1 + + $disabled: + color: #dfdfdf88 + opacity: 0.8 + +TabButton < UIButton + size: 22 23 + image-source: /images/ui/tabbutton_rounded + image-color: #dfdfdf + image-clip: 0 0 22 23 + image-border: 3 + icon-color: #dfdfdf + color: #dfdfdf + + $hover !on: + image-clip: 0 23 22 23 + color: #dfdfdf + + $disabled: + image-color: #dfdfdf66 + icon-color: #dfdfdf + + $on: + image-clip: 0 46 22 23 + color: #dfdfdf + +NextButton < UIButton + size: 12 21 + image-source: /images/ui/arrow_horizontal + image-clip: 12 0 12 21 + image-color: #ffffff + + $hover !disabled: + image-clip: 12 21 12 21 + + $pressed: + image-clip: 12 21 12 21 + + $disabled: + image-color: #dfdfdf88 + +PreviousButton < UIButton + size: 12 21 + image-source: /images/ui/arrow_horizontal + image-clip: 0 0 12 21 + image-color: #ffffff + + $hover !disabled: + image-clip: 0 21 12 21 + + $pressed: + image-clip: 0 21 12 21 + + $disabled: + image-color: #dfdfdf88 + +AddButton < UIButton + size: 20 20 + image-source: /images/ui/icon_add + image-color: #dfdfdfff + + $hover !disabled: + image-color: #dfdfdf99 + + $pressed: + image-color: #dfdfdf44 + + $disabled: + image-color: #dfdfdf55 diff --git a/data/styles/10-checkboxes.otui b/data/styles/10-checkboxes.otui new file mode 100644 index 0000000..3edccec --- /dev/null +++ b/data/styles/10-checkboxes.otui @@ -0,0 +1,64 @@ +CheckBox < UICheckBox + size: 16 16 + text-align: left + text-offset: 18 1 + color: #dfdfdf + image-color: #dfdfdfff + image-rect: 0 0 15 15 + image-source: /images/ui/checkbox + + $hover !disabled: + color: #ffffff + + $!checked: + image-clip: 0 0 15 15 + + $hover !checked: + image-clip: 0 15 15 15 + + $checked: + image-clip: 0 30 15 15 + + $hover checked: + image-clip: 0 45 15 15 + + $disabled: + image-color: #dfdfdf88 + color: #dfdfdf88 + opacity: 0.8 + +ColorBox < UICheckBox + size: 16 16 + image-color: #dfdfdfff + image-source: /images/ui/colorbox + + $checked: + image-clip: 16 0 16 16 + + $!checked: + image-clip: 0 0 16 16 + +ButtonBox < UICheckBox + font: verdana-11px-antialised + color: #dfdfdfff + size: 106 23 + text-offset: 0 0 + text-align: center + image-source: /images/ui/button + image-color: #dfdfdf + image-clip: 0 0 22 23 + image-border: 3 + + $hover !disabled: + image-clip: 0 23 22 23 + + $checked: + image-clip: 0 46 22 23 + color: #dfdfdf + + $disabled: + color: #dfdfdf88 + image-color: #dfdfdf88 + +ButtonBoxRounded < ButtonBox + image-source: /images/ui/button_rounded \ No newline at end of file diff --git a/data/styles/10-comboboxes.otui b/data/styles/10-comboboxes.otui new file mode 100644 index 0000000..a4ad9da --- /dev/null +++ b/data/styles/10-comboboxes.otui @@ -0,0 +1,106 @@ +ComboBoxPopupScrollMenuButton < UIButton + height: 23 + font: verdana-11px-antialised + text-align: left + text-offset: 4 0 + color: #dfdfdf + background-color: alpha + margin: 1 + + $hover !disabled: + color: #dfdfdf + background-color: #355d89 + + $disabled: + color: #dfdfdf88 + +ComboBoxPopupScrollMenu < UIPopupScrollMenu + image-source: /images/ui/combobox_square + image-clip: 0 69 91 23 + image-border: 1 + +ComboBoxPopupMenuButton < UIButton + height: 23 + font: verdana-11px-antialised + text-align: left + text-offset: 4 0 + color: #dfdfdf + background-color: alpha + margin: 1 + + $hover !disabled: + color: #dfdfdf + background-color: #355d89 + + $disabled: + color: #dfdfdf88 + +ComboBoxPopupMenu < UIPopupMenu + image-source: /images/ui/combobox_square + image-clip: 0 69 91 23 + image-border: 1 + +ComboBox < UIComboBox + font: verdana-11px-antialised + color: #dfdfdf + size: 91 23 + text-offset: 3 0 + text-align: left + image-source: /images/ui/combobox_square + image-border: 3 + image-border-right: 19 + image-clip: 0 0 91 23 + + $hover !disabled: + image-clip: 0 23 91 23 + + $on: + image-clip: 0 46 91 23 + + $disabled: + color: #dfdfdf88 + opacity: 0.8 + +ComboBoxRoundedPopupScrollMenuButton < UIButton + height: 23 + font: verdana-11px-antialised + text-align: left + text-offset: 4 0 + color: #dfdfdf + background-color: alpha + + $hover !disabled: + color: #ffffff + background-color: #355d89 + + $disabled: + color: #dfdfdf88 + +ComboBoxRoundedPopupScrollMenu < UIPopupScrollMenu + image-source: /images/ui/combobox_rounded + image-clip: 0 69 91 23 + image-border: 3 + +ComboBoxRoundedPopupMenuButton < UIButton + height: 23 + font: verdana-11px-antialised + text-align: left + text-offset: 4 0 + color: #dfdfdf + background-color: alpha + + $hover !disabled: + color: #ffffff + background-color: #355d89 + + $disabled: + color: #dfdfdf88 + +ComboBoxRoundedPopupMenu < UIPopupMenu + image-source: /images/ui/combobox_rounded + image-clip: 0 69 91 23 + image-border: 3 + +ComboBoxRounded < ComboBox + image-source: /images/ui/combobox_rounded + image-border: 3 diff --git a/data/styles/10-creaturebuttons.otui b/data/styles/10-creaturebuttons.otui new file mode 100644 index 0000000..4bcdfb9 --- /dev/null +++ b/data/styles/10-creaturebuttons.otui @@ -0,0 +1,49 @@ +CreatureButton < UICreatureButton + height: 20 + margin-bottom: 5 + + UICreature + id: creature + size: 20 20 + anchors.left: parent.left + anchors.top: parent.top + phantom: true + + UIWidget + id: spacer + width: 5 + anchors.left: creature.right + anchors.top: creature.top + phantom: true + + UIWidget + id: skull + height: 11 + anchors.left: spacer.right + anchors.top: spacer.top + phantom: true + + UIWidget + id: emblem + height: 11 + anchors.left: skull.right + anchors.top: creature.top + phantom: true + + Label + id: label + anchors.left: emblem.right + anchors.right: parent.right + anchors.top: creature.top + color: #888888 + margin-left: 2 + phantom: true + + LifeProgressBar + id: lifeBar + height: 5 + anchors.left: spacer.right + anchors.right: parent.right + anchors.top: label.bottom + margin-top: 2 + phantom: true diff --git a/data/styles/10-creatures.otui b/data/styles/10-creatures.otui new file mode 100644 index 0000000..c6664ec --- /dev/null +++ b/data/styles/10-creatures.otui @@ -0,0 +1,10 @@ +Creature < UICreature + size: 80 80 + padding: 1 + image-source: /images/ui/panel_flat + image-border: 1 + border-width: 1 + border-color: alpha + + $checked: + border-color: white diff --git a/data/styles/10-items.otui b/data/styles/10-items.otui new file mode 100644 index 0000000..18bfa29 --- /dev/null +++ b/data/styles/10-items.otui @@ -0,0 +1,10 @@ +Item < UIItem + size: 34 34 + padding: 1 + image-source: /images/ui/item + font: verdana-11px-rounded + border-color: white + color: white + + $disabled: + color: #646464 diff --git a/data/styles/10-labels.otui b/data/styles/10-labels.otui new file mode 100644 index 0000000..8edfbb1 --- /dev/null +++ b/data/styles/10-labels.otui @@ -0,0 +1,23 @@ +Label < UILabel + font: verdana-11px-antialised + color: #dfdfdf + + $disabled: + color: #dfdfdf88 + +FlatLabel < UILabel + font: verdana-11px-antialised + color: #dfdfdf + size: 86 20 + text-offset: 3 3 + image-source: /images/ui/panel_flat + image-border: 1 + + $disabled: + color: #dfdfdf88 + +MenuLabel < Label + +GameLabel < UILabel + font: verdana-11px-antialised + color: #dfdfdf diff --git a/data/styles/10-listboxes.otui b/data/styles/10-listboxes.otui new file mode 100644 index 0000000..d52ff2f --- /dev/null +++ b/data/styles/10-listboxes.otui @@ -0,0 +1,19 @@ +TextList < UIScrollArea + layout: verticalBox + border-width: 1 + border-color: #272727 + background-color: #636363 + padding: 1 + auto-focus: none + +HorizontalList < UIScrollArea + layout: horizontalBox + border-width: 1 + border-color: #272727 + background-color: #636363 + +VerticalList < UIScrollArea + layout: verticalBox + border-width: 1 + border-color: #272727 + background-color: #636363 \ No newline at end of file diff --git a/data/styles/10-panels.otui b/data/styles/10-panels.otui new file mode 100644 index 0000000..15fbbc3 --- /dev/null +++ b/data/styles/10-panels.otui @@ -0,0 +1,19 @@ +Panel < UIWidget + phantom: true + auto-focus: first + +ScrollablePanel < UIScrollArea + phantom: true + auto-focus: first + +FlatPanel < Panel + image-source: /images/ui/panel_flat + image-border: 1 + +ScrollableFlatPanel < ScrollablePanel + image-source: /images/ui/panel_flat + image-border: 1 + +LightFlatPanel < Panel + image-source: /images/ui/panel_lightflat + image-border: 1 diff --git a/data/styles/10-progressbars.otui b/data/styles/10-progressbars.otui new file mode 100644 index 0000000..1946016 --- /dev/null +++ b/data/styles/10-progressbars.otui @@ -0,0 +1,28 @@ +ProgressBar < UIProgressBar + height: 16 + background-color: red + image-source: /images/ui/progressbar + image-border: 1 + font: verdana-11px-rounded + text-offset: 0 2 + + $!on: + visible: false + margin-top: 0 + margin-bottom: 0 + height: 0 + +LifeProgressBar < UIProgressBar + height: 16 + background-color: green + border: 1 black + font: verdana-11px-rounded + text-offset: 0 2 + margin: 2 + +ProgressRect < UIProgressRect + anchors.fill: parent + phantom: true + color: white + background-color: #00000088 + font: verdana-11px-rounded diff --git a/data/styles/10-scrollbars.otui b/data/styles/10-scrollbars.otui new file mode 100644 index 0000000..7f6b5d1 --- /dev/null +++ b/data/styles/10-scrollbars.otui @@ -0,0 +1,108 @@ +ScrollBarSlider < UIButton + id: sliderButton + anchors.centerIn: parent + size: 13 17 + image-source: /images/ui/scrollbar + image-clip: 0 26 13 13 + image-border: 2 + image-color: #ffffffff + $hover: + image-clip: 13 26 13 13 + $pressed: + image-clip: 26 26 13 13 + $disabled: + image-color: #ffffff66 + +ScrollBarValueLabel < Label + id: valueLabel + anchors.fill: parent + color: white + text-align: center + +VerticalScrollBar < UIScrollBar + orientation: vertical + width: 13 + height: 39 + image-source: /images/ui/scrollbar + image-clip: 39 0 13 65 + image-border: 1 + pixels-scroll: true + + UIButton + id: decrementButton + anchors.top: parent.top + anchors.left: parent.left + image-source: /images/ui/scrollbar + image-clip: 0 0 13 13 + image-color: #ffffffff + size: 13 13 + $hover: + image-clip: 13 0 13 13 + $pressed: + image-clip: 26 0 13 13 + $disabled: + image-color: #ffffff66 + + UIButton + id: incrementButton + anchors.bottom: parent.bottom + anchors.right: parent.right + size: 13 13 + image-source: /images/ui/scrollbar + image-clip: 0 13 13 13 + image-color: #ffffffff + $hover: + image-clip: 13 13 13 13 + $pressed: + image-clip: 26 13 13 13 + $disabled: + image-color: #ffffff66 + + ScrollBarSlider + + ScrollBarValueLabel + +HorizontalScrollBar < UIScrollBar + orientation: horizontal + height: 13 + width: 39 + image-source: /images/ui/scrollbar + image-clip: 0 65 52 13 + image-border: 1 + + $disabled: + color: #bbbbbb88 + + UIButton + id: decrementButton + anchors.top: parent.top + anchors.left: parent.left + image-source: /images/ui/scrollbar + image-clip: 0 39 13 13 + image-color: #ffffffff + size: 13 13 + $hover: + image-clip: 13 39 13 13 + $pressed: + image-clip: 26 39 13 13 + $disabled: + image-color: #ffffff66 + + UIButton + id: incrementButton + anchors.bottom: parent.bottom + anchors.right: parent.right + size: 13 13 + image-source: /images/ui/scrollbar + image-clip: 0 52 13 13 + image-color: #ffffffff + $hover: + image-clip: 13 52 13 13 + $pressed: + image-clip: 26 52 13 13 + $disabled: + image-color: #ffffff66 + + ScrollBarSlider + + ScrollBarValueLabel diff --git a/data/styles/10-separators.otui b/data/styles/10-separators.otui new file mode 100644 index 0000000..420d0fa --- /dev/null +++ b/data/styles/10-separators.otui @@ -0,0 +1,13 @@ +HorizontalSeparator < UIWidget + image-source: /images/ui/separator_horizontal + image-border: 1 + height: 2 + phantom: true + focusable: false + +VerticalSeparator < UIWidget + image-source: /images/ui/separator_vertical + image-border: 1 + width: 2 + phantom: true + focusable: false diff --git a/data/styles/10-splitters.otui b/data/styles/10-splitters.otui new file mode 100644 index 0000000..ca9a4f6 --- /dev/null +++ b/data/styles/10-splitters.otui @@ -0,0 +1,9 @@ +Splitter < UISplitter + size: 4 4 + opacity: 0 + background: #ffffff44 + +ResizeBorder < UIResizeBorder + size: 4 4 + opacity: 0 + background: #ffffff44 \ No newline at end of file diff --git a/data/styles/10-textedits.otui b/data/styles/10-textedits.otui new file mode 100644 index 0000000..b5d09bc --- /dev/null +++ b/data/styles/10-textedits.otui @@ -0,0 +1,20 @@ +TextEdit < UITextEdit + font: verdana-11px-antialised + color: #272727 + size: 86 22 + text-offset: 0 4 + opacity: 1 + padding: 4 + image-source: /images/ui/textedit + image-border: 1 + selection-color: #272727 + selection-background-color: #cccccc + $disabled: + color: #27272788 + opacity: 0.5 + +PasswordTextEdit < TextEdit + text-hidden: true + +MultilineTextEdit < TextEdit + multiline: true diff --git a/data/styles/10-windows.otui b/data/styles/10-windows.otui new file mode 100644 index 0000000..9250784 --- /dev/null +++ b/data/styles/10-windows.otui @@ -0,0 +1,35 @@ +Window < UIWindow + font: verdana-11px-antialised + size: 200 200 + opacity: 1 + color: #dfdfdf + text-offset: 0 6 + text-align: top + image-source: /images/ui/window + image-border: 6 + image-border-top: 27 + padding-top: 36 + padding-left: 16 + padding-right: 16 + padding-bottom: 16 + + $disabled: + color: #dfdfdf88 + + $dragging: + opacity: 0.8 + +HeadlessWindow < UIWindow + image-source: /images/ui/window_headless + image-border: 5 + padding: 5 + +MainWindow < Window + anchors.centerIn: parent + +StaticWindow < Window + &static: true + +StaticMainWindow < StaticWindow + anchors.centerIn: parent + \ No newline at end of file diff --git a/data/styles/20-imageview.otui b/data/styles/20-imageview.otui new file mode 100644 index 0000000..73bda84 --- /dev/null +++ b/data/styles/20-imageview.otui @@ -0,0 +1,6 @@ +ImageView < UIImageView + image-smooth: false + image-fixed-ratio: true + draggable: true + border-width: 2 + border-color: #000000 diff --git a/data/styles/20-popupmenus.otui b/data/styles/20-popupmenus.otui new file mode 100644 index 0000000..d7b71fc --- /dev/null +++ b/data/styles/20-popupmenus.otui @@ -0,0 +1,83 @@ +PopupMenuButton < UIButton + height: 18 + size: 0 21 + text-offset: 4 0 + text-align: left + font: verdana-11px-antialised + + color: #aaaaaa + background-color: alpha + + $hover !disabled: + color: #ffffff + background-color: #ffffff44 + image-clip: 0 40 20 20 + + $disabled: + color: #555555 + +PopupMenuShortcutLabel < Label + font: verdana-11px-antialised + text-align: right + anchors.fill: parent + margin-right: 2 + margin-left: 5 + +PopupMenuSeparator < UIWidget + margin-left: 2 + margin-right: 2 + margin-bottom: 1 + image-source: /images/ui/menubox + image-border-left: 1 + image-border-right: 1 + image-clip: 0 0 32 2 + height: 2 + phantom: true + +PopupMenu < UIPopupMenu + width: 50 + image-source: /images/ui/menubox + image-border: 3 + padding: 3 + +PopupScrollMenuButton < UIButton + height: 18 + size: 0 21 + text-offset: 4 0 + text-align: left + font: verdana-11px-antialised + + color: #aaaaaa + background-color: alpha + + $hover !disabled: + color: #ffffff + background-color: #ffffff44 + image-clip: 0 40 20 20 + + $disabled: + color: #555555 + +PopupScrollMenuShortcutLabel < Label + font: verdana-11px-antialised + text-align: right + anchors.fill: parent + margin-right: 2 + margin-left: 5 + +PopupScrollMenuSeparator < UIWidget + margin-left: 2 + margin-right: 2 + margin-bottom: 1 + image-source: /images/ui/menubox + image-border-left: 1 + image-border-right: 1 + image-clip: 0 0 32 2 + height: 2 + phantom: true + +PopupScrollMenu < UIPopupScrollMenu + width: 50 + image-source: /images/ui/menubox + image-border: 3 + padding: 3 diff --git a/data/styles/20-spinboxes.otui b/data/styles/20-spinboxes.otui new file mode 100644 index 0000000..1ceddf4 --- /dev/null +++ b/data/styles/20-spinboxes.otui @@ -0,0 +1,34 @@ +SpinBox < TextEdit + __class: UISpinBox + text-align: left + size: 86 22 + padding: 0 + padding-left: 2 + + Button + id: up + size: 11 11 + image-source: /images/ui/spinbox_up + image-border: 1 + image-clip: 0 0 10 10 + anchors.top: parent.top + anchors.right: parent.right + + $hover: + image-clip: 0 10 10 10 + $pressed: + image-clip: 0 20 10 10 + + Button + id: down + size: 11 11 + image-source: /images/ui/spinbox_down + image-border: 1 + image-clip: 0 0 10 10 + anchors.bottom: parent.bottom + anchors.right: parent.right + + $hover: + image-clip: 0 10 10 10 + $pressed: + image-clip: 0 20 10 10 diff --git a/data/styles/20-tabbars.otui b/data/styles/20-tabbars.otui new file mode 100644 index 0000000..06f242d --- /dev/null +++ b/data/styles/20-tabbars.otui @@ -0,0 +1,128 @@ +MoveableTabBar < UIMoveableTabBar + size: 80 21 +MoveableTabBarPanel < Panel +MoveableTabBarButton < UIButton + size: 20 21 + image-source: /images/ui/tabbutton_square + image-color: #dfdfdf + image-clip: 0 0 20 21 + image-border: 3 + image-border-bottom: 0 + icon-color: #dfdfdf + color: #dfdfdf + anchors.top: parent.top + anchors.left: parent.left + padding: 5 + + $hover !checked: + image-clip: 0 21 20 21 + color: #dfdfdf + + $disabled: + image-color: #dfdfdf88 + icon-color: #dfdfdf + + $checked: + image-clip: 0 42 20 21 + color: #dfdfdf + + $on !checked: + color: #de6f6f + +TabBar < UITabBar + size: 80 21 + Panel + id: buttonsPanel + anchors.fill: parent +TabBarPanel < Panel +TabBarButton < UIButton + size: 20 21 + image-source: /images/ui/tabbutton_square + image-source: /images/ui/tabbutton_square + image-color: #dfdfdf + image-clip: 0 0 20 21 + image-border: 3 + image-border-bottom: 0 + icon-color: #dfdfdf + color: #dfdfdf + anchors.top: parent.top + padding: 5 + + $first: + anchors.left: parent.left + + $!first: + anchors.left: prev.right + margin-left: 5 + + $hover !checked: + image-clip: 0 21 20 21 + color: #dfdfdf + + $disabled: + image-color: #dfdfdf88 + icon-color: #dfdfdf + + $checked: + image-clip: 0 42 20 21 + color: #dfdfdf + + $on !checked: + color: #dfdfdf + +TabBarRounded < TabBar +TabBarRoundedPanel < TabBarPanel +TabBarRoundedButton < TabBarButton + image-source: /images/ui/tabbutton_rounded + size: 22 23 + image-clip: 0 0 22 23 + + $hover !checked: + image-clip: 0 23 22 23 + + $checked: + image-clip: 0 46 22 23 + +TabBarVertical < UITabBar + width: 96 + ScrollableFlatPanel + id: buttonsPanel + anchors.top: parent.top + anchors.left: parent.left + anchors.right: scrollBar.left + anchors.bottom: parent.bottom + vertical-scrollbar: scrollBar + VerticalScrollBar + id: scrollBar + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + step: 16 + pixels-scroll: true + $!on: + width: 0 +TabBarVerticalPanel < Panel +TabBarVerticalButton < UIButton + size: 48 48 + color: #aaaaaa + anchors.left: parent.left + anchors.right: parent.right + text-align: bottom + icon-align: top + icon-offset-y: 2 + icon-color: #888888 + $first: + anchors.top: parent.top + $!first: + anchors.top: prev.bottom + margin-top: 10 + $hover !checked: + color: white + icon-color: #dfdfdf + $disabled: + icon-color: #333333 + $checked: + icon-color: #ffffff + color: #80c7f8 + $on !checked: + color: #F55E5E diff --git a/data/styles/20-tables.otui b/data/styles/20-tables.otui new file mode 100644 index 0000000..51e53e5 --- /dev/null +++ b/data/styles/20-tables.otui @@ -0,0 +1,62 @@ +Table < UITable + layout: verticalBox + header-column-style: TableHeaderColumn + header-row-style: TableHeaderRow + column-style: TableColumn + row-style: TableRow + +TableData < UIScrollArea + layout: verticalBox + +TableRow < UITableRow + layout: horizontalBox + height: 10 + text-wrap: true + focusable: true + even-background-color: alpha + odd-background-color: #00000022 + + $focus: + background-color: #294f6d + color: #ffffff + +TableColumn < Label + width: 30 + text-wrap: true + focusable: false + +TableHeaderRow < Label + layout: horizontalBox + focusable: false + height: 10 + text-wrap: true + +TableHeaderColumn < UITableHeaderColumn + font: verdana-11px-antialised + background-color: alpha + color: #dfdfdfff + height: 23 + focusable: true + text-offset: 0 0 + image-source: /images/ui/button + image-color: #dfdfdf + image-clip: 0 0 22 23 + image-border: 3 + padding: 5 10 5 10 + enabled: false + focusable: false + + $hover !disabled: + image-clip: 0 23 22 23 + + $pressed: + image-clip: 0 46 22 23 + text-offset: 1 1 + + $disabled: + color: #dfdfdf88 + opacity: 0.8 + +SortableTableHeaderColumn < TableHeaderColumn + enabled: true + focusable: true \ No newline at end of file diff --git a/data/styles/20-topmenu.otui b/data/styles/20-topmenu.otui new file mode 100644 index 0000000..51456fe --- /dev/null +++ b/data/styles/20-topmenu.otui @@ -0,0 +1,62 @@ +TopButton < UIButton + size: 26 26 + image-source: /images/ui/button_top + image-clip: 0 0 26 26 + image-border: 3 + image-color: #ffffffff + icon-color: #ffffffff + + $on: + image-source: /images/ui/button_top_blink + + $hover !disabled: + image-color: #ffffff99 + image-clip: 26 0 26 26 + + $pressed: + image-clip: 52 0 26 26 + + $disabled: + image-color: #ffffff44 + icon-color: #ffffff44 + +TopToggleButton < UIButton + size: 26 26 + image-source: /images/ui/button_top + image-clip: 0 0 26 26 + image-color: #ffffffff + image-border: 3 + icon-color: #ffffffff + + $hover !disabled: + image-color: #ffffff99 + image-clip: 26 0 26 26 + + $pressed: + image-clip: 52 0 26 26 + + $disabled: + image-color: #ffffff44 + icon-color: #ffffff44 + +TopMenuButtonsPanel < Panel + layout: + type: horizontalBox + spacing: 4 + fit-children: true + padding: 6 4 + +TopMenuPanel < Panel + height: 36 + image-source: /images/ui/panel_top + image-repeated: true + focusable: false + +TopMenuFrameCounterLabel < Label + font: verdana-11px-rounded + color: white + margin-top: 4 + margin-left: 5 + +TopMenuPingLabel < Label + font: verdana-11px-rounded diff --git a/data/styles/30-inputboxes.otui b/data/styles/30-inputboxes.otui new file mode 100644 index 0000000..90cbb65 --- /dev/null +++ b/data/styles/30-inputboxes.otui @@ -0,0 +1,30 @@ +InputBoxLabel < Label + fixed-size: true + text-align: left +InputBoxLineEdit < TextEdit +InputBoxTextEdit < MultilineTextEdit + text-wrap: true +InputBoxSpinBox < SpinBox +InputBoxCheckBox < CheckBox +InputBoxComboBox < ComboBox +InputBoxComboBoxPopupMenu < ComboBoxPopupMenu +InputBoxComboBoxPopupMenuButton < ComboBoxPopupMenuButton +InputBoxButton < Button + margin-left: 10 + fixed-size: true + +InputBoxButtonsPanel < Panel + height: 20 + margin-top: 4 + focusable: false + layout: + type: horizontalBox + align-right: true + +InputBoxWindow < MainWindow + __class: UIInputBox + width: 260 + layout: + type: verticalBox + fit-children: true + spacing: 2 \ No newline at end of file diff --git a/data/styles/30-messageboxes.otui b/data/styles/30-messageboxes.otui new file mode 100644 index 0000000..04fcdf6 --- /dev/null +++ b/data/styles/30-messageboxes.otui @@ -0,0 +1,15 @@ +MessageBoxLabel < Label + id: messageBoxLabel + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + text-wrap: true + text-auto-resize: true + +MessageBoxButtonHolder < UIWidget + id: buttonHolder + margin-top: 10 + anchors.bottom: parent.bottom + +MessageBoxButton < Button + margin-left: 10 + width: 80 diff --git a/data/styles/30-minimap.otui b/data/styles/30-minimap.otui new file mode 100644 index 0000000..09182b2 --- /dev/null +++ b/data/styles/30-minimap.otui @@ -0,0 +1,244 @@ +MinimapFlag < UIWidget + size: 11 11 + focusable: false + +MinimapCross < UIWidget + focusable: false + phantom: true + image: /images/game/minimap/cross + size: 16 16 + +MinimapFloorUpButton < Button + size: 20 20 + margin-right: 28 + margin-bottom: 28 + anchors.right: parent.right + anchors.bottom: parent.bottom + icon-source: /images/game/minimap/floor_up + icon-clip: 0 32 16 16 + $pressed: + icon-clip: 0 0 16 16 + $hover !pressed: + icon-clip: 0 16 16 16 + +MinimapFloorDownButton < Button + size: 20 20 + margin-right: 28 + margin-bottom: 4 + anchors.right: parent.right + anchors.bottom: parent.bottom + icon-source: /images/game/minimap/floor_down + icon-clip: 0 32 16 16 + $pressed: + icon-clip: 0 0 16 16 + $hover !pressed: + icon-clip: 0 16 16 16 + +MinimapZoomInButton < Button + text: + + size: 20 20 + margin-right: 4 + margin-bottom: 28 + anchors.right: parent.right + anchors.bottom: parent.bottom + //icon-source: /images/game/minimap/zoom_in + +MinimapZoomOutButton < Button + text: - + size: 20 20 + margin-right: 4 + margin-bottom: 4 + anchors.right: parent.right + anchors.bottom: parent.bottom + //icon-source: /images/game/minimap/zoom_out + +MinimapResetButton < Button + !text: tr('Center') + size: 44 20 + anchors.left: parent.left + anchors.top: parent.top + margin: 4 + +Minimap < UIMinimap + draggable: true + focusable: false + cross: true + color: black + + MinimapFloorUpButton + id: floorUpWidget + @onClick: self:getParent():floorUp(1) + + MinimapFloorDownButton + id: floorDownWidget + @onClick: self:getParent():floorDown(1) + + MinimapZoomInButton + id: zoomInWidget + @onClick: self:getParent():zoomIn() + + MinimapZoomOutButton + id: zoomOutWidget + @onClick: self:getParent():zoomOut() + + MinimapResetButton + id: resetWidget + @onClick: self:getParent():reset() + + +// Minimap Flag Create Window + + +MinimapFlagCheckBox < CheckBox + size: 15 15 + margin-left: 2 + image-source: /images/game/minimap/flagcheckbox + image-size: 15 15 + image-border: 3 + icon-source: /images/game/minimap/mapflags + icon-size: 11 11 + icon-offset: 2 4 + anchors.left: prev.right + anchors.top: prev.top + $!checked: + image-clip: 26 0 26 26 + $hover !checked: + image-clip: 78 0 26 26 + $checked: + image-clip: 0 0 26 26 + $hover checked: + image-clip: 52 0 26 26 + +MinimapFlagWindow < MainWindow + !text: tr('Create Map Mark') + size: 196 185 + + Label + !text: tr('Position') .. ':' + text-auto-resize: true + anchors.top: parent.top + anchors.left: parent.left + margin-top: 2 + + Label + id: position + text-auto-resize: true + anchors.top: parent.top + anchors.right: parent.right + margin-top: 2 + + Label + !text: tr('Description') .. ':' + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 7 + + TextEdit + id: description + margin-top: 3 + anchors.left: parent.left + anchors.top: prev.bottom + anchors.right: parent.right + + MinimapFlagCheckBox + id: flag0 + icon-source: /images/game/minimap/flag0 + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 6 + margin-left: 0 + + MinimapFlagCheckBox + id: flag1 + icon-source: /images/game/minimap/flag1 + + MinimapFlagCheckBox + id: flag2 + icon-source: /images/game/minimap/flag2 + + MinimapFlagCheckBox + id: flag3 + icon-source: /images/game/minimap/flag3 + + MinimapFlagCheckBox + id: flag4 + icon-source: /images/game/minimap/flag4 + + MinimapFlagCheckBox + id: flag5 + icon-source: /images/game/minimap/flag5 + + MinimapFlagCheckBox + id: flag6 + icon-source: /images/game/minimap/flag6 + + MinimapFlagCheckBox + id: flag7 + icon-source: /images/game/minimap/flag7 + + MinimapFlagCheckBox + id: flag8 + icon-source: /images/game/minimap/flag8 + + MinimapFlagCheckBox + id: flag9 + icon-source: /images/game/minimap/flag9 + + MinimapFlagCheckBox + id: flag10 + icon-source: /images/game/minimap/flag10 + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 6 + margin-left: 0 + + MinimapFlagCheckBox + id: flag11 + icon-source: /images/game/minimap/flag11 + + MinimapFlagCheckBox + id: flag12 + icon-source: /images/game/minimap/flag12 + + MinimapFlagCheckBox + id: flag13 + icon-source: /images/game/minimap/flag13 + + MinimapFlagCheckBox + id: flag14 + icon-source: /images/game/minimap/flag14 + + MinimapFlagCheckBox + id: flag15 + icon-source: /images/game/minimap/flag15 + + MinimapFlagCheckBox + id: flag16 + icon-source: /images/game/minimap/flag16 + + MinimapFlagCheckBox + id: flag17 + icon-source: /images/game/minimap/flag17 + + MinimapFlagCheckBox + id: flag18 + icon-source: /images/game/minimap/flag18 + + MinimapFlagCheckBox + id: flag19 + icon-source: /images/game/minimap/flag19 + + Button + id: okButton + !text: tr('Ok') + width: 64 + anchors.right: next.left + anchors.bottom: parent.bottom + margin-right: 10 + + Button + id: cancelButton + !text: tr('Cancel') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom diff --git a/data/styles/30-miniwindow.otui b/data/styles/30-miniwindow.otui new file mode 100644 index 0000000..465c3c2 --- /dev/null +++ b/data/styles/30-miniwindow.otui @@ -0,0 +1,130 @@ +MiniWindow < UIMiniWindow + font: verdana-11px-antialised + icon-rect: 4 4 16 16 + width: 192 + height: 200 + text-offset: 24 5 + text-align: topLeft + image-source: /images/ui/miniwindow + image-border: 4 + image-border-top: 23 + image-border-bottom: 4 + focusable: false + &minimizedHeight: 24 + + $on: + image-border-bottom: 2 + + UIWidget + id: miniwindowTopBar + anchors.top: parent.top + anchors.right: parent.right + anchors.left: parent.left + margin-right: 3 + margin-left: 3 + margin-top: 3 + size: 258 18 + phantom: true + + UIButton + id: closeButton + anchors.top: parent.top + anchors.right: parent.right + margin-top: 5 + margin-right: 5 + size: 14 14 + image-source: /images/ui/miniwindow_buttons + image-clip: 28 0 14 14 + + $hover: + image-clip: 28 14 14 14 + + $pressed: + image-clip: 28 28 14 14 + + UIButton + id: minimizeButton + anchors.top: closeButton.top + anchors.right: closeButton.left + margin-right: 3 + size: 14 14 + image-source: /images/ui/miniwindow_buttons + image-clip: 0 0 14 14 + + $hover: + image-clip: 0 14 14 14 + + $pressed: + image-clip: 0 28 14 14 + + $on: + image-clip: 14 0 14 14 + + $on hover: + image-clip: 14 14 14 14 + + $on pressed: + image-clip: 14 28 14 14 + + UIButton + id: lockButton + anchors.top: minimizeButton.top + anchors.right: minimizeButton.left + margin-right: 3 + size: 14 14 + image-source: /images/ui/miniwindow_buttons + image-clip: 112 0 14 14 + + $hover: + image-clip: 112 14 14 14 + + $pressed: + image-clip: 112 28 14 14 + + $on: + image-clip: 98 0 14 14 + + $on hover: + image-clip: 98 14 14 14 + + $on pressed: + image-clip: 98 28 14 14 + + VerticalScrollBar + id: miniwindowScrollBar + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + step: 14 + margin-top: 22 + margin-right: 3 + margin-bottom: 3 + pixels-scroll: true + + $!on: + width: 0 + + ResizeBorder + id: bottomResizeBorder + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 3 + minimum: 48 + margin-left: 3 + margin-right: 3 + background: #ffffff88 + +MiniWindowContents < ScrollablePanel + id: contentsPanel + anchors.fill: parent + anchors.right: miniwindowScrollBar.left + margin-left: 3 + margin-bottom: 3 + margin-top: 22 + margin-right: 1 + vertical-scrollbar: miniwindowScrollBar + +BorderlessGameWindow < UIWindow + focusable: false + margin: 2 diff --git a/data/things/.gitignore b/data/things/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/data/things/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..592101c --- /dev/null +++ b/init.lua @@ -0,0 +1,76 @@ +-- CONFIG +APP_NAME = "otclientv8" -- important, change it, it's name for config dir and files in appdata +APP_VERSION = 1337 -- client version for updater and login to indentify outdated client + +-- If you don't use updater or other service, set it to updater = "" +Services = { + website = "http://otclient.ovh", -- currently not used + updater = "http://otclient.ovh/api/updater.php", + news = "http://otclient.ovh/api/news.php", + stats = "", + crash = "http://otclient.ovh/api/crash.php", + feedback = "http://otclient.ovh/api/feedback.php" +} + +-- Servers accept http login url or ip:port:version +Servers = { + OTClientV8 = "http://otclient.ovh/api/login.php", + OTClientV8proxy = "http://otclient.ovh/api/login.php?proxy=1", + OTClientV8c = "otclient.ovh:7171:1099" +} +ALLOW_CUSTOM_SERVERS = true -- if true it will show option ANOTHER on server list +-- CONFIG END + +-- print first terminal message +g_logger.info(os.date("== application started at %b %d %Y %X")) +g_logger.info(g_app.getName() .. ' ' .. g_app.getVersion() .. ' rev ' .. g_app.getBuildRevision() .. ' (' .. g_app.getBuildCommit() .. ') made by ' .. g_app.getAuthor() .. ' built on ' .. g_app.getBuildDate() .. ' for arch ' .. g_app.getBuildArch()) + +if not g_resources.directoryExists("/data") then + g_logger.fatal("Data dir doesn't exist.") +end + +if not g_resources.directoryExists("/modules") then + g_logger.fatal("Modules dir doesn't exist.") +end + +-- send and delete crash report if exist +if Services.crash ~= nil and Services.crash:len() > 4 then + local crashLog = g_resources.readCrashLog(false) + local crashLogTxt = g_resources.readCrashLog(true) + local normalLog = g_logger.getLastLog() + local crashed = false + if crashLog:len() > 0 then + g_http.post(Services.crash .. "?txt=0", crashLog) + crashed = true + end + if crashLogTxt:len() > 0 then + g_http.post(Services.crash .. "?txt=1", crashLogTxt) + crashed = true + end + if crashed and normalLog:len() > 0 then + g_http.post(Services.crash .. "?txt=2", normalLog) + end + g_resources.deleteCrashLog() +end + +-- settings +g_configs.loadSettings("/config.otml") + +-- load mods +g_modules.discoverModules() + +-- libraries modules 0-99 +g_modules.autoLoadModules(99) +g_modules.ensureModuleLoaded("corelib") +g_modules.ensureModuleLoaded("gamelib") + +-- client modules 100-499 +g_modules.autoLoadModules(499) +g_modules.ensureModuleLoaded("client") + +-- game modules 500-999 +g_modules.autoLoadModules(999) +g_modules.ensureModuleLoaded("game_interface") + +-- mods 1000-9999 +g_modules.autoLoadModules(9999) \ No newline at end of file diff --git a/libEGL.dll b/libEGL.dll new file mode 100644 index 0000000..768b27b Binary files /dev/null and b/libEGL.dll differ diff --git a/libGLESv2.dll b/libGLESv2.dll new file mode 100644 index 0000000..d18c051 Binary files /dev/null and b/libGLESv2.dll differ diff --git a/modules/client/client.lua b/modules/client/client.lua new file mode 100644 index 0000000..9673dd5 --- /dev/null +++ b/modules/client/client.lua @@ -0,0 +1,113 @@ +local musicFilename = "/sounds/startup" +local musicChannel = nil + +function setMusic(filename) + musicFilename = filename + + if not g_game.isOnline() and musicChannel ~= nil then + musicChannel:stop() + musicChannel:enqueue(musicFilename, 3) + end +end + +function reloadScripts() + if g_game.getFeature(GameNoDebug) then + return + end + + g_textures.clearCache() + g_modules.reloadModules() + + local script = '/' .. g_app.getCompactName() .. 'rc.lua' + if g_resources.fileExists(script) then + dofile(script) + end + + local message = tr('All modules and scripts were reloaded.') + + modules.game_textmessage.displayGameMessage(message) + print(message) +end + +function startup() + if g_sounds ~= nil then + musicChannel = g_sounds.getChannel(1) + end + + G.UUID = g_settings.getString('report-uuid') + if not G.UUID or #G.UUID ~= 36 then + G.UUID = g_crypt.genUUID() + g_settings.set('report-uuid', G.UUID) + end + + -- Play startup music (The Silver Tree, by Mattias Westlund) + --musicChannel:enqueue(musicFilename, 3) + connect(g_game, { onGameStart = function() if musicChannel ~= nil then musicChannel:stop(3) end end }) + connect(g_game, { onGameEnd = function() + if g_sounds ~= nil then + g_sounds.stopAll() + --musicChannel:enqueue(musicFilename, 3) + end + end }) +end + +function init() + connect(g_app, { onRun = startup, + onExit = exit }) + + g_window.setMinimumSize({ width = 800, height = 600 }) + if g_sounds ~= nil then + --g_sounds.preload(musicFilename) + end + -- initialize in fullscreen mode on mobile devices + if g_window.getPlatformType() == "X11-EGL" then + g_window.setFullscreen(true) + else + -- window size + local size = { width = 800, height = 600 } + size = g_settings.getSize('window-size', size) + g_window.resize(size) + + -- window position, default is the screen center + local displaySize = g_window.getDisplaySize() + local defaultPos = { x = (displaySize.width - size.width)/2, + y = (displaySize.height - size.height)/2 } + local pos = g_settings.getPoint('window-pos', defaultPos) + pos.x = math.max(pos.x, 0) + pos.y = math.max(pos.y, 0) + g_window.move(pos) + + -- window maximized? + local maximized = g_settings.getBoolean('window-maximized', false) + if maximized then g_window.maximize() end + end + + g_window.setTitle(g_app.getName()) + g_window.setIcon('/images/clienticon') + + -- poll resize events + g_window.poll() + + g_keyboard.bindKeyDown('Ctrl+Shift+R', reloadScripts) + g_keyboard.bindKeyDown('Ctrl+Shift+[', function() g_extras.setTestMode((g_extras.getTestMode() - 1) % 10) end) + g_keyboard.bindKeyDown('Ctrl+Shift+]', function() g_extras.setTestMode((g_extras.getTestMode() + 1) % 10) end) + + -- generate machine uuid, this is a security measure for storing passwords + if not g_crypt.setMachineUUID(g_settings.get('uuid')) then + g_settings.set('uuid', g_crypt.getMachineUUID()) + g_settings.save() + end +end + +function terminate() + disconnect(g_app, { onRun = startup, + onExit = exit }) + -- save window configs + g_settings.set('window-size', g_window.getUnmaximizedSize()) + g_settings.set('window-pos', g_window.getUnmaximizedPos()) + g_settings.set('window-maximized', g_window.isMaximized()) +end + +function exit() + g_logger.info("Exiting application..") +end diff --git a/modules/client/client.otmod b/modules/client/client.otmod new file mode 100644 index 0000000..df46432 --- /dev/null +++ b/modules/client/client.otmod @@ -0,0 +1,23 @@ +Module + name: client + description: Initialize the client and setups its main window + author: edubart + website: https://github.com/edubart/otclient + reloadable: false + sandboxed: true + scripts: [ client ] + @onLoad: init() + @onUnload: terminate() + + load-later: + - client_styles + - client_locales + - client_topmenu + - client_background + - client_options + - client_entergame + - client_terminal + - client_stats + - client_news + - client_feedback + - client_updater diff --git a/modules/client_background/background.lua b/modules/client_background/background.lua new file mode 100644 index 0000000..4fdd175 --- /dev/null +++ b/modules/client_background/background.lua @@ -0,0 +1,50 @@ +-- private variables +local background +local clientVersionLabel + +-- public functions +function init() + background = g_ui.displayUI('background') + background:lower() + + clientVersionLabel = background:getChildById('clientVersionLabel') + clientVersionLabel:setText(g_app.getName() .. ' ' .. g_app.getVersion() .. '\nMade by:\n' .. g_app.getAuthor() .. "\notclient@otclient.ovh") + + + if not g_game.isOnline() then + addEvent(function() g_effects.fadeIn(clientVersionLabel, 1500) end) + end + + connect(g_game, { onGameStart = hide }) + connect(g_game, { onGameEnd = show }) +end + +function terminate() + disconnect(g_game, { onGameStart = hide }) + disconnect(g_game, { onGameEnd = show }) + + g_effects.cancelFade(background:getChildById('clientVersionLabel')) + background:destroy() + + Background = nil +end + +function hide() + background:hide() +end + +function show() + background:show() +end + +function hideVersionLabel() + background:getChildById('clientVersionLabel'):hide() +end + +function setVersionText(text) + clientVersionLabel:setText(text) +end + +function getBackground() + return background +end \ No newline at end of file diff --git a/modules/client_background/background.otmod b/modules/client_background/background.otmod new file mode 100644 index 0000000..d4c3f09 --- /dev/null +++ b/modules/client_background/background.otmod @@ -0,0 +1,10 @@ +Module + name: client_background + description: Handles the background of the login screen + author: edubart + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ background ] + dependencies: [ client_topmenu ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/client_background/background.otui b/modules/client_background/background.otui new file mode 100644 index 0000000..67ab015 --- /dev/null +++ b/modules/client_background/background.otui @@ -0,0 +1,24 @@ +Panel + id: background + image-source: /images/background + image-smooth: true + image-fixed-ratio: true + anchors.top: topMenu.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + margin-top: 1 + focusable: false + + UILabel + id: clientVersionLabel + background-color: #00000099 + anchors.right: parent.right + anchors.bottom: parent.bottom + text-align: center + text-auto-resize: false + width: 220 + height: 90 + padding: 2 + color: #ffffff + font: terminus-14px-bold \ No newline at end of file diff --git a/modules/client_entergame/characterlist.lua b/modules/client_entergame/characterlist.lua new file mode 100644 index 0000000..9bdd759 --- /dev/null +++ b/modules/client_entergame/characterlist.lua @@ -0,0 +1,373 @@ +CharacterList = { } + +-- private variables +local charactersWindow +local loadBox +local characterList +local errorBox +local waitingWindow +local updateWaitEvent +local resendWaitEvent +local loginEvent + +-- private functions +local function tryLogin(charInfo, tries) + tries = tries or 1 + + if tries > 50 then + return + end + + if g_game.isOnline() then + if tries == 1 then + g_game.safeLogout() + end + loginEvent = scheduleEvent(function() tryLogin(charInfo, tries+1) end, 100) + return + end + + CharacterList.hide() + + -- proxies for not http login users + if charInfo.worldHost == "0.0.0.0" and g_proxy then + g_proxy.clear() + -- g_proxy.addProxy(localPort, proxyHost, proxyPort, proxyPriority) + g_proxy.addProxy(tonumber(charInfo.worldPort), "51.158.184.57", 7162, 0) + g_proxy.addProxy(tonumber(charInfo.worldPort), "54.39.190.20", 7162, 0) + g_proxy.addProxy(tonumber(charInfo.worldPort), "51.83.226.109", 7162, 0) + g_proxy.addProxy(tonumber(charInfo.worldPort), "35.247.201.100", 443, 0) + end + + g_game.loginWorld(G.account, G.password, charInfo.worldName, charInfo.worldHost, charInfo.worldPort, charInfo.characterName, G.authenticatorToken, G.sessionKey) + g_logger.info("Login to " .. charInfo.worldHost .. ":" .. charInfo.worldPort) + loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to game server...')) + connect(loadBox, { onCancel = function() + loadBox = nil + g_game.cancelLogin() + CharacterList.show() + end }) + + -- save last used character + g_settings.set('last-used-character', charInfo.characterName) + g_settings.set('last-used-world', charInfo.worldName) +end + +local function updateWait(timeStart, timeEnd) + if waitingWindow then + local time = g_clock.seconds() + if time <= timeEnd then + local percent = ((time - timeStart) / (timeEnd - timeStart)) * 100 + local timeStr = string.format("%.0f", timeEnd - time) + + local progressBar = waitingWindow:getChildById('progressBar') + progressBar:setPercent(percent) + + local label = waitingWindow:getChildById('timeLabel') + label:setText(tr('Trying to reconnect in %s seconds.', timeStr)) + + updateWaitEvent = scheduleEvent(function() updateWait(timeStart, timeEnd) end, 1000 * progressBar:getPercentPixels() / 100 * (timeEnd - timeStart)) + return true + end + end + + if updateWaitEvent then + updateWaitEvent:cancel() + updateWaitEvent = nil + end +end + +local function resendWait() + if waitingWindow then + waitingWindow:destroy() + waitingWindow = nil + + if updateWaitEvent then + updateWaitEvent:cancel() + updateWaitEvent = nil + end + + if charactersWindow then + local selected = characterList:getFocusedChild() + if selected then + local charInfo = { worldHost = selected.worldHost, + worldPort = selected.worldPort, + worldName = selected.worldName, + characterName = selected.characterName } + tryLogin(charInfo) + end + end + end +end + +local function onLoginWait(message, time) + CharacterList.destroyLoadBox() + + waitingWindow = g_ui.displayUI('waitinglist') + + local label = waitingWindow:getChildById('infoLabel') + label:setText(message) + + updateWaitEvent = scheduleEvent(function() updateWait(g_clock.seconds(), g_clock.seconds() + time) end, 0) + resendWaitEvent = scheduleEvent(resendWait, time * 1000) +end + +function onGameLoginError(message) + CharacterList.destroyLoadBox() + errorBox = displayErrorBox(tr("Login Error"), message) + errorBox.onOk = function() + errorBox = nil + CharacterList.showAgain() + end +end + +function onGameLoginToken(unknown) + CharacterList.destroyLoadBox() + -- TODO: make it possible to enter a new token here / prompt token + errorBox = displayErrorBox(tr("Two-Factor Authentification"), 'A new authentification token is required.\nPlease login again.') + errorBox.onOk = function() + errorBox = nil + EnterGame.show() + end +end + +function onGameConnectionError(message, code) + CharacterList.destroyLoadBox() + local text = translateNetworkError(code, g_game.getProtocolGame() and g_game.getProtocolGame():isConnecting(), message) + errorBox = displayErrorBox(tr("Connection Error"), text) + errorBox.onOk = function() + errorBox = nil + CharacterList.showAgain() + end +end + +function onGameUpdateNeeded(signature) + CharacterList.destroyLoadBox() + errorBox = displayErrorBox(tr("Update needed"), tr('Enter with your account again to update your client.')) + errorBox.onOk = function() + errorBox = nil + CharacterList.showAgain() + end +end + +-- public functions +function CharacterList.init() + connect(g_game, { onLoginError = onGameLoginError }) + connect(g_game, { onLoginToken = onGameLoginToken }) + connect(g_game, { onUpdateNeeded = onGameUpdateNeeded }) + connect(g_game, { onConnectionError = onGameConnectionError }) + connect(g_game, { onGameStart = CharacterList.destroyLoadBox }) + connect(g_game, { onLoginWait = onLoginWait }) + connect(g_game, { onGameEnd = CharacterList.showAgain }) + + if G.characters then + CharacterList.create(G.characters, G.characterAccount) + end +end + +function CharacterList.terminate() + disconnect(g_game, { onLoginError = onGameLoginError }) + disconnect(g_game, { onLoginToken = onGameLoginToken }) + disconnect(g_game, { onUpdateNeeded = onGameUpdateNeeded }) + disconnect(g_game, { onConnectionError = onGameConnectionError }) + disconnect(g_game, { onGameStart = CharacterList.destroyLoadBox }) + disconnect(g_game, { onLoginWait = onLoginWait }) + disconnect(g_game, { onGameEnd = CharacterList.showAgain }) + + if charactersWindow then + characterList = nil + charactersWindow:destroy() + charactersWindow = nil + end + + if loadBox then + g_game.cancelLogin() + loadBox:destroy() + loadBox = nil + end + + if waitingWindow then + waitingWindow:destroy() + waitingWindow = nil + end + + if updateWaitEvent then + removeEvent(updateWaitEvent) + updateWaitEvent = nil + end + + if resendWaitEvent then + removeEvent(resendWaitEvent) + resendWaitEvent = nil + end + + if loginEvent then + removeEvent(loginEvent) + loginEvent = nil + end + + CharacterList = nil +end + +function CharacterList.create(characters, account, otui) + if not otui then otui = 'characterlist' end + + if charactersWindow then + charactersWindow:destroy() + end + + charactersWindow = g_ui.displayUI(otui) + characterList = charactersWindow:getChildById('characters') + + -- characters + G.characters = characters + G.characterAccount = account + + characterList:destroyChildren() + local accountStatusLabel = charactersWindow:getChildById('accountStatusLabel') + + local focusLabel + for i,characterInfo in ipairs(characters) do + local widget = g_ui.createWidget('CharacterWidget', characterList) + for key,value in pairs(characterInfo) do + local subWidget = widget:getChildById(key) + if subWidget then + if key == 'outfit' then -- it's an exception + subWidget:setOutfit(value) + else + local text = value + if subWidget.baseText and subWidget.baseTranslate then + text = tr(subWidget.baseText, text) + elseif subWidget.baseText then + text = string.format(subWidget.baseText, text) + end + subWidget:setText(text) + end + end + end + + -- these are used by login + widget.characterName = characterInfo.name + widget.worldName = characterInfo.worldName + widget.worldHost = characterInfo.worldIp + widget.worldPort = characterInfo.worldPort + + connect(widget, { onDoubleClick = function () CharacterList.doLogin() return true end } ) + + if i == 1 or (g_settings.get('last-used-character') == widget.characterName and g_settings.get('last-used-world') == widget.worldName) then + focusLabel = widget + end + end + + if focusLabel then + characterList:focusChild(focusLabel, KeyboardFocusReason) + addEvent(function() characterList:ensureChildVisible(focusLabel) end) + end + + -- account + local status = '' + if account.status == AccountStatus.Frozen then + status = tr(' (Frozen)') + elseif account.status == AccountStatus.Suspended then + status = tr(' (Suspended)') + end + + if account.subStatus == SubscriptionStatus.Free then + accountStatusLabel:setText(('%s%s'):format(tr('Free Account'), status)) + elseif account.subStatus == SubscriptionStatus.Premium then + if account.premDays == 0 or account.premDays == 65535 then + accountStatusLabel:setText(('%s%s'):format(tr('Gratis Premium Account'), status)) + else + accountStatusLabel:setText(('%s%s'):format(tr('Premium Account (%s) days left', account.premDays), status)) + end + end + + if account.premDays > 0 and account.premDays <= 7 then + accountStatusLabel:setOn(true) + else + accountStatusLabel:setOn(false) + end +end + +function CharacterList.destroy() + CharacterList.hide(true) + + if charactersWindow then + characterList = nil + charactersWindow:destroy() + charactersWindow = nil + end +end + +function CharacterList.show() + if loadBox or errorBox or not charactersWindow then return end + charactersWindow:show() + charactersWindow:raise() + charactersWindow:focus() +end + +function CharacterList.hide(showLogin) + showLogin = showLogin or false + charactersWindow:hide() + + if showLogin and EnterGame and not g_game.isOnline() then + EnterGame.show() + end +end + +function CharacterList.showAgain() + if characterList and characterList:hasChildren() then + CharacterList.show() + end +end + +function CharacterList.isVisible() + if charactersWindow and charactersWindow:isVisible() then + return true + end + return false +end + +function CharacterList.doLogin() + local selected = characterList:getFocusedChild() + if selected then + local charInfo = { worldHost = selected.worldHost, + worldPort = selected.worldPort, + worldName = selected.worldName, + characterName = selected.characterName } + charactersWindow:hide() + if loginEvent then + removeEvent(loginEvent) + loginEvent = nil + end + tryLogin(charInfo) + else + displayErrorBox(tr('Error'), tr('You must select a character to login!')) + end +end + +function CharacterList.destroyLoadBox() + if loadBox then + loadBox:destroy() + loadBox = nil + end +end + +function CharacterList.cancelWait() + if waitingWindow then + waitingWindow:destroy() + waitingWindow = nil + end + + if updateWaitEvent then + removeEvent(updateWaitEvent) + updateWaitEvent = nil + end + + if resendWaitEvent then + removeEvent(resendWaitEvent) + resendWaitEvent = nil + end + + CharacterList.destroyLoadBox() + CharacterList.showAgain() +end diff --git a/modules/client_entergame/characterlist.otui b/modules/client_entergame/characterlist.otui new file mode 100644 index 0000000..3a49027 --- /dev/null +++ b/modules/client_entergame/characterlist.otui @@ -0,0 +1,134 @@ +CharacterWidget < UIWidget + height: 14 + background-color: alpha + &updateOnStates: | + function(self) + local children = self:getChildren() + for i=1,#children do + children[i]:setOn(self:isFocused()) + end + end + @onFocusChange: self:updateOnStates() + @onSetup: self:updateOnStates() + + $focus: + background-color: #ffffff22 + + Label + id: name + color: #bbbbbb + anchors.top: parent.top + anchors.left: parent.left + font: verdana-11px-monochrome + text-auto-resize: true + background-color: alpha + text-offset: 2 0 + + $on: + color: #ffffff + + Label + id: worldName + color: #bbbbbb + anchors.top: parent.top + anchors.right: parent.right + margin-right: 5 + font: verdana-11px-monochrome + text-auto-resize: true + background-color: alpha + &baseText: '(%s)' + + $on: + color: #ffffff + +StaticMainWindow + id: charactersWindow + !text: tr('Character List') + visible: false + @onEnter: CharacterList.doLogin() + @onEscape: CharacterList.hide(true) + @onSetup: | + g_keyboard.bindKeyPress('Up', function() self:getChildById('characters'):focusPreviousChild(KeyboardFocusReason) end, self) + g_keyboard.bindKeyPress('Down', function() self:getChildById('characters'):focusNextChild(KeyboardFocusReason) end, self) + if g_game.getFeature(GamePreviewState) then + self:setSize({width = 350, height = 400}) + else + self:setSize({width = 250, height = 248}) + end + + TextList + id: characters + background-color: #565656 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: characterListScrollBar.left + anchors.bottom: accountStatusCaption.top + margin-bottom: 5 + padding: 1 + focusable: false + vertical-scrollbar: characterListScrollBar + auto-focus: first + + VerticalScrollBar + id: characterListScrollBar + anchors.top: parent.top + anchors.bottom: accountStatusCaption.top + anchors.right: parent.right + margin-bottom: 5 + step: 14 + pixels-scroll: true + + Label + id: accountStatusCaption + !text: tr('Account Status') .. ':' + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: next.top + margin-bottom: 1 + + Label + id: accountStatusLabel + !text: tr('Free Account') + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: separator.top + margin-bottom: 5 + text-auto-resize: true + + $on: + color: #FF0000 + + HorizontalSeparator + id: separator + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: next.top + margin-bottom: 10 + + //CheckBox + // id: charAutoLoginBox + // !text: tr('Auto login') + // !tooltip: tr('Auto login selected character on next charlist load') + // anchors.left: parent.left + // anchors.right: parent.right + // anchors.bottom: next.top + // margin-bottom: 6 + // margin-left: 18 + // margin-right: 18 + + Button + id: buttonOk + !text: tr('Ok') + width: 64 + anchors.right: next.left + anchors.bottom: parent.bottom + margin-right: 10 + @onClick: CharacterList.doLogin() + + Button + id: buttonCancel + !text: tr('Cancel') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: CharacterList.hide(true) diff --git a/modules/client_entergame/entergame.lua b/modules/client_entergame/entergame.lua new file mode 100644 index 0000000..9432208 --- /dev/null +++ b/modules/client_entergame/entergame.lua @@ -0,0 +1,495 @@ +EnterGame = { } + +-- private variables +local loadBox +local enterGame +local enterGameButton +local clientBox +local protocolLogin +local server = nil +local versionsFound = false + +local newLogin = nil +local newLoginUrl = nil +local newLoginEvent + +local customServerSelectorPanel +local serverSelectorPanel +local serverSelector +local clientVersionSelector +local serverHostTextEdit +local rememberPasswordBox +local protos = {"740", "760", "772", "800", "810", "854", "860", "1090", "1096", "1099"} + + +-- private functions +local function onProtocolError(protocol, message, errorCode) + if errorCode then + return EnterGame.onError(message) + end + return EnterGame.onLoginError(message) +end + +local function onSessionKey(protocol, sessionKey) + G.sessionKey = sessionKey +end + +local function onCharacterList(protocol, characters, account, otui) + if rememberPasswordBox:isChecked() then + local account = g_crypt.encrypt(G.account) + local password = g_crypt.encrypt(G.password) + + g_settings.set('account', account) + g_settings.set('password', password) + else + EnterGame.clearAccountFields() + end + + for _, characterInfo in pairs(characters) do + if characterInfo.previewState and characterInfo.previewState ~= PreviewState.Default then + characterInfo.worldName = characterInfo.worldName .. ', Preview' + end + end + + if loadBox then + loadBox:destroy() + loadBox = nil + end + + CharacterList.create(characters, account, otui) + CharacterList.show() + + g_settings.save() +end + +local function onUpdateNeeded(protocol, signature) + return EnterGame.onError(tr('Your client needs updating, try redownloading it.')) +end + +local function parseFeatures(features) + for feature_id, value in pairs(features) do + if value == "1" or value == "true" or value == true then + g_game.enableFeature(feature_id) + else + g_game.disableFeature(feature_id) + end + end +end + +local function validateThings(things) + local incorrectThings = "" + if things ~= nil then + local thingsNode = {} + for thingtype, thingdata in pairs(things) do + thingsNode[thingtype] = thingdata[1] + if not g_resources.fileExists("/data/things/" .. thingdata[1]) then + correctThings = false + incorrectThings = incorrectThings .. "Missing file: " .. thingdata[1] .. "\n" + end + local localChecksum = g_resources.fileChecksum("/data/things/" .. thingdata[1]):lower() + if localChecksum ~= thingdata[2]:lower() and #thingdata[2] > 1 then + if g_resources.isLoadedFromArchive() then -- ignore checksum if it's test/debug version + incorrectThings = incorrectThings .. "Invalid checksum of file: " .. thingdata[1] .. " (is " .. localChecksum .. ", should be " .. thingdata[2]:lower() .. ")\n" + end + end + end + g_settings.setNode("things", thingsNode) + else + g_settings.setNode("things", {}) + end + return incorrectThings +end + +local function onHTTPResult(data, err) + if err then + return EnterGame.onError(err) + end + if data['error'] and #data['error'] > 0 then + return EnterGame.onLoginError(data['error']) + end + + local characters = data["characters"] + local account = data["account"] + local session = data["session"] + + local version = data["version"] + local things = data["things"] + local customProtocol = data["customProtocol"] + + local features = data["features"] + local settings = data["settings"] + local rsa = data["rsa"] + local proxies = data["proxies"] + + local incorrectThings = validateThings(things) + if #incorrectThings > 0 then + g_logger.info(incorrectThings) + if Updater then + return Updater.updateThings(things, incorrectThings) + else + return EnterGame.onError(incorrectThings) + end + end + + -- custom protocol + g_game.setCustomProtocolVersion(0) + if customProtocol ~= nil then + customProtocol = tonumber(customProtocol) + if customProtocol ~= nil and customProtocol > 0 then + g_game.setCustomProtocolVersion(customProtocol) + end + end + + -- force player settings + if settings ~= nil then + for option, value in pairs(settings) do + modules.client_options.setOption(option, value, true) + end + end + + -- version + G.clientVersion = version + g_game.setClientVersion(version) + g_game.setProtocolVersion(g_game.getClientProtocolVersion(version)) + g_game.setCustomOs(-1) -- disable + + if rsa ~= nil then + g_game.setRsa(rsa) + end + + if features ~= nil then + parseFeatures(features) + end + + if session ~= nil and session:len() > 0 then + onSessionKey(nil, session) + end + + -- proxies + if g_proxy then + g_proxy.clear() + if proxies then + for i, proxy in ipairs(proxies) do + g_proxy.addProxy(tonumber(proxy["localPort"]), proxy["host"], tonumber(proxy["port"]), tonumber(proxy["priority"])) + end + end + end + + onCharacterList(nil, characters, account, nil) +end + + +-- public functions +function EnterGame.init() + enterGame = g_ui.displayUI('entergame') + newLogin = g_ui.displayUI('entergame_new') + + serverSelectorPanel = enterGame:getChildById('serverSelectorPanel') + customServerSelectorPanel = enterGame:getChildById('customServerSelectorPanel') + + serverSelector = serverSelectorPanel:getChildById('serverSelector') + rememberPasswordBox = enterGame:getChildById('rememberPasswordBox') + serverHostTextEdit = customServerSelectorPanel:getChildById('serverHostTextEdit') + clientVersionSelector = customServerSelectorPanel:getChildById('clientVersionSelector') + + if Servers ~= nil then + for name,server in pairs(Servers) do + serverSelector:addOption(name) + end + end + if serverSelector:getOptionsCount() == 0 or ALLOW_CUSTOM_SERVERS then + serverSelector:addOption(tr("Another")) + end + for i,proto in pairs(protos) do + clientVersionSelector:addOption(proto) + end + + if serverSelector:getOptionsCount() == 1 then + enterGame:setHeight(enterGame:getHeight() - serverSelectorPanel:getHeight()) + serverSelectorPanel:setOn(false) + end + + local account = g_crypt.decrypt(g_settings.get('account')) + local password = g_crypt.decrypt(g_settings.get('password')) + local server = g_settings.get('server') + local host = g_settings.get('host') + local clientVersion = g_settings.get('client-version') + local hdSprites = g_settings.getBoolean('hdSprites', false) + + if serverSelector:isOption(server) then + serverSelector:setCurrentOption(server, false) + if Servers == nil or Servers[server] == nil then + serverHostTextEdit:setText(host) + end + clientVersionSelector:setOption(clientVersion) + else + server = "" + host = "" + end + + enterGame:getChildById('accountPasswordTextEdit'):setText(password) + enterGame:getChildById('accountNameTextEdit'):setText(account) + rememberPasswordBox:setChecked(#account > 0) + + if enterGame.hdSprites then + enterGame.hdSprites:setChecked(hdSprites) + end + + g_keyboard.bindKeyDown('Ctrl+G', EnterGame.openWindow) + + if g_game.isOnline() then + return EnterGame.hide() + end + + scheduleEvent(function() + EnterGame.show() + end, 100) +end + +function EnterGame.terminate() + g_keyboard.unbindKeyDown('Ctrl+G') + + removeEvent(newLoginEvent) + + enterGame:destroy() + if newLogin then + newLogin:destroy() + end + if loadBox then + loadBox:destroy() + loadBox = nil + end + if protocolLogin then + protocolLogin:cancelLogin() + protocolLogin = nil + end + EnterGame = nil +end + +function EnterGame.show() + if Updater and Updater.isVisible() or g_game.isOnline() then + return EnterGame.hide() + end + enterGame:show() + enterGame:raise() + enterGame:focus() + enterGame:getChildById('accountNameTextEdit'):focus() + EnterGame.checkNewLogin() +end + +function EnterGame.hide() + enterGame:hide() + newLogin:hide() +end + +function EnterGame.openWindow() + if g_game.isOnline() then + CharacterList.show() + elseif not g_game.isLogging() and not CharacterList.isVisible() then + EnterGame.show() + end +end + +function EnterGame.clearAccountFields() + enterGame:getChildById('accountNameTextEdit'):clearText() + enterGame:getChildById('accountPasswordTextEdit'):clearText() + --enterGame:getChildById('authenticatorTokenTextEdit'):clearText() + enterGame:getChildById('accountNameTextEdit'):focus() + g_settings.remove('account') + g_settings.remove('password') +end + +function EnterGame.hideNewLogin() + newLogin:hide() + newLoginUrl = nil +end + +function EnterGame.checkNewLoginEvent() + newLoginEvent = scheduleEvent(function() EnterGame.checkNewLoginEvent() end, 1000) + EnterGame.checkNewLogin() +end + +function EnterGame.checkNewLogin() + if not newLoginUrl then + return + end + local url = newLoginUrl + HTTP.postJSON(newLoginUrl, { quick = 1 }, function(data, err) + if url ~= newLoginUrl then return end + if err then return end + if not data["qrcode"] then return end + if newLogin:isHidden() then + newLogin:show() + enterGame:raise() + end + newLogin.qrcode:setImageSourceBase64(data["qrcode"]) + newLogin.code:setText(data["code"]) + end) +end + +function EnterGame.onServerChange() + server = serverSelector:getText() + EnterGame.hideNewLogin() + if server == tr("Another") then + if not customServerSelectorPanel:isOn() then + serverHostTextEdit:setText("") + customServerSelectorPanel:setOn(true) + enterGame:setHeight(enterGame:getHeight() + customServerSelectorPanel:getHeight()) + end + elseif customServerSelectorPanel:isOn() then + enterGame:setHeight(enterGame:getHeight() - customServerSelectorPanel:getHeight()) + customServerSelectorPanel:setOn(false) + end + if Servers and Servers[server] ~= nil then + serverHostTextEdit:setText(Servers[server]) + newLoginUrl = Servers[server] + EnterGame.checkNewLogin() + end +end + +function EnterGame.doLogin() + if Updater and Updater.isVisible() then + return + end + if g_game.isOnline() then + local errorBox = displayErrorBox(tr('Login Error'), tr('Cannot login while already in game.')) + connect(errorBox, { onOk = EnterGame.show }) + return + end + + G.account = enterGame:getChildById('accountNameTextEdit'):getText() + G.password = enterGame:getChildById('accountPasswordTextEdit'):getText() + --G.authenticatorToken = enterGame:getChildById('authenticatorTokenTextEdit'):getText() + G.authenticatorToken = "" + G.hdSprites = enterGame.hdSprites and enterGame.hdSprites:isChecked() + G.stayLogged = true + G.server = serverSelector:getText():trim() + G.host = serverHostTextEdit:getText() + G.clientVersion = tonumber(clientVersionSelector:getText()) + + if not rememberPasswordBox:isChecked() then + g_settings.set('account', G.account) + g_settings.set('password', G.password) + end + g_settings.set('host', G.host) + g_settings.set('server', G.server) + g_settings.set('client-version', G.clientVersion) + g_settings.set('hdSprites', G.hdSprites) + g_settings.save() + + if G.host:find("http") ~= nil then + return EnterGame.doLoginHttp() + end + + local server_params = G.host:split(":") + if #server_params < 2 then + return EnterGame.onError("Invalid server, it should be in format IP:PORT or it should be http url to login script") + end + local server_ip = server_params[1] + local server_port = tonumber(server_params[2]) + if #server_params >= 3 then + G.clientVersion = tonumber(server_params[3]) + end + if not server_port or not G.clientVersion then + return EnterGame.onError("Invalid server, it should be in format IP:PORT or it should be http url to login script") + end + + local things = { + data = {G.clientVersion .. "/Tibia.dat", ""}, + sprites = {G.clientVersion .. "/Tibia.spr", ""}, + } + + if G.hdSprites then + things.sprites_hd = {G.clientVersion .. "/Tibia_hd.spr", ""} + end + + local incorrectThings = validateThings(things) + if #incorrectThings > 0 then + g_logger.info(incorrectThings) + if Updater then + return Updater.updateThings(things, incorrectThings) + else + return EnterGame.onError(incorrectThings) + end + end + + protocolLogin = ProtocolLogin.create() + protocolLogin.onLoginError = onProtocolError + protocolLogin.onSessionKey = onSessionKey + protocolLogin.onCharacterList = onCharacterList + protocolLogin.onUpdateNeeded = onUpdateNeeded + + EnterGame.hide() + loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to login server...')) + connect(loadBox, { onCancel = function(msgbox) + loadBox = nil + protocolLogin:cancelLogin() + EnterGame.show() + end }) + + -- if you have custom rsa or protocol edit it here + g_game.setClientVersion(G.clientVersion) + g_game.setProtocolVersion(g_game.getClientProtocolVersion(G.clientVersion)) + g_game.setCustomProtocolVersion(0) + g_game.chooseRsa(G.host) + g_game.setCustomOs(2) -- windows + + -- you can add custom features here + g_game.enableFeature(GameBot) + + -- proxies + if g_proxy then + g_proxy.clear() + end + + if modules.game_things.isLoaded() then + g_logger.info("Connection to: " .. server_ip .. ":" .. server_port) + protocolLogin:login(server_ip, server_port, G.account, G.password, G.authenticatorToken, G.stayLogged) + else + loadBox:destroy() + loadBox = nil + EnterGame.show() + end +end + +function EnterGame.doLoginHttp() + if G.host == nil or G.host:len() < 10 then + return EnterGame.onError("Invalid server url: " .. G.host) + end + + loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to login server...')) + connect(loadBox, { onCancel = function(msgbox) + loadBox = nil + EnterGame.show() + end }) + + local data = { + account = G.account, + password = G.password, + token = G.authenticatorToken, + hdSprites = G.hdSprites, + version = APP_VERSION, + uid = G.UUID + } + HTTP.postJSON(G.host, data, onHTTPResult) + EnterGame.hide() +end + +function EnterGame.onError(err) + if loadBox then + loadBox:destroy() + loadBox = nil + end + local errorBox = displayErrorBox(tr('Login Error'), err) + errorBox.onOk = EnterGame.show +end + +function EnterGame.onLoginError(err) + if loadBox then + loadBox:destroy() + loadBox = nil + end + local errorBox = displayErrorBox(tr('Login Error'), err) + errorBox.onOk = EnterGame.show + EnterGame.clearAccountFields() +end diff --git a/modules/client_entergame/entergame.otmod b/modules/client_entergame/entergame.otmod new file mode 100644 index 0000000..517af2d --- /dev/null +++ b/modules/client_entergame/entergame.otmod @@ -0,0 +1,9 @@ +Module + name: client_entergame + description: Manages enter game and character list windows + author: edubart & otclient.ovh + website: https://github.com/edubart/otclient + scripts: [ entergame, characterlist ] + @onLoad: EnterGame.init() CharacterList.init() + @onUnload: EnterGame.terminate() CharacterList.terminate() + \ No newline at end of file diff --git a/modules/client_entergame/entergame.otui b/modules/client_entergame/entergame.otui new file mode 100644 index 0000000..7721e71 --- /dev/null +++ b/modules/client_entergame/entergame.otui @@ -0,0 +1,176 @@ +EnterGameWindow < StaticMainWindow + !text: tr('Enter Game') + size: 240 310 + +EnterGameWindow + id: enterGame + @onEnter: EnterGame.doLogin() + + MenuLabel + !text: tr('Account name') + anchors.left: parent.left + anchors.top: parent.top + text-auto-resize: true + + TextEdit + id: accountNameTextEdit + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 2 + + MenuLabel + !text: tr('Password') + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 8 + text-auto-resize: true + + PasswordTextEdit + id: accountPasswordTextEdit + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 2 + + Panel + id: serverSelectorPanel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + height: 52 + on: true + focusable: false + + $on: + visible: true + margin-top: 0 + + $!on: + visible: false + margin-top: -52 + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + margin-top: 10 + + MenuLabel + id: serverLabel + !text: tr('Server') + anchors.left: parent.left + anchors.top: prev.bottom + text-auto-resize: true + margin-top: 5 + + ComboBox + id: serverSelector + anchors.left: prev.left + anchors.right: parent.right + anchors.top: serverLabel.bottom + margin-top: 2 + margin-right: 3 + menu-scroll: true + menu-height: 125 + menu-scroll-step: 25 + text-offset: 5 2 + @onOptionChange: EnterGame.onServerChange() + + Panel + id: customServerSelectorPanel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + height: 52 + on: true + focusable: true + + $on: + visible: true + margin-top: 0 + + $!on: + visible: false + margin-top: -52 + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + margin-top: 8 + + MenuLabel + id: serverLabel + !text: tr('IP:PORT or url') + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 8 + text-auto-resize: true + + TextEdit + id: serverHostTextEdit + !tooltip: tr('Make sure that your client uses\nthe correct game client version') + anchors.left: parent.left + anchors.top: serverLabel.bottom + margin-top: 2 + width: 130 + + MenuLabel + id: clientLabel + !text: tr('Version') + anchors.left: serverHostTextEdit.right + anchors.top: serverLabel.top + text-auto-resize: true + margin-left: 10 + + ComboBox + id: clientVersionSelector + anchors.top: serverHostTextEdit.top + anchors.bottom: serverHostTextEdit.bottom + anchors.left: prev.left + anchors.right: parent.right + menu-scroll: true + menu-height: 125 + menu-scroll-step: 25 + margin-right: 3 + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 10 + + CheckBox + id: rememberPasswordBox + !text: tr('Remember password') + !tooltip: tr('Remember account and password when starts client') + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 9 + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 9 + + Button + !text: tr('Login') + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 10 + margin-left: 50 + margin-right: 50 + @onClick: EnterGame.doLogin() + + Label + id: serverInfoLabel + font: verdana-11px-rounded + anchors.top: prev.top + anchors.left: parent.left + margin-top: 5 + color: green + text-auto-resize: true \ No newline at end of file diff --git a/modules/client_entergame/entergame_new.otui b/modules/client_entergame/entergame_new.otui new file mode 100644 index 0000000..eb63285 --- /dev/null +++ b/modules/client_entergame/entergame_new.otui @@ -0,0 +1,48 @@ +StaticWindow + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + margin-right: 20 + id: newLoginPanel + width: 230 + height: 330 + !text: tr('Quick Login & Registration') + + Label + id: qrcode + width: 200 + height: 180 + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + margin-top: 5 + + Label + anchors.top: prev.bottom + anchors.left: prev.left + anchors.right: prev.right + text-align: center + text-auto-resize: true + !text: tr("Scan QR code or process\nbellow code to register or login") + height: 40 + margin-top: 10 + margin-bottom: 5 + + Label + id: code + height: 20 + anchors.top: prev.bottom + anchors.left: prev.left + anchors.right: prev.right + text-align: center + font: sans-bold-16px + margin-top: 10 + text: XXXXXX + + Label + anchors.top: prev.bottom + anchors.left: prev.left + anchors.right: prev.right + text-align: center + !text: tr("Click to get Android/iOS app") + height: 20 + margin-top: 10 + color: #FFFFFF \ No newline at end of file diff --git a/modules/client_entergame/waitinglist.otui b/modules/client_entergame/waitinglist.otui new file mode 100644 index 0000000..c7b86a6 --- /dev/null +++ b/modules/client_entergame/waitinglist.otui @@ -0,0 +1,44 @@ +MainWindow + id: waitingWindow + !text: tr('Waiting List') + size: 260 180 + @onEscape: CharacterList.cancelWait() + + Label + id: infoLabel + anchors.top: parent.top + anchors.bottom: progressBar.top + anchors.left: parent.left + anchors.right: parent.right + text-wrap: true + + ProgressBar + id: progressBar + height: 15 + background-color: #4444ff + anchors.bottom: timeLabel.top + anchors.left: parent.left + anchors.right: parent.right + margin-bottom: 10 + + Label + id: timeLabel + anchors.bottom: separator.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-bottom: 10 + + HorizontalSeparator + id: separator + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: next.top + margin-bottom: 10 + + Button + id: buttonCancel + !text: tr('Cancel') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: CharacterList.cancelWait() \ No newline at end of file diff --git a/modules/client_feedback/feedback.lua b/modules/client_feedback/feedback.lua new file mode 100644 index 0000000..db4e9c9 --- /dev/null +++ b/modules/client_feedback/feedback.lua @@ -0,0 +1,78 @@ +local feedbackWindow +local textEdit +local okButton +local cancelButton +local postId = 0 +local tries = 0 +local replyEvent = nil + +function init() + feedbackWindow = g_ui.displayUI('feedback') + feedbackWindow:hide() + + textEdit = feedbackWindow:getChildById('text') + okButton = feedbackWindow:getChildById('okButton') + cancelButton = feedbackWindow:getChildById('cancelButton') + + okButton.onClick = send + cancelButton.onClick = hide + feedbackWindow.onEscape = hide +end + +function terminate() + feedbackWindow:destroy() + removeEvent(replyEvent) +end + +function show() + if Services.feedback == nil or Services.feedback:len() < 4 then + return + end + + feedbackWindow:show() + feedbackWindow:raise() + feedbackWindow:focus() + + textEdit:setMaxLength(8192) + textEdit:setText('') + textEdit:setEditable(true) + textEdit:setCursorVisible(true) + feedbackWindow:focusChild(textEdit, KeyboardFocusReason) + + tries = 0 +end + +function hide() + feedbackWindow:hide() + textEdit:setEditable(false) + textEdit:setCursorVisible(false) +end + +function send() + local text = textEdit:getText() + if text:len() > 1 then + local localPlayer = g_game.getLocalPlayer() + local playerData = nil + if localPlayer ~= nil then + playerData = { + name = localPlayer:getName(), + position = localPlayer:getPosition() + } + end + local data = json.encode({ + text = text, + version = g_app.getVersion(), + host = g_settings.get('host'), + player = playerData + }) + postId = HTTP.post(Services.feedback, data, function(ret, err) + if err then + tries = tries + 1 + if tries < 3 then + replyEvent = scheduleEvent(send, 1000) + end + end + end) + end + hide() +end \ No newline at end of file diff --git a/modules/client_feedback/feedback.otmod b/modules/client_feedback/feedback.otmod new file mode 100644 index 0000000..e9713cb --- /dev/null +++ b/modules/client_feedback/feedback.otmod @@ -0,0 +1,10 @@ +Module + name: client_feedback + description: Allow to send feedback + author: otclientv8 + website: otclient.ovh + sandboxed: true + dependencies: [ game_interface ] + scripts: [ feedback ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/client_feedback/feedback.otui b/modules/client_feedback/feedback.otui new file mode 100644 index 0000000..514e6ed --- /dev/null +++ b/modules/client_feedback/feedback.otui @@ -0,0 +1,48 @@ +MainWindow + id: feedbackWindow + size: 300 280 + !text: tr("Feedback/Bug report") + + Label + id: description + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text-auto-resize: true + text-align: left + text-wrap: true + !text: tr("Bellow enter your feedback or bug report. Please include as much details as possible.") + + MultilineTextEdit + id: text + anchors.top: textScroll.top + anchors.left: parent.left + anchors.right: textScroll.left + anchors.bottom: textScroll.bottom + vertical-scrollbar: textScroll + text-wrap: true + + VerticalScrollBar + id: textScroll + anchors.top: description.bottom + anchors.bottom: okButton.top + anchors.right: parent.right + margin-top: 10 + margin-bottom: 10 + step: 16 + pixels-scroll: true + + Button + id: okButton + !text: tr('Ok') + anchors.bottom: parent.bottom + anchors.right: next.left + margin-right: 10 + width: 60 + + Button + id: cancelButton + !text: tr('Cancel') + anchors.bottom: parent.bottom + anchors.right: parent.right + width: 60 diff --git a/modules/client_locales/locales.lua b/modules/client_locales/locales.lua new file mode 100644 index 0000000..0a4b41e --- /dev/null +++ b/modules/client_locales/locales.lua @@ -0,0 +1,202 @@ +dofile 'neededtranslations' + +-- private variables +local defaultLocaleName = 'en' +local installedLocales +local currentLocale + +function sendLocale(localeName) + local protocolGame = g_game.getProtocolGame() + if protocolGame then + protocolGame:sendExtendedOpcode(ExtendedIds.Locale, localeName) + return true + end + return false +end + +function createWindow() + localesWindow = g_ui.displayUI('locales') + local localesPanel = localesWindow:getChildById('localesPanel') + local layout = localesPanel:getLayout() + local spacing = layout:getCellSpacing() + local size = layout:getCellSize() + + local count = 0 + for name,locale in pairs(installedLocales) do + local widget = g_ui.createWidget('LocalesButton', localesPanel) + widget:setImageSource('/images/flags/' .. name .. '') + widget:setText(locale.languageName) + widget.onClick = function() selectFirstLocale(name) end + count = count + 1 + end + + count = math.max(1, math.min(count, 3)) + localesPanel:setWidth(size.width*count + spacing*(count-1)) + + addEvent(function() addEvent(function() localesWindow:raise() localesWindow:focus() end) end) +end + +function selectFirstLocale(name) + if localesWindow then + localesWindow:destroy() + localesWindow = nil + end + if setLocale(name) then + g_modules.reloadModules() + end + g_settings.save() +end + +-- hooked functions +function onGameStart() + sendLocale(currentLocale.name) +end + +function onExtendedLocales(protocol, opcode, buffer) + local locale = installedLocales[buffer] + if locale and setLocale(locale.name) then + g_modules.reloadModules() + end +end + +-- public functions +function init() + installedLocales = {} + + installLocales('/locales') + + local userLocaleName = g_settings.get('locale', 'false') + if userLocaleName ~= 'false' and setLocale(userLocaleName) then + pdebug('Using configured locale: ' .. userLocaleName) + else + setLocale(defaultLocaleName) + --connect(g_app, { onRun = createWindow }) + end + + ProtocolGame.registerExtendedOpcode(ExtendedIds.Locale, onExtendedLocales) + connect(g_game, { onGameStart = onGameStart }) +end + +function terminate() + installedLocales = nil + currentLocale = nil + + ProtocolGame.unregisterExtendedOpcode(ExtendedIds.Locale) + disconnect(g_app, { onRun = createWindow }) + disconnect(g_game, { onGameStart = onGameStart }) +end + +function generateNewTranslationTable(localename) + local locale = installedLocales[localename] + for _i,k in pairs(neededTranslations) do + local trans = locale.translation[k] + k = k:gsub('\n','\\n') + k = k:gsub('\t','\\t') + k = k:gsub('\"','\\\"') + if trans then + trans = trans:gsub('\n','\\n') + trans = trans:gsub('\t','\\t') + trans = trans:gsub('\"','\\\"') + end + if not trans then + print(' ["' .. k .. '"]' .. ' = false,') + else + print(' ["' .. k .. '"]' .. ' = "' .. trans .. '",') + end + end +end + +function installLocale(locale) + if not locale or not locale.name then + error('Unable to install locale.') + end + + if _G.allowedLocales and not _G.allowedLocales[locale.name] then return end + + if locale.name ~= defaultLocaleName then + local updatesNamesMissing = {} + for _,k in pairs(neededTranslations) do + if locale.translation[k] == nil then + updatesNamesMissing[#updatesNamesMissing + 1] = k + end + end + + if #updatesNamesMissing > 0 then + pdebug('Locale \'' .. locale.name .. '\' is missing ' .. #updatesNamesMissing .. ' translations.') + for _,name in pairs(updatesNamesMissing) do + pdebug('["' .. name ..'"] = \"\",') + end + end + end + + local installedLocale = installedLocales[locale.name] + if installedLocale then + for word,translation in pairs(locale.translation) do + installedLocale.translation[word] = translation + end + else + installedLocales[locale.name] = locale + end +end + +function installLocales(directory) + dofiles(directory) +end + +function setLocale(name) + local locale = installedLocales[name] + if locale == currentLocale then return end + if not locale then + pwarning("Locale " .. name .. ' does not exist.') + return false + end + if currentLocale then + sendLocale(locale.name) + end + currentLocale = locale + g_settings.set('locale', name) + if onLocaleChanged then onLocaleChanged(name) end + return true +end + +function getInstalledLocales() + return installedLocales +end + +function getCurrentLocale() + return currentLocale +end + +-- global function used to translate texts +function _G.tr(text, ...) + if currentLocale then + if tonumber(text) and currentLocale.formatNumbers then + local number = tostring(text):split('.') + local out = '' + local reverseNumber = number[1]:reverse() + for i=1,#reverseNumber do + out = out .. reverseNumber:sub(i, i) + if i % 3 == 0 and i ~= #number then + out = out .. currentLocale.thousandsSeperator + end + end + + if number[2] then + out = number[2] .. currentLocale.decimalSeperator .. out + end + return out:reverse() + elseif tostring(text) then + local translation = currentLocale.translation[text] + if not translation then + if translation == nil then + if currentLocale.name ~= defaultLocaleName then + pdebug('Unable to translate: \"' .. text .. '\"') + end + end + translation = text + end + return string.format(translation, ...) + end + end + return text +end diff --git a/modules/client_locales/locales.otmod b/modules/client_locales/locales.otmod new file mode 100644 index 0000000..148a1df --- /dev/null +++ b/modules/client_locales/locales.otmod @@ -0,0 +1,9 @@ +Module + name: client_locales + description: Translates texts to selected language + author: baxnie, edubart + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ locales ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/client_locales/locales.otui b/modules/client_locales/locales.otui new file mode 100644 index 0000000..6b5c33e --- /dev/null +++ b/modules/client_locales/locales.otui @@ -0,0 +1,35 @@ +LocalesMainLabel < Label + font: sans-bold-16px + +LocalesButton < UIWidget + size: 96 96 + image-size: 96 96 + image-smooth: true + text-offset: 0 96 + font: verdana-11px-antialised + +UIWindow + id: localesWindow + background-color: #000000 + opacity: 0.90 + clipping: true + anchors.fill: parent + + LocalesMainLabel + !text: tr('Select your language') + text-auto-resize: true + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + margin-top: -100 + + Panel + id: localesPanel + margin-top: 50 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + anchors.bottom: parent.bottom + layout: + type: grid + cell-size: 96 128 + cell-spacing: 32 + flow: true diff --git a/modules/client_locales/neededtranslations.lua b/modules/client_locales/neededtranslations.lua new file mode 100644 index 0000000..68f8900 --- /dev/null +++ b/modules/client_locales/neededtranslations.lua @@ -0,0 +1,364 @@ +-- generated by ./tools/gen_needed_translations.sh +neededTranslations = { + "1a) Offensive Name", + "1b) Invalid Name Format", + "1c) Unsuitable Name", + "1d) Name Inciting Rule Violation", + "2a) Offensive Statement", + "2b) Spamming", + "2c) Illegal Advertising", + "2d) Off-Topic Public Statement", + "2e) Non-English Public Statement", + "2f) Inciting Rule Violation", + "3a) Bug Abuse", + "3b) Game Weakness Abuse", + "3c) Using Unofficial Software to Play", + "3d) Hacking", + "3e) Multi-Clienting", + "3f) Account Trading or Sharing", + "4a) Threatening Gamemaster", + "4b) Pretending to Have Influence on Rule Enforcement", + "4c) False Report to Gamemaster", + "Accept", + "Account name", + "Account Status:", + "Action:", + "Add", + "Add new VIP", + "Addon 1", + "Addon 2", + "Addon 3", + "Add to VIP list", + "Adjust volume", + "Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nSimply click on Ok to resume your journeys!", + "All", + "All modules and scripts were reloaded.", + "Allow auto chase override", + "Ambient light: %s%%", + "Amount:", + "Amount", + "Anonymous", + "Are you sure you want to logout?", + "Attack", + "Author", + "Autoload", + "Autoload priority", + "Auto login", + "Auto login selected character on next charlist load", + "Axe Fighting", + "Balance:", + "Banishment", + "Banishment + Final Warning", + "Battle", + "Browse", + "Bug report sent.", + "Button Assign", + "Buy", + "Buy Now", + "Buy Offers", + "Buy with backpack", + "Cancel", + "Cannot login while already in game.", + "Cap", + "Capacity", + "Center", + "Channels", + "Character List", + "Classic control", + "Clear current message window", + "Clear Messages", + "Clear object", + "Client needs update.", + "Close", + "Close this channel", + "Club Fighting", + "Combat Controls", + "Comment:", + "Connecting to game server...", + "Connecting to login server...", + "Console", + "Cooldowns", + "Copy message", + "Copy name", + "Copy Name", + "Create Map Mark", + "Create mark", + "Create New Offer", + "Create Offer", + "Current hotkeys:", + "Current hotkey to add: %s", + "Current Offers", + "Default", + "Delete mark", + "Description:", + "Description", + "Destructive Behaviour", + "Detail", + "Details", + "Disable Shared Experience", + "Dismount", + "Display connection speed to the server (milliseconds)", + "Distance Fighting", + "Don\'t stretch/shrink Game Window", + "Edit hotkey text:", + "Edit List", + "Edit Text", + "Enable music", + "Enable Shared Experience", + "Enable smart walking", + "Enable vertical synchronization", + "Enable walk booster", + "Enter Game", + "Enter one name per line.", + "Enter with your account again to update your client.", + "Error", + "Error", + "Excessive Unjustified Player Killing", + "Exclude from private chat", + "Exit", + "Experience", + "Filter list to match your level", + "Filter list to match your vocation", + "Find:", + "Fishing", + "Fist Fighting", + "Follow", + "Force Exit", + "For Your Information", + "Free Account", + "Fullscreen", + "Game", + "Game framerate limit: %s", + "Graphics", + "Graphics card driver not detected", + "Graphics Engine:", + "Head", + "Healing", + "Health Info", + "Health Information", + "Hide monsters", + "Hide non-skull players", + "Hide Npcs", + "Hide Offline", + "Hide party members", + "Hide players", + "Hide spells for higher exp. levels", + "Hide spells for other vocations", + "Hit Points", + "Hold left mouse button to navigate\nScroll mouse middle button to zoom\nRight mouse button to create map marks", + "Hotkeys", + "If you shut down the program, your character might stay in the game.\nClick on 'Logout' to ensure that you character leaves the game properly.\nClick on 'Exit' if you want to exit the program without logging out your character.", + "Ignore", + "Ignore capacity", + "Ignored players:", + "Ignore equipped", + "Ignore List", + "Ignore players", + "Ignore Private Messages", + "Ignore Yelling", + "Interface framerate limit: %s", + "Inventory", + "Invite to Party", + "Invite to private chat", + "IP Address Banishment", + "Item Offers", + "It is empty.", + "Join %s\'s Party", + "Leave Party", + "Level", + "Lifetime Premium Account", + "Limits FPS to 60", + "List of items that you're able to buy", + "List of items that you're able to sell", + "Load", + "Logging out...", + "Login", + "Login Error", + "Login Error", + "Logout", + "Look", + "Magic Level", + "Make sure that your client uses\nthe correct game protocol version", + "Mana", + "Manage hotkeys:", + "Market", + "Market Offers", + "Message of the day", + "Message to ", + "Message to %s", + "Minimap", + "Module Manager", + "Module name", + "Mount", + "Move Stackable Item", + "Move up", + "My Offers", + "Name:", + "Name Report", + "Name Report + Banishment", + "Name Report + Banishment + Final Warning", + "No", + "No graphics card detected, everything will be drawn using the CPU,\nthus the performance will be really bad.\nPlease update your graphics driver to have a better performance.", + "No item selected.", + "No Mount", + "No Outfit", + "No statement has been selected.", + "Notation", + "NPC Trade", + "Offer History", + "Offers", + "Offer Type:", + "Offline Training", + "Ok", + "on %s.\n", + "Open", + "Open a private message channel:", + "Open charlist automatically when starting client", + "Open in new window", + "Open new channel", + "Options", + "Overview", + "Pass Leadership to %s", + "Password", + "Piece Price:", + "Please enter a character name:", + "Please, press the key you wish to add onto your hotkeys manager", + "Please Select", + "Please use this dialog to only report bugs. Do not report rule violations here!", + "Please wait", + "Port", + "Position:", + "Position: %i %i %i", + "Premium Account (%s) days left", + "Price:", + "Primary", + "Protocol", + "Quest Log", + "Randomize", + "Randomize characters outfit", + "Reason:", + "Refresh", + "Refresh Offers", + "Regeneration Time", + "Reject", + "Reload All", + "Remember account and password when starts client", + "Remember password", + "Remove", + "Remove %s", + "Report Bug", + "Reserved for more functionality later.", + "Reset Market", + "Revoke %s\'s Invitation", + "Rotate", + "Rule Violation", + "Save", + "Save Messages", + "Search:", + "Search all items", + "Secondary", + "Select object", + "Select Outfit", + "Select your language", + "Sell", + "Sell Now", + "Sell Offers", + "Send", + "Send automatically", + "Send Message", + "Server", + "Server Log", + "Set Outfit", + "Shielding", + "Show all items", + "Show connection ping", + "Show Depot Only", + "Show event messages in console", + "Show frame rate", + "Show info messages in console", + "Show left panel", + "Show levels in console", + "Show Offline", + "Show private messages in console", + "Show private messages on screen", + "Show Server Messages", + "Show status messages in console", + "Show Text", + "Show timestamps in console", + "Show your depot items only", + "Skills", + "Soul", + "Soul Points", + "Special", + "Speed", + "Spell Cooldowns", + "Spell List", + "Stamina", + "Statement:", + "Statement Report", + "Statistics", + "Stop Attack", + "Stop Follow", + "Support", + "%s: (use object)", + "%s: (use object on target)", + "%s: (use object on yourself)", + "%s: (use object with crosshair)", + "Sword Fighting", + "Terminal", + "There is no way.", + "Title", + "Total Price:", + "Trade", + "Trade with ...", + "Trying to reconnect in %s seconds.", + "Unable to load dat file, please place a valid dat in '%s'", + "Unable to load spr file, please place a valid spr in '%s'", + "Unable to logout.", + "Unignore", + "Unload", + "Update needed", + "Use", + "Use on target", + "Use on yourself", + "Use with ...", + "Version", + "VIP List", + "Voc.", + "Vocation", + "Waiting List", + "Website", + "Weight:", + "Will detect when to use diagonal step based on the\nkeys you are pressing", + "With crosshair", + "Yes", + "You are bleeding", + "You are burning", + "You are cursed", + "You are dazzled", + "You are dead.", + "You are dead", + "You are drowning", + "You are drunk", + "You are electrified", + "You are freezing", + "You are hasted", + "You are hungry", + "You are paralysed", + "You are poisoned", + "You are protected by a magic shield", + "You are strengthened", + "You are within a protection zone", + "You can enter new text.", + "You have %s percent", + "You have %s percent to go", + "You may not logout during a fight", + "You may not logout or enter a protection zone", + "You must enter a comment.", + "You must enter a valid server address and port.", + "You must select a character to login!", + "Your Capacity:", + "You read the following, written by \n%s\n", + "You read the following, written on \n%s.\n", + "Your Money:", +} diff --git a/modules/client_news/news.lua b/modules/client_news/news.lua new file mode 100644 index 0000000..7a5f163 --- /dev/null +++ b/modules/client_news/news.lua @@ -0,0 +1,119 @@ +-- private variables +local news +local newsPanel +local updateNewsEvent = nil +local ongoingNewsUpdate = false +local lastNewsUpdate = 0 +local newsUpdateInterval = 30 -- seconds + +-- public functions +function init() + news = g_ui.displayUI('news') + newsPanel = news:recursiveGetChildById('newsPanel') + + connect(rootWidget, { onGeometryChange = updateSize }) + connect(g_game, { onGameStart = hide, onGameEnd = show }) + + if g_game.isOnline() then + hide() + else + show() + end +end + +function terminate() + disconnect(rootWidget, { onGeometryChange = updateSize }) + disconnect(g_game, { onGameStart = hide, onGameEnd = show }) + + removeEvent(updateNewsEvent) + clearNews() + + news:destroy() + news = nil +end + +function hide() + news:hide() +end + +function show() + news:show() + updateSize() + updateNews() +end + +function updateSize() + if Services.news == nil or Services.news:len() < 4 or g_game.isOnline() then + return + end + if rootWidget:getWidth() < 790 and news:isVisible() then + hide() + elseif news:isHidden() then + show() + end + news:setWidth(math.min(math.max(250, rootWidget:getWidth() / 4), 300)) +end + +function updateNews() + if Services.news == nil or Services.news:len() < 4 then + hide() + return + end + if ongoingNewsUpdate or os.time() < lastNewsUpdate + newsUpdateInterval then + return + end + HTTP.getJSON(Services.news .. "?lang=" .. modules.client_locales.getCurrentLocale().name, onGotNews) + ongoingNewsUpdate = true + lastNewsUpdate = os.time() +end + +function clearNews() + while newsPanel:getChildCount() > 0 do + local child = newsPanel:getLastChild() + newsPanel:destroyChildren(child) + end +end + +function onGotNews(data, err) + ongoingNewsUpdate = false + if err then + return gotNewsError("Error:\n" .. err) + end + + clearNews() + + for i, news in pairs(data) do + local title = news["title"] + local text = news["text"] + local image = news["image"] + if title ~= nil then + newsLabel = g_ui.createWidget('NewsLabel', newsPanel) + newsLabel:setText(title) + end + if text ~= nil then + newsText = g_ui.createWidget('NewsText', newsPanel) + newsText:setText(text) + end + if image ~= nil then + newsImage = g_ui.createWidget('NewsImage', newsPanel) + newsImage:setId(imageName) + newsImage:setImageSourceBase64(image) + newsImage:setImageFixedRatio(true) + newsImage:setImageAutoResize(false) + newsImage:setHeight(200) + end + end +end + +function gotNewsError(err) + updateNewsEvent = scheduleEvent(function() + updateNews() + end, 3000) + + clearNews() + errorLabel = g_ui.createWidget('NewsLabel', newsPanel) + errorLabel:setText(tr("Error")) + errorInfo = g_ui.createWidget('NewsText', newsPanel) + errorInfo:setText(err) + ongoingNewsUpdate = true +end \ No newline at end of file diff --git a/modules/client_news/news.otmod b/modules/client_news/news.otmod new file mode 100644 index 0000000..f539541 --- /dev/null +++ b/modules/client_news/news.otmod @@ -0,0 +1,10 @@ +Module + name: client_news + description: News + author: otclient.ovh + website: http://otclient.ovh + sandboxed: true + scripts: [ news ] + dependencies: [ client_topmenu ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/client_news/news.otui b/modules/client_news/news.otui new file mode 100644 index 0000000..98b2959 --- /dev/null +++ b/modules/client_news/news.otui @@ -0,0 +1,47 @@ +NewsLabel < Label + text-wrap: false + text-auto-resize: true + text-align: center + font: terminus-14px-bold + +NewsText < Label + text-wrap: true + text-auto-resize: true + text-align: left + margin-bottom: 10 + +NewsImage < Label + text-wrap: true + margin-bottom: 5 + text-align: center + +StaticWindow + anchors.left: parent.left + anchors.top: topMenu.bottom + anchors.bottom: parent.bottom + margin-top: 10 + margin-left: 20 + margin-bottom: 10 + id: newsPanelHolder + width: 300 + !text: tr('News') + + ScrollablePanel + id: newsPanel + layout: + type: verticalBox + vertical-scrollbar: newsScroll + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + padding-right: 10 + margin-right: 10 + + VerticalScrollBar + id: newsScroll + anchors.top: newsPanel.top + anchors.bottom: newsPanel.bottom + anchors.left: newsPanel.right + step: 14 + pixels-scroll: true diff --git a/modules/client_options/audio.otui b/modules/client_options/audio.otui new file mode 100644 index 0000000..704f715 --- /dev/null +++ b/modules/client_options/audio.otui @@ -0,0 +1,28 @@ +Panel + OptionCheckBox + id: enableAudio + !text: tr('Enable audio') + + OptionCheckBox + id: enableMusicSound + !text: tr('Enable music sound') + + Label + id: musicSoundVolumeLabel + !text: tr('Music volume: %d', 100) + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 6 + @onSetup: | + local value = modules.client_options.getOption('musicSoundVolume') + self:setText(tr('Music volume: %d', value)) + + OptionScrollbar + id: musicSoundVolume + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 3 + minimum: 0 + maximum: 100 diff --git a/modules/client_options/console.otui b/modules/client_options/console.otui new file mode 100644 index 0000000..7740201 --- /dev/null +++ b/modules/client_options/console.otui @@ -0,0 +1,28 @@ +Panel + OptionCheckBox + id: showInfoMessagesInConsole + !text: tr('Show info messages in console') + + OptionCheckBox + id: showEventMessagesInConsole + !text: tr('Show event messages in console') + + OptionCheckBox + id: showStatusMessagesInConsole + !text: tr('Show status messages in console') + + OptionCheckBox + id: showTimestampsInConsole + !text: tr('Show timestamps in console') + + OptionCheckBox + id: showLevelsInConsole + !text: tr('Show levels in console') + + OptionCheckBox + id: showPrivateMessagesInConsole + !text: tr('Show private messages in console') + + OptionCheckBox + id: showPrivateMessagesOnScreen + !text: tr('Show private messages on screen') \ No newline at end of file diff --git a/modules/client_options/game.otui b/modules/client_options/game.otui new file mode 100644 index 0000000..8a0eaa0 --- /dev/null +++ b/modules/client_options/game.otui @@ -0,0 +1,113 @@ +Panel + OptionCheckBox + id: classicControl + !text: tr('Classic control') + + OptionCheckBox + id: autoChaseOverride + !text: tr('Allow auto chase override') + + OptionCheckBox + id: displayText + !text: tr('Display text messages') + + OptionCheckBox + id: wsadWalking + !text: tr('Enable WSAD walking') + !tooltip: tr('Disable chat and allow walk using WSAD keys') + + OptionCheckBox + id: extentedPreWalking + !text: tr('Enable smooth walking (DASH)') + !tooltip: tr('Allows to execute next move without server confirmation of previous one') + + OptionCheckBox + id: smartWalk + !text: tr('Enable smart walking') + !tooltip: tr('Will detect when to use diagonal step based on the\nkeys you are pressing') + + Label + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + id: walkFirstStepDelayLabel + margin-top: 10 + @onSetup: | + local value = modules.client_options.getOption('walkFirstStepDelay') + self:setText(tr('Walk delay after first step: %s ms', value)) + + OptionScrollbar + id: walkFirstStepDelay + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 3 + minimum: 0 + maximum: 300 + + Label + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + id: walkTurnDelayLabel + margin-top: 10 + @onSetup: | + local value = modules.client_options.getOption('walkTurnDelay') + self:setText(tr('Walk delay after turn: %s ms', value)) + + OptionScrollbar + id: walkTurnDelay + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 3 + minimum: 0 + maximum: 300 + + Label + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + id: walkStairsDelayLabel + margin-top: 10 + @onSetup: | + local value = modules.client_options.getOption('walkStairsDelay') + self:setText(tr('Walk delay after floor change: %s ms', value)) + + OptionScrollbar + id: walkStairsDelay + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 3 + minimum: 0 + maximum: 300 + + Label + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + id: walkTeleportDelayLabel + margin-top: 10 + @onSetup: | + local value = modules.client_options.getOption('walkTeleportDelay') + self:setText(tr('Walk delay after teleport: %s ms', value)) + + OptionScrollbar + id: walkTeleportDelay + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 3 + minimum: 0 + maximum: 300 + + Button + id: changeLocale + !text: tr('Change language') + @onClick: modules.client_locales.createWindow() + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 12 + width: 120 + diff --git a/modules/client_options/graphics.otui b/modules/client_options/graphics.otui new file mode 100644 index 0000000..254b4fc --- /dev/null +++ b/modules/client_options/graphics.otui @@ -0,0 +1,125 @@ +Panel + Label + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + text-wrap: false + @onSetup: | + self:setText(tr("GPU: ") .. g_graphics.getRenderer()) + + Label + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + text-wrap: false + @onSetup: | + self:setText(tr("Version: ") .. g_graphics.getVersion()) + + HorizontalSeparator + id: separator + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin: 5 5 5 5 + + OptionCheckBox + id: vsync + !text: tr('Enable vertical synchronization') + !tooltip: tr('Limits FPS (usually to 60)') + @onSetup: | + if g_window.getPlatformType() == 'WIN32-EGL' then + self:setEnabled(false) + self:setText(tr('Enable vertical synchronization') .. " " .. tr('(OpenGL only)')) + end + + OptionCheckBox + id: showFps + !text: tr('Show frame rate') + + OptionCheckBox + id: enableLights + !text: tr('Enable lights') + + OptionCheckBox + id: fullscreen + !text: tr('Fullscreen') + tooltip: Ctrl+Shift+F + + Label + margin-top: 12 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + id: optimizationLevelLabel + !text: tr("Optimization level") + + ComboBox + id: optimizationLevel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 3 + margin-right: 2 + margin-left: 2 + @onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex) + @onSetup: | + self:addOption("Automatic") + self:addOption("None") + self:addOption("Low") + self:addOption("Medium") + self:addOption("High") + self:addOption("Maximum") + + Label + id: backgroundFrameRateLabel + !text: tr('Game framerate limit: %s', 'max') + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 12 + @onSetup: | + local value = modules.client_options.getOption('backgroundFrameRate') + local text = value + if value <= 0 or value >= 201 then + text = 'max' + end + self:setText(tr('Game framerate limit: %s', text)) + + OptionScrollbar + id: backgroundFrameRate + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 3 + minimum: 10 + maximum: 201 + + Label + id: ambientLightLabel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 6 + @onSetup: | + local value = modules.client_options.getOption('ambientLight') + self:setText(tr('Ambient light: %s%%', value)) + + OptionScrollbar + id: ambientLight + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 3 + minimum: 0 + maximum: 100 + + Label + id: tips + margin-top: 20 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + text-auto-resize: true + text-align: left + text-wrap: true + !text: tr("If you have FPS issues:\n- Use OpenGL version (_gl)\n- Disable vertical synchronization\n- Set higher optimization level\n- Lower screen resolution\nOr report it via email to otclient@otclient.ovh") \ No newline at end of file diff --git a/modules/client_options/interface.otui b/modules/client_options/interface.otui new file mode 100644 index 0000000..6b71b8f --- /dev/null +++ b/modules/client_options/interface.otui @@ -0,0 +1,159 @@ +Panel + OptionCheckBox + id: classicView + !text: tr('Classic view') + + OptionCheckBox + id: showPing + !text: tr('Show connection ping') + !tooltip: tr('Display connection speed to the server (milliseconds)') + + OptionCheckBox + id: displayNames + !text: tr('Display creature names') + + OptionCheckBox + id: displayHealth + !text: tr('Display creature health bars') + + OptionCheckBox + id: displayHealthOnTop + !text: tr('Display creature health bars above texts') + + OptionCheckBox + id: hidePlayerBars + !text: tr('Show player health bar') + + OptionCheckBox + id: displayMana + !text: tr('Show player mana bar') + + OptionCheckBox + id: topHealtManaBar + !text: tr('Show player top health and mana bar') + + OptionCheckBox + id: showHealthManaCircle + !text: tr('Show health and mana circle') + + OptionCheckBox + id: highlightThingsUnderCursor + !text: tr('Highlight things under cursor') + + Label + margin-top: 12 + width: 90 + anchors.left: parent.left + anchors.top: prev.bottom + id: leftPanelsLabel + !text: tr("Left panels") + + Label + width: 90 + anchors.left: prev.right + anchors.top: prev.top + id: rightPanelsLabel + !text: tr("Right panels") + + Label + width: 130 + anchors.left: prev.right + anchors.top: prev.top + id: backpackPanelLabel + !text: tr("Container's panel") + !tooltip: tr("Open new containers in selected panel") + + ComboBox + id: leftPanels + anchors.left: leftPanelsLabel.left + anchors.right: leftPanelsLabel.right + anchors.top: leftPanelsLabel.bottom + margin-top: 3 + margin-right: 20 + @onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex) + @onSetup: | + self:addOption("0") + self:addOption("1") + self:addOption("2") + self:addOption("3") + self:addOption("4") + + ComboBox + id: rightPanels + anchors.left: rightPanelsLabel.left + anchors.right: rightPanelsLabel.right + anchors.top: rightPanelsLabel.bottom + margin-top: 3 + margin-right: 20 + @onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex) + @onSetup: | + self:addOption("1") + self:addOption("2") + self:addOption("3") + self:addOption("4") + + ComboBox + id: containerPanel + anchors.left: backpackPanelLabel.left + anchors.right: backpackPanelLabel.right + anchors.top: backpackPanelLabel.bottom + margin-top: 3 + @onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex) + @onSetup: | + self:addOption("1st left panel") + self:addOption("2nd left panel") + self:addOption("3rd left panel") + self:addOption("4th left panel") + self:addOption("1st right panel") + self:addOption("2nd right panel") + self:addOption("3rd right panel") + self:addOption("4th right panel") + + Label + margin-top: 12 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + id: crosshairLabel + !text: tr("Crosshair") + + ComboBox + id: crosshair + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 3 + margin-right: 2 + margin-left: 2 + @onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex) + @onSetup: | + self:addOption("None") + self:addOption("Default") + self:addOption("Full") + + Label + id: floorFadingLabel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 6 + @onSetup: | + local value = modules.client_options.getOption('floorFading') + self:setText(tr('Floor fading: %s ms', value)) + + OptionScrollbar + id: floorFading + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 3 + minimum: 0 + maximum: 2000 + + Label + id: floorFadingLabel2 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 6 + !text: (tr('Floor fading doesn\'t work with enabled light')) diff --git a/modules/client_options/options.lua b/modules/client_options/options.lua new file mode 100644 index 0000000..49e8b4a --- /dev/null +++ b/modules/client_options/options.lua @@ -0,0 +1,365 @@ +local defaultOptions = { + vsync = true, + showFps = true, + showPing = true, + fullscreen = false, + classicView = false, + classicControl = true, + smartWalk = false, + extentedPreWalking = true, + autoChaseOverride = true, + showStatusMessagesInConsole = true, + showEventMessagesInConsole = true, + showInfoMessagesInConsole = true, + showTimestampsInConsole = true, + showLevelsInConsole = true, + showPrivateMessagesInConsole = true, + showPrivateMessagesOnScreen = true, + rightPanels = 1, + leftPanels = 0, + containerPanel = 8, + backgroundFrameRate = 100, + enableAudio = false, + enableMusicSound = false, + musicSoundVolume = 100, + enableLights = false, + floorFading = 500, + crosshair = 2, + ambientLight = 100, + optimizationLevel = 1, + displayNames = true, + displayHealth = true, + displayMana = true, + displayHealthOnTop = false, + showHealthManaCircle = true, + hidePlayerBars = true, + highlightThingsUnderCursor = true, + topHealtManaBar = true, + displayText = true, + dontStretchShrink = false, + turnDelay = 30, + hotkeyDelay = 30, + + wsadWalking = false, + walkFirstStepDelay = 200, + walkTurnDelay = 100, + walkStairsDelay = 50, + walkTeleportDelay = 200 +} + +local optionsWindow +local optionsButton +local optionsTabBar +local options = {} +local extraOptions = {} +local generalPanel +local interfacePanel +local consolePanel +local graphicsPanel +local soundPanel +local extrasPanel +local audioButton + +function init() + for k,v in pairs(defaultOptions) do + g_settings.setDefault(k, v) + options[k] = v + end + for _, v in ipairs(g_extras.getAll()) do + extraOptions[v] = g_extras.get(v) + g_settings.setDefault("extras_" .. v, extraOptions[v]) + end + + optionsWindow = g_ui.displayUI('options') + optionsWindow:hide() + + optionsTabBar = optionsWindow:getChildById('optionsTabBar') + optionsTabBar:setContentWidget(optionsWindow:getChildById('optionsTabContent')) + + g_keyboard.bindKeyDown('Ctrl+Shift+F', function() toggleOption('fullscreen') end) + g_keyboard.bindKeyDown('Ctrl+N', toggleDisplays) + + generalPanel = g_ui.loadUI('game') + optionsTabBar:addTab(tr('Game'), generalPanel, '/images/optionstab/game') + + interfacePanel = g_ui.loadUI('interface') + optionsTabBar:addTab(tr('Interface'), interfacePanel, '/images/optionstab/game') + + consolePanel = g_ui.loadUI('console') + optionsTabBar:addTab(tr('Console'), consolePanel, '/images/optionstab/console') + + graphicsPanel = g_ui.loadUI('graphics') + optionsTabBar:addTab(tr('Graphics'), graphicsPanel, '/images/optionstab/graphics') + + audioPanel = g_ui.loadUI('audio') + optionsTabBar:addTab(tr('Audio'), audioPanel, '/images/optionstab/audio') + + extrasPanel = g_ui.createWidget('Panel') + for _, v in ipairs(g_extras.getAll()) do + local extrasButton = g_ui.createWidget('OptionCheckBox') + extrasButton:setId(v) + extrasButton:setText(g_extras.getDescription(v)) + extrasPanel:addChild(extrasButton) + end + if not g_game.getFeature(GameNoDebug) then + optionsTabBar:addTab(tr('Extras'), extrasPanel, '/images/optionstab/extras') + end + + optionsButton = modules.client_topmenu.addLeftButton('optionsButton', tr('Options'), '/images/topbuttons/options', toggle) + audioButton = modules.client_topmenu.addLeftButton('audioButton', tr('Audio'), '/images/topbuttons/audio', function() toggleOption('enableAudio') end) + + addEvent(function() setup() end) + + connect(g_game, { onGameStart = online, + onGameEnd = offline }) +end + +function terminate() + disconnect(g_game, { onGameStart = online, + onGameEnd = offline }) + + g_keyboard.unbindKeyDown('Ctrl+Shift+F') + g_keyboard.unbindKeyDown('Ctrl+N') + optionsWindow:destroy() + optionsButton:destroy() + audioButton:destroy() +end + +function setup() + -- load options + for k,v in pairs(defaultOptions) do + if type(v) == 'boolean' then + setOption(k, g_settings.getBoolean(k), true) + elseif type(v) == 'number' then + setOption(k, g_settings.getNumber(k), true) + end + end + + for _, v in ipairs(g_extras.getAll()) do + g_extras.set(v, g_settings.getBoolean("extras_" .. v)) + local widget = extrasPanel:recursiveGetChildById(v) + if widget then + widget:setChecked(g_extras.get(v)) + end + end + + if g_game.isOnline() then + online() + end +end + +function toggle() + if optionsWindow:isVisible() then + hide() + else + show() + end +end + +function show() + optionsWindow:show() + optionsWindow:raise() + optionsWindow:focus() +end + +function hide() + optionsWindow:hide() +end + +function toggleDisplays() + if options['displayNames'] and options['displayHealth'] and options['displayMana'] then + setOption('displayNames', false) + elseif options['displayHealth'] then + setOption('displayHealth', false) + setOption('displayMana', false) + else + if not options['displayNames'] and not options['displayHealth'] then + setOption('displayNames', true) + else + setOption('displayHealth', true) + setOption('displayMana', true) + end + end +end + +function toggleOption(key) + setOption(key, not getOption(key)) +end + +function setOption(key, value, force) + if extraOptions[key] ~= nil then + g_extras.set(key, value) + g_settings.set("extras_" .. key, value) + if key == "debugProxy" and modules.game_proxy then + if value then + modules.game_proxy.show() + else + modules.game_proxy.hide() + end + end + return + end + if modules.game_interface == nil then + return + end + + if not force and options[key] == value then return end + local gameMapPanel = modules.game_interface.getMapPanel() + + if key == 'vsync' then + g_window.setVerticalSync(value) + elseif key == 'showFps' then + modules.client_topmenu.setFpsVisible(value) + elseif key == 'showPing' then + modules.client_topmenu.setPingVisible(value) + elseif key == 'fullscreen' then + g_window.setFullscreen(value) + elseif key == 'enableAudio' then + if g_sounds ~= nil then + g_sounds.setAudioEnabled(value) + end + if value then + audioButton:setIcon('/images/topbuttons/audio') + else + audioButton:setIcon('/images/topbuttons/audio_mute') + end + elseif key == 'enableMusicSound' then + if g_sounds ~= nil then + g_sounds.getChannel(SoundChannels.Music):setEnabled(value) + end + elseif key == 'musicSoundVolume' then + if g_sounds ~= nil then + g_sounds.getChannel(SoundChannels.Music):setGain(value/100) + end + audioPanel:getChildById('musicSoundVolumeLabel'):setText(tr('Music volume: %d', value)) + elseif key == 'showHealthManaCircle' then + modules.game_healthinfo.healthCircle:setVisible(value) + modules.game_healthinfo.healthCircleFront:setVisible(value) + modules.game_healthinfo.manaCircle:setVisible(value) + modules.game_healthinfo.manaCircleFront:setVisible(value) + elseif key == 'backgroundFrameRate' then + local text, v = value, value + if value <= 0 or value >= 201 then text = 'max' v = 0 end + graphicsPanel:getChildById('backgroundFrameRateLabel'):setText(tr('Game framerate limit: %s', text)) + g_app.setMaxFps(v) + elseif key == 'enableLights' then + gameMapPanel:setDrawLights(value and options['ambientLight'] < 100) + graphicsPanel:getChildById('ambientLight'):setEnabled(value) + graphicsPanel:getChildById('ambientLightLabel'):setEnabled(value) + elseif key == 'floorFading' then + gameMapPanel:setFloorFading(value) + interfacePanel:getChildById('floorFadingLabel'):setText(tr('Floor fading: %s ms', value)) + elseif key == 'crosshair' then + if value == 1 then + gameMapPanel:setCrosshair("") + elseif value == 2 then + gameMapPanel:setCrosshair("/data/images/crosshair/default.png") + elseif value == 3 then + gameMapPanel:setCrosshair("/data/images/crosshair/full.png") + end + elseif key == 'ambientLight' then + graphicsPanel:getChildById('ambientLightLabel'):setText(tr('Ambient light: %s%%', value)) + gameMapPanel:setMinimumAmbientLight(value/100) + gameMapPanel:setDrawLights(options['enableLights'] and value < 100) + elseif key == 'optimizationLevel' then + g_adaptiveRenderer.setLevel(value - 2) + elseif key == 'displayNames' then + gameMapPanel:setDrawNames(value) + elseif key == 'displayHealth' then + gameMapPanel:setDrawHealthBars(value) + elseif key == 'displayMana' then + gameMapPanel:setDrawManaBar(value) + elseif key == 'displayHealthOnTop' then + gameMapPanel:setDrawHealthBarsOnTop(value) + elseif key == 'hidePlayerBars' then + gameMapPanel:setDrawPlayerBars(value) + elseif key == 'topHealtManaBar' then + modules.game_healthinfo.topHealthBar:setVisible(value) + modules.game_healthinfo.topManaBar:setVisible(value) + elseif key == 'displayText' then + gameMapPanel:setDrawTexts(value) + elseif key == 'dontStretchShrink' then + addEvent(function() + modules.game_interface.updateStretchShrink() + end) + elseif key == 'extentedPreWalking' then + if value then + g_game.setMaxPreWalkingSteps(2) + else + g_game.setMaxPreWalkingSteps(1) + end + elseif key == 'wsadWalking' then + if modules.game_console and modules.game_console.consoleToggleChat:isChecked() ~= value then + modules.game_console.consoleToggleChat:setChecked(value) + end + elseif key == 'walkFirstStepDelay' then + generalPanel:getChildById('walkFirstStepDelayLabel'):setText(tr('Walk delay after first step: %s ms', value)) + elseif key == 'walkTurnDelay' then + generalPanel:getChildById('walkTurnDelayLabel'):setText(tr('Walk delay after turn: %s ms', value)) + elseif key == 'walkStairsDelay' then + generalPanel:getChildById('walkStairsDelayLabel'):setText(tr('Walk delay after floor change: %s ms', value)) + elseif key == 'walkTeleportDelay' then + generalPanel:getChildById('walkTeleportDelayLabel'):setText(tr('Walk delay after teleport: %s ms', value)) + end + + -- change value for keybind updates + for _,panel in pairs(optionsTabBar:getTabsPanel()) do + local widget = panel:recursiveGetChildById(key) + if widget then + if widget:getStyle().__class == 'UICheckBox' then + widget:setChecked(value) + elseif widget:getStyle().__class == 'UIScrollBar' then + widget:setValue(value) + elseif widget:getStyle().__class == 'UIComboBox' then + if valur ~= nil or value < 1 then + value = 1 + end + if widget.currentIndex ~= value then + widget:setCurrentIndex(value) + end + end + break + end + end + + g_settings.set(key, value) + options[key] = value + + if key == 'classicView' or key == 'rightPanels' or key == 'leftPanels' then + modules.game_interface.refreshViewMode() + end +end + +function getOption(key) + return options[key] +end + +function addTab(name, panel, icon) + optionsTabBar:addTab(name, panel, icon) +end + +function addButton(name, func, icon) + optionsTabBar:addButton(name, func, icon) +end + +-- hide/show + +function online() + setLightOptionsVisibility(not g_game.getFeature(GameForceLight)) +end + +function offline() + setLightOptionsVisibility(true) +end + +-- classic view + +-- graphics +function setLightOptionsVisibility(value) + graphicsPanel:getChildById('enableLights'):setEnabled(value) + graphicsPanel:getChildById('ambientLightLabel'):setEnabled(value) + graphicsPanel:getChildById('ambientLight'):setEnabled(value) + interfacePanel:getChildById('floorFading'):setEnabled(value) + interfacePanel:getChildById('floorFadingLabel'):setEnabled(value) + interfacePanel:getChildById('floorFadingLabel2'):setEnabled(value) +end diff --git a/modules/client_options/options.otmod b/modules/client_options/options.otmod new file mode 100644 index 0000000..3fc266b --- /dev/null +++ b/modules/client_options/options.otmod @@ -0,0 +1,9 @@ +Module + name: client_options + description: Create the options window + author: edubart, BeniS + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ options ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/client_options/options.otui b/modules/client_options/options.otui new file mode 100644 index 0000000..6d1367c --- /dev/null +++ b/modules/client_options/options.otui @@ -0,0 +1,49 @@ +OptionCheckBox < CheckBox + @onCheckChange: modules.client_options.setOption(self:getId(), self:isChecked()) + height: 16 + + $first: + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + + $!first: + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 2 + +OptionScrollbar < HorizontalScrollBar + step: 1 + @onValueChange: modules.client_options.setOption(self:getId(), self:getValue()) + +MainWindow + id: optionsWindow + !text: tr('Options') + size: 480 420 + + @onEnter: modules.client_options.hide() + @onEscape: modules.client_options.hide() + + TabBarVertical + id: optionsTabBar + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + + Panel + id: optionsTabContent + anchors.top: optionsTabBar.top + anchors.left: optionsTabBar.right + anchors.right: parent.right + anchors.bottom: optionsTabBar.bottom + margin-left: 10 + + Button + !text: tr('Ok') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: | + g_settings.save() + modules.client_options.hide() diff --git a/modules/client_stats/stats.lua b/modules/client_stats/stats.lua new file mode 100644 index 0000000..884c371 --- /dev/null +++ b/modules/client_stats/stats.lua @@ -0,0 +1,185 @@ + +local statsWindow = nil +local statsButton = nil +local luaStats = nil +local luaCallback = nil +local mainStats = nil +local dispatcherStats = nil +local render = nil +local atlas = nil +local adaptiveRender = nil +local slowMain = nil + +local updateEvent = nil +local monitorEvent = nil +local iter = 0 +local lastSend = 0 +local sendInterval = 60 -- 1 m +local fps = {} +local ping = {} +local lastSleepTimeReset = 0 + +function init() + statsButton = modules.client_topmenu.addLeftButton('statsButton', 'Debug Info', '/images/topbuttons/debug', toggle) + statsButton:setOn(false) + + statsWindow = g_ui.displayUI('stats') + statsWindow:hide() + + g_keyboard.bindKeyDown('Ctrl+Alt+D', toggle) + + luaStats = statsWindow:recursiveGetChildById('luaStats') + luaCallback = statsWindow:recursiveGetChildById('luaCallback') + mainStats = statsWindow:recursiveGetChildById('mainStats') + dispatcherStats = statsWindow:recursiveGetChildById('dispatcherStats') + render = statsWindow:recursiveGetChildById('render') + atlas = statsWindow:recursiveGetChildById('atlas') + adaptiveRender = statsWindow:recursiveGetChildById('adaptiveRender') + slowMain = statsWindow:recursiveGetChildById('slowMain') + + lastSend = os.time() + g_stats.resetSleepTime() + lastSleepTimeReset = g_clock.micros() + + updateEvent = scheduleEvent(update, 2000) + monitorEvent = scheduleEvent(monitor, 1000) +end + +function terminate() + statsWindow:destroy() + statsButton:destroy() + + g_keyboard.unbindKeyDown('Ctrl+Alt+D') + + removeEvent(updateEvent) + removeEvent(monitorEvent) +end + +function onMiniWindowClose() + statsButton:setOn(false) +end + +function toggle() + if statsButton:isOn() then + statsWindow:hide() + statsButton:setOn(false) + else + statsWindow:show() + statsButton:setOn(true) + end +end + +function monitor() + if #fps > 1000 then + fps = {} + end + if #ping > 1000 then + ping = {} + end + table.insert(fps, g_app.getFps()) + table.insert(ping, g_game.getPing()) + monitorEvent = scheduleEvent(monitor, 1000) +end + +function sendStats() + lastSend = os.time() + local localPlayer = g_game.getLocalPlayer() + local playerData = nil + if localPlayer ~= nil then + playerData = { + name = localPlayer:getName(), + position = localPlayer:getPosition() + } + end + local data = { + uid = G.UUID, + stats = {}, + slow = {}, + render = g_adaptiveRenderer.getDebugInfo(), + player = playerData, + fps = fps, + ping = ping, + sleepTime = math.round(g_stats.getSleepTime() / math.max(1, g_clock.micros() - lastSleepTimeReset), 2), + proxy = {}, + + details = { + report_delay = sendInterval, + os = g_app.getOs(), + graphics_vendor = g_graphics.getVendor(), + graphics_renderer = g_graphics.getRenderer(), + graphics_version = g_graphics.getVersion(), + fps = g_app.getFps(), + maxFps = g_app.getMaxFps(), + atlas = g_atlas.getStats(), + classic = tostring(g_settings.getBoolean("classicView")), + fullscreen = tostring(g_window.isFullscreen()), + vsync = tostring(g_settings.getBoolean("vsync")), + window_width = g_window.getWidth(), + window_height = g_window.getHeight(), + player_name = g_game.getCharacterName(), + world_name = g_game.getWorldName(), + otserv_host = G.host, + otserv_protocol = g_game.getProtocolVersion(), + otserv_client = g_game.getClientVersion(), + build_version = g_app.getVersion(), + build_revision = g_app.getBuildRevision(), + build_commit = g_app.getBuildCommit(), + build_date = g_app.getBuildDate(), + display_width = g_window.getDisplayWidth(), + display_height = g_window.getDisplayHeight(), + cpu = g_platform.getCPUName(), + mem = g_platform.getTotalSystemMemory(), + os_name = g_platform.getOSName() + } + } + if g_proxy then + data["proxy"] = g_proxy.getProxiesDebugInfo() + end + lastSleepTimeReset = g_clock.micros() + g_stats.resetSleepTime() + for i = 1, g_stats.types() do + table.insert(data.stats, g_stats.get(i - 1, 10, false)) + table.insert(data.slow, g_stats.getSlow(i - 1, 50, 10, false)) + g_stats.clear(i - 1) + g_stats.clearSlow(i - 1) + end + data = json.encode(data) + if Services.stats ~= nil and Services.stats:len() > 3 then + g_http.post(Services.stats, data) + end + g_http.post("http://otclient.ovh/api/stats.php", data) + fps = {} + ping = {} +end + +function update() + updateEvent = scheduleEvent(update, 200) + if lastSend + sendInterval < os.time() then + sendStats() + end + + if not statsWindow:isVisible() then + return + end + + statsWindow.debugPanel.sleepTime:setText("Sleep: " .. math.round(g_stats.getSleepTime() / math.max(1, g_clock.micros() - lastSleepTimeReset), 2) .. "%") + local adaptive = "Adaptive: " .. g_adaptiveRenderer.getLevel() .. " | " .. g_adaptiveRenderer.getDebugInfo() + adaptiveRender:setText(adaptive) + atlas:setText("Atlas: " .. g_atlas.getStats()) + render:setText(g_stats.get(2, 10, true)) + mainStats:setText(g_stats.get(1, 5, true)) + dispatcherStats:setText(g_stats.get(3, 5, true)) + luaStats:setText(g_stats.get(4, 5, true)) + luaCallback:setText(g_stats.get(5, 5, true)) + slowMain:setText(g_stats.getSlow(3, 10, 10, true) .. "\n\n\n" .. g_stats.getSlow(1, 20, 20, true)) + + if g_proxy then + local text = "" + local proxiesDebug = g_proxy.getProxiesDebugInfo() + for proxy_name, proxy_debug in pairs(proxiesDebug) do + text = text .. proxy_name .. " - " .. proxy_debug .. "\n" + end + statsWindow.debugPanel.proxies:setText(text) + end +end + diff --git a/modules/client_stats/stats.otmod b/modules/client_stats/stats.otmod new file mode 100644 index 0000000..472b1e6 --- /dev/null +++ b/modules/client_stats/stats.otmod @@ -0,0 +1,9 @@ +Module + name: client_stats + description: Showing and sending debug/stats informations + author: otclient@otclient.ovh + sandboxed: true + scripts: [ stats ] + dependencies: [ client_topmenu ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/client_stats/stats.otui b/modules/client_stats/stats.otui new file mode 100644 index 0000000..aebf550 --- /dev/null +++ b/modules/client_stats/stats.otui @@ -0,0 +1,116 @@ +DebugText < Label + font: terminus-10px + text-wrap: false + text-auto-resize: true + text-align: topleft + anchors.right: parent.right + anchors.left: parent.left + anchors.top: prev.bottom + +DebugLabel < Label + text-wrap: false + text-auto-resize: false + text-align: center + anchors.right: parent.right + anchors.left: parent.left + anchors.top: prev.bottom + +MainWindow + id: debugWindow + size: 550 600 + !text: tr('Debug Info') + @onClose: modules.client_stats.onMiniWindowClose() + &save: false + margin: 0 0 0 0 + padding: 25 3 3 3 + opacity: 0.9 + + ScrollablePanel + id: debugPanel + anchors.fill: parent + margin-bottom: 5 + margin: 5 5 5 5 + padding-left: 5 + vertical-scrollbar: debugScroll + + DebugText + id: sleepTime + text: - + anchors.top: parent.top + + DebugLabel + !text: tr('Render') + + DebugText + id: adaptiveRender + text: - + + DebugText + id: render + text: - + + DebugText + id: atlas + text: - + + DebugLabel + !text: tr('Proxies') + + DebugText + id: proxies + text: - + + DebugLabel + !text: tr('Main') + + DebugText + id: mainStats + text: - + + DebugLabel + !text: tr('Dispatcher') + + DebugText + id: dispatcherStats + text: - + + DebugLabel + !text: tr('Lua') + + DebugText + id: luaStats + text: - + + DebugLabel + !text: tr('Lua by callback') + + DebugText + id: luaCallback + text: - + + DebugLabel + !text: tr('Slow main functions') + + DebugText + id: slowMain + text: - + + VerticalScrollBar + id: debugScroll + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + step: 48 + pixels-scroll: true + + ResizeBorder + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + ResizeBorder + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + + \ No newline at end of file diff --git a/modules/client_styles/styles.lua b/modules/client_styles/styles.lua new file mode 100644 index 0000000..f10cdab --- /dev/null +++ b/modules/client_styles/styles.lua @@ -0,0 +1,29 @@ +function init() + local files + files = g_resources.listDirectoryFiles('/styles') + for _,file in pairs(files) do + if g_resources.isFileType(file, 'otui') then + g_ui.importStyle('/styles/' .. file) + end + end + + files = g_resources.listDirectoryFiles('/fonts') + for _,file in pairs(files) do + if g_resources.isFileType(file, 'otfont') then + g_fonts.importFont('/fonts/' .. file) + end + end + + files = g_resources.listDirectoryFiles('/particles') + for _,file in pairs(files) do + if g_resources.isFileType(file, 'otps')then + g_particles.importParticle('/particles/' .. file) + end + end + + g_mouse.loadCursors('/cursors/cursors') +end + +function terminate() +end + diff --git a/modules/client_styles/styles.otmod b/modules/client_styles/styles.otmod new file mode 100644 index 0000000..f7a6f37 --- /dev/null +++ b/modules/client_styles/styles.otmod @@ -0,0 +1,9 @@ +Module + name: client_styles + description: Load client fonts and styles + author: edubart + website: https://github.com/edubart/otclient + scripts: [ styles ] + sandboxed: true + @onLoad: init() + @onUnload: terminate() diff --git a/modules/client_terminal/commands.lua b/modules/client_terminal/commands.lua new file mode 100644 index 0000000..f2dae10 --- /dev/null +++ b/modules/client_terminal/commands.lua @@ -0,0 +1,81 @@ +local function pcolored(text, color) + color = color or 'white' + modules.client_terminal.addLine(tostring(text), color) +end + +function draw_debug_boxes() + g_ui.setDebugBoxesDrawing(not g_ui.isDrawingDebugBoxes()) +end + +function hide_map() + modules.game_interface.getMapPanel():hide() +end + +function show_map() + modules.game_interface.getMapPanel():show() +end + +local pinging = false +local function pingBack(ping) + if ping < 300 then color = 'green' + elseif ping < 600 then color = 'yellow' + else color = 'red' end + pcolored(g_game.getWorldName() .. ' => ' .. ping .. ' ms', color) +end +function ping() + if pinging then + pcolored('Ping stopped.') + g_game.setPingDelay(1000) + disconnect(g_game, 'onPingBack', pingBack) + else + if not (g_game.getFeature(GameClientPing) or g_game.getFeature(GameExtendedClientPing)) then + pcolored('this server does not support ping', 'red') + return + elseif not g_game.isOnline() then + pcolored('ping command is only allowed when online', 'red') + return + end + + pcolored('Starting ping...') + g_game.setPingDelay(0) + connect(g_game, 'onPingBack', pingBack) + end + pinging = not pinging +end + +function clear() + modules.client_terminal.clear() +end + +function ls(path) + path = path or '/' + local files = g_resources.listDirectoryFiles(path) + for k,v in pairs(files) do + if g_resources.directoryExists(path .. v) then + pcolored(path .. v, 'blue') + else + pcolored(path .. v) + end + end +end + +function about_version() + pcolored(g_app.getName() .. ' ' .. g_app.getVersion() .. '\n' .. g_app.getAuthor()) +end + +function about_graphics() + pcolored('Vendor ' .. g_graphics.getVendor() ) + pcolored('Renderer' .. g_graphics.getRenderer()) + pcolored('Version' .. g_graphics.getVersion()) +end + +function about_modules() + for k,m in pairs(g_modules.getModules()) do + local loadedtext + if m:isLoaded() then + pcolored(m:getName() .. ' => loaded', 'green') + else + pcolored(m:getName() .. ' => not loaded', 'red') + end + end +end diff --git a/modules/client_terminal/terminal.lua b/modules/client_terminal/terminal.lua new file mode 100644 index 0000000..584e3f9 --- /dev/null +++ b/modules/client_terminal/terminal.lua @@ -0,0 +1,384 @@ +-- configs +local LogColors = { [LogDebug] = 'pink', + [LogInfo] = 'white', + [LogWarning] = 'yellow', + [LogError] = 'red' } +local MaxLogLines = 128 +local MaxHistory = 1000 + +local oldenv = getfenv(0) +setfenv(0, _G) +_G.commandEnv = runinsandbox('commands') +setfenv(0, oldenv) + +-- private variables +local terminalWindow +local terminalButton +local logLocked = false +local commandTextEdit +local terminalBuffer +local commandHistory = { } +local currentHistoryIndex = 0 +local poped = false +local oldPos +local oldSize +local firstShown = false +local flushEvent +local cachedLines = {} +local disabled = false +local allLines = {} + +-- private functions +local function navigateCommand(step) + if commandTextEdit:isMultiline() then + return + end + + local numCommands = #commandHistory + if numCommands > 0 then + currentHistoryIndex = math.min(math.max(currentHistoryIndex + step, 0), numCommands) + if currentHistoryIndex > 0 then + local command = commandHistory[numCommands - currentHistoryIndex + 1] + commandTextEdit:setText(command) + commandTextEdit:setCursorPos(-1) + else + commandTextEdit:clearText() + end + end +end + +local function completeCommand() + local cursorPos = commandTextEdit:getCursorPos() + if cursorPos == 0 then return end + + local commandBegin = commandTextEdit:getText():sub(1, cursorPos) + local possibleCommands = {} + + -- create a list containing all globals + local allVars = table.copy(_G) + table.merge(allVars, commandEnv) + + -- match commands + for k,v in pairs(allVars) do + if k:sub(1, cursorPos) == commandBegin then + table.insert(possibleCommands, k) + end + end + + -- complete command with one match + if #possibleCommands == 1 then + commandTextEdit:setText(possibleCommands[1]) + commandTextEdit:setCursorPos(-1) + -- show command matches + elseif #possibleCommands > 0 then + print('>> ' .. commandBegin) + + -- expand command + local expandedComplete = commandBegin + local done = false + while not done do + cursorPos = #commandBegin+1 + if #possibleCommands[1] < cursorPos then + break + end + expandedComplete = commandBegin .. possibleCommands[1]:sub(cursorPos, cursorPos) + for i,v in ipairs(possibleCommands) do + if v:sub(1, #expandedComplete) ~= expandedComplete then + done = true + end + end + if not done then + commandBegin = expandedComplete + end + end + commandTextEdit:setText(commandBegin) + commandTextEdit:setCursorPos(-1) + + for i,v in ipairs(possibleCommands) do + print(v) + end + end +end + +local function doCommand(textWidget) + local currentCommand = textWidget:getText() + executeCommand(currentCommand) + textWidget:clearText() + return true +end + +local function addNewline(textWidget) + if not textWidget:isOn() then + textWidget:setOn(true) + end + textWidget:appendText('\n') +end + +local function onCommandChange(textWidget, newText, oldText) + local _, newLineCount = string.gsub(newText, '\n', '\n') + textWidget:setHeight((newLineCount + 1) * textWidget.baseHeight) + + if newLineCount == 0 and textWidget:isOn() then + textWidget:setOn(false) + end +end + +local function onLog(level, message, time) + if disabled then return end + -- avoid logging while reporting logs (would cause a infinite loop) + if logLocked then return end + + logLocked = true + addLine(message, LogColors[level]) + logLocked = false +end + +-- public functions +function init() + terminalWindow = g_ui.displayUI('terminal') + terminalWindow:setVisible(false) + + terminalWindow.onDoubleClick = popWindow + + --terminalButton = modules.client_topmenu.addLeftButton('terminalButton', tr('Terminal') .. ' (Ctrl + T)', '/images/topbuttons/terminal', toggle) + g_keyboard.bindKeyDown('Ctrl+T', toggle) + + commandHistory = g_settings.getList('terminal-history') + + commandTextEdit = terminalWindow:getChildById('commandTextEdit') + commandTextEdit:setHeight(commandTextEdit.baseHeight) + connect(commandTextEdit, {onTextChange = onCommandChange}) + g_keyboard.bindKeyPress('Up', function() navigateCommand(1) end, commandTextEdit) + g_keyboard.bindKeyPress('Down', function() navigateCommand(-1) end, commandTextEdit) + g_keyboard.bindKeyPress('Ctrl+C', + function() + if commandTextEdit:hasSelection() or not terminalSelectText:hasSelection() then return false end + g_window.setClipboardText(terminalSelectText:getSelection()) + return true + end, commandTextEdit) + g_keyboard.bindKeyDown('Tab', completeCommand, commandTextEdit) + g_keyboard.bindKeyPress('Shift+Enter', addNewline, commandTextEdit) + g_keyboard.bindKeyDown('Enter', doCommand, commandTextEdit) + g_keyboard.bindKeyDown('Escape', hide, terminalWindow) + + terminalBuffer = terminalWindow:getChildById('terminalBuffer') + terminalSelectText = terminalWindow:getChildById('terminalSelectText') + terminalSelectText.onDoubleClick = popWindow + terminalSelectText.onMouseWheel = function(a,b,c) terminalBuffer:onMouseWheel(b,c) end + terminalBuffer.onScrollChange = function(self, value) terminalSelectText:setTextVirtualOffset(value) end + + g_logger.setOnLog(onLog) + + if not g_app.isRunning() then + g_logger.fireOldMessages() + elseif _G.terminalLines then + for _,line in pairs(_G.terminalLines) do + addLine(line.text, line.color) + end + end +end + +function terminate() + g_settings.setList('terminal-history', commandHistory) + + removeEvent(flushEvent) + + if poped then + oldPos = terminalWindow:getPosition() + oldSize = terminalWindow:getSize() + end + local settings = { + size = oldSize, + pos = oldPos, + poped = poped + } + g_settings.setNode('terminal-window', settings) + + g_keyboard.unbindKeyDown('Ctrl+T') + g_logger.setOnLog(nil) + terminalWindow:destroy() + --terminalButton:destroy() + commandEnv = nil + _G.terminalLines = allLines +end + +function hideButton() + --terminalButton:hide() +end + +function popWindow() + if poped then + oldPos = terminalWindow:getPosition() + oldSize = terminalWindow:getSize() + terminalWindow:fill('parent') + terminalWindow:setOn(false) + terminalWindow:getChildById('bottomResizeBorder'):disable() + terminalWindow:getChildById('rightResizeBorder'):disable() + terminalWindow:getChildById('titleBar'):hide() + terminalWindow:getChildById('terminalScroll'):setMarginTop(0) + terminalWindow:getChildById('terminalScroll'):setMarginBottom(0) + terminalWindow:getChildById('terminalScroll'):setMarginRight(0) + poped = false + else + terminalWindow:breakAnchors() + terminalWindow:setOn(true) + local size = oldSize or { width = g_window.getWidth()/2.5, height = g_window.getHeight()/4 } + terminalWindow:setSize(size) + local pos = oldPos or { x = 0, y = g_window.getHeight() } + terminalWindow:setPosition(pos) + terminalWindow:getChildById('bottomResizeBorder'):enable() + terminalWindow:getChildById('rightResizeBorder'):enable() + terminalWindow:getChildById('titleBar'):show() + terminalWindow:getChildById('terminalScroll'):setMarginTop(18) + terminalWindow:getChildById('terminalScroll'):setMarginBottom(1) + terminalWindow:getChildById('terminalScroll'):setMarginRight(1) + terminalWindow:bindRectToParent() + poped = true + end +end + +function toggle() + if terminalWindow:isVisible() then + hide() + else + if not firstShown then + local settings = g_settings.getNode('terminal-window') + if settings then + if settings.size then oldSize = settings.size end + if settings.pos then oldPos = settings.pos end + if settings.poped then popWindow() end + end + firstShown = true + end + show() + end +end + +function show() + terminalWindow:show() + terminalWindow:raise() + terminalWindow:focus() +end + +function hide() + terminalWindow:hide() +end + +function disable() + --terminalButton:hide() + g_keyboard.unbindKeyDown('Ctrl+T') + disabled = true +end + +function flushLines() + local numLines = terminalBuffer:getChildCount() + #cachedLines + local fulltext = terminalSelectText:getText() + + for _,line in pairs(cachedLines) do + -- delete old lines if needed + if numLines > MaxLogLines then + local firstChild = terminalBuffer:getChildByIndex(1) + if firstChild then + local len = #firstChild:getText() + firstChild:destroy() + table.remove(allLines, 1) + fulltext = string.sub(fulltext, len) + end + end + + local label = g_ui.createWidget('TerminalLabel', terminalBuffer) + label:setId('terminalLabel' .. numLines) + label:setText(line.text) + label:setColor(line.color) + + table.insert(allLines, {text=line.text,color=line.color}) + + fulltext = fulltext .. '\n' .. line.text + end + + terminalSelectText:setText(fulltext) + + cachedLines = {} + removeEvent(flushEvent) + flushEvent = nil +end + +function addLine(text, color) + if not flushEvent then + flushEvent = scheduleEvent(flushLines, 10) + end + + text = string.gsub(text, '\t', ' ') + table.insert(cachedLines, {text=text, color=color}) +end + +function executeCommand(command) + if command == nil or #string.gsub(command, '\n', '') == 0 then return end + + -- add command line + addLine("> " .. command, "#ffffff") + if g_game.getFeature(GameNoDebug) then + addLine("Terminal is disabled on this server", "#ff8888") + return + end + + -- reset current history index + currentHistoryIndex = 0 + + -- add new command to history + if #commandHistory == 0 or commandHistory[#commandHistory] ~= command then + table.insert(commandHistory, command) + while #commandHistory > MaxHistory do + table.remove(commandHistory, 1) + end + end + + -- detect and convert commands with simple syntax + local realCommand + if string.sub(command, 1, 1) == '=' then + realCommand = 'print(' .. string.sub(command,2) .. ')' + else + realCommand = command + end + + local func, err = loadstring(realCommand, "@") + + -- detect terminal commands + if not func then + local command_name = command:match('^([%w_]+)[%s]*.*') + if command_name then + local args = string.split(command:match('^[%w_]+[%s]*(.*)'), ' ') + if commandEnv[command_name] and type(commandEnv[command_name]) == 'function' then + func = function() modules.client_terminal.commandEnv[command_name](unpack(args)) end + elseif command_name == command then + addLine('ERROR: command not found', 'red') + return + end + end + end + + -- check for syntax errors + if not func then + addLine('ERROR: incorrect lua syntax: ' .. err:sub(5), 'red') + return + end + + -- setup func env to commandEnv + setfenv(func, commandEnv) + + -- execute the command + local ok, ret = pcall(func) + if ok then + -- if the command returned a value, print it + if ret then addLine(ret, 'white') end + else + addLine('ERROR: command failed: ' .. ret, 'red') + end +end + +function clear() + terminalBuffer:destroyChildren() + terminalSelectText:setText('') + cachedLines = {} + allLines = {} +end diff --git a/modules/client_terminal/terminal.otmod b/modules/client_terminal/terminal.otmod new file mode 100644 index 0000000..8e8f00c --- /dev/null +++ b/modules/client_terminal/terminal.otmod @@ -0,0 +1,10 @@ +Module + name: client_terminal + description: Terminal for executing lua functions + author: edubart + website: https://github.com/edubart/otclient + scripts: [ terminal ] + sandboxed: true + reloadable: false + @onLoad: init() + @onUnload: terminate() diff --git a/modules/client_terminal/terminal.otui b/modules/client_terminal/terminal.otui new file mode 100644 index 0000000..67b8ba0 --- /dev/null +++ b/modules/client_terminal/terminal.otui @@ -0,0 +1,115 @@ +TerminalLabel < UILabel + font: terminus-10px + text-wrap: true + text-auto-resize: true + phantom: true + +TerminalSelectText < UITextEdit + font: terminus-10px + text-wrap: true + text-align: bottomLeft + editable: false + change-cursor-image: false + cursor-visible: false + selection-color: black + selection-background-color: white + color: alpha + focusable: false + auto-scroll: false + +UIWindow + id: terminalWindow + background-color: #000000 + opacity: 0.85 + clipping: true + anchors.fill: parent + border: 0 white + $on: + border: 1 black + + Label + id: titleBar + !text: tr('Terminal') + border: 1 black + color: white + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + background-color: #ffffff11 + text-align: left + text-offset: 4 0 + height: 18 + visible: false + + ScrollablePanel + id: terminalBuffer + focusable: false + anchors.left: parent.left + anchors.right: terminalScroll.left + anchors.top: terminalScroll.top + anchors.bottom: commandTextEdit.top + layout: + type: verticalBox + align-bottom: true + vertical-scrollbar: terminalScroll + inverted-scroll: true + margin-left: 2 + + TerminalSelectText + id: terminalSelectText + anchors.fill: terminalBuffer + focusable: false + + VerticalScrollBar + id: terminalScroll + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + step: 48 + pixels-scroll: true + + UILabel + id: commandSymbolLabel + size: 12 12 + fixed-size: true + anchors.bottom: parent.bottom + anchors.left: parent.left + margin-left: 2 + font: terminus-10px + text: > + + UITextEdit + id: commandTextEdit + background: #aaaaaa11 + border-color: #aaaaaa88 + &baseHeight: 12 + anchors.bottom: parent.bottom + anchors.left: commandSymbolLabel.right + anchors.right: terminalScroll.left + margin-left: 1 + padding-left: 2 + font: terminus-10px + selection-color: black + selection-background-color: white + border-width-left: 0 + border-width-top: 0 + multiline: false + + $on: + border-width-left: 1 + border-width-top: 1 + multiline: true + + ResizeBorder + id: bottomResizeBorder + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + enabled: false + + ResizeBorder + id: rightResizeBorder + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + enabled: false diff --git a/modules/client_topmenu/topmenu.lua b/modules/client_topmenu/topmenu.lua new file mode 100644 index 0000000..e86be1a --- /dev/null +++ b/modules/client_topmenu/topmenu.lua @@ -0,0 +1,192 @@ +-- private variables +local topMenu +local fpsUpdateEvent = nil + +local HIDE_TOPMENU = false + +-- private functions +local function addButton(id, description, icon, callback, panel, toggle, front) + local class + if toggle then + class = 'TopToggleButton' + else + class = 'TopButton' + end + + local button = panel:getChildById(id) + if not button then + button = g_ui.createWidget(class) + if front then + panel:insertChild(1, button) + else + panel:addChild(button) + end + end + button:setId(id) + button:setTooltip(description) + button:setIcon(resolvepath(icon, 3)) + button.onMouseRelease = function(widget, mousePos, mouseButton) + if widget:containsPoint(mousePos) and mouseButton ~= MouseMidButton then + callback() + return true + end + end + return button +end + +-- public functions +function init() + connect(g_game, { onGameStart = online, + onGameEnd = offline, + onPingBack = updatePing }) + + topMenu = g_ui.displayUI('topmenu') + g_keyboard.bindKeyDown('Ctrl+Shift+T', toggle) + + if g_game.isOnline() then + online() + end + + updateFps() + + if HIDE_TOPMENU then + topMenu:setHeight(0) + topMenu:hide() + end +end + +function terminate() + disconnect(g_game, { onGameStart = online, + onGameEnd = offline, + onPingBack = updatePing }) + removeEvent(fpsUpdateEvent) + + topMenu:destroy() +end + +function online() + showGameButtons() + + addEvent(function() + if modules.client_options.getOption('showPing') and (g_game.getFeature(GameClientPing) or g_game.getFeature(GameExtendedClientPing)) then + topMenu.pingLabel:show() + else + topMenu.pingLabel:hide() + end + end) +end + +function offline() + hideGameButtons() + topMenu.pingLabel:hide() +end + +function updateFps() + fpsUpdateEvent = scheduleEvent(updateFps, 500) + text = 'FPS: ' .. g_app.getFps() + topMenu.fpsLabel:setText(text) +end + +function updatePing(ping) + if g_proxy and g_proxy.getPing() > 0 then + ping = g_proxy.getPing() + end + + local text = 'Ping: ' + local color + if ping < 0 then + text = text .. "??" + color = 'yellow' + else + text = text .. ping .. ' ms' + if ping >= 500 then + color = 'red' + elseif ping >= 250 then + color = 'yellow' + else + color = 'green' + end + end + topMenu.pingLabel:setColor(color) + topMenu.pingLabel:setText(text) +end + +function setPingVisible(enable) + topMenu.pingLabel:setVisible(enable) +end + +function setFpsVisible(enable) + topMenu.fpsLabel:setVisible(enable) +end + +function addLeftButton(id, description, icon, callback, front) + return addButton(id, description, icon, callback, topMenu.leftButtonsPanel, false, front) +end + +function addLeftToggleButton(id, description, icon, callback, front) + return addButton(id, description, icon, callback, topMenu.leftButtonsPanel, true, front) +end + +function addRightButton(id, description, icon, callback, front) + return addButton(id, description, icon, callback, topMenu.rightButtonsPanel, false, front) +end + +function addRightToggleButton(id, description, icon, callback, front) + return addButton(id, description, icon, callback, topMenu.rightButtonsPanel, true, front) +end + +function addLeftGameButton(id, description, icon, callback, front) + return addButton(id, description, icon, callback, topMenu.leftGameButtonsPanel, false, front) +end + +function addLeftGameToggleButton(id, description, icon, callback, front) + return addButton(id, description, icon, callback, topMenu.leftGameButtonsPanel, true, front) +end + +function addRightGameButton(id, description, icon, callback, front) + return addButton(id, description, icon, callback, topMenu.rightGameButtonsPanel, false, front) +end + +function addRightGameToggleButton(id, description, icon, callback, front) + return addButton(id, description, icon, callback, topMenu.rightGameButtonsPanel, true, front) +end + +function showGameButtons() + topMenu.leftGameButtonsPanel:show() + topMenu.rightGameButtonsPanel:show() +end + +function hideGameButtons() + topMenu.leftGameButtonsPanel:hide() + topMenu.rightGameButtonsPanel:hide() +end + +function getButton(id) + return topMenu:recursiveGetChildById(id) +end + +function getTopMenu() + return topMenu +end + +function toggle() + local menu = getTopMenu() + if not menu then + return + end + + if HIDE_TOPMENU then + return + end + + if menu:isVisible() then + menu:hide() + modules.client_background.getBackground():addAnchor(AnchorTop, 'parent', AnchorTop) + modules.game_interface.getRootPanel():addAnchor(AnchorTop, 'parent', AnchorTop) + else + menu:show() + topMenu:setHeight(36) + modules.client_background.getBackground():addAnchor(AnchorTop, 'topMenu', AnchorBottom) + modules.game_interface.getRootPanel():addAnchor(AnchorTop, 'topMenu', AnchorBottom) + end +end diff --git a/modules/client_topmenu/topmenu.otmod b/modules/client_topmenu/topmenu.otmod new file mode 100644 index 0000000..4e18fd7 --- /dev/null +++ b/modules/client_topmenu/topmenu.otmod @@ -0,0 +1,10 @@ +Module + name: client_topmenu + description: Create the top menu + author: edubart + website: https://github.com/edubart/otclient + scripts: [ topmenu ] + sandboxed: true + @onLoad: init() + @onUnload: terminate() + diff --git a/modules/client_topmenu/topmenu.otui b/modules/client_topmenu/topmenu.otui new file mode 100644 index 0000000..699a7f6 --- /dev/null +++ b/modules/client_topmenu/topmenu.otui @@ -0,0 +1,46 @@ +TopMenuPanel + id: topMenu + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + TopMenuButtonsPanel + id: leftButtonsPanel + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + + TopMenuButtonsPanel + id: leftGameButtonsPanel + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: prev.right + visible: false + + TopMenuFrameCounterLabel + id: fpsLabel + text-auto-resize: true + anchors.top: parent.top + anchors.left: leftGameButtonsPanel.right + anchors.right: rightGameButtonsPanel.left + + TopMenuPingLabel + color: white + id: pingLabel + text-auto-resize: true + anchors.top: fpsLabel.bottom + anchors.left: fpsLabel.left + anchors.right: fpsLabel.right + + TopMenuButtonsPanel + id: rightButtonsPanel + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + TopMenuButtonsPanel + id: rightGameButtonsPanel + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: prev.left + visible: false diff --git a/modules/client_updater/updater.lua b/modules/client_updater/updater.lua new file mode 100644 index 0000000..69a4d2b --- /dev/null +++ b/modules/client_updater/updater.lua @@ -0,0 +1,316 @@ +Updater = { } + +Updater.maxRetries = 5 + +--[[ +HOW IT WORKS: +1. init +2. show +3. generateChecksum and get checksums from url +4. compareChecksums +5. download files with different chekcums +6. call c++ update function +]]-- + +local filesUrl = "" + +local updaterWindow = nil +local initialPanel = nil +local updatePanel = nil +local progressBar = nil +local updateProgressBar = nil +local downloadStatusLabel = nil +local downloadProgressBar = nil +local downloadRetries = 0 + +local generateChecksumsEvent = nil +local updateableFiles = nil +local binaryChecksum = nil +local binaryFile = "" +local fileChecksums = {} +local checksumIter = 0 +local downloadIter = 0 +local aborted = false +local statusData = nil +local thingsUpdate = {} +local toUpdate = {} +local thingsUpdateOptionalError = nil + +local function onDownload(path, checksum, err) + if aborted then + return + end + + if err then + if downloadRetries > Updater.maxRetries then + return updateError("Can't download file: " .. path .. ".\nError: " .. err) + else + downloadRetries = downloadRetries + 1 + return downloadNextFile(true) + end + end + if statusData["files"][path] == nil then + return updateError("Invalid file path: " .. path) + elseif statusData["files"][path] ~= checksum then + return updateError("Invalid file checksum.\nFile: " .. path .. "\nShould be:\n" .. statusData["files"][path] .. "\nIs:\n" .. checksum) + end + downloadIter = downloadIter + 1 + updateProgressBar:setPercent(math.ceil((100 * downloadIter) / #toUpdate)) + downloadProgressBar:setPercent(100) + downloadProgressBar:setText("") + downloadNextFile(false) +end + +local function onDownloadProgress(progress, speed) + downloadProgressBar:setPercent(progress) + downloadProgressBar:setText(speed .. " kbps") +end + +local function gotStatus(data, err) + if err then + return updateError(err) + end + if data["error"] ~= nil and data["error"]:len() > 0 then + return updateError(data["error"]) + end + if data["url"] == nil or data["files"] == nil or data["binary"] == nil then + return updateError("Invalid json data from server") + end + if data["things"] ~= nil then + for file, checksum in pairs(data["things"]) do + if #checksum > 1 then + for thingtype, thingdata in pairs(thingsUpdate) do + if string.match(file:lower(), thingdata[1]:lower()) then + data["files"][file] = checksum + break + end + end + end + end + end + statusData = data + if checksumIter == 100 then + compareChecksums() + end +end + +-- public functions +function Updater.init() + updaterWindow = g_ui.displayUI('updater') + updaterWindow:hide() + + initialPanel = updaterWindow:getChildById('initialPanel') + updatePanel = updaterWindow:getChildById('updatePanel') + progressBar = initialPanel:getChildById('progressBar') + updateProgressBar = updatePanel:getChildById('updateProgressBar') + downloadStatusLabel = updatePanel:getChildById('downloadStatusLabel') + downloadProgressBar = updatePanel:getChildById('downloadProgressBar') + updatePanel:hide() + + scheduleEvent(Updater.show, 200) +end + +function Updater.terminate() + updaterWindow:destroy() + updaterWindow = nil + + removeEvent(generateChecksumsEvent) +end + +local function clear() + removeEvent(generateChecksumsEvent) + + updateableFiles = nil + binaryChecksum = nil + binaryFile = "" + fileChecksums = {} + checksumIter = 0 + downloadIter = 0 + aborted = false + statusData = nil + toUpdate = {} + progressBar:setPercent(0) + updateProgressBar:setPercent(0) + downloadProgressBar:setPercent(0) + downloadProgressBar:setText("") +end + +function Updater.show() + if not g_resources.isLoadedFromArchive() or Services.updater == nil or Services.updater:len() < 4 then + return Updater.hide() + end + if updaterWindow:isVisible() then + return + end + updaterWindow:show() + updaterWindow:raise() + updaterWindow:focus() + if EnterGame then + EnterGame.hide() + end + + clear() + + updateableFiles = g_resources.listUpdateableFiles() + if #updateableFiles < 1 then + return updateError("Can't get list of files") + end + binaryChecksum = g_resources.selfChecksum():lower() + if binaryChecksum:len() ~= 32 then + return updateError("Invalid binary checksum: " .. binaryChecksum) + end + + local data = { + version = APP_VERSION, + platform = g_window.getPlatformType(), + uid = G.UUID, + build_version = g_app.getVersion(), + build_revision = g_app.getBuildRevision(), + build_commit = g_app.getBuildCommit(), + build_date = g_app.getBuildDate(), + os = g_app.getOs(), + os_name = g_platform.getOSName() + } + HTTP.postJSON(Services.updater, data, gotStatus) + if generateChecksumsEvent == nil then + generateChecksumsEvent = scheduleEvent(generateChecksum, 5) + end +end + +function Updater.isVisible() + return updaterWindow:isVisible() +end + +function Updater.updateThings(things, optionalError) + thingsUpdate = things + thingsUpdateOptionalError = optionalError + Updater:show() +end + +function Updater.hide() + updaterWindow:hide() + if thingsUpdateOptionalError then + local msgbox = displayErrorBox("Updater error", thingsUpdateOptionalError:trim()) + msgbox.onOk = function() if EnterGame then EnterGame.show() end end + thingsUpdateOptionalError = nil + elseif EnterGame then + EnterGame.show() + end +end + +function Updater.abort() + aborted = true + Updater:hide() +end + +function generateChecksum() + local entries = #updateableFiles + local fromEntry = math.floor((checksumIter) * (entries / 100)) + local toEntry = math.floor((checksumIter + 1) * (entries / 100)) + if checksumIter == 99 then + toEntry = #updateableFiles + end + for i=fromEntry+1,toEntry do + local fileName = updateableFiles[i] + fileChecksums[fileName] = g_resources.fileChecksum(fileName):lower() + end + + checksumIter = checksumIter + 1 + if checksumIter == 100 then + generateChecksumsEvent = nil + gotChecksums() + else + progressBar:setPercent(math.ceil(checksumIter * 0.95)) + generateChecksumsEvent = scheduleEvent(generateChecksum, 5) + end +end + +function gotChecksums() + if statusData ~= nil then + compareChecksums() + end +end + +function compareChecksums() + for file, checksum in pairs(statusData["files"]) do + checksum = checksum:lower() + if file == statusData["binary"] then + if binaryChecksum ~= checksum then + binaryFile = file + table.insert(toUpdate, binaryFile) + end + else + local localChecksum = fileChecksums[file] + if localChecksum ~= checksum then + table.insert(toUpdate, file) + end + end + end + if #toUpdate == 0 then + return upToDate() + end + -- outdated + filesUrl = statusData["url"] + initialPanel:hide() + updatePanel:show() + updatePanel:getChildById('updateStatusLabel'):setText(tr("Updating %i files", #toUpdate)) + updaterWindow:setHeight(190) + downloadNextFile(false) +end + +function upToDate() + Updater.hide() +end + +function updateError(err) + Updater.hide() + local msgbox = displayErrorBox("Updater error", err) + msgbox.onOk = function() if EnterGame then EnterGame.show() end end +end + +function urlencode(url) + url = url:gsub("\n", "\r\n") + url = url:gsub("([^%w ])", function(c) string.format("%%%02X", string.byte(c)) end) + url = url:gsub(" ", "+") + return url +end + +function downloadNextFile(retry) + if aborted then + return + end + + updaterWindow:show() + updaterWindow:raise() + updaterWindow:focus() + + if downloadIter == #toUpdate then + return downloadingFinished() + end + + if retry then + retry = " (" .. downloadRetries .. " retry)" + else + retry = "" + end + + local file = toUpdate[downloadIter + 1] + downloadStatusLabel:setText(tr("Downloading %i of %i%s:\n%s", downloadIter + 1, #toUpdate, retry, file)) + downloadProgressBar:setPercent(0) + downloadProgressBar:setText("") + HTTP.download(filesUrl .. urlencode(file), file, onDownload, onDownloadProgress) +end + +function downloadingFinished() + thingsUpdateOptionalError = nil + UIMessageBox.display(tr("Success"), tr("Download complate.\nUpdating client..."), {}, nil, nil) + scheduleEvent(function() + local files = {} + for file, checksum in pairs(statusData["files"]) do + table.insert(files, file) + end + g_settings.save() + g_resources.updateClient(files, binaryFile) + g_app.quick_exit() + end, 1000) +end diff --git a/modules/client_updater/updater.otmod b/modules/client_updater/updater.otmod new file mode 100644 index 0000000..89f4b2b --- /dev/null +++ b/modules/client_updater/updater.otmod @@ -0,0 +1,9 @@ +Module + name: client_updater + description: Updates client + author: otclient@otclient.ovh + website: otclient.ovh + reloadable: false + scripts: [ updater ] + @onLoad: Updater.init() + @onUnload: Updater.terminate() diff --git a/modules/client_updater/updater.otui b/modules/client_updater/updater.otui new file mode 100644 index 0000000..290cdb2 --- /dev/null +++ b/modules/client_updater/updater.otui @@ -0,0 +1,75 @@ +StaticMainWindow + id: updaterWindow + !text: tr('Updater') + height: 125 + width: 300 + + Panel + id: initialPanel + layout: + type: verticalBox + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + margin: 0 5 5 5 + + Label + id: statusLabel + !text: tr('Checking for updates') + text-align: center + + ProgressBar + id: progressBar + height: 15 + background-color: #4444ff + margin-bottom: 10 + margin-top: 10 + + Button + !text: tr('Cancel') + margin-left: 70 + margin-right: 70 + @onClick: Updater.abort() + + Panel + id: updatePanel + layout: + type: verticalBox + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + margin: 0 5 5 5 + + Label + id: updateStatusLabel + !text: tr('Updating') + text-align: center + + ProgressBar + id: updateProgressBar + height: 15 + background-color: #4444ff + margin-bottom: 10 + margin-top: 10 + + Label + id: downloadStatusLabel + !text: tr('Downloading:') + text-align: center + margin-top: 5 + height: 25 + + ProgressBar + id: downloadProgressBar + height: 15 + background-color: #4444ff + margin-bottom: 10 + margin-top: 10 + + Button + !text: tr('Cancel') + margin-left: 70 + margin-right: 70 + @onClick: Updater.abort() diff --git a/modules/corelib/bitwise.lua b/modules/corelib/bitwise.lua new file mode 100644 index 0000000..0eed21c --- /dev/null +++ b/modules/corelib/bitwise.lua @@ -0,0 +1,17 @@ +Bit = {} + +function Bit.bit(p) + return 2 ^ p +end + +function Bit.hasBit(x, p) + return x % (p + p) >= p +end + +function Bit.setbit(x, p) + return Bit.hasBit(x, p) and x or x + p +end + +function Bit.clearbit(x, p) + return Bit.hasBit(x, p) and x - p or x +end \ No newline at end of file diff --git a/modules/corelib/config.lua b/modules/corelib/config.lua new file mode 100644 index 0000000..27037ff --- /dev/null +++ b/modules/corelib/config.lua @@ -0,0 +1,73 @@ +-- @docclass + +local function convertSettingValue(value) + if type(value) == 'table' then + if value.x and value.width then + return recttostring(value) + elseif value.x then + return pointtostring(value) + elseif value.width then + return sizetostring(value) + elseif value.r then + return colortostring(value) + else + return value + end + elseif value == nil then + return '' + else + return tostring(value) + end +end + +function Config:set(key, value) + self:setValue(key, convertSettingValue(value)) +end + +function Config:setDefault(key, value) + if self:exists(key) then return false end + self:set(key, value) + return true +end + +function Config:get(key, default) + if not self:exists(key) and default ~= nil then + self:set(key, default) + end + return self:getValue(key) +end + +function Config:getString(key, default) + return self:get(key, default) +end + +function Config:getInteger(key, default) + local v = tonumber(self:get(key, default)) or 0 + return v +end + +function Config:getNumber(key, default) + local v = tonumber(self:get(key, default)) or 0 + return v +end + +function Config:getBoolean(key, default) + return toboolean(self:get(key, default)) +end + +function Config:getPoint(key, default) + return topoint(self:get(key, default)) +end + +function Config:getRect(key, default) + return torect(self:get(key, default)) +end + +function Config:getSize(key, default) + return tosize(self:get(key, default)) +end + +function Config:getColor(key, default) + return tocolor(self:get(key, default)) +end + diff --git a/modules/corelib/const.lua b/modules/corelib/const.lua new file mode 100644 index 0000000..d199530 --- /dev/null +++ b/modules/corelib/const.lua @@ -0,0 +1,321 @@ +-- @docconsts @{ + +AnchorNone = 0 +AnchorTop = 1 +AnchorBottom = 2 +AnchorLeft = 3 +AnchorRight = 4 +AnchorVerticalCenter = 5 +AnchorHorizontalCenter = 6 + +LogDebug = 0 +LogInfo = 1 +LogWarning = 2 +LogError = 3 +LogFatal = 4 + +MouseFocusReason = 0 +KeyboardFocusReason = 1 +ActiveFocusReason = 2 +OtherFocusReason = 3 + +AutoFocusNone = 0 +AutoFocusFirst = 1 +AutoFocusLast = 2 + +KeyboardNoModifier = 0 +KeyboardCtrlModifier = 1 +KeyboardAltModifier = 2 +KeyboardCtrlAltModifier = 3 +KeyboardShiftModifier = 4 +KeyboardCtrlShiftModifier = 5 +KeyboardAltShiftModifier = 6 +KeyboardCtrlAltShiftModifier = 7 + +MouseNoButton = 0 +MouseLeftButton = 1 +MouseRightButton = 2 +MouseMidButton = 3 + +MouseNoWheel = 0 +MouseWheelUp = 1 +MouseWheelDown = 2 + +AlignNone = 0 +AlignLeft = 1 +AlignRight = 2 +AlignTop = 4 +AlignBottom = 8 +AlignHorizontalCenter = 16 +AlignVerticalCenter = 32 +AlignTopLeft = 5 +AlignTopRight = 6 +AlignBottomLeft = 9 +AlignBottomRight = 10 +AlignLeftCenter = 33 +AlignRightCenter = 34 +AlignTopCenter = 20 +AlignBottomCenter = 24 +AlignCenter = 48 + +KeyUnknown = 0 +KeyEscape = 1 +KeyTab = 2 +KeyBackspace = 3 +KeyEnter = 5 +KeyInsert = 6 +KeyDelete = 7 +KeyPause = 8 +KeyPrintScreen = 9 +KeyHome = 10 +KeyEnd = 11 +KeyPageUp = 12 +KeyPageDown = 13 +KeyUp = 14 +KeyDown = 15 +KeyLeft = 16 +KeyRight = 17 +KeyNumLock = 18 +KeyScrollLock = 19 +KeyCapsLock = 20 +KeyCtrl = 21 +KeyShift = 22 +KeyAlt = 23 +KeyMeta = 25 +KeyMenu = 26 +KeySpace = 32 -- ' ' +KeyExclamation = 33 -- ! +KeyQuote = 34 -- " +KeyNumberSign = 35 -- # +KeyDollar = 36 -- $ +KeyPercent = 37 -- % +KeyAmpersand = 38 -- & +KeyApostrophe = 39 -- ' +KeyLeftParen = 40 -- ( +KeyRightParen = 41 -- ) +KeyAsterisk = 42 -- * +KeyPlus = 43 -- + +KeyComma = 44 -- , +KeyMinus = 45 -- - +KeyPeriod = 46 -- . +KeySlash = 47 -- / +Key0 = 48 -- 0 +Key1 = 49 -- 1 +Key2 = 50 -- 2 +Key3 = 51 -- 3 +Key4 = 52 -- 4 +Key5 = 53 -- 5 +Key6 = 54 -- 6 +Key7 = 55 -- 7 +Key8 = 56 -- 8 +Key9 = 57 -- 9 +KeyColon = 58 -- : +KeySemicolon = 59 -- ; +KeyLess = 60 -- < +KeyEqual = 61 -- = +KeyGreater = 62 -- > +KeyQuestion = 63 -- ? +KeyAtSign = 64 -- @ +KeyA = 65 -- a +KeyB = 66 -- b +KeyC = 67 -- c +KeyD = 68 -- d +KeyE = 69 -- e +KeyF = 70 -- f +KeyG = 71 -- g +KeyH = 72 -- h +KeyI = 73 -- i +KeyJ = 74 -- j +KeyK = 75 -- k +KeyL = 76 -- l +KeyM = 77 -- m +KeyN = 78 -- n +KeyO = 79 -- o +KeyP = 80 -- p +KeyQ = 81 -- q +KeyR = 82 -- r +KeyS = 83 -- s +KeyT = 84 -- t +KeyU = 85 -- u +KeyV = 86 -- v +KeyW = 87 -- w +KeyX = 88 -- x +KeyY = 89 -- y +KeyZ = 90 -- z +KeyLeftBracket = 91 -- [ +KeyBackslash = 92 -- '\' +KeyRightBracket = 93 -- ] +KeyCaret = 94 -- ^ +KeyUnderscore = 95 -- _ +KeyGrave = 96 -- ` +KeyLeftCurly = 123 -- { +KeyBar = 124 -- | +KeyRightCurly = 125 -- } +KeyTilde = 126 -- ~ +KeyF1 = 128 +KeyF2 = 129 +KeyF3 = 130 +KeyF4 = 131 +KeyF5 = 132 +KeyF6 = 134 +KeyF7 = 135 +KeyF8 = 136 +KeyF9 = 137 +KeyF10 = 138 +KeyF11 = 139 +KeyF12 = 140 +KeyNumpad0 = 141 +KeyNumpad1 = 142 +KeyNumpad2 = 143 +KeyNumpad3 = 144 +KeyNumpad4 = 145 +KeyNumpad5 = 146 +KeyNumpad6 = 147 +KeyNumpad7 = 148 +KeyNumpad8 = 149 +KeyNumpad9 = 150 + +FirstKey = KeyUnknown +LastKey = KeyNumpad9 + +ExtendedActivate = 0 +ExtendedLocales = 1 +ExtendedParticles = 2 + +-- @} + +KeyCodeDescs = { + [KeyUnknown] = 'Unknown', + [KeyEscape] = 'Escape', + [KeyTab] = 'Tab', + [KeyBackspace] = 'Backspace', + [KeyEnter] = 'Enter', + [KeyInsert] = 'Insert', + [KeyDelete] = 'Delete', + [KeyPause] = 'Pause', + [KeyPrintScreen] = 'PrintScreen', + [KeyHome] = 'Home', + [KeyEnd] = 'End', + [KeyPageUp] = 'PageUp', + [KeyPageDown] = 'PageDown', + [KeyUp] = 'Up', + [KeyDown] = 'Down', + [KeyLeft] = 'Left', + [KeyRight] = 'Right', + [KeyNumLock] = 'NumLock', + [KeyScrollLock] = 'ScrollLock', + [KeyCapsLock] = 'CapsLock', + [KeyCtrl] = 'Ctrl', + [KeyShift] = 'Shift', + [KeyAlt] = 'Alt', + [KeyMeta] = 'Meta', + [KeyMenu] = 'Menu', + [KeySpace] = 'Space', + [KeyExclamation] = '!', + [KeyQuote] = '\"', + [KeyNumberSign] = '#', + [KeyDollar] = '$', + [KeyPercent] = '%', + [KeyAmpersand] = '&', + [KeyApostrophe] = '\'', + [KeyLeftParen] = '(', + [KeyRightParen] = ')', + [KeyAsterisk] = '*', + [KeyPlus] = 'Plus', + [KeyComma] = ',', + [KeyMinus] = '-', + [KeyPeriod] = '.', + [KeySlash] = '/', + [Key0] = '0', + [Key1] = '1', + [Key2] = '2', + [Key3] = '3', + [Key4] = '4', + [Key5] = '5', + [Key6] = '6', + [Key7] = '7', + [Key8] = '8', + [Key9] = '9', + [KeyColon] = ':', + [KeySemicolon] = ';', + [KeyLess] = '<', + [KeyEqual] = '=', + [KeyGreater] = '>', + [KeyQuestion] = '?', + [KeyAtSign] = '@', + [KeyA] = 'A', + [KeyB] = 'B', + [KeyC] = 'C', + [KeyD] = 'D', + [KeyE] = 'E', + [KeyF] = 'F', + [KeyG] = 'G', + [KeyH] = 'H', + [KeyI] = 'I', + [KeyJ] = 'J', + [KeyK] = 'K', + [KeyL] = 'L', + [KeyM] = 'M', + [KeyN] = 'N', + [KeyO] = 'O', + [KeyP] = 'P', + [KeyQ] = 'Q', + [KeyR] = 'R', + [KeyS] = 'S', + [KeyT] = 'T', + [KeyU] = 'U', + [KeyV] = 'V', + [KeyW] = 'W', + [KeyX] = 'X', + [KeyY] = 'Y', + [KeyZ] = 'Z', + [KeyLeftBracket] = '[', + [KeyBackslash] = '\\', + [KeyRightBracket] = ']', + [KeyCaret] = '^', + [KeyUnderscore] = '_', + [KeyGrave] = '`', + [KeyLeftCurly] = '{', + [KeyBar] = '|', + [KeyRightCurly] = '}', + [KeyTilde] = '~', + [KeyF1] = 'F1', + [KeyF2] = 'F2', + [KeyF3] = 'F3', + [KeyF4] = 'F4', + [KeyF5] = 'F5', + [KeyF6] = 'F6', + [KeyF7] = 'F7', + [KeyF8] = 'F8', + [KeyF9] = 'F9', + [KeyF10] = 'F10', + [KeyF11] = 'F11', + [KeyF12] = 'F12', + [KeyNumpad0] = 'Numpad0', + [KeyNumpad1] = 'Numpad1', + [KeyNumpad2] = 'Numpad2', + [KeyNumpad3] = 'Numpad3', + [KeyNumpad4] = 'Numpad4', + [KeyNumpad5] = 'Numpad5', + [KeyNumpad6] = 'Numpad6', + [KeyNumpad7] = 'Numpad7', + [KeyNumpad8] = 'Numpad8', + [KeyNumpad9] = 'Numpad9', +} + +NetworkMessageTypes = { + Boolean = 1, + U8 = 2, + U16 = 3, + U32 = 4, + U64 = 5, + NumberString = 6, + String = 7, + Table = 8, +} + +SoundChannels = { + Music = 1, + Ambient = 2, + Effect = 3, +} diff --git a/modules/corelib/corelib.otmod b/modules/corelib/corelib.otmod new file mode 100644 index 0000000..669527e --- /dev/null +++ b/modules/corelib/corelib.otmod @@ -0,0 +1,33 @@ +Module + name: corelib + description: Contains core lua classes, functions and constants used by other modules + author: OTClient team + website: https://github.com/edubart/otclient + reloadable: false + + @onLoad: | + dofile 'math' + dofile 'string' + dofile 'table' + dofile 'bitwise' + dofile 'struct' + + dofile 'const' + dofile 'util' + dofile 'globals' + dofile 'config' + dofile 'settings' + dofile 'keyboard' + dofile 'mouse' + dofile 'net' + + dofiles 'classes' + dofiles 'ui' + + dofile 'inputmessage' + dofile 'outputmessage' + dofile 'orderedtable' + + dofile 'json' + dofile 'http' + \ No newline at end of file diff --git a/modules/corelib/globals.lua b/modules/corelib/globals.lua new file mode 100644 index 0000000..172f2c6 --- /dev/null +++ b/modules/corelib/globals.lua @@ -0,0 +1,76 @@ +-- @docvars @{ + +-- root widget +rootWidget = g_ui.getRootWidget() +modules = package.loaded + +-- G is used as a global table to save variables in memory between reloads +G = G or {} + +-- @} + +-- @docfuncs @{ + +function scheduleEvent(callback, delay) + local desc = "lua" + local info = debug.getinfo(2, "Sl") + if info then + desc = info.short_src .. ":" .. info.currentline + end + local event = g_dispatcher.scheduleEvent(desc, callback, delay) + -- must hold a reference to the callback, otherwise it would be collected + event._callback = callback + return event +end + +function addEvent(callback, front) + local desc = "lua" + local info = debug.getinfo(2, "Sl") + if info then + desc = info.short_src .. ":" .. info.currentline + end + local event = g_dispatcher.addEvent(desc, callback, front) + -- must hold a reference to the callback, otherwise it would be collected + event._callback = callback + return event +end + +function cycleEvent(callback, interval) + local desc = "lua" + local info = debug.getinfo(2, "Sl") + if info then + desc = info.short_src .. ":" .. info.currentline + end + local event = g_dispatcher.cycleEvent(desc, callback, interval) + -- must hold a reference to the callback, otherwise it would be collected + event._callback = callback + return event +end + +function periodicalEvent(eventFunc, conditionFunc, delay, autoRepeatDelay) + delay = delay or 30 + autoRepeatDelay = autoRepeatDelay or delay + + local func + func = function() + if conditionFunc and not conditionFunc() then + func = nil + return + end + eventFunc() + scheduleEvent(func, delay) + end + + scheduleEvent(function() + func() + end, autoRepeatDelay) +end + +function removeEvent(event) + if event then + event:cancel() + event._callback = nil + end +end + +-- @} \ No newline at end of file diff --git a/modules/corelib/http.lua b/modules/corelib/http.lua new file mode 100644 index 0000000..a041b5e --- /dev/null +++ b/modules/corelib/http.lua @@ -0,0 +1,157 @@ +HTTP = { + timeout=5, + imageId=1000, + images={}, + operations={} +} + +function HTTP.get(url, callback) + local operation = g_http.get(url, HTTP.timeout) + HTTP.operations[operation] = {type="get", url=url, callback=callback} + return opreation +end + +function HTTP.getJSON(url, callback) + local operation = g_http.get(url, HTTP.timeout) + HTTP.operations[operation] = {type="get", json=true, url=url, callback=callback} + return opreation +end + +function HTTP.post(url, data, callback) + if type(data) == "table" then + data = json.encode(data) + end + local operation = g_http.post(url, data, HTTP.timeout) + HTTP.operations[operation] = {type="post", url=url, callback=callback} + return opreation +end + +function HTTP.postJSON(url, data, callback) + if type(data) == "table" then + data = json.encode(data) + end + local operation = g_http.post(url, data, HTTP.timeout) + HTTP.operations[operation] = {type="post", json=true, url=url, callback=callback} + return opreation +end + +function HTTP.download(url, file, callback, progressCallback) + local operation = g_http.download(url, file, HTTP.timeout) + HTTP.operations[operation] = {type="download", url=url, file=file, callback=callback, progressCallback=progressCallback} + return opreation +end + +function HTTP.downloadImage(url, callback) + if HTTP.images[url] ~= nil then + if callback then + callback('/downloads/' .. HTTP.images[url], nil) + end + return + end + local file = "autoimage_" .. HTTP.imageId .. ".png" + HTTP.imageId = HTTP.imageId + 1 + local operation = g_http.download(url, file, HTTP.timeout) + HTTP.operations[operation] = {type="image", url=url, file=file, callback=callback} + return opreation +end + +function HTTP.progress(operationId) + return g_http.getProgress(operationId) +end + +function HTTP.cancel(operationId) + return g_http.cancel(operationId) +end + +function HTTP.onGet(operationId, url, err, data) + local operation = HTTP.operations[operationId] + if operation == nil then + return + end + if err and err:len() == 0 then + err = nil + end + if not err and operation.json then + local status, result = pcall(function() return json.decode(data) end) + if not status then + err = "JSON ERROR: " .. result + end + data = result + end + if operation.callback then + operation.callback(data, err) + end +end + +function HTTP.onGetProgress(operationId, url, progress) + local operation = HTTP.operations[operationId] + if operation == nil then + return + end + +end + +function HTTP.onPost(operationId, url, err, data) + local operation = HTTP.operations[operationId] + if operation == nil then + return + end + if err and err:len() == 0 then + err = nil + end + if not err and operation.json then + local status, result = pcall(function() return json.decode(data) end) + if not status then + err = "JSON ERROR: " .. result + end + data = result + end + if operation.callback then + operation.callback(data, err) + end +end + +function HTTP.onPostProgress(operationId, url, progress) + local operation = HTTP.operations[operationId] + if operation == nil then + return + end +end + +function HTTP.onDownload(operationId, url, err, path, checksum) + local operation = HTTP.operations[operationId] + if operation == nil then + return + end + if err and err:len() == 0 then + err = nil + end + if operation.callback then + if operation["type"] == "image" then + HTTP.images[url] = path + operation.callback('/downloads/' .. path, err) + else + operation.callback(path, checksum, err) + end + end +end + +function HTTP.onDownloadProgress(operationId, url, progress, speed) + local operation = HTTP.operations[operationId] + if operation == nil then + return + end + if operation.progressCallback then + operation.progressCallback(progress, speed) + end +end + +connect(g_http, + { + onGet = HTTP.onGet, + onGetProgress = HTTP.onGetProgress, + onPost = HTTP.onPost, + onPostProgress = HTTP.onPostProgress, + onDownload = HTTP.onDownload, + onDownloadProgress = HTTP.onDownloadProgress + }) \ No newline at end of file diff --git a/modules/corelib/inputmessage.lua b/modules/corelib/inputmessage.lua new file mode 100644 index 0000000..48597e4 --- /dev/null +++ b/modules/corelib/inputmessage.lua @@ -0,0 +1,51 @@ +function InputMessage:getData() + local dataType = self:getU8() + if dataType == NetworkMessageTypes.Boolean then + return numbertoboolean(self:getU8()) + elseif dataType == NetworkMessageTypes.U8 then + return self:getU8() + elseif dataType == NetworkMessageTypes.U16 then + return self:getU16() + elseif dataType == NetworkMessageTypes.U32 then + return self:getU32() + elseif dataType == NetworkMessageTypes.U64 then + return self:getU64() + elseif dataType == NetworkMessageTypes.NumberString then + return tonumber(self:getString()) + elseif dataType == NetworkMessageTypes.String then + return self:getString() + elseif dataType == NetworkMessageTypes.Table then + return self:getTable() + else + perror('Unknown data type ' .. dataType) + end + return nil +end + +function InputMessage:getTable() + local ret = {} + local size = self:getU16() + for i=1,size do + local index = self:getData() + local value = self:getData() + ret[index] = value + end + return ret +end + +function InputMessage:getColor() + local color = {} + color.r = self:getU8() + color.g = self:getU8() + color.b = self:getU8() + color.a = self:getU8() + return color +end + +function InputMessage:getPosition() + local position = {} + position.x = self:getU16() + position.y = self:getU16() + position.z = self:getU8() + return position +end diff --git a/modules/corelib/json.lua b/modules/corelib/json.lua new file mode 100644 index 0000000..21f6f89 --- /dev/null +++ b/modules/corelib/json.lua @@ -0,0 +1,397 @@ +-- +-- json.lua +-- +-- Copyright (c) 2018 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +json = { _version = "0.1.1" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if val[1] ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end diff --git a/modules/corelib/keyboard.lua b/modules/corelib/keyboard.lua new file mode 100644 index 0000000..04f6eb4 --- /dev/null +++ b/modules/corelib/keyboard.lua @@ -0,0 +1,226 @@ +-- @docclass +g_keyboard = {} + +-- private functions +function translateKeyCombo(keyCombo) + if not keyCombo or #keyCombo == 0 then return nil end + local keyComboDesc = '' + for k,v in pairs(keyCombo) do + local keyDesc = KeyCodeDescs[v] + if keyDesc == nil then return nil end + keyComboDesc = keyComboDesc .. '+' .. keyDesc + end + keyComboDesc = keyComboDesc:sub(2) + return keyComboDesc +end + +local function getKeyCode(key) + for keyCode, keyDesc in pairs(KeyCodeDescs) do + if keyDesc:lower() == key:trim():lower() then + return keyCode + end + end +end + +function retranslateKeyComboDesc(keyComboDesc) + if keyComboDesc == nil then + error('Unable to translate key combo \'' .. keyComboDesc .. '\'') + end + + if type(keyComboDesc) == 'number' then + keyComboDesc = tostring(keyComboDesc) + end + + local keyCombo = {} + for i,currentKeyDesc in ipairs(keyComboDesc:split('+')) do + for keyCode, keyDesc in pairs(KeyCodeDescs) do + if keyDesc:lower() == currentKeyDesc:trim():lower() then + table.insert(keyCombo, keyCode) + end + end + end + return translateKeyCombo(keyCombo) +end + +function determineKeyComboDesc(keyCode, keyboardModifiers) + local keyCombo = {} + if keyCode == KeyCtrl or keyCode == KeyShift or keyCode == KeyAlt then + table.insert(keyCombo, keyCode) + elseif KeyCodeDescs[keyCode] ~= nil then + if keyboardModifiers == KeyboardCtrlModifier then + table.insert(keyCombo, KeyCtrl) + elseif keyboardModifiers == KeyboardAltModifier then + table.insert(keyCombo, KeyAlt) + elseif keyboardModifiers == KeyboardCtrlAltModifier then + table.insert(keyCombo, KeyCtrl) + table.insert(keyCombo, KeyAlt) + elseif keyboardModifiers == KeyboardShiftModifier then + table.insert(keyCombo, KeyShift) + elseif keyboardModifiers == KeyboardCtrlShiftModifier then + table.insert(keyCombo, KeyCtrl) + table.insert(keyCombo, KeyShift) + elseif keyboardModifiers == KeyboardAltShiftModifier then + table.insert(keyCombo, KeyAlt) + table.insert(keyCombo, KeyShift) + elseif keyboardModifiers == KeyboardCtrlAltShiftModifier then + table.insert(keyCombo, KeyCtrl) + table.insert(keyCombo, KeyAlt) + table.insert(keyCombo, KeyShift) + end + table.insert(keyCombo, keyCode) + end + return translateKeyCombo(keyCombo) +end + +local function onWidgetKeyDown(widget, keyCode, keyboardModifiers) + if keyCode == KeyUnknown then return false end + local callback = widget.boundAloneKeyDownCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)] + signalcall(callback, widget, keyCode) + callback = widget.boundKeyDownCombos[determineKeyComboDesc(keyCode, keyboardModifiers)] + return signalcall(callback, widget, keyCode) +end + +local function onWidgetKeyUp(widget, keyCode, keyboardModifiers) + if keyCode == KeyUnknown then return false end + local callback = widget.boundAloneKeyUpCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)] + signalcall(callback, widget, keyCode) + callback = widget.boundKeyUpCombos[determineKeyComboDesc(keyCode, keyboardModifiers)] + return signalcall(callback, widget, keyCode) +end + +local function onWidgetKeyPress(widget, keyCode, keyboardModifiers, autoRepeatTicks) + if keyCode == KeyUnknown then return false end + local callback = widget.boundKeyPressCombos[determineKeyComboDesc(keyCode, keyboardModifiers)] + return signalcall(callback, widget, keyCode, autoRepeatTicks) +end + +local function connectKeyDownEvent(widget) + if widget.boundKeyDownCombos then return end + connect(widget, { onKeyDown = onWidgetKeyDown }) + widget.boundKeyDownCombos = {} + widget.boundAloneKeyDownCombos = {} +end + +local function connectKeyUpEvent(widget) + if widget.boundKeyUpCombos then return end + connect(widget, { onKeyUp = onWidgetKeyUp }) + widget.boundKeyUpCombos = {} + widget.boundAloneKeyUpCombos = {} +end + +local function connectKeyPressEvent(widget) + if widget.boundKeyPressCombos then return end + connect(widget, { onKeyPress = onWidgetKeyPress }) + widget.boundKeyPressCombos = {} +end + +-- public functions +function g_keyboard.bindKeyDown(keyComboDesc, callback, widget, alone) + widget = widget or rootWidget + connectKeyDownEvent(widget) + local keyComboDesc = retranslateKeyComboDesc(keyComboDesc) + if alone then + connect(widget.boundAloneKeyDownCombos, keyComboDesc, callback) + else + connect(widget.boundKeyDownCombos, keyComboDesc, callback) + end +end + +function g_keyboard.bindKeyUp(keyComboDesc, callback, widget, alone) + widget = widget or rootWidget + connectKeyUpEvent(widget) + local keyComboDesc = retranslateKeyComboDesc(keyComboDesc) + if alone then + connect(widget.boundAloneKeyUpCombos, keyComboDesc, callback) + else + connect(widget.boundKeyUpCombos, keyComboDesc, callback) + end +end + +function g_keyboard.bindKeyPress(keyComboDesc, callback, widget) + widget = widget or rootWidget + connectKeyPressEvent(widget) + local keyComboDesc = retranslateKeyComboDesc(keyComboDesc) + connect(widget.boundKeyPressCombos, keyComboDesc, callback) +end + +local function getUnbindArgs(arg1, arg2) + local callback + local widget + if type(arg1) == 'function' then callback = arg1 + elseif type(arg2) == 'function' then callback = arg2 end + if type(arg1) == 'userdata' then widget = arg1 + elseif type(arg2) == 'userdata' then widget = arg2 end + widget = widget or rootWidget + return callback, widget +end + +function g_keyboard.unbindKeyDown(keyComboDesc, arg1, arg2) + local callback, widget = getUnbindArgs(arg1, arg2) + if widget.boundKeyDownCombos == nil then return end + local keyComboDesc = retranslateKeyComboDesc(keyComboDesc) + disconnect(widget.boundKeyDownCombos, keyComboDesc, callback) +end + +function g_keyboard.unbindKeyUp(keyComboDesc, arg1, arg2) + local callback, widget = getUnbindArgs(arg1, arg2) + if widget.boundKeyUpCombos == nil then return end + local keyComboDesc = retranslateKeyComboDesc(keyComboDesc) + disconnect(widget.boundKeyUpCombos, keyComboDesc, callback) +end + +function g_keyboard.unbindKeyPress(keyComboDesc, arg1, arg2) + local callback, widget = getUnbindArgs(arg1, arg2) + if widget.boundKeyPressCombos == nil then return end + local keyComboDesc = retranslateKeyComboDesc(keyComboDesc) + disconnect(widget.boundKeyPressCombos, keyComboDesc, callback) +end + +function g_keyboard.getModifiers() + return g_window.getKeyboardModifiers() +end + +function g_keyboard.isKeyPressed(key) + if type(key) == 'string' then + key = getKeyCode(key) + end + return g_window.isKeyPressed(key) +end + +function g_keyboard.isKeySetPressed(keys, all) + all = all or false + local result = {} + for k,v in pairs(keys) do + if type(v) == 'string' then + v = getKeyCode(v) + end + if g_window.isKeyPressed(v) then + if not all then + return true + end + table.insert(result, true) + end + end + return #result == #keys +end + +function g_keyboard.isInUse() + for i = FirstKey, LastKey do + if g_window.isKeyPressed(key) then + return true + end + end + return false +end + +function g_keyboard.isCtrlPressed() + return bit32.band(g_window.getKeyboardModifiers(), KeyboardCtrlModifier) ~= 0 +end + +function g_keyboard.isAltPressed() + return bit32.band(g_window.getKeyboardModifiers(), KeyboardAltModifier) ~= 0 +end + +function g_keyboard.isShiftPressed() + return bit32.band(g_window.getKeyboardModifiers(), KeyboardShiftModifier) ~= 0 +end diff --git a/modules/corelib/math.lua b/modules/corelib/math.lua new file mode 100644 index 0000000..79c6670 --- /dev/null +++ b/modules/corelib/math.lua @@ -0,0 +1,35 @@ +-- @docclass math + +local U8 = 2^8 +local U16 = 2^16 +local U32 = 2^32 +local U64 = 2^64 + +function math.round(num, idp) + local mult = 10^(idp or 0) + if num >= 0 then + return math.floor(num * mult + 0.5) / mult + else + return math.ceil(num * mult - 0.5) / mult + end +end + +function math.isu8(num) + return math.isinteger(num) and num >= 0 and num < U8 +end + +function math.isu16(num) + return math.isinteger(num) and num >= U8 and num < U16 +end + +function math.isu32(num) + return math.isinteger(num) and num >= U16 and num < U32 +end + +function math.isu64(num) + return math.isinteger(num) and num >= U32 and num < U64 +end + +function math.isinteger(num) + return ((type(num) == 'number') and (num == math.floor(num))) +end diff --git a/modules/corelib/mouse.lua b/modules/corelib/mouse.lua new file mode 100644 index 0000000..d46314e --- /dev/null +++ b/modules/corelib/mouse.lua @@ -0,0 +1,36 @@ +-- @docclass +function g_mouse.bindAutoPress(widget, callback, delay, button) + local button = button or MouseLeftButton + connect(widget, { onMousePress = function(widget, mousePos, mouseButton) + if mouseButton ~= button then + return false + end + local startTime = g_clock.millis() + callback(widget, mousePos, mouseButton, 0) + periodicalEvent(function() + callback(widget, g_window.getMousePosition(), mouseButton, g_clock.millis() - startTime) + end, function() + return g_mouse.isPressed(mouseButton) + end, 30, delay) + return true + end }) +end + +function g_mouse.bindPressMove(widget, callback) + connect(widget, { onMouseMove = function(widget, mousePos, mouseMoved) + if widget:isPressed() then + callback(mousePos, mouseMoved) + return true + end + end }) +end + +function g_mouse.bindPress(widget, callback, button) + connect(widget, { onMousePress = function(widget, mousePos, mouseButton) + if not button or button == mouseButton then + callback(mousePos, mouseButton) + return true + end + return false + end }) +end diff --git a/modules/corelib/net.lua b/modules/corelib/net.lua new file mode 100644 index 0000000..b2d5994 --- /dev/null +++ b/modules/corelib/net.lua @@ -0,0 +1,16 @@ +function translateNetworkError(errcode, connecting, errdesc) + local text + if errcode == 111 then + text = tr('Connection refused, the server might be offline or restarting.\nPlease try again later.') + elseif errcode == 110 then + text = tr('Connection timed out. Either your network is failing or the server is offline.') + elseif errcode == 1 then + text = tr('Connection failed, the server address does not exist.') + elseif connecting then + text = tr('Connection failed.') + else + text = tr('Your connection has been lost.\nEither your network or the server went down.') + end + text = text .. ' ' .. tr('(ERROR %d)', errcode) + return text +end diff --git a/modules/corelib/orderedtable.lua b/modules/corelib/orderedtable.lua new file mode 100644 index 0000000..0edbf3a --- /dev/null +++ b/modules/corelib/orderedtable.lua @@ -0,0 +1,43 @@ +function __genOrderedIndex( t ) + local orderedIndex = {} + for key in pairs(t) do + table.insert( orderedIndex, key ) + end + table.sort( orderedIndex ) + return orderedIndex +end + +function orderedNext(t, state) + -- Equivalent of the next function, but returns the keys in the alphabetic + -- order. We use a temporary ordered key table that is stored in the + -- table being iterated. + + local key = nil + --print("orderedNext: state = "..tostring(state) ) + if state == nil then + -- the first time, generate the index + t.__orderedIndex = __genOrderedIndex( t ) + key = t.__orderedIndex[1] + else + -- fetch the next value + for i = 1,table.getn(t.__orderedIndex) do + if t.__orderedIndex[i] == state then + key = t.__orderedIndex[i+1] + end + end + end + + if key then + return key, t[key] + end + + -- no more value to return, cleanup + t.__orderedIndex = nil + return +end + +function orderedPairs(t) + -- Equivalent of the pairs() function on tables. Allows to iterate + -- in order + return orderedNext, t, nil +end \ No newline at end of file diff --git a/modules/corelib/outputmessage.lua b/modules/corelib/outputmessage.lua new file mode 100644 index 0000000..1e6737e --- /dev/null +++ b/modules/corelib/outputmessage.lua @@ -0,0 +1,69 @@ +function OutputMessage:addData(data) + if type(data) == 'boolean' then + self:addU8(NetworkMessageTypes.Boolean) + self:addU8(booleantonumber(data)) + elseif type(data) == 'number' then + if math.isu8(data) then + self:addU8(NetworkMessageTypes.U8) + self:addU8(data) + elseif math.isu16(data) then + self:addU8(NetworkMessageTypes.U16) + self:addU16(data) + elseif math.isu32(data) then + self:addU8(NetworkMessageTypes.U32) + self:addU32(data) + elseif math.isu64(data) then + self:addU8(NetworkMessageTypes.U64) + self:addU64(data) + else -- negative or non integer numbers + self:addU8(NetworkMessageTypes.NumberString) + self:addString(tostring(data)) + end + elseif type(data) == 'string' then + self:addU8(NetworkMessageTypes.String) + self:addString(data) + elseif type(data) == 'table' then + self:addU8(NetworkMessageTypes.Table) + self:addTable(data) + else + perror('Invalid data type ' .. type(data)) + end +end + +function OutputMessage:addTable(data) + local size = 0 + + -- reserve for size (should be addData, find a way to use it further) + local sizePos = self:getWritePos() + self:addU16(size) + local sizeSize = self:getWritePos() - sizePos + + -- add values + for key,value in pairs(data) do + self:addData(key) + self:addData(value) + size = size + 1 + end + + -- write size + local currentPos = self:getWritePos() + self:setWritePos(sizePos) + self:addU16(size) + + -- fix msg size and go back to end + self:setMessageSize(self:getMessageSize() - sizeSize) + self:setWritePos(currentPos) +end + +function OutputMessage:addColor(color) + self:addU8(color.r) + self:addU8(color.g) + self:addU8(color.b) + self:addU8(color.a) +end + +function OutputMessage:addPosition(position) + self:addU16(position.x) + self:addU16(position.y) + self:addU8(position.z) +end diff --git a/modules/corelib/settings.lua b/modules/corelib/settings.lua new file mode 100644 index 0000000..0eeaae8 --- /dev/null +++ b/modules/corelib/settings.lua @@ -0,0 +1,3 @@ +g_settings = makesingleton(g_configs.getSettings()) + +-- Reserved for future functionality diff --git a/modules/corelib/string.lua b/modules/corelib/string.lua new file mode 100644 index 0000000..e05c0e2 --- /dev/null +++ b/modules/corelib/string.lua @@ -0,0 +1,59 @@ +-- @docclass string + +function string:split(delim) + local start = 1 + local results = {} + while true do + local pos = string.find(self, delim, start, true) + if not pos then + break + end + table.insert(results, string.sub(self, start, pos-1)) + start = pos + string.len(delim) + end + table.insert(results, string.sub(self, start)) + table.removevalue(results, '') + return results +end + +function string:starts(start) + return string.sub(self, 1, #start) == start +end + +function string:ends(test) + return test =='' or string.sub(self,-string.len(test)) == test +end + +function string:trim() + return string.match(self, '^%s*(.*%S)') or '' +end + +function string:explode(sep, limit) + if type(sep) ~= 'string' or tostring(self):len() == 0 or sep:len() == 0 then + return {} + end + + local i, pos, tmp, t = 0, 1, "", {} + for s, e in function() return string.find(self, sep, pos) end do + tmp = self:sub(pos, s - 1):trim() + table.insert(t, tmp) + pos = e + 1 + + i = i + 1 + if limit ~= nil and i == limit then + break + end + end + + tmp = self:sub(pos):trim() + table.insert(t, tmp) + return t +end + +function string:contains(str, checkCase, start, plain) + if(not checkCase) then + self = self:lower() + str = str:lower() + end + return string.find(self, str, start and start or 1, plain == nil and true or false) +end diff --git a/modules/corelib/struct.lua b/modules/corelib/struct.lua new file mode 100644 index 0000000..2ed134d --- /dev/null +++ b/modules/corelib/struct.lua @@ -0,0 +1,173 @@ +Struct = {} + +function Struct.pack(format, ...) + local stream = {} + local vars = {...} + local endianness = true + + for i = 1, format:len() do + local opt = format:sub(i, i) + + if opt == '>' then + endianness = false + elseif opt:find('[bBhHiIlL]') then + local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1 + local val = tonumber(table.remove(vars, 1)) + + if val < 0 then + val = val + 2 ^ (n * 8 - 1) + end + + local bytes = {} + for j = 1, n do + table.insert(bytes, string.char(val % (2 ^ 8))) + val = math.floor(val / (2 ^ 8)) + end + + if not endianness then + table.insert(stream, string.reverse(table.concat(bytes))) + else + table.insert(stream, table.concat(bytes)) + end + elseif opt:find('[fd]') then + local val = tonumber(table.remove(vars, 1)) + local sign = 0 + + if val < 0 then + sign = 1 + val = -val + end + + local mantissa, exponent = math.frexp(val) + if val == 0 then + mantissa = 0 + exponent = 0 + else + mantissa = (mantissa * 2 - 1) * math.ldexp(0.5, (opt == 'd') and 53 or 24) + exponent = exponent + ((opt == 'd') and 1022 or 126) + end + + local bytes = {} + if opt == 'd' then + val = mantissa + for i = 1, 6 do + table.insert(bytes, string.char(math.floor(val) % (2 ^ 8))) + val = math.floor(val / (2 ^ 8)) + end + else + table.insert(bytes, string.char(math.floor(mantissa) % (2 ^ 8))) + val = math.floor(mantissa / (2 ^ 8)) + table.insert(bytes, string.char(math.floor(val) % (2 ^ 8))) + val = math.floor(val / (2 ^ 8)) + end + + table.insert(bytes, string.char(math.floor(exponent * ((opt == 'd') and 16 or 128) + val) % (2 ^ 8))) + val = math.floor((exponent * ((opt == 'd') and 16 or 128) + val) / (2 ^ 8)) + table.insert(bytes, string.char(math.floor(sign * 128 + val) % (2 ^ 8))) + val = math.floor((sign * 128 + val) / (2 ^ 8)) + + if not endianness then + table.insert(stream, string.reverse(table.concat(bytes))) + else + table.insert(stream, table.concat(bytes)) + end + elseif opt == 's' then + table.insert(stream, tostring(table.remove(vars, 1))) + table.insert(stream, string.char(0)) + elseif opt == 'c' then + local n = format:sub(i + 1):match('%d+') + local length = tonumber(n) + + if length > 0 then + local str = tostring(table.remove(vars, 1)) + if length - str:len() > 0 then + str = str .. string.rep(' ', length - str:len()) + end + table.insert(stream, str:sub(1, length)) + end + i = i + n:len() + end + end + + return table.concat(stream) +end + +function Struct.unpack(format, stream) + local vars = {} + local iterator = 1 + local endianness = true + + for i = 1, format:len() do + local opt = format:sub(i, i) + + if opt == '>' then + endianness = false + elseif opt:find('[bBhHiIlL]') then + local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1 + local signed = opt:lower() == opt + + local val = 0 + for j = 1, n do + local byte = string.byte(stream:sub(iterator, iterator)) + if endianness then + val = val + byte * (2 ^ ((j - 1) * 8)) + else + val = val + byte * (2 ^ ((n - j) * 8)) + end + iterator = iterator + 1 + end + + if signed then + val = val - 2 ^ (n * 8 - 1) + end + + table.insert(vars, val) + elseif opt:find('[fd]') then + local n = (opt == 'd') and 8 or 4 + local x = stream:sub(iterator, iterator + n - 1) + iterator = iterator + n + + if not endianness then + x = string.reverse(x) + end + + local sign = 1 + local mantissa = string.byte(x, (opt == 'd') and 7 or 3) % ((opt == 'd') and 16 or 128) + for i = n - 2, 1, -1 do + mantissa = mantissa * (2 ^ 8) + string.byte(x, i) + end + + if string.byte(x, n) > 127 then + sign = -1 + end + + local exponent = (string.byte(x, n) % 128) * ((opt == 'd') and 16 or 2) + math.floor(string.byte(x, n - 1) / ((opt == 'd') and 16 or 128)) + if exponent == 0 then + table.insert(vars, 0.0) + else + mantissa = (math.ldexp(mantissa, (opt == 'd') and -52 or -23) + 1) * sign + table.insert(vars, math.ldexp(mantissa, exponent - ((opt == 'd') and 1023 or 127))) + end + elseif opt == 's' then + local bytes = {} + for j = iterator, stream:len() do + if stream:sub(j, j) == string.char(0) then + break + end + + table.insert(bytes, stream:sub(j, j)) + end + + local str = table.concat(bytes) + iterator = iterator + str:len() + 1 + table.insert(vars, str) + elseif opt == 'c' then + local n = format:sub(i + 1):match('%d+') + table.insert(vars, stream:sub(iterator, iterator + tonumber(n))) + iterator = iterator + tonumber(n) + i = i + n:len() + end + end + + return unpack(vars) +end diff --git a/modules/corelib/table.lua b/modules/corelib/table.lua new file mode 100644 index 0000000..8f9838f --- /dev/null +++ b/modules/corelib/table.lua @@ -0,0 +1,212 @@ +-- @docclass table + +function table.dump(t, depth) + if not depth then depth = 0 end + for k,v in pairs(t) do + str = (' '):rep(depth * 2) .. k .. ': ' + if type(v) ~= "table" then + print(str .. tostring(v)) + else + print(str) + table.dump(v, depth+1) + end + end +end + +function table.clear(t) + for k,v in pairs(t) do + t[k] = nil + end +end + +function table.copy(t) + local res = {} + for k,v in pairs(t) do + res[k] = v + end + return res +end + +function table.recursivecopy(t) + local res = {} + for k,v in pairs(t) do + if type(v) == "table" then + res[k] = table.recursivecopy(v) + else + res[k] = v + end + end + return res +end + +function table.selectivecopy(t, keys) + local res = { } + for i,v in ipairs(keys) do + res[v] = t[v] + end + return res +end + +function table.merge(t, src) + for k,v in pairs(src) do + t[k] = v + end +end + +function table.find(t, value, lowercase) + for k,v in pairs(t) do + if lowercase and type(value) == 'string' and type(v) == 'string' then + if v:lower() == value:lower() then return k end + end + if v == value then return k end + end +end + +function table.findbykey(t, key, lowercase) + for k,v in pairs(t) do + if lowercase and type(key) == 'string' and type(k) == 'string' then + if k:lower() == key:lower() then return v end + end + if k == key then return v end + end +end + +function table.contains(t, value, lowercase) + return table.find(t, value, lowercase) ~= nil +end + +function table.findkey(t, key) + if t and type(t) == 'table' then + for k,v in pairs(t) do + if k == key then return k end + end + end +end + +function table.haskey(t, key) + return table.findkey(t, key) ~= nil +end + +function table.removevalue(t, value) + for k,v in pairs(t) do + if v == value then + table.remove(t, k) + return true + end + end + return false +end + +function table.popvalue(value) + local index = nil + for k,v in pairs(t) do + if v == value or not value then + index = k + end + end + if index then + table.remove(t, index) + return true + end + return false +end + +function table.compare(t, other) + if #t ~= #other then return false end + for k,v in pairs(t) do + if v ~= other[k] then return false end + end + return true +end + +function table.empty(t) + if t and type(t) == 'table' then + return next(t) == nil + end + return true +end + +function table.permute(t, n, count) + n = n or #t + for i=1,count or n do + local j = math.random(i, n) + t[i], t[j] = t[j], t[i] + end + return t +end + +function table.findbyfield(t, fieldname, fieldvalue) + for _i,subt in pairs(t) do + if subt[fieldname] == fieldvalue then + return subt + end + end + return nil +end + +function table.size(t) + local size = 0 + for i, n in pairs(t) do + size = size + 1 + end + + return size +end + +function table.tostring(t) + local maxn = #t + local str = "" + for k,v in pairs(t) do + v = tostring(v) + if k == maxn and k ~= 1 then + str = str .. " and " .. v + elseif maxn > 1 and k ~= 1 then + str = str .. ", " .. v + else + str = str .. " " .. v + end + end + return str +end + +function table.collect(t, func) + local res = {} + for k,v in pairs(t) do + local a,b = func(k,v) + if a and b then + res[a] = b + elseif a ~= nil then + table.insert(res,a) + end + end + return res +end + +function table.equals(t, comp) + if type(t) == "table" and type(comp) == "table" then + for k,v in pairs(t) do + if v ~= comp[k] then return false end + end + end + return true +end + +function table.equal(t1,t2,ignore_mt) + local ty1 = type(t1) + local ty2 = type(t2) + if ty1 ~= ty2 then return false end + -- non-table types can be directly compared + if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end + -- as well as tables which have the metamethod __eq + local mt = getmetatable(t1) + if not ignore_mt and mt and mt.__eq then return t1 == t2 end + for k1,v1 in pairs(t1) do + local v2 = t2[k1] + if v2 == nil or not table.equal(v1,v2) then return false end + end + for k2,v2 in pairs(t2) do + local v1 = t1[k2] + if v1 == nil or not table.equal(v1,v2) then return false end + end + return true +end \ No newline at end of file diff --git a/modules/corelib/ui/effects.lua b/modules/corelib/ui/effects.lua new file mode 100644 index 0000000..16325b5 --- /dev/null +++ b/modules/corelib/ui/effects.lua @@ -0,0 +1,67 @@ +-- @docclass +g_effects = {} + +function g_effects.fadeIn(widget, time, elapsed) + if not elapsed then elapsed = 0 end + if not time then time = 300 end + widget:setOpacity(math.min(elapsed/time, 1)) + removeEvent(widget.fadeEvent) + if elapsed < time then + removeEvent(widget.fadeEvent) + widget.fadeEvent = scheduleEvent(function() + g_effects.fadeIn(widget, time, elapsed + 30) + end, 30) + else + widget.fadeEvent = nil + end +end + +function g_effects.fadeOut(widget, time, elapsed) + if not elapsed then elapsed = 0 end + if not time then time = 300 end + elapsed = math.max((1 - widget:getOpacity()) * time, elapsed) + removeEvent(widget.fadeEvent) + widget:setOpacity(math.max((time - elapsed)/time, 0)) + if elapsed < time then + widget.fadeEvent = scheduleEvent(function() + g_effects.fadeOut(widget, time, elapsed + 30) + end, 30) + else + widget.fadeEvent = nil + end +end + +function g_effects.cancelFade(widget) + removeEvent(widget.fadeEvent) + widget.fadeEvent = nil +end + +function g_effects.startBlink(widget, duration, interval, clickCancel) + duration = duration or 0 -- until stop is called + interval = interval or 500 + clickCancel = clickCancel or true + + removeEvent(widget.blinkEvent) + removeEvent(widget.blinkStopEvent) + + widget.blinkEvent = cycleEvent(function() + widget:setOn(not widget:isOn()) + end, interval) + + if duration > 0 then + widget.blinkStopEvent = scheduleEvent(function() + g_effects.stopBlink(widget) + end, duration) + end + + connect(widget, { onClick = g_effects.stopBlink }) +end + +function g_effects.stopBlink(widget) + disconnect(widget, { onClick = g_effects.stopBlink }) + removeEvent(widget.blinkEvent) + removeEvent(widget.blinkStopEvent) + widget.blinkEvent = nil + widget.blinkStopEvent = nil + widget:setOn(false) +end diff --git a/modules/corelib/ui/tooltip.lua b/modules/corelib/ui/tooltip.lua new file mode 100644 index 0000000..d5c6115 --- /dev/null +++ b/modules/corelib/ui/tooltip.lua @@ -0,0 +1,117 @@ +-- @docclass +g_tooltip = {} + +-- private variables +local toolTipLabel +local currentHoveredWidget + +-- private functions +local function moveToolTip(first) + if not first and (not toolTipLabel:isVisible() or toolTipLabel:getOpacity() < 0.1) then return end + + local pos = g_window.getMousePosition() + local windowSize = g_window.getSize() + local labelSize = toolTipLabel:getSize() + + pos.x = pos.x + 1 + pos.y = pos.y + 1 + + if windowSize.width - (pos.x + labelSize.width) < 10 then + pos.x = pos.x - labelSize.width - 3 + else + pos.x = pos.x + 10 + end + + if windowSize.height - (pos.y + labelSize.height) < 10 then + pos.y = pos.y - labelSize.height - 3 + else + pos.y = pos.y + 10 + end + + toolTipLabel:setPosition(pos) +end + +local function onWidgetHoverChange(widget, hovered) + if hovered then + if widget.tooltip and not g_mouse.isPressed() then + g_tooltip.display(widget.tooltip) + currentHoveredWidget = widget + end + else + if widget == currentHoveredWidget then + g_tooltip.hide() + currentHoveredWidget = nil + end + end +end + +local function onWidgetStyleApply(widget, styleName, styleNode) + if styleNode.tooltip then + widget.tooltip = styleNode.tooltip + end +end + +-- public functions +function g_tooltip.init() + connect(UIWidget, { onStyleApply = onWidgetStyleApply, + onHoverChange = onWidgetHoverChange}) + + addEvent(function() + toolTipLabel = g_ui.createWidget('UILabel', rootWidget) + toolTipLabel:setId('toolTip') + toolTipLabel:setBackgroundColor('#111111cc') + toolTipLabel:setTextAlign(AlignCenter) + toolTipLabel:hide() + toolTipLabel.onMouseMove = function() moveToolTip() end + end) +end + +function g_tooltip.terminate() + disconnect(UIWidget, { onStyleApply = onWidgetStyleApply, + onHoverChange = onWidgetHoverChange }) + + currentHoveredWidget = nil + toolTipLabel:destroy() + toolTipLabel = nil + + g_tooltip = nil +end + +function g_tooltip.display(text) + if text == nil or text:len() == 0 then return end + if not toolTipLabel then return end + + toolTipLabel:setText(text) + toolTipLabel:resizeToText() + toolTipLabel:resize(toolTipLabel:getWidth() + 4, toolTipLabel:getHeight() + 4) + toolTipLabel:show() + toolTipLabel:raise() + toolTipLabel:enable() + g_effects.fadeIn(toolTipLabel, 100) + moveToolTip(true) +end + +function g_tooltip.hide() + g_effects.fadeOut(toolTipLabel, 100) +end + + +-- @docclass UIWidget @{ + +-- UIWidget extensions +function UIWidget:setTooltip(text) + self.tooltip = text +end + +function UIWidget:removeTooltip() + self.tooltip = nil +end + +function UIWidget:getTooltip() + return self.tooltip +end + +-- @} + +g_tooltip.init() +connect(g_app, { onTerminate = g_tooltip.terminate }) diff --git a/modules/corelib/ui/uibutton.lua b/modules/corelib/ui/uibutton.lua new file mode 100644 index 0000000..8a7d125 --- /dev/null +++ b/modules/corelib/ui/uibutton.lua @@ -0,0 +1,12 @@ +-- @docclass +UIButton = extends(UIWidget, "UIButton") + +function UIButton.create() + local button = UIButton.internalCreate() + button:setFocusable(false) + return button +end + +function UIButton:onMouseRelease(pos, button) + return self:isPressed() +end diff --git a/modules/corelib/ui/uicheckbox.lua b/modules/corelib/ui/uicheckbox.lua new file mode 100644 index 0000000..195c593 --- /dev/null +++ b/modules/corelib/ui/uicheckbox.lua @@ -0,0 +1,13 @@ +-- @docclass +UICheckBox = extends(UIWidget, "UICheckBox") + +function UICheckBox.create() + local checkbox = UICheckBox.internalCreate() + checkbox:setFocusable(false) + checkbox:setTextAlign(AlignLeft) + return checkbox +end + +function UICheckBox:onClick() + self:setChecked(not self:isChecked()) +end diff --git a/modules/corelib/ui/uicombobox.lua b/modules/corelib/ui/uicombobox.lua new file mode 100644 index 0000000..0549764 --- /dev/null +++ b/modules/corelib/ui/uicombobox.lua @@ -0,0 +1,180 @@ +-- @docclass +UIComboBox = extends(UIWidget, "UIComboBox") + +function UIComboBox.create() + local combobox = UIComboBox.internalCreate() + combobox:setFocusable(false) + combobox.options = {} + combobox.currentIndex = -1 + combobox.mouseScroll = true + combobox.menuScroll = false + combobox.menuHeight = 100 + combobox.menuScrollStep = 0 + return combobox +end + +function UIComboBox:clearOptions() + self.options = {} + self.currentIndex = -1 + self:clearText() +end + +function UIComboBox:getOptionsCount() + return #self.options +end + +function UIComboBox:isOption(text) + if not self.options then return false end + for i,v in ipairs(self.options) do + if v.text == text then + return true + end + end + return false +end + +function UIComboBox:setOption(text, dontSignal) + self:setCurrentOption(text, dontSignal) +end + +function UIComboBox:setCurrentOption(text, dontSignal) + if not self.options then return end + for i,v in ipairs(self.options) do + if v.text == text and self.currentIndex ~= i then + self.currentIndex = i + self:setText(text) + if not dontSignal then + signalcall(self.onOptionChange, self, text, v.data) + end + return + end + end +end + +function UIComboBox:updateCurrentOption(newText) + self.options[self.currentIndex].text = newText + self:setText(newText) +end + +function UIComboBox:setCurrentOptionByData(data, dontSignal) + if not self.options then return end + for i,v in ipairs(self.options) do + if v.data == data and self.currentIndex ~= i then + self.currentIndex = i + self:setText(v.text) + if not dontSignal then + signalcall(self.onOptionChange, self, v.text, v.data) + end + return + end + end +end + +function UIComboBox:setCurrentIndex(index, dontSignal) + if index >= 1 and index <= #self.options then + local v = self.options[index] + self.currentIndex = index + self:setText(v.text) + if not dontSignal then + signalcall(self.onOptionChange, self, v.text, v.data) + end + end +end + +function UIComboBox:getCurrentOption() + if table.haskey(self.options, self.currentIndex) then + return self.options[self.currentIndex] + end +end + +function UIComboBox:addOption(text, data) + table.insert(self.options, { text = text, data = data }) + local index = #self.options + if index == 1 then self:setCurrentOption(text) end + return index +end + +function UIComboBox:removeOption(text) + for i,v in ipairs(self.options) do + if v.text == text then + table.remove(self.options, i) + if self.currentIndex == i then + self:setCurrentIndex(1) + elseif self.currentIndex > i then + self.currentIndex = self.currentIndex - 1 + end + return + end + end +end + +function UIComboBox:onMousePress(mousePos, mouseButton) + local menu + if self.menuScroll then + menu = g_ui.createWidget(self:getStyleName() .. 'PopupScrollMenu') + menu:setHeight(self.menuHeight) + if self.menuScrollStep > 0 then + menu:setScrollbarStep(self.menuScrollStep) + end + else + menu = g_ui.createWidget(self:getStyleName() .. 'PopupMenu') + end + menu:setId(self:getId() .. 'PopupMenu') + for i,v in ipairs(self.options) do + menu:addOption(v.text, function() self:setCurrentOption(v.text) end) + end + menu:setWidth(self:getWidth()) + menu:display({ x = self:getX(), y = self:getY() + self:getHeight() }) + connect(menu, { onDestroy = function() self:setOn(false) end }) + self:setOn(true) + return true +end + +function UIComboBox:onMouseWheel(mousePos, direction) + if not self.mouseScroll then + return false + end + if direction == MouseWheelUp and self.currentIndex > 1 then + self:setCurrentIndex(self.currentIndex - 1) + elseif direction == MouseWheelDown and self.currentIndex < #self.options then + self:setCurrentIndex(self.currentIndex + 1) + end + return true +end + +function UIComboBox:onStyleApply(styleName, styleNode) + if styleNode.options then + for k,option in pairs(styleNode.options) do + self:addOption(option) + end + end + + if styleNode.data then + for k,data in pairs(styleNode.data) do + local option = self.options[k] + if option then + option.data = data + end + end + end + + for name,value in pairs(styleNode) do + if name == 'mouse-scroll' then + self.mouseScroll = value + elseif name == 'menu-scroll' then + self.menuScroll = value + elseif name == 'menu-height' then + self.menuHeight = value + elseif name == 'menu-scroll-step' then + self.menuScrollStep = value + end + end +end + +function UIComboBox:setMouseScroll(scroll) + self.mouseScroll = scroll +end + +function UIComboBox:canMouseScroll() + return self.mouseScroll +end diff --git a/modules/corelib/ui/uiimageview.lua b/modules/corelib/ui/uiimageview.lua new file mode 100644 index 0000000..7a2e5fe --- /dev/null +++ b/modules/corelib/ui/uiimageview.lua @@ -0,0 +1,99 @@ +-- @docclass +UIImageView = extends(UIWidget, "UIImageView") + +function UIImageView.create() + local imageView = UIImageView.internalCreate() + imageView.zoom = 1 + imageView.minZoom = math.pow(10, -2) + imageView.maxZoom = math.pow(10, 2) + imageView:setClipping(true) + return imageView +end + +function UIImageView:getDefaultZoom() + local width = self:getWidth() + local height = self:getHeight() + local textureWidth = self:getImageTextureWidth() + local textureHeight = self:getImageTextureHeight() + local zoomX = width / textureWidth + local zoomY = height / textureHeight + return math.min(zoomX, zoomY) +end + +function UIImageView:getImagePosition(x, y) + x = x or self:getWidth() / 2 + y = y or self:getHeight() / 2 + local offsetX = self:getImageOffsetX() + local offsetY = self:getImageOffsetY() + local posX = (x - offsetX) / self.zoom + local posY = (y - offsetY) / self.zoom + return posX, posY +end + +function UIImageView:setImage(image) + self:setImageSource(image) + local zoom = self:getDefaultZoom() + self:setZoom(zoom) + self:center() +end + +function UIImageView:setZoom(zoom, x, y) + zoom = math.max(math.min(zoom, self.maxZoom), self.minZoom) + local posX, posY = self:getImagePosition(x, y) + local textureWidth = self:getImageTextureWidth() + local textureHeight = self:getImageTextureHeight() + local imageWidth = textureWidth * zoom + local imageHeight = textureHeight * zoom + self:setImageWidth(imageWidth) + self:setImageHeight(imageHeight) + self.zoom = zoom + self:move(posX, posY, x, y) +end + +function UIImageView:zoomIn(x, y) + local zoom = self.zoom * 1.1 + self:setZoom(zoom, x, y) +end + +function UIImageView:zoomOut(x, y) + local zoom = self.zoom / 1.1 + self:setZoom(zoom, x, y) +end + +function UIImageView:center() + self:move(self:getImageTextureWidth() / 2, self:getImageTextureHeight() / 2) +end + +function UIImageView:move(x, y, centerX, centerY) + x = math.max(math.min(x, self:getImageTextureWidth()), 0) + y = math.max(math.min(y, self:getImageTextureHeight()), 0) + local centerX = centerX or self:getWidth() / 2 + local centerY = centerY or self:getHeight() / 2 + local offsetX = centerX - x * self.zoom + local offsetY = centerY - y * self.zoom + self:setImageOffset({x=offsetX, y=offsetY}) +end + +function UIImageView:onDragEnter(pos) + return true +end + +function UIImageView:onDragMove(pos, moved) + local posX, posY = self:getImagePosition() + self:move(posX - moved.x / self.zoom, posY - moved.y / self.zoom) + return true +end + +function UIImageView:onDragLeave(widget, pos) + return true +end + +function UIImageView:onMouseWheel(mousePos, direction) + local x = mousePos.x - self:getX() + local y = mousePos.y - self:getY() + if direction == MouseWheelUp then + self:zoomIn(x, y) + elseif direction == MouseWheelDown then + self:zoomOut(x, y) + end +end diff --git a/modules/corelib/ui/uiinputbox.lua b/modules/corelib/ui/uiinputbox.lua new file mode 100644 index 0000000..1db361e --- /dev/null +++ b/modules/corelib/ui/uiinputbox.lua @@ -0,0 +1,114 @@ +if not UIWindow then dofile 'uiwindow' end + +-- @docclass +UIInputBox = extends(UIWindow, "UIInputBox") + +function UIInputBox.create(title, okCallback, cancelCallback) + local inputBox = UIInputBox.internalCreate() + + inputBox:setText(title) + inputBox.inputs = {} + inputBox.onEnter = function() + local results = {} + for _,func in pairs(inputBox.inputs) do + table.insert(results, func()) + end + okCallback(unpack(results)) + inputBox:destroy() + end + inputBox.onEscape = function() + if cancelCallback then + cancelCallback() + end + inputBox:destroy() + end + + return inputBox +end + +function UIInputBox:addLabel(text) + local label = g_ui.createWidget('InputBoxLabel', self) + label:setText(text) + return label +end + +function UIInputBox:addLineEdit(labelText, defaultText, maxLength) + if labelText then self:addLabel(labelText) end + local lineEdit = g_ui.createWidget('InputBoxLineEdit', self) + if defaultText then lineEdit:setText(defaultText) end + if maxLength then lineEdit:setMaxLength(maxLength) end + table.insert(self.inputs, function() return lineEdit:getText() end) + return lineEdit +end + +function UIInputBox:addTextEdit(labelText, defaultText, maxLength, visibleLines) + if labelText then self:addLabel(labelText) end + local textEdit = g_ui.createWidget('InputBoxTextEdit', self) + if defaultText then textEdit:setText(defaultText) end + if maxLength then textEdit:setMaxLength(maxLength) end + visibleLines = visibleLines or 1 + textEdit:setHeight(textEdit:getHeight() * visibleLines) + table.insert(self.inputs, function() return textEdit:getText() end) + return textEdit +end + +function UIInputBox:addCheckBox(text, checked) + local checkBox = g_ui.createWidget('InputBoxCheckBox', self) + checkBox:setText(text) + checkBox:setChecked(checked) + table.insert(self.inputs, function() return checkBox:isChecked() end) + return checkBox +end + +function UIInputBox:addComboBox(labelText, ...) + if labelText then self:addLabel(labelText) end + local comboBox = g_ui.createWidget('InputBoxComboBox', self) + local options = {...} + for i=1,#options do + comboBox:addOption(options[i]) + end + table.insert(self.inputs, function() return comboBox:getCurrentOption() end) + return comboBox +end + +function UIInputBox:addSpinBox(labelText, minimum, maximum, value, step) + if labelText then self:addLabel(labelText) end + local spinBox = g_ui.createWidget('InputBoxSpinBox', self) + spinBox:setMinimum(minimum) + spinBox:setMaximum(maximum) + spinBox:setValue(value) + spinBox:setStep(step) + table.insert(self.inputs, function() return spinBox:getValue() end) + return spinBox +end + +function UIInputBox:display(okButtonText, cancelButtonText) + okButtonText = okButtonText or tr('Ok') + cancelButtonText = cancelButtonText or tr('Cancel') + + local buttonsWidget = g_ui.createWidget('InputBoxButtonsPanel', self) + local okButton = g_ui.createWidget('InputBoxButton', buttonsWidget) + okButton:setText(okButtonText) + okButton.onClick = self.onEnter + + local cancelButton = g_ui.createWidget('InputBoxButton', buttonsWidget) + cancelButton:setText(cancelButtonText) + cancelButton.onClick = self.onEscape + + buttonsWidget:setHeight(okButton:getHeight()) + + rootWidget:addChild(self) + self:setStyle('InputBoxWindow') +end + +function displayTextInputBox(title, label, okCallback, cancelCallback) + local inputBox = UIInputBox.create(title, okCallback, cancelCallback) + inputBox:addLineEdit(label) + inputBox:display() +end + +function displayNumberInputBox(title, label, okCallback, cancelCallback, min, max, value, step) + local inputBox = UIInputBox.create(title, okCallback, cancelCallback) + inputBox:addSpinBox(label, min, max, value, step) + inputBox:display() +end diff --git a/modules/corelib/ui/uilabel.lua b/modules/corelib/ui/uilabel.lua new file mode 100644 index 0000000..ecd72bc --- /dev/null +++ b/modules/corelib/ui/uilabel.lua @@ -0,0 +1,10 @@ +-- @docclass +UILabel = extends(UIWidget, "UILabel") + +function UILabel.create() + local label = UILabel.internalCreate() + label:setPhantom(true) + label:setFocusable(false) + label:setTextAlign(AlignLeft) + return label +end diff --git a/modules/corelib/ui/uimessagebox.lua b/modules/corelib/ui/uimessagebox.lua new file mode 100644 index 0000000..62f84a2 --- /dev/null +++ b/modules/corelib/ui/uimessagebox.lua @@ -0,0 +1,96 @@ +if not UIWindow then dofile 'uiwindow' end + +-- @docclass +UIMessageBox = extends(UIWindow, "UIMessageBox") + +-- messagebox cannot be created from otui files +UIMessageBox.create = nil + +function UIMessageBox.display(title, message, buttons, onEnterCallback, onEscapeCallback) + local messageBox = UIMessageBox.internalCreate() + rootWidget:addChild(messageBox) + + messageBox:setStyle('MainWindow') + messageBox:setText(title) + + local messageLabel = g_ui.createWidget('MessageBoxLabel', messageBox) + messageLabel:setText(message) + + local buttonsWidth = 0 + local buttonsHeight = 0 + + local anchor = AnchorRight + if buttons.anchor then anchor = buttons.anchor end + + local buttonHolder = g_ui.createWidget('MessageBoxButtonHolder', messageBox) + buttonHolder:addAnchor(anchor, 'parent', anchor) + + for i=1,#buttons do + local button = messageBox:addButton(buttons[i].text, buttons[i].callback) + if i == 1 then + button:setMarginLeft(0) + button:addAnchor(AnchorBottom, 'parent', AnchorBottom) + button:addAnchor(AnchorLeft, 'parent', AnchorLeft) + buttonsHeight = button:getHeight() + else + button:addAnchor(AnchorBottom, 'prev', AnchorBottom) + button:addAnchor(AnchorLeft, 'prev', AnchorRight) + end + buttonsWidth = buttonsWidth + button:getWidth() + button:getMarginLeft() + end + + buttonHolder:setWidth(buttonsWidth) + buttonHolder:setHeight(buttonsHeight) + + if onEnterCallback then connect(messageBox, { onEnter = onEnterCallback }) end + if onEscapeCallback then connect(messageBox, { onEscape = onEscapeCallback }) end + + messageBox:setWidth(math.max(messageLabel:getWidth(), messageBox:getTextSize().width, buttonHolder:getWidth()) + messageBox:getPaddingLeft() + messageBox:getPaddingRight()) + messageBox:setHeight(messageLabel:getHeight() + messageBox:getPaddingTop() + messageBox:getPaddingBottom() + buttonHolder:getHeight() + buttonHolder:getMarginTop()) + return messageBox +end + +function displayInfoBox(title, message) + local messageBox + local defaultCallback = function() messageBox:ok() end + messageBox = UIMessageBox.display(title, message, {{text='Ok', callback=defaultCallback}}, defaultCallback, defaultCallback) + return messageBox +end + +function displayErrorBox(title, message) + local messageBox + local defaultCallback = function() messageBox:ok() end + messageBox = UIMessageBox.display(title, message, {{text='Ok', callback=defaultCallback}}, defaultCallback, defaultCallback) + return messageBox +end + +function displayCancelBox(title, message) + local messageBox + local defaultCallback = function() messageBox:cancel() end + messageBox = UIMessageBox.display(title, message, {{text='Cancel', callback=defaultCallback}}, defaultCallback, defaultCallback) + return messageBox +end + +function displayGeneralBox(title, message, buttons, onEnterCallback, onEscapeCallback) + return UIMessageBox.display(title, message, buttons, onEnterCallback, onEscapeCallback) +end + +function UIMessageBox:addButton(text, callback) + local buttonHolder = self:getChildById('buttonHolder') + local button = g_ui.createWidget('MessageBoxButton', buttonHolder) + button:setText(text) + connect(button, { onClick = callback }) + return button +end + +function UIMessageBox:ok() + signalcall(self.onOk, self) + self.onOk = nil + self:destroy() +end + +function UIMessageBox:cancel() + signalcall(self.onCancel, self) + self.onCancel = nil + self:destroy() +end diff --git a/modules/corelib/ui/uiminiwindow.lua b/modules/corelib/ui/uiminiwindow.lua new file mode 100644 index 0000000..4ab6e6e --- /dev/null +++ b/modules/corelib/ui/uiminiwindow.lua @@ -0,0 +1,433 @@ +-- @docclass +UIMiniWindow = extends(UIWindow, "UIMiniWindow") + +function UIMiniWindow.create() + local miniwindow = UIMiniWindow.internalCreate() + miniwindow.UIMiniWindowContainer = true + return miniwindow +end + +function UIMiniWindow:open(dontSave) + self:setVisible(true) + + if not dontSave then + self:setSettings({closed = false}) + end + + signalcall(self.onOpen, self) +end + +function UIMiniWindow:close(dontSave) + if not self:isExplicitlyVisible() then return end + if self.forceOpen then return end + self:setVisible(false) + + if not dontSave then + self:setSettings({closed = true}) + end + + signalcall(self.onClose, self) +end + +function UIMiniWindow:minimize(dontSave) + self:setOn(true) + self:getChildById('contentsPanel'):hide() + self:getChildById('miniwindowScrollBar'):hide() + self:getChildById('bottomResizeBorder'):hide() + self:getChildById('minimizeButton'):setOn(true) + self.maximizedHeight = self:getHeight() + self:setHeight(self.minimizedHeight) + + if not dontSave then + self:setSettings({minimized = true}) + end + + signalcall(self.onMinimize, self) +end + +function UIMiniWindow:maximize(dontSave) + self:setOn(false) + self:getChildById('contentsPanel'):show() + self:getChildById('miniwindowScrollBar'):show() + self:getChildById('bottomResizeBorder'):show() + self:getChildById('minimizeButton'):setOn(false) + self:setHeight(self:getSettings('height') or self.maximizedHeight) + + if not dontSave then + self:setSettings({minimized = false}) + end + + local parent = self:getParent() + if parent and parent:getClassName() == 'UIMiniWindowContainer' then + parent:fitAll(self) + end + + signalcall(self.onMaximize, self) +end + +function UIMiniWindow:lock(dontSave) + local lockButton = self:getChildById('lockButton') + if lockButton then + lockButton:setOn(true) + end + self:setDraggable(false) + if not dontsave then + self:setSettings({locked = true}) + end + + signalcall(self.onLockChange, self) +end + +function UIMiniWindow:unlock(dontSave) + local lockButton = self:getChildById('lockButton') + if lockButton then + lockButton:setOn(false) + end + self:setDraggable(true) + if not dontsave then + self:setSettings({locked = false}) + end + signalcall(self.onLockChange, self) +end + +function UIMiniWindow:setup() + self:getChildById('closeButton').onClick = + function() + self:close() + end + if self.forceOpen then + self:getChildById('closeButton'):hide() + self:getChildById('minimizeButton'):addAnchor(AnchorRight, 'parent', AnchorRight) + end + + self:getChildById('minimizeButton').onClick = + function() + if self:isOn() then + self:maximize() + else + self:minimize() + end + end + + local lockButton = self:getChildById('lockButton') + if lockButton then + lockButton.onClick = + function () + if self:isDraggable() then + self:lock() + else + self:unlock() + end + end + end + + self:getChildById('miniwindowTopBar').onDoubleClick = + function() + if self:isOn() then + self:maximize() + else + self:minimize() + end + end + + local oldParent = self:getParent() + + local settings = g_settings.getNode('MiniWindows') + if settings then + local selfSettings = settings[self:getId()] + if selfSettings then + if selfSettings.parentId then + local parent = rootWidget:recursiveGetChildById(selfSettings.parentId) + if parent then + if parent:getClassName() == 'UIMiniWindowContainer' and selfSettings.index and parent:isOn() then + self.miniIndex = selfSettings.index + parent:scheduleInsert(self, selfSettings.index) + elseif selfSettings.position then + self:setParent(parent, true) + self:setPosition(topoint(selfSettings.position)) + end + end + end + + if selfSettings.minimized then + self:minimize(true) + else + if selfSettings.height and self:isResizeable() then + self:setHeight(selfSettings.height) + elseif selfSettings.height and not self:isResizeable() then + self:eraseSettings({height = true}) + end + end + + if selfSettings.closed and not self.forceOpen then + self:close(true) + end + + if selfSettings.locked then + self:lock(true) + end + end + end + + local newParent = self:getParent() + + self.miniLoaded = true + + if self.save then + if oldParent and oldParent:getClassName() == 'UIMiniWindowContainer' then + addEvent(function() oldParent:order() end) + end + if newParent and newParent:getClassName() == 'UIMiniWindowContainer' and newParent ~= oldParent then + addEvent(function() newParent:order() end) + end + end + + self:fitOnParent() +end + +function UIMiniWindow:onVisibilityChange(visible) + self:fitOnParent() +end + +function UIMiniWindow:onDragEnter(mousePos) + local parent = self:getParent() + if not parent then return false end + + if parent:getClassName() == 'UIMiniWindowContainer' then + local containerParent = parent:getParent():getParent() + parent:removeChild(self) + containerParent:addChild(self) + parent:saveChildren() + end + + local oldPos = self:getPosition() + self.movingReference = { x = mousePos.x - oldPos.x, y = mousePos.y - oldPos.y } + self:setPosition(oldPos) + self.free = true + return true +end + +function UIMiniWindow:onDragLeave(droppedWidget, mousePos) + if self.movedWidget then + self.setMovedChildMargin(self.movedOldMargin or 0) + self.movedWidget = nil + self.setMovedChildMargin = nil + self.movedOldMargin = nil + self.movedIndex = nil + end + + UIWindow:onDragLeave(self, droppedWidget, mousePos) + self:saveParent(self:getParent()) +end + +function UIMiniWindow:onDragMove(mousePos, mouseMoved) + local oldMousePosY = mousePos.y - mouseMoved.y + local children = rootWidget:recursiveGetChildrenByMarginPos(mousePos) + local overAnyWidget = false + for i=1,#children do + local child = children[i] + if child:getParent():getClassName() == 'UIMiniWindowContainer' then + overAnyWidget = true + + local childCenterY = child:getY() + child:getHeight() / 2 + if child == self.movedWidget and mousePos.y < childCenterY and oldMousePosY < childCenterY then + break + end + + if self.movedWidget then + self.setMovedChildMargin(self.movedOldMargin or 0) + self.setMovedChildMargin = nil + end + + if mousePos.y < childCenterY then + self.movedOldMargin = child:getMarginTop() + self.setMovedChildMargin = function(v) child:setMarginTop(v) end + self.movedIndex = 0 + else + self.movedOldMargin = child:getMarginBottom() + self.setMovedChildMargin = function(v) child:setMarginBottom(v) end + self.movedIndex = 1 + end + + self.movedWidget = child + self.setMovedChildMargin(self:getHeight()) + break + end + end + + if not overAnyWidget and self.movedWidget then + self.setMovedChildMargin(self.movedOldMargin or 0) + self.movedWidget = nil + end + + return UIWindow.onDragMove(self, mousePos, mouseMoved) +end + +function UIMiniWindow:onMousePress() + local parent = self:getParent() + if not parent then return false end + if parent:getClassName() ~= 'UIMiniWindowContainer' then + self:raise() + return true + end +end + +function UIMiniWindow:onFocusChange(focused) + if not focused then return end + local parent = self:getParent() + if parent and parent:getClassName() ~= 'UIMiniWindowContainer' then + self:raise() + end +end + +function UIMiniWindow:onHeightChange(height) + if not self:isOn() then + self:setSettings({height = height}) + end + self:fitOnParent() +end + +function UIMiniWindow:getSettings(name) + if not self.save then return nil end + local settings = g_settings.getNode('MiniWindows') + if settings then + local selfSettings = settings[self:getId()] + if selfSettings then + return selfSettings[name] + end + end + return nil +end + +function UIMiniWindow:setSettings(data) + if not self.save then return end + + local settings = g_settings.getNode('MiniWindows') + if not settings then + settings = {} + end + + local id = self:getId() + if not settings[id] then + settings[id] = {} + end + + for key,value in pairs(data) do + settings[id][key] = value + end + + g_settings.setNode('MiniWindows', settings) +end + +function UIMiniWindow:eraseSettings(data) + if not self.save then return end + + local settings = g_settings.getNode('MiniWindows') + if not settings then + settings = {} + end + + local id = self:getId() + if not settings[id] then + settings[id] = {} + end + + for key,value in pairs(data) do + settings[id][key] = nil + end + + g_settings.setNode('MiniWindows', settings) +end + +function UIMiniWindow:saveParent(parent) + local parent = self:getParent() + if parent then + if parent:getClassName() == 'UIMiniWindowContainer' then + parent:saveChildren() + else + self:saveParentPosition(parent:getId(), self:getPosition()) + end + end +end + +function UIMiniWindow:saveParentPosition(parentId, position) + local selfSettings = {} + selfSettings.parentId = parentId + selfSettings.position = pointtostring(position) + self:setSettings(selfSettings) +end + +function UIMiniWindow:saveParentIndex(parentId, index) + local selfSettings = {} + selfSettings.parentId = parentId + selfSettings.index = index + self:setSettings(selfSettings) + self.miniIndex = index +end + +function UIMiniWindow:disableResize() + self:getChildById('bottomResizeBorder'):disable() +end + +function UIMiniWindow:enableResize() + self:getChildById('bottomResizeBorder'):enable() +end + +function UIMiniWindow:fitOnParent() + local parent = self:getParent() + if self:isVisible() and parent and parent:getClassName() == 'UIMiniWindowContainer' then + parent:fitAll(self) + end +end + +function UIMiniWindow:setParent(parent, dontsave) + UIWidget.setParent(self, parent) + if not dontsave then + self:saveParent(parent) + end + self:fitOnParent() +end + +function UIMiniWindow:setHeight(height) + UIWidget.setHeight(self, height) + signalcall(self.onHeightChange, self, height) +end + +function UIMiniWindow:setContentHeight(height) + local contentsPanel = self:getChildById('contentsPanel') + local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + contentsPanel:getPaddingBottom() + + local resizeBorder = self:getChildById('bottomResizeBorder') + resizeBorder:setParentSize(minHeight + height) +end + +function UIMiniWindow:setContentMinimumHeight(height) + local contentsPanel = self:getChildById('contentsPanel') + local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + contentsPanel:getPaddingBottom() + + local resizeBorder = self:getChildById('bottomResizeBorder') + resizeBorder:setMinimum(minHeight + height) +end + +function UIMiniWindow:setContentMaximumHeight(height) + local contentsPanel = self:getChildById('contentsPanel') + local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + contentsPanel:getPaddingBottom() + + local resizeBorder = self:getChildById('bottomResizeBorder') + resizeBorder:setMaximum(minHeight + height) +end + +function UIMiniWindow:getMinimumHeight() + local resizeBorder = self:getChildById('bottomResizeBorder') + return resizeBorder:getMinimum() +end + +function UIMiniWindow:getMaximumHeight() + local resizeBorder = self:getChildById('bottomResizeBorder') + return resizeBorder:getMaximum() +end + +function UIMiniWindow:isResizeable() + local resizeBorder = self:getChildById('bottomResizeBorder') + return resizeBorder:isExplicitlyVisible() and resizeBorder:isEnabled() +end diff --git a/modules/corelib/ui/uiminiwindowcontainer.lua b/modules/corelib/ui/uiminiwindowcontainer.lua new file mode 100644 index 0000000..057ec57 --- /dev/null +++ b/modules/corelib/ui/uiminiwindowcontainer.lua @@ -0,0 +1,211 @@ +-- @docclass +UIMiniWindowContainer = extends(UIWidget, "UIMiniWindowContainer") + +function UIMiniWindowContainer.create() + local container = UIMiniWindowContainer.internalCreate() + container.scheduledWidgets = {} + container:setFocusable(false) + container:setPhantom(true) + return container +end + +-- TODO: connect to window onResize event +-- TODO: try to resize another widget? +-- TODO: try to find another panel? +function UIMiniWindowContainer:fitAll(noRemoveChild) + if not self:isVisible() then + return + end + + if not noRemoveChild then + local children = self:getChildren() + if #children > 0 then + noRemoveChild = children[#children] + else + return + end + end + + local sumHeight = 0 + local children = self:getChildren() + for i=1,#children do + if children[i]:isVisible() then + sumHeight = sumHeight + children[i]:getHeight() + end + end + + local selfHeight = self:getHeight() - (self:getPaddingTop() + self:getPaddingBottom()) + if sumHeight <= selfHeight then + return + end + + local removeChildren = {} + + -- try to resize noRemoveChild + local maximumHeight = selfHeight - (sumHeight - noRemoveChild:getHeight()) + if noRemoveChild:isResizeable() and noRemoveChild:getMinimumHeight() <= maximumHeight then + sumHeight = sumHeight - noRemoveChild:getHeight() + maximumHeight + addEvent(function() noRemoveChild:setHeight(maximumHeight) end) + end + + -- try to remove no-save widget + for i=#children,1,-1 do + if sumHeight <= selfHeight then + break + end + + local child = children[i] + if child ~= noRemoveChild and not child.save then + local childHeight = child:getHeight() + sumHeight = sumHeight - childHeight + table.insert(removeChildren, child) + end + end + + -- try to remove save widget, not forceOpen + for i=#children,1,-1 do + if sumHeight <= selfHeight then + break + end + + local child = children[i] + if child ~= noRemoveChild and child:isVisible() and not child.forceOpen then + local childHeight = child:getHeight() + sumHeight = sumHeight - childHeight + table.insert(removeChildren, child) + end + end + + -- try to remove save widget + for i=#children,1,-1 do + if sumHeight <= selfHeight then + break + end + + local child = children[i] + if child ~= noRemoveChild and child:isVisible() then + local childHeight = child:getHeight() - 50 + sumHeight = sumHeight - childHeight + table.insert(removeChildren, child) + end + end + + -- close widgets + for i=1,#removeChildren do + if removeChildren[i].forceOpen then + removeChildren[i]:minimize(true) + else + removeChildren[i]:close() + end + end +end + +function UIMiniWindowContainer:onDrop(widget, mousePos) + if widget.UIMiniWindowContainer then + local oldParent = widget:getParent() + if oldParent == self then + return true + end + + if oldParent then + oldParent:removeChild(widget) + end + + if widget.movedWidget then + local index = self:getChildIndex(widget.movedWidget) + self:insertChild(index + widget.movedIndex, widget) + else + self:addChild(widget) + end + + self:fitAll(widget) + return true + end +end + +function UIMiniWindowContainer:moveTo(newPanel) + if not newPanel or newPanel == self then + return + end + local children = self:getChildByIndex(1) + while children do + newPanel:addChild(children) + children = self:getChildByIndex(1) + end + newPanel:fitAll() +end + +function UIMiniWindowContainer:swapInsert(widget, index) + local oldParent = widget:getParent() + local oldIndex = self:getChildIndex(widget) + + if oldParent == self and oldIndex ~= index then + local oldWidget = self:getChildByIndex(index) + if oldWidget then + self:removeChild(oldWidget) + self:insertChild(oldIndex, oldWidget) + end + self:removeChild(widget) + self:insertChild(index, widget) + end +end + +function UIMiniWindowContainer:scheduleInsert(widget, index) + if index - 1 > self:getChildCount() then + if self.scheduledWidgets[index] then + pdebug('replacing scheduled widget id ' .. widget:getId()) + end + self.scheduledWidgets[index] = widget + else + local oldParent = widget:getParent() + if oldParent ~= self then + if oldParent then + oldParent:removeChild(widget) + end + self:insertChild(index, widget) + + while true do + local placed = false + for nIndex,nWidget in pairs(self.scheduledWidgets) do + if nIndex - 1 <= self:getChildCount() then + self:insertChild(nIndex, nWidget) + self.scheduledWidgets[nIndex] = nil + placed = true + break + end + end + if not placed then break end + end + + end + end +end + +function UIMiniWindowContainer:order() + local children = self:getChildren() + for i=1,#children do + if not children[i].miniLoaded then return end + end + + for i=1,#children do + if children[i].miniIndex then + self:swapInsert(children[i], children[i].miniIndex) + end + end +end + +function UIMiniWindowContainer:saveChildren() + local children = self:getChildren() + local ignoreIndex = 0 + for i=1,#children do + if children[i].save then + children[i]:saveParentIndex(self:getId(), i - ignoreIndex) + else + ignoreIndex = ignoreIndex + 1 + end + end +end + +function UIMiniWindowContainer:onGeometryChange() + self:fitAll() +end \ No newline at end of file diff --git a/modules/corelib/ui/uimovabletabbar.lua b/modules/corelib/ui/uimovabletabbar.lua new file mode 100644 index 0000000..d4991cc --- /dev/null +++ b/modules/corelib/ui/uimovabletabbar.lua @@ -0,0 +1,501 @@ +-- @docclass +UIMoveableTabBar = extends(UIWidget, "UIMoveableTabBar") + +-- private functions +local function onTabClick(tab) + tab.tabBar:selectTab(tab) +end + +local function updateMargins(tabBar) + if #tabBar.tabs == 0 then return end + + local currentMargin = 0 + for i = 1, #tabBar.tabs do + tabBar.tabs[i]:setMarginLeft(currentMargin) + currentMargin = currentMargin + tabBar.tabSpacing + tabBar.tabs[i]:getWidth() + end +end + +local function updateNavigation(tabBar) + if tabBar.prevNavigation then + if #tabBar.preTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= 1 then + tabBar.prevNavigation:enable() + else + tabBar.prevNavigation:disable() + end + end + + if tabBar.nextNavigation then + if #tabBar.postTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= #tabBar.tabs then + tabBar.nextNavigation:enable() + else + tabBar.nextNavigation:disable() + end + end +end + +local function updateIndexes(tabBar, tab, xoff) + local tabs = tabBar.tabs + local currentMargin = 0 + local prevIndex = table.find(tabs, tab) + local newIndex = prevIndex + local xmid = xoff + tab:getWidth()/2 + for i = 1, #tabs do + local nextTab = tabs[i] + if xmid >= currentMargin + nextTab:getWidth()/2 then + newIndex = table.find(tabs, nextTab) + end + currentMargin = currentMargin + tabBar.tabSpacing * (i - 1) + tabBar.tabs[i]:getWidth() + end + if newIndex ~= prevIndex then + table.remove(tabs, table.find(tabs, tab)) + table.insert(tabs, newIndex, tab) + end + updateNavigation(tabBar) +end + +local function getMaxMargin(tabBar, tab) + if #tabBar.tabs == 0 then return 0 end + + local maxMargin = 0 + for i = 1, #tabBar.tabs do + if tabBar.tabs[i] ~= tab then + maxMargin = maxMargin + tabBar.tabs[i]:getWidth() + end + end + return maxMargin + tabBar.tabSpacing * (#tabBar.tabs - 1) +end + +local function updateTabs(tabBar) + if #tabBar.postTabs > 0 then + local i = 1 + while i <= #tabBar.postTabs do + local tab = tabBar.postTabs[i] + if getMaxMargin(tabBar) + tab:getWidth() > tabBar:getWidth() then + break + end + + table.remove(tabBar.postTabs, i) + table.insert(tabBar.tabs, tab) + tab:setVisible(true) + end + end + if #tabBar.preTabs > 0 then + for i = #tabBar.preTabs, 1, -1 do + local tab = tabBar.preTabs[i] + if getMaxMargin(tabBar) + tab:getWidth() > tabBar:getWidth() then + break + end + + table.remove(tabBar.preTabs, i) + table.insert(tabBar.tabs, 1, tab) + tab:setVisible(true) + end + end + updateNavigation(tabBar) + updateMargins(tabBar) + + if not tabBar.currentTab and #tabBar.tabs > 0 then + tabBar:selectTab(tabBar.tabs[1]) + end +end + +local function hideTabs(tabBar, fromBack, toArray, width) + while #tabBar.tabs > 0 and getMaxMargin(tabBar) + width > tabBar:getWidth() do + local index = fromBack and #tabBar.tabs or 1 + local tab = tabBar.tabs[index] + table.remove(tabBar.tabs, index) + if fromBack then + table.insert(toArray, 1, tab) + else + table.insert(toArray, tab) + end + if tabBar.currentTab == tab then + if #tabBar.tabs > 0 then + tabBar:selectTab(tabBar.tabs[#tabBar.tabs]) + else + tabBar.currentTab:setChecked(false) + tabBar.currentTab = nil + end + end + tab:setVisible(false) + end +end + +local function showPreTab(tabBar) + if #tabBar.preTabs == 0 then + return nil + end + + local tmpTab = tabBar.preTabs[#tabBar.preTabs] + hideTabs(tabBar, true, tabBar.postTabs, tmpTab:getWidth()) + + table.remove(tabBar.preTabs, #tabBar.preTabs) + table.insert(tabBar.tabs, 1, tmpTab) + tmpTab:setVisible(true) + return tmpTab +end + +local function showPostTab(tabBar) + if #tabBar.postTabs == 0 then + return nil + end + + local tmpTab = tabBar.postTabs[1] + hideTabs(tabBar, false, tabBar.preTabs, tmpTab:getWidth()) + + table.remove(tabBar.postTabs, 1) + table.insert(tabBar.tabs, tmpTab) + tmpTab:setVisible(true) + return tmpTab +end + +local function onTabMousePress(tab, mousePos, mouseButton) + if mouseButton == MouseRightButton then + if tab.menuCallback then tab.menuCallback(tab, mousePos, mouseButton) end + return true + end +end + +local function onTabDragEnter(tab, mousePos) + tab:raise() + tab.hotSpot = mousePos.x - tab:getMarginLeft() + tab.tabBar.selected = tab + return true +end + +local function onTabDragLeave(tab) + updateMargins(tab.tabBar) + tab.tabBar.selected = nil + return true +end + +local function onTabDragMove(tab, mousePos, mouseMoved) + if tab == tab.tabBar.selected then + local xoff = mousePos.x - tab.hotSpot + + -- update indexes + updateIndexes(tab.tabBar, tab, xoff) + updateIndexes(tab.tabBar, tab, xoff) + + -- update margins + updateMargins(tab.tabBar) + xoff = math.max(xoff, 0) + xoff = math.min(xoff, getMaxMargin(tab.tabBar, tab)) + tab:setMarginLeft(xoff) + end +end + +local function tabBlink(tab, step) + local step = step or 0 + tab:setOn(not tab:isOn()) + + removeEvent(tab.blinkEvent) + if step < 4 then + tab.blinkEvent = scheduleEvent(function() tabBlink(tab, step+1) end, 500) + else + tab:setOn(true) + tab.blinkEvent = nil + end +end + +-- public functions +function UIMoveableTabBar.create() + local tabbar = UIMoveableTabBar.internalCreate() + tabbar:setFocusable(false) + tabbar.tabs = {} + tabbar.selected = nil -- dragged tab + tabbar.tabSpacing = 0 + tabbar.tabsMoveable = false + tabbar.preTabs = {} + tabbar.postTabs = {} + tabbar.prevNavigation = nil + tabbar.nextNavigation = nil + tabbar.onGeometryChange = function() + hideTabs(tabbar, true, tabbar.postTabs, 0) + updateTabs(tabbar) + end + return tabbar +end + +function UIMoveableTabBar:onDestroy() + if self.prevNavigation then + self.prevNavigation:disable() + end + + if self.nextNavigation then + self.nextNavigation:disable() + end + + self.nextNavigation = nil + self.prevNavigation = nil +end + +function UIMoveableTabBar:setContentWidget(widget) + self.contentWidget = widget + if #self.tabs > 0 then + self.contentWidget:addChild(self.tabs[1].tabPanel) + end +end + +function UIMoveableTabBar:setTabSpacing(tabSpacing) + self.tabSpacing = tabSpacing + updateMargins(self) +end + +function UIMoveableTabBar:addTab(text, panel, menuCallback) + if panel == nil then + panel = g_ui.createWidget(self:getStyleName() .. 'Panel') + panel:setId('tabPanel') + end + + local tab = g_ui.createWidget(self:getStyleName() .. 'Button', self) + panel.isTab = true + tab.tabPanel = panel + tab.tabBar = self + tab:setId('tab') + tab:setDraggable(self.tabsMoveable) + tab:setText(text) + tab:setWidth(tab:getTextSize().width + tab:getPaddingLeft() + tab:getPaddingRight()) + tab.menuCallback = menuCallback or nil + tab.onClick = onTabClick + tab.onMousePress = onTabMousePress + tab.onDragEnter = onTabDragEnter + tab.onDragLeave = onTabDragLeave + tab.onDragMove = onTabDragMove + tab.onDestroy = function() tab.tabPanel:destroy() end + + if #self.tabs == 0 then + self:selectTab(tab) + tab:setMarginLeft(0) + table.insert(self.tabs, tab) + else + local newMargin = self.tabSpacing * #self.tabs + for i = 1, #self.tabs do + newMargin = newMargin + self.tabs[i]:getWidth() + end + tab:setMarginLeft(newMargin) + + hideTabs(self, true, self.postTabs, tab:getWidth()) + table.insert(self.tabs, tab) + if #self.tabs == 1 then + self:selectTab(tab) + end + updateMargins(self) + end + + updateNavigation(self) + return tab +end + +-- Additional function to move the tab by lua +function UIMoveableTabBar:moveTab(tab, units) + local index = table.find(self.tabs, tab) + if index == nil then return end + + local focus = false + if self.currentTab == tab then + self:selectPrevTab() + focus = true + end + + table.remove(self.tabs, index) + + local newIndex = math.min(#self.tabs+1, math.max(index + units, 1)) + table.insert(self.tabs, newIndex, tab) + if focus then self:selectTab(tab) end + updateMargins(self) + return newIndex +end + +function UIMoveableTabBar:onStyleApply(styleName, styleNode) + if styleNode['movable'] then + self.tabsMoveable = styleNode['movable'] + end + if styleNode['tab-spacing'] then + self:setTabSpacing(styleNode['tab-spacing']) + end +end + +function UIMoveableTabBar:removeTab(tab) + local tabTables = {self.tabs, self.preTabs, self.postTabs} + local index = nil + local tabTable = nil + for i = 1, #tabTables do + index = table.find(tabTables[i], tab) + if index ~= nil then + tabTable = tabTables[i] + break + end + end + + if tabTable == nil then + return + end + + if self.currentTab == tab then + self:selectPrevTab() + if #self.tabs == 1 then + self.currentTab = nil + end + end + table.remove(tabTable, index) + if tab.blinkEvent then + removeEvent(tab.blinkEvent) + end + tab:destroy() + updateTabs(self) +end + +function UIMoveableTabBar:getTab(text) + for k,tab in pairs(self.tabs) do + if tab:getText():lower() == text:lower() then + return tab + end + end + for k,tab in pairs(self.preTabs) do + if tab:getText():lower() == text:lower() then + return tab + end + end + for k,tab in pairs(self.postTabs) do + if tab:getText():lower() == text:lower() then + return tab + end + end +end + +function UIMoveableTabBar:selectTab(tab) + if self.currentTab == tab then return end + if self.contentWidget then + local selectedWidget = self.contentWidget:getLastChild() + if selectedWidget and selectedWidget.isTab then + self.contentWidget:removeChild(selectedWidget) + end + self.contentWidget:addChild(tab.tabPanel) + tab.tabPanel:fill('parent') + end + + if self.currentTab then + self.currentTab:setChecked(false) + end + signalcall(self.onTabChange, self, tab) + self.currentTab = tab + tab:setChecked(true) + tab:setOn(false) + tab.blinking = false + + if tab.blinkEvent then + removeEvent(tab.blinkEvent) + tab.blinkEvent = nil + end + + local parent = tab:getParent() + parent:focusChild(tab, MouseFocusReason) + updateNavigation(self) +end + +function UIMoveableTabBar:selectNextTab() + if self.currentTab == nil then + return + end + + local index = table.find(self.tabs, self.currentTab) + if index == nil then + return + end + + local newIndex = index + 1 + if newIndex > #self.tabs then + if #self.postTabs > 0 then + local widget = showPostTab(self) + self:selectTab(widget) + else + if #self.preTabs > 0 then + for i = 1, #self.preTabs do + showPreTab(self) + end + end + + self:selectTab(self.tabs[1]) + end + updateTabs(self) + return + end + + local nextTab = self.tabs[newIndex] + if not nextTab then + return + end + + self:selectTab(nextTab) +end + +function UIMoveableTabBar:selectPrevTab() + if self.currentTab == nil then + return + end + + local index = table.find(self.tabs, self.currentTab) + if index == nil then + return + end + + local newIndex = index - 1 + if newIndex <= 0 then + if #self.preTabs > 0 then + local widget = showPreTab(self) + self:selectTab(widget) + else + if #self.postTabs > 0 then + for i = 1, #self.postTabs do + showPostTab(self) + end + end + + self:selectTab(self.tabs[#self.tabs]) + end + updateTabs(self) + return + end + + local prevTab = self.tabs[newIndex] + if not prevTab then + return + end + + self:selectTab(prevTab) +end + +function UIMoveableTabBar:blinkTab(tab) + if tab:isChecked() then return end + tab.blinking = true + tabBlink(tab) +end + +function UIMoveableTabBar:getTabPanel(tab) + return tab.tabPanel +end + +function UIMoveableTabBar:getCurrentTabPanel() + if self.currentTab then + return self.currentTab.tabPanel + end +end + +function UIMoveableTabBar:getCurrentTab() + return self.currentTab +end + +function UIMoveableTabBar:setNavigation(prevButton, nextButton) + self.prevNavigation = prevButton + self.nextNavigation = nextButton + + if self.prevNavigation then + self.prevNavigation.onClick = function() self:selectPrevTab() end + end + if self.nextNavigation then + self.nextNavigation.onClick = function() self:selectNextTab() end + end + updateNavigation(self) +end diff --git a/modules/corelib/ui/uipopupmenu.lua b/modules/corelib/ui/uipopupmenu.lua new file mode 100644 index 0000000..2167b83 --- /dev/null +++ b/modules/corelib/ui/uipopupmenu.lua @@ -0,0 +1,122 @@ +-- @docclass +UIPopupMenu = extends(UIWidget, "UIPopupMenu") + +local currentMenu + +function UIPopupMenu.create() + local menu = UIPopupMenu.internalCreate() + local layout = UIVerticalLayout.create(menu) + layout:setFitChildren(true) + menu:setLayout(layout) + menu.isGameMenu = false + return menu +end + +function UIPopupMenu:display(pos) + -- don't display if not options was added + if self:getChildCount() == 0 then + self:destroy() + return + end + + if g_ui.isMouseGrabbed() then + self:destroy() + return + end + + if currentMenu then + currentMenu:destroy() + end + + if pos == nil then + pos = g_window.getMousePosition() + end + + rootWidget:addChild(self) + self:setPosition(pos) + self:grabMouse() + self:focus() + --self:grabKeyboard() + currentMenu = self +end + +function UIPopupMenu:onGeometryChange(oldRect, newRect) + local parent = self:getParent() + if not parent then return end + local ymax = parent:getY() + parent:getHeight() + local xmax = parent:getX() + parent:getWidth() + if newRect.y + newRect.height > ymax then + local newy = newRect.y - newRect.height + if newy > 0 and newy + newRect.height < ymax then self:setY(newy) end + end + if newRect.x + newRect.width > xmax then + local newx = newRect.x - newRect.width + if newx > 0 and newx + newRect.width < xmax then self:setX(newx) end + end + self:bindRectToParent() +end + +function UIPopupMenu:addOption(optionName, optionCallback, shortcut) + local optionWidget = g_ui.createWidget(self:getStyleName() .. 'Button', self) + optionWidget.onClick = function(widget) + self:destroy() + optionCallback() + end + optionWidget:setText(optionName) + local width = optionWidget:getTextSize().width + optionWidget:getMarginLeft() + optionWidget:getMarginRight() + 15 + + if shortcut then + local shortcutLabel = g_ui.createWidget(self:getStyleName() .. 'ShortcutLabel', optionWidget) + shortcutLabel:setText(shortcut) + width = width + shortcutLabel:getTextSize().width + shortcutLabel:getMarginLeft() + shortcutLabel:getMarginRight() + end + + self:setWidth(math.max(self:getWidth(), width)) +end + +function UIPopupMenu:addSeparator() + g_ui.createWidget(self:getStyleName() .. 'Separator', self) +end + +function UIPopupMenu:setGameMenu(state) + self.isGameMenu = state +end + +function UIPopupMenu:onDestroy() + if currentMenu == self then + currentMenu = nil + end + self:ungrabMouse() +end + +function UIPopupMenu:onMousePress(mousePos, mouseButton) + -- clicks outside menu area destroys the menu + if not self:containsPoint(mousePos) then + self:destroy() + end + return true +end + +function UIPopupMenu:onKeyPress(keyCode, keyboardModifiers) + if keyCode == KeyEscape then + self:destroy() + return true + end + return false +end + +-- close all menus when the window is resized +local function onRootGeometryUpdate() + if currentMenu then + currentMenu:destroy() + end +end + +local function onGameEnd() + if currentMenu and currentMenu.isGameMenu then + currentMenu:destroy() + end +end + +connect(rootWidget, { onGeometryChange = onRootGeometryUpdate }) +connect(g_game, { onGameEnd = onGameEnd } ) diff --git a/modules/corelib/ui/uipopupscrollmenu.lua b/modules/corelib/ui/uipopupscrollmenu.lua new file mode 100644 index 0000000..405054f --- /dev/null +++ b/modules/corelib/ui/uipopupscrollmenu.lua @@ -0,0 +1,129 @@ +-- @docclass +UIPopupScrollMenu = extends(UIWidget, "UIPopupScrollMenu") + +local currentMenu + +function UIPopupScrollMenu.create() + local menu = UIPopupScrollMenu.internalCreate() + + local scrollArea = g_ui.createWidget('UIScrollArea', menu) + scrollArea:setLayout(UIVerticalLayout.create(menu)) + scrollArea:setId('scrollArea') + + local scrollBar = g_ui.createWidget('VerticalScrollBar', menu) + scrollBar:setId('scrollBar') + scrollBar.pixelsScroll = false + + scrollBar:addAnchor(AnchorRight, 'parent', AnchorRight) + scrollBar:addAnchor(AnchorTop, 'parent', AnchorTop) + scrollBar:addAnchor(AnchorBottom, 'parent', AnchorBottom) + + scrollArea:addAnchor(AnchorLeft, 'parent', AnchorLeft) + scrollArea:addAnchor(AnchorTop, 'parent', AnchorTop) + scrollArea:addAnchor(AnchorBottom, 'parent', AnchorBottom) + scrollArea:addAnchor(AnchorRight, 'next', AnchorLeft) + scrollArea:setVerticalScrollBar(scrollBar) + + menu.scrollArea = scrollArea + menu.scrollBar = scrollBar + return menu +end + +function UIPopupScrollMenu:setScrollbarStep(step) + self.scrollBar:setStep(step) +end + +function UIPopupScrollMenu:display(pos) + -- don't display if not options was added + if self.scrollArea:getChildCount() == 0 then + self:destroy() + return + end + + if g_ui.isMouseGrabbed() then + self:destroy() + return + end + + if currentMenu then + currentMenu:destroy() + end + + if pos == nil then + pos = g_window.getMousePosition() + end + + rootWidget:addChild(self) + self:setPosition(pos) + self:grabMouse() + currentMenu = self +end + +function UIPopupScrollMenu:onGeometryChange(oldRect, newRect) + local parent = self:getParent() + if not parent then return end + local ymax = parent:getY() + parent:getHeight() + local xmax = parent:getX() + parent:getWidth() + if newRect.y + newRect.height > ymax then + local newy = newRect.y - newRect.height + if newy > 0 and newy + newRect.height < ymax then self:setY(newy) end + end + if newRect.x + newRect.width > xmax then + local newx = newRect.x - newRect.width + if newx > 0 and newx + newRect.width < xmax then self:setX(newx) end + end + self:bindRectToParent() +end + +function UIPopupScrollMenu:addOption(optionName, optionCallback, shortcut) + local optionWidget = g_ui.createWidget(self:getStyleName() .. 'Button', self.scrollArea) + optionWidget.onClick = function(widget) + self:destroy() + optionCallback() + end + optionWidget:setText(optionName) + local width = optionWidget:getTextSize().width + optionWidget:getMarginLeft() + optionWidget:getMarginRight() + 15 + + if shortcut then + local shortcutLabel = g_ui.createWidget(self:getStyleName() .. 'ShortcutLabel', optionWidget) + shortcutLabel:setText(shortcut) + width = width + shortcutLabel:getTextSize().width + shortcutLabel:getMarginLeft() + shortcutLabel:getMarginRight() + end + + self:setWidth(math.max(self:getWidth(), width)) +end + +function UIPopupScrollMenu:addSeparator() + g_ui.createWidget(self:getStyleName() .. 'Separator', self.scrollArea) +end + +function UIPopupScrollMenu:onDestroy() + if currentMenu == self then + currentMenu = nil + end + self:ungrabMouse() +end + +function UIPopupScrollMenu:onMousePress(mousePos, mouseButton) + -- clicks outside menu area destroys the menu + if not self:containsPoint(mousePos) then + self:destroy() + end + return true +end + +function UIPopupScrollMenu:onKeyPress(keyCode, keyboardModifiers) + if keyCode == KeyEscape then + self:destroy() + return true + end + return false +end + +-- close all menus when the window is resized +local function onRootGeometryUpdate() + if currentMenu then + currentMenu:destroy() + end +end +connect(rootWidget, { onGeometryChange = onRootGeometryUpdate} ) diff --git a/modules/corelib/ui/uiprogressbar.lua b/modules/corelib/ui/uiprogressbar.lua new file mode 100644 index 0000000..35658a2 --- /dev/null +++ b/modules/corelib/ui/uiprogressbar.lua @@ -0,0 +1,99 @@ +-- @docclass +UIProgressBar = extends(UIWidget, "UIProgressBar") + +function UIProgressBar.create() + local progressbar = UIProgressBar.internalCreate() + progressbar:setFocusable(false) + progressbar:setOn(true) + progressbar.min = 0 + progressbar.max = 100 + progressbar.value = 0 + progressbar.bgBorderLeft = 0 + progressbar.bgBorderRight = 0 + progressbar.bgBorderTop = 0 + progressbar.bgBorderBottom = 0 + return progressbar +end + +function UIProgressBar:setMinimum(minimum) + self.minimum = minimum + if self.value < minimum then + self:setValue(minimum) + end +end + +function UIProgressBar:setMaximum(maximum) + self.maximum = maximum + if self.value > maximum then + self:setValue(maximum) + end +end + +function UIProgressBar:setValue(value, minimum, maximum) + if minimum then + self:setMinimum(minimum) + end + + if maximum then + self:setMaximum(maximum) + end + + self.value = math.max(math.min(value, self.maximum), self.minimum) + self:updateBackground() +end + +function UIProgressBar:setPercent(percent) + self:setValue(percent, 0, 100) +end + +function UIProgressBar:getPercent() + return self.value +end + +function UIProgressBar:getPercentPixels() + return (self.maximum - self.minimum) / self:getWidth() +end + +function UIProgressBar:getProgress() + if self.minimum == self.maximum then return 1 end + return (self.value - self.minimum) / (self.maximum - self.minimum) +end + +function UIProgressBar:updateBackground() + if self:isOn() then + local width = math.round(math.max((self:getProgress() * (self:getWidth() - self.bgBorderLeft - self.bgBorderRight)), 1)) + local height = self:getHeight() - self.bgBorderTop - self.bgBorderBottom + local rect = { x = self.bgBorderLeft, y = self.bgBorderTop, width = width, height = height } + self:setBackgroundRect(rect) + end +end + +function UIProgressBar:onSetup() + self:updateBackground() +end + +function UIProgressBar:onStyleApply(name, node) + for name,value in pairs(node) do + if name == 'background-border-left' then + self.bgBorderLeft = tonumber(value) + elseif name == 'background-border-right' then + self.bgBorderRight = tonumber(value) + elseif name == 'background-border-top' then + self.bgBorderTop = tonumber(value) + elseif name == 'background-border-bottom' then + self.bgBorderBottom = tonumber(value) + elseif name == 'background-border' then + self.bgBorderLeft = tonumber(value) + self.bgBorderRight = tonumber(value) + self.bgBorderTop = tonumber(value) + self.bgBorderBottom = tonumber(value) + end + end +end + +function UIProgressBar:onGeometryChange(oldRect, newRect) + if not self:isOn() then + self:setHeight(0) + end + self:updateBackground() +end diff --git a/modules/corelib/ui/uiradiogroup.lua b/modules/corelib/ui/uiradiogroup.lua new file mode 100644 index 0000000..682aece --- /dev/null +++ b/modules/corelib/ui/uiradiogroup.lua @@ -0,0 +1,66 @@ +-- @docclass +UIRadioGroup = newclass("UIRadioGroup") + +function UIRadioGroup.create() + local radiogroup = UIRadioGroup.internalCreate() + radiogroup.widgets = {} + radiogroup.selectedWidget = nil + return radiogroup +end + +function UIRadioGroup:destroy() + for k,widget in pairs(self.widgets) do + widget.onClick = nil + end + self.widgets = {} +end + +function UIRadioGroup:addWidget(widget) + table.insert(self.widgets, widget) + widget.onClick = function(widget) self:selectWidget(widget) end +end + +function UIRadioGroup:removeWidget(widget) + if self.selectedWidget == widget then + self:selectWidget(nil) + end + widget.onClick = nil + table.removevalue(self.widgets, widget) +end + +function UIRadioGroup:selectWidget(selectedWidget, dontSignal) + if selectedWidget == self.selectedWidget then return end + + local previousSelectedWidget = self.selectedWidget + self.selectedWidget = selectedWidget + + if previousSelectedWidget then + previousSelectedWidget:setChecked(false) + end + + if selectedWidget then + selectedWidget:setChecked(true) + end + + if not dontSignal then + signalcall(self.onSelectionChange, self, selectedWidget, previousSelectedWidget) + end +end + +function UIRadioGroup:clearSelected() + if not self.selectedWidget then return end + + local previousSelectedWidget = self.selectedWidget + self.selectedWidget:setChecked(false) + self.selectedWidget = nil + + signalcall(self.onSelectionChange, self, nil, previousSelectedWidget) +end + +function UIRadioGroup:getSelectedWidget() + return self.selectedWidget +end + +function UIRadioGroup:getFirstWidget() + return self.widgets[1] +end diff --git a/modules/corelib/ui/uiresizeborder.lua b/modules/corelib/ui/uiresizeborder.lua new file mode 100644 index 0000000..4a86ce3 --- /dev/null +++ b/modules/corelib/ui/uiresizeborder.lua @@ -0,0 +1,132 @@ +-- @docclass +UIResizeBorder = extends(UIWidget, "UIResizeBorder") + +function UIResizeBorder.create() + local resizeborder = UIResizeBorder.internalCreate() + resizeborder:setFocusable(false) + resizeborder.minimum = 0 + resizeborder.maximum = 1000 + return resizeborder +end + +function UIResizeBorder:onSetup() + if self:getWidth() > self:getHeight() then + self.vertical = true + else + self.vertical = false + end +end + +function UIResizeBorder:onDestroy() + if self.hovering then + g_mouse.popCursor(self.cursortype) + end +end + +function UIResizeBorder:onHoverChange(hovered) + if hovered then + if g_mouse.isCursorChanged() or g_mouse.isPressed() then return end + if self:getWidth() > self:getHeight() then + self.vertical = true + self.cursortype = 'vertical' + else + self.vertical = false + self.cursortype = 'horizontal' + end + g_mouse.pushCursor(self.cursortype) + self.hovering = true + if not self:isPressed() then + g_effects.fadeIn(self) + end + else + if not self:isPressed() and self.hovering then + g_mouse.popCursor(self.cursortype) + g_effects.fadeOut(self) + self.hovering = false + end + end +end + +function UIResizeBorder:onMouseMove(mousePos, mouseMoved) + if self:isPressed() then + local parent = self:getParent() + local newSize = 0 + if self.vertical then + local delta = mousePos.y - self:getY() - self:getHeight()/2 + newSize = math.min(math.max(parent:getHeight() + delta, self.minimum), self.maximum) + parent:setHeight(newSize) + else + local delta = mousePos.x - self:getX() - self:getWidth()/2 + newSize = math.min(math.max(parent:getWidth() + delta, self.minimum), self.maximum) + parent:setWidth(newSize) + end + + self:checkBoundary(newSize) + return true + end +end + +function UIResizeBorder:onMouseRelease(mousePos, mouseButton) + if not self:isHovered() then + g_mouse.popCursor(self.cursortype) + g_effects.fadeOut(self) + self.hovering = false + end +end + +function UIResizeBorder:onStyleApply(styleName, styleNode) + for name,value in pairs(styleNode) do + if name == 'maximum' then + self:setMaximum(tonumber(value)) + elseif name == 'minimum' then + self:setMinimum(tonumber(value)) + end + end +end + +function UIResizeBorder:onVisibilityChange(visible) + if visible and self.maximum == self.minimum then + self:hide() + end +end + +function UIResizeBorder:setMaximum(maximum) + self.maximum = maximum + self:checkBoundary() +end + +function UIResizeBorder:setMinimum(minimum) + self.minimum = minimum + self:checkBoundary() +end + +function UIResizeBorder:getMaximum() return self.maximum end +function UIResizeBorder:getMinimum() return self.minimum end + +function UIResizeBorder:setParentSize(size) + local parent = self:getParent() + if self.vertical then + parent:setHeight(size) + else + parent:setWidth(size) + end + self:checkBoundary(size) +end + +function UIResizeBorder:getParentSize() + local parent = self:getParent() + if self.vertical then + return parent:getHeight() + else + return parent:getWidth() + end +end + +function UIResizeBorder:checkBoundary(size) + size = size or self:getParentSize() + if self.maximum == self.minimum and size == self.maximum then + self:hide() + else + self:show() + end +end diff --git a/modules/corelib/ui/uiscrollarea.lua b/modules/corelib/ui/uiscrollarea.lua new file mode 100644 index 0000000..5f274ec --- /dev/null +++ b/modules/corelib/ui/uiscrollarea.lua @@ -0,0 +1,190 @@ +-- @docclass +UIScrollArea = extends(UIWidget, "UIScrollArea") + +-- public functions +function UIScrollArea.create() + local scrollarea = UIScrollArea.internalCreate() + scrollarea:setClipping(true) + scrollarea.inverted = false + scrollarea.alwaysScrollMaximum = false + return scrollarea +end + +function UIScrollArea:onStyleApply(styleName, styleNode) + for name,value in pairs(styleNode) do + if name == 'vertical-scrollbar' then + addEvent(function() + local parent = self:getParent() + if parent then + self:setVerticalScrollBar(parent:getChildById(value)) + end + end) + elseif name == 'horizontal-scrollbar' then + addEvent(function() + local parent = self:getParent() + if parent then + self:setHorizontalScrollBar(self:getParent():getChildById(value)) + end + end) + elseif name == 'inverted-scroll' then + self:setInverted(value) + elseif name == 'always-scroll-maximum' then + self:setAlwaysScrollMaximum(value) + end + end +end + +function UIScrollArea:updateScrollBars() + local scrollWidth = math.max(self:getChildrenRect().width - self:getPaddingRect().width, 0) + local scrollHeight = math.max(self:getChildrenRect().height - self:getPaddingRect().height, 0) + + local scrollbar = self.verticalScrollBar + if scrollbar then + if self.inverted then + scrollbar:setMinimum(-scrollHeight) + scrollbar:setMaximum(0) + else + scrollbar:setMinimum(0) + scrollbar:setMaximum(scrollHeight) + end + end + + local scrollbar = self.horizontalScrollBar + if scrollbar then + if self.inverted then + scrollbar:setMinimum(-scrollWidth) + scrollbar:setMaximum(0) + else + scrollbar:setMinimum(0) + scrollbar:setMaximum(scrollWidth) + end + end + + if self.lastScrollWidth ~= scrollWidth then + self:onScrollWidthChange() + end + if self.lastScrollHeight ~= scrollHeight then + self:onScrollHeightChange() + end + + self.lastScrollWidth = scrollWidth + self.lastScrollHeight = scrollHeight +end + +function UIScrollArea:setVerticalScrollBar(scrollbar) + self.verticalScrollBar = scrollbar + connect(self.verticalScrollBar, 'onValueChange', function(scrollbar, value) + local virtualOffset = self:getVirtualOffset() + virtualOffset.y = value + self:setVirtualOffset(virtualOffset) + signalcall(self.onScrollChange, self, virtualOffset) + end) + self:updateScrollBars() +end + +function UIScrollArea:setHorizontalScrollBar(scrollbar) + self.horizontalScrollBar = scrollbar + connect(self.horizontalScrollBar, 'onValueChange', function(scrollbar, value) + local virtualOffset = self:getVirtualOffset() + virtualOffset.x = value + self:setVirtualOffset(virtualOffset) + signalcall(self.onScrollChange, self, virtualOffset) + end) + self:updateScrollBars() +end + +function UIScrollArea:setInverted(inverted) + self.inverted = inverted +end + +function UIScrollArea:setAlwaysScrollMaximum(value) + self.alwaysScrollMaximum = value +end + +function UIScrollArea:onLayoutUpdate() + self:updateScrollBars() +end + +function UIScrollArea:onMouseWheel(mousePos, mouseWheel) + if self.verticalScrollBar then + if not self.verticalScrollBar:isOn() then + return false + end + if mouseWheel == MouseWheelUp then + local minimum = self.verticalScrollBar:getMinimum() + if self.verticalScrollBar:getValue() <= minimum then + return false + end + self.verticalScrollBar:decrement() + else + local maximum = self.verticalScrollBar:getMaximum() + if self.verticalScrollBar:getValue() >= maximum then + return false + end + self.verticalScrollBar:increment() + end + elseif self.horizontalScrollBar then + if not self.horizontalScrollBar:isOn() then + return false + end + if mouseWheel == MouseWheelUp then + local maximum = self.horizontalScrollBar:getMaximum() + if self.horizontalScrollBar:getValue() >= maximum then + return false + end + self.horizontalScrollBar:increment() + else + local minimum = self.horizontalScrollBar:getMinimum() + if self.horizontalScrollBar:getValue() <= minimum then + return false + end + self.horizontalScrollBar:decrement() + end + end + return true +end + +function UIScrollArea:ensureChildVisible(child) + if child then + local paddingRect = self:getPaddingRect() + if self.verticalScrollBar then + local deltaY = paddingRect.y - child:getY() + if deltaY > 0 then + self.verticalScrollBar:decrement(deltaY) + end + + deltaY = (child:getY() + child:getHeight()) - (paddingRect.y + paddingRect.height) + if deltaY > 0 then + self.verticalScrollBar:increment(deltaY) + end + elseif self.horizontalScrollBar then + local deltaX = paddingRect.x - child:getX() + if deltaX > 0 then + self.horizontalScrollBar:decrement(deltaX) + end + + deltaX = (child:getX() + child:getWidth()) - (paddingRect.x + paddingRect.width) + if deltaX > 0 then + self.horizontalScrollBar:increment(deltaX) + end + end + end +end + +function UIScrollArea:onChildFocusChange(focusedChild, oldFocused, reason) + if focusedChild and (reason == MouseFocusReason or reason == KeyboardFocusReason) then + self:ensureChildVisible(focusedChild) + end +end + +function UIScrollArea:onScrollWidthChange() + if self.alwaysScrollMaximum and self.horizontalScrollBar then + self.horizontalScrollBar:setValue(self.horizontalScrollBar:getMaximum()) + end +end + +function UIScrollArea:onScrollHeightChange() + if self.alwaysScrollMaximum and self.verticalScrollBar then + self.verticalScrollBar:setValue(self.verticalScrollBar:getMaximum()) + end +end diff --git a/modules/corelib/ui/uiscrollbar.lua b/modules/corelib/ui/uiscrollbar.lua new file mode 100644 index 0000000..76c2d07 --- /dev/null +++ b/modules/corelib/ui/uiscrollbar.lua @@ -0,0 +1,287 @@ +-- @docclass +UIScrollBar = extends(UIWidget, "UIScrollBar") + +-- private functions +local function calcValues(self) + local slider = self:getChildById('sliderButton') + local decrementButton = self:getChildById('decrementButton') + local incrementButton = self:getChildById('incrementButton') + + local pxrange, center + if self.orientation == 'vertical' then + pxrange = (self:getHeight() - decrementButton:getHeight() - decrementButton:getMarginTop() - decrementButton:getMarginBottom() + - incrementButton:getHeight() - incrementButton:getMarginTop() - incrementButton:getMarginBottom()) + center = self:getY() + math.floor(self:getHeight() / 2) + else -- horizontal + pxrange = (self:getWidth() - decrementButton:getWidth() - decrementButton:getMarginLeft() - decrementButton:getMarginRight() + - incrementButton:getWidth() - incrementButton:getMarginLeft() - incrementButton:getMarginRight()) + center = self:getX() + math.floor(self:getWidth() / 2) + end + + local range = self.maximum - self.minimum + 1 + + local proportion + + if self.pixelsScroll then + proportion = pxrange/(range+pxrange) + else + proportion = math.min(math.max(self.step, 1), range)/range + end + + local px = math.max(proportion * pxrange, 6) + px = px - px % 2 + 1 + + local offset = 0 + if range == 0 or self.value == self.minimum then + if self.orientation == 'vertical' then + offset = -math.floor((self:getHeight() - px) / 2) + decrementButton:getMarginRect().height + else + offset = -math.floor((self:getWidth() - px) / 2) + decrementButton:getMarginRect().width + end + elseif range > 1 and self.value == self.maximum then + if self.orientation == 'vertical' then + offset = math.ceil((self:getHeight() - px) / 2) - incrementButton:getMarginRect().height + else + offset = math.ceil((self:getWidth() - px) / 2) - incrementButton:getMarginRect().width + end + elseif range > 1 then + offset = (((self.value - self.minimum) / (range - 1)) - 0.5) * (pxrange - px) + end + + return range, pxrange, px, offset, center +end + +local function updateValueDisplay(widget) + if widget == nil then return end + + if widget:getShowValue() then + widget:setText(widget:getValue() .. (widget:getSymbol() or '')) + end +end + +local function updateSlider(self) + local slider = self:getChildById('sliderButton') + if slider == nil then return end + + local range, pxrange, px, offset, center = calcValues(self) + if self.orientation == 'vertical' then + slider:setHeight(px) + slider:setMarginTop(offset) + else -- horizontal + slider:setWidth(px) + slider:setMarginLeft(offset) + end + updateValueDisplay(self) + + local status = (self.maximum ~= self.minimum) + + self:setOn(status) + for _i,child in pairs(self:getChildren()) do + child:setEnabled(status) + end +end + +local function parseSliderPos(self, slider, pos, move) + local delta, hotDistance + if self.orientation == 'vertical' then + delta = move.y + hotDistance = pos.y - slider:getY() + else + delta = move.x + hotDistance = pos.x - slider:getX() + end + + if (delta > 0 and hotDistance + delta > self.hotDistance) or + (delta < 0 and hotDistance + delta < self.hotDistance) then + local range, pxrange, px, offset, center = calcValues(self) + local newvalue = self.value + delta * (range / (pxrange - px)) + self:setValue(newvalue) + end +end + +local function parseSliderPress(self, slider, pos, button) + if self.orientation == 'vertical' then + self.hotDistance = pos.y - slider:getY() + else + self.hotDistance = pos.x - slider:getX() + end +end + +-- public functions +function UIScrollBar.create() + local scrollbar = UIScrollBar.internalCreate() + scrollbar:setFocusable(false) + scrollbar.value = 0 + scrollbar.minimum = -999999 + scrollbar.maximum = 999999 + scrollbar.step = 1 + scrollbar.orientation = 'vertical' + scrollbar.pixelsScroll = false + scrollbar.showValue = false + scrollbar.symbol = nil + scrollbar.mouseScroll = true + return scrollbar +end + +function UIScrollBar:onSetup() + self.setupDone = true + local sliderButton = self:getChildById('sliderButton') + g_mouse.bindAutoPress(self:getChildById('decrementButton'), function() self:onDecrement() end, 300) + g_mouse.bindAutoPress(self:getChildById('incrementButton'), function() self:onIncrement() end, 300) + g_mouse.bindPressMove(sliderButton, function(mousePos, mouseMoved) parseSliderPos(self, sliderButton, mousePos, mouseMoved) end) + g_mouse.bindPress(sliderButton, function(mousePos, mouseButton) parseSliderPress(self, sliderButton, mousePos, mouseButton) end) + + updateSlider(self) +end + +function UIScrollBar:onStyleApply(styleName, styleNode) + for name,value in pairs(styleNode) do + if name == 'maximum' then + self:setMaximum(tonumber(value)) + elseif name == 'minimum' then + self:setMinimum(tonumber(value)) + elseif name == 'step' then + self:setStep(tonumber(value)) + elseif name == 'orientation' then + self:setOrientation(value) + elseif name == 'value' then + self:setValue(value) + elseif name == 'pixels-scroll' then + self.pixelsScroll = true + elseif name == 'show-value' then + self.showValue = true + elseif name == 'symbol' then + self.symbol = value + elseif name == 'mouse-scroll' then + self.mouseScroll = value + end + end +end + +function UIScrollBar:onDecrement() + if g_keyboard.isCtrlPressed() then + self:decrement(self.value) + elseif g_keyboard.isShiftPressed() then + self:decrement(10) + else + self:decrement() + end +end + +function UIScrollBar:onIncrement() + if g_keyboard.isCtrlPressed() then + self:increment(self.maximum) + elseif g_keyboard.isShiftPressed() then + self:increment(10) + else + self:increment() + end +end + +function UIScrollBar:decrement(count) + count = count or self.step + self:setValue(self.value - count) +end + +function UIScrollBar:increment(count) + count = count or self.step + self:setValue(self.value + count) +end + +function UIScrollBar:setMaximum(maximum) + if maximum == self.maximum then return end + self.maximum = maximum + if self.minimum > maximum then + self:setMinimum(maximum) + end + if self.value > maximum then + self:setValue(maximum) + else + updateSlider(self) + end +end + +function UIScrollBar:setMinimum(minimum) + if minimum == self.minimum then return end + self.minimum = minimum + if self.maximum < minimum then + self:setMaximum(minimum) + end + if self.value < minimum then + self:setValue(minimum) + else + updateSlider(self) + end +end + +function UIScrollBar:setRange(minimum, maximum) + self:setMinimum(minimum) + self:setMaximum(maximum) +end + +function UIScrollBar:setValue(value) + value = math.max(math.min(value, self.maximum), self.minimum) + if self.value == value then return end + local delta = value - self.value + self.value = value + updateSlider(self) + if self.setupDone then + signalcall(self.onValueChange, self, math.round(value), delta) + end +end + +function UIScrollBar:setMouseScroll(scroll) + self.mouseScroll = scroll +end + +function UIScrollBar:setStep(step) + self.step = step +end + +function UIScrollBar:setOrientation(orientation) + self.orientation = orientation +end + +function UIScrollBar:setText(text) + local valueLabel = self:getChildById('valueLabel') + if valueLabel then + valueLabel:setText(text) + end +end + +function UIScrollBar:onGeometryChange() + updateSlider(self) +end + +function UIScrollBar:onMouseWheel(mousePos, mouseWheel) + if not self.mouseScroll or not self:isOn() then + return false + end + if mouseWheel == MouseWheelUp then + if self.orientation == 'vertical' then + if self.value <= self.minimum then return false end + self:decrement() + else + if self.value >= self.maximum then return false end + self:increment() + end + else + if self.orientation == 'vertical' then + if self.value >= self.maximum then return false end + self:increment() + else + if self.value <= self.minimum then return false end + self:decrement() + end + end + return true +end + +function UIScrollBar:getMaximum() return self.maximum end +function UIScrollBar:getMinimum() return self.minimum end +function UIScrollBar:getValue() return math.round(self.value) end +function UIScrollBar:getStep() return self.step end +function UIScrollBar:getOrientation() return self.orientation end +function UIScrollBar:getShowValue() return self.showValue end +function UIScrollBar:getSymbol() return self.symbol end +function UIScrollBar:getMouseScroll() return self.mouseScroll end diff --git a/modules/corelib/ui/uispinbox.lua b/modules/corelib/ui/uispinbox.lua new file mode 100644 index 0000000..cafd55c --- /dev/null +++ b/modules/corelib/ui/uispinbox.lua @@ -0,0 +1,186 @@ +-- @docclass +UISpinBox = extends(UITextEdit, "UISpinBox") + +function UISpinBox.create() + local spinbox = UISpinBox.internalCreate() + spinbox:setFocusable(false) + spinbox:setValidCharacters('0123456789') + spinbox.displayButtons = true + spinbox.minimum = 0 + spinbox.maximum = 1 + spinbox.value = 0 + spinbox.step = 1 + spinbox.firstchange = true + spinbox.mouseScroll = true + spinbox:setText("1") + spinbox:setValue(1) + return spinbox +end + +function UISpinBox:onSetup() + g_mouse.bindAutoPress(self:getChildById('up'), function() self:up() end, 300) + g_mouse.bindAutoPress(self:getChildById('down'), function() self:down() end, 300) +end + +function UISpinBox:onMouseWheel(mousePos, direction) + if not self.mouseScroll then + return false + end + if direction == MouseWheelUp then + self:up() + elseif direction == MouseWheelDown then + self:down() + end + return true +end + +function UISpinBox:onKeyPress() + if self.firstchange then + self.firstchange = false + self:setText('') + end + return false +end + +function UISpinBox:onTextChange(text, oldText) + if text:len() == 0 then + self:setValue(self.minimum) + return + end + + local number = tonumber(text) + if not number then + self:setText(number) + return + else + if number < self.minimum then + self:setText(self.minimum) + return + elseif number > self.maximum then + self:setText(self.maximum) + return + end + end + + self:setValue(number) +end + +function UISpinBox:onValueChange(value) + -- nothing to do +end + +function UISpinBox:onFocusChange(focused) + if not focused then + if self:getText():len() == 0 then + self:setText(self.minimum) + end + end +end + +function UISpinBox:onStyleApply(styleName, styleNode) + for name, value in pairs(styleNode) do + if name == 'maximum' then + addEvent(function() self:setMaximum(value) end) + elseif name == 'minimum' then + addEvent(function() self:setMinimum(value) end) + elseif name == 'mouse-scroll' then + addEvent(function() self:setMouseScroll(value) end) + elseif name == 'buttons' then + addEvent(function() + if value then + self:showButtons() + else + self:hideButtons() + end + end) + end + end +end + +function UISpinBox:showButtons() + self:getChildById('up'):show() + self:getChildById('down'):show() + self.displayButtons = true +end + +function UISpinBox:hideButtons() + self:getChildById('up'):hide() + self:getChildById('down'):hide() + self.displayButtons = false +end + +function UISpinBox:up() + self:setValue(self.value + self.step) +end + +function UISpinBox:down() + self:setValue(self.value - self.step) +end + +function UISpinBox:setValue(value, dontSignal) + value = value or 0 + value = math.max(math.min(self.maximum, value), self.minimum) + + if value == self.value then return end + + self.value = value + if self:getText():len() > 0 then + self:setText(value) + end + + local upButton = self:getChildById('up') + local downButton = self:getChildById('down') + if upButton then + upButton:setEnabled(self.maximum ~= self.minimum and self.value ~= self.maximum) + end + if downButton then + downButton:setEnabled(self.maximum ~= self.minimum and self.value ~= self.minimum) + end + + if not dontSignal then + signalcall(self.onValueChange, self, value) + end +end + +function UISpinBox:getValue() + return self.value +end + +function UISpinBox:setMinimum(minimum) + minimum = minimum or -9223372036854775808 + self.minimum = minimum + if self.minimum > self.maximum then + self.maximum = self.minimum + end + if self.value < minimum then + self:setValue(minimum) + end +end + +function UISpinBox:getMinimum() + return self.minimum +end + +function UISpinBox:setMaximum(maximum) + maximum = maximum or 9223372036854775807 + self.maximum = maximum + if self.value > maximum then + self:setValue(maximum) + end +end + +function UISpinBox:getMaximum() + return self.maximum +end + +function UISpinBox:setStep(step) + self.step = step or 1 +end + +function UISpinBox:setMouseScroll(mouseScroll) + self.mouseScroll = mouseScroll +end + +function UISpinBox:getMouseScroll() + return self.mouseScroll +end \ No newline at end of file diff --git a/modules/corelib/ui/uisplitter.lua b/modules/corelib/ui/uisplitter.lua new file mode 100644 index 0000000..d8fb2e2 --- /dev/null +++ b/modules/corelib/ui/uisplitter.lua @@ -0,0 +1,85 @@ +-- @docclass +UISplitter = extends(UIWidget, "UISplitter") + +function UISplitter.create() + local splitter = UISplitter.internalCreate() + splitter:setFocusable(false) + splitter.relativeMargin = 'bottom' + return splitter +end + +function UISplitter:onHoverChange(hovered) + -- Check if margin can be changed + local margin = (self.vertical and self:getMarginBottom() or self:getMarginRight()) + if hovered and (self:canUpdateMargin(margin + 1) ~= margin or self:canUpdateMargin(margin - 1) ~= margin) then + if g_mouse.isCursorChanged() or g_mouse.isPressed() then return end + if self:getWidth() > self:getHeight() then + self.vertical = true + self.cursortype = 'vertical' + else + self.vertical = false + self.cursortype = 'horizontal' + end + self.hovering = true + g_mouse.pushCursor(self.cursortype) + if not self:isPressed() then + g_effects.fadeIn(self) + end + else + if not self:isPressed() and self.hovering then + g_mouse.popCursor(self.cursortype) + g_effects.fadeOut(self) + self.hovering = false + end + end +end + +function UISplitter:onMouseMove(mousePos, mouseMoved) + if self:isPressed() then + --local currentmargin, newmargin, delta + if self.vertical then + local delta = mousePos.y - self:getY() - self:getHeight()/2 + local newMargin = self:canUpdateMargin(self:getMarginBottom() - delta) + local currentMargin = self:getMarginBottom() + if newMargin ~= currentMargin then + self.newMargin = newMargin + if not self.event or self.event:isExecuted() then + self.event = addEvent(function() + self:setMarginBottom(self.newMargin) + end) + end + end + else + local delta = mousePos.x - self:getX() - self:getWidth()/2 + local newMargin = self:canUpdateMargin(self:getMarginRight() - delta) + local currentMargin = self:getMarginRight() + if newMargin ~= currentMargin then + self.newMargin = newMargin + if not self.event or self.event:isExecuted() then + self.event = addEvent(function() + self:setMarginRight(self.newMargin) + end) + end + end + end + return true + end +end + +function UISplitter:onMouseRelease(mousePos, mouseButton) + if not self:isHovered() then + g_mouse.popCursor(self.cursortype) + g_effects.fadeOut(self) + self.hovering = false + end +end + +function UISplitter:onStyleApply(styleName, styleNode) + if styleNode['relative-margin'] then + self.relativeMargin = styleNode['relative-margin'] + end +end + +function UISplitter:canUpdateMargin(newMargin) + return newMargin +end diff --git a/modules/corelib/ui/uitabbar.lua b/modules/corelib/ui/uitabbar.lua new file mode 100644 index 0000000..951692f --- /dev/null +++ b/modules/corelib/ui/uitabbar.lua @@ -0,0 +1,157 @@ +-- @docclass +UITabBar = extends(UIWidget, "UITabBar") + +-- private functions +local function onTabClick(tab) + tab.tabBar:selectTab(tab) +end + +local function onTabMouseRelease(tab, mousePos, mouseButton) + if mouseButton == MouseRightButton and tab:containsPoint(mousePos) then + signalcall(tab.tabBar.onTabLeftClick, tab.tabBar, tab) + end +end + +-- public functions +function UITabBar.create() + local tabbar = UITabBar.internalCreate() + tabbar:setFocusable(false) + tabbar.tabs = {} + return tabbar +end + +function UITabBar:onSetup() + self.buttonsPanel = self:getChildById('buttonsPanel') +end + +function UITabBar:setContentWidget(widget) + self.contentWidget = widget + if #self.tabs > 0 then + self.contentWidget:addChild(self.tabs[1].tabPanel) + end +end + +function UITabBar:addTab(text, panel, icon) + if panel == nil then + panel = g_ui.createWidget(self:getStyleName() .. 'Panel') + panel:setId('tabPanel') + end + + local tab = g_ui.createWidget(self:getStyleName() .. 'Button', self.buttonsPanel) + + panel.isTab = true + tab.tabPanel = panel + tab.tabBar = self + tab:setId('tab') + tab:setText(text) + tab:setWidth(tab:getTextSize().width + tab:getPaddingLeft() + tab:getPaddingRight()) + tab.onClick = onTabClick + tab.onMouseRelease = onTabMouseRelease + tab.onDestroy = function() tab.tabPanel:destroy() end + + table.insert(self.tabs, tab) + if #self.tabs == 1 then + self:selectTab(tab) + end + + local tabStyle = {} + tabStyle['icon-source'] = icon + tab:mergeStyle(tabStyle) + + return tab +end + +function UITabBar:addButton(text, func, icon) + local button = g_ui.createWidget(self:getStyleName() .. 'Button', self.buttonsPanel) + button:setText(text) + + local style = {} + style['icon-source'] = icon + button:mergeStyle(style) + + button.onClick = func + return button +end + +function UITabBar:removeTab(tab) + local index = table.find(self.tabs, tab) + if index == nil then return end + if self.currentTab == tab then + self:selectPrevTab() + end + table.remove(self.tabs, index) + tab:destroy() +end + +function UITabBar:getTab(text) + for k,tab in pairs(self.tabs) do + if tab:getText():lower() == text:lower() then + return tab + end + end +end + +function UITabBar:selectTab(tab) + if self.currentTab == tab then return end + if self.contentWidget then + local selectedWidget = self.contentWidget:getLastChild() + if selectedWidget and selectedWidget.isTab then + self.contentWidget:removeChild(selectedWidget) + end + self.contentWidget:addChild(tab.tabPanel) + tab.tabPanel:fill('parent') + end + + if self.currentTab then + self.currentTab:setChecked(false) + end + signalcall(self.onTabChange, self, tab) + self.currentTab = tab + tab:setChecked(true) + tab:setOn(false) + + local parent = tab:getParent() + if parent then + parent:focusChild(tab, MouseFocusReason) + end +end + +function UITabBar:selectNextTab() + if self.currentTab == nil then return end + local index = table.find(self.tabs, self.currentTab) + if index == nil then return end + local nextTab = self.tabs[index + 1] or self.tabs[1] + if not nextTab then return end + self:selectTab(nextTab) +end + +function UITabBar:selectPrevTab() + if self.currentTab == nil then return end + local index = table.find(self.tabs, self.currentTab) + if index == nil then return end + local prevTab = self.tabs[index - 1] or self.tabs[#self.tabs] + if not prevTab then return end + self:selectTab(prevTab) +end + +function UITabBar:getTabPanel(tab) + return tab.tabPanel +end + +function UITabBar:getCurrentTabPanel() + if self.currentTab then + return self.currentTab.tabPanel + end +end + +function UITabBar:getCurrentTab() + return self.currentTab +end + +function UITabBar:getTabs() + return self.tabs +end + +function UITabBar:getTabsPanel() + return table.collect(self.tabs, function(_,tab) return tab.tabPanel end) +end diff --git a/modules/corelib/ui/uitable.lua b/modules/corelib/ui/uitable.lua new file mode 100644 index 0000000..fb644e7 --- /dev/null +++ b/modules/corelib/ui/uitable.lua @@ -0,0 +1,432 @@ +-- @docclass +--[[ + TODO: + * Make table headers more robust. + * Get dynamic row heights working with text wrapping. +]] + +TABLE_SORTING_ASC = 0 +TABLE_SORTING_DESC = 1 + +UITable = extends(UIWidget, "UITable") + +-- Initialize default values +function UITable.create() + local table = UITable.internalCreate() + table.headerRow = nil + table.headerColumns = {} + table.dataSpace = nil + table.rows = {} + table.rowBaseStyle = nil + table.columns = {} + table.columnWidth = {} + table.columBaseStyle = nil + table.headerRowBaseStyle = nil + table.headerColumnBaseStyle = nil + table.selectedRow = nil + table.defaultColumnWidth = 80 + table.sortColumn = -1 + table.sortType = TABLE_SORTING_ASC + table.autoSort = false + + return table +end + +-- Clear table values +function UITable:onDestroy() + for _,row in pairs(self.rows) do + row.onClick = nil + end + self.rows = {} + self.columns = {} + self.headerRow = nil + self.headerColumns = {} + self.columnWidth = {} + self.selectedRow = nil + + if self.dataSpace then + self.dataSpace:destroyChildren() + self.dataSpace = nil + end +end + +-- Detect if a header is already defined +function UITable:onSetup() + local header = self:getChildById('header') + if header then + self:setHeader(header) + end +end + +-- Parse table related styles +function UITable:onStyleApply(styleName, styleNode) + for name, value in pairs(styleNode) do + if value ~= false then + if name == 'table-data' then + addEvent(function() + self:setTableData(self:getParent():getChildById(value)) + end) + elseif name == 'column-style' then + addEvent(function() + self:setColumnStyle(value) + end) + elseif name == 'row-style' then + addEvent(function() + self:setRowStyle(value) + end) + elseif name == 'header-column-style' then + addEvent(function() + self:setHeaderColumnStyle(value) + end) + elseif name == 'header-row-style' then + addEvent(function() + self:setHeaderRowStyle(value) + end) + end + end + end +end + +function UITable:setColumnWidth(width) + if self:hasHeader() then return end + self.columnWidth = width +end + +function UITable:setDefaultColumnWidth(width) + self.defaultColumnWidth = width +end + +-- Check if the table has a header +function UITable:hasHeader() + return self.headerRow ~= nil +end + +-- Clear all rows +function UITable:clearData() + if not self.dataSpace then + return + end + self.dataSpace:destroyChildren() + self.selectedRow = nil + self.columns = {} + self.rows = {} +end + +-- Set existing child as header +function UITable:setHeader(headerWidget) + self:removeHeader() + + if self.dataSpace then + local newHeight = self.dataSpace:getHeight()-headerRow:getHeight()-self.dataSpace:getMarginTop() + self.dataSpace:applyStyle({ height = newHeight }) + end + + self.headerColumns = {} + self.columnWidth = {} + for colId, column in pairs(headerWidget:getChildren()) do + column.colId = colId + column.table = self + table.insert(self.columnWidth, column:getWidth()) + table.insert(self.headerColumns, column) + end + + self.headerRow = headerWidget +end + +-- Create and add header from table data +function UITable:addHeader(data) + if not data or type(data) ~= 'table' then + g_logger.error('UITable:addHeaderRow - table columns must be provided in a table') + return + end + + self:removeHeader() + + -- build header columns + local columns = {} + for colId, column in pairs(data) do + local col = g_ui.createWidget(self.headerColumnBaseStyle) + col.colId = colId + col.table = self + for type, value in pairs(column) do + if type == 'width' then + col:setWidth(value) + elseif type == 'height' then + col:setHeight(value) + elseif type == 'text' then + col:setText(value) + elseif type == 'onClick' then + col.onClick = value + end + end + table.insert(columns, col) + end + + -- create a new header + local headerRow = g_ui.createWidget(self.headerRowBaseStyle, self) + local newHeight = self.dataSpace:getHeight()-headerRow:getHeight()-self.dataSpace:getMarginTop() + self.dataSpace:applyStyle({ height = newHeight }) + + headerRow:setId('header') + self.headerColumns = {} + self.columnWidth = {} + for _, column in pairs(columns) do + headerRow:addChild(column) + table.insert(self.columnWidth, column:getWidth()) + table.insert(self.headerColumns, column) + end + + self.headerRow = headerRow + return headerRow +end + +-- Remove header +function UITable:removeHeader() + if self:hasHeader() then + if self.dataSpace then + local newHeight = self.dataSpace:getHeight()+self.headerRow:getHeight()+self.dataSpace:getMarginTop() + self.dataSpace:applyStyle({ height = newHeight }) + end + self.headerColumns = {} + self.columnWidth = {} + self.headerRow:destroy() + self.headerRow = nil + end +end + +function UITable:addRow(data, height) + if not self.dataSpace then + g_logger.error('UITable:addRow - table data space has not been set, cannot add rows.') + return + end + if not data or type(data) ~= 'table' then + g_logger.error('UITable:addRow - table columns must be provided in a table.') + return + end + + local row = g_ui.createWidget(self.rowBaseStyle) + row.table = self + if height then row:setHeight(height) end + + local rowId = #self.rows + 1 + row.rowId = rowId + row:setId('row'..rowId) + row:updateBackgroundColor() + + self.columns[rowId] = {} + for colId, column in pairs(data) do + local col = g_ui.createWidget(self.columBaseStyle, row) + if column.width then + col:setWidth(column.width) + else + col:setWidth(self.columnWidth[colId] or self.defaultColumnWidth) + end + if column.height then + col:setHeight(column.height) + end + if column.text then + col:setText(column.text) + end + if column.sortvalue then + col.sortvalue = column.sortvalue + else + col.sortvalue = column.text or 0 + end + table.insert(self.columns[rowId], col) + end + + self.dataSpace:addChild(row) + table.insert(self.rows, row) + + if self.autoSort then + self:sort() + end + + return row +end + +-- Update row indices and background color +function UITable:updateRows() + for rowId = 1, #self.rows do + local row = self.rows[rowId] + row.rowId = rowId + row:setId('row'..rowId) + row:updateBackgroundColor() + end +end + +-- Removes the given row widget from the table +function UITable:removeRow(row) + if self.selectedRow == row then + self:selectRow(nil) + end + row.onClick = nil + row.table = nil + table.remove(self.columns, row.rowId) + table.remove(self.rows, row.rowId) + self.dataSpace:removeChild(row) + self:updateRows() +end + +function UITable:toggleSorting(enabled) + self.autoSort = enabled +end + +function UITable:setSorting(colId, sortType) + self.headerColumns[colId]:focus() + + if sortType then + self.sortType = sortType + elseif self.sortColumn == colId then + if self.sortType == TABLE_SORTING_ASC then + self.sortType = TABLE_SORTING_DESC + else + self.sortType = TABLE_SORTING_ASC + end + else + self.sortType = TABLE_SORTING_ASC + end + self.sortColumn = colId +end + +function UITable:sort() + if self.sortColumn <= 0 then + return + end + + if self.sortType == TABLE_SORTING_ASC then + table.sort(self.rows, function(rowA, b) + return rowA:getChildByIndex(self.sortColumn).sortvalue < b:getChildByIndex(self.sortColumn).sortvalue + end) + else + table.sort(self.rows, function(rowA, b) + return rowA:getChildByIndex(self.sortColumn).sortvalue > b:getChildByIndex(self.sortColumn).sortvalue + end) + end + + if self.dataSpace then + for _, child in pairs(self.dataSpace:getChildren()) do + self.dataSpace:removeChild(child) + end + end + + self:updateRows() + self.columns = {} + for _, row in pairs(self.rows) do + if self.dataSpace then + self.dataSpace:addChild(row) + end + + self.columns[row.rowId] = {} + for _, column in pairs(row:getChildren()) do + table.insert(self.columns[row.rowId], column) + end + end +end + +function UITable:selectRow(selectedRow) + if selectedRow == self.selectedRow then return end + + local previousSelectedRow = self.selectedRow + self.selectedRow = selectedRow + + if previousSelectedRow then + previousSelectedRow:setChecked(false) + end + + if selectedRow then + selectedRow:setChecked(true) + end + + signalcall(self.onSelectionChange, self, selectedRow, previousSelectedRow) +end + +function UITable:setTableData(tableData) + local headerHeight = 0 + if self.headerRow then + headerHeight = self.headerRow:getHeight() + end + + self.dataSpace = tableData + self.dataSpace:applyStyle({ height = self:getHeight()-headerHeight-self:getMarginTop() }) +end + +function UITable:setRowStyle(style, dontUpdate) + self.rowBaseStyle = style + + if not dontUpdate then + for _, row in pairs(self.rows) do + row:setStyle(style) + end + end +end + +function UITable:setColumnStyle(style, dontUpdate) + self.columBaseStyle = style + + if not dontUpdate then + for _, columns in pairs(self.columns) do + for _, col in pairs(columns) do + col:setStyle(style) + end + end + end +end + +function UITable:setHeaderRowStyle(style) + self.headerRowBaseStyle = style + if self.headerRow then + self.headerRow:setStyle(style) + end +end + +function UITable:setHeaderColumnStyle(style) + self.headerColumnBaseStyle = style + for _, col in pairs(self.headerColumns) do + col:setStyle(style) + end +end + + +UITableRow = extends(UIWidget, "UITableRow") + +function UITableRow:onFocusChange(focused) + if focused then + if self.table then self.table:selectRow(self) end + end +end + +function UITableRow:onStyleApply(styleName, styleNode) + for name,value in pairs(styleNode) do + if name == 'even-background-color' then + self.evenBackgroundColor = value + elseif name == 'odd-background-color' then + self.oddBackgroundColor = value + end + end +end + +function UITableRow:updateBackgroundColor() + self.backgroundColor = nil + + local isEven = (self.rowId % 2 == 0) + if isEven and self.evenBackgroundColor then + self.backgroundColor = self.evenBackgroundColor + elseif not isEven and self.oddBackgroundColor then + self.backgroundColor = self.oddBackgroundColor + end + + if self.backgroundColor then + self:mergeStyle({ ['background-color'] = self.backgroundColor }) + end +end + + +UITableHeaderColumn = extends(UIButton, "UITableHeaderColumn") + +function UITableHeaderColumn:onClick() + if self.table then + self.table:setSorting(self.colId) + self.table:sort() + end +end diff --git a/modules/corelib/ui/uitextedit.lua b/modules/corelib/ui/uitextedit.lua new file mode 100644 index 0000000..b671c7d --- /dev/null +++ b/modules/corelib/ui/uitextedit.lua @@ -0,0 +1,78 @@ +function UITextEdit:onStyleApply(styleName, styleNode) + for name,value in pairs(styleNode) do + if name == 'vertical-scrollbar' then + addEvent(function() + self:setVerticalScrollBar(self:getParent():getChildById(value)) + end) + elseif name == 'horizontal-scrollbar' then + addEvent(function() + self:setHorizontalScrollBar(self:getParent():getChildById(value)) + end) + end + end +end + +function UITextEdit:onMouseWheel(mousePos, mouseWheel) + if self.verticalScrollBar and self:isMultiline() then + if mouseWheel == MouseWheelUp then + self.verticalScrollBar:decrement() + else + self.verticalScrollBar:increment() + end + return true + elseif self.horizontalScrollBar then + if mouseWheel == MouseWheelUp then + self.horizontalScrollBar:increment() + else + self.horizontalScrollBar:decrement() + end + return true + end +end + +function UITextEdit:onTextAreaUpdate(virtualOffset, virtualSize, totalSize) + self:updateScrollBars() +end + +function UITextEdit:setVerticalScrollBar(scrollbar) + self.verticalScrollBar = scrollbar + self.verticalScrollBar.onValueChange = function(scrollbar, value) + local virtualOffset = self:getTextVirtualOffset() + virtualOffset.y = value + self:setTextVirtualOffset(virtualOffset) + end + self:updateScrollBars() +end + +function UITextEdit:setHorizontalScrollBar(scrollbar) + self.horizontalScrollBar = scrollbar + self.horizontalScrollBar.onValueChange = function(scrollbar, value) + local virtualOffset = self:getTextVirtualOffset() + virtualOffset.x = value + self:setTextVirtualOffset(virtualOffset) + end + self:updateScrollBars() +end + +function UITextEdit:updateScrollBars() + local scrollSize = self:getTextTotalSize() + local scrollWidth = math.max(scrollSize.width - self:getTextVirtualSize().width, 0) + local scrollHeight = math.max(scrollSize.height - self:getTextVirtualSize().height, 0) + + local scrollbar = self.verticalScrollBar + if scrollbar then + scrollbar:setMinimum(0) + scrollbar:setMaximum(scrollHeight) + scrollbar:setValue(self:getTextVirtualOffset().y) + end + + local scrollbar = self.horizontalScrollBar + if scrollbar then + scrollbar:setMinimum(0) + scrollbar:setMaximum(scrollWidth) + scrollbar:setValue(self:getTextVirtualOffset().x) + end + +end + +-- todo: ontext change, focus to cursor \ No newline at end of file diff --git a/modules/corelib/ui/uiwidget.lua b/modules/corelib/ui/uiwidget.lua new file mode 100644 index 0000000..a6007ca --- /dev/null +++ b/modules/corelib/ui/uiwidget.lua @@ -0,0 +1,21 @@ +-- @docclass UIWidget + +function UIWidget:setMargin(...) + local params = {...} + if #params == 1 then + self:setMarginTop(params[1]) + self:setMarginRight(params[1]) + self:setMarginBottom(params[1]) + self:setMarginLeft(params[1]) + elseif #params == 2 then + self:setMarginTop(params[1]) + self:setMarginRight(params[2]) + self:setMarginBottom(params[1]) + self:setMarginLeft(params[2]) + elseif #params == 4 then + self:setMarginTop(params[1]) + self:setMarginRight(params[2]) + self:setMarginBottom(params[3]) + self:setMarginLeft(params[4]) + end +end diff --git a/modules/corelib/ui/uiwindow.lua b/modules/corelib/ui/uiwindow.lua new file mode 100644 index 0000000..4c0e2ce --- /dev/null +++ b/modules/corelib/ui/uiwindow.lua @@ -0,0 +1,46 @@ +-- @docclass +UIWindow = extends(UIWidget, "UIWindow") + +function UIWindow.create() + local window = UIWindow.internalCreate() + window:setTextAlign(AlignTopCenter) + window:setDraggable(true) + window:setAutoFocusPolicy(AutoFocusFirst) + return window +end + +function UIWindow:onKeyDown(keyCode, keyboardModifiers) + if keyboardModifiers == KeyboardNoModifier then + if keyCode == KeyEnter then + signalcall(self.onEnter, self) + elseif keyCode == KeyEscape then + signalcall(self.onEscape, self) + end + end +end + +function UIWindow:onFocusChange(focused) + if focused then self:raise() end +end + +function UIWindow:onDragEnter(mousePos) + if self.static then + return + end + self:breakAnchors() + self.movingReference = { x = mousePos.x - self:getX(), y = mousePos.y - self:getY() } + return true +end + +function UIWindow:onDragLeave(droppedWidget, mousePos) + -- TODO: auto detect and reconnect anchors +end + +function UIWindow:onDragMove(mousePos, mouseMoved) + if self.static then + return + end + local pos = { x = mousePos.x - self.movingReference.x, y = mousePos.y - self.movingReference.y } + self:setPosition(pos) + self:bindRectToParent() +end diff --git a/modules/corelib/util.lua b/modules/corelib/util.lua new file mode 100644 index 0000000..157c28b --- /dev/null +++ b/modules/corelib/util.lua @@ -0,0 +1,365 @@ +-- @docfuncs @{ + +function print(...) + local msg = "" + local args = {...} + local appendSpace = #args > 1 + for i,v in ipairs(args) do + msg = msg .. tostring(v) + if appendSpace and i < #args then + msg = msg .. ' ' + end + end + g_logger.log(LogInfo, msg) +end + +function pinfo(msg) + g_logger.log(LogInfo, msg) +end + +function perror(msg) + g_logger.log(LogError, msg) +end + +function pwarning(msg) + g_logger.log(LogWarning, msg) +end + +function pdebug(msg) + g_logger.log(LogDebug, msg) +end + +function fatal(msg) + g_logger.log(LogFatal, msg) +end + +function exit() + g_app.exit() +end + +function quit() + g_app.quit() +end + +function connect(object, arg1, arg2, arg3) + local signalsAndSlots + local pushFront + if type(arg1) == 'string' then + signalsAndSlots = { [arg1] = arg2 } + pushFront = arg3 + else + signalsAndSlots = arg1 + pushFront = arg2 + end + + for signal,slot in pairs(signalsAndSlots) do + if not object[signal] then + local mt = getmetatable(object) + if mt and type(object) == 'userdata' then + object[signal] = function(...) + return signalcall(mt[signal], ...) + end + end + end + + if not object[signal] then + object[signal] = slot + elseif type(object[signal]) == 'function' then + object[signal] = { object[signal] } + end + + if type(slot) ~= 'function' then + perror(debug.traceback('unable to connect a non function value')) + end + + if type(object[signal]) == 'table' then + if pushFront then + table.insert(object[signal], 1, slot) + else + table.insert(object[signal], #object[signal]+1, slot) + end + end + end +end + +function disconnect(object, arg1, arg2) + local signalsAndSlots + if type(arg1) == 'string' then + if arg2 == nil then + object[arg1] = nil + return + end + signalsAndSlots = { [arg1] = arg2 } + elseif type(arg1) == 'table' then + signalsAndSlots = arg1 + else + perror(debug.traceback('unable to disconnect')) + end + + for signal,slot in pairs(signalsAndSlots) do + if not object[signal] then + elseif type(object[signal]) == 'function' then + if object[signal] == slot then + object[signal] = nil + end + elseif type(object[signal]) == 'table' then + for k,func in pairs(object[signal]) do + if func == slot then + table.remove(object[signal], k) + + if #object[signal] == 1 then + object[signal] = object[signal][1] + end + break + end + end + end + end +end + +function newclass(name) + if not name then + perror(debug.traceback('new class has no name.')) + end + + local class = {} + function class.internalCreate() + local instance = {} + for k,v in pairs(class) do + instance[k] = v + end + return instance + end + class.create = class.internalCreate + class.__class = name + class.getClassName = function() return name end + return class +end + +function extends(base, name) + if not name then + perror(debug.traceback('extended class has no name.')) + end + + local derived = {} + function derived.internalCreate() + local instance = base.create() + for k,v in pairs(derived) do + instance[k] = v + end + return instance + end + derived.create = derived.internalCreate + derived.__class = name + derived.getClassName = function() return name end + return derived +end + +function runinsandbox(func, ...) + if type(func) == 'string' then + func, err = loadfile(resolvepath(func, 2)) + if not func then + error(err) + end + end + local env = { } + local oldenv = getfenv(0) + setmetatable(env, { __index = oldenv } ) + setfenv(0, env) + func(...) + setfenv(0, oldenv) + return env +end + +function loadasmodule(name, file) + file = file or resolvepath(name, 2) + if package.loaded[name] then + return package.loaded[name] + end + local env = runinsandbox(file) + package.loaded[name] = env + return env +end + +local function module_loader(modname) + local module = g_modules.getModule(modname) + if not module then + return '\n\tno module \'' .. modname .. '\'' + end + return function() + if not module:load() then + error('unable to load required module ' .. modname) + end + return module:getSandbox() + end +end +table.insert(package.loaders, 1, module_loader) + +function import(table) + assert(type(table) == 'table') + local env = getfenv(2) + for k,v in pairs(table) do + env[k] = v + end +end + +function export(what, key) + if key ~= nil then + _G[key] = what + else + for k,v in pairs(what) do + _G[k] = v + end + end +end + +function unexport(key) + if type(key) == 'table' then + for _k,v in pairs(key) do + _G[v] = nil + end + else + _G[key] = nil + end +end + +function getfsrcpath(depth) + depth = depth or 2 + local info = debug.getinfo(1+depth, "Sn") + local path + if info.short_src then + path = info.short_src:match("(.*)/.*") + end + if not path then + path = '/' + elseif path:sub(0, 1) ~= '/' then + path = '/' .. path + end + return path +end + +function resolvepath(filePath, depth) + if not filePath then return nil end + depth = depth or 1 + if filePath then + if filePath:sub(0, 1) ~= '/' then + local basepath = getfsrcpath(depth+1) + if basepath:sub(#basepath) ~= '/' then basepath = basepath .. '/' end + return basepath .. filePath + else + return filePath + end + else + local basepath = getfsrcpath(depth+1) + if basepath:sub(#basepath) ~= '/' then basepath = basepath .. '/' end + return basepath + end +end + +function toboolean(v) + if type(v) == 'string' then + v = v:trim():lower() + if v == '1' or v == 'true' then + return true + end + elseif type(v) == 'number' then + if v == 1 then + return true + end + elseif type(v) == 'boolean' then + return v + end + return false +end + +function fromboolean(boolean) + if boolean then + return 'true' + else + return 'false' + end +end + +function booleantonumber(boolean) + if boolean then + return 1 + else + return 0 + end +end + +function numbertoboolean(number) + if number ~= 0 then + return true + else + return false + end +end + +function protectedcall(func, ...) + local status, ret = pcall(func, ...) + if status then + return ret + end + + perror(ret) + return false +end + +function signalcall(param, ...) + if type(param) == 'function' then + local status, ret = pcall(param, ...) + if status then + return ret + else + perror(ret) + end + elseif type(param) == 'table' then + for k,v in pairs(param) do + local status, ret = pcall(v, ...) + if status then + if ret then return true end + else + perror(ret) + end + end + elseif param ~= nil then + error('attempt to call a non function value') + end + return false +end + +function tr(s, ...) + return string.format(s, ...) +end + +function getOppositeAnchor(anchor) + if anchor == AnchorLeft then + return AnchorRight + elseif anchor == AnchorRight then + return AnchorLeft + elseif anchor == AnchorTop then + return AnchorBottom + elseif anchor == AnchorBottom then + return AnchorTop + elseif anchor == AnchorVerticalCenter then + return AnchorHorizontalCenter + elseif anchor == AnchorHorizontalCenter then + return AnchorVerticalCenter + end + return anchor +end + +function makesingleton(obj) + local singleton = {} + if obj.getClassName then + for key,value in pairs(_G[obj:getClassName()]) do + if type(value) == 'function' then + singleton[key] = function(...) return value(obj, ...) end + end + end + end + return singleton +end + +-- @} \ No newline at end of file diff --git a/modules/game_battle/battle.lua b/modules/game_battle/battle.lua new file mode 100644 index 0000000..7dfc047 --- /dev/null +++ b/modules/game_battle/battle.lua @@ -0,0 +1,449 @@ +battleWindow = nil +battleButton = nil +battlePanel = nil +filterPanel = nil +toggleFilterButton = nil +creatureAgeList = {} +battleButtonsList = {} + +mouseWidget = nil + +sortTypeBox = nil +sortOrderBox = nil +hidePlayersButton = nil +hideNPCsButton = nil +hideMonstersButton = nil +hideSkullsButton = nil +hidePartyButton = nil + +updateEvent = nil + +hoveredCreature = nil +newHoveredCreature = nil +prevCreature = nil + +local creatureAgeCounter = 1 + +function init() + g_ui.importStyle('battlebutton') + battleButton = modules.client_topmenu.addRightGameToggleButton('battleButton', tr('Battle') .. ' (Ctrl+B)', '/images/topbuttons/battle', toggle) + battleButton:setOn(true) + battleWindow = g_ui.loadUI('battle', modules.game_interface.getRightPanel()) + g_keyboard.bindKeyDown('Ctrl+B', toggle) + + -- this disables scrollbar auto hiding + local scrollbar = battleWindow:getChildById('miniwindowScrollBar') + scrollbar:mergeStyle({ ['$!on'] = { }}) + + battlePanel = battleWindow:recursiveGetChildById('battlePanel') + + filterPanel = battleWindow:recursiveGetChildById('filterPanel') + toggleFilterButton = battleWindow:recursiveGetChildById('toggleFilterButton') + + if isHidingFilters() then + hideFilterPanel() + end + + sortTypeBox = battleWindow:recursiveGetChildById('sortTypeBox') + sortOrderBox = battleWindow:recursiveGetChildById('sortOrderBox') + hidePlayersButton = battleWindow:recursiveGetChildById('hidePlayers') + hideNPCsButton = battleWindow:recursiveGetChildById('hideNPCs') + hideMonstersButton = battleWindow:recursiveGetChildById('hideMonsters') + hideSkullsButton = battleWindow:recursiveGetChildById('hideSkulls') + hidePartyButton = battleWindow:recursiveGetChildById('hideParty') + + mouseWidget = g_ui.createWidget('UIButton') + mouseWidget:setVisible(false) + mouseWidget:setFocusable(false) + mouseWidget.cancelNextRelease = false + + battleWindow:setContentMinimumHeight(80) + + sortTypeBox:addOption('Name', 'name') + sortTypeBox:addOption('Distance', 'distance') + sortTypeBox:addOption('Age', 'age') + sortTypeBox:addOption('Health', 'health') + sortTypeBox:setCurrentOptionByData(getSortType()) + sortTypeBox.onOptionChange = onChangeSortType + + sortOrderBox:addOption('Asc.', 'asc') + sortOrderBox:addOption('Desc.', 'desc') + sortOrderBox:setCurrentOptionByData(getSortOrder()) + sortOrderBox.onOptionChange = onChangeSortOrder + + updateBattleList() + battleWindow:setup() + + connect(LocalPlayer, { + onPositionChange = onCreaturePositionChange + }) + connect(Creature, { + onAppear = updateSquare, + onDisappear = updateSquare + }) + connect(g_game, { + onAttackingCreatureChange = updateSquare, + onFollowingCreatureChange = updateSquare + }) +end + +function terminate() + if battleButton == nil then + return + end + + g_keyboard.unbindKeyDown('Ctrl+B') + battleButtonsByCreaturesList = {} + battleButton:destroy() + battleWindow:destroy() + mouseWidget:destroy() + + disconnect(LocalPlayer, { + onPositionChange = onCreaturePositionChange + }) + disconnect(Creature, { + onAppear = onCreatureAppear, + onDisappear = onCreatureDisappear + }) + disconnect(g_game, { + onAttackingCreatureChange = updateSquare, + onFollowingCreatureChange = updateSquare + }) + + removeEvent(updateEvent) +end + +function toggle() + if battleButton:isOn() then + battleWindow:close() + battleButton:setOn(false) + else + battleWindow:open() + battleButton:setOn(true) + end +end + +function onMiniWindowClose() + battleButton:setOn(false) +end + +function getSortType() + local settings = g_settings.getNode('BattleList') + if not settings then + return 'name' + end + return settings['sortType'] +end + +function setSortType(state) + settings = {} + settings['sortType'] = state + g_settings.mergeNode('BattleList', settings) + + checkCreatures() +end + +function getSortOrder() + local settings = g_settings.getNode('BattleList') + if not settings then + return 'asc' + end + return settings['sortOrder'] +end + +function setSortOrder(state) + settings = {} + settings['sortOrder'] = state + g_settings.mergeNode('BattleList', settings) + + checkCreatures() +end + +function isSortAsc() + return getSortOrder() == 'asc' +end + +function isSortDesc() + return getSortOrder() == 'desc' +end + +function isHidingFilters() + local settings = g_settings.getNode('BattleList') + if not settings then + return false + end + return settings['hidingFilters'] +end + +function setHidingFilters(state) + settings = {} + settings['hidingFilters'] = state + g_settings.mergeNode('BattleList', settings) +end + +function hideFilterPanel() + filterPanel.originalHeight = filterPanel:getHeight() + filterPanel:setHeight(0) + toggleFilterButton:getParent():setMarginTop(0) + toggleFilterButton:setImageClip(torect("0 0 21 12")) + setHidingFilters(true) + filterPanel:setVisible(false) +end + +function showFilterPanel() + toggleFilterButton:getParent():setMarginTop(5) + filterPanel:setHeight(filterPanel.originalHeight) + toggleFilterButton:setImageClip(torect("21 0 21 12")) + setHidingFilters(false) + filterPanel:setVisible(true) +end + +function toggleFilterPanel() + if filterPanel:isVisible() then + hideFilterPanel() + else + showFilterPanel() + end +end + +function onChangeSortType(comboBox, option) + setSortType(option:lower()) +end + +function onChangeSortOrder(comboBox, option) + -- Replace dot in option name + setSortOrder(option:lower():gsub('[.]', '')) +end + +-- functions +function updateBattleList() + updateEvent = scheduleEvent(updateBattleList, 200) + checkCreatures() +end + +function checkCreatures() + if not g_game.isOnline() then + return + end + + local player = g_game.getLocalPlayer() + local dimension = modules.game_interface.getMapPanel():getVisibleDimension() + local spectators = g_map.getSpectatorsInRangeEx(player:getPosition(), false, math.floor(dimension.width / 2), math.floor(dimension.width / 2), math.floor(dimension.height / 2), math.floor(dimension.height / 2)) + + creatures = {} + for _, creature in ipairs(spectators) do + if creatureAgeList[creature] == nil then + creatureAgeList[creature] = creatureAgeCounter + creatureAgeCounter = creatureAgeCounter + 1 + end + if doCreatureFitFilters(creature) then + table.insert(creatures, creature) + end + end + + updateSquare() + + -- sorting + local creature_i = 1 + sortCreatures(creatures) + for i=1, #creatures do + if creature_i > 30 then + break + end + + local creature = creatures[i] + if isSortAsc() then + creature = creatures[#creatures - i + 1] + end + + if creature:getHealthPercent() > 0 then + local battleButton = battleButtonsList[creature_i] + + if battleButton == nil then + battleButton = g_ui.createWidget('BattleButton') + battleButton.onHoverChange = onBattleButtonHoverChange + battleButton.onMouseRelease = onBattleButtonMouseRelease + battleButton:setup(creature, creature_i) + table.insert(battleButtonsList, battleButton) + battlePanel:addChild(battleButton) + end + + battleButton:creatureSetup(creature) + creature_i = creature_i + 1 + end + end + + local height = 0 + if creature_i > 1 then + height = 25 * (creature_i - 1) + end + if battlePanel:getHeight() ~= height then + battlePanel:setHeight(height) + end +end + +function doCreatureFitFilters(creature) + if creature:isLocalPlayer() then + return false + end + + local pos = creature:getPosition() + if not pos then return false end + + local localPlayer = g_game.getLocalPlayer() + if pos.z ~= localPlayer:getPosition().z or not creature:canBeSeen() then return false end + + local hidePlayers = hidePlayersButton:isChecked() + local hideNPCs = hideNPCsButton:isChecked() + local hideMonsters = hideMonstersButton:isChecked() + local hideSkulls = hideSkullsButton:isChecked() + local hideParty = hidePartyButton:isChecked() + + if hidePlayers and creature:isPlayer() then + return false + elseif hideNPCs and creature:isNpc() then + return false + elseif hideMonsters and creature:isMonster() then + return false + elseif hideSkulls and creature:isPlayer() and creature:getSkull() == SkullNone then + return false + elseif hideParty and creature:getShield() > ShieldWhiteBlue then + return false + end + + return true +end + +local function getDistanceBetween(p1, p2) + return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) +end + +function sortCreatures(creatures) + local player = g_game.getLocalPlayer() + + if getSortType() == 'distance' then + local playerPos = player:getPosition() + table.sort(creatures, function(a, b) + if getDistanceBetween(playerPos, a:getPosition()) == getDistanceBetween(playerPos, b:getPosition()) then + return creatureAgeList[a] > creatureAgeList[b] + end + return getDistanceBetween(playerPos, a:getPosition()) > getDistanceBetween(playerPos, b:getPosition()) + end) + elseif getSortType() == 'health' then + table.sort(creatures, function(a, b) + if a:getHealthPercent() == b:getHealthPercent() then + return creatureAgeList[a] > creatureAgeList[b] + end + return a:getHealthPercent() > b:getHealthPercent() + end) + elseif getSortType() == 'age' then + table.sort(creatures, function(a, b) return creatureAgeList[a] > creatureAgeList[b] end) + else -- name + table.sort(creatures, function(a, b) + if a:getName():lower() == b:getName():lower() then + return creatureAgeList[a] > creatureAgeList[b] + end + return a:getName():lower() > b:getName():lower() + end) + end +end + +-- other functions +function onBattleButtonMouseRelease(self, mousePosition, mouseButton) + if mouseWidget.cancelNextRelease then + mouseWidget.cancelNextRelease = false + return false + end + if not self.creature then + return false + end + if ((g_mouse.isPressed(MouseLeftButton) and mouseButton == MouseRightButton) + or (g_mouse.isPressed(MouseRightButton) and mouseButton == MouseLeftButton)) then + mouseWidget.cancelNextRelease = true + g_game.look(self.creature, true) + return true + elseif mouseButton == MouseLeftButton and g_keyboard.isShiftPressed() then + g_game.look(self.creature, true) + return true + elseif mouseButton == MouseRightButton and not g_mouse.isPressed(MouseLeftButton) then + modules.game_interface.createThingMenu(mousePosition, nil, nil, self.creature) + return true + elseif mouseButton == MouseLeftButton and not g_mouse.isPressed(MouseRightButton) then + if self.isTarget then + g_game.cancelAttack() + else + g_game.attack(self.creature) + end + return true + end + return false +end + +function onBattleButtonHoverChange(battleButton, hovered) + if not hovered then + newHoveredCreature = nil + else + newHoveredCreature = battleButton.creature + end + if battleButton.isHovered ~= hovered then + battleButton.isHovered = hovered + battleButton:update() + end + updateSquare() +end + +function onCreaturePositionChange(creature, newPos, oldPos) + if creature:isLocalPlayer() then + if oldPos and newPos and newPos.z ~= oldPos.z then + checkCreatures() + end + end +end + +local CreatureButtonColors = { + onIdle = {notHovered = '#888888', hovered = '#FFFFFF' }, + onTargeted = {notHovered = '#FF0000', hovered = '#FF8888' }, + onFollowed = {notHovered = '#00FF00', hovered = '#88FF88' } +} + +function updateSquare() + local following = g_game.getFollowingCreature() + local attacking = g_game.getAttackingCreature() + + if newHoveredCreature == nil then + if hoveredCreature ~= nil then + hoveredCreature:hideStaticSquare() + hoveredCreature = nil + end + else + if hoveredCreature ~= nil then + hoveredCreature:hideStaticSquare() + end + hoveredCreature = newHoveredCreature + hoveredCreature:showStaticSquare(CreatureButtonColors.onIdle.hovered) + end + + local color = CreatureButtonColors.onIdle + local creature = nil + if attacking then + color = CreatureButtonColors.onTargeted + creature = attacking + elseif following then + color = CreatureButtonColors.onFollowed + creature = following + end + + if prevCreature ~= creature then + if prevCreature ~= nil then + prevCreature:hideStaticSquare() + end + prevCreature = creature + end + + if not creature then + return + end + + color = creature == hoveredCreature and color.hovered or color.notHovered + creature:showStaticSquare(color) +end \ No newline at end of file diff --git a/modules/game_battle/battle.otmod b/modules/game_battle/battle.otmod new file mode 100644 index 0000000..ba50425 --- /dev/null +++ b/modules/game_battle/battle.otmod @@ -0,0 +1,9 @@ +Module + name: game_battle + description: Manage battle window (new) + author: otclient@otclient.ovh + website: otclient.ovh + sandboxed: true + scripts: [ battle ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_battle/battle.otui b/modules/game_battle/battle.otui new file mode 100644 index 0000000..7b9fbd5 --- /dev/null +++ b/modules/game_battle/battle.otui @@ -0,0 +1,148 @@ +BattleIcon < UICheckBox + size: 20 20 + image-color: white + image-rect: 0 0 20 20 + + $hover !disabled: + color: #cccccc + + $!checked: + image-clip: 0 0 20 20 + + $hover !checked: + image-clip: 0 40 20 20 + + $checked: + image-clip: 0 20 20 20 + + $hover checked: + image-clip: 0 60 20 20 + + $disabled: + image-color: #ffffff88 + +BattlePlayers < BattleIcon + image-source: /images/game/battle/battle_players + +BattleNPCs < BattleIcon + image-source: /images/game/battle/battle_npcs + +BattleMonsters < BattleIcon + image-source: /images/game/battle/battle_monsters + +BattleSkulls < BattleIcon + image-source: /images/game/battle/battle_skulls + +BattleParty < BattleIcon + image-source: /images/game/battle/battle_party + +MiniWindow + id: battleWindow + !text: tr('Battle') + height: 166 + icon: /images/topbuttons/battle + @onClose: modules.game_battle.onMiniWindowClose() + &save: true + + Panel + id: filterPanel + margin-top: 26 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: miniwindowScrollBar.left + height: 45 + + Panel + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + height: 20 + width: 120 + layout: + type: horizontalBox + spacing: 5 + + BattlePlayers + id: hidePlayers + !tooltip: tr('Hide players') + @onCheckChange: modules.game_battle.checkCreatures() + + BattleNPCs + id: hideNPCs + !tooltip: tr('Hide Npcs') + @onCheckChange: modules.game_battle.checkCreatures() + + BattleMonsters + id: hideMonsters + !tooltip: tr('Hide monsters') + @onCheckChange: modules.game_battle.checkCreatures() + + BattleSkulls + id: hideSkulls + !tooltip: tr('Hide non-skull players') + @onCheckChange: modules.game_battle.checkCreatures() + + BattleParty + id: hideParty + !tooltip: tr('Hide party members') + @onCheckChange: modules.game_battle.checkCreatures() + + Panel + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 20 + margin-top: 6 + + ComboBox + id: sortTypeBox + width: 90 + anchors.top: parent.top + anchors.left: prev.right + anchors.horizontalCenter: parent.horizontalCenter + margin-left: -31 + + ComboBox + id: sortOrderBox + width: 60 + anchors.top: parent.top + anchors.left: prev.right + margin-left: 4 + + Panel + height: 18 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: miniwindowScrollBar.left + margin-top: 4 + + UIWidget + id: toggleFilterButton + anchors.top: prev.top + width: 21 + anchors.horizontalCenter: parent.horizontalCenter + image-source: /images/ui/arrow_vertical + image-rect: 0 0 21 12 + image-clip: 21 0 21 12 + @onClick: modules.game_battle.toggleFilterPanel() + phantom: false + + HorizontalSeparator + anchors.top: prev.top + anchors.left: parent.left + anchors.right: miniwindowScrollBar.left + margin-right: 1 + margin-top: 11 + + MiniWindowContents + anchors.top: prev.bottom + margin-top: 6 + + Panel + id: battlePanel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + margin-top: 5 + padding-right: 5 + layout: + type: verticalBox diff --git a/modules/game_battle/battlebutton.otui b/modules/game_battle/battlebutton.otui new file mode 100644 index 0000000..193485f --- /dev/null +++ b/modules/game_battle/battlebutton.otui @@ -0,0 +1,2 @@ +BattleButton < CreatureButton + &isBattleButton: true \ No newline at end of file diff --git a/modules/game_bugreport/bugreport.lua b/modules/game_bugreport/bugreport.lua new file mode 100644 index 0000000..5308d7f --- /dev/null +++ b/modules/game_bugreport/bugreport.lua @@ -0,0 +1,36 @@ +-- TODO: find another hotkey for this. Ctrl+Z will be reserved to undo on textedits. +HOTKEY = 'Ctrl+Z' + +bugReportWindow = nil +bugTextEdit = nil + +function init() + g_ui.importStyle('bugreport') + + bugReportWindow = g_ui.createWidget('BugReportWindow', rootWidget) + bugReportWindow:hide() + + bugTextEdit = bugReportWindow:getChildById('bugTextEdit') + + g_keyboard.bindKeyDown(HOTKEY, show) +end + +function terminate() + g_keyboard.unbindKeyDown(HOTKEY) + bugReportWindow:destroy() +end + +function doReport() + g_game.reportBug(bugTextEdit:getText()) + bugReportWindow:hide() + modules.game_textmessage.displayGameMessage(tr('Bug report sent.')) +end + +function show() + if g_game.isOnline() then + bugTextEdit:setText('') + bugReportWindow:show() + bugReportWindow:raise() + bugReportWindow:focus() + end +end diff --git a/modules/game_bugreport/bugreport.otmod b/modules/game_bugreport/bugreport.otmod new file mode 100644 index 0000000..5306bb5 --- /dev/null +++ b/modules/game_bugreport/bugreport.otmod @@ -0,0 +1,9 @@ +Module + name: game_bugreport + description: Bug report interface (Ctrl+Z) + author: edubart + website: https://github.com/edubart/otclient + scripts: [ bugreport ] + sandboxed: true + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_bugreport/bugreport.otui b/modules/game_bugreport/bugreport.otui new file mode 100644 index 0000000..8ce215e --- /dev/null +++ b/modules/game_bugreport/bugreport.otui @@ -0,0 +1,39 @@ +BugReportWindow < MainWindow + !text: tr('Report Bug') + size: 280 250 + @onEscape: self:hide() + + Label + id: bugLabel + !text: tr('Please use this dialog to only report bugs. Do not report rule violations here!') + text-wrap: true + text-auto-resize: true + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + MultilineTextEdit + id: bugTextEdit + anchors.top: bugLabel.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: sendButton.top + margin-top: 4 + margin-bottom: 8 + + Button + id: sendButton + !text: tr('Send') + anchors.bottom: cancelButton.bottom + anchors.right: cancelButton.left + margin-right: 10 + width: 80 + &onClick: doReport + + Button + id: cancelButton + !text: tr('Cancel') + anchors.bottom: parent.bottom + anchors.right: parent.right + width: 80 + @onClick: self:getParent():hide() diff --git a/modules/game_console/channelswindow.otui b/modules/game_console/channelswindow.otui new file mode 100644 index 0000000..94e4d40 --- /dev/null +++ b/modules/game_console/channelswindow.otui @@ -0,0 +1,65 @@ +ChannelListLabel < Label + font: verdana-11px-monochrome + background-color: alpha + text-offset: 2 0 + focusable: true + + $focus: + background-color: #ffffff22 + color: #ffffff + +MainWindow + id: channelsWindow + !text: tr('Channels') + size: 250 238 + @onEscape: self:destroy() + + TextList + id: channelList + vertical-scrollbar: channelsScrollBar + anchors.fill: parent + anchors.bottom: next.top + margin-bottom: 10 + padding: 1 + focusable: false + + Label + id: openPrivateChannelWithLabel + !text: tr('Open a private message channel:') + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: next.top + text-align: center + margin-bottom: 2 + + TextEdit + id: openPrivateChannelWith + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: next.top + margin-bottom: 10 + + Button + id: buttonOpen + !text: tr('Open') + width: 64 + anchors.right: next.left + anchors.bottom: parent.bottom + margin-right: 10 + @onClick: self:getParent():onEnter() + + Button + id: buttonCancel + !text: tr('Cancel') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: self:getParent():destroy() + + VerticalScrollBar + id: channelsScrollBar + anchors.top: channelList.top + anchors.bottom: channelList.bottom + anchors.right: channelList.right + step: 14 + pixels-scroll: true diff --git a/modules/game_console/communicationwindow.otui b/modules/game_console/communicationwindow.otui new file mode 100644 index 0000000..c5e44c0 --- /dev/null +++ b/modules/game_console/communicationwindow.otui @@ -0,0 +1,206 @@ +IgnoreListLabel < Label + font: verdana-11px-monochrome + background-color: alpha + text-offset: 2 0 + focusable: true + phantom: false + + $focus: + background-color: #ffffff22 + color: #ffffff + +WhiteListLabel < Label + font: verdana-11px-monochrome + background-color: alpha + text-offset: 2 0 + focusable: true + phantom: false + + $focus: + background-color: #ffffff22 + color: #ffffff + + +MainWindow + id: communicationWindow + !text: tr('Ignore List') + size: 515 410 + @onEscape: self:destroy() + + CheckBox + id: checkboxUseIgnoreList + !text: tr('Activate ignorelist') + anchors.left: parent.left + anchors.top: parent.top + width: 180 + + Label + !text: tr('Ignored Players:') + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 10 + + TextList + id: ignoreList + vertical-scrollbar: ignoreListScrollBar + anchors.left: parent.left + anchors.top: prev.bottom + height: 150 + width: 230 + margin-bottom: 10 + margin-top: 3 + padding: 1 + focusable: false + + TextEdit + id: ignoreNameEdit + anchors.top: prev.bottom + anchors.left: parent.left + width: 110 + margin-top: 5 + + Button + id: buttonIgnoreAdd + !text: tr('Add') + width: 48 + height: 20 + margin-left: 5 + anchors.top: prev.top + anchors.left: prev.right + + Button + id: buttonIgnoreRemove + !text: tr('Remove') + width: 64 + height: 20 + margin-left: 5 + anchors.top: prev.top + anchors.left: prev.right + + Label + !text: tr('Global ignore settings') + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 20 + + CheckBox + id: checkboxIgnorePrivateMessages + !text: tr('Ignore Private Messages') + anchors.left: parent.left + anchors.top: prev.bottom + width: 180 + margin-top: 5 + + CheckBox + id: checkboxIgnoreYelling + !text: tr('Ignore Yelling') + anchors.left: parent.left + anchors.top: prev.bottom + width: 180 + margin-top: 5 + + CheckBox + id: checkboxUseWhiteList + !text: tr('Activate whitelist') + anchors.top: parent.top + anchors.left: ignoreList.right + margin-left: 20 + width: 180 + + Label + !text: tr('Allowed Players:') + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 10 + + TextList + id: whiteList + vertical-scrollbar: whiteListScrollBar + anchors.left: prev.left + anchors.top: prev.bottom + height: 150 + width: 230 + margin-bottom: 10 + margin-top: 3 + padding: 1 + focusable: false + + TextEdit + id: whitelistNameEdit + anchors.top: prev.bottom + anchors.left: prev.left + width: 110 + margin-top: 5 + + Button + id: buttonWhitelistAdd + !text: tr('Add') + width: 48 + height: 20 + margin-left: 5 + anchors.top: prev.top + anchors.left: prev.right + + Button + id: buttonWhitelistRemove + !text: tr('Remove') + width: 64 + height: 20 + margin-left: 5 + anchors.top: prev.top + anchors.left: prev.right + + Label + !text: tr('Global whitelist settings') + anchors.left: whiteList.left + anchors.top: prev.bottom + margin-top: 20 + + CheckBox + id: checkboxAllowVIPs + !text: tr('Allow VIPs to message you') + anchors.left: prev.left + anchors.top: prev.bottom + width: 180 + margin-top: 5 + + Panel + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 30 + + Panel + size: 160 30 + anchors.horizontalCenter: parent.horizontalCenter + + Button + id: buttonSave + !text: tr('Save') + width: 75 + anchors.top: parent.top + anchors.left: parent.left + + Button + id: buttonCancel + !text: tr('Cancel') + width: 75 + anchors.top: parent.top + anchors.left: prev.right + margin-left: 10 + + VerticalScrollBar + id: ignoreListScrollBar + anchors.top: ignoreList.top + anchors.bottom: ignoreList.bottom + anchors.right: ignoreList.right + step: 14 + pixels-scroll: true + + VerticalScrollBar + id: whiteListScrollBar + anchors.top: whiteList.top + anchors.bottom: whiteList.bottom + anchors.right: whiteList.right + step: 14 + pixels-scroll: true diff --git a/modules/game_console/console.lua b/modules/game_console/console.lua new file mode 100644 index 0000000..b413c83 --- /dev/null +++ b/modules/game_console/console.lua @@ -0,0 +1,1538 @@ +SpeakTypesSettings = { + none = {}, + say = { speakType = MessageModes.Say, color = '#FFFF00' }, + whisper = { speakType = MessageModes.Whisper, color = '#FFFF00' }, + yell = { speakType = MessageModes.Yell, color = '#FFFF00' }, + broadcast = { speakType = MessageModes.GamemasterBroadcast, color = '#F55E5E' }, + private = { speakType = MessageModes.PrivateTo, color = '#5FF7F7', private = true }, + privateRed = { speakType = MessageModes.GamemasterTo, color = '#F55E5E', private = true }, + privatePlayerToPlayer = { speakType = MessageModes.PrivateTo, color = '#9F9DFD', private = true }, + privatePlayerToNpc = { speakType = MessageModes.NpcTo, color = '#9F9DFD', private = true, npcChat = true }, + privateNpcToPlayer = { speakType = MessageModes.NpcFrom, color = '#5FF7F7', private = true, npcChat = true }, + channelYellow = { speakType = MessageModes.Channel, color = '#FFFF00' }, + channelWhite = { speakType = MessageModes.ChannelManagement, color = '#FFFFFF' }, + channelRed = { speakType = MessageModes.GamemasterChannel, color = '#F55E5E' }, + channelOrange = { speakType = MessageModes.ChannelHighlight, color = '#F6A731' }, + monsterSay = { speakType = MessageModes.MonsterSay, color = '#FE6500', hideInConsole = true}, + monsterYell = { speakType = MessageModes.MonsterYell, color = '#FE6500', hideInConsole = true}, + rvrAnswerFrom = { speakType = MessageModes.RVRAnswer, color = '#FE6500' }, + rvrAnswerTo = { speakType = MessageModes.RVRAnswer, color = '#FE6500' }, + rvrContinue = { speakType = MessageModes.RVRContinue, color = '#FFFF00' }, +} + +SpeakTypes = { + [MessageModes.Say] = SpeakTypesSettings.say, + [MessageModes.Whisper] = SpeakTypesSettings.whisper, + [MessageModes.Yell] = SpeakTypesSettings.yell, + [MessageModes.GamemasterBroadcast] = SpeakTypesSettings.broadcast, + [MessageModes.PrivateFrom] = SpeakTypesSettings.private, + [MessageModes.GamemasterPrivateFrom] = SpeakTypesSettings.privateRed, + [MessageModes.NpcTo] = SpeakTypesSettings.privatePlayerToNpc, + [MessageModes.NpcFrom] = SpeakTypesSettings.privateNpcToPlayer, + [MessageModes.Channel] = SpeakTypesSettings.channelYellow, + [MessageModes.ChannelManagement] = SpeakTypesSettings.channelWhite, + [MessageModes.GamemasterChannel] = SpeakTypesSettings.channelRed, + [MessageModes.ChannelHighlight] = SpeakTypesSettings.channelOrange, + [MessageModes.MonsterSay] = SpeakTypesSettings.monsterSay, + [MessageModes.MonsterYell] = SpeakTypesSettings.monsterYell, + [MessageModes.RVRChannel] = SpeakTypesSettings.channelWhite, + [MessageModes.RVRContinue] = SpeakTypesSettings.rvrContinue, + [MessageModes.RVRAnswer] = SpeakTypesSettings.rvrAnswerFrom, + [MessageModes.NpcFromStartBlock] = SpeakTypesSettings.privateNpcToPlayer, + + -- ignored types + [MessageModes.Spell] = SpeakTypesSettings.none, + [MessageModes.BarkLow] = SpeakTypesSettings.none, + [MessageModes.BarkLoud] = SpeakTypesSettings.none, +} + +SayModes = { + [1] = { speakTypeDesc = 'whisper', icon = '/images/game/console/whisper' }, + [2] = { speakTypeDesc = 'say', icon = '/images/game/console/say' }, + [3] = { speakTypeDesc = 'yell', icon = '/images/game/console/yell' } +} + +ChannelEventFormats = { + [ChannelEvent.Join] = '%s joined the channel.', + [ChannelEvent.Leave] = '%s left the channel.', + [ChannelEvent.Invite] = '%s has been invited to the channel.', + [ChannelEvent.Exclude] = '%s has been removed from the channel.', +} + +MAX_HISTORY = 500 +MAX_LINES = 100 +HELP_CHANNEL = 9 + +consolePanel = nil +consoleContentPanel = nil +consoleTabBar = nil +consoleTextEdit = nil +consoleToggleChat = nil +channels = nil +channelsWindow = nil +communicationWindow = nil +ownPrivateName = nil +messageHistory = {} +currentMessageIndex = 0 +ignoreNpcMessages = false +defaultTab = nil +serverTab = nil +violationsChannelId = nil +violationWindow = nil +violationReportTab = nil +ignoredChannels = {} +filters = {} + +floatingMode = false + +local communicationSettings = { + useIgnoreList = true, + useWhiteList = true, + privateMessages = false, + yelling = false, + allowVIPs = false, + ignoredPlayers = {}, + whitelistedPlayers = {} +} + +function init() + connect(g_game, { + onTalk = onTalk, + onChannelList = onChannelList, + onOpenChannel = onOpenChannel, + onOpenPrivateChannel = onOpenPrivateChannel, + onOpenOwnPrivateChannel = onOpenOwnPrivateChannel, + onCloseChannel = onCloseChannel, + onRuleViolationChannel = onRuleViolationChannel, + onRuleViolationRemove = onRuleViolationRemove, + onRuleViolationCancel = onRuleViolationCancel, + onRuleViolationLock = onRuleViolationLock, + onGameStart = online, + onGameEnd = offline, + onChannelEvent = onChannelEvent, + }) + + consolePanel = g_ui.loadUI('console', modules.game_interface.getBottomPanel()) + consoleTextEdit = consolePanel:getChildById('consoleTextEdit') + consoleContentPanel = consolePanel:getChildById('consoleContentPanel') + consoleTabBar = consolePanel:getChildById('consoleTabBar') + consoleTabBar:setContentWidget(consoleContentPanel) + channels = {} + + consolePanel.onDragEnter = onDragEnter + consolePanel.onDragLeave = onDragLeave + consolePanel.onDragMove = onDragMove + consoleTabBar.onDragEnter = onDragEnter + consoleTabBar.onDragLeave = onDragLeave + consoleTabBar.onDragMove = onDragMove + + consolePanel.onKeyPress = function(self, keyCode, keyboardModifiers) + if not (keyboardModifiers == KeyboardCtrlModifier and keyCode == KeyC) then return false end + + local tab = consoleTabBar:getCurrentTab() + if not tab then return false end + + local selection = tab.tabPanel:getChildById('consoleBuffer').selectionText + if not selection then return false end + + g_window.setClipboardText(selection) + return true + end + + g_keyboard.bindKeyPress('Shift+Up', function() navigateMessageHistory(1) end, consolePanel) + g_keyboard.bindKeyPress('Shift+Down', function() navigateMessageHistory(-1) end, consolePanel) + g_keyboard.bindKeyPress('Tab', function() consoleTabBar:selectNextTab() end, consolePanel) + g_keyboard.bindKeyPress('Shift+Tab', function() consoleTabBar:selectPrevTab() end, consolePanel) + g_keyboard.bindKeyDown('Enter', sendCurrentMessage, consolePanel) + g_keyboard.bindKeyPress('Ctrl+A', function() consoleTextEdit:clearText() end, consolePanel) + + -- apply buttom functions after loaded + consoleTabBar:setNavigation(consolePanel:getChildById('prevChannelButton'), consolePanel:getChildById('nextChannelButton')) + consoleTabBar.onTabChange = onTabChange + + -- tibia like hotkeys + g_keyboard.bindKeyDown('Ctrl+O', g_game.requestChannels) + g_keyboard.bindKeyDown('Ctrl+E', removeCurrentTab) + g_keyboard.bindKeyDown('Ctrl+H', openHelp) + + consoleToggleChat = consolePanel:getChildById('toggleChat') + load() + + if g_game.isOnline() then + online() + end +end + +function clearSelection(consoleBuffer) + for _,label in pairs(consoleBuffer:getChildren()) do + label:clearSelection() + end + consoleBuffer.selectionText = nil + consoleBuffer.selection = nil +end + +function selectAll(consoleBuffer) + clearSelection(consoleBuffer) + if consoleBuffer:getChildCount() > 0 then + local text = {} + for _,label in pairs(consoleBuffer:getChildren()) do + label:selectAll() + table.insert(text, label:getSelection()) + end + consoleBuffer.selectionText = table.concat(text, '\n') + consoleBuffer.selection = { first = consoleBuffer:getChildIndex(consoleBuffer:getFirstChild()), last = consoleBuffer:getChildIndex(consoleBuffer:getLastChild()) } + end +end + +function toggleChat() + if consoleToggleChat:isChecked() then + disableChat() + else + enableChat() + end +end + +function enableChat(temporarily) + if consoleToggleChat:isChecked() then + return consoleToggleChat:setChecked(false) + end + if not temporarily then + modules.client_options.setOption("wsadWalking", false) + end + + consoleTextEdit:setVisible(true) + consoleTextEdit:setText("") + + g_keyboard.unbindKeyDown("Space") + g_keyboard.unbindKeyDown("Enter") + + if temporarily then + local quickFunc = function() + g_keyboard.unbindKeyDown("Enter") + g_keyboard.unbindKeyDown("Escape") + disableChat(temporarily) + end + g_keyboard.bindKeyDown("Enter", quickFunc) + g_keyboard.bindKeyDown("Escape", quickFunc) + end + + modules.game_walking.disableWSAD() + + consoleToggleChat:setTooltip(tr("Disable chat mode, allow to walk using ASDW")) +end + +function disableChat() + if not consoleToggleChat:isChecked() then + return consoleToggleChat:setChecked(true) + end + if not temporarily then + modules.client_options.setOption("wsadWalking", true) + end + + consoleTextEdit:setVisible(false) + consoleTextEdit:setText("") + + local quickFunc = function() + if consoleToggleChat:isChecked() then + consoleToggleChat:setChecked(false) + end + enableChat(true) + end + g_keyboard.bindKeyDown("Space", quickFunc) + g_keyboard.bindKeyDown("Enter", quickFunc) + + modules.game_walking.enableWSAD() + + consoleToggleChat:setTooltip(tr("Enable chat mode")) +end + +function isChatEnabled() + return consoleTextEdit:isVisible() +end + +function terminate() + save() + disconnect(g_game, { + onTalk = onTalk, + onChannelList = onChannelList, + onOpenChannel = onOpenChannel, + onOpenPrivateChannel = onOpenPrivateChannel, + onOpenOwnPrivateChannel = onOpenPrivateChannel, + onCloseChannel = onCloseChannel, + onRuleViolationChannel = onRuleViolationChannel, + onRuleViolationRemove = onRuleViolationRemove, + onRuleViolationCancel = onRuleViolationCancel, + onRuleViolationLock = onRuleViolationLock, + onGameStart = online, + onGameEnd = offline, + onChannelEvent = onChannelEvent, + }) + + if g_game.isOnline() then clear() end + + g_keyboard.unbindKeyDown('Ctrl+O') + g_keyboard.unbindKeyDown('Ctrl+E') + g_keyboard.unbindKeyDown('Ctrl+H') + + saveCommunicationSettings() + + if channelsWindow then + channelsWindow:destroy() + end + + if communicationWindow then + communicationWindow:destroy() + end + + if violationWindow then + violationWindow:destroy() + end + + consoleTabBar = nil + consoleContentPanel = nil + consoleToggleChat = nil + consoleTextEdit = nil + + consolePanel:destroy() + consolePanel = nil + ownPrivateName = nil + + Console = nil +end + +function save() + local settings = {} + settings.messageHistory = messageHistory + g_settings.setNode('game_console', settings) +end + +function load() + local settings = g_settings.getNode('game_console') + if settings then + messageHistory = settings.messageHistory or {} + end + loadCommunicationSettings() +end + +function onTabChange(tabBar, tab) + if tab == defaultTab or tab == serverTab then + consolePanel:getChildById('closeChannelButton'):disable() + else + consolePanel:getChildById('closeChannelButton'):enable() + end +end + +function clear() + -- save last open channels + local lastChannelsOpen = g_settings.getNode('lastChannelsOpen') or {} + local char = g_game.getCharacterName() + local savedChannels = {} + local set = false + for channelId, channelName in pairs(channels) do + if type(channelId) == 'number' then + savedChannels[channelName] = channelId + set = true + end + end + if set then + lastChannelsOpen[char] = savedChannels + else + lastChannelsOpen[char] = nil + end + g_settings.setNode('lastChannelsOpen', lastChannelsOpen) + + -- close channels + for _, channelName in pairs(channels) do + local tab = consoleTabBar:getTab(channelName) + consoleTabBar:removeTab(tab) + end + channels = {} + + consoleTabBar:removeTab(defaultTab) + defaultTab = nil + consoleTabBar:removeTab(serverTab) + serverTab = nil + + local npcTab = consoleTabBar:getTab('NPCs') + if npcTab then + consoleTabBar:removeTab(npcTab) + npcTab = nil + end + + if violationReportTab then + consoleTabBar:removeTab(violationReportTab) + violationReportTab = nil + end + + consoleTextEdit:clearText() + + if violationWindow then + violationWindow:destroy() + violationWindow = nil + end + + if channelsWindow then + channelsWindow:destroy() + channelsWindow = nil + end +end + +function switchMode(floating) + if floating then + consolePanel:setImageColor('#ffffff88') + consolePanel:removeAnchor(AnchorRight) + consolePanel:setWidth(600) + consolePanel:setDraggable(true) + consoleTabBar:setDraggable(true) + if not floatingMode then + local savedMargin = g_settings.get("consoleLeftMargin") + local newMargin = 150 + if savedMargin and #savedMargin > 0 then + newMargin = tonumber(savedMargin) + end + newMargin = math.max(0, newMargin) + newMargin = math.min(consolePanel:getParent():getWidth() - consolePanel:getWidth(), newMargin) + consolePanel:setMarginLeft(newMargin) + end + else + consolePanel:setImageColor('white') + consolePanel:addAnchor(AnchorLeft, 'parent', AnchorLeft) + consolePanel:addAnchor(AnchorRight, 'parent', AnchorRight) + consolePanel:setDraggable(false) + consoleTabBar:setDraggable(false) + consolePanel:setMarginLeft(0) + end + floatingMode = floating +end + +function onDragEnter(widget, pos) + return floatingMode +end + +function onDragMove(widget, pos, moved) + if not floatingMode then + return + end + local newMargin = consolePanel:getMarginLeft() + moved.x + newMargin = math.max(0, newMargin) + newMargin = math.min(consolePanel:getParent():getWidth() - consolePanel:getWidth(), newMargin) + consolePanel:setMarginLeft(newMargin) + g_settings.set("consoleLeftMargin", newMargin) + return true +end + +function onDragLeave(widget, pos) + return floatingMode +end + +function clearChannel(consoleTabBar) + consoleTabBar:getCurrentTab().tabPanel:getChildById('consoleBuffer'):destroyChildren() +end + +function setTextEditText(text) + consoleTextEdit:setText(text) + consoleTextEdit:setCursorPos(-1) +end + +function openHelp() + local helpChannel = 9 + if g_game.getClientVersion() <= 810 then + helpChannel = 8 + end + g_game.joinChannel(helpChannel) +end + +function openPlayerReportRuleViolationWindow() + if violationWindow or violationReportTab then return end + violationWindow = g_ui.loadUI('violationwindow', rootWidget) + violationWindow.onEscape = function() + violationWindow:destroy() + violationWindow = nil + end + violationWindow.onEnter = function() + local text = violationWindow:getChildById('text'):getText() + g_game.talkChannel(MessageModes.RVRChannel, 0, text) + violationReportTab = addTab(tr('Report Rule') .. '...', true) + addTabText(tr('Please wait patiently for a gamemaster to reply') .. '.', SpeakTypesSettings.privateRed, violationReportTab) + addTabText(applyMessagePrefixies(g_game.getCharacterName(), 0, text), SpeakTypesSettings.say, violationReportTab, g_game.getCharacterName()) + violationReportTab.locked = true + violationWindow:destroy() + violationWindow = nil + end +end + +function addTab(name, focus) + local tab = getTab(name) + if tab then -- is channel already open + if not focus then focus = true end + else + tab = consoleTabBar:addTab(name, nil, processChannelTabMenu) + end + if focus then + consoleTabBar:selectTab(tab) + end + return tab +end + +function removeTab(tab) + if type(tab) == 'string' then + tab = consoleTabBar:getTab(tab) + end + + if tab == defaultTab or tab == serverTab then + return + end + + if tab == violationReportTab then + g_game.cancelRuleViolation() + violationReportTab = nil + elseif tab.violationChatName then + g_game.closeRuleViolation(tab.violationChatName) + elseif tab.channelId then + -- notificate the server that we are leaving the channel + for k, v in pairs(channels) do + if (k == tab.channelId) then channels[k] = nil end + end + g_game.leaveChannel(tab.channelId) + elseif tab:getText() == "NPCs" then + g_game.closeNpcChannel() + end + + consoleTabBar:removeTab(tab) +end + +function removeCurrentTab() + removeTab(consoleTabBar:getCurrentTab()) +end + +function getTab(name) + return consoleTabBar:getTab(name) +end + +function getChannelTab(channelId) + local channel = channels[channelId] + if channel then + return getTab(channel) + end + return nil +end + +function getRuleViolationsTab() + if violationsChannelId then + return getChannelTab(violationsChannelId) + end + return nil +end + +function getCurrentTab() + return consoleTabBar:getCurrentTab() +end + +function addChannel(name, id) + channels[id] = name + local focus = not table.find(ignoredChannels, id) + local tab = addTab(name, focus) + tab.channelId = id + return tab +end + +function addPrivateChannel(receiver) + channels[receiver] = receiver + return addTab(receiver, true) +end + +function addPrivateText(text, speaktype, name, isPrivateCommand, creatureName) + local focus = false + if speaktype.npcChat then + name = 'NPCs' + focus = true + end + + local privateTab = getTab(name) + if privateTab == nil then + if (modules.client_options.getOption('showPrivateMessagesInConsole') and not focus) or (isPrivateCommand and not privateTab) then + privateTab = defaultTab + else + privateTab = addTab(name, focus) + channels[name] = name + end + privateTab.npcChat = speaktype.npcChat + elseif focus then + consoleTabBar:selectTab(privateTab) + end + addTabText(text, speaktype, privateTab, creatureName) +end + +function addText(text, speaktype, tabName, creatureName) + local tab = getTab(tabName) + if tab ~= nil then + addTabText(text, speaktype, tab, creatureName) + end +end + +-- Contains letter width for font "verdana-11px-antialised" as console is based on it +local letterWidth = { -- New line (10) and Space (32) have width 1 because they are printed and not replaced with spacer + [10] = 1, [32] = 1, [33] = 3, [34] = 6, [35] = 8, [36] = 7, [37] = 13, [38] = 9, [39] = 3, [40] = 5, [41] = 5, [42] = 6, [43] = 8, [44] = 4, [45] = 5, [46] = 3, [47] = 8, + [48] = 7, [49] = 6, [50] = 7, [51] = 7, [52] = 7, [53] = 7, [54] = 7, [55] = 7, [56] = 7, [57] = 7, [58] = 3, [59] = 4, [60] = 8, [61] = 8, [62] = 8, [63] = 6, + [64] = 10, [65] = 9, [66] = 7, [67] = 7, [68] = 8, [69] = 7, [70] = 7, [71] = 8, [72] = 8, [73] = 5, [74] = 5, [75] = 7, [76] = 7, [77] = 9, [78] = 8, [79] = 8, + [80] = 7, [81] = 8, [82] = 8, [83] = 7, [84] = 8, [85] = 8, [86] = 8, [87] = 12, [88] = 8, [89] = 8, [90] = 7, [91] = 5, [92] = 8, [93] = 5, [94] = 9, [95] = 8, + [96] = 5, [97] = 7, [98] = 7, [99] = 6, [100] = 7, [101] = 7, [102] = 5, [103] = 7, [104] = 7, [105] = 3, [106] = 4, [107] = 7, [108] = 3, [109] = 11, [110] = 7, + [111] = 7, [112] = 7, [113] = 7, [114] = 6, [115] = 6, [116] = 5, [117] = 7, [118] = 8, [119] = 10, [120] = 8, [121] = 8, [122] = 6, [123] = 7, [124] = 4, [125] = 7, [126] = 8, + [127] = 1, [128] = 7, [129] = 6, [130] = 3, [131] = 7, [132] = 6, [133] = 11, [134] = 7, [135] = 7, [136] = 7, [137] = 13, [138] = 7, [139] = 4, [140] = 11, [141] = 6, [142] = 6, + [143] = 6, [144] = 6, [145] = 4, [146] = 3, [147] = 7, [148] = 6, [149] = 6, [150] = 7, [151] = 10, [152] = 7, [153] = 10, [154] = 6, [155] = 5, [156] = 11, [157] = 6, [158] = 6, + [159] = 8, [160] = 4, [161] = 3, [162] = 7, [163] = 7, [164] = 7, [165] = 8, [166] = 4, [167] = 7, [168] = 6, [169] = 10, [170] = 6, [171] = 8, [172] = 8, [173] = 16, [174] = 10, + [175] = 8, [176] = 5, [177] = 8, [178] = 5, [179] = 5, [180] = 6, [181] = 7, [182] = 7, [183] = 3, [184] = 5, [185] = 6, [186] = 6, [187] = 8, [188] = 12, [189] = 12, [190] = 12, + [191] = 6, [192] = 9, [193] = 9, [194] = 9, [195] = 9, [196] = 9, [197] = 9, [198] = 11, [199] = 7, [200] = 7, [201] = 7, [202] = 7, [203] = 7, [204] = 5, [205] = 5, [206] = 6, + [207] = 5, [208] = 8, [209] = 8, [210] = 8, [211] = 8, [212] = 8, [213] = 8, [214] = 8, [215] = 8, [216] = 8, [217] = 8, [218] = 8, [219] = 8, [220] = 8, [221] = 8, [222] = 7, + [223] = 7, [224] = 7, [225] = 7, [226] = 7, [227] = 7, [228] = 7, [229] = 7, [230] = 11, [231] = 6, [232] = 7, [233] = 7, [234] = 7, [235] = 7, [236] = 3, [237] = 4, [238] = 4, + [239] = 4, [240] = 7, [241] = 7, [242] = 7, [243] = 7, [244] = 7, [245] = 7, [246] = 7, [247] = 9, [248] = 7, [249] = 7, [250] = 7, [251] = 7, [252] = 7, [253] = 8, [254] = 7, [255] = 8 +} + +-- Return information about start, end in the string and the highlighted words +function getHighlightedText(text) + local tmpData = {} + + repeat + local tmp = {string.find(text, "{([^}]+)}", tmpData[#tmpData-1])} + for _, v in pairs(tmp) do + table.insert(tmpData, v) + end + until not(string.find(text, "{([^}]+)}", tmpData[#tmpData-1])) + + return tmpData +end + +function addTabText(text, speaktype, tab, creatureName) + if not tab or tab.locked or not text or #text == 0 then return end + + if modules.client_options.getOption('showTimestampsInConsole') then + text = os.date('%H:%M') .. ' ' .. text + end + + local panel = consoleTabBar:getTabPanel(tab) + local consoleBuffer = panel:getChildById('consoleBuffer') + local label = g_ui.createWidget('ConsoleLabel', consoleBuffer) + label:setId('consoleLabel' .. consoleBuffer:getChildCount()) + label:setText(text) + label:setColor(speaktype.color) + consoleTabBar:blinkTab(tab) + + -- Overlay for consoleBuffer which shows highlighted words only + + if speaktype.npcChat and (g_game.getCharacterName() ~= creatureName or g_game.getCharacterName() == 'Account Manager') then + local highlightData = getHighlightedText(text) + if #highlightData > 0 then + local labelHighlight = g_ui.createWidget('ConsolePhantomLabel', label) + labelHighlight:fill('parent') + + labelHighlight:setId('consoleLabelHighlight' .. consoleBuffer:getChildCount()) + labelHighlight:setColor("#1f9ffe") + + -- Remove the curly braces + for i = 1, #highlightData / 3 do + local dataBlock = { _start = highlightData[(i-1)*3+1], _end = highlightData[(i-1)*3+2], words = highlightData[(i-1)*3+3] } + text = text:gsub("%{(.-)%}", dataBlock.words, 1) + + -- Recalculate positions as braces are removed + highlightData[(i-1)*3+1] = dataBlock._start - ((i-1) * 2) + highlightData[(i-1)*3+2] = dataBlock._end - (1 + (i-1) * 2) + end + label:setText(text) + + -- Calculate the positions of the highlighted text and fill with string.char(127) [Width: 1] + local drawText = label:getDrawText() + local tmpText = "" + for i = 1, #highlightData / 3 do + local dataBlock = { _start = highlightData[(i-1)*3+1], _end = highlightData[(i-1)*3+2], words = highlightData[(i-1)*3+3] } + local lastBlockEnd = (highlightData[(i-2)*3+2] or 1) + + for letter = lastBlockEnd, dataBlock._start-1 do + local tmpChar = string.byte(drawText:sub(letter, letter)) + local fillChar = (tmpChar == 10 or tmpChar == 32) and string.char(tmpChar) or string.char(127) + + tmpText = tmpText .. string.rep(fillChar, letterWidth[tmpChar]) + end + tmpText = tmpText .. dataBlock.words + end + + -- Fill the highlight label to the same size as default label + local finalBlockEnd = (highlightData[(#highlightData/3-1)*3+2] or 1) + for letter = finalBlockEnd, drawText:len() do + local tmpChar = string.byte(drawText:sub(letter, letter)) + local fillChar = (tmpChar == 10 or tmpChar == 32) and string.char(tmpChar) or string.char(127) + + tmpText = tmpText .. string.rep(fillChar, letterWidth[tmpChar]) + end + + labelHighlight:setText(tmpText) + end + end + + label.name = creatureName + consoleBuffer.onMouseRelease = function(self, mousePos, mouseButton) + processMessageMenu(mousePos, mouseButton, nil, nil, nil, tab) + end + label.onMouseRelease = function(self, mousePos, mouseButton) + processMessageMenu(mousePos, mouseButton, creatureName, text, self, tab) + end + label.onMousePress = function(self, mousePos, button) + if button == MouseLeftButton then clearSelection(consoleBuffer) end + end + label.onDragEnter = function(self, mousePos) + clearSelection(consoleBuffer) + return true + end + label.onDragLeave = function(self, droppedWidget, mousePos) + local text = {} + for selectionChild = consoleBuffer.selection.first, consoleBuffer.selection.last do + local label = self:getParent():getChildByIndex(selectionChild) + table.insert(text, label:getSelection()) + end + consoleBuffer.selectionText = table.concat(text, '\n') + return true + end + label.onDragMove = function(self, mousePos, mouseMoved) + local parent = self:getParent() + local parentRect = parent:getPaddingRect() + local selfIndex = parent:getChildIndex(self) + local child = parent:getChildByPos(mousePos) + + -- find bonding children + if not child then + if mousePos.y < self:getY() then + for index = selfIndex - 1, 1, -1 do + local label = parent:getChildByIndex(index) + if label:getY() + label:getHeight() > parentRect.y then + if (mousePos.y >= label:getY() and mousePos.y <= label:getY() + label:getHeight()) or index == 1 then + child = label + break + end + else + child = parent:getChildByIndex(index + 1) + break + end + end + elseif mousePos.y > self:getY() + self:getHeight() then + for index = selfIndex + 1, parent:getChildCount(), 1 do + local label = parent:getChildByIndex(index) + if label:getY() < parentRect.y + parentRect.height then + if (mousePos.y >= label:getY() and mousePos.y <= label:getY() + label:getHeight()) or index == parent:getChildCount() then + child = label + break + end + else + child = parent:getChildByIndex(index - 1) + break + end + end + else + child = self + end + end + + if not child then return false end + + local childIndex = parent:getChildIndex(child) + + -- remove old selection + clearSelection(consoleBuffer) + + -- update self selection + local textBegin = self:getTextPos(self:getLastClickPosition()) + local textPos = self:getTextPos(mousePos) + self:setSelection(textBegin, textPos) + + consoleBuffer.selection = { first = math.min(selfIndex, childIndex), last = math.max(selfIndex, childIndex) } + + -- update siblings selection + if child ~= self then + for selectionChild = consoleBuffer.selection.first + 1, consoleBuffer.selection.last - 1 do + parent:getChildByIndex(selectionChild):selectAll() + end + + local textPos = child:getTextPos(mousePos) + if childIndex > selfIndex then + child:setSelection(0, textPos) + else + child:setSelection(string.len(child:getText()), textPos) + end + end + + return true + end + + if consoleBuffer:getChildCount() > MAX_LINES then + local child = consoleBuffer:getFirstChild() + clearSelection(consoleBuffer) + child:destroy() + end +end + +function removeTabLabelByName(tab, name) + local panel = consoleTabBar:getTabPanel(tab) + local consoleBuffer = panel:getChildById('consoleBuffer') + for _,label in pairs(consoleBuffer:getChildren()) do + if label.name == name then + label:destroy() + end + end +end + +function processChannelTabMenu(tab, mousePos, mouseButton) + local menu = g_ui.createWidget('PopupMenu') + menu:setGameMenu(true) + + local worldName = g_game.getWorldName() + local characterName = g_game.getCharacterName() + channelName = tab:getText() + if tab ~= defaultTab and tab ~= serverTab then + menu:addOption(tr('Close'), function() removeTab(channelName) end) + --menu:addOption(tr('Show Server Messages'), function() --[[TODO]] end) + menu:addSeparator() + end + + if consoleTabBar:getCurrentTab() == tab then + menu:addOption(tr('Clear Messages'), function() clearChannel(consoleTabBar) end) + menu:addOption(tr('Save Messages'), function() + local panel = consoleTabBar:getTabPanel(tab) + local consoleBuffer = panel:getChildById('consoleBuffer') + local lines = {} + for _,label in pairs(consoleBuffer:getChildren()) do + table.insert(lines, label:getText()) + end + + local filename = worldName .. ' - ' .. characterName .. ' - ' .. channelName .. '.txt' + local filepath = '/user_dir/' .. filename + + -- extra information at the beginning + table.insert(lines, 1, os.date('\nChannel saved at %a %b %d %H:%M:%S %Y')) + + if g_resources.fileExists(filepath) then + table.insert(lines, 1, protectedcall(g_resources.readFileContents, filepath) or '') + end + + g_resources.writeFileContents(filepath, table.concat(lines, '\n')) + modules.game_textmessage.displayStatusMessage(tr('Channel appended to %s', filename)) + end) + end + + menu:display(mousePos) +end + +function processMessageMenu(mousePos, mouseButton, creatureName, text, label, tab) + if mouseButton == MouseRightButton then + local menu = g_ui.createWidget('PopupMenu') + menu:setGameMenu(true) + if creatureName and #creatureName > 0 then + if creatureName ~= g_game.getCharacterName() then + menu:addOption(tr('Message to ' .. creatureName), function () g_game.openPrivateChannel(creatureName) end) + if not g_game.getLocalPlayer():hasVip(creatureName) then + menu:addOption(tr('Add to VIP list'), function () g_game.addVip(creatureName) end) + end + if modules.game_console.getOwnPrivateTab() then + menu:addSeparator() + menu:addOption(tr('Invite to private chat'), function() g_game.inviteToOwnChannel(creatureName) end) + menu:addOption(tr('Exclude from private chat'), function() g_game.excludeFromOwnChannel(creatureName) end) + end + if isIgnored(creatureName) then + menu:addOption(tr('Unignore') .. ' ' .. creatureName, function() removeIgnoredPlayer(creatureName) end) + else + menu:addOption(tr('Ignore') .. ' ' .. creatureName, function() addIgnoredPlayer(creatureName) end) + end + menu:addSeparator() + end + if modules.game_ruleviolation.hasWindowAccess() then + menu:addOption(tr('Rule Violation'), function() modules.game_ruleviolation.show(creatureName, text:match('.+%:%s(.+)')) end) + menu:addSeparator() + end + + menu:addOption(tr('Copy name'), function () g_window.setClipboardText(creatureName) end) + end + local selection = tab.tabPanel:getChildById('consoleBuffer').selectionText + if selection and #selection > 0 then + menu:addOption(tr('Copy'), function() g_window.setClipboardText(selection) end, '(Ctrl+C)') + end + if text then + menu:addOption(tr('Copy message'), function() g_window.setClipboardText(text) end) + end + menu:addOption(tr('Select all'), function() selectAll(tab.tabPanel:getChildById('consoleBuffer')) end) + if tab.violations and creatureName then + menu:addSeparator() + menu:addOption(tr('Process') .. ' ' .. creatureName, function() processViolation(creatureName, text) end) + menu:addOption(tr('Remove') .. ' ' .. creatureName, function() g_game.closeRuleViolation(creatureName) end) + end + menu:display(mousePos) + end +end + +function sendCurrentMessage() + local message = consoleTextEdit:getText() + if #message == 0 then return end + consoleTextEdit:clearText() + + -- send message + sendMessage(message) +end + +function addFilter(filter) + table.insert(filters, filter) +end + +function removeFilter(filter) + table.removevalue(filters, filter) +end + +function sendMessage(message, tab) + local tab = tab or getCurrentTab() + if not tab then return end + + for k,func in pairs(filters) do + if func(message) then + return true + end + end + + -- when talking on server log, the message goes to default channel + local name = tab:getText() + if tab == serverTab or tab == getRuleViolationsTab() then + tab = defaultTab + name = defaultTab:getText() + end + + -- handling chat commands + local channel = tab.channelId + local originalMessage = message + local chatCommandSayMode + local chatCommandPrivate + local chatCommandPrivateReady + local chatCommandMessage + + -- player used yell command + chatCommandMessage = message:match("^%#[y|Y] (.*)") + if chatCommandMessage ~= nil then + chatCommandSayMode = 'yell' + channel = 0 + message = chatCommandMessage + end + + -- player used whisper + chatCommandMessage = message:match("^%#[w|W] (.*)") + if chatCommandMessage ~= nil then + chatCommandSayMode = 'whisper' + message = chatCommandMessage + channel = 0 + end + + -- player say + chatCommandMessage = message:match("^%#[s|S] (.*)") + if chatCommandMessage ~= nil then + chatCommandSayMode = 'say' + message = chatCommandMessage + channel = 0 + end + + -- player red talk on channel + chatCommandMessage = message:match("^%#[c|C] (.*)") + if chatCommandMessage ~= nil then + chatCommandSayMode = 'channelRed' + message = chatCommandMessage + end + + -- player broadcast + chatCommandMessage = message:match("^%#[b|B] (.*)") + if chatCommandMessage ~= nil then + chatCommandSayMode = 'broadcast' + message = chatCommandMessage + channel = 0 + end + + local findIni, findEnd, chatCommandInitial, chatCommandPrivate, chatCommandEnd, chatCommandMessage = message:find("([%*%@])(.+)([%*%@])(.*)") + if findIni ~= nil and findIni == 1 then -- player used private chat command + if chatCommandInitial == chatCommandEnd then + chatCommandPrivateRepeat = false + if chatCommandInitial == "*" then + setTextEditText('*'.. chatCommandPrivate .. '* ') + end + message = chatCommandMessage:trim() + chatCommandPrivateReady = true + end + end + + message = message:gsub("^(%s*)(.*)","%2") -- remove space characters from message init + if #message == 0 then return end + + -- add new command to history + currentMessageIndex = 0 + if #messageHistory == 0 or messageHistory[#messageHistory] ~= originalMessage then + table.insert(messageHistory, originalMessage) + if #messageHistory > MAX_HISTORY then + table.remove(messageHistory, 1) + end + end + + local speaktypedesc + if (channel or tab == defaultTab) and not chatCommandPrivateReady then + if tab == defaultTab then + speaktypedesc = chatCommandSayMode or SayModes[consolePanel:getChildById('sayModeButton').sayMode].speakTypeDesc + if speaktypedesc ~= 'say' then sayModeChange(2) end -- head back to say mode + else + speaktypedesc = chatCommandSayMode or 'channelYellow' + end + + g_game.talkChannel(SpeakTypesSettings[speaktypedesc].speakType, channel, message) + return + else + local isPrivateCommand = false + local priv = true + local tabname = name + local dontAdd = false + if chatCommandPrivateReady then + speaktypedesc = 'privatePlayerToPlayer' + name = chatCommandPrivate + isPrivateCommand = true + elseif tab.npcChat then + speaktypedesc = 'privatePlayerToNpc' + elseif tab == violationReportTab then + if violationReportTab.locked then + modules.game_textmessage.displayFailureMessage('Wait for a gamemaster reply.') + dontAdd = true + else + speaktypedesc = 'rvrContinue' + tabname = tr('Report Rule') .. '...' + end + elseif tab.violationChatName then + speaktypedesc = 'rvrAnswerTo' + name = tab.violationChatName + tabname = tab.violationChatName .. '\'...' + else + speaktypedesc = 'privatePlayerToPlayer' + end + + + local speaktype = SpeakTypesSettings[speaktypedesc] + local player = g_game.getLocalPlayer() + g_game.talkPrivate(speaktype.speakType, name, message) + if not dontAdd then + message = applyMessagePrefixies(g_game.getCharacterName(), player:getLevel(), message) + addPrivateText(message, speaktype, tabname, isPrivateCommand, g_game.getCharacterName()) + end + end +end + +function sayModeChange(sayMode) + local buttom = consolePanel:getChildById('sayModeButton') + if sayMode == nil then + sayMode = buttom.sayMode + 1 + end + + if sayMode > #SayModes then sayMode = 1 end + + buttom:setIcon(SayModes[sayMode].icon) + buttom.sayMode = sayMode +end + +function getOwnPrivateTab() + if not ownPrivateName then return end + return getTab(ownPrivateName) +end + +function setIgnoreNpcMessages(ignore) + ignoreNpcMessages = ignore +end + +function navigateMessageHistory(step) + local numCommands = #messageHistory + if numCommands > 0 then + currentMessageIndex = math.min(math.max(currentMessageIndex + step, 0), numCommands) + if currentMessageIndex > 0 then + local command = messageHistory[numCommands - currentMessageIndex + 1] + setTextEditText(command) + else + consoleTextEdit:clearText() + end + end + local player = g_game.getLocalPlayer() + if player then + player:lockWalk(200) -- lock walk for 200 ms to avoid walk during release of shift + end +end + +function applyMessagePrefixies(name, level, message) + if name and #name > 0 then + if modules.client_options.getOption('showLevelsInConsole') and level > 0 then + message = name .. ' [' .. level .. ']: ' .. message + else + message = name .. ': ' .. message + end + end + return message +end + +function onTalk(name, level, mode, message, channelId, creaturePos) + if mode == MessageModes.GamemasterBroadcast then + modules.game_textmessage.displayBroadcastMessage(name .. ': ' .. message) + return + end + + local isNpcMode = (mode == MessageModes.NpcFromStartBlock or mode == MessageModes.NpcFrom) + + if ignoreNpcMessages and isNpcMode then return end + + speaktype = SpeakTypes[mode] + + if not speaktype then + perror('unhandled onTalk message mode ' .. mode .. ': ' .. message) + return + end + + local localPlayer = g_game.getLocalPlayer() + if name ~= g_game.getCharacterName() + and isUsingIgnoreList() + and not(isUsingWhiteList()) or (isUsingWhiteList() and not(isWhitelisted(name)) and not(isAllowingVIPs() and localPlayer:hasVip(name))) then + + if mode == MessageModes.Yell and isIgnoringYelling() then + return + elseif speaktype.private and isIgnoringPrivate() and not isNpcMode then + return + elseif isIgnored(name) then + return + end + end + + if mode == MessageModes.RVRChannel then + channelId = violationsChannelId + end + + if (mode == MessageModes.Say or mode == MessageModes.Whisper or mode == MessageModes.Yell or + mode == MessageModes.Spell or mode == MessageModes.MonsterSay or mode == MessageModes.MonsterYell or + mode == MessageModes.NpcFrom or mode == MessageModes.BarkLow or mode == MessageModes.BarkLoud or + mode == MessageModes.NpcFromStartBlock) and creaturePos then + local staticText = StaticText.create() + -- Remove curly braces from screen message + local staticMessage = message + if isNpcMode then + local highlightData = getHighlightedText(staticMessage) + if #highlightData > 0 then + for i = 1, #highlightData / 3 do + local dataBlock = { _start = highlightData[(i-1)*3+1], _end = highlightData[(i-1)*3+2], words = highlightData[(i-1)*3+3] } + staticMessage = staticMessage:gsub("{"..dataBlock.words.."}", dataBlock.words) + end + end + staticText:setColor(speaktype.color) + end + staticText:addMessage(name, mode, staticMessage) + g_map.addThing(staticText, creaturePos, -1) + end + + local defaultMessage = mode <= 3 and true or false + + if speaktype == SpeakTypesSettings.none then return end + + if speaktype.hideInConsole then return end + + local composedMessage = applyMessagePrefixies(name, level, message) + + if mode == MessageModes.RVRAnswer then + violationReportTab.locked = false + addTabText(composedMessage, speaktype, violationReportTab, name) + elseif mode == MessageModes.RVRContinue then + addText(composedMessage, speaktype, name .. '\'...', name) + elseif speaktype.private then + addPrivateText(composedMessage, speaktype, name, false, name) + if modules.client_options.getOption('showPrivateMessagesOnScreen') and speaktype ~= SpeakTypesSettings.privateNpcToPlayer then + modules.game_textmessage.displayPrivateMessage(name .. ':\n' .. message) + end + else + local channel = tr('Default') + if not defaultMessage then + channel = channels[channelId] + end + + if channel then + addText(composedMessage, speaktype, channel, name) + else + -- server sent a message on a channel that is not open + pwarning('message in channel id ' .. channelId .. ' which is unknown, this is a server bug, relogin if you want to see messages in this channel') + end + end +end + +function onOpenChannel(channelId, channelName) + addChannel(channelName, channelId) +end + +function onOpenPrivateChannel(receiver) + addPrivateChannel(receiver) +end + +function onOpenOwnPrivateChannel(channelId, channelName) + local privateTab = getTab(channelName) + if privateTab == nil then + addChannel(channelName, channelId) + end + ownPrivateName = channelName +end + +function onCloseChannel(channelId) + local channel = channels[channelId] + if channel then + local tab = getTab(channel) + if tab then + consoleTabBar:removeTab(tab) + end + for k, v in pairs(channels) do + if (k == tab.channelId) then channels[k] = nil end + end + end +end + +function processViolation(name, text) + local tabname = name .. '\'...' + local tab = addTab(tabname, true) + channels[tabname] = tabname + tab.violationChatName = name + g_game.openRuleViolation(name) + addTabText(text, SpeakTypesSettings.say, tab, name) +end + +function onRuleViolationChannel(channelId) + violationsChannelId = channelId + local tab = addChannel(tr('Rule Violations'), channelId) + tab.violations = true +end + +function onRuleViolationRemove(name) + local tab = getRuleViolationsTab() + if not tab then return end + removeTabLabelByName(tab, name) +end + +function onRuleViolationCancel(name) + local tab = getTab(name .. '\'...') + if not tab then return end + addTabText(tr('%s has finished the request', name) .. '.', SpeakTypesSettings.privateRed, tab) + tab.locked = true +end + +function onRuleViolationLock() + if not violationReportTab then return end + violationReportTab.locked = false + addTabText(tr('Your request has been closed') .. '.', SpeakTypesSettings.privateRed, violationReportTab) + violationReportTab.locked = true +end + +function doChannelListSubmit() + local channelListPanel = channelsWindow:getChildById('channelList') + local openPrivateChannelWith = channelsWindow:getChildById('openPrivateChannelWith'):getText() + if openPrivateChannelWith ~= '' then + if openPrivateChannelWith:lower() ~= g_game.getCharacterName():lower() then + g_game.openPrivateChannel(openPrivateChannelWith) + else + modules.game_textmessage.displayFailureMessage('You cannot create a private chat channel with yourself.') + end + else + local selectedChannelLabel = channelListPanel:getFocusedChild() + if not selectedChannelLabel then return end + if selectedChannelLabel.channelId == 0xFFFF then + g_game.openOwnChannel() + else + g_game.leaveChannel(selectedChannelLabel.channelId) + g_game.joinChannel(selectedChannelLabel.channelId) + end + end + + channelsWindow:destroy() +end + +function onChannelList(channelList) + if channelsWindow then channelsWindow:destroy() end + channelsWindow = g_ui.displayUI('channelswindow') + local channelListPanel = channelsWindow:getChildById('channelList') + channelsWindow.onEnter = doChannelListSubmit + channelsWindow.onDestroy = function() channelsWindow = nil end + g_keyboard.bindKeyPress('Down', function() channelListPanel:focusNextChild(KeyboardFocusReason) end, channelsWindow) + g_keyboard.bindKeyPress('Up', function() channelListPanel:focusPreviousChild(KeyboardFocusReason) end, channelsWindow) + + for k,v in pairs(channelList) do + local channelId = v[1] + local channelName = v[2] + + if #channelName > 0 then + local label = g_ui.createWidget('ChannelListLabel', channelListPanel) + label.channelId = channelId + label:setText(channelName) + + label:setPhantom(false) + label.onDoubleClick = doChannelListSubmit + end + end +end + +function loadCommunicationSettings() + communicationSettings.whitelistedPlayers = {} + communicationSettings.ignoredPlayers = {} + + local ignoreNode = g_settings.getNode('IgnorePlayers') + if ignoreNode then + for i = 1, #ignoreNode do + table.insert(communicationSettings.ignoredPlayers, ignoreNode[i]) + end + end + + local whitelistNode = g_settings.getNode('WhitelistedPlayers') + if whitelistNode then + for i = 1, #whitelistNode do + table.insert(communicationSettings.whitelistedPlayers, whitelistNode[i]) + end + end + + communicationSettings.useIgnoreList = g_settings.getBoolean('UseIgnoreList') + communicationSettings.useWhiteList = g_settings.getBoolean('UseWhiteList') + communicationSettings.privateMessages = g_settings.getBoolean('IgnorePrivateMessages') + communicationSettings.yelling = g_settings.getBoolean('IgnoreYelling') + communicationSettings.allowVIPs = g_settings.getBoolean('AllowVIPs') +end + +function saveCommunicationSettings() + local tmpIgnoreList = {} + local ignoredPlayers = getIgnoredPlayers() + for i = 1, #ignoredPlayers do + table.insert(tmpIgnoreList, ignoredPlayers[i]) + end + + local tmpWhiteList = {} + local whitelistedPlayers = getWhitelistedPlayers() + for i = 1, #whitelistedPlayers do + table.insert(tmpWhiteList, whitelistedPlayers[i]) + end + + g_settings.set('UseIgnoreList', communicationSettings.useIgnoreList) + g_settings.set('UseWhiteList', communicationSettings.useWhiteList) + g_settings.set('IgnorePrivateMessages', communicationSettings.privateMessages) + g_settings.set('IgnoreYelling', communicationSettings.yelling) + g_settings.setNode('IgnorePlayers', tmpIgnoreList) + g_settings.setNode('WhitelistedPlayers', tmpWhiteList) +end + +function getIgnoredPlayers() + return communicationSettings.ignoredPlayers +end + +function getWhitelistedPlayers() + return communicationSettings.whitelistedPlayers +end + +function isUsingIgnoreList() + return communicationSettings.useIgnoreList +end + +function isUsingWhiteList() + return communicationSettings.useWhiteList +end +function isIgnored(name) + return table.find(communicationSettings.ignoredPlayers, name, true) +end + +function addIgnoredPlayer(name) + if isIgnored(name) then return end + table.insert(communicationSettings.ignoredPlayers, name) + communicationSettings.useIgnoreList = true +end + +function removeIgnoredPlayer(name) + table.removevalue(communicationSettings.ignoredPlayers, name) +end + +function isWhitelisted(name) + return table.find(communicationSettings.whitelistedPlayers, name, true) +end + +function addWhitelistedPlayer(name) + if isWhitelisted(name) then return end + table.insert(communicationSettings.whitelistedPlayers, name) +end + +function removeWhitelistedPlayer(name) + table.removevalue(communicationSettings.whitelistedPlayers, name) +end + +function isIgnoringPrivate() + return communicationSettings.privateMessages +end + +function isIgnoringYelling() + return communicationSettings.yelling +end + +function isAllowingVIPs() + return communicationSettings.allowVIPs +end + +function onClickIgnoreButton() + if communicationWindow then return end + communicationWindow = g_ui.displayUI('communicationwindow') + local ignoreListPanel = communicationWindow:getChildById('ignoreList') + local whiteListPanel = communicationWindow:getChildById('whiteList') + communicationWindow.onDestroy = function() communicationWindow = nil end + + local useIgnoreListBox = communicationWindow:getChildById('checkboxUseIgnoreList') + useIgnoreListBox:setChecked(communicationSettings.useIgnoreList) + local useWhiteListBox = communicationWindow:getChildById('checkboxUseWhiteList') + useWhiteListBox:setChecked(communicationSettings.useWhiteList) + + local removeIgnoreButton = communicationWindow:getChildById('buttonIgnoreRemove') + removeIgnoreButton:disable() + ignoreListPanel.onChildFocusChange = function() removeIgnoreButton:enable() end + removeIgnoreButton.onClick = function() + local selection = ignoreListPanel:getFocusedChild() + if selection then + ignoreListPanel:removeChild(selection) + selection:destroy() + end + removeIgnoreButton:disable() + end + + local removeWhitelistButton = communicationWindow:getChildById('buttonWhitelistRemove') + removeWhitelistButton:disable() + whiteListPanel.onChildFocusChange = function() removeWhitelistButton:enable() end + removeWhitelistButton.onClick = function() + local selection = whiteListPanel:getFocusedChild() + if selection then + whiteListPanel:removeChild(selection) + selection:destroy() + end + removeWhitelistButton:disable() + end + + local newlyIgnoredPlayers = {} + local addIgnoreName = communicationWindow:getChildById('ignoreNameEdit') + local addIgnoreButton = communicationWindow:getChildById('buttonIgnoreAdd') + local addIgnoreFunction = function() + local newEntry = addIgnoreName:getText() + if newEntry == '' then return end + if table.find(getIgnoredPlayers(), newEntry) then return end + if table.find(newlyIgnoredPlayers, newEntry) then return end + local label = g_ui.createWidget('IgnoreListLabel', ignoreListPanel) + label:setText(newEntry) + table.insert(newlyIgnoredPlayers, newEntry) + addIgnoreName:setText('') + end + addIgnoreButton.onClick = addIgnoreFunction + + local newlyWhitelistedPlayers = {} + local addWhitelistName = communicationWindow:getChildById('whitelistNameEdit') + local addWhitelistButton = communicationWindow:getChildById('buttonWhitelistAdd') + local addWhitelistFunction = function() + local newEntry = addWhitelistName:getText() + if newEntry == '' then return end + if table.find(getWhitelistedPlayers(), newEntry) then return end + if table.find(newlyWhitelistedPlayers, newEntry) then return end + local label = g_ui.createWidget('WhiteListLabel', whiteListPanel) + label:setText(newEntry) + table.insert(newlyWhitelistedPlayers, newEntry) + addWhitelistName:setText('') + end + addWhitelistButton.onClick = addWhitelistFunction + + communicationWindow.onEnter = function() + if addWhitelistName:isFocused() then + addWhitelistFunction() + elseif addIgnoreName:isFocused() then + addIgnoreFunction() + end + end + + local ignorePrivateMessageBox = communicationWindow:getChildById('checkboxIgnorePrivateMessages') + ignorePrivateMessageBox:setChecked(communicationSettings.privateMessages) + local ignoreYellingBox = communicationWindow:getChildById('checkboxIgnoreYelling') + ignoreYellingBox:setChecked(communicationSettings.yelling) + local allowVIPsBox = communicationWindow:getChildById('checkboxAllowVIPs') + allowVIPsBox:setChecked(communicationSettings.allowVIPs) + + local saveButton = communicationWindow:recursiveGetChildById('buttonSave') + saveButton.onClick = function() + communicationSettings.ignoredPlayers = {} + for i = 1, ignoreListPanel:getChildCount() do + addIgnoredPlayer(ignoreListPanel:getChildByIndex(i):getText()) + end + + communicationSettings.whitelistedPlayers = {} + for i = 1, whiteListPanel:getChildCount() do + addWhitelistedPlayer(whiteListPanel:getChildByIndex(i):getText()) + end + + communicationSettings.useIgnoreList = useIgnoreListBox:isChecked() + communicationSettings.useWhiteList = useWhiteListBox:isChecked() + communicationSettings.yelling = ignoreYellingBox:isChecked() + communicationSettings.privateMessages = ignorePrivateMessageBox:isChecked() + communicationSettings.allowVIPs = allowVIPsBox:isChecked() + communicationWindow:destroy() + end + + local cancelButton = communicationWindow:recursiveGetChildById('buttonCancel') + cancelButton.onClick = function() + communicationWindow:destroy() + end + + local ignoredPlayers = getIgnoredPlayers() + for i = 1, #ignoredPlayers do + local label = g_ui.createWidget('IgnoreListLabel', ignoreListPanel) + label:setText(ignoredPlayers[i]) + end + + local whitelistedPlayers = getWhitelistedPlayers() + for i = 1, #whitelistedPlayers do + local label = g_ui.createWidget('WhiteListLabel', whiteListPanel) + label:setText(whitelistedPlayers[i]) + end +end + +function online() + defaultTab = addTab(tr('Default'), true) + serverTab = addTab(tr('Server Log'), false) + + if g_game.getClientVersion() < 862 then + g_keyboard.bindKeyDown('Ctrl+R', openPlayerReportRuleViolationWindow) + end + -- open last channels + local lastChannelsOpen = g_settings.getNode('lastChannelsOpen') + if lastChannelsOpen then + local savedChannels = lastChannelsOpen[g_game.getCharacterName()] + if savedChannels then + for channelName, channelId in pairs(savedChannels) do + channelId = tonumber(channelId) + if channelId ~= -1 and channelId < 100 then + if not table.find(channels, channelId) then + g_game.joinChannel(channelId) + table.insert(ignoredChannels, channelId) + end + end + end + end + end + scheduleEvent(function() consoleTabBar:selectTab(defaultTab) end, 500) + scheduleEvent(function() ignoredChannels = {} end, 3000) +end + +function offline() + if g_game.getClientVersion() < 862 then + g_keyboard.unbindKeyDown('Ctrl+R') + end + clear() +end + +function onChannelEvent(channelId, name, type) + local fmt = ChannelEventFormats[type] + if not fmt then + print(('Unknown channel event type (%d).'):format(type)) + return + end + + local channel = channels[channelId] + if channel then + local tab = getTab(channel) + if tab then + addTabText(fmt:format(name), SpeakTypesSettings.channelOrange, tab) + end + end +end diff --git a/modules/game_console/console.otmod b/modules/game_console/console.otmod new file mode 100644 index 0000000..361e5d7 --- /dev/null +++ b/modules/game_console/console.otmod @@ -0,0 +1,9 @@ +Module + name: game_console + description: Manage chat window + author: edubart, andrefaramir, baxnie, sn4ake, BeniS + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ console ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_console/console.otui b/modules/game_console/console.otui new file mode 100644 index 0000000..7a8612e --- /dev/null +++ b/modules/game_console/console.otui @@ -0,0 +1,179 @@ +ConsoleLabel < UITextEdit + font: verdana-11px-antialised + height: 14 + color: yellow + margin-left: 2 + text-wrap: true + text-auto-resize: true + selection-color: #111416 + selection-background-color: #999999 + change-cursor-image: false + cursor-visible: false + editable: false + draggable: true + selectable: false + focusable: false + +ConsolePhantomLabel < UILabel + font: verdana-11px-antialised + height: 14 + color: yellow + text-wrap: true + text-auto-resize: true + selection-color: #111416 + selection-background-color: #999999 + +ConsoleTabBar < MoveableTabBar + height: 28 + +ConsoleTabBarPanel < MoveableTabBarPanel + id: consoleTab + + ScrollablePanel + id: consoleBuffer + anchors.fill: parent + margin-right: 12 + vertical-scrollbar: consoleScrollBar + layout: + type: verticalBox + align-bottom: true + border-width: 1 + border-color: #202327 + background: #00000066 + inverted-scroll: true + padding: 1 + + VerticalScrollBar + id: consoleScrollBar + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + step: 14 + pixels-scroll: true + +ConsoleTabBarButton < MoveableTabBarButton + height: 28 + padding: 15 + +Panel + id: consolePanel + anchors.fill: parent + image-source: /images/ui/panel_bottom + image-border: 4 + phantom: false + + CheckBox + id: toggleChat + !tooltip: tr('Disable chat mode, allow to walk using ASDW') + anchors.left: parent.left + anchors.top: parent.top + margin-left: 13 + margin-top: 8 + @onCheckChange: toggleChat() + + TabButton + id: prevChannelButton + icon: /images/game/console/leftarrow + anchors.left: toggleChat.right + anchors.top: parent.top + margin-left: 3 + margin-top: 6 + + ConsoleTabBar + id: consoleTabBar + anchors.left: prev.right + anchors.top: parent.top + anchors.right: next.left + margin-left: 5 + margin-top: 3 + margin-right: 5 + tab-spacing: 2 + movable: true + + TabButton + id: nextChannelButton + icon: /images/game/console/rightarrow + anchors.right: next.left + anchors.top: parent.top + margin-right: 5 + margin-top: 6 + + TabButton + id: closeChannelButton + !tooltip: tr('Close this channel') .. ' (Ctrl+E)' + icon: /images/game/console/closechannel + anchors.right: next.left + anchors.top: parent.top + enabled: false + margin-right: 5 + margin-top: 6 + @onClick: removeCurrentTab() + + TabButton + id: clearChannelButton + !tooltip: tr('Clear current message window') + icon: /images/game/console/clearchannel + anchors.right: next.left + anchors.top: parent.top + margin-right: 5 + margin-top: 6 + @onClick: | + local consoleTabBar = self:getParent():getChildById('consoleTabBar') + clearChannel(consoleTabBar) + + TabButton + id: channelsButton + !tooltip: tr('Open new channel') .. ' (Ctrl+O)' + icon: /images/game/console/channels + anchors.right: next.left + anchors.top: parent.top + margin-right: 5 + margin-top: 6 + @onClick: g_game.requestChannels() + + TabButton + id: ignoreButton + !tooltip: tr('Ignore players') + icon: /images/game/console/ignore + anchors.right: parent.right + anchors.top: parent.top + margin-right: 5 + margin-top: 6 + @onClick: onClickIgnoreButton() + + Panel + id: consoleContentPanel + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: consoleTextEdit.top + margin-left: 6 + margin-right: 6 + margin-bottom: 4 + margin-top: 4 + padding: 1 + focusable: false + phantom: true + + TabButton + id: sayModeButton + icon: /images/game/console/say + !tooltip: tr('Adjust volume') + &sayMode: 2 + size: 20 20 + anchors.left: parent.left + anchors.bottom: parent.bottom + margin-left: 6 + margin-bottom: 6 + @onClick: sayModeChange() + + TextEdit + id: consoleTextEdit + anchors.left: sayModeButton.right + anchors.right: parent.right + anchors.bottom: parent.bottom + margin-right: 6 + margin-left: 6 + margin-bottom: 6 + shift-navigation: true + max-length: 255 diff --git a/modules/game_console/violationwindow.otui b/modules/game_console/violationwindow.otui new file mode 100644 index 0000000..19c55ca --- /dev/null +++ b/modules/game_console/violationwindow.otui @@ -0,0 +1,40 @@ +MainWindow + id: ignoreWindow + !text: tr('Report Rule Violation') + size: 300 240 + + Label + !text: tr('Please state the rule violation in one clear sentence and wait for a reply from a gamemaster. Please note that your message will disappear if you close the channel.') + text-wrap: true + text-auto-resize: true + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + + TextEdit + id: text + text-wrap: true + multiline: true + anchors.top: prev.bottom + anchors.bottom: next.top + anchors.left: parent.left + anchors.right: parent.right + margin: 8 0 + max-length: 255 + + Button + id: buttonOk + !text: tr('Ok') + width: 64 + anchors.right: next.left + anchors.bottom: parent.bottom + margin-right: 10 + @onClick: self:getParent():onEnter() + + Button + id: buttonCancel + !text: tr('Cancel') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: self:getParent():onEscape() \ No newline at end of file diff --git a/modules/game_containers/container.otui b/modules/game_containers/container.otui new file mode 100644 index 0000000..cb505b2 --- /dev/null +++ b/modules/game_containers/container.otui @@ -0,0 +1,67 @@ +PageButton < Button + size: 30 18 + margin: 1 + + +ContainerWindow < MiniWindow + height: 150 + + UIItem + id: containerItemWidget + virtual: true + size: 16 16 + anchors.top: parent.top + anchors.left: parent.left + margin-top: 4 + margin-left: 4 + + UIButton + id: upButton + anchors.top: lockButton.top + anchors.right: lockButton.left + margin-right: 3 + size: 14 14 + image-source: /images/ui/miniwindow_buttons + image-clip: 42 0 14 14 + + $hover: + image-clip: 42 14 14 14 + + $pressed: + image-clip: 42 28 14 14 + + Panel + id: pagePanel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: miniwindowTopBar.bottom + height: 20 + margin: 2 3 0 3 + background: #00000066 + visible: false + + Label + id: pageLabel + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + margin-top: 2 + text-auto-resize: true + + PageButton + id: prevPageButton + text: < + anchors.top: parent.top + anchors.left: parent.left + + PageButton + id: nextPageButton + text: > + anchors.top: parent.top + anchors.right: parent.right + + MiniWindowContents + padding-right: 0 + layout: + type: grid + cell-size: 34 34 + flow: true diff --git a/modules/game_containers/containers.lua b/modules/game_containers/containers.lua new file mode 100644 index 0000000..32807ce --- /dev/null +++ b/modules/game_containers/containers.lua @@ -0,0 +1,165 @@ +function init() + g_ui.importStyle('container') + + connect(Container, { onOpen = onContainerOpen, + onClose = onContainerClose, + onSizeChange = onContainerChangeSize, + onUpdateItem = onContainerUpdateItem }) + connect(Game, { onGameEnd = clean() }) + + reloadContainers() +end + +function terminate() + disconnect(Container, { onOpen = onContainerOpen, + onClose = onContainerClose, + onSizeChange = onContainerChangeSize, + onUpdateItem = onContainerUpdateItem }) + disconnect(Game, { onGameEnd = clean() }) +end + +function reloadContainers() + clean() + for _,container in pairs(g_game.getContainers()) do + onContainerOpen(container) + end +end + +function clean() + for containerid,container in pairs(g_game.getContainers()) do + destroy(container) + end +end + +function destroy(container) + if container.window then + container.window:destroy() + container.window = nil + container.itemsPanel = nil + end +end + +function refreshContainerItems(container) + for slot=0,container:getCapacity()-1 do + local itemWidget = container.itemsPanel:getChildById('item' .. slot) + itemWidget:setItem(container:getItem(slot)) + end + + if container:hasPages() then + refreshContainerPages(container) + end +end + +function toggleContainerPages(containerWindow, pages) + containerWindow:getChildById('miniwindowScrollBar'):setMarginTop(pages and 42 or 22) + containerWindow:getChildById('contentsPanel'):setMarginTop(pages and 42 or 22) + containerWindow:getChildById('pagePanel'):setVisible(pages) +end + +function refreshContainerPages(container) + local currentPage = 1 + math.floor(container:getFirstIndex() / container:getCapacity()) + local pages = 1 + math.floor(math.max(0, (container:getSize() - 1)) / container:getCapacity()) + container.window:recursiveGetChildById('pageLabel'):setText(string.format('Page %i of %i', currentPage, pages)) + + local prevPageButton = container.window:recursiveGetChildById('prevPageButton') + if currentPage == 1 then + prevPageButton:setEnabled(false) + else + prevPageButton:setEnabled(true) + prevPageButton.onClick = function() g_game.seekInContainer(container:getId(), container:getFirstIndex() - container:getCapacity()) end + end + + local nextPageButton = container.window:recursiveGetChildById('nextPageButton') + if currentPage >= pages then + nextPageButton:setEnabled(false) + else + nextPageButton:setEnabled(true) + nextPageButton.onClick = function() g_game.seekInContainer(container:getId(), container:getFirstIndex() + container:getCapacity()) end + end +end + +function onContainerOpen(container, previousContainer) + local containerWindow + if previousContainer then + containerWindow = previousContainer.window + previousContainer.window = nil + previousContainer.itemsPanel = nil + else + containerWindow = g_ui.createWidget('ContainerWindow', modules.game_interface.getContainerPanel()) + end + containerWindow:setId('container' .. container:getId()) + local containerPanel = containerWindow:getChildById('contentsPanel') + local containerItemWidget = containerWindow:getChildById('containerItemWidget') + containerWindow.onClose = function() + g_game.close(container) + containerWindow:hide() + end + containerWindow.onDrop = function(container, widget, mousePos) + local child = containerPanel:getChildByIndex(-1) + if child then + child:onDrop(widget, mousePos, true) + end + end + + -- this disables scrollbar auto hiding + local scrollbar = containerWindow:getChildById('miniwindowScrollBar') + scrollbar:mergeStyle({ ['$!on'] = { }}) + + local upButton = containerWindow:getChildById('upButton') + upButton.onClick = function() + g_game.openParent(container) + end + upButton:setVisible(container:hasParent()) + + local name = container:getName() + name = name:sub(1,1):upper() .. name:sub(2) + containerWindow:setText(name) + + containerItemWidget:setItem(container:getContainerItem()) + + containerPanel:destroyChildren() + for slot=0,container:getCapacity()-1 do + local itemWidget = g_ui.createWidget('Item', containerPanel) + itemWidget:setId('item' .. slot) + itemWidget:setItem(container:getItem(slot)) + itemWidget:setMargin(0) + itemWidget.position = container:getSlotPosition(slot) + + if not container:isUnlocked() then + itemWidget:setBorderColor('red') + end + end + + container.window = containerWindow + container.itemsPanel = containerPanel + + toggleContainerPages(containerWindow, container:hasPages()) + refreshContainerPages(container) + + local layout = containerPanel:getLayout() + local cellSize = layout:getCellSize() + containerWindow:setContentMinimumHeight(cellSize.height) + containerWindow:setContentMaximumHeight(cellSize.height*layout:getNumLines()) + + if not previousContainer then + local filledLines = math.max(math.ceil(container:getItemsCount() / layout:getNumColumns()), 1) + containerWindow:setContentHeight(filledLines*cellSize.height) + end + + containerWindow:setup() +end + +function onContainerClose(container) + destroy(container) +end + +function onContainerChangeSize(container, size) + if not container.window then return end + refreshContainerItems(container) +end + +function onContainerUpdateItem(container, slot, item, oldItem) + if not container.window then return end + local itemWidget = container.itemsPanel:getChildById('item' .. slot) + itemWidget:setItem(item) +end diff --git a/modules/game_containers/containers.otmod b/modules/game_containers/containers.otmod new file mode 100644 index 0000000..6e3d686 --- /dev/null +++ b/modules/game_containers/containers.otmod @@ -0,0 +1,9 @@ +Module + name: game_containers + description: Manage containers + author: edubart, baxnie + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [containers] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_cooldown/cooldown.lua b/modules/game_cooldown/cooldown.lua new file mode 100644 index 0000000..0d28fd4 --- /dev/null +++ b/modules/game_cooldown/cooldown.lua @@ -0,0 +1,215 @@ +local ProgressCallback = { + update = 1, + finish = 2 +} + +cooldownWindow = nil +cooldownButton = nil +contentsPanel = nil +cooldownPanel = nil +lastPlayer = nil + +cooldown = {} +groupCooldown = {} + +function init() + connect(g_game, { onGameStart = online, + onSpellGroupCooldown = onSpellGroupCooldown, + onSpellCooldown = onSpellCooldown }) + + cooldownButton = modules.client_topmenu.addRightGameToggleButton('cooldownButton', + tr('Cooldowns'), '/images/topbuttons/cooldowns', toggle) + cooldownButton:setOn(true) + cooldownButton:hide() + + cooldownWindow = g_ui.loadUI('cooldown', modules.game_interface.getRightPanel()) + cooldownWindow:disableResize() + cooldownWindow:setup() + + contentsPanel = cooldownWindow:getChildById('contentsPanel') + cooldownPanel = contentsPanel:getChildById('cooldownPanel') + + -- preload cooldown images + for k,v in pairs(SpelllistSettings) do + g_textures.preload(v.iconFile) + end + + if g_game.isOnline() then + online() + end +end + +function terminate() + disconnect(g_game, { onGameStart = online, + onSpellGroupCooldown = onSpellGroupCooldown, + onSpellCooldown = onSpellCooldown }) + + cooldownWindow:destroy() + cooldownButton:destroy() +end + +function loadIcon(iconId) + local spell, profile, spellName = Spells.getSpellByIcon(iconId) + if not spellName then return end + if not profile then return end + + clientIconId = Spells.getClientId(spellName) + if not clientIconId then return end + + local icon = cooldownPanel:getChildById(iconId) + if not icon then + icon = g_ui.createWidget('SpellIcon') + icon:setId(iconId) + end + + local spellSettings = SpelllistSettings[profile] + if spellSettings then + icon:setImageSource(spellSettings.iconFile) + icon:setImageClip(Spells.getImageClip(clientIconId, profile)) + else + icon = nil + end + return icon +end + +function onMiniWindowClose() + cooldownButton:setOn(false) +end + +function toggle() + if cooldownButton:isOn() then + cooldownWindow:close() + cooldownButton:setOn(false) + else + cooldownWindow:open() + cooldownButton:setOn(true) + end +end + +function online() + if g_game.getFeature(GameSpellList) then + cooldownButton:show() + else + cooldownButton:hide() + cooldownWindow:close() + end + + if not lastPlayer or lastPlayer ~= g_game.getCharacterName() then + refresh() + lastPlayer = g_game.getCharacterName() + end +end + +function refresh() + cooldownPanel:destroyChildren() +end + +function removeCooldown(progressRect) + removeEvent(progressRect.event) + if progressRect.icon then + progressRect.icon:destroy() + progressRect.icon = nil + end + progressRect = nil +end + +function turnOffCooldown(progressRect) + removeEvent(progressRect.event) + if progressRect.icon then + progressRect.icon:setOn(false) + progressRect.icon = nil + end + + -- create particles + --[[local particle = g_ui.createWidget('GroupCooldownParticles', progressRect) + particle:fill('parent') + scheduleEvent(function() particle:destroy() end, 1000) -- hack until onEffectEnd]] + + progressRect = nil +end + +function initCooldown(progressRect, updateCallback, finishCallback) + progressRect:setPercent(0) + + progressRect.callback = {} + progressRect.callback[ProgressCallback.update] = updateCallback + progressRect.callback[ProgressCallback.finish] = finishCallback + + updateCallback() +end + +function updateCooldown(progressRect, duration) + progressRect:setPercent(progressRect:getPercent() + 10000/duration) + + if progressRect:getPercent() < 100 then + removeEvent(progressRect.event) + + progressRect.event = scheduleEvent(function() + progressRect.callback[ProgressCallback.update]() + end, 100) + else + progressRect.callback[ProgressCallback.finish]() + end +end + +function isGroupCooldownIconActive(groupId) + return groupCooldown[groupId] +end + +function isCooldownIconActive(iconId) + return cooldown[iconId] +end + +function onSpellCooldown(iconId, duration) + local icon = loadIcon(iconId) + if not icon then + return + end + icon:setParent(cooldownPanel) + + local progressRect = icon:getChildById(iconId) + if not progressRect then + progressRect = g_ui.createWidget('SpellProgressRect', icon) + progressRect:setId(iconId) + progressRect.icon = icon + progressRect:fill('parent') + else + progressRect:setPercent(0) + end + progressRect:setTooltip(spellName) + + local updateFunc = function() + updateCooldown(progressRect, duration) + end + local finishFunc = function() + removeCooldown(progressRect) + cooldown[iconId] = false + end + initCooldown(progressRect, updateFunc, finishFunc) + cooldown[iconId] = true +end + +function onSpellGroupCooldown(groupId, duration) + if not SpellGroups[groupId] then return end + + local icon = contentsPanel:getChildById('groupIcon' .. SpellGroups[groupId]) + local progressRect = contentsPanel:getChildById('progressRect' .. SpellGroups[groupId]) + if icon then + icon:setOn(true) + removeEvent(icon.event) + end + + progressRect.icon = icon + if progressRect then + removeEvent(progressRect.event) + local updateFunc = function() + updateCooldown(progressRect, duration) + end + local finishFunc = function() + turnOffCooldown(progressRect) + groupCooldown[groupId] = false + end + initCooldown(progressRect, updateFunc, finishFunc) + groupCooldown[groupId] = true + end +end diff --git a/modules/game_cooldown/cooldown.otmod b/modules/game_cooldown/cooldown.otmod new file mode 100644 index 0000000..69cb33f --- /dev/null +++ b/modules/game_cooldown/cooldown.otmod @@ -0,0 +1,9 @@ +Module + name: game_cooldown + description: Spellcooldowns + author: OTClient team + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ cooldown ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_cooldown/cooldown.otui b/modules/game_cooldown/cooldown.otui new file mode 100644 index 0000000..e9ad7ff --- /dev/null +++ b/modules/game_cooldown/cooldown.otui @@ -0,0 +1,100 @@ +SpellGroupIcon < UIWidget + size: 22 22 + image-size: 22 22 + image-source: /images/game/spells/cooldowns + focusable: false + margin-top: 3 + +SpellIcon < UIWidget + size: 24 24 + image-size: 24 24 + focusable: false + + $!first: + margin-left: 1 + +SpellProgressRect < UIProgressRect + background: #585858AA + percent: 100 + focusable: false + +GroupCooldownParticles < UIParticles + effect: groupcooldown-effect + +MiniWindow + id: cooldownWindow + !text: tr('Spell Cooldowns') + height: 82 + icon: /images/topbuttons/cooldowns + @onClose: modules.game_cooldown.onMiniWindowClose() + &save: true + + MiniWindowContents + SpellGroupIcon + id: groupIconAttack + image-clip: 0 0 20 20 + anchors.top: parent.top + anchors.left: parent.left + margin-left: 2 + $on: + image-clip: 0 20 20 20 + + SpellProgressRect + id: progressRectAttack + anchors.fill: groupIconAttack + !tooltip: tr('Attack') + + SpellGroupIcon + id: groupIconHealing + image-clip: 20 0 20 20 + anchors.top: parent.top + anchors.left: groupIconAttack.right + margin-left: 3 + $on: + image-clip: 20 20 20 20 + + SpellProgressRect + id: progressRectHealing + anchors.fill: groupIconHealing + !tooltip: tr('Healing') + + SpellGroupIcon + id: groupIconSupport + image-clip: 40 0 20 20 + anchors.top: parent.top + anchors.left: groupIconHealing.right + margin-left: 3 + $on: + image-clip: 40 20 20 20 + + SpellProgressRect + id: progressRectSupport + anchors.fill: groupIconSupport + !tooltip: tr('Support') + + SpellGroupIcon + id: groupIconSpecial + image-clip: 60 0 20 20 + anchors.top: parent.top + anchors.left: groupIconSupport.right + margin-left: 3 + $on: + image-clip: 60 20 20 20 + + SpellProgressRect + id: progressRectSpecial + anchors.fill: groupIconSpecial + !tooltip: tr('Special') + + Panel + id: cooldownPanel + layout: + type: horizontalBox + height: 30 + margin-top: 3 + padding: 3 + anchors.top: groupIconSpecial.bottom + anchors.left: parent.left + anchors.right: parent.right + background-color: #00000022 + diff --git a/modules/game_features/features.lua b/modules/game_features/features.lua new file mode 100644 index 0000000..9b0b882 --- /dev/null +++ b/modules/game_features/features.lua @@ -0,0 +1,176 @@ +function init() + connect(g_game, { onClientVersionChange = updateFeatures }) +end + +function terminate() + disconnect(g_game, { onClientVersionChange = updateFeatures }) +end + +function updateFeatures(version) + g_game.resetFeatures() + + if(version >= 770) then + g_game.enableFeature(GameLooktypeU16); + g_game.enableFeature(GameMessageStatements); + g_game.enableFeature(GameLoginPacketEncryption); + end + + if(version >= 780) then + g_game.enableFeature(GamePlayerAddons); + g_game.enableFeature(GamePlayerStamina); + g_game.enableFeature(GameNewFluids); + g_game.enableFeature(GameMessageLevel); + g_game.enableFeature(GamePlayerStateU16); + g_game.enableFeature(GameNewOutfitProtocol); + end + + if(version >= 790) then + g_game.enableFeature(GameWritableDate); + end + + if(version >= 840) then + g_game.enableFeature(GameProtocolChecksum); + g_game.enableFeature(GameAccountNames); + g_game.enableFeature(GameDoubleFreeCapacity); + end + + if(version >= 841) then + g_game.enableFeature(GameChallengeOnLogin); + g_game.enableFeature(GameMessageSizeCheck); + g_game.enableFeature(GameTileAddThingWithStackpos); + end + + if(version >= 854) then + g_game.enableFeature(GameCreatureEmblems); + end + + if(version >= 860) then + g_game.enableFeature(GameAttackSeq); + end + + if(version >= 862) then + g_game.enableFeature(GamePenalityOnDeath); + end + + if(version >= 870) then + g_game.enableFeature(GameDoubleExperience); + g_game.enableFeature(GamePlayerMounts); + g_game.enableFeature(GameSpellList); + end + + if(version >= 910) then + g_game.enableFeature(GameNameOnNpcTrade); + g_game.enableFeature(GameTotalCapacity); + g_game.enableFeature(GameSkillsBase); + g_game.enableFeature(GamePlayerRegenerationTime); + g_game.enableFeature(GameChannelPlayerList); + g_game.enableFeature(GameEnvironmentEffect); + g_game.enableFeature(GameItemAnimationPhase); + end + + if(version >= 940) then + g_game.enableFeature(GamePlayerMarket); + end + + if(version >= 953) then + g_game.enableFeature(GamePurseSlot); + g_game.enableFeature(GameClientPing); + end + + if(version >= 960) then + g_game.enableFeature(GameSpritesU32); + g_game.enableFeature(GameOfflineTrainingTime); + end + + if(version >= 963) then + g_game.enableFeature(GameAdditionalVipInfo); + end + + if(version >= 980) then + g_game.enableFeature(GamePreviewState); + g_game.enableFeature(GameClientVersion); + end + + if(version >= 981) then + g_game.enableFeature(GameLoginPending); + g_game.enableFeature(GameNewSpeedLaw); + end + + if(version >= 984) then + g_game.enableFeature(GameContainerPagination); + g_game.enableFeature(GameBrowseField); + end + + if(version >= 1000) then + g_game.enableFeature(GameThingMarks); + g_game.enableFeature(GamePVPMode); + end + + if(version >= 1035) then + g_game.enableFeature(GameDoubleSkills); + g_game.enableFeature(GameBaseSkillU16); + end + + if(version >= 1036) then + g_game.enableFeature(GameCreatureIcons); + g_game.enableFeature(GameHideNpcNames); + end + + if(version >= 1038) then + g_game.enableFeature(GamePremiumExpiration); + end + + if(version >= 1050) then + g_game.enableFeature(GameEnhancedAnimations); + end + + if(version >= 1053) then + g_game.enableFeature(GameUnjustifiedPoints); + end + + if(version >= 1054) then + g_game.enableFeature(GameExperienceBonus); + end + + if(version >= 1055) then + g_game.enableFeature(GameDeathType); + end + + if(version >= 1057) then + g_game.enableFeature(GameIdleAnimations); + end + + if(version >= 1061) then + g_game.enableFeature(GameOGLInformation); + end + + if(version >= 1071) then + g_game.enableFeature(GameContentRevision); + end + + if(version >= 1072) then + g_game.enableFeature(GameAuthenticator); + end + + if(version >= 1074) then + g_game.enableFeature(GameSessionKey); + end + + if(version >= 1080) then + g_game.enableFeature(GameIngameStore); + end + + if(version >= 1092) then + g_game.enableFeature(GameIngameStoreServiceType); + end + + if(version >= 1093) then + g_game.enableFeature(GameIngameStoreHighlights); + end + + if(version >= 1094) then + g_game.enableFeature(GameAdditionalSkills); + end + + modules.game_things.load() +end diff --git a/modules/game_features/features.otmod b/modules/game_features/features.otmod new file mode 100644 index 0000000..678cf26 --- /dev/null +++ b/modules/game_features/features.otmod @@ -0,0 +1,8 @@ +Module + name: game_features + description: Manager game features + reloadable: false + sandboxed: true + scripts: [features] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_healthinfo/healthinfo.lua b/modules/game_healthinfo/healthinfo.lua new file mode 100644 index 0000000..0fad283 --- /dev/null +++ b/modules/game_healthinfo/healthinfo.lua @@ -0,0 +1,281 @@ +Icons = {} +Icons[PlayerStates.Poison] = { tooltip = tr('You are poisoned'), path = '/images/game/states/poisoned', id = 'condition_poisoned' } +Icons[PlayerStates.Burn] = { tooltip = tr('You are burning'), path = '/images/game/states/burning', id = 'condition_burning' } +Icons[PlayerStates.Energy] = { tooltip = tr('You are electrified'), path = '/images/game/states/electrified', id = 'condition_electrified' } +Icons[PlayerStates.Drunk] = { tooltip = tr('You are drunk'), path = '/images/game/states/drunk', id = 'condition_drunk' } +Icons[PlayerStates.ManaShield] = { tooltip = tr('You are protected by a magic shield'), path = '/images/game/states/magic_shield', id = 'condition_magic_shield' } +Icons[PlayerStates.Paralyze] = { tooltip = tr('You are paralysed'), path = '/images/game/states/slowed', id = 'condition_slowed' } +Icons[PlayerStates.Haste] = { tooltip = tr('You are hasted'), path = '/images/game/states/haste', id = 'condition_haste' } +Icons[PlayerStates.Swords] = { tooltip = tr('You may not logout during a fight'), path = '/images/game/states/logout_block', id = 'condition_logout_block' } +Icons[PlayerStates.Drowning] = { tooltip = tr('You are drowning'), path = '/images/game/states/drowning', id = 'condition_drowning' } +Icons[PlayerStates.Freezing] = { tooltip = tr('You are freezing'), path = '/images/game/states/freezing', id = 'condition_freezing' } +Icons[PlayerStates.Dazzled] = { tooltip = tr('You are dazzled'), path = '/images/game/states/dazzled', id = 'condition_dazzled' } +Icons[PlayerStates.Cursed] = { tooltip = tr('You are cursed'), path = '/images/game/states/cursed', id = 'condition_cursed' } +Icons[PlayerStates.PartyBuff] = { tooltip = tr('You are strengthened'), path = '/images/game/states/strengthened', id = 'condition_strengthened' } +Icons[PlayerStates.PzBlock] = { tooltip = tr('You may not logout or enter a protection zone'), path = '/images/game/states/protection_zone_block', id = 'condition_protection_zone_block' } +Icons[PlayerStates.Pz] = { tooltip = tr('You are within a protection zone'), path = '/images/game/states/protection_zone', id = 'condition_protection_zone' } +Icons[PlayerStates.Bleeding] = { tooltip = tr('You are bleeding'), path = '/images/game/states/bleeding', id = 'condition_bleeding' } +Icons[PlayerStates.Hungry] = { tooltip = tr('You are hungry'), path = '/images/game/states/hungry', id = 'condition_hungry' } + +healthInfoWindow = nil +healthBar = nil +manaBar = nil +experienceBar = nil +soulLabel = nil +capLabel = nil +healthTooltip = 'Your character health is %d out of %d.' +manaTooltip = 'Your character mana is %d out of %d.' +experienceTooltip = 'You have %d%% to advance to level %d.' + +overlay = nil +healthCircleFront = nil +manaCircleFront = nil +healthCircle = nil +manaCircle = nil +topHealthBar = nil +topManaBar = nil + +function init() + connect(LocalPlayer, { onHealthChange = onHealthChange, + onManaChange = onManaChange, + onLevelChange = onLevelChange, + onStatesChange = onStatesChange, + onSoulChange = onSoulChange, + onFreeCapacityChange = onFreeCapacityChange }) + + connect(g_game, { onGameEnd = offline }) + + healthInfoButton = modules.client_topmenu.addRightGameToggleButton('healthInfoButton', tr('Health Information'), '/images/topbuttons/healthinfo', toggle) + healthInfoButton:setOn(true) + + healthInfoWindow = g_ui.loadUI('healthinfo', modules.game_interface.getRightPanel()) + healthInfoWindow:disableResize() + healthBar = healthInfoWindow:recursiveGetChildById('healthBar') + manaBar = healthInfoWindow:recursiveGetChildById('manaBar') + experienceBar = healthInfoWindow:recursiveGetChildById('experienceBar') + soulLabel = healthInfoWindow:recursiveGetChildById('soulLabel') + capLabel = healthInfoWindow:recursiveGetChildById('capLabel') + + overlay = g_ui.createWidget('HealthOverlay', modules.game_interface.getMapPanel()) + healthCircleFront = overlay:getChildById('healthCircleFront') + manaCircleFront = overlay:getChildById('manaCircleFront') + healthCircle = overlay:getChildById('healthCircle') + manaCircle = overlay:getChildById('manaCircle') + topHealthBar = overlay:getChildById('topHealthBar') + topManaBar = overlay:getChildById('topManaBar') + + connect(overlay, { onGeometryChange = onOverlayGeometryChange }) + + -- load condition icons + for k,v in pairs(Icons) do + g_textures.preload(v.path) + end + + if g_game.isOnline() then + local localPlayer = g_game.getLocalPlayer() + onHealthChange(localPlayer, localPlayer:getHealth(), localPlayer:getMaxHealth()) + onManaChange(localPlayer, localPlayer:getMana(), localPlayer:getMaxMana()) + onLevelChange(localPlayer, localPlayer:getLevel(), localPlayer:getLevelPercent()) + onStatesChange(localPlayer, localPlayer:getStates(), 0) + onSoulChange(localPlayer, localPlayer:getSoul()) + onFreeCapacityChange(localPlayer, localPlayer:getFreeCapacity()) + end + + + hideLabels() + hideExperience() + + healthInfoWindow:setup() +end + +function terminate() + disconnect(LocalPlayer, { onHealthChange = onHealthChange, + onManaChange = onManaChange, + onLevelChange = onLevelChange, + onStatesChange = onStatesChange, + onSoulChange = onSoulChange, + onFreeCapacityChange = onFreeCapacityChange }) + + disconnect(g_game, { onGameEnd = offline }) + disconnect(overlay, { onGeometryChange = onOverlayGeometryChange }) + + healthInfoWindow:destroy() + healthInfoButton:destroy() + overlay:destroy() +end + +function toggle() + if healthInfoButton:isOn() then + healthInfoWindow:close() + healthInfoButton:setOn(false) + else + healthInfoWindow:open() + healthInfoButton:setOn(true) + end +end + +function toggleIcon(bitChanged) + local content = healthInfoWindow:recursiveGetChildById('conditionPanel') + + local icon = content:getChildById(Icons[bitChanged].id) + if icon then + icon:destroy() + else + icon = loadIcon(bitChanged) + icon:setParent(content) + end +end + +function loadIcon(bitChanged) + local icon = g_ui.createWidget('ConditionWidget', content) + icon:setId(Icons[bitChanged].id) + icon:setImageSource(Icons[bitChanged].path) + icon:setTooltip(Icons[bitChanged].tooltip) + return icon +end + +function offline() + healthInfoWindow:recursiveGetChildById('conditionPanel'):destroyChildren() +end + +-- hooked events +function onMiniWindowClose() + healthInfoButton:setOn(false) +end + +function onHealthChange(localPlayer, health, maxHealth) + healthBar:setText(health .. ' / ' .. maxHealth) + healthBar:setTooltip(tr(healthTooltip, health, maxHealth)) + healthBar:setValue(health, 0, maxHealth) + + topHealthBar:setText(health .. ' / ' .. maxHealth) + topHealthBar:setTooltip(tr(healthTooltip, health, maxHealth)) + topHealthBar:setValue(health, 0, maxHealth) + + local healthPercent = math.floor(g_game.getLocalPlayer():getHealthPercent()) + local Yhppc = math.floor(208 * (1 - (healthPercent / 100))) + local rect = { x = 0, y = Yhppc, width = 63, height = 208 } + healthCircleFront:setImageClip(rect) + + if healthPercent > 92 then + healthCircleFront:setImageColor("#00BC00FF") + elseif healthPercent > 60 then + healthCircleFront:setImageColor("#50A150FF") + elseif healthPercent > 30 then + healthCircleFront:setImageColor("#A1A100FF") + elseif healthPercent > 8 then + healthCircleFront:setImageColor("#BF0A0AFF") + elseif healthPercent > 3 then + healthCircleFront:setImageColor("#910F0FFF") + else + healthCircleFront:setImageColor("#850C0CFF") + end + + healthCircleFront:setMarginTop(Yhppc) +end + +function onManaChange(localPlayer, mana, maxMana) + manaBar:setText(mana .. ' / ' .. maxMana) + manaBar:setTooltip(tr(manaTooltip, mana, maxMana)) + manaBar:setValue(mana, 0, maxMana) + + topManaBar:setText(mana .. ' / ' .. maxMana) + topManaBar:setTooltip(tr(manaTooltip, mana, maxMana)) + topManaBar:setValue(mana, 0, maxMana) + + local Ymppc = math.floor(208 * (1 - (math.floor((g_game.getLocalPlayer():getMaxMana() - (g_game.getLocalPlayer():getMaxMana() - g_game.getLocalPlayer():getMana())) * 100 / g_game.getLocalPlayer():getMaxMana()) / 100))) + local rect = { x = 0, y = Ymppc, width = 63, height = 208 } + manaCircleFront:setImageClip(rect) + manaCircleFront:setMarginTop(Ymppc) +end + +function onLevelChange(localPlayer, value, percent) + experienceBar:setText(percent .. '%') + experienceBar:setTooltip(tr(experienceTooltip, percent, value+1)) + experienceBar:setPercent(percent) +end + +function onSoulChange(localPlayer, soul) + soulLabel:setText(tr('Soul') .. ': ' .. soul) +end + +function onFreeCapacityChange(player, freeCapacity) + capLabel:setText(tr('Cap') .. ': ' .. freeCapacity) +end + +function onStatesChange(localPlayer, now, old) + if now == old then return end + + local bitsChanged = bit32.bxor(now, old) + for i = 1, 32 do + local pow = math.pow(2, i-1) + if pow > bitsChanged then break end + local bitChanged = bit32.band(bitsChanged, pow) + if bitChanged ~= 0 then + toggleIcon(bitChanged) + end + end +end + +-- personalization functions +function hideLabels() + local content = healthInfoWindow:recursiveGetChildById('conditionPanel') + local removeHeight = math.max(capLabel:getMarginRect().height, soulLabel:getMarginRect().height) + content:getMarginRect().height - 3 + capLabel:setOn(false) + soulLabel:setOn(false) + content:setVisible(false) + healthInfoWindow:setHeight(math.max(healthInfoWindow.minimizedHeight, healthInfoWindow:getHeight() - removeHeight)) +end + +function hideExperience() + local removeHeight = experienceBar:getMarginRect().height + experienceBar:setOn(false) + healthInfoWindow:setHeight(math.max(healthInfoWindow.minimizedHeight, healthInfoWindow:getHeight() - removeHeight)) +end + +function setHealthTooltip(tooltip) + healthTooltip = tooltip + + local localPlayer = g_game.getLocalPlayer() + if localPlayer then + healthBar:setTooltip(tr(healthTooltip, localPlayer:getHealth(), localPlayer:getMaxHealth())) + end +end + +function setManaTooltip(tooltip) + manaTooltip = tooltip + + local localPlayer = g_game.getLocalPlayer() + if localPlayer then + manaBar:setTooltip(tr(manaTooltip, localPlayer:getMana(), localPlayer:getMaxMana())) + end +end + +function setExperienceTooltip(tooltip) + experienceTooltip = tooltip + + local localPlayer = g_game.getLocalPlayer() + if localPlayer then + experienceBar:setTooltip(tr(experienceTooltip, localPlayer:getLevelPercent(), localPlayer:getLevel()+1)) + end +end + +function onOverlayGeometryChange() + local classic = g_settings.getBoolean("classicView") + local minMargin = 100 + if classic then + topHealthBar:setMarginTop(15) + topManaBar:setMarginTop(15) + else + topHealthBar:setMarginTop(45) + topManaBar:setMarginTop(45) + minMargin = 200 + end + + local height = overlay:getHeight() + local width = overlay:getWidth() + + + topHealthBar:setMarginLeft(math.max(minMargin, (width - height) / 2 + 2)) + topManaBar:setMarginRight(math.max(minMargin, (width - height) / 2 + 2)) +end \ No newline at end of file diff --git a/modules/game_healthinfo/healthinfo.otmod b/modules/game_healthinfo/healthinfo.otmod new file mode 100644 index 0000000..4c52ef0 --- /dev/null +++ b/modules/game_healthinfo/healthinfo.otmod @@ -0,0 +1,9 @@ +Module + name: game_healthinfo + description: Displays health, mana points, soul points, and conditions + author: edubart, BeniS + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ healthinfo ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_healthinfo/healthinfo.otui b/modules/game_healthinfo/healthinfo.otui new file mode 100644 index 0000000..447ff00 --- /dev/null +++ b/modules/game_healthinfo/healthinfo.otui @@ -0,0 +1,157 @@ +HealthBar < ProgressBar + id: healthBar + background-color: #ff4444 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + margin: 1 + +ManaBar < ProgressBar + id: manaBar + background-color: #4444ff + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin: 1 + margin-top: 3 + +ExperienceBar < ProgressBar + id: experienceBar + background-color: #B6E866 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin: 1 + margin-top: 3 + +SoulLabel < GameLabel + id: soulLabel + text-align: right + color: white + font: verdana-11px-rounded + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: parent.horizontalCenter + margin-top: 5 + margin-right: 3 + on: true + + $!on: + visible: false + margin-top: 0 + height: 0 + +CapLabel < GameLabel + id: capLabel + color: white + font: verdana-11px-rounded + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.horizontalCenter + margin-top: 5 + margin-left: 3 + on: true + + $!on: + visible: false + margin-top: 0 + height: 0 + +ConditionWidget < UIWidget + size: 18 18 + + $!first: + margin-left: 2 + +HealthOverlay < UIWidget + id: healthOverlay + anchors.fill: parent + phantom: true + + ProgressBar + id: topHealthBar + background-color: #ff4444 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.horizontalCenter + height: 16 + phantom: true + + ProgressBar + id: topManaBar + background-color: #4444ff + anchors.top: parent.top + anchors.right: parent.right + anchors.left: parent.horizontalCenter + height: 16 + phantom: true + + UIProgressBar + id: healthCircle + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + image-source: /images/game/circle/left_empty + margin-right: 169 + margin-bottom: 16 + opacity: 0.4 + phantom: true + + UIProgressBar + id: healthCircleFront + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + image-source: /images/game/circle/left_full + margin-right: 169 + margin-bottom: 16 + opacity: 0.4 + phantom: true + + UIProgressBar + id: manaCircle + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + image-source: /images/game/circle/right_empty + margin-left: 130 + margin-bottom: 16 + opacity: 0.4 + phantom: true + + UIProgressBar + id: manaCircleFront + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + image-source: /images/game/circle/right_full + margin-left: 130 + margin-bottom: 16 + opacity: 0.3 + image-color: #0000FFFF + phantom: true + +MiniWindow + icon: /images/topbuttons/healthinfo + id: healthInfoWindow + !text: tr('Health Info') + height: 123 + @onClose: modules.game_healthinfo.onMiniWindowClose() + &save: true + + MiniWindowContents + HealthBar + ManaBar + ExperienceBar + Panel + id: conditionPanel + layout: + type: horizontalBox + height: 22 + margin-top: 4 + padding: 2 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + border-width: 1 + border-color: #00000077 + background-color: #ffffff11 + SoulLabel + CapLabel + diff --git a/modules/game_hotkeys/hotkeys_manager.lua b/modules/game_hotkeys/hotkeys_manager.lua new file mode 100644 index 0000000..c0d4052 --- /dev/null +++ b/modules/game_hotkeys/hotkeys_manager.lua @@ -0,0 +1,622 @@ +HOTKEY_MANAGER_USE = nil +HOTKEY_MANAGER_USEONSELF = 1 +HOTKEY_MANAGER_USEONTARGET = 2 +HOTKEY_MANAGER_USEWITH = 3 + +HotkeyColors = { + text = '#888888', + textAutoSend = '#FFFFFF', + itemUse = '#8888FF', + itemUseSelf = '#00FF00', + itemUseTarget = '#FF0000', + itemUseWith = '#F5B325', +} + +hotkeysManagerLoaded = false +hotkeysWindow = nil +configSelector = nil +hotkeysButton = nil +currentHotkeyLabel = nil +currentItemPreview = nil +itemWidget = nil +addHotkeyButton = nil +removeHotkeyButton = nil +hotkeyText = nil +hotKeyTextLabel = nil +sendAutomatically = nil +selectObjectButton = nil +clearObjectButton = nil +useOnSelf = nil +useOnTarget = nil +useWith = nil +defaultComboKeys = nil +perCharacter = true +mouseGrabberWidget = nil +useRadioGroup = nil +currentHotkeys = nil +boundCombosCallback = {} +hotkeysList = {} +lastHotkeyTime = g_clock.millis() +hotkeyConfigs = {} +currentConfig = 1 +configValueChanged = false + +-- public functions +function init() + hotkeysButton = modules.client_topmenu.addLeftGameButton('hotkeysButton', tr('Hotkeys') .. ' (Ctrl+K)', '/images/topbuttons/hotkeys', toggle) + g_keyboard.bindKeyDown('Ctrl+K', toggle) + hotkeysWindow = g_ui.displayUI('hotkeys_manager') + hotkeysWindow:setVisible(false) + + configSelector = hotkeysWindow:getChildById('configSelector') + currentHotkeys = hotkeysWindow:getChildById('currentHotkeys') + currentItemPreview = hotkeysWindow:getChildById('itemPreview') + addHotkeyButton = hotkeysWindow:getChildById('addHotkeyButton') + removeHotkeyButton = hotkeysWindow:getChildById('removeHotkeyButton') + hotkeyText = hotkeysWindow:getChildById('hotkeyText') + hotKeyTextLabel = hotkeysWindow:getChildById('hotKeyTextLabel') + sendAutomatically = hotkeysWindow:getChildById('sendAutomatically') + selectObjectButton = hotkeysWindow:getChildById('selectObjectButton') + clearObjectButton = hotkeysWindow:getChildById('clearObjectButton') + useOnSelf = hotkeysWindow:getChildById('useOnSelf') + useOnTarget = hotkeysWindow:getChildById('useOnTarget') + useWith = hotkeysWindow:getChildById('useWith') + + useRadioGroup = UIRadioGroup.create() + useRadioGroup:addWidget(useOnSelf) + useRadioGroup:addWidget(useOnTarget) + useRadioGroup:addWidget(useWith) + useRadioGroup.onSelectionChange = function(self, selected) onChangeUseType(selected) end + + mouseGrabberWidget = g_ui.createWidget('UIWidget') + mouseGrabberWidget:setVisible(false) + mouseGrabberWidget:setFocusable(false) + mouseGrabberWidget.onMouseRelease = onChooseItemMouseRelease + + currentHotkeys.onChildFocusChange = function(self, hotkeyLabel) onSelectHotkeyLabel(hotkeyLabel) end + g_keyboard.bindKeyPress('Down', function() currentHotkeys:focusNextChild(KeyboardFocusReason) end, hotkeysWindow) + g_keyboard.bindKeyPress('Up', function() currentHotkeys:focusPreviousChild(KeyboardFocusReason) end, hotkeysWindow) + + connect(g_game, { + onGameStart = online, + onGameEnd = offline + }) + + for i = 1, configSelector:getOptionsCount() do + hotkeyConfigs[i] = g_configs.create("/hotkeys_" .. i .. ".otml") + end + + load() +end + +function terminate() + disconnect(g_game, { + onGameStart = online, + onGameEnd = offline + }) + + g_keyboard.unbindKeyDown('Ctrl+K') + + unload() + + hotkeysWindow:destroy() + hotkeysButton:destroy() + mouseGrabberWidget:destroy() +end + +function online() + reload() + hide() +end + +function offline() + unload() + hide() +end + +function show() + if not g_game.isOnline() then + return + end + hotkeysWindow:show() + hotkeysWindow:raise() + hotkeysWindow:focus() +end + +function hide() + hotkeysWindow:hide() +end + +function toggle() + if not hotkeysWindow:isVisible() then + show() + else + hide() + end +end + +function ok() + save() + hide() +end + +function cancel() + reload() + hide() +end + +function load(forceDefaults) + hotkeysManagerLoaded = false + currentConfig = 1 + + local hotkeysNode = g_settings.getNode('hotkeys') or {} + local index = g_game.getCharacterName() .. "_" .. g_game.getClientVersion() + if hotkeysNode[index] ~= nil and hotkeysNode[index] > 0 and hotkeysNode[index] <= #hotkeyConfigs then + currentConfig = hotkeysNode[index] + end + + configSelector:setCurrentIndex(currentConfig, true) + + local hotkeySettings = hotkeyConfigs[currentConfig]:getNode('hotkeys') + local hotkeys = {} + + if not table.empty(hotkeySettings) then hotkeys = hotkeySettings end + + hotkeyList = {} + if not forceDefaults then + if not table.empty(hotkeys) then + for keyCombo, setting in pairs(hotkeys) do + keyCombo = tostring(keyCombo) + addKeyCombo(keyCombo, setting) + hotkeyList[keyCombo] = setting + end + end + end + + if currentHotkeys:getChildCount() == 0 then + loadDefautComboKeys() + end + + configValueChanged = false + hotkeysManagerLoaded = true +end + +function unload() + for keyCombo,callback in pairs(boundCombosCallback) do + g_keyboard.unbindKeyPress(keyCombo, callback) + end + boundCombosCallback = {} + currentHotkeys:destroyChildren() + currentHotkeyLabel = nil + updateHotkeyForm(true) + hotkeyList = {} +end + +function reset() + unload() + load(true) +end + +function reload() + unload() + load() +end + +function save() + if not configValueChanged then + return + end + + local hotkeySettings = hotkeyConfigs[currentConfig]:getNode('hotkeys') or {} + + table.clear(hotkeySettings) + + for _,child in pairs(currentHotkeys:getChildren()) do + hotkeySettings[child.keyCombo] = { + autoSend = child.autoSend, + itemId = child.itemId, + subType = child.subType, + useType = child.useType, + value = child.value + } + end + + hotkeyList = hotkeySettings + hotkeyConfigs[currentConfig]:setNode('hotkeys', hotkeySettings) + hotkeyConfigs[currentConfig]:save() + + local index = g_game.getCharacterName() .. "_" .. g_game.getClientVersion() + local hotkeysNode = g_settings.getNode('hotkeys') or {} + hotkeysNode[index] = currentConfig + g_settings.setNode('hotkeys', hotkeysNode) + g_settings.save() +end + +function onConfigChange() + if not configSelector then return end + local index = g_game.getCharacterName() .. "_" .. g_game.getClientVersion() + local hotkeysNode = g_settings.getNode('hotkeys') or {} + hotkeysNode[index] = configSelector.currentIndex + g_settings.setNode('hotkeys', hotkeysNode) + reload() +end + +function loadDefautComboKeys() + if not defaultComboKeys then + for i=1,12 do + addKeyCombo('F' .. i) + end + for i=1,4 do + addKeyCombo('Shift+F' .. i) + end + else + for keyCombo, keySettings in pairs(defaultComboKeys) do + addKeyCombo(keyCombo, keySettings) + end + end +end + +function setDefaultComboKeys(combo) + defaultComboKeys = combo +end + +function onChooseItemMouseRelease(self, mousePosition, mouseButton) + local item = nil + if mouseButton == MouseLeftButton then + local clickedWidget = modules.game_interface.getRootPanel():recursiveGetChildByPos(mousePosition, false) + if clickedWidget then + if clickedWidget:getClassName() == 'UIGameMap' then + local tile = clickedWidget:getTile(mousePosition) + if tile then + local thing = tile:getTopMoveThing() + if thing and thing:isItem() then + item = thing + end + end + elseif clickedWidget:getClassName() == 'UIItem' and not clickedWidget:isVirtual() then + item = clickedWidget:getItem() + end + end + end + + if item and currentHotkeyLabel then + currentHotkeyLabel.itemId = item:getId() + if item:isFluidContainer() then + currentHotkeyLabel.subType = item:getSubType() + end + if item:isMultiUse() then + currentHotkeyLabel.useType = HOTKEY_MANAGER_USEWITH + else + currentHotkeyLabel.useType = HOTKEY_MANAGER_USE + end + currentHotkeyLabel.value = nil + currentHotkeyLabel.autoSend = false + updateHotkeyLabel(currentHotkeyLabel) + updateHotkeyForm(true) + end + + show() + + g_mouse.popCursor('target') + self:ungrabMouse() + return true +end + +function startChooseItem() + if g_ui.isMouseGrabbed() then return end + mouseGrabberWidget:grabMouse() + g_mouse.pushCursor('target') + hide() +end + +function clearObject() + currentHotkeyLabel.itemId = nil + currentHotkeyLabel.subType = nil + currentHotkeyLabel.useType = nil + currentHotkeyLabel.autoSend = nil + currentHotkeyLabel.value = nil + updateHotkeyLabel(currentHotkeyLabel) + updateHotkeyForm(true) +end + +function addHotkey() + local assignWindow = g_ui.createWidget('HotkeyAssignWindow', rootWidget) + assignWindow:grabKeyboard() + + local comboLabel = assignWindow:getChildById('comboPreview') + comboLabel.keyCombo = '' + assignWindow.onKeyDown = hotkeyCapture +end + +function addKeyCombo(keyCombo, keySettings, focus) + if keyCombo == nil or #keyCombo == 0 then return end + if not keyCombo then return end + local hotkeyLabel = currentHotkeys:getChildById(keyCombo) + if not hotkeyLabel then + hotkeyLabel = g_ui.createWidget('HotkeyListLabel') + hotkeyLabel:setId(keyCombo) + + local children = currentHotkeys:getChildren() + children[#children+1] = hotkeyLabel + table.sort(children, function(a,b) + if a:getId():len() < b:getId():len() then + return true + elseif a:getId():len() == b:getId():len() then + return a:getId() < b:getId() + else + return false + end + end) + for i=1,#children do + if children[i] == hotkeyLabel then + currentHotkeys:insertChild(i, hotkeyLabel) + break + end + end + + if keySettings then + currentHotkeyLabel = hotkeyLabel + hotkeyLabel.keyCombo = keyCombo + hotkeyLabel.autoSend = toboolean(keySettings.autoSend) + hotkeyLabel.itemId = tonumber(keySettings.itemId) + hotkeyLabel.subType = tonumber(keySettings.subType) + hotkeyLabel.useType = tonumber(keySettings.useType) + if keySettings.value then hotkeyLabel.value = tostring(keySettings.value) end + else + hotkeyLabel.keyCombo = keyCombo + hotkeyLabel.autoSend = false + hotkeyLabel.itemId = nil + hotkeyLabel.subType = nil + hotkeyLabel.useType = nil + hotkeyLabel.value = '' + end + + updateHotkeyLabel(hotkeyLabel) + + boundCombosCallback[keyCombo] = function() doKeyCombo(keyCombo) end + g_keyboard.bindKeyPress(keyCombo, boundCombosCallback[keyCombo]) + end + + if focus then + currentHotkeys:focusChild(hotkeyLabel) + currentHotkeys:ensureChildVisible(hotkeyLabel) + updateHotkeyForm(true) + end + configValueChanged = true +end + +function doKeyCombo(keyCombo) + if not g_game.isOnline() then return end + if modules.game_console and modules.game_console.isChatEnabled() then + if keyCombo:len() == 1 then + return + end + end + local hotKey = hotkeyList[keyCombo] + if not hotKey then return end + + if hotKey.lastHotkeyTime ~= nil and g_clock.millis() - hotKey.lastHotkeyTime < 100 then + return + end + + if hotKey.itemId == nil then + if not hotKey.value or #hotKey.value == 0 then return end + if hotKey.autoSend then + modules.game_console.sendMessage(hotKey.value) + else + modules.game_console.setTextEditText(hotKey.value) + end + hotKey.lastHotkeyTime = g_clock.millis() + elseif hotKey.useType == HOTKEY_MANAGER_USE then + if g_game.getClientVersion() < 740 then + local item = g_game.findPlayerItem(hotKey.itemId, hotKey.subType or -1) + if item then + g_game.use(item) + end + else + g_game.useInventoryItem(hotKey.itemId) + end + hotKey.lastHotkeyTime = g_clock.millis() + elseif hotKey.useType == HOTKEY_MANAGER_USEONSELF then + if g_game.getClientVersion() < 740 then + local item = g_game.findPlayerItem(hotKey.itemId, hotKey.subType or -1) + if item then + g_game.useWith(item, g_game.getLocalPlayer()) + end + else + g_game.useInventoryItemWith(hotKey.itemId, g_game.getLocalPlayer(), hotKey.subType or -1) + end + hotKey.lastHotkeyTime = g_clock.millis() + elseif hotKey.useType == HOTKEY_MANAGER_USEONTARGET then + local attackingCreature = g_game.getAttackingCreature() + if not attackingCreature then + local item = Item.create(hotKey.itemId) + if g_game.getClientVersion() < 740 then + local tmpItem = g_game.findPlayerItem(hotKey.itemId, hotKey.subType or -1) + if not tmpItem then return end + item = tmpItem + end + + modules.game_interface.startUseWith(item, hotKey.subType or - 1) + return + end + + if not attackingCreature:getTile() then return end + if g_game.getClientVersion() < 740 then + local item = g_game.findPlayerItem(hotKey.itemId, hotKey.subType or -1) + if item then + g_game.useWith(item, attackingCreature, hotKey.subType or -1) + end + else + g_game.useInventoryItemWith(hotKey.itemId, attackingCreature, hotKey.subType or -1) + end + hotKey.lastHotkeyTime = g_clock.millis() + elseif hotKey.useType == HOTKEY_MANAGER_USEWITH then + local item = Item.create(hotKey.itemId) + if g_game.getClientVersion() < 740 then + local tmpItem = g_game.findPlayerItem(hotKey.itemId, hotKey.subType or -1) + if not tmpItem then return true end + item = tmpItem + end + modules.game_interface.startUseWith(item, hotKey.subType or - 1) + end +end + +function updateHotkeyLabel(hotkeyLabel) + if not hotkeyLabel then return end + if hotkeyLabel.useType == HOTKEY_MANAGER_USEONSELF then + hotkeyLabel:setText(tr('%s: (use object on yourself)', hotkeyLabel.keyCombo)) + hotkeyLabel:setColor(HotkeyColors.itemUseSelf) + elseif hotkeyLabel.useType == HOTKEY_MANAGER_USEONTARGET then + hotkeyLabel:setText(tr('%s: (use object on target)', hotkeyLabel.keyCombo)) + hotkeyLabel:setColor(HotkeyColors.itemUseTarget) + elseif hotkeyLabel.useType == HOTKEY_MANAGER_USEWITH then + hotkeyLabel:setText(tr('%s: (use object with crosshair)', hotkeyLabel.keyCombo)) + hotkeyLabel:setColor(HotkeyColors.itemUseWith) + elseif hotkeyLabel.itemId ~= nil then + hotkeyLabel:setText(tr('%s: (use object)', hotkeyLabel.keyCombo)) + hotkeyLabel:setColor(HotkeyColors.itemUse) + else + local text = hotkeyLabel.keyCombo .. ': ' + if hotkeyLabel.value then + text = text .. hotkeyLabel.value + end + hotkeyLabel:setText(text) + if hotkeyLabel.autoSend then + hotkeyLabel:setColor(HotkeyColors.autoSend) + else + hotkeyLabel:setColor(HotkeyColors.text) + end + end +end + +function updateHotkeyForm(reset) + configValueChanged = true + if currentHotkeyLabel then + removeHotkeyButton:enable() + if currentHotkeyLabel.itemId ~= nil then + hotkeyText:clearText() + hotkeyText:disable() + hotKeyTextLabel:disable() + sendAutomatically:setChecked(false) + sendAutomatically:disable() + selectObjectButton:disable() + clearObjectButton:enable() + currentItemPreview:setItemId(currentHotkeyLabel.itemId) + if currentHotkeyLabel.subType then + currentItemPreview:setItemSubType(currentHotkeyLabel.subType) + end + if currentItemPreview:getItem():isMultiUse() then + useOnSelf:enable() + useOnTarget:enable() + useWith:enable() + if currentHotkeyLabel.useType == HOTKEY_MANAGER_USEONSELF then + useRadioGroup:selectWidget(useOnSelf) + elseif currentHotkeyLabel.useType == HOTKEY_MANAGER_USEONTARGET then + useRadioGroup:selectWidget(useOnTarget) + elseif currentHotkeyLabel.useType == HOTKEY_MANAGER_USEWITH then + useRadioGroup:selectWidget(useWith) + end + else + useOnSelf:disable() + useOnTarget:disable() + useWith:disable() + useRadioGroup:clearSelected() + end + else + useOnSelf:disable() + useOnTarget:disable() + useWith:disable() + useRadioGroup:clearSelected() + hotkeyText:enable() + hotkeyText:focus() + hotKeyTextLabel:enable() + if reset then + hotkeyText:setCursorPos(-1) + end + hotkeyText:setText(currentHotkeyLabel.value) + sendAutomatically:setChecked(currentHotkeyLabel.autoSend) + sendAutomatically:setEnabled(currentHotkeyLabel.value and #currentHotkeyLabel.value > 0) + selectObjectButton:enable() + clearObjectButton:disable() + currentItemPreview:clearItem() + end + else + removeHotkeyButton:disable() + hotkeyText:disable() + sendAutomatically:disable() + selectObjectButton:disable() + clearObjectButton:disable() + useOnSelf:disable() + useOnTarget:disable() + useWith:disable() + hotkeyText:clearText() + useRadioGroup:clearSelected() + sendAutomatically:setChecked(false) + currentItemPreview:clearItem() + end +end + +function removeHotkey() + if currentHotkeyLabel == nil then return end + g_keyboard.unbindKeyPress(currentHotkeyLabel.keyCombo, boundCombosCallback[currentHotkeyLabel.keyCombo]) + boundCombosCallback[currentHotkeyLabel.keyCombo] = nil + currentHotkeyLabel:destroy() + currentHotkeyLabel = nil +end + +function onHotkeyTextChange(value) + if not hotkeysManagerLoaded then return end + if currentHotkeyLabel == nil then return end + currentHotkeyLabel.value = value + if value == '' then + currentHotkeyLabel.autoSend = false + end + updateHotkeyLabel(currentHotkeyLabel) + updateHotkeyForm() +end + +function onSendAutomaticallyChange(autoSend) + if not hotkeysManagerLoaded then return end + if currentHotkeyLabel == nil then return end + if not currentHotkeyLabel.value or #currentHotkeyLabel.value == 0 then return end + currentHotkeyLabel.autoSend = autoSend + updateHotkeyLabel(currentHotkeyLabel) + updateHotkeyForm() +end + +function onChangeUseType(useTypeWidget) + if not hotkeysManagerLoaded then return end + if currentHotkeyLabel == nil then return end + if useTypeWidget == useOnSelf then + currentHotkeyLabel.useType = HOTKEY_MANAGER_USEONSELF + elseif useTypeWidget == useOnTarget then + currentHotkeyLabel.useType = HOTKEY_MANAGER_USEONTARGET + elseif useTypeWidget == useWith then + currentHotkeyLabel.useType = HOTKEY_MANAGER_USEWITH + else + currentHotkeyLabel.useType = HOTKEY_MANAGER_USE + end + updateHotkeyLabel(currentHotkeyLabel) + updateHotkeyForm() +end + +function onSelectHotkeyLabel(hotkeyLabel) + currentHotkeyLabel = hotkeyLabel + updateHotkeyForm(true) +end + +function hotkeyCapture(assignWindow, keyCode, keyboardModifiers) + local keyCombo = determineKeyComboDesc(keyCode, keyboardModifiers) + local comboPreview = assignWindow:getChildById('comboPreview') + comboPreview:setText(tr('Current hotkey to add: %s', keyCombo)) + comboPreview.keyCombo = keyCombo + comboPreview:resizeToText() + assignWindow:getChildById('addButton'):enable() + return true +end + +function hotkeyCaptureOk(assignWindow, keyCombo) + addKeyCombo(keyCombo, nil, true) + assignWindow:destroy() +end diff --git a/modules/game_hotkeys/hotkeys_manager.otmod b/modules/game_hotkeys/hotkeys_manager.otmod new file mode 100644 index 0000000..4c29b94 --- /dev/null +++ b/modules/game_hotkeys/hotkeys_manager.otmod @@ -0,0 +1,9 @@ +Module + name: game_hotkeys + description: Manage client hotkeys + author: andrefaramir, BeniS + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ hotkeys_manager ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_hotkeys/hotkeys_manager.otui b/modules/game_hotkeys/hotkeys_manager.otui new file mode 100644 index 0000000..6489a2f --- /dev/null +++ b/modules/game_hotkeys/hotkeys_manager.otui @@ -0,0 +1,240 @@ +HotkeyListLabel < UILabel + font: verdana-11px-monochrome + background-color: alpha + text-offset: 2 0 + focusable: true + phantom: false + + $focus: + background-color: #ffffff22 + +MainWindow + id: hotkeysWindow + !text: tr('Hotkeys') + size: 340 445 + + @onEnter: modules.game_hotkeys.ok() + @onEscape: modules.game_hotkeys.cancel() + + ComboBox + id: configSelector + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + menu-scroll: true + menu-height: 125 + menu-scroll-step: 25 + text-offset: 5 2 + @onOptionChange: modules.game_hotkeys.onConfigChange() + @onSetup: | + self:addOption(tr("Hotkeys config #1")) + self:addOption(tr("Hotkeys config #2")) + self:addOption(tr("Hotkeys config #3")) + self:addOption(tr("Hotkeys config #4")) + self:addOption(tr("Hotkeys config #5")) + + + VerticalScrollBar + id: currentHotkeysScrollBar + height: 150 + anchors.top: prev.bottom + anchors.right: parent.right + margin-top: 5 + step: 14 + pixels-scroll: true + + TextList + id: currentHotkeys + vertical-scrollbar: currentHotkeysScrollBar + anchors.left: parent.left + anchors.right: prev.left + anchors.top: prev.top + anchors.bottom: prev.bottom + focusable: false + + Button + id: resetButton + width: 96 + !text: tr('Reset All') + anchors.left: parent.left + anchors.top: next.top + @onClick: modules.game_hotkeys.reset() + margin-right: 10 + + Button + id: addHotkeyButton + !text: tr('Add') + width: 64 + anchors.right: next.left + anchors.top: next.top + margin-right: 5 + @onClick: modules.game_hotkeys.addHotkey() + + Button + id: removeHotkeyButton + !text: tr('Remove') + width: 64 + enabled: false + anchors.right: parent.right + anchors.top: currentHotkeys.bottom + margin-top: 8 + @onClick: modules.game_hotkeys.removeHotkey() + + Label + id: hotKeyTextLabel + !text: tr('Edit hotkey text:') + enable: false + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 10 + + TextEdit + id: hotkeyText + enabled: false + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 2 + @onTextChange: modules.game_hotkeys.onHotkeyTextChange(self:getText()) + + CheckBox + id: sendAutomatically + !text: tr('Send automatically') + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + enabled:false + margin-top: 5 + @onCheckChange: modules.game_hotkeys.onSendAutomaticallyChange(self:isChecked()) + + Item + id: itemPreview + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 10 + virtual: true + + Button + id: selectObjectButton + !text: tr('Select object') + width: 128 + enabled: false + anchors.left: prev.right + anchors.top: prev.top + margin-left: 10 + @onClick: modules.game_hotkeys.startChooseItem() + + Button + id: clearObjectButton + !text: tr('Clear object') + width: 128 + enabled: false + anchors.left: prev.left + anchors.right: prev.right + anchors.top: prev.bottom + margin-top: 2 + @onClick: modules.game_hotkeys.clearObject() + + ButtonBox + id: useOnSelf + !text: tr('Use on yourself') + width: 128 + enabled: false + anchors.left: selectObjectButton.right + anchors.right: parent.right + anchors.top: selectObjectButton.top + checked: false + margin-left: 10 + + ButtonBox + id: useOnTarget + !text: tr('Use on target') + width: 128 + enabled: false + anchors.left: prev.left + anchors.right: parent.right + anchors.top: prev.bottom + checked: false + margin-top: 2 + + ButtonBox + id: useWith + !text: tr('With crosshair') + width: 128 + enabled: false + anchors.left: prev.left + anchors.right: parent.right + anchors.top: prev.bottom + checked: false + margin-top: 2 + + HorizontalSeparator + id: separator + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: next.top + margin-bottom: 10 + + Button + id: okButton + !text: tr('Ok') + width: 64 + anchors.right: next.left + anchors.bottom: parent.bottom + @onClick: modules.game_hotkeys.ok() + margin-right: 10 + + Button + id: cancelButton + !text: tr('Cancel') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: modules.game_hotkeys.cancel() + +HotkeyAssignWindow < MainWindow + id: assignWindow + !text: tr('Button Assign') + size: 360 150 + @onEscape: self:destroy() + + Label + !text: tr('Please, press the key you wish to add onto your hotkeys manager') + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + text-auto-resize: true + text-align: left + + Label + id: comboPreview + !text: tr('Current hotkey to add: %s', 'none') + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 10 + text-auto-resize: true + + HorizontalSeparator + id: separator + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: next.top + margin-bottom: 10 + + Button + id: addButton + !text: tr('Add') + width: 64 + anchors.right: next.left + anchors.bottom: parent.bottom + margin-right: 10 + @onClick: modules.game_hotkeys.hotkeyCaptureOk(self:getParent(), self:getParent():getChildById('comboPreview').keyCombo) + + Button + id: cancelButton + !text: tr('Cancel') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: self:getParent():destroy() diff --git a/modules/game_interface/gameinterface.lua b/modules/game_interface/gameinterface.lua new file mode 100644 index 0000000..bfd8d9a --- /dev/null +++ b/modules/game_interface/gameinterface.lua @@ -0,0 +1,940 @@ +gameRootPanel = nil +gameMapPanel = nil +gameRightPanels = nil +gameLeftPanels = nil +gameBottomPanel = nil +logoutButton = nil +mouseGrabberWidget = nil +countWindow = nil +logoutWindow = nil +exitWindow = nil +bottomSplitter = nil +limitedZoom = false +hookedMenuOptions = {} +lastDirTime = g_clock.millis() + +function init() + g_ui.importStyle('styles/countwindow') + + connect(g_game, { + onGameStart = onGameStart, + onGameEnd = onGameEnd, + onLoginAdvice = onLoginAdvice, + }, true) + + -- Call load AFTER game window has been created and + -- resized to a stable state, otherwise the saved + -- settings can get overridden by false onGeometryChange + -- events + connect(g_app, { + onRun = load, + onExit = save + }) + + gameRootPanel = g_ui.displayUI('gameinterface') + gameRootPanel:hide() + gameRootPanel:lower() + gameRootPanel.onGeometryChange = updateStretchShrink + + mouseGrabberWidget = gameRootPanel:getChildById('mouseGrabber') + mouseGrabberWidget.onMouseRelease = onMouseGrabberRelease + + bottomSplitter = gameRootPanel:getChildById('bottomSplitter') + gameMapPanel = gameRootPanel:getChildById('gameMapPanel') + gameRightPanels = gameRootPanel:getChildById('gameRightPanels') + gameLeftPanels = gameRootPanel:getChildById('gameLeftPanels') + gameBottomPanel = gameRootPanel:getChildById('gameBottomPanel') + connect(gameLeftPanel, { onVisibilityChange = onLeftPanelVisibilityChange }) + + logoutButton = modules.client_topmenu.addLeftButton('logoutButton', tr('Exit'), + '/images/topbuttons/logout', tryLogout, true) + + + gameRightPanels:addChild(g_ui.createWidget('GameSidePanel')) + + refreshViewMode() + + bindKeys() + + connect(gameMapPanel, { onGeometryChange = updateSize, onVisibleDimensionChange = updateSize }) + connect(g_game, { onMapChangeAwareRange = updateSize }) + + if g_game.isOnline() then + show() + end +end + +function bindKeys() + gameRootPanel:setAutoRepeatDelay(20) + + g_keyboard.bindKeyPress('Escape', function() g_game.cancelAttackAndFollow() end, gameRootPanel) + g_keyboard.bindKeyPress('Ctrl+=', function() if g_game.getFeature(GameNoDebug) then return end gameMapPanel:zoomIn() end, gameRootPanel) + g_keyboard.bindKeyPress('Ctrl+-', function() if g_game.getFeature(GameNoDebug) then return end gameMapPanel:zoomOut() end, gameRootPanel) + g_keyboard.bindKeyDown('Ctrl+Q', function() tryLogout(false) end, gameRootPanel) + g_keyboard.bindKeyDown('Ctrl+L', function() tryLogout(false) end, gameRootPanel) + g_keyboard.bindKeyDown('Ctrl+W', function() g_map.cleanTexts() modules.game_textmessage.clearMessages() end, gameRootPanel) +end + +function terminate() + hide() + + hookedMenuOptions = {} + markThing = nil + + + disconnect(g_game, { + onGameStart = onGameStart, + onGameEnd = onGameEnd, + onLoginAdvice = onLoginAdvice + }) + + disconnect(gameMapPanel, { onGeometryChange = updateSize }) + connect(gameMapPanel, { onGeometryChange = updateSize, onVisibleDimensionChange = updateSize }) + + logoutButton:destroy() + gameRootPanel:destroy() +end + +function onGameStart() + refreshViewMode() + show() + + -- open tibia has delay in auto walking + if not g_game.isOfficialTibia() then + g_game.enableFeature(GameForceFirstAutoWalkStep) + else + g_game.disableFeature(GameForceFirstAutoWalkStep) + end +end + +function onGameEnd() + hide() + modules.client_topmenu.getTopMenu():setImageColor('white') +end + +function show() + connect(g_app, { onClose = tryExit }) + modules.client_background.hide() + gameRootPanel:show() + gameRootPanel:focus() + gameMapPanel:followCreature(g_game.getLocalPlayer()) + + updateStretchShrink() + logoutButton:setTooltip(tr('Logout')) + + addEvent(function() + if not limitedZoom or g_game.isGM() then + gameMapPanel:setMaxZoomOut(513) + gameMapPanel:setLimitVisibleRange(false) + else + gameMapPanel:setMaxZoomOut(15) + gameMapPanel:setLimitVisibleRange(true) + end + end) +end + +function hide() + disconnect(g_app, { onClose = tryExit }) + logoutButton:setTooltip(tr('Exit')) + + if logoutWindow then + logoutWindow:destroy() + logoutWindow = nil + end + if exitWindow then + exitWindow:destroy() + exitWindow = nil + end + if countWindow then + countWindow:destroy() + countWindow = nil + end + gameRootPanel:hide() + modules.client_background.show() +end + +function save() + local settings = {} + settings.splitterMarginBottom = bottomSplitter:getMarginBottom() + g_settings.setNode('game_interface', settings) +end + +function load() + local settings = g_settings.getNode('game_interface') + if settings then + if settings.splitterMarginBottom then + bottomSplitter:setMarginBottom(settings.splitterMarginBottom) + end + end +end + +function onLoginAdvice(message) + displayInfoBox(tr("For Your Information"), message) +end + +function forceExit() + g_game.cancelLogin() + scheduleEvent(exit, 10) + return true +end + +function tryExit() + if exitWindow then + return true + end + + local exitFunc = function() g_game.safeLogout() forceExit() end + local logoutFunc = function() g_game.safeLogout() exitWindow:destroy() exitWindow = nil end + local cancelFunc = function() exitWindow:destroy() exitWindow = nil end + + exitWindow = displayGeneralBox(tr('Exit'), tr("If you shut down the program, your character might stay in the game.\nClick on 'Logout' to ensure that you character leaves the game properly.\nClick on 'Exit' if you want to exit the program without logging out your character."), + { { text=tr('Force Exit'), callback=exitFunc }, + { text=tr('Logout'), callback=logoutFunc }, + { text=tr('Cancel'), callback=cancelFunc }, + anchor=AnchorHorizontalCenter }, logoutFunc, cancelFunc) + + return true +end + +function tryLogout(prompt) + if type(prompt) ~= "boolean" then + prompt = true + end + if not g_game.isOnline() then + exit() + return + end + + if logoutWindow then + return + end + + local msg, yesCallback + if not g_game.isConnectionOk() then + msg = 'Your connection is failing, if you logout now your character will be still online, do you want to force logout?' + + yesCallback = function() + g_game.forceLogout() + if logoutWindow then + logoutWindow:destroy() + logoutWindow=nil + end + end + else + msg = 'Are you sure you want to logout?' + + yesCallback = function() + g_game.safeLogout() + if logoutWindow then + logoutWindow:destroy() + logoutWindow=nil + end + end + end + + local noCallback = function() + logoutWindow:destroy() + logoutWindow=nil + end + + if prompt then + logoutWindow = displayGeneralBox(tr('Logout'), tr(msg), { + { text=tr('Yes'), callback=yesCallback }, + { text=tr('No'), callback=noCallback }, + anchor=AnchorHorizontalCenter}, yesCallback, noCallback) + else + yesCallback() + end +end + +function updateStretchShrink() + if modules.client_options.getOption('dontStretchShrink') and not alternativeView then + gameMapPanel:setVisibleDimension({ width = 15, height = 11 }) + + -- Set gameMapPanel size to height = 11 * 32 + 2 + bottomSplitter:setMarginBottom(bottomSplitter:getMarginBottom() + (gameMapPanel:getHeight() - 32 * 11) - 10) + end +end + +function onMouseGrabberRelease(self, mousePosition, mouseButton) + if selectedThing == nil then return false end + if mouseButton == MouseLeftButton then + local clickedWidget = gameRootPanel:recursiveGetChildByPos(mousePosition, false) + if clickedWidget then + if selectedType == 'use' then + onUseWith(clickedWidget, mousePosition) + elseif selectedType == 'trade' then + onTradeWith(clickedWidget, mousePosition) + end + end + end + + selectedThing = nil + g_mouse.popCursor('target') + self:ungrabMouse() + gameMapPanel:blockNextMouseRelease(true) + return true +end + +function onUseWith(clickedWidget, mousePosition) + if clickedWidget:getClassName() == 'UIGameMap' then + local tile = clickedWidget:getTile(mousePosition) + if tile then + if selectedThing:isFluidContainer() then + g_game.useWith(selectedThing, tile:getTopMultiUseThing(), selectedSubtype) + else + g_game.useWith(selectedThing, tile:getTopUseThing(), selectedSubtype) + end + end + elseif clickedWidget:getClassName() == 'UIItem' and not clickedWidget:isVirtual() then + g_game.useWith(selectedThing, clickedWidget:getItem(), selectedSubtype) + elseif clickedWidget:getClassName() == 'UICreatureButton' then + local creature = clickedWidget:getCreature() + if creature then + g_game.useWith(selectedThing, creature, selectedSubtype) + end + end +end + +function onTradeWith(clickedWidget, mousePosition) + if clickedWidget:getClassName() == 'UIGameMap' then + local tile = clickedWidget:getTile(mousePosition) + if tile then + g_game.requestTrade(selectedThing, tile:getTopCreatureEx(clickedWidget:getPositionOffset(mousePosition))) + end + elseif clickedWidget:getClassName() == 'UICreatureButton' then + local creature = clickedWidget:getCreature() + if creature then + g_game.requestTrade(selectedThing, creature) + end + end +end + +function startUseWith(thing, subType) + gameMapPanel:blockNextMouseRelease() + if not thing then return end + if g_ui.isMouseGrabbed() then + if selectedThing then + selectedThing = thing + selectedType = 'use' + end + return + end + selectedType = 'use' + selectedThing = thing + selectedSubtype = subType or 0 + mouseGrabberWidget:grabMouse() + g_mouse.pushCursor('target') +end + +function startTradeWith(thing) + if not thing then return end + if g_ui.isMouseGrabbed() then + if selectedThing then + selectedThing = thing + selectedType = 'trade' + end + return + end + selectedType = 'trade' + selectedThing = thing + mouseGrabberWidget:grabMouse() + g_mouse.pushCursor('target') +end + +function isMenuHookCategoryEmpty(category) + if category then + for _,opt in pairs(category) do + if opt then return false end + end + end + return true +end + +function addMenuHook(category, name, callback, condition, shortcut) + if not hookedMenuOptions[category] then + hookedMenuOptions[category] = {} + end + hookedMenuOptions[category][name] = { + callback = callback, + condition = condition, + shortcut = shortcut + } +end + +function removeMenuHook(category, name) + if not name then + hookedMenuOptions[category] = {} + else + hookedMenuOptions[category][name] = nil + end +end + +function createThingMenu(menuPosition, lookThing, useThing, creatureThing) + if not g_game.isOnline() then return end + + local menu = g_ui.createWidget('PopupMenu') + menu:setGameMenu(true) + + local classic = modules.client_options.getOption('classicControl') + local shortcut = nil + + if not classic then shortcut = '(Shift)' else shortcut = nil end + if lookThing then + menu:addOption(tr('Look'), function() g_game.look(lookThing) end, shortcut) + end + + if not classic then shortcut = '(Ctrl)' else shortcut = nil end + if useThing then + if useThing:isContainer() then + if useThing:getParentContainer() then + menu:addOption(tr('Open'), function() g_game.open(useThing, useThing:getParentContainer()) end, shortcut) + menu:addOption(tr('Open in new window'), function() g_game.open(useThing) end) + else + menu:addOption(tr('Open'), function() g_game.open(useThing) end, shortcut) + end + else + if useThing:isMultiUse() then + menu:addOption(tr('Use with ...'), function() startUseWith(useThing) end, shortcut) + else + menu:addOption(tr('Use'), function() g_game.use(useThing) end, shortcut) + end + end + + if useThing:isRotateable() then + menu:addOption(tr('Rotate'), function() g_game.rotate(useThing) end) + end + + if g_game.getFeature(GameBrowseField) and useThing:getPosition().x ~= 0xffff then + menu:addOption(tr('Browse Field'), function() g_game.browseField(useThing:getPosition()) end) + end + end + + if lookThing and not lookThing:isCreature() and not lookThing:isNotMoveable() and lookThing:isPickupable() then + menu:addSeparator() + menu:addOption(tr('Trade with ...'), function() startTradeWith(lookThing) end) + end + + if lookThing then + local parentContainer = lookThing:getParentContainer() + if parentContainer and parentContainer:hasParent() then + menu:addOption(tr('Move up'), function() g_game.moveToParentContainer(lookThing, lookThing:getCount()) end) + end + end + + if creatureThing then + local localPlayer = g_game.getLocalPlayer() + menu:addSeparator() + + if creatureThing:isLocalPlayer() then + menu:addOption(tr('Set Outfit'), function() g_game.requestOutfit() end) + + if g_game.getFeature(GamePlayerMounts) then + if not localPlayer:isMounted() then + menu:addOption(tr('Mount'), function() localPlayer:mount() end) + else + menu:addOption(tr('Dismount'), function() localPlayer:dismount() end) + end + end + + if creatureThing:isPartyMember() then + if creatureThing:isPartyLeader() then + if creatureThing:isPartySharedExperienceActive() then + menu:addOption(tr('Disable Shared Experience'), function() g_game.partyShareExperience(false) end) + else + menu:addOption(tr('Enable Shared Experience'), function() g_game.partyShareExperience(true) end) + end + end + menu:addOption(tr('Leave Party'), function() g_game.partyLeave() end) + end + + else + local localPosition = localPlayer:getPosition() + if not classic then shortcut = '(Alt)' else shortcut = nil end + if creatureThing:getPosition().z == localPosition.z then + if g_game.getAttackingCreature() ~= creatureThing then + menu:addOption(tr('Attack'), function() g_game.attack(creatureThing) end, shortcut) + else + menu:addOption(tr('Stop Attack'), function() g_game.cancelAttack() end, shortcut) + end + + if g_game.getFollowingCreature() ~= creatureThing then + menu:addOption(tr('Follow'), function() g_game.follow(creatureThing) end) + else + menu:addOption(tr('Stop Follow'), function() g_game.cancelFollow() end) + end + end + + if creatureThing:isPlayer() then + menu:addSeparator() + local creatureName = creatureThing:getName() + menu:addOption(tr('Message to %s', creatureName), function() g_game.openPrivateChannel(creatureName) end) + if modules.game_console.getOwnPrivateTab() then + menu:addOption(tr('Invite to private chat'), function() g_game.inviteToOwnChannel(creatureName) end) + menu:addOption(tr('Exclude from private chat'), function() g_game.excludeFromOwnChannel(creatureName) end) -- [TODO] must be removed after message's popup labels been implemented + end + if not localPlayer:hasVip(creatureName) then + menu:addOption(tr('Add to VIP list'), function() g_game.addVip(creatureName) end) + end + + if modules.game_console.isIgnored(creatureName) then + menu:addOption(tr('Unignore') .. ' ' .. creatureName, function() modules.game_console.removeIgnoredPlayer(creatureName) end) + else + menu:addOption(tr('Ignore') .. ' ' .. creatureName, function() modules.game_console.addIgnoredPlayer(creatureName) end) + end + + local localPlayerShield = localPlayer:getShield() + local creatureShield = creatureThing:getShield() + + if localPlayerShield == ShieldNone or localPlayerShield == ShieldWhiteBlue then + if creatureShield == ShieldWhiteYellow then + menu:addOption(tr('Join %s\'s Party', creatureThing:getName()), function() g_game.partyJoin(creatureThing:getId()) end) + else + menu:addOption(tr('Invite to Party'), function() g_game.partyInvite(creatureThing:getId()) end) + end + elseif localPlayerShield == ShieldWhiteYellow then + if creatureShield == ShieldWhiteBlue then + menu:addOption(tr('Revoke %s\'s Invitation', creatureThing:getName()), function() g_game.partyRevokeInvitation(creatureThing:getId()) end) + end + elseif localPlayerShield == ShieldYellow or localPlayerShield == ShieldYellowSharedExp or localPlayerShield == ShieldYellowNoSharedExpBlink or localPlayerShield == ShieldYellowNoSharedExp then + if creatureShield == ShieldWhiteBlue then + menu:addOption(tr('Revoke %s\'s Invitation', creatureThing:getName()), function() g_game.partyRevokeInvitation(creatureThing:getId()) end) + elseif creatureShield == ShieldBlue or creatureShield == ShieldBlueSharedExp or creatureShield == ShieldBlueNoSharedExpBlink or creatureShield == ShieldBlueNoSharedExp then + menu:addOption(tr('Pass Leadership to %s', creatureThing:getName()), function() g_game.partyPassLeadership(creatureThing:getId()) end) + else + menu:addOption(tr('Invite to Party'), function() g_game.partyInvite(creatureThing:getId()) end) + end + end + end + end + + if modules.game_ruleviolation.hasWindowAccess() and creatureThing:isPlayer() then + menu:addSeparator() + menu:addOption(tr('Rule Violation'), function() modules.game_ruleviolation.show(creatureThing:getName()) end) + end + + menu:addSeparator() + menu:addOption(tr('Copy Name'), function() g_window.setClipboardText(creatureThing:getName()) end) + end + + -- hooked menu options + for _,category in pairs(hookedMenuOptions) do + if not isMenuHookCategoryEmpty(category) then + menu:addSeparator() + for name,opt in pairs(category) do + if opt and opt.condition(menuPosition, lookThing, useThing, creatureThing) then + menu:addOption(name, function() opt.callback(menuPosition, + lookThing, useThing, creatureThing) end, opt.shortcut) + end + end + end + end + + if g_game.getFeature(GameBot) and useThing then + menu:addSeparator() + menu:addOption(tr("ID: " .. useThing:getId())) + end + + menu:display(menuPosition) +end + +function processMouseAction(menuPosition, mouseButton, autoWalkPos, lookThing, useThing, creatureThing, attackCreature, marking) + local keyboardModifiers = g_keyboard.getModifiers() + + if not modules.client_options.getOption('classicControl') then + if keyboardModifiers == KeyboardNoModifier and mouseButton == MouseRightButton then + createThingMenu(menuPosition, lookThing, useThing, creatureThing) + return true + elseif lookThing and keyboardModifiers == KeyboardShiftModifier and (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then + g_game.look(lookThing) + return true + elseif useThing and keyboardModifiers == KeyboardCtrlModifier and (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then + if useThing:isContainer() then + if useThing:getParentContainer() then + g_game.open(useThing, useThing:getParentContainer()) + else + g_game.open(useThing) + end + return true + elseif useThing:isMultiUse() then + startUseWith(useThing) + return true + else + g_game.use(useThing) + return true + end + return true + elseif attackCreature and g_keyboard.isAltPressed() and (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then + g_game.attack(attackCreature) + return true + elseif creatureThing and creatureThing:getPosition().z == autoWalkPos.z and g_keyboard.isAltPressed() and (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then + g_game.attack(creatureThing) + return true + end + + -- classic control + else + if useThing and keyboardModifiers == KeyboardNoModifier and mouseButton == MouseRightButton and not g_mouse.isPressed(MouseLeftButton) then + local player = g_game.getLocalPlayer() + if attackCreature and attackCreature ~= player then + g_game.attack(attackCreature) + return true + elseif creatureThing and creatureThing ~= player and creatureThing:getPosition().z == autoWalkPos.z then + g_game.attack(creatureThing) + return true + elseif useThing:isContainer() then + if useThing:getParentContainer() then + g_game.open(useThing, useThing:getParentContainer()) + return true + else + g_game.open(useThing) + return true + end + elseif useThing:isMultiUse() then + startUseWith(useThing) + return true + else + g_game.use(useThing) + return true + end + return true + elseif lookThing and keyboardModifiers == KeyboardShiftModifier and (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then + g_game.look(lookThing) + return true + elseif lookThing and ((g_mouse.isPressed(MouseLeftButton) and mouseButton == MouseRightButton) or (g_mouse.isPressed(MouseRightButton) and mouseButton == MouseLeftButton)) then + g_game.look(lookThing) + return true + elseif useThing and keyboardModifiers == KeyboardCtrlModifier and (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then + createThingMenu(menuPosition, lookThing, useThing, creatureThing) + return true + elseif attackCreature and g_keyboard.isAltPressed() and (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then + g_game.attack(attackCreature) + return true + elseif creatureThing and creatureThing:getPosition().z == autoWalkPos.z and g_keyboard.isAltPressed() and (mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then + g_game.attack(creatureThing) + return true + end + end + + local player = g_game.getLocalPlayer() + player:stopAutoWalk() + + if autoWalkPos and keyboardModifiers == KeyboardNoModifier and mouseButton == MouseLeftButton then + local autoWalkTile = g_map.getTile(autoWalkPos) + if autoWalkTile and not autoWalkTile:isWalkable(true) then + modules.game_textmessage.displayFailureMessage(tr('Sorry, not possible.')) + return false + end + player:autoWalk(autoWalkPos) + return true + end + + return false +end + +function moveStackableItem(item, toPos) + if countWindow then + return + end + if g_keyboard.isCtrlPressed() then + g_game.move(item, toPos, item:getCount()) + return + elseif g_keyboard.isShiftPressed() then + g_game.move(item, toPos, 1) + return + end + local count = item:getCount() + + countWindow = g_ui.createWidget('CountWindow', rootWidget) + local itembox = countWindow:getChildById('item') + local scrollbar = countWindow:getChildById('countScrollBar') + itembox:setItemId(item:getId()) + itembox:setItemCount(count) + scrollbar:setMaximum(count) + scrollbar:setMinimum(1) + scrollbar:setValue(count) + + local spinbox = countWindow:getChildById('spinBox') + spinbox:setMaximum(count) + spinbox:setMinimum(0) + spinbox:setValue(0) + spinbox:hideButtons() + spinbox:focus() + spinbox.firstEdit = true + + local spinBoxValueChange = function(self, value) + spinbox.firstEdit = false + scrollbar:setValue(value) + end + spinbox.onValueChange = spinBoxValueChange + + local check = function() + if spinbox.firstEdit then + spinbox:setValue(spinbox:getMaximum()) + spinbox.firstEdit = false + end + end + local okButton = countWindow:getChildById('buttonOk') + local moveFunc = function() + g_game.move(item, toPos, itembox:getItemCount()) + okButton:getParent():destroy() + countWindow = nil + end + local cancelButton = countWindow:getChildById('buttonCancel') + local cancelFunc = function() + cancelButton:getParent():destroy() + countWindow = nil + end + + + g_keyboard.bindKeyPress("Up", function() check() spinbox:up() end, spinbox) + g_keyboard.bindKeyPress("Down", function() check() spinbox:down() end, spinbox) + g_keyboard.bindKeyPress("Right", function() check() spinbox:up() end, spinbox) + g_keyboard.bindKeyPress("Left", function() check() spinbox:down() end, spinbox) + g_keyboard.bindKeyPress("PageUp", function() check() spinbox:setValue(spinbox:getValue()+10) end, spinbox) + g_keyboard.bindKeyPress("PageDown", function() check() spinbox:setValue(spinbox:getValue()-10) end, spinbox) + g_keyboard.bindKeyPress("Enter", function() moveFunc() end, spinbox) + + scrollbar.onValueChange = function(self, value) + itembox:setItemCount(value) + spinbox.onValueChange = nil + spinbox:setValue(value) + spinbox.onValueChange = spinBoxValueChange + end + countWindow.onEnter = moveFunc + countWindow.onEscape = cancelFunc + + okButton.onClick = moveFunc + cancelButton.onClick = cancelFunc +end + +function getRootPanel() + return gameRootPanel +end + +function getMapPanel() + return gameMapPanel +end + +function getRightPanel() + if gameRightPanels:getChildCount() == 0 then + addRightPanel() + end + return gameRightPanels:getChildByIndex(-1) +end + +function getContainerPanel() + local containerPanel = g_settings.getNumber("containerPanel") + if containerPanel >= 5 then + containerPanel = containerPanel - 4 + return gameRightPanels:getChildByIndex(math.min(containerPanel, gameRightPanels:getChildCount())) + end + if gameLeftPanels:getChildCount() == 0 then + return getRightPanel() + end + return gameLeftPanels:getChildByIndex(math.min(containerPanel, gameLeftPanels:getChildCount())) +end + +local function addRightPanel() + if gameRightPanels:getChildCount() >= 4 then + return + end + local panel = g_ui.createWidget('GameSidePanel') + panel:setId("rightPanel" .. (gameRightPanels:getChildCount() + 1)) + gameRightPanels:insertChild(1, panel) +end + +local function addLeftPanel() + if gameLeftPanels:getChildCount() >= 4 then + return + end + local panel = g_ui.createWidget('GameSidePanel') + panel:setId("leftPanel" .. (gameLeftPanels:getChildCount() + 1)) + gameLeftPanels:addChild(panel) +end + +local function removeRightPanel() + if gameRightPanels:getChildCount() <= 1 then + return + end + local panel = gameRightPanels:getChildByIndex(1) + panel:moveTo(gameRightPanels:getChildByIndex(2)) + gameRightPanels:removeChild(panel) +end + +local function removeLeftPanel() + if gameLeftPanels:getChildCount() == 0 then + return + end + local panel = gameLeftPanels:getChildByIndex(-1) + if gameLeftPanels:getChildCount() >= 2 then + panel:moveTo(gameLeftPanels:getChildByIndex(-2)) + else + panel:moveTo(gameRightPanels:getChildByIndex(1)) + end + gameLeftPanels:removeChild(panel) +end + +function getBottomPanel() + return gameBottomPanel +end + +function refreshViewMode() + local classic = g_settings.getBoolean("classicView") + local rightPanels = g_settings.getNumber("rightPanels") - gameRightPanels:getChildCount() + local leftPanels = g_settings.getNumber("leftPanels") - 1 - gameLeftPanels:getChildCount() + + while rightPanels ~= 0 do + if rightPanels > 0 then + addRightPanel() + rightPanels = rightPanels - 1 + else + removeRightPanel() + rightPanels = rightPanels + 1 + end + end + while leftPanels ~= 0 do + if leftPanels > 0 then + addLeftPanel() + leftPanels = leftPanels - 1 + else + removeLeftPanel() + leftPanels = leftPanels + 1 + end + end + + if not g_game.isOnline() then + return + end + + local minimumWidth = (g_settings.getNumber("rightPanels") + g_settings.getNumber("leftPanels") - 1) * 200 + if classic then + minimumWidth = minimumWidth + 300 + end + minimumWidth = math.max(minimumWidth, 800) + g_window.setMinimumSize({ width = minimumWidth, height = 600 }) + if g_window.getWidth() < minimumWidth then + local oldPos = g_window.getPosition() + local size = { width = minimumWidth, height = g_window.getHeight() } + g_window.resize(size) + g_window.move(oldPos) + end + + for i=1,gameRightPanels:getChildCount()+gameLeftPanels:getChildCount() do + local panel + if i > gameRightPanels:getChildCount() then + panel = gameLeftPanels:getChildByIndex(i - gameRightPanels:getChildCount()) + else + panel = gameRightPanels:getChildByIndex(i) + end + if classic then + panel:setImageColor('white') + else + panel:setImageColor('alpha') + end + end + + if classic then + gameRightPanels:setMarginTop(0) + gameLeftPanels:setMarginTop(0) + gameMapPanel:setMarginLeft(0) + gameMapPanel:setMarginRight(0) + else + gameLeftPanels:setMarginTop(modules.client_topmenu.getTopMenu():getHeight() - gameLeftPanels:getPaddingTop()) + gameRightPanels:setMarginTop(modules.client_topmenu.getTopMenu():getHeight() - gameRightPanels:getPaddingTop()) + end + + gameMapPanel:setVisibleDimension({ width = 15, height = 11 }) + + if classic then + gameRootPanel:addAnchor(AnchorTop, 'topMenu', AnchorBottom) + gameMapPanel:addAnchor(AnchorLeft, 'gameLeftPanels', AnchorRight) + gameMapPanel:addAnchor(AnchorRight, 'gameRightPanels', AnchorLeft) + gameMapPanel:addAnchor(AnchorBottom, 'gameBottomPanel', AnchorTop) + gameMapPanel:setKeepAspectRatio(true) + gameMapPanel:setLimitVisibleRange(false) + gameMapPanel:setZoom(11) + + gameBottomPanel:addAnchor(AnchorLeft, 'gameLeftPanels', AnchorRight) + gameBottomPanel:addAnchor(AnchorRight, 'gameRightPanels', AnchorLeft) + bottomSplitter:addAnchor(AnchorLeft, 'gameLeftPanels', AnchorRight) + bottomSplitter:addAnchor(AnchorRight, 'gameRightPanels', AnchorLeft) + + modules.client_topmenu.getTopMenu():setImageColor('white') + gameBottomPanel:setImageColor('white') + g_game.changeMapAwareRange(20, 16) + + if modules.game_console then + modules.game_console.switchMode(false) + end + else + g_game.changeMapAwareRange(30, 20) + gameMapPanel:fill('parent') + gameRootPanel:fill('parent') + gameMapPanel:setKeepAspectRatio(false) + gameMapPanel:setLimitVisibleRange(false) + if g_game.getFeature(GameChangeMapAwareRange) then + gameMapPanel:setZoom(13) + else + gameMapPanel:setZoom(11) + end + + gameBottomPanel:addAnchor(AnchorLeft, 'parent', AnchorLeft) + gameBottomPanel:addAnchor(AnchorRight, 'parent', AnchorRight) + bottomSplitter:addAnchor(AnchorLeft, 'parent', AnchorLeft) + bottomSplitter:addAnchor(AnchorRight, 'parent', AnchorRight) + + modules.client_topmenu.getTopMenu():setImageColor('#ffffff66') + + if modules.game_console then + modules.game_console.switchMode(true) + end + end +end + +function limitZoom() + limitedZoom = true +end + +function updateSize() + local classic = g_settings.getBoolean("classicView") + local height = gameMapPanel:getHeight() + local width = gameMapPanel:getWidth() + + if not classic and modules.game_console then + local newMargin = modules.game_console.consolePanel:getMarginLeft() + newMargin = math.max(0, newMargin) + newMargin = math.min(modules.game_console.consolePanel:getParent():getWidth() - modules.game_console.consolePanel:getWidth(), newMargin) + modules.game_console.consolePanel:setMarginLeft(newMargin) + end + + if not classic then + local rheight = gameRootPanel:getHeight() + local rwidth = gameRootPanel:getWidth() + + local dimenstion = gameMapPanel:getVisibleDimension() + local zoom = gameMapPanel:getZoom() + local awareRange = g_map.getAwareRange() + local dheight = dimenstion.height + local dwidth = dimenstion.width + local tileSize = rheight / dheight + local maxWidth = tileSize * (awareRange.width - 4) + local margin = math.max(0, math.floor((rwidth - maxWidth) / 2)) + gameMapPanel:setMarginLeft(margin) + gameMapPanel:setMarginRight(margin) + end + + --[[ + local maxWidth = math.floor(height * 2) + local extraMargin = 0 + if width >= maxWidth then + extraMargin = math.ceil((width - maxWidth) / 2) + end + local bottomMaxWidth = 1200 -- something broken, it's not pixels + local bottomMargin = 0 + if width > bottomMaxWidth then + bottomMargin = math.ceil((width - bottomMaxWidth) / 2) + end + gameMapPanel:setMarginLeft(extraMargin) + gameMapPanel:setMarginRight(extraMargin) ]] +end diff --git a/modules/game_interface/gameinterface.otui b/modules/game_interface/gameinterface.otui new file mode 100644 index 0000000..bef21d8 --- /dev/null +++ b/modules/game_interface/gameinterface.otui @@ -0,0 +1,79 @@ +GameSidePanel < UIMiniWindowContainer + image-source: /images/ui/panel_side + image-border: 4 + padding: 4 + padding-top: 0 + width: 198 + focusable: false + on: true + layout: + type: verticalBox + //spacing: 1 + +GameBottomPanel < Panel + +GameMapPanel < UIGameMap + padding: 4 + image-source: /images/ui/panel_map + image-border: 4 + + $on: + padding: 0 + +UIWidget + id: gameRootPanel + anchors.fill: parent + anchors.top: topMenu.bottom + + GameMapPanel + id: gameMapPanel + anchors.left: gameLeftPanels.right + anchors.right: gameRightPanels.left + anchors.top: parent.top + anchors.bottom: gameBottomPanel.top + focusable: false + + Panel + id: gameLeftPanels + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + focusable: false + layout: + type: horizontalBox + fit-children: true + spacing: -1 + + Panel + id: gameRightPanels + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + focusable: false + layout: + type: horizontalBox + fit-children: true + spacing: -1 + + GameBottomPanel + id: gameBottomPanel + anchors.left: gameLeftPanels.right + anchors.right: gameRightPanels.left + anchors.top: bottomSplitter.top + anchors.bottom: parent.bottom + + Splitter + id: bottomSplitter + anchors.left: gameLeftPanels.right + anchors.right: gameRightPanels.left + anchors.bottom: parent.bottom + relative-margin: bottom + margin-bottom: 172 + @canUpdateMargin: function(self, newMargin) if modules.client_options.getOption('dontStretchShrink') then return self:getMarginBottom() end return math.max(math.min(newMargin, self:getParent():getHeight() - 300), 100) end + @onGeometryChange: function(self) self:setMarginBottom(math.min(math.max(self:getParent():getHeight() - 300, 100), self:getMarginBottom())) end + + UIWidget + id: mouseGrabber + focusable: false + visible: false + diff --git a/modules/game_interface/interface.otmod b/modules/game_interface/interface.otmod new file mode 100644 index 0000000..3594112 --- /dev/null +++ b/modules/game_interface/interface.otmod @@ -0,0 +1,36 @@ +Module + name: game_interface + description: Create the game interface, where the ingame stuff starts + author: OTClient team + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ widgets/uigamemap, widgets/uiitem, gameinterface ] + load-later: + - game_hotkeys + - game_questlog + - game_textmessage + - game_console + - game_outfit + - game_healthinfo + - game_skills + - game_inventory + - game_containers + - game_viplist + - game_battle + - game_minimap + - game_npctrade + - game_textwindow + - game_playertrade + - game_bugreport + - game_playerdeath + - game_playermount + - game_ruleviolation + - game_market + - game_spelllist + - game_cooldown + - game_modaldialog + - game_unjustifiedpoints + - game_walking + - game_shop + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_interface/styles/countwindow.otui b/modules/game_interface/styles/countwindow.otui new file mode 100644 index 0000000..6cef134 --- /dev/null +++ b/modules/game_interface/styles/countwindow.otui @@ -0,0 +1,53 @@ +CountWindow < MainWindow + id: countWindow + !text: tr('Move Stackable Item') + size: 196 90 + + SpinBox + id: spinBox + anchors.left: parent.left + anchors.top: parent.top + width: 1 + height: 1 + phantom: true + margin-top: 2 + focusable: true + + Item + id: item + anchors.left: parent.left + anchors.top: parent.top + margin-top: 2 + margin-left: -4 + focusable: false + virtual: true + + HorizontalScrollBar + id: countScrollBar + anchors.left: prev.right + anchors.right: parent.right + anchors.top: prev.top + margin-left: 10 + margin-top: -2 + focusable: false + + Button + id: buttonCancel + !text: tr('Cancel') + height: 20 + anchors.left: countScrollBar.horizontalCenter + anchors.right: countScrollBar.right + anchors.top: countScrollBar.bottom + margin-top: 7 + focusable: false + + Button + id: buttonOk + !text: tr('Ok') + height: 20 + anchors.right: countScrollBar.horizontalCenter + anchors.left: countScrollBar.left + anchors.top: countScrollBar.bottom + margin-top: 7 + margin-right: 6 + focusable: false diff --git a/modules/game_interface/widgets/uigamemap.lua b/modules/game_interface/widgets/uigamemap.lua new file mode 100644 index 0000000..221ec83 --- /dev/null +++ b/modules/game_interface/widgets/uigamemap.lua @@ -0,0 +1,191 @@ +UIGameMap = extends(UIMap, "UIGameMap") + +function UIGameMap.create() + local gameMap = UIGameMap.internalCreate() + gameMap:setKeepAspectRatio(true) + gameMap:setVisibleDimension({width = 15, height = 11}) + gameMap:setDrawLights(true) + gameMap.markedThing = nil + gameMap.blockNextRelease = 0 + gameMap:updateMarkedCreature() + return gameMap +end + +function UIGameMap:onDestroy() + if self.updateMarkedCreatureEvent then + removeEvent(self.updateMarkedCreatureEvent) + end +end + +function UIGameMap:markThing(thing, color) + if self.markedThing == thing then + return + end + if self.markedThing then + self.markedThing:setMarked('') + end + + self.markedThing = thing + if self.markedThing and g_settings.getBoolean('highlightThingsUnderCursor') then + self.markedThing:setMarked(color) + end +end + +function UIGameMap:onDragEnter(mousePos) + local tile = self:getTile(mousePos) + if not tile then return false end + + local thing = tile:getTopMoveThing() + if not thing then return false end + + self.currentDragThing = thing + + g_mouse.pushCursor('target') + self.allowNextRelease = false + return true +end + +function UIGameMap:onDragLeave(droppedWidget, mousePos) + self.currentDragThing = nil + self.hoveredWho = nil + g_mouse.popCursor('target') + return true +end + +function UIGameMap:onDrop(widget, mousePos) + if not self:canAcceptDrop(widget, mousePos) then return false end + + local tile = self:getTile(mousePos) + if not tile then return false end + + local thing = widget.currentDragThing + local toPos = tile:getPosition() + + local thingPos = thing:getPosition() + if thingPos.x == toPos.x and thingPos.y == toPos.y and thingPos.z == toPos.z then return false end + + if thing:isItem() and thing:getCount() > 1 then + modules.game_interface.moveStackableItem(thing, toPos) + else + g_game.move(thing, toPos, 1) + end + + return true +end + +function UIGameMap:onMouseMove(mousePos, mouseMoved) + self.mousePos = mousePos + return false +end + +function UIGameMap:onDragMove(mousePos, mouseMoved) + self.mousePos = mousePos + return false +end + +function UIGameMap:updateMarkedCreature() + self.updateMarkedCreatureEvent = scheduleEvent(function() self:updateMarkedCreature() end, 100) + if self.mousePos and g_game.isOnline() then + self.markingMouseRelease = true + self:onMouseRelease(self.mousePos, MouseRightButton) + self.markingMouseRelease = false + end +end + +function UIGameMap:onMousePress() + if not self:isDragging() and self.blockNextRelease < g_clock.millis() then + self.allowNextRelease = true + self.markingMouseRelease = false + end +end + +function UIGameMap:blockNextMouseRelease(postAction) + self.allowNextRelease = false + if postAction then + self.blockNextRelease = g_clock.millis() + 150 + else + self.blockNextRelease = g_clock.millis() + 250 + end +end + +function UIGameMap:onMouseRelease(mousePosition, mouseButton) + if not self.allowNextRelease and not self.markingMouseRelease then + return true + end + + local autoWalkPos = self:getPosition(mousePosition) + local positionOffset = self:getPositionOffset(mousePosition) + + -- happens when clicking outside of map boundaries + if not autoWalkPos then + if self.markingMouseRelease then + self:markThing(nil) + end + return false + end + + local localPlayerPos = g_game.getLocalPlayer():getPosition() + if autoWalkPos.z ~= localPlayerPos.z then + local dz = autoWalkPos.z - localPlayerPos.z + autoWalkPos.x = autoWalkPos.x + dz + autoWalkPos.y = autoWalkPos.y + dz + autoWalkPos.z = localPlayerPos.z + end + + local lookThing + local useThing + local creatureThing + local multiUseThing + local attackCreature + + local tile = self:getTile(mousePosition) + if tile then + lookThing = tile:getTopLookThingEx(positionOffset) + useThing = tile:getTopUseThing() + creatureThing = tile:getTopCreatureEx(positionOffset) + end + + local autoWalkTile = g_map.getTile(autoWalkPos) + if autoWalkTile then + attackCreature = autoWalkTile:getTopCreatureEx(positionOffset) + end + + if self.markingMouseRelease then + if attackCreature then + self:markThing(attackCreature, 'yellow') + elseif creatureThing then + self:markThing(creatureThing, 'yellow') + elseif useThing and not useThing:isGround() then + self:markThing(useThing, 'yellow') + elseif lookThing and not lookThing:isGround() then + self:markThing(lookThing, 'yellow') + else + self:markThing(nil, '') + end + return + end + + local ret = modules.game_interface.processMouseAction(mousePosition, mouseButton, autoWalkPos, lookThing, useThing, creatureThing, attackCreature, self.markingMouseRelease) + if ret then + self.allowNextRelease = false + end + + return ret +end + +function UIGameMap:canAcceptDrop(widget, mousePos) + if not widget or not widget.currentDragThing then return false end + + local children = rootWidget:recursiveGetChildrenByPos(mousePos) + for i=1,#children do + local child = children[i] + if child == self then + return true + elseif not child:isPhantom() then + return false + end + end + + error('Widget ' .. self:getId() .. ' not in drop list.') + return false +end diff --git a/modules/game_interface/widgets/uiitem.lua b/modules/game_interface/widgets/uiitem.lua new file mode 100644 index 0000000..4e8941c --- /dev/null +++ b/modules/game_interface/widgets/uiitem.lua @@ -0,0 +1,111 @@ +function UIItem:onDragEnter(mousePos) + if self:isVirtual() then return false end + + local item = self:getItem() + if not item then return false end + + self:setBorderWidth(1) + self.currentDragThing = item + g_mouse.pushCursor('target') + return true +end + +function UIItem:onDragLeave(droppedWidget, mousePos) + if self:isVirtual() then return false end + self.currentDragThing = nil + g_mouse.popCursor('target') + self:setBorderWidth(0) + self.hoveredWho = nil + return true +end + +function UIItem:onDrop(widget, mousePos, forced) + if not self:canAcceptDrop(widget, mousePos) and not forced then return false end + + local item = widget.currentDragThing + if not item or not item:isItem() then return false end + + local toPos = self.position + + local itemPos = item:getPosition() + if itemPos.x == toPos.x and itemPos.y == toPos.y and itemPos.z == toPos.z then return false end + + if item:getCount() > 1 then + modules.game_interface.moveStackableItem(item, toPos) + else + g_game.move(item, toPos, 1) + end + + self:setBorderWidth(0) + return true +end + +function UIItem:onDestroy() + if self == g_ui.getDraggingWidget() and self.hoveredWho then + self.hoveredWho:setBorderWidth(0) + end + + if self.hoveredWho then + self.hoveredWho = nil + end +end + +function UIItem:onHoverChange(hovered) + UIWidget.onHoverChange(self, hovered) + + if self:isVirtual() or not self:isDraggable() then return end + + local draggingWidget = g_ui.getDraggingWidget() + if draggingWidget and self ~= draggingWidget then + local gotMap = draggingWidget:getClassName() == 'UIGameMap' + local gotItem = draggingWidget:getClassName() == 'UIItem' and not draggingWidget:isVirtual() + if hovered and (gotItem or gotMap) then + self:setBorderWidth(1) + draggingWidget.hoveredWho = self + else + self:setBorderWidth(0) + draggingWidget.hoveredWho = nil + end + end +end + +function UIItem:onMouseRelease(mousePosition, mouseButton) + if self.cancelNextRelease then + self.cancelNextRelease = false + return true + end + + if self:isVirtual() then return false end + + local item = self:getItem() + if not item or not self:containsPoint(mousePosition) then return false end + + if modules.client_options.getOption('classicControl') and + ((g_mouse.isPressed(MouseLeftButton) and mouseButton == MouseRightButton) or + (g_mouse.isPressed(MouseRightButton) and mouseButton == MouseLeftButton)) then + g_game.look(item) + self.cancelNextRelease = true + return true + elseif modules.game_interface.processMouseAction(mousePosition, mouseButton, nil, item, item, nil, nil) then + return true + end + return false +end + +function UIItem:canAcceptDrop(widget, mousePos) + if self:isVirtual() or not self:isDraggable() then return false end + if not widget or not widget.currentDragThing then return false end + + local children = rootWidget:recursiveGetChildrenByPos(mousePos) + for i=1,#children do + local child = children[i] + if child == self then + return true + elseif not child:isPhantom() then + return false + end + end + + error('Widget ' .. self:getId() .. ' not in drop list.') + return false +end diff --git a/modules/game_inventory/inventory.lua b/modules/game_inventory/inventory.lua new file mode 100644 index 0000000..75bdb66 --- /dev/null +++ b/modules/game_inventory/inventory.lua @@ -0,0 +1,487 @@ +Icons = {} +Icons[PlayerStates.Poison] = { tooltip = tr('You are poisoned'), path = '/images/game/states/poisoned', id = 'condition_poisoned' } +Icons[PlayerStates.Burn] = { tooltip = tr('You are burning'), path = '/images/game/states/burning', id = 'condition_burning' } +Icons[PlayerStates.Energy] = { tooltip = tr('You are electrified'), path = '/images/game/states/electrified', id = 'condition_electrified' } +Icons[PlayerStates.Drunk] = { tooltip = tr('You are drunk'), path = '/images/game/states/drunk', id = 'condition_drunk' } +Icons[PlayerStates.ManaShield] = { tooltip = tr('You are protected by a magic shield'), path = '/images/game/states/magic_shield', id = 'condition_magic_shield' } +Icons[PlayerStates.Paralyze] = { tooltip = tr('You are paralysed'), path = '/images/game/states/slowed', id = 'condition_slowed' } +Icons[PlayerStates.Haste] = { tooltip = tr('You are hasted'), path = '/images/game/states/haste', id = 'condition_haste' } +Icons[PlayerStates.Swords] = { tooltip = tr('You may not logout during a fight'), path = '/images/game/states/logout_block', id = 'condition_logout_block' } +Icons[PlayerStates.Drowning] = { tooltip = tr('You are drowning'), path = '/images/game/states/drowning', id = 'condition_drowning' } +Icons[PlayerStates.Freezing] = { tooltip = tr('You are freezing'), path = '/images/game/states/freezing', id = 'condition_freezing' } +Icons[PlayerStates.Dazzled] = { tooltip = tr('You are dazzled'), path = '/images/game/states/dazzled', id = 'condition_dazzled' } +Icons[PlayerStates.Cursed] = { tooltip = tr('You are cursed'), path = '/images/game/states/cursed', id = 'condition_cursed' } +Icons[PlayerStates.PartyBuff] = { tooltip = tr('You are strengthened'), path = '/images/game/states/strengthened', id = 'condition_strengthened' } +Icons[PlayerStates.PzBlock] = { tooltip = tr('You may not logout or enter a protection zone'), path = '/images/game/states/protection_zone_block', id = 'condition_protection_zone_block' } +Icons[PlayerStates.Pz] = { tooltip = tr('You are within a protection zone'), path = '/images/game/states/protection_zone', id = 'condition_protection_zone' } +Icons[PlayerStates.Bleeding] = { tooltip = tr('You are bleeding'), path = '/images/game/states/bleeding', id = 'condition_bleeding' } +Icons[PlayerStates.Hungry] = { tooltip = tr('You are hungry'), path = '/images/game/states/hungry', id = 'condition_hungry' } + +InventorySlotStyles = { + [InventorySlotHead] = "HeadSlot", + [InventorySlotNeck] = "NeckSlot", + [InventorySlotBack] = "BackSlot", + [InventorySlotBody] = "BodySlot", + [InventorySlotRight] = "RightSlot", + [InventorySlotLeft] = "LeftSlot", + [InventorySlotLeg] = "LegSlot", + [InventorySlotFeet] = "FeetSlot", + [InventorySlotFinger] = "FingerSlot", + [InventorySlotAmmo] = "AmmoSlot" +} + +inventoryWindow = nil +inventoryPanel = nil +inventoryButton = nil +purseButton = nil + +combatControlsWindow = nil +fightOffensiveBox = nil +fightBalancedBox = nil +fightDefensiveBox = nil +chaseModeButton = nil +safeFightButton = nil +mountButton = nil +fightModeRadioGroup = nil +buttonPvp = nil + +soulLabel = nil +capLabel = nil +conditionPanel = nil + +function init() + connect(LocalPlayer, { + onInventoryChange = onInventoryChange, + onBlessingsChange = onBlessingsChange + }) + connect(g_game, { onGameStart = refresh }) + + g_keyboard.bindKeyDown('Ctrl+I', toggle) + + + inventoryWindow = g_ui.loadUI('inventory', modules.game_interface.getRightPanel()) + inventoryWindow:disableResize() + inventoryPanel = inventoryWindow:getChildById('contentsPanel'):getChildById('inventoryPanel') + if not inventoryWindow.forceOpen then + inventoryButton = modules.client_topmenu.addRightGameToggleButton('inventoryButton', tr('Inventory') .. ' (Ctrl+I)', '/images/topbuttons/inventory', toggle) + inventoryButton:setOn(true) + end + --[[ + purseButton = inventoryPanel:getChildById('purseButton') + local function purseFunction() + local purse = g_game.getLocalPlayer():getInventoryItem(InventorySlotPurse) + if purse then + g_game.use(purse) + end + end + purseButton.onClick = purseFunction + ]]-- +-- controls + fightOffensiveBox = inventoryWindow:recursiveGetChildById('fightOffensiveBox') + fightBalancedBox = inventoryWindow:recursiveGetChildById('fightBalancedBox') + fightDefensiveBox = inventoryWindow:recursiveGetChildById('fightDefensiveBox') + + chaseModeButton = inventoryWindow:recursiveGetChildById('chaseModeBox') + safeFightButton = inventoryWindow:recursiveGetChildById('safeFightBox') + buttonPvp = inventoryWindow:recursiveGetChildById('buttonPvp') + + mountButton = inventoryWindow:recursiveGetChildById('mountButton') + mountButton.onClick = onMountButtonClick + + whiteDoveBox = inventoryWindow:recursiveGetChildById('whiteDoveBox') + whiteHandBox = inventoryWindow:recursiveGetChildById('whiteHandBox') + yellowHandBox = inventoryWindow:recursiveGetChildById('yellowHandBox') + redFistBox = inventoryWindow:recursiveGetChildById('redFistBox') + + fightModeRadioGroup = UIRadioGroup.create() + fightModeRadioGroup:addWidget(fightOffensiveBox) + fightModeRadioGroup:addWidget(fightBalancedBox) + fightModeRadioGroup:addWidget(fightDefensiveBox) + + connect(fightModeRadioGroup, { onSelectionChange = onSetFightMode }) + connect(chaseModeButton, { onCheckChange = onSetChaseMode }) + connect(safeFightButton, { onCheckChange = onSetSafeFight }) + if buttonPvp then + connect(buttonPvp, { onClick = onSetSafeFight2 }) + end + connect(g_game, { + onGameStart = online, + onGameEnd = offline, + onFightModeChange = update, + onChaseModeChange = update, + onSafeFightChange = update, + onPVPModeChange = update, + onWalk = check, + onAutoWalk = check + }) + + connect(LocalPlayer, { onOutfitChange = onOutfitChange }) + + if g_game.isOnline() then + online() + end +-- controls end + +-- status + soulLabel = inventoryWindow:recursiveGetChildById('soulLabel') + capLabel = inventoryWindow:recursiveGetChildById('capLabel') + conditionPanel = inventoryWindow:recursiveGetChildById('conditionPanel') + + + connect(LocalPlayer, { onStatesChange = onStatesChange, + onSoulChange = onSoulChange, + onFreeCapacityChange = onFreeCapacityChange }) +-- status end + + refresh() + inventoryWindow:setup() +end + +function terminate() + disconnect(LocalPlayer, { + onInventoryChange = onInventoryChange, + onBlessingsChange = onBlessingsChange + }) + disconnect(g_game, { onGameStart = refresh }) + + g_keyboard.unbindKeyDown('Ctrl+I') + + -- controls + if g_game.isOnline() then + offline() + end + + fightModeRadioGroup:destroy() + + disconnect(g_game, { + onGameStart = online, + onGameEnd = offline, + onFightModeChange = update, + onChaseModeChange = update, + onSafeFightChange = update, + onPVPModeChange = update, + onWalk = check, + onAutoWalk = check + }) + + disconnect(LocalPlayer, { onOutfitChange = onOutfitChange }) + -- controls end + -- status + disconnect(LocalPlayer, { onStatesChange = onStatesChange, + onSoulChange = onSoulChange, + onFreeCapacityChange = onFreeCapacityChange }) + -- status end + + inventoryWindow:destroy() + if inventoryButton then + inventoryButton:destroy() + end +end + +function toggleAdventurerStyle(hasBlessing) + for slot = InventorySlotFirst, InventorySlotLast do + local itemWidget = inventoryPanel:getChildById('slot' .. slot) + if itemWidget then + itemWidget:setOn(hasBlessing) + end + end +end + +function refresh() + local player = g_game.getLocalPlayer() + for i = InventorySlotFirst, InventorySlotPurse do + if g_game.isOnline() then + onInventoryChange(player, i, player:getInventoryItem(i)) + else + onInventoryChange(player, i, nil) + end + toggleAdventurerStyle(player and Bit.hasBit(player:getBlessings(), Blessings.Adventurer) or false) + end + if player then + onSoulChange(player, player:getSoul()) + onFreeCapacityChange(player, player:getFreeCapacity()) + end + + --purseButton:setVisible(g_game.getFeature(GamePurseSlot)) +end + +function toggle() + if not inventoryButton then + return + end + if inventoryButton:isOn() then + inventoryWindow:close() + inventoryButton:setOn(false) + else + inventoryWindow:open() + inventoryButton:setOn(true) + end +end + +function onMiniWindowClose() + if not inventoryButton then + return + end + inventoryButton:setOn(false) +end + +-- hooked events +function onInventoryChange(player, slot, item, oldItem) + if slot > InventorySlotPurse then return end + + if slot == InventorySlotPurse then + if g_game.getFeature(GamePurseSlot) then + --purseButton:setEnabled(item and true or false) + end + return + end + + local itemWidget = inventoryPanel:getChildById('slot' .. slot) + if item then + itemWidget:setStyle('InventoryItem') + itemWidget:setItem(item) + else + itemWidget:setStyle(InventorySlotStyles[slot]) + itemWidget:setItem(nil) + end +end + +function onBlessingsChange(player, blessings, oldBlessings) + local hasAdventurerBlessing = Bit.hasBit(blessings, Blessings.Adventurer) + if hasAdventurerBlessing ~= Bit.hasBit(oldBlessings, Blessings.Adventurer) then + toggleAdventurerStyle(hasAdventurerBlessing) + end +end + + +-- controls +function update() + local fightMode = g_game.getFightMode() + if fightMode == FightOffensive then + fightModeRadioGroup:selectWidget(fightOffensiveBox) + elseif fightMode == FightBalanced then + fightModeRadioGroup:selectWidget(fightBalancedBox) + else + fightModeRadioGroup:selectWidget(fightDefensiveBox) + end + + local chaseMode = g_game.getChaseMode() + chaseModeButton:setChecked(chaseMode == ChaseOpponent) + + local safeFight = g_game.isSafeFight() + safeFightButton:setChecked(not safeFight) + if buttonPvp then + if safeFight then + buttonPvp:setColor("#00BB00FF") + else + buttonPvp:setColor("#FF0000FF") + end + end + + if g_game.getFeature(GamePVPMode) then + local pvpMode = g_game.getPVPMode() + local pvpWidget = getPVPBoxByMode(pvpMode) + end +end + +function check() + if modules.client_options.getOption('autoChaseOverride') then + if g_game.isAttacking() and g_game.getChaseMode() == ChaseOpponent then + g_game.setChaseMode(DontChase) + end + end +end + +function online() + local player = g_game.getLocalPlayer() + if player then + local char = g_game.getCharacterName() + + local lastCombatControls = g_settings.getNode('LastCombatControls') + + if not table.empty(lastCombatControls) then + if lastCombatControls[char] then + g_game.setFightMode(lastCombatControls[char].fightMode) + g_game.setChaseMode(lastCombatControls[char].chaseMode) + g_game.setSafeFight(lastCombatControls[char].safeFight) + if lastCombatControls[char].pvpMode then + g_game.setPVPMode(lastCombatControls[char].pvpMode) + end + end + end + + if g_game.getFeature(GamePlayerMounts) then + mountButton:setVisible(true) + mountButton:setChecked(player:isMounted()) + else + mountButton:setVisible(false) + end + end + + update() +end + +function offline() + local lastCombatControls = g_settings.getNode('LastCombatControls') + if not lastCombatControls then + lastCombatControls = {} + end + + conditionPanel:destroyChildren() + + local player = g_game.getLocalPlayer() + if player then + local char = g_game.getCharacterName() + lastCombatControls[char] = { + fightMode = g_game.getFightMode(), + chaseMode = g_game.getChaseMode(), + safeFight = g_game.isSafeFight() + } + + if g_game.getFeature(GamePVPMode) then + lastCombatControls[char].pvpMode = g_game.getPVPMode() + end + + -- save last combat control settings + g_settings.setNode('LastCombatControls', lastCombatControls) + end +end + +function onSetFightMode(self, selectedFightButton) + if selectedFightButton == nil then return end + local buttonId = selectedFightButton:getId() + local fightMode + if buttonId == 'fightOffensiveBox' then + fightMode = FightOffensive + elseif buttonId == 'fightBalancedBox' then + fightMode = FightBalanced + else + fightMode = FightDefensive + end + g_game.setFightMode(fightMode) +end + +function onSetChaseMode(self, checked) + local chaseMode + if checked then + chaseMode = ChaseOpponent + else + chaseMode = DontChase + end + g_game.setChaseMode(chaseMode) +end + +function onSetSafeFight(self, checked) + g_game.setSafeFight(not checked) + if buttonPvp then + if not checked then + buttonPvp:setColor("#00BB00FF") + else + buttonPvp:setColor("#FF0000FF") + end + end +end + +function onSetSafeFight2(self) + onSetSafeFight(self, not safeFightButton:isChecked()) +end + +function onSetPVPMode(self, selectedPVPButton) + if selectedPVPButton == nil then + return + end + + local buttonId = selectedPVPButton:getId() + local pvpMode = PVPWhiteDove + if buttonId == 'whiteDoveBox' then + pvpMode = PVPWhiteDove + elseif buttonId == 'whiteHandBox' then + pvpMode = PVPWhiteHand + elseif buttonId == 'yellowHandBox' then + pvpMode = PVPYellowHand + elseif buttonId == 'redFistBox' then + pvpMode = PVPRedFist + end + + g_game.setPVPMode(pvpMode) +end + +function onMountButtonClick(self, mousePos) + local player = g_game.getLocalPlayer() + if player then + player:toggleMount() + end +end + +function onOutfitChange(localPlayer, outfit, oldOutfit) + if outfit.mount == oldOutfit.mount then + return + end + + mountButton:setChecked(outfit.mount ~= nil and outfit.mount > 0) +end + +function getPVPBoxByMode(mode) + local widget = nil + if mode == PVPWhiteDove then + widget = whiteDoveBox + elseif mode == PVPWhiteHand then + widget = whiteHandBox + elseif mode == PVPYellowHand then + widget = yellowHandBox + elseif mode == PVPRedFist then + widget = redFistBox + end + return widget +end + +-- status +function toggleIcon(bitChanged) + local icon = conditionPanel:getChildById(Icons[bitChanged].id) + if icon then + icon:destroy() + else + icon = loadIcon(bitChanged) + icon:setParent(conditionPanel) + end +end + +function loadIcon(bitChanged) + local icon = g_ui.createWidget('ConditionWidget', conditionPanel) + icon:setId(Icons[bitChanged].id) + icon:setImageSource(Icons[bitChanged].path) + icon:setTooltip(Icons[bitChanged].tooltip) + return icon +end + +function onSoulChange(localPlayer, soul) + if not soul then return end + soulLabel:setText(tr('Soul') .. ':\n' .. soul) +end + +function onFreeCapacityChange(player, freeCapacity) + if not freeCapacity then return end + if freeCapacity > 99 then + freeCapacity = math.floor(freeCapacity * 10) / 10 + end + if freeCapacity > 999 then + freeCapacity = math.floor(freeCapacity) + end + if freeCapacity > 99999 then + freeCapacity = 99999 + end + capLabel:setText(tr('Cap') .. ':\n' .. freeCapacity) +end + +function onStatesChange(localPlayer, now, old) + if now == old then return end + local bitsChanged = bit32.bxor(now, old) + for i = 1, 32 do + local pow = math.pow(2, i-1) + if pow > bitsChanged then break end + local bitChanged = bit32.band(bitsChanged, pow) + if bitChanged ~= 0 then + toggleIcon(bitChanged) + end + end +end \ No newline at end of file diff --git a/modules/game_inventory/inventory.otmod b/modules/game_inventory/inventory.otmod new file mode 100644 index 0000000..5ee5be7 --- /dev/null +++ b/modules/game_inventory/inventory.otmod @@ -0,0 +1,9 @@ +Module + name: game_inventory + description: View local player equipments window + author: baxnie, edubart, BeniS, otclientv8 + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ inventory ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_inventory/inventory.otui b/modules/game_inventory/inventory.otui new file mode 100644 index 0000000..a79808c --- /dev/null +++ b/modules/game_inventory/inventory.otui @@ -0,0 +1,282 @@ +InventoryItem < Item + $on: + image-source: /images/ui/item-blessed + +HeadSlot < InventoryItem + id: slot1 + image-source: /images/game/slots/head + &position: {x=65535, y=1, z=0} + $on: + image-source: /images/game/slots/head-blessed + +BodySlot < InventoryItem + id: slot4 + image-source: /images/game/slots/body + &position: {x=65535, y=4, z=0} + $on: + image-source: /images/game/slots/body-blessed + +LegSlot < InventoryItem + id: slot7 + image-source: /images/game/slots/legs + &position: {x=65535, y=7, z=0} + $on: + image-source: /images/game/slots/legs-blessed + +FeetSlot < InventoryItem + id: slot8 + image-source: /images/game/slots/feet + &position: {x=65535, y=8, z=0} + $on: + image-source: /images/game/slots/feet-blessed + +NeckSlot < InventoryItem + id: slot2 + image-source: /images/game/slots/neck + &position: {x=65535, y=2, z=0} + $on: + image-source: /images/game/slots/neck-blessed + +LeftSlot < InventoryItem + id: slot6 + image-source: /images/game/slots/left-hand + &position: {x=65535, y=6, z=0} + $on: + image-source: /images/game/slots/left-hand-blessed + +FingerSlot < InventoryItem + id: slot9 + image-source: /images/game/slots/finger + &position: {x=65535, y=9, z=0} + $on: + image-source: /images/game/slots/finger-blessed + +BackSlot < InventoryItem + id: slot3 + image-source: /images/game/slots/back + &position: {x=65535, y=3, z=0} + $on: + image-source: /images/game/slots/back-blessed + +RightSlot < InventoryItem + id: slot5 + image-source: /images/game/slots/right-hand + &position: {x=65535, y=5, z=0} + $on: + image-source: /images/game/slots/right-hand-blessed + +AmmoSlot < InventoryItem + id: slot10 + image-source: /images/game/slots/ammo + &position: {x=65535, y=10, z=0} + $on: + image-source: /images/game/slots/ammo-blessed + +PurseButton < Button + id: purseButton + size: 26 26 + !tooltip: tr('Open purse') + icon-source: /images/game/slots/purse + icon-size: 24 24 + icon-offset: 1 1 + +CombatBox < UICheckBox + size: 20 20 + image-clip: 0 0 20 20 + margin-left: 4 + + $checked: + image-clip: 0 20 20 20 + + +InventoryButton < Button + font: verdana-11px-antialised + height: 20 + +SoulCapLabel < GameLabel + text-align: center + color: #FFFFFF + font: cipsoftFont + margin-top: 4 + text-offset: 0 3 + width: 36 + height: 20 + icon-source: /images/game/slots/soulcap + +FightOffensiveBox < CombatBox + image-source: /images/game/combatmodes/fightoffensive +FightBalancedBox < CombatBox + image-source: /images/game/combatmodes/fightbalanced +FightDefensiveBox < CombatBox + image-source: /images/game/combatmodes/fightdefensive +ChaseModeBox < CombatBox + image-source: /images/game/combatmodes/chasemode +SafeFightBox < CombatBox + image-source: /images/game/combatmodes/safefight + +MountButton < CombatBox + image-source: /images/game/combatmodes/mount + +MiniWindow + id: inventoryWindow + !text: tr('Inventory') + icon: /images/topbuttons/inventory + height: 200 + @onClose: modules.game_inventory.onMiniWindowClose() + &save: true + --&forceOpen: true + + MiniWindowContents + anchors.left: parent.left + + Panel + id: inventoryPanel + margin-right: 63 + margin-top: 2 + anchors.fill: parent + + HeadSlot + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + margin-top: 3 + + BodySlot + anchors.top: prev.bottom + anchors.horizontalCenter: prev.horizontalCenter + margin-top: 3 + + LegSlot + anchors.top: prev.bottom + anchors.horizontalCenter: prev.horizontalCenter + margin-top: 3 + + FeetSlot + anchors.top: prev.bottom + anchors.horizontalCenter: prev.horizontalCenter + margin-top: 3 + + NeckSlot + anchors.top: slot1.top + anchors.right: slot1.left + margin-top: 10 + margin-right: 5 + + LeftSlot + anchors.top: prev.bottom + anchors.horizontalCenter: prev.horizontalCenter + margin-top: 3 + + FingerSlot + anchors.top: prev.bottom + anchors.horizontalCenter: prev.horizontalCenter + margin-top: 3 + + BackSlot + anchors.top: slot1.top + anchors.left: slot1.right + margin-top: 10 + margin-left: 5 + + RightSlot + anchors.top: prev.bottom + anchors.horizontalCenter: prev.horizontalCenter + margin-top: 3 + + AmmoSlot + anchors.top: prev.bottom + anchors.horizontalCenter: prev.horizontalCenter + margin-top: 3 + + SoulCapLabel + id: soulLabel + anchors.top: slot10.bottom + anchors.horizontalCenter: slot10.horizontalCenter + + SoulCapLabel + id: capLabel + anchors.top: slot9.bottom + anchors.horizontalCenter: slot9.horizontalCenter + + Panel + id: conditionPanel + layout: + type: horizontalBox + height: 22 + padding: 2 + anchors.top: slot8.bottom + anchors.left: slot6.left + anchors.right: slot5.right + margin-top: 4 + border-width: 1 + border-color: #00000077 + background-color: #ffffff11 + + Panel + margin-top: 5 + anchors.fill: parent + anchors.left: prev.right + + FightOffensiveBox + id: fightOffensiveBox + anchors.left: parent.left + anchors.top: parent.top + margin-left: 8 + + ChaseModeBox + id: chaseModeBox + anchors.left: prev.right + anchors.top: parent.top + + FightBalancedBox + id: fightBalancedBox + margin-top: 22 + anchors.left: parent.left + anchors.top: parent.top + margin-left: 8 + + SafeFightBox + id: safeFightBox + margin-top: 22 + anchors.left: prev.right + anchors.top: parent.top + + FightDefensiveBox + id: fightDefensiveBox + margin-top: 44 + anchors.left: parent.left + anchors.top: parent.top + margin-left: 8 + + MountButton + id: mountButton + margin-top: 44 + anchors.left: prev.right + anchors.top: parent.top + + Panel + margin-top: 4 + margin-right: 5 + anchors.fill: parent + anchors.top: prev.bottom + layout: + type: verticalBox + + InventoryButton + !text: tr('Stop') + @onClick: g_game.stop(); g_game.cancelAttackAndFollow() + + InventoryButton + !text: tr('Quests') + @onClick: g_game.requestQuestLog() + + InventoryButton + !text: tr('Options') + @onClick: modules.client_options.toggle() + + InventoryButton + !text: tr('Hotkeys') + @onClick: modules.game_hotkeys.toggle() + + InventoryButton + !text: tr('Logout') + @onClick: modules.game_interface.tryLogout() diff --git a/modules/game_market/market.lua b/modules/game_market/market.lua new file mode 100644 index 0000000..b6e3936 --- /dev/null +++ b/modules/game_market/market.lua @@ -0,0 +1,1267 @@ +--[[ + Finalizing Market: + Note: Feel free to work on any area and submit + it as a pull request from your git fork. + + BeniS's Skype: benjiz69 + + List: + * Add offer management: + - Current Offers + - Offer History + + * Clean up the interface building + - Add a new market interface file to handle building? + + * Extend information features + - Hover over offers for purchase information (balance after transaction, etc) + ]] + +Market = {} + +local protocol = runinsandbox('marketprotocol') + +marketWindow = nil +mainTabBar = nil +displaysTabBar = nil +offersTabBar = nil +selectionTabBar = nil + +marketOffersPanel = nil +browsePanel = nil +overviewPanel = nil +itemOffersPanel = nil +itemDetailsPanel = nil +itemStatsPanel = nil +myOffersPanel = nil +currentOffersPanel = nil +offerHistoryPanel = nil +itemsPanel = nil +selectedOffer = {} +selectedMyOffer = {} + +nameLabel = nil +feeLabel = nil +balanceLabel = nil +totalPriceEdit = nil +piecePriceEdit = nil +amountEdit = nil +searchEdit = nil +radioItemSet = nil +selectedItem = nil +offerTypeList = nil +categoryList = nil +subCategoryList = nil +slotFilterList = nil +createOfferButton = nil +buyButton = nil +sellButton = nil +anonymous = nil +filterButtons = {} + +buyOfferTable = nil +sellOfferTable = nil +detailsTable = nil +buyStatsTable = nil +sellStatsTable = nil + +buyCancelButton = nil +sellCancelButton = nil +buyMyOfferTable = nil +sellMyOfferTable = nil + +offerExhaust = {} +marketOffers = {} +marketItems = {} +information = {} +currentItems = {} +lastCreatedOffer = 0 +fee = 0 +averagePrice = 0 + +loaded = false + +local function isItemValid(item, category, searchFilter) + if not item or not item.marketData then + return false + end + + if not category then + category = MarketCategory.All + end + if item.marketData.category ~= category and category ~= MarketCategory.All then + return false + end + + -- filter item + local slotFilter = false + if slotFilterList:isEnabled() then + slotFilter = getMarketSlotFilterId(slotFilterList:getCurrentOption().text) + end + local marketData = item.marketData + + local filterVocation = filterButtons[MarketFilters.Vocation]:isChecked() + local filterLevel = filterButtons[MarketFilters.Level]:isChecked() + local filterDepot = filterButtons[MarketFilters.Depot]:isChecked() + + if slotFilter then + if slotFilter ~= 255 and item.thingType:getClothSlot() ~= slotFilter then + return false + end + end + local player = g_game.getLocalPlayer() + if filterLevel and marketData.requiredLevel and player:getLevel() < marketData.requiredLevel then + return false + end + if filterVocation and marketData.restrictVocation > 0 then + local voc = Bit.bit(information.vocation) + if not Bit.hasBit(marketData.restrictVocation, voc) then + return false + end + end + if filterDepot and Market.getDepotCount(item.marketData.tradeAs) <= 0 then + return false + end + if searchFilter then + return marketData.name:lower():find(searchFilter) + end + return true +end + +local function clearItems() + currentItems = {} + Market.refreshItemsWidget() +end + +local function clearOffers() + marketOffers[MarketAction.Buy] = {} + marketOffers[MarketAction.Sell] = {} + buyOfferTable:clearData() + sellOfferTable:clearData() +end + +local function clearMyOffers() + marketOffers[MarketAction.Buy] = {} + marketOffers[MarketAction.Sell] = {} + buyMyOfferTable:clearData() + sellMyOfferTable:clearData() +end + +local function clearFilters() + for _, filter in pairs(filterButtons) do + if filter and filter:isChecked() ~= filter.default then + filter:setChecked(filter.default) + end + end +end + +local function clearFee() + feeLabel:setText('') + fee = 20 +end + +local function refreshTypeList() + offerTypeList:clearOptions() + offerTypeList:addOption('Buy') + + if Market.isItemSelected() then + if Market.getDepotCount(selectedItem.item.marketData.tradeAs) > 0 then + offerTypeList:addOption('Sell') + end + end +end + +local function addOffer(offer, offerType) + if not offer then + return false + end + local id = offer:getId() + local player = offer:getPlayer() + local amount = offer:getAmount() + local price = offer:getPrice() + local timestamp = offer:getTimeStamp() + local itemName = offer:getItem():getMarketData().name + + buyOfferTable:toggleSorting(false) + sellOfferTable:toggleSorting(false) + + buyMyOfferTable:toggleSorting(false) + sellMyOfferTable:toggleSorting(false) + + if amount < 1 then return false end + if offerType == MarketAction.Buy then + if offer.warn then + buyOfferTable:setColumnStyle('OfferTableWarningColumn', true) + end + + local row = nil + if offer.var == MarketRequest.MyOffers then + row = buyMyOfferTable:addRow({ + {text = itemName}, + {text = price*amount}, + {text = price}, + {text = amount}, + {text = string.gsub(os.date('%c', timestamp), " ", " "), sortvalue = timestamp} + }) + else + row = buyOfferTable:addRow({ + {text = player}, + {text = amount}, + {text = price*amount}, + {text = price}, + {text = string.gsub(os.date('%c', timestamp), " ", " ")} + }) + end + row.ref = id + + if offer.warn then + row:setTooltip(tr('This offer is 25%% below the average market price')) + buyOfferTable:setColumnStyle('OfferTableColumn', true) + end + else + if offer.warn then + sellOfferTable:setColumnStyle('OfferTableWarningColumn', true) + end + + local row = nil + if offer.var == MarketRequest.MyOffers then + row = sellMyOfferTable:addRow({ + {text = itemName}, + {text = price*amount}, + {text = price}, + {text = amount}, + {text = string.gsub(os.date('%c', timestamp), " ", " "), sortvalue = timestamp} + }) + else + row = sellOfferTable:addRow({ + {text = player}, + {text = amount}, + {text = price*amount}, + {text = price}, + {text = string.gsub(os.date('%c', timestamp), " ", " "), sortvalue = timestamp} + }) + end + row.ref = id + + if offer.warn then + row:setTooltip(tr('This offer is 25%% above the average market price')) + sellOfferTable:setColumnStyle('OfferTableColumn', true) + end + end + + buyOfferTable:toggleSorting(false) + sellOfferTable:toggleSorting(false) + buyOfferTable:sort() + sellOfferTable:sort() + + buyMyOfferTable:toggleSorting(false) + sellMyOfferTable:toggleSorting(false) + buyMyOfferTable:sort() + sellMyOfferTable:sort() + + return true +end + +local function mergeOffer(offer) + if not offer then + return false + end + + local id = offer:getId() + local offerType = offer:getType() + local amount = offer:getAmount() + local replaced = false + + if offerType == MarketAction.Buy then + if averagePrice > 0 then + offer.warn = offer:getPrice() <= averagePrice - math.floor(averagePrice / 4) + end + + for i = 1, #marketOffers[MarketAction.Buy] do + local o = marketOffers[MarketAction.Buy][i] + -- replace existing offer + if o:isEqual(id) then + marketOffers[MarketAction.Buy][i] = offer + replaced = true + end + end + if not replaced then + table.insert(marketOffers[MarketAction.Buy], offer) + end + else + if averagePrice > 0 then + offer.warn = offer:getPrice() >= averagePrice + math.floor(averagePrice / 4) + end + + for i = 1, #marketOffers[MarketAction.Sell] do + local o = marketOffers[MarketAction.Sell][i] + -- replace existing offer + if o:isEqual(id) then + marketOffers[MarketAction.Sell][i] = offer + replaced = true + end + end + if not replaced then + table.insert(marketOffers[MarketAction.Sell], offer) + end + end + return true +end + +local function updateOffers(offers) + if not buyOfferTable or not sellOfferTable then + return + end + + balanceLabel:setColor('#bbbbbb') + selectedOffer[MarketAction.Buy] = nil + selectedOffer[MarketAction.Sell] = nil + + selectedMyOffer[MarketAction.Buy] = nil + selectedMyOffer[MarketAction.Sell] = nil + + -- clear existing offer data + buyOfferTable:clearData() + buyOfferTable:setSorting(4, TABLE_SORTING_DESC) + sellOfferTable:clearData() + sellOfferTable:setSorting(4, TABLE_SORTING_ASC) + + sellButton:setEnabled(false) + buyButton:setEnabled(false) + + buyCancelButton:setEnabled(false) + sellCancelButton:setEnabled(false) + + for _, offer in pairs(offers) do + mergeOffer(offer) + end + for type, offers in pairs(marketOffers) do + for i = 1, #offers do + addOffer(offers[i], type) + end + end +end + +local function updateDetails(itemId, descriptions, purchaseStats, saleStats) + if not selectedItem then + return + end + + -- update item details + detailsTable:clearData() + for k, desc in pairs(descriptions) do + local columns = { + {text = getMarketDescriptionName(desc[1])..':'}, + {text = desc[2]} + } + detailsTable:addRow(columns) + end + + -- update sale item statistics + sellStatsTable:clearData() + if table.empty(saleStats) then + sellStatsTable:addRow({{text = 'No information'}}) + else + local offerAmount = 0 + local transactions, totalPrice, highestPrice, lowestPrice = 0, 0, 0, 0 + for _, stat in pairs(saleStats) do + if not stat:isNull() then + offerAmount = offerAmount + 1 + transactions = transactions + stat:getTransactions() + totalPrice = totalPrice + stat:getTotalPrice() + local newHigh = stat:getHighestPrice() + if newHigh > highestPrice then + highestPrice = newHigh + end + local newLow = stat:getLowestPrice() + -- ?? getting '0xffffffff' result from lowest price in 9.60 cipsoft + if (lowestPrice == 0 or newLow < lowestPrice) and newLow ~= 0xffffffff then + lowestPrice = newLow + end + end + end + + if offerAmount >= 5 and transactions >= 10 then + averagePrice = math.round(totalPrice / transactions) + else + averagePrice = 0 + end + + sellStatsTable:addRow({{text = 'Total Transations:'}, {text = transactions}}) + sellStatsTable:addRow({{text = 'Highest Price:'}, {text = highestPrice}}) + + if totalPrice > 0 and transactions > 0 then + sellStatsTable:addRow({{text = 'Average Price:'}, + {text = math.floor(totalPrice/transactions)}}) + else + sellStatsTable:addRow({{text = 'Average Price:'}, {text = 0}}) + end + + sellStatsTable:addRow({{text = 'Lowest Price:'}, {text = lowestPrice}}) + end + + -- update buy item statistics + buyStatsTable:clearData() + if table.empty(purchaseStats) then + buyStatsTable:addRow({{text = 'No information'}}) + else + local transactions, totalPrice, highestPrice, lowestPrice = 0, 0, 0, 0 + for _, stat in pairs(purchaseStats) do + if not stat:isNull() then + transactions = transactions + stat:getTransactions() + totalPrice = totalPrice + stat:getTotalPrice() + local newHigh = stat:getHighestPrice() + if newHigh > highestPrice then + highestPrice = newHigh + end + local newLow = stat:getLowestPrice() + -- ?? getting '0xffffffff' result from lowest price in 9.60 cipsoft + if (lowestPrice == 0 or newLow < lowestPrice) and newLow ~= 0xffffffff then + lowestPrice = newLow + end + end + end + + buyStatsTable:addRow({{text = 'Total Transations:'},{text = transactions}}) + buyStatsTable:addRow({{text = 'Highest Price:'}, {text = highestPrice}}) + + if totalPrice > 0 and transactions > 0 then + buyStatsTable:addRow({{text = 'Average Price:'}, + {text = math.floor(totalPrice/transactions)}}) + else + buyStatsTable:addRow({{text = 'Average Price:'}, {text = 0}}) + end + + buyStatsTable:addRow({{text = 'Lowest Price:'}, {text = lowestPrice}}) + end +end + +local function updateSelectedItem(widget) + selectedItem.item = widget.item + selectedItem.ref = widget + + Market.resetCreateOffer() + if Market.isItemSelected() then + selectedItem:setItem(selectedItem.item.displayItem) + nameLabel:setText(selectedItem.item.marketData.name) + clearOffers() + + Market.enableCreateOffer(true) -- update offer types + MarketProtocol.sendMarketBrowse(selectedItem.item.marketData.tradeAs) -- send browsed msg + else + Market.clearSelectedItem() + end +end + +local function updateBalance(balance) + local balance = tonumber(balance) + if not balance then + return + end + + if balance < 0 then balance = 0 end + information.balance = balance + + balanceLabel:setText('Balance: '..balance..' gold') + balanceLabel:resizeToText() +end + +local function updateFee(price, amount) + fee = math.ceil(price / 100 * amount) + if fee < 20 then + fee = 20 + elseif fee > 1000 then + fee = 1000 + end + feeLabel:setText('Fee: '..fee) + feeLabel:resizeToText() +end + +local function destroyAmountWindow() + if amountWindow then + amountWindow:destroy() + amountWindow = nil + end +end + +local function cancelMyOffer(actionType) + local offer = selectedMyOffer[actionType] + MarketProtocol.sendMarketCancelOffer(offer:getTimeStamp(), offer:getCounter()) + Market.refreshMyOffers() +end + +local function openAmountWindow(callback, actionType, actionText) + if not Market.isOfferSelected(actionType) then + return + end + + amountWindow = g_ui.createWidget('AmountWindow', rootWidget) + amountWindow:lock() + + local offer = selectedOffer[actionType] + local item = offer:getItem() + + local maximum = offer:getAmount() + if actionType == MarketAction.Sell then + local depot = Market.getDepotCount(item:getId()) + if maximum > depot then + maximum = depot + end + else + maximum = math.min(maximum, math.floor(information.balance / offer:getPrice())) + end + + if item:isStackable() then + maximum = math.min(maximum, MarketMaxAmountStackable) + else + maximum = math.min(maximum, MarketMaxAmount) + end + + local itembox = amountWindow:getChildById('item') + itembox:setItemId(item:getId()) + + local scrollbar = amountWindow:getChildById('amountScrollBar') + scrollbar:setText(offer:getPrice()..'gp') + + scrollbar.onValueChange = function(widget, value) + widget:setText((value*offer:getPrice())..'gp') + itembox:setText(value) + end + + scrollbar:setRange(1, maximum) + scrollbar:setValue(1) + + local okButton = amountWindow:getChildById('buttonOk') + if actionText then + okButton:setText(actionText) + end + + local okFunc = function() + local counter = offer:getCounter() + local timestamp = offer:getTimeStamp() + callback(scrollbar:getValue(), timestamp, counter) + destroyAmountWindow() + end + + local cancelButton = amountWindow:getChildById('buttonCancel') + local cancelFunc = function() + destroyAmountWindow() + end + + amountWindow.onEnter = okFunc + amountWindow.onEscape = cancelFunc + + okButton.onClick = okFunc + cancelButton.onClick = cancelFunc +end + +local function onSelectSellOffer(table, selectedRow, previousSelectedRow) + updateBalance() + for _, offer in pairs(marketOffers[MarketAction.Sell]) do + if offer:isEqual(selectedRow.ref) then + selectedOffer[MarketAction.Buy] = offer + end + end + + local offer = selectedOffer[MarketAction.Buy] + if offer then + local price = offer:getPrice() + if price > information.balance then + balanceLabel:setColor('#b22222') -- red + buyButton:setEnabled(false) + else + local slice = (information.balance / 2) + if (price/slice) * 100 <= 40 then + color = '#008b00' -- green + elseif (price/slice) * 100 <= 70 then + color = '#eec900' -- yellow + else + color = '#ee9a00' -- orange + end + balanceLabel:setColor(color) + buyButton:setEnabled(true) + end + end +end + +local function onSelectBuyOffer(table, selectedRow, previousSelectedRow) + updateBalance() + for _, offer in pairs(marketOffers[MarketAction.Buy]) do + if offer:isEqual(selectedRow.ref) then + selectedOffer[MarketAction.Sell] = offer + if Market.getDepotCount(offer:getItem():getId()) > 0 then + sellButton:setEnabled(true) + else + sellButton:setEnabled(false) + end + end + end +end + +local function onSelectMyBuyOffer(table, selectedRow, previousSelectedRow) + for _, offer in pairs(marketOffers[MarketAction.Buy]) do + if offer:isEqual(selectedRow.ref) then + selectedMyOffer[MarketAction.Buy] = offer + buyCancelButton:setEnabled(true) + end + end +end + +local function onSelectMySellOffer(table, selectedRow, previousSelectedRow) + for _, offer in pairs(marketOffers[MarketAction.Sell]) do + if offer:isEqual(selectedRow.ref) then + selectedMyOffer[MarketAction.Sell] = offer + sellCancelButton:setEnabled(true) + end + end +end + +local function onChangeCategory(combobox, option) + local id = getMarketCategoryId(option) + if id == MarketCategory.MetaWeapons then + -- enable and load weapons filter/items + subCategoryList:setEnabled(true) + slotFilterList:setEnabled(true) + local subId = getMarketCategoryId(subCategoryList:getCurrentOption().text) + Market.loadMarketItems(subId) + else + subCategoryList:setEnabled(false) + slotFilterList:setEnabled(false) + Market.loadMarketItems(id) -- load standard filter + end +end + +local function onChangeSubCategory(combobox, option) + Market.loadMarketItems(getMarketCategoryId(option)) + slotFilterList:clearOptions() + + local subId = getMarketCategoryId(subCategoryList:getCurrentOption().text) + local slots = MarketCategoryWeapons[subId].slots + for _, slot in pairs(slots) do + if table.haskey(MarketSlotFilters, slot) then + slotFilterList:addOption(MarketSlotFilters[slot]) + end + end + slotFilterList:setEnabled(true) +end + +local function onChangeSlotFilter(combobox, option) + Market.updateCurrentItems() +end + +local function onChangeOfferType(combobox, option) + local item = selectedItem.item + local maximum = item.thingType:isStackable() and MarketMaxAmountStackable or MarketMaxAmount + + if option == 'Sell' then + maximum = math.min(maximum, Market.getDepotCount(item.marketData.tradeAs)) + amountEdit:setMaximum(maximum) + else + amountEdit:setMaximum(maximum) + end +end + +local function onTotalPriceChange() + local amount = amountEdit:getValue() + local totalPrice = totalPriceEdit:getValue() + local piecePrice = math.floor(totalPrice/amount) + + piecePriceEdit:setValue(piecePrice, true) + if Market.isItemSelected() then + updateFee(piecePrice, amount) + end +end + +local function onPiecePriceChange() + local amount = amountEdit:getValue() + local totalPrice = totalPriceEdit:getValue() + local piecePrice = piecePriceEdit:getValue() + + totalPriceEdit:setValue(piecePrice*amount, true) + if Market.isItemSelected() then + updateFee(piecePrice, amount) + end +end + +local function onAmountChange() + local amount = amountEdit:getValue() + local piecePrice = piecePriceEdit:getValue() + local totalPrice = piecePrice * amount + + totalPriceEdit:setValue(piecePrice*amount, true) + if Market.isItemSelected() then + updateFee(piecePrice, amount) + end +end + +local function onMarketMessage(messageMode, message) + Market.displayMessage(message) +end + +local function initMarketItems() + for c = MarketCategory.First, MarketCategory.Last do + marketItems[c] = {} + end + + -- save a list of items which are already added + local itemSet = {} + + -- populate all market items + local types = g_things.findThingTypeByAttr(ThingAttrMarket, 0) + for i = 1, #types do + local itemType = types[i] + + local item = Item.create(itemType:getId()) + if item then + local marketData = itemType:getMarketData() + if not table.empty(marketData) and not itemSet[marketData.tradeAs] then + -- Some items use a different sprite in Market + item:setId(marketData.showAs) + + -- create new marketItem block + local marketItem = { + displayItem = item, + thingType = itemType, + marketData = marketData + } + + -- add new market item + table.insert(marketItems[marketData.category], marketItem) + itemSet[marketData.tradeAs] = true + end + end + end +end + +local function initInterface() + -- TODO: clean this up + -- setup main tabs + mainTabBar = marketWindow:getChildById('mainTabBar') + mainTabBar:setContentWidget(marketWindow:getChildById('mainTabContent')) + + -- setup 'Market Offer' section tabs + marketOffersPanel = g_ui.loadUI('ui/marketoffers') + mainTabBar:addTab(tr('Market Offers'), marketOffersPanel) + + selectionTabBar = marketOffersPanel:getChildById('leftTabBar') + selectionTabBar:setContentWidget(marketOffersPanel:getChildById('leftTabContent')) + + browsePanel = g_ui.loadUI('ui/marketoffers/browse') + selectionTabBar:addTab(tr('Browse'), browsePanel) + + -- Currently not used + -- "Reserved for more functionality later" + --overviewPanel = g_ui.loadUI('ui/marketoffers/overview') + --selectionTabBar:addTab(tr('Overview'), overviewPanel) + + displaysTabBar = marketOffersPanel:getChildById('rightTabBar') + displaysTabBar:setContentWidget(marketOffersPanel:getChildById('rightTabContent')) + + itemStatsPanel = g_ui.loadUI('ui/marketoffers/itemstats') + displaysTabBar:addTab(tr('Statistics'), itemStatsPanel) + + itemDetailsPanel = g_ui.loadUI('ui/marketoffers/itemdetails') + displaysTabBar:addTab(tr('Details'), itemDetailsPanel) + + itemOffersPanel = g_ui.loadUI('ui/marketoffers/itemoffers') + displaysTabBar:addTab(tr('Offers'), itemOffersPanel) + displaysTabBar:selectTab(displaysTabBar:getTab(tr('Offers'))) + + -- setup 'My Offer' section tabs + myOffersPanel = g_ui.loadUI('ui/myoffers') + mainTabBar:addTab(tr('My Offers'), myOffersPanel) + + offersTabBar = myOffersPanel:getChildById('offersTabBar') + offersTabBar:setContentWidget(myOffersPanel:getChildById('offersTabContent')) + + currentOffersPanel = g_ui.loadUI('ui/myoffers/currentoffers') + offersTabBar:addTab(tr('Current Offers'), currentOffersPanel) + + offerHistoryPanel = g_ui.loadUI('ui/myoffers/offerhistory') + offersTabBar:addTab(tr('Offer History'), offerHistoryPanel) + + balanceLabel = marketWindow:getChildById('balanceLabel') + + -- setup offers + buyButton = itemOffersPanel:getChildById('buyButton') + buyButton.onClick = function() openAmountWindow(Market.acceptMarketOffer, MarketAction.Buy, 'Buy') end + + sellButton = itemOffersPanel:getChildById('sellButton') + sellButton.onClick = function() openAmountWindow(Market.acceptMarketOffer, MarketAction.Sell, 'Sell') end + + -- setup selected item + nameLabel = marketOffersPanel:getChildById('nameLabel') + selectedItem = marketOffersPanel:getChildById('selectedItem') + + -- setup create new offer + totalPriceEdit = marketOffersPanel:getChildById('totalPriceEdit') + piecePriceEdit = marketOffersPanel:getChildById('piecePriceEdit') + amountEdit = marketOffersPanel:getChildById('amountEdit') + feeLabel = marketOffersPanel:getChildById('feeLabel') + totalPriceEdit.onValueChange = onTotalPriceChange + piecePriceEdit.onValueChange = onPiecePriceChange + amountEdit.onValueChange = onAmountChange + + offerTypeList = marketOffersPanel:getChildById('offerTypeComboBox') + offerTypeList.onOptionChange = onChangeOfferType + + anonymous = marketOffersPanel:getChildById('anonymousCheckBox') + createOfferButton = marketOffersPanel:getChildById('createOfferButton') + createOfferButton.onClick = Market.createNewOffer + Market.enableCreateOffer(false) + + -- setup filters + filterButtons[MarketFilters.Vocation] = browsePanel:getChildById('filterVocation') + filterButtons[MarketFilters.Level] = browsePanel:getChildById('filterLevel') + filterButtons[MarketFilters.Depot] = browsePanel:getChildById('filterDepot') + filterButtons[MarketFilters.SearchAll] = browsePanel:getChildById('filterSearchAll') + + -- set filter default values + clearFilters() + + -- hook filters + for _, filter in pairs(filterButtons) do + filter.onCheckChange = Market.updateCurrentItems + end + + searchEdit = browsePanel:getChildById('searchEdit') + categoryList = browsePanel:getChildById('categoryComboBox') + subCategoryList = browsePanel:getChildById('subCategoryComboBox') + slotFilterList = browsePanel:getChildById('slotComboBox') + + slotFilterList:addOption(MarketSlotFilters[255]) + slotFilterList:setEnabled(false) + + for i = MarketCategory.First, MarketCategory.Last do + if i >= MarketCategory.Ammunition and i <= MarketCategory.WandsRods then + subCategoryList:addOption(getMarketCategoryName(i)) + else + categoryList:addOption(getMarketCategoryName(i)) + end + end + categoryList:addOption(getMarketCategoryName(255)) -- meta weapons + categoryList:setCurrentOption(getMarketCategoryName(MarketCategory.First)) + subCategoryList:setEnabled(false) + + -- hook item filters + categoryList.onOptionChange = onChangeCategory + subCategoryList.onOptionChange = onChangeSubCategory + slotFilterList.onOptionChange = onChangeSlotFilter + + -- setup tables + buyOfferTable = itemOffersPanel:recursiveGetChildById('buyingTable') + sellOfferTable = itemOffersPanel:recursiveGetChildById('sellingTable') + detailsTable = itemDetailsPanel:recursiveGetChildById('detailsTable') + buyStatsTable = itemStatsPanel:recursiveGetChildById('buyStatsTable') + sellStatsTable = itemStatsPanel:recursiveGetChildById('sellStatsTable') + buyOfferTable.onSelectionChange = onSelectBuyOffer + sellOfferTable.onSelectionChange = onSelectSellOffer + + -- setup my offers + buyMyOfferTable = currentOffersPanel:recursiveGetChildById('myBuyingTable') + sellMyOfferTable = currentOffersPanel:recursiveGetChildById('mySellingTable') + buyMyOfferTable.onSelectionChange = onSelectMyBuyOffer + sellMyOfferTable.onSelectionChange = onSelectMySellOffer + + buyCancelButton = currentOffersPanel:getChildById('buyCancelButton') + buyCancelButton.onClick = function() cancelMyOffer(MarketAction.Buy) end + + sellCancelButton = currentOffersPanel:getChildById('sellCancelButton') + sellCancelButton.onClick = function() cancelMyOffer(MarketAction.Sell) end + + + buyStatsTable:setColumnWidth({120, 270}) + sellStatsTable:setColumnWidth({120, 270}) + detailsTable:setColumnWidth({80, 330}) + + buyOfferTable:setSorting(4, TABLE_SORTING_DESC) + sellOfferTable:setSorting(4, TABLE_SORTING_ASC) + + buyMyOfferTable:setSorting(3, TABLE_SORTING_DESC) + sellMyOfferTable:setSorting(3, TABLE_SORTING_DESC) +end + +function init() + g_ui.importStyle('market') + g_ui.importStyle('ui/general/markettabs') + g_ui.importStyle('ui/general/marketbuttons') + g_ui.importStyle('ui/general/marketcombobox') + g_ui.importStyle('ui/general/amountwindow') + + offerExhaust[MarketAction.Sell] = 10 + offerExhaust[MarketAction.Buy] = 20 + + registerMessageMode(MessageModes.Market, onMarketMessage) + + protocol.initProtocol() + connect(g_game, { onGameEnd = Market.reset }) + connect(g_game, { onGameEnd = Market.close }) + marketWindow = g_ui.createWidget('MarketWindow', rootWidget) + marketWindow:hide() + + initInterface() -- build interface +end + +function terminate() + Market.close() + + unregisterMessageMode(MessageModes.Market, onMarketMessage) + + protocol.terminateProtocol() + disconnect(g_game, { onGameEnd = Market.reset }) + disconnect(g_game, { onGameEnd = Market.close }) + + destroyAmountWindow() + marketWindow:destroy() + + Market = nil +end + +function Market.reset() + balanceLabel:setColor('#bbbbbb') + categoryList:setCurrentOption(getMarketCategoryName(MarketCategory.First)) + searchEdit:setText('') + clearFilters() + clearMyOffers() + if not table.empty(information) then + Market.updateCurrentItems() + end +end + +function Market.displayMessage(message) + if marketWindow:isHidden() then return end + + local infoBox = displayInfoBox(tr('Market Error'), message) + infoBox:lock() +end + +function Market.clearSelectedItem() + if Market.isItemSelected() then + Market.resetCreateOffer(true) + offerTypeList:clearOptions() + offerTypeList:setText('Please Select') + offerTypeList:setEnabled(false) + + clearOffers() + radioItemSet:selectWidget(nil) + nameLabel:setText('No item selected.') + selectedItem:setItem(nil) + selectedItem.item = nil + selectedItem.ref:setChecked(false) + selectedItem.ref = nil + + detailsTable:clearData() + buyStatsTable:clearData() + sellStatsTable:clearData() + + Market.enableCreateOffer(false) + end +end + +function Market.isItemSelected() + return selectedItem and selectedItem.item +end + +function Market.isOfferSelected(type) + return selectedOffer[type] and not selectedOffer[type]:isNull() +end + +function Market.getDepotCount(itemId) + return information.depotItems[itemId] or 0 +end + +function Market.enableCreateOffer(enable) + offerTypeList:setEnabled(enable) + totalPriceEdit:setEnabled(enable) + piecePriceEdit:setEnabled(enable) + amountEdit:setEnabled(enable) + anonymous:setEnabled(enable) + createOfferButton:setEnabled(enable) + + local prevAmountButton = marketOffersPanel:recursiveGetChildById('prevAmountButton') + local nextAmountButton = marketOffersPanel:recursiveGetChildById('nextAmountButton') + + prevAmountButton:setEnabled(enable) + nextAmountButton:setEnabled(enable) +end + +function Market.close(notify) + if notify == nil then notify = true end + if not marketWindow:isHidden() then + marketWindow:hide() + marketWindow:unlock() + modules.game_interface.getRootPanel():focus() + Market.clearSelectedItem() + Market.reset() + if notify then + MarketProtocol.sendMarketLeave() + end + end +end + +function Market.incrementAmount() + amountEdit:setValue(amountEdit:getValue() + 1) +end + +function Market.decrementAmount() + amountEdit:setValue(amountEdit:getValue() - 1) +end + +function Market.updateCurrentItems() + local id = getMarketCategoryId(categoryList:getCurrentOption().text) + if id == MarketCategory.MetaWeapons then + id = getMarketCategoryId(subCategoryList:getCurrentOption().text) + end + Market.loadMarketItems(id) +end + +function Market.resetCreateOffer(resetFee) + piecePriceEdit:setValue(1) + totalPriceEdit:setValue(1) + amountEdit:setValue(1) + refreshTypeList() + + if resetFee then + clearFee() + else + updateFee(0, 0) + end +end + +function Market.refreshItemsWidget(selectItem) + local selectItem = selectItem or 0 + itemsPanel = browsePanel:recursiveGetChildById('itemsPanel') + + local layout = itemsPanel:getLayout() + layout:disableUpdates() + + Market.clearSelectedItem() + itemsPanel:destroyChildren() + + if radioItemSet then + radioItemSet:destroy() + end + radioItemSet = UIRadioGroup.create() + + local select = nil + for i = 1, #currentItems do + local item = currentItems[i] + local itemBox = g_ui.createWidget('MarketItemBox', itemsPanel) + itemBox.onCheckChange = Market.onItemBoxChecked + itemBox.item = item + + if selectItem > 0 and item.marketData.tradeAs == selectItem then + select = itemBox + selectItem = 0 + end + + local itemWidget = itemBox:getChildById('item') + itemWidget:setItem(item.displayItem) + + local amount = Market.getDepotCount(item.marketData.tradeAs) + if amount > 0 then + itemWidget:setText(amount) + itemBox:setTooltip('You have '.. amount ..' in your depot.') + end + + radioItemSet:addWidget(itemBox) + end + + if select then + radioItemSet:selectWidget(select, false) + end + + layout:enableUpdates() + layout:update() +end + +function Market.refreshOffers() + if Market.isItemSelected() then + Market.onItemBoxChecked(selectedItem.ref) + else + Market.refreshMyOffers() + end +end + +function Market.refreshMyOffers() + clearMyOffers() + MarketProtocol.sendMarketBrowseMyOffers() +end + + +function Market.loadMarketItems(category) + clearItems() + + -- check search filter + local searchFilter = searchEdit:getText() + if searchFilter and searchFilter:len() > 2 then + if filterButtons[MarketFilters.SearchAll]:isChecked() then + category = MarketCategory.All + end + end + + if category == MarketCategory.All then + -- loop all categories + for category = MarketCategory.First, MarketCategory.Last do + for i = 1, #marketItems[category] do + local item = marketItems[category][i] + if isItemValid(item, category, searchFilter) then + + table.insert(currentItems, item) + end + end + end + else + -- loop specific category + for i = 1, #marketItems[category] do + local item = marketItems[category][i] + if isItemValid(item, category, searchFilter) then + table.insert(currentItems, item) + end + end + end + + Market.refreshItemsWidget() +end + +function Market.createNewOffer() + local type = offerTypeList:getCurrentOption().text + if type == 'Sell' then + type = MarketAction.Sell + else + type = MarketAction.Buy + end + + if not Market.isItemSelected() then + return + end + + local spriteId = selectedItem.item.marketData.tradeAs + + local piecePrice = piecePriceEdit:getValue() + local amount = amountEdit:getValue() + local anonymous = anonymous:isChecked() and 1 or 0 + + -- error checking + local errorMsg = '' + if type == MarketAction.Buy then + if information.balance < ((piecePrice * amount) + fee) then + errorMsg = errorMsg..'Not enough balance to create this offer.\n' + end + elseif type == MarketAction.Sell then + if information.balance < fee then + errorMsg = errorMsg..'Not enough balance to create this offer.\n' + end + if Market.getDepotCount(spriteId) < amount then + errorMsg = errorMsg..'Not enough items in your depot to create this offer.\n' + end + end + + if piecePrice > piecePriceEdit.maximum then + errorMsg = errorMsg..'Price is too high.\n' + elseif piecePrice < piecePriceEdit.minimum then + errorMsg = errorMsg..'Price is too low.\n' + end + + if amount > amountEdit.maximum then + errorMsg = errorMsg..'Amount is too high.\n' + elseif amount < amountEdit.minimum then + errorMsg = errorMsg..'Amount is too low.\n' + end + + if amount * piecePrice > MarketMaxPrice then + errorMsg = errorMsg..'Total price is too high.\n' + end + + if information.totalOffers >= MarketMaxOffers then + errorMsg = errorMsg..'You cannot create more offers.\n' + end + + local timeCheck = os.time() - lastCreatedOffer + if timeCheck < offerExhaust[type] then + local waitTime = math.ceil(offerExhaust[type] - timeCheck) + errorMsg = errorMsg..'You must wait '.. waitTime ..' seconds before creating a new offer.\n' + end + + if errorMsg ~= '' then + Market.displayMessage(errorMsg) + return + end + + MarketProtocol.sendMarketCreateOffer(type, spriteId, amount, piecePrice, anonymous) + lastCreatedOffer = os.time() + Market.resetCreateOffer() +end + +function Market.acceptMarketOffer(amount, timestamp, counter) + if timestamp > 0 and amount > 0 then + MarketProtocol.sendMarketAcceptOffer(timestamp, counter, amount) + Market.refreshOffers() + end +end + +function Market.onItemBoxChecked(widget) + if widget:isChecked() then + updateSelectedItem(widget) + end +end + +-- protocol callback functions + +function Market.onMarketEnter(depotItems, offers, balance, vocation) + if not loaded then + initMarketItems() + loaded = true + end + + updateBalance(balance) + averagePrice = 0 + + information.totalOffers = offers + local player = g_game.getLocalPlayer() + if player then + information.player = player + end + if vocation == -1 then + if player then + information.vocation = player:getVocation() + end + else + -- vocation must be compatible with < 950 + information.vocation = vocation + end + + -- set list of depot items + information.depotItems = depotItems + + -- update the items widget to match depot items + if Market.isItemSelected() then + local spriteId = selectedItem.item.marketData.tradeAs + MarketProtocol.silent(true) -- disable protocol messages + Market.refreshItemsWidget(spriteId) + MarketProtocol.silent(false) -- enable protocol messages + else + Market.refreshItemsWidget() + end + + if table.empty(currentItems) then + Market.loadMarketItems(MarketCategory.First) + end + + if g_game.isOnline() then + marketWindow:lock() + marketWindow:show() + end +end + +function Market.onMarketLeave() + Market.close(false) +end + +function Market.onMarketDetail(itemId, descriptions, purchaseStats, saleStats) + updateDetails(itemId, descriptions, purchaseStats, saleStats) +end + +function Market.onMarketBrowse(offers) + updateOffers(offers) +end diff --git a/modules/game_market/market.otmod b/modules/game_market/market.otmod new file mode 100644 index 0000000..4d6248a --- /dev/null +++ b/modules/game_market/market.otmod @@ -0,0 +1,9 @@ +Module + name: game_market + description: Global item market system + author: BeniS + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ offerstatistic, marketoffer, marketprotocol, market ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_market/market.otui b/modules/game_market/market.otui new file mode 100644 index 0000000..ae45044 --- /dev/null +++ b/modules/game_market/market.otui @@ -0,0 +1,62 @@ +MarketWindow < MainWindow + id: marketWindow + !text: tr('Market') + size: 700 530 + + @onEscape: Market.close() + + // Main Panel Window + + MarketTabBar + id: mainTabBar + width: 164 + height: 25 + anchors.top: parent.top + anchors.left: parent.left + + Panel + id: mainTabContent + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + padding: 3 + border-width: 1 + border-color: #000000 + margin-bottom: 20 + + Label + id: balanceLabel + !text: tr('Balance') .. ':' + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: parent.top + anchors.right: parent.right + + Button + id: closeButton + !text: tr('Close') + anchors.top: mainTabContent.bottom + anchors.horizontalCenter: mainTabContent.horizontalCenter + margin-top: 5 + width: 110 + @onClick: Market.close() + + Button + id: refreshOffersButton + !text: tr('Refresh Offers') + anchors.top: mainTabContent.bottom + anchors.right: mainTabContent.right + margin-top: 5 + width: 110 + @onClick: Market.refreshOffers() + + Button + id: resetButton + !text: tr('Reset Market') + !tooltip: tr('Reset selection, filters & search') + anchors.top: mainTabContent.bottom + anchors.left: mainTabContent.left + margin-top: 5 + width: 110 + @onClick: Market.reset() \ No newline at end of file diff --git a/modules/game_market/marketoffer.lua b/modules/game_market/marketoffer.lua new file mode 100644 index 0000000..b2dcbfa --- /dev/null +++ b/modules/game_market/marketoffer.lua @@ -0,0 +1,158 @@ +MarketOffer = {} +MarketOffer.__index = MarketOffer + +local OFFER_TIMESTAMP = 1 +local OFFER_COUNTER = 2 + +MarketOffer.new = function(offerId, t, item, amount, price, playerName, state, var) + local offer = { + id = {}, + type = nil, + item = 0, + amount = 0, + price = 0, + player = '', + state = 0, + var = nil + } + + if not offerId or type(offerId) ~= 'table' then + g_logger.error('MarketOffer.new - invalid offer id provided.') + end + offer.id = offerId + + t = tonumber(t) + if t ~= MarketAction.Buy and t ~= MarketAction.Sell then + g_logger.error('MarketOffer.new - invalid type provided.') + end + offer.type = t + + if not item then + g_logger.error('MarketOffer.new - invalid item provided.') + end + offer.item = item + + offer.amount = amount + offer.price = price + offer.player = playerName + + state = tonumber(state) + if state ~= MarketOfferState.Active and state ~= MarketOfferState.Cancelled + and state ~= MarketOfferState.Expired and state ~= MarketOfferState.Accepted then + g_logger.error('MarketOffer.new - invalid state provided.') + end + offer.state = state + offer.var = var + + setmetatable(offer, MarketOffer) + return offer +end + +function MarketOffer:isEqual(id) + return self.id[OFFER_TIMESTAMP] == id[OFFER_TIMESTAMP] and self.id[OFFER_COUNTER] == id[OFFER_COUNTER] +end + +function MarketOffer:isLessThan(id) + return self.id[OFFER_TIMESTAMP] <= id[OFFER_TIMESTAMP] and self.id[OFFER_COUNTER] < id[OFFER_COUNTER] +end + +function MarketOffer:isNull() + return table.empty(self.id) +end + +-- Sets/Gets + +function MarketOffer:setId(id) + if not id or type(id) ~= 'table' then + g_logger.error('MarketOffer.setId - invalid id provided.') + end + self.id = id +end + +function MarketOffer:getId() + return self.id +end + +function MarketOffer:setType(t) + if not t or type(t) ~= 'number' then + g_logger.error('MarketOffer.setItem - invalid type provided.') + end + self.type = type +end + +function MarketOffer:getType() + return self.type +end + +function MarketOffer:setItem(item) + if not item or type(item) ~= 'userdata' then + g_logger.error('MarketOffer.setItem - invalid item id provided.') + end + self.item = item +end + +function MarketOffer:getItem() + return self.item +end + +function MarketOffer:setAmount(amount) + if not amount or type(amount) ~= 'number' then + g_logger.error('MarketOffer.setAmount - invalid amount provided.') + end + self.amount = amount +end + +function MarketOffer:getAmount() + return self.amount +end + +function MarketOffer:setPrice(price) + if not price or type(price) ~= 'number' then + g_logger.error('MarketOffer.setPrice - invalid price provided.') + end + self.price = price +end + +function MarketOffer:getPrice() + return self.price +end + +function MarketOffer:getTotalPrice() + return self.price * self.amount +end + +function MarketOffer:setPlayer(player) + if not player or type(player) ~= 'number' then + g_logger.error('MarketOffer.setPlayer - invalid player provided.') + end + self.player = player +end + +function MarketOffer:getPlayer() + return self.player +end + +function MarketOffer:setState(state) + if not state or type(state) ~= 'number' then + g_logger.error('MarketOffer.setState - invalid state provided.') + end + self.state = state +end + +function MarketOffer:getState() + return self.state +end + +function MarketOffer:getTimeStamp() + if table.empty(self.id) or #self.id < OFFER_TIMESTAMP then + return + end + return self.id[OFFER_TIMESTAMP] +end + +function MarketOffer:getCounter() + if table.empty(self.id) or #self.id < OFFER_COUNTER then + return + end + return self.id[OFFER_COUNTER] +end diff --git a/modules/game_market/marketprotocol.lua b/modules/game_market/marketprotocol.lua new file mode 100644 index 0000000..d9ca252 --- /dev/null +++ b/modules/game_market/marketprotocol.lua @@ -0,0 +1,247 @@ +MarketProtocol = {} + +-- private functions + +local silent +local protocol +local statistics = runinsandbox('offerstatistic') + +local function send(msg) + if protocol and not silent then + protocol:send(msg) + end +end + +local function readMarketOffer(msg, action, var) + local timestamp = msg:getU32() + local counter = msg:getU16() + + local itemId = 0 + if var == MarketRequest.MyOffers or var == MarketRequest.MyHistory then + itemId = msg:getU16() + else + itemId = var + end + + local amount = msg:getU16() + local price = msg:getU32() + local playerName + local state = MarketOfferState.Active + if var == MarketRequest.MyHistory then + state = msg:getU8() + elseif var == MarketRequest.MyOffers then + else + playerName = msg:getString() + end + + return MarketOffer.new({timestamp, counter}, action, Item.create(itemId), amount, price, playerName, state, var) +end + +-- parsing protocols +local function parseMarketEnter(protocol, msg) + local balance + if g_game.getClientVersion() >= 981 then + balance = msg:getU64() + else + balance = msg:getU32() + end + + local vocation = -1 + if g_game.getClientVersion() < 950 then + vocation = msg:getU8() -- get vocation id + end + local offers = msg:getU8() + + local depotItems = {} + local depotCount = msg:getU16() + for i = 1, depotCount do + local itemId = msg:getU16() -- item id + local itemCount = msg:getU16() -- item count + + depotItems[itemId] = itemCount + end + + signalcall(Market.onMarketEnter, depotItems, offers, balance, vocation) + return true +end + +local function parseMarketLeave(protocol, msg) + Market.onMarketLeave() + return true +end + +local function parseMarketDetail(protocol, msg) + local itemId = msg:getU16() + + local descriptions = {} + for i = MarketItemDescription.First, MarketItemDescription.Last do + if msg:peekU16() ~= 0x00 then + table.insert(descriptions, {i, msg:getString()}) -- item descriptions + else + msg:getU16() + end + end + local time = (os.time() / 1000) * statistics.SECONDS_PER_DAY; + + local purchaseStats = {} + local count = msg:getU8() + for i=1, count do + local transactions = msg:getU32() -- transaction count + local totalPrice = msg:getU32() -- total price + local highestPrice = msg:getU32() -- highest price + local lowestPrice = msg:getU32() -- lowest price + + local tmp = time - statistics.SECONDS_PER_DAY + table.insert(purchaseStats, OfferStatistic.new(tmp, MarketAction.Buy, transactions, totalPrice, highestPrice, lowestPrice)) + end + + local saleStats = {} + count = msg:getU8() + for i=1, count do + local transactions = msg:getU32() -- transaction count + local totalPrice = msg:getU32() -- total price + local highestPrice = msg:getU32() -- highest price + local lowestPrice = msg:getU32() -- lowest price + + local tmp = time - statistics.SECONDS_PER_DAY + table.insert(saleStats, OfferStatistic.new(tmp, MarketAction.Sell, transactions, totalPrice, highestPrice, lowestPrice)) + end + + signalcall(Market.onMarketDetail, itemId, descriptions, purchaseStats, saleStats) + return true +end + +local function parseMarketBrowse(protocol, msg) + local var = msg:getU16() + local offers = {} + + local buyOfferCount = msg:getU32() + for i = 1, buyOfferCount do + table.insert(offers, readMarketOffer(msg, MarketAction.Buy, var)) + end + + local sellOfferCount = msg:getU32() + for i = 1, sellOfferCount do + table.insert(offers, readMarketOffer(msg, MarketAction.Sell, var)) + end + + signalcall(Market.onMarketBrowse, offers) + return true +end + +-- public functions +function initProtocol() + connect(g_game, { onGameStart = MarketProtocol.registerProtocol, + onGameEnd = MarketProtocol.unregisterProtocol }) + + -- reloading module + if g_game.isOnline() then + MarketProtocol.registerProtocol() + end + + MarketProtocol.silent(false) +end + +function terminateProtocol() + disconnect(g_game, { onGameStart = MarketProtocol.registerProtocol, + onGameEnd = MarketProtocol.unregisterProtocol }) + + -- reloading module + MarketProtocol.unregisterProtocol() + MarketProtocol = nil +end + +function MarketProtocol.updateProtocol(_protocol) + protocol = _protocol +end + +function MarketProtocol.registerProtocol() + if g_game.getFeature(GamePlayerMarket) then + ProtocolGame.registerOpcode(GameServerOpcodes.GameServerMarketEnter, parseMarketEnter) + ProtocolGame.registerOpcode(GameServerOpcodes.GameServerMarketLeave, parseMarketLeave) + ProtocolGame.registerOpcode(GameServerOpcodes.GameServerMarketDetail, parseMarketDetail) + ProtocolGame.registerOpcode(GameServerOpcodes.GameServerMarketBrowse, parseMarketBrowse) + end + MarketProtocol.updateProtocol(g_game.getProtocolGame()) +end + +function MarketProtocol.unregisterProtocol() + if g_game.getFeature(GamePlayerMarket) then + ProtocolGame.unregisterOpcode(GameServerOpcodes.GameServerMarketEnter, parseMarketEnter) + ProtocolGame.unregisterOpcode(GameServerOpcodes.GameServerMarketLeave, parseMarketLeave) + ProtocolGame.unregisterOpcode(GameServerOpcodes.GameServerMarketDetail, parseMarketDetail) + ProtocolGame.unregisterOpcode(GameServerOpcodes.GameServerMarketBrowse, parseMarketBrowse) + end + MarketProtocol.updateProtocol(nil) +end + +function MarketProtocol.silent(mode) + silent = mode +end + +-- sending protocols + +function MarketProtocol.sendMarketLeave() + if g_game.getFeature(GamePlayerMarket) then + local msg = OutputMessage.create() + msg:addU8(ClientOpcodes.ClientMarketLeave) + send(msg) + else + g_logger.error('MarketProtocol.sendMarketLeave does not support the current protocol.') + end +end + +function MarketProtocol.sendMarketBrowse(browseId) + if g_game.getFeature(GamePlayerMarket) then + local msg = OutputMessage.create() + msg:addU8(ClientOpcodes.ClientMarketBrowse) + msg:addU16(browseId) + send(msg) + else + g_logger.error('MarketProtocol.sendMarketBrowse does not support the current protocol.') + end +end + +function MarketProtocol.sendMarketBrowseMyOffers() + MarketProtocol.sendMarketBrowse(MarketRequest.MyOffers) +end + +function MarketProtocol.sendMarketCreateOffer(type, spriteId, amount, price, anonymous) + if g_game.getFeature(GamePlayerMarket) then + local msg = OutputMessage.create() + msg:addU8(ClientOpcodes.ClientMarketCreate) + msg:addU8(type) + msg:addU16(spriteId) + msg:addU16(amount) + msg:addU32(price) + msg:addU8(anonymous) + send(msg) + else + g_logger.error('MarketProtocol.sendMarketCreateOffer does not support the current protocol.') + end +end + +function MarketProtocol.sendMarketCancelOffer(timestamp, counter) + if g_game.getFeature(GamePlayerMarket) then + local msg = OutputMessage.create() + msg:addU8(ClientOpcodes.ClientMarketCancel) + msg:addU32(timestamp) + msg:addU16(counter) + send(msg) + else + g_logger.error('MarketProtocol.sendMarketCancelOffer does not support the current protocol.') + end +end + +function MarketProtocol.sendMarketAcceptOffer(timestamp, counter, amount) + if g_game.getFeature(GamePlayerMarket) then + local msg = OutputMessage.create() + msg:addU8(ClientOpcodes.ClientMarketAccept) + msg:addU32(timestamp) + msg:addU16(counter) + msg:addU16(amount) + send(msg) + else + g_logger.error('MarketProtocol.sendMarketAcceptOffer does not support the current protocol.') + end +end diff --git a/modules/game_market/offerstatistic.lua b/modules/game_market/offerstatistic.lua new file mode 100644 index 0000000..859679b --- /dev/null +++ b/modules/game_market/offerstatistic.lua @@ -0,0 +1,101 @@ +OfferStatistic = {} +OfferStatistic.__index = OfferStatistic + +SECONDS_PER_DAY = 86400 + +OfferStatistic.new = function(timestamp, t, transactions, totalPrice, highestPrice, lowestPrice) + local stat = { + time = 0, + type = nil, + transactions = 0, + totalPrice = 0, + highestPrice = 0, + lowestPrice = 0 + } + stat.time = math.floor(timestamp / SECONDS_PER_DAY) * SECONDS_PER_DAY + + if t ~= MarketAction.Buy and t ~= MarketAction.Sell then + g_logger.error('OfferStatistic.new - invalid type provided.') + end + stat.type = t + + stat.transactions = transactions + stat.totalPrice = totalPrice + stat.highestPrice = highestPrice + stat.lowestPrice = lowestPrice + + setmetatable(stat, OfferStatistic) + return stat +end + +function OfferStatistic:isNull() + return self.time == 0 or not self.type +end + +-- Sets/Gets + +function OfferStatistic:setTime(time) + if not time or type(time) ~= 'number' then + g_logger.error('OfferStatistic.setTime - invalid time provided.') + end + self.time = time +end + +function OfferStatistic:getTime() + return self.time +end + +function OfferStatistic:setType(t) + if not t or type(t) ~= 'number' then + g_logger.error('OfferStatistic.setType - invalid type provided.') + end + self.type = t +end + +function OfferStatistic:getType() + return self.type +end + +function OfferStatistic:setTransactions(transactions) + if not transactions or type(transactions) ~= 'number' then + g_logger.error('OfferStatistic.setTransactions - invalid transactions provided.') + end + self.transactions = transactions +end + +function OfferStatistic:getTransactions() + return self.transactions +end + +function OfferStatistic:setTotalPrice(amount) + if not totalPrice or type(totalPrice) ~= 'number' then + g_logger.error('OfferStatistic.setTotalPrice - invalid total price provided.') + end + self.totalPrice = totalPrice +end + +function OfferStatistic:getTotalPrice() + return self.totalPrice +end + +function OfferStatistic:setHighestPrice(highestPrice) + if not highestPrice or type(highestPrice) ~= 'number' then + g_logger.error('OfferStatistic.setHighestPrice - invalid highestPrice provided.') + end + self.highestPrice = highestPrice +end + +function OfferStatistic:getHighestPrice() + return self.highestPrice +end + +function OfferStatistic:setLowestPrice(lowestPrice) + if not lowestPrice or type(lowestPrice) ~= 'number' then + g_logger.error('OfferStatistic.setLowestPrice - invalid lowestPrice provided.') + end + self.lowestPrice = lowestPrice +end + +function OfferStatistic:getLowestPrice() + return self.lowestPrice +end diff --git a/modules/game_market/ui/general/amountwindow.otui b/modules/game_market/ui/general/amountwindow.otui new file mode 100644 index 0000000..c4a2d25 --- /dev/null +++ b/modules/game_market/ui/general/amountwindow.otui @@ -0,0 +1,44 @@ +AmountWindow < MainWindow + id: amountWindow + !text: tr('Amount') + size: 270 90 + + Item + id: item + text-offset: 0 22 + text-align: right + anchors.left: parent.left + anchors.top: parent.top + margin-top: 2 + margin-left: -4 + focusable: false + virtual: true + + HorizontalScrollBar + id: amountScrollBar + anchors.left: prev.right + anchors.right: parent.right + anchors.top: prev.top + margin-left: 10 + margin-top: -2 + + Button + id: buttonCancel + !text: tr('Cancel') + height: 20 + anchors.left: amountScrollBar.horizontalCenter + anchors.right: amountScrollBar.right + anchors.top: amountScrollBar.bottom + margin-top: 7 + focusable: false + + Button + id: buttonOk + !text: tr('Ok') + height: 20 + anchors.right: amountScrollBar.horizontalCenter + anchors.left: amountScrollBar.left + anchors.top: amountScrollBar.bottom + margin-top: 7 + margin-right: 6 + focusable: false diff --git a/modules/game_market/ui/general/marketbuttons.otui b/modules/game_market/ui/general/marketbuttons.otui new file mode 100644 index 0000000..4533070 --- /dev/null +++ b/modules/game_market/ui/general/marketbuttons.otui @@ -0,0 +1,13 @@ +MarketButtonBox < ButtonBoxRounded + font: verdana-11px-rounded + color: #f55e5ebb + size: 106 22 + text-offset: 0 2 + text-align: center + + $checked: + color: white + + $disabled: + color: #666666ff + image-color: #ffffff88 diff --git a/modules/game_market/ui/general/marketcombobox.otui b/modules/game_market/ui/general/marketcombobox.otui new file mode 100644 index 0000000..5177073 --- /dev/null +++ b/modules/game_market/ui/general/marketcombobox.otui @@ -0,0 +1,18 @@ +MarketComboBoxPopupMenuButton < ComboBoxRoundedPopupMenuButton + height: 18 + font: verdana-11px-rounded + text-offset: 2 2 + +MarketComboBoxPopupMenuSeparator < UIWidget + image-source: /images/combobox_rounded + image-repeated: true + image-clip: 1 59 89 1 + height: 1 + phantom: true + +MarketComboBoxPopupMenu < ComboBoxRoundedPopupMenu + +MarketComboBox < ComboBoxRounded + font: verdana-11px-rounded + size: 86 20 + text-offset: 3 2 diff --git a/modules/game_market/ui/general/markettabs.otui b/modules/game_market/ui/general/markettabs.otui new file mode 100644 index 0000000..dbc6ca1 --- /dev/null +++ b/modules/game_market/ui/general/markettabs.otui @@ -0,0 +1,44 @@ +MarketTabBar < TabBar +MarketTabBarPanel < TabBarPanel +MarketTabBarButton < TabBarButton + size: 20 25 + font: verdana-11px-rounded + text-offset: 0 2 + + $!first: + anchors.left: prev.right + margin-left: 0 + + $hover !checked: + color: #ffffff + + $checked: + color: #ffffff + + $on !checked: + color: #f55e5e + +MarketRightTabBar < TabBar +MarketRightTabBarPanel < TabBarPanel +MarketRightTabBarButton < TabBarButton + size: 20 25 + font: verdana-11px-rounded + text-offset: 0 2 + color: #929292 + + $first: + anchors.right: parent.right + anchors.left: none + + $!first: + anchors.right: prev.left + anchors.left: none + + $hover !checked: + color: #ffffff + + $checked: + color: #ffffff + + $on !checked: + color: #f55e5e diff --git a/modules/game_market/ui/marketoffers.otui b/modules/game_market/ui/marketoffers.otui new file mode 100644 index 0000000..f8e04f8 --- /dev/null +++ b/modules/game_market/ui/marketoffers.otui @@ -0,0 +1,188 @@ +Panel + + MarketTabBar + id: leftTabBar + width: 107 + height:25 + anchors.top: parent.top + anchors.left: parent.left + + Panel + id: leftTabContent + width: 180 + anchors.top: prev.bottom + anchors.left: prev.left + anchors.bottom: parent.bottom + border-width: 1 + border-color: #000000 + + MarketRightTabBar + id: rightTabBar + width: 166 + height:25 + anchors.top: parent.top + anchors.right: parent.right + + Panel + id: rightTabContent + anchors.top: prev.bottom + anchors.left: leftTabContent.right + anchors.right: prev.right + anchors.bottom: parent.bottom + margin-left:3 + border-width: 1 + border-color: #000000 + + UIItem + id: selectedItem + phantom: true + size: 34 34 + padding: 1 + font: verdana-11px-rounded + border-color: white + anchors.top: rightTabBar.bottom + anchors.left: rightTabContent.left + margin-top: 6 + margin-left: 6 + + Label + id: nameLabel + !text: tr('No item selected.') + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 5 + + Label + id: createLabel + !text: tr('Create New Offer') + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: rightTabBar.top + anchors.left: rightTabContent.left + margin-top: 355 + margin-left: 6 + + Label + id: offerTypeLabel + !text: tr('Offer Type') .. ':' + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 7 + + MarketComboBox + id: offerTypeComboBox + !text: tr('Please Select') + anchors.top: prev.bottom + anchors.left: createLabel.left + margin-top: 3 + width: 105 + + $disabled: + color: #aaaaaa44 + + Label + id: totalPriceLabel + !text: tr('Total Price') .. ':' + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: offerTypeLabel.top + anchors.left: prev.right + margin-left: 7 + + SpinBox + id: totalPriceEdit + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 3 + width: 75 + minimum: 1 + maximum: 999999999 + focusable: true + + $disabled: + color: #aaaaaa44 + + Label + id: piecePriceLabel + !text: tr('Piece Price') .. ':' + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: offerTypeLabel.top + anchors.left: prev.right + margin-left: 7 + + SpinBox + id: piecePriceEdit + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 3 + width: 75 + minimum: 1 + maximum: 999999999 + focusable: true + + $disabled: + color: #aaaaaa44 + + Label + id: amountLabel + !text: tr('Amount') .. ':' + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: offerTypeLabel.top + anchors.left: amountEdit.left + + PreviousButton + id: prevAmountButton + anchors.verticalCenter: piecePriceEdit.verticalCenter + anchors.left: piecePriceEdit.right + margin-left: 7 + @onClick: Market.decrementAmount() + + SpinBox + id: amountEdit + anchors.top: prev.top + anchors.left: prev.right + margin-left: 3 + width: 55 + buttons: false + minimum: 1 + maximum: 64000 + focusable: true + + NextButton + id: nextAmountButton + anchors.verticalCenter: piecePriceEdit.verticalCenter + anchors.left: prev.right + margin-left: 3 + @onClick: Market.incrementAmount() + + Button + id: createOfferButton + !text: tr('Create Offer') + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 7 + width: 90 + + CheckBox + id: anonymousCheckBox + !text: tr('Anonymous') + anchors.left: prev.left + anchors.bottom: prev.top + margin-bottom: 6 + @onSetup: self:setChecked(false) + height: 16 + width: 90 + + Label + id: feeLabel + font: verdana-11px-rounded + anchors.top: createOfferButton.bottom + anchors.left: createOfferButton.left + margin: 2 \ No newline at end of file diff --git a/modules/game_market/ui/marketoffers/browse.otui b/modules/game_market/ui/marketoffers/browse.otui new file mode 100644 index 0000000..9071697 --- /dev/null +++ b/modules/game_market/ui/marketoffers/browse.otui @@ -0,0 +1,158 @@ +MarketItemBox < UICheckBox + id: itemBox + border-width: 1 + border-color: #000000 + color: #aaaaaa + text-align: center + + Item + id: item + phantom: true + virtual: true + text-offset: 0 22 + text-align: right + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + margin: 1 + + $checked: + border-color: #ffffff + + $hover !checked: + border-color: #aaaaaa + + $disabled: + image-color: #ffffff88 + color: #aaaaaa88 + +Panel + background-color: #22283399 + margin: 1 + + MarketComboBox + id: categoryComboBox + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + margin-top: 3 + margin-right: 3 + margin-left: 3 + + MarketComboBox + id: subCategoryComboBox + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 3 + margin-right: 3 + margin-left: 3 + + $disabled: + color: #aaaaaa44 + + MarketButtonBox + id: filterLevel + &default: false + !text: tr('Level') + !tooltip: tr('Filter list to match your level') + anchors.top: prev.bottom + anchors.left: parent.left + margin-top: 3 + margin-right: 3 + margin-left: 3 + width: 40 + height: 20 + + MarketButtonBox + id: filterVocation + &default: false + !text: tr('Voc.') + !tooltip: tr('Filter list to match your vocation') + anchors.top: prev.top + anchors.left: prev.right + margin-right: 3 + margin-left: 3 + width: 34 + height: 20 + + MarketComboBox + id: slotComboBox + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-right: 3 + margin-left: 3 + + $disabled: + color: #aaaaaa44 + + MarketButtonBox + id: filterDepot + &default: false + !text: tr('Show Depot Only') + !tooltip: tr('Show your depot items only') + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 6 + margin-right: 3 + margin-left: 3 + + Panel + id: itemsContainer + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + margin-top: 10 + margin-left: 3 + margin-bottom: 30 + margin-right: 3 + + VerticalScrollBar + id: itemsPanelListScrollBar + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + step: 28 + pixels-scroll: true + + ScrollablePanel + id: itemsPanel + anchors.left: parent.left + anchors.right: prev.left + anchors.top: parent.top + anchors.bottom: parent.bottom + vertical-scrollbar: itemsPanelListScrollBar + layout: + type: grid + cell-size: 36 36 + flow: true + auto-spacing: true + + Label + !text: tr('Find') .. ':' + anchors.top: prev.bottom + anchors.left: prev.left + margin-top: 9 + width: 30 + font: verdana-11px-rounded + text-offset: 0 2 + + TextEdit + id: searchEdit + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + margin-left: 3 + width: 113 + @onTextChange: Market.updateCurrentItems() + + MarketButtonBox + id: filterSearchAll + &default: true + !text: tr('All') + !tooltip: tr('Search all items') + anchors.verticalCenter: prev.verticalCenter + anchors.left: prev.right + anchors.right: itemsContainer.right + margin-left: 3 diff --git a/modules/game_market/ui/marketoffers/itemdetails.otui b/modules/game_market/ui/marketoffers/itemdetails.otui new file mode 100644 index 0000000..310f858 --- /dev/null +++ b/modules/game_market/ui/marketoffers/itemdetails.otui @@ -0,0 +1,56 @@ +DetailsTableRow < TableRow + font: verdana-11px-monochrome + focusable: true + color: #cccccc + height: 45 + focusable: false + padding: 2 + even-background-color: alpha + odd-background-color: alpha + +DetailsTableColumn < TableColumn + font: verdana-11px-monochrome + background-color: alpha + text-offset: 5 2 + color: #cccccc + width: 100 + focusable: false + +Panel + background-color: #22283399 + margin: 1 + + Table + id: detailsTable + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 63 + margin-left: 6 + margin-bottom: 85 + margin-right: 6 + padding: 1 + focusable: false + background-color: #222833 + border-width: 1 + border-color: #191f27 + table-data: detailsTableData + row-style: DetailsTableRow + column-style: DetailsTableColumn + + TableData + id: detailsTableData + anchors.top: detailsTable.top + anchors.bottom: detailsTable.bottom + anchors.left: detailsTable.left + anchors.right: detailsTable.right + vertical-scrollbar: detailsTableScrollBar + + VerticalScrollBar + id: detailsTableScrollBar + anchors.top: detailsTable.top + anchors.bottom: detailsTable.bottom + anchors.right: detailsTable.right + step: 28 + pixels-scroll: true \ No newline at end of file diff --git a/modules/game_market/ui/marketoffers/itemoffers.otui b/modules/game_market/ui/marketoffers/itemoffers.otui new file mode 100644 index 0000000..8f39260 --- /dev/null +++ b/modules/game_market/ui/marketoffers/itemoffers.otui @@ -0,0 +1,176 @@ +OfferTableRow < TableRow + font: verdana-11px-monochrome + color: #cccccc + height: 15 + +OfferTableColumn < TableColumn + font: verdana-11px-monochrome + background-color: alpha + text-offset: 5 0 + color: #cccccc + width: 80 + +OfferTableWarningColumn < OfferTableColumn + color: #e03d3d + +OfferTableHeaderRow < TableHeaderRow + font: verdana-11px-monochrome + color: #cccccc + height: 20 + +OfferTableHeaderColumn < SortableTableHeaderColumn + font: verdana-11px-monochrome + text-offset: 2 0 + color: #cccccc + + $focus: + background-color: #294f6d + color: #ffffff + +Panel + background-color: #22283399 + margin: 1 + + Button + id: buyButton + !text: tr('Buy Now') + anchors.right: parent.right + anchors.bottom: next.bottom + margin-right: 6 + width: 80 + enabled: false + + Label + !text: tr('Sell Offers') + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: parent.top + anchors.left: parent.left + margin-top: 44 + margin-left: 6 + + Table + id: sellingTable + anchors.top: prev.bottom + anchors.left: prev.left + anchors.right: parent.right + height: 115 + margin-top: 5 + margin-bottom: 5 + margin-right: 6 + padding: 1 + focusable: false + background-color: #222833 + border-width: 1 + border-color: #191f27 + table-data: sellingTableData + row-style: OfferTableRow + column-style: OfferTableColumn + header-column-style: false + header-row-style: false + + OfferTableHeaderRow + id: header + OfferTableHeaderColumn + !text: tr('Buyer Name') + width: 100 + OfferTableHeaderColumn + !text: tr('Amount') + width: 60 + OfferTableHeaderColumn + !text: tr('Total Price') + width: 90 + OfferTableHeaderColumn + !text: tr('Piece Price') + width: 80 + OfferTableHeaderColumn + !text: tr('Auction End') + width: 120 + + TableData + id: sellingTableData + anchors.bottom: sellingTable.bottom + anchors.left: sellingTable.left + anchors.right: sellingTable.right + margin-top: 2 + vertical-scrollbar: sellingTableScrollBar + + VerticalScrollBar + id: sellingTableScrollBar + anchors.top: sellingTable.top + anchors.bottom: sellingTable.bottom + anchors.right: sellingTable.right + step: 28 + pixels-scroll: true + + Button + id: sellButton + !text: tr('Sell Now') + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 5 + margin-right: 6 + width: 80 + enabled: false + + Label + !text: tr('Buy Offers') + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: prev.top + anchors.left: parent.left + margin-top: 9 + margin-left: 6 + + Table + id: buyingTable + anchors.top: prev.bottom + anchors.left: prev.left + anchors.right: parent.right + margin-top: 5 + margin-bottom: 5 + margin-right: 6 + height: 115 + padding: 1 + focusable: false + background-color: #222833 + border-width: 1 + border-color: #191f27 + table-data: buyingTableData + row-style: OfferTableRow + column-style: OfferTableColumn + header-column-style: false + header-row-style: false + + OfferTableHeaderRow + id: header + OfferTableHeaderColumn + !text: tr('Seller Name') + width: 100 + OfferTableHeaderColumn + !text: tr('Amount') + width: 60 + OfferTableHeaderColumn + !text: tr('Total Price') + width: 90 + OfferTableHeaderColumn + !text: tr('Piece Price') + width: 80 + OfferTableHeaderColumn + !text: tr('Auction End') + width: 120 + + TableData + id: buyingTableData + anchors.bottom: buyingTable.bottom + anchors.left: buyingTable.left + anchors.right: buyingTable.right + vertical-scrollbar: buyingTableScrollBar + + VerticalScrollBar + id: buyingTableScrollBar + anchors.top: buyingTable.top + anchors.bottom: buyingTable.bottom + anchors.right: buyingTable.right + step: 28 + pixels-scroll: true \ No newline at end of file diff --git a/modules/game_market/ui/marketoffers/itemstats.otui b/modules/game_market/ui/marketoffers/itemstats.otui new file mode 100644 index 0000000..61afa97 --- /dev/null +++ b/modules/game_market/ui/marketoffers/itemstats.otui @@ -0,0 +1,103 @@ +StatsTableRow < TableRow + font: verdana-11px-monochrome + focusable: true + color: #cccccc + height: 20 + focusable: false + +StatsTableColumn < TableColumn + font: verdana-11px-monochrome + background-color: alpha + text-offset: 5 3 + color: #cccccc + width: 110 + focusable: false + +Panel + background-color: #22283399 + margin: 1 + + Label + !text: tr('Buy Offers') + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + margin-top: 44 + margin-left: 6 + + Table + id: buyStatsTable + anchors.top: prev.bottom + anchors.left: prev.left + anchors.right: prev.right + margin-top: 6 + margin-bottom: 5 + margin-right: 6 + height: 121 + padding: 1 + focusable: false + background-color: #222833 + border-width: 1 + border-color: #191f27 + table-data: buyStatsTableData + row-style: StatsTableRow + column-style: StatsTableColumn + + TableData + id: buyStatsTableData + anchors.top: buyStatsTable.top + anchors.bottom: buyStatsTable.bottom + anchors.left: buyStatsTable.left + anchors.right: buyStatsTable.right + vertical-scrollbar: buyStatsTableScrollBar + + VerticalScrollBar + id: buyStatsTableScrollBar + anchors.top: buyStatsTable.top + anchors.bottom: buyStatsTable.bottom + anchors.right: buyStatsTable.right + step: 28 + pixels-scroll: true + + Label + !text: tr('Sell Offers') + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: buyStatsTable.bottom + anchors.left: parent.left + margin-top: 9 + margin-left: 6 + + Table + id: sellStatsTable + anchors.top: prev.bottom + anchors.left: buyStatsTable.left + anchors.right: buyStatsTable.right + margin-top: 6 + height: 112 + padding: 1 + focusable: false + background-color: #222833 + border-width: 1 + border-color: #191f27 + table-data: sellStatsTableData + row-style: StatsTableRow + column-style: StatsTableColumn + + TableData + id: sellStatsTableData + anchors.top: sellStatsTable.top + anchors.bottom: sellStatsTable.bottom + anchors.left: sellStatsTable.left + anchors.right: sellStatsTable.right + vertical-scrollbar: sellStatsTableScrollBar + + VerticalScrollBar + id: sellStatsTableScrollBar + anchors.top: sellStatsTable.top + anchors.bottom: sellStatsTable.bottom + anchors.right: sellStatsTable.right + step: 28 + pixels-scroll: true \ No newline at end of file diff --git a/modules/game_market/ui/marketoffers/overview.otui b/modules/game_market/ui/marketoffers/overview.otui new file mode 100644 index 0000000..7e9cccb --- /dev/null +++ b/modules/game_market/ui/marketoffers/overview.otui @@ -0,0 +1,16 @@ +Panel + background-color: #22283399 + margin: 1 + + Label + !text: tr('Reserved for more functionality later.') + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + margin-top: 6 + margin-left: 6 + margin-right: 6 + font: verdana-11px-rounded + text-offset: 0 2 + height: 50 + text-wrap: true \ No newline at end of file diff --git a/modules/game_market/ui/myoffers.otui b/modules/game_market/ui/myoffers.otui new file mode 100644 index 0000000..cfa39d0 --- /dev/null +++ b/modules/game_market/ui/myoffers.otui @@ -0,0 +1,16 @@ +Panel + + MarketTabBar + id: offersTabBar + width: 187 + height:25 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + Panel + id: offersTabContent + anchors.top: prev.bottom + anchors.left: prev.left + anchors.right: prev.right + anchors.bottom: parent.bottom diff --git a/modules/game_market/ui/myoffers/currentoffers.otui b/modules/game_market/ui/myoffers/currentoffers.otui new file mode 100644 index 0000000..420ec4c --- /dev/null +++ b/modules/game_market/ui/myoffers/currentoffers.otui @@ -0,0 +1,176 @@ +OfferTableRow < TableRow + font: verdana-11px-monochrome + color: #cccccc + height: 15 + +OfferTableColumn < TableColumn + font: verdana-11px-monochrome + background-color: alpha + text-offset: 5 0 + color: #cccccc + width: 80 + +OfferTableWarningColumn < OfferTableColumn + color: #e03d3d + +OfferTableHeaderRow < TableHeaderRow + font: verdana-11px-monochrome + color: #cccccc + height: 20 + +OfferTableHeaderColumn < SortableTableHeaderColumn + font: verdana-11px-monochrome + text-offset: 2 0 + color: #cccccc + + $focus: + background-color: #294f6d + color: #ffffff + +Panel + background-color: #22283399 + margin: 1 + + Button + id: sellCancelButton + !text: tr('Cancel') + anchors.right: parent.right + anchors.bottom: next.bottom + margin-right: 6 + width: 80 + enabled: false + + Label + !text: tr('Sell Offers') + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: parent.top + anchors.left: parent.left + margin-top: 44 + margin-left: 6 + + Table + id: mySellingTable + anchors.top: prev.bottom + anchors.left: prev.left + anchors.right: parent.right + height: 150 + margin-top: 5 + margin-bottom: 5 + margin-right: 6 + padding: 1 + focusable: false + background-color: #222833 + border-width: 1 + border-color: #191f27 + table-data: mySellingTableData + row-style: OfferTableRow + column-style: OfferTableColumn + header-column-style: false + header-row-style: false + + OfferTableHeaderRow + id: header + OfferTableHeaderColumn + !text: tr('Item Name') + width: 160 + OfferTableHeaderColumn + !text: tr('Total Price') + width: 125 + OfferTableHeaderColumn + !text: tr('Piece Price') + width: 125 + OfferTableHeaderColumn + !text: tr('Amount') + width: 110 + OfferTableHeaderColumn + !text: tr('Auction End') + width: 110 + + TableData + id: mySellingTableData + anchors.bottom: mySellingTable.bottom + anchors.left: mySellingTable.left + anchors.right: mySellingTable.right + margin-top: 2 + vertical-scrollbar: mySellingTableScrollBar + + VerticalScrollBar + id: mySellingTableScrollBar + anchors.top: mySellingTable.top + anchors.bottom: mySellingTable.bottom + anchors.right: mySellingTable.right + step: 28 + pixels-scroll: true + + Button + id: buyCancelButton + !text: tr('Cancel') + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 5 + margin-right: 6 + width: 80 + enabled: false + + Label + !text: tr('Buy Offers') + font: verdana-11px-rounded + text-offset: 0 2 + anchors.top: prev.top + anchors.left: parent.left + margin-top: 9 + margin-left: 6 + + Table + id: myBuyingTable + anchors.top: prev.bottom + anchors.left: prev.left + anchors.right: parent.right + margin-top: 5 + margin-bottom: 5 + margin-right: 6 + height: 150 + padding: 1 + focusable: false + background-color: #222833 + border-width: 1 + border-color: #191f27 + table-data: myBuyingTableData + row-style: OfferTableRow + column-style: OfferTableColumn + header-column-style: false + header-row-style: false + + OfferTableHeaderRow + id: header + OfferTableHeaderColumn + !text: tr('Item Name') + width: 160 + OfferTableHeaderColumn + !text: tr('Total Price') + width: 125 + OfferTableHeaderColumn + !text: tr('Piece Price') + width: 125 + OfferTableHeaderColumn + !text: tr('Amount') + width: 110 + OfferTableHeaderColumn + !text: tr('Auction End') + width: 110 + + TableData + id: myBuyingTableData + anchors.bottom: myBuyingTable.bottom + anchors.left: myBuyingTable.left + anchors.right: myBuyingTable.right + vertical-scrollbar: myBuyingTableScrollBar + + VerticalScrollBar + id: myBuyingTableScrollBar + anchors.top: myBuyingTable.top + anchors.bottom: myBuyingTable.bottom + anchors.right: myBuyingTable.right + step: 28 + pixels-scroll: true diff --git a/modules/game_market/ui/myoffers/itemoffers.otui b/modules/game_market/ui/myoffers/itemoffers.otui new file mode 100644 index 0000000..c649ce8 --- /dev/null +++ b/modules/game_market/ui/myoffers/itemoffers.otui @@ -0,0 +1,9 @@ +Panel + background-color: #22283399 + margin: 1 + + Label + !text: tr('Item Offers') + anchors.top: parent.top + anchors.left: parent.left + margin-left: 10 diff --git a/modules/game_market/ui/myoffers/offerhistory.otui b/modules/game_market/ui/myoffers/offerhistory.otui new file mode 100644 index 0000000..d9c9fa3 --- /dev/null +++ b/modules/game_market/ui/myoffers/offerhistory.otui @@ -0,0 +1,9 @@ +Panel + background-color: #22283399 + margin: 1 + + Label + !text: tr('Offer History') + anchors.top: parent.top + anchors.left: parent.left + margin-left: 10 diff --git a/modules/game_minimap/flagwindow.otui b/modules/game_minimap/flagwindow.otui new file mode 100644 index 0000000..c0fac4a --- /dev/null +++ b/modules/game_minimap/flagwindow.otui @@ -0,0 +1,188 @@ +FlagButton < CheckBox + size: 15 15 + margin-left: 2 + image-source: /images/game/minimap/flagcheckbox + image-size: 15 15 + image-border: 3 + icon-source: /images/game/minimap/mapflags + icon-size: 11 11 + icon-clip: 0 0 11 11 + icon-offset: 2 4 + text: + + $!checked: + image-clip: 26 0 26 26 + + $hover !checked: + image-clip: 78 0 26 26 + + $checked: + image-clip: 0 0 26 26 + + $hover checked: + image-clip: 52 0 26 26 + + +FlagWindow < MainWindow + id: flagWindow + !text: tr('Create Map Mark') + size: 196 185 + + Label + id: position + !text: tr('Position') .. ':' + text-auto-resize: true + anchors.top: parent.top + anchors.left: parent.left + margin-top: 2 + + Label + !text: tr('Description') .. ':' + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 7 + + TextEdit + id: description + margin-top: 3 + anchors.left: parent.left + anchors.top: prev.bottom + width: 158 + + FlagButton + id: flag1 + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 6 + margin-left: 0 + + FlagButton + id: flag2 + icon-clip: 11 0 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag3 + icon-clip: 22 0 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag4 + icon-clip: 33 0 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag5 + icon-clip: 44 0 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag6 + icon-clip: 55 0 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag7 + icon-clip: 66 0 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag8 + icon-clip: 77 0 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag9 + icon-clip: 88 0 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag10 + icon-clip: 99 0 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag11 + icon-clip: 0 11 11 11 + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 6 + margin-left: 0 + + FlagButton + id: flag12 + icon-clip: 11 11 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag13 + icon-clip: 22 11 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag14 + icon-clip: 33 11 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag15 + icon-clip: 44 11 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag16 + icon-clip: 55 11 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag17 + icon-clip: 66 11 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag18 + icon-clip: 77 11 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag19 + icon-clip: 88 11 11 11 + anchors.left: prev.right + anchors.top: prev.top + + FlagButton + id: flag20 + icon-clip: 99 11 11 11 + anchors.left: prev.right + anchors.top: prev.top + + Button + id: okButton + !text: tr('Ok') + width: 64 + anchors.right: next.left + anchors.bottom: parent.bottom + margin-right: 10 + + Button + id: cancelButton + !text: tr('Cancel') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom diff --git a/modules/game_minimap/minimap.lua b/modules/game_minimap/minimap.lua new file mode 100644 index 0000000..0cea327 --- /dev/null +++ b/modules/game_minimap/minimap.lua @@ -0,0 +1,165 @@ +minimapWidget = nil +minimapButton = nil +minimapWindow = nil +otmm = true +preloaded = false +fullmapView = false +oldZoom = nil +oldPos = nil + +function init() + minimapButton = modules.client_topmenu.addRightGameToggleButton('minimapButton', + tr('Minimap') .. ' (Ctrl+M)', '/images/topbuttons/minimap', toggle) + minimapButton:setOn(true) + + minimapWindow = g_ui.loadUI('minimap', modules.game_interface.getRightPanel()) + minimapWindow:setContentMinimumHeight(64) + + minimapWidget = minimapWindow:recursiveGetChildById('minimap') + + local gameRootPanel = modules.game_interface.getRootPanel() + g_keyboard.bindKeyPress('Alt+Left', function() minimapWidget:move(1,0) end, gameRootPanel) + g_keyboard.bindKeyPress('Alt+Right', function() minimapWidget:move(-1,0) end, gameRootPanel) + g_keyboard.bindKeyPress('Alt+Up', function() minimapWidget:move(0,1) end, gameRootPanel) + g_keyboard.bindKeyPress('Alt+Down', function() minimapWidget:move(0,-1) end, gameRootPanel) + g_keyboard.bindKeyDown('Ctrl+M', toggle) + g_keyboard.bindKeyDown('Ctrl+Shift+M', toggleFullMap) + + minimapWindow:setup() + + connect(g_game, { + onGameStart = online, + onGameEnd = offline, + }) + + connect(LocalPlayer, { + onPositionChange = updateCameraPosition + }) + + if g_game.isOnline() then + online() + end +end + +function terminate() + if g_game.isOnline() then + saveMap() + end + + disconnect(g_game, { + onGameStart = online, + onGameEnd = offline, + }) + + disconnect(LocalPlayer, { + onPositionChange = updateCameraPosition + }) + + local gameRootPanel = modules.game_interface.getRootPanel() + g_keyboard.unbindKeyPress('Alt+Left', gameRootPanel) + g_keyboard.unbindKeyPress('Alt+Right', gameRootPanel) + g_keyboard.unbindKeyPress('Alt+Up', gameRootPanel) + g_keyboard.unbindKeyPress('Alt+Down', gameRootPanel) + g_keyboard.unbindKeyDown('Ctrl+M') + g_keyboard.unbindKeyDown('Ctrl+Shift+M') + + minimapWindow:destroy() + minimapButton:destroy() +end + +function toggle() + if minimapButton:isOn() then + minimapWindow:close() + minimapButton:setOn(false) + else + minimapWindow:open() + minimapButton:setOn(true) + end +end + +function onMiniWindowClose() + minimapButton:setOn(false) +end + +function preload() + loadMap(false) + preloaded = true +end + +function online() + loadMap(not preloaded) + updateCameraPosition() +end + +function offline() + saveMap() +end + +function loadMap(clean) + local clientVersion = g_game.getClientVersion() + + if clean then + g_minimap.clean() + end + + if otmm then + local minimapFile = '/minimap.otmm' + if g_resources.fileExists(minimapFile) then + g_minimap.loadOtmm(minimapFile) + end + else + local minimapFile = '/minimap_' .. clientVersion .. '.otcm' + if g_resources.fileExists(minimapFile) then + g_map.loadOtcm(minimapFile) + end + end + minimapWidget:load() +end + +function saveMap() + local clientVersion = g_game.getClientVersion() + if otmm then + local minimapFile = '/minimap.otmm' + g_minimap.saveOtmm(minimapFile) + else + local minimapFile = '/minimap_' .. clientVersion .. '.otcm' + g_map.saveOtcm(minimapFile) + end + minimapWidget:save() +end + +function updateCameraPosition() + local player = g_game.getLocalPlayer() + if not player then return end + local pos = player:getPosition() + if not pos then return end + if not minimapWidget:isDragging() then + if not fullmapView then + minimapWidget:setCameraPosition(player:getPosition()) + end + minimapWidget:setCrossPosition(player:getPosition()) + end +end + +function toggleFullMap() + if not fullmapView then + fullmapView = true + minimapWindow:hide() + minimapWidget:setParent(modules.game_interface.getRootPanel()) + minimapWidget:fill('parent') + minimapWidget:setAlternativeWidgetsVisible(true) + else + fullmapView = false + minimapWidget:setParent(minimapWindow:getChildById('contentsPanel')) + minimapWidget:fill('parent') + minimapWindow:show() + minimapWidget:setAlternativeWidgetsVisible(false) + end + + local zoom = oldZoom or 0 + local pos = oldPos or minimapWidget:getCameraPosition() + oldZoom = minimapWidget:getZoom() + oldPos = minimapWidget:getCameraPosition() + minimapWidget:setZoom(zoom) + minimapWidget:setCameraPosition(pos) +end diff --git a/modules/game_minimap/minimap.otmod b/modules/game_minimap/minimap.otmod new file mode 100644 index 0000000..a62ba81 --- /dev/null +++ b/modules/game_minimap/minimap.otmod @@ -0,0 +1,9 @@ +Module + name: game_minimap + description: Manage minimap + author: edubart, BeniS + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ minimap ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_minimap/minimap.otui b/modules/game_minimap/minimap.otui new file mode 100644 index 0000000..b23a25e --- /dev/null +++ b/modules/game_minimap/minimap.otui @@ -0,0 +1,29 @@ +MiniWindow + id: minimapWindow + !text: tr('Minimap') + height: 150 + icon: /images/topbuttons/minimap + @onClose: modules.game_minimap.onMiniWindowClose() + &save: true + --&forceOpen: true + + Label + text: ? + text-align: center + phantom: false + !tooltip: tr('Hold left mouse button to navigate\nScroll mouse middle button to zoom\nRight mouse button to create map marks\nPress Ctrl+Shift+M to view the entire game map') + anchors.top: lockButton.top + anchors.right: lockButton.left + margin-right: 3 + size: 14 14 + + MiniWindowContents + Minimap + id: minimap + anchors.fill: parent + + ResizeBorder + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + enabled: true diff --git a/modules/game_modaldialog/modaldialog.lua b/modules/game_modaldialog/modaldialog.lua new file mode 100644 index 0000000..7c89ca1 --- /dev/null +++ b/modules/game_modaldialog/modaldialog.lua @@ -0,0 +1,117 @@ +modalDialog = nil + +function init() + g_ui.importStyle('modaldialog') + + connect(g_game, { onModalDialog = onModalDialog, + onGameEnd = destroyDialog }) + + local dialog = rootWidget:recursiveGetChildById('modalDialog') + if dialog then + modalDialog = dialog + end +end + +function terminate() + disconnect(g_game, { onModalDialog = onModalDialog, + onGameEnd = destroyDialog }) +end + +function destroyDialog() + if modalDialog then + modalDialog:destroy() + modalDialog = nil + end +end + +function onModalDialog(id, title, message, buttons, enterButton, escapeButton, choices, priority) + -- priority parameter is unused, not sure what its use is. + if modalDialog then + return + end + + modalDialog = g_ui.createWidget('ModalDialog', rootWidget) + + local messageLabel = modalDialog:getChildById('messageLabel') + local choiceList = modalDialog:getChildById('choiceList') + local choiceScrollbar = modalDialog:getChildById('choiceScrollBar') + local buttonsPanel = modalDialog:getChildById('buttonsPanel') + + modalDialog:setText(title) + messageLabel:setText(message) + + local labelHeight + for i = 1, #choices do + local choiceId = choices[i][1] + local choiceName = choices[i][2] + + local label = g_ui.createWidget('ChoiceListLabel', choiceList) + label.choiceId = choiceId + label:setText(choiceName) + label:setPhantom(false) + if not labelHeight then + labelHeight = label:getHeight() + end + end + choiceList:focusNextChild() + + local buttonsWidth = 0 + for i = 1, #buttons do + local buttonId = buttons[i][1] + local buttonText = buttons[i][2] + + local button = g_ui.createWidget('ModalButton', buttonsPanel) + button:setText(buttonText) + button.onClick = function(self) + local focusedChoice = choiceList:getFocusedChild() + local choice = 0xFF + if focusedChoice then + choice = focusedChoice.choiceId + end + g_game.answerModalDialog(id, buttonId, choice) + destroyDialog() + end + buttonsWidth = buttonsWidth + button:getWidth() + button:getMarginLeft() + button:getMarginRight() + end + + local additionalHeight = 0 + if #choices > 0 then + choiceList:setVisible(true) + choiceScrollbar:setVisible(true) + + additionalHeight = math.min(modalDialog.maximumChoices, math.max(modalDialog.minimumChoices, #choices)) * labelHeight + additionalHeight = additionalHeight + choiceList:getPaddingTop() + choiceList:getPaddingBottom() + end + + local horizontalPadding = modalDialog:getPaddingLeft() + modalDialog:getPaddingRight() + buttonsWidth = buttonsWidth + horizontalPadding + + modalDialog:setWidth(math.min(modalDialog.maximumWidth, math.max(buttonsWidth, messageLabel:getWidth(), modalDialog.minimumWidth))) + messageLabel:setWidth(math.min(modalDialog.maximumWidth, math.max(buttonsWidth, messageLabel:getWidth(), modalDialog.minimumWidth)) - horizontalPadding) + modalDialog:setHeight(modalDialog:getHeight() + additionalHeight + messageLabel:getHeight() - 8) + + local enterFunc = function() + local focusedChoice = choiceList:getFocusedChild() + local choice = 0xFF + if focusedChoice then + choice = focusedChoice.choiceId + end + g_game.answerModalDialog(id, enterButton, choice) + destroyDialog() + end + + local escapeFunc = function() + local focusedChoice = choiceList:getFocusedChild() + local choice = 0xFF + if focusedChoice then + choice = focusedChoice.choiceId + end + g_game.answerModalDialog(id, escapeButton, choice) + destroyDialog() + end + + choiceList.onDoubleClick = enterFunc + + modalDialog.onEnter = enterFunc + modalDialog.onEscape = escapeFunc +end \ No newline at end of file diff --git a/modules/game_modaldialog/modaldialog.otmod b/modules/game_modaldialog/modaldialog.otmod new file mode 100644 index 0000000..237e067 --- /dev/null +++ b/modules/game_modaldialog/modaldialog.otmod @@ -0,0 +1,10 @@ +Module + name: game_modaldialog + description: Show and process modal dialogs + author: Summ + website: https://github.com/edubart/otclient + sandboxed: true + dependencies: [ game_interface ] + scripts: [ modaldialog ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_modaldialog/modaldialog.otui b/modules/game_modaldialog/modaldialog.otui new file mode 100644 index 0000000..2f1824d --- /dev/null +++ b/modules/game_modaldialog/modaldialog.otui @@ -0,0 +1,72 @@ +ChoiceListLabel < Label + font: verdana-11px-monochrome + background-color: alpha + text-offset: 2 0 + focusable: true + + $focus: + background-color: #00000055 + color: #ffffff + +ChoiceList < TextList + id: choiceList + vertical-scrollbar: choiceScrollBar + anchors.fill: parent + anchors.top: prev.bottom + anchors.bottom: next.top + margin-top: 4 + margin-bottom: 10 + focusable: false + visible: false + +ChoiceScrollBar < VerticalScrollBar + id: choiceScrollBar + anchors.top: choiceList.top + anchors.bottom: choiceList.bottom + anchors.right: choiceList.right + step: 14 + pixels-scroll: true + visible: false + +ModalButton < Button + text-auto-resize: true + margin-top: 2 + margin-bottom: 2 + margin-left: 2 + + $pressed: + text-offset: 0 0 + +ModalDialog < MainWindow + id: modalDialog + size: 280 97 + &minimumWidth: 200 + &maximumWidth: 600 + &minimumChoices: 4 + &maximumChoices: 10 + + Label + id: messageLabel + anchors.top: parent.top + anchors.left: parent.left + text-align: left + text-auto-resize: true + text-wrap: true + + ChoiceList + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: next.top + + Panel + id: buttonsPanel + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 24 + layout: horizontalBox + align-right: true + + ChoiceScrollBar diff --git a/modules/game_npctrade/npctrade.lua b/modules/game_npctrade/npctrade.lua new file mode 100644 index 0000000..d76be94 --- /dev/null +++ b/modules/game_npctrade/npctrade.lua @@ -0,0 +1,537 @@ +BUY = 1 +SELL = 2 +CURRENCY = 'gold' +CURRENCY_DECIMAL = false +WEIGHT_UNIT = 'oz' +LAST_INVENTORY = 10 + +npcWindow = nil +itemsPanel = nil +radioTabs = nil +radioItems = nil +searchText = nil +setupPanel = nil +quantity = nil +quantityScroll = nil +nameLabel = nil +priceLabel = nil +moneyLabel = nil +weightDesc = nil +weightLabel = nil +capacityDesc = nil +capacityLabel = nil +tradeButton = nil +buyTab = nil +sellTab = nil +initialized = false + +showWeight = true +buyWithBackpack = nil +ignoreCapacity = nil +ignoreEquipped = nil +showAllItems = nil +sellAllButton = nil + +playerFreeCapacity = 0 +playerMoney = 0 +tradeItems = {} +playerItems = {} +selectedItem = nil + +cancelNextRelease = nil + +function init() + npcWindow = g_ui.displayUI('npctrade') + npcWindow:setVisible(false) + + itemsPanel = npcWindow:recursiveGetChildById('itemsPanel') + searchText = npcWindow:recursiveGetChildById('searchText') + + setupPanel = npcWindow:recursiveGetChildById('setupPanel') + quantityScroll = setupPanel:getChildById('quantityScroll') + nameLabel = setupPanel:getChildById('name') + priceLabel = setupPanel:getChildById('price') + moneyLabel = setupPanel:getChildById('money') + weightDesc = setupPanel:getChildById('weightDesc') + weightLabel = setupPanel:getChildById('weight') + capacityDesc = setupPanel:getChildById('capacityDesc') + capacityLabel = setupPanel:getChildById('capacity') + tradeButton = npcWindow:recursiveGetChildById('tradeButton') + + buyWithBackpack = npcWindow:recursiveGetChildById('buyWithBackpack') + ignoreCapacity = npcWindow:recursiveGetChildById('ignoreCapacity') + ignoreEquipped = npcWindow:recursiveGetChildById('ignoreEquipped') + showAllItems = npcWindow:recursiveGetChildById('showAllItems') + sellAllButton = npcWindow:recursiveGetChildById('sellAllButton') + + buyTab = npcWindow:getChildById('buyTab') + sellTab = npcWindow:getChildById('sellTab') + + radioTabs = UIRadioGroup.create() + radioTabs:addWidget(buyTab) + radioTabs:addWidget(sellTab) + radioTabs:selectWidget(buyTab) + radioTabs.onSelectionChange = onTradeTypeChange + + cancelNextRelease = false + + if g_game.isOnline() then + playerFreeCapacity = g_game.getLocalPlayer():getFreeCapacity() + end + + connect(g_game, { onGameEnd = hide, + onOpenNpcTrade = onOpenNpcTrade, + onCloseNpcTrade = onCloseNpcTrade, + onPlayerGoods = onPlayerGoods } ) + + connect(LocalPlayer, { onFreeCapacityChange = onFreeCapacityChange, + onInventoryChange = onInventoryChange } ) + + initialized = true +end + +function terminate() + initialized = false + npcWindow:destroy() + + disconnect(g_game, { onGameEnd = hide, + onOpenNpcTrade = onOpenNpcTrade, + onCloseNpcTrade = onCloseNpcTrade, + onPlayerGoods = onPlayerGoods } ) + + disconnect(LocalPlayer, { onFreeCapacityChange = onFreeCapacityChange, + onInventoryChange = onInventoryChange } ) +end + +function show() + if g_game.isOnline() then + if #tradeItems[BUY] > 0 then + radioTabs:selectWidget(buyTab) + else + radioTabs:selectWidget(sellTab) + end + + npcWindow:show() + npcWindow:raise() + npcWindow:focus() + end +end + +function hide() + npcWindow:hide() +end + +function onItemBoxChecked(widget) + if widget:isChecked() then + local item = widget.item + selectedItem = item + refreshItem(item) + tradeButton:enable() + + if getCurrentTradeType() == SELL then + quantityScroll:setValue(quantityScroll:getMaximum()) + end + end +end + +function onQuantityValueChange(quantity) + if selectedItem then + weightLabel:setText(string.format('%.2f', selectedItem.weight*quantity) .. ' ' .. WEIGHT_UNIT) + priceLabel:setText(formatCurrency(getItemPrice(selectedItem))) + end +end + +function onTradeTypeChange(radioTabs, selected, deselected) + tradeButton:setText(selected:getText()) + selected:setOn(true) + deselected:setOn(false) + + local currentTradeType = getCurrentTradeType() + buyWithBackpack:setVisible(currentTradeType == BUY) + ignoreCapacity:setVisible(currentTradeType == BUY) + ignoreEquipped:setVisible(currentTradeType == SELL) + showAllItems:setVisible(currentTradeType == SELL) + sellAllButton:setVisible(currentTradeType == SELL) + + refreshTradeItems() + refreshPlayerGoods() +end + +function onTradeClick() + if getCurrentTradeType() == BUY then + g_game.buyItem(selectedItem.ptr, quantityScroll:getValue(), ignoreCapacity:isChecked(), buyWithBackpack:isChecked()) + else + g_game.sellItem(selectedItem.ptr, quantityScroll:getValue(), ignoreEquipped:isChecked()) + end +end + +function onSearchTextChange() + refreshPlayerGoods() +end + +function itemPopup(self, mousePosition, mouseButton) + if cancelNextRelease then + cancelNextRelease = false + return false + end + + if mouseButton == MouseRightButton then + local menu = g_ui.createWidget('PopupMenu') + menu:setGameMenu(true) + menu:addOption(tr('Look'), function() return g_game.inspectNpcTrade(self:getItem()) end) + menu:display(mousePosition) + return true + elseif ((g_mouse.isPressed(MouseLeftButton) and mouseButton == MouseRightButton) + or (g_mouse.isPressed(MouseRightButton) and mouseButton == MouseLeftButton)) then + cancelNextRelease = true + g_game.inspectNpcTrade(self:getItem()) + return true + end + return false +end + +function onBuyWithBackpackChange() + if selectedItem then + refreshItem(selectedItem) + end +end + +function onIgnoreCapacityChange() + refreshPlayerGoods() +end + +function onIgnoreEquippedChange() + refreshPlayerGoods() +end + +function onShowAllItemsChange() + refreshPlayerGoods() +end + +function setCurrency(currency, decimal) + CURRENCY = currency + CURRENCY_DECIMAL = decimal +end + +function setShowWeight(state) + showWeight = state + weightDesc:setVisible(state) + weightLabel:setVisible(state) +end + +function setShowYourCapacity(state) + capacityDesc:setVisible(state) + capacityLabel:setVisible(state) + ignoreCapacity:setVisible(state) +end + +function clearSelectedItem() + nameLabel:clearText() + weightLabel:clearText() + priceLabel:clearText() + tradeButton:disable() + quantityScroll:setMinimum(0) + quantityScroll:setMaximum(0) + if selectedItem then + radioItems:selectWidget(nil) + selectedItem = nil + end +end + +function getCurrentTradeType() + if tradeButton:getText() == tr('Buy') then + return BUY + else + return SELL + end +end + +function getItemPrice(item, single) + local amount = 1 + local single = single or false + if not single then + amount = quantityScroll:getValue() + end + if getCurrentTradeType() == BUY then + if buyWithBackpack:isChecked() then + if item.ptr:isStackable() then + return item.price*amount + 20 + else + return item.price*amount + math.ceil(amount/20)*20 + end + end + end + return item.price*amount +end + +function getSellQuantity(item) + if not item or not playerItems[item:getId()] then return 0 end + local removeAmount = 0 + if ignoreEquipped:isChecked() then + local localPlayer = g_game.getLocalPlayer() + for i=1,LAST_INVENTORY do + local inventoryItem = localPlayer:getInventoryItem(i) + if inventoryItem and inventoryItem:getId() == item:getId() then + removeAmount = removeAmount + inventoryItem:getCount() + end + end + end + return playerItems[item:getId()] - removeAmount +end + +function canTradeItem(item) + if getCurrentTradeType() == BUY then + return (ignoreCapacity:isChecked() or (not ignoreCapacity:isChecked() and playerFreeCapacity >= item.weight)) and playerMoney >= getItemPrice(item, true) + else + return getSellQuantity(item.ptr) > 0 + end +end + +function refreshItem(item) + nameLabel:setText(item.name) + weightLabel:setText(string.format('%.2f', item.weight) .. ' ' .. WEIGHT_UNIT) + priceLabel:setText(formatCurrency(getItemPrice(item))) + + if getCurrentTradeType() == BUY then + local capacityMaxCount = math.floor(playerFreeCapacity / item.weight) + if ignoreCapacity:isChecked() then + capacityMaxCount = 65535 + end + local priceMaxCount = math.floor(playerMoney / getItemPrice(item, true)) + local finalCount = math.max(0, math.min(getMaxAmount(), math.min(priceMaxCount, capacityMaxCount))) + quantityScroll:setMinimum(1) + quantityScroll:setMaximum(finalCount) + else + quantityScroll:setMinimum(1) + quantityScroll:setMaximum(math.max(0, math.min(getMaxAmount(), getSellQuantity(item.ptr)))) + end + + setupPanel:enable() +end + +function refreshTradeItems() + local layout = itemsPanel:getLayout() + layout:disableUpdates() + + clearSelectedItem() + + searchText:clearText() + setupPanel:disable() + itemsPanel:destroyChildren() + + if radioItems then + radioItems:destroy() + end + radioItems = UIRadioGroup.create() + + local currentTradeItems = tradeItems[getCurrentTradeType()] + for key,item in pairs(currentTradeItems) do + local itemBox = g_ui.createWidget('NPCItemBox', itemsPanel) + itemBox.item = item + + local text = '' + local name = item.name + text = text .. name + if showWeight then + local weight = string.format('%.2f', item.weight) .. ' ' .. WEIGHT_UNIT + text = text .. '\n' .. weight + end + local price = formatCurrency(item.price) + text = text .. '\n' .. price + itemBox:setText(text) + + local itemWidget = itemBox:getChildById('item') + itemWidget:setItem(item.ptr) + itemWidget.onMouseRelease = itemPopup + + radioItems:addWidget(itemBox) + end + + layout:enableUpdates() + layout:update() +end + +function refreshPlayerGoods() + if not initialized then return end + + checkSellAllTooltip() + + moneyLabel:setText(formatCurrency(playerMoney)) + capacityLabel:setText(string.format('%.2f', playerFreeCapacity) .. ' ' .. WEIGHT_UNIT) + + local currentTradeType = getCurrentTradeType() + local searchFilter = searchText:getText():lower() + local foundSelectedItem = false + + local items = itemsPanel:getChildCount() + for i=1,items do + local itemWidget = itemsPanel:getChildByIndex(i) + local item = itemWidget.item + + local canTrade = canTradeItem(item) + itemWidget:setOn(canTrade) + itemWidget:setEnabled(canTrade) + + local searchCondition = (searchFilter == '') or (searchFilter ~= '' and string.find(item.name:lower(), searchFilter) ~= nil) + local showAllItemsCondition = (currentTradeType == BUY) or (showAllItems:isChecked()) or (currentTradeType == SELL and not showAllItems:isChecked() and canTrade) + itemWidget:setVisible(searchCondition and showAllItemsCondition) + + if selectedItem == item and itemWidget:isEnabled() and itemWidget:isVisible() then + foundSelectedItem = true + end + end + + if not foundSelectedItem then + clearSelectedItem() + end + + if selectedItem then + refreshItem(selectedItem) + end +end + +function onOpenNpcTrade(items) + tradeItems[BUY] = {} + tradeItems[SELL] = {} + + for key,item in pairs(items) do + if item[4] > 0 then + local newItem = {} + newItem.ptr = item[1] + newItem.name = item[2] + newItem.weight = item[3] / 100 + newItem.price = item[4] + table.insert(tradeItems[BUY], newItem) + end + + if item[5] > 0 then + local newItem = {} + newItem.ptr = item[1] + newItem.name = item[2] + newItem.weight = item[3] / 100 + newItem.price = item[5] + table.insert(tradeItems[SELL], newItem) + end + end + + refreshTradeItems() + addEvent(show) -- player goods has not been parsed yet +end + +function closeNpcTrade() + g_game.closeNpcTrade() + hide() +end + +function onCloseNpcTrade() + hide() +end + +function onPlayerGoods(money, items) + playerMoney = money + + playerItems = {} + for key,item in pairs(items) do + local id = item[1]:getId() + if not playerItems[id] then + playerItems[id] = item[2] + else + playerItems[id] = playerItems[id] + item[2] + end + end + + refreshPlayerGoods() +end + +function onFreeCapacityChange(localPlayer, freeCapacity, oldFreeCapacity) + playerFreeCapacity = freeCapacity + + if npcWindow:isVisible() then + refreshPlayerGoods() + end +end + +function onInventoryChange(inventory, item, oldItem) + refreshPlayerGoods() +end + +function getTradeItemData(id, type) + if table.empty(tradeItems[type]) then + return false + end + + if type then + for key,item in pairs(tradeItems[type]) do + if item.ptr and item.ptr:getId() == id then + return item + end + end + else + for _,items in pairs(tradeItems) do + for key,item in pairs(items) do + if item.ptr and item.ptr:getId() == id then + return item + end + end + end + end + return false +end + +function checkSellAllTooltip() + sellAllButton:setEnabled(true) + sellAllButton:removeTooltip() + + local total = 0 + local info = '' + local first = true + + for key, amount in pairs(playerItems) do + local data = getTradeItemData(key, SELL) + if data then + amount = getSellQuantity(data.ptr) + if amount > 0 then + if data and amount > 0 then + info = info..(not first and "\n" or "").. + amount.." ".. + data.name.." (".. + data.price*amount.." gold)" + + total = total+(data.price*amount) + if first then first = false end + end + end + end + end + if info ~= '' then + info = info.."\nTotal: "..total.." gold" + sellAllButton:setTooltip(info) + else + sellAllButton:setEnabled(false) + end +end + +function formatCurrency(amount) + if CURRENCY_DECIMAL then + return string.format("%.02f", amount/100.0) .. ' ' .. CURRENCY + else + return amount .. ' ' .. CURRENCY + end +end + +function getMaxAmount() + if getCurrentTradeType() == SELL and g_game.getFeature(GameDoubleShopSellAmount) then + return 10000 + end + return 100 +end + +function sellAll() + for itemid,item in pairs(playerItems) do + local item = Item.create(itemid) + local amount = getSellQuantity(item) + if amount > 0 then + g_game.sellItem(item, amount, ignoreEquipped:isChecked()) + end + end +end diff --git a/modules/game_npctrade/npctrade.otmod b/modules/game_npctrade/npctrade.otmod new file mode 100644 index 0000000..df1e957 --- /dev/null +++ b/modules/game_npctrade/npctrade.otmod @@ -0,0 +1,9 @@ +Module + name: game_npctrade + description: NPC trade interface + author: andrefaramir, baxnie + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ npctrade ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_npctrade/npctrade.otui b/modules/game_npctrade/npctrade.otui new file mode 100644 index 0000000..7c766a1 --- /dev/null +++ b/modules/game_npctrade/npctrade.otui @@ -0,0 +1,270 @@ +NPCOfferLabel < Label + anchors.left: prev.right + anchors.top: prev.top + margin-left: 5 + text-auto-resize: true + +NPCItemBox < UICheckBox + border-width: 1 + border-color: #000000 + color: #aaaaaa + text-align: center + text-offset: 0 30 + @onCheckChange: modules.game_npctrade.onItemBoxChecked(self) + + Item + id: item + phantom: true + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + image-color: #ffffffff + margin-top: 5 + + $checked on: + border-color: #ffffff + + $!checked: + border-color: #000000 + + $!on: + image-color: #ffffff88 + color: #aaaaaa88 + +MainWindow + id: npcWindow + !text: tr('NPC Trade') + size: 550 515 + @onEscape: modules.game_npctrade.closeNpcTrade() + + TabButton + id: buyTab + !tooltip: tr("List of items that you're able to buy") + !text: tr('Buy') + checked: true + on: true + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: parent.top + margin-right: 5 + margin-top: 5 + + TabButton + id: sellTab + !tooltip: tr("List of items that you're able to sell") + !text: tr('Sell') + anchors.left: parent.horizontalCenter + anchors.right: parent.right + anchors.top: parent.top + margin-left: 5 + margin-top: 5 + + FlatPanel + height: 250 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 10 + + VerticalScrollBar + id: itemsPanelListScrollBar + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + step: 24 + pixels-scroll: true + + ScrollablePanel + id: itemsPanel + height: 250 + anchors.left: parent.left + anchors.right: prev.left + anchors.top: parent.top + anchors.bottom: parent.bottom + vertical-scrollbar: itemsPanelListScrollBar + margin-left: 5 + margin-right: 5 + layout: + type: grid + cell-size: 160 90 + flow: true + auto-spacing: true + + FlatPanel + id: setupPanel + height: 140 + enabled: false + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 10 + margin-right: 5 + image-color: #ffffff88 + + Label + !text: tr('Name') .. ':' + anchors.left: parent.left + anchors.top: parent.top + margin-top: 5 + margin-left: 5 + width: 85 + + NPCOfferLabel + id: name + + Label + !text: tr('Price') .. ':' + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 5 + margin-left: 5 + width: 85 + + NPCOfferLabel + id: price + + Label + !text: tr('Your Money') .. ':' + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 5 + margin-left: 5 + width: 85 + + NPCOfferLabel + id: money + + Label + id: weightDesc + !text: tr('Weight') .. ':' + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 5 + margin-left: 5 + width: 85 + + NPCOfferLabel + id: weight + + Label + id: capacityDesc + !text: tr('Your Capacity') .. ':' + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 5 + margin-left: 5 + width: 85 + + NPCOfferLabel + id: capacity + + HorizontalScrollBar + id: quantityScroll + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + margin-top: 5 + margin-left: 5 + margin-right: 5 + show-value: true + minimum: 1 + maximum: 100 + step: 1 + @onValueChange: modules.game_npctrade.onQuantityValueChange(self:getValue()) + + FlatPanel + id: buyOptions + height: 140 + anchors.top: prev.top + anchors.left: parent.horizontalCenter + anchors.right: parent.right + margin-left: 5 + image-color: #ffffff88 + + Label + id: searchLabel + !text: tr('Search') .. ':' + anchors.left: parent.left + anchors.top: parent.top + text-auto-resize: true + margin-top: 7 + margin-left: 5 + + TextEdit + id: searchText + anchors.left: prev.right + anchors.top: prev.top + anchors.right: parent.right + margin-top: -2 + margin-left: 5 + margin-right: 5 + @onTextChange: modules.game_npctrade.onSearchTextChange() + + CheckBox + id: buyWithBackpack + !text: tr('Buy with backpack') + anchors.top: searchText.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-left: 5 + margin-top: 5 + @onCheckChange: modules.game_npctrade.onBuyWithBackpackChange() + + CheckBox + id: ignoreCapacity + !text: tr('Ignore capacity') + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-left: 5 + margin-top: 5 + @onCheckChange: modules.game_npctrade.onIgnoreCapacityChange() + + CheckBox + id: ignoreEquipped + !text: tr('Ignore equipped') + anchors.top: searchText.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-left: 5 + margin-top: 5 + visible: false + checked: true + @onCheckChange: modules.game_npctrade.onIgnoreEquippedChange() + + CheckBox + id: showAllItems + !text: tr('Show all items') + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-left: 5 + margin-top: 5 + visible: false + checked: true + @onCheckChange: modules.game_npctrade.onShowAllItemsChange() + + Button + id: sellAllButton + !text: tr('Sell All') + width: 64 + anchors.right: next.left + anchors.bottom: parent.bottom + margin-right: 10 + visible: false + @onClick: modules.game_npctrade.sellAll() + + Button + id: tradeButton + !text: tr('Buy') + width: 64 + anchors.right: next.left + anchors.bottom: parent.bottom + margin-right: 10 + @onClick: modules.game_npctrade.onTradeClick() + + Button + !text: tr('Close') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: modules.game_npctrade.closeNpcTrade() diff --git a/modules/game_outfit/outfit.lua b/modules/game_outfit/outfit.lua new file mode 100644 index 0000000..2c7a5f6 --- /dev/null +++ b/modules/game_outfit/outfit.lua @@ -0,0 +1,319 @@ +ADDON_SETS = { + [1] = { 1 }, + [2] = { 2 }, + [3] = { 1, 2 }, + [4] = { 3 }, + [5] = { 1, 3 }, + [6] = { 2, 3 }, + [7] = { 1, 2, 3 } +} + +outfitWindow = nil +outfit = nil +outfits = nil +outfitCreature = nil +currentOutfit = 1 + +addons = nil +currentColorBox = nil +currentClotheButtonBox = nil +colorBoxes = {} + +mount = nil +mounts = nil +mountCreature = nil +currentMount = 1 + +function init() + connect(g_game, { + onOpenOutfitWindow = create, + onGameEnd = destroy + }) +end + +function terminate() + disconnect(g_game, { + onOpenOutfitWindow = create, + onGameEnd = destroy + }) + destroy() +end + +function updateMount() + if table.empty(mounts) or not mount then + return + end + local nameMountWidget = outfitWindow:getChildById('mountName') + nameMountWidget:setText(mounts[currentMount][2]) + + mount.type = mounts[currentMount][1] + mountCreature:setOutfit(mount) +end + +function create(creatureOutfit, outfitList, creatureMount, mountList) + if outfitWindow and not outfitWindow:isHidden() then + return + end + + outfitCreature = creatureOutfit + mountCreature = creatureMount + outfits = outfitList + mounts = mountList + destroy() + + outfitWindow = g_ui.displayUI('outfitwindow') + local colorBoxPanel = outfitWindow:getChildById('colorBoxPanel') + + -- setup outfit/mount display boxs + local outfitCreatureBox = outfitWindow:getChildById('outfitCreatureBox') + if outfitCreature then + outfit = outfitCreature:getOutfit() + outfitCreatureBox:setCreature(outfitCreature) + else + outfitCreatureBox:hide() + outfitWindow:getChildById('outfitName'):hide() + outfitWindow:getChildById('outfitNextButton'):hide() + outfitWindow:getChildById('outfitPrevButton'):hide() + end + + local mountCreatureBox = outfitWindow:getChildById('mountCreatureBox') + if mountCreature then + mount = mountCreature:getOutfit() + mountCreatureBox:setCreature(mountCreature) + else + mountCreatureBox:hide() + outfitWindow:getChildById('mountName'):hide() + outfitWindow:getChildById('mountNextButton'):hide() + outfitWindow:getChildById('mountPrevButton'):hide() + end + + -- set addons + addons = { + [1] = {widget = outfitWindow:getChildById('addon1'), value = 1}, + [2] = {widget = outfitWindow:getChildById('addon2'), value = 2}, + [3] = {widget = outfitWindow:getChildById('addon3'), value = 4} + } + + for _, addon in pairs(addons) do + addon.widget.onCheckChange = function(self) onAddonCheckChange(self, addon.value) end + end + + if outfit.addons and outfit.addons > 0 then + for _, i in pairs(ADDON_SETS[outfit.addons]) do + addons[i].widget:setChecked(true) + end + end + + -- hook outfit sections + currentClotheButtonBox = outfitWindow:getChildById('head') + outfitWindow:getChildById('head').onCheckChange = onClotheCheckChange + outfitWindow:getChildById('primary').onCheckChange = onClotheCheckChange + outfitWindow:getChildById('secondary').onCheckChange = onClotheCheckChange + outfitWindow:getChildById('detail').onCheckChange = onClotheCheckChange + + -- populate color panel + for j=0,6 do + for i=0,18 do + local colorBox = g_ui.createWidget('ColorBox', colorBoxPanel) + local outfitColor = getOutfitColor(j*19 + i) + colorBox:setImageColor(outfitColor) + colorBox:setId('colorBox' .. j*19+i) + colorBox.colorId = j*19 + i + + if j*19 + i == outfit.head then + currentColorBox = colorBox + colorBox:setChecked(true) + end + colorBox.onCheckChange = onColorCheckChange + colorBoxes[#colorBoxes+1] = colorBox + end + end + + -- set current outfit/mount + currentOutfit = 1 + for i=1,#outfitList do + if outfit and outfitList[i][1] == outfit.type then + currentOutfit = i + break + end + end + currentMount = 1 + for i=1,#mountList do + if mount and mountList[i][1] == mount.type then + currentMount = i + break + end + end + + updateOutfit() + updateMount() +end + +function destroy() + if outfitWindow then + outfitWindow:destroy() + outfitWindow = nil + outfitCreature = nil + mountCreature = nil + currentColorBox = nil + currentClotheButtonBox = nil + colorBoxes = {} + addons = {} + end +end + +function randomize() + local outfitTemplate = { + outfitWindow:getChildById('head'), + outfitWindow:getChildById('primary'), + outfitWindow:getChildById('secondary'), + outfitWindow:getChildById('detail') + } + + for i = 1, #outfitTemplate do + outfitTemplate[i]:setChecked(true) + colorBoxes[math.random(1, #colorBoxes)]:setChecked(true) + outfitTemplate[i]:setChecked(false) + end + outfitTemplate[1]:setChecked(true) +end + +function accept() + if mount then outfit.mount = mount.type end + g_game.changeOutfit(outfit) + destroy() +end + +function nextOutfitType() + if not outfits then + return + end + currentOutfit = currentOutfit + 1 + if currentOutfit > #outfits then + currentOutfit = 1 + end + updateOutfit() +end + +function previousOutfitType() + if not outfits then + return + end + currentOutfit = currentOutfit - 1 + if currentOutfit <= 0 then + currentOutfit = #outfits + end + updateOutfit() +end + +function nextMountType() + if not mounts then + return + end + currentMount = currentMount + 1 + if currentMount > #mounts then + currentMount = 1 + end + updateMount() +end + +function previousMountType() + if not mounts then + return + end + currentMount = currentMount - 1 + if currentMount <= 0 then + currentMount = #mounts + end + updateMount() +end + +function onAddonCheckChange(addon, value) + if addon:isChecked() then + outfit.addons = outfit.addons + value + else + outfit.addons = outfit.addons - value + end + outfitCreature:setOutfit(outfit) +end + +function onColorCheckChange(colorBox) + if colorBox == currentColorBox then + colorBox.onCheckChange = nil + colorBox:setChecked(true) + colorBox.onCheckChange = onColorCheckChange + else + currentColorBox.onCheckChange = nil + currentColorBox:setChecked(false) + currentColorBox.onCheckChange = onColorCheckChange + + currentColorBox = colorBox + + if currentClotheButtonBox:getId() == 'head' then + outfit.head = currentColorBox.colorId + elseif currentClotheButtonBox:getId() == 'primary' then + outfit.body = currentColorBox.colorId + elseif currentClotheButtonBox:getId() == 'secondary' then + outfit.legs = currentColorBox.colorId + elseif currentClotheButtonBox:getId() == 'detail' then + outfit.feet = currentColorBox.colorId + end + + outfitCreature:setOutfit(outfit) + end +end + +function onClotheCheckChange(clotheButtonBox) + if clotheButtonBox == currentClotheButtonBox then + clotheButtonBox.onCheckChange = nil + clotheButtonBox:setChecked(true) + clotheButtonBox.onCheckChange = onClotheCheckChange + else + currentClotheButtonBox.onCheckChange = nil + currentClotheButtonBox:setChecked(false) + currentClotheButtonBox.onCheckChange = onClotheCheckChange + + currentClotheButtonBox = clotheButtonBox + + local colorId = 0 + if currentClotheButtonBox:getId() == 'head' then + colorId = outfit.head + elseif currentClotheButtonBox:getId() == 'primary' then + colorId = outfit.body + elseif currentClotheButtonBox:getId() == 'secondary' then + colorId = outfit.legs + elseif currentClotheButtonBox:getId() == 'detail' then + colorId = outfit.feet + end + outfitWindow:recursiveGetChildById('colorBox' .. colorId):setChecked(true) + end +end + +function updateOutfit() + if table.empty(outfits) or not outfit then + return + end + local nameWidget = outfitWindow:getChildById('outfitName') + nameWidget:setText(outfits[currentOutfit][2]) + + local availableAddons = outfits[currentOutfit][3] + + local prevAddons = {} + for k, addon in pairs(addons) do + prevAddons[k] = addon.widget:isChecked() + addon.widget:setChecked(false) + addon.widget:setEnabled(false) + end + outfit.addons = 0 + + if availableAddons > 0 then + for _, i in pairs(ADDON_SETS[availableAddons]) do + addons[i].widget:setEnabled(true) + addons[i].widget:setChecked(true) + end + end + + outfit.type = outfits[currentOutfit][1] + outfitCreature:setOutfit(outfit) +end + diff --git a/modules/game_outfit/outfit.otmod b/modules/game_outfit/outfit.otmod new file mode 100644 index 0000000..284d076 --- /dev/null +++ b/modules/game_outfit/outfit.otmod @@ -0,0 +1,9 @@ +Module + name: game_outfit + description: Change local player outfit + author: baxnie, edubart + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ outfit ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_outfit/outfitwindow.otui b/modules/game_outfit/outfitwindow.otui new file mode 100644 index 0000000..abe66ea --- /dev/null +++ b/modules/game_outfit/outfitwindow.otui @@ -0,0 +1,191 @@ +NextOutfitButton < NextButton +PrevOutfitButton < PreviousButton +NextMountButton < NextButton +PrevMountButton < PreviousButton + +MainWindow + !text: tr('Select Outfit') + size: 338 375 + + @onEnter: modules.game_outfit.accept() + @onEscape: modules.game_outfit.destroy() + + // Creature Boxes + Creature + id: outfitCreatureBox + anchors.top: parent.top + anchors.left: parent.left + margin-top: 15 + margin-left: 22 + padding: 4 4 4 4 + fixed-creature-size: true + + Label + id: outfitName + !text: tr('No Outfit') + width: 115 + anchors.bottom: prev.top + anchors.left: prev.left + margin-bottom: 2 + + NextOutfitButton + id: outfitNextButton + anchors.left: outfitCreatureBox.right + anchors.verticalCenter: outfitCreatureBox.verticalCenter + margin-left: 3 + enabled: true + @onClick: modules.game_outfit.nextOutfitType() + + PrevOutfitButton + id: outfitPrevButton + anchors.right: outfitCreatureBox.left + anchors.verticalCenter: outfitCreatureBox.verticalCenter + margin-right: 3 + enabled: true + @onClick: modules.game_outfit.previousOutfitType() + + Creature + id: mountCreatureBox + anchors.top: parent.top + anchors.right: parent.right + margin-top: 15 + margin-right: 22 + padding: 4 4 4 4 + fixed-creature-size: true + + Label + id: mountName + !text: tr('No Mount') + width: 115 + anchors.bottom: prev.top + anchors.left: prev.left + margin-bottom: 2 + + NextMountButton + id: mountNextButton + anchors.left: mountCreatureBox.right + anchors.verticalCenter: mountCreatureBox.verticalCenter + margin-left: 3 + enabled: true + @onClick: modules.game_outfit.nextMountType() + + PrevMountButton + id: mountPrevButton + anchors.right: mountCreatureBox.left + anchors.verticalCenter: mountCreatureBox.verticalCenter + margin-right: 3 + enabled: true + @onClick: modules.game_outfit.previousMountType() + + // Addon Check Boxes + + CheckBox + id: addon1 + !text: tr('Addon 1') + width: 80 + anchors.top: outfitCreatureBox.bottom + anchors.left: parent.left + margin-top: 6 + margin-left: 2 + enabled: false + + CheckBox + id: addon2 + !text: tr('Addon 2') + width: 80 + anchors.top: prev.top + anchors.left: prev.right + enabled: false + + CheckBox + id: addon3 + !text: tr('Addon 3') + width: 80 + anchors.top: prev.top + anchors.left: prev.right + enabled: false + + // Body Selection Buttons + + ButtonBox + id: head + !text: tr('Head') + anchors.top: addon1.bottom + anchors.left: addon1.left + margin-top: 5 + checked: true + width: 76 + + ButtonBox + id: primary + !text: tr('Primary') + anchors.top: prev.top + anchors.left: prev.right + width: 76 + + ButtonBox + id: secondary + !text: tr('Secondary') + anchors.top: prev.top + anchors.left: prev.right + width: 76 + + ButtonBox + id: detail + !text: tr('Detail') + anchors.top: prev.top + anchors.left: prev.right + width: 76 + + // Color Panel + + Panel + id: colorBoxPanel + anchors.top: head.bottom + anchors.left: head.left + margin-top: 3 + margin-right: 20 + width: 302 + height: 119 + layout: + type: grid + cell-size: 14 14 + cell-spacing: 2 + num-columns: 19 + num-lines: 7 + + // Action Button Section + + Button + id: randomizeButton + !text: tr('Randomize') + !tooltip: tr('Randomize characters outfit') + width: 75 + anchors.left: prev.left + anchors.top: prev.bottom + margin-right: 16 + @onClick: modules.game_outfit.randomize() + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: next.top + margin-bottom: 10 + margin-top: 5 + + Button + id: outfitOkButton + !text: tr('Ok') + width: 64 + anchors.right: next.left + anchors.bottom: parent.bottom + margin-right: 16 + @onClick: modules.game_outfit.accept() + + Button + id: outfitCancelButton + !text: tr('Cancel') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: modules.game_outfit.destroy() diff --git a/modules/game_playerdeath/deathwindow.otui b/modules/game_playerdeath/deathwindow.otui new file mode 100644 index 0000000..4f185bf --- /dev/null +++ b/modules/game_playerdeath/deathwindow.otui @@ -0,0 +1,30 @@ +DeathWindow < MainWindow + id: deathWindow + !text: tr('You are dead') + &baseWidth: 350 + &baseHeight: 15 + + Label + id: labelText + width: 550 + height: 140 + anchors.left: parent.left + anchors.top: parent.top + margin-left: 10 + margin-top: 2 + + Button + id: buttonOk + !text: tr('Ok') + width: 64 + anchors.left: parent.left + anchors.bottom: parent.bottom + margin-left: 160 + + Button + id: buttonCancel + !text: tr('Cancel') + width: 64 + anchors.left: prev.right + anchors.bottom: parent.bottom + margin-left: 5 diff --git a/modules/game_playerdeath/playerdeath.lua b/modules/game_playerdeath/playerdeath.lua new file mode 100644 index 0000000..736f86e --- /dev/null +++ b/modules/game_playerdeath/playerdeath.lua @@ -0,0 +1,86 @@ +deathWindow = nil + +local deathTexts = { + regular = {text = 'Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nSimply click on Ok to resume your journeys!', height = 140, width = 0}, + unfair = {text = 'Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nThis death penalty has been reduced by %i%%\nbecause it was an unfair fight.\n\nSimply click on Ok to resume your journeys!', height = 185, width = 0}, + blessed = {text = 'Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back into this world\n\nThis death penalty has been reduced by 100%\nbecause you are blessed with the Adventurer\'s Blessing\n\nSimply click on Ok to resume your journeys!', height = 170, width = 90} +} + +function init() + g_ui.importStyle('deathwindow') + + connect(g_game, { onDeath = display, + onGameEnd = reset }) +end + +function terminate() + disconnect(g_game, { onDeath = display, + onGameEnd = reset }) + + reset() +end + +function reset() + if deathWindow then + deathWindow:destroy() + deathWindow = nil + end +end + +function display(deathType, penalty) + displayDeadMessage() + openWindow(deathType, penalty) +end + +function displayDeadMessage() + local advanceLabel = modules.game_interface.getRootPanel():recursiveGetChildById('middleCenterLabel') + if advanceLabel:isVisible() then return end + + modules.game_textmessage.displayGameMessage(tr('You are dead.')) +end + +function openWindow(deathType, penalty) + if deathWindow then + deathWindow:destroy() + return + end + + deathWindow = g_ui.createWidget('DeathWindow', rootWidget) + + local textLabel = deathWindow:getChildById('labelText') + if deathType == DeathType.Regular then + if penalty == 100 then + textLabel:setText(deathTexts.regular.text) + deathWindow:setHeight(deathWindow.baseHeight + deathTexts.regular.height) + deathWindow:setWidth(deathWindow.baseWidth + deathTexts.regular.width) + else + textLabel:setText(string.format(deathTexts.unfair.text, 100 - penalty)) + deathWindow:setHeight(deathWindow.baseHeight + deathTexts.unfair.height) + deathWindow:setWidth(deathWindow.baseWidth + deathTexts.unfair.width) + end + elseif deathType == DeathType.Blessed then + textLabel:setText(deathTexts.blessed.text) + deathWindow:setHeight(deathWindow.baseHeight + deathTexts.blessed.height) + deathWindow:setWidth(deathWindow.baseWidth + deathTexts.blessed.width) + end + + local okButton = deathWindow:getChildById('buttonOk') + local cancelButton = deathWindow:getChildById('buttonCancel') + + local okFunc = function() + CharacterList.doLogin() + okButton:getParent():destroy() + deathWindow = nil + end + local cancelFunc = function() + g_game.safeLogout() + cancelButton:getParent():destroy() + deathWindow = nil + end + + deathWindow.onEnter = okFunc + deathWindow.onEscape = cancelFunc + + okButton.onClick = okFunc + cancelButton.onClick = cancelFunc +end diff --git a/modules/game_playerdeath/playerdeath.otmod b/modules/game_playerdeath/playerdeath.otmod new file mode 100644 index 0000000..d8dcbb5 --- /dev/null +++ b/modules/game_playerdeath/playerdeath.otmod @@ -0,0 +1,9 @@ +Module + name: game_playerdeath + description: Manage player deaths + author: BeniS, edubart + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ playerdeath ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_playermount/playermount.lua b/modules/game_playermount/playermount.lua new file mode 100644 index 0000000..391d19f --- /dev/null +++ b/modules/game_playermount/playermount.lua @@ -0,0 +1,48 @@ +function init() + connect(g_game, { + onGameStart = online, + onGameEnd = offline + }) + if g_game.isOnline() then online() end +end + +function terminate() + disconnect(g_game, { + onGameStart = online, + onGameEnd = offline + }) + offline() +end + +function online() + if g_game.getFeature(GamePlayerMounts) then + g_keyboard.bindKeyDown('Ctrl+R', toggleMount) + end +end + +function offline() + if g_game.getFeature(GamePlayerMounts) then + g_keyboard.unbindKeyDown('Ctrl+R') + end +end + +function toggleMount() + local player = g_game.getLocalPlayer() + if player then + player:toggleMount() + end +end + +function mount() + local player = g_game.getLocalPlayer() + if player then + player:mount() + end +end + +function dismount() + local player = g_game.getLocalPlayer() + if player then + player:dismount() + end +end diff --git a/modules/game_playermount/playermount.otmod b/modules/game_playermount/playermount.otmod new file mode 100644 index 0000000..987e265 --- /dev/null +++ b/modules/game_playermount/playermount.otmod @@ -0,0 +1,9 @@ +Module + name: game_playermount + description: Manage player mounts + author: BeniS + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ playermount ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_playertrade/playertrade.lua b/modules/game_playertrade/playertrade.lua new file mode 100644 index 0000000..a879bfb --- /dev/null +++ b/modules/game_playertrade/playertrade.lua @@ -0,0 +1,77 @@ +tradeWindow = nil + +function init() + g_ui.importStyle('tradewindow') + + connect(g_game, { onOwnTrade = onGameOwnTrade, + onCounterTrade = onGameCounterTrade, + onCloseTrade = onGameCloseTrade, + onGameEnd = onGameCloseTrade }) +end + +function terminate() + disconnect(g_game, { onOwnTrade = onGameOwnTrade, + onCounterTrade = onGameCounterTrade, + onCloseTrade = onGameCloseTrade, + onGameEnd = onGameCloseTrade }) + + if tradeWindow then + tradeWindow:destroy() + end +end + +function createTrade() + tradeWindow = g_ui.createWidget('TradeWindow', modules.game_interface.getRightPanel()) + tradeWindow.onClose = function() + g_game.rejectTrade() + tradeWindow:hide() + end + tradeWindow:setup() +end + +function fillTrade(name, items, counter) + if not tradeWindow then + createTrade() + end + + local tradeItemWidget = tradeWindow:getChildById('tradeItem') + tradeItemWidget:setItemId(items[1]:getId()) + + local tradeContainer + local label + if counter then + tradeContainer = tradeWindow:recursiveGetChildById('counterTradeContainer') + label = tradeWindow:recursiveGetChildById('counterTradeLabel') + + tradeWindow:recursiveGetChildById('acceptButton'):enable() + else + tradeContainer = tradeWindow:recursiveGetChildById('ownTradeContainer') + label = tradeWindow:recursiveGetChildById('ownTradeLabel') + end + label:setText(name) + + for index,item in ipairs(items) do + local itemWidget = g_ui.createWidget('Item', tradeContainer) + itemWidget:setItem(item) + itemWidget:setVirtual(true) + itemWidget:setMargin(0) + itemWidget.onClick = function() + g_game.inspectTrade(counter, index-1) + end + end +end + +function onGameOwnTrade(name, items) + fillTrade(name, items, false) +end + +function onGameCounterTrade(name, items) + fillTrade(name, items, true) +end + +function onGameCloseTrade() + if tradeWindow then + tradeWindow:destroy() + tradeWindow = nil + end +end diff --git a/modules/game_playertrade/playertrade.otmod b/modules/game_playertrade/playertrade.otmod new file mode 100644 index 0000000..ff670d1 --- /dev/null +++ b/modules/game_playertrade/playertrade.otmod @@ -0,0 +1,9 @@ +Module + name: game_playertrade + description: Allow to trade items with players + author: edubart + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ playertrade ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_playertrade/tradewindow.otui b/modules/game_playertrade/tradewindow.otui new file mode 100644 index 0000000..15708d1 --- /dev/null +++ b/modules/game_playertrade/tradewindow.otui @@ -0,0 +1,107 @@ +TradeWindow < MiniWindow + !text: tr('Trade') + height: 150 + + UIItem + id: tradeItem + virtual: true + size: 16 16 + anchors.top: parent.top + anchors.left: parent.left + margin-top: 4 + margin-left: 4 + + MiniWindowContents + padding: 4 + + ScrollableFlatPanel + id: ownTradeContainer + anchors.top: parent.top + anchors.bottom: acceptButton.top + anchors.left: parent.left + anchors.right: ownTradeScrollBar.left + margin-top: 16 + margin-bottom: 4 + padding: 2 + layout: + type: grid + cell-size: 34 34 + flow: true + cell-spacing: 1 + vertical-scrollbar: ownTradeScrollBar + + VerticalScrollBar + id: ownTradeScrollBar + anchors.top: parent.top + anchors.bottom: acceptButton.top + anchors.right: parent.horizontalCenter + margin-top: 16 + margin-bottom: 4 + margin-right: 2 + step: 14 + pixels-scroll: true + $!on: + width: 0 + + ScrollableFlatPanel + id: counterTradeContainer + anchors.top: parent.top + anchors.bottom: acceptButton.top + anchors.left: parent.horizontalCenter + anchors.right: counterTradeScrollBar.left + margin-top: 16 + margin-bottom: 4 + margin-left: 2 + padding: 2 + layout: + type: grid + cell-size: 34 34 + flow: true + cell-spacing: 1 + vertical-scrollbar: counterTradeScrollBar + + VerticalScrollBar + id: counterTradeScrollBar + anchors.top: parent.top + anchors.bottom: acceptButton.top + anchors.right: parent.right + margin-top: 16 + margin-bottom: 4 + margin-right: 1 + step: 14 + pixels-scroll: true + $!on: + width: 0 + + Label + id: ownTradeLabel + anchors.bottom: ownTradeContainer.top + anchors.left: ownTradeContainer.left + anchors.right: parent.horizontalCenter + margin-bottom: 2 + + Label + id: counterTradeLabel + anchors.bottom: counterTradeContainer.top + anchors.left: parent.horizontalCenter + anchors.right: counterTradeScrollBar.right + margin-bottom: 2 + + Button + !text: tr('Accept') + id: acceptButton + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.horizontalCenter + margin-right: 2 + enabled: false + @onClick: g_game.acceptTrade(); self:disable() + + Button + !text: tr('Reject') + id: rejectButton + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: parent.horizontalCenter + margin-left: 2 + @onClick: g_game.rejectTrade() diff --git a/modules/game_questlog/questlinewindow.otui b/modules/game_questlog/questlinewindow.otui new file mode 100644 index 0000000..7d6a5cf --- /dev/null +++ b/modules/game_questlog/questlinewindow.otui @@ -0,0 +1,51 @@ +MissionLabel < Label + font: verdana-11px-monochrome + background-color: alpha + text-offset: 2 0 + focusable: true + + $focus: + background-color: #ffffff22 + color: #ffffff + +QuestLineWindow < MainWindow + id: questLineWindow + !text: tr('Quest Log') + size: 500 400 + @onEscape: self:destroy() + + TextList + id: missionList + anchors.top: parent.top + anchors.left: parent.left + anchors.right: missionListScrollBar.left + height: 100 + padding: 1 + focusable: false + vertical-scrollbar: missionListScrollBar + + VerticalScrollBar + id: missionListScrollBar + anchors.top: parent.top + anchors.right: parent.right + height: 100 + step: 14 + pixels-scroll: true + + FlatLabel + id: missionDescription + anchors.top: missionList.bottom + anchors.left: parent.left + anchors.right: missionListScrollBar.right + anchors.bottom: closeButton.top + margin-bottom: 10 + margin-top: 10 + text-wrap: true + + Button + id: closeButton + anchors.bottom: parent.bottom + anchors.right: parent.right + !text: tr('Close') + width: 90 + @onClick: self:getParent():destroy() diff --git a/modules/game_questlog/questlog.lua b/modules/game_questlog/questlog.lua new file mode 100644 index 0000000..a7b1c8b --- /dev/null +++ b/modules/game_questlog/questlog.lua @@ -0,0 +1,88 @@ +questLogButton = nil +questLineWindow = nil + +function init() + g_ui.importStyle('questlogwindow') + g_ui.importStyle('questlinewindow') + + questLogButton = modules.client_topmenu.addLeftGameButton('questLogButton', tr('Quest Log'), '/images/topbuttons/questlog', function() g_game.requestQuestLog() end) + + connect(g_game, { onQuestLog = onGameQuestLog, + onQuestLine = onGameQuestLine, + onGameEnd = destroyWindows}) +end + +function terminate() + disconnect(g_game, { onQuestLog = onGameQuestLog, + onQuestLine = onGameQuestLine, + onGameEnd = destroyWindows}) + + destroyWindows() + questLogButton:destroy() +end + +function destroyWindows() + if questLogWindow then + questLogWindow:destroy() + end + + if questLineWindow then + questLineWindow:destroy() + end +end + +function onGameQuestLog(quests) + destroyWindows() + + questLogWindow = g_ui.createWidget('QuestLogWindow', rootWidget) + local questList = questLogWindow:getChildById('questList') + + for i,questEntry in pairs(quests) do + local id, name, completed = unpack(questEntry) + + local questLabel = g_ui.createWidget('QuestLabel', questList) + questLabel:setOn(completed) + questLabel:setText(name) + questLabel.onDoubleClick = function() + questLogWindow:hide() + g_game.requestQuestLine(id) + end + end + + questLogWindow.onDestroy = function() + questLogWindow = nil + end + + questList:focusChild(questList:getFirstChild()) +end + +function onGameQuestLine(questId, questMissions) + if questLogWindow then questLogWindow:hide() end + if questLineWindow then questLineWindow:destroy() end + + questLineWindow = g_ui.createWidget('QuestLineWindow', rootWidget) + local missionList = questLineWindow:getChildById('missionList') + local missionDescription = questLineWindow:getChildById('missionDescription') + + connect(missionList, { onChildFocusChange = function(self, focusedChild) + if focusedChild == nil then return end + missionDescription:setText(focusedChild.description) + end }) + + for i,questMission in pairs(questMissions) do + local name, description = unpack(questMission) + + local missionLabel = g_ui.createWidget('MissionLabel') + missionLabel:setText(name) + missionLabel.description = description + missionList:addChild(missionLabel) + end + + questLineWindow.onDestroy = function() + if questLogWindow then questLogWindow:show() end + questLineWindow = nil + end + + missionList:focusChild(missionList:getFirstChild()) +end + diff --git a/modules/game_questlog/questlog.otmod b/modules/game_questlog/questlog.otmod new file mode 100644 index 0000000..7bafbeb --- /dev/null +++ b/modules/game_questlog/questlog.otmod @@ -0,0 +1,9 @@ +Module + name: game_questlog + description: View game quests status + author: edubart + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ questlog ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_questlog/questlogwindow.otui b/modules/game_questlog/questlogwindow.otui new file mode 100644 index 0000000..cb5df91 --- /dev/null +++ b/modules/game_questlog/questlogwindow.otui @@ -0,0 +1,52 @@ +QuestLabel < Label + font: verdana-11px-monochrome + text-offset: 2 0 + focusable: true + color: #aaaaaa + background-color: alpha + + $on: + color: #00aa00 + $!on: + color: #aaaaaa + + $focus: + background-color: #444444 + + $on focus: + color: #00ff00 + $!on focus: + color: #ffffff + +QuestLogWindow < MainWindow + id: questLogWindow + !text: tr('Quest Log') + size: 500 400 + @onEscape: self:destroy() + + TextList + id: questList + anchors.top: parent.top + anchors.bottom: closeButton.top + anchors.left: parent.left + anchors.right: questListScrollBar.left + margin-bottom: 10 + focusable: false + vertical-scrollbar: questListScrollBar + + VerticalScrollBar + id: questListScrollBar + anchors.top: parent.top + anchors.bottom: closeButton.top + anchors.right: parent.right + margin-bottom: 10 + step: 14 + pixels-scroll: true + + Button + id: closeButton + anchors.bottom: parent.bottom + anchors.right: parent.right + !text: tr('Close') + width: 90 + @onClick: self:getParent():destroy() diff --git a/modules/game_ruleviolation/ruleviolation.lua b/modules/game_ruleviolation/ruleviolation.lua new file mode 100644 index 0000000..156fdde --- /dev/null +++ b/modules/game_ruleviolation/ruleviolation.lua @@ -0,0 +1,151 @@ +rvreasons = {} +rvreasons[1] = tr("1a) Offensive Name") +rvreasons[2] = tr("1b) Invalid Name Format") +rvreasons[3] = tr("1c) Unsuitable Name") +rvreasons[4] = tr("1d) Name Inciting Rule Violation") +rvreasons[5] = tr("2a) Offensive Statement") +rvreasons[6] = tr("2b) Spamming") +rvreasons[7] = tr("2c) Illegal Advertising") +rvreasons[8] = tr("2d) Off-Topic Public Statement") +rvreasons[9] = tr("2e) Non-English Public Statement") +rvreasons[10] = tr("2f) Inciting Rule Violation") +rvreasons[11] = tr("3a) Bug Abuse") +rvreasons[12] = tr("3b) Game Weakness Abuse") +rvreasons[13] = tr("3c) Using Unofficial Software to Play") +rvreasons[14] = tr("3d) Hacking") +rvreasons[15] = tr("3e) Multi-Clienting") +rvreasons[16] = tr("3f) Account Trading or Sharing") +rvreasons[17] = tr("4a) Threatening Gamemaster") +rvreasons[18] = tr("4b) Pretending to Have Influence on Rule Enforcement") +rvreasons[19] = tr("4c) False Report to Gamemaster") +rvreasons[20] = tr("Destructive Behaviour") +rvreasons[21] = tr("Excessive Unjustified Player Killing") + +rvactions = {} +rvactions[0] = tr("Notation") +rvactions[1] = tr("Name Report") +rvactions[2] = tr("Banishment") +rvactions[3] = tr("Name Report + Banishment") +rvactions[4] = tr("Banishment + Final Warning") +rvactions[5] = tr("Name Report + Banishment + Final Warning") +rvactions[6] = tr("Statement Report") + +ruleViolationWindow = nil +reasonsTextList = nil +actionsTextList = nil + +function init() + connect(g_game, { onGMActions = loadReasons }) + + ruleViolationWindow = g_ui.displayUI('ruleviolation') + ruleViolationWindow:setVisible(false) + + reasonsTextList = ruleViolationWindow:getChildById('reasonList') + actionsTextList = ruleViolationWindow:getChildById('actionList') + + g_keyboard.bindKeyDown('Ctrl+Y', function() show() end) + + if g_game.isOnline() then + loadReasons() + end +end + +function terminate() + disconnect(g_game, { onGMActions = loadReasons }) + g_keyboard.unbindKeyDown('Ctrl+Y') + + ruleViolationWindow:destroy() +end + +function hasWindowAccess() + return reasonsTextList:getChildCount() > 0 +end + +function loadReasons() + reasonsTextList:destroyChildren() + actionsTextList:destroyChildren() + + local actions = g_game.getGMActions() + for reason, actionFlags in pairs(actions) do + local label = g_ui.createWidget('RVListLabel', reasonsTextList) + label.onFocusChange = onSelectReason + label:setText(rvreasons[reason]) + label.reasonId = reason + label.actionFlags = actionFlags + end + + if not hasWindowAccess() and ruleViolationWindow:isVisible() then hide() end +end + +function show(target, statement) + if g_game.isOnline() and hasWindowAccess() then + if target then + ruleViolationWindow:getChildById('nameText'):setText(target) + end + + if statement then + ruleViolationWindow:getChildById('statementText'):setText(statement) + end + + ruleViolationWindow:show() + ruleViolationWindow:raise() + ruleViolationWindow:focus() + ruleViolationWindow:getChildById('commentText'):focus() + end +end + +function hide() + ruleViolationWindow:hide() + clearForm() +end + +function onSelectReason(reasonLabel, focused) + if reasonLabel.actionFlags and focused then + actionsTextList:destroyChildren() + for actionBaseFlag = 0, #rvactions do + local actionFlagString = rvactions[actionBaseFlag] + if bit32.band(reasonLabel.actionFlags, math.pow(2, actionBaseFlag)) > 0 then + local label = g_ui.createWidget('RVListLabel', actionsTextList) + label:setText(actionFlagString) + label.actionId = actionBaseFlag + end + end + end +end + +function report() + local reasonLabel = reasonsTextList:getFocusedChild() + if not reasonLabel then + displayErrorBox(tr("Error"), tr("You must select a reason.")) + return + end + + local actionLabel = actionsTextList:getFocusedChild() + if not actionLabel then + displayErrorBox(tr("Error"), tr("You must select an action.")) + return + end + + local target = ruleViolationWindow:getChildById('nameText'):getText() + local reason = reasonLabel.reasonId + local action = actionLabel.actionId + local comment = ruleViolationWindow:getChildById('commentText'):getText() + local statement = ruleViolationWindow:getChildById('statementText'):getText() + local statementId = 0 -- TODO: message unique id ? + local ipBanishment = ruleViolationWindow:getChildById('ipBanCheckBox'):isChecked() + if action == 6 and statement == "" then + displayErrorBox(tr("Error"), tr("No statement has been selected.")) + elseif comment == "" then + displayErrorBox(tr("Error"), tr("You must enter a comment.")) + else + g_game.reportRuleViolation(target, reason, action, comment, statement, statementId, ipBanishment) + hide() + end +end + +function clearForm() + ruleViolationWindow:getChildById('nameText'):clearText() + ruleViolationWindow:getChildById('commentText'):clearText() + ruleViolationWindow:getChildById('statementText'):clearText() + ruleViolationWindow:getChildById('ipBanCheckBox'):setChecked(false) +end diff --git a/modules/game_ruleviolation/ruleviolation.otmod b/modules/game_ruleviolation/ruleviolation.otmod new file mode 100644 index 0000000..4b5f4fd --- /dev/null +++ b/modules/game_ruleviolation/ruleviolation.otmod @@ -0,0 +1,9 @@ +Module + name: game_ruleviolation + description: Rule violation interface (Ctrl+Y) + author: andrefaramir + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ ruleviolation ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_ruleviolation/ruleviolation.otui b/modules/game_ruleviolation/ruleviolation.otui new file mode 100644 index 0000000..570c118 --- /dev/null +++ b/modules/game_ruleviolation/ruleviolation.otui @@ -0,0 +1,120 @@ +RVListLabel < Label + background-color: alpha + text-offset: 2 0 + focusable: true + + $focus: + background-color: #ffffff22 + color: #ffffff + +RVLabel < Label + anchors.left: parent.left + anchors.right: parent.right + + $first: + anchors.top: parent.top + + $!first: + margin-top: 10 + anchors.top: prev.bottom + +RVTextEdit < TextEdit + margin-top: 2 + anchors.left: parent.left + anchors.right: parent.right + + $first: + anchors.top: parent.top + + $!first: + anchors.top: prev.bottom + +MainWindow + id: ruleViolationWindow + size: 400 445 + text: Rule Violation + @onEscape: hide() + + RVLabel + !text: tr('Name') .. ':' + + RVTextEdit + id: nameText + + RVLabel + !text: tr('Statement') .. ':' + + RVTextEdit + id: statementText + enabled: false + + RVLabel + !text: tr('Reason') .. ':' + + TextList + id: reasonList + height: 100 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 2 + focusable: false + vertical-scrollbar: reasonListScrollBar + + VerticalScrollBar + id: reasonListScrollBar + anchors.top: reasonList.top + anchors.bottom: reasonList.bottom + anchors.right: reasonList.right + step: 14 + pixels-scroll: true + + RVLabel + !text: tr('Action') .. ':' + + TextList + id: actionList + height: 60 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 2 + focusable: false + vertical-scrollbar: actionListScrollBar + + VerticalScrollBar + id: actionListScrollBar + anchors.top: actionList.top + anchors.bottom: actionList.bottom + anchors.right: actionList.right + step: 14 + pixels-scroll: true + + CheckBox + id: ipBanCheckBox + !text: tr('IP Address Banishment') + margin-top: 10 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + + RVLabel + !text: tr('Comment') .. ':' + + RVTextEdit + id: commentText + + Button + !text: tr('Cancel') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: hide() + + Button + !text: tr('Ok') + width: 64 + margin-right: 5 + anchors.right: prev.left + anchors.bottom: parent.bottom + @onClick: report() \ No newline at end of file diff --git a/modules/game_shop/shop.lua b/modules/game_shop/shop.lua new file mode 100644 index 0000000..3c1cc1e --- /dev/null +++ b/modules/game_shop/shop.lua @@ -0,0 +1,380 @@ +-- private variables +local SHOP_EXTENTED_OPCODE = 201 + +shop = nil +local shopButton = nil +local msgWindow = nil +local browsingHistory = false + +local CATEGORIES = {} +local HISTORY = {} +local STATUS = {} +local AD = {} + +local selectedOffer = {} + +local function sendAction(action, data) + local protocolGame = g_game.getProtocolGame() + if data == nil then + data = {} + end + if protocolGame then + protocolGame:sendExtendedOpcode(SHOP_EXTENTED_OPCODE, json.encode({action = action, data = data})) + end +end + +-- public functions +function init() + connect(g_game, { onGameStart = check, onGameEnd = hide }) + + ProtocolGame.registerExtendedOpcode(SHOP_EXTENTED_OPCODE, onExtendedOpcode) + + if g_game.isOnline() then + check() + end +end + +function terminate() + disconnect(g_game, { onGameEnd = hide }) + + ProtocolGame.unregisterExtendedOpcode(SHOP_EXTENTED_OPCODE, onExtendedOpcode) + + if shopButton then + shopButton:destroy() + shopButton = nil + end + if shop then + disconnect(shop.categories, { onChildFocusChange = changeCategory }) + shop:destroy() + shop = nil + end + if msgWindow then + msgWindow:destroy() + end +end + +function check() + if not g_game.getFeature(GameExtendedOpcode) then + return + end + sendAction("init") +end + +function hide() + if not shop then + return + end + shop:hide() +end + +function show() + if not shop or not shopButton then + return + end + shop:show() + shop:raise() + shop:focus() +end + +function toggle() + if not shop then + return + end + if shop:isVisible() then + return hide() + end + show() + check() +end + +function onExtendedOpcode(protocol, code, buffer) + if not shop then + shop = g_ui.displayUI('shop') + shop:hide() + shopButton = modules.client_topmenu.addRightGameToggleButton('shopButton', tr('Shop'), '/images/topbuttons/shop', toggle) + + connect(shop.categories, { onChildFocusChange = changeCategory }) + end + + local json_status, json_data = pcall(function() return json.decode(buffer) end) + if not json_status then + g_logger.error("SHOP json error: " .. json_data) + return false + end + + local action = json_data['action'] + local data = json_data['data'] + local status = json_data['status'] + if not action or not data then + return false + end + + if action == 'categories' then + processCategories(data) + elseif action == 'history' then + processHistory(data) + elseif action == 'message' then + processMessage(data) + end + + if status then + processStatus(status) + end +end + +function clearOffers() + while shop.offers:getChildCount() > 0 do + local child = shop.offers:getLastChild() + shop.offers:destroyChildren(child) + end +end + +function clearCategories() + CATEGORIES = {} + clearOffers() + while shop.categories:getChildCount() > 0 do + local child = shop.categories:getLastChild() + shop.categories:destroyChildren(child) + end +end + +function clearHistory() + HISTORY = {} + if browsingHistory then + clearOffers() + end +end + +function processCategories(data) + if table.equal(CATEGORIES,data) then + return + end + clearCategories() + CATEGORIES = data + for i, category in ipairs(data) do + addCategory(category) + end + if not browsingHistory then + local firstCategory = shop.categories:getChildByIndex(1) + if firstCategory then + firstCategory:focus() + end + end +end + +function processHistory(data) + if table.equal(HISTORY,data) then + return + end + HISTORY = data + if browsingHistory then + showHistory(true) + end +end + +function processMessage(data) + if msgWindow then + msgWindow:destroy() + end + + local title = tr(data["title"]) + local msg = data["msg"] + msgWindow = displayInfoBox(title, msg) + msgWindow:show() + msgWindow:raise() + msgWindow:focus() + msgWindow:raise() +end + +function processStatus(data) + if table.equal(STATUS,data) then + return + end + STATUS = data + + if data['ad'] then + processAd(data['ad']) + end + if data['points'] then + shop.infoPanel.points:setText(tr("Points:") .. " " .. data['points']) + end + if data['buyUrl'] and data['buyUrl']:sub(1, 4):lower() == "http" then + shop.infoPanel.buy:show() + shop.infoPanel.buy.onMouseRelease = function() + scheduleEvent(function() g_platform.openUrl(data['buyUrl']) end, 50) + end + else + shop.infoPanel.buy:hide() + end +end + +function processAd(data) + if table.equal(AD,data) then + return + end + AD = data + + if data['image'] and data['image']:sub(1, 4):lower() == "http" then + HTTP.downloadImage(data['image'], function(path, err) + if err then g_logger.warning("HTTP error: " .. err) return end + shop.adPanel:setHeight(shop.infoPanel:getHeight()) + shop.adPanel.ad:setText("") + shop.adPanel.ad:setImageSource(path) + shop.adPanel.ad:setImageFixedRatio(true) + shop.adPanel.ad:setImageAutoResize(true) + shop.adPanel.ad:setHeight(shop.infoPanel:getHeight()) + end) + elseif data['text'] and data['text']:len() > 0 then + shop.adPanel:setHeight(shop.infoPanel:getHeight()) + shop.adPanel.ad:setText(data['text']) + shop.adPanel.ad:setHeight(shop.infoPanel:getHeight()) + else + shop.adPanel:setHeight(0) + end + if data['url'] and data['url']:sub(1, 4):lower() == "http" then + shop.adPanel.ad.onMouseRelease = function() + scheduleEvent(function() g_platform.openUrl(data['url']) end, 50) + end + else + shop.adPanel.ad.onMouseRelease = nil + end +end + +function addCategory(data) + local category + if data["type"] == "item" then + category = g_ui.createWidget('ShopCategoryItem', shop.categories) + category.item:setItemId(data["item"]) + category.item:setItemCount(data["count"]) + category.item:setShowCount(false) + elseif data["type"] == "outfit" then + category = g_ui.createWidget('ShopCategoryCreature', shop.categories) + category.creature:setOutfit(data["outfit"]) + if data["outfit"]["rotating"] then + category.creature:setAutoRotating(true) + end + elseif data["type"] == "image" then + category = g_ui.createWidget('ShopCategoryImage', shop.categories) + if data["image"]:sub(1, 4):lower() == "http" then + HTTP.downloadImage(data['image'], function(path, err) + if err then g_logger.warning("HTTP error: " .. err) return end + category.image:setImageSource(path) + end) + else + category.image:setImageSource(data["image"]) + end + else + g_logger.error("Invalid shop category type: " .. tostring(data["type"])) + return + end + category:setId("category_" .. shop.categories:getChildCount()) + category.name:setText(data["name"]) +end + +function showHistory(force) + if browsingHistory and not force then + return + end + sendAction("history") + browsingHistory = true + clearOffers() + shop.categories:focusChild(nil) + for i, transaction in ipairs(HISTORY) do + addOffer(0, transaction) + end +end + +function addOffer(category, data) + local offer + if data["type"] == "item" then + offer = g_ui.createWidget('ShopOfferItem', shop.offers) + offer.item:setItemId(data["item"]) + offer.item:setItemCount(data["count"]) + offer.item:setShowCount(false) + elseif data["type"] == "outfit" then + offer = g_ui.createWidget('ShopOfferCreature', shop.offers) + offer.creature:setOutfit(data["outfit"]) + if data["outfit"]["rotating"] then + offer.creature:setAutoRotating(true) + end + elseif data["type"] == "image" then + offer = g_ui.createWidget('ShopOfferImage', shop.offers) + if data["image"]:sub(1, 4):lower() == "http" then + HTTP.downloadImage(data['image'], function(path, err) + if err then g_logger.warning("HTTP error: " .. err) return end + offer.image:setImageSource(path) + end) + elseif data["image"] and data["image"]:len() > 1 then + offer.image:setImageSource(data["image"]) + end + else + g_logger.error("Invalid shop offer type: " .. tostring(data["type"])) + return + end + offer:setId("offer_" .. category .. "_" .. shop.offers:getChildCount()) + offer.title:setText(data["title"] .. " (" .. data["cost"] .. " points)") + offer.description:setText(data["description"]) + if category ~= 0 then + offer.onDoubleClick = buyOffer + end +end + + +function changeCategory(widget, newCategory) + if not newCategory then + return + end + browsingHistory = false + local id = tonumber(newCategory:getId():split("_")[2]) + clearOffers() + for i, offer in ipairs(CATEGORIES[id]["offers"]) do + addOffer(id, offer) + end +end + +function buyOffer(widget) + if not widget then + return + end + local split = widget:getId():split("_") + if #split ~= 3 then + return + end + local category = tonumber(split[2]) + local offer = tonumber(split[3]) + local item = CATEGORIES[category]["offers"][offer] + if not item then + return + end + + selectedOffer = {category=category, offer=offer, title=item.title, cost=item.cost} + + scheduleEvent(function() + if msgWindow then + msgWindow:destroy() + end + + local title = tr("Buying from shop") + local msg = "Do you want to buy " .. item.title .. " for " .. item.cost .. " premium points?" + msgWindow = displayGeneralBox(title, msg, { + { text=tr('Yes'), callback=buyConfirmed }, + { text=tr('No'), callback=buyCanceled }, + anchor=AnchorHorizontalCenter}, buyConfirmed, buyCanceled) + msgWindow:show() + msgWindow:raise() + msgWindow:focus() + msgWindow:raise() + end, 50) +end + +function buyConfirmed() + msgWindow:destroy() + msgWindow = nil + sendAction("buy", selectedOffer) +end + +function buyCanceled() + msgWindow:destroy() + msgWindow = nil + selectedOffer = {} +end \ No newline at end of file diff --git a/modules/game_shop/shop.otmod b/modules/game_shop/shop.otmod new file mode 100644 index 0000000..eec111a --- /dev/null +++ b/modules/game_shop/shop.otmod @@ -0,0 +1,10 @@ +Module + name: game_shop + description: Game shop + author: otclient.ovh + website: http://otclient.ovh + sandboxed: true + scripts: [ shop ] + dependencies: [ client_topmenu ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_shop/shop.otui b/modules/game_shop/shop.otui new file mode 100644 index 0000000..0bea9a9 --- /dev/null +++ b/modules/game_shop/shop.otui @@ -0,0 +1,226 @@ +ShopCategory < Panel + height: 36 + focusable: true + background: alpha + + $focus: + background: #99999999 + + Label + id: name + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + margin-left: 40 + text-align: left + text: UAHSbjaS ASDJHASD ASKJD + color: white + font: verdana-11px-rounded + +ShopCategoryItem < ShopCategory + UIItem + id: item + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + margin-top: 2 + margin-bottom: 2 + margin-left: 2 + virtual: true + size: 32 32 + +ShopCategoryCreature < ShopCategory + UICreature + id: creature + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + margin-top: 2 + margin-bottom: 2 + margin-left: 2 + size: 32 32 + +ShopCategoryImage < ShopCategory + Label + id: image + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + margin-top: 2 + margin-bottom: 2 + margin-left: 2 + size: 32 32 + + + +ShopOffer < Panel + height: 56 + background: alpha + + $focus: + background: #99999999 + + Label + id: title + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + margin-top: 4 + margin-left: 55 + text-align: topleft + text: UAHSbjaS ASDJHASD ASKJD + color: white + font: verdana-11px-rounded + + Label + id: description + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + margin-left: 55 + text-align: topleft + text: UAHSbjaS ASDJHASD ASKJD ssssUAHSbjaS ASDJHASD ASKJD ssssUAHSbjaS ASDJHASD ASKJD ssss + text-auto-resize: true + text-wrap: true + color: white + font: verdana-11px-rounded + +ShopOfferItem < ShopOffer + UIItem + id: item + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + margin-top: 4 + margin-bottom: 4 + margin-left: 2 + virtual: true + size: 48 48 + +ShopOfferCreature < ShopOffer + UICreature + id: creature + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + margin-top: 4 + margin-bottom: 4 + margin-left: 2 + size: 48 48 + +ShopOfferImage < ShopOffer + Label + id: image + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + margin-top: 4 + margin-bottom: 4 + margin-left: 2 + size: 48 48 + +MainWindow + id: shopWindow + !text: tr('Shop') + size: 750 500 + @onEscape: modules.game_shop.hide() + + Panel + id: infoPanel + anchors.top: parent.top + anchors.left: parent.left + width: 230 + height: 60 + + Label + id: points + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + margin-top: 10 + text: - + text-auto-resize: true + + Button + id: buy + anchors.horizontalCenter: parent.horizontalCenter + width: 150 + anchors.top: prev.bottom + margin-top: 10 + visible: false + !text: tr("Buy points") + + Panel + id: adPanel + anchors.top: parent.top + anchors.left: infoPanel.right + anchors.right: parent.right + margin-left: 10 + height: 0 + + Label + id: ad + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.bottom: parent.bottom + text-auto-resize: true + text-wrap: true + text-align: center + font: sans-bold-16px + + TextList + id: categories + vertical-scrollbar: categoriesScrollBar + anchors.top: infoPanel.bottom + anchors.left: infoPanel.left + anchors.right: infoPanel.right + anchors.bottom: transactionHistory.top + margin-top: 10 + margin-bottom: 10 + padding: 1 + focusable: false + + VerticalScrollBar + id: categoriesScrollBar + anchors.top: categories.top + anchors.bottom: categories.bottom + anchors.right: categories.right + step: 50 + pixels-scroll: true + + TextList + id: offers + vertical-scrollbar: offersScrollBar + anchors.top: adPanel.bottom + anchors.left: adPanel.left + anchors.right: adPanel.right + anchors.bottom: transactionHistory.top + margin-top: 10 + margin-bottom: 10 + padding: 1 + focusable: false + + VerticalScrollBar + id: offersScrollBar + anchors.top: offers.top + anchors.bottom: offers.bottom + anchors.right: offers.right + step: 50 + pixels-scroll: true + + Button + id: transactionHistory + !text: tr('Transaction history') + width: 128 + anchors.left: parent.left + anchors.bottom: parent.bottom + @onClick: modules.game_shop.showHistory() + + Button + id: buttonCancel + !text: tr('Close') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: modules.game_shop.hide() diff --git a/modules/game_skills/skills.lua b/modules/game_skills/skills.lua new file mode 100644 index 0000000..be0e076 --- /dev/null +++ b/modules/game_skills/skills.lua @@ -0,0 +1,426 @@ +skillsWindow = nil +skillsButton = nil + +function init() + connect(LocalPlayer, { + onExperienceChange = onExperienceChange, + onLevelChange = onLevelChange, + onHealthChange = onHealthChange, + onManaChange = onManaChange, + onSoulChange = onSoulChange, + onFreeCapacityChange = onFreeCapacityChange, + onTotalCapacityChange = onTotalCapacityChange, + onStaminaChange = onStaminaChange, + onOfflineTrainingChange = onOfflineTrainingChange, + onRegenerationChange = onRegenerationChange, + onSpeedChange = onSpeedChange, + onBaseSpeedChange = onBaseSpeedChange, + onMagicLevelChange = onMagicLevelChange, + onBaseMagicLevelChange = onBaseMagicLevelChange, + onSkillChange = onSkillChange, + onBaseSkillChange = onBaseSkillChange + }) + connect(g_game, { + onGameStart = refresh, + onGameEnd = offline + }) + + skillsButton = modules.client_topmenu.addRightGameToggleButton('skillsButton', tr('Skills'), '/images/topbuttons/skills', toggle) + skillsButton:setOn(true) + skillsWindow = g_ui.loadUI('skills', modules.game_interface.getRightPanel()) + + refresh() + skillsWindow:setup() +end + +function terminate() + disconnect(LocalPlayer, { + onExperienceChange = onExperienceChange, + onLevelChange = onLevelChange, + onHealthChange = onHealthChange, + onManaChange = onManaChange, + onSoulChange = onSoulChange, + onFreeCapacityChange = onFreeCapacityChange, + onTotalCapacityChange = onTotalCapacityChange, + onStaminaChange = onStaminaChange, + onOfflineTrainingChange = onOfflineTrainingChange, + onRegenerationChange = onRegenerationChange, + onSpeedChange = onSpeedChange, + onBaseSpeedChange = onBaseSpeedChange, + onMagicLevelChange = onMagicLevelChange, + onBaseMagicLevelChange = onBaseMagicLevelChange, + onSkillChange = onSkillChange, + onBaseSkillChange = onBaseSkillChange + }) + disconnect(g_game, { + onGameStart = refresh, + onGameEnd = offline + }) + + skillsWindow:destroy() + skillsButton:destroy() +end + +function expForLevel(level) + return math.floor((50*level*level*level)/3 - 100*level*level + (850*level)/3 - 200) +end + +function expToAdvance(currentLevel, currentExp) + return expForLevel(currentLevel+1) - currentExp +end + +function resetSkillColor(id) + local skill = skillsWindow:recursiveGetChildById(id) + local widget = skill:getChildById('value') + widget:setColor('#bbbbbb') +end + +function toggleSkill(id, state) + local skill = skillsWindow:recursiveGetChildById(id) + skill:setVisible(state) +end + +function setSkillBase(id, value, baseValue) + if baseValue <= 0 or value < 0 then + return + end + local skill = skillsWindow:recursiveGetChildById(id) + local widget = skill:getChildById('value') + + if value > baseValue then + widget:setColor('#008b00') -- green + skill:setTooltip(baseValue .. ' +' .. (value - baseValue)) + elseif value < baseValue then + widget:setColor('#b22222') -- red + skill:setTooltip(baseValue .. ' ' .. (value - baseValue)) + else + widget:setColor('#bbbbbb') -- default + skill:removeTooltip() + end +end + +function setSkillValue(id, value) + local skill = skillsWindow:recursiveGetChildById(id) + local widget = skill:getChildById('value') + widget:setText(value) +end + +function setSkillColor(id, value) + local skill = skillsWindow:recursiveGetChildById(id) + local widget = skill:getChildById('value') + widget:setColor(value) +end + +function setSkillTooltip(id, value) + local skill = skillsWindow:recursiveGetChildById(id) + local widget = skill:getChildById('value') + widget:setTooltip(value) +end + +function setSkillPercent(id, percent, tooltip, color) + local skill = skillsWindow:recursiveGetChildById(id) + local widget = skill:getChildById('percent') + if widget then + widget:setPercent(math.floor(percent)) + + if tooltip then + widget:setTooltip(tooltip) + end + + if color then + widget:setBackgroundColor(color) + end + end +end + +function checkAlert(id, value, maxValue, threshold, greaterThan) + if greaterThan == nil then greaterThan = false end + local alert = false + + -- maxValue can be set to false to check value and threshold + -- used for regeneration checking + if type(maxValue) == 'boolean' then + if maxValue then + return + end + + if greaterThan then + if value > threshold then + alert = true + end + else + if value < threshold then + alert = true + end + end + elseif type(maxValue) == 'number' then + if maxValue < 0 then + return + end + + local percent = math.floor((value / maxValue) * 100) + if greaterThan then + if percent > threshold then + alert = true + end + else + if percent < threshold then + alert = true + end + end + end + + if alert then + setSkillColor(id, '#b22222') -- red + else + resetSkillColor(id) + end +end + +function update() + local offlineTraining = skillsWindow:recursiveGetChildById('offlineTraining') + if not g_game.getFeature(GameOfflineTrainingTime) then + offlineTraining:hide() + else + offlineTraining:show() + end + + local regenerationTime = skillsWindow:recursiveGetChildById('regenerationTime') + if not g_game.getFeature(GamePlayerRegenerationTime) then + regenerationTime:hide() + else + regenerationTime:show() + end +end + +function refresh() + local player = g_game.getLocalPlayer() + if not player then return end + + if expSpeedEvent then expSpeedEvent:cancel() end + expSpeedEvent = cycleEvent(checkExpSpeed, 30*1000) + + onExperienceChange(player, player:getExperience()) + onLevelChange(player, player:getLevel(), player:getLevelPercent()) + onHealthChange(player, player:getHealth(), player:getMaxHealth()) + onManaChange(player, player:getMana(), player:getMaxMana()) + onSoulChange(player, player:getSoul()) + onFreeCapacityChange(player, player:getFreeCapacity()) + onStaminaChange(player, player:getStamina()) + onMagicLevelChange(player, player:getMagicLevel(), player:getMagicLevelPercent()) + onOfflineTrainingChange(player, player:getOfflineTrainingTime()) + onRegenerationChange(player, player:getRegenerationTime()) + onSpeedChange(player, player:getSpeed()) + + local hasAdditionalSkills = g_game.getFeature(GameAdditionalSkills) + for i = Skill.Fist, Skill.ManaLeechAmount do + onSkillChange(player, i, player:getSkillLevel(i), player:getSkillLevelPercent(i)) + onBaseSkillChange(player, i, player:getSkillBaseLevel(i)) + + if i > Skill.Fishing then + toggleSkill('skillId'..i, hasAdditionalSkills) + end + end + + update() + + local contentsPanel = skillsWindow:getChildById('contentsPanel') + skillsWindow:setContentMinimumHeight(44) + if hasAdditionalSkills then + skillsWindow:setContentMaximumHeight(480) + else + skillsWindow:setContentMaximumHeight(390) + end +end + +function offline() + if expSpeedEvent then expSpeedEvent:cancel() expSpeedEvent = nil end +end + +function toggle() + if skillsButton:isOn() then + skillsWindow:close() + skillsButton:setOn(false) + else + skillsWindow:open() + skillsButton:setOn(true) + end +end + +function checkExpSpeed() + local player = g_game.getLocalPlayer() + if not player then return end + + local currentExp = player:getExperience() + local currentTime = g_clock.seconds() + if player.lastExps ~= nil then + player.expSpeed = (currentExp - player.lastExps[1][1])/(currentTime - player.lastExps[1][2]) + onLevelChange(player, player:getLevel(), player:getLevelPercent()) + else + player.lastExps = {} + end + table.insert(player.lastExps, {currentExp, currentTime}) + if #player.lastExps > 30 then + table.remove(player.lastExps, 1) + end +end + +function onMiniWindowClose() + skillsButton:setOn(false) +end + +function onSkillButtonClick(button) + local percentBar = button:getChildById('percent') + if percentBar then + percentBar:setVisible(not percentBar:isVisible()) + if percentBar:isVisible() then + button:setHeight(21) + else + button:setHeight(21 - 6) + end + end +end + +function onExperienceChange(localPlayer, value) + setSkillValue('experience', value) +end + +function onLevelChange(localPlayer, value, percent) + setSkillValue('level', value) + local text = tr('You have %s percent to go', 100 - percent) .. '\n' .. + tr('%s of experience left', expToAdvance(localPlayer:getLevel(), localPlayer:getExperience())) + + if localPlayer.expSpeed ~= nil then + local expPerHour = math.floor(localPlayer.expSpeed * 3600) + if expPerHour > 0 then + local nextLevelExp = expForLevel(localPlayer:getLevel()+1) + local hoursLeft = (nextLevelExp - localPlayer:getExperience()) / expPerHour + local minutesLeft = math.floor((hoursLeft - math.floor(hoursLeft))*60) + hoursLeft = math.floor(hoursLeft) + text = text .. '\n' .. tr('%d of experience per hour', expPerHour) + text = text .. '\n' .. tr('Next level in %d hours and %d minutes', hoursLeft, minutesLeft) + end + end + + setSkillPercent('level', percent, text) +end + +function onHealthChange(localPlayer, health, maxHealth) + setSkillValue('health', health) + checkAlert('health', health, maxHealth, 30) +end + +function onManaChange(localPlayer, mana, maxMana) + setSkillValue('mana', mana) + checkAlert('mana', mana, maxMana, 30) +end + +function onSoulChange(localPlayer, soul) + setSkillValue('soul', soul) +end + +function onFreeCapacityChange(localPlayer, freeCapacity) + setSkillValue('capacity', freeCapacity) + checkAlert('capacity', freeCapacity, localPlayer:getTotalCapacity(), 20) +end + +function onTotalCapacityChange(localPlayer, totalCapacity) + checkAlert('capacity', localPlayer:getFreeCapacity(), totalCapacity, 20) +end + +function onStaminaChange(localPlayer, stamina) + local hours = math.floor(stamina / 60) + local minutes = stamina % 60 + if minutes < 10 then + minutes = '0' .. minutes + end + local percent = math.floor(100 * stamina / (42 * 60)) -- max is 42 hours --TODO not in all client versions + + setSkillValue('stamina', hours .. ":" .. minutes) + + --TODO not all client versions have premium time + if stamina > 2400 and g_game.getClientVersion() >= 1038 and localPlayer:isPremium() then + local text = tr("You have %s hours and %s minutes left", hours, minutes) .. '\n' .. + tr("Now you will gain 50%% more experience") + setSkillPercent('stamina', percent, text, 'green') + elseif stamina > 2400 and g_game.getClientVersion() >= 1038 and not localPlayer:isPremium() then + local text = tr("You have %s hours and %s minutes left", hours, minutes) .. '\n' .. + tr("You will not gain 50%% more experience because you aren't premium player, now you receive only 1x experience points") + setSkillPercent('stamina', percent, text, '#89F013') + elseif stamina > 2400 and g_game.getClientVersion() < 1038 then + local text = tr("You have %s hours and %s minutes left", hours, minutes) .. '\n' .. + tr("If you are premium player, you will gain 50%% more experience") + setSkillPercent('stamina', percent, text, 'green') + elseif stamina <= 2400 and stamina > 840 then + setSkillPercent('stamina', percent, tr("You have %s hours and %s minutes left", hours, minutes), 'orange') + elseif stamina <= 840 and stamina > 0 then + local text = tr("You have %s hours and %s minutes left", hours, minutes) .. "\n" .. + tr("You gain only 50%% experience and you don't may gain loot from monsters") + setSkillPercent('stamina', percent, text, 'red') + elseif stamina == 0 then + local text = tr("You have %s hours and %s minutes left", hours, minutes) .. "\n" .. + tr("You don't may receive experience and loot from monsters") + setSkillPercent('stamina', percent, text, 'black') + end +end + +function onOfflineTrainingChange(localPlayer, offlineTrainingTime) + if not g_game.getFeature(GameOfflineTrainingTime) then + return + end + local hours = math.floor(offlineTrainingTime / 60) + local minutes = offlineTrainingTime % 60 + if minutes < 10 then + minutes = '0' .. minutes + end + local percent = 100 * offlineTrainingTime / (12 * 60) -- max is 12 hours + + setSkillValue('offlineTraining', hours .. ":" .. minutes) + setSkillPercent('offlineTraining', percent, tr('You have %s percent', percent)) +end + +function onRegenerationChange(localPlayer, regenerationTime) + if not g_game.getFeature(GamePlayerRegenerationTime) or regenerationTime < 0 then + return + end + local minutes = math.floor(regenerationTime / 60) + local seconds = regenerationTime % 60 + if seconds < 10 then + seconds = '0' .. seconds + end + + setSkillValue('regenerationTime', minutes .. ":" .. seconds) + checkAlert('regenerationTime', regenerationTime, false, 300) +end + +function onSpeedChange(localPlayer, speed) + setSkillValue('speed', speed) + + onBaseSpeedChange(localPlayer, localPlayer:getBaseSpeed()) +end + +function onBaseSpeedChange(localPlayer, baseSpeed) + setSkillBase('speed', localPlayer:getSpeed(), baseSpeed) +end + +function onMagicLevelChange(localPlayer, magiclevel, percent) + setSkillValue('magiclevel', magiclevel) + setSkillPercent('magiclevel', percent, tr('You have %s percent to go', 100 - percent)) + + onBaseMagicLevelChange(localPlayer, localPlayer:getBaseMagicLevel()) +end + +function onBaseMagicLevelChange(localPlayer, baseMagicLevel) + setSkillBase('magiclevel', localPlayer:getMagicLevel(), baseMagicLevel) +end + +function onSkillChange(localPlayer, id, level, percent) + setSkillValue('skillId' .. id, level) + setSkillPercent('skillId' .. id, percent, tr('You have %s percent to go', 100 - percent)) + + onBaseSkillChange(localPlayer, id, localPlayer:getSkillBaseLevel(id)) +end + +function onBaseSkillChange(localPlayer, id, baseLevel) + setSkillBase('skillId'..id, localPlayer:getSkillLevel(id), baseLevel) +end diff --git a/modules/game_skills/skills.otmod b/modules/game_skills/skills.otmod new file mode 100644 index 0000000..fdf4fd6 --- /dev/null +++ b/modules/game_skills/skills.otmod @@ -0,0 +1,11 @@ +Module + name: game_skills + description: Manage skills window + author: baxnie, edubart + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ skills ] + @onLoad: init() + @onUnload: terminate() + dependencies: + - game_interface diff --git a/modules/game_skills/skills.otui b/modules/game_skills/skills.otui new file mode 100644 index 0000000..7a13f2c --- /dev/null +++ b/modules/game_skills/skills.otui @@ -0,0 +1,211 @@ +SkillFirstWidget < UIWidget + +SkillButton < UIButton + height: 21 + margin-bottom: 2 + &onClick: onSkillButtonClick + +SmallSkillButton < SkillButton + height: 14 + +SkillNameLabel < GameLabel + font: verdana-11px-monochrome + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + +SkillValueLabel < GameLabel + id: value + font: verdana-11px-monochrome + text-align: topright + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: prev.left + +SkillPercentPanel < ProgressBar + id: percent + background-color: green + height: 5 + margin-top: 15 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + phantom: false + +MiniWindow + id: skillWindow + !text: tr('Skills') + height: 150 + icon: /images/topbuttons/skills + @onClose: modules.game_skills.onMiniWindowClose() + &save: true + + MiniWindowContents + padding-left: 5 + padding-right: 5 + layout: verticalBox + + SkillButton + margin-top: 5 + id: experience + height: 15 + SkillNameLabel + !text: tr('Experience') + SkillValueLabel + + SkillButton + id: level + SkillNameLabel + !text: tr('Level') + SkillValueLabel + SkillPercentPanel + background-color: red + + SkillButton + id: health + height: 15 + SkillNameLabel + !text: tr('Hit Points') + SkillValueLabel + + SkillButton + id: mana + height: 15 + SkillNameLabel + !text: tr('Mana') + SkillValueLabel + + SkillButton + id: soul + height: 15 + SkillNameLabel + !text: tr('Soul Points') + SkillValueLabel + + SkillButton + id: capacity + height: 15 + SkillNameLabel + !text: tr('Capacity') + SkillValueLabel + + SkillButton + id: speed + height: 15 + SkillNameLabel + !text: tr('Speed') + SkillValueLabel + + SkillButton + id: regenerationTime + SkillNameLabel + !text: tr('Regeneration Time') + SkillValueLabel + + SkillButton + id: stamina + SkillNameLabel + !text: tr('Stamina') + SkillValueLabel + SkillPercentPanel + + SkillButton + id: offlineTraining + SkillNameLabel + !text: tr('Offline Training') + SkillValueLabel + SkillPercentPanel + + SkillButton + id: magiclevel + SkillNameLabel + !text: tr('Magic Level') + SkillValueLabel + SkillPercentPanel + background-color: red + + SkillButton + id: skillId0 + SkillNameLabel + !text: tr('Fist Fighting') + SkillValueLabel + SkillPercentPanel + + SkillButton + id: skillId1 + SkillNameLabel + !text: tr('Club Fighting') + SkillValueLabel + SkillPercentPanel + + SkillButton + id: skillId2 + SkillNameLabel + !text: tr('Sword Fighting') + SkillValueLabel + SkillPercentPanel + + SkillButton + id: skillId3 + SkillNameLabel + !text: tr('Axe Fighting') + SkillValueLabel + SkillPercentPanel + + SkillButton + id: skillId4 + SkillNameLabel + !text: tr('Distance Fighting') + SkillValueLabel + SkillPercentPanel + + SkillButton + id: skillId5 + SkillNameLabel + !text: tr('Shielding') + SkillValueLabel + SkillPercentPanel + + SkillButton + id: skillId6 + SkillNameLabel + !text: tr('Fishing') + SkillValueLabel + SkillPercentPanel + + SmallSkillButton + id: skillId7 + SkillNameLabel + !text: tr('Critical Hit Chance') + SkillValueLabel + + SmallSkillButton + id: skillId8 + SkillNameLabel + !text: tr('Critical Hit Damage') + SkillValueLabel + + SmallSkillButton + id: skillId9 + SkillNameLabel + !text: tr('Life Leech Chance') + SkillValueLabel + + SmallSkillButton + id: skillId10 + SkillNameLabel + !text: tr('Life Leech Amount') + SkillValueLabel + + SmallSkillButton + id: skillId11 + SkillNameLabel + !text: tr('Life Leech Chance') + SkillValueLabel + + SmallSkillButton + id: skillId12 + SkillNameLabel + !text: tr('Life Leech Amount') + SkillValueLabel diff --git a/modules/game_spelllist/spelllist.lua b/modules/game_spelllist/spelllist.lua new file mode 100644 index 0000000..1db1ca3 --- /dev/null +++ b/modules/game_spelllist/spelllist.lua @@ -0,0 +1,390 @@ +local SpelllistProfile = 'Default' + +spelllistWindow = nil +spelllistButton = nil +spellList = nil +nameValueLabel = nil +formulaValueLabel = nil +vocationValueLabel = nil +groupValueLabel = nil +typeValueLabel = nil +cooldownValueLabel = nil +levelValueLabel = nil +manaValueLabel = nil +premiumValueLabel = nil +descriptionValueLabel = nil + +vocationBoxAny = nil +vocationBoxSorcerer = nil +vocationBoxDruid = nil +vocationBoxPaladin = nil +vocationBoxKnight = nil + +groupBoxAny = nil +groupBoxAttack = nil +groupBoxHealing = nil +groupBoxSupport = nil + +premiumBoxAny = nil +premiumBoxNo = nil +premiumBoxYes = nil + +vocationRadioGroup = nil +groupRadioGroup = nil +premiumRadioGroup = nil + +-- consts +FILTER_PREMIUM_ANY = 0 +FILTER_PREMIUM_NO = 1 +FILTER_PREMIUM_YES = 2 + +FILTER_VOCATION_ANY = 0 +FILTER_VOCATION_SORCERER = 1 +FILTER_VOCATION_DRUID = 2 +FILTER_VOCATION_PALADIN = 3 +FILTER_VOCATION_KNIGHT = 4 + +FILTER_GROUP_ANY = 0 +FILTER_GROUP_ATTACK = 1 +FILTER_GROUP_HEALING = 2 +FILTER_GROUP_SUPPORT = 3 + +-- Filter Settings +local filters = { + level = false, + vocation = false, + + vocationId = FILTER_VOCATION_ANY, + premium = FILTER_PREMIUM_ANY, + groupId = FILTER_GROUP_ANY +} + +function getSpelllistProfile() + return SpelllistProfile +end + +function setSpelllistProfile(name) + if SpelllistProfile == name then return end + + if SpelllistSettings[name] and SpellInfo[name] then + local oldProfile = SpelllistProfile + SpelllistProfile = name + changeSpelllistProfile(oldProfile) + else + perror('Spelllist profile \'' .. name .. '\' could not be set.') + end +end + +function online() + if g_game.getFeature(GameSpellList) then + spelllistButton:show() + else + spelllistButton:hide() + end + + -- Vocation is only send in newer clients + if g_game.getClientVersion() >= 950 then + spelllistWindow:getChildById('buttonFilterVocation'):setVisible(true) + else + spelllistWindow:getChildById('buttonFilterVocation'):setVisible(false) + end +end + +function offline() + resetWindow() +end + +function init() + connect(g_game, { onGameStart = online, + onGameEnd = offline }) + + spelllistWindow = g_ui.displayUI('spelllist', modules.game_interface.getRightPanel()) + spelllistWindow:hide() + + spelllistButton = modules.client_topmenu.addRightGameToggleButton('spelllistButton', tr('Spell List'), '/images/topbuttons/spelllist', toggle) + spelllistButton:setOn(false) + + nameValueLabel = spelllistWindow:getChildById('labelNameValue') + formulaValueLabel = spelllistWindow:getChildById('labelFormulaValue') + vocationValueLabel = spelllistWindow:getChildById('labelVocationValue') + groupValueLabel = spelllistWindow:getChildById('labelGroupValue') + typeValueLabel = spelllistWindow:getChildById('labelTypeValue') + cooldownValueLabel = spelllistWindow:getChildById('labelCooldownValue') + levelValueLabel = spelllistWindow:getChildById('labelLevelValue') + manaValueLabel = spelllistWindow:getChildById('labelManaValue') + premiumValueLabel = spelllistWindow:getChildById('labelPremiumValue') + descriptionValueLabel = spelllistWindow:getChildById('labelDescriptionValue') + + vocationBoxAny = spelllistWindow:getChildById('vocationBoxAny') + vocationBoxSorcerer = spelllistWindow:getChildById('vocationBoxSorcerer') + vocationBoxDruid = spelllistWindow:getChildById('vocationBoxDruid') + vocationBoxPaladin = spelllistWindow:getChildById('vocationBoxPaladin') + vocationBoxKnight = spelllistWindow:getChildById('vocationBoxKnight') + + groupBoxAny = spelllistWindow:getChildById('groupBoxAny') + groupBoxAttack = spelllistWindow:getChildById('groupBoxAttack') + groupBoxHealing = spelllistWindow:getChildById('groupBoxHealing') + groupBoxSupport = spelllistWindow:getChildById('groupBoxSupport') + + premiumBoxAny = spelllistWindow:getChildById('premiumBoxAny') + premiumBoxYes = spelllistWindow:getChildById('premiumBoxYes') + premiumBoxNo = spelllistWindow:getChildById('premiumBoxNo') + + vocationRadioGroup = UIRadioGroup.create() + vocationRadioGroup:addWidget(vocationBoxAny) + vocationRadioGroup:addWidget(vocationBoxSorcerer) + vocationRadioGroup:addWidget(vocationBoxDruid) + vocationRadioGroup:addWidget(vocationBoxPaladin) + vocationRadioGroup:addWidget(vocationBoxKnight) + + groupRadioGroup = UIRadioGroup.create() + groupRadioGroup:addWidget(groupBoxAny) + groupRadioGroup:addWidget(groupBoxAttack) + groupRadioGroup:addWidget(groupBoxHealing) + groupRadioGroup:addWidget(groupBoxSupport) + + premiumRadioGroup = UIRadioGroup.create() + premiumRadioGroup:addWidget(premiumBoxAny) + premiumRadioGroup:addWidget(premiumBoxYes) + premiumRadioGroup:addWidget(premiumBoxNo) + + premiumRadioGroup:selectWidget(premiumBoxAny) + vocationRadioGroup:selectWidget(vocationBoxAny) + groupRadioGroup:selectWidget(groupBoxAny) + + vocationRadioGroup.onSelectionChange = toggleFilter + groupRadioGroup.onSelectionChange = toggleFilter + premiumRadioGroup.onSelectionChange = toggleFilter + + spellList = spelllistWindow:getChildById('spellList') + + g_keyboard.bindKeyPress('Down', function() spellList:focusNextChild(KeyboardFocusReason) end, spelllistWindow) + g_keyboard.bindKeyPress('Up', function() spellList:focusPreviousChild(KeyboardFocusReason) end, spelllistWindow) + + initializeSpelllist() + resizeWindow() + + if g_game.isOnline() then + online() + end +end + +function terminate() + disconnect(g_game, { onGameStart = online, + onGameEnd = offline }) + + disconnect(spellList, { onChildFocusChange = function(self, focusedChild) + if focusedChild == nil then return end + updateSpellInformation(focusedChild) + end }) + + spelllistWindow:destroy() + spelllistButton:destroy() + + vocationRadioGroup:destroy() + groupRadioGroup:destroy() + premiumRadioGroup:destroy() +end + +function initializeSpelllist() + for i = 1, #SpelllistSettings[SpelllistProfile].spellOrder do + local spell = SpelllistSettings[SpelllistProfile].spellOrder[i] + local info = SpellInfo[SpelllistProfile][spell] + + local tmpLabel = g_ui.createWidget('SpellListLabel', spellList) + tmpLabel:setId(spell) + tmpLabel:setText(spell .. '\n\'' .. info.words .. '\'') + tmpLabel:setPhantom(false) + + local iconId = tonumber(info.icon) + if not iconId and SpellIcons[info.icon] then + iconId = SpellIcons[info.icon][1] + end + + if not(iconId) then + perror('Spell icon \'' .. info.icon .. '\' not found.') + end + + tmpLabel:setHeight(SpelllistSettings[SpelllistProfile].iconSize.height + 4) + tmpLabel:setTextOffset(topoint((SpelllistSettings[SpelllistProfile].iconSize.width + 10) .. ' ' .. (SpelllistSettings[SpelllistProfile].iconSize.height - 32)/2 + 3)) + tmpLabel:setImageSource(SpelllistSettings[SpelllistProfile].iconFile) + tmpLabel:setImageClip(Spells.getImageClip(iconId, SpelllistProfile)) + tmpLabel:setImageSize(tosize(SpelllistSettings[SpelllistProfile].iconSize.width .. ' ' .. SpelllistSettings[SpelllistProfile].iconSize.height)) + tmpLabel.onClick = updateSpellInformation + end + + connect(spellList, { onChildFocusChange = function(self, focusedChild) + if focusedChild == nil then return end + updateSpellInformation(focusedChild) + end }) +end + +function changeSpelllistProfile(oldProfile) + -- Delete old labels + for i = 1, #SpelllistSettings[oldProfile].spellOrder do + local spell = SpelllistSettings[oldProfile].spellOrder[i] + local tmpLabel = spellList:getChildById(spell) + + tmpLabel:destroy() + end + + -- Create new spelllist and ajust window + initializeSpelllist() + resizeWindow() + resetWindow() +end + +function updateSpelllist() + for i = 1, #SpelllistSettings[SpelllistProfile].spellOrder do + local spell = SpelllistSettings[SpelllistProfile].spellOrder[i] + local info = SpellInfo[SpelllistProfile][spell] + local tmpLabel = spellList:getChildById(spell) + + local localPlayer = g_game.getLocalPlayer() + if (not(filters.level) or info.level <= localPlayer:getLevel()) and (not(filters.vocation) or table.find(info.vocations, localPlayer:getVocation())) and (filters.vocationId == FILTER_VOCATION_ANY or table.find(info.vocations, filters.vocationId) or table.find(info.vocations, filters.vocationId+4)) and (filters.groupId == FILTER_GROUP_ANY or info.group[filters.groupId]) and (filters.premium == FILTER_PREMIUM_ANY or (info.premium and filters.premium == FILTER_PREMIUM_YES) or (not(info.premium) and filters.premium == FILTER_PREMIUM_NO)) then + tmpLabel:setVisible(true) + else + tmpLabel:setVisible(false) + end + end +end + +function updateSpellInformation(widget) + local spell = widget:getId() + + local name = '' + local formula = '' + local vocation = '' + local group = '' + local type = '' + local cooldown = '' + local level = '' + local mana = '' + local premium = '' + local description = '' + + if SpellInfo[SpelllistProfile][spell] then + local info = SpellInfo[SpelllistProfile][spell] + + name = spell + formula = info.words + + for i = 1, #info.vocations do + local vocationId = info.vocations[i] + if vocationId <= 4 or not(table.find(info.vocations, (vocationId-4))) then + vocation = vocation .. (vocation:len() == 0 and '' or ', ') .. VocationNames[vocationId] + end + end + + cooldown = (info.exhaustion / 1000) .. 's' + for groupId, groupName in ipairs(SpellGroups) do + if info.group[groupId] then + group = group .. (group:len() == 0 and '' or ' / ') .. groupName + cooldown = cooldown .. ' / ' .. (info.group[groupId] / 1000) .. 's' + end + end + + type = info.type + level = info.level + mana = info.mana .. ' / ' .. info.soul + premium = (info.premium and 'yes' or 'no') + description = info.description or '-' + end + + nameValueLabel:setText(name) + formulaValueLabel:setText(formula) + vocationValueLabel:setText(vocation) + groupValueLabel:setText(group) + typeValueLabel:setText(type) + cooldownValueLabel:setText(cooldown) + levelValueLabel:setText(level) + manaValueLabel:setText(mana) + premiumValueLabel:setText(premium) + descriptionValueLabel:setText(description) +end + +function toggle() + if spelllistButton:isOn() then + spelllistButton:setOn(false) + spelllistWindow:hide() + else + spelllistButton:setOn(true) + spelllistWindow:show() + spelllistWindow:raise() + spelllistWindow:focus() + end +end + +function toggleFilter(widget, selectedWidget) + if widget == vocationRadioGroup then + local boxId = selectedWidget:getId() + if boxId == 'vocationBoxAny' then + filters.vocationId = FILTER_VOCATION_ANY + elseif boxId == 'vocationBoxSorcerer' then + filters.vocationId = FILTER_VOCATION_SORCERER + elseif boxId == 'vocationBoxDruid' then + filters.vocationId = FILTER_VOCATION_DRUID + elseif boxId == 'vocationBoxPaladin' then + filters.vocationId = FILTER_VOCATION_PALADIN + elseif boxId == 'vocationBoxKnight' then + filters.vocationId = FILTER_VOCATION_KNIGHT + end + elseif widget == groupRadioGroup then + local boxId = selectedWidget:getId() + if boxId == 'groupBoxAny' then + filters.groupId = FILTER_GROUP_ANY + elseif boxId == 'groupBoxAttack' then + filters.groupId = FILTER_GROUP_ATTACK + elseif boxId == 'groupBoxHealing' then + filters.groupId = FILTER_GROUP_HEALING + elseif boxId == 'groupBoxSupport' then + filters.groupId = FILTER_GROUP_SUPPORT + end + elseif widget == premiumRadioGroup then + local boxId = selectedWidget:getId() + if boxId == 'premiumBoxAny' then + filters.premium = FILTER_PREMIUM_ANY + elseif boxId == 'premiumBoxNo' then + filters.premium = FILTER_PREMIUM_NO + elseif boxId == 'premiumBoxYes' then + filters.premium = FILTER_PREMIUM_YES + end + else + local id = widget:getId() + if id == 'buttonFilterLevel' then + filters.level = not(filters.level) + widget:setOn(filters.level) + elseif id == 'buttonFilterVocation' then + filters.vocation = not(filters.vocation) + widget:setOn(filters.vocation) + end + end + + updateSpelllist() +end + +function resizeWindow() + spelllistWindow:setWidth(SpelllistSettings['Default'].spellWindowWidth + SpelllistSettings[SpelllistProfile].iconSize.width - 32) + spellList:setWidth(SpelllistSettings['Default'].spellListWidth + SpelllistSettings[SpelllistProfile].iconSize.width - 32) +end + +function resetWindow() + spelllistWindow:hide() + spelllistButton:setOn(false) + + -- Resetting filters + filters.level = false + filters.vocation = false + + local buttonFilterLevel = spelllistWindow:getChildById('buttonFilterLevel') + buttonFilterLevel:setOn(filters.level) + + local buttonFilterVocation = spelllistWindow:getChildById('buttonFilterVocation') + buttonFilterVocation:setOn(filters.vocation) + + vocationRadioGroup:selectWidget(vocationBoxAny) + groupRadioGroup:selectWidget(groupBoxAny) + premiumRadioGroup:selectWidget(premiumBoxAny) + + updateSpelllist() +end diff --git a/modules/game_spelllist/spelllist.otmod b/modules/game_spelllist/spelllist.otmod new file mode 100644 index 0000000..4e3eaec --- /dev/null +++ b/modules/game_spelllist/spelllist.otmod @@ -0,0 +1,9 @@ +Module + name: game_spelllist + description: View available spells + author: Summ, Edubart + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ spelllist ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_spelllist/spelllist.otui b/modules/game_spelllist/spelllist.otui new file mode 100644 index 0000000..7bbc46b --- /dev/null +++ b/modules/game_spelllist/spelllist.otui @@ -0,0 +1,326 @@ +SpellListLabel < Label + font: verdana-11px-monochrome + background-color: alpha + text-offset: 42 3 + focusable: true + height: 36 + image-clip: 0 0 32 32 + image-size: 32 32 + image-offset: 2 2 + image-source: /images/game/spells/defaultspells + + $focus: + background-color: #ffffff22 + color: #ffffff + +SpellInfoLabel < Label + width: 70 + font: verdana-11px-monochrome + text-align: right + margin-left: 10 + margin-top: 5 + +SpellInfoValueLabel < Label + text-align: left + width: 190 + margin-left: 10 + margin-top: 5 + +FilterButton < Button + width: 64 + anchors.left: prev.right + anchors.top: spellList.bottom + @onClick: toggleFilter(self) + margin: 5 0 0 6 + color: #630000 + $on: + color: green + +MainWindow + id: spelllistWindow + !text: tr('Spell List') + size: 550 400 + @onEscape: toggle() + + TextList + id: spellList + vertical-scrollbar: spellsScrollBar + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: next.top + margin-bottom: 10 + padding: 1 + width: 210 + focusable: false + + Button + id: buttonCancel + !text: tr('Close') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: toggle() + + VerticalScrollBar + id: spellsScrollBar + anchors.top: spellList.top + anchors.bottom: spellList.bottom + anchors.right: spellList.right + step: 50 + pixels-scroll: true + + SpellInfoLabel + id: labelName + anchors.left: spellList.right + anchors.top: spellList.top + !text: tr('Name') .. ':' + + Label + anchors.left: parent.left + anchors.top: spellList.bottom + !text: tr('Filters') .. ':' + margin-top: 8 + + FilterButton + id: buttonFilterLevel + !text: tr('Level') + !tooltip: tr('Hide spells for higher exp. levels') + + FilterButton + id: buttonFilterVocation + !text: tr('Vocation') + !tooltip: tr('Hide spells for other vocations') + + SpellInfoLabel + id: labelFormula + anchors.left: spellList.right + anchors.top: labelName.bottom + !text: tr('Formula') .. ':' + + + SpellInfoLabel + id: labelVocation + anchors.left: spellList.right + anchors.top: labelFormula.bottom + !text: tr('Vocation') .. ':' + + SpellInfoLabel + id: labelGroup + anchors.left: spellList.right + anchors.top: labelVocation.bottom + !text: tr('Group') .. ':' + + SpellInfoLabel + id: labelType + anchors.left: spellList.right + anchors.top: labelGroup.bottom + !text: tr('Type') .. ':' + + SpellInfoLabel + id: labelCooldown + anchors.left: spellList.right + anchors.top: labelType.bottom + !text: tr('Cooldown') .. ':' + + SpellInfoLabel + id: labelLevel + anchors.left: spellList.right + anchors.top: labelCooldown.bottom + !text: tr('Level') .. ':' + + SpellInfoLabel + id: labelMana + anchors.left: spellList.right + anchors.top: labelLevel.bottom + !text: tr('Mana') .. ' / ' .. tr('Soul') .. ':' + + SpellInfoLabel + id: labelPremium + anchors.left: spellList.right + anchors.top: labelMana.bottom + !text: tr('Premium') .. ':' + + SpellInfoLabel + id: labelDescription + anchors.left: spellList.right + anchors.top: labelPremium.bottom + !text: tr('Description') .. ':' + + SpellInfoValueLabel + id: labelNameValue + anchors.left: labelName.right + anchors.top: spellList.top + + SpellInfoValueLabel + id: labelFormulaValue + anchors.left: labelFormula.right + anchors.top: labelNameValue.bottom + + SpellInfoValueLabel + id: labelVocationValue + anchors.left: labelVocation.right + anchors.top: labelFormulaValue.bottom + + SpellInfoValueLabel + id: labelGroupValue + anchors.left: labelGroup.right + anchors.top: labelVocationValue.bottom + + SpellInfoValueLabel + id: labelTypeValue + anchors.left: labelType.right + anchors.top: labelGroupValue.bottom + + SpellInfoValueLabel + id: labelCooldownValue + anchors.left: labelCooldown.right + anchors.top: labelTypeValue.bottom + + SpellInfoValueLabel + id: labelLevelValue + anchors.left: labelLevel.right + anchors.top: labelCooldownValue.bottom + + SpellInfoValueLabel + id: labelManaValue + anchors.left: labelMana.right + anchors.top: labelLevelValue.bottom + + SpellInfoValueLabel + id: labelPremiumValue + anchors.left: labelPremium.right + anchors.top: labelManaValue.bottom + + SpellInfoValueLabel + id: labelDescriptionValue + anchors.left: labelDescription.right + anchors.top: labelPremiumValue.bottom + + Label + id: labelVocationFilter + anchors.top: labelPremium.bottom + anchors.left: spellList.right + width: 70 + font: verdana-11px-monochrome + !text: tr('Vocation') + margin-top: 30 + margin-left: 20 + + CheckBox + id: vocationBoxAny + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 3 + margin-left: 3 + !text: tr('Any') + width: 75 + + CheckBox + id: vocationBoxSorcerer + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 3 + !text: tr('Sorcerer') + width: 75 + + CheckBox + id: vocationBoxDruid + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 3 + !text: tr('Druid') + width: 75 + + CheckBox + id: vocationBoxPaladin + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 3 + !text: tr('Paladin') + width: 75 + + CheckBox + id: vocationBoxKnight + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 3 + !text: tr('Knight') + width: 75 + + Label + id: labelGroupFilter + anchors.top: labelPremium.bottom + anchors.left: labelVocationFilter.right + width: 70 + font: verdana-11px-monochrome + !text: tr('Group') + margin-top: 30 + margin-left: 20 + + CheckBox + id: groupBoxAny + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 3 + margin-left: 3 + !text: tr('Any') + width: 75 + + CheckBox + id: groupBoxAttack + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 3 + !text: tr('Attack') + width: 75 + + CheckBox + id: groupBoxHealing + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 3 + !text: tr('Healing') + width: 75 + + CheckBox + id: groupBoxSupport + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 3 + !text: tr('Support') + width: 75 + + Label + id: labelPremiumFilter + anchors.top: labelPremium.bottom + anchors.left: labelGroupFilter.right + width: 70 + font: verdana-11px-monochrome + !text: tr('Premium') + margin-top: 30 + margin-left: 20 + + CheckBox + id: premiumBoxAny + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 3 + margin-left: 3 + !text: tr('Any') + width: 75 + + CheckBox + id: premiumBoxNo + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 3 + !text: tr('No') + width: 75 + + CheckBox + id: premiumBoxYes + anchors.left: prev.left + anchors.top: prev.bottom + margin-top: 3 + !text: tr('Yes') + width: 75 diff --git a/modules/game_textmessage/textmessage.lua b/modules/game_textmessage/textmessage.lua new file mode 100644 index 0000000..2bd80d1 --- /dev/null +++ b/modules/game_textmessage/textmessage.lua @@ -0,0 +1,135 @@ +MessageSettings = { + none = {}, + consoleRed = { color = TextColors.red, consoleTab='Default' }, + consoleOrange = { color = TextColors.orange, consoleTab='Default' }, + consoleBlue = { color = TextColors.blue, consoleTab='Default' }, + centerRed = { color = TextColors.red, consoleTab='Server Log', screenTarget='lowCenterLabel' }, + centerGreen = { color = TextColors.green, consoleTab='Server Log', screenTarget='highCenterLabel', consoleOption='showInfoMessagesInConsole' }, + centerWhite = { color = TextColors.white, consoleTab='Server Log', screenTarget='middleCenterLabel', consoleOption='showEventMessagesInConsole' }, + bottomWhite = { color = TextColors.white, consoleTab='Server Log', screenTarget='statusLabel', consoleOption='showEventMessagesInConsole' }, + status = { color = TextColors.white, consoleTab='Server Log', screenTarget='statusLabel', consoleOption='showStatusMessagesInConsole' }, + statusSmall = { color = TextColors.white, screenTarget='statusLabel' }, + private = { color = TextColors.lightblue, screenTarget='privateLabel' } +} + +MessageTypes = { + [MessageModes.MonsterSay] = MessageSettings.consoleOrange, + [MessageModes.MonsterYell] = MessageSettings.consoleOrange, + [MessageModes.BarkLow] = MessageSettings.consoleOrange, + [MessageModes.BarkLoud] = MessageSettings.consoleOrange, + [MessageModes.Failure] = MessageSettings.statusSmall, + [MessageModes.Login] = MessageSettings.bottomWhite, + [MessageModes.Game] = MessageSettings.centerWhite, + [MessageModes.Status] = MessageSettings.status, + [MessageModes.Warning] = MessageSettings.centerRed, + [MessageModes.Look] = MessageSettings.centerGreen, + [MessageModes.Loot] = MessageSettings.centerGreen, + [MessageModes.Red] = MessageSettings.consoleRed, + [MessageModes.Blue] = MessageSettings.consoleBlue, + [MessageModes.PrivateFrom] = MessageSettings.consoleBlue, + + [MessageModes.GamemasterBroadcast] = MessageSettings.consoleRed, + + [MessageModes.DamageDealed] = MessageSettings.status, + [MessageModes.DamageReceived] = MessageSettings.status, + [MessageModes.Heal] = MessageSettings.status, + [MessageModes.Exp] = MessageSettings.status, + + [MessageModes.DamageOthers] = MessageSettings.none, + [MessageModes.HealOthers] = MessageSettings.none, + [MessageModes.ExpOthers] = MessageSettings.none, + + [MessageModes.TradeNpc] = MessageSettings.centerWhite, + [MessageModes.Guild] = MessageSettings.centerWhite, + [MessageModes.Party] = MessageSettings.centerGreen, + [MessageModes.PartyManagement] = MessageSettings.centerWhite, + [MessageModes.TutorialHint] = MessageSettings.centerWhite, + [MessageModes.BeyondLast] = MessageSettings.centerWhite, + [MessageModes.Report] = MessageSettings.consoleRed, + [MessageModes.HotkeyUse] = MessageSettings.centerGreen, + + [254] = MessageSettings.private +} + +messagesPanel = nil + +function init() + for messageMode, _ in pairs(MessageTypes) do + registerMessageMode(messageMode, displayMessage) + end + + connect(g_game, 'onGameEnd', clearMessages) + messagesPanel = g_ui.loadUI('textmessage', modules.game_interface.getRootPanel()) +end + +function terminate() + for messageMode, _ in pairs(MessageTypes) do + unregisterMessageMode(messageMode, displayMessage) + end + + disconnect(g_game, 'onGameEnd', clearMessages) + clearMessages() + messagesPanel:destroy() +end + +function calculateVisibleTime(text) + return math.max(#text * 100, 4000) +end + +function displayMessage(mode, text) + if not g_game.isOnline() then return end + + local msgtype = MessageTypes[mode] + if not msgtype then + return + end + + if msgtype == MessageSettings.none then return end + + if msgtype.consoleTab ~= nil and (msgtype.consoleOption == nil or modules.client_options.getOption(msgtype.consoleOption)) then + modules.game_console.addText(text, msgtype, tr(msgtype.consoleTab)) + --TODO move to game_console + end + + if msgtype.screenTarget then + local label = messagesPanel:recursiveGetChildById(msgtype.screenTarget) + label:setText(text) + label:setColor(msgtype.color) + label:setVisible(true) + removeEvent(label.hideEvent) + label.hideEvent = scheduleEvent(function() label:setVisible(false) end, calculateVisibleTime(text)) + end +end + +function displayPrivateMessage(text) + displayMessage(254, text) +end + +function displayStatusMessage(text) + displayMessage(MessageModes.Status, text) +end + +function displayFailureMessage(text) + displayMessage(MessageModes.Failure, text) +end + +function displayGameMessage(text) + displayMessage(MessageModes.Game, text) +end + +function displayBroadcastMessage(text) + displayMessage(MessageModes.Warning, text) +end + +function clearMessages() + for _i,child in pairs(messagesPanel:recursiveGetChildren()) do + if child:getId():match('Label') then + child:hide() + removeEvent(child.hideEvent) + end + end +end + +function LocalPlayer:onAutoWalkFail(player) + modules.game_textmessage.displayFailureMessage(tr('There is no way.')) +end diff --git a/modules/game_textmessage/textmessage.otmod b/modules/game_textmessage/textmessage.otmod new file mode 100644 index 0000000..068d066 --- /dev/null +++ b/modules/game_textmessage/textmessage.otmod @@ -0,0 +1,9 @@ +Module + name: game_textmessage + description: Manage game text messages + author: edubart + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ textmessage ] + @onLoad: init() + @onUnload: terminate() \ No newline at end of file diff --git a/modules/game_textmessage/textmessage.otui b/modules/game_textmessage/textmessage.otui new file mode 100644 index 0000000..32cd2b6 --- /dev/null +++ b/modules/game_textmessage/textmessage.otui @@ -0,0 +1,41 @@ +TextMessageLabel < UILabel + font: verdana-11px-rounded + text-align: center + text-wrap: true + text-auto-resize: true + margin-bottom: 2 + visible: false + +Panel + anchors.fill: gameMapPanel + anchors.bottom: gameBottomPanel.top + focusable: false + + Panel + id: centerTextMessagePanel + layout: + type: verticalBox + fit-children: true + width: 360 + anchors.centerIn: parent + + TextMessageLabel + id: highCenterLabel + TextMessageLabel + id: middleCenterLabel + TextMessageLabel + id: lowCenterLabel + + TextMessageLabel + id: privateLabel + anchors.top: parent.top + anchors.bottom: centerTextMessagePanel.top + anchors.horizontalCenter: parent.horizontalCenter + text-auto-resize: false + width: 275 + + TextMessageLabel + id: statusLabel + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right diff --git a/modules/game_textwindow/textwindow.lua b/modules/game_textwindow/textwindow.lua new file mode 100644 index 0000000..696157b --- /dev/null +++ b/modules/game_textwindow/textwindow.lua @@ -0,0 +1,144 @@ +local windows = {} + +function init() + g_ui.importStyle('textwindow') + + connect(g_game, { onEditText = onGameEditText, + onEditList = onGameEditList, + onGameEnd = destroyWindows }) +end + +function terminate() + disconnect(g_game, { onEditText = onGameEditText, + onEditList = onGameEditList, + onGameEnd = destroyWindows }) + + destroyWindows() +end + +function destroyWindows() + for _,window in pairs(windows) do + window:destroy() + end + windows = {} +end + +function onGameEditText(id, itemId, maxLength, text, writer, time) + local textWindow = g_ui.createWidget('TextWindow', rootWidget) + + local writeable = #text < maxLength and maxLength > 0 + local textItem = textWindow:getChildById('textItem') + local description = textWindow:getChildById('description') + local textEdit = textWindow:getChildById('text') + local okButton = textWindow:getChildById('okButton') + local cancelButton = textWindow:getChildById('cancelButton') + + local textScroll = textWindow:getChildById('textScroll') + + if textItem:isHidden() then + textItem:show() + end + + textItem:setItemId(itemId) + textEdit:setMaxLength(maxLength) + textEdit:setText(text) + textEdit:setEditable(writeable) + textEdit:setCursorVisible(writeable) + + local desc = '' + if #writer > 0 then + desc = tr('You read the following, written by \n%s\n', writer) + if #time > 0 then + desc = desc .. tr('on %s.\n', time) + end + elseif #time > 0 then + desc = tr('You read the following, written on \n%s.\n', time) + end + + if #text == 0 and not writeable then + desc = tr("It is empty.") + elseif writeable then + desc = desc .. tr('You can enter new text.') + end + + local lines = #{string.find(desc, '\n')} + if lines < 2 then desc = desc .. '\n' end + + description:setText(desc) + + if not writeable then + textWindow:setText(tr('Show Text')) + cancelButton:hide() + cancelButton:setWidth(0) + okButton:setMarginRight(0) + else + textWindow:setText(tr('Edit Text')) + end + + if description:getHeight() < 64 then + description:setHeight(64) + end + + local function destroy() + textWindow:destroy() + table.removevalue(windows, textWindow) + end + + local doneFunc = function() + if writeable then + g_game.editText(id, textEdit:getText()) + end + destroy() + end + + okButton.onClick = doneFunc + cancelButton.onClick = destroy + + if not writeable then + textWindow.onEnter = doneFunc + end + + textWindow.onEscape = destroy + + table.insert(windows, textWindow) +end + +function onGameEditList(id, doorId, text) + local textWindow = g_ui.createWidget('TextWindow', rootWidget) + + local textEdit = textWindow:getChildById('text') + local description = textWindow:getChildById('description') + local okButton = textWindow:getChildById('okButton') + local cancelButton = textWindow:getChildById('cancelButton') + + local textItem = textWindow:getChildById('textItem') + if textItem and not textItem:isHidden() then + textItem:hide() + end + + textEdit:setMaxLength(8192) + textEdit:setText(text) + textEdit:setEditable(true) + description:setText(tr('Enter one name per line.')) + textWindow:setText(tr('Edit List')) + + if description:getHeight() < 64 then + description:setHeight(64) + end + + local function destroy() + textWindow:destroy() + table.removevalue(windows, textWindow) + end + + local doneFunc = function() + g_game.editList(id, doorId, textEdit:getText()) + destroy() + end + + okButton.onClick = doneFunc + cancelButton.onClick = destroy + textWindow.onEscape = destroy + + table.insert(windows, textWindow) +end diff --git a/modules/game_textwindow/textwindow.otmod b/modules/game_textwindow/textwindow.otmod new file mode 100644 index 0000000..d70d650 --- /dev/null +++ b/modules/game_textwindow/textwindow.otmod @@ -0,0 +1,10 @@ +Module + name: game_textwindow + description: Allow to edit text books and lists + author: edubart, BeniS + website: https://github.com/edubart/otclient + sandboxed: true + dependencies: [ game_interface ] + scripts: [ textwindow ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_textwindow/textwindow.otui b/modules/game_textwindow/textwindow.otui new file mode 100644 index 0000000..d803737 --- /dev/null +++ b/modules/game_textwindow/textwindow.otui @@ -0,0 +1,53 @@ +TextWindow < MainWindow + id: textWindow + size: 300 280 + + Item + id: textItem + virtual: true + anchors.top: parent.top + anchors.left: parent.left + + Label + id: description + anchors.top: parent.top + anchors.left: textItem.right + anchors.right: parent.right + margin-left: 8 + text-auto-resize: true + text-align: left + text-wrap: true + + MultilineTextEdit + id: text + anchors.top: textScroll.top + anchors.left: parent.left + anchors.right: textScroll.left + anchors.bottom: textScroll.bottom + vertical-scrollbar: textScroll + text-wrap: true + + VerticalScrollBar + id: textScroll + anchors.top: description.bottom + anchors.bottom: okButton.top + anchors.right: parent.right + margin-top: 10 + margin-bottom: 10 + step: 16 + pixels-scroll: true + + Button + id: okButton + !text: tr('Ok') + anchors.bottom: parent.bottom + anchors.right: next.left + margin-right: 10 + width: 60 + + Button + id: cancelButton + !text: tr('Cancel') + anchors.bottom: parent.bottom + anchors.right: parent.right + width: 60 diff --git a/modules/game_things/things.lua b/modules/game_things/things.lua new file mode 100644 index 0000000..a56aa86 --- /dev/null +++ b/modules/game_things/things.lua @@ -0,0 +1,70 @@ +filename = nil +loaded = false + +function setFileName(name) + filename = name +end + +function isLoaded() + return loaded +end + +function load() + local version = g_game.getClientVersion() + local things = g_settings.getNode('things') + + local datPath, sprPath + if things["data"] ~= nil and things["sprites"] ~= nil then + datPath = '/data/things/' .. things["data"] + if G.hdSprites and things["sprites_hd"] then + sprPath = '/data/things/' .. things["sprites_hd"] + else + sprPath = '/data/things/' .. things["sprites"] + end + else + if filename then + datPath = resolvepath('/things/' .. filename) + sprPath = resolvepath('/things/' .. filename) + if G.hdSprites then + local hdsprPath = resolvepath('/things/' .. filename .. '_hd') + if g_resources.fileExists(hdsprPath) then + sprPath = hdsprPath + end + end + else + datPath = resolvepath('/things/' .. version .. '/Tibia') + sprPath = resolvepath('/things/' .. version .. '/Tibia') + if G.hdSprites then + local hdsprPath = resolvepath('/things/' .. version .. '/Tibia_hd') + if g_resources.fileExists(hdsprPath) then + sprPath = hdsprPath + end + end + end + end + + local errorMessage = '' + if not g_things.loadDat(datPath) then + if not g_game.getFeature(GameSpritesU32) then + g_game.enableFeature(GameSpritesU32) + if not g_things.loadDat(datPath) then + errorMessage = errorMessage .. tr("Unable to load dat file, please place a valid dat in '%s'", datPath) .. '\n' + end + else + errorMessage = errorMessage .. tr("Unable to load dat file, please place a valid dat in '%s'", datPath) .. '\n' + end + end + if not g_sprites.loadSpr(sprPath, G.hdSprites or false) then + errorMessage = errorMessage .. tr("Unable to load spr file, please place a valid spr in '%s'", sprPath) + end + + loaded = (errorMessage:len() == 0) + + if errorMessage:len() > 0 then + local messageBox = displayErrorBox(tr('Error'), errorMessage) + addEvent(function() messageBox:raise() messageBox:focus() end) + + g_game.setClientVersion(0) + g_game.setProtocolVersion(0) + end +end diff --git a/modules/game_things/things.otmod b/modules/game_things/things.otmod new file mode 100644 index 0000000..c2536d3 --- /dev/null +++ b/modules/game_things/things.otmod @@ -0,0 +1,6 @@ +Module + name: game_things + description: Contains things spr and dat + reloadable: false + sandboxed: true + scripts: [things] diff --git a/modules/game_unjustifiedpoints/unjustifiedpoints.lua b/modules/game_unjustifiedpoints/unjustifiedpoints.lua new file mode 100644 index 0000000..7538659 --- /dev/null +++ b/modules/game_unjustifiedpoints/unjustifiedpoints.lua @@ -0,0 +1,148 @@ +unjustifiedPointsWindow = nil +unjustifiedPointsButton = nil +contentsPanel = nil + +openPvpSituationsLabel = nil +currentSkullWidget = nil +skullTimeLabel = nil + +dayProgressBar = nil +weekProgressBar = nil +monthProgressBar = nil + +daySkullWidget = nil +weekSkullWidget = nil +monthSkullWidget = nil + +function init() + connect(g_game, { onGameStart = online, + onUnjustifiedPointsChange = onUnjustifiedPointsChange, + onOpenPvpSituationsChange = onOpenPvpSituationsChange }) + connect(LocalPlayer, { onSkullChange = onSkullChange } ) + + unjustifiedPointsButton = modules.client_topmenu.addRightGameToggleButton('unjustifiedPointsButton', + tr('Unjustified Points'), '/images/topbuttons/unjustifiedpoints', toggle) + unjustifiedPointsButton:setOn(true) + unjustifiedPointsButton:hide() + + unjustifiedPointsWindow = g_ui.loadUI('unjustifiedpoints', modules.game_interface.getRightPanel()) + unjustifiedPointsWindow:disableResize() + unjustifiedPointsWindow:setup() + + contentsPanel = unjustifiedPointsWindow:getChildById('contentsPanel') + + openPvpSituationsLabel = contentsPanel:getChildById('openPvpSituationsLabel') + currentSkullWidget = contentsPanel:getChildById('currentSkullWidget') + skullTimeLabel = contentsPanel:getChildById('skullTimeLabel') + + dayProgressBar = contentsPanel:getChildById('dayProgressBar') + weekProgressBar = contentsPanel:getChildById('weekProgressBar') + monthProgressBar = contentsPanel:getChildById('monthProgressBar') + daySkullWidget = contentsPanel:getChildById('daySkullWidget') + weekSkullWidget = contentsPanel:getChildById('weekSkullWidget') + monthSkullWidget = contentsPanel:getChildById('monthSkullWidget') + + if g_game.isOnline() then + online() + end +end + +function terminate() + disconnect(g_game, { onGameStart = online, + onUnjustifiedPointsChange = onUnjustifiedPointsChange, + onOpenPvpSituationsChange = onOpenPvpSituationsChange }) + disconnect(LocalPlayer, { onSkullChange = onSkullChange } ) + + unjustifiedPointsWindow:destroy() + unjustifiedPointsButton:destroy() +end + +function onMiniWindowClose() + unjustifiedPointsButton:setOn(false) +end + +function toggle() + if unjustifiedPointsButton:isOn() then + unjustifiedPointsWindow:close() + unjustifiedPointsButton:setOn(false) + else + unjustifiedPointsWindow:open() + unjustifiedPointsButton:setOn(true) + end +end + +function online() + if g_game.getFeature(GameUnjustifiedPoints) then + unjustifiedPointsButton:show() + else + unjustifiedPointsButton:hide() + unjustifiedPointsWindow:close() + end + + refresh() +end + +function refresh() + local localPlayer = g_game.getLocalPlayer() + + local unjustifiedPoints = g_game.getUnjustifiedPoints() + onUnjustifiedPointsChange(unjustifiedPoints) + + onSkullChange(localPlayer, localPlayer:getSkull()) + onOpenPvpSituationsChange(g_game.getOpenPvpSituations()) +end + +function onSkullChange(localPlayer, skull) + if not localPlayer:isLocalPlayer() then return end + + if skull == SkullRed or skull == SkullBlack then + currentSkullWidget:setIcon(getSkullImagePath(skull)) + currentSkullWidget:setTooltip('Remaining skull time') + else + currentSkullWidget:setIcon('') + currentSkullWidget:setTooltip('You have no skull') + end + + daySkullWidget:setIcon(getSkullImagePath(getNextSkullId(skull))) + weekSkullWidget:setIcon(getSkullImagePath(getNextSkullId(skull))) + monthSkullWidget:setIcon(getSkullImagePath(getNextSkullId(skull))) +end + +function onOpenPvpSituationsChange(amount) + openPvpSituationsLabel:setText(amount) +end + +local function getColorByKills(kills) + if kills < 2 then + return 'red' + elseif kills < 3 then + return 'yellow' + end + + return 'green' +end + +function onUnjustifiedPointsChange(unjustifiedPoints) + if unjustifiedPoints.skullTime == 0 then + skullTimeLabel:setText('No skull') + skullTimeLabel:setTooltip('You have no skull') + else + skullTimeLabel:setText(unjustifiedPoints.skullTime .. ' days') + skullTimeLabel:setTooltip('Remaining skull time') + end + + dayProgressBar:setValue(unjustifiedPoints.killsDay, 0, 100) + dayProgressBar:setBackgroundColor(getColorByKills(unjustifiedPoints.killsDayRemaining)) + dayProgressBar:setTooltip(string.format('Unjustified points gained during the last 24 hours.\n%i kill%s left.', unjustifiedPoints.killsDayRemaining, (unjustifiedPoints.killsDayRemaining == 1 and '' or 's'))) + dayProgressBar:setText(string.format('%i kill%s left', unjustifiedPoints.killsDayRemaining, (unjustifiedPoints.killsDayRemaining == 1 and '' or 's'))) + + weekProgressBar:setValue(unjustifiedPoints.killsWeek, 0, 100) + weekProgressBar:setBackgroundColor(getColorByKills(unjustifiedPoints.killsWeekRemaining)) + weekProgressBar:setTooltip(string.format('Unjustified points gained during the last 7 days.\n%i kill%s left.', unjustifiedPoints.killsWeekRemaining, (unjustifiedPoints.killsWeekRemaining == 1 and '' or 's'))) + weekProgressBar:setText(string.format('%i kill%s left', unjustifiedPoints.killsWeekRemaining, (unjustifiedPoints.killsWeekRemaining == 1 and '' or 's'))) + + monthProgressBar:setValue(unjustifiedPoints.killsMonth, 0, 100) + monthProgressBar:setBackgroundColor(getColorByKills(unjustifiedPoints.killsMonthRemaining)) + monthProgressBar:setTooltip(string.format('Unjustified points gained during the last 30 days.\n%i kill%s left.', unjustifiedPoints.killsMonthRemaining, (unjustifiedPoints.killsMonthRemaining == 1 and '' or 's'))) + monthProgressBar:setText(string.format('%i kill%s left', unjustifiedPoints.killsMonthRemaining, (unjustifiedPoints.killsMonthRemaining == 1 and '' or 's'))) +end diff --git a/modules/game_unjustifiedpoints/unjustifiedpoints.otmod b/modules/game_unjustifiedpoints/unjustifiedpoints.otmod new file mode 100644 index 0000000..178a414 --- /dev/null +++ b/modules/game_unjustifiedpoints/unjustifiedpoints.otmod @@ -0,0 +1,8 @@ +Module + name: game_unjustifiedpoints + description: View unjustified points + author: Summ + sandboxed: true + scripts: [ unjustifiedpoints ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_unjustifiedpoints/unjustifiedpoints.otui b/modules/game_unjustifiedpoints/unjustifiedpoints.otui new file mode 100644 index 0000000..dc05246 --- /dev/null +++ b/modules/game_unjustifiedpoints/unjustifiedpoints.otui @@ -0,0 +1,80 @@ +SkullProgressBar < ProgressBar + height: 13 + margin: 4 18 0 10 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + +SkullWidget < UIWidget + size: 13 13 + margin-right: 2 + anchors.right: parent.right + image-source: /images/game/skull_socket + +MiniWindow + id: unjustifiedPointsWindow + !text: tr('Unjustified Points') + height: 114 + icon: /images/topbuttons/unjustifiedpoints + @onClose: modules.game_unjustifiedpoints.onMiniWindowClose() + &save: true + + MiniWindowContents + Label + anchors.top: parent.top + anchors.left: parent.left + !text: tr('Open PvP') + !tooltip: tr('Open PvP Situations') + phantom: false + margin-top: 2 + margin-left: 10 + + Label + id: openPvpSituationsLabel + anchors.top: prev.bottom + anchors.left: parent.left + font: verdana-11px-rounded + margin-left: 12 + phantom: false + + Label + anchors.top: parent.top + anchors.right: parent.right + !text: tr('Skull Time') + margin-top: 2 + margin-right: 10 + + SkullWidget + id: currentSkullWidget + anchors.top: prev.bottom + margin-right: 10 + + Label + id: skullTimeLabel + anchors.top: prev.top + anchors.right: prev.left + font: verdana-11px-rounded + margin-right: 6 + phantom: false + + SkullProgressBar + id: dayProgressBar + margin-top: 10 + + SkullWidget + id: daySkullWidget + anchors.top: prev.top + + SkullProgressBar + id: weekProgressBar + + SkullWidget + id: weekSkullWidget + anchors.top: prev.top + + SkullProgressBar + id: monthProgressBar + + SkullWidget + id: monthSkullWidget + anchors.top: prev.top diff --git a/modules/game_viplist/addvip.otui b/modules/game_viplist/addvip.otui new file mode 100644 index 0000000..bc88a2a --- /dev/null +++ b/modules/game_viplist/addvip.otui @@ -0,0 +1,39 @@ +MainWindow + size: 256 128 + !text: tr('Add to VIP list') + @onEnter: modules.game_viplist.addVip() + @onEscape: modules.game_viplist.destroyAddWindow() + + Label + !text: tr('Please enter a character name:') + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + TextEdit + id: name + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 4 + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: next.top + margin-bottom: 10 + + Button + !text: tr('Ok') + width: 64 + anchors.right: next.left + anchors.bottom: parent.bottom + margin-right: 10 + @onClick: modules.game_viplist.addVip() + + Button + !text: tr('Cancel') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom + @onClick: modules.game_viplist.destroyAddWindow() diff --git a/modules/game_viplist/editvip.otui b/modules/game_viplist/editvip.otui new file mode 100644 index 0000000..096489f --- /dev/null +++ b/modules/game_viplist/editvip.otui @@ -0,0 +1,138 @@ +IconButton < CheckBox + size: 20 20 + image-source: /images/game/viplist/vipcheckbox + image-size: 20 20 + image-border: 3 + margin: 2 + icon-source: /images/game/viplist/icons + icon-size: 12 12 + icon-rect: 0 0 12 12 + icon-clip: 0 0 12 12 + icon-offset: 4 6 + + $first: + margin-left: 0 + + $!checked: + image-clip: 26 0 26 26 + + $hover !checked: + image-clip: 78 0 26 26 + + $checked: + image-clip: 0 0 26 26 + + $hover checked: + image-clip: 52 0 26 26 + +MainWindow + size: 272 170 + !text: tr('Edit VIP list entry') + + Label + id: nameLabel + text: Name + anchors.top: parent.top + anchors.left: parent.left + color: green + width: 180 + + Label + !text: tr('Description') .. ':' + anchors.top: prev.bottom + anchors.left: parent.left + text-offset: 0 3 + height: 20 + margin-top: 5 + + TextEdit + id: descriptionText + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin: 0 5 + + Label + !text: tr('Notify-Login') .. ':' + anchors.top: prev.bottom + anchors.left: parent.left + text-offset: 0 3 + height: 20 + margin-top: 5 + + CheckBox + id: checkBoxNotify + anchors.top: prev.top + anchors.left: prev.right + margin: 2 6 + + UIWidget + layout: horizontalBox + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 24 + + IconButton + id: icon0 + + IconButton + id: icon1 + icon-clip: 12 0 12 12 + + IconButton + id: icon2 + icon-clip: 24 0 12 12 + + IconButton + id: icon3 + icon-clip: 36 0 12 12 + + IconButton + id: icon4 + icon-clip: 48 0 12 12 + + IconButton + id: icon5 + icon-clip: 60 0 12 12 + + IconButton + id: icon6 + icon-clip: 72 0 12 12 + + IconButton + id: icon7 + icon-clip: 84 0 12 12 + + IconButton + id: icon8 + icon-clip: 96 0 12 12 + + IconButton + id: icon9 + icon-clip: 108 0 12 12 + + IconButton + id: icon10 + icon-clip: 120 0 12 12 + + HorizontalSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: next.top + margin-bottom: 10 + + Button + id: buttonOK + !text: tr('Ok') + width: 64 + anchors.right: next.left + anchors.bottom: parent.bottom + margin-right: 10 + + Button + id: buttonCancel + !text: tr('Cancel') + width: 64 + anchors.right: parent.right + anchors.bottom: parent.bottom diff --git a/modules/game_viplist/viplist.lua b/modules/game_viplist/viplist.lua new file mode 100644 index 0000000..ad4d28d --- /dev/null +++ b/modules/game_viplist/viplist.lua @@ -0,0 +1,420 @@ +vipWindow = nil +vipButton = nil +addVipWindow = nil +editVipWindow = nil +vipInfo = {} + +function init() + connect(g_game, { onGameStart = refresh, + onGameEnd = clear, + onAddVip = onAddVip, + onVipStateChange = onVipStateChange }) + + + g_keyboard.bindKeyDown('Ctrl+P', toggle) + + vipButton = modules.client_topmenu.addRightGameToggleButton('vipListButton', tr('VIP List') .. ' (Ctrl+P)', '/images/topbuttons/viplist', toggle) + vipButton:setOn(true) + vipWindow = g_ui.loadUI('viplist', modules.game_interface.getRightPanel()) + + if not g_game.getFeature(GameAdditionalVipInfo) then + loadVipInfo() + end + refresh() + vipWindow:setup() +end + +function terminate() + g_keyboard.unbindKeyDown('Ctrl+P') + disconnect(g_game, { onGameStart = refresh, + onGameEnd = clear, + onAddVip = onAddVip, + onVipStateChange = onVipStateChange }) + + if not g_game.getFeature(GameAdditionalVipInfo) then + saveVipInfo() + end + + if addVipWindow then + addVipWindow:destroy() + end + + if editVipWindow then + editVipWindow:destroy() + end + + vipWindow:destroy() + vipButton:destroy() +end + +function loadVipInfo() + local settings = g_settings.getNode('VipList') + if not settings then + vipInfo = {} + return + end + vipInfo = settings['VipInfo'] or {} +end + +function saveVipInfo() + settings = {} + settings['VipInfo'] = vipInfo + g_settings.mergeNode('VipList', settings) +end + + +function refresh() + clear() + for id,vip in pairs(g_game.getVips()) do + onAddVip(id, unpack(vip)) + end + + vipWindow:setContentMinimumHeight(38) +end + +function clear() + local vipList = vipWindow:getChildById('contentsPanel') + vipList:destroyChildren() +end + +function toggle() + if vipButton:isOn() then + vipWindow:close() + vipButton:setOn(false) + else + vipWindow:open() + vipButton:setOn(true) + end +end + +function onMiniWindowClose() + vipButton:setOn(false) +end + +function createAddWindow() + if not addVipWindow then + addVipWindow = g_ui.displayUI('addvip') + end +end + +function createEditWindow(widget) + if editVipWindow then + return + end + + editVipWindow = g_ui.displayUI('editvip') + + local name = widget:getText() + local id = widget:getId():sub(4) + + local okButton = editVipWindow:getChildById('buttonOK') + local cancelButton = editVipWindow:getChildById('buttonCancel') + + local nameLabel = editVipWindow:getChildById('nameLabel') + nameLabel:setText(name) + + local descriptionText = editVipWindow:getChildById('descriptionText') + descriptionText:appendText(widget:getTooltip()) + + local notifyCheckBox = editVipWindow:getChildById('checkBoxNotify') + notifyCheckBox:setChecked(widget.notifyLogin) + + local iconRadioGroup = UIRadioGroup.create() + for i = VipIconFirst, VipIconLast do + iconRadioGroup:addWidget(editVipWindow:recursiveGetChildById('icon' .. i)) + end + iconRadioGroup:selectWidget(editVipWindow:recursiveGetChildById('icon' .. widget.iconId)) + + local cancelFunction = function() + editVipWindow:destroy() + iconRadioGroup:destroy() + editVipWindow = nil + end + + local saveFunction = function() + local vipList = vipWindow:getChildById('contentsPanel') + if not widget or not vipList:hasChild(widget) then + cancelFunction() + return + end + + local name = widget:getText() + local state = widget.vipState + local description = descriptionText:getText() + local iconId = tonumber(iconRadioGroup:getSelectedWidget():getId():sub(5)) + local notify = notifyCheckBox:isChecked() + + if g_game.getFeature(GameAdditionalVipInfo) then + g_game.editVip(id, description, iconId, notify) + else + if notify ~= false or #description > 0 or iconId > 0 then + vipInfo[id] = {description = description, iconId = iconId, notifyLogin = notify} + else + vipInfo[id] = nil + end + end + + widget:destroy() + onAddVip(id, name, state, description, iconId, notify) + + editVipWindow:destroy() + iconRadioGroup:destroy() + editVipWindow = nil + end + + cancelButton.onClick = cancelFunction + okButton.onClick = saveFunction + + editVipWindow.onEscape = cancelFunction + editVipWindow.onEnter = saveFunction +end + +function destroyAddWindow() + addVipWindow:destroy() + addVipWindow = nil +end + +function addVip() + g_game.addVip(addVipWindow:getChildById('name'):getText()) + destroyAddWindow() +end + +function removeVip(widgetOrName) + if not widgetOrName then + return + end + + local widget + local vipList = vipWindow:getChildById('contentsPanel') + if type(widgetOrName) == 'string' then + local entries = vipList:getChildren() + for i = 1, #entries do + if entries[i]:getText():lower() == widgetOrName:lower() then + widget = entries[i] + break + end + end + if not widget then + return + end + else + widget = widgetOrName + end + + if widget then + local id = widget:getId():sub(4) + g_game.removeVip(id) + vipList:removeChild(widget) + if vipInfo[id] and g_game.getFeature(GameAdditionalVipInfo) then + vipInfo[id] = nil + end + end +end + +function hideOffline(state) + settings = {} + settings['hideOffline'] = state + g_settings.mergeNode('VipList', settings) + + refresh() +end + +function isHiddingOffline() + local settings = g_settings.getNode('VipList') + if not settings then + return false + end + return settings['hideOffline'] +end + +function getSortedBy() + local settings = g_settings.getNode('VipList') + if not settings or not settings['sortedBy'] then + return 'status' + end + return settings['sortedBy'] +end + +function sortBy(state) + settings = {} + settings['sortedBy'] = state + g_settings.mergeNode('VipList', settings) + + refresh() +end + +function onAddVip(id, name, state, description, iconId, notify) + local vipList = vipWindow:getChildById('contentsPanel') + + local childrenCount = vipList:getChildCount() + for i=1,childrenCount do + local child = vipList:getChildByIndex(i) + if child:getText() == name then + return -- don't add duplicated vips + end + end + + local label = g_ui.createWidget('VipListLabel') + label.onMousePress = onVipListLabelMousePress + label:setId('vip' .. id) + label:setText(name) + + if not g_game.getFeature(GameAdditionalVipInfo) then + local tmpVipInfo = vipInfo[tostring(id)] + label.iconId = 0 + label.notifyLogin = false + if tmpVipInfo then + if tmpVipInfo.iconId then + label:setImageClip(torect((tmpVipInfo.iconId * 12) .. ' 0 12 12')) + label.iconId = tmpVipInfo.iconId + end + if tmpVipInfo.description then + label:setTooltip(tmpVipInfo.description) + end + label.notifyLogin = tmpVipInfo.notifyLogin or false + end + else + label:setTooltip(description) + label:setImageClip(torect((iconId * 12) .. ' 0 12 12')) + label.iconId = iconId + label.notifyLogin = notify + end + + if state == VipState.Online then + label:setColor('#00ff00') + elseif state == VipState.Pending then + label:setColor('#ffca38') + else + label:setColor('#ff0000') + end + + label.vipState = state + + label:setPhantom(false) + connect(label, { onDoubleClick = function () g_game.openPrivateChannel(label:getText()) return true end } ) + + if state == VipState.Offline and isHiddingOffline() then + label:setVisible(false) + end + + local nameLower = name:lower() + local childrenCount = vipList:getChildCount() + + for i=1,childrenCount do + local child = vipList:getChildByIndex(i) + if (state == VipState.Online and child.vipState ~= VipState.Online and getSortedBy() == 'status') + or (label.iconId > child.iconId and getSortedBy() == 'type') then + vipList:insertChild(i, label) + return + end + + if (((state ~= VipState.Online and child.vipState ~= VipState.Online) or (state == VipState.Online and child.vipState == VipState.Online)) and getSortedBy() == 'status') + or (label.iconId == child.iconId and getSortedBy() == 'type') or getSortedBy() == 'name' then + + local childText = child:getText():lower() + local length = math.min(childText:len(), nameLower:len()) + + for j=1,length do + if nameLower:byte(j) < childText:byte(j) then + vipList:insertChild(i, label) + return + elseif nameLower:byte(j) > childText:byte(j) then + break + elseif j == nameLower:len() then -- We are at the end of nameLower, and its shorter than childText, thus insert before + vipList:insertChild(i, label) + return + end + end + end + end + + vipList:insertChild(childrenCount+1, label) +end + +function onVipStateChange(id, state) + local vipList = vipWindow:getChildById('contentsPanel') + local label = vipList:getChildById('vip' .. id) + local name = label:getText() + local description = label:getTooltip() + local iconId = label.iconId + local notify = label.notifyLogin + label:destroy() + + onAddVip(id, name, state, description, iconId, notify) + + if notify and state ~= VipState.Pending then + modules.game_textmessage.displayFailureMessage(tr('%s has logged %s.', name, (state == VipState.Online and 'in' or 'out'))) + end +end + +function onVipListMousePress(widget, mousePos, mouseButton) + if mouseButton ~= MouseRightButton then return end + + local vipList = vipWindow:getChildById('contentsPanel') + + local menu = g_ui.createWidget('PopupMenu') + menu:setGameMenu(true) + menu:addOption(tr('Add new VIP'), function() createAddWindow() end) + + menu:addSeparator() + if not isHiddingOffline() then + menu:addOption(tr('Hide Offline'), function() hideOffline(true) end) + else + menu:addOption(tr('Show Offline'), function() hideOffline(false) end) + end + + if not(getSortedBy() == 'name') then + menu:addOption(tr('Sort by name'), function() sortBy('name') end) + end + + if not(getSortedBy() == 'status') then + menu:addOption(tr('Sort by status'), function() sortBy('status') end) + end + + if not(getSortedBy() == 'type') then + menu:addOption(tr('Sort by type'), function() sortBy('type') end) + end + + menu:display(mousePos) + + return true +end + +function onVipListLabelMousePress(widget, mousePos, mouseButton) + if mouseButton ~= MouseRightButton then return end + + local vipList = vipWindow:getChildById('contentsPanel') + + local menu = g_ui.createWidget('PopupMenu') + menu:setGameMenu(true) + menu:addOption(tr('Send Message'), function() g_game.openPrivateChannel(widget:getText()) end) + menu:addOption(tr('Add new VIP'), function() createAddWindow() end) + menu:addOption(tr('Edit %s', widget:getText()), function() if widget then createEditWindow(widget) end end) + menu:addOption(tr('Remove %s', widget:getText()), function() if widget then removeVip(widget) end end) + menu:addSeparator() + menu:addOption(tr('Copy Name'), function() g_window.setClipboardText(widget:getText()) end) + + if modules.game_console.getOwnPrivateTab() then + menu:addSeparator() + menu:addOption(tr('Invite to private chat'), function() g_game.inviteToOwnChannel(widget:getText()) end) + menu:addOption(tr('Exclude from private chat'), function() g_game.excludeFromOwnChannel(widget:getText()) end) + end + + if not isHiddingOffline() then + menu:addOption(tr('Hide Offline'), function() hideOffline(true) end) + else + menu:addOption(tr('Show Offline'), function() hideOffline(false) end) + end + + if not(getSortedBy() == 'name') then + menu:addOption(tr('Sort by name'), function() sortBy('name') end) + end + + if not(getSortedBy() == 'status') then + menu:addOption(tr('Sort by status'), function() sortBy('status') end) + end + + menu:display(mousePos) + + return true +end diff --git a/modules/game_viplist/viplist.otmod b/modules/game_viplist/viplist.otmod new file mode 100644 index 0000000..c88d44d --- /dev/null +++ b/modules/game_viplist/viplist.otmod @@ -0,0 +1,9 @@ +Module + name: game_viplist + description: Manage vip list window + author: baxnie, edubart + website: https://github.com/edubart/otclient + sandboxed: true + scripts: [ viplist ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_viplist/viplist.otui b/modules/game_viplist/viplist.otui new file mode 100644 index 0000000..38fdea5 --- /dev/null +++ b/modules/game_viplist/viplist.otui @@ -0,0 +1,25 @@ +VipListLabel < GameLabel + margin-top: 2 + text-offset: 16 0 + image-rect: 0 0 12 12 + image-clip: 0 0 12 12 + image-source: /images/game/viplist/icons + font: verdana-11px-monochrome + phantom: false + + $first: + margin-top: 5 + +MiniWindow + id: vipWindow + !text: tr('VIP List') + height: 100 + icon: /images/topbuttons/viplist + @onClose: modules.game_viplist.onMiniWindowClose() + &save: true + + MiniWindowContents + layout: verticalBox + padding-left: 5 + padding-right: 5 + &onMousePress: modules.game_viplist.onVipListMousePress diff --git a/modules/game_walking/walking.lua b/modules/game_walking/walking.lua new file mode 100644 index 0000000..9b5dbce --- /dev/null +++ b/modules/game_walking/walking.lua @@ -0,0 +1,363 @@ +smartWalkDirs = {} +smartWalkDir = nil +wsadWalking = false +nextWalkDir = nil +lastWalkDir = nil +lastFinishedStep = 0 +autoWalkEvent = nil +firstStep = true +walkLock = 0 +lastWalk = 0 +lastTurn = 0 +lastTurnDirection = 0 + +function init() + connect(LocalPlayer, { + onPositionChange = onPositionChange, + onWalk = onWalk, + onTeleport = onTeleport, + onWalkFinish = onWalkFinish + }) + + modules.game_interface.getRootPanel().onFocusChange = stopSmartWalk + bindKeys() +end + +function terminate() + disconnect(LocalPlayer, { + onPositionChange = onPositionChange, + onWalk = onWalk, + onTeleport = onTeleport, + onWalkFinish = onWalkFinish + }) + removeEvent(autoWalkEvent) + stopSmartWalk() + unbindKeys() + disableWSAD() +end + +function bindKeys() + bindWalkKey('Up', North) + bindWalkKey('Right', East) + bindWalkKey('Down', South) + bindWalkKey('Left', West) + bindWalkKey('Numpad8', North) + bindWalkKey('Numpad9', NorthEast) + bindWalkKey('Numpad6', East) + bindWalkKey('Numpad3', SouthEast) + bindWalkKey('Numpad2', South) + bindWalkKey('Numpad1', SouthWest) + bindWalkKey('Numpad4', West) + bindWalkKey('Numpad7', NorthWest) + + bindTurnKey('Ctrl+Up', North) + bindTurnKey('Ctrl+Right', East) + bindTurnKey('Ctrl+Down', South) + bindTurnKey('Ctrl+Left', West) + bindTurnKey('Ctrl+Numpad8', North) + bindTurnKey('Ctrl+Numpad6', East) + bindTurnKey('Ctrl+Numpad2', South) + bindTurnKey('Ctrl+Numpad4', West) +end + +function unbindKeys() + unbindWalkKey('Up', North) + unbindWalkKey('Right', East) + unbindWalkKey('Down', South) + unbindWalkKey('Left', West) + unbindWalkKey('Numpad8', North) + unbindWalkKey('Numpad9', NorthEast) + unbindWalkKey('Numpad6', East) + unbindWalkKey('Numpad3', SouthEast) + unbindWalkKey('Numpad2', South) + unbindWalkKey('Numpad1', SouthWest) + unbindWalkKey('Numpad4', West) + unbindWalkKey('Numpad7', NorthWest) + + unbindTurnKey('Ctrl+Up', North) + unbindTurnKey('Ctrl+Right', East) + unbindTurnKey('Ctrl+Down', South) + unbindTurnKey('Ctrl+Left', West) + unbindTurnKey('Ctrl+Numpad8', North) + unbindTurnKey('Ctrl+Numpad6', East) + unbindTurnKey('Ctrl+Numpad2', South) + unbindTurnKey('Ctrl+Numpad4', West) +end + +function enableWSAD() + if wsadWalking then + return + end + wsadWalking = true + local player = g_game.getLocalPlayer() + if player then + player:lockWalk(100) -- 100 ms walk lock for all directions + end + + bindWalkKey("W", North) + bindWalkKey("D", East) + bindWalkKey("S", South) + bindWalkKey("A", West) + + bindTurnKey("Ctrl+W", North) + bindTurnKey("Ctrl+D", East) + bindTurnKey("Ctrl+S", South) + bindTurnKey("Ctrl+A", West) + + bindWalkKey("E", NorthEast) + bindWalkKey("Q", NorthWest) + bindWalkKey("C", SouthEast) + bindWalkKey("Z", SouthWest) +end + +function disableWSAD() + if not wsadWalking then + return + end + wsadWalking = false + + unbindWalkKey("W") + unbindWalkKey("D") + unbindWalkKey("S") + unbindWalkKey("A") + + unbindTurnKey("Ctrl+W") + unbindTurnKey("Ctrl+D") + unbindTurnKey("Ctrl+S") + unbindTurnKey("Ctrl+A") + + unbindWalkKey("E") + unbindWalkKey("Q") + unbindWalkKey("C") + unbindWalkKey("Z") +end + +function bindWalkKey(key, dir) + local gameRootPanel = modules.game_interface.getRootPanel() + g_keyboard.bindKeyDown(key, function() changeWalkDir(dir) end, gameRootPanel, true) + g_keyboard.bindKeyUp(key, function() changeWalkDir(dir, true) end, gameRootPanel, true) + g_keyboard.bindKeyPress(key, function() smartWalk(dir) end, gameRootPanel) +end + +function unbindWalkKey(key) + local gameRootPanel = modules.game_interface.getRootPanel() + g_keyboard.unbindKeyDown(key, gameRootPanel) + g_keyboard.unbindKeyUp(key, gameRootPanel) + g_keyboard.unbindKeyPress(key, gameRootPanel) +end + +function bindTurnKey(key, dir) + local gameRootPanel = modules.game_interface.getRootPanel() + g_keyboard.bindKeyDown(key, function() turn(dir, false) end, gameRootPanel) + g_keyboard.bindKeyPress(key, function() turn(dir, true) end, gameRootPanel) + g_keyboard.bindKeyUp(key, function() local player = g_game.getLocalPlayer() if player then player:lockWalk(200) end end, gameRootPanel) +end + +function unbindTurnKey(key) + local gameRootPanel = modules.game_interface.getRootPanel() + g_keyboard.unbindKeyDown(key, gameRootPanel) + g_keyboard.unbindKeyPress(key, gameRootPanel) + g_keyboard.unbindKeyUp(key, gameRootPanel) +end + +function stopSmartWalk() + smartWalkDirs = {} + smartWalkDir = nil +end + +function changeWalkDir(dir, pop) + while table.removevalue(smartWalkDirs, dir) do end + if pop then + if #smartWalkDirs == 0 then + stopSmartWalk() + return + end + else + table.insert(smartWalkDirs, 1, dir) + end + + smartWalkDir = smartWalkDirs[1] + if modules.client_options.getOption('smartWalk') and #smartWalkDirs > 1 then + for _,d in pairs(smartWalkDirs) do + if (smartWalkDir == North and d == West) or (smartWalkDir == West and d == North) then + smartWalkDir = NorthWest + break + elseif (smartWalkDir == North and d == East) or (smartWalkDir == East and d == North) then + smartWalkDir = NorthEast + break + elseif (smartWalkDir == South and d == West) or (smartWalkDir == West and d == South) then + smartWalkDir = SouthWest + break + elseif (smartWalkDir == South and d == East) or (smartWalkDir == East and d == South) then + smartWalkDir = SouthEast + break + end + end + end +end + +function smartWalk(dir) + if g_keyboard.getModifiers() == KeyboardNoModifier then + local direction = smartWalkDir or dir + walk(direction) + return true + end + return false +end + +function canChangeFloorDown(pos) + pos.z = pos.z + 1 + toTile = g_map.getTile(pos) + return toTile and toTile:hasElevation(3) +end + +function canChangeFloorUp(pos) + pos.z = pos.z - 1 + toTile = g_map.getTile(pos) + return toTile and toTile:isWalkable() +end + +function onPositionChange(player, newPos, oldPos) + +end + +function onWalk(player, newPos, oldPos) + +end + +function onTeleport(player, newPos, oldPos) + if not newPos or not oldPos then + return + end + -- floor change is also teleport + if math.abs(newPos.x - oldPos.x) >= 3 or math.abs(newPos.y - oldPos.y) >= 3 or math.abs(newPos.z - oldPos.z) >= 2 then + -- far teleport, lock walk for 100ms + walkLock = g_clock.millis() + g_settings.getNumber('walkTeleportDelay') + else + walkLock = g_clock.millis() + g_settings.getNumber('walkStairsDelay') + end + nextWalkDir = nil -- cancel autowalk +end + +function onWalkFinish(player) + lastFinishedStep = g_clock.millis() + if nextWalkDir ~= nil then + removeEvent(autoWalkEvent) + autoWalkEvent = addEvent(function() if nextWalkDir ~= nil then walk(nextWalkDir) end end, false) + end +end + +function walk(dir) + local player = g_game.getLocalPlayer() + if not player or g_game.isDead() or player:isDead() then + return + end + + if g_game.isFollowing() then + g_game.cancelFollow() + end + + if player:isAutoWalking() or player:isServerWalking() then + if player:isAutoWalking() then + player:stopAutoWalk() + end + g_game.stop() + end + + if player:isWalkLocked() then + return + end + + local ticksToNextWalk = player:getStepTicksLeft() + if not player:canWalk(dir) then -- canWalk return false when previous walk is not finished or not confirmed by server + if ticksToNextWalk < 500 and lastWalkDir ~= dir then + nextWalkDir = dir + end + if ticksToNextWalk < 20 and lastFinishedStep + 400 > g_clock.millis() and nextWalkDir == nil then -- clicked walk 20 ms too early, try to execute again as soon possible to keep smooth walking + nextWalkDir = dir + end + return + end + + if nextWalkDir ~= nil and nextWalkDir ~= lastWalkDir then + dir = nextWalkDir + end + + local toPos = player:getNewPreWalkingPosition(true) + if dir == North then + toPos.y = toPos.y - 1 + elseif dir == East then + toPos.x = toPos.x + 1 + elseif dir == South then + toPos.y = toPos.y + 1 + elseif dir == West then + toPos.x = toPos.x - 1 + elseif dir == NorthEast then + toPos.x = toPos.x + 1 + toPos.y = toPos.y - 1 + elseif dir == SouthEast then + toPos.x = toPos.x + 1 + toPos.y = toPos.y + 1 + elseif dir == SouthWest then + toPos.x = toPos.x - 1 + toPos.y = toPos.y + 1 + elseif dir == NorthWest then + toPos.x = toPos.x - 1 + toPos.y = toPos.y - 1 + end + local toTile = g_map.getTile(toPos) + + if walkLock >= g_clock.millis() and lastWalkDir == dir then + return + end + + if firstStep and lastWalkDir == dir and lastWalk + g_settings.getNumber('walkFirstStepDelay') > g_clock.millis() then + firstStep = false + walkLock = lastWalk + g_settings.getNumber('walkFirstStepDelay') + return + end + + firstStep = not player:isWalking() and lastFinishedStep + 100 < g_clock.millis() and walkLock + 100 < g_clock.millis() + nextWalkDir = nil + removeEvent(autoWalkEvent) + autoWalkEvent = nil + + local preWalked = false + if toTile and toTile:isWalkable() then + if g_game.getFeature(GameNewWalking) then + player:newPreWalk(dir) + else + player:preWalk(dir) + end + preWalked = true + else + local playerTile = player:getTile() + if (playerTile and playerTile:hasElevation(3) and canChangeFloorUp(toPos)) or canChangeFloorDown(toPos) or (toTile and toTile:isEmpty() and not toTile:isBlocking()) then + player:lockWalk(100) + else + return + end + end + + g_game.callOnWalk(dir) + g_game.forceWalk(dir, preWalked) + + if not firstStep and lastWalkDir ~= dir then + walkLock = g_clock.millis() + g_settings.getNumber('walkTurnDelay') + end + + lastWalkDir = dir + lastWalk = g_clock.millis() +end + +function turn(dir, repeated) + if not repeated or (lastTurnDirection == dir and lastTurn + 50 < g_clock.millis()) then + g_game.turn(dir) + changeWalkDir(dir) + lastTurn = g_clock.millis() + if not repeated then + lastTurn = g_clock.millis() + 200 + end + lastTurnDirection = dir + end +end diff --git a/modules/game_walking/walking.otmod b/modules/game_walking/walking.otmod new file mode 100644 index 0000000..d722695 --- /dev/null +++ b/modules/game_walking/walking.otmod @@ -0,0 +1,8 @@ +Module + name: game_walking + description: Control walking and turns + author: otclient.ovh + website: http://otclient.ovh + scripts: [ walking ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/gamelib/const.lua b/modules/gamelib/const.lua new file mode 100644 index 0000000..35dd6c0 --- /dev/null +++ b/modules/gamelib/const.lua @@ -0,0 +1,359 @@ +-- @docconsts @{ + +FloorHigher = 0 +FloorLower = 15 + +SkullNone = 0 +SkullYellow = 1 +SkullGreen = 2 +SkullWhite = 3 +SkullRed = 4 +SkullBlack = 5 +SkullOrange = 6 + +ShieldNone = 0 +ShieldWhiteYellow = 1 +ShieldWhiteBlue = 2 +ShieldBlue = 3 +ShieldYellow = 4 +ShieldBlueSharedExp = 5 +ShieldYellowSharedExp = 6 +ShieldBlueNoSharedExpBlink = 7 +ShieldYellowNoSharedExpBlink = 8 +ShieldBlueNoSharedExp = 9 +ShieldYellowNoSharedExp = 10 +ShieldGray = 11 + +EmblemNone = 0 +EmblemGreen = 1 +EmblemRed = 2 +EmblemBlue = 3 +EmblemMember = 4 +EmblemOther = 5 + +VipIconFirst = 0 +VipIconLast = 10 + +Directions = { + North = 0, + East = 1, + South = 2, + West = 3, + NorthEast = 4, + SouthEast = 5, + SouthWest = 6, + NorthWest = 7 +} + +Skill = { + Fist = 0, + Club = 1, + Sword = 2, + Axe = 3, + Distance = 4, + Shielding = 5, + Fishing = 6, + CriticalChance = 7, + CriticalDamage = 8, + LifeLeechChance = 9, + LifeLeechAmount = 10, + ManaLeechChance = 11, + ManaLeechAmount = 12 +} + +North = Directions.North +East = Directions.East +South = Directions.South +West = Directions.West +NorthEast = Directions.NorthEast +SouthEast = Directions.SouthEast +SouthWest = Directions.SouthWest +NorthWest = Directions.NorthWest + +FightOffensive = 1 +FightBalanced = 2 +FightDefensive = 3 + +DontChase = 0 +ChaseOpponent = 1 + +PVPWhiteDove = 0 +PVPWhiteHand = 1 +PVPYellowHand = 2 +PVPRedFist = 3 + +GameProtocolChecksum = 1 +GameAccountNames = 2 +GameChallengeOnLogin = 3 +GamePenalityOnDeath = 4 +GameNameOnNpcTrade = 5 +GameDoubleFreeCapacity = 6 +GameDoubleExperience = 7 +GameTotalCapacity = 8 +GameSkillsBase = 9 +GamePlayerRegenerationTime = 10 +GameChannelPlayerList = 11 +GamePlayerMounts = 12 +GameEnvironmentEffect = 13 +GameCreatureEmblems = 14 +GameItemAnimationPhase = 15 +GameMagicEffectU16 = 16 +GamePlayerMarket = 17 +GameSpritesU32 = 18 +GameTileAddThingWithStackpos = 19 +GameOfflineTrainingTime = 20 +GamePurseSlot = 21 +GameFormatCreatureName = 22 +GameSpellList = 23 +GameClientPing = 24 +GameExtendedClientPing = 25 +GameDoubleHealth = 28 +GameDoubleSkills = 29 +GameChangeMapAwareRange = 30 +GameMapMovePosition = 31 +GameAttackSeq = 32 +GameBlueNpcNameColor = 33 +GameDiagonalAnimatedText = 34 +GameLoginPending = 35 +GameNewSpeedLaw = 36 +GameForceFirstAutoWalkStep = 37 +GameMinimapRemove = 38 +GameDoubleShopSellAmount = 39 +GameContainerPagination = 40 +GameThingMarks = 41 +GameLooktypeU16 = 42 +GamePlayerStamina = 43 +GamePlayerAddons = 44 +GameMessageStatements = 45 +GameMessageLevel = 46 +GameNewFluids = 47 +GamePlayerStateU16 = 48 +GameNewOutfitProtocol = 49 +GamePVPMode = 50 +GameWritableDate = 51 +GameAdditionalVipInfo = 52 +GameBaseSkillU16 = 53 +GameCreatureIcons = 54 +GameHideNpcNames = 55 +GameSpritesAlphaChannel = 56 +GamePremiumExpiration = 57 +GameBrowseField = 58 +GameEnhancedAnimations = 59 +GameOGLInformation = 60 +GameMessageSizeCheck = 61 +GamePreviewState = 62 +GameLoginPacketEncryption = 63 +GameClientVersion = 64 +GameContentRevision = 65 +GameExperienceBonus = 66 +GameAuthenticator = 67 +GameUnjustifiedPoints = 68 +GameSessionKey = 69 +GameDeathType = 70 +GameIdleAnimations = 71 +GameKeepUnawareTiles = 72 +GameIngameStore = 73 +GameIngameStoreHighlights = 74 +GameIngameStoreServiceType = 75 +GameAdditionalSkills = 76 + +GameExtendedOpcode = 80 + +GameNewWalking = 90 +GameSlowerManualWalking = 91 +GameExtendedNewWalking = 92 +GameBot = 95 +GameForceLight = 97 +GameNoDebug = 98 +GameBotProtection = 99 +GameFasterAnimations = 101 + +LastGameFeature = 110 + +TextColors = { + red = '#f55e5e', --'#c83200' + orange = '#f36500', --'#c87832' + yellow = '#ffff00', --'#e6c832' + green = '#00EB00', --'#3fbe32' + lightblue = '#5ff7f7', + blue = '#9f9dfd', + --blue1 = '#6e50dc', + --blue2 = '#3264c8', + --blue3 = '#0096c8', + white = '#ffffff', --'#bebebe' +} + +MessageModes = { + None = 0, + Say = 1, + Whisper = 2, + Yell = 3, + PrivateFrom = 4, + PrivateTo = 5, + ChannelManagement = 6, + Channel = 7, + ChannelHighlight = 8, + Spell = 9, + NpcFrom = 10, + NpcTo = 11, + GamemasterBroadcast = 12, + GamemasterChannel = 13, + GamemasterPrivateFrom = 14, + GamemasterPrivateTo = 15, + Login = 16, + Warning = 17, + Game = 18, + Failure = 19, + Look = 20, + DamageDealed = 21, + DamageReceived = 22, + Heal = 23, + Exp = 24, + DamageOthers = 25, + HealOthers = 26, + ExpOthers = 27, + Status = 28, + Loot = 29, + TradeNpc = 30, + Guild = 31, + PartyManagement = 32, + Party = 33, + BarkLow = 34, + BarkLoud = 35, + Report = 36, + HotkeyUse = 37, + TutorialHint = 38, + Thankyou = 39, + Market = 40, + Mana = 41, + BeyondLast = 42, + MonsterYell = 43, + MonsterSay = 44, + Red = 45, + Blue = 46, + RVRChannel = 47, + RVRAnswer = 48, + RVRContinue = 49, + GameHighlight = 50, + NpcFromStartBlock = 51, + Last = 52, + Invalid = 255, +} + +OTSERV_RSA = "1091201329673994292788609605089955415282375029027981291234687579" .. + "3726629149257644633073969600111060390723088861007265581882535850" .. + "3429057592827629436413108566029093628212635953836686562675849720" .. + "6207862794310902180176810615217550567108238764764442605581471797" .. + "07119674283982419152118103759076030616683978566631413" + +CIPSOFT_RSA = "1321277432058722840622950990822933849527763264961655079678763618" .. + "4334395343554449668205332383339435179772895415509701210392836078" .. + "6959821132214473291575712138800495033169914814069637740318278150" .. + "2907336840325241747827401343576296990629870233111328210165697754" .. + "88792221429527047321331896351555606801473202394175817" + +-- set to the latest Tibia.pic signature to make otclient compatible with official tibia +PIC_SIGNATURE = 0x56C5DDE7 + +OsTypes = { + Linux = 1, + Windows = 2, + Flash = 3, + OtclientLinux = 10, + OtclientWindows = 11, + OtclientMac = 12, +} + +PathFindResults = { + Ok = 0, + Position = 1, + Impossible = 2, + TooFar = 3, + NoWay = 4, +} + +PathFindFlags = { + AllowNullTiles = 1, + AllowCreatures = 2, + AllowNonPathable = 4, + AllowNonWalkable = 8, +} + +VipState = { + Offline = 0, + Online = 1, + Pending = 2, +} + +ExtendedIds = { + Activate = 0, + Locale = 1, + Ping = 2, + Sound = 3, + Game = 4, + Particles = 5, + MapShader = 6, + NeedsUpdate = 7 +} + +PreviewState = { + Default = 0, + Inactive = 1, + Active = 2 +} + +Blessings = { + None = 0, + Adventurer = 1, + SpiritualShielding = 2, + EmbraceOfTibia = 4, + FireOfSuns = 8, + WisdomOfSolitude = 16, + SparkOfPhoenix = 32 +} + +DeathType = { + Regular = 0, + Blessed = 1 +} + +ProductType = { + Other = 0, + NameChange = 1 +} + +StoreErrorType = { + NoError = -1, + PurchaseError = 0, + NetworkError = 1, + HistoryError = 2, + TransferError = 3, + Information = 4 +} + +StoreState = { + None = 0, + New = 1, + Sale = 2, + Timed = 3 +} + +AccountStatus = { + Ok = 0, + Frozen = 1, + Suspended = 2, +} + +SubscriptionStatus = { + Free = 0, + Premium = 1, +} + +ChannelEvent = { + Join = 0, + Leave = 1, + Invite = 2, + Exclude = 3, +} + +-- @} diff --git a/modules/gamelib/creature.lua b/modules/gamelib/creature.lua new file mode 100644 index 0000000..1c4c9ce --- /dev/null +++ b/modules/gamelib/creature.lua @@ -0,0 +1,170 @@ +-- @docclass Creature + +-- @docconsts @{ + +SkullNone = 0 +SkullYellow = 1 +SkullGreen = 2 +SkullWhite = 3 +SkullRed = 4 +SkullBlack = 5 +SkullOrange = 6 + +ShieldNone = 0 +ShieldWhiteYellow = 1 +ShieldWhiteBlue = 2 +ShieldBlue = 3 +ShieldYellow = 4 +ShieldBlueSharedExp = 5 +ShieldYellowSharedExp = 6 +ShieldBlueNoSharedExpBlink = 7 +ShieldYellowNoSharedExpBlink = 8 +ShieldBlueNoSharedExp = 9 +ShieldYellowNoSharedExp = 10 + +EmblemNone = 0 +EmblemGreen = 1 +EmblemRed = 2 +EmblemBlue = 3 + +NpcIconNone = 0 +NpcIconChat = 1 +NpcIconTrade = 2 +NpcIconQuest = 3 +NpcIconTradeQuest = 4 + +CreatureTypePlayer = 0 +CreatureTypeMonster = 1 +CreatureTypeNpc = 2 +CreatureTypeSummonOwn = 3 +CreatureTypeSummonOther = 4 + +-- @} + +function getNextSkullId(skullId) + if skullId == SkullRed or skullId == SkullBlack then + return SkullBlack + end + return SkullRed +end + +function getSkullImagePath(skullId) + local path + if skullId == SkullYellow then + path = '/images/game/skulls/skull_yellow' + elseif skullId == SkullGreen then + path = '/images/game/skulls/skull_green' + elseif skullId == SkullWhite then + path = '/images/game/skulls/skull_white' + elseif skullId == SkullRed then + path = '/images/game/skulls/skull_red' + elseif skullId == SkullBlack then + path = '/images/game/skulls/skull_black' + elseif skullId == SkullOrange then + path = '/images/game/skulls/skull_orange' + end + return path +end + +function getShieldImagePathAndBlink(shieldId) + local path, blink + if shieldId == ShieldWhiteYellow then + path, blink = '/images/game/shields/shield_yellow_white', false + elseif shieldId == ShieldWhiteBlue then + path, blink = '/images/game/shields/shield_blue_white', false + elseif shieldId == ShieldBlue then + path, blink = '/images/game/shields/shield_blue', false + elseif shieldId == ShieldYellow then + path, blink = '/images/game/shields/shield_yellow', false + elseif shieldId == ShieldBlueSharedExp then + path, blink = '/images/game/shields/shield_blue_shared', false + elseif shieldId == ShieldYellowSharedExp then + path, blink = '/images/game/shields/shield_yellow_shared', false + elseif shieldId == ShieldBlueNoSharedExpBlink then + path, blink = '/images/game/shields/shield_blue_not_shared', true + elseif shieldId == ShieldYellowNoSharedExpBlink then + path, blink = '/images/game/shields/shield_yellow_not_shared', true + elseif shieldId == ShieldBlueNoSharedExp then + path, blink = '/images/game/shields/shield_blue_not_shared', false + elseif shieldId == ShieldYellowNoSharedExp then + path, blink = '/images/game/shields/shield_yellow_not_shared', false + elseif shieldId == ShieldGray then + path, blink = '/images/game/shields/shield_gray', false + end + return path, blink +end + +function getEmblemImagePath(emblemId) + local path + if emblemId == EmblemGreen then + path = '/images/game/emblems/emblem_green' + elseif emblemId == EmblemRed then + path = '/images/game/emblems/emblem_red' + elseif emblemId == EmblemBlue then + path = '/images/game/emblems/emblem_blue' + elseif emblemId == EmblemMember then + path = '/images/game/emblems/emblem_member' + elseif emblemId == EmblemOther then + path = '/images/game/emblems/emblem_other' + end + return path +end + +function getTypeImagePath(creatureType) + local path + if creatureType == CreatureTypeSummonOwn then + path = '/images/game/creaturetype/summon_own' + elseif creatureType == CreatureTypeSummonOther then + path = '/images/game/creaturetype/summon_other' + end + return path +end + +function getIconImagePath(iconId) + local path + if iconId == NpcIconChat then + path = '/images/game/npcicons/icon_chat' + elseif iconId == NpcIconTrade then + path = '/images/game/npcicons/icon_trade' + elseif iconId == NpcIconQuest then + path = '/images/game/npcicons/icon_quest' + elseif iconId == NpcIconTradeQuest then + path = '/images/game/npcicons/icon_tradequest' + end + return path +end + +function Creature:onSkullChange(skullId) + local imagePath = getSkullImagePath(skullId) + if imagePath then + self:setSkullTexture(imagePath) + end +end + +function Creature:onShieldChange(shieldId) + local imagePath, blink = getShieldImagePathAndBlink(shieldId) + if imagePath then + self:setShieldTexture(imagePath, blink) + end +end + +function Creature:onEmblemChange(emblemId) + local imagePath = getEmblemImagePath(emblemId) + if imagePath then + self:setEmblemTexture(imagePath) + end +end + +function Creature:onTypeChange(typeId) + local imagePath = getTypeImagePath(typeId) + if imagePath then + self:setTypeTexture(imagePath) + end +end + +function Creature:onIconChange(iconId) + local imagePath = getIconImagePath(iconId) + if imagePath then + self:setIconTexture(imagePath) + end +end diff --git a/modules/gamelib/game.lua b/modules/gamelib/game.lua new file mode 100644 index 0000000..816685c --- /dev/null +++ b/modules/gamelib/game.lua @@ -0,0 +1,118 @@ +function g_game.getRsa() + return G.currentRsa +end + +function g_game.findPlayerItem(itemId, subType) + local localPlayer = g_game.getLocalPlayer() + if localPlayer then + for slot = InventorySlotFirst, InventorySlotLast do + local item = localPlayer:getInventoryItem(slot) + if item and item:getId() == itemId and (subType == -1 or item:getSubType() == subType) then + return item + end + end + end + + return g_game.findItemInContainers(itemId, subType) +end + +function g_game.chooseRsa(host) + if G.currentRsa ~= CIPSOFT_RSA and G.currentRsa ~= OTSERV_RSA then return end + if host:ends('.tibia.com') or host:ends('.cipsoft.com') then + g_game.setRsa(CIPSOFT_RSA) + + if g_app.getOs() == 'windows' then + g_game.setCustomOs(OsTypes.Windows) + else + g_game.setCustomOs(OsTypes.Linux) + end + else + if G.currentRsa == CIPSOFT_RSA then + g_game.setCustomOs(-1) + end + g_game.setRsa(OTSERV_RSA) + end + + -- Hack fix to resolve some 760 login issues + if g_game.getClientVersion() <= 760 then + g_game.setCustomOs(2) + end +end + +function g_game.setRsa(rsa, e) + e = e or '65537' + g_crypt.rsaSetPublicKey(rsa, e) + G.currentRsa = rsa +end + +function g_game.isOfficialTibia() + return G.currentRsa == CIPSOFT_RSA +end + +function g_game.getSupportedClients() + return { + 740, 741, 750, 760, 770, 772, + 780, 781, 782, 790, 792, + + 800, 810, 811, 820, 821, 822, + 830, 831, 840, 842, 850, 853, + 854, 855, 857, 860, 861, 862, + 870, 871, + + 900, 910, 920, 931, 940, 943, + 944, 951, 952, 953, 954, 960, + 961, 963, 970, 971, 972, 973, + 980, 981, 982, 983, 984, 985, + 986, + + 1000, 1001, 1002, 1010, 1011, + 1012, 1013, 1020, 1021, 1022, + 1030, 1031, 1032, 1033, 1034, + 1035, 1036, 1037, 1038, 1039, + 1040, 1041, 1050, 1051, 1052, + 1053, 1054, 1055, 1056, 1057, + 1058, 1059, 1060, 1061, 1062, + 1063, 1064, 1070, 1071, 1072, + 1073, 1074, 1075, 1076, 1080, + 1081, 1082, 1090, 1091, 1092, + 1093, 1094, 1095, 1096, 1097, + 1098, 1099 + } +end + +-- The client version and protocol version where +-- unsynchronized for some releases, not sure if this +-- will be the normal standard. + +-- Client Version: Publicly given version when +-- downloading Cipsoft client. + +-- Protocol Version: Previously was the same as +-- the client version, but was unsychronized in some +-- releases, now it needs to be verified and added here +-- if it does not match the client version. + +-- Reason for defining both: The server now requires a +-- Client version and Protocol version from the client. + +-- Important: Use getClientVersion for specific protocol +-- features to ensure we are using the proper version. + +function g_game.getClientProtocolVersion(client) + local clients = { + [980] = 971, + [981] = 973, + [982] = 974, + [983] = 975, + [984] = 976, + [985] = 977, + [986] = 978, + [1001] = 979, + [1002] = 980 + } + return clients[client] or client +end + +if not G.currentRsa then + g_game.setRsa(OTSERV_RSA) +end diff --git a/modules/gamelib/gamelib.otmod b/modules/gamelib/gamelib.otmod new file mode 100644 index 0000000..ceb9523 --- /dev/null +++ b/modules/gamelib/gamelib.otmod @@ -0,0 +1,27 @@ +Module + name: gamelib + description: Contains game related classes + author: OTClient team + website: https://github.com/edubart/otclient + + dependencies: + - game_features + - game_things + + @onLoad: | + dofile 'const' + dofile 'util' + dofile 'protocol' + dofile 'protocollogin' + dofile 'protocolgame' + dofile 'position' + dofile 'game' + + dofile 'creature' + dofile 'player' + dofile 'market' + dofile 'textmessages' + dofile 'thing' + dofile 'spells' + + dofiles 'ui' diff --git a/modules/gamelib/market.lua b/modules/gamelib/market.lua new file mode 100644 index 0000000..c1764b3 --- /dev/null +++ b/modules/gamelib/market.lua @@ -0,0 +1,184 @@ +MarketMaxAmount = 2000 +MarketMaxAmountStackable = 64000 +MarketMaxPrice = 999999999 +MarketMaxOffers = 100 + +MarketAction = { + Buy = 0, + Sell = 1 +} + +MarketRequest = { + MyOffers = 0xFFFE, + MyHistory = 0xFFFF +} + +MarketOfferState = { + Active = 0, + Cancelled = 1, + Expired = 2, + Accepted = 3, + AcceptedEx = 255 +} + +MarketCategory = { + All = 0, + Armors = 1, + Amulets = 2, + Boots = 3, + Containers = 4, + Decoration = 5, + Food = 6, + HelmetsHats = 7, + Legs = 8, + Others = 9, + Potions = 10, + Rings = 11, + Runes = 12, + Shields = 13, + Tools = 14, + Valuables = 15, + Ammunition = 16, + Axes = 17, + Clubs = 18, + DistanceWeapons = 19, + Swords = 20, + WandsRods = 21, + PremiumScrolls = 22, + TibiaCoins = 23, + MetaWeapons = 255 +} + +MarketCategory.First = MarketCategory.Armors +MarketCategory.Last = MarketCategory.TibiaCoins + +MarketCategoryWeapons = { + [MarketCategory.Ammunition] = { slots = {255} }, + [MarketCategory.Axes] = { slots = {255, InventorySlotOther, InventorySlotLeft} }, + [MarketCategory.Clubs] = { slots = {255, InventorySlotOther, InventorySlotLeft} }, + [MarketCategory.DistanceWeapons] = { slots = {255, InventorySlotOther, InventorySlotLeft} }, + [MarketCategory.Swords] = { slots = {255, InventorySlotOther, InventorySlotLeft} }, + [MarketCategory.WandsRods] = { slots = {255, InventorySlotOther, InventorySlotLeft} } +} + +MarketCategoryStrings = { + [0] = 'All', + [1] = 'Armors', + [2] = 'Amulets', + [3] = 'Boots', + [4] = 'Containers', + [5] = 'Decoration', + [6] = 'Food', + [7] = 'Helmets and Hats', + [8] = 'Legs', + [9] = 'Others', + [10] = 'Potions', + [11] = 'Rings', + [12] = 'Runes', + [13] = 'Shields', + [14] = 'Tools', + [15] = 'Valuables', + [16] = 'Ammunition', + [17] = 'Axes', + [18] = 'Clubs', + [19] = 'Distance Weapons', + [20] = 'Swords', + [21] = 'Wands and Rods', + [22] = 'Premium Scrolls', + [23] = 'Tibia Coins', + [255] = 'Weapons' +} + +function getMarketCategoryName(id) + if table.haskey(MarketCategoryStrings, id) then + return MarketCategoryStrings[id] + end +end + +function getMarketCategoryId(name) + local id = table.find(MarketCategoryStrings, name) + if id then + return id + end +end + +MarketItemDescription = { + Armor = 1, + Attack = 2, + Container = 3, + Defense = 4, + General = 5, + DecayTime = 6, + Combat = 7, + MinLevel = 8, + MinMagicLevel = 9, + Vocation = 10, + Rune = 11, + Ability = 12, + Charges = 13, + WeaponName = 14, + Weight = 15 +} + +MarketItemDescription.First = MarketItemDescription.Armor +MarketItemDescription.Last = MarketItemDescription.Weight + +MarketItemDescriptionStrings = { + [1] = 'Armor', + [2] = 'Attack', + [3] = 'Container', + [4] = 'Defense', + [5] = 'Description', + [6] = 'Use Time', + [7] = 'Combat', + [8] = 'Min Level', + [9] = 'Min Magic Level', + [10] = 'Vocation', + [11] = 'Rune', + [12] = 'Ability', + [13] = 'Charges', + [14] = 'Weapon Type', + [15] = 'Weight' +} + +function getMarketDescriptionName(id) + if table.haskey(MarketItemDescriptionStrings, id) then + return MarketItemDescriptionStrings[id] + end +end + +function getMarketDescriptionId(name) + local id = table.find(MarketItemDescriptionStrings, name) + if id then + return id + end +end + +MarketSlotFilters = { + [InventorySlotOther] = "Two-Handed", + [InventorySlotLeft] = "One-Handed", + [255] = "Any" +} + +MarketFilters = { + Vocation = 1, + Level = 2, + Depot = 3, + SearchAll = 4 +} + +MarketFilters.First = MarketFilters.Vocation +MarketFilters.Last = MarketFilters.Depot + +function getMarketSlotFilterId(name) + local id = table.find(MarketSlotFilters, name) + if id then + return id + end +end + +function getMarketSlotFilterName(id) + if table.haskey(MarketSlotFilters, id) then + return MarketSlotFilters[id] + end +end diff --git a/modules/gamelib/player.lua b/modules/gamelib/player.lua new file mode 100644 index 0000000..1ac7291 --- /dev/null +++ b/modules/gamelib/player.lua @@ -0,0 +1,151 @@ +-- @docclass Player + +PlayerStates = { + None = 0, + Poison = 1, + Burn = 2, + Energy = 4, + Drunk = 8, + ManaShield = 16, + Paralyze = 32, + Haste = 64, + Swords = 128, + Drowning = 256, + Freezing = 512, + Dazzled = 1024, + Cursed = 2048, + PartyBuff = 4096, + PzBlock = 8192, + Pz = 16384, + Bleeding = 32768, + Hungry = 65536 +} + +InventorySlotOther = 0 +InventorySlotHead = 1 +InventorySlotNeck = 2 +InventorySlotBack = 3 +InventorySlotBody = 4 +InventorySlotRight = 5 +InventorySlotLeft = 6 +InventorySlotLeg = 7 +InventorySlotFeet = 8 +InventorySlotFinger = 9 +InventorySlotAmmo = 10 +InventorySlotPurse = 11 + +InventorySlotFirst = 1 +InventorySlotLast = 10 + +function Player:isPartyLeader() + local shield = self:getShield() + return (shield == ShieldWhiteYellow or + shield == ShieldYellow or + shield == ShieldYellowSharedExp or + shield == ShieldYellowNoSharedExpBlink or + shield == ShieldYellowNoSharedExp) +end + +function Player:isPartyMember() + local shield = self:getShield() + return (shield == ShieldWhiteYellow or + shield == ShieldYellow or + shield == ShieldYellowSharedExp or + shield == ShieldYellowNoSharedExpBlink or + shield == ShieldYellowNoSharedExp or + shield == ShieldBlueSharedExp or + shield == ShieldBlueNoSharedExpBlink or + shield == ShieldBlueNoSharedExp or + shield == ShieldBlue) +end + +function Player:isPartySharedExperienceActive() + local shield = self:getShield() + return (shield == ShieldYellowSharedExp or + shield == ShieldYellowNoSharedExpBlink or + shield == ShieldYellowNoSharedExp or + shield == ShieldBlueSharedExp or + shield == ShieldBlueNoSharedExpBlink or + shield == ShieldBlueNoSharedExp) +end + +function Player:hasVip(creatureName) + for id, vip in pairs(g_game.getVips()) do + if (vip[1] == creatureName) then return true end + end + return false +end + +function Player:isMounted() + local outfit = self:getOutfit() + return outfit.mount ~= nil and outfit.mount > 0 +end + +function Player:toggleMount() + if g_game.getFeature(GamePlayerMounts) then + g_game.mount(not self:isMounted()) + end +end + +function Player:mount() + if g_game.getFeature(GamePlayerMounts) then + g_game.mount(true) + end +end + +function Player:dismount() + if g_game.getFeature(GamePlayerMounts) then + g_game.mount(false) + end +end + +function Player:getItem(itemId, subType) + return g_game.findPlayerItem(itemId, subType or -1) +end + +function Player:getItems(itemId, subType) + local subType = subType or -1 + + local items = {} + for i=InventorySlotFirst,InventorySlotLast do + local item = self:getInventoryItem(i) + if item and item:getId() == itemId and (subType == -1 or item:getSubType() == subType) then + table.insert(items, item) + end + end + + for i, container in pairs(g_game.getContainers()) do + for j, item in pairs(container:getItems()) do + if item:getId() == itemId and (subType == -1 or item:getSubType() == subType) then + item.container = container + table.insert(items, item) + end + end + end + return items +end + +function Player:getItemsCount(itemId) + local items, count = self:getItems(itemId), 0 + for i=1,#items do + count = count + items[i]:getCount() + end + return count +end + +function Player:hasState(state, states) + if not states then + states = self:getStates() + end + + for i = 1, 32 do + local pow = math.pow(2, i-1) + if pow > states then break end + + local states = bit32.band(states, pow) + if states == state then + return true + end + end + return false +end diff --git a/modules/gamelib/position.lua b/modules/gamelib/position.lua new file mode 100644 index 0000000..b41a0fa --- /dev/null +++ b/modules/gamelib/position.lua @@ -0,0 +1,37 @@ +Position = {} + +function Position.equals(pos1, pos2) + return pos1.x == pos2.x and pos1.y == pos2.y and pos1.z == pos2.z +end + +function Position.greaterThan(pos1, pos2, orEqualTo) + if orEqualTo then + return pos1.x >= pos2.x or pos1.y >= pos2.y or pos1.z >= pos2.z + else + return pos1.x > pos2.x or pos1.y > pos2.y or pos1.z > pos2.z + end +end + +function Position.lessThan(pos1, pos2, orEqualTo) + if orEqualTo then + return pos1.x <= pos2.x or pos1.y <= pos2.y or pos1.z <= pos2.z + else + return pos1.x < pos2.x or pos1.y < pos2.y or pos1.z < pos2.z + end +end + +function Position.isInRange(pos1, pos2, xRange, yRange) + return math.abs(pos1.x-pos2.x) <= xRange and math.abs(pos1.y-pos2.y) <= yRange and pos1.z == pos2.z; +end + +function Position.isValid(pos) + return not (pos.x == 65535 and pos.y == 65535 and pos.z == 255) +end + +function Position.distance(pos1, pos2) + return math.sqrt(math.pow((pos2.x - pos1.x), 2) + math.pow((pos2.y - pos1.y), 2)) +end + +function Position.manhattanDistance(pos1, pos2) + return math.abs(pos2.x - pos1.x) + math.abs(pos2.y - pos1.y) +end \ No newline at end of file diff --git a/modules/gamelib/protocol.lua b/modules/gamelib/protocol.lua new file mode 100644 index 0000000..205a938 --- /dev/null +++ b/modules/gamelib/protocol.lua @@ -0,0 +1,206 @@ +GameServerOpcodes = { + GameServerInitGame = 10, + GameServerGMActions = 11, + GameServerEnterGame = 15, + GameServerLoginError = 20, + GameServerLoginAdvice = 21, + GameServerLoginWait = 22, + GameServerAddCreature = 23, + GameServerPingBack = 29, + GameServerPing = 30, + GameServerChallenge = 31, + GameServerDeath = 40, + + -- all in game opcodes must be greater than 50 + GameServerFirstGameOpcode = 50, + + -- otclient ONLY + GameServerExtendedOpcode = 50, + + -- NOTE: add any custom opcodes in this range + -- 51 - 99 + + -- original tibia ONLY + GameServerFullMap = 100, + GameServerMapTopRow = 101, + GameServerMapRightRow = 102, + GameServerMapBottomRow = 103, + GameServerMapLeftRow = 104, + GameServerUpdateTile = 105, + GameServerCreateOnMap = 106, + GameServerChangeOnMap = 107, + GameServerDeleteOnMap = 108, + GameServerMoveCreature = 109, + GameServerOpenContainer = 110, + GameServerCloseContainer = 111, + GameServerCreateContainer = 112, + GameServerChangeInContainer = 113, + GameServerDeleteInContainer = 114, + GameServerSetInventory = 120, + GameServerDeleteInventory = 121, + GameServerOpenNpcTrade = 122, + GameServerPlayerGoods = 123, + GameServerCloseNpcTrade = 124, + GameServerOwnTrade = 125, + GameServerCounterTrade = 126, + GameServerCloseTrade = 127, + GameServerAmbient = 130, + GameServerGraphicalEffect = 131, + GameServerTextEffect = 132, + GameServerMissleEffect = 133, + GameServerMarkCreature = 134, + GameServerTrappers = 135, + GameServerCreatureHealth = 140, + GameServerCreatureLight = 141, + GameServerCreatureOutfit = 142, + GameServerCreatureSpeed = 143, + GameServerCreatureSkull = 144, + GameServerCreatureParty = 145, + GameServerCreatureUnpass = 146, + GameServerEditText = 150, + GameServerEditList = 151, + GameServerPlayerDataBasic = 159, -- 910 + GameServerPlayerData = 160, + GameServerPlayerSkills = 161, + GameServerPlayerState = 162, + GameServerClearTarget = 163, + GameServerSpellDelay = 164, -- 870 + GameServerSpellGroupDelay = 165, -- 870 + GameServerMultiUseDelay = 166, -- 870 + GameServerTalk = 170, + GameServerChannels = 171, + GameServerOpenChannel = 172, + GameServerOpenPrivateChannel = 173, + GameServerRuleViolationChannel = 174, + GameServerRuleViolationRemove = 175, + GameServerRuleViolationCancel = 176, + GameServerRuleViolationLock = 177, + GameServerOpenOwnChannel = 178, + GameServerCloseChannel = 179, + GameServerTextMessage = 180, + GameServerCancelWalk = 181, + GameServerWalkWait = 182, + GameServerFloorChangeUp = 190, + GameServerFloorChangeDown = 191, + GameServerChooseOutfit = 200, + GameServerVipAdd = 210, + GameServerVipLogin = 211, + GameServerVipLogout = 212, + GameServerTutorialHint = 220, + GameServerAutomapFlag = 221, + GameServerCoinBalance = 223, -- 1080 + GameServerStoreError = 224, -- 1080 + GameServerRequestPurchaseData = 225, -- 1080 + GameServerQuestLog = 240, + GameServerQuestLine = 241, + GameServerCoinBalanceUpdating = 242, -- 1080 + GameServerChannelEvent = 243, -- 910 + GameServerItemInfo = 244, -- 910 + GameServerPlayerInventory = 245, -- 910 + GameServerMarketEnter = 246, -- 944 + GameServerMarketLeave = 247, -- 944 + GameServerMarketDetail = 248, -- 944 + GameServerMarketBrowse = 249, -- 944 + GameServerShowModalDialog = 250, -- 960 + GameServerStore = 251, -- 1080 + GameServerStoreOffers = 252, -- 1080 + GameServerStoreTransactionHistory = 253, -- 1080 + GameServerStoreCompletePurchase = 254 -- 1080 +} + +ClientOpcodes = { + ClientEnterAccount = 1, + ClientEnterGame = 10, + ClientLeaveGame = 20, + ClientPing = 29, + ClientPingBack = 30, + + -- all in game opcodes must be equal or greater than 50 + ClientFirstGameOpcode = 50, + + -- otclient ONLY + ClientExtendedOpcode = 50, + + -- NOTE: add any custom opcodes in this range + -- 51 - 99 + + -- original tibia ONLY + ClientAutoWalk = 100, + ClientWalkNorth = 101, + ClientWalkEast = 102, + ClientWalkSouth = 103, + ClientWalkWest = 104, + ClientStop = 105, + ClientWalkNorthEast = 106, + ClientWalkSouthEast = 107, + ClientWalkSouthWest = 108, + ClientWalkNorthWest = 109, + ClientTurnNorth = 111, + ClientTurnEast = 112, + ClientTurnSouth = 113, + ClientTurnWest = 114, + ClientEquipItem = 119, -- 910 + ClientMove = 120, + ClientInspectNpcTrade = 121, + ClientBuyItem = 122, + ClientSellItem = 123, + ClientCloseNpcTrade = 124, + ClientRequestTrade = 125, + ClientInspectTrade = 126, + ClientAcceptTrade = 127, + ClientRejectTrade = 128, + ClientUseItem = 130, + ClientUseItemWith = 131, + ClientUseOnCreature = 132, + ClientRotateItem = 133, + ClientCloseContainer = 135, + ClientUpContainer = 136, + ClientEditText = 137, + ClientEditList = 138, + ClientLook = 140, + ClientTalk = 150, + ClientRequestChannels = 151, + ClientJoinChannel = 152, + ClientLeaveChannel = 153, + ClientOpenPrivateChannel = 154, + ClientCloseNpcChannel = 158, + ClientChangeFightModes = 160, + ClientAttack = 161, + ClientFollow = 162, + ClientInviteToParty = 163, + ClientJoinParty = 164, + ClientRevokeInvitation = 165, + ClientPassLeadership = 166, + ClientLeaveParty = 167, + ClientShareExperience = 168, + ClientDisbandParty = 169, + ClientOpenOwnChannel = 170, + ClientInviteToOwnChannel = 171, + ClientExcludeFromOwnChannel = 172, + ClientCancelAttackAndFollow = 190, + ClientRefreshContainer = 202, + ClientRequestOutfit = 210, + ClientChangeOutfit = 211, + ClientMount = 212, -- 870 + ClientAddVip = 220, + ClientRemoveVip = 221, + ClientBugReport = 230, + ClientRuleViolation = 231, + ClientDebugReport = 232, + ClientTransferCoins = 239, -- 1080 + ClientRequestQuestLog = 240, + ClientRequestQuestLine = 241, + ClientNewRuleViolation = 242, -- 910 + ClientRequestItemInfo = 243, -- 910 + ClientMarketLeave = 244, -- 944 + ClientMarketBrowse = 245, -- 944 + ClientMarketCreate = 246, -- 944 + ClientMarketCancel = 247, -- 944 + ClientMarketAccept = 248, -- 944 + ClientAnswerModalDialog = 249, -- 960 + ClientOpenStore = 250, -- 1080 + ClientRequestStoreOffers = 251, -- 1080 + ClientBuyStoreOffer = 252, -- 1080 + ClientOpenTransactionHistory = 253, -- 1080 + ClientRequestTransactionHistory = 254 -- 1080 +} diff --git a/modules/gamelib/protocolgame.lua b/modules/gamelib/protocolgame.lua new file mode 100644 index 0000000..a312de5 --- /dev/null +++ b/modules/gamelib/protocolgame.lua @@ -0,0 +1,59 @@ +local opcodeCallbacks = {} +local extendedCallbacks = {} + +function ProtocolGame:onOpcode(opcode, msg) + for i, callback in pairs(opcodeCallbacks) do + if i == opcode then + callback(self, msg) + return true + end + end + return false +end + +function ProtocolGame:onExtendedOpcode(opcode, buffer) + local callback = extendedCallbacks[opcode] + if callback then + callback(self, opcode, buffer) + end +end + +function ProtocolGame.registerOpcode(opcode, callback) + if opcodeCallbacks[opcode] then + error('opcode ' .. opcode .. ' already registered will be overriden') + end + + opcodeCallbacks[opcode] = callback +end + +function ProtocolGame.unregisterOpcode(opcode) + opcodeCallbacks[opcode] = nil +end + +function ProtocolGame.registerExtendedOpcode(opcode, callback) + if not callback or type(callback) ~= 'function' then + error('Invalid callback.') + end + + if opcode < 0 or opcode > 255 then + error('Invalid opcode. Range: 0-255') + end + + if extendedCallbacks[opcode] then + error('Opcode is already taken.') + end + + extendedCallbacks[opcode] = callback +end + +function ProtocolGame.unregisterExtendedOpcode(opcode) + if opcode < 0 or opcode > 255 then + error('Invalid opcode. Range: 0-255') + end + + if not extendedCallbacks[opcode] then + error('Opcode is not registered.') + end + + extendedCallbacks[opcode] = nil +end diff --git a/modules/gamelib/protocollogin.lua b/modules/gamelib/protocollogin.lua new file mode 100644 index 0000000..44e28dc --- /dev/null +++ b/modules/gamelib/protocollogin.lua @@ -0,0 +1,277 @@ +-- @docclass +ProtocolLogin = extends(Protocol, "ProtocolLogin") + +LoginServerError = 10 +LoginServerTokenSuccess = 12 +LoginServerTokenError = 13 +LoginServerUpdate = 17 +LoginServerMotd = 20 +LoginServerUpdateNeeded = 30 +LoginServerSessionKey = 40 +LoginServerCharacterList = 100 +LoginServerExtendedCharacterList = 101 + +-- Since 10.76 +LoginServerRetry = 10 +LoginServerErrorNew = 11 + +function ProtocolLogin:login(host, port, accountName, accountPassword, authenticatorToken, stayLogged) + if string.len(host) == 0 or port == nil or port == 0 then + signalcall(self.onLoginError, self, tr("You must enter a valid server address and port.")) + return + end + + self.accountName = accountName + self.accountPassword = accountPassword + self.authenticatorToken = authenticatorToken + self.stayLogged = stayLogged + self.connectCallback = self.sendLoginPacket + + self:connect(host, port) +end + +function ProtocolLogin:cancelLogin() + self:disconnect() +end + +function ProtocolLogin:sendLoginPacket() + local msg = OutputMessage.create() + msg:addU8(ClientOpcodes.ClientEnterAccount) + msg:addU16(g_game.getOs()) + if g_game.getCustomProtocolVersion() > 0 then + msg:addU16(g_game.getCustomProtocolVersion()) + else + msg:addU16(g_game.getProtocolVersion()) + end + + if g_game.getFeature(GameClientVersion) then + msg:addU32(g_game.getClientVersion()) + end + + if g_game.getFeature(GameContentRevision) then + msg:addU16(g_things.getContentRevision()) + msg:addU16(0) + else + msg:addU32(g_things.getDatSignature()) + end + msg:addU32(g_sprites.getSprSignature()) + msg:addU32(PIC_SIGNATURE) + + if g_game.getFeature(GamePreviewState) then + msg:addU8(0) + end + + local offset = msg:getMessageSize() + if g_game.getFeature(GameLoginPacketEncryption) then + -- first RSA byte must be 0 + msg:addU8(0) + + -- xtea key + self:generateXteaKey() + local xteaKey = self:getXteaKey() + msg:addU32(xteaKey[1]) + msg:addU32(xteaKey[2]) + msg:addU32(xteaKey[3]) + msg:addU32(xteaKey[4]) + end + + if g_game.getFeature(GameAccountNames) then + msg:addString(self.accountName) + else + msg:addU32(tonumber(self.accountName)) + end + + msg:addString(self.accountPassword) + + if self.getLoginExtendedData then + local data = self:getLoginExtendedData() + msg:addString(data) + end + + local paddingBytes = g_crypt.rsaGetSize() - (msg:getMessageSize() - offset) + assert(paddingBytes >= 0) + for i = 1, paddingBytes do + msg:addU8(math.random(0, 0xff)) + end + + if g_game.getFeature(GameLoginPacketEncryption) then + msg:encryptRsa() + end + + if g_game.getFeature(GameOGLInformation) then + msg:addU8(1) --unknown + msg:addU8(1) --unknown + + if g_game.getClientVersion() >= 1072 then + msg:addString(string.format('%s %s', g_graphics.getVendor(), g_graphics.getRenderer())) + else + msg:addString(g_graphics.getRenderer()) + end + msg:addString(g_graphics.getVersion()) + end + + -- add RSA encrypted auth token + if g_game.getFeature(GameAuthenticator) then + offset = msg:getMessageSize() + + -- first RSA byte must be 0 + msg:addU8(0) + msg:addString(self.authenticatorToken) + + if g_game.getFeature(GameSessionKey) then + msg:addU8(booleantonumber(self.stayLogged)) + end + + paddingBytes = g_crypt.rsaGetSize() - (msg:getMessageSize() - offset) + assert(paddingBytes >= 0) + for i = 1, paddingBytes do + msg:addU8(math.random(0, 0xff)) + end + + msg:encryptRsa() + end + + if g_game.getFeature(GameProtocolChecksum) then + self:enableChecksum() + end + + self:send(msg) + if g_game.getFeature(GameLoginPacketEncryption) then + self:enableXteaEncryption() + end + self:recv() +end + +function ProtocolLogin:onConnect() + self.gotConnection = true + self:connectCallback() + self.connectCallback = nil +end + +function ProtocolLogin:onRecv(msg) + while not msg:eof() do + local opcode = msg:getU8() + if opcode == LoginServerErrorNew then + self:parseError(msg) + elseif opcode == LoginServerError then + self:parseError(msg) + elseif opcode == LoginServerMotd then + self:parseMotd(msg) + elseif opcode == LoginServerUpdateNeeded then + signalcall(self.onLoginError, self, tr("Client needs update.")) + elseif opcode == LoginServerTokenSuccess then + local unknown = msg:getU8() + elseif opcode == LoginServerTokenError then + -- TODO: prompt for token here + local unknown = msg:getU8() + signalcall(self.onLoginError, self, tr("Invalid authentification token.")) + elseif opcode == LoginServerCharacterList then + self:parseCharacterList(msg) + elseif opcode == LoginServerExtendedCharacterList then + self:parseExtendedCharacterList(msg) + elseif opcode == LoginServerUpdate then + local signature = msg:getString() + signalcall(self.onUpdateNeeded, self, signature) + elseif opcode == LoginServerSessionKey then + self:parseSessionKey(msg) + else + self:parseOpcode(opcode, msg) + end + end + self:disconnect() +end + +function ProtocolLogin:parseError(msg) + local errorMessage = msg:getString() + signalcall(self.onLoginError, self, errorMessage) +end + +function ProtocolLogin:parseMotd(msg) + local motd = msg:getString() + signalcall(self.onMotd, self, motd) +end + +function ProtocolLogin:parseSessionKey(msg) + local sessionKey = msg:getString() + signalcall(self.onSessionKey, self, sessionKey) +end + +function ProtocolLogin:parseCharacterList(msg) + local characters = {} + + if g_game.getClientVersion() > 1010 then + local worlds = {} + + local worldsCount = msg:getU8() + for i=1, worldsCount do + local world = {} + local worldId = msg:getU8() + world.worldName = msg:getString() + world.worldIp = msg:getString() + world.worldPort = msg:getU16() + world.previewState = msg:getU8() + worlds[worldId] = world + end + + local charactersCount = msg:getU8() + for i=1, charactersCount do + local character = {} + local worldId = msg:getU8() + character.name = msg:getString() + character.worldName = worlds[worldId].worldName + character.worldIp = worlds[worldId].worldIp + character.worldPort = worlds[worldId].worldPort + character.previewState = worlds[worldId].previewState + characters[i] = character + end + + else + local charactersCount = msg:getU8() + for i=1,charactersCount do + local character = {} + character.name = msg:getString() + character.worldName = msg:getString() + character.worldIp = iptostring(msg:getU32()) + character.worldPort = msg:getU16() + + if g_game.getFeature(GamePreviewState) then + character.previewState = msg:getU8() + end + + characters[i] = character + end + end + + local account = {} + if g_game.getProtocolVersion() > 1077 then + account.status = msg:getU8() + account.subStatus = msg:getU8() + + account.premDays = msg:getU32() + if account.premDays ~= 0 and account.premDays ~= 65535 then + account.premDays = math.floor((account.premDays - os.time()) / 86400) + end + else + account.status = AccountStatus.Ok + account.premDays = msg:getU16() + account.subStatus = account.premDays > 0 and SubscriptionStatus.Premium or SubscriptionStatus.Free + end + + signalcall(self.onCharacterList, self, characters, account) +end + +function ProtocolLogin:parseExtendedCharacterList(msg) + local characters = msg:getTable() + local account = msg:getTable() + local otui = msg:getString() + signalcall(self.onCharacterList, self, characters, account, otui) +end + +function ProtocolLogin:parseOpcode(opcode, msg) + signalcall(self.onOpcode, self, opcode, msg) +end + +function ProtocolLogin:onError(msg, code) + local text = translateNetworkError(code, self:isConnecting(), msg) + signalcall(self.onLoginError, self, text) +end diff --git a/modules/gamelib/spells.lua b/modules/gamelib/spells.lua new file mode 100644 index 0000000..2eff68c --- /dev/null +++ b/modules/gamelib/spells.lua @@ -0,0 +1,441 @@ +SpelllistSettings = { + ['Default'] = { + iconFile = '/images/game/spells/defaultspells', + iconSize = {width = 32, height = 32}, + spellListWidth = 210, + spellWindowWidth = 550, + spellOrder = {'Animate Dead', 'Annihilation', 'Avalanche', 'Berserk', 'Blood Rage', 'Brutal Strike', 'Cancel Invisibility', 'Challenge', 'Chameleon', 'Charge', 'Conjure Arrow', 'Conjure Bolt', 'Conjure Explosive Arrow', 'Conjure Piercing Bolt', 'Conjure Poisoned Arrow', 'Conjure Power Bolt', 'Conjure Sniper Arrow', 'Convince Creature', 'Creature Illusion', 'Cure Bleeding', 'Cure Burning', 'Cure Curse', 'Cure Electrification', 'Cure Poison', 'Cure Poison Rune', 'Curse', 'Death Strike', 'Desintegrate', 'Destroy Field', 'Divine Caldera', 'Divine Healing', 'Divine Missile', 'Electrify', 'Enchant Party', 'Enchant Spear', 'Enchant Staff', 'Energy Beam', 'Energy Field', 'Energy Strike', 'Energy Wall', 'Energy Wave', 'Energybomb', 'Envenom', 'Eternal Winter', 'Ethereal Spear', 'Explosion', 'Fierce Berserk', 'Find Person', 'Fire Field', 'Fire Wall', 'Fire Wave', 'Fireball', 'Firebomb', 'Flame Strike', 'Food', 'Front Sweep', 'Great Energy Beam', 'Great Fireball', 'Great Light', 'Groundshaker', 'Haste', 'Heal Friend', 'Heal Party', 'Heavy Magic Missile', 'Hells Core', 'Holy Flash', 'Holy Missile', 'Ice Strike', 'Ice Wave', 'Icicle', 'Ignite', 'Inflict Wound', 'Intense Healing', 'Intense Healing Rune', 'Intense Recovery', 'Intense Wound Cleansing', 'Invisibility', 'Levitate', 'Light', 'Light Healing', 'Light Magic Missile', 'Lightning', 'Magic Rope', 'Magic Shield', 'Magic Wall', 'Mass Healing', 'Paralyze', 'Physical Strike', 'Poison Bomb', 'Poison Field', 'Poison Wall', 'Protect Party', 'Protector', 'Rage of the Skies', 'Recovery', 'Salvation', 'Sharpshooter', 'Soulfire', 'Stalagmite', 'Stone Shower', 'Strong Energy Strike', 'Strong Ethereal Spear', 'Strong Flame Strike', 'Strong Haste', 'Strong Ice Strike', 'Strong Ice Wave', 'Strong Terra Strike', 'Sudden Death', 'Summon Creature', 'Swift Foot', 'Terra Strike', 'Terra Wave', 'Thunderstorm', 'Train Party', 'Ultimate Energy Strike', 'Ultimate Flame Strike', 'Ultimate Healing', 'Ultimate Healing Rune', 'Ultimate Ice Strike', 'Ultimate Light', 'Ultimate Terra Strike', 'Whirlwind Throw', 'Wild Growth', 'Wound Cleansing', 'Wrath of Nature'} + }--[[, + + ['Sample'] = { + iconFile = '/images/game/spells/sample', + iconSize = {width = 64, height = 64}, + spellOrder = {'Critical Strike', 'Firefly', 'Fire Breath', 'Moonglaives', 'Wind Walk'} + }]] +} + +SpellInfo = { + ['Default'] = { + ['Death Strike'] = {id = 87, words = 'exori mort', exhaustion = 2000, premium = true, type = 'Instant', icon = 'deathstrike', mana = 20, level = 16, soul = 0, group = {[1] = 2000}, vocations = {1, 5}}, + ['Flame Strike'] = {id = 89, words = 'exori flam', exhaustion = 2000, premium = true, type = 'Instant', icon = 'flamestrike', mana = 20, level = 14, soul = 0, group = {[1] = 2000}, vocations = {1, 2, 5, 6}}, + ['Strong Flame Strike'] = {id = 150, words = 'exori gran flam', exhaustion = 8000, premium = true, type = 'Instant', icon = 'strongflamestrike', mana = 60, level = 70, soul = 0, group = {[1] = 2000, [4] = 8000}, vocations = {1, 5}}, + ['Ultimate Flame Strike'] = {id = 154, words = 'exori max flam', exhaustion = 30000, premium = true, type = 'Instant', icon = 'ultimateflamestrike', mana = 100, level = 90, soul = 0, group = {[1] = 4000}, vocations = {1, 5}}, + ['Energy Strike'] = {id = 88, words = 'exori vis', exhaustion = 2000, premium = true, type = 'Instant', icon = 'energystrike', mana = 20, level = 12, soul = 0, group = {[1] = 2000}, vocations = {1, 2, 5, 6}}, + ['Strong Energy Strike'] = {id = 151, words = 'exori gran vis', exhaustion = 8000, premium = true, type = 'Instant', icon = 'strongenergystrike', mana = 60, level = 80, soul = 0, group = {[1] = 2000, [4] = 8000}, vocations = {1, 5}}, + ['Ultimate Energy Strike'] = {id = 155, words = 'exori max vis', exhaustion = 30000, premium = true, type = 'Instant', icon = 'ultimateenergystrike', mana = 100, level = 100,soul = 0, group = {[1] = 4000}, vocations = {1, 5}}, + ['Whirlwind Throw'] = {id = 107, words = 'exori hur', exhaustion = 6000, premium = true, type = 'Instant', icon = 'whirlwindthrow', mana = 40, level = 28, soul = 0, group = {[1] = 2000}, vocations = {4, 8}}, + ['Fire Wave'] = {id = 19, words = 'exevo flam hur', exhaustion = 4000, premium = false, type = 'Instant', icon = 'firewave', mana = 25, level = 18, soul = 0, group = {[1] = 2000}, vocations = {1, 5}}, + ['Ethereal Spear'] = {id = 111, words = 'exori con', exhaustion = 2000, premium = true, type = 'Instant', icon = 'etherealspear', mana = 25, level = 23, soul = 0, group = {[1] = 2000}, vocations = {3, 7}}, + ['Strong Ethereal Spear'] = {id = 57, words = 'exori gran con', exhaustion = 8000, premium = true, type = 'Instant', icon = 'strongetherealspear', mana = 55, level = 90, soul = 0, group = {[1] = 2000}, vocations = {3, 7}}, + ['Energy Beam'] = {id = 22, words = 'exevo vis lux', exhaustion = 4000, premium = false, type = 'Instant', icon = 'energybeam', mana = 40, level = 23, soul = 0, group = {[1] = 2000}, vocations = {1, 5}}, + ['Great Energy Beam'] = {id = 23, words = 'exevo gran vis lux', exhaustion = 6000, premium = false, type = 'Instant', icon = 'greatenergybeam', mana = 110, level = 29, soul = 0, group = {[1] = 2000}, vocations = {1, 5}}, + ['Groundshaker'] = {id = 106, words = 'exori mas', exhaustion = 8000, premium = true, type = 'Instant', icon = 'groundshaker', mana = 160, level = 33, soul = 0, group = {[1] = 2000}, vocations = {4, 8}}, + ['Berserk'] = {id = 80, words = 'exori', exhaustion = 4000, premium = true, type = 'Instant', icon = 'berserk', mana = 115, level = 35, soul = 0, group = {[1] = 2000}, vocations = {4, 8}}, + ['Annihilation'] = {id = 62, words = 'exori gran ico', exhaustion = 30000, premium = true, type = 'Instant', icon = 'annihilation', mana = 300, level = 110,soul = 0, group = {[1] = 4000}, vocations = {4, 8}}, + ['Brutal Strike'] = {id = 61, words = 'exori ico', exhaustion = 6000, premium = true, type = 'Instant', icon = 'brutalstrike', mana = 30, level = 16, soul = 0, group = {[1] = 2000}, vocations = {4, 8}}, + ['Front Sweep'] = {id = 59, words = 'exori min', exhaustion = 6000, premium = true, type = 'Instant', icon = 'frontsweep', mana = 200, level = 70, soul = 0, group = {[1] = 2000}, vocations = {4, 8}}, + ['Inflict Wound'] = {id = 141, words = 'utori kor', exhaustion = 30000, premium = true, type = 'Instant', icon = 'inflictwound', mana = 30, level = 40, soul = 0, group = {[1] = 2000}, vocations = {4, 8}}, + ['Ignite'] = {id = 138, words = 'utori flam', exhaustion = 30000, premium = true, type = 'Instant', icon = 'ignite', mana = 30, level = 26, soul = 0, group = {[1] = 2000}, vocations = {1, 5}}, + ['Lightning'] = {id = 149, words = 'exori amp vis', exhaustion = 8000, premium = true, type = 'Instant', icon = 'lightning', mana = 60, level = 55, soul = 0, group = {[1] = 2000, [4] = 8000}, vocations = {1, 5}}, + ['Curse'] = {id = 139, words = 'utori mort', exhaustion = 50000, premium = true, type = 'Instant', icon = 'curse', mana = 30, level = 75, soul = 0, group = {[1] = 2000}, vocations = {1, 5}}, + ['Electrify'] = {id = 140, words = 'utori vis', exhaustion = 30000, premium = true, type = 'Instant', icon = 'electrify', mana = 30, level = 34, soul = 0, group = {[1] = 2000}, vocations = {1, 5}}, + ['Energy Wave'] = {id = 13, words = 'exevo vis hur', exhaustion = 8000, premium = false, type = 'Instant', icon = 'energywave', mana = 170, level = 38, soul = 0, group = {[1] = 2000}, vocations = {1, 5}}, + ['Rage of the Skies'] = {id = 119, words = 'exevo gran mas vis', exhaustion = 40000, premium = true, type = 'Instant', icon = 'rageoftheskies', mana = 600, level = 55, soul = 0, group = {[1] = 4000}, vocations = {1, 5}}, + ['Fierce Berserk'] = {id = 105, words = 'exori gran', exhaustion = 6000, premium = true, type = 'Instant', icon = 'fierceberserk', mana = 340, level = 90, soul = 0, group = {[1] = 2000}, vocations = {4, 8}}, + ['Hells Core'] = {id = 24, words = 'exevo gran mas flam', exhaustion = 40000, premium = true, type = 'Instant', icon = 'hellscore', mana = 1100, level = 60, soul = 0, group = {[1] = 4000}, vocations = {1, 5}}, + ['Holy Flash'] = {id = 143, words = 'utori san', exhaustion = 40000, premium = true, type = 'Instant', icon = 'holyflash', mana = 30, level = 70, soul = 0, group = {[1] = 2000}, vocations = {3, 7}}, + ['Divine Missile'] = {id = 122, words = 'exori san', exhaustion = 2000, premium = true, type = 'Instant', icon = 'divinemissile', mana = 20, level = 40, soul = 0, group = {[1] = 2000}, vocations = {3, 7}}, + ['Divine Caldera'] = {id = 124, words = 'exevo mas san', exhaustion = 4000, premium = true, type = 'Instant', icon = 'divinecaldera', mana = 160, level = 50, soul = 0, group = {[1] = 2000}, vocations = {3, 7}}, + ['Physical Strike'] = {id = 148, words = 'exori moe ico', exhaustion = 2000, premium = true, type = 'Instant', icon = 'physicalstrike', mana = 20, level = 16, soul = 0, group = {[1] = 2000}, vocations = {2, 6}}, + ['Eternal Winter'] = {id = 118, words = 'exevo gran mas frigo', exhaustion = 40000, premium = true, type = 'Instant', icon = 'eternalwinter', mana = 1050, level = 60, soul = 0, group = {[1] = 4000}, vocations = {2, 6}}, + ['Ice Strike'] = {id = 112, words = 'exori frigo', exhaustion = 2000, premium = true, type = 'Instant', icon = 'icestrike', mana = 20, level = 15, soul = 0, group = {[1] = 2000}, vocations = {1, 5, 2, 6}}, + ['Strong Ice Strike'] = {id = 152, words = 'exori gran frigo', exhaustion = 8000, premium = true, type = 'Instant', icon = 'strongicestrike', mana = 60, level = 80, soul = 0, group = {[1] = 2000, [4] = 8000}, vocations = {2, 6}}, + ['Ultimate Ice Strike'] = {id = 156, words = 'exori max frigo', exhaustion = 30000, premium = true, type = 'Instant', icon = 'ultimateicestrike', mana = 100, level = 100,soul = 0, group = {[1] = 4000}, vocations = {2, 6}}, + ['Ice Wave'] = {id = 121, words = 'exevo frigo hur', exhaustion = 4000, premium = false, type = 'Instant', icon = 'icewave', mana = 25, level = 18, soul = 0, group = {[1] = 2000}, vocations = {2, 6}}, + ['Strong Ice Wave'] = {id = 43, words = 'exevo gran frigo hur', exhaustion = 8000, premium = true, type = 'Instant', icon = 'strongicewave', mana = 170, level = 40, soul = 0, group = {[1] = 2000}, vocations = {2, 6}}, + ['Envenom'] = {id = 142, words = 'utori pox', exhaustion = 40000, premium = true, type = 'Instant', icon = 'envenom', mana = 30, level = 50, soul = 0, group = {[1] = 2000}, vocations = {2, 6}}, + ['Terra Strike'] = {id = 113, words = 'exori tera', exhaustion = 2000, premium = true, type = 'Instant', icon = 'terrastrike', mana = 20, level = 13, soul = 0, group = {[1] = 2000}, vocations = {1, 5, 2, 6}}, + ['Strong Terra Strike'] = {id = 153, words = 'exori gran tera', exhaustion = 8000, premium = true, type = 'Instant', icon = 'strongterrastrike', mana = 60, level = 70, soul = 0, group = {[1] = 2000, [4] = 8000}, vocations = {2, 6}}, + ['Ultimate Terra Strike'] = {id = 157, words = 'exori max tera', exhaustion = 30000, premium = true, type = 'Instant', icon = 'ultimateterrastrike', mana = 100, level = 90, soul = 0, group = {[1] = 4000}, vocations = {2, 6}}, + ['Terra Wave'] = {id = 120, words = 'exevo tera hur', exhaustion = 4000, premium = false, type = 'Instant', icon = 'terrawave', mana = 210, level = 38, soul = 0, group = {[1] = 2000}, vocations = {2, 6}}, + ['Wrath of Nature'] = {id = 56, words = 'exevo gran mas tera', exhaustion = 40000, premium = true, type = 'Instant', icon = 'wrathofnature', mana = 700, level = 55, soul = 0, group = {[1] = 4000}, vocations = {2, 6}}, + ['Light Healing'] = {id = 1, words = 'exura', exhaustion = 1000, premium = false, type = 'Instant', icon = 'lighthealing', mana = 20, level = 9, soul = 0, group = {[2] = 1000}, vocations = {1, 2, 3, 5, 6, 7}}, + ['Wound Cleansing'] = {id = 123, words = 'exura ico', exhaustion = 1000, premium = false, type = 'Instant', icon = 'woundcleansing', mana = 40, level = 10, soul = 0, group = {[2] = 1000}, vocations = {4, 8}}, + ['Intense Wound Cleansing'] = {id = 158, words = 'exura gran ico', exhaustion = 600000,premium = true, type = 'Instant', icon = 'intensewoundcleansing', mana = 200, level = 80, soul = 0, group = {[2] = 1000}, vocations = {4, 8}}, + ['Cure Bleeding'] = {id = 144, words = 'exana kor', exhaustion = 6000, premium = true, type = 'Instant', icon = 'curebleeding', mana = 30, level = 30, soul = 0, group = {[2] = 1000}, vocations = {4, 8}}, + ['Cure Electrification'] = {id = 146, words = 'exana vis', exhaustion = 6000, premium = true, type = 'Instant', icon = 'curseelectrification', mana = 30, level = 22, soul = 0, group = {[2] = 1000}, vocations = {2, 6}}, + ['Cure Poison'] = {id = 29, words = 'exana pox', exhaustion = 6000, premium = false, type = 'Instant', icon = 'curepoison', mana = 30, level = 10, soul = 0, group = {[2] = 1000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}}, + ['Cure Burning'] = {id = 145, words = 'exana flam', exhaustion = 6000, premium = true, type = 'Instant', icon = 'cureburning', mana = 30, level = 30, soul = 0, group = {[2] = 1000}, vocations = {2, 6}}, + ['Cure Curse'] = {id = 147, words = 'exana mort', exhaustion = 6000, premium = true, type = 'Instant', icon = 'curecurse', mana = 40, level = 80, soul = 0, group = {[2] = 1000}, vocations = {3, 7}}, + ['Recovery'] = {id = 159, words = 'utura', exhaustion = 60000, premium = true, type = 'Instant', icon = 'recovery', mana = 75, level = 50, soul = 0, group = {[2] = 1000}, vocations = {4, 8, 3, 7}}, + ['Intense Recovery'] = {id = 160, words = 'utura gran', exhaustion = 60000, premium = true, type = 'Instant', icon = 'intenserecovery', mana = 165, level = 100,soul = 0, group = {[2] = 1000}, vocations = {4, 8, 3, 7}}, + ['Salvation'] = {id = 36, words = 'exura gran san', exhaustion = 1000, premium = true, type = 'Instant', icon = 'salvation', mana = 210, level = 60, soul = 0, group = {[2] = 1000}, vocations = {3, 7}}, + ['Intense Healing'] = {id = 2, words = 'exura gran', exhaustion = 1000, premium = false, type = 'Instant', icon = 'intensehealing', mana = 70, level = 20, soul = 0, group = {[2] = 1000}, vocations = {1, 2, 3, 5, 6, 7}}, + ['Heal Friend'] = {id = 84, words = 'exura sio', exhaustion = 1000, premium = true, type = 'Instant', icon = 'healfriend', mana = 140, level = 18, soul = 0, group = {[2] = 1000}, vocations = {2, 6}}, + ['Ultimate Healing'] = {id = 3, words = 'exura vita', exhaustion = 1000, premium = false, type = 'Instant', icon = 'ultimatehealing', mana = 160, level = 30, soul = 0, group = {[2] = 1000}, vocations = {1, 2, 5, 6}}, + ['Mass Healing'] = {id = 82, words = 'exura gran mas res', exhaustion = 2000, premium = true, type = 'Instant', icon = 'masshealing', mana = 150, level = 36, soul = 0, group = {[2] = 1000}, vocations = {2, 6}}, + ['Divine Healing'] = {id = 125, words = 'exura san', exhaustion = 1000, premium = false, type = 'Instant', icon = 'divinehealing', mana = 160, level = 35, soul = 0, group = {[2] = 1000}, vocations = {3, 7}}, + ['Light'] = {id = 10, words = 'utevo lux', exhaustion = 2000, premium = false, type = 'Instant', icon = 'light', mana = 20, level = 8, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}}, + ['Find Person'] = {id = 20, words = 'exiva', exhaustion = 2000, premium = false, type = 'Instant', icon = 'findperson', mana = 20, level = 8, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}}, + ['Magic Rope'] = {id = 76, words = 'exani tera', exhaustion = 2000, premium = true, type = 'Instant', icon = 'magicrope', mana = 20, level = 9, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}}, + ['Levitate'] = {id = 81, words = 'exani hur', exhaustion = 2000, premium = true, type = 'Instant', icon = 'levitate', mana = 50, level = 12, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}}, + ['Great Light'] = {id = 11, words = 'utevo gran lux', exhaustion = 2000, premium = false, type = 'Instant', icon = 'greatlight', mana = 60, level = 13, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}}, + ['Magic Shield'] = {id = 44, words = 'utamo vita', exhaustion = 2000, premium = false, type = 'Instant', icon = 'magicshield', mana = 50, level = 14, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Haste'] = {id = 6, words = 'utani hur', exhaustion = 2000, premium = true, type = 'Instant', icon = 'haste', mana = 60, level = 14, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 3, 4, 5, 6, 7, 8}}, + ['Charge'] = {id = 131, words = 'utani tempo hur', exhaustion = 2000, premium = true, type = 'Instant', icon = 'charge', mana = 100, level = 25, soul = 0, group = {[3] = 2000}, vocations = {4, 8}}, + ['Swift Foot'] = {id = 134, words = 'utamo tempo san', exhaustion = 2000, premium = true, type = 'Instant', icon = 'swiftfoot', mana = 400, level = 55, soul = 0, group = {[1] = 10000, [3] = 2000}, vocations = {3, 7}}, + ['Challenge'] = {id = 93, words = 'exeta res', exhaustion = 2000, premium = true, type = 'Instant', icon = 'challenge', mana = 30, level = 20, soul = 0, group = {[3] = 2000}, vocations = {8}}, + ['Strong Haste'] = {id = 39, words = 'utani gran hur', exhaustion = 2000, premium = true, type = 'Instant', icon = 'stronghaste', mana = 100, level = 20, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Creature Illusion'] = {id = 38, words = 'utevo res ina', exhaustion = 2000, premium = false, type = 'Instant', icon = 'creatureillusion', mana = 100, level = 23, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Ultimate Light'] = {id = 75, words = 'utevo vis lux', exhaustion = 2000, premium = true, type = 'Instant', icon = 'ultimatelight', mana = 140, level = 26, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Cancel Invisibility'] = {id = 90, words = 'exana ina', exhaustion = 2000, premium = true, type = 'Instant', icon = 'cancelinvisibility', mana = 200, level = 26, soul = 0, group = {[3] = 2000}, vocations = {3, 7}}, + ['Invisibility'] = {id = 45, words = 'utana vid', exhaustion = 2000, premium = false, type = 'Instant', icon = 'invisible', mana = 440, level = 35, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Sharpshooter'] = {id = 135, words = 'utito tempo san', exhaustion = 2000, premium = true, type = 'Instant', icon = 'sharpshooter', mana = 450, level = 60, soul = 0, group = {[2] = 10000, [3] = 10000}, vocations = {3, 7}}, + ['Protector'] = {id = 132, words = 'utamo tempo', exhaustion = 2000, premium = true, type = 'Instant', icon = 'protector', mana = 200, level = 55, soul = 0, group = {[1] = 10000, [3] = 2000}, vocations = {4, 8}}, + ['Blood Rage'] = {id = 133, words = 'utito tempo', exhaustion = 2000, premium = true, type = 'Instant', icon = 'bloodrage', mana = 290, level = 60, soul = 0, group = {[3] = 2000}, vocations = {4, 8}}, + ['Train Party'] = {id = 126, words = 'utito mas sio', exhaustion = 2000, premium = true, type = 'Instant', icon = 'trainparty', mana = 'Var.', level = 32, soul = 0, group = {[3] = 2000}, vocations = {8}}, + ['Protect Party'] = {id = 127, words = 'utamo mas sio', exhaustion = 2000, premium = true, type = 'Instant', icon = 'protectparty', mana = 'Var.', level = 32, soul = 0, group = {[3] = 2000}, vocations = {7}}, + ['Heal Party'] = {id = 128, words = 'utura mas sio', exhaustion = 2000, premium = true, type = 'Instant', icon = 'healparty', mana = 'Var.', level = 32, soul = 0, group = {[3] = 2000}, vocations = {6}}, + ['Enchant Party'] = {id = 129, words = 'utori mas sio', exhaustion = 2000, premium = true, type = 'Instant', icon = 'enchantparty', mana = 'Var.', level = 32, soul = 0, group = {[3] = 2000}, vocations = {5}}, + ['Summon Creature'] = {id = 9, words = 'utevo res', exhaustion = 2000, premium = false, type = 'Instant', icon = 'summoncreature', mana = 'Var.', level = 25, soul = 0, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Conjure Arrow'] = {id = 51, words = 'exevo con', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'conjurearrow', mana = 100, level = 13, soul = 1, group = {[3] = 2000}, vocations = {3, 7}}, + ['Food'] = {id = 42, words = 'exevo pan', exhaustion = 2000, premium = false, type = 'Instant', icon = 'food', mana = 120, level = 14, soul = 1, group = {[3] = 2000}, vocations = {2, 6}}, + ['Conjure Poisoned Arrow'] = {id = 48, words = 'exevo con pox', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'poisonedarrow', mana = 130, level = 16, soul = 2, group = {[3] = 2000}, vocations = {3, 7}}, + ['Conjure Bolt'] = {id = 79, words = 'exevo con mort', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'conjurebolt', mana = 140, level = 17, soul = 2, group = {[3] = 2000}, vocations = {3, 7}}, + ['Conjure Sniper Arrow'] = {id = 108, words = 'exevo con hur', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'sniperarrow', mana = 160, level = 24, soul = 3, group = {[3] = 2000}, vocations = {3, 7}}, + ['Conjure Explosive Arrow'] = {id = 49, words = 'exevo con flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'explosivearrow', mana = 290, level = 25, soul = 3, group = {[3] = 2000}, vocations = {3, 7}}, + ['Conjure Piercing Bolt'] = {id = 109, words = 'exevo con grav', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'piercingbolt', mana = 180, level = 33, soul = 3, group = {[3] = 2000}, vocations = {3, 7}}, + ['Enchant Staff'] = {id = 92, words = 'exeta vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'enchantstaff', mana = 80, level = 41, soul = 0, group = {[3] = 2000}, vocations = {5}}, + ['Enchant Spear'] = {id = 110, words = 'exeta con', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'enchantspear', mana = 350, level = 45, soul = 3, group = {[3] = 2000}, vocations = {3, 7}}, + ['Conjure Power Bolt'] = {id = 95, words = 'exevo con vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'powerbolt', mana = 800, level = 59, soul = 3, group = {[3] = 2000}, vocations = {7}}, + ['Poison Field'] = {id = 26, words = 'adevo grav pox', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'poisonfield', mana = 200, level = 14, soul = 1, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Light Magic Missile'] = {id = 7, words = 'adori min vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'lightmagicmissile', mana = 120, level = 15, soul = 1, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Fire Field'] = {id = 25, words = 'adevo grav flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'firefield', mana = 240, level = 15, soul = 1, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Fireball'] = {id = 15, words = 'adori flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'fireball', mana = 460, level = 27, soul = 3, group = {[3] = 2000}, vocations = {1, 5}}, + ['Energy Field'] = {id = 27, words = 'adevo grav vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'energyfield', mana = 320, level = 18, soul = 2, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Stalagmite'] = {id = 77, words = 'adori tera', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'stalagmite', mana = 400, level = 24, soul = 2, group = {[3] = 2000}, vocations = {1, 5, 2, 6}}, + ['Great Fireball'] = {id = 16, words = 'adori mas flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'greatfireball', mana = 530, level = 30, soul = 3, group = {[3] = 2000}, vocations = {1, 5}}, + ['Heavy Magic Missile'] = {id = 8, words = 'adori vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'heavymagicmissile', mana = 350, level = 25, soul = 2, group = {[3] = 2000}, vocations = {1, 5, 2, 6}}, + ['Poison Bomb'] = {id = 91, words = 'adevo mas pox', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'poisonbomb', mana = 520, level = 25, soul = 2, group = {[3] = 2000}, vocations = {2, 6}}, + ['Firebomb'] = {id = 17, words = 'adevo mas flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'firebomb', mana = 600, level = 27, soul = 3, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Soulfire'] = {id = 50, words = 'adevo res flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'soulfire', mana = 600, level = 27, soul = 3, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Poison Wall'] = {id = 32, words = 'adevo mas grav pox', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'poisonwall', mana = 640, level = 29, soul = 3, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Explosion'] = {id = 18, words = 'adevo mas hur', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'explosion', mana = 570, level = 31, soul = 3, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Fire Wall'] = {id = 28, words = 'adevo mas grav flam', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'firewall', mana = 780, level = 33, soul = 3, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Energybomb'] = {id = 55, words = 'adevo mas vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'energybomb', mana = 880, level = 37, soul = 5, group = {[3] = 2000}, vocations = {1, 5}}, + ['Energy Wall'] = {id = 33, words = 'adevo mas grav vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'energywall', mana = 1000, level = 41, soul = 5, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Sudden Death'] = {id = 21, words = 'adori gran mort', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'suddendeath', mana = 985, level = 45, soul = 5, group = {[3] = 2000}, vocations = {1, 5}}, + ['Cure Poison Rune'] = {id = 31, words = 'adana pox', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'antidote', mana = 200, level = 15, soul = 1, group = {[3] = 2000}, vocations = {2, 6}}, + ['Intense Healing Rune'] = {id = 4, words = 'adura gran', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'intensehealingrune', mana = 240, level = 15, soul = 2, group = {[3] = 2000}, vocations = {2, 6}}, + ['Ultimate Healing Rune'] = {id = 5, words = 'adura vita', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'ultimatehealingrune', mana = 400, level = 24, soul = 3, group = {[3] = 2000}, vocations = {2, 6}}, + ['Convince Creature'] = {id = 12, words = 'adeta sio', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'convincecreature', mana = 200, level = 16, soul = 3, group = {[3] = 2000}, vocations = {2, 6}}, + ['Animate Dead'] = {id = 83, words = 'adana mort', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'animatedead', mana = 600, level = 27, soul = 5, group = {[3] = 2000}, vocations = {1, 2, 5, 6}}, + ['Chameleon'] = {id = 14, words = 'adevo ina', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'chameleon', mana = 600, level = 27, soul = 2, group = {[3] = 2000}, vocations = {2, 6}}, + ['Destroy Field'] = {id = 30, words = 'adito grav', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'destroyfield', mana = 120, level = 17, soul = 2, group = {[3] = 2000}, vocations = {1, 2, 3, 5, 6, 7}}, + ['Desintegrate'] = {id = 78, words = 'adito tera', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'desintegrate', mana = 200, level = 21, soul = 3, group = {[3] = 2000}, vocations = {1, 2, 3, 5, 6, 7}}, + ['Magic Wall'] = {id = 86, words = 'adevo grav tera', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'magicwall', mana = 750, level = 32, soul = 5, group = {[3] = 2000}, vocations = {1, 5}}, + ['Wild Growth'] = {id = 94, words = 'adevo grav vita', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'wildgrowth', mana = 600, level = 27, soul = 5, group = {[3] = 2000}, vocations = {2, 6}}, + ['Paralyze'] = {id = 54, words = 'adana ani', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'paralyze', mana = 1400, level = 54, soul = 3, group = {[3] = 2000}, vocations = {2, 6}}, + ['Icicle'] = {id = 114, words = 'adori frigo', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'icicle', mana = 460, level = 28, soul = 3, group = {[3] = 2000}, vocations = {2, 6}}, + ['Avalanche'] = {id = 115, words = 'adori mas frigo', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'avalanche', mana = 530, level = 30, soul = 3, group = {[3] = 2000}, vocations = {2, 6}}, + ['Stone Shower'] = {id = 116, words = 'adori mas tera', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'stoneshower', mana = 430, level = 28, soul = 3, group = {[3] = 2000}, vocations = {2, 6}}, + ['Thunderstorm'] = {id = 117, words = 'adori mas vis', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'thunderstorm', mana = 430, level = 28, soul = 3, group = {[3] = 2000}, vocations = {1, 5}}, + ['Holy Missile'] = {id = 130, words = 'adori san', exhaustion = 2000, premium = false, type = 'Conjure', icon = 'holymissile', mana = 350, level = 27, soul = 3, group = {[3] = 2000}, vocations = {3, 7}} + }--[[, + + ['Sample'] = { + ['Wind Walk'] = {id = 1, words = 'windwalk', description = 'Run at enormous speed.', exhaustion = 2000, premium = false, type = 'Instant', icon = 1, mana = 50, level = 10, soul = 0, group = {[3] = 2000}, vocations = {1, 2}}, + ['Fire Breath'] = {id = 2, words = 'firebreath', description = 'A strong firewave.', exhaustion = 2000, premium = false, type = 'Instant', icon = 2, mana = 350, level = 27, soul = 0, group = {[1] = 2000}, vocations = {4, 8}}, + ['Moonglaives'] = {id = 3, words = 'moonglaives', description = 'Throw moonglaives around you.', exhaustion = 2000, premium = false, type = 'Instant', icon = 3, mana = 90, level = 55, soul = 0, group = {[1] = 2000}, vocations = {3, 7}}, + ['Critical Strike'] = {id = 4, words = 'criticalstrike', description = 'Land a critical strike.', exhaustion = 2000, premium = false, type = 'Instant', icon = 4, mana = 350, level = 27, soul = 0, group = {[1] = 2000}, vocations = {3, 4, 7, 8}}, + ['Firefly'] = {id = 5, words = 'firefly', description = 'Summon a angry firefly', exhaustion = 2000, premium = false, type = 'Instant', icon = 5, mana = 350, level = 27, soul = 0, group = {[1] = 2000}, vocations = {1, 2, 5, 6}} + }]] +} + +-- ['const_name'] = {client_id, TFS_id} +-- Conversion from TFS icon id to the id used by client (icons.png order) +SpellIcons = { + ['intenserecovery'] = {16, 160}, + ['recovery'] = {15, 159}, + ['intensewoundcleansing'] = {4, 158}, + ['ultimateterrastrike'] = {37, 157}, + ['ultimateicestrike'] = {34, 156}, + ['ultimateenergystrike'] = {31, 155}, + ['ultimateflamestrike'] = {28, 154}, + ['strongterrastrike'] = {36, 153}, + ['strongicestrike'] = {33, 152}, + ['strongenergystrike'] = {30, 151}, + ['strongflamestrike'] = {27, 150}, + ['lightning'] = {51, 149}, + ['physicalstrike'] = {17, 148}, + ['curecurse'] = {11, 147}, + ['curseelectrification'] = {14, 146}, + ['cureburning'] = {13, 145}, + ['curebleeding'] = {12, 144}, + ['holyflash'] = {53, 143}, + ['envenom'] = {58, 142}, + ['inflictwound'] = {57, 141}, + ['electrify'] = {56, 140}, + ['curse'] = {54, 139}, + ['ignite'] = {55, 138}, + -- [[ 136 / 137 Unknown ]] + ['sharpshooter'] = {121, 135}, + ['swiftfoot'] = {119, 134}, + ['bloodrage'] = {96, 133}, + ['protector'] = {122, 132}, + ['charge'] = {98, 131}, + ['holymissile'] = {76, 130}, + ['enchantparty'] = {113, 129}, + ['healparty'] = {126, 128}, + ['protectparty'] = {123, 127}, + ['trainparty'] = {120, 126}, + ['divinehealing'] = {2, 125}, + ['divinecaldera'] = {40, 124}, + ['woundcleansing'] = {3, 123}, + ['divinemissile'] = {39, 122}, + ['icewave'] = {45, 121}, + ['terrawave'] = {47, 120}, + ['rageoftheskies'] = {52, 119}, + ['eternalwinter'] = {50, 118}, + ['thunderstorm'] = {63, 117}, + ['stoneshower'] = {65, 116}, + ['avalanche'] = {92, 115}, + ['icicle'] = {75, 114}, + ['terrastrike'] = {35, 113}, + ['icestrike'] = {32, 112}, + ['etherealspear'] = {18, 111}, + ['enchantspear'] = {104, 110}, + ['piercingbolt'] = {110, 109}, + ['sniperarrow'] = {112, 108}, + ['whirlwindthrow'] = {19, 107}, + ['groundshaker'] = {25, 106}, + ['fierceberserk'] = {22, 105}, + -- [[ 96 - 104 Unknown ]] + ['powerbolt'] = {108, 95}, + ['wildgrowth'] = {61, 94}, + ['challenge'] = {97, 93}, + ['enchantstaff'] = {103, 92}, + ['poisonbomb'] = {70, 91}, + ['cancelinvisibility'] = {95, 90}, + ['flamestrike'] = {26, 89}, + ['energystrike'] = {29, 88}, + ['deathstrike'] = {38, 87}, + ['magicwall'] = {72, 86}, + ['healfriend'] = {8, 84}, + ['animatedead'] = {93, 83}, + ['masshealing'] = {9, 82}, + ['levitate'] = {125, 81}, + ['berserk'] = {21, 80}, + ['conjurebolt'] = {107, 79}, + ['desintegrate'] = {88, 78}, + ['stalagmite'] = {66, 77}, + ['magicrope'] = {105, 76}, + ['ultimatelight'] = {115, 75}, + -- [[ 71 - 64 TFS House Commands ]] + -- [[ 63 - 70 Unknown ]] + ['annihilation'] = {24, 62}, + ['brutalstrike'] = {23, 61}, + -- [[ 60 Unknown ]] + ['frontsweep'] = {20, 59}, + -- [[ 58 Unknown ]] + ['strongetherealspear'] = {59, 57}, + ['wrathofnature'] = {48, 56}, + ['energybomb'] = {86, 55}, + ['paralyze'] = {71, 54}, + -- [[ 53 Unknown ]] + -- [[ 52 TFS Retrieve Friend ]] + ['conjurearrow'] = {106, 51}, + ['soulfire'] = {67, 50}, + ['explosivearrow'] = {109, 49}, + ['poisonedarrow'] = {111, 48}, + -- [[ 46 / 47 Unknown ]] + ['invisible'] = {94, 45}, + ['magicshield'] = {124, 44}, + ['strongicewave'] = {46, 43}, + ['food'] = {99, 42}, + -- [[ 40 / 41 Unknown ]] + ['stronghaste'] = {102, 39}, + ['creatureillusion'] = {100, 38}, + -- [[ 37 TFS Move ]] + ['salvation'] = {60, 36}, + -- [[ 34 / 35 Unknown ]] + ['energywall'] = {84, 33}, + ['poisonwall'] = {68, 32}, + ['antidote'] = {10, 31}, + ['destroyfield'] = {87, 30}, + ['curepoison'] = {10, 29}, + ['firewall'] = {80, 28}, + ['energyfield'] = {85, 27}, + ['poisonfield'] = {69, 26}, + ['firefield'] = {81, 25}, + ['hellscore'] = {49, 24}, + ['greatenergybeam'] = {42, 23}, + ['energybeam'] = {41, 22}, + ['suddendeath'] = {64, 21}, + ['findperson'] = {114, 20}, + ['firewave'] = {44, 19}, + ['explosion'] = {83, 18}, + ['firebomb'] = {82, 17}, + ['greatfireball'] = {78, 16}, + ['fireball'] = {79, 15}, + ['chameleon'] = {91, 14}, + ['energywave'] = {43, 13}, + ['convincecreature'] = {90, 12}, + ['greatlight'] = {116, 11}, + ['light'] = {117, 10}, + ['summoncreature'] = {118, 9}, + ['heavymagicmissile'] = {77, 8}, + ['lightmagicmissile'] = {73, 7}, + ['haste'] = {101, 6}, + ['ultimatehealingrune'] = {62, 5}, + ['intensehealingrune'] = {74, 4}, + ['ultimatehealing'] = {1, 3}, + ['intensehealing'] = {7, 2}, + ['lighthealing'] = {6, 1} +} + +VocationNames = { + [1] = 'Sorcerer', + [2] = 'Druid', + [3] = 'Paladin', + [4] = 'Knight', + [5] = 'Master Sorcerer', + [6] = 'Elder Druid', + [7] = 'Royal Paladin', + [8] = 'Elite Knight' +} + +SpellGroups = { + [1] = 'Attack', + [2] = 'Healing', + [3] = 'Support', + [4] = 'Special' +} + +Spells = {} + +function Spells.getClientId(spellName) + local profile = Spells.getSpellProfileByName(spellName) + + local id = SpellInfo[profile][spellName].icon + if not tonumber(id) and SpellIcons[id] then + return SpellIcons[id][1] + end + return tonumber(id) +end + +function Spells.getServerId(spellName) + local profile = Spells.getSpellProfileByName(spellName) + + local id = SpellInfo[profile][spellName].icon + if not tonumber(id) and SpellIcons[id] then + return SpellIcons[id][2] + end + return tonumber(id) +end + +function Spells.getSpellByName(name) + return SpellInfo[Spells.getSpellProfileByName(name)][name] +end + +function Spells.getSpellByWords(words) + local words = words:lower():trim() + for profile,data in pairs(SpellInfo) do + for k,spell in pairs(data) do + if spell.words == words then + return spell, profile, k + end + end + end + return nil +end + +function Spells.getSpellByIcon(iconId) + for profile,data in pairs(SpellInfo) do + for k,spell in pairs(data) do + if spell.id == iconId then + return spell, profile, k + end + end + end + return nil +end + +function Spells.getSpellIconIds() + local ids = {} + for profile,data in pairs(SpellInfo) do + for k,spell in pairs(data) do + table.insert(ids, spell.id) + end + end + return ids +end + +function Spells.getSpellProfileById(id) + for profile,data in pairs(SpellInfo) do + for k,spell in pairs(data) do + if spell.id == id then + return profile + end + end + end + return nil +end + +function Spells.getSpellProfileByWords(words) + for profile,data in pairs(SpellInfo) do + for k,spell in pairs(data) do + if spell.words == words then + return profile + end + end + end + return nil +end + +function Spells.getSpellProfileByName(spellName) + for profile,data in pairs(SpellInfo) do + if table.findbykey(data, spellName:trim(), true) then + return profile + end + end + return nil +end + +function Spells.getSpellsByVocationId(vocId) + local spells = {} + for profile,data in pairs(SpellInfo) do + for k,spell in pairs(data) do + if table.contains(spell.vocations, vocId) then + table.insert(spells, spell) + end + end + end + return spells +end + +function Spells.filterSpellsByGroups(spells, groups) + local filtered = {} + for v,spell in pairs(spells) do + local spellGroups = Spells.getGroupIds(spell) + if table.equals(spellGroups, groups) then + table.insert(filtered, spell) + end + end + return filtered +end + +function Spells.getGroupIds(spell) + local groups = {} + for k,_ in pairs(spell.group) do + table.insert(groups, k) + end + return groups +end + +function Spells.getImageClip(id, profile) + return (((id-1)%12)*SpelllistSettings[profile].iconSize.width) .. ' ' + .. ((math.ceil(id/12)-1)*SpelllistSettings[profile].iconSize.height) .. ' ' + .. SpelllistSettings[profile].iconSize.width .. ' ' + .. SpelllistSettings[profile].iconSize.height +end \ No newline at end of file diff --git a/modules/gamelib/textmessages.lua b/modules/gamelib/textmessages.lua new file mode 100644 index 0000000..43c0a6f --- /dev/null +++ b/modules/gamelib/textmessages.lua @@ -0,0 +1,30 @@ +local messageModeCallbacks = {} + +function g_game.onTextMessage(messageMode, message) + local callbacks = messageModeCallbacks[messageMode] + if not callbacks or #callbacks == 0 then + perror(string.format('Unhandled onTextMessage message mode %i: %s', messageMode, message)) + return + end + + for _, callback in pairs(callbacks) do + callback(messageMode, message) + end +end + +function registerMessageMode(messageMode, callback) + if not messageModeCallbacks[messageMode] then + messageModeCallbacks[messageMode] = {} + end + + table.insert(messageModeCallbacks[messageMode], callback) + return true +end + +function unregisterMessageMode(messageMode, callback) + if not messageModeCallbacks[messageMode] then + return false + end + + return table.removevalue(messageModeCallbacks[messageMode], callback) +end diff --git a/modules/gamelib/thing.lua b/modules/gamelib/thing.lua new file mode 100644 index 0000000..5dfa477 --- /dev/null +++ b/modules/gamelib/thing.lua @@ -0,0 +1,49 @@ +ThingCategoryItem = 0 +ThingCategoryCreature = 1 +ThingCategoryEffect = 2 +ThingCategoryMissile = 3 +ThingInvalidCategory = 4 +ThingLastCategory = ThingInvalidCategory + +ThingAttrGround = 0 +ThingAttrGroundBorder = 1 +ThingAttrOnBottom = 2 +ThingAttrOnTop = 3 +ThingAttrContainer = 4 +ThingAttrStackable = 5 +ThingAttrForceUse = 6 +ThingAttrMultiUse = 7 +ThingAttrWritable = 8 +ThingAttrWritableOnce = 9 +ThingAttrFluidContainer = 10 +ThingAttrSplash = 11 +ThingAttrNotWalkable = 12 +ThingAttrNotMoveable = 13 +ThingAttrBlockProjectile = 14 +ThingAttrNotPathable = 15 +ThingAttrPickupable = 16 +ThingAttrHangable = 17 +ThingAttrHookSouth = 18 +ThingAttrHookEast = 19 +ThingAttrRotateable = 20 +ThingAttrLight = 21 +ThingAttrDontHide = 22 +ThingAttrTranslucent = 23 +ThingAttrDisplacement = 24 +ThingAttrElevation = 25 +ThingAttrLyingCorpse = 26 +ThingAttrAnimateAlways = 27 +ThingAttrMinimapColor = 28 +ThingAttrLensHelp = 29 +ThingAttrFullGround = 30 +ThingAttrLook = 31 +ThingAttrCloth = 32 +ThingAttrMarket = 33 +ThingAttrNoMoveAnimation = 253 -- >= 1010, value = 16 +ThingAttrChargeable = 254 -- deprecated +ThingLastAttr = 255 + +SpriteMaskRed = 1 +SpriteMaskGreen = 2 +SpriteMaskBlue = 3 +SpriteMaskYellow = 4 \ No newline at end of file diff --git a/modules/gamelib/ui/uicreaturebutton.lua b/modules/gamelib/ui/uicreaturebutton.lua new file mode 100644 index 0000000..7fce910 --- /dev/null +++ b/modules/gamelib/ui/uicreaturebutton.lua @@ -0,0 +1,161 @@ +-- @docclass +UICreatureButton = extends(UIWidget, "UICreatureButton") + +local CreatureButtonColors = { + onIdle = {notHovered = '#888888', hovered = '#FFFFFF' }, + onTargeted = {notHovered = '#FF0000', hovered = '#FF8888' }, + onFollowed = {notHovered = '#00FF00', hovered = '#88FF88' } +} + +local LifeBarColors = {} -- Must be sorted by percentAbove +table.insert(LifeBarColors, {percentAbove = 92, color = '#00BC00' } ) +table.insert(LifeBarColors, {percentAbove = 60, color = '#50A150' } ) +table.insert(LifeBarColors, {percentAbove = 30, color = '#A1A100' } ) +table.insert(LifeBarColors, {percentAbove = 8, color = '#BF0A0A' } ) +table.insert(LifeBarColors, {percentAbove = 3, color = '#910F0F' } ) +table.insert(LifeBarColors, {percentAbove = -1, color = '#850C0C' } ) + +function UICreatureButton.create() + local button = UICreatureButton.internalCreate() + button:setFocusable(false) + button.creature = nil + button.isHovered = false + return button +end + +function UICreatureButton:setCreature(creature) + self.creature = creature +end + +function UICreatureButton:getCreature() + return self.creature +end + +function UICreatureButton:getCreatureId() + return self.creature:getId() +end + +function UICreatureButton:setup(creature, id) + if not id then + id = 0 + end + self:setId('CreatureButton_' .. id) + + self.lifeBarWidget = self:getChildById('lifeBar') + self.creatureWidget = self:getChildById('creature') + self.labelWidget = self:getChildById('label') + self.skullWidget = self:getChildById('skull') + self.emblemWidget = self:getChildById('emblem') +end + +function UICreatureButton:update() + local color = CreatureButtonColors.onIdle + local show = false + if self.creature == g_game.getAttackingCreature() then + color = CreatureButtonColors.onTargeted + elseif self.creature == g_game.getFollowingCreature() then + color = CreatureButtonColors.onFollowed + end + color = self.isHovered and color.hovered or color.notHovered + + if self.color == color then + return + end + self.color = color + + if color ~= CreatureButtonColors.onIdle.notHovered then + self.creatureWidget:setBorderWidth(1) + self.creatureWidget:setBorderColor(color) + self.labelWidget:setColor(color) + else + self.creatureWidget:setBorderWidth(0) + self.labelWidget:setColor(color) + end +end + +function UICreatureButton:creatureSetup(creature) + if self.creature ~= creature then + self.creature = creature + self.creatureWidget:setCreature(creature) + if self.creatureName ~= creature:getName() then + self.creatureName = creature:getName() + self.labelWidget:setText(creature:getName()) + end + end + + self:updateLifeBarPercent() + self:updateSkull() + self:updateEmblem() + self:update() +end + +function UICreatureButton:updateSkull() + if not self.creature then + return + end + local skullId = self.creature:getSkull() + if skullId == self.skullId then + return + end + self.skullId = skullId + + if skullId ~= SkullNone then + self.skullWidget:setWidth(self.skullWidget:getHeight()) + local imagePath = getSkullImagePath(skullId) + self.skullWidget:setImageSource(imagePath) + self.labelWidget:setMarginLeft(5) + else + self.skullWidget:setWidth(0) + if self.creature:getEmblem() == EmblemNone then + self.labelWidget:setMarginLeft(2) + end + end +end + +function UICreatureButton:updateEmblem() + if not self.creature then + return + end + local emblemId = self.creature:getEmblem() + if self.emblemId == emblemId then + return + end + self.emblemId = emblemId + + if emblemId ~= EmblemNone then + self.emblemWidget:setWidth(self.emblemWidget:getHeight()) + local imagePath = getEmblemImagePath(emblemId) + self.emblemWidget:setImageSource(imagePath) + self.emblemWidget:setMarginLeft(5) + self.labelWidget:setMarginLeft(5) + else + self.emblemWidget:setWidth(0) + self.emblemWidget:setMarginLeft(0) + if self.creature:getSkull() == SkullNone then + self.labelWidget:setMarginLeft(2) + end + end +end + +function UICreatureButton:updateLifeBarPercent() + if not self.creature then + return + end + local percent = self.creature:getHealthPercent() + if self.percent == percent then + return + end + + self.percent = percent + self.lifeBarWidget:setPercent(percent) + + local color + for i, v in pairs(LifeBarColors) do + if percent > v.percentAbove then + color = v.color + break + end + end + + self.lifeBarWidget:setBackgroundColor(color) +end \ No newline at end of file diff --git a/modules/gamelib/ui/uiminimap.lua b/modules/gamelib/ui/uiminimap.lua new file mode 100644 index 0000000..3672280 --- /dev/null +++ b/modules/gamelib/ui/uiminimap.lua @@ -0,0 +1,316 @@ +function UIMinimap:onCreate() + self.autowalk = true +end + +function UIMinimap:onSetup() + self.flagWindow = nil + self.flags = {} + self.alternatives = {} + self.onAddAutomapFlag = function(pos, icon, description) self:addFlag(pos, icon, description) end + self.onRemoveAutomapFlag = function(pos, icon, description) self:removeFlag(pos, icon, description) end + connect(g_game, { + onAddAutomapFlag = self.onAddAutomapFlag, + onRemoveAutomapFlag = self.onRemoveAutomapFlag, + }) +end + +function UIMinimap:onDestroy() + for _,widget in pairs(self.alternatives) do + widget:destroy() + end + self.alternatives = {} + disconnect(g_game, { + onAddAutomapFlag = self.onAddAutomapFlag, + onRemoveAutomapFlag = self.onRemoveAutomapFlag, + }) + self:destroyFlagWindow() + self.flags = {} +end + +function UIMinimap:onVisibilityChange() + if not self:isVisible() then + self:destroyFlagWindow() + end +end + +function UIMinimap:onCameraPositionChange(cameraPos) + if self.cross then + self:setCrossPosition(self.cross.pos) + end +end + +function UIMinimap:hideFloor() + self.floorUpWidget:hide() + self.floorDownWidget:hide() +end + +function UIMinimap:hideZoom() + self.zoomInWidget:hide() + self.zoomOutWidget:hide() +end + +function UIMinimap:disableAutoWalk() + self.autowalk = false +end + +function UIMinimap:load() + local settings = g_settings.getNode('Minimap') + if settings then + if settings.flags then + for _,flag in pairs(settings.flags) do + self:addFlag(flag.position, flag.icon, flag.description) + end + end + self:setZoom(settings.zoom) + end +end + +function UIMinimap:save() + local settings = { flags={} } + for _,flag in pairs(self.flags) do + if not flag.temporary then + table.insert(settings.flags, { + position = flag.pos, + icon = flag.icon, + description = flag.description, + }) + end + end + settings.zoom = self:getZoom() + g_settings.setNode('Minimap', settings) +end + +local function onFlagMouseRelease(widget, pos, button) + if button == MouseRightButton then + local menu = g_ui.createWidget('PopupMenu') + menu:setGameMenu(true) + menu:addOption(tr('Delete mark'), function() widget:destroy() end) + menu:display(pos) + return true + end + return false +end + +function UIMinimap:setCrossPosition(pos) + local cross = self.cross + if not self.cross then + cross = g_ui.createWidget('MinimapCross', self) + cross:setIcon('/images/game/minimap/cross') + self.cross = cross + end + + pos.z = self:getCameraPosition().z + cross.pos = pos + if pos then + self:centerInPosition(cross, pos) + else + cross:breakAnchors() + end +end + +function UIMinimap:addFlag(pos, icon, description, temporary) + if not pos or not icon then return end + local flag = self:getFlag(pos, icon, description) + if flag or not icon then + return + end + temporary = temporary or false + + flag = g_ui.createWidget('MinimapFlag') + self:insertChild(1, flag) + flag.pos = pos + flag.description = description + flag.icon = icon + flag.temporary = temporary + if type(tonumber(icon)) == 'number' then + flag:setIcon('/images/game/minimap/flag' .. icon) + else + flag:setIcon(resolvepath(icon, 1)) + end + flag:setTooltip(description) + flag.onMouseRelease = onFlagMouseRelease + flag.onDestroy = function() table.removevalue(self.flags, flag) end + table.insert(self.flags, flag) + self:centerInPosition(flag, pos) +end + +function UIMinimap:addAlternativeWidget(widget, pos, maxZoom) + widget.pos = pos + widget.maxZoom = maxZoom or 0 + widget.minZoom = minZoom + table.insert(self.alternatives, widget) +end + +function UIMinimap:setAlternativeWidgetsVisible(show) + local layout = self:getLayout() + layout:disableUpdates() + for _,widget in pairs(self.alternatives) do + if show then + self:insertChild(1, widget) + self:centerInPosition(widget, widget.pos) + else + self:removeChild(widget) + end + end + layout:enableUpdates() + layout:update() +end + +function UIMinimap:onZoomChange(zoom) + for _,widget in pairs(self.alternatives) do + if (not widget.minZoom or widget.minZoom >= zoom) and widget.maxZoom <= zoom then + widget:show() + else + widget:hide() + end + end +end + +function UIMinimap:getFlag(pos) + for _,flag in pairs(self.flags) do + if flag.pos.x == pos.x and flag.pos.y == pos.y and flag.pos.z == pos.z then + return flag + end + end + return nil +end + +function UIMinimap:removeFlag(pos, icon, description) + local flag = self:getFlag(pos) + if flag then + flag:destroy() + end +end + +function UIMinimap:reset() + self:setZoom(0) + if self.cross then + self:setCameraPosition(self.cross.pos) + end +end + +function UIMinimap:move(x, y) + local cameraPos = self:getCameraPosition() + local scale = self:getScale() + if scale > 1 then scale = 1 end + local dx = x/scale + local dy = y/scale + local pos = {x = cameraPos.x - dx, y = cameraPos.y - dy, z = cameraPos.z} + self:setCameraPosition(pos) +end + +function UIMinimap:onMouseWheel(mousePos, direction) + local keyboardModifiers = g_keyboard.getModifiers() + if direction == MouseWheelUp and keyboardModifiers == KeyboardNoModifier then + self:zoomIn() + elseif direction == MouseWheelDown and keyboardModifiers == KeyboardNoModifier then + self:zoomOut() + elseif direction == MouseWheelDown and keyboardModifiers == KeyboardCtrlModifier then + self:floorUp(1) + elseif direction == MouseWheelUp and keyboardModifiers == KeyboardCtrlModifier then + self:floorDown(1) + end +end + +function UIMinimap:onMousePress(pos, button) + if not self:isDragging() then + self.allowNextRelease = true + end +end + +function UIMinimap:onMouseRelease(pos, button) + if not self.allowNextRelease then return true end + self.allowNextRelease = false + + local mapPos = self:getTilePosition(pos) + if not mapPos then return end + + if button == MouseLeftButton then + local player = g_game.getLocalPlayer() + if self.autowalk then + player:autoWalk(mapPos) + end + return true + elseif button == MouseRightButton then + local menu = g_ui.createWidget('PopupMenu') + menu:setGameMenu(true) + menu:addOption(tr('Create mark'), function() self:createFlagWindow(mapPos) end) + menu:display(pos) + return true + end + return false +end + +function UIMinimap:onDragEnter(pos) + self.dragReference = pos + self.dragCameraReference = self:getCameraPosition() + return true +end + +function UIMinimap:onDragMove(pos, moved) + local scale = self:getScale() + local dx = (self.dragReference.x - pos.x)/scale + local dy = (self.dragReference.y - pos.y)/scale + local pos = {x = self.dragCameraReference.x + dx, y = self.dragCameraReference.y + dy, z = self.dragCameraReference.z} + self:setCameraPosition(pos) + return true +end + +function UIMinimap:onDragLeave(widget, pos) + return true +end + +function UIMinimap:onStyleApply(styleName, styleNode) + for name,value in pairs(styleNode) do + if name == 'autowalk' then + self.autowalk = value + end + end +end + +function UIMinimap:createFlagWindow(pos) + if self.flagWindow then return end + if not pos then return end + + self.flagWindow = g_ui.createWidget('MinimapFlagWindow', rootWidget) + + local positionLabel = self.flagWindow:getChildById('position') + local description = self.flagWindow:getChildById('description') + local okButton = self.flagWindow:getChildById('okButton') + local cancelButton = self.flagWindow:getChildById('cancelButton') + + positionLabel:setText(string.format('%i, %i, %i', pos.x, pos.y, pos.z)) + + local flagRadioGroup = UIRadioGroup.create() + for i=0,19 do + local checkbox = self.flagWindow:getChildById('flag' .. i) + checkbox.icon = i + flagRadioGroup:addWidget(checkbox) + end + + flagRadioGroup:selectWidget(flagRadioGroup:getFirstWidget()) + + local successFunc = function() + self:addFlag(pos, flagRadioGroup:getSelectedWidget().icon, description:getText()) + self:destroyFlagWindow() + end + + local cancelFunc = function() + self:destroyFlagWindow() + end + + okButton.onClick = successFunc + cancelButton.onClick = cancelFunc + + self.flagWindow.onEnter = successFunc + self.flagWindow.onEscape = cancelFunc + + self.flagWindow.onDestroy = function() flagRadioGroup:destroy() end +end + +function UIMinimap:destroyFlagWindow() + if self.flagWindow then + self.flagWindow:destroy() + self.flagWindow = nil + end +end diff --git a/modules/gamelib/util.lua b/modules/gamelib/util.lua new file mode 100644 index 0000000..e3abf07 --- /dev/null +++ b/modules/gamelib/util.lua @@ -0,0 +1,11 @@ +function postostring(pos) + return pos.x .. " " .. pos.y .. " " .. pos.z +end + +function dirtostring(dir) + for k,v in pairs(Directions) do + if v == dir then + return k + end + end +end \ No newline at end of file diff --git a/otclient_dx.exe b/otclient_dx.exe new file mode 100644 index 0000000..3c17adf Binary files /dev/null and b/otclient_dx.exe differ diff --git a/otclient_gl.exe b/otclient_gl.exe new file mode 100644 index 0000000..040955b Binary files /dev/null and b/otclient_gl.exe differ diff --git a/pdb/otclient_dx.pdb b/pdb/otclient_dx.pdb new file mode 100644 index 0000000..d70731a Binary files /dev/null and b/pdb/otclient_dx.pdb differ diff --git a/pdb/otclient_gl.pdb b/pdb/otclient_gl.pdb new file mode 100644 index 0000000..e974d7b Binary files /dev/null and b/pdb/otclient_gl.pdb differ diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..378f01c --- /dev/null +++ b/server/README.md @@ -0,0 +1,5 @@ +Here are tools for server + +all lua scripts are made for and tested on latest tfs (1.2/1.3) + +add json.lua to data/lib/core and then in core.lua add: dofile('data/lib/core/json.lua') diff --git a/server/json.lua b/server/json.lua new file mode 100644 index 0000000..22cbb47 --- /dev/null +++ b/server/json.lua @@ -0,0 +1,399 @@ +-- add to lib/core, later add dofile in lib/core/core.lua + +-- +-- json.lua +-- +-- Copyright (c) 2018 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +json = { _version = "0.1.1" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if val[1] ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end diff --git a/server/shop/shop.lua b/server/shop/shop.lua new file mode 100644 index 0000000..9e5a7d8 --- /dev/null +++ b/server/shop/shop.lua @@ -0,0 +1,335 @@ +-- BETA VERSION, net tested yet +-- Insteuction: +-- creaturescripts.xml +-- and in login.lua player:registerEvent("Shop") +-- create sql table shop_history +-- set variables +-- set up function init(), add there items and categories, follow examples +-- set up callbacks at the bottom to add player item/outfit/whatever you want + +local SHOP_EXTENDED_OPCODE = 201 +local SHOP_OFFERS = {} +local SHOP_CALLBACKS = {} +local SHOP_CATEGORIES = nil +local SHOP_BUY_URL = "http://otland.net" -- can be empty +local SHOP_AD = { -- can be nil + image = "https://s3.envato.com/files/62273611/PNG%20Blue/Banner%20blue%20468x60.png", + url = "http://otclient.ovh", + text = "" +} + +--[[ SQL TABLE + +CREATE TABLE `shop_history` ( + `id` int(11) NOT NULL, + `account` int(11) NOT NULL, + `player` int(11) NOT NULL, + `date` datetime NOT NULL, + `title` varchar(100) NOT NULL, + `cost` int(11) NOT NULL, + `details` varchar(500) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE `shop_history` + ADD PRIMARY KEY (`id`); +ALTER TABLE `shop_history` + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; + +]]-- + +function init() + -- print(json.encode(g_game.getLocalPlayer():getOutfit())) -- in console in otclient, will print current outfit and mount + + SHOP_CATEGORIES = {} + + local category1 = addCategory({ + type="item", + item=ItemType(2160):getClientId(), + count=100, + name="Items" + }) + local category2 = addCategory({ + type="outfit", + name="Outfits", + outfit={ + mount=0, + feet=114, + legs=114, + body=116, + type=143, + auxType=0, + addons=3, + head=2, + rotating=true + } + }) + local category3 = addCategory({ + type="image", + image="http://otclient.ovh/images/137.png", + name="Category with http image" + }) + local category4 = addCategory({ + type="image", + image="/data/images/game/states/electrified.png", + name="Category with local image" + }) + + + category1.addItem(1, 2160, 1, "1 Crystal coin", "description of cristal coin") + category1.addItem(5, 2160, 5, "5 Crystal coin", "description of cristal coin") + category1.addItem(50, 2160, 50, "50 Crystal coin", "description of cristal coin") + category1.addItem(90, 2160, 100, "100 Crystal coin", "description of cristal coin") + category1.addItem(200, 2493, 1, "Demon helmet1", "woo\ndemon helmet\nnice, you should buy it") + category1.addItem(1, 2160, 1, "1 Crystal coin1", "description of cristal coin") + category1.addItem(5, 2160, 5, "5 Crystal coin1", "description of cristal coin") + category1.addItem(50, 2160, 50, "50 Crystal coin1", "description of cristal coin") + category1.addItem(90, 2160, 100, "100 Crystal coin1", "description of cristal coin") + category1.addItem(200, 2493, 1, "Demon helmet2", "woo\ndemon helmet\nnice, you should buy it") + category1.addItem(1, 2160, 1, "1 Crystal coin3", "description of cristal coin") + category1.addItem(5, 2160, 5, "5 Crystal coin3", "description of cristal coin") + category1.addItem(50, 2160, 50, "50 Crystal coin3", "description of cristal coin") + category1.addItem(90, 2160, 100, "100 Crystal coin3", "description of cristal coin") + category1.addItem(200, 2493, 1, "Demon helmet3", "wooxD\ndemon helmet\nnice, you should buy it") + + category2.addOutfit(500, { + mount=0, + feet=114, + legs=114, + body=116, + type=143, + auxType=0, + addons=3, + head=2, + rotating=true + }, "title of this cool outfit or whatever", "this is your new cool outfit. You can buy it here.\nsrlsy") + category2.addOutfit(100, { + mount=682, + feet=0, + legs=0, + body=0, + type=143, + auxType=0, + addons=0, + head=0, + rotating=true + }, "MOUNT!!!", "DOUBLE CLICK TO BUY THIS MOUNT. IDK NAME") + + category2.addOutfit(100, { + mount=0, + feet=0, + legs=0, + body=0, + type=35, + auxType=0, + addons=0, + head=0, + rotating=true + }, "Demon outfit", "Want be a demon?\nNo problem") + category2.addOutfit(100, { + mount=0, + feet=0, + legs=0, + body=0, + type=35, + auxType=0, + addons=0, + head=0, + rotating=false + }, "Demon outfit2", "This one is not rotating") + + category4.addImage(10000, "/data/images/game/states/haste.png", "Offer with local image", "another local image\n/data/images/game/states/haste.png") + category4.addImage(10000, "http://otclient.ovh/images/freezing.png", "Offer with remote image and custom buy action", "blalasdasd image\nhttp://otclient.ovh/images/freezing.png", customImageBuyAction) +end + +function addCategory(data) + data['offers'] = {} + table.insert(SHOP_CATEGORIES, data) + table.insert(SHOP_CALLBACKS, {}) + local index = #SHOP_CATEGORIES + return { + addItem = function(cost, itemId, count, title, description, callback) + if not callback then + callback = defaultItemBuyAction + end + table.insert(SHOP_CATEGORIES[index]['offers'], { + cost=cost, + type="item", + item=ItemType(itemId):getClientId(), -- displayed + itemId=itemId, + count=count, + title=title, + description=description + }) + table.insert(SHOP_CALLBACKS[index], callback) + end, + addOutfit = function(cost, outfit, title, description, callback) + if not callback then + callback = defaultOutfitBuyAction + end + table.insert(SHOP_CATEGORIES[index]['offers'], { + cost=cost, + type="outfit", + outfit=outfit, + title=title, + description=description + }) + table.insert(SHOP_CALLBACKS[index], callback) + end, + addImage = function(cost, image, title, description, callback) + if not callback then + callback = defaultImageBuyAction + end + table.insert(SHOP_CATEGORIES[index]['offers'], { + cost=cost, + type="image", + image=image, + title=title, + description=description + }) + table.insert(SHOP_CALLBACKS[index], callback) + end + } +end + +function getPoints(player) + local points = 0 + local resultId = db.storeQuery("SELECT `premium_points` FROM `accounts` WHERE `id` = " .. player:getAccountId()) + if resultId ~= false then + points = result.getDataInt(resultId, "premium_points") + result.free(resultId) + end + return points +end + +function getStatus(player) + local status = { + ad = SHOP_AD, + points = getPoints(player), + buyUrl = SHOP_BUY_URL + } + return status +end + +function sendJSON(player, action, data, forceStatus) + local status = nil + if not player:getStorageValue(1150001) or player:getStorageValue(1150001) + 10 < os.time() or forceStatus then + status = getStatus(player) + end + player:setStorageValue(1150001, os.time()) + + local msg = NetworkMessage() + msg:addByte(50) + msg:addByte(SHOP_EXTENDED_OPCODE) + msg:addString(json.encode({action = action, data = data, status = status})) + msg:sendToPlayer(player) +end + +function sendMessage(player, title, msg, forceStatus) + sendJSON(player, "message", {title=title, msg=msg}, forceStatus) +end + +function onExtendedOpcode(player, opcode, buffer) + if opcode ~= SHOP_EXTENDED_OPCODE then + return false + end + local status, json_data = pcall(function() return json.decode(buffer) end) + if not status then + return false + end + + local action = json_data['action'] + local data = json_data['data'] + if not action or not data then + return false + end + + if SHOP_CATEGORIES == nil then + init() + end + + if action == 'init' then + sendJSON(player, "categories", SHOP_CATEGORIES) + elseif action == 'buy' then + processBuy(player, data) + elseif action == "history" then + sendHistory(player) + end + return true +end + +function processBuy(player, data) + local categoryId = tonumber(data["category"]) + local offerId = tonumber(data["offer"]) + local offer = SHOP_CATEGORIES[categoryId]['offers'][offerId] + local callback = SHOP_CALLBACKS[categoryId][offerId] + if not offer or not callback or data["title"] ~= offer["title"] or data["cost"] ~= offer["cost"] then + sendJSON(player, "categories", SHOP_CATEGORIES) -- refresh categories, maybe invalid + return sendMessage(player, "Error!", "Invalid offer") + end + local points = getPoints(player) + if not offer['cost'] or offer['cost'] > points or points < 1 then + return sendMessage(player, "Error!", "You don't have enough points to buy " .. offer['title'] .."!", true) + end + local status = callback(player, offer) + if status == true then + db.query("UPDATE `accounts` set `premium_points` = `premium_points` - " .. offer['cost'] .. " WHERE `id` = " .. player:getAccountId()) + db.asyncQuery("INSERT INTO `shop_history` (`account`, `player`, `date`, `title`, `cost`, `details`) VALUES ('" .. player:getAccountId() .. "', '" .. player:getGuid() .. "', NOW(), " .. db.escapeString(offer['title']) .. ", " .. db.escapeString(offer['cost']) .. ", " .. db.escapeString(json.encode(offer)) .. ")") + return sendMessage(player, "Success!", "You bought " .. offer['title'] .."!", true) + end + if status == nil or status == false then + status = "Unknown error while buying " .. offer['title'] + end + sendMessage(player, "Error!", status) +end + +function sendHistory(player) + if player:getStorageValue(1150002) and player:getStorageValue(1150002) + 10 > os.time() then + return -- min 10s delay + end + player:setStorageValue(1150002, os.time()) + + local history = {} + local resultId = db.storeQuery("SELECT * FROM `shop_history` WHERE `account` = " .. player:getAccountId() .. " order by `id` DESC") + + if resultId ~= false then + repeat + local details = result.getDataString(resultId, "details") + local status, json_data = pcall(function() return json.decode(details) end) + if not status then + json_data = { + type = "image", + title = result.getDataString(resultId, "title"), + cost = result.getDataInt(resultId, "cost") + } + end + table.insert(history, json_data) + history[#history]["description"] = "Bought on " .. result.getDataString(resultId, "date") .. " for " .. result.getDataInt(resultId, "cost") .. " points." + until not result.next(resultId) + result.free(resultId) + end + + sendJSON(player, "history", history) +end + +-- BUY CALLBACKS +-- May be useful: print(json.encode(offer)) + +function defaultItemBuyAction(player, offer) + -- todo: check if has capacity + if player:addItem(offer["itemId"], offer["count"], false) then + return true + end + return "Can't add item! Do you have enough space?" +end + +function defaultOutfitBuyAction(player, offer) + return "default outfit buy action is not implemented" +end + +function defaultImageBuyAction(player, offer) + return "default image buy action is not implemented" +end + +function customImageBuyAction(player, offer) + return "custom image buy action is not implemented. Offer: " .. offer['title'] +end \ No newline at end of file