diff --git a/.gitignore b/.gitignore index ad308601..8fbefabd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,5 @@ -# Git -*.orig - # cache -/cache/template/* -/cache/alphaMaps/* -/cache/setup/* +/cache/* # extract from MPQ /setup/mpqdata/* @@ -17,10 +12,8 @@ /static/widgets/searchbox.js /static/widgets/searchbox/searchbox.html /static/download/searchplugins/aowow.xml -/config/config.php +/config/* /datasets/* -!/datasets/zones -# /datasets/item-scaling # extracted sounds /static/wowsounds/* @@ -30,7 +23,7 @@ /static/images/wow/icons/medium/* /static/images/wow/icons/small/* /static/images/wow/icons/tiny/* -!/static/images/wow/icons/tiny/quest_* +!/static/images/wow/icons/tiny/quest_[end|start] /static/images/wow/hunterpettalents/* /static/images/wow/Interface/* /static/images/wow/loadingscreens/* @@ -50,3 +43,7 @@ /static/uploads/signatures/* /static/uploads/temp/* /static/uploads/guide/images/* + +# composer +/includes/libs/* +composer.phar diff --git a/.htaccess b/.htaccess index 0c4ad8e7..0e0530c5 100644 --- a/.htaccess +++ b/.htaccess @@ -26,8 +26,10 @@ AddDefaultCharset utf8 # UHD screenshots can get pretty large (cannot be set in config) + php_value upload_max_filesize 20M php_value post_max_size 25M + RewriteEngine on # RewriteBase /~user/localPath/ # enable if the rules do not work, when they should diff --git a/README.md b/README.md index 145f4c51..e182aced 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Build Status -![fuck it ship it](http://forthebadge.com/images/badges/fuck-it-ship-it.svg) +![fuck it ship it](https://forthebadge.com/badges/fuck-it-ship-it.svg) ## Introduction @@ -27,7 +27,8 @@ Also, this project is not meant to be used for commercial purposes of any kind! + [Internationalization](https://www.php.net/manual/en/book.intl.php) + [GNU Multiple Precision](https://www.php.net/manual/en/book.gmp.php) (When using TrinityCore as auth source) + MySQL ≥ 5.7.0 OR MariaDB ≥ 10.6.4 OR similar -+ [TDB 335.21101](https://github.com/TrinityCore/TrinityCore/releases/tag/TDB335.21101) (no other other providers are supported at this time) ++ [Composer](https://getcomposer.org/download/) ++ [TDB 335.25101](https://github.com/TrinityCore/TrinityCore/releases/tag/TDB335.25101) including updates up to [TrinityCore/TrinityCore@f3b691d](https://github.com/TrinityCore/TrinityCore/commit/f3b691dcb085014ec3f0e2c60ab94fc9c00e8aa8) (no other other providers are supported at this time) + WIN: php.exe needs to be added to the `PATH` system variable, if it isn't already. + Tools require cmake: Please refer to the individual repositories for detailed information + [MPQExtractor](https://github.com/Sarjuuk/MPQExtractor) / [FFmpeg](https://ffmpeg.org/download.html) / (optional: [BLPConverter](https://github.com/Sarjuuk/BLPConverter)) @@ -38,7 +39,7 @@ audio processing may require [lame](https://sourceforge.net/projects/lame/files/ #### Highly Recommended -+ setting the following configuration values on your TrinityCore server will greatly increase the accuracy of spawn points ++ setting the following configuration values on your TrinityCore server (and running it once) will greatly increase the accuracy of spawn points > Calculate.Creature.Zone.Area.Data = 1 > Calculate.Gameobject.Zone.Area.Data = 1 @@ -50,8 +51,10 @@ audio processing may require [lame](https://sourceforge.net/projects/lame/files/ `git clone git@github.com:Sarjuuk/MPQExtractor.git MPQExtractor` #### 2. Prepare the database -Ensure that the account you are going to use has **full** access on the database AoWoW is going to occupy and ideally only **read** access on the world database you are going to reference. -Import files 01 - 03 from `setup/sql/` in order into the AoWoW database `mysql -p {your-db-here} < setup/sql/01-db_structure.sql`, etc. +Ensure that the account you are going to use has **full** access on the database AoWoW is going to occupy and ideally only **read** access on the world and optionally auth and characters databases you are going to reference. +Import files 01 - 03 from `setup/sql/` in order into the AoWoW database `mysql --default-character-set=utf8 -p {your-db-here} < setup/sql/01-db_structure.sql`, etc. + +**Optional**: If you are using MySQL ≥ 8.4.0 and want to support fulltext search for locale zhCN, additionally import `setup/sql/04-db_optional_mysql_only.sql`. Enables this in settings after AoWoW has been set up. #### 3. Server created files See to it, that the web server is able to write the following directories and their children. If they are missing, the setup will create them with appropriate permissions @@ -82,17 +85,17 @@ Extract the following directories from the client archives into `setup/mpqdata/` > \/Interface/FlavorImages > \/Interface/Calendar/Holidays/ > \/Sound/ - - .. optionally (not used in AoWoW): - > \/Interface/Glues/Loadingscreens/ - > \/Interface/Glues/Credits/ #### 5. Reencode the audio files WAV-files need to be reencoded as `ogg/vorbis` and some MP3s may identify themselves as `application/octet-stream` instead of `audio/mpeg`. * [example for WIN](https://gist.github.com/Sarjuuk/d77b203f7b71d191509afddabad5fc9f) * [example for \*nix](https://gist.github.com/Sarjuuk/1f05ef2affe49a7e7ca0fad7b01c081d) -#### 6. Run the initial setup from the CLI +#### 6. Install dependencies with composer +`php composer.phar install --no-dev` on a project level composer install, or +`composer install --no-dev` on a system level composer install + +#### 7. Run the initial setup from the CLI `php aowow --setup`. This should guide you through with minimal input required from your end, but will take some time though, especially compiling the zone-images. Use it to familiarize yourself with the other functions this setup has. Yes, I'm dead serious: *Go read the code!* It will help you understand how to configure AoWoW and keep it in sync with your world database. When you've created your admin account you are done. @@ -101,10 +104,10 @@ When you've created your admin account you are done. ## Troubleshooting Q: The Page appears white, without any styles. -A: The static content is not being displayed. You are either using SSL and AoWoW is unable to detect it or STATIC_HOST is not defined properly. Either way this can be fixed via config `php aowow --siteconfig` +A: The static content is not being displayed. You are either using SSL and AoWoW is unable to detect it or STATIC_HOST is not defined properly. Either way this can be fixed via config `php aowow --configure` Q: Fatal error: Can't inherit abstract function \ (previously declared abstract in \) in \ -A: You are using cache optimization modules for php, that are in conflict with each other. (Zend OPcache, XCache, ..) Disable all but one. +A: You are using multiple cache optimization modules for php that are in conflict with each other. (Zend OPcache, XCache, ..) Disable all but one. Q: Some generated images appear distorted or have alpha-channel issues. A: Image compression is beyond my understanding, so i am unable to fix these issues within the blpReader. @@ -118,7 +121,7 @@ Q: I'm getting random javascript errors! A: Some server configurations or external services (like Cloudflare) come with modules, that automatically minify js and css files. Sometimes they break in the process. Disable the module in this case. Q: Some search results within the profiler act rather strange. How does it work? -A: Whenever you try to view a new character, AoWoW needs to fetch it first. Since the data is structured for the needs of TrinityCore and not for easy viewing, AoWoW needs to save and restructure it locally. To this end, every char request is placed in a queue. While the queue is not empty, a single instance of `prQueue` is run in the background as not to overwhelm the characters database with requests. This also means, some more exotic search queries can't be run against the characters database and have to use the incomplete/outdated cached profiles of AoWoW. +A: Whenever you try to view a new character, AoWoW needs to fetch it first. Since the data is structured for the needs of TrinityCore and not for easy viewing, AoWoW needs to save and restructure it locally. To this end, every char request is placed in a queue. While the queue is not empty, a single instance of `prQueue` is run in the background as not to overwhelm the characters database with requests. This also means complex search queries can't be run against the characters database and have to use the incomplete/outdated cached profiles of AoWoW. Q: Screenshot upload fails, because the file size is too large and/or the subdirectories are visible from the web! A: That's a web server configuration issue. If you are using Apache you may need to [enable the use of .htaccess](http://httpd.apache.org/docs/2.4/de/mod/core.html#allowoverride). Other servers require individual configuration. @@ -138,4 +141,4 @@ A: A search is only conducted against the currently used locale. You may have on Said website with the red smiling rocket, for providing this beautiful website! Please do not regard this project as blatant rip-off, rather as "We do really liked your presentation, but since time and content progresses, you are sadly no longer supplying the data we need". -![uses badges](http://forthebadge.com/images/badges/uses-badges.svg) +![uses badges](https://forthebadge.com/badges/uses-badges.svg) diff --git a/aowow b/aowow index 428d109f..3e1adb62 100755 --- a/aowow +++ b/aowow @@ -9,6 +9,9 @@ if (PHP_SAPI === 'cli' && getcwd().DIRECTORY_SEPARATOR.'aowow' != __FILE__) require_once 'includes/kernel.php'; require_once 'includes/setup/cli.class.php'; require_once 'includes/setup/timer.class.php'; +require_once 'includes/setup/datatypes/primitives.php'; +require_once 'includes/setup/files/binaryfile.class.php'; +require_once 'includes/setup/files/dbcfile.class.php'; require_once 'setup/setup.php'; diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..fc43b9aa --- /dev/null +++ b/composer.json @@ -0,0 +1,30 @@ +{ + "name": "aowow/aowow", + "description": "Server and client database visualization for World of Warcraft/TrinityCore v3.3.5a, including community tools.", + "version": "2.0", + "type": "project", + "keywords": ["World of Warcraft", "wow", "database", "db", "frontend", "Wrath of the Lich King", "wotlk", "335a", "3.3.5a"], + "authors": [ + { + "name": "Sarjuuk", + "role": "Developer" + } + ], + "config": { + "vendor-dir": "includes/libs" + }, + "require": { + "dibi/dibi": "^5.1", + "php": "8.2 - 8.4", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-gd": "*", + "ext-mysqli": "*", + "ext-fileinfo": "*", + "ext-intl": "*" + }, + "require-dev": { + "jfcherng/php-diff": "6.16", + "triggerhappy/mpq": "dev-master" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..8d9a0b87 --- /dev/null +++ b/composer.lock @@ -0,0 +1,454 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "25b289a987a7600746f0fd1f2491864b", + "packages": [ + { + "name": "dibi/dibi", + "version": "v5.1.0", + "source": { + "type": "git", + "url": "https://github.com/dg/dibi.git", + "reference": "32b6976209859f61eb79380c5a8904ea33db47df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dg/dibi/zipball/32b6976209859f61eb79380c5a8904ea33db47df", + "reference": "32b6976209859f61eb79380c5a8904ea33db47df", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "replace": { + "dg/dibi": "*" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.0", + "nette/di": "^3.1", + "nette/tester": "^2.5", + "phpstan/phpstan-nette": "^2.0@stable", + "tracy/tracy": "^2.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "psr-4": { + "Dibi\\": "src/Dibi" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + } + ], + "description": "Dibi is Database Abstraction Library for PHP", + "homepage": "https://dibiphp.com", + "keywords": [ + "access", + "database", + "dbal", + "mssql", + "mysql", + "odbc", + "oracle", + "pdo", + "postgresql", + "sqlite", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/dg/dibi/issues", + "source": "https://github.com/dg/dibi/tree/v5.1.0" + }, + "time": "2025-08-06T22:26:19+00:00" + } + ], + "packages-dev": [ + { + "name": "chdemko/sorted-collections", + "version": "1.0.10", + "source": { + "type": "git", + "url": "https://github.com/chdemko/php-sorted-collections.git", + "reference": "d9cf7021e6fda1eb68b9f35caf99215327f6db76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/chdemko/php-sorted-collections/zipball/d9cf7021e6fda1eb68b9f35caf99215327f6db76", + "reference": "d9cf7021e6fda1eb68b9f35caf99215327f6db76", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.7", + "phpbench/phpbench": "^1.3", + "phpunit/phpunit": "^11.3", + "squizlabs/php_codesniffer": "^3.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "chdemko\\SortedCollection\\": "src/SortedCollection" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Christophe Demko", + "email": "chdemko@gmail.com", + "homepage": "https://chdemko.pagelab.univ-lr.fr/", + "role": "Developer" + } + ], + "description": "Sorted Collections for PHP >= 8.1", + "homepage": "https://php-sorted-collections.readthedocs.io/en/latest/?badge=latest", + "keywords": [ + "avl", + "collection", + "iterator", + "map", + "ordered", + "set", + "sorted", + "tree", + "treemap", + "treeset" + ], + "support": { + "issues": "https://github.com/chdemko/php-sorted-collections/issues", + "source": "https://github.com/chdemko/php-sorted-collections/tree/1.0.10" + }, + "time": "2024-08-04T14:31:40+00:00" + }, + { + "name": "jfcherng/php-color-output", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/jfcherng/php-color-output.git", + "reference": "6c7bf16686cc6a291647fcb87491640a2d5edd20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jfcherng/php-color-output/zipball/6c7bf16686cc6a291647fcb87491640a2d5edd20", + "reference": "6c7bf16686cc6a291647fcb87491640a2d5edd20", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19", + "liip/rmt": "^1.6", + "phan/phan": "^2 || ^3 || ^4", + "phpunit/phpunit": ">=7 <10", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jfcherng\\Utility\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jack Cherng", + "email": "jfcherng@gmail.com" + } + ], + "description": "Make your PHP command-line application colorful.", + "keywords": [ + "ansi-colors", + "color", + "command-line", + "str-color" + ], + "support": { + "issues": "https://github.com/jfcherng/php-color-output/issues", + "source": "https://github.com/jfcherng/php-color-output/tree/3.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.me/jfcherng/5usd", + "type": "custom" + } + ], + "time": "2021-05-27T02:45:54+00:00" + }, + { + "name": "jfcherng/php-diff", + "version": "6.16.0", + "source": { + "type": "git", + "url": "https://github.com/jfcherng/php-diff.git", + "reference": "8b49edeba6e367df22977fca0f0324b4a99b78a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jfcherng/php-diff/zipball/8b49edeba6e367df22977fca0f0324b4a99b78a0", + "reference": "8b49edeba6e367df22977fca0f0324b4a99b78a0", + "shasum": "" + }, + "require": { + "jfcherng/php-color-output": "^3", + "jfcherng/php-mb-string": "^1.4.6 || ^2", + "jfcherng/php-sequence-matcher": "^3.2.10 || ^4", + "php": ">=7.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.8", + "liip/rmt": "^1.6", + "phan/phan": "^5", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jfcherng\\Diff\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jack Cherng", + "email": "jfcherng@gmail.com" + }, + { + "name": "Chris Boulton", + "email": "chris.boulton@interspire.com" + } + ], + "description": "A comprehensive library for generating differences between two strings in multiple formats (unified, side by side HTML etc).", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/jfcherng/php-diff/issues", + "source": "https://github.com/jfcherng/php-diff/tree/6.16.0" + }, + "funding": [ + { + "url": "https://www.paypal.me/jfcherng/5usd", + "type": "custom" + } + ], + "time": "2024-03-05T08:44:05+00:00" + }, + { + "name": "jfcherng/php-mb-string", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/jfcherng/php-mb-string.git", + "reference": "8407bfefde47849c9e7c9594e6de2ac85a0f845d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jfcherng/php-mb-string/zipball/8407bfefde47849c9e7c9594e6de2ac85a0f845d", + "reference": "8407bfefde47849c9e7c9594e6de2ac85a0f845d", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "phan/phan": "^5", + "phpunit/phpunit": "^9 || ^10" + }, + "suggest": { + "ext-iconv": "Either \"ext-iconv\" or \"ext-mbstring\" is requried.", + "ext-mbstring": "Either \"ext-iconv\" or \"ext-mbstring\" is requried." + }, + "type": "library", + "autoload": { + "psr-4": { + "Jfcherng\\Utility\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jack Cherng", + "email": "jfcherng@gmail.com" + } + ], + "description": "A high performance multibytes sting implementation for frequently reading/writing operations.", + "support": { + "issues": "https://github.com/jfcherng/php-mb-string/issues", + "source": "https://github.com/jfcherng/php-mb-string/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/jfcherng/5usd", + "type": "custom" + } + ], + "time": "2023-04-17T14:23:16+00:00" + }, + { + "name": "jfcherng/php-sequence-matcher", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/jfcherng/php-sequence-matcher.git", + "reference": "d2038ac29627340a7458609072a8ba355e80ec5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jfcherng/php-sequence-matcher/zipball/d2038ac29627340a7458609072a8ba355e80ec5b", + "reference": "d2038ac29627340a7458609072a8ba355e80ec5b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "phan/phan": "^5", + "phpunit/phpunit": "^9 || ^10", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jfcherng\\Diff\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jack Cherng", + "email": "jfcherng@gmail.com" + }, + { + "name": "Chris Boulton", + "email": "chris.boulton@interspire.com" + } + ], + "description": "A longest sequence matcher. The logic is primarily based on the Python difflib package.", + "support": { + "issues": "https://github.com/jfcherng/php-sequence-matcher/issues", + "source": "https://github.com/jfcherng/php-sequence-matcher/tree/4.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/jfcherng/5usd", + "type": "custom" + } + ], + "time": "2023-05-21T07:57:08+00:00" + }, + { + "name": "triggerhappy/mpq", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/cipherxof/PHP-MPQ.git", + "reference": "628ca77b307d1cdf28b76da9750f3c8cbe958f49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cipherxof/PHP-MPQ/zipball/628ca77b307d1cdf28b76da9750f3c8cbe958f49", + "reference": "628ca77b307d1cdf28b76da9750f3c8cbe958f49", + "shasum": "" + }, + "require": { + "chdemko/sorted-collections": "1.0.*@dev", + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "5.2.*" + }, + "default-branch": true, + "type": "project", + "autoload": { + "psr-4": { + "TriggerHappy\\MPQ\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "description": "Handle the MPQ (MoPaQ) format natively from PHP with support for Warcraft III & Starcraft II.", + "keywords": [ + "MPQ", + "archive", + "mopaq", + "php-mpq", + "phpmpq", + "triggerhappy" + ], + "support": { + "issues": "https://github.com/cipherxof/PHP-MPQ/issues", + "source": "https://github.com/cipherxof/PHP-MPQ/tree/master" + }, + "time": "2018-07-31T04:22:01+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "triggerhappy/mpq": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "8.2 - 8.4", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-gd": "*", + "ext-mysqli": "*", + "ext-fileinfo": "*", + "ext-intl": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/config/extAuth.php.in b/config/extAuth.php.in index f05e4b83..6158987f 100644 --- a/config/extAuth.php.in +++ b/config/extAuth.php.in @@ -4,7 +4,7 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); - function extAuth(string &$usernameOrEmail, string $password, int &$userId = 0, int &$userGroup = -1) : int + function extAuth(string &$usernameOrEmail, #[\SensitiveParameter] string $password, int &$userId = 0, int &$userGroup = -1) : int { /* insert some auth mechanism here diff --git a/endpoints/aboutus/aboutus.php b/endpoints/aboutus/aboutus.php index f6ef07ba..524dfc0c 100644 --- a/endpoints/aboutus/aboutus.php +++ b/endpoints/aboutus/aboutus.php @@ -13,11 +13,11 @@ class AboutusBaseResponse extends TemplateResponse protected ?int $activeTab = parent::TAB_MORE; protected array $breadcrumb = [2, 0]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - if ($pageParam) + if ($rawParam) $this->generateError(); } diff --git a/endpoints/account/account.php b/endpoints/account/account.php index 2512d508..5c4ed8dd 100644 --- a/endpoints/account/account.php +++ b/endpoints/account/account.php @@ -42,19 +42,19 @@ class AccountBaseResponse extends TemplateResponse public ?array $bans; - public function __construct($pageParam) + public function __construct($rawParam) { if (!User::isLoggedIn()) $this->forwardToSignIn('account'); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void { array_unshift($this->title, Lang::account('settings')); - $user = DB::Aowow()->selectRow('SELECT `debug`, `email`, `description`, `avatar`, `wowicon`, `renameCooldown` FROM ?_account WHERE `id` = ?d', User::$id); + $user = DB::Aowow()->selectRow('SELECT `debug`, `email`, `description`, `avatar`, `wowicon`, `renameCooldown` FROM ::account WHERE `id` = %i', User::$id); Lang::sort('game', 'ra'); @@ -65,11 +65,11 @@ class AccountBaseResponse extends TemplateResponse /* Ban Popup */ /*************/ - $b = DB::Aowow()->select( + $b = DB::Aowow()->selectAssoc( 'SELECT ab.`end` AS "0", ab.`reason` AS "1", a.`username` AS "2" - FROM ?_account_banned ab - LEFT JOIN ?_account a ON a.`id` = ab.`staffId` - WHERE ab.`userId` = ?d AND ab.`typeMask` & ?d AND (ab.`end` = 0 OR ab.`end` > UNIX_TIMESTAMP())', + FROM ::account_banned ab + LEFT JOIN ::account a ON a.`id` = ab.`staffId` + WHERE ab.`userId` = %i AND ab.`typeMask` & %i AND (ab.`end` = 0 OR ab.`end` > UNIX_TIMESTAMP())', User::$id, ACC_BAN_TEMP | ACC_BAN_PERM ); @@ -99,7 +99,7 @@ class AccountBaseResponse extends TemplateResponse /* GENERAL */ // Modelviewer - if ($_ = DB::Aowow()->selectCell('SELECT `data` FROM ?_account_cookies WHERE `name` = ? AND `userId` = ?d', 'default_3dmodel', User::$id)) + if ($_ = DB::Aowow()->selectCell('SELECT `data` FROM ::account_cookies WHERE `name` = %s AND `userId` = %i', 'default_3dmodel', User::$id)) [$this->modelrace, $this->modelgender] = explode(',', $_); // Lists @@ -112,10 +112,10 @@ class AccountBaseResponse extends TemplateResponse // Username $this->curName = User::$username; - $this->renameCD = Util::formatTime(Cfg::get('ACC_RENAME_DECAY') * 1000); + $this->renameCD = DateTime::formatTimeElapsedFloat(Cfg::get('ACC_RENAME_DECAY') * 1000); if ($user['renameCooldown'] > time()) { - $locCode = implode('_', str_split(Lang::getLocale()->json(), 2)); // ._. + $locCode = substr_replace(Lang::getLocale()->json(), '_', 2, 0); // ._. $this->activeCD = (new \IntlDateFormatter($locCode, pattern: Lang::main('dateFmtIntl')))->format($user['renameCooldown']); } @@ -131,24 +131,6 @@ class AccountBaseResponse extends TemplateResponse $this->wowicon = $user['wowicon']; $this->avMode = $user['avatar']; - // status [reviewing, ok, rejected]? (only 2: rejected processed in js) - if (User::isPremium() && ($cuAvatars = DB::Aowow()->select('SELECT `id`, `name`, `current`, `size`, `status`, `when` FROM ?_account_avatars WHERE `userId` = ?d', User::$id))) - { - array_walk($cuAvatars, function (&$x) { - $x['when'] *= 1000; // uploaded timestamp expected as msec for some reason - $x['caption'] = $x['name']; // only used for getVisibleText, duplicates name? - $x['type'] = 1; // always 1 ?, Dialog-popup doesn't work without it - }); - - foreach ($cuAvatars as $a) - if ($a['status'] != AvatarMgr::STATUS_REJECTED) - $this->customicons[$a['id']] = $a['name']; - - // TODO - replace with array_find in PHP 8.4 - if ($x = array_filter($cuAvatars, fn($x) => $x['current'] > 0 )) - $this->customicon = array_pop($x)['id']; - } - /* PREMIUM */ $this->premium = User::isPremium(); @@ -156,8 +138,23 @@ class AccountBaseResponse extends TemplateResponse if (!$this->premium) return; + // required by js to calc reputation border color in user selection $this->reputation = User::getReputation(); + // status [reviewing, ok, rejected]? (only 2: rejected processed in js) + // * 'when': uploaded timestamp expected as msec for some reason + // * 'caption': only used for getVisibleText, duplicates name? + // * 'type': always 1 ?, Dialog-popup doesn't work without it + if ($cuAvatars = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `id`, `name`, `name` AS "caption", `current`, `size`, `status`, `when` * 1000 AS "when", 1 AS "type" FROM ::account_avatars WHERE `userId` = %i', User::$id)) + { + foreach ($cuAvatars as $id => $a) + if ($a['status'] != AvatarMgr::STATUS_REJECTED) + $this->customicons[$id] = $a['name']; + + if ($id = array_find_key($cuAvatars, fn($x) => $x['current'] > 0 )) + $this->customicon = $id; + } + // Avatar Manager $this->avatarManager = new Listview([ 'template' => 'avatar', diff --git a/endpoints/account/activate.php b/endpoints/account/activate.php index d437d75c..e56fad9e 100644 --- a/endpoints/account/activate.php +++ b/endpoints/account/activate.php @@ -52,13 +52,13 @@ class AccountActivateResponse extends TemplateResponse if (!$this->assertGET('key')) return Lang::main('intError'); - if (DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE `status` IN (?a) AND `token` = ?', [ACC_STATUS_NONE, ACC_STATUS_NEW], $this->_get['key'])) + if (DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE `status` IN %in AND `token` = %s', [ACC_STATUS_NONE, ACC_STATUS_NEW], $this->_get['key'])) { // don't remove the token yet. It's needed on signin page. - DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `userGroups` = ?d WHERE `token` = ?', ACC_STATUS_NONE, U_GROUP_NONE, $this->_get['key']); + DB::Aowow()->qry('UPDATE ::account SET `status` = %i, `statusTimer` = 0, `userGroups` = %i WHERE `token` = %s', ACC_STATUS_NONE, U_GROUP_NONE, $this->_get['key']); // fully apply block for further registration attempts from this ip - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d + 1, UNIX_TIMESTAMP() + ?d)', + DB::Aowow()->qry('REPLACE INTO ::account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (%s, %i, %i + 1, UNIX_TIMESTAMP() + %i)', User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); $this->success = true; diff --git a/endpoints/account/confirm-delete.php b/endpoints/account/confirm-delete.php index b5d0b6b9..c0cdcbdc 100644 --- a/endpoints/account/confirm-delete.php +++ b/endpoints/account/confirm-delete.php @@ -35,12 +35,12 @@ class AccountConfirmdeleteResponse extends TemplateResponse private bool $success = false; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) $this->generateError(); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -54,14 +54,14 @@ class AccountConfirmdeleteResponse extends TemplateResponse $msg = Lang::account('inputbox', 'error', 'purgeTokenUsed'); // display default confirm template - if ($this->assertGET('key') && DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP() AND `token` = ?', ACC_STATUS_PURGING, $this->_get['key'])) + if ($this->assertGET('key') && DB::Aowow()->selectCell('SELECT 1 FROM ::account WHERE `status` = %i AND `statusTimer` > UNIX_TIMESTAMP() AND `token` = %s', ACC_STATUS_PURGING, $this->_get['key'])) { $this->key = $this->_get['key']; return; } // perform action and display status - if ($this->assertPOST('key') && ($userId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP() AND `token` = ?', ACC_STATUS_PURGING, $this->_post['key']))) + if ($this->assertPOST('key') && ($userId = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE `status` = %i AND `statusTimer` > UNIX_TIMESTAMP() AND `token` = %s', ACC_STATUS_PURGING, $this->_post['key']))) { if ($this->_post['cancel']) $msg = $this->cancel($userId); @@ -79,7 +79,7 @@ class AccountConfirmdeleteResponse extends TemplateResponse private function cancel(int $userId) : string { - if (DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "" WHERE `id` = ?d', ACC_STATUS_NONE, $userId)) + if (DB::Aowow()->qry('UPDATE ::account SET `status` = %i, `statusTimer` = 0, `token` = "" WHERE `id` = %i', ACC_STATUS_NONE, $userId)) { $this->success = true; return Lang::account('inputbox', 'message', 'deleteCancel'); @@ -91,32 +91,32 @@ class AccountConfirmdeleteResponse extends TemplateResponse private function purge(int $userId) : string { // empty all user settings and cookies - DB::Aowow()->query('DELETE FROM ?_account_cookies WHERE `userId` = ?d', $userId); - DB::Aowow()->query('DELETE FROM ?_account_avatars WHERE `userId` = ?d', $userId); - DB::Aowow()->query('DELETE FROM ?_account_excludes WHERE `userId` = ?d', $userId); - DB::Aowow()->query('DELETE FROM ?_account_favorites WHERE `userId` = ?d', $userId); - DB::Aowow()->query('DELETE FROM ?_account_reputation WHERE `userId` = ?d', $userId); - DB::Aowow()->query('DELETE FROM ?_account_weightscales WHERE `userId` = ?d', $userId); // cascades to aowow_account_weightscale_data + DB::Aowow()->qry('DELETE FROM ::account_cookies WHERE `userId` = %i', $userId); + DB::Aowow()->qry('DELETE FROM ::account_avatars WHERE `userId` = %i', $userId); + DB::Aowow()->qry('DELETE FROM ::account_excludes WHERE `userId` = %i', $userId); + DB::Aowow()->qry('DELETE FROM ::account_favorites WHERE `userId` = %i', $userId); + DB::Aowow()->qry('DELETE FROM ::account_reputation WHERE `userId` = %i', $userId); + DB::Aowow()->qry('DELETE FROM ::account_weightscales WHERE `userId` = %i', $userId); // cascades to aowow_account_weightscale_data // delete profiles, unlink chars - DB::Aowow()->query('DELETE pp FROM ?_profiler_profiles pp JOIN ?_account_profiles ap ON ap.`profileId` = pp.`id` WHERE ap.`accountId` = ?d', $userId); - // DB::Aowow()->query('DELETE FROM ?_account_profiles WHERE `accountId` = ?d', $userId); // already deleted via FK? + DB::Aowow()->qry('DELETE pp FROM ::profiler_profiles pp JOIN ::account_profiles ap ON ap.`profileId` = pp.`id` WHERE ap.`accountId` = %i', $userId); + // DB::Aowow()->qry('DELETE FROM ::account_profiles WHERE `accountId` = %i', $userId); // already deleted via FK? // delete all sessions and bans - DB::Aowow()->query('DELETE FROM ?_account_banned WHERE `userId` = ?d', $userId); - DB::Aowow()->query('DELETE FROM ?_account_sessions WHERE `userId` = ?d', $userId); + DB::Aowow()->qry('DELETE FROM ::account_banned WHERE `userId` = %i', $userId); + DB::Aowow()->qry('DELETE FROM ::account_sessions WHERE `userId` = %i', $userId); // delete forum posts (msg: This post was from a user who has deleted their account. (no translations at src); comments/replies are unaffected) // ... // replace username with userId and empty fields - DB::Aowow()->query( - 'UPDATE ?_account SET + DB::Aowow()->qry( + 'UPDATE ::account SET `login` = "", `passHash` = "", `username` = `id`, `email` = NULL, `userGroups` = 0, `userPerms` = 0, `curIp` = "", `prevIp` = "", `curLogin` = 0, `prevLogin` = 0, `locale` = 0, `debug` = 0, `avatar` = 0, `wowicon` = "", `title` = "", `description` = "", `excludeGroups` = 0, - `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "", `renameCooldown` = 0 - WHERE `id` = ?d', + `status` = %i, `statusTimer` = 0, `token` = "", `updateValue` = "", `renameCooldown` = 0 + WHERE `id` = %i', ACC_STATUS_DELETED, $userId ); diff --git a/endpoints/account/confirm-email-address.php b/endpoints/account/confirm-email-address.php index eb11801c..05a4f217 100644 --- a/endpoints/account/confirm-email-address.php +++ b/endpoints/account/confirm-email-address.php @@ -46,12 +46,12 @@ class AccountConfirmemailaddressResponse extends TemplateResponse if (!$this->assertGET('key')) return Lang::main('intError'); - $acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']); + $acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ::account WHERE `token` = %s', $this->_get['key']); if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_EMAIL || $acc['statusTimer'] < time()) return Lang::account('inputbox', 'error', 'mailTokenUsed'); // 0 changes == error - if (!DB::Aowow()->query('UPDATE ?_account SET `email` = `updateValue`, `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key'])) + if (!DB::Aowow()->qry('UPDATE ::account SET `email` = `updateValue`, `status` = %i, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = %s', ACC_STATUS_NONE, $this->_get['key'])) return Lang::main('intError'); $this->success = true; diff --git a/endpoints/account/confirm-password.php b/endpoints/account/confirm-password.php index 4eec91f0..bc972f70 100644 --- a/endpoints/account/confirm-password.php +++ b/endpoints/account/confirm-password.php @@ -44,12 +44,12 @@ class AccountConfirmpasswordResponse extends TemplateResponse if (!$this->assertGET('key')) return Lang::main('intError'); - $acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']); + $acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ::account WHERE `token` = %s', $this->_get['key']); if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_PASS || $acc['statusTimer'] < time()) return Lang::account('inputbox', 'error', 'passTokenUsed'); // 0 changes == error - if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = `updateValue`, `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key'])) + if (!DB::Aowow()->qry('UPDATE ::account SET `passHash` = `updateValue`, `status` = %i, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = %s', ACC_STATUS_NONE, $this->_get['key'])) return Lang::main('intError'); $this->success = true; diff --git a/endpoints/account/delete-icon.php b/endpoints/account/delete-icon.php index e70ea64a..4e71147b 100644 --- a/endpoints/account/delete-icon.php +++ b/endpoints/account/delete-icon.php @@ -28,15 +28,15 @@ class AccountDeleteiconResponse extends TextResponse return; // non-int > error - $selected = DB::Aowow()->selectCell('SELECT `current` FROM ?_account_avatars WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id); + $selected = DB::Aowow()->selectCell('SELECT `current` FROM ::account_avatars WHERE `id` = %i AND `userId` = %i', $this->_post['id'], User::$id); if ($selected === null || $selected === false) return; - DB::Aowow()->query('DELETE FROM ?_account_avatars WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id); + DB::Aowow()->qry('DELETE FROM ::account_avatars WHERE `id` = %i AND `userId` = %i', $this->_post['id'], User::$id); // if deleted avatar is also currently selected, unset if ($selected) - DB::Aowow()->query('UPDATE ?_account SET `avatar` = 0 WHERE `id` = ?d', User::$id); + DB::Aowow()->qry('UPDATE ::account SET `avatar` = 0 WHERE `id` = %i', User::$id); $path = sprintf('static/uploads/avatars/%d.jpg', $this->_post['id']); if (!unlink($path)) diff --git a/endpoints/account/delete.php b/endpoints/account/delete.php index 34da70e2..04a46e2b 100644 --- a/endpoints/account/delete.php +++ b/endpoints/account/delete.php @@ -28,12 +28,12 @@ class AccountDeleteResponse extends TemplateResponse public string $deleteFormTarget = '?account=delete'; public ?array $inputbox = null; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) $this->generateError(); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -47,11 +47,11 @@ class AccountDeleteResponse extends TemplateResponse if ($this->_post['proceed']) { $error = false; - if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `status` NOT IN (?a) AND `statusTimer` > UNIX_TIMESTAMP() AND `id` = ?d', [ACC_STATUS_NEW, ACC_STATUS_NONE, ACC_STATUS_PURGING], User::$id)) + if (!DB::Aowow()->selectCell('SELECT 1 FROM ::account WHERE `status` NOT IN %in AND `statusTimer` > UNIX_TIMESTAMP() AND `id` = %i', [ACC_STATUS_NEW, ACC_STATUS_NONE, ACC_STATUS_PURGING], User::$id)) { $token = Util::createHash(40); - DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d', + DB::Aowow()->qry('UPDATE ::account SET `status` = %i, `statusTimer` = UNIX_TIMESTAMP() + %i, `token` = %s WHERE `id` = %i', ACC_STATUS_PURGING, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id); Util::sendMail(User::$email, 'delete-account', [$token, User::$email, User::$username]); diff --git a/endpoints/account/exclude.php b/endpoints/account/exclude.php index 52c8b3ed..17a3a032 100644 --- a/endpoints/account/exclude.php +++ b/endpoints/account/exclude.php @@ -34,7 +34,7 @@ class AccountExcludeResponse extends TextResponse else if ($this->_post['reset'] == 1) // defaults to unavailable $this->resetExcludes(); - else if ($this->_post['groups']) // exclude by group mask + else if ($this->_post['groups'] !== null) // exclude by group mask $this->updateGroups(); } @@ -49,12 +49,17 @@ class AccountExcludeResponse extends TextResponse // we don't get signaled whether an id should be added to or removed from either includes or excludes // so we throw everything into one table and toggle the mode if its already in here - $includes = DB::Aowow()->selectCol('SELECT `typeId` FROM ?_profiler_excludes WHERE `type` = ?d AND `typeId` IN (?a)', $this->_post['type'], $validIds); - + $includes = DB::Aowow()->selectCol('SELECT `typeId` FROM ::profiler_excludes WHERE `type` = %i AND `typeId` IN %in', $this->_post['type'], $validIds); + $insert = []; foreach ($validIds as $typeId) - DB::Aowow()->query('INSERT INTO ?_account_excludes (`userId`, `type`, `typeId`, `mode`) VALUES (?a) ON DUPLICATE KEY UPDATE `mode` = (`mode` ^ 0x3)', - [User::$id, $this->_post['type'], $typeId, in_array($typeId, $includes) ? 2 : 1] - ); + { + $insert['userId'][] = User::$id; + $insert['type'][] = $this->_post['type']; + $insert['typeId'][] = $typeId; + $insert['mode'][] = in_array($typeId, $includes) ? Profiler::COMPLETION_INCLUDE : Profiler::COMPLETION_EXCLUDE; + }; + + DB::Aowow()->qry('INSERT INTO ::account_excludes %m ON DUPLICATE KEY UPDATE `mode` = (`mode` ^ 0x3)', $insert); } else trigger_error('AccountExcludeResponse::excludeById - validation failed [type: '.$this->_post['type'].', typeId: '.implode(',', $this->_post['id']).']', E_USER_NOTICE); @@ -62,14 +67,14 @@ class AccountExcludeResponse extends TextResponse private function resetExcludes() : void { - DB::Aowow()->query('DELETE FROM ?_account_excludes WHERE `userId` = ?d', User::$id); - DB::Aowow()->query('UPDATE ?_account SET `excludeGroups` = ?d WHERE `id` = ?d', PR_EXCLUDE_GROUP_UNAVAILABLE, User::$id); + DB::Aowow()->qry('DELETE FROM ::account_excludes WHERE `userId` = %i', User::$id); + DB::Aowow()->qry('UPDATE ::account SET `excludeGroups` = %i WHERE `id` = %i', PR_EXCLUDE_GROUP_UNAVAILABLE, User::$id); } private function updateGroups() : void { if ($this->assertPOST('groups')) // clamp to real groups - DB::Aowow()->query('UPDATE ?_account SET `excludeGroups` = ?d WHERE `id` = ?d', $this->_post['groups'] & PR_EXCLUDE_GROUP_ANY, User::$id); + DB::Aowow()->qry('UPDATE ::account SET `excludeGroups` = %i WHERE `id` = %i', $this->_post['groups'] & PR_EXCLUDE_GROUP_ANY, User::$id); } } diff --git a/endpoints/account/favorites.php b/endpoints/account/favorites.php index 46641e10..e459d253 100644 --- a/endpoints/account/favorites.php +++ b/endpoints/account/favorites.php @@ -37,13 +37,13 @@ class AccountFavoritesResponse extends TextResponse private function removeFavorite() : void { if ($this->assertPOST('id', 'remove')) - DB::Aowow()->query('DELETE FROM ?_account_favorites WHERE `userId` = ?d AND `type` = ?d AND `typeId` = ?d', User::$id, $this->_post['remove'], $this->_post['id']); + DB::Aowow()->qry('DELETE FROM ::account_favorites WHERE `userId` = %i AND `type` = %i AND `typeId` = %i', User::$id, $this->_post['remove'], $this->_post['id']); } private function addFavorite() : void { if ($this->assertPOST('id', 'add') && Type::validateIds($this->_post['add'], $this->_post['id'])) - DB::Aowow()->query('INSERT INTO ?_account_favorites (`userId`, `type`, `typeId`) VALUES (?d, ?d, ?d)', User::$id, $this->_post['add'], $this->_post['id']); + DB::Aowow()->qry('INSERT INTO ::account_favorites (`userId`, `type`, `typeId`) VALUES (%i, %i, %i)', User::$id, $this->_post['add'], $this->_post['id']); else trigger_error('AccountFavoritesResponse::addFavorite() - failed to add [userId: '.User::$id.', type: '.$this->_post['add'].', typeId: '.$this->_post['id'], E_USER_NOTICE); } diff --git a/endpoints/account/forgot-password.php b/endpoints/account/forgot-password.php index 4121f47f..c0ab9581 100644 --- a/endpoints/account/forgot-password.php +++ b/endpoints/account/forgot-password.php @@ -29,7 +29,7 @@ class AccountforgotpasswordResponse extends TemplateResponse private bool $success = false; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { // don't redirect logged in users // you can be forgetful AND logged in @@ -40,7 +40,7 @@ class AccountforgotpasswordResponse extends TemplateResponse if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) $this->generateError(); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -72,14 +72,14 @@ class AccountforgotpasswordResponse extends TemplateResponse if (!$this->_post['email']) return Lang::account('emailInvalid'); - $timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_PASSWORD_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT')); + $timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ::account_bannedips WHERE `ip` = %s AND `type` = %i AND `count` > %i AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_PASSWORD_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT')); // on cooldown pretend we dont know the email address if ($timeout && $timeout > time()) - return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + return Cfg::get('DEBUG') ? 'resend on cooldown: '.DateTime::formatTimeElapsed($timeout * 1000).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); // pretend recovery started - if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ?', $this->_post['email'])) + if (!DB::Aowow()->selectCell('SELECT 1 FROM ::account WHERE `email` = %s', $this->_post['email'])) { // do not confirm or deny existence of email $this->success = !Cfg::get('DEBUG'); @@ -90,7 +90,7 @@ class AccountforgotpasswordResponse extends TemplateResponse if ($err = $this->startRecovery(ACC_STATUS_RECOVER_PASS, 'reset-password', $this->_post['email'])) return $err; - DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d', + DB::Aowow()->qry('INSERT INTO ::account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (%s, %i, %i, UNIX_TIMESTAMP() + %i) ON DUPLICATE KEY UPDATE `count` = `count` + %i, `unbanDate` = UNIX_TIMESTAMP() + %i', User::$ip, IP_BAN_TYPE_PASSWORD_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); $this->success = true; diff --git a/endpoints/account/forgot-username.php b/endpoints/account/forgot-username.php index 4a6245d4..c1cff916 100644 --- a/endpoints/account/forgot-username.php +++ b/endpoints/account/forgot-username.php @@ -28,7 +28,7 @@ class AccountforgotusernameResponse extends TemplateResponse private bool $success = false; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { // if the user is looged in goto account dashboard if (User::isLoggedIn()) @@ -40,7 +40,7 @@ class AccountforgotusernameResponse extends TemplateResponse if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) $this->generateError(); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -71,14 +71,14 @@ class AccountforgotusernameResponse extends TemplateResponse if (!$this->_post['email']) return Lang::account('emailInvalid'); - $timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_USERNAME_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT')); + $timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ::account_bannedips WHERE `ip` = %s AND `type` = %i AND `count` > %i AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_USERNAME_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT')); // on cooldown pretend we dont know the email address if ($timeout && $timeout > time()) - return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + return Cfg::get('DEBUG') ? 'resend on cooldown: '.DateTime::formatTimeElapsed($timeout * 1000).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); // pretend recovery started - if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ?', $this->_post['email'])) + if (!DB::Aowow()->selectCell('SELECT 1 FROM ::account WHERE `email` = %s', $this->_post['email'])) { // do not confirm or deny existence of email $this->success = !Cfg::get('DEBUG'); @@ -89,7 +89,7 @@ class AccountforgotusernameResponse extends TemplateResponse if ($err = $this->startRecovery(ACC_STATUS_RECOVER_USER, 'recover-user', $this->_post['email'])) return $err; - DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d', + DB::Aowow()->qry('INSERT INTO ::account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (%s, %i, %i, UNIX_TIMESTAMP() + %i) ON DUPLICATE KEY UPDATE `count` = `count` + %i, `unbanDate` = UNIX_TIMESTAMP() + %i', User::$ip, IP_BAN_TYPE_USERNAME_RECOVERY, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); $this->success = true; diff --git a/endpoints/account/forum-avatar.php b/endpoints/account/forum-avatar.php index a123d572..e7bd9840 100644 --- a/endpoints/account/forum-avatar.php +++ b/endpoints/account/forum-avatar.php @@ -48,7 +48,7 @@ class AccountForumavatarResponse extends TextResponse private function unset() : string { - $x = DB::Aowow()->query('UPDATE ?_account SET `avatar` = 0 WHERE `id` = ?d', User::$id); + $x = DB::Aowow()->qry('UPDATE ::account SET `avatar` = 0 WHERE `id` = %i', User::$id); if ($x === null || $x === false) return Lang::main('genericError'); @@ -64,17 +64,17 @@ class AccountForumavatarResponse extends TextResponse $icon = strtolower(trim($this->_post['wowicon'])); - if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_icons WHERE `name` = ?', $icon)) + if (!DB::Aowow()->selectCell('SELECT 1 FROM ::icons WHERE `name` = %s', $icon)) return Lang::account('updateMessage', 'avNotFound'); - $x = DB::Aowow()->query('UPDATE ?_account SET `avatar` = 1, `wowicon` = ? WHERE `id` = ?d', strtolower($icon), User::$id); - if ($x === null || $x === false) + $x = DB::Aowow()->qry('UPDATE ::account SET `avatar` = 1, `wowicon` = %s WHERE `id` = %i', $icon, User::$id); + if (is_null($x)) return Lang::main('genericError'); $this->success = true; $msg = Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess'); - if (($qty = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_account WHERE `wowicon` = ?', $icon)) > 1) + if (($qty = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ::account WHERE `wowicon` = %s', $icon)) > 1) $msg .= ' '.Lang::account('updateMessage', 'avNthUser', [$qty]); else $msg .= ' '.Lang::account('updateMessage', 'av1stUser'); @@ -92,11 +92,11 @@ class AccountForumavatarResponse extends TextResponse $customIcon = $this->_post['customicon'] ?? $this->_get['customicon']; - $x = DB::Aowow()->query('UPDATE ?_account_avatars SET `current` = IF(`id` = ?d, 1, 0) WHERE `userId` = ?d AND `status` <> ?d', $customIcon, User::$id, AvatarMgr::STATUS_REJECTED); + $x = DB::Aowow()->qry('UPDATE ::account_avatars SET `current` = IF(`id` = %i, 1, 0) WHERE `userId` = %i AND `status` <> %i', $customIcon, User::$id, AvatarMgr::STATUS_REJECTED); if (!is_int($x)) return Lang::main('genericError'); - if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `avatar` = 2 WHERE `id` = ?d', User::$id))) + if (!is_int(DB::Aowow()->qry('UPDATE ::account SET `avatar` = 2 WHERE `id` = %i', User::$id))) return Lang::main('intError'); $this->success = true; diff --git a/endpoints/account/premium-border.php b/endpoints/account/premium-border.php index e1ee43d8..7374a210 100644 --- a/endpoints/account/premium-border.php +++ b/endpoints/account/premium-border.php @@ -28,8 +28,8 @@ class AccountPremiumborderResponse extends TextResponse if (!$this->assertPOST('avatarborder')) return; - $x = DB::Aowow()->query('UPDATE ?_account SET `avatarborder` = ?d WHERE `id` = ?d', $this->_post['avatarborder'], User::$id); - if (!is_int($x)) + $x = DB::Aowow()->qry('UPDATE ::account SET `avatarborder` = %i WHERE `id` = %i', $this->_post['avatarborder'], User::$id); + if (is_null($x)) $_SESSION['msg'] = ['premiumborder', false, Lang::main('genericError')]; else if (!$x) $_SESSION['msg'] = ['premiumborder', true, Lang::account('updateMessage', 'avNoChange')]; diff --git a/endpoints/account/rename-icon.php b/endpoints/account/rename-icon.php index 46735d01..c88ff2bb 100644 --- a/endpoints/account/rename-icon.php +++ b/endpoints/account/rename-icon.php @@ -29,7 +29,7 @@ class AccountRenameiconResponse extends TextResponse return; // regexp same as in account.js - DB::Aowow()->query('UPDATE ?_account_avatars SET `name` = ? WHERE `id` = ?d AND `userId` = ?d', trim($this->_post['name']), $this->_post['id'], User::$id); + DB::Aowow()->qry('UPDATE ::account_avatars SET `name` = %s WHERE `id` = %i AND `userId` = %i', trim($this->_post['name']), $this->_post['id'], User::$id); } } diff --git a/endpoints/account/resend-submit.php b/endpoints/account/resend-submit.php index c45c0499..28fe9c51 100644 --- a/endpoints/account/resend-submit.php +++ b/endpoints/account/resend-submit.php @@ -20,12 +20,12 @@ class AccountResendsubmitResponse extends TemplateResponse 'email' => ['filter' => FILTER_VALIDATE_EMAIL, 'flags' => FILTER_FLAG_STRIP_AOWOW] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) $this->generateError(); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void diff --git a/endpoints/account/resend.php b/endpoints/account/resend.php index 234aea17..7c1fecbf 100644 --- a/endpoints/account/resend.php +++ b/endpoints/account/resend.php @@ -22,7 +22,7 @@ class AccountResendResponse extends TemplateResponse private bool $success = false; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { if (Cfg::get('ACC_EXT_RECOVER_URL')) $this->forward(Cfg::get('ACC_EXT_RECOVER_URL')); @@ -30,7 +30,7 @@ class AccountResendResponse extends TemplateResponse if (!Cfg::get('ACC_ALLOW_REGISTER') || Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) $this->generateError(); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -69,19 +69,19 @@ class AccountResendResponse extends TemplateResponse if (!$this->_post['email']) return Lang::account('emailInvalid'); - $timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d AND `count` > ?d AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT')); + $timeout = DB::Aowow()->selectCell('SELECT `unbanDate` FROM ::account_bannedips WHERE `ip` = %s AND `type` = %i AND `count` > %i AND `unbanDate` > UNIX_TIMESTAMP()', User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT')); // on cooldown pretend we dont know the email address if ($timeout && $timeout > time()) - return Cfg::get('DEBUG') ? 'resend on cooldown: '.Util::formatTimeDiff($timeout).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); + return Cfg::get('DEBUG') ? 'resend on cooldown: '.DateTime::formatTimeElapsed($timeout * 1000).' remaining' : Lang::account('inputbox', 'error', 'emailNotFound'); // check email and account status - if ($token = DB::Aowow()->selectCell('SELECT `token` FROM ?_account WHERE `email` = ? AND `status` = ?d', $this->_post['email'], ACC_STATUS_NEW)) + if ($token = DB::Aowow()->selectCell('SELECT `token` FROM ::account WHERE `email` = %s AND `status` = %i', $this->_post['email'], ACC_STATUS_NEW)) { if (!Util::sendMail($this->_post['email'], 'activate-account', [$token])) return Lang::main('intError'); - DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, ?d, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + ?d, `unbanDate` = UNIX_TIMESTAMP() + ?d', + DB::Aowow()->qry('INSERT INTO ::account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (%s, %i, %i, UNIX_TIMESTAMP() + %i) ON DUPLICATE KEY UPDATE `count` = `count` + %i, `unbanDate` = UNIX_TIMESTAMP() + %i', User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_COUNT') + 1, Cfg::get('ACC_FAILED_AUTH_COUNT'), Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); $this->success = true; diff --git a/endpoints/account/reset-password.php b/endpoints/account/reset-password.php index 44c39b0b..4d71153f 100644 --- a/endpoints/account/reset-password.php +++ b/endpoints/account/reset-password.php @@ -25,7 +25,7 @@ class AccountresetpasswordResponse extends TemplateResponse protected array $expectedGET = array( 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], - 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ] + 'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/' ]] ); protected array $expectedPOST = array( 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], @@ -59,7 +59,7 @@ class AccountresetpasswordResponse extends TemplateResponse $errMsg = ''; if (!$this->assertGET('key') && !$this->assertPOST('key')) $errMsg = Lang::account('inputbox', 'error', 'passTokenLost'); - else if ($this->_get['key'] && !DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `token` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()', $this->_get['key'], ACC_STATUS_RECOVER_PASS)) + else if ($this->_get['key'] && !DB::Aowow()->selectCell('SELECT 1 FROM ::account WHERE `token` = %s AND `status` = %i AND `statusTimer` > UNIX_TIMESTAMP()', $this->_get['key'], ACC_STATUS_RECOVER_PASS)) $errMsg = Lang::account('inputbox', 'error', 'passTokenUsed'); if ($errMsg) @@ -99,7 +99,7 @@ class AccountresetpasswordResponse extends TemplateResponse if ($this->_post['password'] != $this->_post['c_password']) return Lang::account('passCheckFail'); - $userData = DB::Aowow()->selectRow('SELECT `id`, `passHash` FROM ?_account WHERE `token` = ? AND `email` = ? AND `status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()', + $userData = DB::Aowow()->selectRow('SELECT `id`, `passHash` FROM ::account WHERE `token` = %s AND `email` = %s AND `status` = %i AND `statusTimer` > UNIX_TIMESTAMP()', $this->_post['key'], $this->_post['email'], ACC_STATUS_RECOVER_PASS @@ -110,7 +110,7 @@ class AccountresetpasswordResponse extends TemplateResponse if (!User::verifyCrypt($this->_post['c_password'], $userData['passHash'])) return Lang::account('newPassDiff'); - if (!DB::Aowow()->query('UPDATE ?_account SET `passHash` = ?, `status` = ?d WHERE `id` = ?d', User::hashCrypt($this->_post['c_password']), ACC_STATUS_NONE, $userData['id'])) + if (!DB::Aowow()->qry('UPDATE ::account SET `passHash` = %s, `status` = %i WHERE `id` = %i', User::hashCrypt($this->_post['c_password']), ACC_STATUS_NONE, $userData['id'])) return Lang::main('intError'); $this->success = true; diff --git a/endpoints/account/revert-email-address.php b/endpoints/account/revert-email-address.php index 37475082..106fb6f0 100644 --- a/endpoints/account/revert-email-address.php +++ b/endpoints/account/revert-email-address.php @@ -46,12 +46,12 @@ class AccountRevertemailaddressResponse extends TemplateResponse if (!$this->assertGET('key')) return Lang::main('intError'); - $acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ?_account WHERE `token` = ?', $this->_get['key']); + $acc = DB::Aowow()->selectRow('SELECT `updateValue`, `status`, `statusTimer` FROM ::account WHERE `token` = %s', $this->_get['key']); if (!$acc || $acc['status'] != ACC_STATUS_CHANGE_EMAIL || $acc['statusTimer'] < time()) return Lang::account('inputbox', 'error', 'mailTokenUsed'); // 0 changes == error - if (!DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = ?', ACC_STATUS_NONE, $this->_get['key'])) + if (!DB::Aowow()->qry('UPDATE ::account SET `status` = %i, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `token` = %s', ACC_STATUS_NONE, $this->_get['key'])) return Lang::main('intError'); $this->success = true; diff --git a/endpoints/account/signin.php b/endpoints/account/signin.php index c4a0329e..ed8fbf07 100644 --- a/endpoints/account/signin.php +++ b/endpoints/account/signin.php @@ -28,7 +28,7 @@ class AccountSigninResponse extends TemplateResponse ); protected array $expectedGET = array( 'key' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[a-zA-Z0-9]{40}$/']], - 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW ] + 'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/'] ] ); private bool $success = false; @@ -53,7 +53,7 @@ class AccountSigninResponse extends TemplateResponse // coming from user recovery or creation, prefill username if ($this->_get['key']) { - if ($userData = DB::Aowow()->selectRow('SELECT a.`login` AS "0", IF(s.`expires`, 0, 1) AS "1" FROM ?_account a LEFT JOIN ?_account_sessions s ON a.`id` = s.`userId` AND a.`token` = s.`sessionId` WHERE a.`status` IN (?a) AND a.`token` = ?', + if ($userData = DB::Aowow()->selectRow('SELECT a.`login` AS "0", IF(s.`expires`, 0, 1) AS "1" FROM ::account a LEFT JOIN ::account_sessions s ON a.`id` = s.`userId` AND a.`token` = s.`sessionId` WHERE a.`status` IN %in AND a.`token` = %s', [ACC_STATUS_RECOVER_USER, ACC_STATUS_NONE], $this->_get['key'])) [$username, $rememberMe] = $userData; } @@ -99,7 +99,7 @@ class AccountSigninResponse extends TemplateResponse // AUTH_BANNED => Lang::account('accBanned'); // ToDo: should this return an error? the actual account functionality should be blocked elsewhere AUTH_WRONGUSER => Lang::account('userNotFound'), AUTH_WRONGPASS => Lang::account('wrongPass'), - AUTH_IPBANNED => Lang::account('inputbox', 'error', 'loginExceeded', [Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]), + AUTH_IPBANNED => Lang::account('inputbox', 'error', 'loginExceeded', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]), AUTH_INTERNAL_ERR => Lang::main('intError'), default => Lang::main('intError') }; @@ -116,7 +116,7 @@ class AccountSigninResponse extends TemplateResponse } // reset account status, update expiration - $ok = DB::Aowow()->query('UPDATE ?_account SET `prevIP` = IF(`curIp` = ?, `prevIP`, `curIP`), `curIP` = IF(`curIp` = ?, `curIP`, ?), `status` = IF(`status` = ?d, `status`, 0), `statusTimer` = IF(`status` = ?d, `statusTimer`, 0), `token` = IF(`status` = ?d, `token`, "") WHERE `id` = ?d', + $ok = DB::Aowow()->qry('UPDATE ::account SET `prevIP` = IF(`curIp` = %s, `prevIP`, `curIP`), `curIP` = IF(`curIp` = %s, `curIP`, %s), `status` = IF(`status` = %i, `status`, 0), `statusTimer` = IF(`status` = %i, `statusTimer`, 0), `token` = IF(`status` = %i, `token`, "") WHERE `id` = %i', User::$ip, User::$ip, User::$ip, ACC_STATUS_NEW, ACC_STATUS_NEW, ACC_STATUS_NEW, User::$id // available after successful User:authenticate @@ -130,12 +130,12 @@ class AccountSigninResponse extends TemplateResponse // DELETE temp session if ($this->_get['key']) - DB::Aowow()->query('DELETE FROM ?_account_sessions WHERE `sessionId` = ?', $this->_get['key']); + DB::Aowow()->qry('DELETE FROM ::account_sessions WHERE `sessionId` = %s', $this->_get['key']); session_regenerate_id(true); // user status changed => regenerate id // create new session entry - DB::Aowow()->query('INSERT INTO ?_account_sessions (`userId`, `sessionId`, `created`, `expires`, `touched`, `deviceInfo`, `ip`, `status`) VALUES (?d, ?, ?d, ?d, ?d, ?, ?, ?d)', + DB::Aowow()->qry('INSERT INTO ::account_sessions (`userId`, `sessionId`, `created`, `expires`, `touched`, `deviceInfo`, `ip`, `status`) VALUES (%i, %s, %i, %i, %i, %s, %s, %i)', User::$id, session_id(), time(), $this->_post['remember_me'] ? 0 : time() + Cfg::get('SESSION_TIMEOUT_DELAY'), time(), User::$agent, User::$ip, SESSION_ACTIVE); if (User::init()) // reinitialize the user diff --git a/endpoints/account/signout.php b/endpoints/account/signout.php index 7447f92e..73374d1b 100644 --- a/endpoints/account/signout.php +++ b/endpoints/account/signout.php @@ -11,25 +11,25 @@ class AccountSignoutResponse extends TextResponse use TrGetNext; protected array $expectedGET = array( - 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH], - 'global' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ] + 'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']], + 'global' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { // if the user not is logged in goto login page if (!User::isLoggedIn()) $this->forwardToSignIn(); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void { if ($this->_get['global']) - DB::Aowow()->query('UPDATE ?_account_sessions SET `touched` = ?d, `status` = ?d WHERE `userId` = ?d', time(), SESSION_FORCED_LOGOUT, User::$id); + DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `status` = %i WHERE `userId` = %i', time(), SESSION_FORCED_LOGOUT, User::$id); else - DB::Aowow()->query('UPDATE ?_account_sessions SET `touched` = ?d, `status` = ?d WHERE `sessionId` = ?', time(), SESSION_LOGOUT, session_id()); + DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `status` = %i WHERE `sessionId` = %s', time(), SESSION_LOGOUT, session_id()); User::destroy(); diff --git a/endpoints/account/signup.php b/endpoints/account/signup.php index f8a819e1..2fa29278 100644 --- a/endpoints/account/signup.php +++ b/endpoints/account/signup.php @@ -26,7 +26,7 @@ class AccountSignupResponse extends TemplateResponse ); protected array $expectedGET = array( - 'next' => ['filter' => FILTER_SANITIZE_URL, 'flags' => FILTER_FLAG_STRIP_AOWOW] + 'next' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']] ); private bool $success = false; @@ -99,7 +99,7 @@ class AccountSignupResponse extends TemplateResponse // check password if (!Util::validatePassword($this->_post['password'], $e)) - return Lang::account($e == 1 ? 'errPassLength' : 'errPassChars'); + return $e == 1 ? Lang::account('errPassLength') : Lang::main('intError'); if ($this->_post['password'] !== $this->_post['c_password']) return Lang::account('passMismatch'); @@ -109,24 +109,24 @@ class AccountSignupResponse extends TemplateResponse return Lang::main('intError'); // limit account creation - if (DB::Aowow()->selectRow('SELECT 1 FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ? AND `count` >= ?d AND `unbanDate` >= UNIX_TIMESTAMP()', IP_BAN_TYPE_REGISTRATION_ATTEMPT, User::$ip, Cfg::get('ACC_FAILED_AUTH_COUNT'))) + if (DB::Aowow()->selectRow('SELECT 1 FROM ::account_bannedips WHERE `type` = %i AND `ip` = %s AND `count` >= %i AND `unbanDate` >= UNIX_TIMESTAMP()', IP_BAN_TYPE_REGISTRATION_ATTEMPT, User::$ip, Cfg::get('ACC_FAILED_AUTH_COUNT'))) { - DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ? AND `type` = ?d', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT); - return Lang::account('inputbox', 'error', 'signupExceeded', [Util::formatTime(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]); + DB::Aowow()->qry('UPDATE ::account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + %i WHERE `ip` = %s AND `type` = %i', Cfg::get('ACC_FAILED_AUTH_BLOCK'), User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT); + return Lang::account('inputbox', 'error', 'signupExceeded', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_FAILED_AUTH_BLOCK') * 1000)]); } // username / email taken - if ($inUseData = DB::Aowow()->SelectRow('SELECT `id`, `username`, `status` = ?d AND `statusTimer` < UNIX_TIMESTAMP() AS "expired" FROM ?_account WHERE (LOWER(`username`) = LOWER(?) OR LOWER(`email`) = LOWER(?))', ACC_STATUS_NEW, $this->_post['username'], $this->_post['email'])) + if ($inUseData = DB::Aowow()->SelectRow('SELECT `id`, `username`, `status` = %i AND `statusTimer` < UNIX_TIMESTAMP() AS "expired" FROM ::account WHERE (LOWER(`username`) = LOWER(%s) OR LOWER(`email`) = LOWER(%s))', ACC_STATUS_NEW, $this->_post['username'], $this->_post['email'])) { if ($inUseData['expired']) - DB::Aowow()->query('DELETE FROM ?_account WHERE `id` = ?d', $inUseData['id']); + DB::Aowow()->qry('DELETE FROM ::account WHERE `id` = %i', $inUseData['id']); else return Util::lower($inUseData['username']) == Util::lower($this->_post['username']) ? Lang::account('nameInUse') : Lang::account('mailInUse'); } // create.. $token = Util::createHash(); - $userId = DB::Aowow()->query('INSERT INTO ?_account (`login`, `passHash`, `username`, `email`, `joindate`, `curIP`, `locale`, `userGroups`, `status`, `statusTimer`, `token`) VALUES (?, ?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, ?d, ?d, UNIX_TIMESTAMP() + ?d, ?)', + $userId = DB::Aowow()->qry('INSERT INTO ::account (`login`, `passHash`, `username`, `email`, `joindate`, `curIP`, `locale`, `userGroups`, `status`, `statusTimer`, `token`) VALUES (%s, %s, %s, %s, UNIX_TIMESTAMP(), %s, %i, %i, %i, UNIX_TIMESTAMP() + %i, %s)', $this->_post['username'], User::hashCrypt($this->_post['password']), $this->_post['username'], @@ -143,14 +143,14 @@ class AccountSignupResponse extends TemplateResponse return Lang::main('intError'); // create session tied to the token to store remember_me status - DB::Aowow()->query('INSERT INTO ?_account_sessions (`userId`, `sessionId`, `created`, `expires`, `touched`, `deviceInfo`, `ip`, `status`) VALUES (?d, ?, ?d, ?d, ?d, ?, ?, ?d)', + DB::Aowow()->qry('INSERT INTO ::account_sessions (`userId`, `sessionId`, `created`, `expires`, `touched`, `deviceInfo`, `ip`, `status`) VALUES (%i, %s, %i, %i, %i, %s, %s, %i)', $userId, $token, time(), $this->_post['remember_me'] ? 0 : time() + Cfg::get('SESSION_TIMEOUT_DELAY'), time(), User::$agent, User::$ip, SESSION_ACTIVE); if (!Util::sendMail($this->_post['email'], 'activate-account', [$token], Cfg::get('ACC_CREATE_SAVE_DECAY'))) return Lang::main('intError2', ['send mail']); // success: update ip-bans - DB::Aowow()->query('INSERT INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, 1, UNIX_TIMESTAMP() + ?d) ON DUPLICATE KEY UPDATE `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d', + DB::Aowow()->qry('INSERT INTO ::account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (%s, %i, 1, UNIX_TIMESTAMP() + %i) ON DUPLICATE KEY UPDATE `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + %i', User::$ip, IP_BAN_TYPE_REGISTRATION_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_BLOCK'), Cfg::get('ACC_FAILED_AUTH_BLOCK')); Util::gainSiteReputation($userId, SITEREP_ACTION_REGISTER); diff --git a/endpoints/account/update-community-settings.php b/endpoints/account/update-community-settings.php index a09921f8..2bc1ed2d 100644 --- a/endpoints/account/update-community-settings.php +++ b/endpoints/account/update-community-settings.php @@ -37,7 +37,7 @@ class AccountUpdatecommunitysettingsResponse extends TextResponse return Lang::main('genericError'); // description - 0 modified rows is still success - if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `description` = ? WHERE `id` = ?d', $this->_post['desc'], User::$id))) + if (!is_int(DB::Aowow()->qry('UPDATE ::account SET `description` = %s WHERE `id` = %i', $this->_post['desc'], User::$id))) return Lang::main('genericError'); $this->success = true; diff --git a/endpoints/account/update-email.php b/endpoints/account/update-email.php index 104e18a5..0cf750be 100644 --- a/endpoints/account/update-email.php +++ b/endpoints/account/update-email.php @@ -22,12 +22,12 @@ class AccountUpdateemailResponse extends TextResponse private bool $success = false; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) (new TemplateResponse())->generateError(); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -48,21 +48,21 @@ class AccountUpdateemailResponse extends TextResponse if (!$this->_post['newemail']) return Lang::account('emailInvalid'); - if (DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE `email` = ? AND `id` <> ?d', $this->_post['newemail'], User::$id)) + if (DB::Aowow()->selectCell('SELECT 1 FROM ::account WHERE `email` = %s AND `id` <> %i', $this->_post['newemail'], User::$id)) return Lang::account('mailInUse'); - $status = DB::Aowow()->selectCell('SELECT `status` FROM ?_account WHERE `statusTimer` > UNIX_TIMESTAMP() AND `id` = ?d', User::$id); + $status = DB::Aowow()->selectCell('SELECT `status` FROM ::account WHERE `statusTimer` > UNIX_TIMESTAMP() AND `id` = %i', User::$id); if ($status != ACC_STATUS_NONE && $status != ACC_STATUS_CHANGE_EMAIL) - return Lang::account('isRecovering', [Util::formatTime(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); + return Lang::account('inputbox', 'error', 'isRecovering', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); - $oldEmail = DB::Aowow()->selectCell('SELECT `email` FROM ?_account WHERE `id` = ?d', User::$id); + $oldEmail = DB::Aowow()->selectCell('SELECT `email` FROM ::account WHERE `id` = %i', User::$id); if ($this->_post['newemail'] == $oldEmail) return Lang::account('newMailDiff'); $token = Util::createHash(); // store new mail in updateValue field, exchange when confirmation mail gets confirmed - if (!DB::Aowow()->query('UPDATE ?_account SET `updateValue` = ?, `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d', + if (!DB::Aowow()->qry('UPDATE ::account SET `updateValue` = %s, `status` = %i, `statusTimer` = UNIX_TIMESTAMP() + %i, `token` = %s WHERE `id` = %i', $this->_post['newemail'], ACC_STATUS_CHANGE_EMAIL, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id)) return Lang::main('intError'); diff --git a/endpoints/account/update-general-settings.php b/endpoints/account/update-general-settings.php index 6e56ce3a..73a8cda0 100644 --- a/endpoints/account/update-general-settings.php +++ b/endpoints/account/update-general-settings.php @@ -41,15 +41,15 @@ class AccountUpdategeneralsettingsResponse extends TextResponse if ($this->_post['modelrace'] && !ChrRace::tryFrom($this->_post['modelrace'])) return Lang::main('genericError'); - // js handles this as cookie, so saved as cookie; Q - also save in ?_account table? - if (!DB::Aowow()->query('REPLACE INTO ?_account_cookies (`userId`, `name`, `data`) VALUES (?d, ?, ?)', User::$id, 'default_3dmodel', $this->_post['modelrace']. ',' . $this->_post['modelgender'])) + // js handles this as cookie, so saved as cookie; Q - also save in ::account table? + if (!DB::Aowow()->qry('REPLACE INTO ::account_cookies (`userId`, `name`, `data`) VALUES (%i, %s, %s)', User::$id, 'default_3dmodel', $this->_post['modelrace']. ',' . $this->_post['modelgender'])) return Lang::main('genericError'); if (!setcookie('default_3dmodel', $this->_post['modelrace']. ',' . $this->_post['modelgender'], 0, '/')) return Lang::main('intError'); // int > number of edited rows > no changes is still success - if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `debug` = ?d WHERE `id` = ?d', $this->_post['idsInLists'] ? 1 : 0, User::$id))) + if (!is_int(DB::Aowow()->qry('UPDATE ::account SET `debug` = %i WHERE `id` = %i', $this->_post['idsInLists'] ? 1 : 0, User::$id))) return Lang::main('intError'); $this->success = true; diff --git a/endpoints/account/update-password.php b/endpoints/account/update-password.php index cc4151d6..8038f8be 100644 --- a/endpoints/account/update-password.php +++ b/endpoints/account/update-password.php @@ -25,12 +25,12 @@ class AccountUpdatepasswordResponse extends TextResponse private bool $success = false; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) (new TemplateResponse())->generateError(); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -48,14 +48,14 @@ class AccountUpdatepasswordResponse extends TextResponse return Lang::main('intError'); if (!Util::validatePassword($this->_post['newPassword'], $e)) - return Lang::account($e == 1 ? 'errPassLength' : 'errPassChars'); + return $e == 1 ? Lang::account('errPassLength') : Lang::main('intError'); if ($this->_post['newPassword'] !== $this->_post['confirmPassword']) return Lang::account('passMismatch'); - $userData = DB::Aowow()->selectRow('SELECT `status`, `passHash`, `statusTimer` FROM ?_account WHERE `id` = ?d', User::$id); + $userData = DB::Aowow()->selectRow('SELECT `status`, `passHash`, `statusTimer` FROM ::account WHERE `id` = %i', User::$id); if ($userData['status'] != ACC_STATUS_NONE && $userData['status'] != ACC_STATUS_CHANGE_PASS && $userData['statusTimer'] > time()) - return Lang::account('isRecovering', [Util::formatTime(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); + return Lang::account('inputbox', 'error', 'isRecovering', [DateTime::formatTimeElapsedFloat(Cfg::get('ACC_RECOVERY_DECAY') * 1000)]); if (!User::verifyCrypt($this->_post['currentPassword'], $userData['passHash'])) return Lang::account('wrongPass'); @@ -66,17 +66,17 @@ class AccountUpdatepasswordResponse extends TextResponse $token = Util::createHash(); // store new hash in updateValue field, exchange when confirmation mail gets confirmed - if (!DB::Aowow()->query('UPDATE ?_account SET `updateValue` = ?, `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d, `token` = ? WHERE `id` = ?d', + if (!DB::Aowow()->qry('UPDATE ::account SET `updateValue` = %s, `status` = %i, `statusTimer` = UNIX_TIMESTAMP() + %i, `token` = %s WHERE `id` = %i', User::hashCrypt($this->_post['newPassword']), ACC_STATUS_CHANGE_PASS, Cfg::get('ACC_RECOVERY_DECAY'), $token, User::$id)) return Lang::main('intError'); - $email = DB::Aowow()->selectCell('SELECT `email` FROM ?_account WHERE `id` = ?d', User::$id); + $email = DB::Aowow()->selectCell('SELECT `email` FROM ::account WHERE `id` = %i', User::$id); if (!Util::sendMail($email, 'update-password', [$token, $email], Cfg::get('ACC_RECOVERY_DECAY'))) return Lang::main('intError2', ['send mail']); // logout all other active sessions if ($this->_post['globalLogout']) - DB::Aowow()->query('UPDATE ?_account_sessions SET `status` = ?d, `touched` = ?d WHERE `userId` = ?d AND `sessionId` <> ? AND `status` = ?d', SESSION_FORCED_LOGOUT, time(), User::$id, session_id(), SESSION_ACTIVE); + DB::Aowow()->qry('UPDATE ::account_sessions SET `status` = %i, `touched` = %i WHERE `userId` = %i AND `sessionId` <> ? AND `status` = %i', SESSION_FORCED_LOGOUT, time(), User::$id, session_id(), SESSION_ACTIVE); $this->success = true; return Lang::account('updateMessage', 'personal', [User::$email]); diff --git a/endpoints/account/update-username.php b/endpoints/account/update-username.php index d301fb6a..7876d605 100644 --- a/endpoints/account/update-username.php +++ b/endpoints/account/update-username.php @@ -22,12 +22,12 @@ class AccountUpdateusernameResponse extends TextResponse private bool $success = false; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { if (Cfg::get('ACC_AUTH_MODE') != AUTH_MODE_SELF) (new TemplateResponse())->generateError(); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -44,14 +44,14 @@ class AccountUpdateusernameResponse extends TextResponse if (!$this->assertPOST('newUsername')) return Lang::main('intError'); - if (DB::Aowow()->selectCell('SELECT `renameCooldown` FROM ?_account WHERE `id` = ?d', User::$id) > time()) + if (DB::Aowow()->selectCell('SELECT `renameCooldown` FROM ::account WHERE `id` = %i', User::$id) > time()) return Lang::main('intError'); // should have grabbed the error response.. // yes, including your current name. you don't want to change into your current name, right? - if (DB::Aowow()->selectCell('SELECT 1 FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_post['newUsername'])) + if (DB::Aowow()->selectCell('SELECT 1 FROM ::account WHERE LOWER(`username`) = LOWER(%s)', $this->_post['newUsername'])) return Lang::account('nameInUse'); - DB::Aowow()->query('UPDATE ?_account SET `username` = ?, `renameCooldown` = ?d WHERE `id` = ?d', $this->_post['newUsername'], time() + Cfg::get('acc_rename_decay'), User::$id); + DB::Aowow()->qry('UPDATE ::account SET `username` = %s, `renameCooldown` = %i WHERE `id` = %i', $this->_post['newUsername'], time() + Cfg::get('acc_rename_decay'), User::$id); $this->success = true; return Lang::account('updateMessage', 'username', [User::$username, $this->_post['newUsername']]); diff --git a/endpoints/account/weightscales.php b/endpoints/account/weightscales.php index 13bba67e..8bae7835 100644 --- a/endpoints/account/weightscales.php +++ b/endpoints/account/weightscales.php @@ -46,11 +46,11 @@ class AccountWeightscalesResponse extends TextResponse if (!$this->assertPOST('name', 'scale')) return; - $nScales = DB::Aowow()->selectCell('SELECT COUNT(`id`) FROM ?_account_weightscales WHERE `userId` = ?d', User::$id); + $nScales = DB::Aowow()->selectCell('SELECT COUNT(`id`) FROM ::account_weightscales WHERE `userId` = %i', User::$id); if ($nScales >= self::MAX_SCALES) return; - if ($id = DB::Aowow()->query('INSERT INTO ?_account_weightscales (`userId`, `name`) VALUES (?d, ?)', User::$id, $this->_post['name'])) + if ($id = DB::Aowow()->qry('INSERT INTO ::account_weightscales (`userId`, `name`) VALUES (%i, %s)', User::$id, $this->_post['name'])) if ($this->storeScaleData($id)) $this->result = $id; } @@ -61,13 +61,13 @@ class AccountWeightscalesResponse extends TextResponse return; // not in DB or not owned by user - if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_account_weightscales WHERE `userId` = ?d AND `id` = ?d', User::$id, $this->_post['id'])) + if (!DB::Aowow()->selectCell('SELECT 1 FROM ::account_weightscales WHERE `userId` = %i AND `id` = %i', User::$id, $this->_post['id'])) { trigger_error('AccountWeightscalesResponse::updateWeights - scale #'.$this->_post['id'].' not in db or not owned by user #'.User::$id, E_USER_ERROR); return; } - DB::Aowow()->query('UPDATE ?_account_weightscales SET `name` = ? WHERE `id` = ?d', $this->_post['name'], $this->_post['id']); + DB::Aowow()->qry('UPDATE ::account_weightscales SET `name` = %s WHERE `id` = %i', $this->_post['name'], $this->_post['id']); $this->storeScaleData($this->_post['id']); // return edited id on success @@ -77,20 +77,24 @@ class AccountWeightscalesResponse extends TextResponse private function deleteWeights() : void { if ($this->assertPOST('id')) - DB::Aowow()->query('DELETE FROM ?_account_weightscales WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id); + DB::Aowow()->qry('DELETE FROM ::account_weightscales WHERE `id` = %i AND `userId` = %i', $this->_post['id'], User::$id); $this->result = ''; } private function storeScaleData(int $scaleId) : bool { - if (!is_int(DB::Aowow()->query('DELETE FROM ?_account_weightscale_data WHERE `id` = ?d', $scaleId))) + if (!is_int(DB::Aowow()->qry('DELETE FROM ::account_weightscale_data WHERE `id` = %i', $scaleId))) return false; - foreach ($this->_post['scale'] as [$k, $v]) - if (in_array($k, Util::$weightScales)) // $v is known to be a positive int due to regex check - if (!is_int(DB::Aowow()->query('INSERT INTO ?_account_weightscale_data VALUES (?d, ?, ?d)', $scaleId, $k, $v))) - return false; + // $x['val'] is known to be a positive int due to regex check + $scaleData = array_filter($this->_post['scale'], fn($x) => Stat::getWeightJson($x['field']) && $x['val'] > 0); + + array_walk($scaleData, fn(&$x) => $x['id'] = $scaleId); + + foreach ($scaleData as $sd) + if (is_null(DB::Aowow()->qry('INSERT INTO ::account_weightscale_data %v', $sd))) + return false; return true; } @@ -103,7 +107,7 @@ class AccountWeightscalesResponse extends TextResponse protected static function checkScale(string $val) : array { if (preg_match('/^((\w+:\d+)(,\w+:\d+)*)$/', $val)) - return array_map(fn($x) => explode(':', $x), explode(',', $val)); + return array_map(fn($x) => array_combine(['field', 'val'], explode(':', $x)), explode(',', $val)); return []; } diff --git a/endpoints/achievement/achievement.php b/endpoints/achievement/achievement.php index 3d36494b..6c63fd11 100644 --- a/endpoints/achievement/achievement.php +++ b/endpoints/achievement/achievement.php @@ -21,7 +21,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'achievement'; protected string $pageName = 'achievement'; @@ -73,7 +73,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache while ($curCat > 0) { $catPath[] = $curCat; - $curCat = DB::Aowow()->SelectCell('SELECT `parentCat` FROM ?_achievementcategory WHERE `id` = ?d', $curCat); + $curCat = DB::Aowow()->SelectCell('SELECT `parentCat` FROM ::achievementcategory WHERE `id` = %i', $curCat); } $this->breadcrumb = array_merge($this->breadcrumb, array_reverse($catPath)); @@ -107,15 +107,32 @@ class AchievementBaseResponse extends TemplateResponse implements ICache default => Lang::game('si', SIDE_BOTH) // 0, 3 }; + // id + $infobox[] = Lang::achievement('id') . $this->typeId; + // icon if ($_ = $this->subject->getField('iconId')) { - $infobox[] = Util::ucFirst(lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; $this->extendGlobalIds(Type::ICON, $_); } + // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) + if (Cfg::get('PROFILER_ENABLE') && !($this->subject->getField('flags') & ACHIEVEMENT_FLAG_COUNTER)) + { + $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ::profiler_completion_achievements WHERE `achievementId` = %i', $this->typeId); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ::profiler_profiles WHERE `custom` = 0 AND `stub` = 0'); + $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); + + // completion row added by InfoboxMarkup + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) - $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', !($this->subject->getField('flags') & ACHIEVEMENT_FLAG_COUNTER)); /**********/ @@ -190,7 +207,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache $this->rewards = [$rewItems, $rewTitles, $text]; // factionchange-equivalent - if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = ?d, `alliance_id`, -`horde_id`) FROM player_factionchange_achievement WHERE `alliance_id` = ?d OR `horde_id` = ?d', $this->typeId, $this->typeId, $this->typeId)) + if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = %i, `alliance_id`, -`horde_id`) FROM player_factionchange_achievement WHERE `alliance_id` = %i OR `horde_id` = %i', $this->typeId, $this->typeId, $this->typeId)) { $altAcv = new AchievementList(array(['id', abs($pendant)])); if (!$altAcv->error) @@ -213,7 +230,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache // serverside extra-Data (not sure why ACHIEVEMENT_CRITERIA_DATA_TYPE_NONE is set, let a lone a couple hundred times) if ($crtIds = array_column($this->subject->getCriteria(), 'id')) - $crtExtraData = DB::World()->select('SELECT `criteria_id` AS ARRAY_KEY, `type` AS ARRAY_KEY2, `value1`, `value2`, `ScriptName` FROM achievement_criteria_data WHERE `type` <> ?d AND `criteria_id` IN (?a)', ACHIEVEMENT_CRITERIA_DATA_TYPE_NONE, $crtIds); + $crtExtraData = DB::World()->selectAssoc('SELECT `criteria_id` AS ARRAY_KEY, `type` AS ARRAY_KEY2, `value1`, `value2`, `ScriptName` FROM achievement_criteria_data WHERE `type` <> %i AND `criteria_id` IN %in', ACHIEVEMENT_CRITERIA_DATA_TYPE_NONE, $crtIds); else $crtExtraData = []; @@ -244,7 +261,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache case ACHIEVEMENT_CRITERIA_TYPE_PLAY_ARENA: case ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_BATTLEGROUND: case ACHIEVEMENT_CRITERIA_TYPE_DEATH_AT_MAP: - $zoneId = DB::Aowow()->selectCell('SELECT `id` FROM ?_zones WHERE `mapId` = ?', $obj); + $zoneId = DB::Aowow()->selectCell('SELECT `id` FROM ::zones WHERE `mapId` = %s', $obj); $crtIcon = new IconElement(Type::ZONE, $zoneId ?: 0, $crtName ?: ZoneList::getName($zoneId), size: IconElement::SIZE_SMALL, element: 'iconlist-icon'); break; // link to area @@ -353,8 +370,17 @@ class AchievementBaseResponse extends TemplateResponse implements ICache $extraData[] = ''.$we->getField('name', true).''; break; case ACHIEVEMENT_CRITERIA_DATA_TYPE_MAP_ID: - if ($z = new ZoneList(array(['mapIdBak', $xData['value1']]))) - $extraData[] = ''.$z->getField('name', true).''; + $extraData[] = match((int)$xData['value1']) + { + 0 => Lang::maps('EasternKingdoms'), + 1 => Lang::maps('Kalimdor'), + 530 => Lang::maps('Outland'), + 571 => Lang::maps('Northrend'), + default => (function(int $mapId) { + $z = new ZoneList(array(['mapId', $mapId])); + return ''.$z->getField('name', true).''; + })($xData['value1']) + }; break; case ACHIEVEMENT_CRITERIA_DATA_TYPE_S_KNOWN_TITLE: $extraData[] = TitleList::makeLink($xData['value1']); @@ -397,7 +423,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache } // tab: criteria of - $refs = DB::Aowow()->SelectCol('SELECT `refAchievementId` FROM ?_achievementcriteria WHERE `type` = ?d AND `value1` = ?d', + $refs = DB::Aowow()->SelectCol('SELECT `refAchievementId` FROM ::achievementcriteria WHERE `type` = %i AND `value1` = %i', ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_ACHIEVEMENT, $this->typeId ); @@ -437,7 +463,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache { if ($_ = $this->subject->getField('mailTemplate')) { - $letter = DB::Aowow()->selectRow('SELECT * FROM ?_mails WHERE `id` = ?d', $_); + $letter = DB::Aowow()->selectRow('SELECT * FROM ::mails WHERE `id` = %i', $_); if (!$letter) return false; @@ -476,7 +502,7 @@ class AchievementBaseResponse extends TemplateResponse implements ICache $avlb = []; foreach (Profiler::getRealms() AS $rId => $rData) - if (!DB::Characters($rId)->selectCell('SELECT 1 FROM character_achievement WHERE `achievement` = ?d', $pt->typeId)) + if (!DB::Characters($rId)->selectCell('SELECT 1 FROM character_achievement WHERE `achievement` = %i', $pt->typeId)) $avlb[] = Util::ucWords($rData['name']); if (!$avlb) diff --git a/endpoints/achievements/achievements.php b/endpoints/achievements/achievements.php index 70fe460e..6597306f 100644 --- a/endpoints/achievements/achievements.php +++ b/endpoints/achievements/achievements.php @@ -11,7 +11,7 @@ class AchievementsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::ACHIEVEMENT; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'achievements'; protected string $pageName = 'achievements'; @@ -46,14 +46,22 @@ class AchievementsBaseResponse extends TemplateResponse implements ICache ) ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new AchievementListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -61,7 +69,7 @@ class AchievementsBaseResponse extends TemplateResponse implements ICache { $this->h1 = Util::ucFirst(Lang::game('achievements')); - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; @@ -69,13 +77,9 @@ class AchievementsBaseResponse extends TemplateResponse implements ICache if ($this->category) $conditions[] = ['category', end($this->category)]; - $this->filter->evalCriteria(); - if ($fiCnd = $this->filter->getConditions()) $conditions[] = $fiCnd; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /*************/ /* Menu Path */ @@ -116,10 +120,10 @@ class AchievementsBaseResponse extends TemplateResponse implements ICache if (!$acvList->getMatches() && $this->category) { // ToDo - we also branch into here if the filter prohibits results. That should be skipped. - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if ($fiCnd) $conditions[] = $fiCnd; - if ($catList = DB::Aowow()->SelectCol('SELECT `id` FROM ?_achievementcategory WHERE `parentCat` IN (?a) OR `parentCat2` IN (?a) ', end($this->category), end($this->category))) + if ($catList = DB::Aowow()->SelectCol('SELECT `id` FROM ::achievementcategory WHERE `parentCat` IN %in OR `parentCat2` IN %in ', $this->category, $this->category)) $conditions[] = ['category', $catList]; $acvList = new AchievementList($conditions, ['calcTotal' => true]); @@ -141,9 +145,9 @@ class AchievementsBaseResponse extends TemplateResponse implements ICache $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; // create note if search limit was exceeded - if ($acvList->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($acvList->getMatches() > Listview::DEFAULT_SIZE) { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_achievementsfound', $acvList->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_achievementsfound', $acvList->getMatches(), Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } } diff --git a/endpoints/admin/announcements.php b/endpoints/admin/announcements.php index ccd92eb7..b1fdc774 100644 --- a/endpoints/admin/announcements.php +++ b/endpoints/admin/announcements.php @@ -44,13 +44,13 @@ class AdminAnnouncementsResponse extends TemplateResponse return; } - if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_announcements WHERE `id` = ?d', $this->_get['id'])) + if (!DB::Aowow()->selectCell('SELECT 1 FROM ::announcements WHERE `id` = %i', $this->_get['id'])) { trigger_error('AdminAnnouncementsResponse::updateStatus - announcement does not exist'); return; } - DB::Aowow()->query('UPDATE ?_announcements SET `status` = ?d WHERE `id` = ?d', $this->_get['status'], $this->_get['id']); + DB::Aowow()->qry('UPDATE ::announcements SET `status` = %i WHERE `id` = %i', $this->_get['status'], $this->_get['id']); } private function displayEditor() : void diff --git a/endpoints/admin/comment.php b/endpoints/admin/comment.php index 08d4f8ba..26b78e29 100644 --- a/endpoints/admin/comment.php +++ b/endpoints/admin/comment.php @@ -33,13 +33,13 @@ class AdminCommentResponse extends TextResponse $ok = false; if ($this->_post['status']) // outdated, mark as deleted and clear other flags (sticky + outdated) { - if ($ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = ?d, `deleteUserId` = ?d, `deleteDate` = ?d WHERE `id` = ?d', CC_FLAG_DELETED, User::$id, time(), $this->_post['id'])) + if ($ok = DB::Aowow()->qry('UPDATE ::comments SET `flags` = %i, `deleteUserId` = %i, `deleteDate` = %i WHERE `id` = %i', CC_FLAG_DELETED, User::$id, time(), $this->_post['id'])) if ($rep = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id'])) $rep->close(Report::STATUS_CLOSED_SOLVED); } else // up to date { - if ($ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id'])) + if ($ok = DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` & ~%i WHERE `id` = %i', CC_FLAG_OUTDATED, $this->_post['id'])) if ($rep = new Report(Report::MODE_COMMENT, Report::CO_OUT_OF_DATE, $this->_post['id'])) $rep->close(Report::STATUS_CLOSED_WONTFIX); } diff --git a/endpoints/admin/guide.php b/endpoints/admin/guide.php index 8a6d93d8..90ee553e 100644 --- a/endpoints/admin/guide.php +++ b/endpoints/admin/guide.php @@ -31,7 +31,7 @@ class AdminGuideResponse extends TextResponse return; } - $guide = DB::Aowow()->selectRow('SELECT `userId`, `status` FROM ?_guides WHERE `id` = ?d', $this->_post['id']); + $guide = DB::Aowow()->selectRow('SELECT `userId`, `status` FROM ::guides WHERE `id` = %i', $this->_post['id']); if (!$guide) { trigger_error('AdminGuideResponse - guide #'.$this->_post['id'].' not found', E_USER_ERROR); @@ -63,16 +63,16 @@ class AdminGuideResponse extends TextResponse private function update(int $id, int $status, ?string $msg = null) : bool { if ($status == GuideMgr::STATUS_APPROVED) // set display rev to latest - $ok = DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d, `rev` = (SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC LIMIT 1), `approveUserId` = ?d, `approveDate` = ?d WHERE `id` = ?d', $status, Type::GUIDE, $id, User::$id, time(), $id); + $ok = DB::Aowow()->qry('UPDATE ::guides SET `status` = %i, `rev` = (SELECT `rev` FROM ::articles WHERE `type` = %i AND `typeId` = %i ORDER BY `rev` DESC LIMIT 1), `approveUserId` = %i, `approveDate` = %i WHERE `id` = %i', $status, Type::GUIDE, $id, User::$id, time(), $id); else - $ok = DB::Aowow()->query('UPDATE ?_guides SET `status` = ?d WHERE `id` = ?d', $status, $id); + $ok = DB::Aowow()->qry('UPDATE ::guides SET `status` = %i WHERE `id` = %i', $status, $id); if (!$ok) return false; - DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $id, time(), User::$id, $status); + DB::Aowow()->qry('INSERT INTO ::guides_changelog (`id`, `date`, `userId`, `status`) VALUES (%i, %i, %i, %i)', $id, time(), User::$id, $status); if ($msg) - DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?)', $id, time(), User::$id, $msg); + DB::Aowow()->qry('INSERT INTO ::guides_changelog (`id`, `date`, `userId`, `msg`) VALUES (%i, %i, %i, %s)', $id, time(), User::$id, $msg); return true; } diff --git a/endpoints/admin/guides.php b/endpoints/admin/guides.php index 16aa460b..26d6f844 100644 --- a/endpoints/admin/guides.php +++ b/endpoints/admin/guides.php @@ -30,7 +30,7 @@ class AdminGuidesResponse extends TemplateResponse else { $data = $pending->getListviewData(); - $latest = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, MAX(`rev`) FROM ?_articles WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `rev`', Type::GUIDE, $pending->getFoundIDs()); + $latest = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, MAX(`rev`) FROM ::articles WHERE `type` = %i AND `typeId` IN %in GROUP BY `rev`', Type::GUIDE, $pending->getFoundIDs()); foreach ($latest as $id => $rev) $data[$id]['rev'] = $rev; } diff --git a/endpoints/admin/screenshots.php b/endpoints/admin/screenshots.php index 33fe5a21..c8b87ba8 100644 --- a/endpoints/admin/screenshots.php +++ b/endpoints/admin/screenshots.php @@ -52,7 +52,7 @@ class AdminScreenshotsResponse extends TemplateResponse else if ($this->_get['user']) { if (mb_strlen($this->_get['user']) >= 3) - if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])) + if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE LOWER(`username`) = LOWER(%s)', $this->_get['user'])) $ssData = ScreenshotMgr::getScreenshots(userId: $uId, nFound: $nMatches); } else diff --git a/endpoints/admin/screenshots_approve.php b/endpoints/admin/screenshots_approve.php index da4c9763..5bb528c2 100644 --- a/endpoints/admin/screenshots_approve.php +++ b/endpoints/admin/screenshots_approve.php @@ -2,8 +2,6 @@ namespace Aowow; -use GdImage; - if (!defined('AOWOW_REVISION')) die('illegal access'); @@ -27,7 +25,7 @@ class AdminScreenshotsActionApproveResponse extends TextResponse ScreenshotMgr::init(); // create resized and thumb version of screenshot - $ssEntries = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId` FROM ?_screenshots WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_APPROVED, $this->_get['id']); + $ssEntries = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId` FROM ::screenshots WHERE (`status` & %i) = 0 AND `id` IN %in', CC_FLAG_APPROVED, $this->_get['id']); foreach ($ssEntries as $id => $ssData) { if (!ScreenshotMgr::loadFile(ScreenshotMgr::PATH_PENDING, $id)) @@ -44,14 +42,14 @@ class AdminScreenshotsActionApproveResponse extends TextResponse continue; // set as approved in DB - DB::Aowow()->query('UPDATE ?_screenshots SET `status` = ?d, `userIdApprove` = ?d WHERE `id` = ?d', CC_FLAG_APPROVED, User::$id, $id); + DB::Aowow()->qry('UPDATE ::screenshots SET `status` = %i, `userIdApprove` = %i WHERE `id` = %i', CC_FLAG_APPROVED, User::$id, $id); // gain siterep Util::gainSiteReputation($ssData['userIdOwner'], SITEREP_ACTION_SUBMIT_SCREENSHOT, ['id' => $id, 'what' => 1, 'date' => $ssData['date']]); // flag DB entry as having screenshots if ($tbl = Type::getClassAttrib($ssData['type'], 'dataTable')) - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_SCREENSHOT, $ssData['typeId']); + DB::Aowow()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_SCREENSHOT, $ssData['typeId']); unset($ssEntries[$id]); } diff --git a/endpoints/admin/screenshots_delete.php b/endpoints/admin/screenshots_delete.php index 402bc0ec..bd61a07c 100644 --- a/endpoints/admin/screenshots_delete.php +++ b/endpoints/admin/screenshots_delete.php @@ -26,9 +26,9 @@ class AdminScreenshotsActionDeleteResponse extends TextResponse foreach ($this->_get['id'] as $id) { // irrevocably purge files already flagged as deleted (should only exist as pending) - if (User::isInGroup(U_GROUP_ADMIN) && DB::Aowow()->selectCell('SELECT 1 FROM ?_screenshots WHERE `status` & ?d AND `id` = ?d', CC_FLAG_DELETED, $id)) + if (User::isInGroup(U_GROUP_ADMIN) && DB::Aowow()->selectCell('SELECT 1 FROM ::screenshots WHERE `status` & %i AND `id` = %i', CC_FLAG_DELETED, $id)) { - DB::Aowow()->query('DELETE FROM ?_screenshots WHERE `id` = ?d', $id); + DB::Aowow()->qry('DELETE FROM ::screenshots WHERE `id` = %i', $id); if (file_exists(sprintf(ScreenshotMgr::PATH_PENDING, $id))) unlink(sprintf(ScreenshotMgr::PATH_PENDING, $id)); @@ -47,16 +47,16 @@ class AdminScreenshotsActionDeleteResponse extends TextResponse } // flag as deleted if not aready - $oldEntries = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, GROUP_CONCAT(`typeId`) FROM ?_screenshots WHERE `id` IN (?a) GROUP BY `type`', $this->_get['id']); - DB::Aowow()->query('UPDATE ?_screenshots SET `status` = ?d, `userIdDelete` = ?d WHERE `id` IN (?a)', CC_FLAG_DELETED, User::$id, $this->_get['id']); + $oldEntries = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, GROUP_CONCAT(`typeId`) FROM ::screenshots WHERE `id` IN %in GROUP BY `type`', $this->_get['id']); + DB::Aowow()->qry('UPDATE ::screenshots SET `status` = %i, `userIdDelete` = %i WHERE `id` IN %in', CC_FLAG_DELETED, User::$id, $this->_get['id']); // deflag db entry as having screenshots foreach ($oldEntries as $type => $typeIds) { $typeIds = explode(',', $typeIds); - $toUnflag = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(BIT_OR(`status`) & ?d, 1, 0) AS "hasMore" FROM ?_screenshots WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId` HAVING `hasMore` = 0', CC_FLAG_APPROVED, $type, $typeIds); + $toUnflag = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(BIT_OR(`status`) & %i, 1, 0) AS "hasMore" FROM ::screenshots WHERE `type` = %i AND `typeId` IN %in GROUP BY `typeId` HAVING `hasMore` = 0', CC_FLAG_APPROVED, $type, $typeIds); if ($toUnflag && ($tbl = Type::getClassAttrib($type, 'dataTable'))) - DB::Aowow()->query('UPDATE ?# SET cuFlags = cuFlags & ~?d WHERE id IN (?a)', $tbl, CUSTOM_HAS_SCREENSHOT, array_keys($toUnflag)); + DB::Aowow()->qry('UPDATE %n SET cuFlags = cuFlags & ~%i WHERE id IN %in', $tbl, CUSTOM_HAS_SCREENSHOT, array_keys($toUnflag)); } } } diff --git a/endpoints/admin/screenshots_editalt.php b/endpoints/admin/screenshots_editalt.php index 0c5feee4..2dca89cc 100644 --- a/endpoints/admin/screenshots_editalt.php +++ b/endpoints/admin/screenshots_editalt.php @@ -24,7 +24,7 @@ class AdminScreenshotsActionEditaltResponse extends TextResponse if (!$this->assertGET('id')) return; - DB::Aowow()->query('UPDATE ?_screenshots SET `caption` = ? WHERE `id` = ?d', + DB::Aowow()->qry('UPDATE ::screenshots SET `caption` = %s WHERE `id` = %i', $this->handleCaption($this->_post['alt']), $this->_get['id'] ); diff --git a/endpoints/admin/screenshots_manage.php b/endpoints/admin/screenshots_manage.php index eddd0d92..752469d1 100644 --- a/endpoints/admin/screenshots_manage.php +++ b/endpoints/admin/screenshots_manage.php @@ -23,7 +23,7 @@ class AdminScreenshotsActionManageResponse extends TextResponse if ($this->_get['type'] && $this->_get['typeid']) $res = ScreenshotMgr::getScreenshots($this->_get['type'], $this->_get['typeid']); else if ($this->_get['user']) - if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])) + if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE LOWER(`username`) = LOWER(%s)', $this->_get['user'])) $res = ScreenshotMgr::getScreenshots(userId: $uId); $this->result = 'ssm_screenshotData = '.Util::toJSON($res); diff --git a/endpoints/admin/screenshots_relocate.php b/endpoints/admin/screenshots_relocate.php index 3f387fb7..03fd0d5d 100644 --- a/endpoints/admin/screenshots_relocate.php +++ b/endpoints/admin/screenshots_relocate.php @@ -24,7 +24,7 @@ class AdminScreenshotsActionRelocateResponse extends TextResponse return; } - [$type, $oldTypeId] = array_values(DB::Aowow()->selectRow('SELECT `type`, `typeId` FROM ?_screenshots WHERE `id` = ?d', $this->_get['id'])); + [$type, $oldTypeId] = array_values(DB::Aowow()->selectRow('SELECT `type`, `typeId` FROM ::screenshots WHERE `id` = %i', $this->_get['id'])); $typeId = $this->_get['typeid']; if (Type::validateIds($type, $typeId)) @@ -32,15 +32,15 @@ class AdminScreenshotsActionRelocateResponse extends TextResponse $tbl = Type::getClassAttrib($type, 'dataTable'); // move screenshot - DB::Aowow()->query('UPDATE ?_screenshots SET `typeId` = ?d WHERE `id` = ?d', $typeId, $this->_get['id']); + DB::Aowow()->qry('UPDATE ::screenshots SET `typeId` = %i WHERE `id` = %i', $typeId, $this->_get['id']); // flag target as having screenshot - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_SCREENSHOT, $typeId); + DB::Aowow()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_SCREENSHOT, $typeId); // deflag source for having had screenshots (maybe) - $ssInfo = DB::Aowow()->selectRow('SELECT IF(BIT_OR(~`status`) & ?d, 1, 0) AS "hasMore" FROM ?_screenshots WHERE `status`& ?d AND `type` = ?d AND `typeId` = ?d', CC_FLAG_DELETED, CC_FLAG_APPROVED, $type, $oldTypeId); + $ssInfo = DB::Aowow()->selectRow('SELECT IF(BIT_OR(~`status`) & %i, 1, 0) AS "hasMore" FROM ::screenshots WHERE `status`& %i AND `type` = %i AND `typeId` = %i', CC_FLAG_DELETED, CC_FLAG_APPROVED, $type, $oldTypeId); if ($ssInfo || !$ssInfo['hasMore']) - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` & ~?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_SCREENSHOT, $oldTypeId); + DB::Aowow()->qry('UPDATE %n SET `cuFlags` = `cuFlags` & ~%i WHERE `id` = %i', $tbl, CUSTOM_HAS_SCREENSHOT, $oldTypeId); } else trigger_error('AdminScreenshotsActionRelocateResponse - invalid typeId #'.$typeId.' for type #'.$type, E_USER_ERROR); diff --git a/endpoints/admin/screenshots_sticky.php b/endpoints/admin/screenshots_sticky.php index 26e3235f..d95ac874 100644 --- a/endpoints/admin/screenshots_sticky.php +++ b/endpoints/admin/screenshots_sticky.php @@ -25,7 +25,7 @@ class AdminScreenshotsActionStickyResponse extends TextResponse // this one is a bit strange: as far as i've seen, the only thing a 'sticky' screenshot does is show up in the infobox // this also means, that only one screenshot per page should be sticky // so, handle it one by one and the last one affecting one particular type/typId-key gets the cake - $ssEntries = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId`, `status` FROM ?_screenshots WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_DELETED, $this->_get['id']); + $ssEntries = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId`, `status` FROM ::screenshots WHERE (`status` & %i) = 0 AND `id` IN %in', CC_FLAG_DELETED, $this->_get['id']); foreach ($ssEntries as $id => $ssData) { // approve yet unapproved screenshots @@ -47,21 +47,21 @@ class AdminScreenshotsActionStickyResponse extends TextResponse continue; // set as approved in DB - DB::Aowow()->query('UPDATE ?_screenshots SET `status` = ?d, `userIdApprove` = ?d WHERE `id` = ?d', CC_FLAG_APPROVED, User::$id, $id); + DB::Aowow()->qry('UPDATE ::screenshots SET `status` = %i, `userIdApprove` = %i WHERE `id` = %i', CC_FLAG_APPROVED, User::$id, $id); // gain siterep Util::gainSiteReputation($ssData['userIdOwner'], SITEREP_ACTION_SUBMIT_SCREENSHOT, ['id' => $id, 'what' => 1, 'date' => $ssData['date']]); // flag DB entry as having screenshots if ($tbl = Type::getClassAttrib($ssData['type'], 'dataTable')) - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_SCREENSHOT, $ssData['typeId']); + DB::Aowow()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_SCREENSHOT, $ssData['typeId']); } // reset all others - DB::Aowow()->query('UPDATE ?_screenshots a, ?_screenshots b SET a.`status` = a.`status` & ~?d WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND a.`id` <> b.`id` AND b.`id` = ?d', CC_FLAG_STICKY, $id); + DB::Aowow()->qry('UPDATE ::screenshots a, ::screenshots b SET a.`status` = a.`status` & ~%i WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND a.`id` <> b.`id` AND b.`id` = %i', CC_FLAG_STICKY, $id); // toggle sticky status - DB::Aowow()->query('UPDATE ?_screenshots SET `status` = IF(`status` & ?d, `status` & ~?d, `status` | ?d) WHERE `id` = ?d AND `status` & ?d', CC_FLAG_STICKY, CC_FLAG_STICKY, CC_FLAG_STICKY, $id, CC_FLAG_APPROVED); + DB::Aowow()->qry('UPDATE ::screenshots SET `status` = IF(`status` & %i, `status` & ~%i, `status` | %i) WHERE `id` = %i AND `status` & %i', CC_FLAG_STICKY, CC_FLAG_STICKY, CC_FLAG_STICKY, $id, CC_FLAG_APPROVED); unset($ssEntries[$id]); } diff --git a/endpoints/admin/spawn-override.php b/endpoints/admin/spawn-override.php index 803d7c1a..8b6931ab 100644 --- a/endpoints/admin/spawn-override.php +++ b/endpoints/admin/spawn-override.php @@ -2,8 +2,6 @@ namespace Aowow; -use Error; - if (!defined('AOWOW_REVISION')) die('illegal access'); @@ -47,7 +45,7 @@ class AdminSpawnoverrideResponse extends TextResponse return; } - DB::Aowow()->query('REPLACE INTO ?_spawns_override (`type`, `typeGuid`, `areaId`, `floor`, `revision`) VALUES (?d, ?d, ?d, ?d, ?d)', $type, $guid, $area, $floor, AOWOW_REVISION); + DB::Aowow()->qry('REPLACE INTO ::spawns_override (`type`, `typeGuid`, `areaId`, `floor`, `revision`) VALUES (%i, %i, %i, %i, %i)', $type, $guid, $area, $floor, AOWOW_REVISION); $wPos = WorldPosition::getForGUID($type, $guid); if (!$wPos) @@ -74,39 +72,30 @@ class AdminSpawnoverrideResponse extends TextResponse // if creature try for waypoints if ($type == Type::NPC) { - $jobs = array( - 'SELECT -w.`id` AS "entry", w.`point` AS "pointId", w.`position_x` AS "posX", w.`position_y` AS "posY" FROM creature_addon ca JOIN waypoint_data w ON w.`id` = ca.`path_id` WHERE ca.`guid` = ?d AND ca.`path_id` <> 0', - 'SELECT `entry`, `pointId`, `location_x` AS "posX", `location_y` AS "posY" FROM `script_waypoint` WHERE `entry` = ?d', - 'SELECT `entry`, `pointId`, `position_x` AS "posX", `position_y` AS "posY" FROM `waypoints` WHERE `entry` = ?d' - ); - - foreach ($jobs as $idx => $job) + if ($swp = DB::World()->selectAssoc('SELECT -w.`id` AS "entry", w.`point` AS "pointId", w.`position_x` AS "posX", w.`position_y` AS "posY" FROM creature_addon ca JOIN waypoint_data w ON w.`id` = ca.`path_id` WHERE ca.`guid` = %i AND ca.`path_id` <> 0', $guid)) { - if ($swp = DB::World()->select($job, $idx ? $wPos[$guid]['id'] : $guid)) + foreach ($swp as $w) { - foreach ($swp as $w) + if ($point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $w['posX'], $w['posY'], $area, $floor)) { - if ($point = WorldPosition::toZonePos($wPos[$guid]['mapId'], $w['posX'], $w['posY'], $area, $floor)) - { - $p = array( - 'posX' => $point[0]['posX'], - 'posY' => $point[0]['posY'], - 'areaId' => $point[0]['areaId'], - 'floor' => $point[0]['floor'] - ); + $p = array( + 'posX' => $point[0]['posX'], + 'posY' => $point[0]['posY'], + 'areaId' => $point[0]['areaId'], + 'floor' => $point[0]['floor'] + ); - DB::Aowow()->query('UPDATE ?_creature_waypoints SET ?a WHERE `creatureOrPath` = ?d AND `point` = ?d', $p, $w['entry'], $w['pointId']); - } + DB::Aowow()->qry('UPDATE ::creature_waypoints SET %a WHERE `creatureOrPath` = %i AND `point` = %i', $p, $w['entry'], $w['pointId']); } } } // also move linked vehicle accessories (on the very same position) - $updGUIDs = array_merge($updGUIDs, DB::Aowow()->selectCol('SELECT s2.`guid` FROM ?_spawns s1 JOIN ?_spawns s2 ON s1.`posX` = s2.`posX` AND s1.`posY` = s2.`posY` AND - s1.`areaId` = s2.`areaId` AND s1.`floor` = s2.`floor` AND s2.`guid` < 0 WHERE s1.`guid` = ?d', $guid)); + $updGUIDs = array_merge($updGUIDs, DB::Aowow()->selectCol('SELECT s2.`guid` FROM ::spawns s1 JOIN ::spawns s2 ON s1.`posX` = s2.`posX` AND s1.`posY` = s2.`posY` AND + s1.`areaId` = s2.`areaId` AND s1.`floor` = s2.`floor` AND s2.`guid` < 0 WHERE s1.`guid` = %i', $guid)); } - if (DB::Aowow()->query('UPDATE ?_spawns SET ?a WHERE `type` = ?d AND `guid` IN (?a)', $newPos, $type, $updGUIDs)) + if (DB::Aowow()->qry('UPDATE ::spawns SET %a WHERE `type` = %i AND `guid` IN %in', $newPos, $type, $updGUIDs)) $this->result = self::ERR_NONE; else $this->result = self::ERR_WRITE_DB; diff --git a/endpoints/admin/videos.php b/endpoints/admin/videos.php index 10beece8..04956306 100644 --- a/endpoints/admin/videos.php +++ b/endpoints/admin/videos.php @@ -52,7 +52,7 @@ class AdminVideosResponse extends TemplateResponse else if ($this->_get['user']) { if (mb_strlen($this->_get['user']) >= 3) - if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])) + if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE LOWER(`username`) = LOWER(%s)', $this->_get['user'])) $viData = VideoMgr::getVideos(userId: $uId, nFound: $nMatches); } else diff --git a/endpoints/admin/videos_approve.php b/endpoints/admin/videos_approve.php index 32e866e9..efa12210 100644 --- a/endpoints/admin/videos_approve.php +++ b/endpoints/admin/videos_approve.php @@ -2,8 +2,6 @@ namespace Aowow; -use GdImage; - if (!defined('AOWOW_REVISION')) die('illegal access'); @@ -24,18 +22,18 @@ class AdminVideosActionApproveResponse extends TextResponse return; } - $viEntries = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId` FROM ?_videos WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_APPROVED, $this->_get['id']); + $viEntries = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId` FROM ::videos WHERE (`status` & %i) = 0 AND `id` IN %in', CC_FLAG_APPROVED, $this->_get['id']); foreach ($viEntries as $id => $viData) { // set as approved in DB - DB::Aowow()->query('UPDATE ?_videos SET `status` = ?d, `userIdApprove` = ?d WHERE `id` = ?d', CC_FLAG_APPROVED, User::$id, $id); + DB::Aowow()->qry('UPDATE ::videos SET `status` = %i, `userIdApprove` = %i WHERE `id` = %i', CC_FLAG_APPROVED, User::$id, $id); // gain siterep Util::gainSiteReputation($viData['userIdOwner'], SITEREP_ACTION_SUGGEST_VIDEO, ['id' => $id, 'what' => 1, 'date' => $viData['date']]); // flag DB entry as having videos if ($tbl = Type::getClassAttrib($viData['type'], 'dataTable')) - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $viData['typeId']); + DB::Aowow()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_VIDEO, $viData['typeId']); unset($viEntries[$id]); } diff --git a/endpoints/admin/videos_delete.php b/endpoints/admin/videos_delete.php index d3b6c19e..7a404124 100644 --- a/endpoints/admin/videos_delete.php +++ b/endpoints/admin/videos_delete.php @@ -25,19 +25,19 @@ class AdminVideosActionDeleteResponse extends TextResponse // irrevocably purge files already flagged as deleted (should only exist as pending) if (User::isInGroup(U_GROUP_ADMIN)) - DB::Aowow()->selectCell('SELECT 1 FROM ?_videos WHERE `status` & ?d AND `id` IN (?a)', CC_FLAG_DELETED, $this->_get['id']); + DB::Aowow()->selectCell('SELECT 1 FROM ::videos WHERE `status` & %i AND `id` IN %in', CC_FLAG_DELETED, $this->_get['id']); // flag as deleted if not aready - $oldEntries = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, GROUP_CONCAT(`typeId`) FROM ?_videos WHERE `id` IN (?a) GROUP BY `type`', $this->_get['id']); - DB::Aowow()->query('UPDATE ?_videos SET `status` = ?d, `userIdDelete` = ?d WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_DELETED, User::$id, CC_FLAG_DELETED, $this->_get['id']); + $oldEntries = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, GROUP_CONCAT(`typeId`) FROM ::videos WHERE `id` IN %in GROUP BY `type`', $this->_get['id']); + DB::Aowow()->qry('UPDATE ::videos SET `status` = %i, `userIdDelete` = %i WHERE (`status` & %i) = 0 AND `id` IN %in', CC_FLAG_DELETED, User::$id, CC_FLAG_DELETED, $this->_get['id']); // deflag db entry as having videos foreach ($oldEntries as $type => $typeIds) { $typeIds = explode(',', $typeIds); - $toUnflag = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(BIT_OR(`status`) & ?d, 1, 0) AS "hasMore" FROM ?_videos WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId` HAVING `hasMore` = 0', CC_FLAG_APPROVED, $type, $typeIds); + $toUnflag = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(BIT_OR(`status`) & %i, 1, 0) AS "hasMore" FROM ::videos WHERE `type` = %i AND `typeId` IN %in GROUP BY `typeId` HAVING `hasMore` = 0', CC_FLAG_APPROVED, $type, $typeIds); if ($toUnflag && ($tbl = Type::getClassAttrib($type, 'dataTable'))) - DB::Aowow()->query('UPDATE ?# SET cuFlags = cuFlags & ~?d WHERE id IN (?a)', $tbl, CUSTOM_HAS_VIDEO, array_keys($toUnflag)); + DB::Aowow()->qry('UPDATE %n SET cuFlags = cuFlags & ~%i WHERE id IN %in', $tbl, CUSTOM_HAS_VIDEO, array_keys($toUnflag)); } } } diff --git a/endpoints/admin/videos_edittitle.php b/endpoints/admin/videos_edittitle.php index d9c92dfe..d6523f6d 100644 --- a/endpoints/admin/videos_edittitle.php +++ b/endpoints/admin/videos_edittitle.php @@ -26,6 +26,6 @@ class AdminVideosActionEdittitleResponse extends TextResponse $caption = $this->handleCaption($this->_post['title']); - DB::Aowow()->query('UPDATE ?_videos SET `caption` = ? WHERE `id` = ?d', $caption, $this->_get['id'][0]); + DB::Aowow()->qry('UPDATE ::videos SET `caption` = %s WHERE `id` = %i', $caption, $this->_get['id'][0]); } } diff --git a/endpoints/admin/videos_manage.php b/endpoints/admin/videos_manage.php index 0bd4e31e..fb5180f7 100644 --- a/endpoints/admin/videos_manage.php +++ b/endpoints/admin/videos_manage.php @@ -23,7 +23,7 @@ class AdminVideosActionManageResponse extends TextResponse if ($this->_get['type'] && $this->_get['typeid']) $res = VideoMgr::getVideos($this->_get['type'], $this->_get['typeid']); else if ($this->_get['user']) - if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user'])) + if ($uId = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE LOWER(`username`) = LOWER(%s)', $this->_get['user'])) $res = VideoMgr::getVideos(userId: $uId); $this->result = 'vim_videoData = '.Util::toJSON($res); diff --git a/endpoints/admin/videos_order.php b/endpoints/admin/videos_order.php index f11f64eb..05fd749e 100644 --- a/endpoints/admin/videos_order.php +++ b/endpoints/admin/videos_order.php @@ -25,7 +25,7 @@ class AdminVideosActionOrderResponse extends TextResponse $id = $this->_get['id'][0]; - $videos = DB::Aowow()->selectCol('SELECT a.`id` AS ARRAY_KEY, a.`pos` FROM ?_videos a, ?_videos b WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND (a.`status` & ?d) = 0 AND b.`id` = ?d ORDER BY a.`pos` ASC', CC_FLAG_DELETED, $id); + $videos = DB::Aowow()->selectCol('SELECT a.`id` AS ARRAY_KEY, a.`pos` FROM ::videos a, ::videos b WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND (a.`status` & %i) = 0 AND b.`id` = %i ORDER BY a.`pos` ASC', CC_FLAG_DELETED, $id); if (!$videos || count($videos) == 1) { trigger_error('AdminVideosActionOrderResponse - not enough videos to sort', E_USER_WARNING); @@ -52,6 +52,6 @@ class AdminVideosActionOrderResponse extends TextResponse $videos[$id] += $dir; foreach ($videos as $id => $pos) - DB::Aowow()->query('UPDATE ?_videos SET `pos` = ?d WHERE `id` = ?d', $pos, $id); + DB::Aowow()->qry('UPDATE ::videos SET `pos` = %i WHERE `id` = %i', $pos, $id); } } diff --git a/endpoints/admin/videos_relocate.php b/endpoints/admin/videos_relocate.php index aa2bd6a1..12230906 100644 --- a/endpoints/admin/videos_relocate.php +++ b/endpoints/admin/videos_relocate.php @@ -25,7 +25,7 @@ class AdminVideosActionRelocateResponse extends TextResponse } $id = $this->_get['id'][0]; - [$type, $oldTypeId] = array_values(DB::Aowow()->selectRow('SELECT `type`, `typeId` FROM ?_videos WHERE `id` = ?d', $id)); + [$type, $oldTypeId] = array_values(DB::Aowow()->selectRow('SELECT `type`, `typeId` FROM ::videos WHERE `id` = %i', $id)); $typeId = $this->_get['typeid']; if (Type::validateIds($type, $typeId)) @@ -33,15 +33,15 @@ class AdminVideosActionRelocateResponse extends TextResponse $tbl = Type::getClassAttrib($type, 'dataTable'); // move video - DB::Aowow()->query('UPDATE ?_videos SET `typeId` = ?d WHERE `id` = ?d', $typeId, $id); + DB::Aowow()->qry('UPDATE ::videos SET `typeId` = %i WHERE `id` = %i', $typeId, $id); // flag target as having video - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $typeId); + DB::Aowow()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_VIDEO, $typeId); // deflag source for having had videos (maybe) - $viInfo = DB::Aowow()->selectRow('SELECT IF(BIT_OR(~`status`) & ?d, 1, 0) AS "hasMore" FROM ?_videos WHERE `status`& ?d AND `type` = ?d AND `typeId` = ?d', CC_FLAG_DELETED, CC_FLAG_APPROVED, $type, $oldTypeId); + $viInfo = DB::Aowow()->selectRow('SELECT IF(BIT_OR(~`status`) & %i, 1, 0) AS "hasMore" FROM ::videos WHERE `status`& %i AND `type` = %i AND `typeId` = %i', CC_FLAG_DELETED, CC_FLAG_APPROVED, $type, $oldTypeId); if ($viInfo || !$viInfo['hasMore']) - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` & ~?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $oldTypeId); + DB::Aowow()->qry('UPDATE %n SET `cuFlags` = `cuFlags` & ~%i WHERE `id` = %i', $tbl, CUSTOM_HAS_VIDEO, $oldTypeId); } else trigger_error('AdminVideosActionRelocateResponse - invalid typeId #'.$typeId.' for type #'.$type, E_USER_ERROR); diff --git a/endpoints/admin/videos_sticky.php b/endpoints/admin/videos_sticky.php index ea79ac4b..40c537af 100644 --- a/endpoints/admin/videos_sticky.php +++ b/endpoints/admin/videos_sticky.php @@ -24,28 +24,28 @@ class AdminVideosActionStickyResponse extends TextResponse // this one is a bit strange: as far as i've seen, the only thing a 'sticky' video does is show up in the infobox // this also means, that only one video per page should be sticky // so, handle it one by one and the last one affecting one particular type/typId-key gets the cake - $viEntries = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId`, `status` FROM ?_videos WHERE (`status` & ?d) = 0 AND `id` IN (?a)', CC_FLAG_DELETED, $this->_get['id']); + $viEntries = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `userIdOwner`, `date`, `type`, `typeId`, `status` FROM ::videos WHERE (`status` & %i) = 0 AND `id` IN %in', CC_FLAG_DELETED, $this->_get['id']); foreach ($viEntries as $id => $viData) { // approve yet unapproved videos if (!($viData['status'] & CC_FLAG_APPROVED)) { // set as approved in DB - DB::Aowow()->query('UPDATE ?_videos SET `status` = ?d, `userIdApprove` = ?d WHERE `id` = ?d', CC_FLAG_APPROVED, User::$id, $id); + DB::Aowow()->qry('UPDATE ::videos SET `status` = %i, `userIdApprove` = %i WHERE `id` = %i', CC_FLAG_APPROVED, User::$id, $id); // gain siterep Util::gainSiteReputation($viData['userIdOwner'], SITEREP_ACTION_SUGGEST_VIDEO, ['id' => $id, 'what' => 1, 'date' => $viData['date']]); // flag DB entry as having videos if ($tbl = Type::getClassAttrib($viData['type'], 'dataTable')) - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_VIDEO, $viData['typeId']); + DB::Aowow()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_VIDEO, $viData['typeId']); } // reset all others - DB::Aowow()->query('UPDATE ?_videos a, ?_videos b SET a.`status` = a.`status` & ~?d WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND a.`id` <> b.`id` AND b.`id` = ?d', CC_FLAG_STICKY, $id); + DB::Aowow()->qry('UPDATE ::videos a, ::videos b SET a.`status` = a.`status` & ~%i WHERE a.`type` = b.`type` AND a.`typeId` = b.`typeId` AND a.`id` <> b.`id` AND b.`id` = %i', CC_FLAG_STICKY, $id); // toggle sticky status - DB::Aowow()->query('UPDATE ?_videos SET `status` = IF(`status` & ?d, `status` & ~?d, `status` | ?d) WHERE `id` = ?d AND `status` & ?d', CC_FLAG_STICKY, CC_FLAG_STICKY, CC_FLAG_STICKY, $id, CC_FLAG_APPROVED); + DB::Aowow()->qry('UPDATE ::videos SET `status` = IF(`status` & %i, `status` & ~%i, `status` | %i) WHERE `id` = %i AND `status` & %i', CC_FLAG_STICKY, CC_FLAG_STICKY, CC_FLAG_STICKY, $id, CC_FLAG_APPROVED); unset($viEntries[$id]); } diff --git a/endpoints/admin/weight-presets.php b/endpoints/admin/weight-presets.php index a59e6ef6..3fa4a0ab 100644 --- a/endpoints/admin/weight-presets.php +++ b/endpoints/admin/weight-presets.php @@ -27,8 +27,8 @@ class AdminWeightpresetsResponse extends TemplateResponse $head = $body = ''; - $scales = DB::Aowow()->select('SELECT `class` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `name`, `icon` FROM ?_account_weightscales WHERE `userId` = 0'); - $weights = DB::Aowow()->selectCol('SELECT awd.`id` AS ARRAY_KEY, awd.`field` AS ARRAY_KEY2, awd.`val` FROM ?_account_weightscale_data awd JOIN ?_account_weightscales ad ON awd.`id` = ad.`id` WHERE ad.`userId` = 0'); + $scales = DB::Aowow()->selectAssoc('SELECT `class` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `name`, `icon` FROM ::account_weightscales WHERE `userId` = 0'); + $weights = DB::Aowow()->selectCol('SELECT awd.`id` AS ARRAY_KEY, awd.`field` AS ARRAY_KEY2, awd.`val` FROM ::account_weightscale_data awd JOIN ::account_weightscales ad ON awd.`id` = ad.`id` WHERE ad.`userId` = 0'); foreach ($scales as $cl => $data) { $ul = ''; diff --git a/endpoints/admin/weight-presets_save.php b/endpoints/admin/weight-presets_save.php index 50038a54..c73c17ff 100644 --- a/endpoints/admin/weight-presets_save.php +++ b/endpoints/admin/weight-presets_save.php @@ -30,17 +30,17 @@ class AdminWeightpresetsActionSaveResponse extends TextResponse } // save to db - DB::Aowow()->query('DELETE FROM ?_account_weightscale_data WHERE `id` = ?d', $this->_post['id']); - DB::Aowow()->query('UPDATE ?_account_weightscales SET `icon`= ? WHERE `id` = ?d', $this->_post['__icon'], $this->_post['id']); + DB::Aowow()->qry('DELETE FROM ::account_weightscale_data WHERE `id` = %i', $this->_post['id']); + DB::Aowow()->qry('UPDATE ::account_weightscales SET `icon`= %s WHERE `id` = %i', $this->_post['__icon'], $this->_post['id']); foreach (explode(',', $this->_post['scale']) as $s) { [$k, $v] = explode(':', $s); - if (!in_array($k, Util::$weightScales) || $v < 1) + if (!Stat::getWeightJson($k) || $v < 1) continue; - if (DB::Aowow()->query('INSERT INTO ?_account_weightscale_data VALUES (?d, ?, ?d)', $this->_post['id'], $k, $v) === null) + if (DB::Aowow()->qry('INSERT INTO ::account_weightscale_data VALUES (%i, %s, %i)', $this->_post['id'], $k, $v) === null) { trigger_error('AdminWeightpresetsActionSaveResponse - failed to write to database', E_USER_ERROR); $this->result = self::ERR_WRITE_DB; diff --git a/endpoints/areatrigger/areatrigger.php b/endpoints/areatrigger/areatrigger.php index b0ce07ca..c8a02e54 100644 --- a/endpoints/areatrigger/areatrigger.php +++ b/endpoints/areatrigger/areatrigger.php @@ -10,7 +10,7 @@ class AreatriggerBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected int $requiredUserGroup = U_GROUP_STAFF; protected string $template = 'detail-page-generic'; @@ -105,7 +105,7 @@ class AreatriggerBaseResponse extends TemplateResponse implements ICache // tab: conditions $cnd = new Conditions(); - $cnd->getBySourceEntry($this->typeId, Conditions::SRC_AREATRIGGER_CLIENT)->prepare(); + $cnd->getBySource(Conditions::SRC_AREATRIGGER_CLIENT, entry: $this->typeId)->prepare(); if ($tab = $cnd->toListviewTab()) { $this->extendGlobalData($cnd->getJsGlobals()); diff --git a/endpoints/areatriggers/areatriggers.php b/endpoints/areatriggers/areatriggers.php index 04b06d46..0d681c95 100644 --- a/endpoints/areatriggers/areatriggers.php +++ b/endpoints/areatriggers/areatriggers.php @@ -11,7 +11,7 @@ class AreatriggersBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::AREATRIGGER; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected int $requiredUserGroup = U_GROUP_STAFF; protected string $template = 'areatriggers'; @@ -23,16 +23,22 @@ class AreatriggersBaseResponse extends TemplateResponse implements ICache protected array $expectedGET = ['filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]]]; protected array $validCats = [0, 1, 2, 3, 4, 5]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); if (isset($this->category[0])) $this->forward('?areatriggers&filter=ty='.$this->category[0]); - parent::__construct($pageParam); + parent::__construct($rawParam); $this->filter = new AreaTriggerListFilter($this->_get['filter'] ?? ''); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -40,8 +46,6 @@ class AreatriggersBaseResponse extends TemplateResponse implements ICache { $this->h1 = Util::ucFirst(Lang::game('areatriggers')); - $this->filter->evalCriteria(); - $fiForm = $this->filter->values; @@ -69,12 +73,10 @@ class AreatriggersBaseResponse extends TemplateResponse implements ICache $this->redButtons[BUTTON_WOWHEAD] = false; - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - $tabData = []; $trigger = new AreaTriggerList($conditions, ['calcTotal' => true]); if (!$trigger->error) @@ -82,9 +84,9 @@ class AreatriggersBaseResponse extends TemplateResponse implements ICache $tabData['data'] = $trigger->getListviewData(); // create note if search limit was exceeded; overwriting 'note' is intentional - if ($trigger->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($trigger->getMatches() > Listview::DEFAULT_SIZE) { - $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $trigger->getMatches(), '"'.Lang::game('areatriggers').'"', Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryFilteringEntityString, $trigger->getMatches(), '"'.Lang::game('areatriggers').'"', Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } } diff --git a/endpoints/arena-team/arena-team.php b/endpoints/arena-team/arena-team.php index 2d40ce97..fc1e1a52 100644 --- a/endpoints/arena-team/arena-team.php +++ b/endpoints/arena-team/arena-team.php @@ -46,24 +46,26 @@ class ArenateamBaseResponse extends TemplateResponse // 3 possibilities // 1) already synced to aowow - if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `cuFlags` FROM ?_profiler_arena_team WHERE `realm` = ?d AND `nameUrl` = ?', $this->realmId, Profiler::urlize($this->subjectName))) + if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `stub` FROM ::profiler_arena_team WHERE `realm` = %i AND `nameUrl` = %s', $this->realmId, Profiler::urlize($this->subjectName))) { $this->typeId = $subject['id']; - if ($subject['cuFlags'] & PROFILER_CU_NEEDS_RESYNC) + if ($subject['stub']) $this->handleIncompleteData(Type::ARENA_TEAM, $subject['realmGUID']); return; } // 2) not yet synced but exists on realm (wont work if we get passed an urlized name, but there is nothing we can do about it) - else if ($subject = DB::Characters($this->realmId)->selectRow('SELECT at.`arenaTeamId` AS "realmGUID", at.`name`, at.`type` FROM arena_team at WHERE at.`name` = ?', Util::ucFirst($this->subjectName))) + $subjects = DB::Characters($this->realmId)->selectAssoc('SELECT at.`arenaTeamId` AS "realmGUID", at.`name`, at.`type` FROM arena_team at WHERE at.`name` = %s', $this->subjectName); + if ($subject = array_find($subjects ?: [], fn($x) => Util::lower($x['name']) === Util::lower($this->subjectName))) { $subject['realm'] = $this->realmId; - $subject['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; + $subject['stub'] = 1; + $subject['nameUrl'] = Profiler::urlize($subject['name']); // create entry from realm with basic info - DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team (?#) VALUES (?a)', array_keys($subject), array_values($subject)); + DB::Aowow()->qry('INSERT IGNORE INTO ::profiler_arena_team %v', $subject); $this->handleIncompleteData(Type::ARENA_TEAM, $subject['realmGUID']); return; diff --git a/endpoints/arena-team/resync.php b/endpoints/arena-team/resync.php index dc16f103..ac57929d 100644 --- a/endpoints/arena-team/resync.php +++ b/endpoints/arena-team/resync.php @@ -13,9 +13,9 @@ class ArenaTeamResyncResponse extends TextResponse 'profile' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -32,12 +32,12 @@ class ArenaTeamResyncResponse extends TextResponse if (!$this->assertGET('id')) return; - if ($teams = DB::Aowow()->select('SELECT `realm`, `realmGUID` FROM ?_profiler_arena_team WHERE `id` IN (?a)', $this->_get['id'])) + if ($teams = DB::Aowow()->selectAssoc('SELECT `realm`, `realmGUID` FROM ::profiler_arena_team WHERE `id` IN %in', $this->_get['id'])) foreach ($teams as $t) Profiler::scheduleResync(Type::ARENA_TEAM, $t['realm'], $t['realmGUID']); if ($this->_get['profile']) - if ($chars = DB::Aowow()->select('SELECT `realm`, `realmGUID` FROM ?_profiler_profiles p JOIN ?_profiler_arena_team_member atm ON atm.`profileId` = p.`id` WHERE atm.`arenaTeamId` IN (?a)', $this->_get['id'])) + if ($chars = DB::Aowow()->selectAssoc('SELECT `realm`, `realmGUID` FROM ::profiler_profiles p JOIN ::profiler_arena_team_member atm ON atm.`profileId` = p.`id` WHERE atm.`arenaTeamId` IN %in', $this->_get['id'])) foreach ($chars as $c) Profiler::scheduleResync(Type::PROFILE, $c['realm'], $c['realmGUID']); diff --git a/endpoints/arena-team/status.php b/endpoints/arena-team/status.php index b9029d61..ad89f016 100644 --- a/endpoints/arena-team/status.php +++ b/endpoints/arena-team/status.php @@ -12,9 +12,9 @@ class ArenaTeamStatusResponse extends TextResponse 'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); diff --git a/endpoints/arena-teams/arena-teams.php b/endpoints/arena-teams/arena-teams.php index 2b8dbf73..9983422d 100644 --- a/endpoints/arena-teams/arena-teams.php +++ b/endpoints/arena-teams/arena-teams.php @@ -29,14 +29,14 @@ class ArenateamsBaseResponse extends TemplateResponse implements IProfilerList private int $sumSubjects = 0; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { if (!Cfg::get('PROFILER_ENABLE')) $this->generateError(); - $this->getSubjectFromUrl($pageParam); + $this->getSubjectFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); $realms = []; foreach (Profiler::getRealms() as $idx => $r) @@ -51,8 +51,16 @@ class ArenateamsBaseResponse extends TemplateResponse implements IProfilerList $realms[] = $idx; } - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + if ($this->category) + $this->subCat = '='.implode('.', $this->category); + $this->filter = new ArenaTeamListFilter($this->_get['filter'] ?? '', ['realms' => $realms]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -84,7 +92,7 @@ class ArenateamsBaseResponse extends TemplateResponse implements IProfilerList /* Main Content */ /****************/ - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = ['at.seasonGames', 0, '>']; @@ -123,12 +131,12 @@ class ArenateamsBaseResponse extends TemplateResponse implements IProfilerList $tabData['data'] = $teams->getListviewData(); // create note if search limit was exceeded - if ($this->filter->query && $teams->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($this->filter->query && $teams->getMatches() > Listview::DEFAULT_SIZE) { $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_arenateamsfound2', $this->sumSubjects, $teams->getMatches()); $tabData['_truncated'] = 1; } - else if ($teams->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + else if ($teams->getMatches() > Listview::DEFAULT_SIZE) $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_arenateamsfound', $this->sumSubjects, 0); } diff --git a/endpoints/class/class.php b/endpoints/class/class.php index 50ca4666..47be4379 100644 --- a/endpoints/class/class.php +++ b/endpoints/class/class.php @@ -12,7 +12,7 @@ class ClassBaseResponse extends TemplateResponse implements ICache private const TC_CLASS_IDS = [null, 8, 3, 1, 5, 4, 9, 6, 2, 7, null, 0]; // see TalentCalc.js - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'class'; @@ -103,6 +103,20 @@ class ClassBaseResponse extends TemplateResponse implements ICache if ($specList) $infobox[] = Lang::game('specs').'[ul][li]'.implode('[/li][li]', $specList).'[/li][/ul]'; + // id + $infobox[] = Lang::chrClass('id') . $this->typeId; + + // icon + if ($_ = $this->subject->getField('iconId')) + { + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; + $this->extendGlobalIds(Type::ICON, $_); + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); @@ -112,7 +126,6 @@ class ClassBaseResponse extends TemplateResponse implements ICache /****************/ $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; - $this->headIcons = ['class_'.$cl->json()]; $this->redButtons = array( BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId], BUTTON_WOWHEAD => true, @@ -120,6 +133,9 @@ class ClassBaseResponse extends TemplateResponse implements ICache BUTTON_FORUM => false // todo (low): Cfg::get('BOARD_URL') + X ); + if ($_ = $this->subject->getField('iconString')) + $this->headIcons[] = $_; + /**************/ /* Extra Tabs */ @@ -127,7 +143,7 @@ class ClassBaseResponse extends TemplateResponse implements ICache $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); - // Tab: Spells (grouped) + // tab: spells (grouped) // '$LANG.tab_armorproficiencies', // '$LANG.tab_weaponskills', // '$LANG.tab_glyphs', @@ -137,19 +153,18 @@ class ClassBaseResponse extends TemplateResponse implements ICache ['s.typeCat', [-13, -11, -2, 7]], [['s.cuFlags', (SPELL_CU_TRIGGERED | CUSTOM_EXCLUDE_FOR_LISTVIEW), '&'], 0], [ - 'OR', + DB::OR, // Glyphs, Proficiencies ['s.reqClassMask', $cl->toMask(), '&'], // Abilities / Talents ['s.skillLine1', $this->subject->getField('skills')], - ['AND', ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->subject->getField('skills')]] + [DB::AND, ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->subject->getField('skills')]] ], [ // last rank or unranked - 'OR', + DB::OR, ['s.cuFlags', SPELL_CU_LAST_RANK, '&'], ['s.rankNo', 0] - ], - Cfg::get('SQL_LIMIT_NONE') + ] ); $genSpells = new SpellList($conditions); @@ -169,13 +184,10 @@ class ClassBaseResponse extends TemplateResponse implements ICache ), SpellList::$brickFile)); } - // Tab: Items (grouped) + // tab: items (grouped) $conditions = array( - ['requiredClass', 0, '>'], ['requiredClass', $cl->toMask(), '&'], - [['requiredClass', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!'], - ['itemset', 0], - Cfg::get('SQL_LIMIT_NONE') + ['itemset', 0] ); $items = new ItemList($conditions); @@ -200,7 +212,7 @@ class ClassBaseResponse extends TemplateResponse implements ICache ), ItemList::$brickFile)); } - // Tab: Quests + // tab: quests $conditions = array( ['reqClassMask', $cl->toMask(), '&'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!'] @@ -217,7 +229,7 @@ class ClassBaseResponse extends TemplateResponse implements ICache ), QuestList::$brickFile)); } - // Tab: Itemsets + // tab: itemsets $sets = new ItemsetList(array(['classMask', $cl->toMask(), '&'])); if (!$sets->error) { @@ -231,7 +243,7 @@ class ClassBaseResponse extends TemplateResponse implements ICache ), ItemsetList::$brickFile)); } - // Tab: Trainer + // tab: trainers $conditions = array( ['npcflag', NPC_FLAG_TRAINER | NPC_FLAG_CLASS_TRAINER, '&'], ['trainerType', 0], // trains class spells @@ -249,11 +261,33 @@ class ClassBaseResponse extends TemplateResponse implements ICache ), CreatureList::$brickFile)); } - // Tab: Races + // tab: races $races = new CharRaceList(array(['classMask', $cl->toMask(), '&'])); if (!$races->error) $this->lvTabs->addListviewTab(new Listview(['data' => $races->getListviewData()], CharRaceList::$brickFile)); + // tab: criteria-of + $conditions = array( + DB::AND, + ['ac.type', ACHIEVEMENT_CRITERIA_TYPE_HK_CLASS], + ['ac.value1', $this->typeId] + ); + + if ($extraCrt = DB::World()->selectCol('SELECT `criteria_id` FROM achievement_criteria_data WHERE `type` IN %in AND `value1` = %i', [ACHIEVEMENT_CRITERIA_DATA_TYPE_S_PLAYER_CLASS_RACE, ACHIEVEMENT_CRITERIA_DATA_TYPE_T_PLAYER_CLASS_RACE], $this->typeId)) + $conditions = [DB::OR, $conditions, ['ac.id', $extraCrt]]; + + $crtOf = new AchievementList($conditions); + if (!$crtOf->error) + { + $this->extendGlobalData($crtOf->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $crtOf->getListviewData(), + 'name' => '$LANG.tab_criteriaof', + 'id' => 'criteria-of' + ), AchievementList::$brickFile)); + } + // tab: condition-for $cnd = new Conditions(); $cnd->getByCondition(Type::CHR_CLASS, $this->typeId)->prepare(); diff --git a/endpoints/classes/classes.php b/endpoints/classes/classes.php index 3e7251a0..07e1f6f9 100644 --- a/endpoints/classes/classes.php +++ b/endpoints/classes/classes.php @@ -11,18 +11,18 @@ class ClassesBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::CHR_CLASS; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'classes'; protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 12]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void diff --git a/endpoints/comment/add-reply.php b/endpoints/comment/add-reply.php index ab2ab71d..6b9d7931 100644 --- a/endpoints/comment/add-reply.php +++ b/endpoints/comment/add-reply.php @@ -29,7 +29,7 @@ class CommentAddreplyResponse extends TextResponse if (!User::canReply()) $this->generate404(Lang::main('cannotComment')); - if (!$this->_post['commentId'] || !DB::Aowow()->selectCell('SELECT 1 FROM ?_comments WHERE `id` = ?d', $this->_post['commentId'])) + if (!$this->_post['commentId'] || !DB::Aowow()->selectCell('SELECT 1 FROM ::comments WHERE `id` = %i', $this->_post['commentId'])) { trigger_error('CommentAddreplyResponse - parent comment #'.$this->_post['commentId'].' does not exist', E_USER_ERROR); $this->generate404(Lang::main('intError')); @@ -38,7 +38,7 @@ class CommentAddreplyResponse extends TextResponse if (mb_strlen($this->_post['body']) < CommunityContent::REPLY_LENGTH_MIN || mb_strlen($this->_post['body']) > CommunityContent::REPLY_LENGTH_MAX) $this->generate404(Lang::main('textLength', [mb_strlen($this->_post['body']), CommunityContent::REPLY_LENGTH_MIN, CommunityContent::REPLY_LENGTH_MAX])); - if (!DB::Aowow()->query('INSERT INTO ?_comments (`userId`, `roles`, `body`, `date`, `replyTo`) VALUES (?d, ?d, ?, UNIX_TIMESTAMP(), ?d)', User::$id, User::$groups, $this->_post['body'], $this->_post['commentId'])) + if (!DB::Aowow()->qry('INSERT INTO ::comments (`userId`, `roles`, `body`, `date`, `replyTo`) VALUES (%i, %i, %s, UNIX_TIMESTAMP(), %i)', User::$id, User::$groups, $this->_post['body'], $this->_post['commentId'])) { trigger_error('CommentAddreplyResponse - write to db failed', E_USER_ERROR); $this->generate404(Lang::main('intError')); diff --git a/endpoints/comment/add.php b/endpoints/comment/add.php index 3cc8201e..78dffad6 100644 --- a/endpoints/comment/add.php +++ b/endpoints/comment/add.php @@ -30,7 +30,7 @@ class CommentAddResponse extends TextResponse // we now have a valid return target $idOrUrl = $this->_get['typeid']; if ($this->_get['type'] == Type::GUIDE) - if ($_ = DB::Aowow()->selectCell('SELECT `url` FROM ?_guides WHERE `id` = ?d', $this->_get['typeid'])) + if ($_ = DB::Aowow()->selectCell('SELECT `url` FROM ::guides WHERE `id` = %i', $this->_get['typeid'])) $idOrUrl = $_; $this->redirectTo = '?'.Type::getFileString($this->_get['type']).'='.$idOrUrl.'#comments'; @@ -57,16 +57,16 @@ class CommentAddResponse extends TextResponse return; } - if ($postId = DB::Aowow()->query('INSERT INTO ?_comments (`type`, `typeId`, `userId`, `roles`, `body`, `date`) VALUES (?d, ?d, ?d, ?d, ?, UNIX_TIMESTAMP())', $this->_get['type'], $this->_get['typeid'], User::$id, User::$groups, $this->_post['commentbody'])) + if ($postId = DB::Aowow()->qry('INSERT INTO ::comments (`type`, `typeId`, `userId`, `roles`, `body`, `date`) VALUES (%i, %i, %i, %i, %s, UNIX_TIMESTAMP())', $this->_get['type'], $this->_get['typeid'], User::$id, User::$groups, $this->_post['commentbody'])) { Util::gainSiteReputation(User::$id, SITEREP_ACTION_COMMENT, ['id' => $postId]); // every comment starts with a rating of +1 and i guess the simplest thing to do is create a db-entry with the system as owner - DB::Aowow()->query('INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, 0, 1)', RATING_COMMENT, $postId); + DB::Aowow()->qry('INSERT INTO ::user_ratings (`type`, `entry`, `userId`, `value`) VALUES (%i, %i, 0, 1)', RATING_COMMENT, $postId); // flag target with hasComment if ($tbl = Type::getClassAttrib($this->_get['type'], 'dataTable')) - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $this->_get['typeid']); + DB::Aowow()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_COMMENT, $this->_get['typeid']); return; } diff --git a/endpoints/comment/delete-reply.php b/endpoints/comment/delete-reply.php index 52b24b27..096a62dd 100644 --- a/endpoints/comment/delete-reply.php +++ b/endpoints/comment/delete-reply.php @@ -23,16 +23,13 @@ class CommentDeletereplyResponse extends TextResponse $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); } - // flag as deleted (unset sticky (can a reply even be sticky?) - $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d | ?d, `deleteUserId` = ?d, `deleteDate` = UNIX_TIMESTAMP() WHERE `id` = ?d { AND `userId` = ?d }', - CC_FLAG_STICKY, CC_FLAG_DELETED, - User::$id, - $this->_post['id'], - User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id - ); + $where = [['`id` = %i', $this->_post['id']]]; + if (!User::isInGroup(U_GROUP_MODERATOR)) + $where[] = ['`userId` = %i', User::$id]; - if ($ok) - DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d', RATING_COMMENT, $this->_post['id']); + // flag as deleted + if (DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` | %i, `deleteUserId` = %i, `deleteDate` = UNIX_TIMESTAMP() WHERE %and', CC_FLAG_DELETED, User::$id, $where)) + DB::Aowow()->qry('DELETE FROM ::user_ratings WHERE `type` = %i AND `entry` = %i', RATING_COMMENT, $this->_post['id']); else { trigger_error('CommentDeletereplyResponse - deleting reply #'.$this->_post['id'].' by user #'.User::$id.' from db failed', E_USER_ERROR); diff --git a/endpoints/comment/delete.php b/endpoints/comment/delete.php index 3893c308..582a0b01 100644 --- a/endpoints/comment/delete.php +++ b/endpoints/comment/delete.php @@ -24,24 +24,21 @@ class CommentDeleteResponse extends TextResponse } // in theory, there is a username passed alongside if executed from userpage... lets just use the current user (see user.js) - $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d, `deleteUserId` = ?d, `deleteDate` = UNIX_TIMESTAMP() WHERE `id` IN (?a) { AND `userId` = ?d }', - CC_FLAG_DELETED, - User::$id, - $this->_post['id'], - User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id - ); + $where = [['`id` IN %in', $this->_post['id']]]; + if (!User::isInGroup(U_GROUP_MODERATOR)) + $where[] = ['`userId` = %i', User::$id]; - // unflag subject: hasComment - if ($ok) + // flag as deleted; unflag subject: hasComment + if (DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` | %i, `deleteUserId` = %i, `deleteDate` = UNIX_TIMESTAMP() WHERE %and', CC_FLAG_DELETED, User::$id, $where)) { - $coInfo = DB::Aowow()->select( - 'SELECT IF(BIT_OR(~b.`flags`) & ?d, 1, 0) AS "0", b.`type` AS "1", b.`typeId` AS "2" FROM ?_comments a JOIN ?_comments b ON a.`type` = b.`type` AND a.`typeId` = b.`typeId` WHERE a.`id` IN (?a) GROUP BY b.`type`, b.`typeId`', + $coInfo = DB::Aowow()->selectAssoc( + 'SELECT IF(BIT_OR(~b.`flags`) & %i, 1, 0) AS "0", b.`type` AS "1", b.`typeId` AS "2" FROM ::comments a JOIN ::comments b ON a.`type` = b.`type` AND a.`typeId` = b.`typeId` WHERE a.`id` IN %in GROUP BY b.`type`, b.`typeId`', CC_FLAG_DELETED, $this->_post['id'] ); foreach ($coInfo as [$hasMore, $type, $typeId]) if (!$hasMore && ($tbl = Type::getClassAttrib($type, 'dataTable'))) - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` & ~?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $typeId); + DB::Aowow()->qry('UPDATE %n SET `cuFlags` = `cuFlags` & ~%i WHERE `id` = %i', $tbl, CUSTOM_HAS_COMMENT, $typeId); return; } diff --git a/endpoints/comment/detach-reply.php b/endpoints/comment/detach-reply.php index 6512a2fc..7c79f29a 100644 --- a/endpoints/comment/detach-reply.php +++ b/endpoints/comment/detach-reply.php @@ -23,7 +23,7 @@ class CommentDetachreplyResponse extends TextResponse $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); } - DB::Aowow()->query('UPDATE ?_comments c1, ?_comments c2 SET c1.`replyTo` = 0, c1.`type` = c2.`type`, c1.`typeId` = c2.`typeId` WHERE c1.`replyTo` = c2.`id` AND c1.`id` = ?d', $this->_post['id']); + DB::Aowow()->qry('UPDATE ::comments c1, ::comments c2 SET c1.`replyTo` = 0, c1.`type` = c2.`type`, c1.`typeId` = c2.`typeId` WHERE c1.`replyTo` = c2.`id` AND c1.`id` = %i', $this->_post['id']); } } diff --git a/endpoints/comment/downvote-reply.php b/endpoints/comment/downvote-reply.php index 541a0d9b..cd035abc 100644 --- a/endpoints/comment/downvote-reply.php +++ b/endpoints/comment/downvote-reply.php @@ -26,7 +26,7 @@ class CommentDownvotereplyResponse extends TextResponse if (!User::canDownvote()) $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'cannot downvote' : ''); - $comment = DB::Aowow()->selectRow('SELECT `userId`, IF(`flags` & ?d, 1, 0) AS "deleted" FROM ?_comments WHERE `id` = ?d', CC_FLAG_DELETED, $this->_post['id']); + $comment = DB::Aowow()->selectRow('SELECT `userId`, IF(`flags` & %i, 1, 0) AS "deleted" FROM ::comments WHERE `id` = %i', CC_FLAG_DELETED, $this->_post['id']); if (!$comment) { trigger_error('CommentDownvotereplyResponse - comment #'.$this->_post['id'].' not found in db', E_USER_ERROR); @@ -39,15 +39,9 @@ class CommentDownvotereplyResponse extends TextResponse if ($comment['deleted']) $this->generate404('LANG.votedeleted_tip'); - $ok = DB::Aowow()->query( - 'INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', - RATING_COMMENT, - $this->_post['id'], - User::$id, - User::canSupervote() ? -2 : -1 - ); - - if (!$ok) + if (is_null(DB::Aowow()->qry('INSERT INTO ::user_ratings (`type`, `entry`, `userId`, `value`) VALUES (%i, %i, %i, %i)', + RATING_COMMENT, $this->_post['id'], User::$id, User::canSupervote() ? -2 : -1 + ))) { trigger_error('CommentDownvotereplyResponse - write to db failed', E_USER_ERROR); $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'write to db failed' : ''); diff --git a/endpoints/comment/edit-reply.php b/endpoints/comment/edit-reply.php index fa56e999..7a6aed50 100644 --- a/endpoints/comment/edit-reply.php +++ b/endpoints/comment/edit-reply.php @@ -26,7 +26,7 @@ class CommentEditreplyResponse extends TextResponse $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); } - $ownerId = DB::Aowow()->selectCell('SELECT `userId` FROM ?_comments WHERE `id` = ?d AND `replyTo` = ?d', $this->_post['replyId'], $this->_post['commentId']); + $ownerId = DB::Aowow()->selectCell('SELECT `userId` FROM ::comments WHERE `id` = %i AND `replyTo` = %i', $this->_post['replyId'], $this->_post['commentId']); if (!User::canReply() || (User::$id != $ownerId && !User::isInGroup(U_GROUP_MODERATOR))) $this->generate404(Lang::main('cannotComment')); @@ -48,8 +48,11 @@ class CommentEditreplyResponse extends TextResponse if (User::$id == $ownerId) $update['roles'] = User::$groups; - if (!DB::Aowow()->query('UPDATE ?_comments SET `editCount` = `editCount` + 1, ?a WHERE `id` = ?d AND `replyTo` = ?d { AND `userId` = ?d }', - $update, $this->_post['replyId'], $this->_post['commentId'], User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id)) + $where = [['`id` = %i', $this->_post['replyId']], ['`replyTo` = %i', $this->_post['commentId']]]; + if (!User::isInGroup(U_GROUP_MODERATOR)) + $where[] = ['`userId` = %i', User::$id]; + + if (!DB::Aowow()->qry('UPDATE ::comments SET `editCount` = `editCount` + 1, %a WHERE %and', $update, $where)) { trigger_error('CommentEditreplyResponse - write to db failed', E_USER_ERROR); $this->generate404(Lang::main('intError')); diff --git a/endpoints/comment/edit.php b/endpoints/comment/edit.php index 4969990d..a3a2e3cb 100644 --- a/endpoints/comment/edit.php +++ b/endpoints/comment/edit.php @@ -26,7 +26,7 @@ class CommentEditResponse extends TextResponse return; } - $ownerId = DB::Aowow()->selectCell('SELECT `userId` FROM ?_comments WHERE `id` = ?d', $this->_get['id']); + $ownerId = DB::Aowow()->selectCell('SELECT `userId` FROM ::comments WHERE `id` = %i', $this->_get['id']); if (!User::canComment() || (User::$id != $ownerId && !User::isInGroup(U_GROUP_MODERATOR))) { @@ -56,7 +56,7 @@ class CommentEditResponse extends TextResponse $update['responseRoles'] = User::$groups; } - DB::Aowow()->query('UPDATE ?_comments SET `editCount` = `editCount` + 1, ?a WHERE `id` = ?d', $update, $this->_get['id']); + DB::Aowow()->qry('UPDATE ::comments SET `editCount` = `editCount` + 1, %a WHERE `id` = %i', $update, $this->_get['id']); } } diff --git a/endpoints/comment/flag-reply.php b/endpoints/comment/flag-reply.php index 91044e50..55589b45 100644 --- a/endpoints/comment/flag-reply.php +++ b/endpoints/comment/flag-reply.php @@ -23,7 +23,7 @@ class CommentFlagreplyResponse extends TextResponse $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'request malformed' : ''); } - $replyOwner = DB::Aowow()->selectCell('SELECT `userId` FROM ?_commments WHERE `id` = ?d', $this->_post['id']); + $replyOwner = DB::Aowow()->selectCell('SELECT `userId` FROM ::commments WHERE `id` = %i', $this->_post['id']); if (!$replyOwner) { trigger_error('CommentFlagreplyResponse - reply not found', E_USER_ERROR); @@ -38,7 +38,7 @@ class CommentFlagreplyResponse extends TextResponse if (!$report->create('Report Reply Button Click')) $this->generate404('LANG.ct_resp_error'.$report->getError()); else if (count($report->getSimilar()) >= CommunityContent::REPORT_THRESHOLD_AUTO_DELETE) - DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d WHERE `id` = ?d', CC_FLAG_DELETED, $this->_post['id']); + DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` | %i WHERE `id` = %i', CC_FLAG_DELETED, $this->_post['id']); } } diff --git a/endpoints/comment/out-of-date.php b/endpoints/comment/out-of-date.php index d1c35432..70cb3dfd 100644 --- a/endpoints/comment/out-of-date.php +++ b/endpoints/comment/out-of-date.php @@ -30,9 +30,9 @@ class CommentOutofdateResponse extends TextResponse if (User::isInGroup(U_GROUP_MODERATOR)) // directly mark as outdated { if (!$this->_post['remove']) - $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id']); + $ok = DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` | %i WHERE `id` = %i', CC_FLAG_OUTDATED, $this->_post['id']); else - $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id']); + $ok = DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` & ~%i WHERE `id` = %i', CC_FLAG_OUTDATED, $this->_post['id']); } else // try to report as outdated { @@ -41,7 +41,7 @@ class CommentOutofdateResponse extends TextResponse $this->result = Lang::main('intError'); if (count($report->getSimilar()) >= CommunityContent::REPORT_THRESHOLD_AUTO_OUT_OF_DATE) - $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d WHERE `id` = ?d', CC_FLAG_OUTDATED, $this->_post['id']); + $ok = DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` | %i WHERE `id` = %i', CC_FLAG_OUTDATED, $this->_post['id']); } if (!$ok) diff --git a/endpoints/comment/rating.php b/endpoints/comment/rating.php index bf69da13..d080e71e 100644 --- a/endpoints/comment/rating.php +++ b/endpoints/comment/rating.php @@ -21,7 +21,7 @@ class CommentRatingResponse extends TextResponse return; } - if ($votes = DB::Aowow()->selectRow('SELECT 1 AS "success", SUM(IF(`value` > 0, `value`, 0)) AS "up", SUM(IF(`value` < 0, -`value`, 0)) AS "down" FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` <> 0 GROUP BY `entry`', RATING_COMMENT, $this->_get['id'])) + if ($votes = DB::Aowow()->selectRow('SELECT 1 AS "success", SUM(IF(`value` > 0, `value`, 0)) AS "up", SUM(IF(`value` < 0, -`value`, 0)) AS "down" FROM ::user_ratings WHERE `type` = %i AND `entry` = %i AND `userId` <> 0 GROUP BY `entry`', RATING_COMMENT, $this->_get['id'])) $this->result = Util::toJSON($votes); else $this->result = Util::toJSON(['success' => 1, 'up' => 0, 'down' => 0]); diff --git a/endpoints/comment/sticky.php b/endpoints/comment/sticky.php index f2ecd1e9..16fcf3a7 100644 --- a/endpoints/comment/sticky.php +++ b/endpoints/comment/sticky.php @@ -25,9 +25,9 @@ class CommentStickyResponse extends TextResponse } if ($this->_post['sticky']) - DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` | ?d WHERE `id` = ?d', CC_FLAG_STICKY, $this->_post['id']); + DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` | %i WHERE `id` = %i', CC_FLAG_STICKY, $this->_post['id']); else - DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` = ?d', CC_FLAG_STICKY, $this->_post['id']); + DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` & ~%i WHERE `id` = %i', CC_FLAG_STICKY, $this->_post['id']); } } diff --git a/endpoints/comment/undelete.php b/endpoints/comment/undelete.php index eb56df49..1d21a6a1 100644 --- a/endpoints/comment/undelete.php +++ b/endpoints/comment/undelete.php @@ -24,19 +24,20 @@ class CommentUndeleteResponse extends TextResponse } // in theory, there is a username passed alongside if executed from userpage... lets just use the current user (see user.js) - $ok = DB::Aowow()->query('UPDATE ?_comments SET `flags` = `flags` & ~?d WHERE `id` IN (?a) { AND `userId` = `deleteUserId` AND `deleteUserId` = ?d }', - CC_FLAG_DELETED, - $this->_post['id'], - User::isInGroup(U_GROUP_MODERATOR) ? DBSIMPLE_SKIP : User::$id - ); + $where = [['`id` IN %in', $this->_post['id']]]; + if (!User::isInGroup(U_GROUP_MODERATOR)) + { + $where[] = ['`deleteUserId` = `userId']; + $where[] = ['`deleteUserId` = %i', User::$id]; + } // unflag subject: hasComment - if ($ok) + if (DB::Aowow()->qry('UPDATE ::comments SET `flags` = `flags` & ~%i WHERE %and', CC_FLAG_DELETED, $where)) { - $coInfo = DB::Aowow()->select('SELECT `type` AS "0", `typeId` AS "1" FROM ?_comments WHERE `id` IN (?a) GROUP BY `type`, `typeId`', $this->_post['id']); + $coInfo = DB::Aowow()->selectAssoc('SELECT `type` AS "0", `typeId` AS "1" FROM ::comments WHERE `id` IN %in GROUP BY `type`, `typeId`', $this->_post['id']); foreach ($coInfo as [$type, $typeId]) if ($tbl = Type::getClassAttrib($type, 'dataTable')) - DB::Aowow()->query('UPDATE ?# SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', $tbl, CUSTOM_HAS_COMMENT, $typeId); + DB::Aowow()->qry('UPDATE %n SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', $tbl, CUSTOM_HAS_COMMENT, $typeId); return; } diff --git a/endpoints/comment/upvote-reply.php b/endpoints/comment/upvote-reply.php index d40b5b75..f6e84a34 100644 --- a/endpoints/comment/upvote-reply.php +++ b/endpoints/comment/upvote-reply.php @@ -26,7 +26,7 @@ class CommentUpvotereplyResponse extends TextResponse if (!User::canUpvote()) $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'cannot upvote' : ''); - $comment = DB::Aowow()->selectRow('SELECT `userId`, IF(`flags` & ?d, 1, 0) AS "deleted" FROM ?_comments WHERE `id` = ?d', CC_FLAG_DELETED, $this->_post['id']); + $comment = DB::Aowow()->selectRow('SELECT `userId`, IF(`flags` & %i, 1, 0) AS "deleted" FROM ::comments WHERE `id` = %i', CC_FLAG_DELETED, $this->_post['id']); if (!$comment) { trigger_error('CommentUpvotereplyResponse - comment #'.$this->_post['id'].' not found in db', E_USER_ERROR); @@ -39,15 +39,9 @@ class CommentUpvotereplyResponse extends TextResponse if ($comment['deleted']) $this->generate404('LANG.votedeleted_tip'); - $ok = DB::Aowow()->query( - 'INSERT INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', - RATING_COMMENT, - $this->_post['id'], - User::$id, - User::canSupervote() ? 2 : 1 - ); - - if (!$ok) + if (is_null(DB::Aowow()->qry('INSERT INTO ::user_ratings (`type`, `entry`, `userId`, `value`) VALUES (%i, %i, %i, %i)', + RATING_COMMENT, $this->_post['id'], User::$id, User::canSupervote() ? 2 : 1 + ))) { trigger_error('CommentUpvotereplyResponse - write to db failed', E_USER_ERROR); $this->generate404(User::isInGroup(U_GROUP_STAFF) ? 'write to db failed' : ''); diff --git a/endpoints/comment/vote.php b/endpoints/comment/vote.php index 7f459253..1bf3b1ff 100644 --- a/endpoints/comment/vote.php +++ b/endpoints/comment/vote.php @@ -32,7 +32,7 @@ class CommentVoteResponse extends TextResponse } $target = DB::Aowow()->selectRow( - 'SELECT c.`userId` AS "owner", ur.`value`, IF(c.`flags` & ?d, 1, 0) AS "deleted" FROM ?_comments c LEFT JOIN ?_user_ratings ur ON ur.`type` = ?d AND ur.`entry` = c.id AND ur.`userId` = ?d WHERE c.id = ?d', + 'SELECT c.`userId` AS "owner", ur.`value`, IF(c.`flags` & %i, 1, 0) AS "deleted" FROM ::comments c LEFT JOIN ::user_ratings ur ON ur.`type` = %i AND ur.`entry` = c.id AND ur.`userId` = %i WHERE c.id = %i', CC_FLAG_DELETED, RATING_COMMENT, User::$id, $this->_get['id'] ); if (!$target) @@ -62,9 +62,9 @@ class CommentVoteResponse extends TextResponse $ok = false; // old and new have same sign; undo vote (user may have gained/lost access to superVote in the meantime) if ($target['value'] && ($target['value'] < 0) == ($val < 0)) - $ok = DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` = ?d', RATING_COMMENT, $this->_get['id'], User::$id); + $ok = DB::Aowow()->qry('DELETE FROM ::user_ratings WHERE `type` = %i AND `entry` = %i AND `userId` = %i', RATING_COMMENT, $this->_get['id'], User::$id); else // replace, because we may be overwriting an old, opposing vote - if ($ok = DB::Aowow()->query('REPLACE INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', RATING_COMMENT, $this->_get['id'], User::$id, $val)) + if ($ok = DB::Aowow()->qry('REPLACE INTO ::user_ratings (`type`, `entry`, `userId`, `value`) VALUES (%i, %i, %i, %i)', RATING_COMMENT, $this->_get['id'], User::$id, $val)) User::decrementDailyVotes(); // do not refund retracted votes! if ($ok) diff --git a/endpoints/compare/compare.php b/endpoints/compare/compare.php index 5268795d..67604b4f 100644 --- a/endpoints/compare/compare.php +++ b/endpoints/compare/compare.php @@ -34,9 +34,9 @@ class CompareBaseResponse extends TemplateResponse private string $compareString = ''; - public function __construct($pageParam) + public function __construct($rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); // prefer GET over COOKIE if ($this->_get['compare']) @@ -106,7 +106,7 @@ class CompareBaseResponse extends TemplateResponse protected static function checkCompareString(string $val) : string { $val = urldecode($val); - if (preg_match('/[^\d\.:;]/', $val)) + if (preg_match('/[^-?\d\.:;]/', $val)) return ''; return $val; diff --git a/endpoints/contactus/contactus.php b/endpoints/contactus/contactus.php index 196ad9c6..ace3fa02 100644 --- a/endpoints/contactus/contactus.php +++ b/endpoints/contactus/contactus.php @@ -9,15 +9,15 @@ if (!defined('AOWOW_REVISION')) class ContactusBaseResponse extends TextResponse { protected array $expectedPOST = array( - 'mode' => ['filter' => FILTER_VALIDATE_INT ], - 'reason' => ['filter' => FILTER_VALIDATE_INT ], - 'ua' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], - 'appname' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']], - 'page' => ['filter' => FILTER_SANITIZE_URL ], - 'desc' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob']], - 'id' => ['filter' => FILTER_VALIDATE_INT ], - 'relatedurl' => ['filter' => FILTER_SANITIZE_URL ], - 'email' => ['filter' => FILTER_SANITIZE_EMAIL ] + 'mode' => ['filter' => FILTER_VALIDATE_INT ], + 'reason' => ['filter' => FILTER_VALIDATE_INT ], + 'ua' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'appname' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine'] ], + 'page' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']], + 'desc' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ], + 'id' => ['filter' => FILTER_VALIDATE_INT ], + 'relatedurl' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/']], + 'email' => ['filter' => FILTER_SANITIZE_EMAIL ] ); /* responses diff --git a/endpoints/cookie/cookie.php b/endpoints/cookie/cookie.php index e62793e8..5b5f9af0 100644 --- a/endpoints/cookie/cookie.php +++ b/endpoints/cookie/cookie.php @@ -32,7 +32,7 @@ class CookieBaseResponse extends TextResponse { if (!$this->param && $this->_get['purge']) { - if (User::$id && DB::Aowow()->query('UPDATE ?_account_cookies SET `data` = "purged" WHERE `userId` = ?d AND `name` LIKE "announcement-%"', User::$id) !== null) + if (User::$id && DB::Aowow()->qry('UPDATE ::account_cookies SET `data` = "purged" WHERE `userId` = %i AND `name` LIKE "announcement-%"', User::$id) !== null) $this->result = 0; return; @@ -44,7 +44,7 @@ class CookieBaseResponse extends TextResponse return; } - if (DB::Aowow()->query('REPLACE INTO ?_account_cookies VALUES (?d, ?, ?)', User::$id, $this->param, $this->_get[$this->param])) + if (DB::Aowow()->qry('REPLACE INTO ::account_cookies VALUES (%i, %s, %s)', User::$id, $this->param, $this->_get[$this->param])) $this->result = 0; else trigger_error('CookieBaseResponse - write to db failed', E_USER_ERROR); diff --git a/endpoints/currencies/currencies.php b/endpoints/currencies/currencies.php index 0a5aec25..bc07dbca 100644 --- a/endpoints/currencies/currencies.php +++ b/endpoints/currencies/currencies.php @@ -11,7 +11,7 @@ class CurrenciesBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::CURRENCY; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'currencies'; @@ -20,11 +20,11 @@ class CurrenciesBaseResponse extends TemplateResponse implements ICache protected array $validCats = [1, 2, 3, 22]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void diff --git a/endpoints/currency/currency.php b/endpoints/currency/currency.php index 31bc6823..92a761a7 100644 --- a/endpoints/currency/currency.php +++ b/endpoints/currency/currency.php @@ -10,7 +10,7 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'currency'; @@ -72,13 +72,20 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache if ($_ = $this->subject->getField('cap')) $infobox[] = Lang::currency('cap').Lang::nf($_); + // id + $infobox[] = Lang::currency('id') . $this->typeId; + // icon if ($_ = $this->subject->getField('iconId')) { - $infobox[] = Util::ucFirst(lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; $this->extendGlobalIds(Type::ICON, $_); } + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); @@ -110,9 +117,9 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache if ($this->typeId != CURRENCY_HONOR_POINTS && $this->typeId != CURRENCY_ARENA_POINTS) { // tabs: this currency is contained in.. - $lootTabs = new Loot(); + $lootTabs = new LootByItem($_relItemId); - if ($lootTabs->getByItem($_relItemId)) + if ($lootTabs->getByItem()) { $this->extendGlobalData($lootTabs->jsGlobals); @@ -121,6 +128,15 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache if ($template == 'npc' || $template == 'object') $this->addDataLoader('zones'); + if ($template != 'quest') + { + foreach ($tabData['data'] as &$row) + if (!empty($row['stack'])) + $row['currency'] = [[$this->typeId, $row['stack'][0]]]; + + $tabData['extraCols'][] = '$Listview.extraCols.currency'; + } + $this->lvTabs->addListviewTab(new Listview($tabData, $template)); } } @@ -182,10 +198,10 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache } } - // tab: created by (spell) [for items its handled in Loot::getByContainer()] + // tab: created by (spell) [for items its handled in LootByItem] if ($this->typeId == CURRENCY_HONOR_POINTS) { - $createdBy = new SpellList(array(['effect1Id', SPELL_EFFECT_ADD_HONOR], ['effect2Id', SPELL_EFFECT_ADD_HONOR], ['effect3Id', SPELL_EFFECT_ADD_HONOR], 'OR')); + $createdBy = new SpellList(array(['effect1Id', SPELL_EFFECT_ADD_HONOR], ['effect2Id', SPELL_EFFECT_ADD_HONOR], ['effect3Id', SPELL_EFFECT_ADD_HONOR], DB::OR)); if (!$createdBy->error) { $this->extendGlobalData($createdBy->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); @@ -221,8 +237,8 @@ class CurrencyBaseResponse extends TemplateResponse implements ICache if (!$n && !is_null(ItemListFilter::getCriteriaIndex(158, $_relItemId))) $n = '?items&filter=cr=158;crs='.$_relItemId.';crv=0'; - $xCosts = DB::Aowow()->selectCol('SELECT `id` FROM ?_itemextendedcost WHERE '.$w); - $boughtBy = $xCosts ? DB::World()->selectCol('SELECT `item` FROM npc_vendor WHERE `extendedCost` IN (?a) UNION SELECT `item` FROM game_event_npc_vendor WHERE `extendedCost` IN (?a)', $xCosts, $xCosts) : []; + $xCosts = DB::Aowow()->selectCol('SELECT `id` FROM ::itemextendedcost WHERE '.$w); + $boughtBy = $xCosts ? DB::World()->selectCol('SELECT `item` FROM npc_vendor WHERE `extendedCost` IN %in UNION SELECT `item` FROM game_event_npc_vendor WHERE `extendedCost` IN %in', $xCosts, $xCosts) : []; if ($boughtBy) { $boughtBy = new ItemList(array(['id', $boughtBy])); diff --git a/endpoints/data/data.php b/endpoints/data/data.php index 174e24c2..8b38397b 100644 --- a/endpoints/data/data.php +++ b/endpoints/data/data.php @@ -9,7 +9,7 @@ if (!defined('AOWOW_REVISION')) class DataBaseResponse extends TextResponse { protected array $expectedGET = array( - 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFrom'] ], + 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkLocale' ]], 't' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine' ]], 'catg' => ['filter' => FILTER_VALIDATE_INT ], 'skill' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkSkill' ]], @@ -17,9 +17,9 @@ class DataBaseResponse extends TextResponse 'callback' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCallback' ]] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if ($this->_get['locale']?->validate()) Lang::load($this->_get['locale']); @@ -31,7 +31,7 @@ class DataBaseResponse extends TextResponse foreach ($this->params as $set) { // requires valid token to hinder automated access - if ($set != 'item-scaling' && (!$this->_get['t'] || empty($_SESSION['dataKey']) || $this->_get['t'] != $_SESSION['dataKey'])) + if ($set != 'item-scaling' && $set != 'spell-scaling' && (!$this->_get['t'] || empty($_SESSION['dataKey']) || $this->_get['t'] != $_SESSION['dataKey'])) { trigger_error('DataBaseResponse::generate - session data key empty or expired', E_USER_ERROR); continue; @@ -54,6 +54,7 @@ class DataBaseResponse extends TextResponse 'quick-excludes', 'weight-presets', 'item-scaling', + 'spell-scaling', 'realms', 'statistics' => $this->loadAgnosticFile($set), // localized diff --git a/endpoints/emote/emote.php b/endpoints/emote/emote.php index d5e16070..8a73cfdd 100644 --- a/endpoints/emote/emote.php +++ b/endpoints/emote/emote.php @@ -10,7 +10,7 @@ class EmoteBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'emote'; @@ -94,6 +94,9 @@ class EmoteBaseResponse extends TemplateResponse implements ICache } } + // id + $infobox[] = Lang::emote('id') . $this->typeId; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); @@ -106,7 +109,7 @@ class EmoteBaseResponse extends TemplateResponse implements ICache if ($this->subject->getField('cuFlags') & EMOTE_CU_MISSING_CMD) $text .= Lang::emote('noCommand').'[br][br]'; - else if ($aliasses = DB::Aowow()->selectCol('SELECT `command` FROM ?_emotes_aliasses WHERE `id` = ?d AND `locales` & ?d', $this->typeId, 1 << Lang::getLocale()->value)) + else if ($aliasses = DB::Aowow()->selectCol('SELECT `command` FROM ::emotes_aliasses WHERE `id` = %i AND `locales` & %i', $this->typeId, 1 << Lang::getLocale()->value)) { $text .= '[h3]'.Lang::emote('aliases').'[/h3][ul]'; foreach ($aliasses as $a) @@ -181,13 +184,12 @@ class EmoteBaseResponse extends TemplateResponse implements ICache } // tab: sound - $ems = DB::Aowow()->select( + $ems = DB::Aowow()->selectAssoc( 'SELECT `soundId` AS ARRAY_KEY, BIT_OR(1 << (`raceId` - 1)) AS "raceMask", BIT_OR(1 << (`gender` - 1)) AS "gender" - FROM ?_emotes_sounds - WHERE `emoteId` = ?d { OR -`emoteId` = ?d } + FROM ::emotes_sounds + WHERE %if', $this->typeId < 0, '-`emoteId` = %i OR', $this->subject->getField('parentEmote'), '%end `emoteId` = %i GROUP BY `soundId`', $this->typeId, - $this->typeId < 0 ? $this->subject->getField('parentEmote') : DBSIMPLE_SKIP ); if ($ems) diff --git a/endpoints/emotes/emotes.php b/endpoints/emotes/emotes.php index f3103995..de77397a 100644 --- a/endpoints/emotes/emotes.php +++ b/endpoints/emotes/emotes.php @@ -10,7 +10,7 @@ class EmotesBaseResponse extends TemplateResponse implements ICache { use TrListPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected int $type = Type::EMOTE; protected string $template = 'list-page-generic'; @@ -18,11 +18,11 @@ class EmotesBaseResponse extends TemplateResponse implements ICache protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 100]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -41,7 +41,7 @@ class EmotesBaseResponse extends TemplateResponse implements ICache /* Main Content */ /****************/ - $cnd = [Cfg::get('SQL_LIMIT_NONE')]; + $cnd = []; // don't limit, for we have no filter or category if (!User::isInGroup(U_GROUP_STAFF)) $cnd[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; diff --git a/endpoints/enchantment/enchantment.php b/endpoints/enchantment/enchantment.php index ab874512..98ae613f 100644 --- a/endpoints/enchantment/enchantment.php +++ b/endpoints/enchantment/enchantment.php @@ -10,7 +10,7 @@ class EnchantmentBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'enchantment'; protected string $pageName = 'enchantment'; @@ -85,6 +85,13 @@ class EnchantmentBaseResponse extends TemplateResponse implements ICache $infobox[] = $foo; } + // id + $infobox[] = Lang::enchantment('id') . $this->typeId; + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); @@ -187,10 +194,10 @@ class EnchantmentBaseResponse extends TemplateResponse implements ICache // used by spell // used by useItem $cnd = array( - 'OR', - ['AND', ['effect1Id', SpellList::EFFECTS_ENCHANTMENT], ['effect1MiscValue', $this->typeId]], - ['AND', ['effect2Id', SpellList::EFFECTS_ENCHANTMENT], ['effect2MiscValue', $this->typeId]], - ['AND', ['effect3Id', SpellList::EFFECTS_ENCHANTMENT], ['effect3MiscValue', $this->typeId]], + DB::OR, + [DB::AND, ['effect1Id', SpellList::EFFECTS_ENCHANTMENT], ['effect1MiscValue', $this->typeId]], + [DB::AND, ['effect2Id', SpellList::EFFECTS_ENCHANTMENT], ['effect2MiscValue', $this->typeId]], + [DB::AND, ['effect3Id', SpellList::EFFECTS_ENCHANTMENT], ['effect3MiscValue', $this->typeId]], ); $spellList = new SpellList($cnd); if (!$spellList->error) @@ -200,12 +207,12 @@ class EnchantmentBaseResponse extends TemplateResponse implements ICache $spellIds = $spellList->getFoundIDs(); $conditions = array( - 'OR', - ['AND', ['spellTrigger1', [SPELL_TRIGGER_USE, SPELL_TRIGGER_USE_NODELAY]], ['spellId1', $spellIds]], - ['AND', ['spellTrigger2', [SPELL_TRIGGER_USE, SPELL_TRIGGER_USE_NODELAY]], ['spellId2', $spellIds]], - ['AND', ['spellTrigger3', [SPELL_TRIGGER_USE, SPELL_TRIGGER_USE_NODELAY]], ['spellId3', $spellIds]], - ['AND', ['spellTrigger4', [SPELL_TRIGGER_USE, SPELL_TRIGGER_USE_NODELAY]], ['spellId4', $spellIds]], - ['AND', ['spellTrigger5', [SPELL_TRIGGER_USE, SPELL_TRIGGER_USE_NODELAY]], ['spellId5', $spellIds]] + DB::OR, + [DB::AND, ['spellTrigger1', [SPELL_TRIGGER_USE, SPELL_TRIGGER_USE_NODELAY]], ['spellId1', $spellIds]], + [DB::AND, ['spellTrigger2', [SPELL_TRIGGER_USE, SPELL_TRIGGER_USE_NODELAY]], ['spellId2', $spellIds]], + [DB::AND, ['spellTrigger3', [SPELL_TRIGGER_USE, SPELL_TRIGGER_USE_NODELAY]], ['spellId3', $spellIds]], + [DB::AND, ['spellTrigger4', [SPELL_TRIGGER_USE, SPELL_TRIGGER_USE_NODELAY]], ['spellId4', $spellIds]], + [DB::AND, ['spellTrigger5', [SPELL_TRIGGER_USE, SPELL_TRIGGER_USE_NODELAY]], ['spellId5', $spellIds]] ); $ubItems = new ItemList($conditions); @@ -252,19 +259,19 @@ class EnchantmentBaseResponse extends TemplateResponse implements ICache } // used by randomAttrItem - $ire = DB::Aowow()->select( - 'SELECT *, ABS(`id`) AS ARRAY_KEY FROM ?_itemrandomenchant WHERE `enchantId1` = ?d OR `enchantId2` = ?d OR `enchantId3` = ?d OR `enchantId4` = ?d OR `enchantId5` = ?d', + $ire = DB::Aowow()->selectAssoc( + 'SELECT *, ABS(`id`) AS ARRAY_KEY FROM ::itemrandomenchant WHERE `enchantId1` = %i OR `enchantId2` = %i OR `enchantId3` = %i OR `enchantId4` = %i OR `enchantId5` = %i', $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId ); if ($ire) { - if ($iet = DB::World()->select('SELECT `entry` AS ARRAY_KEY, `ench`, `chance` FROM item_enchantment_template WHERE `ench` IN (?a)', array_keys($ire))) + if ($iet = DB::World()->selectAssoc('SELECT `entry` AS ARRAY_KEY, `ench`, `chance` FROM item_enchantment_template WHERE `ench` IN %in', array_keys($ire))) { $randIds = []; // transform back to signed format foreach ($iet as $tplId => $data) $randIds[$ire[$data['ench']]['id'] > 0 ? $tplId : -$tplId] = $ire[$data['ench']]['id']; - $randItems = new ItemList(array(Cfg::get('SQL_LIMIT_NONE'), ['randomEnchant', array_keys($randIds)])); + $randItems = new ItemList(array(['randomEnchant', array_keys($randIds)])); if (!$randItems->error) { $data = $randItems->getListviewData(); diff --git a/endpoints/enchantments/enchantments.php b/endpoints/enchantments/enchantments.php index 1d35f2b2..3dfbb79a 100644 --- a/endpoints/enchantments/enchantments.php +++ b/endpoints/enchantments/enchantments.php @@ -11,7 +11,7 @@ class EnchantmentsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::ENCHANTMENT; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'enchantments'; protected string $pageName = 'enchantments'; @@ -24,17 +24,25 @@ class EnchantmentsBaseResponse extends TemplateResponse implements ICache ); protected array $validCats = [1, 2, 3, 4, 5, 6, 7, 8]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); if ($this->category) $this->forward('?enchantments&filter=ty='.$this->category[0]); - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + if ($this->category) + $this->subCat = '='.implode('.', $this->category); + $this->filter = new EnchantmentListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -42,18 +50,13 @@ class EnchantmentsBaseResponse extends TemplateResponse implements ICache { $this->h1 = Util::ucFirst(Lang::game('enchantments')); - $this->filter->evalCriteria(); - - $conditions = []; - + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /**************/ /* Page Title */ @@ -111,9 +114,9 @@ class EnchantmentsBaseResponse extends TemplateResponse implements ICache if (!$ench->hasSetFields('skillLine')) $tabData['hiddenCols'] = ['skill']; - if ($ench->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($ench->getMatches() > Listview::DEFAULT_SIZE) { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_enchantmentsfound', $ench->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_enchantmentsfound', $ench->getMatches(), Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } diff --git a/endpoints/event/event.php b/endpoints/event/event.php index 5e1530b8..7796afc1 100644 --- a/endpoints/event/event.php +++ b/endpoints/event/event.php @@ -10,7 +10,7 @@ class EventBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'event'; @@ -88,9 +88,23 @@ class EventBaseResponse extends TemplateResponse implements ICache $infobox[] = Lang::npc('rank', 3).Lang::main('colon').'[npc='.$_.']'; } - // display internal id to staff - if (User::isInGroup(U_GROUP_STAFF)) - $infobox[] = 'Event-Id'.Lang::main('colon').$this->typeId; + // id + $infobox[] = Lang::event('id') . $this->typeId; + + // display holiday id to staff + if ($_holidayId && User::isInGroup(U_GROUP_STAFF)) + $infobox[] = 'Holiday ID'.Lang::main('colon').$_holidayId; + + // icon + if ($_ = $this->subject->getField('iconId')) + { + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; + $this->extendGlobalIds(Type::ICON, $_); + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); @@ -100,7 +114,7 @@ class EventBaseResponse extends TemplateResponse implements ICache /* Main Content */ /****************/ - // no entry in ?_articles? use default HolidayDescription + // no entry in ::articles? use default HolidayDescription if ($_holidayId && empty($this->article)) $this->article = new Markup($this->subject->getField('description', true), ['dbpage' => true]); @@ -123,7 +137,7 @@ class EventBaseResponse extends TemplateResponse implements ICache $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); // tab: npcs - if ($npcIds = DB::World()->selectCol('SELECT `id` AS ARRAY_KEY, IF(ec.`eventEntry` > 0, 1, 0) AS "added" FROM creature c, game_event_creature ec WHERE ec.`guid` = c.`guid` AND ABS(ec.`eventEntry`) = ?d', $this->typeId)) + if ($npcIds = DB::World()->selectCol('SELECT `id` AS ARRAY_KEY, IF(ec.`eventEntry` > 0, 1, 0) AS "added" FROM creature c, game_event_creature ec WHERE ec.`guid` = c.`guid` AND ABS(ec.`eventEntry`) = %i', $this->typeId)) { $creatures = new CreatureList(array(['id', array_keys($npcIds)])); if (!$creatures->error) @@ -143,7 +157,7 @@ class EventBaseResponse extends TemplateResponse implements ICache } // tab: objects - if ($objectIds = DB::World()->selectCol('SELECT `id` AS ARRAY_KEY, IF(eg.`eventEntry` > 0, 1, 0) AS "added" FROM gameobject g, game_event_gameobject eg WHERE eg.`guid` = g.`guid` AND ABS(eg.`eventEntry`) = ?d', $this->typeId)) + if ($objectIds = DB::World()->selectCol('SELECT `id` AS ARRAY_KEY, IF(eg.`eventEntry` > 0, 1, 0) AS "added" FROM gameobject g, game_event_gameobject eg WHERE eg.`guid` = g.`guid` AND ABS(eg.`eventEntry`) = %i', $this->typeId)) { $objects = new GameObjectList(array(['id', array_keys($objectIds)])); if (!$objects->error) @@ -163,6 +177,7 @@ class EventBaseResponse extends TemplateResponse implements ICache } // tab: achievements + $exclAcvs = []; if ($_ = $this->subject->getField('achievementCatOrId')) { $condition = $_ > 0 ? [['category', $_]] : [['id', -$_]]; @@ -176,6 +191,9 @@ class EventBaseResponse extends TemplateResponse implements ICache 'visibleCols' => ['category'] ); + // don't reuse for criteria-of tab + $exclAcvs = array_keys($tabData['data']); + if ($_holidayId && AchievementListFilter::getCriteriaIndex(11, $_holidayId)) $tabData['note'] = sprintf(Util::$filterResultString, '?achievements&filter=cr=11;crs='.$_holidayId.';crv=0'); @@ -186,37 +204,54 @@ class EventBaseResponse extends TemplateResponse implements ICache $itemCnd = []; if ($_holidayId) { - $itemCnd = array( - 'OR', - ['eventId', $this->typeId], // direct requirement on item - ); - - // tab: quests (by table, go & creature) - $quests = new QuestList(array(['eventId', $this->typeId])); - if (!$quests->error) + // tab: criteria-of + if ($extraCrt = DB::World()->selectCol('SELECT `criteria_id` FROM achievement_criteria_data WHERE `type` = %i AND `value1` = %i', ACHIEVEMENT_CRITERIA_DATA_TYPE_HOLIDAY, $_holidayId)) { - $this->extendGlobalData($quests->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + $condition = array(['ac.id', $extraCrt]); + if ($exclAcvs) + $condition[] = ['a.id', $exclAcvs, '!']; - $tabData = ['data'=> $quests->getListviewData()]; + $crtOf = new AchievementList($condition); + if (!$crtOf->error) + { + $this->extendGlobalData($crtOf->getJSGlobals()); - if (QuestListFilter::getCriteriaIndex(33, $_holidayId)) - $tabData['note'] = sprintf(Util::$filterResultString, '?quests&filter=cr=33;crs='.$_holidayId.';crv=0'); - - $this->lvTabs->addListviewTab(new Listview($tabData, QuestList::$brickFile)); - - $questItems = []; - foreach (array_column($quests->rewards, Type::ITEM) as $arr) - $questItems = array_merge($questItems, array_keys($arr)); - - foreach (array_column($quests->choices, Type::ITEM) as $arr) - $questItems = array_merge($questItems, array_keys($arr)); - - foreach (array_column($quests->requires, Type::ITEM) as $arr) - $questItems = array_merge($questItems, $arr); - - if ($questItems) - $itemCnd[] = ['id', $questItems]; + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $crtOf->getListviewData(), + 'name' => '$LANG.tab_criteriaof', + 'id' => 'criteria-of' + ), AchievementList::$brickFile)); + } } + + $itemCnd[] = ['eventId', $this->typeId]; // direct requirement on item + } + + // tab: quests (by table, go & creature) + $quests = new QuestList(array(['eventId', $this->typeId])); + if (!$quests->error) + { + $this->extendGlobalData($quests->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + + $tabData = ['data'=> $quests->getListviewData()]; + + if (QuestListFilter::getCriteriaIndex(33, $_holidayId)) + $tabData['note'] = sprintf(Util::$filterResultString, '?quests&filter=cr=33;crs='.$_holidayId.';crv=0'); + + $this->lvTabs->addListviewTab(new Listview($tabData, QuestList::$brickFile)); + + $questItems = []; + foreach (array_column($quests->rewards, Type::ITEM) as $arr) + $questItems = array_merge($questItems, array_keys($arr)); + + foreach (array_column($quests->choices, Type::ITEM) as $arr) + $questItems = array_merge($questItems, array_keys($arr)); + + foreach (array_column($quests->requires, Type::ITEM) as $arr) + $questItems = array_merge($questItems, $arr); + + if ($questItems) + $itemCnd[] = ['id', $questItems]; } // items from creature @@ -225,9 +260,9 @@ class EventBaseResponse extends TemplateResponse implements ICache // vendor $cIds = $creatures->getFoundIDs(); if ($sells = DB::World()->selectCol( - 'SELECT `item` FROM npc_vendor nv WHERE `entry` IN (?a) UNION - SELECT nv1.`item` FROM npc_vendor nv1 JOIN npc_vendor nv2 ON -nv1.`entry` = nv2.`item` WHERE nv2.`entry` IN (?a) UNION - SELECT `item` FROM game_event_npc_vendor genv JOIN creature c ON genv.`guid` = c.`guid` WHERE c.`id` IN (?a)', + 'SELECT `item` FROM npc_vendor nv WHERE `entry` IN %in UNION + SELECT nv1.`item` FROM npc_vendor nv1 JOIN npc_vendor nv2 ON -nv1.`entry` = nv2.`item` WHERE nv2.`entry` IN %in UNION + SELECT `item` FROM game_event_npc_vendor genv JOIN creature c ON genv.`guid` = c.`guid` WHERE c.`id` IN %in', $cIds, $cIds, $cIds )) $itemCnd[] = ['id', $sells]; @@ -237,6 +272,7 @@ class EventBaseResponse extends TemplateResponse implements ICache // not checking for loot ... cant distinguish between eventLoot and fillerCrapLoot if ($itemCnd) { + array_unshift($itemCnd, DB::OR); $eventItems = new ItemList($itemCnd); if (!$eventItems->error) { @@ -252,7 +288,7 @@ class EventBaseResponse extends TemplateResponse implements ICache } // tab: see also (event conditions) - if ($rel = DB::World()->selectCol('SELECT IF(`eventEntry` = `prerequisite_event`, NULL, IF(`eventEntry` = ?d, `prerequisite_event`, -`eventEntry`)) FROM game_event_prerequisite WHERE `prerequisite_event` = ?d OR `eventEntry` = ?d', $this->typeId, $this->typeId, $this->typeId)) + if ($rel = DB::World()->selectCol('SELECT IF(`eventEntry` = `prerequisite_event`, NULL, IF(`eventEntry` = %i, `prerequisite_event`, -`eventEntry`)) FROM game_event_prerequisite WHERE `prerequisite_event` = %i OR `eventEntry` = %i', $this->typeId, $this->typeId, $this->typeId)) { if (array_filter($rel, fn($x) => $x === null)) trigger_error('game_event_prerequisite: this event has itself as prerequisite', E_USER_WARNING); @@ -320,7 +356,7 @@ class EventBaseResponse extends TemplateResponse implements ICache // interval if ($rec > 0) - $infobox[] = Lang::event('interval').Util::formatTime($rec * 1000); + $infobox[] = Lang::event('interval').DateTime::formatTimeElapsed($rec * 1000); // in progress if ($start < time() && $end > time()) diff --git a/endpoints/events/events.php b/endpoints/events/events.php index 2ae4a9b9..885a7821 100644 --- a/endpoints/events/events.php +++ b/endpoints/events/events.php @@ -11,7 +11,7 @@ class EventsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::WORLDEVENT; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'events'; @@ -20,11 +20,11 @@ class EventsBaseResponse extends TemplateResponse implements ICache protected array $validCats = [0, 1, 2, 3]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -55,7 +55,7 @@ class EventsBaseResponse extends TemplateResponse implements ICache $this->redButtons[BUTTON_WOWHEAD] = true; - $condition = []; + $condition = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $condition[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; diff --git a/endpoints/faction/faction.php b/endpoints/faction/faction.php index 1279b62b..e85f5a79 100644 --- a/endpoints/faction/faction.php +++ b/endpoints/faction/faction.php @@ -10,7 +10,7 @@ class FactionBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'faction'; @@ -96,8 +96,25 @@ class FactionBaseResponse extends TemplateResponse implements ICache if ($_ = $this->subject->getField('side')) $infobox[] = Lang::main('side').'[span class=icon-'.($_ == SIDE_ALLIANCE ? 'alliance' : 'horde').']'.Lang::game('si', $_).'[/span]'; - if ($infobox) - $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + // id + $infobox[] = Lang::faction('id') . $this->typeId; + + // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) + if (Cfg::get('PROFILER_ENABLE') && !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW)) + { + $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ::profiler_completion_reputation WHERE `exalted` = 1 AND `factionId` = %i', $this->typeId); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ::profiler_profiles WHERE `custom` = 0 AND `stub` = 0'); + $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); + + // completion row added by InfoboxMarkup + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + + if ($infobox) // unsure if this should be tracked (needs data dump in User::getCompletion()) + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', 0); /****************/ @@ -115,7 +132,7 @@ class FactionBaseResponse extends TemplateResponse implements ICache CONCAT_WS(" ", faction1, faction2, faction3, faction4) AS faction, CONCAT_WS(" ", rate_1, rate_2, rate_3, rate_4) AS rate, CONCAT_WS(" ", rank_1, rank_2, rank_3, rank_4) AS rank - FROM reputation_spillover_template WHERE faction = ?d', $this->typeId); + FROM reputation_spillover_template WHERE faction = %i', $this->typeId); */ @@ -125,7 +142,7 @@ class FactionBaseResponse extends TemplateResponse implements ICache ); if ($p = $this->subject->getField('parentFactionId')) // linked via parent - $conditions[] = ['OR', ['id', $p], ['parentFactionId', $p]]; + $conditions[] = [DB::OR, ['id', $p], ['parentFactionId', $p]]; else // self as parent $conditions[] = ['parentFactionId', $this->typeId]; @@ -145,7 +162,7 @@ class FactionBaseResponse extends TemplateResponse implements ICache ); // reward rates (ultimately this should be calculated into each reward display) - if ($rates = DB::World()->selectRow('SELECT `quest_rate`, `quest_daily_rate`, `quest_weekly_rate`, `quest_monthly_rate`, `quest_repeatable_rate`, `creature_rate`, `spell_rate` FROM reputation_reward_rate WHERE `faction` = ?d', $this->typeId)) + if ($rates = DB::World()->selectRow('SELECT `quest_rate`, `quest_daily_rate`, `quest_weekly_rate`, `quest_monthly_rate`, `quest_repeatable_rate`, `creature_rate`, `spell_rate` FROM reputation_reward_rate WHERE `faction` = %i', $this->typeId)) { $buff = ''; foreach ($rates as $k => $v) @@ -174,7 +191,7 @@ class FactionBaseResponse extends TemplateResponse implements ICache } // factionchange-equivalent - if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = ?d, `alliance_id`, -`horde_id`) FROM player_factionchange_reputations WHERE `alliance_id` = ?d OR `horde_id` = ?d', $this->typeId, $this->typeId, $this->typeId)) + if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = %i, `alliance_id`, -`horde_id`) FROM player_factionchange_reputations WHERE `alliance_id` = %i OR `horde_id` = %i', $this->typeId, $this->typeId, $this->typeId)) { $altFac = new FactionList(array(['id', abs($pendant)])); if (!$altFac->error) @@ -196,7 +213,7 @@ class FactionBaseResponse extends TemplateResponse implements ICache $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); // tab: items - $items = new ItemList(array(['requiredFaction', $this->typeId]), ['calcTotal' => true]); + $items = new ItemList(array(Listview::DEFAULT_SIZE, ['requiredFaction', $this->typeId]), ['calcTotal' => true]); if (!$items->error) { $this->extendGlobalData($items->getJSGlobals(GLOBALINFO_SELF)); @@ -207,8 +224,9 @@ class FactionBaseResponse extends TemplateResponse implements ICache 'sort' => ['standing', 'name'] ); - if ($items->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=17;crs='.$this->typeId.';crv=0'); + if ($items->getMatches() > Listview::DEFAULT_SIZE) + if (!is_null(ItemListFilter::getCriteriaIndex(17, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=17;crs='.$this->typeId.';crv=0'); $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile, 'itemStandingCol')); } @@ -219,16 +237,16 @@ class FactionBaseResponse extends TemplateResponse implements ICache { // inherit siblings/children from $spillover $cRep = DB::World()->selectCol('SELECT DISTINCT `creature_id` AS ARRAY_KEY, `qty` FROM ( - SELECT `creature_id`, `RewOnKillRepValue1` as "qty" FROM creature_onkill_reputation WHERE `RewOnKillRepValue1` > 0 AND (`RewOnKillRepFaction1` = ?d { OR (`RewOnKillRepFaction1` IN (?a) AND `IsTeamAward1` <> 0) } ) UNION - SELECT `creature_id`, `RewOnKillRepValue2` as "qty" FROM creature_onkill_reputation WHERE `RewOnKillRepValue2` > 0 AND (`RewOnKillRepFaction2` = ?d { OR (`RewOnKillRepFaction2` IN (?a) AND `IsTeamAward2` <> 0) } ) + SELECT `creature_id`, `RewOnKillRepValue1` as "qty" FROM creature_onkill_reputation WHERE `RewOnKillRepValue1` > 0 AND (`RewOnKillRepFaction1` = %i OR (`RewOnKillRepFaction1` IN %in AND `IsTeamAward1` <> 0) ) UNION + SELECT `creature_id`, `RewOnKillRepValue2` as "qty" FROM creature_onkill_reputation WHERE `RewOnKillRepValue2` > 0 AND (`RewOnKillRepFaction2` = %i OR (`RewOnKillRepFaction2` IN %in AND `IsTeamAward2` <> 0) ) ) x', - $this->typeId, $spillover->getFoundIDs() ?: DBSIMPLE_SKIP, - $this->typeId, $spillover->getFoundIDs() ?: DBSIMPLE_SKIP + $this->typeId, $spillover->getFoundIDs() ?: [0], + $this->typeId, $spillover->getFoundIDs() ?: [0] ); if ($cRep) { - $killCreatures = new CreatureList(array(['id', array_keys($cRep)]), ['calcTotal' => true]); + $killCreatures = new CreatureList(array(Listview::DEFAULT_SIZE, ['id', array_keys($cRep)]), ['calcTotal' => true]); if (!$killCreatures->error) { $data = $killCreatures->getListviewData(); @@ -241,8 +259,9 @@ class FactionBaseResponse extends TemplateResponse implements ICache 'sort' => ['-reputation', 'name'] ); - if ($killCreatures->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=42;crs='.$this->typeId.';crv=0'); + if ($killCreatures->getMatches() > Listview::DEFAULT_SIZE) + if (!is_null(CreatureListFilter::getCriteriaIndex(42, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=42;crs='.$this->typeId.';crv=0'); $this->addDataLoader('zones'); $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile, 'npcRepCol')); @@ -253,7 +272,7 @@ class FactionBaseResponse extends TemplateResponse implements ICache // tab: members if ($_ = $this->subject->getField('templateIds')) { - $members = new CreatureList(array(['faction', $_]), ['calcTotal' => true]); + $members = new CreatureList(array(Listview::DEFAULT_SIZE, ['faction', $_]), ['calcTotal' => true]); if (!$members->error) { $tabData = array( @@ -262,8 +281,9 @@ class FactionBaseResponse extends TemplateResponse implements ICache 'name' => '$LANG.tab_members' ); - if ($members->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=3;crs='.$this->typeId.';crv=0'); + if ($members->getMatches() > Listview::DEFAULT_SIZE) + if (!is_null(CreatureListFilter::getCriteriaIndex(3, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=3;crs='.$this->typeId.';crv=0'); $this->addDataLoader('zones'); $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); @@ -283,12 +303,13 @@ class FactionBaseResponse extends TemplateResponse implements ICache // tab: quests $conditions = array( - ['AND', ['rewardFactionId1', $this->typeId], ['rewardFactionValue1', 0, '>']], - ['AND', ['rewardFactionId2', $this->typeId], ['rewardFactionValue2', 0, '>']], - ['AND', ['rewardFactionId3', $this->typeId], ['rewardFactionValue3', 0, '>']], - ['AND', ['rewardFactionId4', $this->typeId], ['rewardFactionValue4', 0, '>']], - ['AND', ['rewardFactionId5', $this->typeId], ['rewardFactionValue5', 0, '>']], - 'OR' + DB::OR, + Listview::DEFAULT_SIZE, + [DB::AND, ['rewardFactionId1', $this->typeId], ['rewardFactionValue1', 0, '>']], + [DB::AND, ['rewardFactionId2', $this->typeId], ['rewardFactionValue2', 0, '>']], + [DB::AND, ['rewardFactionId3', $this->typeId], ['rewardFactionValue3', 0, '>']], + [DB::AND, ['rewardFactionId4', $this->typeId], ['rewardFactionValue4', 0, '>']], + [DB::AND, ['rewardFactionId5', $this->typeId], ['rewardFactionValue5', 0, '>']] ); $quests = new QuestList($conditions, ['calcTotal' => true]); if (!$quests->error) @@ -300,8 +321,9 @@ class FactionBaseResponse extends TemplateResponse implements ICache 'extraCols' => '$_' ); - if ($quests->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) - $tabData['note'] = sprintf(Util::$filterResultString, '?quests&filter=cr=1;crs='.$this->typeId.';crv=0'); + if ($quests->getMatches() > Listview::DEFAULT_SIZE) + if (!is_null(QuestListFilter::getCriteriaIndex(1, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?quests&filter=cr=1;crs='.$this->typeId.';crv=0'); $this->lvTabs->addListviewTab(new Listview($tabData, QuestList::$brickFile, 'questRepCol')); } diff --git a/endpoints/factions/factions.php b/endpoints/factions/factions.php index db3caeb6..5e8dd308 100644 --- a/endpoints/factions/factions.php +++ b/endpoints/factions/factions.php @@ -11,7 +11,7 @@ class FactionsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::FACTION; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'factions'; @@ -25,11 +25,11 @@ class FactionsBaseResponse extends TemplateResponse implements ICache 0 => true ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -71,7 +71,7 @@ class FactionsBaseResponse extends TemplateResponse implements ICache $this->redButtons[BUTTON_WOWHEAD] = true; - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) // unlisted factions $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; @@ -81,11 +81,11 @@ class FactionsBaseResponse extends TemplateResponse implements ICache else if (isset($this->category[0])) { if ($this->category[0]) - $subs = DB::Aowow()->selectCol('SELECT `id` FROM ?_factions WHERE `parentFactionId` = ?d', $this->category[0]); + $subs = DB::Aowow()->selectCol('SELECT `id` FROM ::factions WHERE `parentFactionId` = %i', $this->category[0]); else $subs = [0]; - $conditions[] = ['OR', ['parentFactionId', $subs], ['id', $subs]]; + $conditions[] = [DB::OR, ['parentFactionId', $subs], ['id', $subs]]; } $data = []; diff --git a/endpoints/faq/faq.php b/endpoints/faq/faq.php index 70337871..7946f354 100644 --- a/endpoints/faq/faq.php +++ b/endpoints/faq/faq.php @@ -13,11 +13,11 @@ class FaqBaseResponse extends TemplateResponse protected ?int $activeTab = parent::TAB_MORE; protected array $breadcrumb = [2, 3]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - if ($pageParam) + if ($rawParam) $this->generateError(); } diff --git a/endpoints/filter/filter.php b/endpoints/filter/filter.php index f6fdc23b..9830385c 100644 --- a/endpoints/filter/filter.php +++ b/endpoints/filter/filter.php @@ -12,21 +12,35 @@ class FilterBaseResponse extends TextResponse private string $page = ''; private ?Filter $filter = null; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - if (!$pageParam) + if (!$rawParam) return; - parent::__construct($pageParam); + parent::__construct($rawParam); - $catg = null; - if (strstr($pageParam, '=')) - [$this->page, $catg] = explode('=', $pageParam); + $catg = $page = null; + if (strstr($rawParam, '=')) + [$page, $catg] = explode('=', $rawParam); else - $this->page = $pageParam; + $page = $rawParam; + + if (!$page || preg_match('/[^a-z\-]/i', $page)) + return; + + $this->page = strtolower($page); if ($catg !== null) - $this->catg = explode('.', $catg); + { + // category is a string for profiler (region.realm) but not passed through here + foreach (explode('.', $catg) as $c) + { + if (preg_match('/\D/', $c)) + break; + + $this->catg[] = intval($c); + } + } $opts = ['parentCats' => $this->catg]; @@ -38,14 +52,22 @@ class FilterBaseResponse extends TextResponse }; // yes, the whole _POST! .. should the input fields be exposed and static so they can be evaluated via BaseResponse::initRequestData() ? - $this->filter = Type::newFilter($fileStr, $_POST, $opts); + if (!$this->filter = Type::newFilter($fileStr, $_POST, $opts)) + trigger_error('Filter::__construct - tried to init filter from bogus GET data', E_USER_WARNING); } protected function generate() : void { + // could not build filter from $this->page > go to front page + if (!$this->filter) + { + $this->redirectTo = '.'; + return; + } + $url = '?'.$this->page; - $this->filter?->mergeCat($this->catg); + $this->filter->mergeCat($this->catg); if ($this->catg) $url .= '='.implode('.', $this->catg); @@ -53,7 +75,7 @@ class FilterBaseResponse extends TextResponse if ($x = $this->filter?->buildGETParam()) $url .= '&filter='.$x; - if ($this->filter?->error) + if ($this->filter->error) $_SESSION['error']['fi'] = $this->filter::class; // do get request diff --git a/endpoints/go-to-comment/go-to-comment.php b/endpoints/go-to-comment/go-to-comment.php index 16112048..c99d55f4 100644 --- a/endpoints/go-to-comment/go-to-comment.php +++ b/endpoints/go-to-comment/go-to-comment.php @@ -21,8 +21,9 @@ class GotocommentBaseResponse extends TextResponse return; } - // type <> 0 AND typeId <> 0 AND replyTo = 0 for comments - $comment = DB::Aowow()->selectRow('SELECT `id`, `type`, `typeId` FROM ?_comments WHERE `replyTo` = 0 AND `id` = ?d', $this->_get['id']); + // the reputation-history listview only creates go-to-comment links. So either upvoting replies does not grant reputation, or.... bug.? + + $comment = DB::Aowow()->selectRow('SELECT IFNULL(c2.`id`, c1.`id`) AS "id", IFNULL(c2.`type`, c1.`type`) AS "type", IFNULL(c2.`typeId`, c1.`typeId`) AS "typeId" FROM ::comments c1 LEFT JOIN ::comments c2 ON c1.`replyTo` = c2.`id` WHERE c1.`id` = %i', $this->_get['id']); if (!$comment) { trigger_error('GotocommentBaseResponse - comment #'.$this->_get['id'].' not found', E_USER_ERROR); @@ -36,6 +37,8 @@ class GotocommentBaseResponse extends TextResponse } $this->redirectTo = sprintf('?%s=%d#comments:id=%d', Type::getFileString($comment['type']), $comment['typeId'], $comment['id']); + if ($comment['id'] != $this->_get['id']) // i am reply + $this->redirectTo .= ':reply='.$this->_get['id']; } } diff --git a/endpoints/go-to-reply/go-to-reply.php b/endpoints/go-to-reply/go-to-reply.php index c13fdebd..e3cbbcc4 100644 --- a/endpoints/go-to-reply/go-to-reply.php +++ b/endpoints/go-to-reply/go-to-reply.php @@ -22,7 +22,7 @@ class GotoreplyBaseResponse extends TextResponse } // type = typeId = 0 AND replyTo <> 0 for replies - $reply = DB::Aowow()->selectRow('SELECT c.`id`, r.`id` AS "reply", c.`type`, c.`typeId` FROM ?_comments r JOIN ?_comments c ON r.`replyTo` = c.`id` WHERE r.`id` = ?d', $this->_get['id']); + $reply = DB::Aowow()->selectRow('SELECT c.`id`, r.`id` AS "reply", c.`type`, c.`typeId` FROM ::comments r JOIN ::comments c ON r.`replyTo` = c.`id` WHERE r.`id` = %i', $this->_get['id']); if (!$reply) { trigger_error('GotoreplyBaseResponse - reply #'.$this->_get['id'].' not found', E_USER_ERROR); diff --git a/endpoints/guide/changelog.php b/endpoints/guide/changelog.php index 685dfad3..eee823e7 100644 --- a/endpoints/guide/changelog.php +++ b/endpoints/guide/changelog.php @@ -33,7 +33,7 @@ class GuideChangelogResponse extends TemplateResponse if (!$guide->canBeViewed() && !$guide->userCanView()) $this->forward('?guides='.$guide->getField('category')); - $this->h1 = lang::guide('clTitle', [$this->_get['id'], $guide->getField('title')]); + $this->h1 = Lang::guide('clTitle', [$this->_get['id'], $guide->getField('title')]); if (!$this->h1) $this->h1 = $guide->getField('name'); @@ -79,20 +79,21 @@ class GuideChangelogResponse extends TemplateResponse $buff = '
    '; $inp = fn($rev) => User::isInGroup(U_GROUP_STAFF) && false ? ($rev !== null ? '' : '') : ''; + $now = new DateTime(); - $logEntries = DB::Aowow()->select('SELECT a.`username` AS `name`, gcl.`date`, gcl.`status`, gcl.`msg`, gcl.`rev` FROM ?_guides_changelog gcl JOIN ?_account a ON a.`id` = gcl.`userId` WHERE gcl.`id` = ?d ORDER BY gcl.`date` DESC', $this->_get['id']); + $logEntries = DB::Aowow()->selectAssoc('SELECT a.`username` AS `name`, gcl.`date`, gcl.`status`, gcl.`msg`, gcl.`rev` FROM ::guides_changelog gcl JOIN ::account a ON a.`id` = gcl.`userId` WHERE gcl.`id` = %i ORDER BY gcl.`date` DESC', $this->_get['id']); foreach ($logEntries as $log) { if ($log['status'] != GuideMgr::STATUS_NONE) - $buff .= '
  • '.$inp($log['rev']).''.Lang::guide('clStatusSet', [Lang::guide('status', $log['status'])]).''.Util::formatTimeDiff($log['date'])."
  • \n"; + $buff .= '
  • '.$inp($log['rev']).''.Lang::guide('clStatusSet', [Lang::guide('status', $log['status'])]).''.$now->formatDate($log['date'], true)."
  • \n"; else if ($log['msg']) - $buff .= '
  • '.$inp($log['rev']).''.Util::formatTimeDiff($log['date']).Lang::main('colon').''.$log['msg'].' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
  • \n"; + $buff .= '
  • '.$inp($log['rev']).''.$now->formatDate($log['date'], true).Lang::main('colon').''.$log['msg'].' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
  • \n"; else - $buff .= '
  • '.$inp($log['rev']).''.Util::formatTimeDiff($log['date']).Lang::main('colon').''.Lang::guide('clMinorEdit').' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
  • \n"; + $buff .= '
  • '.$inp($log['rev']).''.$now->formatDate($log['date'], true).Lang::main('colon').''.Lang::guide('clMinorEdit').' '.Lang::main('byUser', [$log['name'], 'style="text-decoration:underline"'])."
  • \n"; } // append creation - $buff .= '
  • '.$inp(0).''.Lang::guide('clCreated').''.Util::formatTimeDiff($guide->getField('date'))."
  • \n
\n"; + $buff .= '
  • '.$inp(0).''.Lang::guide('clCreated').''.$now->formatDate($guide->getField('date'), true)."
  • \n\n"; if (User::isInGroup(U_GROUP_STAFF) && false) $buff .= ''; diff --git a/endpoints/guide/edit.php b/endpoints/guide/edit.php index b2897167..be86d440 100644 --- a/endpoints/guide/edit.php +++ b/endpoints/guide/edit.php @@ -45,7 +45,7 @@ class GuideEditResponse extends TemplateResponse 'description' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkDescription'] ], 'changelog' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ], 'body' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextBlob'] ], - 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFrom'] ], + 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkLocale'] ], 'category' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => 1, 'max_value' => 9] ], 'specId' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => -1, 'max_value' => 2, 'default' => -1]], 'classId' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => 1, 'max_value' => 11, 'default' => 0]] @@ -66,7 +66,7 @@ class GuideEditResponse extends TemplateResponse return; $this->typeId = $this->_get['id']; // just to display sensible not-found msg - $status = DB::Aowow()->selectCell('SELECT `status` FROM ?_guides WHERE `id` = ?d AND `status` <> ?d { AND `userId` = ?d }', $this->typeId, GuideMgr::STATUS_ARCHIVED, User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : User::$id); + $status = DB::Aowow()->selectCell('SELECT `status` FROM ::guides WHERE %if', !User::isInGroup(U_GROUP_STAFF), '`userId` = %i AND', User::$id, '%end `id` = %i AND `status` <> %i', $this->typeId, GuideMgr::STATUS_ARCHIVED); if (!$status && $this->typeId) $this->generateNotFound(Lang::game('guide'), Lang::guide('notFound')); else if (!$this->typeId) @@ -75,7 +75,7 @@ class GuideEditResponse extends TemplateResponse // just so we don't have to access GuideMgr from template $this->isDraft = $status == GuideMgr::STATUS_DRAFT; $this->editStatus = $status; - $this->editRev = DB::Aowow()->selectCell('SELECT `rev` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d ORDER BY `rev` DESC', Type::GUIDE, $this->typeId); + $this->editRev = DB::Aowow()->selectCell('SELECT `rev` FROM ::articles WHERE `type` = %i AND `typeId` = %i ORDER BY `rev` DESC', Type::GUIDE, $this->typeId); } protected function generate() : void @@ -157,15 +157,15 @@ class GuideEditResponse extends TemplateResponse if ($this->_get['id'] === 0) { $guideData += ['userId' => User::$id]; - if (!($this->typeId = (int)DB::Aowow()->query('INSERT INTO ?_guides (?#) VALUES (?a)', array_keys($guideData), array_values($guideData)))) + if (!($this->typeId = (int)DB::Aowow()->qry('INSERT INTO ::guides %v', $guideData))) { trigger_error('GuideEditResponse::saveGuide - failed to save guide to db', E_USER_ERROR); return false; } } // existing guide > :shrug: - else if (DB::Aowow()->query('UPDATE ?_guides SET ?a WHERE `id` = ?d', $guideData, $this->typeId)) - DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `rev`, `date`, `userId`, `msg`) VALUES (?d, ?d, ?d, ?d, ?)', $this->typeId, $this->editRev, time(), User::$id, $this->_post['changelog']); + else if (DB::Aowow()->qry('UPDATE ::guides SET %a WHERE `id` = %i', $guideData, $this->typeId)) + DB::Aowow()->qry('INSERT INTO ::guides_changelog (`id`, `rev`, `date`, `userId`, `msg`) VALUES (%i, %i, %i, %i, %s)', $this->typeId, $this->editRev, time(), User::$id, $this->_post['changelog']); else { trigger_error('GuideEditResponse::saveGuide - failed to update guide in db', E_USER_ERROR); @@ -173,8 +173,8 @@ class GuideEditResponse extends TemplateResponse } // insert Article - $articleId = DB::Aowow()->query( - 'INSERT INTO ?_articles (`type`, `typeId`, `locale`, `rev`, `editAccess`, `article`) VALUES (?d, ?d, ?d, ?d, ?d, ?)', + $articleId = DB::Aowow()->qry( + 'INSERT INTO ::articles (`type`, `typeId`, `locale`, `rev`, `editAccess`, `article`) VALUES (%i, %i, %i, %i, %i, %s)', Type::GUIDE, $this->typeId, $this->_post['locale']->value, @@ -186,14 +186,14 @@ class GuideEditResponse extends TemplateResponse if (!is_int($articleId)) { if ($this->_get['id'] === 0) - DB::Aowow()->query('DELETE FROM ?_guides WHERE `id` = ?d', $this->typeId); + DB::Aowow()->qry('DELETE FROM ::guides WHERE `id` = %i', $this->typeId); trigger_error('GuideEditResponse::saveGuide - failed to save article to db', E_USER_ERROR); return false; } if ($this->_post['submit'] && $this->editStatus != GuideMgr::STATUS_REVIEW) - DB::Aowow()->query('INSERT INTO ?_guides_changelog (`id`, `date`, `userId`, `status`) VALUES (?d, ?d, ?d, ?d)', $this->typeId, time(), User::$id, GuideMgr::STATUS_REVIEW); + DB::Aowow()->qry('INSERT INTO ::guides_changelog (`id`, `date`, `userId`, `status`) VALUES (%i, %i, %i, %i)', $this->typeId, time(), User::$id, GuideMgr::STATUS_REVIEW); $this->editStatus = $guideData['status']; diff --git a/endpoints/guide/guide.php b/endpoints/guide/guide.php index 90de7eb8..45e30c07 100644 --- a/endpoints/guide/guide.php +++ b/endpoints/guide/guide.php @@ -10,7 +10,7 @@ class GuideBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'guide'; @@ -42,7 +42,7 @@ class GuideBaseResponse extends TemplateResponse implements ICache $this->typeId = $nameOrId; else if (preg_match(GuideMgr::VALID_URL, $nameOrId)) { - if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `url` = ?', Util::lower($nameOrId))) + if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ::guides WHERE `url` = %s', Util::lower($nameOrId))) { $this->typeId = intVal($id); $this->articleUrl = Util::lower($nameOrId); @@ -68,12 +68,12 @@ class GuideBaseResponse extends TemplateResponse implements ICache $this->contribute = CONTRIBUTE_NONE; } - if ($this->articleUrl) + // owner or staff and manual rev passed + if ($this->subject->userCanView() && $this->_get['rev']) + $this->guideRevision = $this->_get['rev']; + // has publicly viewable version + else if ($this->subject->canBeViewed()) $this->guideRevision = $this->subject->getField('rev'); - else if ($this->subject->userCanView()) - $this->guideRevision = $this->_get['rev'] ?? $this->subject->getField('latest'); - else - $this->subject->getField('rev'); $this->h1 = $this->subject->getField('name'); @@ -127,7 +127,7 @@ class GuideBaseResponse extends TemplateResponse implements ICache $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], __forceTabs: true); - // the article text itself is added by PageTemplate::addArticle() + // the article text itself is added by TemplateResponse::addArticle() parent::generate(); $this->result->registerDisplayHook('infobox', [self::class, 'infoboxHook']); @@ -215,9 +215,9 @@ class GuideBaseResponse extends TemplateResponse implements ICache return; // increment and display views - DB::Aowow()->query('UPDATE ?_guides SET `views` = `views` + 1 WHERE `id` = ?d', $pt->typeId); + DB::Aowow()->qry('UPDATE ::guides SET `views` = `views` + 1 WHERE `id` = %i', $pt->typeId); - $nViews = DB::Aowow()->selectCell('SELECT `views` FROM ?_guides WHERE `id` = ?d', $pt->typeId); + $nViews = DB::Aowow()->selectCell('SELECT `views` FROM ::guides WHERE `id` = %i', $pt->typeId); $infobox->addItem(Lang::guide('views').'[n5='.$nViews.']'); diff --git a/endpoints/guide/guide_power.php b/endpoints/guide/guide_power.php index 99fd4dba..970bc302 100644 --- a/endpoints/guide/guide_power.php +++ b/endpoints/guide/guide_power.php @@ -32,7 +32,7 @@ class GuidePowerResponse extends TextResponse implements ICache if (Util::checkNumeric($idOrName, NUM_CAST_INT)) $this->typeId = $idOrName; - else if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_guides WHERE `url` = ?', Util::lower($idOrName))) + else if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ::guides WHERE `url` = %s', Util::lower($idOrName))) { $this->typeId = intVal($id); $this->url = Util::lower($idOrName); diff --git a/endpoints/guide/vote.php b/endpoints/guide/vote.php index ca7ee945..ff82c837 100644 --- a/endpoints/guide/vote.php +++ b/endpoints/guide/vote.php @@ -29,18 +29,18 @@ class GuideVoteResponse extends TextResponse // by id, not own, published $points = $votes = 0; - if ($g = DB::Aowow()->selectRow('SELECT `userId`, `cuFlags` FROM ?_guides WHERE `id` = ?d AND (`status` = ?d OR `rev` > 0)', $this->_post['id'], GuideMgr::STATUS_APPROVED)) + if ($g = DB::Aowow()->selectRow('SELECT `userId`, `cuFlags` FROM ::guides WHERE `id` = %i AND (`status` = %i OR `rev` > 0)', $this->_post['id'], GuideMgr::STATUS_APPROVED)) { // apparently you are allowed to vote on your own guide if ($g['cuFlags'] & GUIDE_CU_NO_RATING) $this->generate403(); if (!$this->_post['rating']) - DB::Aowow()->query('DELETE FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d AND `userId` = ?d', RATING_GUIDE, $this->_post['id'], User::$id); + DB::Aowow()->qry('DELETE FROM ::user_ratings WHERE `type` = %i AND `entry` = %i AND `userId` = %i', RATING_GUIDE, $this->_post['id'], User::$id); else - DB::Aowow()->query('REPLACE INTO ?_user_ratings (`type`, `entry`, `userId`, `value`) VALUES (?d, ?d, ?d, ?d)', RATING_GUIDE, $this->_post['id'], User::$id, $this->_post['rating']); + DB::Aowow()->qry('REPLACE INTO ::user_ratings (`type`, `entry`, `userId`, `value`) VALUES (%i, %i, %i, %i)', RATING_GUIDE, $this->_post['id'], User::$id, $this->_post['rating']); - [$points, $votes] = DB::Aowow()->selectRow('SELECT IFNULL(SUM(`value`), 0) AS "0", IFNULL(COUNT(*), 0) AS "1" FROM ?_user_ratings WHERE `type` = ?d AND `entry` = ?d', RATING_GUIDE, $this->_post['id']); + [$points, $votes] = DB::Aowow()->selectRow('SELECT IFNULL(SUM(`value`), 0) AS "0", IFNULL(COUNT(*), 0) AS "1" FROM ::user_ratings WHERE `type` = %i AND `entry` = %i', RATING_GUIDE, $this->_post['id']); } $this->result = Util::toJSON($votes ? ['rating' => $points / $votes, 'nvotes' => $votes] : ['rating' => 0, 'nvotes' => 0]); diff --git a/endpoints/guides/guides.php b/endpoints/guides/guides.php index 9c0db0ee..23116f41 100644 --- a/endpoints/guides/guides.php +++ b/endpoints/guides/guides.php @@ -10,7 +10,7 @@ class GuidesBaseResponse extends TemplateResponse // implements ICache { use TrListPage/* , TrCache */; - // protected int $cacheType = CACHE_TYPE_PAGE; // really do? cache would need to be destroyed externally with each guide status update + // protected int $cacheType = CACHE_TYPE_LIST_PAGE; // really do? cache would need to be destroyed externally with each guide status update protected int $type = Type::GUIDE; protected string $template = 'list-page-generic'; @@ -20,11 +20,11 @@ class GuidesBaseResponse extends TemplateResponse // implements ICache protected array $validCats = [null, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -45,7 +45,7 @@ class GuidesBaseResponse extends TemplateResponse // implements ICache ['locale', Lang::getLocale()->value], ['status', GuideMgr::STATUS_ARCHIVED, '!'], // never archived guides [ - 'OR', + DB::OR, ['status', GuideMgr::STATUS_APPROVED], // currently approved ['rev', 0, '>'] // has previously approved revision ] diff --git a/endpoints/guild/guild.php b/endpoints/guild/guild.php index 8855b26c..9db0d252 100644 --- a/endpoints/guild/guild.php +++ b/endpoints/guild/guild.php @@ -46,24 +46,26 @@ class GuildBaseResponse extends TemplateResponse // 3 possibilities // 1) already synced to aowow - if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `cuFlags` FROM ?_profiler_guild WHERE `realm` = ?d AND `nameUrl` = ?', $this->realmId, Profiler::urlize($this->subjectName))) + if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `stub` FROM ::profiler_guild WHERE `realm` = %i AND `nameUrl` = %s', $this->realmId, Profiler::urlize($this->subjectName))) { $this->typeId = $subject['id']; - if ($subject['cuFlags'] & PROFILER_CU_NEEDS_RESYNC) + if ($subject['stub']) $this->handleIncompleteData(Type::GUILD, $subject['realmGUID']); return; } // 2) not yet synced but exists on realm (wont work if we get passed an urlized name, but there is nothing we can do about it) - else if ($subject = DB::Characters($this->realmId)->selectRow('SELECT `guildid` AS "realmGUID", `name` FROM guild WHERE `name` = ?', Util::ucFirst($this->subjectName))) + $subjects = DB::Characters($this->realmId)->selectAssoc('SELECT `guildid` AS "realmGUID", `name` FROM guild WHERE `name` = %s', $this->subjectName); + if ($subject = array_find($subjects ?: [], fn($x) => Util::lower($x['name']) === Util::lower($this->subjectName))) { $subject['realm'] = $this->realmId; - $subject['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; + $subject['stub'] = 1; + $subject['nameUrl'] = Profiler::urlize($subject['name']); // create entry from realm with basic info - DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES (?a)', array_keys($subject), array_values($subject)); + DB::Aowow()->qry('INSERT IGNORE INTO ::profiler_guild %v', $subject); $this->handleIncompleteData(Type::GUILD, $subject['realmGUID']); return; @@ -119,7 +121,7 @@ class GuildBaseResponse extends TemplateResponse // statistic calculations here // smuggle the guild ranks into the html - if ($ranks = DB::Aowow()->selectCol('SELECT `rank` AS ARRAY_KEY, `name` FROM ?_profiler_guild_rank WHERE `guildId` = ?d', $this->typeId)) + if ($ranks = DB::Aowow()->selectCol('SELECT `rank` AS ARRAY_KEY, `name` FROM ::profiler_guild_rank WHERE `guildId` = %i', $this->typeId)) $this->extraHTML = ''; @@ -130,7 +132,7 @@ class GuildBaseResponse extends TemplateResponse $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated'); // tab: members - $member = new LocalProfileList(array(['p.guild', $this->typeId], Cfg::get('SQL_LIMIT_NONE'))); + $member = new LocalProfileList(array(['p.guild', $this->typeId])); $this->lvTabs->addListviewTab(new Listview(array( 'data' => $member->getListviewData(PROFILEINFO_CHARACTER | PROFILEINFO_GUILD), 'sort' => [-15], diff --git a/endpoints/guild/resync.php b/endpoints/guild/resync.php index 2af65bb7..82df7f8f 100644 --- a/endpoints/guild/resync.php +++ b/endpoints/guild/resync.php @@ -13,9 +13,9 @@ class GuildResyncResponse extends TextResponse 'profile' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -32,12 +32,12 @@ class GuildResyncResponse extends TextResponse if (!$this->assertGET('id')) return; - if ($guilds = DB::Aowow()->select('SELECT `realm`, `realmGUID` FROM ?_profiler_guild WHERE `id` IN (?a)', $this->_get['id'])) + if ($guilds = DB::Aowow()->selectAssoc('SELECT `realm`, `realmGUID` FROM ::profiler_guild WHERE `id` IN %in', $this->_get['id'])) foreach ($guilds as $g) Profiler::scheduleResync(Type::GUILD, $g['realm'], $g['realmGUID']); if ($this->_get['profile']) - if ($chars = DB::Aowow()->select('SELECT `realm`, `realmGUID` FROM ?_profiler_profiles WHERE `guild` IN (?a)', $this->_get['id'])) + if ($chars = DB::Aowow()->selectAssoc('SELECT `realm`, `realmGUID` FROM ::profiler_profiles WHERE `guild` IN %in', $this->_get['id'])) foreach ($chars as $c) Profiler::scheduleResync(Type::PROFILE, $c['realm'], $c['realmGUID']); diff --git a/endpoints/guild/status.php b/endpoints/guild/status.php index 128cc5fd..df311ee0 100644 --- a/endpoints/guild/status.php +++ b/endpoints/guild/status.php @@ -12,9 +12,9 @@ class GuildStatusResponse extends TextResponse 'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); diff --git a/endpoints/guilds/guilds.php b/endpoints/guilds/guilds.php index 4142461b..38f76557 100644 --- a/endpoints/guilds/guilds.php +++ b/endpoints/guilds/guilds.php @@ -29,14 +29,14 @@ class GuildsBaseResponse extends TemplateResponse implements IProfilerList private int $sumSubjects = 0; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { if (!Cfg::get('PROFILER_ENABLE')) $this->generateError(); - $this->getSubjectFromUrl($pageParam); + $this->getSubjectFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); $realms = []; foreach (Profiler::getRealms() as $idx => $r) @@ -51,8 +51,16 @@ class GuildsBaseResponse extends TemplateResponse implements IProfilerList $realms[] = $idx; } - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + if ($this->category) + $this->subCat = '='.implode('.', $this->category); + $this->filter = new GuildListFilter($this->_get['filter'] ?? '', ['realms' => $realms]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -85,6 +93,7 @@ class GuildsBaseResponse extends TemplateResponse implements IProfilerList /****************/ $conditions = array( + Listview::DEFAULT_SIZE, ['c.deleteInfos_Account', null], ['c.level', MAX_LEVEL, '<='], // prevents JS errors [['c.extra_flags', Profiler::CHAR_GMFLAGS, '&'], 0] @@ -120,12 +129,12 @@ class GuildsBaseResponse extends TemplateResponse implements IProfilerList $tabData['data'] = $guilds->getListviewData(); // create note if search limit was exceeded - if ($this->filter->query && $guilds->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($this->filter->query && $guilds->getMatches() > Listview::DEFAULT_SIZE) { $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_guildsfound2', $this->sumSubjects, $guilds->getMatches()); $tabData['_truncated'] = 1; } - else if ($guilds->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + else if ($guilds->getMatches() > Listview::DEFAULT_SIZE) $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_guildsfound', $this->sumSubjects, 0); } diff --git a/endpoints/help/help.php b/endpoints/help/help.php index 224a0c46..92e5f7f8 100644 --- a/endpoints/help/help.php +++ b/endpoints/help/help.php @@ -17,18 +17,19 @@ class HelpBaseResponse extends TemplateResponse private string $catg = ''; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - if (!$pageParam) + if (!$rawParam) $this->generateError(); - $pageId = array_search($pageParam, $this->validCats); + $pageId = array_search($rawParam, $this->validCats); if ($pageId === false) $this->generateError(); - $this->catg = $pageParam; + $this->catg = $rawParam; + $this->articleUrl = $this->pageName.'='.$rawParam; } protected function generate() : void diff --git a/endpoints/home/home.php b/endpoints/home/home.php index 4dd0c69f..8ca0caba 100644 --- a/endpoints/home/home.php +++ b/endpoints/home/home.php @@ -25,18 +25,18 @@ class HomeBaseResponse extends TemplateResponse protected function generate() : void { // set element - if ($_ = DB::Aowow()->selectCell('SELECT `title` FROM ?_home_titles WHERE `active` = 1 AND `locale` = ?d ORDER BY RAND()', Lang::getLocale()->value)) + if ($_ = DB::Aowow()->selectCell('SELECT `title` FROM ::home_titles WHERE `active` = 1 AND `locale` = %i ORDER BY RAND()', Lang::getLocale()->value)) $this->homeTitle = Util::jsEscape(Cfg::get('NAME').Lang::main('colon').$_); // load oneliner - if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_home_oneliner WHERE `active` = 1 ORDER BY RAND() LIMIT 1')) + if ($_ = DB::Aowow()->selectRow('SELECT * FROM ::home_oneliner WHERE `active` = 1 ORDER BY RAND() LIMIT 1')) $this->oneliner = new Markup(new LocString($_, 'text'), [], 'home-oneliner'); if ($_ = $this->oneliner?->getJsGlobals()) $this->extendGlobalData($_); // load featuredBox (user web server time) - if ($box = DB::Aowow()->selectRow('SELECT * FROM ?_home_featuredbox WHERE ?d BETWEEN `startDate` AND `endDate` ORDER BY `id` DESC', time())) + if ($box = DB::Aowow()->selectRow('SELECT * FROM ::home_featuredbox WHERE %i BETWEEN `startDate` AND `endDate` ORDER BY `id` DESC', time())) { // define text constants for all fields (STATIC_URL, HOST_URL, etc.) $box = Util::defStatic($box); @@ -55,9 +55,9 @@ class HomeBaseResponse extends TemplateResponse $this->extendGlobalData($_); // load overlay links - foreach (DB::Aowow()->select('SELECT * FROM ?_home_featuredbox_overlay WHERE `featureId` = ?d', $box['id']) as $ovl) + foreach (DB::Aowow()->selectAssoc('SELECT * FROM ::home_featuredbox_overlay WHERE `featureId` = %i', $box['id']) as $ovl) { - $ovl = Util::defStatic($ovl); + $ovl = Util::defStatic((array)$ovl); $this->featuredBox['overlays'][] = array( 'url' => $ovl['url'], diff --git a/endpoints/icon/get-id-from-name.php b/endpoints/icon/get-id-from-name.php index 178602cc..a74d9f62 100644 --- a/endpoints/icon/get-id-from-name.php +++ b/endpoints/icon/get-id-from-name.php @@ -21,7 +21,7 @@ class IconGetidfromnameResponse extends TextResponse } $this->result = 0; - if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_icons WHERE `name` = ?', $this->_get['name'])) + if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ::icons WHERE `name` = %s', $this->_get['name'])) $this->result = $id; } } diff --git a/endpoints/icon/icon.php b/endpoints/icon/icon.php index 2a67b53e..b0620539 100644 --- a/endpoints/icon/icon.php +++ b/endpoints/icon/icon.php @@ -10,7 +10,7 @@ class IconBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'icon'; protected string $pageName = 'icon'; @@ -39,7 +39,7 @@ class IconBaseResponse extends TemplateResponse implements ICache $this->extendGlobalData($this->subject->getJSGlobals()); - $this->h1 = $this->subject->getField('name'); + $this->h1 = $this->subject->getField('name_source'); $this->icon = $this->subject->getField('name', true, true); $this->gPageInfo += array( diff --git a/endpoints/icons/icons.php b/endpoints/icons/icons.php index 7613240e..75464fae 100644 --- a/endpoints/icons/icons.php +++ b/endpoints/icons/icons.php @@ -11,7 +11,7 @@ class IconsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::ICON; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'icons'; protected string $pageName = 'icons'; @@ -24,14 +24,22 @@ class IconsBaseResponse extends TemplateResponse implements ICache ); protected array $validCats = [0, 1, 2, 3]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new IconListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -43,13 +51,9 @@ class IconsBaseResponse extends TemplateResponse implements ICache if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /**************/ /* Page Title */ diff --git a/endpoints/item/item.php b/endpoints/item/item.php index f476b5b9..87778919 100644 --- a/endpoints/item/item.php +++ b/endpoints/item/item.php @@ -10,7 +10,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'item'; protected string $pageName = 'item'; @@ -103,10 +103,13 @@ class ItemBaseResponse extends TemplateResponse implements ICache SIDE_BOTH => Lang::game('si', SIDE_BOTH) }; + // id + $infobox[] = Lang::item('id') . $this->typeId; + // icon if ($_ = $this->subject->getField('iconId')) { - $infobox[] = Util::ucFirst(lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; $this->extendGlobalIds(Type::ICON, $_); } @@ -141,13 +144,13 @@ class ItemBaseResponse extends TemplateResponse implements ICache // tool if ($tId = $this->subject->getField('totemCategory')) - if ($tName = DB::Aowow()->selectRow('SELECT * FROM ?_totemcategory WHERE `id` = ?d', $tId)) + if ($tName = DB::Aowow()->selectRow('SELECT * FROM ::totemcategory WHERE `id` = %i', $tId)) $infobox[] = Lang::item('tool').'[url=?items&filter=cr=91;crs='.$tId.';crv=0]'.Util::localizedString($tName, 'name').'[/url]'; // extendedCost - if (!empty($this->subject->getExtendedCost([], $_reqRating)[$this->subject->id])) + if (!empty($this->subject->getExtendedCost([], $_reqRating)[$this->typeId])) { - $vendors = $this->subject->getExtendedCost()[$this->subject->id]; + $vendors = $this->subject->getExtendedCost()[$this->typeId]; $stack = $this->subject->getField('buyCount'); $divisor = $stack; $each = ''; @@ -316,8 +319,15 @@ class ItemBaseResponse extends TemplateResponse implements ICache if ($_bagFamily & 0x0100) $infobox[] = Lang::item('atKeyring'); + // completion row added by InfoboxMarkup + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + + $hasCompletion = !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW) && ($_class == ITEM_CLASS_RECIPE || ($_class == ITEM_CLASS_MISC && in_array($_subClass, [2, 5, -7]))); if ($infobox) - $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', $hasCompletion); /****************/ @@ -365,6 +375,8 @@ class ItemBaseResponse extends TemplateResponse implements ICache ); // merge identical stats and names for normal users (e.g. spellPower of a specific school became general spellPower with 3.0) + // see: https://web.archive.org/web/20101118041612/wowhead.com/item=11946 + // stats should also be merged if only the keys are the same, resulting in "+(8 - 9) Spirit" etc. if (!User::isInGroup(U_GROUP_EMPLOYEE)) { @@ -384,7 +396,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache } // factionchange-equivalent - if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = ?d, `alliance_id`, -`horde_id`) FROM player_factionchange_items WHERE `alliance_id` = ?d OR `horde_id` = ?d', $this->typeId, $this->typeId, $this->typeId)) + if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = %i, `alliance_id`, -`horde_id`) FROM player_factionchange_items WHERE `alliance_id` = %i OR `horde_id` = %i', $this->typeId, $this->typeId, $this->typeId)) { $altItem = new ItemList(array(['id', abs($pendant)])); if (!$altItem->error) @@ -408,7 +420,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); // tab: createdBy (perfect item specific) - if ($perfItem = DB::World()->select('SELECT *, `spellId` AS ARRAY_KEY FROM skill_perfect_item_template WHERE `perfectItemType` = ?d', $this->typeId)) + if ($perfItem = DB::World()->selectAssoc('SELECT *, `spellId` AS ARRAY_KEY FROM skill_perfect_item_template WHERE `perfectItemType` = %i', $this->typeId)) { $perfSpells = new SpellList(array(['id', array_column($perfItem, 'spellId')])); if (!$perfSpells->error) @@ -433,9 +445,9 @@ class ItemBaseResponse extends TemplateResponse implements ICache } // tabs: this item is contained in.. - $lootTabs = new Loot(); + $lootTabs = new LootByItem($this->typeId); $createdBy = []; - if ($lootTabs->getByItem($this->typeId)) + if ($lootTabs->getByItem()) { $this->extendGlobalData($lootTabs->jsGlobals); @@ -444,23 +456,44 @@ class ItemBaseResponse extends TemplateResponse implements ICache if (!$tabData['data']) continue; - if ($idx == 16) + if ($idx == LootByItem::SPELL_CREATED) $createdBy = array_column($tabData['data'], 'id'); - if ($idx == 1) + if ($idx == LootByItem::ITEM_DISENCHANTED) $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=163;crs='.$this->typeId.';crv=0'); - if ($idx == 4 && $this->subject->getSources($s, $sm) && $s[0] == SRC_DROP && isset($sm[0]['dd'])) - { - switch ($sm[0]['dd']) + if ($idx == LootByItem::NPC_DROPPED && $this->subject->getSources($s, $sm) && $s[0] == SRC_DROP && isset($sm[0]['dd'])) + $tabData['note'] = match($sm[0]['dd']) { - case -1: $tabData['note'] = '$LANG.lvnote_itemdropsinnormalonly'; break; - case -2: $tabData['note'] = '$LANG.lvnote_itemdropsinheroiconly'; break; - case -3: $tabData['note'] = '$LANG.lvnote_itemdropsinnormalheroic'; break; - case 1: $tabData['note'] = '$LANG.lvnote_itemdropsinnormal10only'; break; - case 2: $tabData['note'] = '$LANG.lvnote_itemdropsinnormal25only'; break; - case 3: $tabData['note'] = '$LANG.lvnote_itemdropsinheroic10only'; break; - case 4: $tabData['note'] = '$LANG.lvnote_itemdropsinheroic25only'; break; + -1 => '$LANG.lvnote_itemdropsinnormalonly', + -2 => '$LANG.lvnote_itemdropsinheroiconly', + -3 => '$LANG.lvnote_itemdropsinnormalheroic', + 1 => '$LANG.lvnote_itemdropsinnormal10only', + 2 => '$LANG.lvnote_itemdropsinnormal25only', + 3 => '$LANG.lvnote_itemdropsinheroic10only', + 4 => '$LANG.lvnote_itemdropsinheroic25only', + default => null + }; + + if ($idx == LootByItem::OBJECT_FISHED && !$this->map) + { + $nodeIds = array_map(fn($x) => $x['id'], $tabData['data']); + $fishedIn = new GameObjectList(array(['id', $nodeIds])); + if (!$fishedIn->error) + { + // show mapper for fishing locations + if ($nodeSpawns = $fishedIn->getSpawns(SPAWNINFO_FULL, true, true, true, true)) + { + $this->map = array( + ['parent' => 'mapper-generic'], // Mapper + $nodeSpawns, // mapperData + null, // ShowOnMap + [Lang::item('fishedIn')], // foundIn + Lang::item('fishingLoc') // title + ); + foreach ($nodeSpawns as $areaId => $_) + $this->map[3][$areaId] = ZoneList::getName($areaId); + } } } @@ -473,24 +506,25 @@ class ItemBaseResponse extends TemplateResponse implements ICache // tabs: this item contains.. $sourceFor = array( - [LOOT_ITEM, $this->subject->id, '$LANG.tab_contains', 'contains', ['$Listview.extraCols.percent'], [] ], - [LOOT_PROSPECTING, $this->subject->id, '$LANG.tab_prospecting', 'prospecting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']], - [LOOT_MILLING, $this->subject->id, '$LANG.tab_milling', 'milling', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']], - [LOOT_DISENCHANT, $this->subject->getField('disenchantId'), '$LANG.tab_disenchanting', 'disenchanting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']] + [Loot::ITEM, $this->typeId, '$LANG.tab_contains', 'contains', ['$Listview.extraCols.percent'], [] ], + [Loot::PROSPECTING, $this->typeId, '$LANG.tab_prospecting', 'prospecting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']], + [Loot::MILLING, $this->typeId, '$LANG.tab_milling', 'milling', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']], + [Loot::DISENCHANT, $this->subject->getField('disenchantId'), '$LANG.tab_disenchanting', 'disenchanting', ['$Listview.extraCols.percent'], ['side', 'slot', 'reqlevel']] ); foreach ($sourceFor as [$lootTemplate, $lootId, $tabName, $tabId, $extraCols, $hiddenCols]) { - $lootTab = new Loot(); - if ($lootTab->getByContainer($lootTemplate, $lootId)) + $lootTab = new LootByContainer(); + if ($lootTab->getByContainer($lootTemplate, [$lootId])) { $this->extendGlobalData($lootTab->jsGlobals); $extraCols = array_merge($extraCols, $lootTab->extraCols); $tabData = array( - 'data' => $lootTab->getResult(), - 'name' => $tabName, - 'id' => $tabId, + 'data' => $lootTab->getResult(), + 'name' => $tabName, + 'id' => $tabId, + 'computeDataFunc' => '$Listview.funcBox.initLootTable' ); if ($extraCols) @@ -503,10 +537,39 @@ class ItemBaseResponse extends TemplateResponse implements ICache } } + // append spell loot mimicking item opening + if ($this->subject->getField('spellTrigger1') === SPELL_TRIGGER_USE && ($s = $this->subject->getField('spellId1'))) + { + if (($spellLoot = new LootByContainer())->getByContainer(Loot::SPELL, [$s])) + { + $this->extendGlobalData($spellLoot->jsGlobals); + + $makeNew = true; + foreach ($this->lvTabs->iterate() as $k => $tab) + { + if ($tab->getId() != 'contains') + continue; + + $tab->appendData($spellLoot->getResult()); + $makeNew = false; + break; + } + + if ($makeNew) + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $spellLoot->getResult(), + 'name' => '$LANG.tab_contains', + 'id' => 'contains', + 'computeDataFunc' => '$Listview.funcBox.initLootTable', + 'extraCols' => array_merge(['$Listview.extraCols.percent'], $spellLoot->extraCols) + ), ItemList::$brickFile)); + } + } + // tab: container can contain if ($this->subject->getField('slots') > 0) { - $contains = new ItemList(array(['bagFamily', $_bagFamily, '&'], ['slots', 1, '<'], Cfg::get('SQL_LIMIT_NONE'))); + $contains = new ItemList(array(['bagFamily', $_bagFamily, '&'], ['slots', 1, '<'])); if (!$contains->error) { $this->extendGlobalData($contains->getJSGlobals(GLOBALINFO_SELF)); @@ -527,7 +590,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache // tab: can be contained in (except keys) else if ($_bagFamily != 0x0100) { - $contains = new ItemList(array(['bagFamily', $_bagFamily, '&'], ['slots', 0, '>'], Cfg::get('SQL_LIMIT_NONE'))); + $contains = new ItemList(array(['bagFamily', $_bagFamily, '&'], ['slots', 0, '>'])); if (!$contains->error) { $this->extendGlobalData($contains->getJSGlobals(GLOBALINFO_SELF)); @@ -567,7 +630,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache // tab: reagent for $conditions = array( - 'OR', + DB::OR, ['reagent1', $this->typeId], ['reagent2', $this->typeId], ['reagent3', $this->typeId], ['reagent4', $this->typeId], ['reagent5', $this->typeId], ['reagent6', $this->typeId], ['reagent7', $this->typeId], ['reagent8', $this->typeId] ); @@ -585,12 +648,14 @@ class ItemBaseResponse extends TemplateResponse implements ICache ), SpellList::$brickFile)); } - // tab: unlocks (object or item) - LOCK_TYPE_ITEM: 1 + // tab: unlocks (object or item) $lockIds = DB::Aowow()->selectCol( - 'SELECT `id` FROM ?_lock WHERE (`type1` = 1 AND `properties1` = ?d) OR - (`type2` = 1 AND `properties2` = ?d) OR (`type3` = 1 AND `properties3` = ?d) OR - (`type4` = 1 AND `properties4` = ?d) OR (`type5` = 1 AND `properties5` = ?d)', - $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId + 'SELECT `id` FROM ::lock WHERE (`type1` = %i AND `properties1` = %i) OR + (`type2` = %i AND `properties2` = %i) OR (`type3` = %i AND `properties3` = %i) OR + (`type4` = %i AND `properties4` = %i) OR (`type5` = %i AND `properties5` = %i)', + LOCK_TYPE_ITEM, $this->typeId, LOCK_TYPE_ITEM, $this->typeId, + LOCK_TYPE_ITEM, $this->typeId, LOCK_TYPE_ITEM, $this->typeId, + LOCK_TYPE_ITEM, $this->typeId ); if ($lockIds) @@ -621,40 +686,6 @@ class ItemBaseResponse extends TemplateResponse implements ICache } } - // tab: see also - $conditions = array( - ['id', $this->typeId, '!'], - [ - 'OR', - ['name_loc'.Lang::getLocale()->value, $this->subject->getField('name', true)], - [ - 'AND', - ['class', $_class], - ['subClass', $_subClass], - ['slot', $_slot], - ['itemLevel', $_ilvl - 15, '>'], - ['itemLevel', $_ilvl + 15, '<'], - ['quality', $this->subject->getField('quality')], - ['requiredClass', $this->subject->getField('requiredClass') ?: -1] // todo: fix db data in setup and not on fetch - ] - ] - ); - - if ($_ = $this->subject->getField('itemset')) - $conditions[1][] = ['AND', ['slot', $_slot], ['itemset', $_]]; - - $saItems = new ItemList($conditions); - if (!$saItems->error) - { - $this->extendGlobalData($saItems->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs->addListviewTab(new Listview(array( - 'data' => $saItems->getListviewData(), - 'name' => '$LANG.tab_seealso', - 'id' => 'see-also' - ), ItemList::$brickFile)); - } - // tab: starts (quest) if ($qId = $this->subject->getField('startQuest')) { @@ -673,7 +704,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache // tab: objective of (quest) $conditions = array( - 'OR', + DB::OR, ['reqItemId1', $this->typeId], ['reqItemId2', $this->typeId], ['reqItemId3', $this->typeId], ['reqItemId4', $this->typeId], ['reqItemId5', $this->typeId], ['reqItemId6', $this->typeId] ); @@ -691,7 +722,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache // tab: provided for (quest) $conditions = array( - 'OR', ['sourceItemId', $this->typeId], + DB::OR, ['sourceItemId', $this->typeId], ['reqSourceItemId1', $this->typeId], ['reqSourceItemId2', $this->typeId], ['reqSourceItemId3', $this->typeId], ['reqSourceItemId4', $this->typeId] ); @@ -707,28 +738,10 @@ class ItemBaseResponse extends TemplateResponse implements ICache ), QuestList::$brickFile)); } - // tab: same model as - // todo (low): should also work for creatures summoned by item - if (($model = $this->subject->getField('model')) && $_slot) - { - $sameModel = new ItemList(array(['model', $model], ['id', $this->typeId, '!'], ['slot', $_slot])); - if (!$sameModel->error) - { - $this->extendGlobalData($sameModel->getJSGlobals(GLOBALINFO_SELF)); - - $this->lvTabs->addListviewTab(new Listview(array( - 'data' => $sameModel->getListviewData(ITEMINFO_MODEL), - 'name' => '$LANG.tab_samemodelas', - 'id' => 'same-model-as', - 'genericlinktype' => 'item' - ), 'genericmodel')); - } - } - // tab: sold by - if (!empty($this->subject->getExtendedCost()[$this->subject->id])) + if (!empty($this->subject->getExtendedCost()[$this->typeId])) { - $vendors = $this->subject->getExtendedCost()[$this->subject->id]; + $vendors = $this->subject->getExtendedCost()[$this->typeId]; $soldBy = new CreatureList(array(['id', array_keys($vendors)])); if (!$soldBy->error) { @@ -736,13 +749,14 @@ class ItemBaseResponse extends TemplateResponse implements ICache if ($vendorSpawns = $soldBy->getSpawns(SPAWNINFO_FULL, true, true, true, true)) { $this->map = array( - ['parent' => 'mapper-generic'], // Mapper - $vendorSpawns, // mapperData - null, // ShowOnMap - [Lang::item('purchasedIn')] // foundIn + ['parent' => 'mapper-generic'], // Mapper + $vendorSpawns, // mapperData + null, // ShowOnMap + [Lang::item('purchasedIn')], // foundIn + Lang::item('vendorLoc') // title ); foreach ($vendorSpawns as $areaId => $_) - $this->map['extra'][$areaId] = ZoneList::getName($areaId); + $this->map[3][$areaId] = ZoneList::getName($areaId); } $sbData = $soldBy->getListviewData(); @@ -751,7 +765,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache $extraCols = ['$Listview.extraCols.stock', "\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost']; $cnd = new Conditions(); - $cnd->getBySourceEntry($this->typeId, Conditions::SRC_NPC_VENDOR)->prepare(); + $cnd->getBySource(Conditions::SRC_NPC_VENDOR, entry: $this->typeId)->prepare(); foreach ($sbData as $k => &$row) { $currency = []; @@ -824,8 +838,8 @@ class ItemBaseResponse extends TemplateResponse implements ICache if (!$n && !is_null(ItemListFilter::getCriteriaIndex(158, $this->typeId))) $n = '?items&filter=cr=158;crs='.$this->typeId.';crv=0'; - $xCosts = DB::Aowow()->selectCol('SELECT `id` FROM ?_itemextendedcost WHERE '.$w); - $boughtBy = $xCosts ? DB::World()->selectCol('SELECT `item` FROM npc_vendor WHERE `extendedCost` IN (?a) UNION SELECT `item` FROM game_event_npc_vendor WHERE `extendedCost` IN (?a)', $xCosts, $xCosts) : null; + $xCosts = DB::Aowow()->selectCol('SELECT `id` FROM ::itemextendedcost WHERE '.$w); + $boughtBy = $xCosts ? DB::World()->selectCol('SELECT `item` FROM npc_vendor WHERE `extendedCost` IN %in UNION SELECT `item` FROM game_event_npc_vendor WHERE `extendedCost` IN %in', $xCosts, $xCosts) : null; if ($boughtBy) { $boughtBy = new ItemList(array(['id', $boughtBy])); @@ -892,6 +906,58 @@ class ItemBaseResponse extends TemplateResponse implements ICache } } + // tab: see also + $conditions = array( + ['id', $this->typeId, '!'], + [ + DB::OR, + ['name_loc'.Lang::getLocale()->value, $this->subject->getField('name', true)], + [ + DB::AND, + ['class', $_class], + ['subClass', $_subClass], + ['slot', $_slot], + ['itemLevel', $_ilvl - 15, '>'], + ['itemLevel', $_ilvl + 15, '<'], + ['quality', $this->subject->getField('quality')], + ['requiredClass', $this->subject->getField('requiredClass')] + ] + ] + ); + + if ($_ = $this->subject->getField('itemset')) + $conditions[1][] = [DB::AND, ['slot', $_slot], ['itemset', $_]]; + + $saItems = new ItemList($conditions); + if (!$saItems->error) + { + $this->extendGlobalData($saItems->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $saItems->getListviewData(), + 'name' => '$LANG.tab_seealso', + 'id' => 'see-also' + ), ItemList::$brickFile)); + } + + // tab: same model as + // todo (low): should also work for creatures summoned by item + if (($model = $this->subject->getField('model')) && $_slot) + { + $sameModel = new ItemList(array(['model', $model], ['id', $this->typeId, '!'], ['slot', $_slot])); + if (!$sameModel->error) + { + $this->extendGlobalData($sameModel->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $sameModel->getListviewData(ITEMINFO_MODEL), + 'name' => '$LANG.tab_samemodelas', + 'id' => 'same-model-as', + 'genericlinktype' => 'item' + ), 'genericmodel')); + } + } + // tab: Shared cooldown $cdCats = []; $useSpells = []; @@ -906,7 +972,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache $useSpells[] = $this->subject->getField('spellId'.$i); } if ($useSpells) - if ($_ = DB::Aowow()->selectCol('SELECT `category` FROM ?_spell WHERE `id` IN (?a) AND `recoveryCategory` > 0', $useSpells)) + if ($_ = DB::Aowow()->selectCol('SELECT `category` FROM ::spell WHERE `id` IN %in AND `recoveryCategory` > 0', $useSpells)) $cdCats += $_; if ($cdCats) @@ -914,7 +980,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache $conditions = array( ['id', $this->typeId, '!'], [ - 'OR', + DB::OR, ['spellCategory1', $cdCats], ['spellCategory2', $cdCats], ['spellCategory3', $cdCats], @@ -923,7 +989,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache ] ); - if ($spellsByCat = DB::Aowow()->selectCol('SELECT `id` FROM ?_spell WHERE `category` IN (?a)', $cdCats)) + if ($spellsByCat = DB::Aowow()->selectCol('SELECT `id` FROM ::spell WHERE `category` IN %in', $cdCats)) for ($i = 1; $i < 6; $i++) $conditions[1][] = ['spellId'.$i, $spellsByCat]; @@ -948,7 +1014,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache if ($this->subject->getField('soundOverrideSubclass') > 0) $scm = (1 << $this->subject->getField('soundOverrideSubclass')); - $soundIds = DB::Aowow()->selectCol('SELECT `soundId` FROM ?_items_sounds WHERE `subClassMask` & ?d', $scm); + $soundIds = DB::Aowow()->selectCol('SELECT `soundId` FROM ::items_sounds WHERE `subClassMask` & %i', $scm); } $fields = ['pickUpSoundId', 'dropDownSoundId', 'sheatheSoundId', 'unsheatheSoundId']; @@ -958,7 +1024,7 @@ class ItemBaseResponse extends TemplateResponse implements ICache if ($x = $this->subject->getField('spellVisualId')) { - if ($spellSounds = DB::Aowow()->selectRow('SELECT * FROM ?_spell_sounds WHERE `id` = ?d', $x)) + if ($spellSounds = DB::Aowow()->selectRow('SELECT * FROM ::spell_sounds WHERE `id` = %i', $x)) { array_shift($spellSounds); // bye 'id'-field foreach ($spellSounds as $ss) diff --git a/endpoints/item/item_xml.php b/endpoints/item/item_xml.php index 5599be5c..19997124 100644 --- a/endpoints/item/item_xml.php +++ b/endpoints/item/item_xml.php @@ -148,10 +148,10 @@ class ItemXmlResponse extends TextResponse implements ICache // reagents $cnd = array( - 'OR', - ['AND', ['effect1CreateItemId', $this->typeId], ['OR', ['effect1Id', SpellList::EFFECTS_ITEM_CREATE], ['effect1AuraId', SpellList::AURAS_ITEM_CREATE]]], - ['AND', ['effect2CreateItemId', $this->typeId], ['OR', ['effect2Id', SpellList::EFFECTS_ITEM_CREATE], ['effect2AuraId', SpellList::AURAS_ITEM_CREATE]]], - ['AND', ['effect3CreateItemId', $this->typeId], ['OR', ['effect3Id', SpellList::EFFECTS_ITEM_CREATE], ['effect3AuraId', SpellList::AURAS_ITEM_CREATE]]], + DB::OR, + [DB::AND, ['effect1CreateItemId', $this->typeId], [DB::OR, ['effect1Id', SpellList::EFFECTS_ITEM_CREATE], ['effect1AuraId', SpellList::AURAS_ITEM_CREATE]]], + [DB::AND, ['effect2CreateItemId', $this->typeId], [DB::OR, ['effect2Id', SpellList::EFFECTS_ITEM_CREATE], ['effect2AuraId', SpellList::AURAS_ITEM_CREATE]]], + [DB::AND, ['effect3CreateItemId', $this->typeId], [DB::OR, ['effect3Id', SpellList::EFFECTS_ITEM_CREATE], ['effect3AuraId', SpellList::AURAS_ITEM_CREATE]]], ); $spellSource = new SpellList($cnd); @@ -192,7 +192,7 @@ class ItemXmlResponse extends TextResponse implements ICache } // link - $xml->addChild('link', Cfg::get('HOST_URL').'?item='.$this->subject->id); + $xml->addChild('link', Cfg::get('HOST_URL').'?item='.$this->typeId); $this->result = $root->asXML(); } @@ -220,8 +220,7 @@ class ItemXmlResponse extends TextResponse implements ICache { return array( $this->type, // DBType - $this->typeId, // DBTypeId - -1, // category + $this->typeId, // DBTypeId/category -1, // staff mask (content does not diff) '' // misc (unused) ); diff --git a/endpoints/items/items.php b/endpoints/items/items.php index 21aff287..0f5f48fd 100644 --- a/endpoints/items/items.php +++ b/endpoints/items/items.php @@ -11,7 +11,7 @@ class ItemsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::ITEM; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'items'; protected string $pageName = 'items'; @@ -91,14 +91,22 @@ class ItemsBaseResponse extends TemplateResponse implements ICache 'extraCols' => [] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new ItemListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -106,18 +114,13 @@ class ItemsBaseResponse extends TemplateResponse implements ICache { $this->h1 = Util::ucFirst(Lang::game('items')); - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - $this->filter->evalWeights(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /*******************/ /* evaluate filter */ @@ -188,7 +191,7 @@ class ItemsBaseResponse extends TemplateResponse implements ICache /* handle auto-gemming */ /***********************/ - $this->createGemScores($fiForm['gm'] ?? 0); + $this->createGemScores(); /*************************/ @@ -254,7 +257,7 @@ class ItemsBaseResponse extends TemplateResponse implements ICache $nameSource = []; $grouping = $fiForm['gb'] ?? ItemListFilter::GROUP_BY_NONE; $extraOpts = []; - $maxResults = Cfg::get('SQL_LIMIT_DEFAULT'); + $maxResults = Listview::DEFAULT_SIZE; $forceTabs = false; $tabs = []; @@ -286,7 +289,7 @@ class ItemsBaseResponse extends TemplateResponse implements ICache case ItemListFilter::GROUP_BY_LEVEL: // itemlevel: first, try to find 10 level steps within range (if given) as tabs // ohkayy, maybe i need to rethink $this $this->filterOpts = $this->filter->extraOpts; - $this->filterOpts['is']['o'] = [null]; // remove 'order by' from ?_item_stats + $this->filterOpts['is']['o'] = [null]; // remove 'order by' from ::item_stats $extraOpts = array_merge($this->filterOpts, ['i' => ['g' => ['itemlevel'], 'o' => ['itemlevel DESC']]]); $levelRef = new ItemList(array_merge($conditions, [10]), ['extraOpts' => $extraOpts]); @@ -343,8 +346,17 @@ class ItemsBaseResponse extends TemplateResponse implements ICache if ($items->error) continue; + // if sold by vendor; append cost column + if ($this->filter->getSetCriteria(92) && is_array($this->sharedLV['extraCols'])) + { + $this->sharedLV['extraCols']['cost'] = '$Listview.extraCols.cost'; + $data = $items->getListviewData($infoMask | ITEMINFO_VENDOR); + } + else + $data = $items->getListviewData($infoMask); + $tabData = array_merge( - ['data' => $items->getListviewData($infoMask)], + ['data' => $data], $this->sharedLV ); $this->extendGlobalData($items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); @@ -425,7 +437,7 @@ class ItemsBaseResponse extends TemplateResponse implements ICache } else if ($items->getMatches() > $maxResults) { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_itemsfound', $items->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_itemsfound', $items->getMatches(), Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } @@ -462,7 +474,7 @@ class ItemsBaseResponse extends TemplateResponse implements ICache } // fetch best possible gems for chosen weights - private function createGemScores(int $gemQuality) : void + private function createGemScores() : void { if (!$this->filter->fiSetWeights) return; @@ -473,15 +485,15 @@ class ItemsBaseResponse extends TemplateResponse implements ICache array_push($this->sharedLV['hiddenCols'], 'type', 'source'); - if (!$gemQuality) + if (!$this->filter->values['gm']) return; $this->sharedLV['computeDataFunc'] = '$fi_scoreSockets'; - $q = intVal($gemQuality); + $q = $this->filter->values['gm']; $mask = 0xE; $cnd = [10, ['class', ITEM_CLASS_GEM], ['gemColorMask', &$mask, '&'], ['quality', &$q]]; - if (!isset($fiForm['jc'])) + if (!$this->filter->values['jc']) $cnd[] = ['itemLimitCategory', 0]; // Jeweler's Gems if ($this->filter->wtCnd) @@ -497,7 +509,7 @@ class ItemsBaseResponse extends TemplateResponse implements ICache for ($i = 0; $i < 4; $i++) { $mask = 1 << $i; - $q = !$i ? ITEM_QUALITY_RARE : intVal($gemQuality); // meta gems are always included.. ($q is backReferenced) + $q = !$i ? ITEM_QUALITY_RARE : $this->filter->values['gm']; // meta gems are always included.. ($q is backReferenced) $byColor = new ItemList($cnd, ['extraOpts' => $this->filter->extraOpts]); if (!$byColor->error) { diff --git a/endpoints/itemset/itemset.php b/endpoints/itemset/itemset.php index 398fe55e..c86ae527 100644 --- a/endpoints/itemset/itemset.php +++ b/endpoints/itemset/itemset.php @@ -10,7 +10,7 @@ class ItemsetBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'itemset'; protected string $pageName = 'itemset'; @@ -95,17 +95,10 @@ class ItemsetBaseResponse extends TemplateResponse implements ICache // itemLevel if ($min = $this->subject->getField('minLevel')) - { - $foo = Lang::game('level').Lang::main('colon').$min; - $max = $this->subject->getField('maxLevel'); - - if ($min < $max) - $foo .= ' - '.$max; - - $infobox[] = $foo; - } + $infobox[] = Lang::game('level').Lang::main('colon').Util::createNumRange($min, $this->subject->getField('maxLevel'), ' - '); // class + $jsg = []; if ($cl = Lang::getClassString($this->subject->getField('classMask'), $jsg, Lang::FMT_MARKUP)) { $this->extendGlobalIds(Type::CHR_CLASS, ...$jsg); @@ -125,6 +118,13 @@ class ItemsetBaseResponse extends TemplateResponse implements ICache if ($_ta) $infobox[] = Lang::itemset('_tag').'[url=?itemsets&filter=ta='.$_ta.']'.Lang::itemset('notes', $_ta).'[/url]'; + // id + $infobox[] = Lang::itemset('id') . $this->subject->getField('refSetId'); + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); diff --git a/endpoints/itemsets/itemsets.php b/endpoints/itemsets/itemsets.php index a0acd354..945e5100 100644 --- a/endpoints/itemsets/itemsets.php +++ b/endpoints/itemsets/itemsets.php @@ -11,7 +11,7 @@ class ItemsetsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::ITEMSET; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'itemsets'; protected string $pageName = 'itemsets'; @@ -24,14 +24,22 @@ class ItemsetsBaseResponse extends TemplateResponse implements ICache 'filter' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => Filter::PATTERN_PARAM]] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new ItemsetListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -39,17 +47,13 @@ class ItemsetsBaseResponse extends TemplateResponse implements ICache { $this->h1 = Util::ucWords(Lang::game('itemsets')); - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /*************/ /* Menu Path */ @@ -86,9 +90,9 @@ class ItemsetsBaseResponse extends TemplateResponse implements ICache $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; // create note if search limit was exceeded - if ($itemsets->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($itemsets->getMatches() > Listview::DEFAULT_SIZE) { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_itemsetsfound', $itemsets->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_itemsetsfound', $itemsets->getMatches(), Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } diff --git a/endpoints/latest-comments/latest-comments.php b/endpoints/latest-comments/latest-comments.php index da742f32..fbb809db 100644 --- a/endpoints/latest-comments/latest-comments.php +++ b/endpoints/latest-comments/latest-comments.php @@ -33,10 +33,10 @@ class LatestcommentsBaseResponse extends TemplateResponse $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); - $comments = CommunityContent::getCommentPreviews(['comments' => true, 'replies' => false]); + $comments = CommunityContent::getCommentPreviews(['comments' => true, 'replies' => false], resultLimit: Listview::DEFAULT_SIZE); $this->lvTabs->addListviewTab(new Listview(['data' => $comments], 'commentpreview')); - $replies = CommunityContent::getCommentPreviews(['comments' => false, 'replies' => true]); + $replies = CommunityContent::getCommentPreviews(['comments' => false, 'replies' => true], resultLimit: Listview::DEFAULT_SIZE); $this->lvTabs->addListviewTab(new Listview(['data' => $replies], 'replypreview')); parent::generate(); diff --git a/endpoints/latest-comments/latest-comments_rss.php b/endpoints/latest-comments/latest-comments_rss.php index d3611151..20b61fa5 100644 --- a/endpoints/latest-comments/latest-comments_rss.php +++ b/endpoints/latest-comments/latest-comments_rss.php @@ -14,15 +14,22 @@ class LatestcommentsRssResponse extends TextResponse protected function generate() : void { - foreach (CommunityContent::getCommentPreviews(dateFmt: false) as $comment) + $now = new DateTime(); + + foreach (CommunityContent::getCommentPreviews(['comments' => 1, 'replies' => 1], dateFmt: false, resultLimit: 100) as $comment) { + if (empty($comment['commentid'])) + $url = Cfg::get('HOST_URL').'/?go-to-comment&id='.$comment['id']; + else + $url = Cfg::get('HOST_URL').'/?go-to-reply&id='.$comment['id']; + // todo (low): preview should be html-formated $this->feedData[] = array( 'title' => [true, [], Lang::typeName($comment['type']).Lang::main('colon').htmlentities($comment['subject'])], - 'link' => [false, [], Cfg::get('HOST_URL').'/?go-to-comment&id='.$comment['id']], - 'description' => [true, [], htmlentities($comment['preview'])."<br /><br />".Lang::main('byUser', [$comment['user'], '']) . Util::formatTimeDiff($comment['date'])], + 'link' => [false, [], $url], + 'description' => [true, [], htmlentities($comment['preview'])."<br /><br />".Lang::main('byUser', [$comment['user'], '']) . $now->formatDate($comment['date'], true)], 'pubDate' => [false, [], date(DATE_RSS, $comment['date'])], - 'guid' => [false, [], Cfg::get('HOST_URL').'/?go-to-comment&id='.$comment['id']] + 'guid' => [false, [], $url] // 'domain' => [false, [], null] ); } diff --git a/endpoints/latest-screenshots/latest-screenshots.php b/endpoints/latest-screenshots/latest-screenshots.php index cc08465f..6e8ba355 100644 --- a/endpoints/latest-screenshots/latest-screenshots.php +++ b/endpoints/latest-screenshots/latest-screenshots.php @@ -33,7 +33,7 @@ class LatestscreenshotsBaseResponse extends TemplateResponse $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); - $data = CommunityContent::getScreenshots(); + $data = CommunityContent::getScreenshots(resultLimit: Listview::DEFAULT_SIZE); $this->lvTabs->addListviewTab(new Listview(['data' => $data], 'screenshot')); parent::generate(); diff --git a/endpoints/latest-screenshots/latest-screenshots_rss.php b/endpoints/latest-screenshots/latest-screenshots_rss.php index a91c0127..50e6f215 100644 --- a/endpoints/latest-screenshots/latest-screenshots_rss.php +++ b/endpoints/latest-screenshots/latest-screenshots_rss.php @@ -14,12 +14,14 @@ class LatestscreenshotsRssResponse extends TextResponse protected function generate() : void { - foreach (CommunityContent::getScreenshots(dateFmt: false) as $screenshot) + $now = new DateTime(); + + foreach (CommunityContent::getScreenshots(dateFmt: false, resultLimit: 100) as $screenshot) { $desc = '<a href="'.Cfg::get('HOST_URL').'/?'.Type::getFileString($screenshot['type']).'='.$screenshot['typeId'].'#screenshots:id='.$screenshot['id'].'"><img src="'.Cfg::get('STATIC_URL').'/uploads/screenshots/thumb/'.$screenshot['id'].'.jpg" alt="" /></a>'; if ($screenshot['caption']) $desc .= '<br />'.$screenshot['caption']; - $desc .= "<br /><br />".Lang::main('byUser', [$screenshot['user'], '']) . Util::formatTimeDiff($screenshot['date'], true); + $desc .= "<br /><br />".Lang::main('byUser', [$screenshot['user'], '']) . $now->formatDate($screenshot['date'], true); // enclosure/length => filesize('static/uploads/screenshots/thumb/'.$screenshot['id'].'.jpg') .. always set to this placeholder value though $this->feedData[] = array( diff --git a/endpoints/latest-videos/latest-videos.php b/endpoints/latest-videos/latest-videos.php index 24406460..610552e3 100644 --- a/endpoints/latest-videos/latest-videos.php +++ b/endpoints/latest-videos/latest-videos.php @@ -33,7 +33,7 @@ class LatestvideosBaseResponse extends TemplateResponse $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); - $data = CommunityContent::getVideos(); + $data = CommunityContent::getVideos(resultLimit: Listview::DEFAULT_SIZE); $this->lvTabs->addListviewTab(new Listview(['data' => $data], 'video')); parent::generate(); diff --git a/endpoints/latest-videos/latest-videos_rss.php b/endpoints/latest-videos/latest-videos_rss.php index 4fd63165..5e3980ec 100644 --- a/endpoints/latest-videos/latest-videos_rss.php +++ b/endpoints/latest-videos/latest-videos_rss.php @@ -14,12 +14,14 @@ class LatestvideosRssResponse extends TextResponse protected function generate() : void { - foreach (CommunityContent::getvideos(dateFmt: false) as $video) + $now = new DateTime(); + + foreach (CommunityContent::getvideos(dateFmt: false, resultLimit: 100) as $video) { $desc = '<a href="'.Cfg::get('HOST_URL').'/?'.Type::getFileString($video['type']).'='.$video['typeId'].'#videos:id='.$video['id'].'"><img src="//i3.ytimg.com/vi/'.$video['videoId'].'/default.jpg" alt="" /></a>'; if ($video['caption']) $desc .= '<br />'.$video['caption']; - $desc .= "<br /><br />".Lang::main('byUser', [$video['user'], '']) . Util::formatTimeDiff($video['date'], true); + $desc .= "<br /><br />".Lang::main('byUser', [$video['user'], '']) . $now->formatDate($video['date'], true); // is enclosure/length .. is this even relevant..? $this->feedData[] = array( diff --git a/endpoints/locale/locale.php b/endpoints/locale/locale.php index fccf2962..260ae500 100644 --- a/endpoints/locale/locale.php +++ b/endpoints/locale/locale.php @@ -9,7 +9,7 @@ if (!defined('AOWOW_REVISION')) class LocaleBaseResponse extends TextResponse { protected array $expectedGET = array( - 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFrom']] + 'locale' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkLocale']] ); protected function generate() : void diff --git a/endpoints/mail/mail.php b/endpoints/mail/mail.php index 705c6aca..0b6fe31b 100644 --- a/endpoints/mail/mail.php +++ b/endpoints/mail/mail.php @@ -10,7 +10,7 @@ class MailBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'mail'; @@ -63,51 +63,56 @@ class MailBaseResponse extends TemplateResponse implements ICache // sender + delay if ($this->typeId < 0) // def. achievement { - if ($npcId = DB::World()->selectCell('SELECT `Sender` FROM achievement_reward WHERE `ID` = ?d', -$this->typeId)) + if ($npcId = DB::World()->selectCell('SELECT `Sender` FROM achievement_reward WHERE `ID` = %i', -$this->typeId)) { $infobox[] = Lang::mail('sender', ['[npc='.$npcId.']']); $this->extendGlobalIds(Type::NPC, $npcId); } } - else if ($mlr = DB::World()->selectRow('SELECT * FROM mail_level_reward WHERE `mailTemplateId` = ?d', $this->typeId)) // level rewards + else if ($mlr = DB::World()->selectRow('SELECT * FROM mail_level_reward WHERE `mailTemplateId` = %i', $this->typeId)) // level rewards { - if ($mlr['level']) - $infobox[] = Lang::game('level').Lang::main('colon').$mlr['level']; + if ($mlr['level']) + $infobox[] = Lang::game('level').Lang::main('colon').$mlr['level']; - if ($r = Lang::getRaceString($mlr['raceMask'], $rIds, Lang::FMT_MARKUP)) - { - $infobox[] = Lang::game('races').Lang::main('colon').$r; - $this->extendGlobalIds(Type::CHR_RACE, ...$rIds); - } + $jsg = []; + if ($r = Lang::getRaceString($mlr['raceMask'], $jsg, Lang::FMT_MARKUP)) + { + $this->extendGlobalIds(Type::CHR_RACE, ...$jsg); + $t = count($jsg) == 1 ? Lang::game('race') : Lang::game('races'); + $infobox[] = Util::ucFirst($t).Lang::main('colon').$r; + } - $infobox[] = Lang::mail('sender', ['[npc='.$mlr['senderEntry'].']']); - $this->extendGlobalIds(Type::NPC, $mlr['senderEntry']); + $infobox[] = Lang::mail('sender', ['[npc='.$mlr['senderEntry'].']']); + $this->extendGlobalIds(Type::NPC, $mlr['senderEntry']); } else // achievement or quest { - if ($q = DB::Aowow()->selectRow('SELECT `id`, `rewardMailDelay` FROM ?_quests WHERE `rewardMailTemplateId` = ?d', $this->typeId)) + if ($q = DB::Aowow()->selectRow('SELECT `id`, `rewardMailDelay` FROM ::quests WHERE `rewardMailTemplateId` = %i', $this->typeId)) { - if ($npcId= DB::World()->selectCell('SELECT `RewardMailSenderEntry` FROM quest_mail_sender WHERE `QuestId` = ?d', $q['id'])) + if ($npcId= DB::World()->selectCell('SELECT `RewardMailSenderEntry` FROM quest_mail_sender WHERE `QuestId` = %i', $q['id'])) { $infobox[] = Lang::mail('sender', ['[npc='.$npcId.']']); $this->extendGlobalIds(Type::NPC, $npcId); } - else if ($npcId = DB::Aowow()->selectCell('SELECT `typeId` FROM ?_quests_startend WHERE `questId` = ?d AND `type` = ?d AND `method` & ?d', $q['id'], Type::NPC, 0x2)) + else if ($npcId = DB::Aowow()->selectCell('SELECT `typeId` FROM ::quests_startend WHERE `questId` = %i AND `type` = %i AND `method` & %i', $q['id'], Type::NPC, 0x2)) { $infobox[] = Lang::mail('sender', ['[npc='.$npcId.']']); $this->extendGlobalIds(Type::NPC, $npcId); } if ($q['rewardMailDelay'] > 0) - $infobox[] = Lang::mail('delay', [Util::formatTime($q['rewardMailDelay'] * 1000)]); + $infobox[] = Lang::mail('delay', [DateTime::formatTimeElapsed($q['rewardMailDelay'] * 1000)]); } - else if ($npcId = DB::World()->selectCell('SELECT `Sender` FROM achievement_reward WHERE `MailTemplateId` = ?d', $this->typeId)) + else if ($npcId = DB::World()->selectCell('SELECT `Sender` FROM achievement_reward WHERE `MailTemplateId` = %i', $this->typeId)) { $infobox[] = Lang::mail('sender', ['[npc='.$npcId.']']); $this->extendGlobalIds(Type::NPC, $npcId); } } + // id + $infobox[] = Lang::mail('id') . $this->typeId; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); @@ -146,7 +151,7 @@ class MailBaseResponse extends TemplateResponse implements ICache } if ($this->typeId < 0 || // used by: achievement - ($acvId = DB::World()->selectCell('SELECT `ID` FROM achievement_reward WHERE `MailTemplateId` = ?d', $this->typeId))) + ($acvId = DB::World()->selectCell('SELECT `ID` FROM achievement_reward WHERE `MailTemplateId` = %i', $this->typeId))) { $ubAchievements = new AchievementList(array(['id', $this->typeId < 0 ? -$this->typeId : $acvId])); if (!$ubAchievements->error) diff --git a/endpoints/mails/mails.php b/endpoints/mails/mails.php index e892948f..e1319aca 100644 --- a/endpoints/mails/mails.php +++ b/endpoints/mails/mails.php @@ -11,18 +11,18 @@ class MailsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::MAIL; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'mails'; protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 103]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void diff --git a/endpoints/maps/maps.php b/endpoints/maps/maps.php index de62e4bc..0f99f470 100644 --- a/endpoints/maps/maps.php +++ b/endpoints/maps/maps.php @@ -8,8 +8,6 @@ if (!defined('AOWOW_REVISION')) class MapsBaseResponse extends TemplateResponse { - protected int $cacheType = CACHE_TYPE_PAGE; - protected string $template = 'maps'; protected string $pageName = 'maps'; protected ?int $activeTab = parent::TAB_TOOLS; diff --git a/endpoints/most-comments/most-comments.php b/endpoints/most-comments/most-comments.php index 7d4f178e..20985809 100644 --- a/endpoints/most-comments/most-comments.php +++ b/endpoints/most-comments/most-comments.php @@ -15,11 +15,11 @@ class MostcommentsBaseResponse extends TemplateResponse protected array $validCats = [1, 7, 30]; - public function __construct($pageParam) + public function __construct($rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function onInvalidCategory() : never @@ -68,8 +68,8 @@ class MostcommentsBaseResponse extends TemplateResponse foreach (Type::getClassesFor() as $type => $classStr) { $comments = DB::Aowow()->selectCol( - 'SELECT `typeId` AS ARRAY_KEY, COUNT(1) FROM ?_comments - WHERE `replyTo` = 0 AND (`flags` & ?d) = 0 AND `type`= ?d AND `date` > (UNIX_TIMESTAMP() - ?d) + 'SELECT `typeId` AS ARRAY_KEY, COUNT(1) FROM ::comments + WHERE `replyTo` = 0 AND (`flags` & %i) = 0 AND `type`= %i AND `date` > (UNIX_TIMESTAMP() - %i) GROUP BY `type`, `typeId` LIMIT 100', CC_FLAG_DELETED, diff --git a/endpoints/most-comments/most-comments_rss.php b/endpoints/most-comments/most-comments_rss.php index da5a1baf..1f99587d 100644 --- a/endpoints/most-comments/most-comments_rss.php +++ b/endpoints/most-comments/most-comments_rss.php @@ -14,9 +14,9 @@ class MostcommentsRssResponse extends TextResponse private array $validCats = [1, 7, 30]; - public function __construct($pageParam) + public function __construct($rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if ($this->params && !in_array($this->params[0], $this->validCats)) $this->forward('?most-comments=1&rss'); @@ -27,8 +27,8 @@ class MostcommentsRssResponse extends TextResponse foreach (Type::getClassesFor() as $type => $classStr) { $comments = DB::Aowow()->selectCol( - 'SELECT `typeId` AS ARRAY_KEY, COUNT(1) FROM ?_comments - WHERE `replyTo` = 0 AND (`flags` & ?d) = 0 AND `type`= ?d AND `date` > (UNIX_TIMESTAMP() - ?d) + 'SELECT `typeId` AS ARRAY_KEY, COUNT(1) FROM ::comments + WHERE `replyTo` = 0 AND (`flags` & %i) = 0 AND `type`= %i AND `date` > (UNIX_TIMESTAMP() - %i) GROUP BY `type`, `typeId` LIMIT 100', CC_FLAG_DELETED, diff --git a/endpoints/my-guides/my-guides.php b/endpoints/my-guides/my-guides.php index 288d87aa..f335369f 100644 --- a/endpoints/my-guides/my-guides.php +++ b/endpoints/my-guides/my-guides.php @@ -17,11 +17,11 @@ class MyguidesBaseResponse extends TemplateResponse protected ?int $activeTab = parent::TAB_GUIDES; // protected array $breadcrumb = [6]; // breadcrumb menu not displayed by WH.? - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - if (!User::isLoggedIn() || $pageParam) + if (!User::isLoggedIn() || $rawParam) $this->generateError(); } diff --git a/endpoints/npc/npc.php b/endpoints/npc/npc.php index 3d4624e2..1ddf45e5 100644 --- a/endpoints/npc/npc.php +++ b/endpoints/npc/npc.php @@ -10,7 +10,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'npc'; protected string $pageName = 'npc'; @@ -90,7 +90,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache $this->altNPCs = new CreatureList(array(['id', array_keys($_altIds)])); } - if ($_ = DB::World()->selectCol('SELECT DISTINCT `entry` FROM vehicle_template_accessory WHERE `accessory_entry` = ?d', $this->typeId)) + if ($_ = DB::World()->selectCol('SELECT DISTINCT `entry` FROM vehicle_template_accessory WHERE `accessory_entry` = %i', $this->typeId)) { $vehicles = new CreatureList(array(['id', $_])); foreach ($vehicles->iterate() as $id => $__) @@ -103,27 +103,26 @@ class NpcBaseResponse extends TemplateResponse implements ICache /**********************/ $mapType = 0; - if ($maps = DB::Aowow()->selectCol('SELECT DISTINCT `areaId` FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d', Type::NPC, $this->typeId)) + if ($maps = DB::Aowow()->selectCell('SELECT IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ::spawns WHERE `type` = %i AND `typeId` = %i', Type::NPC, $this->typeId)) { - if (count($maps) == 1) // should only exist in one instance - $mapType = match (DB::Aowow()->selectCell('SELECT `type` FROM ?_zones WHERE `id` = ?d', $maps[0])) - { - // MAP_TYPE_DUNGEON, - MAP_TYPE_DUNGEON_HC => 1, - // MAP_TYPE_RAID, - MAP_TYPE_MMODE_RAID, - MAP_TYPE_MMODE_RAID_HC => 2, - default => 0 - }; + $mapType = match (DB::Aowow()->selectCell('SELECT `type` FROM ::zones WHERE `id` = %i', $maps)) + { + // MAP_TYPE_DUNGEON, + MAP_TYPE_DUNGEON_HC => 1, + // MAP_TYPE_RAID, + MAP_TYPE_MMODE_RAID, + MAP_TYPE_MMODE_RAID_HC => 2, + default => 0 + }; } // npc is difficulty dummy: get max difficulty from parent npc - if ($this->placeholder && ($mt = DB::Aowow()->selectCell('SELECT IF(`difficultyEntry1` = ?d, 1, 2) FROM ?_creature WHERE `difficultyEntry1` = ?d OR `difficultyEntry2` = ?d OR `difficultyEntry3` = ?d', $this->typeId, $this->typeId, $this->typeId, $this->typeId))) + if ($this->placeholder && ($mt = DB::Aowow()->selectCell('SELECT IF(`difficultyEntry1` = %i, 1, 2) FROM ::creature WHERE `difficultyEntry1` = %i OR `difficultyEntry2` = %i OR `difficultyEntry3` = %i', $this->typeId, $this->typeId, $this->typeId, $this->typeId))) $mapType = max($mapType, $mt); // npc has difficulty dummys: 2+ dummies -> definitely raid (10/25 + hc); 1 dummy -> may be heroic (used here), but may also be 10/25-raid if ($_altIds) $mapType = max($mapType, count($_altIds) > 1 ? 2 : 1); // for event encounters a single npc may be reused over multiple difficulties but have different chests assigned - if ($d = DB::Aowow()->selectCell('SELECT MAX(`difficulty`) FROM ?_loot_link WHERE `npcId` IN (?a)', array_merge($_altIds, [$this->typeId]))) + if ($d = DB::Aowow()->selectCell('SELECT MAX(`difficulty`) FROM ::loot_link WHERE `npcId` IN %in', array_merge($_altIds, [$this->typeId]))) $mapType = max($mapType, $d > 2 ? 2 : 1); @@ -134,7 +133,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); // Event (ignore events, where the object only gets removed) - if ($_ = DB::World()->selectCol('SELECT DISTINCT ge.`eventEntry` FROM game_event ge, game_event_creature gec, creature c WHERE ge.`eventEntry` = gec.`eventEntry` AND c.`guid` = gec.`guid` AND c.`id` = ?d', $this->typeId)) + if ($_ = DB::World()->selectCol('SELECT DISTINCT ge.`eventEntry` FROM game_event ge, game_event_creature gec, creature c WHERE ge.`eventEntry` = gec.`eventEntry` AND c.`guid` = gec.`guid` AND c.`id` = %i', $this->typeId)) { $this->extendGlobalIds(Type::WORLDEVENT, ...$_); $ev = []; @@ -194,13 +193,60 @@ class NpcBaseResponse extends TemplateResponse implements ICache if ($this->subject->getField('npcflag') & (NPC_FLAG_SPIRIT_HEALER | NPC_FLAG_SPIRIT_GUIDE)) $infobox[] = Lang::npc('extraFlags', CREATURE_FLAG_EXTRA_GHOST_VISIBILITY); + // id + $infobox[] = Lang::npc('id') . $this->typeId; + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if (User::isInGroup(U_GROUP_EMPLOYEE)) { + $spawnData = DB::Aowow()->selectAssoc('SELECT `guid` AS "0", `ScriptName` AS "1", `StringId` AS "2" FROM ::spawns WHERE `type` = %i AND `typeId` = %i AND `ScriptName` IS NOT NULL ORDER BY `guid` ASC', Type::NPC, $this->typeId); + // AI - if ($_ = $this->subject->getField('scriptName')) - $infobox[] = 'Script'.Lang::main('colon').$_; - else if ($_ = $this->subject->getField('aiName')) - $infobox[] = 'AI'.Lang::main('colon').$_; + $scripts = null; + if ($_ = $this->subject->getField('ScriptOrAI')) + $scripts = match($_) + { + 'NullAI', 'AggressorAI', + 'ReactorAI', 'GuardAI', + 'PetAI', 'TotemAI', + 'SmartAI' => 'AI'.Lang::main('colon').$_, + default => 'Script'.Lang::main('colon').$_ + }; + + + if ($moreAI = array_filter(array_column($spawnData, 1, 0))) + { + $scripts ??= 'Script'.Lang::main('colon').'…'; + $scripts = '[toggler=hidden id=scriptName]'.$scripts.'[/toggler][div=hidden id=scriptName][ul]'; + foreach ($moreAI as $guid => $script) + $scripts .= sprintf('[li]GUID: %d - %s[/li]', $guid, $script); + + $scripts .= '[/ul][/div]'; + } + + if ($scripts) + $infobox[] = $scripts; + + // StringId + $stringIDs = null; + if ($_ = $this->subject->getField('StringId')) + $stringIDs = 'StringID'.Lang::main('colon').$_; + + if ($moreStrings = array_filter(array_column($spawnData, 2, 0))) + { + $stringIDs ??= 'StringID'.Lang::main('colon').'…'; + $stringIDs = '[toggler=hidden id=stringId]'.$stringIDs.'[/toggler][div=hidden id=stringId][ul]'; + foreach ($moreStrings as $guid => $stringId) + $stringIDs .= sprintf('[li]GUID: %d - %s[/li]', $guid, $stringId); + + $stringIDs .= '[/ul][/div]'; + } + + if ($stringIDs) + $infobox[] = $stringIDs; // Mechanic immune if ($immuneMask = $this->subject->getField('mechanicImmuneMask')) @@ -237,7 +283,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache } if ($stats = $this->getCreatureStats($mapType, $_altIds)) - $infobox[] = Lang::npc('stats').($_altIds ? ' ('.Lang::npc('modes', $mapType, 0).')' : '').Lang::main('colon').'[ul][li]'.implode('[/li][li]', $stats).'[/li][/ul]'; + $infobox[] = Lang::npc('stats').($_altIds ? ' ('.Lang::game('modes', $mapType, 0).')' : '').Lang::main('colon').'[ul][li]'.implode('[/li][li]', $stats).'[/li][/ul]'; if ($infobox) { @@ -266,13 +312,13 @@ class NpcBaseResponse extends TemplateResponse implements ICache // smart AI $sai = null; - if ($this->subject->getField('aiName') == 'SmartAI') + if ($this->subject->getField('ScriptOrAI') == 'SmartAI') { $sai = new SmartAI(SmartAI::SRC_TYPE_CREATURE, $this->typeId); if (!$sai->prepare()) // no smartAI found .. check per guid { // at least one of many - $guids = DB::World()->selectCol('SELECT `guid` FROM creature WHERE `id` = ?d', $this->typeId); + $guids = DB::World()->selectCol('SELECT `guid` FROM creature WHERE `id` = %i', $this->typeId); while ($_ = array_pop($guids)) { $sai = new SmartAI(SmartAI::SRC_TYPE_CREATURE, -$_, ['baseEntry' => $this->typeId, 'title' => ' [small](for GUID: '.$_.')[/small]']); @@ -287,7 +333,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache $this->smartAI = $sai->getMarkup(); } else - trigger_error('Creature has SmartAI set in template but no SmartAI defined.'); + trigger_error('Creature has `AIName`: SmartAI set in template but no SmartAI defined.'); } // consider pooled spawns @@ -312,7 +358,8 @@ class NpcBaseResponse extends TemplateResponse implements ICache // tab: abilities / tab_controlledabilities (dep: VehicleId) $tplSpells = []; $genSpells = []; - $conditions = ['OR']; + $spellClick = []; + $conditions = [DB::OR]; for ($i = 1; $i < 9; $i++) if ($_ = $this->subject->getField('spell'.$i)) @@ -324,7 +371,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache if ($smartSpells = SmartAI::getSpellCastsForOwner($this->typeId, SmartAI::SRC_TYPE_CREATURE)) $genSpells = $smartSpells; - if ($auras = DB::World()->selectCell('SELECT `auras` FROM creature_template_addon WHERE `entry` = ?d', $this->typeId)) + if ($auras = DB::World()->selectCell('SELECT `auras` FROM creature_template_addon WHERE `entry` = %i', $this->typeId)) { $auras = preg_replace('/[^\d ]/', ' ', $auras); // remove erroneous chars from string $genSpells = array_merge($genSpells, array_filter(explode(' ', $auras))); @@ -333,6 +380,12 @@ class NpcBaseResponse extends TemplateResponse implements ICache if ($genSpells) $conditions[] = ['id', $genSpells]; + if ($spellClick = DB::World()->selectAssoc('SELECT `spell_id` AS ARRAY_KEY, `cast_flags` AS "0", `user_type` AS "1" FROM npc_spellclick_spells WHERE `npc_entry` = %i', $this->typeId)) + { + $genSpells = array_merge($genSpells, array_keys($spellClick)); + $conditions[] = ['id', array_keys($spellClick)]; + } + // Pet-Abilities if (($_typeFlags & NPC_TYPEFLAG_TAMEABLE) && ($_ = $this->subject->getField('family'))) { @@ -348,13 +401,13 @@ class NpcBaseResponse extends TemplateResponse implements ICache break; } $conditions[] = [ - 'AND', + DB::AND, ['s.typeCat', -3], [ - 'OR', + DB::OR, ['skillLine1', $skill], - ['AND', ['skillLine1', 0, '>'], ['skillLine2OrMask', $skill]], - ['AND', ['skillLine1', -1], ['skillLine2OrMask', $mask, '&']] + [DB::AND, ['skillLine1', 0, '>'], ['skillLine2OrMask', $skill]], + [DB::AND, ['skillLine1', -1], ['skillLine2OrMask', $mask, '&']] ] ]; } @@ -370,6 +423,9 @@ class NpcBaseResponse extends TemplateResponse implements ICache foreach ($controled as $id => $values) { + if (isset($spellClick[$id])) + $values['spellclick'] = $spellClick[$id]; + if (in_array($id, $genSpells)) { $normal[$id] = $values; @@ -379,7 +435,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache } $cnd = new Conditions(); - $cnd->getBySourceGroup($this->typeId, Conditions::SRC_VEHICLE_SPELL)->prepare(); + $cnd->getBySource(Conditions::SRC_VEHICLE_SPELL, group: $this->typeId)->prepare(); if ($cnd->toListviewColumn($controled, $extraCols, $this->typeId, 'id')) $this->extendGlobalData($cnd->getJsGlobals()); @@ -402,10 +458,10 @@ class NpcBaseResponse extends TemplateResponse implements ICache // tab: summoned by [spell] $conditions = array( - 'OR', - ['AND', ['effect1Id', [SPELL_EFFECT_SUMMON, SPELL_EFFECT_SUMMON_PET, SPELL_EFFECT_SUMMON_DEMON]], ['effect1MiscValue', $this->typeId]], - ['AND', ['effect2Id', [SPELL_EFFECT_SUMMON, SPELL_EFFECT_SUMMON_PET, SPELL_EFFECT_SUMMON_DEMON]], ['effect2MiscValue', $this->typeId]], - ['AND', ['effect3Id', [SPELL_EFFECT_SUMMON, SPELL_EFFECT_SUMMON_PET, SPELL_EFFECT_SUMMON_DEMON]], ['effect3MiscValue', $this->typeId]] + DB::OR, + [DB::AND, ['effect1Id', [SPELL_EFFECT_SUMMON, SPELL_EFFECT_SUMMON_PET, SPELL_EFFECT_SUMMON_DEMON]], ['effect1MiscValue', $this->typeId]], + [DB::AND, ['effect2Id', [SPELL_EFFECT_SUMMON, SPELL_EFFECT_SUMMON_PET, SPELL_EFFECT_SUMMON_DEMON]], ['effect2MiscValue', $this->typeId]], + [DB::AND, ['effect3Id', [SPELL_EFFECT_SUMMON, SPELL_EFFECT_SUMMON_PET, SPELL_EFFECT_SUMMON_DEMON]], ['effect3MiscValue', $this->typeId]] ); $sbSpell = new SpellList($conditions); @@ -462,9 +518,9 @@ class NpcBaseResponse extends TemplateResponse implements ICache 'SELECT ts.`SpellId` AS ARRAY_KEY, ts.`MoneyCost` AS "cost", ts.`ReqSkillLine` AS "reqSkillId", ts.`ReqSkillRank` AS "reqSkillValue", ts.`ReqLevel` AS "reqLevel", ts.`ReqAbility1` AS "reqSpellId1", ts.`reqAbility2` AS "reqSpellId2" FROM trainer_spell ts JOIN creature_default_trainer cdt ON cdt.`TrainerId` = ts.`TrainerId` - WHERE cdt.`Creatureid` = ?d'; + WHERE cdt.`Creatureid` = %i'; - if ($tSpells = DB::World()->select($teachQuery, $this->typeId)) + if ($tSpells = DB::World()->selectAssoc($teachQuery, $this->typeId)) { $teaches = new SpellList(array(['id', array_keys($tSpells)])); if (!$teaches->error) @@ -517,16 +573,16 @@ class NpcBaseResponse extends TemplateResponse implements ICache // tab: sells if ($sells = DB::World()->selectCol( - 'SELECT nv.`item` FROM npc_vendor nv WHERE nv.`entry` = ?d UNION - SELECT nv1.`item` FROM npc_vendor nv1 JOIN npc_vendor nv2 ON -nv1.`entry` = nv2.`item` WHERE nv2.`entry` = ?d UNION - SELECT genv.`item` FROM game_event_npc_vendor genv JOIN creature c ON genv.`guid` = c.`guid` WHERE c.`id` = ?d', + 'SELECT nv.`item` FROM npc_vendor nv WHERE nv.`entry` = %i UNION + SELECT nv1.`item` FROM npc_vendor nv1 JOIN npc_vendor nv2 ON -nv1.`entry` = nv2.`item` WHERE nv2.`entry` = %i UNION + SELECT genv.`item` FROM game_event_npc_vendor genv JOIN creature c ON genv.`guid` = c.`guid` WHERE c.`id` = %i', $this->typeId, $this->typeId, $this->typeId) ) { $soldItems = new ItemList(array(['id', $sells])); if (!$soldItems->error) { - $colAddIn = null; + $colAddIn = ''; $extraCols = ["\$Listview.funcBox.createSimpleCol('stack', 'stack', '10%', 'stack')", '$Listview.extraCols.cost']; $lvData = $soldItems->getListviewData(ITEMINFO_VENDOR, [Type::NPC => [$this->typeId]]); @@ -541,7 +597,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache } $cnd = new Conditions(); - if ($cnd->getBySourceGroup($this->typeId, Conditions::SRC_NPC_VENDOR)->prepare()) + if ($cnd->getBySource(Conditions::SRC_NPC_VENDOR, group: $this->typeId)->prepare()) { $this->extendGlobalData($cnd->getJsGlobals()); $cnd->toListviewColumn($lvData, $extraCols, $this->typeId, 'id'); @@ -559,79 +615,108 @@ class NpcBaseResponse extends TemplateResponse implements ICache } // tabs: this creature contains.. - $skinTab = ['tab_skinning', 'skinning', SKILL_SKINNING]; - if ($_typeFlags & NPC_TYPEFLAG_SKIN_WITH_HERBALISM) - $skinTab = ['tab_herbalism', 'herbalism', SKILL_HERBALISM]; - else if ($_typeFlags & NPC_TYPEFLAG_SKIN_WITH_MINING) - $skinTab = ['tab_mining', 'mining', SKILL_MINING]; - else if ($_typeFlags & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING) - $skinTab = ['tab_engineering', 'engineering', SKILL_ENGINEERING]; - - /* - extraCols: [Listview.extraCols.count, Listview.extraCols.percent, Listview.extraCols.mode], - _totalCount: 22531, - computeDataFunc: Listview.funcBox.initLootTable, - onAfterCreate: Listview.funcBox.addModeIndicator, - - modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}} - */ + if ($this->subject->isGatherable()) + $skinTab = ['$LANG.tab_herbalism', 'herbalism', SKILL_HERBALISM]; + else if ($this->subject->isMineable()) + $skinTab = ['$LANG.tab_mining', 'mining', SKILL_MINING]; + else if ($this->subject->isSalvageable()) + $skinTab = ['$LANG.tab_engineering', 'engineering', SKILL_ENGINEERING]; + else + $skinTab = ['$LANG.tab_skinning', 'skinning', SKILL_SKINNING]; $sourceFor = array( - 0 => [LOOT_CREATURE, $this->subject->getField('lootId'), '$LANG.tab_drops', 'drops', [ ], ''], - 8 => [LOOT_PICKPOCKET, $this->subject->getField('pickpocketLootId'), '$LANG.tab_pickpocketing', 'pickpocketing', ['side', 'slot', 'reqlevel'], ''], - 9 => [LOOT_SKINNING, $this->subject->getField('skinLootId'), '$LANG.'.$skinTab[0], $skinTab[1], ['side', 'slot', 'reqlevel'], ''] + 0 => [Loot::CREATURE, [4 => $this->subject->getField('lootId')], '$LANG.tab_drops', 'drops', [ ], ''], + 1 => [Loot::GAMEOBJECT, [], '$LANG.tab_drops', 'drops-object', [ ], ''], + 2 => [Loot::PICKPOCKET, [4 => $this->subject->getField('pickpocketLootId')], '$LANG.tab_pickpocketing', 'pickpocketing', ['side', 'slot', 'reqlevel'], ''], + 3 => [Loot::SKINNING, [4 => $this->subject->getField('skinLootId')], $skinTab[0], $skinTab[1], ['side', 'slot', 'reqlevel'], ''] ); - // temp: manually add loot for difficulty-versions - $langref = array( - "-2" => '$LANG.tab_heroic', - "-1" => '$LANG.tab_normal', - 1 => '$$WH.sprintf(LANG.tab_normalX, 10)', - 2 => '$$WH.sprintf(LANG.tab_normalX, 25)', - 3 => '$$WH.sprintf(LANG.tab_heroicX, 10)', - 4 => '$$WH.sprintf(LANG.tab_heroicX, 25)' - ); + /* loot tabs to sub tabs + * (1 << 0) => '$LANG.tab_heroic', + * (1 << 1) => '$LANG.tab_normal', + * (1 << 2) => '$LANG.tab_drops', + * (1 << 3) => '$$WH.sprintf(LANG.tab_normalX, 10)', + * (1 << 4) => '$$WH.sprintf(LANG.tab_normalX, 25)', + * (1 << 5) => '$$WH.sprintf(LANG.tab_heroicX, 10)', + * (1 << 6) => '$$WH.sprintf(LANG.tab_heroicX, 25)' + */ + + $getBit = function(int $type, int $difficulty) : int + { + if ($type == 1) // dungeon + return 1 << (2 - $difficulty); + if ($type == 2) // raid + return 1 << (2 + $difficulty); + return 4; // generic case + }; + + foreach (DB::Aowow()->selectAssoc('SELECT l.`difficulty` AS ARRAY_KEY, o.`id`, o.`lootId`, o.`name_loc0`, o.`name_loc2`, o.`name_loc3`, o.`name_loc4`, o.`name_loc6`, o.`name_loc8` FROM ::loot_link l JOIN ::objects o ON o.`id` = l.`objectId` WHERE l.`npcId` = %i ORDER BY `difficulty` ASC', $this->typeId) as $difficulty => $lgo) + { + $sourceFor[1][1][$getBit($mapType, $difficulty)] = $lgo['lootId']; + $sourceFor[1][5] = $sourceFor[1][5] ?: '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$lgo['id'].', "'.Util::localizedString($lgo, 'name').'")'; + } if ($_altIds) { - $sourceFor[0][2] = $mapType == 1 ? $langref[-1] : $langref[1]; + if ($mapType == 1) // map generic loot to dungeon NH + { + $sourceFor[0][1] = [2 => $sourceFor[0][1][4]]; + $sourceFor[2][1] = [2 => $sourceFor[2][1][4]]; + $sourceFor[3][1] = [2 => $sourceFor[3][1][4]]; + } + if ($mapType == 2) // map generic loot to raid 10NH + { + $sourceFor[0][1] = [8 => $sourceFor[0][1][4]]; + $sourceFor[2][1] = [8 => $sourceFor[2][1][4]]; + $sourceFor[3][1] = [8 => $sourceFor[3][1][4]]; + } + foreach ($this->altNPCs->iterate() as $id => $__) { - $mode = ($_altIds[$id] + 1) * ($mapType == 1 ? -1 : 1); - foreach (DB::Aowow()->select('SELECT o.`id`, o.`lootId`, o.`name_loc0`, o.`name_loc2`, o.`name_loc3`, o.`name_loc4`, o.`name_loc6`, o.`name_loc8`, l.`difficulty` FROM ?_loot_link l JOIN ?_objects o ON o.`id` = l.`objectId` WHERE l.`npcId` = ?d', $id) as $l) - $sourceFor[(($l['difficulty'] - 1) * 2) + 1] = [LOOT_GAMEOBJECT, $l['lootId'], $langref[$l['difficulty'] * ($mapType == 1 ? -1 : 1)], 'drops-object-'.$l['difficulty'], [], '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$l['id'].', "'.Util::localizedString($l, 'name').'")']; + foreach (DB::Aowow()->selectAssoc('SELECT l.`difficulty` AS ARRAY_KEY, o.`id`, o.`lootId`, o.`name_loc0`, o.`name_loc2`, o.`name_loc3`, o.`name_loc4`, o.`name_loc6`, o.`name_loc8` FROM ::loot_link l JOIN ::objects o ON o.`id` = l.`objectId` WHERE l.`npcId` = %i ORDER BY `difficulty` ASC', $id) as $difficulty => $lgo) + { + $sourceFor[1][1][$getBit($mapType, $difficulty)] = $lgo['lootId']; + $sourceFor[1][5] = $sourceFor[1][5] ?: '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$lgo['id'].', "'.Util::localizedString($lgo, 'name').'")'; + } + if ($lootId = $this->altNPCs->getField('lootId')) - $sourceFor[($mode - 1) * 2] = [LOOT_CREATURE, $lootId, $langref[$mode], 'drops-'.abs($mode), [], '']; + $sourceFor[0][1][$getBit($mapType, $_altIds[$id] + 1)] = $lootId; + if ($lootId = $this->altNPCs->getField('pickpocketLootId')) + $sourceFor[2][1][$getBit($mapType, $_altIds[$id] + 1)] = $lootId; + if ($lootId = $this->altNPCs->getField('skinLootId')) + $sourceFor[3][1][$getBit($mapType, $_altIds[$id] + 1)] = $lootId; } } - foreach (DB::Aowow()->select('SELECT l.`difficulty` AS ARRAY_KEY, o.`id`, o.`lootId`, o.`name_loc0`, o.`name_loc2`, o.`name_loc3`, o.`name_loc4`, o.`name_loc6`, o.`name_loc8` FROM ?_loot_link l JOIN ?_objects o ON o.`id` = l.`objectId` WHERE l.`npcId` = ?d', $this->typeId) as $difficulty => $lgo) - $sourceFor[(($difficulty - 1) * 2) + 1] = [LOOT_GAMEOBJECT, $lgo['lootId'], $mapType ? $langref[$difficulty * ($mapType == 1 ? -1 : 1)] : '$LANG.tab_drops', 'drops-object-'.$difficulty, [], '$$WH.sprintf(LANG.lvnote_npcobjectsource, '.$lgo['id'].', "'.Util::localizedString($lgo, 'name').'")']; - - ksort($sourceFor); - - foreach ($sourceFor as [$lootTpl, $lootId, $tabName, $tabId, $hiddenCols, $note]) + foreach ($sourceFor as [$lootTpl, $lootEntries, $tabName, $tabId, $hiddenCols, $note]) { - $creatureLoot = new Loot(); - if ($creatureLoot->getByContainer($lootTpl, $lootId)) + $creatureLoot = new LootByContainer(); + if ($creatureLoot->getByContainer($lootTpl, $lootEntries)) { $extraCols = $creatureLoot->extraCols; - $extraCols[] = '$Listview.extraCols.percent'; + array_push($extraCols, '$Listview.extraCols.count', '$Listview.extraCols.percent'); + if (count($lootEntries) > 1) + $extraCols[] = '$Listview.extraCols.mode'; + + $hiddenCols[] = 'count'; $this->extendGlobalData($creatureLoot->jsGlobals); $tabData = array( - 'data' => $creatureLoot->getResult(), - 'name' => $tabName, - 'id' => $tabId, - 'extraCols' => array_unique($extraCols), - 'hiddenCols' => $hiddenCols ?: null, - 'sort' => ['-percent', 'name'] + 'data' => $creatureLoot->getResult(), + 'id' => $tabId, + 'name' => $tabName, + 'extraCols' => array_unique($extraCols), + 'hiddenCols' => $hiddenCols ?: null, + 'sort' => ['-percent', 'name'], + '_totalCount' => 10000, + 'computeDataFunc' => '$Listview.funcBox.initLootTable', + 'onAfterCreate' => '$Listview.funcBox.addModeIndicator', ); if ($note) $tabData['note'] = $note; - else if ($lootTpl == LOOT_SKINNING) + else if ($lootTpl == Loot::SKINNING) $tabData['note'] = '<b>'.Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill($skinTab[2], $this->subject->getField('maxLevel') * 5), Lang::FMT_HTML).'</b>'; $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); @@ -672,12 +757,16 @@ class NpcBaseResponse extends TemplateResponse implements ICache // tab: objective of quest $conditions = array( - 'OR', - ['AND', ['reqNpcOrGo1', $this->typeId], ['reqNpcOrGoCount1', 0, '>']], - ['AND', ['reqNpcOrGo2', $this->typeId], ['reqNpcOrGoCount2', 0, '>']], - ['AND', ['reqNpcOrGo3', $this->typeId], ['reqNpcOrGoCount3', 0, '>']], - ['AND', ['reqNpcOrGo4', $this->typeId], ['reqNpcOrGoCount4', 0, '>']], + DB::OR, + [DB::AND, ['reqNpcOrGo1', [$this->typeId]], ['reqNpcOrGoCount1', 0, '>']], + [DB::AND, ['reqNpcOrGo2', [$this->typeId]], ['reqNpcOrGoCount2', 0, '>']], + [DB::AND, ['reqNpcOrGo3', [$this->typeId]], ['reqNpcOrGoCount3', 0, '>']], + [DB::AND, ['reqNpcOrGo4', [$this->typeId]], ['reqNpcOrGoCount4', 0, '>']] ); + foreach ([1, 2] as $i) + if (($_ = $this->subject->getField('KillCredit'.$i)) > 0) + for ($j = 1; $j < 5; $j++) + $conditions[$j][1][1][] = $_; $objectiveOf = new QuestList($conditions); if (!$objectiveOf->error) @@ -693,10 +782,14 @@ class NpcBaseResponse extends TemplateResponse implements ICache // tab: criteria of [ACHIEVEMENT_CRITERIA_TYPE_KILL_CREATURE_TYPE have no data set to check for] $conditions = array( + DB::AND, ['ac.type', [ACHIEVEMENT_CRITERIA_TYPE_KILL_CREATURE, ACHIEVEMENT_CRITERIA_TYPE_KILLED_BY_CREATURE]], ['ac.value1', $this->typeId] ); + if ($extraCrt = DB::World()->selectCol('SELECT `criteria_id` FROM achievement_criteria_data WHERE `type` = %i AND `value1` = %i', ACHIEVEMENT_CRITERIA_DATA_TYPE_T_CREATURE, $this->typeId)) + $conditions = [DB::OR, $conditions, ['ac.id', $extraCrt]]; + $crtOf = new AchievementList($conditions); if (!$crtOf->error) { @@ -710,7 +803,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache } // tab: passengers - if ($_ = DB::World()->selectCol('SELECT `accessory_entry` AS ARRAY_KEY, GROUP_CONCAT(`seat_id` SEPARATOR ", ") FROM vehicle_template_accessory WHERE `entry` = ?d GROUP BY `accessory_entry`', $this->typeId)) + if ($_ = DB::World()->selectCol('SELECT `accessory_entry` AS ARRAY_KEY, GROUP_CONCAT(`seat_id` SEPARATOR ", ") FROM vehicle_template_accessory WHERE `entry` = %i GROUP BY `accessory_entry`', $this->typeId)) { $passengers = new CreatureList(array(['id', array_keys($_)])); if (!$passengers->error) @@ -746,7 +839,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache $this->soundIds = array_merge($this->soundIds, SmartAI::getSoundsPlayedForOwner($this->typeId, SmartAI::SRC_TYPE_CREATURE)); // up to 4 possible displayIds .. for the love of things betwixt, just use the first! - $activitySounds = DB::Aowow()->selectRow('SELECT * FROM ?_creature_sounds WHERE `id` = ?d', $this->subject->getField('displayId1')); + $activitySounds = DB::Aowow()->selectRow('SELECT * FROM ::creature_sounds WHERE `id` = %i', $this->subject->getField('displayId1')); array_shift($activitySounds); // remove id-column $this->soundIds = array_merge($this->soundIds, array_values($activitySounds)); @@ -770,8 +863,8 @@ class NpcBaseResponse extends TemplateResponse implements ICache // tab: conditions $cnd = new Conditions(); - $cnd->getBySourceEntry($this->typeId, Conditions::SRC_CREATURE_TEMPLATE_VEHICLE) - ->getBySourceGroup($this->typeId, Conditions::SRC_SPELL_CLICK_EVENT) + $cnd->getBySource(Conditions::SRC_CREATURE_TEMPLATE_VEHICLE, entry: $this->typeId) + ->getBySource(Conditions::SRC_SPELL_CLICK_EVENT, group: $this->typeId) ->getByCondition(Type::NPC, $this->typeId) ->prepare(); if ($tab = $cnd->toListviewTab()) @@ -785,11 +878,11 @@ class NpcBaseResponse extends TemplateResponse implements ICache private function getRepForId(array $entries, array &$spillover) : array { - $rows = DB::World()->select( + $rows = DB::World()->selectAssoc( 'SELECT `creature_id` AS "npc", `RewOnKillRepFaction1` AS "faction", `RewOnKillRepValue1` AS "qty", `MaxStanding1` AS "maxRank", `isTeamAward1` AS "spillover" - FROM creature_onkill_reputation WHERE `creature_id` IN (?a) AND `RewOnKillRepFaction1` > 0 UNION + FROM creature_onkill_reputation WHERE `creature_id` IN %in AND `RewOnKillRepFaction1` > 0 UNION SELECT `creature_id` AS "npc", `RewOnKillRepFaction2` AS "faction", `RewOnKillRepValue2` AS "qty", `MaxStanding2` AS "maxRank", `isTeamAward2` AS "spillover" - FROM creature_onkill_reputation WHERE `creature_id` IN (?a) AND `RewOnKillRepFaction2` > 0', + FROM creature_onkill_reputation WHERE `creature_id` IN %in AND `RewOnKillRepFaction2` > 0', $entries, $entries ); @@ -810,7 +903,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache 0 // spilloverCat ); - $cuRate = DB::World()->selectCell('SELECT `creature_rate` FROM reputation_reward_rate WHERE `creature_rate` <> 1 AND `faction` = ?d', $row['faction']); + $cuRate = DB::World()->selectCell('SELECT `creature_rate` FROM reputation_reward_rate WHERE `creature_rate` <> 1 AND `faction` = %i', $row['faction']); if ($cuRate && User::isInGroup(U_GROUP_EMPLOYEE)) $set[1][1] = $set[1][0] . sprintf(Util::$dfnString, Lang::faction('customRewRate'), ($set[1][0] > 0 ? '+' : '').($set[1][0] * ($cuRate - 1))); else if ($cuRate) @@ -825,7 +918,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache $spill[0][1] = $set[1][1] / 2; $spillover[$factions->getField('cat')] = $spill; - $set[6] = $factions->getField('cat'); // set spillover + $set[5] = $factions->getField('cat'); // set spillover } $result[] = $set; @@ -841,7 +934,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache // base NPC if ($base = $this->getRepForId([$this->typeId], $spilledParents)) - $reputation[] = [Lang::npc('modes', 1, 0), $base]; + $reputation[] = [Lang::game('modes', 1, 0), $base]; // difficulty dummys if ($dummyIds && ($mapType == 1 || $mapType == 2)) @@ -855,7 +948,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache // apply by difficulty foreach ($alt as $mode => $dat) - $reputation[] = [Lang::npc('modes', $mapType, $mode), $dat]; + $reputation[] = [Lang::game('modes', $mapType, $mode), $dat]; } // get spillover factions and apply @@ -865,7 +958,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache foreach ($reputation as $i => [, $data]) { - foreach ($data as [$factionId, , , , , , $spillover]) + foreach ($data as [$factionId, , , , , $spillover]) { if (!$spillover) continue; @@ -927,10 +1020,18 @@ class NpcBaseResponse extends TemplateResponse implements ICache // Resistances $resNames = [null, 'hol', 'fir', 'nat', 'fro', 'sha', 'arc']; $tmpRes = []; + $res = $this->subject->getBaseStats('resistance'); // $sc => $amt $stats['resistance'] = ''; - foreach ($this->subject->getBaseStats('resistance') as $sc => $amt) - if ($amt) - $tmpRes[] = '[span class="moneyschool'.$resNames[$sc].'"]'.$amt.'[/span]'; + foreach ($resNames as $idx => $sc) + { + if (!$sc) + continue; + + if ((1 << $idx) & $this->subject->getField('schoolImmuneMask')) + $tmpRes[] = '[tooltip=tooltip_immune][span class="tip moneyschool'.$sc.'"]∞[/span][/tooltip]'; + else if ($res[$idx]) + $tmpRes[] = '[span class="moneyschool'.$sc.'"]'.$res[$idx].'[/span]'; + } if ($tmpRes) { @@ -957,7 +1058,7 @@ class NpcBaseResponse extends TemplateResponse implements ICache if (!$this->altNPCs->getEntry($id)) continue; - $m = Lang::npc('modes', $mapType, $mode); + $m = Lang::game('modes', $mapType, $mode); // Health $health = $this->altNPCs->getBaseStats('health'); @@ -981,8 +1082,17 @@ class NpcBaseResponse extends TemplateResponse implements ICache $stats['resistance'] = Lang::npc('resistances').'…'; $tmpRes = ''; - foreach ($this->altNPCs->getBaseStats('resistance') as $sc => $amt) - $tmpRes .= '[td][span style="margin: 0px 5px"]'.$amt.'[/span][/td]'; + $res = $this->altNPCs->getBaseStats('resistance'); + foreach ($resNames as $idx => $sc) + { + if (!$sc) + continue; + + if ((1 << $idx) & $this->altNPCs->getField('schoolImmuneMask')) + $tmpRes .= '[td][span style="margin: 0px 5px"]∞[/span][/td]'; + else if ($res[$idx]) + $tmpRes .= '[td][span style="margin: 0px 5px"]'.$res[$idx].'[/span][/td]'; + } $modes['resistance'][] = '[td]'.$m.'    [/td]'.$tmpRes; } @@ -999,10 +1109,13 @@ class NpcBaseResponse extends TemplateResponse implements ICache $modes['ranged'][] = sprintf($modeRow, $m, Lang::nf($ranged[0]).' - '.Lang::nf($ranged[1])); } + // todo: resistances can be present/missing in either $stats or $modes + // should be handled separately..? + if ($modes) foreach ($stats as $k => $v) if ($v) - $stats[$k] = sprintf($hint, implode('[/tr][tr]', $modes[$k]), $v, $k); + $stats[$k] = isset($modes[$k]) ? sprintf($hint, implode('[/tr][tr]', $modes[$k]), $v, $k) : $v; return $stats; } diff --git a/endpoints/npcs/npcs.php b/endpoints/npcs/npcs.php index 1bd05bfb..de03c9a4 100644 --- a/endpoints/npcs/npcs.php +++ b/endpoints/npcs/npcs.php @@ -11,7 +11,7 @@ class NpcsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::NPC; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'npcs'; protected string $pageName = 'npcs'; @@ -27,14 +27,22 @@ class NpcsBaseResponse extends TemplateResponse implements ICache public bool $petFamPanel = false; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new CreatureListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -42,17 +50,13 @@ class NpcsBaseResponse extends TemplateResponse implements ICache { $this->h1 = Lang::game('npcs'); - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - if ($this->category) { $conditions[] = ['type', $this->category[0]]; @@ -91,6 +95,8 @@ class NpcsBaseResponse extends TemplateResponse implements ICache /****************/ $this->redButtons[BUTTON_WOWHEAD] = true; + if ($fiQuery = $this->filter->buildGETParam()) + $this->wowheadLink .= '&filter='.$fiQuery; // beast subtypes are selected via filter $tabData = ['data' => []]; @@ -107,9 +113,9 @@ class NpcsBaseResponse extends TemplateResponse implements ICache $tabData['hiddenCols'] = ['type']; // create note if search limit was exceeded - if ($npcs->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($npcs->getMatches() > Listview::DEFAULT_SIZE) { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_npcsfound', $npcs->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_npcsfound', $npcs->getMatches(), Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } } diff --git a/endpoints/object/object.php b/endpoints/object/object.php index 9c6a1ba2..60d9281e 100644 --- a/endpoints/object/object.php +++ b/endpoints/object/object.php @@ -10,7 +10,7 @@ class ObjectBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'object'; protected string $pageName = 'object'; @@ -22,6 +22,9 @@ class ObjectBaseResponse extends TemplateResponse implements ICache public ?Book $book = null; public ?array $relBoss = null; + private array $difficulties = []; + private int $mapType = 0; + private GameObjectList $subject; public function __construct(string $id) @@ -61,6 +64,37 @@ class ObjectBaseResponse extends TemplateResponse implements ICache array_unshift($this->title, Lang::unescapeUISequences($this->subject->getField('name', true), Lang::FMT_RAW), Util::ucFirst(Lang::game('object'))); + /**********************/ + /* Determine Map Type */ + /**********************/ + + if ($objectdifficulty = DB::Aowow()->selectAssoc( // has difficulty versions of itself + 'SELECT `normal10` AS "0", `normal25` AS "1", + `heroic10` AS "2", `heroic25` AS "3", + `mapType` AS ARRAY_KEY + FROM ::objectdifficulty + WHERE `normal10` = %i OR `normal25` = %i OR + `heroic10` = %i OR `heroic25` = %i', + $this->typeId, $this->typeId, $this->typeId, $this->typeId + )) + { + $this->mapType = key($objectdifficulty); + $this->difficulties = array_pop($objectdifficulty); + } + else if ($maps = DB::Aowow()->selectCell('SELECT IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ::spawns WHERE `type` = %i AND `typeId` = %i', Type::OBJECT, $this->typeId)) + { + $this->mapType = match ((int)DB::Aowow()->selectCell('SELECT `type` FROM ::zones WHERE `id` = %i', $maps)) + { + // MAP_TYPE_DUNGEON, + MAP_TYPE_DUNGEON_HC => 1, + // MAP_TYPE_RAID, + MAP_TYPE_MMODE_RAID, + MAP_TYPE_MMODE_RAID_HC => 2, + default => 0 + }; + } + + /***********/ /* Infobox */ /***********/ @@ -68,7 +102,7 @@ class ObjectBaseResponse extends TemplateResponse implements ICache $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); // Event (ignore events, where the object only gets removed) - if ($_ = DB::World()->selectCol('SELECT DISTINCT ge.`eventEntry` FROM game_event ge, game_event_gameobject geg, gameobject g WHERE ge.`eventEntry` = geg.`eventEntry` AND g.`guid` = geg.`guid` AND g.`id` = ?d', $this->typeId)) + if ($_ = DB::World()->selectCol('SELECT DISTINCT ge.`eventEntry` FROM game_event ge, game_event_gameobject geg, gameobject g WHERE ge.`eventEntry` = geg.`eventEntry` AND g.`guid` = geg.`guid` AND g.`id` = %i', $this->typeId)) { $this->extendGlobalIds(Type::WORLDEVENT, ...$_); $ev = []; @@ -79,7 +113,7 @@ class ObjectBaseResponse extends TemplateResponse implements ICache } // Faction - if ($_ = DB::Aowow()->selectCell('SELECT `factionId` FROM ?_factiontemplate WHERE `id` = ?d', $this->subject->getField('faction'))) + if ($_ = DB::Aowow()->selectCell('SELECT `factionId` FROM ::factiontemplate WHERE `id` = %i', $this->subject->getField('faction'))) { $this->extendGlobalIds(Type::FACTION, $_); $infobox[] = Util::ucFirst(Lang::game('faction')).Lang::main('colon').'[faction='.$_.']'; @@ -141,51 +175,49 @@ class ObjectBaseResponse extends TemplateResponse implements ICache // SpellFocus if ($_ = $this->subject->getField('spellFocusId')) - if ($sfo = DB::Aowow()->selectRow('SELECT * FROM ?_spellfocusobject WHERE `id` = ?d', $_)) - $infobox[] = '[tooltip name=focus]'.Lang::gameObject('focusDesc').'[/tooltip][span class=tip tooltip=focus]'.Lang::gameObject('focus').Lang::main('colon').Util::localizedString($sfo, 'name').'[/span]'; - - // lootinfo: [min, max, restock] - if ($this->subject->getField('lootStack')) { - [$min, $max, $restock] = $this->subject->getField('lootStack'); - $buff = Lang::spell('spellModOp', 4).Lang::main('colon').$min; - if ($min < $max) - $buff .= Lang::game('valueDelim').$max; + if ($sfo = DB::Aowow()->selectRow('SELECT * FROM ::spellfocusobject WHERE `id` = %i', $_)) + { + $n = Util::localizedString($sfo, 'name'); + if (!is_null(GameObjectListFilter::getCriteriaIndex(50, $_))) + $n = '[url=?objects&filter=cr=50;crs='.$_.';crv=0]'.$n.'[/url]'; - // since Veins don't have charges anymore, the timer is questionable - $infobox[] = $restock > 1 ? '[tooltip name=restock]'.Lang::gameObject('restock', [Util::formatTime($restock * 1000)]).'[/tooltip][span class=tip tooltip=restock]'.$buff.'[/span]' : $buff; + $infobox[] = '[tooltip name=focus]'.Lang::gameObject('focusDesc').'[/tooltip][span class=tip tooltip=focus]'.Lang::gameObject('focus').Lang::main('colon').$n.'[/span]'; + } } - // meeting stone [minLevel, maxLevel, zone] - if ($this->subject->getField('type') == OBJECT_MEETINGSTONE && $this->subject->getField('mStone')) + // lootinfo: [min, max, restock] + if (([$min, $max, $restock] = $this->subject->getField('lootStack')) && $min) { - [$minLevel, $maxLevel, $zone] = $this->subject->getField('mStone'); + $buff = Lang::spell('spellModOp', 4).Lang::main('colon').Util::createNumRange($min, $max); + // ore veins don't have charges in 335a, but the functionality is still there + $infobox[] = $restock > 1 ? '[tooltip name=restock]'.Lang::gameObject('restock', [DateTime::formatTimeElapsed($restock * 1000)]).'[/tooltip][span class=tip tooltip=restock]'.$buff.'[/span]' : $buff; + } + + // meeting stone (only on type: OBJECT_MEETINGSTONE) + if ([$minLevel, $maxLevel, $zone] = $this->subject->getField('mStone')) + { $this->extendGlobalIds(Type::ZONE, $zone); $m = Lang::game('meetingStone').'[zone='.$zone.']'; - - $l = $minLevel; - if ($minLevel > 1 && $maxLevel > $minLevel) - $l .= Lang::game('valueDelim').min($maxLevel, MAX_LEVEL); + $l = Util::createNumRange($minLevel, min($maxLevel, MAX_LEVEL)); $infobox[] = $l ? '[tooltip name=meetingstone]'.Lang::game('reqLevel', [$l]).'[/tooltip][span class=tip tooltip=meetingstone]'.$m.'[/span]' : $m; } - // capture area - if ($this->subject->getField('type') == OBJECT_CAPTURE_POINT && $this->subject->getField('capture')) + // capture area (only on type: OBJECT_CAPTURE_POINT) + if ([$minPlayer, $maxPlayer, $minTime, $maxTime, $radius] = $this->subject->getField('capture')) { - [$minPlayer, $maxPlayer, $minTime, $maxTime, $radius] = $this->subject->getField('capture'); - $buff = Lang::gameObject('capturePoint'); if ($minTime > 1 || $minPlayer || $radius) $buff .= Lang::main('colon').'[ul]'; - if ($minTime > 1) - $buff .= '[li]'.Lang::game('duration').Lang::main('colon').($maxTime > $minTime ? Util::FormatTime($maxTime * 1000, true).' - ' : '').Util::FormatTime($minTime * 1000, true).'[/li]'; + if ($minTime > 1) // sign shenannigans reverse the display order + $buff .= '[li]'.Lang::game('duration').Lang::main('colon').Util::createNumRange(-$maxTime, -$minTime, fn: fn($x) => DateTime::formatTimeElapsed(-$x * 1000)).'[/li]'; if ($minPlayer) - $buff .= '[li]'.Lang::main('players').Lang::main('colon').$minPlayer.($maxPlayer > $minPlayer ? ' - '.$maxPlayer : '').'[/li]'; + $buff .= '[li]'.Lang::main('players').Lang::main('colon').Util::createNumRange($minPlayer, $maxPlayer).'[/li]'; if ($radius) $buff .= '[li]'.Lang::spell('range', [$radius]).'[/li]'; @@ -196,10 +228,55 @@ class ObjectBaseResponse extends TemplateResponse implements ICache $infobox[] = $buff; } - // AI + // id + $infobox[] = Lang::gameObject('id') . $this->typeId; + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + + // used in mode + foreach ($this->difficulties as $n => $id) + if ($id == $this->typeId) + $infobox[] = Lang::game('mode').Lang::game('modes', $this->mapType, $n); + if (User::isInGroup(U_GROUP_EMPLOYEE)) + { + $spawnData = DB::Aowow()->selectAssoc('SELECT `guid` AS "0", `ScriptName` AS "1", `StringId` AS "2" FROM ::spawns WHERE `type` = %i AND `typeId` = %i AND `ScriptName` IS NOT NULL ORDER BY `guid` ASC', Type::OBJECT, $this->typeId); + + // AI + $scripts = null; if ($_ = $this->subject->getField('ScriptOrAI')) - $infobox[] = ($_ == 'SmartGameObjectAI' ? 'AI' : 'Script').Lang::main('colon').$_; + $scripts = ($_ == 'SmartGameObjectAI' ? 'AI' : 'Script').Lang::main('colon').$_; + + if ($moreAI = array_filter(array_column($spawnData, 1, 0))) + { + $scripts ??= 'Script'.Lang::main('colon').'…'; + $scripts = '[toggler=hidden id=scriptName]'.$scripts.'[/toggler][div=hidden id=scriptName][ul]'; + foreach ($moreAI as $guid => $script) + $scripts .= sprintf('[li]GUID: %d - %s[/li]', $guid, $script); + + $scripts .= '[/ul][/div]'; + } + + if ($scripts) + $infobox[] = $scripts; + + // StringId + $stringIDs = null; + if ($_ = $this->subject->getField('StringId')) + $stringIDs = 'StringID'.Lang::main('colon').$_; + + if ($moreStrings = array_filter(array_column($spawnData, 2, 0))) + { + $stringIDs ??= 'StringID'.Lang::main('colon').'…'; + $stringIDs = '[toggler=hidden id=stringId]'.$stringIDs.'[/toggler][div=hidden id=stringId][ul]'; + foreach ($moreStrings as $guid => $stringId) + $stringIDs .= sprintf('[li]GUID: %d - %s[/li]', $guid, $stringId); + + $stringIDs .= '[/ul][/div]'; + } + } if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); @@ -234,16 +311,16 @@ class ObjectBaseResponse extends TemplateResponse implements ICache // todo (low): consider pooled spawns - if ($ll = DB::Aowow()->selectRow('SELECT * FROM ?_loot_link WHERE `objectId` = ?d ORDER BY `priority` DESC LIMIT 1', $this->typeId)) + if ($ll = DB::Aowow()->selectRow('SELECT * FROM ::loot_link WHERE `objectId` = %i ORDER BY `priority` DESC LIMIT 1', $this->typeId)) { // group encounter if ($ll['encounterId']) $this->relBoss = [$ll['npcId'], Lang::profiler('encounterNames', $ll['encounterId'])]; // difficulty dummy - else if ($c = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM ?_creature WHERE `difficultyEntry1` = ?d OR `difficultyEntry2` = ?d OR `difficultyEntry3` = ?d', $ll['npcId'], $ll['npcId'], $ll['npcId'])) + else if ($c = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM ::creature WHERE `difficultyEntry1` = %i OR `difficultyEntry2` = %i OR `difficultyEntry3` = %i', $ll['npcId'], $ll['npcId'], $ll['npcId'])) $this->relBoss = [$c['id'], Util::localizedString($c, 'name')]; // base creature - else if ($c = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM ?_creature WHERE `id` = ?d', $ll['npcId'])) + else if ($c = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM ::creature WHERE `id` = %i', $ll['npcId'])) $this->relBoss = [$c['id'], Util::localizedString($c, 'name')]; } @@ -255,7 +332,7 @@ class ObjectBaseResponse extends TemplateResponse implements ICache if (!$sai->prepare()) // no smartAI found .. check per guid { // at least one of many - $guids = DB::World()->selectCol('SELECT `guid` FROM gameobject WHERE `id` = ?d', $this->typeId); + $guids = DB::World()->selectCol('SELECT `guid` FROM gameobject WHERE `id` = %i', $this->typeId); while ($_ = array_pop($guids)) { $sai = new SmartAI(SmartAI::SRC_TYPE_OBJECT, -$_, ['title' => ' [small](for GUID: '.$_.')[/small]']); @@ -270,7 +347,7 @@ class ObjectBaseResponse extends TemplateResponse implements ICache $this->smartAI = $sai->getMarkup(); } else - trigger_error('Gameobject has AIName set in template but no SmartAI defined.'); + trigger_error('Gameobject has `AIName`: SmartGameObjectAI set in template but no SmartAI defined.'); } $this->redButtons = array( @@ -296,10 +373,10 @@ class ObjectBaseResponse extends TemplateResponse implements ICache SPELL_EFFECT_SUMMON_OBJECT_SLOT4 ); $conditions = array( - 'OR', - ['AND', ['effect1Id', $summonEffects], ['effect1MiscValue', $this->typeId]], - ['AND', ['effect2Id', $summonEffects], ['effect2MiscValue', $this->typeId]], - ['AND', ['effect3Id', $summonEffects], ['effect3MiscValue', $this->typeId]] + DB::OR, + [DB::AND, ['effect1Id', $summonEffects], ['effect1MiscValue', $this->typeId]], + [DB::AND, ['effect2Id', $summonEffects], ['effect2MiscValue', $this->typeId]], + [DB::AND, ['effect3Id', $summonEffects], ['effect3MiscValue', $this->typeId]] ); $summons = new SpellList($conditions); @@ -400,12 +477,37 @@ class ObjectBaseResponse extends TemplateResponse implements ICache // tab: contains if ($_ = $this->subject->getField('lootId')) { - $goLoot = new Loot(); - if ($goLoot->getByContainer(LOOT_GAMEOBJECT, $_)) + // check if loot_link entry exists (only difficulty: 1) + if ($npcId = DB::Aowow()->selectCell('SELECT `npcId` FROM ::loot_link WHERE `objectId` = %i AND `difficulty` = 1', $this->typeId)) { - $extraCols = $goLoot->extraCols; - $extraCols[] = '$Listview.extraCols.percent'; - $hiddenCols = ['source', 'side', 'slot', 'reqlevel']; + // get id set of npc + $lootEntries = DB::Aowow()->selectCol( + 'SELECT ll.`difficulty` AS ARRAY_KEY, o.`lootId` + FROM ::creature c + LEFT JOIN ::loot_link ll ON ll.`npcId` IN (c.`id`, c.`difficultyEntry1`, c.`difficultyEntry2`, c.`difficultyEntry3`) + LEFT JOIN ::objects o ON o.`id` = ll.`objectId` + WHERE c.`id` = %i + ORDER BY ll.`difficulty` ASC', + $npcId + ); + + if ($this->mapType == 2 || count($lootEntries) > 2) // always raid + $lootEntries = array_combine(array_map(fn($x) => 1 << (2 + $x), array_keys($lootEntries)), array_values($lootEntries)); + else if ($this->mapType == 1 || count($lootEntries) == 2) // dungeon or raid, assume dungeon + $lootEntries = array_combine(array_map(fn($x) => 1 << (2 - $x), array_keys($lootEntries)), array_values($lootEntries)); + } + else + $lootEntries = [4 => $_]; + + $goLoot = new LootByContainer(); + if ($goLoot->getByContainer(Loot::GAMEOBJECT, $lootEntries)) + { + $extraCols = $goLoot->extraCols; + array_push($extraCols, '$Listview.extraCols.count', '$Listview.extraCols.percent'); + if (count($lootEntries) > 1) + $extraCols[] = '$Listview.extraCols.mode'; + + $hiddenCols = ['source', 'side', 'slot', 'reqlevel', 'count']; $this->extendGlobalData($goLoot->jsGlobals); $lootResult = $goLoot->getResult(); @@ -419,12 +521,16 @@ class ObjectBaseResponse extends TemplateResponse implements ICache } $this->lvTabs->addListviewTab(new Listview(array( - 'data' => $lootResult, - 'id' => 'contains', - 'name' => '$LANG.tab_contains', - 'sort' => ['-percent', 'name'], - 'extraCols' => array_unique($extraCols), - 'hiddenCols' => $hiddenCols ?: null + 'data' => $lootResult, + 'id' => 'contains', + 'name' => '$LANG.tab_contains', + 'sort' => ['-percent', 'name'], + 'extraCols' => array_unique($extraCols), + 'hiddenCols' => $hiddenCols ?: null, + 'sort' => ['-percent', 'name'], + '_totalCount' => 10000, + 'computeDataFunc' => '$Listview.funcBox.initLootTable', + 'onAfterCreate' => '$Listview.funcBox.addModeIndicator', ), ItemList::$brickFile)); } } @@ -432,7 +538,7 @@ class ObjectBaseResponse extends TemplateResponse implements ICache // tab: Spell Focus for if ($sfId = $this->subject->getField('spellFocusId')) { - $focusSpells = new SpellList(array(['spellFocusObject', $sfId]), ['calcTotal' => true]); + $focusSpells = new SpellList(array(Listview::DEFAULT_SIZE, ['spellFocusObject', $sfId]), ['calcTotal' => true]); if (!$focusSpells->error) { $tabData = array( @@ -444,9 +550,9 @@ class ObjectBaseResponse extends TemplateResponse implements ICache $this->extendGlobalData($focusSpells->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); // create note if search limit was exceeded - if ($focusSpells->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($focusSpells->getMatches() > Listview::DEFAULT_SIZE) { - $tabData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $focusSpells->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryNarrowingString, 'LANG.lvnote_spellsfound', $focusSpells->getMatches(), Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } @@ -469,6 +575,51 @@ class ObjectBaseResponse extends TemplateResponse implements ICache ), GameObjectList::$brickFile)); } + // tab: see also + if ($this->difficulties) + { + $conditions = array( + DB::AND, + ['id', $this->difficulties], + ['id', $this->typeId, '!'] + ); + + $saObjects = new GameObjectList($conditions); + if (!$saObjects->error) + { + $data = $saObjects->getListviewData(); + if ($this->difficulties) + { + $saE = ['$Listview.extraCols.mode']; + + foreach ($data as $id => &$d) + { + if (($modeBit = array_search($id, $this->difficulties)) !== false) + { + if ($this->mapType) + $d['modes'] = ['mode' => 1 << ($modeBit + 3)]; + else + $d['modes'] = ['mode' => 2 - $modeBit]; + } + else + $d['modes'] = ['mode' => 0]; + } + } + + $tabData = array( + 'data' => $data, + 'id' => 'see-also', + 'name' => '$LANG.tab_seealso', + 'visibleCols' => ['level'], + ); + + if (isset($saE)) + $tabData['extraCols'] = $saE; + + $this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile)); + } + } + // tab: Same model as $sameModel = new GameObjectList(array(['displayId', $this->subject->getField('displayId')], ['id', $this->typeId, '!'])); if (!$sameModel->error) diff --git a/endpoints/objects/objects.php b/endpoints/objects/objects.php index e08e5782..cf4d8976 100644 --- a/endpoints/objects/objects.php +++ b/endpoints/objects/objects.php @@ -11,7 +11,7 @@ class ObjectsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::OBJECT; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'objects'; protected string $pageName = 'objects'; @@ -27,14 +27,22 @@ class ObjectsBaseResponse extends TemplateResponse implements ICache public bool $petFamPanel = false; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new GameObjectListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -42,20 +50,16 @@ class ObjectsBaseResponse extends TemplateResponse implements ICache { $this->h1 = Util::ucFirst(Lang::game('objects')); - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; if ($this->category) $conditions[] = ['typeCat', (int)$this->category[0]]; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /*************/ /* Menu Path */ @@ -91,9 +95,9 @@ class ObjectsBaseResponse extends TemplateResponse implements ICache $tabData['visibleCols'] = ['skill']; // create note if search limit was exceeded - if ($objects->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($objects->getMatches() > Listview::DEFAULT_SIZE) { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_objectsfound', $objects->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_objectsfound', $objects->getMatches(), Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } } diff --git a/endpoints/pet/pet.php b/endpoints/pet/pet.php index f383a0f7..70064a1d 100644 --- a/endpoints/pet/pet.php +++ b/endpoints/pet/pet.php @@ -10,7 +10,7 @@ class PetBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'pet'; @@ -73,13 +73,20 @@ class PetBaseResponse extends TemplateResponse implements ICache if ($this->subject->getField('exotic')) $infobox[] = '[url=?spell=53270]'.Lang::pet('exotic').'[/url]'; + // id + $infobox[] = Lang::pet('id') . $this->typeId; + // icon if ($_ = $this->subject->getField('iconId')) { - $infobox[] = Util::ucFirst(lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; $this->extendGlobalIds(Type::ICON, $_); } + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); @@ -109,7 +116,7 @@ class PetBaseResponse extends TemplateResponse implements ICache ['ct.typeFlags', NPC_TYPEFLAG_TAMEABLE, '&'], ['ct.family', $this->typeId], // displayed petType [ - 'OR', // at least neutral to at least one faction + DB::OR, // at least neutral to at least one faction ['ft.A', 1, '<'], ['ft.H', 1, '<'] ] @@ -135,7 +142,7 @@ class PetBaseResponse extends TemplateResponse implements ICache if ($mask & (1 << ($i - 1))) $list[] = $i; - $food = new ItemList(array(['i.subClass', [ITEM_SUBCLASS_FOOD, ITEM_SUBCLASS_MISC_CONSUMABLE]], ['i.FoodType', $list], Cfg::get('SQL_LIMIT_NONE'))); + $food = new ItemList(array(['i.subClass', [ITEM_SUBCLASS_FOOD, ITEM_SUBCLASS_MISC_CONSUMABLE]], ['i.FoodType', $list])); $this->extendGlobalData($food->getJSGlobals()); $this->lvTabs->addListviewTab(new Listview(array( @@ -159,13 +166,13 @@ class PetBaseResponse extends TemplateResponse implements ICache $conditions = [ ['s.typeCat', -3], // Pet-Ability [ - 'OR', + DB::OR, // match: first skillLine ['skillLine1', $this->subject->getField('skillLineId')], // match: second skillLine (if not mask) - ['AND', ['skillLine1', 0, '>'], ['skillLine2OrMask', $this->subject->getField('skillLineId')]], + [DB::AND, ['skillLine1', 0, '>'], ['skillLine2OrMask', $this->subject->getField('skillLineId')]], // match: skillLineMask (if mask) - ['AND', ['skillLine1', -1], ['skillLine2OrMask', $mask, '&']] + [DB::AND, ['skillLine1', -1], ['skillLine2OrMask', $mask, '&']] ] ]; @@ -183,7 +190,7 @@ class PetBaseResponse extends TemplateResponse implements ICache $conditions = array( ['s.typeCat', -7], [ // last rank or unranked - 'OR', + DB::OR, ['s.cuFlags', SPELL_CU_LAST_RANK, '&'], ['s.rankNo', 0] ] diff --git a/endpoints/pets/pets.php b/endpoints/pets/pets.php index 714bd151..c95fcc5c 100644 --- a/endpoints/pets/pets.php +++ b/endpoints/pets/pets.php @@ -11,7 +11,7 @@ class PetsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::PET; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'pets'; @@ -20,11 +20,11 @@ class PetsBaseResponse extends TemplateResponse implements ICache protected array $validCats = [PET_TALENT_TYPE_FEROCITY, PET_TALENT_TYPE_TENACITY, PET_TALENT_TYPE_CUNNING]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -55,7 +55,7 @@ class PetsBaseResponse extends TemplateResponse implements ICache $this->redButtons[BUTTON_WOWHEAD] = true; - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; diff --git a/endpoints/privilege/privilege.php b/endpoints/privilege/privilege.php index 6b4d30b7..9065193e 100644 --- a/endpoints/privilege/privilege.php +++ b/endpoints/privilege/privilege.php @@ -34,17 +34,17 @@ class PrivilegeBaseResponse extends TemplateResponse 17 => 'REP_REQ_PREMIUM' // premium status ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); - if (!$pageParam) + if (!$rawParam) $this->generateError(); // apply actual values - $this->repVal = Cfg::get($this->req2priv[$pageParam]); + $this->repVal = Cfg::get($this->req2priv[$rawParam]); } protected function generate() : void diff --git a/endpoints/privileges/privileges.php b/endpoints/privileges/privileges.php index 68a80ed3..83857f04 100644 --- a/endpoints/privileges/privileges.php +++ b/endpoints/privileges/privileges.php @@ -31,11 +31,11 @@ class PrivilegesBaseResponse extends TemplateResponse 17 => 'REP_REQ_PREMIUM' // premium status ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - if ($pageParam) + if ($rawParam) $this->generateError(); // apply actual values and order by requirement ASC diff --git a/endpoints/profile/avatar.php b/endpoints/profile/avatar.php index 6b6ce37f..aa8a9ed2 100644 --- a/endpoints/profile/avatar.php +++ b/endpoints/profile/avatar.php @@ -26,9 +26,9 @@ class ProfileAvatarResponse extends TextResponse 'size' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -47,7 +47,7 @@ class ProfileAvatarResponse extends TextResponse $profileId = substr($this->_get['id'], 0, -4); - $charData = DB::Aowow()->selectRow('SELECT `race`, `gender` FROM ?_profiler_profiles WHERE id = ?d', $profileId); + $charData = DB::Aowow()->selectRow('SELECT `race`, `gender` FROM ::profiler_profiles WHERE id = %i', $profileId); if (!$charData) $this->generate404(); diff --git a/endpoints/profile/delete.php b/endpoints/profile/delete.php index 0bf905cd..ed7572fd 100644 --- a/endpoints/profile/delete.php +++ b/endpoints/profile/delete.php @@ -14,9 +14,9 @@ class ProfileDeleteResponse extends TextResponse 'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']], ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -35,14 +35,12 @@ class ProfileDeleteResponse extends TextResponse return; } + $where = [['`id` IN %in', $this->_get['id']], ['`custom` = 1']]; + if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $where[] = ['`user` = %i', User::$id]; + // only flag as deleted; only custom profiles - DB::Aowow()->query( - 'UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` | ?d WHERE `id` IN (?a) AND `cuFlags` & ?d {AND `user` = ?d}', - PROFILER_CU_DELETED, - $this->_get['id'], - PROFILER_CU_PROFILE, - User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU) ? DBSIMPLE_SKIP : User::$id - ); + DB::Aowow()->qry('UPDATE ::profiler_profiles SET `deleted` = 1 WHERE %and', $where); } } diff --git a/endpoints/profile/link.php b/endpoints/profile/link.php index bcd32ab0..ad15f385 100644 --- a/endpoints/profile/link.php +++ b/endpoints/profile/link.php @@ -14,9 +14,9 @@ class ProfileLinkResponse extends TextResponse 'id' => ['filter' => FILTER_VALIDATE_INT] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -36,10 +36,10 @@ class ProfileLinkResponse extends TextResponse } // only link characters, not custom profiles - $newId = DB::Aowow()->query( - 'REPLACE INTO ?_account_profiles (`accountId`, `profileId`, `extraFlags`) - SELECT ?d, p.`id`, 0 FROM ?_profiler_profiles p WHERE p.`id` = ?d AND (`cuFlags` & ?d) = 0', - User::$id, $this->_get['id'], PROFILER_CU_PROFILE + $newId = DB::Aowow()->qry( + 'REPLACE INTO ::account_profiles (`accountId`, `profileId`, `extraFlags`) + SELECT %i, p.`id`, 0 FROM ::profiler_profiles p WHERE p.`id` = %i AND `custom` = 0', + User::$id, $this->_get['id'] ); if (!is_int($newId)) diff --git a/endpoints/profile/load.php b/endpoints/profile/load.php index 8be7a2da..17277037 100644 --- a/endpoints/profile/load.php +++ b/endpoints/profile/load.php @@ -13,9 +13,9 @@ class ProfileLoadResponse extends TextResponse 'items' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkItemList']] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -40,14 +40,14 @@ class ProfileLoadResponse extends TextResponse return; } - $pBase = DB::Aowow()->selectRow('SELECT pg.`name` AS "guildname", p.* FROM ?_profiler_profiles p LEFT JOIN ?_profiler_guild pg ON pg.`id` = p.`guild` WHERE p.`id` = ?d', $this->_get['id'][0]); + $pBase = DB::Aowow()->selectRow('SELECT pg.`name` AS "guildname", p.* FROM ::profiler_profiles p LEFT JOIN ::profiler_guild pg ON pg.`id` = p.`guild` WHERE p.`id` = %i', $this->_get['id'][0]); if (!$pBase) { trigger_error('ProfileLoadResponse - called with invalid profileId #'.$this->_get['id'][0], E_USER_WARNING); return; } - if (($pBase['cuFlags'] & PROFILER_CU_DELETED) && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + if ($pBase['deleted'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) return; @@ -56,7 +56,7 @@ class ProfileLoadResponse extends TextResponse if ($rId == $pBase['realm']) break; - if (!$rData) // realm doesn't exist or access is restricted + if ($pBase['realm'] && !$rData) // realm doesn't exist or access is restricted return; $profile = array( @@ -103,7 +103,7 @@ class ProfileLoadResponse extends TextResponse 'activity' => [], // recent raid activity [achievementId => 1] (is a subset of statistics) ); - if ($pBase['cuFlags'] & PROFILER_CU_PROFILE) + if ($pBase['custom']) { // this parameter is _really_ strange .. probably still not doing this right $profile['source'] = $pBase['realm'] ? $pBase['sourceId'] : 0; @@ -111,7 +111,7 @@ class ProfileLoadResponse extends TextResponse $profile['sourcename'] = $pBase['sourceName']; $profile['description'] = $pBase['description']; $profile['user'] = $pBase['user']; - $profile['username'] = DB::Aowow()->selectCell('SELECT `username` FROM ?_account WHERE `id` = ?d', $pBase['user']); + $profile['username'] = DB::Aowow()->selectCell('SELECT `username` FROM ::account WHERE `id` = %i', $pBase['user']); } // custom profiles inherit this when copied from real char :( @@ -123,19 +123,19 @@ class ProfileLoadResponse extends TextResponse } // bookmarks - if ($_ = DB::Aowow()->selectCol('SELECT `accountId` FROM ?_account_profiles WHERE `profileId` = ?d', $pBase['id'])) + if ($_ = DB::Aowow()->selectCol('SELECT `accountId` FROM ::account_profiles WHERE `profileId` = %i', $pBase['id'])) $profile['bookmarks'] = $_; // arena teams - [size(2|3|5) => name]; name gets urlized to use as link - if ($at = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `name` FROM ?_profiler_arena_team at JOIN ?_profiler_arena_team_member atm ON atm.`arenaTeamId` = at.`id` WHERE atm.`profileId` = ?d', $pBase['id'])) + if ($at = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `name` FROM ::profiler_arena_team at JOIN ::profiler_arena_team_member atm ON atm.`arenaTeamId` = at.`id` WHERE atm.`profileId` = %i', $pBase['id'])) $profile['arenateams'] = $at; // pets if hunter fields: [name:name, family:petFamily, npc:npcId, displayId:modelId, talents:talentString] - if ($pets = DB::Aowow()->select('SELECT `name`, `family`, `npc`, `displayId`, CONCAT("$\"", `talents`, "\"") AS "talents" FROM ?_profiler_pets WHERE `owner` = ?d', $pBase['id'])) + if ($pets = DB::Aowow()->selectAssoc('SELECT `name`, `family`, `npc`, `displayId`, CONCAT(\'$"\', `talents`, \'"\') AS "talents" FROM ::profiler_pets WHERE `owner` = %i', $pBase['id'])) $profile['pets'] = $pets; // source for custom profiles; profileId => [name, ownerId, iconString(optional)] - if ($customs = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `name`, `user`, `icon` FROM ?_profiler_profiles WHERE `sourceId` = ?d AND `sourceId` <> `id` {AND (`cuFlags` & ?d) = 0}', $pBase['id'], User::isInGroup(U_GROUP_STAFF) ? DBSIMPLE_SKIP : PROFILER_CU_DELETED)) + if ($customs = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `name`, `user`, `icon` FROM ::profiler_profiles WHERE `sourceId` = %i AND `sourceId` <> `id` AND `deleted` IN %in', $pBase['id'], User::isInGroup(U_GROUP_STAFF) ? [0, 1] : [0])) { foreach ($customs as $id => $cu) { @@ -158,37 +158,37 @@ class ProfileLoadResponse extends TextResponse // questId => [cat1, cat2] $profile['quests'] = []; - if ($quests = DB::Aowow()->selectCol('SELECT `questId` FROM ?_profiler_completion_quests WHERE `id` = ?d', $pBase['id'])) + if ($quests = DB::Aowow()->selectCol('SELECT `questId` FROM ::profiler_completion_quests WHERE `id` = %i', $pBase['id'])) { - $qList = new QuestList(array(['id', $quests], Cfg::get('SQL_LIMIT_NONE'))); + $qList = new QuestList(array(['id', $quests])); if (!$qList->error) foreach ($qList->iterate() as $id => $__) $profile['quests'][$id] = [$qList->getField('cat1'), $qList->getField('cat2')]; } // skillId => [value, max] - $profile['skills'] = DB::Aowow()->select('SELECT `skillId` AS ARRAY_KEY, `value` AS "0", `max` AS "1" FROM ?_profiler_completion_skills WHERE `id` = ?d', $pBase['id']); + $profile['skills'] = DB::Aowow()->selectAssoc('SELECT `skillId` AS ARRAY_KEY, `value` AS "0", `max` AS "1" FROM ::profiler_completion_skills WHERE `id` = %i', $pBase['id']); // factionId => amount - $profile['reputation'] = DB::Aowow()->selectCol('SELECT `factionId` AS ARRAY_KEY, `standing` FROM ?_profiler_completion_reputation WHERE `id` = ?d', $pBase['id']); + $profile['reputation'] = DB::Aowow()->selectCol('SELECT `factionId` AS ARRAY_KEY, `standing` FROM ::profiler_completion_reputation WHERE `id` = %i', $pBase['id']); // titleId => 1 - $profile['titles'] = DB::Aowow()->selectCol('SELECT `titleId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_titles WHERE `id` = ?d', $pBase['id']); + $profile['titles'] = DB::Aowow()->selectCol('SELECT `titleId` AS ARRAY_KEY, 1 FROM ::profiler_completion_titles WHERE `id` = %i', $pBase['id']); // achievementId => js date object - $profile['achievements'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, CONCAT("$new Date(", `date` * 1000, ")") FROM ?_profiler_completion_achievements WHERE `id` = ?d', $pBase['id']); + $profile['achievements'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, CONCAT("$new Date(", `date` * 1000, ")") FROM ::profiler_completion_achievements WHERE `id` = %i', $pBase['id']); // just points - $profile['achievementpoints'] = $profile['achievements'] ? DB::Aowow()->selectCell('SELECT SUM(`points`) FROM ?_achievement WHERE `id` IN (?a)', array_keys($profile['achievements'])) : 0; + $profile['achievementpoints'] = $profile['achievements'] ? DB::Aowow()->selectCell('SELECT SUM(`points`) FROM ::achievement WHERE `id` IN %in', array_keys($profile['achievements'])) : 0; // achievementId => counter - $profile['statistics'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, `counter` FROM ?_profiler_completion_statistics WHERE `id` = ?d', $pBase['id']); + $profile['statistics'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, `counter` FROM ::profiler_completion_statistics WHERE `id` = %i', $pBase['id']); // achievementId => 1 - $profile['activity'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_statistics WHERE `id` = ?d AND `date` > ?d', $pBase['id'], time() - MONTH); + $profile['activity'] = DB::Aowow()->selectCol('SELECT `achievementId` AS ARRAY_KEY, 1 FROM ::profiler_completion_statistics WHERE `id` = %i AND `date` > %i', $pBase['id'], time() - MONTH); // spellId => 1 - $profile['spells'] = DB::Aowow()->selectCol('SELECT `spellId` AS ARRAY_KEY, 1 FROM ?_profiler_completion_spells WHERE `id` = ?d', $pBase['id']); + $profile['spells'] = DB::Aowow()->selectCol('SELECT `spellId` AS ARRAY_KEY, 1 FROM ::profiler_completion_spells WHERE `id` = %i', $pBase['id']); $gItems = []; @@ -224,9 +224,9 @@ class ProfileLoadResponse extends TextResponse } } - if ($items = DB::Aowow()->select('SELECT * FROM ?_profiler_items WHERE `id` = ?d', $pBase['id'])) + if ($items = DB::Aowow()->selectAssoc('SELECT * FROM ::profiler_items WHERE `id` = %i', $pBase['id'])) { - $itemz = new ItemList(array(['id', array_column($items, 'item')], Cfg::get('SQL_LIMIT_NONE'))); + $itemz = new ItemList(array(['id', array_column($items, 'item')])); if (!$itemz->error) { $data = $itemz->getListviewData(ITEMINFO_JSON | ITEMINFO_SUBITEMS); @@ -255,7 +255,7 @@ class ProfileLoadResponse extends TextResponse // if ($au = $char->getField('auras')) // { - // $auraz = new SpellList(array(['id', $char->getField('auras')], Cfg::get('SQL_LIMIT_NONE'))); + // $auraz = new SpellList(array(['id', $char->getField('auras')])); // $dataz = $auraz->getListviewData(); // $modz = $auraz->getProfilerMods(); diff --git a/endpoints/profile/pin.php b/endpoints/profile/pin.php index aef2ef64..9b871a44 100644 --- a/endpoints/profile/pin.php +++ b/endpoints/profile/pin.php @@ -15,9 +15,9 @@ class ProfilePinResponse extends TextResponse 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -40,7 +40,7 @@ class ProfilePinResponse extends TextResponse if (!$this->_get['user'] || User::$username == $this->_get['user']) $uid = User::$id; else if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']); + $uid = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE LOWER(`username`) = LOWER(%s)', $this->_get['user']); if (!$uid) { @@ -49,9 +49,9 @@ class ProfilePinResponse extends TextResponse } // since only one character can be pinned at a time we can reset everything - DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` & ~?d WHERE `accountId` = ?d', PROFILER_CU_PINNED, $uid); + DB::Aowow()->qry('UPDATE ::account_profiles SET `extraFlags` = `extraFlags` & ~%i WHERE `accountId` = %i', PROFILER_CU_PINNED, $uid); // and set a single char if necessary (Replace, because this entry may not exist yet) - DB::Aowow()->query('REPLACE INTO ?_account_profiles (`accountId`, `profileId`, `extraFlags`) VALUES (?d, ?d, ?d)', $uid, $this->_get['id'][0], PROFILER_CU_PINNED); + DB::Aowow()->qry('REPLACE INTO ::account_profiles (`accountId`, `profileId`, `extraFlags`) VALUES (%i, %i, %i)', $uid, $this->_get['id'][0], PROFILER_CU_PINNED); } } diff --git a/endpoints/profile/private.php b/endpoints/profile/private.php index 5c72d9a5..c42a50c2 100644 --- a/endpoints/profile/private.php +++ b/endpoints/profile/private.php @@ -16,9 +16,9 @@ class ProfilePrivateResponse extends TextResponse // 'bookmarked' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ] // something with signatures? (must have bookmarked profile to create signature from) ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -48,7 +48,7 @@ class ProfilePrivateResponse extends TextResponse if (!$this->_get['user'] || User::$username == $this->_get['user']) $uid = User::$id; else if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']); + $uid = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE LOWER(`username`) = LOWER(%s)', $this->_get['user']); if (!$uid) { @@ -56,8 +56,8 @@ class ProfilePrivateResponse extends TextResponse return; } - DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` & ~?d WHERE `profileId` IN (?a) AND `accountId` = ?d', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); - DB::Aowow()->query('UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` & ~?d WHERE `id` IN (?a) AND `user` = ?d', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); + DB::Aowow()->qry('UPDATE ::account_profiles SET `extraFlags` = `extraFlags` & ~%i WHERE `profileId` IN %in AND `accountId` = %i', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); + DB::Aowow()->qry('UPDATE ::profiler_profiles SET `cuFlags` = `cuFlags` & ~%i WHERE `id` IN %in AND `user` = %i', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); } } diff --git a/endpoints/profile/profile.php b/endpoints/profile/profile.php index 284ed283..53a7f8be 100644 --- a/endpoints/profile/profile.php +++ b/endpoints/profile/profile.php @@ -67,11 +67,11 @@ class ProfileBaseResponse extends TemplateResponse // 3 possibilities // 1) already synced to aowow - if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `cuFlags` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND `name` = ? AND `renameItr` = ?d', $this->realmId, Util::ucFirst($this->subjectName), $rnItr)) + if ($subject = DB::Aowow()->selectRow('SELECT `id`, `realmGUID`, `stub` FROM ::profiler_profiles WHERE `realm` = %i AND `custom` = 0 AND `name` = %s AND `renameItr` = %i', $this->realmId, Util::ucFirst($this->subjectName), $rnItr)) { $this->typeId = $subject['id']; - if ($subject['cuFlags'] & PROFILER_CU_NEEDS_RESYNC) + if ($subject['stub']) $this->handleIncompleteData(Type::PROFILE, $subject['realmGUID']); return; @@ -82,34 +82,35 @@ class ProfileBaseResponse extends TemplateResponse $this->notFound(); // 2) not yet synced but exists on realm (and not a gm character) - if ($subject = DB::Characters($this->realmId)->selectRow( + $subjects = DB::Characters($this->realmId)->selectAssoc( 'SELECT c.`guid` AS "realmGUID", c.`name`, c.`race`, c.`class`, c.`level`, c.`gender`, c.`at_login`, g.`guildid` AS "guildGUID", IFNULL(g.`name`, "") AS "guildName", IFNULL(gm.`rank`, 0) AS "guildRank" FROM characters c LEFT JOIN guild_member gm ON gm.`guid` = c.`guid` LEFT JOIN guild g ON g.`guildid` = gm.`guildid` - WHERE c.`name` = ? AND `level` <= ?d AND (`extra_flags` & ?d) = 0', + WHERE c.`name` = %s AND `level` <= %i AND (`extra_flags` & %i) = 0', Util::ucFirst($this->subjectName), MAX_LEVEL, Profiler::CHAR_GMFLAGS - )) + ); + if ($subject = array_find($subjects ?: [], fn($x) => Util::lower($x['name']) == Util::lower($this->subjectName))) { - $subject['realm'] = $this->realmId; - $subject['cuFlags'] = PROFILER_CU_NEEDS_RESYNC; + $subject['realm'] = $this->realmId; + $subject['stub'] = 1; if ($subject['at_login'] & 0x1) - $subject['renameItr'] = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND `name` = ?', $this->realmId, $subject['name']); + $subject['renameItr'] = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ::profiler_profiles WHERE `realm` = %i AND `custom` = 0 AND `name` = %s', $this->realmId, $subject['name']); if ($subject['guildGUID']) { - // create empty guild if nessecary to satisfy foreign keys - $subject['guild'] = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_guild WHERE `realm` = ?d AND `realmGUID` = ?d', $this->realmId, $subject['guildGUID']); + // create empty guild if necessary to satisfy foreign keys + $subject['guild'] = DB::Aowow()->selectCell('SELECT `id` FROM ::profiler_guild WHERE `realm` = %i AND `realmGUID` = %i', $this->realmId, $subject['guildGUID']); if (!$subject['guild']) - $subject['guild'] = DB::Aowow()->query('INSERT INTO ?_profiler_guild (`realm`, `realmGUID`, `cuFlags`, `name`) VALUES (?d, ?d, ?d, ?)', $this->realmId, $subject['guildGUID'], PROFILER_CU_NEEDS_RESYNC, $subject['guildName']); + $subject['guild'] = DB::Aowow()->qry('INSERT INTO ::profiler_guild (`realm`, `realmGUID`, `stub`, `name`, `nameUrl`) VALUES (%i, %i, 1, %s, %s)', $this->realmId, $subject['guildGUID'], $subject['guildName'], Profiler::urlize($subject['guildName'])); } unset($subject['guildGUID'], $subject['guildName'], $subject['at_login']); // create entry from realm with enough basic info to disply tooltips - DB::Aowow()->query('REPLACE INTO ?_profiler_profiles (?#) VALUES (?a)', array_keys($subject), array_values($subject)); - $this->typeId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` = ?d', $this->realmId, $subject['realmGUID']); + DB::Aowow()->qry('REPLACE INTO ::profiler_profiles %v', $subject); + $this->typeId = DB::Aowow()->selectCell('SELECT `id` FROM ::profiler_profiles WHERE `realm` = %i AND `realmGUID` = %i', $this->realmId, $subject['realmGUID']); $this->handleIncompleteData(Type::PROFILE, $subject['realmGUID']); return; @@ -122,7 +123,10 @@ class ProfileBaseResponse extends TemplateResponse protected function generate() : void { if ($this->doResync) + { + parent::generate(); return; + } if ($this->typeId) { diff --git a/endpoints/profile/profile_power.php b/endpoints/profile/profile_power.php index ea15084a..cbc0d0e6 100644 --- a/endpoints/profile/profile_power.php +++ b/endpoints/profile/profile_power.php @@ -19,9 +19,9 @@ class ProfilePowerResponse extends TextResponse implements ICache 'domain' => ['filter' => FILTER_CALLBACK, 'options' => [Locale::class, 'tryFromDomain']] ); - public function __construct(string $pageParam) + public function __construct(private string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -30,15 +30,15 @@ class ProfilePowerResponse extends TextResponse implements ICache if ($this->_get['domain']) Lang::load($this->_get['domain']); - $this->getSubjectFromUrl($pageParam); + $this->getSubjectFromUrl($rawParam); - if ($this->subjectName) // pageParam is fully defined profiler string + if ($this->subjectName) // rawParam is fully defined profiler string { // pending rename if (preg_match('/([^\-]+)-(\d+)/i', $this->subjectName, $m)) [, $this->subjectName, $renameItr] = $m; - if ($x = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND LOWER(`name`) = ? AND `renameItr` = ?d', $this->realmId, $this->subjectName, $renameItr ?? 0)) + if ($x = DB::Aowow()->selectCell('SELECT `id` FROM ::profiler_profiles WHERE `realm` = %i AND `custom` = 0 AND `name` = %s AND `renameItr` = %i', $this->realmId, Util::ucWords($this->subjectName), $renameItr ?? 0)) $this->typeId = $x; } @@ -72,8 +72,12 @@ class ProfilePowerResponse extends TextResponse implements ICache ); } + if ($_ = $profile->getField('renameItr')) + $ri = '-'.$_; + + // the 'id' must be exactly as the js requested it or the tooltip won't register if ($this->subjectName) - $id = implode('.', [$this->region, Profiler::urlize($this->realm, true), urlencode($this->subjectName)]); + $id = urlencode($this->rawParam) . ($ri ?? ''); else $id = $this->typeId; diff --git a/endpoints/profile/public.php b/endpoints/profile/public.php index 71cb65bb..98f70eae 100644 --- a/endpoints/profile/public.php +++ b/endpoints/profile/public.php @@ -16,9 +16,9 @@ class ProfilePublicResponse extends TextResponse // 'bookmarked' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet'] ] // something with signatures? (must have bookmarked profile to create signature from) ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -48,7 +48,7 @@ class ProfilePublicResponse extends TextResponse if (!$this->_get['user'] || User::$username == $this->_get['user']) $uid = User::$id; else if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']); + $uid = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE LOWER(`username`) = LOWER(%s)', $this->_get['user']); if (!$uid) { @@ -56,8 +56,8 @@ class ProfilePublicResponse extends TextResponse return; } - DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` | ?d WHERE `profileId` IN (?a) AND `accountId` = ?d', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); - DB::Aowow()->query('UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` | ?d WHERE `id` IN (?a) AND `user` = ?d', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); + DB::Aowow()->qry('UPDATE ::account_profiles SET `extraFlags` = `extraFlags` | %i WHERE `profileId` IN %in AND `accountId` = %i', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); + DB::Aowow()->qry('UPDATE ::profiler_profiles SET `cuFlags` = `cuFlags` | %i WHERE `id` IN %in AND `user` = %i', PROFILER_CU_PUBLISHED, $this->_get['id'], $uid); } } diff --git a/endpoints/profile/resync.php b/endpoints/profile/resync.php index afeb0d4c..0b93276a 100644 --- a/endpoints/profile/resync.php +++ b/endpoints/profile/resync.php @@ -12,9 +12,9 @@ class ProfileResyncResponse extends TextResponse 'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList']] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -27,7 +27,7 @@ class ProfileResyncResponse extends TextResponse */ protected function generate() : void { - if ($chars = DB::Aowow()->select('SELECT `realm`, `realmGUID` FROM ?_profiler_profiles WHERE `id` IN (?a)', $this->_get['id'])) + if ($chars = DB::Aowow()->selectAssoc('SELECT `realm`, `realmGUID` FROM ::profiler_profiles WHERE `id` IN %in', $this->_get['id'])) { foreach ($chars as $c) Profiler::scheduleResync(Type::PROFILE, $c['realm'], $c['realmGUID']); diff --git a/endpoints/profile/save.php b/endpoints/profile/save.php index b063cd39..70a4e9f7 100644 --- a/endpoints/profile/save.php +++ b/endpoints/profile/save.php @@ -32,12 +32,12 @@ class ProfileSaveResponse extends TextResponse 'copy' => ['filter' => FILTER_VALIDATE_INT ], 'public' => ['filter' => FILTER_VALIDATE_INT ], 'gearscore' => ['filter' => FILTER_VALIDATE_INT ], - 'inv' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned'], 'flags' => FILTER_REQUIRE_ARRAY] + 'inv' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdList'], 'flags' => FILTER_REQUIRE_ARRAY] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -72,7 +72,8 @@ class ProfileSaveResponse extends TextResponse 'glyphs2' => $this->_post['glyphs2'], 'gearscore' => $this->_post['gearscore'], 'icon' => $this->_post['icon'], - 'cuFlags' => PROFILER_CU_PROFILE | ($this->_post['public'] ? PROFILER_CU_PUBLISHED : 0) + 'custom' => 1, + 'cuFlags' => $this->_post['public'] ? PROFILER_CU_PUBLISHED : 0 ); // remnant of a conflict between wotlk generic icons and cata+ auto-generated, char-based icons (see profile=avatar) @@ -88,26 +89,26 @@ class ProfileSaveResponse extends TextResponse if ($_ = $this->_post['copy']) // gets set to source profileId when "save as" is clicked. Whats the difference to 'source' though? { // get character origin info if possible - if ($r = DB::Aowow()->selectCell('SELECT `realm` FROM ?_profiler_profiles WHERE `id` = ?d AND `realm` IS NOT NULL', $_)) + if ($r = DB::Aowow()->selectCell('SELECT `realm` FROM ::profiler_profiles WHERE `id` = %i AND `custom` = 0', $_)) $cuProfile['realm'] = $r; $cuProfile['sourceId'] = $_; } if (!empty($cuProfile['sourceId'])) - $cuProfile['sourceName'] = DB::Aowow()->selectCell('SELECT `name` FROM ?_profiler_profiles WHERE `id` = ?d', $cuProfile['sourceId']); + $cuProfile['sourceName'] = DB::Aowow()->selectCell('SELECT `name` FROM ::profiler_profiles WHERE `id` = %i', $cuProfile['sourceId']); $charId = -1; if ($id = $this->_get['id'][0]) // update { - if ($charId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `id` = ?d', $id)) - DB::Aowow()->query('UPDATE ?_profiler_profiles SET ?a WHERE `id` = ?d', $cuProfile, $id); + if ($charId = DB::Aowow()->selectCell('SELECT `id` FROM ::profiler_profiles WHERE `id` = %i', $id)) + DB::Aowow()->qry('UPDATE ::profiler_profiles SET %a WHERE `id` = %i', $cuProfile, $id); } else // new { - $nProfiles = DB::Aowow()->selectCell('SELECT COUNT(*) FROM ?_profiler_profiles WHERE `user` = ?d AND (`cuFlags` & ?d) = 0 AND `realmGUID` IS NULL', User::$id, PROFILER_CU_DELETED); + $nProfiles = DB::Aowow()->selectCell('SELECT COUNT(*) FROM ::profiler_profiles WHERE `user` = %i AND `deleted` = 0 AND `custom` = 1', User::$id); if ($nProfiles < 10 || User::isPremium()) - if ($newId = DB::Aowow()->query('INSERT INTO ?_profiler_profiles (?#) VALUES (?a)', array_keys($cuProfile), array_values($cuProfile))) + if ($newId = DB::Aowow()->qry('INSERT INTO ::profiler_profiles %v', $cuProfile)) $charId = $newId; } @@ -139,9 +140,15 @@ class ProfileSaveResponse extends TextResponse { foreach ($this->_post['inv'] as $slot => $itemData) { + if (!$itemData) + { + trigger_error('ProfileSaveResponse::generate - skipping malformed inventory definition for slot #'.$slot.': '.Util::toString($itemData), E_USER_NOTICE); + continue; + } + if ($slot + 1 == array_sum($itemData)) // only slot definition set => empty slot { - DB::Aowow()->query('DELETE FROM ?_profiler_items WHERE `id` = ?d AND `slot` = ?d', $charId, $itemData[0]); + DB::Aowow()->qry('DELETE FROM ::profiler_items WHERE `id` = %i AND `slot` = %i', $charId, $itemData[0]); continue; } @@ -169,7 +176,7 @@ class ProfileSaveResponse extends TextResponse // looks good array_unshift($itemData, $charId); - DB::Aowow()->query('REPLACE INTO ?_profiler_items (?#) VALUES (?a)', $keys, $itemData); + DB::Aowow()->qry('REPLACE INTO ::profiler_items %v', array_combine($keys, $itemData)); } } } diff --git a/endpoints/profile/status.php b/endpoints/profile/status.php index 15d0d781..ac086f84 100644 --- a/endpoints/profile/status.php +++ b/endpoints/profile/status.php @@ -14,9 +14,9 @@ class ProfileStatusResponse extends TextResponse 'arena-team' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkEmptySet']] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -31,15 +31,15 @@ class ProfileStatusResponse extends TextResponse { // roster resync for this guild was requested -> get char list if ($this->_get['guild']) - $ids = DB::Aowow()->selectCol('SELECT `id` FROM ?_profiler_profiles WHERE `guild` IN (?a)', $this->_get['id']); + $ids = DB::Aowow()->selectCol('SELECT `id` FROM ::profiler_profiles WHERE `guild` IN %in', $this->_get['id']); else if ($this->_get['arena-team']) - $ids = DB::Aowow()->selectCol('SELECT `profileId` FROM ?_profiler_arena_team_member WHERE `arenaTeamId` IN (?a)', $this->_get['id']); + $ids = DB::Aowow()->selectCol('SELECT `profileId` FROM ::profiler_arena_team_member WHERE `arenaTeamId` IN %in', $this->_get['id']); else $ids = $this->_get['id']; if (!$ids) { - trigger_error('ProfileStatusResponse - no profileIds to resync'.($this->_get['guild'] ? ' for guild #'.$this->_get['guild'] : ($this->_get['arena-team'] ? ' for areana team #'.$this->_get['arena-team'] : '')), E_USER_ERROR); + trigger_error('ProfileStatusResponse - no profileIds to resync'.($this->_get['guild'] ? ' for guild #' : ($this->_get['arena-team'] ? ' for areana team #' : ' #')).Util::toString($this->_get['id']), E_USER_WARNING); $this->result = Util::toJSON([1, [PR_QUEUE_STATUS_ERROR, 0, 0, PR_QUEUE_ERROR_CHAR]]); } diff --git a/endpoints/profile/unlink.php b/endpoints/profile/unlink.php index 8904db6e..322521e3 100644 --- a/endpoints/profile/unlink.php +++ b/endpoints/profile/unlink.php @@ -15,9 +15,9 @@ class ProfileUnlinkResponse extends TextResponse 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -47,7 +47,7 @@ class ProfileUnlinkResponse extends TextResponse if (!$this->_get['user'] || User::$username == $this->_get['user']) $uid = User::$id; else if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']); + $uid = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE LOWER(`username`) = LOWER(%s)', $this->_get['user']); if (!$uid) { @@ -55,7 +55,7 @@ class ProfileUnlinkResponse extends TextResponse return; } - DB::Aowow()->query('DELETE FROM ?_account_profiles WHERE `accountId` = ?d AND `profileId` IN (?a)', $uid, $this->_get['id']); + DB::Aowow()->qry('DELETE FROM ::account_profiles WHERE `accountId` = %i AND `profileId` IN %in', $uid, $this->_get['id']); } } diff --git a/endpoints/profile/unpin.php b/endpoints/profile/unpin.php index 7139f15a..dceb00f8 100644 --- a/endpoints/profile/unpin.php +++ b/endpoints/profile/unpin.php @@ -15,9 +15,9 @@ class ProfileUnpinResponse extends TextResponse 'user' => ['filter' => FILTER_CALLBACK, 'options' => [Util::class, 'validateUsername']] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generate404(); @@ -40,7 +40,7 @@ class ProfileUnpinResponse extends TextResponse if (!$this->_get['user'] || User::$username == $this->_get['user']) $uid = User::$id; else if ($this->_get['user'] && User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $uid = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $this->_get['user']); + $uid = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE LOWER(`username`) = LOWER(%s)', $this->_get['user']); if (!$uid) { @@ -48,7 +48,7 @@ class ProfileUnpinResponse extends TextResponse return; } - DB::Aowow()->query('UPDATE ?_account_profiles SET `extraFlags` = `extraFlags` & ~?d WHERE `accountId` = ?d', PROFILER_CU_PINNED, $uid); + DB::Aowow()->qry('UPDATE ::account_profiles SET `extraFlags` = `extraFlags` & ~%i WHERE `accountId` = %i', PROFILER_CU_PINNED, $uid); } } diff --git a/endpoints/profiles/profiles.php b/endpoints/profiles/profiles.php index e3d4dd78..8821cb0c 100644 --- a/endpoints/profiles/profiles.php +++ b/endpoints/profiles/profiles.php @@ -33,11 +33,11 @@ class ProfilesBaseResponse extends TemplateResponse implements IProfilerList private int $sumSubjects = 0; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getSubjectFromUrl($pageParam); + $this->getSubjectFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); if (!Cfg::get('PROFILER_ENABLE')) $this->generateError(); @@ -51,12 +51,20 @@ class ProfilesBaseResponse extends TemplateResponse implements IProfilerList if ($this->realm && $r['name'] != $this->realm) continue; - $this->sumSubjects += DB::Characters($idx)->selectCell('SELECT COUNT(*) FROM characters WHERE `deleteInfos_Name` IS NULL AND `level` <= ?d AND (`extra_flags` & ?) = 0', MAX_LEVEL, Profiler::CHAR_GMFLAGS); + $this->sumSubjects += DB::Characters($idx)->selectCell('SELECT COUNT(*) FROM characters WHERE `deleteInfos_Name` IS NULL AND `level` <= %i AND (`extra_flags` & ?) = 0', MAX_LEVEL, Profiler::CHAR_GMFLAGS); $realms[] = $idx; } - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; + if ($this->category) + $this->subCat = '='.implode('.', $this->category); + $this->filter = new ProfileListFilter($this->_get['filter'] ?? '', ['realms' => $realms]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -64,8 +72,6 @@ class ProfilesBaseResponse extends TemplateResponse implements IProfilerList { $this->h1 = Util::ucFirst(Lang::game('profiles')); - $this->filter->evalCriteria(); - /*************/ /* Menu Path */ @@ -90,12 +96,10 @@ class ProfilesBaseResponse extends TemplateResponse implements IProfilerList /* Main Content */ /****************/ - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - $fiExtraCols = $this->filter->fiExtraCols; $lvData = []; @@ -173,12 +177,12 @@ class ProfilesBaseResponse extends TemplateResponse implements IProfilerList $lvVisibleCols[] = 'guildrank'; // create note if search limit was exceeded - if ($this->filter->query && $profiles->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($this->filter->query && $profiles->getMatches() > Listview::DEFAULT_SIZE) { $lvNote = sprintf(Util::$tryFilteringString, 'LANG.lvnote_charactersfound2', $this->sumSubjects, $profiles->getMatches()); $lv_truncated = 1; } - else if ($profiles->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + else if ($profiles->getMatches() > Listview::DEFAULT_SIZE) $lvNote = sprintf(Util::$tryFilteringString, 'LANG.lvnote_charactersfound', $this->sumSubjects, 0); if ($this->filter->useLocalList) diff --git a/endpoints/quest/quest.php b/endpoints/quest/quest.php index 305900d6..b3d1d74e 100644 --- a/endpoints/quest/quest.php +++ b/endpoints/quest/quest.php @@ -10,7 +10,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'quest'; protected string $pageName = 'quest'; @@ -64,6 +64,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache $_flags = $this->subject->getField('flags'); $_specialFlags = $this->subject->getField('specialFlags'); $_side = ChrRace::sideFromMask($this->subject->getField('reqRaceMask')); + $hasCompletion = !($_flags & QUEST_FLAG_UNAVAILABLE || $this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW); /*************/ @@ -116,11 +117,11 @@ class QuestBaseResponse extends TemplateResponse implements ICache } // loremaster (i dearly hope those flags cover every case...) - if ($this->subject->getField('zoneOrSortBak') > 0 && !$this->subject->isRepeatable()) + if ($this->subject->getField('questSortIdBak') > 0 && !$this->subject->isRepeatable()) { $conditions = array( ['ac.type', ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUESTS_IN_ZONE], - ['ac.value1', $this->subject->getField('zoneOrSortBak')], + ['ac.value1', $this->subject->getField('questSortIdBak')], ['a.faction', $_side, '&'] ); $loremaster = new AchievementList($conditions); @@ -152,23 +153,23 @@ class QuestBaseResponse extends TemplateResponse implements ICache else if ($_specialFlags & QUEST_FLAG_SPECIAL_MONTHLY) $_[] = Lang::quest('monthly'); - if ($t = $this->subject->getField('type')) + if ($t = $this->subject->getField('questInfoId')) $_[] = Lang::quest('questInfo', $t); if ($_) $infobox[] = Lang::game('type').implode(' ', $_); // side - $infobox[] = Lang::main('side') . match ($this->subject->getField('faction')) + $infobox[] = Lang::main('side') . match ($_side) { SIDE_ALLIANCE => '[span class=icon-alliance]'.Lang::game('si', SIDE_ALLIANCE).'[/span]', SIDE_HORDE => '[span class=icon-horde]'.Lang::game('si', SIDE_HORDE).'[/span]', default => Lang::game('si', SIDE_BOTH) // 0, 3 }; - $jsg = []; // races - if ($_ = Lang::getRaceString($this->subject->getField('reqRaceMask'), $jsg, Lang::FMT_MARKUP)) + $jsg = []; + if (($_ = Lang::getRaceString($this->subject->getField('reqRaceMask'), $jsg, Lang::FMT_MARKUP)) && $jsg) { $this->extendGlobalIds(Type::CHR_RACE, ...$jsg); $t = count($jsg) == 1 ? Lang::game('race') : Lang::game('races'); @@ -176,6 +177,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache } // classes + $jsg = []; if ($_ = Lang::getClassString($this->subject->getField('reqClassMask'), $jsg, Lang::FMT_MARKUP)) { $this->extendGlobalIds(Type::CHR_CLASS, ...$jsg); @@ -196,9 +198,9 @@ class QuestBaseResponse extends TemplateResponse implements ICache // timer if ($_ = $this->subject->getField('timeLimit')) - $infobox[] = Lang::quest('timer').Util::formatTime($_ * 1000); + $infobox[] = Lang::quest('timer').DateTime::formatTimeElapsedFloat($_ * 1000); - $startEnd = DB::Aowow()->select('SELECT * FROM ?_quests_startend WHERE `questId` = ?d', $this->typeId); + $startEnd = DB::Aowow()->selectAssoc('SELECT * FROM ::quests_startend WHERE `questId` = %i', $this->typeId); // start $start = '[icon name=quest_start'.($this->subject->isRepeatable() ? '_daily' : '').']'.Lang::event('start').'[/icon]'; @@ -231,7 +233,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache $infobox[] = implode('[br]', $e); // auto accept - if ($_flags & QUEST_FLAG_AUTO_ACCEPT) + if ($this->subject->isAutoAccept()) $infobox[] = Lang::quest('autoaccept'); // Repeatable @@ -242,7 +244,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache $infobox[] = $_flags & QUEST_FLAG_SHARABLE ? Lang::quest('sharable') : Lang::quest('notSharable'); // Keeps you PvP flagged - if ($this->subject->isPvPEnabled()) + if ($_flags & QUEST_FLAG_FLAGS_PVP) $infobox[] = Lang::quest('keepsPvpFlag'); // difficulty (todo (low): formula unclear. seems to be [minLevel,] -4, -2, (level), +3, +(9 to 15)) @@ -271,8 +273,25 @@ class QuestBaseResponse extends TemplateResponse implements ICache $infobox[] = Lang::game('difficulty').implode('[small]  [/small]', $_); } + // id + $infobox[] = Lang::quest('id') . $this->typeId; + + // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) + if (Cfg::get('PROFILER_ENABLE') && $hasCompletion) + { + $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ::profiler_completion_quests WHERE `questId` = %i', $this->typeId); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ::profiler_profiles WHERE `custom` = 0 AND `stub` = 0'); + $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); + + // completion row added by InfoboxMarkup + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) - $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', $hasCompletion); /*******************/ @@ -300,7 +319,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache $olItems[$i] = [$id, $qty, $id == $olItems[0][0]]; } - if ($ids = array_column($olItems, 0)) + if ($ids = array_filter(array_column($olItems, 0))) { $olItemData = new ItemList(array(['id', $ids])); $this->extendGlobalData($olItemData->getJSGlobals(GLOBALINFO_SELF)); @@ -316,11 +335,11 @@ class QuestBaseResponse extends TemplateResponse implements ICache if (!$olItemData->getEntry($itemId)) { - $this->objectiveList[] = [0, new IconElement(0, 0, Util::ucFirst(Lang::game('item')).' #'.$itemId, $qty > 1 ? $qty : '', extraText: $provided ? Lang::quest('provided') : null)]; + $this->objectiveList[] = new IconElement(0, 0, Util::ucFirst(Lang::game('item')).' #'.$itemId, $qty > 1 ? $qty : '', size: IconElement::SIZE_SMALL, extraText: $provided ? Lang::quest('provided') : null); continue; } - $this->objectiveList[] = [0, new IconElement( + $this->objectiveList[] = new IconElement( Type::ITEM, $itemId, Lang::unescapeUISequences($olItemData->json[$itemId]['name'], Lang::FMT_HTML), @@ -329,7 +348,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache size: IconElement::SIZE_SMALL, element: 'iconlist-icon', extraText: $provided ? Lang::quest('provided') : null - )]; + ); } // if providd item is not required by quest, list it below other requirements @@ -365,7 +384,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache // .. creature kills if ($ids = array_keys($olNPCs)) { - $olNPCData = new CreatureList(array('OR', ['id', $ids], ['killCredit1', $ids], ['killCredit2', $ids])); + $olNPCData = new CreatureList(array(DB::OR, ['id', $ids], ['killCredit1', $ids], ['killCredit2', $ids])); $this->extendGlobalData($olNPCData->getJSGlobals(GLOBALINFO_SELF)); // create proxy-references @@ -395,17 +414,17 @@ class QuestBaseResponse extends TemplateResponse implements ICache array_slice($proxies, ceil(count($proxies) / 2), null, true) ); - $this->objectiveList[] = [2, array( + $this->objectiveList[] = array( 'id' => $i, 'text' => ($altText ?: Util::localizedString($olNPCData->getEntry($i), 'name')) . ((($_specialFlags & QUEST_FLAG_SPECIAL_SPELLCAST) || $altText) ? '' : ' '.Lang::achievement('slain')), 'qty' => $qty > 1 ? $qty : 0, 'proxy' => array_filter($proxies) - )]; + ); } else if (!$olNPCData->getEntry($i)) - $this->objectiveList[] = [0, new IconElement(0, 0, Util::ucFirst(Lang::game('npc')).' #'.$i, $qty > 1 ? $qty : '')]; + $this->objectiveList[] = new IconElement(0, 0, Util::ucFirst(Lang::game('npc')).' #'.$i, $qty > 1 ? $qty : ''); else - $this->objectiveList[] = [0, new IconElement( + $this->objectiveList[] = new IconElement( Type::NPC, $i, $altText ?: Util::localizedString($olNPCData->getEntry($i), 'name'), @@ -413,7 +432,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache size: IconElement::SIZE_SMALL, element: 'iconlist-icon', extraText: (($_specialFlags & QUEST_FLAG_SPECIAL_SPELLCAST) || $altText) ? '' : Lang::achievement('slain'), - )]; + ); } } @@ -429,16 +448,16 @@ class QuestBaseResponse extends TemplateResponse implements ICache continue; if (!$olGOData->getEntry($i)) - $this->objectiveList[] = [0, new IconElement(0, 0, Util::ucFirst(Lang::game('object')).' #'.$i, $qty > 1 ? $qty : '')]; + $this->objectiveList[] = new IconElement(0, 0, Util::ucFirst(Lang::game('object')).' #'.$i, $qty > 1 ? $qty : '', size: IconElement::SIZE_SMALL); else - $this->objectiveList[] = [0, new IconElement( + $this->objectiveList[] = new IconElement( Type::OBJECT, $i, $altText ?: Lang::unescapeUISequences(Util::localizedString($olGOData->getEntry($i), 'name'), Lang::FMT_HTML), $qty > 1 ? $qty : '', size: IconElement::SIZE_SMALL, element: 'iconlist-icon', - )]; + ); } } @@ -463,14 +482,14 @@ class QuestBaseResponse extends TemplateResponse implements ICache if (!$i || !in_array($i, $olFactionsData->getFoundIDs())) continue; - $this->objectiveList[] = [0, new IconElement( + $this->objectiveList[] = new IconElement( Type::FACTION, $i, Util::localizedString($olFactionsData->getEntry($i), 'name'), size: IconElement::SIZE_SMALL, element: 'iconlist-icon', extraText: sprintf(Util::$dfnString, $val.' '.Lang::achievement('points'), '('.Lang::getReputationLevelForPoints($val).')') - )]; + ); } } @@ -478,16 +497,16 @@ class QuestBaseResponse extends TemplateResponse implements ICache if ($_ = $this->subject->getField('sourceSpellId')) { $this->extendGlobalIds(Type::SPELL, $_); - $this->objectiveList[] = [0, new IconElement(Type::SPELL, $_, SpellList::getName($_), extraText: Lang::quest('provided'), element: 'iconlist-icon')]; + $this->objectiveList[] = new IconElement(Type::SPELL, $_, SpellList::getName($_), extraText: Lang::quest('provided'), element: 'iconlist-icon', size: IconElement::SIZE_SMALL); } // required money if ($this->subject->getField('rewardOrReqMoney') < 0) - $this->objectiveList[] = [1, Lang::quest('reqMoney', [Util::formatMoney(abs($this->subject->getField('rewardOrReqMoney')))])]; + $this->objectiveList[] = Lang::quest('reqMoney', [Util::formatMoney(abs($this->subject->getField('rewardOrReqMoney')))]); // required pvp kills if ($_ = $this->subject->getField('reqPlayerKills')) - $this->objectiveList[] = [1, Lang::quest('playerSlain', [$_])]; + $this->objectiveList[] = Lang::quest('playerSlain', [$_]); /**********/ @@ -500,8 +519,8 @@ class QuestBaseResponse extends TemplateResponse implements ICache // todo (med): this double list creation very much sucks ... $getItemSource = function ($itemId, $method = 0) use (&$mapNPCs, &$mapGOs) { - $lootTabs = new Loot(); - if ($lootTabs->getByItem($itemId)) + $lootTabs = new LootByItem($itemId); + if ($lootTabs->getByItem()) { /* todo (med): sanity check: @@ -513,22 +532,22 @@ class QuestBaseResponse extends TemplateResponse implements ICache for the moment: if an item has >10 sources, only display sources with >80% chance - always filter sources with <1% chance + always filter sources with <5% chance */ $nSources = 0; foreach ($lootTabs->iterate() as [$type, $data]) if ($type == 'creature' || $type == 'object') - $nSources += count(array_filter($data['data'], function($val) { return $val['percent'] >= 1.0; })); + $nSources += count(array_filter($data['data'], fn($x) => $x['percent'] >= 5.0)); - foreach ($lootTabs->iterate() as $idx => [$file, $tabData]) + foreach ($lootTabs->iterate() as [$file, $tabData]) { if (!$tabData['data']) continue; foreach ($tabData['data'] as $data) { - if ($data['percent'] < 1.0) + if ($data['percent'] < 5.0) continue; if ($nSources > 10 && $data['percent'] < 80.0) @@ -553,9 +572,9 @@ class QuestBaseResponse extends TemplateResponse implements ICache // dear god, if you are one of the types who puts queststarter-items in container-items, in conatiner-items, in container-items, in container-GOs .. you should kill yourself by killing yourself! // so yeah .. no recursion checking $vendors = DB::World()->selectCol( - 'SELECT nv.`entry` FROM npc_vendor nv WHERE nv.`item` = ?d UNION - SELECT nv1.`entry` FROM npc_vendor nv1 JOIN npc_vendor nv2 ON -nv1.`item` = nv2.`entry` WHERE nv2.`item` = ?d UNION - SELECT c.`id` FROM game_event_npc_vendor genv JOIN creature c ON c.`guid` = genv.`guid` WHERE genv.`item` = ?d', + 'SELECT nv.`entry` FROM npc_vendor nv WHERE nv.`item` = %i UNION + SELECT nv1.`entry` FROM npc_vendor nv1 JOIN npc_vendor nv2 ON -nv1.`item` = nv2.`entry` WHERE nv2.`item` = %i UNION + SELECT c.`id` FROM game_event_npc_vendor genv JOIN creature c ON c.`guid` = genv.`guid` WHERE genv.`item` = %i', $itemId, $itemId, $itemId ); foreach ($vendors as $v) @@ -615,16 +634,16 @@ class QuestBaseResponse extends TemplateResponse implements ICache // PSA: 'redundant' data is on purpose (e.g. creature required for kill, also dropps item required to collect) // external events - $endTextWrapper = '%s'; + $endText = $this->subject->parseText('end', false); if ($_specialFlags & QUEST_FLAG_SPECIAL_EXT_COMPLETE) { // areatrigger - if ($atir = DB::Aowow()->selectCol('SELECT `id` FROM ?_areatrigger WHERE `type` = ?d AND `quest` = ?d', AT_TYPE_OBJECTIVE, $this->typeId)) + if ($atir = DB::Aowow()->selectCol('SELECT `id` FROM ::areatrigger WHERE `type` = %i AND `quest` = %i', AT_TYPE_OBJECTIVE, $this->typeId)) { - if ($atSpawns = DB::AoWoW()->select('SELECT `typeId` AS ARRAY_KEY, `posX`, `posY`, `floor`, `areaId` FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a)', Type::AREATRIGGER, $atir)) + if ($atSpawns = DB::AoWoW()->selectAssoc('SELECT `typeId` AS ARRAY_KEY, `posX`, `posY`, `floor`, `areaId` FROM ::spawns WHERE `type` = %i AND `typeId` IN %in', Type::AREATRIGGER, $atir)) { if (User::isInGroup(U_GROUP_STAFF)) - $endTextWrapper = '<a href="?areatrigger='.$atir[0].'">%s</a>'; + $endText = '<a href="?areatrigger='.$atir[0].'">'.($endText ?: Lang::areatrigger('unnamed', [$atir[0]])).'</a>'; foreach ($atSpawns as $atId => $atsp) { @@ -632,7 +651,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache 'type' => User::isInGroup(U_GROUP_STAFF) ? Type::AREATRIGGER : -1, 'id' => $atId, 'point' => 'requirement', - 'name' => $this->subject->parseText('end', false), + 'name' => $this->subject->parseText('end', false) ?: Lang::areatrigger('unnamed', [$atir[0]]), 'coord' => [$atsp['posX'], $atsp['posY']], 'coords' => [[$atsp['posX'], $atsp['posY']]], 'objective' => $objectiveIdx++ @@ -653,9 +672,9 @@ class QuestBaseResponse extends TemplateResponse implements ICache } } // complete-spell - else if ($endSpell = new SpellList(array('OR', ['AND', ['effect1Id', SPELL_EFFECT_QUEST_COMPLETE], ['effect1MiscValue', $this->typeId]], ['AND', ['effect2Id', SPELL_EFFECT_QUEST_COMPLETE], ['effect2MiscValue', $this->typeId]], ['AND', ['effect3Id', SPELL_EFFECT_QUEST_COMPLETE], ['effect3MiscValue', $this->typeId]]))) + else if ($endSpell = new SpellList(array(DB::OR, [DB::AND, ['effect1Id', SPELL_EFFECT_QUEST_COMPLETE], ['effect1MiscValue', $this->typeId]], [DB::AND, ['effect2Id', SPELL_EFFECT_QUEST_COMPLETE], ['effect2MiscValue', $this->typeId]], [DB::AND, ['effect3Id', SPELL_EFFECT_QUEST_COMPLETE], ['effect3MiscValue', $this->typeId]]))) if (!$endSpell->error) - $endTextWrapper = '<a href="?spell='.$endSpell->id.'">%s</a>'; + $endText = '<a href="?spell='.$endSpell->id.'">'.($endText ?: $endSpell->getField('name', true)).'</a>'; } // ..adding creature kill requirements @@ -871,15 +890,15 @@ class QuestBaseResponse extends TemplateResponse implements ICache /* Main Content */ /****************/ - $this->series = $this->createSeries($_side); - $this->gains = $this->createGains(); - $this->rewards = $this->createRewards($_side); + $this->series = $this->createSeries(); + $this->gains = $this->createGains($_side); + $this->rewards = $this->createRewards(); $this->objectives = $this->subject->parseText('objectives', false); $this->details = $this->subject->parseText('details', false); $this->offerReward = $this->subject->parseText('offerReward', false); $this->requestItems = $this->subject->parseText('requestItems', false); $this->completed = $this->subject->parseText('completed', false); - $this->end = sprintf($endTextWrapper, $this->subject->parseText('end', false)); + $this->end = $endText; $this->suggestedPl = $this->subject->getField('suggestedPlayers'); $this->unavailable = $_flags & QUEST_FLAG_UNAVAILABLE || $this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW; $this->redButtons = array( @@ -897,7 +916,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache $this->addScript([SC_CSS_FILE, 'css/Book.css']); // factionchange-equivalent - if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = ?d, `alliance_id`, -`horde_id`) FROM player_factionchange_quests WHERE `alliance_id` = ?d OR `horde_id` = ?d', $this->typeId, $this->typeId, $this->typeId)) + if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = %i, `alliance_id`, -`horde_id`) FROM player_factionchange_quests WHERE `alliance_id` = %i OR `horde_id` = %i', $this->typeId, $this->typeId, $this->typeId)) { $altQuest = new QuestList(array(['id', abs($pendant)])); if (!$altQuest->error) @@ -919,7 +938,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); // tab: see also - $seeAlso = new QuestList(array(['name_loc'.Lang::getLocale()->value, '%'.Util::htmlEscape($this->subject->getField('name', true)).'%'], ['id', $this->typeId, '!'])); + $seeAlso = new QuestList(array(['name_loc'.Lang::getLocale()->value, Util::htmlEscape($this->subject->getField('name', true))], ['id', $this->typeId, '!'])); if (!$seeAlso->error) { $this->extendGlobalData($seeAlso->getJSGlobals()); @@ -943,9 +962,9 @@ class QuestBaseResponse extends TemplateResponse implements ICache } // tab: spawning pool (for the swarm) - if ($qp = DB::World()->selectCol('SELECT qpm2.`questId` FROM quest_pool_members qpm1 JOIN quest_pool_members qpm2 ON qpm1.`poolId` = qpm2.`poolId` WHERE qpm1.`questId` = ?d', $this->typeId)) + if ($qp = DB::World()->selectCol('SELECT qpm2.`questId` FROM quest_pool_members qpm1 JOIN quest_pool_members qpm2 ON qpm1.`poolId` = qpm2.`poolId` WHERE qpm1.`questId` = %i', $this->typeId)) { - $max = DB::World()->selectCell('SELECT `numActive` FROM quest_pool_template qpt JOIN quest_pool_members qpm ON qpm.`poolId` = qpt.`poolId` WHERE qpm.`questId` = ?d', $this->typeId); + $max = DB::World()->selectCell('SELECT `numActive` FROM quest_pool_template qpt JOIN quest_pool_members qpm ON qpm.`poolId` = qpt.`poolId` WHERE qpm.`questId` = %i', $this->typeId); $pooledQuests = new QuestList(array(['id', $qp])); if (!$pooledQuests->error) { @@ -961,15 +980,33 @@ class QuestBaseResponse extends TemplateResponse implements ICache // tab: conditions $cnd = new Conditions(); - $cnd->getBySourceEntry($this->typeId, Conditions::SRC_QUEST_AVAILABLE, Conditions::SRC_QUEST_SHOW_MARK) + $cnd->getBySource([Conditions::SRC_QUEST_AVAILABLE, Conditions::SRC_QUEST_SHOW_MARK], entry: $this->typeId) ->getByCondition(Type::QUEST, $this->typeId) ->prepare(); - if ($_ = $this->subject->getField('reqMinRepFaction')) - $cnd->addExternalCondition(Conditions::SRC_QUEST_AVAILABLE, '0:'.$this->typeId, [Conditions::REPUTATION_RANK, $_, 1 << Game::getReputationLevelForPoints($this->subject->getField('reqMinRepValue'))]); - if ($_ = $this->subject->getField('reqMaxRepFaction')) - $cnd->addExternalCondition(Conditions::SRC_QUEST_AVAILABLE, '0:'.$this->typeId, [-Conditions::REPUTATION_RANK, $_, 1 << Game::getReputationLevelForPoints($this->subject->getField('reqMaxRepValue'))]); + $minRepFac = $this->subject->getField('reqMinRepFaction'); + $maxRepFac = $this->subject->getField('reqMaxRepFaction'); + // add +/- 2 to contain edgecases. ie a reqMaxRepValue of 1 should not include the whole of REP_NEUTRAL + $minRepRank = $minRepFac ? Game::getReputationLevelForPoints($this->subject->getField('reqMinRepValue') + 2) : REP_HATED; + $maxRepRank = $maxRepFac ? Game::getReputationLevelForPoints($this->subject->getField('reqMaxRepValue') - 2) : REP_EXALTED; + + $convertRankBits = function (int $minRank, int $maxRank) : int + { + $bits = 0; + for ($i = $minRank; $i <= $maxRank; $i++) + $bits |= (1 << $i); + + return $bits; + }; + + if ($minRepFac && $maxRepFac && $minRepFac <> $maxRepFac) + { + $cnd->addExternalCondition(Conditions::SRC_QUEST_AVAILABLE, '0:'.$this->typeId, [Conditions::REPUTATION_RANK, $minRepFac, $convertRankBits($minRepRank, REP_EXALTED)]); + $cnd->addExternalCondition(Conditions::SRC_QUEST_AVAILABLE, '0:'.$this->typeId, [Conditions::REPUTATION_RANK, $maxRepFac, $convertRankBits(REP_HATED, $maxRepRank)]); + } + else if (($_ = $minRepFac) || ($_ = $maxRepFac)) + $cnd->addExternalCondition(Conditions::SRC_QUEST_AVAILABLE, '0:'.$this->typeId, [Conditions::REPUTATION_RANK, $_, $convertRankBits($minRepRank, $maxRepRank)]); if ($tab = $cnd->toListviewTab()) { @@ -980,7 +1017,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache parent::generate(); } - private function createRewards(int $side) : ?array + private function createRewards() : ?array { $rewards = [[], [], [], '']; // [spells, items, choice, money] @@ -1037,9 +1074,9 @@ class QuestBaseResponse extends TemplateResponse implements ICache } } - if (!empty($this->subject->rewards[$this->typeId][Type::CURRENCY])) + if ($currency = array_filter($this->subject->rewards[$this->typeId][Type::CURRENCY] ?? [], + fn($x) => $x != CURRENCY_ARENA_POINTS && $x != CURRENCY_HONOR_POINTS, ARRAY_FILTER_USE_KEY)) { - $currency = $this->subject->rewards[$this->typeId][Type::CURRENCY]; $rewCurr = new CurrencyList(array(['id', array_keys($currency)])); if (!$rewCurr->error) { @@ -1050,7 +1087,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache $id, $rewCurr->getField('name', true), quality: ITEM_QUALITY_NORMAL, - num: $currency[$id] * ($side == SIDE_HORDE ? -1 : 1), // toggles the icon + num: $currency[$id] ); } } @@ -1132,7 +1169,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache return false; $delay = $this->subject->getField('rewardMailDelay'); - $letter = DB::Aowow()->selectRow('SELECT * FROM ?_mails WHERE `id` = ?d', $rmtId); + $letter = DB::Aowow()->selectRow('SELECT * FROM ::mails WHERE `id` = %i', $rmtId); $this->mail = array( 'attachments' => [], @@ -1141,12 +1178,12 @@ class QuestBaseResponse extends TemplateResponse implements ICache 'header' => array( $rmtId, null, - $delay ? Lang::mail('mailIn', [Util::formatTime($delay * 1000)]) : null, + $delay ? Lang::mail('mailIn', [DateTime::formatTimeElapsed($delay * 1000)]) : null, ) ); $senderTypeId = 0; - if ($_= DB::World()->selectCell('SELECT `RewardMailSenderEntry` FROM quest_mail_sender WHERE `QuestId` = ?d', $this->typeId)) + if ($_= DB::World()->selectCell('SELECT `RewardMailSenderEntry` FROM quest_mail_sender WHERE `QuestId` = %i', $this->typeId)) $senderTypeId = $_; else foreach ($startEnd as $se) @@ -1157,8 +1194,8 @@ class QuestBaseResponse extends TemplateResponse implements ICache $this->mail['header'][1] = Lang::mail('mailBy', [$senderTypeId, $ti]); // while mail attachemnts are handled as loot, it has no variance. Always 100% chance, always one item. - $mailLoot = new Loot(); - if ($mailLoot->getByContainer(LOOT_MAIL, $rmtId)) + $mailLoot = new LootByContainer(); + if ($mailLoot->getByContainer(Loot::MAIL, [$rmtId])) { $this->extendGlobalData($mailLoot->jsGlobals); foreach ($mailLoot->getResult() as $loot) @@ -1168,13 +1205,22 @@ class QuestBaseResponse extends TemplateResponse implements ICache return true; } - private function createGains() : ?array + private function createGains(int $side) : ?array { $gains = []; // xp $gains[0] = $this->subject->getField('rewardXP'); + // arena points + $gains[5] = $this->subject->getField('rewardArenaPoints'); + + // honor points + if ($_ = $this->subject->getField('rewardHonorPoints')) + $gains[4] = [$_, $side]; + else + $gains[4] = null; + // talent points $gains[3] = $this->subject->getField('rewardTalents'); @@ -1199,7 +1245,7 @@ class QuestBaseResponse extends TemplateResponse implements ICache 'name' => FactionList::getName($fac) ); - if ($cuRates = DB::World()->selectRow('SELECT * FROM reputation_reward_rate WHERE `faction` = ?d', $fac)) + if ($cuRates = DB::World()->selectRow('SELECT * FROM reputation_reward_rate WHERE `faction` = %i', $fac)) { if ($this->subject->isRepeatable()) $rep['qty'][1] = $rep['qty'][0] * ($cuRates['quest_repeatable_rate'] - 1); @@ -1247,14 +1293,15 @@ class QuestBaseResponse extends TemplateResponse implements ICache // so we fast forward to the last quest and go backwards from there. $lastQuestId = $this->subject->getField('nextQuestIdChain'); - while ($newLast = DB::Aowow()->selectCell('SELECT `nextQuestIdChain` FROM ?_quests WHERE `id` = ?d AND `id` <> `nextQuestIdChain`', $lastQuestId)) + while ($newLast = DB::Aowow()->selectCell('SELECT `nextQuestIdChain` FROM ::quests WHERE `id` = %i AND `id` <> `nextQuestIdChain`', $lastQuestId)) $lastQuestId = $newLast; - $end = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `reqRaceMask` FROM ?_quests WHERE `id` = ?d', $lastQuestId ?: $this->typeId); + $end = DB::Aowow()->selectRow('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `reqRaceMask` FROM ::quests WHERE `id` = %i', $lastQuestId ?: $this->typeId); $chain = array(array($makeSeriesItem($end))); // series / step / quest $prevStepIds = [$lastQuestId ?: $this->typeId]; - while ($prevQuests = DB::Aowow()->select('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `reqRaceMask` FROM ?_quests WHERE `nextQuestIdChain` IN (?a) AND `id` <> `nextQuestIdChain`', $prevStepIds)) + while ($prevQuests = DB::Aowow()->selectAssoc('SELECT `id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `reqRaceMask` FROM ::quests WHERE `nextQuestIdChain` IN %in AND `id` <> `nextQuestIdChain` AND (`cuFlags` & %i) = 0', + $prevStepIds, User::isInGroup(U_GROUP_STAFF) ? CUSTOM_EXCLUDE_FOR_LISTVIEW : 0)) { $step = []; foreach ($prevQuests as $pQuest) @@ -1283,13 +1330,13 @@ class QuestBaseResponse extends TemplateResponse implements ICache $extraLists = array( // Requires all of these quests (Quests that you must follow to get this quest) - ['reqQ', array('OR', ['AND', ['nextQuestId', $this->typeId], ['exclusiveGroup', 0, '<']], ['AND', ['id', $this->subject->getField('prevQuestId')], ['nextQuestIdChain', $this->typeId, '!']])], + ['reqQ', array(DB::OR, [DB::AND, ['nextQuestId', $this->typeId], ['exclusiveGroup', 0, '<']], [DB::AND, ['id', $this->subject->getField('prevQuestId')], ['nextQuestIdChain', $this->typeId, '!']])], // Requires one of these quests (Requires one of the quests to choose from) - ['reqOneQ', array('OR', ['AND', ['exclusiveGroup', 0, '>'], ['nextQuestId', $this->typeId]], ['breadCrumbForQuestId', $this->typeId])], + ['reqOneQ', array(DB::OR, [DB::AND, ['exclusiveGroup', 0, '>='], ['nextQuestId', $this->typeId]], ['breadCrumbForQuestId', $this->typeId])], // Opens Quests (Quests that become available only after complete this quest (optionally only one)) - ['opensQ', array('OR', ['AND', ['prevQuestId', $this->typeId], ['id', $this->subject->getField('nextQuestIdChain'), '!']], ['id', $this->subject->getField('nextQuestId')], ['id', $this->subject->getField('breadcrumbForQuestId')])], + ['opensQ', array(DB::OR, [DB::AND, ['prevQuestId', $this->typeId], ['id', $this->subject->getField('nextQuestIdChain'), '!']], ['id', $this->subject->getField('nextQuestId')], ['id', $this->subject->getField('breadcrumbForQuestId')])], // Closes Quests (Quests that become inaccessible after completing this quest) ['closesQ', array(['exclusiveGroup', 0, '>'], ['exclusiveGroup', $this->subject->getField('exclusiveGroup')], ['id', $this->typeId, '!'])], diff --git a/endpoints/quests/quests.php b/endpoints/quests/quests.php index fb4d944f..86dc7e9b 100644 --- a/endpoints/quests/quests.php +++ b/endpoints/quests/quests.php @@ -32,7 +32,7 @@ class QuestsBaseResponse extends TemplateResponse implements ICache ); protected int $type = Type::QUEST; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'quests'; protected string $pageName = 'quests'; @@ -45,14 +45,29 @@ class QuestsBaseResponse extends TemplateResponse implements ICache ); protected array $validCats = Game::QUEST_CLASSES; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); + + if ($this->category) + { + // basically: everywhere except for page structure quests require cat AND cat2 to be set + // causing links to be generated for /?quests=-2.0, which only exist as /?quests=-2 + if ($this->category[0] == -2 && isset($this->category[1])) + $this->forward('?' . $this->pageName . '=-2'); + + $this->subCat = '='.implode('.', $this->category); + } - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new QuestListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -60,21 +75,17 @@ class QuestsBaseResponse extends TemplateResponse implements ICache { $this->h1 = Util::ucFirst(Lang::game('quests')); - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - if (isset($this->category[1])) - $conditions[] = ['zoneOrSort', $this->category[1]]; + $conditions[] = ['questSortId', $this->category[1]]; else if (isset($this->category[0])) - $conditions[] = ['zoneOrSort', $this->validCats[$this->category[0]]]; + $conditions[] = ['questSortId', $this->validCats[$this->category[0]]]; /*************/ @@ -123,9 +134,9 @@ class QuestsBaseResponse extends TemplateResponse implements ICache $tabData['extraCols'] = '$fi_getExtraCols(fi_extraCols, 0, 0)'; // create note if search limit was exceeded - if ($quests->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($quests->getMatches() > Listview::DEFAULT_SIZE) { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_questsfound', $quests->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_questsfound', $quests->getMatches(), Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } else if (isset($this->category[1]) && $this->category[1] > 0) diff --git a/endpoints/race/race.php b/endpoints/race/race.php index 1bb261c6..e331f1b0 100644 --- a/endpoints/race/race.php +++ b/endpoints/race/race.php @@ -16,7 +16,7 @@ class RaceBaseResponse extends TemplateResponse implements ICache [7952, 33554], null, [16264, 33557], [17584, 33657] ); - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'race'; @@ -99,6 +99,33 @@ class RaceBaseResponse extends TemplateResponse implements ICache $infobox[] = Lang::race('startZone').Lang::main('colon').'[zone='.$_.']'; } + // id + $infobox[] = Lang::race('id') . $this->typeId; + + // icon + $mIcon = $this->subject->getField('iconId0'); + $fIcon = $this->subject->getField('iconId1'); + if ($mIcon || $fIcon) + { + $buff = ''; + if ($mIcon) + { + $buff .= '[icondb='.$mIcon.(!$fIcon ? ' name=true': '').']'; + $this->extendGlobalIds(Type::ICON, $mIcon); + } + if ($fIcon) + { + $buff .= '[icondb='.$fIcon.' name=true]'; + $this->extendGlobalIds(Type::ICON, $fIcon); + } + + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').$buff; + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); @@ -108,12 +135,16 @@ class RaceBaseResponse extends TemplateResponse implements ICache /****************/ $this->expansion = Util::$expansionString[$this->subject->getField('expansion')]; - $this->headIcons = ['race_'.$ra->json().'_male', 'race_'.$ra->json().'_female']; $this->redButtons = array( BUTTON_WOWHEAD => true, BUTTON_LINKS => ['type' => $this->type, 'typeId' => $this->typeId] ); + if ($_ = $this->subject->getField('iconStringMale')) + $this->headIcons[] = $_; + if ($_ = $this->subject->getField('iconStringFemale')) + $this->headIcons[] = $_; + /**************/ /* Extra Tabs */ @@ -121,7 +152,7 @@ class RaceBaseResponse extends TemplateResponse implements ICache $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); - // Classes + // tab: classes $classes = new CharClassList(array(['racemask', $ra->toMask(), '&'])); if (!$classes->error) { @@ -129,7 +160,7 @@ class RaceBaseResponse extends TemplateResponse implements ICache $this->lvTabs->addListviewTab(new Listview(['data' => $classes->getListviewData()], CharClassList::$brickFile)); } - // Tongues + // tab: languages $conditions = array( ['typeCat', -11], // proficiencies ['reqRaceMask', $ra->toMask(), '&'] // only languages are race-restricted @@ -147,7 +178,7 @@ class RaceBaseResponse extends TemplateResponse implements ICache ), SpellList::$brickFile)); } - // Racials + // tab: racial-traits $conditions = array( ['typeCat', -4], // racial traits ['reqRaceMask', $ra->toMask(), '&'] @@ -169,7 +200,7 @@ class RaceBaseResponse extends TemplateResponse implements ICache $this->lvTabs->addListviewTab(new Listview($tabData, SpellList::$brickFile)); } - // Quests + // tab: quests $conditions = array( ['reqRaceMask', $ra->toMask(), '&'], [['reqRaceMask', ChrRace::MASK_HORDE, '&'], ChrRace::MASK_HORDE, '!'], @@ -183,11 +214,11 @@ class RaceBaseResponse extends TemplateResponse implements ICache $this->lvTabs->addListviewTab(new Listview(['data' => $quests->getListviewData()], QuestList::$brickFile)); } - // Mounts + // tab: mounts // ok, this sucks, but i rather hardcode the trainer, than fetch items by namepart if (isset(self::MOUNT_VENDORS[$this->typeId])) { - if ($items = DB::World()->selectCol('SELECT `item` FROM npc_vendor WHERE `entry` IN (?a)', self::MOUNT_VENDORS[$this->typeId])) + if ($items = DB::World()->selectCol('SELECT `item` FROM npc_vendor WHERE `entry` IN %in', self::MOUNT_VENDORS[$this->typeId])) { $conditions = array( ['i.id', $items], @@ -209,8 +240,8 @@ class RaceBaseResponse extends TemplateResponse implements ICache } } - // Sounds - if ($vo = DB::Aowow()->selectCol('SELECT `soundId` AS ARRAY_KEY, `gender` FROM ?_races_sounds WHERE `raceId` = ?d', $this->typeId)) + // tab: sounds + if ($vo = DB::Aowow()->selectCol('SELECT `soundId` AS ARRAY_KEY, `gender` FROM ::races_sounds WHERE `raceId` = %i', $this->typeId)) { $sounds = new SoundList(array(['id', array_keys($vo)])); if (!$sounds->error) @@ -227,6 +258,28 @@ class RaceBaseResponse extends TemplateResponse implements ICache } } + // tab: criteria-of + $conditions = array( + DB::AND, + ['ac.type', ACHIEVEMENT_CRITERIA_TYPE_HK_RACE], + ['ac.value1', $this->typeId] + ); + + if ($extraCrt = DB::World()->selectCol('SELECT `criteria_id` FROM achievement_criteria_data WHERE `type` IN %in AND `value2` = %i', [ACHIEVEMENT_CRITERIA_DATA_TYPE_S_PLAYER_CLASS_RACE, ACHIEVEMENT_CRITERIA_DATA_TYPE_T_PLAYER_CLASS_RACE], $this->typeId)) + $conditions = [DB::OR, $conditions, ['ac.id', $extraCrt]]; + + $crtOf = new AchievementList($conditions); + if (!$crtOf->error) + { + $this->extendGlobalData($crtOf->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $crtOf->getListviewData(), + 'name' => '$LANG.tab_criteriaof', + 'id' => 'criteria-of' + ), AchievementList::$brickFile)); + } + // tab: condition-for $cnd = new Conditions(); $cnd->getByCondition(Type::CHR_RACE, $this->typeId)->prepare(); diff --git a/endpoints/races/races.php b/endpoints/races/races.php index e5c67324..43d370b6 100644 --- a/endpoints/races/races.php +++ b/endpoints/races/races.php @@ -11,18 +11,18 @@ class RacesBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::CHR_RACE; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'races'; protected ?int $activeTab = parent::TAB_DATABASE; protected array $breadcrumb = [0, 13]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -35,7 +35,7 @@ class RacesBaseResponse extends TemplateResponse implements ICache $this->redButtons[BUTTON_WOWHEAD] = true; - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; diff --git a/endpoints/reputation/reputation.php b/endpoints/reputation/reputation.php index 55d05faa..0e160637 100644 --- a/endpoints/reputation/reputation.php +++ b/endpoints/reputation/reputation.php @@ -15,11 +15,11 @@ class ReputationBaseResponse extends TemplateResponse protected ?int $activeTab = parent::TAB_COMMUNITY; protected array $breadcrumb = [3, 10]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - if ($pageParam) + if ($rawParam) $this->generateError(); } @@ -29,7 +29,7 @@ class ReputationBaseResponse extends TemplateResponse array_unshift($this->title, $this->h1); - if ($repData = DB::Aowow()->select('SELECT `action`, `amount`, `date` AS "when", IF(`action` IN (?a), `sourceA`, 0) AS "param" FROM ?_account_reputation WHERE `userId` = ?d', + if ($repData = DB::Aowow()->selectAssoc('SELECT `action`, `amount`, `date` AS "when", IF(`action` IN %in, `sourceA`, 0) AS "param" FROM ::account_reputation WHERE `userId` = %i', [SITEREP_ACTION_COMMENT, SITEREP_ACTION_UPVOTED, SITEREP_ACTION_DOWNVOTED], User::$id)) { array_walk($repData, fn(&$x) => $x['when'] = date(Util::$dateFormatInternal, $x['when'])); diff --git a/endpoints/screenshot/add.php b/endpoints/screenshot/add.php index f6452771..294a4285 100644 --- a/endpoints/screenshot/add.php +++ b/endpoints/screenshot/add.php @@ -25,9 +25,9 @@ class ScreenshotAddResponse extends TextResponse private int $destType = 0; private int $destTypeId = 0; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); // get screenshot destination // target delivered as screenshot=<command>&<type>.<typeId>.<hash:16> (hash is optional) diff --git a/endpoints/screenshot/complete.php b/endpoints/screenshot/complete.php index 0ea503bc..5bf80622 100644 --- a/endpoints/screenshot/complete.php +++ b/endpoints/screenshot/complete.php @@ -30,9 +30,9 @@ class ScreenshotCompleteResponse extends TextResponse private int $destTypeId = 0; private string $imgHash = ''; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); // get screenshot destination // target delivered as screenshot=<command>&<type>.<typeId>.<hash:16> (hash is optional) @@ -77,8 +77,8 @@ class ScreenshotCompleteResponse extends TextResponse ['oWidth' => $w, 'oHeight' => $h] = ScreenshotMgr::calcImgDimensions(); // write to db - $newId = DB::Aowow()->query( - 'INSERT INTO ?_screenshots (`type`, `typeId`, `userIdOwner`, `date`, `width`, `height`, `caption`, `status`) VALUES (?d, ?d, ?d, UNIX_TIMESTAMP(), ?d, ?d, ?, 0)', + $newId = DB::Aowow()->qry( + 'INSERT INTO ::screenshots (`type`, `typeId`, `userIdOwner`, `date`, `width`, `height`, `caption`, `status`) VALUES (%i, %i, %i, UNIX_TIMESTAMP(), %i, %i, %s, 0)', $this->destType, $this->destTypeId, User::$id, $w, $h, diff --git a/endpoints/screenshot/crop.php b/endpoints/screenshot/crop.php index cabae726..9f446899 100644 --- a/endpoints/screenshot/crop.php +++ b/endpoints/screenshot/crop.php @@ -32,9 +32,9 @@ class ScreenshotCropResponse extends TemplateResponse public int $destTypeId = 0; public string $imgHash = ''; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); // get screenshot destination // target delivered as screenshot=<command>&<type>.<typeId>.<hash:16> (hash is optional) diff --git a/endpoints/screenshot/thankyou.php b/endpoints/screenshot/thankyou.php index 53b62bfc..d2517671 100644 --- a/endpoints/screenshot/thankyou.php +++ b/endpoints/screenshot/thankyou.php @@ -25,9 +25,9 @@ class ScreenshotThankyouResponse extends TemplateResponse private int $destType = 0; private int $destTypeId = 0; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); // get screenshot destination // target delivered as screenshot=<command>&<type>.<typeId>.<hash:16> (hash is optional) diff --git a/endpoints/search/search.php b/endpoints/search/search.php index edf8cb73..11bce6f4 100644 --- a/endpoints/search/search.php +++ b/endpoints/search/search.php @@ -29,18 +29,15 @@ class SearchBaseResponse extends TemplateResponse implements ICache public string $invalidTerms = ''; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); // just to set g_user and g_locale + parent::__construct($rawParam); // just to set g_user and g_locale - $this->query = $this->_get['search']; // technically pageParam, but prepared - - if ($limit = Cfg::get('SQL_LIMIT_SEARCH')) - $this->maxResults = $limit; + $this->query = $this->_get['search']; // technically rawParam, but prepared $this->searchMask = Search::TYPE_REGULAR | self::SEARCH_MODS_ALL; - $this->searchObj = new Search($this->query, $this->searchMask, $this->maxResults); + $this->searchObj = new Search($this->query, $this->searchMask); } protected function generate() : void @@ -62,19 +59,23 @@ class SearchBaseResponse extends TemplateResponse implements ICache $canRedirect = true; $redirectTo = ''; - foreach ($this->searchObj->perform() as $lvData) + + if ($this->searchObj->canPerform()) { - if ($lvData[1] == 'npc' || $lvData[1] == 'object') - $this->addDataLoader('zones'); + foreach ($this->searchObj->perform() as $lvData) + { + if ($lvData[1] == 'npc' || $lvData[1] == 'object') + $this->addDataLoader('zones'); - $this->lvTabs->addListviewTab(new Listview(...$lvData)); + $this->lvTabs->addListviewTab(new Listview(...$lvData)); - // we already have a target > can't have more targets > no redirects - if ($canRedirect && $redirectTo) - $canRedirect = false; + // we already have a target > can't have more targets > no redirects + if (($canRedirect && $redirectTo) || count($lvData[0]['data']) > 1) + $canRedirect = false; - if ($canRedirect) // note - we are very lucky that in case of searches $template is identical to the typeString - $redirectTo = '?'.$lvData[1].'='.key($lvData[0]['data']); + if ($canRedirect) // note - we are very lucky that in case of searches $template is identical to the typeString + $redirectTo = '?'.$lvData[1].'='.key($lvData[0]['data']); + } } $this->extendGlobalData($this->searchObj->getJSGlobals()); diff --git a/endpoints/search/search_json.php b/endpoints/search/search_json.php index 61c6f754..fa8d4b5c 100644 --- a/endpoints/search/search_json.php +++ b/endpoints/search/search_json.php @@ -33,11 +33,11 @@ class SearchJsonResponse extends TextResponse implements ICache private array $extraOpts = []; // for weighted search private array $extraCnd = []; // for weighted search - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - $this->query = $this->_get['search']; // technically pageParam, but prepared + $this->query = $this->_get['search']; // technically rawParam, but prepared if ($this->_get['wt'] && $this->_get['wtv']) // slots and type should get ignored { @@ -52,16 +52,13 @@ class SearchJsonResponse extends TextResponse implements ICache if ($_ = array_filter($this->_get['slots'] ?? [])) $this->extraCnd[] = ['slot', $_]; - if ($limit = Cfg::get('SQL_LIMIT_SEARCH')) - $this->maxResults = $limit; - $this->searchMask = Search::TYPE_JSON; if ($this->_get['slots'] || $this->_get['type'] == Type::ITEM) $this->searchMask |= 1 << Search::MOD_ITEM; else if ($this->_get['type'] == Type::ITEMSET) $this->searchMask |= 1 << Search::MOD_ITEM | 1 << Search::MOD_ITEMSET; - $this->searchObj = new Search($this->query, $this->searchMask, $this->maxResults, $this->extraCnd, $this->extraOpts); + $this->searchObj = new Search($this->query, $this->searchMask, $this->extraCnd, $this->extraOpts); } // !note! dear reader, if you ever try to generate a string, that is to be evaled by JS, NEVER EVER terminate with a \n ..... $totalHoursWasted +=2; diff --git a/endpoints/search/search_open.php b/endpoints/search/search_open.php index ad154697..699fc3fa 100644 --- a/endpoints/search/search_open.php +++ b/endpoints/search/search_open.php @@ -53,6 +53,8 @@ class SearchOpenResponse extends TextResponse implements ICache 1 << Search::MOD_ZONE | 1 << Search::MOD_OBJECT | 1 << Search::MOD_FACTION | 1 << Search::MOD_SKILL | 1 << Search::MOD_PET; + private int $maxResults = Search::SUGGESTIONS_MAX_RESULTS; + protected string $contentType = MIME_TYPE_OPENSEARCH; protected int $cacheType = CACHE_TYPE_SEARCH; @@ -60,18 +62,15 @@ class SearchOpenResponse extends TextResponse implements ICache 'search' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkTextLine']] ); - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); // just to set g_user and g_locale + parent::__construct($rawParam); // just to set g_user and g_locale - $this->query = $this->_get['search']; // technically pageParam, but prepared - - if ($limit = Cfg::get('SQL_LIMIT_QUICKSEARCH')) - $this->maxResults = $limit; + $this->query = $this->_get['search']; // technically rawParam, but prepared $this->searchMask = Search::TYPE_OPEN | self::SEARCH_MODS_OPEN; - $this->searchObj = new Search($this->query, $this->searchMask, $this->maxResults); + $this->searchObj = new Search($this->query, $this->searchMask, maxResults: $this->maxResults); } protected function generate() : void diff --git a/endpoints/searchbox/searchbox.php b/endpoints/searchbox/searchbox.php index eda8f931..22c289d9 100644 --- a/endpoints/searchbox/searchbox.php +++ b/endpoints/searchbox/searchbox.php @@ -13,11 +13,11 @@ class SearchboxBaseResponse extends TemplateResponse protected ?int $activeTab = parent::TAB_MORE; protected array $breadcrumb = [2, 16]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - if ($pageParam) + if ($rawParam) $this->generateError(); } diff --git a/endpoints/searchplugins/searchplugins.php b/endpoints/searchplugins/searchplugins.php index 43925cce..6dd00b1b 100644 --- a/endpoints/searchplugins/searchplugins.php +++ b/endpoints/searchplugins/searchplugins.php @@ -13,11 +13,11 @@ class SearchpluginsBaseResponse extends TemplateResponse protected ?int $activeTab = parent::TAB_MORE; protected array $breadcrumb = [2, 8]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - if ($pageParam) + if ($rawParam) $this->generateError(); } diff --git a/endpoints/signature/delete.php b/endpoints/signature/delete.php new file mode 100644 index 00000000..2b9a9506 --- /dev/null +++ b/endpoints/signature/delete.php @@ -0,0 +1,30 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + + +class SignatureDeleteResponse extends TextResponse +{ + protected bool $requiresLogin = true; + protected array $expectedGET = array( + 'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkIdListUnsigned']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + } + + protected function generate() : void + { + if (!$this->assertGET('id')) + $this->generate404(); + + // DB::Aowow()->qry(DELETE FROM ::account_signatures WHERE %if', !User::isInGroup(U_GROUP_MODERATOR), '`accountId` = %i AND', User::$id, '%end `id` IN %in', $this->_get['id]); + } +} + +?> diff --git a/endpoints/signature/generate.php b/endpoints/signature/generate.php new file mode 100644 index 00000000..a04ef5ac --- /dev/null +++ b/endpoints/signature/generate.php @@ -0,0 +1,60 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + +// introductory blog post +// https://web.archive.org/web/20210419162936/https://www.wowhead.com/news/new-feature-preview-forum-signatures-175630 +// looks like it was .. at best .. live for half a year + +// only example seen. looks like archive.org had a hickup when parsing the markup js +// https://web.archive.org/web/20110924014309/http://www.wowhead.com/signature=generate&id='+b.unnamed+'.png + +// no clue where generated images are stored. +// static/uploads/signatures/ indicates users can upload their own backgrounds +// unclear when updated. With every char sync? + +// generating and also viewing +class SignatureGenerateResponse extends TextResponse +{ + protected string $contentType = MIME_TYPE_PNG; + protected array $expectedGET = array( + 'id' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkProfileId']] + ); + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + } + + protected function generate() : void + { + parent::generate(); + + if (!$this->assertGET('id')) + $this->generate404(); + + // find file in storage + + // build image + } + + public function generate404(?string $out = null) : never + { + // "Signature Unavailable" + $out = /*data:image/png;base64,*/'iVBORw0KGgoAAAANSUhEUgAAAdQAAAA8CAIAAABQJdxgAAALbElEQVR4nO3bXWxT5R8H8OectnvtDtB2fVnXN6Cr29zW0SCSjMgSh0YSifPtjgsu1EuvjXFojPfKDRLitYQYxEggEwzhQiJb1+GG4MrWjrbr1petW6FubXee/8UTT05aKBv85az4/VyY9uyc5y32y9PfOeXMZjMBAIBni1d6AAAA/0UIXwAABSB8AQAUgPAFAFAAwhcAQAEIXwAABSB8AQAUgPAFAFAAwhcAQAEIXwAABSB8AQAUgPAFAFAAwhcAQAEIXwAABSB8AQAUgPAFAFAAwhcAQAEIXwAABSB8AQAUgPAFAFAAwhcAQAEIXwAABaiVHgDA8+bzzz+XXouiKH+72TM33hRUHYRv1di7d++BAwcEQZCOcBzHXgwNDR07dsxkMhFCFhYWvvvuO2WG+K8pD52hoaHy4+zgs1e++D6fjxBCKR0bG6t87WPP3HhTUF0QvtVhYGCgr6/P5XJt376dZS6ldH19XaPR+P1+QojJZNq3bx8h5Pfff/+3B/Ppp5+q1WpCiCiKX3zxxb/dHcMyiGFTLjkuP/iMlS/+7Oysw+GQ/nWsIBQKuVyuCmduvCmoLqj5VoeXX37Z4/HodLpCoXDz5s2JiYlMJsMSUMJx3LP5iKrVap/P5/P5eP7Z/f8TCoXYi2AwKD+eSqUIIfPz889sJA8lX/zTp0+zUT3WqVOnFhcXK5yw8aag6mDnWx1UKlVjYyMhZGpqamlpaXx8vK+vz263s2+77Nv3+Pg4IaS2tpb999ChQ93d3RqNpkIiU0ql1ysrK2fPno1EIoSQvr6+ffv2NTU1SX9NpVLnzp2LxWLyb/ocx1WuQrIypfwcSimllOM4Sumff/45MjJy+PBhg8HABlksFkdHR4eHh0VRlLfz7bfffvjhhy6Xi43zxIkT7PhPP/301ltvGQyGeDx+5syZko42NbUPPvigwkQIIb/88kuFC+WLzzoq8ah+5eccP36cvchms8PDwxMTEw9tyul0bmTRYItD+FYZt9vN8/yBAwey2Ww0Go1Go+x4SWXw/fff93g8bre7vr6eyKrDhBC/3y+dHAwGd+7cqVKpKKWRSGRwcPDrr78mhOzfv7+np6epqYntbUVRvHfv3ttvv/3NN98Q2Td99kJqkPzz3b+8TCkdmZ6edrlcPM/n8/l8Pt/e3m42m61WK+sol8tpNJpCoXDlyhX5rOfm5uRvpc3gH3/88eabb7IR/vXXX/KONjs1UlbZsFqtZrNZevvYCyuXZStczlgsFrPZzP5ZSqVSgiAUCoU7d+6UtGO3248ePbqRRYMtDmWH6rC2tra0tEQIqaur6+jo8Hq9XV1dPM9ns1m2DZydnSWykHW5XB0dHQ0NDYlEYnx8nH23LRQKLBzZV3iO4/R6/cTERCKR4HneZrPt2LFD6lEQhJs3b/r9/mg0qlKpHA6HTqdjf5Kqq36/X96ghFUG5Ikv9dja2nr37l1KqVqt1mg0ZrPZZrMVCoVAIJBOpxsbG9va2l566aUNLkuhUJBer6+vP+XUQqEQ2y+zIkYsFkun05RSaXaPurBk8R+lwpISQkRRHBsbm5qaopQajUar1frqq6+WN/L6668/5aLBFoHwrQ6XL18OhUKRSCSfzxNC1Gq1wWDwer1Wq3VwcLC8MshxHKsIz83NpdNplg5qtZpSKq8zhsNhlgVEVrXkeZ7juNHRUVEUa2trGxoa5H8teaJgaGjo5MmT8sLliRMnVlZW5OfIe7x161Y6nb5x48bIyAjP8y0tLRzHsdSLxWKEEK1Wy768P4GnmdrJkyeTyWQulyOEbN++XRTFtbU1jUaTy+VSqdTp06cfdeFGyrKVl5RZWFg4f/78vXv3WKmhublZr9eXN2WxWP6/iwZKQdmhOoyMjKRSqf7+fpvNVlNTY7Va9Xo9x3Eul2t5ebm8MkgpLRaLGo3G4XAQQux2OyFkfX1d+rhKzp8/39vbS2QbtzfeeINtpurr61Uq1YMHD0jFbV08Hpe/LU+ikh6/+uor9uL48eMqlYoQ0t7evsF1YFQqFdvnajSaCh1tamrxeDwQCAiC4PF4amtrWblGq9Xevn07EAj09PQ86sKHlmVLbHBJA4FAJpM5duyY0+lkeV3eFMdxT7ZosNUgfKuDdCvp3LlzGo1mYGDAaDTu3r2bfQ7LhcPhuro6t9ut0+l0Oh2ldHV1NRgMsi2wnPzGFLNnz56Ojo76+vr79+/fuXOH4zh5MfSxnuARiEAgIL9ZVD4k6TjHcTzP2+12Vgpoa2tjVd1HnV9ypPLUrl275vP58vl8bW2tw+G4f/9+oVDIZrNXr179+OOPn2ZNNr6klW+Qym1w0WDLQtmhavh8vtbW1iNHjgiCkEgkWP2B2bZtm/xMs9l85syZtbU1tVodDAb9fv/Y2Njk5OTq6qooijabTX4ye4RAYjAYeJ6vq6sjhNy9ezeTycjDVKvVloyK7ayJ7MP/0UcflTRYMjx5+ZXFhyAI8Xjc7/enUqlkMnnr1q3y6VNK2cl6vf7dd9/1eDzd3d1HjhwxmUyiKLLeSzra7NSWl5dnZmbYzT2tVms0GqPR6Ozs7MrKSoULLRaLvBez2VwyDKPRWOFyuffee+/w4cOs2sC29uVNbWrRYCtTlX+cYAvq7+9vaWlpbGzUarU6nW7btm0ej0elUi0uLs7Nzb3yyiuNjY0Gg4EQ8vfff3d1dV2+fPngwYOsOmGxWFihsLm5uaampqurSzo5l8u1t7c3NDQ0NzcTQh48eOD1egkhrKpICBFF0ePxqNVqjuOWlpYcDsfo6Gh/f7/FYuE4rlgsut1uu92+Y8cOdkSlUjU3NzudTlZxZg3u379f6jGbze7Zs+fq1auEkM7OzsbGxqamJkEQisWixWLp7e3duXPn7du3y6PkxRdfrKurEwRBEASVSrVr167Ozk6bzWY2m1lE3rhx45NPPnnKqaVSqY6ODpPJxPO8KIrT09M//PBDNps9ePDgoy70er0liy+fby6X6+7urtBvsVhsaWmhlOp0Oo7jDAaD0+kkhMzNzc3MzAwMDJQ0lUwmN75osJVh51tlBEHo6urq6elRq9WJRCIcDrMHjF544QV2wq5duwghR48eZeFI/rmxw3FcTU2N2+2Wn7x7924iqx6yv+ZyuWg0Sik1mUzd3d2ZTGZ1dZVSKl21urrKHgOw2Wws0TKZDLvEaDRaLJZYLMa2oqxBeY8ej0eay/DwcDwen5+f53m+s7Ozp6eH5/nJycmHTvznn3+en5+fmZkpFAqtra29vb1er9dgMLD7kBcuXCjp6Mmmxp5wYGXrpaWlTCbDNsKVLyxZ/PJhVL6cUppOp/P5fG9vb1tbGyEkHo8vLCxcunSpvKlNLRpsZaj5Vg32UJe8tLe8vHzlyhX2rGvJj2udTicL6Onp6UwmQwjR6XTsAVtS9kvckrdnz54dHBxcWFhgb/P5fCwWk3ZthJCLFy/yPB8Oh6Xx/Pjjj++8804ikZAaKXni9aG//Q0Gg99///1rr70m3SgTRXFycvLixYvlJ4fD4VOnTg0MDCSTSemnfZTSaDR66dIlqYWnnBoh5Ndff9Xr9ewxiWvXrj32Qo7jyme3qX6vX7/OKtfSvFZWVi5cuMDq2iVNbWrRYCvjpMfI4Xny2Wef7d27l+O4ZDIZiUQ4jrPZbAaDYXFxcWpq6ssvv1R6gAD/ddj5Pp8KhUIkEmlpaTEYDKxiSCnNZDKhUGh4eFjp0QEAdr7PKbvdzn4KJb+xns1mf/vtt+vXrys4MABgEL4AAArA0w4AAApA+AIAKADhCwCgAIQvAIACEL4AAApA+AIAKADhCwCgAIQvAIACEL4AAApA+AIAKADhCwCgAIQvAIACEL4AAApA+AIAKADhCwCgAIQvAIACEL4AAApA+AIAKADhCwCggP8BQFB72fG1SEAAAAAASUVORK5CYII='; + parent::generate404(base64_decode($out)); + } + + protected static function checkProfileId(string $sigId) : ?int + { + if (preg_match('/^(\d+)\.png$/i', $sigId, $m)) + return $m[0]; + + return null; + } +} + +?> diff --git a/endpoints/signature/signature.php b/endpoints/signature/signature.php new file mode 100644 index 00000000..e0a5b76b --- /dev/null +++ b/endpoints/signature/signature.php @@ -0,0 +1,55 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + + +class SignatureBaseResponse extends TemplateResponse +{ + protected bool $requiresLogin = true; + protected string $template = 'text-page-generic'; + protected string $pageName = 'signature'; + + protected array $expectedGET = array( + 'profile' => ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkProfile']] // optional - full profile string to build sig from + ); + + private int $id = 0; + + public function __construct(string $rawParam) + { + parent::__construct($rawParam); + + if ($rawParam) + $this->id = intVal($rawParam); + else if ($this->assertGET('profile')) + $this->id = $this->_get['profile']; + else + $this->generateError(); + } + + protected function generate() : void + { + // show editor + + parent::generate(); + } + + protected static function checkProfile(string $profile) : ?int + { + if (!preg_match('/^([a-z]+)\.([a-z_]+)\.(.+)$/i', $profile, $m)) + return null; + + [, $region, $realm, $char] = $m; + + $realms = Profiler::getRealms(); + if ($rId = array_find_key($realms, fn($x) => $x['region'] == $region && $x['name'] == $realm)) + return DB::Aowow()->selectCell('SELECT `id` FROM ::profiler_profiles WHERE `realm` = %i AND `custom` = 0 AND `name` = %s', $rId, urldecode($char)) ?: null; + + return null; + } +} + +?> diff --git a/endpoints/sitemap/sitemap.php b/endpoints/sitemap/sitemap.php new file mode 100644 index 00000000..09e5ca53 --- /dev/null +++ b/endpoints/sitemap/sitemap.php @@ -0,0 +1,52 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + + +class SitemapBaseResponse extends TextResponse implements ICache +{ + use TrCache; + + protected string $contentType = MIME_TYPE_XML; + protected int $cacheType = CACHE_TYPE_XML; + + protected array $expectedGET = array( + 'page' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_value' => 1]] + ); + + private string $page; + + public function __construct(string $pageParam) + { + $this->page = $pageParam; + + parent::__construct($pageParam); + } + + protected function generate() : void + { + if ($xml = Sitemap::generate($this->page, $this->_get['page'] ?? 1)) + $this->result = $xml; + else if (Sitemap::$maxPage) + (new TemplateResponse($this->page))->generateNotFound(Sitemap::ERR_TITLE, sprintf(Sitemap::ERR_OFFSET, Sitemap::$maxPage)); + else + (new TemplateResponse($this->page))->generateNotFound(Sitemap::ERR_TITLE, Sitemap::ERR_PAGE); + } + + public function getCacheKeyComponents() : array + { + $misc = $this->page . serialize($this->_get['page'] ?? 1); + + return array( + -1, // DBType + -1, // DBTypeId/category + -1, // staff mask (content does not diff) + md5($misc) // misc + ); + } +} + +?> diff --git a/endpoints/skill/skill.php b/endpoints/skill/skill.php index 2277c66b..a95221bf 100644 --- a/endpoints/skill/skill.php +++ b/endpoints/skill/skill.php @@ -10,7 +10,7 @@ class SkillBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'skill'; @@ -70,6 +70,9 @@ class SkillBaseResponse extends TemplateResponse implements ICache $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + // id + $infobox[] = Lang::skill('id') . $this->typeId; + // icon if ($_ = $this->subject->getField('iconId')) { @@ -77,6 +80,10 @@ class SkillBaseResponse extends TemplateResponse implements ICache $this->extendGlobalIds(Type::ICON, $_); } + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); @@ -107,9 +114,8 @@ class SkillBaseResponse extends TemplateResponse implements ICache { // tab: recipes [spells] (crafted) $condition = array( - ['OR', ['s.reagent1', 0, '>'], ['s.reagent2', 0, '>'], ['s.reagent3', 0, '>'], ['s.reagent4', 0, '>'], ['s.reagent5', 0, '>'], ['s.reagent6', 0, '>'], ['s.reagent7', 0, '>'], ['s.reagent8', 0, '>']], - ['OR', ['s.skillLine1', $this->typeId], ['AND', ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->typeId]]], - Cfg::get('SQL_LIMIT_NONE') + [DB::OR, ['s.reagent1', 0, '>'], ['s.reagent2', 0, '>'], ['s.reagent3', 0, '>'], ['s.reagent4', 0, '>'], ['s.reagent5', 0, '>'], ['s.reagent6', 0, '>'], ['s.reagent7', 0, '>'], ['s.reagent8', 0, '>']], + [DB::OR, ['s.skillLine1', $this->typeId], [DB::AND, ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->typeId]]] ); $recipes = new SpellList($condition); // also relevant for 3 @@ -129,8 +135,7 @@ class SkillBaseResponse extends TemplateResponse implements ICache $filterRecipe = [null, SKILL_LEATHERWORKING, SKILL_TAILORING, SKILL_ENGINEERING, SKILL_BLACKSMITHING, SKILL_COOKING, SKILL_ALCHEMY, SKILL_FIRST_AID, SKILL_ENCHANTING, SKILL_FISHING, SKILL_JEWELCRAFTING, SKILL_INSCRIPTION, SKILL_MINING, SKILL_HERBALISM]; $conditions = array( ['requiredSkill', $this->typeId], - ['class', ITEM_CLASS_RECIPE], - Cfg::get('SQL_LIMIT_NONE') + ['class', ITEM_CLASS_RECIPE] ); $recipeItems = new ItemList($conditions); @@ -159,7 +164,7 @@ class SkillBaseResponse extends TemplateResponse implements ICache if ($created) { - $created = new ItemList(array(['i.id', $created], Cfg::get('SQL_LIMIT_NONE'))); + $created = new ItemList(array(['i.id', $created])); if (!$created->error) { $this->extendGlobalData($created->getJSGlobals(GLOBALINFO_SELF)); @@ -180,8 +185,7 @@ class SkillBaseResponse extends TemplateResponse implements ICache // tab: required by [item] $conditions = array( ['requiredSkill', $this->typeId], - ['class', ITEM_CLASS_RECIPE, '!'], - Cfg::get('SQL_LIMIT_NONE') + ['class', ITEM_CLASS_RECIPE, '!'] ); $reqBy = new ItemList($conditions); @@ -202,12 +206,7 @@ class SkillBaseResponse extends TemplateResponse implements ICache } // tab: required by [itemset] - $conditions = array( - ['skillId', $this->typeId], - Cfg::get('SQL_LIMIT_NONE') - ); - - $reqBy = new ItemsetList($conditions); + $reqBy = new ItemsetList(array(['skillId', $this->typeId])); if (!$reqBy->error) { $this->extendGlobalData($reqBy->getJSGlobals(GLOBALINFO_SELF)); @@ -220,20 +219,39 @@ class SkillBaseResponse extends TemplateResponse implements ICache } } + // tab: modified by [spell] + $conditions = array( + DB::OR, + [DB::AND, ['effect1AuraId', [SPELL_AURA_MOD_SKILL, SPELL_AURA_MOD_SKILL_TALENT]], ['effect1MiscValue', $this->typeId]], + [DB::AND, ['effect2AuraId', [SPELL_AURA_MOD_SKILL, SPELL_AURA_MOD_SKILL_TALENT]], ['effect2MiscValue', $this->typeId]], + [DB::AND, ['effect3AuraId', [SPELL_AURA_MOD_SKILL, SPELL_AURA_MOD_SKILL_TALENT]], ['effect3MiscValue', $this->typeId]] + ); + $modBy = new SpellList($conditions); + if (!$modBy->error) + { + $this->extendGlobalData($modBy->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $modBy->getListviewData(), + 'id' => 'modified-by', + 'name' => '$LANG.tab_modifiedby', + 'hiddenCols' => ['skill'], + ), SpellList::$brickFile)); + } + // tab: spells [spells] (exclude first tab) $reqClass = 0x0; $reqRace = 0x0; $condition = array( - ['AND', ['s.reagent1', 0], ['s.reagent2', 0], ['s.reagent3', 0], ['s.reagent4', 0], ['s.reagent5', 0], ['s.reagent6', 0], ['s.reagent7', 0], ['s.reagent8', 0]], - ['OR', ['s.skillLine1', $this->typeId], ['AND', ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->typeId]]], - Cfg::get('SQL_LIMIT_NONE') + [DB::AND, ['s.reagent1', 0], ['s.reagent2', 0], ['s.reagent3', 0], ['s.reagent4', 0], ['s.reagent5', 0], ['s.reagent6', 0], ['s.reagent7', 0], ['s.reagent8', 0]], + [DB::OR, ['s.skillLine1', $this->typeId], [DB::AND, ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->typeId]]] ); foreach (Game::$skillLineMask as $line1 => $sets) foreach ($sets as $idx => [, $skillLineId]) if ($skillLineId == $this->typeId) { - $condition[1][] = array('AND', ['s.skillLine1', $line1], ['s.skillLine2OrMask', 1 << $idx, '&']); + $condition[1][] = array(DB::AND, ['s.skillLine1', $line1], ['s.skillLine2OrMask', 1 << $idx, '&']); break 2; } @@ -274,16 +292,15 @@ class SkillBaseResponse extends TemplateResponse implements ICache $mask |= 1 << $idx; $spellIds = DB::Aowow()->selectCol( - 'SELECT `id` FROM ?_spell WHERE (`skillLine1` = ?d OR (`skillLine1` > 0 AND `skillLine2OrMask` = ?d) {OR (`skillLine1` = -3 AND `skillLine2OrMask` = ?d)})', + 'SELECT `id` FROM ::spell WHERE %if', $mask, '(`skillLine1` = -3 AND `skillLine2OrMask` = %i) OR', $mask, '%end (`skillLine1` = %i OR (`skillLine1` > 0 AND `skillLine2OrMask` = %i))', $this->typeId, - $this->typeId, - $mask ?: DBSIMPLE_SKIP + $this->typeId ); - $list = $spellIds ? DB::World()->selectCol('SELECT cdt.`CreatureId` FROM creature_default_trainer cdt JOIN trainer_spell ts ON ts.`TrainerId` = cdt.`TrainerId` WHERE ts.`SpellID` IN (?a)', $spellIds) : []; + $list = $spellIds ? DB::World()->selectCol('SELECT cdt.`CreatureId` FROM creature_default_trainer cdt JOIN trainer_spell ts ON ts.`TrainerId` = cdt.`TrainerId` WHERE ts.`SpellID` IN %in', $spellIds) : []; if ($list) { - $trainer = new CreatureList(array(Cfg::get('SQL_LIMIT_NONE'), ['ct.id', $list], ['s.guid', NULL, '!'], ['ct.npcflag', 0x10, '&'])); + $trainer = new CreatureList(array(['ct.id', $list], ['s.guid', NULL, '!'], ['ct.npcflag', 0x10, '&'])); if (!$trainer->error) { @@ -319,7 +336,7 @@ class SkillBaseResponse extends TemplateResponse implements ICache if ($sort) { - $quests = new QuestList(array(['zoneOrSort', -$sort], Cfg::get('SQL_LIMIT_NONE'))); + $quests = new QuestList(array(['questSortId', -$sort])); if (!$quests->error) { $this->extendGlobalData($quests->getJSGlobals()); diff --git a/endpoints/skills/skills.php b/endpoints/skills/skills.php index f6275491..1d1f1a36 100644 --- a/endpoints/skills/skills.php +++ b/endpoints/skills/skills.php @@ -11,7 +11,7 @@ class SkillsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::SKILL; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'skills'; @@ -20,11 +20,11 @@ class SkillsBaseResponse extends TemplateResponse implements ICache protected array $validCats = [-6, -5, -4, 6, 7, 8, 9, 10, 11]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -43,7 +43,7 @@ class SkillsBaseResponse extends TemplateResponse implements ICache $this->redButtons[BUTTON_WOWHEAD] = true; - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; diff --git a/endpoints/sound/sound.php b/endpoints/sound/sound.php index 6207cf2a..8db7cf8f 100644 --- a/endpoints/sound/sound.php +++ b/endpoints/sound/sound.php @@ -10,7 +10,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'sound'; protected string $pageName = 'sound'; @@ -80,7 +80,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache } // get full path in-game for sound (workaround for missing PlaySoundKit()) - $fullpath = DB::Aowow()->selectCell('SELECT IF(sf.`path` <> "", CONCAT(sf.`path`, "\\\\", sf.`file`), sf.`file`) FROM ?_sounds_files sf JOIN ?_sounds s ON s.`soundFile1` = sf.`id` WHERE s.`id` = ?d', $this->typeId); + $fullpath = DB::Aowow()->selectCell('SELECT IF(sf.`path` <> "", CONCAT(sf.`path`, "\\", sf.`file`), sf.`file`) FROM ::sounds_files sf JOIN ::sounds s ON s.`soundFile1` = sf.`id` WHERE s.`id` = %i', $this->typeId); $this->redButtons = array( BUTTON_WOWHEAD => true, @@ -107,25 +107,25 @@ class SoundBaseResponse extends TemplateResponse implements ICache // skipping (always empty): ready, castertargeting, casterstate, targetstate $displayIds = DB::Aowow()->selectCol( 'SELECT `id` - FROM ?_spell_sounds - WHERE `animation` = ?d OR `precast` = ?d OR `cast` = ?d OR `impact` = ?d OR `state` = ?d OR - `statedone` = ?d OR `channel` = ?d OR `casterimpact` = ?d OR `targetimpact` = ?d OR `missiletargeting` = ?d OR - `instantarea` = ?d OR `persistentarea` = ?d OR `missile` = ?d OR `impactarea` = ?d', + FROM ::spell_sounds + WHERE `animation` = %i OR `precast` = %i OR `cast` = %i OR `impact` = %i OR `state` = %i OR + `statedone` = %i OR `channel` = %i OR `casterimpact` = %i OR `targetimpact` = %i OR `missiletargeting` = %i OR + `instantarea` = %i OR `persistentarea` = %i OR `missile` = %i OR `impactarea` = %i', $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId ); $seMiscValues = DB::Aowow()->selectCol( 'SELECT `id` - FROM ?_screeneffect_sounds - WHERE `ambienceDay` = ?d OR `ambienceNight` = ?d OR `musicDay` = ?d OR `musicNight` = ?d', + FROM ::screeneffect_sounds + WHERE `ambienceDay` = %i OR `ambienceNight` = %i OR `musicDay` = %i OR `musicNight` = %i', $this->typeId, $this->typeId, $this->typeId, $this->typeId ); $cnd = array( - 'OR', - ['AND', ['effect1Id', [SPELL_EFFECT_PLAY_MUSIC, SPELL_EFFECT_PLAY_SOUND]], ['effect1MiscValue', $this->typeId]], - ['AND', ['effect2Id', [SPELL_EFFECT_PLAY_MUSIC, SPELL_EFFECT_PLAY_SOUND]], ['effect2MiscValue', $this->typeId]], - ['AND', ['effect3Id', [SPELL_EFFECT_PLAY_MUSIC, SPELL_EFFECT_PLAY_SOUND]], ['effect3MiscValue', $this->typeId]] + DB::OR, + [DB::AND, ['effect1Id', [SPELL_EFFECT_PLAY_MUSIC, SPELL_EFFECT_PLAY_SOUND]], ['effect1MiscValue', $this->typeId]], + [DB::AND, ['effect2Id', [SPELL_EFFECT_PLAY_MUSIC, SPELL_EFFECT_PLAY_SOUND]], ['effect2MiscValue', $this->typeId]], + [DB::AND, ['effect3Id', [SPELL_EFFECT_PLAY_MUSIC, SPELL_EFFECT_PLAY_SOUND]], ['effect3MiscValue', $this->typeId]] ); if ($displayIds) @@ -133,10 +133,10 @@ class SoundBaseResponse extends TemplateResponse implements ICache if ($seMiscValues) $cnd[] = array( - 'OR', - ['AND', ['effect1AuraId', SPELL_AURA_SCREEN_EFFECT], ['effect1MiscValue', $seMiscValues]], - ['AND', ['effect2AuraId', SPELL_AURA_SCREEN_EFFECT], ['effect2MiscValue', $seMiscValues]], - ['AND', ['effect3AuraId', SPELL_AURA_SCREEN_EFFECT], ['effect3MiscValue', $seMiscValues]] + DB::OR, + [DB::AND, ['effect1AuraId', SPELL_AURA_SCREEN_EFFECT], ['effect1MiscValue', $seMiscValues]], + [DB::AND, ['effect2AuraId', SPELL_AURA_SCREEN_EFFECT], ['effect2MiscValue', $seMiscValues]], + [DB::AND, ['effect3AuraId', SPELL_AURA_SCREEN_EFFECT], ['effect3MiscValue', $seMiscValues]] ); $spells = new SpellList($cnd); @@ -148,20 +148,23 @@ class SoundBaseResponse extends TemplateResponse implements ICache // tab: Items $subClasses = []; - if ($subClassMask = DB::Aowow()->selectCell('SELECT `subClassMask` FROM ?_items_sounds WHERE `soundId` = ?d', $this->typeId)) + if ($subClassMask = DB::Aowow()->selectCell('SELECT `subClassMask` FROM ::items_sounds WHERE `soundId` = %i', $this->typeId)) for ($i = 0; $i <= 20; $i++) if ($subClassMask & (1 << $i)) $subClasses[] = $i; - $itemIds = DB::Aowow()->selectCol( - 'SELECT id - FROM ?_items - WHERE `pickUpSoundId` = ?d OR `dropDownSoundId` = ?d OR `sheatheSoundId` = ?d OR `unsheatheSoundId` = ?d - { OR `spellVisualId` IN (?a) } - { OR (IF (`soundOverrideSubclass` > 0, `soundOverrideSubclass`, `subclass`) IN (?a) AND `class` = ?d) }', - $this->typeId, $this->typeId, $this->typeId, $this->typeId, $displayIds ?: DBSIMPLE_SKIP, $subClasses ?: DBSIMPLE_SKIP, ITEM_CLASS_WEAPON + $where = array( + ['`pickUpSoundId` = %i', $this->typeId], + ['`dropDownSoundId` = %i', $this->typeId], + ['`sheatheSoundId` = %i', $this->typeId], + ['`unsheatheSoundId` = %i', $this->typeId] ); - if ($itemIds) + if ($displayIds) + $where[] = ['`spellVisualId` IN %in', $displayIds]; + if ($subClasses) + $where[] = [DB::AND, [['IF (`soundOverrideSubclass` > 0, `soundOverrideSubclass`, `subclass`) IN %in', $subClasses], ['`class` = %i', ITEM_CLASS_WEAPON]]]; + + if ($itemIds = DB::Aowow()->selectCol('SELECT `id` FROM ::items WHERE %or', $where)) { $items = new ItemList(array(['id', $itemIds])); if (!$items->error) @@ -172,7 +175,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache } // tab: Zones - if ($zoneIds = DB::Aowow()->select('SELECT `id`, `worldStateId`, `worldStateValue` FROM ?_zones_sounds WHERE `ambienceDay` = ?d OR `ambienceNight` = ?d OR `musicDay` = ?d OR `musicNight` = ?d OR `intro` = ?d', $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId)) + if ($zoneIds = DB::Aowow()->selectAssoc('SELECT `id`, `worldStateId`, `worldStateValue` FROM ::zones_sounds WHERE `ambienceDay` = %i OR `ambienceNight` = %i OR `musicDay` = %i OR `musicNight` = %i OR `intro` = %i', $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId)) { $zones = new ZoneList(array(['id', array_column($zoneIds, 'id')])); if (!$zones->error) @@ -212,7 +215,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache } } - if ($worldStates = array_filter($zoneIds, function ($x) { return $x['worldStateId'] > 0; })) + if ($worldStates = array_filter($zoneIds, fn($x) => $x['worldStateId'] > 0)) { $tabData['extraCols'] = ['$Listview.extraCols.condition']; @@ -222,7 +225,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache Conditions::extendListviewRow($zoneData[$state['id']], Conditions::SRC_NONE, $this->typeId, [Conditions::WORLD_STATE, $state['worldStateId'], $state['worldStateValue']]); else foreach ($zoneData as &$d) - if (in_array($state['id'], $d['subzones'])) + if (in_array($state['id'], $d['subzones'] ?? [])) Conditions::extendListviewRow($d, Conditions::SRC_NONE, $this->typeId, [Conditions::WORLD_STATE, $state['worldStateId'], $state['worldStateValue']]); } } @@ -235,7 +238,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache } // tab: Races (VocalUISounds (containing error voice overs)) - if ($vo = DB::Aowow()->selectCol('SELECT `raceId` FROM ?_races_sounds WHERE `soundId` = ?d GROUP BY `raceId`', $this->typeId)) + if ($vo = DB::Aowow()->selectCol('SELECT `raceId` FROM ::races_sounds WHERE `soundId` = %i GROUP BY `raceId`', $this->typeId)) { $races = new CharRaceList(array(['id', $vo])); if (!$races->error) @@ -246,7 +249,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache } // tab: Emotes (EmotesTextSound (containing emote audio)) - if ($em = DB::Aowow()->selectCol('SELECT `emoteId` FROM ?_emotes_sounds WHERE `soundId` = ?d GROUP BY `emoteId` UNION SELECT `id` FROM ?_emotes WHERE `soundId` = ?d', $this->typeId, $this->typeId)) + if ($em = DB::Aowow()->selectCol('SELECT `emoteId` FROM ::emotes_sounds WHERE `soundId` = %i GROUP BY `emoteId` UNION SELECT `id` FROM ::emotes WHERE `soundId` = %i', $this->typeId, $this->typeId)) { $races = new EmoteList(array(['id', $em])); if (!$races->error) @@ -259,7 +262,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache } } - $creatureIds = DB::World()->selectCol('SELECT ct.`CreatureID` FROM creature_text ct LEFT JOIN broadcast_text bct ON bct.`ID` = ct.`BroadCastTextId` WHERE bct.`SoundEntriesID` = ?d OR ct.`Sound` = ?d', $this->typeId, $this->typeId); + $creatureIds = DB::World()->selectCol('SELECT ct.`CreatureID` FROM creature_text ct LEFT JOIN broadcast_text bct ON bct.`ID` = ct.`BroadCastTextId` WHERE bct.`SoundEntriesID` = %i OR ct.`Sound` = %i', $this->typeId, $this->typeId); // can objects or areatrigger play sound...? if ($goosp = SmartAI::getOwnerOfSoundPlayed($this->typeId, Type::NPC)) @@ -269,12 +272,12 @@ class SoundBaseResponse extends TemplateResponse implements ICache // skipping (always empty): transforms, footsteps $displayIds = DB::Aowow()->selectCol( 'SELECT `id` - FROM ?_creature_sounds - WHERE `greeting` = ?d OR `farewell` = ?d OR `angry` = ?d OR `exertion` = ?d OR `exertioncritical` = ?d OR - `injury` = ?d OR `injurycritical` = ?d OR `death` = ?d OR `stun` = ?d OR `stand` = ?d OR - `aggro` = ?d OR `wingflap` = ?d OR `wingglide` = ?d OR `alert` = ?d OR `fidget` = ?d OR - `customattack` = ?d OR `loop` = ?d OR `jumpstart` = ?d OR `jumpend` = ?d OR `petattack` = ?d OR - `petorder` = ?d OR `petdismiss` = ?d OR `birth` = ?d OR `spellcast` = ?d OR `submerge` = ?d OR `submerged` = ?d', + FROM ::creature_sounds + WHERE `greeting` = %i OR `farewell` = %i OR `angry` = %i OR `exertion` = %i OR `exertioncritical` = %i OR + `injury` = %i OR `injurycritical` = %i OR `death` = %i OR `stun` = %i OR `stand` = %i OR + `aggro` = %i OR `wingflap` = %i OR `wingglide` = %i OR `alert` = %i OR `fidget` = %i OR + `customattack` = %i OR `loop` = %i OR `jumpstart` = %i OR `jumpend` = %i OR `petattack` = %i OR + `petorder` = %i OR `petdismiss` = %i OR `birth` = %i OR `spellcast` = %i OR `submerge` = %i OR `submerged` = %i', $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId, @@ -286,7 +289,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache if ($creatureIds || $displayIds) { $extra = []; - $cnds = [Cfg::get('SQL_LIMIT_NONE'), &$extra]; + $cnds = [&$extra]; if (!User::isInGroup(U_GROUP_STAFF)) $cnds[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; @@ -297,7 +300,7 @@ class SoundBaseResponse extends TemplateResponse implements ICache $extra[] = ['displayId1', $displayIds]; if (count($extra) > 1) - array_unshift($extra, 'OR'); + array_unshift($extra, DB::OR); else $extra = array_pop($extra); diff --git a/endpoints/sounds/sounds.php b/endpoints/sounds/sounds.php index 3ed6eaa6..eef7a1ea 100644 --- a/endpoints/sounds/sounds.php +++ b/endpoints/sounds/sounds.php @@ -11,7 +11,7 @@ class SoundsBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::SOUND; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'sounds'; protected string $pageName = 'sounds'; @@ -24,16 +24,24 @@ class SoundsBaseResponse extends TemplateResponse implements ICache ); protected array $validCats = [1, 2, 3, 4, 6, 9, 10, 12, 13, 14, 16, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 50, 52, 53]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); if ($this->category) $this->forward('?sounds&filter=ty='.$this->category[0]); - parent::__construct($pageParam); + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new SoundListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -41,18 +49,13 @@ class SoundsBaseResponse extends TemplateResponse implements ICache { $this->h1 = Util::ucFirst(Lang::game('sounds')); - $this->filter->evalCriteria(); - - $conditions = []; - + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /**************/ /* Page Title */ @@ -91,9 +94,9 @@ class SoundsBaseResponse extends TemplateResponse implements ICache $tabData['data'] = $sounds->getListviewData(); // create note if search limit was exceeded; overwriting 'note' is intentional - if ($sounds->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($sounds->getMatches() > Listview::DEFAULT_SIZE) { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_soundsfound', $sounds->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_soundsfound', $sounds->getMatches(), Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } } diff --git a/endpoints/spell/spell.php b/endpoints/spell/spell.php index 8f1e6645..3819ddc6 100644 --- a/endpoints/spell/spell.php +++ b/endpoints/spell/spell.php @@ -14,9 +14,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache SPELL_AURA_ABILITY_PERIODIC_CRIT, SPELL_AURA_MOD_TARGET_ABILITY_ABSORB_SCHOOL, SPELL_AURA_ABILITY_IGNORE_AURASTATE, SPELL_AURA_ALLOW_ONLY_ABILITY, SPELL_AURA_IGNORE_MELEE_RESET, SPELL_AURA_ABILITY_CONSUME_NO_AMMO, SPELL_AURA_MOD_IGNORE_SHAPESHIFT, SPELL_AURA_PERIODIC_HASTE, SPELL_AURA_OVERRIDE_CLASS_SCRIPTS, - SPELL_AURA_MOD_DAMAGE_FROM_CASTER, SPELL_AURA_ADD_TARGET_TRIGGER, /* SPELL_AURA_DUMMY ? */]; + SPELL_AURA_MOD_DAMAGE_FROM_CASTER, SPELL_AURA_ADD_TARGET_TRIGGER, SPELL_AURA_IGNORE_COMBAT_RESULT, /* SPELL_AURA_DUMMY ? */]; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'spell'; protected string $pageName = 'spell'; @@ -26,7 +26,6 @@ class SpellBaseResponse extends TemplateResponse implements ICache public int $type = Type::SPELL; public int $typeId = 0; public array $reagents = [false, null]; - public array $scaling = []; public string $items = ''; public array $tools = []; public array $effects = []; @@ -50,6 +49,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache private int $firstRank = 0; private array $modelInfo = []; private array $difficulties = []; + private int $mapType = 0; public function __construct(string $id) { @@ -68,28 +68,33 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($jsg = $this->subject->getJSGlobals(GLOBALINFO_ANY, $extra)) $this->extendGlobalData($jsg, $extra); - $this->modelInfo = $this->subject->getModelInfo($this->typeId); - $this->difficulties = DB::Aowow()->selectRow( // has difficulty versions of itself + $this->modelInfo = $this->subject->getModelInfo($this->typeId); + if ($spelldifficulty = DB::Aowow()->selectAssoc( // has difficulty versions of itself 'SELECT `normal10` AS "0", `normal25` AS "1", - `heroic10` AS "2", `heroic25` AS "3" - FROM ?_spelldifficulty - WHERE `normal10` = ?d OR `normal25` = ?d OR - `heroic10` = ?d OR `heroic25` = ?d', + `heroic10` AS "2", `heroic25` AS "3", + `mapType` AS ARRAY_KEY + FROM ::spelldifficulty + WHERE `normal10` = %i OR `normal25` = %i OR + `heroic10` = %i OR `heroic25` = %i', $this->typeId, $this->typeId, $this->typeId, $this->typeId - ); + )) + { + $this->mapType = key($spelldifficulty); + $this->difficulties = array_pop($spelldifficulty); + } // returns self or firstRank - if ($fr = DB::World()->selectCell('SELECT `first_spell_id` FROM spell_ranks WHERE `spell_id` = ?d', $this->typeId)) + if ($fr = DB::World()->selectCell('SELECT `first_spell_id` FROM spell_ranks WHERE `spell_id` = %i', $this->typeId)) $this->firstRank = $fr; else $this->firstRank = DB::Aowow()->selectCell( 'SELECT IF(s1.`RankNo` <> 1 AND s2.`id`, s2.`id`, s1.`id`) - FROM ?_spell s1 - LEFT JOIN ?_spell s2 + FROM ::spell s1 + LEFT JOIN ::spell s2 ON s1.`SpellFamilyId` = s2.`SpelLFamilyId` AND s1.`SpellFamilyFlags1` = s2.`SpelLFamilyFlags1` AND s1.`SpellFamilyFlags2` = s2.`SpellFamilyFlags2` AND s1.`SpellFamilyFlags3` = s2.`SpellFamilyFlags3` AND s1.`name_loc0` = s2.`name_loc0` AND s2.`RankNo` = 1 - WHERE s1.`id` = ?d', + WHERE s1.`id` = %i', $this->typeId ); @@ -161,13 +166,6 @@ class SpellBaseResponse extends TemplateResponse implements ICache $this->createReagentList(); - /**********************/ - /* Spell Scaling Info */ - /**********************/ - - $this->createScalingData(); - - /******************/ /* Required Items */ /******************/ @@ -211,7 +209,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache /*************************/ // factionchange-equivalent - if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = ?d, `alliance_id`, -`horde_id`) FROM player_factionchange_spells WHERE `alliance_id` = ?d OR `horde_id` = ?d', $this->typeId, $this->typeId, $this->typeId)) + if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = %i, `alliance_id`, -`horde_id`) FROM player_factionchange_spells WHERE `alliance_id` = %i OR `horde_id` = %i', $this->typeId, $this->typeId, $this->typeId)) { $altSpell = new SpellList(array(['id', abs($pendant)])); if (!$altSpell->error) @@ -236,7 +234,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $this->castTime = $this->subject->createCastTimeForCurrent(false, false); $this->level = $this->subject->getField('spellLevel'); $this->rangeName = $this->subject->getField('rangeText', true); - $this->gcd = Util::formatTime($this->subject->getField('startRecoveryTime')); + $this->gcd = DateTime::formatTimeElapsedFloat($this->subject->getField('startRecoveryTime')); $this->school = $this->fmtStaffTip(Lang::getMagicSchools($this->subject->getField('schoolMask')), Util::asHex($this->subject->getField('schoolMask'))); $this->dispel = $this->subject->getField('dispelType') ? Lang::game('dt', $this->subject->getField('dispelType')) : null; $this->mechanic = $this->subject->getField('mechanic') ? Lang::game('me', $this->subject->getField('mechanic')) : null; @@ -266,12 +264,12 @@ class SpellBaseResponse extends TemplateResponse implements ICache $this->stances = Lang::getStances($this->subject->getField('stanceMask')); if (($_ = $this->subject->getField('recoveryTime')) && $_ > 0) - $this->cooldown = Util::formatTime($_); + $this->cooldown = DateTime::formatTimeElapsedFloat($_); else if (($_ = $this->subject->getField('recoveryCategory')) && $_ > 0) - $this->cooldown = Util::formatTime($_); + $this->cooldown = DateTime::formatTimeElapsedFloat($_); if (($_ = $this->subject->getField('duration')) && $_ > 0) - $this->duration = Util::formatTime($_); + $this->duration = DateTime::formatTimeElapsedFloat($_); /**************/ @@ -286,7 +284,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $formSpells = []; for ($i = 1; $i < 4; $i++) if ($this->subject->getField('effect'.$i.'AuraId') == SPELL_AURA_MOD_SHAPESHIFT) - if ($_ = DB::Aowow()->selectRow('SELECT `spellId1`, `spellId2`, `spellId3`, `spellId4`, `spellId5`, `spellId6`, `spellId7`, `spellId8` FROM ?_shapeshiftforms WHERE `id` = ?d', $this->subject->getField('effect'.$i.'MiscValue'))) + if ($_ = DB::Aowow()->selectRow('SELECT `spellId1`, `spellId2`, `spellId3`, `spellId4`, `spellId5`, `spellId6`, `spellId7`, `spellId8` FROM ::shapeshiftforms WHERE `id` = %i', $this->subject->getField('effect'.$i.'MiscValue'))) $formSpells = array_merge($formSpells, $_); if ($formSpells) @@ -335,7 +333,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $classSpells = $miscSpells = []; $this->effects[$i]['modifies'] = [&$classSpells, &$miscSpells]; - $sub = ['OR', ['s.spellFamilyFlags1', $m1, '&'], ['s.spellFamilyFlags2', $m2, '&'], ['s.spellFamilyFlags3', $m3, '&']]; + $sub = [DB::OR, ['s.spellFamilyFlags1', $m1, '&'], ['s.spellFamilyFlags2', $m2, '&'], ['s.spellFamilyFlags3', $m3, '&']]; $modSpells = new SpellList($conditions); if (!$modSpells->error) @@ -350,7 +348,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($classSpells) { - foreach (DB::World()->select('SELECT `spell_id` AS ARRAY_KEY, `first_spell_id` AS "0", `rank` AS "1" FROM spell_ranks WHERE `spell_id` IN (?a)', array_keys($classSpells)) as $spellId => [$firstSpellId, $rank]) + foreach (DB::World()->selectAssoc('SELECT `spell_id` AS ARRAY_KEY, `first_spell_id` AS "0", `rank` AS "1" FROM spell_ranks WHERE `spell_id` IN %in', array_keys($classSpells)) as $spellId => [$firstSpellId, $rank]) { $classSpells[$firstSpellId][1][0] = min($classSpells[$firstSpellId][1][0] ?? $rank, $rank); $classSpells[$firstSpellId][1][1] = max($classSpells[$firstSpellId][1][1] ?? $rank, $rank); @@ -394,7 +392,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // tab: [$this is] modified by - $sub = ['OR']; + $sub = [DB::OR]; $conditions = [ ['s.typeCat', [-9], '!'], // GM (-9); also include uncategorized (0), NPC-Spell (-8)?; NPC includes totems, lightwell and others :/ ['s.spellFamilyId', $this->subject->getField('spellFamilyId')], @@ -411,10 +409,10 @@ class SpellBaseResponse extends TemplateResponse implements ICache continue; $sub[] = array( - 'AND', + DB::AND, ['s.effect'.$i.'AuraId', self::MOD_AURAS], [ - 'OR', + DB::OR, ['s.effect'.$i.'SpellClassMaskA', $m1, '&'], ['s.effect'.$i.'SpellClassMaskB', $m2, '&'], ['s.effect'.$i.'SpellClassMaskC', $m3, '&'] @@ -449,43 +447,32 @@ class SpellBaseResponse extends TemplateResponse implements ICache ['s.effect1Id', $this->subject->getField('effect1Id')], ['s.effect2Id', $this->subject->getField('effect2Id')], ['s.effect3Id', $this->subject->getField('effect3Id')], - ['s.id', $this->subject->id, '!'], + ['s.id', $this->typeId, '!'], ['s.name_loc'.Lang::getLocale()->value, $this->subject->getField('name', true)] ); + if ($this->difficulties) + $conditions = [DB::OR, [DB::AND, ...$conditions], [DB::AND, ['s.id', $this->difficulties], ['s.id', $this->typeId, '!']]]; + $saSpells = new SpellList($conditions); if (!$saSpells->error) { $data = $saSpells->getListviewData(); - if ($this->difficulties) // needs a way to distinguish between dungeon and raid :x; creature using this -> map -> areaType? + if ($this->difficulties) { $saE = ['$Listview.extraCols.mode']; foreach ($data as $id => &$d) { - $d['modes'] = ['mode' => 0]; - - if ($this->difficulties[0] == $id) // b0001000 + if (($modeBit = array_search($id, $this->difficulties)) !== false) { - if (!$this->difficulties[2] && !$this->difficulties[3]) - $d['modes']['mode'] |= 0x2; + if ($this->mapType) + $d['modes'] = ['mode' => 1 << ($modeBit + 3)]; else - $d['modes']['mode'] |= 0x8; + $d['modes'] = ['mode' => 2 - $modeBit]; } - - if ($this->difficulties[1] == $id) // b0010000 - { - if (!$this->difficulties[2] && !$this->difficulties[3]) - $d['modes']['mode'] |= 0x1; - else - $d['modes']['mode'] |= 0x10; - } - - if ($this->difficulties[2] == $id) // b0100000 - $d['modes']['mode'] |= 0x20; - - if ($this->difficulties[3] == $id) // b1000000 - $d['modes']['mode'] |= 0x40; + else + $d['modes'] = ['mode' => 0]; } } @@ -533,14 +520,37 @@ class SpellBaseResponse extends TemplateResponse implements ICache } } - // tab: used by - spell - if ($so = DB::Aowow()->selectCell('SELECT `id` FROM ?_spelloverride WHERE `spellId1` = ?d OR `spellId2` = ?d OR `spellId3` = ?d OR `spellId4` = ?d OR `spellId5` = ?d', $this->subject->id, $this->subject->id, $this->subject->id, $this->subject->id, $this->subject->id)) + // tab: glyphs + if ($gpIds = DB::Aowow()->selectCol('SELECT `id` FROM ::glyphproperties WHERE `spellId` = %i', $this->typeId)) { $conditions = array( - 'OR', - ['AND', ['effect1AuraId', SPELL_AURA_OVERRIDE_SPELLS], ['effect1MiscValue', $so]], - ['AND', ['effect2AuraId', SPELL_AURA_OVERRIDE_SPELLS], ['effect2MiscValue', $so]], - ['AND', ['effect3AuraId', SPELL_AURA_OVERRIDE_SPELLS], ['effect3MiscValue', $so]] + DB::OR, + [DB::AND, ['effect1Id', SPELL_EFFECT_APPLY_GLYPH], ['effect1MiscValue', $gpIds]], + [DB::AND, ['effect2Id', SPELL_EFFECT_APPLY_GLYPH], ['effect2MiscValue', $gpIds]], + [DB::AND, ['effect3Id', SPELL_EFFECT_APPLY_GLYPH], ['effect3MiscValue', $gpIds]] + ); + $glyphSpells = new SpellList($conditions); + if (!$glyphSpells->error) + { + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $glyphSpells->getListviewData(), + 'visibleCols' => ['singleclass', 'glyphtype'], + 'id' => 'glyphs', + 'name' => '$LANG.tab_glyphs' + ), SpellList::$brickFile)); + + $this->extendGlobalData($glyphSpells->getJSGlobals(GLOBALINFO_SELF)); + } + } + + // tab: used by - spell + if ($so = DB::Aowow()->selectCell('SELECT `id` FROM ::spelloverride WHERE `spellId1` = %i OR `spellId2` = %i OR `spellId3` = %i OR `spellId4` = %i OR `spellId5` = %i', $this->typeId, $this->typeId, $this->typeId, $this->typeId, $this->typeId)) + { + $conditions = array( + DB::OR, + [DB::AND, ['effect1AuraId', SPELL_AURA_OVERRIDE_SPELLS], ['effect1MiscValue', $so]], + [DB::AND, ['effect2AuraId', SPELL_AURA_OVERRIDE_SPELLS], ['effect2MiscValue', $so]], + [DB::AND, ['effect3AuraId', SPELL_AURA_OVERRIDE_SPELLS], ['effect3MiscValue', $so]] ); $ubSpells = new SpellList($conditions); if (!$ubSpells->error) @@ -555,12 +565,11 @@ class SpellBaseResponse extends TemplateResponse implements ICache } } - // tab: used by - itemset $conditions = array( - 'OR', - ['spell1', $this->subject->id], ['spell2', $this->subject->id], ['spell3', $this->subject->id], ['spell4', $this->subject->id], - ['spell5', $this->subject->id], ['spell6', $this->subject->id], ['spell7', $this->subject->id], ['spell8', $this->subject->id] + DB::OR, + ['spell1', $this->typeId], ['spell2', $this->typeId], ['spell3', $this->typeId], ['spell4', $this->typeId], + ['spell5', $this->typeId], ['spell6', $this->typeId], ['spell7', $this->typeId], ['spell8', $this->typeId] ); $ubSets = new ItemsetList($conditions); @@ -577,12 +586,12 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: used by - item $conditions = array( - 'OR', - ['AND', ['spellTrigger1', SPELL_TRIGGER_LEARN, '!'], ['spellId1', $this->subject->id]], - ['AND', ['spellTrigger2', SPELL_TRIGGER_LEARN, '!'], ['spellId2', $this->subject->id]], - ['AND', ['spellTrigger3', SPELL_TRIGGER_LEARN, '!'], ['spellId3', $this->subject->id]], - ['AND', ['spellTrigger4', SPELL_TRIGGER_LEARN, '!'], ['spellId4', $this->subject->id]], - ['AND', ['spellTrigger5', SPELL_TRIGGER_LEARN, '!'], ['spellId5', $this->subject->id]] + DB::OR, + [DB::AND, ['spellTrigger1', SPELL_TRIGGER_LEARN, '!'], ['spellId1', $this->typeId]], + [DB::AND, ['spellTrigger2', SPELL_TRIGGER_LEARN, '!'], ['spellId2', $this->typeId]], + [DB::AND, ['spellTrigger3', SPELL_TRIGGER_LEARN, '!'], ['spellId3', $this->typeId]], + [DB::AND, ['spellTrigger4', SPELL_TRIGGER_LEARN, '!'], ['spellId4', $this->typeId]], + [DB::AND, ['spellTrigger5', SPELL_TRIGGER_LEARN, '!'], ['spellId5', $this->typeId]] ); $ubItems = new ItemList($conditions); @@ -599,9 +608,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: used by - object $conditions = array( - 'OR', - ['onUseSpell', $this->subject->id], ['onSuccessSpell', $this->subject->id], - ['auraSpell', $this->subject->id], ['triggeredSpell', $this->subject->id] + DB::OR, + ['onUseSpell', $this->typeId], ['onSuccessSpell', $this->typeId], + ['auraSpell', $this->typeId], ['triggeredSpell', $this->typeId] ); if (!empty($ubSAI[Type::OBJECT])) $conditions[] = ['id', $ubSAI[Type::OBJECT]]; @@ -638,11 +647,16 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: criteria of $conditions = array( + DB::AND, ['ac.type', [ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET, ACHIEVEMENT_CRITERIA_TYPE_BE_SPELL_TARGET2, ACHIEVEMENT_CRITERIA_TYPE_CAST_SPELL, ACHIEVEMENT_CRITERIA_TYPE_CAST_SPELL2, ACHIEVEMENT_CRITERIA_TYPE_LEARN_SPELL] ], ['ac.value1', $this->typeId] ); + + if ($extraCrt = DB::World()->selectCol('SELECT `criteria_id` FROM achievement_criteria_data WHERE `type` IN %in AND `value1` = %i', [ACHIEVEMENT_CRITERIA_DATA_TYPE_S_AURA, ACHIEVEMENT_CRITERIA_DATA_TYPE_T_AURA], $this->typeId)) + $conditions = [DB::OR, $conditions, ['ac.id', $extraCrt]]; + $coAchievemnts = new AchievementList($conditions); if (!$coAchievemnts->error) { @@ -656,56 +670,56 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // tab: contains - // spell_loot_template & skill_extra_item_template - $extraItem = DB::World()->selectRow('SELECT * FROM skill_extra_item_template WHERE `spellid` = ?d', $this->subject->id); - $spellLoot = new Loot(); - - if ($spellLoot->getByContainer(LOOT_SPELL, $this->subject->id) || $extraItem) + // spell_loot_template + $spellLoot = new LootByContainer(); + if ($spellLoot->getByContainer(Loot::SPELL, [$this->typeId])) { $this->extendGlobalData($spellLoot->jsGlobals); - $lv = $spellLoot->getResult(); $extraCols = $spellLoot->extraCols; $extraCols[] = '$Listview.extraCols.percent'; - $lvName = '$LANG.tab_contains'; - - if ($extraItem && $this->subject->canCreateItem()) - { - $foo = $this->subject->relItems->getListviewData(); - - for ($i = 1; $i < 4; $i++) - { - if (($bar = $this->subject->getField('effect'.$i.'CreateItemId')) && isset($foo[$bar])) - { - $lvName = '$LANG.tab_bonusloot'; - $lv[$bar] = $foo[$bar]; - $lv[$bar]['percent'] = $extraItem['additionalCreateChance']; - $lv[$bar]['pctstack'] = $this->buildPctStack($extraItem['additionalCreateChance'] / 100, $extraItem['additionalMaxNum']); - if ($max = ($extraItem['additionalMaxNum'] - 1)) - $lv[$bar]['stack'] = [1, $max]; - - if (Conditions::extendListviewRow($lv[$bar], Conditions::SRC_NONE, $this->typeId, [Conditions::SPELL, $extraItem['requiredSpecialization']])) - { - $this->extendGlobalIds(Type::SPELL, $extraItem['requiredSpecialization']); - $extraCols[] = '$Listview.extraCols.condition'; - } - - break; // skill_extra_item_template can only contain 1 item - } - } - } $this->lvTabs->addListviewTab(new Listview(array( - 'data' => $lv, - 'name' => $lvName, - 'id' => 'contains', - 'hiddenCols' => ['side', 'slot', 'source', 'reqlevel'], - 'extraCols' => array_unique($extraCols) + 'data' => $spellLoot->getResult(), + 'name' => '$LANG.tab_contains', + 'id' => 'contains', + 'hiddenCols' => ['side', 'slot', 'source', 'reqlevel'], + 'extraCols' => array_unique($extraCols), + 'computeDataFunc' => '$Listview.funcBox.initLootTable' ), ItemList::$brickFile)); } + // tab: bonus loot + if ($extraItemData = DB::World()->selectAssoc('SELECT `spellId` AS ARRAY_KEY, `additionalCreateChance` AS "0", `additionalMaxNum` AS "1" FROM skill_extra_item_template WHERE `requiredSpecialization` = %i', $this->typeId)) + { + $extraSpells = new SpellList(array(['id', array_keys($extraItemData)])); + if (!$extraSpells->error) + { + $this->extendGlobalData($extraSpells->getJSGlobals(GLOBALINFO_RELATED)); + $lvItems = $extraSpells->getListviewData(); + + foreach ($lvItems as $iId => $data) + { + [$chance, $maxItr] = $extraItemData[$iId]; + + $lvItems[$iId]['count'] = 1; // expected by js or the pct-col becomes unsortable + $lvItems[$iId]['percent'] = $chance; + $lvItems[$iId]['pctstack'] = $this->buildPctStack($chance / 100, $maxItr, $data['creates'][1]); + $lvItems[$iId]['creates'][2] *= $maxItr; + } + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lvItems, + 'name' => '$LANG.tab_bonusloot', + 'id' => 'bonusloot', + 'hiddenCols' => ['side', 'reqlevel'], + 'extraCols' => ['$Listview.extraCols.percent'] + ), SpellList::$brickFile)); + } + } + // tab: exclusive with - if ($this->firstRank && DB::World()->selectCell('SELECT 1 FROM spell_group WHERE `spell_id` = ?d', $this->firstRank)) + if ($this->firstRank && DB::World()->selectCell('SELECT 1 FROM spell_group WHERE `spell_id` = %i', $this->firstRank)) { $groups = DB::World()->selectCol('SELECT `id` AS ARRAY_KEY, `spell_id` AS ARRAY_KEY2, `spell_id` FROM spell_group'); // unpack recursion @@ -727,12 +741,12 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($filtered = array_filter($groups, fn($x) => in_array($this->firstRank, $x))) { // get rule set - $rules = DB::World()->selectCol('SELECT `group_id` AS ARRAY_KEY, `stack_rule` FROM spell_group_stack_rules WHERE `group_id` IN (?a)', array_keys($filtered)); + $rules = DB::World()->selectCol('SELECT `group_id` AS ARRAY_KEY, `stack_rule` FROM spell_group_stack_rules WHERE `group_id` IN %in', array_keys($filtered)); // only use groups that have rules set if ($filtered = array_intersect_key($filtered, $rules)) { - $cnd = ['OR']; + $cnd = [DB::OR]; foreach ($filtered as $gr) $cnd[] = ['s.id', $gr]; @@ -756,7 +770,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $tabData = array( 'data' => $data, - 'id' => 'spell-group-stack-'.$rules[$gId], + 'id' => 'spell-group-stack-'.$gId, 'name' => Lang::spell('stackGroup'), 'visibleCols' => ['stackRules'] ); @@ -772,10 +786,10 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // tab: linked with - $rows = DB::World()->select( - 'SELECT `spell_trigger` AS "trigger", `spell_effect` AS "effect", `type`, IF(ABS(`spell_effect`) = ?d, ABS(`spell_trigger`), ABS(`spell_effect`)) AS "related" + $rows = DB::World()->selectAssoc( + 'SELECT `spell_trigger` AS "trigger", `spell_effect` AS "effect", `type`, IF(ABS(`spell_effect`) = %i, ABS(`spell_trigger`), ABS(`spell_effect`)) AS "related" FROM spell_linked_spell - WHERE ABS(`spell_effect`) = ?d OR ABS(`spell_trigger`) = ?d', + WHERE ABS(`spell_effect`) = %i OR ABS(`spell_trigger`) = %i', $this->typeId, $this->typeId, $this->typeId ); @@ -818,10 +832,10 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: triggered by $conditions = array( - 'OR', - ['AND', ['OR', ['effect1Id', SpellList::EFFECTS_TRIGGER], ['effect1AuraId', SpellList::AURAS_TRIGGER]], ['effect1TriggerSpell', $this->subject->id]], - ['AND', ['OR', ['effect2Id', SpellList::EFFECTS_TRIGGER], ['effect2AuraId', SpellList::AURAS_TRIGGER]], ['effect2TriggerSpell', $this->subject->id]], - ['AND', ['OR', ['effect3Id', SpellList::EFFECTS_TRIGGER], ['effect3AuraId', SpellList::AURAS_TRIGGER]], ['effect3TriggerSpell', $this->subject->id]], + DB::OR, + [DB::AND, [DB::OR, ['effect1Id', SpellList::EFFECTS_TRIGGER], ['effect1AuraId', SpellList::AURAS_TRIGGER]], ['effect1TriggerSpell', $this->typeId]], + [DB::AND, [DB::OR, ['effect2Id', SpellList::EFFECTS_TRIGGER], ['effect2AuraId', SpellList::AURAS_TRIGGER]], ['effect2TriggerSpell', $this->typeId]], + [DB::AND, [DB::OR, ['effect3Id', SpellList::EFFECTS_TRIGGER], ['effect3AuraId', SpellList::AURAS_TRIGGER]], ['effect3TriggerSpell', $this->typeId]], ); $trigger = new SpellList($conditions); @@ -838,16 +852,19 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: used by - creature $conditions = array( - 'OR', + DB::OR, ['spell1', $this->typeId], ['spell2', $this->typeId], ['spell3', $this->typeId], ['spell4', $this->typeId], ['spell5', $this->typeId], ['spell6', $this->typeId], ['spell7', $this->typeId], ['spell8', $this->typeId] ); if (!empty($ubSAI[Type::NPC])) $conditions[] = ['id', $ubSAI[Type::NPC]]; - if ($auras = DB::World()->selectCol('SELECT `entry` FROM creature_template_addon WHERE `auras` REGEXP ?', '\\b'.$this->typeId.'\\b')) + if ($auras = DB::World()->selectCol('SELECT `entry` FROM creature_template_addon WHERE `auras` REGEXP %s', '\\b'.$this->typeId.'\\b')) $conditions[] = ['id', $auras]; + if ($spellClick = DB::World()->selectCol('SELECT `npc_entry` FROM npc_spellclick_spells WHERE `spell_id` = %i', $this->typeId)) + $conditions[] = ['id', $spellClick]; + $ubCreature = new CreatureList($conditions); if (!$ubCreature->error) { @@ -862,7 +879,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // tab: zone - if ($areaSpells = DB::World()->select('SELECT `area` AS ARRAY_KEY, `aura_spell` AS "0", `quest_start` AS "1", `quest_end` AS "2", `quest_start_status` AS "3", `quest_end_status` AS "4", `racemask` AS "5", `gender` AS "6" FROM spell_area WHERE `spell` = ?d', $this->typeId)) + if ($areaSpells = DB::World()->selectAssoc('SELECT `area` AS ARRAY_KEY, `aura_spell` AS "0", `quest_start` AS "1", `quest_end` AS "2", `quest_start_status` AS "3", `quest_end_status` AS "4", `racemask` AS "5", `gender` AS "6" FROM spell_area WHERE `spell` = %i', $this->typeId)) { $zones = new ZoneList(array(['id', array_keys($areaSpells)])); if (!$zones->error) @@ -1000,19 +1017,19 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // tab: taught by npc - if ($this->subject->getSources($s) && in_array(SRC_TRAINER, $s)) + if ($this->subject->getRawSource(SRC_TRAINER)) { - $trainers = DB::World()->select( + $trainers = DB::World()->selectAssoc( 'SELECT cdt.`CreatureId` AS ARRAY_KEY, ts.`ReqSkillLine` AS "reqSkillId", ts.`ReqSkillRank` AS "reqSkillValue", ts.`ReqLevel` AS "reqLevel", ts.`ReqAbility1` AS "reqSpellId1", ts.`reqAbility2` AS "reqSpellId2" FROM creature_default_trainer cdt JOIN trainer_spell ts ON ts.`TrainerId` = cdt.`TrainerId` - WHERE ts.`SpellId` = ?d', + WHERE ts.`SpellId` = %i', $this->typeId ); if ($trainers) { - $tbTrainer = new CreatureList(array(Cfg::get('SQL_LIMIT_NONE'), ['ct.id', array_keys($trainers)], ['s.guid', null, '!'], ['ct.npcflag', NPC_FLAG_TRAINER, '&'])); + $tbTrainer = new CreatureList(array(['ct.id', array_keys($trainers)], ['s.guid', null, '!'], ['ct.npcflag', NPC_FLAG_TRAINER, '&'])); if (!$tbTrainer->error) { $this->extendGlobalData($tbTrainer->getJSGlobals()); @@ -1056,16 +1073,17 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: taught by spell $conditions = array( - 'OR', - ['AND', ['effect1Id', SpellList::EFFECTS_TEACH], ['effect1TriggerSpell', $this->subject->id]], - ['AND', ['effect2Id', SpellList::EFFECTS_TEACH], ['effect2TriggerSpell', $this->subject->id]], - ['AND', ['effect3Id', SpellList::EFFECTS_TEACH], ['effect3TriggerSpell', $this->subject->id]], + DB::OR, + [DB::AND, ['effect1Id', SpellList::EFFECTS_TEACH], ['effect1TriggerSpell', $this->typeId]], + [DB::AND, ['effect2Id', SpellList::EFFECTS_TEACH], ['effect2TriggerSpell', $this->typeId]], + [DB::AND, ['effect3Id', SpellList::EFFECTS_TEACH], ['effect3TriggerSpell', $this->typeId]], ); $tbSpell = new SpellList($conditions); $tbsData = []; if (!$tbSpell->error) { + $tbsData = $tbSpell->getFoundIDs(); $this->lvTabs->addListviewTab(new Listview(array( 'data' => $tbSpell->getListviewData(), 'id' => 'taught-by-spell', @@ -1076,15 +1094,14 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // tab: taught by quest - $conditions = ['OR', ['sourceSpellId', $this->typeId], ['rewardSpell', $this->typeId]]; + $conditions = array( + DB::OR, + ['sourceSpellId', $this->typeId], + ['rewardSpell', $this->typeId], + ['rewardSpellCast', $this->typeId] + ); if ($tbsData) - { - $conditions[] = ['rewardSpell', array_keys($tbsData)]; - if (User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = ['rewardSpellCast', array_keys($tbsData)]; - } - if (User::isInGroup(U_GROUP_EMPLOYEE)) - $conditions[] = ['rewardSpellCast', $this->typeId]; + array_push($conditions, ['rewardSpell', $tbsData], ['rewardSpellCast', $tbsData]); $tbQuest = new QuestList($conditions); if (!$tbQuest->error) @@ -1100,12 +1117,12 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: taught by item (i'd like to precheck $this->subject->sources, but there is no source:item only complicated crap like "drop" and "vendor") $conditions = array( - 'OR', - ['AND', ['spellTrigger1', SPELL_TRIGGER_LEARN], ['spellId1', $this->subject->id]], - ['AND', ['spellTrigger2', SPELL_TRIGGER_LEARN], ['spellId2', $this->subject->id]], - ['AND', ['spellTrigger3', SPELL_TRIGGER_LEARN], ['spellId3', $this->subject->id]], - ['AND', ['spellTrigger4', SPELL_TRIGGER_LEARN], ['spellId4', $this->subject->id]], - ['AND', ['spellTrigger5', SPELL_TRIGGER_LEARN], ['spellId5', $this->subject->id]], + DB::OR, + [DB::AND, ['spellTrigger1', SPELL_TRIGGER_LEARN], ['spellId1', $this->typeId]], + [DB::AND, ['spellTrigger2', SPELL_TRIGGER_LEARN], ['spellId2', $this->typeId]], + [DB::AND, ['spellTrigger3', SPELL_TRIGGER_LEARN], ['spellId3', $this->typeId]], + [DB::AND, ['spellTrigger4', SPELL_TRIGGER_LEARN], ['spellId4', $this->typeId]], + [DB::AND, ['spellTrigger5', SPELL_TRIGGER_LEARN], ['spellId5', $this->typeId]], ); $tbItem = new ItemList($conditions); @@ -1122,10 +1139,10 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: enchantments $conditions = array( - 'OR', - ['AND', ['type1', [ENCHANTMENT_TYPE_COMBAT_SPELL, ENCHANTMENT_TYPE_EQUIP_SPELL, ENCHANTMENT_TYPE_USE_SPELL]], ['object1', $this->typeId]], - ['AND', ['type2', [ENCHANTMENT_TYPE_COMBAT_SPELL, ENCHANTMENT_TYPE_EQUIP_SPELL, ENCHANTMENT_TYPE_USE_SPELL]], ['object2', $this->typeId]], - ['AND', ['type3', [ENCHANTMENT_TYPE_COMBAT_SPELL, ENCHANTMENT_TYPE_EQUIP_SPELL, ENCHANTMENT_TYPE_USE_SPELL]], ['object3', $this->typeId]] + DB::OR, + [DB::AND, ['type1', [ENCHANTMENT_TYPE_COMBAT_SPELL, ENCHANTMENT_TYPE_EQUIP_SPELL, ENCHANTMENT_TYPE_USE_SPELL]], ['object1', $this->typeId]], + [DB::AND, ['type2', [ENCHANTMENT_TYPE_COMBAT_SPELL, ENCHANTMENT_TYPE_EQUIP_SPELL, ENCHANTMENT_TYPE_USE_SPELL]], ['object2', $this->typeId]], + [DB::AND, ['type3', [ENCHANTMENT_TYPE_COMBAT_SPELL, ENCHANTMENT_TYPE_EQUIP_SPELL, ENCHANTMENT_TYPE_USE_SPELL]], ['object3', $this->typeId]] ); $enchList = new EnchantmentList($conditions); if (!$enchList->error) @@ -1143,9 +1160,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache $seSounds = []; for ($i = 1; $i < 4; $i++) // sounds from screen effect if ($this->subject->getField('effect'.$i.'AuraId') == SPELL_AURA_SCREEN_EFFECT) - $seSounds = DB::Aowow()->selectRow('SELECT `ambienceDay`, `ambienceNight`, `musicDay`, `musicNight` FROM ?_screeneffect_sounds WHERE `id` = ?d', $this->subject->getField('effect'.$i.'MiscValue')); + $seSounds = DB::Aowow()->selectRow('SELECT `ambienceDay`, `ambienceNight`, `musicDay`, `musicNight` FROM ::screeneffect_sounds WHERE `id` = %i', $this->subject->getField('effect'.$i.'MiscValue')); - $activitySounds = DB::Aowow()->selectRow('SELECT * FROM ?_spell_sounds WHERE `id` = ?d', $this->subject->getField('spellVisualId')); + $activitySounds = DB::Aowow()->selectRow('SELECT * FROM ::spell_sounds WHERE `id` = %i', $this->subject->getField('spellVisualId')); array_shift($activitySounds); // remove id-column if ($soundIDs = $activitySounds + $seSounds) { @@ -1166,6 +1183,55 @@ class SpellBaseResponse extends TemplateResponse implements ICache } } + // tab: unlocks (object or item) + $lockIds = DB::Aowow()->selectCol( + 'SELECT `id` FROM ::lock WHERE (`type1` = %i AND `properties1` = %i) OR + (`type2` = %i AND `properties2` = %i) OR (`type3` = %i AND `properties3` = %i) OR + (`type4` = %i AND `properties4` = %i) OR (`type5` = %i AND `properties5` = %i)', + LOCK_TYPE_SPELL, $this->typeId, LOCK_TYPE_SPELL, $this->typeId, + LOCK_TYPE_SPELL, $this->typeId, LOCK_TYPE_SPELL, $this->typeId, + LOCK_TYPE_SPELL, $this->typeId + ); + + // we know this spell effect is only in use on index 1 + if ($this->subject->getField('effect1Id') == SPELL_EFFECT_OPEN_LOCK && ($lockId = $this->subject->getField('effect1MiscValue'))) + $lockIds += DB::Aowow()->selectCol( + 'SELECT `id` FROM ::lock WHERE (`type1` = %i AND `properties1` = %i) OR + (`type2` = %i AND `properties2` = %i) OR (`type3` = %i AND `properties3` = %i) OR + (`type4` = %i AND `properties4` = %i) OR (`type5` = %i AND `properties5` = %i)', + LOCK_TYPE_SKILL, $lockId, LOCK_TYPE_SKILL, $lockId, + LOCK_TYPE_SKILL, $lockId, LOCK_TYPE_SKILL, $lockId, + LOCK_TYPE_SKILL, $lockId + ); + + if ($lockIds) + { + // objects + $lockedObj = new GameObjectList(array(['lockId', $lockIds])); + if (!$lockedObj->error) + { + $this->addDataLoader('zones'); + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lockedObj->getListviewData(), + 'name' => '$LANG.tab_unlocks', + 'id' => 'unlocks-object', + 'visibleCols' => $lockedObj->hasSetFields('reqSkill') ? ['skill'] : null + ), GameObjectList::$brickFile)); + } + + $lockedItm = new ItemList(array(['lockId', $lockIds])); + if (!$lockedItm->error) + { + $this->extendGlobalData($lockedItm->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $lockedItm->getListviewData(), + 'name' => '$LANG.tab_unlocks', + 'id' => 'unlocks-item' + ), ItemList::$brickFile)); + } + } + // find associated NPC, Item and merge results // taughtbypets (unused..?) // taughtbyquest (usually the spell casted as quest reward teaches something; exclude those seplls from taughtBySpell) @@ -1174,7 +1240,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache // tab: conditions $cnd = new Conditions(); - $cnd->getBySourceEntry($this->typeId, Conditions::SRC_SPELL_IMPLICIT_TARGET, Conditions::SRC_SPELL, Conditions::SRC_SPELL_CLICK_EVENT, Conditions::SRC_VEHICLE_SPELL, Conditions::SRC_SPELL_PROC) + $cnd->getBySource([Conditions::SRC_SPELL_IMPLICIT_TARGET, Conditions::SRC_SPELL, Conditions::SRC_SPELL_CLICK_EVENT, Conditions::SRC_VEHICLE_SPELL, Conditions::SRC_SPELL_PROC], entry: $this->typeId) ->getByCondition(Type::SPELL, $this->typeId) ->prepare(); if ($tab = $cnd->toListviewTab()) @@ -1191,7 +1257,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache /* SpellLoot recursive dropchance builder */ /******************************************/ - private function buildPctStack(float $baseChance, int $maxStack) : string + private function buildPctStack(float $baseChance, int $maxStack, int $baseCount = 1) : string { // note: pctStack does not contain absolute values but chances relative to the overall drop chance // e.g.: dropChance is 17% then [1 => 50, 2 => 25, 3 => 25] displays > Stack of 1: 8%; Stack of 2: 4%; Stack of 3: 4% @@ -1212,6 +1278,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache // cleanup tiny fractions $pctStack = array_filter($pctStack, fn($x) => ($x * $baseChance) >= 0.01); + if ($baseCount > 1) + $pctStack = array_combine(array_map(fn($x) => $x * $baseCount, array_keys($pctStack)), $pctStack); + return json_encode($pctStack, JSON_NUMERIC_CHECK); // do not replace with Util::toJSON ! } @@ -1227,9 +1296,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache $item = DB::Aowow()->selectRow( 'SELECT `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, i.`id`, ic.`name` AS `iconString`, `quality`, `spellId1`, `spellCharges1` - FROM ?_items i - LEFT JOIN ?_icons ic ON ic.`id` = i.`iconId` - WHERE i.`id` = ?d', + FROM ::items i + LEFT JOIN ::icons ic ON ic.`id` = i.`iconId` + WHERE i.`id` = %i', $itemId ); @@ -1267,18 +1336,19 @@ class SpellBaseResponse extends TemplateResponse implements ICache { $level++; // assume that tradeSpells only use the first index to create items, so this runs somewhat efficiently >.< - $spells = DB::Aowow()->select( + $spells = DB::Aowow()->selectAssoc( 'SELECT `reagent1`, `reagent2`, `reagent3`, `reagent4`, `reagent5`, `reagent6`, `reagent7`, `reagent8`, `reagentCount1`, `reagentCount2`, `reagentCount3`, `reagentCount4`, `reagentCount5`, `reagentCount6`, `reagentCount7`, `reagentCount8`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `iconIdBak`, s.`id` AS ARRAY_KEY, ic.`name` AS `iconString` - FROM ?_spell s - JOIN ?_icons ic ON s.`iconId` = ic.`id` - WHERE (`effect1CreateItemId` = ?d AND `effect1Id` = ?d)',// OR - // (`effect2CreateItemId` = ?d AND `effect2Id` = ?d) OR - // (`effect3CreateItemId` = ?d AND `effect3Id` = ?d)', - $itemId, SPELL_EFFECT_CREATE_ITEM //, $itemId, SPELL_EFFECT_CREATE_ITEM, $itemId, SPELL_EFFECT_CREATE_ITEM + FROM ::spell s + JOIN ::icons ic ON s.`iconId` = ic.`id` + WHERE (s.`cuFlags` & %i) = 0 AND + (`effect1CreateItemId` = %i AND `effect1Id` = %i)',// OR + // (`effect2CreateItemId` = %i AND `effect2Id` = %i) OR + // (`effect3CreateItemId` = %i AND `effect3Id` = %i)', + CUSTOM_UNAVAILABLE, $itemId, SPELL_EFFECT_CREATE_ITEM //, $itemId, SPELL_EFFECT_CREATE_ITEM, $itemId, SPELL_EFFECT_CREATE_ITEM ); if (!$spells) @@ -1333,10 +1403,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache if (!$reagents) return; - foreach ($this->subject->relItems->iterate() as $iId => $__) + foreach ($reagents as [$iId, $num]) { - if (!in_array($iId, array_keys($reagents))) - continue; + $relItem = $this->subject->relItems->getEntry($iId); $data = array( 'path' => Type::ITEM.'-'.$iId, // id of the html-element @@ -1345,11 +1414,12 @@ class SpellBaseResponse extends TemplateResponse implements ICache 'typeStr' => Type::getFileString(Type::ITEM), 'icon' => new IconElement( Type::ITEM, - $iId, - $this->subject->relItems->getField('name', true), - $reagents[$iId][1], - quality: $this->subject->relItems->getField('quality'), + is_null($relItem) ? 0 : $iId, + is_null($relItem) ? 'Item #'.$iId : $this->subject->relItems->getField('name', true), + $num, + quality: $relItem['quality'] ?? 'q', size: IconElement::SIZE_SMALL, + link: !is_null($relItem), align: 'right', element: 'iconlist-icon' ) @@ -1372,9 +1442,15 @@ class SpellBaseResponse extends TemplateResponse implements ICache $this->reagents = [$enhanced, $reagentResult]; } - private function createScalingData() : void // calculation mostly like seen in TC + private function calculateEffectScaling() : array // calculation mostly like seen in TC { - $scaling = ['directSP' => 0, 'dotSP' => 0, 'directAP' => 0, 'dotAP' => 0]; + if ($this->subject->getField('attributes3') & SPELL_ATTR3_NO_DONE_BONUS) + return [0, 0, 0, 0]; + + if (!$this->subject->isScalableDamagingSpell() && !$this->subject->isScalableHealingSpell()) + return [0, 0, 0, 0]; + + $scaling = [0, 0, 0, 0]; $pMask = $this->subject->periodicEffectsMask(); $allDoTs = true; @@ -1385,25 +1461,20 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($pMask & 1 << ($i - 1)) { - $scaling['dotSP'] = $this->subject->getField('effect'.$i.'BonusMultiplier'); + $scaling[1] = $this->subject->getField('effect'.$i.'BonusMultiplier'); continue; } - else - $scaling['directSP'] = $this->subject->getField('effect'.$i.'BonusMultiplier'); + else if ($this->subject->getField('damageClass') == SPELL_DAMAGE_CLASS_MAGIC) + $scaling[0] = $this->subject->getField('effect'.$i.'BonusMultiplier'); $allDoTs = false; } - if ($s = DB::World()->selectRow('SELECT `direct_bonus` AS "directSP", `dot_bonus` AS "dotSP", `ap_bonus` AS "directAP", `ap_dot_bonus` AS "dotAP" FROM spell_bonus_data WHERE `entry` = ?d', $this->firstRank)) + if ($s = DB::World()->selectRow('SELECT `direct_bonus` AS "0", `dot_bonus` AS "1", `ap_bonus` AS "2", `ap_dot_bonus` AS "3" FROM spell_bonus_data WHERE `entry` = %i', $this->firstRank)) $scaling = $s; - if ((!$this->subject->isDamagingSpell() && !$this->subject->isHealingSpell()) || - !in_array($this->subject->getField('typeCat'), [-2, -3, -7, 7]) || - $this->subject->getField('damageClass') == SPELL_DAMAGE_CLASS_NONE) - { - $this->scaling = array_filter($scaling, fn($x) => $x > 0); - return; - } + if (!in_array($this->subject->getField('typeCat'), [-2, -3, -7, 7]) || $this->subject->getField('damageClass') == SPELL_DAMAGE_CLASS_NONE) + return array_map(fn($x) => $x < 0 ? 0 : $x, $scaling); foreach ($scaling as $k => $v) { @@ -1412,15 +1483,15 @@ class SpellBaseResponse extends TemplateResponse implements ICache continue; // no known calculation for physical abilities - if ($k == 'directAP' || $k == 'dotAP') + if (in_array($k, [2, 3])) // [direct AP, DoT AP] continue; // dont use spellPower to scale physical Abilities - if ($this->subject->getField('schoolMask') == (1 << SPELL_SCHOOL_NORMAL) && ($k == 'directSP' || $k == 'dotSP')) + if ($this->subject->getField('schoolMask') == (1 << SPELL_SCHOOL_NORMAL) && in_array($k, [0, 1])) continue; $isDOT = false; - if ($k == 'dotSP' || $k == 'dotAP') + if (in_array($k, [1, 3])) // [DoT SP, DoT AP] { if ($pMask) $isDOT = true; @@ -1458,7 +1529,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache } } - if ($this->subject->isHealingSpell()) + if ($this->subject->isScalableHealingSpell()) $castingTime *= 1.88; // SPELL_SCHOOL_MASK_NORMAL @@ -1468,7 +1539,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $scaling[$k] = 0; // would be 1 ($dotFactor), but we dont want it to be displayed } - $this->scaling = array_filter($scaling, fn($x) => $x > 0); + return array_map(fn($x) => $x < 0 ? 0 : $x, $scaling); } private function createRequiredItems() : void @@ -1520,7 +1591,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache 'cooldown' => 0 ); - if ($sp = DB::World()->selectRow('SELECT IF(`ProcsPerMinute` > 0, -`ProcsPerMinute`, `Chance`) AS "chance", `Cooldown` AS "cooldown" FROM `spell_proc` WHERE ABS(`SpellId`) = ?d', $this->firstRank)) + if ($sp = DB::World()->selectRow('SELECT IF(`ProcsPerMinute` > 0, -`ProcsPerMinute`, `Chance`) AS "chance", `Cooldown` AS "cooldown" FROM `spell_proc` WHERE ABS(`SpellId`) = %i', $this->firstRank)) { $procData['chance'] = $sp['chance'] ?: $procData['chance']; $procData['cooldown'] = $sp['cooldown'] ?: $procData['cooldown']; @@ -1529,7 +1600,8 @@ class SpellBaseResponse extends TemplateResponse implements ICache $effects = []; $spellIdx = array_unique(array_merge($this->subject->canTriggerSpell(), $this->subject->canTeachSpell())); $itemIdx = $this->subject->canCreateItem(); - $perfItem = DB::World()->selectRow('SELECT `perfectItemType` AS "itemId", `requiredSpecialization` AS "reqSpellId", `perfectCreateChance` AS "chance" FROM skill_perfect_item_template WHERE `spellId` = ?d', $this->typeId); + $perfItem = DB::World()->selectRow('SELECT `perfectItemType` AS "itemId", `requiredSpecialization` AS "reqSpellId", `perfectCreateChance` AS "chance" FROM skill_perfect_item_template WHERE `spellId` = %i', $this->typeId); + $scaling = $this->calculateEffectScaling(); // Iterate through all effects: for ($i = 1; $i < 4; $i++) @@ -1561,7 +1633,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache */ $_nameEffect = $_nameAura = $_nameMV = $_nameMVB = $_markup = ''; - $_icon = $_perfItem = $_footer = []; + $_icon = $_perfItem = $_footer = []; $_footer['value'] = [0, 0]; $valueFmt = '%s'; @@ -1578,7 +1650,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache Type::ITEM, $itemId, $itemEntry ? $this->subject->relItems->getField('name', true) : Util::ucFirst(Lang::game('item')).' #'.$itemId, - ($effBP + 1) . ($effDS > 1 ? '-' . ($effBP + $effDS) : ''), + $this->createNumRange($effBP, $effDS), quality: $itemEntry ? $this->subject->relItems->getField('quality') : '', link: !empty($itemEntry) ); @@ -1591,7 +1663,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache if (!$cndSpell->error) { $_perfItem = array( - 'spellId' => $perfItem['reqSpellId'], + 'spellId' => $cndSpell->id, 'spellName' => $cndSpell->getField('name', true), 'icon' => $cndSpell->getField('iconString'), 'chance' => $perfItem['chance'], @@ -1604,6 +1676,26 @@ class SpellBaseResponse extends TemplateResponse implements ICache ); } } + else if ($extraItem = DB::World()->selectRow('SELECT * FROM skill_extra_item_template WHERE `spellid` = %i', $this->typeId)) + { + $cndSpell = new SpellList(array(['id', $extraItem['requiredSpecialization']])); + if (!$cndSpell->error) + { + $_perfItem = array( + 'spellId' => $cndSpell->id, + 'spellName' => $cndSpell->getField('name', true), + 'icon' => $cndSpell->getField('iconString'), + 'chance' => $extraItem['additionalCreateChance'], + 'item' => new IconElement( + Type::ITEM, + $this->subject->relItems->id, + $this->subject->relItems->getField('name', true), + num: '+'.$this->createNumRange($effBP, $effDS, $extraItem['additionalMaxNum']), + quality: $this->subject->relItems->getField('quality') + ) + ); + } + } } // .. from spell else if (in_array($i, $spellIdx) || $effId == SPELL_EFFECT_UNLEARN_SPECIALIZATION) @@ -1638,16 +1730,16 @@ class SpellBaseResponse extends TemplateResponse implements ICache $_footer['radius'] = Lang::spell('_radius').$this->subject->getField('effect'.$i.'RadiusMax').' '.Lang::spell('_distUnit'); if ($this->subject->getField('effect'.$i.'Periode') > 0) - $_footer['interval'] = Lang::spell('_interval').Util::formatTime($this->subject->getField('effect'.$i.'Periode')); + $_footer['interval'] = Lang::spell('_interval').DateTime::formatTimeElapsedFloat($this->subject->getField('effect'.$i.'Periode')); if ($_ = $this->subject->getField('effect'.$i.'Mechanic')) $_footer['mechanic'] = Lang::game('mechanic').Lang::main('colon').Lang::game('me', $_); if (in_array($i, $this->subject->canTriggerSpell()) && $procData['chance'] && $procData['chance'] < 100) { - $_footer['proc'] = $procData['chance'] < 0 ? Lang::spell('ppm', [Lang::nf(-$procData['chance'], 1)]) : Lang::spell('procChance') . $procData['chance'] . '%'; + $_footer['proc'] = $procData['chance'] < 0 ? Lang::spell('ppm', [-$procData['chance']]) : Lang::spell('procChance', [$procData['chance']]); if ($procData['cooldown']) - $_footer['procCD'] = Lang::game('cooldown', [Util::formatTime($procData['cooldown'], true)]); + $_footer['procCD'] = Lang::game('cooldown', [DateTime::formatTimeElapsed($procData['cooldown'])]); } // Effect Name @@ -1690,7 +1782,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $effMVB = 67; // TC uses hardcoded summon property 67 // DO NOT BREAK ! case SPELL_EFFECT_SUMMON: - if (($sp = DB::Aowow()->selectRow('SELECT `control`, `slot` FROM ?_summonproperties WHERE `id` = ?d', $effMVB))) + if (($sp = DB::Aowow()->selectRow('SELECT `control`, `slot` FROM ::summonproperties WHERE `id` = %i', $effMVB))) $_nameMVB = $this->fmtStaffTip(Lang::spell('summonControl', $sp['control']).' – '.Lang::spell('summonSlot', $sp['slot']) , 'SummonProperty: '.$effMVB); // DO NOT BREAK ! case SPELL_EFFECT_SUMMON_DEMON: @@ -1739,7 +1831,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); break; case SPELL_EFFECT_APPLY_GLYPH: - if ($_ = DB::Aowow()->selectCell('SELECT `spellId` FROM ?_glyphproperties WHERE `id` = ?d', $effMV)) + if ($_ = DB::Aowow()->selectCell('SELECT `spellId` FROM ::glyphproperties WHERE `id` = %i', $effMV)) { if ($a = SpellList::makeLink($_)) $_nameMV = $a; @@ -1775,7 +1867,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache break; case SPELL_EFFECT_PLAY_SOUND: case SPELL_EFFECT_PLAY_MUSIC: - if (DB::Aowow()->selectCell('SELECT 1 FROM ?_sounds WHERE `id` = ?d', $effMV)) + if (DB::Aowow()->selectCell('SELECT 1 FROM ::sounds WHERE `id` = %i', $effMV)) { $_markup = '[sound='.$effMV.']'; $effMV = 0; // prevent default display @@ -1790,20 +1882,30 @@ class SpellBaseResponse extends TemplateResponse implements ICache $_nameMV = Util::ucFirst(Lang::game('faction')).' #'.$effMV; // apply custom reward rated - if ($cuRate = DB::World()->selectCell('SELECT `spell_rate` FROM reputation_reward_rate WHERE `spell_rate` <> 1 AND `faction` = ?d', $effMV)) + if ($cuRate = DB::World()->selectCell('SELECT `spell_rate` FROM reputation_reward_rate WHERE `spell_rate` <> 1 AND `faction` = %i', $effMV)) $_footer['value'][2] = sprintf(Util::$dfnString, Lang::faction('customRewRate'), ' ('.(($cuRate < 1 ? '-' : '+').intVal(($cuRate - 1) * $_footer['value'][0])).')'); break; case SPELL_EFFECT_SEND_TAXI: $_ = DB::Aowow()->selectRow( - 'SELECT tn1.`name_loc0` AS `start_loc0`, tn1.name_loc?d AS start_loc?d, tn2.`name_loc0` AS `end_loc0`, tn2.name_loc?d AS end_loc?d - FROM ?_taxipath tp - JOIN ?_taxinodes tn1 ON tp.`startNodeId` = tn1.`id` - JOIN ?_taxinodes tn2 ON tp.`endNodeId` = tn2.`id` - WHERE tp.`id` = ?d', + 'SELECT tn1.`areaId` AS "startAreaId", tn1.`areaX` AS "startPosX", tn1.`areaY` AS "startPosY", tn1.`name_loc0` AS "start_loc0", tn1.name_loc%i AS start_loc%i, + tn2.`areaId` AS "endAreaId", tn2.`areaX` AS "endPosX", tn2.`areaY` AS "endPosY", tn2.`name_loc0` AS "end_loc0", tn2.name_loc%i AS end_loc%i + FROM ::taxipath tp + JOIN ::taxinodes tn1 ON tp.`startNodeId` = tn1.`id` + JOIN ::taxinodes tn2 ON tp.`endNodeId` = tn2.`id` + WHERE tp.`id` = %i', Lang::getLocale()->value, Lang::getLocale()->value, Lang::getLocale()->value, Lang::getLocale()->value, $effMV ); if ($_) - $_nameMV = $this->fmtStaffTip('<span class="breadcrumb-arrow">'.Util::localizedString($_, 'start').'</span>'.Util::localizedString($_, 'end'), 'MiscValue: '.$effMV); + { + $start = Util::localizedString($_, 'start'); + if ($_['startAreaId']) + $start = sprintf('<a href="?maps=%d:%03d%03d">%s</a>', $_['startAreaId'], $_['startPosX'] * 10, $_['startPosY'] * 10, $start); + $end = Util::localizedString($_, 'end'); + if ($_['endAreaId']) + $end = sprintf('<a href="?maps=%d:%03d%03d">%s</a>', $_['endAreaId'], $_['endPosX'] * 10, $_['endPosY'] * 10, $end); + + $_nameMV = $this->fmtStaffTip('<span class="breadcrumb-arrow">'.$start.'</span>'.$end, 'MiscValue: '.$effMV); + } break; case SPELL_EFFECT_TITAN_GRIP: $effMV = 0; // effMV is trigger spell and was handled earlier @@ -1892,6 +1994,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); break; case SPELL_AURA_MOD_LANGUAGE: + case SPELL_AURA_COMPREHEND_LANGUAGE: if ($_ = Lang::game('languages', $effMV)) $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.$effMV); break; @@ -1967,8 +2070,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($_ = Lang::getMagicSchools($effMV)) $_nameMV = $this->fmtStaffTip($_, 'MiscValue: '.Util::asHex($effMV)); break; - case SPELL_AURA_MOD_SKILL: - case SPELL_AURA_MOD_SKILL_TALENT: + case SPELL_AURA_MOD_SKILL: // temp + case SPELL_AURA_MOD_SKILL_TALENT: // perm + $valueFmt = '%+d'; if ($a = SkillList::makeLink($effMV)) $_nameMV = $a; else @@ -2051,7 +2155,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache $_nameMV = Util::ucFirst(Lang::game('faction')).' #'.$effMV; break; // also breaks for SPELL_AURA_FORCE_REACTION case SPELL_AURA_OVERRIDE_SPELLS: - if ($so = DB::Aowow()->selectRow('SELECT `spellId1`, `spellId2`, `spellId3`, `spellId4`, `spellId5` FROM ?_spelloverride WHERE `id` = ?d', $effMV)) + if ($so = DB::Aowow()->selectRow('SELECT `spellId1`, `spellId2`, `spellId3`, `spellId4`, `spellId5` FROM ::spelloverride WHERE `id` = %i', $effMV)) { if ($so = array_filter($so)) { @@ -2078,7 +2182,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache trigger_error('unused case #'.$effMV.' found for aura #'.$effAura); break; case SPELL_AURA_SCREEN_EFFECT: - if ($ses = DB::Aowow()->selectRow('SELECT `name`, `ambienceDay` AS "0", IF(`ambienceNight` <> `ambienceDay`, `ambienceNight`, 0) AS "1", `musicDay` AS "2", IF(`musicNight` <> `musicDay`, `musicNight`, 0) AS "3" FROM ?_screeneffect_sounds WHERE `id` = ?d', $effMV)) + if ($ses = DB::Aowow()->selectRow('SELECT `name`, `ambienceDay` AS "0", IF(`ambienceNight` <> `ambienceDay`, `ambienceNight`, 0) AS "1", `musicDay` AS "2", IF(`musicNight` <> `musicDay`, `musicNight`, 0) AS "3" FROM ::screeneffect_sounds WHERE `id` = %i', $effMV)) { $_nameMV = $this->fmtStaffTip($ses['name'], 'MiscValue: '.$effMV); for ($j = 0; $j < 4; $j++) @@ -2137,6 +2241,21 @@ class SpellBaseResponse extends TemplateResponse implements ICache if (isset($_footer['value'][2])) $buffer .= $_footer['value'][2]; + if (in_array($effId, SpellList::EFFECTS_SCALING_DAMAGE)) + { + if ($scaling[2]) + $buffer .= Lang::spell('apMod', [$scaling[2]]); + if ($scaling[0]) + $buffer .= Lang::spell('spMod', [$scaling[0]]); + } + if (in_array($effAura, SpellList::AURAS_SCALING_DAMAGE)) + { + if ($scaling[3]) + $buffer .= Lang::spell('apMod', [$scaling[3]]); + if ($scaling[1]) + $buffer .= Lang::spell('spMod', [$scaling[1]]); + } + $_footer['value'] = $buffer; } else @@ -2197,10 +2316,16 @@ class SpellBaseResponse extends TemplateResponse implements ICache $this->attributes = $list; } + private function createNumRange(int $bp, int $ds, int $mult = 1) : string + { + return Util::createNumRange($bp + 1, ($bp + $ds) * $mult, '-'); + } + private function generatePath() { $cat = $this->subject->getField('typeCat'); $cf = $this->subject->getField('cuFlags'); + $sl = $this->subject->getField('skillLines'); $this->breadcrumb[] = $cat; @@ -2222,14 +2347,15 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($cat == -13) $this->breadcrumb[] = ($cf & (SPELL_CU_GLYPH_MAJOR | SPELL_CU_GLYPH_MINOR)) >> 6; - else - $this->breadcrumb[] = $this->subject->getField('skillLines')[0]; + else if ($sl) + $this->breadcrumb[] = $sl[0]; break; case 9: case -3: case 11: - $this->breadcrumb[] = $this->subject->getField('skillLines')[0]; + if ($sl) + $this->breadcrumb[] = $sl[0]; if ($cat == 11) if ($_ = $this->subject->getField('reqSpellId')) @@ -2238,7 +2364,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache break; case -11: foreach (SpellList::$skillLines as $line => $skills) - if (in_array($this->subject->getField('skillLines')[0], $skills)) + if (in_array($sl[0] ?? [], $skills)) $this->breadcrumb[] = $line; break; case -7: // only spells unique in skillLineAbility will always point to the right skillLine :/ @@ -2263,8 +2389,9 @@ class SpellBaseResponse extends TemplateResponse implements ICache private function createInfobox() : void { - $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); - $typeCat = $this->subject->getField('typeCat'); + $infobox = Lang::getInfoBoxForFlags($this->subject->getField('cuFlags')); + $typeCat = $this->subject->getField('typeCat'); + $hasCompletion = in_array($typeCat, [-5, -6]) && !($this->subject->getField('cuFlags') & CUSTOM_EXCLUDE_FOR_LISTVIEW); // level if (!in_array($typeCat, [-5, -6])) // not mount or vanity pet @@ -2275,8 +2402,8 @@ class SpellBaseResponse extends TemplateResponse implements ICache $infobox[] = (in_array($typeCat, [-2, 7, -13]) ? Lang::game('reqLevel', [$_]) : Lang::game('level').Lang::main('colon').$_); } - $jsg = []; // races + $jsg = []; if ($_ = Lang::getRaceString($this->subject->getField('reqRaceMask'), $jsg, Lang::FMT_MARKUP)) { $this->extendGlobalIds(Type::CHR_RACE, ...$jsg); @@ -2285,6 +2412,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // classes + $jsg = []; if ($_ = Lang::getClassString($this->subject->getField('reqClassMask'), $jsg, Lang::FMT_MARKUP)) { $this->extendGlobalIds(Type::CHR_CLASS, ...$jsg); @@ -2295,10 +2423,12 @@ class SpellBaseResponse extends TemplateResponse implements ICache // spell focus if ($_ = $this->subject->getField('spellFocusObject')) { - if ($sfObj = DB::Aowow()->selectRow('SELECT * FROM ?_spellfocusobject WHERE `id` = ?d', $_)) + if ($sfObj = DB::Aowow()->selectRow('SELECT * FROM ::spellfocusobject WHERE `id` = %i', $_)) { $n = Util::localizedString($sfObj, 'name'); - if ($objId = DB::Aowow()->selectCell('SELECT `id` FROM ?_objects WHERE `spellFocusId` = ?d', $_)) + if (!is_null(GameObjectListFilter::getCriteriaIndex(50, $_))) + $n = '[url=?objects&filter=cr=50;crs='.$_.';crv=0]'.$n.'[/url]'; + else if ($objId = DB::Aowow()->selectCell('SELECT `id` FROM ::objects WHERE `spellFocusId` = %i', $_)) $n = '[url=?object='.$objId.']'.$n.'[/url]'; $infobox[] = Lang::game('requires2').' '.$n; @@ -2309,11 +2439,11 @@ class SpellBaseResponse extends TemplateResponse implements ICache if (in_array($typeCat, [9, 11])) { // skill - if ($_ = $this->subject->getField('skillLines')[0]) + if ($_ = $this->subject->getField('skillLines')) { - $this->extendGlobalIds(Type::SKILL, $_); + $this->extendGlobalIds(Type::SKILL, $_[0]); - $bar = Lang::game('requires', [' [skill='.$_.']']); + $bar = Lang::game('requires', [' [skill='.$_[0].']']); if ($_ = $this->subject->getField('learnedAt')) $bar .= ' ('.$_.')'; @@ -2333,29 +2463,43 @@ class SpellBaseResponse extends TemplateResponse implements ICache } // accquisition.. 10: starter spell; 7: discovery - if ($this->subject->getSources($s)) - { - if (in_array(SRC_STARTER, $s)) - $infobox[] = Lang::spell('starter'); - else if (in_array(SRC_DISCOVERY, $s)) - $infobox[] = Lang::spell('discovered'); - } + if ($this->subject->getRawSource(SRC_STARTER)) + $infobox[] = Lang::spell('starter'); + else if ($this->subject->getRawSource(SRC_DISCOVERY)) + $infobox[] = Lang::spell('discovered'); // training cost if ($cost = $this->subject->getField('trainingCost')) $infobox[] = Lang::spell('trainingCost').'[money='.$cost.']'; + // id + $infobox[] = Lang::spell('id') . $this->typeId; + // icon if ($_ = $this->subject->getField('iconId')) { + $infobox[] = Util::ucFirst(Lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; $this->extendGlobalIds(Type::ICON, $_); - $infobox[] = Util::ucFirst(lang::game('icon')).Lang::main('colon').'[icondb='.$_.' name=true]'; } + // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) + if (Cfg::get('PROFILER_ENABLE') && $hasCompletion) + { + $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ::profiler_completion_spells WHERE `spellId` = %i', $this->typeId); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ::profiler_profiles WHERE `custom` = 0 AND `stub` = 0'); + $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); + + // completion row added by InfoboxMarkup + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + // used in mode foreach ($this->difficulties as $n => $id) if ($id == $this->typeId) - $infobox[] = Lang::game('mode').Lang::game('modes', $n); + $infobox[] = Lang::game('mode').Lang::game('modes', $this->mapType, $n); // Creature Type from Aura: Shapeshift foreach ($this->modelInfo as $mI) @@ -2371,11 +2515,11 @@ class SpellBaseResponse extends TemplateResponse implements ICache // spell script if (User::isInGroup(U_GROUP_STAFF)) - if ($_ = DB::World()->selectCell('SELECT `ScriptName` FROM spell_script_names WHERE ABS(`spell_id`) = ?d', $this->firstRank)) + if ($_ = DB::World()->selectCell('SELECT `ScriptName` FROM spell_script_names WHERE ABS(`spell_id`) = %i', $this->firstRank)) $infobox[] = 'Script'.Lang::main('colon').$_; - $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', $hasCompletion); // append glyph symbol if available $glyphId = 0; @@ -2383,7 +2527,7 @@ class SpellBaseResponse extends TemplateResponse implements ICache if ($this->subject->getField('effect'.$i.'Id') == SPELL_EFFECT_APPLY_GLYPH) $glyphId = $this->subject->getField('effect'.$i.'MiscValue'); - if ($_ = DB::Aowow()->selectCell('SELECT ic.`name` FROM ?_glyphproperties gp JOIN ?_icons ic ON gp.`iconId` = ic.`id` WHERE gp.`spellId` = ?d { OR gp.`id` = ?d }', $this->typeId, $glyphId ?: DBSIMPLE_SKIP)) + if ($_ = DB::Aowow()->selectCell('SELECT ic.`name` FROM ::glyphproperties gp JOIN ::icons ic ON gp.`iconId` = ic.`id` WHERE %if', $glyphId, 'gp.`id` = %i OR', $glyphId, '%end gp.`spellId` = %i', $this->typeId)) if (file_exists('static/images/wow/Interface/Spellbook/'.$_.'.png')) $this->infobox->append('[img src='.Cfg::get('STATIC_URL').'/images/wow/Interface/Spellbook/'.$_.'.png border=0 float=center margin=15]'); } diff --git a/endpoints/spells/spells.php b/endpoints/spells/spells.php index db58d8e9..eadebde5 100644 --- a/endpoints/spells/spells.php +++ b/endpoints/spells/spells.php @@ -26,7 +26,7 @@ class SpellsBaseResponse extends TemplateResponse implements ICache ); protected int $type = Type::SPELL; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'spells'; protected string $pageName = 'spells'; @@ -91,14 +91,22 @@ class SpellsBaseResponse extends TemplateResponse implements ICache public bool $classPanel = false; public bool $glyphPanel = false; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); + + if ($this->category) + $this->subCat = '='.implode('.', $this->category); - $this->subCat = $pageParam !== '' ? '='.$pageParam : ''; $this->filter = new SpellListFilter($this->_get['filter'] ?? '', ['parentCats' => $this->category]); + if ($this->filter->shouldReload) + { + $_SESSION['error']['fi'] = $this->filter::class; + $get = $this->filter->buildGETParam(); + $this->forward('?' . $this->pageName . $this->subCat . ($get ? '&filter=' . $get : '')); + } $this->filterError = $this->filter->error; } @@ -106,17 +114,13 @@ class SpellsBaseResponse extends TemplateResponse implements ICache { $this->h1 = Util::ucFirst(Lang::game('spells')); - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; - $this->filter->evalCriteria(); - if ($_ = $this->filter->getConditions()) $conditions[] = $_; - $this->filterError = $this->filter->error; // maybe the evalX() caused something - /*************/ /* Menu Path */ @@ -203,26 +207,26 @@ class SpellsBaseResponse extends TemplateResponse implements ICache { if ($skillLineId == $this->category[1]) { - $xCond = ['AND', ['s.skillLine1', $i], ['s.skillLine2OrMask', 1 << $idx, '&']]; + $xCond = [DB::AND, ['s.skillLine1', $i], ['s.skillLine2OrMask', 1 << $idx, '&']]; break; } } } $conditions[] = [ - 'OR', + DB::OR, $xCond, ['s.skillLine1', $this->category[1]], - ['AND', ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->category[1]]] + [DB::AND, ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->category[1]]] ]; } else { $conditions[] = [ - 'OR', + DB::OR, ['s.skillLine1', [-1, -2]], ['s.skillLine1', $this->validCats[-3]], - ['AND', ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->validCats[-3]]] + [DB::AND, ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->validCats[-3]]] ]; } @@ -244,16 +248,16 @@ class SpellsBaseResponse extends TemplateResponse implements ICache switch ($this->category[1]) { case 1: - $conditions[] = ['OR', - ['AND', ['effect2AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED], ['effect3AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED, '!']], - ['AND', ['effect3AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED], ['effect2AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED, '!']] + $conditions[] = [DB::OR, + [DB::AND, ['effect2AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED], ['effect3AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED, '!']], + [DB::AND, ['effect3AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED], ['effect2AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED, '!']] ]; break; case 2: - $conditions[] = ['OR', ['effect2AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED], ['effect3AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED]]; + $conditions[] = [DB::OR, ['effect2AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED], ['effect3AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED]]; break; case 3: - $conditions[] = ['AND', + $conditions[] = [DB::AND, ['effect2AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED, '!'], ['effect2AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED, '!'], ['effect3AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_SPEED, '!'], ['effect3AuraId', SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED, '!'] ]; @@ -302,7 +306,7 @@ class SpellsBaseResponse extends TemplateResponse implements ICache if (isset($this->category[1])) { if ($this->category[1] == 6) // todo (med): we know Weapon(6) includes spell Shoot(3018), that has a mask; but really, ANY proficiency or petSkill should be in that mask so there is no need to differenciate - $conditions[] = ['OR', ['s.skillLine1', SpellList::$skillLines[$this->category[1]]], ['s.skillLine1', -3]]; + $conditions[] = [DB::OR, ['s.skillLine1', SpellList::$skillLines[$this->category[1]]], ['s.skillLine1', -3]]; else $conditions[] = ['s.skillLine1', SpellList::$skillLines[$this->category[1]]]; } @@ -335,24 +339,24 @@ class SpellsBaseResponse extends TemplateResponse implements ICache // $conditions[] = [ // [['s.attributes0', 0x80, '&'], 0], // ~SPELL_ATTR0_HIDDEN_CLIENTSIDE // ['s.attributes0', 0x20, '&'], // SPELL_ATTR0_TRADESPELL (DK: Runeforging) - // 'OR' + // DB::OR // ]; if (isset($this->category[2])) { $conditions[] = [ - 'OR', + DB::OR, ['s.skillLine1', $this->category[2]], - ['AND', ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->category[2]]] + [DB::AND, ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->category[2]]] ]; } else if (isset($this->category[1])) { $conditions[] = [ - 'OR', + DB::OR, ['s.skillLine1', $this->validCats[7][$this->category[1]]], - ['AND', ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->validCats[7][$this->category[1]]]] + [DB::AND, ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->validCats[7][$this->category[1]]]] ]; } @@ -366,9 +370,9 @@ class SpellsBaseResponse extends TemplateResponse implements ICache if (isset($this->category[1])) { $conditions[] = [ - 'OR', + DB::OR, ['s.skillLine1', $this->category[1]], - ['AND', ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->category[1]]] + [DB::AND, ['s.skillLine1', 0, '>'], ['s.skillLine2OrMask', $this->category[1]]] ]; if (!empty(self::SHORT_FILTER[$this->category[1]])) @@ -436,9 +440,9 @@ class SpellsBaseResponse extends TemplateResponse implements ICache array_push($visibleCols, 'level'); $conditions[] = [ - 'OR', + DB::OR, ['s.typeCat', 0], - ['AND', ['s.cuFlags', SPELL_CU_TRIGGERED, '&'], ['s.typeCat', [7, -2]]] + [DB::AND, ['s.cuFlags', SPELL_CU_TRIGGERED, '&'], ['s.typeCat', [7, -2]]] ]; break; @@ -482,9 +486,9 @@ class SpellsBaseResponse extends TemplateResponse implements ICache $visibleCols[] = 'source'; // create note if search limit was exceeded; overwriting 'note' is intentional - if ($spells->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($spells->getMatches() > Listview::DEFAULT_SIZE) { - $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_spellsfound', $spells->getMatches(), Cfg::get('SQL_LIMIT_DEFAULT')); + $tabData['note'] = sprintf(Util::$tryFilteringString, 'LANG.lvnote_spellsfound', $spells->getMatches(), Listview::DEFAULT_SIZE); $tabData['_truncated'] = 1; } diff --git a/endpoints/title/title.php b/endpoints/title/title.php index 84783c94..21959316 100644 --- a/endpoints/title/title.php +++ b/endpoints/title/title.php @@ -10,7 +10,7 @@ class TitleBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected array $breadcrumb = [0, 10]; @@ -84,8 +84,25 @@ class TitleBaseResponse extends TemplateResponse implements ICache $infobox[] = Lang::game('eventShort', ['[event='.$eId.']']); } + // id + $infobox[] = Lang::title('id') . $this->typeId; + + // profiler relateed (note that this is part of the cache. I don't think this is important enough to calc for every view) + if (Cfg::get('PROFILER_ENABLE')) + { + $x = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ::profiler_completion_titles WHERE `titleId` = %i', $this->typeId); + $y = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ::profiler_profiles WHERE `custom` = 0 AND `stub` = 0'); + $infobox[] = Lang::profiler('attainedBy', [round(($x ?: 0) * 100 / ($y ?: 1))]); + + // completion row added by InfoboxMarkup + } + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($infobox) - $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0'); + $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF, 'dbpage' => true], 'infobox-contents0', 1); /****************/ @@ -99,7 +116,7 @@ class TitleBaseResponse extends TemplateResponse implements ICache ); // factionchange-equivalent - if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = ?d, `alliance_id`, -`horde_id`) FROM player_factionchange_titles WHERE `alliance_id` = ?d OR `horde_id` = ?d', $this->typeId, $this->typeId, $this->typeId)) + if ($pendant = DB::World()->selectCell('SELECT IF(`horde_id` = %i, `alliance_id`, -`horde_id`) FROM player_factionchange_titles WHERE `alliance_id` = %i OR `horde_id` = %i', $this->typeId, $this->typeId, $this->typeId)) { $altTitle = new TitleList(array(['id', abs($pendant)])); if (!$altTitle->error) @@ -120,7 +137,7 @@ class TitleBaseResponse extends TemplateResponse implements ICache $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); - // tab: quest source + // tab: reward-from-quest $quests = new QuestList(array(['rewardTitleId', $this->typeId])); if (!$quests->error) { @@ -135,8 +152,8 @@ class TitleBaseResponse extends TemplateResponse implements ICache ), QuestList::$brickFile)); } - // tab: achievement source - if ($aIds = DB::World()->selectCol('SELECT `ID` FROM achievement_reward WHERE `TitleA` = ?d OR `TitleH` = ?d', $this->typeId, $this->typeId)) + // tab: reward-from-achievement + if ($aIds = DB::World()->selectCol('SELECT `ID` FROM achievement_reward WHERE `TitleA` = %i OR `TitleH` = %i', $this->typeId, $this->typeId)) { $acvs = new AchievementList(array(['id', $aIds])); if (!$acvs->error) @@ -153,8 +170,8 @@ class TitleBaseResponse extends TemplateResponse implements ICache } } - // tab: criteria of - if ($crt = DB::World()->selectCol('SELECT `criteria_id` FROM achievement_criteria_data WHERE `type` = 23 AND `value1` = ?d', $this->typeId)) + // tab: criteria-of + if ($crt = DB::World()->selectCol('SELECT `criteria_id` FROM achievement_criteria_data WHERE `type` = %i AND `value1` = %i', ACHIEVEMENT_CRITERIA_DATA_TYPE_S_KNOWN_TITLE, $this->typeId)) { $acvs = new AchievementList(array(['ac.id', $crt])); if (!$acvs->error) diff --git a/endpoints/titles/titles.php b/endpoints/titles/titles.php index a9b916d6..a6c3d4d5 100644 --- a/endpoints/titles/titles.php +++ b/endpoints/titles/titles.php @@ -11,7 +11,7 @@ class TitlesBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::TITLE; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'titles'; @@ -20,11 +20,11 @@ class TitlesBaseResponse extends TemplateResponse implements ICache protected array $validCats = [0, 1, 2, 3, 4, 5, 6]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -43,7 +43,7 @@ class TitlesBaseResponse extends TemplateResponse implements ICache $this->redButtons[BUTTON_WOWHEAD] = true; - $conditions = []; + $conditions = [Listview::DEFAULT_SIZE]; if (!User::isInGroup(U_GROUP_EMPLOYEE)) // hide unused titles $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; diff --git a/endpoints/tooltips/tooltips.php b/endpoints/tooltips/tooltips.php index d9507e36..7ead16a0 100644 --- a/endpoints/tooltips/tooltips.php +++ b/endpoints/tooltips/tooltips.php @@ -13,11 +13,11 @@ class TooltipsBaseResponse extends TemplateResponse protected ?int $activeTab = parent::TAB_MORE; protected array $breadcrumb = [2, 10]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - if ($pageParam) + if ($rawParam) $this->generateError(); } diff --git a/endpoints/top-users/top-users.php b/endpoints/top-users/top-users.php index b342baa1..464fea0a 100644 --- a/endpoints/top-users/top-users.php +++ b/endpoints/top-users/top-users.php @@ -8,16 +8,18 @@ if (!defined('AOWOW_REVISION')) class TopusersBaseResponse extends TemplateResponse { + private const /* int */ MAX_RESULTS = 500; + protected string $template = 'list-page-generic'; protected string $pageName = 'top-users'; protected ?int $activeTab = parent::TAB_COMMUNITY; protected array $breadcrumb = [3, 11]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - if ($pageParam) + if ($rawParam) $this->generateError(); } @@ -47,25 +49,25 @@ class TopusersBaseResponse extends TemplateResponse foreach ($tabs as [$time, $tabId, $tabName]) { // stuff received - $res = DB::Aowow()->select( + $res = DB::Aowow()->selectAssoc( 'SELECT a.`id` AS ARRAY_KEY, a.`username`, a.`userGroups` AS "groups", a.`joinDate` AS "creation", - SUM(r.`amount`) AS "reputation", SUM(IF(r.`action` = ?d, 1, 0)) AS "comments", SUM(IF(r.`action` = ?d, 1, 0)) AS "screenshots", SUM(IF(r.`action` = ?d, 1, 0)) AS "reports" - FROM ?_account_reputation r - JOIN ?_account a ON a.`id` = r.`userId` - { WHERE r.`date` > ?d } + SUM(r.`amount`) AS "reputation", SUM(IF(r.`action` = %i, 1, 0)) AS "comments", SUM(IF(r.`action` = %i, 1, 0)) AS "screenshots", SUM(IF(r.`action` = %i, 1, 0)) AS "reports" + FROM ::account_reputation r + JOIN ::account a ON a.`id` = r.`userId`', + SITEREP_ACTION_COMMENT, SITEREP_ACTION_SUBMIT_SCREENSHOT, SITEREP_ACTION_GOOD_REPORT, + '%if', $time, 'WHERE r.`date` > %i', $time, '%end GROUP BY a.`id` ORDER BY reputation DESC - LIMIT ?d', - SITEREP_ACTION_COMMENT, SITEREP_ACTION_SUBMIT_SCREENSHOT, SITEREP_ACTION_GOOD_REPORT, - $time ?: DBSIMPLE_SKIP, Cfg::get('SQL_LIMIT_SEARCH') + LIMIT %i', + self::MAX_RESULTS ); $data = []; if ($res) { // stuff given - $votes = DB::Aowow()->selectCol('SELECT `sourceB` AS ARRAY_KEY, SUM(1) FROM ?_account_reputation WHERE `action` IN (?a) AND `sourceB` IN (?a) { AND `date` > ?d } GROUP BY `sourceB`', - [SITEREP_ACTION_UPVOTED, SITEREP_ACTION_DOWNVOTED], array_keys($res), $time ?: DBSIMPLE_SKIP + $votes = DB::Aowow()->selectCol('SELECT `sourceB` AS ARRAY_KEY, SUM(1) FROM ::account_reputation WHERE %if', $time, '`date` > %i AND', $time, '%end `action` IN %in AND `sourceB` IN %in GROUP BY `sourceB`', + [SITEREP_ACTION_UPVOTED, SITEREP_ACTION_DOWNVOTED], array_keys($res), ); foreach ($res as $uId => &$r) { diff --git a/endpoints/unrated-comments/unrated-comments.php b/endpoints/unrated-comments/unrated-comments.php index bd0027e8..2b7fefc5 100644 --- a/endpoints/unrated-comments/unrated-comments.php +++ b/endpoints/unrated-comments/unrated-comments.php @@ -31,7 +31,7 @@ class UnratedcommentsBaseResponse extends TemplateResponse $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"]); - $data = CommunityContent::getCommentPreviews(['unrated' => true, 'comments' => true]); + $data = CommunityContent::getCommentPreviews(['unrated' => true, 'comments' => true], resultLimit: Listview::DEFAULT_SIZE); $this->lvTabs->addListviewTab(new Listview(['data' => $data], 'commentpreview')); parent::generate(); diff --git a/endpoints/upload/image-complete.php b/endpoints/upload/image-complete.php index 10f9ea3b..ab9c111d 100644 --- a/endpoints/upload/image-complete.php +++ b/endpoints/upload/image-complete.php @@ -19,12 +19,12 @@ class UploadImagecompleteResponse extends TextResponse public string $imgHash; public int $newId; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { if (User::isBanned()) $this->generate404(); - parent::__construct($pageParam); + parent::__construct($rawParam); if (!preg_match('/^upload=image-complete&(\d+)\.(\w{16})$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) $this->generate404(); @@ -62,7 +62,7 @@ class UploadImagecompleteResponse extends TextResponse if (!$fSize) return false; - $newId = DB::Aowow()->query('INSERT INTO ?_account_avatars (`id`, `userId`, `name`, `when`, `size`) VALUES (?d, ?d, ?, ?d, ?d)', $this->newId, User::$id, 'Avatar '.$this->newId, time(), $fSize); + $newId = DB::Aowow()->qry('INSERT INTO ::account_avatars (`id`, `userId`, `name`, `when`, `size`) VALUES (%i, %i, %s, %i, %i)', $this->newId, User::$id, 'Avatar '.$this->newId, time(), $fSize); if (!is_int($newId)) { trigger_error('UploadImagecompleteResponse - avatar query failed', E_USER_ERROR); diff --git a/endpoints/upload/image-crop.php b/endpoints/upload/image-crop.php index ba046dd6..cf7f8279 100644 --- a/endpoints/upload/image-crop.php +++ b/endpoints/upload/image-crop.php @@ -23,12 +23,12 @@ class UploadImagecropResponse extends TemplateResponse public int $nextId = 0; public string $imgHash = ''; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { if (User::isBanned()) $this->generateError(); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -67,12 +67,12 @@ class UploadImagecropResponse extends TemplateResponse if (!AvatarMgr::loadUpload()) return Lang::main('intError'); - $n = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_account_avatars WHERE `userId` = ?d', User::$id); + $n = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ::account_avatars WHERE `userId` = %i', User::$id); if ($n && $n > Cfg::get('ACC_MAX_AVATAR_UPLOADS')) return Lang::main('intError'); // why is ++(<IntExpression>); illegal syntax? WHO KNOWS!? - $this->nextId = (DB::Aowow()->selectCell('SELECT MAX(`id`) FROM ?_account_avatars') ?: 0) + 1; + $this->nextId = (DB::Aowow()->selectCell('SELECT MAX(`id`) FROM ::account_avatars') ?: 0) + 1; if (!AvatarMgr::tempSaveUpload(['avatar', $this->nextId], $this->imgHash)) return Lang::main('intError'); diff --git a/endpoints/user/user.php b/endpoints/user/user.php index c89a6cf2..cff2c560 100644 --- a/endpoints/user/user.php +++ b/endpoints/user/user.php @@ -28,21 +28,21 @@ class UserBaseResponse extends TemplateResponse private array $user = []; - public function __construct($pageParam) + public function __construct($rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - if (!$pageParam && User::isLoggedIn()) + if (!$rawParam && User::isLoggedIn()) $this->forward('?user='.User::$username); - if (!$pageParam) + if (!$rawParam) $this->forwardToSignIn('user'); - if ($user = DB::Aowow()->selectRow('SELECT a.`id`, a.`username`, a.`consecutiveVisits`, a.`userGroups`, a.`avatar`, a.`avatarborder`, a.`wowicon`, a.`title`, a.`description`, a.`joinDate`, a.`prevLogin`, IFNULL(SUM(ar.`amount`), 0) AS "sumRep", a.`prevIP`, a.`email` FROM ?_account a LEFT JOIN ?_account_reputation ar ON a.`id` = ar.`userId` WHERE LOWER(a.`username`) = LOWER(?) GROUP BY a.`id`', $pageParam)) + if ($user = DB::Aowow()->selectRow('SELECT a.`id`, a.`username`, a.`consecutiveVisits`, a.`userGroups`, a.`avatar`, a.`avatarborder`, a.`wowicon`, a.`title`, a.`description`, a.`joinDate`, a.`prevLogin`, IFNULL(SUM(ar.`amount`), 0) AS "sumRep", a.`prevIP`, a.`email` FROM ::account a LEFT JOIN ::account_reputation ar ON a.`id` = ar.`userId` WHERE a.`id` <> 0 AND LOWER(a.`username`) = LOWER(%s) GROUP BY a.`id`', $rawParam)) $this->user = $user; else - $this->generateNotFound(Lang::user('notFound', [$pageParam])); - } + $this->generateNotFound(Lang::user('notFound', [Util::htmlEscape($rawParam)])); + } protected function generate() : void { @@ -70,14 +70,16 @@ class UserBaseResponse extends TemplateResponse } if ($this->user['joinDate']) - $infobox[] = Lang::user('joinDate') . '[tooltip name=joinDate]'. date('l, G:i:s', $this->user['joinDate']). '[/tooltip][span class=tip tooltip=joinDate]'. date(Lang::main('dateFmtShort'), $this->user['joinDate']). '[/span]'; + $infobox[] = Lang::user('joinDate') . '[tooltip name=joinDate]'. date('l, G:i:s', $this->user['joinDate']). '[/tooltip][span class=tip tooltip=joinDate]'.(new DateTime())->formatDate($this->user['joinDate']). '[/span]'; if ($this->user['prevLogin']) - $infobox[] = Lang::user('lastLogin') . '[tooltip name=lastLogin]'.date('l, G:i:s', $this->user['prevLogin']).'[/tooltip][span class=tip tooltip=lastLogin]'.date(Lang::main('dateFmtShort'), $this->user['prevLogin']).'[/span]'; + $infobox[] = Lang::user('lastLogin') . '[tooltip name=lastLogin]'.date('l, G:i:s', $this->user['prevLogin']).'[/tooltip][span class=tip tooltip=lastLogin]'.(new DateTime())->formatDate($this->user['prevLogin']).'[/span]'; if ($groups) $infobox[] = Lang::user('userGroups') . implode(', ', $groups); $infobox[] = Lang::user('consecVisits'). $this->user['consecutiveVisits']; - $infobox[] = Lang::main('siteRep') . Lang::nf($this->user['sumRep']); + + if ($this->user['sumRep']) + $infobox[] = Lang::main('siteRep') . Lang::nf($this->user['sumRep']); if ($infobox) $this->infobox = new InfoboxMarkup($infobox, ['allow' => Markup::CLASS_STAFF], 'infobox-contents0'); @@ -111,7 +113,7 @@ class UserBaseResponse extends TemplateResponse $avatarMore = match ((int)$this->user['avatar']) { 1 => $this->user['wowicon'], - 2 => DB::Aowow()->selectCell('SELECT `id` FROM ?_account_avatars WHERE `current` = 1 AND `userId` = ?d', $this->user['id']), + 2 => DB::Aowow()->selectCell('SELECT `id` FROM ::account_avatars WHERE `current` = 1 AND `userId` = %i', $this->user['id']), default => '' }; @@ -145,14 +147,14 @@ class UserBaseResponse extends TemplateResponse // Reputation changelog (params only for comment-events) if (User::$id == $this->user['id'] || User::isInGroup(U_GROUP_MODERATOR)) - if ($repData = DB::Aowow()->select('SELECT `action`, `amount`, `date` AS "when", IF(`action` IN (3, 4, 5), `sourceA`, 0) AS "param" FROM ?_account_reputation WHERE `userId` = ?d', $this->user['id'])) + if ($repData = DB::Aowow()->selectAssoc('SELECT `action`, `amount`, `date` AS "when", IF(`action` IN (3, 4, 5), `sourceA`, 0) AS "param" FROM ::account_reputation WHERE `userId` = %i', $this->user['id'])) { array_walk($repData, fn(&$x) => $x['when'] = date(Util::$dateFormatInternal, $x['when'])); $this->lvTabs->addListviewTab(new Listview(['data' => $repData], 'reputationhistory')); } // Comments - if ($_ = CommunityContent::getCommentPreviews(['user' => $this->user['id'], 'comments' => true], $nFound)) + if ($_ = CommunityContent::getCommentPreviews(['user' => $this->user['id'], 'comments' => true], $nFound, resultLimit: Listview::DEFAULT_SIZE)) { $tabData = array( 'data' => $_, @@ -161,7 +163,7 @@ class UserBaseResponse extends TemplateResponse '_totalCount' => $nFound ); - if ($nFound > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($nFound > Listview::DEFAULT_SIZE) { $tabData['name'] = '$LANG.tab_latestcomments'; $tabData['note'] = '$$WH.sprintf(LANG.lvnote_usercomments, '.$nFound.')'; @@ -171,7 +173,7 @@ class UserBaseResponse extends TemplateResponse } // Comment Replies - if ($_ = CommunityContent::getCommentPreviews(['user' => $this->user['id'], 'replies' => true], $nFound)) + if ($_ = CommunityContent::getCommentPreviews(['user' => $this->user['id'], 'replies' => true], $nFound, resultLimit: Listview::DEFAULT_SIZE)) { $tabData = array( 'data' => $_, @@ -180,7 +182,7 @@ class UserBaseResponse extends TemplateResponse '_totalCount' => $nFound ); - if ($nFound > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($nFound > Listview::DEFAULT_SIZE) { $tabData['name'] = '$LANG.tab_latestreplies'; $tabData['note'] = '$$WH.sprintf(LANG.lvnote_userreplies, '.$nFound.')'; @@ -190,14 +192,14 @@ class UserBaseResponse extends TemplateResponse } // Screenshots - if ($_ = CommunityContent::getScreenshots(-$this->user['id'], 0, $nFound)) + if ($_ = CommunityContent::getScreenshots(-$this->user['id'], 0, $nFound, resultLimit: Listview::DEFAULT_SIZE)) { $tabData = array( 'data' => $_, '_totalCount' => $nFound ); - if ($nFound > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($nFound > Listview::DEFAULT_SIZE) { $tabData['name'] = '$LANG.tab_latestscreenshots'; $tabData['note'] = '$$WH.sprintf(LANG.lvnote_userscreenshots, '.$nFound.')'; @@ -207,14 +209,14 @@ class UserBaseResponse extends TemplateResponse } // Videos - if ($_ = CommunityContent::getVideos(-$this->user['id'], 0, $nFound)) + if ($_ = CommunityContent::getVideos(-$this->user['id'], 0, $nFound, resultLimit: Listview::DEFAULT_SIZE)) { $tabData = array( 'data' => $_, '_totalCount' => $nFound ); - if ($nFound > Cfg::get('SQL_LIMIT_DEFAULT')) + if ($nFound > Listview::DEFAULT_SIZE) { $tabData['name'] = '$LANG.tab_latestvideos'; $tabData['note'] = '$$WH.sprintf(LANG.lvnote_uservideos, '.$nFound.')'; @@ -229,34 +231,48 @@ class UserBaseResponse extends TemplateResponse if (Cfg::get('PROFILER_ENABLE')) { - $conditions = array( - ['OR', ['cuFlags', PROFILER_CU_PUBLISHED, '&'], ['ap.extraFlags', PROFILER_CU_PUBLISHED, '&']], - [['cuFlags', PROFILER_CU_DELETED, '&'], 0], - ['OR', ['user', $this->user['id']], ['ap.accountId', $this->user['id']]] - ); - - if (User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $conditions = array_slice($conditions, 2); - else if (User::$id == $this->user['id']) - array_shift($conditions); + $conditions = [['user', $this->user['id']]]; + if (User::$id != $this->user['id'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $conditions[] = ['cuFlags', PROFILER_CU_PUBLISHED, '&']; + if (!User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $conditions[] = ['deleted', 0]; $profiles = new LocalProfileList($conditions); if (!$profiles->error) { $this->addDataLoader('weight-presets'); - // Characters - if ($chars = $profiles->getListviewData(PROFILEINFO_CHARACTER | PROFILEINFO_USER)) - $this->charactersLvData = array_values($chars); - - // Profiles if ($prof = $profiles->getListviewData(PROFILEINFO_PROFILE | PROFILEINFO_USER)) $this->profilesLvData = array_values($prof); } + + $conditions = [['ap.accountId', $this->user['id']]]; + if (User::$id != $this->user['id'] && !User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $conditions[] = ['ap.extraFlags', PROFILER_CU_PUBLISHED, '&']; + + $characters = new LocalProfileList($conditions); + if (!$characters->error) + { + $this->addDataLoader('weight-presets'); + + if ($chars = $characters->getListviewData(PROFILEINFO_CHARACTER | PROFILEINFO_USER)) + $this->charactersLvData = array_values($chars); + } + + // signatures + /* $this->lvTabs->addListviewTab(new Listview(array( + * 'id' => 'signatures', + * 'name' => '$LANG.tab_signatures', + * 'hiddenCols' => ['name','faction','location','guild'], + * 'extraCols' => ['$Listview.extraCols.signature'], + * 'onBeforeCreate' => '$Listview.funcBox.beforeUserSignatures', + * 'data' => [ ProfileList->getListviewData() ] // no extra signature related data observed + * ), 'profile')); + */ } // My Guides - $guides = new GuideList(['status', [GuideMgr::STATUS_APPROVED, GuideMgr::STATUS_ARCHIVED]], ['userId', $this->user['id']]); + $guides = new GuideList(array(['status', [GuideMgr::STATUS_APPROVED, GuideMgr::STATUS_ARCHIVED]], ['userId', $this->user['id']])); if (!$guides->error) { $this->lvTabs->addListviewTab(new Listview(array( @@ -271,7 +287,7 @@ class UserBaseResponse extends TemplateResponse private function getCommentStats() : ?string { $co = DB::Aowow()->selectRow( - 'SELECT COUNT(DISTINCT c.`id`) AS "0", SUM(IFNULL(ur.`value`, 0)) AS "1" FROM ?_comments c LEFT JOIN ?_user_ratings ur ON ur.`entry` = c.`id` AND ur.`type` = ?d AND ur.`userId` <> 0 WHERE c.`replyTo` = 0 AND c.`userId` = ?d', + 'SELECT COUNT(DISTINCT c.`id`) AS "0", SUM(IFNULL(ur.`value`, 0)) AS "1" FROM ::comments c LEFT JOIN ::user_ratings ur ON ur.`entry` = c.`id` AND ur.`type` = %i AND ur.`userId` <> 0 WHERE c.`replyTo` = 0 AND c.`userId` = %i', RATING_COMMENT, $this->user['id'] ); @@ -280,13 +296,16 @@ class UserBaseResponse extends TemplateResponse [$sum, $nRatings] = $co; + if (!$sum) + return null; + return Lang::user('comments').$sum.($nRatings ? ' [small]([tooltip=tooltip_totalratings]'.$nRatings.'[/tooltip])[/small]' : ''); } private function getScreenshotStats() : ?string { $ss = DB::Aowow()->selectRow( - 'SELECT COUNT(*) AS "0", SUM(IF(`status` & ?d, 1, 0)) AS "1", SUM(IF(`status` & ?d, 0, 1)) AS "2" FROM ?_screenshots WHERE `userIdOwner` = ?d AND (`status` & ?d) = 0', + 'SELECT COUNT(*) AS "0", SUM(IF(`status` & %i, 1, 0)) AS "1", SUM(IF(`status` & %i, 0, 1)) AS "2" FROM ::screenshots WHERE `userIdOwner` = %i AND (`status` & %i) = 0', CC_FLAG_STICKY, CC_FLAG_APPROVED, $this->user['id'], CC_FLAG_DELETED ); @@ -295,6 +314,9 @@ class UserBaseResponse extends TemplateResponse [$sum, $nSticky, $nPending] = $ss; + if (!$sum) + return null; + $buff = []; if ($nSticky || $nPending) { @@ -314,7 +336,7 @@ class UserBaseResponse extends TemplateResponse private function getVideoStats() : ?string { $vi = DB::Aowow()->selectRow( - 'SELECT COUNT(*) AS "0", SUM(IF(`status` & ?d, 1, 0)) AS "1", SUM(IF(`status` & ?d, 0, 1)) AS "2" FROM ?_videos WHERE `userIdOwner` = ?d AND (`status` & ?d) = 0', + 'SELECT COUNT(*) AS "0", SUM(IF(`status` & %i, 1, 0)) AS "1", SUM(IF(`status` & %i, 0, 1)) AS "2" FROM ::videos WHERE `userIdOwner` = %i AND (`status` & %i) = 0', CC_FLAG_STICKY, CC_FLAG_APPROVED, $this->user['id'], CC_FLAG_DELETED ); @@ -323,6 +345,9 @@ class UserBaseResponse extends TemplateResponse [$sum, $nSticky, $nPending] = $vi; + if (!$sum) + return null; + $buff = []; if ($nSticky || $nPending) { @@ -355,6 +380,9 @@ class UserBaseResponse extends TemplateResponse if ($nReplies) $buff[] = '[tooltip=replies]'.$nReplies.'[/tooltip]'; + if (!$buff) + return null; + return Lang::user('posts').($nTopics + $nReplies).($buff ? ' [small]('.implode(' + ', $buff).')[/small]' : ''); } } diff --git a/endpoints/video/add.php b/endpoints/video/add.php index 36c67ebc..9459e9a9 100644 --- a/endpoints/video/add.php +++ b/endpoints/video/add.php @@ -27,9 +27,9 @@ class VideoAddResponse extends TextResponse private int $destType = 0; private int $destTypeId = 0; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); // get video destination // target delivered as video=<command>&<type>.<typeId>.<hash:16> (hash is optional) diff --git a/endpoints/video/complete.php b/endpoints/video/complete.php index 46cf9d0b..3bb554aa 100644 --- a/endpoints/video/complete.php +++ b/endpoints/video/complete.php @@ -27,9 +27,9 @@ class VideoCompleteResponse extends TextResponse private int $destType = 0; private int $destTypeId = 0; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); // get video destination // target delivered as video=<command>&<type>.<typeId>.<hash:16> (hash is optional) @@ -60,13 +60,13 @@ class VideoCompleteResponse extends TextResponse if (!VideoMgr::loadSuggestion($videoInfo, $this->destType, $this->destTypeId, $this->videoHash)) $this->generate404(); - $pos = DB::Aowow()->selectCell('SELECT MAX(`pos`) FROM ?_videos WHERE `type` = ?d AND `typeId` = ?d AND (`status` & ?d) = 0', $this->destType, $this->destTypeId, CC_FLAG_DELETED); + $pos = DB::Aowow()->selectCell('SELECT MAX(`pos`) FROM ::videos WHERE `type` = %i AND `typeId` = %i AND (`status` & %i) = 0', $this->destType, $this->destTypeId, CC_FLAG_DELETED); if (!is_int($pos)) $pos = -1; // write to db - $newId = DB::Aowow()->query( - 'INSERT INTO ?_videos (`type`, `typeId`, `userIdOwner`, `date`, `videoId`, `pos`, `url`, `width`, `height`, `name`, `caption`, `status`) VALUES (?d, ?d, ?d, UNIX_TIMESTAMP(), ?, ?d, ?, ?d, ?d, ?, ?, 0)', + $newId = DB::Aowow()->qry( + 'INSERT INTO ::videos (`type`, `typeId`, `userIdOwner`, `date`, `videoId`, `pos`, `url`, `width`, `height`, `name`, `caption`, `status`) VALUES (%i, %i, %i, UNIX_TIMESTAMP(), %s, %i, %s, %i, %i, %s, %s, 0)', $this->destType, $this->destTypeId, User::$id, $videoInfo->id, $pos + 1, diff --git a/endpoints/video/confirm.php b/endpoints/video/confirm.php index 055670ef..46ed385a 100644 --- a/endpoints/video/confirm.php +++ b/endpoints/video/confirm.php @@ -32,9 +32,9 @@ class VideoConfirmResponse extends TemplateResponse public array $video = []; public string $viTitle = ''; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); // get video destination // target delivered as video=<command>&<type>.<typeId>.<hash:16> (hash is optional) diff --git a/endpoints/video/thankyou.php b/endpoints/video/thankyou.php index c90e7922..f07822ee 100644 --- a/endpoints/video/thankyou.php +++ b/endpoints/video/thankyou.php @@ -23,9 +23,9 @@ class VideoThankyouResponse extends TemplateResponse private int $destType = 0; private int $destTypeId = 0; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); // get video destination // target delivered as video=<command>&<type>.<typeId> diff --git a/endpoints/whats-new/whats-new.php b/endpoints/whats-new/whats-new.php index 080a69c4..63124dfe 100644 --- a/endpoints/whats-new/whats-new.php +++ b/endpoints/whats-new/whats-new.php @@ -13,11 +13,11 @@ class WhatsnewBaseResponse extends TemplateResponse protected ?int $activeTab = parent::TAB_MORE; protected array $breadcrumb = [2, 7]; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - parent::__construct($pageParam); + parent::__construct($rawParam); - if ($pageParam) + if ($rawParam) $this->generateError(); } diff --git a/endpoints/zone/zone.php b/endpoints/zone/zone.php index 548b4a0d..3e54eb4f 100644 --- a/endpoints/zone/zone.php +++ b/endpoints/zone/zone.php @@ -10,7 +10,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache { use TrDetailPage, TrCache; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_DETAIL_PAGE; protected string $template = 'detail-page-generic'; protected string $pageName = 'zone'; @@ -74,7 +74,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache /* Infobox */ /***********/ - $quickFactsRows = DB::Aowow()->selectCol('SELECT `orderIdx` AS ARRAY_KEY, `row` FROM ?_quickfacts WHERE `type` = ?d AND `typeId` = ?d ORDER BY `orderIdx` ASC', $this->type, $this->typeId); + $quickFactsRows = DB::Aowow()->selectCol('SELECT `orderIdx` AS ARRAY_KEY, `row` FROM ::quickfacts WHERE `type` = %i AND `typeId` = %i ORDER BY `orderIdx` ASC', $this->type, $this->typeId); $quickFactsRows = preg_replace_callback('/\|L:(\w+)((:\w+)+)\|/i', function ($m) { [, $grp, $args] = $m; @@ -148,14 +148,14 @@ class ZoneBaseResponse extends TemplateResponse implements ICache } // Instances - if ($_ = DB::Aowow()->selectCol('SELECT `typeId` FROM ?_spawns WHERE `type`= ?d AND `areaId` = ?d ', Type::ZONE, $this->typeId)) + if ($_ = DB::Aowow()->selectCol('SELECT `typeId` FROM ::spawns WHERE `type`= %i AND `areaId` = %i ', Type::ZONE, $this->typeId)) { $this->extendGlobalIds(Type::ZONE, ...$_); $infobox[] = Lang::maps('Instances').Lang::main('colon').Lang::concat($_, Lang::CONCAT_NONE, fn($x) => "\n[zone=".$x."]"); } // start area - if ($_ = DB::Aowow()->selectCol('SELECT `id` FROM ?_races WHERE `startAreaId` = ?d', $this->typeId)) + if ($_ = DB::Aowow()->selectCol('SELECT `id` FROM ::races WHERE `startAreaId` = %i', $this->typeId)) { $this->extendGlobalIds(Type::CHR_RACE, ...$_); $infobox[] = Lang::concat($_, Lang::CONCAT_NONE, fn($x) => '[race='.$x.']').' '.Lang::race('startZone'); @@ -164,7 +164,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache parent::generate(); // calls applyGlobals .. probably too early here, but addMoveLocationMenu requires PageTemplate to be initialized // location (if instance) - if ($pa = DB::Aowow()->selectRow('SELECT `areaId`, `posX`, `posY`, `floor` FROM ?_spawns WHERE `type`= ?d AND `typeId` = ?d ', Type::ZONE, $this->typeId)) + if ($pa = DB::Aowow()->selectRow('SELECT `areaId`, `posX`, `posY`, `floor` FROM ::spawns WHERE `type`= %i AND `typeId` = %i ', Type::ZONE, $this->typeId)) { $this->addMoveLocationMenu($pa['areaId'], $pa['floor']); @@ -188,6 +188,13 @@ class ZoneBaseResponse extends TemplateResponse implements ICache } } + // id + $infobox[] = Lang::zone('id') . $this->typeId; + + // original name + if (Lang::getLocale() != Locale::EN) + $infobox[] = Util::ucFirst(Lang::lang(Locale::EN->value) . Lang::main('colon')) . '[copy button=false]'.$this->subject->getField('name_loc0').'[/copy][/li]'; + if ($botRows = array_filter($quickFactsRows, fn($x) => $x > 0, ARRAY_FILTER_USE_KEY)) $infobox = array_merge($infobox, $botRows); @@ -199,15 +206,15 @@ class ZoneBaseResponse extends TemplateResponse implements ICache /* Main Content */ /****************/ - $addToSOM = function (string $what, array $entry) use (&$som) : void + $addToSOM = function (string $what, string $group, array $entry) use (&$som) : void { // entry always contains: type, id, name, level, coords[] - if (!isset($som[$what][$entry['name']])) // not found yet - $som[$what][$entry['id']][] = $entry; + if (!isset($som[$what][$group])) // not found yet + $som[$what][$group][] = $entry; else // found .. something.. { // check for identical floors - foreach ($som[$what][$entry['id']] as &$byFloor) + foreach ($som[$what][$group] as &$byFloor) { if ($byFloor['level'] != $entry['level']) continue; @@ -218,7 +225,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache } // floor not used yet, create it - $som[$what][$entry['id']][] = $entry; + $som[$what][$group][] = $entry; } }; @@ -229,11 +236,11 @@ class ZoneBaseResponse extends TemplateResponse implements ICache } // we cannot fetch spawns via lists. lists are grouped by entry - $oSpawns = DB::Aowow()->select('SELECT * FROM ?_spawns WHERE `areaId` = ?d AND `type` = ?d AND `posX` > 0 AND `posY` > 0', $this->typeId, Type::OBJECT); - $cSpawns = DB::Aowow()->select('SELECT * FROM ?_spawns WHERE `areaId` = ?d AND `type` = ?d AND `posX` > 0 AND `posY` > 0', $this->typeId, Type::NPC); - $aSpawns = User::isInGroup(U_GROUP_STAFF) ? DB::Aowow()->select('SELECT * FROM ?_spawns WHERE `areaId` = ?d AND `type` = ?d AND `posX` > 0 AND `posY` > 0', $this->typeId, Type::AREATRIGGER) : []; + $oSpawns = DB::Aowow()->selectAssoc('SELECT * FROM ::spawns WHERE `areaId` = %i AND `type` = %i AND `posX` > 0 AND `posY` > 0', $this->typeId, Type::OBJECT); + $cSpawns = DB::Aowow()->selectAssoc('SELECT * FROM ::spawns WHERE `areaId` = %i AND `type` = %i AND `posX` > 0 AND `posY` > 0', $this->typeId, Type::NPC); + $aSpawns = User::isInGroup(U_GROUP_STAFF) ? DB::Aowow()->selectAssoc('SELECT * FROM ::spawns WHERE `areaId` = %i AND `type` = %i AND `posX` > 0 AND `posY` > 0', $this->typeId, Type::AREATRIGGER) : []; - $conditions = [Cfg::get('SQL_LIMIT_NONE'), ['s.areaId', $this->typeId]]; + $conditions = [['s.areaId', $this->typeId]]; if (!User::isInGroup(U_GROUP_STAFF)) $conditions[] = [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0]; @@ -303,9 +310,12 @@ class ZoneBaseResponse extends TemplateResponse implements ICache ); if ($what == 'mail') - $blob['side'] = (($tpl['A'] < 0 ? 0 : 0x1) | ($tpl['H'] < 0 ? 0 : 0x2)); - - $addToSOM($what, $blob); + { + $blob['side'] = (($tpl['A'] < 0 ? 0 : SIDE_ALLIANCE) | ($tpl['H'] < 0 ? 0 : SIDE_HORDE)); + $addToSOM($what, $tpl['id'], $blob); + } + else + $addToSOM($what, $n, $blob); } if ($tpl['startsQuests']) @@ -317,7 +327,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache // store data for misc tabs foreach ($started->getListviewData() as $id => $data) { - if ($started->getField('zoneOrSort') > 0 && !in_array($started->getField('zoneOrSort'), $relQuestZOS)) + if ($started->getField('questSortId') > 0 && !in_array($started->getField('questSortId'), $relQuestZOS)) continue; if (!empty($started->rewards[$id][Type::ITEM])) @@ -332,24 +342,24 @@ class ZoneBaseResponse extends TemplateResponse implements ICache $this->extendGlobalData($started->getJSGlobals()); if (($tpl['A'] != -1) && ($_ = $started->getSOMData(SIDE_ALLIANCE))) - $addToSOM('alliancequests', array( + $addToSOM('alliancequests', $n, array( 'coords' => [[$spawn['posX'], $spawn['posY']]], 'level' => $spawn['floor'], 'name' => $n, 'type' => Type::OBJECT, 'id' => $tpl['id'], - 'side' => (($tpl['A'] < 0 ? 0 : 0x1) | ($tpl['H'] < 0 ? 0 : 0x2)), + 'side' => (($tpl['A'] < 0 ? 0 : SIDE_ALLIANCE) | ($tpl['H'] < 0 ? 0 : SIDE_HORDE)), 'quests' => array_values($_) )); if (($tpl['H'] != -1) && ($_ = $started->getSOMData(SIDE_HORDE))) - $addToSOM('hordequests', array( + $addToSOM('hordequests', $n, array( 'coords' => [[$spawn['posX'], $spawn['posY']]], 'level' => $spawn['floor'], 'name' => $n, 'type' => Type::OBJECT, 'id' => $tpl['id'], - 'side' => (($tpl['A'] < 0 ? 0 : 0x1) | ($tpl['H'] < 0 ? 0 : 0x2)), + 'side' => (($tpl['A'] < 0 ? 0 : SIDE_ALLIANCE) | ($tpl['H'] < 0 ? 0 : SIDE_HORDE)), 'quests' => array_values($_) )); } @@ -383,7 +393,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache if ($creatureSpawns->isBoss()) $what = 'boss'; - else if ($tpl['rank'] == 2 || $tpl['rank'] == 4) + else if ($tpl['rank'] == NPC_RANK_RARE_ELITE || $tpl['rank'] == NPC_RANK_RARE) $what = 'rare'; else foreach ($flagsMap as $flag => $what) @@ -394,7 +404,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache $flightNodes[$tpl['id']] = [$spawn['posX'], $spawn['posY']]; if ($what) - $addToSOM($what, array( + $addToSOM($what, $n, array( 'coords' => [[$spawn['posX'], $spawn['posY']]], 'level' => $spawn['floor'], 'name' => $n, @@ -414,7 +424,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache // store data for misc tabs foreach ($started->getListviewData() as $id => $data) { - if ($started->getField('zoneOrSort') > 0 && !in_array($started->getField('zoneOrSort'), $relQuestZOS)) + if ($started->getField('questSortId') > 0 && !in_array($started->getField('questSortId'), $relQuestZOS)) continue; if (!empty($started->rewards[$id][Type::ITEM])) @@ -429,7 +439,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache $this->extendGlobalData($started->getJSGlobals()); if (($tpl['A'] != -1) && ($_ = $started->getSOMData(SIDE_ALLIANCE))) - $addToSOM('alliancequests', array( + $addToSOM('alliancequests', $n, array( 'coords' => [[$spawn['posX'], $spawn['posY']]], 'level' => $spawn['floor'], 'name' => $n, @@ -442,7 +452,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache )); if (($tpl['H'] != -1) && ($_ = $started->getSOMData(SIDE_HORDE))) - $addToSOM('hordequests', array( + $addToSOM('hordequests', $n, array( 'coords' => [[$spawn['posX'], $spawn['posY']]], 'level' => $spawn['floor'], 'name' => $n, @@ -465,10 +475,11 @@ class ZoneBaseResponse extends TemplateResponse implements ICache if (!$tpl) continue; - $addToSOM('areatrigger', array( + $n = Util::localizedString($tpl, 'name', true, true); + $addToSOM('areatrigger', $n, array( 'coords' => [[$spawn['posX'], $spawn['posY']]], 'level' => $spawn['floor'], - 'name' => Util::localizedString($tpl, 'name', true, true), + 'name' => $n, 'type' => Type::AREATRIGGER, 'id' => $spawn['typeId'], 'description' => Lang::game('type').Lang::areatrigger('types', $tpl['type']) @@ -508,7 +519,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache return $n1 <=> $n2; }); - $paths = DB::Aowow()->select('SELECT n1.`typeId` AS "0", n2.`typeId` AS "1" FROM ?_taxipath p JOIN ?_taxinodes n1 ON n1.`id` = p.`startNodeId` JOIN ?_taxinodes n2 ON n2.`id` = p.`endNodeId` WHERE n1.`typeId` IN (?a) AND n2.`typeId` IN (?a)', array_keys($flightNodes), array_keys($flightNodes)); + $paths = DB::Aowow()->selectAssoc('SELECT n1.`typeId` AS "0", n2.`typeId` AS "1" FROM ::taxipath p JOIN ::taxinodes n1 ON n1.`id` = p.`startNodeId` JOIN ::taxinodes n2 ON n2.`id` = p.`endNodeId` WHERE n1.`typeId` IN %in AND n2.`typeId` IN %in', array_keys($flightNodes), array_keys($flightNodes)); foreach ($paths as $k => $path) { @@ -558,15 +569,57 @@ class ZoneBaseResponse extends TemplateResponse implements ICache $this->lvTabs = new Tabs(['parent' => "\$\$WH.ge('tabs-generic')"], 'tabsRelated', true); - // tab: NPCs - if ($cSpawns && !$creatureSpawns->error) + // tab: drops + if (in_array($this->subject->getField('category'), [MAP_TYPE_DUNGEON, MAP_TYPE_RAID])) { + // Issue 1 - if the bosses drop items that are also sold by vendors moreZoneId will be 0 as vendor location and boss location are likely in conflict with each other + // Issue 2 - if the boss/chest isn't spawned the loot will not show up + $items = new ItemList(array(['src.moreZoneId', $this->typeId], ['src.src2', 0, '>'], ['quality', ITEM_QUALITY_UNCOMMON, '>=']), ['calcTotal' => true]); + $data = $items->getListviewData(); + $subTabs = false; + foreach ($items->iterate() as $id => $__) + { + $src = $items->getRawSource(SRC_DROP); + $map = ($items->getField('moreMask') ?: 0) & (SRC_FLAG_DUNGEON_DROP | SRC_FLAG_RAID_DROP); + if (!$src || !$map) + continue; + + $subTabs = true; + + if ($map & SRC_FLAG_RAID_DROP) + $mode = ($src[0] << 3); + else + $mode = ($src[0] & 0x1 ? 0x2 : 0) | ($src[0] & 0x2 ? 0x1 : 0); + + $data[$id] += ['modes' => ['mode' => $mode]]; + } + $tabData = array( - 'data' => $creatureSpawns->getListviewData(), - 'note' => sprintf(Util::$filterResultString, '?npcs&filter=cr=6;crs='.$this->typeId.';crv=0') + 'data' => $data, + 'id' => 'drops', + 'name' => '$LANG.tab_drops', + 'extraCols' => $subTabs ? ['$Listview.extraCols.mode'] : null, + 'computeDataFunc' => '$Listview.funcBox.initLootTable', + 'onAfterCreate' => $subTabs ? '$Listview.funcBox.addModeIndicator' : null ); - if ($creatureSpawns->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if (!is_null(ItemListFilter::getCriteriaIndex(16, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?items&filter=cr=16;crs='.$this->typeId.';crv=0'); + + $this->extendGlobalData($items->getJSGlobals(GLOBALINFO_SELF)); + + $this->lvTabs->addListviewTab(new Listview($tabData, ItemList::$brickFile)); + } + + // tab: npcs + if ($cSpawns && !$creatureSpawns->error) + { + $tabData = ['data' => $creatureSpawns->getListviewData()]; + + if (!is_null(CreatureListFilter::getCriteriaIndex(6, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?npcs&filter=cr=6;crs='.$this->typeId.';crv=0'); + + if ($creatureSpawns->getMatches() > Listview::DEFAULT_SIZE) $tabData['_truncated'] = 1; $this->extendGlobalData($creatureSpawns->getJSGlobals(GLOBALINFO_SELF)); @@ -574,15 +627,15 @@ class ZoneBaseResponse extends TemplateResponse implements ICache $this->lvTabs->addListviewTab(new Listview($tabData, CreatureList::$brickFile)); } - // tab: Objects + // tab: objects if ($oSpawns && !$objectSpawns->error) { - $tabData = array( - 'data' => $objectSpawns->getListviewData(), - 'note' => sprintf(Util::$filterResultString, '?objects&filter=cr=1;crs='.$this->typeId.';crv=0') - ); + $tabData = ['data' => $objectSpawns->getListviewData()]; - if ($objectSpawns->getMatches() > Cfg::get('SQL_LIMIT_DEFAULT')) + if (!is_null(GameObjectListFilter::getCriteriaIndex(1, $this->typeId))) + $tabData['note'] = sprintf(Util::$filterResultString, '?objects&filter=cr=1;crs='.$this->typeId.';crv=0'); + + if ($objectSpawns->getMatches() > Listview::DEFAULT_SIZE) $tabData['_truncated'] = 1; $this->extendGlobalData($objectSpawns->getJSGlobals(GLOBALINFO_SELF)); @@ -590,7 +643,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache $this->lvTabs->addListviewTab(new Listview($tabData, GameObjectList::$brickFile)); } - $quests = new QuestList(array(['zoneOrSort', $this->typeId])); + $quests = new QuestList(array(['questSortId', $this->typeId])); if (!$quests->error) { $this->extendGlobalData($quests->getJSGlobals()); @@ -606,7 +659,7 @@ class ZoneBaseResponse extends TemplateResponse implements ICache } } - // tab: Quests [including data collected by SOM-routine] + // tab: quests [including data collected by SOM-routine] if ($questsLV) { $tabData = ['data' => $questsLV]; @@ -616,19 +669,22 @@ class ZoneBaseResponse extends TemplateResponse implements ICache if (!in_array($this->typeId, $children)) continue; - $tabData['note'] = '$$WH.sprintf(LANG.lvnote_zonequests, '.$parent.', '.$this->typeId.',"'.$this->subject->getField('name', true).'", '.$this->typeId.')'; + if (!is_null(ItemListFilter::getCriteriaIndex(126, $this->typeId))) + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_zonequests, '.$parent.', '.$this->typeId.',"'.$this->subject->getField('name', true).'", '.$this->typeId.')'; + else + $tabData['note'] = '$$WH.sprintf(LANG.lvnote_questsind, '.$parent.', '.$this->typeId.',"'.$this->subject->getField('name', true).'")'; break; } $this->lvTabs->addListviewTab(new Listview($tabData, QuestList::$brickFile)); } - // tab: item-quest starter + // tab: starts-quest // select every quest starter, that is a drop - $questStartItem = DB::Aowow()->select( + $questStartItem = DB::Aowow()->selectAssoc( 'SELECT qse.`typeId` AS ARRAY_KEY, `moreType`, `moreTypeId`, `moreZoneId` - FROM ?_quests_startend qse JOIN ?_source src ON src.`type` = qse.`type` AND src.`typeId` = qse.`typeId` - WHERE src.`src2` IS NOT NULL AND qse.`type` = ?d AND (`moreZoneId` = ?d OR (`moreType` = ?d AND `moreTypeId` IN (?a)) OR (`moreType` = ?d AND `moreTypeId` IN (?a)))', + FROM ::quests_startend qse JOIN ::source src ON src.`type` = qse.`type` AND src.`typeId` = qse.`typeId` + WHERE src.`src2` IS NOT NULL AND qse.`type` = %i AND (`moreZoneId` = %i OR (`moreType` = %i AND `moreTypeId` IN %in) OR (`moreType` = %i AND `moreTypeId` IN %in))', Type::ITEM, $this->typeId, Type::NPC, array_unique(array_column($cSpawns, 'typeId')) ?: [0], Type::OBJECT, array_unique(array_column($oSpawns, 'typeId')) ?: [0] @@ -649,17 +705,21 @@ class ZoneBaseResponse extends TemplateResponse implements ICache } } - // tab: Quest Rewards [ids collected by SOM-routine] + // tab: quest-rewards [ids collected by SOM-routine] if ($rewardsLV) { $rewards = new ItemList(array(['id', array_unique($rewardsLV)])); if (!$rewards->error) { + $note = null; + if (!is_null(ItemListFilter::getCriteriaIndex(126, $this->typeId))) + $note = sprintf(Util::$filterResultString, '?items&filter=cr=126;crs='.$this->typeId.';crv=0'); + $this->lvTabs->addListviewTab(new Listview(array( 'data' => $rewards->getListviewData(), 'name' => '$LANG.tab_questrewards', 'id' => 'quest-rewards', - 'note' => sprintf(Util::$filterResultString, '?items&filter=cr=126;crs='.$this->typeId.';crv=0') + 'note' => $note ), ItemList::$brickFile)); $this->extendGlobalData($rewards->getJSGlobals(GLOBALINFO_SELF)); @@ -668,31 +728,72 @@ class ZoneBaseResponse extends TemplateResponse implements ICache // tab: achievements - // tab: fished in zone - $fish = new Loot(); - if ($fish->getByContainer(LOOT_FISHING, $this->typeId)) + // tab: criteria-of + $conditions = array(DB::OR, + array( + DB::AND, + ['ac.type', [ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_QUESTS_IN_ZONE, ACHIEVEMENT_CRITERIA_TYPE_HONORABLE_KILL_AT_AREA]], + ['ac.value1', $this->typeId] + ) + ); + + if ($extraCrt = DB::World()->selectCol('SELECT `criteria_id` FROM achievement_criteria_data WHERE `type` = %i AND `value1` = %i', ACHIEVEMENT_CRITERIA_DATA_TYPE_S_AREA, $this->typeId)) + $conditions[] = ['ac.id', $extraCrt]; + + if ($this->subject->getField('category') != MAP_TYPE_ZONE) + { + $conditions[] = array ( + DB::AND, + ['ac.type', [ACHIEVEMENT_CRITERIA_TYPE_WIN_BG, ACHIEVEMENT_CRITERIA_TYPE_WIN_ARENA, + ACHIEVEMENT_CRITERIA_TYPE_PLAY_ARENA, ACHIEVEMENT_CRITERIA_TYPE_COMPLETE_BATTLEGROUND, + ACHIEVEMENT_CRITERIA_TYPE_DEATH_AT_MAP] + ], + ['ac.value1', $this->subject->getField('mapId')] + ); + + if ($extraCrt = DB::World()->selectCol('SELECT `criteria_id` FROM achievement_criteria_data WHERE `type` = %i AND `value1` = %i', ACHIEVEMENT_CRITERIA_DATA_TYPE_MAP_ID, $this->subject->getField('mapId'))) + $conditions[] = ['ac.id', $extraCrt]; + + } + + $crtOf = new AchievementList($conditions); + if (!$crtOf->error) + { + $this->extendGlobalData($crtOf->getJSGlobals()); + + $this->lvTabs->addListviewTab(new Listview(array( + 'data' => $crtOf->getListviewData(), + 'name' => '$LANG.tab_criteriaof', + 'id' => 'criteria-of' + ), AchievementList::$brickFile)); + } + + // tab: fishing + $fish = new LootByContainer(); + if ($fish->getByContainer(Loot::FISHING, [$this->typeId])) { $this->extendGlobalData($fish->jsGlobals); $xCols = array_merge(['$Listview.extraCols.percent'], $fish->extraCols); $note = null; - if ($skill = DB::World()->selectCell('SELECT `skill` FROM skill_fishing_base_level WHERE `entry` = ?d', $this->typeId)) + if ($skill = DB::World()->selectCell('SELECT `skill` FROM skill_fishing_base_level WHERE `entry` = %i', $this->typeId)) $note = sprintf(Util::$lvTabNoteString, Lang::zone('fishingSkill'), Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_FISHING, $skill), Lang::FMT_HTML)); - else if ($_parentArea && ($skill = DB::World()->selectCell('SELECT `skill` FROM skill_fishing_base_level WHERE `entry` = ?d', $_parentArea))) + else if ($_parentArea && ($skill = DB::World()->selectCell('SELECT `skill` FROM skill_fishing_base_level WHERE `entry` = %i', $_parentArea))) $note = sprintf(Util::$lvTabNoteString, Lang::zone('fishingSkill'), Lang::formatSkillBreakpoints(Game::getBreakpointsForSkill(SKILL_FISHING, $skill), Lang::FMT_HTML)); $this->lvTabs->addListviewTab(new Listview(array( - 'data' => $fish->getResult(), - 'name' => '$LANG.tab_fishing', - 'id' => 'fishing', - 'extraCols' => array_unique($xCols), - 'hiddenCols' => ['side'], - 'note' => $note + 'data' => $fish->getResult(), + 'name' => '$LANG.tab_fishing', + 'id' => 'fishing', + 'extraCols' => array_unique($xCols), + 'hiddenCols' => ['side'], + 'note' => $note, + 'computeDataFunc' => '$Listview.funcBox.initLootTable' ), ItemList::$brickFile)); } // tab: spells - if ($saData = DB::World()->select('SELECT * FROM spell_area WHERE `area` = ?d', $this->typeId)) + if ($saData = DB::World()->selectAssoc('SELECT * FROM spell_area WHERE `area` = %i', $this->typeId)) { $spells = new SpellList(array(['id', array_column($saData, 'spell')])); if (!$spells->error) @@ -755,18 +856,18 @@ class ZoneBaseResponse extends TemplateResponse implements ICache $areaIds[] = $this->typeId; $soundIds = []; - $zoneMusic = DB::Aowow()->select( + $zoneMusic = DB::Aowow()->selectAssoc( 'SELECT x.`soundId` AS ARRAY_KEY, x.`soundId`, x.`worldStateId`, x.`worldStateValue`, x.`type` - FROM (SELECT `ambienceDay` AS "soundId", `worldStateId`, `worldStateValue`, 1 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `ambienceDay` > 0 UNION - SELECT `ambienceNight` AS "soundId", `worldStateId`, `worldStateValue`, 1 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `ambienceNight` > 0 UNION - SELECT `musicDay` AS "soundId", `worldStateId`, `worldStateValue`, 2 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `musicDay` > 0 UNION - SELECT `musicNight` AS "soundId", `worldStateId`, `worldStateValue`, 2 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `musicNight` > 0 UNION - SELECT `intro` AS "soundId", `worldStateId`, `worldStateValue`, 3 AS "type" FROM ?_zones_sounds WHERE `id` IN (?a) AND `intro` > 0) x + FROM (SELECT `ambienceDay` AS "soundId", `worldStateId`, `worldStateValue`, 1 AS "type" FROM ::zones_sounds WHERE `id` IN %in AND `ambienceDay` > 0 UNION + SELECT `ambienceNight` AS "soundId", `worldStateId`, `worldStateValue`, 1 AS "type" FROM ::zones_sounds WHERE `id` IN %in AND `ambienceNight` > 0 UNION + SELECT `musicDay` AS "soundId", `worldStateId`, `worldStateValue`, 2 AS "type" FROM ::zones_sounds WHERE `id` IN %in AND `musicDay` > 0 UNION + SELECT `musicNight` AS "soundId", `worldStateId`, `worldStateValue`, 2 AS "type" FROM ::zones_sounds WHERE `id` IN %in AND `musicNight` > 0 UNION + SELECT `intro` AS "soundId", `worldStateId`, `worldStateValue`, 3 AS "type" FROM ::zones_sounds WHERE `id` IN %in AND `intro` > 0) x GROUP BY x.soundId, x.worldStateId, x.worldStateValue', $areaIds, $areaIds, $areaIds, $areaIds, $areaIds ); - if ($sSpawns = DB::Aowow()->selectCol('SELECT `typeId` FROM ?_spawns WHERE `areaId` = ?d AND `type` = ?d', $this->typeId, Type::SOUND)) + if ($sSpawns = DB::Aowow()->selectCol('SELECT `typeId` FROM ::spawns WHERE `areaId` = %i AND `type` = %i', $this->typeId, Type::SOUND)) $soundIds = array_merge($soundIds, $sSpawns); if ($zoneMusic) diff --git a/endpoints/zones/zones.php b/endpoints/zones/zones.php index 77247254..0f58fd4a 100644 --- a/endpoints/zones/zones.php +++ b/endpoints/zones/zones.php @@ -11,7 +11,7 @@ class ZonesBaseResponse extends TemplateResponse implements ICache use TrListPage, TrCache; protected int $type = Type::ZONE; - protected int $cacheType = CACHE_TYPE_PAGE; + protected int $cacheType = CACHE_TYPE_LIST_PAGE; protected string $template = 'list-page-generic'; protected string $pageName = 'zones'; @@ -23,11 +23,11 @@ class ZonesBaseResponse extends TemplateResponse implements ICache public ?array $map = null; - public function __construct(string $pageParam) + public function __construct(string $rawParam) { - $this->getCategoryFromUrl($pageParam); + $this->getCategoryFromUrl($rawParam); - parent::__construct($pageParam); + parent::__construct($rawParam); } protected function generate() : void @@ -60,7 +60,7 @@ class ZonesBaseResponse extends TemplateResponse implements ICache $this->redButtons[BUTTON_WOWHEAD] = true; - $conditions = [Cfg::get('SQL_LIMIT_NONE')]; + $conditions = []; // do not limit $visibleCols = []; $hiddenCols = []; @@ -117,15 +117,15 @@ class ZonesBaseResponse extends TemplateResponse implements ICache if ($mapFile) { $somData = ['flightmaster' => []]; - $nodes = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, tn.* FROM ?_taxinodes tn WHERE `mapId` = ?d AND `type` <> 0 AND `typeId` <> 0', $spawnMap); - $paths = DB::Aowow()->select( + $nodes = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, tn.* FROM ::taxinodes tn WHERE `mapId` = %i AND `type` <> 0 AND `typeId` <> 0', $spawnMap); + $paths = DB::Aowow()->selectAssoc( 'SELECT IF(tn1.`reactA` = tn1.`reactH` AND tn2.`reactA` = tn2.`reactH`, 1, 0) AS "neutral", - tp.`startNodeId` AS "startId", tn1.`posX` AS "startPosX", tn1.`posY` AS "startPosY", - tp.`endNodeId` AS "endId", tn2.`posX` AS "endPosX", tn2.`posY` AS "endPosY" - FROM ?_taxipath tp, ?_taxinodes tn1, ?_taxinodes tn2 + tp.`startNodeId` AS "startId", tn1.`mapX` AS "startPosX", tn1.`mapY` AS "startPosY", + tp.`endNodeId` AS "endId", tn2.`mapX` AS "endPosX", tn2.`mapY` AS "endPosY" + FROM ::taxipath tp, ::taxinodes tn1, ::taxinodes tn2 WHERE tn1.`Id` = tp.`endNodeId` AND tn2.`Id` = tp.`startNodeId` AND tn1.`type` <> 0 AND tn2.`type` <> 0 AND - (tp.`startNodeId` IN (?a) OR tp.`EndNodeId` IN (?a))', + (tp.`startNodeId` IN %in OR tp.`EndNodeId` IN %in)', array_keys($nodes), array_keys($nodes) ); @@ -134,7 +134,7 @@ class ZonesBaseResponse extends TemplateResponse implements ICache $neutral = $n['reactH'] == $n['reactA']; $data = array( - 'coords' => [[$n['posX'], $n['posY']]], + 'coords' => [[$n['mapX'], $n['mapY']]], 'level' => 0, // floor 'name' => Util::localizedString($n, 'name'), 'type' => $n['type'], diff --git a/includes/cfg.class.php b/includes/cfg.class.php index b82cb1ad..56b287c8 100644 --- a/includes/cfg.class.php +++ b/includes/cfg.class.php @@ -55,7 +55,7 @@ class Cfg 'profiler_enable' => ['realms', 'realmMenu'], 'battlegroup' => ['realms', 'realmMenu'], 'name_short' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo'], - 'site_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo', 'power'], + 'site_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'demo', 'power', 'robots'], 'static_host' => ['searchplugin', 'searchboxBody', 'searchboxScript', 'power'], 'contact_email' => ['globaljs'], 'locales' => ['globaljs'] @@ -63,10 +63,10 @@ class Cfg public static function load() : void { - if (!DB::isConnectable(DB_AOWOW)) + if (!DB::isConnected(DB_AOWOW)) return; - $sets = DB::Aowow()->select('SELECT `key` AS ARRAY_KEY, `value` AS "0", `flags` AS "1", `cat` AS "2", `default` AS "3", `comment` AS "4" FROM ?_config ORDER BY `key` ASC'); + $sets = DB::Aowow()->selectAssoc('SELECT `key` AS ARRAY_KEY, `value` AS "0", `flags` AS "1", `cat` AS "2", `default` AS "3", `comment` AS "4" FROM ::config ORDER BY `key` ASC'); foreach ($sets as $key => [$value, $flags, $catg, $default, $comment]) { $php = $flags & self::FLAG_PHP; @@ -129,7 +129,7 @@ class Cfg return 'this configuration option cannot be set'; $flags = self::FLAG_TYPE_STRING | self::FLAG_PHP; - if (!is_int(DB::Aowow()->query('INSERT IGNORE INTO ?_config (`key`, `value`, `cat`, `flags`) VALUES (?, ?, ?d, ?d)', $key, $value, self::CAT_MISCELLANEOUS, $flags))) + if (!is_int(DB::Aowow()->qry('INSERT IGNORE INTO ::config (`key`, `value`, `cat`, `flags`) VALUES (%s, %s, %i, %i)', $key, $value, self::CAT_MISCELLANEOUS, $flags))) return 'internal error'; self::$store[$key] = [$value, $flags, self::CAT_MISCELLANEOUS, null, null]; @@ -155,7 +155,7 @@ class Cfg if (self::$store[$key][self::IDX_FLAGS] & self::FLAG_INTERNAL) return 'can\'t delete internal option'; - if (!DB::Aowow()->query('DELETE FROM ?_config WHERE `key` = ? AND (`flags` & ?d) = 0 AND (`flags` & ?d) > 0', $key, self::FLAG_PERSISTENT, self::FLAG_PHP)) + if (!DB::Aowow()->qry('DELETE FROM ::config WHERE `key` = %s AND (`flags` & %i) = 0 AND (`flags` & %i) > 0', $key, self::FLAG_PERSISTENT, self::FLAG_PHP)) return 'internal error'; unset(self::$store[$key]); @@ -175,9 +175,9 @@ class Cfg } if ($fromDB && $fullInfo) - return array_values(DB::Aowow()->selectRow('SELECT `value`, `flags`, `cat`, `default`, `comment` FROM ?_config WHERE `key` = ?', $key)); + return array_values(DB::Aowow()->selectRow('SELECT `value`, `flags`, `cat`, `default`, `comment` FROM ::config WHERE `key` = %s', $key)); if ($fromDB) - return DB::Aowow()->selectCell('SELECT `value` FROM ?_config WHERE `key` = ?', $key); + return DB::Aowow()->selectCell('SELECT `value` FROM ::config WHERE `key` = %s', $key); if ($fullInfo) return self::$store[$key]; @@ -205,7 +205,7 @@ class Cfg if ($flags & self::FLAG_REQUIRED && !strlen($value)) return 'empty value given for required config'; - DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $value, $key); + DB::Aowow()->qry('UPDATE ::config SET `value` = %s WHERE `key` = %s', $value, $key); self::$store[$key][self::IDX_VALUE] = $value; // validate change @@ -220,7 +220,7 @@ class Cfg if ($errMsg) { // rollback change - DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $oldValue, $key); + DB::Aowow()->qry('UPDATE ::config SET `value` = %s WHERE `key` = %s', $oldValue, $key); self::$store[$key][self::IDX_VALUE] = $oldValue; return $errMsg; @@ -261,7 +261,7 @@ class Cfg if (!($flags & Cfg::FLAG_TYPE_STRING)) $default = @eval('return ('.$default.');'); - DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $default, $key); + DB::Aowow()->qry('UPDATE ::config SET `value` = %s WHERE `key` = %s', $default, $key); self::$store[$key][self::IDX_VALUE] = $default; // validate change @@ -276,7 +276,7 @@ class Cfg if ($errMsg) { // rollback change - DB::Aowow()->query('UPDATE ?_config SET `value` = ? WHERE `key` = ?', $oldValue, $key); + DB::Aowow()->qry('UPDATE ::config SET `value` = %s WHERE `key` = %s', $oldValue, $key); self::$store[$key][self::IDX_VALUE] = $oldValue; return $errMsg; @@ -473,6 +473,27 @@ class Cfg return true; } + + private static function logographic_ft_search(int|string $value, ?string &$msg = '') : bool + { + if (!$value) + return true; + + $ok = true; + foreach (['::spell', '::items', '::objects', '::creature', '::quests'] as $tbl) + { + if (DB::Aowow()->selectRow('SHOW INDEX FROM %n WHERE `column_name` = %s AND `index_type` = "FULLTEXT"', $tbl, 'name_loc4')) + continue; + + $ok = false; + $msg .= "\nNo fulltext index found on col: 'name_loc4'; tbl: '".$tbl."'."; + } + + if (!$ok) + $msg .= "\nCannot enable option.\n"; + + return $ok; + } } ?> diff --git a/includes/components/Conditions/Conditions.class.php b/includes/components/Conditions/Conditions.class.php index 6168ca1f..9512d766 100644 --- a/includes/components/Conditions/Conditions.class.php +++ b/includes/components/Conditions/Conditions.class.php @@ -118,6 +118,9 @@ class Conditions // public const SCENARIO_STEP = 54; // ❌ reserved for TC master // public const SCENE_IN_PROGRESS = 55; // ❌ reserved for TC master // public const PLAYER_CONDITION = 56; // ❌ reserved for TC master +// public const PRIVATE_OBJECT = 57; // ❌ reserved for TC master + public const STRING_ID = 58; // go or npc has StringId NULL, NULL, NULL +// public const LABEL = 59; // ❌ reserved for TC master private const IDX_SRC_GROUP = 0; private const IDX_SRC_ENTRY = 1; @@ -208,7 +211,8 @@ class Conditions self::QUESTSTATE => [Type::QUEST, true, null, null], self::QUEST_OBJECTIVE_PROGRESS => [Type::QUEST, true, true, null], self::DIFFICULTY_ID => [true, null, null, null], - self::GAMEMASTER => [true, null, null, null] + self::GAMEMASTER => [true, null, null, null], + self::STRING_ID => [true, null, null, null] ); private $jsGlobals = []; @@ -221,29 +225,34 @@ class Conditions /* IN */ /******/ - public function getBySourceEntry(int $entry, int ...$srcType) : self + public function getBySource(int|array $type, int|array $group = 0, int|array $entry = 0, int|array $id = 0) : self { - $this->rows = array_merge($this->rows, DB::World()->select( - 'SELECT `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `SourceId`, `ElseGroup`, - `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition` - FROM conditions - WHERE `SourceTypeOrReferenceId` IN (?a) AND `SourceEntry` = ?d - ORDER BY `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `ElseGroup` ASC', - $srcType, $entry - )); + if ($group) + $group = is_int($group) ? [$group] : array_map('intVal', $group); + if ($entry) + $entry = is_int($entry) ? [$entry] : array_map('intVal', $entry); + if ($id) + $id = is_int($id) ? [$id] : array_map('intVal', $id); + if ($type) + $type = is_int($type) ? [$type] : array_map('intVal', $type); + else + return $this; - return $this; - } + $where = [['`SourceTypeOrReferenceId` IN %in', $type]]; + if ($group) + $where[] = ['`SourceGroup` IN %in', $group]; + if ($entry) + $where[] = ['`SourceEntry` IN %in', $entry]; + if ($id) + $where[] = ['`SourceId` IN %in', $id]; - public function getBySourceGroup(int $group, int ...$srcType) : self - { - $this->rows = array_merge($this->rows, DB::World()->select( + $this->rows = array_merge($this->rows, DB::World()->selectAssoc( 'SELECT `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `SourceId`, `ElseGroup`, - `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition` + `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `ConditionStringValue1`, `NegativeCondition` FROM conditions - WHERE `SourceTypeOrReferenceId` IN (?a) AND `SourceGroup` = ?d + WHERE %and ORDER BY `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `ElseGroup` ASC', - $srcType, $group + $where )); return $this; @@ -256,23 +265,23 @@ class Conditions if ($type === $cVal1 /* && (!$conditionIds || in_array($cId, $conditionIds)) */ ) { if ($cId == self::CHR_CLASS || $cId == self::CHR_RACE) - $lookups[] = sprintf("(c2.`ConditionTypeOrReference` = %d AND (c2.`ConditionValue1` & %d) > 0)", $cId, 1 << ($typeId - 1)); + $lookups[] = [DB::AND, [['c2.`ConditionTypeOrReference` = %i', $cId], ['(c2.`ConditionValue1` & %i) > 0', 1 << ($typeId - 1)]]]; else - $lookups[] = sprintf("(c2.`ConditionTypeOrReference` = %d AND c2.`ConditionValue1` = %d)", $cId, $typeId); + $lookups[] = [DB::AND, [['c2.`ConditionTypeOrReference` = %i', $cId], ['c2.`ConditionValue1` = %i', $typeId]]]; } if (!$lookups) return $this; - $this->rows = array_merge($this->rows, DB::World()->select(sprintf( + $this->rows = array_merge($this->rows, DB::World()->selectAssoc( 'SELECT c1.`SourceTypeOrReferenceId`, c1.`SourceEntry`, c1.`SourceGroup`, c1.`SourceId`, c1.`ElseGroup`, - c1.`ConditionTypeOrReference`, c1.`ConditionTarget`, c1.`ConditionValue1`, c1.`ConditionValue2`, c1.`ConditionValue3`, c1.`NegativeCondition` + c1.`ConditionTypeOrReference`, c1.`ConditionTarget`, c1.`ConditionValue1`, c1.`ConditionValue2`, c1.`ConditionValue3`, c1.`ConditionStringValue1`, c1.`NegativeCondition` FROM conditions c1 JOIN conditions c2 ON c1.SourceTypeOrReferenceId = c2.SourceTypeOrReferenceId AND c1.SourceEntry = c2.SourceEntry AND c1.SourceGroup = c2.SourceGroup AND c1.SourceId = c2.SourceId - WHERE %s + WHERE %or GROUP BY `SourceTypeOrReferenceId`,`SourceGroup`,`SourceEntry`,`SourceId`,`ElseGroup`,`ConditionTypeOrReference`,`ConditionTarget`,`ConditionValue1`,`ConditionValue2`,`ConditionValue3` ORDER BY `SourceTypeOrReferenceId`, `SourceEntry`, `SourceGroup`, `ElseGroup` ASC', - implode(' OR ', $lookups)) + $lookups )); return $this; @@ -283,7 +292,7 @@ class Conditions if (!isset(self::$source[$srcType])) return; - [$cId, $cVal1, $cVal2, $cVal3] = array_pad($condition, 5, 0); + [$cId, $cVal1, $cVal2, $cVal3, $cString] = array_pad(array_pad($condition, 5, 0), 6, ''); if (!isset(self::$conditions[abs($cId)])) return; @@ -293,7 +302,7 @@ class Conditions if (!$this->prepareSource($srcType, ...explode(':', $groupKey))) return; - if ($c = $this->prepareCondition($cId, $cVal1, $cVal2, $cVal3)) + if ($c = $this->prepareCondition($cId, $cVal1, $cVal2, $cVal3, $cString)) { if ($orGroup) $this->result[$srcType][$groupKey][] = [$c]; @@ -386,6 +395,14 @@ class Conditions return $success; } + public function toMarkupTag() : string + { + if (!$this->result) + return ''; + + return '[condition]' . json_encode($this->result, JSON_NUMERIC_CHECK) . '[/condition]'; + } + public function getJsGlobals() : array { return $this->jsGlobals; @@ -398,22 +415,22 @@ class Conditions public static function lootTableToConditionSource(string $lootTable) : int { - switch ($lootTable) + return match ($lootTable) { - case LOOT_FISHING: return self::SRC_FISHING_LOOT_TEMPLATE; - case LOOT_CREATURE: return self::SRC_CREATURE_LOOT_TEMPLATE; - case LOOT_GAMEOBJECT: return self::SRC_GAMEOBJECT_LOOT_TEMPLATE; - case LOOT_ITEM: return self::SRC_ITEM_LOOT_TEMPLATE; - case LOOT_DISENCHANT: return self::SRC_DISENCHANT_LOOT_TEMPLATE; - case LOOT_PROSPECTING: return self::SRC_PROSPECTING_LOOT_TEMPLATE; - case LOOT_MILLING: return self::SRC_MILLING_LOOT_TEMPLATE; - case LOOT_PICKPOCKET: return self::SRC_PICKPOCKETING_LOOT_TEMPLATE; - case LOOT_SKINNING: return self::SRC_SKINNING_LOOT_TEMPLATE; - case LOOT_MAIL: return self::SRC_MAIL_LOOT_TEMPLATE; - case LOOT_SPELL: return self::SRC_SPELL_LOOT_TEMPLATE; - case LOOT_REFERENCE: return self::SRC_REFERENCE_LOOT_TEMPLATE; - default: return self::SRC_NONE; - } + Loot::FISHING => self::SRC_FISHING_LOOT_TEMPLATE, + Loot::CREATURE => self::SRC_CREATURE_LOOT_TEMPLATE, + Loot::GAMEOBJECT => self::SRC_GAMEOBJECT_LOOT_TEMPLATE, + Loot::ITEM => self::SRC_ITEM_LOOT_TEMPLATE, + Loot::DISENCHANT => self::SRC_DISENCHANT_LOOT_TEMPLATE, + Loot::PROSPECTING => self::SRC_PROSPECTING_LOOT_TEMPLATE, + Loot::MILLING => self::SRC_MILLING_LOOT_TEMPLATE, + Loot::PICKPOCKET => self::SRC_PICKPOCKETING_LOOT_TEMPLATE, + Loot::SKINNING => self::SRC_SKINNING_LOOT_TEMPLATE, + Loot::MAIL => self::SRC_MAIL_LOOT_TEMPLATE, + Loot::SPELL => self::SRC_SPELL_LOOT_TEMPLATE, + Loot::REFERENCE => self::SRC_REFERENCE_LOOT_TEMPLATE, + default => self::SRC_NONE + }; } public static function extendListviewRow(array &$lvRow, int $srcType, int $groupKey, array $condition) : bool @@ -421,14 +438,14 @@ class Conditions if (!isset(self::$source[$srcType])) return false; - [$cId, $cVal1, $cVal2, $cVal3] = array_pad($condition, 5, 0); + [$cId, $cVal1, $cVal2, $cVal3, $cString1] = array_pad(array_pad($condition, 5, 0), 6, ''); if (!isset(self::$conditions[abs($cId)])) return false; while (substr_count($groupKey, ':') < 3) $groupKey .= ':0'; // pad with missing srcEntry, SrcId, cndTarget to group key - if ($c = (new self())->prepareCondition($cId, $cVal1, $cVal2, $cVal3)) + if ($c = (new self())->prepareCondition($cId, $cVal1, $cVal2, $cVal3, $cString1)) $lvRow['condition'][$srcType][$groupKey][] = [$c]; return true; @@ -462,7 +479,8 @@ class Conditions $r['NegativeCondition'] ? -$r['ConditionTypeOrReference'] : $r['ConditionTypeOrReference'], $r['ConditionValue1'], $r['ConditionValue2'], - $r['ConditionValue3'] + $r['ConditionValue3'], + $r['ConditionStringValue1'] ); if (!$cnd) continue; @@ -486,9 +504,9 @@ class Conditions $this->jsGlobals[$grp][$sGroup] = $sGroup; if (is_int($entry)) $this->jsGlobals[$entry][$sEntry] = $sEntry; - // Note: sourceId currently has no typed content - // if (is_int($id)) - // $this->jsGlobals[$id][$sId] = $sId; + // Note: sourceId currently has no typed content + // if (is_int($id)) + // $this->jsGlobals[$id][$sId] = $sId; // more checks? not all sources can retarget $cTarget = min(1, max(0, $cTarget)); @@ -496,10 +514,10 @@ class Conditions return [$sType, $sGroup, $sEntry, $sId, $cTarget]; } - private function prepareCondition($cId, $cVal1, $cVal2, $cVal3) : array + private function prepareCondition($cId, $cVal1, $cVal2, $cVal3, $cString1) : array { if ($fn = self::$conditions[abs($cId)][self::IDX_CND_FN]) - if (!$this->$fn(abs($cId), $cVal1, $cVal2, $cVal3)) + if (!$this->$fn(abs($cId), $cVal1, $cVal2, $cVal3, $cString1)) return []; $result = [$cId]; @@ -514,10 +532,16 @@ class Conditions $result[] = ${'cVal'.($i+1)}; // variable amount of condition values } + if ($cString1) + { + $result = array_pad($result, 4, null); + $result[4] = $cString1; + } + return $result; } - private function factionToSide($cndId, &$cVal1, $cVal2, $cVal3) : bool + private function factionToSide($cndId, &$cVal1, $cVal2, $cVal3, $cString1) : bool { if ($cVal1 == 469) $cVal1 = SIDE_ALLIANCE; @@ -529,7 +553,7 @@ class Conditions return true; } - private function mapToZone($cndId, &$cVal1, &$cVal2, $cVal3) : bool + private function mapToZone($cndId, &$cVal1, &$cVal2, $cVal3, $cString1) : bool { // use g_zone_categories id if ($cVal1 == 530) // outland @@ -538,7 +562,7 @@ class Conditions $cVal1 = 10; else if ($cVal1 == 0 || $cVal1 == 1) // eastern kingdoms / kalimdor ; // cVal alrady correct - NOP - else if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ?_zones WHERE `mapId` = ?d AND `parentArea` = 0 AND (`cuFlags` & ?d) = 0', $cVal1, CUSTOM_EXCLUDE_FOR_LISTVIEW)) + else if ($id = DB::Aowow()->selectCell('SELECT `id` FROM ::zones WHERE `mapId` = %i AND `parentArea` = 0 AND (`cuFlags` & %i) = 0', $cVal1, CUSTOM_EXCLUDE_FOR_LISTVIEW)) { // remap for instanced area - do not use List (pointless overhead) $this->jsGlobals[Type::ZONE][$id] = $id; @@ -554,7 +578,7 @@ class Conditions return true; } - private function maskToBits($cndId, &$cVal1, $cVal2, $cVal3) : bool + private function maskToBits($cndId, &$cVal1, $cVal2, $cVal3, $cString1) : bool { if ($cndId == self::CHR_CLASS) { @@ -573,11 +597,11 @@ class Conditions return true; } - private function typeidToId($cndId, $cVal1, &$cVal2, &$cVal3) : bool + private function typeidToId($cndId, $cVal1, &$cVal2, &$cVal3, $cString1) : bool { if ($cVal1 == self::TYPEID_UNIT) { - if ($cVal3 && ($_ = DB::Aowow()->selectCell('SELECT `typeId` FROM ?_spawns WHERE `type` = ?d AND `guid` = ?d', Type::NPC, $cVal3))) + if ($cVal3 && ($_ = DB::Aowow()->selectCell('SELECT `typeId` FROM ::spawns WHERE `type` = %i AND `guid` = %i', Type::NPC, $cVal3))) $cVal2 = intVal($_); if ($cVal2) @@ -585,7 +609,7 @@ class Conditions } else if ($cVal1 == self::TYPEID_GAMEOBJECT) { - if ($cVal3 && ($_ = DB::Aowow()->selectCell('SELECT `typeId` FROM ?_spawns WHERE `type` = ?d AND `guid` = ?d', Type::OBJECT, $cVal3))) + if ($cVal3 && ($_ = DB::Aowow()->selectCell('SELECT `typeId` FROM ::spawns WHERE `type` = %i AND `guid` = %i', Type::OBJECT, $cVal3))) $cVal2 = intVal($_); if ($cVal2) @@ -606,7 +630,7 @@ class Conditions return false; } - if ($npcs = DB::Aowow()->selectCol('SELECT `id` FROM ?_creature WHERE `lootId` = ?d', $sGroup)) + if ($npcs = DB::Aowow()->selectCol('SELECT `id` FROM ::creature WHERE `lootId` = %i', $sGroup)) { $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; foreach ($npcs as $npcId) @@ -630,7 +654,7 @@ class Conditions return false; } - if ($items = DB::Aowow()->selectCol('SELECT `id` FROM ?_items WHERE `disenchantId` = ?d', $sGroup)) + if ($items = DB::Aowow()->selectCol('SELECT `id` FROM ::items WHERE `disenchantId` = %i', $sGroup)) { $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; foreach ($items as $itemId) @@ -654,7 +678,7 @@ class Conditions return false; } - if ($gos = DB::Aowow()->selectCol('SELECT `id` FROM ?_objects WHERE `lootId` = ?d', $sGroup)) + if ($gos = DB::Aowow()->selectCol('SELECT `id` FROM ::objects WHERE `lootId` = %i', $sGroup)) { $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; foreach ($gos as $goId) @@ -678,7 +702,7 @@ class Conditions return false; } - if ($quests = DB::Aowow()->selectCol('SELECT `id` FROM ?_quests WHERE `rewardMailTemplateId` = ?d', $sGroup)) + if ($quests = DB::Aowow()->selectCol('SELECT `id` FROM ::quests WHERE `rewardMailTemplateId` = %i', $sGroup)) { $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; foreach ($quests as $questId) @@ -702,7 +726,7 @@ class Conditions return false; } - if ($npcs = DB::Aowow()->selectCol('SELECT `id` FROM ?_creature WHERE `pickpocketLootId` = ?d', $sGroup)) + if ($npcs = DB::Aowow()->selectCol('SELECT `id` FROM ::creature WHERE `pickpocketLootId` = %i', $sGroup)) { $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; foreach ($npcs as $npcId) @@ -726,7 +750,7 @@ class Conditions return false; } - if ($npcs = DB::Aowow()->selectCol('SELECT `id` FROM ?_creature WHERE `skinLootId` = ?d', $sGroup)) + if ($npcs = DB::Aowow()->selectCol('SELECT `id` FROM ::creature WHERE `skinLootId` = %i', $sGroup)) { $group = $sGroup . ':' . $sEntry . ':' . $sId . ':' . $cTarget; foreach ($npcs as $npcId) diff --git a/includes/components/SmartAI/SmartAI.class.php b/includes/components/SmartAI/SmartAI.class.php index 1518e426..3bf9b55e 100644 --- a/includes/components/SmartAI/SmartAI.class.php +++ b/includes/components/SmartAI/SmartAI.class.php @@ -12,7 +12,7 @@ trait SmartHelper { private function resolveGuid(int $type, int $guid) : ?int { - if ($_ = DB::Aowow()->selectCell('SELECT `typeId` FROM ?_spawns WHERE `type` = ?d AND `guid` = ?d', $type, $guid)) + if ($_ = DB::Aowow()->selectCell('SELECT `typeId` FROM ::spawns WHERE `type` = %i AND `guid` = %i', $type, $guid)) return $_; trigger_error('SmartAI::resolveGuid - failed to resolve guid '.$guid.' of type '.$type, E_USER_WARNING); @@ -21,14 +21,10 @@ trait SmartHelper private function numRange(int $min, int $max, bool $isTime) : string { - if (!$min && !$max) - return ''; + if ($isTime) + return Util::createNumRange($min, $max, ' – ', fn($x) => DateTime::formatTimeElapsedFloat($x)); - $str = $isTime ? Util::formatTime($min, true) : $min; - if ($max > $min) - $str .= ' – '.($isTime ? Util::formatTime($max, true) : $max); - - return $str; + return Util::createNumRange($min, $max, ' – ') ?: 0; } private function formatTime(int $time, int $_, bool $isMilliSec) : string @@ -36,11 +32,17 @@ trait SmartHelper if (!$time) return ''; - return Util::formatTime($time * ($isMilliSec ? 1 : 1000), false); + return DateTime::formatTimeElapsedFloat($time * ($isMilliSec ? 1 : 1000)); } private function castFlags(int $flags) : string { + if ($x = ($flags & ~SmartAI::CAST_FLAG_VALIDATE)) + { + trigger_error('SmartAI::castFlags - unknown SmartCastFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= SmartAI::CAST_FLAG_VALIDATE; + } + $cf = []; for ($i = 1; $i <= SmartAI::CAST_FLAG_COMBAT_MOVE; $i <<= 1) if (($flags & $i) && ($x = Lang::smartAI('castFlags', $i))) @@ -51,6 +53,12 @@ trait SmartHelper private function npcFlags(int $flags) : string { + if ($x = ($flags & ~NPC_FLAG_VALIDATE)) + { + trigger_error('SmartAI::npcFlags - unknown NpcFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= NPC_FLAG_VALIDATE; + } + $nf = []; for ($i = 1; $i <= NPC_FLAG_MAILBOX; $i <<= 1) if (($flags & $i) && ($x = Lang::npc('npcFlags', $i))) @@ -61,6 +69,12 @@ trait SmartHelper private function dynFlags(int $flags) : string { + if ($x = ($flags & ~UNIT_DYNFLAG_VALIDATE)) + { + trigger_error('SmartAI::dynFlags - unknown unit dynFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= UNIT_DYNFLAG_VALIDATE; + } + $df = []; for ($i = 1; $i <= UNIT_DYNFLAG_TAPPED_BY_ALL_THREAT_LIST; $i <<= 1) if (($flags & $i) && ($x = Lang::unit('dynFlags', $i))) @@ -71,6 +85,12 @@ trait SmartHelper private function goFlags(int $flags) : string { + if ($x = ($flags & ~GO_FLAG_VALIDATE)) + { + trigger_error('SmartAI::goFlags - unknown GameobjectFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= GO_FLAG_VALIDATE; + } + $gf = []; for ($i = 1; $i <= GO_FLAG_DESTROYED; $i <<= 1) if (($flags & $i) && ($x = Lang::gameObject('goFlags', $i))) @@ -81,6 +101,12 @@ trait SmartHelper private function spawnFlags(int $flags) : string { + if ($x = ($flags & ~SmartAI::SPAWN_FLAG_VALIDATE)) + { + trigger_error('SmartAI::spawnFlags - unknown SmartSpawnFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= SmartAI::SPAWN_FLAG_VALIDATE; + } + $sf = []; for ($i = 1; $i <= SmartAI::SPAWN_FLAG_NOSAVE_RESPAWN; $i <<= 1) if (($flags & $i) && ($x = Lang::smartAI('spawnFlags', $i))) @@ -91,6 +117,18 @@ trait SmartHelper private function unitFlags(int $flags, int $flags2) : string { + if ($x = ($flags & ~UNIT_FLAG_VALIDATE)) + { + trigger_error('SmartAI::unitFlags - unknown UnitFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= UNIT_FLAG_VALIDATE; + } + + if ($x = ($flags2 & ~UNIT_FLAG2_VALIDATE)) + { + trigger_error('SmartAI::unitFlags - unknown UnitFlags2 '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags2 &= UNIT_FLAG2_VALIDATE; + } + $field = $flags2 ? 'flags2' : 'flags'; $max = $flags2 ? UNIT_FLAG2_ALLOW_CHEAT_SPELLS : UNIT_FLAG_UNK_31; $uf = []; @@ -185,6 +223,7 @@ class SmartAI // public const CAST_FORCE_TARGET_SELF = 0x10; // the target to cast this spell on itself public const CAST_FLAG_AURA_MISSING = 0x20; // Only casts the spell if the target does not have an aura from the spell public const CAST_FLAG_COMBAT_MOVE = 0x40; // Prevents combat movement if cast successful. Allows movement on range, OOM, LOS + public const CAST_FLAG_VALIDATE = self::CAST_FLAG_INTERRUPT_PREV | self::CAST_FLAG_TRIGGERED | self::CAST_FLAG_AURA_MISSING | self::CAST_FLAG_COMBAT_MOVE; public const REACT_PASSIVE = 0; public const REACT_DEFENSIVE = 1; @@ -211,14 +250,23 @@ class SmartAI public const SPAWN_FLAG_IGNORE_RESPAWN = 0x01; // onSpawnIn - ignore & reset respawn timer public const SPAWN_FLAG_FORCE_SPAWN = 0x02; // onSpawnIn - force additional spawn if already in world public const SPAWN_FLAG_NOSAVE_RESPAWN = 0x04; // onDespawn - remove respawn time + public const SPAWN_FLAG_VALIDATE = self::SPAWN_FLAG_IGNORE_RESPAWN | self::SPAWN_FLAG_FORCE_SPAWN | self::SPAWN_FLAG_NOSAVE_RESPAWN; private array $jsGlobals = []; private array $rawData = []; private array $result = []; private array $tabs = []; private array $itr = []; + private array $quotes = []; - private array $quotes = []; + public string $css = <<<CSS + #smartai-generic .grid { clear:left; display: grid; } + #smartai-generic .tabbed-contents { padding:0px; clear:left; } + #smartai-generic .grid thead, + #smartai-generic .grid tbody, + #smartai-generic .grid tr { display: contents; } + #sai { display: grid; } + CSS; // misc data public readonly int $baseEntry; // I'm a timed action list belonging to this entry @@ -231,24 +279,28 @@ class SmartAI $this->title = $miscData['title'] ?? ''; $this->teleportTargetArea = $miscData['teleportTargetArea'] ?? 0; - $raw = DB::World()->select( - 'SELECT `id`, `link`, - `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, - `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, - `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o` - FROM smart_scripts - WHERE `entryorguid` = ?d AND `source_type` = ?d - ORDER BY `id` ASC', + if ($this->baseEntry) // my parent handles base css + $this->css = ''; + + $raw = DB::World()->selectAssoc( + 'SELECT `id`, `link`, + `event_type`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `event_phase_mask`, `event_chance`, `event_flags`, + `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, + `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o` + FROM smart_scripts + WHERE `entryorguid` = %i AND `source_type` = %i + ORDER BY `id` ASC', $this->entry, $this->srcType); foreach ($raw as $r) { $this->rawData[$r['id']] = array( - 'id' => $r['id'], - 'link' => $r['link'], - 'event' => new SmartEvent($r['id'], $r['event_type'], $r['event_phase_mask'], $r['event_chance'], $r['event_flags'], [$r['event_param1'], $r['event_param2'], $r['event_param3'], $r['event_param4'], $r['event_param5']], $this), - 'action' => new SmartAction($r['id'], $r['action_type'], [$r['action_param1'], $r['action_param2'], $r['action_param3'], $r['action_param4'], $r['action_param5'], $r['action_param6']], $this), - 'target' => new SmartTarget($r['id'], $r['target_type'], [$r['target_param1'], $r['target_param2'], $r['target_param3'], $r['target_param4']], [$r['target_x'], $r['target_y'], $r['target_z'], $r['target_o']], $this) + 'id' => $r['id'], + 'link' => $r['link'], + 'event' => new SmartEvent($r['id'], $r['event_type'], $r['event_phase_mask'], $r['event_chance'], $r['event_flags'], [$r['event_param1'], $r['event_param2'], $r['event_param3'], $r['event_param4'], $r['event_param5']], $this), + 'action' => new SmartAction($r['id'], $r['action_type'], [$r['action_param1'], $r['action_param2'], $r['action_param3'], $r['action_param4'], $r['action_param5'], $r['action_param6']], $this), + 'target' => new SmartTarget($r['id'], $r['target_type'], [$r['target_param1'], $r['target_param2'], $r['target_param3'], $r['target_param4']], [$r['target_x'], $r['target_y'], $r['target_z'], $r['target_o']], $this), + 'condition' => (new Conditions())->getBySource(Conditions::SRC_SMART_EVENT, $r['id'] + 1, $entry, $srcType) ); } } @@ -268,15 +320,15 @@ class SmartAI SmartAction::ACTION_MOUNT_TO_ENTRY_OR_MODEL => [1 => $npcId] ); - if ($npcGuids = DB::Aowow()->selectCol('SELECT `guid` FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d', Type::NPC, $npcId)) - if ($groups = DB::World()->selectCol('SELECT `groupId` FROM spawn_group WHERE `spawnType` = 0 AND `spawnId` IN (?a)', $npcGuids)) + if ($npcGuids = DB::Aowow()->selectCol('SELECT `guid` FROM ::spawns WHERE `type` = %i AND `typeId` = %i', Type::NPC, $npcId)) + if ($groups = DB::World()->selectCol('SELECT `groupId` FROM spawn_group WHERE `spawnType` = 0 AND `spawnId` IN %in', $npcGuids)) foreach ($groups as $g) $lookup[SmartAction::ACTION_SPAWN_SPAWNGROUP][1] = $g; $result = self::getActionOwner($lookup, $typeFilter); // can skip lookups for SmartAction::ACTION_SUMMON_CREATURE_GROUP as creature_summon_groups already contains summoner info - if ($sgs = DB::World()->select('SELECT `summonerType` AS "0", `summonerId` AS "1" FROM creature_summon_groups WHERE `entry` = ?d', $npcId)) + if ($sgs = DB::World()->selectAssoc('SELECT `summonerType` AS "0", `summonerId` AS "1" FROM creature_summon_groups WHERE `entry` = %i', $npcId)) foreach ($sgs as [$type, $typeId]) $result[$type][] = $typeId; @@ -292,8 +344,8 @@ class SmartAI SmartAction::ACTION_SUMMON_GO => [1 => $objectId] ); - if ($objGuids = DB::Aowow()->selectCol('SELECT `guid` FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d', Type::OBJECT, $objectId)) - if ($groups = DB::World()->selectCol('SELECT `groupId` FROM spawn_group WHERE `spawnType` = 1 AND `spawnId` IN (?a)', $objGuids)) + if ($objGuids = DB::Aowow()->selectCol('SELECT `guid` FROM ::spawns WHERE `type` = %i AND `typeId` = %i', Type::OBJECT, $objectId)) + if ($groups = DB::World()->selectCol('SELECT `groupId` FROM spawn_group WHERE `spawnType` = 1 AND `spawnId` IN %in', $objGuids)) foreach ($groups as $g) $lookup[SmartAction::ACTION_SPAWN_SPAWNGROUP][1] = $g; @@ -350,38 +402,62 @@ class SmartAI break; } + $where = $qParts = []; foreach ($lookup as $action => $params) { - $aq = '(`action_type` = '.(int)$action.' AND ('; $pq = []; + $aq = [DB::AND, [['`action_type` = %i', $action], [DB::OR, &$pq]]]; foreach ($params as $idx => $p) - $pq[] = '`action_param'.(int)$idx.'` = '.(int)$p; + $pq[] = ["`action_param$idx` = %i", $p]; - if ($pq) - $qParts[] = $aq.implode(' OR ', $pq).'))'; + $qParts[] = $aq; + unset($pq); } - $smartS = DB::World()->select(sprintf('SELECT `source_type` AS "0", `entryOrGUID` AS "1" FROM smart_scripts WHERE (%s){ AND `source_type` IN (?a)}', $qParts ? implode(' OR ', $qParts) : '0'), $genFilter ?: DBSIMPLE_SKIP); + if ($genFilter) + $where[] = ['`source_type` IN %in', $genFilter]; + if ($qParts) + $where[] = [DB::OR, $qParts]; + + $smartS = DB::World()->selectAssoc('SELECT `source_type` AS "0", `entryOrGUID` AS "1" FROM smart_scripts WHERE %and', $where ?: [0]); // filter for TAL shenanigans if ($smartTAL = array_filter($smartS, fn($x) => $x[0] == self::SRC_TYPE_ACTIONLIST)) { $smartS = array_diff_key($smartS, $smartTAL); - $q = []; + $q = $where = []; foreach ($smartTAL as [, $eog]) { // SmartAction::ACTION_CALL_TIMED_ACTIONLIST - $q[] = '`action_type` = '.SmartAction::ACTION_CALL_TIMED_ACTIONLIST.' AND `action_param1` = '.$eog; + $q[] = [DB::AND, array( + ['`action_type` = %i', SmartAction::ACTION_CALL_TIMED_ACTIONLIST], + ['`action_param1` = %i', $eog] + )]; // SmartAction::ACTION_CALL_RANDOM_TIMED_ACTIONLIST - $q[] = '`action_type` = '.SmartAction::ACTION_CALL_RANDOM_TIMED_ACTIONLIST.' AND (`action_param1` = '.$eog.' OR `action_param2` = '.$eog.' OR `action_param3` = '.$eog.' OR `action_param4` = '.$eog.' OR `action_param5` = '.$eog.')'; + $q[] = [DB::AND, array( + ['`action_type` = %i', SmartAction::ACTION_CALL_RANDOM_TIMED_ACTIONLIST], + ['`action_param1` = %i', $eog], + ['`action_param2` = %i', $eog], + ['`action_param3` = %i', $eog], + ['`action_param4` = %i', $eog], + ['`action_param5` = %i', $eog] + )]; // SmartAction::ACTION_CALL_RANDOM_RANGE_TIMED_ACTIONLIST - $q[] = '`action_type` = '.SmartAction::ACTION_CALL_RANDOM_RANGE_TIMED_ACTIONLIST.' AND `action_param1` <= '.$eog.' AND `action_param2` >= '.$eog; + $q[] = [DB::AND, array( + ['`action_type` = %i', SmartAction::ACTION_CALL_RANDOM_RANGE_TIMED_ACTIONLIST], + ['%i BETWEEN `action_param1` AND `action_param2`', $eog] + )]; } - if ($_ = DB::World()->select(sprintf('SELECT `source_type` AS "0", `entryOrGUID` AS "1" FROM smart_scripts WHERE ((%s)){ AND `source_type` IN (?a)}', $q ? implode(') OR (', $q) : '0'), $talFilter ?: DBSIMPLE_SKIP)) + if ($talFilter) + $where[] = ['`source_type` IN %in', $talFilter]; + if ($q) + $where[] = [DB::OR, $q]; + + if ($_ = DB::World()->selectAssoc('SELECT `source_type` AS "0", `entryOrGUID` AS "1" FROM smart_scripts WHERE %and', $where ?: [0])) $smartS = array_merge($smartS, $_); } @@ -390,18 +466,18 @@ class SmartAI { $smartS = array_diff_key($smartS, $smartG); - $q = []; + $where = []; foreach ($smartG as [$st, $eog]) { if ($st == self::SRC_TYPE_CREATURE) - $q[] = '`type` = '.Type::NPC.' AND `guid` = '.-$eog; + $where[] = [DB::AND, [['`type` = %i', Type::NPC], ['`guid` = %i', -$eog]]]; else if ($st == self::SRC_TYPE_OBJECT) - $q[] = '`type` = '.Type::OBJECT.' AND `guid` = '.-$eog; + $where[] = [DB::AND, [['`type` = %i', Type::OBJECT], ['`guid` = %i', -$eog]]]; } - if ($q) + if ($where) { - $owner = DB::Aowow()->select(sprintf('SELECT `type`, `typeId` FROM ?_spawns WHERE (%s)', implode(') OR (', $q))); + $owner = DB::Aowow()->selectAssoc('SELECT `type`, `typeId` FROM ::spawns WHERE %or', $where); foreach ($owner as $o) $result[$o['type']][] = $o['typeId']; } @@ -440,15 +516,15 @@ class SmartAI if ($srcType == self::SRC_TYPE_CREATURE || $srcType == self::SRC_TYPE_OBJECT) { $st = $srcType == self::SRC_TYPE_CREATURE ? SUMMONER_TYPE_CREATURE : SUMMONER_TYPE_GAMEOBJECT; - if ($csg = DB::World()->selectCol('SELECT `entry` FROM creature_summon_groups WHERE `summonerType` = ?d AND `summonerId` = ?d', $st, $entry)) + if ($csg = DB::World()->selectCol('SELECT `entry` FROM creature_summon_groups WHERE `summonerType` = %i AND `summonerId` = %i', $st, $entry)) $result = array_merge($result, $csg); } if (!empty($moreInfo[SmartAction::ACTION_SPAWN_SPAWNGROUP])) { $grp = $moreInfo[SmartAction::ACTION_SPAWN_SPAWNGROUP]; - if ($sgs = DB::World()->selectCol('SELECT `spawnId` FROM spawn_group WHERE `spawnType` = ?d AND `groupId` IN (?a)', SUMMONER_TYPE_CREATURE, $grp)) - if ($ids = DB::Aowow()->selectCol('SELECT DISTINCT `typeId` FROM ?_spawns WHERE `type` = ?d AND `guid` IN (?a)', Type::NPC, $sgs)) + if ($sgs = DB::World()->selectCol('SELECT `spawnId` FROM spawn_group WHERE `spawnType` = %i AND `groupId` IN %in', SUMMONER_TYPE_CREATURE, $grp)) + if ($ids = DB::Aowow()->selectCol('SELECT DISTINCT `typeId` FROM ::spawns WHERE `type` = %i AND `guid` IN %in', Type::NPC, $sgs)) $result = array_merge($result, $ids); } @@ -468,8 +544,8 @@ class SmartAI if (!empty($moreInfo[SmartAction::ACTION_SPAWN_SPAWNGROUP])) { $grp = $moreInfo[SmartAction::ACTION_SPAWN_SPAWNGROUP]; - if ($sgs = DB::World()->selectCol('SELECT `spawnId` FROM spawn_group WHERE `spawnType` = ?d AND `groupId` IN (?a)', SUMMONER_TYPE_GAMEOBJECT, $grp)) - if ($ids = DB::Aowow()->selectCol('SELECT DISTINCT `typeId` FROM ?_spawns WHERE `type` = ?d AND `guid` IN (?a)', Type::OBJECT, $sgs)) + if ($sgs = DB::World()->selectCol('SELECT `spawnId` FROM spawn_group WHERE `spawnType` = %i AND `groupId` IN %in', SUMMONER_TYPE_GAMEOBJECT, $grp)) + if ($ids = DB::Aowow()->selectCol('SELECT DISTINCT `typeId` FROM ::spawns WHERE `type` = %i AND `guid` IN %in', Type::OBJECT, $sgs)) $result = array_merge($result, $ids); } @@ -505,9 +581,9 @@ class SmartAI if ($entry < 0) // no lookup by GUID return []; - $actionQuery = 'SELECT `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6` FROM smart_scripts WHERE `source_type` = ?d AND `action_type` IN (?a) AND `entryOrGUID` IN (?a)'; + $actionQuery = 'SELECT `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6` FROM smart_scripts WHERE `source_type` = %i AND `action_type` IN %in AND `entryOrGUID` IN %in'; - $smartScripts = DB::World()->select($actionQuery, $sourceType, array_merge(array_keys($lookup), SmartAction::ACTION_ALL_TIMED_ACTION_LISTS), [$entry]); + $smartScripts = DB::World()->selectAssoc($actionQuery, $sourceType, array_merge(array_keys($lookup), SmartAction::ACTION_ALL_TIMED_ACTION_LISTS), [$entry]); $smartResults = []; $smartTALs = []; foreach ($smartScripts as $s) @@ -536,7 +612,7 @@ class SmartAI if ($smartTALs) { - if ($TALActList = DB::World()->select($actionQuery, self::SRC_TYPE_ACTIONLIST, array_keys($lookup), $smartTALs)) + if ($TALActList = DB::World()->selectAssoc($actionQuery, self::SRC_TYPE_ACTIONLIST, array_keys($lookup), $smartTALs)) { foreach ($TALActList as $e) { @@ -579,10 +655,9 @@ class SmartAI if ($this->result) return true; - $hidePhase = - $hideChance = true; + $visibleCols = (1 << 0) | (1 << 2) | (1 << 4); - foreach ($this->iterate() as $id => $__) + foreach ($this->iterate() as $__) { $rowIdx = Util::createHash(8); @@ -596,53 +671,60 @@ class SmartAI $evtBody = str_replace(['#target#', '#rowIdx#'], [$this->itr['target']->process(), $rowIdx], $evtBody); $actBody = str_replace(['#target#', '#rowIdx#'], [$this->itr['target']->process(), $rowIdx], $actBody); - if (!$this->itr['event']->hasPhases()) - $hidePhase = false; + if ($this->itr['event']->hasPhases()) + $visibleCols |= (1 << 1); if ($this->itr['event']->chance != 100) - $hideChance = false; + $visibleCols |= (1 << 3); + + if ($this->itr['condition']->prepare()) + { + $visibleCols |= (1 << 5); + Util::mergeJsGlobals($this->jsGlobals, $this->itr['condition']->getJsGlobals()); + } $this->result[] = array( $this->itr['id'], implode(', ', Util::mask2bits($this->itr['event']->phaseMask, 1)), - $evtBody.($evtFooter ? '[div float=right margin=0px clear=both][i][small class=q0]'.$evtFooter.'[/small][/i][/div]' : null), + $evtBody.($evtFooter ? '[div float=right margin=0px clear=both width=100% align=right][i][small class=q0]'.$evtFooter.'[/small][/i][/div]' : ''), $this->itr['event']->chance.'%', - $actBody.($actFooter ? '[div float=right margin=0px clear=both][i][small class=q0]'.$actFooter.'[/small][/i][/div]' : null) + $actBody.($actFooter ? '[div float=right margin=0px clear=both width=100% align=right][i][small class=q0]'.$actFooter.'[/small][/i][/div]' : ''), + $this->itr['condition']->toMarkupTag() ); } $th = array( - '#' => 16, - 'Phase' => 32, - 'Event' => 350, - 'Chance' => 24, - 'Action' => 0 + ['#' , '24px'], + ['Phase', '48px'], + ['Event', '30%%'], + ['Chance', '60px'], + ['Action', 'auto'], + ['Condition', 'auto'] ); - if ($hidePhase) + for ($i = 0, $j = count($th); $i < $j; $i++) { - unset($th['Phase']); - foreach ($this->result as &$r) - unset($r[1]); - } - unset($r); + if ($visibleCols & (1 << $i)) + continue; - if ($hideChance) - { - unset($th['Chance']); + unset($th[$i]); foreach ($this->result as &$r) - unset($r[3]); - } - unset($r); + unset($r[$i]); - $tbl = '[tr]'; - foreach ($th as $n => $w) - $tbl .= '[td header '.($w ? 'width='.$w.'px' : null).']'.$n.'[/td]'; - $tbl .= '[/tr]'; + unset($r); + } + + $tblId = Util::createHash(12); + + $this->css .= "\n#tbl-".$tblId." { grid-template-columns: ".implode(' ', array_column($th, 1))."; }"; + + $tbl = '[tr]' . array_reduce(array_column($th, 0), fn($out, $n) => $out .= '[td header]'.$n.'[/td]', '') . '[/tr]'; foreach ($this->result as $r) $tbl .= '[tr][td]'.implode('[/td][td]', $r).'[/td][/tr]'; + $tbl = '[table id=tbl-'.$tblId.' class=grid]'.$tbl.'[/table]'; + if ($this->srcType == self::SRC_TYPE_ACTIONLIST) $this->tabs[$this->entry] = $tbl; else @@ -658,16 +740,16 @@ class SmartAI if (!$this->rawData) return null; - $wrapper = '[table class=grid width=940px]%s[/table]'; - $return = '[style]#smartai-generic .grid { clear:left; } #smartai-generic .tabbed-contents { padding:0px; clear:left; }[/style][pad][h3][toggler id=sai]SmartAI'.$this->title.'[/toggler][/h3][div id=sai clear=left]%s[/div]'; + $wrapper = '%s'; + $return = '[style]'.strtr($this->css, "\n", ' ').'[/style][pad][h3][toggler id=sai]SmartAI'.$this->title.'[/toggler][/h3][div id=sai clear=left]%s[/div]'; $tabs = ''; if (count($this->tabs) > 1) { - $wrapper = '[tabs name=sai width=942px]%s[/tabs]'; + $wrapper = '[tabs name=sai]%s[/tabs]'; $return = "[script]function TalTabClick(id) { $('#dsf67g4d-sai').find('[href=\'#sai-actionlist-' + id + '\']').click(); }[/script]" . $return; foreach ($this->tabs as $guid => $data) { - $buff = '[tab name="'.($guid ? 'ActionList #'.$guid : 'Main').'"][table class=grid width=940px]'.$data.'[/table][/tab]'; + $buff = '[tab name="'.($guid ? 'ActionList #'.$guid : 'Main').'"]'.$data.'[/tab]'; if ($guid) $tabs .= $buff; else diff --git a/includes/components/SmartAI/SmartAction.class.php b/includes/components/SmartAI/SmartAction.class.php index c80c8b7f..05a354eb 100644 --- a/includes/components/SmartAI/SmartAction.class.php +++ b/includes/components/SmartAI/SmartAction.class.php @@ -64,7 +64,7 @@ class SmartAction public const ACTION_SUMMON_GO = 50; // Spawns Gameobject, use target_type to set spawn position. public const ACTION_KILL_UNIT = 51; // Kills Creature. public const ACTION_ACTIVATE_TAXI = 52; // Sends player to flight path. You have to be close to Flight Master, which gives Taxi ID you need. - public const ACTION_WP_START = 53; // Creature starts Waypoint Movement. Use waypoints table to create movement. + public const ACTION_WP_START = 53; // Creature starts Waypoint Movement. Use waypoint_data table to create movement. public const ACTION_WP_PAUSE = 54; // Creature pauses its Waypoint Movement for given time. public const ACTION_WP_STOP = 55; // Creature stops its Waypoint Movement. public const ACTION_ADD_ITEM = 56; // Adds item(s) to player. @@ -416,7 +416,7 @@ class SmartAction case self::ACTION_SET_FACTION: // 2 -> any target if ($this->param[0]) { - $this->param[10] = DB::Aowow()->selectCell('SELECT `factionId` FROM ?_factiontemplate WHERE `id` = ?d', $this->param[0]); + $this->param[10] = DB::Aowow()->selectCell('SELECT `factionId` FROM ::factiontemplate WHERE `id` = %i', $this->param[0]); $this->jsGlobals[Type::FACTION][$this->param[10]] = $this->param[10]; } break; @@ -460,15 +460,15 @@ class SmartAction break; case self::ACTION_ACTIVATE_TAXI: // 52 -> invoker $nodes = DB::Aowow()->selectRow( - 'SELECT tn1.`name_loc0` AS "start_loc0", tn1.name_loc?d AS start_loc?d, tn2.`name_loc0` AS "end_loc0", tn2.name_loc?d AS end_loc?d - FROM ?_taxipath tp - JOIN ?_taxinodes tn1 ON tp.`startNodeId` = tn1.`id` - JOIN ?_taxinodes tn2 ON tp.`endNodeId` = tn2.`id` - WHERE tp.`id` = ?d', + 'SELECT tn1.`name_loc0` AS "start_loc0", tn1.name_loc%i AS start_loc%i, tn2.`name_loc0` AS "end_loc0", tn2.name_loc%i AS end_loc%i + FROM ::taxipath tp + JOIN ::taxinodes tn1 ON tp.`startNodeId` = tn1.`id` + JOIN ::taxinodes tn2 ON tp.`endNodeId` = tn2.`id` + WHERE tp.`id` = %i', Lang::getLocale()->value, Lang::getLocale()->value, Lang::getLocale()->value, Lang::getLocale()->value, $this->param[0] ); - $this->param[10] = Util::jsEscape(Util::localizedString($nodes, 'start')); - $this->param[11] = Util::jsEscape(Util::localizedString($nodes, 'end')); + $this->param[10] = Util::localizedString($nodes, 'start'); + $this->param[11] = Util::localizedString($nodes, 'end'); break; case self::ACTION_SET_INGAME_PHASE_MASK: // 44 -> any target if ($this->param[0]) @@ -486,7 +486,7 @@ class SmartAction $this->param[11] = str_pad($pos[0]['posX'] * 10, 3, '0', STR_PAD_LEFT).str_pad($pos[0]['posY'] * 10, 3, '0', STR_PAD_LEFT); } // maybe the mapId is an instane map - else if ($areaId = DB::Aowow()->selectCell('SELECT `id` FROM ?_zones WHERE `mapId` = ?d', $this->param[0])) + else if ($areaId = DB::Aowow()->selectCell('SELECT `id` FROM ::zones WHERE `mapId` = %i', $this->param[0])) $this->param[10] = $areaId; // ...whelp else @@ -507,7 +507,7 @@ class SmartAction if ($this->param[0]) { $slots = $this->param[1] ? Util::mask2bits($this->param[1], 1) : [1, 2, 3]; - $items = DB::World()->selectRow('SELECT `ItemID1`, `ItemID2`, `ItemID3` FROM creature_equip_template WHERE `CreatureID` = ?d AND `ID` = ?d', $this->smartAI->getEntry(), $this->param[0]); + $items = DB::World()->selectRow('SELECT `ItemID1`, `ItemID2`, `ItemID3` FROM creature_equip_template WHERE `CreatureID` = %i AND `ID` = %i', $this->smartAI->getEntry(), $this->param[0]); foreach ($slots as $s) if ($_ = $items['ItemID'.$s]) @@ -536,7 +536,7 @@ class SmartAction $buff = []; if ($this->param[0]) { - $items = DB::World()->selectRow('SELECT `ItemID1`, `ItemID2`, `ItemID3` FROM creature_equip_template WHERE `CreatureID` = ?d AND `ID` = ?d', $this->smartAI->getEntry(), $this->param[0]); + $items = DB::World()->selectRow('SELECT `ItemID1`, `ItemID2`, `ItemID3` FROM creature_equip_template WHERE `CreatureID` = %i AND `ID` = %i', $this->smartAI->getEntry(), $this->param[0]); foreach ($items as $i) { if (!$i) @@ -562,6 +562,8 @@ class SmartAction $tal = new SmartAI(SmartAI::SRC_TYPE_ACTIONLIST, $this->param[0], ['baseEntry' => $this->smartAI->getEntry()]); $tal->prepare(); + $this->smartAI->css .= $tal->css; + Util::mergeJsGlobals($this->jsGlobals, $tal->getJSGlobals()); foreach ($tal->getTabs() as $guid => $tt) @@ -587,6 +589,8 @@ class SmartAction $tal = new SmartAI(SmartAI::SRC_TYPE_ACTIONLIST, $this->param[$i], ['baseEntry' => $this->smartAI->getEntry()]); $tal->prepare(); + $this->smartAI->css .= $tal->css; + Util::mergeJsGlobals($this->jsGlobals, $tal->getJSGlobals()); foreach ($tal->getTabs() as $guid => $tt) @@ -603,6 +607,8 @@ class SmartAction $tal = new SmartAI(SmartAI::SRC_TYPE_ACTIONLIST, $i, ['baseEntry' => $this->smartAI->getEntry()]); $tal->prepare(); + $this->smartAI->css .= $tal->css; + Util::mergeJsGlobals($this->jsGlobals, $tal->getJSGlobals()); foreach ($tal->getTabs() as $guid => $tt) @@ -620,7 +626,7 @@ class SmartAction break; case self::ACTION_SUMMON_CREATURE_GROUP: // 107 -> untargeted if ($this->summons === null) - $this->summons = DB::World()->selectCol('SELECT `groupId` AS ARRAY_KEY, `entry` AS ARRAY_KEY2, COUNT(*) AS "n" FROM creature_summon_groups WHERE `summonerId` = ?d GROUP BY `groupId`, `entry`', $this->smartAI->getEntry()); + $this->summons = DB::World()->selectCol('SELECT `groupId` AS ARRAY_KEY, `entry` AS ARRAY_KEY2, COUNT(*) AS "n" FROM creature_summon_groups WHERE `summonerId` = %i GROUP BY `groupId`, `entry`', $this->smartAI->getEntry()); $buff = []; if (!empty($this->summons[$this->param[0]])) @@ -663,8 +669,8 @@ class SmartAction break; case self::ACTION_SPAWN_SPAWNGROUP: // 131 case self::ACTION_DESPAWN_SPAWNGROUP: // 132 - $this->param[10] = Util::jsEscape(DB::World()->selectCell('SELECT `GroupName` FROM spawn_group_template WHERE `groupId` = ?d', $this->param[0])); - $entities = DB::World()->select('SELECT `spawnType` AS "0", `spawnId` AS "1" FROM spawn_group WHERE `groupId` = ?d', $this->param[0]); + $this->param[10] = Util::jsEscape(DB::World()->selectCell('SELECT `GroupName` FROM spawn_group_template WHERE `groupId` = %i', $this->param[0])); + $entities = DB::World()->selectAssoc('SELECT `spawnType` AS "0", `spawnId` AS "1" FROM spawn_group WHERE `groupId` = %i', $this->param[0]); $n = 5; $buff = []; @@ -712,13 +718,19 @@ class SmartAction $this->param[10] = $this->param[1] + $this->param[2] / pow(10, floor(log10($this->param[2] ?: 1.0) + 1)); // i know string concatenation is a thing. don't @ me! break; case self::ACTION_TALK: // 1 -> any target + $talkTarget = $this->param[2]; case self::ACTION_SIMPLE_TALK: // 84 -> any target $playerSrc = false; if ($npcId = $this->smartAI->getTarget()->getTalkSource($playerSrc)) { if ($quotes = $this->smartAI->getQuote($npcId, $this->param[0], $npcSrc)) + { foreach ($quotes as ['text' => $text]) - $this->param[10] .= sprintf($text, $playerSrc ? Lang::main('thePlayer') : $npcSrc); + { + $talkTarget = ($talkTarget ?? true) ? Lang::game('target') : $npcSrc; + $this->param[10] .= sprintf($text, $playerSrc ? Lang::main('thePlayer') : $npcSrc, $talkTarget); + } + } } else trigger_error('SmartAI::action - could not determine talk source for action #'.$this->type); diff --git a/includes/components/SmartAI/SmartEvent.class.php b/includes/components/SmartAI/SmartEvent.class.php index 722179dc..5a1f0316 100644 --- a/includes/components/SmartAI/SmartEvent.class.php +++ b/includes/components/SmartAI/SmartEvent.class.php @@ -98,6 +98,10 @@ class SmartEvent public const EVENT_ON_SPELL_FAILED = 84; // On Unit::InterruptSpell public const EVENT_ON_SPELL_START = 85; // On Spell::prapare public const EVENT_ON_DESPAWN = 86; // On before creature removed + public const EVENT_SEND_EVENT_TRIGGER = 87; // [RESERVED] UNUSED NEEDS CHERRYPICK + public const EVENT_AREATRIGGER_EXIT = 88; // [RESERVED] don't use on 3.3.5a + public const EVENT_ON_AURA_APPLIED = 89; // + public const EVENT_ON_AURA_REMOVED = 90; // public const FLAG_NO_REPEAT = 0x0001; public const FLAG_DIFFICULTY_0 = 0x0002; @@ -108,6 +112,7 @@ class SmartEvent public const FLAG_NO_RESET = 0x0100; public const FLAG_WHILE_CHARMED = 0x0200; public const FLAG_ALL_DIFFICULTIES = self::FLAG_DIFFICULTY_0 | self::FLAG_DIFFICULTY_1 | self::FLAG_DIFFICULTY_2 | self::FLAG_DIFFICULTY_3; + public const FLAG_VALIDATE = self::FLAG_NO_REPEAT | self::FLAG_DEBUG_ONLY | self::FLAG_NO_RESET | self::FLAG_WHILE_CHARMED | self::FLAG_ALL_DIFFICULTIES; private const EVENT_CELL_TPL = '[tooltip name=e-#rowIdx#]%1$s[/tooltip][span tooltip=e-#rowIdx#]%2$s[/span]'; @@ -198,7 +203,11 @@ class SmartEvent self::EVENT_ON_SPELL_CAST => [Type::SPELL, ['numRange', -1, true], null, null, null, 0], // SpellID, CooldownMin, CooldownMax self::EVENT_ON_SPELL_FAILED => [Type::SPELL, ['numRange', -1, true], null, null, null, 0], // SpellID, CooldownMin, CooldownMax self::EVENT_ON_SPELL_START => [Type::SPELL, ['numRange', -1, true], null, null, null, 0], // SpellID, CooldownMin, CooldownMax - self::EVENT_ON_DESPAWN => [null, null, null, null, null, 0] // NONE + self::EVENT_ON_DESPAWN => [null, null, null, null, null, 0], // NONE + self::EVENT_SEND_EVENT_TRIGGER => [null, null, null, null, null, 2], // UNUSED NEEDS CHERRYPICK + self::EVENT_AREATRIGGER_EXIT => [null, null, null, null, null, 2], // don't use on 3.3.5a + self::EVENT_ON_AURA_APPLIED => [Type::SPELL, ['numRange', -1, true], null, null, null, 0], // SpellID, CooldownMin, CooldownMax + self::EVENT_ON_AURA_REMOVED => [Type::SPELL, ['numRange', -1, true], null, null, null, 0] // SpellID, CooldownMin, CooldownMax ); private array $jsGlobals = []; @@ -267,7 +276,7 @@ class SmartEvent case 530: $this->param[10] = Lang::maps('Outland'); break; case 571: $this->param[10] = Lang::maps('Northrend'); break; default: - if ($aId = DB::Aowow()->selectCell('SELECT `id` FROM ?_zones WHERE `mapId` = ?d', $this->param[1])) + if ($aId = DB::Aowow()->selectCell('SELECT `id` FROM ::zones WHERE `mapId` = %i', $this->param[1])) { $this->param[11] = $aId; $this->jsGlobals[Type::ZONE][$aId] = $aId; @@ -281,22 +290,20 @@ class SmartEvent break; case self::EVENT_LINK: // 61 - Used to link together multiple events as a chain of events. - if ($links = DB::World()->selectCol('SELECT `id` FROM smart_scripts WHERE `link` = ?d AND `entryorguid` = ?d AND `source_type` = ?d', $this->id, $this->smartAI->entry, $this->smartAI->srcType)) + if ($links = DB::World()->selectCol('SELECT `id` FROM smart_scripts WHERE `link` = %i AND `entryorguid` = %i AND `source_type` = %i', $this->id, $this->smartAI->entry, $this->smartAI->srcType)) $this->param[10] = Lang::concat($links, Lang::CONCAT_OR, fn($x) => "#[b]".$x."[/b]"); break; case self::EVENT_GOSSIP_SELECT: // 62 - On gossip clicked (gossip_menu_option335). $gmo = DB::World()->selectRow( - 'SELECT gmo.`OptionText` AS "text_loc0" {, gmol.`OptionText` AS text_loc?d } + 'SELECT gmo.`OptionText` AS "text_loc0" %if', Lang::getLocale() != Locale::EN, ', gmol.`OptionText` AS %s', 'text_loc' . Lang::getLocale()->value, '%end FROM gossip_menu_option gmo - LEFT JOIN gossip_menu_option_locale gmol ON gmo.`MenuID` = gmol.`MenuID` AND gmo.`OptionID` = gmol.`OptionID` AND gmol.`Locale` = ?d - WHERE gmo.`MenuId` = ?d AND gmo.`OptionID` = ?d', - Lang::getLocale() != Locale::EN ? Lang::getLocale()->value : DBSIMPLE_SKIP, - Lang::getLocale()->json(), - $this->param[0], $this->param[1] + LEFT JOIN gossip_menu_option_locale gmol ON gmo.`MenuID` = gmol.`MenuID` AND gmo.`OptionID` = gmol.`OptionID` AND gmol.`Locale` = %s + WHERE gmo.`MenuId` = %i AND gmo.`OptionID` = %i', + Lang::getLocale()->json(), $this->param[0], $this->param[1] ); if ($gmo) - $this->param[10] = Util::jsEscape(Util::localizedString($gmo, 'text')); + $this->param[10] = Util::localizedString($gmo, 'text'); else trigger_error('SmartAI::event - could not find gossip menu option for event #'.$this->type); break; @@ -359,13 +366,19 @@ class SmartEvent public function hasPhases() : bool { - return $this->phaseMask == 0; + return $this->phaseMask && ($this->phaseMask & 0xFFF) != 0xFFF; } private function formatFlags() : string { $flags = $this->flags; + if ($x = ($flags & ~self::FLAG_VALIDATE)) + { + trigger_error('SmartEvent::formatFlags - unused SmartEventFlags '.Util::asBin($x).' set on id #'.$this->id, E_USER_NOTICE); + $flags &= self::FLAG_VALIDATE; + } + if (($flags & self::FLAG_ALL_DIFFICULTIES) == self::FLAG_ALL_DIFFICULTIES) $flags &= ~self::FLAG_ALL_DIFFICULTIES; diff --git a/includes/components/communitycontent.class.php b/includes/components/communitycontent.class.php index 8b04d7d5..ce7999af 100644 --- a/includes/components/communitycontent.class.php +++ b/includes/components/communitycontent.class.php @@ -31,11 +31,7 @@ class CommunityContent private static array $jsGlobals = []; private static array $subjCache = []; - private static string $coCountQuery = - 'SELECT COUNT(1) - FROM ?_comments c - WHERE c.`replyTo` = ?d AND c.`type` = ?d AND c.`typeId` = ?d AND - ((c.`flags` & ?d) = 0 OR c.`userId` = ?d OR ?d)'; + private static string $coCountQuery = 'SELECT COUNT(1) FROM ::comments c WHERE %and'; private static string $coQuery = 'SELECT c.*, @@ -44,55 +40,54 @@ class CommunityContent a3.`username` AS "deleteUser", a4.`username` AS "responseUser", IFNULL(SUM(ur.`value`), 0) AS "rating", - SUM(IF(ur.`userId` > 0 AND ur.`userId` = ?d, ur.`value`, 0)) AS "userRating", + SUM(IF(ur.`userId` > 0 AND ur.`userId` = %i, ur.`value`, 0)) AS "userRating", IF(r.`id` IS NULL, 0, 1) AS "userReported" - FROM ?_comments c - JOIN ?_account a1 ON c.`userId` = a1.`id` - LEFT JOIN ?_account a2 ON c.`editUserId` = a2.`id` - LEFT JOIN ?_account a3 ON c.`deleteUserId` = a3.`id` - LEFT JOIN ?_account a4 ON c.`responseUserId` = a4.`id` - LEFT JOIN ?_user_ratings ur ON c.`id` = ur.`entry` AND ur.`type` = ?d - LEFT JOIN ?_reports r ON r.`subject` = c.`id` AND r.`mode` = ?d AND r.`userId` = ?d - WHERE c.`replyTo` = ?d AND c.`type` = ?d AND c.`typeId` = ?d AND - ((c.`flags` & ?d) = 0 OR c.`userId` = ?d OR ?d) + FROM ::comments c + JOIN ::account a1 ON c.`userId` = a1.`id` + LEFT JOIN ::account a2 ON c.`editUserId` = a2.`id` + LEFT JOIN ::account a3 ON c.`deleteUserId` = a3.`id` + LEFT JOIN ::account a4 ON c.`responseUserId` = a4.`id` + LEFT JOIN ::user_ratings ur ON c.`id` = ur.`entry` AND ur.`type` = %i + LEFT JOIN ::reports r ON r.`subject` = c.`id` AND r.`mode` = %i AND r.`userId` = %i + WHERE %and GROUP BY c.`id` - ORDER BY c.`date` ASC'; + ORDER BY c.`date` ASC + %lmt'; private static string $ssQuery = - 'SELECT s.`id` AS ARRAY_KEY, s.`id`, a.`username` AS "user", s.`date`, s.`width`, s.`height`, s.`caption`, IF(s.`status` & ?d, 1, 0) AS "sticky", s.`type`, s.`typeId` - FROM ?_screenshots s - LEFT JOIN ?_account a ON s.`userIdOwner` = a.`id` - WHERE { s.`userIdOwner` = ?d AND }{ s.`type` = ? AND }{ s.`typeId` = ? AND } s.`status` & ?d AND (s.`status` & ?d) = 0 - { ORDER BY ?# DESC } - { LIMIT ?d }'; + 'SELECT s.`id` AS ARRAY_KEY, s.`id`, a.`username` AS "user", s.`date`, s.`width`, s.`height`, s.`caption`, IF(s.`status` & %i, 1, 0) AS "sticky", s.`type`, s.`typeId` + FROM ::screenshots s + LEFT JOIN ::account a ON s.`userIdOwner` = a.`id` + WHERE %and + ORDER BY `date` DESC + %lmt'; private static string $viQuery = - 'SELECT v.`id` AS ARRAY_KEY, v.`id`, a.`username` AS "user", v.`date`, v.`videoId`, v.`caption`, IF(v.`status` & ?d, 1, 0) AS "sticky", v.`type`, v.`typeId` - FROM ?_videos v - LEFT JOIN ?_account a ON v.`userIdOwner` = a.`id` - WHERE { v.`userIdOwner` = ?d AND }{ v.`type` = ? AND }{ v.`typeId` = ? AND } v.`status` & ?d AND (v.`status` & ?d) = 0 - { ORDER BY ?# ASC } - { LIMIT ?d }'; + 'SELECT v.`id` AS ARRAY_KEY, v.`id`, a.`username` AS "user", v.`date`, v.`videoId`, v.`caption`, IF(v.`status` & %i, 1, 0) AS "sticky", v.`type`, v.`typeId` + FROM ::videos v + LEFT JOIN ::account a ON v.`userIdOwner` = a.`id` + WHERE %and + ORDER BY %by + %lmt'; private static string $previewQuery = 'SELECT c.`id`, c.`body` AS "preview", c.`date`, c.`replyTo` AS "commentid", - IF(c.`flags` & ?d, 1, 0) AS "deleted", + IF(c.`flags` & %i, 1, 0) AS "deleted", IF(c.`type` <> 0, c.`type`, c2.`type`) AS "type", IF(c.`typeId` <> 0, c.`typeId`, c2.`typeId`) AS "typeId", IFNULL(SUM(ur.`value`), 0) AS "rating", a.`username` AS "user" - FROM ?_comments c - JOIN ?_account a ON c.`userId` = a.`id` - LEFT JOIN ?_user_ratings ur ON ur.`entry` = c.`id` AND ur.`userId` <> 0 AND ur.`type` = 1 - LEFT JOIN ?_comments c2 ON c.`replyTo` = c2.`id` - WHERE %s - ((c.`flags` & ?d) = 0 OR c.`userId` = ?d OR ?d) + FROM ::comments c + JOIN ::account a ON c.`userId` = a.`id` + LEFT JOIN ::user_ratings ur ON ur.`entry` = c.`id` AND ur.`userId` <> 0 AND ur.`type` = 1 + LEFT JOIN ::comments c2 ON c.`replyTo` = c2.`id` + WHERE %and GROUP BY c.`id` ORDER BY c.`date` DESC - { LIMIT ?d }'; + %lmt'; private static function addSubject(int $type, int $typeId) : void { @@ -108,7 +103,7 @@ class CommunityContent if (!$_) continue; - $obj = Type::newList($type, [Cfg::get('SQL_LIMIT_NONE'), ['id', $_]]); + $obj = Type::newList($type, [['id', $_]]); if (!$obj) continue; @@ -117,7 +112,7 @@ class CommunityContent } } - public static function getCommentPreviews(array $opt = [], ?int &$nFound = 0, bool $dateFmt = true) : array + public static function getCommentPreviews(array $opt = [], ?int &$nFound = 0, bool $dateFmt = true, int $resultLimit = PHP_INT_MAX) : array { /* purged:0, <- doesnt seem to be used anymore @@ -127,41 +122,28 @@ class CommunityContent // add default values $opt += ['user' => 0, 'unrated' => 0, 'comments' => 0, 'replies' => 0, 'flags' => 0]; - $w = []; + $where = []; + if (!User::isInGroup(U_GROUP_COMMENTS_MODERATOR)) + $where[] = [DB::OR, [['(c.`flags` & %i) = 0', CC_FLAG_DELETED], ['c.`userId` = %i', User::$id]]]; if ($opt['user']) - $w[] = sprintf('c.`userId` = %d AND', $opt['user']); + $where[] = ['c.`userId` = %i', $opt['user']]; if ($opt['unrated']) - $w[] = 'ur.`entry` IS NULL AND'; + $where[] = ['ur.`entry` IS %sN', null]; if ($opt['flags']) - $w[] = sprintf('(c.`flags` & %d) > 0 AND', $opt['flags']); + $where[] = ['(c.`flags` & %i) > 0', $opt['flags']]; if ($opt['comments'] && !$opt['replies']) - $w[] = 'c.`replyTo` = 0 AND'; + $where[] = ['c.`replyTo` = 0']; else if (!$opt['comments'] && $opt['replies']) - $w[] = 'c.`replyTo` <> 0 AND'; + $where[] = ['c.`replyTo` <> 0']; // else // pick both and no extra constraint needed for that - $query = sprintf(self::$previewQuery, implode(' ', $w)); - - $comments = DB::Aowow()->select( - $query, - CC_FLAG_DELETED, - CC_FLAG_DELETED, - User::$id, - User::isInGroup(U_GROUP_COMMENTS_MODERATOR), - Cfg::get('SQL_LIMIT_DEFAULT') - ); + $comments = DB::Aowow()->selectAssoc(self::$previewQuery, CC_FLAG_DELETED, $where, $resultLimit); if (!$comments) return []; - $nFound = DB::Aowow()->selectCell( - substr_replace($query, 'SELECT COUNT(*) ', 0, strpos($query, 'FROM')), - CC_FLAG_DELETED, - User::$id, - User::isInGroup(U_GROUP_COMMENTS_MODERATOR), - DBSIMPLE_SKIP - ); + $nFound = DB::Aowow()->selectCell(substr_replace(self::$previewQuery, 'SELECT COUNT(*) ', 0, strpos(self::$previewQuery, 'FROM')), $where, PHP_INT_MAX); foreach ($comments as $c) self::addSubject($c['type'], $c['typeId']); @@ -196,15 +178,22 @@ class CommunityContent return array_values($comments); } - public static function getCommentReplies(int $commentId, int $limit = 0, ?int &$nFound = 0) : array + public static function getCommentReplies(int $commentId, int $resultLimit = PHP_INT_MAX, ?int &$nFound = 0) : array { - $replies = []; - $query = $limit > 0 ? self::$coQuery.' LIMIT '.$limit : self::$coQuery; + $where = array( + ['c.`replyTo` = %i', $commentId], + ['c.`type` = %i', 0], + ['c.`typeId` = %i', 0] + ); + + if (!User::isInGroup(U_GROUP_COMMENTS_MODERATOR)) + $where[] = [DB::OR, [['(c.`flags` & %i) = 0', CC_FLAG_DELETED], ['c.`userId` = %i', User::$id]]]; // get replies - if ($results = DB::Aowow()->select($query, User::$id, RATING_COMMENT, Report::MODE_COMMENT, User::$id, $commentId, 0, 0, CC_FLAG_DELETED, User::$id, User::isInGroup(U_GROUP_COMMENTS_MODERATOR))) + $replies = []; + if ($results = DB::Aowow()->selectAssoc(self::$coQuery, User::$id, RATING_COMMENT, Report::MODE_COMMENT, User::$id, $where, $resultLimit)) { - $nFound = DB::Aowow()->selectCell(self::$coCountQuery, $commentId, 0, 0, CC_FLAG_DELETED, User::$id, User::isInGroup(U_GROUP_COMMENTS_MODERATOR)); + $nFound = DB::Aowow()->selectCell(self::$coCountQuery, $where); foreach ($results as $r) { @@ -239,7 +228,17 @@ class CommunityContent public static function getComments(int $type, int $typeId) : array { - $results = DB::Aowow()->query(self::$coQuery, User::$id, RATING_COMMENT, Report::MODE_COMMENT, User::$id, 0, $type, $typeId, CC_FLAG_DELETED, User::$id, (int)User::isInGroup(U_GROUP_COMMENTS_MODERATOR)); + $where = array( + ['c.`replyTo` = %i', 0], + ['c.`type` = %i', $type], + ['c.`typeId` = %i', $typeId] + ); + + if (!User::isInGroup(U_GROUP_COMMENTS_MODERATOR)) + $where[] = [DB::OR, [['(c.`flags` & %i) = 0', CC_FLAG_DELETED], ['c.`userId` = %i', User::$id]]]; + + // get replies + $results = DB::Aowow()->selectAssoc(self::$coQuery, User::$id, RATING_COMMENT, Report::MODE_COMMENT, User::$id, $where, PHP_INT_MAX); $comments = []; // additional informations @@ -295,32 +294,27 @@ class CommunityContent return $comments; } - public static function getVideos(int $typeOrUser = 0, int $typeId = 0, ?int &$nFound = 0, bool $dateFmt = true) : array + public static function getVideos(int $typeOrUser = 0, int $typeId = 0, ?int &$nFound = 0, bool $dateFmt = true, int $resultLimit = PHP_INT_MAX) : array { - $videos = DB::Aowow()->select(self::$viQuery, - CC_FLAG_STICKY, - $typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP, - $typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP, - $typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP, - CC_FLAG_APPROVED, - CC_FLAG_DELETED, - !$typeOrUser ? 'date' : 'pos', - !$typeOrUser ? Cfg::get('SQL_LIMIT_SEARCH') : DBSIMPLE_SKIP + $where = array( + ['v.`status` & %i', CC_FLAG_APPROVED], + ['(v.`status` & %i) = 0', CC_FLAG_DELETED] + ); + if ($typeOrUser < 0) + $where[] = ['v.`userIdOwner` = %i', -$typeOrUser]; + if ($typeOrUser > 0) + { + $where[] = ['v.`type` = %i', $typeOrUser]; + $where[] = ['v.`typeId` = %i', $typeId]; + } + + $videos = DB::Aowow()->selectAssoc(self::$viQuery, CC_FLAG_STICKY, $where, $typeOrUser ? ['date' => false] : ['pos' => true], $resultLimit); if (!$videos) return []; - $nFound = DB::Aowow()->selectCell( - substr_replace(self::$viQuery, 'SELECT COUNT(*) ', 0, strpos(self::$viQuery, 'FROM')), - $typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP, - $typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP, - $typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP, - CC_FLAG_APPROVED, - CC_FLAG_DELETED, - !$typeOrUser ? 'date' : 'pos', - DBSIMPLE_SKIP - ); + $nFound = DB::Aowow()->selectCell(substr_replace(self::$viQuery, 'SELECT COUNT(*) ', 0, strpos(self::$viQuery, 'FROM')), $where, $typeOrUser ? ['date' => false] : ['pos' => true], PHP_INT_MAX); if ($typeOrUser <= 0) // not for search by type/typeId { @@ -354,32 +348,31 @@ class CommunityContent return array_values($videos); } - public static function getScreenshots(int $typeOrUser = 0, int $typeId = 0, ?int &$nFound = 0, bool $dateFmt = true) : array + public static function getScreenshots(int $typeOrUser = 0, int $typeId = 0, ?int &$nFound = 0, bool $dateFmt = true, int $resultLimit = PHP_INT_MAX) : array { - $screenshots = DB::Aowow()->select(self::$ssQuery, + $where = array( + ['s.`status` & %i', CC_FLAG_APPROVED], + ['(s.`status` & %i) = 0', CC_FLAG_DELETED] + + ); + if ($typeOrUser < 0) + $where[] = ['s.`userIdOwner` = %i', -$typeOrUser]; + if ($typeOrUser > 0) + { + $where[] = ['s.`type` = %i', $typeOrUser]; + $where[] = ['s.`typeId` = %i', $typeId]; + } + + $screenshots = DB::Aowow()->selectAssoc(self::$ssQuery, CC_FLAG_STICKY, - $typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP, - $typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP, - $typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP, - CC_FLAG_APPROVED, - CC_FLAG_DELETED, - !$typeOrUser ? 'date' : DBSIMPLE_SKIP, - !$typeOrUser ? Cfg::get('SQL_LIMIT_SEARCH') : DBSIMPLE_SKIP + $where, + $resultLimit ); if (!$screenshots) return []; - $nFound = DB::Aowow()->selectCell( - substr_replace(self::$ssQuery, 'SELECT COUNT(*) ', 0, strpos(self::$ssQuery, 'FROM')), - $typeOrUser < 0 ? -$typeOrUser : DBSIMPLE_SKIP, - $typeOrUser > 0 ? $typeOrUser : DBSIMPLE_SKIP, - $typeOrUser > 0 ? $typeId : DBSIMPLE_SKIP, - CC_FLAG_APPROVED, - CC_FLAG_DELETED, - !$typeOrUser ? 'date' : DBSIMPLE_SKIP, - DBSIMPLE_SKIP - ); + $nFound = DB::Aowow()->selectCell(substr_replace(self::$ssQuery, 'SELECT COUNT(*) ', 0, strpos(self::$ssQuery, 'FROM')), $where, PHP_INT_MAX); if ($typeOrUser <= 0) // not for search by type/typeId { @@ -412,19 +405,6 @@ class CommunityContent return array_values($screenshots); } - public static function getAll(int $type, int $typeId, array &$jsg) : array - { - $result = array( - 'vi' => self::getVideos($type, $typeId), - 'ss' => self::getScreenshots($type, $typeId), - 'co' => self::getComments($type, $typeId) - ); - - Util::mergeJsGlobals($jsg, self::$jsGlobals); - - return $result; - } - public static function getJSGlobals() : array { return self::$jsGlobals; diff --git a/includes/components/dbtypelist.class.php b/includes/components/dbtypelist.class.php index 204a8319..c0d98f82 100644 --- a/includes/components/dbtypelist.class.php +++ b/includes/components/dbtypelist.class.php @@ -17,6 +17,7 @@ abstract class DBTypeList protected array $queryOpts = []; private array $itrStack = []; + private array $prefixes = []; public static int $type; public static int $contribute = CONTRIBUTE_ANY; @@ -29,12 +30,14 @@ abstract class DBTypeList * expression: str - must match fieldname; * int - 1: select everything; 0: select nothing * array - another condition array - * value: str - operator defaults to: LIKE <val> - * int - operator defaults to: = <val> + * value: str - operator defaults to: = <val> + * num - operator defaults to: = <val> * array - operator defaults to: IN (<val>) * null - operator defaults to: IS [NULL] * operator: modifies/overrides default * ! - negated default value (NOT LIKE; <>; NOT IN) + * MATCH - creates fulltext search ('value' must be array; column must have fulltext index) + * LIKE / NOT LIKE - partial string matching ('value' must be string (*d'uh*)) * condition as str * defines linking (AND || OR) * condition as int @@ -45,23 +48,24 @@ abstract class DBTypeList * ['id', 45], * ['name', 'test%', '!'], * [ - * 'AND', + * DB::AND, * ['flags', 0xFF, '&'], * ['flags2', 0xF, '&'], * ] * [['mask', 0x3, '&'], 0], + * ['nameField', ['+contains*', '-excludes'], 'MATCH], * ['joinedTbl.field', NULL] // NULL must be explicitly specified "['joinedTbl.field']" would be skipped as erroneous definition (only really usefull when left-joining) - * 'OR', + * DB::OR, * 5 * ) * results in - * WHERE ((`id` = 45) OR (`name` NOT LIKE "test%") OR ((`flags` & 255) AND (`flags2` & 15)) OR ((`mask` & 3) = 0)) OR (`joinedTbl`.`field` IS NULL) LIMIT 5 + * WHERE ((`id` = 45) OR (`name` NOT LIKE "test%") OR ((`flags` & 255) AND (`flags2` & 15)) OR ((`mask` & 3) = 0)) OR (MATCH(`nameField`) AGAINST("+contains* -excludes" IN BOOLEAN MODE)) OR (`joinedTbl`.`field` IS NULL) LIMIT 5 */ public function __construct(array $conditions = [], array $miscData = []) { - $where = []; - $linking = ' AND '; - $limit = Cfg::get('SQL_LIMIT_DEFAULT'); + $where = []; + $linking = DB::AND; + $limit = 0; $calcTotal = false; $totalQuery = ''; @@ -69,11 +73,10 @@ abstract class DBTypeList if (!$this->queryBase || $conditions === null) return; - $prefixes = []; - if (preg_match('/FROM \??[\w\_]+( AS)?\s?`?(\w+)`?$/i', $this->queryBase, $match)) - $prefixes['base'] = $match[2]; + if (preg_match('/FROM (?:::)?[\w\_]+( AS)?\s?`?(\w+)`?$/i', $this->queryBase, $match)) + $this->prefixes['base'] = $match[2]; else - $prefixes['base'] = ''; + $this->prefixes['base'] = ''; if (!empty($miscData['extraOpts'])) $this->extendQueryOpts($miscData['extraOpts']); @@ -81,122 +84,6 @@ abstract class DBTypeList if (!empty($miscData['calcTotal'])) $calcTotal = true; - $resolveCondition = function (array $c, string $supLink) use (&$resolveCondition, &$prefixes) : ?string - { - $subLink = ''; - - if (!$c) - return null; - - foreach ($c as $foo) - { - if ($foo === 'AND') - $subLink = ' AND '; - else if ($foo === 'OR') // nessi-bug: if (0 == 'OR') was true once... w/e - $subLink = ' OR '; - } - - // need to manually set link for subgroups to be recognized as condition set - if ($subLink) - { - $sql = []; - - foreach ($c as $foo) - if (is_array($foo)) - if ($x = $resolveCondition($foo, $supLink)) - $sql[] = $x; - - return $sql ? '('.implode($subLink, $sql).')' : null; - } - else - { - if ($c[0] == '1') - return '1'; - else if ($c[0] == '0') - return '(0)'; // trick if ($x = 0) into true... - else if (is_array($c[0]) && isset($c[1])) - $field = $resolveCondition($c[0], $supLink); - else if ($c[0]) - { - $setPrefix = function(mixed $f) use(&$prefixes) : ?string - { - if (is_array($f)) - $f = $f[0]; - - // numeric allows for formulas e.g. (1 < 3) - if (Util::checkNumeric($f)) - return $f; - - // skip condition if fieldName contains illegal chars - if (preg_match('/[^\d\w\.\_]/i', $f)) - return null; - - $f = explode('.', $f); - - switch (count($f)) - { - case 2: - if (!in_array($f[0], $prefixes)) - { - // choose table to join or return null if prefix does not exist - if (!in_array($f[0], array_keys($this->queryOpts))) - return null; - - $prefixes[] = $f[0]; - } - - return '`'.$f[0].'`.`'.$f[1].'`'; - case 1: - return '`'.$prefixes['base'].'`.`'.$f[0].'`'; - default: - return null; - } - }; - - // basic formulas - if (preg_match('/^\([\s\+\-\*\/\w\(\)\.]+\)$/i', strtr($c[0], ['`' => '', '´' => '', '--' => '']))) - $field = preg_replace_callback('/[\w\]*\.?[\w]+/i', $setPrefix, $c[0]); - else - $field = $setPrefix($c[0]); - - if (!$field) - return null; - } - else - return null; - - if (is_array($c[1]) && !empty($c[1])) - { - array_walk($c[1], fn(&$x) => $x = Util::checkNumeric($x) ? $x : DB::Aowow()->escape($x)); - - $op = (isset($c[2]) && $c[2] == '!') ? 'NOT IN' : 'IN'; - $val = '('.implode(', ', $c[1]).')'; - } - else if (Util::checkNumeric($c[1])) // Note: should this be a NUM_REQ_* check? - { - $op = (isset($c[2]) && $c[2] == '!') ? '<>' : '='; - $val = $c[1]; - } - else if (is_string($c[1])) - { - $op = (isset($c[2]) && $c[2] == '!') ? 'NOT LIKE' : 'LIKE'; - $val = DB::Aowow()->escape($c[1]); - } - else if (count($c) > 1 && $c[1] === null) // specifficly check for NULL - { - $op = (isset($c[2]) && $c[2] == '!') ? 'IS NOT' : 'IS'; - $val = 'NULL'; - } - else // null for example - return null; - - if (isset($c[2]) && $c[2] != '!') - $op = $c[2]; - - return '('.$field.' '.$op.' '.$val.')'; - } - }; - foreach ($conditions as $i => $c) { switch (getType($c)) @@ -205,29 +92,31 @@ abstract class DBTypeList break; case 'string': case 'integer': - if (is_string($c)) - $linking = $c == 'AND' ? ' AND ' : ' OR '; - else - $limit = $c > 0 ? $c : 0; + if (is_numeric($c)) + $limit = max(0, (int)$c); + else if ($c === DB::AND) + $linking = DB::AND; + else if ($c === DB::OR) + $linking = DB::OR; default: unset($conditions[$i]); } } foreach ($conditions as $c) - if ($x = $resolveCondition($c, $linking)) + if ($x = $this->resolveCondition($c, $linking)) $where[] = $x; // optional query parts may require other optional parts to work - foreach ($prefixes as $pre) + foreach ($this->prefixes as $pre) if (isset($this->queryOpts[$pre][0])) foreach ($this->queryOpts[$pre][0] as $req) - if (!in_array($req, $prefixes)) - $prefixes[] = $req; + if (!in_array($req, $this->prefixes)) + $this->prefixes[] = $req; // remove optional query parts, that are not required foreach ($this->queryOpts as $k => $arr) - if (!in_array($k, $prefixes)) + if (!in_array($k, $this->prefixes)) unset($this->queryOpts[$k]); // prepare usage of guids if using multiple realms (which have non-zoro indizes) @@ -245,7 +134,7 @@ abstract class DBTypeList // append conditions if ($where) - $this->queryBase .= ' WHERE ('.implode($linking, $where).')'; + $this->queryBase .= ' WHERE '.$linking; // append grouping if ($g = array_filter(array_column($this->queryOpts, 'g'))) @@ -255,6 +144,9 @@ abstract class DBTypeList if ($h = array_filter(array_column($this->queryOpts, 'h'))) $this->queryBase .= ' HAVING '.implode(' AND ', $h); + // fill in locale + $this->queryBase = str_replace(['DB_LOC_I', 'DB_LOC_S'], [Lang::getLocale()->value, '"'.Lang::getLocale()->json().'"'], $this->queryBase); + // without applied LIMIT and ORDER if ($calcTotal) $totalQuery = $this->queryBase; @@ -268,14 +160,15 @@ abstract class DBTypeList $this->queryBase .= ' LIMIT '.$limit; // execute query (finally) - $rows = []; // this is purely because of multiple realms per server foreach ($this->dbNames as $dbIdx => $n) { - $query = str_replace('DB_IDX', $dbIdx, $this->queryBase); - if ($rows = DB::{$n}($dbIdx)->select($query)) + try // does not go through the compatibility layer as we need to be able to fetch individual rows here { - if ($calcTotal) + $query = str_replace('DB_IDX', $dbIdx, $this->queryBase); + $result = DB::{$n}($dbIdx)->query($query, $where); + + if ($calcTotal && $result->getRowCount()) { // hackfix the inner items query to not contain duplicate column names // yes i know the real solution would be to not have items and item_stats share column names @@ -283,17 +176,21 @@ abstract class DBTypeList if (get_class($this) == ItemList::class) $totalQuery = str_replace([', `is`.*', ', i.`id` AS "id"'], '', $totalQuery); - $this->matches += DB::{$n}($dbIdx)->selectCell('SELECT COUNT(*) FROM ('.$totalQuery.') x'); + $this->matches += DB::{$n}($dbIdx)->selectCell('SELECT COUNT(*) FROM ('.$totalQuery.') x', $where); } - foreach ($rows as $id => $row) + foreach ($result->getIterator() as $row) { - if (isset($this->templates[$id])) - trigger_error('GUID for List already in use #'.$id.'. Additional occurrence omitted!', E_USER_ERROR); + // just .. roll with the unparsed, deprecated ARRAY_KEY, hmk? + if (isset($this->templates[$row['ARRAY_KEY']])) + trigger_error('GUID for List already in use #'.$row['ARRAY_KEY'].'. Additional occurrence omitted!', E_USER_ERROR); else - $this->templates[$id] = $row; + $this->templates[$row['ARRAY_KEY']] = (array)$row; } + + $result->free(); } + catch (\Exception $e) {} // logged via \Dibi\Event in DB::errorLogger } if (!$this->templates) @@ -306,6 +203,111 @@ abstract class DBTypeList $this->error = false; } + private function resolveCondition(array $c, string $supLink) : ?array + { + if (!$c) + return null; + + // i am recursive subcondition + if ($subLink = array_find($c, fn($x) => $x === DB::AND || $x === DB::OR)) + { + $sql = []; + + foreach ($c as $foo) + if (is_array($foo)) + if ($x = $this->resolveCondition($foo, $supLink)) + $sql[] = $x; + + return $sql ? [$subLink, $sql] : null; + } + + [$expOrField, $value, $op] = array_pad($c, 3, null); + + if (is_numeric($expOrField)) + return [$expOrField ? 1 : 0]; // [1] / [0] + if (!$expOrField) // '', null, [] + return null; + + $literal = false; + + if (is_array($expOrField) && $op != 'MATCH') + $field = $this->resolveCondition($expOrField, $supLink); + else + { + // basic formulas ex: [((minGold + maxGold) / 2), 0, '>'] + if (is_string($expOrField) && preg_match('/^\([\s\+\-\*\/\w\(\)\.]+\)$/i', strtr($expOrField, ['`' => '', '´' => '', '--' => '']))) + { + $field = preg_replace_callback('/[\w\]*\.?[\w]+/i', $this->setColPrefix(...), $expOrField); + $literal = true; + } + else + $field = $this->setColPrefix($expOrField); + + if (!$field) + return null; + } + + $neg = $op === '!'; + $expr = match (gettype($value)) + { + 'integer' => ($neg ? '<>' : ($op ?: '=')) . ' %i', + 'double' => ($neg ? '<>' : ($op ?: '=')) . ' %f', + 'string' => ($neg ? '<>' : ($op ?: '=')) . ' %s', + 'NULL' => ($neg ? 'IS NOT' : 'IS') . ' %sN', + 'array' => ($neg ? 'NOT IN' : 'IN') . ' %in', + default => null + }; + + if (!$expr) + return null; + + if ($op == 'MATCH' && gettype($value) == 'array') + return ['MATCH(%n)', $field, 'AGAINST(%s IN BOOLEAN MODE)', DB::Aowow()->translate($value)]; + if (($op == 'LIKE' || $op == 'NOT LIKE') && gettype($value) == 'string') + return ['%n', $field, $op, '%~like~', $value]; + if (is_array($field)) // $field is expression: [[flags, 0x4, '&'], 0] -> (`flags` & 4) = 0 + return [...$field, $expr, $value]; + + return [$literal ? '%SQL' : '%n', $field, $expr, $value]; + } + + private function setColPrefix(mixed $colName) : null|string|array + { + if (is_array($colName)) + return array_filter(array_map([$this, 'setColPrefix'], $colName)) ?: null; + + // numeric allows for formulas e.g. (1 < 3) + if (Util::checkNumeric($colName)) + return $colName; + + // skip condition if fieldName contains illegal chars + if (preg_match('/[^\d\w\.\_]/i', $colName)) + return null; + + [$prefix, $col, $err] = array_pad(explode('.', $colName), 3, null); + + if ($err) // more than one period + return null; + if (!$col) // prefix not set, so everything is shifted to the left :/ + return $this->prefixes['base'].'.'.$prefix; + + if (!in_array($prefix, $this->prefixes)) + { + // choose table to join or return null if prefix does not exist + if (!in_array($prefix, array_keys($this->queryOpts))) + return null; + + $this->prefixes[] = $prefix; + } + + return $prefix.'.'.$col; + } + + /** + * iterate over fetched templates + * + * @return array the current template + */ public function &iterate() : \Generator { if (!$this->templates) @@ -392,11 +394,8 @@ abstract class DBTypeList public function getRandomId() : int { - // ORDER BY RAND() is not optimal, so if anyone has an alternative idea.. - $where = User::isInGroup(U_GROUP_EMPLOYEE) ? ' WHERE (`cuFlags` & '.CUSTOM_EXCLUDE_FOR_LISTVIEW.') = 0' : ''; - - if (preg_match('/SELECT .*? FROM (\?\_[\w_-]+) /i', $this->queryBase, $m)) - return DB::Aowow()->selectCell(sprintf('SELECT `id` FROM %s%s ORDER BY RAND() ASC LIMIT 1', $m[1], $where)); + if (preg_match('/SELECT .*? FROM (::[\w_-]+) /i', $this->queryBase, $m)) + return DB::Aowow()->selectCell('SELECT `id` FROM %n WHERE (`cuFlags` & %i) = 0 ORDER BY RAND() ASC LIMIT 1', $m[1], User::isInGroup(U_GROUP_EMPLOYEE) ? 0 : CUSTOM_EXCLUDE_FOR_LISTVIEW) ?: 0; return 0; } @@ -458,7 +457,7 @@ abstract class DBTypeList public static function getName(int $id) : ?LocString { - if ($n = DB::Aowow()->SelectRow('SELECT `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM ?# WHERE `id` = ?d', static::$dataTable, $id)) + if ($n = DB::Aowow()->SelectRow('SELECT `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM %n WHERE `id` = %i', static::$dataTable, $id)) return new LocString($n); return null; } @@ -483,7 +482,7 @@ abstract class DBTypeList 't': type [always set] 'ti': typeId [always set] 'bd': BossDrop [0; 1] [Creature / GO] - 'dd': DungeonDifficulty [-2: DungeonHC; -1: DungeonNM; 1: Raid10NM; 2:Raid25NM; 3:Raid10HM; 4: Raid25HM] [Creature / GO] + 'dd': DungeonDifficulty [-2: DungeonHC; -1: DungeonNM; 1: Raid10NM; 2:Raid25NM; 3:Raid10HM; 4: Raid25HM; 99: filler trash] [Creature / GO] 'q': cssQuality [Items] 'z': zone [set when all happens in here] 'p': PvP [pvpSourceId] @@ -612,10 +611,10 @@ trait spawnHelper $this->spawnResult[SPAWNINFO_SHORT] = new \StdClass; // first get zone/floor with the most spawns - if ($res = DB::Aowow()->selectRow('SELECT `areaId`, `floor` FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d AND `posX` > 0 AND `posY` > 0 GROUP BY `areaId`, `floor` ORDER BY COUNT(1) DESC LIMIT 1', self::$type, $this->id)) + if ($res = DB::Aowow()->selectRow('SELECT `areaId`, `floor` FROM ::spawns WHERE `type` = %i AND `typeId` = %i AND `posX` > 0 AND `posY` > 0 GROUP BY `areaId`, `floor` ORDER BY COUNT(1) DESC LIMIT 1', self::$type, $this->id)) { // get relevant spawn points - $points = DB::Aowow()->select('SELECT `posX`, `posY` FROM ?_spawns WHERE `type` = ?d AND `typeId` = ?d AND `areaId` = ?d AND `floor` = ?d AND `posX` > 0 AND `posY` > 0', self::$type, $this->id, $res['areaId'], $res['floor']); + $points = DB::Aowow()->selectAssoc('SELECT `posX`, `posY` FROM ::spawns WHERE `type` = %i AND `typeId` = %i AND `areaId` = %i AND `floor` = %i AND `posX` > 0 AND `posY` > 0', self::$type, $this->id, $res['areaId'], $res['floor']); $spawns = []; foreach ($points as $p) $spawns[] = [$p['posX'], $p['posY']]; @@ -632,7 +631,18 @@ trait spawnHelper $wpSum = []; $wpIdx = 0; $worldPos = []; - $spawns = DB::Aowow()->select("SELECT * FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) AND `posX` > 0 AND `posY` > 0", self::$type, $this->getFoundIDs()) ?: []; + $spawns = DB::Aowow()->selectAssoc( + 'SELECT CASE WHEN z.`type` = %i THEN 1 + WHEN z.`type` = %i THEN 2 + WHEN z.`type` = %i THEN 2 + ELSE 0 + END AS "mapType", s.* + FROM ::spawns s + JOIN ::zones z ON s.areaId = z.id + WHERE s.`type` = %i AND s.`typeId` IN %in AND s.`posX` > 0 AND s.`posY` > 0', + MAP_TYPE_DUNGEON_HC, MAP_TYPE_MMODE_RAID, MAP_TYPE_MMODE_RAID_HC, + self::$type, $this->getFoundIDs() + ) ?: []; if (!$skipAdmin && User::isInGroup(U_GROUP_MODERATOR)) if ($guids = array_column(array_filter($spawns, fn($x) => $x['guid'] > 0 || $x['type'] != Type::NPC), 'guid')) @@ -646,14 +656,14 @@ trait spawnHelper // we will get a nice clusterfuck of dots if we do this for more GUIDs, than we have colors though if (!$skipWPs && count($spawns) < 6 && $s['type'] == Type::NPC) { - if ($wPoints = DB::Aowow()->select('SELECT * FROM ?_creature_waypoints WHERE creatureOrPath = ?d AND floor = ?d', $s['pathId'] ? -$s['pathId'] : $this->id, $s['floor'])) + if ($wPoints = DB::Aowow()->selectAssoc('SELECT * FROM ::creature_waypoints WHERE creatureOrPath = %i AND floor = %i', $s['pathId'] ? -$s['pathId'] : $this->id, $s['floor'])) { foreach ($wPoints as $i => $p) { $label = [Lang::npc('waypoint').Lang::main('colon').$p['point']]; if ($p['wait']) - $label[] = Lang::npc('wait').Lang::main('colon').Util::formatTime($p['wait'], false); + $label[] = Lang::npc('wait').Lang::main('colon').DateTime::formatTimeElapsedFloat($p['wait']); $opts = array( // \0 doesn't get printed and tricks Util::toJSON() into handling this as a string .. i feel slightly dirty now 'label' => "\0$<br /><span class=\"q0\">".implode('<br />', $label).'</span>', @@ -696,29 +706,34 @@ trait spawnHelper $info[2] = Lang::game('phases').Lang::main('colon').Util::asHex($s['phaseMask']); if ($s['spawnMask'] == 15) - $info[3] = Lang::game('mode').Lang::game('modes', -1); + $info[3] = Lang::game('mode').Lang::game('modes', 0, -1); else if ($s['spawnMask']) { $_ = []; for ($i = 0; $i < 4; $i++) if ($s['spawnMask'] & 1 << $i) - $_[] = Lang::game('modes', $i); + $_[] = Lang::game('modes', $s['mapType'], $i); $info[4] = Lang::game('mode').implode(', ', $_); } + if ($s['ScriptName']) + $info[5] = 'ScriptName'.Lang::main('colon').$s['ScriptName']; + if ($s['StringId']) + $info[6] = 'StringId'.Lang::main('colon').$s['StringId']; + if ($s['type'] == Type::AREATRIGGER) { // teleporter endpoint if ($s['guid'] < 0) { $opts['type'] = 4; - $info[5] = 'Teleport Destination'; + $info[7] = 'Teleport Destination'; } else { $o = Util::O2Deg($this->getField('orientation')); - $info[5] = 'Orientation'.Lang::main('colon').$o[0].'° ('.$o[1].')'; + $info[7] = 'Orientation'.Lang::main('colon').$o[0].'° ('.$o[1].')'; } } @@ -776,7 +791,7 @@ trait spawnHelper private function createZoneSpawns() : void // [zoneId1, zoneId2, ..] for locations-column in listview { - $res = DB::Aowow()->selectCol("SELECT `typeId` AS ARRAY_KEY, GROUP_CONCAT(DISTINCT `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) AND `posX` > 0 AND `posY` > 0 GROUP BY `typeId`", self::$type, $this->getfoundIDs()); + $res = DB::Aowow()->selectCol("SELECT `typeId` AS ARRAY_KEY, GROUP_CONCAT(DISTINCT `areaId`) FROM ::spawns WHERE `type` = %i AND `typeId` IN %in AND `posX` > 0 AND `posY` > 0 GROUP BY `typeId`", self::$type, $this->getfoundIDs()); foreach ($res as &$r) { $r = explode(',', $r); @@ -792,7 +807,7 @@ trait spawnHelper if (self::$type == Type::SOUND) return; - $res = DB::Aowow()->select('SELECT `areaId`, `floor`, `typeId`, `posX`, `posY` FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) AND `posX` > 0 AND `posY` > 0', self::$type, $this->getFoundIDs()); + $res = DB::Aowow()->selectAssoc('SELECT `areaId`, `floor`, `typeId`, `posX`, `posY` FROM ::spawns WHERE `type` = %i AND `typeId` IN %in AND `posX` > 0 AND `posY` > 0', self::$type, $this->getFoundIDs()); $spawns = []; foreach ($res as $data) { @@ -880,8 +895,13 @@ trait profilerHelper trait sourceHelper { - protected $sources = []; - protected $sourceMore = null; + protected array $sources = []; + protected ?array $sourceMore = null; + + public function getRawSource(int $src) : array + { + return $this->sources[$this->id][$src] ?? []; + } public function getSources(?array &$s = [], ?array &$sm = []) : bool { @@ -899,7 +919,7 @@ trait sourceHelper $buff[$_curTpl['moreType']][] = $_curTpl['moreTypeId']; foreach ($buff as $type => $ids) - $this->sourceMore[$type] = Type::newList($type, [Cfg::get('SQL_LIMIT_NONE'), ['id', $ids]]); + $this->sourceMore[$type] = Type::newList($type, [['id', $ids]]); } $s = array_keys($this->sources[$this->id]); @@ -923,7 +943,9 @@ trait sourceHelper 10H 0b0100 2 0b011 25H 0b1000 3 0b100 */ - if ($this->curTpl['moreMask'] & SRC_FLAG_DUNGEON_DROP) + if ($this->curTpl['moreMask'] & SRC_FLAG_COMMON) + $sm['dd'] = 99; + else if ($this->curTpl['moreMask'] & SRC_FLAG_DUNGEON_DROP) $sm['dd'] = $this->sources[$this->id][SRC_DROP][0] * -1; else if ($this->curTpl['moreMask'] & SRC_FLAG_RAID_DROP) { diff --git a/includes/components/filter.class.php b/includes/components/filter.class.php index 9f7db3c6..de0339f2 100644 --- a/includes/components/filter.class.php +++ b/includes/components/filter.class.php @@ -56,6 +56,7 @@ abstract class Filter public const V_LIST = 10; public const V_CALLBACK = 11; public const V_REGEX = 12; + public const V_NAME = 13; protected const ENUM_ANY = -2323; protected const ENUM_NONE = -2324; @@ -63,26 +64,26 @@ abstract class Filter protected const PATTERN_NAME = '/[\p{C};%\\\\]/ui'; protected const PATTERN_CRV = '/[\p{C};:%\\\\]/ui'; protected const PATTERN_INT = '/\D/'; - public const PATTERN_PARAM = '/^[\p{L}\p{Sm} \d\p{P}]+$/i'; + public const PATTERN_PARAM = '/^[\p{L}\p{Sm} \d\p{P}]+$/ui'; + public const PATTERN_FT = '/[^[:alpha:] \d_]/iu'; // +-*<>@()~" have special meaning; ' seems to fuck up the search; other irregular cases? protected const ENUM_FACTION = array( 469, 1037, 1106, 529, 1012, 87, 21, 910, 609, 942, 909, 530, 69, 577, 930, 1068, 1104, 729, 369, 92, 54, 946, 67, 1052, 749, 47, 989, 1090, 1098, 978, 1011, 93, 1015, 1038, 76, 470, 349, 1031, 1077, 809, 911, 890, 970, 169, 730, 72, 70, 932, 1156, 933, 510, 1126, 1067, 1073, 509, 941, 1105, 990, 934, 935, 1094, 1119, 1124, 1064, 967, 1091, 59, 947, 81, 576, 922, 68, 1050, 1085, 889, 589, 270); - protected const ENUM_CURRENCY = array(32572, 32569, 29736, 44128, 20560, 20559, 29434, 37829, 23247, 44990, 24368, 52027, 52030, 43016, 41596, 34052, 45624, 49426, 40752, 47241, - 40753, 29024, 24245, 26045, 26044, 38425, 29735, 24579, 24581, 32897, 22484, 52026, 52029, 4291, 28558, 43228, 34664, 47242, 52025, 52028, - 37836, 20558, 34597, 43589); + protected const ENUM_CURRENCY = array(32572, 32569, 29736, 44128, 20560, 20559, 29434, 37829, 23247, 44990, 24368, 43016, 41596, 34052, 45624, 49426, 40752, 47241, 40753, 29024, + 24245, 26045, 26044, 38425, 29735, 24579, 24581, 32897, 22484, 4291, 28558, 43228, 34664, 37836, 20558, 34597, 43589); protected const ENUM_EVENT = array( 372, 283, 285, 353, 420, 400, 284, 201, 374, 409, 141, 324, 321, 424, 423, 327, 341, 181, 404, 398, 301); - protected const ENUM_ZONE = array( 4494, 36, 2597, 3358, 45, 331, 3790, 4277, 16, 3524, 3, 3959, 719, 1584, 25, 1583, 2677, 3702, 3522, 4, - 3525, 3537, 46, 1941, 2918, 3905, 4024, 2817, 4395, 4378, 148, 393, 1657, 41, 2257, 405, 2557, 65, 4196, 1, - 14, 10, 15, 139, 12, 3430, 3820, 361, 357, 3433, 721, 394, 3923, 4416, 2917, 4272, 4820, 4264, 3483, 3562, - 267, 495, 4742, 3606, 210, 4812, 1537, 4710, 4080, 3457, 38, 4131, 3836, 3792, 2100, 2717, 493, 215, 3518, 3698, - 3456, 3523, 2367, 2159, 1637, 4813, 4298, 2437, 722, 491, 44, 3429, 3968, 796, 2057, 51, 3607, 3791, 3789, 209, - 3520, 3703, 3711, 1377, 3487, 130, 3679, 406, 1519, 4384, 33, 2017, 1477, 4075, 8, 440, 141, 3428, 3519, 3848, - 17, 2366, 3840, 3713, 3847, 3775, 4100, 1581, 3557, 3845, 4500, 4809, 47, 3849, 4265, 4493, 4228, 3698, 4406, 3714, - 3717, 3715, 717, 67, 3716, 457, 4415, 400, 1638, 1216, 85, 4723, 4722, 1337, 4273, 490, 1497, 206, 1196, 4603, - 718, 3277, 28, 40, 11, 4197, 618, 3521, 3805, 66, 1176, 1977); + protected const ENUM_ZONE = array( 36, 45, 3, 4, 46, 41, 2257, 1, 10, 139, 12, 3430, 3433, 267, 1537, 4080, 38, 4298, 44, 51, + 3487, 130, 1519, 33, 8, 47, 85, 1497, 28, 40, 11, 331, 16, 3524, 3525, 148, 1657, 405, 14, 15, + 361, 357, 493, 215, 1637, 1377, 406, 440, 141, 17, 3557, 400, 1638, 490, 618, 4494, 3790, 4277, 719, 1584, + 1583, 3713, 1581, 2557, 4196, 721, 4416, 4272, 4820, 4264, 3562, 4131, 3792, 2100, 2367, 4813, 2437, 722, 491, 796, + 2057, 3791, 3789, 209, 3714, 3717, 717, 2017, 1477, 3848, 2366, 3847, 4100, 4809, 3717, 3849, 4265, 4228, 3715, 4723, + 1337, 3716, 206, 1196, 4415, 718, 1176, 3428, 3959, 2677, 3923, 4812, 3457, 3836, 2717, 3456, 2159, 3429, 3607, 3845, + 3606, 4500, 4493, 4987, 4075, 4722, 4273, 4603, 3805, 1977, 2597, 3358, 3820, 4710, 4384, 3277, 3522, 3483, 3518, 3523, + 3520, 3703, 3519, 3521, 3702, 4378, 3698, 3968, 4406, 3537, 2817, 4395, 65, 394, 495, 4742, 210, 3711, 67, 4197, + 66); protected const ENUM_HEROICDUNGEON = array( 4494, 3790, 4277, 4196, 4416, 4272, 4820, 4264, 3562, 4131, 3792, 2367, 4813, 3791, 3789, 3848, 2366, 3713, 3847, 4100, 4809, 3849, 4265, 4228, 3714, 3717, 3715, 3716, 4415, 4723, 206, 1196); protected const ENUM_MULTIMODERAID = array( 4812, 3456, 2159, 4500, 4493, 4722, 4273, 4603, 4987); @@ -91,31 +92,43 @@ abstract class Filter protected const ENUM_RACE = array( null, 1, 2, 3, 4, 5, 6, 7, 8, null, 10, 11, true, false); protected const ENUM_PROFESSION = array( null, 171, 164, 185, 333, 202, 129, 755, 165, 186, 197, true, false, 356, 182, 773); - public bool $error = false; // erroneous search fields + public bool $error = false; + public bool $shouldReload = false; // erroneous params have been corrected. Build GET string and reload // item related - public array $upgrades = []; // [itemId => slotId] - public array $extraOpts = []; // score for statWeights - public array $wtCnd = []; // DBType condition for statWeights + public array $upgrades = []; // [itemId => slotId] + public array $extraOpts = []; // score for statWeights + public array $wtCnd = []; // DBType condition for statWeights private array $cndSet = []; // db type query storage private array $rawData = []; - /* genericFilter: [FILTER_TYPE, colOrFnName, param1, param2] + protected string $type = ''; // set by child + protected array $parentCats = []; // used to validate ty-filter + protected array $inTokens = []; // text search includes + protected array $exTokens = []; // text search excludes + protected array $ftTokens = []; // fulltext search + + /* $genericFilter: [FILTER_TYPE, colOrFnName, param1, param2] [self::CR_BOOLEAN, <string:colName>, <bool:isString>, null] [self::CR_FLAG, <string:colName>, <int:testBit>, <bool:matchAny>] # default param2: matchExact [self::CR_NUMERIC, <string:colName>, <int:NUM_FLAGS>, <bool:addExtraCol>] - [self::CR_STRING, <string:colName>, <int:STR_FLAGS>, null] - [self::CR_ENUM, <string:colName>, <bool:ANYNONE>, <bool:isEnumVal>] # param3 ? crv is val in enum : key in enum + [self::CR_STRING, <string:colName>, <int:STR_FLAGS>, <string:fulltextColName] + [self::CR_ENUM, <string:colName>, <bool:ANY_NONE>, <bool:isEnumVal>] # param3 ? crv is val in enum : key in enum [self::CR_STAFFFLAG, <string:colName>, null, null] [self::CR_CALLBACK, <string:fnName>, <mixed:param1>, <mixed:param2>] [self::CR_NYI_PH, null, <int:returnVal>, param2] # mostly 1: to ignore this criterium; 0: to fail the whole query - */ - protected string $type = ''; // set by child - protected array $parentCats = []; // used to validate ty-filter + $inputFields: fieldName => [VALUE_TYPE, checkInfo, fieldIsArray] + [self::V_EQUAL, <mixed:exactValue>, <bool:isArray>] + [self::V_RANGE, <array:minMaxInt>, <bool:isArray>] + [self::V_LIST, <array:validInts>, <bool:isArray>] + [self::V_CALLBACK, <string:fnName>, <bool:isArray>] + [self::V_REGEX, <string:regexp>, <bool:isArray>] + [self::V_NAME, <bool:matchExact>, <bool:isArray>] + */ protected static array $genericFilter = []; - protected static array $inputFields = []; // list of input fields defined per page - fieldName => [checkType, checkValue[, fieldIsArray]] + protected static array $inputFields = []; // list of input fields defined per page protected static array $enums = []; // validation for opt lists per page - criteriumID => [validOptionList] // express Filters in template @@ -126,8 +139,7 @@ abstract class Filter public array $fiReputationCols = []; // fn params ([[factionId, factionName], ...]) public array $fiExtraCols = []; // public string $query = ''; // as in url query params - public array $values = []; // old fiData['v'] - public array $criteria = []; // old fiData['c'] + public array $values = []; // prefiltered rawData // parse the provided request into a usable format public function __construct(string|array $data, array $opts = []) @@ -157,6 +169,8 @@ abstract class Filter } $this->initFields(); + $this->evalCriteria(); + $this->evalWeights(); } public function mergeCat(array &$cats) : void @@ -167,13 +181,13 @@ abstract class Filter private function &criteriaIterator() : \Generator { - if (!$this->criteria) + if (empty($this->values['cr'])) return; - for ($i = 0; $i < count($this->criteria['cr']); $i++) + for ($i = 0; $i < count($this->values['cr']); $i++) { // throws a notice if yielded directly "Only variable references should be yielded by reference" - $v = [&$this->criteria['cr'][$i], &$this->criteria['crs'][$i], &$this->criteria['crv'][$i]]; + $v = [&$this->values['cr'][$i], &$this->values['crs'][$i], &$this->values['crv'][$i]]; yield $i => $v; } } @@ -195,7 +209,7 @@ abstract class Filter public function buildGETParam(array $override = [], array $addCr = []) : string { $get = []; - foreach (array_merge($this->criteria, $this->values, $override) as $k => $v) + foreach (array_merge($this->values, $override) as $k => $v) { if (isset($addCr[$k])) { @@ -228,23 +242,30 @@ abstract class Filter $this->cndSet = $this->createSQLForValues(); // criteria - foreach ($this->criteriaIterator() as &$_cr) + $filters = []; + foreach ($this->criteriaIterator() as $_cr) if ($cnd = $this->createSQLForCriterium(...$_cr)) - $this->cndSet[] = $cnd; + $filters[] = $cnd; - if ($this->cndSet) // Note: TYPE_SOUND does not use 'match any' - array_unshift($this->cndSet, empty($this->values['ma']) ? 'AND' : 'OR'); + if ($filters) // if a filter uses criteria it must have a [ma]tch selector + { + array_unshift($filters, $this->values['ma'] ? DB::OR : DB::AND); + $this->cndSet[] = $filters; + } } + if ($this->cndSet) + array_unshift($this->cndSet, DB::AND); + return $this->cndSet; } public function getSetCriteria(int ...$cr) : array { - if (!$cr || !$this->fiSetCriteria) - return $this->fiSetCriteria; + if (!$cr || empty($this->values['cr'])) + return []; - return array_values(array_intersect($this->fiSetCriteria['cr'], $cr)); + return array_values(array_intersect($this->values['cr'], $cr)); } @@ -258,12 +279,17 @@ abstract class Filter return []; $data = []; + + // someone copy/pasted a WH filter + $get = preg_replace('/^(\d+(:\d+)*);(\d+(:\d+)*);(\P{C}+(:\P{C}+)*)$/', 'cr=$1;crs=$3;crv=$5', $get); + foreach (explode(';', $get) as $field) { if (!strstr($field, '=')) { trigger_error('Filter::transformGET - malformed GET string', E_USER_NOTICE); - $this->error = true; + $this->error = + $this->shouldReload = true; continue; } @@ -272,7 +298,8 @@ abstract class Filter if (!isset(static::$inputFields[$k])) { trigger_error('Filter::transformGET - GET param not in filter: '.$k, E_USER_NOTICE); - $this->error = true; + $this->error = + $this->shouldReload = true; continue; } @@ -286,13 +313,24 @@ abstract class Filter private function initFields() : void { + /* quirks: + * - in the POST step there may be excess criteria selectors with a value of '', as unselecting a criteria that is not the last will not remove the row from the UI + * - if there are no criteria selected, the placeholder selection will always be sent as ['', null, null], similar to the previous quirk + * + * same for stat weights on ItemListFilter + */ + if (!empty($this->rawData['cr'])) + $this->rawData['cr'] = array_filter($this->rawData['cr'], fn($x) => $x !== '') ?: null; + + if (!empty($this->rawData['wt'])) + $this->rawData['wt'] = array_filter($this->rawData['wt'], fn($x) => $x !== '') ?: null; + + $cleanupCr = []; foreach (static::$inputFields as $inp => [$type, $valid, $asArray]) { - $var = in_array($inp, ['cr', 'crs', 'crv']) ? 'criteria' : 'values'; - if (!isset($this->rawData[$inp]) || $this->rawData[$inp] === '') { - $this->$var[$inp] = $asArray ? [] : null; + $this->values[$inp] = $asArray ? [] : null; continue; } @@ -300,42 +338,62 @@ abstract class Filter if ($asArray) { - // quirk: in the POST step criteria can be [[''], null, null] if not selected. $buff = []; - foreach ((array)$val as $v) // can be string|int in POST step if only one value present - if ($v !== '' && $this->checkInput($type, $valid, $v)) + foreach ((array)$val as $i => $v) // can be string|int in POST step if only one value present + { + if (in_array($inp, ['cr', 'crs', 'crv'])) + { + if (!$this->checkInput($type, $valid, $v)) + $cleanupCr[] = $i; + $buff[] = $v; // always assign, gets removed later as tuple + } + else if ($this->checkInput($type, $valid, $v)) $buff[] = $v; + } - $this->$var[$inp] = $buff; + $this->values[$inp] = $buff; } else - $this->$var[$inp] = $this->checkInput($type, $valid, $val) ? $val : null; + $this->values[$inp] = $this->checkInput($type, $valid, $val) ? $val : null; + } + + if ($cleanupCr) + { + $this->error = + $this->shouldReload = true; + + foreach (array_unique($cleanupCr) as $i) + unset($this->values['cr'][$i], $this->values['crs'][$i], $this->values['crv'][$i]); + + $this->values['cr'] = array_values($this->values['cr']); + $this->values['crs'] = array_values($this->values['crs']); + $this->values['crv'] = array_values($this->values['crv']); } } - public function evalCriteria() : void // [cr]iterium, [cr].[s]ign, [cr].[v]alue + private function evalCriteria() : void // [cr]iterium, [cr].[s]ign, [cr].[v]alue { - if (empty($this->criteria['cr']) && empty($this->criteria['crs']) && empty($this->criteria['crv'])) + if (empty($this->values['cr']) && empty($this->values['crs']) && empty($this->values['crv'])) return; - else if (empty($this->criteria['cr']) || empty($this->criteria['crs']) || empty($this->criteria['crv'])) - { - unset($this->criteria['cr']); - unset($this->criteria['crs']); - unset($this->criteria['crv']); - trigger_error('Filter::setCriteria - one of cr, crs, crv is missing', E_USER_NOTICE); - $this->error = true; + if (empty($this->values['cr']) || empty($this->values['crs']) || empty($this->values['crv'])) + { + trigger_error('Filter::evalCriteria - one of cr, crs, crv is missing', E_USER_NOTICE); + unset($this->values['cr'], $this->values['crs'], $this->values['crv']); + + $this->error = + $this->shouldReload = true; return; } - $_cr = &$this->criteria['cr']; - $_crs = &$this->criteria['crs']; - $_crv = &$this->criteria['crv']; + $_cr = &$this->values['cr']; + $_crs = &$this->values['crs']; + $_crv = &$this->values['crv']; if (count($_cr) != count($_crv) || count($_cr) != count($_crs) || count($_cr) > 5 || count($_crs) > 5 /*|| count($_crv) > 5*/) { // use min provided criterion as basis; 5 criteria at most - $min = max(5, min(count($_cr), count($_crv), count($_crs))); + $min = min(5, count($_cr), count($_crv), count($_crs)); if (count($_cr) > $min) array_splice($_cr, $min); @@ -345,70 +403,94 @@ abstract class Filter if (count($_crs) > $min) array_splice($_crs, $min); - trigger_error('Filter::setCriteria - cr, crs, crv are imbalanced', E_USER_NOTICE); - $this->error = true; + trigger_error('Filter::evalCriteria - cr, crs, crv are imbalanced', E_USER_NOTICE); + $this->error = + $this->shouldReload = true; } for ($i = 0; $i < count($_cr); $i++) { - // conduct filter specific checks & casts here - $unsetme = false; - if (isset(static::$genericFilter[$_cr[$i]])) + if (!isset(static::$genericFilter[$_cr[$i]]) || $_crs[$i] === '' || $_crv[$i] === '') { - $gf = static::$genericFilter[$_cr[$i]]; - switch ($gf[0]) - { - case self::CR_NUMERIC: - $_ = $_crs[$i]; - if (!Util::checkNumeric($_crv[$i], $gf[2]) || !$this->int2Op($_)) - $unsetme = true; - break; - case self::CR_BOOLEAN: - case self::CR_FLAG: - $_ = $_crs[$i]; - if (!$this->int2Bool($_)) - $unsetme = true; - break; - case self::CR_ENUM: - case self::CR_STAFFFLAG: - if (!Util::checkNumeric($_crs[$i], NUM_CAST_INT)) - $unsetme = true; - break; - } + if ($_crs[$i] === '' || $_crv[$i] === '') + trigger_error('Filter::evalCriteria - received malformed criterium ["'.$_cr[$i].'", "'.$_crs[$i].'", "'.$_crv[$i].'"]', E_USER_NOTICE); + else + trigger_error('Filter::evalCriteria - received unhandled criterium: '.$_cr[$i], E_USER_NOTICE); + + unset($_cr[$i], $_crs[$i], $_crv[$i]); + + $this->error = + $this->shouldReload = true; + continue; } - if (!$unsetme && intval($_cr[$i]) && $_crs[$i] !== '' && $_crv[$i] !== '') - continue; + [$crType, $colOrFn, $param1, $param2] = array_pad(static::$genericFilter[$_cr[$i]], 4, null); - unset($_cr[$i]); - unset($_crs[$i]); - unset($_crv[$i]); + // conduct filter specific checks & casts here + switch ($crType) + { + case self::CR_NUMERIC: + $_ = $_crs[$i]; + if (Util::checkNumeric($_crv[$i], $param1) && $this->int2Op($_)) + continue 2; + break; + case self::CR_BOOLEAN: + case self::CR_FLAG: + $_ = $_crs[$i]; + if ($this->int2Bool($_)) + continue 2; + break; + case self::CR_STAFFFLAG: + if (User::isInGroup(U_GROUP_EMPLOYEE) && Util::checkNumeric($_crs[$i], NUM_CAST_INT)) + continue 2; + break; + case self::CR_ENUM: + if (Util::checkNumeric($_crs[$i], NUM_CAST_INT) && ( + (!$param2 && isset(static::$enums[$_cr[$i]][$_crs[$i]])) || + ($param2 && in_array($_crs[$i], static::$enums[$_cr[$i]])) || + ($param1 && ($_crs[$i] == self::ENUM_ANY || $_crs[$i] == self::ENUM_NONE)) + )) + continue 2; + break; + case self::CR_STRING: + if ($param1 & STR_LOCALIZED) + $colOrFn .= '_loc'.Lang::getLocale()->value; - trigger_error('Filter::setCriteria - generic check failed ["'.$_cr[$i].'", "'.$_crs[$i].'", "'.$_crv[$i].'"]', E_USER_NOTICE); - $this->error = true; + if ($this->tokenizeString($colOrFn, $_crv[$i], $param1 & STR_MATCH_EXACT, $param1 & STR_ALLOW_SHORT)) + continue 2; + break; + case self::CR_CALLBACK: + case self::CR_NYI_PH: + continue 2; + default: + trigger_error('Filter::evalCriteria - unknown criteria type: '.$crType, E_USER_WARNING); + break; + } + + trigger_error('Filter::evalCriteria - generic check failed ["'.$_cr[$i].'", "'.$_crs[$i].'", "'.$_crv[$i].'"]', E_USER_NOTICE); + unset($_cr[$i], $_crs[$i], $_crv[$i]); + + $this->error = + $this->shouldReload = true; } - $this->fiSetCriteria = array( - 'cr' => $_cr, - 'crs' => $_crs, - 'crv' => $_crv - ); + $this->fiSetCriteria = [$_cr, $_crs, $_crv]; } - public function evalWeights() : void + private function evalWeights() : void { // both empty: not in use; not an error - if (!$this->values['wt'] && !$this->values['wtv']) + if (empty($this->values['wt']) && empty($this->values['wtv'])) return; // one empty: erroneous manual input? if (!$this->values['wt'] || !$this->values['wtv']) { - unset($this->values['wt']); - unset($this->values['wtv']); - trigger_error('Filter::setWeights - one of wt, wtv is missing', E_USER_NOTICE); - $this->error = true; + unset($this->values['wt'], $this->values['wtv']); + + $this->error = + $this->shouldReload = true; return; } @@ -421,7 +503,8 @@ abstract class Filter if ($nwt != $nwtv) { trigger_error('Filter::setWeights - wt, wtv are imbalanced', E_USER_NOTICE); - $this->error = true; + $this->error = + $this->shouldReload = true; } if ($nwt > $nwtv) @@ -432,19 +515,19 @@ abstract class Filter $this->fiSetWeights = [$_wt, $_wtv]; } - protected function checkInput(int $type, mixed $valid, mixed &$val, bool $recursive = false) : bool + protected function checkInput(int $type, mixed $checkInfo, mixed &$val, bool $recursive = false) : bool { switch ($type) { case self::V_EQUAL: - if (gettype($valid) == 'integer') + if (gettype($checkInfo) == 'integer') $val = intval($val); - else if (gettype($valid) == 'double') + else if (gettype($checkInfo) == 'double') $val = floatval($val); - else /* if (gettype($valid) == 'string') */ + else /* if (gettype($checkInfo) == 'string') */ $val = strval($val); - if ($valid == $val) + if ($checkInfo == $val) return true; break; @@ -452,99 +535,168 @@ abstract class Filter if (!Util::checkNumeric($val, NUM_CAST_INT)) return false; - foreach ($valid as $k => $v) + if (in_array($val, $checkInfo)) + return true; + + foreach ($checkInfo as $v) { if (gettype($v) != 'array') continue; if ($this->checkInput(self::V_RANGE, $v, $val, true)) return true; - - unset($valid[$k]); } - if (in_array($val, $valid)) - return true; - break; case self::V_RANGE: - if (Util::checkNumeric($val, NUM_CAST_INT) && $val >= $valid[0] && $val <= $valid[1]) + if (Util::checkNumeric($val, NUM_CAST_INT) && $val >= $checkInfo[0] && $val <= $checkInfo[1]) return true; break; case self::V_CALLBACK: - if ($this->$valid($val)) + if ($this->$checkInfo($val)) return true; break; case self::V_REGEX: - if (!preg_match($valid, $val)) + if (!preg_match($checkInfo, $val)) return true; break; + case self::V_NAME: + if (preg_match(self::PATTERN_NAME, $val)) + break; + + if (!$this->tokenizeString('na', $val, $checkInfo && $this->values['ex'])) + return false; // quit without logging more errors + + return true; } if (!$recursive) { - trigger_error('Filter::checkInput - check failed [type: '.$type.' valid: '.((string)$valid).' val: '.((string)$val).']', E_USER_NOTICE); + trigger_error('Filter::checkInput - check failed [type: '.$type.' valid: '.Util::toString($checkInfo).' val: '.((string)$val).']', E_USER_NOTICE); $this->error = true; } return false; } - protected function transformToken(string $string, bool $exact) : string + public static function transformToken(string &$string, bool $allowShort = false) : ?array { + $lenTest = fn($x) => $x !== '' && (mb_strlen($x) > 2 || $allowShort || Lang::getLocale()->isLogographic()); + $string = trim($string); + + if ($string === '') + return null; + + // invalid chars for both LIKE and MATCH + $str = str_replace(['\\', '%'], '', $string); + + if ($neg = ($str[0] === '-')) + $str = mb_substr($str, 1); + + if (!$lenTest($str)) + return null; + + // if the fulltext token contains invalid chars, should it be sub-tokenized (current behavior) or should the chars just be stripped + if ($tok = explode(' ', preg_replace(self::PATTERN_FT, ' ', $str))) + { + $ft = array_filter($tok, $lenTest); + + if (count($tok) > 1) + $ft[] = implode('', $tok); + } + // escape manually entered _; entering % should be prohibited - $string = str_replace('_', '\\_', $string); + // then replace search wildcards with sql wildcards + $lk = strtr(str_replace('_', '\\_', $str), self::$wCards); - // now replace search wildcards with sql wildcards - $string = strtr($string, self::$wCards); - - return sprintf($exact ? '%s' : '%%%s%%', $string); + return [$lk, $ft, $neg]; } - protected function tokenizeString(array $fields, string $string = '', bool $exact = false, bool $shortStr = false) : array + protected function tokenizeString(string $field, string $string, bool $exact = false, bool $allowShort = false) : bool { - if (!$string && $this->values['na']) - $string = $this->values['na']; + // always allow sub 3 chars for logographic locales + if (Lang::getLocale()->isLogographic()) + $allowShort = true; - $qry = []; - foreach ($fields as $f) + $tokens = $exact ? [$string] : explode(' ', $string); + foreach ($tokens as $t) { - $sub = []; - $parts = $exact ? [$string] : array_filter(explode(' ', $string)); - foreach ($parts as $p) + if ([$like, $fulltext, $ex] = self::transformToken($t, $allowShort)) { - if ($p[0] == '-' && (mb_strlen($p) > 3 || $shortStr)) - $sub[] = [$f, $this->transformToken(mb_substr($p, 1), $exact), '!']; - else if ($p[0] != '-' && (mb_strlen($p) > 2 || $shortStr)) - $sub[] = [$f, $this->transformToken($p, $exact)]; - } + if ($like) + $this->{$ex ? 'exTokens' : 'inTokens'}[$field][] = $like; - // single cnd? - if (!$sub) - continue; - else if (count($sub) > 1) - array_unshift($sub, 'AND'); - else + // don't bother with fulltext search if exact is specified + if ($exact) + continue; + + // note: a fulltext search purely from exclude tokens will return no result + foreach ($fulltext as $ft) + $this->ftTokens[$field][] = ($ex ? '-' : '+') . '(' . $ft . '* ' . Util::strrev($ft) . '*)'; + } + } + + if (empty($this->inTokens[$field])) + { + trigger_error('Filter::tokenizeString - could not tokenize string: "'.$string.'" for input: '.$field, E_USER_NOTICE); + $this->error = true; + return false; + } + + return true; + } + + protected function buildLikeLookup(array $fields, bool $exact = false) : array + { + $qry = []; + foreach ($fields as [$field, $col]) + { + $sub = []; + if (!empty($this->inTokens[$field])) + $sub = array_merge($sub, array_map(fn($x) => [$col, $x, $exact ? null : 'LIKE'], $this->inTokens[$field])); + if (!empty($this->exTokens[$field])) + $sub = array_merge($sub, array_map(fn($x) => [$col, $x, $exact ? null : 'NOT LIKE'], $this->exTokens[$field])); + + if (count($sub) > 1) + array_unshift($sub, DB::AND); + else if ($sub) $sub = $sub[0]; - $qry[] = $sub; + if ($sub) + $qry[] = $sub; } - // single cnd? - if (!$qry) + return $qry ? [DB::OR, ...$qry] : []; + } + + protected function buildMatchLookup(array $fields, bool $exact = false) : array + { + if (Lang::getLocale()->isLogographic() && !Cfg::get('LOGOGRAPHIC_FT_SEARCH')) + return []; + + $qry = []; + foreach ($fields as [$field, $col]) { - trigger_error('Filter::tokenizeString - could not tokenize string: '.$string, E_USER_NOTICE); - $this->error = true; + if (!empty($this->ftTokens[$field])) + $qry[] = [$col, array_unique($this->ftTokens[$field]), 'MATCH']; + else + { + $tok = $this->values[$field]; + if (self::transformToken($tok)) + { + if (!is_array($col)) + $qry[] = [$col, $tok]; + else + foreach ($col as $c) + $qry[] = [$c, $tok]; + } + } } - else if (count($qry) > 1) - array_unshift($qry, 'OR'); - else - $qry = $qry[0]; - return $qry; + return $qry ? [DB::OR, ...$qry] : []; } protected function int2Op(mixed &$op) : bool @@ -616,14 +768,19 @@ abstract class Filter return [[$field, $value, '&'], $value]; } - private function genericString(string $field, string $value, ?int $strFlags) : ?array + private function genericString(string $field, ?int $strFlags, ?string $fulltextCol = null) : ?array { $strFlags ??= 0x0; + $lkCol = $field; if ($strFlags & STR_LOCALIZED) - $field .= '_loc'.Lang::getLocale()->value; + $lkCol .= '_loc'.Lang::getLocale()->value; - return $this->tokenizeString([$field], $value, $strFlags & STR_MATCH_EXACT, $strFlags & STR_ALLOW_SHORT); + $lookup = null; + if ($fulltextCol) + $lookup = $this->buildMatchLookup([[$lkCol, $fulltextCol]]); + + return $lookup ?: ($field ? $this->buildLikeLookup([[$lkCol, $lkCol]]) : null); } private function genericNumeric(string $field, int|float $value, int $op, int $typeCast) : ?array @@ -651,83 +808,72 @@ abstract class Filter return null; } - private function genericCriterion(int $cr, int $crs, string $crv) : ?array - { - [$crType, $colOrFn, $param1, $param2] = array_pad(static::$genericFilter[$cr], 4, null); - $result = null; - - switch ($crType) - { - case self::CR_NUMERIC: - $result = $this->genericNumeric($colOrFn, $crv, $crs, $param1); - break; - case self::CR_FLAG: - $result = $this->genericBooleanFlags($colOrFn, $param1, $crs, $param2); - break; - case self::CR_STAFFFLAG: - if (User::isInGroup(U_GROUP_EMPLOYEE) && $crs > 0) - $result = $this->genericBooleanFlags($colOrFn, (1 << ($crs - 1)), true); - break; - case self::CR_BOOLEAN: - $result = $this->genericBoolean($colOrFn, $crs, !empty($param1)); - break; - case self::CR_STRING: - $result = $this->genericString($colOrFn, $crv, $param1); - break; - case self::CR_ENUM: - if (!$param2 && isset(static::$enums[$cr][$crs])) - $result = $this->genericEnum($colOrFn, static::$enums[$cr][$crs]); - if ($param2 && in_array($crs, static::$enums[$cr])) - $result = $this->genericEnum($colOrFn, $crs); - else if ($param1 && ($crs == self::ENUM_ANY || $crs == self::ENUM_NONE)) - $result = $this->genericEnum($colOrFn, $crs); - break; - case self::CR_CALLBACK: - $result = $this->{$colOrFn}($cr, $crs, $crv, $param1, $param2); - break; - case self::CR_NYI_PH: // do not limit with not implemented filters - if (is_int($param1)) - return [$param1]; - - // for nonsensical values; compare against 0 - if ($this->int2Op($crs) && Util::checkNumeric($crv)) - { - if ($crs == '=') - $crs = '=='; - - return eval('return ('.$crv.' '.$crs.' 0);') ? [1] : [0]; - } - else - return [0]; - } - - if ($result && $crType == self::CR_NUMERIC && !empty($param2)) - $this->fiExtraCols[] = $cr; - - return $result; - } - /***********************************/ /* create conditions from */ /* non-generic values and criteria */ /***********************************/ - protected function createSQLForCriterium(int &$cr, int &$crs, string &$crv) : array + protected function createSQLForCriterium(int $cr, int $crs, string $crv) : array { if (!static::$genericFilter) // criteria not in use - no error return []; - if (isset(static::$genericFilter[$cr])) - if ($genCr = $this->genericCriterion($cr, $crs, $crv)) - return $genCr; + [$crType, $colOrFn, $param1, $param2] = array_pad(static::$genericFilter[$cr], 4, null); - trigger_error('Filter::createSQLForCriterium - received unhandled criterium: ["'.$cr.'", "'.$crs.'", "'.$crv.'"]', E_USER_NOTICE); - $this->error = true; + $handleEnum = function(int $cr, int $crs, string $col, ?bool $hasAnyNone, ?bool $crsAsVal) : ?array + { + if ($hasAnyNone && ($crs == self::ENUM_ANY || $crs == self::ENUM_NONE)) + return $this->genericEnum($col, $crs); + else if (!$crsAsVal && isset(static::$enums[$cr][$crs])) + return $this->genericEnum($col, static::$enums[$cr][$crs]); + else if ($crsAsVal && in_array($crs, static::$enums[$cr])) + return $this->genericEnum($col, $crs); - unset($cr, $crs, $crv); + return null; + }; - return []; + $handleNYIPH = function(int $crs, string $crv, ?int $forceResult) : ?array + { + if (is_int($forceResult)) + return [$forceResult]; + + // for nonsensical values; compare against 0 + if ($this->int2Op($crs) && Util::checkNumeric($crv)) + { + if ($crs == '=') + $crs = '=='; + + return eval('return ('.$crv.' '.$crs.' 0);') ? [1] : [0]; + } + else + return [0]; + }; + + $result = match ($crType) + { + self::CR_NUMERIC => $this->genericNumeric($colOrFn, $crv, $crs, $param1), + self::CR_FLAG => $this->genericBooleanFlags($colOrFn, $param1, $crs, $param2), + self::CR_STAFFFLAG => $this->genericBooleanFlags($colOrFn, (1 << ($crs - 1)), true), + self::CR_BOOLEAN => $this->genericBoolean($colOrFn, $crs, !empty($param1)), + self::CR_STRING => $this->genericString($colOrFn, $param1, $param2), + self::CR_CALLBACK => $this->{$colOrFn}($cr, $crs, $crv, $param1, $param2), + self::CR_ENUM => $handleEnum($cr, $crs, $colOrFn, $param1, $param2), + self::CR_NYI_PH => $handleNYIPH($crs, $crv, $param1), + default => null + }; + + if (!$result) + { + // this really should not have happened. The relevant checks are run on __construct() + trigger_error('Filter::createSQLForCriterium - failed to resolve criterium: ["'.$cr.'", "'.$crs.'", "'.$crv.'"]', E_USER_WARNING); + return []; + } + + if ($crType == self::CR_NUMERIC && !empty($param2)) + $this->fiExtraCols[] = $cr; + + return $result; } abstract protected function createSQLForValues() : array; diff --git a/includes/components/frontend/iconelement.class.php b/includes/components/frontend/iconelement.class.php index a9fdac67..6014bd31 100644 --- a/includes/components/frontend/iconelement.class.php +++ b/includes/components/frontend/iconelement.class.php @@ -15,12 +15,12 @@ class IconElement private const CREATE_ICON_TPL = "\$WH.ge('%s%d').appendChild(%s.createIcon(%s));\n"; private int $idx = 0; - private string $href = ''; - private bool $noIcon = false; public readonly string $quality; public readonly ?string $align; + public readonly ?string $href; public readonly int $size; + public readonly bool $noIcon; public function __construct( public readonly int $type, @@ -70,6 +70,8 @@ class IconElement if ($link || $url) $this->href = $url ?: '?'.Type::getFileString($this->type).'='.$this->typeId; + else + $this->href = null; // see Spell/Tools having icon container but no actual icon and having to be inline with other IconElements $this->noIcon = !$typeId || !Type::hasIcon($type); @@ -102,7 +104,7 @@ class IconElement } if ($this->href) - ($a = $dom->createElement('a', $this->text))->setAttribute('href', $this->href); + ($a = $dom->createElement('a', htmlentities($this->text)))->setAttribute('href', $this->href); else $a = $dom->createTextNode($this->text); @@ -148,9 +150,9 @@ class IconElement $params = [$this->typeId, $this->size]; if ($this->num || $this->qty) - $params[] = is_numeric($this->num) ? $this->num : "'".$this->num."'"; + $params[] = is_int($this->num) ? $this->num : "'".$this->num."'"; if ($this->qty) - $params[] = is_numeric($this->qty) ? $this->qty : "'".$this->qty."'"; + $params[] = is_int($this->qty) ? $this->qty : "'".$this->qty."'"; return str_repeat(' ', $lpad) . sprintf(self::CREATE_ICON_TPL, $this->element, $this->idx, Type::getJSGlobalString($this->type), implode(', ', $params)); } diff --git a/includes/components/frontend/infoboxmarkup.class.php b/includes/components/frontend/infoboxmarkup.class.php index 7e6b5d74..dff22075 100644 --- a/includes/components/frontend/infoboxmarkup.class.php +++ b/includes/components/frontend/infoboxmarkup.class.php @@ -8,7 +8,7 @@ if (!defined('AOWOW_REVISION')) class InfoboxMarkup extends Markup { - public function __construct(private array $items, array $opts, string $parent = '') + public function __construct(private array $items, array $opts, string $parent = '', private int $completionRowType = 0) { parent::__construct('', $opts, $parent); } @@ -23,27 +23,48 @@ class InfoboxMarkup extends Markup public function append(string $text) : self { - if ($this->items && !$this->__text) - $this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]'); + if ($_ = $this->prepare()) + $this->replace($_); return parent::append($text); } public function __toString() : string { - if ($this->items && !$this->__text) - $this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]'); + // inject before output to avoid adding it to cache + if ($this->completionRowType && User::getCharacters()) + $this->items[] = [Lang::profiler('completion') . '[span class="compact-completion-display"][/span]', ['style' => 'display:none']]; + + if ($_ = $this->prepare()) + $this->replace($_); return parent::__toString(); } public function getJsGlobals() : array { - if ($this->items && !$this->__text) - $this->replace('[ul][li]' . implode('[/li][li]', $this->items) . '[/li][/ul]'); + if ($_ = $this->prepare()) + $this->replace($_); return parent::getJsGlobals(); } + + private function prepare() : string + { + if (!$this->items || $this->__text) + return ''; + + $buff = ''; + foreach ($this->items as $row) + { + if (is_array($row)) + $buff .= '[li'.Util::nodeAttributes($row[1]).']' . $row[0] . '[/li]'; + else if (is_string($row)) + $buff .= '[li]' . $row . '[/li]'; + } + + return $buff ? '[ul]'.$buff.'[/ul]' : ''; + } } ?> diff --git a/includes/components/frontend/listview.class.php b/includes/components/frontend/listview.class.php index 7180d6ba..0c6da5c8 100644 --- a/includes/components/frontend/listview.class.php +++ b/includes/components/frontend/listview.class.php @@ -8,12 +8,14 @@ if (!defined('AOWOW_REVISION')) class Listview implements \JsonSerializable { - public const MODE_DEFAULT = 0; - public const MODE_CHECKBOX = 1; - public const MODE_DIV = 2; - public const MODE_TILED = 3; - public const MODE_CALENDAR = 4; - public const MODE_FLEXGRID = 5; + public const /* int */ MODE_DEFAULT = 0; + public const /* int */ MODE_CHECKBOX = 1; + public const /* int */ MODE_DIV = 2; + public const /* int */ MODE_TILED = 3; + public const /* int */ MODE_CALENDAR = 4; + public const /* int */ MODE_FLEXGRID = 5; + + public const /* int */ DEFAULT_SIZE = 300; private const TEMPLATES = array( 'achievement' => ['template' => 'achievement', 'id' => 'achievements', 'name' => '$LANG.tab_achievements' ], @@ -125,6 +127,9 @@ class Listview implements \JsonSerializable $this->__addIn = 'template/listviews/'.$addIn.'.tpl'; } + /** + * @return \Generator<int, array> rowIndex => dataRow + */ public function &iterate() : \Generator { reset($this->data); @@ -133,11 +138,22 @@ class Listview implements \JsonSerializable yield $idx => $row; } + public function appendData(array $moreData) : void + { + foreach ($moreData as $md) + $this->data[] = $md; + } + public function getTemplate() : string { return $this->template; } + public function getId() : string + { + return $this->id; + } + public function setTabs(string $tabVar) : void { if ($tabVar[0] !== '$') // expects a jsVar, which we denote with a prefixed $ @@ -146,9 +162,9 @@ class Listview implements \JsonSerializable $this->tabs = $tabVar; } - public function setError() : void + public function setError(bool $enable) : void { - $this->_errors = 1; + $this->_errors = $enable ? 1 : null; } public function jsonSerialize() : array @@ -164,10 +180,11 @@ class Listview implements \JsonSerializable public function __toString() : string { + $addIn = ''; if ($this->__addIn) - include($this->__addIn); + $addIn = file_get_contents($this->__addIn).PHP_EOL; - return "new Listview(".Util::toJSON($this).");\n"; + return $addIn.'new Listview('.Util::toJSON($this).');'.PHP_EOL; } } diff --git a/includes/components/frontend/tabs.class.php b/includes/components/frontend/tabs.class.php index 2563d7a9..1fa14cd1 100644 --- a/includes/components/frontend/tabs.class.php +++ b/includes/components/frontend/tabs.class.php @@ -31,6 +31,9 @@ class Tabs implements \JsonSerializable, \Countable } } + /** + * @return \Generator<int, Listview> tabIndex => Listview + */ public function &iterate() : \Generator { reset($this->__tabs); diff --git a/includes/components/frontend/tooltip.class.php b/includes/components/frontend/tooltip.class.php index 78118900..0042c417 100644 --- a/includes/components/frontend/tooltip.class.php +++ b/includes/components/frontend/tooltip.class.php @@ -19,8 +19,11 @@ class Tooltip implements \JsonSerializable private ?string $buff = null; private ?array $buffspells = null; - public function __construct(private string $__powerTpl, private string $__subject, array $opts = []) + public function __construct(private string $__powerTpl, private int|string $__subject, array $opts = []) { + if (!is_int($this->__subject)) + $this->__subject = Util::toJSON($this->__subject, JSON_UNESCAPED_UNICODE); + foreach ($opts as $k => $v) { if (property_exists($this, $k)) @@ -54,7 +57,7 @@ class Tooltip implements \JsonSerializable public function __toString() : string { - return sprintf($this->__powerTpl, Util::toJSON($this->__subject, JSON_AOWOW_POWER), Lang::getLocale()->value, Util::toJSON($this, JSON_AOWOW_POWER))."\n"; + return sprintf($this->__powerTpl, $this->__subject, Lang::getLocale()->value, Util::toJSON($this, JSON_AOWOW_POWER))."\n"; } } diff --git a/includes/components/guidemgr.class.php b/includes/components/guidemgr.class.php index 3b738404..61a41b5b 100644 --- a/includes/components/guidemgr.class.php +++ b/includes/components/guidemgr.class.php @@ -2,8 +2,6 @@ namespace Aowow; -use GdImage; - if (!defined('AOWOW_REVISION')) die('illegal access'); @@ -47,7 +45,7 @@ class GuideMgr self::$ratingsStore = array_fill_keys($guideIds, ['nvotes' => 0, 'rating' => -1]); - $ratings = DB::Aowow()->select('SELECT `entry` AS ARRAY_KEY, IFNULL(SUM(`value`), 0) AS "0", IFNULL(COUNT(*), 0) AS "1", IFNULL(MAX(IF(`userId` = ?d, `value`, 0)), 0) AS "2" FROM ?_user_ratings WHERE `type` = ?d AND `entry` IN (?a) GROUP BY `entry`', User::$id, RATING_GUIDE, $guideIds); + $ratings = DB::Aowow()->selectAssoc('SELECT `entry` AS ARRAY_KEY, IFNULL(SUM(`value`), 0) AS "0", IFNULL(COUNT(*), 0) AS "1", IFNULL(MAX(IF(`userId` = %i, `value`, 0)), 0) AS "2" FROM ::user_ratings WHERE `type` = %i AND `entry` IN %in GROUP BY `entry`', User::$id, RATING_GUIDE, $guideIds); foreach ($ratings as $id => [$total, $count, $self]) { self::$ratingsStore[$id]['nvotes'] = (int)$count; diff --git a/includes/components/locstring.class.php b/includes/components/locstring.class.php index 53da1fc9..2bfec006 100644 --- a/includes/components/locstring.class.php +++ b/includes/components/locstring.class.php @@ -6,7 +6,7 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); -class LocString +class LocString implements \JsonSerializable { private \WeakMap $store; @@ -24,6 +24,11 @@ class LocString $this->store[$l] = (string)$callback($data[$key.'_loc'.$l->value] ?? ''); } + public function jsonSerialize() : string + { + return $this->__toString(); + } + public function __toString() : string { if ($str = $this->store[Lang::getLocale()]) diff --git a/includes/components/pagetemplate.class.php b/includes/components/pagetemplate.class.php index b1360f23..59c3d50b 100644 --- a/includes/components/pagetemplate.class.php +++ b/includes/components/pagetemplate.class.php @@ -27,12 +27,14 @@ class PageTemplate private array $pageData = []; // processed by display hooks // template data that needs further processing .. ! WARNING ! they will not get aut fetched from $context as they are already defined here - private string $gStaticUrl; - private string $gHost; - private string $gServerTime; - private ?string $analyticsTag = null; - private bool $consentFooter = false; - private string $dbProfiles = ''; + private string $gStaticUrl; + private string $gHost; + private string $gServerTime; + private string $gUser; + private string $gFavorites; + private bool $hasAnalytics = false; + private bool $consentFooter = false; + private string $dbProfiles = ''; private readonly string $user; // becomes User object @@ -44,12 +46,10 @@ class PageTemplate public function __construct(private string $template, private ?\Aowow\TemplateResponse $context = null) { - $this->locale = Lang::getLocale(); - $this->gStaticUrl = Cfg::get('STATIC_URL'); - $this->gHost = Cfg::get('HOST_URL'); - $this->analyticsTag = Cfg::get('GTAG_MEASUREMENT_ID'); - $this->gServerTime = sprintf("new Date('%s')", date(Util::$dateFormatInternal)); - $this->user = User::class; + $this->locale = Lang::getLocale(); + $this->user = User::class; + + self::__wakeup(); // init non-cached properties; } public function addDataLoader(string ...$dataFile) : void @@ -69,8 +69,6 @@ class PageTemplate default => '' }; - if (!$tpl || !$str) - if (!$str) { trigger_error('PageTemplate::addScript - content empty', E_USER_WARNING); @@ -209,27 +207,27 @@ class PageTemplate return Cfg::get($name); } - private function json(mixed $var, int $jsonFlags = 0x0) : string + private function json(mixed $var, int $jsonFlags = 0x0, bool $varRef = false) : string { - if (is_string($var) && $this->$var) - $var = $this->$var; + if (!is_string($var)) + return preg_replace('/script\s*\>/i', 'scr"+"ipt>', Util::toJSON($var, $jsonFlags) ?: "{}"); - return preg_replace('/script\s*\>/i', 'scr"+"ipt>', Util::toJSON($var, $jsonFlags)); + return preg_replace('/script\s*\>/i', 'scr"+"ipt>', Util::toJSON($varRef ? $this->$var : $var, $jsonFlags) ?: "{}"); } - private function escHTML(string $var) : string|array + private function escHTML(string $var, bool $varRef = false) : string|array { - return Util::htmlEscape($this->$var ?? $var); + return Util::htmlEscape($varRef ? $this->$var : $var); } - private function escJS(string $var) : string|array + private function escJS(string $var, bool $varRef = false) : string|array { - return Util::jsEscape($this->$var ?? $var); + return Util::jsEscape($varRef ? $this->$var : $var); } - private function ucFirst(string $var) : string + private function ucFirst(string $var, bool $varRef = false) : string { - return Util::ucFirst($this->$var ?? $var); + return Util::ucFirst($varRef ? $this->$var : $var); } @@ -336,7 +334,7 @@ class PageTemplate $result[] = "var fi_type = '".$this->filter->fiType."'"; if ($this->filter->fiSetCriteria) // arr:criteria, arr:signs, arr:values - $result[] = 'fi_setCriteria('.mb_substr(Util::toJSON(array_values($this->filter->fiSetCriteria)), 1, -1).");"; + $result[] = 'fi_setCriteria('.mb_substr(Util::toJSON($this->filter->fiSetCriteria), 1, -1).");"; /* nt: don't try to match provided weights on predefined weight sets (preselects preset from opt list and ..?) @@ -469,17 +467,21 @@ class PageTemplate // refresh vars that shouldn't be cached private function update() : void { - // analytics + consent - if (!isset($_COOKIE['consent'])) + // not set, but should be + if (!isset($_COOKIE['consent']) && $this->hasAnalytics) { - $this->addScript(SC_CSS_FILE, 'css/consent.css'); - $this->addScript(SC_JS_FILE, 'js/consent.js'); + $this->addScript(SC_CSS_FILE, 'css/consent.css', SC_FLAG_NOCACHE); + $this->addScript(SC_JS_FILE, 'js/consent.js', SC_FLAG_NOCACHE); $this->consentFooter = true; - $this->analyticsTag = null; } - else if ($this->analyticsTag && !$_COOKIE['consent']) - $this->analyticsTag = null; + else + $this->consentFooter = false; + + // analytics + consent + // not set or declined + if (empty($_COOKIE['consent'])) + $this->hasAnalytics = false; // js + css $this->prepareScripts(); @@ -496,7 +498,7 @@ class PageTemplate foreach ($this->lvTabs->iterate() as $lv) if ($lv instanceof \Aowow\Listview) - $lv->setError(); + $lv->setError(true); } // pre-serialization: if a var is relevant it was stored in $rawData @@ -505,6 +507,23 @@ class PageTemplate $this->context = null; // unlink from TemplateResponse $this->pageData = []; // clear modified data + unset( // must be recreated on __wakeup + $this->gStaticUrl, + $this->gHost, + $this->hasAnalytics, + $this->gServerTime, + $this->gUser, + $this->gFavorites + ); + + if ($this->lvTabs) // do not store lvErrors in cache + foreach ($this->lvTabs->iterate() as $lv) + if ($lv instanceof \Aowow\Listview) + $lv->setError(false); + + // clear out scripts flagged as non-caching + $this->scripts = array_filter($this->scripts, fn($x) => !($x[2] & SC_FLAG_NOCACHE)); + $vars = []; foreach ($this as $k => $_) $vars[] = $k; @@ -516,8 +535,10 @@ class PageTemplate { $this->gStaticUrl = Cfg::get('STATIC_URL'); $this->gHost = Cfg::get('HOST_URL'); - $this->analyticsTag = Cfg::get('GTAG_MEASUREMENT_ID'); + $this->hasAnalytics = !!Cfg::get('GTAG_MEASUREMENT_ID'); $this->gServerTime = sprintf("new Date('%s')", date(Util::$dateFormatInternal)); + $this->gUser = Util::toJSON(User::getUserGlobal()); + $this->gFavorites = Util::toJSON(User::getFavorites()); } public function __set(string $var, mixed $value) : void @@ -536,7 +557,7 @@ class PageTemplate if (!$this->context) return null; - if (!property_exists($this->context, $var)) + if (!isset(get_object_vars($this->context)[$var])) return null; $this->rawData[$var] = $this->context->$var; diff --git a/includes/components/profiler.class.php b/includes/components/profiler.class.php index 6d18ec7d..e3c4d452 100644 --- a/includes/components/profiler.class.php +++ b/includes/components/profiler.class.php @@ -8,10 +8,20 @@ if (!defined('AOWOW_REVISION')) class Profiler { - public const PID_FILE = 'config/pr-queue-pid'; - public const CHAR_GMFLAGS = 0x1 | 0x8 | 0x10 | 0x20; // PLAYER_EXTRA_ :: GM_ON | TAXICHEAT | GM_INVISIBLE | GM_CHAT + public const /* string */ PID_FILE = 'config/pr-queue-pid'; + public const /* int */ CHAR_GMFLAGS = 0x1 | 0x8 | 0x10 | 0x20; // PLAYER_EXTRA_ :: GM_ON | TAXICHEAT | GM_INVISIBLE | GM_CHAT - public const REGIONS = array( // see cfg_categories.dbc + public const /* int */ FETCH_RESULT_OK = 1; + public const /* int */ FETCH_RESULT_OK_UNCHANGED = 2; + public const /* int */ FETCH_RESULT_ERR_NOT_FOUND = 3; + public const /* int */ FETCH_RESULT_ERR_NAME_EMPTY = 4; + public const /* int */ FETCH_RESULT_ERR_NO_MEMBERS = 5; + public const /* int */ FETCH_RESULT_ERR_INTERNAL = 6; + + public const /* int */ COMPLETION_EXCLUDE = 1; + public const /* int */ COMPLETION_INCLUDE = 2; + + public const /* array */ REGIONS = array( // see cfg_categories.dbc 'us' => [2, 3, 4, 5], // US (us, oceanic, latin america, americas - tournament) 'kr' => [6, 7], // KR (kr, tournament) 'eu' => [8, 9, 10, 11, 12, 13], // EU (english, german, french, spanish, russian, eu - tournament) @@ -20,9 +30,9 @@ class Profiler 'dev' => [1, 26, 27, 28, 30] // Development, Test Server, Test Server - tournament, QA Server, Test Server 2 ); - private static $realms = []; + private static array $realms = []; - public static $slot2InvType = array( + public static array $slot2InvType = array( 1 => [INVTYPE_HEAD], // head 2 => [INVTYPE_NECK], // neck 3 => [INVTYPE_SHOULDERS], // shoulder @@ -44,7 +54,7 @@ class Profiler 19 => [INVTYPE_TABARD], // tabard ); - public static $raidProgression = array( // statisticAchievement => relevantCriterium ; don't forget to enable this in /js/Profiler.js as well + public static array $raidProgression = array( // statisticAchievement => relevantCriterium ; don't forget to enable this in /js/Profiler.js as well 1361 => 5100, 1362 => 5101, 1363 => 5102, 1365 => 5104, 1366 => 5108, 1364 => 5110, 1369 => 5112, 1370 => 5113, 1371 => 5114, 1372 => 5117, 1373 => 5119, 1374 => 5120, 1375 => 7805, 1376 => 5122, 1377 => 5123, // Naxxramas 10 1367 => 5103, 1368 => 5111, 1378 => 5124, 1379 => 5125, 1380 => 5126, 1381 => 5127, 1382 => 5128, 1383 => 7806, 1384 => 5130, 1385 => 5131, 1386 => 5132, 1387 => 5133, 1388 => 5134, 1389 => 5135, 1390 => 5136, // Naxxramas 25 2856 => 9938, 2857 => 9939, 2858 => 9940, 2859 => 9941, 2861 => 9943, 2865 => 9947, 2866 => 9948, 2868 => 9950, 2869 => 9951, 2870 => 9952, 2863 => 10558, 2864 => 10559, 2862 => 10560, 2867 => 10565, 2860 => 10580, // Ulduar 10 @@ -65,17 +75,17 @@ class Profiler 4821 => 13466, // Ruby Sanctum 10 nh ); - public static function getBuyoutForItem($itemId) + public static function getBuyoutForItem(int $itemId) : int { if (!$itemId) return 0; // try, when having filled char-DB at hand - // return DB::Characters()->selectCell('SELECT SUM(a.buyoutprice) / SUM(ii.count) FROM auctionhouse a JOIN item_instance ii ON ii.guid = a.itemguid WHERE ii.itemEntry = ?d', $itemId); + // return DB::Characters()->selectCell('SELECT SUM(a.buyoutprice) / SUM(ii.count) FROM auctionhouse a JOIN item_instance ii ON ii.guid = a.itemguid WHERE ii.itemEntry = %i', $itemId); return 0; } - public static function queueStart(&$msg = '') + public static function queueStart(?string &$msg = '') : bool { $queuePID = self::queueStatus(); @@ -100,7 +110,7 @@ class Profiler } } - public static function queueStatus() + public static function queueStatus() : int { if (!file_exists(self::PID_FILE)) return 0; @@ -117,7 +127,7 @@ class Profiler return 0; } - public static function queueLock($pid) + public static function queueLock(int $pid) : bool { $queuePID = self::queueStatus(); if ($queuePID && $queuePID != $pid) @@ -139,12 +149,12 @@ class Profiler return $ok; } - public static function queueFree() + public static function queueFree() : void { unlink(self::PID_FILE); } - public static function urlize($str, $allowLocales = false, $profile = false) + public static function urlize(string $str, bool $allowLocales = false, bool $profile = false) : string { $search = ['<', '>', ' / ', "'"]; $replace = ['<', '>', '-', '' ]; @@ -194,7 +204,7 @@ class Profiler if (!DB::isConnectable(DB_AUTH) || self::$realms) return self::$realms; - self::$realms = DB::Auth()->select( + $realms = DB::Auth()->selectAssoc( 'SELECT `id` AS ARRAY_KEY, `name`, CASE WHEN `timezone` BETWEEN 2 AND 5 THEN "us" # US, Oceanic, Latin America, Americas-Tournament @@ -205,18 +215,18 @@ class Profiler ELSE "dev" END AS "region", # 1: Dev, 26: Test, 27: Test Tournament, 28: QA, 30: Test2, 31+: misc `allowedSecurityLevel` AS "access" FROM `realmlist` - WHERE `gamebuild` = ?d', + WHERE `gamebuild` = %i', WOW_BUILD ); - foreach (self::$realms as $rId => &$rData) + if (!$realms) + return []; + + foreach ($realms as $rId => $rData) { // realm in db but no connection info set if (!DB::isConnectable(DB_CHARACTERS . $rId)) - { - unset(self::$realms[$rId]); continue; - } // filter by access level if ($rData['access'] == SEC_ADMINISTRATOR && (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN))) @@ -226,10 +236,7 @@ class Profiler else if ($rData['access'] == SEC_MODERATOR && (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_MOD | U_GROUP_BUREAU))) $rData['access'] = U_GROUP_DEV | U_GROUP_ADMIN | U_GROUP_MOD | U_GROUP_BUREAU; else if ($rData['access'] > SEC_PLAYER && !CLI) - { - unset(self::$realms[$rId]); continue; - } // filter dev realms if ($rData['region'] === 'dev') @@ -237,11 +244,10 @@ class Profiler if (CLI || User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) $rData['access'] = U_GROUP_DEV | U_GROUP_ADMIN; else - { - unset(self::$realms[$rId]); continue; - } } + + self::$realms[$rId] = $rData; } return self::$realms; @@ -251,43 +257,43 @@ class Profiler { self::getRealms(); - // sort depends on encountered order in `logon`.`realmlist`. Is that a problem? + // sort depends on encountered order in `auth`.`realmlist`. Is that a problem? return array_unique(array_column(self::$realms, 'region')); } - private static function queueInsert($realmId, $guid, $type, $localId) + private static function queueInsert(int $realmId, int $guid, int $type, int $localId) : void { - if ($rData = DB::Aowow()->selectRow('SELECT `requestTime` AS "time", `status` FROM ?_profiler_sync WHERE `realm` = ?d AND `realmGUID` = ?d AND `type` = ?d AND `typeId` = ?d AND `status` <> ?d', $realmId, $guid, $type, $localId, PR_QUEUE_STATUS_WORKING)) + if ($rData = DB::Aowow()->selectRow('SELECT `requestTime` AS "time", `status` FROM ::profiler_sync WHERE `realm` = %i AND `realmGUID` = %i AND `type` = %i AND `typeId` = %i AND `status` <> %i', $realmId, $guid, $type, $localId, PR_QUEUE_STATUS_WORKING)) { // not on already scheduled - recalc time and set status to PR_QUEUE_STATUS_WAITING if ($rData['status'] != PR_QUEUE_STATUS_WAITING) { $newTime = Cfg::get('DEBUG') ? time() : max($rData['time'] + Cfg::get('PROFILER_RESYNC_DELAY'), time()); - DB::Aowow()->query('UPDATE ?_profiler_sync SET `requestTime` = ?d, `status` = ?d, `errorCode` = 0 WHERE `realm` = ?d AND `realmGUID` = ?d AND `type` = ?d AND `typeId` = ?d', $newTime, PR_QUEUE_STATUS_WAITING, $realmId, $guid, $type, $localId); + DB::Aowow()->qry('UPDATE ::profiler_sync SET `requestTime` = %i, `status` = %i, `errorCode` = 0 WHERE `realm` = %i AND `realmGUID` = %i AND `type` = %i AND `typeId` = %i', $newTime, PR_QUEUE_STATUS_WAITING, $realmId, $guid, $type, $localId); } } else - DB::Aowow()->query('REPLACE INTO ?_profiler_sync (`realm`, `realmGUID`, `type`, `typeId`, `requestTime`, `status`, `errorCode`) VALUES (?d, ?d, ?d, ?d, UNIX_TIMESTAMP(), ?d, 0)', $realmId, $guid, $type, $localId, PR_QUEUE_STATUS_WAITING); + DB::Aowow()->qry('REPLACE INTO ::profiler_sync (`realm`, `realmGUID`, `type`, `typeId`, `requestTime`, `status`, `errorCode`) VALUES (%i, %i, %i, %i, UNIX_TIMESTAMP(), %i, 0)', $realmId, $guid, $type, $localId, PR_QUEUE_STATUS_WAITING); } - public static function scheduleResync($type, $realmId, $guid) + public static function scheduleResync(int $type, int $realmId, int $guid) : int { $newId = 0; switch ($type) { case Type::PROFILE: - if ($newId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $guid)) + if ($newId = DB::Aowow()->selectCell('SELECT `id` FROM ::profiler_profiles WHERE `realm` = %i AND `realmGUID` = %i', $realmId, $guid)) self::queueInsert($realmId, $guid, Type::PROFILE, $newId); break; case Type::GUILD: - if ($newId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_guild WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $guid)) + if ($newId = DB::Aowow()->selectCell('SELECT `id` FROM ::profiler_guild WHERE `realm` = %i AND `realmGUID` = %i', $realmId, $guid)) self::queueInsert($realmId, $guid, Type::GUILD, $newId); break; case Type::ARENA_TEAM: - if ($newId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_arena_team WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $guid)) + if ($newId = DB::Aowow()->selectCell('SELECT `id` FROM ::profiler_arena_team WHERE `realm` = %i AND `realmGUID` = %i', $realmId, $guid)) self::queueInsert($realmId, $guid, Type::ARENA_TEAM, $newId); break; @@ -337,16 +343,18 @@ class Profiler else { // error out all profiles with status WORKING, that are older than 60sec - DB::Aowow()->query('UPDATE ?_profiler_sync SET `status` = ?d, `errorCode` = ?d WHERE `status` = ?d AND `requestTime` < ?d', PR_QUEUE_STATUS_ERROR, PR_QUEUE_ERROR_UNK, PR_QUEUE_STATUS_WORKING, time() - MINUTE); + DB::Aowow()->qry('UPDATE ::profiler_sync SET `status` = %i, `errorCode` = %i WHERE `status` = %i AND `requestTime` < %i', PR_QUEUE_STATUS_ERROR, PR_QUEUE_ERROR_UNK, PR_QUEUE_STATUS_WORKING, time() - MINUTE); - $subjectStatus = DB::Aowow()->select('SELECT `typeId` AS ARRAY_KEY, `status`, `realm`, `errorCode` FROM ?_profiler_sync WHERE `type` = ?d AND `typeId` IN (?a)', $type, $subjectGUIDs); - $queue = DB::Aowow()->selectCol('SELECT CONCAT(`type`, ":", `typeId`) FROM ?_profiler_sync WHERE `status` = ?d AND `requestTime` < UNIX_TIMESTAMP() ORDER BY `requestTime` ASC', PR_QUEUE_STATUS_WAITING); + $subjectStatus = DB::Aowow()->selectAssoc('SELECT `typeId` AS ARRAY_KEY, `status`, `realm`, `errorCode`, `requestTime` FROM ::profiler_sync WHERE `type` = %i AND `typeId` IN %in', $type, $subjectGUIDs); + $queue = DB::Aowow()->selectCol('SELECT CONCAT(`type`, ":", `typeId`) FROM ::profiler_sync WHERE `status` = %i AND `requestTime` < UNIX_TIMESTAMP() ORDER BY `requestTime` ASC', PR_QUEUE_STATUS_WAITING); foreach ($subjectGUIDs as $guid) { if (empty($subjectStatus[$guid])) // whelp, thats some error.. $response[] = [PR_QUEUE_STATUS_ERROR, 0, 0, PR_QUEUE_ERROR_UNK]; else if ($subjectStatus[$guid]['status'] == PR_QUEUE_STATUS_ERROR) $response[] = [PR_QUEUE_STATUS_ERROR, 0, 0, $subjectStatus[$guid]['errorCode']]; + else if ($subjectStatus[$guid]['requestTime'] > time()) + $response[] = [PR_QUEUE_STATUS_READY, 0, 0, 0]; else $response[] = array( $subjectStatus[$guid]['status'], @@ -361,22 +369,19 @@ class Profiler return Util::toJSON($response); } - public static function getCharFromRealm($realmId, $charGuid) + public static function getCharFromRealm(int $realmId, int $charGuid) : int { - $char = DB::Characters($realmId)->selectRow('SELECT c.* FROM characters c WHERE c.`guid` = ?d', $charGuid); + $char = DB::Characters($realmId)->selectRow('SELECT c.* FROM characters c WHERE c.`guid` = %i', $charGuid); if (!$char) - return false; + return self::FETCH_RESULT_ERR_NOT_FOUND; if (!$char['name']) - { - trigger_error('char #'.$charGuid.' on realm #'.$realmId.' has empty name. skipping...', E_USER_WARNING); - return false; - } + return self::FETCH_RESULT_ERR_NAME_EMPTY; // reminder: this query should not fail: a placeholder entry is created as soon as a char listview is created or profile detail page is called - $profile = DB::Aowow()->selectRow('SELECT `id`, `lastupdated` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $char['guid']); + $profile = DB::Aowow()->selectRow('SELECT `id`, `lastupdated` FROM ::profiler_profiles WHERE `realm` = %i AND `realmGUID` = %i', $realmId, $char['guid']); if (!$profile) - return false; // well ... it failed + return self::FETCH_RESULT_ERR_INTERNAL; // well ... it failed $profileId = $profile['id']; @@ -384,9 +389,8 @@ class Profiler if (!$char['online'] && $char['logout_time'] <= $profile['lastupdated']) { - DB::Aowow()->query('UPDATE ?_profiler_profiles SET `lastupdated` = ?d WHERE `id` = ?d', time(), $profileId); - CLI::write('char did not log in since last update. skipping...'); - return true; + DB::Aowow()->qry('UPDATE ::profiler_profiles SET `lastupdated` = %i WHERE `id` = %i', time(), $profileId); + return self::FETCH_RESULT_OK_UNCHANGED; } CLI::write('writing...'); @@ -411,8 +415,8 @@ class Profiler */ - DB::Aowow()->query('DELETE FROM ?_profiler_items WHERE `id` = ?d', $profileId); - $items = DB::Characters($realmId)->select('SELECT ci.`slot` AS ARRAY_KEY, ii.`itemEntry`, ii.`enchantments`, ii.`randomPropertyId` FROM character_inventory ci JOIN item_instance ii ON ci.`item` = ii.`guid` WHERE ci.`guid` = ?d AND `bag` = 0 AND `slot` BETWEEN 0 AND 18', $char['guid']); + DB::Aowow()->qry('DELETE FROM ::profiler_items WHERE `id` = %i', $profileId); + $items = DB::Characters($realmId)->selectAssoc('SELECT ci.`slot` AS ARRAY_KEY, ii.`itemEntry`, ii.`enchantments`, ii.`randomPropertyId` FROM character_inventory ci JOIN item_instance ii ON ci.`item` = ii.`guid` WHERE ci.`guid` = %i AND `bag` = 0 AND `slot` BETWEEN 0 AND 18', $char['guid']); $gemItems = []; $permEnch = []; @@ -429,7 +433,7 @@ class Profiler if ($gEnch) { - $gi = DB::Aowow()->selectCol('SELECT `gemEnchantmentId` AS ARRAY_KEY, `id` FROM ?_items WHERE `class` = ?d AND `gemEnchantmentId` IN (?a)', ITEM_CLASS_GEM, $gEnch); + $gi = DB::Aowow()->selectCol('SELECT `gemEnchantmentId` AS ARRAY_KEY, `id` FROM ::items WHERE `class` = %i AND `gemEnchantmentId` IN %in', ITEM_CLASS_GEM, $gEnch); foreach ($gEnch as $eId) { if (isset($gemItems[$eId])) @@ -461,7 +465,7 @@ class Profiler 'gem4' => 0 // serverside items cant have more than 3 sockets. (custom profile thing) ); - DB::Aowow()->query('INSERT INTO ?_profiler_items (?#) VALUES (?a)', array_keys($data), array_values($data)); + DB::Aowow()->qry('INSERT INTO ::profiler_items %v', $data); } CLI::write(' ..inventory'); @@ -485,7 +489,7 @@ class Profiler 'hairstyle' => $char['hairStyle'], 'haircolor' => $char['hairColor'], 'features' => $char['facialStyle'], // maybe facetype - 'title' => $char['chosenTitle'] ? DB::Aowow()->selectCell('SELECT `id` FROM ?_titles WHERE `bitIdx` = ?d', $char['chosenTitle']) : 0, + 'title' => $char['chosenTitle'] ? DB::Aowow()->selectCell('SELECT `id` FROM ::titles WHERE `bitIdx` = %i', $char['chosenTitle']) : 0, 'playedtime' => $char['totaltime'], 'nomodelMask' => ($char['playerFlags'] & 0x400 ? (1 << SLOT_HEAD) : 0) | ($char['playerFlags'] & 0x800 ? (1 << SLOT_BACK) : 0), 'talenttree1' => 0, @@ -505,8 +509,12 @@ class Profiler // char is flagged for rename if ($char['at_login'] & 0x1) { - $ri = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND `name` = ?', $realmId, $char['name']); - $data['renameItr'] = $ri ? ++$ri : 1; + if ($ri = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ::profiler_profiles WHERE `realm` = %i AND `realmGUID` = %i', $realmId, $charGuid)) + $data['renameItr'] = $ri; + else if ($ri = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ::profiler_profiles WHERE `realm` = %i AND `custom` = 0 AND `name` = %s', $realmId, $char['name'])) + $data['renameItr'] = ++$ri; + else + $data['renameItr'] = 1; } @@ -514,14 +522,14 @@ class Profiler /* talents + glyphs */ /********************/ - $t = DB::Characters($realmId)->selectCol('SELECT `talentGroup` AS ARRAY_KEY, `spell` AS ARRAY_KEY2, `spell` FROM character_talent WHERE `guid` = ?d', $char['guid']); - $g = DB::Characters($realmId)->select('SELECT `talentGroup` AS ARRAY_KEY, `glyph1` AS "g1", `glyph2` AS "g4", `glyph3` AS "g5", `glyph4` AS "g2", `glyph5` AS "g3", `glyph6` AS "g6" FROM character_glyphs WHERE `guid` = ?d', $char['guid']); + $t = DB::Characters($realmId)->selectCol('SELECT `talentGroup` AS ARRAY_KEY, `spell` AS ARRAY_KEY2, `spell` FROM character_talent WHERE `guid` = %i', $char['guid']); + $g = DB::Characters($realmId)->selectAssoc('SELECT `talentGroup` AS ARRAY_KEY, `glyph1` AS "g1", `glyph2` AS "g4", `glyph3` AS "g5", `glyph4` AS "g2", `glyph5` AS "g3", `glyph6` AS "g6" FROM character_glyphs WHERE `guid` = %i', $char['guid']); for ($i = 0; $i < 2; $i++) { // talents for ($j = 0; $j < 3; $j++) { - $_ = DB::Aowow()->selectCol('SELECT `spell` AS ARRAY_KEY, MAX(IF(`spell` IN (?a), `rank`, 0)) FROM ?_talents WHERE `class` = ?d AND `tab` = ?d GROUP BY `id` ORDER BY `row`, `col` ASC', $t[$i] ?? [0], $cl->value, $j); + $_ = DB::Aowow()->selectCol('SELECT `spell` AS ARRAY_KEY, MAX(IF(`spell` IN %in, `rank`, 0)) FROM ::talents WHERE `class` = %i AND `tab` = %i GROUP BY `id` ORDER BY `row`, `col` ASC', $t[$i] ?? [0], $cl->value, $j); $data['talentbuild'.($i + 1)] .= implode('', $_); if ($data['activespec'] == $i) $data['talenttree'.($j + 1)] = array_sum($_); @@ -539,10 +547,10 @@ class Profiler { $gItems = DB::Aowow()->selectCol( 'SELECT i.`id` - FROM ?_glyphproperties gp - JOIN ?_spell s ON s.`effect1MiscValue` = gp.`id` AND s.`effect1Id` = ?d - JOIN ?_items i ON i.`class` = ?d AND i.`spellId1` = s.`id` AND (i.`cuFlags` & ?d) = 0 - WHERE gp.`id` IN (?a)', + FROM ::glyphproperties gp + JOIN ::spell s ON s.`effect1MiscValue` = gp.`id` AND s.`effect1Id` = %i + JOIN ::items i ON i.`class` = %i AND i.`spellId1` = s.`id` AND (i.`cuFlags` & %i) = 0 + WHERE gp.`id` IN %in', SPELL_EFFECT_APPLY_GLYPH, ITEM_CLASS_GLYPH, CUSTOM_DISABLED | CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW, $gProps ); @@ -580,7 +588,7 @@ class Profiler // enchantId => multiple spells => multiple items with varying itemlevels, quality, whatevs // cant reasonably get to the castItem from enchantId and slot - $profSpec = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `skillLevel` AS "1", `skillLine` AS "0" FROM ?_itemenchantment WHERE `id` IN (?a)', $permEnch); + $profSpec = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `skillLevel` AS "1", `skillLine` AS "0" FROM ::itemenchantment WHERE `id` IN %in', $permEnch); foreach ($permEnch as $slot => $eId) { if (!isset($profSpec[$eId])) @@ -607,23 +615,38 @@ class Profiler if ($cl == ChrClass::HUNTER) { - DB::Aowow()->query('DELETE FROM ?_profiler_pets WHERE `owner` = ?d', $profileId); - $pets = DB::Characters($realmId)->select('SELECT `id` AS ARRAY_KEY, `entry`, `modelId`, `name` FROM character_pet WHERE `owner` = ?d', $charGuid); + DB::Aowow()->qry('DELETE FROM ::profiler_pets WHERE `owner` = %i', $profileId); + $pets = DB::Characters($realmId)->selectAssoc('SELECT `id` AS ARRAY_KEY, `entry`, `modelId`, `name` FROM character_pet WHERE `owner` = %i', $charGuid); foreach ($pets as $petGuid => $petData) { - $petSpells = DB::Characters($realmId)->selectCol('SELECT `spell` FROM pet_spell WHERE `guid` = ?d', $petGuid); + $petSpells = DB::Characters($realmId)->selectCol('SELECT `spell` FROM pet_spell WHERE `guid` = %i', $petGuid); $morePet = DB::Aowow()->selectRow( 'SELECT IFNULL(c3.`id`, IFNULL(c2.`id`, IFNULL(c1.`id`, c.`id`))) AS "entry", p.`type`, c.`family` - FROM ?_pet p - JOIN ?_creature c ON c.`family` = p.`id` - LEFT JOIN ?_creature c1 ON c1.`difficultyEntry1` = c.`id` - LEFT JOIN ?_creature c2 ON c2.`difficultyEntry2` = c.`id` - LEFT JOIN ?_creature c3 ON c3.`difficultyEntry3` = c.`id` - WHERE c.`id` = ?d', + FROM ::pet p + JOIN ::creature c ON c.`family` = p.`id` + LEFT JOIN ::creature c1 ON c1.`difficultyEntry1` = c.`id` + LEFT JOIN ::creature c2 ON c2.`difficultyEntry2` = c.`id` + LEFT JOIN ::creature c3 ON c3.`difficultyEntry3` = c.`id` + WHERE c.`id` = %i', $petData['entry'] ); - $_ = DB::Aowow()->selectCol('SELECT `spell` AS ARRAY_KEY, MAX(IF(`spell` IN (?a), `rank`, 0)) FROM ?_talents WHERE `class` = 0 AND `petTypeMask` = ?d GROUP BY `row`, `col` ORDER BY `row`, `col` ASC', $petSpells ?: [0], 1 << $morePet['type']); + if (!$morePet) + { + trigger_error('char #'.$charGuid.' on realm #'.$realmId.' owns pet #'.$petGuid.' (creature entry: #'.$petData['entry'].') without pet family. skipping...', E_USER_WARNING); + continue; + } + + $_ = DB::Aowow()->selectCol( + 'SELECT IFNULL(t2.`rank`, 0) + FROM ::talents t1 + LEFT JOIN (SELECT `id`, `rank` FROM ::talents WHERE `spell` IN %in) t2 ON t2.`id` = t1.`id` + WHERE `class` = 0 AND `petTypeMask` = %i + GROUP BY t1.`id` + ORDER BY t1.`row`, t1.`col`, t1.`id` ASC', + $petSpells ?: [0], 1 << $morePet['type'] + ); + $pet = array( 'id' => $petGuid, 'owner' => $profileId, @@ -634,7 +657,7 @@ class Profiler 'talents' => implode('', $_) ); - DB::Aowow()->query('INSERT INTO ?_profiler_pets (?#) VALUES (?a)', array_keys($pet), array_values($pet)); + DB::Aowow()->qry('INSERT INTO ::profiler_pets %v', $pet); } CLI::write(' ..hunter pets'); @@ -647,112 +670,115 @@ class Profiler // done quests // - DB::Aowow()->query('DELETE FROM ?_profiler_completion_quests WHERE `id` = ?d', $profileId); + DB::Aowow()->qry('DELETE FROM ::profiler_completion_quests WHERE `id` = %i', $profileId); - if ($quests = DB::Characters($realmId)->select('SELECT ?d AS `id`, `quest` AS `questId` FROM character_queststatus_rewarded WHERE `guid` = ?d', $profileId, $char['guid'])) - foreach (Util::createSqlBatchInsert($quests) as $q) - DB::Aowow()->query('INSERT INTO ?_profiler_completion_quests (?#) VALUES '.$q, array_keys($quests[0])); + if ($quests = DB::Characters($realmId)->selectCol('SELECT `quest` FROM character_queststatus_rewarded WHERE `guid` = %i', $char['guid'])) + DB::Aowow()->qry('INSERT INTO ::profiler_completion_quests %m', array( + 'id' => array_fill(0, count($quests), $profileId), + 'questId' => $quests + )); CLI::write(' ..quests'); // known skills (professions only) // - DB::Aowow()->query('DELETE FROM ?_profiler_completion_skills WHERE `id` = ?d', $profileId); + DB::Aowow()->qry('DELETE FROM ::profiler_completion_skills WHERE `id` = %i', $profileId); - $skAllowed = DB::Aowow()->selectCol('SELECT `id` FROM ?_skillline WHERE `typeCat` IN (9, 11) AND (`cuFlags` & ?d) = 0', CUSTOM_EXCLUDE_FOR_LISTVIEW); - $skills = DB::Characters($realmId)->select('SELECT ?d AS `id`, `skill` AS `skillId`, `value`, `max` FROM character_skills WHERE `guid` = ?d AND `skill` IN (?a)', $profileId, $char['guid'], $skAllowed); - $racials = DB::Aowow()->select('SELECT `effect1MiscValue` AS ARRAY_KEY, `effect1DieSides` + `effect1BasePoints` AS qty, `reqRaceMask`, `reqClassMask` FROM ?_spell WHERE `typeCat` = -4 AND `effect1Id` = ?d AND `effect1AuraId` = ?d', SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_SKILL_TALENT); - - foreach ($skills as &$sk) // apply racial profession bonuses + $skAllowed = DB::Aowow()->selectCol('SELECT `id` FROM ::skillline WHERE `typeCat` IN (9, 11) AND (`cuFlags` & %i) = 0', CUSTOM_EXCLUDE_FOR_LISTVIEW); + if ($skills = DB::Characters($realmId)->selectAssoc('SELECT `skill`, `value`, `max` FROM character_skills WHERE `guid` = %i AND `skill` IN %in', $char['guid'], $skAllowed)) { - if (!isset($racials[$sk['skillId']])) - continue; + $racials = DB::Aowow()->selectAssoc('SELECT `effect1MiscValue` AS ARRAY_KEY, `effect1DieSides` + `effect1BasePoints` AS qty, `reqRaceMask`, `reqClassMask` FROM ::spell WHERE `typeCat` = -4 AND `effect1Id` = %i AND `effect1AuraId` = %i', SPELL_EFFECT_APPLY_AURA, SPELL_AURA_MOD_SKILL_TALENT); - $r = $racials[$sk['skillId']]; - if ($ra->matches($r['reqRaceMask']) && $cl->matches($r['reqClassMask'])) + foreach ($skills as &$sk) // apply racial profession bonuses { - $sk['value'] += $r['qty']; - $sk['max'] += $r['qty']; - } - } - unset($sk); + if (!isset($racials[$sk['skill']])) + continue; - if ($skills) - foreach (Util::createSqlBatchInsert($skills) as $sk) - DB::Aowow()->query('INSERT INTO ?_profiler_completion_skills (?#) VALUES '.$sk, array_keys($skills[0])); + $r = $racials[$sk['skill']]; + if ($ra->matches($r['reqRaceMask']) && $cl->matches($r['reqClassMask'])) + { + $sk['value'] += $r['qty']; + $sk['max'] += $r['qty']; + } + } + unset($sk); + + DB::Aowow()->qry('INSERT INTO ::profiler_completion_skills %m', array( + 'id' => array_fill(0, count($skills), $profileId), + 'skillId' => array_column($skills, 'skill'), + 'value' => array_column($skills, 'value'), + 'max' => array_column($skills, 'max') + )); + } CLI::write(' ..professions'); // reputation // - DB::Aowow()->query('DELETE FROM ?_profiler_completion_reputation WHERE `id` = ?d', $profileId); + DB::Aowow()->qry('DELETE FROM ::profiler_completion_reputation WHERE `id` = %i', $profileId); // get base values for this race/class $reputation = []; $baseRep = DB::Aowow()->selectCol( - 'SELECT `id` AS ARRAY_KEY, `baseRepValue1` FROM aowow_factions WHERE `baseRepValue1` AND (`baseRepRaceMask1` & ?d OR (`baseRepClassMask1` AND NOT `baseRepRaceMask1`)) AND ((`baseRepClassMask1` & ?d) OR NOT `baseRepClassMask1`) UNION - SELECT `id` AS ARRAY_KEY, `baseRepValue2` FROM aowow_factions WHERE `baseRepValue2` AND (`baseRepRaceMask2` & ?d OR (`baseRepClassMask2` AND NOT `baseRepRaceMask2`)) AND ((`baseRepClassMask2` & ?d) OR NOT `baseRepClassMask2`) UNION - SELECT `id` AS ARRAY_KEY, `baseRepValue3` FROM aowow_factions WHERE `baseRepValue3` AND (`baseRepRaceMask3` & ?d OR (`baseRepClassMask3` AND NOT `baseRepRaceMask3`)) AND ((`baseRepClassMask3` & ?d) OR NOT `baseRepClassMask3`) UNION - SELECT `id` AS ARRAY_KEY, `baseRepValue4` FROM aowow_factions WHERE `baseRepValue4` AND (`baseRepRaceMask4` & ?d OR (`baseRepClassMask4` AND NOT `baseRepRaceMask4`)) AND ((`baseRepClassMask4` & ?d) OR NOT `baseRepClassMask4`)', + 'SELECT `id` AS ARRAY_KEY, `baseRepValue1` FROM ::factions WHERE `baseRepValue1` AND (`baseRepRaceMask1` & %i OR (`baseRepClassMask1` AND NOT `baseRepRaceMask1`)) AND ((`baseRepClassMask1` & %i) OR NOT `baseRepClassMask1`) UNION + SELECT `id` AS ARRAY_KEY, `baseRepValue2` FROM ::factions WHERE `baseRepValue2` AND (`baseRepRaceMask2` & %i OR (`baseRepClassMask2` AND NOT `baseRepRaceMask2`)) AND ((`baseRepClassMask2` & %i) OR NOT `baseRepClassMask2`) UNION + SELECT `id` AS ARRAY_KEY, `baseRepValue3` FROM ::factions WHERE `baseRepValue3` AND (`baseRepRaceMask3` & %i OR (`baseRepClassMask3` AND NOT `baseRepRaceMask3`)) AND ((`baseRepClassMask3` & %i) OR NOT `baseRepClassMask3`) UNION + SELECT `id` AS ARRAY_KEY, `baseRepValue4` FROM ::factions WHERE `baseRepValue4` AND (`baseRepRaceMask4` & %i OR (`baseRepClassMask4` AND NOT `baseRepRaceMask4`)) AND ((`baseRepClassMask4` & %i) OR NOT `baseRepClassMask4`)', $ra->toMask(), $cl->toMask(), $ra->toMask(), $cl->toMask(), $ra->toMask(), $cl->toMask(), $ra->toMask(), $cl->toMask() ); - if ($reputation = DB::Characters($realmId)->select('SELECT ?d AS `id`, `faction` AS `factionId`, `standing` FROM character_reputation WHERE `guid` = ?d AND (`flags` & 0x4) = 0', $profileId, $char['guid'])) + $insCols = []; + if ($reputation = DB::Characters($realmId)->selectAssoc('SELECT `faction`, `standing` FROM character_reputation WHERE `guid` = %i AND (`flags` & 0x4) = 0', $char['guid'])) { // merge back base values for encountered factions - foreach ($reputation as &$set) + foreach ($reputation as $set) { - if (empty($baseRep[$set['factionId']])) - continue; + $insCols['id'][] = $profileId; + $insCols['factionId'][] = $set['faction']; + $insCols['standing'][] = $set['standing'] + ($baseRep[$set['faction']] ?? 0); - $set['standing'] += $baseRep[$set['factionId']]; - unset($baseRep[$set['factionId']]); + unset($baseRep[$set['faction']]); } } // insert base values for not yet encountered factions foreach ($baseRep as $id => $val) - $reputation[] = array( - 'id' => $profileId, - 'factionId' => $id, - 'standing' => $val - ); + { + $insCols['id'][] = $profileId; + $insCols['factionId'][] = $id; + $insCols['standing'][] = $val; + } - foreach (Util::createSqlBatchInsert($reputation) as $rep) - DB::Aowow()->query('INSERT INTO ?_profiler_completion_reputation (?#) VALUES '.$rep, array_keys($reputation[0])); + DB::Aowow()->qry('INSERT INTO ::profiler_completion_reputation %m', $insCols); CLI::write(' ..reputation'); // known titles // - DB::Aowow()->query('DELETE FROM ?_profiler_completion_titles WHERE `id` = ?d', $profileId); + DB::Aowow()->qry('DELETE FROM ::profiler_completion_titles WHERE `id` = %i', $profileId); - $tBlocks = explode(' ', $char['knownTitles']); - $indizes = []; - for ($i = 0; $i < 6; $i++) - for ($j = 0; $j < 32; $j++) - if ($tBlocks[$i] & (1 << $j)) - $indizes[] = $j + ($i * 32); - - if ($indizes) - DB::Aowow()->query('INSERT INTO ?_profiler_completion_titles SELECT ?d, `id` FROM ?_titles WHERE `bitIdx` IN (?a)', $profileId, $indizes); + if ($indizes = Util::indexBitBlob($char['knownTitles'])) + DB::Aowow()->qry('INSERT INTO ::profiler_completion_titles SELECT %i, `id` FROM ::titles WHERE `bitIdx` IN %in', $profileId, $indizes); CLI::write(' ..titles'); // achievements // - DB::Aowow()->query('DELETE FROM ?_profiler_completion_achievements WHERE `id` = ?d', $profileId); + DB::Aowow()->qry('DELETE FROM ::profiler_completion_achievements WHERE `id` = %i', $profileId); - if ($achievements = DB::Characters($realmId)->select('SELECT ?d AS id, `achievement` AS `achievementId`, `date` FROM character_achievement WHERE `guid` = ?d', $profileId, $char['guid'])) + if ($achievements = DB::Characters($realmId)->selectAssoc('SELECT `achievement`, `date` FROM character_achievement WHERE `guid` = %i', $char['guid'])) { - foreach (Util::createSqlBatchInsert($achievements) as $a) - DB::Aowow()->query('INSERT INTO ?_profiler_completion_achievements (?#) VALUES '.$a, array_keys($achievements[0])); + DB::Aowow()->qry('INSERT INTO ::profiler_completion_achievements %m', array( + 'id' => array_fill(0, count($achievements), $profileId), + 'achievementId' => array_column($achievements, 'achievement'), + 'date' => array_column($achievements, 'date') + )); - $data['achievementpoints'] = DB::Aowow()->selectCell('SELECT SUM(`points`) FROM ?_achievement WHERE `id` IN (?a) AND (`flags` & ?d) = 0', array_column($achievements, 'achievementId'), ACHIEVEMENT_FLAG_COUNTER); + $data['achievementpoints'] = DB::Aowow()->selectCell('SELECT SUM(`points`) FROM ::achievement WHERE `id` IN %in AND (`flags` & %i) = 0', array_column($achievements, 'achievement'), ACHIEVEMENT_FLAG_COUNTER); } CLI::write(' ..achievements'); @@ -760,13 +786,18 @@ class Profiler // raid progression // - DB::Aowow()->query('DELETE FROM ?_profiler_completion_statistics WHERE `id` = ?d', $profileId); + DB::Aowow()->qry('DELETE FROM ::profiler_completion_statistics WHERE `id` = %i', $profileId); - if ($progress = DB::Characters($realmId)->select('SELECT ?d AS `id`, `criteria` AS `achievementId`, `date`, `counter` FROM character_achievement_progress WHERE `guid` = ?d AND `criteria` IN (?a)', $profileId, $char['guid'], self::$raidProgression)) + if ($progress = DB::Characters($realmId)->selectAssoc('SELECT `criteria`, `date`, `counter` FROM character_achievement_progress WHERE `guid` = %i AND `criteria` IN %in', $char['guid'], self::$raidProgression)) { - array_walk($progress, function (&$val) { $val['achievementId'] = array_search($val['achievementId'], self::$raidProgression); }); - foreach (Util::createSqlBatchInsert($progress) as $p) - DB::Aowow()->query('INSERT INTO ?_profiler_completion_statistics (?#) VALUES '.$p, array_keys($progress[0])); + array_walk($progress, fn(&$x) => $x['achievement'] = array_search($x['criteria'], self::$raidProgression)); + + DB::Aowow()->qry('INSERT INTO ::profiler_completion_statistics %m', array( + 'id' => array_fill(0, count($progress), $profileId), + 'achievementId' => array_column($progress, 'achievement'), + 'date' => array_column($progress, 'date'), + 'counter' => array_column($progress, 'counter') + )); } CLI::write(' ..raid progression'); @@ -774,22 +805,24 @@ class Profiler // known spells // - DB::Aowow()->query('DELETE FROM ?_profiler_completion_spells WHERE `id` = ?d', $profileId); + DB::Aowow()->qry('DELETE FROM ::profiler_completion_spells WHERE `id` = %i', $profileId); - if ($spells = DB::Characters($realmId)->select('SELECT ?d AS `id`, `spell` AS `spellId` FROM character_spell WHERE `guid` = ?d AND `disabled` = 0', $profileId, $char['guid'])) - foreach (Util::createSqlBatchInsert($spells) as $s) - DB::Aowow()->query('INSERT INTO ?_profiler_completion_spells (?#) VALUES '.$s, array_keys($spells[0])); + if ($spells = DB::Characters($realmId)->selectCol('SELECT `spell` FROM character_spell WHERE `guid` = %i AND `disabled` = 0', $char['guid'])) + DB::Aowow()->qry('INSERT INTO ::profiler_completion_spells %m', array( + 'id' => array_fill(0, count($spells), $profileId), + 'questId' => $spells + )); // apply auto-learned spells from trade skills if ($skills) - DB::Aowow()->query( - 'INSERT INTO ?_profiler_completion_spells - SELECT ?d, `spellId` - FROM ?_skilllineability - WHERE `skillLineId` IN (?a) AND + DB::Aowow()->qry( + 'INSERT INTO ::profiler_completion_spells + SELECT %i, `spellId` + FROM ::skilllineability + WHERE `skillLineId` IN %in AND `acquireMethod` = 1 AND - (`reqRaceMask` = 0 OR `reqRaceMask` & ?d) AND - (`reqClassMask` = 0 OR `reqClassMask` & ?d)', + (`reqRaceMask` = 0 OR `reqRaceMask` & %i) AND + (`reqClassMask` = 0 OR `reqClassMask` & %i)', $profileId, array_column($skills, 'skillId'), $ra->toMask(), @@ -804,20 +837,20 @@ class Profiler /****************/ // guilds - if ($guild = DB::Characters($realmId)->selectRow('SELECT g.`name` AS `name`, g.`guildid` AS `id`, gm.`rank` FROM guild_member gm JOIN guild g ON g.`guildid` = gm.`guildid` WHERE gm.`guid` = ?d', $char['guid'])) + if ($guild = DB::Characters($realmId)->selectRow('SELECT g.`name` AS `name`, g.`guildid` AS `id`, gm.`rank` FROM guild_member gm JOIN guild g ON g.`guildid` = gm.`guildid` WHERE gm.`guid` = %i', $char['guid'])) { $guildId = 0; - if (!($guildId = DB::Aowow()->selectCell('SELECT id FROM ?_profiler_guild WHERE realm = ?d AND realmGUID = ?d', $realmId, $guild['id']))) + if (!($guildId = DB::Aowow()->selectCell('SELECT id FROM ::profiler_guild WHERE realm = %i AND realmGUID = %i', $realmId, $guild['id']))) { $gData = array( // only most basic data 'realm' => $realmId, 'realmGUID' => $guild['id'], 'name' => $guild['name'], 'nameUrl' => self::urlize($guild['name']), - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC + 'stub' => 1 ); - $guildId = DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_guild (?#) VALUES (?a)', array_keys($gData), array_values($gData)); + $guildId = DB::Aowow()->qry('INSERT IGNORE INTO ::profiler_guild %v', $gData); } $data['guild'] = $guildId; @@ -828,11 +861,11 @@ class Profiler // arena teams - $teams = DB::Characters($realmId)->select('SELECT at.`arenaTeamId` AS ARRAY_KEY, at.`name`, at.`type`, IF(at.`captainGuid` = atm.`guid`, 1, 0) AS `captain`, atm.* FROM arena_team at JOIN arena_team_member atm ON atm.`arenaTeamId` = at.`arenaTeamId` WHERE atm.`guid` = ?d', $char['guid']); + $teams = DB::Characters($realmId)->selectAssoc('SELECT at.`arenaTeamId` AS ARRAY_KEY, at.`name`, at.`type`, IF(at.`captainGuid` = atm.`guid`, 1, 0) AS `captain`, atm.* FROM arena_team at JOIN arena_team_member atm ON atm.`arenaTeamId` = at.`arenaTeamId` WHERE atm.`guid` = %i', $char['guid']); foreach ($teams as $rGuid => $t) { $teamId = 0; - if (!($teamId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_arena_team WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $rGuid))) + if (!($teamId = DB::Aowow()->selectCell('SELECT `id` FROM ::profiler_arena_team WHERE `realm` = %i AND `realmGUID` = %i', $realmId, $rGuid))) { $team = array( // only most basic data 'realm' => $realmId, @@ -840,10 +873,10 @@ class Profiler 'name' => $t['name'], 'nameUrl' => self::urlize($t['name']), 'type' => $t['type'], - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC + 'stub' => 1 ); - $teamId = DB::Aowow()->query('INSERT IGNORE INTO ?_profiler_arena_team (?#) VALUES (?a)', array_keys($team), array_values($team)); + $teamId = DB::Aowow()->qry('INSERT IGNORE INTO ::profiler_arena_team %v', $team); } $member = array( @@ -857,16 +890,16 @@ class Profiler 'personalRating' => $t['personalRating'] ); - // Delete members from other teams of the same type - DB::Aowow()->query( + // delete members from other teams of the same type + DB::Aowow()->qry( 'DELETE atm - FROM ?_profiler_arena_team_member atm - JOIN ?_profiler_arena_team at ON atm.`arenaTeamId` = at.`id` AND at.`type` = ?d - WHERE atm.`profileId` = ?d AND atm.`arenaTeamId` <> ?d', + FROM ::profiler_arena_team_member atm + JOIN ::profiler_arena_team at ON atm.`arenaTeamId` = at.`id` AND at.`type` = %i + WHERE atm.`profileId` = %i AND atm.`arenaTeamId` <> %i', $t['type'], $profileId, $teamId ); - DB::Aowow()->query('INSERT INTO ?_profiler_arena_team_member (?#) VALUES (?a) ON DUPLICATE KEY UPDATE ?a', array_keys($member), array_values($member), array_slice($member, 2)); + DB::Aowow()->qry('INSERT INTO ::profiler_arena_team_member %v ON DUPLICATE KEY UPDATE %a', $member, array_slice($member, 2)); } CLI::write(' ..associated arena teams'); @@ -875,26 +908,23 @@ class Profiler /* mark char as done */ /*********************/ - if (DB::Aowow()->query('UPDATE ?_profiler_profiles SET ?a WHERE `realm` = ?d AND `realmGUID` = ?d', $data, $realmId, $charGuid) !== null) - DB::Aowow()->query('UPDATE ?_profiler_profiles SET `cuFlags` = `cuFlags` & ?d WHERE `id` = ?d', ~PROFILER_CU_NEEDS_RESYNC, $profileId); + if (DB::Aowow()->qry('UPDATE ::profiler_profiles SET %a WHERE `realm` = %i AND `realmGUID` = %i', $data, $realmId, $charGuid) !== null) + DB::Aowow()->qry('UPDATE ::profiler_profiles SET `stub` = 0 WHERE `id` = %i', $profileId); - return true; + return self::FETCH_RESULT_OK; } - public static function getGuildFromRealm($realmId, $guildGuid) + public static function getGuildFromRealm(int $realmId, int $guildGuid) : int { - $guild = DB::Characters($realmId)->selectRow('SELECT `guildId`, `name`, `createDate`, `info`, `backgroundColor`, `emblemStyle`, `emblemColor`, `borderStyle`, `borderColor` FROM guild WHERE `guildId` = ?d', $guildGuid); + $guild = DB::Characters($realmId)->selectRow('SELECT `guildId`, `name`, `createDate`, `info`, `backgroundColor`, `emblemStyle`, `emblemColor`, `borderStyle`, `borderColor` FROM guild WHERE `guildId` = %i', $guildGuid); if (!$guild) - return false; + return self::FETCH_RESULT_ERR_NOT_FOUND; if (!$guild['name']) - { - trigger_error('guild #'.$guildGuid.' on realm #'.$realmId.' has empty name. skipping...', E_USER_WARNING); - return false; - } + return self::FETCH_RESULT_ERR_NAME_EMPTY; // reminder: this query should not fail: a placeholder entry is created as soon as a team listview is created or team detail page is called - $guildId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_guild WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $guild['guildId']); + $guildId = DB::Aowow()->selectCell('SELECT `id` FROM ::profiler_guild WHERE `realm` = %i AND `realmGUID` = %i', $realmId, $guild['guildId']); CLI::write('fetching guild #'.$guildGuid.' from realm #'.$realmId); CLI::write('writing...'); @@ -907,13 +937,13 @@ class Profiler unset($guild['guildId']); $guild['nameUrl'] = self::urlize($guild['name']); - DB::Aowow()->query('UPDATE ?_profiler_guild SET ?a WHERE `realm` = ?d AND `realmGUID` = ?d', $guild, $realmId, $guildGuid); + DB::Aowow()->qry('UPDATE ::profiler_guild SET %a WHERE `realm` = %i AND `realmGUID` = %i', $guild, $realmId, $guildGuid); // ranks - DB::Aowow()->query('DELETE FROM ?_profiler_guild_rank WHERE `guildId` = ?d', $guildId); - if ($ranks = DB::Characters($realmId)->select('SELECT ?d AS `guildId`, `rid` AS "rank", `rname` AS "name" FROM guild_rank WHERE `guildid` = ?d', $guildId, $guildGuid)) - foreach (Util::createSqlBatchInsert($ranks) as $r) - DB::Aowow()->query('INSERT INTO ?_profiler_guild_rank (?#) VALUES '.$r, array_keys(reset($ranks))); + DB::Aowow()->qry('DELETE FROM ::profiler_guild_rank WHERE `guildId` = %i', $guildId); + if ($ranks = DB::Characters($realmId)->selectAssoc('SELECT %i AS `guildId`, `rid` AS "rank", `rname` AS "name" FROM guild_rank WHERE `guildid` = %i', $guildId, $guildGuid)) + foreach ($ranks as $r) // at most 10 per guild. don't bother setting up a multi-insert (%m) + DB::Aowow()->qry('INSERT INTO ::profiler_guild_rank %v', $r); CLI::write(' ..guild data'); @@ -931,10 +961,10 @@ class Profiler // this here should all happen within ProfileList $members = new RemoteProfileList($conditions, ['sv' => $realmId]); - if (!$members->error) - $members->initializeLocalEntries(); - else - return false; + if ($members->error) + return self::FETCH_RESULT_ERR_NO_MEMBERS; + + $members->initializeLocalEntries(); CLI::write(' ..guild members'); @@ -943,25 +973,22 @@ class Profiler /* mark guild as done */ /*********************/ - DB::Aowow()->query('UPDATE ?_profiler_guild SET `cuFlags` = `cuFlags` & ?d WHERE `id` = ?d', ~PROFILER_CU_NEEDS_RESYNC, $guildId); + DB::Aowow()->qry('UPDATE ::profiler_guild SET `stub` = 0 WHERE `id` = %i', $guildId); - return true; + return self::FETCH_RESULT_OK; } - public static function getArenaTeamFromRealm($realmId, $teamGuid) + public static function getArenaTeamFromRealm(int $realmId, int $teamGuid) : int { - $team = DB::Characters($realmId)->selectRow('SELECT `arenaTeamId`, `name`, `type`, `captainGuid`, `rating`, `seasonGames`, `seasonWins`, `weekGames`, `weekWins`, `rank`, `backgroundColor`, `emblemStyle`, `emblemColor`, `borderStyle`, `borderColor` FROM arena_team WHERE `arenaTeamId` = ?d', $teamGuid); + $team = DB::Characters($realmId)->selectRow('SELECT `arenaTeamId`, `name`, `type`, `captainGuid`, `rating`, `seasonGames`, `seasonWins`, `weekGames`, `weekWins`, `rank`, `backgroundColor`, `emblemStyle`, `emblemColor`, `borderStyle`, `borderColor` FROM arena_team WHERE `arenaTeamId` = %i', $teamGuid); if (!$team) - return false; + return self::FETCH_RESULT_ERR_NOT_FOUND; if (!$team['name']) - { - trigger_error('arena team #'.$teamGuid.' on realm #'.$realmId.' has empty name. skipping...', E_USER_WARNING); - return false; - } + return self::FETCH_RESULT_ERR_NAME_EMPTY; // reminder: this query should not fail: a placeholder entry is created as soon as a team listview is created or team detail page is called - $teamId = DB::Aowow()->selectCell('SELECT `id` FROM ?_profiler_arena_team WHERE `realm` = ?d AND `realmGUID` = ?d', $realmId, $team['arenaTeamId']); + $teamId = DB::Aowow()->selectCell('SELECT `id` FROM ::profiler_arena_team WHERE `realm` = %i AND `realmGUID` = %i', $realmId, $team['arenaTeamId']); CLI::write('fetching arena team #'.$teamGuid.' from realm #'.$realmId); CLI::write('writing...'); @@ -972,11 +999,10 @@ class Profiler /*************/ $captain = $team['captainGuid']; - unset($team['captainGuid']); - unset($team['arenaTeamId']); + unset($team['captainGuid'], $team['arenaTeamId']); $team['nameUrl'] = self::urlize($team['name']); - DB::Aowow()->query('UPDATE ?_profiler_arena_team SET ?a WHERE `realm` = ?d AND `realmGUID` = ?d', $team, $realmId, $teamGuid); + DB::Aowow()->qry('UPDATE ::profiler_arena_team SET %a WHERE `realm` = %i AND `realmGUID` = %i', $team, $realmId, $teamGuid); CLI::write(' ..team data'); @@ -985,14 +1011,14 @@ class Profiler /* Member Data */ /***************/ - $members = DB::Characters($realmId)->select( + $members = DB::Characters($realmId)->selectAssoc( 'SELECT atm.`guid` AS ARRAY_KEY, atm.`arenaTeamId`, atm.`weekGames`, atm.`weekWins`, atm.`seasonGames`, atm.`seasonWins`, atm.`personalrating` FROM arena_team_member atm JOIN characters c ON c.`guid` = atm.`guid` AND c.`deleteInfos_Account` IS NULL AND - c.`level` <= ?d AND - (c.`extra_flags` & ?d) = 0 - WHERE `arenaTeamId` = ?d', + c.`level` <= %i AND + (c.`extra_flags` & %i) = 0 + WHERE `arenaTeamId` = %i', MAX_LEVEL, self::CHAR_GMFLAGS, $teamGuid @@ -1007,7 +1033,7 @@ class Profiler $mProfiles = new RemoteProfileList($conditions, ['sv' => $realmId]); if ($mProfiles->error) - return false; + return self::FETCH_RESULT_ERR_NO_MEMBERS; $mProfiles->initializeLocalEntries(); foreach ($mProfiles->iterate() as $__) @@ -1020,21 +1046,21 @@ class Profiler $members[$mGuid]['profileId'] = $mProfiles->getField('id'); } - // Delete members from other teams of the same type... - DB::Aowow()->query( + // delete members from other teams of the same type... + DB::Aowow()->qry( 'DELETE atm - FROM ?_profiler_arena_team_member atm - JOIN ?_profiler_arena_team at ON atm.`arenaTeamId` = at.`id` AND at.`type` = ?d - WHERE atm.`profileId` IN (?a)', + FROM ::profiler_arena_team_member atm + JOIN ::profiler_arena_team at ON atm.`arenaTeamId` = at.`id` AND at.`type` = %i + WHERE atm.`profileId` IN %in', $team['type'], array_column($members, 'profileId') ); // ...and purge this teams member - DB::Aowow()->query('DELETE FROM ?_profiler_arena_team_member WHERE `arenaTeamId` = ?d', $teamId); + DB::Aowow()->qry('DELETE FROM ::profiler_arena_team_member WHERE `arenaTeamId` = %i', $teamId); - foreach (Util::createSqlBatchInsert($members) as $m) - DB::Aowow()->query('INSERT INTO ?_profiler_arena_team_member (?#) VALUES '.$m, array_keys(reset($members))); + foreach ($members as $m) // at most 10 per team (5x2) don't bother setting up multi-insert (%m) + DB::Aowow()->qry('INSERT INTO ::profiler_arena_team_member %v', $m); CLI::write(' ..team members'); @@ -1043,9 +1069,9 @@ class Profiler /* mark team as done */ /*********************/ - DB::Aowow()->query('UPDATE ?_profiler_arena_team SET `cuFlags` = `cuFlags` & ?d WHERE `id` = ?d', ~PROFILER_CU_NEEDS_RESYNC, $teamId); + DB::Aowow()->qry('UPDATE ::profiler_arena_team SET `stub` = 0 WHERE `id` = %i', $teamId); - return true; + return self::FETCH_RESULT_OK; } } diff --git a/includes/components/report.class.php b/includes/components/report.class.php index f3b766c3..26b4ca63 100644 --- a/includes/components/report.class.php +++ b/includes/components/report.class.php @@ -144,11 +144,21 @@ class Report $this->subject ??= 0; // 0 for utility, tools and misc pages? } - private function checkTargetContext() : int + private function checkTargetContext(?string $url) : int { - // check already reported - $field = User::isLoggedIn() ? 'userId' : 'ip'; - if (DB::Aowow()->selectCell('SELECT 1 FROM ?_reports WHERE `mode` = ?d AND `reason`= ?d AND `subject` = ?d AND ?# = ?', $this->mode, $this->reason, $this->subject, $field, User::$id ?: User::$ip)) + $where = array( + ['`mode` = %i ', $this->mode], + ['`reason`= %i ', $this->reason], + ['`subject` = %i', $this->subject], + ); + if (User::isLoggedIn()) // check already reported + $where[] = ['`userId` = %i', User::$id]; + else + $where[] = ['`ip` = %s', User::$ip]; + if ($url) + $where[] = ['`url` = %s', $url]; + + if (DB::Aowow()->selectCell('SELECT 1 FROM ::reports WHERE %and', $where)) return self::ERR_ALREADY_REPORTED; // check targeted post/postOwner staff status @@ -157,9 +167,9 @@ class Report { $roles = User::$groups; if ($this->mode == self::MODE_COMMENT) - $roles = DB::Aowow()->selectCell('SELECT `roles` FROM ?_comments WHERE `id` = ?d', $this->subject); + $roles = DB::Aowow()->selectCell('SELECT `roles` FROM ::comments WHERE `id` = %i', $this->subject); // else if if ($this->mode == self::MODE_FORUM_POST) - // $roles = DB::Aowow()->selectCell('SELECT `roles` FROM ?_forum_posts WHERE `id` = ?d', $this->subject); + // $roles = DB::Aowow()->selectCell('SELECT `roles` FROM ::forum_posts WHERE `id` = %i', $this->subject); return $roles & $ctxCheck ? self::ERR_NONE : self::ERR_MISCELLANEOUS; } @@ -190,7 +200,28 @@ class Report return false; } - if($err = $this->checkTargetContext()) + // clean up src url: dont use anchors, clean up query + if ($pageUrl) + { + $urlParts = parse_url($pageUrl); + if (!empty($urlParts['query'])) + { + parse_str($urlParts['query'], $query); // kills redundant param declarations + unset($query['locale']); // locale param shouldn't be needed. more..? + $urlParts['query'] = http_build_query($query); + } + + $pageUrl = ''; + if (isset($urlParts['scheme'])) + $pageUrl .= $urlParts['scheme'].':'; + + $pageUrl .= '//'.($urlParts['host'] ?? '').($urlParts['path'] ?? ''); + + if (isset($urlParts['query'])) + $pageUrl .= '?'.$urlParts['query']; + } + + if ($err = $this->checkTargetContext($pageUrl)) { $this->errorCode = $err; return false; @@ -217,7 +248,7 @@ class Report if ($email) $update['email'] = $email; - return DB::Aowow()->query('INSERT INTO ?_reports (?#) VALUES (?a)', array_keys($update), array_values($update)); + return DB::Aowow()->qry('INSERT INTO ::reports %v', $update); } public function getSimilar(int ...$status) : array @@ -229,8 +260,8 @@ class Report if ($s < self::STATUS_OPEN || $s > self::STATUS_CLOSED_SOLVED) unset($s); - return DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, r.* FROM ?_reports r WHERE {`status` IN (?a) AND }`mode` = ?d AND `reason` = ?d AND `subject` = ?d', - $status ?: DBSIMPLE_SKIP, $this->mode, $this->reason, $this->subject); + return DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, r.* FROM ::reports r WHERE %if', $status, '`status` IN %in AND', $status, '%end `mode` = %i AND `reason` = %i AND `subject` = %i', + $this->mode, $this->reason, $this->subject); } public function close(int $closeStatus, bool $inclAssigned = false) : bool @@ -245,10 +276,10 @@ class Report if ($inclAssigned) $fromStatus[] = self::STATUS_ASSIGNED; - if ($reports = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `userId` FROM ?_reports WHERE `status` IN (?a) AND `mode` = ?d AND `reason` = ?d AND `subject` = ?d', + if ($reports = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `userId` FROM ::reports WHERE `status` IN %in AND `mode` = %i AND `reason` = %i AND `subject` = %i', $fromStatus, $this->mode, $this->reason, $this->subject)) { - DB::Aowow()->query('UPDATE ?_reports SET `status` = ?d, `assigned` = 0 WHERE `id` IN (?a)', $closeStatus, array_keys($reports)); + DB::Aowow()->qry('UPDATE ::reports SET `status` = %i, `assigned` = 0 WHERE `id` IN %in', $closeStatus, array_keys($reports)); foreach ($reports as $rId => $uId) Util::gainSiteReputation($uId, $closeStatus == self::STATUS_CLOSED_SOLVED ? SITEREP_ACTION_GOOD_REPORT : SITEREP_ACTION_BAD_REPORT, ['id' => $rId]); diff --git a/includes/components/response/baseresponse.class.php b/includes/components/response/baseresponse.class.php index 986fa509..29c0bbc2 100644 --- a/includes/components/response/baseresponse.class.php +++ b/includes/components/response/baseresponse.class.php @@ -18,12 +18,12 @@ trait TrRecoveryHelper return Lang::main('intError'); // check if already processing - if ($_ = DB::Aowow()->selectCell('SELECT `statusTimer` - UNIX_TIMESTAMP() FROM ?_account WHERE `email` = ? AND `status` > ?d AND `statusTimer` > UNIX_TIMESTAMP()', $email, ACC_STATUS_NEW)) - return Lang::account('inputbox', 'error', 'isRecovering', [Util::formatTime($_ * 1000)]); + if ($_ = DB::Aowow()->selectCell('SELECT `statusTimer` - UNIX_TIMESTAMP() FROM ::account WHERE `email` = %s AND `status` > %i AND `statusTimer` > UNIX_TIMESTAMP()', $email, ACC_STATUS_NEW)) + return Lang::account('inputbox', 'error', 'isRecovering', [DateTime::formatTimeElapsed($_ * 1000)]); // create new token and write to db $token = Util::createHash(); - if (!DB::Aowow()->query('UPDATE ?_account SET `token` = ?, `status` = ?d, `statusTimer` = UNIX_TIMESTAMP() + ?d WHERE `email` = ?', $token, $newStatus, Cfg::get('ACC_RECOVERY_DECAY'), $email)) + if (!DB::Aowow()->qry('UPDATE ::account SET `token` = %s, `status` = %i, `statusTimer` = UNIX_TIMESTAMP() + %i WHERE `email` = %s', $token, $newStatus, Cfg::get('ACC_RECOVERY_DECAY'), $email)) return Lang::main('intError'); // send recovery mail @@ -201,9 +201,9 @@ trait TrCache { $this->initCache(); - // type+typeId+catg; 3+6+10 + // type+typeId/catg+mode; 3+10+1 $cKey = $this->formatCacheKey(); - $cKey[2] = substr($cKey[2], 0, 19); + $cKey[2] = substr($cKey[2], 0, 13); if ($modeMask & CACHE_MODE_MEMCACHED) foreach ($this->memcached()?->getAllKeys() ?? [] as $k) @@ -265,29 +265,27 @@ trait TrCache // https://stackoverflow.com/questions/466521 private function formatCacheKey() : array { - [$dbType, $dbTypeId, $category, $staffMask, $miscInfo] = $this->getCacheKeyComponents(); + [$dbType, $dbTypeIdOrCat, $staffMask, $miscInfo] = $this->getCacheKeyComponents(); $fileKey = ''; // DBType: 3 $fileKey .= str_pad(dechex($dbType & 0xFFF), 3, 0, STR_PAD_LEFT); - // DBTypeId: 6 - $fileKey .= str_pad(dechex($dbTypeId & 0xFFFFFF), 6, 0, STR_PAD_LEFT); - // category: (2+4+4) - $fileKey .= str_pad(dechex($category & 0xFFFFFFFFFF), 2+4+4, 0, STR_PAD_LEFT); + // DBTypeId: 6 / category: (2+4+4) + $fileKey .= str_pad(dechex($dbTypeIdOrCat & 0xFFFFFFFFFF), 2+4+4, 0, STR_PAD_LEFT); // cacheType: 1 $fileKey .= str_pad(dechex($this->_cacheType & 0xF), 1, 0, STR_PAD_LEFT); // localeId: 2, $fileKey .= str_pad(dechex(Lang::getLocale()->value & 0xFF), 2, 0, STR_PAD_LEFT); // staff mask: 4 $fileKey .= str_pad(dechex($staffMask & 0xFFFFFFFF), 4, 0, STR_PAD_LEFT); - // optioal: miscInfo + // optional: miscInfo if ($miscInfo) $fileKey .= '-'.$miscInfo; // topDir, 2ndDir, file return array( str_pad(dechex($dbType & 0xFF), 2, 0, STR_PAD_LEFT), - str_pad(dechex(($dbTypeId > 0 ? $dbTypeId : $category) & 0xFF), 2, 0, STR_PAD_LEFT), + str_pad(dechex(($dbTypeIdOrCat) & 0xFF), 2, 0, STR_PAD_LEFT), $fileKey ); } @@ -318,19 +316,23 @@ trait TrCache trait TrSearch { - private int $maxResults = 500; private string $query = ''; // sanitized search string private int $searchMask = 0; // what to search for private Search $searchObj; public function getCacheKeyComponents() : array { + $misc = $this->query . // can be empty for upgrade search + serialize($this->_get['wt'] ?? null) . // extra &_GET not expected for normal and opensearch + serialize($this->_get['wtv'] ?? null) . + serialize($this->_get['type'] ?? null) . + serialize($this->_get['slots'] ?? null); + return array( -1, // DBType - -1, // DBTypeId - $this->searchMask, // category + $this->searchMask, // DBTypeId/category User::$groups, // staff mask - md5($this->query) // misc (here search query) + md5($misc) // misc ); } } @@ -453,8 +455,8 @@ trait TrProfilerList abstract class BaseResponse { - protected const PATTERN_TEXT_LINE = '/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Cn}]/ui'; - protected const PATTERN_TEXT_BLOB = '/[\x00-\x09\x0B-\x1F\p{Cf}\p{Co}\p{Cs}\p{Cn}]/ui'; + protected const PATTERN_TEXT_LINE = '/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Cn}]/iu'; + protected const PATTERN_TEXT_BLOB = '/[\x00-\x09\x0B-\x1F\p{Cf}\p{Co}\p{Cs}\p{Cn}]/iu'; protected static array $sql = []; // debug: sql stats container @@ -542,9 +544,11 @@ abstract class BaseResponse protected function sumSQLStats() : void { - Util::arraySumByKey(self::$sql, DB::Aowow()->getStatistics(), DB::World()->getStatistics()); - foreach (Profiler::getRealms() as $rId => $_) - Util::arraySumByKey(self::$sql, DB::Characters($rId)->getStatistics()); + self::$sql = array( + 'count' => \dibi::$numOfQueries, + 'time' => \dibi::$totalTime, + 'elapsed' => \dibi::$elapsedTime + ); } protected function sendNoCacheHeader() @@ -634,17 +638,27 @@ abstract class BaseResponse protected static function checkTextLine(string $val) : string { + // remove invalid characters + $val = mb_convert_encoding(trim($val), 'utf-8', 'utf-8'); // trim non-printable chars - return preg_replace(self::PATTERN_TEXT_LINE, '', trim(urldecode($val))); + return preg_replace(self::PATTERN_TEXT_LINE, '', $val); } protected static function checkTextBlob(string $val) : string { + $val = mb_convert_encoding(trim($val), 'utf-8', 'utf-8'); // trim non-printable chars + excessive whitespaces (pattern includes \r) - $str = preg_replace(self::PATTERN_TEXT_BLOB, '', trim($val)); + $str = preg_replace(self::PATTERN_TEXT_BLOB, '', $val); return preg_replace('/ +/', ' ', trim($str)); } + protected static function checkLocale(string $localeId) : ?Locale + { + if (Util::checkNumeric($localeId, NUM_CAST_INT)) + return Locale::tryFrom($localeId); + return null; + } + /********************/ /* child implements */ diff --git a/includes/components/response/templateresponse.class.php b/includes/components/response/templateresponse.class.php index a8760bd4..d2eb3c94 100644 --- a/includes/components/response/templateresponse.class.php +++ b/includes/components/response/templateresponse.class.php @@ -21,8 +21,7 @@ trait TrDetailPage { return array( $this->type, // DBType - $this->typeId, // DBTypeId - -1, // category + $this->typeId, // DBTypeId/category User::$groups, // staff mask '' // misc (here unused) ); @@ -32,20 +31,26 @@ trait TrDetailPage trait TrListPage { - public ?string $subCat = null; + public string $subCat = ''; public ?Filter $filter = null; public function getCacheKeyComponents() : array { // max. 3 catgs - // catg max 65535 + // catg max 32767 - largest in use should be 11.197.26801 (Spells: Professions > Tailoring > Spellfire Tailoring) if ($this->category) { $catg = 0x0; for ($i = 0; $i < 3; $i++) { - $catg <<= 4; - $catg |= ($this->category[$i] ?? 0) & 0xFFFF; + $catg <<= 4 * 4; + if (!isset($this->category[$i])) + continue; + + if ($this->category[$i]) + $catg |= ($this->category[$i] << 1) & 0xFFFF; + else + $catg |= 1; } } @@ -54,8 +59,7 @@ trait TrListPage return array( $this->type, // DBType - -1, // DBTypeId - $catg ?? -1, // category + $catg ?? -1, // DBTypeId/category User::$groups, // staff mask $misc ?? '' // misc (here filter) ); @@ -146,7 +150,7 @@ class TemplateResponse extends BaseResponse public array $pageTemplate = []; // js PageTemplate object public array $jsGlobals = []; // ready to be used in template - public function __construct(string $pageParam = '') + public function __construct(string $rawParam = '') { $this->title[] = Cfg::get('NAME'); self::$time = microtime(true); @@ -154,8 +158,10 @@ class TemplateResponse extends BaseResponse parent::__construct(); $this->fullParams = $this->pageName; - if ($pageParam) - $this->fullParams .= '='.$pageParam; + if ($this->category) + $this->fullParams .= '='.implode('.', $this->category); + else if (in_array(__NAMESPACE__.'\TrDetailPage', class_uses($this)) && ($id = intVal($rawParam))) + $this->fullParams .= '='.$id; // prep js+css includes $parentVars = get_class_vars(__CLASS__); @@ -166,12 +172,12 @@ class TemplateResponse extends BaseResponse array_push($this->scripts, [SC_CSS_FILE, 'css/staff.css'], [SC_JS_FILE, 'js/staff.js']); // get alt header logo - if ($ahl = DB::Aowow()->selectCell('SELECT `altHeaderLogo` FROM ?_home_featuredbox WHERE ?d BETWEEN `startDate` AND `endDate` ORDER BY `id` DESC', time())) + if ($ahl = DB::Aowow()->selectCell('SELECT `altHeaderLogo` FROM ::home_featuredbox WHERE %i BETWEEN `startDate` AND `endDate` ORDER BY `id` DESC', time())) $this->headerLogo = Util::defStatic($ahl); if ($this->pageName) { - $this->wowheadLink = sprintf(WOWHEAD_LINK, Lang::getLocale()->domain(), $this->pageName, $pageParam ? '=' . $pageParam : ''); + $this->wowheadLink = sprintf(WOWHEAD_LINK, Lang::getLocale()->domain(), $this->fullParams, ''); $this->pageTemplate['pageName'] = $this->pageName; } @@ -217,16 +223,16 @@ class TemplateResponse extends BaseResponse if (!$this->result) $this->dataLoader = array_merge($this->dataLoader, $dataFiles); else - $this->result->addDataLoader($dataFiles); + $this->result->addDataLoader(...$dataFiles); } public static function pageStatsHook(Template\PageTemplate &$pt, array &$stats) : void { if (User::isInGroup(U_GROUP_EMPLOYEE)) { - $stats['time'] = Util::formatTime((microtime(true) - self::$time) * 1000, true); - $stats['sql'] = ['count' => parent::$sql['count'], 'time' => Util::formatTime(parent::$sql['time'] * 1000, true)]; - $stats['cache'] = !empty(static::$cacheStats) ? [static::$cacheStats[0], Util::formatTimeDiff(static::$cacheStats[1])] : null; + $stats['time'] = DateTime::formatTimeElapsed((microtime(true) - self::$time) * 1000); + $stats['sql'] = ['count' => parent::$sql['count'], 'time' => DateTime::formatTimeElapsed(parent::$sql['time'] * 1000)]; + $stats['cache'] = !empty(static::$cacheStats) ? [static::$cacheStats[0], (new DateTime())->formatDate(static::$cacheStats[1])] : null; } else $stats = []; @@ -345,7 +351,7 @@ class TemplateResponse extends BaseResponse $this->initJSGlobal($type); - $obj = Type::newList($type, [Cfg::get('SQL_LIMIT_NONE'), ['id', array_unique($ids, SORT_NUMERIC)]]); + $obj = Type::newList($type, [['id', array_unique($ids, SORT_NUMERIC)]]); if (!$obj) continue; @@ -392,14 +398,12 @@ class TemplateResponse extends BaseResponse } // fetch announcements - $fromDB = DB::Aowow()->select( + $fromDB = DB::Aowow()->selectAssoc( 'SELECT `id`, `mode`, `status`, `name`, `style`, `text_loc0`, `text_loc2`, `text_loc3`, `text_loc4`, `text_loc6`, `text_loc8` - FROM ?_announcements - WHERE (`status` = ?d { OR `status` = ?d } ) AND - (`page` = "*" { OR `page` = ? } ) AND - (`groupMask` = 0 OR `groupMask` & ?d)', - Announcement::STATUS_ENABLED, User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU) ? Announcement::STATUS_DISABLED : DBSIMPLE_SKIP, - $onlyGenerics || !$this->pageName ? DBSIMPLE_SKIP : $this->pageName, + FROM ::announcements + WHERE (`page` = "*" %if', !$onlyGenerics && $this->pageName, 'OR `page` = %s', $this->pageName, '%end) AND + `status` IN (%i) AND (`groupMask` = 0 OR `groupMask` & %i)', + User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU) ? [Announcement::STATUS_ENABLED, Announcement::STATUS_DISABLED] : [Announcement::STATUS_ENABLED], User::$groups ); @@ -418,13 +422,13 @@ class TemplateResponse extends BaseResponse $article = []; if (isset($this->guideRevision)) - $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d AND `rev` = ?d', + $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ::articles WHERE `type` = %i AND `typeId` = %i AND `rev` = %i', Type::GUIDE, $this->typeId, $this->guideRevision); - if (!$article && $this->gPageInfo['articleUrl']) - $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ?_articles WHERE `url` = ? AND `locale` IN (?a) ORDER BY `locale` DESC, `rev` DESC LIMIT 1', + if (!$article && !empty($this->gPageInfo['articleUrl'])) + $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ::articles WHERE `url` = %s AND `locale` IN %in ORDER BY `locale` DESC, `rev` DESC LIMIT 1', $this->gPageInfo['articleUrl'], [Lang::getLocale()->value, Locale::EN->value]); if (!$article && !empty($this->type) && isset($this->typeId)) - $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ?_articles WHERE `type` = ?d AND `typeId` = ?d AND `locale` IN (?a) ORDER BY `locale` DESC, `rev` DESC LIMIT 1', + $article = DB::Aowow()->selectRow('SELECT `article`, `locale`, `editAccess` FROM ::articles WHERE `type` = %i AND `typeId` = %i AND `locale` IN (%i) ORDER BY `locale` DESC, `rev` DESC LIMIT 1', $this->type, $this->typeId, [Lang::getLocale()->value, Locale::EN->value]); if (!$article) @@ -625,7 +629,19 @@ class TemplateResponse extends BaseResponse // as this may be loaded from cache, it will be unlinked from its response if ($ptJSG = $this->result->jsGlobals) { - Util::mergeJsGlobals($ptJSG, $this->jsGlobals); + foreach ($this->jsGlobals as $type => [, $data, ]) + { + if (!isset($ptJSG[$type]) || $type == Type::USER) + $ptJSG[$type] = $this->jsGlobals[$type]; + else + { + $masterJSG = [$type => &$ptJSG[$type][1]]; + Util::mergeJsGlobals($masterJSG, [$type => $data]); + } + + unset($masterJSG); + } + $this->result->jsGlobals = $ptJSG; } else if ($this->jsGlobals) @@ -656,15 +672,13 @@ class TemplateResponse extends BaseResponse // has a valid combination of categories private function isValidPage() : bool { - if (!$this->category || !$this->validCats) + if (!$this->category) return true; $c = $this->category; // shorthand switch (count($c)) { - case 0: // no params works always - return true; case 1: // null is valid || value in a 1-dim-array || (key for a n-dim-array && ( has more subcats || no further subCats )) $filtered = array_filter($this->validCats, fn ($x) => is_int($x)); return $c[0] === null || in_array($c[0], $filtered) || (!empty($this->validCats[$c[0]]) && (is_array($this->validCats[$c[0]]) || $this->validCats[$c[0]] === true)); diff --git a/includes/components/response/textresponse.class.php b/includes/components/response/textresponse.class.php index aef1843d..1541d239 100644 --- a/includes/components/response/textresponse.class.php +++ b/includes/components/response/textresponse.class.php @@ -14,14 +14,13 @@ trait TrTooltip { $key = array( $this->type, // DBType - $this->typeId, // DBTypeId - -1, // category + $this->typeId, // DBTypeId/category User::$groups, // staff mask '' // misc (here tooltip) ); if ($this->enhancedTT) - $key[4] = md5(serialize($this->enhancedTT)); + $key[3] = md5(serialize($this->enhancedTT)); return $key; } @@ -90,19 +89,19 @@ trait TrCommunityHelper } } -abstract class TextResponse extends BaseResponse +class TextResponse extends BaseResponse { protected string $contentType = MIME_TYPE_JAVASCRIPT; protected ?string $redirectTo = null; protected array $params = []; /// generation stats - protected static float $time = 0.0; + protected static float $time = 0.0; - public function __construct(string $pageParam = '') + public function __construct(string $rawParam = '') { self::$time = microtime(true); - $this->params = explode('.', $pageParam); + $this->params = explode('.', $rawParam); // todo - validate params? parent::__construct(); @@ -150,20 +149,22 @@ abstract class TextResponse extends BaseResponse $this->sumSQLStats(); echo "/*\n"; - echo " * generated in ".Util::formatTime((microtime(true) - self::$time) * 1000)."\n"; - echo " * " . parent::$sql['count'] . " SQL queries in " . Util::formatTime(parent::$sql['time'] * 1000) . "\n"; + echo " * generated in ".DateTime::formatTimeElapsedFloat((microtime(true) - self::$time) * 1000)."\n"; + echo " * " . parent::$sql['count'] . " SQL queries in " . DateTime::formatTimeElapsedFloat(parent::$sql['time'] * 1000) . "\n"; if ($this instanceof ICache && static::$cacheStats) { [$mode, $set, $lifetime] = static::$cacheStats; echo " * stored in " . ($mode == CACHE_MODE_MEMCACHED ? 'Memcached' : 'filecache') . ":\n"; - echo " * + ".date('c', $set) . ' - ' . Util::formatTimeDiff($set) . "\n"; - echo " * - ".date('c', $set + $lifetime) . ' - in '.Util::formatTime(($set + $lifetime - time()) * 1000) . "\n"; + echo " * + ".date('c', $set) . ' - ' . DateTime::formatTimeElapsedFloat((time() - $set) * 1000) . " ago\n"; + echo " * - ".date('c', $set + $lifetime) . ' - in '.DateTime::formatTimeElapsedFloat(($set + $lifetime - time()) * 1000) . "\n"; } echo " */\n\n"; } echo $out; } + + protected function generate() : void {} } ?> diff --git a/includes/components/screenshotmgr.class.php b/includes/components/screenshotmgr.class.php index 5a5ff4a1..be782877 100644 --- a/includes/components/screenshotmgr.class.php +++ b/includes/components/screenshotmgr.class.php @@ -100,29 +100,23 @@ class ScreenshotMgr extends ImageUpload public static function getScreenshots(int $type = 0, int $typeId = 0, $userId = 0, ?int &$nFound = 0) : array { - $screenshots = DB::Aowow()->select( + if ($userId) + $where = [['s.`userIdOwner` = %i', $userId]]; + else + $where = [['s.`type` = %i', $type], ['s.`typeId` = %i', $typeId]]; + + $screenshots = DB::Aowow()->selectAssoc( 'SELECT s.`id`, a.`username` AS "user", s.`date`, s.`width`, s.`height`, s.`type`, s.`typeId`, s.`caption`, s.`status`, s.`status` AS "flags" - FROM ?_screenshots s - LEFT JOIN ?_account a ON s.`userIdOwner` = a.`id` - WHERE - { s.`type` = ?d } - { AND s.`typeId` = ?d } - { s.`userIdOwner` = ?d } - { LIMIT ?d }', - $userId ? DBSIMPLE_SKIP : $type, - $userId ? DBSIMPLE_SKIP : $typeId, - $userId ? $userId : DBSIMPLE_SKIP, - $userId || $type ? DBSIMPLE_SKIP : 100 + FROM ::screenshots s + LEFT JOIN ::account a ON s.`userIdOwner` = a.`id` + WHERE %and + %lmt', + $where, $userId || $type ? PHP_INT_MAX : 100 ); $num = []; foreach ($screenshots as $s) - { - if (empty($num[$s['type']][$s['typeId']])) - $num[$s['type']][$s['typeId']] = 1; - else - $num[$s['type']][$s['typeId']]++; - } + $num[$s['type']][$s['typeId']] = ($num[$s['type']][$s['typeId']] ?? 0) + 1; $nFound = 0; @@ -177,15 +171,7 @@ class ScreenshotMgr extends ImageUpload { // i GUESS .. ss_getALL ? everything : pending $nFound = 0; - $pages = DB::Aowow()->select( - 'SELECT s.`type`, s.`typeId`, COUNT(1) AS "count", MIN(s.`date`) AS "date" - FROM ?_screenshots s - { WHERE (s.`status` & ?d) = 0 } - GROUP BY s.`type`, s.`typeId`', - $all ? DBSIMPLE_SKIP : CC_FLAG_APPROVED | CC_FLAG_DELETED - ); - - if ($pages) + if ($pages = DB::Aowow()->selectAssoc('SELECT `type`, `typeId`, COUNT(1) AS "count", MIN(`date`) AS "date" FROM ::screenshots %if', !$all, 'WHERE (`status` & %i) = 0', CC_FLAG_APPROVED | CC_FLAG_DELETED, '%end GROUP BY `type`, `typeId`')) { // limit to one actually existing type each foreach (array_unique(array_column($pages, 'type')) as $t) @@ -198,7 +184,7 @@ class ScreenshotMgr extends ImageUpload if (!$ids) continue; - $obj = Type::newList($t, [Cfg::get('SQL_LIMIT_NONE'), ['id', $ids]]); + $obj = Type::newList($t, [['id', $ids]]); if (!$obj || $obj->error) continue; diff --git a/includes/components/search.class.php b/includes/components/search.class.php index 734a45a6..5587071c 100644 --- a/includes/components/search.class.php +++ b/includes/components/search.class.php @@ -8,6 +8,9 @@ if (!defined('AOWOW_REVISION')) class Search { + public const /* int */ DEFAULT_MAX_RESULTS = 500; + public const /* int */ SUGGESTIONS_MAX_RESULTS = 10; + public const /* int */ MOD_CLASS = 0; public const /* int */ MOD_RACE = 1; public const /* int */ MOD_TITLE = 2; @@ -76,12 +79,13 @@ class Search private array $resultStore = []; private array $included = []; private array $excluded = []; - private array $cndBase = ['AND']; + private array $fulltext = []; + private array $cndBase = [DB::AND]; private bool $idSearch = false; public array $invalid = []; - public function __construct(private string $query, private int $moduleMask = -1, private int $maxResults = 500, private array $extraCnd = [], private array $extraOpts = []) + public function __construct(private string $query, private int $moduleMask = -1, private array $extraCnd = [], private array $extraOpts = [], private int $maxResults = self::DEFAULT_MAX_RESULTS) { $this->tokenizeQuery(); @@ -104,31 +108,24 @@ class Search return; } + $allowShort = Lang::getLocale()->isLogographic(); + foreach (explode(' ', $this->query) as $raw) { - $clean = str_replace(['\\', '%'], '', $raw); - - if (!$clean === '') - continue; - - if ($clean[0] == '-') + if ([$like, $fulltext, $ex] = Filter::transformToken($raw, $allowShort)) { - if (mb_strlen($clean) < 4 && !Lang::getLocale()->isLogographic()) - $this->invalid[] = mb_substr($raw, 1); - else - $this->excluded[] = mb_substr(str_replace('_', '\\_', $clean), 1); + $this->{$ex ? 'excluded' : 'included'}[] = $like; + + // note: a fulltext search purely from exclude tokens will return no result + foreach ($fulltext as $ft) + $this->fulltext[] = ($ex ? '-' : '+') . '(' . $ft . '* ' . Util::strrev($ft) . '*)'; } else - { - if (mb_strlen($clean) < 3 && !Lang::getLocale()->isLogographic()) - $this->invalid[] = $raw; - else - $this->included[] = str_replace('_', '\\_', $clean); - } + $this->invalid[] = $raw; } } - private function createLookup(array $fields = []) : array + private function createLikeLookup(array $fields = []) : array { if ($this->idSearch && $this->included) return ['id', $this->included]; @@ -144,15 +141,12 @@ class Search foreach ($fields as $f) { $sub = []; - foreach ($this->included as $i) - $sub[] = [$f, '%'.$i.'%']; - - foreach ($this->excluded as $x) - $sub[] = [$f, '%'.$x.'%', '!']; + $sub = array_merge($sub, array_map(fn($x) => [$f, $x, 'LIKE'], $this->included)); + $sub = array_merge($sub, array_map(fn($x) => [$f, $x, 'NOT LIKE'], $this->excluded)); // single cnd? if (count($sub) > 1) - array_unshift($sub, 'AND'); + array_unshift($sub, DB::AND); else $sub = $sub[0]; @@ -161,13 +155,30 @@ class Search // single cnd? if (count($qry) > 1) - array_unshift($qry, 'OR'); + array_unshift($qry, DB::OR); else $qry = $qry[0]; return $qry; } + private function createMatchLookup() : array + { + if ($this->idSearch && $this->included) + return ['id', $this->included]; + + if (Lang::getLocale()->isLogographic() && !Cfg::get('LOGOGRAPHIC_FT_SEARCH')) + return $this->createLikeLookup(); + + if ($this->fulltext) + return ['nml.nName', $this->fulltext, 'MATCH']; + else if ($strBak = trim($this->query)) + if (mb_strlen($strBak) > 2 || Lang::getLocale()->isLogographic()) + return ['name_loc'.Lang::getLocale()->value, $strBak]; + + return []; + } + public function canPerform() : bool { // has valid search terms or weights and selected modules @@ -203,7 +214,7 @@ class Search private function _searchCharClass() : ?array // 0 Classes: $moduleMask & 0x00000001 { - $cnd = array_merge($this->cndBase, [$this->createLookup()]); + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); $classes = new CharClassList($cnd, ['calcTotal' => true]); $data = $classes->getListviewData(); @@ -242,7 +253,7 @@ class Search private function _searchCharRace() : ?array // 1 Races: $moduleMask & 0x00000002 { - $cnd = array_merge($this->cndBase, [$this->createLookup()]); + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); $races = new CharRaceList($cnd, ['calcTotal' => true]); $data = $races->getListviewData(); @@ -281,7 +292,7 @@ class Search private function _searchTitle() : ?array // 2 Titles: $moduleMask & 0x00000004 { - $cnd = array_merge($this->cndBase, [$this->createLookup(['male_loc'.Lang::getLocale()->value, 'female_loc'.Lang::getLocale()->value])]); + $cnd = array_merge($this->cndBase, [$this->createLikeLookup(['male_loc'.Lang::getLocale()->value, 'female_loc'.Lang::getLocale()->value])]); $titles = new TitleList($cnd, ['calcTotal' => true]); $data = $titles->getListviewData(); @@ -322,9 +333,9 @@ class Search { $cnd = array_merge($this->cndBase, array( array( - 'OR', - $this->createLookup(['h.name_loc'.Lang::getLocale()->value]), - ['AND', $this->createLookup(['e.description']), ['e.holidayId', 0]] + DB::OR, + $this->createLikeLookup(['h.name_loc'.Lang::getLocale()->value]), + [DB::AND, $this->createLikeLookup(['e.description']), ['e.holidayId', 0]] ) )); $wEvents = new WorldEventList($cnd, ['calcTotal' => true]); @@ -365,7 +376,7 @@ class Search private function _searchCurrency() : ?array // 4 Currencies $moduleMask & 0x0000010 { - $cnd = array_merge($this->cndBase, [$this->createLookup()]); + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); $money = new CurrencyList($cnd, ['calcTotal' => true]); $data = $money->getListviewData(); @@ -404,7 +415,7 @@ class Search private function _searchItemset(array &$shared) : ?array// 5 Itemsets $moduleMask & 0x0000020 { - $cnd = array_merge($this->cndBase, [$this->createLookup()]); + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); $sets = new ItemsetList($cnd, ['calcTotal' => true]); $data = $sets->getListviewData(); @@ -461,13 +472,15 @@ class Search private function _searchItem(array &$shared) : ?array // 6 Items $moduleMask & 0x0000040 { $miscData = ['calcTotal' => true]; - $lookup = $this->createLookup(); + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; if ($this->moduleMask & self::TYPE_JSON) { if (!empty($shared['pcsToSet'])) { - $cnd = [['i.id', array_keys($shared['pcsToSet'])], Cfg::get('SQL_LIMIT_NONE')]; + $cnd = [['i.id', array_keys($shared['pcsToSet'])]]; $miscData = ['pcsToSet' => $shared['pcsToSet']]; } else @@ -541,11 +554,15 @@ class Search private function _searchAbility() : ?array // 7 Abilities (Player + Pet) $moduleMask & 0x0000080 { - $cnd = array_merge($this->cndBase, array( // hmm, inclued classMounts..? + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + + $cnd = array_merge($this->cndBase, array( // hmm, inclued classMounts..? ['s.typeCat', [7, -2, -3, -4]], [['s.cuFlags', (SPELL_CU_TRIGGERED | SPELL_CU_TALENT), '&'], 0], [['s.attributes0', 0x80, '&'], 0], - $this->createLookup() + $lookup )); $abilities = new SpellList($cnd, ['calcTotal' => true]); @@ -605,10 +622,11 @@ class Search private function _searchTalent() : ?array // 8 Talents (Player + Pet) $moduleMask & 0x0000100 { - $cnd = array_merge($this->cndBase, array( - ['s.typeCat', [-7, -2]], - $this->createLookup() - )); + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + + $cnd = array_merge($this->cndBase, [['s.typeCat', [-7, -2]], $lookup]); $talents = new SpellList($cnd, ['calcTotal' => true]); $data = $talents->getListviewData(); @@ -664,10 +682,11 @@ class Search private function _searchGlyph() : ?array // 9 Glyphs $moduleMask & 0x0000200 { - $cnd = array_merge($this->cndBase, array( - ['s.typeCat', -13], - $this->createLookup() - )); + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + + $cnd = array_merge($this->cndBase, [['s.typeCat', -13], $lookup]); $glyphs = new SpellList($cnd, ['calcTotal' => true]); $data = $glyphs->getListviewData(); @@ -718,10 +737,11 @@ class Search private function _searchProficiency() : ?array // 10 Proficiencies $moduleMask & 0x0000400 { - $cnd = array_merge($this->cndBase, array( - ['s.typeCat', -11], - $this->createLookup() - )); + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + + $cnd = array_merge($this->cndBase, [['s.typeCat', -11], $lookup]); $prof = new SpellList($cnd, ['calcTotal' => true]); $data = $prof->getListviewData(); @@ -772,10 +792,11 @@ class Search private function _searchProfession() : ?array // 11 Professions (Primary + Secondary) $moduleMask & 0x0000800 { - $cnd = array_merge($this->cndBase, array( - ['s.typeCat', [9, 11]], - $this->createLookup() - )); + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + + $cnd = array_merge($this->cndBase, [['s.typeCat', [9, 11]], $lookup]); $prof = new SpellList($cnd, ['calcTotal' => true]); $data = $prof->getListviewData(); @@ -826,10 +847,11 @@ class Search private function _searchCompanion() : ?array // 12 Companions $moduleMask & 0x0001000 { - $cnd = array_merge($this->cndBase, array( - ['s.typeCat', -6], - $this->createLookup() - )); + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + + $cnd = array_merge($this->cndBase, [['s.typeCat', -6], $lookup]); $vPets = new SpellList($cnd, ['calcTotal' => true]); $data = $vPets->getListviewData(); @@ -880,10 +902,11 @@ class Search private function _searchMount() : ?array // 13 Mounts $moduleMask & 0x0002000 { - $cnd = array_merge($this->cndBase, array( - ['s.typeCat', -5], - $this->createLookup() - )); + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + + $cnd = array_merge($this->cndBase, [['s.typeCat', -5], $lookup]); $mounts = new SpellList($cnd, ['calcTotal' => true]); $data = $mounts->getListviewData(); @@ -933,10 +956,14 @@ class Search private function _searchCreature() : ?array // 14 NPCs $moduleMask & 0x0004000 { + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + $cnd = array_merge($this->cndBase, array( [['flagsExtra', 0x80], 0], // exclude trigger creatures [['cuFlags', NPC_CU_DIFFICULTY_DUMMY, '&'], 0], // exclude difficulty entries - $this->createLookup() + $lookup )); $npcs = new CreatureList($cnd, ['calcTotal' => true]); @@ -986,9 +1013,13 @@ class Search private function _searchQuest() : ?array // 15 Quests $moduleMask & 0x0008000 { + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + $cnd = array_merge($this->cndBase, array( [['flags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&'], 0], - $this->createLookup() + $lookup )); $quests = new QuestList($cnd, ['calcTotal' => true]); @@ -1037,7 +1068,7 @@ class Search { $cnd = array_merge($this->cndBase, array( [['flags', ACHIEVEMENT_FLAG_COUNTER, '&'], 0], // not a statistic - $this->createLookup() + $this->createLikeLookup() )); $acvs = new AchievementList($cnd, ['calcTotal' => true]); @@ -1089,7 +1120,7 @@ class Search { $cnd = array_merge($this->cndBase, array( ['flags', ACHIEVEMENT_FLAG_COUNTER, '&'], // is a statistic - $this->createLookup() + $this->createLikeLookup() )); $stats = new AchievementList($cnd, ['calcTotal' => true]); @@ -1142,7 +1173,7 @@ class Search private function _searchZone() : ?array // 18 Zones $moduleMask & 0x0040000 { - $cnd = array_merge($this->cndBase, [$this->createLookup()]); + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); $zones = new ZoneList($cnd, ['calcTotal' => true]); $data = $zones->getListviewData(); @@ -1185,7 +1216,11 @@ class Search private function _searchObject() : ?array // 19 Objects $moduleMask & 0x0080000 { - $cnd = array_merge($this->cndBase, [$this->createLookup()]); + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + + $cnd = array_merge($this->cndBase, [$lookup]); $objects = new GameObjectList($cnd, ['calcTotal' => true]); $data = $objects->getListviewData(); @@ -1228,7 +1263,7 @@ class Search private function _searchFaction() : ?array // 20 Factions $moduleMask & 0x0100000 { - $cnd = array_merge($this->cndBase, [$this->createLookup()]); + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); $factions = new FactionList($cnd, ['calcTotal' => true]); $data = $factions->getListviewData(); @@ -1264,7 +1299,7 @@ class Search private function _searchSkill() : ?array // 21 Skills $moduleMask & 0x0200000 { - $cnd = array_merge($this->cndBase, [$this->createLookup()]); + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); $skills = new SkillList($cnd, ['calcTotal' => true]); $data = $skills->getListviewData(); @@ -1303,7 +1338,7 @@ class Search private function _searchPet() : ?array // 22 Pets $moduleMask & 0x0400000 { - $cnd = array_merge($this->cndBase, [$this->createLookup()]); + $cnd = array_merge($this->cndBase, [$this->createLikeLookup()]); $pets = new PetList($cnd, ['calcTotal' => true]); $data = $pets->getListviewData(); @@ -1345,10 +1380,11 @@ class Search private function _searchCreatureAbility() : ?array // 23 NPCAbilities $moduleMask & 0x0800000 { - $cnd = array_merge($this->cndBase, array( - ['s.typeCat', -8], - $this->createLookup() - )); + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + + $cnd = array_merge($this->cndBase, [['s.typeCat', -8], $lookup]); $npcAbilities = new SpellList($cnd, ['calcTotal' => true]); $data = $npcAbilities->getListviewData(); @@ -1400,15 +1436,19 @@ class Search private function _searchSpell() : ?array // 24 Spells (Misc + GM + triggered abilities) $moduleMask & 0x1000000 { + $lookup = $this->createMatchLookup(); + if (!$lookup) + return null; + $cnd = array_merge($this->cndBase, array( ['s.typeCat', -8, '!'], [ - 'OR', + DB::OR, ['s.typeCat', [0, -9]], ['s.cuFlags', SPELL_CU_TRIGGERED, '&'], ['s.attributes0', 0x80, '&'] ], - $this->createLookup() + $lookup )); $misc = new SpellList($cnd, ['calcTotal' => true]); @@ -1460,7 +1500,7 @@ class Search private function _searchEmote() : ?array // 25 Emotes $moduleMask & 0x2000000 { - $cnd = array_merge($this->cndBase, [$this->createLookup(['cmd', 'meToExt_loc'.Lang::getLocale()->value, 'meToNone_loc'.Lang::getLocale()->value, 'extToMe_loc'.Lang::getLocale()->value, 'extToExt_loc'.Lang::getLocale()->value, 'extToNone_loc'.Lang::getLocale()->value])]); + $cnd = array_merge($this->cndBase, [$this->createLikeLookup(['cmd', 'meToExt_loc'.Lang::getLocale()->value, 'meToNone_loc'.Lang::getLocale()->value, 'extToMe_loc'.Lang::getLocale()->value, 'extToExt_loc'.Lang::getLocale()->value, 'extToNone_loc'.Lang::getLocale()->value])]); $emote = new EmoteList($cnd, ['calcTotal' => true]); $data = $emote->getListviewData(); @@ -1501,7 +1541,7 @@ class Search private function _searchEnchantment() : ?array // 26 Enchantments $moduleMask & 0x4000000 { - $cnd = array_merge($this->cndBase, [$this->createLookup(['name_loc'.Lang::getLocale()->value])]); + $cnd = array_merge($this->cndBase, [$this->createLikeLookup(['name_loc'.Lang::getLocale()->value])]); $enchantment = new EnchantmentList($cnd, ['calcTotal' => true]); $data = $enchantment->getListviewData(); @@ -1548,7 +1588,7 @@ class Search private function _searchSound() : ?array // 27 Sounds $moduleMask & 0x8000000 { - $cnd = array_merge($this->cndBase, [$this->createLookup(['name'])]); + $cnd = array_merge($this->cndBase, [$this->createLikeLookup(['name'])]); $sounds = new SoundList($cnd, ['calcTotal' => true]); $data = $sounds->getListviewData(); diff --git a/includes/components/sitemap.class.php b/includes/components/sitemap.class.php new file mode 100644 index 00000000..f7d5b4e2 --- /dev/null +++ b/includes/components/sitemap.class.php @@ -0,0 +1,181 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('invalid access'); + + +// assumptions +// every character in this sitemap will be bog-standard ANSI +// so it consumes 1 Byte in UTF-8 +// every item is thus <140 byte +// so we hit 50k entries and have ~3.5MB storage capacity left + +class Sitemap +{ + public const /* string */ ERR_TITLE = 'Invalid sitemap'; + public const /* string */ ERR_PAGE = 'This sitemap does not exist.'; + public const /* string */ ERR_OFFSET = 'The maximum page for this sitemap is %d.'; + + private const /* int */ MAX_ENTRIES = 50000; + private const /* int */ LASTMOD_BASE = 1435701600; // 01.07.2015 - 00:00:00 + + public static int $maxPage = 0; + + private static string $page = ''; + private static int $offset = 1; + private static array $validPages = array( + 'npc' => [Type::NPC, '::creature', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.4)'], + 'object' => [Type::OBJECT, '::objects', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.4)'], + 'item' => [Type::ITEM, '::items', 'IF(x.`cuFlags` & 0x40000000, 0.1, IF(src.`typeId` IS NULL, 0.5, 0.7))'], + 'itemset' => [Type::ITEMSET, '::itemset', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.7)'], + 'quest' => [Type::QUEST, '::quests', 'IF(x.`cuFlags` & 0x40000000, 0.1, IF(src.`typeId` IS NULL, 0.3, 0.5))'], + 'spell' => [Type::SPELL, '::spell', 'IF(x.`cuFlags` & 0x40000000, 0.1, IF(src.`typeId` IS NULL, 0.5, 0.8))'], + 'zone' => [Type::ZONE, '::zones', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.4)'], + 'faction' => [Type::FACTION, '::factions', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.4)'], + 'pet' => [Type::PET, '::pet', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.4)'], + 'achievement' => [Type::ACHIEVEMENT, '::achievement', 'IF(x.`cuFlags` & 0x40000000, 0.1, IF(x.`category` = 81, 0.6, IF(x.`category` IN (1, 122, 133, 141, 134, 14807, 131, 130, 128, 132, 21, 124, 135, 126, 154, 125, 140, 145, 147, 136, 127, 152, 153, 191, 123, 14822, 14821, 14823, 137, 178, 173, 14963, 15021, 15062), 0.3, 0.4)))'], + 'title' => [Type::TITLE, '::titles', 'IF(x.`cuFlags` & 0x40000000, 0.1, IF(src.`typeId` IS NULL, 0.3, 0.4))'], + 'event' => [Type::WORLDEVENT, '::events', 'IF(x.`cuFlags` & 0x40000000, 0.1, IF(x.`holidayId` = 0, 0.2, 0.4))'], + 'class' => [Type::CHR_CLASS, '::classes', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.7)'], + 'race' => [Type::CHR_RACE, '::races', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.7)'], + 'skill' => [Type::SKILL, '::skillline', 'IF(x.`cuFlags` & 0x40000000, 0.1, IF(x.`typeCat` IN(11, 9), 0.5, IF(x.`typeCat` IN (8, 6), 0.4, 0.3)))'], + 'currency' => [Type::CURRENCY, '::currencies', 'IF(x.`cuFlags` & 0x40000000, 0.1, IF(x.`category` = 3, 0.2, IF(x.`description_loc0`, 0.4, 0.3)))'], + 'sound' => [Type::SOUND, '::sounds', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.3)'], + 'icon' => [Type::ICON, '::icons', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.3)'], + 'emote' => [Type::EMOTE, '::emotes', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.3)'], + 'enchantment' => [Type::ENCHANTMENT, '::itemenchantment', 'IF(x.`cuFlags` & 0x40000000, 0.1, IF(x.`type1` IN (1, 7) OR x.`type2` IN (1, 7) OR x.`type3` IN (1, 7), 0.4, 0.3))'], + 'areatrigger' => [Type::AREATRIGGER, '::areatrigger', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.3)'], + 'mail' => [Type::MAIL, '::mails', 'IF(x.`cuFlags` & 0x40000000, 0.1, 0.3)'] + // 'guide' => [Type::GUIDE, '::guides', ''] super low prio .. need a way to filter for publicly visible guides + ); + + public static function generate(string $page, int $offset) : ?string + { + self::$page = $page; + self::$offset = $offset; + + if (!self::$page) + return self::getIndex(); + else if (self::$page == 'special') + return self::getSpecial(); + else if (isset(self::$validPages[self::$page][1])) + return self::getPage(); + + // whoops! + return null; + } + + private static function getIndex() : ?string + { + $root = new SimpleXML('<sitemapindex />'); + $root->addAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); + + $root->addChild('sitemap')->addChild('loc', Cfg::get('HOST_URL').'/?sitemap=special'); + + foreach (self::$validPages as $page => [, $table, ]) + { + $n = DB::Aowow()->selectCell('SELECT CEIL(COUNT(*) / %i) FROM %n', self::MAX_ENTRIES, $table); + for ($i = 1; $i <= $n; $i++) + $root->addChild('sitemap')->addChild('loc', Cfg::get('HOST_URL').'/?sitemap='.$page.'&page='.$i); + } + + return $root->asXML() ?: null; + } + + private static function getSpecial() : ?string + { + if (self::$offset != 1) + { + self::$maxPage = 1; + return null; + } + + $root = new SimpleXML('<urlset />'); + $root->addAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); + + // home + $url = $root->addChild('url'); + $url->addChild('loc', Cfg::get('HOST_URL')); + $url->addChild('priority', 1); + $url->addChild('changefreq', 'monthly'); + + // talent calc + $url = $root->addChild('url'); + $url->addChild('loc', Cfg::get('HOST_URL').'/?talent'); + $url->addChild('priority', 1); + $url->addChild('changefreq', 'yearly'); + + // pet calc + $url = $root->addChild('url'); + $url->addChild('loc', Cfg::get('HOST_URL').'/?petcalc'); + $url->addChild('priority', 0.8); + $url->addChild('changefreq', 'yearly'); + + // item compare + $url = $root->addChild('url'); + $url->addChild('loc', Cfg::get('HOST_URL').'/?compare'); + $url->addChild('priority', 0.9); + $url->addChild('changefreq', 'yearly'); + + // profiler + if (Cfg::get('PROFILER_ENABLE')) + { + $url = $root->addChild('url'); + $url->addChild('loc', Cfg::get('HOST_URL').'/?profiler'); + $url->addChild('priority', 1); + $url->addChild('changefreq', 'yearly'); + } + + // maps + $url = $root->addChild('url'); + $url->addChild('loc', Cfg::get('HOST_URL').'/?maps'); + $url->addChild('priority', 0.7); + $url->addChild('changefreq', 'yearly'); + + return $root->asXML(); + } + + private static function getPage() : ?string + { + [$type, $table, $prioString] = self::$validPages[self::$page]; + + $n = DB::Aowow()->selectCell('SELECT CEIL(COUNT(*) / %i) FROM %n', self::MAX_ENTRIES, $table); + if (self::$offset <= 0 || self::$offset > $n) + { + self::$maxPage = $n; + return null; + } + + $root = new SimpleXML('<urlset />'); + $root->addAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); + + $rows = DB::Aowow()->selectAssoc( + 'SELECT x.`id` AS ARRAY_KEY, ('.$prioString.') AS "priority", GREATEST(IFNULL(MAX(ss.`date`), 0), IFNULL(MAX(vi.`date`), 0), IFNULL(MAX(co.`date`), 0)) AS "lastmod" FROM %n x + LEFT JOIN ::source src ON src.`type` = %i AND src.`typeId` = x.`id` + LEFT JOIN ::comments co ON co.`type` = %i AND co.`typeId` = x.`id` AND (co.`flags` & %i) = 0 + LEFT JOIN ::screenshots ss ON ss.`type` = %i AND ss.`typeId` = x.`id` AND (co.`flags` & %i) = 0 AND (co.`flags` & %i) > 0 + LEFT JOIN ::videos vi ON vi.`type` = %i AND vi.`typeId` = x.`id` AND (co.`flags` & %i) = 0 AND (co.`flags` & %i) > 0 + GROUP BY x.`id` LIMIT %i, %i', + $table, + $type, + $type, CC_FLAG_DELETED, + $type, CC_FLAG_DELETED, CC_FLAG_APPROVED, + $type, CC_FLAG_DELETED, CC_FLAG_APPROVED, + self::MAX_ENTRIES * (self::$offset - 1), self::MAX_ENTRIES + ); + + foreach ($rows as $id => $pair) + { + $url = $root->addChild('url'); + $url->addChild('loc', Cfg::get('HOST_URL').'/?'.self::$page.'='.$id); + $url->addChild('priority', $pair['priority']); + $url->addChild('lastmod', date('c', $pair['lastmod'] ?: self::LASTMOD_BASE)); + } + + return $root->asXML(); + } +} + +?> diff --git a/includes/components/videomgr.class.php b/includes/components/videomgr.class.php index a9b6e07f..1ce894bd 100644 --- a/includes/components/videomgr.class.php +++ b/includes/components/videomgr.class.php @@ -99,20 +99,19 @@ class VideoMgr * unique: bool || null */ - $videos = DB::Aowow()->select( + if ($userId) + $where = [['v.`userIdOwner` = %i', $userId]]; + else + $where = [['v.`type` = %i', $type], ['v.`typeId` = %i', $typeId]]; + + $videos = DB::Aowow()->selectAssoc( 'SELECT v.`id`, a.`username` AS "user", v.`date`, v.`videoId`, v.`type`, v.`typeId`, v.`caption`, v.`status` AS "flags", v.`url`, v.`name` - FROM ?_videos v - LEFT JOIN ?_account a ON v.`userIdOwner` = a.`id` - WHERE - { v.`type` = ?d } - { AND v.`typeId` = ?d } - { v.`userIdOwner` = ?d } - { LIMIT ?d } + FROM ::videos v + LEFT JOIN ::account a ON v.`userIdOwner` = a.`id` + WHERE %and + %lmt ORDER BY `type`, `typeId`, `pos` ASC', - $userId ? DBSIMPLE_SKIP : $type, - $userId ? DBSIMPLE_SKIP : $typeId, - $userId ? $userId : DBSIMPLE_SKIP, - $userId || $type ? DBSIMPLE_SKIP : 100 + $where, $userId || $type ? PHP_INT_MAX : 100 ); $num = []; @@ -176,15 +175,7 @@ class VideoMgr { // i GUESS .. vi_getALL ? everything : pending $nFound = 0; - $pages = DB::Aowow()->select( - 'SELECT v.`type`, v.`typeId`, COUNT(1) AS "count", MIN(v.`date`) AS "date" - FROM ?_videos v - { WHERE (v.`status` & ?d) = 0 } - GROUP BY v.`type`, v.`typeId`', - $all ? DBSIMPLE_SKIP : CC_FLAG_APPROVED | CC_FLAG_DELETED - ); - - if ($pages) + if ($pages = DB::Aowow()->selectAssoc('SELECT `type`, `typeId`, COUNT(1) AS "count", MIN(`date`) AS "date" FROM ::videos %if', !$all, 'WHERE (`status` & %i) = 0', CC_FLAG_APPROVED | CC_FLAG_DELETED, '%end GROUP BY `type`, `typeId`')) { // limit to one actually existing type each foreach (array_unique(array_column($pages, 'type')) as $t) @@ -197,7 +188,7 @@ class VideoMgr if (!$ids) continue; - $obj = Type::newList($t, [Cfg::get('SQL_LIMIT_NONE'), ['id', $ids]]); + $obj = Type::newList($t, [['id', $ids]]); if (!$obj || $obj->error) continue; diff --git a/includes/database.class.php b/includes/database.class.php deleted file mode 100644 index 64956c87..00000000 --- a/includes/database.class.php +++ /dev/null @@ -1,186 +0,0 @@ -<?php - -namespace Aowow; - -if (!defined('AOWOW_REVISION')) - die('illegal access'); - -/* - Class designed by LordJZ for Aowow3 - - https://github.com/LordJZ/aowow3/ -*/ - -class DB -{ - private static array $interfaceCache = []; - private static array $optionsCache = []; - private static array $logs = []; - - private static function createConnectSyntax(array &$options) : string - { - return 'mysqli://'.$options['user'].':'.$options['pass'].'@'.$options['host'].'/'.$options['db']; - } - - public static function connect(int $idx) : void - { - if (self::isConnected($idx)) - { - self::$interfaceCache[$idx]->link->close(); - self::$interfaceCache[$idx] = null; - } - - $options = &self::$optionsCache[$idx]; - $interface = \DbSimple_Generic::connect(self::createConnectSyntax($options)); - - $interface->setErrorHandler(self::errorHandler(...)); - if ($options['prefix']) - $interface->setIdentPrefix($options['prefix']); - - self::$interfaceCache[$idx] = &$interface; - - // should be caught by registered error handler - if (!$interface || !$interface->link) - return; - - $interface->query('SET NAMES ?', 'utf8mb4'); - - // disable STRICT_TRANS_TABLES and STRICT_ALL_TABLES off. It prevents usage of implicit default values. - // disable ONLY_FULL_GROUP_BY (Allows for non-aggregated selects in a group-by query) - $extraModes = ['STRICT_TRANS_TABLES', 'STRICT_ALL_TABLES', 'ONLY_FULL_GROUP_BY', 'NO_ZERO_DATE', 'NO_ZERO_IN_DATE', 'ERROR_FOR_DIVISION_BY_ZERO']; - $oldModes = explode(',', $interface->selectCell('SELECT @@sql_mode')); - $newModes = array_diff($oldModes, $extraModes); - - if ($oldModes != $newModes) - $interface->query("SET SESSION sql_mode = ?", implode(',', $newModes)); - } - - public static function test(array $options, ?string &$err = '') : bool - { - $defPort = ini_get('mysqli.default_port'); - $port = 0; - if (strstr($options['host'], ':')) - [$options['host'], $port] = explode(':', $options['host']); - - if ($link = mysqli_connect($options['host'], $options['user'], $options['pass'], $options['db'], $port ?: $defPort)) - { - mysqli_close($link); - return true; - } - - $err = '['.mysqli_connect_errno().'] '.mysqli_connect_error(); - return false; - } - - public static function errorHandler(string $message, array $data) : void - { - if (!error_reporting()) - return; - - // continue on warning, end on error - $isError = $data['code'] > 0; - - // make number sensible again - $data['code'] = abs($data['code']); - - if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO) - { - echo "\nDB ERROR\n"; - foreach ($data as $k => $v) - echo ' '.str_pad($k.':', 10).$v."\n"; - } - - trigger_error($message, $isError ? E_USER_ERROR : E_USER_WARNING); - } - - public static function profiler(mixed $self, string $query, mixed $trace) : void - { - if ($trace) // actual query - self::$logs[] = [str_replace("\n", ' ', $query)]; - else // the statistics - { - end(self::$logs); - self::$logs[key(self::$logs)][] = substr(explode(';', $query)[0], 5); - } - } - - public static function getProfiles() : string - { - $out = '<pre><table style="font-size:12;"><tr><th></th><th>Time</th><th>Query</th></tr>'; - foreach (self::$logs as $i => [$l, $t]) - { - $c = 'inherit'; - preg_match('/(\d+)/', $t, $m); - if ($m[1] > 100) - $c = '#FFA0A0'; - else if ($m[1] > 20) - $c = '#FFFFA0'; - - $out .= '<tr><td>'.$i.'.</td><td style="background-color:'.$c.';">'.$t.'</td><td>'.$l.'</td></tr>'; - } - - return Util::jsEscape($out).'</table></pre>'; - } - - public static function getDB(int $idx) : ?\DbSimple_Mysqli - { - return self::$interfaceCache[$idx]; - } - - public static function isConnected(int $idx) : bool - { - return isset(self::$interfaceCache[$idx]) && self::$interfaceCache[$idx]->link; - } - - public static function isConnectable(int $idx) : bool - { - return isset(self::$optionsCache[$idx]); - } - - /** - * @static - * @return DbSimple_Mysqli - */ - public static function Characters(int $realmId) : ?\DbSimple_Mysqli - { - if (!isset(self::$optionsCache[DB_CHARACTERS.$realmId])) - die('Connection info not found for live database of realm #'.$realmId.'. Aborted.'); - - return self::getDB(DB_CHARACTERS.$realmId); - } - - /** - * @static - * @return DbSimple_Mysqli - */ - public static function Auth() : ?\DbSimple_Mysqli - { - return self::getDB(DB_AUTH); - } - - /** - * @static - * @return DbSimple_Mysqli - */ - public static function World() : ?\DbSimple_Mysqli - { - return self::getDB(DB_WORLD); - } - - /** - * @static - * @return DbSimple_Mysqli - */ - public static function Aowow() : ?\DbSimple_Mysqli - { - return self::getDB(DB_AOWOW); - } - - public static function load(int $idx, array $config) : void - { - self::$optionsCache[$idx] = $config; - self::connect($idx); - } -} - -?> diff --git a/includes/database.php b/includes/database.php new file mode 100644 index 00000000..f0df4f51 --- /dev/null +++ b/includes/database.php @@ -0,0 +1,332 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + + + +class DibiConnection extends \Dibi\Connection +{ + /** + * Executes SQL query and fetch result - shortcut for query() & fetch(). + */ + public function selectRow(mixed ...$args) : ?array + { + try + { + return (array)$this->query($args)->fetch(); + } + catch (\Exception $e) {} // logged via \Dibi\Event in errorLogger + + return null; + } + + /** + * Executes SQL query and fetch first column - shortcut for query() & fetchSingle(). + */ + public function selectCell(mixed ...$args) : mixed + { + try + { + $x = $this->query($args)->fetchSingle(); + return is_array($x) ? array_pop($x) : $x; + } + catch (\Exception $e) {} // logged via \Dibi\Event in errorLogger + + return null; + } + + /** + * Executes SQL query and fetch first column - shortcut for query() & fetchSingle(). + */ + public function selectCol(mixed ...$args) : ?array + { + try + { + $result = $this->query($args); + if (strpos($args[0], 'ARRAY_KEY2')) + $data = $result->fetchAssoc('ARRAY_KEY|ARRAY_KEY2'); + else if (strpos($args[0], 'ARRAY_KEY')) + $data = $result->fetchAssoc('ARRAY_KEY'); + else + $data = $result->fetchAll(); + + $result->free(); + + // convert Dibi/Row to array + // remove array keys from result set and set result to next cell + array_walk_recursive($data, function(&$row) { + if (get_debug_type($row) == 'Dibi\Row') + $row = (array)$row; + + unset($row['ARRAY_KEY'], $row['ARRAY_KEY2']); + $row = array_pop($row); + }); + return $data; + } + catch (\Exception $e) {} // logged via \Dibi\Event in errorLogger + + return null; + } + + /** + * Executes SQL query and fetch ass associative array + */ + public function selectAssoc(mixed ...$args) : ?array + { + try + { + $result = $this->query($args); + if (strpos($args[0], 'ARRAY_KEY2')) + $data = $result->fetchAssoc('ARRAY_KEY|ARRAY_KEY2'); + else if (strpos($args[0], 'ARRAY_KEY')) + $data = $result->fetchAssoc('ARRAY_KEY'); + else + $data = $result->fetchAll(); + + $result->free(); + + // convert Dibi/Row to array + // remove array keys from result set + array_walk_recursive($data, function(&$row) { + if (get_debug_type($row) == 'Dibi\Row') + $row = (array)$row; + + unset($row['ARRAY_KEY'], $row['ARRAY_KEY2']); + }); + return $data; + } + catch (\Exception $e) {} // logged via \Dibi\Event in errorLogger + + return null; + } + + /** + * Executes SQL query and fetch pairs - shortcut for query() & fetchPairs(). + */ + public function selectPairs(mixed ...$args): ?array + { + try + { + return $this->query($args)->fetchPairs(); + } + catch (\Exception $e) {} // logged via \Dibi\Event in errorLogger + + return null; + } + + /** + * Executes SQL query and returns new insertId or num affected rows. + */ + public function qry(mixed ...$args) : ?int + { + try + { + $this->nativeQuery($this->translate(...$args)); + if (strstr($args[0], 'INSERT')) + return $this->getDriver()?->getResource()?->insert_id; + else + return $this->getAffectedRows(); + } + catch (\Exception $e) {} // logged via \Dibi\Event in errorLogger + + return null; + } +} + +class DB +{ + public const /* string */ AND = '%and'; + public const /* string */ OR = '%or'; + + private static array $interfaceCache = []; + private static array $interfaceTimes = []; + private static array $optionsCache = []; + private static array $logs = []; + + public static function connect(int $idx) : bool + { + if (self::isConnected($idx)) + { + self::$interfaceCache[$idx]->disconnect(); + self::$interfaceCache[$idx] = null; + } + + $config = self::$optionsCache[$idx] + array( + 'charset' => 'utf8mb4', // executes: SET NAMES $charset + 'substitutes' => array( + '' => self::$optionsCache[$idx]['prefix'] // old: ?_ - new: :: + ) + ); + + // alias old DBSimple format + if (empty($config['database']) && !empty($config['db'])) + $config['database'] = &$config['db']; + + try + { + $interface = new DibiConnection($config); + } + catch (\Exception $e) + { + return false; + } + + // disable STRICT_TRANS_TABLES and STRICT_ALL_TABLES. It prevents usage of implicit default values. + // disable ONLY_FULL_GROUP_BY (Allows for non-aggregated selects in a group-by query) + $extraModes = ['STRICT_TRANS_TABLES', 'STRICT_ALL_TABLES', 'ONLY_FULL_GROUP_BY', 'NO_ZERO_DATE', 'NO_ZERO_IN_DATE', 'ERROR_FOR_DIVISION_BY_ZERO']; + $oldModes = explode(',', $interface->fetchSingle('SELECT @@sql_mode')); + $newModes = array_diff($oldModes, $extraModes); + if ($oldModes != $newModes) + $interface->query("SET SESSION sql_mode = %s", implode(',', $newModes)); + + $interface->onEvent[] = self::errorLogger(...); + $interface->onEvent[] = self::profiler(...); + + self::$interfaceCache[$idx] = &$interface; + return true; + } + + public static function test(array $options, ?string &$err = '') : bool + { + $defPort = ini_get('mysqli.default_port'); + $port = 0; + if (strstr($options['host'], ':')) + [$options['host'], $port] = explode(':', $options['host']); + + if ($link = mysqli_connect($options['host'], $options['user'], $options['pass'], $options['db'], $port ?: $defPort)) + { + mysqli_close($link); + return true; + } + + $err = '['.mysqli_connect_errno().'] '.mysqli_connect_error(); + return false; + } + + public static function errorLogger(\Dibi\Event $evt/* string $message, array $data */) : void + { + if (!error_reporting()) + return; + + if (!$evt->result instanceof \Exception) + return; + + $msg = <<<MSG + DB ERROR + code: {$evt->result->getCode()} + message: {$evt->result->getMessage()} + query: {$evt->sql} + context: {$evt->source[0]} line {$evt->source[1]} + MSG; + + if (CLI) + fwrite(STDERR, $msg); + else if (User::isInGroup(U_GROUP_ADMIN) && Cfg::get('DEBUG') >= LOG_LEVEL_INFO) + echo PHP_EOL . '<pre>' . $msg . '</pre>' . PHP_EOL; + + trigger_error($evt->result->getMessage(), E_USER_ERROR); + } + + public static function profiler(\Dibi\Event $evt/* mixed $self, string $query, mixed $trace */) : void + { + $query = \dibi::$sql; + $time = \dibi::$elapsedTime; + + self::$logs[] = [str_replace("\n", ' ', $query), $time]; + } + + public static function getProfiles() : string + { + $out = '<pre><table style="font-size:12;"><tr><th></th><th>Time</th><th>Query</th></tr>'; + foreach (self::$logs as $i => [$l, $t]) + { + // t in seconds + $c = 'inherit'; + if ($t > (100 / 1000)) + $c = '#FFA0A0'; + else if ($t > (20 / 1000)) + $c = '#FFFFA0'; + + $out .= '<tr><td>'.++$i.'.</td><td style="background-color:'.$c.';">'.round($t * 1000, 2).'ms</td><td>'.$l.'</td></tr>'; + } + + $out .= '<tr><td><b>∑t:</b></td><td colspan="2"><b>' . round(array_sum(array_column(self::$logs, 1)) * 1000, 2) . 'ms</b></td></tr>'; + + return Util::jsEscape($out).'</table></pre>'; + } + + public static function load(int $idx, array $config, int $keepAlive = 1 * HOUR) : void + { + self::$optionsCache[$idx] = $config; + if (self::connect($idx)) + self::$interfaceTimes[$idx] = [time() + $keepAlive, $keepAlive]; + } + + public static function isConnected(int $idx) : bool + { + return isset(self::$interfaceCache[$idx]) && self::$interfaceCache[$idx]->isConnected(); + } + + public static function isConnectable(int $idx) : bool + { + return isset(self::$optionsCache[$idx]); + } + + /** + * @static + * @return DibiConnection + */ + public static function Characters(int $realmId) : ?DibiConnection + { + if (!isset(self::$optionsCache[DB_CHARACTERS.$realmId])) + die('Connection info not found for live database of realm #'.$realmId.'. Aborted.'); + + return self::getDB(DB_CHARACTERS.$realmId); + } + + /** + * @static + * @return DibiConnection + */ + public static function Auth() : ?DibiConnection + { + return self::getDB(DB_AUTH); + } + + /** + * @static + * @return DibiConnection + */ + public static function World() : ?DibiConnection + { + return self::getDB(DB_WORLD); + } + + /** + * @static + * @return DibiConnection + */ + public static function Aowow() : ?DibiConnection + { + return self::getDB(DB_AOWOW); + } + + private static function getDB(int $idx) : ?DibiConnection + { + if (self::$interfaceTimes[$idx][0] < time()) + { + self::$interfaceCache[$idx]->disconnect(); + if (!self::connect($idx)) + return null; + + self::$interfaceTimes[$idx][0] = time() + self::$interfaceTimes[$idx][1]; + } + + return self::$interfaceCache[$idx]; + } +} + +?> diff --git a/includes/dbtypes/achievement.class.php b/includes/dbtypes/achievement.class.php index d0b12a37..5a665136 100644 --- a/includes/dbtypes/achievement.class.php +++ b/includes/dbtypes/achievement.class.php @@ -12,14 +12,14 @@ class AchievementList extends DBTypeList public static int $type = Type::ACHIEVEMENT; public static string $brickFile = 'achievement'; - public static string $dataTable = '?_achievement'; + public static string $dataTable = '::achievement'; public array $criteria = []; - protected string $queryBase = 'SELECT `a`.*, `a`.`id` AS ARRAY_KEY FROM ?_achievement a'; + protected string $queryBase = 'SELECT `a`.*, `a`.`id` AS ARRAY_KEY FROM ::achievement a'; protected array $queryOpts = array( 'a' => [['ic'], 'o' => 'orderInGroup ASC'], - 'ic' => ['j' => ['?_icons ic ON ic.id = a.iconId', true], 's' => ', ic.name AS iconString'], - 'ac' => ['j' => ['?_achievementcriteria AS `ac` ON `ac`.`refAchievementId` = `a`.`id`', true], 'g' => '`a`.`id`'] + 'ic' => ['j' => ['::icons ic ON ic.id = a.iconId', true], 's' => ', ic.name AS iconString'], + 'ac' => ['j' => ['::achievementcriteria AS `ac` ON `ac`.`refAchievementId` = `a`.`id`', true], 'g' => '`a`.`id`'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -30,7 +30,7 @@ class AchievementList extends DBTypeList return; // post processing - $rewards = DB::World()->select( + $rewards = DB::World()->selectAssoc( 'SELECT ar.`ID` AS ARRAY_KEY, ar.`TitleA`, ar.`TitleH`, ar.`ItemID`, ar.`Sender` AS "sender", ar.`MailTemplateID`, ar.`Subject` AS "subject_loc0", IFNULL(arl2.`Subject`, "") AS "subject_loc2", IFNULL(arl3.`Subject`, "") AS "subject_loc3", IFNULL(arl4.`Subject`, "") AS "subject_loc4", IFNULL(arl6.`Subject`, "") AS "subject_loc6", IFNULL(arl8.`Subject`, "") AS "subject_loc8", ar.`Body` AS "text_loc0", IFNULL(arl2.`Body`, "") AS "text_loc2", IFNULL(arl3.`Body`, "") AS "text_loc3", IFNULL(arl4.`Body`, "") AS "text_loc4", IFNULL(arl6.`Body`, "") AS "text_loc6", IFNULL(arl8.`Body`, "") AS "text_loc8" @@ -40,7 +40,7 @@ class AchievementList extends DBTypeList LEFT JOIN achievement_reward_locale arl4 ON arl4.`ID` = ar.`ID` AND arl4.`Locale` = "zhCN" LEFT JOIN achievement_reward_locale arl6 ON arl6.`ID` = ar.`ID` AND arl6.`Locale` = "esES" LEFT JOIN achievement_reward_locale arl8 ON arl8.`ID` = ar.`ID` AND arl8.`Locale` = "ruRU" - WHERE ar.`ID` IN (?a)', + WHERE ar.`ID` IN %in', $this->getFoundIDs() ); @@ -57,13 +57,13 @@ class AchievementList extends DBTypeList if ($rewards[$_id]['MailTemplateID']) { // using class Loot creates an inifinite loop cirling between Loot, ItemList and SpellList or something - // $mailSrc = new Loot(); - // $mailSrc->getByContainer(LOOT_MAIL, $rewards[$_id]['MailTemplateID']); + // $mailSrc = new LootByContainer(); + // $mailSrc->getByContainer(Loot::MAIL, $rewards[$_id]['MailTemplateID']); // foreach ($mailSrc->iterate() as $loot) // $_curTpl['rewards'][] = [Type::ITEM, $loot['id']]; // lets just assume for now, that mailRewards for achievements do not contain references - $mailRew = DB::World()->selectCol('SELECT `Item` FROM mail_loot_template WHERE `Reference` <= 0 AND `entry` = ?d', $rewards[$_id]['MailTemplateID']); + $mailRew = DB::World()->selectCol('SELECT `Item` FROM mail_loot_template WHERE `Reference` <= 0 AND `entry` = %i', $rewards[$_id]['MailTemplateID']); foreach ($mailRew AS $mr) $_curTpl['rewards'][] = [Type::ITEM, $mr]; } @@ -140,7 +140,7 @@ class AchievementList extends DBTypeList if (isset($this->criteria[$this->id])) return $this->criteria[$this->id]; - $result = DB::Aowow()->Select('SELECT * FROM ?_achievementcriteria WHERE `refAchievementId` = ?d ORDER BY `order` ASC', $this->curTpl['refAchievement'] ?: $this->id); + $result = DB::Aowow()->selectAssoc('SELECT * FROM ::achievementcriteria WHERE `refAchievementId` = %i ORDER BY `order` ASC', $this->curTpl['refAchievement'] ?: $this->id); if (!$result) return []; @@ -310,7 +310,7 @@ class AchievementListFilter extends Filter 'cr' => [parent::V_RANGE, [2, 18], true ], // criteria ids 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators 'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / description - only printable chars, no delimiter + 'na' => [parent::V_NAME, false, false], // name / description - only printable chars, no delimiter 'ex' => [parent::V_EQUAL, 'on', false], // extended name search 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter 'si' => [parent::V_LIST, [SIDE_ALLIANCE, SIDE_HORDE, SIDE_BOTH, -SIDE_ALLIANCE, -SIDE_HORDE], false], // side @@ -328,9 +328,9 @@ class AchievementListFilter extends Filter { $_ = []; if ($_v['ex'] == 'on') - $_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value, 'reward_loc'.Lang::getLocale()->value, 'description_loc'.Lang::getLocale()->value]); + $_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value], ['na', 'reward_loc'.Lang::getLocale()->value], ['na', 'description_loc'.Lang::getLocale()->value]]); else - $_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]); + $_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]); if ($_) $parts[] = $_; @@ -381,7 +381,7 @@ class AchievementListFilter extends Filter protected function cbSeries(int $cr, int $crs, string $crv, int $seriesFlag) : ?array { if ($this->int2Bool($crs)) - return $crs ? ['AND', ['chainId', 0, '!'], ['cuFlags', $seriesFlag, '&']] : ['AND', ['chainId', 0, '!'], [['cuFlags', $seriesFlag, '&'], 0]]; + return $crs ? [DB::AND, ['chainId', 0, '!'], ['cuFlags', $seriesFlag, '&']] : [DB::AND, ['chainId', 0, '!'], [['cuFlags', $seriesFlag, '&'], 0]]; return null; } diff --git a/includes/dbtypes/areatrigger.class.php b/includes/dbtypes/areatrigger.class.php index 57fd4e59..527afb25 100644 --- a/includes/dbtypes/areatrigger.class.php +++ b/includes/dbtypes/areatrigger.class.php @@ -12,13 +12,13 @@ class AreaTriggerList extends DBTypeList public static int $type = Type::AREATRIGGER; public static string $brickFile = 'areatrigger'; - public static string $dataTable = '?_areatrigger'; + public static string $dataTable = '::areatrigger'; public static int $contribute = CONTRIBUTE_CO; - protected string $queryBase = 'SELECT a.*, a.id AS ARRAY_KEY FROM ?_areatrigger a'; + protected string $queryBase = 'SELECT a.*, a.id AS ARRAY_KEY FROM ::areatrigger a'; protected array $queryOpts = array( 'a' => [['s']], // guid < 0 are teleporter targets, so exclude them here - 's' => ['j' => ['?_spawns s ON s.`type` = 503 AND s.`typeId` = a.`id` AND s.`guid` > 0', true], 's' => ', GROUP_CONCAT(s.`areaId`) AS "areaId"', 'g' => 'a.`id`'] + 's' => ['j' => ['::spawns s ON s.`type` = 503 AND s.`typeId` = a.`id` AND s.`guid` > 0', true], 's' => ', GROUP_CONCAT(s.`areaId`) AS "areaId"', 'g' => 'a.`id`'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -27,13 +27,13 @@ class AreaTriggerList extends DBTypeList foreach ($this->iterate() as $id => &$_curTpl) if (!$_curTpl['name']) - $_curTpl['name'] = 'Unnamed Areatrigger #' . $id; + $_curTpl['name'] = Lang::areatrigger('unnamed', [$id]); } public static function getName(int $id) : ?LocString { - if ($n = DB::Aowow()->SelectRow('SELECT IF(`name`, `name`, CONCAT("Unnamed Areatrigger #", `id`) AS "name_loc0" FROM ?# WHERE `id` = ?d', self::$dataTable, $id)) - return new LocString($n); + if ($n = DB::Aowow()->SelectRow('SELECT `name` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id)) + return new LocString($n, callback: fn($x) => $x ?: Lang::areatrigger('unnamed', [$id])); return null; } @@ -73,7 +73,7 @@ class AreaTriggerListFilter extends Filter 'cr' => [parent::V_LIST, [2], true ], // criteria ids 'crs' => [parent::V_RANGE, [1, 6], true ], // criteria operators 'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - all criteria are numeric here - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter + 'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter 'ty' => [parent::V_RANGE, [0, 5], true ] // types ); @@ -85,7 +85,7 @@ class AreaTriggerListFilter extends Filter // name [str] if ($_v['na']) - if ($_ = $this->tokenizeString(['name'])) + if ($_ = $this->buildLikeLookup([['na', 'name']])) $parts[] = $_; // type [list] diff --git a/includes/dbtypes/arenateam.class.php b/includes/dbtypes/arenateam.class.php index 8bb26a37..9103da0c 100644 --- a/includes/dbtypes/arenateam.class.php +++ b/includes/dbtypes/arenateam.class.php @@ -52,9 +52,9 @@ class ArenaTeamListFilter extends Filter protected string $type = 'arenateams'; protected static array $genericFilter = []; protected static array $inputFields = array( - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter + 'ex' => [parent::V_EQUAL, 'on', false], // only match exact - must be defined before 'na' as it's test relies on 'ex's value + 'na' => [parent::V_NAME, true, false], // name - only printable chars, no delimiter 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter - 'ex' => [parent::V_EQUAL, 'on', false], // only match exact 'si' => [parent::V_LIST, [1, 2], false], // side 'sz' => [parent::V_LIST, [2, 3, 5], false], // tema size 'rg' => [parent::V_CALLBACK, 'cbRegionCheck', false], // region @@ -73,7 +73,7 @@ class ArenaTeamListFilter extends Filter // name [str] if ($_v['na']) - if ($_ = $this->tokenizeString(['at.name'], $_v['na'], $_v['ex'] == 'on')) + if ($_ = $this->buildLikeLookup([['na', 'at.name']], $_v['ex'] == 'on')) $parts[] = $_; // side [list] @@ -120,7 +120,7 @@ class RemoteArenaTeamList extends ArenaTeamList // ranks in DB are inaccurate. recalculate from rating (fetched as DESC from DB) foreach ($this->dbNames as $rId => $__) foreach ([2, 3, 5] as $type) - $this->rankOrder[$rId][$type] = DB::Characters($rId)->selectCol('SELECT `arenaTeamId` FROM arena_team WHERE `type` = ?d ORDER BY `rating` DESC', $type); + $this->rankOrder[$rId][$type] = DB::Characters($rId)->selectCol('SELECT `arenaTeamId` FROM arena_team WHERE `type` = %i ORDER BY `rating` DESC', $type); reset($this->dbNames); // only use when querying single realm $realms = Profiler::getRealms(); @@ -168,20 +168,20 @@ class RemoteArenaTeamList extends ArenaTeamList // get team members foreach ($this->members as $realmId => &$teams) - $teams = DB::Characters($realmId)->select( + $teams = DB::Characters($realmId)->selectAssoc( 'SELECT at.`arenaTeamId` AS ARRAY_KEY, c.`guid` AS ARRAY_KEY2, c.`name` AS "0", c.`class` AS "1", IF(at.`captainguid` = c.`guid`, 1, 0) AS "2" FROM arena_team at JOIN arena_team_member atm ON atm.`arenaTeamId` = at.`arenaTeamId` JOIN characters c ON c.`guid` = atm.`guid` - WHERE at.`arenaTeamId` IN (?a) AND c.`deleteInfos_Account` IS NULL AND c.`level` <= ?d AND (c.`extra_flags` & ?d) = 0', + WHERE at.`arenaTeamId` IN %in AND c.`deleteInfos_Account` IS NULL AND c.`level` <= %i AND (c.`extra_flags` & %i) = 0', $teams, MAX_LEVEL, Profiler::CHAR_GMFLAGS ); // equalize subject distribution across realms + $limit = 0; foreach ($conditions as $c) - if (is_int($c)) - $limit = $c; + if (is_numeric($c)) + $limit = max(0, (int)$c); - $limit ??= Cfg::get('SQL_LIMIT_DEFAULT'); if (!$limit) // int:0 means unlimited, so skip early return; @@ -219,7 +219,7 @@ class RemoteArenaTeamList extends ArenaTeamList foreach ($teams as $team) $gladiators = array_merge($gladiators, array_keys($team)); - $profiles[$realmId] = new RemoteProfileList(array(['c.guid', $gladiators], Cfg::get('SQL_LIMIT_NONE')), ['sv' => $realmId]); + $profiles[$realmId] = new RemoteProfileList(array(['c.guid', $gladiators]), ['sv' => $realmId]); if (!$profiles[$realmId]->error) $profiles[$realmId]->initializeLocalEntries(); @@ -228,26 +228,21 @@ class RemoteArenaTeamList extends ArenaTeamList $data = []; foreach ($this->iterate() as $guid => $__) { - $data[$guid] = array( - 'realm' => $this->getField('realm'), - 'realmGUID' => $this->getField('arenaTeamId'), - 'name' => $this->getField('name'), - 'nameUrl' => Profiler::urlize($this->getField('name')), - 'type' => $this->getField('type'), - 'rating' => $this->getField('rating'), - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC - ); + $data['realm'][$guid] = $this->getField('realm'); + $data['realmGUID'][$guid] = $this->getField('arenaTeamId'); + $data['name'][$guid] = $this->getField('name'); + $data['nameUrl'][$guid] = Profiler::urlize($this->getField('name')); + $data['type'][$guid] = $this->getField('type'); + $data['rating'][$guid] = $this->getField('rating'); + $data['stub'][$guid] = 1; } // basic arena team data - foreach (Util::createSqlBatchInsert($data) as $ins) - DB::Aowow()->query('INSERT INTO ?_profiler_arena_team (?#) VALUES '.$ins.' ON DUPLICATE KEY UPDATE `id` = `id`', array_keys(reset($data))); + DB::Aowow()->qry('INSERT INTO ::profiler_arena_team %m ON DUPLICATE KEY UPDATE `id` = `id`', $data); // merge back local ids - $localIds = DB::Aowow()->selectCol( - 'SELECT CONCAT(`realm`, ":", `realmGUID`) AS ARRAY_KEY, `id` FROM ?_profiler_arena_team WHERE `realm` IN (?a) AND `realmGUID` IN (?a)', - array_column($data, 'realm'), - array_column($data, 'realmGUID') + $localIds = DB::Aowow()->selectCol('SELECT CONCAT(`realm`, ":", `realmGUID`) AS ARRAY_KEY, `id` FROM ::profiler_arena_team WHERE `realm` IN %in AND `realmGUID` IN %in', + $data['realm'], $data['realmGUID'] ); foreach ($this->iterate() as $guid => &$_curTpl) @@ -268,26 +263,24 @@ class RemoteArenaTeamList extends ArenaTeamList foreach ($team as $memberId => $member) { $clearMembers[] = $profiles[$realmId]->getEntry($realmId.':'.$memberId)['id']; - $memberData[] = array( - 'arenaTeamId' => $localIds[$realmId.':'.$teamId], - 'profileId' => $profiles[$realmId]->getEntry($realmId.':'.$memberId)['id'], - 'captain' => $member[2] - ); + + $memberData['arenaTeamId'][] = $localIds[$realmId.':'.$teamId]; + $memberData['profileId'][] = $profiles[$realmId]->getEntry($realmId.':'.$memberId)['id']; + $memberData['captain'][] = $member[2]; } // Delete members from other teams of the same type - DB::Aowow()->query( + DB::Aowow()->qry( 'DELETE atm - FROM ?_profiler_arena_team_member atm - JOIN ?_profiler_arena_team at ON atm.`arenaTeamId` = at.`id` AND at.`type` = ?d - WHERE atm.`profileId` IN (?a)', - $data[$realmId.':'.$teamId]['type'] ?? 0, + FROM ::profiler_arena_team_member atm + JOIN ::profiler_arena_team at ON atm.`arenaTeamId` = at.`id` AND at.`type` = %i + WHERE atm.`profileId` IN %in', + $data['type'][$realmId.':'.$teamId] ?? 0, $clearMembers ); } - foreach (Util::createSqlBatchInsert($memberData) as $ins) - DB::Aowow()->query('INSERT INTO ?_profiler_arena_team_member (?#) VALUES '.$ins.' ON DUPLICATE KEY UPDATE `profileId` = `profileId`', array_keys(reset($memberData))); + DB::Aowow()->qry('INSERT INTO ::profiler_arena_team_member %m ON DUPLICATE KEY UPDATE `profileId` = `profileId`', $memberData); } } } @@ -295,11 +288,11 @@ class RemoteArenaTeamList extends ArenaTeamList class LocalArenaTeamList extends ArenaTeamList { - protected string $queryBase = 'SELECT at.*, at.id AS ARRAY_KEY FROM ?_profiler_arena_team at'; + protected string $queryBase = 'SELECT at.*, at.id AS ARRAY_KEY FROM ::profiler_arena_team at'; protected array $queryOpts = array( 'at' => [['atm', 'c'], 'g' => 'ARRAY_KEY', 'o' => 'rating DESC'], - 'atm' => ['j' => '?_profiler_arena_team_member atm ON atm.`arenaTeamId` = at.`id`'], - 'c' => ['j' => '?_profiler_profiles c ON c.`id` = atm.`profileId`', 's' => ', BIT_OR(IF(c.`race` IN (1, 3, 4, 7, 11), 1, 2)) - 1 AS "faction"'] + 'atm' => ['j' => '::profiler_arena_team_member atm ON atm.`arenaTeamId` = at.`id`'], + 'c' => ['j' => '::profiler_profiles c ON c.`id` = atm.`profileId`', 's' => ', BIT_OR(IF(c.`race` IN (1, 3, 4, 7, 11), 1, 2)) - 1 AS "faction"'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -321,8 +314,8 @@ class LocalArenaTeamList extends ArenaTeamList if ($conditions) { - array_unshift($conditions, 'AND'); - $conditions = ['AND', ['realm', array_keys($realms)], $conditions]; + array_unshift($conditions, DB::AND); + $conditions = [DB::AND, ['realm', array_keys($realms)], $conditions]; } else $conditions = [['realm', array_keys($realms)]]; @@ -333,11 +326,11 @@ class LocalArenaTeamList extends ArenaTeamList return; // post processing - $members = DB::Aowow()->select( + $members = DB::Aowow()->selectAssoc( 'SELECT `arenaTeamId` AS ARRAY_KEY, p.`id` AS ARRAY_KEY2, p.`name` AS "0", p.`class` AS "1", atm.`captain` AS "2" - FROM ?_profiler_arena_team_member atm - JOIN ?_profiler_profiles p ON p.`id` = atm.`profileId` - WHERE `arenaTeamId` IN (?a)', + FROM ::profiler_arena_team_member atm + JOIN ::profiler_profiles p ON p.`id` = atm.`profileId` + WHERE `arenaTeamId` IN %in', $this->getFoundIDs() ); diff --git a/includes/dbtypes/charclass.class.php b/includes/dbtypes/charclass.class.php index 97f8a17b..5c6db072 100644 --- a/includes/dbtypes/charclass.class.php +++ b/includes/dbtypes/charclass.class.php @@ -10,9 +10,13 @@ class CharClassList extends DBTypeList { public static int $type = Type::CHR_CLASS; public static string $brickFile = 'class'; - public static string $dataTable = '?_classes'; + public static string $dataTable = '::classes'; - protected string $queryBase = 'SELECT c.*, id AS ARRAY_KEY FROM ?_classes c'; + protected string $queryBase = 'SELECT c.*, c.`id` AS ARRAY_KEY FROM ::classes c'; + protected array $queryOpts = array( + 'c' => [['ic']], + 'ic' => ['j' => ['::icons ic ON ic.`id` = c.`iconId`', true], 's' => ', ic.`name` AS "iconString"'] + ); public function __construct($conditions = [], array $miscData = []) { diff --git a/includes/dbtypes/charrace.class.php b/includes/dbtypes/charrace.class.php index ccfef599..c790aafe 100644 --- a/includes/dbtypes/charrace.class.php +++ b/includes/dbtypes/charrace.class.php @@ -10,9 +10,14 @@ class CharRaceList extends DBTypeList { public static int $type = Type::CHR_RACE; public static string $brickFile = 'race'; - public static string $dataTable = '?_races'; + public static string $dataTable = '::races'; - protected string $queryBase = 'SELECT r.*, id AS ARRAY_KEY FROM ?_races r'; + protected string $queryBase = 'SELECT r.*, r.`id` AS ARRAY_KEY FROM ::races r'; + protected array $queryOpts = array( + 'r' => [['ic0', 'ic1']], + 'ic0' => ['j' => ['::icons ic0 ON ic0.`id` = r.`iconId0`', true], 's' => ', ic0.`name` AS "iconStringMale"'], + 'ic1' => ['j' => ['::icons ic1 ON ic1.`id` = r.`iconId1`', true], 's' => ', ic1.`name` AS "iconStringFemale"'] + ); public function getListviewData() : array { diff --git a/includes/dbtypes/creature.class.php b/includes/dbtypes/creature.class.php index f06d4910..6c259153 100644 --- a/includes/dbtypes/creature.class.php +++ b/includes/dbtypes/creature.class.php @@ -12,18 +12,19 @@ class CreatureList extends DBTypeList public static int $type = Type::NPC; public static string $brickFile = 'npc'; - public static string $dataTable = '?_creature'; + public static string $dataTable = '::creature'; - protected string $queryBase = 'SELECT ct.*, ct.`id` AS ARRAY_KEY FROM ?_creature ct'; + protected string $queryBase = 'SELECT ct.*, ct.`id` AS ARRAY_KEY FROM ::creature ct'; public array $queryOpts = array( - 'ct' => [['ft', 'qse', 'dct1', 'dct2', 'dct3'], 's' => ', IFNULL(dct1.`id`, IFNULL(dct2.`id`, IFNULL(dct3.`id`, 0))) AS "parentId", IFNULL(dct1.`name_loc0`, IFNULL(dct2.`name_loc0`, IFNULL(dct3.`name_loc0`, ""))) AS "parent_loc0", IFNULL(dct1.`name_loc2`, IFNULL(dct2.`name_loc2`, IFNULL(dct3.`name_loc2`, ""))) AS "parent_loc2", IFNULL(dct1.`name_loc3`, IFNULL(dct2.`name_loc3`, IFNULL(dct3.`name_loc3`, ""))) AS "parent_loc3", IFNULL(dct1.`name_loc4`, IFNULL(dct2.`name_loc4`, IFNULL(dct3.`name_loc4`, ""))) AS "`parent_loc4`", IFNULL(dct1.`name_loc6`, IFNULL(dct2.`name_loc6`, IFNULL(dct3.`name_loc6`, ""))) AS "`parent_loc6`", IFNULL(dct1.name_loc8, IFNULL(dct2.`name_loc8`, IFNULL(dct3.`name_loc8`, ""))) AS "parent_loc8", IF(dct1.`difficultyEntry1` = ct.`id`, 1, IF(dct2.`difficultyEntry2` = ct.`id`, 2, IF(dct3.`difficultyEntry3` = ct.`id`, 3, 0))) AS "difficultyMode"'], - 'dct1' => ['j' => ['?_creature dct1 ON ct.`cuFlags` & 0x02 AND dct1.`difficultyEntry1` = ct.`id`', true]], - 'dct2' => ['j' => ['?_creature dct2 ON ct.`cuFlags` & 0x02 AND dct2.`difficultyEntry2` = ct.`id`', true]], - 'dct3' => ['j' => ['?_creature dct3 ON ct.`cuFlags` & 0x02 AND dct3.`difficultyEntry3` = ct.`id`', true]], - 'ft' => ['j' => '?_factiontemplate ft ON ft.`id` = ct.`faction`', 's' => ', ft.`factionId`, IFNULL(ft.`A`, 0) AS "A", IFNULL(ft.`H`, 0) AS "H"'], - 'qse' => ['j' => ['?_quests_startend qse ON qse.`type` = 1 AND qse.`typeId` = ct.id', true], 's' => ', IF(MIN(qse.`method`) = 1 OR MAX(qse.`method`) = 3, 1, 0) AS "startsQuests", IF(MIN(qse.`method`) = 2 OR MAX(qse.`method`) = 3, 1, 0) AS "endsQuests"', 'g' => 'ct.`id`'], - 'qt' => ['j' => '?_quests qt ON qse.`questId` = qt.`id`'], - 's' => ['j' => ['?_spawns s ON s.`type` = 1 AND s.`typeId` = ct.`id`', true]] + 'ct' => [['ft', 'qse', 'dct1', 'dct2', 'dct3'], 's' => ', IFNULL(dct1.`id`, IFNULL(dct2.`id`, IFNULL(dct3.`id`, 0))) AS "parentId", IFNULL(dct1.`name_loc0`, IFNULL(dct2.`name_loc0`, IFNULL(dct3.`name_loc0`, ""))) AS "parent_loc0", IFNULL(dct1.`name_loc2`, IFNULL(dct2.`name_loc2`, IFNULL(dct3.`name_loc2`, ""))) AS "parent_loc2", IFNULL(dct1.`name_loc3`, IFNULL(dct2.`name_loc3`, IFNULL(dct3.`name_loc3`, ""))) AS "parent_loc3", IFNULL(dct1.`name_loc4`, IFNULL(dct2.`name_loc4`, IFNULL(dct3.`name_loc4`, ""))) AS "parent_loc4", IFNULL(dct1.`name_loc6`, IFNULL(dct2.`name_loc6`, IFNULL(dct3.`name_loc6`, ""))) AS "parent_loc6", IFNULL(dct1.name_loc8, IFNULL(dct2.`name_loc8`, IFNULL(dct3.`name_loc8`, ""))) AS "parent_loc8", IF(dct1.`difficultyEntry1` = ct.`id`, 1, IF(dct2.`difficultyEntry2` = ct.`id`, 2, IF(dct3.`difficultyEntry3` = ct.`id`, 3, 0))) AS "difficultyMode"'], + 'nml' => ['j' => ['::creature_search nml ON nml.`id` = ct.`id` AND nml.`locale` = DB_LOC_I']], + 'dct1' => ['j' => ['::creature dct1 ON ct.`cuFlags` & 0x02 AND dct1.`difficultyEntry1` = ct.`id`', true]], + 'dct2' => ['j' => ['::creature dct2 ON ct.`cuFlags` & 0x02 AND dct2.`difficultyEntry2` = ct.`id`', true]], + 'dct3' => ['j' => ['::creature dct3 ON ct.`cuFlags` & 0x02 AND dct3.`difficultyEntry3` = ct.`id`', true]], + 'ft' => ['j' => '::factiontemplate ft ON ft.`id` = ct.`faction`', 's' => ', ft.`factionId`, IFNULL(ft.`A`, 0) AS "A", IFNULL(ft.`H`, 0) AS "H"'], + 'qse' => ['j' => ['::quests_startend qse ON qse.`type` = 1 AND qse.`typeId` = ct.id', true], 's' => ', IF(MIN(qse.`method`) = 1 OR MAX(qse.`method`) = 3, 1, 0) AS "startsQuests", IF(MIN(qse.`method`) = 2 OR MAX(qse.`method`) = 3, 1, 0) AS "endsQuests"', 'g' => 'ct.`id`'], + 'qt' => ['j' => '::quests qt ON qse.`questId` = qt.`id`'], + 's' => ['j' => ['::spawns s ON s.`type` = 1 AND s.`typeId` = ct.`id`', true]] ); public function __construct(array $conditions = [], array $miscData = []) @@ -59,7 +60,7 @@ class CreatureList extends DBTypeList $row3 = [Lang::game('level')]; $fam = $this->curTpl['family']; - if (!($this->curTpl['typeFlags'] & 0x4)) + if (!($this->curTpl['typeFlags'] & NPC_TYPEFLAG_BOSS_MOB)) { $level = $this->curTpl['minLevel']; if ($level != $this->curTpl['maxLevel']) @@ -108,7 +109,7 @@ class CreatureList extends DBTypeList $data[] = $_; if (count($data) == 1 && ($slotId = array_search($data[0], $totems))) - $data = DB::World()->selectCol('SELECT `DisplayId` FROM player_totem_model WHERE `TotemSlot` = ?d', $slotId); + $data = DB::World()->selectCol('SELECT `DisplayId` FROM player_totem_model WHERE `TotemSlot` = %i', $slotId); return !$data ? 0 : $data[array_rand($data)]; } @@ -154,6 +155,21 @@ class CreatureList extends DBTypeList return ($this->curTpl['cuFlags'] & NPC_CU_INSTANCE_BOSS) || ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_BOSS_MOB && $this->curTpl['rank']); } + public function isMineable() : bool + { + return $this->curTpl['skinLootId'] && ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_MINING); + } + + public function isGatherable() : bool + { + return $this->curTpl['skinLootId'] && ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_HERBALISM); + } + + public function isSalvageable() : bool + { + return $this->curTpl['skinLootId'] && ($this->curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING); + } + public function getListviewData(int $addInfoMask = 0x0) : array { /* looks like this data differs per occasion @@ -169,8 +185,8 @@ class CreatureList extends DBTypeList if ($addInfoMask & NPCINFO_REP && $this->getFoundIDs()) { $rewRep = DB::World()->selectCol( - 'SELECT `creature_id` AS ARRAY_KEY, `RewOnKillRepFaction1` AS ARRAY_KEY2, `RewOnKillRepValue1` FROM creature_onkill_reputation WHERE `creature_id` IN (?a) AND `RewOnKillRepFaction1` > 0 UNION - SELECT `creature_id` AS ARRAY_KEY, `RewOnKillRepFaction2` AS ARRAY_KEY2, `RewOnKillRepValue2` FROM creature_onkill_reputation WHERE `creature_id` IN (?a) AND `RewOnKillRepFaction2` > 0', + 'SELECT `creature_id` AS ARRAY_KEY, `RewOnKillRepFaction1` AS ARRAY_KEY2, `RewOnKillRepValue1` FROM creature_onkill_reputation WHERE `creature_id` IN %in AND `RewOnKillRepFaction1` > 0 UNION + SELECT `creature_id` AS ARRAY_KEY, `RewOnKillRepFaction2` AS ARRAY_KEY2, `RewOnKillRepValue2` FROM creature_onkill_reputation WHERE `creature_id` IN %in AND `RewOnKillRepFaction2` > 0', $this->getFoundIDs(), $this->getFoundIDs() ); @@ -313,12 +329,12 @@ class CreatureListFilter extends Filter 31 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots 32 => [parent::CR_FLAG, 'cuFlags', NPC_CU_INSTANCE_BOSS ], // instanceboss 33 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments - 34 => [parent::CR_STRING, 'modelId', STR_MATCH_EXACT | STR_ALLOW_SHORT ], // usemodel [str] (wants int in string fmt <_<) + 34 => [parent::CR_CALLBACK, 'cbUseModel' ], // usemodel [str] 35 => [parent::CR_STRING, 'textureString' ], // useskin [str] 37 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id 38 => [parent::CR_CALLBACK, 'cbRelEvent', null, null ], // relatedevent [enum] 40 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos - 41 => [parent::CR_NYI_PH, 1, null ], // haslocation [yn] [staff] + 41 => [parent::CR_CALLBACK, 'cbHasLocation' ], // haslocation [yn] [staff] 42 => [parent::CR_CALLBACK, 'cbReputation', '>', null ], // increasesrepwith [enum] 43 => [parent::CR_CALLBACK, 'cbReputation', '<', null ], // decreasesrepwith [enum] 44 => [parent::CR_CALLBACK, 'cbSpecialSkinLoot', NPC_TYPEFLAG_SKIN_WITH_ENGINEERING, null ] // salvageable [yn] @@ -328,12 +344,12 @@ class CreatureListFilter extends Filter 'cr' => [parent::V_LIST, [[1, 3],[5, 12], 15, 16, [18, 25], [27, 29], [31, 35], 37, 38, [40, 44]], true ], // criteria ids 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 9999]], true ], // criteria operators 'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiter - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / subname - only printable chars, no delimiter + 'na' => [parent::V_NAME, false, false], // name / subname - only printable chars, no delimiter 'ex' => [parent::V_EQUAL, 'on', false], // also match subname 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter 'fa' => [parent::V_CALLBACK, 'cbPetFamily', true ], // pet family [list] - cat[0] == 1 - 'minle' => [parent::V_RANGE, [1, 99], false], // min level [int] - 'maxle' => [parent::V_RANGE, [1, 99], false], // max level [int] + 'minle' => [parent::V_RANGE, [0, 99], false], // min level [int] + 'maxle' => [parent::V_RANGE, [0, 99], false], // max level [int] 'cl' => [parent::V_RANGE, [0, 4], true ], // classification [list] 'ra' => [parent::V_LIST, [-1, 0, 1], false], // react alliance [int] 'rh' => [parent::V_LIST, [-1, 0, 1], false] // react horde [int] @@ -349,14 +365,21 @@ class CreatureListFilter extends Filter // name [str] if ($_v['na']) { - $_ = []; - if ($_v['ex'] == 'on') - $_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value, 'subname_loc'.Lang::getLocale()->value]); - else - $_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]); + $f = [['na', ['nml.nName', 'nml.nSubname']]]; + if ($_v['ex'] != 'on') + $f = [['na', 'nml.nName']]; - if ($_) + if ($_ = $this->buildMatchLookup($f)) $parts[] = $_; + else + { + $f = [['na', 'name_loc'.Lang::getLocale()->value], ['na', 'subname_loc'.Lang::getLocale()->value]]; + if ($_v['ex'] != 'on') + $f = [$f[0]]; + + if ($_ = $this->buildLikeLookup($f)) + $parts[] = $_; + } } // pet family [list] @@ -376,11 +399,11 @@ class CreatureListFilter extends Filter $parts[] = ['rank', $_v['cl']]; // react Alliance [int] - if ($_v['ra']) + if (!is_null($_v['ra'])) $parts[] = ['ft.A', $_v['ra']]; // react Horde [int] - if ($_v['rh']) + if (!is_null($_v['rh'])) $parts[] = ['ft.H', $_v['rh']]; return $parts; @@ -404,24 +427,24 @@ class CreatureListFilter extends Filter { if ($crs == parent::ENUM_ANY) { - if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ?_events WHERE `holidayId` <> 0')) - if ($cGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_creature WHERE `eventEntry` IN (?a)', $eventIds)) + if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ::events WHERE `holidayId` <> 0')) + if ($cGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_creature WHERE `eventEntry` IN %in', $eventIds)) return ['s.guid', $cGuids]; return [0]; } else if ($crs == parent::ENUM_NONE) { - if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ?_events WHERE `holidayId` <> 0')) - if ($cGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_creature WHERE `eventEntry` IN (?a)', $eventIds)) - return ['s.guid', $cGuids, '!']; + if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ::events WHERE `holidayId` <> 0')) + if ($cGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_creature WHERE `eventEntry` IN %in', $eventIds)) + return [DB::OR, ['s.guid', $cGuids, '!'], ['s.guid', null]]; return [0]; } else if (in_array($crs, self::$enums[$cr])) { - if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ?_events WHERE `holidayId` = ?d', $crs)) - if ($cGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM `game_event_creature` WHERE `eventEntry` IN (?a)', $eventIds)) + if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ::events WHERE `holidayId` = %i', $crs)) + if ($cGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM `game_event_creature` WHERE `eventEntry` IN %in', $eventIds)) return ['s.guid', $cGuids]; return [0]; @@ -435,7 +458,7 @@ class CreatureListFilter extends Filter if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) return null; - return ['AND', ['((minGold + maxGold) / 2)', $crv, $crs]]; + return [DB::AND, ['((minGold + maxGold) / 2)', $crv, $crs]]; } protected function cbQuestRelation(int $cr, int $crs, string $crv, $field, $val) : ?array @@ -443,13 +466,13 @@ class CreatureListFilter extends Filter switch ($crs) { case 1: // any - return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!']]; + return [DB::AND, ['qse.method', $val, '&'], ['qse.questId', null, '!']]; case 2: // alliance - return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&']]; + return [DB::AND, ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&']]; case 3: // horde - return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']]; + return [DB::AND, ['qse.method', $val, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']]; case 4: // both - return ['AND', ['qse.method', $val, '&'], ['qse.questId', null, '!'], ['OR', ['AND', ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]]; + return [DB::AND, ['qse.method', $val, '&'], ['qse.questId', null, '!'], [DB::OR, [DB::AND, ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]]; case 5: // none $this->extraOpts['ct']['h'][] = $field.' = 0'; return [1]; @@ -488,9 +511,9 @@ class CreatureListFilter extends Filter if ($crs) - return ['AND', ['skinLootId', 0, '>'], ['typeFlags', $typeFlag, '&']]; + return [DB::AND, ['skinLootId', 0, '>'], ['typeFlags', $typeFlag, '&']]; else - return ['OR', ['skinLootId', 0], [['typeFlags', $typeFlag, '&'], 0]]; + return [DB::OR, ['skinLootId', 0], [['typeFlags', $typeFlag, '&'], 0]]; } protected function cbRegularSkinLoot(int $cr, int $crs, string $crv, $typeFlag) : ?array @@ -499,9 +522,9 @@ class CreatureListFilter extends Filter return null; if ($crs) - return ['AND', ['skinLootId', 0, '>'], [['typeFlags', $typeFlag, '&'], 0]]; + return [DB::AND, ['skinLootId', 0, '>'], [['typeFlags', $typeFlag, '&'], 0]]; else - return ['OR', ['skinLootId', 0], ['typeFlags', $typeFlag, '&']]; + return [DB::OR, ['skinLootId', 0], ['typeFlags', $typeFlag, '&']]; } protected function cbReputation(int $cr, int $crs, string $crv, $op) : ?array @@ -509,10 +532,10 @@ class CreatureListFilter extends Filter if (!in_array($crs, self::$enums[$cr])) return null; - if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE `id` = ?d', $crs)) + if ($_ = DB::Aowow()->selectRow('SELECT * FROM ::factions WHERE `id` = %i', $crs)) $this->fiReputationCols[] = [$crs, Util::localizedString($_, 'name')]; - if ($cIds = DB::World()->selectCol('SELECT `creature_id` FROM creature_onkill_reputation WHERE (`RewOnKillRepFaction1` = ?d AND `RewOnKillRepValue1` '.$op.' 0) OR (`RewOnKillRepFaction2` = ?d AND `RewOnKillRepValue2` '.$op.' 0)', $crs, $crs)) + if ($cIds = DB::World()->selectCol('SELECT `creature_id` FROM creature_onkill_reputation WHERE (`RewOnKillRepFaction1` = %i AND `RewOnKillRepValue1` '.$op.' 0) OR (`RewOnKillRepFaction2` = %i AND `RewOnKillRepValue2` '.$op.' 0)', $crs, $crs)) return ['id', $cIds]; else return [0]; @@ -527,12 +550,29 @@ class CreatureListFilter extends Filter return null; $facTpls = []; - $facs = new FactionList(array('OR', ['parentFactionId', $crs], ['id', $crs])); + $facs = new FactionList(array(DB::OR, ['parentFactionId', $crs], ['id', $crs])); foreach ($facs->iterate() as $__) $facTpls = array_merge($facTpls, $facs->getField('templateIds')); return $facTpls ? ['faction', $facTpls] : [0]; } + + // input is string, so there is no prompt for an operator. But a CR_NUMERIC expects crs to not be 0 + protected function cbUseModel(int $cr, int $crs, string $crv) : ?array + { + if (!Util::checkNumeric($crv, NUM_CAST_INT)) + return null; + + return ['modelId', $crv]; + } + + protected function cbHasLocation(int $cr, int $crs, string $crv) : ?array + { + if (!$this->int2Bool($crs)) + return null; + + return ['s.typeId', null, $crs ? '!' : null]; + } } ?> diff --git a/includes/dbtypes/currency.class.php b/includes/dbtypes/currency.class.php index a5be1df9..79744097 100644 --- a/includes/dbtypes/currency.class.php +++ b/includes/dbtypes/currency.class.php @@ -10,12 +10,12 @@ class CurrencyList extends DBTypeList { public static int $type = Type::CURRENCY; public static string $brickFile = 'currency'; - public static string $dataTable = '?_currencies'; + public static string $dataTable = '::currencies'; - protected string $queryBase = 'SELECT c.*, c.`id` AS ARRAY_KEY FROM ?_currencies c'; + protected string $queryBase = 'SELECT c.*, c.`id` AS ARRAY_KEY FROM ::currencies c'; protected array $queryOpts = array( 'c' => [['ic']], - 'ic' => ['j' => ['?_icons ic ON ic.`id` = c.`iconId`', true], 's' => ', ic.`name` AS "iconString"'] + 'ic' => ['j' => ['::icons ic ON ic.`id` = c.`iconId`', true], 's' => ', ic.`name` AS "iconString"'] ); public function __construct(array $conditions = [], array $miscData = []) diff --git a/includes/dbtypes/emote.class.php b/includes/dbtypes/emote.class.php index 6c9d5a76..3510d687 100644 --- a/includes/dbtypes/emote.class.php +++ b/includes/dbtypes/emote.class.php @@ -10,9 +10,9 @@ class EmoteList extends DBTypeList { public static int $type = Type::EMOTE; public static string $brickFile = 'emote'; - public static string $dataTable = '?_emotes'; + public static string $dataTable = '::emotes'; - protected string $queryBase = 'SELECT e.*, e.`id` AS ARRAY_KEY FROM ?_emotes e'; + protected string $queryBase = 'SELECT e.*, e.`id` AS ARRAY_KEY FROM ::emotes e'; public function __construct(array $conditions = [], array $miscData = []) { @@ -28,7 +28,7 @@ class EmoteList extends DBTypeList public static function getName(int $id) : ?LocString { - if ($n = DB::Aowow()->SelectRow('SELECT `cmd` AS "name_loc0" FROM ?# WHERE `id` = ?d', self::$dataTable, $id)) + if ($n = DB::Aowow()->SelectRow('SELECT `cmd` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id)) return new LocString($n); return null; } diff --git a/includes/dbtypes/enchantment.class.php b/includes/dbtypes/enchantment.class.php index fbe71b3a..28c170e4 100644 --- a/includes/dbtypes/enchantment.class.php +++ b/includes/dbtypes/enchantment.class.php @@ -12,16 +12,16 @@ class EnchantmentList extends DBTypeList public static int $type = Type::ENCHANTMENT; public static string $brickFile = 'enchantment'; - public static string $dataTable = '?_itemenchantment'; + public static string $dataTable = '::itemenchantment'; private array $jsonStats = []; private ?SpellList $relSpells = null; private array $triggerIds = []; - protected string $queryBase = 'SELECT ie.*, ie.id AS ARRAY_KEY FROM ?_itemenchantment ie'; + protected string $queryBase = 'SELECT ie.*, ie.id AS ARRAY_KEY FROM ::itemenchantment ie'; protected array $queryOpts = array( // 502 => Type::ENCHANTMENT 'ie' => [['is']], - 'is' => ['j' => ['?_item_stats `is` ON `is`.`type` = 502 AND `is`.`typeId` = `ie`.`id`', true], 's' => ', `is`.*'], + 'is' => ['j' => ['::item_stats `is` ON `is`.`type` = 502 AND `is`.`typeId` = `ie`.`id`', true], 's' => ', `is`.*'], ); public function __construct(array $conditions = [], array $miscData = []) @@ -56,17 +56,23 @@ class EnchantmentList extends DBTypeList break; } } - - // issue with scaling stats enchantments - // stats are stored as NOT NULL to be usable by the search filters and such become indistinguishable from scaling enchantments that _actually_ use the value 0 - // so filter the stats container and if it is empty, rebuild from self. .. there are no mixed scaling/static enchantments, right!? - $this->jsonStats[$this->id] = (new StatsContainer)->fromJson($curTpl, true)->filter(); - if (!count($this->jsonStats[$this->id])) - $this->jsonStats[$this->id]->fromEnchantment($curTpl); } if ($relSpells) $this->relSpells = new SpellList(array(['id', $relSpells])); + + // issue with scaling stats enchantments + // stats are stored as NOT NULL to be usable by the search filters and such become indistinguishable from scaling enchantments that _actually_ use the value 0 + // so we can't rely on ::item_stats and always have to calc stats + foreach ($this->iterate() as $ench) + { + $relSpells = []; + foreach ($ench['spells'] as $s) + if ($_ = $this->relSpells->getEntry($s[0])) + $relSpells[$s[0]] = $_; + + $this->jsonStats[$this->id] = (new StatsContainer($relSpells))->fromEnchantment($ench); + } } public function getListviewData(int $addInfoMask = 0x0) : array @@ -122,7 +128,7 @@ class EnchantmentList extends DBTypeList public function getStatGainForCurrent() : array { - return $this->jsonStats[$this->id]->toJson(includeEmpty: false); + return $this->jsonStats[$this->id]->toJson(includeEmpty: true); } public function getRelSpell(int $id) : ?array @@ -231,7 +237,7 @@ class EnchantmentListFilter extends Filter 'cr' => [parent::V_RANGE, [2, 123], true ], // criteria ids 'crs' => [parent::V_RANGE, [1, 15], true ], // criteria operators 'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numerals - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter + 'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter 'ty' => [parent::V_RANGE, [1, 8], true ] // types ); @@ -243,12 +249,12 @@ class EnchantmentListFilter extends Filter //string if ($_v['na']) - if ($_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value])) + if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]])) $parts[] = $_; // type if ($_v['ty']) - $parts[] = ['OR', ['type1', $_v['ty']], ['type2', $_v['ty']], ['type3', $_v['ty']]]; + $parts[] = [DB::OR, ['type1', $_v['ty']], ['type2', $_v['ty']], ['type3', $_v['ty']]]; return $parts; } diff --git a/includes/dbtypes/faction.class.php b/includes/dbtypes/faction.class.php index 6e0b4d1b..cf76280d 100644 --- a/includes/dbtypes/faction.class.php +++ b/includes/dbtypes/faction.class.php @@ -10,13 +10,13 @@ class FactionList extends DBTypeList { public static int $type = Type::FACTION; public static string $brickFile = 'faction'; - public static string $dataTable = '?_factions'; + public static string $dataTable = '::factions'; - protected string $queryBase = 'SELECT f.*, f.`parentFactionId` AS "cat", f.`id` AS ARRAY_KEY FROM ?_factions f'; + protected string $queryBase = 'SELECT f.*, f.`parentFactionId` AS "cat", f.`id` AS ARRAY_KEY FROM ::factions f'; protected array $queryOpts = array( 'f' => [['f2']], - 'f2' => ['j' => ['?_factions f2 ON f.`parentFactionId` = f2.`id`', true], 's' => ', IFNULL(f2.`parentFactionId`, 0) AS "cat2"'], - 'ft' => ['j' => '?_factiontemplate ft ON ft.`factionId` = f.`id`'] + 'f2' => ['j' => ['::factions f2 ON f.`parentFactionId` = f2.`id`', true], 's' => ', IFNULL(f2.`parentFactionId`, 0) AS "cat2"'], + 'ft' => ['j' => '::factiontemplate ft ON ft.`factionId` = f.`id`'] ); public function __construct(array $conditions = [], array $miscData = []) diff --git a/includes/dbtypes/gameobject.class.php b/includes/dbtypes/gameobject.class.php index bcdc0c84..7dcc92c1 100644 --- a/includes/dbtypes/gameobject.class.php +++ b/includes/dbtypes/gameobject.class.php @@ -12,15 +12,16 @@ class GameObjectList extends DBTypeList public static int $type = Type::OBJECT; public static string $brickFile = 'object'; - public static string $dataTable = '?_objects'; + public static string $dataTable = '::objects'; - protected string $queryBase = 'SELECT o.*, o.`id` AS ARRAY_KEY FROM ?_objects o'; + protected string $queryBase = 'SELECT o.*, o.`id` AS ARRAY_KEY FROM ::objects o'; protected array $queryOpts = array( 'o' => [['ft', 'qse']], - 'ft' => ['j' => ['?_factiontemplate ft ON ft.`id` = o.`faction`', true], 's' => ', ft.`factionId`, IFNULL(ft.`A`, 0) AS "A", IFNULL(ft.`H`, 0) AS "H"'], - 'qse' => ['j' => ['?_quests_startend qse ON qse.`type` = 2 AND qse.`typeId` = o.id', true], 's' => ', IF(MIN(qse.`method`) = 1 OR MAX(qse.`method`) = 3, 1, 0) AS "startsQuests", IF(MIN(qse.`method`) = 2 OR MAX(qse.`method`) = 3, 1, 0) AS "endsQuests"', 'g' => 'o.`id`'], - 'qt' => ['j' => '?_quests qt ON qse.`questId` = qt.`id`'], - 's' => ['j' => '?_spawns s ON s.`type` = 2 AND s.`typeId` = o.`id`'] + 'nml' => ['j' => ['::objects_search nml ON nml.`id` = o.`id` AND nml.`locale` = DB_LOC_I']], + 'ft' => ['j' => ['::factiontemplate ft ON ft.`id` = o.`faction`', true], 's' => ', ft.`factionId`, IFNULL(ft.`A`, 0) AS "A", IFNULL(ft.`H`, 0) AS "H"'], + 'qse' => ['j' => ['::quests_startend qse ON qse.`type` = 2 AND qse.`typeId` = o.id', true], 's' => ', IF(MIN(qse.`method`) = 1 OR MAX(qse.`method`) = 3, 1, 0) AS "startsQuests", IF(MIN(qse.`method`) = 2 OR MAX(qse.`method`) = 3, 1, 0) AS "endsQuests"', 'g' => 'o.`id`'], + 'qt' => ['j' => '::quests qt ON qse.`questId` = qt.`id`'], + 's' => ['j' => '::spawns s ON s.`type` = 2 AND s.`typeId` = o.`id`'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -33,11 +34,13 @@ class GameObjectList extends DBTypeList // post processing foreach ($this->iterate() as $_id => &$curTpl) { - if (!$curTpl['name_loc0']) - $curTpl['name_loc0'] = 'Unnamed Object #' . $_id; + if (!$curTpl['name_loc'.Lang::getLocale()->value]) + $curTpl['name_loc'.Lang::getLocale()->value] = Lang::gameObject('unnamed', [$_id]); // unpack miscInfo - $curTpl['lootStack'] = []; + $curTpl['mStone'] = + $curTpl['capture'] = + $curTpl['lootStack'] = null; $curTpl['spells'] = []; if (in_array($curTpl['type'], [OBJECT_GOOBER, OBJECT_RITUAL, OBJECT_SPELLCASTER, OBJECT_FLAGSTAND, OBJECT_FLAGDROP, OBJECT_AURA_GENERATOR, OBJECT_TRAP])) @@ -70,7 +73,7 @@ class GameObjectList extends DBTypeList $data[$this->id] = array( 'id' => $this->id, 'name' => Lang::unescapeUISequences($this->getField('name', true), Lang::FMT_RAW), - 'type' => $this->curTpl['typeCat'], + 'type' => $this->getField('typeCat'), 'location' => $this->getSpawns(SPAWNINFO_ZONES) ); @@ -99,7 +102,7 @@ class GameObjectList extends DBTypeList if (isset($this->curTpl['lockId'])) if ($locks = Lang::getLocks($this->curTpl['lockId'])) foreach ($locks as $l) - $x .= '<tr><td>'.sprintf(Lang::game('requires'), $l).'</td></tr>'; + $x .= '<tr><td>'.Lang::game('requires', [$l]).'</td></tr>'; $x .= '</table>'; @@ -165,7 +168,7 @@ class GameObjectListFilter extends Filter 'cr' => [parent::V_LIST, [[1, 5], 7, 11, 13, 15, 16, 18, 50], true ], // criteria ids 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 5000]], true ], // criteria operators 'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numeric input values expected - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter + 'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter 'ma' => [parent::V_EQUAL, 1, false] // match any / all filter ); @@ -178,8 +181,12 @@ class GameObjectListFilter extends Filter // name if ($_v['na']) - if ($_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value])) + { + if ($_ = $this->buildMatchLookup([['na', 'nml.nName']])) $parts[] = $_; + else if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]])) + $parts[] = $_; + } return $parts; } @@ -187,7 +194,7 @@ class GameObjectListFilter extends Filter protected function cbOpenable(int $cr, int $crs, string $crv) : ?array { if ($this->int2Bool($crs)) - return $crs ? ['OR', ['flags', 0x2, '&'], ['type', 3]] : ['AND', [['flags', 0x2, '&'], 0], ['type', 3, '!']]; + return $crs ? [DB::OR, ['flags', 0x2, '&'], ['type', 3]] : [DB::AND, [['flags', 0x2, '&'], 0], ['type', 3, '!']]; return null; } @@ -197,13 +204,13 @@ class GameObjectListFilter extends Filter switch ($crs) { case 1: // any - return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!']]; + return [DB::AND, ['qse.method', $value, '&'], ['qse.questId', null, '!']]; case 2: // alliance only - return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&']]; + return [DB::AND, ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&']]; case 3: // horde only - return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']]; + return [DB::AND, ['qse.method', $value, '&'], ['qse.questId', null, '!'], [['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']]; case 4: // both - return ['AND', ['qse.method', $value, '&'], ['qse.questId', null, '!'], ['OR', ['AND', ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]]; + return [DB::AND, ['qse.method', $value, '&'], ['qse.questId', null, '!'], [DB::OR, [DB::AND, ['qt.reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['qt.reqRaceMask', ChrRace::MASK_HORDE, '&']], ['qt.reqRaceMask', 0]]]; case 5: // none todo (low): broken, if entry starts and ends quests... $this->extraOpts['o']['h'][] = $field.' = 0'; return [1]; @@ -216,24 +223,24 @@ class GameObjectListFilter extends Filter { if ($crs == parent::ENUM_ANY) { - if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ?_events WHERE `holidayId` <> 0')) - if ($goGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_gameobject WHERE `eventEntry` IN (?a)', $eventIds)) + if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ::events WHERE `holidayId` <> 0')) + if ($goGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_gameobject WHERE `eventEntry` IN %in', $eventIds)) return ['s.guid', $goGuids]; return [0]; } else if ($crs == parent::ENUM_NONE) { - if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ?_events WHERE `holidayId` <> 0')) - if ($goGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_gameobject WHERE `eventEntry` IN (?a)', $eventIds)) - return ['s.guid', $goGuids, '!']; + if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ::events WHERE `holidayId` <> 0')) + if ($goGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_gameobject WHERE `eventEntry` IN %in', $eventIds)) + return [DB::OR, ['s.guid', $goGuids, '!'], ['s.guid', null]]; return [0]; } else if (in_array($crs, self::$enums[$cr])) { - if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ?_events WHERE `holidayId` = ?d', $crs)) - if ($goGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_gameobject WHERE `eventEntry` IN (?a)', $eventIds)) + if ($eventIds = DB::Aowow()->selectCol('SELECT `id` FROM ::events WHERE `holidayId` = %i', $crs)) + if ($goGuids = DB::World()->selectCol('SELECT DISTINCT `guid` FROM game_event_gameobject WHERE `eventEntry` IN %in', $eventIds)) return ['s.guid', $goGuids]; return [0]; diff --git a/includes/dbtypes/guide.class.php b/includes/dbtypes/guide.class.php index 492b47a0..d1cc5f5c 100644 --- a/includes/dbtypes/guide.class.php +++ b/includes/dbtypes/guide.class.php @@ -12,18 +12,18 @@ class GuideList extends DBTypeList public static int $type = Type::GUIDE; public static string $brickFile = 'guide'; - public static string $dataTable = '?_guides'; + public static string $dataTable = '::guides'; public static int $contribute = CONTRIBUTE_CO; private array $article = []; private array $jsGlobals = []; - protected string $queryBase = 'SELECT g.*, g.`id` AS ARRAY_KEY FROM ?_guides g'; + protected string $queryBase = 'SELECT g.*, g.`id` AS ARRAY_KEY FROM ::guides g'; protected array $queryOpts = array( 'g' => [['a', 'c', 'ar'], 'g' => 'g.`id`'], - 'a' => ['j' => ['?_account a ON a.`id` = g.`userId`', true], 's' => ', IFNULL(a.`username`, "") AS "author"'], - 'c' => ['j' => ['?_comments c ON c.`type` = '.Type::GUIDE.' AND c.`typeId` = g.`id` AND (c.`flags` & '.CC_FLAG_DELETED.') = 0', true], 's' => ', COUNT(c.`id`) AS "comments"'], - 'ar' => ['j' => ['?_articles ar ON ar.`type` = 300 AND ar.`typeId` = g.`id`'], 's' => ', MAX(ar.`rev`) AS "latest"'] + 'a' => ['j' => ['::account a ON a.`id` = g.`userId`', true], 's' => ', IFNULL(a.`username`, "") AS "author"'], + 'c' => ['j' => ['::comments c ON c.`type` = '.Type::GUIDE.' AND c.`typeId` = g.`id` AND (c.`flags` & '.CC_FLAG_DELETED.') = 0', true], 's' => ', COUNT(c.`id`) AS "comments"'], + 'ar' => ['j' => ['::articles ar ON ar.`type` = 300 AND ar.`typeId` = g.`id`'], 's' => ', MAX(ar.`rev`) AS "latest"'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -42,7 +42,7 @@ class GuideList extends DBTypeList public static function getName(int $id) : ?LocString { - if ($n = DB::Aowow()->SelectRow('SELECT `title` AS "name_loc0" FROM ?# WHERE `id` = ?d', self::$dataTable, $id)) + if ($n = DB::Aowow()->SelectRow('SELECT `title` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id)) return new LocString($n); return null; } @@ -54,8 +54,15 @@ class GuideList extends DBTypeList if (empty($this->article[$rev])) { - $a = DB::Aowow()->selectRow('SELECT `article`, `rev` FROM ?_articles WHERE ((`type` = ?d AND `typeId` = ?d){ OR `url` = ?}){ AND `rev`= ?d} ORDER BY `rev` DESC LIMIT 1', - Type::GUIDE, $this->id, $this->getField('url') ?: DBSIMPLE_SKIP, $rev < 0 ? DBSIMPLE_SKIP : $rev); + $where = array( + [DB::OR, [[DB::AND, [['`type` = %i', Type::GUIDE], ['`typeId` = %i', $this->id]]]]] + ); + if ($url = $this->getField('url')) + $where[0][1][] = ['`url` = %s', $url]; + if ($rev >= 0) + $where[] = ['`rev`= %i', $rev]; + + $a = DB::Aowow()->selectRow('SELECT `article`, `rev` FROM ::articles WHERE %and ORDER BY `rev` DESC LIMIT 1', $where); $this->article[$a['rev']] = $a['article']; if ($this->article[$a['rev']]) diff --git a/includes/dbtypes/guild.class.php b/includes/dbtypes/guild.class.php index 93f1c9d1..e31ea4cc 100644 --- a/includes/dbtypes/guild.class.php +++ b/includes/dbtypes/guild.class.php @@ -48,18 +48,16 @@ class GuildList extends DBTypeList if (!$guilds) return; - $stats = DB::Aowow()->select('SELECT `guild` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `level`, `gearscore`, `achievementpoints`, IF(`cuFlags` & ?d, 0, 1) AS "synced" FROM ?_profiler_profiles WHERE `guild` IN (?a) ORDER BY `gearscore` DESC', PROFILER_CU_NEEDS_RESYNC, $guilds); + $stats = DB::Aowow()->selectAssoc('SELECT `guild` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `level`, `gearscore`, `achievementpoints` FROM ::profiler_profiles WHERE `guild` IN %in AND `stub` = 0 ORDER BY `gearscore` DESC', $guilds); foreach ($this->iterate() as &$_curTpl) { $id = $_curTpl['id']; if (empty($stats[$id])) continue; - $guildStats = array_filter($stats[$id], function ($x) { return $x['synced']; } ); - if (!$guildStats) - continue; + $guildStats = $stats[$id]; - $nMaxLevel = count(array_filter($stats[$id], function ($x) { return $x['level'] >= MAX_LEVEL; } )); + $nMaxLevel = count(array_filter($stats[$id], fn($x) => $x['level'] >= MAX_LEVEL)); $levelMod = 1.0; if ($nMaxLevel < 25) @@ -96,9 +94,9 @@ class GuildListFilter extends Filter protected string $type = 'guilds'; protected static array $genericFilter = []; protected static array $inputFields = array( - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter + 'ex' => [parent::V_EQUAL, 'on', false], // only match exact - must be defined before 'na' as it's test relies on 'ex's value + 'na' => [parent::V_NAME, true, false], // name - only printable chars, no delimiter 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter - 'ex' => [parent::V_EQUAL, 'on', false], // only match exact 'si' => [parent::V_LIST, [SIDE_ALLIANCE, SIDE_HORDE], false], // side 'rg' => [parent::V_CALLBACK, 'cbRegionCheck', false], // region 'bg' => [parent::V_EQUAL, null, false], // battlegroup - unsued here, but var expected by template @@ -116,7 +114,7 @@ class GuildListFilter extends Filter // name [str] if ($_v['na']) - if ($_ = $this->tokenizeString(['g.name'], $_v['na'], $_v['ex'] == 'on')) + if ($_ = $this->buildLikeLookup([['na', 'g.name']], $_v['ex'] == 'on')) $parts[] = $_; // side [list] @@ -192,11 +190,12 @@ class RemoteGuildList extends GuildList $distrib[$curTpl['realm']]++; } + // equalize subject distribution across realms + $limit = 0; foreach ($conditions as $c) - if (is_int($c)) - $limit = $c; + if (is_numeric($c)) + $limit = max(0, (int)$c); - $limit ??= Cfg::get('SQL_LIMIT_DEFAULT'); if (!$limit) // int:0 means unlimited, so skip early return; @@ -219,27 +218,25 @@ class RemoteGuildList extends GuildList public function initializeLocalEntries() : void { + if (!$this->templates) + return; + $data = []; foreach ($this->iterate() as $guid => $__) { - $data[$guid] = array( - 'realm' => $this->getField('realm'), - 'realmGUID' => $this->getField('guildid'), - 'name' => $this->getField('name'), - 'nameUrl' => Profiler::urlize($this->getField('name')), - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC - ); + $data['realm'][$guid] = $this->getField('realm'); + $data['realmGUID'][$guid] = $this->getField('guildid'); + $data['name'][$guid] = $this->getField('name'); + $data['nameUrl'][$guid] = Profiler::urlize($this->getField('name')); + $data['stub'][$guid] = 1; } // basic guild data - foreach (Util::createSqlBatchInsert($data) as $ins) - DB::Aowow()->query('INSERT INTO ?_profiler_guild (?#) VALUES '.$ins.' ON DUPLICATE KEY UPDATE `id` = `id`', array_keys(reset($data))); + DB::Aowow()->qry('INSERT INTO ::profiler_guild %m ON DUPLICATE KEY UPDATE `id` = `id`', $data); // merge back local ids - $localIds = DB::Aowow()->selectCol( - 'SELECT CONCAT(`realm`, ":", `realmGUID`) AS ARRAY_KEY, `id` FROM ?_profiler_guild WHERE `realm` IN (?a) AND `realmGUID` IN (?a)', - array_column($data, 'realm'), - array_column($data, 'realmGUID') + $localIds = DB::Aowow()->selectCol('SELECT CONCAT(`realm`, ":", `realmGUID`) AS ARRAY_KEY, `id` FROM ::profiler_guild WHERE `realm` IN %in AND `realmGUID` IN %in', + $data['realm'], $data['realmGUID'] ); foreach ($this->iterate() as $guid => &$_curTpl) @@ -251,7 +248,7 @@ class RemoteGuildList extends GuildList class LocalGuildList extends GuildList { - protected string $queryBase = 'SELECT g.*, g.`id` AS ARRAY_KEY FROM ?_profiler_guild g'; + protected string $queryBase = 'SELECT g.*, g.`id` AS ARRAY_KEY FROM ::profiler_guild g'; public function __construct(array $conditions = [], array $miscData = []) { @@ -272,8 +269,8 @@ class LocalGuildList extends GuildList if ($conditions) { - array_unshift($conditions, 'AND'); - $conditions = ['AND', ['realm', array_keys($realms)], $conditions]; + array_unshift($conditions, DB::AND); + $conditions = [DB::AND, ['realm', array_keys($realms)], $conditions]; } else $conditions = [['realm', array_keys($realms)]]; diff --git a/includes/dbtypes/icon.class.php b/includes/dbtypes/icon.class.php index c324847f..c3971a97 100644 --- a/includes/dbtypes/icon.class.php +++ b/includes/dbtypes/icon.class.php @@ -12,27 +12,27 @@ class IconList extends DBTypeList public static int $type = Type::ICON; public static string $brickFile = 'icongallery'; - public static string $dataTable = '?_icons'; + public static string $dataTable = '::icons'; public static int $contribute = CONTRIBUTE_CO; - private string $pseudoQry = 'SELECT `iconId` AS ARRAY_KEY, COUNT(*) FROM ?# WHERE `iconId` IN (?a) GROUP BY `iconId`'; + private string $pseudoQry = 'SELECT `iconId` AS ARRAY_KEY, COUNT(*) FROM %n WHERE `iconId` IN %in GROUP BY `iconId`'; private array $pseudoJoin = array( - 'nItems' => '?_items', - 'nSpells' => '?_spell', - 'nAchievements' => '?_achievement', - 'nCurrencies' => '?_currencies', - 'nPets' => '?_pet' + 'nItems' => '::items', + 'nSpells' => '::spell', + 'nAchievements' => '::achievement', + 'nCurrencies' => '::currencies', + 'nPets' => '::pet' ); - protected string $queryBase = 'SELECT ic.*, ic.`id` AS ARRAY_KEY FROM ?_icons ic'; + protected string $queryBase = 'SELECT ic.*, ic.`id` AS ARRAY_KEY FROM ::icons ic'; /* this works, but takes ~100x more time than i'm comfortable with .. kept as reference protected array $queryOpts = array( // 29 => Type::ICON 'ic' => [['s', 'i', 'a', 'c', 'p'], 'g' => 'ic.id'], - 'i' => ['j' => ['?_items `i` ON `i`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `i`.`id`) AS "nItems"'], - 's' => ['j' => ['?_spell `s` ON `s`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `s`.`id`) AS "nSpells"'], - 'a' => ['j' => ['?_achievement `a` ON `a`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `a`.`id`) AS "nAchievements"'], - 'c' => ['j' => ['?_currencies `c` ON `c`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `c`.`id`) AS "nCurrencies"'], - 'p' => ['j' => ['?_pet `p` ON `p`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `p`.`id`) AS "nPets"'] + 'i' => ['j' => ['::items `i` ON `i`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `i`.`id`) AS "nItems"'], + 's' => ['j' => ['::spell `s` ON `s`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `s`.`id`) AS "nSpells"'], + 'a' => ['j' => ['::achievement `a` ON `a`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `a`.`id`) AS "nAchievements"'], + 'c' => ['j' => ['::currencies `c` ON `c`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `c`.`id`) AS "nCurrencies"'], + 'p' => ['j' => ['::pet `p` ON `p`.`iconId` = `ic`.`id`', true], 's' => ', COUNT(DISTINCT `p`.`id`) AS "nPets"'] ); */ @@ -53,7 +53,7 @@ class IconList extends DBTypeList public static function getName(int $id) : ?LocString { - if ($n = DB::Aowow()->selectRow('SELECT `name` AS "name_loc0" FROM ?# WHERE `id` = ?d', self::$dataTable, $id)) + if ($n = DB::Aowow()->selectRow('SELECT `name` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id)) return new LocString($n); return null; } @@ -66,7 +66,7 @@ class IconList extends DBTypeList { $data[$this->id] = array( 'id' => $this->id, - 'name' => $this->getField('name', true, true), + 'name' => $this->getField('name_source', true, true), 'icon' => $this->getField('name', true, true), 'itemcount' => (int)$this->getField('nItems'), 'spellcount' => (int)$this->getField('nSpells'), @@ -103,17 +103,17 @@ class IconListFilter extends Filter { private array $iconTotals = []; private array $criterion2field = array( - 1 => '?_items', // items [num] - 2 => '?_spell', // spells [num] - 3 => '?_achievement', // achievements [num] + 1 => '::items', // items [num] + 2 => '::spell', // spells [num] + 3 => '::achievement', // achievements [num] // 4 => '', // battlepets [num] // 5 => '', // battlepetabilities [num] - 6 => '?_currencies', // currencies [num] + 6 => '::currencies', // currencies [num] // 7 => '', // garrisonabilities [num] // 8 => '', // garrisonbuildings [num] - 9 => '?_pet', // hunterpets [num] + 9 => '::pet', // hunterpets [num] // 10 => '', // garrisonmissionthreats [num] - 11 => '', // classes [num] + 11 => '::classes', // classes [num] 13 => '' // used [num] ); @@ -124,7 +124,7 @@ class IconListFilter extends Filter 3 => [parent::CR_CALLBACK, 'cbUsedBy' ], // achievements [num] 6 => [parent::CR_CALLBACK, 'cbUsedBy' ], // currencies [num] 9 => [parent::CR_CALLBACK, 'cbUsedBy' ], // hunterpets [num] - 11 => [parent::CR_NYI_PH, null, 0 ], // classes [num] + 11 => [parent::CR_CALLBACK, 'cbUsedBy' ], // classes [num] 13 => [parent::CR_CALLBACK, 'cbUsedBy', true] // used [num] ); @@ -132,7 +132,7 @@ class IconListFilter extends Filter 'cr' => [parent::V_LIST, [1, 2, 3, 6, 9, 11, 13], true ], // criteria ids 'crs' => [parent::V_RANGE, [1, 6], true ], // criteria operators 'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - all criteria are numeric here - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter + 'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter 'ma' => [parent::V_EQUAL, 1, false] // match any / all filter ); @@ -145,7 +145,7 @@ class IconListFilter extends Filter //string if ($_v['na']) - if ($_ = $this->tokenizeString(['name'])) + if ($_ = $this->buildLikeLookup([['na', 'name']])) $parts[] = $_; return $parts; @@ -186,7 +186,7 @@ class IconListFilter extends Filter if (!$tbl || isset($this->iconTotals[$cr]) || ($forCr && $forCr != $cr)) continue; - $this->iconTotals[$cr] = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM ?# GROUP BY `iconId`', $tbl); + $this->iconTotals[$cr] = DB::Aowow()->selectCol('SELECT `iconId` AS ARRAY_KEY, COUNT(*) AS "n" FROM %n GROUP BY `iconId`', $tbl); } if ($forCr) diff --git a/includes/dbtypes/item.class.php b/includes/dbtypes/item.class.php index d8e11659..2932d2a9 100644 --- a/includes/dbtypes/item.class.php +++ b/includes/dbtypes/item.class.php @@ -12,26 +12,28 @@ class ItemList extends DBTypeList public static int $type = Type::ITEM; public static string $brickFile = 'item'; - public static string $dataTable = '?_items'; + public static string $dataTable = '::items'; public array $json = []; public array $jsonStats = []; public array $rndEnchIds = []; public array $subItems = []; - private array $ssd = []; - private array $vendors = []; - private array $jsGlobals = []; // getExtendedCost creates some and has no access to template - private array $enhanceR = []; - private array $relEnchant = []; + private array $randPropPoints = []; + private array $ssd = []; + private array $vendors = []; + private array $jsGlobals = []; // getExtendedCost creates some and has no access to template + private array $enhanceR = []; + private array $relEnchant = []; - protected string $queryBase = 'SELECT i.*, i.`block` AS "tplBlock", i.`armor` AS tplArmor, i.`dmgMin1` AS "tplDmgMin1", i.`dmgMax1` AS "tplDmgMax1", i.`id` AS ARRAY_KEY, i.`id` AS "id" FROM ?_items i'; + protected string $queryBase = 'SELECT i.*, i.`block` AS "tplBlock", i.`armor` AS tplArmor, i.`dmgMin1` AS "tplDmgMin1", i.`dmgMax1` AS "tplDmgMax1", i.`id` AS ARRAY_KEY, i.`id` AS "id" FROM ::items i'; protected array $queryOpts = array( // 3 => Type::ITEM 'i' => [['is', 'src', 'ic'], 'o' => 'i.`quality` DESC, i.`itemLevel` DESC'], - 'ic' => ['j' => ['?_icons `ic` ON `ic`.`id` = `i`.`iconId`', true], 's' => ', ic.`name` AS "iconString"'], - 'is' => ['j' => ['?_item_stats `is` ON `is`.`type` = 3 AND `is`.`typeId` = `i`.`id`', true], 's' => ', `is`.*'], - 's' => ['j' => ['?_spell `s` ON `s`.`effect1CreateItemId` = `i`.`id`', true], 'g' => 'i.`id`'], - 'e' => ['j' => ['?_events `e` ON `e`.`id` = `i`.`eventId`', true], 's' => ', e.`holidayId`'], - 'src' => ['j' => ['?_source `src` ON `src`.`type` = 3 AND `src`.`typeId` = `i`.`id`', true], 's' => ', `moreType`, `moreTypeId`, `moreZoneId`, `moreMask`, `src1`, `src2`, `src3`, `src4`, `src5`, `src6`, `src7`, `src8`, `src9`, `src10`, `src11`, `src12`, `src13`, `src14`, `src15`, `src16`, `src17`, `src18`, `src19`, `src20`, `src21`, `src22`, `src23`, `src24`'] + 'nml' => ['j' => ['::items_search nml ON nml.`id` = i.`id` AND nml.`locale` = DB_LOC_I']], + 'ic' => ['j' => ['::icons `ic` ON `ic`.`id` = `i`.`iconId`', true], 's' => ', ic.`name` AS "iconString"'], + 'is' => ['j' => ['::item_stats `is` ON `is`.`type` = 3 AND `is`.`typeId` = `i`.`id`', true], 's' => ', `is`.*'], + 's' => ['j' => ['::spell `s` ON `s`.`effect1CreateItemId` = `i`.`id`', true], 'g' => 'i.`id`'], + 'e' => ['j' => ['::events `e` ON `e`.`id` = `i`.`eventId`', true], 's' => ', e.`holidayId`'], + 'src' => ['j' => ['::source `src` ON `src`.`type` = 3 AND `src`.`typeId` = `i`.`id`', true], 's' => ', `moreType`, `moreTypeId`, `moreZoneId`, `moreMask`, `src1`, `src2`, `src3`, `src4`, `src5`, `src6`, `src7`, `src8`, `src9`, `src10`, `src11`, `src12`, `src13`, `src14`, `src15`, `src16`, `src17`, `src18`, `src19`, `src20`, `src21`, `src22`, `src23`, `src24`'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -62,19 +64,6 @@ class ItemList extends DBTypeList $this->relEnchant = $miscData['extraOpts']['relEnchant']; } - // unify those pesky masks - $_ = &$_curTpl['requiredClass']; - $_ &= ChrClass::MASK_ALL; - if ($_ < 0 || $_ == ChrClass::MASK_ALL) - $_ = 0; - unset($_); - - $_ = &$_curTpl['requiredRace']; - $_ &= ChrRace::MASK_ALL; - if ($_ < 0 || $_ == ChrRace::MASK_ALL) - $_ = 0; - unset($_); - // sources for ($i = 1; $i < 25; $i++) { @@ -97,29 +86,28 @@ class ItemList extends DBTypeList if (empty($this->vendors)) { + $itemIds = array_keys($this->templates); + if (!empty($filter[Type::NPC]) && is_array($filter[Type::NPC])) + $itemIds = array_intersect($itemIds, $filter[Type::NPC]); + $itemz = []; $xCostData = []; - $rawEntries = DB::World()->select( - 'SELECT nv.`item`, nv.`entry`, 0 AS "eventId", nv.`maxcount`, nv.`extendedCost`, nv.`incrtime` + $rawEntries = DB::World()->selectAssoc( + 'SELECT nv.`item`, nv.`entry`, 0 AS "eventId", nv.`maxcount`, nv.`extendedCost`, nv.`incrtime` FROM npc_vendor nv - WHERE { nv.`entry` IN (?a) AND } nv.`item` IN (?a) + WHERE nv.`item` IN %in UNION - SELECT nv2.`item`, nv1.`entry`, 0 AS "eventId", nv2.`maxcount`, nv2.`extendedCost`, nv2.`incrtime` + SELECT nv2.`item`, nv1.`entry`, 0 AS "eventId", nv2.`maxcount`, nv2.`extendedCost`, nv2.`incrtime` FROM npc_vendor nv1 - JOIN npc_vendor nv2 ON -nv1.`item` = nv2.`entry` { AND nv1.`entry` IN (?a) } - WHERE nv2.`item` IN (?a) + JOIN npc_vendor nv2 ON -nv1.`item` = nv2.`entry` + WHERE nv2.`item` IN %in UNION - SELECT genv.`item`, c.`id` AS "entry", ge.`eventEntry` AS "eventId", genv.`maxcount`, genv.`extendedCost`, genv.`incrtime` + SELECT genv.`item`, c.`id` AS "entry", ge.`eventEntry` AS "eventId", genv.`maxcount`, genv.`extendedCost`, genv.`incrtime` FROM game_event_npc_vendor genv LEFT JOIN game_event ge ON genv.`eventEntry` = ge.`eventEntry` JOIN creature c ON c.`guid` = genv.`guid` - WHERE { c.`id` IN (?a) AND } genv.`item` IN (?a)', - empty($filter[Type::NPC]) || !is_array($filter[Type::NPC]) ? DBSIMPLE_SKIP : $filter[Type::NPC], - array_keys($this->templates), - empty($filter[Type::NPC]) || !is_array($filter[Type::NPC]) ? DBSIMPLE_SKIP : $filter[Type::NPC], - array_keys($this->templates), - empty($filter[Type::NPC]) || !is_array($filter[Type::NPC]) ? DBSIMPLE_SKIP : $filter[Type::NPC], - array_keys($this->templates) + WHERE genv.`item` IN %in', + $itemIds, $itemIds, $itemIds ); foreach ($rawEntries as $costEntry) @@ -134,7 +122,7 @@ class ItemList extends DBTypeList } if ($xCostData) - $xCostData = DB::Aowow()->select('SELECT *, `id` AS ARRAY_KEY FROM ?_itemextendedcost WHERE `id` IN (?a)', $xCostData); + $xCostData = DB::Aowow()->selectAssoc('SELECT *, `id` AS ARRAY_KEY FROM ::itemextendedcost WHERE `id` IN %in', $xCostData); $cItems = []; foreach ($itemz as $k => $vendors) @@ -438,7 +426,7 @@ class ItemList extends DBTypeList $data[$this->id]['nslots'] = $x; $_ = $this->curTpl['requiredRace']; - if ($_ && $_ & ChrRace::MASK_ALLIANCE != ChrRace::MASK_ALLIANCE && $_ & ChrRace::MASK_HORDE != ChrRace::MASK_HORDE) + if (ChrRace::sideFromMask($_) != SIDE_BOTH) $data[$this->id]['reqrace'] = $_; if ($_ = $this->curTpl['requiredClass']) @@ -486,6 +474,11 @@ class ItemList extends DBTypeList 'quality' => $this->curTpl['quality'], 'icon' => $this->curTpl['iconString'] ); + + if ($this->curTpl['class'] == ITEM_CLASS_RECIPE) + $data[Type::ITEM][$id]['completion_category'] = $this->curTpl['class']; + else if ($this->curTpl['class'] == ITEM_CLASS_MISC && in_array($this->curTpl['subClass'], [2, 5, -7])) + $data[Type::ITEM][$id]['completion_category'] = $this->curTpl['class'].'-'.$this->curTpl['subClass']; } if ($addMask & GLOBALINFO_EXTRA) @@ -547,7 +540,7 @@ class ItemList extends DBTypeList if ($this->enhanceR['enchantId'.$i] <= 0) continue; - $enchant = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE `id` = ?d', $this->enhanceR['enchantId'.$i]); + $enchant = DB::Aowow()->selectRow('SELECT * FROM ::itemenchantment WHERE `id` = %i', $this->enhanceR['enchantId'.$i]); if ($this->enhanceR['allocationPct'.$i] > 0) { $amount = intVal($this->enhanceR['allocationPct'.$i] * $this->generateEnchSuffixFactor()); @@ -581,17 +574,17 @@ class ItemList extends DBTypeList if (($_flags & ITEM_FLAG_HEROIC) && $_quality == ITEM_QUALITY_EPIC) $x .= '<br /><span class="q2">'.Lang::item('heroic').'</span>'; - // requires map (todo: reparse ?_zones for non-conflicting data; generate Link to zone) + // requires map (todo: reparse :zones for non-conflicting data; generate Link to zone) if ($_ = $this->curTpl['map']) { - $map = DB::Aowow()->selectRow('SELECT * FROM ?_zones WHERE `mapId` = ?d LIMIT 1', $_); + $map = DB::Aowow()->selectRow('SELECT * FROM ::zones WHERE `mapId` = %i LIMIT 1', $_); $x .= '<br /><a href="?zone='.$_.'" class="q1">'.Util::localizedString($map, 'name').'</a>'; } // requires area if ($this->curTpl['area']) { - $area = DB::Aowow()->selectRow('SELECT * FROM ?_zones WHERE `id` = ?d LIMIT 1', $this->curTpl['area']); + $area = DB::Aowow()->selectRow('SELECT * FROM ::zones WHERE `id` = %i LIMIT 1', $this->curTpl['area']); $x .= '<br />'.Util::localizedString($area, 'name'); } @@ -615,13 +608,13 @@ class ItemList extends DBTypeList $x .= '<br />'.Lang::item('uniqueEquipped', 0); else if ($this->curTpl['itemLimitCategory']) { - $limit = DB::Aowow()->selectRow("SELECT * FROM ?_itemlimitcategory WHERE `id` = ?", $this->curTpl['itemLimitCategory']); + $limit = DB::Aowow()->selectRow('SELECT * FROM ::itemlimitcategory WHERE `id` = %i', $this->curTpl['itemLimitCategory']); $x .= '<br />'.sprintf(Lang::item($limit['isGem'] ? 'uniqueEquipped' : 'unique', 2), Util::localizedString($limit, 'name'), $limit['count']); } // required holiday if ($eId = $this->curTpl['eventId']) - if ($hName = DB::Aowow()->selectRow('SELECT h.* FROM ?_holidays h JOIN ?_events e ON e.`holidayId` = h.`id` WHERE e.`id` = ?d', $eId)) + if ($hName = DB::Aowow()->selectRow('SELECT h.* FROM ::holidays h JOIN ::events e ON e.`holidayId` = h.`id` WHERE e.`id` = %i', $eId)) $x .= '<br />'.sprintf(Lang::game('requires'), '<a href="?event='.$eId.'" class="q1">'.Util::localizedString($hName, 'name').'</a>'); // item begins a quest @@ -718,7 +711,7 @@ class ItemList extends DBTypeList // Item is a gem (don't mix with sockets) if ($geId = $this->curTpl['gemEnchantmentId']) { - $gemEnch = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE `id` = ?d', $geId); + $gemEnch = DB::Aowow()->selectRow('SELECT * FROM ::itemenchantment WHERE `id` = %i', $geId); $x .= '<span class="q1"><a href="?enchantment='.$geId.'">'.Util::localizedString($gemEnch, 'name').'</a></span><br />'; // activation conditions for meta gems @@ -753,6 +746,13 @@ class ItemList extends DBTypeList case Stat::INTELLECT: case Stat::SPIRIT: case Stat::STAMINA: + // case Stat::ARMOR: // unused by 335a client, still set in item_template + // case Stat::FIRE_RESISTANCE: + // case Stat::FROST_RESISTANCE: + // case Stat::HOLY_RESISTANCE: + // case Stat::SHADOW_RESISTANCE: + // case Stat::NATURE_RESISTANCE: + // case Stat::ARCANE_RESISTANCE: $x .= '<span><!--stat'.$statId.'-->'.Lang::item('statType', $type, [ord($qty > 0 ? '+' : '-'), abs($qty)]).'</span><br />'; break; default: // rating with % for reqLevel @@ -768,7 +768,7 @@ class ItemList extends DBTypeList // Enchantment if (isset($enhance['e'])) { - if ($enchText = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE `id` = ?', $enhance['e'])) + if ($enchText = DB::Aowow()->selectRow('SELECT * FROM ::itemenchantment WHERE `id` = %s', $enhance['e'])) $x .= '<span class="q2"><!--e-->'.Util::localizedString($enchText, 'name').'</span><br />'; else { @@ -782,12 +782,12 @@ class ItemList extends DBTypeList // Sockets w/ Gems if (!empty($enhance['g'])) { - $gems = DB::Aowow()->select( + $gems = DB::Aowow()->selectAssoc( 'SELECT it.`id` AS ARRAY_KEY, ic.`name` AS "iconString", ae.*, it.`gemColorMask` AS "colorMask" - FROM ?_items it - JOIN ?_itemenchantment ae ON ae.`id` = it.`gemEnchantmentId` - JOIN ?_icons ic ON ic.`id` = it.`iconId` - WHERE it.`id` IN (?a)', + FROM ::items it + JOIN ::itemenchantment ae ON ae.`id` = it.`gemEnchantmentId` + JOIN ::icons ic ON ic.`id` = it.`iconId` + WHERE it.`id` IN %in', $enhance['g'] ); @@ -849,7 +849,7 @@ class ItemList extends DBTypeList if ($_ = $this->curTpl['socketBonus']) { - $sbonus = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantment WHERE `id` = ?d', $_); + $sbonus = DB::Aowow()->selectRow('SELECT * FROM ::itemenchantment WHERE `id` = %i', $_); $x .= '<span class="q'.($hasMatch ? '2' : '0').'">'.Lang::item('socketBonus', ['<a href="?enchantment='.$_.'">'.Util::localizedString($sbonus, 'name').'</a>']).'</span><br />'; } @@ -867,23 +867,22 @@ class ItemList extends DBTypeList $x .= Lang::formatTime(abs($dur) * 1000, 'item', 'duration').$rt."<br />"; } - $jsg = []; // required classes + $jsg = []; if ($classes = Lang::getClassString($this->curTpl['requiredClass'], $jsg)) { foreach ($jsg as $js) - if (empty($this->jsGlobals[Type::CHR_CLASS][$js])) - $this->jsGlobals[Type::CHR_CLASS][$js] = $js; + $this->jsGlobals[Type::CHR_CLASS][$js] ??= $js; $x .= Lang::game('classes').Lang::main('colon').$classes.'<br />'; } // required races + $jsg = []; if ($races = Lang::getRaceString($this->curTpl['requiredRace'], $jsg)) { foreach ($jsg as $js) - if (empty($this->jsGlobals[Type::CHR_RACE][$js])) - $this->jsGlobals[Type::CHR_RACE][$js] = $js; + $this->jsGlobals[Type::CHR_RACE][$js] ??= $js; $x .= Lang::game('races').Lang::main('colon').$races.'<br />'; } @@ -929,7 +928,7 @@ class ItemList extends DBTypeList // locked or openable if ($locks = Lang::getLocks($this->curTpl['lockId'], $arr, true)) - $x .= '<span class="q0">'.Lang::item('locked').'<br />'.implode('<br />', array_map(function($x) { return sprintf(Lang::game('requires'), $x); }, $locks)).'</span><br />'; + $x .= '<span class="q0">'.Lang::item('locked').'<br />'.implode('<br />', array_map(fn($x) => Lang::game('requires', [$x]), $locks)).'</span><br />'; else if ($this->curTpl['flags'] & ITEM_FLAG_OPENABLE) $x .= '<span class="q2">'.Lang::item('openClick').'</span><br />'; @@ -952,7 +951,7 @@ class ItemList extends DBTypeList $extra = []; if ($cd >= 5000 && $this->curTpl['spellTrigger'.$j] != SPELL_TRIGGER_EQUIP) { - $pt = Util::parseTime($cd); + $pt = DateTime::parse($cd); if (count(array_filter($pt)) == 1) // simple time: use simple method $extra[] = Lang::formatTime($cd, 'item', 'cooldown'); else // build block with generic time @@ -1034,10 +1033,10 @@ class ItemList extends DBTypeList } } - $pieces = DB::Aowow()->select( + $pieces = DB::Aowow()->selectAssoc( 'SELECT b.`id` AS ARRAY_KEY, b.`name_loc0`, b.`name_loc2`, b.`name_loc3`, b.`name_loc4`, b.`name_loc6`, b.`name_loc8`, GROUP_CONCAT(a.`id` SEPARATOR ":") AS "equiv" - FROM ?_items a, ?_items b - WHERE a.`slotBak` = b.`slotBak` AND a.`itemset` = b.`itemset` AND b.`id` IN (?a) + FROM ::items a, ::items b + WHERE a.`slotBak` = b.`slotBak` AND a.`itemset` = b.`itemset` AND b.`id` IN %in GROUP BY b.`id`', array_keys($itemset->pieceToSet) ); @@ -1213,84 +1212,63 @@ class ItemList extends DBTypeList { // is it available for this item? .. does it even exist?! if (empty($this->enhanceR)) - if (DB::World()->selectCell('SELECT 1 FROM item_enchantment_template WHERE `entry` = ?d AND `ench` = ?d', abs($this->getField('randomEnchant')), abs($randId))) - if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_itemrandomenchant WHERE `id` = ?d', $randId)) + if (DB::World()->selectCell('SELECT 1 FROM item_enchantment_template WHERE `entry` = %i AND `ench` = %i', abs($this->getField('randomEnchant')), abs($randId))) + if ($_ = DB::Aowow()->selectRow('SELECT * FROM ::itemrandomenchant WHERE `id` = %i', $randId)) $this->enhanceR = $_; return !empty($this->enhanceR); } // from Trinity - public function generateEnchSuffixFactor() : int + public function generateEnchSuffixFactor() : float { - $rpp = DB::Aowow()->selectRow('SELECT * FROM ?_itemrandomproppoints WHERE `id` = ?', $this->curTpl['itemLevel']); - if (!$rpp) - return 0; + if (empty($this->randPropPoints[$this->curTpl['itemLevel']])) + $this->randPropPoints[$this->curTpl['itemLevel']] = DB::Aowow()->selectRow('SELECT * FROM ::itemrandomproppoints WHERE `id` = %s', $this->curTpl['itemLevel']); - switch ($this->curTpl['slot']) + $rpp = &$this->randPropPoints[$this->curTpl['itemLevel']]; + + if (!$rpp) + return 0.0; + + $fieldIdx = match((int)$this->curTpl['slot']) { - // Items of that type don`t have points - case INVTYPE_NON_EQUIP: - case INVTYPE_BAG: - case INVTYPE_TABARD: - case INVTYPE_AMMO: - case INVTYPE_QUIVER: - case INVTYPE_RELIC: - return 0; - // Select point coefficient - case INVTYPE_HEAD: - case INVTYPE_BODY: - case INVTYPE_CHEST: - case INVTYPE_LEGS: - case INVTYPE_2HWEAPON: - case INVTYPE_ROBE: - $suffixFactor = 1; - break; - case INVTYPE_SHOULDERS: - case INVTYPE_WAIST: - case INVTYPE_FEET: - case INVTYPE_HANDS: - case INVTYPE_TRINKET: - $suffixFactor = 2; - break; - case INVTYPE_NECK: - case INVTYPE_WRISTS: - case INVTYPE_FINGER: - case INVTYPE_SHIELD: - case INVTYPE_CLOAK: - case INVTYPE_HOLDABLE: - $suffixFactor = 3; - break; - case INVTYPE_WEAPON: - case INVTYPE_WEAPONMAINHAND: - case INVTYPE_WEAPONOFFHAND: - $suffixFactor = 4; - break; - case INVTYPE_RANGED: - case INVTYPE_THROWN: - case INVTYPE_RANGEDRIGHT: - $suffixFactor = 5; - break; - default: - return 0; - } + INVTYPE_HEAD, + INVTYPE_BODY, + INVTYPE_CHEST, + INVTYPE_LEGS, + INVTYPE_2HWEAPON, + INVTYPE_ROBE => 1, + INVTYPE_SHOULDERS, + INVTYPE_WAIST, + INVTYPE_FEET, + INVTYPE_HANDS, + INVTYPE_TRINKET => 2, + INVTYPE_NECK, + INVTYPE_WRISTS, + INVTYPE_FINGER, + INVTYPE_SHIELD, + INVTYPE_CLOAK, + INVTYPE_HOLDABLE => 3, + INVTYPE_WEAPON, + INVTYPE_WEAPONMAINHAND, + INVTYPE_WEAPONOFFHAND => 4, + INVTYPE_RANGED, + INVTYPE_THROWN, + INVTYPE_RANGEDRIGHT => 5, + default => 0 // inv types that don`t have points + }; + + if (!$fieldIdx) + return 0.0; // Select rare/epic modifier - switch ($this->curTpl['quality']) + return match((int)$this->curTpl['quality']) { - case ITEM_QUALITY_UNCOMMON: - return $rpp['uncommon'.$suffixFactor] / 10000; - case ITEM_QUALITY_RARE: - return $rpp['rare'.$suffixFactor] / 10000; - case ITEM_QUALITY_EPIC: - return $rpp['epic'.$suffixFactor] / 10000; - case ITEM_QUALITY_LEGENDARY: - case ITEM_QUALITY_ARTIFACT: - return 0; // not have random properties - default: - break; - } - return 0; + ITEM_QUALITY_UNCOMMON => $rpp['uncommon'.$fieldIdx] / 10000, + ITEM_QUALITY_RARE => $rpp['rare'.$fieldIdx] / 10000, + ITEM_QUALITY_EPIC => $rpp['epic'.$fieldIdx] / 10000, + default => 0.0 // qualities that don't have random properties + }; } public function extendJsonStats() : void @@ -1310,8 +1288,7 @@ class ItemList extends DBTypeList if ($enchantments) { - $eStats = DB::Aowow()->select('SELECT *, `typeId` AS ARRAY_KEY FROM ?_item_stats WHERE `type` = ?d AND `typeId` IN (?a)', Type::ENCHANTMENT, array_keys($enchantments)); - Util::checkNumeric($eStats); + $eStats = DB::Aowow()->selectAssoc('SELECT *, `typeId` AS ARRAY_KEY FROM ::item_stats WHERE `type` = %i AND `typeId` IN %in', Type::ENCHANTMENT, array_keys($enchantments)); // and merge enchantments back foreach ($enchantments as $eId => $items) @@ -1352,11 +1329,11 @@ class ItemList extends DBTypeList continue; if ($spell = DB::Aowow()->selectRow( - 'SELECT `effect1AuraId`, `effect1MiscValue`, `effect1BasePoints`, `effect1DieSides`, - `effect2AuraId`, `effect2MiscValue`, `effect2BasePoints`, `effect2DieSides`, - `effect3AuraId`, `effect3MiscValue`, `effect3BasePoints`, `effect3DieSides` - FROM ?_spell - WHERE `id` = ?d', + 'SELECT `effect1Id`, `effect1TriggerSpell`, `effect1AuraId`, `effect1MiscValue`, `effect1BasePoints`, `effect1DieSides`, + `effect2Id`, `effect2TriggerSpell`, `effect2AuraId`, `effect2MiscValue`, `effect2BasePoints`, `effect2DieSides`, + `effect3Id`, `effect3TriggerSpell`, `effect3AuraId`, `effect3MiscValue`, `effect3BasePoints`, `effect3DieSides` + FROM ::spell + WHERE `id` = %i', $this->curTpl['spellId'.$h] )) $onUseStats->fromSpell($spell); @@ -1415,7 +1392,7 @@ class ItemList extends DBTypeList return 0.0; $subClasses = [ITEM_SUBCLASS_MISC_WEAPON]; - $weaponTypeMask = DB::Aowow()->selectCell('SELECT `weaponTypeMask` FROM ?_classes WHERE `id` = ?d', ChrClass::DRUID->value); + $weaponTypeMask = DB::Aowow()->selectCell('SELECT `weaponTypeMask` FROM ::classes WHERE `id` = %i', ChrClass::DRUID->value); if ($weaponTypeMask) for ($i = 0; $i < 21; $i++) if ($weaponTypeMask & (1 << $i)) @@ -1505,12 +1482,12 @@ class ItemList extends DBTypeList if ($mask & (1 << $i)) $field = Util::$ssdMaskFields[$i]; - return $field ? DB::Aowow()->selectCell('SELECT ?# FROM ?_scalingstatvalues WHERE `id` = ?d', $field, $this->ssd[$this->id]['maxLevel']) : 0; + return $field ? DB::Aowow()->selectCell('SELECT %n FROM ::scalingstatvalues WHERE `id` = %i', $field, $this->ssd[$this->id]['maxLevel']) : 0; } private function initScalingStats() : void { - $this->ssd[$this->id] = DB::Aowow()->selectRow('SELECT * FROM ?_scalingstatdistribution WHERE `id` = ?d', $this->curTpl['scalingStatDistribution']); + $this->ssd[$this->id] = DB::Aowow()->selectRow('SELECT * FROM ::scalingstatdistribution WHERE `id` = %i', $this->curTpl['scalingStatDistribution']); if (!$this->ssd[$this->id]) return; @@ -1567,9 +1544,9 @@ class ItemList extends DBTypeList return; // remember: id < 0: randomSuffix; id > 0: randomProperty - $subItemTpls = DB::World()->select( - 'SELECT CAST( `entry` AS SIGNED) AS ARRAY_KEY, CAST( `ench` AS SIGNED) AS ARRAY_KEY2, `chance` FROM item_enchantment_template WHERE `entry` IN (?a) UNION - SELECT CAST(-`entry` AS SIGNED) AS ARRAY_KEY, CAST(-`ench` AS SIGNED) AS ARRAY_KEY2, `chance` FROM item_enchantment_template WHERE `entry` IN (?a)', + $subItemTpls = DB::World()->selectAssoc( + 'SELECT CAST( `entry` AS SIGNED) AS ARRAY_KEY, CAST( `ench` AS SIGNED) AS ARRAY_KEY2, `chance` FROM item_enchantment_template WHERE `entry` IN %in UNION + SELECT CAST(-`entry` AS SIGNED) AS ARRAY_KEY, CAST(-`ench` AS SIGNED) AS ARRAY_KEY2, `chance` FROM item_enchantment_template WHERE `entry` IN %in', array_keys(array_filter($subItemIds, fn($v) => $v > 0)) ?: [0], array_keys(array_filter($subItemIds, fn($v) => $v < 0)) ?: [0] ); @@ -1581,7 +1558,7 @@ class ItemList extends DBTypeList if (!$randIds) return; - $randEnchants = DB::Aowow()->select('SELECT *, `id` AS ARRAY_KEY FROM ?_itemrandomenchant WHERE `id` IN (?a)', $randIds); + $randEnchants = DB::Aowow()->selectAssoc('SELECT *, `id` AS ARRAY_KEY FROM ::itemrandomenchant WHERE `id` IN %in', $randIds); $enchIds = array_unique(array_merge( array_column($randEnchants, 'enchantId1'), array_column($randEnchants, 'enchantId2'), @@ -1590,7 +1567,7 @@ class ItemList extends DBTypeList array_column($randEnchants, 'enchantId5') )); - $enchants = new EnchantmentList(array(['id', $enchIds], Cfg::get('SQL_LIMIT_NONE'))); + $enchants = new EnchantmentList(array(['id', $enchIds])); foreach ($enchants->iterate() as $eId => $_) { $this->rndEnchIds[$eId] = array( @@ -1664,7 +1641,7 @@ class ItemList extends DBTypeList $mh = $j; else if ($j['id'] == $ohItem) $oh = $j; - else if ($j['gearscore']) + else if (!empty($j['gearscore'])) { if ($j['slot'] == INVTYPE_RELIC) $score += 20; @@ -1696,7 +1673,6 @@ class ItemList extends DBTypeList 'level' => $this->curTpl['itemLevel'], 'reqlevel' => $this->curTpl['requiredLevel'], 'displayid' => $this->curTpl['displayId'], - // 'commondrop' => 'true' / null // set if the item is a loot-filler-item .. check common ref-templates..? 'holres' => $this->curTpl['resHoly'], 'firres' => $this->curTpl['resFire'], 'natres' => $this->curTpl['resNature'], @@ -1773,8 +1749,8 @@ class ItemListFilter extends Filter public const /* int */ GROUP_BY_SOURCE = 3; private array $ubFilter = []; // usable-by - limit weapon/armor selection per CharClass - itemClass => available itemsubclasses - private string $extCostQuery = 'SELECT `item` FROM npc_vendor WHERE `extendedCost` IN (?a) UNION - SELECT `item` FROM game_event_npc_vendor WHERE `extendedCost` IN (?a)'; + private string $extCostQuery = 'SELECT `item` FROM npc_vendor WHERE `extendedCost` IN %in UNION + SELECT `item` FROM game_event_npc_vendor WHERE `extendedCost` IN %in'; protected string $type = 'items'; protected static array $enums = array( @@ -1796,14 +1772,14 @@ class ItemListFilter extends Filter 169 => parent::ENUM_EVENT, // requiresevent 158 => parent::ENUM_CURRENCY, // purchasablewithcurrency 118 => array( // itemcurrency - 34853, 34854, 34855, 34856, 34857, 34858, 34848, 34851, 34852, 40625, 40626, 40627, 45632, 45633, 45634, 34169, 34186, 29754, 29753, 29755, - 31089, 31091, 31090, 40610, 40611, 40612, 30236, 30237, 30238, 45635, 45636, 45637, 34245, 34332, 34339, 34345, 40631, 40632, 40633, 45638, - 45639, 45640, 34244, 34208, 34180, 34229, 34350, 40628, 40629, 40630, 45641, 45642, 45643, 29757, 29758, 29756, 31092, 31094, 31093, 40613, - 40614, 40615, 30239, 30240, 30241, 45644, 45645, 45646, 34342, 34211, 34243, 29760, 29761, 29759, 31097, 31095, 31096, 40616, 40617, 40618, - 30242, 30243, 30244, 45647, 45648, 45649, 34216, 29766, 29767, 29765, 31098, 31100, 31099, 40619, 40620, 40621, 30245, 30246, 30247, 45650, - 45651, 45652, 34167, 40634, 40635, 40636, 45653, 45654, 45655, 40637, 40638, 40639, 45656, 45657, 45658, 34170, 34192, 29763, 29764, 29762, - 31101, 31103, 31102, 30248, 30249, 30250, 47557, 47558, 47559, 34233, 34234, 34202, 34195, 34209, 40622, 40623, 40624, 34193, 45659, 45660, - 45661, 34212, 34351, 34215 + 52027, 52030, 52026, 52029, 52025, 52028, 47242, 47557, 47558, 47559, 45632, 45633, 45634, 45635, 45636, 45637, 45638, 45639, 45640, 45641, + 45642, 45643, 45644, 45645, 45646, 45647, 45648, 45649, 45650, 45651, 45652, 45653, 45654, 45655, 45656, 45657, 45658, 45659, 45660, 45661, + 40625, 40626, 40627, 40610, 40611, 40612, 40631, 40632, 40633, 40628, 40629, 40630, 40613, 40614, 40615, 40616, 40617, 40618, 40619, 40620, + 40621, 40634, 40635, 40636, 40637, 40638, 40639, 40622, 40623, 40624, 34853, 34854, 34855, 34856, 34857, 34858, 34848, 34851, 34852, 31089, + 31091, 31090, 31092, 31094, 31093, 31097, 31095, 31096, 31098, 31100, 31099, 31101, 31103, 31102, 30236, 30237, 30238, 30239, 30240, 30241, + 30242, 30243, 30244, 30245, 30246, 30247, 30248, 30249, 30250, 29754, 29753, 29755, 29757, 29758, 29756, 29760, 29761, 29759, 29766, 29767, + 29765, 29763, 29764, 29762, 34169, 34186, 34245, 34332, 34339, 34345, 34244, 34208, 34180, 34229, 34350, 34342, 34211, 34243, 34216, 34167, + 34170, 34192, 34233, 34234, 34202, 34195, 34209, 34193, 34212, 34351, 34215 ), 163 => array( // enchantment mats 34057, 22445, 11176, 34052, 11082, 34055, 16203, 10939, 11135, 11175, 22446, 16204, 34054, 14344, 11084, 11139, 22449, 11178, 10998, 34056, @@ -1907,15 +1883,15 @@ class ItemListFilter extends Filter 64 => [parent::CR_NUMERIC, 'sellPrice', NUM_CAST_INT, true ], // sellprice 65 => [parent::CR_CALLBACK, 'cbAvgMoneyContent', null, null ], // avgmoney [op] [int] 66 => [parent::CR_ENUM, 'requiredSpell' ], // requiresprofspec - 68 => [parent::CR_CALLBACK, 'cbObtainedBy', 15, null ], // otdisenchanting [yn] - 69 => [parent::CR_CALLBACK, 'cbObtainedBy', 16, null ], // otfishing [yn] - 70 => [parent::CR_CALLBACK, 'cbObtainedBy', 17, null ], // otherbgathering [yn] + 68 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_DISENCHANTMENT, null ], // otdisenchanting [yn] + 69 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_FISHING, null ], // otfishing [yn] + 70 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_GATHERING, null ], // otherbgathering [yn] 71 => [parent::CR_FLAG, 'cuFlags', ITEM_CU_OT_ITEMLOOT ], // otitemopening [yn] - 72 => [parent::CR_CALLBACK, 'cbObtainedBy', 2, null ], // otlooting [yn] - 73 => [parent::CR_CALLBACK, 'cbObtainedBy', 19, null ], // otmining [yn] + 72 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_DROP, null ], // otlooting [yn] + 73 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_MINING, null ], // otmining [yn] 74 => [parent::CR_FLAG, 'cuFlags', ITEM_CU_OT_OBJECTLOOT ], // otobjectopening [yn] - 75 => [parent::CR_CALLBACK, 'cbObtainedBy', 21, null ], // otpickpocketing [yn] - 76 => [parent::CR_CALLBACK, 'cbObtainedBy', 23, null ], // otskinning [yn] + 75 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_PICKPOCKETING, null ], // otpickpocketing [yn] + 76 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_SKINNING, null ], // otskinning [yn] 77 => [parent::CR_NUMERIC, 'is.atkpwr', NUM_CAST_INT, true ], // atkpwr 78 => [parent::CR_NUMERIC, 'is.mlehastertng', NUM_CAST_INT, true ], // mlehastertng 79 => [parent::CR_NUMERIC, 'is.resirtng', NUM_CAST_INT, true ], // resirtng @@ -1926,12 +1902,12 @@ class ItemListFilter extends Filter 85 => [parent::CR_CALLBACK, 'cbObjectiveOfQuest', null, null ], // objectivequest [side] 86 => [parent::CR_CALLBACK, 'cbCraftedByProf', null, null ], // craftedprof [enum] 87 => [parent::CR_CALLBACK, 'cbReagentForAbility', null, null ], // reagentforability [enum] - 88 => [parent::CR_CALLBACK, 'cbObtainedBy', 20, null ], // otprospecting [yn] + 88 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_PROSPECTING, null ], // otprospecting [yn] 89 => [parent::CR_FLAG, 'flags', ITEM_FLAG_PROSPECTABLE ], // prospectable 90 => [parent::CR_CALLBACK, 'cbAvgBuyout', null, null ], // avgbuyout [op] [int] 91 => [parent::CR_ENUM, 'totemCategory', false, true ], // tool - 92 => [parent::CR_CALLBACK, 'cbObtainedBy', 5, null ], // soldbyvendor [yn] - 93 => [parent::CR_CALLBACK, 'cbObtainedBy', 3, null ], // otpvp [pvp] + 92 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_VENDOR, null ], // soldbyvendor [yn] + 93 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_PVP, null ], // otpvp [pvp] 94 => [parent::CR_NUMERIC, 'is.splpen', NUM_CAST_INT, true ], // splpen 95 => [parent::CR_NUMERIC, 'is.mlehitrtng', NUM_CAST_INT, true ], // mlehitrtng 96 => [parent::CR_NUMERIC, 'is.critstrkrtng', NUM_CAST_INT, true ], // critstrkrtng @@ -1942,10 +1918,10 @@ class ItemListFilter extends Filter 101 => [parent::CR_NUMERIC, 'is.rgdhastertng', NUM_CAST_INT, true ], // rgdhastertng 102 => [parent::CR_NUMERIC, 'is.splhastertng', NUM_CAST_INT, true ], // splhastertng 103 => [parent::CR_NUMERIC, 'is.hastertng', NUM_CAST_INT, true ], // hastertng - 104 => [parent::CR_STRING, 'description', STR_LOCALIZED ], // flavortext + 104 => [parent::CR_STRING, 'description', STR_LOCALIZED, 'nml.nDescription'], // flavortext 105 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_DUNGEON_DROP, 1 ], // dropsinnormal [heroicdungeon-any] 106 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_DUNGEON_DROP, 2 ], // dropsinheroic [heroicdungeon-any] - 107 => [parent::CR_NYI_PH, null, 1, ], // effecttext [str] not yet parsed ['effectsParsed_loc'.Lang::getLocale()->value, $crv] + 107 => [parent::CR_STRING, '', STR_LOCALIZED, 'nml.nEffects' ], // effecttext [str] 109 => [parent::CR_CALLBACK, 'cbArmorBonus', null, null ], // armorbonus [op] [int] 111 => [parent::CR_NUMERIC, 'requiredSkillRank', NUM_CAST_INT, true ], // reqskillrank 113 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots @@ -1973,7 +1949,7 @@ class ItemListFilter extends Filter 140 => [parent::CR_NUMERIC, 'is.rgddmgmax', NUM_CAST_INT, true ], // rgddmgmax 141 => [parent::CR_NUMERIC, 'is.rgdspeed', NUM_CAST_FLOAT, true ], // rgdspeed 142 => [parent::CR_STRING, 'ic.name' ], // icon - 143 => [parent::CR_CALLBACK, 'cbObtainedBy', 18, null ], // otmilling [yn] + 143 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_MILLING, null ], // otmilling [yn] 144 => [parent::CR_CALLBACK, 'cbPvpPurchasable', 'reqHonorPoints', null ], // purchasablewithhonor [yn] 145 => [parent::CR_CALLBACK, 'cbPvpPurchasable', 'reqArenaPoints', null ], // purchasablewitharena [yn] 146 => [parent::CR_FLAG, 'flags', ITEM_FLAG_HEROIC ], // heroic @@ -1982,8 +1958,8 @@ class ItemListFilter extends Filter 149 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_RAID_DROP, 4, ], // dropsinheroic10 [heroicraid-any] 150 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_RAID_DROP, 8, ], // dropsinheroic25 [heroicraid-any] 151 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true ], // id - 152 => [parent::CR_CALLBACK, 'cbClassRaceSpec', 'requiredClass', ChrClass::MASK_ALL], // classspecific [enum] - 153 => [parent::CR_CALLBACK, 'cbClassRaceSpec', 'requiredRace', ChrRace::MASK_ALL ], // racespecific [enum] + 152 => [parent::CR_CALLBACK, 'cbClassRaceSpec', 'requiredClass' ], // classspecific [enum] + 153 => [parent::CR_CALLBACK, 'cbClassRaceSpec', 'requiredRace' ], // racespecific [enum] 154 => [parent::CR_FLAG, 'flags', ITEM_FLAG_REFUNDABLE ], // refundable 155 => [parent::CR_FLAG, 'flags', ITEM_FLAG_USABLE_ARENA ], // usableinarenas 156 => [parent::CR_FLAG, 'flags', ITEM_FLAG_USABLE_SHAPED ], // usablewhenshapeshifted @@ -1998,8 +1974,8 @@ class ItemListFilter extends Filter 167 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos 168 => [parent::CR_CALLBACK, 'cbFieldHasVal', 'spellId1', LEARN_SPELLS ], // teachesspell [yn] 169 => [parent::CR_ENUM, 'e.holidayId', true, true ], // requiresevent - 171 => [parent::CR_CALLBACK, 'cbObtainedBy', 8, null ], // otredemption [yn] - 172 => [parent::CR_CALLBACK, 'cbObtainedBy', 12, null ], // rewardedbyachievement [yn] + 171 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_REDEMPTION, null ], // otredemption [yn] + 172 => [parent::CR_CALLBACK, 'cbObtainedBy', SRC_ACHIEVEMENT, null ], // rewardedbyachievement [yn] 176 => [parent::CR_STAFFFLAG, 'flags' ], // flags 177 => [parent::CR_STAFFFLAG, 'flagsExtra' ], // flags2 ); @@ -2014,17 +1990,17 @@ class ItemListFilter extends Filter 'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters 'upg' => [parent::V_REGEX, '/[^\d:]/ui', true ], // upgrade item ids 'gb' => [parent::V_LIST, [0, 1, 2, 3], false], // search result grouping - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter + 'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter 'ub' => [parent::V_LIST, [[1, 9], 11], false], // usable by classId 'qu' => [parent::V_RANGE, [0, 7], true ], // quality ids 'ty' => [parent::V_CALLBACK, 'cbTypeCheck', true ], // item type - dynamic by current group 'sl' => [parent::V_CALLBACK, 'cbSlotCheck', true ], // item slot - dynamic by current group 'si' => [parent::V_LIST, [-SIDE_HORDE, -SIDE_ALLIANCE, SIDE_ALLIANCE, SIDE_HORDE, SIDE_BOTH], false], // side - 'minle' => [parent::V_RANGE, [1, 999], false], // item level min - 'maxle' => [parent::V_RANGE, [1, 999], false], // item level max - 'minrl' => [parent::V_RANGE, [1, MAX_LEVEL], false], // required level min - 'maxrl' => [parent::V_RANGE, [1, MAX_LEVEL], false] // required level max + 'minle' => [parent::V_RANGE, [0, 999], false], // item level min + 'maxle' => [parent::V_RANGE, [0, 999], false], // item level max + 'minrl' => [parent::V_RANGE, [0, MAX_LEVEL], false], // required level min + 'maxrl' => [parent::V_RANGE, [0, MAX_LEVEL], false] // required level max ); public array $extraOpts = []; // score for statWeights @@ -2041,9 +2017,8 @@ class ItemListFilter extends Filter foreach ($this->values['wt'] as $k => $v) { - if ($idx = Stat::getIndexFrom(Stat::IDX_FILTER_CR_ID, $v)) + if ($str = Stat::getWeightJson($v)) { - $str = Stat::getJsonString($idx); $qty = intVal($this->values['wtv'][$k]); $select[] = '(IFNULL(`is`.`'.$str.'`, 0) * '.$qty.')'; @@ -2053,7 +2028,7 @@ class ItemListFilter extends Filter } if (count($this->wtCnd) > 1) - array_unshift($this->wtCnd, 'OR'); + array_unshift($this->wtCnd, DB::OR); else if (count($this->wtCnd) == 1) $this->wtCnd = $this->wtCnd[0]; @@ -2073,7 +2048,7 @@ class ItemListFilter extends Filter { if (!$this->ubFilter) { - $classes = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `weaponTypeMask` AS "0", `armorTypeMask` AS "1" FROM ?_classes'); + $classes = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `weaponTypeMask` AS "0", `armorTypeMask` AS "1" FROM ::classes'); foreach ($classes as $cId => [$weaponTypeMask, $armorTypeMask]) { // preselect misc subclasses @@ -2097,7 +2072,7 @@ class ItemListFilter extends Filter $parts = []; $_v = $this->values; - // weights + // weights [list] if ($_v['wt'] && $_v['wtv']) { // gm - gem quality (qualityId) @@ -2110,10 +2085,10 @@ class ItemListFilter extends Filter $this->fiExtraCols[] = $_; } - // upgrade for [form only] + // upgrade for [list] if ($_v['upg']) { - if ($this->upgrades = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `slot` FROM ?_items WHERE `class` IN (?a) AND `id` IN (?a)', [ITEM_CLASS_WEAPON, ITEM_CLASS_GEM, ITEM_CLASS_ARMOR], $_v['upg'])) + if ($this->upgrades = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `slot` FROM ::items WHERE `class` IN %in AND `id` IN %in', [ITEM_CLASS_WEAPON, ITEM_CLASS_GEM, ITEM_CLASS_ARMOR], $_v['upg'])) $parts[] = ['slot', $this->upgrades]; else $_v['upg'] = null; @@ -2121,50 +2096,50 @@ class ItemListFilter extends Filter // name if ($_v['na']) - if ($_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value])) + { + if ($_ = $this->buildMatchLookup([['na', 'nml.nName']])) $parts[] = $_; + else if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]])) + $parts[] = $_; + } - // usable-by (not excluded by requiredClass && armor or weapons match mask from ?_classes) + // usable-by (not excluded by requiredClass && armor or weapons match mask from ::classes) if ($_v['ub']) { $parts[] = array( - 'AND', - ['OR', ['requiredClass', 0], ['requiredClass', $this->list2Mask((array)$_v['ub']), '&']], + DB::AND, + [DB::OR, ['requiredClass', 0], ['requiredClass', $this->list2Mask((array)$_v['ub']), '&']], [ - 'OR', + DB::OR, ['class', [ITEM_CLASS_WEAPON, ITEM_CLASS_ARMOR], '!'], - ['AND', ['class', ITEM_CLASS_WEAPON], ['subclassbak', $this->ubFilter[$_v['ub']][ITEM_CLASS_WEAPON]]], - ['AND', ['class', ITEM_CLASS_ARMOR], ['subclassbak', $this->ubFilter[$_v['ub']][ITEM_CLASS_ARMOR]]] + [DB::AND, ['class', ITEM_CLASS_WEAPON], ['subclassbak', $this->ubFilter[$_v['ub']][ITEM_CLASS_WEAPON]]], + [DB::AND, ['class', ITEM_CLASS_ARMOR], ['subclassbak', $this->ubFilter[$_v['ub']][ITEM_CLASS_ARMOR]]] ] ); } // quality [list] - if ($_v['qu'] !== null) + if ($_v['qu']) $parts[] = ['quality', $_v['qu']]; - // type - if ($_v['ty'] !== null) + // type [list] + if ($_v['ty']) $parts[] = ['subclass', $_v['ty']]; - // slot + // slot [list] if ($_v['sl']) $parts[] = ['slot', $_v['sl']]; // side if ($_v['si']) { - $excl = [['requiredRace', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!']; - $incl = ['OR', ['requiredRace', 0], [['requiredRace', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL]]; - - // we sanitized v['si'] earlier .. right? $parts[] = match ($_v['si']) { - SIDE_BOTH => ['OR', [['flagsExtra', 0x3, '&'], [0, 3]], ['requiredRace', ChrRace::MASK_ALL], ['requiredRace', 0]], - SIDE_HORDE => ['AND', [['flagsExtra', 0x3, '&'], [0, 1]], ['OR', $incl, ['requiredRace', ChrRace::MASK_HORDE, '&']]], - -SIDE_HORDE => ['OR', [['flagsExtra', 0x3, '&'], 1], ['AND', $excl, ['requiredRace', ChrRace::MASK_HORDE, '&']]], - SIDE_ALLIANCE => ['AND', [['flagsExtra', 0x3, '&'], [0, 2]], ['OR', $incl, ['requiredRace', ChrRace::MASK_ALLIANCE, '&']]], - -SIDE_ALLIANCE => ['OR', [['flagsExtra', 0x3, '&'], 2], ['AND', $excl, ['requiredRace', ChrRace::MASK_ALLIANCE, '&']]], + SIDE_BOTH => [DB::OR, [['flagsExtra', 0x3, '&'], [0, 3]], ['requiredRace', 0]], + -SIDE_HORDE => [DB::OR, [['flagsExtra', 0x3, '&'], 1], ['requiredRace', ChrRace::MASK_HORDE, '&']], + -SIDE_ALLIANCE => [DB::OR, [['flagsExtra', 0x3, '&'], 2], ['requiredRace', ChrRace::MASK_ALLIANCE, '&']], + SIDE_HORDE => [DB::AND, [['flagsExtra', 0x3, '&'], [0, 1]], [DB::OR, ['requiredRace', 0], ['requiredRace', ChrRace::MASK_HORDE, '&']]], + SIDE_ALLIANCE => [DB::AND, [['flagsExtra', 0x3, '&'], [0, 2]], [DB::OR, ['requiredRace', 0], ['requiredRace', ChrRace::MASK_ALLIANCE, '&']]], }; } @@ -2213,7 +2188,7 @@ class ItemListFilter extends Filter return match ($crs) { // Meta, Red, Yellow, Blue - 1, 2, 3, 4 => ['OR', ['socketColor1', 1 << ($crs - 1)], ['socketColor2', 1 << ($crs - 1)], ['socketColor3', 1 << ($crs - 1)]], + 1, 2, 3, 4 => [DB::OR, ['socketColor1', 1 << ($crs - 1)], ['socketColor2', 1 << ($crs - 1)], ['socketColor3', 1 << ($crs - 1)]], 5 => ['is.nsockets', 0, '!'], // Yes 6 => ['is.nsockets', 0], // No default => null @@ -2225,7 +2200,7 @@ class ItemListFilter extends Filter return match ($crs) { // Meta, Red, Yellow, Blue - 1, 2, 3, 4 => ['AND', ['gemEnchantmentId', 0, '!'], ['gemColorMask', 1 << ($crs - 1), '&']], + 1, 2, 3, 4 => [DB::AND, ['gemEnchantmentId', 0, '!'], ['gemColorMask', 1 << ($crs - 1), '&']], 5 => ['gemEnchantmentId', 0, '!'], // Yes 6 => ['gemEnchantmentId', 0], // No default => null @@ -2237,7 +2212,7 @@ class ItemListFilter extends Filter return match ($crs) { // major, minor - 1, 2 => ['AND', ['class', ITEM_CLASS_GLYPH], ['subSubClass', $crs]], + 1, 2 => [DB::AND, ['class', ITEM_CLASS_GLYPH], ['subSubClass', $crs]], default => null }; } @@ -2245,10 +2220,17 @@ class ItemListFilter extends Filter protected function cbHasRandEnchant(int $cr, int $crs, string $crv) : ?array { $n = preg_replace(parent::PATTERN_NAME, '', $crv); - $n = $this->transformToken($n, false); + if (!$this->tokenizeString($cr, $n)) + return null; - $randIds = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, ABS(`id`) AS `id`, name_loc?d, `name_loc0` FROM ?_itemrandomenchant WHERE name_loc?d LIKE ?', Lang::getLocale()->value, Lang::getLocale()->value, $n); - $tplIds = $randIds ? DB::World()->select('SELECT `entry`, `ench` FROM item_enchantment_template WHERE `ench` IN (?a)', array_column($randIds, 'id')) : []; + $where = []; + foreach ($this->inTokens[$cr] ?? [] as $tok) + $where[] = ['name_loc%i LIKE %~like~', Lang::getLocale()->value, $tok]; + foreach ($this->exTokens[$cr] ?? [] as $tok) + $where[] = ['name_loc%i NOT LIKE %~like~', Lang::getLocale()->value, $tok]; + + $randIds = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, ABS(`id`) AS `id`, name_loc%i, `name_loc0` FROM ::itemrandomenchant WHERE %and', Lang::getLocale()->value, $where); + $tplIds = $randIds ? DB::World()->selectAssoc('SELECT `entry`, `ench` FROM item_enchantment_template WHERE `ench` IN %in', array_column($randIds, 'id')) : []; foreach ($tplIds as &$set) { $z = array_column($randIds, 'id'); @@ -2280,22 +2262,22 @@ class ItemListFilter extends Filter $this->fiExtraCols[] = $cr; $items = [0]; - if ($costs = DB::Aowow()->selectCol('SELECT `id` FROM ?_itemextendedcost WHERE `reqPersonalrating` '.$crs.' '.$crv)) + if ($costs = DB::Aowow()->selectCol('SELECT `id` FROM ::itemextendedcost WHERE `reqPersonalrating` %SQL %i', $crs, $crv)) $items = DB::World()->selectCol($this->extCostQuery, $costs, $costs); return ['id', $items]; } - protected function cbClassRaceSpec(int $cr, int $crs, string $crv, string $field, int $mask) : ?array + protected function cbClassRaceSpec(int $cr, int $crs, string $crv, string $field) : ?array { if (!isset(self::$enums[$cr][$crs])) return null; $_ = self::$enums[$cr][$crs]; if (is_bool($_)) - return $_ ? ['AND', [[$field, $mask, '&'], $mask, '!'], [$field, 0, '>']] : ['OR', [[$field, $mask, '&'], $mask], [$field, 0]]; + return $_ ? [$field, 0, '>'] : [$field, 0]; else if (is_int($_)) - return ['AND', [[$field, $mask, '&'], $mask, '!'], [$field, 1 << ($_ - 1), '&']]; + return [$field, 1 << ($_ - 1), '&']; return null; } @@ -2305,7 +2287,7 @@ class ItemListFilter extends Filter if (!$this->checkInput(parent::V_RANGE, [SPELL_SCHOOL_NORMAL, SPELL_SCHOOL_ARCANE], $crs)) return null; - return ['OR', ['dmgType1', $crs], ['dmgType2', $crs]]; + return [DB::OR, ['dmgType1', $crs], ['dmgType2', $crs]]; } protected function cbArmorBonus(int $cr, int $crs, string $crv) : ?array @@ -2314,7 +2296,7 @@ class ItemListFilter extends Filter return null; $this->fiExtraCols[] = $cr; - return ['AND', ['armordamagemodifier', $crv, $crs], ['class', ITEM_CLASS_ARMOR]]; + return [DB::AND, ['armordamagemodifier', $crv, $crs], ['class', ITEM_CLASS_ARMOR]]; } protected function cbCraftedByProf(int $cr, int $crs, string $crv) : ?array @@ -2334,7 +2316,7 @@ class ItemListFilter extends Filter protected function cbQuestRewardIn(int $cr, int $crs, string $crv) : ?array { if (in_array($crs, self::$enums[$cr])) - return ['AND', ['src.src4', null, '!'], ['src.moreZoneId', $crs]]; + return [DB::AND, ['src.src4', null, '!'], ['src.moreZoneId', $crs]]; else if ($crs == parent::ENUM_ANY) return ['src.src4', null, '!']; // well, this seems a bit redundant.. @@ -2344,7 +2326,7 @@ class ItemListFilter extends Filter protected function cbDropsInZone(int $cr, int $crs, string $crv) : ?array { if (in_array($crs, self::$enums[$cr])) - return ['AND', ['src.src2', null, '!'], ['src.moreZoneId', $crs]]; + return [DB::AND, ['src.src2', null, '!'], ['src.moreZoneId', $crs]]; else if ($crs == parent::ENUM_ANY) return ['src.src2', null, '!']; // well, this seems a bit redundant.. @@ -2354,9 +2336,9 @@ class ItemListFilter extends Filter protected function cbDropsInInstance(int $cr, int $crs, string $crv, int $moreFlag, int $modeBit) : ?array { if (in_array($crs, self::$enums[$cr])) - return ['AND', ['src.src2', $modeBit, '&'], ['src.moreMask', $moreFlag, '&'], ['src.moreZoneId', $crs]]; + return [DB::AND, ['src.src2', $modeBit, '&'], ['src.moreMask', $moreFlag, '&'], ['src.moreZoneId', $crs]]; else if ($crs == parent::ENUM_ANY) - return ['AND', ['src.src2', $modeBit, '&'], ['src.moreMask', $moreFlag, '&']]; + return [DB::AND, ['src.src2', $modeBit, '&'], ['src.moreMask', $moreFlag, '&']]; return null; } @@ -2371,7 +2353,7 @@ class ItemListFilter extends Filter return null; $costs = DB::Aowow()->selectCol( - 'SELECT `id` FROM ?_itemextendedcost WHERE `reqItemId1` IN (?a) OR `reqItemId2` IN (?a) OR `reqItemId3` IN (?a) OR `reqItemId4` IN (?a) OR `reqItemId5` IN (?a)', + 'SELECT `id` FROM ::itemextendedcost WHERE `reqItemId1` IN %in OR `reqItemId2` IN %in OR `reqItemId3` IN %in OR `reqItemId4` IN %in OR `reqItemId5` IN %in', $_, $_, $_, $_, $_ ); if ($items = DB::World()->selectCol($this->extCostQuery, $costs, $costs)) @@ -2385,7 +2367,7 @@ class ItemListFilter extends Filter if (!Util::checkNumeric($crv, NUM_CAST_INT)) return null; - if ($iIds = DB::World()->selectCol('SELECT `item` FROM npc_vendor WHERE `entry` = ?d UNION SELECT `item` FROM game_event_npc_vendor v JOIN creature c ON c.`guid` = v.`guid` WHERE c.`id` = ?d', $crv, $crv)) + if ($iIds = DB::World()->selectCol('SELECT `item` FROM npc_vendor WHERE `entry` = %i UNION SELECT `item` FROM game_event_npc_vendor v JOIN creature c ON c.`guid` = v.`guid` WHERE c.`id` = %i', $crv, $crv)) return ['i.id', $iIds]; else return [0]; @@ -2400,7 +2382,7 @@ class ItemListFilter extends Filter { // todo: do something sensible.. // // todo (med): get the avgbuyout into the listview - // if ($_ = DB::Characters()->select('SELECT ii.itemEntry AS ARRAY_KEY, AVG(ah.buyoutprice / ii.count) AS buyout FROM auctionhouse ah JOIN item_instance ii ON ah.itemguid = ii.guid GROUP BY ii.itemEntry HAVING buyout '.$crs.' ?f', $c[1])) + // if ($_ = DB::Characters()->selectAssoc('SELECT ii.itemEntry AS ARRAY_KEY, AVG(ah.buyoutprice / ii.count) AS buyout FROM auctionhouse ah JOIN item_instance ii ON ah.itemguid = ii.guid GROUP BY ii.itemEntry HAVING buyout '.$crs.' %f', $c[1])) // return ['i.id', array_keys($_)]; // else // return [0]; @@ -2416,7 +2398,7 @@ class ItemListFilter extends Filter return null; $this->fiExtraCols[] = $cr; - return ['AND', ['flags', ITEM_FLAG_OPENABLE, '&'], ['((minMoneyLoot + maxMoneyLoot) / 2)', $crv, $crs]]; + return [DB::AND, ['flags', ITEM_FLAG_OPENABLE, '&'], ['((minMoneyLoot + maxMoneyLoot) / 2)', $crv, $crs]]; } protected function cbCooldown(int $cr, int $crs, string $crv) : ?array @@ -2430,12 +2412,12 @@ class ItemListFilter extends Filter $this->extraOpts['is']['s'][] = ', GREATEST(`spellCooldown1`, `spellCooldown2`, `spellCooldown3`, `spellCooldown4`, `spellCooldown5`) AS "cooldown"'; return [ - 'OR', - ['AND', ['spellTrigger1', SPELL_TRIGGER_USE], ['spellId1', 0, '!'], ['spellCooldown1', 0, '>'], ['spellCooldown1', $crv, $crs]], - ['AND', ['spellTrigger2', SPELL_TRIGGER_USE], ['spellId2', 0, '!'], ['spellCooldown2', 0, '>'], ['spellCooldown2', $crv, $crs]], - ['AND', ['spellTrigger3', SPELL_TRIGGER_USE], ['spellId3', 0, '!'], ['spellCooldown3', 0, '>'], ['spellCooldown3', $crv, $crs]], - ['AND', ['spellTrigger4', SPELL_TRIGGER_USE], ['spellId4', 0, '!'], ['spellCooldown4', 0, '>'], ['spellCooldown4', $crv, $crs]], - ['AND', ['spellTrigger5', SPELL_TRIGGER_USE], ['spellId5', 0, '!'], ['spellCooldown5', 0, '>'], ['spellCooldown5', $crv, $crs]], + DB::OR, + [DB::AND, ['spellTrigger1', SPELL_TRIGGER_USE], ['spellId1', 0, '!'], ['spellCooldown1', 0, '>'], ['spellCooldown1', $crv, $crs]], + [DB::AND, ['spellTrigger2', SPELL_TRIGGER_USE], ['spellId2', 0, '!'], ['spellCooldown2', 0, '>'], ['spellCooldown2', $crv, $crs]], + [DB::AND, ['spellTrigger3', SPELL_TRIGGER_USE], ['spellId3', 0, '!'], ['spellCooldown3', 0, '>'], ['spellCooldown3', $crv, $crs]], + [DB::AND, ['spellTrigger4', SPELL_TRIGGER_USE], ['spellId4', 0, '!'], ['spellCooldown4', 0, '>'], ['spellCooldown4', $crv, $crs]], + [DB::AND, ['spellTrigger5', SPELL_TRIGGER_USE], ['spellId5', 0, '!'], ['spellCooldown5', 0, '>'], ['spellCooldown5', $crv, $crs]], ]; } @@ -2446,11 +2428,11 @@ class ItemListFilter extends Filter // any 1 => ['startQuest', 0, '>'], // exclude horde only - 2 => ['AND', ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], SIDE_HORDE]], + 2 => [DB::AND, ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], SIDE_HORDE]], // exclude alliance only - 3 => ['AND', ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], SIDE_ALLIANCE]], + 3 => [DB::AND, ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], SIDE_ALLIANCE]], // both - 4 => ['AND', ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], 0]], + 4 => [DB::AND, ['startQuest', 0, '>'], [['flagsExtra', 0x3, '&'], 0]], // none 5 => ['startQuest', 0], default => null @@ -2478,7 +2460,7 @@ class ItemListFilter extends Filter if (!$this->int2Bool($crs)) return null; - $costs = DB::Aowow()->selectCol('SELECT `id` FROM ?_itemextendedcost WHERE ?# > 0', $field); + $costs = DB::Aowow()->selectCol('SELECT `id` FROM ::itemextendedcost WHERE %n > 0', $field); if ($items = DB::World()->selectCol($this->extCostQuery, $costs, $costs)) return ['id', $items, $crs ? null : '!']; @@ -2494,35 +2476,42 @@ class ItemListFilter extends Filter return null; $refResults = []; - $newRefs = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE `item` = ?d AND `reference` = 0', LOOT_REFERENCE, $crs); + $newRefs = DB::World()->selectCol('SELECT `entry` FROM %n WHERE `item` = %i AND `reference` = 0', Loot::REFERENCE, $crs); while ($newRefs) { $refResults += $newRefs; - $newRefs = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE `reference` IN (?a)', LOOT_REFERENCE, $newRefs); + $newRefs = DB::World()->selectCol('SELECT `entry` FROM %n WHERE `reference` IN %in', Loot::REFERENCE, $newRefs); } - $lootIds = DB::World()->selectCol('SELECT `entry` FROM ?# WHERE {`reference` IN (?a) OR }(`reference` = 0 AND `item` = ?d)', LOOT_DISENCHANT, $refResults ?: DBSIMPLE_SKIP, $crs); + $lootIds = DB::World()->selectCol('SELECT `entry` FROM %n', Loot::DISENCHANT, 'WHERE %if', $refResults, '`reference` IN %in OR', $refResults, '%end (`reference` = 0 AND `item` = %i)', $crs); return $lootIds ? ['disenchantId', $lootIds] : [0]; } protected function cbObjectiveOfQuest(int $cr, int $crs, string $crv) : ?array { - $w = match ($crs) + $where = match ($crs) { - 1, 5 => 1, // Yes / No - 2 => '`reqRaceMask` & '.ChrRace::MASK_ALLIANCE.' AND (`reqRaceMask` & '.ChrRace::MASK_HORDE.') = 0', // Alliance - 3 => '`reqRaceMask` & '.ChrRace::MASK_HORDE.' AND (`reqRaceMask` & '.ChrRace::MASK_ALLIANCE.') = 0', // Horde - 4 => '(`reqRaceMask` & '.ChrRace::MASK_ALLIANCE.' AND `reqRaceMask` & '.ChrRace::MASK_HORDE.') OR `reqRaceMask` = 0', // Both + // Yes / No + 1, 5 => [1], + // Alliance + 2 => [['`reqRaceMask` & %i', ChrRace::MASK_ALLIANCE], ['(`reqRaceMask` & %i) = 0', ChrRace::MASK_HORDE]], + // Horde + 3 => [['`reqRaceMask` & %i', ChrRace::MASK_HORDE], ['(`reqRaceMask` & %i) = 0', ChrRace::MASK_ALLIANCE]], + // Both + 4 => [[DB::OR, [['`reqRaceMask` = 0'], [DB::AND, [['`reqRaceMask` & %i', ChrRace::MASK_ALLIANCE], ['`reqRaceMask` & %i', ChrRace::MASK_HORDE]]]]]], default => null }; - $itemIds = DB::Aowow()->selectCol(sprintf( - 'SELECT `reqItemId1` FROM ?_quests WHERE %1$s UNION SELECT `reqItemId2` FROM ?_quests WHERE %1$s UNION - SELECT `reqItemId3` FROM ?_quests WHERE %1$s UNION SELECT `reqItemId4` FROM ?_quests WHERE %1$s UNION - SELECT `reqItemId5` FROM ?_quests WHERE %1$s UNION SELECT `reqItemId6` FROM ?_quests WHERE %1$s', - $w - )); + if (!$where) + return [0]; + + $itemIds = DB::Aowow()->selectCol( + 'SELECT `reqItemId1` FROM ::quests WHERE %and UNION SELECT `reqItemId2` FROM ::quests WHERE %and UNION + SELECT `reqItemId3` FROM ::quests WHERE %and UNION SELECT `reqItemId4` FROM ::quests WHERE %and UNION + SELECT `reqItemId5` FROM ::quests WHERE %and UNION SELECT `reqItemId6` FROM ::quests WHERE %and', + $where, $where, $where, $where, $where, $where + ); if ($itemIds) return ['id', $itemIds, $crs == 5 ? '!' : null]; @@ -2540,11 +2529,11 @@ class ItemListFilter extends Filter return null; $ids = []; - $spells = DB::Aowow()->select( // todo (med): hmm, selecting all using SpellList would exhaust 128MB of memory :x .. see, that we only select the fields that are really needed + $spells = DB::Aowow()->selectAssoc( // todo (med): hmm, selecting all using SpellList would exhaust 128MB of memory :x .. see, that we only select the fields that are really needed 'SELECT `reagent1`, `reagent2`, `reagent3`, `reagent4`, `reagent5`, `reagent6`, `reagent7`, `reagent8`, `reagentCount1`, `reagentCount2`, `reagentCount3`, `reagentCount4`, `reagentCount5`, `reagentCount6`, `reagentCount7`, `reagentCount8` - FROM ?_spell - WHERE `skillLine1` IN (?a)', + FROM ::spell + WHERE `skillLine1` IN %in', is_bool($_) ? array_filter(self::$enums[99], "is_numeric") : $_ ); foreach ($spells as $spell) @@ -2570,7 +2559,7 @@ class ItemListFilter extends Filter return ['src.src'.$_, null, '!']; else if ($_) // any { - $foo = ['OR']; + $foo = [DB::OR]; foreach (self::$enums[$cr] as $bar) if (is_int($bar)) $foo[] = ['src.src'.$bar, null, '!']; diff --git a/includes/dbtypes/itemset.class.php b/includes/dbtypes/itemset.class.php index 58988b63..a80e5886 100644 --- a/includes/dbtypes/itemset.class.php +++ b/includes/dbtypes/itemset.class.php @@ -12,16 +12,16 @@ class ItemsetList extends DBTypeList public static int $type = Type::ITEMSET; public static string $brickFile = 'itemset'; - public static string $dataTable = '?_itemset'; + public static string $dataTable = '::itemset'; public array $pieceToSet = []; // used to build g_items and search private array $classes = []; // used to build g_classes - protected string $queryBase = 'SELECT `set`.*, `set`.`id` AS ARRAY_KEY FROM ?_itemset `set`'; + protected string $queryBase = 'SELECT `set`.*, `set`.`id` AS ARRAY_KEY FROM ::itemset `set`'; protected array $queryOpts = array( 'set' => ['o' => 'maxlevel DESC'], - 'e' => ['j' => ['?_events e ON `e`.`id` = `set`.`eventId`', true], 's' => ', e.`holidayId`'], - 'src' => ['j' => ['?_source src ON `src`.`typeId` = `set`.`id` AND `src`.`type` = 4', true], 's' => ', `src1`, `src2`, `src3`, `src4`, `src5`, `src6`, `src7`, `src8`, `src9`, `src10`, `src11`, `src12`, `src13`, `src14`, `src15`, `src16`, `src17`, `src18`, `src19`, `src20`, `src21`, `src22`, `src23`, `src24`'] + 'e' => ['j' => ['::events e ON `e`.`id` = `set`.`eventId`', true], 's' => ', e.`holidayId`'], + 'src' => ['j' => ['::source src ON `src`.`typeId` = `set`.`id` AND `src`.`type` = 4', true], 's' => ', `src1`, `src2`, `src3`, `src4`, `src5`, `src6`, `src7`, `src8`, `src9`, `src10`, `src11`, `src12`, `src13`, `src14`, `src15`, `src16`, `src17`, `src18`, `src19`, `src20`, `src21`, `src22`, `src23`, `src24`'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -101,8 +101,8 @@ class ItemsetList extends DBTypeList { $jsg = []; $cl = Lang::getClassString($_, $jsg); - $nCl = count($jsg); - $x .= Util::ucFirst($nCl > 1 ? Lang::game('classes') : Lang::game('class')).Lang::main('colon').$cl.'<br />'; + $t = count($jsg) == 1 ? Lang::game('class') : Lang::game('classes'); + $x .= Util::ucFirst($t).Lang::main('colon').$cl.'<br />'; } if ($_ = $this->getField('contentGroup')) @@ -115,8 +115,8 @@ class ItemsetList extends DBTypeList { $x .= '<span>'; - foreach ($bonuses as $b) - $x .= '<br /><span class="q13">'.$b['bonus'].' '.Lang::itemset('_pieces').Lang::main('colon').'</span>'.$b['desc']; + foreach ($bonuses as [$nItems, , $text]) + $x .= '<br /><span class="q13">'.Lang::itemset('_pieces', [$nItems]).'</span>'.$text; $x .= '</span>'; } @@ -136,22 +136,19 @@ class ItemsetList extends DBTypeList // cant use spell as index, would change order if ($spl && $qty) - $spells[] = ['id' => $spl, 'bonus' => $qty]; + $spells[] = [$qty, $spl]; } // sort by required pieces ASC - usort($spells, fn(array $a, array $b) => $a['bonus'] <=> $b['bonus']); + usort($spells, fn(array $a, array $b) => $a[0] <=> $b[0]); - $setSpells = new SpellList(array(['s.id', array_column($spells, 'id')])); - foreach ($setSpells->iterate() as $spellId => $__) + $setSpells = new SpellList(array(['s.id', array_column($spells, 1)])); + foreach ($spells as &$s) { - foreach ($spells as &$s) - { - if ($spellId != $s['id']) - continue; - - $s['desc'] = $setSpells->parseText('description', $this->getField('reqLevel') ?: MAX_LEVEL)[0]; - } + if ($setSpells->getEntry($s[1])) + $s[2] = $setSpells->parseText('description', $this->getField('reqLevel') ?: MAX_LEVEL)[0]; + else + $s[2] = Lang::spell('unkAura', [$s[1]]); } return $spells; @@ -183,14 +180,14 @@ class ItemsetListFilter extends Filter 'cr' => [parent::V_RANGE, [2, 12], true ], // criteria ids 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 424]], true ], // criteria operators 'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / description - only printable chars, no delimiter + 'na' => [parent::V_NAME, false, false], // name / description - only printable chars, no delimiter 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter 'qu' => [parent::V_RANGE, [0, 7], true ], // quality 'ty' => [parent::V_RANGE, [1, 12], true ], // set type - 'minle' => [parent::V_RANGE, [1, 999], false], // min item level - 'maxle' => [parent::V_RANGE, [1, 999], false], // max itemlevel - 'minrl' => [parent::V_RANGE, [1, MAX_LEVEL], false], // min required level - 'maxrl' => [parent::V_RANGE, [1, MAX_LEVEL], false], // max required level + 'minle' => [parent::V_RANGE, [0, 999], false], // min item level + 'maxle' => [parent::V_RANGE, [0, 999], false], // max itemlevel + 'minrl' => [parent::V_RANGE, [0, MAX_LEVEL], false], // min required level + 'maxrl' => [parent::V_RANGE, [0, MAX_LEVEL], false], // max required level 'cl' => [parent::V_LIST, [[1, 9], 11], false], // class 'ta' => [parent::V_RANGE, [1, 30], false] // tag / content group ); @@ -202,7 +199,7 @@ class ItemsetListFilter extends Filter // name [str] if ($_v['na']) - if ($_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value])) + if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]])) $parts[] = $_; // quality [enum] diff --git a/includes/dbtypes/mail.class.php b/includes/dbtypes/mail.class.php index 9ef67241..0e142428 100644 --- a/includes/dbtypes/mail.class.php +++ b/includes/dbtypes/mail.class.php @@ -10,9 +10,9 @@ class MailList extends DBTypeList { public static int $type = Type::MAIL; public static string $brickFile = 'mail'; - public static string $dataTable = '?_mails'; + public static string $dataTable = '::mails'; - protected string $queryBase = 'SELECT m.*, m.`id` AS ARRAY_KEY FROM ?_mails m'; + protected string $queryBase = 'SELECT m.*, m.`id` AS ARRAY_KEY FROM ::mails m'; protected array $queryOpts = []; public function __construct(array $conditions = [], array $miscData = []) @@ -36,7 +36,7 @@ class MailList extends DBTypeList public static function getName(int $id) : ?LocString { - if ($n = DB::Aowow()->SelectRow('SELECT `subject_loc0`, `subject_loc2`, `subject_loc3`, `subject_loc4`, `subject_loc6`, `subject_loc8` FROM ?# WHERE `id` = ?d', self::$dataTable, $id)) + if ($n = DB::Aowow()->SelectRow('SELECT `subject_loc0`, `subject_loc2`, `subject_loc3`, `subject_loc4`, `subject_loc6`, `subject_loc8` FROM %n WHERE `id` = %i', self::$dataTable, $id)) return new LocString($n, 'subject'); return null; } diff --git a/includes/dbtypes/pet.class.php b/includes/dbtypes/pet.class.php index b0c23dea..86834fa5 100644 --- a/includes/dbtypes/pet.class.php +++ b/includes/dbtypes/pet.class.php @@ -12,12 +12,12 @@ class PetList extends DBTypeList public static int $type = Type::PET; public static string $brickFile = 'pet'; - public static string $dataTable = '?_pet'; + public static string $dataTable = '::pet'; - protected string $queryBase = 'SELECT p.*, p.`id` AS ARRAY_KEY FROM ?_pet p'; + protected string $queryBase = 'SELECT p.*, p.`id` AS ARRAY_KEY FROM ::pet p'; protected array $queryOpts = array( 'p' => [['ic']], - 'ic' => ['j' => ['?_icons ic ON p.`iconId` = ic.`id`', true], 's' => ', ic.`name` AS "iconString"'], + 'ic' => ['j' => ['::icons ic ON p.`iconId` = ic.`id`', true], 's' => ', ic.`name` AS "iconString"'], ); public function getListviewData() : array diff --git a/includes/dbtypes/profile.class.php b/includes/dbtypes/profile.class.php index 8aa712b1..541e4b1a 100644 --- a/includes/dbtypes/profile.class.php +++ b/includes/dbtypes/profile.class.php @@ -83,7 +83,7 @@ class ProfileList extends DBTypeList if ($this->getField('cuFlags') & PROFILER_CU_PINNED) $data[$this->id]['pinned'] = 1; - if ($this->getField('cuFlags') & PROFILER_CU_DELETED) + if ($this->getField('deleted')) $data[$this->id]['deleted'] = 1; } @@ -168,7 +168,7 @@ class ProfileList extends DBTypeList public function isCustom() : bool { - return $this->getField('cuFlags') & PROFILER_CU_PROFILE; + return $this->getField('custom'); } public function isVisibleToUser() : bool @@ -176,7 +176,7 @@ class ProfileList extends DBTypeList if (!$this->isCustom() || User::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) return true; - if ($this->getField('cuFlags') & PROFILER_CU_DELETED) + if ($this->getField('deleted')) return false; if (User::$id == $this->getField('user')) @@ -244,9 +244,9 @@ class ProfileListFilter extends Filter 'cr' => [parent::V_RANGE, [1, 36], true ], // criteria ids 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 5000]], true ], // criteria operators 'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter + 'ex' => [parent::V_EQUAL, 'on', false], // only match exact - must be defined before 'na' as it's test relies on 'ex's value + 'na' => [parent::V_NAME, true, false], // name - only printable chars, no delimiter 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter - 'ex' => [parent::V_EQUAL, 'on', false], // only match exact 'si' => [parent::V_LIST, [SIDE_ALLIANCE, SIDE_HORDE], false], // side 'ra' => [parent::V_LIST, [[1, 8], 10, 11], true ], // race 'cl' => [parent::V_LIST, [[1, 9], 11], true ], // class @@ -269,8 +269,8 @@ class ProfileListFilter extends Filter { parent::__construct($data, $opts); - if (!empty($this->criteria['cr'])) - if (array_intersect($this->criteria['cr'], [2, 5, 6, 7, 21])) + if (!empty($this->values['cr'])) + if (array_intersect($this->values['cr'], [2, 5, 6, 7, 21])) $this->useLocalList = true; } @@ -284,13 +284,20 @@ class ProfileListFilter extends Filter // table key differs between remote and local :< $k = $this->useLocalList ? 'p' : 'c'; - // name [str] - the table is case sensitive. Since i don't want to destroy indizes, lets alter the search terms + // name [str] if ($_v['na']) { - $lower = $this->tokenizeString([$k.'.name'], Util::lower($_v['na']), $_v['ex'] == 'on', true); - $proper = $this->tokenizeString([$k.'.name'], Util::ucWords($_v['na']), $_v['ex'] == 'on', true); + // issue: the table is case sensitive. so we need to alter the tokens for multiple cases + foreach (['inTokens', 'exTokens'] as $prop) + { + if (empty($this->{$prop}['na'])) + continue; - $parts[] = ['OR', $lower, $proper]; + $this->{$prop}['na'] = array_map(Util::lower(...), $this->{$prop}['na']); + $this->{$prop}['_na'] = array_map(Util::ucWords(...), $this->{$prop}['na']); + }; + + $parts[] = $this->buildLikeLookup([['na', $k.'.name'], ['_na', $k.'.name']], $_v['ex'] == 'on'); } // side [list] @@ -331,7 +338,7 @@ class ProfileListFilter extends Filter if ($this->useLocalList) { $this->extraOpts[$k] = array( - 'j' => [sprintf('?_profiler_completion_skills %1$s ON `%1$s`.`id` = p.`id` AND `%1$s`.`skillId` = %2$d AND `%1$s`.`value` %3$s %4$d', $k, $skillId, $crs, $crv), true], + 'j' => [sprintf('::profiler_completion_skills %1$s ON `%1$s`.`id` = p.`id` AND `%1$s`.`skillId` = %2$d AND `%1$s`.`value` %3$s %4$d', $k, $skillId, $crs, $crv), true], 's' => [', '.$k.'.`value` AS "'.$col.'"'] ); return [$k.'.skillId', null, '!']; @@ -358,7 +365,7 @@ class ProfileListFilter extends Filter if ($this->useLocalList) { - $this->extraOpts[$k] = ['j' => [sprintf('?_profiler_completion_achievements %1$s ON `%1$s`.`id` = p.`id` AND `%1$s`.`achievementId` = %2$d', $k, $crv), true]]; + $this->extraOpts[$k] = ['j' => [sprintf('::profiler_completion_achievements %1$s ON `%1$s`.`id` = p.`id` AND `%1$s`.`achievementId` = %2$d', $k, $crv), true]]; return [$k.'.achievementId', null, '!']; } else @@ -378,7 +385,7 @@ class ProfileListFilter extends Filter $k = 'i_'.Util::createHash(12); - $this->extraOpts[$k] = ['j' => [sprintf('?_profiler_items %1$s ON `%1$s`.`id` = p.`id` AND `%1$s`.`item` = %2$d', $k, $crv), true]]; + $this->extraOpts[$k] = ['j' => [sprintf('::profiler_items %1$s ON `%1$s`.`id` = p.`id` AND `%1$s`.`item` = %2$d', $k, $crv), true]]; return [$k.'.item', null, '!']; } @@ -406,8 +413,10 @@ class ProfileListFilter extends Filter protected function cbTeamName(int $cr, int $crs, string $crv, $size) : ?array { - if ($_ = $this->tokenizeString(['at.name'], $crv)) - return ['AND', ['at.type', $size], $_]; + $n = preg_replace(parent::PATTERN_NAME, '', $crv); + if ($this->tokenizeString($cr, $n)) + if ($_ = $this->buildLikeLookup([[$cr, 'at.name']])) + return [DB::AND, ['at.type', $size], $_]; return null; } @@ -417,7 +426,7 @@ class ProfileListFilter extends Filter if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) return null; - return ['AND', ['at.type', $size], ['at.rating', $crv, $crs]]; + return [DB::AND, ['at.type', $size], ['at.rating', $crv, $crs]]; } protected function cbAchievs(int $cr, int $crs, string $crv) : ?array @@ -515,10 +524,10 @@ class RemoteProfileList extends ProfileList if ($curTpl['at_login'] & 0x1) { if (!isset($this->rnItr[$curTpl['name']])) - $this->rnItr[$curTpl['name']] = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` IS NOT NULL AND `name` = ?', $r, $curTpl['name']) ?: 0; + $this->rnItr[$curTpl['name']] = DB::Aowow()->selectCell('SELECT MAX(`renameItr`) FROM ::profiler_profiles WHERE `realm` = %i AND `custom` = 0 AND `name` = %s', $r, $curTpl['name']) ?: 0; // already saved as "pending rename" - if ($rnItr = DB::Aowow()->selectCell('SELECT `renameItr` FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` = ?d', $r, $g)) + if ($rnItr = DB::Aowow()->selectCell('SELECT `renameItr` FROM ::profiler_profiles WHERE `realm` = %i AND `realmGUID` = %i', $r, $g)) $curTpl['renameItr'] = $rnItr; // not yet recognized: get max itr else @@ -531,15 +540,16 @@ class RemoteProfileList extends ProfileList } foreach ($talentLookup as $realm => $chars) - $talentLookup[$realm] = DB::Characters($realm)->selectCol('SELECT `guid` AS ARRAY_KEY, `spell` AS ARRAY_KEY2, `talentGroup` FROM character_talent ct WHERE `guid` IN (?a)', array_keys($chars)); + $talentLookup[$realm] = DB::Characters($realm)->selectCol('SELECT `guid` AS ARRAY_KEY, `spell` AS ARRAY_KEY2, `talentGroup` FROM character_talent ct WHERE `guid` IN %in', array_keys($chars)); - $talentSpells = DB::Aowow()->select('SELECT `spell` AS ARRAY_KEY, `tab`, `rank` FROM ?_talents WHERE `class` IN (?a)', array_unique($talentSpells)); + $talentSpells = DB::Aowow()->selectAssoc('SELECT `spell` AS ARRAY_KEY, `tab`, `rank` FROM ::talents WHERE `class` IN %in', array_unique($talentSpells)); + // equalize subject distribution across realms + $limit = 0; foreach ($conditions as $c) - if (is_int($c)) - $limit = $c; + if (is_numeric($c)) + $limit = max(0, (int)$c); - $limit ??= Cfg::get('SQL_LIMIT_DEFAULT'); if (!$limit) // int:0 means unlimited, so skip process $distrib = []; @@ -589,64 +599,75 @@ class RemoteProfileList extends ProfileList public function initializeLocalEntries() : void { + if (!$this->templates) + return; + $baseData = $guildData = []; foreach ($this->iterate() as $guid => $__) { $realmId = $this->getField('realm'); $guildGUID = $this->getField('guild'); - $baseData[$guid] = array( - 'realm' => $realmId, - 'realmGUID' => $this->getField('guid'), - 'name' => $this->getField('name'), - 'renameItr' => $this->getField('renameItr'), - 'race' => $this->getField('race'), - 'class' => $this->getField('class'), - 'level' => $this->getField('level'), - 'gender' => $this->getField('gender'), - 'guild' => $guildGUID ?: null, - 'guildrank' => $guildGUID ? $this->getField('guildrank') : null, - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC - ); + $baseData['realm'][$guid] = $realmId; + $baseData['realmGUID'][$guid] = $this->getField('guid'); + $baseData['name'][$guid] = $this->getField('name'); + $baseData['renameItr'][$guid] = $this->getField('renameItr'); + $baseData['race'][$guid] = $this->getField('race'); + $baseData['class'][$guid] = $this->getField('class'); + $baseData['level'][$guid] = $this->getField('level'); + $baseData['gender'][$guid] = $this->getField('gender'); + $baseData['guild'][$guid] = $guildGUID ?: null; + $baseData['guildrank'][$guid] = $guildGUID ? $this->getField('guildrank') : null; + $baseData['stub'][$guid] = 1; - if ($guildGUID && empty($guildData[$realmId.'-'.$guildGUID])) - $guildData[$realmId.'-'.$guildGUID] = array( - 'realm' => $realmId, - 'realmGUID' => $guildGUID, - 'name' => $this->getField('guildname'), - 'nameUrl' => Profiler::urlize($this->getField('guildname')), - 'cuFlags' => PROFILER_CU_NEEDS_RESYNC - ); + if ($guildGUID) + { + $guildData['realm'][$realmId.'-'.$guildGUID] = $realmId; + $guildData['realmGUID'][$realmId.'-'.$guildGUID] = $guildGUID; + $guildData['name'][$realmId.'-'.$guildGUID] = $this->getField('guildname'); + $guildData['nameUrl'][$realmId.'-'.$guildGUID] = Profiler::urlize($this->getField('guildname')); + $guildData['stub'][$realmId.'-'.$guildGUID] = 1; + } } // basic guild data (satisfying table constraints) if ($guildData) { - foreach (Util::createSqlBatchInsert($guildData) as $ins) - DB::Aowow()->query('INSERT INTO ?_profiler_guild (?#) VALUES '.$ins.' ON DUPLICATE KEY UPDATE `id` = `id`', array_keys(reset($guildData))); + DB::Aowow()->qry('INSERT INTO ::profiler_guild %m ON DUPLICATE KEY UPDATE `id` = `id`', $guildData); // merge back local ids - $localGuilds = DB::Aowow()->selectCol('SELECT `realm` AS ARRAY_KEY, `realmGUID` AS ARRAY_KEY2, `id` FROM ?_profiler_guild WHERE `realm` IN (?a) AND `realmGUID` IN (?a)', - array_column($guildData, 'realm'), array_column($guildData, 'realmGUID') + $localGuilds = DB::Aowow()->selectCol('SELECT `realm` AS ARRAY_KEY, `realmGUID` AS ARRAY_KEY2, `id` FROM ::profiler_guild WHERE `realm` IN %in AND `realmGUID` IN %in', + $guildData['realm'], $guildData['realmGUID'] ); - foreach ($baseData as &$bd) - if ($bd['guild']) - $bd['guild'] = $localGuilds[$bd['realm']][$bd['guild']]; + foreach ($baseData['guild'] as $i => &$g) + $g = $localGuilds[$baseData['realm'][$i]][$baseData['guild'][$i]] ?? null; } // basic char data (enough for tooltips) if ($baseData) { - foreach ($baseData as $ins) - DB::Aowow()->query('INSERT INTO ?_profiler_profiles (?#) VALUES (?a) ON DUPLICATE KEY UPDATE `name` = ?, `renameItr` = ?d', array_keys($ins), array_values($ins), $ins['name'], $ins['renameItr']); + // this could have been an INSERT ON DUPLICATE KEY UPDATE if MariaDB and MySQL would behave for once! + $insertOrUpdate = $baseData; + $existing = DB::Aowow()->selectAssoc('SELECT `realm` AS ARRAY_KEY, `realmGUID` AS ARRAY_KEY2, 1 FROM ::profiler_profiles WHERE `realm` IN %in AND `realmGUID` IN %in', $insertOrUpdate['realm'], $insertOrUpdate['realmGUID']); + foreach ($insertOrUpdate['realm'] as $guid => $_) + { + if (!isset($existing[$insertOrUpdate['realm'][$guid]][$insertOrUpdate['realmGUID'][$guid]])) + continue; + + // ... ON DUPLICATE KEY UPDATE + DB::Aowow()->qry('UPDATE ::profiler_profiles SET `name` = %s, `renameItr` = %i WHERE `realm` = %i AND `realmGUID` = %i', $insertOrUpdate['name'][$guid], $insertOrUpdate['renameItr'][$guid], $insertOrUpdate['realm'][$guid], $insertOrUpdate['realmGUID'][$guid]); + foreach($insertOrUpdate as $col => $__) + unset($insertOrUpdate[$col][$guid]); + } + + // INSERT ... + if (current($insertOrUpdate)) + DB::Aowow()->qry('INSERT INTO ::profiler_profiles %m', $insertOrUpdate); // merge back local ids - $localIds = DB::Aowow()->select( - 'SELECT CONCAT(`realm`, ":", `realmGUID`) AS ARRAY_KEY, `id`, `gearscore` FROM ?_profiler_profiles WHERE (`cuFlags` & ?d) = 0 AND `realm` IN (?a) AND `realmGUID` IN (?a)', - PROFILER_CU_PROFILE, - array_column($baseData, 'realm'), - array_column($baseData, 'realmGUID') + $localIds = DB::Aowow()->selectAssoc('SELECT CONCAT(`realm`, ":", `realmGUID`) AS ARRAY_KEY, `id`, `gearscore` FROM ::profiler_profiles WHERE `custom` = 0 AND `realm` IN %in AND `realmGUID` IN %in', + $baseData['realm'], $baseData['realmGUID'] ); foreach ($this->iterate() as $guid => &$_curTpl) @@ -659,13 +680,13 @@ class RemoteProfileList extends ProfileList class LocalProfileList extends ProfileList { - protected string $queryBase = 'SELECT p.*, p.`id` AS ARRAY_KEY FROM ?_profiler_profiles p'; + protected string $queryBase = 'SELECT p.*, p.`id` AS ARRAY_KEY FROM ::profiler_profiles p'; protected array $queryOpts = array( 'p' => [['g'], 'g' => 'p.`id`'], - 'ap' => ['j' => ['?_account_profiles ap ON ap.`profileId` = p.`id`', true], 's' => ', (IFNULL(ap.`ExtraFlags`, 0) | p.`cuFlags`) AS "cuFlags"'], - 'atm' => ['j' => ['?_profiler_arena_team_member atm ON atm.`profileId` = p.`id`', true], 's' => ', atm.`captain`, atm.`personalRating` AS "rating", atm.`seasonGames`, atm.`seasonWins`'], - 'at' => [['atm'], 'j' => ['?_profiler_arena_team at ON at.`id` = atm.`arenaTeamId`', true], 's' => ', at.`type`'], - 'g' => ['j' => ['?_profiler_guild g ON g.`id` = p.`guild`', true], 's' => ', g.`name` AS "guildname"'] + 'ap' => ['j' => ['::account_profiles ap ON ap.`profileId` = p.`id`', true], 's' => ', (IFNULL(ap.`extraFlags`, 0) | p.`cuFlags`) AS "cuFlags"'], + 'atm' => ['j' => ['::profiler_arena_team_member atm ON atm.`profileId` = p.`id`', true], 's' => ', atm.`captain`, atm.`personalRating` AS "rating", atm.`seasonGames`, atm.`seasonWins`'], + 'at' => [['atm'], 'j' => ['::profiler_arena_team at ON at.`id` = atm.`arenaTeamId`', true], 's' => ', at.`type`'], + 'g' => ['j' => ['::profiler_guild g ON g.`id` = p.`guild`', true], 's' => ', g.`name` AS "guildname"'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -682,8 +703,8 @@ class LocalProfileList extends ProfileList if ($conditions && $realmIds) { - array_unshift($conditions, 'AND'); - $conditions = ['AND', ['realm', $realmIds], $conditions]; + array_unshift($conditions, DB::AND); + $conditions = [DB::AND, ['realm', $realmIds], $conditions]; } else if ($realmIds) $conditions = [['realm', $realmIds]]; diff --git a/includes/dbtypes/quest.class.php b/includes/dbtypes/quest.class.php index 5cd13bb7..c24c2573 100644 --- a/includes/dbtypes/quest.class.php +++ b/includes/dbtypes/quest.class.php @@ -10,17 +10,18 @@ class QuestList extends DBTypeList { public static int $type = Type::QUEST; public static string $brickFile = 'quest'; - public static string $dataTable = '?_quests'; + public static string $dataTable = '::quests'; public array $requires = []; public array $rewards = []; public array $choices = []; - protected string $queryBase = 'SELECT q.*, q.`id` AS ARRAY_KEY FROM ?_quests q'; + protected string $queryBase = 'SELECT q.*, q.`id` AS ARRAY_KEY FROM ::quests q'; protected array $queryOpts = array( 'q' => [], - 'rsc' => ['j' => '?_spell rsc ON q.`rewardSpellCast` = rsc.`id`'], // limit rewardSpellCasts - 'qse' => ['j' => '?_quests_startend qse ON q.`id` = qse.`questId`', 's' => ', qse.`method`'], // groupConcat..? - 'e' => ['j' => ['?_events e ON e.`id` = q.`eventId`', true], 's' => ', e.`holidayId`'] + 'nml' => ['j' => '::quests_search nml ON nml.`id` = q.`id` AND nml.`locale` = DB_LOC_I'], + 'rsc' => ['j' => '::spell rsc ON q.`rewardSpellCast` = rsc.`id`'], // limit rewardSpellCasts + 'qse' => ['j' => '::quests_startend qse ON q.`id` = qse.`questId`', 's' => ', qse.`method`'], // groupConcat..? + 'e' => ['j' => ['::events e ON e.`id` = q.`eventId`', true], 's' => ', e.`holidayId`'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -28,12 +29,12 @@ class QuestList extends DBTypeList parent::__construct($conditions, $miscData); // i don't like this very much - $currencies = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `itemId` FROM ?_currencies'); + $currencies = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `itemId` FROM ::currencies'); // post processing foreach ($this->iterate() as $id => &$_curTpl) { - $_curTpl['cat1'] = $_curTpl['zoneOrSort']; // should probably be in a method... + $_curTpl['cat1'] = $_curTpl['questSortId']; // should probably be in a method... $_curTpl['cat2'] = 0; foreach (Game::QUEST_CLASSES as $k => $arr) @@ -112,7 +113,7 @@ class QuestList extends DBTypeList public function isRepeatable() : bool { - return $this->curTpl['flags'] & QUEST_FLAG_REPEATABLE || $this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_REPEATABLE; + return $this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_REPEATABLE; } public function isDaily() : int @@ -129,16 +130,15 @@ class QuestList extends DBTypeList return 0; } - // using reqPlayerKills and rewardHonor as a crutch .. has TC this even implemented..? - public function isPvPEnabled() : bool + public function isAutoAccept() : bool { - return $this->curTpl['reqPlayerKills'] || $this->curTpl['rewardHonorPoints'] || $this->curTpl['rewardArenaPoints']; + return $this->curTpl['flags'] & QUEST_FLAG_AUTO_ACCEPT || $this->curTpl['specialFlags'] & QUEST_FLAG_SPECIAL_AUTO_ACCEPT; } // by TC definition public function isSeasonal() : bool { - return in_array($this->getField('zoneOrSortBak'), [-22, -284, -366, -369, -370, -376, -374]) && !$this->isRepeatable(); + return in_array($this->getField('questSortIdBak'), [-22, -284, -366, -369, -370, -376, -374]) && !$this->isRepeatable(); } public function getSourceData(int $id = 0) : array @@ -172,7 +172,7 @@ class QuestList extends DBTypeList continue; [$series, $first] = DB::Aowow()->SelectRow( - 'SELECT IF(prev.`id` OR cur.`nextQuestIdChain`, 1, 0) AS "0", IF(prev.`id` IS NULL AND cur.`nextQuestIdChain`, 1, 0) AS "1" FROM ?_quests cur LEFT JOIN ?_quests prev ON prev.`nextQuestIdChain` = cur.`id` WHERE cur.`id` = ?d', + 'SELECT IF(prev.`id` OR cur.`nextQuestIdChain`, 1, 0) AS "0", IF(prev.`id` IS NULL AND cur.`nextQuestIdChain`, 1, 0) AS "1" FROM ::quests cur LEFT JOIN ::quests prev ON prev.`nextQuestIdChain` = cur.`id` WHERE cur.`id` = %i', $this->id ); @@ -225,7 +225,7 @@ class QuestList extends DBTypeList if ($_ = $this->curTpl['rewardTitleId']) $data[$this->id]['titlereward'] = $_; - if ($_ = $this->curTpl['type']) + if ($_ = $this->curTpl['questInfoId']) $data[$this->id]['type'] = $_; if ($_ = $this->curTpl['reqClassMask']) @@ -263,14 +263,14 @@ class QuestList extends DBTypeList if ($this->isSeasonal()) $data[$this->id]['wflags'] |= QUEST_CU_SEASONAL; - if ($this->curTpl['flags'] & QUEST_FLAG_AUTO_REWARDED) // not shown in log + if ($this->curTpl['flags'] & QUEST_FLAG_TRACKING) // not shown in log $data[$this->id]['wflags'] |= QUEST_CU_SKIP_LOG; - if ($this->curTpl['flags'] & QUEST_FLAG_AUTO_ACCEPT) // self-explanatory + if ($this->isAutoAccept()) $data[$this->id]['wflags'] |= QUEST_CU_AUTO_ACCEPT; - if ($this->isPvPEnabled()) // not sure why this flag also requires auto-accept to be set - $data[$this->id]['wflags'] |= (QUEST_CU_AUTO_ACCEPT | QUEST_CU_PVP_ENABLED); + if ($this->curTpl['flags'] & QUEST_FLAG_FLAGS_PVP) // this flag is only displayed if auto-accept is also set. not sure why. + $data[$this->id]['wflags'] |= QUEST_CU_PVP_ENABLED; $data[$this->id]['reprewards'] = []; for ($i = 1; $i < 6; $i++) @@ -338,13 +338,15 @@ class QuestList extends DBTypeList $rng = $this->curTpl['reqNpcOrGo'.$i]; $rngQty = $this->curTpl['reqNpcOrGoCount'.$i]; - if ($rngQty < 1 && (!$rng || $ot)) + if (!$ot && ($rngQty < 1 || !$rng)) continue; if ($ot) $name = $ot; - else - $name = $rng > 0 ? CreatureList::getName($rng) : Lang::unescapeUISequences(GameObjectList::getName(-$rng), Lang::FMT_HTML); + else if ($rng > 0) + $name = CreatureList::getName($rng); + else if ($rng < 0) + $name = Lang::unescapeUISequences(GameObjectList::getName(-$rng), Lang::FMT_HTML); if (!$name) $name = Util::ucFirst(Lang::game($rng > 0 ? 'npc' : 'object')).' #'.abs($rng); @@ -415,7 +417,15 @@ class QuestList extends DBTypeList } if ($addMask & GLOBALINFO_SELF) + { $data[Type::QUEST][$this->id] = ['name' => $this->getField('name', true)]; + + if ($this->curTpl['flags'] & QUEST_FLAG_DAILY) + $data[Type::QUEST][$this->id]['daily'] = true; + + if ($this->curTpl['flags'] & QUEST_FLAG_WEEKLY) + $data[Type::QUEST][$this->id]['weekly'] = true; + } } return $data; @@ -437,51 +447,52 @@ class QuestListFilter extends Filter ); protected static array $genericFilter = array( - 1 => [parent::CR_CALLBACK, 'cbReputation', '>', null], // increasesrepwith - 2 => [parent::CR_NUMERIC, 'rewardXP', NUM_CAST_INT ], // experiencegained - 3 => [parent::CR_NUMERIC, 'rewardOrReqMoney', NUM_CAST_INT ], // moneyrewarded - 4 => [parent::CR_CALLBACK, 'cbSpellRewards', null, null], // spellrewarded [yn] - 5 => [parent::CR_FLAG, 'flags', QUEST_FLAG_SHARABLE ], // sharable - 6 => [parent::CR_NUMERIC, 'timeLimit', NUM_CAST_INT ], // timer - 7 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_FIRST_SERIES ], // firstquestseries - 9 => [parent::CR_CALLBACK, 'cbEarnReputation', null, null], // objectiveearnrepwith [enum] - 10 => [parent::CR_CALLBACK, 'cbReputation', '<', null], // decreasesrepwith - 11 => [parent::CR_NUMERIC, 'suggestedPlayers', NUM_CAST_INT ], // suggestedplayers - 15 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_LAST_SERIES ], // lastquestseries - 16 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_PART_OF_SERIES ], // partseries - 18 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots - 19 => [parent::CR_CALLBACK, 'cbQuestRelation', 0x1, null], // startsfrom [enum] - 21 => [parent::CR_CALLBACK, 'cbQuestRelation', 0x2, null], // endsat [enum] - 22 => [parent::CR_CALLBACK, 'cbItemRewards', null, null], // itemrewards [op] [int] - 23 => [parent::CR_CALLBACK, 'cbItemChoices', null, null], // itemchoices [op] [int] - 24 => [parent::CR_CALLBACK, 'cbLacksStartEnd', null, null], // lacksstartend [yn] - 25 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments - 27 => [parent::CR_FLAG, 'flags', QUEST_FLAG_DAILY ], // daily - 28 => [parent::CR_FLAG, 'flags', QUEST_FLAG_WEEKLY ], // weekly - 29 => [parent::CR_CALLBACK, 'cbRepeatable', null ], // repeatable - 30 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id - 33 => [parent::CR_ENUM, 'e.holidayId', true, true], // relatedevent - 34 => [parent::CR_CALLBACK, 'cbAvailable', null, null], // availabletoplayers [yn] - 36 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos - 37 => [parent::CR_CALLBACK, 'cbClassSpec', null, null], // classspecific [enum] - 38 => [parent::CR_CALLBACK, 'cbRaceSpec', null, null], // racespecific [enum] - 42 => [parent::CR_STAFFFLAG, 'flags' ], // flags - 43 => [parent::CR_CALLBACK, 'cbCurrencyReward', null, null], // currencyrewarded [enum] - 44 => [parent::CR_CALLBACK, 'cbLoremaster', null, null], // countsforloremaster_stc [yn] - 45 => [parent::CR_BOOLEAN, 'rewardTitleId' ] // titlerewarded + 1 => [parent::CR_CALLBACK, 'cbReputation', '>', null], // increasesrepwith + 2 => [parent::CR_NUMERIC, 'rewardXP', NUM_CAST_INT ], // experiencegained + 3 => [parent::CR_NUMERIC, 'rewardOrReqMoney', NUM_CAST_INT ], // moneyrewarded + 4 => [parent::CR_CALLBACK, 'cbSpellRewards', null, null], // spellrewarded [yn] + 5 => [parent::CR_FLAG, 'flags', QUEST_FLAG_SHARABLE ], // sharable + 6 => [parent::CR_NUMERIC, 'timeLimit', NUM_CAST_INT ], // timer + 7 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_FIRST_SERIES ], // firstquestseries + 9 => [parent::CR_CALLBACK, 'cbEarnReputation', null, null], // objectiveearnrepwith [enum] + 10 => [parent::CR_CALLBACK, 'cbReputation', '<', null], // decreasesrepwith + 11 => [parent::CR_NUMERIC, 'suggestedPlayers', NUM_CAST_INT ], // suggestedplayers + 15 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_LAST_SERIES ], // lastquestseries + 16 => [parent::CR_FLAG, 'cuFlags', QUEST_CU_PART_OF_SERIES ], // partseries + 18 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots + 19 => [parent::CR_CALLBACK, 'cbQuestRelation', 0x1, null], // startsfrom [enum] + 21 => [parent::CR_CALLBACK, 'cbQuestRelation', 0x2, null], // endsat [enum] + 22 => [parent::CR_CALLBACK, 'cbItemRewards', null, null], // itemrewards [op] [int] + 23 => [parent::CR_CALLBACK, 'cbItemChoices', null, null], // itemchoices [op] [int] + 24 => [parent::CR_CALLBACK, 'cbLacksStartEnd', null, null], // lacksstartend [yn] + 25 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_COMMENT ], // hascomments + 27 => [parent::CR_FLAG, 'flags', QUEST_FLAG_DAILY ], // daily + 28 => [parent::CR_FLAG, 'flags', QUEST_FLAG_WEEKLY ], // weekly + 29 => [parent::CR_FLAG, 'specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE], // repeatable + 30 => [parent::CR_NUMERIC, 'id', NUM_CAST_INT, true], // id + 33 => [parent::CR_ENUM, 'e.holidayId', true, true], // relatedevent + 34 => [parent::CR_CALLBACK, 'cbAvailable', null, null], // availabletoplayers [yn] + 36 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_VIDEO ], // hasvideos + 37 => [parent::CR_CALLBACK, 'cbClassSpec', null, null], // classspecific [enum] + 38 => [parent::CR_CALLBACK, 'cbRaceSpec', null, null], // racespecific [enum] + 42 => [parent::CR_STAFFFLAG, 'flags' ], // flags + 43 => [parent::CR_CALLBACK, 'cbCurrencyReward', null, null], // currencyrewarded [enum] + 44 => [parent::CR_CALLBACK, 'cbLoremaster', null, null], // countsforloremaster_stc [yn] + 45 => [parent::CR_BOOLEAN, 'rewardTitleId' ], // titlerewarded + 47 => [parent::CR_FLAG, 'flags', QUEST_FLAG_FLAGS_PVP ] // setspvpflag ); protected static array $inputFields = array( - 'cr' => [parent::V_RANGE, [1, 45], true ], // criteria ids + 'cr' => [parent::V_RANGE, [1, 47], true ], // criteria ids 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators 'crv' => [parent::V_REGEX, parent::PATTERN_INT, true ], // criteria values - only numerals - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / text - only printable chars, no delimiter + 'na' => [parent::V_NAME, false, false], // name / text - only printable chars, no delimiter 'ex' => [parent::V_EQUAL, 'on', false], // also match subname 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter - 'minle' => [parent::V_RANGE, [1, 99], false], // min quest level - 'maxle' => [parent::V_RANGE, [1, 99], false], // max quest level - 'minrl' => [parent::V_RANGE, [1, 99], false], // min required level - 'maxrl' => [parent::V_RANGE, [1, 99], false], // max required level + 'minle' => [parent::V_RANGE, [0, 99], false], // min quest level + 'maxle' => [parent::V_RANGE, [0, 99], false], // max quest level + 'minrl' => [parent::V_RANGE, [0, 99], false], // min required level + 'maxrl' => [parent::V_RANGE, [0, 99], false], // max required level 'si' => [parent::V_LIST, [-SIDE_HORDE, -SIDE_ALLIANCE, SIDE_ALLIANCE, SIDE_HORDE, SIDE_BOTH], false], // side 'ty' => [parent::V_LIST, [0, 1, 21, 41, 62, [81, 85], 88, 89], true ] // type ); @@ -496,14 +507,21 @@ class QuestListFilter extends Filter // name if ($_v['na']) { - $_ = []; - if ($_v['ex'] == 'on') - $_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value, 'objectives_loc'.Lang::getLocale()->value, 'details_loc'.Lang::getLocale()->value]); - else - $_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]); + $f = [['na', ['nml.nName', 'nml.nObjectives', 'nml.nDetails']]]; + if ($_v['ex'] != 'on') + $f = [['na', 'nml.nName']]; - if ($_) + if ($_ = $this->buildMatchLookup($f)) $parts[] = $_; + else + { + $f = [['na', 'name_loc'.Lang::getLocale()->value], ['na', 'objectives_loc'.Lang::getLocale()->value], ['na', 'details_loc'.Lang::getLocale()->value]]; + if ($_v['ex'] != 'on') + $f = [$f[0]]; + + if ($_ = $this->buildLikeLookup($f)) + $parts[] = $_; + } } // level min @@ -526,21 +544,21 @@ class QuestListFilter extends Filter if ($_v['si']) { $excl = [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!']; - $incl = ['OR', ['reqRaceMask', 0], [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL]]; + $incl = [DB::OR, ['reqRaceMask', 0], [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL]]; $parts[] = match ($_v['si']) { SIDE_BOTH => $incl, - SIDE_HORDE => ['OR', $incl, ['reqRaceMask', ChrRace::MASK_HORDE, '&']], - -SIDE_HORDE => ['AND', $excl, ['reqRaceMask', ChrRace::MASK_HORDE, '&']], - SIDE_ALLIANCE => ['OR', $incl, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']], - -SIDE_ALLIANCE => ['AND', $excl, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']] + SIDE_HORDE => [DB::OR, $incl, ['reqRaceMask', ChrRace::MASK_HORDE, '&']], + -SIDE_HORDE => [DB::AND, $excl, ['reqRaceMask', ChrRace::MASK_HORDE, '&']], + SIDE_ALLIANCE => [DB::OR, $incl, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']], + -SIDE_ALLIANCE => [DB::AND, $excl, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']] }; } - // type [list] - if ($_v['ty'] !== null) - $parts[] = ['type', $_v['ty']]; + // questInfoId [list] + if ($_v['ty']) + $parts[] = ['questInfoId', $_v['ty']]; return $parts; } @@ -553,16 +571,16 @@ class QuestListFilter extends Filter if (!in_array($crs, self::$enums[$cr])) return null; - if ($_ = DB::Aowow()->selectRow('SELECT * FROM ?_factions WHERE `id` = ?d', $crs)) + if ($_ = DB::Aowow()->selectRow('SELECT * FROM ::factions WHERE `id` = %i', $crs)) $this->fiReputationCols[] = [$crs, Util::localizedString($_, 'name')]; return [ - 'OR', - ['AND', ['rewardFactionId1', $crs], ['rewardFactionValue1', 0, $sign]], - ['AND', ['rewardFactionId2', $crs], ['rewardFactionValue2', 0, $sign]], - ['AND', ['rewardFactionId3', $crs], ['rewardFactionValue3', 0, $sign]], - ['AND', ['rewardFactionId4', $crs], ['rewardFactionValue4', 0, $sign]], - ['AND', ['rewardFactionId5', $crs], ['rewardFactionValue5', 0, $sign]] + DB::OR, + [DB::AND, ['rewardFactionId1', $crs], ['rewardFactionValue1', 0, $sign]], + [DB::AND, ['rewardFactionId2', $crs], ['rewardFactionValue2', 0, $sign]], + [DB::AND, ['rewardFactionId3', $crs], ['rewardFactionValue3', 0, $sign]], + [DB::AND, ['rewardFactionId4', $crs], ['rewardFactionValue4', 0, $sign]], + [DB::AND, ['rewardFactionId5', $crs], ['rewardFactionValue5', 0, $sign]] ]; } @@ -572,7 +590,7 @@ class QuestListFilter extends Filter { Type::NPC, Type::OBJECT, - Type::ITEM => ['AND', ['qse.type', $crs], ['qse.method', $flags, '&']], + Type::ITEM => [DB::AND, ['qse.type', $crs], ['qse.method', $flags, '&']], default => null }; } @@ -586,7 +604,7 @@ class QuestListFilter extends Filter return null; return [ - 'OR', + DB::OR, ['rewardItemId1', $crs], ['rewardItemId2', $crs], ['rewardItemId3', $crs], ['rewardItemId4', $crs], ['rewardChoiceItemId1', $crs], ['rewardChoiceItemId2', $crs], ['rewardChoiceItemId3', $crs], ['rewardChoiceItemId4', $crs], ['rewardChoiceItemId5', $crs], ['rewardChoiceItemId6', $crs] ]; @@ -603,17 +621,6 @@ class QuestListFilter extends Filter return ['cuFlags', CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&']; } - protected function cbRepeatable(int $cr, int $crs, string $crv) : ?array - { - if (!$this->int2Bool($crs)) - return null; - - if ($crs) - return ['OR', ['flags', QUEST_FLAG_REPEATABLE, '&'], ['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE, '&']]; - else - return ['AND', [['flags', QUEST_FLAG_REPEATABLE, '&'], 0], [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE, '&'], 0]]; - } - protected function cbItemChoices(int $cr, int $crs, string $crv) : ?array { if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) @@ -640,9 +647,9 @@ class QuestListFilter extends Filter return null; if ($crs) - return ['AND', ['zoneOrSort', 0, '>'], [['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY | QUEST_FLAG_REPEATABLE, '&'], 0], [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY, '&'], 0]]; + return [DB::AND, ['questSortId', 0, '>'], [['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY, '&'], 0], [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY, '&'], 0]]; else - return ['OR', ['zoneOrSort', 0, '<'], ['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY | QUEST_FLAG_REPEATABLE, '&'], ['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY, '&']]; + return [DB::OR, ['questSortId', 0, '<'], ['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY, '&'], ['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_MONTHLY, '&']]; } protected function cbSpellRewards(int $cr, int $crs, string $crv) : ?array @@ -651,9 +658,9 @@ class QuestListFilter extends Filter return null; if ($crs) - return ['OR', ['sourceSpellId', 0, '>'], ['rewardSpell', 0, '>'], ['rsc.effect1Id', SpellList::EFFECTS_TEACH], ['rsc.effect2Id', SpellList::EFFECTS_TEACH], ['rsc.effect3Id', SpellList::EFFECTS_TEACH]]; + return [DB::OR, ['sourceSpellId', 0, '>'], ['rewardSpell', 0, '>'], ['rsc.effect1Id', SpellList::EFFECTS_TEACH], ['rsc.effect2Id', SpellList::EFFECTS_TEACH], ['rsc.effect3Id', SpellList::EFFECTS_TEACH]]; else - return ['AND', ['sourceSpellId', 0], ['rewardSpell', 0], ['rewardSpellCast', 0]]; + return [DB::AND, ['sourceSpellId', 0], ['rewardSpell', 0], ['rewardSpellCast', 0]]; } protected function cbEarnReputation(int $cr, int $crs, string $crv) : ?array @@ -662,11 +669,11 @@ class QuestListFilter extends Filter return null; if ($crs == parent::ENUM_ANY) - return ['OR', ['reqFactionId1', 0, '>'], ['reqFactionId2', 0, '>']]; + return [DB::OR, ['reqFactionId1', 0, '>'], ['reqFactionId2', 0, '>']]; else if ($crs == parent::ENUM_NONE) - return ['AND', ['reqFactionId1', 0], ['reqFactionId2', 0]]; + return [DB::AND, ['reqFactionId1', 0], ['reqFactionId2', 0]]; else if (in_array($crs, self::$enums[$cr])) - return ['OR', ['reqFactionId1', $crs], ['reqFactionId2', $crs]]; + return [DB::OR, ['reqFactionId1', $crs], ['reqFactionId2', $crs]]; return null; } @@ -678,11 +685,11 @@ class QuestListFilter extends Filter $_ = self::$enums[$cr][$crs]; if ($_ === true) - return ['AND', ['reqClassMask', 0, '!'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']]; + return [DB::AND, ['reqClassMask', 0, '!'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']]; else if ($_ === false) - return ['OR', ['reqClassMask', 0], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL]]; + return [DB::OR, ['reqClassMask', 0], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL]]; else if (is_int($_)) - return ['AND', ['reqClassMask', ChrClass::from($_)->toMask(), '&'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']]; + return [DB::AND, ['reqClassMask', ChrClass::from($_)->toMask(), '&'], [['reqClassMask', ChrClass::MASK_ALL, '&'], ChrClass::MASK_ALL, '!']]; return null; } @@ -694,11 +701,11 @@ class QuestListFilter extends Filter $_ = self::$enums[$cr][$crs]; if ($_ === true) - return ['AND', ['reqRaceMask', 0, '!'], [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!'], [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ChrRace::MASK_ALLIANCE, '!'], [['reqRaceMask', ChrRace::MASK_HORDE, '&'], ChrRace::MASK_HORDE, '!']]; + return [DB::AND, ['reqRaceMask', 0, '!'], [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!'], [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ChrRace::MASK_ALLIANCE, '!'], [['reqRaceMask', ChrRace::MASK_HORDE, '&'], ChrRace::MASK_HORDE, '!']]; else if ($_ === false) - return ['OR', ['reqRaceMask', 0], ['reqRaceMask', ChrRace::MASK_ALL], ['reqRaceMask', ChrRace::MASK_ALLIANCE], ['reqRaceMask', ChrRace::MASK_HORDE]]; + return [DB::OR, ['reqRaceMask', 0], ['reqRaceMask', ChrRace::MASK_ALL], ['reqRaceMask', ChrRace::MASK_ALLIANCE], ['reqRaceMask', ChrRace::MASK_HORDE]]; else if (is_int($_)) - return ['AND', ['reqRaceMask', ChrRace::from($_)->toMask(), '&'], [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ChrRace::MASK_ALLIANCE, '!'], [['reqRaceMask', ChrRace::MASK_HORDE, '&'], ChrRace::MASK_HORDE, '!']]; + return [DB::AND, ['reqRaceMask', ChrRace::from($_)->toMask(), '&'], [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ChrRace::MASK_ALLIANCE, '!'], [['reqRaceMask', ChrRace::MASK_HORDE, '&'], ChrRace::MASK_HORDE, '!']]; return null; } @@ -708,7 +715,7 @@ class QuestListFilter extends Filter if (!$this->int2Bool($crs)) return null; - $missing = DB::Aowow()->selectCol('SELECT `questId`, BIT_OR(`method`) AS "se" FROM ?_quests_startend GROUP BY `questId` HAVING "se" <> 3'); + $missing = DB::Aowow()->selectCol('SELECT `questId`, BIT_OR(`method`) AS "se" FROM ::quests_startend GROUP BY `questId` HAVING "se" <> 3'); if ($crs) return ['id', $missing]; else diff --git a/includes/dbtypes/skill.class.php b/includes/dbtypes/skill.class.php index ab361031..9aafbcb1 100644 --- a/includes/dbtypes/skill.class.php +++ b/includes/dbtypes/skill.class.php @@ -10,12 +10,12 @@ class SkillList extends DBTypeList { public static int $type = Type::SKILL; public static string $brickFile = 'skill'; - public static string $dataTable = '?_skillline'; + public static string $dataTable = '::skillline'; - protected string $queryBase = 'SELECT sl.*, sl.`id` AS ARRAY_KEY FROM ?_skillline sl'; + protected string $queryBase = 'SELECT sl.*, sl.`id` AS ARRAY_KEY FROM ::skillline sl'; protected array $queryOpts = array( 'sl' => [['ic']], - 'ic' => ['j' => ['?_icons ic ON ic.`id` = sl.`iconId`', true], 's' => ', ic.`name` AS "iconString"'], + 'ic' => ['j' => ['::icons ic ON ic.`id` = sl.`iconId`', true], 's' => ', ic.`name` AS "iconString"'], ); public function __construct(array $conditions = [], array $miscData = []) @@ -29,11 +29,7 @@ class SkillList extends DBTypeList if (!$_) $_ = [0, 0, 0, 0, 0]; else - { - $_ = explode(' ', $_); - while (count($_) < 5) - $_[] = 0; - } + $_ = array_pad(explode(' ', $_), 5, 0); if (!$_curTpl['iconId']) $_curTpl['iconString'] = DEFAULT_ICON; diff --git a/includes/dbtypes/sound.class.php b/includes/dbtypes/sound.class.php index 577611f8..f43a8fb3 100644 --- a/includes/dbtypes/sound.class.php +++ b/includes/dbtypes/sound.class.php @@ -12,10 +12,10 @@ class SoundList extends DBTypeList public static int $type = Type::SOUND; public static string $brickFile = 'sound'; - public static string $dataTable = '?_sounds'; + public static string $dataTable = '::sounds'; public static int $contribute = CONTRIBUTE_CO; - protected string $queryBase = 'SELECT s.*, s.`id` AS ARRAY_KEY FROM ?_sounds s'; + protected string $queryBase = 'SELECT s.*, s.`id` AS ARRAY_KEY FROM ::sounds s'; private array $fileBuffer = []; private static array $fileTypes = [SOUND_TYPE_OGG => MIME_TYPE_OGG, SOUND_TYPE_MP3 => MIME_TYPE_MP3]; @@ -42,7 +42,7 @@ class SoundList extends DBTypeList if ($this->fileBuffer) { - $files = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `id`, `file` AS "title", CAST(`type` AS UNSIGNED) AS "type", `path` FROM ?_sounds_files sf WHERE `id` IN (?a)', array_keys($this->fileBuffer)); + $files = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `id`, `file` AS "title", CAST(`type` AS UNSIGNED) AS "type", `path` FROM ::sounds_files sf WHERE `id` IN %in', array_keys($this->fileBuffer)); foreach ($files as $id => $data) { // 3.3.5 bandaid - need fullpath to play via wow API, remove for cata and later @@ -61,7 +61,7 @@ class SoundList extends DBTypeList public static function getName(int $id) : ?LocString { - if ($n = DB::Aowow()->SelectRow('SELECT `name` AS "name_loc0" FROM ?# WHERE `id` = ?d', self::$dataTable, $id)) + if ($n = DB::Aowow()->SelectRow('SELECT `name` AS "name_loc0" FROM %n WHERE `id` = %i', self::$dataTable, $id)) return new LocString($n); return null; } @@ -104,7 +104,7 @@ class SoundListFilter extends Filter { protected string $type = 'sounds'; protected static array $inputFields = array( - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name - only printable chars, no delimiter + 'na' => [parent::V_NAME, false, false], // name - only printable chars, no delimiter 'ty' => [parent::V_LIST, [[1, 4], 6, 9, 10, 12, 13, 14, 16, 17, [19, 31], 50, 52, 53], true ] // type ); @@ -115,7 +115,7 @@ class SoundListFilter extends Filter // name [str] if ($_v['na']) - if ($_ = $this->tokenizeString(['name'])) + if ($_ = $this->buildLikeLookup([['na', 'name']])) $parts[] = $_; // type [list] diff --git a/includes/dbtypes/spell.class.php b/includes/dbtypes/spell.class.php index 344fa8d2..6aefde19 100644 --- a/includes/dbtypes/spell.class.php +++ b/includes/dbtypes/spell.class.php @@ -10,9 +10,13 @@ class SpellList extends DBTypeList { use listviewHelper, sourceHelper; + public const /* int */ INTERACTIVE_NONE = 0; + public const /* int */ INTERACTIVE_EMBEDDED = 1; // parse combat ratings to % + public const /* int */ INTERACTIVE_FULL = 2; // additionaly allow links and hover tooltips + public static int $type = Type::SPELL; public static string $brickFile = 'spell'; - public static string $dataTable = '?_spell'; + public static string $dataTable = '::spell'; public array $ranks = []; public ?ItemList $relItems = null; public static array $skillLines = array( @@ -23,12 +27,16 @@ class SpellList extends DBTypeList 11 => SKILLS_TRADE_PRIMARY // prim. Professions ); - public const EFFECTS_HEAL = array( - SPELL_EFFECT_NONE, /*SPELL_EFFECT_DUMMY*/ SPELL_EFFECT_HEAL, SPELL_EFFECT_HEAL_MAX_HEALTH, SPELL_EFFECT_HEAL_MECHANICAL, - SPELL_EFFECT_HEAL_PCT + public const EFFECTS_SCALING_HEAL = array( // as per Unit::SpellHealingBonusDone() calls in TC + SPELL_EFFECT_HEAL, SPELL_EFFECT_HEAL_PCT, SPELL_EFFECT_HEAL_MECHANICAL, SPELL_EFFECT_HEALTH_LEECH ); - public const EFFECTS_DAMAGE = array( - SPELL_EFFECT_NONE, SPELL_EFFECT_DUMMY, SPELL_EFFECT_SCHOOL_DAMAGE, SPELL_EFFECT_HEALTH_LEECH, SPELL_EFFECT_POWER_BURN + public const EFFECTS_SCALING_DAMAGE = array( // as per Unit::SpellDamageBonusDone() calls in TC + SPELL_EFFECT_SCHOOL_DAMAGE, SPELL_EFFECT_HEALTH_LEECH, SPELL_EFFECT_POWER_BURN + ); + public const EFFECTS_LDC_SCALING = array( + SPELL_EFFECT_SCHOOL_DAMAGE, SPELL_EFFECT_DUMMY, SPELL_EFFECT_POWER_DRAIN, SPELL_EFFECT_HEALTH_LEECH, SPELL_EFFECT_HEAL, + SPELL_EFFECT_WEAPON_DAMAGE, SPELL_EFFECT_POWER_BURN, SPELL_EFFECT_SCRIPT_EFFECT, SPELL_EFFECT_NORMALIZED_WEAPON_DMG, SPELL_EFFECT_FORCE_CAST_WITH_VALUE, + SPELL_EFFECT_TRIGGER_SPELL_WITH_VALUE, SPELL_EFFECT_TRIGGER_MISSILE_SPELL_WITH_VALUE ); public const EFFECTS_ITEM_CREATE = array( SPELL_EFFECT_CREATE_ITEM, SPELL_EFFECT_SUMMON_CHANGE_ITEM, SPELL_EFFECT_CREATE_RANDOM_ITEM, SPELL_EFFECT_CREATE_MANA_GEM, SPELL_EFFECT_CREATE_ITEM_2 @@ -48,21 +56,23 @@ class SpellList extends DBTypeList public const EFFECTS_MODEL_NPC = array( SPELL_EFFECT_SUMMON, SPELL_EFFECT_SUMMON_PET, SPELL_EFFECT_SUMMON_DEMON, SPELL_EFFECT_KILL_CREDIT, SPELL_EFFECT_KILL_CREDIT2 ); - public const EFFECTS_DIRECT_SCALING = array( + public const EFFECTS_DIRECT_SCALING = array( // as per Unit::GetCastingTimeForBonus() SPELL_EFFECT_SCHOOL_DAMAGE, SPELL_EFFECT_ENVIRONMENTAL_DAMAGE, SPELL_EFFECT_POWER_DRAIN, SPELL_EFFECT_HEALTH_LEECH, SPELL_EFFECT_POWER_BURN, - SPELL_EFFECT_HEAL_MAX_HEALTH + SPELL_EFFECT_HEAL ); public const EFFECTS_ENCHANTMENT = array( SPELL_EFFECT_ENCHANT_ITEM, SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY, SPELL_EFFECT_ENCHANT_HELD_ITEM, SPELL_EFFECT_ENCHANT_ITEM_PRISMATIC ); - public const AURAS_HEAL = array( - SPELL_AURA_DUMMY, SPELL_AURA_PERIODIC_HEAL, SPELL_AURA_PERIODIC_HEALTH_FUNNEL, SPELL_AURA_SCHOOL_ABSORB, SPELL_AURA_MANA_SHIELD, - SPELL_AURA_PERIODIC_DUMMY + public const AURAS_SCALING_HEAL = array( // as per Unit::SpellHealingBonusDone() calls in TC (SPELL_AURA_SCHOOL_ABSORB + SPELL_AURA_MANA_SHIELD priest/mage cases are scripted) + SPELL_AURA_PERIODIC_HEAL, SPELL_AURA_PERIODIC_LEECH, SPELL_AURA_OBS_MOD_HEALTH ); - public const AURAS_DAMAGE = array( - SPELL_AURA_PERIODIC_DAMAGE, SPELL_AURA_DUMMY, SPELL_AURA_DAMAGE_SHIELD, SPELL_AURA_PERIODIC_LEECH, SPELL_AURA_PERIODIC_DAMAGE_PERCENT, - SPELL_AURA_POWER_BURN, SPELL_AURA_PERIODIC_DUMMY + public const AURAS_SCALING_DAMAGE = array( // as per Unit::SpellDamageBonusDone() calls in TC + SPELL_AURA_PERIODIC_DAMAGE, SPELL_AURA_PERIODIC_LEECH, SPELL_AURA_DAMAGE_SHIELD, SPELL_AURA_PROC_TRIGGER_DAMAGE + ); + public const AURAS_LDC_SCALING = array( + SPELL_AURA_PERIODIC_DAMAGE, SPELL_AURA_DUMMY, SPELL_AURA_PERIODIC_HEAL, SPELL_AURA_DAMAGE_SHIELD, SPELL_AURA_PROC_TRIGGER_DAMAGE, + SPELL_AURA_PERIODIC_LEECH, SPELL_AURA_PERIODIC_MANA_LEECH, SPELL_AURA_SCHOOL_ABSORB, SPELL_AURA_PERIODIC_TRIGGER_SPELL_WITH_VALUE ); public const AURAS_ITEM_CREATE = array( SPELL_AURA_CHANNEL_DEATH_ITEM @@ -75,14 +85,14 @@ class SpellList extends DBTypeList SPELL_AURA_TRANSFORM, SPELL_AURA_MOUNTED, SPELL_AURA_CHANGE_MODEL_FOR_ALL_HUMANOIDS, SPELL_AURA_X_RAY, SPELL_AURA_MOD_FAKE_INEBRIATE ); - public const AURAS_PERIODIC_SCALING = array( + public const AURAS_PERIODIC_SCALING = array( // as per Unit::GetCastingTimeForBonus() SPELL_AURA_PERIODIC_DAMAGE, SPELL_AURA_PERIODIC_HEAL, SPELL_AURA_PERIODIC_LEECH ); private array $spellVars = []; private array $refSpells = []; private array $tools = []; - private bool $interactive = false; + private int $interactive = self::INTERACTIVE_EMBEDDED; private int $charLevel = MAX_LEVEL; private array $scaling = []; private array $parsedText = []; @@ -92,13 +102,14 @@ class SpellList extends DBTypeList 10 => 4 ); - protected string $queryBase = 'SELECT s.*, s.`id` AS ARRAY_KEY FROM ?_spell s'; + protected string $queryBase = 'SELECT s.*, s.`id` AS ARRAY_KEY FROM ::spell s'; protected array $queryOpts = array( 's' => [['src', 'sr', 'ic', 'ica']], // 6: Type::SPELL - 'ic' => ['j' => ['?_icons ic ON ic.`id` = s.`iconId`', true], 's' => ', ic.`name` AS "iconString"'], - 'ica' => ['j' => ['?_icons ica ON ica.`id` = s.`iconIdAlt`', true], 's' => ', ica.`name` AS "iconStringAlt"'], - 'sr' => ['j' => ['?_spellrange sr ON sr.`id` = s.`rangeId`'], 's' => ', sr.`rangeMinHostile`, sr.`rangeMinFriend`, sr.`rangeMaxHostile`, sr.`rangeMaxFriend`, sr.`name_loc0` AS "rangeText_loc0", sr.`name_loc2` AS "rangeText_loc2", sr.`name_loc3` AS "rangeText_loc3", sr.`name_loc4` AS "rangeText_loc4", sr.`name_loc6` AS "rangeText_loc6", sr.`name_loc8` AS "rangeText_loc8"'], - 'src' => ['j' => ['?_source src ON `type` = 6 AND `typeId` = s.`id`', true], 's' => ', `moreType`, `moreTypeId`, `moreZoneId`, `moreMask`, `src1`, `src2`, `src3`, `src4`, `src5`, `src6`, `src7`, `src8`, `src9`, `src10`, `src11`, `src12`, `src13`, `src14`, `src15`, `src16`, `src17`, `src18`, `src19`, `src20`, `src21`, `src22`, `src23`, `src24`'] + 'nml' => ['j' => ['::spell_search nml ON nml.`id` = s.`id` AND nml.`locale` = DB_LOC_I']], + 'ic' => ['j' => ['::icons ic ON ic.`id` = s.`iconId`', true], 's' => ', ic.`name` AS "iconString"'], + 'ica' => ['j' => ['::icons ica ON ica.`id` = s.`iconIdAlt`', true], 's' => ', ica.`name` AS "iconStringAlt"'], + 'sr' => ['j' => ['::spellrange sr ON sr.`id` = s.`rangeId`'], 's' => ', sr.`rangeMinHostile`, sr.`rangeMinFriend`, sr.`rangeMaxHostile`, sr.`rangeMaxFriend`, sr.`name_loc0` AS "rangeText_loc0", sr.`name_loc2` AS "rangeText_loc2", sr.`name_loc3` AS "rangeText_loc3", sr.`name_loc4` AS "rangeText_loc4", sr.`name_loc6` AS "rangeText_loc6", sr.`name_loc8` AS "rangeText_loc8"'], + 'src' => ['j' => ['::source src ON `type` = 6 AND `typeId` = s.`id`', true], 's' => ', `moreType`, `moreTypeId`, `moreZoneId`, `moreMask`, `src1`, `src2`, `src3`, `src4`, `src5`, `src6`, `src7`, `src8`, `src9`, `src10`, `src11`, `src12`, `src13`, `src14`, `src15`, `src16`, `src17`, `src18`, `src19`, `src20`, `src21`, `src22`, `src23`, `src24`'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -115,7 +126,7 @@ class SpellList extends DBTypeList $this->charLevel = $miscData['charLevel']; // post processing - $foo = DB::World()->selectCol('SELECT `perfectItemType` FROM skill_perfect_item_template WHERE `spellId` IN (?a)', $this->getFoundIDs()); + $foo = DB::World()->selectCol('SELECT `perfectItemType` FROM skill_perfect_item_template WHERE `spellId` IN %in', $this->getFoundIDs()); foreach ($this->iterate() as &$_curTpl) { // required for globals @@ -180,7 +191,7 @@ class SpellList extends DBTypeList } if ($foo) - $this->relItems = new ItemList(array(['i.id', array_unique($foo)], Cfg::get('SQL_LIMIT_NONE'))); + $this->relItems = new ItemList(array(['i.id', array_unique($foo)])); } // required for item-comparison @@ -230,56 +241,48 @@ class SpellList extends DBTypeList // <statistic> => [123, 'add'] // <statistic> => <value> ... as from getStatGain() - $modXByStat = function (&$arr, $stat, $pts) use (&$mv) + $modXByStat = function (array &$arr, int $srcStat, ?string $destStat, int $pts) : void { - if ($mv == STAT_STRENGTH) - $arr[$stat ?: 'str'] = [$pts / 100, 'percentOf', 'str']; - else if ($mv == STAT_AGILITY) - $arr[$stat ?: 'agi'] = [$pts / 100, 'percentOf', 'agi']; - else if ($mv == STAT_STAMINA) - $arr[$stat ?: 'sta'] = [$pts / 100, 'percentOf', 'sta']; - else if ($mv == STAT_INTELLECT) - $arr[$stat ?: 'int'] = [$pts / 100, 'percentOf', 'int']; - else if ($mv == STAT_SPIRIT) - $arr[$stat ?: 'spi'] = [$pts / 100, 'percentOf', 'spi']; + match ($srcStat) + { + STAT_STRENGTH => $arr[$destStat ?: 'str'] = [$pts / 100, 'percentOf', 'str'], + STAT_AGILITY => $arr[$destStat ?: 'agi'] = [$pts / 100, 'percentOf', 'agi'], + STAT_STAMINA => $arr[$destStat ?: 'sta'] = [$pts / 100, 'percentOf', 'sta'], + STAT_INTELLECT => $arr[$destStat ?: 'int'] = [$pts / 100, 'percentOf', 'int'], + STAT_SPIRIT => $arr[$destStat ?: 'spi'] = [$pts / 100, 'percentOf', 'spi'] + }; }; - $modXBySchool = function (&$arr, $stat, $val, $mask = null) use (&$mv) + $modXBySchool = function (array &$arr, int $srcStat, string $destStat, array|int $val) : void { - if (($mask ?: $mv) & (1 << SPELL_SCHOOL_HOLY)) - $arr['hol'.$stat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'hol'.$stat]; - if (($mask ?: $mv) & (1 << SPELL_SCHOOL_FIRE)) - $arr['fir'.$stat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'fir'.$stat]; - if (($mask ?: $mv) & (1 << SPELL_SCHOOL_NATURE)) - $arr['nat'.$stat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'nat'.$stat]; - if (($mask ?: $mv) & (1 << SPELL_SCHOOL_FROST)) - $arr['fro'.$stat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'fro'.$stat]; - if (($mask ?: $mv) & (1 << SPELL_SCHOOL_SHADOW)) - $arr['sha'.$stat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'sha'.$stat]; - if (($mask ?: $mv) & (1 << SPELL_SCHOOL_ARCANE)) - $arr['arc'.$stat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'arc'.$stat]; + if ($srcStat & (1 << SPELL_SCHOOL_HOLY)) + $arr['hol'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'hol'.$destStat]; + if ($srcStat & (1 << SPELL_SCHOOL_FIRE)) + $arr['fir'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'fir'.$destStat]; + if ($srcStat & (1 << SPELL_SCHOOL_NATURE)) + $arr['nat'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'nat'.$destStat]; + if ($srcStat & (1 << SPELL_SCHOOL_FROST)) + $arr['fro'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'fro'.$destStat]; + if ($srcStat & (1 << SPELL_SCHOOL_SHADOW)) + $arr['sha'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'sha'.$destStat]; + if ($srcStat & (1 << SPELL_SCHOOL_ARCANE)) + $arr['arc'.$destStat] = is_array($val) ? $val : [$val / 100, 'percentOf', 'arc'.$destStat]; }; - $jsonStat = function ($stat) + $jsonStat = function (int $statId) : string { - if ($stat == STAT_STRENGTH) - return 'str'; - if ($stat == STAT_AGILITY) - return 'agi'; - if ($stat == STAT_STAMINA) - return 'sta'; - if ($stat == STAT_INTELLECT) - return 'int'; - if ($stat == STAT_SPIRIT) - return 'spi'; + return match ($statId) + { + STAT_STRENGTH => Stat::getJsonString(Stat::STRENGTH), + STAT_AGILITY => Stat::getJsonString(Stat::AGILITY), + STAT_STAMINA => Stat::getJsonString(Stat::STAMINA), + STAT_INTELLECT => Stat::getJsonString(Stat::INTELLECT), + STAT_SPIRIT => Stat::getJsonString(Stat::SPIRIT) + }; }; foreach ($this->iterate() as $id => $__) { - // kept for reference - if (($this->getField('cuFlags') & SPELL_CU_TALENTSPELL) && $id != 20711) - if (!($this->getField('attributes0') & SPELL_ATTR0_PASSIVE)) - continue; - // Shaman - Spirit Weapons (16268) (parry is normaly stored in g_statistics) // i should recurse into SPELL_EFFECT_LEARN_SPELL and apply SPELL_EFFECT_PARRY from there if ($id == 16268) @@ -288,6 +291,9 @@ class SpellList extends DBTypeList continue; } + if (!($this->getField('attributes0') & SPELL_ATTR0_PASSIVE)) + continue; + for ($i = 1; $i < 4; $i++) { $pts = $this->calculateAmountForCurrent($i)[1]; @@ -311,18 +317,18 @@ class SpellList extends DBTypeList if ($mv == (1 << SPELL_SCHOOL_NORMAL)) $data[$id]['armor'] = [$pts / 100, 'percentOf', ['armor', 0]]; else if ($mv) - $modXBySchool($data[$id], 'res', $pts); + $modXBySchool($data[$id], $mv, 'res', $pts); break; case SPELL_AURA_MOD_RESISTANCE_OF_STAT_PERCENT: // Armor only if explicitly specified if ($mv == (1 << SPELL_SCHOOL_NORMAL)) $data[$id]['armor'] = [$pts / 100, 'percentOf', $jsonStat($mvB)]; else if ($mv) - $modXBySchool($data[$id], 'res', [$pts / 100, 'percentOf', $jsonStat($mvB)]); + $modXBySchool($data[$id], $mv, 'res', [$pts / 100, 'percentOf', $jsonStat($mvB)]); break; case SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE: if ($mv > -1) // one stat - $modXByStat($data[$id], null, $pts); + $modXByStat($data[$id], $mv, null, $pts); else if ($mv < 0) // all stats for ($iMod = ITEM_MOD_AGILITY; $iMod <= ITEM_MOD_STAMINA; $iMod++) if ($idx = Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $iMod)) @@ -331,19 +337,19 @@ class SpellList extends DBTypeList break; case SPELL_AURA_MOD_SPELL_DAMAGE_OF_STAT_PERCENT: $mv = $mv ?: SPELL_MAGIC_SCHOOLS; - $modXBySchool($data[$id], 'spldmg', [$pts / 100, 'percentOf', $jsonStat($mvB)]); + $modXBySchool($data[$id], $mv, 'spldmg', [$pts / 100, 'percentOf', $jsonStat($mvB)]); break; case SPELL_AURA_MOD_RANGED_ATTACK_POWER_OF_STAT_PERCENT: - $modXByStat($data[$id], 'rgdatkpwr', $pts); + $modXByStat($data[$id], $mv, 'rgdatkpwr', $pts); break; case SPELL_AURA_MOD_ATTACK_POWER_OF_STAT_PERCENT: - $modXByStat($data[$id], 'mleatkpwr', $pts); + $modXByStat($data[$id], $mv, 'mleatkpwr', $pts); break; case SPELL_AURA_MOD_SPELL_HEALING_OF_STAT_PERCENT: - $modXByStat($data[$id], 'splheal', $pts); + $modXByStat($data[$id], $mv, 'splheal', $pts); break; case SPELL_AURA_MOD_MANA_REGEN_FROM_STAT: - $modXByStat($data[$id], 'manargn', $pts); + $modXByStat($data[$id], $mv, 'manargn', $pts); break; case SPELL_AURA_MOD_MANA_REGEN_INTERRUPT: $data[$id]['icmanargn'] = [$pts / 100, 'percentOf', 'oocmanargn']; @@ -351,7 +357,7 @@ class SpellList extends DBTypeList case SPELL_AURA_MOD_SPELL_CRIT_CHANCE: case SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL: $mv = $mv ?: SPELL_MAGIC_SCHOOLS; - $modXBySchool($data[$id], 'splcritstrkpct', [$pts, 'add']); + $modXBySchool($data[$id], $mv, 'splcritstrkpct', [$pts, 'add']); if (($mv & SPELL_MAGIC_SCHOOLS) == SPELL_MAGIC_SCHOOLS) $data[$id]['splcritstrkpct'] = [$pts, 'add']; break; @@ -392,7 +398,7 @@ class SpellList extends DBTypeList $data[$id]['health'] = [$pts / 100, 'percentOf', 'health']; break; case SPELL_AURA_MOD_BASE_HEALTH_PCT: // only Tauren - Endurance (20550) ... if you are looking for something elegant, look away! - $data[$id]['health'] = [$pts / 100, 'functionOf', '$function(p) { return g_statistics.combo[p.classs][p.level][5]; }']; + $data[$id]['health'] = [$pts / 100, 'functionOf', '$(x) => g_statistics.combo[x.classs][x.level][5]']; break; case SPELL_AURA_MOD_SHIELD_BLOCKVALUE_PCT: $data[$id]['block'] = [$pts / 100, 'percentOf', 'block']; @@ -404,7 +410,7 @@ class SpellList extends DBTypeList break; case SPELL_AURA_MOD_SPELL_DAMAGE_OF_ATTACK_POWER: $mv = $mv ?: SPELL_MAGIC_SCHOOLS; - $modXBySchool($data[$id], 'spldmg', [$pts / 100, 'percentOf', 'mleatkpwr']); + $modXBySchool($data[$id], $mv, 'spldmg', [$pts / 100, 'percentOf', 'mleatkpwr']); break; case SPELL_AURA_MOD_SPELL_HEALING_OF_ATTACK_POWER: $data[$id]['splheal'] = [$pts / 100, 'percentOf', 'mleatkpwr']; @@ -445,7 +451,7 @@ class SpellList extends DBTypeList // TotemCategory if ($_ = $this->curTpl['toolCategory'.$i]) { - $tc = DB::Aowow()->selectRow('SELECT * FROM ?_totemcategory WHERE `id` = ?d', $_); + $tc = DB::Aowow()->selectRow('SELECT * FROM ::totemcategory WHERE `id` = %i', $_); $tools[$i + 1] = array( 'id' => $_, 'name' => Util::localizedString($tc, 'name')); @@ -519,7 +525,7 @@ class SpellList extends DBTypeList 2289 => [2289, 29415, 29418, 29419, 29420, 29421] // Bear - Tauren ); - if ($st = DB::Aowow()->selectRow('SELECT *, `displayIdA` AS "model1", `displayIdH` AS "model2" FROM ?_shapeshiftforms WHERE `id` = ?d', $effMV)) + if ($st = DB::Aowow()->selectRow('SELECT *, `displayIdA` AS "model1", `displayIdH` AS "model2" FROM ::shapeshiftforms WHERE `id` = %i', $effMV)) { foreach ([1, 2] as $j) if (isset($subForms[$st['model'.$j]])) @@ -631,6 +637,10 @@ class SpellList extends DBTypeList $pps = $this->curTpl['powerPerSecond']; $pcpl = $this->curTpl['powerCostPerLevel']; + // some potion effects have this set, but it's not displayed by client (or enforced by core) + if ($pt == POWER_HAPPINESS) + return ''; + if ($pt == POWER_RAGE || $pt == POWER_RUNIC_POWER) $pc /= 10; @@ -652,6 +662,9 @@ class SpellList extends DBTypeList $str .= $pcp."% ".Lang::spell('pctCostOf', [mb_strtolower(Lang::spell('powerTypes', $pt))]); else if ($pc > 0 || $pps > 0 || $pcpl > 0) { + if ($this->curTpl['attributes0'] & SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION) + $str .= '<!--pts'.$this->curTpl['baseLevel'].':'.$pc.'-->'; + if (Lang::exist('spell', 'powerCost', $pt)) $str .= Lang::spell('powerCost', $pt, intVal($pps > 0), [$pc, $pps]); else @@ -676,7 +689,7 @@ class SpellList extends DBTypeList else if ($noInstant && !in_array($this->curTpl['typeCat'], [11, 7, -3, -6, -8, 0]) && !($this->curTpl['cuFlags'] & SPELL_CU_TALENTSPELL)) return ''; else - return $short ? Lang::formatTime($this->curTpl['castTime'] * 1000, 'spell', 'castTime') : Util::formatTime($this->curTpl['castTime'] * 1000); + return $short ? Lang::formatTime($this->curTpl['castTime'] * 1000, 'spell', 'castTime') : DateTime::formatTimeElapsedFloat($this->curTpl['castTime'] * 1000); } private function createCooldownForCurrent() : string @@ -694,13 +707,17 @@ class SpellList extends DBTypeList // formulae base from TC private function calculateAmountForCurrent(int $effIdx, int $nTicks = 1) : array { - $level = $this->charLevel; - $maxBase = 0; - $rppl = $this->getField('effect'.$effIdx.'RealPointsPerLevel'); - $base = $this->getField('effect'.$effIdx.'BasePoints'); - $add = $this->getField('effect'.$effIdx.'DieSides'); - $maxLvl = $this->getField('maxLevel'); - $baseLvl = $this->getField('baseLevel'); + $level = $this->charLevel; + $maxBase = 0; + $rppl = $this->getField('effect'.$effIdx.'RealPointsPerLevel'); + $base = $this->getField('effect'.$effIdx.'BasePoints'); + $add = $this->getField('effect'.$effIdx.'DieSides'); + $maxLvl = $this->getField('maxLevel'); + $baseLvl = $this->getField('baseLevel'); + $spellLvl = $this->getField('spellLevel'); + $LDSEffs = $this->canLevelDamageScale(); + $modMin = + $modMax = null; if ($rppl) { @@ -709,22 +726,29 @@ class SpellList extends DBTypeList else if ($level < $baseLvl) $level = $baseLvl; - if (!$this->getField('atributes0') & SPELL_ATTR0_PASSIVE) - $level -= $this->getField('spellLevel'); + if (!$this->getField('attributes0') & SPELL_ATTR0_PASSIVE) + $level -= $spellLvl; $maxBase += (int)(($level - $baseLvl) * $rppl); $maxBase *= $nTicks; + } $min = $nTicks * ($add ? $base + 1 : $base); $max = $nTicks * ($add + $base); - return [ - $min + $maxBase, - $max + $maxBase, - $rppl ? '<!--ppl'.$baseLvl.':'.($maxLvl ?: $level).':'.$min.':'.($rppl * 100 * $nTicks).'-->' : null, - $rppl ? '<!--ppl'.$baseLvl.':'.($maxLvl ?: $level).':'.$max.':'.($rppl * 100 * $nTicks).'-->' : null - ]; + if ($rppl) + { + $modMin = '<!--ppl'.$baseLvl.':'.($maxLvl ?: $level).':'.$min.':'.($rppl * 100 * $nTicks).'-->'; + $modMax = '<!--ppl'.$baseLvl.':'.($maxLvl ?: $level).':'.$max.':'.($rppl * 100 * $nTicks).'-->'; + } + else if ($this->getField('attributes0') & SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION && in_array($effIdx, $LDSEffs) && $spellLvl) + { + $modMin = '<!--pts'.$spellLvl.':'.abs($min).'-->'; + $modMax = '<!--pts'.$spellLvl.':'.abs($max).'-->'; + } + + return [$min + $maxBase, $max + $maxBase, $modMin, $modMax]; } public function canCreateItem() : array @@ -770,27 +794,37 @@ class SpellList extends DBTypeList return $idx; } + public function canLevelDamageScale() : array + { + $idx = []; + for ($i = 1; $i < 4; $i++) + if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_LDC_SCALING) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_LDC_SCALING)) + $idx[] = $i; + + return $idx; + } + public function isChanneledSpell() : bool { return $this->curTpl['attributes1'] & (SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2); } - public function isHealingSpell() : bool + public function isScalableHealingSpell() : bool { for ($i = 1; $i < 4; $i++) - if (!in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_HEAL) && !in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_HEAL)) - return false; + if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_SCALING_HEAL) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_SCALING_HEAL)) + return true; - return true; + return false; } - public function isDamagingSpell() : bool + public function isScalableDamagingSpell() : bool { for ($i = 1; $i < 4; $i++) - if (!in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_DAMAGE) && !in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_DAMAGE)) - return false; + if (in_array($this->curTpl['effect'.$i.'Id'], SpellList::EFFECTS_SCALING_DAMAGE) || in_array($this->curTpl['effect'.$i.'AuraId'], SpellList::AURAS_SCALING_DAMAGE)) + return true; - return true; + return false; } public function periodicEffectsMask() : int @@ -804,28 +838,36 @@ class SpellList extends DBTypeList return $effMask; } + private function dfnText(string $tooltip, string $text) : string + { + if ($this->interactive < self::INTERACTIVE_FULL) + return $text; + + return sprintf(Util::$dfnString, $tooltip, $text); + } + // description-, buff-parsing component private function resolveEvaluation(string $formula) : string { // see Traits in javascript locales $PlayerName = Lang::main('name'); - $pl = $PL = /* playerLevel set manually ? $this->charLevel : */ $this->interactive ? sprintf(Util::$dfnString, 'LANG.level', Lang::game('level')) : Lang::game('level'); - $ap = $AP = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.atkpwr[0]', Lang::spell('traitShort', 'atkpwr')) : Lang::spell('traitShort', 'atkpwr'); - $rap = $RAP = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.rgdatkpwr[0]', Lang::spell('traitShort', 'rgdatkpwr')) : Lang::spell('traitShort', 'rgdatkpwr'); - $sp = $SP = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.splpwr[0]', Lang::spell('traitShort', 'splpwr')) : Lang::spell('traitShort', 'splpwr'); - $spa = $SPA = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.arcsplpwr[0]', Lang::spell('traitShort', 'arcsplpwr')) : Lang::spell('traitShort', 'arcsplpwr'); - $spfi = $SPFI = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.firsplpwr[0]', Lang::spell('traitShort', 'firsplpwr')) : Lang::spell('traitShort', 'firsplpwr'); - $spfr = $SPFR = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.frosplpwr[0]', Lang::spell('traitShort', 'frosplpwr')) : Lang::spell('traitShort', 'frosplpwr'); - $sph = $SPH = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.holsplpwr[0]', Lang::spell('traitShort', 'holsplpwr')) : Lang::spell('traitShort', 'holsplpwr'); - $spn = $SPN = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.natsplpwr[0]', Lang::spell('traitShort', 'natsplpwr')) : Lang::spell('traitShort', 'natsplpwr'); - $sps = $SPS = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.shasplpwr[0]', Lang::spell('traitShort', 'shasplpwr')) : Lang::spell('traitShort', 'shasplpwr'); - $bh = $BH = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.splheal[0]', Lang::spell('traitShort', 'splheal')) : Lang::spell('traitShort', 'splheal'); - $spi = $SPI = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.spi[0]', Lang::spell('traitShort', 'spi')) : Lang::spell('traitShort', 'spi'); - $sta = $STA = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.sta[0]', Lang::spell('traitShort', 'sta')) : Lang::spell('traitShort', 'sta'); - $str = $STR = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.str[0]', Lang::spell('traitShort', 'str')) : Lang::spell('traitShort', 'str'); - $agi = $AGI = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.agi[0]', Lang::spell('traitShort', 'agi')) : Lang::spell('traitShort', 'agi'); - $int = $INT = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.int[0]', Lang::spell('traitShort', 'int')) : Lang::spell('traitShort', 'int'); + $pl = $PL = /* playerLevel set manually ? $this->charLevel : */ $this->dfnText('LANG.level', Lang::game('level')); + $ap = $AP = $this->dfnText('LANG.traits.atkpwr[0]', Lang::spell('traitShort', 'atkpwr')); + $rap = $RAP = $this->dfnText('LANG.traits.rgdatkpwr[0]', Lang::spell('traitShort', 'rgdatkpwr')); + $sp = $SP = $this->dfnText('LANG.traits.splpwr[0]', Lang::spell('traitShort', 'splpwr')); + $spa = $SPA = $this->dfnText('LANG.traits.arcsplpwr[0]', Lang::spell('traitShort', 'arcsplpwr')); + $spfi = $SPFI = $this->dfnText('LANG.traits.firsplpwr[0]', Lang::spell('traitShort', 'firsplpwr')); + $spfr = $SPFR = $this->dfnText('LANG.traits.frosplpwr[0]', Lang::spell('traitShort', 'frosplpwr')); + $sph = $SPH = $this->dfnText('LANG.traits.holsplpwr[0]', Lang::spell('traitShort', 'holsplpwr')); + $spn = $SPN = $this->dfnText('LANG.traits.natsplpwr[0]', Lang::spell('traitShort', 'natsplpwr')); + $sps = $SPS = $this->dfnText('LANG.traits.shasplpwr[0]', Lang::spell('traitShort', 'shasplpwr')); + $bh = $BH = $this->dfnText('LANG.traits.splheal[0]', Lang::spell('traitShort', 'splheal')); + $spi = $SPI = $this->dfnText('LANG.traits.spi[0]', Lang::spell('traitShort', 'spi')); + $sta = $STA = $this->dfnText('LANG.traits.sta[0]', Lang::spell('traitShort', 'sta')); + $str = $STR = $this->dfnText('LANG.traits.str[0]', Lang::spell('traitShort', 'str')); + $agi = $AGI = $this->dfnText('LANG.traits.agi[0]', Lang::spell('traitShort', 'agi')); + $int = $INT = $this->dfnText('LANG.traits.int[0]', Lang::spell('traitShort', 'int')); // only 'ron test spell', guess its %-dmg mod; no idea what bc2 might be $pa = '<$PctArcane>'; // %arcane @@ -838,14 +880,14 @@ class SpellList extends DBTypeList $pbhd = '<$PctHealDone>'; // %heal done $bc2 = '<$bc2>'; // bc2 - $HND = $hnd = $this->interactive ? sprintf(Util::$dfnString, '[Hands required by weapon]', 'HND') : 'HND'; // todo (med): localize this one - $MWS = $mws = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.mlespeed[0]', 'MWS') : 'MWS'; - $mw = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.dmgmin1[0]', 'mw') : 'mw'; - $MW = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.dmgmax1[0]', 'MW') : 'MW'; - $mwb = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.mledmgmin[0]', 'mwb') : 'mwb'; - $MWB = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.mledmgmax[0]', 'MWB') : 'MWB'; - $rwb = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.rgddmgmin[0]', 'rwb') : 'rwb'; - $RWB = $this->interactive ? sprintf(Util::$dfnString, 'LANG.traits.rgddmgmax[0]', 'RWB') : 'RWB'; + $HND = $hnd = $this->dfnText('[Hands required by weapon]', 'HND'); // todo (med): localize this one + $MWS = $mws = $this->dfnText('LANG.traits.mlespeed[0]', 'MWS'); + $mw = $this->dfnText('LANG.traits.dmgmin1[0]', 'mw'); + $MW = $this->dfnText('LANG.traits.dmgmax1[0]', 'MW'); + $mwb = $this->dfnText('LANG.traits.mledmgmin[0]', 'mwb'); + $MWB = $this->dfnText('LANG.traits.mledmgmax[0]', 'MWB'); + $rwb = $this->dfnText('LANG.traits.rgddmgmin[0]', 'rwb'); + $RWB = $this->dfnText('LANG.traits.rgddmgmax[0]', 'RWB'); $cond = $COND = fn($a, $b, $c) => $a ? $b : $c; $eq = $EQ = fn($a, $b) => $a == $b; @@ -882,14 +924,14 @@ class SpellList extends DBTypeList if (!$evalable) { // can't eval constructs because of strings present. replace constructs with strings - $cond = $COND = !$this->interactive ? 'COND' : sprintf(Util::$dfnString, 'COND(<span class=\'q1\'>a</span>, <span class=\'q1\'>b</span>, <span class=\'q1\'>c</span>)<br /> <span class=\'q1\'>a</span> ? <span class=\'q1\'>b</span> : <span class=\'q1\'>c</span>', 'COND'); - $eq = $EQ = !$this->interactive ? 'EQ' : sprintf(Util::$dfnString, 'EQ(<span class=\'q1\'>a</span>, <span class=\'q1\'>b</span>)<br /> <span class=\'q1\'>a</span> == <span class=\'q1\'>b</span>', 'EQ'); - $gt = $GT = !$this->interactive ? 'GT' : sprintf(Util::$dfnString, 'GT(<span class=\'q1\'>a</span>, <span class=\'q1\'>b</span>)<br /> <span class=\'q1\'>a</span> > <span class=\'q1\'>b</span>', 'GT'); - $gte = $GTE = !$this->interactive ? 'GTE' : sprintf(Util::$dfnString, 'GTE(<span class=\'q1\'>a</span>, <span class=\'q1\'>b</span>)<br /> <span class=\'q1\'>a</span> >= <span class=\'q1\'>b</span>', 'GTE'); - $floor = $FLOOR = !$this->interactive ? 'FLOOR' : sprintf(Util::$dfnString, 'FLOOR(<span class=\'q1\'>a</span>)', 'FLOOR'); - $min = $MIN = !$this->interactive ? 'MIN' : sprintf(Util::$dfnString, 'MIN(<span class=\'q1\'>a</span>, <span class=\'q1\'>b</span>)', 'MIN'); - $max = $MAX = !$this->interactive ? 'MAX' : sprintf(Util::$dfnString, 'MAX(<span class=\'q1\'>a</span>, <span class=\'q1\'>b</span>)', 'MAX'); - $pl = $PL = !$this->interactive ? 'PL' : sprintf(Util::$dfnString, 'LANG.level', 'PL'); + $cond = $COND = $this->dfnText('COND(<span class=\'q1\'>a</span>, <span class=\'q1\'>b</span>, <span class=\'q1\'>c</span>)<br /> <span class=\'q1\'>a</span> ? <span class=\'q1\'>b</span> : <span class=\'q1\'>c</span>', 'COND'); + $eq = $EQ = $this->dfnText('EQ(<span class=\'q1\'>a</span>, <span class=\'q1\'>b</span>)<br /> <span class=\'q1\'>a</span> == <span class=\'q1\'>b</span>', 'EQ'); + $gt = $GT = $this->dfnText('GT(<span class=\'q1\'>a</span>, <span class=\'q1\'>b</span>)<br /> <span class=\'q1\'>a</span> > <span class=\'q1\'>b</span>', 'GT'); + $gte = $GTE = $this->dfnText('GTE(<span class=\'q1\'>a</span>, <span class=\'q1\'>b</span>)<br /> <span class=\'q1\'>a</span> >= <span class=\'q1\'>b</span>', 'GTE'); + $floor = $FLOOR = $this->dfnText('FLOOR(<span class=\'q1\'>a</span>)', 'FLOOR'); + $min = $MIN = $this->dfnText('MIN(<span class=\'q1\'>a</span>, <span class=\'q1\'>b</span>)', 'MIN'); + $max = $MAX = $this->dfnText('MAX(<span class=\'q1\'>a</span>, <span class=\'q1\'>b</span>)', 'MAX'); + $pl = $PL = $this->dfnText('LANG.level', 'PL'); // space out operators for better readability $formula = preg_replace('/(\+|-|\*|\/)/', ' \1 ', $formula); @@ -910,7 +952,11 @@ class SpellList extends DBTypeList $formula = preg_replace('/(\+|-|\*|\/)(\+|-|\*|\/)/i', '\1 \2', $formula); // there should not be any letters without a leading $ - return eval('return '.$formula.';'); + try { $formula = eval('return '.$formula.';'); } + // but there can be if we are non-interactive + catch (\Throwable $e) { } + + return $formula; } // description-, buff-parsing component @@ -1083,14 +1129,14 @@ class SpellList extends DBTypeList $this->scaling[$this->id] = true; // Aura end - if ($stats) + if ($stats && $this->interactive >= self::INTERACTIVE_EMBEDDED) { $fmtStringMin = '<!--rtg%s-->%s <small>(%s)</small>'; $statId = $stats[0]; // could be multiple ratings in theory, but not expected to be } /* todo: export to and solve formulas in javascript e.g.: spell 10187 - ${$42213m1*8*$<mult>} with $mult = ${${$?s31678[${1.05}][${${$?s31677[${1.04}][${${$?s31676[${1.03}][${${$?s31675[${1.02}][${${$?s31674[${1.01}][${1}]}}]}}]}}]}}]}*${$?s12953[${1.06}][${${$?s12952[${1.04}][${${$?s11151[${1.02}][${1}]}}]}}]}} - else if ($this->interactive && ($modStrMin || $modStrMax)) + else if ($this->interactive == self::INTERACTIVE_FULL && ($modStrMin || $modStrMax)) { $this->scaling[$this->id] = true; $fmtStringMin = $modStrMin.'%s'; @@ -1130,7 +1176,7 @@ class SpellList extends DBTypeList eval("\$max = $max $op $oparg;"); } - if ($this->interactive && ($modStrMin || $modStrMax)) + if ($this->interactive >= self::INTERACTIVE_EMBEDDED && ($modStrMin || $modStrMax)) { $this->scaling[$this->id] = true; @@ -1177,12 +1223,12 @@ class SpellList extends DBTypeList $this->scaling[$this->id] = true; // Aura end - if ($stats) + if ($stats && $this->interactive >= self::INTERACTIVE_EMBEDDED) { $fmtStringMin = '<!--rtg%s-->%s <small>(%s)</small>'; $statId = $stats[0]; // could be multiple ratings in theory, but not expected to be } - else if (($modStrMin || $modStrMax) && $this->interactive) + else if (($modStrMin || $modStrMax) && $this->interactive == self::INTERACTIVE_FULL) { $this->scaling[$this->id] = true; $fmtStringMin = $modStrMin.'%s'; @@ -1346,7 +1392,7 @@ class SpellList extends DBTypeList return [$return, $fSuffix, $fStat]; } - // should probably used only once to create ?_spell. come to think of it, it yields the same results every time.. it absolutely has to! + // should probably used only once to create ::spell. come to think of it, it yields the same results every time.. it absolutely has to! // although it seems to be pretty fast, even on those pesky test-spells with extra complex tooltips (Ron Test Spell X)) public function parseText(string $type = 'description', int $level = MAX_LEVEL) : array { @@ -1358,7 +1404,7 @@ class SpellList extends DBTypeList documentation .. sort of bracket use ${}.x - formulas; .x is optional; x:[0-9] .. max-precision of a floatpoint-result; default: 0 - $[] - conditionals ... like $?condition[true][false]; alternative $?!(cond1|cond2)[true]$?cond3[elseTrue][false]; ?a40120: has aura 40120; ?s40120: knows spell 40120(?) + $[] - conditionals ... like $?condition[true][false]; alternative $?!(cond1|cond2)[true]$?cond3[elseTrue][false]; ?a40120: has aura 40120; ?s40120: knows spell 40120(%s) $<> - variables () - regular use for function-like calls @@ -1435,8 +1481,8 @@ class SpellList extends DBTypeList $this->charLevel = $level; // step -1: already handled? - if (isset($this->parsedText[$this->id][$type][Lang::getLocale()->value][$this->charLevel][(int)$this->interactive])) - return $this->parsedText[$this->id][$type][Lang::getLocale()->value][$this->charLevel][(int)$this->interactive]; + if (isset($this->parsedText[$this->id][$type][Lang::getLocale()->value][$this->charLevel][$this->interactive])) + return $this->parsedText[$this->id][$type][Lang::getLocale()->value][$this->charLevel][$this->interactive]; // step 0: get text $data = $this->getField($type, true); @@ -1448,7 +1494,7 @@ class SpellList extends DBTypeList { if (empty($this->spellVars[$this->id])) { - $spellVars = DB::Aowow()->SelectCell('SELECT `vars` FROM ?_spellvariables WHERE `id` = ?d', $this->curTpl['spellDescriptionVariableId']); + $spellVars = DB::Aowow()->SelectCell('SELECT `vars` FROM ::spellvariables WHERE `id` = %i', $this->curTpl['spellDescriptionVariableId']); $spellVars = explode("\n", $spellVars); foreach ($spellVars as $sv) if (preg_match('/\$(\w*\d*)=(.*)/i', trim($sv), $matches)) @@ -1542,7 +1588,7 @@ class SpellList extends DBTypeList $data = strtr($data, ["\r" => '', "\n" => '<br />']); // cache result - $this->parsedText[$this->id][$type][Lang::getLocale()->value][$this->charLevel][(int)$this->interactive] = [$data, $relSpells, $this->scaling[$this->id]]; + $this->parsedText[$this->id][$type][Lang::getLocale()->value][$this->charLevel][$this->interactive] = [$data, $relSpells, $this->scaling[$this->id]]; return [$data, $relSpells, $this->scaling[$this->id]]; } @@ -1588,10 +1634,8 @@ class SpellList extends DBTypeList } [$formOutVal, $formOutStr, $statId] = $this->resolveFormulaString($formOutStr, $formPrecision ?: ($topLevel ? 0 : 10)); - if ($statId && Util::checkNumeric($formOutVal) && $this->interactive) - $resolved = sprintf($formOutStr, $statId, abs($formOutVal), sprintf(Util::$setRatingLevelString, $this->charLevel, $statId, abs($formOutVal), Util::setRatingLevel($this->charLevel, $statId, abs($formOutVal)))); - else if ($statId && Util::checkNumeric($formOutVal)) - $resolved = sprintf($formOutStr, $statId, abs($formOutVal), Util::setRatingLevel($this->charLevel, $statId, abs($formOutVal))); + if ($statId && Util::checkNumeric($formOutVal)) + $resolved = sprintf($formOutStr, $statId, abs($formOutVal), Util::setRatingLevel($this->charLevel, $statId, abs($formOutVal), $this->interactive == self::INTERACTIVE_FULL)); else $resolved = sprintf($formOutStr, Util::checkNumeric($formOutVal) ? abs($formOutVal) : $formOutVal); @@ -1628,10 +1672,8 @@ class SpellList extends DBTypeList $resolved = is_numeric($minPoints) ? abs($minPoints) : $minPoints; if (isset($fmtStringMin)) { - if (isset($statId) && $this->interactive) - $resolved = sprintf($fmtStringMin, $statId, abs($minPoints), sprintf(Util::$setRatingLevelString, $this->charLevel, $statId, abs($minPoints), Util::setRatingLevel($this->charLevel, $statId, abs($minPoints)))); - else if (isset($statId)) - $resolved = sprintf($fmtStringMin, $statId, abs($minPoints), Util::setRatingLevel($this->charLevel, $statId, abs($minPoints))); + if (isset($statId)) + $resolved = sprintf($fmtStringMin, $statId, abs($minPoints), Util::setRatingLevel($this->charLevel, $statId, abs($minPoints), $this->interactive == self::INTERACTIVE_FULL)); else $resolved = sprintf($fmtStringMin, $resolved); } @@ -1766,7 +1808,7 @@ class SpellList extends DBTypeList return $data; } - public function renderBuff($level = MAX_LEVEL, $interactive = false, ?array &$buffSpells = []) : ?string + public function renderBuff(int $level = MAX_LEVEL, int $interactive = self::INTERACTIVE_EMBEDDED, ?array &$buffSpells = []) : ?string { $buffSpells = []; @@ -1815,7 +1857,7 @@ class SpellList extends DBTypeList return $x; } - public function renderTooltip(?int $level = MAX_LEVEL, ?bool $interactive = false, ?array &$ttSpells = []) : ?string + public function renderTooltip(int $level = MAX_LEVEL, int $interactive = self::INTERACTIVE_EMBEDDED, ?array &$ttSpells = []) : ?string { $ttSpells = []; @@ -1840,8 +1882,13 @@ class SpellList extends DBTypeList // get reagents $reagents = $this->getReagentsForCurrent(); - foreach ($reagents as &$r) - $r[2] = ItemList::getName($r[0]); + foreach ($reagents as $k => $r) + { + if ($item = $this->relItems->getEntry($r[0])) + $reagents[$k] += [2 => new LocString($item), 3 => true]; + else + $reagents[$k] += [2 => 'Item #'.$r[0], 3 => false]; + } $reagents = array_reverse($reagents); @@ -1941,11 +1988,11 @@ class SpellList extends DBTypeList if ($reagents) { $_ = Lang::spell('reagents').':<br/><div class="indent q1">'; - while ($reagent = array_pop($reagents)) + while ([$iId, $qty, $text, $exists] = array_pop($reagents)) { - $_ .= '<a href="?item='.$reagent[0].'">'.$reagent[2].'</a>'; - if ($reagent[1] > 1) - $_ .= ' ('.$reagent[1].')'; + $_ .= $exists ? '<a href="?item='.$iId.'">'.$text.'</a>' : $text; + if ($qty > 1) + $_ .= ' ('.$qty.')'; $_ .= empty($reagents) ? '<br />' : ', '; } @@ -1965,10 +2012,23 @@ class SpellList extends DBTypeList if ($xTmp) $x .= '<table><tr><td>'.implode('<br />', $xTmp).'</td></tr></table>'; - $min = $this->scaling[$this->id] ? ($this->getField('baseLevel') ?: 1) : 1; - $max = $this->scaling[$this->id] ? ($this->getField('maxLevel') ?: MAX_LEVEL) : 1; - // scaling information - spellId:min:max:curr - $x .= '<!--?'.$this->id.':'.$min.':'.$max.':'.min($this->charLevel, $max).'-->'; + // scaling information - spellId:min:max:curr[:scalingDistribution:ScalingFlags] + $scalingInfo = array( + $this->id, + $this->scaling[$this->id] ? ($this->getField('baseLevel') ?: 1) : 1, + $this->scaling[$this->id] ? ($this->getField('maxLevel') ?: MAX_LEVEL) : 1 + ); + + if ($this->getField('attributes0') & SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION) + { + $scalingInfo[] = $this->getField('spellLevel') ?: 1; + $scalingInfo[] = 1; // in 4.x+ proper scaling information; for us just to flag a npc spell as level damage scaling + $scalingInfo[] = 1; + } + else + $scalingInfo[] = min($this->charLevel, $scalingInfo[2]); + + $x .= '<!--?'.implode(':', $scalingInfo).'-->'; return $x; } @@ -2149,8 +2209,11 @@ class SpellList extends DBTypeList { $data[Type::SPELL][$id] = array( 'icon' => $this->curTpl['iconStringAlt'] ?: $this->curTpl['iconString'], - 'name' => $this->getField('name', true), + 'name' => $this->getField('name', true) ); + + if (($_ = $this->curTpl['typeCat']) && in_array($_, [-5, -6, 9, 11])) + $data[Type::SPELL][$id]['completion_category'] = $_; } if ($addMask & GLOBALINFO_EXTRA) @@ -2445,6 +2508,7 @@ class SpellListFilter extends Filter 40 => [parent::CR_ENUM, 'damageClass' ], // damagetype [damagetype] 41 => [parent::CR_FLAG, 'stanceMask', (1 << (22 - 1)) ], // requiresmetamorphosis [yn] 42 => [parent::CR_FLAG, 'attributes5', SPELL_ATTR5_USABLE_WHILE_STUNNED ], // usablewhenstunned [yn] + 43 => [parent::CR_FLAG, 'attributes3', SPELL_ATTR3_BATTLEGROUND ], // usableinbgs [yn] 44 => [parent::CR_FLAG, 'attributes4', SPELL_ATTR4_USABLE_IN_ARENA ], // usableinarenas [yn] 45 => [parent::CR_ENUM, 'powerType' ], // resourcetype [resourcetype] 46 => [parent::CR_FLAG, 'attributes0', SPELL_ATTR0_UNAFFECTED_BY_INVULNERABILITY ], // disregardimmunity [yn] @@ -2520,13 +2584,13 @@ class SpellListFilter extends Filter 'cr' => [parent::V_RANGE, [1, 116], true ], // criteria ids 'crs' => [parent::V_LIST, [parent::ENUM_NONE, parent::ENUM_ANY, [0, 99999]], true ], // criteria operators 'crv' => [parent::V_REGEX, parent::PATTERN_CRV, true ], // criteria values - only printable chars, no delimiters - 'na' => [parent::V_REGEX, parent::PATTERN_NAME, false], // name / text - only printable chars, no delimiter + 'na' => [parent::V_NAME, false, false], // name / text - only printable chars, no delimiter 'ex' => [parent::V_EQUAL, 'on', false], // extended name search 'ma' => [parent::V_EQUAL, 1, false], // match any / all filter - 'minle' => [parent::V_RANGE, [1, 99], false], // spell level min - 'maxle' => [parent::V_RANGE, [1, 99], false], // spell level max - 'minrs' => [parent::V_RANGE, [1, 999], false], // required skill level min - 'maxrs' => [parent::V_RANGE, [1, 999], false], // required skill level max + 'minle' => [parent::V_RANGE, [0, 99], false], // spell level min + 'maxle' => [parent::V_RANGE, [0, 99], false], // spell level max + 'minrs' => [parent::V_RANGE, [0, 999], false], // required skill level min + 'maxrs' => [parent::V_RANGE, [0, 999], false], // required skill level max 'ra' => [parent::V_LIST, [[1, 8], 10, 11], false], // races 'cl' => [parent::V_CALLBACK, 'cbClasses', true ], // classes 'gl' => [parent::V_CALLBACK, 'cbGlyphs', true ], // glyph type @@ -2540,17 +2604,24 @@ class SpellListFilter extends Filter $parts = []; $_v = &$this->values; - //string (extended) + // string (extended) if ($_v['na']) { - $_ = []; - if ($_v['ex'] == 'on') - $_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value, 'buff_loc'.Lang::getLocale()->value, 'description_loc'.Lang::getLocale()->value]); - else - $_ = $this->tokenizeString(['name_loc'.Lang::getLocale()->value]); + $f = [['na', ['nml.nName', 'nml.nBuff', 'nml.nDescription']]]; + if ($_v['ex'] != 'on') + $f = [['na', 'nml.nName']]; - if ($_) + if ($_ = $this->buildMatchLookup($f)) $parts[] = $_; + else + { + $f = [['na', 'name_loc'.Lang::getLocale()->value], ['na', 'buff_loc'.Lang::getLocale()->value], ['na', 'description_loc'.Lang::getLocale()->value]]; + if ($_v['ex'] != 'on') + $f = [$f[0]]; + + if ($_ = $this->buildLikeLookup($f)) + $parts[] = $_; + } } // spellLevel min todo (low): talentSpells (typeCat -2) commonly have spellLevel 1 (and talentLevel >1) -> query is inaccurate @@ -2571,7 +2642,7 @@ class SpellListFilter extends Filter // race if ($_v['ra']) - $parts[] = ['AND', [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!'], ['reqRaceMask', $this->list2Mask([$_v['ra']]), '&']]; + $parts[] = [DB::AND, [['reqRaceMask', ChrRace::MASK_ALL, '&'], ChrRace::MASK_ALL, '!'], ['reqRaceMask', $this->list2Mask([$_v['ra']]), '&']]; // class [list] if ($_v['cl']) @@ -2591,7 +2662,7 @@ class SpellListFilter extends Filter // mechanic if ($_v['me']) - $parts[] = ['OR', ['mechanic', $_v['me']], ['effect1Mechanic', $_v['me']], ['effect2Mechanic', $_v['me']], ['effect3Mechanic', $_v['me']]]; + $parts[] = [DB::OR, ['mechanic', $_v['me']], ['effect1Mechanic', $_v['me']], ['effect2Mechanic', $_v['me']], ['effect3Mechanic', $_v['me']]]; return $parts; } @@ -2629,9 +2700,9 @@ class SpellListFilter extends Filter if (!Util::checkNumeric($crv, NUM_CAST_INT) || !$this->int2Op($crs)) return null; - return ['OR', - ['AND', ['powerType', [POWER_RAGE, POWER_RUNIC_POWER]], ['powerCost', (10 * $crv), $crs]], - ['AND', ['powerType', [POWER_RAGE, POWER_RUNIC_POWER], '!'], ['powerCost', $crv, $crs]] + return [DB::OR, + [DB::AND, ['powerType', [POWER_RAGE, POWER_RUNIC_POWER]], ['powerCost', (10 * $crv), $crs]], + [DB::AND, ['powerType', [POWER_RAGE, POWER_RUNIC_POWER], '!'], ['powerCost', $crv, $crs]] ]; } @@ -2645,7 +2716,7 @@ class SpellListFilter extends Filter return ['src.src'.$_, null, '!']; else if ($_) // any { - $foo = ['OR']; + $foo = [DB::OR]; foreach (self::$enums[$cr] as $bar) if (is_int($bar)) $foo[] = ['src.src'.$bar, null, '!']; @@ -2664,9 +2735,9 @@ class SpellListFilter extends Filter return null; if ($crs) - return ['OR', ['reagent1', 0, '>'], ['reagent2', 0, '>'], ['reagent3', 0, '>'], ['reagent4', 0, '>'], ['reagent5', 0, '>'], ['reagent6', 0, '>'], ['reagent7', 0, '>'], ['reagent8', 0, '>']]; + return [DB::OR, ['reagent1', 0, '>'], ['reagent2', 0, '>'], ['reagent3', 0, '>'], ['reagent4', 0, '>'], ['reagent5', 0, '>'], ['reagent6', 0, '>'], ['reagent7', 0, '>'], ['reagent8', 0, '>']]; else - return ['AND', ['reagent1', 0], ['reagent2', 0], ['reagent3', 0], ['reagent4', 0], ['reagent5', 0], ['reagent6', 0], ['reagent7', 0], ['reagent8', 0]]; + return [DB::AND, ['reagent1', 0], ['reagent2', 0], ['reagent3', 0], ['reagent4', 0], ['reagent5', 0], ['reagent6', 0], ['reagent7', 0], ['reagent8', 0]]; } protected function cbAuraNames(int $cr, int $crs, string $crv) : ?array @@ -2674,7 +2745,7 @@ class SpellListFilter extends Filter if (!$this->checkInput(parent::V_RANGE, [1, self::MAX_SPELL_AURA], $crs)) return null; - return ['OR', ['effect1AuraId', $crs], ['effect2AuraId', $crs], ['effect3AuraId', $crs]]; + return [DB::OR, ['effect1AuraId', $crs], ['effect2AuraId', $crs], ['effect3AuraId', $crs]]; } protected function cbEffectNames(int $cr, int $crs, string $crv) : ?array @@ -2682,7 +2753,7 @@ class SpellListFilter extends Filter if (!$this->checkInput(parent::V_RANGE, [1, self::MAX_SPELL_EFFECT], $crs)) return null; - return ['OR', ['effect1Id', $crs], ['effect2Id', $crs], ['effect3Id', $crs]]; + return [DB::OR, ['effect1Id', $crs], ['effect2Id', $crs], ['effect3Id', $crs]]; } protected function cbInverseFlag(int $cr, int $crs, string $crv, string $field, int $flag) : ?array @@ -2702,9 +2773,9 @@ class SpellListFilter extends Filter return null; if ($crs) - return ['AND', [[$field, $flag, '&'], 0], ['dispelType', SPELL_DAMAGE_CLASS_MAGIC]]; + return [DB::AND, [[$field, $flag, '&'], 0], ['dispelType', SPELL_DAMAGE_CLASS_MAGIC]]; else - return ['OR', [$field, $flag, '&'], ['dispelType', SPELL_DAMAGE_CLASS_MAGIC, '!']]; + return [DB::OR, [$field, $flag, '&'], ['dispelType', SPELL_DAMAGE_CLASS_MAGIC, '!']]; } protected function cbReqFaction(int $cr, int $crs, string $crv) : ?array @@ -2714,11 +2785,11 @@ class SpellListFilter extends Filter // yes 1 => ['reqRaceMask', 0, '!'], // alliance - 2 => ['AND', [['reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']], + 2 => [DB::AND, [['reqRaceMask', ChrRace::MASK_HORDE, '&'], 0], ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&']], // horde - 3 => ['AND', [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['reqRaceMask', ChrRace::MASK_HORDE, '&']], + 3 => [DB::AND, [['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], 0], ['reqRaceMask', ChrRace::MASK_HORDE, '&']], // both - 4 => ['AND', ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['reqRaceMask', ChrRace::MASK_HORDE, '&']], + 4 => [DB::AND, ['reqRaceMask', ChrRace::MASK_ALLIANCE, '&'], ['reqRaceMask', ChrRace::MASK_HORDE, '&']], // no 5 => ['reqRaceMask', 0], default => null @@ -2734,9 +2805,9 @@ class SpellListFilter extends Filter $field = $useInvType ? 'equippedItemInventoryTypeMask' : 'equippedItemSubClassMask'; if ($crs) - return ['AND', ['equippedItemClass', ITEM_CLASS_WEAPON], [$field, $mask, '&']]; + return [DB::AND, ['equippedItemClass', ITEM_CLASS_WEAPON], [$field, $mask, '&']]; else - return ['OR', ['equippedItemClass', ITEM_CLASS_WEAPON, '!'], [[$field, $mask, '&'], 0]]; + return [DB::OR, ['equippedItemClass', ITEM_CLASS_WEAPON, '!'], [[$field, $mask, '&'], 0]]; } /* unused - for reference: attribute flag or cooldown time constraint */ @@ -2746,14 +2817,14 @@ class SpellListFilter extends Filter return null; if ($crs) - return ['AND', + return [DB::AND, [['attributes4', SPELL_ATTR4_NOT_USABLE_IN_ARENA, '&'], 0], - ['OR', ['recoveryTime', 10 * MINUTE * 1000, '<='], ['attributes4', SPELL_ATTR4_USABLE_IN_ARENA, '&']] + [DB::OR, ['recoveryTime', 10 * MINUTE * 1000, '<='], ['attributes4', SPELL_ATTR4_USABLE_IN_ARENA, '&']] ]; else - return ['OR', + return [DB::OR, ['attributes4', SPELL_ATTR4_NOT_USABLE_IN_ARENA, '&'], - ['AND', ['recoveryTime', 10 * MINUTE * 1000, '>'], [['attributes4', SPELL_ATTR4_USABLE_IN_ARENA, '&'], 0]] + [DB::AND, ['recoveryTime', 10 * MINUTE * 1000, '>'], [['attributes4', SPELL_ATTR4_USABLE_IN_ARENA, '&'], 0]] ]; } @@ -2763,9 +2834,9 @@ class SpellListFilter extends Filter return null; if ($crs) // match exact, not as flag - return ['AND', ['attributes1', SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2 | SPELL_ATTR1_CHANNEL_TRACK_TARGET], ['effect1ImplicitTargetA', 21]]; + return [DB::AND, ['attributes1', SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2 | SPELL_ATTR1_CHANNEL_TRACK_TARGET], ['effect1ImplicitTargetA', 21]]; else - return ['OR', ['attributes1', SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2 | SPELL_ATTR1_CHANNEL_TRACK_TARGET, '!'], ['effect1ImplicitTargetA', 21, '!']]; + return [DB::OR, ['attributes1', SPELL_ATTR1_CHANNELED_1 | SPELL_ATTR1_CHANNELED_2 | SPELL_ATTR1_CHANNEL_TRACK_TARGET, '!'], ['effect1ImplicitTargetA', 21, '!']]; } protected function cbProficiency(int $cr, int $crs, string $crv) : ?array @@ -2781,16 +2852,16 @@ class SpellListFilter extends Filter case 1: // Weapons foreach (Game::$skillLineMask[-3] as $bit => $_) $skill2Mask |= (1 << $bit); - $skill1Ids = DB::Aowow()->selectCol('SELECT `id` FROM ?_skillline WHERE `typeCat` = 6'); + $skill1Ids = DB::Aowow()->selectCol('SELECT `id` FROM ::skillline WHERE `typeCat` = 6'); break; case 2: // Armor (Proficiencies + Specializations: so for us it's the same) case 3: // Armor Proficiencies - $skill1Ids = DB::Aowow()->selectCol('SELECT `id` FROM ?_skillline WHERE `typeCat` = 8'); + $skill1Ids = DB::Aowow()->selectCol('SELECT `id` FROM ::skillline WHERE `typeCat` = 8'); break; case 4: // Armor Specializations return [0]; // 4.x+ feature where using purely one type of armor increases your primary stat case 5: // Languages - $skill1Ids = DB::Aowow()->selectCol('SELECT `id` FROM ?_skillline WHERE `typeCat` = 10'); + $skill1Ids = DB::Aowow()->selectCol('SELECT `id` FROM ::skillline WHERE `typeCat` = 10'); break; } @@ -2799,7 +2870,7 @@ class SpellListFilter extends Filter $cnd = ['skillLine1', $skill1Ids]; if ($skill2Mask) - $cnd = ['OR', $cnd, ['AND', ['skillLine1', -3], ['skillLine2OrMask', $skill2Mask, '&']]]; + $cnd = [DB::OR, $cnd, [DB::AND, ['skillLine1', -3], ['skillLine2OrMask', $skill2Mask, '&']]]; return $cnd; } diff --git a/includes/dbtypes/title.class.php b/includes/dbtypes/title.class.php index b8cd1cb2..b583a290 100644 --- a/includes/dbtypes/title.class.php +++ b/includes/dbtypes/title.class.php @@ -12,13 +12,13 @@ class TitleList extends DBTypeList public static int $type = Type::TITLE; public static string $brickFile = 'title'; - public static string $dataTable = '?_titles'; + public static string $dataTable = '::titles'; public array $sources = []; - protected string $queryBase = 'SELECT t.*, t.`id` AS ARRAY_KEY FROM ?_titles t'; + protected string $queryBase = 'SELECT t.*, t.`id` AS ARRAY_KEY FROM ::titles t'; protected array $queryOpts = array( 't' => [['src']], // 11: Type::TITLE - 'src' => ['j' => ['?_source src ON `type` = 11 AND `typeId` = t.`id`', true], 's' => ', `src13`, `moreType`, `moreTypeId`'] + 'src' => ['j' => ['::source src ON `type` = 11 AND `typeId` = t.`id`', true], 's' => ', `src13`, `moreType`, `moreTypeId`'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -55,7 +55,7 @@ class TitleList extends DBTypeList public static function getName(int $id) : ?LocString { - if ($n = DB::Aowow()->SelectRow('SELECT `male_loc0`, `male_loc2`, `male_loc3`, `male_loc4`, `male_loc6`, `male_loc8` FROM ?# WHERE `id` = ?d', self::$dataTable, $id)) + if ($n = DB::Aowow()->SelectRow('SELECT `male_loc0`, `male_loc2`, `male_loc3`, `male_loc4`, `male_loc6`, `male_loc8` FROM %n WHERE `id` = %i', self::$dataTable, $id)) return new LocString($n, 'male', fn($x) => trim(str_replace('%s', '', $x))); return null; } diff --git a/includes/dbtypes/user.class.php b/includes/dbtypes/user.class.php index 44f4f83b..18122f6b 100644 --- a/includes/dbtypes/user.class.php +++ b/includes/dbtypes/user.class.php @@ -13,10 +13,10 @@ class UserList extends DBTypeList public static string $dataTable = ''; public static int $contribute = CONTRIBUTE_NONE; - protected string $queryBase = 'SELECT *, a.`id` AS ARRAY_KEY FROM ?_account a'; + protected string $queryBase = 'SELECT *, a.`id` AS ARRAY_KEY FROM ::account a'; protected array $queryOpts = array( 'a' => [['r']], - 'r' => ['j' => ['?_account_reputation r ON r.`userId` = a.`id`', true], 's' => ', IFNULL(SUM(r.`amount`), 0) AS "reputation"', 'g' => 'a.`id`'] + 'r' => ['j' => ['::account_reputation r ON r.`userId` = a.`id`', true], 's' => ', IFNULL(SUM(r.`amount`), 0) AS "reputation"', 'g' => 'a.`id`'] ); public function getJSGlobals(int $addMask = 0) : array @@ -49,7 +49,7 @@ class UserList extends DBTypeList case 2: if ($this->isPremium()) { - if ($av = DB::Aowow()->selectCell('SELECT `id` FROM ?_account_avatars WHERE `userId` = ?d AND `current` = 1 AND `status` <> ?d', $userId, AvatarMgr::STATUS_REJECTED)) + if ($av = DB::Aowow()->selectCell('SELECT `id` FROM ::account_avatars WHERE `userId` = %i AND `current` = 1 AND `status` <> %i', $userId, AvatarMgr::STATUS_REJECTED)) { $data[$this->curTpl['username']]['avatar'] = $this->curTpl['avatar']; $data[$this->curTpl['username']]['avatarmore'] = $av; diff --git a/includes/dbtypes/worldevent.class.php b/includes/dbtypes/worldevent.class.php index 831b57c3..399d6083 100644 --- a/includes/dbtypes/worldevent.class.php +++ b/includes/dbtypes/worldevent.class.php @@ -10,12 +10,13 @@ class WorldEventList extends DBTypeList { public static int $type = Type::WORLDEVENT; public static string $brickFile = 'event'; - public static string $dataTable = '?_events'; + public static string $dataTable = '::events'; - protected string $queryBase = 'SELECT e.`holidayId`, e.`cuFlags`, e.`startTime`, e.`endTime`, e.`occurence`, e.`length`, e.`requires`, e.`description` AS "nameINT", e.`id` AS "eventId", e.`id` AS "ARRAY_KEY", h.* FROM ?_events e'; + protected string $queryBase = 'SELECT e.`holidayId`, e.`cuFlags`, e.`startTime`, e.`endTime`, e.`occurence`, e.`length`, e.`requires`, e.`description` AS "nameINT", e.`id` AS "eventId", e.`id` AS ARRAY_KEY FROM ::events e'; protected array $queryOpts = array( - 'e' => [['h']], - 'h' => ['j' => ['?_holidays h ON e.`holidayId` = h.`id`', true], 'o' => '-e.`id` ASC'] + 'e' => [['h', 'ic']], + 'h' => ['j' => ['::holidays h ON e.`holidayId` = h.`id`', true], 's' => ', h.*', 'o' => '-e.`id` ASC'], + 'ic' => ['j' => ['::icons ic ON ic.`id` = h.`iconId`', true], 's' => ', ic.`name` AS "iconString"'] ); public function __construct(array $conditions = [], array $miscData = []) @@ -70,9 +71,9 @@ class WorldEventList extends DBTypeList { $row = DB::Aowow()->SelectRow( 'SELECT IFNULL(h.`name_loc0`, e.`description`) AS "name_loc0", h.`name_loc2`, h.`name_loc3`, h.`name_loc4`, h.`name_loc6`, h.`name_loc8` - FROM ?_events e - LEFT JOIN ?_holidays h ON e.`holidayId` = h.`id` - WHERE e.`id` = ?d', + FROM ::events e + LEFT JOIN ::holidays h ON e.`holidayId` = h.`id` + WHERE e.`id` = %i', $id ); @@ -91,7 +92,7 @@ class WorldEventList extends DBTypeList if ($rec < 0 || $date['lastDate'] < time()) return true; - $nIntervals = ceil((time() - $start) / $rec); + $nIntervals = (int)ceil((time() - $end) / $rec); $start += $nIntervals * $rec; $end += $nIntervals * $rec; @@ -105,8 +106,8 @@ class WorldEventList extends DBTypeList { WorldEventList::updateDates($row['_date'] ?? null, $start, $end, $rec); - $row['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : null; - $row['endDate'] = $end ? date(Util::$dateFormatInternal, $end) : null; + $row['startDate'] = $start ? date(Util::$dateFormatInternal, $start) : null; + $row['endDate'] = $end ? date(Util::$dateFormatInternal, $end - 1) : null; $row['rec'] = $rec; unset($row['_date']); @@ -157,9 +158,9 @@ class WorldEventList extends DBTypeList // use string-placeholder for dates // start - $x .= Lang::event('start').Lang::main('colon').'%s<br />'; + $x .= Lang::event('start').'%s<br />'; // end - $x .= Lang::event('end').Lang::main('colon').'%s'; + $x .= Lang::event('end').'%s'; $x .= '</td></tr></table>'; diff --git a/includes/dbtypes/zone.class.php b/includes/dbtypes/zone.class.php index f3bc02d7..668eac6e 100644 --- a/includes/dbtypes/zone.class.php +++ b/includes/dbtypes/zone.class.php @@ -12,9 +12,9 @@ class ZoneList extends DBTypeList public static int $type = Type::ZONE; public static string $brickFile = 'zone'; - public static string $dataTable = '?_zones'; + public static string $dataTable = '::zones'; - protected string $queryBase = 'SELECT z.*, z.`id` AS ARRAY_KEY FROM ?_zones z'; + protected string $queryBase = 'SELECT z.*, z.`id` AS ARRAY_KEY FROM ::zones z'; public function __construct(array $conditions = [], array $miscData = []) { diff --git a/includes/defines.php b/includes/defines.php index e73a84d0..4075085a 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -12,8 +12,8 @@ if (!defined('AOWOW_REVISION')) define('JSON_AOWOW_POWER', JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); define('FILTER_FLAG_STRIP_AOWOW', FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK); -define('TDB_WORLD_MINIMUM_VER', 21101); -define('TDB_WORLD_EXPECTED_VER', 24041); +define('TDB_WORLD_MINIMUM_VER', 25101); +define('TDB_WORLD_EXPECTED_VER', 25101); // as of 01.01.2024 https://www.wowhead.com/wotlk/de/spell=40120/{seo} // https://www.wowhead.com/wotlk/es/search=vuelo @@ -41,6 +41,8 @@ define('CACHE_TYPE_PAGE', 1); define('CACHE_TYPE_TOOLTIP', 2); define('CACHE_TYPE_SEARCH', 3); define('CACHE_TYPE_XML', 4); // only used by items +define('CACHE_TYPE_LIST_PAGE', 5); +define('CACHE_TYPE_DETAIL_PAGE', 6); define('CACHE_MODE_FILECACHE', 0x1); define('CACHE_MODE_MEMCACHED', 0x2); @@ -53,6 +55,7 @@ define ('SC_FLAG_PREFIX', 0x01); define ('SC_FLAG_NO_TIMESTAMP', 0x02); define ('SC_FLAG_APPEND_LOCALE', 0x04); define ('SC_FLAG_LOCALIZED', 0x08); +define ('SC_FLAG_NOCACHE', 0x10); define('ICON_SIZE_TINY', 15); define('ICON_SIZE_SMALL', 18); @@ -128,7 +131,7 @@ define('AUTH_IPBANNED', 4); define('AUTH_ACC_INACTIVE', 5); define('AUTH_INTERNAL_ERR', 6); -define('AUTH_MODE_SELF', 0); // uses ?_accounts +define('AUTH_MODE_SELF', 0); // uses ::accounts define('AUTH_MODE_REALM', 1); // uses given realm-table define('AUTH_MODE_EXTERNAL', 2); // uses external script @@ -372,9 +375,9 @@ define('QUEST_CU_PART_OF_SERIES', 0x0200); define('PROFILER_CU_PUBLISHED', 0x01); define('PROFILER_CU_PINNED', 0x02); -define('PROFILER_CU_DELETED', 0x04); -define('PROFILER_CU_PROFILE', 0x08); -define('PROFILER_CU_NEEDS_RESYNC', 0x10); +// define('PROFILER_CU_DELETED', 0x04); // migrated to separate db cols +// define('PROFILER_CU_PROFILE', 0x08); +// define('PROFILER_CU_NEEDS_RESYNC', 0x10); define('GUIDE_CU_NO_QUICKFACTS', 0x100); // merge with CC_FLAG_* define('GUIDE_CU_NO_RATING', 0x200); @@ -383,20 +386,6 @@ define('MAX_LEVEL', 80); define('MAX_SKILL', 450); define('WOW_BUILD', 12340); -// Loot handles -define('LOOT_FISHING', 'fishing_loot_template'); -define('LOOT_CREATURE', 'creature_loot_template'); -define('LOOT_GAMEOBJECT', 'gameobject_loot_template'); -define('LOOT_ITEM', 'item_loot_template'); -define('LOOT_DISENCHANT', 'disenchant_loot_template'); -define('LOOT_PROSPECTING', 'prospecting_loot_template'); -define('LOOT_MILLING', 'milling_loot_template'); -define('LOOT_PICKPOCKET', 'pickpocketing_loot_template'); -define('LOOT_SKINNING', 'skinning_loot_template'); -define('LOOT_MAIL', 'mail_loot_template'); // used by achievements and quests -define('LOOT_SPELL', 'spell_loot_template'); -define('LOOT_REFERENCE', 'reference_loot_template'); - // Sides define('SIDE_NONE', 0); define('SIDE_ALLIANCE', 1); @@ -492,6 +481,15 @@ define('ITEM_MOD_SPELL_POWER', 45); define('ITEM_MOD_HEALTH_REGEN', 46); define('ITEM_MOD_SPELL_PENETRATION', 47); define('ITEM_MOD_BLOCK_VALUE', 48); +// unknown by 335a client but still used by several item_templates +// define('ITEM_MOD_MASTERY_RATING', 49); +// define('ITEM_MOD_EXTRA_ARMOR', 50); +// define('ITEM_MOD_FIRE_RESISTANCE', 51); +// define('ITEM_MOD_FROST_RESISTANCE', 52); +// define('ITEM_MOD_HOLY_RESISTANCE', 53); +// define('ITEM_MOD_SHADOW_RESISTANCE', 54); +// define('ITEM_MOD_NATURE_RESISTANCE', 55); +// define('ITEM_MOD_ARCANE_RESISTANCE', 56); // Combat Ratings define('CR_WEAPON_SKILL', 0); @@ -598,12 +596,23 @@ define('TEAM_NEUTRAL', 2); // Lock Types define('LOCK_TYPE_ITEM', 1); define('LOCK_TYPE_SKILL', 2); +define('LOCK_TYPE_SPELL', 3); // Lock-Properties (also categorizes GOs) define('LOCK_PROPERTY_FOOTLOCKER', 1); define('LOCK_PROPERTY_HERBALISM', 2); define('LOCK_PROPERTY_MINING', 3); +// FactionFlags +define('FACTION_FLAG_VISIBLE', 0x01); +define('FACTION_FLAG_AT_WAR', 0x02); +define('FACTION_FLAG_HIDDEN', 0x04); +define('FACTION_FLAG_INVISIBLE_FORCED', 0x08); +define('FACTION_FLAG_PEACE_FORCED', 0x10); +define('FACTION_FLAG_INACTIVE', 0x20); +define('FACTION_FLAG_RIVAL', 0x40); +define('FACTION_FLAG_SPECIAL', 0x80); + // Creature define('NPC_TYPEFLAG_TAMEABLE', 0x00000001); define('NPC_TYPEFLAG_VISIBLE_TO_GHOSTS', 0x00000002); @@ -669,6 +678,7 @@ define('NPC_FLAG_STABLE_MASTER', 0x00400000); define('NPC_FLAG_GUILD_BANK', 0x00800000); define('NPC_FLAG_SPELLCLICK', 0x01000000); define('NPC_FLAG_MAILBOX', 0x04000000); +define('NPC_FLAG_VALIDATE', 0x05FFFFF3); define('CREATURE_FLAG_EXTRA_INSTANCE_BIND', 0x00000001); // creature kill binds instance to killer and killer's group define('CREATURE_FLAG_EXTRA_CIVILIAN', 0x00000002); // creature does not aggro (ignore faction/reputation hostility) @@ -708,7 +718,7 @@ define('UNIT_FLAG_IMMUNE_TO_PC', 0x00000100); // disables combat/a define('UNIT_FLAG_IMMUNE_TO_NPC', 0x00000200); // disables combat/assistance with NonPlayerCharacters (NPC) define('UNIT_FLAG_LOOTING', 0x00000400); // Loot animation define('UNIT_FLAG_PET_IN_COMBAT', 0x00000800); // In combat? 2.0.8 -define('UNIT_FLAG_PVP', 0x00001000); // Changed in 3.0.3 +define('UNIT_FLAG_PVP_ENABLING', 0x00001000); // Changed in 3.0.3 define('UNIT_FLAG_SILENCED', 0x00002000); // Can't cast spells define('UNIT_FLAG_CANNOT_SWIM', 0x00004000); // 2.0.8 define('UNIT_FLAG_UNK_15', 0x00008000); // Only Swim ('OnlySwim' from UnitFlags.cs in WPP) @@ -728,6 +738,7 @@ define('UNIT_FLAG_UNK_28', 0x10000000); // (PreventKneelingW define('UNIT_FLAG_UNK_29', 0x20000000); // Used in Feign Death spell or NPC will play dead. (PreventEmotes) define('UNIT_FLAG_SHEATHE', 0x40000000); // define('UNIT_FLAG_UNK_31', 0x80000000); // +define('UNIT_FLAG_VALIDATE', 0x7FFFFFFF); // define('UNIT_FLAG2_FEIGN_DEATH', 0x00000001); // define('UNIT_FLAG2_UNK1', 0x00000002); // Hide unit model (show only player equip) @@ -747,6 +758,7 @@ define('UNIT_FLAG2_DISABLE_TURN', 0x00008000); // define('UNIT_FLAG2_UNK2', 0x00010000); // define('UNIT_FLAG2_PLAY_DEATH_ANIM', 0x00020000); // Plays special death animation upon death define('UNIT_FLAG2_ALLOW_CHEAT_SPELLS', 0x00040000); // allows casting spells with AttributesEx7 & SPELL_ATTR7_IS_CHEAT_SPELL +define('UNIT_FLAG2_VALIDATE', 0x0006FDFF); // // UNIT_FIELD_BYTES_1 - idx 0 (UnitStandStateType) define('UNIT_STAND_STATE_STAND', 0); @@ -768,11 +780,16 @@ define('UNIT_VIS_FLAGS_UNK4', 0x08); define('UNIT_VIS_FLAGS_UNK5', 0x10); // UNIT_FIELD_BYTES_1 - idx 3 (UnitAnimTier) -define('UNIT_BYTE1_ANIM_TIER_GROUND', 0); -define('UNIT_BYTE1_ANIM_TIER_SWIM', 1); -define('UNIT_BYTE1_ANIM_TIER_HOVER', 2); -define('UNIT_BYTE1_ANIM_TIER_FLY', 3); -define('UNIT_BYTE1_ANIM_TIER_SUMBERGED', 4); +define('UNIT_ANIM_TIER_GROUND', 0); +define('UNIT_ANIM_TIER_SWIM', 1); +define('UNIT_ANIM_TIER_HOVER', 2); +define('UNIT_ANIM_TIER_FLY', 3); +define('UNIT_ANIM_TIER_SUMBERGED', 4); + +// UNIT_FIELD_BYTES_2 - idx 1 (UnitPvPStateFlags) +define('UNIT_PVPSTATE_FLAG_PVP', 0x01); +define('UNIT_PVPSTATE_FLAG_FFA_PVP', 0x04); // not expected to be on NPCs, buuuut... +define('UNIT_PVPSTATE_FLAG_SANCTUARY', 0x08); define('UNIT_DYNFLAG_LOOTABLE', 0x01); // define('UNIT_DYNFLAG_TRACK_UNIT', 0x02); // Creature's location will be seen as a small dot in the minimap @@ -782,30 +799,40 @@ define('UNIT_DYNFLAG_SPECIALINFO', 0x10); // define('UNIT_DYNFLAG_DEAD', 0x20); // Makes the creature appear dead (this DOES NOT make the creature's name grey or not attack players). define('UNIT_DYNFLAG_REFER_A_FRIEND', 0x40); // define('UNIT_DYNFLAG_TAPPED_BY_ALL_THREAT_LIST', 0x80); // Lua_UnitIsTappedByAllThreatList +define('UNIT_DYNFLAG_VALIDATE', 0xFF); // define('PET_TALENT_TYPE_FEROCITY', 0); define('PET_TALENT_TYPE_TENACITY', 1); define('PET_TALENT_TYPE_CUNNING', 2); // quest -define('QUEST_FLAG_STAY_ALIVE', 0x00001); -define('QUEST_FLAG_PARTY_ACCEPT', 0x00002); -define('QUEST_FLAG_EXPLORATION', 0x00004); -define('QUEST_FLAG_SHARABLE', 0x00008); -define('QUEST_FLAG_AUTO_REWARDED', 0x00400); -define('QUEST_FLAG_DAILY', 0x01000); -define('QUEST_FLAG_REPEATABLE', 0x02000); -define('QUEST_FLAG_UNAVAILABLE', 0x04000); -define('QUEST_FLAG_WEEKLY', 0x08000); -define('QUEST_FLAG_AUTO_COMPLETE', 0x10000); -define('QUEST_FLAG_AUTO_ACCEPT', 0x80000); +define('QUEST_FLAG_STAY_ALIVE', 0x00001); +define('QUEST_FLAG_PARTY_ACCEPT', 0x00002); +define('QUEST_FLAG_EXPLORATION', 0x00004); +define('QUEST_FLAG_SHARABLE', 0x00008); +define('QUEST_FLAG_HAS_CONDITION', 0x00010); // TC: Not used currently +define('QUEST_FLAG_HIDE_REWARD_POI', 0x00020); // TC: Not used currently: Unsure of content +define('QUEST_FLAG_RAID', 0x00040); +define('QUEST_FLAG_TBC', 0x00080); +define('QUEST_FLAG_NO_MONEY_FROM_XP', 0x00100); +define('QUEST_FLAG_HIDDEN_REWARDS', 0x00200); +define('QUEST_FLAG_TRACKING', 0x00400); // TC: These quests are automatically rewarded on quest complete and they will never appear in quest log client side. +define('QUEST_FLAG_DEPRECATE_REPUTATION', 0x00800); // TC: Not used currently +define('QUEST_FLAG_DAILY', 0x01000); +define('QUEST_FLAG_FLAGS_PVP', 0x02000); +define('QUEST_FLAG_UNAVAILABLE', 0x04000); +define('QUEST_FLAG_WEEKLY', 0x08000); +define('QUEST_FLAG_AUTO_COMPLETE', 0x10000); +define('QUEST_FLAG_DISPLAY_ITEM_IN_TRACKER', 0x20000); // TC: Displays usable item in quest tracker +define('QUEST_FLAG_OBJ_TEXT', 0x40000); // TC: use Objective text as Complete text +define('QUEST_FLAG_AUTO_ACCEPT', 0x80000); -define('QUEST_FLAG_SPECIAL_REPEATABLE', 0x01); -define('QUEST_FLAG_SPECIAL_EXT_COMPLETE', 0x02); -define('QUEST_FLAG_SPECIAL_AUTO_ACCEPT', 0x04); -define('QUEST_FLAG_SPECIAL_DUNGEON_FINDER', 0x08); -define('QUEST_FLAG_SPECIAL_MONTHLY', 0x10); -define('QUEST_FLAG_SPECIAL_SPELLCAST', 0x20); // not documented in wiki! :[ +define('QUEST_FLAG_SPECIAL_REPEATABLE', 0x01); +define('QUEST_FLAG_SPECIAL_EXT_COMPLETE', 0x02); +define('QUEST_FLAG_SPECIAL_AUTO_ACCEPT', 0x04); +define('QUEST_FLAG_SPECIAL_DUNGEON_FINDER', 0x08); +define('QUEST_FLAG_SPECIAL_MONTHLY', 0x10); +define('QUEST_FLAG_SPECIAL_SPELLCAST', 0x20); // not documented in wiki! :[ // GameObject define('OBJECT_DOOR', 0); @@ -851,9 +878,11 @@ define('GO_FLAG_INTERACT_COND', 0x0004); // Untargetable, can define('GO_FLAG_TRANSPORT', 0x0008); // Gameobject can transport (boat, elevator, car) define('GO_FLAG_NOT_SELECTABLE', 0x0010); // Not selectable (Not even in GM-mode) define('GO_FLAG_NODESPAWN', 0x0020); // Never despawns. Typical for gameobjects with on/off state (doors for example) -define('GO_FLAG_TRIGGERED', 0x0040); // typically, summoned objects. Triggered by spell or other events +define('GO_FLAG_AI_OBSTACLE', 0x0040); // makes the client register the object in something called AIObstacleMgr, unknown what it does +define('GO_FLAG_FREEZE_ANIMATION',0x0080); // define('GO_FLAG_DAMAGED', 0x0200); // Gameobject has been siege damaged define('GO_FLAG_DESTROYED', 0x0400); // Gameobject has been destroyed +define('GO_FLAG_VALIDATE', 0x06FF); // define('GO_STATE_ACTIVE', 0); // show in world as used and not reset (closed door open) define('GO_STATE_READY', 1); // show in world as ready (closed door close) @@ -1058,7 +1087,7 @@ define('ENCHANTMENT_TYPE_PRISMATIC_SOCKET', 8); // define('ENCHANT_CONDITION_EQUAL_VALUE', ?); define('ENCHANT_CONDITION_LESS_VALUE', 2); define('ENCHANT_CONDITION_MORE_COMPARE', 3); -// define('ENCHANT_CONDITION_MORE_EQUAL_COMPARE', ?); +// define('ENCHANT_CONDITION_MORE_EQUAL_COMPARE', %s); define('ENCHANT_CONDITION_MORE_VALUE', 5); // define('ENCHANT_CONDITION_NOT_EQUAL_COMPARE', ?); // define('ENCHANT_CONDITION_NOT_EQUAL_VALUE', ?); diff --git a/includes/game/chrrace.class.php b/includes/game/chrrace.class.php index aab28ed7..70e481c1 100644 --- a/includes/game/chrrace.class.php +++ b/includes/game/chrrace.class.php @@ -8,20 +8,31 @@ if (!defined('AOWOW_REVISION')) enum ChrRace : int { - case HUMAN = 1; - case ORC = 2; - case DWARF = 3; - case NIGHTELF = 4; - case UNDEAD = 5; - case TAUREN = 6; - case GNOME = 7; - case TROLL = 8; - case BLOODELF = 10; - case DRAENEI = 11; + case HUMAN = 1; + case ORC = 2; + case DWARF = 3; + case NIGHTELF = 4; + case UNDEAD = 5; + case TAUREN = 6; + case GNOME = 7; + case TROLL = 8; + case GOBLIN = 9; + case BLOODELF = 10; + case DRAENEI = 11; + case FEL_ORC = 12; + case NAGA = 13; + case BROKEN = 14; + case SKELETON = 15; + case VRYKUL = 16; + case TUSKARR = 17; + case FOREST_TROLL = 18; + case TAUNKA = 19; + case NORTHREND_SKELETON = 20; + case ICE_TROLL = 21; - public const MASK_ALLIANCE = 0x44D; - public const MASK_HORDE = 0x2B2; - public const MASK_ALL = 0x6FF; + public const MASK_ALLIANCE = 0x44D; // HUMAN, DWARF, NIGHTELF, GNOME, DRAENEI + public const MASK_HORDE = 0x2B2; // ORC, UNDEAD, TAUREN, TROLL, BLOODELF + public const MASK_ALL = self::MASK_ALLIANCE | self::MASK_HORDE; public function matches(int $raceMask) : bool { @@ -75,12 +86,13 @@ enum ChrRace : int self::ORC => 'orc', self::DWARF => 'dwarf', self::NIGHTELF => 'nightelf', - self::UNDEAD => 'undead', + self::UNDEAD => 'scourge', self::TAUREN => 'tauren', self::GNOME => 'gnome', self::TROLL => 'troll', self::BLOODELF => 'bloodelf', - self::DRAENEI => 'draenei' + self::DRAENEI => 'draenei', + default => '' }; } diff --git a/includes/game/chrstatistics.php b/includes/game/chrstatistics.php index c1819501..1777e31c 100644 --- a/includes/game/chrstatistics.php +++ b/includes/game/chrstatistics.php @@ -116,6 +116,7 @@ abstract class Stat // based on g_statTo public const FLAG_PROFILER = 0x04; // stat used in profiler only public const FLAG_LVL_SCALING = 0x08; // rating effectivenes scales with level public const FLAG_FLOAT_VALUE = 0x10; // not an int + public const FLAG_NO_WEIGHT = 0x20; // for item summary and filter .. basically any fi_filters.items of type: num thats not excluded by noweights: 1 is weightable public const IDX_JSON_STR = 0; public const IDX_ITEM_MOD = 1; // granted by items @@ -123,7 +124,7 @@ abstract class Stat // based on g_statTo public const IDX_FILTER_CR_ID = 3; // also references listview cols public const IDX_FLAGS = 4; - private static /* array */ $data = array( + private static array $data = array( self::HEALTH => ['health', ITEM_MOD_HEALTH, null, 115, self::FLAG_ITEM], self::MANA => ['mana', ITEM_MOD_MANA, null, 116, self::FLAG_ITEM], self::AGILITY => ['agi', ITEM_MOD_AGILITY, null, 21, self::FLAG_ITEM], @@ -172,14 +173,14 @@ abstract class Stat // based on g_statTo self::HEALTH_REGENERATION => ['healthrgn', ITEM_MOD_HEALTH_REGEN, null, 60, self::FLAG_ITEM], self::SPELL_PENETRATION => ['splpen', ITEM_MOD_SPELL_PENETRATION, null, 94, self::FLAG_ITEM], self::BLOCK => ['block', ITEM_MOD_BLOCK_VALUE, null, 43, self::FLAG_ITEM], - // self::MASTERY_RTG => ['mastrtng', null, CR_MASTERY, null, self::FLAG_NONE], - self::ARMOR => ['armor', null, null, 41, self::FLAG_ITEM], - self::FIRE_RESISTANCE => ['firres', null, null, 26, self::FLAG_ITEM], - self::FROST_RESISTANCE => ['frores', null, null, 28, self::FLAG_ITEM], - self::HOLY_RESISTANCE => ['holres', null, null, 30, self::FLAG_ITEM], - self::SHADOW_RESISTANCE => ['shares', null, null, 29, self::FLAG_ITEM], - self::NATURE_RESISTANCE => ['natres', null, null, 27, self::FLAG_ITEM], - self::ARCANE_RESISTANCE => ['arcres', null, null, 25, self::FLAG_ITEM], + // self::MASTERY_RTG => ['mastrtng', ITEM_MOD_MASTERY_RATING, CR_MASTERY, null, self::FLAG_NONE], + self::ARMOR => ['armor', null,/*ITEM_MOD_EXTRA_ARMOR */null, 41, self::FLAG_ITEM], + self::FIRE_RESISTANCE => ['firres', null,/*ITEM_MOD_FIRE_RESISTANCE */null, 26, self::FLAG_ITEM], + self::FROST_RESISTANCE => ['frores', null,/*ITEM_MOD_FROST_RESISTANCE */null, 28, self::FLAG_ITEM], + self::HOLY_RESISTANCE => ['holres', null,/*ITEM_MOD_HOLY_RESISTANCE */null, 30, self::FLAG_ITEM], + self::SHADOW_RESISTANCE => ['shares', null,/*ITEM_MOD_SHADOW_RESISTANCE*/null, 29, self::FLAG_ITEM], + self::NATURE_RESISTANCE => ['natres', null,/*ITEM_MOD_NATURE_RESISTANCE*/null, 27, self::FLAG_ITEM], + self::ARCANE_RESISTANCE => ['arcres', null,/*ITEM_MOD_ARCANE_RESISTANCE*/null, 25, self::FLAG_ITEM], self::FIRE_SPELL_POWER => ['firsplpwr', null, null, 53, self::FLAG_ITEM], self::FROST_SPELL_POWER => ['frosplpwr', null, null, 54, self::FLAG_ITEM], self::HOLY_SPELL_POWER => ['holsplpwr', null, null, 55, self::FLAG_ITEM], @@ -188,7 +189,7 @@ abstract class Stat // based on g_statTo self::ARCANE_SPELL_POWER => ['arcsplpwr', null, null, 52, self::FLAG_ITEM], // v not part of g_statToJson v self::WEAPON_DAMAGE => ['dmg', null, null, null, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE], - self::WEAPON_DAMAGE_TYPE => ['damagetype', null, null, 35, self::FLAG_SERVERSIDE], + self::WEAPON_DAMAGE_TYPE => ['damagetype', null, null, 35, self::FLAG_SERVERSIDE | self::FLAG_NO_WEIGHT], self::WEAPON_DAMAGE_MIN => ['dmgmin1', null, null, 33, self::FLAG_SERVERSIDE], self::WEAPON_DAMAGE_MAX => ['dmgmax1', null, null, 34, self::FLAG_SERVERSIDE], self::WEAPON_SPEED => ['speed', null, null, 36, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE], @@ -201,7 +202,7 @@ abstract class Stat // based on g_statTo self::RANGED_DAMAGE_MAX => ['rgddmgmax', null, null, 140, self::FLAG_SERVERSIDE], self::RANGED_SPEED => ['rgdspeed', null, null, 141, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE], self::RANGED_DPS => ['rgddps', null, null, 138, self::FLAG_SERVERSIDE | self::FLAG_FLOAT_VALUE | self::FLAG_PROFILER], - self::EXTRA_SOCKETS => ['nsockets', null, null, 100, self::FLAG_SERVERSIDE], + self::EXTRA_SOCKETS => ['nsockets', null, null, 100, self::FLAG_SERVERSIDE | self::FLAG_NO_WEIGHT], self::ARMOR_BONUS => ['armorbonus', null, null, 109, self::FLAG_SERVERSIDE], self::MELEE_ATTACK_POWER => ['mleatkpwr', null, null, 37, self::FLAG_SERVERSIDE | self::FLAG_PROFILER], // v Profiler only v @@ -248,6 +249,16 @@ abstract class Stat // based on g_statTo return !(self::$data[$stat][self::IDX_FLAGS] & self::FLAG_LVL_SCALING); } + public static function getWeightJson(string|int $jsonOrCriteriaId) : string + { + if (is_numeric($jsonOrCriteriaId)) + $row = array_find(self::$data, fn($x) => $x[self::IDX_FILTER_CR_ID] == $jsonOrCriteriaId); + else + $row = array_find(self::$data, fn($x) => $x[self::IDX_JSON_STR] == $jsonOrCriteriaId); + + return $row && $row[self::IDX_FILTER_CR_ID] && !($row[self::IDX_FLAGS] & self::FLAG_NO_WEIGHT) ? $row[self::IDX_JSON_STR] : ''; + } + public static function getRatingPctFactor(int $stat) : float { // Note: this makes the weapon skill related combat ratings inaccessible. Is this relevant..? @@ -315,22 +326,27 @@ abstract class Stat // based on g_statTo return $x; } - public static function getIndexFrom(int $idx, string $match) : int + public static function getIndexFrom(int $idx, string $search) : int { - $i = array_search($match, array_column(self::$data, $idx)); - if ($i === false) - return 0; - - return array_keys(self::$data)[$i]; + return array_find_key(self::$data, fn($x) => $x[$idx] == $search) ?: 0; } } class StatsContainer implements \Countable { - private $store = []; + private array $store = []; - private $relSpells = []; - private $relEnchantments = []; + private array $relSpells = []; + private array $relEnchantments = []; + + private static array $combinedSpellStats = array ( + Stat::ATTACK_POWER => [Stat::RANGED_ATTACK_POWER, Stat::MELEE_ATTACK_POWER], + Stat::SPELL_POWER => [Stat::DAMAGE_SPELL_POWER, Stat::HEALING_SPELL_POWER], + // combat ratings below could be merged like this, but easier to handle as they are already in the same bitmask of the same spell effect + // Stat::HIT_RTG => [Stat::MELEE_HIT_RTG, Stat::RANGED_HIT_RTG, Stat::SPELL_HIT_RTG], + // Stat::CRIT_RTG => [Stat::MELEE_CRIT_TAKEN_RTG, Stat::RANGED_CRIT_RTG, Stat::SPELL_CRIT_RTG], + // Stat::RESILIENCE_RTG => [Stat::MELEE_CRIT_RTG, Stat::RANGED_CRIT_TAKEN_RTG, Stat::SPELL_CRIT_TAKEN_RTG] + ); public function __construct(array $relSpells = [], array $relEnchantments = []) { @@ -400,13 +416,14 @@ class StatsContainer implements \Countable return $this; } - public function fromSpell(array $spell) : self + public function fromSpell(array $spell, bool $onlyFoodBuff = false) : self { if (!$spell) return $this; - // if spells grant an equal, non-zero amount of SPELL_DAMAGE and SPELL_HEALING, combine them to SPELL_POWER - // this probably does not affect enchantments + if ($onlyFoodBuff && !($spell['attributes2'] & SPELL_ATTR2_FOOD_BUFF)) + return $this; + $tmpStore = []; for ($i = 1; $i <= 3; $i++) @@ -418,16 +435,30 @@ class StatsContainer implements \Countable if (in_array($eff, SpellList::EFFECTS_ENCHANTMENT) && ($relE = $this->relE($mVal))) $this->fromEnchantment($relE); + else if ($aura == SPELL_AURA_PERIODIC_TRIGGER_SPELL && ($ts = $spell['effect'.$i.'TriggerSpell'])) + { + if ($relS = $this->relS($ts)) + $this->fromSpell($relS, true); + } else foreach ($this->convertSpellEffect($aura, $mVal, $amt) as $idx) Util::arraySumByKey($tmpStore, [$idx => $amt]); } - if (!empty($tmpStore[Stat::HEALING_SPELL_POWER]) && !empty($tmpStore[Stat::DAMAGE_SPELL_POWER]) && $tmpStore[Stat::HEALING_SPELL_POWER] == $tmpStore[Stat::DAMAGE_SPELL_POWER]) + foreach (self::$combinedSpellStats as $combined => $stats) { - Util::arraySumByKey($tmpStore, [Stat::SPELL_POWER => $tmpStore[Stat::HEALING_SPELL_POWER]]); - unset($tmpStore[Stat::HEALING_SPELL_POWER]); - unset($tmpStore[Stat::DAMAGE_SPELL_POWER]); + for ($i = 0; $i < count($stats); $i++) + { + if (empty($tmpStore[$stats[$i]])) + continue 2; + + if ($i && $tmpStore[$stats[$i]] != $tmpStore[$stats[$i - 1]]) + continue 2; + } + + Util::arraySumByKey($tmpStore, [$combined => $tmpStore[$stats[0]]]); + foreach ($stats as $stat) + unset($tmpStore[$stat]); } Util::arraySumByKey($this->store, $tmpStore); @@ -459,7 +490,7 @@ class StatsContainer implements \Countable public function fromDB(int $type, int $typeId, int $fieldFlags = Stat::FLAG_NONE) : self { - foreach (DB::Aowow()->selectRow('SELECT (?#) FROM ?_item_stats WHERE `type` = ?d AND `typeId` = ?d', Stat::getJsonStringsFor($fieldFlags ?: (Stat::FLAG_ITEM | Stat::FLAG_SERVERSIDE)), $type, $typeId) as $key => $amt) + foreach (DB::Aowow()->selectRow('SELECT (%n) FROM ::item_stats WHERE `type` = %i AND `typeId` = %i', Stat::getJsonStringsFor($fieldFlags ?: (Stat::FLAG_ITEM | Stat::FLAG_SERVERSIDE)), $type, $typeId) as $key => $amt) { if ($amt === null) continue; @@ -519,18 +550,12 @@ class StatsContainer implements \Countable private function relE(int $enchantmentId) : array { - if ($enchantmentId <= 0 || !isset($this->relEnchantments[$enchantmentId])) - return []; - - return $this->relEnchantments[$enchantmentId]; + return $this->relEnchantments[$enchantmentId] ?? []; } private function relS(int $spellId) : array { - if ($spellId <= 0 || !isset($this->relSpells[$spellId])) - return []; - - return $this->relSpells[$spellId]; + return $this->relSpells[$spellId] ?? []; } private static function convertEnchantment(int $type, int $object) : array @@ -546,22 +571,17 @@ class StatsContainer implements \Countable case ENCHANTMENT_TYPE_STAT: // ITEM_MOD_* return [Stat::getIndexFrom(Stat::IDX_ITEM_MOD, $object)]; case ENCHANTMENT_TYPE_RESISTANCE: - if ($object == SPELL_SCHOOL_NORMAL) - return [Stat::ARMOR]; - if ($object == SPELL_SCHOOL_HOLY) - return [Stat::HOLY_RESISTANCE]; - if ($object == SPELL_SCHOOL_FIRE) - return [Stat::FIRE_RESISTANCE]; - if ($object == SPELL_SCHOOL_NATURE) - return [Stat::NATURE_RESISTANCE]; - if ($object == SPELL_SCHOOL_FROST) - return [Stat::FROST_RESISTANCE]; - if ($object == SPELL_SCHOOL_SHADOW) - return [Stat::SHADOW_RESISTANCE]; - if ($object == SPELL_SCHOOL_ARCANE) - return [Stat::ARCANE_RESISTANCE]; - - return []; + return match ($object) + { + SPELL_SCHOOL_NORMAL => [Stat::ARMOR], + SPELL_SCHOOL_HOLY => [Stat::HOLY_RESISTANCE], + SPELL_SCHOOL_FIRE => [Stat::FIRE_RESISTANCE], + SPELL_SCHOOL_NATURE => [Stat::NATURE_RESISTANCE], + SPELL_SCHOOL_FROST => [Stat::FROST_RESISTANCE], + SPELL_SCHOOL_SHADOW => [Stat::SHADOW_RESISTANCE], + SPELL_SCHOOL_ARCANE => [Stat::ARCANE_RESISTANCE], + default => [] + }; case ENCHANTMENT_TYPE_EQUIP_SPELL: // handled one level up case ENCHANTMENT_TYPE_COMBAT_SPELL: // we do not average effects, so skip case ENCHANTMENT_TYPE_USE_SPELL: @@ -582,7 +602,6 @@ class StatsContainer implements \Countable if (($mask & $critMask) == $critMask) return [Stat::CRIT_RTG]; // generic crit rating - $takentMask = (1 << CR_CRIT_TAKEN_MELEE) | (1 << CR_CRIT_TAKEN_RANGED) | (1 << CR_CRIT_TAKEN_SPELL); if (($mask & $takentMask) == $takentMask) return [Stat::RESILIENCE_RTG]; // resilience @@ -602,20 +621,15 @@ class StatsContainer implements \Countable switch ($auraId) { case SPELL_AURA_MOD_STAT: - if ($miscValue < 0) // all stats - return [Stat::AGILITY, Stat::STRENGTH, Stat::INTELLECT, Stat::SPIRIT, Stat::STAMINA]; - if ($miscValue == STAT_STRENGTH) // one stat - return [Stat::STRENGTH]; - if ($miscValue == STAT_AGILITY) - return [Stat::AGILITY]; - if ($miscValue == STAT_STAMINA) - return [Stat::STAMINA]; - if ($miscValue == STAT_INTELLECT) - return [Stat::INTELLECT]; - if ($miscValue == STAT_SPIRIT) - return [Stat::SPIRIT]; - - return []; // one bullshit + return match ($miscValue) + { + STAT_STRENGTH => [Stat::STRENGTH], + STAT_AGILITY => [Stat::AGILITY], + STAT_STAMINA => [Stat::STAMINA], + STAT_INTELLECT => [Stat::INTELLECT], + STAT_SPIRIT => [Stat::SPIRIT], + default => $miscValue < 0 ? [Stat::AGILITY, Stat::STRENGTH, Stat::INTELLECT, Stat::SPIRIT, Stat::STAMINA] : [] + }; case SPELL_AURA_MOD_INCREASE_HEALTH: case SPELL_AURA_MOD_INCREASE_HEALTH_NONSTACK: case SPELL_AURA_MOD_INCREASE_HEALTH_2: @@ -629,27 +643,16 @@ class StatsContainer implements \Countable if ($miscValue == SPELL_MAGIC_SCHOOLS) return [Stat::DAMAGE_SPELL_POWER]; - // HolySpellpower (deprecated; still used in randomproperties) if ($miscValue & (1 << SPELL_SCHOOL_HOLY)) $stats[] = Stat::HOLY_SPELL_POWER; - - // FireSpellpower (deprecated; still used in randomproperties) if ($miscValue & (1 << SPELL_SCHOOL_FIRE)) $stats[] = Stat::FIRE_SPELL_POWER; - - // NatureSpellpower (deprecated; still used in randomproperties) if ($miscValue & (1 << SPELL_SCHOOL_NATURE)) $stats[] = Stat::NATURE_SPELL_POWER; - - // FrostSpellpower (deprecated; still used in randomproperties) if ($miscValue & (1 << SPELL_SCHOOL_FROST)) $stats[] = Stat::FROST_SPELL_POWER; - - // ShadowSpellpower (deprecated; still used in randomproperties) if ($miscValue & (1 << SPELL_SCHOOL_SHADOW)) $stats[] = Stat::SHADOW_SPELL_POWER; - - // ArcaneSpellpower (deprecated; still used in randomproperties) if ($miscValue & (1 << SPELL_SCHOOL_ARCANE)) $stats[] = Stat::ARCANE_SPELL_POWER; @@ -657,16 +660,14 @@ class StatsContainer implements \Countable case SPELL_AURA_MOD_HEALING_DONE: // not as a mask.. return [Stat::HEALING_SPELL_POWER]; case SPELL_AURA_MOD_INCREASE_ENERGY: // MiscVal:type see defined Powers only energy/mana in use - if ($miscValue == POWER_ENERGY) - return [Stat::ENERGY]; - if ($miscValue == POWER_RAGE) - return [Stat::RAGE]; - if ($miscValue == POWER_MANA) - return [Stat::MANA]; - if ($miscValue == POWER_RUNIC_POWER) - return [Stat::RUNIC_POWER]; - - return []; + return match ($miscValue) + { + POWER_ENERGY => [Stat::ENERGY], + POWER_RAGE => [Stat::RAGE], + POWER_MANA => [Stat::MANA], + POWER_RUNIC_POWER => [Stat::RUNIC_POWER], + default => [] + }; case SPELL_AURA_MOD_RATING: case SPELL_AURA_MOD_RATING_FROM_STAT: if ($stat = self::convertCombatRating($miscValue)) @@ -703,7 +704,7 @@ class StatsContainer implements \Countable case SPELL_AURA_MOD_POWER_REGEN: // mp5 return [Stat::MANA_REGENERATION]; case SPELL_AURA_MOD_ATTACK_POWER: - return [Stat::ATTACK_POWER/*, Stat::RANGED_ATTACK_POWER*/]; + return [Stat::MELEE_ATTACK_POWER]; case SPELL_AURA_MOD_RANGED_ATTACK_POWER: return [Stat::RANGED_ATTACK_POWER]; case SPELL_AURA_MOD_SHIELD_BLOCKVALUE: diff --git a/includes/game/loot.class.php b/includes/game/loot.class.php deleted file mode 100644 index 1102fd24..00000000 --- a/includes/game/loot.class.php +++ /dev/null @@ -1,693 +0,0 @@ -<?php - -namespace Aowow; - -if (!defined('AOWOW_REVISION')) - die('illegal access'); - - -/* from TC wiki - fishing_loot_template no relation entry is linked with ID of the fishing zone or area - creature_loot_template entry many <- many creature_template lootid - gameobject_loot_template entry many <- many gameobject_template data1 Only GO type 3 (CHEST) or 25 (FISHINGHOLE) - item_loot_template entry many <- one item_template entry - disenchant_loot_template entry many <- many item_template DisenchantID - prospecting_loot_template entry many <- one item_template entry - milling_loot_template entry many <- one item_template entry - pickpocketing_loot_template entry many <- many creature_template pickpocketloot - skinning_loot_template entry many <- many creature_template skinloot Can also store minable/herbable items gathered from creatures - quest_mail_loot_template entry quest_template RewMailTemplateId - reference_loot_template entry many <- many *_loot_template reference -*/ - -class Loot -{ - public $jsGlobals = []; - public $extraCols = []; - - private $entry = 0; // depending on the lookup itemId oder templateId - private $results = []; - private $chanceMods = []; - private $lootTemplates = array( - LOOT_REFERENCE, // internal - LOOT_ITEM, // item - LOOT_DISENCHANT, // item - LOOT_PROSPECTING, // item - LOOT_MILLING, // item - LOOT_CREATURE, // npc - LOOT_PICKPOCKET, // npc - LOOT_SKINNING, // npc (see its flags for mining, herbing, salvaging or actual skinning) - LOOT_FISHING, // zone - LOOT_GAMEOBJECT, // object (see its lockType for mining, herbing, fishing or generic looting) - LOOT_MAIL, // quest + achievement - LOOT_SPELL // spell - ); - - public function &iterate() : iterable - { - reset($this->results); - - foreach ($this->results as $k => [, $tabData]) - if ($tabData['data']) // only yield tabs with content - yield $k => $this->results[$k]; - } - - public function getResult() : array - { - return $this->results; - } - - private function createStack(array $l) : string // issue: TC always has an equal distribution between min/max - { - if (empty($l['min']) || empty($l['max']) || $l['max'] <= $l['min']) - return ''; - - $stack = []; - for ($i = $l['min']; $i <= $l['max']; $i++) - $stack[$i] = round(100 / (1 + $l['max'] - $l['min']), 3); - - // yes, it wants a string .. how weired is that.. - return json_encode($stack, JSON_NUMERIC_CHECK); // do not replace with Util::toJSON ! - } - - private function storeJSGlobals(array $data) : void - { - foreach ($data as $type => $jsData) - { - foreach ($jsData as $k => $v) - { - // was already set at some point with full data - if (isset($this->jsGlobals[$type][$k]) && is_array($this->jsGlobals[$type][$k])) - continue; - - $this->jsGlobals[$type][$k] = $v; - } - } - } - - private function calcChance(array $refs, array $parents = []) : array - { - $retData = []; - $retKeys = []; - - foreach ($refs as $rId => $ref) - { - // check for possible database inconsistencies - if (!$ref['chance'] && !$ref['isGrouped']) - trigger_error('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!', E_USER_WARNING); - - if ($ref['isGrouped'] && $ref['sumChance'] > 100) - trigger_error('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!', E_USER_WARNING); - - if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance']) - trigger_error('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!', E_USER_WARNING); - - $chance = abs($ref['chance'] ?: (100 - $ref['sumChance']) / $ref['nZeroItems']) / 100; - - // apply inherited chanceMods - if (isset($this->chanceMods[$ref['item']])) - { - $chance *= $this->chanceMods[$ref['item']][0]; - $chance = 1 - pow(1 - $chance, $this->chanceMods[$ref['item']][1]); - } - - // save chance for parent-ref - $this->chanceMods[$rId] = [$chance, $ref['multiplier']]; - - // refTemplate doesn't point to a new ref -> we are done - if (!in_array($rId, $parents)) - { - $data = array( - 'percent' => $chance, - 'stack' => [$ref['min'], $ref['max']], - 'count' => 1 // ..and one for the sort script - ); - - if ($_ = self::createStack($ref)) - $data['pctstack'] = $_; - - // sort highest chances first - $i = 0; - for (; $i < count($retData); $i++) - if ($retData[$i]['percent'] < $data['percent']) - break; - - array_splice($retData, $i, 0, [$data]); - array_splice($retKeys, $i, 0, [$rId]); - } - } - - return array_combine($retKeys, $retData); - } - - private function getByContainerRecursive(string $tableName, int $lootId, array &$handledRefs, int $groupId = 0, float $baseChance = 1.0) : array - { - $loot = []; - $rawItems = []; - - if (!$tableName || !$lootId) - return [null, null]; - - $rows = DB::World()->select('SELECT * FROM ?# WHERE entry = ?d{ AND groupid = ?d}', $tableName, $lootId, $groupId ?: DBSIMPLE_SKIP); - if (!$rows) - return [null, null]; - - $groupChances = []; - $nGroupEquals = []; - $cnd = new Conditions(); - foreach ($rows as $entry) - { - $set = array( - 'quest' => $entry['QuestRequired'], - 'group' => $entry['GroupId'], - 'parentRef' => $tableName == LOOT_REFERENCE ? $lootId : 0, - 'realChanceMod' => $baseChance, - 'groupChance' => 0 - ); - - if ($entry['QuestRequired']) - foreach (DB::Aowow()->selectCol('SELECT id FROM ?_quests WHERE (`reqSourceItemId1` = ?d OR `reqSourceItemId2` = ?d OR `reqSourceItemId3` = ?d OR `reqSourceItemId4` = ?d OR `reqItemId1` = ?d OR `reqItemId2` = ?d OR `reqItemId3` = ?d OR `reqItemId4` = ?d OR `reqItemId5` = ?d OR `reqItemId6` = ?d) AND (`cuFlags` & ?d) = 0', - $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], $entry['Item'], CUSTOM_EXCLUDE_FOR_LISTVIEW | CUSTOM_UNAVAILABLE) as $questId) - $cnd->addExternalCondition(Conditions::lootTableToConditionSource($tableName), $lootId . ':' . $entry['Item'], [Conditions::QUESTTAKEN, $questId], true); - - // if ($entry['LootMode'] > 1) - // { - $buff = []; - for ($i = 0; $i < 8; $i++) - if ($entry['LootMode'] & (1 << $i)) - $buff[] = $i + 1; - - $set['mode'] = implode(', ', $buff); - // } - // else - // $set['mode'] = 0; - - /* - modes:{"mode":8,"4":{"count":7173,"outof":17619},"8":{"count":7173,"outof":10684}} - ignore lootmodes from sharedDefines.h use different creatures/GOs from each template - modes.mode = b6543210 - ||||||'dungeon heroic - |||||'dungeon normal - ||||'<empty> - |||'10man normal - ||'25man normal - |'10man heroic - '25man heroic - */ - - if ($entry['Reference']) - { - // bandaid.. remove when propperly handling lootmodes - if (!in_array($entry['Reference'], $handledRefs)) - { // todo (high): find out, why i used this in the first place. (don't do drugs, kids) - [$data, $raw] = self::getByContainerRecursive(LOOT_REFERENCE, $entry['Reference'], $handledRefs, /*$entry['GroupId'],*/ 0, $entry['Chance'] / 100); - - $handledRefs[] = $entry['Reference']; - - $loot = array_merge($loot, $data); - $rawItems = array_merge($rawItems, $raw); - } - $set['reference'] = $entry['Reference']; - $set['multiplier'] = $entry['MaxCount']; - } - else - { - $rawItems[] = $entry['Item']; - $set['content'] = $entry['Item']; - $set['min'] = $entry['MinCount']; - $set['max'] = $entry['MaxCount']; - } - - if (!isset($groupChances[$entry['GroupId']])) - { - $groupChances[$entry['GroupId']] = 0; - $nGroupEquals[$entry['GroupId']] = 0; - } - - if ($set['quest'] || !$set['group']) - $set['groupChance'] = $entry['Chance']; - else if ($entry['GroupId'] && !$entry['Chance']) - { - $nGroupEquals[$entry['GroupId']]++; - $set['groupChance'] = &$groupChances[$entry['GroupId']]; - } - else if ($entry['GroupId'] && $entry['Chance']) - { - $set['groupChance'] = $entry['Chance']; - - if (!$entry['Reference']) - { - if (empty($groupChances[$entry['GroupId']])) - $groupChances[$entry['GroupId']] = 0; - - $groupChances[$entry['GroupId']] += $entry['Chance']; - } - } - else // shouldn't have happened - { - trigger_error('Unhandled case in calculating chance for item '.$entry['Item'].'!', E_USER_WARNING); - continue; - } - - $loot[] = $set; - } - - foreach (array_keys($nGroupEquals) as $k) - { - $sum = $groupChances[$k]; - if (!$sum) - $sum = 0; - else if ($sum >= 100.01) - { - trigger_error('Loot entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!', E_USER_WARNING); - $sum = 100; - } - // is applied as backReference to items with 0-chance - $groupChances[$k] = (100 - $sum) / ($nGroupEquals[$k] ?: 1); - } - - if ($cnd->getBySourceGroup($lootId, Conditions::lootTableToConditionSource($tableName))->prepare()) - { - self::storeJSGlobals($cnd->getJsGlobals()); - $cnd->toListviewColumn($loot, $this->extraCols, $lootId, 'content'); - } - - return [$loot, array_unique($rawItems)]; - } - - public function getByContainer(string $table, int $entry): bool - { - $this->entry = intVal($entry); - - if (!in_array($table, $this->lootTemplates) || !$this->entry) - return false; - - /* - // if (is_array($this->entry) && in_array($table, [LOOT_CREATURE, LOOT_GAMEOBJECT]) - // iterate over the 4 available difficulties and assign modes - - - modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}} - */ - $handledRefs = []; - [$lootRows, $itemIds] = self::getByContainerRecursive($table, $this->entry, $handledRefs); - if (!$lootRows) - return false; - - $items = new ItemList(array(['i.id', $itemIds], Cfg::get('SQL_LIMIT_NONE'))); - self::storeJSGlobals($items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $foo = $items->getListviewData(); - - // assign listview LV rows to loot rows, not the other way round! The same item may be contained multiple times - foreach ($lootRows as $loot) - { - $base = array( - 'percent' => round($loot['groupChance'] * $loot['realChanceMod'], 3), - 'group' => $loot['group'], - 'quest' => $loot['quest'], - 'count' => 1 // satisfies the sort-script - ); - - if ($_ = $loot['mode']) - $base['mode'] = $_; - - if ($_ = $loot['parentRef']) - $base['reference'] = $_; - - if (isset($loot['condition'])) - $base['condition'] = $loot['condition']; - - if ($_ = self::createStack($loot)) - $base['pctstack'] = $_; - - if (empty($loot['reference'])) // regular drop - { - if (!isset($foo[$loot['content']])) - { - trigger_error('Item #'.$loot['content'].' referenced by loot does not exist!', E_USER_WARNING); - continue; - } - - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - { - if (!isset($this->results[$loot['content']])) - $this->results[$loot['content']] = array_merge($foo[$loot['content']], $base, ['stack' => [$loot['min'], $loot['max']]]); - else - $this->results[$loot['content']]['percent'] += $base['percent']; - } - else // in case of limited trash loot, check if $foo[<itemId>] exists - $this->results[] = array_merge($foo[$loot['content']], $base, ['stack' => [$loot['min'], $loot['max']]]); - } - else if (User::isInGroup(U_GROUP_EMPLOYEE)) // create dummy for ref-drop - { - $data = array( - 'id' => $loot['reference'], - 'name' => '@REFERENCE: '.$loot['reference'], - 'icon' => 'trade_engineering', - 'stack' => [$loot['multiplier'], $loot['multiplier']] - ); - $this->results[] = array_merge($base, $data); - - $this->jsGlobals[Type::ITEM][$loot['reference']] = $data; - } - } - - // move excessive % to extra loot - if (!User::isInGroup(U_GROUP_EMPLOYEE)) - { - foreach ($this->results as &$_) - { - if ($_['percent'] <= 100) - continue; - - while ($_['percent'] > 200) - { - $_['stack'][0]++; - $_['stack'][1]++; - $_['percent'] -= 100; - } - - $_['stack'][1]++; - $_['percent'] = 100; - } - } - else - { - $fields = ['mode', 'reference']; - $base = []; - $set = 0; - foreach ($this->results as $foo) - { - foreach ($fields as $idx => $field) - { - $val = isset($foo[$field]) ? $foo[$field] : 0; - if (!isset($base[$idx])) - $base[$idx] = $val; - else if ($base[$idx] != $val) - $set |= 1 << $idx; - } - - if ($set == (pow(2, count($fields)) - 1)) - break; - } - - $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('group', 'Group', '7%', 'group')"; - foreach ($fields as $idx => $field) - if ($set & (1 << $idx)) - $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('".$field."', '".Util::ucFirst($field)."', '7%', '".$field."')"; - } - - return true; - } - - public function getByItem(int $entry, int $maxResults = -1, array $lootTableList = []) : bool - { - $this->entry = $entry; - - if (!$this->entry) - return false; - - if ($maxResults < 0) - $maxResults = Cfg::get('SQL_LIMIT_DEFAULT'); - - // [fileName, tabData, tabName, tabId, extraCols, hiddenCols, visibleCols] - $tabsFinal = array( - [Type::ITEM, [], '$LANG.tab_containedin', 'contained-in-item', [], [], []], - [Type::ITEM, [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []], - [Type::ITEM, [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []], - [Type::ITEM, [], '$LANG.tab_milledfrom', 'milled-from', [], [], []], - [Type::NPC, [], '$LANG.tab_droppedby', 'dropped-by', [], [], []], - [Type::NPC, [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []], - [Type::NPC, [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []], - [Type::NPC, [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []], - [Type::NPC, [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []], - [Type::NPC, [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []], - [Type::QUEST, [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []], - [Type::ZONE, [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []], - [Type::OBJECT, [], '$LANG.tab_containedin', 'contained-in-object', [], [], []], - [Type::OBJECT, [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []], - [Type::OBJECT, [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []], - [Type::OBJECT, [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []], - [Type::SPELL, [], '$LANG.tab_createdby', 'created-by', [], [], []], - [Type::ACHIEVEMENT, [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []] - ); - $refResults = []; - $query = 'SELECT - lt1.entry AS ARRAY_KEY, - IF(lt1.reference = 0, lt1.item, lt1.reference) AS item, - lt1.chance, - SUM(IF(lt2.chance = 0, 1, 0)) AS nZeroItems, - SUM(IF(lt2.reference = 0, lt2.chance, 0)) AS sumChance, - IF(lt1.groupid > 0, 1, 0) AS isGrouped, - IF(lt1.reference = 0, lt1.mincount, 1) AS min, - IF(lt1.reference = 0, lt1.maxcount, 1) AS max, - IF(lt1.reference > 0, lt1.maxcount, 1) AS multiplier - FROM - ?# lt1 - LEFT JOIN - ?# lt2 ON lt1.entry = lt2.entry AND lt1.groupid = lt2.groupid - WHERE - %s - GROUP BY lt2.entry, lt2.groupid'; - - /* - get references containing the item - */ - $newRefs = DB::World()->select( - sprintf($query, 'lt1.item = ?d AND lt1.reference = 0'), - LOOT_REFERENCE, LOOT_REFERENCE, - $this->entry - ); - - /* i'm currently not seeing a reasonable way to blend this into creature/gobject/etc tabs as one entity may drop the same item multiple times, with and without conditions. - if ($newRefs) - { - $cnd = new Conditions(); - if ($cnd->getBySourceEntry($this->entry, Conditions::SRC_REFERENCE_LOOT_TEMPLATE)) - if ($cnd->toListviewColumn($newRefs, $x, $this->entry)) - self::storejsGlobals($cnd->getJsGlobals()); - } - */ - - while ($newRefs) - { - $curRefs = $newRefs; - $newRefs = DB::World()->select( - sprintf($query, 'lt1.reference IN (?a)'), - LOOT_REFERENCE, LOOT_REFERENCE, - array_keys($curRefs) - ); - - $refResults += $this->calcChance($curRefs, array_column($newRefs, 'item')); - } - - /* - search the real loot-templates for the itemId and gathered refds - */ - foreach ($this->lootTemplates as $lootTemplate) - { - if ($lootTableList && !in_array($lootTemplate, $lootTableList)) - continue; - - if ($lootTemplate == LOOT_REFERENCE) - continue; - - $result = $this->calcChance(DB::World()->select( - sprintf($query, '{lt1.reference IN (?a) OR }(lt1.reference = 0 AND lt1.item = ?d)'), - $lootTemplate, $lootTemplate, - $refResults ? array_keys($refResults) : DBSIMPLE_SKIP, - $this->entry - )); - - // do not skip here if $result is empty. Additional loot for spells and quest is added separately - - // format for actual use - foreach ($result as $k => $v) - { - unset($result[$k]); - $v['percent'] = round($v['percent'] * 100, 3); - $result[abs($k)] = $v; - } - - // cap fetched entries to the sql-limit to guarantee, that the highest chance items get selected first - // screws with GO-loot and skinning-loot as these templates are shared for several tabs (fish, herb, ore) (herb, ore, leather) - $ids = array_slice(array_keys($result), 0, $maxResults); - - switch ($lootTemplate) - { - case LOOT_CREATURE: $field = 'lootId'; $tabId = 4; break; - case LOOT_PICKPOCKET: $field = 'pickpocketLootId'; $tabId = 5; break; - case LOOT_SKINNING: $field = 'skinLootId'; $tabId = -6; break; // assigned later - case LOOT_PROSPECTING: $field = 'id'; $tabId = 2; break; - case LOOT_MILLING: $field = 'id'; $tabId = 3; break; - case LOOT_ITEM: $field = 'id'; $tabId = 0; break; - case LOOT_DISENCHANT: $field = 'disenchantId'; $tabId = 1; break; - case LOOT_FISHING: $field = 'id'; $tabId = 11; break; // subAreas are currently ignored - case LOOT_GAMEOBJECT: - if (!$ids) - continue 2; - - $srcObj = new GameObjectList(array(['lootId', $ids])); - if ($srcObj->error) - continue 2; - - $srcData = $srcObj->getListviewData(); - - foreach ($srcObj->iterate() as $curTpl) - { - switch ($curTpl['typeCat']) - { - case 25: $tabId = 15; break; // fishing node - case -3: $tabId = 14; break; // herb - case -4: $tabId = 13; break; // vein - default: $tabId = 12; break; // general chest loot - } - - $tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField('lootId')]); - $tabsFinal[$tabId][4][] = '$Listview.extraCols.percent'; - if ($tabId != 15) - $tabsFinal[$tabId][6][] = 'skill'; - } - continue 2; - case LOOT_MAIL: - // quest part - $conditions = array(['rewardChoiceItemId1', $this->entry], ['rewardChoiceItemId2', $this->entry], ['rewardChoiceItemId3', $this->entry], ['rewardChoiceItemId4', $this->entry], ['rewardChoiceItemId5', $this->entry], - ['rewardChoiceItemId6', $this->entry], ['rewardItemId1', $this->entry], ['rewardItemId2', $this->entry], ['rewardItemId3', $this->entry], ['rewardItemId4', $this->entry], - 'OR'); - if ($ids) - $conditions[] = ['rewardMailTemplateId', $ids]; - - $srcObj = new QuestList($conditions); - if (!$srcObj->error) - { - self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - $srcData = $srcObj->getListviewData(); - - foreach ($srcObj->iterate() as $_) - $tabsFinal[10][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]); - } - - // achievement part - $conditions = array(['itemExtra', $this->entry]); - if ($ar = DB::World()->selectCol('SELECT ID FROM achievement_reward WHERE ItemID = ?d{ OR MailTemplateID IN (?a)}', $this->entry, $ids ?: DBSIMPLE_SKIP)) - array_push($conditions, ['id', $ar], 'OR'); - - $srcObj = new AchievementList($conditions); - if (!$srcObj->error) - { - self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); - $srcData = $srcObj->getListviewData(); - - foreach ($srcObj->iterate() as $_) - $tabsFinal[17][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]); - - $tabsFinal[17][5][] = 'rewards'; - $tabsFinal[17][6][] = 'category'; - } - continue 2; - case LOOT_SPELL: - $conditions = array( - 'OR', - ['AND', ['effect1CreateItemId', $this->entry], ['OR', ['effect1Id', SpellList::EFFECTS_ITEM_CREATE], ['effect1AuraId', SpellList::AURAS_ITEM_CREATE]]], - ['AND', ['effect2CreateItemId', $this->entry], ['OR', ['effect2Id', SpellList::EFFECTS_ITEM_CREATE], ['effect2AuraId', SpellList::AURAS_ITEM_CREATE]]], - ['AND', ['effect3CreateItemId', $this->entry], ['OR', ['effect3Id', SpellList::EFFECTS_ITEM_CREATE], ['effect3AuraId', SpellList::AURAS_ITEM_CREATE]]], - ); - if ($ids) - $conditions[] = ['id', $ids]; - - $srcObj = new SpellList($conditions); - if (!$srcObj->error) - { - self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - $srcData = $srcObj->getListviewData(); - - if (!empty($result)) - $tabsFinal[16][4][] = '$Listview.extraCols.percent'; - - if ($srcObj->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8')) - $tabsFinal[16][6][] = 'reagents'; - - foreach ($srcObj->iterate() as $_) - $tabsFinal[16][1][] = array_merge($srcData[$srcObj->id], empty($result[$srcObj->id]) ? ['percent' => -1] : $result[$srcObj->id]); - } - continue 2; - } - - if (!$ids) - continue; - - $parentData = []; - switch ($tabsFinal[abs($tabId)][0]) - { - case TYPE::NPC: // new CreatureList - if ($baseIds = DB::Aowow()->selectCol( - 'SELECT `difficultyEntry1` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry1 IN (?a) UNION - SELECT `difficultyEntry2` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry2 IN (?a) UNION - SELECT `difficultyEntry3` AS ARRAY_KEY, `id` FROM ?_creature WHERE difficultyEntry3 IN (?a)', - $ids, $ids, $ids)) - { - $parentObj = new CreatureList(array(['id', $baseIds])); - if (!$parentObj->error) - { - self::storeJSGlobals($parentObj->getJSGlobals()); - $parentData = $parentObj->getListviewData(); - $ids = array_diff($ids, $baseIds); - } - } - - case Type::ITEM: // new ItemList - case Type::ZONE: // new ZoneList - $srcObj = Type::newList($tabsFinal[abs($tabId)][0], array([$field, $ids])); - if (!$srcObj->error) - { - $srcData = $srcObj->getListviewData(); - self::storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); - - foreach ($srcObj->iterate() as $curTpl) - { - if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_HERBALISM) - $tabId = 9; - else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_ENGINEERING) - $tabId = 8; - else if ($tabId < 0 && $curTpl['typeFlags'] & NPC_TYPEFLAG_SKIN_WITH_MINING) - $tabId = 7; - else if ($tabId < 0) - $tabId = abs($tabId); // general case (skinning) - - if (($p = $srcObj->getField('parentId')) && ($d = $parentData[$p] ?? null)) - $tabsFinal[$tabId][1][] = array_merge($d, $result[$srcObj->getField($field)]); - else - $tabsFinal[$tabId][1][] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($field)]); - - $tabsFinal[$tabId][4][] = '$Listview.extraCols.percent'; - } - } - break; - } - } - - foreach ($tabsFinal as $tabId => $data) - { - $tabData = array( - 'data' => $data[1], - 'name' => $data[2], - 'id' => $data[3] - ); - - if ($data[4]) - $tabData['extraCols'] = array_unique($data[4]); - - if ($data[5]) - $tabData['hiddenCols'] = array_unique($data[5]); - - if ($data[6]) - $tabData['visibleCols'] = array_unique($data[6]); - - $this->results[$tabId] = [Type::getFileString($data[0]), $tabData]; - } - - return true; - } -} - -?> diff --git a/includes/game/loot/loot.class.php b/includes/game/loot/loot.class.php new file mode 100644 index 00000000..55c6f513 --- /dev/null +++ b/includes/game/loot/loot.class.php @@ -0,0 +1,71 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + + +/* NOTE! + * + * TrinityCore uses "mode", a bitmask in a loot template, to distinguish dynamic properties of the template. i.e. entries get enabled if the boss fight progresses in a certain way. + * WH/we uses "mode", different loot templates, to describe the raid/dungeon difficulty + * + * try to not mix this shit up + */ + +abstract class Loot +{ + // Loot handles + public const FISHING = 'fishing_loot_template'; // fishing_loot_template (no relation entry is linked with ID of the fishing zone or area) + public const CREATURE = 'creature_loot_template'; // creature_loot_template entry many <- many creature_template lootid + public const GAMEOBJECT = 'gameobject_loot_template'; // gameobject_loot_template entry many <- many gameobject_template data1 (see its lockType for mining, herbing, fishing or generic looting) + public const ITEM = 'item_loot_template'; // item_loot_template entry many <- one item_template entry + public const DISENCHANT = 'disenchant_loot_template'; // disenchant_loot_template entry many <- many item_template DisenchantID + public const PROSPECTING = 'prospecting_loot_template'; // prospecting_loot_template entry many <- one item_template entry + public const MILLING = 'milling_loot_template'; // milling_loot_template entry many <- one item_template entry + public const PICKPOCKET = 'pickpocketing_loot_template'; // pickpocketing_loot_template entry many <- many creature_template pickpocketloot + public const SKINNING = 'skinning_loot_template'; // skinning_loot_template entry many <- many creature_template skinloot (see the creatures flags for mining, herbing, salvaging or actual skinning) + public const MAIL = 'mail_loot_template'; // mail_loot_template entry quest_template RewMailTemplateId (quest + achievement) + public const SPELL = 'spell_loot_template'; // spell_loot_template entry many <- one spell.dbc id + public const REFERENCE = 'reference_loot_template'; // reference_loot_template entry many <- many *_loot_template reference + + protected const TEMPLATES = [self::REFERENCE, self::ITEM, self::DISENCHANT, self::PROSPECTING, self::CREATURE, self::MILLING, self::PICKPOCKET, self::SKINNING, self::FISHING, self::GAMEOBJECT, self::MAIL, self::SPELL]; + + public array $jsGlobals = []; + + protected array $results = []; + + /** + * builds stack info string for listview rows + * issue: TC always has an equal distribution between min/max + * and yes, it wants a string .. how weired is that.. + * + * @param int $min min amount + * @param int $max max amount + * @return ?string stack info or null on error + */ + protected static function buildStack(int $min, int $max) : ?string + { + if (!$min || !$max || $max <= $min) + return null; + + $stack = []; + for ($i = $min; $i <= $max; $i++) + $stack[$i] = round(100 / (1 + $max - $min), 3); + + // do not replace with Util::toJSON ! + return json_encode($stack, JSON_NUMERIC_CHECK); + } + + /** + * @param array $data js global data to store + * @return void + */ + protected function storeJSGlobals(array $data) : void + { + Util::mergeJsGlobals($this->jsGlobals, $data); + } +} + +?> diff --git a/includes/game/loot/lootbycontainer.class.php b/includes/game/loot/lootbycontainer.class.php new file mode 100644 index 00000000..64d287d4 --- /dev/null +++ b/includes/game/loot/lootbycontainer.class.php @@ -0,0 +1,333 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + + +class LootByContainer extends Loot +{ + public array $extraCols = []; + + private array $knownRefs = []; // known ref loot results (can be reused) + + /** + * @return array found loot result + */ + public function getResult() : array + { + return $this->results; + } + + /** + * recurse through reference loot while applying modifiers from parent container + * + * @param string $tableName a known loot template table name + * @param int $lootId a loot template entry + * @param int $groupId [optional] limit result to provided loot group + * @param float $baseChance [optional] chance multiplier passed down from parent container + * @return array [[<array>lootRows], [<int>itemIds]] + */ + private function getByContainerRecursive(string $tableName, int $lootId, int $groupId = 0, float $baseChance = 1.0) : array + { + $loot = []; + $rawItems = []; + + if (!$tableName || !$lootId) + return [null, null]; + + $rows = DB::World()->selectAssoc('SELECT * FROM %n', $tableName, 'WHERE %if', $groupId, '`groupid` = %i AND', $groupId, '%end `entry` = %i', $lootId); + if (!$rows) + return [null, null]; + + $groupChances = []; + $nGroupEquals = []; + $cnd = new Conditions(); + foreach ($rows as $entry) + { + $set = array( + 'quest' => $entry['QuestRequired'], + 'group' => $entry['GroupId'], + 'parentRef' => $tableName == self::REFERENCE ? $lootId : 0, + 'realChanceMod' => $baseChance, + 'groupChance' => 0 + ); + + $where = [['(`cuFlags` & %i) = 0', CUSTOM_EXCLUDE_FOR_LISTVIEW | CUSTOM_UNAVAILABLE], [DB::OR, []]]; + for ($i = 1; $i < 5; $i++) + $where[1][1][] = ["`reqSourceItemId$i` = %i", $entry['Item']]; + for ($i = 1; $i < 7; $i++) + $where[1][1][] = ["`reqItemId$i` = %i", $entry['Item']]; + + if ($entry['QuestRequired'] && ($quests = DB::Aowow()->selectCol('SELECT `id` FROM ::quests WHERE %and', $where))) + foreach ($quests as $questId) + $cnd->addExternalCondition(Conditions::lootTableToConditionSource($tableName), $lootId . ':' . $entry['Item'], [Conditions::QUESTTAKEN, $questId], true); + + // TC 'mode' (dynamic loot modifier) + $buff = []; + for ($i = 0; $i < 8; $i++) + if ($entry['LootMode'] & (1 << $i)) + $buff[] = $i + 1; + + $set['mode'] = implode(', ', $buff); + + if ($entry['Reference']) + { + if (!in_array($entry['Reference'], $this->knownRefs)) + $this->knownRefs[$entry['Reference']] = $this->getByContainerRecursive(self::REFERENCE, $entry['Reference'], 0, $entry['Chance'] / 100); + + [$data, $raw] = $this->knownRefs[$entry['Reference']]; + + $loot = array_merge($loot, $data); + $rawItems = array_merge($rawItems, $raw); + + $set['reference'] = $entry['Reference']; + $set['multiplier'] = $entry['MaxCount']; + } + else + { + $rawItems[] = $entry['Item']; + $set['content'] = $entry['Item']; + $set['min'] = $entry['MinCount']; + $set['max'] = $entry['MaxCount']; + } + + if (!isset($groupChances[$entry['GroupId']])) + { + $groupChances[$entry['GroupId']] = 0; + $nGroupEquals[$entry['GroupId']] = 0; + } + + if ($set['quest'] || !$set['group']) + $set['groupChance'] = $entry['Chance']; + else if ($entry['GroupId'] && !$entry['Chance']) + { + $nGroupEquals[$entry['GroupId']]++; + $set['groupChance'] = &$groupChances[$entry['GroupId']]; + } + else if ($entry['GroupId'] && $entry['Chance']) + { + $set['groupChance'] = $entry['Chance']; + + if (!$entry['Reference']) + { + if (empty($groupChances[$entry['GroupId']])) + $groupChances[$entry['GroupId']] = 0; + + $groupChances[$entry['GroupId']] += $entry['Chance']; + } + } + else // shouldn't have happened + { + trigger_error('Unhandled case in calculating chance for item '.$entry['Item'].'!', E_USER_WARNING); + continue; + } + + $loot[] = $set; + } + + foreach (array_keys($nGroupEquals) as $k) + { + $sum = $groupChances[$k]; + if (!$sum) + $sum = 0; + else if ($sum >= 100.01) + { + trigger_error('Loot entry '.$lootId.' / group '.$k.' has a total chance of '.number_format($sum, 2).'%. Some items cannot drop!', E_USER_WARNING); + $sum = 100; + } + // is applied as backReference to items with 0-chance + $groupChances[$k] = (100 - $sum) / ($nGroupEquals[$k] ?: 1); + } + + if ($cnd->getBySource(Conditions::lootTableToConditionSource($tableName), group: $lootId)->prepare()) + { + $this->storeJSGlobals($cnd->getJsGlobals()); + $cnd->toListviewColumn($loot, $this->extraCols, $lootId, 'content'); + } + + return [$loot, array_unique($rawItems)]; + } + + /** + * fetch loot for given loot container and optionally merge multiple container while adding mode info. + * If difficultyBit is 0, no merge will occur + * + * @param string $table a known loote template table name + * @param array $lootEntries array of [difficultyBit => entry]. + * @return bool success and found loot + */ + public function getByContainer(string $table, array $lootEntries): bool + { + if (!in_array($table, self::TEMPLATES)) + return false; + + foreach ($lootEntries as $modeBit => $entry) + { + if (!$entry) + continue; + + [$lootRows, $itemIds] = $this->getByContainerRecursive($table, $entry); + if (!$lootRows) + continue; + + $items = new ItemList(array(['i.id', $itemIds])); + $this->storeJSGlobals($items->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + $itemRows = $items->getListviewData(); + + // assign listview LV rows to loot rows, not the other way round! The same item may be contained multiple times + foreach ($lootRows as $loot) + { + $count = ceil($loot['groupChance'] * $loot['realChanceMod'] * 100); + + /* on modes... + * modes.mode is the (masked) sum of all modes where this item has been seen + * modes.mode & 1 dungeon normal + * modes.mode & 2 dungeon heroic + * modes.mode & 4 generic case (never included in mask for instanced creatures/gos or always === 4 for non-instanced creatures/gos) + * modes.mode & 8 raid 10 nh + * modes.mode & 16 raid 25 nh + * modes.mode & 32 raid 10 hc + * modes.mode & 64 raid 25 hc + * + * modes[4] is _always_ included and is the sum total over all modes: + * ex: modes:{"mode":1,"1":{"count":4408,"outof":16013},"4":{"count":4408,"outof":22531}} + */ + if ($modeBit) + { + $modes = array( // emulate 'percent' with precision: 2 + 'mode' => $modeBit, + $modeBit => ['count' => $count, 'outof' => 10000] + ); + if ($modeBit != 4) + $modes[4] = $modes[$modeBit]; + + + // unsure: force display as noteworthy + // if (!empty($loot['content']) && !empty($itemRows[$loot['content']]) && $itemRows[$loot['content']]['name'][0] == 7 - ITEM_QUALITY_POOR) + // $modes['mode'] = 4; + // else if ($count < 100) // chance < 1% + // $modes['mode'] = 4; + + + // existing result row; merge modes and move on + if (!is_null($k = array_find_key($this->results, function($x) use ($loot) { + if (!empty($loot['reference'])) + return $x['id'] == $loot['reference'] && $x['mode'] == $loot['mode'] && $x['group'] == $loot['group'] && $x['stack'] == [$loot['multiplier'], $loot['multiplier']]; + else + return $x['id'] == $loot['content'] && $x['mode'] == $loot['mode'] && $x['group'] == $loot['group']; + }))) + { + $this->results[$k]['modes']['mode'] |= $modes['mode']; + $this->results[$k]['modes'][$modeBit] = $modes[$modeBit]; + $this->results[$k]['modes'][4]['count'] = max($modes[4]['count'], $this->results[$k]['modes'][4]['count']); + + continue; + } + } + + $base = array( + 'count' => $count, + 'outof' => 10000, + 'group' => $loot['group'], + 'quest' => $loot['quest'], + 'mode' => $loot['mode'] ?: null, // dyn loot mode + 'modes' => $modes ?? null, // difficulties + 'reference' => $loot['parentRef'] ?: null, + 'condition' => $loot['condition'] ?? null, + 'pctstack' => self::buildStack($loot['min'] ?? 0, $loot['max'] ?? 0) + ); + + $base = array_filter($base, fn($x) => $x !== null); + + if (empty($loot['reference'])) // regular drop + { + if ($itemRow = $itemRows[$loot['content']] ?? null) + { + $extra = ['stack' => [$loot['min'], $loot['max']]]; + + // unsure if correct - tag item as trash if chance < 1% and tagged as having many sources + if ($base['count'] < 100 && $items->getEntry($loot['content'])['moreMask'] & SRC_FLAG_COMMON) + $extra['commondrop'] = 1; + + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + { + if (!isset($this->results[$loot['content']])) + $this->results[$loot['content']] = array_merge($itemRow, $base, $extra); + else + $this->results[$loot['content']]['count'] += $base['count']; + } + else + $this->results[] = array_merge($itemRow, $base, $extra); + } + else + trigger_error('Item #'.$loot['content'].' referenced by loot does not exist!', E_USER_WARNING); + } + else if (User::isInGroup(U_GROUP_EMPLOYEE)) // create dummy for ref-drop + { + $data = array( + 'id' => $loot['reference'], + 'name' => '@REFERENCE: '.$loot['reference'], + 'icon' => 'trade_engineering', + 'stack' => [$loot['multiplier'], $loot['multiplier']], + 'commondrop' => 1 + ); + $this->results[] = array_merge($base, $data); + + $this->jsGlobals[Type::ITEM][$loot['reference']] = $data; + } + } + } + + // move excessive % to extra loot + if (!User::isInGroup(U_GROUP_EMPLOYEE)) + { + foreach ($this->results as &$_) + { + // remember 'count' is always relative to a base of 10000 + if ($_['count'] <= 10000) + continue; + + while ($_['count'] > 20000) + { + $_['stack'][0]++; + $_['stack'][1]++; + $_['count'] -= 10000; + } + + $_['stack'][1]++; + $_['count'] = 10000; + } + } + else + { + $fields = [['mode', 'Dyn. Mode'], ['reference', 'Reference']]; + $base = []; + $set = 0; + foreach ($this->results as $foo) + { + foreach ($fields as $idx => [$field, $title]) + { + $val = $foo[$field] ?? 0; + if (!isset($base[$idx])) + $base[$idx] = $val; + else if ($base[$idx] != $val) + $set |= 1 << $idx; + } + + if ($set == (pow(2, count($fields)) - 1)) + break; + } + + $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('group', 'Group', '7%', 'group')"; + foreach ($fields as $idx => [$field, $title]) + if ($set & (1 << $idx)) + $this->extraCols[] = "\$Listview.funcBox.createSimpleCol('".$field."', '".$title."', '7%', '".$field."')"; + } + + return !empty($this->results); + } +} + +?> diff --git a/includes/game/loot/lootbyitem.class.php b/includes/game/loot/lootbyitem.class.php new file mode 100644 index 00000000..ba7a29ce --- /dev/null +++ b/includes/game/loot/lootbyitem.class.php @@ -0,0 +1,441 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + + +class LootByItem extends Loot +{ + public const /* int */ ITEM_CONTAINED = 0; + public const /* int */ ITEM_DISENCHANTED = 1; + public const /* int */ ITEM_PROSPECTED = 2; + public const /* int */ ITEM_MILLED = 3; + public const /* int */ NPC_DROPPED = 4; + public const /* int */ NPC_PICKPOCKETED = 5; + public const /* int */ NPC_SKINNED = 6; + public const /* int */ NPC_MINED = 7; + public const /* int */ NPC_SALVAGED = 8; + public const /* int */ NPC_GATHERED = 9; + public const /* int */ QUEST_REWARD = 10; + public const /* int */ ZONE_FISHED = 11; + public const /* int */ OBJECT_CONTAINED = 12; + public const /* int */ OBJECT_MINED = 13; + public const /* int */ OBJECT_GATHERED = 14; + public const /* int */ OBJECT_FISHED = 15; + public const /* int */ SPELL_CREATED = 16; + public const /* int */ ACHIEVEMENT_REWARD = 17; + + private array $chanceMods = []; + private array $listviewTabs = array( // order here determines tab order on page + // [fileName, tabData, tabName, tabId, extraCols, hiddenCols, visibleCols] + self::NPC_DROPPED => [Type::NPC, [], '$LANG.tab_droppedby', 'dropped-by', [], [], []], + self::QUEST_REWARD => [Type::QUEST, [], '$LANG.tab_rewardfrom', 'reward-from-quest', [], [], []], + self::ITEM_CONTAINED => [Type::ITEM, [], '$LANG.tab_containedin', 'contained-in-item', [], [], []], + self::OBJECT_CONTAINED => [Type::OBJECT, [], '$LANG.tab_containedin', 'contained-in-object', [], [], []], + self::NPC_PICKPOCKETED => [Type::NPC, [], '$LANG.tab_pickpocketedfrom', 'pickpocketed-from', [], [], []], + self::NPC_SKINNED => [Type::NPC, [], '$LANG.tab_skinnedfrom', 'skinned-from', [], [], []], + self::ITEM_DISENCHANTED => [Type::ITEM, [], '$LANG.tab_disenchantedfrom', 'disenchanted-from', [], [], []], + self::ITEM_PROSPECTED => [Type::ITEM, [], '$LANG.tab_prospectedfrom', 'prospected-from', [], [], []], + self::ITEM_MILLED => [Type::ITEM, [], '$LANG.tab_milledfrom', 'milled-from', [], [], []], + self::NPC_MINED => [Type::NPC, [], '$LANG.tab_minedfromnpc', 'mined-from-npc', [], [], []], + self::NPC_SALVAGED => [Type::NPC, [], '$LANG.tab_salvagedfrom', 'salvaged-from', [], [], []], + self::NPC_GATHERED => [Type::NPC, [], '$LANG.tab_gatheredfromnpc', 'gathered-from-npc', [], [], []], + self::OBJECT_MINED => [Type::OBJECT, [], '$LANG.tab_minedfrom', 'mined-from-object', [], [], []], + self::OBJECT_GATHERED => [Type::OBJECT, [], '$LANG.tab_gatheredfrom', 'gathered-from-object', [], [], []], + self::ZONE_FISHED => [Type::ZONE, [], '$LANG.tab_fishedin', 'fished-in-zone', [], [], []], + self::OBJECT_FISHED => [Type::OBJECT, [], '$LANG.tab_fishedin', 'fished-in-object', [], [], []], + self::SPELL_CREATED => [Type::SPELL, [], '$LANG.tab_createdby', 'created-by', [], [], []], + self::ACHIEVEMENT_REWARD => [Type::ACHIEVEMENT, [], '$LANG.tab_rewardfrom', 'reward-from-achievement', [], [], []] + ); + private string $queryTemplate = + 'SELECT lt1.`entry` AS ARRAY_KEY, + IF(lt1.`reference` = 0, lt1.`item`, lt1.`reference`) AS "item", + lt1.`chance` AS "chance", + SUM(IF(lt2.`chance` = 0, 1, 0)) AS "nZeroItems", + SUM(IF(lt2.`reference` = 0, lt2.`chance`, 0)) AS "sumChance", + IF(lt1.`groupid` > 0, 1, 0) AS "isGrouped", + IF(lt1.`reference` = 0, lt1.`mincount`, 1) AS "min", + IF(lt1.`reference` = 0, lt1.`maxcount`, 1) AS "max", + IF(lt1.`reference` > 0, lt1.`maxcount`, 1) AS "multiplier" + FROM %n lt1 + LEFT JOIN %n lt2 ON lt1.`entry` = lt2.`entry` AND lt1.`groupid` = lt2.`groupid` + WHERE %and + GROUP BY lt2.`entry`, lt2.`groupid`'; + + /** + * @param int $entry item id to find loot container for + * @return void + */ + public function __construct(private int $entry) + { + + } + + /** + * iterate over result set + * + * @return iterable [tabIdx => [lvTemplate, lvData]] + */ + public function &iterate() : \Generator + { + reset($this->results); + + foreach ($this->results as $k => [, $tabData]) + if ($tabData['data']) // only yield tabs with content + yield $k => $this->results[$k]; + } + + /** + * calculate chance and stack info and apply to loot rows + * + * @param array $refs loot rows to apply chance + stack info to + * @param array $parents [optional] ref loot ids this call is derived from + * @return array [entry => stack+chance-info] + */ + private function calcChance(array $refs, array $parents = []) : array + { + $result = []; + + foreach ($refs as $rId => $ref) + { + // check for possible database inconsistencies + if (!$ref['chance'] && !$ref['isGrouped']) + trigger_error('Loot by Item: Ungrouped Item/Ref '.$ref['item'].' has 0% chance assigned!', E_USER_WARNING); + + if ($ref['isGrouped'] && $ref['sumChance'] > 100) + trigger_error('Loot by Item: Group with Item/Ref '.$ref['item'].' has '.number_format($ref['sumChance'], 2).'% total chance! Some items cannot drop!', E_USER_WARNING); + + if ($ref['isGrouped'] && $ref['sumChance'] >= 100 && !$ref['chance']) + trigger_error('Loot by Item: Item/Ref '.$ref['item'].' with adaptive chance cannot drop. Group already at 100%!', E_USER_WARNING); + + $chance = abs($ref['chance'] ?: (100 - $ref['sumChance']) / $ref['nZeroItems']) / 100; + + // apply inherited chanceMods + if (isset($this->chanceMods[$ref['item']])) + { + $chance *= $this->chanceMods[$ref['item']][0]; + $chance = 1 - pow(1 - $chance, $this->chanceMods[$ref['item']][1]); + } + + // save chance for parent-ref + $this->chanceMods[$rId] = [$chance, $ref['multiplier']]; + + // refTemplate doesn't point to a new ref -> we are done + if (in_array($rId, $parents)) + continue; + + $result[$rId] = array( + 'percent' => $chance, + 'stack' => [$ref['min'], $ref['max']], + 'count' => 1 // ..and one for the sort script + ); + + if ($_ = self::buildStack($ref['min'], $ref['max'])) + $result[$rId]['pctstack'] = $_; + } + + // sort by % DESC + uasort($result, fn($a, $b) => $b['percent'] <=> $a['percent']); + + return $result; + } + + /** + * fetch loot container for item provided to __construct + * + * @param int $maxResults [optional] SQL_LIMIT override + * @param array $lootTableList [optional] limit lookup to provided loot template table names + * @return bool success + */ + public function getByItem(int $maxResults = Listview::DEFAULT_SIZE, array $lootTableList = []) : bool + { + if (!$this->entry) + return false; + + $refResults = []; + + /* + get references containing the item + */ + $newRefs = DB::World()->selectAssoc( + $this->queryTemplate, + Loot::REFERENCE, Loot::REFERENCE, + [['lt1.`item` = %i', $this->entry], ['lt1.`reference` = 0']] + ); + + /* + i'm currently not seeing a reasonable way to blend this into creature/gobject/etc tabs as one entity may drop the same item multiple times, with and without conditions. + if ($newRefs) + { + $cnd = new Conditions(); + if ($cnd->getBySource(Conditions::SRC_REFERENCE_LOOT_TEMPLATE, entry: $this->entry)) + if ($cnd->toListviewColumn($newRefs, $x, $this->entry)) + $this->storejsGlobals($cnd->getJsGlobals()); + } + */ + + while ($newRefs) + { + $curRefs = $newRefs; + $newRefs = DB::World()->selectAssoc( + $this->queryTemplate, + Loot::REFERENCE, Loot::REFERENCE, + [['lt1.`reference` IN %in', array_keys($curRefs)]] + ); + + $refResults += $this->calcChance($curRefs, array_column($newRefs, 'item')); + } + + /* + search the real loot-templates for the itemId and gathered refs + */ + foreach (self::TEMPLATES as $lootTemplate) + { + if ($lootTableList && !in_array($lootTemplate, $lootTableList)) + continue; + + if ($lootTemplate == Loot::REFERENCE) + continue; + + $where = [[DB::OR, [[DB::AND, [['lt1.`reference` = 0'], ['lt1.`item` = %i', $this->entry]]]]]]; + if ($refResults) + $where[0][1][] = ['lt1.`reference` IN %in', array_keys($refResults)]; + + $result = $this->calcChance(DB::World()->selectAssoc( + $this->queryTemplate, + $lootTemplate, $lootTemplate, + $where + )); + + // do not skip here if $result is empty. Additional loot for spells and quest is added separately + + // format for actual use + foreach ($result as $k => $v) + { + unset($result[$k]); + $v['percent'] = round($v['percent'] * 100, 3); + $result[abs($k)] = $v; + } + + // cap fetched entries to the sql-limit to guarantee that the highest chance items get selected first + // screws with GO-loot and skinning-loot as these templates are shared for several tabs (fish, herb, ore) and (herb, ore, leather) + $ids = array_slice(array_keys($result), 0, $maxResults); + + // fill ListviewTabs + match ($lootTemplate) + { + Loot::GAMEOBJECT => $this->handleObjectLoot( $ids, $result), + Loot::MAIL => $this->handleMailLoot( $ids, $result), + Loot::SPELL => $this->handleSpellLoot( $ids, $result), + Loot::CREATURE => $this->handleNpcLoot( $ids, $result, self::NPC_DROPPED, 'lootId'), + Loot::PICKPOCKET => $this->handleNpcLoot( $ids, $result, self::NPC_PICKPOCKETED, 'pickpocketLootId'), + Loot::SKINNING => $this->handleNpcLoot( $ids, $result, self::NPC_SKINNED, 'skinLootId'), // tabId < 0: assigned real id later + Loot::PROSPECTING => $this->handleGenericLoot($ids, $result, self::ITEM_PROSPECTED, 'id'), + Loot::MILLING => $this->handleGenericLoot($ids, $result, self::ITEM_MILLED, 'id'), + Loot::ITEM => $this->handleGenericLoot($ids, $result, self::ITEM_CONTAINED, 'id'), + Loot::DISENCHANT => $this->handleGenericLoot($ids, $result, self::ITEM_DISENCHANTED, 'disenchantId'), + Loot::FISHING => $this->handleGenericLoot($ids, $result, self::ZONE_FISHED, 'id') // subAreas are currently ignored + }; + } + + // finalize tabs + foreach ($this->listviewTabs as $idx => [$type, $data, $name, $id, $extraCols, $hiddenCols, $visibleCols]) + { + $tabData = array( + 'data' => $data, + 'name' => $name, + 'id' => $id + ); + + if ($extraCols) + $tabData['extraCols'] = array_unique($extraCols); + + if ($hiddenCols) + $tabData['hiddenCols'] = array_unique($hiddenCols); + + if ($visibleCols) + $tabData['visibleCols'] = array_unique($visibleCols); + + $this->results[$idx] = [Type::getFileString($type), $tabData]; + } + + return true; + } + + private function handleGenericLoot(array $ids, array $result, int $tabId, string $dbField) : bool + { + if (!$ids) + return false; + + [$type, &$data, , , &$extraCols, ,] = $this->listviewTabs[$tabId]; + + $srcObj = Type::newList($type, [[$dbField, $ids]]); + if (!$srcObj || $srcObj->error) + return false; + + $srcData = $srcObj->getListviewData(); + $this->storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + + $extraCols[] = '$Listview.extraCols.percent'; + + foreach ($srcObj->iterate() as $__) + $data[] = array_merge($srcData[$srcObj->id], $result[$srcObj->getField($dbField)]); + + return true; + } + + private function handleNpcLoot(array $ids, array $result, int $tabId, string $dbField) : bool + { + if (!$ids) + return false; + + if ($baseIds = DB::Aowow()->selectPairs( + 'SELECT `difficultyEntry1` AS ARRAY_KEY, `id` FROM ::creature WHERE `difficultyEntry1` IN %in UNION + SELECT `difficultyEntry2` AS ARRAY_KEY, `id` FROM ::creature WHERE `difficultyEntry2` IN %in UNION + SELECT `difficultyEntry3` AS ARRAY_KEY, `id` FROM ::creature WHERE `difficultyEntry3` IN %in', + $ids, $ids, $ids + )) + { + $parentObj = new CreatureList(array(['id', $baseIds])); + if (!$parentObj->error) + { + $this->storeJSGlobals($parentObj->getJSGlobals()); + $parentData = $parentObj->getListviewData(); + $ids = array_diff($ids, $baseIds); + } + } + + $npc = new CreatureList(array([$dbField, $ids])); + if ($npc->error) + return false; + + $srcData = $npc->getListviewData(); + $this->storeJSGlobals($npc->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + [, &$data, , , &$extraCols, ,] = $this->listviewTabs[$tabId]; + + foreach ($npc->iterate() as $__) + { + if ($tabId == self::NPC_SKINNED) + { + if ($npc->isMineable()) + $tabId = self::NPC_MINED; + else if ($npc->isGatherable()) + $tabId = self::NPC_GATHERED; + else if ($npc->isSalvageable()) + $tabId = self::NPC_SALVAGED; + } + + $p = $npc->getField('parentId'); + + $data[] = array_merge($parentData[$p] ?? $srcData[$npc->id], $result[$npc->getField($dbField)]); + $extraCols[] = '$Listview.extraCols.percent'; + } + + return true; + } + + private function handleSpellLoot(array $ids, array $result) : bool + { + $conditions = array( + DB::OR, + [DB::AND, ['effect1CreateItemId', $this->entry], [DB::OR, ['effect1Id', SpellList::EFFECTS_ITEM_CREATE], ['effect1AuraId', SpellList::AURAS_ITEM_CREATE]]], + [DB::AND, ['effect2CreateItemId', $this->entry], [DB::OR, ['effect2Id', SpellList::EFFECTS_ITEM_CREATE], ['effect2AuraId', SpellList::AURAS_ITEM_CREATE]]], + [DB::AND, ['effect3CreateItemId', $this->entry], [DB::OR, ['effect3Id', SpellList::EFFECTS_ITEM_CREATE], ['effect3AuraId', SpellList::AURAS_ITEM_CREATE]]] + ); + if ($ids) + $conditions[] = ['id', $ids]; + + $srcObj = new SpellList($conditions); + if ($srcObj->error) + return false; + + $this->storeJSGlobals($srcObj->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_RELATED)); + [, &$data, , , &$extraCols, , &$visibleCols] = $this->listviewTabs[self::SPELL_CREATED]; + + if (!empty($result)) + $extraCols[] = '$Listview.extraCols.percent'; + + if ($srcObj->hasSetFields('reagent1', 'reagent2', 'reagent3', 'reagent4', 'reagent5', 'reagent6', 'reagent7', 'reagent8')) + $visibleCols[] = 'reagents'; + + foreach ($srcObj->getListviewData() as $id => $row) + $data[] = array_merge($row, $result[$id] ?? ['percent' => -1]); + + return true; + } + + private function handleMailLoot(array $ids, array $result) : bool + { + // quest part + $conditions = array(DB::OR, + ['rewardChoiceItemId1', $this->entry], ['rewardChoiceItemId2', $this->entry], ['rewardChoiceItemId3', $this->entry], ['rewardChoiceItemId4', $this->entry], ['rewardChoiceItemId5', $this->entry], + ['rewardChoiceItemId6', $this->entry], ['rewardItemId1', $this->entry], ['rewardItemId2', $this->entry], ['rewardItemId3', $this->entry], ['rewardItemId4', $this->entry] + ); + if ($ids) + $conditions[] = ['rewardMailTemplateId', $ids]; + + $quests = new QuestList($conditions); + if (!$quests->error) + { + $this->storeJSGlobals($quests->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + [, &$qData, , , , , ] = $this->listviewTabs[self::QUEST_REWARD]; + + foreach ($quests->getListviewData() as $id => $row) + $qData[] = array_merge($row, $result[$id] ?? ['percent' => -1]); + } + + // achievement part + $conditions = array(['itemExtra', $this->entry]); + if ($ar = DB::World()->selectCol('SELECT `ID` FROM achievement_reward WHERE %if', $ids, '`MailTemplateID` IN %in OR %end', $ids, '`ItemID` = %i', $this->entry)) + array_push($conditions, ['id', $ar], DB::OR); + + $achievements = new AchievementList($conditions); + if (!$achievements->error) + { + $this->storeJSGlobals($achievements->getJSGlobals(GLOBALINFO_SELF | GLOBALINFO_REWARDS)); + [, &$aData, , , , &$hiddenCols, &$visibleCols] = $this->listviewTabs[self::ACHIEVEMENT_REWARD]; + + foreach ($achievements->getListviewData() as $id => $row) + $aData[] = array_merge($row, $result[$id] ?? ['percent' => -1]); + + $hiddenCols[] = 'rewards'; + $visibleCols[] = 'category'; + } + + return !$quests->error || !$achievements->error; + } + + private function handleObjectLoot(array $ids, array $result) : bool + { + if (!$ids) + return false; + + $srcObj = new GameObjectList(array(['lootId', $ids])); + if ($srcObj->error) + return false; + + foreach ($srcObj->getListviewData() as $id => $row) + { + $tabId = match($row['type']) + { + 25 => self::OBJECT_FISHED, // fishing node + -3 => self::OBJECT_GATHERED, // herb + -4 => self::OBJECT_MINED, // vein + default => self::OBJECT_CONTAINED // general chest loot + }; + + [, &$tabData, , , &$extraCols, , &$visibleCols] = $this->listviewTabs[$tabId]; + + $tabData[] = array_merge($row, $result[$srcObj->getEntry($id)['lootId']]); + $extraCols[] = '$Listview.extraCols.percent'; + if ($tabId != 15) + $visibleCols[] = 'skill'; + } + + return true; + } +} + +?> diff --git a/includes/game/misc.php b/includes/game/misc.php index 67bfc553..11cc6ab1 100644 --- a/includes/game/misc.php +++ b/includes/game/misc.php @@ -8,15 +8,15 @@ if (!defined('AOWOW_REVISION')) class Game { - public static $resistanceFields = array( + public static array $resistanceFields = array( null, 'resHoly', 'resFire', 'resNature', 'resFrost', 'resShadow', 'resArcane' ); - public static $rarityColorStings = array( // zero-indexed + public static array $rarityColorStings = array( // zero-indexed '9d9d9d', 'ffffff', '1eff00', '0070dd', 'a335ee', 'ff8000', 'e5cc80', 'e6cc80' ); - public static $specIconStrings = array( + public static array $specIconStrings = array( -1 => 'inv_misc_questionmark', 0 => 'spell_nature_elementalabsorption', 6 => ['spell_deathknight_bloodpresence', 'spell_deathknight_frostpresence', 'spell_deathknight_unholypresence' ], @@ -46,9 +46,9 @@ class Game 10 => [ 65, 66, 67, 210, 394, 495, 2817, 3537, 3711, 4024, 4197, 4395, 4742] ); - // zoneorsort for quests need updating + // questSortId for quests need updating // partially points non-instanced area with identical name for instance quests - public static $questSortFix = array( + public static array $questSortFix = array( -221 => 440, // Treasure Map => Tanaris -284 => 0, // Special => Misc (some quests get shuffled into seasonal) 151 => 0, // Designer Island => Misc @@ -86,7 +86,7 @@ class Game 1417 => 1477 // Sunken Temple ); - public static $questSubCats = array( + public static array $questSubCats = array( 1 => [132], // Dun Morogh: Coldridge Valley 12 => [9], // Elwynn Forest: Northshire Valley 141 => [188], // Teldrassil: Shadowglen @@ -109,11 +109,11 @@ class Game ); /* why: - Because petSkills (and ranged weapon skills) are the only ones with more than two skillLines attached. Because Left Joining ?_spell with ?_skillLineability causes more trouble than it has uses. + Because petSkills (and ranged weapon skills) are the only ones with more than two skillLines attached. Because Left Joining ::spell with ::skillLineability causes more trouble than it has uses. Because this is more or less the only reaonable way to fit all that information into one database field, so.. - .. the indizes of this array are bits of skillLine2OrMask in ?_spell if skillLineId1 is negative + .. the indizes of this array are bits of skillLine2OrMask in ::spell if skillLineId1 is negative */ - public static $skillLineMask = array( // idx => [familyId, skillLineId] + public static array $skillLineMask = array( // idx => [familyId, skillLineId] -1 => array( // Pets (Hunter) [ 1, 208], [ 2, 209], [ 3, 203], [ 4, 210], [ 5, 211], [ 6, 212], [ 7, 213], // Wolf, Cat, Spider, Bear, Boar, Crocolisk, Carrion Bird [ 8, 214], [ 9, 215], [11, 217], [12, 218], [20, 236], [21, 251], [24, 653], // Crab, Gorilla, Raptor, Tallstrider, Scorpid, Turtle, Bat @@ -129,31 +129,25 @@ class Game ) ); - public static $sockets = array( // jsStyle Strings + public static array $sockets = array( // jsStyle Strings 'meta', 'red', 'yellow', 'blue' ); - public static function getReputationLevelForPoints($pts) + public static function getReputationLevelForPoints(int $pts) : int { - if ($pts >= 41999) - return REP_EXALTED; - else if ($pts >= 20999) - return REP_REVERED; - else if ($pts >= 8999) - return REP_HONORED; - else if ($pts >= 2999) - return REP_FRIENDLY; - else if ($pts >= 0) - return REP_NEUTRAL; - else if ($pts >= -3000) - return REP_UNFRIENDLY; - else if ($pts >= -6000) - return REP_HOSTILE; - else - return REP_HATED; + return match (true) { + $pts >= 42000 => REP_EXALTED, + $pts >= 21000 => REP_REVERED, + $pts >= 9000 => REP_HONORED, + $pts >= 3000 => REP_FRIENDLY, + $pts >= 0 => REP_NEUTRAL, + $pts >= -3000 => REP_UNFRIENDLY, + $pts >= -6000 => REP_HOSTILE, + default => REP_HATED, + }; } - public static function getTaughtSpells(&$spell) + public static function getTaughtSpells(mixed &$spell) : array { $extraIds = [-1]; // init with -1 to prevent empty-array errors $lookup = [-1]; @@ -180,8 +174,8 @@ class Game // note: omits required spell and chance in skill_discovery_template $data = array_merge( - DB::World()->selectCol('SELECT spellId FROM spell_learn_spell WHERE entry IN (?a)', $lookup), - DB::World()->selectCol('SELECT spellId FROM skill_discovery_template WHERE reqSpell IN (?a)', $lookup), + DB::World()->selectCol('SELECT spellId FROM spell_learn_spell WHERE entry IN %in', $lookup), + DB::World()->selectCol('SELECT spellId FROM skill_discovery_template WHERE reqSpell IN %in', $lookup), $extraIds ); @@ -196,7 +190,7 @@ class Game $pages = []; while ($ptId) { - if ($row = DB::World()->selectRow('SELECT ptl.`Text` AS Text_loc?d, pt.* FROM page_text pt LEFT JOIN page_text_locale ptl ON pt.`ID` = ptl.`ID` AND locale = ? WHERE pt.`ID` = ?d', Lang::getLocale()->value, Lang::getLocale()->json(), $ptId)) + if ($row = DB::World()->selectRow('SELECT ptl.`Text` AS Text_loc%i, pt.* FROM page_text pt LEFT JOIN page_text_locale ptl ON pt.`ID` = ptl.`ID` AND locale = %s WHERE pt.`ID` = %i', Lang::getLocale()->value, Lang::getLocale()->json(), $ptId)) { $ptId = $row['NextPageID']; $pages[] = Util::localizedString($row, 'Text'); @@ -216,20 +210,21 @@ class Game $quotes = []; $soundIds = []; - $quoteSrc = DB::World()->select( + $quoteSrc = DB::World()->selectAssoc( 'SELECT ct.`GroupID` AS ARRAY_KEY, ct.`ID` AS ARRAY_KEY2, ct.`Type` AS "talkType", ct.TextRange AS "range", IFNULL(bct.`LanguageID`, ct.`Language`) AS "lang", IFNULL(NULLIF(bct.`Text`, ""), IFNULL(NULLIF(bct.`Text1`, ""), IFNULL(ct.`Text`, ""))) AS "text_loc0", - { IFNULL(NULLIF(bctl.`Text`, ""), IFNULL(NULLIF(bctl.`Text1`, ""), IFNULL(ctl.`Text`, ""))) AS text_loc?d, } + %if', Lang::getLocale()->value, + 'IFNULL(NULLIF(bctl.`Text`, ""), IFNULL(NULLIF(bctl.`Text1`, ""), IFNULL(ctl.`Text`, ""))) AS text_loc%i,', Lang::getLocale()->value, + '%end IF(bct.`SoundEntriesID` > 0, bct.`SoundEntriesID`, ct.`Sound`) AS "soundId" FROM creature_text ct - { LEFT JOIN creature_text_locale ctl ON ct.`CreatureID` = ctl.`CreatureID` AND ct.`GroupID` = ctl.`GroupID` AND ct.`ID` = ctl.`ID` AND ctl.`Locale` = ? } LEFT JOIN broadcast_text bct ON ct.`BroadcastTextId` = bct.`ID` - { LEFT JOIN broadcast_text_locale bctl ON ct.`BroadcastTextId` = bctl.`ID` AND bctl.`locale` = ? } - WHERE ct.`CreatureID` = ?d', - Lang::getLocale()->value ?: DBSIMPLE_SKIP, - Lang::getLocale()->value ? Lang::getLocale()->json() : DBSIMPLE_SKIP, - Lang::getLocale()->value ? Lang::getLocale()->json() : DBSIMPLE_SKIP, + %if', Lang::getLocale()->value, + 'LEFT JOIN creature_text_locale ctl ON ct.`CreatureID` = ctl.`CreatureID` AND ct.`GroupID` = ctl.`GroupID` AND ct.`ID` = ctl.`ID` AND ctl.`Locale` = %s', Lang::getLocale()->json(), + 'LEFT JOIN broadcast_text_locale bctl ON ct.`BroadcastTextId` = bctl.`ID` AND bctl.`locale` = %s', Lang::getLocale()->json(), + '%end + WHERE ct.`CreatureID` = %i', $creatureId ); @@ -268,7 +263,7 @@ class Game }; // prefix - $prefix = '%s '; + $prefix = ''; if ($t['talkType'] != 4) $prefix = ($talkSource ?: '%s').' '.Lang::npc('textTypes', $t['talkType']).Lang::main('colon').($t['lang'] ? '['.Lang::game('languages', $t['lang']).'] ' : ' '); @@ -338,7 +333,7 @@ class Game public static function getEnchantmentCondition(int $conditionId, bool $interactive = false) : string { - $gemCnd = DB::Aowow()->selectRow('SELECT * FROM ?_itemenchantmentcondition WHERE `id` = ?d', $conditionId); + $gemCnd = DB::Aowow()->selectRow('SELECT * FROM ::itemenchantmentcondition WHERE `id` = %i', $conditionId); if (!$gemCnd) return ''; diff --git a/includes/game/worldposition.class.php b/includes/game/worldposition.class.php index 9f8d4ff3..df13aaf6 100644 --- a/includes/game/worldposition.class.php +++ b/includes/game/worldposition.class.php @@ -8,6 +8,7 @@ if (!defined('AOWOW_REVISION')) abstract class WorldPosition { + private static array $zoneMapCache = []; private static array $alphaMapCache = []; private static array $capitalCities = array( // capitals take precedence over their surrounding area 1497, 1637, 1638, 3487, // Undercity, Ogrimmar, Thunder Bluff, Silvermoon City @@ -67,7 +68,7 @@ abstract class WorldPosition // spawn does not really match on a map, but we need at least one result if (!$result) { - usort($points, function ($a, $b) { return ($a['dist'] < $b['dist']) ? -1 : 1; }); + usort($points, fn($a, $b) => $a['dist'] <=> $b['dist']); $result = [1.0, $points[0]]; } @@ -81,25 +82,25 @@ abstract class WorldPosition switch ($type) { case Type::NPC: - $result = DB::World()->select('SELECT `guid` AS ARRAY_KEY, `id`, `map` AS `mapId`, `position_x` AS `posX`, `position_y` AS `posY` FROM creature WHERE `guid` IN (?a)', $guids); + $result = DB::World()->selectAssoc('SELECT `guid` AS ARRAY_KEY, `id`, `map` AS `mapId`, `position_x` AS `posX`, `position_y` AS `posY` FROM creature WHERE `guid` IN %in', $guids); break; case Type::OBJECT: - $result = DB::World()->select('SELECT `guid` AS ARRAY_KEY, `id`, `map` AS `mapId`, `position_x` AS `posX`, `position_y` AS `posY` FROM gameobject WHERE `guid` IN (?a)', $guids); + $result = DB::World()->selectAssoc('SELECT `guid` AS ARRAY_KEY, `id`, `map` AS `mapId`, `position_x` AS `posX`, `position_y` AS `posY` FROM gameobject WHERE `guid` IN %in', $guids); break; case Type::SOUND: - $result = DB::AoWoW()->select('SELECT `id` AS ARRAY_KEY, `soundId` AS `id`, `mapId`, `posX`, `posY` FROM ?_soundemitters WHERE `id` IN (?a)', $guids); + $result = DB::AoWoW()->selectAssoc('SELECT `id` AS ARRAY_KEY, `soundId` AS `id`, `mapId`, `posX`, `posY` FROM ::soundemitters WHERE `id` IN %in', $guids); break; case Type::ZONE: - $result = DB::Aowow()->select('SELECT -`id` AS ARRAY_KEY, `id`, `parentMapId` AS `mapId`, `parentX` AS `posX`, `parentY` AS `posY` FROM ?_zones WHERE -`id` IN (?a)', $guids); + $result = DB::Aowow()->selectAssoc('SELECT -`id` AS ARRAY_KEY, `id`, `parentMapId` AS `mapId`, `parentX` AS `posX`, `parentY` AS `posY` FROM ::zones WHERE -`id` IN %in', $guids); break; case Type::AREATRIGGER: $result = []; if ($base = array_filter($guids, fn($x) => $x > 0)) - $result = array_replace($result, DB::AoWoW()->select('SELECT `id` AS ARRAY_KEY, `id`, `mapId`, `posX`, `posY` FROM ?_areatrigger WHERE `id` IN (?a)', $base)); + $result = array_replace($result, DB::AoWoW()->selectAssoc('SELECT `id` AS ARRAY_KEY, `id`, `mapId`, `posX`, `posY` FROM ::areatrigger WHERE `id` IN %in', $base)); if ($endpoints = array_filter($guids, fn($x) => $x < 0)) - $result = array_replace($result, DB::World()->select( - 'SELECT -`ID` AS ARRAY_KEY, ID AS `id`, `target_map` AS `mapId`, `target_position_x` AS `posX`, `target_position_y` AS `posY` FROM areatrigger_teleport WHERE -`id` IN (?a) UNION - SELECT -`entryorguid` AS ARRAY_KEY, entryorguid AS `id`, `action_param1` AS `mapId`, `target_x` AS `posX`, `target_y` AS `posY` FROM smart_scripts WHERE -`entryorguid` IN (?a) AND `source_type` = ?d AND `action_type` = ?d', + $result = array_replace($result, DB::World()->selectAssoc( + 'SELECT -`ID` AS ARRAY_KEY, ID AS `id`, `target_map` AS `mapId`, `target_position_x` AS `posX`, `target_position_y` AS `posY` FROM areatrigger_teleport WHERE -`id` IN %in UNION + SELECT -`entryorguid` AS ARRAY_KEY, entryorguid AS `id`, `action_param1` AS `mapId`, `target_x` AS `posX`, `target_y` AS `posY` FROM smart_scripts WHERE -`entryorguid` IN %in AND `source_type` = %i AND `action_type` = %i', $endpoints, $endpoints, SmartAI::SRC_TYPE_AREATRIGGER, SmartAction::ACTION_TELEPORT )); break; @@ -118,42 +119,77 @@ abstract class WorldPosition if (!$mapId < 0) return []; - $query = + if (!isset(self::$zoneMapCache[$mapId])) + self::initZoneMaps($mapId); + + $points = []; + for ($i = 0; $i < 2; $i++) + { + foreach (self::$zoneMapCache[$mapId] as $area) + { + if (!$i && $preferedAreaId != 0 && $area['areaId'] != $preferedAreaId) + continue; + + if (!$i && $preferedFloor >= 0 && $area['floor'] != $preferedFloor) + continue; + + if ($mapX < $area['minX'] || $mapX > $area['maxX'] || + $mapY < $area['minY'] || $mapY > $area['maxY']) + continue; + + // dist BETWEEN 0 (center) AND 70.7 (corner) + $posX = round(($area['maxY'] - $mapY) * 100 / ($area['maxY'] - $area['minY']), 1); + $posY = round(($area['maxX'] - $mapX) * 100 / ($area['maxX'] - $area['minX']), 1); + $dist = sqrt(pow(abs($posX - 50), 2) + pow(abs($posY - 50), 2)); + + $points[] = array( + 'id' => $area['id'], + 'areaId' => $area['areaId'], + 'floor' => $area['floor'], + 'multifloor' => $area['multifloor'], + 'srcPrio' => $area['srcPrio'], + 'posX' => $posX, + 'posY' => $posY, + 'dist' => $dist + ); + } + + // retry: pre-instance subareas belong to the instance-maps but are displayed on the outside. There also cases where the zone reaches outside it's own map. + if ($points) + break; + } + + // sort by srcPrio DESC (primary), dist ASC (secondary) + usort($points, fn($a, $b) => ($b['srcPrio'] <=> $a['srcPrio']) ?: ($a['dist'] <=> $b['dist'])); + + return $points; + } + + private static function initZoneMaps(int $mapId) : void + { + self::$zoneMapCache[$mapId] = DB::Aowow()->selectAssoc( 'SELECT x.`id`, x.`areaId`, + x.`minX`, x.`maxX`, x.`minY`, x.`maxY`, IF(x.`defaultDungeonMapId` < 0, x.`floor` + 1, x.`floor`) AS `floor`, - IF(dm.`id` IS NOT NULL OR x.`defaultDungeonMapId` < 0, 1, 0) AS `multifloor`, - ROUND((x.`maxY` - ?d) * 100 / (x.`maxY` - x.`minY`), 1) AS `posX`, - ROUND((x.`maxX` - ?d) * 100 / (x.`maxX` - x.`minX`), 1) AS `posY`, - SQRT(POWER(ABS((x.`maxY` - ?d) * 100 / (x.`maxY` - x.`minY`) - 50), 2) + - POWER(ABS((x.`maxX` - ?d) * 100 / (x.`maxX` - x.`minX`) - 50), 2)) AS `dist` + IF(useDM.`id` IS NOT NULL OR x.`defaultDungeonMapId` < 0, 1, 0) AS `srcPrio`, + IF(multiDM.`id` IS NOT NULL OR x.`defaultDungeonMapId` < 0, 1, 0) AS `multifloor` FROM - (SELECT 0 AS `id`, `areaId`, `mapId`, `right` AS `minY`, `left` AS `maxY`, `top` AS `maxX`, `bottom` AS `minX`, 0 AS `floor`, 0 AS `worldMapAreaId`, `defaultDungeonMapId` FROM ?_worldmaparea wma UNION - SELECT dm.`id`, `areaId`, wma.`mapId`, `minY`, `maxY`, `maxX`, `minX`, `floor`, `worldMapAreaId`, `defaultDungeonMapId` FROM ?_worldmaparea wma - JOIN ?_dungeonmap dm ON dm.`mapId` = wma.`mapId` WHERE wma.`mapId` NOT IN (0, 1, 530, 571) OR wma.`areaId` = 4395) x + (SELECT 0 AS `id`, `areaId`, `mapId`, `right` AS `minY`, `left` AS `maxY`, `top` AS `maxX`, `bottom` AS `minX`, 0 AS `floor`, 0 AS `worldMapAreaId`, `defaultDungeonMapId` FROM aowow_worldmaparea wma UNION + SELECT dm.`id`, `areaId`, wma.`mapId`, `minY`, `maxY`, `maxX`, `minX`, `floor`, `worldMapAreaId`, `defaultDungeonMapId` FROM aowow_worldmaparea wma + JOIN aowow_dungeonmap dm ON dm.`mapId` = wma.`mapId` WHERE wma.`mapId` NOT IN (0, 1, 530, 571) OR wma.`areaId` = 4395) x LEFT JOIN - ?_dungeonmap dm ON dm.`mapId` = x.`mapId` AND dm.`worldMapAreaId` = x.`worldMapAreaId` AND dm.`floor` <> x.`floor` AND dm.`worldMapAreaId` > 0 + aowow_dungeonmap useDM ON useDM.`mapId` = x.`mapId` AND useDM.`worldMapAreaId` = x.`worldMapAreaId` AND useDM.`floor` = x.`floor` AND useDM.`worldMapAreaId` > 0 + LEFT JOIN + aowow_dungeonmap multiDM ON multiDM.`mapId` = x.`mapId` AND multiDM.`worldMapAreaId` = x.`worldMapAreaId` AND multiDM.`floor` <> x.`floor` AND multiDM.`worldMapAreaId` > 0 WHERE - x.`mapId` = ?d AND IF(?d, x.`areaId` = ?d, x.`areaId` <> 0){ AND x.`floor` = ?d - IF(x.`defaultDungeonMapId` < 0, 1, 0)} + x.`mapId` = %i AND x.`areaId` <> 0 AND + x.`minX` <> 0 AND x.`maxX` <> 0 AND x.`minY` <> 0 AND x.`maxY` <> 0 GROUP BY - x.`id`, x.`areaId` - HAVING - (`posX` BETWEEN 0.1 AND 99.9 AND `posY` BETWEEN 0.1 AND 99.9) - ORDER BY - `multifloor` DESC, `dist` ASC'; - - // dist BETWEEN 0 (center) AND 70.7 (corner) - $points = DB::Aowow()->select($query, $mapY, $mapX, $mapY, $mapX, $mapId, $preferedAreaId, $preferedAreaId, $preferedFloor < 0 ? DBSIMPLE_SKIP : $preferedFloor); - if (!$points) // retry: pre-instance subareas belong to the instance-maps but are displayed on the outside. There also cases where the zone reaches outside it's own map. - $points = DB::Aowow()->select($query, $mapY, $mapX, $mapY, $mapX, $mapId, 0, 0, DBSIMPLE_SKIP); - if (!is_array($points)) - { - trigger_error('WorldPosition::toZonePos - query failed', E_USER_ERROR); - return []; - } - - return $points; + x.`id`, x.`areaId`', + $mapId + ) ?: []; } } diff --git a/includes/kernel.php b/includes/kernel.php index cb4c6ac4..6afe3dbb 100644 --- a/includes/kernel.php +++ b/includes/kernel.php @@ -3,10 +3,11 @@ namespace Aowow; mb_internal_encoding('UTF-8'); +mb_substitute_character('none'); // drop invalid chars entirely instead of replacing them with '?' error_reporting(E_ALL); mysqli_report(MYSQLI_REPORT_ERROR); -define('AOWOW_REVISION', 41); +define('AOWOW_REVISION', 47); define('OS_WIN', substr(PHP_OS, 0, 3) == 'WIN'); // OS_WIN as per compile info of php define('CLI', PHP_SAPI === 'cli'); define('CLI_HAS_E', CLI && // WIN10 and later usually support ANSI escape sequences @@ -32,8 +33,9 @@ if ($error) require_once 'includes/defines.php'; require_once 'includes/locale.class.php'; require_once 'localization/lang.class.php'; -require_once 'includes/libs/DbSimple/Generic.php'; // Libraray: http://en.dklab.ru/lib/DbSimple (using variant: https://github.com/ivan1986/DbSimple/tree/master) -require_once 'includes/database.class.php'; // wrap DBSimple +require_once 'localization/datetime.class.php'; +require_once 'includes/libs/autoload.php'; // Composer libraries +require_once 'includes/database.php'; // wrap dg/dibi (https://https://dibi.nette.org/) require_once 'includes/utilities.php'; // helper functions require_once 'includes/type.class.php'; // DB types storage and factory require_once 'includes/cfg.class.php'; // Config holder @@ -53,6 +55,8 @@ spl_autoload_register(function (string $class) : void require_once 'includes/game/chrstatistics.php'; else if (file_exists('includes/game/'.strtolower($class).'.class.php')) require_once 'includes/game/'.strtolower($class).'.class.php'; + else if (file_exists('includes/game/loot/'.strtolower($class).'.class.php')) + require_once 'includes/game/loot/'.strtolower($class).'.class.php'; }); // our site components @@ -129,6 +133,10 @@ set_error_handler(function(int $errNo, string $errStr, string $errFile, int $err if (strstr($errStr, 'mysqli_connect') && $errNo == E_WARNING) return true; + // do not log XDebug shenanigans + if (strstr($errFile, 'xdebug://')) + return true; + // we do not log deprecation notices if ($errNo & (E_DEPRECATED | E_USER_DEPRECATED)) return true; @@ -149,15 +157,23 @@ set_error_handler(function(int $errNo, string $errStr, string $errFile, int $err default => 'UNKNOWN_ERROR' // errors not in this list can not be handled by set_error_handler (as per documentation) or are ignored }; + if (!empty($_POST['password'])) + $_POST['password'] = '******'; + if (!empty($_POST['c_password'])) + $_POST['c_password'] = '******'; + if (DB::isConnected(DB_AOWOW)) - DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', + DB::Aowow()->qry('INSERT INTO ::errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), %i, %i, %s, %i, %s, %s, %i, %s) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', AOWOW_REVISION, $errNo, $errFile, $errLine, CLI ? 'CLI' : substr($_SERVER['QUERY_STRING'] ?? '', 0, 250), empty($_POST) ? '' : http_build_query($_POST), User::$groups, $errStr ); - if (CLI) - CLI::write($errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine, $logLevel); + $logMsg = $errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine; + if (CLI && class_exists(__NAMESPACE__.'\CLI')) + CLI::write($logMsg, $logLevel); + else if (CLI) + fwrite(STDERR, $logMsg); else if (Cfg::get('DEBUG') >= $logLevel) - Util::addNote($errName.' - '.$errStr.' @ '.$errFile. ':'.$errLine, U_GROUP_EMPLOYEE, $logLevel); + Util::addNote($logMsg, U_GROUP_EMPLOYEE, $logLevel); return true; }, E_ALL); @@ -165,8 +181,13 @@ set_error_handler(function(int $errNo, string $errStr, string $errFile, int $err // handle exceptions set_exception_handler(function (\Throwable $e) : void { + if (!empty($_POST['password'])) + $_POST['password'] = '******'; + if (!empty($_POST['c_password'])) + $_POST['c_password'] = '******'; + if (DB::isConnected(DB_AOWOW)) - DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', + DB::Aowow()->qry('INSERT INTO ::errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), %i, %i, %s, %i, %s, %s, %i, %s) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', AOWOW_REVISION, $e->getCode(), $e->getFile(), $e->getLine(), CLI ? 'CLI' : substr($_SERVER['QUERY_STRING'] ?? '', 0, 250), empty($_POST) ? '' : http_build_query($_POST), User::$groups, $e->getMessage() ); @@ -188,8 +209,13 @@ register_shutdown_function(function() : void if ($e = error_get_last()) { + if (!empty($_POST['password'])) + $_POST['password'] = '******'; + if (!empty($_POST['c_password'])) + $_POST['c_password'] = '******'; + if (DB::isConnected(DB_AOWOW)) - DB::Aowow()->query('INSERT INTO ?_errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), ?d, ?d, ?, ?d, ?, ?, ?d, ?) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', + DB::Aowow()->qry('INSERT INTO ::errors (`date`, `version`, `phpError`, `file`, `line`, `query`, `post`, `userGroups`, `message`) VALUES (UNIX_TIMESTAMP(), %i, %i, %s, %i, %s, %s, %i, %s) ON DUPLICATE KEY UPDATE `date` = UNIX_TIMESTAMP()', AOWOW_REVISION, $e['type'], $e['file'], $e['line'], CLI ? 'CLI' : substr($_SERVER['QUERY_STRING'] ?? '', 0, 250), empty($_POST) ? '' : http_build_query($_POST), User::$groups, $e['message'] ); @@ -257,20 +283,6 @@ if (!CLI) Lang::load($loc); else Lang::load(User::$preferedLoc); - - // set up some logging (some queries will execute before we init the user and load the config) - if (Cfg::get('DEBUG') >= LOG_LEVEL_INFO && User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) - { - DB::Aowow()->setLogger(DB::profiler(...)); - DB::World()->setLogger(DB::profiler(...)); - if (DB::isConnected(DB_AUTH)) - DB::Auth()->setLogger(DB::profiler(...)); - - if (!empty($AoWoWconf['characters'])) - foreach ($AoWoWconf['characters'] as $idx => $__) - if (DB::isConnected(DB_CHARACTERS . $idx)) - DB::Characters($idx)->setLogger(DB::profiler(...)); - } } ?> diff --git a/includes/libs/DbSimple/CacherImpl.php b/includes/libs/DbSimple/CacherImpl.php deleted file mode 100644 index 986cb130..00000000 --- a/includes/libs/DbSimple/CacherImpl.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php -/** - * Created by JetBrains PhpStorm. - * User: sib - * Date: 14.10.13 - * Time: 17:40 - * To change this template use File | Settings | File Templates. - */ - -class CacherImpl implements Zend_Cache_Backend_Interface { - - protected $callback; - - public function __construct($callback) { - if ( is_callable($callback) ) { - $this->callback = $callback; - } else { - $this->callback = $this->callbackDummy; - } - } - - public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) {} - - public function remove($id) {} - - public function test($id) {} - - public function save($data, $id, $tags = array(), $specificLifetime = false) - { - return call_user_func($this->callback, $id, $data); - } - - public function load($id, $doNotTestCacheValidity = false) - { - return call_user_func($this->callback, $id); - } - - public function setDirectives($directives) {} - - protected function callbackDummy($k, $v) - { - return null; - } - -} // CacherImpl class \ No newline at end of file diff --git a/includes/libs/DbSimple/Connect.php b/includes/libs/DbSimple/Connect.php deleted file mode 100644 index b9d19c62..00000000 --- a/includes/libs/DbSimple/Connect.php +++ /dev/null @@ -1,261 +0,0 @@ -<?php - -/** - * Используйте константу DBSIMPLE_SKIP в качестве подстановочного значения чтобы пропустить опцональный SQL блок. - */ -define('DBSIMPLE_SKIP', log(0)); -/** - * Имена специализированных колонок в резальтате, - * которые используются как ключи в результирующем массиве - */ -define('DBSIMPLE_ARRAY_KEY', 'ARRAY_KEY'); // hash-based resultset support -define('DBSIMPLE_PARENT_KEY', 'PARENT_KEY'); // forrest-based resultset support - -/** - * Класс обертка для DbSimple - * - * <br>нужен для ленивой инициализации коннекта к базе - * - * @package DbSimple - * @method mixed transaction(string $mode=null) - * @method mixed commit() - * @method mixed rollback() - * @method mixed select(string $query [, $arg1] [,$arg2] ...) - * @method mixed selectRow(string $query [, $arg1] [,$arg2] ...) - * @method array selectCol(string $query [, $arg1] [,$arg2] ...) - * @method string selectCell(string $query [, $arg1] [,$arg2] ...) - * @method mixed query(string $query [, $arg1] [,$arg2] ...) - * @method string escape(mixed $s, bool $isIdent=false) - * @method DbSimple_SubQuery subquery(string $query [, $arg1] [,$arg2] ...) - * @method callback setLogger(callback $logger) - * @method callback setCacher(callback $cacher) - * @method string setIdentPrefix($prx) - * @method string setCachePrefix($prx) - */ -class DbSimple_Connect -{ - /** @var DbSimple_Generic_Database База данных */ - protected $DbSimple; - /** @var string DSN подключения */ - protected $DSN; - /** @var string Тип базы данных */ - protected $shema; - /** @var array Что выставить при коннекте */ - protected $init; - /** @var integer код ошибки */ - public $error = null; - /** @var string сообщение об ошибке */ - public $errmsg = null; - - /** - * Конструктор только запоминает переданный DSN - * создание класса и коннект происходит позже - * - * @param string $dsn DSN строка БД - */ - public function __construct($dsn) - { - $this->DbSimple = null; - $this->DSN = $dsn; - $this->init = array(); - $this->shema = ucfirst(substr($dsn, 0, strpos($dsn, ':'))); - } - - /** - * Взять базу из пула коннектов - * - * @param string $dsn DSN строка БД - * @return DbSimple_Connect - */ - public static function get($dsn) - { - static $pool = array(); - return isset($pool[$dsn]) ? $pool[$dsn] : $pool[$dsn] = new self($dsn); - } - - /** - * Возвращает тип базы данных - * - * @return string имя типа БД - */ - public function getShema() - { - return $this->shema; - } - - /** - * Коннект при первом запросе к базе данных - */ - public function __call($method, $params) - { - if ($this->DbSimple === null) - $this->connect($this->DSN); - return call_user_func_array(array(&$this->DbSimple, $method), $params); - } - - /** - * mixed selectPage(int &$total, string $query [, $arg1] [,$arg2] ...) - * Функцию нужно вызвать отдельно из-за передачи по ссылке - */ - public function selectPage(&$total, $query) - { - if ($this->DbSimple === null) - $this->connect($this->DSN); - $args = func_get_args(); - $args[0] = &$total; - return call_user_func_array(array(&$this->DbSimple, 'selectPage'), $args); - } - - /** - * Подключение к базе данных - * @param string $dsn DSN строка БД - */ - protected function connect($dsn) - { - $parsed = $this->parseDSN($dsn); - if (!$parsed) - $this->errorHandler('Ошибка разбора строки DSN', $dsn); - if (!isset($parsed['scheme'])) - $this->errorHandler('Невозможно загрузить драйвер базы данных', $parsed); - $this->shema = ucfirst($parsed['scheme']); - require_once __DIR__.'/'.$this->shema.'.php'; - $class = 'DbSimple_'.$this->shema; - $this->DbSimple = new $class($parsed); - $this->errmsg = &$this->DbSimple->errmsg; - $this->error = &$this->DbSimple->error; - $prefix = isset($parsed['prefix']) ? $parsed['prefix'] : ($this->_identPrefix ? $this->_identPrefix : false); - if ($prefix) - $this->DbSimple->setIdentPrefix($prefix); - if ($this->_cachePrefix) $this->DbSimple->setCachePrefix($this->_cachePrefix); - if ($this->_cacher) $this->DbSimple->setCacher($this->_cacher); - if ($this->_logger) $this->DbSimple->setLogger($this->_logger); - $this->DbSimple->setErrorHandler($this->errorHandler!==null ? $this->errorHandler : array(&$this, 'errorHandler')); - //выставление переменных - foreach($this->init as $query) - call_user_func_array(array(&$this->DbSimple, 'query'), $query); - $this->init = array(); - } - - /** - * Функция обработки ошибок - стандартный обработчик - * Все вызовы без @ прекращают выполнение скрипта - * - * @param string $msg Сообщение об ошибке - * @param array $info Подробная информация о контексте ошибки - */ - public function errorHandler($msg, $info) - { - // Если использовалась @, ничего не делать. - if (!error_reporting()) return; - // Выводим подробную информацию об ошибке. - echo "SQL Error: $msg<br><pre>"; - print_r($info); - echo "</pre>"; - exit(); - } - - /** - * Выставляет запрос для инициализации - * - * @param string $query запрос - */ - public function addInit(...$args) - { - if ($this->DbSimple !== null) - return call_user_func_array(array(&$this->DbSimple, 'query'), $args); - $this->init[] = $args; - } - - /** - * Устанавливает новый обработчик ошибок - * Обработчик получает 2 аргумента: - * - сообщение об ошибке - * - массив (код, сообщение, запрос, контекст) - * - * @param callback|null|false $handler обработчик ошибок - * <br> null - по умолчанию - * <br> false - отключен - * @return callback|null|false предыдущий обработчик - */ - public function setErrorHandler($handler) - { - $prev = $this->errorHandler; - $this->errorHandler = $handler; - if ($this->DbSimple) - $this->DbSimple->setErrorHandler($handler); - return $prev; - } - - /** @var callback обработчик ошибок */ - private $errorHandler = null; - private $_cachePrefix = ''; - private $_identPrefix = null; - private $_logger = null; - private $_cacher = null; - - /** - * callback setLogger(callback $logger) - * Set query logger called before each query is executed. - * Returns previous logger. - */ - public function setLogger($logger) - { - $prev = $this->_logger; - $this->_logger = $logger; - if ($this->DbSimple) - $this->DbSimple->setLogger($logger); - return $prev; - } - - /** - * callback setCacher(callback $cacher) - * Set cache mechanism called during each query if specified. - * Returns previous handler. - */ - public function setCacher(Zend_Cache_Backend_Interface $cacher=null) - { - $prev = $this->_cacher; - $this->_cacher = $cacher; - if ($this->DbSimple) - $this->DbSimple->setCacher($cacher); - return $prev; - } - - /** - * string setIdentPrefix($prx) - * Set identifier prefix used for $_ placeholder. - */ - public function setIdentPrefix($prx) - { - $old = $this->_identPrefix; - if ($prx !== null) $this->_identPrefix = $prx; - if ($this->DbSimple) - $this->DbSimple->setIdentPrefix($prx); - return $old; - } - - /** - * string setCachePrefix($prx) - * Set cache prefix used in key caclulation. - */ - public function setCachePrefix($prx) - { - $old = $this->_cachePrefix; - if ($prx !== null) $this->_cachePrefix = $prx; - if ($this->DbSimple) - $this->DbSimple->setCachePrefix($prx); - return $old; - } - - /** - * Разбирает строку DSN в массив параметров подключения к базе - * - * @param string $dsn строка DSN для разбора - * @return array Параметры коннекта - */ - protected function parseDSN($dsn) - { - return DbSimple_Generic::parseDSN($dsn); - } - -} diff --git a/includes/libs/DbSimple/Database.php b/includes/libs/DbSimple/Database.php deleted file mode 100644 index d6a6a5bf..00000000 --- a/includes/libs/DbSimple/Database.php +++ /dev/null @@ -1,1406 +0,0 @@ -<?php - -/** - * DbSimple_Database: Base class for all databases. - * (C) Dk Lab, http://en.dklab.ru - * - * 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 2.1 of the License, or (at your option) any later version. - * See http://www.gnu.org/copyleft/lesser.html - * - * Use static DbSimple_Generic::connect($dsn) call if you don't know - * database type and parameters, but have its DSN. - * - * Additional keys can be added by appending a URI query string to the - * end of the DSN. - * - * The format of the supplied DSN is in its fullest form: - * phptype(dbsyntax)://username:password@protocol+hostspec/database?option=8&another=true - * - * Most variations are allowed: - * phptype://username:password@protocol+hostspec:110//usr/db_file.db?mode=0644 - * phptype://username:password@hostspec/database_name - * phptype://username:password@hostspec - * phptype://username@hostspec - * phptype://hostspec/database - * phptype://hostspec - * phptype(dbsyntax) - * phptype - * - * Parsing code is partially grabbed from PEAR DB class, - * initial author: Tomas V.V.Cox <cox@idecnet.com>. - * - * Contains 3 classes: - * - DbSimple_Database: common database methods - * - DbSimple_Blob: common BLOB support - * - DbSimple_LastError: error reporting and tracking - * - * Special result-set fields: - * - ARRAY_KEY* ("*" means "anything") - * - PARENT_KEY - * - * Transforms: - * - GET_ATTRIBUTES - * - CALC_TOTAL - * - GET_TOTAL - * - UNIQ_KEY - * - * Query attributes: - * - BLOB_OBJ - * - CACHE - * - * @author Dmitry Koterov, http://forum.dklab.ru/users/DmitryKoterov/ - * @author Konstantin Zhinko, http://forum.dklab.ru/users/KonstantinGinkoTit/ - * @author Ivan Borzenkov, http://forum.dklab.ru/users/Ivan1986/ - * - * @version 2.x $Id$ - */ - -/** - * Use this constant as placeholder value to skip optional SQL block [...]. - */ -if (!defined('DBSIMPLE_SKIP')) - define('DBSIMPLE_SKIP', log(0)); - -/** - * Names of special columns in result-set which is used - * as array key (or karent key in forest-based resultsets) in - * resulting hash. - */ -if (!defined('DBSIMPLE_ARRAY_KEY')) - define('DBSIMPLE_ARRAY_KEY', 'ARRAY_KEY'); // hash-based resultset support -if (!defined('DBSIMPLE_PARENT_KEY')) - define('DBSIMPLE_PARENT_KEY', 'PARENT_KEY'); // forrest-based resultset support - - -if ( !interface_exists('Zend_Cache_Backend_Interface', false) ) { - require_once __DIR__ . '/Zend/Cache.php'; - require_once __DIR__ . '/Zend/Cache/Backend/Interface.php'; -} - -require_once __DIR__ . '/CacherImpl.php'; - - -/** - * - * Base class for all databases. - * Can create transactions and new BLOBs, parse DSNs. - * - * Logger is COMMON for multiple transactions. - * Error handler is private for each transaction and database. - */ -abstract class DbSimple_Database extends DbSimple_LastError -{ - private $attributes; - - /** - * Public methods. - */ - - /** - * object blob($blob_id) - * Create new blob - */ - public function blob($blob_id = null) - { - $this->_resetLastError(); - return $this->_performNewBlob($blob_id); - } - - /** - * void transaction($mode) - * Create new transaction. - */ - public function transaction($mode=null) - { - $this->_resetLastError(); - $this->_logQuery('-- START TRANSACTION '.$mode); - return $this->_performTransaction($mode); - } - - /** - * mixed commit() - * Commit the transaction. - */ - public function commit() - { - $this->_resetLastError(); - $this->_logQuery('-- COMMIT'); - return $this->_performCommit(); - } - - /** - * mixed rollback() - * Rollback the transaction. - */ - public function rollback() - { - $this->_resetLastError(); - $this->_logQuery('-- ROLLBACK'); - return $this->_performRollback(); - } - - /** - * mixed select(string $query [, $arg1] [,$arg2] ...) - * Execute query and return the result. - */ - public function select(...$args) - { - $total = false; - return $this->_query($args, $total); - } - - /** - * mixed selectPage(int &$total, string $query [, $arg1] [,$arg2] ...) - * Execute query and return the result. - * Total number of found rows (independent to LIMIT) is returned in $total - * (in most cases second query is performed to calculate $total). - */ - public function selectPage(&$total, ...$args) - { - $total = true; - return $this->_query($args, $total); - } - - /** - * hash selectRow(string $query [, $arg1] [,$arg2] ...) - * Return the first row of query result. - * On errors return false and set last error. - * If no one row found, return array()! It is useful while debugging, - * because PHP DOES NOT generates notice on $row['abc'] if $row === null - * or $row === false (but, if $row is empty array, notice is generated). - */ - public function selectRow(...$args) - { - $total = false; - $rows = $this->_query($args, $total); - if (!is_array($rows)) return $rows; - if (!count($rows)) return array(); - reset($rows); - return current($rows); - } - - /** - * array selectCol(string $query [, $arg1] [,$arg2] ...) - * Return the first column of query result as array. - */ - public function selectCol(...$args) - { - $total = false; - $rows = $this->_query($args, $total); - if (!is_array($rows)) return $rows; - $this->_shrinkLastArrayDimensionCallback($rows); - return $rows; - } - - /** - * scalar selectCell(string $query [, $arg1] [,$arg2] ...) - * Return the first cell of the first column of query result. - * If no one row selected, return null. - */ - public function selectCell(...$args) - { - $total = false; - $rows = $this->_query($args, $total); - if (!is_array($rows)) return $rows; - if (!count($rows)) return null; - reset($rows); - $row = current($rows); - if (!is_array($row)) return $row; - reset($row); - return current($row); - } - - /** - * mixed query(string $query [, $arg1] [,$arg2] ...) - * Alias for select(). May be used for INSERT or UPDATE queries. - */ - public function query(...$args) - { - $total = false; - return $this->_query($args, $total); - } - - /** - * string escape(mixed $s, bool $isIdent=false) - * Enclose the string into database quotes correctly escaping - * special characters. If $isIdent is true, value quoted as identifier - * (e.g.: `value` in MySQL, "value" in Firebird, [value] in MSSQL). - */ - public function escape($s, $isIdent=false) - { - return $this->_performEscape($s, $isIdent); - } - - - /** - * DbSimple_SubQuery subquery(string $query [, $arg1] [,$arg2] ...) - * Выполняет разворачивание плейсхолдеров без коннекта к базе - * Нужно для сложных запросов, состоящих из кусков, которые полезно сохранить - * - */ - public function subquery(...$args) - { - $this->_expandPlaceholders($args,$this->_placeholderNativeArgs !== null); - return new DbSimple_SubQuery($args); - } - - - /** - * callback setLogger(callback $logger) - * Set query logger called before each query is executed. - * Returns previous logger. - */ - public function setLogger($logger) - { - $prev = $this->_logger; - $this->_logger = $logger; - return $prev; - } - - /** - * callback setCacher(callback $cacher) - * Set cache mechanism called during each query if specified. - * Returns previous handler. - */ - public function setCacher($cacher=null) - { - $prev = $this->_cacher; - - if ( is_null($cacher) ) { - return $prev; - } - - if ($cacher instanceof Zend_Cache_Backend_Interface) { - $this->_cacher = $cacher; - return $prev; - } - - if ( is_callable($cacher) ) { - $this->_cacher = new CacherImpl($cacher); - return $prev; - } - - return $prev; - } - - /** - * string setIdentPrefix($prx) - * Set identifier prefix used for $_ placeholder. - */ - public function setIdentPrefix($prx) - { - $old = $this->_identPrefix; - if ($prx !== null) $this->_identPrefix = $prx; - return $old; - } - - /** - * string setCachePrefix($prx) - * Set cache prefix used in key caclulation. - */ - public function setCachePrefix($prx) - { - $old = $this->_cachePrefix; - if ($prx !== null) $this->_cachePrefix = $prx; - return $old; - } - - /** - * Задает имя класса строки - * - * <br>для следующего запроса каждая строка будет - * заменена классом, конструктору которого передается - * массив поле=>значение для этой строки - * - * @param string $name имя класса - * @return DbSimple_Generic_Database указатель на себя - */ - public function setClassName($name) - { - $this->_className = $name; - return $this; - } - - /** - * array getStatistics() - * Returns various statistical information. - */ - public function getStatistics() - { - return $this->_statistics; - } - - - /** - * string _performEscape(mixed $s, bool $isIdent=false) - */ - abstract protected function _performEscape($s, $isIdent=false); - - /** - * object _performNewBlob($id) - * - * Returns new blob object. - */ - abstract protected function _performNewBlob($id=null); - - /** - * list _performGetBlobFieldNames($resultResource) - * Get list of all BLOB field names in result-set. - */ - abstract protected function _performGetBlobFieldNames($result); - - /** - * mixed _performTransformQuery(array &$query, string $how) - * - * Transform query different way specified by $how. - * May return some information about performed transform. - */ - abstract protected function _performTransformQuery(&$queryMain, $how); - - - /** - * resource _performQuery($arrayQuery) - * Must return: - * - For SELECT queries: ID of result-set (PHP resource). - * - For other queries: query status (scalar). - * - For error queries: false (and call _setLastError()). - */ - abstract protected function _performQuery($arrayQuery); - - /** - * mixed _performFetch($resultResource) - * Fetch ONE NEXT row from result-set. - * Must return: - * - For SELECT queries: all the rows of the query (2d arrray). - * - For INSERT queries: ID of inserted row. - * - For UPDATE queries: number of updated rows. - * - For other queries: query status (scalar). - * - For error queries: false (and call _setLastError()). - */ - abstract protected function _performFetch($result); - - /** - * mixed _performTransaction($mode) - * Start new transaction. - */ - abstract protected function _performTransaction($mode=null); - - /** - * mixed _performCommit() - * Commit the transaction. - */ - abstract protected function _performCommit(); - - /** - * mixed _performRollback() - * Rollback the transaction. - */ - abstract protected function _performRollback(); - - /** - * string _performGetPlaceholderIgnoreRe() - * Return regular expression which matches ignored query parts. - * This is needed to skip placeholder replacement inside comments, constants etc. - */ - protected function _performGetPlaceholderIgnoreRe() - { - return ''; - } - - /** - * Returns marker for native database placeholder. E.g. in FireBird it is '?', - * in PostgreSQL - '$1', '$2' etc. - * - * @param int $n Number of native placeholder from the beginning of the query (begins from 0!). - * @return string String representation of native placeholder marker (by default - '?'). - */ - protected function _performGetNativePlaceholderMarker($n) - { - return '?'; - } - - - /** - * array parseDSN(mixed $dsn) - * Parse a data source name. - * See parse_url() for details. - */ - protected function parseDSN($dsn) - { - if (is_array($dsn)) return $dsn; - $parsed = @parse_url($dsn); - if (!$parsed) return null; - $params = null; - if (!empty($parsed['query'])) { - parse_str($parsed['query'], $params); - $parsed += $params; - } - $parsed['dsn'] = $dsn; - return $parsed; - } - - - /** - * array _query($query, &$total) - * See _performQuery(). - */ - private function _query($query, &$total) - { - $this->_resetLastError(); - - // Fetch query attributes. - $this->attributes = $this->_transformQuery($query, 'GET_ATTRIBUTES'); - - // Modify query if needed for total counting. - if ($total) - $this->_transformQuery($query, 'CALC_TOTAL'); - - $rows = false; - $cache_it = false; - // Кешер у нас либо null либо соответствует Zend интерфейсу - if ( !empty($this->attributes['CACHE']) && ($this->_cacher instanceof Zend_Cache_Backend_Interface) ) - { - - $hash = $this->_cachePrefix . md5(serialize($query)); - // Getting data from cache if possible - $fetchTime = $firstFetchTime = 0; - $qStart = microtime(true); - $cacheData = unserialize($this->_cacher->load($hash)); - $queryTime = microtime(true) - $qStart; - - $invalCache = isset($cacheData['invalCache']) ? $cacheData['invalCache'] : null; - $result = isset($cacheData['result']) ? $cacheData['result'] : null; - $rows = isset($cacheData['rows']) ? $cacheData['rows'] : null; - - - $cache_params = $this->attributes['CACHE']; - - // Calculating cache time to live - $re = '/ - (?> - ([0-9]+) #1 - hours - h)? [ \t]* - (?> - ([0-9]+) #2 - minutes - m)? [ \t]* - (?> - ([0-9]+) #3 - seconds - s?)? (,)? - /sx'; - $m = null; - preg_match($re, $cache_params, $m); - $ttl = (isset($m[3])?$m[3]:0) - + (isset($m[2])?$m[2]:0) * 60 - + (isset($m[1])?$m[1]:0) * 3600; - // Cutting out time param - now there are just fields for uniqKey or nothing - $cache_params = trim(preg_replace($re, '', $cache_params, 1)); - - $uniq_key = null; - - // UNIQ_KEY calculation - if (!empty($cache_params)) { - $dummy = null; - // There is no need in query, cos' needle in $this->attributes['CACHE'] - $this->_transformQuery($dummy, 'UNIQ_KEY'); - $uniq_key = call_user_func(array(&$this, 'select'), $dummy); - $uniq_key = md5(serialize($uniq_key)); - } - // Check TTL? - $ok = empty($ttl) || $cacheData; - - // Invalidate cache? - if ($ok && $uniq_key == $invalCache) { - $this->_logQuery($query); - $this->_logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows); - - } - else $cache_it = true; - } - - if (false === $rows || true === $cache_it) { - $this->_logQuery($query); - - // Run the query (counting time). - $qStart = microtime(true); - $result = $this->_performQuery($query); - $fetchTime = $firstFetchTime = 0; - - if (is_resource($result) || is_object($result)) { - $rows = array(); - // Fetch result row by row. - $fStart = microtime(true); - $row = $this->_performFetch($result); - $firstFetchTime = microtime(true) - $fStart; - if (!empty($row)) { - $rows[] = $row; - while ($row=$this->_performFetch($result)) { - $rows[] = $row; - } - } - $fetchTime = microtime(true) - $fStart; - } else { - $rows = $result; - } - $queryTime = microtime(true) - $qStart; - - // Log query statistics. - $this->_logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows); - - // Prepare BLOB objects if needed. - if (is_array($rows) && !empty($this->attributes['BLOB_OBJ'])) { - $blobFieldNames = $this->_performGetBlobFieldNames($result); - foreach ($blobFieldNames as $name) { - for ($r = count($rows)-1; $r>=0; $r--) { - $rows[$r][$name] =& $this->_performNewBlob($rows[$r][$name]); - } - } - } - - // Transform resulting rows. - $result = $this->_transformResult($rows); - - // Storing data in cache - if ($cache_it && $this->_cacher) - { - $this->_cacher->save( - serialize(array( - 'invalCache' => $uniq_key, - 'result' => $result, - 'rows' => $rows - )), - $hash, - array(), - $ttl==0?false:$ttl - ); - } - - } - // Count total number of rows if needed. - if (is_array($result) && $total) { - $this->_transformQuery($query, 'GET_TOTAL'); - $total = call_user_func_array(array(&$this, 'selectCell'), $query); - } - - if ($this->_className) - { - foreach($result as $k=>$v) - $result[$k] = new $this->_className($v); - $this->_className = ''; - } - - return $result; - } - - - /** - * mixed _transformQuery(array &$query, string $how) - * - * Transform query different way specified by $how. - * May return some information about performed transform. - */ - private function _transformQuery(&$query, $how) - { - // Do overriden transformation. - $result = $this->_performTransformQuery($query, $how); - if ($result === true) return $result; - // Common transformations. - switch ($how) { - case 'GET_ATTRIBUTES': - // Extract query attributes. - $options = array(); - $q = $query[0]; - $m = null; - while (preg_match('/^ \s* -- [ \t]+ (\w+): ([^\r\n]+) [\r\n]* /sx', $q, $m)) { - $options[$m[1]] = trim($m[2]); - $q = substr($q, strlen($m[0])); - } - return $options; - case 'UNIQ_KEY': - $q = $this->attributes['CACHE']; - $query = array(); - while(preg_match('/(\w+)\.\w+/sx', $q, $m)) { - $query[] = 'SELECT MAX('.$m[0].') AS M, COUNT(*) AS C FROM '.$m[1]; - $q = substr($q, strlen($m[0])); - } - $query = " -- UNIQ_KEY\n". - join("\nUNION\n", $query); - return true; - } - // No such transform. - $this->_setLastError(-1, "No such transform type: $how", $query); - } - - - /** - * void _expandPlaceholders(array &$queryAndArgs, bool $useNative=false) - * Replace placeholders by quoted values. - * Modify $queryAndArgs. - */ - protected function _expandPlaceholders(&$queryAndArgs, $useNative=false) - { - $cacheCode = null; - if ($this->_logger) { - // Serialize is much faster than placeholder expansion. So use caching. - $cacheCode = md5(serialize($queryAndArgs) . '|' . $useNative . '|' . $this->_identPrefix); - if (isset($this->_placeholderCache[$cacheCode])) { - $queryAndArgs = $this->_placeholderCache[$cacheCode]; - return; - } - } - - if (!is_array($queryAndArgs)) { - $queryAndArgs = array($queryAndArgs); - } - - $this->_placeholderNativeArgs = $useNative? array() : null; - $this->_placeholderArgs = array_reverse($queryAndArgs); - - $query = array_pop($this->_placeholderArgs); // array_pop is faster than array_shift - - // Do all the work. - $this->_placeholderNoValueFound = false; - $query = $this->_expandPlaceholdersFlow($query); - - if ($useNative) { - array_unshift($this->_placeholderNativeArgs, $query); - $queryAndArgs = $this->_placeholderNativeArgs; - } else { - $queryAndArgs = array($query); - } - - if ($cacheCode) { - $this->_placeholderCache[$cacheCode] = $queryAndArgs; - } - } - - - /** - * Do real placeholder processing. - * Imply that all interval variables (_placeholder_*) already prepared. - * May be called recurrent! - */ - private function _expandPlaceholdersFlow($query) - { - $re = '{ - (?> - # Ignored chunks. - (?> - # Comment. - -- [^\r\n]* - ) - | - (?> - # DB-specifics. - ' . trim($this->_performGetPlaceholderIgnoreRe()) . ' - ) - ) - | - (?> - # Optional blocks - \{ - # Use "+" here, not "*"! Else nested blocks are not processed well. - ( (?> (?>(\??)[^{}]+) | (?R) )* ) #1 - \} - ) - | - (?> - # Placeholder - (\?) ( [_dsafn&|\#]? ) #2 #3 - ) - }sx'; - $query = preg_replace_callback( - $re, - array(&$this, '_expandPlaceholdersCallback'), - $query - ); - return $query; - } - - static $join = array( - '|' => array('inner' => ' AND ', 'outer' => ') OR (',), - '&' => array('inner' => ' OR ', 'outer' => ') AND (',), - 'a' => array('inner' => ', ', 'outer' => '), (',), - ); - - /** - * string _expandPlaceholdersCallback(list $m) - * Internal function to replace placeholders (see preg_replace_callback). - */ - private function _expandPlaceholdersCallback($m) - { - // Placeholder. - if (!empty($m[3])) { - $type = $m[4]; - - // Idenifier prefix. - if ($type == '_') { - return $this->_identPrefix; - } - - // Value-based placeholder. - if (!$this->_placeholderArgs) return 'DBSIMPLE_ERROR_NO_VALUE'; - $value = array_pop($this->_placeholderArgs); - - // Skip this value? - if ($value === DBSIMPLE_SKIP) { - $this->_placeholderNoValueFound = true; - return ''; - } - - // First process guaranteed non-native placeholders. - switch ($type) { - case 's': - if (!($value instanceof DbSimple_SubQuery)) - return 'DBSIMPLE_ERROR_VALUE_NOT_SUBQUERY'; - return $value->get($this->_placeholderNativeArgs); - case '|': - case '&': - case 'a': - if (!$value) $this->_placeholderNoValueFound = true; - if (!is_array($value)) return 'DBSIMPLE_ERROR_VALUE_NOT_ARRAY'; - $parts = array(); - $multi = array(); //массив для двойной вложенности - $mult = $type!='a' || is_int(key($value)) && is_array(current($value)); - foreach ($value as $prefix => $field) { - //превращаем $value в двумерный нуменованный массив - if (!is_array($field)) { - $field = array($prefix => $field); - $prefix = 0; - } - $prefix = is_int($prefix) ? '' : - $this->escape($this->_addPrefix2Table($prefix), true) . '.'; - //для мультиинсерта очищаем ключи - их быть не может по синтаксису - if ($mult && $type=='a') - $field = array_values($field); - foreach ($field as $k => $v) - { - if ($v instanceof DbSimple_SubQuery) - $v = $v->get($this->_placeholderNativeArgs); - else - $v = $v === null? 'NULL' : $this->escape($v); - if (!is_int($k)) { - $k = $this->escape($k, true); - $parts[] = "$prefix$k=$v"; - } else { - $parts[] = $v; - } - } - if ($mult) - { - $multi[] = join(self::$join[$type]['inner'], $parts); - $parts = array(); - } - } - return $mult ? join(self::$join[$type]['outer'], $multi) : join(', ', $parts); - case '#': - // Identifier. - if (!is_array($value)) - { - if ($value instanceof DbSimple_SubQuery) - return $value->get($this->_placeholderNativeArgs); - return $this->escape($this->_addPrefix2Table($value), true); - } - $parts = array(); - foreach ($value as $table => $identifiers) - { - if (!is_array($identifiers)) - $identifiers = array($identifiers); - $prefix = ''; - if (!is_int($table)) - $prefix = $this->escape($this->_addPrefix2Table($table), true) . '.'; - foreach ($identifiers as $identifier) - if ($identifier instanceof DbSimple_SubQuery) - $parts[] = $identifier->get($this->_placeholderNativeArgs); - elseif (!is_string($identifier)) - return 'DBSIMPLE_ERROR_ARRAY_VALUE_NOT_STRING'; - else - $parts[] = $prefix . ($identifier=='*' ? '*' : - $this->escape($this->_addPrefix2Table($identifier), true)); - } - return join(', ', $parts); - case 'n': - // NULL-based placeholder. - return empty($value)? 'NULL' : intval($value); - } - - // Native arguments are not processed. - if ($this->_placeholderNativeArgs !== null) { - $this->_placeholderNativeArgs[] = $value; - return $this->_performGetNativePlaceholderMarker(count($this->_placeholderNativeArgs) - 1); - } - - // In non-native mode arguments are quoted. - if ($value === null) return 'NULL'; - switch ($type) { - case '': - if (!is_scalar($value)) return 'DBSIMPLE_ERROR_VALUE_NOT_SCALAR'; - return $this->escape($value); - case 'd': - return intval($value); - case 'f': - return str_replace(',', '.', floatval($value)); - } - // By default - escape as string. - return $this->escape($value); - } - - // Optional block. - if (isset($m[1]) && strlen($block=$m[1])) - { - $prev = $this->_placeholderNoValueFound; - if ($this->_placeholderNativeArgs !== null) - $prevPh = $this->_placeholderNativeArgs; - - // Проверка на {? } - условный блок - $skip = false; - if ($m[2]=='?') - { - $skip = array_pop($this->_placeholderArgs) === DBSIMPLE_SKIP; - $block[0] = ' '; - } - - $block = $this->_expandOptionalBlock($block); - - if ($skip) - $block = ''; - - if ($this->_placeholderNativeArgs !== null) - if ($this->_placeholderNoValueFound) - $this->_placeholderNativeArgs = $prevPh; - $this->_placeholderNoValueFound = $prev; // recurrent-safe - return $block; - } - - // Default: skipped part of the string. - return $m[0]; - } - - - /** - * Заменяет ?_ на текущий префикс - * - * @param string $table имя таблицы - * @return string имя таблицы - */ - private function _addPrefix2Table($table) - { - if (substr($table, 0, 2) == '?_') - $table = $this->_identPrefix . substr($table, 2); - return $table; - } - - - /** - * Разбирает опциональный блок - условие | - * - * @param string $block блок, который нужно разобрать - * @return string что получается в результате разбора блока - */ - private function _expandOptionalBlock($block) - { - $alts = array(); - $alt = ''; - $sub=0; - $exp = explode('|',$block); - // Оптимизация, так как в большинстве случаев | не используется - if (count($exp)==1) - $alts=$exp; - else - foreach ($exp as $v) - { - // Реализуем автоматный магазин для нахождения нужной скобки - // На суммарную парность скобок проверять нет необходимости - об этом заботится регулярка - $sub+=substr_count($v,'{'); - $sub-=substr_count($v,'}'); - if ($sub>0) - $alt.=$v.'|'; - else - { - $alts[]=$alt.$v; - $alt=''; - } - } - $r=''; - foreach ($alts as $block) - { - $this->_placeholderNoValueFound = false; - $block = $this->_expandPlaceholdersFlow($block); - // Необходимо пройти все блоки, так как если пропустить оставшиесь, - // то это нарушит порядок подставляемых значений - if ($this->_placeholderNoValueFound == false && $r=='') - $r = ' '.$block.' '; - } - return $r; - } - - - /** - * void _setLastError($code, $msg, $query) - * Set last database error context. - * Aditionally expand placeholders. - */ - protected function _setLastError($code, $msg, $query) - { - if (is_array($query)) { - $this->_expandPlaceholders($query, false); - $query = $query[0]; - } - return parent::_setLastError($code, $msg, $query); - } - - - /** - * Convert SQL field-list to COUNT(...) clause - * (e.g. 'DISTINCT a AS aa, b AS bb' -> 'COUNT(DISTINCT a, b)'). - */ - private function _fieldList2Count($fields) - { - $m = null; - if (preg_match('/^\s* DISTINCT \s* (.*)/sx', $fields, $m)) { - $fields = $m[1]; - $fields = preg_replace('/\s+ AS \s+ .*? (?=,|$)/sx', '', $fields); - return "COUNT(DISTINCT $fields)"; - } else { - return 'COUNT(*)'; - } - } - - - /** - * array _transformResult(list $rows) - * Transform resulting rows to various formats. - */ - private function _transformResult($rows) - { - // is not array - if (!is_array($rows) || !$rows) - return $rows; - - // Find ARRAY_KEY* AND PARENT_KEY fields in field list. - $pk = null; - $ak = array(); - foreach (array_keys(current($rows)) as $fieldName) - if (0 == strncasecmp($fieldName, DBSIMPLE_ARRAY_KEY, strlen(DBSIMPLE_ARRAY_KEY))) - $ak[] = $fieldName; - elseif (0 == strncasecmp($fieldName, DBSIMPLE_PARENT_KEY, strlen(DBSIMPLE_PARENT_KEY))) - $pk = $fieldName; - - if (!$ak) - return $rows; - - natsort($ak); // sort ARRAY_KEY* using natural comparision - // Tree-based array? Fields: ARRAY_KEY, PARENT_KEY - if ($pk !== null) - return $this->_transformResultToForest($rows, $ak[0], $pk); - // Key-based array? Fields: ARRAY_KEY. - return $this->_transformResultToHash($rows, $ak); - } - - - /** - * Converts rowset to key-based array. - * - * @param array $rows Two-dimensional array of resulting rows. - * @param array $ak List of ARRAY_KEY* field names. - * @return array Transformed array. - */ - private function _transformResultToHash(array $rows, array $arrayKeys) - { - $result = array(); - foreach ($rows as $row) { - // Iterate over all of ARRAY_KEY* fields and build array dimensions. - $current =& $result; - foreach ($arrayKeys as $ak) { - $key = $row[$ak]; - unset($row[$ak]); // remove ARRAY_KEY* field from result row - if ($key !== null) { - $current =& $current[$key]; - } else { - // IF ARRAY_KEY field === null, use array auto-indices. - $tmp = array(); - $current[] =& $tmp; - $current =& $tmp; - unset($tmp); // we use $tmp, because don't know the value of auto-index - } - } - $current = $row; // save the row in last dimension - } - return $result; - } - - - /** - * Converts rowset to the forest. - * - * @param array $rows Two-dimensional array of resulting rows. - * @param string $idName Name of ID field. - * @param string $pidName Name of PARENT_ID field. - * @return array Transformed array (tree). - */ - private function _transformResultToForest(array $rows, $idName, $pidName) - { - $children = array(); // children of each ID - $ids = array(); - // Collect who are children of whom. - foreach ($rows as $i=>$r) { - $row =& $rows[$i]; - $id = $row[$idName]; - if ($id === null) { - // Rows without an ID are totally invalid and makes the result tree to - // be empty (because PARENT_ID = null means "a root of the tree"). So - // skip them totally. - continue; - } - $pid = $row[$pidName]; - if ($id == $pid) $pid = null; - $children[$pid][$id] =& $row; - if (!isset($children[$id])) $children[$id] = array(); - $row['childNodes'] =& $children[$id]; - $ids[$id] = true; - } - // Root elements are elements with non-found PIDs. - $forest = array(); - foreach ($rows as $i=>$r) { - $row =& $rows[$i]; - $id = $row[$idName]; - $pid = $row[$pidName]; - if ($pid == $id) $pid = null; - if (!isset($ids[$pid])) { - $forest[$row[$idName]] =& $row; - } - unset($row[$idName]); - unset($row[$pidName]); - } - return $forest; - } - - - /** - * Replaces the last array in a multi-dimensional array $V by its first value. - * Used for selectCol(), when we need to transform (N+1)d resulting array - * to Nd array (column). - */ - private function _shrinkLastArrayDimensionCallback(&$v) - { - if (!$v) return; - reset($v); - if (!is_array($firstCell = current($v))) { - $v = $firstCell; - } else { - array_walk($v, array(&$this, '_shrinkLastArrayDimensionCallback')); - } - } - - - /** - * void _logQuery($query, $noTrace=false) - * Must be called on each query. - * If $noTrace is true, library caller is not solved (speed improvement). - */ - protected function _logQuery($query, $noTrace=false) - { - if (!$this->_logger) return; - $this->_expandPlaceholders($query, false); - $args = array(); - $args[] =& $this; - $args[] = $query[0]; - $args[] = $noTrace? null : $this->findLibraryCaller(); - return call_user_func_array($this->_logger, $args); - } - - - /** - * void _logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows) - * Log information about performed query statistics. - */ - private function _logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows) - { - // Always increment counters. - $this->_statistics['time'] += $queryTime; - $this->_statistics['count']++; - - // If no logger, economize CPU resources and actually log nothing. - if (!$this->_logger) return; - - $dt = round($queryTime * 1000); - $firstFetchTime = round($firstFetchTime*1000); - $tailFetchTime = round($fetchTime * 1000) - $firstFetchTime; - $log = " -- "; - if ($firstFetchTime + $tailFetchTime) { - $log = sprintf(" -- %d ms = %d+%d".($tailFetchTime? "+%d" : ""), $dt, $dt-$firstFetchTime-$tailFetchTime, $firstFetchTime, $tailFetchTime); - } else { - $log = sprintf(" -- %d ms", $dt); - } - $log .= "; returned "; - - if (!is_array($rows)) { - $log .= $this->escape($rows); - } else { - $detailed = null; - if (count($rows) == 1) { - $len = 0; - $values = array(); - foreach ($rows[0] as $k=>$v) { - $len += strlen($v ?? ''); - if ($len > $this->MAX_LOG_ROW_LEN) { - break; - } - $values[] = $v === null? 'NULL' : $this->escape($v); - } - if ($len <= $this->MAX_LOG_ROW_LEN) { - $detailed = "(" . preg_replace("/\r?\n/", "\\n", join(', ', $values)) . ")"; - } - } - if ($detailed) { - $log .= $detailed; - } else { - $log .= count($rows). " row(s)"; - } - } - - $this->_logQuery($log, true); - } - - - // Identifiers prefix (used for ?_ placeholder). - private $_identPrefix = ''; - - // Queries statistics. - private $_statistics = array( - 'time' => 0, - 'count' => 0, - ); - - private $_cachePrefix = ''; - private $_className = ''; - - private $_logger = null; - private $_cacher = null; - private $_placeholderArgs, $_placeholderNativeArgs, $_placeholderCache=array(); - private $_placeholderNoValueFound; - - /** - * When string representation of row (in characters) is greater than this, - * row data will not be logged. - */ - private $MAX_LOG_ROW_LEN = 128; -} - - -/** - * Database BLOB. - * Can read blob chunk by chunk, write data to BLOB. - */ -interface DbSimple_Blob -{ - /** - * string read(int $length) - * Returns following $length bytes from the blob. - */ - public function read($len); - - /** - * string write($data) - * Appends data to blob. - */ - public function write($data); - - /** - * int length() - * Returns length of the blob. - */ - public function length(); - - /** - * blobid close() - * Closes the blob. Return its ID. No other way to obtain this ID! - */ - public function close(); -} - - -/** - * Класс для хранения подзапроса - результата выполнения функции - * DbSimple_Generic_Database::subquery - * - */ -class DbSimple_SubQuery -{ - private $query=array(); - - public function __construct(array $q) - { - $this->query = $q; - } - - /** - * Возвращает сам запрос и добавляет плейсхолдеры в массив переданный по ссылке - * - * @param &array|null - ссылка на массив плейсхолдеров - * @return string - */ - public function get(&$ph) - { - if ($ph !== null) - $ph = array_merge($ph, array_slice($this->query,1,null,true)); - return $this->query[0]; - } -} - - -/** - * Support for error tracking. - * Can hold error messages, error queries and build proper stacktraces. - */ -abstract class DbSimple_LastError -{ - public $error = null; - public $errmsg = null; - private $errorHandler = null; - private $ignoresInTraceRe = 'DbSimple_.*::.* | call_user_func.*'; - - /** - * abstract void _logQuery($query) - * Must be overriden in derived class. - */ - abstract protected function _logQuery($query); - - /** - * void _resetLastError() - * Reset the last error. Must be called on correct queries. - */ - protected function _resetLastError() - { - $this->error = $this->errmsg = null; - } - - /** - * void _setLastError(int $code, string $message, string $query) - * Fill $this->error property with error information. Error context - * (code initiated the query outside DbSimple) is assigned automatically. - */ - protected function _setLastError($code, $msg, $query) - { - $context = "unknown"; - if ($t = $this->findLibraryCaller()) { - $context = (isset($t['file'])? $t['file'] : '?') . ' line ' . (isset($t['line'])? $t['line'] : '?'); - } - $this->error = array( - 'code' => $code, - 'message' => rtrim($msg), - 'query' => $query, - 'context' => $context, - ); - $this->errmsg = rtrim($msg) . ($context? " at $context" : ""); - - $this->_logQuery(" -- error #".$code.": ".preg_replace('/(\r?\n)+/s', ' ', $this->errmsg)); - - if (is_callable($this->errorHandler)) { - call_user_func($this->errorHandler, $this->errmsg, $this->error); - } - - return false; - } - - - /** - * callback setErrorHandler(callback $handler) - * Set new error handler called on database errors. - * Handler gets 3 arguments: - * - error message - * - full error context information (last query etc.) - */ - public function setErrorHandler($handler) - { - $prev = $this->errorHandler; - $this->errorHandler = $handler; - // In case of setting first error handler for already existed - // error - call the handler now (usual after connect()). - if (!$prev && $this->error && $this->errorHandler) { - call_user_func($this->errorHandler, $this->errmsg, $this->error); - } - return $prev; - } - - /** - * void addIgnoreInTrace($reName) - * Add regular expression matching ClassName::functionName or functionName. - * Matched stack frames will be ignored in stack traces passed to query logger. - */ - public function addIgnoreInTrace($name) - { - $this->ignoresInTraceRe .= "|" . $name; - } - - /** - * array of array findLibraryCaller() - * Return part of stacktrace before calling first library method. - * Used in debug purposes (query logging etc.). - */ - public function findLibraryCaller() - { - $caller = call_user_func( - array(&$this, 'debug_backtrace_smart'), - $this->ignoresInTraceRe, - true - ); - return $caller; - } - - /** - * array debug_backtrace_smart($ignoresRe=null, $returnCaller=false) - * - * Return stacktrace. Correctly work with call_user_func* - * (totally skip them correcting caller references). - * If $returnCaller is true, return only first matched caller, - * not all stacktrace. - * - * @version 2.03 - */ - private function debug_backtrace_smart($ignoresRe=null, $returnCaller=false) - { - $trace = debug_backtrace(); - - if ($ignoresRe !== null) - $ignoresRe = "/^(?>{$ignoresRe})$/six"; - $smart = array(); - $framesSeen = 0; - for ($i=0, $n=count($trace); $i<$n; $i++) { - $t = $trace[$i]; - if (!$t) continue; - - // Next frame. - $next = isset($trace[$i+1])? $trace[$i+1] : null; - - // Dummy frame before call_user_func* frames. - if (!isset($t['file'])) { - $t['over_function'] = $trace[$i+1]['function']; - $t = $t + $trace[$i+1]; - $trace[$i+1] = null; // skip call_user_func on next iteration - $next = isset($trace[$i+2])? $trace[$i+2] : null; // Correct Next frame. - } - - // Skip myself frame. - if (++$framesSeen < 2) continue; - - // 'class' and 'function' field of next frame define where - // this frame function situated. Skip frames for functions - // situated in ignored places. - if ($ignoresRe && $next) { - // Name of function "inside which" frame was generated. - $frameCaller = (isset($next['class'])? $next['class'].'::' : '') . (isset($next['function'])? $next['function'] : ''); - if (preg_match($ignoresRe, $frameCaller)) continue; - } - - // On each iteration we consider ability to add PREVIOUS frame - // to $smart stack. - if ($returnCaller) return $t; - $smart[] = $t; - } - return $smart; - } - -} diff --git a/includes/libs/DbSimple/Generic.php b/includes/libs/DbSimple/Generic.php deleted file mode 100644 index 5dc2f144..00000000 --- a/includes/libs/DbSimple/Generic.php +++ /dev/null @@ -1,193 +0,0 @@ -<?php -/** - * DbSimple_Generic: universal database connected by DSN. - * (C) Dk Lab, http://en.dklab.ru - * - * 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 2.1 of the License, or (at your option) any later version. - * See http://www.gnu.org/copyleft/lesser.html - * - * Use static DbSimple_Generic::connect($dsn) call if you don't know - * database type and parameters, but have its DSN. - * - * Additional keys can be added by appending a URI query string to the - * end of the DSN. - * - * The format of the supplied DSN is in its fullest form: - * phptype(dbsyntax)://username:password@protocol+hostspec/database?option=8&another=true - * - * Most variations are allowed: - * phptype://username:password@protocol+hostspec:110//usr/db_file.db?mode=0644 - * phptype://username:password@hostspec/database_name - * phptype://username:password@hostspec - * phptype://username@hostspec - * phptype://hostspec/database - * phptype://hostspec - * phptype(dbsyntax) - * phptype - * - * Parsing code is partially grabbed from PEAR DB class, - * initial author: Tomas V.V.Cox <cox@idecnet.com>. - * - * Contains 3 classes: - * - DbSimple_Generic: database factory class - * - DbSimple_Generic_Database: common database methods - * - DbSimple_Generic_Blob: common BLOB support - * - DbSimple_Generic_LastError: error reporting and tracking - * - * Special result-set fields: - * - ARRAY_KEY* ("*" means "anything") - * - PARENT_KEY - * - * Transforms: - * - GET_ATTRIBUTES - * - CALC_TOTAL - * - GET_TOTAL - * - UNIQ_KEY - * - * Query attributes: - * - BLOB_OBJ - * - CACHE - * - * @author Dmitry Koterov, http://forum.dklab.ru/users/DmitryKoterov/ - * @author Konstantin Zhinko, http://forum.dklab.ru/users/KonstantinGinkoTit/ - * - * @version 2.x $Id$ - */ - -/** - * Use this constant as placeholder value to skip optional SQL block [...]. - */ -if (!defined('DBSIMPLE_SKIP')) - define('DBSIMPLE_SKIP', log(0)); - -/** - * Names of special columns in result-set which is used - * as array key (or karent key in forest-based resultsets) in - * resulting hash. - */ -if (!defined('DBSIMPLE_ARRAY_KEY')) - define('DBSIMPLE_ARRAY_KEY', 'ARRAY_KEY'); // hash-based resultset support -if (!defined('DBSIMPLE_PARENT_KEY')) - define('DBSIMPLE_PARENT_KEY', 'PARENT_KEY'); // forrest-based resultset support - - -/** - * DbSimple factory. - */ -class DbSimple_Generic -{ - /** - * DbSimple_Generic connect(mixed $dsn) - * - * Universal static function to connect ANY database using DSN syntax. - * Choose database driver according to DSN. Return new instance - * of this driver. - * - * You can connect to MySQL by socket using this new syntax (like PDO DSN): - * $dsn = 'mysqli:unix_socket=/cloudsql/app:instance;user=root;pass=;dbname=testdb'; - * $dsn = 'mypdo:unix_socket=/cloudsql/app:instance;charset=utf8;user=testuser;pass=mypassword;dbname=testdb'; - * - * Connection by host also can be made with this syntax. - * Or you can use old syntax: - * $dsn = 'mysql://testuser:mypassword@127.0.0.1/testdb'; - * - */ - public static function connect($dsn) - { - // Load database driver and create its instance. - $parsed = DbSimple_Generic::parseDSN($dsn); - if (!$parsed) { - $dummy = null; - return $dummy; - } - $class = 'DbSimple_'.ucfirst($parsed['scheme']); - if (!class_exists($class)) { - $file = __DIR__.'/'.ucfirst($parsed['scheme']). ".php"; - if (is_file($file)) { - require_once($file); - } else { - trigger_error("Error loading database driver: no file $file", E_USER_ERROR); - return null; - } - } - $object = new $class($parsed); - if (isset($parsed['ident_prefix'])) { - $object->setIdentPrefix($parsed['ident_prefix']); - } - $object->setCachePrefix(md5(serialize($parsed['dsn']))); - return $object; - } - - - /** - * array parseDSN(mixed $dsn) - * Parse a data source name. - * See parse_url() for details. - */ - public static function parseDSN($dsn) - { - if (is_array($dsn)) return $dsn; - $parsed = parse_url($dsn); - if (!$parsed) return null; - - $params = null; - if (!empty($parsed['query'])) { - parse_str($parsed['query'], $params); - $parsed += $params; - } - - if ( empty($parsed['host']) && empty($parsed['socket']) ) { - // Parse as DBO DSN string - $parsedPdo = self::parseDsnPdo($parsed['path']); - unset($parsed['path']); - $parsed = array_merge($parsed, $parsedPdo); - } - - $parsed['dsn'] = $dsn; - return $parsed; - } // parseDSN - - - /** - * Parse string as DBO DSN string. - * - * @param $str - * @return array - */ - public static function parseDsnPdo($str) { - - if (substr($str, 0, strlen('mysql:')) == 'mysql:') { - $str = substr($str, strlen('mysql:')); - } - - $arr = explode(';', $str); - - $result = array(); - foreach ($arr as $k=>$v) { - $v = explode('=', $v); - if (count($v) == 2) - $result[ $v[0] ] = $v[1]; - } - - if ( isset($result['unix_socket']) ) { - $result['socket'] = $result['unix_socket']; - unset($result['unix_socket']); - } - - if ( isset($result['dbname']) ) { - $result['path'] = $result['dbname']; - unset($result['dbname']); - } - - if ( isset($result['charset']) ) { - $result['enc'] = $result['charset']; - unset($result['charset']); - } - - return $result; - } // parseDsnPdo - -} // DbSimple_Generic class diff --git a/includes/libs/DbSimple/Mysqli.php b/includes/libs/DbSimple/Mysqli.php deleted file mode 100644 index 2c1fcccd..00000000 --- a/includes/libs/DbSimple/Mysqli.php +++ /dev/null @@ -1,245 +0,0 @@ -<?php -/** - * DbSimple_Mysql: MySQL database. - * (C) Dk Lab, http://en.dklab.ru - * - * 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 2.1 of the License, or (at your option) any later version. - * See http://www.gnu.org/copyleft/lesser.html - * - * Placeholders end blobs are emulated. - * - * @author Dmitry Koterov, http://forum.dklab.ru/users/DmitryKoterov/ - * @author Konstantin Zhinko, http://forum.dklab.ru/users/KonstantinGinkoTit/ - * - * @version 2.x $Id: Mysqli.php 247 2008-08-18 21:17:08Z dk $ - */ -require_once __DIR__.'/Database.php'; - - -/** - * Database class for MySQL. - */ -class DbSimple_Mysqli extends DbSimple_Database -{ - var $link; - - private $_lastQuery; - - /** - * constructor(string $dsn) - * Connect to MySQL server. - */ - function __construct($dsn) - { - - if (!is_callable("mysqli_connect")) - return $this->_setLastError("-1", "MySQLi extension is not loaded", "mysqli_connect"); - - if (!empty($dsn["persist"])) { - if (version_compare(PHP_VERSION, '5.3') < 0) { - return $this->_setLastError("-1", "Persistent connections in MySQLi is allowable since PHP 5.3", "mysqli_connect"); - } else { - $dsn["host"] = "p:".$dsn["host"]; - } - } - - if ( isset($dsn['socket']) ) { - // Socket connection - $this->link = mysqli_connect( - null // host - ,empty($dsn['user']) ? 'root' : $dsn['user'] // user - ,empty($dsn['pass']) ? '' : $dsn['pass'] // password - ,preg_replace('{^/}s', '', $dsn['path']) // schema - ,null // port - ,$dsn['socket'] // socket - ); - } else if (isset($dsn['host']) ) { - // Host connection - $this->link = mysqli_connect( - $dsn['host'] - ,empty($dsn['user']) ? 'root' : $dsn['user'] - ,empty($dsn['pass']) ? '' : $dsn['pass'] - ,preg_replace('{^/}s', '', $dsn['path']) - ,empty($dsn['port']) ? null : $dsn['port'] - ); - } else { - return $this->_setDbError('mysqli_connect()'); - } - $this->_resetLastError(); - if (!$this->link) return $this->_setDbError('mysqli_connect()'); - - mysqli_set_charset($this->link, isset($dsn['enc']) ? $dsn['enc'] : 'UTF8'); - } - - - protected function _performEscape($s, $isIdent=false) - { - if (!$isIdent) - return "'" . mysqli_real_escape_string($this->link, $s) . "'"; - else - return "`" . str_replace('`', '``', $s) . "`"; - } - - - protected function _performNewBlob($blobid=null) - { - return new DbSimple_Mysqli_Blob($this, $blobid); - } - - - protected function _performGetBlobFieldNames($result) - { - $allFields = mysqli_fetch_fields($result); - $blobFields = array(); - - if (!empty($allFields)) - { - foreach ($allFields as $field) - if (stripos($field["type"], "BLOB") !== false) - $blobFields[] = $field["name"]; - } - return $blobFields; - } - - - protected function _performGetPlaceholderIgnoreRe() - { - return ' - " (?> [^"\\\\]+|\\\\"|\\\\)* " | - \' (?> [^\'\\\\]+|\\\\\'|\\\\)* \' | - ` (?> [^`]+ | ``)* ` | # backticks - /\* .*? \*/ # comments - '; - } - - - protected function _performTransaction($parameters=null) - { - return mysqli_begin_transaction($this->link); - } - - - protected function _performCommit() - { - return mysqli_commit($this->link); - } - - - protected function _performRollback() - { - return mysqli_rollback($this->link); - } - - - protected function _performTransformQuery(&$queryMain, $how) - { - // If we also need to calculate total number of found rows... - switch ($how) - { - // Prepare total calculation (if possible) - case 'CALC_TOTAL': - $m = null; - if (preg_match('/^(\s* SELECT)(.*)/six', $queryMain[0], $m)) - $queryMain[0] = $m[1] . ' SQL_CALC_FOUND_ROWS' . $m[2]; - return true; - - // Perform total calculation. - case 'GET_TOTAL': - // Built-in calculation available? - $queryMain = array('SELECT FOUND_ROWS()'); - return true; - } - - return false; - } - - - protected function _performQuery($queryMain) - { - $this->_lastQuery = $queryMain; - $this->_expandPlaceholders($queryMain, false); - mysqli_ping($this->link); - $result = mysqli_query($this->link, $queryMain[0]); - if ($result === false) - return $this->_setDbError($queryMain[0]); - - if ($this->link->warning_count) { - if ($warn = $this->link->query("SHOW WARNINGS")) { - while ($warnRow = $warn->fetch_row()) - if ($warnRow[0] === 'Warning') - $this->_setLastError(-$warnRow[1], $warnRow[2], $queryMain[0]); - - $warn->close(); - } - } - - if (!is_object($result)) { - if (preg_match('/^\s* INSERT \s+/six', $queryMain[0])) - { - // INSERT queries return generated ID. - return mysqli_insert_id($this->link); - } - // Non-SELECT queries return number of affected rows, SELECT - resource. - return mysqli_affected_rows($this->link); - } - return $result; - } - - - protected function _performFetch($result) - { - $row = mysqli_fetch_assoc($result); - if (mysqli_error($this->link)) return $this->_setDbError($this->_lastQuery); - if ($row === false) return null; - return $row; - } - - - protected function _setDbError($query) - { - if ($this->link) { - return $this->_setLastError(mysqli_errno($this->link), mysqli_error($this->link), $query); - } else { - return $this->_setLastError(mysqli_connect_errno(), mysqli_connect_error(), $query); - } - } -} - - -class DbSimple_Mysqli_Blob implements DbSimple_Blob -{ - // MySQL does not support separate BLOB fetching. - private $blobdata = null; - private $curSeek = 0; - - public function __construct(&$database, $blobdata=null) - { - $this->blobdata = $blobdata; - $this->curSeek = 0; - } - - public function read($len) - { - $p = $this->curSeek; - $this->curSeek = min($this->curSeek + $len, strlen($this->blobdata)); - return substr($this->blobdata, $p, $len); - } - - public function write($data) - { - $this->blobdata .= $data; - } - - public function close() - { - return $this->blobdata; - } - - public function length() - { - return strlen($this->blobdata); - } -} diff --git a/includes/libs/DbSimple/Zend/Cache.php b/includes/libs/DbSimple/Zend/Cache.php deleted file mode 100644 index aff2e653..00000000 --- a/includes/libs/DbSimple/Zend/Cache.php +++ /dev/null @@ -1,250 +0,0 @@ -<?php -/** - * Zend Framework - * - * LICENSE - * - * This source file is subject to the new BSD license that is bundled - * with this package in the file LICENSE.txt. - * It is also available through the world-wide-web at this URL: - * http://framework.zend.com/license/new-bsd - * If you did not receive a copy of the license and are unable to - * obtain it through the world-wide-web, please send an email - * to license@zend.com so we can send you a copy immediately. - * - * @category Zend - * @package Zend_Cache - * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) - * @license http://framework.zend.com/license/new-bsd New BSD License - * @version $Id: Cache.php 24656 2012-02-26 06:02:53Z adamlundrigan $ - */ - - -/** - * @package Zend_Cache - * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) - * @license http://framework.zend.com/license/new-bsd New BSD License - */ -abstract class Zend_Cache -{ - - /** - * Standard frontends - * - * @var array - */ - public static $standardFrontends = array('Core', 'Output', 'Class', 'File', 'Function', 'Page'); - - /** - * Standard backends - * - * @var array - */ - public static $standardBackends = array('File', 'Sqlite', 'Memcached', 'Libmemcached', 'Apc', 'ZendPlatform', - 'Xcache', 'TwoLevels', 'WinCache', 'ZendServer_Disk', 'ZendServer_ShMem'); - - /** - * Standard backends which implement the ExtendedInterface - * - * @var array - */ - public static $standardExtendedBackends = array('File', 'Apc', 'TwoLevels', 'Memcached', 'Libmemcached', 'Sqlite', 'WinCache'); - - /** - * Only for backward compatibility (may be removed in next major release) - * - * @var array - * @deprecated - */ - public static $availableFrontends = array('Core', 'Output', 'Class', 'File', 'Function', 'Page'); - - /** - * Only for backward compatibility (may be removed in next major release) - * - * @var array - * @deprecated - */ - public static $availableBackends = array('File', 'Sqlite', 'Memcached', 'Libmemcached', 'Apc', 'ZendPlatform', 'Xcache', 'WinCache', 'TwoLevels'); - - /** - * Consts for clean() method - */ - const CLEANING_MODE_ALL = 'all'; - const CLEANING_MODE_OLD = 'old'; - const CLEANING_MODE_MATCHING_TAG = 'matchingTag'; - const CLEANING_MODE_NOT_MATCHING_TAG = 'notMatchingTag'; - const CLEANING_MODE_MATCHING_ANY_TAG = 'matchingAnyTag'; - - /** - * Factory - * - * @param mixed $frontend frontend name (string) or Zend_Cache_Frontend_ object - * @param mixed $backend backend name (string) or Zend_Cache_Backend_ object - * @param array $frontendOptions associative array of options for the corresponding frontend constructor - * @param array $backendOptions associative array of options for the corresponding backend constructor - * @param boolean $customFrontendNaming if true, the frontend argument is used as a complete class name ; if false, the frontend argument is used as the end of "Zend_Cache_Frontend_[...]" class name - * @param boolean $customBackendNaming if true, the backend argument is used as a complete class name ; if false, the backend argument is used as the end of "Zend_Cache_Backend_[...]" class name - * @param boolean $autoload if true, there will no require_once for backend and frontend (useful only for custom backends/frontends) - * @throws Zend_Cache_Exception - * @return Zend_Cache_Core|Zend_Cache_Frontend - */ - public static function factory($frontend, $backend, $frontendOptions = array(), $backendOptions = array(), $customFrontendNaming = false, $customBackendNaming = false, $autoload = false) - { - if (is_string($backend)) { - $backendObject = self::_makeBackend($backend, $backendOptions, $customBackendNaming, $autoload); - } else { - if ((is_object($backend)) && (in_array('Zend_Cache_Backend_Interface', class_implements($backend)))) { - $backendObject = $backend; - } else { - self::throwException('backend must be a backend name (string) or an object which implements Zend_Cache_Backend_Interface'); - } - } - if (is_string($frontend)) { - $frontendObject = self::_makeFrontend($frontend, $frontendOptions, $customFrontendNaming, $autoload); - } else { - if (is_object($frontend)) { - $frontendObject = $frontend; - } else { - self::throwException('frontend must be a frontend name (string) or an object'); - } - } - $frontendObject->setBackend($backendObject); - return $frontendObject; - } - - /** - * Backend Constructor - * - * @param string $backend - * @param array $backendOptions - * @param boolean $customBackendNaming - * @param boolean $autoload - * @return Zend_Cache_Backend - */ - public static function _makeBackend($backend, $backendOptions, $customBackendNaming = false, $autoload = false) - { - if (!$customBackendNaming) { - $backend = self::_normalizeName($backend); - } - if (in_array($backend, Zend_Cache::$standardBackends)) { - // we use a standard backend - $backendClass = 'Zend_Cache_Backend_' . $backend; - // security controls are explicit - require_once str_replace('_', DIRECTORY_SEPARATOR, $backendClass) . '.php'; - } else { - // we use a custom backend - if (!preg_match('~^[\w\\\\]+$~D', $backend)) { - Zend_Cache::throwException("Invalid backend name [$backend]"); - } - if (!$customBackendNaming) { - // we use this boolean to avoid an API break - $backendClass = 'Zend_Cache_Backend_' . $backend; - } else { - $backendClass = $backend; - } - if (!$autoload) { - $file = str_replace('_', DIRECTORY_SEPARATOR, $backendClass) . '.php'; - if (!(self::_isReadable($file))) { - self::throwException("file $file not found in include_path"); - } - require_once $file; - } - } - return new $backendClass($backendOptions); - } - - /** - * Frontend Constructor - * - * @param string $frontend - * @param array $frontendOptions - * @param boolean $customFrontendNaming - * @param boolean $autoload - * @return Zend_Cache_Core|Zend_Cache_Frontend - */ - public static function _makeFrontend($frontend, $frontendOptions = array(), $customFrontendNaming = false, $autoload = false) - { - if (!$customFrontendNaming) { - $frontend = self::_normalizeName($frontend); - } - if (in_array($frontend, self::$standardFrontends)) { - // we use a standard frontend - // For perfs reasons, with frontend == 'Core', we can interact with the Core itself - $frontendClass = 'Zend_Cache_' . ($frontend != 'Core' ? 'Frontend_' : '') . $frontend; - // security controls are explicit - require_once str_replace('_', DIRECTORY_SEPARATOR, $frontendClass) . '.php'; - } else { - // we use a custom frontend - if (!preg_match('~^[\w\\\\]+$~D', $frontend)) { - Zend_Cache::throwException("Invalid frontend name [$frontend]"); - } - if (!$customFrontendNaming) { - // we use this boolean to avoid an API break - $frontendClass = 'Zend_Cache_Frontend_' . $frontend; - } else { - $frontendClass = $frontend; - } - if (!$autoload) { - $file = str_replace('_', DIRECTORY_SEPARATOR, $frontendClass) . '.php'; - if (!(self::_isReadable($file))) { - self::throwException("file $file not found in include_path"); - } - require_once $file; - } - } - return new $frontendClass($frontendOptions); - } - - /** - * Throw an exception - * - * Note : for perf reasons, the "load" of Zend/Cache/Exception is dynamic - * @param string $msg Message for the exception - * @throws Zend_Cache_Exception - */ - public static function throwException($msg, Exception $e = null) - { - // For perfs reasons, we use this dynamic inclusion - require_once 'Zend/Cache/Exception.php'; - throw new Zend_Cache_Exception($msg, 0, $e); - } - - /** - * Normalize frontend and backend names to allow multiple words TitleCased - * - * @param string $name Name to normalize - * @return string - */ - protected static function _normalizeName($name) - { - $name = ucfirst(strtolower($name)); - $name = str_replace(array('-', '_', '.'), ' ', $name); - $name = ucwords($name); - $name = str_replace(' ', '', $name); - if (stripos($name, 'ZendServer') === 0) { - $name = 'ZendServer_' . substr($name, strlen('ZendServer')); - } - - return $name; - } - - /** - * Returns TRUE if the $filename is readable, or FALSE otherwise. - * This function uses the PHP include_path, where PHP's is_readable() - * does not. - * - * Note : this method comes from Zend_Loader (see #ZF-2891 for details) - * - * @param string $filename - * @return boolean - */ - private static function _isReadable($filename) - { - if (!$fh = @fopen($filename, 'r', true)) { - return false; - } - @fclose($fh); - return true; - } - -} diff --git a/includes/libs/DbSimple/Zend/Cache/Backend/Interface.php b/includes/libs/DbSimple/Zend/Cache/Backend/Interface.php deleted file mode 100644 index 3f44e2e1..00000000 --- a/includes/libs/DbSimple/Zend/Cache/Backend/Interface.php +++ /dev/null @@ -1,99 +0,0 @@ -<?php -/** - * Zend Framework - * - * LICENSE - * - * This source file is subject to the new BSD license that is bundled - * with this package in the file LICENSE.txt. - * It is also available through the world-wide-web at this URL: - * http://framework.zend.com/license/new-bsd - * If you did not receive a copy of the license and are unable to - * obtain it through the world-wide-web, please send an email - * to license@zend.com so we can send you a copy immediately. - * - * @category Zend - * @package Zend_Cache - * @subpackage Zend_Cache_Backend - * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) - * @license http://framework.zend.com/license/new-bsd New BSD License - * @version $Id: Interface.php 24593 2012-01-05 20:35:02Z matthew $ - */ - - -/** - * @package Zend_Cache - * @subpackage Zend_Cache_Backend - * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) - * @license http://framework.zend.com/license/new-bsd New BSD License - */ -interface Zend_Cache_Backend_Interface -{ - /** - * Set the frontend directives - * - * @param array $directives assoc of directives - */ - public function setDirectives($directives); - - /** - * Test if a cache is available for the given id and (if yes) return it (false else) - * - * Note : return value is always "string" (unserialization is done by the core not by the backend) - * - * @param string $id Cache id - * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested - * @return string|false cached datas - */ - public function load($id, $doNotTestCacheValidity = false); - - /** - * Test if a cache is available or not (for the given id) - * - * @param string $id cache id - * @return mixed|false (a cache is not available) or "last modified" timestamp (int) of the available cache record - */ - public function test($id); - - /** - * Save some string datas into a cache record - * - * Note : $data is always "string" (serialization is done by the - * core not by the backend) - * - * @param string $data Datas to cache - * @param string $id Cache id - * @param array $tags Array of strings, the cache record will be tagged by each string entry - * @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime) - * @return boolean true if no problem - */ - public function save($data, $id, $tags = array(), $specificLifetime = false); - - /** - * Remove a cache record - * - * @param string $id Cache id - * @return boolean True if no problem - */ - public function remove($id); - - /** - * Clean some cache records - * - * Available modes are : - * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used) - * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used) - * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags - * ($tags can be an array of strings or a single string) - * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags} - * ($tags can be an array of strings or a single string) - * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags - * ($tags can be an array of strings or a single string) - * - * @param string $mode Clean mode - * @param array $tags Array of tags - * @return boolean true if no problem - */ - public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()); - -} diff --git a/includes/setup/cli.class.php b/includes/setup/cli.class.php index 14cda6cf..ef16296f 100644 --- a/includes/setup/cli.class.php +++ b/includes/setup/cli.class.php @@ -152,24 +152,14 @@ abstract class CLI if ($timestamp) $msg = str_pad(date('H:i:s'), 10); - switch ($lvl) + $msg .= match ($lvl) { - case self::LOG_ERROR: // red critical error - $msg .= '['.self::red('ERR').'] '; - break; - case self::LOG_WARN: // yellow notice - $msg .= '['.self::yellow('WARN').'] '; - break; - case self::LOG_OK: // green success - $msg .= '['.self::green('OK').'] '; - break; - case self::LOG_INFO: // blue info - $msg .= '['.self::blue('INFO').'] '; - break; - case self::LOG_BLANK: - $msg .= ' '; - break; - } + self::LOG_ERROR => '['.self::red('ERR').'] ', // red critical error + self::LOG_WARN => '['.self::yellow('WARN').'] ', // yellow notice + self::LOG_OK => '['.self::green('OK').'] ', // green success + self::LOG_INFO => '['.self::blue('INFO').'] ', // blue info + default => ' ' + }; $msg .= $txt; } @@ -288,7 +278,12 @@ abstract class CLI continue; // stream_get_contents is always blocking under WIN - fgets should work similary as php always receives a terminated line of text - $chars = str_split(OS_WIN ? fgets(STDIN) : stream_get_contents(STDIN)); + $chars = str_split(OS_WIN ? fgets(STDIN) : stream_get_contents(STDIN)); + + // $chars can be empty if used non-interactive + if (!$chars) + return false; + $ordinals = array_map('ord', $chars); if ($ordinals[0] == self::CHR_ESC) diff --git a/includes/setup/datatypes/primitives.php b/includes/setup/datatypes/primitives.php new file mode 100644 index 00000000..2bf355bc --- /dev/null +++ b/includes/setup/datatypes/primitives.php @@ -0,0 +1,104 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + + +abstract class Primitive +{ + public const /* int */ SIZE = 1; + public const /* string */ PACK_FMT = 'x'; + + protected string $data; + + public function __construct(BinaryFile|string $data) + { + if (is_string($data)) + $this->data = $data; + else + $this->data = $data->read(static::SIZE); + } + + public function pack() : string + { + return $this->data; + } + + public function unpack() : mixed + { + return current(unpack(static::PACK_FMT, $this->data)); + } + + public function __debugInfo() : array + { + return [$this->unpack()]; + } +} + +class Char extends Primitive +{ + public const /* int */ SIZE = 1; + public const /* string */ PACK_FMT = 'C'; + + public function unpack() : string + { + return chr(parent::unpack()); + } +} + +class Boolean extends Primitive +{ + public const /* int */ SIZE = 1; + public const /* string */ PACK_FMT = 'C'; + + public function unpack() : string + { + return !!(parent::unpack()); + } +} + +class UInt8 extends Primitive +{ + public const /* int */ SIZE = 1; + public const /* string */ PACK_FMT = 'C'; +} + +class Int8 extends Primitive +{ + public const /* int */ SIZE = 1; + public const /* string */ PACK_FMT = 'c'; +} + +class UInt16 extends Primitive +{ + public const /* int */ SIZE = 2; + public const /* string */ PACK_FMT = 'v'; +} + +class Int16 extends Primitive +{ + public const /* int */ SIZE = 2; + public const /* string */ PACK_FMT = 's'; +} + +class UInt32 extends Primitive +{ + public const /* int */ SIZE = 4; + public const /* string */ PACK_FMT = 'V'; +} + +class Int32 extends Primitive +{ + public const /* int */ SIZE = 4; + public const /* string */ PACK_FMT = 'l'; +} + +class Double extends Primitive +{ + public const /* int */ SIZE = 4; + public const /* string */ PACK_FMT = 'f'; +} + +?> diff --git a/includes/setup/files/binaryfile.class.php b/includes/setup/files/binaryfile.class.php new file mode 100644 index 00000000..c34dba3d --- /dev/null +++ b/includes/setup/files/binaryfile.class.php @@ -0,0 +1,202 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + + +class BinaryFile +{ + private /*res*/ $handle = null; + private string $data = ''; + private int $pos = 0; + + protected int $filesize = 0; + + public string $error = ''; + + public function __construct(string $file, private bool $inRAM = true) + { + if (!file_exists($file)) + { + $this->error = 'file '.$file.' not found'; + return; + } + + if (!$this->handle = fopen($file, 'rb')) + { + $this->error = 'failed to open file '.$file; + return; + } + + $this->filesize = filesize($file); + + if ($inRAM) + $this->data = file_get_contents($file); + } + + public function __destruct() + { + $this->close(); + } + + + /**********************/ + /* direct file access */ + /**********************/ + + public function read(int $bytes) : ?string + { + if ($this->error || !is_resource($this->handle) || $bytes < 0) + return null; + + $start = $this->pos; + $this->pos += $bytes; + + if ($this->inRAM) + return substr($this->data, $start, $bytes); + else + return fread($this->handle, $bytes); + } + + public function readOffset(int $bytes, int $offset, bool $jumpBack = true) : ?string + { + if ($this->error || !is_resource($this->handle)) + return null; + + if ($jumpBack) + $curPos = $this->inRAM ? $this->pos : ftell($this->handle); + + $this->seek($offset); + + $str = $this->read($bytes); + + if ($jumpBack) + $this->seek($curPos); + + return $str; + } + + public function seek(int $pos) : int + { + if (!is_resource($this->handle)) + return 0; + + if ($pos < 0) + $pos = 0; + if ($pos > $this->filesize) + $pos = $this->filesize; + + $this->pos = $pos; + + if (!$this->inRAM) + fseek($this->handle, $pos, SEEK_SET); + + return $pos; + } + + public function ffwd(int $bytes) : int + { + if (!is_resource($this->handle)) + return 0; + + $curPos = $this->inRAM ? $this->pos : ftell($this->handle); + + if ($curPos + $bytes < 0) + $bytes -= $curPos; + if ($curPos + $bytes > $this->filesize) + $bytes -= $this->filesize; + + $this->pos += $bytes; + + if ($this->inRAM) + return $this->pos; + + fseek($this->handle, $bytes, SEEK_CUR); + return ftell($this->handle); + } + + public function close() : void + { + if (is_resource($this->handle)) + fclose($this->handle); + } + + public function tell() : int + { + if (!is_resource($this->handle)) + return 0; + + return $this->inRAM ? $this->pos : ftell($this->handle); + } + + /******************/ + /* read Primitive */ + /******************/ + + public function readInt8() : ?Int8 + { + if (!is_resource($this->handle)) + return null; + return new Int8($this); + } + + public function readInt16() : ?Int16 + { + if (!is_resource($this->handle)) + return null; + return new Int16($this); + } + + public function readInt32() : ?Int32 + { + if (!is_resource($this->handle)) + return null; + return new Int32($this); + } + + public function readUInt8() : ?UInt8 + { + if (!is_resource($this->handle)) + return null; + return new UInt8($this); + } + + public function readUInt16() : ?UInt16 + { + if (!is_resource($this->handle)) + return null; + return new UInt16($this); + } + + public function readUInt32() : ?UInt32 + { + if (!is_resource($this->handle)) + return null; + return new UInt32($this); + } + + public function readFloat() : ?Double + { + if (!is_resource($this->handle)) + return null; + return new Double($this); + } + + public function readChar() : ?Char + { + if (!is_resource($this->handle)) + return null; + return new Char($this); + } + + public function readBool() : ?Boolean + { + if (!is_resource($this->handle)) + return null; + return new Boolean($this); + } +} + +?> diff --git a/includes/setup/files/dbcfile.class.php b/includes/setup/files/dbcfile.class.php new file mode 100644 index 00000000..25d9d170 --- /dev/null +++ b/includes/setup/files/dbcfile.class.php @@ -0,0 +1,83 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + + +class DBCFile extends BinaryFile +{ + private const /* string */ MAGIC = 'WDBC'; + private const /* int */ HEADER_SIZE = 16; + + private readonly int $stringSize; + private readonly int $stringOffset; + + public readonly int $nCols; + public readonly int $nRows; + public readonly int $recordSize; + + public function __construct(string $file) + { + parent::__construct($file); + + if ($this->filesize < strlen(self::MAGIC) + self::HEADER_SIZE) + { + $this->error = 'file '.$file.' too small for a dbc'; + $this->close(); + return; + } + + if ($this->read(4) != self::MAGIC) + { + $this->error = 'file '.$file.' has incorrect magic bytes'; + $this->close(); + return; + } + + [, $this->nRows, $this->nCols, $this->recordSize, $this->stringSize] = unpack(UInt32::PACK_FMT.'4', $this->read(self::HEADER_SIZE)); + $this->stringOffset = strlen(self::MAGIC) + self::HEADER_SIZE + $this->recordSize * $this->nRows; + + if ($this->stringOffset + $this->stringSize != $this->filesize) + { + $this->error = 'file '.$file.' has unexpected size - expected: '.($this->stringOffset + $this->stringSize).' has: '.$this->filesize; + $this->close(); + return; + } + } + + public function readRecord(string $colFmt = "V*") : array + { + return unpack($colFmt, $this->read($this->recordSize)); + } + + public function readString() : ?string + { + $x = $this->readUInt32(); + if (is_null($x)) + return null; + + return $this->getStringFromBlock($x->unpack()); + } + + public function getStringFromBlock(int $offset) : ?string + { + $curPos = $this->tell(); + + $this->seek($this->stringOffset + $offset); + + // apparently it is more efficient to read more than one byte at once..? + $str = ''; + while (($pos = strpos($str, "\0")) === false) + $str .= $this->read(255); + + $str = substr($str, 0, $pos); + + $this->seek($curPos); + + return $pos ? $str : null; + } +} + +?> diff --git a/includes/setup/timer.class.php b/includes/setup/timer.class.php index a9c22c15..7c36620d 100644 --- a/includes/setup/timer.class.php +++ b/includes/setup/timer.class.php @@ -8,9 +8,9 @@ if (!defined('AOWOW_REVISION')) class Timer { - private $t_cur = 0; - private $t_new = 0; - private $intv = 0; + private float $t_cur = 0; + private float $t_new = 0; + private float $intv = 0; public function __construct(int $intervall) { diff --git a/includes/type.class.php b/includes/type.class.php index 822b31f1..6676009b 100644 --- a/includes/type.class.php +++ b/includes/type.class.php @@ -152,7 +152,7 @@ abstract class Type if (!(self::$data[$type][self::IDX_FLAGS] & self::FLAG_DB_TYPE)) return []; - return DB::Aowow()->selectCol('SELECT `id` FROM ?# WHERE `id` IN (?a)', self::$data[$type][self::IDX_LIST_OBJ]::$dataTable, (array)$ids); + return DB::Aowow()->selectCol('SELECT `id` FROM %n WHERE `id` IN %in', self::$data[$type][self::IDX_LIST_OBJ]::$dataTable, (array)$ids); } public static function hasIcon(int $type) : bool diff --git a/includes/user.class.php b/includes/user.class.php index e310239f..9fa1bd59 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -29,7 +29,28 @@ class User public static function init() { - self::setIP(); + # set ip # + + $ipAddr = ''; + foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'] as $env) + { + if ($rawIp = getenv($env)) + { + if ($env == 'HTTP_X_FORWARDED') + $rawIp = explode(',', $rawIp)[0]; // [ip, proxy1, proxy2] + + if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) + break; + + if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) + break; + } + } + + self::$ip = $ipAddr ?: null; + + + # set locale # if (isset($_SESSION['locale']) && $_SESSION['locale'] instanceof Locale) self::$preferedLoc = $_SESSION['locale']->validate() ?? Locale::getFallback(); @@ -38,36 +59,42 @@ class User else self::$preferedLoc = Locale::getFallback(); - // session have a dataKey to access the JScripts (yes, also the anons) - if (empty($_SESSION['dataKey'])) + + # set basic data # + + if (empty($_SESSION['dataKey'])) // session have a dataKey to access the JScripts (yes, also the anons) $_SESSION['dataKey'] = Util::createHash(); // just some random numbers for identification purpose self::$dataKey = $_SESSION['dataKey']; - self::$agent = $_SERVER['HTTP_USER_AGENT']; + self::$agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; if (!self::$ip) return false; - // check IP bans - if ($ipBan = DB::Aowow()->selectRow('SELECT `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `ip` = ? AND `type` = ?d', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT)) + + # check IP bans # + + if ($ipBan = DB::Aowow()->selectRow('SELECT `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ::account_bannedips WHERE `ip` = %s AND `type` = %i', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT)) { if ($ipBan['count'] > Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active']) return false; else if (!$ipBan['active']) - DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `ip` = ?', self::$ip); + DB::Aowow()->qry('DELETE FROM ::account_bannedips WHERE `ip` = %s', self::$ip); } - // try to restore session + + # try to restore session # + if (empty($_SESSION['user'])) return false; - $session = DB::Aowow()->selectRow('SELECT `userId`, `expires` FROM ?_account_sessions WHERE `status` = ?d AND `sessionId` = ?', SESSION_ACTIVE, session_id()); + $session = DB::Aowow()->selectRow('SELECT `userId`, `expires` FROM ::account_sessions WHERE `status` = %i AND `sessionId` = %s', SESSION_ACTIVE, session_id()); $userData = DB::Aowow()->selectRow( 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email`, a.`debug`, a.`avatar`, a.`avatarborder` - FROM ?_account a - LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() - LEFT JOIN ?_account_reputation r ON a.`id` = r.`userId` - WHERE a.`id` = ?d + FROM ::account a + LEFT JOIN ::account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() + LEFT JOIN ::account_reputation r ON a.`id` = r.`userId` + WHERE a.`id` = %i GROUP BY a.`id`', $_SESSION['user'] ); @@ -79,20 +106,20 @@ class User } else if ($session['expires'] && $session['expires'] < time()) { - DB::Aowow()->query('UPDATE ?_account_sessions SET `touched` = ?d, `status` = ?d WHERE `sessionId` = ?', time(), SESSION_EXPIRED, session_id()); + DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `status` = %i WHERE `sessionId` = %s', time(), SESSION_EXPIRED, session_id()); self::destroy(); return false; } else if ($session['userId'] != $userData['id']) // what in the name of fuck..? { // Don't know why, don't know how .. doesn't matter, both parties are out. - DB::Aowow()->query('UPDATE ?_account_sessions SET `touched` = ?d, `status` = ?d WHERE `userId` IN (?a) AND `status` = ?d', time(), SESSION_FORCED_LOGOUT, [$userData['id'], $session['userId']], SESSION_ACTIVE); + DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `status` = %i WHERE `userId` IN %in AND `status` = %i', time(), SESSION_FORCED_LOGOUT, [$userData['id'], $session['userId']], SESSION_ACTIVE); trigger_error('User::init - tried to resume session "'.session_id().'" of user #'.$_SESSION['user'].' linked to session data for user #'.$session['userId'].' Kicked both!', E_USER_ERROR); self::destroy(); return false; } - DB::Aowow()->query('UPDATE ?_account_sessions SET `touched` = ?d, `expires` = IF(`expires`, ?d, 0) WHERE `sessionId` = ?', time(), time() + Cfg::get('SESSION_TIMEOUT_DELAY'), session_id()); + DB::Aowow()->qry('UPDATE ::account_sessions SET `touched` = %i, `expires` = IF(`expires`, %i, 0) WHERE `sessionId` = %s', time(), time() + Cfg::get('SESSION_TIMEOUT_DELAY'), session_id()); if ($loc = Locale::tryFrom($userData['locale'])) self::$preferedLoc = $loc; @@ -100,7 +127,7 @@ class User // reset expired account statuses if ($userData['statusTimer'] && $userData['statusTimer'] < time() && $userData['status'] != ACC_STATUS_NEW) { - DB::Aowow()->query('UPDATE ?_account SET `status` = ?d, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `id` = ?d', ACC_STATUS_NONE, User::$id); + DB::Aowow()->qry('UPDATE ::account SET `status` = %i, `statusTimer` = 0, `token` = "", `updateValue` = "" WHERE `id` = %i', ACC_STATUS_NONE, User::$id); $userData['status'] = ACC_STATUS_NONE; } @@ -122,91 +149,57 @@ class User self::$email = $userData['email']; self::$avatarborder = $userData['avatarborder']; - if (Cfg::get('PROFILER_ENABLE')) - { - $conditions = [['OR', ['user', self::$id], ['ap.accountId', self::$id]]]; - if (!self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) - $conditions[] = [['cuFlags', PROFILER_CU_DELETED, '&'], 0]; - self::$profiles = (new LocalProfileList($conditions)); - } + # reset premium options # - // reset premium options if (!self::isPremium()) { if ($userData['avatar'] == 2) { - DB::Aowow()->query('UPDATE ?_account SET `avatar` = 1 WHERE `id` = ?d', self::$id); - DB::Aowow()->query('UPDATE ?_account_avatars SET `current` = 0 WHERE `userId` = ?d', self::$id); + DB::Aowow()->qry('UPDATE ::account SET `avatar` = 1 WHERE `id` = %i', self::$id); + DB::Aowow()->qry('UPDATE ::account_avatars SET `current` = 0 WHERE `userId` = %i', self::$id); } // avatar borders // do not reset, it's just not sent to the browser } - // stuff, that updates on a daily basis goes here (if you keep you session alive indefinitly, the signin-handler doesn't do very much) - // - consecutive visits - // - votes per day - // - reputation for daily visit + + # update daily limits # + if (!self::isBanned()) { - $lastLogin = DB::Aowow()->selectCell('SELECT `curLogin` FROM ?_account WHERE `id` = ?d', self::$id); + $lastLogin = DB::Aowow()->selectCell('SELECT `curLogin` FROM ::account WHERE `id` = %i', self::$id); // either the day changed or the last visit was >24h ago if (date('j', $lastLogin) != date('j') || (time() - $lastLogin) > 1 * DAY) { - // daily votes (we need to reset this one) + // - daily votes (we need to reset this one) self::$dailyVotes = self::getMaxDailyVotes(); - DB::Aowow()->query( - 'UPDATE ?_account - SET `dailyVotes` = ?d, `prevLogin` = `curLogin`, `curLogin` = UNIX_TIMESTAMP(), `prevIP` = `curIP`, `curIP` = ? - WHERE `id` = ?d', + DB::Aowow()->qry( + 'UPDATE ::account + SET `dailyVotes` = %i, `prevLogin` = `curLogin`, `curLogin` = UNIX_TIMESTAMP(), `prevIP` = `curIP`, `curIP` = ? + WHERE `id` = %i', self::$dailyVotes, self::$ip, self::$id ); - // gain rep for daily visit + // - gain reputation for daily visit if (!(self::isBanned()) && !self::isInGroup(U_GROUP_PENDING)) Util::gainSiteReputation(self::$id, SITEREP_ACTION_DAILYVISIT); - // increment consecutive visits (next day or first of new month and not more than 48h) - // i bet my ass i forgot a corner case + // - increment consecutive visits (next day or first of new month and not more than 48h) if ((date('j', $lastLogin) + 1 == date('j') || (date('j') == 1 && date('n', $lastLogin) != date('n'))) && (time() - $lastLogin) < 2 * DAY) - DB::Aowow()->query('UPDATE ?_account SET `consecutiveVisits` = `consecutiveVisits` + 1 WHERE `id` = ?d', self::$id); + DB::Aowow()->qry('UPDATE ::account SET `consecutiveVisits` = `consecutiveVisits` + 1 WHERE `id` = %i', self::$id); else - DB::Aowow()->query('UPDATE ?_account SET `consecutiveVisits` = 0 WHERE `id` = ?d', self::$id); + DB::Aowow()->qry('UPDATE ::account SET `consecutiveVisits` = 0 WHERE `id` = %i', self::$id); } } return true; } - private static function setIP() : void - { - $ipAddr = ''; - $method = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR']; - - foreach ($method as $m) - { - if ($rawIp = getenv($m)) - { - if ($m == 'HTTP_X_FORWARDED') - $rawIp = explode(',', $rawIp)[0]; // [ip, proxy1, proxy2] - - // check IPv4 - if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) - break; - - // check IPv6 - if ($ipAddr = filter_var($rawIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) - break; - } - } - - self::$ip = $ipAddr ?: null; - } - public static function save(bool $toDB = false) { $_SESSION['user'] = self::$id; @@ -214,7 +207,7 @@ class User // $_SESSION['dataKey'] does not depend on user login status and is set in User::init() if (self::isLoggedIn() && $toDB) - DB::Aowow()->query('UPDATE ?_account SET `locale` = ? WHERE `id` = ?', self::$preferedLoc->value, self::$id); + DB::Aowow()->qry('UPDATE ::account SET `locale` = %s WHERE `id` = %s', self::$preferedLoc->value, self::$id); } public static function destroy() @@ -236,7 +229,7 @@ class User /* auth mechanisms */ /*******************/ - public static function authenticate(string $login, string $password) : int + public static function authenticate(string $login, #[\SensitiveParameter] string $password) : int { $userId = 0; @@ -259,17 +252,17 @@ class User return $result; } - private static function authSelf(string $nameOrEmail, string $password, int &$userId) : int + private static function authSelf(string $nameOrEmail, #[\SensitiveParameter] string $password, int &$userId) : int { if (!self::$ip) return AUTH_INTERNAL_ERR; // handle login try limitation - $ipBan = DB::Aowow()->selectRow('SELECT `ip`, `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ?', IP_BAN_TYPE_LOGIN_ATTEMPT, self::$ip); + $ipBan = DB::Aowow()->selectRow('SELECT `ip`, `count`, IF(`unbanDate` > UNIX_TIMESTAMP(), 1, 0) AS "active" FROM ::account_bannedips WHERE `type` = %i AND `ip` = %s', IP_BAN_TYPE_LOGIN_ATTEMPT, self::$ip); if (!$ipBan || !$ipBan['active']) // no entry exists or time expired; set count to 1 - DB::Aowow()->query('REPLACE INTO ?_account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (?, ?d, 1, UNIX_TIMESTAMP() + ?d)', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_BLOCK')); + DB::Aowow()->qry('REPLACE INTO ::account_bannedips (`ip`, `type`, `count`, `unbanDate`) VALUES (%s, %i, 1, UNIX_TIMESTAMP() + %i)', self::$ip, IP_BAN_TYPE_LOGIN_ATTEMPT, Cfg::get('ACC_FAILED_AUTH_BLOCK')); else // entry already exists; increment count - DB::Aowow()->query('UPDATE ?_account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + ?d WHERE `ip` = ?', Cfg::get('ACC_FAILED_AUTH_BLOCK'), self::$ip); + DB::Aowow()->qry('UPDATE ::account_bannedips SET `count` = `count` + 1, `unbanDate` = UNIX_TIMESTAMP() + %i WHERE `ip` = %s', Cfg::get('ACC_FAILED_AUTH_BLOCK'), self::$ip); if ($ipBan && $ipBan['count'] >= Cfg::get('ACC_FAILED_AUTH_COUNT') && $ipBan['active']) return AUTH_IPBANNED; @@ -278,12 +271,11 @@ class User $query = DB::Aowow()->SelectRow( 'SELECT a.`id`, a.`passHash`, BIT_OR(ab.`typeMask`) AS "bans", a.`status` - FROM ?_account a - LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() - WHERE { a.`email` = ? } { a.`login` = ? } AND `status` <> ?d + FROM ::account a + LEFT JOIN ::account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() + WHERE %if', $email, 'a.`email` %else a.`login` %end = %s AND `status` <> %i GROUP BY a.`id`', - $email ?: DBSIMPLE_SKIP, - !$email ? $nameOrEmail : DBSIMPLE_SKIP, + $nameOrEmail, ACC_STATUS_DELETED ); @@ -294,7 +286,7 @@ class User return AUTH_WRONGPASS; // successfull auth; clear bans for this IP - DB::Aowow()->query('DELETE FROM ?_account_bannedips WHERE `type` = ?d AND `ip` = ?', IP_BAN_TYPE_LOGIN_ATTEMPT, self::$ip); + DB::Aowow()->qry('DELETE FROM ::account_bannedips WHERE `type` = %i AND `ip` = %s', IP_BAN_TYPE_LOGIN_ATTEMPT, self::$ip); if ($query['bans'] & (ACC_BAN_PERM | ACC_BAN_TEMP)) return AUTH_BANNED; @@ -304,12 +296,12 @@ class User return AUTH_OK; } - private static function authRealm(string $name, string $password, int &$userId) : int + private static function authRealm(string $name, #[\SensitiveParameter] string $password, int &$userId) : int { if (!DB::isConnectable(DB_AUTH)) return AUTH_INTERNAL_ERR; - $wow = DB::Auth()->selectRow('SELECT a.id, a.salt, a.verifier, ab.active AS hasBan FROM account a LEFT JOIN account_banned ab ON ab.id = a.id AND active <> 0 WHERE username = ? LIMIT 1', $name); + $wow = DB::Auth()->selectRow('SELECT a.id, a.salt, a.verifier, ab.active AS hasBan FROM account a LEFT JOIN account_banned ab ON ab.id = a.id AND active <> 0 WHERE username = %s LIMIT 1', $name); if (!$wow) return AUTH_WRONGUSER; @@ -327,7 +319,7 @@ class User return AUTH_OK; } - private static function authExtern(string $nameOrEmail, string $password, int &$userId) : int + private static function authExtern(string $nameOrEmail, #[\SensitiveParameter] string $password, int &$userId) : int { if (!file_exists('config/extAuth.php')) { @@ -365,14 +357,14 @@ class User // create a linked account for our settings if necessary private static function checkOrCreateInDB(int $extId, string $name, int $userGroup = -1) : int { - if ($_ = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE `extId` = ?d', $extId)) + if ($_ = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE `extId` = %i', $extId)) { if ($userGroup >= U_GROUP_NONE) - DB::Aowow()->query('UPDATE ?_account SET `userGroups` = ?d WHERE `extId` = ?d', $userGroup, $extId); + DB::Aowow()->qry('UPDATE ::account SET `userGroups` = %i WHERE `extId` = %i', $userGroup, $extId); return $_; } - $newId = DB::Aowow()->query('INSERT IGNORE INTO ?_account (`extId`, `passHash`, `username`, `joinDate`, `prevIP`, `prevLogin`, `locale`, `status`, `userGroups`) VALUES (?d, "", ?, UNIX_TIMESTAMP(), ?, UNIX_TIMESTAMP(), ?d, ?d, ?d)', + $newId = DB::Aowow()->qry('INSERT IGNORE INTO ::account (`extId`, `passHash`, `username`, `joinDate`, `prevIP`, `prevLogin`, `locale`, `status`, `userGroups`) VALUES (%i, "", %s, UNIX_TIMESTAMP(), %s, UNIX_TIMESTAMP(), %i, %i, %i)', $extId, $name, $_SERVER["REMOTE_ADDR"] ?? '', @@ -387,24 +379,15 @@ class User return $newId ?: 0; } - private static function createSalt() : string + // crypt used by us + public static function hashCrypt(#[\SensitiveParameter] string $pass) : string { - $algo = '$2a'; - $strength = '$09'; - $salt = '$'.Util::createHash(22); - - return $algo.$strength.$salt; + return password_hash($pass, PASSWORD_BCRYPT, ['cost' => 15]); } - // crypt used by aowow - public static function hashCrypt(string $pass) : string + public static function verifyCrypt(#[\SensitiveParameter] string $pass, string $hash) : bool { - return crypt($pass, self::createSalt()); - } - - public static function verifyCrypt(string $pass, string $hash) : bool - { - return $hash === crypt($pass, $hash); + return password_verify($pass, $hash); } // SRP6 used by TC @@ -526,7 +509,7 @@ class User return; self::$dailyVotes--; - DB::Aowow()->query('UPDATE ?_account SET `dailyVotes` = ?d WHERE `id` = ?d', self::$dailyVotes, self::$id); + DB::Aowow()->qry('UPDATE ::account SET `dailyVotes` = %i WHERE `id` = %i', self::$dailyVotes, self::$id); } public static function getCurrentDailyVotes() : int @@ -578,6 +561,7 @@ class User $gUser['downvoteRep'] = Cfg::get('REP_REQ_DOWNVOTE'); $gUser['upvoteRep'] = Cfg::get('REP_REQ_UPVOTE'); $gUser['characters'] = self::getCharacters(); + $gUser['completion'] = self::getCompletion(); $gUser['excludegroups'] = self::$excludeGroups; if (self::$debug) @@ -616,11 +600,11 @@ class User if (!self::isLoggedIn() || self::isBanned()) return $result; - $res = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `name` FROM ?_account_weightscales WHERE `userId` = ?d', self::$id); + $res = DB::Aowow()->selectPairs('SELECT `id`, `name` FROM ::account_weightscales WHERE `userId` = %i', self::$id); if (!$res) return $result; - $weights = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `field` AS ARRAY_KEY2, `val` FROM ?_account_weightscale_data WHERE `id` IN (?a)', array_keys($res)); + $weights = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `field` AS ARRAY_KEY2, `val` FROM ::account_weightscale_data WHERE `id` IN %in', array_keys($res)); foreach ($weights as $id => $data) $result[] = array_merge(['name' => $res[$id], 'id' => $id], $data); @@ -637,9 +621,8 @@ class User if (!Cfg::get('PROFILER_ENABLE')) return $result; - $modes = [1 => 'excludes', 2 => 'includes']; - foreach ($modes as $mode => $field) - if ($ex = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `typeId` AS ARRAY_KEY2, `typeId` FROM ?_account_excludes WHERE `mode` = ?d AND `userId` = ?d', $mode, self::$id)) + foreach ([Profiler::COMPLETION_EXCLUDE => 'excludes', Profiler::COMPLETION_INCLUDE => 'includes'] as $mode => $field) + if ($ex = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `typeId` AS ARRAY_KEY2, `typeId` FROM ::account_excludes WHERE `mode` = %i AND `userId` = %i', $mode, self::$id)) foreach ($ex as $type => $ids) $result[$field][$type] = array_values($ids); @@ -648,7 +631,7 @@ class User public static function getCharacters() : array { - if (!self::$profiles) + if (!self::loadProfiles()) return []; return self::$profiles->getJSGlobals(PROFILEINFO_CHARACTER); @@ -656,7 +639,7 @@ class User public static function getProfiles() : array { - if (!self::$profiles) + if (!self::loadProfiles()) return []; return self::$profiles->getJSGlobals(PROFILEINFO_PROFILE); @@ -664,7 +647,7 @@ class User public static function getPinnedCharacter() : array { - if (!self::$profiles) + if (!self::loadProfiles()) return []; $realms = Profiler::getRealms(); @@ -688,7 +671,7 @@ class User if (!self::isLoggedIn() || self::isBanned(ACC_BAN_GUIDE)) return $result; - if ($guides = DB::Aowow()->select('SELECT `id`, `title`, `url` FROM ?_guides WHERE `userId` = ?d AND `status` <> ?d', self::$id, GuideMgr::STATUS_ARCHIVED)) + if ($guides = DB::Aowow()->selectAssoc('SELECT `id`, `title`, `url` FROM ::guides WHERE `userId` = %i AND `status` <> %i', self::$id, GuideMgr::STATUS_ARCHIVED)) { // fix url array_walk($guides, fn(&$x) => $x['url'] = '?guide='.($x['url'] ?: $x['id'])); @@ -703,7 +686,7 @@ class User if (!self::isLoggedIn()) return []; - return DB::Aowow()->selectCol('SELECT `name` AS ARRAY_KEY, `data` FROM ?_account_cookies WHERE `userId` = ?d', self::$id); + return DB::Aowow()->selectPairs('SELECT `name`, `data` FROM ::account_cookies WHERE `userId` = %i', self::$id); } public static function getFavorites() : array @@ -711,14 +694,14 @@ class User if (!self::isLoggedIn() || self::isBanned()) return []; - $res = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `typeId` AS ARRAY_KEY2, `typeId` FROM ?_account_favorites WHERE `userId` = ?d', self::$id); + $res = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `typeId` AS ARRAY_KEY2, `typeId` FROM ::account_favorites WHERE `userId` = %i', self::$id); if (!$res) return []; $data = []; foreach ($res as $type => $ids) { - $tc = Type::newList($type, [['id', array_values($ids)]]); + $tc = Type::newList($type, [['id', $ids]]); if (!$tc || $tc->error) continue; @@ -732,6 +715,81 @@ class User return $data; } + + public static function getCompletion() : array + { + if (!self::loadProfiles()) + return []; + + $ids = []; + foreach (self::$profiles->iterate() as $_) + if (!self::$profiles->isCustom()) + $ids[] = self::$profiles->id; + + if (!$ids) + return []; + + $completion = []; + + $x = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `questId` AS ARRAY_KEY2, `questId` FROM ::profiler_completion_quests WHERE `id` IN %in', $ids); + $completion[Type::QUEST] = $x ? array_map(array_values(...), $x) : []; + + $x = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `achievementId` AS ARRAY_KEY2, `achievementId` FROM ::profiler_completion_achievements WHERE `id` IN %in', $ids); + $completion[Type::ACHIEVEMENT] = $x ? array_map(array_values(...), $x) : []; + + $x = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `titleId` AS ARRAY_KEY2, `titleId` FROM ::profiler_completion_titles WHERE `id` IN %in', $ids); + $completion[Type::TITLE] = $x ? array_map(array_values(...), $x) : []; + + $completion[Type::ITEM] = []; + + $spells = DB::Aowow()->selectAssoc( + 'SELECT pcs.`id` AS ARRAY_KEY, pcs.`spellId` AS ARRAY_KEY2, pcs.`spellId`, i.`id` AS "itemId" + FROM ::spell s + JOIN ::profiler_completion_spells pcs ON s.`id` = pcs.`spellId` + LEFT JOIN ::items i ON i.`spellId1` IN %in AND i.`spellId2` = pcs.`spellId` + WHERE s.`typeCat` IN %in AND pcs.`id` IN %in', + LEARN_SPELLS, [-5, -6, 9, 11], $ids + ); + + if ($spells) + { + $completion[Type::SPELL] = array_map(fn($x) => array_column($x, 'spellId'), $spells); + + if ($recipes = array_map(fn($x) => array_filter(array_column($x, 'itemId')), $spells)) + foreach ($ids as $id) // array_merge_recursive does not respect numeric keys + $completion[Type::ITEM][$id] = array_merge($completion[Type::ITEM][$id] ?? [], $recipes[$id] ?? []); + } + else + $completion[Type::SPELL] = []; + + // init empty result sets + foreach ($completion as &$c) + foreach ($ids as $id) + if (!isset($c[$id])) + $c[$id] = []; + + return $completion; + } + + private static function loadProfiles() : bool + { + if (!Cfg::get('PROFILER_ENABLE')) + return false; + + if (self::$profiles === null) + { + $ap = DB::Aowow()->selectCol('SELECT `profileId` FROM ::account_profiles WHERE `accountId` = %i', self::$id); + + // the old approach [DB::OR, ['user', self::$id], ['ap.accountId', self::$id]] caused keys to not get used + $conditions = $ap ? [[DB::OR, ['user', self::$id], ['id', $ap]]] : [['user', self::$id]]; + if (!self::isInGroup(U_GROUP_ADMIN | U_GROUP_BUREAU)) + $conditions[] = ['deleted', 0]; + + self::$profiles = (new LocalProfileList($conditions)); + } + + return !!self::$profiles->getFoundIDs(); + } } ?> diff --git a/includes/utilities.php b/includes/utilities.php index 61084146..39e9f297 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -6,6 +6,27 @@ if (!defined('AOWOW_REVISION')) die('illegal access'); + +// PHP 8.4 polyfill +if (version_compare(PHP_VERSION, '8.4.0') < 0) +{ + function array_find(array $array, callable $callback) : mixed + { + foreach ($array as $k => $v) + if ($callback($v, $k)) + return $array[$k]; + return null; + } + + function array_find_key(array $array, callable $callback) : mixed + { + foreach ($array as $k => $v) + if ($callback($v, $k)) + return $k; + return null; + } +} + class SimpleXML extends \SimpleXMLElement { public function addCData(string $cData) : \SimpleXMLElement @@ -35,7 +56,7 @@ abstract class Util private static $perfectGems = null; - public static $regions = array( + public static $regions = array( 'us', 'eu', 'kr', 'tw', 'cn', 'dev' ); @@ -48,18 +69,6 @@ abstract class Util 'clothChestArmor', 'leatherChestArmor', 'mailChestArmor', 'plateChestArmor' ); - public static $weightScales = array( - 'agi', 'int', 'sta', 'spi', 'str', 'health', 'mana', 'healthrgn', 'manargn', - 'armor', 'blockrtng', 'block', 'defrtng', 'dodgertng', 'parryrtng', 'resirtng', - 'atkpwr', 'feratkpwr', 'armorpenrtng', 'critstrkrtng', 'exprtng', 'hastertng', 'hitrtng', 'splpen', - 'splpwr', 'arcsplpwr', 'firsplpwr', 'frosplpwr', 'holsplpwr', 'natsplpwr', 'shasplpwr', - 'dmg', 'mledps', 'rgddps', 'mledmgmin', 'rgddmgmin', 'mledmgmax', 'rgddmgmax', 'mlespeed', 'rgdspeed', - 'arcres', 'firres', 'frores', 'holres', 'natres', 'shares', - 'mleatkpwr', 'mlecritstrkrtng', 'mlehastertng', 'mlehitrtng', 'rgdatkpwr', 'rgdcritstrkrtng', 'rgdhastertng', 'rgdhitrtng', - 'splcritstrkrtng', 'splhastertng', 'splhitrtng', 'spldmg', 'splheal', - 'nsockets' - ); - public static $dateFormatInternal = "Y/m/d H:i:s"; public static $changeLevelString = '<a href="javascript:;" onmousedown="return false" class="tip" style="color: white; cursor: pointer" onclick="$WH.g_staticTooltipLevelClick(this, null, 0)" onmouseover="$WH.Tooltip.showAtCursor(event, \'<span class=\\\'q2\\\'>\' + LANG.tooltip_changelevel + \'</span>\')" onmousemove="$WH.Tooltip.cursorUpdate(event)" onmouseout="$WH.Tooltip.hide()"><!--lvl-->%s</a>'; @@ -104,152 +113,23 @@ abstract class Util return [$notes, $severity]; } - private static $execTime = 0.0; - - public static function execTime(bool $set = false) : string - { - if ($set) - { - self::$execTime = microTime(true); - return ''; - } - - if (!self::$execTime) - return ''; - - $newTime = microTime(true); - $tDiff = $newTime - self::$execTime; - self::$execTime = $newTime; - - return self::formatTime($tDiff * 1000, true); - } - public static function formatMoney(int $qty) : string { - $money = ''; + if ($qty <= 0) + return ''; - if ($qty >= 10000) - { - $g = floor($qty / 10000); - $money .= '<span class="moneygold">'.$g.'</span> '; - $qty -= $g * 10000; - } + $parts = []; - if ($qty >= 100) - { - $s = floor($qty / 100); - $money .= '<span class="moneysilver">'.$s.'</span> '; - $qty -= $s * 100; - } + if ($g = intdiv($qty, 10000)) + $parts[] = '<span class="moneygold">'.$g.'</span>'; - if ($qty > 0) - $money .= '<span class="moneycopper">'.$qty.'</span>'; + if ($s = intdiv($qty % 10000, 100)) + $parts[] = '<span class="moneysilver">'.$s.'</span>'; - return $money; - } + if ($c = ($qty % 100)) + $parts[] = '<span class="moneycopper">'.$c.'</span>'; - public static function parseTime(int $msec) : array - { - $time = [0, 0, 0, 0, 0]; - - if ($_ = ($msec % 1000)) - $time[0] = $_; - - $sec = $msec / 1000; - - if ($sec >= 3600 * 24) - { - $time[4] = floor($sec / 3600 / 24); - $sec -= $time[4] * 3600 * 24; - } - - if ($sec >= 3600) - { - $time[3] = floor($sec / 3600); - $sec -= $time[3] * 3600; - } - - if ($sec >= 60) - { - $time[2] = floor($sec / 60); - $sec -= $time[2] * 60; - } - - if ($sec > 0) - { - $time[1] = (int)$sec; - $sec -= $time[1]; - } - - return $time; - } - - public static function formatTime(int $msec, bool $short = false) : string - { - [$ms, $s, $m, $h, $d] = self::parseTime(abs($msec)); - // \u00A0 is  , but also usable by js - - if ($short) - { - if ($_ = round($d / 365)) - return $_."\u{00A0}".Lang::timeUnits('ab', 0); - if ($_ = round($d / 30)) - return $_."\u{00A0}".Lang::timeUnits('ab', 1); - if ($_ = round($d / 7)) - return $_."\u{00A0}".Lang::timeUnits('ab', 2); - if ($_ = round($d)) - return $_."\u{00A0}".Lang::timeUnits('ab', 3); - if ($_ = round($h)) - return $_."\u{00A0}".Lang::timeUnits('ab', 4); - if ($_ = round($m)) - return $_."\u{00A0}".Lang::timeUnits('ab', 5); - if ($_ = round($s + $ms / 1000, 2)) - return $_."\u{00A0}".Lang::timeUnits('ab', 6); - if ($ms) - return $ms."\u{00A0}".Lang::timeUnits('ab', 7); - - return "0\u{00A0}".Lang::timeUnits('ab', 6); - } - else - { - $_ = $d + $h / 24; - if ($_ > 1 && !($_ % 365)) // whole years - return round(($d + $h / 24) / 365, 2)."\u{00A0}".Lang::timeUnits($d / 365 == 1 && !$h ? 'sg' : 'pl', 0); - if ($_ > 1 && !($_ % 30)) // whole months - return round(($d + $h / 24) / 30, 2)."\u{00A0}".Lang::timeUnits($d / 30 == 1 && !$h ? 'sg' : 'pl', 1); - if ($_ > 1 && !($_ % 7)) // whole weeks - return round(($d + $h / 24) / 7, 2)."\u{00A0}".Lang::timeUnits($d / 7 == 1 && !$h ? 'sg' : 'pl', 2); - if ($d) - return round($d + $h / 24, 2)."\u{00A0}".Lang::timeUnits($d == 1 && !$h ? 'sg' : 'pl', 3); - if ($h) - return round($h + $m / 60, 2)."\u{00A0}".Lang::timeUnits($h == 1 && !$m ? 'sg' : 'pl', 4); - if ($m) - return round($m + $s / 60, 2)."\u{00A0}".Lang::timeUnits($m == 1 && !$s ? 'sg' : 'pl', 5); - if ($s) - return round($s + $ms / 1000, 2)."\u{00A0}".Lang::timeUnits($s == 1 && !$ms ? 'sg' : 'pl', 6); - if ($ms) - return $ms."\u{00A0}".Lang::timeUnits($ms == 1 ? 'sg' : 'pl', 7); - - return "0\u{00A0}".Lang::timeUnits('pl', 6); - } - } - - public static function formatTimeDiff(int $sec) : string - { - $delta = abs(time() - $sec); - - [, $s, $m, $h, $d] = self::parseTime($delta * 1000); - - if ($delta > (1 * MONTH)) // use absolute - return date(Lang::main('dateFmtLong'), $sec); - else if ($delta > (2 * DAY)) // days ago - return Lang::main('timeAgo', [$d . ' ' . Lang::timeUnits('pl', 3)]); - else if ($h) // hours, minutes ago - return Lang::main('timeAgo', [$h . ' ' . Lang::timeUnits('ab', 4) . ' ' . $m . ' ' . Lang::timeUnits('ab', 5)]); - else if ($m) // minutes, seconds ago - return Lang::main('timeAgo', [$m . ' ' . Lang::timeUnits('ab', 5) . ' ' . $s . ' ' . Lang::timeUnits('ab', 6)]); - else // seconds ago - return Lang::main('timeAgo', [$s . ' ' . Lang::timeUnits($s == 1 ? 'sg' : 'pl', 6)]); + return implode(' ', $parts); } // pageTexts, questTexts and mails @@ -288,9 +168,9 @@ abstract class Util $from = array( '/\$g\s*([^:;]*)\s*:\s*([^:;]*)\s*(:?[^:;]*);/ui',// directed gender-reference $g<male>:<female>:<refVariable> - '/\$t([^;]+);/ui', // nonsense, that the client apparently ignores + '/\$t([^;]+);/ui', // HK rank. $t<male>:<female>; (maybe male/female if pvp unranked? Gets replaced with current HK rank.) '/<([^\"=\/>]+\s[^\"=\/>]+)>/ui', // emotes (workaround: at least one whitespace and never " or = between brackets) - '/\$(\d+)w/ui', // worldState(?)-ref found on some pageTexts $1234w + '/\$(\d+)w/ui', // worldState(%d)-ref found on some pageTexts $1234w '/\$c/i', // class-ref '/\$r/i', // race-ref '/\$n/i', // name-ref @@ -299,7 +179,7 @@ abstract class Util $toMD = array( '<\1/\2>', - '', + '<'.implode('/', Lang::game('pvpRank', 1)).'>', '<\1>', '[span class=q0>WorldState #\1[/span]', '<'.Lang::game('class').'>', @@ -310,7 +190,7 @@ abstract class Util $toHTML = array( '<\1/\2>', - '', + '<'.implode('/', Lang::game('pvpRank', 1)).'>', '<\1>', '<span class="q0">WorldState #\1</span>', '<'.Lang::game('class').'>', @@ -342,8 +222,11 @@ abstract class Util return 'b'.$_; } - public static function htmlEscape($data) + public static function htmlEscape(string|array|null $data) : string|array { + if (empty($data)) // null, '', [] and not "0" + return ''; + if (is_array($data)) { foreach ($data as &$v) @@ -355,8 +238,11 @@ abstract class Util return htmlspecialchars($data, ENT_QUOTES | ENT_DISALLOWED | ENT_HTML5, 'utf-8'); } - public static function jsEscape($data) + public static function jsEscape(string|array|null $data) : string|array { + if (empty($data)) // null, '', [] and not "0" + return ''; + if (is_array($data)) { foreach ($data as &$v) @@ -424,7 +310,7 @@ abstract class Util } // for item and spells - public static function setRatingLevel(int $level, int $statId, int $val) : string + public static function setRatingLevel(int $level, int $statId, int $val, bool $interactive = false) : string { if (in_array($statId, [Stat::DEFENSE_RTG, Stat::DODGE_RTG, Stat::PARRY_RTG, Stat::BLOCK_RTG, Stat::RESILIENCE_RTG]) && $level < 34) $level = 34; @@ -450,7 +336,9 @@ abstract class Util if (!in_array($statId, [Stat::DEFENSE_RTG, Stat::EXPERTISE_RTG])) $result .= '%'; - return Lang::item('ratingString', [$statId, $result, $level]); + $result = Lang::item('ratingString', [$statId, $result, $level]); + + return $interactive ? sprintf(self::$setRatingLevelString, $level, $statId, $val, $result) : $result; } // default ucFirst doesn't convert UTF-8 chars (php 8.4 finally implemented this .. see ya in 2027) @@ -472,6 +360,15 @@ abstract class Util return mb_strtolower($str); } + public static function strrev(string $str) : string + { + $out = ''; + for ($i = 1, $len = mb_strlen($str); $i <= $len; $i++) + $out .= mb_substr($str, -$i, 1); + + return $out; + } + // doesn't handle scientific notation .. why would you input 3e3 for 3000..? public static function checkNumeric(mixed &$data, int $typeCast = NUM_ANY) : bool { @@ -554,6 +451,18 @@ abstract class Util } } + public static function createNumRange(int $min, int $max, string $delim = '', ?callable $fn = null) : string + { + if (!$min && !$max) + return ''; + + $fn ??= fn($x) => $x; + $_min = $fn($min); + $_max = $fn($max); + + return $max > $min ? $_min . ($delim ?: Lang::game('valueDelim')) . $_max : $_min; + } + public static function validateLogin(?string $val) : string { if ($_ = self::validateEmail($val)) @@ -712,8 +621,8 @@ abstract class Util if (empty($miscData['id']) || empty($miscData['voterId'])) return false; - DB::Aowow()->query( // delete old votes the user has cast - 'DELETE FROM ?_account_reputation WHERE sourceA = ?d AND sourceB = ?d AND userId = ?d AND action IN (?a)', + DB::Aowow()->qry( // delete old votes the user has cast + 'DELETE FROM ::account_reputation WHERE sourceA = %i AND sourceB = %i AND userId = %i AND action IN %in', $miscData['id'], $miscData['voterId'], $user, @@ -757,13 +666,13 @@ abstract class Util break; } - $x = array_merge($x, array( + $x += array( 'userId' => $user, 'action' => $action, - 'date' => !empty($miscData['date']) ? $miscData['date'] : time() - )); + 'date' => $miscData['date'] ?? time() + ); - return DB::Aowow()->query('INSERT IGNORE INTO ?_account_reputation (?#) VALUES (?a)', array_keys($x), array_values($x)); + return DB::Aowow()->qry('INSERT IGNORE INTO ::account_reputation %v', $x); } public static function toJSON($data, $forceFlags = 0) @@ -782,43 +691,12 @@ abstract class Util return $json; } - public static function createSqlBatchInsert(array $data) : array - { - if (!count($data) || !is_array(reset($data))) - return []; - - $nRows = 100; - $nItems = count(reset($data)); - $result = []; - $buff = []; - - foreach ($data as $d) - { - if (count($d) != $nItems) - return []; - - $d = array_map(fn($x) => $x === null ? 'NULL' : DB::Aowow()->escape($x), $d); - - $buff[] = implode(',', $d); - - if (count($buff) >= $nRows) - { - $result[] = '('.implode('),(', $buff).')'; - $buff = []; - } - } - - if ($buff) - $result[] = '('.implode('),(', $buff).')'; - - return $result; - } /*****************/ /* file handling */ /*****************/ - public static function writeFile($file, $content) + public static function writeFile(string $file, string $content) : bool { $success = false; @@ -966,7 +844,7 @@ abstract class Util { // prepare score-lookup if (empty(self::$perfectGems)) - self::$perfectGems = DB::World()->selectCol('SELECT perfectItemType FROM skill_perfect_item_template WHERE requiredSpecialization = ?d', 55534); + self::$perfectGems = DB::World()->selectCol('SELECT perfectItemType FROM skill_perfect_item_template WHERE requiredSpecialization = %i', 55534); // epic - WotLK - increased stats / profession specific (Dragon's Eyes) if ($profSpec) @@ -1142,6 +1020,55 @@ abstract class Util return $bits; } + public static function indexBitBlob(string $bitBlob, int $blobSize = 32) : array + { + $indizes = []; + $blocks = explode(' ', trim($bitBlob)); + for ($i = 0; $i < count($blocks); $i++) + for ($j = 0; $j < $blobSize; $j++) + if ($blocks[$i] & (1 << $j)) + $indizes[] = $j + ($i * $blobSize); + + return $indizes; + } + + public static function toString(mixed $var) : string + { + if (is_array($var)) + return '[' . implode(', ', array_map(self::toString(...), $var)) . ']'; + + if (is_object($var)) + { + // hm, respect object stringability? + // if ($var instanceof Stringable) + // return (string)$var; + + $buff = []; + foreach ($var as $k => $v) + $buff[] = $k.':'.self::toString($v); + + return '{' . implode(', ', $buff) . '}'; + } + + return (string)$var; + } + + public static function nodeAttributes(?array $attribs) : string + { + if (!$attribs) + return ''; + + return array_reduce(array_keys($attribs), fn($carry, $name) => $carry . match(gettype($attribs[$name])) + { + 'boolean' => ' ' . $attribs[$name] ? $name : '', + 'integer', + 'double' => ' ' . $name . '="' . $attribs[$name] . '"', + 'string' => ' ' . $name . '="' . self::htmlEscape($attribs[$name]) . '"', + 'array' => ' ' . $name . '="' . implode(' ', self::htmlEscape($attribs[$name])) . '"', + default => '' + }, ''); + } + public static function buildPosFixMenu(int $mapId, float $posX, float $posY, int $type, int $guid, int $parentArea = 0, int $parentFloor = 0) : array { $points = WorldPosition::toZonePos($mapId, $posX, $posY); @@ -1215,7 +1142,7 @@ abstract class Util if ($expiration) { $vars += array_fill(0, 9, null); // vsprintf requires all unused indizes to also be set... - $vars[9] = Util::formatTime($expiration * 1000); + $vars[9] = DateTime::formatTimeElapsed($expiration * 1000, 0); } if ($vars) diff --git a/index.php b/index.php index 672173b2..6be334f8 100644 --- a/index.php +++ b/index.php @@ -13,9 +13,23 @@ $pageParam = ''; parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $query); foreach ($query as $page => $param) { - $page = preg_replace('/[^\w\-]/i', '', $page); + // could be an array + if (!is_string($param)) + { + $pageCall = ''; // just .. fail + break; + } - $pageCall = Util::lower($page); + // fix page calls - pages like search use the page call directly and expect it as lower case + if (preg_match('/[A-Z]/', $page)) + { + $url = explode('=', $_SERVER['REQUEST_URI'], 2); + $page = Util::lower(array_shift($url)).($url ? '=' . $url[0] : ''); + header('Location: '.$page, true, 302); + exit; + } + + $pageCall = preg_replace('/[^\w\-]/i', '', $page); $pageParam = $param ?? ''; break; // only use first k/v-pair to determine page } @@ -50,7 +64,7 @@ try { $responder = new \StdClass; // 1. try specialized response - if (file_exists('endpoints/'.$pageCall.'/'.$file.'.php')) + if (file_exists('endpoints/'.$pageCall.'/'.$file.'.php') && $pageCall != $file) { require_once 'endpoints/'.$pageCall.'/'.$file.'.php'; diff --git a/localization/datetime.class.php b/localization/datetime.class.php new file mode 100644 index 00000000..98d60c6e --- /dev/null +++ b/localization/datetime.class.php @@ -0,0 +1,222 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + + +class DateTime extends \DateTimeImmutable +{ + // in msec yr mo w d h m s ms + private const /* array */ RANGE = [31557600000, 2629800000, 604800000, 86400000, 3600000, 60000, 1000, 1]; + private const /* string */ NBSP = "\u{00A0}"; // \u00A0 is usable by js + + public function __construct(int $seconds = 0) + { + $datetime = $seconds ? date(DATE_ATOM, $seconds) : 'now'; + parent::__construct($datetime); + } + + /** + * Adaptive, human-readable format of a date + * exact date if larger than 1 month + * relative days/time if smaller than 1 month + * + * @param int $timestamp unix timestamp to display + * @param bool $withTime [optional] append time on exact dates + * @return string adaptive date/time string + */ + public function formatDate(int $timestamp, bool $withTime = false) : string + { + $txt = ''; + $elapsed = abs($this->getTimestamp() - $timestamp); + + $today = new DateTime(); + $eventDay = new DateTime(time() - $elapsed); + + $todayMidnight = $today->setTime(0, 0); + $eventDayMidnight = $eventDay->setTime(0, 0); + + $delta = $todayMidnight->diff($eventDayMidnight, true)->days; + + if ($elapsed >= 2592000) /* More than a month ago */ + $txt = Lang::main('date_on') . $eventDay->formatDateSimple($withTime); + else if ($delta > 1) + $txt = Lang::main('ddaysago', [$delta]); + else if ($elapsed >= 43200) + { + if ($today->format('j') == $eventDay->format('j')) + $txt = Lang::main('today'); + else + $txt = Lang::main('yesterday'); + + $txt = $eventDay->formatTimeSimple($txt); + } + else /* Less than 12 hours ago */ + $txt = Lang::main('date_ago', [self::formatTimeElapsed($elapsed * 1000)]); + + return $txt; + } + + /** + * Human-readable format of a date. Optionally append time of day. + * + * @param bool $withTime [optional] affixes day time + * @return string a formatted date string. + */ + public function formatDateSimple(bool $withTime = false) : string + { + $txt = ''; + $day = $this->format('d'); + $month = $this->format('m'); + $year = $this->format('Y'); + + if ($year <= 1970) + $txt .= Lang::main('unknowndate_stc'); + else + $txt .= Lang::main('date_simple', [$day, $month, $year]); + + if ($withTime) + $txt = $this->formatTimeSimple($txt); + + return $txt; + } + + /** + * Human-readable format of the time of day. + * + * @param string $txt [optional] text to affeix the day time to + * @param bool $noPrefix [optional] don't use " at " to affix time of day to $txt + * @return string a formatted time of day string. + */ + public function formatTimeSimple(string $txt = '', bool $noPrefix = false) : string + { + $hours = $this->format('G'); + $minutes = $this->format('i'); + + $txt .= ($noPrefix ? ' ' : Lang::main('date_at')); + + if ($hours == 12) + $txt .= Lang::main('noon'); + else if ($hours == 0) + $txt .= Lang::main('midnight'); + else if ($hours > 12) + $txt .= ($hours - 12) . ':' . $minutes . ' ' . Lang::main('pm'); + else + $txt .= $hours . ':' . $minutes . ' ' . Lang::main('am'); + + return $txt; + } + + /** + * Calculate component values from timestamp + * + * @param int $msec time in milliseconds to parse + * @return int[] [msec, sec, min, hr, day] + */ + public static function parse(int $msec) : array + { + $time = [0, 0, 0, 0, 0]; + $msec = abs($msec); + + for ($i = 3; $i < count(self::RANGE); ++$i) + { + if ($msec < self::RANGE[$i]) + continue; + + $time[7 - $i] = intVal($msec / self::RANGE[$i]); + $msec %= self::RANGE[$i]; + } + + return $time; + } + + /** + * Human-readable longform format of a timespan. + * + * @param int $delay time in milliseconds to format + * @return string a formatted time string. If an error occured "n/a" (localized) is returned + */ + public static function formatTimeElapsedFloat(int $delay) : string + { + $delay = abs($delay); + $nbsp = Lang::getLocale()->isLogographic() ? '' : self::NBSP; + + for ($i = 0; $i < count(self::RANGE); ++$i) + { + if ($delay < self::RANGE[$i]) + continue; + + $v = round($delay / self::RANGE[$i], 2); + return $v . $nbsp . Lang::timeUnits($v === 1.0 ? 'sg' : 'pl', $i); + } + + return '0' . $nbsp . Lang::timeUnits('pl', 6); // 0 seconds + } + + /** + * Human-readable format of a timespan. + * + * @param int $delay time in milliseconds to format + * @param int $maxRange [optional] time unit index - 0 (year) ... 7 (milliseconds) + * @return string a formatted time string. If an error occured "n/a" (localized) is returned + */ + public static function formatTimeElapsed(int $delay, int $maxRange = 3) : string + { + if ($maxRange > 7 || $maxRange < 0) + $maxRange = 3; // default: days + + $subunit = [1, 3, 3, -1, 5, -1, 7, -1]; + $delay = max($delay, 1); + + for ($i = $maxRange; $i < count(self::RANGE); ++$i) + { + if ($delay >= self::RANGE[$i]) + { + $i1 = $i; + $v1 = floor($delay / self::RANGE[$i1]); + + if ($subunit[$i1] != -1) + { + $i2 = $subunit[$i1]; + $delay %= self::RANGE[$i1]; + $v2 = floor($delay / self::RANGE[$i2]); + $nbsp = Lang::getLocale()->isLogographic() ? '' : self::NBSP; + + if ($v2 > 0) + return self::OMG($v1, $i1, true) . $nbsp . self::OMG($v2, $i2, true); + } + + return self::OMG($v1, $i1, false); + } + } + + return Lang::main('n_a'); + } + + /** + * internal number formatter + * + * @param int $value unit value + * @param int $unit time unit index 0 (year) ... 7 (milliseconds) + * @param bool $abbrv use abbreviation + * @return string value + unit + */ + private static function OMG(int $value, int $unit, bool $abbrv) : string + { + if ($abbrv && !Lang::timeUnits('ab', $unit)) + $abbrv = false; + + $nbsp = Lang::getLocale()->isLogographic() ? '' : self::NBSP; + + return $value .= $nbsp . match(true) + { + $abbrv => Lang::timeUnits('ab', $unit), + $value == 1 => Lang::timeUnits('sg', $unit), + default => Lang::timeUnits('pl', $unit) + }; + } +} + +?> diff --git a/localization/lang.class.php b/localization/lang.class.php index 8e7cfa79..7579c1de 100644 --- a/localization/lang.class.php +++ b/localization/lang.class.php @@ -239,10 +239,10 @@ class Lang $tmp = []; if ($cuFlags & CUSTOM_DISABLED) - $tmp[] = '[tooltip name=disabledHint]'.Util::jsEscape(self::main('disabledHint')).'[/tooltip][span class=tip tooltip=disabledHint]'.Util::jsEscape(self::main('disabled')).'[/span]'; + $tmp[] = '[tooltip name=disabledHint]'.self::main('disabledHint').'[/tooltip][span class=tip tooltip=disabledHint]'.self::main('disabled').'[/span]'; if ($cuFlags & CUSTOM_SERVERSIDE) - $tmp[] = '[tooltip name=serversideHint]'.Util::jsEscape(self::main('serversideHint')).'[/tooltip][span class=tip tooltip=serversideHint]'.Util::jsEscape(self::main('serverside')).'[/span]'; + $tmp[] = '[tooltip name=serversideHint]'.self::main('serversideHint').'[/tooltip][span class=tip tooltip=serversideHint]'.self::main('serverside').'[/span]'; if ($cuFlags & CUSTOM_UNAVAILABLE) $tmp[] = self::main('unavailable'); @@ -257,7 +257,7 @@ class Lang { $locks = []; $ids = []; - $lock = DB::Aowow()->selectRow('SELECT * FROM ?_lock WHERE `id` = ?d', $lockId); + $lock = DB::Aowow()->selectRow('SELECT * FROM ::lock WHERE `id` = %i', $lockId); if (!$lock) return $locks; @@ -267,75 +267,87 @@ class Lang $rank = $lock['reqSkill'.$i]; $name = ''; - if ($lock['type'.$i] == LOCK_TYPE_ITEM) + switch ($lock['type'.$i]) { - $name = ItemList::getName($prop); - if (!$name) - continue; - - if ($fmt == self::FMT_HTML) - $name = $interactive ? '<a class="q1" href="?item='.$prop.'">'.$name.'</a>' : '<span class="q1">'.$name.'</span>'; - else if ($interactive && $fmt == self::FMT_MARKUP) - { - $name = '[item='.$prop.']'; - $ids[Type::ITEM][] = $prop; - } - else - $name = $prop; - - } - else if ($lock['type'.$i] == LOCK_TYPE_SKILL) - { - $name = self::spell('lockType', $prop); - if (!$name) - continue; - - // skills - if (in_array($prop, [1, 2, 3, 20])) - { - $skills = array( - 1 => SKILL_LOCKPICKING, - 2 => SKILL_HERBALISM, - 3 => SKILL_MINING, - 20 => SKILL_INSCRIPTION - ); + case LOCK_TYPE_ITEM: + $name = ItemList::getName($prop); + if (!$name) + continue 2; if ($fmt == self::FMT_HTML) - $name = $interactive ? '<a href="?skill='.$skills[$prop].'">'.$name.'</a>' : '<span class="q1">'.$name.'</span>'; + $name = $interactive ? '<a class="q1" href="?item='.$prop.'">'.$name.'</a>' : '<span class="q1">'.$name.'</span>'; else if ($interactive && $fmt == self::FMT_MARKUP) { - $name = '[skill='.$skills[$prop].']'; - $ids[Type::SKILL][] = $skills[$prop]; + $name = '[item='.$prop.']'; + $ids[Type::ITEM][] = $prop; + } + + break; + case LOCK_TYPE_SKILL: + $name = self::spell('lockType', $prop); + if (!$name) + continue 2; + + // skills + if (in_array($prop, [1, 2, 3, 20])) + { + $skills = array( + 1 => SKILL_LOCKPICKING, + 2 => SKILL_HERBALISM, + 3 => SKILL_MINING, + 20 => SKILL_INSCRIPTION + ); + + if ($fmt == self::FMT_HTML) + $name = $interactive ? '<a href="?skill='.$skills[$prop].'">'.$name.'</a>' : '<span class="q1">'.$name.'</span>'; + else if ($interactive && $fmt == self::FMT_MARKUP) + { + $name = '[skill='.$skills[$prop].']'; + $ids[Type::SKILL][] = $skills[$prop]; + } + else + $name = SkillList::getName($prop); + + if ($rank > 0) + $name .= ' ('.$rank.')'; + } + // Lockpicking + else if ($prop == 4) + { + if ($fmt == self::FMT_HTML) + $name = $interactive ? '<a href="?spell=1842">'.$name.'</a>' : '<span class="q1">'.$name.'</span>'; + else if ($interactive && $fmt == self::FMT_MARKUP) + { + $name = '[spell=1842]'; + $ids[Type::SPELL][] = 1842; + } + } + // exclude unusual stuff + else if (User::isInGroup(U_GROUP_STAFF)) + { + if ($rank > 0) + $name .= ' ('.$rank.')'; } else - $name = $skills[$prop]; + continue 2; + break; + case LOCK_TYPE_SPELL: + $name = SpellList::getName($prop); + if (!$name) + continue 2; - if ($rank > 0) - $name .= ' ('.$rank.')'; - } - // Lockpicking - else if ($prop == 4) - { if ($fmt == self::FMT_HTML) - $name = $interactive ? '<a href="?spell=1842">'.$name.'</a>' : '<span class="q1">'.$name.'</span>'; + $name = $interactive ? '<a class="q1" href="?spell='.$prop.'">'.$name.'</a>' : '<span class="q1">'.$name.'</span>'; else if ($interactive && $fmt == self::FMT_MARKUP) { - $name = '[spell=1842]'; - $ids[Type::SPELL][] = 1842; + $name = '[spell='.$prop.']'; + $ids[Type::SPELL][] = $prop; } - // else $name = $name - } - // exclude unusual stuff - else if (User::isInGroup(U_GROUP_STAFF)) - { - if ($rank > 0) - $name .= ' ('.$rank.')'; - } - else - continue; + + break; + default: + continue 2; } - else - continue; $locks[$lock['type'.$i] == LOCK_TYPE_ITEM ? $prop : -$prop] = $name; } @@ -435,7 +447,7 @@ class Lang return implode(', ', $tmp); } - public static function getClassString(int $classMask, ?array &$ids = [], int $fmt = self::FMT_HTML) : string + public static function getClassString(int $classMask, array &$ids = [], int $fmt = self::FMT_HTML) : string { $classMask &= ChrClass::MASK_ALL; // clamp to available classes.. @@ -459,7 +471,7 @@ class Lang return implode(', ', $tmp); } - public static function getRaceString(int $raceMask, ?array &$ids = [], int $fmt = self::FMT_HTML) : string + public static function getRaceString(int $raceMask, array &$ids = [], int $fmt = self::FMT_HTML) : string { $raceMask &= ChrRace::MASK_ALL; // clamp to available races.. @@ -523,7 +535,7 @@ class Lang if ($msec < 0) $msec = 0; - $time = Util::parseTime($msec); // [$ms, $s, $m, $h, $d] + $time = DateTime::parse($msec); // [$ms, $s, $m, $h, $d] $mult = [0, 1000, 60, 60, 24]; $total = 0; $ref = []; @@ -540,33 +552,22 @@ class Lang if (!$msec) return self::vspf($ref[0], [0]); - if ($concat) - { - for ($i = 4; $i > 0; $i--) - { - $total += $time[$i]; - if (isset($ref[$i]) && ($total || ($i == 1 && !$result))) - { - $result[] = self::vspf($ref[$i], [$total]); - $total = 0; - } - else - $total *= $mult[$i]; - } - - return implode(', ', $result); - } - for ($i = 4; $i > 0; $i--) { $total += $time[$i]; - if (isset($ref[$i]) && ($total || $i == 1)) - return self::vspf($ref[$i], [$total + ($time[$i-1] ?? 0) / $mult[$i]]); + if (isset($ref[$i]) && ($total || ($i == 1 && !$result))) + { + if (!$concat) + return self::vspf($ref[$i], [$total + ($time[$i-1] ?? 0) / $mult[$i]]); + + $result[] = self::vspf($ref[$i], [$total]); + $total = 0; + } else $total *= $mult[$i]; } - return ''; + return implode(', ', $result); } private static function vspf(null|array|string $var, array $args = []) : null|array|string @@ -708,7 +709,7 @@ class Lang $spfVars[0] = $linkType; break; case 'talent': - if ($spell = DB::Aowow()->selectCell('SELECT `spell` FROM ?_talents WHERE `id` = ?d AND `rank` = ?d', $linkVars[0], $linkVars[1])) + if ($spell = DB::Aowow()->selectCell('SELECT `spell` FROM ::talents WHERE `id` = %i AND `rank` = %i', $linkVars[0], $linkVars[1])) { $spfVars[0] = 'spell'; $spfVars[1] = $spell; @@ -746,28 +747,28 @@ class Lang }, $var); // |2 - frFR preposition: de |2 <word> - $var = preg_replace_callback('/\|2\s?(\w)/i', function ($m) + $var = preg_replace_callback('/\|2\s?(.)/i', function ($m) { - [$_, $word] = $m; + [$_, $char] = $m; - switch (strtolower($word[1])) + switch (strtolower($char)) { case 'h': if (self::$locale != Locale::FR) - return 'de ' . $word; + return 'de ' . $char; case 'a': case 'e': case 'i': case 'o': case 'u': - return "d'" . $word; + return "d'" . $char; default: - return 'de ' . $word; + return 'de ' . $char; } }, $var); // |3 - ruRU declinations |3-<caseIdx>(<word>) - $var = preg_replace_callback('/\|3-(\d)\(([^\)]+)\)/iu', function ($m) + $var = preg_replace_callback('/\|3-(\d+)\(([^\)]+)\)/iu', function ($m) { [$_, $caseIdx, $word] = $m; @@ -777,7 +778,7 @@ class Lang if (preg_match('/\P{Cyrillic}/iu', $word)) // not in cyrillic script return $word; - if ($declWord = DB::Aowow()->selectCell('SELECT dwc.word FROM ?_declinedwordcases dwc JOIN ?_declinedword dc ON dwc.wordId = dc.id WHERE dwc.caseIdx = ?d AND dc.word = ?', $caseIdx, $word)) + if ($declWord = DB::Aowow()->selectCell('SELECT dwc.`word` FROM ::declinedwordcases dwc JOIN ::declinedword dc ON dwc.`wordId` = dc.`id` WHERE dwc.`caseIdx` = %i AND dc.`word` = %s', $caseIdx, $word)) return $declWord; return $word; diff --git a/localization/locale_dede.php b/localization/locale_dede.php index af068ae1..1ca688b4 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -134,8 +134,25 @@ $lang = array( 'dateFmtShort' => "d.m.Y", 'dateFmtLong' => "d.m.Y \u\m H:i", 'dateFmtIntl' => "d. MMMM y", - 'timeAgo' => 'vor %s', 'nfSeparators' => ['.', ','], + 'n_a' => "n. v.", + + // date time + 'date' => "Datum", + 'date_colon' => "Datum: ", + 'date_on' => "am ", + 'date_ago' => "vor %s", + 'date_at' => " um ", + 'date_to' => " bis ", + 'date_simple' => '%1$d.%2$d.%3$d', + 'unknowndate' => "Unbekanntes Datum", + 'ddaysago' => "vor %d Tagen", + 'today' => "heute", + 'yesterday' => "gestern", + 'noon' => "Mittag", + 'midnight' => "Mitternacht", + 'am' => "vormittags", + 'pm' => "nachmittags", // error 'intError' => "Ein interner Fehler ist aufgetreten.", @@ -236,6 +253,8 @@ $lang = array( 'atCaptain' => "Teamkapitän", 'atSize' => "Größe: ", 'profiler' => "Charakter-Profiler", + 'completion' => "Vervollständigung: ", + 'attainedBy' => "Erlangt von %d%% der Profile", 'notFound' => array( 'guild' => "Diese Gilde existiert nicht oder wurde noch nicht in die Datenbank übernommen.", 'arenateam' => "Dieses Arena Team existiert nicht oder wurde noch nicht in die Datenbank übernommen.", @@ -364,6 +383,7 @@ $lang = array( 'school' => "Magieart", 'type' => "Art: ", 'valueDelim' => " - ", // " bis " + 'target' => "<Ziel>", 'pvp' => "PvP", 'honorPoints' => "Ehrenpunkte", @@ -378,7 +398,11 @@ $lang = array( 'phases' => "Phasen", 'mode' => "Modus: ", - 'modes' => [-1 => "Beliebig", "Normal / Normal 10", "Heroisch / Normal 25", "Heroisch 10", "Heroisch 25"], + 'modes' => array( + [-1 => "Beliebig", "Normal / Normal 10", "Heroisch / Normal 25", "Heroisch 10", "Heroisch 25"], + ["Normal", "Heroisch"], + ["Normal 10", "Normal 25", "Heroisch 10", "Heroisch 25"] + ), 'expansions' => ["Classic", "The Burning Crusade", "Wrath of the Lich King"], 'stats' => ["Stärke", "Beweglichkeit", "Ausdauer", "Intelligenz", "Willenskraft"], 'timeAbbrev' => array( @@ -478,7 +502,7 @@ $lang = array( UNIT_FLAG_IMMUNE_TO_NPC => 'Immun gegen Kreaturen', UNIT_FLAG_LOOTING => 'Lootanimation', UNIT_FLAG_PET_IN_COMBAT => 'Pet im Kampf', - UNIT_FLAG_PVP => 'PvP', + UNIT_FLAG_PVP_ENABLING => 'PvP', UNIT_FLAG_SILENCED => 'Zum Schweigen gebracht', UNIT_FLAG_CANNOT_SWIM => 'Kann nicht schwimmen', UNIT_FLAG_UNK_15 => 'UNK-15 (kann nur schwimmen)', @@ -551,11 +575,11 @@ $lang = array( UNIT_VIS_FLAGS_UNK5 => 'UNK-5' ), /*idx:3*/ array( - UNIT_BYTE1_ANIM_TIER_GROUND => 'Bodenanimationen', - UNIT_BYTE1_ANIM_TIER_SWIM => 'Schwimmanimationen', - UNIT_BYTE1_ANIM_TIER_HOVER => 'Schwebeanimationen', - UNIT_BYTE1_ANIM_TIER_FLY => 'Fluganimationen', - UNIT_BYTE1_ANIM_TIER_SUMBERGED => 'abgetauchte Animationen' + UNIT_ANIM_TIER_GROUND => 'Bodenanimationen', + UNIT_ANIM_TIER_SWIM => 'Schwimmanimationen', + UNIT_ANIM_TIER_HOVER => 'Schwebeanimationen', + UNIT_ANIM_TIER_FLY => 'Fluganimationen', + UNIT_ANIM_TIER_SUMBERGED => 'abgetauchte Animationen' ), 'bytesIdx' => ['StandState', null, 'VisFlags', 'AnimTier'], 'valueUNK' => '[span class=q10]unbenutzter Wert [b class=q1]%d[/b] übergeben an UnitFieldBytes1 auf Offset [b class=q1]%d[/b][/span]', @@ -641,18 +665,22 @@ $lang = array( SmartEvent::EVENT_ACTION_DONE => ['Aktion #[b]%1$d[/b] angefordert von anderem Skript', ''], SmartEvent::EVENT_ON_SPELLCLICK => ['Ein \'SpellClick\' wurde ausgelöst', ''], SmartEvent::EVENT_FRIENDLY_HEALTH_PCT => ['Gesundheit von #target# ist %11$s%%', 'Wiederhole alle %s'], - SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; ist in einem %3$dm Umkreis', 'Wiederhole alle %s'], - SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; ist in einem %3$dm Umkreis', 'Wiederhole alle %s'], + SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; ist in einem %3$dm Umkreis', 'Wiederhole alle %s'], + SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; ist in einem %3$dm Umkreis', 'Wiederhole alle %s'], SmartEvent::EVENT_COUNTER_SET => ['Zähler #[b]%1$d[/b] ist gleich [b]%2$d[/b]', 'Abklingzeit: %s'], SmartEvent::EVENT_SCENE_START => null, SmartEvent::EVENT_SCENE_TRIGGER => null, /* 80*/ SmartEvent::EVENT_SCENE_CANCEL => null, SmartEvent::EVENT_SCENE_COMPLETE => null, SmartEvent::EVENT_SUMMONED_UNIT_DIES => ['Durch mich beschworener (%1$d)?[npc=%1$d]:NPC; stirbt', 'Abklingzeit: %s'], - SmartEvent::EVENT_ON_SPELL_CAST => ['Bei \'cast success\' von [spell=%1$d] ', 'Abklingzeit: %s'], - SmartEvent::EVENT_ON_SPELL_FAILED => ['Bei \'cast failed\' von [spell=%1$d] ', 'Abklingzeit: %s'], - SmartEvent::EVENT_ON_SPELL_START => ['Bei \'cast start\' von [spell=%1$d] ', 'Abklingzeit: %s'], + SmartEvent::EVENT_ON_SPELL_CAST => ['Bei \'cast success\' von [spell=%1$d]', 'Abklingzeit: %s'], + SmartEvent::EVENT_ON_SPELL_FAILED => ['Bei \'cast failed\' von [spell=%1$d]', 'Abklingzeit: %s'], + SmartEvent::EVENT_ON_SPELL_START => ['Bei \'cast start\' von [spell=%1$d]', 'Abklingzeit: %s'], SmartEvent::EVENT_ON_DESPAWN => ['Beim Verschwinden', ''], + SmartEvent::EVENT_SEND_EVENT_TRIGGER => null, + SmartEvent::EVENT_AREATRIGGER_EXIT => null, + SmartEvent::EVENT_ON_AURA_APPLIED => ['Wenn Aura [spell=%1$d] angewendet wird', 'Abklingzeit: %s'], + SmartEvent::EVENT_ON_AURA_REMOVED => ['Wenn Aura [spell=%1$d] endet', 'Abklingzeit: %s'] ), 'eventFlags' => array( SmartEvent::FLAG_NO_REPEAT => 'Nicht wiederholbar', @@ -696,7 +724,7 @@ $lang = array( SmartAction::ACTION_CALL_GROUPEVENTHAPPENS => ['Erfülle Entdeckungsereignis von [quest=%1$d] für Gruppe von #target#.', ''], SmartAction::ACTION_COMBAT_STOP => ['Beende aktuellen Kampf.', ''], SmartAction::ACTION_REMOVEAURASFROMSPELL => ['Entferne(%2$d)? %2$d Aufladungen von:;(%1$d)? alle Auren:Aura [spell=%1$d]; von #target#.', 'Nur eigene Auren'], - SmartAction::ACTION_FOLLOW => ['Folge #target#(%1$d)? mit %1$dm Abstand:;(%3$d)? bis zum Erreichen von [npc=%3$d]:;.(%12$d)? Am Ende wird ein Entdeckungsereignis für [quest=%4$d] erfüllt.:;(%13$d)? Am Ende wird ein Tod von [npc=%4$d] gutgeschrieben.:;', '(%11$d)?Folgt im Winkel von\u003A %11$.2f°:;'], + SmartAction::ACTION_FOLLOW => ['Folge #target#(%1$d)? mit %1$dm Abstand:;(%3$d)? bis zum Erreichen von [npc=%3$d]:;.(%12$d)? Am Ende wird ein Entdeckungsereignis für [quest=%4$d] erfüllt.:;(%13$d)? Am Ende wird ein Tod von [npc=%4$d] gutgeschrieben.:;', '(%11$d)?Folgt im Winkel von %11$.2f°:;'], /* 30*/ SmartAction::ACTION_RANDOM_PHASE => ['Wähle zufällige Ereignisphase aus %11$s.', ''], SmartAction::ACTION_RANDOM_PHASE_RANGE => ['Wähle zufällige Ereignisphase zwischen %1$d und %2$d.', ''], SmartAction::ACTION_RESET_GOBJECT => ['Setze #target# zurück.', ''], @@ -798,8 +826,8 @@ $lang = array( SmartAction::ACTION_PLAY_ANIMKIT => null, SmartAction::ACTION_SCENE_PLAY => null, /*130*/ SmartAction::ACTION_SCENE_CANCEL => null, - SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Spawne SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Abklingzeit: %s'], - SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Despawne SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Abklingzeit: %s'], + SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Spawne SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Abklingzeit: %s'], + SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Despawne SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Abklingzeit: %s'], SmartAction::ACTION_RESPAWN_BY_SPAWNID => ['Respawne %11$s [small class=q0](GUID: %2$d)[/small]', ''], SmartAction::ACTION_INVOKER_CAST => ['Auslöser wirkt [spell=%1$d] auf #target#.(%4$d)? (max. %4$d |4Ziel:Ziele;):;', '%1$s'], SmartAction::ACTION_PLAY_CINEMATIC => ['Gebe Film #[b]%1$d[/b] für #target# wieder.', ''], @@ -1070,6 +1098,7 @@ $lang = array( 'posts' => "Forenbeiträge: " ), 'emote' => array( + 'id' => "Emote-ID: ", 'notFound' => "Dieses Emote existiert nicht.", // 'self' => "An Euch selbst", // 'target' => "An Andere mit Ziel", @@ -1102,9 +1131,10 @@ $lang = array( 'state' => ['Einmalig', 'Stetiger Zustand', 'Stetiges Emote'] ), 'enchantment' => array( + 'id' => "Verzauberungs-ID: ", + 'notFound' => "Diese Verzauberung existiert nicht.", 'details' => "Details", 'activation' => "Aktivierung", - 'notFound' => "Diese Verzauberung existiert nicht.", 'types' => array( 1 => "Zauber (Auslösung)", 3 => "Zauber (Anlegen)", 7 => "Zauber (Benutzen)", 8 => "Prismatischer Sockel", 5 => "Statistik", 2 => "Waffenschaden", 6 => "DPS", 4 => "Verteidigung" @@ -1113,10 +1143,13 @@ $lang = array( 'areatrigger' => array( 'notFound' => "Dieser Areatrigger existiert nicht.", 'foundIn' => "Dieser Areatrigger befindet sich in", + 'unnamed' => "Unbenannter Areatrigger #%d", 'types' => ['Unbenutzt', 'Gasthaus', 'Teleporter', 'Questziel', 'Smarter Trigger', 'Script'] ), 'gameObject' => array( + 'id' => "Objekt-ID: ", 'notFound' => "Dieses Objekt existiert nicht.", + 'unnamed' => "Unbenanntes Objekt #%d", 'cat' => [0 => "Anderes", 3 => "Behälter", 6 => "Fallen", 9 => "Bücher", 25 => "Fischschwärme", -5 => "Truhen", -3 => "Kräuter", -4 => "Erzadern", -2 => "Quest", -6 => "Werkzeuge"], 'type' => [ 3 => "Behälter", 6 => "", 9 => "Buch", 25 => "", -5 => "Truhe", -3 => "Kraut", -4 => "Erzvorkommen", -2 => "Quest", -6 => ""], 'unkPosition' => "Der Standort dieses Objekts ist nicht bekannt.", @@ -1130,14 +1163,15 @@ $lang = array( 'foundIn' => "Dieses Objekt befindet sich in", 'restock' => "Wird alle %s wieder aufgefüllt.", 'goFlags' => array( - GO_FLAG_IN_USE => 'In Benutzung', - GO_FLAG_LOCKED => 'Verschlossen', - GO_FLAG_INTERACT_COND => 'Nicht interagierbar', - GO_FLAG_TRANSPORT => 'Transporter', - GO_FLAG_NOT_SELECTABLE => 'Nicht anwählbar', - GO_FLAG_TRIGGERED => 'Ausgelöst', - GO_FLAG_DAMAGED => 'Belagerung beschädigt', - GO_FLAG_DESTROYED => 'Belagerung zerstört' + GO_FLAG_IN_USE => 'In Benutzung', + GO_FLAG_LOCKED => 'Verschlossen', + GO_FLAG_INTERACT_COND => 'Nicht interagierbar', + GO_FLAG_TRANSPORT => 'Transporter', + GO_FLAG_NOT_SELECTABLE => 'Nicht anwählbar', + GO_FLAG_AI_OBSTACLE => 'Ausgelöst', + GO_FLAG_FREEZE_ANIMATION => 'Pausiert Animation', + GO_FLAG_DAMAGED => 'Belagerung beschädigt', + GO_FLAG_DESTROYED => 'Belagerung zerstört' ), 'actions' => array( "None", "Animate Custom 0", "Animate Custom 1", "Animate Custom 2", "Animate Custom 3", @@ -1148,6 +1182,7 @@ $lang = array( ) ), 'npc' => array( + 'id' => "NPC-ID: ", 'notFound' => "Dieser NPC existiert nicht.", 'classification'=> "Einstufung: %s", 'petFamily' => "Tierart: ", @@ -1180,10 +1215,6 @@ $lang = array( 'mechanicimmune'=> 'Nicht anfällig für Mechanik: %s', '_extraFlags' => 'Extra Flags: ', 'versions' => 'Schwierigkeitsgrade: ', - 'modes' => array( - 1 => ["Normal", "Heroisch"], - 2 => ["10-Spieler Normal", "25-Spieler Normal", "10-Spieler Heroisch", "25-Spieler Heroisch"] - ), 'cat' => array( "Nicht kategorisiert", "Wildtiere", "Drachkin", "Dämonen", "Elementare", "Riesen", "Untote", "Humanoide", "Tiere", "Mechanisch", "Nicht spezifiziert", "Totems", "Haustiere", "Gaswolken" @@ -1243,6 +1274,7 @@ $lang = array( ) ), 'event' => array( + 'id' => "Weltereignis-ID: ", 'notFound' => "Dieses Weltereignis existiert nicht.", 'start' => "Anfang: ", 'end' => "Ende: ", @@ -1251,6 +1283,7 @@ $lang = array( 'category' => ["Nicht kategorisiert", "Feiertage", "Wiederkehrend", "Spieler vs. Spieler"] ), 'achievement' => array( + 'id' => "Erfolgs-ID: ", 'notFound' => "Dieser Erfolg existiert nicht.", 'criteria' => "Kriterien", 'points' => "Punkte", @@ -1309,9 +1342,11 @@ $lang = array( ) ), 'chrClass' => array( + 'id' => "Klassen-ID: ", 'notFound' => "Diese Klasse existiert nicht." ), 'race' => array( + 'id' => "Volks-ID: ", 'notFound' => "Dieses Volk existiert nicht.", 'racialLeader' => "Volksanführer: ", 'startZone' => "Startgebiet", @@ -1349,6 +1384,7 @@ $lang = array( ) ), 'zone' => array( + 'id' => "Gebiets-ID: ", 'notFound' => "Dieses Gebiet existiert nicht.", 'attunement' => ["Einstimmung: ", "Heroische Einstimmung: "], 'key' => ["Schlüssel: ", "Heroischer Schlüssel: "], @@ -1377,6 +1413,7 @@ $lang = array( ) ), 'quest' => array( + 'id' => "Quest-ID: ", 'notFound' => "Diese Quest existiert nicht.", '_transfer' => 'Dieses Quest wird mit <a href="?quest=%d" class="q1">%s</a> vertauscht, wenn Ihr zur <span class="icon-%s">%s</span> wechselt.', 'questLevel' => "Stufe %s", @@ -1514,6 +1551,7 @@ $lang = array( 'notFound' => "Dieses Icon existiert nicht." ), 'title' => array( + 'id' => "Titel-ID: ", 'notFound' => "Dieser Titel existiert nicht.", '_transfer' => 'Dieser Titel wird mit <a href="?title=%d" class="q1">%s</a> vertauscht, wenn Ihr zur <span class="icon-%s">%s</span> wechselt.', 'cat' => array( @@ -1521,6 +1559,7 @@ $lang = array( ) ), 'skill' => array( + 'id' => "Fertigkeits-ID: ", 'notFound' => "Diese Fertigkeit existiert nicht.", 'cat' => array( -6 => "Haustiere", -5 => "Reittiere", -4 => "Völkerfertigkeiten", 5 => "Attribute", 6 => "Waffenfertigkeiten", 7 => "Klassenfertigkeiten", 8 => "Rüstungssachverstand", @@ -1528,6 +1567,7 @@ $lang = array( ) ), 'currency' => array( + 'id' => "Währungs-ID: ", 'notFound' => "Diese Währung existiert nicht.", 'cap' => "Obergrenze: ", 'cat' => array( @@ -1549,6 +1589,7 @@ $lang = array( ) ), 'mail' => array( + 'id' => "Brief-ID: ", 'notFound' => "Dieser Brief existiert nicht.", 'attachment' => "Anlage", 'mailDelivery' => 'Ihr werdet <a href="?mail=%d">diesen Brief</a>%s%s erhalten', @@ -1559,12 +1600,14 @@ $lang = array( 'untitled' => "Unbetitelter Brief #%d" ), 'pet' => array( + 'id' => "Tierart-ID: ", 'notFound' => "Diese Tierart existiert nicht.", 'exotic' => "Exotisch", 'cat' => ["Wildheit", "Hartnäckigkeit", "Gerissenheit"], 'food' => ["Fleisch", "Fisch", "Käse", "Brot", "Fungus", "Obst", "Rohes Fleisch", "Roher Fisch"] ), 'faction' => array( + 'id' => "Fraktions-ID: ", 'notFound' => "Diese Fraktion existiert nicht.", 'spillover' => "Reputationsüberlauf", 'spilloverDesc' => "Für diese Fraktion erhaltener Ruf wird zusätzlich mit den unten aufgeführten Fraktionen anteilig verrechnet.", @@ -1580,12 +1623,13 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "Ausrüstungsset-ID: ", 'notFound' => "Dieses Ausrüstungsset existiert nicht.", '_desc' => "<b>%s</b> ist das <b>%s</b>. Es enthält %s Teile.", '_descTagless' => "<b>%s</b> ist ein Ausrüstungsset, das %s Teile enthält.", '_setBonuses' => "Setboni", '_conveyBonus' => "Das Tragen mehrerer Gegenstände aus diesem Set gewährt Eurem Charakter Boni.", - '_pieces' => "Teile", + '_pieces' => "%d Teile: ", '_unavailable' => "Dieses Ausrüstungsset ist nicht für Spieler verfügbar.", '_tag' => "Tag: ", 'summary' => "Zusammenfassung", @@ -1605,13 +1649,14 @@ $lang = array( ) ), 'spell' => array( + 'id' => "Zauber-ID: ", 'notFound' => "Dieser Zauber existiert nicht.", '_spellDetails' => "Zauberdetails", '_cost' => "Kosten", '_range' => "Reichweite", '_castTime' => "Zauberzeit", '_cooldown' => "Abklingzeit", - '_distUnit' => "Meter", + '_distUnit' => " Meter", '_forms' => "Gestalten", '_aura' => "Aura", '_effect' => "Effekt", @@ -1631,14 +1676,13 @@ $lang = array( '_rankRange' => "Rang: %d - %d", '_showXmore' => "Zeige %d weitere", - 'n_a' => "n. v.", 'normal' => "Normal", 'special' => "Besonders", 'currentArea' => '<Momentanes Gebiet>', 'discovered' => "Durch Geistesblitz erlernt", - 'ppm' => "(%s Auslösungen pro Minute)", - 'procChance' => "Procchance: ", + 'ppm' => "(%.1f Auslösungen pro Minute)", + 'procChance' => "Procchance: %.4g%%", 'starter' => "Basiszauber", 'trainingCost' => "Trainingskosten: ", 'channeled' => "Kanalisiert", @@ -1654,7 +1698,8 @@ $lang = array( 'pointsPerCP' => ", plus %s pro Combopunkt", 'stackGroup' => "Stack Gruppierung", 'linkedWith' => "Verknüpft mit", - '_scaling' => "Skalierung", + 'apMod' => " (AP mod: %.3g)", + 'spMod' => " (ZM mod: %.3g)", 'instantPhys' => "Sofort", 'castTime' => array( "Spontanzauber", @@ -1701,10 +1746,6 @@ $lang = array( -1 => "Munition", -41 => "Pyrit", -61 => "Dampfdruck", -101 => "Hitze", -121 => "Schlamm", -141 => "Blutmacht", -142 => "Wrath" ), - 'scaling' => array( - 'directSP' => "+%.2f%% der Zaubermacht zum direkten Effekt", 'directAP' => "+%.2f%% der Angriffskraft zum direkten Effekt", - 'dotSP' => "+%.2f%% der Zaubermacht pro Tick", 'dotAP' => "+%.2f%% der Angriffskraft pro Tick" - ), 'relItems' => array( 'base' => "<small>%s im Zusammenhang mit <b>%s</b> anzeigen</small>", 'link' => " oder ", @@ -1818,14 +1859,14 @@ $lang = array( /*102+ */ 'Dismiss Pet', 'Give Reputation', 'Summon Object (Trap)', 'Summon Object (Battle S.)','Summon Object (#3)', 'Summon Object (#4)', /*108+ */ 'Dispel Mechanic', 'Summon Dead Pet', 'Destroy All Totems', 'Durability Damage - Flat', 'Summon Demon', 'Resurrect with Flat Health', /*114+ */ 'Taunt', 'Durability Damage - %', 'Skin Player Corpse (PvP)', 'AoE Resurrect with % Health','Learn Skill', 'Apply Area Aura - Pet', -/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', null, 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', +/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', '', 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', /*126+ */ 'Spell Steal ', 'Prospect', 'Apply Area Aura - Friend', 'Apply Area Aura - Enemy', 'Redirect Done Threat %', 'Play Sound', /*132+ */ 'Play Music', 'Unlearn Specialization', 'Kill Credit 2', 'Call Pet', 'Heal for % of Total Health','Give % of Total Power', /*138+ */ 'Leap Back', 'Abandon Quest', 'Force Cast', 'Force Spell Cast with Value','Trigger Spell with Value','Apply Area Aura - Pet Owner', -/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', null, 'Charge to Dest', +/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', 'Trigger Missile with Value','Charge to Dest', /*150+ */ 'Start Quest', 'Trigger Spell 2', 'Summon - Refer-A-Friend', 'Create Tamed Pet', 'Discover Flight Path', 'Dual Wield 2H Weapons', /*156+ */ 'Add Socket to Item', 'Create Tradeskill Item', 'Milling', 'Rename Pet', 'Force Cast 2', 'Change Talent Spec. Count', -/*162-167*/ 'Activate Talent Spec.', null, 'Remove Aura', null, null, 'Update Player Phase' +/*162-167*/ 'Activate Talent Spec.', '', 'Remove Aura' ), 'unkAura' => 'Unknown Aura (%1$d)', 'auras' => array( @@ -1838,7 +1879,7 @@ $lang = array( 'Mod Skill - Temporary', 'Increase Run Speed %', 'Mod Mounted Speed %', 'Decrease Run Speed %', 'Mod Maximum Health - Flat', 'Mod Maximum Power - Flat', 'Shapeshift', 'Spell Effect Immunity', 'Spell Aura Immunity', 'Spell School Immunity', 'Damage Immunity', 'Dispel Type Immunity', 'Proc Trigger Spell', 'Proc Trigger Damage', 'Track Creatures', - 'Track Resources', 'Ignore All Gear', 'Mod Parry %', null, 'Mod Dodge %', + 'Track Resources', 'Ignore All Gear', 'Mod Parry %', 'Periodic Trigger Spell from Client', 'Mod Dodge %', /*50+ */ 'Mod Critical Healing Amount %', 'Mod Block %', 'Mod Physical Crit Chance', 'Periodically Drain Health', 'Mod Physical Hit Chance', 'Mod Spell Hit Chance', 'Transform', 'Mod Spell Crit Chance', 'Increase Swim Speed %', 'Mod Damage Done Versus Creature', 'Pacify & Silence', 'Mod Size %', 'Periodically Transfer Health', 'Periodic Transfer Power', 'Periodic Drain Power', @@ -1861,19 +1902,19 @@ $lang = array( 'Increase Pet Talent Points', 'Allow Exotic Pets Taming', 'Mechanic Immunity Mask', 'Retain Combo Points', 'Reduce Pushback Time %', /*150+ */ 'Mod Shield Block Value - %', 'Track Stealthed', 'Mod Player Aggro Range', 'Split Damage - Flat', 'Mod Stealth Level', 'Mod Underwater Breathing %', 'Mod All Reputation Gained by %', 'Done Pet Damage Multiplier', 'Mod Shield Block Value - Flat', 'No PvP Credit', - 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', null, + 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', '', 'Mod Attacker Melee Attack Power', 'Mod Melee Attack Power - %', 'Mod Ranged Attack Power - %', 'Mod Damage Done vs Creature', 'Mod Crit Chance vs Creature', - 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', null, 'Mod Spell Power by % of Stat', + 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', '', 'Mod Spell Power by % of Stat', /*175+ */ 'Mod Healing Power by % of Stat', 'Spirit of Redemption', 'AoE Charm', 'Mod Debuff Resistance - %', 'Mod Attacker Spell Crit Chance', - 'Mod Spell Power vs Creature', null, 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', + 'Mod Spell Power vs Creature', '', 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', 'Mod Attacker Ranged Hit Chance', 'Mod Attacker Spell Hit Chance', 'Mod Attacker Melee Crit Chance', 'Mod Attacker Ranged Crit Chance', 'Mod Rating', 'Mod Reputation Gained %', 'Limit Movement Speed', 'Mod Attack Speed %', 'Mod Haste % (gain)', 'Mod Target School Absorb %', - 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', null, 'Mod Spell Hit Chance', + 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', '', 'Mod Spell Hit Chance', /*200+ */ 'Mod Kill Experience Gained %', 'Can Fly', 'Ignore Combat Result', 'Mod Attacker Melee Crit Damage %', 'Mod Attacker Ranged Crit Damage %', 'Mod Attacker Spell Crit Damage %', 'Mod Vehicle Flight Speed %', 'Mod Mounted Flight Speed %', 'Mod Flight Speed %', 'Mod Mounted Flight Speed % (always)', 'Mod Vehicle Speed % (always)', 'Mod Flight Speed % (not stacking)', 'Mod Ranged Attack Power by % of Stat', 'Mod Rage Generated from Damage Dealt', 'Tamed Pet Passive', 'Arena Preparation', 'Mod Spell Haste %', 'Killing Spree', 'Mod Ranged Haste %', 'Mod Mana Regeneration by % of Stat', - 'Mod Combat Rating by % of Stat', 'Ignore Threat', null, 'Raid Proc from Charge', null, + 'Mod Combat Rating by % of Stat', 'Ignore Threat', '', 'Raid Proc from Charge', '', /*225+ */ 'Raid Proc from Charge with Value', 'Periodic Dummy', 'Periodically Trigger Spell with Value','Detect Stealth', 'Mod AoE Damage Taken %', 'Mod Maximum Health - Flat (no stacking)','Proc Trigger Spell with Value', 'Mod Mechanic Duration %', 'Change other Humanoid Display', 'Mod Mechanic Duration % (not stacking)', 'Mod Dispel Resistance %', 'Control Vehicle', 'Mod Spell Power by % of Attack Power', 'Mod Healing Power by % of Attack Power','Mod Size % (not stacking)', @@ -1881,17 +1922,17 @@ $lang = array( 'Mod Aura Duration by Dispel Type', 'Mod Aura Duration by Dispel Type (not stacking)', 'Clone Caster', 'Mod Combat Result Chance', 'Convert Rune', /*250+ */ 'Mod Maximum Health - Flat (stacking)', 'Mod Enemy Dodge Chance', 'Mod Haste % (loss)', 'Mod Critical Block Chance', 'Disarm Offhand', 'Mod Mechanic Damage Taken %', 'No Reagent Cost', 'Mod Target Resistance by Spell Class', 'Mod Spell Visual', 'Mod Periodic Healing Taken %', - 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', null, - null, null, 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', + 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', '', + '', '', 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', 'Ignore Target Resistance for Ability', 'Mod Damage Taken % from Caster', 'Ignore Swing Timer Reset', 'X-Ray', 'Ability Consume No Ammo', /*275+ */ 'Mod Ability Ignore Shapeshift', 'Mod Mechanic Damage Done %', 'Mod Max Affected Targets', 'Disarm Ranged Weapon', 'Spawn Effect', 'Mod Armor Penetration %', 'Mod Honor Gain %', 'Mod Base Health %', 'Mod Healing Taken % from Caster', 'Linked Aura', - 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', null, + 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', '', 'Mod Crit Chance', 'Mod Quest Experience Gained %', 'Open Stable', 'Override Spells', 'Prevent Power Regeneration', - null, 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', null, -/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', null, 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', - 'Mod Minimum Speed %', null, 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster',null, - 'Mod Pet AoE Damage Avoidance', null, null, null, 'Prevent Ressurection', + '', 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', '', +/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', '', 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', + 'Mod Minimum Speed %', '', 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster','', + 'Mod Pet AoE Damage Avoidance', '', '', '', 'Prevent Ressurection', /* -316*/ 'Underwater Walking', 'Periodic Haste' ), 'attributes0' => array( @@ -2168,6 +2209,7 @@ $lang = array( ) ), 'item' => array( + 'id' => "Gegenstands-ID: ", 'notFound' => "Dieser Gegenstand existiert nicht .", 'armor' => "%s Rüstung", 'block' => "%s Blocken", @@ -2226,8 +2268,10 @@ $lang = array( 'uniqueEquipped'=> ["Einzigartig anlegbar", null, "Einzigartig angelegt: %s (%d)"], 'speed' => "Tempo", 'dps' => "(%.1f Schaden pro Sekunde)", - 'vendorIn' => "Händlerstandpunkte", + 'vendorLoc' => "Händlerstandpunkte", 'purchasedIn' => "Dieser Gegenstand kann gekauft werden in", + 'fishingLoc' => "Angelplätze", + 'fishedIn' => "Dieser Gegenstand kann geangelt werden in", 'duration' => array( // ITEM_DURATION_* '', "Dauer: %d Sek.", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 0901c76b..6735239a 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -134,8 +134,25 @@ $lang = array( 'dateFmtShort' => "Y/m/d", 'dateFmtLong' => "Y/m/d \a\\t g:i A", 'dateFmtIntl' => "MMMM d, y", - 'timeAgo' => "%s ago", 'nfSeparators' => [',', '.'], + 'n_a' => "n/a", + + // date time + 'date' => "Date", + 'date_colon' => "Date: ", + 'date_on' => "on ", + 'date_ago' => "%s ago", + 'date_at' => " at ", + 'date_to' => " to ", + 'date_simple' => '%2$d/%1$d/%3$d', + 'unknowndate' => "Unknown date", + 'ddaysago' => "%d days ago", + 'today' => "today", + 'yesterday' => "yesterday", + 'noon' => "noon", + 'midnight' => "midnight", + 'am' => "AM", + 'pm' => "PM", // error 'intError' => "An internal error has occurred.", @@ -236,6 +253,8 @@ $lang = array( 'atCaptain' => "Arena Team Captain", 'atSize' => "Size: ", 'profiler' => "Character Profiler", + 'completion' => "Completion: ", + 'attainedBy' => "Attained by %d%% of profiles", 'notFound' => array( 'guild' => "This Guild doesn't exist or is not yet in the database.", 'arenateam' => "This Arena Team doesn't exist or is not yet in the database.", @@ -364,6 +383,7 @@ $lang = array( 'school' => "School", 'type' => "Type: ", 'valueDelim' => " to ", + 'target' => "<target>", 'pvp' => "PvP", // PVP 'honorPoints' => "Honor Points", // HONOR_POINTS @@ -378,7 +398,11 @@ $lang = array( 'phases' => "Phases", 'mode' => "Mode: ", - 'modes' => [-1 => "Any", "Normal / Normal 10", "Heroic / Normal 25", "Heroic 10", "Heroic 25"], + 'modes' => array( + [-1 => "Any", "Normal / Normal 10", "Heroic / Normal 25", "Heroic 10", "Heroic 25"], + ["Normal", "Heroic"], + ["Normal 10", "Normal 25", "Heroic 10", "Heroic 25"] + ), 'expansions' => ["Classic", "The Burning Crusade", "Wrath of the Lich King"], 'stats' => ["Strength", "Agility", "Stamina", "Intellect", "Spirit"], 'timeAbbrev' => array( // <time>S_ABBR @@ -478,7 +502,7 @@ $lang = array( UNIT_FLAG_IMMUNE_TO_NPC => 'Immune to creatures', UNIT_FLAG_LOOTING => 'Loot animation', UNIT_FLAG_PET_IN_COMBAT => 'Pet in combat', - UNIT_FLAG_PVP => 'PvP', + UNIT_FLAG_PVP_ENABLING => 'PvP', UNIT_FLAG_SILENCED => 'Silenced', UNIT_FLAG_CANNOT_SWIM => 'Cannot swim', UNIT_FLAG_UNK_15 => 'UNK-15 (can only swim)', @@ -551,11 +575,11 @@ $lang = array( UNIT_VIS_FLAGS_UNK5 => 'UNK-5' ), /*idx:3*/ array( - UNIT_BYTE1_ANIM_TIER_GROUND => 'ground animations', - UNIT_BYTE1_ANIM_TIER_SWIM => 'swimming animations', - UNIT_BYTE1_ANIM_TIER_HOVER => 'hovering animations', - UNIT_BYTE1_ANIM_TIER_FLY => 'flying animations', - UNIT_BYTE1_ANIM_TIER_SUMBERGED => 'submerged animations' + UNIT_ANIM_TIER_GROUND => 'ground animations', + UNIT_ANIM_TIER_SWIM => 'swimming animations', + UNIT_ANIM_TIER_HOVER => 'hovering animations', + UNIT_ANIM_TIER_FLY => 'flying animations', + UNIT_ANIM_TIER_SUMBERGED => 'submerged animations' ), 'bytesIdx' => ['StandState', null, 'VisFlags', 'AnimTier'], 'valueUNK' => '[span class=q10]unhandled value [b class=q1]%d[/b] provided for UnitFieldBytes1 on offset [b class=q1]%d[/b][/span]', @@ -641,8 +665,8 @@ $lang = array( SmartEvent::EVENT_ACTION_DONE => ['Action #[b]%1$d[/b] requested by other script', ''], SmartEvent::EVENT_ON_SPELLCLICK => ['SpellClick was triggered', ''], SmartEvent::EVENT_FRIENDLY_HEALTH_PCT => ['Health of #target# is at %11$s%%', 'Repeat every %s'], - SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], - SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], + SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], + SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], SmartEvent::EVENT_COUNTER_SET => ['Counter #[b]%1$d[/b] is equal to [b]%2$d[/b]', 'Cooldown: %s'], SmartEvent::EVENT_SCENE_START => null, SmartEvent::EVENT_SCENE_TRIGGER => null, @@ -653,6 +677,10 @@ $lang = array( SmartEvent::EVENT_ON_SPELL_FAILED => ['On [spell=%1$d] cast failed', 'Cooldown: %s'], SmartEvent::EVENT_ON_SPELL_START => ['On [spell=%1$d] cast start', 'Cooldown: %s'], SmartEvent::EVENT_ON_DESPAWN => ['On despawn', ''], + SmartEvent::EVENT_SEND_EVENT_TRIGGER => null, + SmartEvent::EVENT_AREATRIGGER_EXIT => null, + SmartEvent::EVENT_ON_AURA_APPLIED => ['On aura [spell=%1$d] applied', 'Cooldown: %s'], + SmartEvent::EVENT_ON_AURA_REMOVED => ['On aura [spell=%1$d] removed', 'Cooldown: %s'] ), 'eventFlags' => array( SmartEvent::FLAG_NO_REPEAT => 'No Repeat', @@ -696,7 +724,7 @@ $lang = array( SmartAction::ACTION_CALL_GROUPEVENTHAPPENS => ['Satisfy exploration event of [quest=%1$d] for group of #target#.', ''], SmartAction::ACTION_COMBAT_STOP => ['End current combat.', ''], SmartAction::ACTION_REMOVEAURASFROMSPELL => ['Remove(%2$d)? %2$d charges of:;(%1$d)? all auras: [spell=%1$d]\'s aura; from #target#.', 'Only own auras'], - SmartAction::ACTION_FOLLOW => ['Follow #target#(%1$d)? at %1$dm distance:;(%3$d)? until reaching [npc=%3$d]:;.(%12$d)?Exploration event of [quest=%4$d] will be satisfied.:;(%13$d)? A kill of [npc=%4$d] will be credited.:;', '(%11$d)?Follow angle\u003A %7$.2f°:;'], + SmartAction::ACTION_FOLLOW => ['Follow #target#(%1$d)? at %1$dm distance:;(%3$d)? until reaching [npc=%3$d]:;.(%12$d)?Exploration event of [quest=%4$d] will be satisfied.:;(%13$d)? A kill of [npc=%4$d] will be credited.:;', '(%11$d)?Follow angle %7$.2f°:;'], /* 30*/ SmartAction::ACTION_RANDOM_PHASE => ['Pick random Event Phase from %11$s.', ''], SmartAction::ACTION_RANDOM_PHASE_RANGE => ['Pick random Event Phase between %1$d and %2$d.', ''], SmartAction::ACTION_RESET_GOBJECT => ['Reset #target#.', ''], @@ -798,8 +826,8 @@ $lang = array( SmartAction::ACTION_PLAY_ANIMKIT => null, SmartAction::ACTION_SCENE_PLAY => null, /*130*/ SmartAction::ACTION_SCENE_CANCEL => null, - SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Spawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Cooldown: %s'], - SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Despawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Cooldown: %s'], + SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Spawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Cooldown: %s'], + SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Despawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Cooldown: %s'], SmartAction::ACTION_RESPAWN_BY_SPAWNID => ['Respawn %11$s [small class=q0](GUID: %2$d)[/small]', ''], SmartAction::ACTION_INVOKER_CAST => ['Invoker casts [spell=%1$d] at #target#.(%4$d)? (max. %4$d |4target:targets;):;', '%1$s'], SmartAction::ACTION_PLAY_CINEMATIC => ['Play cinematic #[b]%1$d[/b] for #target#', ''], @@ -1070,6 +1098,7 @@ $lang = array( 'posts' => "Forum posts: " ), 'emote' => array( + 'id' => "Emote ID: ", 'notFound' => "This Emote doesn't exist.", // 'self' => "To Yourself", // 'target' => "To others with a target", @@ -1102,9 +1131,10 @@ $lang = array( 'state' => ['Oneshot', 'Continuous State', 'Continuous Emote'] ), 'enchantment' => array( + 'id' => "Enchantment ID: ", + 'notFound' => "This enchantment doesn't exist.", 'details' => "Details", 'activation' => "Activation", - 'notFound' => "This enchantment doesn't exist.", 'types' => array( 1 => "Proc Spell", 3 => "Equip Spell", 7 => "Use Spell", 8 => "Prismatic Socket", 5 => "Statistics", 2 => "Weapon Damage", 6 => "DPS", 4 => "Defense" @@ -1113,10 +1143,13 @@ $lang = array( 'areatrigger' => array( 'notFound' => "This areatrigger doesn't exist.", 'foundIn' => "This areatrigger can be found in", + 'unnamed' => "Unnamed areatrigger #%d", 'types' => ['Unused', 'Tavern', 'Teleporter', 'Quest Objective', 'Smart Trigger', 'Script'] ), 'gameObject' => array( + 'id' => "Object ID: ", 'notFound' => "This object doesn't exist.", + 'unnamed' => "Unnamed object #%d", 'cat' => [0 => "Other", 3 => "Containers", 6 => "Traps", 9 => "Books", 25 => "Fishing Pools", -5 => "Chests", -3 => "Herbs", -4 => "Mineral Veins", -2 => "Quest", -6 => "Tools"], 'type' => [ 3 => "Container", 6 => "", 9 => "Book", 25 => "", -5 => "Chest", -3 => "Herb", -4 => "Mineral Vein", -2 => "Quest", -6 => ""], // used for tooltip 'unkPosition' => "The location of this object is unknown.", @@ -1130,14 +1163,15 @@ $lang = array( 'foundIn' => "This object can be found in", 'restock' => "Restocks every %s.", 'goFlags' => array( - GO_FLAG_IN_USE => 'In use', - GO_FLAG_LOCKED => 'Locked', - GO_FLAG_INTERACT_COND => 'Cannot interact', - GO_FLAG_TRANSPORT => 'Transport', - GO_FLAG_NOT_SELECTABLE => 'Not selectable', - GO_FLAG_TRIGGERED => 'Triggered', - GO_FLAG_DAMAGED => 'Siege damaged', - GO_FLAG_DESTROYED => 'Siege destroyed' + GO_FLAG_IN_USE => 'In use', + GO_FLAG_LOCKED => 'Locked', + GO_FLAG_INTERACT_COND => 'Cannot interact', + GO_FLAG_TRANSPORT => 'Transport', + GO_FLAG_NOT_SELECTABLE => 'Not selectable', + GO_FLAG_AI_OBSTACLE => 'Triggered', + GO_FLAG_FREEZE_ANIMATION => 'Freeze Animation', + GO_FLAG_DAMAGED => 'Siege damaged', + GO_FLAG_DESTROYED => 'Siege destroyed' ), 'actions' => array( "None", "Animate Custom 0", "Animate Custom 1", "Animate Custom 2", "Animate Custom 3", @@ -1148,6 +1182,7 @@ $lang = array( ) ), 'npc' => array( + 'id' => "NPC ID: ", 'notFound' => "This NPC doesn't exist.", 'classification'=> "Classification: %s", 'petFamily' => "Pet familiy: ", @@ -1180,10 +1215,6 @@ $lang = array( 'mechanicimmune'=> 'Not affected by mechanic: %s', '_extraFlags' => 'Extra Flags: ', 'versions' => 'Difficulty Versions: ', - 'modes' => array( - 1 => ["Normal", "Heroic"], - 2 => ["10-player Normal", "25-player Normal", "10-player Heroic", "25-player Heroic"] - ), 'cat' => array( "Uncategorized", "Beasts", "Dragonkins", "Demons", "Elementals", "Giants", "Undead", "Humanoids", "Critters", "Mechanicals", "Not specified", "Totems", "Non-combat Pets", "Gas Clouds" @@ -1243,6 +1274,7 @@ $lang = array( ) ), 'event' => array( + 'id' => "World Event ID: ", 'notFound' => "This world event doesn't exist.", 'start' => "Start: ", 'end' => "End: ", @@ -1251,6 +1283,7 @@ $lang = array( 'category' => ["Uncategorized", "Holidays", "Recurring", "Player vs. Player"] ), 'achievement' => array( + 'id' => "Achievement ID: ", 'notFound' => "This achievement doesn't exist.", 'criteria' => "Criteria", 'points' => "Points", @@ -1309,9 +1342,11 @@ $lang = array( ) ), 'chrClass' => array( + 'id' => "Class ID: ", 'notFound' => "This class doesn't exist." ), 'race' => array( + 'id' => "Race ID: ", 'notFound' => "This race doesn't exist.", 'racialLeader' => "Racial leader: ", 'startZone' => "Starting zone", @@ -1349,6 +1384,7 @@ $lang = array( ) ), 'zone' => array( + 'id' => "Zone ID: ", 'notFound' => "This zone doesn't exist.", 'attunement' => ["Attunement: ", "Heroic attunement: "], 'key' => ["Key: ", "Heroic key: "], @@ -1377,6 +1413,7 @@ $lang = array( ) ), 'quest' => array( + 'id' => "Quest ID: ", 'notFound' => "This quest doesn't exist.", '_transfer' => 'This quest will be converted to <a href="?quest=%d" class="q1">%s</a> if you transfer to <span class="icon-%s">%s</span>.', 'questLevel' => "Level %s", @@ -1514,6 +1551,7 @@ $lang = array( 'notFound' => "This icon doesn't exist." ), 'title' => array( + 'id' => "Title ID: ", 'notFound' => "This title doesn't exist.", '_transfer' => 'This title will be converted to <a href="?title=%d" class="q1">%s</a> if you transfer to <span class="icon-%s">%s</span>.', 'cat' => array( @@ -1521,6 +1559,7 @@ $lang = array( ) ), 'skill' => array( + 'id' => "Skill ID: ", 'notFound' => "This skill doesn't exist.", 'cat' => array( -6 => "Companions", -5 => "Mounts", -4 => "Racial Traits", 5 => "Attributes", 6 => "Weapon Skills", 7 => "Class Skills", 8 => "Armor Proficiencies", @@ -1528,6 +1567,7 @@ $lang = array( ) ), 'currency' => array( + 'id' => "Currency ID: ", 'notFound' => "This currency doesn't exist.", 'cap' => "Total cap: ", 'cat' => array( @@ -1549,6 +1589,7 @@ $lang = array( ) ), 'mail' => array( + 'id' => "Mail ID: ", 'notFound' => "This mail doesn't exist.", 'attachment' => "Attachment", 'mailDelivery' => 'You will receive <a href="?mail=%d">this letter</a>%s%s', @@ -1559,12 +1600,14 @@ $lang = array( 'untitled' => "Untitled Mail #%d" ), 'pet' => array( + 'id' => "Pet family ID: ", 'notFound' => "This pet family doesn't exist.", 'exotic' => "Exotic", 'cat' => ["Ferocity", "Tenacity", "Cunning"], 'food' => ["Meat", "Fish", "Cheese", "Bread", "Fungus", "Fruit", "Raw Meat", "Raw Fish"] // ItemPetFood.dbc ), 'faction' => array( + 'id' => "Faction ID: ", 'notFound' => "This faction doesn't exist.", 'spillover' => "Reputation Spillover", 'spilloverDesc' => "Gaining reputation with this faction also yields a proportional gain with the factions listed below.", @@ -1580,12 +1623,13 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "Item Set ID: ", 'notFound' => "This item set doesn't exist.", '_desc' => "<b>%s</b> is the <b>%s</b>. It contains %s pieces.", '_descTagless' => "<b>%s</b> is an item set that contains %s pieces.", '_setBonuses' => "Set Bonuses", '_conveyBonus' => "Wearing more pieces of this set will convey bonuses to your character.", - '_pieces' => "pieces", + '_pieces' => "%d pieces: ", '_unavailable' => "This item set is not available to players.", '_tag' => "Tag: ", 'summary' => "Summary", @@ -1605,13 +1649,14 @@ $lang = array( ) ), 'spell' => array( + 'id' => "Spell ID: ", 'notFound' => "This spell doesn't exist.", '_spellDetails' => "Spell Details", '_cost' => "Cost", '_range' => "Range", '_castTime' => "Cast time", '_cooldown' => "Cooldown", - '_distUnit' => "yards", + '_distUnit' => " yards", '_forms' => "Forms", '_aura' => "Aura", '_effect' => "Effect", @@ -1631,14 +1676,13 @@ $lang = array( '_rankRange' => "Rank: %d - %d", '_showXmore' => "Show %d More", - 'n_a' => "n/a", 'normal' => "Normal", 'special' => "Special", 'currentArea' => '<current area>', 'discovered' => "Learned via discovery", - 'ppm' => "(%s procs per minute)", - 'procChance' => "Proc chance: ", + 'ppm' => "(%.1f procs per minute)", + 'procChance' => "Proc chance: %.4g%%", 'starter' => "Starter spell", 'trainingCost' => "Training cost: ", 'channeled' => "Channeled", // SPELL_CAST_CHANNELED @@ -1654,7 +1698,8 @@ $lang = array( 'pointsPerCP' => ", plus %s per combo point", 'stackGroup' => "Stack Group", 'linkedWith' => "Linked with", - '_scaling' => "Scaling", + 'apMod' => " (AP mod: %.3g)", + 'spMod' => " (SP mod: %.3g)", 'instantPhys' => "Instant", // SPELL_CAST_TIME_INSTANT_NO_MANA 'castTime' => array( "Instant cast", // SPELL_CAST_TIME_INSTANT @@ -1701,10 +1746,6 @@ $lang = array( -1 => "Ammo", -41 => "Pyrite", -61 => "Steam Pressure", -101 => "Heat", -121 => "Ooze", -141 => "Blood Power", -142 => "Wrath" ), - 'scaling' => array( - 'directSP' => "+%.2f%% of spell power to direct component", 'directAP' => "+%.2f%% of attack power to direct component", - 'dotSP' => "+%.2f%% of spell power per tick", 'dotAP' => "+%.2f%% of attack power per tick" - ), 'relItems' => array( 'base' => "<small>Show %s related to <b>%s</b></small>", 'link' => " or ", @@ -1818,14 +1859,14 @@ $lang = array( /*102+ */ "Dismiss Pet", "Give Reputation", "Summon Object (Trap)", "Summon Object (Battle S.)","Summon Object (#3)", "Summon Object (#4)", /*108+ */ "Dispel Mechanic", "Summon Dead Pet", "Destroy All Totems", "Durability Damage - Flat", "Summon Demon", "Resurrect with Flat Health", /*114+ */ "Taunt", "Durability Damage - %", "Skin Player Corpse (PvP)", "AoE Resurrect with % Health","Learn Skill", "Apply Area Aura - Pet", -/*120+ */ "Teleport to Graveyard", "Normalized Weapon Damage", null, "Take Flight Path", "Pull Towards", "Modify Threat - %", +/*120+ */ "Teleport to Graveyard", "Normalized Weapon Damage", "", "Take Flight Path", "Pull Towards", "Modify Threat - %", /*126+ */ "Spell Steal ", "Prospect", "Apply Area Aura - Friend", "Apply Area Aura - Enemy", "Redirect Done Threat %", "Play Sound", /*132+ */ "Play Music", "Unlearn Specialization", "Kill Credit 2", "Call Pet", "Heal for % of Total Health","Give % of Total Power", /*138+ */ "Leap Back", "Abandon Quest", "Force Cast", "Force Spell Cast with Value","Trigger Spell with Value","Apply Area Aura - Pet Owner", -/*144+ */ "Knockback to Dest.", "Pull Towards Dest.", "Activate Rune", "Fail Quest", null, "Charge to Dest", +/*144+ */ "Knockback to Dest.", "Pull Towards Dest.", "Activate Rune", "Fail Quest", "Trigger Missile with Value","Charge to Dest", /*150+ */ "Start Quest", "Trigger Spell 2", "Summon - Refer-A-Friend", "Create Tamed Pet", "Discover Flight Path", "Dual Wield 2H Weapons", /*156+ */ "Add Socket to Item", "Create Tradeskill Item", "Milling", "Rename Pet", "Force Cast 2", "Change Talent Spec. Count", -/*162-167*/ "Activate Talent Spec.", null, "Remove Aura", null, null, "Update Player Phase" +/*162-167*/ "Activate Talent Spec.", "", "Remove Aura" ), 'unkAura' => 'Unknown Aura (%1$d)', 'auras' => array( @@ -1838,7 +1879,7 @@ $lang = array( "Mod Skill - Temporary", "Increase Run Speed %", "Mod Mounted Speed %", "Decrease Run Speed %", "Mod Maximum Health - Flat", "Mod Maximum Power - Flat", "Shapeshift", "Spell Effect Immunity", "Spell Aura Immunity", "Spell School Immunity", "Damage Immunity", "Dispel Type Immunity", "Proc Trigger Spell", "Proc Trigger Damage", "Track Creatures", - "Track Resources", "Ignore All Gear", "Mod Parry %", null, "Mod Dodge %", + "Track Resources", "Ignore All Gear", "Mod Parry %", "Periodic Trigger Spell from Client", "Mod Dodge %", /*50+ */ "Mod Critical Healing Amount %", "Mod Block %", "Mod Physical Crit Chance", "Periodically Drain Health", "Mod Physical Hit Chance", "Mod Spell Hit Chance", "Transform", "Mod Spell Crit Chance", "Increase Swim Speed %", "Mod Damage Done Versus Creature", "Pacify & Silence", "Mod Size %", "Periodically Transfer Health", "Periodic Transfer Power", "Periodic Drain Power", @@ -1861,19 +1902,19 @@ $lang = array( "Increase Pet Talent Points", "Allow Exotic Pets Taming", "Mechanic Immunity Mask", "Retain Combo Points", "Reduce Pushback Time %", /*150+ */ "Mod Shield Block Value - %", "Track Stealthed", "Mod Player Aggro Range", "Split Damage - Flat", "Mod Stealth Level", "Mod Underwater Breathing %", "Mod All Reputation Gained by %", "Done Pet Damage Multiplier", "Mod Shield Block Value - Flat", "No PvP Credit", - "Mod AoE Avoidance", "Mod Health Regen During Combat", "Mana Burn", "Mod Melee Critical Damage %", null, + "Mod AoE Avoidance", "Mod Health Regen During Combat", "Mana Burn", "Mod Melee Critical Damage %", "", "Mod Attacker Melee Attack Power", "Mod Melee Attack Power - %", "Mod Ranged Attack Power - %", "Mod Damage Done vs Creature", "Mod Crit Chance vs Creature", - "Change Object Visibility for Player", "Mod Run Speed (not stacking)", "Mod Mounted Speed (not stacking)", null, "Mod Spell Power by % of Stat", + "Change Object Visibility for Player", "Mod Run Speed (not stacking)", "Mod Mounted Speed (not stacking)", "", "Mod Spell Power by % of Stat", /*175+ */ "Mod Healing Power by % of Stat", "Spirit of Redemption", "AoE Charm", "Mod Debuff Resistance - %", "Mod Attacker Spell Crit Chance", - "Mod Spell Power vs Creature", null, "Mod Resistance by % of Stat", "Mod Threat % of Critical Hits", "Mod Attacker Melee Hit Chance", + "Mod Spell Power vs Creature", "", "Mod Resistance by % of Stat", "Mod Threat % of Critical Hits", "Mod Attacker Melee Hit Chance", "Mod Attacker Ranged Hit Chance", "Mod Attacker Spell Hit Chance", "Mod Attacker Melee Crit Chance", "Mod Attacker Ranged Crit Chance", "Mod Rating", "Mod Reputation Gained %", "Limit Movement Speed", "Mod Attack Speed %", "Mod Haste % (gain)", "Mod Target School Absorb %", - "Mod Target School Absorb for Ability", "Mod Cooldowns", "Mod Attacker Crit Chance", null, "Mod Spell Hit Chance", + "Mod Target School Absorb for Ability", "Mod Cooldowns", "Mod Attacker Crit Chance", "", "Mod Spell Hit Chance", /*200+ */ "Mod Kill Experience Gained %", "Can Fly", "Ignore Combat Result", "Mod Attacker Melee Crit Damage %", "Mod Attacker Ranged Crit Damage %", "Mod Attacker Spell Crit Damage %", "Mod Vehicle Flight Speed %", "Mod Mounted Flight Speed %", "Mod Flight Speed %", "Mod Mounted Flight Speed % (always)", "Mod Vehicle Speed % (always)", "Mod Flight Speed % (not stacking)", "Mod Ranged Attack Power by % of Stat", "Mod Rage Generated from Damage Dealt", "Tamed Pet Passive", "Arena Preparation", "Mod Spell Haste %", "Killing Spree", "Mod Ranged Haste %", "Mod Mana Regeneration by % of Stat", - "Mod Combat Rating by % of Stat", "Ignore Threat", null, "Raid Proc from Charge", null, + "Mod Combat Rating by % of Stat", "Ignore Threat", "", "Raid Proc from Charge", "", /*225+ */ "Raid Proc from Charge with Value", "Periodic Dummy", "Periodically Trigger Spell with Value","Detect Stealth", "Mod AoE Damage Taken %", "Mod Maximum Health - Flat (no stacking)","Proc Trigger Spell with Value", "Mod Mechanic Duration %", "Change other Humanoid Display", "Mod Mechanic Duration % (not stacking)", "Mod Dispel Resistance %", "Control Vehicle", "Mod Spell Power by % of Attack Power", "Mod Healing Power by % of Attack Power","Mod Size % (not stacking)", @@ -1881,17 +1922,17 @@ $lang = array( "Mod Aura Duration by Dispel Type", "Mod Aura Duration by Dispel Type (not stacking)", "Clone Caster", "Mod Combat Result Chance", "Convert Rune", /*250+ */ "Mod Maximum Health - Flat (stacking)", "Mod Enemy Dodge Chance", "Mod Haste % (loss)", "Mod Critical Block Chance", "Disarm Offhand", "Mod Mechanic Damage Taken %", "No Reagent Cost", "Mod Target Resistance by Spell Class", "Mod Spell Visual", "Mod Periodic Healing Taken %", - "Screen Effect", "Phase", "Ability Ignore Aurastate", "Allow Only Ability", null, - null, null, "Cancel Aura Buffer at % of Caster Health","Mod Attack Power by % of Stat", "Ignore Target Resistance", + "Screen Effect", "Phase", "Ability Ignore Aurastate", "Allow Only Ability", "", + "", "", "Cancel Aura Buffer at % of Caster Health","Mod Attack Power by % of Stat", "Ignore Target Resistance", "Ignore Target Resistance for Ability", "Mod Damage Taken % from Caster", "Ignore Swing Timer Reset", "X-Ray", "Ability Consume No Ammo", /*275+ */ "Mod Ability Ignore Shapeshift", "Mod Mechanic Damage Done %", "Mod Max Affected Targets", "Disarm Ranged Weapon", "Spawn Effect", "Mod Armor Penetration %", "Mod Honor Gain %", "Mod Base Health %", "Mod Healing Taken % from Caster", "Linked Aura", - "Mod Attack Power by School Resistance","Allow Periodic Ability to Crit", "Mod Spell Deflect Chance", "Ignore Hit Direction", null, + "Mod Attack Power by School Resistance","Allow Periodic Ability to Crit", "Mod Spell Deflect Chance", "Ignore Hit Direction", "", "Mod Crit Chance", "Mod Quest Experience Gained %", "Open Stable", "Override Spells", "Prevent Power Regeneration", - null, "Set Vehicle Id", "Spirit Burst", "Strangulate", null, -/*300+ */ "Share Damage %", "Mod Absorb School Healing", null, "Mod Damage Done vs Aurastate - %", "Fake Inebriate", - "Mod Minimum Speed %", null, "Heal Absorb Test", "Mod Critical Strike Chance for Caster",null, - "Mod Pet AoE Damage Avoidance", null, null, null, "Prevent Ressurection", + "", "Set Vehicle Id", "Spirit Burst", "Strangulate", "", +/*300+ */ "Share Damage %", "Mod Absorb School Healing", "", "Mod Damage Done vs Aurastate - %", "Fake Inebriate", + "Mod Minimum Speed %", "", "Heal Absorb Test", "Mod Critical Strike Chance for Caster","", + "Mod Pet AoE Damage Avoidance", "", "", "", "Prevent Ressurection", /* -316*/ "Underwater Walking", "Periodic Haste" ), 'attributes0' => array( @@ -2168,6 +2209,7 @@ $lang = array( ) ), 'item' => array( + 'id' => "Item ID: ", 'notFound' => "This item doesn't exist.", 'armor' => "%s Armor", // ARMOR_TEMPLATE 'block' => "%s Block", // SHIELD_BLOCK_TEMPLATE @@ -2226,8 +2268,10 @@ $lang = array( 'uniqueEquipped'=> ["Unique-Equipped", null, "Unique-Equipped: %s (%d)"], // ITEM_UNIQUE_EQUIPPABLE, null, ITEM_LIMIT_CATEGORY_MULTIPLE 'speed' => "Speed", // SPEED 'dps' => "(%.1f damage per second)", // DPS_TEMPLATE - 'vendorIn' => "Vendor Locations", + 'vendorLoc' => "Vendor Locations", 'purchasedIn' => "This item can be purchased in", + 'fishingLoc' => "Fishing Locations", + 'fishedIn' => "This item can be fished in", 'duration' => array( // ITEM_DURATION_* '', "Duration: %d sec", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 0f75fb34..bae6c931 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -134,8 +134,25 @@ $lang = array( 'dateFmtShort' => "d/m/Y", 'dateFmtLong' => "d/m/Y \a \l\a\s g:i A", 'dateFmtIntl' => "d 'de' MMMM 'de' y", - 'timeAgo' => 'hace %s', 'nfSeparators' => ['.', ','], + 'n_a' => "n/d", + + // date time + 'date' => "Fecha", + 'date_colon' => "Fecha: ", + 'date_on' => "el ", + 'date_ago' => "hace %s", + 'date_at' => " a las ", + 'date_to' => " al ", + 'date_simple' => '%1$d/%2$d/%3$d', + 'unknowndate' => "Fecha desconocida", + 'ddaysago' => "Hace %d días", + 'today' => "hoy", + 'yesterday' => "ayer", + 'noon' => "medio día", + 'midnight' => "medianoche", + 'am' => "a.m.", + 'pm' => "p.m.", // error 'intError' => "Un error interno ha ocurrido.", @@ -236,6 +253,8 @@ $lang = array( 'atCaptain' => "Capitán de equipo de arena", 'atSize' => "Tamaño: ", 'profiler' => "Gestor de Perfiles", // Perfiles de Personaje? (character profiler) + 'completion' => "Terminación: ", + 'attainedBy' => "Obtenido por %d%% de perfiles", 'notFound' => array( 'guild' => "Esta hermandad no existe o aún no está en la base de datos.", 'arenateam' => "Este equipo de arena no existe o aún no está en la base de datos.", @@ -315,7 +334,7 @@ $lang = array( 'achievements' => "Logros", 'title' => "título", 'titles' => "Títulos", - 'event' => "Suceso mundial ", + 'event' => "Suceso mundial", 'events' => "Eventos del mundo", 'class' => "clase", 'classes' => "Clases", @@ -323,7 +342,7 @@ $lang = array( 'races' => "Razas", 'skill' => "habilidad", 'skills' => "Habilidades", - 'currency' => "monedas", + 'currency' => "moneda", 'currencies' => "Monedas", 'sound' => "sonido", 'sounds' => "Sonidos", @@ -364,6 +383,7 @@ $lang = array( 'school' => "Escuela", 'type' => "Tipo: ", 'valueDelim' => " - ", + 'target' => "<objetivo>", 'pvp' => "JcJ", 'honorPoints' => "Puntos de Honor", @@ -378,7 +398,11 @@ $lang = array( 'phases' => "Fases", 'mode' => "Modo: ", - 'modes' => [-1 => "Cualquiera", "Normal / Normal 10", "Heroico / Normal 25", "Heróico 10", "Heróico 25"], + 'modes' => array( + [-1 => "Cualquiera", "Normal / Normal 10", "Heroico / Normal 25", "Heróico 10", "Heróico 25"], + ["Normal", "Heroico"], + ["Normal 10", "Normal 25", "Heróico 10", "Heróico 25"], + ), 'expansions' => ["World of Warcraft", "The Burning Crusade", "Wrath of the Lich King"], 'stats' => ["Fuerza", "Agilidad", "Aguante", "Intelecto", "Espíritu"], 'timeAbbrev' => array( @@ -478,7 +502,7 @@ $lang = array( UNIT_FLAG_IMMUNE_TO_NPC => 'Inmune a criaturas', UNIT_FLAG_LOOTING => 'Animación de botín', UNIT_FLAG_PET_IN_COMBAT => 'Mascota en combate', - UNIT_FLAG_PVP => 'JcJ', + UNIT_FLAG_PVP_ENABLING => 'JcJ', UNIT_FLAG_SILENCED => 'Silenciado', UNIT_FLAG_CANNOT_SWIM => 'No puede nadar', UNIT_FLAG_UNK_15 => 'UNK-15 (solo puede nadar)', @@ -551,11 +575,11 @@ $lang = array( UNIT_VIS_FLAGS_UNK5 => 'UNK-5' ), /*idx:3*/ array( - UNIT_BYTE1_ANIM_TIER_GROUND => 'animaciones en tierra', - UNIT_BYTE1_ANIM_TIER_SWIM => 'animaciones de nado', - UNIT_BYTE1_ANIM_TIER_HOVER => 'animaciones de flotar', - UNIT_BYTE1_ANIM_TIER_FLY => 'animaciones de vuelo', - UNIT_BYTE1_ANIM_TIER_SUMBERGED => 'animaciones sumergidas' + UNIT_ANIM_TIER_GROUND => 'animaciones en tierra', + UNIT_ANIM_TIER_SWIM => 'animaciones de nado', + UNIT_ANIM_TIER_HOVER => 'animaciones de flotar', + UNIT_ANIM_TIER_FLY => 'animaciones de vuelo', + UNIT_ANIM_TIER_SUMBERGED => 'animaciones sumergidas' ), 'bytesIdx' => ['StandState', null, 'VisFlags', 'AnimTier'], 'valueUNK' => '[span class=q10]valor no manejado [b class=q1]%d[/b] proporcionado para UnitFieldBytes1 en el desplazamiento [b class=q1]%d[/b][/span]', @@ -641,8 +665,8 @@ $lang = array( SmartEvent::EVENT_ACTION_DONE => ['Acción #[b]%1$d[/b] solicitada por otro script', ''], SmartEvent::EVENT_ON_SPELLCLICK => ['SpellClick fue activado', ''], SmartEvent::EVENT_FRIENDLY_HEALTH_PCT => ['La salud de #target# está al %11$s%%', 'Repetir cada %s'], - SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; está a %3$dm', 'Repetir cada %s'], - SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; está a %3$dm', 'Repetir cada %s'], + SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; está a %3$dm', 'Repetir cada %s'], + SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; está a %3$dm', 'Repetir cada %s'], SmartEvent::EVENT_COUNTER_SET => ['Contador #[b]%1$d[/b] es igual a [b]%2$d[/b]', 'Enfriamiento: %s'], SmartEvent::EVENT_SCENE_START => null, SmartEvent::EVENT_SCENE_TRIGGER => null, @@ -653,6 +677,10 @@ $lang = array( SmartEvent::EVENT_ON_SPELL_FAILED => ['Al fallar [spell=%1$d]', 'Enfriamiento: %s'], SmartEvent::EVENT_ON_SPELL_START => ['Al comenzar a lanzar [spell=%1$d]', 'Enfriamiento: %s'], SmartEvent::EVENT_ON_DESPAWN => ['Al desaparecer', ''], + SmartEvent::EVENT_SEND_EVENT_TRIGGER => null, + SmartEvent::EVENT_AREATRIGGER_EXIT => null, + SmartEvent::EVENT_ON_AURA_APPLIED => ['Al aplicar el aura [spell=%1$d]', 'Enfriamiento: %s'], + SmartEvent::EVENT_ON_AURA_REMOVED => ['Al eliminar el aura [spell=%1$d]', 'Enfriamiento: %s'] ), 'eventFlags' => array( SmartEvent::FLAG_NO_REPEAT => 'Sin repetir', @@ -696,7 +724,7 @@ $lang = array( SmartAction::ACTION_CALL_GROUPEVENTHAPPENS => ['Satisfacer evento de exploración de [quest=%1$d] para el grupo de #target#.', ''], SmartAction::ACTION_COMBAT_STOP => ['Terminar combate actual.', ''], SmartAction::ACTION_REMOVEAURASFROMSPELL => ['Quitar(%2$d)? %2$d cargas de:;(%1$d)? todas las auras: el aura de [spell=%1$d]; de #target#.', 'Solo auras propias'], - SmartAction::ACTION_FOLLOW => ['Seguir a #target#(%1$d)? a %1$dm de distancia:;(%3$d)? hasta alcanzar a [npc=%3$d]:;.(%12$d)?El evento de exploración de [quest=%4$d] será satisfecho.:;(%13$d)? Se acreditará una muerte de [npc=%4$d].:;', '(%11$d)?Ángulo de seguimiento\u003A %7$.2f°:;'], + SmartAction::ACTION_FOLLOW => ['Seguir a #target#(%1$d)? a %1$dm de distancia:;(%3$d)? hasta alcanzar a [npc=%3$d]:;.(%12$d)?El evento de exploración de [quest=%4$d] será satisfecho.:;(%13$d)? Se acreditará una muerte de [npc=%4$d].:;', '(%11$d)?Ángulo de seguimiento %7$.2f°:;'], /* 30*/ SmartAction::ACTION_RANDOM_PHASE => ['Elegir fase de evento aleatoria de %11$s.', ''], SmartAction::ACTION_RANDOM_PHASE_RANGE => ['Elegir fase de evento aleatoria entre %1$d y %2$d.', ''], SmartAction::ACTION_RESET_GOBJECT => ['Restablecer #target#.', ''], @@ -798,8 +826,8 @@ $lang = array( SmartAction::ACTION_PLAY_ANIMKIT => null, SmartAction::ACTION_SCENE_PLAY => null, /*130*/ SmartAction::ACTION_SCENE_CANCEL => null, - SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Generar GrupoDeAparición [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Enfriamiento: %s'], - SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Desaparecer GrupoDeAparición [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Enfriamiento: %s'], + SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Generar GrupoDeAparición [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Enfriamiento: %s'], + SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Desaparecer GrupoDeAparición [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Enfriamiento: %s'], SmartAction::ACTION_RESPAWN_BY_SPAWNID => ['Reaparecer %11$s [small class=q0](GUID: %2$d)[/small]', ''], SmartAction::ACTION_INVOKER_CAST => ['El invocador lanza [spell=%1$d] a #target#.(%4$d)? (máx. %4$d |4objetivo:objetivos;):;', '%1$s'], SmartAction::ACTION_PLAY_CINEMATIC => ['Reproducir cinemática #[b]%1$d[/b] para #target#', ''], @@ -909,7 +937,7 @@ $lang = array( 'forgot' => "Se me olvidó mi", 'forgotUser' => "Nombre de usuario", 'forgotPass' => "Contraseña", - 'accCreate ' => '¿No tienes una cuenta? <a href="?account=signup">¡Crea una ahora!</a>', + 'accCreate' => '¿No tienes una cuenta? <a href="?account=signup">¡Crea una ahora!</a>', // recovery 'newPass' => "Nueva Contraseña:", @@ -1070,6 +1098,7 @@ $lang = array( 'posts' => "Mensajes en los foros: " ), 'emote' => array( + 'id' => "ID de Emoticón: ", 'notFound' => "Este emoticón no existe.", // 'self' => "Para Usted", // 'target' => "Para otros con un objetivo", @@ -1102,9 +1131,10 @@ $lang = array( 'state' => ['Oneshot', 'Estado continuo', 'Emoticono continuo'] ), 'enchantment' => array( + 'id' => "ID de Encantamiento: ", + 'notFound' => "Este encantamiento no existe.", 'details' => "Detalles", 'activation' => "Activación", - 'notFound' => "Este encantamiento no existe.", 'types' => array( 1 => "Prob. Hechizo", 3 => "Equipar Hechizo", 7 => "Usar Hechizo", 8 => "Ranura prismática", 5 => "Atributos", 2 => "Daño de arma", 6 => "DPS", 4 => "Defensa" @@ -1113,10 +1143,13 @@ $lang = array( 'areatrigger' => array( 'notFound' => "Este activador de área no existe.", 'foundIn' => "Este activador de área se puede encontrar en", + 'unnamed' => "[Unnamed areatrigger] #%d", 'types' => ['Sin usar', 'Taberna', 'Teletransportador', 'Objetivo de misión', 'Activador inteligente', 'Script'] ), 'gameObject' => array( + 'id' => "ID de Entidad: ", 'notFound' => "Esta entidad no existe.", + 'unnamed' => "[Unnamed object] #%d", 'cat' => [0 => "Otros", 3 => "Contenedores", 6 => "Trampas", 9 => "Libros", 25 => "Bancos de peces", -5 => "Cofres", -3 => "Hierbas", -4 => "Venas de minerales", -2 => "Misiones", -6 => "Herramientas"], 'type' => [ 3 => "Contenedore", 6 => "", 9 => "Libro", 25 => "", -5 => "Cofre", -3 => "Hierba", -4 => "Filóne de mineral", -2 => "Misión", -6 => ""], 'unkPosition' => "No se conoce la ubicación de esta entidad.", @@ -1130,14 +1163,15 @@ $lang = array( 'foundIn' => "Esta entidad se puede encontrar en", 'restock' => "Se renueva cada %s.", 'goFlags' => array( - GO_FLAG_IN_USE => 'En uso', - GO_FLAG_LOCKED => 'Bloqueado', - GO_FLAG_INTERACT_COND => 'No se puede interactuar', - GO_FLAG_TRANSPORT => 'Transporte', - GO_FLAG_NOT_SELECTABLE => 'No seleccionable', - GO_FLAG_TRIGGERED => 'Activado', - GO_FLAG_DAMAGED => 'Dañado por asedio', - GO_FLAG_DESTROYED => 'Destruido por asedio' + GO_FLAG_IN_USE => 'En uso', + GO_FLAG_LOCKED => 'Bloqueado', + GO_FLAG_INTERACT_COND => 'No se puede interactuar', + GO_FLAG_TRANSPORT => 'Transporte', + GO_FLAG_NOT_SELECTABLE => 'No seleccionable', + GO_FLAG_AI_OBSTACLE => 'Activado', + GO_FLAG_FREEZE_ANIMATION => 'Congelar animación', + GO_FLAG_DAMAGED => 'Dañado por asedio', + GO_FLAG_DESTROYED => 'Destruido por asedio' ), 'actions' => array( "Ninguno", "Animar personalizado 0", "Animar personalizado 1", "Animar personalizado 2", "Animar personalizado 3", @@ -1148,6 +1182,7 @@ $lang = array( ) ), 'npc' => array( + 'id' => "ID de PNJ: ", 'notFound' => "Este PNJ no existe.", 'classification'=> "Clasificación: %s", 'petFamily' => "Familia de mascota: ", @@ -1180,10 +1215,6 @@ $lang = array( 'mechanicimmune'=> 'No afectado por la mecánica: %s', '_extraFlags' => 'Banderas extra: ', 'versions' => 'Versiones de dificultad: ', - 'modes' => array( - 1 => ["Normal", "Heroico"], - 2 => ["10 jugadores Normal", "25 jugadores Normal", "10 jugadores Heroico", "25 jugadores Heroico"] - ), 'cat' => array( "Sin categoría", "Bestia", "Dragonante", "Demonio", "Elemental", "Gigante", "No-muerto", "Humanoide", "Alimaña", "Mecánico", "Sin especificar", "Tótem", "Mascota mansa", "Nube de gas" @@ -1243,6 +1274,7 @@ $lang = array( ) ), 'event' => array( + 'id' => "ID de Suceso mundial: ", 'notFound' => "Este evento del mundo no existe.", 'start' => "Empieza: ", 'end' => "Termina: ", @@ -1251,6 +1283,7 @@ $lang = array( 'category' => ["Sin categoría", "Vacacionales", "Periódicos", "Jugador contra Jugador"] ), 'achievement' => array( + 'id' => "ID de Logro: ", 'notFound' => "Este logro no existe.", 'criteria' => "Requisitos", 'points' => "Puntos", @@ -1309,9 +1342,11 @@ $lang = array( ) ), 'chrClass' => array( + 'id' => "ID de Clase: ", 'notFound' => "Esta clase no existe." ), 'race' => array( + 'id' => "ID de Raza: ", 'notFound' => "Esta raza no existe.", 'racialLeader' => "Lider racial: ", 'startZone' => "Zona de inicio", @@ -1349,6 +1384,7 @@ $lang = array( ) ), 'zone' => array( + 'id' => "ID de Zona: ", 'notFound' => "Esta zona no existe.", 'attunement' => ["Requisito: ", "Requisito heroica: "], 'key' => ["Llave: ", "Llave heroica: "], @@ -1377,6 +1413,7 @@ $lang = array( ) ), 'quest' => array( + 'id' => "ID de Misión: ", 'notFound' => "Esta misión no existe.", '_transfer' => 'Esta misión será convertido a <a href="?quest=%d" class="q1">%s</a> si lo transfieres a la <span class="icon-%s">%s</span>.', 'questLevel' => 'Nivel %s', @@ -1514,6 +1551,7 @@ $lang = array( 'notFound' => "Este icono no existe." ), 'title' => array( + 'id' => "ID de Título: ", 'notFound' => "Este título no existe.", '_transfer' => 'Este título será convertido a <a href="?title=%d" class="q1">%s</a> si lo transfieres a la <span class="icon-%s">%s</span>.', 'cat' => array( @@ -1521,6 +1559,7 @@ $lang = array( ) ), 'skill' => array( + 'id' => "ID de Habilidad: ", 'notFound' => "Esta habilidad no existe.", 'cat' => array( -6 => "Compañeros", -5 => "Monturas", -4 => "Habilidades de raza", 5 => "Atributos", 6 => "Habilidades con armas", 7 => "Habilidades de clase", 8 => "Armaduras disponibles", @@ -1528,6 +1567,7 @@ $lang = array( ) ), 'currency' => array( + 'id' => "ID de Moneda: ", 'notFound' => "Esta moneda no existe.", 'cap' => "Límite total: ", 'cat' => array( @@ -1549,6 +1589,7 @@ $lang = array( ) ), 'mail' => array( + 'id' => "ID de Correo: ", 'notFound' => "Este correo no existe.", 'attachment' => "Adjunto", 'mailDelivery' => "Usted recibirá esta carta%s%s", @@ -1559,12 +1600,14 @@ $lang = array( 'untitled' => "Correo sin título #%d" ), 'pet' => array( + 'id' => "ID de Familia de mascotas: ", 'notFound' => "Esta familia de mascotas no existe.", 'exotic' => "Exótica", 'cat' => ["Ferocidad", "Tenacidad", "Astucia"], 'food' => ["Carne", "Pescado", "Queso", "Pan", "Hongo", "Fruta", "Carne cruda", "Pescado crudo"] ), 'faction' => array( + 'id' => "ID de Facción: ", 'notFound' => "Esta facción no existe.", 'spillover' => "Excedente de reputación", 'spilloverDesc' => "Ganar reputación con esta facción tambien una proporción ganada con las facciones listadas a continuación.", @@ -1580,12 +1623,13 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "ID de Conjunto de objetos: ", 'notFound' => "Este conjunto de objetos no existe.", '_desc' => "<b>%s</b> es el <b>%s</b>. Contiene %s piezas.", '_descTagless' => "<b>%s</b> es un conjunto de objetos que tiene %s piezas.", '_setBonuses' => "Bonificación de conjunto", '_conveyBonus' => "Tener puestos mas objetos de este conjunto le aplicará una bonificación a tu personaje.", - '_pieces' => "piezas", + '_pieces' => "%d piezas: ", '_unavailable' => "Este conjunto de objetos no está disponible para jugadores.", '_tag' => "Etiqueta: ", 'summary' => "Resúmen", @@ -1605,13 +1649,14 @@ $lang = array( ) ), 'spell' => array( + 'id' => "ID de Hechizo: ", 'notFound' => "Este hechizo no existe.", '_spellDetails' => "Detalles de hechizos", '_cost' => "Costo", '_range' => "Rango", '_castTime' => "Tiempo de lanzamiento", '_cooldown' => "Reutilización", - '_distUnit' => "metros", + '_distUnit' => " metros", '_forms' => "Formas", '_aura' => "Aura", '_effect' => "Efecto", @@ -1631,14 +1676,13 @@ $lang = array( '_rankRange' => "Rango: %d - %d", '_showXmore' => "Mostrar %d más", - 'n_a' => "n/d", 'normal' => "Normal", 'special' => "Especial", 'currentArea' => '<current area>', 'discovered' => "Aprendido via descubrimiento", - 'ppm' => "(%s procs por minuto)", - 'procChance' => "Probabilidad de que accione: ", + 'ppm' => "(%.1f procs por minuto)", + 'procChance' => "Probabilidad de que accione: %.4g%%", 'starter' => "Hechizo inicial", 'trainingCost' => "Costo de enseñanza: ", 'channeled' => "Canalizado", @@ -1654,7 +1698,8 @@ $lang = array( 'pointsPerCP' => ", mas %s por punto de combo", 'stackGroup' => "Grupo de aplilamiento", 'linkedWith' => "Asociado con", - '_scaling' => "Escala", + 'apMod' => " (Mod AP: %.3g)", + 'spMod' => " (Mod SP: %.3g)", 'instantPhys' => "Instantáneo", 'castTime' => array( "Hechizo instantáneo", @@ -1701,10 +1746,6 @@ $lang = array( -1 => "Munición", -41 => "Pirita", -61 => "Presión de vapor", -101 => "Calor", -121 => "Moco", -141 => "Poder de sangre", -142 => "Cólera" ), - 'scaling' => array( - 'directSP' => "+%.2f%% del poder de hechizo al componente directo", 'directAP' => "+%.2f%% del poder de ataque al componente directo", - 'dotSP' => "+%.2f%% del poder de hechizo por tick", 'dotAP' => "+%.2f%% del poder de ataque por tick" - ), 'relItems' => array( 'base' => "<small>Muestra %s relacionados con <b>%s</b></small>", 'link' => " u ", @@ -1818,14 +1859,14 @@ $lang = array( /*102+ */ "Despedir mascota", "Dar reputación", "Invocar objeto (trampa)", "Invocar objeto (batalla)", "Invocar objeto (#3)", "Invocar objeto (#4)", /*108+ */ "Disipar mecánica", "Invocar mascota muerta", "Destruir todos los tótems", "Daño de durabilidad - Fijo", "Invocar demonio", "Resucitar con salud fija", /*114+ */ "Provocar", "Daño de durabilidad - %", "Desollar cadáver de jugador (JcJ)", "Resucitar en área con % salud", "Aprender habilidad", "Aplicar aura de área - Mascota", -/*120+ */ "Teletransportar al cementerio", "Daño de arma normalizado", null, "Tomar ruta de vuelo", "Atraer hacia", "Modificar amenaza - %", +/*120+ */ "Teletransportar al cementerio", "Daño de arma normalizado", "", "Tomar ruta de vuelo", "Atraer hacia", "Modificar amenaza - %", /*126+ */ "Robar hechizo", "Prospectar", "Aplicar aura de área - Amigo", "Aplicar aura de área - Enemigo", "Redirigir amenaza hecha %", "Reproducir sonido", /*132+ */ "Reproducir música", "Olvidar especialización", "Crédito de muerte 2", "Llamar mascota", "Sanar % de salud total", "Dar % de poder total", /*138+ */ "Saltar hacia atrás", "Abandonar misión", "Forzar lanzamiento", "Forzar lanzamiento con valor", "Activar hechizo con valor", "Aplicar aura de área - Dueño de mascota", -/*144+ */ "Empujar a destino", "Atraer a destino", "Activar runa", "Fallar misión", null, "Cargar a destino", +/*144+ */ "Empujar a destino", "Atraer a destino", "Activar runa", "Fallar misión", "Disparar misil con valor", "Cargar a destino", /*150+ */ "Iniciar misión", "Activar hechizo 2", "Invocar - Recluta a un amigo", "Crear mascota domesticada", "Descubrir ruta de vuelo", "Doble empuñadura de armas 2M", /*156+ */ "Añadir hueco a objeto", "Crear objeto de profesión", "Moler", "Renombrar mascota", "Forzar lanzamiento 2", "Cambiar número de espec. de talentos", -/*162-167*/ "Activar espec. de talentos", null, "Eliminar aura", null, null, "Actualizar fase del jugador" +/*162-167*/ "Activar espec. de talentos", "", "Eliminar aura" ), 'unkAura' => 'Aura desconocida (%1$d)', 'auras' => array( @@ -1838,7 +1879,7 @@ $lang = array( "Modificar habilidad - Temporal", "Aumentar velocidad de carrera %", "Modificar velocidad montado %", "Reducir velocidad de carrera %", "Modificar salud máxima - Fijo", "Modificar poder máximo - Fijo", "Cambio de forma", "Inmunidad a efectos de hechizo", "Inmunidad a auras de hechizo", "Inmunidad a escuela de hechizo", "Inmunidad a daño", "Inmunidad a tipo de disipación", "Activar hechizo al recibir efecto", "Activar daño al recibir efecto", "Rastrear criaturas", - "Rastrear recursos", "Ignorar todo el equipo", "Modificar parada %", null, "Modificar esquiva %", + "Rastrear recursos", "Ignorar todo el equipo", "Modificar parada %", "Activar hechizo periódicamente desde el cliente", "Modificar esquiva %", /*50+ */ "Modificar sanación crítica %", "Modificar bloqueo %", "Modificar probabilidad de crítico físico", "Drenar salud periódicamente", "Modificar probabilidad de golpe físico", "Modificar probabilidad de golpe de hechizo", "Transformar", "Modificar probabilidad de crítico de hechizo", "Aumentar velocidad de nado %", "Modificar daño hecho contra criatura", "Pacificar y silenciar", "Modificar tamaño %", "Transferir salud periódicamente", "Transferir poder periódicamente", "Drenar poder periódicamente", @@ -1861,19 +1902,19 @@ $lang = array( "Aumentar puntos de talento de mascota", "Permitir domar mascotas exóticas", "Máscara de inmunidad a mecánicas", "Retener puntos de combo", "Reducir tiempo de retroceso %", /*150+ */ "Modificar valor de bloqueo de escudo - %", "Rastrear en sigilo", "Modificar rango de agresión del jugador", "Dividir daño - Fijo", "Modificar nivel de sigilo", "Modificar respiración bajo el agua %", "Modificar toda la reputación ganada %", "Multiplicador de daño de mascota", "Modificar valor de bloqueo de escudo - Fijo", "Sin crédito JcJ", - "Modificar evasión de área de efecto", "Modificar regeneración de salud en combate", "Quemar maná", "Modificar daño crítico cuerpo a cuerpo %", null, + "Modificar evasión de área de efecto", "Modificar regeneración de salud en combate", "Quemar maná", "Modificar daño crítico cuerpo a cuerpo %", "", "Modificar poder de ataque cuerpo a cuerpo del atacante", "Modificar poder de ataque cuerpo a cuerpo - %", "Modificar poder de ataque a distancia - %", "Modificar daño hecho vs criatura", "Modificar probabilidad de crítico vs criatura", - "Cambiar visibilidad de objeto para jugador", "Modificar velocidad de carrera (no acumulable)", "Modificar velocidad montado (no acumulable)", null, "Modificar poder con % de estadística", + "Cambiar visibilidad de objeto para jugador", "Modificar velocidad de carrera (no acumulable)", "Modificar velocidad montado (no acumulable)", "", "Modificar poder con % de estadística", /*175+ */ "Modificar poder de sanación con % de estadística", "Espíritu de redención", "Encantamiento de área de efecto", "Modificar resistencia a perjuicios - %", "Modificar probabilidad de crítico de hechizo del atacante", - "Modificar poder de hechizo vs criatura", null, "Modificar resistencia con % de estadística", "Modificar amenaza % de golpes críticos", "Modificar probabilidad de golpe cuerpo a cuerpo del atacante", + "Modificar poder de hechizo vs criatura", "", "Modificar resistencia con % de estadística", "Modificar amenaza % de golpes críticos", "Modificar probabilidad de golpe cuerpo a cuerpo del atacante", "Modificar probabilidad de golpe a distancia del atacante", "Modificar probabilidad de golpe de hechizo del atacante", "Modificar probabilidad de crítico cuerpo a cuerpo del atacante", "Modificar probabilidad de crítico a distancia del atacante", "Modificar índice", "Modificar reputación ganada %", "Limitar velocidad de movimiento", "Modificar velocidad de ataque %", "Modificar celeridad % (ganancia)", "Modificar absorción de escuela objetivo %", - "Modificar absorción de escuela objetivo para habilidad", "Modificar tiempos de reutilización", "Modificar probabilidad de crítico del atacante", null, "Modificar probabilidad de golpe de hechizo", + "Modificar absorción de escuela objetivo para habilidad", "Modificar tiempos de reutilización", "Modificar probabilidad de crítico del atacante", "", "Modificar probabilidad de golpe de hechizo", /*200+ */ "Modificar experiencia de muerte ganada %", "Puede volar", "Ignorar resultado de combate", "Modificar daño crítico cuerpo a cuerpo del atacante %", "Modificar daño crítico a distancia del atacante %", "Modificar daño crítico de hechizo del atacante %", "Modificar velocidad de vuelo de vehículo %", "Modificar velocidad de vuelo montado %", "Modificar velocidad de vuelo %", "Modificar velocidad de vuelo montado % (siempre)", "Modificar velocidad de vehículo % (siempre)", "Modificar velocidad de vuelo % (no acumulable)", "Modificar poder de ataque a distancia con % de estadística", "Modificar ira generada por daño infligido", "Mascota domesticada pasiva", "Preparación de arena", "Modificar celeridad de hechizo %", "Racha de muertes", "Modificar celeridad a distancia %", "Modificar regeneración de maná con % de estadística", - "Modificar índice de combate con % de estadística", "Ignorar amenaza", null, "Proc de banda por carga", null, + "Modificar índice de combate con % de estadística", "Ignorar amenaza", "", "Proc de banda por carga", "", /*225+ */ "Proc de banda por carga con valor", "Ficticio periódico", "Activar hechizo periódicamente con valor", "Detectar sigilo", "Modificar daño de área de efecto recibido %", "Modificar salud máxima - Fijo (sin acumulación)", "Activar hechizo al recibir efecto con valor", "Modificar duración de mecánica %", "Cambiar apariencia de humanoide", "Modificar duración de mecánica % (no acumulable)", "Modificar resistencia a disipación %", "Controlar vehículo", "Modificar poder de hechizo con poder de ataque", "Modificar poder de sanación con poder de ataque", "Modificar tamaño % (no acumulable)", @@ -1881,17 +1922,17 @@ $lang = array( "Modificar duración de aura por tipo de disipación", "Modificar duración de aura por tipo de disipación (no acumulable)", "Clonar lanzador", "Modificar probabilidad de resultado de combate", "Convertir runa", /*250+ */ "Modificar salud máxima - Fijo (acumulable)", "Modificar probabilidad de esquiva enemiga", "Modificar celeridad % (pérdida)", "Modificar probabilidad de bloqueo crítico", "Desarmar mano secundaria", "Modificar daño de mecánica %", "Sin coste de reagente", "Modificar resistencia objetivo por clase de hechizo", "Modificar visual de hechizo", "Modificar sanación periódica recibida %", - "Efecto de pantalla", "Fase", "Ignorar estado de aura de habilidad", "Permitir solo habilidad", null, - null, null, "Cancelar buffer de aura al % de salud del lanzador", "Modificar poder de ataque con % de estadística", "Ignorar resistencia del objetivo", + "Efecto de pantalla", "Fase", "Ignorar estado de aura de habilidad", "Permitir solo habilidad", "", + "", "", "Cancelar buffer de aura al % de salud del lanzador", "Modificar poder de ataque con % de estadística", "Ignorar resistencia del objetivo", "Ignorar resistencia del objetivo para habilidad", "Modificar daño recibido % del lanzador", "Ignorar reinicio de temporizador de golpe", "Rayos X", "Habilidad no consume munición", /*275+ */ "Ignorar cambio de forma para habilidad", "Modificar daño de mecánica hecho %", "Modificar máximo de objetivos afectados", "Desarmar arma a distancia", "Efecto de aparición", "Modificar penetración de armadura %", "Modificar ganancia de honor %", "Modificar salud base %", "Modificar sanación recibida % del lanzador", "Aura vinculada", - "Modificar poder de ataque con resistencia de escuela", "Permitir crítico periódico de habilidad", "Modificar probabilidad de desvío de hechizo", "Ignorar dirección de golpe", null, + "Modificar poder de ataque con resistencia de escuela", "Permitir crítico periódico de habilidad", "Modificar probabilidad de desvío de hechizo", "Ignorar dirección de golpe", "", "Modificar probabilidad de crítico", "Modificar experiencia de misión ganada %", "Abrir establo", "Sobrescribir hechizos", "Prevenir regeneración de poder", - null, "Establecer ID de vehículo", "Explosión espiritual", "Estrangular", null, -/*300+ */ "Compartir daño %", "Modificar absorción de sanación de escuela", null, "Modificar daño hecho vs aura de estado - %", "Fingir embriaguez", - "Modificar velocidad mínima %", null, "Prueba de absorción de sanación", "Modificar probabilidad de golpe crítico para lanzador", null, - "Evitar daño de área de efecto en mascota", null, null, null, "Prevenir resurrección", + "", "Establecer ID de vehículo", "Explosión espiritual", "Estrangular", "", +/*300+ */ "Compartir daño %", "Modificar absorción de sanación de escuela", "", "Modificar daño hecho vs aura de estado - %", "Fingir embriaguez", + "Modificar velocidad mínima %", "", "Prueba de absorción de sanación", "Modificar probabilidad de golpe crítico para lanzador", "", + "Evitar daño de área de efecto en mascota", "", "", "", "Prevenir resurrección", /* -316*/ "Caminar bajo el agua", "Celeridad periódica" ), 'attributes0' => array( @@ -2168,6 +2209,7 @@ $lang = array( ) ), 'item' => array( + 'id' => "ID de Objeto: ", 'notFound' => "Este objeto no existe.", 'armor' => "%s armadura", 'block' => "%s bloqueo", @@ -2226,8 +2268,10 @@ $lang = array( 'uniqueEquipped'=> ["Único-Equipado", null, "Único-Equipado: %s (%d)"], 'speed' => "Veloc.", 'dps' => "(%.1f daño por segundo)", - 'vendorIn' => "Ubicación de Vendedores", - 'purchasedIn' => "[This item can be purchased in]", + 'vendorLoc' => "Ubicación de Vendedores", + 'purchasedIn' => "Este objeto se puede comprar en", + 'fishingLoc' => "Lugares de pesca", + 'fishedIn' => "Este objeto se puede pescar en", 'duration' => array( '', "Duración: %d s", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 1a717c01..eef73e93 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -134,8 +134,25 @@ $lang = array( 'dateFmtShort' => "Y-m-d", 'dateFmtLong' => "Y-m-d à g:i A", 'dateFmtIntl' => "d MMMM y", - 'timeAgo' => 'il y a %s', 'nfSeparators' => [' ', ','], + 'n_a' => "n/d", + + // date time + 'date' => "Date", + 'date_colon' => "Date : ", + 'date_on' => "le ", + 'date_ago' => "il y a %s", + 'date_at' => " à ", + 'date_to' => " à ", + 'date_simple' => '%1$d-%2$d-%3$d', + 'unknowndate' => "Date inconnue", + 'ddaysago' => "%d jours avant", + 'today' => "aujourd'hui", + 'yesterday' => "hier", + 'noon' => "midi", + 'midnight' => "minuit", + 'am' => "AM", + 'pm' => "PM", // error 'intError' => "[An internal error occured.]", @@ -236,6 +253,8 @@ $lang = array( 'atCaptain' => "Capitaine d'équipe", 'atSize' => "Type : ", 'profiler' => "Profiler de Personnage", + 'completion' => "Achèvement : ", + 'attainedBy' => "Obtenu par %d%% des profils", 'notFound' => array( 'guild' => "[This Guild doesn't exist or is not yet in the database.]", 'arenateam' => "[This Arena Team doesn't exist or is not yet in the database.]", @@ -323,7 +342,7 @@ $lang = array( 'races' => "Races", 'skill' => "compétence", 'skills' => "Compétences", - 'currency' => "monnaies", + 'currency' => "monnaie", 'currencies' => "Monnaies", 'sound' => "son", 'sounds' => "Sons", @@ -364,6 +383,7 @@ $lang = array( 'school' => "École", 'type' => "Type : ", 'valueDelim' => " - ", + 'target' => "<cible>", 'pvp' => "JcJ", 'honorPoints' => "Points d'honneur", @@ -378,7 +398,11 @@ $lang = array( 'phases' => "Phases", 'mode' => "Mode : ", - 'modes' => [-1 => "Tout", "Standard / Normal 10", "Héroïque / Normal 25", "10 héroïque", "25 héroïque"], + 'modes' => array( + [-1 => "Tout", "Standard / Normal 10", "Héroïque / Normal 25", "10 Héroïque", "25 Héroïque"], + ["Normal", "Héroïque"], + ["10 Normal", "25 Normal", "10 Héroïque", "25 Héroïque"], + ), 'expansions' => ["Classique", "The Burning Crusade", "Wrath of the Lich King"], 'stats' => ["Force", "Agilité", "Endurance", "Intelligence", "Esprit"], 'timeAbbrev' => array( @@ -478,7 +502,7 @@ $lang = array( UNIT_FLAG_IMMUNE_TO_NPC => 'Immune to creatures', UNIT_FLAG_LOOTING => 'Loot animation', UNIT_FLAG_PET_IN_COMBAT => 'Pet in combat', - UNIT_FLAG_PVP => 'PvP', + UNIT_FLAG_PVP_ENABLING => 'PvP', UNIT_FLAG_SILENCED => 'Silenced', UNIT_FLAG_CANNOT_SWIM => 'Cannot swim', UNIT_FLAG_UNK_15 => 'UNK-15 (can only swim)', @@ -551,11 +575,11 @@ $lang = array( UNIT_VIS_FLAGS_UNK5 => 'UNK-5' ), /*idx:3*/ array( - UNIT_BYTE1_ANIM_TIER_GROUND => 'ground animations', - UNIT_BYTE1_ANIM_TIER_SWIM => 'swimming animations', - UNIT_BYTE1_ANIM_TIER_HOVER => 'hovering animations', - UNIT_BYTE1_ANIM_TIER_FLY => 'flying animations', - UNIT_BYTE1_ANIM_TIER_SUMBERGED => 'submerged animations' + UNIT_ANIM_TIER_GROUND => 'ground animations', + UNIT_ANIM_TIER_SWIM => 'swimming animations', + UNIT_ANIM_TIER_HOVER => 'hovering animations', + UNIT_ANIM_TIER_FLY => 'flying animations', + UNIT_ANIM_TIER_SUMBERGED => 'submerged animations' ), 'bytesIdx' => ['StandState', null, 'VisFlags', 'AnimTier'], 'valueUNK' => '[span class=q10]unhandled value [b class=q1]%d[/b] provided for UnitFieldBytes1 on offset [b class=q1]%d[/b][/span]', @@ -641,8 +665,8 @@ $lang = array( SmartEvent::EVENT_ACTION_DONE => ['Action #[b]%1$d[/b] requested by other script', ''], SmartEvent::EVENT_ON_SPELLCLICK => ['SpellClick was triggered', ''], SmartEvent::EVENT_FRIENDLY_HEALTH_PCT => ['Health of #target# is at %11$s%%', 'Repeat every %s'], - SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], - SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], + SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], + SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], SmartEvent::EVENT_COUNTER_SET => ['Counter #[b]%1$d[/b] is equal to [b]%2$d[/b]', 'Cooldown: %s'], SmartEvent::EVENT_SCENE_START => null, SmartEvent::EVENT_SCENE_TRIGGER => null, @@ -653,6 +677,10 @@ $lang = array( SmartEvent::EVENT_ON_SPELL_FAILED => ['On [spell=%1$d] cast failed', 'Cooldown: %s'], SmartEvent::EVENT_ON_SPELL_START => ['On [spell=%1$d] cast start', 'Cooldown: %s'], SmartEvent::EVENT_ON_DESPAWN => ['On despawn', ''], + SmartEvent::EVENT_SEND_EVENT_TRIGGER => null, + SmartEvent::EVENT_AREATRIGGER_EXIT => null, + SmartEvent::EVENT_ON_AURA_APPLIED => ['On aura [spell=%1$d] applied', 'Cooldown: %s'], + SmartEvent::EVENT_ON_AURA_REMOVED => ['On aura [spell=%1$d] removed', 'Cooldown: %s'] ), 'eventFlags' => array( SmartEvent::FLAG_NO_REPEAT => 'No Repeat', @@ -696,7 +724,7 @@ $lang = array( SmartAction::ACTION_CALL_GROUPEVENTHAPPENS => ['Satisfy exploration event of [quest=%1$d] for group of #target#.', ''], SmartAction::ACTION_COMBAT_STOP => ['End current combat.', ''], SmartAction::ACTION_REMOVEAURASFROMSPELL => ['Remove(%2$d)? %2$d charges of:;(%1$d)? all auras: [spell=%1$d]\'s aura; from #target#.', 'Only own auras'], - SmartAction::ACTION_FOLLOW => ['Follow #target#(%1$d)? at %1$dm distance:;(%3$d)? until reaching [npc=%3$d]:;.(%12$d)?Exploration event of [quest=%4$d] will be satisfied.:;(%13$d)? A kill of [npc=%4$d] will be credited.:;', '(%11$d)?Follow angle\u003A %7$.2f°:;'], + SmartAction::ACTION_FOLLOW => ['Follow #target#(%1$d)? at %1$dm distance:;(%3$d)? until reaching [npc=%3$d]:;.(%12$d)?Exploration event of [quest=%4$d] will be satisfied.:;(%13$d)? A kill of [npc=%4$d] will be credited.:;', '(%11$d)?Follow angle %7$.2f°:;'], /* 30*/ SmartAction::ACTION_RANDOM_PHASE => ['Pick random Event Phase from %11$s.', ''], SmartAction::ACTION_RANDOM_PHASE_RANGE => ['Pick random Event Phase between %1$d and %2$d.', ''], SmartAction::ACTION_RESET_GOBJECT => ['Reset #target#.', ''], @@ -798,8 +826,8 @@ $lang = array( SmartAction::ACTION_PLAY_ANIMKIT => null, SmartAction::ACTION_SCENE_PLAY => null, /*130*/ SmartAction::ACTION_SCENE_CANCEL => null, - SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Spawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Cooldown: %s'], - SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Despawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Cooldown: %s'], + SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Spawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Cooldown: %s'], + SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Despawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Cooldown: %s'], SmartAction::ACTION_RESPAWN_BY_SPAWNID => ['Respawn %11$s [small class=q0](GUID: %2$d)[/small]', ''], SmartAction::ACTION_INVOKER_CAST => ['Invoker casts [spell=%1$d] at #target#.(%4$d)? (max. %4$d |4target:targets;):;', '%1$s'], SmartAction::ACTION_PLAY_CINEMATIC => ['Play cinematic #[b]%1$d[/b] for #target#', ''], @@ -1070,6 +1098,7 @@ $lang = array( 'posts' => "Messages sur le forum : " ), 'emote' => array( + 'id' => "ID Emote : ", 'notFound' => "[This Emote doesn't exist.]", // 'self' => "Vers vous-même", // 'target' => "Vers les autres avec une cible", @@ -1102,9 +1131,10 @@ $lang = array( 'state' => ['[Oneshot]', '[Continuous State]', '[Continuous Emote]'] ), 'enchantment' => array( + 'id' => "ID Enchantement : ", + 'notFound' => "Cet enchantement n'existe pas.", 'details' => "En détail", 'activation' => "Activation", - 'notFound' => "Cet enchantement n'existe pas.", 'types' => array( 1 => "Sort proc", 3 => "Sort équipé", 7 => "Sort utilisé", 8 => "Châsse prismatique", 5 => "Statistiques", 2 => "Dégâts d'arme", 6 => "DPS", 4 => "Défense" @@ -1113,10 +1143,13 @@ $lang = array( 'areatrigger' => array( 'notFound' => "This areatrigger doesn't exist.", 'foundIn' => "This areatrigger can be found in", + 'unnamed' => "[Unnamed areatrigger] #%d", 'types' => ['Unused', 'Tavern', 'Teleporter', 'Quest Objective', 'Smart Trigger', 'Script'] ), 'gameObject' => array( + 'id' => "ID Entité: ", 'notFound' => "Cette entité n'existe pas.", + 'unnamed' => "[Unnamed object] #%d", 'cat' => [0 => "Autre", 3 => "Conteneurs", 6 => "Pièges", 9 => "Livres", 25 => "Bancs de poissons", -5 => "Coffres", -3 => "Herbes", -4 => "Filons de minerai", -2 => "Quêtes", -6 => "Outils"], 'type' => [ 3 => "Conteneur", 6 => "", 9 => "Livre", 25 => "", -5 => "Coffre", -3 => "Herbe", -4 => "Filon de minerai", -2 => "Quête", -6 => ""], 'unkPosition' => "L'emplacement de cette entité est inconnu.", @@ -1130,14 +1163,15 @@ $lang = array( 'foundIn' => "Cette entité se trouve dans", 'restock' => "Se remplit toutes les %s.", 'goFlags' => array( - GO_FLAG_IN_USE => 'In use', - GO_FLAG_LOCKED => 'Locked', - GO_FLAG_INTERACT_COND => 'Cannot interact', - GO_FLAG_TRANSPORT => 'Transport', - GO_FLAG_NOT_SELECTABLE => 'Not selectable', - GO_FLAG_TRIGGERED => 'Triggered', - GO_FLAG_DAMAGED => 'Siege damaged', - GO_FLAG_DESTROYED => 'Siege destroyed' + GO_FLAG_IN_USE => 'In use', + GO_FLAG_LOCKED => 'Locked', + GO_FLAG_INTERACT_COND => 'Cannot interact', + GO_FLAG_TRANSPORT => 'Transport', + GO_FLAG_NOT_SELECTABLE => 'Not selectable', + GO_FLAG_AI_OBSTACLE => 'Triggered', + GO_FLAG_FREEZE_ANIMATION => 'Freeze Animation', + GO_FLAG_DAMAGED => 'Siege damaged', + GO_FLAG_DESTROYED => 'Siege destroyed' ), 'actions' => array( "None", "Animate Custom 0", "Animate Custom 1", "Animate Custom 2", "Animate Custom 3", @@ -1148,6 +1182,7 @@ $lang = array( ) ), 'npc' => array( + 'id' => "ID PNJ : ", 'notFound' => "Ce PNJ n'existe pas.", 'classification'=> "Classification : %s", 'petFamily' => "Familier : ", @@ -1180,10 +1215,6 @@ $lang = array( 'mechanicimmune'=> '[Not affected by mechanic] : %s', '_extraFlags' => '[Extra Flags] : ', 'versions' => '[Difficulty Versions] : ', - 'modes' => array( - 1 => ["Normal", "Héroïque"], - 2 => ["10-joueurs Normal", "25-joueurs Normal", "10-joueurs Héroïque", "25-joueurs Héroïque"] - ), 'cat' => array( "Non classés", "Bêtes", "Draconien", "Démons", "Élémentaires", "Géants", "Mort-vivant", "Humanoïdes", "Bestioles", "Mécaniques", "Non spécifié", "Totems", "Familier pacifique", "Nuages de gaz" @@ -1243,6 +1274,7 @@ $lang = array( ) ), 'event' => array( + 'id' => "ID Évènement mondial : ", 'notFound' => "Cet évènement mondial n'existe pas.", 'start' => "Début : ", 'end' => "Fin : ", @@ -1251,6 +1283,7 @@ $lang = array( 'category' => ["Non classés", "Vacances", "Récurrent", "Joueur ctr. Joueur"] ), 'achievement' => array( + 'id' => "ID Haut fait : ", 'notFound' => "Ce haut fait n'existe pas.", 'criteria' => "Critères", 'points' => "Points", @@ -1309,9 +1342,11 @@ $lang = array( ) ), 'chrClass' => array( + 'id' => "ID Classe : ", 'notFound' => "Cette classe n'existe pas." ), 'race' => array( + 'id' => "ID Race : ", 'notFound' => "Cette race n'existe pas.", 'racialLeader' => "Leader racial : ", 'startZone' => "Zone initiales", @@ -1349,6 +1384,7 @@ $lang = array( ) ), 'zone' => array( + 'id' => "ID Zone : ", 'notFound' => "Cette zone n'existe pas.", 'attunement' => ["Accès : ", "Accès Héroïque : "], 'key' => ["Clef : ", "Clef Héroïque : "], @@ -1377,6 +1413,7 @@ $lang = array( ) ), 'quest' => array( + 'id' => "ID Quête : ", 'notFound' => "Cette quête n'existe pas.", '_transfer' => 'Cette quête sera converti en <a href="?quest=%d" class="q1">%s</a> si vous transférez en <span class="icon-%s">%s</span>.', 'questLevel' => "Niveau %s", @@ -1514,6 +1551,7 @@ $lang = array( 'notFound' => "Cette icône n'existe pas." ), 'title' => array( + 'id' => "ID Titre : ", 'notFound' => "Ce titre n'existe pas.", '_transfer' => 'Ce titre sera converti en <a href="?title=%d" class="q1">%s</a> si vous transférez en <span class="icon-%s">%s</span>.', 'cat' => array( @@ -1521,6 +1559,7 @@ $lang = array( ) ), 'skill' => array( + 'id' => "ID Compétence : ", 'notFound' => "Cette compétence n'existe pas.", 'cat' => array( -6 => "Compagnons", -5 => "Montures", -4 => "Traits raciaux", 5 => "Caractéristiques", 6 => "Compétences d'armes", 7 => "Compétences de classe", 8 => "Armures utilisables", @@ -1528,6 +1567,7 @@ $lang = array( ) ), 'currency' => array( + 'id' => "ID Monnaie : ", 'notFound' => "Cette monnaie n'existe pas.", 'cap' => "Maximum total : ", 'cat' => array( @@ -1549,22 +1589,25 @@ $lang = array( ) ), 'mail' => array( - 'notFound' => "This mail doesn't exist.", + 'id' => "[Mail ID] : ", + 'notFound' => "[This mail doesn't exist.]", 'attachment' => "[Attachment]", 'mailDelivery' => "Vous recevrez cette lettre%s%s", 'mailBy' => ' de <a href="?npc=%d">%s</a>', 'mailIn' => " après %s", - 'delay' => "Delay : %s", - 'sender' => "Sender : %s", - 'untitled' => "Untitled Mail #%d" + 'delay' => "[Delay] : %s", + 'sender' => "[Sender] : %s", + 'untitled' => "[Untitled Mail] #%d" ), 'pet' => array( + 'id' => "ID Famille de familiers : ", 'notFound' => "Cette famille de familiers n'existe pas.", 'exotic' => "Exotique", 'cat' => ["Férocité", "Tenacité", "Ruse"], 'food' => ["Viande", "Poisson", "Fromage", "Pain", "Champignon", "Fruit", "Viande crue", "Poisson cru"] ), 'faction' => array( + 'id' => "ID Faction : ", 'notFound' => "Cette faction n'existe pas.", 'spillover' => "Partage de réputations", 'spilloverDesc' => "Gagner de la réputation avec cette faction fourni une réputation proportionnelle avec les factions ci-dessous.", @@ -1580,12 +1623,13 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "ID d'ensemble d'objets : ", 'notFound' => "Cet ensemble d'objets n'existe pas.", '_desc' => "<b>%s</b> est le <b>%s</b>. Il contient %s pièces.", '_descTagless' => "<b>%s</b> est un ensemble d'objet qui contient %s pièces.", '_setBonuses' => "Bonus de l'ensemble", '_conveyBonus' => "Plus d'objets de cet ensemble sont équipés, plus votre personnage aura des bonus de caractéristiques.", - '_pieces' => "pièces", + '_pieces' => "%d pièces : ", '_unavailable' => "Cet objet n'est plus disponible aux joueurs.", '_tag' => "Étiquette : ", 'summary' => "Résumé", @@ -1605,13 +1649,14 @@ $lang = array( ) ), 'spell' => array( + 'id' => "ID Sort: ", 'notFound' => "Ce sort n'existe pas.", '_spellDetails' => "Détails sur le sort", '_cost' => "Coût", '_range' => "Portée", '_castTime' => "Incantation", '_cooldown' => "Recharge", - '_distUnit' => "mètres", + '_distUnit' => " mètres", '_forms' => "Formes", '_aura' => "Aura", '_effect' => "Effet", @@ -1631,14 +1676,13 @@ $lang = array( '_rankRange' => "Rang : %d - %d", '_showXmore' => "En afficher %d de plus", - 'n_a' => "n/d", 'normal' => "Standard", 'special' => "Spécial", 'currentArea' => '<current area>', 'discovered' => "Appris via une découverte", - 'ppm' => "(%s déclenchements par minute)", - 'procChance' => "Chance : ", + 'ppm' => "(%.1f déclenchements par minute)", + 'procChance' => "Chance : %.4g%%", 'starter' => "Sortilège initiaux", 'trainingCost' => "Coût d'entraînement : ", 'channeled' => "Canalisée", @@ -1654,7 +1698,8 @@ $lang = array( 'pointsPerCP' => ", plus %s par point de combo", 'stackGroup' => "[Stack Group]", 'linkedWith' => "[Linked with]", - '_scaling' => "[Scaling]", + 'apMod' => " (Mod. AP : %.2f)", + 'spMod' => " (Mod. SP : %.2f)", 'instantPhys' => "Instantané", 'castTime' => array( "Incantation immédiate", @@ -1670,7 +1715,7 @@ $lang = array( ), 'duration' => array( "jusqu’à annulation", - "%.2G sec.", + "%.2G sec", "%.2G min", "%.2G h", "%.2G |4jour:jours;" @@ -1701,10 +1746,6 @@ $lang = array( -1 => "Munitions", -41 => "Pyrite", -61 => "Pression vapeur", -101 => "Chaleur", -121 => "Limon", -141 => "Puissance de sang", -142 => "Courroux" ), - 'scaling' => array( - 'directSP' => "+%.2f%% de la puissance des sorts directe", 'directAP' => "+%.2f%% de la puissance d'attaque directe", - 'dotSP' => "+%.2f%% de la puissance des sorts par tick", 'dotAP' => "+%.2f%% de la puissance d'attaque par tick" - ), 'relItems' => array( 'base' => "<small>Montre %s reliés à <b>%s</b></small>", 'link' => " ou ", @@ -1818,14 +1859,14 @@ $lang = array( /*102+ */ 'Dismiss Pet', 'Give Reputation', 'Summon Object (Trap)', 'Summon Object (Battle S.)','Summon Object (#3)', 'Summon Object (#4)', /*108+ */ 'Dispel Mechanic', 'Summon Dead Pet', 'Destroy All Totems', 'Durability Damage - Flat', 'Summon Demon', 'Resurrect with Flat Health', /*114+ */ 'Taunt', 'Durability Damage - %', 'Skin Player Corpse (PvP)', 'AoE Resurrect with % Health','Learn Skill', 'Apply Area Aura - Pet', -/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', null, 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', +/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', '', 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', /*126+ */ 'Spell Steal ', 'Prospect', 'Apply Area Aura - Friend', 'Apply Area Aura - Enemy', 'Redirect Done Threat %', 'Play Sound', /*132+ */ 'Play Music', 'Unlearn Specialization', 'Kill Credit 2', 'Call Pet', 'Heal for % of Total Health','Give % of Total Power', /*138+ */ 'Leap Back', 'Abandon Quest', 'Force Cast', 'Force Spell Cast with Value','Trigger Spell with Value','Apply Area Aura - Pet Owner', -/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', null, 'Charge to Dest', +/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', 'Trigger Missile with Value','Charge to Dest', /*150+ */ 'Start Quest', 'Trigger Spell 2', 'Summon - Refer-A-Friend', 'Create Tamed Pet', 'Discover Flight Path', 'Dual Wield 2H Weapons', /*156+ */ 'Add Socket to Item', 'Create Tradeskill Item', 'Milling', 'Rename Pet', 'Force Cast 2', 'Change Talent Spec. Count', -/*162-167*/ 'Activate Talent Spec.', null, 'Remove Aura', null, null, 'Update Player Phase' +/*162-167*/ 'Activate Talent Spec.', '', 'Remove Aura' ), 'unkAura' => 'Unknown Aura (%1$d)', 'auras' => array( @@ -1838,7 +1879,7 @@ $lang = array( 'Mod Skill - Temporary', 'Increase Run Speed %', 'Mod Mounted Speed %', 'Decrease Run Speed %', 'Mod Maximum Health - Flat', 'Mod Maximum Power - Flat', 'Shapeshift', 'Spell Effect Immunity', 'Spell Aura Immunity', 'Spell School Immunity', 'Damage Immunity', 'Dispel Type Immunity', 'Proc Trigger Spell', 'Proc Trigger Damage', 'Track Creatures', - 'Track Resources', 'Ignore All Gear', 'Mod Parry %', null, 'Mod Dodge %', + 'Track Resources', 'Ignore All Gear', 'Mod Parry %', 'Periodic Trigger Spell from Client', 'Mod Dodge %', /*50+ */ 'Mod Critical Healing Amount %', 'Mod Block %', 'Mod Physical Crit Chance', 'Periodically Drain Health', 'Mod Physical Hit Chance', 'Mod Spell Hit Chance', 'Transform', 'Mod Spell Crit Chance', 'Increase Swim Speed %', 'Mod Damage Done Versus Creature', 'Pacify & Silence', 'Mod Size %', 'Periodically Transfer Health', 'Periodic Transfer Power', 'Periodic Drain Power', @@ -1861,19 +1902,19 @@ $lang = array( 'Increase Pet Talent Points', 'Allow Exotic Pets Taming', 'Mechanic Immunity Mask', 'Retain Combo Points', 'Reduce Pushback Time %', /*150+ */ 'Mod Shield Block Value - %', 'Track Stealthed', 'Mod Player Aggro Range', 'Split Damage - Flat', 'Mod Stealth Level', 'Mod Underwater Breathing %', 'Mod All Reputation Gained by %', 'Done Pet Damage Multiplier', 'Mod Shield Block Value - Flat', 'No PvP Credit', - 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', null, + 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', '', 'Mod Attacker Melee Attack Power', 'Mod Melee Attack Power - %', 'Mod Ranged Attack Power - %', 'Mod Damage Done vs Creature', 'Mod Crit Chance vs Creature', - 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', null, 'Mod Spell Power by % of Stat', + 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', '', 'Mod Spell Power by % of Stat', /*175+ */ 'Mod Healing Power by % of Stat', 'Spirit of Redemption', 'AoE Charm', 'Mod Debuff Resistance - %', 'Mod Attacker Spell Crit Chance', - 'Mod Spell Power vs Creature', null, 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', + 'Mod Spell Power vs Creature', '', 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', 'Mod Attacker Ranged Hit Chance', 'Mod Attacker Spell Hit Chance', 'Mod Attacker Melee Crit Chance', 'Mod Attacker Ranged Crit Chance', 'Mod Rating', 'Mod Reputation Gained %', 'Limit Movement Speed', 'Mod Attack Speed %', 'Mod Haste % (gain)', 'Mod Target School Absorb %', - 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', null, 'Mod Spell Hit Chance', + 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', '', 'Mod Spell Hit Chance', /*200+ */ 'Mod Kill Experience Gained %', 'Can Fly', 'Ignore Combat Result', 'Mod Attacker Melee Crit Damage %', 'Mod Attacker Ranged Crit Damage %', 'Mod Attacker Spell Crit Damage %', 'Mod Vehicle Flight Speed %', 'Mod Mounted Flight Speed %', 'Mod Flight Speed %', 'Mod Mounted Flight Speed % (always)', 'Mod Vehicle Speed % (always)', 'Mod Flight Speed % (not stacking)', 'Mod Ranged Attack Power by % of Stat', 'Mod Rage Generated from Damage Dealt', 'Tamed Pet Passive', 'Arena Preparation', 'Mod Spell Haste %', 'Killing Spree', 'Mod Ranged Haste %', 'Mod Mana Regeneration by % of Stat', - 'Mod Combat Rating by % of Stat', 'Ignore Threat', null, 'Raid Proc from Charge', null, + 'Mod Combat Rating by % of Stat', 'Ignore Threat', '', 'Raid Proc from Charge', '', /*225+ */ 'Raid Proc from Charge with Value', 'Periodic Dummy', 'Periodically Trigger Spell with Value','Detect Stealth', 'Mod AoE Damage Taken %', 'Mod Maximum Health - Flat (no stacking)','Proc Trigger Spell with Value', 'Mod Mechanic Duration %', 'Change other Humanoid Display', 'Mod Mechanic Duration % (not stacking)', 'Mod Dispel Resistance %', 'Control Vehicle', 'Mod Spell Power by % of Attack Power', 'Mod Healing Power by % of Attack Power','Mod Size % (not stacking)', @@ -1881,17 +1922,17 @@ $lang = array( 'Mod Aura Duration by Dispel Type', 'Mod Aura Duration by Dispel Type (not stacking)', 'Clone Caster', 'Mod Combat Result Chance', 'Convert Rune', /*250+ */ 'Mod Maximum Health - Flat (stacking)', 'Mod Enemy Dodge Chance', 'Mod Haste % (loss)', 'Mod Critical Block Chance', 'Disarm Offhand', 'Mod Mechanic Damage Taken %', 'No Reagent Cost', 'Mod Target Resistance by Spell Class', 'Mod Spell Visual', 'Mod Periodic Healing Taken %', - 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', null, - null, null, 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', + 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', '', + '', '', 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', 'Ignore Target Resistance for Ability', 'Mod Damage Taken % from Caster', 'Ignore Swing Timer Reset', 'X-Ray', 'Ability Consume No Ammo', /*275+ */ 'Mod Ability Ignore Shapeshift', 'Mod Mechanic Damage Done %', 'Mod Max Affected Targets', 'Disarm Ranged Weapon', 'Spawn Effect', 'Mod Armor Penetration %', 'Mod Honor Gain %', 'Mod Base Health %', 'Mod Healing Taken % from Caster', 'Linked Aura', - 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', null, + 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', '', 'Mod Crit Chance', 'Mod Quest Experience Gained %', 'Open Stable', 'Override Spells', 'Prevent Power Regeneration', - null, 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', null, -/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', null, 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', - 'Mod Minimum Speed %', null, 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster',null, - 'Mod Pet AoE Damage Avoidance', null, null, null, 'Prevent Ressurection', + '', 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', '', +/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', '', 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', + 'Mod Minimum Speed %', '', 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster','', + 'Mod Pet AoE Damage Avoidance', '', '', '', 'Prevent Ressurection', /* -316*/ 'Underwater Walking', 'Periodic Haste' ), 'attributes0' => array( @@ -2168,6 +2209,7 @@ $lang = array( ) ), 'item' => array( + 'id' => "ID Objet : ", 'notFound' => "Cet objet n'existe pas.", 'armor' => "Armure : %s", 'block' => "Bloquer : %s", @@ -2226,8 +2268,10 @@ $lang = array( 'uniqueEquipped'=> ["Unique - Equipé", null, "Unique - Equipé: %s (%d)"], // ITEM_UNIQUE_EQUIPPABLE, null, ITEM_LIMIT_CATEGORY_MULTIPLE 'speed' => "Vitesse", 'dps' => "(%.1f dégâts par seconde)", - 'vendorIn' => "Lieux de vente", + 'vendorLoc' => "Lieux de vente", 'purchasedIn' => "Cet objet peut être acheté dans", + 'fishingLoc' => "Lieux de pêche", + 'fishedIn' => "Cet objet peut être pêché dans", 'duration' => array( '', "Durée : %d sec", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 81e51b44..04429780 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -134,8 +134,25 @@ $lang = array( 'dateFmtShort' => "Y-m-d", 'dateFmtLong' => "Y-m-d в g:i A", 'dateFmtIntl' => "d MMMM y г.", - 'timeAgo' => '%s назад', 'nfSeparators' => [' ', ','], + 'n_a' => "нет", + + // date time + 'date' => "По дате", + 'date_colon' => "Дата: ", + 'date_on' => "на ", + 'date_ago' => "%s назад", + 'date_at' => " в ", + 'date_to' => " в ", + 'date_simple' => '%1$d.%2$d.%3$d', + 'unknowndate' => "Неизвестная дата", + 'ddaysago' => "%d дней назад", + 'today' => "сегодня", + 'yesterday' => "вчера", + 'noon' => "полдень", + 'midnight' => "полночь", + 'am' => "a.m.", + 'pm' => "p.m.", // error 'intError' => "[An internal error occured.]", @@ -236,6 +253,8 @@ $lang = array( 'atCaptain' => "Капитан команды арены", 'atSize' => "Численности: ", 'profiler' => "Профили персонажей", + 'completion' => "Завершено: ", + 'attainedBy' => "Получено %d%% пользователей с профилями", 'notFound' => array( 'profile' => "Этот персонаж не существует, либо еще не добавлен в базу данных.", 'arenateam' => "[This Arena Team doesn't exist or is not yet in the database.]", @@ -364,6 +383,7 @@ $lang = array( 'school' => "Школа", 'type' => "Тип: ", 'valueDelim' => " - ", + 'target' => "<цель>", 'pvp' => "PvP", 'honorPoints' => "Очки Чести", @@ -378,7 +398,11 @@ $lang = array( 'phases' => "Фазы", 'mode' => "Режим: ", - 'modes' => [-1 => "Все", "Обычный / 10-норм.", "Героический / 25-норм.", "10-героич", "25-героич"], + 'modes' => array( + [-1 => "Все", "Обычный / 10-норм.", "Героический / 25-норм.", "10-героич", "25-героич"], + ["Обычный", "Героический"], + ["10-нормал", "25-нормал", "10-героич", "25-героич"], + ), 'expansions' => array("World of Warcraft", "The Burning Crusade", "Wrath of the Lich King"), 'stats' => array("к силе", "к ловкости", "к выносливости", "к интеллекту", "к духу"), 'timeAbbrev' => array( @@ -478,7 +502,7 @@ $lang = array( UNIT_FLAG_IMMUNE_TO_NPC => 'Immune to creatures', UNIT_FLAG_LOOTING => 'Loot animation', UNIT_FLAG_PET_IN_COMBAT => 'Pet in combat', - UNIT_FLAG_PVP => 'PvP', + UNIT_FLAG_PVP_ENABLING => 'PvP', UNIT_FLAG_SILENCED => 'Silenced', UNIT_FLAG_CANNOT_SWIM => 'Cannot swim', UNIT_FLAG_UNK_15 => 'UNK-15 (can only swim)', @@ -551,11 +575,11 @@ $lang = array( UNIT_VIS_FLAGS_UNK5 => 'UNK-5' ), /*idx:3*/ array( - UNIT_BYTE1_ANIM_TIER_GROUND => 'ground animations', - UNIT_BYTE1_ANIM_TIER_SWIM => 'swimming animations', - UNIT_BYTE1_ANIM_TIER_HOVER => 'hovering animations', - UNIT_BYTE1_ANIM_TIER_FLY => 'flying animations', - UNIT_BYTE1_ANIM_TIER_SUMBERGED => 'submerged animations' + UNIT_ANIM_TIER_GROUND => 'ground animations', + UNIT_ANIM_TIER_SWIM => 'swimming animations', + UNIT_ANIM_TIER_HOVER => 'hovering animations', + UNIT_ANIM_TIER_FLY => 'flying animations', + UNIT_ANIM_TIER_SUMBERGED => 'submerged animations' ), 'bytesIdx' => ['StandState', null, 'VisFlags', 'AnimTier'], 'valueUNK' => '[span class=q10]unhandled value [b class=q1]%d[/b] provided for UnitFieldBytes1 on offset [b class=q1]%d[/b][/span]', @@ -641,8 +665,8 @@ $lang = array( SmartEvent::EVENT_ACTION_DONE => ['Action #[b]%1$d[/b] requested by other script', ''], SmartEvent::EVENT_ON_SPELLCLICK => ['SpellClick was triggered', ''], SmartEvent::EVENT_FRIENDLY_HEALTH_PCT => ['Health of #target# is at %11$s%%', 'Repeat every %s'], - SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], - SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], + SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], + SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], SmartEvent::EVENT_COUNTER_SET => ['Counter #[b]%1$d[/b] is equal to [b]%2$d[/b]', 'Cooldown: %s'], SmartEvent::EVENT_SCENE_START => null, SmartEvent::EVENT_SCENE_TRIGGER => null, @@ -653,6 +677,10 @@ $lang = array( SmartEvent::EVENT_ON_SPELL_FAILED => ['On [spell=%1$d] cast failed', 'Cooldown: %s'], SmartEvent::EVENT_ON_SPELL_START => ['On [spell=%1$d] cast start', 'Cooldown: %s'], SmartEvent::EVENT_ON_DESPAWN => ['On despawn', ''], + SmartEvent::EVENT_SEND_EVENT_TRIGGER => null, + SmartEvent::EVENT_AREATRIGGER_EXIT => null, + SmartEvent::EVENT_ON_AURA_APPLIED => ['On aura [spell=%1$d] applied', 'Cooldown: %s'], + SmartEvent::EVENT_ON_AURA_REMOVED => ['On aura [spell=%1$d] removed', 'Cooldown: %s'] ), 'eventFlags' => array( SmartEvent::FLAG_NO_REPEAT => 'No Repeat', @@ -696,7 +724,7 @@ $lang = array( SmartAction::ACTION_CALL_GROUPEVENTHAPPENS => ['Satisfy exploration event of [quest=%1$d] for group of #target#.', ''], SmartAction::ACTION_COMBAT_STOP => ['End current combat.', ''], SmartAction::ACTION_REMOVEAURASFROMSPELL => ['Remove(%2$d)? %2$d charges of:;(%1$d)? all auras: [spell=%1$d]\'s aura; from #target#.', 'Only own auras'], - SmartAction::ACTION_FOLLOW => ['Follow #target#(%1$d)? at %1$dm distance:;(%3$d)? until reaching [npc=%3$d]:;.(%12$d)?Exploration event of [quest=%4$d] will be satisfied.:;(%13$d)? A kill of [npc=%4$d] will be credited.:;', '(%11$d)?Follow angle\u003A %7$.2f°:;'], + SmartAction::ACTION_FOLLOW => ['Follow #target#(%1$d)? at %1$dm distance:;(%3$d)? until reaching [npc=%3$d]:;.(%12$d)?Exploration event of [quest=%4$d] will be satisfied.:;(%13$d)? A kill of [npc=%4$d] will be credited.:;', '(%11$d)?Follow angle %7$.2f°:;'], /* 30*/ SmartAction::ACTION_RANDOM_PHASE => ['Pick random Event Phase from %11$s.', ''], SmartAction::ACTION_RANDOM_PHASE_RANGE => ['Pick random Event Phase between %1$d and %2$d.', ''], SmartAction::ACTION_RESET_GOBJECT => ['Reset #target#.', ''], @@ -798,8 +826,8 @@ $lang = array( SmartAction::ACTION_PLAY_ANIMKIT => null, SmartAction::ACTION_SCENE_PLAY => null, /*130*/ SmartAction::ACTION_SCENE_CANCEL => null, - SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Spawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Cooldown: %s'], - SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Despawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Cooldown: %s'], + SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Spawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Cooldown: %s'], + SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Despawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Cooldown: %s'], SmartAction::ACTION_RESPAWN_BY_SPAWNID => ['Respawn %11$s [small class=q0](GUID: %2$d)[/small]', ''], SmartAction::ACTION_INVOKER_CAST => ['Invoker casts [spell=%1$d] at #target#.(%4$d)? (max. %4$d |4target:targets;):;', '%1$s'], SmartAction::ACTION_PLAY_CINEMATIC => ['Play cinematic #[b]%1$d[/b] for #target#', ''], @@ -1070,6 +1098,7 @@ $lang = array( 'posts' => "Сообщений на форумах: " ), 'emote' => array( + 'id' => "Эмоция ID: ", 'notFound' => "[This Emote doesn't exist.]", // 'self' => "[To Yourself]", // 'target' => "[To others with a target]", @@ -1102,9 +1131,10 @@ $lang = array( 'state' => ['[Oneshot]', '[Continuous State]', '[Continuous Emote]'] ), 'enchantment' => array( + 'id' => "Улучшение ID: ", + 'notFound' => "Такой улучшение не существует.", 'details' => "Подробности", 'activation' => "Активации", - 'notFound' => "Такой улучшение не существует.", 'types' => array( 1 => "[Proc Spell]", 3 => "[Equip Spell]", 7 => "[Use Spell]", 8 => "Бесцветное гнездо", 5 => "Характеристики", 2 => "Урон оружия", 6 => "УВС", 4 => "Защита" @@ -1113,10 +1143,13 @@ $lang = array( 'areatrigger' => array( 'notFound' => "This areatrigger doesn't exist.", 'foundIn' => "This areatrigger can be found in", + 'unnamed' => "[Unnamed areatrigger] #%d", 'types' => ['Unused', 'Tavern', 'Teleporter', 'Quest Objective', 'Smart Trigger', 'Script'] ), 'gameObject' => array( + 'id' => "Объект ID: ", 'notFound' => "Такой объект не существует.", + 'unnamed' => "[Unnamed object] #%d", 'cat' => [0 => "Другое", 3 => "Контейнеры", 6 => "Ловушки", 9 => "Книги", 25 => "Рыболовные лунки", -5 => "Сундуки", -3 => "Травы", -4 => "Полезные ископаемые", -2 => "Задания", -6 => "Инструменты"], 'type' => [ 3 => "Контейнер", 6 => "", 9 => "Книга", 25 => "", -5 => "Сундук", -3 => "Растение", -4 => "Полезное ископаемое", -2 => "Задание", -6 => ""], 'unkPosition' => "Местонахождение этого объекта неизвестно.", @@ -1130,14 +1163,15 @@ $lang = array( 'foundIn' => "Этот НИП может быть найден в следующих зонах:", 'restock' => "[Restocks every %s.]", 'goFlags' => array( - GO_FLAG_IN_USE => 'In use', - GO_FLAG_LOCKED => 'Locked', - GO_FLAG_INTERACT_COND => 'Cannot interact', - GO_FLAG_TRANSPORT => 'Transport', - GO_FLAG_NOT_SELECTABLE => 'Not selectable', - GO_FLAG_TRIGGERED => 'Triggered', - GO_FLAG_DAMAGED => 'Siege damaged', - GO_FLAG_DESTROYED => 'Siege destroyed' + GO_FLAG_IN_USE => 'In use', + GO_FLAG_LOCKED => 'Locked', + GO_FLAG_INTERACT_COND => 'Cannot interact', + GO_FLAG_TRANSPORT => 'Transport', + GO_FLAG_NOT_SELECTABLE => 'Not selectable', + GO_FLAG_AI_OBSTACLE => 'Triggered', + GO_FLAG_FREEZE_ANIMATION => 'Freeze Animation', + GO_FLAG_DAMAGED => 'Siege damaged', + GO_FLAG_DESTROYED => 'Siege destroyed' ), 'actions' => array( "None", "Animate Custom 0", "Animate Custom 1", "Animate Custom 2", "Animate Custom 3", @@ -1148,6 +1182,7 @@ $lang = array( ) ), 'npc' => array( + 'id' => "НИП ID: ", 'notFound' => "Такой НИП не существует.", 'classification'=> "Классификация: %s", 'petFamily' => "Семейство питомца: ", @@ -1180,10 +1215,6 @@ $lang = array( 'mechanicimmune'=> '[Not affected by mechanic]: %s', '_extraFlags' => '[Extra Flags]: ', 'versions' => '[Difficulty Versions]: ', - 'modes' => array( - 1 => ["Обычный", "Героический"], - 2 => ["10 нормал.", "25 нормал.", "10 героич.", "25 героич."] - ), 'cat' => array( "Разное", "Животные", "Дракон", "Демоны", "Элементали", "Великаны", "Нежить", "Гуманоиды", "Существа", "Механизмы", "Не указано", "Тотемы", "Спутники", "Облака газа" @@ -1243,6 +1274,7 @@ $lang = array( ) ), 'event' => array( + 'id' => "Игровое событие ID: ", 'notFound' => "Это игровое событие не существует.", 'start' => "Начало: ", 'end' => "Конец: ", @@ -1251,6 +1283,7 @@ $lang = array( 'category' => array("Разное", "Праздники", "Периодические", "PvP") ), 'achievement' => array( + 'id' => "Достижение ID: ", 'notFound' => "Такое достижение не существует.", 'criteria' => "Критерий", 'points' => "Очки", @@ -1309,9 +1342,11 @@ $lang = array( ) ), 'chrClass' => array( + 'id' => "Класс ID: ", 'notFound' => "Такой класс не существует." ), 'race' => array( + 'id' => "Раса ID: ", 'notFound' => "Такая раса не существует.", 'racialLeader' => "Лидер расы: ", 'startZone' => "Начальная локация", @@ -1349,6 +1384,7 @@ $lang = array( ) ), 'zone' => array( + 'id' => "Игровая зона ID: ", 'notFound' => "Такая местность не существует.", 'attunement' => ["[Attunement]: ", "[Heroic attunement]: "], 'key' => ["[Key]: ", "[Heroic key]: "], @@ -1377,6 +1413,7 @@ $lang = array( ) ), 'quest' => array( + 'id' => "Задание ID: ", 'notFound' => "Такое задание не существует.", '_transfer' => 'Этот предмет превратится в <a href="?quest=%d" class="q1">%s</a>, если вы перейдете за <span class="icon-%s">%s</span>.', 'questLevel' => "%s-го уровня", @@ -1514,6 +1551,7 @@ $lang = array( 'notFound' => "Этой иконки не существует" ), 'title' => array( + 'id' => "Звание ID: ", 'notFound' => "Такое звание не существует.", '_transfer' => 'Этот предмет превратится в <a href="?title=%d" class="q1">%s</a>, если вы перейдете за <span class="icon-%s">%s</span>.', 'cat' => array( @@ -1521,6 +1559,7 @@ $lang = array( ) ), 'skill' => array( + 'id' => "Навык ID: ", 'notFound' => "Этот навык не существует.", 'cat' => array( -6 => "Спутники", -5 => "Транспорт", -4 => "Классовые навыки", 5 => "Характеристики", 6 => "Оружейные навыки", 7 => "Классовые навыки", 8 => "Доспехи", @@ -1528,6 +1567,7 @@ $lang = array( ) ), 'currency' => array( + 'id' => "Валюта ID: ", 'notFound' => "Такая валюта не существует.", 'cap' => "Максимум всего: ", 'cat' => array( @@ -1549,6 +1589,7 @@ $lang = array( ) ), 'mail' => array( + 'id' => "[Mail ID]: ", 'notFound' => "[This mail doesn't exist].", 'attachment' => "[Attachment]", 'mailDelivery' => "Вы получите это письмо%s%s", @@ -1559,12 +1600,14 @@ $lang = array( 'untitled' => "[Untitled Mail] #%d" ), 'pet' => array( + 'id' => "Питомец ID: ", 'notFound' => "Такой породы питомцев не существует.", 'exotic' => "Экзотический", 'cat' => ["Свирепость", "Упорство", "Хитрость"], 'food' => ["Мясо", "Рыба", "Сыр", "Хлеб", "Грибы", "Фрукты", "Сырое мясо", "Сырая рыба"] ), 'faction' => array( + 'id' => "Фракция ID: ", 'notFound' => "Такая фракция не существует.", 'spillover' => "Распространение репутации", 'spilloverDesc' => "Получение репутации у этой фракции также дает пропорциональный выигрыш по отношению к фракциям, перечисленным ниже.", @@ -1580,12 +1623,13 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "Комплект ID: ", 'notFound' => "Такой комплект не существует.", '_desc' => "<b>%s</b> — <b>%s</b>. Он состоит из %s предметов.", '_descTagless' => "<b>%s</b> — набор из %s предметов.", '_setBonuses' => "Бонус за комплект", '_conveyBonus' => "Ношение большего числа предметов из этого комплекта предоставит бонусы для вашего персонажа.", - '_pieces' => "частей", + '_pieces' => "%d частей: ", '_unavailable' => "Этот набор предметов не доступен игрокам.", '_tag' => "Тэг: ", 'summary' => "Сводка", @@ -1605,13 +1649,14 @@ $lang = array( ) ), 'spell' => array( + 'id' => "Заклинание ID: ", 'notFound' => "Такое заклинание не существует.", '_spellDetails' => "Описание заклинания", '_cost' => "Цена", '_range' => "Радиус действия", '_castTime' => "Применение", '_cooldown' => "Восстановление", - '_distUnit' => "метров", + '_distUnit' => " метров", '_forms' => "Форма", '_aura' => "аура", '_effect' => "Эффект", @@ -1631,14 +1676,13 @@ $lang = array( '_rankRange' => "Ранг: %d - %d", '_showXmore' => "Показать на %d больше", - 'n_a' => "нет", 'normal' => "Обычный", 'special' => "Особый", 'currentArea' => '<current area>', 'discovered' => "Изучается путём освоения местности", - 'ppm' => "(Срабатывает %s раз в минуту)", - 'procChance' => "Шанс срабатывания: ", + 'ppm' => "(Срабатывает %.1f раз в минуту)", + 'procChance' => "Шанс срабатывания: %.4g%%", 'starter' => "Начальное заклинание", 'trainingCost' => "Цена обучения: ", 'channeled' => "Направляемое", @@ -1654,7 +1698,8 @@ $lang = array( 'pointsPerCP' => ", плюс %s в прием в серии", 'stackGroup' => "[Stack Group]", 'linkedWith' => "[Linked with]", - '_scaling' => "[Scaling]", + 'apMod' => " (Мод.-р АП:%.3g)", + 'spMod' => " (Мод.-р СП:%.3g)", 'instantPhys' => "Мгновенное действие", 'castTime' => array( "Мгновенное действие", @@ -1701,10 +1746,6 @@ $lang = array( -1 => "Боеприпасы", -41 => "Колчедан", -61 => "Давление пара", -101 => "Жар", -121 => "Слизнюк", -141 => "Сила крови", -142 => "Гнев" ), - 'scaling' => array( - 'directSP' => "[+%.2f%% of spell power to direct component]", 'directAP' => "[+%.2f%% of attack power to direct component]", - 'dotSP' => "[+%.2f%% of spell power per tick]", 'dotAP' => "[+%.2f%% of attack power per tick]" - ), 'relItems' => array( 'base' => "<small>Показать %s, относящиеся к профессии <b>%s</b></small>", 'link' => " или ", @@ -1818,14 +1859,14 @@ $lang = array( /*102+ */ 'Dismiss Pet', 'Give Reputation', 'Summon Object (Trap)', 'Summon Object (Battle S.)','Summon Object (#3)', 'Summon Object (#4)', /*108+ */ 'Dispel Mechanic', 'Summon Dead Pet', 'Destroy All Totems', 'Durability Damage - Flat', 'Summon Demon', 'Resurrect with Flat Health', /*114+ */ 'Taunt', 'Durability Damage - %', 'Skin Player Corpse (PvP)', 'AoE Resurrect with % Health','Learn Skill', 'Apply Area Aura - Pet', -/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', null, 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', +/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', '', 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', /*126+ */ 'Spell Steal ', 'Prospect', 'Apply Area Aura - Friend', 'Apply Area Aura - Enemy', 'Redirect Done Threat %', 'Play Sound', /*132+ */ 'Play Music', 'Unlearn Specialization', 'Kill Credit 2', 'Call Pet', 'Heal for % of Total Health','Give % of Total Power', /*138+ */ 'Leap Back', 'Abandon Quest', 'Force Cast', 'Force Spell Cast with Value','Trigger Spell with Value','Apply Area Aura - Pet Owner', -/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', null, 'Charge to Dest', +/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', 'Trigger Missile with Value','Charge to Dest', /*150+ */ 'Start Quest', 'Trigger Spell 2', 'Summon - Refer-A-Friend', 'Create Tamed Pet', 'Discover Flight Path', 'Dual Wield 2H Weapons', /*156+ */ 'Add Socket to Item', 'Create Tradeskill Item', 'Milling', 'Rename Pet', 'Force Cast 2', 'Change Talent Spec. Count', -/*162-167*/ 'Activate Talent Spec.', null, 'Remove Aura', null, null, 'Update Player Phase' +/*162-167*/ 'Activate Talent Spec.', '', 'Remove Aura' ), 'unkAura' => 'Unknown Aura (%1$d)', 'auras' => array( @@ -1838,7 +1879,7 @@ $lang = array( 'Mod Skill - Temporary', 'Increase Run Speed %', 'Mod Mounted Speed %', 'Decrease Run Speed %', 'Mod Maximum Health - Flat', 'Mod Maximum Power - Flat', 'Shapeshift', 'Spell Effect Immunity', 'Spell Aura Immunity', 'Spell School Immunity', 'Damage Immunity', 'Dispel Type Immunity', 'Proc Trigger Spell', 'Proc Trigger Damage', 'Track Creatures', - 'Track Resources', 'Ignore All Gear', 'Mod Parry %', null, 'Mod Dodge %', + 'Track Resources', 'Ignore All Gear', 'Mod Parry %', 'Periodic Trigger Spell from Client', 'Mod Dodge %', /*50+ */ 'Mod Critical Healing Amount %', 'Mod Block %', 'Mod Physical Crit Chance', 'Periodically Drain Health', 'Mod Physical Hit Chance', 'Mod Spell Hit Chance', 'Transform', 'Mod Spell Crit Chance', 'Increase Swim Speed %', 'Mod Damage Done Versus Creature', 'Pacify & Silence', 'Mod Size %', 'Periodically Transfer Health', 'Periodic Transfer Power', 'Periodic Drain Power', @@ -1861,19 +1902,19 @@ $lang = array( 'Increase Pet Talent Points', 'Allow Exotic Pets Taming', 'Mechanic Immunity Mask', 'Retain Combo Points', 'Reduce Pushback Time %', /*150+ */ 'Mod Shield Block Value - %', 'Track Stealthed', 'Mod Player Aggro Range', 'Split Damage - Flat', 'Mod Stealth Level', 'Mod Underwater Breathing %', 'Mod All Reputation Gained by %', 'Done Pet Damage Multiplier', 'Mod Shield Block Value - Flat', 'No PvP Credit', - 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', null, + 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', '', 'Mod Attacker Melee Attack Power', 'Mod Melee Attack Power - %', 'Mod Ranged Attack Power - %', 'Mod Damage Done vs Creature', 'Mod Crit Chance vs Creature', - 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', null, 'Mod Spell Power by % of Stat', + 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', '', 'Mod Spell Power by % of Stat', /*175+ */ 'Mod Healing Power by % of Stat', 'Spirit of Redemption', 'AoE Charm', 'Mod Debuff Resistance - %', 'Mod Attacker Spell Crit Chance', - 'Mod Spell Power vs Creature', null, 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', + 'Mod Spell Power vs Creature', '', 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', 'Mod Attacker Ranged Hit Chance', 'Mod Attacker Spell Hit Chance', 'Mod Attacker Melee Crit Chance', 'Mod Attacker Ranged Crit Chance', 'Mod Rating', 'Mod Reputation Gained %', 'Limit Movement Speed', 'Mod Attack Speed %', 'Mod Haste % (gain)', 'Mod Target School Absorb %', - 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', null, 'Mod Spell Hit Chance', + 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', '', 'Mod Spell Hit Chance', /*200+ */ 'Mod Kill Experience Gained %', 'Can Fly', 'Ignore Combat Result', 'Mod Attacker Melee Crit Damage %', 'Mod Attacker Ranged Crit Damage %', 'Mod Attacker Spell Crit Damage %', 'Mod Vehicle Flight Speed %', 'Mod Mounted Flight Speed %', 'Mod Flight Speed %', 'Mod Mounted Flight Speed % (always)', 'Mod Vehicle Speed % (always)', 'Mod Flight Speed % (not stacking)', 'Mod Ranged Attack Power by % of Stat', 'Mod Rage Generated from Damage Dealt', 'Tamed Pet Passive', 'Arena Preparation', 'Mod Spell Haste %', 'Killing Spree', 'Mod Ranged Haste %', 'Mod Mana Regeneration by % of Stat', - 'Mod Combat Rating by % of Stat', 'Ignore Threat', null, 'Raid Proc from Charge', null, + 'Mod Combat Rating by % of Stat', 'Ignore Threat', '', 'Raid Proc from Charge', '', /*225+ */ 'Raid Proc from Charge with Value', 'Periodic Dummy', 'Periodically Trigger Spell with Value','Detect Stealth', 'Mod AoE Damage Taken %', 'Mod Maximum Health - Flat (no stacking)','Proc Trigger Spell with Value', 'Mod Mechanic Duration %', 'Change other Humanoid Display', 'Mod Mechanic Duration % (not stacking)', 'Mod Dispel Resistance %', 'Control Vehicle', 'Mod Spell Power by % of Attack Power', 'Mod Healing Power by % of Attack Power','Mod Size % (not stacking)', @@ -1881,17 +1922,17 @@ $lang = array( 'Mod Aura Duration by Dispel Type', 'Mod Aura Duration by Dispel Type (not stacking)', 'Clone Caster', 'Mod Combat Result Chance', 'Convert Rune', /*250+ */ 'Mod Maximum Health - Flat (stacking)', 'Mod Enemy Dodge Chance', 'Mod Haste % (loss)', 'Mod Critical Block Chance', 'Disarm Offhand', 'Mod Mechanic Damage Taken %', 'No Reagent Cost', 'Mod Target Resistance by Spell Class', 'Mod Spell Visual', 'Mod Periodic Healing Taken %', - 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', null, - null, null, 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', + 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', '', + '', '', 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', 'Ignore Target Resistance for Ability', 'Mod Damage Taken % from Caster', 'Ignore Swing Timer Reset', 'X-Ray', 'Ability Consume No Ammo', /*275+ */ 'Mod Ability Ignore Shapeshift', 'Mod Mechanic Damage Done %', 'Mod Max Affected Targets', 'Disarm Ranged Weapon', 'Spawn Effect', 'Mod Armor Penetration %', 'Mod Honor Gain %', 'Mod Base Health %', 'Mod Healing Taken % from Caster', 'Linked Aura', - 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', null, + 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', '', 'Mod Crit Chance', 'Mod Quest Experience Gained %', 'Open Stable', 'Override Spells', 'Prevent Power Regeneration', - null, 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', null, -/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', null, 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', - 'Mod Minimum Speed %', null, 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster',null, - 'Mod Pet AoE Damage Avoidance', null, null, null, 'Prevent Ressurection', + '', 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', '', +/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', '', 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', + 'Mod Minimum Speed %', '', 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster','', + 'Mod Pet AoE Damage Avoidance', '', '', '', 'Prevent Ressurection', /* -316*/ 'Underwater Walking', 'Periodic Haste' ), 'attributes0' => array( @@ -2168,6 +2209,7 @@ $lang = array( ) ), 'item' => array( + 'id' => "Предмет ID: ", 'notFound' => "Такой предмет не существует.", 'armor' => "Броня: %s", 'block' => "Блок: %s", @@ -2226,8 +2268,10 @@ $lang = array( 'uniqueEquipped'=> ["Уникальный использующийся", null, "Уникальный использующийся предмет: %s (%d)"], 'speed' => "Скорость", 'dps' => "(%.1f ед. урона в секунду)", - 'vendorIn' => "Расположение торговца", + 'vendorLoc' => "Расположение торговца", 'purchasedIn' => "Этот предмет приобретается в", + 'fishingLoc' => "Места рыбной ловли", + 'fishedIn' => "Этот предмет вылавливается в", 'duration' => array( '', "Срок действия: %d |4секунда:секунды:секунд;", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index 05c1d150..ec6a1025 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -134,8 +134,25 @@ $lang = array( 'dateFmtShort' => "Y/m/d", 'dateFmtLong' => "Y/m/d \a\\t g:i A", 'dateFmtIntl' => "y年M月d日", - 'timeAgo' => '%s之前', 'nfSeparators' => [',', '.'], + 'n_a' => "n/a", + + // date time + 'date' => "日期", + 'date_colon' => "日期:", + 'date_on' => "在 ", + 'date_ago' => "%s前", + 'date_at' => "于", + 'date_to' => "至", + 'date_simple' => '%3$d/%2$d/%1$d', + 'unknowndate' => "未知日期", + 'ddaysago' => "%d天前", + 'today' => "今日", + 'yesterday' => "昨天", + 'noon' => "正午", + 'midnight' => "午夜", + 'am' => "AM", + 'pm' => "PM", // error 'intError' => "发生内部错误。", @@ -236,6 +253,8 @@ $lang = array( 'atCaptain' => "竞技场战队队长", 'atSize' => "团队规模:", 'profiler' => "角色概况", + 'completion' => "达成:", + 'attainedBy' => "%d%%角色已完成", 'notFound' => array( 'guild' => "该公会不存在或尚未被收录到数据库中。", 'arenateam' => "该竞技场战队不存在或尚未被收录到数据库中。", @@ -364,6 +383,7 @@ $lang = array( 'school' => "类型", 'type' => "类型:", 'valueDelim' => "到", + 'target' => "<目标>", 'pvp' => "PvP", 'honorPoints' => "荣誉点数", @@ -378,7 +398,11 @@ $lang = array( 'phases' => "阶段", 'mode' => "模式:", - 'modes' => [-1 => "任何", "普通 / 普通 10人", "英雄 / 普通 25人", "英雄 10人", "英雄 25人"], + 'modes' => array( + [-1 => "任何", "普通 / 普通 10人", "英雄 / 普通 25人", "英雄 10人", "英雄 25人"], + ["普通", "英雄"], + ["普通 10人", "普通 25人", "英雄 10人", "英雄 25人"], + ), 'expansions' => ["经典旧世", "燃烧的远征", "巫妖王之怒"], 'stats' => ["力量", "敏捷", "耐力", "智力", "精神"], 'timeAbbrev' => array( @@ -478,7 +502,7 @@ $lang = array( UNIT_FLAG_IMMUNE_TO_NPC => 'Immune to creatures', UNIT_FLAG_LOOTING => 'Loot animation', UNIT_FLAG_PET_IN_COMBAT => 'Pet in combat', - UNIT_FLAG_PVP => 'PvP', + UNIT_FLAG_PVP_ENABLING => 'PvP', UNIT_FLAG_SILENCED => 'Silenced', UNIT_FLAG_CANNOT_SWIM => 'Cannot swim', UNIT_FLAG_UNK_15 => 'UNK-15 (can only swim)', @@ -551,11 +575,11 @@ $lang = array( UNIT_VIS_FLAGS_UNK5 => 'UNK-5' ), /*idx:3*/ array( - UNIT_BYTE1_ANIM_TIER_GROUND => 'ground animations', - UNIT_BYTE1_ANIM_TIER_SWIM => 'swimming animations', - UNIT_BYTE1_ANIM_TIER_HOVER => 'hovering animations', - UNIT_BYTE1_ANIM_TIER_FLY => 'flying animations', - UNIT_BYTE1_ANIM_TIER_SUMBERGED => 'submerged animations' + UNIT_ANIM_TIER_GROUND => 'ground animations', + UNIT_ANIM_TIER_SWIM => 'swimming animations', + UNIT_ANIM_TIER_HOVER => 'hovering animations', + UNIT_ANIM_TIER_FLY => 'flying animations', + UNIT_ANIM_TIER_SUMBERGED => 'submerged animations' ), 'bytesIdx' => ['StandState', null, 'VisFlags', 'AnimTier'], 'valueUNK' => '[span class=q10]unhandled value [b class=q1]%d[/b] provided for UnitFieldBytes1 on offset [b class=q1]%d[/b][/span]', @@ -641,8 +665,8 @@ $lang = array( SmartEvent::EVENT_ACTION_DONE => ['Action #[b]%1$d[/b] requested by other script', ''], SmartEvent::EVENT_ON_SPELLCLICK => ['SpellClick was triggered', ''], SmartEvent::EVENT_FRIENDLY_HEALTH_PCT => ['Health of #target# is at %11$s%%', 'Repeat every %s'], - SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], - SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID\u003A %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], + SmartEvent::EVENT_DISTANCE_CREATURE => ['[npc=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], + SmartEvent::EVENT_DISTANCE_GAMEOBJECT => ['[object=%11$d](%1$d)? [small class=q0](GUID %1$d)[/small]:; is within %3$dm', 'Repeat every %s'], SmartEvent::EVENT_COUNTER_SET => ['Counter #[b]%1$d[/b] is equal to [b]%2$d[/b]', 'Cooldown: %s'], SmartEvent::EVENT_SCENE_START => null, SmartEvent::EVENT_SCENE_TRIGGER => null, @@ -653,6 +677,10 @@ $lang = array( SmartEvent::EVENT_ON_SPELL_FAILED => ['On [spell=%1$d] cast failed', 'Cooldown: %s'], SmartEvent::EVENT_ON_SPELL_START => ['On [spell=%1$d] cast start', 'Cooldown: %s'], SmartEvent::EVENT_ON_DESPAWN => ['On despawn', ''], + SmartEvent::EVENT_SEND_EVENT_TRIGGER => null, + SmartEvent::EVENT_AREATRIGGER_EXIT => null, + SmartEvent::EVENT_ON_AURA_APPLIED => ['On aura [spell=%1$d] applied', 'Cooldown: %s'], + SmartEvent::EVENT_ON_AURA_REMOVED => ['On aura [spell=%1$d] removed', 'Cooldown: %s'] ), 'eventFlags' => array( SmartEvent::FLAG_NO_REPEAT => 'No Repeat', @@ -696,7 +724,7 @@ $lang = array( SmartAction::ACTION_CALL_GROUPEVENTHAPPENS => ['Satisfy exploration event of [quest=%1$d] for group of #target#.', ''], SmartAction::ACTION_COMBAT_STOP => ['End current combat.', ''], SmartAction::ACTION_REMOVEAURASFROMSPELL => ['Remove(%2$d)? %2$d charges of:;(%1$d)? all auras: [spell=%1$d]\'s aura; from #target#.', 'Only own auras'], - SmartAction::ACTION_FOLLOW => ['Follow #target#(%1$d)? at %1$dm distance:;(%3$d)? until reaching [npc=%3$d]:;.(%12$d)?Exploration event of [quest=%4$d] will be satisfied.:;(%13$d)? A kill of [npc=%4$d] will be credited.:;', '(%11$d)?Follow angle\u003A %7$.2f°:;'], + SmartAction::ACTION_FOLLOW => ['Follow #target#(%1$d)? at %1$dm distance:;(%3$d)? until reaching [npc=%3$d]:;.(%12$d)?Exploration event of [quest=%4$d] will be satisfied.:;(%13$d)? A kill of [npc=%4$d] will be credited.:;', '(%11$d)?Follow angle %7$.2f°:;'], /* 30*/ SmartAction::ACTION_RANDOM_PHASE => ['Pick random Event Phase from %11$s.', ''], SmartAction::ACTION_RANDOM_PHASE_RANGE => ['Pick random Event Phase between %1$d and %2$d.', ''], SmartAction::ACTION_RESET_GOBJECT => ['Reset #target#.', ''], @@ -798,8 +826,8 @@ $lang = array( SmartAction::ACTION_PLAY_ANIMKIT => null, SmartAction::ACTION_SCENE_PLAY => null, /*130*/ SmartAction::ACTION_SCENE_CANCEL => null, - SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Spawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Cooldown: %s'], - SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Despawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags\u003A %12$s:; %13$s', 'Cooldown: %s'], + SmartAction::ACTION_SPAWN_SPAWNGROUP => ['Spawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Cooldown: %s'], + SmartAction::ACTION_DESPAWN_SPAWNGROUP => ['Despawn SpawnGroup [b]%11$s[/b](%12$s)? SpawnFlags %12$s:; %13$s', 'Cooldown: %s'], SmartAction::ACTION_RESPAWN_BY_SPAWNID => ['Respawn %11$s [small class=q0](GUID: %2$d)[/small]', ''], SmartAction::ACTION_INVOKER_CAST => ['Invoker casts [spell=%1$d] at #target#.(%4$d)? (max. %4$d |4target:targets;):;', '%1$s'], SmartAction::ACTION_PLAY_CINEMATIC => ['Play cinematic #[b]%1$d[/b] for #target#', ''], @@ -1070,6 +1098,7 @@ $lang = array( 'posts' => "论坛帖子:" ), 'emote' => array( + 'id' => "表情ID:", 'notFound' => "这个表情不存在。", // 'self' => "对你自己", // 'target' => "对别人并且选择了目标", @@ -1102,9 +1131,10 @@ $lang = array( 'state' => ['[Oneshot]', '[Continuous State]', '[Continuous Emote]'] ), 'enchantment' => array( + 'id' => "附魔ID:", + 'notFound' => "这个附魔不存在。", 'details' => "细节", 'activation' => "激活", - 'notFound' => "这个附魔不存在。", 'types' => array( 1 => "触发法术", 3 => "装备法术", 7 => "使用法术", 8 => "棱形插槽", 5 => "统计", 2 => "武器伤害", 6 => "DPS", 4 => "防御" @@ -1113,10 +1143,13 @@ $lang = array( 'areatrigger' => array( 'notFound' => "这个区域触发器不存在。", 'foundIn' => "这个区域触发器可以在以下地区找到:", + 'unnamed' => "[Unnamed areatrigger] #%d", 'types' => ['未使用', '酒馆', '传送门', '任务目标', 'Smart Trigger', '脚本'] ), 'gameObject' => array( + 'id' => "对象ID:", 'notFound' => "这个对象不存在。", + 'unnamed' => "[Unnamed object] #%d", 'cat' => [0 => "其他", 3 => "容器", 6 => "陷阱", 9 => "书籍", 25 => "钓鱼点", -5 => "宝箱", -3 => "草药", -4 => "矿脉", -2 => "任务", -6 => "工具"], 'type' => [ 3 => "容器", 6 => "", 9 => "书籍", 25 => "", -5 => "宝箱", -3 => "草药", -4 => "矿脉", -2 => "任务", -6 => ""], 'unkPosition' => "这个对象的位置未知。", @@ -1130,14 +1163,15 @@ $lang = array( 'foundIn' => "这个游戏对象可以在以下地区找到:", 'restock' => "每%s重新补给一次。", 'goFlags' => array( - GO_FLAG_IN_USE => '正在使用', - GO_FLAG_LOCKED => '已锁定', - GO_FLAG_INTERACT_COND => '无法交互', - GO_FLAG_TRANSPORT => '传送', - GO_FLAG_NOT_SELECTABLE => '不可选择', - GO_FLAG_TRIGGERED => '已触发', - GO_FLAG_DAMAGED => 'Siege damaged', - GO_FLAG_DESTROYED => 'Siege destroyed' + GO_FLAG_IN_USE => '正在使用', + GO_FLAG_LOCKED => '已锁定', + GO_FLAG_INTERACT_COND => '无法交互', + GO_FLAG_TRANSPORT => '传送', + GO_FLAG_NOT_SELECTABLE => '不可选择', + GO_FLAG_AI_OBSTACLE => '已触发', + GO_FLAG_FREEZE_ANIMATION => 'Freeze Animation', + GO_FLAG_DAMAGED => 'Siege damaged', + GO_FLAG_DESTROYED => 'Siege destroyed' ), 'actions' => array( "None", "Animate Custom 0", "Animate Custom 1", "Animate Custom 2", "Animate Custom 3", @@ -1148,6 +1182,7 @@ $lang = array( ) ), 'npc' => array( + 'id' => "NPC ID:", 'notFound' => "这个NPC不存在。", 'classification'=> "分类:%s", 'petFamily' => "宠物家族:", @@ -1180,10 +1215,6 @@ $lang = array( 'mechanicimmune'=> '[Not affected by mechanic]:%s', '_extraFlags' => '[Extra Flags]:', 'versions' => '[Difficulty Versions]:', - 'modes' => array( - 1 => ["普通", "英雄"], - 2 => ["10人普通", "25人普通", "10人英雄", "25人英雄"] - ), 'cat' => array( "未分类", '野兽', '龙类', '恶魔', '元素生物', '巨人', '亡灵', '人型生物', '小动物', '机械', '未指定', '图腾', '非战斗宠物', '气体云雾' @@ -1243,6 +1274,7 @@ $lang = array( ) ), 'event' => array( + 'id' => "世界事件ID:", 'notFound' => "这个世界事件不存在。", 'start' => "开始:", 'end' => "结束:", @@ -1251,6 +1283,7 @@ $lang = array( 'category' => ["未分类", "节日", "循环", "PvP"] ), 'achievement' => array( + 'id' => "成就ID:", 'notFound' => "这个成就不存在。", 'criteria' => "达成条件", 'points' => "点数", @@ -1309,9 +1342,11 @@ $lang = array( ) ), 'chrClass' => array( + 'id' => "职业ID:", 'notFound' => "这个职业不存在。" ), 'race' => array( + 'id' => "种族ID:", 'notFound' => "这个种族不存在。", 'racialLeader' => "种族领袖:", 'startZone' => "起始区域" @@ -1349,6 +1384,7 @@ $lang = array( ) ), 'zone' => array( + 'id' => "区域ID:", 'notFound' => "这个区域不存在。", 'attunement' => ["调整:", "英雄调整:"], 'key' => ["钥匙:", "英雄钥匙:"], @@ -1377,6 +1413,7 @@ $lang = array( ) ), 'quest' => array( + 'id' => "任务ID:", 'notFound' => "这个任务不存在。", '_transfer' => '这个任务将被转换到<a href="?quest=%d" class="q1">%s</a>,如果你转移到<span class="icon-%s">%s</span>。', 'questLevel' => "等级%s", @@ -1514,6 +1551,7 @@ $lang = array( 'notFound' => "这个图标不存在。" ), 'title' => array( + 'id' => "头衔ID:", 'notFound' => "这个头衔不存在。", '_transfer' => '这个头衔将被转换到<a href="?title=%d" class="q1">%s</a>,如果你转移到<span class="icon-%s">%s</span>。', 'cat' => array( @@ -1521,6 +1559,7 @@ $lang = array( ) ), 'skill' => array( + 'id' => "技能ID:", 'notFound' => "这个技能不存在。", 'cat' => array( -6 => "伙伴", -5 => "坐骑", -4 => "种族特性", 5 => "属性", 6 => "武器技能", 7 => "职业技能", 8 => "护甲精通", @@ -1528,6 +1567,7 @@ $lang = array( ) ), 'currency' => array( + 'id' => "货币ID:", 'notFound' => "这个货币不存在。", 'cap' => "总共上限:", 'cat' => array( @@ -1549,6 +1589,7 @@ $lang = array( ) ), 'mail' => array( + 'id' => "邮件ID:", 'notFound' => "该邮件不存在。", 'attachment' => "附件", 'mailDelivery' => '你将收到 <a href="?mail=%d">这封信</a>%s%s', // "你会收到这封信%s%s", @@ -1559,12 +1600,14 @@ $lang = array( 'untitled' => "无标题邮件 #%d" ), 'pet' => array( + 'id' => "猎人宠物ID:", 'notFound' => "这个宠物家族不存在。", 'exotic' => "异域的", 'cat' => ["狂野", "坚韧", "狡诈"], 'food' => ["肉", "鱼", "奶酪", "面包", "蘑菇", "水果", "生肉", "生鱼"] ), 'faction' => array( + 'id' => "阵营ID:", 'notFound' => "这个阵营不存在。", 'spillover' => "声望额外效果", 'spilloverDesc' => "获得这个阵营的声望也将按比例获得下列阵营的声望。", @@ -1580,12 +1623,13 @@ $lang = array( ) ), 'itemset' => array( + 'id' => "套装ID:", 'notFound' => "这个物品套装不存在。", '_desc' => "<b>%s</b>是<b>%s</b>。它包含%s件。", '_descTagless' => "<b>%s</b>是物品套装包含%s件。", '_setBonuses' => "套装奖励", '_conveyBonus' => "穿更多这个套装的部分将会提供给你角色奖励。", - '_pieces' => "件", + '_pieces' => "%d件:", '_unavailable' => "这个物品套装对玩家不可用。", '_tag' => "标签:", 'summary' => "摘要", @@ -1605,6 +1649,7 @@ $lang = array( ) ), 'spell' => array( + 'id' => "法术ID:", 'notFound' => "这个法术不存在。", '_spellDetails' => "法术细节", '_cost' => "花费", @@ -1631,14 +1676,13 @@ $lang = array( '_rankRange' => "排名: %d - %d", '_showXmore' => "[Show %d More]", - 'n_a' => "n/a", 'normal' => "普通", 'special' => "特殊", 'currentArea' => '<当前区域>', 'discovered' => "通过发现学习", - 'ppm' => "%s每分钟触发几率", - 'procChance' => "触发几率:", + 'ppm' => "%.1f每分钟触发几率", + 'procChance' => "触发几率:%.4g%%", 'starter' => "初始法术", 'trainingCost' => "训练成本:", 'channeled' => "需引导", @@ -1654,7 +1698,8 @@ $lang = array( 'pointsPerCP' => ",加%s每连击", 'stackGroup' => "Stack Group", 'linkedWith' => "Linked with", - '_scaling' => "缩放比例", + 'apMod' => "(攻强 mod:%.3g)", + 'spMod' => "(法力 mod:%.3g)", 'instantPhys' => "瞬发", 'castTime' => array( "瞬发法术", @@ -1701,10 +1746,6 @@ $lang = array( -1 => "弹药", -41 => "蓝铁", -61 => "蒸汽动力", -101 => "热能", -121 => "软泥", -141 => "鲜血能量", -142 => "愤怒" ), - 'scaling' => array( - 'directSP' => "直接效果的攻击强度 +%.2f%%", 'directAP' => "直接效果的攻击强度 +%.2f%%", - 'dotSP' => "每个周期的法术强度 +%.2f%%", 'dotAP' => "每个周期的攻击强度 +%.2f%%" - ), 'relItems' => array( 'base' => "<small>显示与<b>%s</b>相关的 %s</small>", 'link' => "或", @@ -1818,14 +1859,14 @@ $lang = array( /*102+ */ 'Dismiss Pet', 'Give Reputation', 'Summon Object (Trap)', 'Summon Object (Battle S.)','Summon Object (#3)', 'Summon Object (#4)', /*108+ */ 'Dispel Mechanic', 'Summon Dead Pet', 'Destroy All Totems', 'Durability Damage - Flat', 'Summon Demon', 'Resurrect with Flat Health', /*114+ */ 'Taunt', 'Durability Damage - %', 'Skin Player Corpse (PvP)', 'AoE Resurrect with % Health','Learn Skill', 'Apply Area Aura - Pet', -/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', null, 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', +/*120+ */ 'Teleport to Graveyard', 'Normalized Weapon Damage', '', 'Take Flight Path', 'Pull Towards', 'Modify Threat - %', /*126+ */ 'Spell Steal ', 'Prospect', 'Apply Area Aura - Friend', 'Apply Area Aura - Enemy', 'Redirect Done Threat %', 'Play Sound', /*132+ */ 'Play Music', 'Unlearn Specialization', 'Kill Credit 2', 'Call Pet', 'Heal for % of Total Health','Give % of Total Power', /*138+ */ 'Leap Back', 'Abandon Quest', 'Force Cast', 'Force Spell Cast with Value','Trigger Spell with Value','Apply Area Aura - Pet Owner', -/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', null, 'Charge to Dest', +/*144+ */ 'Knockback to Dest.', 'Pull Towards Dest.', 'Activate Rune', 'Fail Quest', 'Trigger Missile with Value','Charge to Dest', /*150+ */ 'Start Quest', 'Trigger Spell 2', 'Summon - Refer-A-Friend', 'Create Tamed Pet', 'Discover Flight Path', 'Dual Wield 2H Weapons', /*156+ */ 'Add Socket to Item', 'Create Tradeskill Item', 'Milling', 'Rename Pet', 'Force Cast 2', 'Change Talent Spec. Count', -/*162-167*/ 'Activate Talent Spec.', null, 'Remove Aura', null, null, 'Update Player Phase' +/*162-167*/ 'Activate Talent Spec.', '', 'Remove Aura' ), 'unkAura' => '未知光环 (%1$d)', 'auras' => array( @@ -1838,7 +1879,7 @@ $lang = array( 'Mod Skill - Temporary', 'Increase Run Speed %', 'Mod Mounted Speed %', 'Decrease Run Speed %', 'Mod Maximum Health - Flat', 'Mod Maximum Power - Flat', 'Shapeshift', 'Spell Effect Immunity', 'Spell Aura Immunity', 'Spell School Immunity', 'Damage Immunity', 'Dispel Type Immunity', 'Proc Trigger Spell', 'Proc Trigger Damage', 'Track Creatures', - 'Track Resources', 'Ignore All Gear', 'Mod Parry %', null, 'Mod Dodge %', + 'Track Resources', 'Ignore All Gear', 'Mod Parry %', 'Periodic Trigger Spell from Client', 'Mod Dodge %', /*50+ */ 'Mod Critical Healing Amount %', 'Mod Block %', 'Mod Physical Crit Chance', 'Periodically Drain Health', 'Mod Physical Hit Chance', 'Mod Spell Hit Chance', 'Transform', 'Mod Spell Crit Chance', 'Increase Swim Speed %', 'Mod Damage Done Versus Creature', 'Pacify & Silence', 'Mod Size %', 'Periodically Transfer Health', 'Periodic Transfer Power', 'Periodic Drain Power', @@ -1861,19 +1902,19 @@ $lang = array( 'Increase Pet Talent Points', 'Allow Exotic Pets Taming', 'Mechanic Immunity Mask', 'Retain Combo Points', 'Reduce Pushback Time %', /*150+ */ 'Mod Shield Block Value - %', 'Track Stealthed', 'Mod Player Aggro Range', 'Split Damage - Flat', 'Mod Stealth Level', 'Mod Underwater Breathing %', 'Mod All Reputation Gained by %', 'Done Pet Damage Multiplier', 'Mod Shield Block Value - Flat', 'No PvP Credit', - 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', null, + 'Mod AoE Avoidance', 'Mod Health Regen During Combat', 'Mana Burn', 'Mod Melee Critical Damage %', '', 'Mod Attacker Melee Attack Power', 'Mod Melee Attack Power - %', 'Mod Ranged Attack Power - %', 'Mod Damage Done vs Creature', 'Mod Crit Chance vs Creature', - 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', null, 'Mod Spell Power by % of Stat', + 'Change Object Visibility for Player', 'Mod Run Speed (not stacking)', 'Mod Mounted Speed (not stacking)', '', 'Mod Spell Power by % of Stat', /*175+ */ 'Mod Healing Power by % of Stat', 'Spirit of Redemption', 'AoE Charm', 'Mod Debuff Resistance - %', 'Mod Attacker Spell Crit Chance', - 'Mod Spell Power vs Creature', null, 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', + 'Mod Spell Power vs Creature', '', 'Mod Resistance by % of Stat', 'Mod Threat % of Critical Hits', 'Mod Attacker Melee Hit Chance', 'Mod Attacker Ranged Hit Chance', 'Mod Attacker Spell Hit Chance', 'Mod Attacker Melee Crit Chance', 'Mod Attacker Ranged Crit Chance', 'Mod Rating', 'Mod Reputation Gained %', 'Limit Movement Speed', 'Mod Attack Speed %', 'Mod Haste % (gain)', 'Mod Target School Absorb %', - 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', null, 'Mod Spell Hit Chance', + 'Mod Target School Absorb for Ability', 'Mod Cooldowns', 'Mod Attacker Crit Chance', '', 'Mod Spell Hit Chance', /*200+ */ 'Mod Kill Experience Gained %', 'Can Fly', 'Ignore Combat Result', 'Mod Attacker Melee Crit Damage %', 'Mod Attacker Ranged Crit Damage %', 'Mod Attacker Spell Crit Damage %', 'Mod Vehicle Flight Speed %', 'Mod Mounted Flight Speed %', 'Mod Flight Speed %', 'Mod Mounted Flight Speed % (always)', 'Mod Vehicle Speed % (always)', 'Mod Flight Speed % (not stacking)', 'Mod Ranged Attack Power by % of Stat', 'Mod Rage Generated from Damage Dealt', 'Tamed Pet Passive', 'Arena Preparation', 'Mod Spell Haste %', 'Killing Spree', 'Mod Ranged Haste %', 'Mod Mana Regeneration by % of Stat', - 'Mod Combat Rating by % of Stat', 'Ignore Threat', null, 'Raid Proc from Charge', null, + 'Mod Combat Rating by % of Stat', 'Ignore Threat', '', 'Raid Proc from Charge', '', /*225+ */ 'Raid Proc from Charge with Value', 'Periodic Dummy', 'Periodically Trigger Spell with Value','Detect Stealth', 'Mod AoE Damage Taken %', 'Mod Maximum Health - Flat (no stacking)','Proc Trigger Spell with Value', 'Mod Mechanic Duration %', 'Change other Humanoid Display', 'Mod Mechanic Duration % (not stacking)', 'Mod Dispel Resistance %', 'Control Vehicle', 'Mod Spell Power by % of Attack Power', 'Mod Healing Power by % of Attack Power','Mod Size % (not stacking)', @@ -1881,17 +1922,17 @@ $lang = array( 'Mod Aura Duration by Dispel Type', 'Mod Aura Duration by Dispel Type (not stacking)', 'Clone Caster', 'Mod Combat Result Chance', 'Convert Rune', /*250+ */ 'Mod Maximum Health - Flat (stacking)', 'Mod Enemy Dodge Chance', 'Mod Haste % (loss)', 'Mod Critical Block Chance', 'Disarm Offhand', 'Mod Mechanic Damage Taken %', 'No Reagent Cost', 'Mod Target Resistance by Spell Class', 'Mod Spell Visual', 'Mod Periodic Healing Taken %', - 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', null, - null, null, 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', + 'Screen Effect', 'Phase', 'Ability Ignore Aurastate', 'Allow Only Ability', '', + '', '', 'Cancel Aura Buffer at % of Caster Health','Mod Attack Power by % of Stat', 'Ignore Target Resistance', 'Ignore Target Resistance for Ability', 'Mod Damage Taken % from Caster', 'Ignore Swing Timer Reset', 'X-Ray', 'Ability Consume No Ammo', /*275+ */ 'Mod Ability Ignore Shapeshift', 'Mod Mechanic Damage Done %', 'Mod Max Affected Targets', 'Disarm Ranged Weapon', 'Spawn Effect', 'Mod Armor Penetration %', 'Mod Honor Gain %', 'Mod Base Health %', 'Mod Healing Taken % from Caster', 'Linked Aura', - 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', null, + 'Mod Attack Power by School Resistance','Allow Periodic Ability to Crit', 'Mod Spell Deflect Chance', 'Ignore Hit Direction', '', 'Mod Crit Chance', 'Mod Quest Experience Gained %', 'Open Stable', 'Override Spells', 'Prevent Power Regeneration', - null, 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', null, -/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', null, 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', - 'Mod Minimum Speed %', null, 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster',null, - 'Mod Pet AoE Damage Avoidance', null, null, null, 'Prevent Ressurection', + '', 'Set Vehicle Id', 'Spirit Burst', 'Strangulate', '', +/*300+ */ 'Share Damage %', 'Mod Absorb School Healing', '', 'Mod Damage Done vs Aurastate - %', 'Fake Inebriate', + 'Mod Minimum Speed %', '', 'Heal Absorb Test', 'Mod Critical Strike Chance for Caster','', + 'Mod Pet AoE Damage Avoidance', '', '', '', 'Prevent Ressurection', /* -316*/ 'Underwater Walking', 'Periodic Haste' ), 'attributes0' => array( @@ -2168,6 +2209,7 @@ $lang = array( ) ), 'item' => array( + 'id' => "物品ID:", 'notFound' => "这个物品不存在。", 'armor' => "%d点护甲", 'block' => "%d格挡", @@ -2226,8 +2268,10 @@ $lang = array( 'uniqueEquipped'=> ["装备唯一", null, "装备唯一:%s (%d)"], 'speed' => "速度", 'dps' => "(每秒伤害%.1f)", - 'vendorIn' => "[Vendor Locations]", + 'vendorLoc' => "[Vendor Locations]", 'purchasedIn' => "[This item can be purchased in]", + 'fishingLoc' => "钓鱼地点", + 'fishedIn' => "此物品位于", 'duration' => array( '', "持续时间:%d秒", diff --git a/prQueue b/prQueue index 4e7f388f..20b12827 100755 --- a/prQueue +++ b/prQueue @@ -29,7 +29,7 @@ CLI::write('profiler queue started', CLI::LOG_OK); set_time_limit(0); $tCycle = microtime(true); -$error = function (int $type, int $realmGUID, int $realmId) : void +$error = function (int $type, int $realmGUID, int $realmId, int $fetchResult) : void { $what = match ($type) { @@ -38,8 +38,17 @@ $error = function (int $type, int $realmGUID, int $realmId) : void Type::ARENA_TEAM => 'arena team' }; - DB::Aowow()->query('UPDATE ?_profiler_sync SET `status` = ?d, `errorCode` = ?d WHERE `realm` = ?d AND `realmGUID` = ?d AND `type` = ?d', PR_QUEUE_STATUS_ERROR, PR_QUEUE_ERROR_CHAR, $realmId, $realmGUID, $type); - trigger_error('prQueue - '.$what.' #'.$realmGUID.' on realm #'.$realmId.' not found. Truncating local placeholder.', E_USER_WARNING); + $msg = match ($fetchResult) + { + Profiler::FETCH_RESULT_ERR_NAME_EMPTY => 'Subject has an empty name and was skipped.', + Profiler::FETCH_RESULT_ERR_NOT_FOUND => 'Subject was not found. Truncating local placeholder.', + Profiler::FETCH_RESULT_ERR_NO_MEMBERS => 'Subject has no members. Truncating local placeholder.', + Profiler::FETCH_RESULT_ERR_INTERNAL => 'Internal Error - Data stub is missing.' + }; + + trigger_error('prQueue - [realm: '.$realmId.' '.$what.' guid: '.$realmGUID.'] '.$msg, E_USER_WARNING); + + DB::Aowow()->qry('UPDATE ::profiler_sync SET `status` = %i, `errorCode` = %i WHERE `realm` = %i AND `realmGUID` = %i AND `type` = %i', PR_QUEUE_STATUS_ERROR, PR_QUEUE_ERROR_CHAR, $realmId, $realmGUID, $type); }; @@ -53,7 +62,7 @@ while (Cfg::get('PROFILER_ENABLE', true)) usleep($wait * 1000 * 1000); } - $row = DB::Aowow()->selectRow('SELECT * FROM ?_profiler_sync WHERE `status` = ?d ORDER BY `requestTime` ASC', PR_QUEUE_STATUS_WAITING); + $row = DB::Aowow()->selectRow('SELECT * FROM ::profiler_sync WHERE `status` = %i ORDER BY `requestTime` ASC', PR_QUEUE_STATUS_WAITING); if (!$row) { // nothing more to do @@ -67,51 +76,64 @@ while (Cfg::get('PROFILER_ENABLE', true)) if (empty(Profiler::getRealms()[$row['realm']])) { - DB::Aowow()->query('UPDATE ?_profiler_sync SET `status` = ?d, `errorCode` = ?d WHERE `realm` = ?d AND `type` = ?d AND `typeId` = ?d', PR_QUEUE_STATUS_ERROR, PR_QUEUE_ERROR_ARMORY, $row['realm'], $row['type'], $row['typeId']); - CLI::write('realm #'.$row['realm'].' for subject guid '.$row['realmGUID'].' is undefined', CLI::LOG_WARN); + DB::Aowow()->qry('UPDATE ::profiler_sync SET `status` = %i, `errorCode` = %i WHERE `realm` = %i AND `type` = %i AND `typeId` = %i', PR_QUEUE_STATUS_ERROR, PR_QUEUE_ERROR_ARMORY, $row['realm'], $row['type'], $row['typeId']); + CLI::write('realm #'.$row['realm'].' for subject guid '.$row['realmGUID'].' is missing/inaccessible.', CLI::LOG_WARN); continue; } else - DB::Aowow()->query('UPDATE ?_profiler_sync SET `status` = ?d WHERE `realm` = ?d AND `type` = ?d AND `typeId` = ?d', PR_QUEUE_STATUS_WORKING, $row['realm'], $row['type'], $row['typeId']); + DB::Aowow()->qry('UPDATE ::profiler_sync SET `status` = %i WHERE `realm` = %i AND `type` = %i AND `typeId` = %i', PR_QUEUE_STATUS_WORKING, $row['realm'], $row['type'], $row['typeId']); switch ($row['type']) { case Type::PROFILE: - if (!Profiler::getCharFromRealm($row['realm'], $row['realmGUID'])) + switch ($result = Profiler::getCharFromRealm($row['realm'], $row['realmGUID'])) { - $error(Type::PROFILE, $row['realmGUID'], $row['realm']); - DB::Aowow()->query('DELETE FROM ?_profiler_profiles WHERE `realm` = ?d AND `realmGUID` = ?d', $row['realm'], $row['realmGUID']); - continue 2; + case Profiler::FETCH_RESULT_OK_UNCHANGED: + CLI::write('char #'.$row['realmGUID'].' on realm #'.$row['realm'].' did not log in since last update. skipping...'); + case Profiler::FETCH_RESULT_OK: + break 2; + case Profiler::FETCH_RESULT_ERR_NAME_EMPTY: + case Profiler::FETCH_RESULT_ERR_NOT_FOUND: + DB::Aowow()->qry('DELETE FROM ::profiler_profiles WHERE `realm` = %i AND `realmGUID` = %i', $row['realm'], $row['realmGUID']); + default: + $error(Type::PROFILE, $row['realmGUID'], $row['realm'], $result); + continue 3; } - - break; case Type::GUILD: - if (!Profiler::getGuildFromRealm($row['realm'], $row['realmGUID'])) + switch ($result = Profiler::getGuildFromRealm($row['realm'], $row['realmGUID'])) { - $error(Type::GUILD, $row['realmGUID'], $row['realm']); - DB::Aowow()->query('DELETE FROM ?_profiler_guild WHERE `realm` = ?d AND `realmGUID` = ?d', $row['realm'], $row['realmGUID']); - continue 2; + case Profiler::FETCH_RESULT_OK: + break 2; + case Profiler::FETCH_RESULT_ERR_NAME_EMPTY: + case Profiler::FETCH_RESULT_ERR_NOT_FOUND: + case Profiler::FETCH_RESULT_ERR_NO_MEMBERS: + DB::Aowow()->qry('DELETE FROM ::profiler_guild WHERE `realm` = %i AND `realmGUID` = %i', $row['realm'], $row['realmGUID']); + default: + $error(Type::GUILD, $row['realmGUID'], $row['realm'], $result); + continue 3; } - - break; case Type::ARENA_TEAM: - if (!Profiler::getArenaTeamFromRealm($row['realm'], $row['realmGUID'])) + switch ($result = Profiler::getArenaTeamFromRealm($row['realm'], $row['realmGUID'])) { - $error(Type::ARENA_TEAM, $row['realmGUID'], $row['realm']); - DB::Aowow()->query('DELETE FROM ?_profiler_arena_team WHERE `realm` = ?d AND `realmGUID` = ?d', $row['realm'], $row['realmGUID']); - continue 2; + case Profiler::FETCH_RESULT_OK: + break 2; + case Profiler::FETCH_RESULT_ERR_NAME_EMPTY: + case Profiler::FETCH_RESULT_ERR_NOT_FOUND: + case Profiler::FETCH_RESULT_ERR_NO_MEMBERS: + DB::Aowow()->qry('DELETE FROM ::profiler_arena_team WHERE `realm` = %i AND `realmGUID` = %i', $row['realm'], $row['realmGUID']); + default: + $error(Type::ARENA_TEAM, $row['realmGUID'], $row['realm'], $result); + continue 3; } - - break; default: - DB::Aowow()->query('DELETE FROM ?_profiler_sync WHERE realm = ?d AND type = ?d AND typeId = ?d', $row['realm'], $row['type'], $row['typeId']); + DB::Aowow()->qry('DELETE FROM ::profiler_sync WHERE realm = %i AND type = %i AND typeId = %i', $row['realm'], $row['type'], $row['typeId']); trigger_error('prQueue - unknown type #'.$row['type'].' to sync into profiler. Removing from queue...', E_USER_ERROR); } $tCycle = microtime(true); // mark as ready - DB::Aowow()->query('UPDATE ?_profiler_sync SET `status` = ?d, `errorCode` = 0 WHERE `realm` = ?d AND `type` = ?d AND `typeId` = ?d', PR_QUEUE_STATUS_READY, $row['realm'], $row['type'], $row['typeId']); + DB::Aowow()->qry('UPDATE ::profiler_sync SET `status` = %i, `errorCode` = 0 WHERE `realm` = %i AND `type` = %i AND `typeId` = %i', PR_QUEUE_STATUS_READY, $row['realm'], $row['type'], $row['typeId']); } Profiler::queueFree(); diff --git a/setup/setup.php b/setup/setup.php index 8d59647b..029eb864 100644 --- a/setup/setup.php +++ b/setup/setup.php @@ -12,7 +12,7 @@ if (!CLI) require_once 'setup/tools/setupScript.class.php'; require_once 'setup/tools/utilityScript.class.php'; require_once 'setup/tools/CLISetup.class.php'; -require_once 'setup/tools/dbc.class.php'; +require_once 'setup/tools/dbcreader.class.php'; require_once 'setup/tools/imagecreatefromblp.func.php'; CLISetup::init(); diff --git a/setup/sql/01-db_structure.sql b/setup/sql/01-db_structure.sql index d3709dca..86c69286 100644 --- a/setup/sql/01-db_structure.sql +++ b/setup/sql/01-db_structure.sql @@ -196,7 +196,7 @@ DROP TABLE IF EXISTS `aowow_account_reputation`; CREATE TABLE `aowow_account_reputation` ( `userId` int(10) unsigned NOT NULL, `action` tinyint(3) unsigned NOT NULL COMMENT 'e.g. upvote a comment', - `amount` tinyint(3) unsigned NOT NULL, + `amount` tinyint(3) NOT NULL, `sourceA` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'e.g. upvoting user', `sourceB` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'e.g. upvoted commentId', `date` int(10) unsigned NOT NULL DEFAULT 0, @@ -404,6 +404,7 @@ DROP TABLE IF EXISTS `aowow_classes`; CREATE TABLE `aowow_classes` ( `id` int(11) NOT NULL, `fileString` varchar(128) DEFAULT NULL, + `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, `name_loc0` varchar(128) DEFAULT NULL, `name_loc2` varchar(128) DEFAULT NULL, `name_loc3` varchar(128) DEFAULT NULL, @@ -550,7 +551,6 @@ CREATE TABLE `aowow_creature` ( `vehicleId` mediumint(8) unsigned NOT NULL DEFAULT 0, `minGold` mediumint(8) unsigned NOT NULL DEFAULT 0, `maxGold` mediumint(8) unsigned NOT NULL DEFAULT 0, - `aiName` varchar(50) NOT NULL DEFAULT '', `healthMin` int(10) unsigned NOT NULL DEFAULT 1, `healthMax` int(10) unsigned NOT NULL DEFAULT 1, `manaMin` int(10) unsigned NOT NULL DEFAULT 1, @@ -565,16 +565,51 @@ CREATE TABLE `aowow_creature` ( `resistance6` smallint(6) NOT NULL DEFAULT 0, `racialLeader` tinyint(3) unsigned NOT NULL DEFAULT 0, `mechanicImmuneMask` int(10) unsigned NOT NULL DEFAULT 0, + `schoolImmuneMask` int(10) unsigned NOT NULL DEFAULT 0, `flagsExtra` int(10) unsigned NOT NULL DEFAULT 0, - `scriptName` varchar(50) NOT NULL DEFAULT '', + `ScriptOrAI` varchar(64) DEFAULT NULL, + `StringId` varchar(64) DEFAULT NULL, PRIMARY KEY (`id`), - KEY `idx_name` (`name_loc0`), KEY `difficultyEntry1` (`difficultyEntry1`), KEY `difficultyEntry2` (`difficultyEntry2`), KEY `difficultyEntry3` (`difficultyEntry3`), KEY `idx_loot` (`lootId`), KEY `idx_pickpocketloot` (`pickpocketLootId`), - KEY `idx_skinloot` (`skinLootId`) + KEY `idx_skinloot` (`skinLootId`), + KEY `idx_trainer` (`trainerType`), + KEY `idx_trainerrequirement` (`trainerRequirement`), + KEY `idx_name0` (`name_loc0`), + KEY `idx_name2` (`name_loc2`), + KEY `idx_name3` (`name_loc3`), + KEY `idx_name4` (`name_loc4`), + KEY `idx_name6` (`name_loc6`), + KEY `idx_name8` (`name_loc8`), + KEY `idx_spell1` (`spell1`), + KEY `idx_spell2` (`spell2`), + KEY `idx_spell3` (`spell3`), + KEY `idx_spell4` (`spell4`), + KEY `idx_spell5` (`spell5`), + KEY `idx_spell6` (`spell6`), + KEY `idx_spell7` (`spell7`), + KEY `idx_spell8` (`spell8`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_creature_search` +-- + +DROP TABLE IF EXISTS `aowow_creature_search`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_creature_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(100) DEFAULT NULL, + `nSubname` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`,`locale`), + FULLTEXT KEY `idx_ft_na` (`nName`), + FULLTEXT KEY `idx_ft_na_ex` (`nName`,`nSubname`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -863,8 +898,8 @@ CREATE TABLE `aowow_factions` ( `baseRepClassMask4` mediumint(8) unsigned NOT NULL, `baseRepValue1` mediumint(9) NOT NULL, `baseRepValue2` mediumint(9) NOT NULL, - `baseRepValue4` mediumint(9) NOT NULL, `baseRepValue3` mediumint(9) NOT NULL, + `baseRepValue4` mediumint(9) NOT NULL, `side` tinyint(3) unsigned NOT NULL, `expansion` tinyint(3) unsigned NOT NULL, `qmNpcIds` varchar(12) NOT NULL COMMENT 'space separated', @@ -993,7 +1028,7 @@ CREATE TABLE `aowow_holidays` ( `looping` tinyint(4) NOT NULL, `scheduleType` tinyint(4) NOT NULL, `textureString` varchar(30) NOT NULL DEFAULT '', - `iconString` varchar(51) NOT NULL DEFAULT '0', + `iconId` smallint(5) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1106,8 +1141,10 @@ CREATE TABLE `aowow_icons` ( `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT, `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', `name` varchar(55) NOT NULL DEFAULT '', + `name_source` varchar(55) NOT NULL DEFAULT '', PRIMARY KEY (`id`), - KEY `name` (`name`) + KEY `name` (`name`), + KEY `idx_sourcename` (`name_source`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1303,8 +1340,8 @@ CREATE TABLE `aowow_items` ( `repairPrice` int(10) unsigned NOT NULL, `slot` tinyint(4) NOT NULL, `slotBak` tinyint(3) unsigned NOT NULL DEFAULT 0, - `requiredClass` int(11) NOT NULL DEFAULT -1, - `requiredRace` int(11) NOT NULL DEFAULT -1, + `requiredClass` smallint(5) unsigned NOT NULL DEFAULT 0, + `requiredRace` smallint(5) unsigned NOT NULL DEFAULT 0, `itemLevel` smallint(5) unsigned NOT NULL DEFAULT 0, `requiredLevel` tinyint(3) unsigned NOT NULL DEFAULT 0, `requiredSkill` smallint(5) unsigned NOT NULL DEFAULT 0, @@ -1437,11 +1474,48 @@ CREATE TABLE `aowow_items` ( `unsheatheSoundId` smallint(5) unsigned NOT NULL DEFAULT 0, `flagsCustom` int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`id`), - KEY `idx_name` (`name_loc0`), KEY `items_index` (`class`), KEY `idx_model` (`displayId`), KEY `idx_faction` (`requiredFaction`), - KEY `iconId` (`iconId`) + KEY `iconId` (`iconId`), + KEY `idx_spell1` (`spellId1`), + KEY `idx_spell2` (`spellId2`), + KEY `idx_spell3` (`spellId3`), + KEY `idx_spell4` (`spellId4`), + KEY `idx_spell5` (`spellId5`), + KEY `idx_trigger1` (`spellTrigger1`), + KEY `idx_trigger2` (`spellTrigger2`), + KEY `idx_trigger3` (`spellTrigger3`), + KEY `idx_trigger4` (`spellTrigger4`), + KEY `idx_trigger5` (`spellTrigger5`), + KEY `idx_reqskill` (`requiredSkill`), + KEY `idx_name0` (`name_loc0`), + KEY `idx_name2` (`name_loc2`), + KEY `idx_name3` (`name_loc3`), + KEY `idx_name4` (`name_loc4`), + KEY `idx_name6` (`name_loc6`), + KEY `idx_name8` (`name_loc8`), + KEY `idx_itemset` (`itemset`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_items_search` +-- + +DROP TABLE IF EXISTS `aowow_items_search`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_items_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(127) DEFAULT NULL, + `nDescription` varchar(255) DEFAULT NULL, + `nEffects` text DEFAULT NULL, + PRIMARY KEY (`id`,`locale`), + FULLTEXT KEY `idx_ft_na` (`nName`), + FULLTEXT KEY `idx_ft_description` (`nDescription`), + FULLTEXT KEY `idx_ft_effects` (`nEffects`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1569,6 +1643,26 @@ CREATE TABLE `aowow_mails` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `aowow_objectdifficulty` +-- + +DROP TABLE IF EXISTS `aowow_objectdifficulty`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_objectdifficulty` ( + `normal10` mediumint(8) unsigned NOT NULL, + `normal25` mediumint(8) unsigned NOT NULL, + `heroic10` mediumint(8) unsigned NOT NULL, + `heroic25` mediumint(8) unsigned NOT NULL, + `mapType` tinyint(3) unsigned NOT NULL, + KEY `normal10` (`normal10`), + KEY `normal25` (`normal25`), + KEY `heroic10` (`heroic10`), + KEY `heroic25` (`heroic25`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `aowow_objects` -- @@ -1603,9 +1697,35 @@ CREATE TABLE `aowow_objects` ( `auraSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, `triggeredSpell` mediumint(8) unsigned NOT NULL DEFAULT 0, `miscInfo` varchar(128) NOT NULL, - `ScriptOrAI` varchar(64) NOT NULL, + `ScriptOrAI` varchar(64) DEFAULT NULL, + `StringId` varchar(64) DEFAULT NULL, PRIMARY KEY (`id`), - KEY `idx_name` (`name_loc0`) + KEY `idx_onusespell` (`onUseSpell`), + KEY `idx_onsuccessspell` (`onSuccessSpell`), + KEY `idx_auraspell` (`auraSpell`), + KEY `idx_triggeredspell` (`triggeredSpell`), + KEY `idx_name0` (`name_loc0`), + KEY `idx_name2` (`name_loc2`), + KEY `idx_name3` (`name_loc3`), + KEY `idx_name4` (`name_loc4`), + KEY `idx_name6` (`name_loc6`), + KEY `idx_name8` (`name_loc8`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_objects_search` +-- + +DROP TABLE IF EXISTS `aowow_objects_search`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_objects_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(127) DEFAULT NULL, + PRIMARY KEY (`id`,`locale`), + FULLTEXT KEY `idx_ft_na` (`nName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1661,6 +1781,7 @@ CREATE TABLE `aowow_profiler_arena_team` ( `nameUrl` varchar(24) NOT NULL, `type` tinyint(3) unsigned NOT NULL DEFAULT 0, `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `stub` tinyint(1) DEFAULT 0 COMMENT 'arena team stub needs resync', `rating` smallint(5) unsigned NOT NULL DEFAULT 0, `seasonGames` smallint(5) unsigned NOT NULL DEFAULT 0, `seasonWins` smallint(5) unsigned NOT NULL DEFAULT 0, @@ -1674,7 +1795,10 @@ CREATE TABLE `aowow_profiler_arena_team` ( `borderColor` int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `realm_realmGUID` (`realm`,`realmGUID`), - KEY `name` (`name`) + KEY `name` (`name`), + KEY `idx_stub` (`stub`), + KEY `idx_type` (`type`), + KEY `idx_rating` (`rating`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1729,6 +1853,7 @@ CREATE TABLE `aowow_profiler_completion_quests` ( `id` int(10) unsigned NOT NULL, `questId` mediumint(8) unsigned NOT NULL, KEY `id` (`id`), + KEY `typeId` (`questId`), CONSTRAINT `FK_pr_completion_quests` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1744,7 +1869,10 @@ CREATE TABLE `aowow_profiler_completion_reputation` ( `id` int(10) unsigned NOT NULL, `factionId` smallint(5) unsigned NOT NULL, `standing` mediumint(9) DEFAULT NULL, + `exalted` tinyint(1) GENERATED ALWAYS AS (`standing` >= 42000) STORED, KEY `id` (`id`), + KEY `typeId` (`factionId`), + KEY `idx_exalted` (`exalted`), CONSTRAINT `FK_pr_completion_reputation` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1763,6 +1891,7 @@ CREATE TABLE `aowow_profiler_completion_skills` ( `max` smallint(5) unsigned DEFAULT NULL, KEY `id` (`id`), KEY `typeId` (`skillId`), + KEY `idx_value` (`value`), CONSTRAINT `FK_pr_completion_skills` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1778,6 +1907,7 @@ CREATE TABLE `aowow_profiler_completion_spells` ( `id` int(10) unsigned NOT NULL, `spellId` mediumint(8) unsigned NOT NULL, KEY `id` (`id`), + KEY `typeId` (`spellId`), CONSTRAINT `FK_pr_completion_spells` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1811,6 +1941,7 @@ CREATE TABLE `aowow_profiler_completion_titles` ( `id` int(10) unsigned NOT NULL, `titleId` tinyint(3) unsigned NOT NULL, KEY `id` (`id`), + KEY `typeId` (`titleId`), CONSTRAINT `FK_pr_completion_titles` FOREIGN KEY (`id`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1843,6 +1974,7 @@ CREATE TABLE `aowow_profiler_guild` ( `realm` int(10) unsigned NOT NULL, `realmGUID` int(10) unsigned NOT NULL, `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `stub` tinyint(1) DEFAULT 0 COMMENT 'guild stub needs resync', `name` varchar(26) NOT NULL, `nameUrl` varchar(26) NOT NULL, `emblemStyle` tinyint(3) unsigned NOT NULL DEFAULT 0, @@ -1854,7 +1986,8 @@ CREATE TABLE `aowow_profiler_guild` ( `createDate` int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `realm_realmGUID` (`realm`,`realmGUID`), - KEY `name` (`name`) + KEY `name` (`name`), + KEY `idx_stub` (`stub`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1915,7 +2048,7 @@ CREATE TABLE `aowow_profiler_pets` ( `family` tinyint(3) unsigned DEFAULT NULL, `npc` smallint(5) unsigned DEFAULT NULL, `displayId` smallint(5) unsigned DEFAULT NULL, - `talents` varchar(20) DEFAULT NULL, + `talents` varchar(22) DEFAULT NULL, PRIMARY KEY (`id`), KEY `owner` (`owner`), CONSTRAINT `FK_pr_pets` FOREIGN KEY (`owner`) REFERENCES `aowow_profiler_profiles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE @@ -1934,6 +2067,9 @@ CREATE TABLE `aowow_profiler_profiles` ( `realm` tinyint(3) unsigned DEFAULT NULL, `realmGUID` int(10) unsigned DEFAULT NULL, `cuFlags` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'see defines.php for flags', + `custom` tinyint(1) DEFAULT 0 COMMENT 'custom profile', + `stub` tinyint(1) DEFAULT 0 COMMENT 'profile stub needs resync', + `deleted` tinyint(1) DEFAULT 0 COMMENT 'only on custom profiles', `sourceId` int(10) unsigned DEFAULT NULL, `sourceName` varchar(50) DEFAULT NULL, `copy` int(10) unsigned DEFAULT NULL, @@ -1972,6 +2108,18 @@ CREATE TABLE `aowow_profiler_profiles` ( KEY `user` (`user`), KEY `guild` (`guild`), KEY `name` (`name`), + KEY `idx_custom` (`custom`), + KEY `idx_stub` (`stub`), + KEY `idx_deleted` (`deleted`), + KEY `idx_race` (`race`), + KEY `idx_class` (`class`), + KEY `idx_level` (`level`), + KEY `idx_guildrank` (`guildrank`), + KEY `idx_gearscore` (`gearscore`), + KEY `idx_achievementpoints` (`achievementpoints`), + KEY `idx_talenttree1` (`talenttree1`), + KEY `idx_talenttree2` (`talenttree2`), + KEY `idx_talenttree3` (`talenttree3`), CONSTRAINT `FK_aowow_profiler_profiles_aowow_profiler_guild` FOREIGN KEY (`guild`) REFERENCES `aowow_profiler_guild` (`id`) ON DELETE SET NULL ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2005,13 +2153,13 @@ DROP TABLE IF EXISTS `aowow_quests`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `aowow_quests` ( `id` mediumint(8) unsigned NOT NULL DEFAULT 0, - `method` tinyint(3) unsigned NOT NULL DEFAULT 2, + `questType` tinyint(3) unsigned NOT NULL DEFAULT 2, `level` smallint(6) NOT NULL DEFAULT 1, `minLevel` tinyint(3) unsigned NOT NULL DEFAULT 0, `maxLevel` tinyint(3) unsigned NOT NULL DEFAULT 0, - `zoneOrSort` smallint(6) NOT NULL DEFAULT 0, - `zoneOrSortBak` smallint(6) NOT NULL DEFAULT 0, - `type` smallint(5) unsigned NOT NULL DEFAULT 0, + `questSortId` smallint(6) NOT NULL DEFAULT 0, + `questSortIdBak` smallint(6) NOT NULL DEFAULT 0, + `questInfoId` smallint(5) unsigned NOT NULL DEFAULT 0, `suggestedPlayers` tinyint(3) unsigned NOT NULL DEFAULT 0, `timeLimit` int(10) unsigned NOT NULL DEFAULT 0, `eventId` smallint(5) unsigned NOT NULL DEFAULT 0, @@ -2080,12 +2228,12 @@ CREATE TABLE `aowow_quests` ( `rewardFactionValue3` mediumint(9) NOT NULL DEFAULT 0, `rewardFactionValue4` mediumint(9) NOT NULL DEFAULT 0, `rewardFactionValue5` mediumint(9) NOT NULL DEFAULT 0, - `name_loc0` text DEFAULT NULL, - `name_loc2` text DEFAULT NULL, - `name_loc3` text DEFAULT NULL, - `name_loc4` text DEFAULT NULL, - `name_loc6` text DEFAULT NULL, - `name_loc8` text DEFAULT NULL, + `name_loc0` varchar(100) DEFAULT NULL, + `name_loc2` varchar(100) DEFAULT NULL, + `name_loc3` varchar(100) DEFAULT NULL, + `name_loc4` varchar(100) DEFAULT NULL, + `name_loc6` varchar(100) DEFAULT NULL, + `name_loc8` varchar(100) DEFAULT NULL, `objectives_loc0` text DEFAULT NULL, `objectives_loc2` text DEFAULT NULL, `objectives_loc3` text DEFAULT NULL, @@ -2175,7 +2323,58 @@ CREATE TABLE `aowow_quests` ( `objectiveText4_loc6` text DEFAULT NULL, `objectiveText4_loc8` text DEFAULT NULL, PRIMARY KEY (`id`), - KEY `nextQuestIdChain` (`nextQuestIdChain`) + KEY `nextQuestIdChain` (`nextQuestIdChain`), + KEY `idx_name0` (`name_loc0`), + KEY `idx_name2` (`name_loc2`), + KEY `idx_name3` (`name_loc3`), + KEY `idx_name4` (`name_loc4`), + KEY `idx_name6` (`name_loc6`), + KEY `idx_name8` (`name_loc8`), + KEY `idx_sourcespell` (`sourceSpellId`), + KEY `idx_rewardspell` (`rewardSpell`), + KEY `idx_rewardcastspell` (`rewardSpellCast`), + KEY `idx_classmask` (`reqRaceMask`), + KEY `idx_racemask` (`reqClassMask`), + KEY `idx_questsort` (`questSortId`), + KEY `idx_rewarditem1` (`rewardChoiceItemId1`), + KEY `idx_rewarditem2` (`rewardChoiceItemId2`), + KEY `idx_rewarditem3` (`rewardChoiceItemId3`), + KEY `idx_rewarditem4` (`rewardChoiceItemId4`), + KEY `idx_rewarditem5` (`rewardChoiceItemId5`), + KEY `idx_rewarditem6` (`rewardChoiceItemId6`), + KEY `idx_rewardfaction1` (`rewardFactionId1`), + KEY `idx_rewardfaction2` (`rewardFactionId2`), + KEY `idx_rewardfaction3` (`rewardFactionId3`), + KEY `idx_rewardfaction4` (`rewardFactionId4`), + KEY `idx_rewardfaction5` (`rewardFactionId5`), + KEY `idx_choiceitem1` (`rewardItemId1`), + KEY `idx_choiceitem2` (`rewardItemId2`), + KEY `idx_choiceitem3` (`rewardItemId3`), + KEY `idx_choiceitem4` (`rewardItemId4`), + KEY `idx_requirement1` (`reqNpcOrGo1`), + KEY `idx_requirement2` (`reqNpcOrGo2`), + KEY `idx_requirement3` (`reqNpcOrGo3`), + KEY `idx_requirement4` (`reqNpcOrGo4`), + KEY `idx_event` (`eventId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_quests_search` +-- + +DROP TABLE IF EXISTS `aowow_quests_search`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_quests_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(100) DEFAULT NULL, + `nObjectives` text DEFAULT NULL, + `nDetails` text DEFAULT NULL, + PRIMARY KEY (`id`,`locale`), + FULLTEXT KEY `idx_ft_na` (`nName`), + FULLTEXT KEY `idx_ft_na_ex` (`nName`,`nObjectives`,`nDetails`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2230,6 +2429,8 @@ CREATE TABLE `aowow_races` ( `baseLanguage` tinyint(3) unsigned NOT NULL, `side` tinyint(3) unsigned NOT NULL, `fileString` varchar(64) DEFAULT NULL, + `iconId0` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'male icon', + `iconId1` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT 'female icon', `name_loc0` varchar(64) DEFAULT NULL, `name_loc2` varchar(64) DEFAULT NULL, `name_loc3` varchar(64) DEFAULT NULL, @@ -2512,6 +2713,8 @@ CREATE TABLE `aowow_spawns` ( `posX` float unsigned NOT NULL, `posY` float unsigned NOT NULL, `pathId` int(10) unsigned NOT NULL DEFAULT 0, + `ScriptName` varchar(64) DEFAULT NULL, + `StringId` varchar(64) DEFAULT NULL, PRIMARY KEY (`guid`,`type`,`floor`), KEY `type_idx` (`typeId`,`type`), KEY `zone_idx` (`areaId`), @@ -2729,9 +2932,58 @@ CREATE TABLE `aowow_spell` ( PRIMARY KEY (`id`), KEY `category` (`typeCat`), KEY `spell` (`id`) USING BTREE, - KEY `effects` (`effect1Id`,`effect2Id`,`effect3Id`), - KEY `items` (`effect1CreateItemId`,`effect2CreateItemId`,`effect3CreateItemId`), - KEY `iconId` (`iconId`) + KEY `iconId` (`iconId`), + KEY `reagent1` (`reagent1`), + KEY `reagent2` (`reagent2`), + KEY `reagent3` (`reagent3`), + KEY `reagent4` (`reagent4`), + KEY `reagent5` (`reagent5`), + KEY `reagent6` (`reagent6`), + KEY `reagent7` (`reagent7`), + KEY `reagent8` (`reagent8`), + KEY `effect1CreateItemId` (`effect1CreateItemId`), + KEY `effect2CreateItemId` (`effect2CreateItemId`), + KEY `effect3CreateItemId` (`effect3CreateItemId`), + KEY `effect1Id` (`effect1Id`), + KEY `effect2Id` (`effect2Id`), + KEY `effect3Id` (`effect3Id`), + KEY `effect1AuraId` (`effect1AuraId`), + KEY `effect2AuraId` (`effect2AuraId`), + KEY `effect3AuraId` (`effect3AuraId`), + KEY `idx_skill1` (`skillLine1`), + KEY `idx_skill2` (`skillLine2OrMask`), + KEY `idx_name0` (`name_loc0`), + KEY `idx_name2` (`name_loc2`), + KEY `idx_name3` (`name_loc3`), + KEY `idx_name4` (`name_loc4`), + KEY `idx_name6` (`name_loc6`), + KEY `idx_name8` (`name_loc8`), + KEY `idx_spellfamily` (`spellFamilyId`), + KEY `idx_miscvalue1` (`effect1MiscValue`), + KEY `idx_miscvalue2` (`effect2MiscValue`), + KEY `idx_miscvalue3` (`effect3MiscValue`), + KEY `idx_triggerspell1` (`effect1TriggerSpell`), + KEY `idx_triggerspell2` (`effect2TriggerSpell`), + KEY `idx_triggerspell3` (`effect3TriggerSpell`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `aowow_spell_search` +-- + +DROP TABLE IF EXISTS `aowow_spell_search`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `aowow_spell_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(185) DEFAULT NULL, + `nDescription` text DEFAULT NULL, + `nBuff` text DEFAULT NULL, + PRIMARY KEY (`id`,`locale`), + FULLTEXT KEY `idx_ft_na` (`nName`), + FULLTEXT KEY `idx_ft_na_ex` (`nName`,`nDescription`,`nBuff`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2778,6 +3030,7 @@ CREATE TABLE `aowow_spelldifficulty` ( `normal25` mediumint(8) unsigned NOT NULL, `heroic10` mediumint(8) unsigned NOT NULL, `heroic25` mediumint(8) unsigned NOT NULL, + `mapType` tinyint(3) unsigned NOT NULL, KEY `normal10` (`normal10`), KEY `normal25` (`normal25`), KEY `heroic10` (`heroic10`), @@ -2817,8 +3070,11 @@ DROP TABLE IF EXISTS `aowow_taxinodes`; CREATE TABLE `aowow_taxinodes` ( `id` smallint(5) unsigned NOT NULL, `mapId` smallint(5) unsigned NOT NULL, - `posX` float unsigned NOT NULL, - `posY` float unsigned NOT NULL, + `mapX` float unsigned NOT NULL, + `mapY` float unsigned NOT NULL, + `areaId` smallint(5) unsigned NOT NULL, + `areaX` float unsigned NOT NULL, + `areaY` float unsigned NOT NULL, `type` enum('NPC','GOBJECT') NOT NULL, `typeId` mediumint(8) unsigned NOT NULL, `reactA` tinyint(4) NOT NULL, diff --git a/setup/sql/02-db_initial_data.sql b/setup/sql/02-db_initial_data.sql index fb3e1d8a..fe750cf3 100644 --- a/setup/sql/02-db_initial_data.sql +++ b/setup/sql/02-db_initial_data.sql @@ -61,7 +61,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_config` WRITE; /*!40000 ALTER TABLE `aowow_config` DISABLE KEYS */; -INSERT INTO `aowow_config` VALUES ('acc_allow_register','1','1',3,132,'allow/disallow account creation (requires AUTH_MODE: aowow)'),('acc_auth_mode','0','0',3,1425,'source to auth against - 0:AoWoW, 1:TC auth-table, 2:External script (config/extAuth.php)'),('acc_create_save_decay','604800','604800',3,129,'time in wich an unconfirmed account cannot be overwritten by new registrations'),('acc_ext_create_url','',NULL,3,136,'if auth mode is not self; link to external account creation'),('acc_ext_recover_url','',NULL,3,136,'if auth mode is not self; link to external account recovery'),('acc_failed_auth_block','900','15 * 60',3,129,'how long an account is closed after exceeding FAILED_AUTH_COUNT (in seconds)'),('acc_failed_auth_count','5','5',3,129,'how often invalid passwords are tolerated'),('acc_max_avatar_uploads','10','10',3,129,'premium users may upload this many avatars'),('acc_recovery_decay','300','300',3,129,'time to recover your account and new recovery requests are blocked'),('acc_rename_decay','2592000','30 * 24 * 60 * 60',3,129,'delay between username changes'),('battlegroup','Pure Pwnage',NULL,1,136,'pretend, we belong to a battlegroup to satisfy profiler-related javascripts'),('board_url','http://www.wowhead.com/forums?board=',NULL,1,136,'another halfbaked javascript thing..'),('cache_decay','25200','60 * 60 * 7',2,129,'time to keep cache in seconds'),('cache_dir','','cache/template',2,136,'generated pages are saved here (requires CACHE_MODE: filecache)'),('cache_mode','1','1',2,1185,'set cache method - 0:filecache, 1:memcached'),('contact_email','feedback@aowow.org',NULL,1,136,'displayed sender for auth-mails, ect'),('debug','0','0',1,145,'disable cache, enable error_reporting - 0:None, 1:Error, 2:Warning, 3:Info'),('default_charset','utf-8',NULL,0,72,''),('force_ssl','0','0',1,132,'enforce SSL, if auto-detect fails'),('gtag_measurement_id','',NULL,6,136,'enter your Google Tag measurement ID here to track site stats'),('locales','349','0x15D',1,1441,'allowed locales - 0:English, 2:French, 3:German, 4:Chinese, 6:Spanish, 8:Russian'),('maintenance','1','0',1,132,'display brb gnomes and block access for non-staff'),('memory_limit','1500M','1500M',0,200,'parsing spell.dbc is quite intense'),('name','Aowow Database Viewer (ADV)',NULL,1,136,'website title'),('name_short','Aowow',NULL,1,136,'feed title'),('profiler_enable','0','0',7,1412,'enable/disable profiler feature'),('profiler_queue_delay','3000','3000',7,129,'min. delay between queue cycles (in ms)'),('profiler_resync_delay','3600','1 * 60 * 60',7,129,'how often a character can be refreshed (in sec)'),('profiler_resync_ping','5000','5000',7,129,'how often the javascript asks for for updates, when queued (in ms)'),('rep_req_border_epic','15000','15000',5,129,'required reputation for epic quality avatar border'),('rep_req_border_legendary','25000','25000',5,129,'required reputation for legendary quality avatar border'),('rep_req_border_rare','10000','10000',5,129,'required reputation for rare quality avatar border'),('rep_req_border_uncommon','5000','5000',5,129,'required reputation for uncommon quality avatar border'),('rep_req_comment','75','75',5,129,'required reputation to write a comment'),('rep_req_downvote','250','250',5,129,'required reputation to downvote comments'),('rep_req_ext_links','150','150',5,129,'required reputation to link to external sites'),('rep_req_premium','25000','25000',5,129,'required reputation for premium status through reputation'),('rep_req_reply','75','75',5,129,'required reputation to write a reply'),('rep_req_supervote','2500','2500',5,129,'required reputation for double vote effect'),('rep_req_upvote','125','125',5,129,'required reputation to upvote comments'),('rep_req_votemore_add','250','250',5,129,'required reputation per additional vote past threshold'),('rep_req_votemore_base','2000','2000',5,129,'gains more votes past this threshold'),('rep_reward_article','100','100',5,129,'submitted an approved article/guide'),('rep_reward_bad_report','0','0',5,129,'filed a rejected report'),('rep_reward_comment','1','1',5,129,'created a comment (not a reply)'),('rep_reward_dailyvisit','5','5',5,129,'daily visit'),('rep_reward_downvoted','0','0',5,129,'comment received downvote'),('rep_reward_good_report','10','10',5,129,'filed an accepted report'),('rep_reward_register','100','100',5,129,'activated an account'),('rep_reward_submit_screenshot','10','10',5,129,'uploaded screenshot was approved'),('rep_reward_suggest_video','10','10',5,129,'suggested video was approved'),('rep_reward_upvoted','5','5',5,129,'comment received upvote'),('rep_reward_user_suspended','-200','-200',5,129,'moderator revoked rights'),('rep_reward_user_warned','-50','-50',5,129,'moderator imposed a warning'),('screenshot_min_size','200','200',1,1153,'minimum dimensions of uploaded screenshots in px (yes, it\'s square, no it cant go below 200)'),('serialize_precision','5',NULL,0,65,''),('session_cache_dir','',NULL,4,136,'php sessions are saved here. Leave empty to use php default directory.'),('session_timeout_delay','3600','60 * 60',4,129,'non-permanent session times out in time() + X'),('session.gc_divisor','100','100',4,200,'probability to remove session data on garbage collection'),('session.gc_maxlifetime','604800','7 * 24 * 60 * 60',4,200,'lifetime of session data'),('session.gc_probability','1','0',4,200,'probability to remove session data on garbage collection'),('site_host','',NULL,1,904,'points js to executable files'),('sql_limit_default','300','300',1,129,'max results for listviews'),('sql_limit_none','0','0',1,129,'unlimited results (i wouldn\'t change that mate)'),('sql_limit_quicksearch','10','10',1,129,'max results for suggestions'),('sql_limit_search','500','500',1,129,'max results for search'),('static_host','',NULL,1,904,'points js to images & scripts'),('ttl_rss','60','60',1,129,'time to live for RSS (in seconds)'),('ua_measurement_key','',NULL,6,136,'[DEPRECATED ?] Enter your Google Universal Analytics key here to track site stats'),('user_max_votes','50','50',1,129,'vote limit per day'); +INSERT INTO `aowow_config` VALUES ('logographic_ft_search','0','0',1,1156,'enables fulltext search for logographic languages (CN, KR, TW). The database MUST support this (i.e. MySQL implements ngram)'),('acc_allow_register','1','1',3,132,'allow/disallow account creation (requires AUTH_MODE: aowow)'),('acc_auth_mode','0','0',3,1425,'source to auth against - 0:AoWoW, 1:TC auth-table, 2:External script (config/extAuth.php)'),('acc_create_save_decay','604800','604800',3,129,'time in wich an unconfirmed account cannot be overwritten by new registrations'),('acc_ext_create_url','',NULL,3,136,'if auth mode is not self; link to external account creation'),('acc_ext_recover_url','',NULL,3,136,'if auth mode is not self; link to external account recovery'),('acc_failed_auth_block','900','15 * 60',3,129,'how long an account is closed after exceeding FAILED_AUTH_COUNT (in seconds)'),('acc_failed_auth_count','5','5',3,129,'how often invalid passwords are tolerated'),('acc_max_avatar_uploads','10','10',3,129,'premium users may upload this many avatars'),('acc_recovery_decay','300','300',3,129,'time to recover your account and new recovery requests are blocked'),('acc_rename_decay','2592000','30 * 24 * 60 * 60',3,129,'delay between username changes'),('battlegroup','Pure Pwnage',NULL,1,136,'pretend, we belong to a battlegroup to satisfy profiler-related javascripts'),('board_url','http://www.wowhead.com/forums?board=',NULL,1,136,'another halfbaked javascript thing..'),('cache_decay','25200','60 * 60 * 7',2,129,'time to keep cache in seconds'),('cache_dir','','cache/template',2,136,'generated pages are saved here (requires CACHE_MODE: filecache)'),('cache_mode','1','1',2,1185,'set cache method - 0:filecache, 1:memcached'),('contact_email','feedback@aowow.org',NULL,1,136,'displayed sender for auth-mails, ect'),('debug','0','0',1,145,'disable cache, enable error_reporting - 0:None, 1:Error, 2:Warning, 3:Info'),('default_charset','utf-8',NULL,0,72,''),('force_ssl','0','0',1,132,'enforce SSL, if auto-detect fails'),('gtag_measurement_id','',NULL,6,136,'enter your Google Tag measurement ID here to track site stats'),('locales','349','0x15D',1,1441,'allowed locales - 0:English, 2:French, 3:German, 4:Chinese, 6:Spanish, 8:Russian'),('maintenance','1','0',1,132,'display brb gnomes and block access for non-staff'),('memory_limit','1500M','1500M',0,200,'parsing spell.dbc is quite intense'),('name','Aowow Database Viewer (ADV)',NULL,1,136,'website title'),('name_short','Aowow',NULL,1,136,'feed title'),('profiler_enable','0','0',7,1412,'enable/disable profiler feature'),('profiler_queue_delay','3000','3000',7,129,'min. delay between queue cycles (in ms)'),('profiler_resync_delay','3600','1 * 60 * 60',7,129,'how often a character can be refreshed (in sec)'),('profiler_resync_ping','5000','5000',7,129,'how often the javascript asks for for updates, when queued (in ms)'),('rep_req_border_epic','15000','15000',5,129,'required reputation for epic quality avatar border'),('rep_req_border_legendary','25000','25000',5,129,'required reputation for legendary quality avatar border'),('rep_req_border_rare','10000','10000',5,129,'required reputation for rare quality avatar border'),('rep_req_border_uncommon','5000','5000',5,129,'required reputation for uncommon quality avatar border'),('rep_req_comment','75','75',5,129,'required reputation to write a comment'),('rep_req_downvote','250','250',5,129,'required reputation to downvote comments'),('rep_req_ext_links','150','150',5,129,'required reputation to link to external sites'),('rep_req_premium','25000','25000',5,129,'required reputation for premium status through reputation'),('rep_req_reply','75','75',5,129,'required reputation to write a reply'),('rep_req_supervote','2500','2500',5,129,'required reputation for double vote effect'),('rep_req_upvote','125','125',5,129,'required reputation to upvote comments'),('rep_req_votemore_add','250','250',5,129,'required reputation per additional vote past threshold'),('rep_req_votemore_base','2000','2000',5,129,'gains more votes past this threshold'),('rep_reward_article','100','100',5,129,'submitted an approved article/guide'),('rep_reward_bad_report','0','0',5,129,'filed a rejected report'),('rep_reward_comment','1','1',5,129,'created a comment (not a reply)'),('rep_reward_dailyvisit','5','5',5,129,'daily visit'),('rep_reward_downvoted','0','0',5,129,'comment received downvote'),('rep_reward_good_report','10','10',5,129,'filed an accepted report'),('rep_reward_register','100','100',5,129,'activated an account'),('rep_reward_submit_screenshot','10','10',5,129,'uploaded screenshot was approved'),('rep_reward_suggest_video','10','10',5,129,'suggested video was approved'),('rep_reward_upvoted','5','5',5,129,'comment received upvote'),('rep_reward_user_suspended','-200','-200',5,129,'moderator revoked rights'),('rep_reward_user_warned','-50','-50',5,129,'moderator imposed a warning'),('screenshot_min_size','200','200',1,1153,'minimum dimensions of uploaded screenshots in px (yes, it\'s square, no it cant go below 200)'),('serialize_precision','5',NULL,0,65,''),('session_cache_dir','',NULL,4,136,'php sessions are saved here. Leave empty to use php default directory.'),('session_timeout_delay','3600','60 * 60',4,129,'non-permanent session times out in time() + X'),('session.gc_divisor','100','100',4,200,'probability to remove session data on garbage collection'),('session.gc_maxlifetime','604800','7 * 24 * 60 * 60',4,200,'lifetime of session data'),('session.gc_probability','1','0',4,200,'probability to remove session data on garbage collection'),('site_host','',NULL,1,904,'points js to executable files'),('static_host','',NULL,1,904,'points js to images & scripts'),('ttl_rss','60','60',1,129,'time to live for RSS (in seconds)'),('ua_measurement_key','',NULL,6,136,'[DEPRECATED ?] Enter your Google Universal Analytics key here to track site stats'),('user_max_votes','50','50',1,129,'vote limit per day'); /*!40000 ALTER TABLE `aowow_config` ENABLE KEYS */; UNLOCK TABLES; @@ -71,7 +71,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_dbversion` WRITE; /*!40000 ALTER TABLE `aowow_dbversion` DISABLE KEYS */; -INSERT INTO `aowow_dbversion` VALUES (1758578400,17,NULL,NULL); +INSERT INTO `aowow_dbversion` VALUES (1774551740,0,NULL,NULL); /*!40000 ALTER TABLE `aowow_dbversion` ENABLE KEYS */; UNLOCK TABLES; @@ -115,6 +115,17 @@ INSERT INTO `aowow_loot_link` VALUES (19710,184465,1,0,0),(19218,184465,1,1,0),( /*!40000 ALTER TABLE `aowow_loot_link` ENABLE KEYS */; UNLOCK TABLES; +-- +-- Dumping data for table `aowow_objectdifficulty` +-- + +LOCK TABLES `aowow_objectdifficulty` WRITE; +/*!40000 ALTER TABLE `aowow_objectdifficulty` DISABLE KEYS */; +INSERT INTO `aowow_objectdifficulty` VALUES (181366,193426,0,0,2),(193905,193967,0,0,2),(194307,194308,194200,194201,2),(194312,194314,194313,194315,2),(194324,194328,194327,194331,2),(194789,194956,194957,194958,2),(194821,194822,0,0,2),(195046,195047,0,0,2),(195631,195632,195633,195635,2),(202178,202180,202177,202179,2),(202239,202240,202238,202241,2),(201959,202339,202338,202340,2),(0,0,195668,195672,2),(0,0,195667,195671,2),(0,0,195666,195670,2),(0,0,195665,195669,2),(185168,185169,0,0,1),(184465,184849,0,0,1),(190586,193996,0,0,1),(190663,193597,0,0,1),(191349,193603,0,0,1),(195709,195710,0,0,1),(195323,195324,0,0,1),(195374,195375,0,0,1),(201710,202336,0,0,1); +/*!40000 ALTER TABLE `aowow_objectdifficulty` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + -- -- Dumping data for table `aowow_profiler_excludes` -- @@ -142,7 +153,7 @@ UNLOCK TABLES; LOCK TABLES `aowow_setup_custom_data` WRITE; /*!40000 ALTER TABLE `aowow_setup_custom_data` DISABLE KEYS */; -INSERT INTO `aowow_setup_custom_data` VALUES ('zones',2257,'cuFlags','0','Deeprun Tram - make visible'),('zones',2257,'category','0','Deeprun Tram - Category: Eastern Kingdoms'),('zones',2257,'type','1','Deeprun Tram - Type: Transit'),('zones',3698,'expansion','1','Nagrand Arena - Addon: BC'),('zones',3702,'expansion','1','Blades Edge Arena - Addon: BC'),('zones',3968,'expansion','1','Ruins of Lordaeron Arena - Addon: BC'),('zones',4378,'expansion','1','Dalaran Arena - Addon: WotLK'),('zones',4406,'expansion','1','Ring of Valor Arena - Addon: WotLK'),('zones',2597,'maxPlayer','40','Alterac Valey - Players: 40 [battlemasterlist.dbc: 5]'),('zones',4710,'maxPlayer','40','Isle of Conquest - Players: 40 [battlemasterlist.dbc: 5]'),('zones',4893,'cuFlags','1073741824','The Frost Queen\'s Lair - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4894,'cuFlags','1073741824','Putricide\'s Laboratory [..] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('achievement',1956,'itemExtra','44738','Higher Learning - item rewarded through gossip'),('zones',4895,'cuFlags','1073741824','The Crimson Hall - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('titles',137,'gender','2','Matron - female'),('zones',4896,'cuFlags','1073741824','The Frozen Throne - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4897,'cuFlags','1073741824','The Sanctum of Blood - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4076,'cuFlags','1073741824','Reuse Me 7 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',207,'cuFlags','1073741824','The Great Sea - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',208,'cuFlags','1073741824','Unused Ironcladcove - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',2817,'levelMin','74','Crystalsong Forest - missing lfgDungeons entry'),('zones',1417,'cuFlags','1073741824','Sunken Temple [extra area on map 109] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',22,'cuFlags','1073741824','Programmer Isle - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',151,'cuFlags','1073741824','Designer Island - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',3948,'cuFlags','1073741824','Brian and Pat Test - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4019,'cuFlags','1073741824','Development Land - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',3605,'cuFlags','1073741824','Hyjal Past [extra area on map 560] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',3535,'cuFlags','1073741824','Hellfire Citadel [extra area on map 540] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',41,'levelMin','50','Deadwind Pass - missing lfgDungeons entry'),('zones',41,'levelMax','60','Deadwind Pass - missing lfgDungeons entry'),('zones',2257,'levelMin','1','Deeprun Tram - missing lfgDungeons entry'),('zones',2257,'levelMax','80','Deeprun Tram - missing lfgDungeons entry'),('zones',4298,'category','0','Plaguelands: The Scarlet Enclave - Parent: Eastern Kingdoms'),('zones',4298,'levelMin','55','Plaguelands: The Scarlet Enclave - missing lfgDungeons entry'),('zones',4298,'levelMax','58','Plaguelands: The Scarlet Enclave - missing lfgDungeons entry'),('zones',493,'levelMin','15','Moonglade - missing lfgDungeons entry'),('zones',493,'levelMax','60','Moonglade - missing lfgDungeons entry'),('zones',2817,'levelMax','76','Crystalsong Forest - missing lfgDungeons entry'),('zones',4742,'levelMin','77','Hrothgar\'s Landing - missing lfgDungeons entry'),('zones',4742,'levelMax','80','Hrothgar\'s Landing - missing lfgDungeons entry'),('classes',1,'roles','10','Warrior - rngDPS'),('classes',2,'roles','11','Paladin - mleDPS + Tank + Heal'),('classes',3,'roles','4','Hunter - rngDPS'),('classes',4,'roles','2','Rogue - mleDPS'),('classes',5,'roles','5','Priest - rngDPS + Heal'),('classes',6,'roles','10','Death Knight - mleDPS + Tank'),('classes',7,'roles','7','Shaman - mleDPS + rngDPS + Heal'),('classes',8,'roles','4','Mage - rngDPS'),('classes',9,'roles','4','Warlock - rngDPS'),('classes',11,'roles','15','Druid - mleDPS + Tank + Heal + rngDPS'),('currencies',103,'cap','10000','Arena Points - cap'),('currencies',104,'cap','75000','Honor Points - cap'),('currencies',1,'cuFlags','1073741824','Currency Token Test Token 1 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',2,'cuFlags','1073741824','Currency Token Test Token 2 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',4,'cuFlags','1073741824','Currency Token Test Token 3 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',22,'cuFlags','1073741824','Birmingham Test Item 3 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',141,'cuFlags','1073741824','zzzOLDDaily Quest Faction Token - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',1,'category','3','Currency Token Test Token 1 - category: unused'),('currencies',2,'category','3','Currency Token Test Token 2 - category: unused'),('currencies',4,'category','3','Currency Token Test Token 3 - category: unused'),('currencies',22,'category','3','Birmingham Test Item 3 - category: unused'),('currencies',141,'category','3','zzzOLDDaily Quest Faction Token - category: unused'),('factions',68,'qmNpcIds','33555','Undercity - set Quartermaster'),('factions',47,'qmNpcIds','33310','Ironforge - set Quartermaster'),('factions',69,'qmNpcIds','33653','Darnassus - set Quartermaster'),('factions',72,'qmNpcIds','33307','Stormwind - set Quartermaster'),('factions',76,'qmNpcIds','33553','Orgrimmar - set Quartermaster'),('factions',81,'qmNpcIds','33556','Thunder Bluff - set Quartermaster'),('factions',922,'qmNpcIds','16528','Tranquillien - set Quartermaster'),('factions',930,'qmNpcIds','33657','Exodar - set Quartermaster'),('factions',932,'qmNpcIds','19321','The Aldor - set Quartermaster'),('factions',933,'qmNpcIds','20242 23007','The Consortium - set Quartermaster'),('factions',935,'qmNpcIds','21432','The Sha\'tar - set Quartermaster'),('factions',941,'qmNpcIds','20241','The Mag\'har - set Quartermaster'),('factions',942,'qmNpcIds','17904','Cenarion Expedition - set Quartermaster'),('factions',946,'qmNpcIds','17657','Honor Hold - set Quartermaster'),('factions',947,'qmNpcIds','17585','Thrallmar - set Quartermaster'),('factions',970,'qmNpcIds','18382','Sporeggar - set Quartermaster'),('factions',978,'qmNpcIds','20240','Kurenai - set Quartermaster'),('factions',989,'qmNpcIds','21643','Keepers of Time - set Quartermaster'),('factions',1011,'qmNpcIds','21655','Lower City - set Quartermaster'),('factions',1012,'qmNpcIds','23159','Ashtongue Deathsworn - set Quartermaster'),('factions',1037,'qmNpcIds','32773 32564','Alliance Vanguard - set Quartermaster'),('factions',1038,'qmNpcIds','23428','Ogri\'la - set Quartermaster'),('factions',1052,'qmNpcIds','32774 32565','Horde Expedition - set Quartermaster'),('factions',1073,'qmNpcIds','31916 32763','The Kalu\'ak - set Quartermaster'),('factions',1090,'qmNpcIds','32287','Kirin Tor - set Quartermaster'),('factions',1091,'qmNpcIds','32533','The Wyrmrest Accord - set Quartermaster'),('factions',1094,'qmNpcIds','34881','The Silver Covenant - set Quartermaster'),('factions',1105,'qmNpcIds','31910','The Oracles - set Quartermaster'),('factions',1106,'qmNpcIds','30431','Argent Crusade - set Quartermaster'),('factions',1119,'qmNpcIds','32540','The Sons of Hodir - set Quartermaster'),('factions',1124,'qmNpcIds','34772','The Sunreavers - set Quartermaster'),('factions',1156,'qmNpcIds','37687','The Ashen Verdict - set Quartermaster'),('factions',1082,'cuFlags','1073741824','REUSE - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('factions',952,'cuFlags','1073741824','Test Faction 3 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('titles',138,'gender','1','Patron - male'),('sounds',15407,'cat','10','UR_Algalon_Summon03 - is not an item pickup'),('shapeshiftforms',1,'displayIdH','8571','Cat Form - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',15,'displayIdH','8571','Creature - Cat - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',5,'displayIdH','2289','Bear Form - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',8,'displayIdH','2289','Dire Bear Form - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',14,'displayIdH','2289','Creature - Bear - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',27,'displayIdH','21244','Flight Form, Epic - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',29,'displayIdH','20872','Flight Form - spellshapeshiftform.dbc missing displayId'),('races',1,'leader','29611','Human - King Varian Wrynn'),('races',1,'factionId','72','Human - Stormwind'),('races',1,'startAreaId','12','Human - Elwynn Forest'),('races',2,'leader','4949','Orc - Thrall'),('races',2,'factionId','76','Orc - Orgrimmar'),('races',2,'startAreaId','14','Orc - Durotar'),('races',3,'leader','2784','Dwarf - King Magni Bronzebeard'),('races',3,'factionId','47','Dwarf - Ironforge'),('races',3,'startAreaId','1','Dwarf - Dun Morogh'),('races',4,'leader','7999','Night Elf - Tyrande Whisperwind'),('races',4,'factionId','69','Night Elf - Darnassus'),('races',4,'startAreaId','141','Night Elf - Teldrassil'),('races',5,'leader','10181','Undead - Lady Sylvanas Windrunner'),('races',5,'factionId','68','Undead - Undercity'),('races',5,'startAreaId','85','Undead - Tirisfal Glades'),('races',6,'leader','3057','Tauren - Cairne Bloodhoof'),('races',6,'factionId','81','Tauren - Thunder Bluff'),('races',6,'startAreaId','215','Tauren - Mulgore'),('races',7,'leader','7937','Gnome - High Tinker Mekkatorque'),('races',7,'factionId','54','Gnome - Gnomeregan Exiles'),('races',7,'startAreaId','1','Gnome - Dun Morogh'),('races',8,'leader','10540','Troll - Vol\'jin'),('races',8,'factionId','530','Troll - Darkspear Trolls'),('races',8,'startAreaId','14','Troll - Durotar'),('races',10,'leader','16802','Blood Elf - Lor\'themar Theron'),('races',10,'factionId','911','Blood Elf - Silvermoon City'),('races',10,'startAreaId','3430','Blood Elf - Eversong Woods'),('races',11,'leader','17468','Draenei - Prophet Velen'),('races',11,'factionId','930','Draenei - Exodar'),('races',11,'startAreaId','3524','Draenei - Azuremyst Isle'),('holidays',62,'iconString','inv_misc_missilelarge_red','Fireworks Spectacular'),('holidays',141,'iconString','calendar_winterveilstart','Feast of Winter Veil'),('holidays',181,'iconString','calendar_noblegardenstart','Noblegarden'),('holidays',201,'iconString','calendar_childrensweekstart','Children\'s Week'),('holidays',283,'iconString','inv_jewelry_necklace_21','Call to Arms: Alterac Valley'),('holidays',284,'iconString','inv_misc_rune_07','Call to Arms: Warsong Gulch'),('holidays',285,'iconString','inv_jewelry_amulet_07','Call to Arms: Arathi Basin'),('holidays',301,'iconString','calendar_fishingextravaganzastart','Stranglethorn Fishing Extravaganza'),('holidays',321,'iconString','calendar_harvestfestivalstart','Harvest Festival'),('holidays',324,'iconString','calendar_hallowsendstart','Hallow\'s End'),('holidays',327,'iconString','calendar_lunarfestivalstart','Lunar Festival'),('holidays',335,'iconString','calendar_loveintheairstart','Love is in the Air'),('holidays',341,'iconString','calendar_midsummerstart','Midsummer Fire Festival'),('holidays',353,'iconString','spell_nature_eyeofthestorm','Call to Arms: Eye of the Storm'),('holidays',372,'iconString','calendar_brewfeststart','Brewfest'),('holidays',374,'iconString','calendar_darkmoonfaireelwynnstart','Darkmoon Faire'),('holidays',375,'iconString','calendar_darkmoonfairemulgorestart','Darkmoon Faire'),('holidays',376,'iconString','calendar_darkmoonfaireterokkarstart','Darkmoon Faire'),('holidays',398,'iconString','calendar_piratesdaystart','Pirates\' Day'),('holidays',400,'iconString','achievement_bg_winsoa','Call to Arms: Strand of the Ancients'),('holidays',404,'iconString','calendar_harvestfestivalstart','Pilgrim\'s Bounty'),('holidays',406,'iconString','achievement_boss_lichking','Wrath of the Lich King Launch'),('holidays',409,'iconString','calendar_dayofthedeadstart','Day of the Dead'),('holidays',420,'iconString','achievement_bg_winwsg','Call to Arms: Isle of Conquest'),('holidays',423,'iconString','calendar_loveintheairstart','Love is in the Air'),('holidays',424,'iconString','calendar_fishingextravaganzastart','Kalu\'ak Fishing Derby'),('holidays',141,'achievementCatOrId','156','Feast of Winter Veil - Category: Feast of Winter Veil'),('holidays',181,'achievementCatOrId','159','Noblegarden - Category: Noblegarden'),('holidays',201,'achievementCatOrId','163','Children\'s Week - Category: Children\'s Week'),('holidays',324,'achievementCatOrId','158','Hallow\'s End - Category: Hallow\'s End'),('holidays',327,'achievementCatOrId','160','Lunar Festival - Category: Lunar Festival'),('holidays',341,'achievementCatOrId','161','Midsummer Fire Festival - Category: Midsummer Fire Festival'),('holidays',372,'achievementCatOrId','162','Brewfest - Category: Brewfest'),('holidays',398,'achievementCatOrId','-3457','Pirates\' Day - Achievement: The Captain\'s Booty'),('holidays',404,'achievementCatOrId','14981','Pilgrim\'s Bounty - Category: Pilgrim\'s Bounty'),('holidays',409,'achievementCatOrId','-3456','Day of the Dead - Achievement: Dead Man\'s Party'),('holidays',423,'achievementCatOrId','187','Love is in the Air - Category: Love is in the Air'),('holidays',324,'bossCreature','23682','Hallow\'s End - Headless Horseman'),('holidays',327,'bossCreature','15467','Lunar Festival - Omen'),('holidays',341,'bossCreature','25740','Midsummer Fire Festival - Ahune'),('holidays',372,'bossCreature','23872','Brewfest - Coren Direbrew'),('holidays',423,'bossCreature','36296','Love is in the Air - Apothecary Hummel'),('skillline',197,'professionMask','512','Tailoring'),('skillline',186,'professionMask','256','Mining'),('skillline',165,'specializations','10656 10658 10660','Leatherworking'),('skillline',165,'recipeSubClass','1','Leatherworking'),('skillline',165,'professionMask','128','Leatherworking'),('skillline',755,'recipeSubClass','10','Jewelcrafting'),('skillline',755,'professionMask','64','Jewelcrafting'),('skillline',129,'recipeSubClass','7','First Aid'),('skillline',129,'professionMask','32','First Aid'),('skillline',202,'specializations','20219 20222','Engineering'),('skillline',202,'recipeSubClass','3','Engineering'),('skillline',202,'professionMask','16','Engineering'),('skillline',333,'recipeSubClass','8','Enchanting'),('skillline',333,'professionMask','8','Enchanting'),('skillline',185,'recipeSubClass','5','Cooking'),('skillline',185,'professionMask','4','Cooking'),('skillline',164,'specializations','9788 9787 17041 17040 17039','Blacksmithing'),('skillline',164,'recipeSubClass','4','Blacksmithing'),('skillline',164,'professionMask','2','Blacksmithing'),('skillline',171,'specializations','28677 28675 28672','Alchemy'),('skillline',171,'recipeSubClass','6','Alchemy'),('skillline',171,'professionMask','1','Alchemy'),('skillline',393,'professionMask','0','Skinning'),('skillline',197,'recipeSubClass','2','Tailoring'),('skillline',197,'specializations','26798 26801 26797','Tailoring'),('skillline',356,'professionMask','1024','Fishing'),('skillline',356,'recipeSubClass','9','Fishing'),('skillline',182,'professionMask','2048','Herbalism'),('skillline',773,'professionMask','4096','Inscription'),('skillline',773,'recipeSubClass','11','Inscription'),('skillline',785,'name_loc0','Pet - Wasp','Pet - Wasp'),('skillline',781,'name_loc2','Familier - diablosaure exotique','Pet - Exotic Devlisaur'),('skillline',758,'name_loc6','Mascota: Evento - Control remoto','Pet - Event - Remote Control'),('skillline',758,'name_loc3','Tier - Ereignis Ferngesteuert','Pet - Event - Remote Control'),('skillline',758,'categoryId','7','Pet - Event - Remote Control - bring in line with other pets'),('skillline',788,'categoryId','7','Pet - Exotic Spirit Beast - bring in line with other pets'),('items',33147,'class','9','Formula: Enchant Cloak - Subtlety - Class: Recipes'),('items',33147,'subClass','8','Formula: Enchant Cloak - Subtlety - Subclass: Enchanting'),('currencies',1,'description_loc0','Text that describes this item can be found here.',''),('currencies',1,'description_loc2','Un texte qui décrit l\'objet figure ici.',''),('currencies',1,'description_loc3','Text, der den Gegenstand beschreibt, wird hier angezeigt.',''),('currencies',1,'description_loc6','Aquí puede encontrarse el texto que describe a este objeto.',''),('currencies',1,'description_loc8','Здесь находится описание предмета.',''),('currencies',61,'description_loc0','Tiffany Cartier\'s shop in Dalaran will gladly accept these tokens for unique jewelcrafting recipes.',''),('currencies',61,'description_loc2','La boutique de Tiffany Kartier, à Dalaran, accepte avec joie ces marques contre des dessins de joaillerie uniques.',''),('currencies',61,'description_loc3','Tiffany Cartiers Geschäft in Dalaran wird diese Symbole im Tausch gegen einzigartige Juweliersrezepte dankend annehmen.',''),('currencies',61,'description_loc4','达拉然的蒂凡妮·卡蒂亚会欣然接受这些代币,并用稀有的珠宝加工图鉴来交换。',''),('currencies',61,'description_loc6','La tienda de Tiffany Cartier en Dalaran cambiará gustosamente estos talismanes por recetas de joyería.',''),('currencies',61,'description_loc8','В магазине Тиффани Картье, что в Даларане, вам с радостью обменяют эти знаки на уникальные ювелирные эскизы.',''),('currencies',81,'description_loc0','Visit special cooking vendors in Dalaran and the capital cities to to purchase unusual cooking recipes, spices, and even a fine hat!',''),('currencies',81,'description_loc2','Rendez visite aux marchands de fournitures de cuisine à Dalaran et dans les autres capitales pour acheter des recettes de cuisine spéciales, des épices, et même une superbe toque !',''),('currencies',81,'description_loc3','Besucht besondere Kochhändler in Dalaran und den Hauptstädten, um ungewöhnliche Kochrezepte, Gewürze und sogar eine großartige Mütze zu kaufen!',''),('currencies',81,'description_loc4','造访达拉然以及各个主城的特殊烹饪供应商,购买罕见的烹饪配方、香料以及大厨的帽子!',''),('currencies',81,'description_loc6','Visita a los vendedores de cocina especiales de Dalaran y de las capitales para comprar recetas de cocina poco frecuentes, especias, ¡e incluso un bonito gorro!',''),('currencies',81,'description_loc8','Посетите торговцев кулинарными товарами в Даларане и других столицах, чтобы приобрести особые кулинарные рецепты, специи и даже головной убор!',''),('currencies',241,'description_loc0','Awarded for valiant acts in the Crusader\'s Coliseum.',''),('currencies',241,'description_loc2','Obtenu en récompense d’actes de bravoure au colisée des Croisés.',''),('currencies',241,'description_loc3','Werden für hehre Taten im Kolosseum der Kreuzfahrer verliehen.',''),('currencies',241,'description_loc4','表彰你在十字军演武场中展示的武勇。',''),('currencies',241,'description_loc6','Otorgado por las hazañas en el Coliseo de los Cruzados.',''),('currencies',241,'description_loc8','За храбрость, проявленную на турнирах Колизея Авангарда.',''),('currencies',181,'description_loc0','If you can read this, you\'ve found a bug. REPORT IT!',''),('currencies',181,'description_loc2','Si vous lisez ceci, c\'est un bug. SIGNALEZ-LE !',''),('currencies',181,'description_loc3','Wenn Ihr das hier lesen könnt, habt Ihr einen Bug gefunden. MELDET IHN!',''),('currencies',181,'description_loc6','Si puedes leer esto, has encontrado un error. ¡Informa!',''),('currencies',181,'description_loc8','Если вы видите это сообщение, это значит, что вы обнаружили ошибку. Сообщите о ней!',''),('currencies',103,'description_loc0','Used to purchase powerful PvP armor and weapons.',''),('currencies',103,'description_loc2','Utilisés pour acheter des armures et armes de JcJ puissantes.',''),('currencies',103,'description_loc3','Können für den Erwerb von mächtigen PVP-Waffen und -Rüstungen verwendet werden.',''),('currencies',103,'description_loc4','竞技场点数是通过在竞技场战斗中获胜而赢得的。你可以消费这些点数来购买强大的奖励品!',''),('currencies',103,'description_loc6','Se utilizan para comprar armas y armaduras de JcJ poderosas.',''),('currencies',103,'description_loc8','За эти очки можно покупать мощное оружие и доспехи для PvP-сражений.',''),('currencies',104,'description_loc0','Used to purchase less-powerful PvP armor and weapons.',''),('currencies',104,'description_loc2','Utilisés pour acheter des armures et armes de JcJ moyennement puissantes.',''),('currencies',104,'description_loc3','Können für den Erwerb von weniger mächtigen PVP-Waffen und -Rüstungen verwendet werden.',''),('currencies',104,'description_loc4','荣誉是通过在PvP战斗中 杀死敌对阵营的成员获得的。你可以使用荣誉点数购买特殊的物品。',''),('currencies',104,'description_loc6','Se utilizan para comprar armas y armaduras de JcJ menos poderosas.',''),('currencies',104,'description_loc8','За эти очки можно покупать не очень мощное оружие и доспехи для PvP-сражений.',''),('currencies',221,'description_loc0','Used to purchase less-powerful armor and weapons.',''),('currencies',221,'description_loc2','Utilisés pour acheter des armures et armes de JcJ moyennement puissantes.',''),('currencies',221,'description_loc3','Können für den Erwerb von weniger mächtigen Waffen und Rüstungen verwendet werden.',''),('currencies',221,'description_loc6','Se utilizan para comprar armas y armaduras menos poderosas.',''),('currencies',221,'description_loc8','За эти очки можно покупать не очень мощное оружие и доспехи.',''),('currencies',341,'description_loc0','Used to purchase powerful PvE armor and weapons.',''),('currencies',341,'description_loc2','Utilisés pour acheter des armures et armes de JcE puissantes.',''),('currencies',341,'description_loc3','Können für den Erwerb von mächtigen PVE-Waffen und -Rüstungen verwendet werden.',''),('currencies',341,'description_loc6','Se utilizan para comprar armas y armaduras de JcE poderosas.',''),('currencies',341,'description_loc8','За эти очки можно покупать мощное оружие и доспехи для PvE-сражений.',''),('spell',9787,'reqSpellId',9787,'Weaponsmith - requires itself'),('spell',9788,'reqSpellId',9788,'Armorsmith - requires itself'),('spell',10656,'reqSpellId',10656,'Dragonscale Leatherworking - requires itself'),('spell',10658,'reqSpellId',10658,'Elemental Leatherworking - requires itself'),('spell',10660,'reqSpellId',10660,'Tribal Leatherworking - requires itself'),('spell',17039,'reqSpellId',17039,'Master Swordsmith - requires itself'),('spell',17040,'reqSpellId',17040,'Master Hammersmith - requires itself'),('spell',17041,'reqSpellId',17041,'Master Axesmith - requires itself'),('spell',20219,'reqSpellId',20219,'Gnomish Engineer - requires itself'),('spell',20222,'reqSpellId',20222,'Goblin Engineer - requires itself'),('spell',26797,'reqSpellId',26797,'Spellfire Tailoring - requires itself'),('spell',26798,'reqSpellId',26798,'Mooncloth Tailoring - requires itself'),('spell',26801,'reqSpellId',26801,'Shadoweave Tailoring - requires itself'),('spell',379,'cuFLags',1073741824,'Earth Shield - hide'),('spell',17567,'cuFLags',1073741824,'Summon Blood Parrot - hide'),('spell',19483,'cuFLags',1073741824,'Immolation - hide'),('spell',20154,'cuFLags',1073741824,'Seal of Righteousness - hide'),('spell',21169,'cuFLags',1073741824,'Reincarnation - hide'),('spell',22845,'cuFLags',1073741824,'Frenzied Regeneration - hide'),('spell',23885,'cuFLags',1073741824,'Bloodthirst - hide'),('spell',27813,'cuFLags',1073741824,'Blessed Recovery - hide'),('spell',27817,'cuFLags',1073741824,'Blessed Recovery - hide'),('spell',27818,'cuFLags',1073741824,'Blessed Recovery - hide'),('spell',29442,'cuFLags',1073741824,'Magic Absorption - hide'),('spell',29841,'cuFLags',1073741824,'Second Wind - hide'),('spell',29842,'cuFLags',1073741824,'Second Wind - hide'),('spell',29886,'cuFLags',1073741824,'Create Soulwell - hide'),('spell',30708,'cuFLags',1073741824,'Totem of Wrath - hide'),('spell',30874,'cuFLags',1073741824,'Gift of the Water Spirit - hide'),('spell',31643,'cuFLags',1073741824,'Blazing Speed - hide'),('spell',32841,'cuFLags',1073741824,'Mass Resurrection - hide'),('spell',34919,'cuFLags',1073741824,'Vampiric Touch - hide'),('spell',44450,'cuFLags',1073741824,'Burnout - hide'),('spell',47633,'cuFLags',1073741824,'Death Coil - hide'),('spell',48954,'cuFLags',1073741824,'Swift Zhevra - hide'),('spell',49575,'cuFLags',1073741824,'Death Grip - hide'),('spell',50536,'cuFLags',1073741824,'Unholy Blight - hide'),('spell',52374,'cuFLags',1073741824,'Blood Strike - hide'),('spell',56816,'cuFLags',1073741824,'Rune Strike - hide'),('spell',58427,'cuFLags',1073741824,'Overkill - hide'),('spell',58889,'cuFLags',1073741824,'Create Soulwell - hide'),('spell',64380,'cuFLags',1073741824,'Shattering Throw - hide'),('spell',66122,'cuFLags',1073741824,'Magic Rooster - hide'),('spell',66123,'cuFLags',1073741824,'Magic Rooster - hide'),('spell',66124,'cuFLags',1073741824,'Magic Rooster - hide'),('spell',66175,'cuFLags',1073741824,'Macabre Marionette - hide'),('spell',54910,'cuFLags',1073741824,'Glyph of the Red Lynx - hide unused glyph'),('spell',57231,'cuFLags',1073741824,'Death Knight Glyph 25 - hide unused glyph'),('spell',58166,'cuFLags',1073741824,'Glyph of the Forest Lynx - hide unused glyph'),('spell',58239,'cuFLags',1073741824,'Glyph of the Penguin - hide unused glyph'),('spell',58240,'cuFLags',1073741824,'Glyph of the Bear Cub - hide unused glyph'),('spell',58261,'cuFLags',1073741824,'Glyph of the Arctic Wolf - hide unused glyph'),('spell',58262,'cuFLags',1073741824,'Glyph of the Black Wolf - hide unused glyph'),('spell',60460,'cuFLags',1073741824,'Glyph of Raise Dead - hide unused glyph'),('spell',54910,'skillLine1',0,'Glyph of the Red Lynx - hide unused glyph'),('spell',57231,'skillLine1',0,'Death Knight Glyph 25 - hide unused glyph'),('spell',58166,'skillLine1',0,'Glyph of the Forest Lynx - hide unused glyph'),('spell',58239,'skillLine1',0,'Glyph of the Penguin - hide unused glyph'),('spell',58240,'skillLine1',0,'Glyph of the Bear Cub - hide unused glyph'),('spell',58261,'skillLine1',0,'Glyph of the Arctic Wolf - hide unused glyph'),('spell',58262,'skillLine1',0,'Glyph of the Black Wolf - hide unused glyph'),('spell',60460,'skillLine1',0,'Glyph of Raise Dead - hide unused glyph'),('spell',54910,'iconIdAlt',0,'Glyph of the Red Lynx - hide unused glyph'),('spell',57231,'iconIdAlt',0,'Death Knight Glyph 25 - hide unused glyph'),('spell',58166,'iconIdAlt',0,'Glyph of the Forest Lynx - hide unused glyph'),('spell',58239,'iconIdAlt',0,'Glyph of the Penguin - hide unused glyph'),('spell',58240,'iconIdAlt',0,'Glyph of the Bear Cub - hide unused glyph'),('spell',58261,'iconIdAlt',0,'Glyph of the Arctic Wolf - hide unused glyph'),('spell',58262,'iconIdAlt',0,'Glyph of the Black Wolf - hide unused glyph'),('spell',60460,'iconIdAlt',0,'Glyph of Raise Dead - hide unused glyph'),('quests',9572,'zoneOrSort','3562','Weaken the Ramparts - category Hellfire Citadel -> Hellfire Ramparts'),('quests',9575,'zoneOrSort','3562','Weaken the Ramparts - category Hellfire Citadel -> Hellfire Ramparts'),('quests',11354,'zoneOrSort','3562','Wanted: Nazan\'s Riding Crop - category Hellfire Citadel -> Hellfire Ramparts'),('quests',9589,'zoneOrSort','3713','The Blood is Life - category Hellfire Citadel -> Blood Furnace'),('quests',9590,'zoneOrSort','3713','The Blood is Life - category Hellfire Citadel -> Blood Furnace'),('quests',9607,'zoneOrSort','3713','Heart of Rage - category Hellfire Citadel -> Blood Furnace'),('quests',9608,'zoneOrSort','3713','Heart of Rage - category Hellfire Citadel -> Blood Furnace'),('quests',11362,'zoneOrSort','3713','Wanted: Keli\'dan\'s Feathered Stave - category Hellfire Citadel -> Blood Furnace'),('quests',9492,'zoneOrSort','3714','Turning the Tide - category Hellfire Citadel -> Shattered Halls'),('quests',9493,'zoneOrSort','3714','Pride of the Fel Horde - category Hellfire Citadel -> Shattered Halls'),('quests',9494,'zoneOrSort','3714','Fel Embers - category Hellfire Citadel -> Shattered Halls'),('quests',9495,'zoneOrSort','3714','The Will of the Warchief - category Hellfire Citadel -> Shattered Halls'),('quests',9496,'zoneOrSort','3714','Pride of the Fel Horde - category Hellfire Citadel -> Shattered Halls'),('quests',9497,'zoneOrSort','3714','Emblem of the Fel Horde - category Hellfire Citadel -> Shattered Halls'),('quests',9524,'zoneOrSort','3714','Imprisoned in the Citadel - category Hellfire Citadel -> Shattered Halls'),('quests',9525,'zoneOrSort','3714','Imprisoned in the Citadel - category Hellfire Citadel -> Shattered Halls'),('quests',11363,'zoneOrSort','3714','Wanted: Bladefist\'s Seal - category Hellfire Citadel -> Shattered Halls'),('quests',11364,'zoneOrSort','3714','Wanted: Shattered Hand Centurions - category Hellfire Citadel -> Shattered Halls'); +INSERT INTO `aowow_setup_custom_data` VALUES ('zones',2257,'cuFlags','0','Deeprun Tram - make visible'),('zones',2257,'category','0','Deeprun Tram - Category: Eastern Kingdoms'),('zones',2257,'type','1','Deeprun Tram - Type: Transit'),('zones',3698,'expansion','1','Nagrand Arena - Addon: BC'),('zones',3702,'expansion','1','Blades Edge Arena - Addon: BC'),('zones',3968,'expansion','1','Ruins of Lordaeron Arena - Addon: BC'),('zones',4378,'expansion','1','Dalaran Arena - Addon: WotLK'),('zones',4406,'expansion','1','Ring of Valor Arena - Addon: WotLK'),('zones',2597,'maxPlayer','40','Alterac Valey - Players: 40 [battlemasterlist.dbc: 5]'),('zones',4710,'maxPlayer','40','Isle of Conquest - Players: 40 [battlemasterlist.dbc: 5]'),('zones',4893,'cuFlags','1073741824','The Frost Queen\'s Lair - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4894,'cuFlags','1073741824','Putricide\'s Laboratory [..] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('achievement',1956,'itemExtra','44738','Higher Learning - item rewarded through gossip'),('zones',4895,'cuFlags','1073741824','The Crimson Hall - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('titles',137,'gender','2','Matron - female'),('zones',4896,'cuFlags','1073741824','The Frozen Throne - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4897,'cuFlags','1073741824','The Sanctum of Blood - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4076,'cuFlags','1073741824','Reuse Me 7 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',207,'cuFlags','1073741824','The Great Sea - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',208,'cuFlags','1073741824','Unused Ironcladcove - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',2817,'levelMin','74','Crystalsong Forest - missing lfgDungeons entry'),('zones',1417,'cuFlags','1073741824','Sunken Temple [extra area on map 109] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',22,'cuFlags','1073741824','Programmer Isle - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',151,'cuFlags','1073741824','Designer Island - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',3948,'cuFlags','1073741824','Brian and Pat Test - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',4019,'cuFlags','1073741824','Development Land - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',3605,'cuFlags','1073741824','Hyjal Past [extra area on map 560] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',3535,'cuFlags','1073741824','Hellfire Citadel [extra area on map 540] - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('zones',41,'levelMin','50','Deadwind Pass - missing lfgDungeons entry'),('zones',41,'levelMax','60','Deadwind Pass - missing lfgDungeons entry'),('zones',2257,'levelMin','1','Deeprun Tram - missing lfgDungeons entry'),('zones',2257,'levelMax','80','Deeprun Tram - missing lfgDungeons entry'),('zones',4298,'category','0','Plaguelands: The Scarlet Enclave - Parent: Eastern Kingdoms'),('zones',4298,'levelMin','55','Plaguelands: The Scarlet Enclave - missing lfgDungeons entry'),('zones',4298,'levelMax','58','Plaguelands: The Scarlet Enclave - missing lfgDungeons entry'),('zones',493,'levelMin','15','Moonglade - missing lfgDungeons entry'),('zones',493,'levelMax','60','Moonglade - missing lfgDungeons entry'),('zones',2817,'levelMax','76','Crystalsong Forest - missing lfgDungeons entry'),('zones',4742,'levelMin','77','Hrothgar\'s Landing - missing lfgDungeons entry'),('zones',4742,'levelMax','80','Hrothgar\'s Landing - missing lfgDungeons entry'),('classes',1,'roles','10','Warrior - rngDPS'),('classes',2,'roles','11','Paladin - mleDPS + Tank + Heal'),('classes',3,'roles','4','Hunter - rngDPS'),('classes',4,'roles','2','Rogue - mleDPS'),('classes',5,'roles','5','Priest - rngDPS + Heal'),('classes',6,'roles','10','Death Knight - mleDPS + Tank'),('classes',7,'roles','7','Shaman - mleDPS + rngDPS + Heal'),('classes',8,'roles','4','Mage - rngDPS'),('classes',9,'roles','4','Warlock - rngDPS'),('classes',11,'roles','15','Druid - mleDPS + Tank + Heal + rngDPS'),('currencies',103,'cap','10000','Arena Points - cap'),('currencies',104,'cap','75000','Honor Points - cap'),('currencies',1,'cuFlags','1073741824','Currency Token Test Token 1 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',2,'cuFlags','1073741824','Currency Token Test Token 2 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',4,'cuFlags','1073741824','Currency Token Test Token 3 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',22,'cuFlags','1073741824','Birmingham Test Item 3 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',141,'cuFlags','1073741824','zzzOLDDaily Quest Faction Token - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('currencies',1,'category','3','Currency Token Test Token 1 - category: unused'),('currencies',2,'category','3','Currency Token Test Token 2 - category: unused'),('currencies',4,'category','3','Currency Token Test Token 3 - category: unused'),('currencies',22,'category','3','Birmingham Test Item 3 - category: unused'),('currencies',141,'category','3','zzzOLDDaily Quest Faction Token - category: unused'),('factions',68,'qmNpcIds','33555','Undercity - set Quartermaster'),('factions',47,'qmNpcIds','33310','Ironforge - set Quartermaster'),('factions',69,'qmNpcIds','33653','Darnassus - set Quartermaster'),('factions',72,'qmNpcIds','33307','Stormwind - set Quartermaster'),('factions',76,'qmNpcIds','33553','Orgrimmar - set Quartermaster'),('factions',81,'qmNpcIds','33556','Thunder Bluff - set Quartermaster'),('factions',922,'qmNpcIds','16528','Tranquillien - set Quartermaster'),('factions',930,'qmNpcIds','33657','Exodar - set Quartermaster'),('factions',932,'qmNpcIds','19321','The Aldor - set Quartermaster'),('factions',933,'qmNpcIds','20242 23007','The Consortium - set Quartermaster'),('factions',935,'qmNpcIds','21432','The Sha\'tar - set Quartermaster'),('factions',941,'qmNpcIds','20241','The Mag\'har - set Quartermaster'),('factions',942,'qmNpcIds','17904','Cenarion Expedition - set Quartermaster'),('factions',946,'qmNpcIds','17657','Honor Hold - set Quartermaster'),('factions',947,'qmNpcIds','17585','Thrallmar - set Quartermaster'),('factions',970,'qmNpcIds','18382','Sporeggar - set Quartermaster'),('factions',978,'qmNpcIds','20240','Kurenai - set Quartermaster'),('factions',989,'qmNpcIds','21643','Keepers of Time - set Quartermaster'),('factions',1011,'qmNpcIds','21655','Lower City - set Quartermaster'),('factions',1012,'qmNpcIds','23159','Ashtongue Deathsworn - set Quartermaster'),('factions',1037,'qmNpcIds','32773 32564','Alliance Vanguard - set Quartermaster'),('factions',1038,'qmNpcIds','23428','Ogri\'la - set Quartermaster'),('factions',1052,'qmNpcIds','32774 32565','Horde Expedition - set Quartermaster'),('factions',1073,'qmNpcIds','31916 32763','The Kalu\'ak - set Quartermaster'),('factions',1090,'qmNpcIds','32287','Kirin Tor - set Quartermaster'),('factions',1091,'qmNpcIds','32533','The Wyrmrest Accord - set Quartermaster'),('factions',1094,'qmNpcIds','34881','The Silver Covenant - set Quartermaster'),('factions',1105,'qmNpcIds','31910','The Oracles - set Quartermaster'),('factions',1106,'qmNpcIds','30431','Argent Crusade - set Quartermaster'),('factions',1119,'qmNpcIds','32540','The Sons of Hodir - set Quartermaster'),('factions',1124,'qmNpcIds','34772','The Sunreavers - set Quartermaster'),('factions',1156,'qmNpcIds','37687','The Ashen Verdict - set Quartermaster'),('factions',1082,'cuFlags','1073741824','REUSE - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('factions',952,'cuFlags','1073741824','Test Faction 3 - set: CUSTOM_EXCLUDE_FOR_LISTVIEW'),('titles',138,'gender','1','Patron - male'),('sounds',15407,'cat','10','UR_Algalon_Summon03 - is not an item pickup'),('shapeshiftforms',1,'displayIdH','8571','Cat Form - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',15,'displayIdH','8571','Creature - Cat - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',5,'displayIdH','2289','Bear Form - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',8,'displayIdH','2289','Dire Bear Form - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',14,'displayIdH','2289','Creature - Bear - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',27,'displayIdH','21244','Flight Form, Epic - spellshapeshiftform.dbc missing displayId'),('shapeshiftforms',29,'displayIdH','20872','Flight Form - spellshapeshiftform.dbc missing displayId'),('races',1,'leader','29611','Human - King Varian Wrynn'),('races',1,'factionId','72','Human - Stormwind'),('races',1,'startAreaId','12','Human - Elwynn Forest'),('races',2,'leader','4949','Orc - Thrall'),('races',2,'factionId','76','Orc - Orgrimmar'),('races',2,'startAreaId','14','Orc - Durotar'),('races',3,'leader','2784','Dwarf - King Magni Bronzebeard'),('races',3,'factionId','47','Dwarf - Ironforge'),('races',3,'startAreaId','1','Dwarf - Dun Morogh'),('races',4,'leader','7999','Night Elf - Tyrande Whisperwind'),('races',4,'factionId','69','Night Elf - Darnassus'),('races',4,'startAreaId','141','Night Elf - Teldrassil'),('races',5,'leader','10181','Undead - Lady Sylvanas Windrunner'),('races',5,'factionId','68','Undead - Undercity'),('races',5,'startAreaId','85','Undead - Tirisfal Glades'),('races',6,'leader','3057','Tauren - Cairne Bloodhoof'),('races',6,'factionId','81','Tauren - Thunder Bluff'),('races',6,'startAreaId','215','Tauren - Mulgore'),('races',7,'leader','7937','Gnome - High Tinker Mekkatorque'),('races',7,'factionId','54','Gnome - Gnomeregan Exiles'),('races',7,'startAreaId','1','Gnome - Dun Morogh'),('races',8,'leader','10540','Troll - Vol\'jin'),('races',8,'factionId','530','Troll - Darkspear Trolls'),('races',8,'startAreaId','14','Troll - Durotar'),('races',10,'leader','16802','Blood Elf - Lor\'themar Theron'),('races',10,'factionId','911','Blood Elf - Silvermoon City'),('races',10,'startAreaId','3430','Blood Elf - Eversong Woods'),('races',11,'leader','17468','Draenei - Prophet Velen'),('races',11,'factionId','930','Draenei - Exodar'),('races',11,'startAreaId','3524','Draenei - Azuremyst Isle'),('holidays',141,'achievementCatOrId','156','Feast of Winter Veil - Category: Feast of Winter Veil'),('holidays',181,'achievementCatOrId','159','Noblegarden - Category: Noblegarden'),('holidays',201,'achievementCatOrId','163','Children\'s Week - Category: Children\'s Week'),('holidays',324,'achievementCatOrId','158','Hallow\'s End - Category: Hallow\'s End'),('holidays',327,'achievementCatOrId','160','Lunar Festival - Category: Lunar Festival'),('holidays',341,'achievementCatOrId','161','Midsummer Fire Festival - Category: Midsummer Fire Festival'),('holidays',372,'achievementCatOrId','162','Brewfest - Category: Brewfest'),('holidays',398,'achievementCatOrId','-3457','Pirates\' Day - Achievement: The Captain\'s Booty'),('holidays',404,'achievementCatOrId','14981','Pilgrim\'s Bounty - Category: Pilgrim\'s Bounty'),('holidays',409,'achievementCatOrId','-3456','Day of the Dead - Achievement: Dead Man\'s Party'),('holidays',423,'achievementCatOrId','187','Love is in the Air - Category: Love is in the Air'),('holidays',324,'bossCreature','23682','Hallow\'s End - Headless Horseman'),('holidays',327,'bossCreature','15467','Lunar Festival - Omen'),('holidays',341,'bossCreature','25740','Midsummer Fire Festival - Ahune'),('holidays',372,'bossCreature','23872','Brewfest - Coren Direbrew'),('holidays',423,'bossCreature','36296','Love is in the Air - Apothecary Hummel'),('skillline',197,'professionMask','512','Tailoring'),('skillline',186,'professionMask','256','Mining'),('skillline',165,'specializations','10656 10658 10660','Leatherworking'),('skillline',165,'recipeSubClass','1','Leatherworking'),('skillline',165,'professionMask','128','Leatherworking'),('skillline',755,'recipeSubClass','10','Jewelcrafting'),('skillline',755,'professionMask','64','Jewelcrafting'),('skillline',129,'recipeSubClass','7','First Aid'),('skillline',129,'professionMask','32','First Aid'),('skillline',202,'specializations','20219 20222','Engineering'),('skillline',202,'recipeSubClass','3','Engineering'),('skillline',202,'professionMask','16','Engineering'),('skillline',333,'recipeSubClass','8','Enchanting'),('skillline',333,'professionMask','8','Enchanting'),('skillline',185,'recipeSubClass','5','Cooking'),('skillline',185,'professionMask','4','Cooking'),('skillline',164,'specializations','9788 9787 17041 17040 17039','Blacksmithing'),('skillline',164,'recipeSubClass','4','Blacksmithing'),('skillline',164,'professionMask','2','Blacksmithing'),('skillline',171,'specializations','28677 28675 28672','Alchemy'),('skillline',171,'recipeSubClass','6','Alchemy'),('skillline',171,'professionMask','1','Alchemy'),('skillline',393,'professionMask','0','Skinning'),('skillline',197,'recipeSubClass','2','Tailoring'),('skillline',197,'specializations','26798 26801 26797','Tailoring'),('skillline',356,'professionMask','1024','Fishing'),('skillline',356,'recipeSubClass','9','Fishing'),('skillline',182,'professionMask','2048','Herbalism'),('skillline',773,'professionMask','4096','Inscription'),('skillline',773,'recipeSubClass','11','Inscription'),('skillline',785,'name_loc0','Pet - Wasp','Pet - Wasp'),('skillline',781,'name_loc2','Familier - diablosaure exotique','Pet - Exotic Devlisaur'),('skillline',758,'name_loc6','Mascota: Evento - Control remoto','Pet - Event - Remote Control'),('skillline',758,'name_loc3','Tier - Ereignis Ferngesteuert','Pet - Event - Remote Control'),('skillline',758,'categoryId','7','Pet - Event - Remote Control - bring in line with other pets'),('skillline',788,'categoryId','7','Pet - Exotic Spirit Beast - bring in line with other pets'),('items',33147,'class','9','Formula: Enchant Cloak - Subtlety - Class: Recipes'),('items',33147,'subClass','8','Formula: Enchant Cloak - Subtlety - Subclass: Enchanting'),('currencies',1,'description_loc0','Text that describes this item can be found here.',''),('currencies',1,'description_loc2','Un texte qui d├®crit l\'objet figure ici.',''),('currencies',1,'description_loc3','Text, der den Gegenstand beschreibt, wird hier angezeigt.',''),('currencies',1,'description_loc6','Aqu├¡ puede encontrarse el texto que describe a este objeto.',''),('currencies',1,'description_loc8','ðùð┤ðÁÐüÐî ð¢ð░Ðàð¥ð┤ð©ÐéÐüÐÅ ð¥ð┐ð©Ðüð░ð¢ð©ðÁ ð┐ÐÇðÁð┤ð╝ðÁÐéð░.',''),('currencies',61,'description_loc0','Tiffany Cartier\'s shop in Dalaran will gladly accept these tokens for unique jewelcrafting recipes.',''),('currencies',61,'description_loc2','La boutique de Tiffany Kartier, ├á Dalaran, accepte avec joie ces marques contre des dessins de joaillerie uniques.',''),('currencies',61,'description_loc3','Tiffany Cartiers Gesch├ñft in Dalaran wird diese Symbole im Tausch gegen einzigartige Juweliersrezepte dankend annehmen.',''),('currencies',61,'description_loc4','Þ¥¥µïëþäÂþÜäÞÆéÕçíÕª«┬ÀÕìíÞÆéõ║Üõ╝ܵ¼úþäµÄÑÕÅùÞ┐Öõ║øõ╗úÕ©ü´╝îÕ╣Âþö¿þ¿Çµ£ëþÜäþÅáÕ«ØÕèáÕÀÑÕø¥Úë┤µØÑõ║ñµìóÒÇé',''),('currencies',61,'description_loc6','La tienda de Tiffany Cartier en Dalaran cambiar├í gustosamente estos talismanes por recetas de joyer├¡a.',''),('currencies',61,'description_loc8','ðÆ ð╝ð░ð│ð░ðÀð©ð¢ðÁ ðóð©ÐäÐäð░ð¢ð© ðÜð░ÐÇÐéÐîðÁ, ÐçÐéð¥ ð▓ ðöð░ð╗ð░ÐÇð░ð¢ðÁ, ð▓ð░ð╝ Ðü ÐÇð░ð┤ð¥ÐüÐéÐîÐÄ ð¥ð▒ð╝ðÁð¢ÐÅÐÄÐé ÐìÐéð© ðÀð¢ð░ð║ð© ð¢ð░ Ðâð¢ð©ð║ð░ð╗Ðîð¢ÐïðÁ ÐÄð▓ðÁð╗ð©ÐÇð¢ÐïðÁ ÐìÐüð║ð©ðÀÐï.',''),('currencies',81,'description_loc0','Visit special cooking vendors in Dalaran and the capital cities to to purchase unusual cooking recipes, spices, and even a fine hat!',''),('currencies',81,'description_loc2','Rendez visite aux marchands de fournitures de cuisine ├á Dalaran et dans les autres capitales pour acheter des recettes de cuisine sp├®ciales, des ├®pices, et m├¬me une superbe toque !',''),('currencies',81,'description_loc3','Besucht besondere Kochh├ñndler in Dalaran und den Hauptst├ñdten, um ungew├Âhnliche Kochrezepte, Gew├╝rze und sogar eine gro├ƒartige M├╝tze zu kaufen!',''),('currencies',81,'description_loc4','ÚÇáÞ«┐Þ¥¥µïëþäÂõ╗ÑÕÅèÕÉäõ©¬õ©╗ÕƒÄþÜäþë╣µ«èþâ╣ÚѬõ¥øÕ║öÕòå´╝îÞ┤¡õ╣░þ¢òÞºüþÜäþâ╣ÚѬÚàìµû╣ÒÇüÚªÖµûÖõ╗ÑÕÅèÕñºÕÄ¿þÜäÕ©¢Õ¡É´╝ü',''),('currencies',81,'description_loc6','Visita a los vendedores de cocina especiales de Dalaran y de las capitales para comprar recetas de cocina poco frecuentes, especias, ┬íe incluso un bonito gorro!',''),('currencies',81,'description_loc8','ðƒð¥ÐüðÁÐéð©ÐéðÁ Ðéð¥ÐÇð│ð¥ð▓ÐåðÁð▓ ð║Ðâð╗ð©ð¢ð░ÐÇð¢Ðïð╝ð© Ðéð¥ð▓ð░ÐÇð░ð╝ð© ð▓ ðöð░ð╗ð░ÐÇð░ð¢ðÁ ð© ð┤ÐÇÐâð│ð©Ðà ÐüÐéð¥ð╗ð©Ðåð░Ðà, ÐçÐéð¥ð▒Ðï ð┐ÐÇð©ð¥ð▒ÐÇðÁÐüÐéð© ð¥Ðüð¥ð▒ÐïðÁ ð║Ðâð╗ð©ð¢ð░ÐÇð¢ÐïðÁ ÐÇðÁÐåðÁð┐ÐéÐï, Ðüð┐ðÁÐåð©ð© ð© ð┤ð░ðÂðÁ ð│ð¥ð╗ð¥ð▓ð¢ð¥ð╣ Ðâð▒ð¥ÐÇ!',''),('currencies',241,'description_loc0','Awarded for valiant acts in the Crusader\'s Coliseum.',''),('currencies',241,'description_loc2','Obtenu en r├®compense dÔÇÖactes de bravoure au colis├®e des Crois├®s.',''),('currencies',241,'description_loc3','Werden f├╝r hehre Taten im Kolosseum der Kreuzfahrer verliehen.',''),('currencies',241,'description_loc4','Þí¿Õ¢░õ¢áÕ£¿ÕìüÕ¡ùÕåøµ╝öµ¡ªÕ£║õ©¡Õ▒òþñ║þÜ䵡ªÕïçÒÇé',''),('currencies',241,'description_loc6','Otorgado por las haza├▒as en el Coliseo de los Cruzados.',''),('currencies',241,'description_loc8','ðùð░ ÐàÐÇð░ð▒ÐÇð¥ÐüÐéÐî, ð┐ÐÇð¥ÐÅð▓ð╗ðÁð¢ð¢ÐâÐÄ ð¢ð░ ÐéÐâÐÇð¢ð©ÐÇð░Ðà ðÜð¥ð╗ð©ðÀðÁÐÅ ðÉð▓ð░ð¢ð│ð░ÐÇð┤ð░.',''),('currencies',181,'description_loc0','If you can read this, you\'ve found a bug. REPORT IT!',''),('currencies',181,'description_loc2','Si vous lisez ceci, c\'est un bug. SIGNALEZ-LE !',''),('currencies',181,'description_loc3','Wenn Ihr das hier lesen k├Ânnt, habt Ihr einen Bug gefunden. MELDET IHN!',''),('currencies',181,'description_loc6','Si puedes leer esto, has encontrado un error. ┬íInforma!',''),('currencies',181,'description_loc8','ðòÐüð╗ð© ð▓Ðï ð▓ð©ð┤ð©ÐéðÁ ÐìÐéð¥ Ðüð¥ð¥ð▒ÐëðÁð¢ð©ðÁ, ÐìÐéð¥ ðÀð¢ð░Ðçð©Ðé, ÐçÐéð¥ ð▓Ðï ð¥ð▒ð¢ð░ÐÇÐâðÂð©ð╗ð© ð¥Ðêð©ð▒ð║Ðâ. ðíð¥ð¥ð▒Ðëð©ÐéðÁ ð¥ ð¢ðÁð╣!',''),('currencies',103,'description_loc0','Used to purchase powerful PvP armor and weapons.',''),('currencies',103,'description_loc2','Utilis├®s pour acheter des armures et armes de JcJ puissantes.',''),('currencies',103,'description_loc3','K├Ânnen f├╝r den Erwerb von m├ñchtigen PVP-Waffen und -R├╝stungen verwendet werden.',''),('currencies',103,'description_loc4','þ½×µèÇÕ£║þé╣µò░µÿ»ÚÇÜÞ┐çÕ£¿þ½×µèÇÕ£║µêÿµûùõ©¡ÞÄÀÞâ£ÞÇîÞÁóÕ¥ùþÜäÒÇéõ¢áÕÅ»õ╗ѵÂêÞ┤╣Þ┐Öõ║øþé╣µò░µØÑÞ┤¡õ╣░Õ╝║ÕñºþÜäÕÑûÕè▒Õôü´╝ü',''),('currencies',103,'description_loc6','Se utilizan para comprar armas y armaduras de JcJ poderosas.',''),('currencies',103,'description_loc8','ðùð░ ÐìÐéð© ð¥Ðçð║ð© ð╝ð¥ðÂð¢ð¥ ð┐ð¥ð║Ðâð┐ð░ÐéÐî ð╝ð¥Ðëð¢ð¥ðÁ ð¥ÐÇÐâðÂð©ðÁ ð© ð┤ð¥Ðüð┐ðÁÐàð© ð┤ð╗ÐÅ PvP-ÐüÐÇð░ðÂðÁð¢ð©ð╣.',''),('currencies',104,'description_loc0','Used to purchase less-powerful PvP armor and weapons.',''),('currencies',104,'description_loc2','Utilis├®s pour acheter des armures et armes de JcJ moyennement puissantes.',''),('currencies',104,'description_loc3','K├Ânnen f├╝r den Erwerb von weniger m├ñchtigen PVP-Waffen und -R├╝stungen verwendet werden.',''),('currencies',104,'description_loc4','ÞìúÞ¬ëµÿ»ÚÇÜÞ┐çÕ£¿PvPµêÿµûùõ©¡ µØÇµ¡╗µòîÕ»╣ÚÿÁÞÉÑþÜäµêÉÕæÿÞÄÀÕ¥ùþÜäÒÇéõ¢áÕÅ»õ╗Ñõ¢┐þö¿ÞìúÞ¬ëþé╣µò░Þ┤¡õ╣░þë╣µ«èþÜäþë®ÕôüÒÇé',''),('currencies',104,'description_loc6','Se utilizan para comprar armas y armaduras de JcJ menos poderosas.',''),('currencies',104,'description_loc8','ðùð░ ÐìÐéð© ð¥Ðçð║ð© ð╝ð¥ðÂð¢ð¥ ð┐ð¥ð║Ðâð┐ð░ÐéÐî ð¢ðÁ ð¥ÐçðÁð¢Ðî ð╝ð¥Ðëð¢ð¥ðÁ ð¥ÐÇÐâðÂð©ðÁ ð© ð┤ð¥Ðüð┐ðÁÐàð© ð┤ð╗ÐÅ PvP-ÐüÐÇð░ðÂðÁð¢ð©ð╣.',''),('currencies',221,'description_loc0','Used to purchase less-powerful armor and weapons.',''),('currencies',221,'description_loc2','Utilis├®s pour acheter des armures et armes de JcJ moyennement puissantes.',''),('currencies',221,'description_loc3','K├Ânnen f├╝r den Erwerb von weniger m├ñchtigen Waffen und R├╝stungen verwendet werden.',''),('currencies',221,'description_loc6','Se utilizan para comprar armas y armaduras menos poderosas.',''),('currencies',221,'description_loc8','ðùð░ ÐìÐéð© ð¥Ðçð║ð© ð╝ð¥ðÂð¢ð¥ ð┐ð¥ð║Ðâð┐ð░ÐéÐî ð¢ðÁ ð¥ÐçðÁð¢Ðî ð╝ð¥Ðëð¢ð¥ðÁ ð¥ÐÇÐâðÂð©ðÁ ð© ð┤ð¥Ðüð┐ðÁÐàð©.',''),('currencies',341,'description_loc0','Used to purchase powerful PvE armor and weapons.',''),('currencies',341,'description_loc2','Utilis├®s pour acheter des armures et armes de JcE puissantes.',''),('currencies',341,'description_loc3','K├Ânnen f├╝r den Erwerb von m├ñchtigen PVE-Waffen und -R├╝stungen verwendet werden.',''),('currencies',341,'description_loc6','Se utilizan para comprar armas y armaduras de JcE poderosas.',''),('currencies',341,'description_loc8','ðùð░ ÐìÐéð© ð¥Ðçð║ð© ð╝ð¥ðÂð¢ð¥ ð┐ð¥ð║Ðâð┐ð░ÐéÐî ð╝ð¥Ðëð¢ð¥ðÁ ð¥ÐÇÐâðÂð©ðÁ ð© ð┤ð¥Ðüð┐ðÁÐàð© ð┤ð╗ÐÅ PvE-ÐüÐÇð░ðÂðÁð¢ð©ð╣.',''),('spell',9787,'reqSpellId','9787','Weaponsmith - requires itself'),('spell',9788,'reqSpellId','9788','Armorsmith - requires itself'),('spell',10656,'reqSpellId','10656','Dragonscale Leatherworking - requires itself'),('spell',10658,'reqSpellId','10658','Elemental Leatherworking - requires itself'),('spell',10660,'reqSpellId','10660','Tribal Leatherworking - requires itself'),('spell',17039,'reqSpellId','17039','Master Swordsmith - requires itself'),('spell',17040,'reqSpellId','17040','Master Hammersmith - requires itself'),('spell',17041,'reqSpellId','17041','Master Axesmith - requires itself'),('spell',20219,'reqSpellId','20219','Gnomish Engineer - requires itself'),('spell',20222,'reqSpellId','20222','Goblin Engineer - requires itself'),('spell',26797,'reqSpellId','26797','Spellfire Tailoring - requires itself'),('spell',26798,'reqSpellId','26798','Mooncloth Tailoring - requires itself'),('spell',26801,'reqSpellId','26801','Shadoweave Tailoring - requires itself'),('spell',379,'cuFLags','1073741824','Earth Shield - hide'),('spell',17567,'cuFLags','1073741824','Summon Blood Parrot - hide'),('spell',19483,'cuFLags','1073741824','Immolation - hide'),('spell',20154,'cuFLags','1073741824','Seal of Righteousness - hide'),('spell',21169,'cuFLags','1073741824','Reincarnation - hide'),('spell',22845,'cuFLags','1073741824','Frenzied Regeneration - hide'),('spell',23885,'cuFLags','1073741824','Bloodthirst - hide'),('spell',27813,'cuFLags','1073741824','Blessed Recovery - hide'),('spell',27817,'cuFLags','1073741824','Blessed Recovery - hide'),('spell',27818,'cuFLags','1073741824','Blessed Recovery - hide'),('spell',29442,'cuFLags','1073741824','Magic Absorption - hide'),('spell',29841,'cuFLags','1073741824','Second Wind - hide'),('spell',29842,'cuFLags','1073741824','Second Wind - hide'),('spell',29886,'cuFLags','1073741824','Create Soulwell - hide'),('spell',30708,'cuFLags','1073741824','Totem of Wrath - hide'),('spell',30874,'cuFLags','1073741824','Gift of the Water Spirit - hide'),('spell',31643,'cuFLags','1073741824','Blazing Speed - hide'),('spell',32841,'cuFLags','1073741824','Mass Resurrection - hide'),('spell',34919,'cuFLags','1073741824','Vampiric Touch - hide'),('spell',44450,'cuFLags','1073741824','Burnout - hide'),('spell',47633,'cuFLags','1073741824','Death Coil - hide'),('spell',48954,'cuFLags','1073741824','Swift Zhevra - hide'),('spell',49575,'cuFLags','1073741824','Death Grip - hide'),('spell',50536,'cuFLags','1073741824','Unholy Blight - hide'),('spell',52374,'cuFLags','1073741824','Blood Strike - hide'),('spell',56816,'cuFLags','1073741824','Rune Strike - hide'),('spell',58427,'cuFLags','1073741824','Overkill - hide'),('spell',58889,'cuFLags','1073741824','Create Soulwell - hide'),('spell',64380,'cuFLags','1073741824','Shattering Throw - hide'),('spell',66122,'cuFLags','1073741824','Magic Rooster - hide'),('spell',66123,'cuFLags','1073741824','Magic Rooster - hide'),('spell',66124,'cuFLags','1073741824','Magic Rooster - hide'),('spell',66175,'cuFLags','1073741824','Macabre Marionette - hide'),('spell',54910,'cuFLags','1073741824','Glyph of the Red Lynx - hide unused glyph'),('spell',57231,'cuFLags','1073741824','Death Knight Glyph 25 - hide unused glyph'),('spell',58166,'cuFLags','1073741824','Glyph of the Forest Lynx - hide unused glyph'),('spell',58239,'cuFLags','1073741824','Glyph of the Penguin - hide unused glyph'),('spell',58240,'cuFLags','1073741824','Glyph of the Bear Cub - hide unused glyph'),('spell',58261,'cuFLags','1073741824','Glyph of the Arctic Wolf - hide unused glyph'),('spell',58262,'cuFLags','1073741824','Glyph of the Black Wolf - hide unused glyph'),('spell',60460,'cuFLags','1073741824','Glyph of Raise Dead - hide unused glyph'),('spell',54910,'skillLine1','0','Glyph of the Red Lynx - hide unused glyph'),('spell',57231,'skillLine1','0','Death Knight Glyph 25 - hide unused glyph'),('spell',58166,'skillLine1','0','Glyph of the Forest Lynx - hide unused glyph'),('spell',58239,'skillLine1','0','Glyph of the Penguin - hide unused glyph'),('spell',58240,'skillLine1','0','Glyph of the Bear Cub - hide unused glyph'),('spell',58261,'skillLine1','0','Glyph of the Arctic Wolf - hide unused glyph'),('spell',58262,'skillLine1','0','Glyph of the Black Wolf - hide unused glyph'),('spell',60460,'skillLine1','0','Glyph of Raise Dead - hide unused glyph'),('spell',54910,'iconIdAlt','0','Glyph of the Red Lynx - hide unused glyph'),('spell',57231,'iconIdAlt','0','Death Knight Glyph 25 - hide unused glyph'),('spell',58166,'iconIdAlt','0','Glyph of the Forest Lynx - hide unused glyph'),('spell',58239,'iconIdAlt','0','Glyph of the Penguin - hide unused glyph'),('spell',58240,'iconIdAlt','0','Glyph of the Bear Cub - hide unused glyph'),('spell',58261,'iconIdAlt','0','Glyph of the Arctic Wolf - hide unused glyph'),('spell',58262,'iconIdAlt','0','Glyph of the Black Wolf - hide unused glyph'),('spell',60460,'iconIdAlt','0','Glyph of Raise Dead - hide unused glyph'),('quests',9572,'questSortId','3562','Weaken the Ramparts - category Hellfire Citadel -> Hellfire Ramparts'),('quests',9575,'questSortId','3562','Weaken the Ramparts - category Hellfire Citadel -> Hellfire Ramparts'),('quests',11354,'questSortId','3562','Wanted: Nazan\'s Riding Crop - category Hellfire Citadel -> Hellfire Ramparts'),('quests',9589,'questSortId','3713','The Blood is Life - category Hellfire Citadel -> Blood Furnace'),('quests',9590,'questSortId','3713','The Blood is Life - category Hellfire Citadel -> Blood Furnace'),('quests',9607,'questSortId','3713','Heart of Rage - category Hellfire Citadel -> Blood Furnace'),('quests',9608,'questSortId','3713','Heart of Rage - category Hellfire Citadel -> Blood Furnace'),('quests',11362,'questSortId','3713','Wanted: Keli\'dan\'s Feathered Stave - category Hellfire Citadel -> Blood Furnace'),('quests',9492,'questSortId','3714','Turning the Tide - category Hellfire Citadel -> Shattered Halls'),('quests',9493,'questSortId','3714','Pride of the Fel Horde - category Hellfire Citadel -> Shattered Halls'),('quests',9494,'questSortId','3714','Fel Embers - category Hellfire Citadel -> Shattered Halls'),('quests',9495,'questSortId','3714','The Will of the Warchief - category Hellfire Citadel -> Shattered Halls'),('quests',9496,'questSortId','3714','Pride of the Fel Horde - category Hellfire Citadel -> Shattered Halls'),('quests',9497,'questSortId','3714','Emblem of the Fel Horde - category Hellfire Citadel -> Shattered Halls'),('quests',9524,'questSortId','3714','Imprisoned in the Citadel - category Hellfire Citadel -> Shattered Halls'),('quests',9525,'questSortId','3714','Imprisoned in the Citadel - category Hellfire Citadel -> Shattered Halls'),('quests',11363,'questSortId','3714','Wanted: Bladefist\'s Seal - category Hellfire Citadel -> Shattered Halls'),('quests',11364,'questSortId','3714','Wanted: Shattered Hand Centurions - category Hellfire Citadel -> Shattered Halls'),('pet',30,'expansion','1','Pet - Dragonhawk: BC'),('pet',31,'expansion','1','Pet - Ravager: BC'),('pet',32,'expansion','1','Pet - Warp Stalker: BC'),('pet',33,'expansion','1','Pet - Sporebat: BC'),('pet',34,'expansion','1','Pet - Nether Ray: BC'),('pet',37,'expansion','2','Pet - Moth: WotLK'),('pet',38,'expansion','2','Pet - Chimaera: WotLK'),('pet',39,'expansion','2','Pet - Devilsaur: WotLK'),('pet',41,'expansion','2','Pet - Silithid: WotLK'),('pet',42,'expansion','2','Pet - Worm: WotLK'),('pet',43,'expansion','2','Pet - Rhino: WotLK'),('pet',44,'expansion','2','Pet - Wasp: WotLK'),('pet',45,'expansion','2','Pet - Core Hound: WotLK'),('pet',46,'expansion','2','Pet - Spirit Beast: WotLK'),('spell',17579,'cuFlags','1610612736','Alchemy: Greater Holy Protection Potion - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',54020,'cuFlags','1610612736','Alchemy: Transmute: Eternal Might - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',2336,'cuFlags','1610612736','Alchemy: Elixir of Tongues - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',13460,'cuFlags','1610612736','Greater Holy Protection Potion - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',40248,'cuFlags','1610612736','Eternal Might - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',2460,'cuFlags','1610612736','Elixir of Tongues - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',8366,'cuFlags','1610612736','Blacksmithing: Ironforge Chain - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',2671,'cuFlags','1610612736','Blacksmithing: Rough Bronze Bracers - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',8368,'cuFlags','1610612736','Blacksmithing: Ironforge Gauntlets - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',9942,'cuFlags','1610612736','Blacksmithing: Mithril Scale Gloves - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',16960,'cuFlags','1610612736','Blacksmithing: Thorium Greatsword - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',16965,'cuFlags','1610612736','Blacksmithing: Bleakwood Hew - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',16967,'cuFlags','1610612736','Blacksmithing: Inlaid Thorium Hammer - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',16980,'cuFlags','1610612736','Blacksmithing: Rune Edge - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',28244,'cuFlags','536870912','Blacksmithing: Icebane Bracers - set: CUSTOM_UNAVAILABLE'),('spell',28242,'cuFlags','536870912','Blacksmithing: Icebane Breastplate - set: CUSTOM_UNAVAILABLE'),('spell',28243,'cuFlags','536870912','Blacksmithing: Icebane Gauntlets - set: CUSTOM_UNAVAILABLE'),('spell',16986,'cuFlags','1610612736','Blacksmithing: Blood Talon - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',16987,'cuFlags','1610612736','Blacksmithing: Darkspear - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',2867,'cuFlags','1610612736','Rough Bronze Bracers - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',6730,'cuFlags','1610612736','Ironforge Chain - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',6733,'cuFlags','1610612736','Ironforge Gauntlets - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',7925,'cuFlags','1610612736','Mithril Scale Gloves - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',12764,'cuFlags','1610612736','Thorium Greatsword - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',12769,'cuFlags','1610612736','Bleakwood Hew - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',12772,'cuFlags','1610612736','Inlaid Thorium Hammer - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',12779,'cuFlags','1610612736','Rune Edge - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',12795,'cuFlags','1610612736','Blood Talon - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',12802,'cuFlags','1610612736','Darkspear - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',22671,'cuFlags','536870912','Icebane Bracers - set: CUSTOM_UNAVAILABLE'),('items',22669,'cuFlags','536870912','Icebane Breastplate - set: CUSTOM_UNAVAILABLE'),('items',22670,'cuFlags','536870912','Icebane Gauntlets - set: CUSTOM_UNAVAILABLE'),('spell',28021,'cuFlags','1610612736','Enchanting: Arcane Dust - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',44612,'cuFlags','1610612736','Enchanting: Enchant Gloves - Greater Blasting - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',62257,'cuFlags','1610612736','Enchanting: Enchant Weapon - Titanguard - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',38985,'cuFlags','1610612736','Scroll of Enchant Gloves - Greater Blasting - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',44946,'cuFlags','1610612736','Scroll of Enchant Weapon - Titanguard - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',44945,'cuFlags','1610612736','Formula: Enchant Weapon - Titanguard - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',30549,'cuFlags','1610612736','Engineering: Critter Enlarger - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',67790,'cuFlags','1610612736','Engineering: Dimensional Folder: K3 - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',30343,'cuFlags','1610612736','Engineering: Blue Smoke Flare - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',30342,'cuFlags','1610612736','Engineering: Red Smoke Flare - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',30561,'cuFlags','1610612736','Engineering: Goblin Tonk Controller - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',30573,'cuFlags','1610612736','Engineering: Gnomish Tonk Controller - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',12722,'cuFlags','1610612736','Engineering: Goblin Radio - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',12904,'cuFlags','1610612736','Engineering: Gnomish Ham Radio - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',12720,'cuFlags','1610612736','Engineering: Goblin \"Boom\" Box - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',12900,'cuFlags','1610612736','Engineering: Mobile Alarm- set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',23882,'cuFlags','1610612736','Schematic: Critter Enlarger - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',23820,'cuFlags','1610612736','Critter Enlarger - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',48933,'cuFlags','1610612736','Dimensional Folder: K3 - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',23770,'cuFlags','1610612736','Blue Smoke Flare - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',23769,'cuFlags','1610612736','Red Smoke Flare - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',23831,'cuFlags','1610612736','Goblin Tonk Controller - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',23832,'cuFlags','1610612736','Gnomish Tonk Controller - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',10585,'cuFlags','1610612736','Goblin Radio - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',10723,'cuFlags','1610612736','Gnomish Ham Radio - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',10580,'cuFlags','1610612736','Goblin \"Boom\" Box - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',10719,'cuFlags','1610612736','Mobile Alarm - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',8387,'cuFlags','1610612736','Herbalism: Find Herbs - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',2369,'cuFlags','1610612736','Herbalism: Herb Gathering - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',2371,'cuFlags','1610612736','Herbalism: Herb Gathering - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',52175,'cuFlags','1610612736','Inscription: Decipher - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',25614,'cuFlags','1610612736','Jewelcrafting: Silver Rose Pendant - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',32810,'cuFlags','1610612736','Jewelcrafting: Primal Stone Statue - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',26918,'cuFlags','1610612736','Jewelcrafting: Arcanite Sword Pendant - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',26920,'cuFlags','1610612736','Jewelcrafting: Blood Crown - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',20956,'cuFlags','1610612736','Silver Rose Pendant - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',25884,'cuFlags','1610612736','Primal Stone Statue - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',21793,'cuFlags','1610612736','Arcanite Sword Pendant - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',21780,'cuFlags','1610612736','Blood Crown - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',10550,'cuFlags','1610612736','Leatherworking: Nightscape Cloak - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',28224,'cuFlags','536870912','Leatherworking: Icy Scale Bracers - set: CUSTOM_UNAVAILABLE'),('spell',28222,'cuFlags','536870912','Leatherworking: Icy Scale Breastplate - set: CUSTOM_UNAVAILABLE'),('spell',28223,'cuFlags','536870912','Leatherworking: Icy Scale Gauntlets - set: CUSTOM_UNAVAILABLE'),('spell',28221,'cuFlags','536870912','Leatherworking: Polar Bracers - set: CUSTOM_UNAVAILABLE'),('spell',28220,'cuFlags','536870912','Leatherworking: Polar Gloves - set: CUSTOM_UNAVAILABLE'),('spell',28219,'cuFlags','536870912','Leatherworking: Polar Tunic - set: CUSTOM_UNAVAILABLE'),('spell',55243,'cuFlags','1610612736','Leatherworking: Bracers of Deflection - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',19106,'cuFlags','536870912','Leatherworking: Onyxia Scale Breastplate - set: CUSTOM_UNAVAILABLE'),('items',8195,'cuFlags','1610612736','Nightscape Cloak - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',22665,'cuFlags','536870912','Icy Scale Bracers - set: CUSTOM_UNAVAILABLE'),('items',22664,'cuFlags','536870912','Icy Scale Breastplate - set: CUSTOM_UNAVAILABLE'),('items',22666,'cuFlags','536870912','Icy Scale Gauntlets - set: CUSTOM_UNAVAILABLE'),('items',22663,'cuFlags','536870912','Polar Bracers - set: CUSTOM_UNAVAILABLE'),('items',22662,'cuFlags','536870912','Polar Gloves - set: CUSTOM_UNAVAILABLE'),('items',22661,'cuFlags','536870912','Polar Tunic - set: CUSTOM_UNAVAILABLE'),('items',41264,'cuFlags','1610612736','Bracers of Deflection - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',15141,'cuFlags','536870912','Onyxia Scale Breastplate - set: CUSTOM_UNAVAILABLE'),('spell',8388,'cuFlags','1610612736','Mining: Find Minerals - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',7636,'cuFlags','1610612736','Tailoring: Green Woolen Robe - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',8778,'cuFlags','1610612736','Tailoring: Boots of Darkness - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',12063,'cuFlags','1610612736','Tailoring: Stormcloth Gloves - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',12062,'cuFlags','1610612736','Tailoring: Stormcloth Pants - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',12068,'cuFlags','1610612736','Tailoring: Stormcloth Vest - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',12083,'cuFlags','1610612736','Tailoring: Stormcloth Headband - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',12087,'cuFlags','1610612736','Tailoring: Stormcloth Shoulders - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',12090,'cuFlags','1610612736','Tailoring: Stormcloth Boots - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',28208,'cuFlags','536870912','Tailoring: Glacial Cloak - set: CUSTOM_UNAVAILABLE'),('spell',28205,'cuFlags','536870912','Tailoring: Glacial Gloves - set: CUSTOM_UNAVAILABLE'),('spell',28207,'cuFlags','536870912','Tailoring: Glacial Vest - set: CUSTOM_UNAVAILABLE'),('spell',28209,'cuFlags','536870912','Tailoring: Glacial Wrists - set: CUSTOM_UNAVAILABLE'),('spell',36670,'cuFlags','1610612736','Tailoring: Lifeblood Belt - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',36672,'cuFlags','1610612736','Tailoring: Lifeblood Bracers - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',36669,'cuFlags','1610612736','Tailoring: Lifeblood Leggings - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',36667,'cuFlags','1610612736','Tailoring: Netherflame Belt - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',36668,'cuFlags','1610612736','Tailoring: Netherflame Boots - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',36665,'cuFlags','1610612736','Tailoring: Netherflame Robe - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',56048,'cuFlags','1610612736','Tailoring: Duskweave Boots - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('spell',31461,'cuFlags','1610612736','Tailoring: Heavy Netherweave Net - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',6243,'cuFlags','1610612736','Green Woolen Robe - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',7027,'cuFlags','1610612736','Boots of Darkness - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',10011,'cuFlags','1610612736','Stormcloth Gloves - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',10010,'cuFlags','1610612736','Stormcloth Pants - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',10020,'cuFlags','1610612736','Stormcloth Vest - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',10032,'cuFlags','1610612736','Stormcloth Headband - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',10038,'cuFlags','1610612736','Stormcloth Shoulders - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',10039,'cuFlags','1610612736','Stormcloth Boots - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',22658,'cuFlags','536870912','Glacial Cloak - set: CUSTOM_UNAVAILABLE'),('items',22654,'cuFlags','536870912','Glacial Gloves - set: CUSTOM_UNAVAILABLE'),('items',22652,'cuFlags','536870912','Glacial Vest - set: CUSTOM_UNAVAILABLE'),('items',22655,'cuFlags','536870912','Glacial Wrists - set: CUSTOM_UNAVAILABLE'),('items',30463,'cuFlags','1610612736','Lifeblood Belt - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',30464,'cuFlags','1610612736','Lifeblood Bracers - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',30465,'cuFlags','1610612736','Lifeblood Leggings - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',30460,'cuFlags','1610612736','Netherflame Belt - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',30461,'cuFlags','1610612736','Netherflame Boots - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',30459,'cuFlags','1610612736','Netherflame Robe - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',41544,'cuFlags','1610612736','Duskweave Boots - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'),('items',24269,'cuFlags','1610612736','Heavy Netherweave Net - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'); /*!40000 ALTER TABLE `aowow_setup_custom_data` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; diff --git a/setup/sql/04-db_optional_mysql_only.sql b/setup/sql/04-db_optional_mysql_only.sql new file mode 100644 index 00000000..80561cb0 --- /dev/null +++ b/setup/sql/04-db_optional_mysql_only.sql @@ -0,0 +1,5 @@ +ALTER TABLE `aowow_creature` ADD FULLTEXT `idx_ft_name4` (`name_loc4`) WITH PARSER ngram; +ALTER TABLE `aowow_items` ADD FULLTEXT `idx_ft_name4` (`name_loc4`) WITH PARSER ngram; +ALTER TABLE `aowow_objects` ADD FULLTEXT `idx_ft_name4` (`name_loc4`) WITH PARSER ngram; +ALTER TABLE `aowow_quests` ADD FULLTEXT `idx_ft_name4` (`name_loc4`) WITH PARSER ngram; +ALTER TABLE `aowow_spell` ADD FULLTEXT `idx_ft_name4` (`name_loc4`) WITH PARSER ngram; diff --git a/setup/sql/updates/1759504522_01.sql b/setup/sql/updates/1759504522_01.sql new file mode 100644 index 00000000..12872d0e --- /dev/null +++ b/setup/sql/updates/1759504522_01.sql @@ -0,0 +1,11 @@ +ALTER TABLE aowow_profiler_completion_quests + ADD KEY `typeId` (`questId`); + +ALTER TABLE aowow_profiler_completion_reputation + ADD KEY `typeId` (`factionId`); + +ALTER TABLE aowow_profiler_completion_spells + ADD KEY `typeId` (`spellId`); + +ALTER TABLE aowow_profiler_completion_titles + ADD KEY `typeId` (`titleId`); diff --git a/setup/sql/updates/1759504522_02.sql b/setup/sql/updates/1759504522_02.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1759504522_02.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/sql/updates/1760300362_01.sql b/setup/sql/updates/1760300362_01.sql new file mode 100644 index 00000000..bef0e1a9 --- /dev/null +++ b/setup/sql/updates/1760300362_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' source'); diff --git a/setup/sql/updates/1760557948_01.sql b/setup/sql/updates/1760557948_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1760557948_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/sql/updates/1760911493_01.sql b/setup/sql/updates/1760911493_01.sql new file mode 100644 index 00000000..b4f362f0 --- /dev/null +++ b/setup/sql/updates/1760911493_01.sql @@ -0,0 +1,7 @@ +ALTER TABLE `aowow_profiler_pets` + MODIFY COLUMN `talents` varchar(22) DEFAULT NULL; + +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' talenticons talentcalc'); + +-- flag all hunters as requiring update +UPDATE `aowow_profiler_profiles` SET `flags` = `flags` | 16, `lastupdated` = 0 WHERE `class` = 3 AND `realmGUID` IS NOT NULL; diff --git a/setup/sql/updates/1760979519_01.sql b/setup/sql/updates/1760979519_01.sql new file mode 100644 index 00000000..c0c38b25 --- /dev/null +++ b/setup/sql/updates/1760979519_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spawns'); diff --git a/setup/sql/updates/1760979719_01.sql b/setup/sql/updates/1760979719_01.sql new file mode 100644 index 00000000..c2bbcf7a --- /dev/null +++ b/setup/sql/updates/1760979719_01.sql @@ -0,0 +1,8 @@ +ALTER TABLE `aowow_factions` + DROP COLUMN `baseRepValue3`, + DROP COLUMN `baseRepValue4`, + ADD COLUMN `baseRepValue3` mediumint(9) NOT NULL AFTER `baseRepValue2`, + ADD COLUMN `baseRepValue4` mediumint(9) NOT NULL AFTER `baseRepValue3` +; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' factions'); diff --git a/setup/sql/updates/1761145594_01.sql b/setup/sql/updates/1761145594_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1761145594_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/sql/updates/1761148832_01.sql b/setup/sql/updates/1761148832_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1761148832_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/sql/updates/1762199391_01.sql b/setup/sql/updates/1762199391_01.sql new file mode 100644 index 00000000..168a5f1b --- /dev/null +++ b/setup/sql/updates/1762199391_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs tooltips'); diff --git a/setup/sql/updates/1762352733_01.sql b/setup/sql/updates/1762352733_01.sql new file mode 100644 index 00000000..203b6646 --- /dev/null +++ b/setup/sql/updates/1762352733_01.sql @@ -0,0 +1,3 @@ +ALTER TABLE `aowow_items` + ADD KEY spellId1 (`spellId1`), + ADD KEY spellId2 (`spellId2`); diff --git a/setup/sql/updates/1762543652_01.sql b/setup/sql/updates/1762543652_01.sql new file mode 100644 index 00000000..2e3027b3 --- /dev/null +++ b/setup/sql/updates/1762543652_01.sql @@ -0,0 +1,18 @@ +ALTER TABLE `aowow_spell` + ADD KEY reagent1 (`reagent1`), + ADD KEY reagent2 (`reagent2`), + ADD KEY reagent3 (`reagent3`), + ADD KEY reagent4 (`reagent4`), + ADD KEY reagent5 (`reagent5`), + ADD KEY reagent6 (`reagent6`), + ADD KEY reagent7 (`reagent7`), + ADD KEY reagent8 (`reagent8`), + ADD KEY effect1CreateItemId (`effect1CreateItemId`), + ADD KEY effect2CreateItemId (`effect2CreateItemId`), + ADD KEY effect3CreateItemId (`effect3CreateItemId`), + ADD KEY effect1Id (`effect1Id`), + ADD KEY effect2Id (`effect2Id`), + ADD KEY effect3Id (`effect3Id`), + ADD KEY effect1AuraId (`effect1AuraId`), + ADD KEY effect2AuraId (`effect2AuraId`), + ADD KEY effect3AuraId (`effect3AuraId`); diff --git a/setup/sql/updates/1762629696_01.sql b/setup/sql/updates/1762629696_01.sql new file mode 100644 index 00000000..40beca4d --- /dev/null +++ b/setup/sql/updates/1762629696_01.sql @@ -0,0 +1,18 @@ +ALTER TABLE aowow_profiler_profiles + ADD COLUMN `custom` tinyint(1) DEFAULT 0 COMMENT 'custom profile' AFTER `cuFlags`, + ADD COLUMN `stub` tinyint(1) DEFAULT 0 COMMENT 'character stub needs resync' AFTER `custom`, + ADD COLUMN `deleted` tinyint(1) DEFAULT 0 COMMENT 'only on custom profiles' AFTER `stub`, + ADD KEY `idx_custom` (`custom`), + ADD KEY `idx_stub` (`stub`), + ADD KEY `idx_deleted` (`deleted`) +; + +ALTER TABLE aowow_profiler_arena_team + ADD COLUMN `stub` tinyint(1) DEFAULT 0 COMMENT 'arena team stub needs resync' AFTER `cuFlags`, + ADD KEY `idx_stub` (`stub`) +; + +ALTER TABLE aowow_profiler_guild + ADD COLUMN `stub` tinyint(1) DEFAULT 0 COMMENT 'guild stub needs resync' AFTER `cuFlags`, + ADD KEY `idx_stub` (`stub`) +; diff --git a/setup/sql/updates/1762629696_02.sql b/setup/sql/updates/1762629696_02.sql new file mode 100644 index 00000000..c9f18965 --- /dev/null +++ b/setup/sql/updates/1762629696_02.sql @@ -0,0 +1,10 @@ +UPDATE aowow_profiler_profiles SET `deleted` = 1 WHERE `cuFlags` & 4; +UPDATE aowow_profiler_profiles SET `custom` = 1 WHERE `cuFlags` & 8; +UPDATE aowow_profiler_profiles SET `stub` = 1 WHERE `cuFlags` & 16; +UPDATE aowow_profiler_profiles SET `cuFlags` = `cuFlags` & ~(4 | 8 | 16); + +UPDATE aowow_profiler_arena_team SET `stub` = 1 WHERE `cuFlags` & 16; +UPDATE aowow_profiler_arena_team SET `cuFlags` = `cuFlags` & ~16; + +UPDATE aowow_profiler_guild SET `stub` = 1 WHERE `cuFlags` & 16; +UPDATE aowow_profiler_guild SET `cuFlags` = `cuFlags` & ~16; diff --git a/setup/sql/updates/1762700147_01.sql b/setup/sql/updates/1762700147_01.sql new file mode 100644 index 00000000..dd011d10 --- /dev/null +++ b/setup/sql/updates/1762700147_01.sql @@ -0,0 +1,4 @@ +ALTER TABLE aowow_profiler_completion_reputation + ADD COLUMN `exalted` tinyint(1) GENERATED ALWAYS AS (`standing` >= 42000) STORED AFTER `standing`, + ADD KEY idx_exalted (`exalted`) +; diff --git a/setup/sql/updates/1763168697_01.sql b/setup/sql/updates/1763168697_01.sql new file mode 100644 index 00000000..bef0e1a9 --- /dev/null +++ b/setup/sql/updates/1763168697_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' source'); diff --git a/setup/sql/updates/1763200071_01.sql b/setup/sql/updates/1763200071_01.sql new file mode 100644 index 00000000..27b16cdf --- /dev/null +++ b/setup/sql/updates/1763200071_01.sql @@ -0,0 +1,22 @@ +UPDATE `aowow_pet` SET `expansion` = 1 WHERE `id` IN (30, 31, 32, 33, 34); +UPDATE `aowow_pet` SET `expansion` = 2 WHERE `id` IN (37, 38, 39, 41, 42, 43, 44, 45, 46); + +DELETE FROM `aowow_setup_custom_data` WHERE `command` = 'pet' AND `field` = 'expansion'; +INSERT INTO `aowow_setup_custom_data` VALUES + ('pet', 30, 'expansion', 1, 'Pet - Dragonhawk: BC'), + ('pet', 31, 'expansion', 1, 'Pet - Ravager: BC'), + ('pet', 32, 'expansion', 1, 'Pet - Warp Stalker: BC'), + ('pet', 33, 'expansion', 1, 'Pet - Sporebat: BC'), + ('pet', 34, 'expansion', 1, 'Pet - Nether Ray: BC'), + ('pet', 37, 'expansion', 2, 'Pet - Moth: WotLK'), + ('pet', 38, 'expansion', 2, 'Pet - Chimaera: WotLK'), + ('pet', 39, 'expansion', 2, 'Pet - Devilsaur: WotLK'), + ('pet', 41, 'expansion', 2, 'Pet - Silithid: WotLK'), + ('pet', 42, 'expansion', 2, 'Pet - Worm: WotLK'), + ('pet', 43, 'expansion', 2, 'Pet - Rhino: WotLK'), + ('pet', 44, 'expansion', 2, 'Pet - Wasp: WotLK'), + ('pet', 45, 'expansion', 2, 'Pet - Core Hound: WotLK'), + ('pet', 46, 'expansion', 2, 'Pet - Spirit Beast: WotLK') +; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' pet'); diff --git a/setup/sql/updates/1763240934_01.sql b/setup/sql/updates/1763240934_01.sql new file mode 100644 index 00000000..c0c38b25 --- /dev/null +++ b/setup/sql/updates/1763240934_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spawns'); diff --git a/setup/sql/updates/1763555620_01.sql b/setup/sql/updates/1763555620_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1763555620_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/sql/updates/1763557620_01.sql b/setup/sql/updates/1763557620_01.sql new file mode 100644 index 00000000..f7f6787e --- /dev/null +++ b/setup/sql/updates/1763557620_01.sql @@ -0,0 +1,42 @@ +DROP TABLE IF EXISTS `aowow_objectdifficulty`; +CREATE TABLE `aowow_objectdifficulty` ( + `normal10` mediumint(8) unsigned NOT NULL, + `normal25` mediumint(8) unsigned NOT NULL, + `heroic10` mediumint(8) unsigned NOT NULL, + `heroic25` mediumint(8) unsigned NOT NULL, + `mapType` tinyint(3) unsigned NOT NULL, + KEY `normal10` (`normal10`), + KEY `normal25` (`normal25`), + KEY `heroic10` (`heroic10`), + KEY `heroic25` (`heroic25`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO `aowow_objectdifficulty` VALUES + (181366, 193426, 0, 0 , 2), -- naxxramas: four horsemen chest + (193905, 193967, 0, 0 , 2), -- eoe: alexstrasza's gift + (194307, 194308, 194200, 194201, 2), -- ulduar: cache of winter + (194312, 194314, 194313, 194315, 2), -- ulduar: cache of storms + (194324, 194328, 194325, 194329, 2), -- ulduar: freya's gift +1 elder + (194324, 194328, 194326, 194330, 2), -- ulduar: freya's gift +2 elder + (194324, 194328, 194327, 194331, 2), -- ulduar: freya's gift +3 elder + (194789, 194956, 194957, 194958, 2), -- ulduar: cache of innovation + (194821, 194822, 0, 0 , 2), -- ulduar: gift of the observer + (195046, 195047, 0, 0 , 2), -- ulduar: cache of living stone + (195631, 195632, 195633, 195635, 2), -- toc25: champions' cache + (202178, 202180, 202177, 202179, 2), -- icc: gunship armory (horde) + (201873, 201874, 201872, 201875, 2), -- icc: gunship armory (alliance) + (202239, 202240, 202238, 202241, 2), -- icc: deathbringer's cache + (201959, 202339, 202338, 202340, 2), -- icc: cache of the dreamwalker + (0, 0, 195668, 195672, 2), -- toc25: argent crusade tribute chest 1TL + (0, 0, 195667, 195671, 2), -- toc25: argent crusade tribute chest 25TL + (0, 0, 195666, 195670, 2), -- toc25: argent crusade tribute chest 45TL + (0, 0, 195665, 195669, 2), -- toc25: argent crusade tribute chest 50TL + (185168, 185169, 0, 0 , 1), -- hellfire ramparts: reinforced fel iron chest + (184465, 184849, 0, 0 , 1), -- mechanar: cache of the legion + (190586, 193996, 0, 0 , 1), -- halls of stone: tribunal chest + (190663, 193597, 0, 0 , 1), -- cot - cos: dark runed chest + (191349, 193603, 0, 0 , 1), -- oculus: cache of eregos + (195709, 195710, 0, 0 , 1), -- toc5: champion's cache + (195323, 195324, 0, 0 , 1), -- toc5: confessor's cache + (195374, 195375, 0, 0 , 1), -- toc5: eadric's cache + (201710, 202336, 0, 0 , 1); -- hor: captain's chest diff --git a/setup/sql/updates/1763557620_02.sql b/setup/sql/updates/1763557620_02.sql new file mode 100644 index 00000000..fb068d31 --- /dev/null +++ b/setup/sql/updates/1763557620_02.sql @@ -0,0 +1,18 @@ +ALTER TABLE `aowow_spelldifficulty` + ADD COLUMN `mapType` tinyint(3) unsigned NOT NULL AFTER `heroic25` +; + +-- move linked chest for icc: gunship battle. duplicate saurfang to muradin +DELETE FROM `aowow_loot_link` WHERE `npcId` IN (36939, 38156, 38637, 38638, 36948, 38157, 38639, 38640); +INSERT INTO `aowow_loot_link` (`npcId`, `objectId`, `difficulty`, `priority`, `encounterId`) VALUES + (36939, 201873, 1, 0, 847), + (38156, 201874, 2, 0, 847), + (38637, 201872, 3, 0, 847), + (38638, 201875, 4, 0, 847), + (36948, 202178, 1, 0, 847), + (38157, 202180, 2, 0, 847), + (38639, 202177, 3, 0, 847), + (38640, 202179, 4, 0, 847) +; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' source spelldifficulty'); diff --git a/setup/sql/updates/1763580264_01.sql b/setup/sql/updates/1763580264_01.sql new file mode 100644 index 00000000..4b88130c --- /dev/null +++ b/setup/sql/updates/1763580264_01.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_account_reputation` + MODIFY COLUMN `amount` tinyint(3) signed NOT NULL; diff --git a/setup/sql/updates/1763677664_01.sql b/setup/sql/updates/1763677664_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1763677664_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/sql/updates/1763760598_01.sql b/setup/sql/updates/1763760598_01.sql new file mode 100644 index 00000000..6ec5e09b --- /dev/null +++ b/setup/sql/updates/1763760598_01.sql @@ -0,0 +1,2 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spell'); diff --git a/setup/sql/updates/1763850348_01.sql b/setup/sql/updates/1763850348_01.sql new file mode 100644 index 00000000..c0c38b25 --- /dev/null +++ b/setup/sql/updates/1763850348_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' spawns'); diff --git a/setup/sql/updates/1764273019_01.sql b/setup/sql/updates/1764273019_01.sql new file mode 100644 index 00000000..9fa47c05 --- /dev/null +++ b/setup/sql/updates/1764273019_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' stats'); diff --git a/setup/sql/updates/1764691622_01.sql b/setup/sql/updates/1764691622_01.sql new file mode 100644 index 00000000..8c20c87c --- /dev/null +++ b/setup/sql/updates/1764691622_01.sql @@ -0,0 +1,4 @@ +ALTER TABLE aowow_creature + ADD COLUMN `schoolImmuneMask` int(10) unsigned NOT NULL DEFAULT 0 AFTER `mechanicImmuneMask`; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' creature'); diff --git a/setup/sql/updates/1764798161_01.sql b/setup/sql/updates/1764798161_01.sql new file mode 100644 index 00000000..0688d1f0 --- /dev/null +++ b/setup/sql/updates/1764798161_01.sql @@ -0,0 +1,7 @@ +ALTER TABLE aowow_icons + ADD COLUMN `name_source` varchar(55) NOT NULL AFTER `name`; + +UPDATE `aowow_dbversion` SET + `sql` = CONCAT(IFNULL(`sql`, ''), ' icons races classes holidays'), + `build` = CONCAT(IFNULL(`build`, ''), ' simpleimg') +; diff --git a/setup/sql/updates/1764798161_02.sql b/setup/sql/updates/1764798161_02.sql new file mode 100644 index 00000000..139dd8be --- /dev/null +++ b/setup/sql/updates/1764798161_02.sql @@ -0,0 +1,19 @@ +-- drop obsolete custom data for holiday icons +DELETE FROM aowow_setup_custom_data WHERE `command` = 'holidays' AND `field` = 'iconString'; +UPDATE aowow_holidays SET `iconString` = ''; + +-- support calendar_* icons +ALTER TABLE aowow_holidays + CHANGE COLUMN `iconString` `iconId` smallint(5) unsigned NOT NULL DEFAULT 0 +; + +-- support class_* icons +ALTER TABLE aowow_classes + ADD COLUMN `iconId` smallint(5) unsigned NOT NULL DEFAULT 0 AFTER `fileString` +; + +-- support race_* icons +ALTER TABLE aowow_races + ADD COLUMN `iconId0` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT "male icon" AFTER `fileString`, + ADD COLUMN `iconId1` smallint(5) unsigned NOT NULL DEFAULT 0 COMMENT "female icon" AFTER `iconId0` +; diff --git a/setup/sql/updates/1764966675_01.sql b/setup/sql/updates/1764966675_01.sql new file mode 100644 index 00000000..bef0e1a9 --- /dev/null +++ b/setup/sql/updates/1764966675_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' source'); diff --git a/setup/sql/updates/1765116606_01.sql b/setup/sql/updates/1765116606_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1765116606_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/sql/updates/1765569409_01.sql b/setup/sql/updates/1765569409_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1765569409_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/sql/updates/1767026729_01.sql b/setup/sql/updates/1767026729_01.sql new file mode 100644 index 00000000..276abad4 --- /dev/null +++ b/setup/sql/updates/1767026729_01.sql @@ -0,0 +1,18 @@ +ALTER TABLE `aowow_spawns` + ADD COLUMN `ScriptName` varchar(64) DEFAULT NULL AFTER `pathId`, + ADD COLUMN `StringId` varchar(64) DEFAULT NULL AFTER `ScriptName` +; + +ALTER TABLE `aowow_objects` + MODIFY COLUMN `ScriptOrAI` varchar(64) DEFAULT NULL, + ADD COLUMN `StringId` varchar(64) DEFAULT NULL AFTER `ScriptOrAI` +; + +ALTER TABLE `aowow_creature` + DROP COLUMN `aiName`, + DROP COLUMN `scriptName`, + ADD COLUMN `ScriptOrAI` varchar(64) DEFAULT NULL AFTER `flagsExtra`, + ADD COLUMN `StringId` varchar(64) DEFAULT NULL AFTER `ScriptOrAI` +; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' creature objects spawns'); diff --git a/setup/sql/updates/1767034443_01.sql b/setup/sql/updates/1767034443_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1767034443_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/sql/updates/1767051301_01.sql b/setup/sql/updates/1767051301_01.sql new file mode 100644 index 00000000..1e13478c --- /dev/null +++ b/setup/sql/updates/1767051301_01.sql @@ -0,0 +1,4 @@ +DELETE FROM aowow_config WHERE `key` = 'sql_limit_default'; +DELETE FROM aowow_config WHERE `key` = 'sql_limit_none'; +DELETE FROM aowow_config WHERE `key` = 'sql_limit_quicksearch'; +DELETE FROM aowow_config WHERE `key` = 'sql_limit_search'; diff --git a/setup/sql/updates/1767117346_01.sql b/setup/sql/updates/1767117346_01.sql new file mode 100644 index 00000000..5a565565 --- /dev/null +++ b/setup/sql/updates/1767117346_01.sql @@ -0,0 +1,6 @@ +ALTER TABLE aowow_quests + CHANGE COLUMN `method` `questType` tinyint(3) unsigned NOT NULL DEFAULT 2, + CHANGE COLUMN `zoneOrSort` `questSortId` smallint(6) NOT NULL DEFAULT 0, + CHANGE COLUMN `zoneOrSortBak` `questSortIdBak` smallint(6) NOT NULL DEFAULT 0, + CHANGE COLUMN `type` `questInfoId` smallint(5) unsigned NOT NULL DEFAULT 0 +; diff --git a/setup/sql/updates/1767117346_02.sql b/setup/sql/updates/1767117346_02.sql new file mode 100644 index 00000000..ac8b6c36 --- /dev/null +++ b/setup/sql/updates/1767117346_02.sql @@ -0,0 +1 @@ +UPDATE aowow_setup_custom_data SET `field` = 'questSortId' WHERE `field` = 'zoneOrSort'; diff --git a/setup/sql/updates/1768155583_01.sql b/setup/sql/updates/1768155583_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1768155583_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/sql/updates/1768324765_01.sql b/setup/sql/updates/1768324765_01.sql new file mode 100644 index 00000000..3b04a1df --- /dev/null +++ b/setup/sql/updates/1768324765_01.sql @@ -0,0 +1,9 @@ +ALTER TABLE `aowow_taxinodes` + CHANGE COLUMN `posX` `mapX` float unsigned NOT NULL, + CHANGE COLUMN `posY` `mapY` float unsigned NOT NULL, + ADD COLUMN `areaId` smallint(5) unsigned NOT NULL AFTER `mapY`, + ADD COLUMN `areaX` float unsigned NOT NULL AFTER `areaId`, + ADD COLUMN `areaY` float unsigned NOT NULL AFTER `areaX` +; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' taxi'); diff --git a/setup/sql/updates/1768385060_01.sql b/setup/sql/updates/1768385060_01.sql new file mode 100644 index 00000000..06dea004 --- /dev/null +++ b/setup/sql/updates/1768385060_01.sql @@ -0,0 +1,20 @@ +ALTER TABLE `aowow_profiler_profiles` + ADD INDEX idx_race (`race`), + ADD INDEX idx_class (`class`), + ADD INDEX idx_level (`level`), + ADD INDEX idx_guildrank (`guildrank`), + ADD INDEX idx_gearscore (`gearscore`), + ADD INDEX idx_achievementpoints (`achievementpoints`), + ADD INDEX idx_talenttree1 (`talenttree1`), + ADD INDEX idx_talenttree2 (`talenttree2`), + ADD INDEX idx_talenttree3 (`talenttree3`) +; + +ALTER TABLE aowow_profiler_completion_skills + ADD INDEX idx_value (`value`) +; + +ALTER TABLE aowow_profiler_arena_team + ADD INDEX idx_type (`type`), + ADD INDEX idx_rating (`rating`) +; diff --git a/setup/sql/updates/1768517243_01.sql b/setup/sql/updates/1768517243_01.sql new file mode 100644 index 00000000..e2fa5d5c --- /dev/null +++ b/setup/sql/updates/1768517243_01.sql @@ -0,0 +1,9 @@ +UPDATE `aowow_items` SET + `requiredClass` = IF((`requiredClass` & 1535) = 1535, 0, `requiredClass` & 1535), + `requiredRace` = IF((`requiredRace` & 1791) = 1791, 0, `requiredRace` & 1791) +; + +ALTER TABLE `aowow_items` + MODIFY COLUMN `requiredClass` smallint(5) unsigned NOT NULL DEFAULT 0, + MODIFY COLUMN `requiredRace` smallint(5) unsigned NOT NULL DEFAULT 0 +; diff --git a/setup/sql/updates/1768556688_01.sql b/setup/sql/updates/1768556688_01.sql new file mode 100644 index 00000000..b1e65dfb --- /dev/null +++ b/setup/sql/updates/1768556688_01.sql @@ -0,0 +1,120 @@ +ALTER TABLE `aowow_spell` + DROP INDEX `items`, + DROP INDEX `effects`, + ADD INDEX `idx_skill1` (`skillLine1`), + ADD INDEX `idx_skill2` (`skillLine2OrMask`), + ADD FULLTEXT `idx_name0` (`name_loc0`), + ADD FULLTEXT `idx_name2` (`name_loc2`), + ADD FULLTEXT `idx_name3` (`name_loc3`), + ADD FULLTEXT `idx_name4` (`name_loc4`), + ADD FULLTEXT `idx_name6` (`name_loc6`), + ADD FULLTEXT `idx_name8` (`name_loc8`), + ADD INDEX `idx_spellfamily` (`spellFamilyId`), + ADD INDEX `idx_miscvalue1` (`effect1MiscValue`), + ADD INDEX `idx_miscvalue2` (`effect2MiscValue`), + ADD INDEX `idx_miscvalue3` (`effect3MiscValue`), + ADD INDEX `idx_triggerspell1` (`effect1TriggerSpell`), + ADD INDEX `idx_triggerspell2` (`effect2TriggerSpell`), + ADD INDEX `idx_triggerspell3` (`effect3TriggerSpell`) +; + +ALTER TABLE `aowow_quests` + MODIFY COLUMN `name_loc0` varchar(100) DEFAULT NULL, + MODIFY COLUMN `name_loc2` varchar(100) DEFAULT NULL, + MODIFY COLUMN `name_loc3` varchar(100) DEFAULT NULL, + MODIFY COLUMN `name_loc4` varchar(100) DEFAULT NULL, + MODIFY COLUMN `name_loc6` varchar(100) DEFAULT NULL, + MODIFY COLUMN `name_loc8` varchar(100) DEFAULT NULL, + ADD FULLTEXT `idx_name0` (`name_loc0`), + ADD FULLTEXT `idx_name2` (`name_loc2`), + ADD FULLTEXT `idx_name3` (`name_loc3`), + ADD FULLTEXT `idx_name4` (`name_loc4`), + ADD FULLTEXT `idx_name6` (`name_loc6`), + ADD FULLTEXT `idx_name8` (`name_loc8`), + ADD INDEX `idx_sourcespell` (`sourceSpellId`), + ADD INDEX `idx_rewardspell` (`rewardSpell`), + ADD INDEX `idx_rewardcastspell` (`rewardSpellCast`), + ADD INDEX `idx_classmask` (`reqRaceMask`), + ADD INDEX `idx_racemask` (`reqClassMask`), + ADD INDEX `idx_questsort` (`questSortId`), + ADD INDEX `idx_rewarditem1` (`rewardChoiceItemId1`), + ADD INDEX `idx_rewarditem2` (`rewardChoiceItemId2`), + ADD INDEX `idx_rewarditem3` (`rewardChoiceItemId3`), + ADD INDEX `idx_rewarditem4` (`rewardChoiceItemId4`), + ADD INDEX `idx_rewarditem5` (`rewardChoiceItemId5`), + ADD INDEX `idx_rewarditem6` (`rewardChoiceItemId6`), + ADD INDEX `idx_rewardfaction1` (`rewardFactionId1`), + ADD INDEX `idx_rewardfaction2` (`rewardFactionId2`), + ADD INDEX `idx_rewardfaction3` (`rewardFactionId3`), + ADD INDEX `idx_rewardfaction4` (`rewardFactionId4`), + ADD INDEX `idx_rewardfaction5` (`rewardFactionId5`), + ADD INDEX `idx_choiceitem1` (`rewardItemId1`), + ADD INDEX `idx_choiceitem2` (`rewardItemId2`), + ADD INDEX `idx_choiceitem3` (`rewardItemId3`), + ADD INDEX `idx_choiceitem4` (`rewardItemId4`), + ADD INDEX `idx_requirement1` (`reqNpcOrGo1`), + ADD INDEX `idx_requirement2` (`reqNpcOrGo2`), + ADD INDEX `idx_requirement3` (`reqNpcOrGo3`), + ADD INDEX `idx_requirement4` (`reqNpcOrGo4`), + ADD INDEX `idx_event` (`eventId`) +; + +ALTER TABLE `aowow_creature` + DROP INDEX `idx_name`, + ADD INDEX `idx_trainer` (`trainerType`), + ADD INDEX `idx_trainerrequirement` (`trainerRequirement`), + ADD FULLTEXT `idx_name0` (`name_loc0`), + ADD FULLTEXT `idx_name2` (`name_loc2`), + ADD FULLTEXT `idx_name3` (`name_loc3`), + ADD FULLTEXT `idx_name4` (`name_loc4`), + ADD FULLTEXT `idx_name6` (`name_loc6`), + ADD FULLTEXT `idx_name8` (`name_loc8`), + ADD INDEX `idx_spell1` (`spell1`), + ADD INDEX `idx_spell2` (`spell2`), + ADD INDEX `idx_spell3` (`spell3`), + ADD INDEX `idx_spell4` (`spell4`), + ADD INDEX `idx_spell5` (`spell5`), + ADD INDEX `idx_spell6` (`spell6`), + ADD INDEX `idx_spell7` (`spell7`), + ADD INDEX `idx_spell8` (`spell8`) +; + +ALTER TABLE `aowow_items` + DROP INDEX `spellId1`, + DROP INDEX `spellId2`, + DROP INDEX `idx_name`, + ADD INDEX `idx_spell1` (`spellId1`), + ADD INDEX `idx_spell2` (`spellId2`), + ADD INDEX `idx_spell3` (`spellId3`), + ADD INDEX `idx_spell4` (`spellId4`), + ADD INDEX `idx_spell5` (`spellId5`), + ADD INDEX `idx_trigger1` (`spellTrigger1`), + ADD INDEX `idx_trigger2` (`spellTrigger2`), + ADD INDEX `idx_trigger3` (`spellTrigger3`), + ADD INDEX `idx_trigger4` (`spellTrigger4`), + ADD INDEX `idx_trigger5` (`spellTrigger5`), + ADD INDEX `idx_reqskill` (`requiredSkill`), + ADD FULLTEXT `idx_name0` (`name_loc0`), + ADD FULLTEXT `idx_name2` (`name_loc2`), + ADD FULLTEXT `idx_name3` (`name_loc3`), + ADD FULLTEXT `idx_name4` (`name_loc4`), + ADD FULLTEXT `idx_name6` (`name_loc6`), + ADD FULLTEXT `idx_name8` (`name_loc8`), + ADD INDEX `idx_itemset` (`itemset`) +; + +ALTER TABLE `aowow_objects` + DROP INDEX `idx_name`, + ADD INDEX `idx_onusespell` (`onUseSpell`), + ADD INDEX `idx_onsuccessspell` (`onSuccessSpell`), + ADD INDEX `idx_auraspell` (`auraSpell`), + ADD INDEX `idx_triggeredspell` (`triggeredSpell`), + ADD FULLTEXT `idx_name0` (`name_loc0`), + ADD FULLTEXT `idx_name2` (`name_loc2`), + ADD FULLTEXT `idx_name3` (`name_loc3`), + ADD FULLTEXT `idx_name4` (`name_loc4`), + ADD FULLTEXT `idx_name6` (`name_loc6`), + ADD FULLTEXT `idx_name8` (`name_loc8`) +; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' achievementcriteria'); diff --git a/setup/sql/updates/1768672799_01.sql b/setup/sql/updates/1768672799_01.sql new file mode 100644 index 00000000..140bf2d6 --- /dev/null +++ b/setup/sql/updates/1768672799_01.sql @@ -0,0 +1,16 @@ +ALTER TABLE `aowow_creature` DROP INDEX `idx_name4`; +ALTER TABLE `aowow_items` DROP INDEX `idx_name4`; +ALTER TABLE `aowow_objects` DROP INDEX `idx_name4`; +ALTER TABLE `aowow_quests` DROP INDEX `idx_name4`; +ALTER TABLE `aowow_spell` DROP INDEX `idx_name4`; + +SET SESSION innodb_ft_enable_stopword = OFF; + +OPTIMIZE TABLE `aowow_spell`; +OPTIMIZE TABLE `aowow_quests`; +OPTIMIZE TABLE `aowow_creature`; +OPTIMIZE TABLE `aowow_items`; +OPTIMIZE TABLE `aowow_objects`; + +REPLACE INTO `aowow_config` VALUES + ('logographic_ft_search', '0', '0', 1, 0x484, 'enables fulltext search for logographic languages (CN, KR, TW). The database MUST support this (i.e. MySQL implements ngram)'); diff --git a/setup/sql/updates/1769190110_01.sql b/setup/sql/updates/1769190110_01.sql new file mode 100644 index 00000000..4aced6c3 --- /dev/null +++ b/setup/sql/updates/1769190110_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' quests'); diff --git a/setup/sql/updates/1769622382_01.sql b/setup/sql/updates/1769622382_01.sql new file mode 100644 index 00000000..fecbced8 --- /dev/null +++ b/setup/sql/updates/1769622382_01.sql @@ -0,0 +1,6 @@ +ALTER TABLE `aowow_icons` ADD KEY idx_sourcename (`name_source`); + +UPDATE `aowow_dbversion` SET + `sql` = CONCAT(IFNULL(`sql`, ''), ' achievement currencies glyphproperties holidays icons items pet skillline spell'), + `build` = CONCAT(IFNULL(`build`, ''), ' enchants gems glyphs talenticons') +; diff --git a/setup/sql/updates/1770309983_01.sql b/setup/sql/updates/1770309983_01.sql new file mode 100644 index 00000000..27c9e220 --- /dev/null +++ b/setup/sql/updates/1770309983_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' spellscaling itemscaling'); diff --git a/setup/sql/updates/1770626911_01.sql b/setup/sql/updates/1770626911_01.sql new file mode 100644 index 00000000..a71c1fa1 --- /dev/null +++ b/setup/sql/updates/1770626911_01.sql @@ -0,0 +1,91 @@ +SET SESSION innodb_ft_enable_stopword = OFF; + +ALTER TABLE aowow_creature + DROP INDEX idx_name0, + DROP INDEX idx_name2, + DROP INDEX idx_name3, + DROP INDEX idx_name6, + DROP INDEX idx_name8, + ADD INDEX idx_name0 (`name_loc0`), + ADD INDEX idx_name2 (`name_loc2`), + ADD INDEX idx_name3 (`name_loc3`), + ADD INDEX idx_name4 (`name_loc4`), + ADD INDEX idx_name6 (`name_loc6`), + ADD INDEX idx_name8 (`name_loc8`), + ADD FULLTEXT idx_ft_name0 (`name_loc0`), + ADD FULLTEXT idx_ft_name2 (`name_loc2`), + ADD FULLTEXT idx_ft_name3 (`name_loc3`), + ADD FULLTEXT idx_ft_name6 (`name_loc6`), + ADD FULLTEXT idx_ft_name8 (`name_loc8`); + +ALTER TABLE aowow_items + DROP INDEX idx_name0, + DROP INDEX idx_name2, + DROP INDEX idx_name3, + DROP INDEX idx_name6, + DROP INDEX idx_name8, + ADD INDEX idx_name0 (`name_loc0`), + ADD INDEX idx_name2 (`name_loc2`), + ADD INDEX idx_name3 (`name_loc3`), + ADD INDEX idx_name4 (`name_loc4`), + ADD INDEX idx_name6 (`name_loc6`), + ADD INDEX idx_name8 (`name_loc8`), + ADD FULLTEXT idx_ft_name0 (`name_loc0`), + ADD FULLTEXT idx_ft_name2 (`name_loc2`), + ADD FULLTEXT idx_ft_name3 (`name_loc3`), + ADD FULLTEXT idx_ft_name6 (`name_loc6`), + ADD FULLTEXT idx_ft_name8 (`name_loc8`); + +ALTER TABLE aowow_objects + DROP INDEX idx_name0, + DROP INDEX idx_name2, + DROP INDEX idx_name3, + DROP INDEX idx_name6, + DROP INDEX idx_name8, + ADD INDEX idx_name0 (`name_loc0`), + ADD INDEX idx_name2 (`name_loc2`), + ADD INDEX idx_name3 (`name_loc3`), + ADD INDEX idx_name4 (`name_loc4`), + ADD INDEX idx_name6 (`name_loc6`), + ADD INDEX idx_name8 (`name_loc8`), + ADD FULLTEXT idx_ft_name0 (`name_loc0`), + ADD FULLTEXT idx_ft_name2 (`name_loc2`), + ADD FULLTEXT idx_ft_name3 (`name_loc3`), + ADD FULLTEXT idx_ft_name6 (`name_loc6`), + ADD FULLTEXT idx_ft_name8 (`name_loc8`); + +ALTER TABLE aowow_quests + DROP INDEX idx_name0, + DROP INDEX idx_name2, + DROP INDEX idx_name3, + DROP INDEX idx_name6, + DROP INDEX idx_name8, + ADD INDEX idx_name0 (`name_loc0`), + ADD INDEX idx_name2 (`name_loc2`), + ADD INDEX idx_name3 (`name_loc3`), + ADD INDEX idx_name4 (`name_loc4`), + ADD INDEX idx_name6 (`name_loc6`), + ADD INDEX idx_name8 (`name_loc8`), + ADD FULLTEXT idx_ft_name0 (`name_loc0`), + ADD FULLTEXT idx_ft_name2 (`name_loc2`), + ADD FULLTEXT idx_ft_name3 (`name_loc3`), + ADD FULLTEXT idx_ft_name6 (`name_loc6`), + ADD FULLTEXT idx_ft_name8 (`name_loc8`); + +ALTER TABLE aowow_spell + DROP INDEX idx_name0, + DROP INDEX idx_name2, + DROP INDEX idx_name3, + DROP INDEX idx_name6, + DROP INDEX idx_name8, + ADD INDEX idx_name0 (`name_loc0`), + ADD INDEX idx_name2 (`name_loc2`), + ADD INDEX idx_name3 (`name_loc3`), + ADD INDEX idx_name4 (`name_loc4`), + ADD INDEX idx_name6 (`name_loc6`), + ADD INDEX idx_name8 (`name_loc8`), + ADD FULLTEXT idx_ft_name0 (`name_loc0`), + ADD FULLTEXT idx_ft_name2 (`name_loc2`), + ADD FULLTEXT idx_ft_name3 (`name_loc3`), + ADD FULLTEXT idx_ft_name6 (`name_loc6`), + ADD FULLTEXT idx_ft_name8 (`name_loc8`); diff --git a/setup/sql/updates/1770889048_01.sql b/setup/sql/updates/1770889048_01.sql new file mode 100644 index 00000000..54df351e --- /dev/null +++ b/setup/sql/updates/1770889048_01.sql @@ -0,0 +1,9 @@ +ALTER TABLE aowow_items + ADD COLUMN `effects_loc0` text DEFAULT NULL AFTER `flagsCustom`, + ADD COLUMN `effects_loc2` text DEFAULT NULL AFTER `effects_loc0`, + ADD COLUMN `effects_loc3` text DEFAULT NULL AFTER `effects_loc2`, + ADD COLUMN `effects_loc4` text DEFAULT NULL AFTER `effects_loc3`, + ADD COLUMN `effects_loc6` text DEFAULT NULL AFTER `effects_loc4`, + ADD COLUMN `effects_loc8` text DEFAULT NULL AFTER `effects_loc6`; + +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' items'); diff --git a/setup/sql/updates/1771934998_01.sql b/setup/sql/updates/1771934998_01.sql new file mode 100644 index 00000000..9fa47c05 --- /dev/null +++ b/setup/sql/updates/1771934998_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' stats'); diff --git a/setup/sql/updates/1772564118_01.sql b/setup/sql/updates/1772564118_01.sql new file mode 100644 index 00000000..9c80c4e1 --- /dev/null +++ b/setup/sql/updates/1772564118_01.sql @@ -0,0 +1,56 @@ +DROP TABLE IF EXISTS `aowow_quests_search`; +CREATE TABLE `aowow_quests_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(100) DEFAULT NULL, + `nObjectives` text DEFAULT NULL, + `nDetails` text DEFAULT NULL, + PRIMARY KEY (`id`, `locale`), + FULLTEXT `idx_ft_na` (`nName`), + FULLTEXT `idx_ft_na_ex` (`nName`, `nObjectives`, `nDetails`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `aowow_objects_search`; +CREATE TABLE `aowow_objects_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(127) DEFAULT NULL, + PRIMARY KEY (`id`, `locale`), + FULLTEXT `idx_ft_na` (`nName`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `aowow_items_search`; +CREATE TABLE `aowow_items_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(127) DEFAULT NULL, + `nDescription` varchar(255) DEFAULT NULL, + `nEffects` text DEFAULT NULL, + PRIMARY KEY (`id`, `locale`), + FULLTEXT `idx_ft_na` (`nName`), + FULLTEXT `idx_ft_description` (`nDescription`), + FULLTEXT `idx_ft_effects` (`nEffects`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `aowow_creature_search`; +CREATE TABLE `aowow_creature_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(100) DEFAULT NULL, + `nSubname` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`, `locale`), + FULLTEXT `idx_ft_na` (`nName`), + FULLTEXT `idx_ft_na_ex` (`nName`, `nSubname`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DROP TABLE IF EXISTS `aowow_spell_search`; +CREATE TABLE `aowow_spell_search` ( + `id` mediumint(8) unsigned NOT NULL, + `locale` tinyint(3) unsigned NOT NULL, + `nName` varchar(185) DEFAULT NULL, + `nDescription` text DEFAULT NULL, + `nBuff` text DEFAULT NULL, + PRIMARY KEY (`id`, `locale`), + FULLTEXT `idx_ft_na` (`nName`), + FULLTEXT `idx_ft_na_ex` (`nName`, `nDescription`, `nBuff`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/setup/sql/updates/1772564118_02.sql b/setup/sql/updates/1772564118_02.sql new file mode 100644 index 00000000..55f353a7 --- /dev/null +++ b/setup/sql/updates/1772564118_02.sql @@ -0,0 +1,40 @@ +ALTER TABLE `aowow_creature` + DROP INDEX `idx_ft_name0`, + DROP INDEX `idx_ft_name2`, + DROP INDEX `idx_ft_name3`, + DROP INDEX `idx_ft_name6`, + DROP INDEX `idx_ft_name8`; + +ALTER TABLE `aowow_objects` + DROP INDEX `idx_ft_name0`, + DROP INDEX `idx_ft_name2`, + DROP INDEX `idx_ft_name3`, + DROP INDEX `idx_ft_name6`, + DROP INDEX `idx_ft_name8`; + +ALTER TABLE `aowow_quests` + DROP INDEX `idx_ft_name0`, + DROP INDEX `idx_ft_name2`, + DROP INDEX `idx_ft_name3`, + DROP INDEX `idx_ft_name6`, + DROP INDEX `idx_ft_name8`; + +ALTER TABLE `aowow_spell` + DROP INDEX `idx_ft_name0`, + DROP INDEX `idx_ft_name2`, + DROP INDEX `idx_ft_name3`, + DROP INDEX `idx_ft_name6`, + DROP INDEX `idx_ft_name8`; + +ALTER TABLE `aowow_items` + DROP COLUMN `effects_loc0`, + DROP COLUMN `effects_loc2`, + DROP COLUMN `effects_loc3`, + DROP COLUMN `effects_loc4`, + DROP COLUMN `effects_loc6`, + DROP COLUMN `effects_loc8`, + DROP INDEX `idx_ft_name0`, + DROP INDEX `idx_ft_name2`, + DROP INDEX `idx_ft_name3`, + DROP INDEX `idx_ft_name6`, + DROP INDEX `idx_ft_name8`; diff --git a/setup/sql/updates/1774468408_01.sql b/setup/sql/updates/1774468408_01.sql new file mode 100644 index 00000000..f30e8121 --- /dev/null +++ b/setup/sql/updates/1774468408_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' search'); diff --git a/setup/sql/updates/1774551739_01.sql b/setup/sql/updates/1774551739_01.sql new file mode 100644 index 00000000..debc3e95 --- /dev/null +++ b/setup/sql/updates/1774551739_01.sql @@ -0,0 +1,135 @@ +INSERT INTO `aowow_setup_custom_data` (`command`, `entry`, `field`, `value`, `comment`) VALUES + ('spell', 17579, 'cuFlags', 1610612736, 'Alchemy: Greater Holy Protection Potion - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 54020, 'cuFlags', 1610612736, 'Alchemy: Transmute: Eternal Might - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 2336, 'cuFlags', 1610612736, 'Alchemy: Elixir of Tongues - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 13460, 'cuFlags', 1610612736, 'Greater Holy Protection Potion - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 40248, 'cuFlags', 1610612736, 'Eternal Might - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 2460, 'cuFlags', 1610612736, 'Elixir of Tongues - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 8366, 'cuFlags', 1610612736, 'Blacksmithing: Ironforge Chain - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 2671, 'cuFlags', 1610612736, 'Blacksmithing: Rough Bronze Bracers - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 8368, 'cuFlags', 1610612736, 'Blacksmithing: Ironforge Gauntlets - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 9942, 'cuFlags', 1610612736, 'Blacksmithing: Mithril Scale Gloves - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 16960, 'cuFlags', 1610612736, 'Blacksmithing: Thorium Greatsword - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 16965, 'cuFlags', 1610612736, 'Blacksmithing: Bleakwood Hew - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 16967, 'cuFlags', 1610612736, 'Blacksmithing: Inlaid Thorium Hammer - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 16980, 'cuFlags', 1610612736, 'Blacksmithing: Rune Edge - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 28244, 'cuFlags', 536870912, 'Blacksmithing: Icebane Bracers - set: CUSTOM_UNAVAILABLE'), + ('spell', 28242, 'cuFlags', 536870912, 'Blacksmithing: Icebane Breastplate - set: CUSTOM_UNAVAILABLE'), + ('spell', 28243, 'cuFlags', 536870912, 'Blacksmithing: Icebane Gauntlets - set: CUSTOM_UNAVAILABLE'), + ('spell', 16986, 'cuFlags', 1610612736, 'Blacksmithing: Blood Talon - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 16987, 'cuFlags', 1610612736, 'Blacksmithing: Darkspear - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 2867, 'cuFlags', 1610612736, 'Rough Bronze Bracers - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 6730, 'cuFlags', 1610612736, 'Ironforge Chain - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 6733, 'cuFlags', 1610612736, 'Ironforge Gauntlets - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 7925, 'cuFlags', 1610612736, 'Mithril Scale Gloves - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 12764, 'cuFlags', 1610612736, 'Thorium Greatsword - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 12769, 'cuFlags', 1610612736, 'Bleakwood Hew - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 12772, 'cuFlags', 1610612736, 'Inlaid Thorium Hammer - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 12779, 'cuFlags', 1610612736, 'Rune Edge - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 12795, 'cuFlags', 1610612736, 'Blood Talon - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 12802, 'cuFlags', 1610612736, 'Darkspear - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 22671, 'cuFlags', 536870912, 'Icebane Bracers - set: CUSTOM_UNAVAILABLE'), + ('items', 22669, 'cuFlags', 536870912, 'Icebane Breastplate - set: CUSTOM_UNAVAILABLE'), + ('items', 22670, 'cuFlags', 536870912, 'Icebane Gauntlets - set: CUSTOM_UNAVAILABLE'), + ('spell', 28021, 'cuFlags', 1610612736, 'Enchanting: Arcane Dust - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 44612, 'cuFlags', 1610612736, 'Enchanting: Enchant Gloves - Greater Blasting - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 62257, 'cuFlags', 1610612736, 'Enchanting: Enchant Weapon - Titanguard - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 38985, 'cuFlags', 1610612736, 'Scroll of Enchant Gloves - Greater Blasting - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 44946, 'cuFlags', 1610612736, 'Scroll of Enchant Weapon - Titanguard - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 44945, 'cuFlags', 1610612736, 'Formula: Enchant Weapon - Titanguard - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 30549, 'cuFlags', 1610612736, 'Engineering: Critter Enlarger - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 67790, 'cuFlags', 1610612736, 'Engineering: Dimensional Folder: K3 - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 30343, 'cuFlags', 1610612736, 'Engineering: Blue Smoke Flare - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 30342, 'cuFlags', 1610612736, 'Engineering: Red Smoke Flare - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 30561, 'cuFlags', 1610612736, 'Engineering: Goblin Tonk Controller - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 30573, 'cuFlags', 1610612736, 'Engineering: Gnomish Tonk Controller - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 12722, 'cuFlags', 1610612736, 'Engineering: Goblin Radio - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 12904, 'cuFlags', 1610612736, 'Engineering: Gnomish Ham Radio - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 12720, 'cuFlags', 1610612736, 'Engineering: Goblin "Boom" Box - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 12900, 'cuFlags', 1610612736, 'Engineering: Mobile Alarm- set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 23882, 'cuFlags', 1610612736, 'Schematic: Critter Enlarger - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 23820, 'cuFlags', 1610612736, 'Critter Enlarger - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 48933, 'cuFlags', 1610612736, 'Dimensional Folder: K3 - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 23770, 'cuFlags', 1610612736, 'Blue Smoke Flare - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 23769, 'cuFlags', 1610612736, 'Red Smoke Flare - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 23831, 'cuFlags', 1610612736, 'Goblin Tonk Controller - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 23832, 'cuFlags', 1610612736, 'Gnomish Tonk Controller - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 10585, 'cuFlags', 1610612736, 'Goblin Radio - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 10723, 'cuFlags', 1610612736, 'Gnomish Ham Radio - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 10580, 'cuFlags', 1610612736, 'Goblin "Boom" Box - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 10719, 'cuFlags', 1610612736, 'Mobile Alarm - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 8387, 'cuFlags', 1610612736, 'Herbalism: Find Herbs - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 2369, 'cuFlags', 1610612736, 'Herbalism: Herb Gathering - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 2371, 'cuFlags', 1610612736, 'Herbalism: Herb Gathering - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 52175, 'cuFlags', 1610612736, 'Inscription: Decipher - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 25614, 'cuFlags', 1610612736, 'Jewelcrafting: Silver Rose Pendant - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 32810, 'cuFlags', 1610612736, 'Jewelcrafting: Primal Stone Statue - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 26918, 'cuFlags', 1610612736, 'Jewelcrafting: Arcanite Sword Pendant - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 26920, 'cuFlags', 1610612736, 'Jewelcrafting: Blood Crown - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 20956, 'cuFlags', 1610612736, 'Silver Rose Pendant - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 25884, 'cuFlags', 1610612736, 'Primal Stone Statue - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 21793, 'cuFlags', 1610612736, 'Arcanite Sword Pendant - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 21780, 'cuFlags', 1610612736, 'Blood Crown - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 10550, 'cuFlags', 1610612736, 'Leatherworking: Nightscape Cloak - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 28224, 'cuFlags', 536870912, 'Leatherworking: Icy Scale Bracers - set: CUSTOM_UNAVAILABLE'), + ('spell', 28222, 'cuFlags', 536870912, 'Leatherworking: Icy Scale Breastplate - set: CUSTOM_UNAVAILABLE'), + ('spell', 28223, 'cuFlags', 536870912, 'Leatherworking: Icy Scale Gauntlets - set: CUSTOM_UNAVAILABLE'), + ('spell', 28221, 'cuFlags', 536870912, 'Leatherworking: Polar Bracers - set: CUSTOM_UNAVAILABLE'), + ('spell', 28220, 'cuFlags', 536870912, 'Leatherworking: Polar Gloves - set: CUSTOM_UNAVAILABLE'), + ('spell', 28219, 'cuFlags', 536870912, 'Leatherworking: Polar Tunic - set: CUSTOM_UNAVAILABLE'), + ('spell', 55243, 'cuFlags', 1610612736, 'Leatherworking: Bracers of Deflection - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 19106, 'cuFlags', 536870912, 'Leatherworking: Onyxia Scale Breastplate - set: CUSTOM_UNAVAILABLE'), + ('items', 8195, 'cuFlags', 1610612736, 'Nightscape Cloak - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 22665, 'cuFlags', 536870912, 'Icy Scale Bracers - set: CUSTOM_UNAVAILABLE'), + ('items', 22664, 'cuFlags', 536870912, 'Icy Scale Breastplate - set: CUSTOM_UNAVAILABLE'), + ('items', 22666, 'cuFlags', 536870912, 'Icy Scale Gauntlets - set: CUSTOM_UNAVAILABLE'), + ('items', 22663, 'cuFlags', 536870912, 'Polar Bracers - set: CUSTOM_UNAVAILABLE'), + ('items', 22662, 'cuFlags', 536870912, 'Polar Gloves - set: CUSTOM_UNAVAILABLE'), + ('items', 22661, 'cuFlags', 536870912, 'Polar Tunic - set: CUSTOM_UNAVAILABLE'), + ('items', 41264, 'cuFlags', 1610612736, 'Bracers of Deflection - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 15141, 'cuFlags', 536870912, 'Onyxia Scale Breastplate - set: CUSTOM_UNAVAILABLE'), + ('spell', 8388, 'cuFlags', 1610612736, 'Mining: Find Minerals - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 7636, 'cuFlags', 1610612736, 'Tailoring: Green Woolen Robe - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 8778, 'cuFlags', 1610612736, 'Tailoring: Boots of Darkness - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 12063, 'cuFlags', 1610612736, 'Tailoring: Stormcloth Gloves - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 12062, 'cuFlags', 1610612736, 'Tailoring: Stormcloth Pants - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 12068, 'cuFlags', 1610612736, 'Tailoring: Stormcloth Vest - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 12083, 'cuFlags', 1610612736, 'Tailoring: Stormcloth Headband - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 12087, 'cuFlags', 1610612736, 'Tailoring: Stormcloth Shoulders - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 12090, 'cuFlags', 1610612736, 'Tailoring: Stormcloth Boots - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 28208, 'cuFlags', 536870912, 'Tailoring: Glacial Cloak - set: CUSTOM_UNAVAILABLE'), + ('spell', 28205, 'cuFlags', 536870912, 'Tailoring: Glacial Gloves - set: CUSTOM_UNAVAILABLE'), + ('spell', 28207, 'cuFlags', 536870912, 'Tailoring: Glacial Vest - set: CUSTOM_UNAVAILABLE'), + ('spell', 28209, 'cuFlags', 536870912, 'Tailoring: Glacial Wrists - set: CUSTOM_UNAVAILABLE'), + ('spell', 36670, 'cuFlags', 1610612736, 'Tailoring: Lifeblood Belt - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 36672, 'cuFlags', 1610612736, 'Tailoring: Lifeblood Bracers - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 36669, 'cuFlags', 1610612736, 'Tailoring: Lifeblood Leggings - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 36667, 'cuFlags', 1610612736, 'Tailoring: Netherflame Belt - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 36668, 'cuFlags', 1610612736, 'Tailoring: Netherflame Boots - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 36665, 'cuFlags', 1610612736, 'Tailoring: Netherflame Robe - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 56048, 'cuFlags', 1610612736, 'Tailoring: Duskweave Boots - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('spell', 31461, 'cuFlags', 1610612736, 'Tailoring: Heavy Netherweave Net - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 6243, 'cuFlags', 1610612736, 'Green Woolen Robe - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 7027, 'cuFlags', 1610612736, 'Boots of Darkness - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 10011, 'cuFlags', 1610612736, 'Stormcloth Gloves - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 10010, 'cuFlags', 1610612736, 'Stormcloth Pants - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 10020, 'cuFlags', 1610612736, 'Stormcloth Vest - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 10032, 'cuFlags', 1610612736, 'Stormcloth Headband - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 10038, 'cuFlags', 1610612736, 'Stormcloth Shoulders - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 10039, 'cuFlags', 1610612736, 'Stormcloth Boots - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 22658, 'cuFlags', 536870912, 'Glacial Cloak - set: CUSTOM_UNAVAILABLE'), + ('items', 22654, 'cuFlags', 536870912, 'Glacial Gloves - set: CUSTOM_UNAVAILABLE'), + ('items', 22652, 'cuFlags', 536870912, 'Glacial Vest - set: CUSTOM_UNAVAILABLE'), + ('items', 22655, 'cuFlags', 536870912, 'Glacial Wrists - set: CUSTOM_UNAVAILABLE'), + ('items', 30463, 'cuFlags', 1610612736, 'Lifeblood Belt - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 30464, 'cuFlags', 1610612736, 'Lifeblood Bracers - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 30465, 'cuFlags', 1610612736, 'Lifeblood Leggings - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 30460, 'cuFlags', 1610612736, 'Netherflame Belt - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 30461, 'cuFlags', 1610612736, 'Netherflame Boots - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 30459, 'cuFlags', 1610612736, 'Netherflame Robe - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 41544, 'cuFlags', 1610612736, 'Duskweave Boots - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'), + ('items', 24269, 'cuFlags', 1610612736, 'Heavy Netherweave Net - set: CUSTOM_UNAVAILABLE | CUSTOM_EXCLUDE_FOR_LISTVIEW'); + +UPDATE `aowow_dbversion` SET + `sql` = CONCAT(IFNULL(`sql`, ''), ' spell items'), + `build` = CONCAT(IFNULL(`build`, ''), ' profiler'); diff --git a/setup/sql/updates/1774728772_01.sql b/setup/sql/updates/1774728772_01.sql new file mode 100644 index 00000000..f30e8121 --- /dev/null +++ b/setup/sql/updates/1774728772_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `sql` = CONCAT(IFNULL(`sql`, ''), ' search'); diff --git a/setup/sql/updates/1775758635_01.sql b/setup/sql/updates/1775758635_01.sql new file mode 100644 index 00000000..6d163b57 --- /dev/null +++ b/setup/sql/updates/1775758635_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' profiler'); diff --git a/setup/tools/CLISetup.class.php b/setup/tools/CLISetup.class.php index 5529f9dd..cab2a554 100644 --- a/setup/tools/CLISetup.class.php +++ b/setup/tools/CLISetup.class.php @@ -46,6 +46,7 @@ class CLISetup 'force' => [self::OPT_GRP_MISC, ['f'], self::ARGV_NONE, 'Force existing files to be overwritten.', '' ], 'locales' => [self::OPT_GRP_MISC, [], self::ARGV_ARRAY | self::ARGV_OPTIONAL, 'Limit setup to enUS, frFR, deDE, zhCN, esES and/or ruRU. (does not override config settings)', '=<regionCodes,>'], 'datasrc' => [self::OPT_GRP_MISC, [], self::ARGV_OPTIONAL, 'Manually point to directory with extracted mpq files. This is limited to setup/ (default: setup/mpqdata/)', '=path/' ], + 'step' => [self::OPT_GRP_MISC, [], self::ARGV_REQUIRED, 'Start setup at given step (can be used to better automate the setup process).', '=step' ], ); private static $utilScriptRefs = []; @@ -650,10 +651,10 @@ class CLISetup return false; } - if (DB::Aowow()->selectCell('SHOW TABLES LIKE ?', 'dbc_'.$name) && DB::Aowow()->selectCell('SELECT count(1) FROM ?#', 'dbc_'.$name)) + if (DB::Aowow()->selectCell('SHOW TABLES LIKE %s', 'dbc_'.$name) && DB::Aowow()->selectCell('SELECT count(1) FROM %n', 'dbc_'.$name)) return true; - $dbc = new DBC($name, ['temporary' => self::getOpt('delete')]); + $dbc = new DBCReader($name, ['temporary' => self::getOpt('delete')]); if ($dbc->error) { CLI::write('CLISetup::loadDBC() - required DBC '.$name.'.dbc not found!', CLI::LOG_ERROR); diff --git a/setup/tools/clisetup/account.us.php b/setup/tools/clisetup/account.us.php index 9cc03d9d..905ae979 100644 --- a/setup/tools/clisetup/account.us.php +++ b/setup/tools/clisetup/account.us.php @@ -64,26 +64,29 @@ CLISetup::registerUtility(new class extends UtilityScript else $email = ''; - if ($this->fields && CLI::read($this->fields, $uiAccount)) + if ($this->fields && CLI::read($this->fields, $uiAccount) && $uiAccount) { CLI::write(); - if (!$name && !Util::validateUsername($uiAccount['name'], $e)) + if (!$name && !Util::validateUsername($uiAccount['name'], $e) && $e) CLI::write(Lang::account($e == 1 ? 'errNameLength' : 'errNameChars'), CLI::LOG_ERROR); else if (!$name) $name = $uiAccount['name']; - if (!$passw && !Util::validatePassword($uiAccount['pass1'], $e)) - CLI::write(Lang::account($e == 1 ? 'errPassLength' : 'errPassChars'), CLI::LOG_ERROR); + if (!$passw && !Util::validatePassword($uiAccount['pass1'], $e) && $e) + CLI::write($e == 1 ? Lang::account('errPassLength') : Lang::main('intError'), CLI::LOG_ERROR); else if (!$passw && $uiAccount['pass1'] != $uiAccount['pass2']) CLI::write(Lang::account('passMismatch'), CLI::LOG_ERROR); else if (!$passw) $passw = $uiAccount['pass1']; - if (!$email && Util::validateEmail($uiAccount['email'])) + if (!$email && !empty($uiAccount['email']) && Util::validateEmail($uiAccount['email'])) $email = $uiAccount['email']; - else if (!$email && $uiAccount && $uiAccount['email']) - CLI::write('[account] email invalid ... using default: ' . Cfg::get('CONTACT_EMAIL'), CLI::LOG_INFO); + else if (!$email && empty($uiAccount['email'])) + { + $email = Cfg::get('CONTACT_EMAIL'); + CLI::write('[account] no email given, using default: ' . Cfg::get('CONTACT_EMAIL'), CLI::LOG_INFO); + } } else if ($this->fields) { @@ -93,20 +96,20 @@ CLISetup::registerUtility(new class extends UtilityScript return true; } - if (DB::Aowow()->SelectCell('SELECT 1 FROM ?_account WHERE LOWER(`username`) = LOWER(?) AND (`status` <> ?d OR (`status` = ?d AND `statusTimer` > UNIX_TIMESTAMP()))', $name, ACC_STATUS_NEW, ACC_STATUS_NEW)) + if (!$name || !$passw || !$email) + return false; + + if ($username = DB::Aowow()->selectCell('SELECT `username` FROM ::account WHERE (LOWER(`username`) = LOWER(%s) OR LOWER(`email`) = LOWER(%s)) AND (`status` <> %i OR (`status` = %i AND `statusTimer` > UNIX_TIMESTAMP()))', $name, $email, ACC_STATUS_NEW, ACC_STATUS_NEW)) { - CLI::write('[account] ' . Lang::account('nameInUse'), CLI::LOG_ERROR); + CLI::write('[account] ' . (Util::lower($name) == Util::lower($username) ? Lang::account('nameInUse') : Lang::account('mailInUse')), CLI::LOG_ERROR); CLI::write(); return false; } - if (!$name || !$passw) - return false; - - if (DB::Aowow()->query('REPLACE INTO ?_account (`login`, `passHash`, `username`, `joindate`, `email`, `userGroups`, `userPerms`) VALUES (?, ?, ?, UNIX_TIMESTAMP(), ?, ?d, 1)', - $name, User::hashCrypt($passw), $name, $email ?: Cfg::get('CONTACT_EMAIL'), U_GROUP_ADMIN)) + if (DB::Aowow()->qry('REPLACE INTO ::account (`login`, `passHash`, `username`, `joindate`, `email`, `userGroups`, `userPerms`) VALUES (%s, %s, %s, UNIX_TIMESTAMP(), %s, %i, 1)', + $name, User::hashCrypt($passw), $name, $email, U_GROUP_ADMIN)) { - $newId = DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE LOWER(`username`) = LOWER(?)', $name); + $newId = DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE LOWER(`username`) = LOWER(%s)', $name); Util::gainSiteReputation($newId, SITEREP_ACTION_REGISTER); CLI::write("[account] admin ".$name." created successfully", CLI::LOG_OK); @@ -124,7 +127,7 @@ CLISetup::registerUtility(new class extends UtilityScript public function test(?array &$error = []) : bool { $error = []; - return !!DB::Aowow()->selectCell('SELECT `id` FROM ?_account WHERE `userPerms` = 1'); + return !!DB::Aowow()->selectCell('SELECT `id` FROM ::account WHERE `userPerms` = 1'); } }); diff --git a/setup/tools/clisetup/datagen.us.php b/setup/tools/clisetup/datagen.us.php index 11385ffe..03491690 100644 --- a/setup/tools/clisetup/datagen.us.php +++ b/setup/tools/clisetup/datagen.us.php @@ -111,6 +111,14 @@ CLISetup::registerUtility(new class extends UtilityScript CLI::write('[sql] subscript \''.$cmd.'\' returned '.($success ? 'successfully' : 'with errors'), $success ? CLI::LOG_OK : CLI::LOG_ERROR); CLI::write(); set_time_limit($this->defaultExecTime); // reset to default for the next script + + // try to free memory + unset($scriptRef, $this->generators[$cmd]); + if (gc_enabled()) + { + gc_collect_cycles(); + gc_mem_caches(); + } } return $allOk; diff --git a/setup/tools/clisetup/dbc.us.php b/setup/tools/clisetup/dbc.us.php index 0ae2500c..de910e6d 100644 --- a/setup/tools/clisetup/dbc.us.php +++ b/setup/tools/clisetup/dbc.us.php @@ -40,7 +40,7 @@ CLISetup::registerUtility(new class extends UtilityScript if ($args[0]) $opts['tableName'] = $args[0]; - $dbc = new DBC(strtolower($n), $opts, $args[1] ?: DBC::DEFAULT_WOW_BUILD); + $dbc = new DBCReader(strtolower($n), $opts, $args[1] ?: DBCReader::DEFAULT_WOW_BUILD); if ($dbc->error) { CLI::write('[dbc] required DBC '.CLI::bold($n).'.dbc not found!', CLI::LOG_ERROR); @@ -84,7 +84,7 @@ CLISetup::registerUtility(new class extends UtilityScript CLI::write(); CLI::write(' Known DBC files:', -1, false); - $defs = DBC::getDefinitions(); + $defs = DBCReader::getDefinitions(); $letter = ''; $buff = []; diff --git a/setup/tools/clisetup/dbconfig.us.php b/setup/tools/clisetup/dbconfig.us.php index 3bc433df..e2120989 100644 --- a/setup/tools/clisetup/dbconfig.us.php +++ b/setup/tools/clisetup/dbconfig.us.php @@ -85,8 +85,8 @@ CLISetup::registerUtility(new class extends UtilityScript { CLI::write(); if (!DB::isConnectable(DB_AUTH) || !$this->test()) - CLI::write('[db] auth server not yet set up.', CLI::LOG_ERROR); - else if ($realms = DB::Auth()->select('SELECT `id` AS "0", `name` AS "1", `icon` AS "2", `timezone` AS "3", `allowedSecurityLevel` AS "4" FROM realmlist')) + CLI::write('[db] auth db not yet set up.', CLI::LOG_ERROR); + else if ($realms = DB::Auth()->selectAssoc('SELECT `id` AS "0", `name` AS "1", `icon` AS "2", `timezone` AS "3", `allowedSecurityLevel` AS "4" FROM realmlist')) { $tbl = [['Realm Id', 'Name', 'Type', 'Region', 'GMLevel', 'Status']]; foreach ($realms as [$id, $name, $icon, $region, $level]) @@ -216,13 +216,13 @@ CLISetup::registerUtility(new class extends UtilityScript switch ($idx) { case DB_AOWOW: - if (DB::Aowow()->selectCell('SHOW TABLES LIKE ?', 'aowow_dbversion')) + if (DB::Aowow()->selectCell('SHOW TABLES LIKE %s', 'aowow_dbversion')) Cfg::load(); // first time load after successful db setup else $error[] = ' * '.$what.': doesn\'t seem to contain aowow tables!'; break; case DB_WORLD: - if (!DB::World()->selectCell('SHOW TABLES LIKE ?', 'version')) + if (!DB::World()->selectCell('SHOW TABLES LIKE %s', 'version')) $error[] = ' * '.$what.': doesn\'t seem to contain TrinityCore world tables!'; else if (DB::World()->selectCell('SELECT `cache_id` FROM `version`') < TDB_WORLD_MINIMUM_VER) $error[] = ' * '.$what.': TDB world db is structurally outdated! (min rev.: '.CLI::bold(TDB_WORLD_MINIMUM_VER).')'; @@ -255,9 +255,9 @@ CLISetup::registerUtility(new class extends UtilityScript switch ($idx) { case DB_AOWOW: - if (DB::Aowow()->selectCell('SHOW TABLES LIKE ?', 'aowow_dbversion')) + if (DB::Aowow()->selectCell('SHOW TABLES LIKE %s', 'aowow_dbversion')) { - if ($date = DB::Aowow()->selectCell('SELECT `date` FROM ?_dbversion')) + if ($date = DB::Aowow()->selectCell('SELECT `date` FROM ::dbversion')) { $note = 'AoWoW DB version @ ' . date(Util::$dateFormatInternal, $date); $ok = true; @@ -266,10 +266,10 @@ CLISetup::registerUtility(new class extends UtilityScript $note = CLI::yellow('AoWoW DB version empty! Import of DB dump failed?'); } else - $note = CLI::yellow('DB test failed to find dbversion table. setup/db_structure.sql not yet imported?'); + $note = CLI::yellow('DB test failed to find dbversion table. ').CLI::bold('setup/sql/01-db_structure.sql').CLI::yellow(' not yet imported?'); break; case DB_WORLD: - if (DB::World()->selectCell('SHOW TABLES LIKE ?', 'version')) + if (DB::World()->selectCell('SHOW TABLES LIKE %s', 'version')) { [$vString, $vNo] = DB::World()->selectRow('SELECT `db_version` AS "0", `cache_id` AS "1" FROM `version`'); if (strpos($vString, 'TDB') === 0) @@ -289,7 +289,7 @@ CLISetup::registerUtility(new class extends UtilityScript else $note = CLI::yellow('DB test found unexpected vendor in expected version table. Uhh.. Good Luck..!?'); } - else if (DB::World()->selectCell('SHOW TABLES LIKE ?', 'db_version')) + else if (DB::World()->selectCell('SHOW TABLES LIKE %s', 'db_version')) $note = CLI::yellow('DB test found MaNGOS styled version table. MaNGOS DB structure is not supported!'); else $note = CLI::yellow('DB test failed to find version table. TrinityDB world not yet imported?'); @@ -309,15 +309,21 @@ CLISetup::registerUtility(new class extends UtilityScript $buff[] = $dbInfo['prefix'] ? 'pre.: '.$dbInfo['prefix'] : ''; $buff[] = $note; } + else if ($idx == DB_AUTH) + $buff[] = CLI::bold('<optional>'); else - $buff[] = CLI::bold('<empty>'); + $buff[] = CLI::bold('<required>'); return $buff; } public function writeCLIHelp() : bool { - CLI::write(' Remember to use the correct Realm Id from '.CLI::bold('`logon`.`realmlist`').' when connecting to your characters DB.', -1, false); + CLI::write(' Connecting to '.CLI::bold('`auth`').' is optional and only required when using the character profiler tool or using the game accounts for login.', -1, false); + CLI::write(); + CLI::write(' Connecting to '.CLI::bold('`characters`').' is optional and only required when using the character profiler tool.', -1, false); + CLI::write(' Remember to use the correct Realm Id from '.CLI::bold('`auth`.`realmlist`').' when connecting to your characters DB.', -1, false); + CLI::write(); CLI::write(' To remove a db entry edit it and leave all fields empty.', -1, false); CLI::write(); CLI::write(); diff --git a/setup/tools/clisetup/filegen.us.php b/setup/tools/clisetup/filegen.us.php index 70188e42..426a0769 100644 --- a/setup/tools/clisetup/filegen.us.php +++ b/setup/tools/clisetup/filegen.us.php @@ -126,6 +126,14 @@ CLISetup::registerUtility(new class extends UtilityScript CLI::write(); set_time_limit($this->defaultExecTime); // reset to default for the next script + + // try to free memory + unset($scriptRef, $this->generators[$cmd]); + if (gc_enabled()) + { + gc_collect_cycles(); + gc_mem_caches(); + } } return $allOk; diff --git a/setup/tools/clisetup/setup.us.php b/setup/tools/clisetup/setup.us.php index 0c84b0a1..5b8c4762 100644 --- a/setup/tools/clisetup/setup.us.php +++ b/setup/tools/clisetup/setup.us.php @@ -25,9 +25,8 @@ CLISetup::registerUtility(new class extends UtilityScript public const SITE_LOCK = CLISetup::LOCK_ON; - private $startStep = 0; - private $dynArgs = ['doSql' => [], 'doBuild' => []]; // ref to pass commands from 'update' to 'sync' - private $steps = array( + private $dynArgs = ['doSql' => [], 'doBuild' => []]; // ref to pass commands from 'update' to 'sync' + private $steps = array( // [staticUS, $name, [...args]] ['database', '', []], ['configure', '', []], @@ -39,21 +38,21 @@ CLISetup::registerUtility(new class extends UtilityScript private const STEP_FILE = 'cache/setup/firstrun'; - // Note! Must be loaded after all SetupScripts have been registered - public function __construct() + private function getSavedStartStep() : int { - /********************/ - /* get current step */ - /********************/ - if (file_exists(self::STEP_FILE)) { $rows = file(self::STEP_FILE); if ((int)$rows[0] == AOWOW_REVISION) - $this->startStep = (int)$rows[1]; + return (int)$rows[1]; } + return 0; + } + // Note! Must be loaded after all SetupScripts have been registered + public function __construct() + { /****************/ /* define steps */ /****************/ @@ -78,23 +77,44 @@ CLISetup::registerUtility(new class extends UtilityScript // args: null, null, null, null // nnnn public function run(&$args) : bool { - if ($this->startStep) + /******************/ + /* get start step */ + /******************/ + + $startStep = 0; + if (($cliStartStep = CLISetup::getOpt('step')) !== false) { - CLI::write('[setup] found firstrun progression info. (Halted on subscript: '.($this->steps[$this->startStep][1] ?: $this->steps[$this->startStep][0]).')', CLI::LOG_INFO); - $msg = ''; - if (!CLI::read(['x' => ['continue setup? (y/n)', true, true, '/y|n/i']], $uiN) || !$uiN || strtolower($uiN['x']) == 'n') + $startStep = ((int)$cliStartStep) - 1; + if ($startStep < 0 || $startStep >= count($this->steps)) { - $msg = '[setup] starting from scratch...'; - $this->startStep = 0; + CLI::write('Invalid step number. Use --step <1-'.count($this->steps).'>', CLI::LOG_ERROR); + return false; + } + + CLI::write('[setup] starting from step '.($startStep + 1).'...'); + } + elseif (($startStep = $this->getSavedStartStep()) !== 0) + { + CLI::write('[setup] found firstrun progression info. (Halted on subscript: '.($this->steps[$startStep][1] ?: $this->steps[$startStep][0]).')', CLI::LOG_INFO); + if (!CLI::read(['x' => ['continue setup? (y/n)', true, true, '/y|n/i']], $uiN)) + { + CLI::write('Failed to read answer. Use --step in a non-interactive environment.', CLI::LOG_ERROR); + return false; } - else - $msg = '[setup] resuming from step '.($this->startStep + 1).'...'; CLI::write(); - CLI::write($msg); + if (strtolower($uiN['x']) == 'n') + { + $startStep = 0; + CLI::write('[setup] starting from scratch...'); + } + else + CLI::write('[setup] resuming from step '.($startStep + 1).'...'); + sleep(1); } + // init temp setup dir if ($info = new \SplFileInfo(self::STEP_FILE)) CLISetup::writeDir($info->getPath()); @@ -106,7 +126,7 @@ CLISetup::registerUtility(new class extends UtilityScript foreach ($this->steps as $idx => [$usName, , $param]) { - if ($this->startStep > $idx) + if ($startStep > $idx) continue; while (true) @@ -147,17 +167,19 @@ CLISetup::registerUtility(new class extends UtilityScript public function writeCLIHelp() : bool { - CLI::write(' usage: php aowow --setup [--locales: --datasrc:]', -1, false); + CLI::write(' usage: php aowow --setup [--locales: --datasrc:] [--step=<step>]', -1, false); CLI::write(); CLI::write(' Initially essential connection information are set up and basic connectivity tests run afterwards.', -1, false); + CLI::write(); CLI::write(' In the main stage dbc and world data is compiled into the database and required sound, image and data files are generated.', -1, false); CLI::write(' This should not require further input and will take about 15-20 minutes, plus 10 minutes per additional locale.', -1, false); + CLI::write(); CLI::write(' Lastly pending updates are applied and you are prompted to create an administrator account.', -1, false); - if ($this->startStep) + if (($startStep = $this->getSavedStartStep()) !== 0) { CLI::write(); - CLI::write(' You are currently on step '.($this->startStep + 1).' / '.count($this->steps).' ('.($this->steps[$this->startStep][1] ?: $this->steps[$this->startStep][0]).'). You can resume or restart the setup process.', -1, false); + CLI::write(' You are currently on step '.($startStep + 1).' / '.count($this->steps).' ('.($this->steps[$startStep][1] ?: $this->steps[$startStep][0]).'). You can resume or restart the setup process.', -1, false); } CLI::write(); diff --git a/setup/tools/clisetup/siteconfig.us.php b/setup/tools/clisetup/siteconfig.us.php index 405061b7..25b8247a 100644 --- a/setup/tools/clisetup/siteconfig.us.php +++ b/setup/tools/clisetup/siteconfig.us.php @@ -406,7 +406,7 @@ CLISetup::registerUtility(new class extends UtilityScript $prot = Cfg::get('FORCE_SSL') ? 'https://' : 'http://'; $cases = array( - 'site_host' => [$prot, Cfg::get('SITE_HOST'), '/robots.txt'], + 'site_host' => [$prot, Cfg::get('SITE_HOST'), '/index.php'], 'static_host' => [$prot, Cfg::get('STATIC_HOST'), '/css/aowow.css'] ); @@ -451,7 +451,7 @@ CLISetup::registerUtility(new class extends UtilityScript $res = get_headers($protocol.$host.$testFile, true, $ctx); - if (!preg_match('/HTTP\/[0-9\.]+\s+([0-9]+)/', $res[0], $m)) + if (!$res || !preg_match('/HTTP\/[0-9\.]+\s+([0-9]+)/', $res[0], $m)) return false; $status = $m[1]; diff --git a/setup/tools/clisetup/sync.us.php b/setup/tools/clisetup/sync.us.php index 05b30367..38d20d81 100644 --- a/setup/tools/clisetup/sync.us.php +++ b/setup/tools/clisetup/sync.us.php @@ -45,14 +45,14 @@ CLISetup::registerUtility(new class extends UtilityScript { $io = ['doSql' => $s, 'doneSql' => []]; CLISetup::run('sql', $io); - DB::Aowow()->query('UPDATE ?_dbversion SET `sql` = ?', implode(' ', array_diff($io['doSql'], $io['doneSql']))); + DB::Aowow()->qry('UPDATE ::dbversion SET `sql` = %s', implode(' ', array_diff($io['doSql'], $io['doneSql']))); } if ($b) { $io = ['doBuild' => $b, 'doneBuild' => []]; CLISetup::run('build', $io); - DB::Aowow()->query('UPDATE ?_dbversion SET `build` = ?', implode(' ', array_diff($io['doBuild'], $io['doneBuild']))); + DB::Aowow()->qry('UPDATE ::dbversion SET `build` = %s', implode(' ', array_diff($io['doBuild'], $io['doneBuild']))); } return true; diff --git a/setup/tools/clisetup/update.us.php b/setup/tools/clisetup/update.us.php index ba957aaa..9b4cc185 100644 --- a/setup/tools/clisetup/update.us.php +++ b/setup/tools/clisetup/update.us.php @@ -32,7 +32,7 @@ CLISetup::registerUtility(new class extends UtilityScript public function __construct() { if (DB::isConnected(DB_AOWOW)) - [$this->date, $this->part] = array_values(DB::Aowow()->selectRow('SELECT `date`, `part` FROM ?_dbversion')); + [$this->date, $this->part] = array_values(DB::Aowow()->selectRow('SELECT `date`, `part` FROM ::dbversion')); } // args: null, null, sqlToDo, buildToDo // nnoo @@ -75,21 +75,21 @@ CLISetup::registerUtility(new class extends UtilityScript // semicolon at the end -> end of query if (substr(trim($line), -1, 1) == ';') { - if (DB::Aowow()->query($updQuery)) + if (DB::Aowow()->qry($updQuery)) $nQuerys++; $updQuery = ''; } } - DB::Aowow()->query('UPDATE ?_dbversion SET `date`= ?d, `part` = ?d', $fDate, $fPart); + DB::Aowow()->qry('UPDATE ::dbversion SET `date`= %i, `part` = %i', $fDate, $fPart); CLI::write(' -> '.date('d.m.Y', $fDate).' #'.$fPart.': '.$nQuerys.' queries applied', CLI::LOG_OK); } CLI::write('[update] ' . ($nFiles ? 'applied '.$nFiles.' update(s)' : 'db is already up to date'), CLI::LOG_OK); // fetch sql/build after applying updates, as they may contain sync-prompts - [$sql, $build] = DB::Aowow()->selectRow('SELECT `sql` AS "0", `build` AS "1" FROM ?_dbversion'); + [$sql, $build] = DB::Aowow()->selectRow('SELECT `sql` AS "0", `build` AS "1" FROM ::dbversion'); $sql = trim($sql) ? array_unique(explode(' ', trim(preg_replace('/[^a-z_\-]+/i', ' ', $sql)))) : []; $build = trim($build) ? array_unique(explode(' ', trim(preg_replace('/[^a-z_\-]+/i', ' ', $build)))) : []; @@ -111,8 +111,13 @@ CLISetup::registerUtility(new class extends UtilityScript CLI::write(); CLI::write(' Checks /setup/sql/updates for new *.sql files and applies them. If required by an applied update, the --sql and --build command are triggered afterwards.', -1, false); CLI::write(' Use this after fetching the latest rev. from Github.', -1, false); - CLI::write(); - CLI::write(' Last Update: '.date(Util::$dateFormatInternal, $this->date).' (Part #'.$this->part.')', -1, false); + + if ($this->date) + { + CLI::write(); + CLI::write(' Last Update: '.date(Util::$dateFormatInternal, $this->date).' (Part #'.$this->part.')', -1, false); + } + CLI::write(); CLI::write(); diff --git a/setup/tools/dbc.class.php b/setup/tools/dbc.class.php deleted file mode 100644 index fd72555b..00000000 --- a/setup/tools/dbc.class.php +++ /dev/null @@ -1,500 +0,0 @@ -<?php - -namespace Aowow; - -/* - DBC::read - PHP function for loading DBC file into array - This file is a part of AoWoW project. - Copyright (C) 2009-2010 Mix <ru-mangos.ru> - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see <http://www.gnu.org/licenses/>. -*/ - -if (!defined('AOWOW_REVISION')) - die('illegal access'); - -if (!CLI) - die('not in cli mode'); - - -class DBC -{ - private $isGameTable = false; - private $localized = false; - private $tempTable = true; - private $tableName = ''; - - private $dataBuffer = []; - private $bufferSize = 500; - - private static $structs = []; - - private $fileRefs = []; - private $curFile = ''; - - public $error = true; - public $fields = []; - public $format = []; - public $file = ''; - - private $macro = array( - 'LOC' => 'sxsssxsxsxxxxxxxx', // pre 4.x locale block (in use) - 'X_LOC' => 'xxxxxxxxxxxxxxxxx' // pre 4.x locale block (unused) - ); - - private $unpackFmt = array( // Supported format characters: - 'x' => 'x/x/x/x', // x - not used/unknown, 4 bytes - 'X' => 'x', // X - not used/unknown, 1 byte - 's' => 'V', // s - string block index, 4 bytes - 'S' => 'V', // S - string block index, 4 bytes - localized; autofill - 'f' => 'f', // f - float, 4 bytes (rounded to 4 digits after comma) - 'i' => 'l', // i - signed int, 4 bytes - 'u' => 'V', // u - unsigned int, 4 bytes - 'b' => 'C', // b - unsigned char, 1 byte - 'd' => 'x4', // d - ordered by this field, not included in array - 'n' => 'V' // n - int, 4 bytes, ordered by this field - ); - - public const DEFAULT_WOW_BUILD = '12340'; - private const INI_FILE_PATH = 'setup/tools/dbc/%s.ini'; - - public function __construct($file, $opts = [], string $wowBuild = self::DEFAULT_WOW_BUILD) - { - self::loadStructs($wowBuild); - - $file = strtolower($file); - if (empty(self::$structs[$file])) - { - CLI::write('no structure known for '.$file.'.dbc, build '.$wowBuild, CLI::LOG_ERROR); - return; - } - - foreach (self::$structs[$file] as $name => $type) - { - // resolove locale macro - if (isset($this->macro[$type])) - { - $this->localized = true; - for ($i = 0; $i < strlen($this->macro[$type]); $i++) - $this->format[$name.'_loc'.$i] = $this->macro[$type][$i]; - } - else - { - $this->format[$name] = $type; - if ($type == 'S') - $this->localized = true; - } - } - - $this->file = $file; - - if (is_bool($opts['temporary'])) - $this->tempTable = $opts['temporary']; - - if (!empty($opts['tableName'])) - $this->tableName = $opts['tableName']; - else - $this->tableName = 'dbc_'.$file; - - // gameTable-DBCs don't have an index and are accessed through value order - // allas, you cannot do this with mysql, so we add a 'virtual' index - $this->isGameTable = array_values($this->format) == ['f'] && substr($file, 0, 2) == 'gt'; - - $foundMask = 0x0; - foreach (Locale::cases() as $loc) - { - if (!in_array($loc, CLISetup::$locales)) - continue; - - if ($foundMask & (1 << $loc->value)) - continue; - - foreach ($loc->gameDirs() as $dir) - { - $fullPath = CLI::nicePath($this->file.'.dbc', CLISetup::$srcDir, $dir, 'DBFilesClient'); - if (!CLISetup::fileExists($fullPath)) - continue; - - $this->curFile = $fullPath; - if ($this->validateFile($loc)) - { - $foundMask |= (1 << $loc->value); - break; - } - } - } - - if (!$this->fileRefs) - { - CLI::write('no suitable files found for '.$file.'.dbc, aborting.', CLI::LOG_ERROR); - return; - } - - // check if DBCs are identical - $headers = array_column($this->fileRefs, 2); - $x = array_unique(array_column($headers, 'recordCount')); - if (count($x) != 1) - { - CLI::write('some DBCs have different record counts ('.implode(', ', $x).' respectively). cannot merge!', CLI::LOG_ERROR); - return; - } - $x = array_unique(array_column($headers, 'fieldCount')); - if (count($x) != 1) - { - CLI::write('some DBCs have differenct field counts ('.implode(', ', $x).' respectively). cannot merge!', CLI::LOG_ERROR); - return; - } - $x = array_unique(array_column($headers, 'recordSize')); - if (count($x) != 1) - { - CLI::write('some DBCs have differenct record sizes ('.implode(', ', $x).' respectively). cannot merge!', CLI::LOG_ERROR); - return; - } - - $this->error = false; - } - - public function readFile() - { - if (!$this->file || $this->error) - return []; - - $this->createTable(); - - if ($this->localized) - CLI::write(' - DBC: reading and merging '.$this->file.'.dbc for locales '.Lang::concat(array_keys($this->fileRefs), callback: fn($x) => CLI::bold(Locale::from($x)->name))); - else - CLI::write(' - DBC: reading '.$this->file.'.dbc'); - - if (!$this->read()) - { - CLI::write(' - DBC::read() returned with error', CLI::LOG_ERROR); - return false; - } - - return true; - } - - public function getTableName() : string - { - return $this->tableName; - } - - public static function getDefinitions() : array - { - if (empty(self::$structs)) - self::loadStructs(); - - return array_keys(self::$structs); - } - - private static function loadStructs(string $wowBuild = self::DEFAULT_WOW_BUILD) : void - { - $structFile = sprintf(self::INI_FILE_PATH, $wowBuild); - - if (!file_exists($structFile)) - { - CLI::write('no structure file found for wow build '.$wowBuild, CLI::LOG_ERROR); - return; - } - - self::$structs = parse_ini_file($structFile, true); - } - - private function endClean() - { - foreach ($this->fileRefs as &$ref) - fclose($ref[0]); - - $this->dataBuffer = null; - } - - private function readHeader(&$handle = null) : array - { - if (!is_resource($handle)) - $handle = fopen($this->curFile, 'rb'); - - if (!$handle) - return []; - - if (fread($handle, 4) != 'WDBC') - { - CLI::write('file '.$this->curFile.' has incorrect magic bytes', CLI::LOG_ERROR); - fclose($handle); - return []; - } - - return unpack('VrecordCount/VfieldCount/VrecordSize/VstringSize', fread($handle, 16)); - } - - private function validateFile(Locale $loc) : bool - { - $filesize = filesize($this->curFile); - if ($filesize < 20) - { - CLI::write('file '.$this->curFile.' is too small for a DBC file', CLI::LOG_ERROR); - return false; - } - - $header = $this->readHeader($handle); - if (!$header) - { - CLI::write('cannot open file '.$this->curFile, CLI::LOG_ERROR); - return false; - } - - // Different debug checks to be sure, that file was opened correctly - $debugStr = '(recordCount='.$header['recordCount']. - ' fieldCount=' .$header['fieldCount'] . - ' recordSize=' .$header['recordSize'] . - ' stringSize=' .$header['stringSize'] .')'; - - if ($header['recordCount'] * $header['recordSize'] + $header['stringSize'] + 20 != $filesize) - { - CLI::write('file '.$this->curFile.' has incorrect size '.$filesize.': '.$debugStr, CLI::LOG_ERROR); - fclose($handle); - return false; - } - - if ($header['fieldCount'] != count($this->format)) - { - CLI::write('incorrect format ('.implode('', $this->format).') specified for file '.$this->curFile.' fieldCount='.$header['fieldCount'], CLI::LOG_ERROR); - fclose($handle); - return false; - } - - $this->fileRefs[$loc->value] = [$handle, $this->curFile, $header]; - - return true; - } - - private function createTable() - { - if ($this->error) - return; - - $pKey = ''; - $query = 'CREATE '.($this->tempTable ? 'TEMPORARY' : '').' TABLE `'.$this->tableName.'` ('; - - if ($this->isGameTable) - { - $query .= '`idx` INT SIGNED NOT NULL, '; - $pKey = 'idx'; - } - - foreach ($this->format as $name => $type) - { - switch ($type) - { - case 'f': - $query .= '`'.$name.'` FLOAT NOT NULL, '; - break; - case 'S': - for ($l = 0; $l < strlen($this->macro['LOC']); $l++) - if ($this->macro['LOC'][$l] == 's') - $query .= '`'.$name.'_loc'.$l.'` TEXT NULL, '; - - break; - case 's': - $query .= '`'.$name.'` TEXT NULL, '; - break; - case 'b': - $query .= '`'.$name.'` TINYINT UNSIGNED NOT NULL, '; - break; - case 'i': - case 'n': - $query .= '`'.$name.'` INT SIGNED NOT NULL, '; - break; - case 'u': - $query .= '`'.$name.'` INT UNSIGNED NOT NULL, '; - break; - default: // 'x', 'X', 'd' - continue 2; - } - - if ($type == 'n') - $pKey = $name; - } - - if ($pKey) - $query .= 'PRIMARY KEY (`'.$pKey.'`) '; - else - $query = substr($query, 0, -2); - - $query .= ') COLLATE=\'utf8mb4_unicode_ci\' ENGINE=InnoDB'; - - DB::Aowow()->query('DROP TABLE IF EXISTS ?#', $this->tableName); - DB::Aowow()->query($query); - } - - private function writeToDB() - { - if (!$this->dataBuffer || $this->error) - return; - - $cols = []; - foreach ($this->format as $n => $type) - { - switch ($type) - { - case 'x': - case 'X': - case 'd': - continue 2; - case 'S': - for ($l = 0; $l < strlen($this->macro['LOC']); $l++) - if ($this->macro['LOC'][$l] == 's') - $cols[] = $n.'_loc'.$l; - break; - default: - $cols[] = $n; - } - } - - if ($this->isGameTable) - array_unshift($cols, 'idx'); - - DB::Aowow()->query('INSERT INTO ?# (?#) VALUES (?a)', $this->tableName, $cols, $this->dataBuffer); - $this->dataBuffer = []; - } - - private function read() - { - // Check that record size also matches - $itr = 0; - $recSize = 0; - $unpackStr = ''; - foreach ($this->format as $ch) - { - if ($ch == 'X' || $ch == 'b') - $recSize += 1; - else - $recSize += 4; - - if (!isset($this->unpackFmt[$ch])) - { - CLI::write('unknown format parameter \''.$ch.'\' in format string', CLI::LOG_ERROR); - return false; - } - - $unpackStr .= '/'.$this->unpackFmt[$ch]; - - if ($ch != 'X' && $ch != 'x') - $unpackStr .= 'f'.$itr; // output can't have numeric key as it gets interpreted as repeat factor here - - $itr++; - } - - $unpackStr = substr($unpackStr, 1); - - // Optimizing unpack string: 'x/x/x/x/x/x' => 'x6' - while (preg_match('/(x\/)+x/', $unpackStr, $r)) - $unpackStr = substr_replace($unpackStr, 'x'.((strlen($r[0]) + 1) / 2), strpos($unpackStr, $r[0]), strlen($r[0])); - - // we asserted all DBCs to be identical in structure. pick first header for checks - $header = reset($this->fileRefs)[2]; - - if ($recSize != $header['recordSize']) - { - CLI::write('format string size ('.$recSize.') for file '.$this->file.' does not match actual size ('.$header['recordSize'].')', CLI::LOG_ERROR); - return false; - } - - // And, finally, extract the records - $strBlock = 4 + 16 + $header['recordSize'] * $header['recordCount']; - - for ($i = 0; $i < $header['recordCount']; $i++) - { - $row = []; - $idx = $i; - - // add 'virtual' enumerator for gt*-dbcs - if ($this->isGameTable) - $row[-1] = $i; - - foreach ($this->fileRefs as $locId => [$handle, $fullPath, $header]) - { - $rec = unpack($unpackStr, fread($handle, $header['recordSize'])); - - $offset = 0; - foreach (array_values($this->format) as $j => $type) - { - if (!isset($rec['f'.$j])) - continue; - - $outIdx = $j + $offset; - - if (!empty($row[$outIdx]) && $type != 'S') - continue; - - switch ($type) - { - case 'S': // localized String - autofill - $offset = substr_count($this->macro['LOC'], 's'); - - for ($k = 0; $k < strlen($this->macro['LOC']); $k++) - { - if ($this->macro['LOC'][$k] != 's') - continue; - - if (!isset($row[$j + $k])) // prep locale fields - $row[$j + $k] = null; - } - - // provide outIdx for passthrough - $outIdx = $j + $locId; - case 's': - $curPos = ftell($handle); - fseek($handle, $strBlock + $rec['f'.$j]); - - $str = $chr = ''; - do - { - $str .= $chr; - $chr = fread($handle, 1); - } - while ($chr != "\000"); - - fseek($handle, $curPos); - $row[$outIdx] = $str; - break; - case 'f': - $row[$outIdx] = round($rec['f'.$j], 8); - break; - case 'n': // DO NOT BREAK! - $idx = $rec['f'.$j]; - default: // nothing special .. 'i', 'u' and the likes - $row[$outIdx] = $rec['f'.$j]; - } - } - - if (!$this->localized) // one match is enough - break; - } - - $this->dataBuffer[$idx] = array_values($row); - - if (count($this->dataBuffer) >= $this->bufferSize) - $this->writeToDB(); - } - - $this->writeToDB(); - - $this->endClean(); - - return true; - } -} - -?> diff --git a/setup/tools/dbc/12340.ini b/setup/tools/dbc/12340.ini index 0d59bbda..808fa661 100644 --- a/setup/tools/dbc/12340.ini +++ b/setup/tools/dbc/12340.ini @@ -39,8 +39,8 @@ UNUSED3 = x [achievement_criteria] id = n refAchievementId = i -type = i -value1 = i +type = I +value1 = I value2 = i value3 = i value4 = i @@ -476,6 +476,9 @@ chance = f [gtcombatratings] ratio = f +[gtnpcmanacostscaler] +factor = f + [gtoctclasscombatratingscalar] idx = n ratio = f diff --git a/setup/tools/dbcreader.class.php b/setup/tools/dbcreader.class.php new file mode 100644 index 00000000..3138f623 --- /dev/null +++ b/setup/tools/dbcreader.class.php @@ -0,0 +1,362 @@ +<?php + +namespace Aowow; + +/* + DBCReader::read - PHP function for loading DBC file into array + This file is a part of AoWoW project. + Copyright (C) 2009-2010 Mix <ru-mangos.ru> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + +if (!CLI) + die('not in cli mode'); + + +class DBCReader +{ + private const /* string */ INI_FILE_PATH = 'setup/tools/dbc/%s.ini'; + private const /* int */ MAX_INSERT_ROWS = 500; + + public const /* string */ DEFAULT_WOW_BUILD = '12340'; + + private bool $isGameTable = false; + private bool $isLocalized = false; + private bool $isTempTable = true; + private string $tableName = ''; + private array $dataBuffer = []; + private array $fileRefs = []; + private array $format = []; + private string $recordFmt = ''; + private array $macro = array( + 'LOC' => 'sxsssxsxsxxxxxxxx', // pre 4.x locale block (in use) + 'X_LOC' => 'xxxxxxxxxxxxxxxxx' // pre 4.x locale block (unused) + ); + private array $unpackFmt = array( // Supported format characters: + 'x' => Primitive::PACK_FMT.'4', // x - not used/unknown, 4 bytes + 'X' => Primitive::PACK_FMT, // X - not used/unknown, 1 byte + 's' => UInt32::PACK_FMT, // s - string block index, 4 bytes + 'S' => UInt32::PACK_FMT, // S - string block index, 4 bytes - localized; autofill + 'f' => Double::PACK_FMT, // f - float, 4 bytes (rounded to 4 digits after comma) + 'i' => Int32::PACK_FMT, // i - signed int, 4 bytes + 'I' => Int32::PACK_FMT, // I - signed int, 4 bytes, sql index + 'u' => UInt32::PACK_FMT, // u - unsigned int, 4 bytes + 'U' => UInt32::PACK_FMT, // U - unsigned int, 4 bytes, sql index + 'b' => UInt8::PACK_FMT, // b - unsigned char, 1 byte + 'd' => Primitive::PACK_FMT.'4', // d - ordered by this field, not included in array + 'n' => UInt32::PACK_FMT // n - unsigned int, 4 bytes, sql primary key + ); + + private static array $structs = []; + + public bool $error = true; + + public function __construct(public string $file, array $opts = [], string $wowBuild = self::DEFAULT_WOW_BUILD) + { + self::loadStructs($wowBuild); + + $this->file = strtolower($this->file); + if (empty(self::$structs[$this->file])) + { + CLI::write('no structure known for '.$this->file.'.dbc, build '.$wowBuild, CLI::LOG_ERROR); + return; + } + + foreach (self::$structs[$this->file] as $name => $type) + { + // resolove locale macro + if (isset($this->macro[$type])) + { + $this->isLocalized = true; + for ($i = 0; $i < strlen($this->macro[$type]); $i++) + { + $this->format[$name.'_loc'.$i] = $this->macro[$type][$i]; + $this->recordFmt .= '/'.$this->unpackFmt[$this->macro[$type][$i]].$name.'_loc'.$i; + } + } + else if (!isset($this->unpackFmt[$type])) + { + CLI::write('unknown format parameter '.CLI::bold($type).' at for field '.CLI::bold($name).' in format string', CLI::LOG_ERROR); + return; + } + else + { + $this->format[$name] = $type; + $this->recordFmt .= '/'.$this->unpackFmt[$type]; + if ($type !== 'x' && $type !== 'X') + $this->recordFmt .= $name; + + if ($type === 'S') + $this->isLocalized = true; + } + } + + // Optimizing unpack string: 'x/x/x/x/x/x' => 'x6' + $this->recordFmt = preg_replace_callback('/x(\/x)+/i', fn($m) => 'x'.((strlen($m[0]) + 1) / 2), substr($this->recordFmt, 1)); + + if (is_bool($opts['temporary'])) + $this->isTempTable = $opts['temporary']; + + if (!empty($opts['tableName'])) + $this->tableName = $opts['tableName']; + else + $this->tableName = 'dbc_'.$this->file; + + // gameTable-DBCs don't have an index and are accessed through value order + // allas, you cannot do this with mysql, so we add a 'virtual' index + $this->isGameTable = array_values($this->format) == ['f'] && substr($this->file, 0, 2) == 'gt'; + + $foundMask = 0x0; + foreach (Locale::cases() as $loc) + { + if (!in_array($loc, CLISetup::$locales)) + continue; + + if ($foundMask & (1 << $loc->value)) + continue; + + foreach ($loc->gameDirs() as $dir) + { + $fullPath = CLI::nicePath($this->file.'.dbc', CLISetup::$srcDir, $dir, 'DBFilesClient'); + if (!CLISetup::fileExists($fullPath)) + continue; + + $dbcFile = new DBCFile($fullPath); + if ($dbcFile->error) + { + CLI::write($dbcFile->error, CLI::LOG_ERROR); + unset($dbcFile); + continue; + } + + if ($dbcFile->nCols != count($this->format)) + { + CLI::write('incorrect format specified for file '.$this->file.' - expected fields: '.count($this->format).' read fields: '.$dbcFile->nCols, CLI::LOG_ERROR); + unset($dbcFile); + continue; + } + + $recSize = 0; + foreach ($this->format as $ch) + $recSize += ($ch == 'X' || $ch == 'b') ? 1 : 4; + + if ($recSize != $dbcFile->recordSize) + { + CLI::write('format string size ('.$recSize.') for file '.$this->file.' does not match actual size ('.$dbcFile->recordSize.')', CLI::LOG_ERROR); + unset($dbcFile); + continue; + } + + $this->fileRefs[$loc->value] = $dbcFile; + $foundMask |= (1 << $loc->value); + } + } + + if (!$this->fileRefs) + { + CLI::write('no suitable files found for '.$this->file.'.dbc, aborting.', CLI::LOG_ERROR); + return; + } + + // check if DBCs are identical + + $tests = ['nRows' => null, 'nCols' => null, 'recordSize' => null]; + foreach ($this->fileRefs as $fileRef) + { + foreach ($tests as $field => $val) + { + if ($val === null) + $tests[$field] = $fileRef->{$field}; + else if ($val != $fileRef->{$field}) + { + CLI::write('some DBCs have different '.$field.': '.CLI::bold($val).' <> '.CLI::bold($fileRef->{$field}).' respectively. cannot merge!', CLI::LOG_ERROR); + return; + } + } + } + + $this->error = false; + } + + public function readFile() : bool + { + if (!$this->file || $this->error) + return false; + + $this->createTable(); + + if ($this->isLocalized) + CLI::write(' - DBC: reading and merging '.$this->file.'.dbc for locales '.Lang::concat(array_keys($this->fileRefs), callback: fn($x) => CLI::bold(Locale::from($x)->name))); + else + CLI::write(' - DBC: reading '.$this->file.'.dbc'); + + $this->read(); + + return true; + } + + public function getTableName() : string + { + return $this->tableName; + } + + public static function getDefinitions() : array + { + if (empty(self::$structs)) + self::loadStructs(); + + return array_keys(self::$structs); + } + + private static function loadStructs(string $wowBuild = self::DEFAULT_WOW_BUILD) : void + { + $structFile = sprintf(self::INI_FILE_PATH, $wowBuild); + + if (!file_exists($structFile)) + { + CLI::write('no structure file found for wow build '.$wowBuild, CLI::LOG_ERROR); + return; + } + + self::$structs = parse_ini_file($structFile, true); + } + + private function endClean() : void + { + unset($this->fileRefs, $this->dataBuffer); + } + + private function createTable() : void + { + if ($this->error) + return; + + $pKey = ''; + $query = 'CREATE '.($this->isTempTable ? 'TEMPORARY' : '').' TABLE `'.$this->tableName.'` ('; + $indizes = []; + + if ($this->isGameTable) + { + $query .= '`idx` INT SIGNED NOT NULL, '; + $pKey = 'idx'; + } + + foreach ($this->format as $name => $type) + { + $query .= match($type) + { + 'f' => '`'.$name.'` FLOAT NOT NULL, ', + 's' => '`'.$name.'` TEXT NULL, ', + 'b' => '`'.$name.'` TINYINT UNSIGNED NOT NULL, ', + 'i', 'I', 'n' => '`'.$name.'` INT SIGNED NOT NULL, ', + 'u', 'U' => '`'.$name.'` INT SIGNED NOT NULL, ', + 'S' => (function ($n) { + $buf = ''; + for ($l = 0; $l < strlen($this->macro['LOC']); $l++) + if ($this->macro['LOC'][$l] == 's') + $buf .= '`'.$n.'_loc'.$l.'` TEXT NULL, '; + return $buf; + })($name), + default => '' // 'x', 'X', 'd' + }; + + if ($this->isGameTable) + continue; + + if ($type == 'I' || $type == 'U') + $indizes[] = $name; + if ($type == 'n') + $pKey = $name; + } + + foreach ($indizes as $i) + $query .= 'KEY `idx_'.$i.'` (`'.$i.'`), '; + + if ($pKey) + $query .= 'PRIMARY KEY (`'.$pKey.'`) '; + else + $query = substr($query, 0, -2); + + $query .= ') COLLATE=\'utf8mb4_unicode_ci\' ENGINE=InnoDB'; + + DB::Aowow()->qry('DROP TABLE IF EXISTS %n', $this->tableName); + DB::Aowow()->qry($query); + } + + private function writeToDB() : void + { + if (!$this->dataBuffer || $this->error) + return; + + DB::Aowow()->qry('INSERT INTO %n %m', $this->tableName, $this->dataBuffer); + + $this->dataBuffer = []; + } + + private function read() : void + { + $nRows = reset($this->fileRefs)->nRows; // set to actual value once we have a file handle + + for ($i = 0; $i < $nRows; $i++) + { + // add 'virtual' enumerator for gt*-dbcs + if ($this->isGameTable) + $this->dataBuffer['idx'][$i] = $i; + + foreach ($this->fileRefs as $locId => $dbcFile) + { + // note that the file pointer is already on the first record as the DBCFile reads its own header + $row = $dbcFile->readRecord($this->recordFmt); + + foreach ($row as $name => $value) + { + $type = $this->format[$name]; + + // handle locale fields for post 3.3.5a DBCs + if ($type === 'S') + { + for ($k = 0; $k < strlen($this->macro['LOC']); $k++) + if ($this->macro['LOC'][$k] === 's') + $this->dataBuffer[$name.'_loc'.$k][$i] ??= null; + + $this->dataBuffer[$name.'_loc'.$locId][$i] ??= $dbcFile->getStringFromBlock($value); + } + if (empty($this->dataBuffer[$name][$i])) + { + if ($type == 's') + $this->dataBuffer[$name][$i] ??= $dbcFile->getStringFromBlock($value); + else + $this->dataBuffer[$name][$i] = $value; + } + } + + if (!$this->isLocalized) // one match is enough + break; + } + + if (count(current($this->dataBuffer)) >= self::MAX_INSERT_ROWS) + $this->writeToDB(); + } + + $this->writeToDB(); + + $this->endClean(); + } +} + +?> diff --git a/setup/tools/filegen/enchants.ss.php b/setup/tools/filegen/enchants.ss.php index fd2e6304..bc6521fc 100644 --- a/setup/tools/filegen/enchants.ss.php +++ b/setup/tools/filegen/enchants.ss.php @@ -67,34 +67,34 @@ CLISetup::registerSetup("build", new class extends SetupScript $slotPointer = [13, 17, 15, 15, 13, 17, 17, 13, 17, null, 17, null, null, 13, null, 13, null, null, null, null, 17]; $castItems = []; - $enchantSpells = DB::Aowow()->select( + $enchantSpells = DB::Aowow()->selectAssoc( 'SELECT s.`id` AS ARRAY_KEY, `effect1MiscValue`, `equippedItemClass`, `equippedItemInventoryTypeMask`, `equippedItemSubClassMask`, `skillLine1`, - IFNULL(i.`name`, ?) AS "iconString", + IFNULL(i.`name`, %s) AS "iconString", `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` - FROM ?_spell s - LEFT JOIN ?_icons i ON i.`id` = s.`iconId` - WHERE `effect1Id` = ?d AND + FROM ::spell s + LEFT JOIN ::icons i ON i.`id` = s.`iconId` + WHERE `effect1Id` = %i AND `name_loc0` NOT LIKE "QA%"', DEFAULT_ICON, SPELL_EFFECT_ENCHANT_ITEM ); $enchIds = array_column($enchantSpells, 'effect1MiscValue'); - $enchantments = new EnchantmentList(array(['id', $enchIds], Cfg::get('SQL_LIMIT_NONE'))); + $enchantments = new EnchantmentList(array(['id', $enchIds])); if ($enchantments->error) { - CLI::write('[enchants] Required table ?_itemenchantment seems to be empty!', CLI::LOG_ERROR); + CLI::write('[enchants] Required table ::itemenchantment seems to be empty!', CLI::LOG_ERROR); CLI::write(); return false; } - $castItems = new ItemList(array(['spellId1', array_keys($enchantSpells)], ['src.typeId', null, '!'], Cfg::get('SQL_LIMIT_NONE'))); + $castItems = new ItemList(array(['spellId1', array_keys($enchantSpells)], ['src.typeId', null, '!'])); if ($castItems->error) { - CLI::write('[enchants] Required table ?_items seems to be empty!', CLI::LOG_ERROR); + CLI::write('[enchants] Required table ::items seems to be empty!', CLI::LOG_ERROR); CLI::write(); return false; } @@ -203,10 +203,10 @@ CLISetup::registerSetup("build", new class extends SetupScript if ($cI->getField('quality') > $ench['quality']) $ench['quality'] = $cI->getField('quality'); - if ($cI->getField('requiredClass') > 0) + if ($rc = $cI->getField('requiredClass')) { - $ench['classes'] = $cI->getField('requiredClass'); - $ench['jsonequip']['classes'] = $cI->getField('requiredClass'); + $ench['classes'] = $rc; + $ench['jsonequip']['classes'] = $rc; } if (!isset($ench['jsonequip']['reqlevel'])) diff --git a/setup/tools/filegen/gems.ss.php b/setup/tools/filegen/gems.ss.php index 85dac421..6ab27d67 100644 --- a/setup/tools/filegen/gems.ss.php +++ b/setup/tools/filegen/gems.ss.php @@ -36,27 +36,27 @@ CLISetup::registerSetup("build", new class extends SetupScript { // sketchy, but should work // id < 36'000 || ilevel < 70 ? BC : WOTLK - $gems = DB::Aowow()->select( + $gems = DB::Aowow()->selectAssoc( 'SELECT i.`id` AS "itemId", i.`name_loc0`, i.`name_loc2`, i.`name_loc3`, i.`name_loc4`, i.`name_loc6`, i.`name_loc8`, - IF (i.`id` < 36000 OR i.`itemLevel` < 70, ?d, ?d) AS "expansion", + IF (i.`id` < 36000 OR i.`itemLevel` < 70, %i, %i) AS "expansion", i.`quality`, ic.`name` AS "icon", i.`gemEnchantmentId` AS "enchId", i.`gemColorMask` AS "colors", i.`requiredSkill`, i.`itemLevel` - FROM ?_items i - JOIN ?_icons ic ON ic.`id` = i.`iconId` + FROM ::items i + JOIN ::icons ic ON ic.`id` = i.`iconId` WHERE i.`gemEnchantmentId` <> 0 ORDER BY i.`id` DESC', EXP_BC, EXP_WOTLK ); - $enchantments = new EnchantmentList(array(['id', array_column($gems, 'enchId')], Cfg::get('SQL_LIMIT_NONE'))); + $enchantments = new EnchantmentList(array(['id', array_column($gems, 'enchId')])); if ($enchantments->error) { - CLI::write('[gems] Required table ?_itemenchantment seems to be empty!', CLI::LOG_ERROR); + CLI::write('[gems] Required table ::itemenchantment seems to be empty!', CLI::LOG_ERROR); CLI::write(); return false; } diff --git a/setup/tools/filegen/glyphs.ss.php b/setup/tools/filegen/glyphs.ss.php index 479a641b..f746276a 100644 --- a/setup/tools/filegen/glyphs.ss.php +++ b/setup/tools/filegen/glyphs.ss.php @@ -33,7 +33,7 @@ CLISetup::registerSetup("build", new class extends SetupScript public function generate() : bool { - $glyphList = DB::Aowow()->Select( + $glyphList = DB::Aowow()->selectAssoc( 'SELECT i.`id` AS "itemId", i.*, IF (g.`typeFlags` & 0x1, 2, 1) AS "type", @@ -44,15 +44,15 @@ CLISetup::registerSetup("build", new class extends SetupScript s1.`skillLine1` AS "skillId", s2.`id` AS "glyphEffect", s2.`id` AS ARRAY_KEY - FROM ?_items i - JOIN ?_spell s1 ON s1.`id` = i.`spellid1` - JOIN ?_glyphproperties g ON g.`id` = s1.`effect1MiscValue` - JOIN ?_spell s2 ON s2.`id` = g.`spellId` - JOIN ?_icons ic ON ic.`id` = s1.`iconIdAlt` - WHERE i.classBak = ?d', + FROM ::items i + JOIN ::spell s1 ON s1.`id` = i.`spellid1` + JOIN ::glyphproperties g ON g.`id` = s1.`effect1MiscValue` + JOIN ::spell s2 ON s2.`id` = g.`spellId` + JOIN ::icons ic ON ic.`id` = s1.`iconIdAlt` + WHERE i.classBak = %i', ITEM_CLASS_GLYPH); - $glyphSpells = new SpellList(array(['s.id', array_keys($glyphList)], Cfg::get('SQL_LIMIT_NONE'))); + $glyphSpells = new SpellList(array(['s.id', array_keys($glyphList)])); foreach (CLISetup::$locales as $loc) { diff --git a/setup/tools/filegen/img-maps.ss.php b/setup/tools/filegen/img-maps.ss.php index 3282210b..380129cf 100644 --- a/setup/tools/filegen/img-maps.ss.php +++ b/setup/tools/filegen/img-maps.ss.php @@ -401,9 +401,9 @@ CLISetup::registerSetup("build", new class extends SetupScript private function prepare() : bool { - $this->wmOverlays = DB::Aowow()->select('SELECT *, `worldMapAreaId` AS ARRAY_KEY, `id` AS ARRAY_KEY2 FROM dbc_worldmapoverlay WHERE `textureString` <> ""'); - $this->wmAreas = DB::Aowow()->select('SELECT `id`, `mapId`, `areaId`, UPPER(`nameINT`) AS `nameINT`, IF(`areaId`, `areaId`, -`id`) AS ARRAY_KEY FROM dbc_worldmaparea'); - $this->dmFloorData = DB::Aowow()->select('SELECT IF(`mapId` IN (?a), -`worldMapAreaId`, `mapId`) AS ARRAY_KEY, GROUP_CONCAT(DISTINCT `floor` SEPARATOR " ") AS "0", COUNT(DISTINCT `floor`) AS "1" FROM dbc_dungeonmap WHERE `worldMapAreaId` <> 0 GROUP BY ARRAY_KEY', self::CONTINENTS); + $this->wmOverlays = DB::Aowow()->selectAssoc('SELECT *, `worldMapAreaId` AS ARRAY_KEY, `id` AS ARRAY_KEY2 FROM dbc_worldmapoverlay WHERE `textureString` <> ""'); + $this->wmAreas = DB::Aowow()->selectAssoc('SELECT `id`, `mapId`, `areaId`, UPPER(`nameINT`) AS `nameINT`, IF(`areaId`, `areaId`, -`id`) AS ARRAY_KEY FROM dbc_worldmaparea'); + $this->dmFloorData = DB::Aowow()->selectAssoc('SELECT IF(`mapId` IN %in, -`worldMapAreaId`, `mapId`) AS ARRAY_KEY, GROUP_CONCAT(DISTINCT `floor` SEPARATOR " ") AS "0", COUNT(DISTINCT `floor`) AS "1" FROM dbc_dungeonmap WHERE `worldMapAreaId` <> 0 GROUP BY ARRAY_KEY', self::CONTINENTS); if (!$this->wmOverlays || !$this->wmAreas || !$this->dmFloorData) { CLI::write('[img-maps] - could not read required dbc files: WorldMapArea.dbc ['.count($this->wmAreas ?: []).' entries]; WorldMapOverlay.dbc ['.count($this->wmOverlays ?: []).'] entries; DungeonMap.dbc ['.count($this->dmFloorData ?: []).' entries]', CLI::LOG_ERROR); diff --git a/setup/tools/filegen/img-talentcalc.ss.php b/setup/tools/filegen/img-talentcalc.ss.php index e9ac6a6d..c32221c5 100644 --- a/setup/tools/filegen/img-talentcalc.ss.php +++ b/setup/tools/filegen/img-talentcalc.ss.php @@ -55,7 +55,7 @@ CLISetup::registerSetup("build", new class extends SetupScript sleep(2); - $tTabs = DB::Aowow()->select('SELECT tt.`creatureFamilyMask`, tt.`textureFile`, tt.`tabNumber`, cc.`fileString` FROM dbc_talenttab tt LEFT JOIN dbc_chrclasses cc ON cc.`id` = IF(tt.`classMask`, LOG(2, tt.`classMask`) + 1, 0)'); + $tTabs = DB::Aowow()->selectAssoc('SELECT tt.`creatureFamilyMask`, tt.`textureFile`, tt.`tabNumber`, cc.`fileString` FROM dbc_talenttab tt LEFT JOIN dbc_chrclasses cc ON cc.`id` = IF(tt.`classMask`, LOG(2, tt.`classMask`) + 1, 0)'); if (!$tTabs) { CLI::write(' - TalentTab.dbc or ChrClasses.dbc is empty...?', CLI::LOG_ERROR); diff --git a/setup/tools/filegen/itemscaling.ss.php b/setup/tools/filegen/itemscaling.ss.php index 4613d579..3ddf0b3a 100644 --- a/setup/tools/filegen/itemscaling.ss.php +++ b/setup/tools/filegen/itemscaling.ss.php @@ -74,7 +74,7 @@ CLISetup::registerSetup("build", new class extends SetupScript $offsets = array_map(function ($v) { // LookupEntry(cr*GT_MAX_LEVEL+level-1) return $v * 100 + 60 - 1; // combat rating where introduced during the transition vanilla > burnig crusade. So at level 60 (at the time) the rating on the item was equal to 1% effect and is still the baseline in 3.3.5a. }, $ratings); - $base = DB::Aowow()->selectCol('SELECT CAST((idx + 1 - 60) / 100 AS UNSIGNED) AS ARRAY_KEY, ratio FROM dbc_gtcombatratings WHERE idx IN (?a)', $offsets); + $base = DB::Aowow()->selectCol('SELECT CAST((idx + 1 - 60) / 100 AS UNSIGNED) AS ARRAY_KEY, ratio FROM dbc_gtcombatratings WHERE idx IN %in', $offsets); /* non-1 scaler in 3.3.5.12340 | ratingId | classId | ratio | @@ -88,7 +88,7 @@ CLISetup::registerSetup("build", new class extends SetupScript $offsets = array_map(function ($v) { // LookupEntry((getClass()-1)*GT_MAX_RATING+cr+1) return (ChrClass::WARRIOR->value - 1) * 32 + $v + 1; // should this be dynamic per pinned character? ITEM_MOD HASTE has a worse scaler for a subset of classes (see table) }, $ratings); - $mods = DB::Aowow()->selectCol('SELECT idx - 1 AS ARRAY_KEY, ratio FROM dbc_gtoctclasscombatratingscalar WHERE idx IN (?a)', $offsets); + $mods = DB::Aowow()->selectCol('SELECT idx - 1 AS ARRAY_KEY, ratio FROM dbc_gtoctclasscombatratingscalar WHERE idx IN %in', $offsets); foreach ($data as &$val) $val = Cfg::get('DEBUG') ? $base[$val].' / '.$mods[$val] : $base[$val] / $mods[$val]; @@ -115,7 +115,7 @@ CLISetup::registerSetup("build", new class extends SetupScript $v = $v ?: '0 AS idx'.$k; // NULL => 0 (plus some index so we can have 2x 0) }); - $data = DB::Aowow()->select('SELECT id AS ARRAY_KEY, '.implode(', ', $fields).' FROM dbc_scalingstatvalues'); + $data = DB::Aowow()->selectAssoc('SELECT id AS ARRAY_KEY, '.implode(', ', $fields).' FROM dbc_scalingstatvalues'); foreach ($data as &$d) $d = array_values($d); // strip indizes @@ -124,7 +124,7 @@ CLISetup::registerSetup("build", new class extends SetupScript private function itemScalingSD() : string { - $data = DB::Aowow()->select('SELECT *, id AS ARRAY_KEY FROM dbc_scalingstatdistribution'); + $data = DB::Aowow()->selectAssoc('SELECT *, id AS ARRAY_KEY FROM dbc_scalingstatdistribution'); foreach ($data as &$row) { $row = array_values($row); diff --git a/setup/tools/filegen/itemsets.ss.php b/setup/tools/filegen/itemsets.ss.php index 0280f2ec..3b367648 100644 --- a/setup/tools/filegen/itemsets.ss.php +++ b/setup/tools/filegen/itemsets.ss.php @@ -43,7 +43,7 @@ CLISetup::registerSetup("build", new class extends SetupScript public function generate() : bool { - $setList = DB::Aowow()->Select('SELECT * FROM ?_itemset ORDER BY `refSetId` DESC'); + $setList = DB::Aowow()->selectAssoc('SELECT * FROM ::itemset ORDER BY `refSetId` DESC'); $jsonBonus = []; foreach (CLISetup::$locales as $loc) diff --git a/setup/tools/filegen/pets.ss.php b/setup/tools/filegen/pets.ss.php index 3f96968e..d5f223e4 100644 --- a/setup/tools/filegen/pets.ss.php +++ b/setup/tools/filegen/pets.ss.php @@ -15,7 +15,7 @@ if (!CLI) name:'Forest Spider', minlevel:5, maxlevel:6, - location:[12], // master-AreaTableId's (?) + location:[12], // master-AreaTableId's (%s) react:[-1,-1], classification:0, // 0:"Normal", 1:"Elite", 2:"Rar Elite", 3:"Boss", 4:"Rar" family:3, // creatureFamily @@ -40,7 +40,7 @@ CLISetup::registerSetup("build", new class extends SetupScript public function generate() : bool { - $petList = DB::Aowow()->Select( + $petList = DB::Aowow()->selectAssoc( 'SELECT cr.`id`, cr.`name_loc0`, cr.`name_loc2`, cr.`name_loc3`, cr.`name_loc4`, cr.`name_loc6`, cr.`name_loc8`, cr.`minLevel`, cr.`maxLevel`, @@ -49,17 +49,17 @@ CLISetup::registerSetup("build", new class extends SetupScript cr.`family`, cr.`displayId1` AS "displayId", cr.`textureString` AS "skin", - LOWER(SUBSTRING_INDEX(cf.`iconString`, "\\\\", -1)) AS "icon", + LOWER(SUBSTRING_INDEX(cf.`iconString`, "\\", -1)) AS "icon", cf.`petTalentType` AS "type" - FROM ?_creature cr - JOIN ?_factiontemplate ft ON ft.`id` = cr.`faction` + FROM ::creature cr + JOIN ::factiontemplate ft ON ft.`id` = cr.`faction` JOIN dbc_creaturefamily cf ON cf.`id` = cr.`family` - WHERE cr.`typeFlags` & 0x1 AND (cr.`cuFlags` & ?d) = 0 + WHERE cr.`typeFlags` & 0x1 AND (cr.`cuFlags` & %i) = 0 ORDER BY cr.`id` ASC', NPC_CU_DIFFICULTY_DUMMY ); - $locations = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, `areaId` AS ARRAY_KEY2, `areaId` FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId`, `areaId`', Type::NPC, array_column($petList, 'id')); + $locations = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, `areaId` AS ARRAY_KEY2, `areaId` FROM ::spawns WHERE `type` = %i AND `typeId` IN %in GROUP BY `typeId`, `areaId`', Type::NPC, array_column($petList, 'id')); foreach (CLISetup::$locales as $loc) { diff --git a/setup/tools/filegen/profiler.ss.php b/setup/tools/filegen/profiler.ss.php index 576d057b..835e229e 100644 --- a/setup/tools/filegen/profiler.ss.php +++ b/setup/tools/filegen/profiler.ss.php @@ -56,10 +56,9 @@ CLISetup::registerSetup("build", new class extends SetupScript $questorder = []; $questtotal = []; $condition = [ - Cfg::get('SQL_LIMIT_NONE'), - 'AND', + DB::AND, [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW | CUSTOM_UNAVAILABLE | CUSTOM_DISABLED, '&'], 0], - [['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY | QUEST_FLAG_REPEATABLE | QUEST_FLAG_AUTO_REWARDED, '&'], 0], + [['flags', QUEST_FLAG_DAILY | QUEST_FLAG_WEEKLY | QUEST_FLAG_TRACKING, '&'], 0], [['specialFlags', QUEST_FLAG_SPECIAL_REPEATABLE | QUEST_FLAG_SPECIAL_DUNGEON_FINDER | QUEST_FLAG_SPECIAL_MONTHLY, '&'], 0] ]; @@ -68,7 +67,7 @@ CLISetup::registerSetup("build", new class extends SetupScript if ($cat2 < 0) continue; - $cond = array_merge($condition, [['zoneOrSort', $cat]]); + $cond = array_merge($condition, [['questSortId', $cat]]); $questz = new QuestList($cond); if ($questz->error) continue; @@ -125,11 +124,7 @@ CLISetup::registerSetup("build", new class extends SetupScript private function titles(): void { - $condition = array( - Cfg::get('SQL_LIMIT_NONE'), - [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], - ); - $titlez = new TitleList($condition); + $titlez = new TitleList(array([['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0])); // get titles for exclusion foreach ($titlez->iterate() as $id => $__) @@ -161,14 +156,13 @@ CLISetup::registerSetup("build", new class extends SetupScript private function mounts() : void { $condition = array( - Cfg::get('SQL_LIMIT_NONE'), [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], ['typeCat', -5], ['castTime', 0, '!'] ); $mountz = new SpellList($condition); - $conditionSet = DB::World()->selectCol('SELECT `SourceEntry` AS ARRAY_KEY, `ConditionValue1` FROM conditions WHERE `SourceTypeOrReferenceId` = ?d AND `ConditionTypeOrReference` = ?d AND `SourceEntry` IN (?a)', Conditions::SRC_SPELL, Conditions::SKILL, $mountz->getFoundIDs()); + $conditionSet = DB::World()->selectCol('SELECT `SourceEntry` AS ARRAY_KEY, `ConditionValue1` FROM conditions WHERE `SourceTypeOrReferenceId` = %i AND `ConditionTypeOrReference` = %i AND `SourceEntry` IN %in', Conditions::SRC_SPELL, Conditions::SKILL, $mountz->getFoundIDs()); // get mounts for exclusion foreach ($conditionSet as $mount => $skill) @@ -210,12 +204,11 @@ CLISetup::registerSetup("build", new class extends SetupScript private function companions() : void { $condition = array( - Cfg::get('SQL_LIMIT_NONE'), [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], ['typeCat', -6] ); $companionz = new SpellList($condition); - $legit = DB::Aowow()->selectCol('SELECT `spellId2` FROM ?_items WHERE `class` = ?d AND `subClass` = ?d AND `spellId1` IN (?a) AND `spellId2` IN (?a)', ITEM_CLASS_MISC, 2, LEARN_SPELLS, $companionz->getFoundIDs()); + $legit = DB::Aowow()->selectCol('SELECT `spellId2` FROM ::items WHERE `class` = %i AND `subClass` = %i AND `spellId1` IN %in AND `spellId2` IN %in', ITEM_CLASS_MISC, 2, LEARN_SPELLS, $companionz->getFoundIDs()); foreach ($companionz->iterate() as $id => $_) if (!$companionz->getSources()) @@ -246,11 +239,7 @@ CLISetup::registerSetup("build", new class extends SetupScript private function factions() : void { - $condition = array( - Cfg::get('SQL_LIMIT_NONE'), - [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0] - ); - $factionz = new FactionList($condition); + $factionz = new FactionList(array([['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0])); foreach (CLISetup::$locales as $loc) { @@ -279,13 +268,12 @@ CLISetup::registerSetup("build", new class extends SetupScript ); $baseCnd = array( - Cfg::get('SQL_LIMIT_NONE'), [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], // Inscryption Engineering ['effect1Id', [SPELL_EFFECT_APPLY_AURA, SPELL_EFFECT_TRADE_SKILL, SPELL_EFFECT_PROSPECTING, SPELL_EFFECT_OPEN_LOCK, SPELL_EFFECT_MILLING, SPELL_EFFECT_DISENCHANT, SPELL_EFFECT_SUMMON, SPELL_EFFECT_SKINNING], '!'], // not the skill itself ['effect2Id', [SPELL_EFFECT_SKILL, SPELL_EFFECT_PROFICIENCY], '!'], - ['OR', ['typeCat', 9], ['typeCat', 11]] + [DB::OR, ['typeCat', 9], ['typeCat', 11]] ); foreach ($skills as $s) @@ -340,9 +328,8 @@ CLISetup::registerSetup("build", new class extends SetupScript private function achievements() : void { $condition = array( - Cfg::get('SQL_LIMIT_NONE'), [['cuFlags', CUSTOM_EXCLUDE_FOR_LISTVIEW, '&'], 0], - [['flags', 1, '&'], 0], // no statistics + [['flags', 1, '&'], 0], // no statistics ); $achievez = new AchievementList($condition); @@ -356,8 +343,10 @@ CLISetup::registerSetup("build", new class extends SetupScript $buff = "var _ = g_achievements;\n"; foreach ($achievez->getListviewData(ACHIEVEMENTINFO_PROFILE) as $id => $data) { - $sumPoints += $data['points']; - $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; + if ($data['side'] & SIDE_ALLIANCE) // both sides have the same point total + $sumPoints += $data['points']; + + $buff .= '_['.$id.'] = '.Util::toJSON($data).";\n"; } // categories to sort by @@ -375,15 +364,15 @@ CLISetup::registerSetup("build", new class extends SetupScript set_time_limit(2); CLI::write('[profiler] applying '.count($this->exclusions).' baseline exclusions'); - DB::Aowow()->query('DELETE FROM ?_profiler_excludes WHERE `comment` = ""'); + DB::Aowow()->qry('DELETE FROM ::profiler_excludes WHERE `comment` = ""'); foreach ($this->exclusions as $ex) - DB::Aowow()->query('REPLACE INTO ?_profiler_excludes (?#) VALUES (?a)', array_keys($ex), array_values($ex)); + DB::Aowow()->qry('REPLACE INTO ::profiler_excludes %v', $ex); // excludes; type => [excludeGroupBit => [typeIds]] $excludes = []; - $exData = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `typeId` AS ARRAY_KEY2, `groups` FROM ?_profiler_excludes'); + $exData = DB::Aowow()->selectCol('SELECT `type` AS ARRAY_KEY, `typeId` AS ARRAY_KEY2, `groups` FROM ::profiler_excludes'); for ($i = 0; (1 << $i) < PR_EXCLUDE_GROUP_ANY; $i++) foreach ($exData as $type => $data) if ($ids = array_keys(array_filter($data, fn($x) => $x & (1 << $i)))) diff --git a/setup/tools/filegen/robots.ss.php b/setup/tools/filegen/robots.ss.php new file mode 100644 index 00000000..b9f8447c --- /dev/null +++ b/setup/tools/filegen/robots.ss.php @@ -0,0 +1,24 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + +if (!CLI) + die('not in cli mode'); + + +CLISetup::registerSetup("build", new class extends SetupScript +{ + use TrTemplateFile; + + protected $info = array( + 'robots' => [[], CLISetup::ARGV_PARAM, 'Fills robots.txt with site variables.'] + ); + + protected $fileTemplateSrc = ['robots.txt.in']; + protected $fileTemplateDest = ['robots.txt']; // aowow root +}); + +?> diff --git a/setup/tools/filegen/simpleimg.ss.php b/setup/tools/filegen/simpleimg.ss.php index 9d582974..6e30aedb 100644 --- a/setup/tools/filegen/simpleimg.ss.php +++ b/setup/tools/filegen/simpleimg.ss.php @@ -220,7 +220,7 @@ CLISetup::registerSetup("build", new class extends SetupScript foreach ($row as $x => $name) { $j++; - $outFile = CLI::nicePath(($isIcon ? strtolower($name) : $name).'.'.$ext, $dest); + $outFile = CLI::nicePath(($isIcon ? $this->fixIconName($name) : $name).'.'.$ext, $dest); $this->status = ' - '.str_pad($j.'/'.$nFiles, 12).str_pad('('.number_format($j * 100 / $nFiles, 2).'%)', 9); @@ -293,7 +293,7 @@ CLISetup::registerSetup("build", new class extends SetupScript { $j++; $this->status = ' - '.str_pad($j.'/'.$nFiles, 12).str_pad('('.number_format($j * 100 / $nFiles, 2).'%)', 9); - $outFile = CLI::nicePath(($isIcon ? strtolower($img) : $img).'.'.$ext, $dest); + $outFile = CLI::nicePath(($isIcon ? $this->fixIconName($img) : $img).'.'.$ext, $dest); if (!CLISetup::getOpt('force') && file_exists($outFile)) { @@ -372,7 +372,7 @@ CLISetup::registerSetup("build", new class extends SetupScript return $m ? $m[1] : null; }, $missing); - DB::Aowow()->query('UPDATE ?_icons SET `cuFlags` = `cuFlags` | ?d WHERE `name` IN (?a)', CUSTOM_EXCLUDE_FOR_LISTVIEW, $iconNames); + DB::Aowow()->qry('UPDATE ::icons SET `cuFlags` = `cuFlags` | %i WHERE `name` IN %in', CUSTOM_EXCLUDE_FOR_LISTVIEW, $iconNames); CLI::write('[simpleimg] the following '.count($missing).' images where referenced by DBC but not in the mpqData directory. They may need to be converted by hand later on.', CLI::LOG_WARN); foreach ($missing as $m) @@ -381,6 +381,11 @@ CLISetup::registerSetup("build", new class extends SetupScript return $this->success; } + + private function fixIconName(string $name) : string + { + return preg_replace('/[^a-z0-9_-]/', '-', strtolower($name)); + } }); ?> diff --git a/setup/tools/filegen/soundfiles.ss.php b/setup/tools/filegen/soundfiles.ss.php index 10d63834..36ea6b4e 100644 --- a/setup/tools/filegen/soundfiles.ss.php +++ b/setup/tools/filegen/soundfiles.ss.php @@ -21,7 +21,7 @@ CLISetup::registerSetup("build", new class extends SetupScript public function generate() : bool { // ALL files - $files = DB::Aowow()->selectCol('SELECT ABS(`id`) AS ARRAY_KEY, CONCAT(`path`, "/", `file`) FROM ?_sounds_files'); + $files = DB::Aowow()->selectCol('SELECT ABS(`id`) AS ARRAY_KEY, CONCAT(`path`, "/", `file`) FROM ::sounds_files'); $nFiles = count($files); $qtLen = strlen($nFiles); $sum = 0; @@ -63,7 +63,7 @@ CLISetup::registerSetup("build", new class extends SetupScript CLI::write('[soundfiles] - did not find file: '.CLI::bold(CLI::nicePath($filePath, CLISetup::$srcDir, '['.implode(',', array_map(fn($x) => $x->json(), CLISetup::$locales)).']')), CLI::LOG_WARN); $time->reset(); // flag as unusable in DB - DB::Aowow()->query('UPDATE ?_sounds_files SET `id` = ?d WHERE ABS(`id`) = ?d', -$fileId, $fileId); + DB::Aowow()->qry('UPDATE ::sounds_files SET `id` = %i WHERE ABS(`id`) = %i', -$fileId, $fileId); } return $this->success; diff --git a/setup/tools/filegen/spellscaling.ss.php b/setup/tools/filegen/spellscaling.ss.php new file mode 100644 index 00000000..e91b80c5 --- /dev/null +++ b/setup/tools/filegen/spellscaling.ss.php @@ -0,0 +1,42 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + +if (!CLI) + die('not in cli mode'); + + +CLISetup::registerSetup("build", new class extends SetupScript +{ + use TrTemplateFile; + + protected $info = array( + 'spellscaling' => [[], CLISetup::ARGV_PARAM, 'Compiles spell scaling data to file for spells with attribute SPELL_ATTR0_LEVEL_DAMAGE_CALCULATION.'] + ); + + protected $fileTemplateDest = ['datasets/spell-scaling']; + protected $fileTemplateSrc = ['spell-scaling.in']; + + protected $dbcSourceFiles = ['gtnpcmanacostscaler']; + + private function debugify(array $data) : string + { + $buff = []; + foreach ($data as $id => $row) + $buff[] = str_pad($id, 7, " ", STR_PAD_LEFT).": ".$row; + + return "{\r\n".implode(",\r\n", $buff)."\r\n}"; + } + + private function spellScalingSV() : string + { + $data = DB::Aowow()->selectCol('SELECT `idx` + 1 AS ARRAY_KEY, `factor` FROM dbc_gtnpcmanacostscaler'); + + return Cfg::get('DEBUG') ? $this->debugify($data) : Util::toJSON($data); + } +}) + +?> diff --git a/setup/tools/filegen/statistics.ss.php b/setup/tools/filegen/statistics.ss.php index 1f33db72..3b0afb13 100644 --- a/setup/tools/filegen/statistics.ss.php +++ b/setup/tools/filegen/statistics.ss.php @@ -73,7 +73,7 @@ CLISetup::registerSetup("build", new class extends SetupScript ); foreach ($dataz as $class => &$data) - $data[2] = array_values(DB::Aowow()->selectRow('SELECT mle.chance*100 cMle, spl.chance*100 cSpl FROM dbc_gtchancetomeleecritbase mle, dbc_gtchancetospellcritbase spl WHERE mle.idx = spl.idx AND mle.idx = ?d', $class - 1)); + $data[2] = array_values(DB::Aowow()->selectRow('SELECT mle.chance*100 cMle, spl.chance*100 cSpl FROM dbc_gtchancetomeleecritbase mle, dbc_gtchancetospellcritbase spl WHERE mle.idx = spl.idx AND mle.idx = %i', $class - 1)); return $dataz; } @@ -81,7 +81,7 @@ CLISetup::registerSetup("build", new class extends SetupScript // { str, agi, sta, int, spi, raceMod1, raceMod2 } private function race() : array { - $raceData = DB::World()->select('SELECT `race` AS ARRAY_KEY, MIN(`str`), MIN(`agi`), MIN(`sta`), MIN(`inte`), MIN(`spi`) FROM player_levelstats WHERE `level` = 1 GROUP BY `race` ORDER BY `race` ASC'); + $raceData = DB::World()->selectAssoc('SELECT `race` AS ARRAY_KEY, MIN(`str`), MIN(`agi`), MIN(`sta`), MIN(`inte`), MIN(`spi`) FROM player_levelstats WHERE `level` = 1 GROUP BY `race` ORDER BY `race` ASC'); foreach ($raceData as &$rd) $rd = array_values($rd + [[], []]); @@ -130,27 +130,27 @@ CLISetup::registerSetup("build", new class extends SetupScript else $offset = array_values(DB::World()->selectRow('SELECT MIN(`str`), MIN(`agi`), MIN(`sta`), MIN(`inte`), MIN(`spi`) FROM player_levelstats WHERE `level` = 1 AND `race` = 1')); - $gtData = DB::Aowow()->select( - 'SELECT mlecrt.idx - ?d AS ARRAY_KEY, mlecrt.chance * 100, splcrt.chance * 100, mlecrt.chance * 100 * ?f, baseHP5.ratio * 1, extraHP5.ratio * 1 + $gtData = DB::Aowow()->selectAssoc( + 'SELECT mlecrt.idx - %i AS ARRAY_KEY, mlecrt.chance * 100, splcrt.chance * 100, mlecrt.chance * 100 * %f, baseHP5.ratio * 1, extraHP5.ratio * 1 FROM dbc_gtchancetomeleecrit mlecrt JOIN dbc_gtchancetospellcrit splcrt ON splcrt.idx = mlecrt.idx JOIN dbc_gtoctregenhp baseHP5 ON baseHP5.idx = mlecrt.idx JOIN dbc_gtregenhpperspt extraHP5 ON extraHP5.idx = mlecrt.idx - WHERE mlecrt.idx BETWEEN ?d AND ?d', + WHERE mlecrt.idx BETWEEN %i AND %i', (($class - 1) * 100) - 1, // class-offset $mod, (($class - 1) * 100) + 0, // lvl 1 (($class - 1) * 100) + 79 // lvl 80 ); - $rows = DB::World()->select( + $rows = DB::World()->selectAssoc( 'SELECT pls.level AS ARRAY_KEY, - pls.str - ?d, pls.agi - ?d, pls.sta - ?d, pls.inte - ?d, pls.spi - ?d, + pls.str - %i, pls.agi - %i, pls.sta - %i, pls.inte - %i, pls.spi - %i, pcls.basehp, IF(pcls.basemana <> 0, pcls.basemana, 100) FROM player_levelstats pls JOIN player_classlevelstats pcls ON pls.level = pcls.level AND pls.class = pcls.class - WHERE pls.race = ?d AND - pls.class = ?d + WHERE pls.race = %i AND + pls.class = %i ORDER BY pls.level ASC', $offset[0], $offset[1], $offset[2], $offset[3], $offset[4], in_array($class, [3, 7, 11]) ? 6 : 1, @@ -170,13 +170,13 @@ CLISetup::registerSetup("build", new class extends SetupScript // content of gtRegenMPPerSpt.dbc private function level() : array { - return DB::Aowow()->selectCol('SELECT idx-99 AS ARRAY_KEY, ratio FROM dbc_gtregenmpperspt WHERE idx >= 100 AND idx < 100 + ?d', MAX_LEVEL); + return DB::Aowow()->selectCol('SELECT idx-99 AS ARRAY_KEY, ratio FROM dbc_gtregenmpperspt WHERE idx >= 100 AND idx < 100 + %i', MAX_LEVEL); } // profession perks ... too lazy to formulate a search algorithm for two occurences private function skills() : array { - // DB::Aowow()->select( + // DB::Aowow()->selectAssoc( // 'SELECT sk.id AS "skillId", sla.reqSkillLevel, s.effect1AuraId AS "auraId", s.effect1MiscValue, s.effect1BasePoints + s.effect1DieSides AS "qty" // FROM dbc_skilllineability sla // JOIN dbc_skillline sk ON sk.id = sla.skilllineid diff --git a/setup/tools/filegen/talentcalc.ss.php b/setup/tools/filegen/talentcalc.ss.php index 6443f871..1207fdf4 100644 --- a/setup/tools/filegen/talentcalc.ss.php +++ b/setup/tools/filegen/talentcalc.ss.php @@ -49,12 +49,12 @@ CLISetup::registerSetup("build", new class extends SetupScript // my neighbour is noisy as fuck and my head hurts, so .. $this->petFamIcons = ['Ability_Druid_KingoftheJungle', 'Ability_Druid_DemoralizingRoar', 'Ability_EyeOfTheOwl']; // .. i've no idea where to fetch these from - $this->spellMods = (new SpellList(array(['typeCat', -2], Cfg::get('SQL_LIMIT_NONE'))))->getProfilerMods(); + $this->spellMods = (new SpellList(array(['typeCat', -2])))->getProfilerMods(); - $petIcons = Util::toJSON(DB::Aowow()->SelectCol('SELECT `id` AS ARRAY_KEY, LOWER(SUBSTRING_INDEX(`iconString`, "\\\\", -1)) AS "iconString" FROM dbc_creaturefamily WHERE `petTalentType` IN (0, 1, 2)')); + $petIcons = Util::toJSON(DB::Aowow()->SelectCol('SELECT `id` AS ARRAY_KEY, LOWER(SUBSTRING_INDEX(`iconString`, "\\", -1)) AS "iconString" FROM dbc_creaturefamily WHERE `petTalentType` IN (0, 1, 2)')); $tSpellIds = DB::Aowow()->selectCol('SELECT `rank1` FROM dbc_talent UNION SELECT `rank2` FROM dbc_talent UNION SELECT `rank3` FROM dbc_talent UNION SELECT `rank4` FROM dbc_talent UNION SELECT `rank5` FROM dbc_talent'); - $this->tSpells = new SpellList(array(['s.id', $tSpellIds], Cfg::get('SQL_LIMIT_NONE'))); + $this->tSpells = new SpellList(array(['s.id', $tSpellIds])); foreach (CLISetup::$locales as $loc) { @@ -89,18 +89,18 @@ CLISetup::registerSetup("build", new class extends SetupScript $petCategories = []; // All "tabs" of a given class talent - $tabs = DB::Aowow()->select('SELECT * FROM dbc_talenttab WHERE `classMask` = ?d ORDER BY `tabNumber`, `creatureFamilyMask`', $classMask); + $tabs = DB::Aowow()->selectAssoc('SELECT * FROM dbc_talenttab WHERE `classMask` = %i ORDER BY `tabNumber`, `creatureFamilyMask`', $classMask); $result = []; for ($tabIdx = 0; $tabIdx < count($tabs); $tabIdx++) { - $talents = DB::Aowow()->select( + $talents = DB::Aowow()->selectAssoc( 'SELECT t.id AS "tId", t.*, IF(t.rank5, 5, IF(t.rank4, 4, IF(t.rank3, 3, IF(t.rank2, 2, 1)))) AS "maxRank", s.`name_loc0`, s.`name_loc2`, s.`name_loc3`, s.`name_loc4`, s.`name_loc6`, s.`name_loc8`, - LOWER(SUBSTRING_INDEX(si.`iconPath`, "\\\\", -1)) AS "iconString" + LOWER(SUBSTRING_INDEX(si.`iconPath`, "\\", -1)) AS "iconString" FROM dbc_talent t, dbc_spell s, dbc_spellicon si - WHERE si.`id` = s.`iconId` AND t.`tabId`= ?d AND s.`id` = t.`rank1` - ORDER BY t.`row`, t.`column`', + WHERE si.`id` = s.`iconId` AND t.`tabId`= %i AND s.`id` = t.`rank1` + ORDER BY t.`row`, t.`column`, t.`id` ASC', $tabs[$tabIdx]['id'] ); @@ -113,7 +113,7 @@ CLISetup::registerSetup("build", new class extends SetupScript { $petFamId = log($tabs[$tabIdx]['creatureFamilyMask'], 2); $result[$tabIdx]['icon'] = $this->petFamIcons[$petFamId]; - $petCategories = DB::Aowow()->SelectCol('SELECT `id` AS ARRAY_KEY, `categoryEnumID` FROM dbc_creaturefamily WHERE `petTalentType` = ?d', $petFamId); + $petCategories = DB::Aowow()->SelectCol('SELECT `id` AS ARRAY_KEY, `categoryEnumID` FROM dbc_creaturefamily WHERE `petTalentType` = %i', $petFamId); $result[$tabIdx]['f'] = array_keys($petCategories); } diff --git a/setup/tools/filegen/talenticons.ss.php b/setup/tools/filegen/talenticons.ss.php index d7187ddc..efbdfdbe 100644 --- a/setup/tools/filegen/talenticons.ss.php +++ b/setup/tools/filegen/talenticons.ss.php @@ -79,13 +79,13 @@ CLISetup::registerSetup("build", new class extends SetupScript private function compileTexture(string $ttField, int $searchMask, int $tabIdx) : ?\GdImage { $icons = DB::Aowow()->SelectCol( - 'SELECT ic.`name` AS "iconString" - FROM ?_icons ic - JOIN ?_spell s ON s.`iconId` = ic.`id` + 'SELECT ic.`name` + FROM ::icons ic + JOIN ::spell s ON s.`iconId` = ic.`id` JOIN dbc_talent t ON t.`rank1` = s.`id` JOIN dbc_talenttab tt ON tt.`id` = t.`tabId` - WHERE tt.?# = ?d AND tt.`tabNumber` = ?d - ORDER BY t.`row`, t.`column` ASC, s.`id` DESC', + WHERE tt.%n = %i AND tt.`tabNumber` = %i + ORDER BY t.`row`, t.`column`, t.`id` ASC', $ttField, $searchMask, $tabIdx); if (empty($icons)) diff --git a/setup/tools/filegen/templates/global.js/0_user.js b/setup/tools/filegen/templates/global.js/0_user.js index 7202a369..cb968339 100644 --- a/setup/tools/filegen/templates/global.js/0_user.js +++ b/setup/tools/filegen/templates/global.js/0_user.js @@ -3,6 +3,21 @@ 'abbr article aside audio canvas details figcaption figure footer header hgroup mark menu meter nav output progress section summary time video'.replace(/\w+/g,function(n){document.createElement(n)}) +// aowow - extend Date for holidaycal +Date.prototype.getLocaleDay = function() { + const dayNo = this.getDay(); + switch (Locale.getId()) + { + case LOCALE_FRFR: + case LOCALE_DEDE: + case LOCALE_ESES: + case LOCALE_RURU: + return !dayNo ? 6 : dayNo - 1; + default: + return dayNo; + } +}; + /* User-related functions TODO: Move global variables/functions into User class diff --git a/setup/tools/filegen/templates/global.js/animations.js b/setup/tools/filegen/templates/global.js/animations.js index a41697ef..da37f6fe 100644 --- a/setup/tools/filegen/templates/global.js/animations.js +++ b/setup/tools/filegen/templates/global.js/animations.js @@ -68,7 +68,7 @@ } while ( elem = elem.parentNode ); return getRGB(color); - }; + } // Some named colors to work with // From Interface by Stefan Petre diff --git a/setup/tools/filegen/templates/global.js/announcement.js b/setup/tools/filegen/templates/global.js/announcement.js index 0149b984..82628760 100644 --- a/setup/tools/filegen/templates/global.js/announcement.js +++ b/setup/tools/filegen/templates/global.js/announcement.js @@ -110,7 +110,12 @@ Announcement.prototype = { // aowow - animation fix - jQuery.animate hard snaps into place after half the time passed this.parentDiv.style.opacity = '100'; this.parentDiv.style.height = (this.parentDiv.offsetHeight + 10) + 'px'; - g_trackEvent('Announcements', 'Show', '' + this.name); + + $WH.Track.nonInteractiveEvent({ + category: 'Announcements', + action: 'Show', + label: '' + this.name + }); }, hide: function() @@ -132,7 +137,11 @@ Announcement.prototype = { markRead: function() { - g_trackEvent('Announcements', 'Close', '' + this.name); + $WH.Track.interactiveEvent({ + category: 'Announcements', + action: 'Close', + label: '' + this.name + }); g_setWowheadCookie('announcement-' + this.id, 'closed'); this.hide(); }, @@ -147,11 +156,22 @@ Announcement.prototype = { { this.text = text; Markup.printHtml(this.text, this.parent + '-markup'); - g_addAnalyticsToNode($WH.ge(this.parent + '-markup'), { - 'category': 'Announcements', - 'actions': { - 'Follow link': function(node) { return true; } - } - }, this.id); + + let parent = $WH.ge(this.parent + '-markup'); + $WH.qsa('a', parent).forEach(link => { + $WH.aE(link, 'click', () => { + let label = 'unknown'; + let txt = g_getFirstTextContent(link); + if (txt) + label = g_urlize(txt).substr(0, 80); + else if (link.title) + label = g_urlize(link.title).substr(0, 80); + else if (link.id) + label = g_urlize(link.id).substr(0, 80); + + label = `${ this.id || 0 }-${ label }`; + $WH.Track.linkClick(link, { category: 'Announcements', label: label }); + }); + }); } }; diff --git a/setup/tools/filegen/templates/global.js/clicktocopy.js b/setup/tools/filegen/templates/global.js/clicktocopy.js index 9225b6f4..d8c20bfd 100644 --- a/setup/tools/filegen/templates/global.js/clicktocopy.js +++ b/setup/tools/filegen/templates/global.js/clicktocopy.js @@ -23,7 +23,7 @@ $WH.clickToCopy = function (el, textOrFn, opt) $WH.clickToCopy.getTooltip.bind(null, false, opt), undefined, // { - /* byCursor: */ !opt.attachToElement, + /* byCursor: ! */ opt.attachToElement, // stopPropagation: opt.overrideOtherTooltips // } ); diff --git a/setup/tools/filegen/templates/global.js/conditionList.js b/setup/tools/filegen/templates/global.js/conditionList.js index c788c24c..f899593f 100644 --- a/setup/tools/filegen/templates/global.js/conditionList.js +++ b/setup/tools/filegen/templates/global.js/conditionList.js @@ -194,13 +194,11 @@ var ConditionList = new function() { str = g_conditions[strIdx]; // fill in params - str = $WH.sprintfa(str, param[0], param[1], param[2]); - + return $WH.sprintfa(str, param[0], param[1], param[2], param[3]) // resolve NegativeCondition - str = str.replace(/\$N([^:]*):([^;]*);/g, '$' + (negate > 0 ? 2 : 1)); - + .replace(/\$N([^:]*):([^;]*);/g, '$' + (negate > 0 ? 2 : 1)) // resolve vars - return str.replace(/\$C(\d+)([^:]*):([^;]*);/g, (_, i, y, n) => (i > 0 ? y : n)); + .replace(/\$C(\d+)([^:]*):([^;]*);/g, (_, i, y, n) => (i > 0 ? y : n)); } function _createTab() diff --git a/setup/tools/filegen/templates/global.js/guide.js b/setup/tools/filegen/templates/global.js/guide.js index 254abab5..20822e62 100644 --- a/setup/tools/filegen/templates/global.js/guide.js +++ b/setup/tools/filegen/templates/global.js/guide.js @@ -228,7 +228,7 @@ function g_enhanceTextarea (ta, opt) { } ta.data("wh-enhanced", true); -}; +} $WH.createOptionsMenuWidget = function (id, txt, opt) { var chevron = $WH.createOptionsMenuWidget.chevron; diff --git a/setup/tools/filegen/templates/global.js/listview.js b/setup/tools/filegen/templates/global.js/listview.js index 6720feaf..79394951 100644 --- a/setup/tools/filegen/templates/global.js/listview.js +++ b/setup/tools/filegen/templates/global.js/listview.js @@ -1098,7 +1098,7 @@ Listview.prototype = { $WH.ae(this.tbody, tr); tr = $WH.ce('tr'); - for (var k = 0; k < this.dates[starti].date.getDay(); ++k) + for (var k = 0; k < this.dates[starti].date.getLocaleDay(); ++k) { var foo = $WH.ce('td'); foo.className = 'empty-cell'; @@ -2572,7 +2572,6 @@ Listview.extraCols = { var side = null; var items = row.cost[2]; var currency = row.cost[1]; - var achievementPoints = 0; if (row.side != null) side = row.side; @@ -2584,7 +2583,7 @@ Listview.extraCols = { side = 2; } - Listview.funcBox.appendMoney(td, money, side, items, currency, achievementPoints); + Listview.funcBox.appendMoney(td, money, side, items, currency); } }, sortFunc: function(a, b, col) @@ -2946,7 +2945,6 @@ Listview.extraCols = { id: 'condition', name: LANG.tab_conditions, type: 'text', - width: '25%', compute: function(row, td) { if (!row.condition) @@ -2955,6 +2953,7 @@ Listview.extraCols = { td.className = 'small'; td.style.lineHeight = '18px'; td.style.textAlign = 'left'; + td.style.whiteSpace = 'nowrap'; // tiny links are hard to hit, hmkey? td.onclick = (e) => $WH.sp(e); @@ -2962,7 +2961,7 @@ Listview.extraCols = { var mText = ConditionList.createCell(row.condition); Markup.printHtml(mText, td); - return; + }, getVisibleText: function(row) { @@ -3876,7 +3875,7 @@ Listview.funcBox = { } ); } - return; + }, coFlagOutOfDate: function(comment) @@ -3976,7 +3975,7 @@ Listview.funcBox = { } // aowow: custom end - return; + } }, diff --git a/setup/tools/filegen/templates/global.js/listview_templates.js b/setup/tools/filegen/templates/global.js/listview_templates.js index 2dc8aca2..8217c524 100644 --- a/setup/tools/filegen/templates/global.js/listview_templates.js +++ b/setup/tools/filegen/templates/global.js/listview_templates.js @@ -144,7 +144,7 @@ Listview.templates = { $(wrapper).append(revText); } - return; + }, getVisibleText: function(guide) { @@ -359,7 +359,7 @@ Listview.templates = { span.addClass('q2'); $(td).append(span); - return; + }, sortFunc: function(a, b) { @@ -411,7 +411,10 @@ Listview.templates = { } if (item.id) { - $WH.ae(i, g_items.createIcon(item.id, (this.iconSize == null ? 1 : this.iconSize), num, qty)); + if (item.name.charAt(0) == '@') // aowow - don't create link on icon for ref loot + $WH.ae(i, Icon.create(g_items.getIcon(item.id), this.iconSize ?? 1, null, null, num, qty)); + else + $WH.ae(i, g_items.createIcon(item.id, (this.iconSize == null ? 1 : this.iconSize), num, qty)); } $WH.ae(tr, i); td.style.borderLeft = 'none'; @@ -905,6 +908,43 @@ Listview.templates = { var _ = Listview.funcBox.getItemType; return $WH.strcmp(_(a.classs, a.subclass, a.subsubclass).text, _(b.classs, b.subclass, b.subsubclass).text); } + }, + { + id: 'completed', // Listview.COLUMN_ID_COMPLETION + name: LANG.completion, // WH.TERMS.completion + hidden: true, + compute: function (item, td) + { + var skip = !item.hasOwnProperty('classs') || !Listview.templates.item._validCompletionCategory(item.classs, item.subclass, item.quality); + $WH.addCompletionIcons(td, 3, item.id, skip); + }, + sortFunc: function (a, b) + { + // return $WH.stringCompare( + return $WH.strcmp( + $WH.getCompletionFlags(3, a.id), + $WH.getCompletionFlags(3, b.id) + ); + }, + getValue: function (item) { + var value = 0; + var completionData = g_user.completion?.hasOwnProperty(3) ? g_user.completion[3] : {}; + + if (!item.hasOwnProperty('classs') || !Listview.templates.item._validCompletionCategory(item.classs, item.subclass, item.quality)) + return -1; + + for (var i in g_user.characters) + { + var profile = g_user.characters[i]; + if (!(profile.id in completionData)) + continue; + + if ($WH.in_array(completionData[profile.id], item.id) != -1) + value++; + } + + return value; + } } ], @@ -912,6 +952,12 @@ Listview.templates = { return item.name.charAt(0) == '@' ? 'javascript:;' : '?item=' + item.id; }, + _validCompletionCategory: function (classs, subclass, quality) { + return $WH.in_array(g_completion_categories[3], classs) != -1 || + $WH.in_array(g_completion_categories[3], '' + classs + '-' + subclass) != -1 || + $WH.in_array(g_completion_categories[3], '' + classs + 'q' + quality) != -1 + }, + onBeforeCreate: function() { var nComparable = false; @@ -930,6 +976,23 @@ Listview.templates = { this.mode = Listview.MODE_CHECKBOX; this._nComparable = nComparable; } + + for (var i in this.columns) + { + if (this.columns[i].id == 'completed' && this.columns[i].hidden) + { + if ($WH.isset('g_user') && 'characters' in g_user && $WH.in_array(this.hiddenCols, this.columns[i].id) == -1) + { + var n = 0; + for (var j in this.data) + if (this.data[j].hasOwnProperty('classs') && Listview.templates.item._validCompletionCategory(this.data[j].classs, this.data[j].subclass, this.data[j].quality)) + n++; + + if (n > this.data.length * 0.1) + this.visibility.push(parseInt(i)); + } + } + } }, createCbControls: function(div, topBar) { @@ -1148,7 +1211,7 @@ Listview.templates = { } } else { - return - 1; + return -1; } }, sortFunc: function(a, b, col) { @@ -1902,11 +1965,69 @@ Listview.templates = { var _ = Listview.funcBox.getQuestCategory; return $WH.strcmp(_(a.category), _(b.category)); } + }, + { + id: 'completed', // Listview.COLUMN_ID_COMPLETION + name: LANG.completion, // WH.TERMS.completion + hidden: true, + compute: function (quest, td) + { + if (quest.daily || quest.weekly) + return; + + $WH.addCompletionIcons(td, 5, quest.id) + }, + sortFunc: function (a, b) + { + // return $WH.stringCompare( + return $WH.strcmp( + $WH.getCompletionFlags(5, a.id), + $WH.getCompletionFlags(5, b.id) + ); + }, + getValue: function (quest) { + var value = 0; + var completionData = g_user.completion?.hasOwnProperty(5) ? g_user.completion[5] : {}; + + if (quest.daily || quest.weekly) + return -1; + + for (var i in g_user.characters) + { + var profile = g_user.characters[i]; + if (!(profile.id in completionData)) + continue; + + if ($WH.in_array(completionData[profile.id], quest.id) != -1) + value++; + } + + return value; + } } ], getItemLink: function(quest) { return '?quest=' + quest.id; + }, + + onBeforeCreate: function () { + for (var i in this.columns) + { + if (this.columns[i].id == 'completed' && this.columns[i].hidden) + { + if ($WH.isset('g_user') && 'characters' in g_user && $WH.in_array(this.hiddenCols, this.columns[i].id) == -1) + { + var n = 0; + for (var j in this.data) + if (!this.data[j].daily && !this.data[j].weekly) + n++; + + if (n > this.data.length * 0.1) + this.visibility.push(parseInt(i)); + } + } + } } }, @@ -2087,7 +2208,7 @@ Listview.templates = { a.addClass('listview-cleartext'); a.attr('href', '?user=' + user.username); $(td).append(a); - return; + }, getVisibleText: function(user) { return user.username; @@ -2105,7 +2226,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.reputation)); - return; + }, sortFunc: function(a, b) { if (b.reputation == a.reputation) @@ -2130,7 +2251,7 @@ Listview.templates = { sp.html(buf); $(td).append(sp); - return; + }, sortFunc: function(a, b) { var sumA = (a.gold * 1000 * 1000) + (a.silver * 1000) + a.copper; @@ -2146,7 +2267,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.comments)); - return; + }, sortFunc: function(a, b) { if (a.comments == b.comments) @@ -2160,7 +2281,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.posts)); - return; + }, sortFunc: function(a, b) { if (a.posts == b.posts) @@ -2174,7 +2295,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.screenshots)); - return; + }, sortFunc: function(a, b) { if (a.screenshots == b.screenshots) @@ -2188,7 +2309,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.reports)); - return; + }, sortFunc: function(a, b) { if (a.reports == b.reports) @@ -2202,7 +2323,7 @@ Listview.templates = { type: 'text', compute: function(user, td) { $(td).append($WH.number_format(user.votes)); - return; + }, sortFunc: function(a, b) { if (a.votes == b.votes) @@ -2216,7 +2337,7 @@ Listview.templates = { type: 'text', compute: function(user, c) { $(c).append($WH.number_format(user.uploads)); - return; + }, sortFunc: function(a, c) { if (a.uploads == c.uploads) @@ -2275,7 +2396,7 @@ Listview.templates = { else { $(td).append(l_reputation_names[rep.action]); } - return; + }, getVisibleText: function(rep) { return l_reputation_names[rep.action]; @@ -2297,7 +2418,7 @@ Listview.templates = { } $(td).append(span); - return; + }, getVisibleText: function(rep) { return rep.amount; @@ -2517,6 +2638,30 @@ Listview.templates = { } $WH.ae(wrapper, d); } + else if (spell.spellclick) { + td.style.position = 'relative'; + + [flags, who] = spell.spellclick; + + let buff = 'onClick'; + if (who == 1) // Friendly + buff += $WH.sprintf(LANG.qty, g_reputation_standings[4]); + else if (who == 2) // Raid + buff += $WH.sprintf(LANG.qty, g_quest_types[62]); + else if (who == 3) // Party + buff += $WH.sprintf(LANG.qty, g_quest_types[1]); + + buff += LANG.colon + '<span class="breadcrumb-arrow">' + (flags & 0x1 ? g_world_object_types[4] : g_pageInfo.name) + '</span>'; + buff += flags & 0x2 ? g_world_object_types[4] : g_pageInfo.name; + + $(td).append($('<div>', {class: 'small'}) + .css('fontStyle', 'italic') + .css('position', 'absolute') + .css('right', '3px') + .css('bottom', '3px') + .html(buff) + ); + } $WH.ae(td, wrapper); }, getVisibleText: function(spell) { @@ -3151,7 +3296,7 @@ Listview.templates = { return Listview.funcBox.assocArrCmp(a.skill, b.skill, g_spell_skills); } }, - /* AoWoW: custom start */ + /* AoWoW: custom start */ { id: 'stackRules', name: LANG.asr_behaviour, @@ -3432,12 +3577,74 @@ Listview.templates = { return 0; } + }, + /* AoWoW: custom end */ + { + id: 'completed', // Listview.COLUMN_ID_COMPLETION + name: LANG.completion, // WH.TERMS.completion + hidden: true, + compute: function (spell, td) + { + if (!spell.hasOwnProperty('cat') || !Listview.templates.spell._validCompletionCategory(spell.cat)) + return; + + $WH.addCompletionIcons(td, 6, spell.id) + }, + sortFunc: function (a, b) + { + // return $WH.stringCompare( + return $WH.strcmp( + $WH.getCompletionFlags(6, a.id), + $WH.getCompletionFlags(6, b.id) + ); + }, + getValue: function (spell) { + var value = 0; + var completionData = g_user.completion?.hasOwnProperty(6) ? g_user.completion[6] : {}; + + if (!spell.hasOwnProperty('cat') || !Listview.templates.spell._validCompletionCategory(spell.cat)) + return -1; + + for (var i in g_user.characters) + { + var profile = g_user.characters[i]; + if (!(profile.id in completionData)) + continue; + + if ($WH.in_array(completionData[profile.id], spell.id) != -1) + value++; + } + + return value; + } } - /* AoWoW: custom end */ ], getItemLink: function(spell) { return '?spell=' + spell.id; + }, + + _validCompletionCategory: function (category) { + return $WH.in_array(g_completion_categories[6], category) != -1; + }, + + onBeforeCreate: function () { + for (var i in this.columns) + { + if (this.columns[i].id == 'completed' && this.columns[i].hidden) + { + if ($WH.isset('g_user') && 'characters' in g_user && $WH.in_array(this.hiddenCols, this.columns[i].id) == -1) + { + var n = 0; + for (var j in this.data) + if (this.data[j].hasOwnProperty('cat') && Listview.templates.spell._validCompletionCategory(this.data[j].cat)) + n++; + + if (n > this.data.length * 0.1) + this.visibility.push(parseInt(i)); + } + } + } } }, @@ -5834,11 +6041,69 @@ Listview.templates = { return $WH.strcmp(g_achievement_categories[a.category], g_achievement_categories[b.category]); }, hidden: true + }, + { + id: 'completed', // Listview.COLUMN_ID_COMPLETION + name: LANG.completion, // WH.TERMS.completion + hidden: true, + compute: function (achievement, td) + { + if (achievement.type) + return; + + $WH.addCompletionIcons(td, 10, achievement.id) + }, + sortFunc: function (a, b) + { + // return $WH.stringCompare( + return $WH.strcmp( + $WH.getCompletionFlags(10, a.id), + $WH.getCompletionFlags(10, b.id) + ); + }, + getValue: function (achievement) { + var value = 0; + var completionData = g_user.completion?.hasOwnProperty(10) ? g_user.completion[10] : {}; + + if (achievement.type) + return -1; + + for (var i in g_user.characters) + { + var profile = g_user.characters[i]; + if (!(profile.id in completionData)) + continue; + + if ($WH.in_array(completionData[profile.id], achievement.id) != -1) + value++; + } + + return value; + } } ], getItemLink: function(achievement) { return '?achievement=' + achievement.id; + }, + + onBeforeCreate: function () { + for (var i in this.columns) + { + if (this.columns[i].id == 'completed' && this.columns[i].hidden) + { + if ($WH.isset('g_user') && 'characters' in g_user && $WH.in_array(this.hiddenCols, this.columns[i].id) == -1) + { + var n = 0; + for (var j in this.data) + if (!this.data[j].type) + n++; + + if (n > this.data.length * 0.1) + this.visibility.push(parseInt(i)); + } + } + } } }, @@ -6052,11 +6317,55 @@ Listview.templates = { return $WH.strcmp(g_title_categories[a.category], g_title_categories[b.category]); }, hidden: true + }, + { + id: 'completed', // Listview.COLUMN_ID_COMPLETION + name: LANG.completion, // WH.TERMS.completion + hidden: true, + compute: function (title, td) + { + $WH.addCompletionIcons(td, 11, title.id) + }, + sortFunc: function (a, b) + { + // return $WH.stringCompare( + return $WH.strcmp( + $WH.getCompletionFlags(11, a.id), + $WH.getCompletionFlags(11, b.id) + ); + }, + getValue: function (title) { + var value = 0; + var completionData = g_user.completion?.hasOwnProperty(11) ? g_user.completion[11] : {}; + + for (var i in g_user.characters) + { + var profile = g_user.characters[i]; + if (!(profile.id in completionData)) + continue; + + if ($WH.in_array(completionData[profile.id], title.id) != -1) + value++; + } + + return value; + } } ], getItemLink: function(title) { return '?title=' + title.id; + }, + + onBeforeCreate: function () { + for (var i in this.columns) + { + if (this.columns[i].id == 'completed' && this.columns[i].hidden) + { + if ($WH.isset('g_user') && 'characters' in g_user && $WH.in_array(this.hiddenCols, this.columns[i].id) == -1) + this.visibility.push(parseInt(i)); + } + } } }, @@ -7041,7 +7350,7 @@ Listview.templates = { $(td).mouseover(function (event, menu) { $WH.Tooltip.showAtCursor(menu, event, 0, 0); }.bind(td, tt)); $(td).mousemove(function (event) { $WH.Tooltip.cursorUpdate(event); }) .mouseout(function () { $WH.Tooltip.hide(); }); -/* aowow - we dont do patches + /* aowow - we dont do patches var g = typeof g_hearthhead != "undefined" && g_hearthhead ? "hearthstone" : "wow"; if (!g_getPatchVersionObject.hasOwnProperty("parsed") || !g_getPatchVersionObject.parsed[g]) { g_getPatchVersionObject(); @@ -7060,7 +7369,7 @@ Listview.templates = { j = j.replace(f, f + " (" + new Date(c.timestamp).toDateString() + ")"); } } -*/ + */ let j = changelog.version; // aowow - tmp $(td).html(j); }, diff --git a/setup/tools/filegen/templates/global.js/mapper.js b/setup/tools/filegen/templates/global.js/mapper.js index 243af3b5..03f067f6 100644 --- a/setup/tools/filegen/templates/global.js/mapper.js +++ b/setup/tools/filegen/templates/global.js/mapper.js @@ -152,7 +152,7 @@ function Mapper(opt, noScroll) this.setZones(opt.zoneparent, opt.zones); this.updateMap(noScroll); -}; +} Mapper.sizes = [ [ 488, 325, 'normal'], @@ -1080,7 +1080,7 @@ Mapper.prototype = { this.onPinUpdate && this.onPinUpdate(this); - return; + }, pinOver: function() diff --git a/setup/tools/filegen/templates/global.js/mapviewer.js b/setup/tools/filegen/templates/global.js/mapviewer.js index 2f6ad712..59cec45b 100644 --- a/setup/tools/filegen/templates/global.js/mapviewer.js +++ b/setup/tools/filegen/templates/global.js/mapviewer.js @@ -276,6 +276,12 @@ var MapViewer = new function() this.show = function(opt) { + $WH.Track.interactiveEvent({ + category: "Zone Maps", + action: "Show", + label: opt.link ? opt.link : "General" + }); + if (opt.link) { tempParent = $WH.ce('div'); diff --git a/setup/tools/filegen/templates/global.js/markup.js b/setup/tools/filegen/templates/global.js/markup.js index e72e5dd2..5cb8f459 100644 --- a/setup/tools/filegen/templates/global.js/markup.js +++ b/setup/tools/filegen/templates/global.js/markup.js @@ -465,6 +465,102 @@ var Markup = { return [str, '</span>']; } }, + condition: + { + ltrim: true, + rtrim: true, + empty: false, + allowedClass: MARKUP_CLASS_STAFF, + allowedChildren: { '<text>': 1 }, + toHtml: function(attr) + { + return ['<span>' + Markup.toHtml(ConditionList.createCell(JSON.parse(attr._nodes[0].attr._rawText)), { skipReset: true }) + '</span>']; + } + }, + copy: + { + empty: false, + attr: + { + unnamed: { req: false }, + button: { req: false, valid: /^false$/ }, + size: { req: false, valid: /^large$/ } + }, + allowedClass: MARKUP_CLASS_STAFF, + allowedChildren: { '<text>': 1 }, + // invisibleContent: true, + // rawText: true, + toHtml: function(attr) + { + let title = attr.unnamed ? LANG.copy_format.replace('%s', attr.unnamed) : LANG.copy_clipboard; + // let text = attr._contents.textContent; + let text = attr._textContents; + + // aowow - can't access dom nodes from here .. hopefully this does not become relevant + // while (attr._contents.hasChildNodes()) + // attr._contents.removeChild(attr._contents.firstChild); + + if (attr.button === 'false') + { + // aowow - must return string, not node + // let span = $WH.ce('span', { className: 'tip' }, $WH.ct(text)); + // $WH.clickToCopy(span, text); + // return span; + + let rand = Math.random().toString().substr(2); + let html = '<span class="tip" id="ctc-' + rand + '">'; + setTimeout(() => { + span = $WH.ge('ctc-' + rand); + $WH.clickToCopy(span, span.textContent); + }, 500); + + return [html, '</span>']; + } + + /* aowow - red buttons aren't <input type=button> yet + * let btn = $WH.ce('button', { className: 'btn btn-site fa fa-clipboard', type: 'button' }, $WH.ct(title)); + * + * if (attr.size !== 'large') + * btn.classList.add('btn-small'); + * + * $WH.aE(btn, 'click', () => { + * $WH.copyToClipboard(text); + * btn.classList.remove('fa-clipboard'); + * btn.classList.add('fa-check'); + * setTimeout(() => { + * btn.classList.remove('fa-check'); + * btn.classList.add('fa-clipboard'); + * }, 3 * 1000); + * }); + * + * return btn; + */ + + let rand = Math.random().toString().substr(2); + let btn = RedButton.create(title); + + btn.id = 'ctc-' + rand; + btn.classList.add('fa-clipboard'); + + btn.style.float = 'initial'; + btn.style.display = 'inline-block'; + + setTimeout(() => { + btn = $WH.ge('ctc-' + rand); + $WH.aE(btn, 'click', () => { + $WH.copyToClipboard(text); + btn.classList.remove('fa-clipboard'); + btn.classList.add('fa-check'); + setTimeout(() => { + btn.classList.remove('fa-check'); + btn.classList.add('fa-clipboard'); + }, 3 * 1000); + }, 500); + }); + + return [btn.outerHTML]; + } + }, currency: { empty: true, @@ -3042,7 +3138,7 @@ var Markup = { str += "<li><b><a href='#guide-changelog' onclick=\"Markup.toggleReveal('changelog');\">Changelog</a></b></li>"; lastNode = "h2"; } - if (node.name == 'h2' && node.attr.toc !== false) + if (node.name == 'h2' && node.attr.toc !== 'false') { if (lastNode == 'h3') { @@ -3052,7 +3148,7 @@ var Markup = { str += '<li><b><a href=\'#' + (node.attr.id ? g_urlize(node.attr.id) : g_urlize(node.attr._textContents)) + '\'>' + node.attr._textContents + '</a></b></li>'; lastNode = 'h2'; } - if (node.name == 'h3' && allowH3 && node.attr.toc !== false && (lastNode != '' || nodes.h2.length == 0)) + if (node.name == 'h3' && allowH3 && node.attr.toc !== 'false' && (lastNode != '' || nodes.h2.length == 0)) { if (lastNode == 'h2') { diff --git a/setup/tools/filegen/templates/global.js/menu.js b/setup/tools/filegen/templates/global.js/menu.js index c2416ae6..f65be754 100644 --- a/setup/tools/filegen/templates/global.js/menu.js +++ b/setup/tools/filegen/templates/global.js/menu.js @@ -792,7 +792,7 @@ var Menu = new function() }); explodeInto(menu, implodedMenu); - }; + } function explodeInto(menu, implodedMenu) // Reverse of implode { diff --git a/setup/tools/filegen/templates/global.js/modelviewer.js b/setup/tools/filegen/templates/global.js/modelviewer.js index a1ffe708..0c078614 100644 --- a/setup/tools/filegen/templates/global.js/modelviewer.js +++ b/setup/tools/filegen/templates/global.js/modelviewer.js @@ -444,8 +444,11 @@ var ModelViewer = new function() rightDiv.append(animDiv); - var a1 = $('<a/>', { 'class': 'modelviewer-help', href: '?help=modelviewer', target: '_blank', text: LANG.help }), - a2 = $('<a/>', { 'class': 'modelviewer-close', href: 'javascript:;', click: Lightbox.hide, text: LANG.close }); + var a1 = $('<a/>', { 'class': 'modelviewer-help', href: '?help=modelviewer', target: '_blank'/* , text: LANG.help */ }), + a2 = $('<a/>', { 'class': 'modelviewer-close', href: 'javascript:;', click: Lightbox.hide/* , text: LANG.close */ }); + + a1.append($('<span/>')); + a2.append($('<span/>')); rightDiv.append(a2); rightDiv.append(a1); @@ -601,7 +604,11 @@ var ModelViewer = new function() } } - g_trackEvent('Model Viewer', 'Show', g_urlize(trackCode)); + $WH.Track.interactiveEvent({ + category: 'Model Viewer', + action: 'Show', + label: g_urlize(trackCode) // WH.Strings.slug(trackCode) + }); oldHash = location.hash; } diff --git a/setup/tools/filegen/templates/global.js/profiler.js b/setup/tools/filegen/templates/global.js/profiler.js index 52a23ecc..273ea12f 100644 --- a/setup/tools/filegen/templates/global.js/profiler.js +++ b/setup/tools/filegen/templates/global.js/profiler.js @@ -17,3 +17,137 @@ function g_getProfileUrl(profile) { function g_getProfileRealmUrl(profile) { return '?profiles=' + profile.region + '.' + profile.realm; } + +$WH.prepInfobox = function () { + $('.infobox').each(function () + { + var row = $(this); + if (row.data('infobox-completion-info-added') !== true && g_user.characters && + typeof g_pageInfo == 'object' && typeof g_pageInfo.type == 'number' && typeof g_pageInfo.typeId == 'number') + { + var wardrobe = $('.compact-completion-display', row); + if (wardrobe.length) + { + var completionIcon = $('span', wardrobe); + if (completionIcon.length) + { + var i = 0; + completionIcon.each(function () + { + var e = $(this); + var t = e.html(); + + try { t = JSON.parse(t) } + catch (e) { return } + + var a; + for (var n = 0, s; s = g_user.characters[n]; n++) + { + if (s.id == t.id) + { + a = s; + break + } + } + + if (a) + { + e.parent().append($WH.createCompletionIcon(a, t.completed ? 1 : 0, t.rel)); + e.remove(); + i++ + } + }); + + if (i) + wardrobe.parent().parent().show() + } + else if (wardrobe.is(':empty') && $WH.addCompletionIcons(wardrobe.get(0), g_pageInfo.type, g_pageInfo.typeId)) + wardrobe.parent().parent().show() + } + + row.data('infobox-completion-info-added', true) + } + }); +}; + +$WH.addCompletionIcons = function (parent, type, typeIdOrData, skipIncomplete) { + var nComplete = 0; + + for (var i in g_user.characters) + { + if (!g_user.characters.hasOwnProperty(i)) + continue; + + let profile = g_user.characters[i]; + + let completion = 0; + let completionData = g_user.completion?.hasOwnProperty(type) ? g_user.completion[type] : {}; + if (!completionData.hasOwnProperty(profile.id)) + continue; + + completion = completionData[profile.id].includes(typeIdOrData) ? 1 : 0; + + if (skipIncomplete && !completion) + continue; + + $WH.ae(parent, $WH.createCompletionIcon(profile, completion)); + nComplete++; + } + + return nComplete; +}; + +$WH.createCompletionIcon = function (profile, completePct, rel) { + var icon = $WH.ce('a'); + icon.href = '?profile=' + profile.region + '.' + profile.realm + '.' + profile.name; + + // aowow - so the generic tooltips dont override our completion tooltip + icon.setAttribute('data-disable-wowhead-tooltip', true); + + icon.className = 'progress-icon progress-' + (completePct ? Math.max(1, Math.floor(completePct * 8)) : 0); + $WH.Tooltip.simple(icon, $WH.getCompletionTooltip(profile, completePct), null, true); + + if (rel) + icon.rel = rel; + + return icon; +}; + +$WH.getCompletionTooltip = function (profile, completePct) { + let tooltip = $WH.ce('div'); + $WH.ae(tooltip, $WH.ce('span', { className: 'q' }, $WH.ct((completePct >= 1 ? LANG.complete : LANG.incomplete) + LANG.colon))); + $WH.ae(tooltip, $WH.ce('br')); + + let charRow = $WH.ce('span', { style: { whiteSpace: 'nowrap' } }); + $WH.ae(tooltip, charRow); + $WH.ae(charRow, $WH.ce('b', { className: 'c' + profile.classs }, $WH.ct(profile.name))); + + let server = [' ', profile.realmname]; + + if (profile.hasOwnProperty('region')) + server.push(profile.region.toUpperCase()); + + $WH.ae(charRow, $WH.ce('span', { className: 'q0' }, $WH.ct(server.join(' ')))); + + if (completePct > 0 && completePct < 1) + $WH.ae(charRow, $WH.ct( $WH.sprintf(LANG.parens_format, '', Math.round(completePct * 100) + '%') )); + + return tooltip.innerHTML; +}; + +$WH.getCompletionFlags = function (type, typeId) { + var flags = 0; + var profiles = g_user.characters || []; + let completionData = g_user.completion?.hasOwnProperty(type) ? g_user.completion[type] : {}; + + for (var i = profiles.length - 1; i >= 0; i--) + { + var profile = profiles[i]; + if (!(profile.id in completionData)) + continue + + flags = flags << 1 | (completionData[profile.id].includes(typeId) ? 1 : 0) + } + + return flags; +}; diff --git a/setup/tools/filegen/templates/global.js/screenshots.js b/setup/tools/filegen/templates/global.js/screenshots.js index 44ea902d..3a42f3c8 100644 --- a/setup/tools/filegen/templates/global.js/screenshots.js +++ b/setup/tools/filegen/templates/global.js/screenshots.js @@ -182,7 +182,11 @@ var ScreenshotViewer = new function() if (!resizing) { - g_trackEvent('Screenshots', 'Show', screenshot.id + ( (screenshot.caption && screenshot.caption.length) ? ' (' + screenshot.caption + ')' : '')); + $WH.Track.interactiveEvent({ + category: 'Screenshots', + action: 'Show', + label: screenshot.id + (screenshot.caption && screenshot.caption.length ? ` (${ screenshot.caption })` : '') + }); // ORIGINAL diff --git a/setup/tools/filegen/templates/global.js/tabs.js b/setup/tools/filegen/templates/global.js/tabs.js index 8f3406ca..c12767c0 100644 --- a/setup/tools/filegen/templates/global.js/tabs.js +++ b/setup/tools/filegen/templates/global.js/tabs.js @@ -359,6 +359,11 @@ Tabs.trackClick = function(tab) if (!this.trackable || tab.tracked) return; - g_trackEvent('Tabs', 'Show', this.trackable + ': ' + tab.id); + $WH.Track.interactiveEvent({ + category: 'Tab Click', + action: 'Page: ' + this.trackable, + label: 'Tab: ' + tab.id + }); + tab.tracked = 1; } diff --git a/setup/tools/filegen/templates/global.js/tracking.js b/setup/tools/filegen/templates/global.js/tracking.js index 6fe22256..a91901c6 100644 --- a/setup/tools/filegen/templates/global.js/tracking.js +++ b/setup/tools/filegen/templates/global.js/tracking.js @@ -1,130 +1,207 @@ -/* -TODO: Create "Tracking" class -*/ - -function g_trackPageview(tag) +// https://developers.google.com/tag-platform/security/guides/consent +$WH.Track = new function () { - function track() - { - if (typeof ga == 'function') - ga('send', 'pageview', tag); + const trackAction = 'Click'; + const siteVariables = { + // adsBlocked: 3, + // adsUnblocked: 4, + loggedInUserIsPremium: 2, + userIsLoggedIn: 1, + // userShouldSeeAds: 5 + }; + const maxRetryTime = 10000; + const retryTimeout = 10; + const scrollDepthPoints = [25, 50, 75, 90, 100]; + const _self = { + gaReady: false, + scriptAdded: undefined }; - $(document).ready(track); -} - -function g_trackEvent(category, action, label, value) -{ - function track() + this.gaInit = function (nTries) { - if (typeof ga == 'function') - ga('send', 'event', category, action, label, value); - }; - - $(document).ready(track); -} - -function g_attachTracking(node, category, action, label, value) -{ - var $node = $(node); - - $node.click(function() - { - g_trackEvent(category, action, label, value); - }); -} - -function g_addAnalytics() -{ - var objs = { - 'home-logo': { - 'category': 'Homepage Logo', - 'actions': { - 'Click image': function(node) { return true; } - } - }, - 'home-featuredbox': { - 'category': 'Featured Box', - 'actions': { - 'Follow link': function(node) { return (node.parentNode.className != 'home-featuredbox-links'); }, - 'Click image': function(node) { return (node.parentNode.className == 'home-featuredbox-links'); } - } - }, - 'home-oneliner': { - 'category': 'Oneliner', - 'actions': { - 'Follow link': function(node) { return true; } - } - }, - 'sidebar-container': { - 'category': 'Page sidebar', - 'actions': { - 'Click image': function(node) { return true; } - } - }, - 'toptabs-promo': { - 'category': 'Page header', - 'actions': { - 'Click image': function(node) { return true; } - } - } - }; - - for (var i in objs) - { - var e = $WH.ge(i); - if (e) - g_addAnalyticsToNode(e, objs[i]); - } -} - -function g_getNodeTextId(node) -{ - var id = null, - text = g_getFirstTextContent(node); - - if (text) - id = g_urlize(text); - else if (node.title) - id = g_urlize(node.title); - else if (node.id) - id = g_urlize(node.id); - - return id; -} - -function g_addAnalyticsToNode(node, opts, labelPrefix) -{ - if (!opts || !opts.actions || !opts.category) - { - if ($WH.isset('g_dev') && g_dev) + if (!_self.scriptAdded) { - console.log('Tried to add analytics event without appropriate parameters.'); - console.log(node); - console.log(opts); - } - - return; - } - - var category = opts.category; - var tags = $WH.gE(node, 'a'); - for (var i = 0; i < tags.length; ++i) - { - var node = tags[i]; - var action = 'Follow link'; - for (var a in opts.actions) - { - if (opts.actions[a] && opts.actions[a](node)) + (function (_window, _document, node, src, varName, gaJSNode, firstJSNode) { - action = a; - break; - } + _window['GoogleAnalyticsObject'] = varName; + _window[varName] = _window[varName] || function () { (_window[varName].q = _window[varName].q || []).push(arguments) }, + _window[varName].l = 1 * new Date; + + gaJSNode = _document.createElement(node), + firstJSNode = _document.getElementsByTagName(node)[0]; + gaJSNode.async = 1; + gaJSNode.src = src; + firstJSNode.parentNode.insertBefore(gaJSNode, firstJSNode); + })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); + + let attachGTAG = () => { + let script = document.createElement('script'); + script.async = true; + script.src = 'https://www.googletagmanager.com/gtag/js?id=CFG_GTAG_MEASUREMENT_ID'; + document.body.appendChild(script); + }; + + if (document.body) + attachGTAG(); + else + $WH.aE(document, 'DOMContentLoaded', attachGTAG); + + window.dataLayer = window.dataLayer || []; + window.gtag = function () { dataLayer.push(arguments) }; + + gtag('js', new Date); + gtag('config', 'CFG_GTAG_MEASUREMENT_ID'); + + _self.scriptAdded = true; } - var label = (labelPrefix ? labelPrefix + '-' : '') + g_getNodeTextId(node); - g_attachTracking(node, category, action, label); + if (!window.ga || !ga.create) + { + if (!nTries) + nTries = 1; + + if (nTries > 100) + return; + + setTimeout($WH.Track.gaInit.bind($WH.Track, nTries + 1), nTries * 9); + return; + } + + ga('create', 'UA_MEASUREMENT_KEY', 'CFG_UA_MEASUREMENT_KEY'); + // trackSiteVar(siteVariables.userShouldSeeAds, $WH.WAS.showAds()); + trackSiteVar(siteVariables.userIsLoggedIn, /* $WH.User.isLoggedIn() */g_user.id > 0); + // if ($WH.User.isLoggedIn()) + if (g_user.id > 0) + trackSiteVar(siteVariables.loggedInUserIsPremium, /* $WH.User.isPremium() */g_user.premium); + + ga('set', 'anonymizeIp', true); + ga('send', 'pageview'); + + _self.gaReady = true; + scrollDepthPoints.forEach(registerTrackScroll); + }; + + this.interactiveEvent = evt => trackEvent(evt ); + this.nonInteractiveEvent = evt => trackEvent(evt, { nonInteraction: true }); + this.interactiveEventOutgoing = evt => trackEvent(evt, { isOutgoing: true }); + this.linkClick = (anchor, evt) => trackEvent({ ...evt, action: trackAction, value: anchor.href }, { isOutgoing: true }); + + function trackSiteVar(idx, val) + { + ga('set', 'dimension' + idx, val) } -} -$(document).ready(g_addAnalytics); + function registerTrackScroll(depth) + { + let trackDone = false; + const trackScroll = () => { + if (trackDone) + return; + + trackDone = true; + requestAnimationFrame(() => { + const y = window.scrollY; + const h = document.documentElement.scrollHeight - document.documentElement.clientHeight; + if (y / h * 100 >= depth) + { + trackEvent({ + action: 'scroll_event', + event_category: 'Scroll Depth', + event_label: `${ depth }%`, + scroll_depth: depth + }); + + window.removeEventListener('scroll', trackScroll, { passive: true }) + } + trackDone = false; + }); + }; + + window.addEventListener('scroll', trackScroll, { passive: true }) + } + + function trackEvent(evt, opts) + { + const { action: act, ...o } = evt; + const { category: cat, label: lab, value: val } = o; + const { nonInteraction: ni, isOutgoing: io } = opts || {}; + let { retryCount: rc } = opts || {}; + + if (!_self.gaReady) + { + if ($WH.isset('g_dev') && g_dev) + return; + + if (!rc) + rc = 0; + + rc++; + if (rc * retryTimeout > maxRetryTime) + return; + + setTimeout(trackEvent.bind(null, evt, { + nonInteraction: ni, + isOutgoing: io, + retryCount: rc + }), retryTimeout); + + return; + } + + let attr; + if (typeof ni === 'boolean') + { + attr ??= {}; + attr.nonInteraction = ni ? 1 : 0; + } + + if (io) + { + attr ??= {}; + attr.transport = 'beacon'; + } + + if (cat) + ga('send', 'event', cat, act, lab, val, attr); + + gtag('event', act, o); + } +}; + +// aowow - repurpose old tracking +$(document).ready(function () { + var trackObjs = { + 'header-logo': { 'label': 'Database Logo', 'actions': { 'Click image': (node) => true } }, + 'home-logo': { 'label': 'Homepage Logo', 'actions': { 'Click image': (node) => true } }, + 'home-oneliner': { 'label': 'Oneliner', 'actions': { 'Follow link': (node) => true } }, + 'home-featuredbox': { 'label': 'Featured Box', 'actions': { 'Follow link': (node) => node.parentNode.className != 'home-featuredbox-links', + 'Click image': (node) => node.parentNode.className == 'home-featuredbox-links' } + } + }; + + Object.entries(trackObjs).forEach(([nodeId, trackInfo]) => { + let parent = $WH.ge(nodeId); + if (!parent) + return; + + $WH.qsa('a', parent).forEach(link => { + Object.entries(trackInfo.actions).forEach(([action, testFn]) => { + $WH.aE(link, 'click', evt => { + if (!testFn(link)) + return; + + let txt = 'unknown'; + if (_ = g_getFirstTextContent(link)) + txt = g_urlize(_).substr(0, 80); + else if (link.title) + txt = g_urlize(link.title).substr(0, 80); + else if (link.id) + txt = g_urlize(link.id).substr(0, 80); + + label = `${trackInfo.label}-${action}-${txt}`; + $WH.Track.linkClick(link, { category: PageTemplate.get('pageName') || 'unknown', label: label }); + }); + }); + }); + }); +}); diff --git a/setup/tools/filegen/templates/global.js/ui_ux.js b/setup/tools/filegen/templates/global.js/ui_ux.js index aa3a6821..b4663894 100644 --- a/setup/tools/filegen/templates/global.js/ui_ux.js +++ b/setup/tools/filegen/templates/global.js/ui_ux.js @@ -151,7 +151,7 @@ function g_GetCommentRoleLabel(roles, title) return LANG.premiumuser; return null; -}; +} function g_formatDate(sp, elapsed, theDate, time, alone) { diff --git a/setup/tools/filegen/templates/global.js/videos.js b/setup/tools/filegen/templates/global.js/videos.js index 5d8a858e..1e4664ae 100644 --- a/setup/tools/filegen/templates/global.js/videos.js +++ b/setup/tools/filegen/templates/global.js/videos.js @@ -176,7 +176,13 @@ var VideoViewer = new function() if (!resizing) { - g_trackEvent('Videos', 'Show', video.id + (video.caption.length ? ' (' + video.caption + ')' : '')); + var hasCaption = (video.caption != null && video.caption.length); + + $WH.Track.interactiveEvent({ + category: 'Videos', + action: 'Show', + label: video.id + (hasCaption ? ` (${ video.caption })` : '') + }); if (video.videoType == 1) imgDiv.innerHTML = Markup.toHtml('[youtube=' + video.videoId + ' width=' + imgWidth + ' height=' + imgHeight + ' autoplay=true]', {mode:Markup.MODE_ARTICLE}); @@ -249,7 +255,6 @@ var VideoViewer = new function() // CAPTION - var hasCaption = (video.caption != null && video.caption.length); var hasSubject = (video.subject != null && video.subject.length && video.type && video.typeId); if (hasCaption || hasSubject) diff --git a/setup/tools/filegen/templates/global.js/wow.js b/setup/tools/filegen/templates/global.js/wow.js index d482e5a7..99323c8b 100644 --- a/setup/tools/filegen/templates/global.js/wow.js +++ b/setup/tools/filegen/templates/global.js/wow.js @@ -269,6 +269,12 @@ var g_types = { 504: 'mail' }; +var g_completion_categories = { + // 1: [12], // NPCs: Battle Pets + 3: [9, "15-2", "15-5", "15--7"], // Items: Recipes, Minipets, Mounts (Ground), Mounts (Flying) + 6: [-5, -6, 9, 11] // Spells: Mounts, Minipets, Sec. Skills, Prim. Skills +}; + // Items $WH.cO(g_items, { add: function(id, json) diff --git a/setup/tools/filegen/templates/item-scaling.in b/setup/tools/filegen/templates/item-scaling.in index b6521d1b..2e4d7807 100644 --- a/setup/tools/filegen/templates/item-scaling.in +++ b/setup/tools/filegen/templates/item-scaling.in @@ -6,5 +6,4 @@ $WH.g_convertScalingFactor.SD = /*setup:itemScalingSD*/; if ($WH.isset('$WowheadPower')) { $WowheadPower.loadScales(3); - $WowheadPower.loadScales(6); } diff --git a/setup/tools/filegen/templates/power.js.in b/setup/tools/filegen/templates/power.js.in index 717345d7..ce648f5a 100644 --- a/setup/tools/filegen/templates/power.js.in +++ b/setup/tools/filegen/templates/power.js.in @@ -90,7 +90,7 @@ if (typeof $WowheadPower == "undefined") { }, SCALES = { 3: { url: "?data=item-scaling" }, - 6: { url: "?data=item-scaling" } + 6: { url: "?data=spell-scaling" } }, LOCALES = { 0: "enus", @@ -644,6 +644,49 @@ if (typeof $WowheadPower == "undefined") { } } + if (!isRemote && window.g_user && g_user.characters) + { + var completion = ''; + let completionData = g_user.completion.hasOwnProperty(currentType) ? g_user.completion[currentType] : false; + + let entity = {}; + if (currentType == TYPE_QUEST && $WH.isset('g_quests')) + entity = g_quests[currentId] || {}; + if (currentType == TYPE_ACHIEVEMENT && $WH.isset('g_achievements')) + entity = g_achievements[currentId] || {}; + if (currentType == TYPE_ITEM && $WH.isset('g_items')) + entity = g_items[currentId] || {}; + if (currentType == TYPE_SPELL && $WH.isset('g_spells')) + entity = g_spells[currentId] || {}; + + if ((!LOOKUPS[currentType][0] || LOOKUPS[currentType][0][currentId].status[currentLocale] !== STATUS_OK) || + (currentType === TYPE_QUEST && (entity.daily || entity.weekly)) || + (currentType === TYPE_ACHIEVEMENT && entity.type)) + completionData = false; + + let CompetionWithoutCatg = !(completionData && currentType in g_completion_categories && $WH.in_array(g_completion_categories[currentType], entity.completion_category) === -1); + + if (completionData) + { + for (var i in g_user.characters) + { + var profile = g_user.characters[i]; + if (!(profile.id in completionData)) + continue; + + let isComplete = $WH.in_array(completionData[profile.id], currentId) !== - 1; + if (!isComplete && !CompetionWithoutCatg) + continue; + + completion += '<br><span class="progress-icon ' + (isComplete ? 'progress-8' : 'progress-0') + '"></span> '; + completion += profile.name + ' - ' + profile.realmname + ' ' + profile.region.toUpperCase(); + } + } + + if (completion !== '') + html += '<br><span class="q">' + LANG.completion + ':</span>' + completion; + } + if (currentParams.map && map && map.getMap) { html2 = map.getMap(); } diff --git a/robots.txt b/setup/tools/filegen/templates/robots.txt.in similarity index 50% rename from robots.txt rename to setup/tools/filegen/templates/robots.txt.in index 053da057..ae268c86 100644 --- a/robots.txt +++ b/setup/tools/filegen/templates/robots.txt.in @@ -8,8 +8,21 @@ Disallow: /?profile=* Disallow: /?profiles Disallow: /profiles/ Disallow: /?profiles=* +Disallow: /?guild +Disallow: /guild/ +Disallow: /?guild=* +Disallow: /?guilds +Disallow: /guilds/ +Disallow: /?guilds=* +Disallow: /?arena-team +Disallow: /arena-team/ +Disallow: /?arena-team=* +Disallow: /?arena-teams +Disallow: /arena-teams/ +Disallow: /?arena-teams=* Disallow: /?random Disallow: /random/ Disallow: /?search Disallow: /search/ Disallow: /?search=* +Sitemap: CFG_HOST_URL/?sitemap diff --git a/setup/tools/filegen/templates/spell-scaling.in b/setup/tools/filegen/templates/spell-scaling.in new file mode 100644 index 00000000..75c49c11 --- /dev/null +++ b/setup/tools/filegen/templates/spell-scaling.in @@ -0,0 +1,5 @@ +$WH.g_convertScalingSpell.SV = /*setup:spellScalingSV*/; + +if ($WH.isset('$WowheadPower')) { + $WowheadPower.loadScales(6); +} diff --git a/setup/tools/filegen/weightpresets.ss.php b/setup/tools/filegen/weightpresets.ss.php index e884fa2f..00569920 100644 --- a/setup/tools/filegen/weightpresets.ss.php +++ b/setup/tools/filegen/weightpresets.ss.php @@ -21,11 +21,11 @@ CLISetup::registerSetup("build", new class extends SetupScript public function generate() : bool { $wtPresets = []; - $scales = DB::Aowow()->select('SELECT `id`, `name`, `icon`, `class` FROM ?_account_weightscales WHERE `userId` = 0 ORDER BY `class`, `orderIdx` ASC'); + $scales = DB::Aowow()->selectAssoc('SELECT `id`, `name`, `icon`, `class` FROM ::account_weightscales WHERE `userId` = 0 ORDER BY `class`, `orderIdx` ASC'); foreach ($scales as $s) { - if ($weights = DB::Aowow()->selectCol('SELECT `field` AS ARRAY_KEY, `val` FROM ?_account_weightscale_data WHERE `id` = ?d', $s['id'])) + if ($weights = DB::Aowow()->selectCol('SELECT `field` AS ARRAY_KEY, `val` FROM ::account_weightscale_data WHERE `id` = %i', $s['id'])) $wtPresets[$s['class']]['pve'][$s['name']] = array_merge(['__icon' => $s['icon']], $weights); else { diff --git a/setup/tools/setupScript.class.php b/setup/tools/setupScript.class.php index 7ad40a84..048c0926 100644 --- a/setup/tools/setupScript.class.php +++ b/setup/tools/setupScript.class.php @@ -30,7 +30,7 @@ trait TrDBCcopy CLI::write('[sql] copying '.$this->dbcSourceFiles[0].'.dbc into aowow_'.$this->command); - $dbc = new DBC($this->dbcSourceFiles[0], ['temporary' => false, 'tableName' => 'aowow_'.$this->command]); + $dbc = new DBCReader($this->dbcSourceFiles[0], ['temporary' => false, 'tableName' => 'aowow_'.$this->command]); if ($dbc->error) return false; @@ -44,11 +44,11 @@ trait TrCustomData public function applyCustomData() : bool { $ok = true; - foreach ((DB::Aowow()->selectCol('SELECT `entry` AS ARRAY_KEY, `field` AS ARRAY_KEY2, `value` FROM ?_setup_custom_data WHERE `command` = ?', $this->getName()) ?: []) as $id => $data) + foreach ((DB::Aowow()->selectCol('SELECT `entry` AS ARRAY_KEY, `field` AS ARRAY_KEY2, `value` FROM ::setup_custom_data WHERE `command` = %s', $this->getName()) ?: []) as $id => $data) { try { - DB::Aowow()->query('UPDATE ?_'.$this->getName().' SET ?a WHERE id = ?d', $data, $id); + DB::Aowow()->qry('UPDATE %n SET %a WHERE id = %i', '::'.$this->getName(), $data, $id); } catch (\Exception $e) { @@ -602,9 +602,9 @@ abstract class SetupScript return; } - DB::Aowow()->query('UPDATE ?_'.$tbl.' x, ?_comments y SET x.`cuFlags` = x.`cuFlags` | ?d WHERE x.`id` = y.`typeId` AND y.`type` = ?d AND y.`flags` & ?d', CUSTOM_HAS_COMMENT, $type, CC_FLAG_APPROVED); - DB::Aowow()->query('UPDATE ?_'.$tbl.' x, ?_screenshots y SET x.`cuFlags` = x.`cuFlags` | ?d WHERE x.`id` = y.`typeId` AND y.`type` = ?d AND y.`status` & ?d', CUSTOM_HAS_SCREENSHOT, $type, CC_FLAG_APPROVED); - DB::Aowow()->query('UPDATE ?_'.$tbl.' x, ?_videos y SET x.`cuFlags` = x.`cuFlags` | ?d WHERE x.`id` = y.`typeId` AND y.`type` = ?d AND y.`status` & ?d', CUSTOM_HAS_VIDEO, $type, CC_FLAG_APPROVED); + DB::Aowow()->qry('UPDATE ::'.$tbl.' x, ::comments y SET x.`cuFlags` = x.`cuFlags` | %i WHERE x.`id` = y.`typeId` AND y.`type` = %i AND y.`flags` & %i', CUSTOM_HAS_COMMENT, $type, CC_FLAG_APPROVED); + DB::Aowow()->qry('UPDATE ::'.$tbl.' x, ::screenshots y SET x.`cuFlags` = x.`cuFlags` | %i WHERE x.`id` = y.`typeId` AND y.`type` = %i AND y.`status` & %i', CUSTOM_HAS_SCREENSHOT, $type, CC_FLAG_APPROVED); + DB::Aowow()->qry('UPDATE ::'.$tbl.' x, ::videos y SET x.`cuFlags` = x.`cuFlags` | %i WHERE x.`id` = y.`typeId` AND y.`type` = %i AND y.`status` & %i', CUSTOM_HAS_VIDEO, $type, CC_FLAG_APPROVED); } } diff --git a/setup/tools/sqlgen/achievement.ss.php b/setup/tools/sqlgen/achievement.ss.php index fc5d54be..f68bb490 100644 --- a/setup/tools/sqlgen/achievement.ss.php +++ b/setup/tools/sqlgen/achievement.ss.php @@ -21,10 +21,10 @@ CLISetup::registerSetup('sql', new class extends SetupScript protected $worldDependency = ['dbc_achievement', 'disables']; protected $setupAfter = [['icons'], []]; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_achievement'); - DB::Aowow()->query('TRUNCATE ?_achievementcategory'); + DB::Aowow()->qry('TRUNCATE ::achievement'); + DB::Aowow()->qry('TRUNCATE ::achievementcategory'); /**************/ /* categories */ @@ -32,7 +32,7 @@ CLISetup::registerSetup('sql', new class extends SetupScript CLI::write('[achievement] - resolving categories'); - DB::Aowow()->query('INSERT INTO ?_achievementcategory SELECT ac.id, GREATEST(ac.parentcategory, 0), GREATEST(IFNULL(ac1.parentcategory, 0), 0) FROM dbc_achievement_category ac LEFT JOIN dbc_achievement_category ac1 ON ac1.id = ac.parentCategory'); + DB::Aowow()->qry('INSERT INTO ::achievementcategory SELECT ac.id, GREATEST(ac.parentcategory, 0), GREATEST(IFNULL(ac1.parentcategory, 0), 0) FROM dbc_achievement_category ac LEFT JOIN dbc_achievement_category ac1 ON ac1.id = ac.parentCategory'); /************/ /* dbc data */ @@ -40,8 +40,8 @@ CLISetup::registerSetup('sql', new class extends SetupScript CLI::write('[achievement] - basic dbc data'); - DB::Aowow()->query( - 'INSERT INTO ?_achievement + DB::Aowow()->qry( + 'INSERT INTO ::achievement SELECT a.id, 2 - a.faction, a.map, @@ -64,9 +64,7 @@ CLISetup::registerSetup('sql', new class extends SetupScript FROM dbc_achievement a LEFT JOIN dbc_achievement_category ac ON ac.id = a.category LEFT JOIN dbc_spellicon si ON si.id = a.iconId - LEFT JOIN ?_icons i ON LOWER(SUBSTRING_INDEX(si.iconPath, "\\\\", -1)) = i.name - { WHERE a.id IN (?a) }', - $ids ?: DBSIMPLE_SKIP + LEFT JOIN ::icons i ON LOWER(SUBSTRING_INDEX(si.iconPath, "\\", -1)) = i.name_source', ); @@ -76,9 +74,7 @@ CLISetup::registerSetup('sql', new class extends SetupScript CLI::write('[achievement] - serverside achievement data'); - $serverAchievements = DB::World()->select('SELECT `ID` AS "id", IF(`requiredFaction` = -1, 3, IF(`requiredFaction` = 0, 2, 1)) AS "faction", `mapID` AS "map", `points`, `flags`, `count` AS "reqCriteriaCount", `refAchievement` FROM achievement_dbc{ WHERE `id` IN (?a)}', - $ids ?: DBSIMPLE_SKIP - ); + $serverAchievements = DB::World()->selectAssoc('SELECT `ID` AS "id", IF(`requiredFaction` = -1, 3, IF(`requiredFaction` = 0, 2, 1)) AS "faction", `mapID` AS "map", `points`, `flags`, `count` AS "reqCriteriaCount", `refAchievement` FROM achievement_dbc'); foreach ($serverAchievements as &$sa) { @@ -91,7 +87,7 @@ CLISetup::registerSetup('sql', new class extends SetupScript unset($sa); foreach ($serverAchievements as $sa) - DB::Aowow()->query('INSERT INTO ?_achievement (?#) VALUES (?a)', array_keys($sa), array_values($sa)); + DB::Aowow()->qry('INSERT INTO ::achievement %v', $sa); /********************************/ @@ -104,7 +100,7 @@ CLISetup::registerSetup('sql', new class extends SetupScript foreach ($parents as $chainId => $next) { $tree = [null, $next]; - while ($next = DB::Aowow()->selectCell('SELECT `id` FROM dbc_achievement WHERE `previous` = ?d', $next)) + while ($next = DB::Aowow()->selectCell('SELECT `id` FROM dbc_achievement WHERE `previous` = %i', $next)) $tree[] = $next; foreach ($tree as $idx => $aId) @@ -112,7 +108,7 @@ CLISetup::registerSetup('sql', new class extends SetupScript if (!$aId) continue; - DB::Aowow()->query('UPDATE ?_achievement SET `cuFlags` = `cuFlags` | ?d, `chainId` = ?d, `chainPos` = ?d WHERE `id` = ?d', + DB::Aowow()->qry('UPDATE ::achievement SET `cuFlags` = `cuFlags` | %i, `chainId` = %i, `chainPos` = %i WHERE `id` = %i', $idx == 1 ? ACHIEVEMENT_CU_FIRST_SERIES : (count($tree) == $idx + 1 ? ACHIEVEMENT_CU_LAST_SERIES : 0), $chainId + 1, $idx, @@ -129,7 +125,7 @@ CLISetup::registerSetup('sql', new class extends SetupScript CLI::write('[achievement] - disabling disabled achievements from table disables'); if ($criteria = DB::World()->selectCol('SELECT `entry` FROM disables WHERE `sourceType` = 4')) - DB::Aowow()->query('UPDATE ?_achievement a JOIN ?_achievementcriteria ac ON a.`id` = ac.`refAchievementId` SET a.`cuFlags` = ?d WHERE ac.`id` IN (?a)', CUSTOM_DISABLED, $criteria); + DB::Aowow()->qry('UPDATE ::achievement a JOIN ::achievementcriteria ac ON a.`id` = ac.`refAchievementId` SET a.`cuFlags` = %i WHERE ac.`id` IN %in', CUSTOM_DISABLED, $criteria); $this->reapplyCCFlags('achievement', Type::ACHIEVEMENT); diff --git a/setup/tools/sqlgen/areatrigger.ss.php b/setup/tools/sqlgen/areatrigger.ss.php index 02c36d8b..8d775e76 100644 --- a/setup/tools/sqlgen/areatrigger.ss.php +++ b/setup/tools/sqlgen/areatrigger.ss.php @@ -18,10 +18,10 @@ CLISetup::registerSetup('sql', new class extends SetupScript protected $dbcSourceFiles = ['areatrigger']; protected $worldDependency = ['areatrigger_involvedrelation', 'areatrigger_scripts', 'areatrigger_tavern', 'areatrigger_teleport', 'quest_template', 'quest_template_addon']; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_areatrigger'); - DB::Aowow()->query('INSERT INTO ?_areatrigger SELECT `id`, 0, 0, `mapId`, `posX`, `posY`, `orientation`, NULL, NULL FROM dbc_areatrigger'); + DB::Aowow()->qry('TRUNCATE ::areatrigger'); + DB::Aowow()->qry('INSERT INTO ::areatrigger SELECT `id`, 0, 0, `mapId`, `posX`, `posY`, `orientation`, NULL, NULL FROM dbc_areatrigger'); /* notes: * while areatrigger DO have dimensions, displaying them on a map is almost always futile, @@ -31,42 +31,42 @@ CLISetup::registerSetup('sql', new class extends SetupScript // 1: Taverns CLI::write('[areatrigger] - fetching taverns'); - $addData = DB::World()->select('SELECT `id` AS ARRAY_KEY, `name`, ?d AS `type` FROM areatrigger_tavern', AT_TYPE_TAVERN); + $addData = DB::World()->selectAssoc('SELECT `id` AS ARRAY_KEY, `name`, %i AS `type` FROM areatrigger_tavern', AT_TYPE_TAVERN); foreach ($addData as $id => $ad) - DB::Aowow()->query('UPDATE ?_areatrigger SET ?a WHERE `id` = ?d', $ad, $id); + DB::Aowow()->qry('UPDATE ::areatrigger SET %a WHERE `id` = %i', $ad, $id); // 2: Teleporter CLI::write('[areatrigger] - teleporter type and name'); - $addData = DB::World()->select( + $addData = DB::World()->selectAssoc( 'SELECT `ID` AS ARRAY_KEY, `Name` AS `name` FROM areatrigger_teleport UNION - SELECT `entryorguid` AS ARRAY_KEY, "SAI Teleport" AS `name` FROM smart_scripts WHERE `source_type` = ?d AND `action_type` = ?d', + SELECT `entryorguid` AS ARRAY_KEY, "SAI Teleport" AS `name` FROM smart_scripts WHERE `source_type` = %i AND `action_type` = %i', SmartAI::SRC_TYPE_AREATRIGGER, SmartAction::ACTION_TELEPORT ); foreach ($addData as $id => $ad) - DB::Aowow()->query('UPDATE ?_areatrigger SET `name` = ?, `type` = ?d WHERE `id` = ?d', $ad['name'], AT_TYPE_TELEPORT, $id); + DB::Aowow()->qry('UPDATE ::areatrigger SET `name` = %s, `type` = %i WHERE `id` = %i', $ad['name'], AT_TYPE_TELEPORT, $id); // 3: Quest Objectives CLI::write('[areatrigger] - satisfying quest objectives'); - $addData = DB::World()->select('SELECT atir.`id` AS ARRAY_KEY, `qt`.ID AS `quest`, NULLIF(qt.`AreaDescription`, "") AS `name`, qta.`SpecialFlags` FROM quest_template qt LEFT JOIN quest_template_addon qta ON qta.`ID` = qt.`ID` JOIN areatrigger_involvedrelation atir ON atir.`quest` = qt.`ID`'); + $addData = DB::World()->selectAssoc('SELECT atir.`id` AS ARRAY_KEY, `qt`.ID AS `quest`, NULLIF(qt.`AreaDescription`, "") AS `name`, qta.`SpecialFlags` FROM quest_template qt LEFT JOIN quest_template_addon qta ON qta.`ID` = qt.`ID` JOIN areatrigger_involvedrelation atir ON atir.`quest` = qt.`ID`'); foreach ($addData as $id => $ad) { if (!($ad['SpecialFlags'] & QUEST_FLAG_SPECIAL_EXT_COMPLETE)) CLI::write('[areatrigger] '.str_pad('['.$id.']', 8).' is involved in quest '.CLI::bold($ad['quest']).', but quest is not flagged for external completion (SpecialFlags & '.Util::asHex(QUEST_FLAG_SPECIAL_EXT_COMPLETE).')', CLI::LOG_WARN); - DB::Aowow()->query('UPDATE ?_areatrigger SET `name` = ?, type = ?d, `quest` = ?d WHERE `id` = ?d', $ad['name'], AT_TYPE_OBJECTIVE, $ad['quest'], $id); + DB::Aowow()->qry('UPDATE ::areatrigger SET `name` = %s, type = %i, `quest` = %i WHERE `id` = %i', $ad['name'], AT_TYPE_OBJECTIVE, $ad['quest'], $id); } // 4/5 Scripted CLI::write('[areatrigger] - assigning scripts'); - $addData = DB::World()->select('SELECT `entry` AS ARRAY_KEY, IF(`ScriptName` = "SmartTrigger", NULL, `ScriptName`) AS `name`, IF(`ScriptName` = "SmartTrigger", 4, 5) AS `type` FROM areatrigger_scripts'); + $addData = DB::World()->selectAssoc('SELECT `entry` AS ARRAY_KEY, IF(`ScriptName` = "SmartTrigger", NULL, `ScriptName`) AS `name`, IF(`ScriptName` = "SmartTrigger", 4, 5) AS `type` FROM areatrigger_scripts'); foreach ($addData as $id => $ad) - DB::Aowow()->query('UPDATE ?_areatrigger SET ?a WHERE `id` = ?d', $ad, $id); + DB::Aowow()->qry('UPDATE ::areatrigger SET %a WHERE `id` = %i', $ad, $id); $this->reapplyCCFlags('areatrigger', Type::AREATRIGGER); diff --git a/setup/tools/sqlgen/classes.ss.php b/setup/tools/sqlgen/classes.ss.php index d4d0db93..f972ac20 100644 --- a/setup/tools/sqlgen/classes.ss.php +++ b/setup/tools/sqlgen/classes.ss.php @@ -17,23 +17,29 @@ CLISetup::registerSetup("sql", new class extends SetupScript 'classes' => [[], CLISetup::ARGV_PARAM, 'Compiles data for type: PlayerClass from dbc.'] ); + protected $setupAfter = [['icons'], []]; protected $dbcSourceFiles = ['spell', 'charbaseinfo', 'skillraceclassinfo', 'skilllineability', 'chrclasses']; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_classes'); + DB::Aowow()->qry('TRUNCATE ::classes'); - $classes = DB::Aowow()->select('SELECT *, `id` AS ARRAY_KEY FROM dbc_chrclasses'); + $classes = DB::Aowow()->selectAssoc('SELECT *, `id` AS ARRAY_KEY FROM dbc_chrclasses'); // add raceMask - $races = DB::Aowow()->select('SELECT `classId` AS ARRAY_KEY, BIT_OR(1 << (`raceId` - 1)) AS "raceMask" FROM dbc_charbaseinfo GROUP BY `classId`'); + $races = DB::Aowow()->selectAssoc('SELECT `classId` AS ARRAY_KEY, BIT_OR(1 << (`raceId` - 1)) AS "raceMask" FROM dbc_charbaseinfo GROUP BY `classId`'); Util::arraySumByKey($classes, $races); // add skills - if ($skills = DB::Aowow()->selectCol('SELECT LOG(2, `classMask`) + 1 AS ARRAY_KEY, GROUP_CONCAT(`skillLine` SEPARATOR \' \') FROM dbc_skillraceclassinfo WHERE `flags` = ?d GROUP BY `classMask` HAVING ARRAY_KEY = CAST(LOG(2, `classMask`) + 1 AS SIGNED)', 0x410)) + if ($skills = DB::Aowow()->selectCol('SELECT LOG(2, `classMask`) + 1 AS ARRAY_KEY, GROUP_CONCAT(`skillLine` SEPARATOR \' \') FROM dbc_skillraceclassinfo WHERE `flags` = %i GROUP BY `classMask` HAVING ARRAY_KEY = CAST(LOG(2, `classMask`) + 1 AS SIGNED)', 0x410)) foreach ($skills as $classId => $skillStr) $classes[$classId]['skills'] = $skillStr; + // collect iconIds + $iconIds = DB::Aowow()->selectCol('SELECT `id`, `name` AS ARRAY_KEY FROM ::icons WHERE `name` IN %in', array_filter(array_map(fn($x) => 'class_'.strtolower($x['fileString']), $classes))); + foreach ($classes AS $id => $class) + $classes[$id]['iconId'] = $iconIds['class_'.strtolower($class['fileString'])] ?? 0; + // add weaponTypeMask & armorTypeMask foreach ($classes as $id => &$data) { @@ -42,22 +48,22 @@ CLISetup::registerSetup("sql", new class extends SetupScript 'SELECT BIT_OR(`equippedItemSubClassMask`) FROM dbc_spell s JOIN dbc_skilllineability sla ON sla.`spellId` = s.`id` - JOIN dbc_skillraceclassinfo srci ON srci.`skillLine` = sla.`skillLineId` AND srci.`classMask` & ?d - WHERE sla.`skilllineid` <> 183 AND (sla.`reqClassMask` & ?d OR sla.`reqClassMask` = 0) AND `equippedItemClass` = ?d AND (`effect1Id` = ?d OR `effect2Id` = ?d)', + JOIN dbc_skillraceclassinfo srci ON srci.`skillLine` = sla.`skillLineId` AND srci.`classMask` & %i + WHERE sla.`skilllineid` <> 183 AND (sla.`reqClassMask` & %i OR sla.`reqClassMask` = 0) AND `equippedItemClass` = %i AND (`effect1Id` = %i OR `effect2Id` = %i)', $mask, $mask, ITEM_CLASS_WEAPON, SPELL_EFFECT_PROFICIENCY, SPELL_EFFECT_PROFICIENCY ); $data['armorTypeMask'] = DB::Aowow()->selectCell( 'SELECT BIT_OR(`equippedItemSubClassMask`) FROM dbc_spell s JOIN dbc_skilllineability sla ON sla.`spellId` = s.`id` - JOIN dbc_skillraceclassinfo srci ON srci.`skillLine` = sla.`skillLineId` AND srci.`classMask` & ?d - WHERE sla.`reqClassMask` & ?d AND `equippedItemClass` = ?d AND (`effect1Id` = ?d OR `effect2Id` = ?d)', + JOIN dbc_skillraceclassinfo srci ON srci.`skillLine` = sla.`skillLineId` AND srci.`classMask` & %i + WHERE sla.`reqClassMask` & %i AND `equippedItemClass` = %i AND (`effect1Id` = %i OR `effect2Id` = %i)', $mask, $mask, ITEM_CLASS_ARMOR, SPELL_EFFECT_PROFICIENCY, SPELL_EFFECT_PROFICIENCY ); } foreach ($classes as $cl) - DB::Aowow()->query('INSERT INTO ?_classes (?#) VALUES (?a)', array_keys($cl), array_values($cl)); + DB::Aowow()->qry('INSERT INTO ::classes %v', $cl); $this->reapplyCCFlags('classes', Type::CHR_CLASS); diff --git a/setup/tools/sqlgen/creature.ss.php b/setup/tools/sqlgen/creature.ss.php index 4b8b7e41..fea5bb27 100644 --- a/setup/tools/sqlgen/creature.ss.php +++ b/setup/tools/sqlgen/creature.ss.php @@ -18,11 +18,11 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['creaturedisplayinfo', 'creaturedisplayinfoextra']; protected $worldDependency = ['creature_template', 'creature_template_locale', 'creature_template_resistance', 'creature_template_spell', 'creature_classlevelstats', 'creature_default_trainer', 'trainer', 'instance_encounters']; - public function generate(array $ids = []) : bool + public function generate() : bool { $baseQuery = 'SELECT ct.entry, - IF(ie.creditEntry IS NULL, 0, ?d) AS cuFlags, + IF(ie.creditEntry IS NULL, 0, %i) AS cuFlags, difficulty_entry_1, difficulty_entry_2, difficulty_entry_3, KillCredit1, KillCredit2, modelid1, modelid2, modelid3, modelid4, @@ -61,7 +61,6 @@ CLISetup::registerSetup("sql", new class extends SetupScript PetSpellDataId, VehicleId, mingold, maxgold, - AIName, (CASE ct.exp WHEN 0 THEN min.basehp0 WHEN 1 THEN min.basehp1 ELSE min.basehp2 END) * ct.HealthModifier AS healthMin, (CASE ct.exp WHEN 0 THEN max.basehp0 WHEN 1 THEN max.basehp1 ELSE max.basehp2 END) * ct.HealthModifier AS healthMax, min.basemana * ct.ManaModifier AS manaMin, @@ -71,8 +70,10 @@ CLISetup::registerSetup("sql", new class extends SetupScript IFNULL(ctr1.Resistance, 0), IFNULL(ctr2.Resistance, 0), IFNULL(ctr3.Resistance, 0), IFNULL(ctr4.Resistance, 0), IFNULL(ctr5.Resistance, 0), IFNULL(ctr6.Resistance, 0), RacialLeader, mechanic_immune_mask, + spell_school_immune_mask, flags_extra, - ScriptName + NULLIF(IF(ScriptName <> "", ScriptName, AIName), ""), + StringId FROM creature_template ct JOIN creature_classlevelstats min ON ct.unit_class = min.class AND ct.minlevel = min.level JOIN creature_classlevelstats max ON ct.unit_class = max.class AND ct.maxlevel = max.level @@ -98,22 +99,22 @@ CLISetup::registerSetup("sql", new class extends SetupScript LEFT JOIN creature_template_resistance ctr4 ON ct.entry = ctr4.CreatureID AND ctr4.School = 4 LEFT JOIN creature_template_resistance ctr5 ON ct.entry = ctr5.CreatureID AND ctr5.School = 5 LEFT JOIN creature_template_resistance ctr6 ON ct.entry = ctr6.CreatureID AND ctr6.School = 6 - { WHERE ct.entry IN (?a) } - LIMIT ?d, ?d'; + LIMIT %i, %i'; + + DB::Aowow()->qry('TRUNCATE ::creature'); $i = 0; - DB::Aowow()->query('TRUNCATE ?_creature'); - while ($npcs = DB::World()->select($baseQuery, NPC_CU_INSTANCE_BOSS, $ids ?: DBSIMPLE_SKIP, CLISetup::SQL_BATCH * $i, CLISetup::SQL_BATCH)) + while ($npcs = DB::World()->selectAssoc($baseQuery, NPC_CU_INSTANCE_BOSS, CLISetup::SQL_BATCH * $i, CLISetup::SQL_BATCH)) { CLI::write(' * batch #' . ++$i . ' (' . count($npcs) . ')', CLI::LOG_BLANK, true, true); foreach ($npcs as $npc) - DB::Aowow()->query('INSERT INTO ?_creature VALUES (?a)', array_values($npc)); + DB::Aowow()->qry('INSERT INTO ::creature VALUES %l', $npc); } // apply "textureString", "modelId" and "iconSring" - DB::Aowow()->query( - 'UPDATE ?_creature c + DB::Aowow()->qry( + 'UPDATE ::creature c JOIN dbc_creaturedisplayinfo cdi ON c.displayId1 = cdi.id LEFT JOIN dbc_creaturedisplayinfoextra cdie ON cdi.extraInfoId = cdie.id SET c.textureString = IFNULL(cdie.textureString, cdi.skin1), @@ -123,21 +124,21 @@ CLISetup::registerSetup("sql", new class extends SetupScript ); // apply cuFlag: difficultyDummy - DB::Aowow()->query( - 'UPDATE ?_creature a - JOIN (SELECT b.difficultyEntry1 AS dummy FROM ?_creature b UNION - SELECT c.difficultyEntry2 AS dummy FROM ?_creature c UNION - SELECT d.difficultyEntry3 AS dummy FROM ?_creature d) j - SET a.cuFlags = a.cuFlags | ?d + DB::Aowow()->qry( + 'UPDATE ::creature a + JOIN (SELECT b.difficultyEntry1 AS dummy FROM ::creature b UNION + SELECT c.difficultyEntry2 AS dummy FROM ::creature c UNION + SELECT d.difficultyEntry3 AS dummy FROM ::creature d) j + SET a.cuFlags = a.cuFlags | %i WHERE a.id = j.dummy', NPC_CU_DIFFICULTY_DUMMY | CUSTOM_EXCLUDE_FOR_LISTVIEW ); // apply cuFlag: excludeFromListview [for trigger-creatures] - DB::Aowow()->query('UPDATE ?_creature SET cuFlags = cuFlags | ?d WHERE flagsExtra & ?d', CUSTOM_EXCLUDE_FOR_LISTVIEW, 0x80); + DB::Aowow()->qry('UPDATE ::creature SET cuFlags = cuFlags | %i WHERE flagsExtra & %i', CUSTOM_EXCLUDE_FOR_LISTVIEW, 0x80); // apply cuFlag: exCludeFromListview [for nameparts indicating internal usage] - DB::Aowow()->query('UPDATE ?_creature SET cuFlags = cuFlags | ?d WHERE name_loc0 LIKE "%[%" OR name_loc0 LIKE "%(%" OR name_loc0 LIKE "%visual%" OR name_loc0 LIKE "%trigger%" OR name_loc0 LIKE "%credit%" OR name_loc0 LIKE "%marker%"', CUSTOM_EXCLUDE_FOR_LISTVIEW); + DB::Aowow()->qry('UPDATE ::creature SET cuFlags = cuFlags | %i WHERE name_loc0 LIKE "%[%" OR name_loc0 LIKE "%(%" OR name_loc0 LIKE "%visual%" OR name_loc0 LIKE "%trigger%" OR name_loc0 LIKE "%credit%" OR name_loc0 LIKE "%marker%"', CUSTOM_EXCLUDE_FOR_LISTVIEW); $this->reapplyCCFlags('creature', Type::NPC); diff --git a/setup/tools/sqlgen/currencies.ss.php b/setup/tools/sqlgen/currencies.ss.php index cae734fc..79fcf193 100644 --- a/setup/tools/sqlgen/currencies.ss.php +++ b/setup/tools/sqlgen/currencies.ss.php @@ -21,15 +21,15 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $worldDependency = ['item_template', 'item_template_locale']; protected $setupAfter = [['icons'], []]; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_currencies'); - DB::Aowow()->query('INSERT INTO ?_currencies (`id`, `category`, `itemId`) SELECT `id`, LEAST(`category`, 41), `itemId` FROM dbc_currencytypes'); + DB::Aowow()->qry('TRUNCATE ::currencies'); + DB::Aowow()->qry('INSERT INTO ::currencies (`id`, `category`, `itemId`) SELECT `id`, LEAST(`category`, 41), `itemId` FROM dbc_currencytypes'); - $moneyItems = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `itemId` FROM dbc_currencytypes{ WHERE `id` IN (?a)}', $ids ?: DBSIMPLE_SKIP); + $moneyItems = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `itemId` FROM dbc_currencytypes'); // apply names & cap - $moneyNames = DB::World()->select( + $moneyNames = DB::World()->selectAssoc( 'SELECT it.`entry` AS ARRAY_KEY, it.`name` AS `name_loc0`, IFNULL(itl2.`Name`, "") AS `name_loc2`, IFNULL(itl3.`Name`, "") AS `name_loc3`, IFNULL(itl4.`Name`, "") AS `name_loc4`, IFNULL(itl6.`Name`, "") AS `name_loc6`, IFNULL(itl8.`Name`, "") AS `name_loc8`, it.`maxCount` AS `cap` @@ -39,7 +39,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript LEFT JOIN item_template_locale itl4 ON it.entry = itl4.ID AND itl4.locale = "zhCN" LEFT JOIN item_template_locale itl6 ON it.entry = itl6.ID AND itl6.locale = "esES" LEFT JOIN item_template_locale itl8 ON it.entry = itl8.ID AND itl8.locale = "ruRU" - WHERE it.entry IN (?a)', + WHERE it.entry IN %in', $moneyItems ); @@ -53,16 +53,16 @@ CLISetup::registerSetup("sql", new class extends SetupScript $strings = ['name_loc0' => 'Item #'.$itemId.' not in DB', 'iconId' => 0, 'cuFlags' => CUSTOM_EXCLUDE_FOR_LISTVIEW, 'category' => 3]; } - DB::Aowow()->query('UPDATE ?_currencies SET ?a WHERE itemId = ?d', $strings, $itemId); + DB::Aowow()->qry('UPDATE ::currencies SET %a WHERE itemId = %i', $strings, $itemId); } // apply icons - $displayIds = DB::World()->selectCol('SELECT `entry` AS ARRAY_KEY, `displayid` FROM item_template WHERE `entry` IN (?a)', $moneyItems); + $displayIds = DB::World()->selectCol('SELECT `entry` AS ARRAY_KEY, `displayid` FROM item_template WHERE `entry` IN %in', $moneyItems); foreach ($displayIds as $itemId => $iconId) - DB::Aowow()->query( - 'UPDATE ?_currencies c, ?_icons i, dbc_itemdisplayinfo idi + DB::Aowow()->qry( + 'UPDATE ::currencies c, ::icons i, dbc_itemdisplayinfo idi SET c.`iconId` = i.`id` - WHERE i.`name` = LOWER(idi.`inventoryIcon1`) AND idi.`id` = ?d AND c.`itemId` = ?d', + WHERE i.`name_source` = LOWER(idi.`inventoryIcon1`) AND idi.`id` = %i AND c.`itemId` = %i', $iconId, $itemId ); diff --git a/setup/tools/sqlgen/declinedword.ss.php b/setup/tools/sqlgen/declinedword.ss.php index e0837c70..2173f03a 100644 --- a/setup/tools/sqlgen/declinedword.ss.php +++ b/setup/tools/sqlgen/declinedword.ss.php @@ -17,15 +17,15 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['declinedword', 'declinedwordcases']; - public function generate(array $ids = []) : bool + public function generate() : bool { CLI::write('[declinedwords] - copying declinedword.dbc into aowow_declinedword'); - DB::Aowow()->query('TRUNCATE ?_declinedword'); - DB::Aowow()->query('INSERT INTO ?_declinedword SELECT * FROM dbc_declinedword'); + DB::Aowow()->qry('TRUNCATE ::declinedword'); + DB::Aowow()->qry('INSERT INTO ::declinedword SELECT * FROM dbc_declinedword'); CLI::write('[declinedwords] - copying declinedwordcases.dbc into aowow_declinedwordcases'); - DB::Aowow()->query('TRUNCATE ?_declinedwordcases'); - DB::Aowow()->query('INSERT INTO ?_declinedwordcases SELECT `wordId`, `caseIdx`, `word` FROM dbc_declinedwordcases'); + DB::Aowow()->qry('TRUNCATE ::declinedwordcases'); + DB::Aowow()->qry('INSERT INTO ::declinedwordcases SELECT `wordId`, `caseIdx`, `word` FROM dbc_declinedwordcases'); return true; } diff --git a/setup/tools/sqlgen/emotes.ss.php b/setup/tools/sqlgen/emotes.ss.php index 6a3258ca..0492acbf 100644 --- a/setup/tools/sqlgen/emotes.ss.php +++ b/setup/tools/sqlgen/emotes.ss.php @@ -20,7 +20,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript private $textData = []; - public function generate(array $ids = []) : bool + public function generate() : bool { $globStrPath = CLISetup::$srcDir.'%sInterface/FrameXML/GlobalStrings.lua'; $allOK = true; @@ -33,8 +33,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write(' Emote aliasses can not be generated for affected locales!', CLI::LOG_WARN); } - DB::Aowow()->query('TRUNCATE ?_emotes'); - DB::Aowow()->query('TRUNCATE ?_emotes_aliasses'); + DB::Aowow()->qry('TRUNCATE ::emotes'); + DB::Aowow()->qry('TRUNCATE ::emotes_aliasses'); /*********************/ @@ -53,13 +53,13 @@ CLISetup::registerSetup("sql", new class extends SetupScript 12 female others no ext -> none 4 - */ - $this->textData = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `text_loc0` AS "0", `text_loc2` AS "2", `text_loc3` AS "3", `text_loc4` AS "4", `text_loc6` AS "6", `text_loc8` AS "8" FROM dbc_emotestextdata'); + $this->textData = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `text_loc0` AS "0", `text_loc2` AS "2", `text_loc3` AS "3", `text_loc4` AS "4", `text_loc6` AS "6", `text_loc8` AS "8" FROM dbc_emotestextdata'); - $texts = DB::Aowow()->select('SELECT et.`id` AS ARRAY_KEY, LOWER(`command`) AS "cmd", IF(e.`animationId`, 1, 0) AS "anim", -`emoteId` AS "parent", e.`soundId`, `etd0`, `etd1`, `etd2`, `etd4`, `etd6`, `etd8`, `etd9`, `etd12` FROM dbc_emotestext et LEFT JOIN dbc_emotes e ON e.`id` = et.`emoteId`'); + $texts = DB::Aowow()->selectAssoc('SELECT et.`id` AS ARRAY_KEY, LOWER(`command`) AS "cmd", IF(e.`animationId`, 1, 0) AS "anim", -`emoteId` AS "parent", e.`soundId`, `etd0`, `etd1`, `etd2`, `etd4`, `etd6`, `etd8`, `etd9`, `etd12` FROM dbc_emotestext et LEFT JOIN dbc_emotes e ON e.`id` = et.`emoteId`'); foreach ($texts AS $id => $t) { - DB::Aowow()->query( - 'INSERT INTO ?_emotes ( + DB::Aowow()->qry( + 'INSERT INTO ::emotes ( `id`, `cmd`, `isAnimated`, `parentEmote`, `soundId`, `extToExt_loc0`, `extToMe_loc0`, `meToExt_loc0`, `extToNone_loc0`, `meToNone_loc0`, `extToExt_loc2`, `extToMe_loc2`, `meToExt_loc2`, `extToNone_loc2`, `meToNone_loc2`, @@ -68,7 +68,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript `extToExt_loc6`, `extToMe_loc6`, `meToExt_loc6`, `extToNone_loc6`, `meToNone_loc6`, `extToExt_loc8`, `extToMe_loc8`, `meToExt_loc8`, `extToNone_loc8`, `meToNone_loc8`) VALUES - (?d, ?, ?d, ?d, ?d, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + (%i, %s, %i, %i, %i, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)', $id, $t['cmd'], $t['anim'], $t['parent'], $t['soundId'], $this->mergeGenderedStrings($t['etd0'], $t['etd8'], Locale::EN), $this->mergeGenderedStrings($t['etd1'], $t['etd9'], Locale::EN), $this->textData[$t['etd2']][Locale::EN->value] ?? '', $this->mergeGenderedStrings($t['etd4'], $t['etd12'], Locale::EN), $this->textData[$t['etd6']][Locale::EN->value] ?? '', $this->mergeGenderedStrings($t['etd0'], $t['etd8'], Locale::FR), $this->mergeGenderedStrings($t['etd1'], $t['etd9'], Locale::FR), $this->textData[$t['etd2']][Locale::FR->value] ?? '', $this->mergeGenderedStrings($t['etd4'], $t['etd12'], Locale::FR), $this->textData[$t['etd6']][Locale::FR->value] ?? '', @@ -88,12 +88,12 @@ CLISetup::registerSetup("sql", new class extends SetupScript foreach (CLISetup::searchGlobalStrings('/^VOICEMACRO_LABEL_([A-Z]+)\d+ = \"([^"]+)\";$/') as $locId => [, $cmd, $alias]) $voiceAliases[$cmd][] = [$locId, $alias]; - $emotes = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, cmd FROM ?_emotes'); + $emotes = DB::Aowow()->selectCol('SELECT id AS ARRAY_KEY, cmd FROM ::emotes'); foreach($emotes as $eId => $cmd) { foreach ($voiceAliases[strtoupper($cmd)] ?? [] as [$locId, $alias]) - DB::Aowow()->query('INSERT IGNORE INTO ?_emotes_aliasses VALUES (?d, ?d, ?) ON DUPLICATE KEY UPDATE `locales` = `locales` | ?d', $eId, (1 << $locId), mb_strtolower($alias), (1 << $locId)); + DB::Aowow()->qry('INSERT IGNORE INTO ::emotes_aliasses VALUES (%i, %i, %s) ON DUPLICATE KEY UPDATE `locales` = `locales` | %i', $eId, (1 << $locId), mb_strtolower($alias), (1 << $locId)); foreach ($aliasses as $data) { @@ -101,20 +101,20 @@ CLISetup::registerSetup("sql", new class extends SetupScript continue; foreach ($data as [$locId, $alias]) - DB::Aowow()->query('INSERT IGNORE INTO ?_emotes_aliasses VALUES (?d, ?d, ?) ON DUPLICATE KEY UPDATE `locales` = `locales` | ?d', $eId, (1 << $locId), mb_strtolower($alias), (1 << $locId)); + DB::Aowow()->qry('INSERT IGNORE INTO ::emotes_aliasses VALUES (%i, %i, %s) ON DUPLICATE KEY UPDATE `locales` = `locales` | %i', $eId, (1 << $locId), mb_strtolower($alias), (1 << $locId)); break; } } - DB::Aowow()->query('UPDATE ?_emotes e LEFT JOIN ?_emotes_aliasses ea ON ea.`id` = e.`id` SET e.`cuFlags` = e.`cuFlags` | ?d WHERE ea.`id` IS NULL', CUSTOM_EXCLUDE_FOR_LISTVIEW | EMOTE_CU_MISSING_CMD); + DB::Aowow()->qry('UPDATE ::emotes e LEFT JOIN ::emotes_aliasses ea ON ea.`id` = e.`id` SET e.`cuFlags` = e.`cuFlags` | %i WHERE ea.`id` IS NULL', CUSTOM_EXCLUDE_FOR_LISTVIEW | EMOTE_CU_MISSING_CMD); /*********************/ /* Server controlled */ /*********************/ - DB::Aowow()->query('INSERT INTO ?_emotes (`id`, `cmd`, `flags`, `isAnimated`, `parentEmote`, `soundId`, `state`, `stateParam`, `cuFlags`) SELECT -`id`, `name`, `flags`, IF(`animationId`, 1, 0), 0, `soundId`, `state`, `stateParam`, ?d FROM dbc_emotes WHERE 1', CUSTOM_EXCLUDE_FOR_LISTVIEW); + DB::Aowow()->qry('INSERT INTO ::emotes (`id`, `cmd`, `flags`, `isAnimated`, `parentEmote`, `soundId`, `state`, `stateParam`, `cuFlags`) SELECT -`id`, `name`, `flags`, IF(`animationId`, 1, 0), 0, `soundId`, `state`, `stateParam`, %i FROM dbc_emotes WHERE 1', CUSTOM_EXCLUDE_FOR_LISTVIEW); $this->reapplyCCFlags('emotes', Type::EMOTE); diff --git a/setup/tools/sqlgen/events.ss.php b/setup/tools/sqlgen/events.ss.php index d1860b92..1cfdda26 100644 --- a/setup/tools/sqlgen/events.ss.php +++ b/setup/tools/sqlgen/events.ss.php @@ -17,11 +17,11 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $worldDependency = ['game_event', 'game_event_prerequisite']; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_events'); + DB::Aowow()->qry('TRUNCATE ::events'); - $events = DB::World()->select( + $events = DB::World()->selectAssoc( 'SELECT ge.eventEntry, holiday, 0, -- cuFlags @@ -33,13 +33,11 @@ CLISetup::registerSetup("sql", new class extends SetupScript description FROM game_event ge LEFT JOIN game_event_prerequisite gep ON gep.eventEntry = ge.eventEntry - { WHERE ge.eventEntry IN (?a) } GROUP BY ge.eventEntry', - $ids ?: DBSIMPLE_SKIP ); foreach ($events as $e) - DB::Aowow()->query('INSERT INTO ?_events VALUES (?a)', array_values($e)); + DB::Aowow()->qry('INSERT INTO ::events VALUES %l', $e); $this->reapplyCCFlags('events', Type::WORLDEVENT); diff --git a/setup/tools/sqlgen/factions.ss.php b/setup/tools/sqlgen/factions.ss.php index 7fb7e001..d62eba8f 100644 --- a/setup/tools/sqlgen/factions.ss.php +++ b/setup/tools/sqlgen/factions.ss.php @@ -19,72 +19,72 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['faction', 'factiontemplate']; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_factions'); - DB::Aowow()->query('TRUNCATE ?_factiontemplate'); + DB::Aowow()->qry('TRUNCATE ::factions'); + DB::Aowow()->qry('TRUNCATE ::factiontemplate'); - DB::Aowow()->query( - 'INSERT INTO ?_factions - SELECT f.id, - f.repIdx, - baseRepRaceMask1, baseRepRaceMask2, baseRepRaceMask3, baseRepRaceMask4, - baseRepClassMask1, baseRepClassMask2, baseRepClassMask3, baseRepClassMask4, - baseRepValue1, baseRepValue2, baseRepValue3, baseRepValue4, - IF(SUM(ft.ourMask & 0x6) / COUNT(1) = 0x4, 2, IF(SUM(ft.ourMask & 0x6) / COUNT(1) = 0x2, 1, 0)) as side, + DB::Aowow()->qry( + 'INSERT INTO ::factions + SELECT f.`id`, + f.`repIdx`, + `baseRepRaceMask1`, `baseRepRaceMask2`, `baseRepRaceMask3`, `baseRepRaceMask4`, + `baseRepClassMask1`, `baseRepClassMask2`, `baseRepClassMask3`, `baseRepClassMask4`, + `baseRepValue1`, `baseRepValue2`, `baseRepValue3`, `baseRepValue4`, + IF(SUM(ft.`ourMask` & 0x6) / COUNT(1) = 0x4, 2, IF(SUM(ft.`ourMask` & 0x6) / COUNT(1) = 0x2, 1, 0)) AS "side", 0, -- expansion "", -- quartermasterNpcIds "", -- factionTemplateIds 0, -- cuFlags - parentFaction, - spilloverRateIn, spilloverRateOut, spilloverMaxRank, - name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8 + `parentFaction`, + `spilloverRateIn`, `spilloverRateOut`, `spilloverMaxRank`, + `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8` FROM dbc_faction f - LEFT JOIN dbc_factiontemplate ft ON ft.factionid = f.id - GROUP BY f.id' + LEFT JOIN dbc_factiontemplate ft ON ft.`factionid` = f.`id` + GROUP BY f.`id`' ); - DB::Aowow()->query( - 'INSERT INTO ?_factiontemplate - SELECT id, - factionId, - IF(friendFactionId1 = 1 OR friendFactionId2 = 1 OR friendFactionId3 = 1 OR friendFactionId4 = 1 OR friendlyMask & 0x3, 1, - IF(enemyFactionId1 = 1 OR enemyFactionId2 = 1 OR enemyFactionId3 = 1 OR enemyFactionId4 = 1 OR hostileMask & 0x3, -1, 0)), - IF(friendFactionId1 = 2 OR friendFactionId2 = 2 OR friendFactionId3 = 2 OR friendFactionId4 = 2 OR friendlyMask & 0x5, 1, - IF(enemyFactionId1 = 2 OR enemyFactionId2 = 2 OR enemyFactionId3 = 2 OR enemyFactionId4 = 2 OR hostileMask & 0x5, -1, 0)) + DB::Aowow()->qry( + 'INSERT INTO ::factiontemplate + SELECT `id`, + `factionId`, + IF(`friendFactionId1` = 1 OR `friendFactionId2` = 1 OR `friendFactionId3` = 1 OR `friendFactionId4` = 1 OR `friendlyMask` & 0x3, 1, + IF(`enemyFactionId1` = 1 OR `enemyFactionId2` = 1 OR `enemyFactionId3` = 1 OR `enemyFactionId4` = 1 OR `hostileMask` & 0x3, -1, 0)), + IF(`friendFactionId1` = 2 OR `friendFactionId2` = 2 OR `friendFactionId3` = 2 OR `friendFactionId4` = 2 OR `friendlyMask` & 0x5, 1, + IF(`enemyFactionId1` = 2 OR `enemyFactionId2` = 2 OR `enemyFactionId3` = 2 OR `enemyFactionId4` = 2 OR `hostileMask` & 0x5, -1, 0)) FROM dbc_factiontemplate' ); - DB::Aowow()->query( - 'UPDATE ?_factions f - JOIN (SELECT ft.factionId, GROUP_CONCAT(ft.id SEPARATOR " ") AS tplIds FROM dbc_factiontemplate ft GROUP BY ft.factionId) temp ON f.id = temp.factionId - SET f.templateIds = temp.tplIds' + DB::Aowow()->qry( + 'UPDATE ::factions f + JOIN (SELECT ft.`factionId`, GROUP_CONCAT(ft.`id` SEPARATOR " ") AS "tplIds" FROM dbc_factiontemplate ft GROUP BY ft.`factionId`) temp ON f.`id` = temp.`factionId` + SET f.`templateIds` = temp.`tplIds`' ); - DB::Aowow()->query( - 'UPDATE ?_factions x - JOIN dbc_faction f ON f.id = x.id - LEFT JOIN dbc_factiontemplate ft ON f.id = ft.factionId - SET cuFlags = cuFlags | ?d - WHERE f.repIdx < 0 OR ( f.repIdx > 0 AND (f.repFlags1 & 0x8 OR ft.id IS NULL) AND (f.repFlags1 & 0x80) = 0 )', - CUSTOM_EXCLUDE_FOR_LISTVIEW + DB::Aowow()->qry( + 'UPDATE ::factions x + JOIN dbc_faction f ON f.`id` = x.`id` + SET `cuFlags` = `cuFlags` | %i + WHERE f.`repIdx` < 0 OR f.`id` = 952 OR ( (f.`repFlags1` & %i) > 0 AND f.`id` NOT IN (67, 169, 469, 589, 1085) AND (f.`repFLags1` & %i) = 0 )', + CUSTOM_EXCLUDE_FOR_LISTVIEW, + FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED, FACTION_FLAG_SPECIAL ); $pairs = array( - [[980], ['expansion' => 1]], - [[1097], ['expansion' => 2]], - [[469, 891, 1037], ['side' => 1]], - [[ 67, 892, 1052], ['side' => 2]], + [[980], ['expansion' => EXP_BC]], + [[1097], ['expansion' => EXP_WOTLK]], + [[469, 891, 1037], ['side' => SIDE_ALLIANCE]], + [[ 67, 892, 1052], ['side' => SIDE_HORDE]], ); - foreach ($pairs as $p) - DB::Aowow()->query( - 'UPDATE ?_factions top - JOIN (SELECT id, parentFactionId FROM ?_factions) mid ON mid.parentFactionId IN (?a) - LEFT JOIN (SELECT id, parentFactionId FROM ?_factions) low ON low.parentFactionId = mid.id - SET ?a - WHERE repIdx > 0 AND (top.id IN (?a) OR top.id = mid.id OR top.id = low.id)', - $p[0], $p[1], $p[0] + foreach ($pairs as [$factions, $update]) + DB::Aowow()->qry( + 'UPDATE ::factions top + JOIN (SELECT `id`, `parentFactionId` FROM ::factions) mid ON mid.`parentFactionId` IN %in + LEFT JOIN (SELECT `id`, `parentFactionId` FROM ::factions) low ON low.`parentFactionId` = mid.`id` + SET %a + WHERE `repIdx` > 0 AND (top.`id` IN %in OR top.`id` = mid.`id` OR top.`id` = low.`id`)', + $factions, $update, $factions ); $this->reapplyCCFlags('factions', Type::FACTION); diff --git a/setup/tools/sqlgen/glyphproperties.ss.php b/setup/tools/sqlgen/glyphproperties.ss.php index b335d600..53736593 100644 --- a/setup/tools/sqlgen/glyphproperties.ss.php +++ b/setup/tools/sqlgen/glyphproperties.ss.php @@ -18,12 +18,12 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['glyphproperties', 'spellicon']; protected $setupAfter = [['icons'], []]; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_glyphproperties'); - DB::Aowow()->query('INSERT INTO ?_glyphproperties SELECT id, spellId, typeFlags, 0, iconId FROM dbc_glyphproperties'); + DB::Aowow()->qry('TRUNCATE ::glyphproperties'); + DB::Aowow()->qry('INSERT INTO ::glyphproperties SELECT `id`, `spellId`, `typeFlags`, 0, `iconId` FROM dbc_glyphproperties'); - DB::Aowow()->query('UPDATE ?_glyphproperties gp, ?_icons ic, dbc_spellicon si SET gp.iconId = ic.id WHERE gp.iconIdBak = si.id AND ic.name = LOWER(SUBSTRING_INDEX(si.iconPath, "\\\\", -1))'); + DB::Aowow()->qry('UPDATE ::glyphproperties gp, ::icons ic, dbc_spellicon si SET gp.`iconId` = ic.`id` WHERE gp.`iconIdBak` = si.`id` AND ic.`name_source` = LOWER(SUBSTRING_INDEX(si.`iconPath`, "\\", -1))'); return true; } diff --git a/setup/tools/sqlgen/holidays.ss.php b/setup/tools/sqlgen/holidays.ss.php index aa96c93a..2f79c122 100644 --- a/setup/tools/sqlgen/holidays.ss.php +++ b/setup/tools/sqlgen/holidays.ss.php @@ -14,23 +14,41 @@ CLISetup::registerSetup("sql", new class extends SetupScript { use TrCustomData; // import custom data from DB + private const /* array */ CUSTOM_ICONS = array( + 62 => 'calendar_fireworksstart', // has no texture in dbc but exists as blp + 283 => 'inv_jewelry_necklace_21', + 284 => 'inv_misc_rune_07', + 285 => 'inv_jewelry_amulet_07', + 353 => 'spell_nature_eyeofthestorm', + 400 => 'achievement_bg_winsoa', + 420 => 'achievement_bg_winwsg' + ); + protected $info = array( 'holidays' => [[], CLISetup::ARGV_PARAM, 'Compiles supplemental data for type: Event from dbc.'] ); + protected $setupAfter = [['icons'], []]; protected $dbcSourceFiles = ['holidays', 'holidaydescriptions', 'holidaynames']; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_holidays'); - DB::Aowow()->query( - 'INSERT INTO ?_holidays (id, name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8, description_loc0, description_loc2, description_loc3, description_loc4, description_loc6, description_loc8, looping, scheduleType, textureString) - SELECT h.id, n.name_loc0, n.name_loc2, n.name_loc3, n.name_loc4, n.name_loc6, n.name_loc8, d.description_loc0, d.description_loc2, d.description_loc3, d.description_loc4, d.description_loc6, d.description_loc8, h.looping, h.scheduleType, h.textureString + DB::Aowow()->qry('TRUNCATE ::holidays'); + DB::Aowow()->qry( + 'INSERT INTO ::holidays (`id`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `description_loc0`, `description_loc2`, `description_loc3`, `description_loc4`, `description_loc6`, `description_loc8`, `looping`, `scheduleType`, `textureString`) + SELECT h.`id`, n.`name_loc0`, n.`name_loc2`, n.`name_loc3`, n.`name_loc4`, n.`name_loc6`, n.`name_loc8`, d.`description_loc0`, d.`description_loc2`, d.`description_loc3`, d.`description_loc4`, d.`description_loc6`, d.`description_loc8`, h.`looping`, h.`scheduleType`, h.`textureString` FROM dbc_holidays h - LEFT JOIN dbc_holidaynames n ON n.id = h.nameId - LEFT JOIN dbc_holidaydescriptions d ON d.id = h.descriptionId' + LEFT JOIN dbc_holidaynames n ON n.`id` = h.`nameId` + LEFT JOIN dbc_holidaydescriptions d ON d.`id` = h.`descriptionId`' ); + // set derived icons + DB::Aowow()->qry('UPDATE ::holidays h, ::icons i SET h.`iconId` = i.`id` WHERE i.`name_source` LIKE CONCAT(LOWER(h.`textureString`), "%") AND h.`textureString` <> ""'); + + // set custom icons + foreach (self::CUSTOM_ICONS as $hId => $iconString) + DB::Aowow()->qry('UPDATE ::holidays h SET h.`iconId` = (SELECT i.`id` FROM ::icons i WHERE `name_source` = ?) WHERE `id` = %i', $iconString, $hId); + return true; } }); diff --git a/setup/tools/sqlgen/icons.ss.php b/setup/tools/sqlgen/icons.ss.php index 16d79f4f..d41fac0f 100644 --- a/setup/tools/sqlgen/icons.ss.php +++ b/setup/tools/sqlgen/icons.ss.php @@ -11,25 +11,63 @@ if (!CLI) CLISetup::registerSetup("sql", new class extends SetupScript { + private const /* array */ HOLIDAY_ICONS = array( + 'calendar_winterveilstart', + 'calendar_noblegardenstart', + 'calendar_childrensweekstart', + 'calendar_fishingextravaganzastart', + 'calendar_harvestfestivalstart', + 'calendar_hallowsendstart', + 'calendar_lunarfestivalstart', + 'calendar_loveintheairstart', + 'calendar_midsummerstart', + 'calendar_brewfeststart', + 'calendar_darkmoonfaireelwynnstart', + 'calendar_darkmoonfairemulgorestart', + 'calendar_darkmoonfaireterokkarstart', + 'calendar_piratesdaystart', + 'calendar_wotlklaunchstart', + 'calendar_dayofthedeadstart', + 'calendar_fireworksstart' + ); + protected $info = array( 'icons' => [[], CLISetup::ARGV_PARAM, 'Compiles data for type: Icons from dbc.'] ); protected $dbcSourceFiles = ['spellicon', 'itemdisplayinfo', 'creaturefamily']; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_icons'); - DB::Aowow()->query('ALTER TABLE ?_icons AUTO_INCREMENT = 1'); - DB::Aowow()->query( - 'INSERT INTO ?_icons (`name`) SELECT x FROM + DB::Aowow()->qry('TRUNCATE ::icons'); + DB::Aowow()->qry('ALTER TABLE ::icons AUTO_INCREMENT = 1'); + DB::Aowow()->qry( + 'INSERT INTO ::icons (`name`, `name_source`) SELECT REGEXP_REPLACE(x, "\\W", "-"), x FROM ( - (SELECT LOWER(SUBSTRING_INDEX(iconPath, "\\\\", -1)) AS x FROM dbc_spellicon WHERE iconPath LIKE "%icons%") UNION - (SELECT LOWER(inventoryIcon1) AS x FROM dbc_itemdisplayinfo WHERE inventoryIcon1 <> "") UNION - (SELECT LOWER(SUBSTRING_INDEX(iconString, "\\\\", -1)) AS x FROM dbc_creaturefamily WHERE iconString LIKE "%icons%") + (SELECT LOWER(SUBSTRING_INDEX(`iconPath`, "\\", -1)) AS x FROM dbc_spellicon WHERE `iconPath` LIKE "%icons%") UNION + (SELECT LOWER(`inventoryIcon1`) AS x FROM dbc_itemdisplayinfo WHERE `inventoryIcon1` <> "") UNION + (SELECT LOWER(SUBSTRING_INDEX(`iconString`, "\\", -1)) AS x FROM dbc_creaturefamily WHERE `iconString` LIKE "%icons%") ) y GROUP BY x' ); + // invent class icons + foreach (ChrClass::cases() as $cl) + DB::Aowow()->qry('INSERT INTO ::icons (`name`, `name_source`) VALUES (%s, %s)', 'class_'.$cl->json(), 'class_'.$cl->json()); + + // invent race icons + foreach (ChrRace::cases() as $ra) + { + if ($na = $ra->json()) // unused races have no json + { + DB::Aowow()->qry('INSERT INTO ::icons (`name`, `name_source`) VALUES (%s, %s)', 'race_'.$na.'_male', 'race_'.$na.'_male'); + DB::Aowow()->qry('INSERT INTO ::icons (`name`, `name_source`) VALUES (%s, %s)', 'race_'.$na.'_female', 'race_'.$na.'_female'); + } + } + + // halucinate holidays + foreach (self::HOLIDAY_ICONS as $h) + DB::Aowow()->qry('INSERT INTO ::icons (`name`, `name_source`) VALUES (%s, %s)', $h, $h); + $this->reapplyCCFlags('icons', Type::ICON); return true; diff --git a/setup/tools/sqlgen/itemenchantment.ss.php b/setup/tools/sqlgen/itemenchantment.ss.php index 6dfa1505..ac118862 100644 --- a/setup/tools/sqlgen/itemenchantment.ss.php +++ b/setup/tools/sqlgen/itemenchantment.ss.php @@ -18,22 +18,22 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['spellitemenchantment']; protected $worldDependency = ['spell_enchant_proc_data']; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_itemenchantment'); - DB::Aowow()->query( - 'INSERT INTO ?_itemenchantment + DB::Aowow()->qry('TRUNCATE ::itemenchantment'); + DB::Aowow()->qry( + 'INSERT INTO ::itemenchantment SELECT `Id`, `charges`, 0, 0, 0, `type1`, `type2`, `type3`, `amount1`, `amount2`, `amount3`, `object1`, `object2`, `object3`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `conditionId`, `skillLine`, `skillLevel`, `requiredLevel` FROM dbc_spellitemenchantment' ); - $cuProcs = DB::World()->select('SELECT `EnchantID` AS ARRAY_KEY, `Chance` AS `procChance`, `ProcsPerMinute` AS `ppmRate` FROM spell_enchant_proc_data'); + $cuProcs = DB::World()->selectAssoc('SELECT `EnchantID` AS ARRAY_KEY, `Chance` AS `procChance`, `ProcsPerMinute` AS `ppmRate` FROM spell_enchant_proc_data'); foreach ($cuProcs as $id => $vals) - DB::Aowow()->query('UPDATE ?_itemenchantment SET ?a WHERE `id` = ?d', $vals, $id); + DB::Aowow()->qry('UPDATE ::itemenchantment SET %a WHERE `id` = %i', $vals, $id); // hide strange stuff - DB::Aowow()->query('UPDATE ?_itemenchantment SET `cuFlags` = ?d WHERE `type1` = 0 AND `type2` = 0 AND `type3` = 0', CUSTOM_EXCLUDE_FOR_LISTVIEW); - DB::Aowow()->query('UPDATE ?_itemenchantment SET `cuFlags` = ?d WHERE `name_loc0` LIKE "%test%"', CUSTOM_EXCLUDE_FOR_LISTVIEW); + DB::Aowow()->qry('UPDATE ::itemenchantment SET `cuFlags` = %i WHERE `type1` = 0 AND `type2` = 0 AND `type3` = 0', CUSTOM_EXCLUDE_FOR_LISTVIEW); + DB::Aowow()->qry('UPDATE ::itemenchantment SET `cuFlags` = %i WHERE `name_loc0` LIKE "%test%"', CUSTOM_EXCLUDE_FOR_LISTVIEW); $this->reapplyCCFlags('itemenchantment', Type::ENCHANTMENT); diff --git a/setup/tools/sqlgen/itemrandomenchant.ss.php b/setup/tools/sqlgen/itemrandomenchant.ss.php index 2f72ba67..a6c7cde3 100644 --- a/setup/tools/sqlgen/itemrandomenchant.ss.php +++ b/setup/tools/sqlgen/itemrandomenchant.ss.php @@ -17,11 +17,11 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['itemrandomsuffix', 'itemrandomproperties']; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_itemrandomenchant'); - DB::Aowow()->query( - 'INSERT INTO ?_itemrandomenchant + DB::Aowow()->qry('TRUNCATE ::itemrandomenchant'); + DB::Aowow()->qry( + 'INSERT INTO ::itemrandomenchant SELECT -id, name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8, nameINT, enchantId1, enchantId2, enchantId3, enchantId4, enchantId5, allocationPct1, allocationPct2, allocationPct3, allocationPct4, allocationPct5 FROM dbc_itemrandomsuffix UNION SELECT id, name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8, nameINT, enchantId1, enchantId2, enchantId3, enchantId4, enchantId5, 0, 0, 0, 0, 0 FROM dbc_itemrandomproperties' ); diff --git a/setup/tools/sqlgen/items.ss.php b/setup/tools/sqlgen/items.ss.php index 640a65bd..fecd0ee5 100644 --- a/setup/tools/sqlgen/items.ss.php +++ b/setup/tools/sqlgen/items.ss.php @@ -19,9 +19,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['gemproperties', 'itemdisplayinfo', 'spell', 'glyphproperties', 'durabilityquality', 'durabilitycosts']; protected $worldDependency = ['item_template', 'item_template_locale', 'spell_group', 'game_event']; - protected $setupAfter = [['icons'], []]; + protected $setupAfter = [['icons', 'spell', 'spellvariables', 'spellrange'], []]; - private $skill2cat = array( + private const /* array */ SKILL_CATG = array( SKILL_INSCRIPTION => 11, SKILL_FISHING => 9, SKILL_MINING => 12, @@ -29,7 +29,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript SKILL_ALCHEMY => 6 ); - public function generate(array $ids = []) : bool + public function generate() : bool { $baseQuery = 'SELECT it.entry, @@ -46,7 +46,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript BuyCount, BuyPrice, SellPrice, 0 AS repairPrice, InventoryType AS slot, InventoryType AS slotBak, - AllowableClass, AllowableRace, + IF((`AllowableClass` & 1535) = 1535, 0, `AllowableClass` & 1535) AS "requiredClass", + IF((`AllowableRace` & 1791) = 1791, 0, `AllowableRace` & 1791) AS "requiredRace", ItemLevel, RequiredLevel, RequiredSkill, RequiredSkillRank, @@ -126,35 +127,37 @@ CLISetup::registerSetup("sql", new class extends SetupScript LEFT JOIN item_template_locale itl8 ON it.entry = itl8.ID AND itl8.locale = "ruRU" LEFT JOIN spell_group sg ON sg.spell_id = it.spellid_1 AND it.class = 0 AND it.subclass = 2 AND sg.id IN (1, 2) LEFT JOIN game_event ge ON ge.holiday = it.HolidayId AND it.HolidayId > 0 - { WHERE it.entry IN (?a) } - LIMIT ?d, ?d'; + LIMIT %i, %i'; + + DB::Aowow()->qry('TRUNCATE ::items'); $i = 0; - DB::Aowow()->query('TRUNCATE ?_items'); - while ($items = DB::World()->select($baseQuery, $ids ?: DBSIMPLE_SKIP, CLISetup::SQL_BATCH * $i, CLISetup::SQL_BATCH)) + while ($items = DB::World()->selectAssoc($baseQuery, CLISetup::SQL_BATCH * $i, CLISetup::SQL_BATCH)) { - CLI::write(' * batch #' . ++$i . ' (' . count($items) . ')', CLI::LOG_BLANK, true, true); + CLI::write(' * batch #' . ++$i . ' (' . count($items) . ')', tmpRow: true); foreach ($items as $item) - DB::Aowow()->query('INSERT INTO ?_items VALUES (?a)', array_values($item)); + DB::Aowow()->qry('INSERT INTO ::items VALUES %l', $item); } + CLI::write('[items] - applying misc data & fixes'); + // merge with gemProperties - DB::Aowow()->query('UPDATE ?_items i, dbc_gemproperties gp SET i.gemEnchantmentId = gp.enchantmentId, i.gemColorMask = gp.colorMask WHERE i.gemColorMask = gp.id'); + DB::Aowow()->qry('UPDATE ::items i, dbc_gemproperties gp SET i.gemEnchantmentId = gp.enchantmentId, i.gemColorMask = gp.colorMask WHERE i.gemColorMask = gp.id'); // get modelString - DB::Aowow()->query('UPDATE ?_items i, dbc_itemdisplayinfo idi SET i.model = IF(idi.leftModelName = "", idi.rightModelName, idi.leftModelName) WHERE i.displayId = idi.id'); + DB::Aowow()->qry('UPDATE ::items i, dbc_itemdisplayinfo idi SET i.model = IF(idi.leftModelName = "", idi.rightModelName, idi.leftModelName) WHERE i.displayId = idi.id'); // get iconId - DB::Aowow()->query('UPDATE ?_items i, dbc_itemdisplayinfo idi, ?_icons ic SET i.iconId = ic.id WHERE i.displayId = idi.id AND LOWER(idi.inventoryIcon1) = ic.name'); + DB::Aowow()->qry('UPDATE ::items i, dbc_itemdisplayinfo idi, ::icons ic SET i.iconId = ic.id WHERE i.displayId = idi.id AND LOWER(idi.inventoryIcon1) = ic.name_source'); // unify slots: Robes => Chest; Ranged (right) => Ranged - DB::Aowow()->query('UPDATE ?_items SET slot = ?d WHERE slotbak = ?d', INVTYPE_RANGED, INVTYPE_RANGEDRIGHT); - DB::Aowow()->query('UPDATE ?_items SET slot = ?d WHERE slotbak = ?d', INVTYPE_CHEST, INVTYPE_ROBE); + DB::Aowow()->qry('UPDATE ::items SET slot = %i WHERE slotbak = %i', INVTYPE_RANGED, INVTYPE_RANGEDRIGHT); + DB::Aowow()->qry('UPDATE ::items SET slot = %i WHERE slotbak = %i', INVTYPE_CHEST, INVTYPE_ROBE); // custom sub-classes - DB::Aowow()->query( - 'UPDATE ?_items SET subclass = IF( + DB::Aowow()->qry( + 'UPDATE ::items SET subclass = IF( slotbak = 4, -8, IF( -- shirt slotbak = 19, -7, IF( -- tabard slotbak = 16, -6, IF( -- cloak @@ -166,49 +169,49 @@ CLISetup::registerSetup("sql", new class extends SetupScript ); // move alchemist stones to trinkets (Armor) - DB::Aowow()->query('UPDATE ?_items SET class = 4, subClass = -4 WHERE classBak = 7 AND subClassBak = 11 AND slotBak = ?d', INVTYPE_TRINKET); + DB::Aowow()->qry('UPDATE ::items SET class = 4, subClass = -4 WHERE classBak = 7 AND subClassBak = 11 AND slotBak = %i', INVTYPE_TRINKET); // mark keys as key (if not quest items) - DB::Aowow()->query('UPDATE ?_items SET class = 13, subClass = 0 WHERE classBak IN (0, 15) AND bagFamily & 0x100'); + DB::Aowow()->qry('UPDATE ::items SET class = 13, subClass = 0 WHERE classBak IN (0, 15) AND bagFamily & 0x100'); // set subSubClass for Glyphs (major/minor) - DB::Aowow()->query('UPDATE ?_items i, dbc_spell s, dbc_glyphproperties gp SET i.subSubClass = IF(gp.typeFlags & 0x1, 2, 1) WHERE i.spellId1 = s.id AND s.effect1MiscValue = gp.id AND i.classBak = 16'); + DB::Aowow()->qry('UPDATE ::items i, dbc_spell s, dbc_glyphproperties gp SET i.subSubClass = IF(gp.typeFlags & 0x1, 2, 1) WHERE i.spellId1 = s.id AND s.effect1MiscValue = gp.id AND i.classBak = 16'); // filter misc(class:15) junk(subclass:0) to appropriate categories // assign pets and mounts to category - DB::Aowow()->query('UPDATE ?_items i, dbc_spell s SET subClass = IF(effect1AuraId <> 78, 2, IF(effect2AuraId = 207 OR effect3AuraId = 207 OR (s.id <> 65917 AND effect2AuraId = 4 AND effect3Id = 77), -7, 5)) WHERE s.id = spellId2 AND class = 15 AND spellId1 IN (?a)', LEARN_SPELLS); + DB::Aowow()->qry('UPDATE ::items i, dbc_spell s SET subClass = IF(effect1AuraId <> 78, 2, IF(effect2AuraId = 207 OR effect3AuraId = 207 OR (s.id <> 65917 AND effect2AuraId = 4 AND effect3Id = 77), -7, 5)) WHERE s.id = spellId2 AND class = 15 AND spellId1 IN %in', LEARN_SPELLS); // more corner cases (mounts that are not actualy learned) - DB::Aowow()->query('UPDATE ?_items i, dbc_spell s SET i.subClass = -7 WHERE (effect1Id = 64 OR (effect1AuraId = 78 AND effect2AuraId = 4 AND effect3Id = 77) OR effect1AuraId = 207 OR effect2AuraId = 207 OR effect3AuraId = 207) AND s.id = i.spellId1 AND i.class = 15 AND i.subClass = 5'); - DB::Aowow()->query('UPDATE ?_items i, dbc_spell s SET i.subClass = 5 WHERE s.effect1AuraId = 78 AND s.id = i.spellId1 AND i.class = 15 AND i.subClass = 0'); + DB::Aowow()->qry('UPDATE ::items i, dbc_spell s SET i.subClass = -7 WHERE (effect1Id = 64 OR (effect1AuraId = 78 AND effect2AuraId = 4 AND effect3Id = 77) OR effect1AuraId = 207 OR effect2AuraId = 207 OR effect3AuraId = 207) AND s.id = i.spellId1 AND i.class = 15 AND i.subClass = 5'); + DB::Aowow()->qry('UPDATE ::items i, dbc_spell s SET i.subClass = 5 WHERE s.effect1AuraId = 78 AND s.id = i.spellId1 AND i.class = 15 AND i.subClass = 0'); // move some permanent enchantments to own category - DB::Aowow()->query('UPDATE ?_items i, dbc_spell s SET i.class = 0, i.subClass = 6 WHERE s.effect1Id = 53 AND s.id = i.spellId1 AND i.class = 15'); + DB::Aowow()->qry('UPDATE ::items i, dbc_spell s SET i.class = 0, i.subClass = 6 WHERE s.effect1Id = 53 AND s.id = i.spellId1 AND i.class = 15'); // move temporary enchantments to own category - DB::Aowow()->query('UPDATE ?_items i, dbc_spell s SET i.subClass = -3 WHERE s.effect1Id = 54 AND s.id = i.spellId1 AND i.class = 0 AND i.subClassBak = 8'); + DB::Aowow()->qry('UPDATE ::items i, dbc_spell s SET i.subClass = -3 WHERE s.effect1Id = 54 AND s.id = i.spellId1 AND i.class = 0 AND i.subClassBak = 8'); // move armor tokens to own category - DB::Aowow()->query('UPDATE ?_items SET subClass = -2 WHERE quality = 4 AND class = 15 AND subClassBak = 0 AND requiredClass AND (requiredClass & 0x5FF) <> 0x5FF'); + DB::Aowow()->qry('UPDATE ::items SET subClass = -2 WHERE quality = 4 AND class = 15 AND subClassBak = 0 AND requiredClass > 0'); // move some junk to holiday if it requires one - DB::Aowow()->query('UPDATE ?_items SET subClass = 3 WHERE classBak = 15 AND subClassBak = 0 AND eventId <> 0'); + DB::Aowow()->qry('UPDATE ::items SET subClass = 3 WHERE classBak = 15 AND subClassBak = 0 AND eventId <> 0'); // move misc items that start quests to class: quest (except Sayges scrolls for consistency) - DB::Aowow()->query('UPDATE ?_items SET class = 12 WHERE classBak = 15 AND startQuest <> 0 AND name_loc0 NOT LIKE "sayge\'s fortune%"'); + DB::Aowow()->qry('UPDATE ::items SET class = 12 WHERE classBak = 15 AND startQuest <> 0 AND name_loc0 NOT LIKE "sayge\'s fortune%"'); // move perm. enchantments into appropriate cat/subcat - DB::Aowow()->query('UPDATE ?_items i, dbc_spell s SET i.class = 0, i.subClass = 6 WHERE s.id = i.spellId1 AND s.effect1Id = 53 AND i.classBak = 12'); + DB::Aowow()->qry('UPDATE ::items i, dbc_spell s SET i.class = 0, i.subClass = 6 WHERE s.id = i.spellId1 AND s.effect1Id = 53 AND i.classBak = 12'); // move some generic recipes into appropriate sub-categories - foreach ($this->skill2cat as $skill => $cat) - DB::Aowow()->query('UPDATE ?_items SET subClass = ?d WHERE classBak = 9 AND subClassBak = 0 AND requiredSkill = ?d', $cat, $skill); + foreach (self::SKILL_CATG as $skill => $cat) + DB::Aowow()->qry('UPDATE ::items SET subClass = %i WHERE classBak = 9 AND subClassBak = 0 AND requiredSkill = %i', $cat, $skill); // assign slot from onUse spell to item (todo (med): handle multi slot enchantments (like armor kits)) - DB::Aowow()->query( - 'UPDATE ?_items i - JOIN (SELECT `id`, LOG(2, `equippedItemInventoryTypeMask` & ~?d) AS `mask` + DB::Aowow()->qry( + 'UPDATE ::items i + JOIN (SELECT `id`, LOG(2, `equippedItemInventoryTypeMask` & ~%i) AS `mask` FROM dbc_spell WHERE `equippedItemInventoryTypeMask` > 0 HAVING CAST(`mask` AS UNSIGNED) = CAST(`mask` AS FLOAT)) s @@ -219,8 +222,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript ); // calculate durabilityCosts - DB::Aowow()->query( - 'UPDATE ?_items i + DB::Aowow()->qry( + 'UPDATE ::items i JOIN dbc_durabilityquality dq ON dq.id = ((i.quality + 1) * 2) JOIN dbc_durabilitycosts dc ON dc.id = i.itemLevel SET i.repairPrice = (durability * dq.mod * IF(i.classBak = 2, @@ -238,9 +241,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript ); // hide some nonsense - DB::Aowow()->query( - 'UPDATE ?_items - SET `cuFlags` = `cuFlags` | ?d + DB::Aowow()->qry( + 'UPDATE ::items + SET `cuFlags` = `cuFlags` | %i WHERE `name_loc0` LIKE "Monster - %" OR `name_loc0` LIKE "Creature - %" OR `name_loc0` LIKE "%[PH]%" OR `name_loc0` LIKE "% PH %" OR `name_loc0` LIKE "%(new)%" OR `name_loc0` LIKE "%(old)%" OR @@ -257,7 +260,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript [[INVTYPE_RANGED, INVTYPE_RANGEDRIGHT], [2, 3, 16, 18, 14, 19]] ); foreach ($checks as [$slots, $subclasses]) - DB::Aowow()->query('UPDATE ?_items SET `cuFlags` = `cuFlags` | ?d WHERE `class`= ?d AND `slotBak` IN (?a) AND `subClass` NOT IN (?a)', CUSTOM_EXCLUDE_FOR_LISTVIEW, ITEM_CLASS_WEAPON, $slots, $subclasses); + DB::Aowow()->qry('UPDATE ::items SET `cuFlags` = `cuFlags` | %i WHERE `class`= %i AND `slotBak` IN %in AND `subClass` NOT IN %in', CUSTOM_EXCLUDE_FOR_LISTVIEW, ITEM_CLASS_WEAPON, $slots, $subclasses); + $this->reapplyCCFlags('items', Type::ITEM); diff --git a/setup/tools/sqlgen/itemset.ss.php b/setup/tools/sqlgen/itemset.ss.php index 32704569..eed06406 100644 --- a/setup/tools/sqlgen/itemset.ss.php +++ b/setup/tools/sqlgen/itemset.ss.php @@ -185,17 +185,26 @@ CLISetup::registerSetup("sql", new class extends SetupScript $data['contentGroup'] = reset($items)['note']; } - public function generate(array $ids = []) : bool + public function generate() : bool { // find events associated with holidayIds - if ($pairs = DB::World()->selectCol('SELECT `holiday` AS ARRAY_KEY, `eventEntry` FROM game_event WHERE `holiday` IN (?a)', array_values($this->setToHoliday))) + if ($pairs = DB::World()->selectCol('SELECT `holiday` AS ARRAY_KEY, `eventEntry` FROM game_event WHERE `holiday` IN %in', array_values($this->setToHoliday))) foreach ($this->setToHoliday as &$hId) $hId = !empty($pairs[$hId]) ? $pairs[$hId] : 0; - DB::Aowow()->query('TRUNCATE TABLE ?_itemset'); + DB::Aowow()->qry('TRUNCATE TABLE ::itemset'); $virtualId = 0; - $sets = DB::Aowow()->select('SELECT *, `id` AS ARRAY_KEY FROM dbc_itemset'); + $sets = DB::Aowow()->selectAssoc('SELECT *, `id` AS ARRAY_KEY FROM dbc_itemset'); + $spells = array_merge( + array_column($sets, 'spellId1'), array_column($sets, 'spellId2'), array_column($sets, 'spellId3'), + array_column($sets, 'spellId4'), array_column($sets, 'spellId5'), array_column($sets, 'spellId6'), + array_column($sets, 'spellId7'), array_column($sets, 'spellId8'), array_column($sets, 'spellId9') + ); + + $bonusSpells = new SpellList(array(['s.id', array_unique($spells)]), ['interactive' => SpellList::INTERACTIVE_NONE]); + + $pieces = DB::World()->selectAssoc('SELECT `itemset` AS ARRAY_KEY, `entry` AS ARRAY_KEY2, `entry`, `name`, `class`, `subclass`, `Quality`, `AllowableClass`, `ItemLevel`, `RequiredLevel`, `itemset`, IF (`Flags` & %i, 1, 0) AS "heroic", IF(`InventoryType` = 15, 26, IF(`InventoryType` = 5, 20, `InventoryType`)) AS "slot" FROM item_template WHERE `itemset` > 0', ITEM_FLAG_HEROIC); foreach ($sets as $setId => $setData) { @@ -210,25 +219,16 @@ CLISetup::registerSetup("sql", new class extends SetupScript if ($setData['reqSkillLevel']) $row['skillLevel'] = $setData['reqSkillLevel']; - - /********************/ - /* calc statbonuses */ - /********************/ - - $spells = []; - + $j = 1; for ($i = 1; $i < 9; $i++) - if ($setData['spellId'.$i] > 0 && $setData['itemCount'.$i] > 0) - $spells[$i] = [$setData['spellId'.$i], $setData['itemCount'.$i]]; + { + if ($setData['spellId'.$i] <= 0 || $setData['itemCount'.$i] <= 0) + continue; - $bonusSpells = new SpellList(array(['s.id', array_column($spells, 0)])); - - $spells = array_pad($spells, 8, [0, 0]); - - foreach (array_column($spells, 0) as $idx => $spellId) - $row['spell'.($idx+1)] = $spellId; - foreach (array_column($spells, 1) as $idx => $nItems) - $row['bonus'.($idx+1)] = $nItems; + $row['spell'.$j] = $setData['spellId'.$i]; + $row['bonus'.$j] = $setData['itemCount'.$i]; + $j++; + } /**************************/ @@ -243,16 +243,18 @@ CLISetup::registerSetup("sql", new class extends SetupScript $row['name_loc'.$locId] = Util::localizedString($setData, 'name'); - foreach ($bonusSpells->iterate() as $__) + for ($i = 1; $i < 9; $i++) { + if (!$setData['spellId'.$i] || !$bonusSpells->getEntry($setData['spellId'.$i])) + continue; + if (!isset($descText[$locId])) $descText[$locId] = ''; $descText[$locId] .= $bonusSpells->parseText()[0]."\n"; } - // strip rating blocks - e.g. <!--rtg19-->14 <small>(<!--rtg%19-->0.30% @ L<!--lvl-->80)</small> - $row['bonusText_loc'.$locId] = preg_replace('/<!--rtg\d+-->(\d+) .*?<\/small>/i', '\1', $descText[$locId]); + $row['bonusText_loc'.$locId] = $descText[$locId]; } @@ -260,8 +262,6 @@ CLISetup::registerSetup("sql", new class extends SetupScript /* determine type and reuse from pieces */ /****************************************/ - $pieces = DB::World()->select('SELECT `entry`, `name`, `class`, `subclass`, `Quality`, `AllowableClass`, `ItemLevel`, `RequiredLevel`, `itemset`, IF (`Flags` & ?d, 1, 0) AS "heroic", IF(`InventoryType` = 15, 26, IF(`InventoryType` = 5, 20, `InventoryType`)) AS "slot", `entry` AS ARRAY_KEY FROM item_template WHERE `itemset` = ?d', ITEM_FLAG_HEROIC, $setId); - /* possible cases: 1) unused entry (empty data) @@ -269,17 +269,17 @@ CLISetup::registerSetup("sql", new class extends SetupScript 3) one itemset from one dbc entry (basic case). duplicate items per slot possible */ - if (count($pieces) < 2) + if (count($pieces[$setId] ?? []) < 2) { $row['cuFlags'] = CUSTOM_EXCLUDE_FOR_LISTVIEW; - DB::Aowow()->query('INSERT INTO ?_itemset (?#) VALUES (?a)', array_keys($row), array_values($row)); + DB::Aowow()->qry('INSERT INTO ::itemset %v', $row); CLI::write('[item set] '.str_pad('['.$setId.']', 7).CLI::bold($setData['name_loc0']).' has no associated items', CLI::LOG_INFO); continue; } $sorted = []; // sort available items by slot, sort slot by itemID ASC - foreach ($pieces as $data) + foreach ($pieces[$setId] as $data) { $data['note'] = $this->getContentGroup($data); if (in_array($data['note'], [23, 25, 27, 29, 17, 19, 20, 22, 24, 26, 28, 30])) @@ -294,7 +294,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript else if (in_array($data['slot'], [INVTYPE_WEAPON, INVTYPE_FINGER, INVTYPE_TRINKET])) $sorted[$k][-$data['slot']] = $data; // slot confict. If item is being sold, replace old item (imperfect solution :/) - else if (DB::World()->selectCell('SELECT SUM(`n`) FROM (SELECT COUNT(1) AS "n" FROM npc_vendor WHERE `item` = ?d UNION SELECT COUNT(1) AS "n" FROM game_event_npc_vendor WHERE `item` = ?d) x', $data['entry'], $data['entry'])) + else if (DB::World()->selectCell('SELECT SUM(`n`) FROM (SELECT COUNT(1) AS "n" FROM npc_vendor WHERE `item` = %i UNION SELECT COUNT(1) AS "n" FROM game_event_npc_vendor WHERE `item` = %i) x', $data['entry'], $data['entry'])) $sorted[$k][$data['slot']] = $data; } @@ -311,7 +311,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript if ($i++) $vRow['id'] = --$virtualId; - DB::Aowow()->query('INSERT INTO ?_itemset (?#) VALUES (?a)', array_keys($vRow), array_values($vRow)); + DB::Aowow()->qry('INSERT INTO ::itemset %v', $vRow); } } diff --git a/setup/tools/sqlgen/itemstats.ss.php b/setup/tools/sqlgen/itemstats.ss.php index 7bec32ca..e1619581 100644 --- a/setup/tools/sqlgen/itemstats.ss.php +++ b/setup/tools/sqlgen/itemstats.ss.php @@ -11,41 +11,41 @@ if (!CLI) class ItemStatSetup extends ItemList { - private $relSpells = []; - private $relEnchants = []; - - public function __construct($start, $limit, array $ids, array $relEnchants, array $relSpells) + public function __construct(int $start, int $limit, int $itemClass, private bool $applyTriggered, private array $relEnchants, private array $relSpells) { $this->queryOpts['i']['o'] = 'i.id ASC'; unset($this->queryOpts['is']); // do not reference the stats table we are going to write to $conditions = array( ['i.id', $start, '>'], - ['class', [ITEM_CLASS_WEAPON, ITEM_CLASS_GEM, ITEM_CLASS_ARMOR, ITEM_CLASS_CONSUMABLE, ITEM_CLASS_AMMUNITION]], + ['class', $itemClass], $limit ); - if ($ids) - $conditions[] = ['id', $ids]; - parent::__construct($conditions); - - $this->relSpells = $relSpells; - $this->relEnchants = $relEnchants; } public function writeStatsTable() : void { foreach ($this->iterate() as $id => $curTpl) { - $spellIds = []; + $spellIds = []; for ($i = 1; $i <= 5; $i++) if ($this->curTpl['spellId'.$i] > 0 && !isset($this->relSpells[$this->curTpl['spellId'.$i]]) && (($this->curTpl['class'] == ITEM_CLASS_CONSUMABLE && $this->curTpl['spellTrigger'.$i] == SPELL_TRIGGER_USE) || $this->curTpl['spellTrigger'.$i] == SPELL_TRIGGER_EQUIP)) $spellIds[] = $this->curTpl['spellId'.$i]; if ($spellIds) // array_merge kills the keys - $this->relSpells = array_replace($this->relSpells, DB::Aowow()->select('SELECT *, id AS ARRAY_KEY FROM ?_spell WHERE id IN (?a)', $spellIds)); + { + $newSpells = DB::Aowow()->selectAssoc('SELECT *, id AS ARRAY_KEY FROM ::spell WHERE id IN %in', $spellIds); + $this->relSpells = array_replace($this->relSpells, $newSpells); + + // include triggered spell to calculate nutritional values + if ($this->applyTriggered) + if ($t = array_filter(array_merge(array_column($newSpells, 'effect1TriggerSpell'), array_column($newSpells, 'effect2TriggerSpell'), array_column($newSpells, 'effect3TriggerSpell')))) + if ($t = array_diff($t, array_keys($this->relSpells))) + $this->relSpells = array_replace($this->relSpells, DB::Aowow()->selectAssoc('SELECT *, id AS ARRAY_KEY FROM ::spell WHERE id IN %in', $t)); + } // fromItem: itemMods, spell, enchants from template - fromJson: calculated stats (feralAP, dps, ...) if ($stats = (new StatsContainer($this->relSpells, $this->relEnchants))->fromItem($curTpl)->fromJson($this->json[$id])->toJson(Stat::FLAG_ITEM | Stat::FLAG_SERVERSIDE)) @@ -55,12 +55,9 @@ class ItemStatSetup extends ItemList if ($this->getField('class') == ITEM_CLASS_WEAPON) $stats += $shared + ($this->isRangedWeapon() ? ['rgddps' => 0, 'rgddmgmin' => 0, 'rgddmgmax' => 0, 'rgdspeed' => 0] : ['mledps' => 0, 'mledmgmin' => 0, 'mledmgmax' => 0, 'mlespeed' => 0]); else if ($this->getField('class') == ITEM_CLASS_ARMOR) - $stats += ['armorbonus' => 0]; //ArmorDamageModifier only valid on armor(?) + $stats += ['armorbonus' => 0]; //ArmorDamageModifier only valid on armor(%s) - // apply PK - $stats += ['type' => Type::ITEM, 'typeId' => $this->id]; - - DB::Aowow()->query('INSERT INTO ?_item_stats (?#) VALUES (?a)', array_keys($stats), array_values($stats)); + DB::Aowow()->qry('INSERT INTO ::item_stats %v', ['type' => Type::ITEM, 'typeId' => $this->id] + $stats); } } } @@ -75,13 +72,13 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['spellitemenchantment']; protected $setupAfter = [['items', 'spell'], []]; - private $relSpells = []; + private array $relSpells = []; private function enchantment_stats(?int &$total = 0, ?int &$effective = 0) : array { - $enchants = DB::Aowow()->select('SELECT *, `id` AS ARRAY_KEY FROM dbc_spellitemenchantment'); + $enchants = DB::Aowow()->selectAssoc('SELECT *, `id` AS ARRAY_KEY FROM dbc_spellitemenchantment'); $spells = []; - $result = []; + $stats = []; $effective = 0; $total = count($enchants); @@ -91,21 +88,21 @@ CLISetup::registerSetup("sql", new class extends SetupScript $spells[] = $e['object'.$i]; if ($spells) - $this->relSpells = DB::Aowow()->select('SELECT *, id AS ARRAY_KEY FROM ?_spell WHERE id IN (?a)', $spells); + $this->relSpells = DB::Aowow()->selectAssoc('SELECT *, id AS ARRAY_KEY FROM ::spell WHERE id IN %in', $spells); foreach ($enchants as $eId => $e) - if ($result[$eId] = (new StatsContainer($this->relSpells))->fromEnchantment($e)->toJson(Stat::FLAG_ITEM | Stat::FLAG_SERVERSIDE)) + if ($stats = (new StatsContainer($this->relSpells))->fromEnchantment($e)->toJson(Stat::FLAG_ITEM | Stat::FLAG_SERVERSIDE)) { - DB::Aowow()->query('INSERT INTO ?_item_stats (?#) VALUES (?a)', array_merge(['type', 'typeId'], array_keys($result[$eId])), array_merge([Type::ENCHANTMENT, $eId], array_values($result[$eId]))); + DB::Aowow()->qry('INSERT INTO ::item_stats %v', ['type' => Type::ENCHANTMENT, 'typeId' => $eId] + $stats); $effective++; } return $enchants; } - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_item_stats'); + DB::Aowow()->qry('TRUNCATE ::item_stats'); CLI::write('[stats] - applying stats for enchantments'); @@ -114,19 +111,29 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[stats] - applying stats for items'); - $i = 0; - $offset = 0; - while (true) + $classes = array( + ITEM_CLASS_WEAPON => [false, 'weapons'], + ITEM_CLASS_ARMOR => [false, 'armor'], + ITEM_CLASS_GEM => [false, 'gems'], + ITEM_CLASS_CONSUMABLE => [true, 'consumables'], + ITEM_CLASS_AMMUNITION => [false, 'ammunition'] + ); + foreach ($classes as $itemClass => [$applyTriggered, $name]) { - $items = new ItemStatSetup($offset, CLISetup::SQL_BATCH, $ids, $enchStats, $this->relSpells); - if ($items->error) - break; + $i = 0; + $offset = 0; + while (true) + { + $items = new ItemStatSetup($offset, CLISetup::SQL_BATCH, $itemClass, $applyTriggered, $enchStats, $this->relSpells); + if ($items->error) + break; - CLI::write('[stats] * batch #' . ++$i . ' (' . count($items->getFoundIDs()) . ')', CLI::LOG_BLANK, true, true); + CLI::write('[stats] * '.$name.' batch #' . ++$i . ' (' . count($items->getFoundIDs()) . ')', CLI::LOG_BLANK, true, true); - $offset = max($items->getFoundIDs()); + $offset = max($items->getFoundIDs()); - $items->writeStatsTable(); + $items->writeStatsTable(); + } } return true; diff --git a/setup/tools/sqlgen/mailtemplate.ss.php b/setup/tools/sqlgen/mailtemplate.ss.php index d2e17d1e..f3cdb5c5 100644 --- a/setup/tools/sqlgen/mailtemplate.ss.php +++ b/setup/tools/sqlgen/mailtemplate.ss.php @@ -18,16 +18,16 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['mailtemplate']; protected $worldDependency = ['achievement_reward', 'achievement_reward_locale', 'mail_loot_template']; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_mails'); + DB::Aowow()->qry('TRUNCATE ::mails'); // copy data over from dbc - DB::Aowow()->query('INSERT INTO ?_mails SELECT `id`, 0, `subject_loc0`, `subject_loc2`, `subject_loc3`, `subject_loc4`, `subject_loc6`, `subject_loc8`, `text_loc0`, `text_loc2`, `text_loc3`, `text_loc4`, `text_loc6`, `text_loc8`, 0 FROM dbc_mailtemplate'); + DB::Aowow()->qry('INSERT INTO ::mails SELECT `id`, 0, `subject_loc0`, `subject_loc2`, `subject_loc3`, `subject_loc4`, `subject_loc6`, `subject_loc8`, `text_loc0`, `text_loc2`, `text_loc3`, `text_loc4`, `text_loc6`, `text_loc8`, 0 FROM dbc_mailtemplate'); CLI::write('[mails] - loading data from achievement_reward'); - $acvMail = DB::World()->select( + $acvMail = DB::World()->selectAssoc( 'SELECT -ar.`ID`, 0, IFNULL(ar.`Subject`, "") AS "s0", IFNULL(arl2.`Subject`, "") AS "s2", IFNULL(arl3.`Subject`, "") AS "s3", IFNULL(arl4.`Subject`, "") AS "s4", IFNULL(arl6.`Subject`, "") AS "s6", IFNULL(arl8.`Subject`, "") AS "s8", IFNULL(ar.`Body`, "") AS "t0", IFNULL(arl2.`Body`, "") AS "t2", IFNULL(arl3.`Body`, "") AS "t3", IFNULL(arl4.`Body`, "") AS "t4", IFNULL(arl6.`Body`, "") AS "t6", IFNULL(arl8.`Body`, "") AS "t8", @@ -41,14 +41,15 @@ CLISetup::registerSetup("sql", new class extends SetupScript WHERE ar.`MailTemplateID` = 0 AND ar.`Body` <> ""' ); - DB::Aowow()->query('INSERT INTO ?_mails VALUES (?a)', array_values($acvMail)); + foreach ($acvMail as $am) + DB::Aowow()->qry('INSERT INTO ::mails VALUES %l', $am); CLI::write('[mails] - loading data from mail_loot_template'); // assume mails to only contain one single item, wich works for an unmodded installation $mlt = DB::World()->selectCol('SELECT `Entry` AS ARRAY_KEY, `Item` FROM mail_loot_template'); foreach ($mlt as $k => $v) - DB::Aowow()->query('UPDATE ?_mails SET `attachment` = ?d WHERE `id` = ?d', $v, $k); + DB::Aowow()->qry('UPDATE ::mails SET `attachment` = %i WHERE `id` = %i', $v, $k); return true; } diff --git a/setup/tools/sqlgen/objects.ss.php b/setup/tools/sqlgen/objects.ss.php index b785dd0a..069df674 100644 --- a/setup/tools/sqlgen/objects.ss.php +++ b/setup/tools/sqlgen/objects.ss.php @@ -18,7 +18,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['lock']; protected $worldDependency = ['gameobject_template', 'gameobject_template_addon', 'gameobject_template_locale', 'gameobject_questitem']; - public function generate(array $ids = []) : bool + public function generate() : bool { $baseQuery = 'SELECT go.entry, @@ -53,7 +53,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript IF(`type` = 3, CONCAT_WS(" ", data4, data5, data2), -- miscInfo: loot v IF(`type` = 25, CONCAT_WS(" ", data2, data3, 0), IF(`type` = 23, CONCAT_WS(" ", data0, data1, data2), "")))), -- miscInfo: meetingStone - IF(ScriptName <> "", ScriptName, AIName) + NULLIF(IF(ScriptName <> "", ScriptName, AIName), ""), + StringId FROM gameobject_template go LEFT JOIN gameobject_template_addon goa ON go.entry = goa.entry LEFT JOIN gameobject_template_locale gtl2 ON go.entry = gtl2.entry AND gtl2.`locale` = "frFR" @@ -62,31 +63,29 @@ CLISetup::registerSetup("sql", new class extends SetupScript LEFT JOIN gameobject_template_locale gtl6 ON go.entry = gtl6.entry AND gtl6.`locale` = "esES" LEFT JOIN gameobject_template_locale gtl8 ON go.entry = gtl8.entry AND gtl8.`locale` = "ruRU" LEFT JOIN gameobject_questitem gqi ON gqi.GameObjectEntry = go.entry - { WHERE go.entry IN (?a) } GROUP BY go.entry - LIMIT ?d, ?d'; + LIMIT %i, %i'; + + DB::Aowow()->qry('TRUNCATE ::objects'); $i = 0; - DB::Aowow()->query('TRUNCATE ?_objects'); - while ($objects = DB::World()->select($baseQuery, $ids ?: DBSIMPLE_SKIP, CLISetup::SQL_BATCH * $i, CLISetup::SQL_BATCH)) + while ($objects = DB::World()->selectAssoc($baseQuery, CLISetup::SQL_BATCH * $i, CLISetup::SQL_BATCH)) { CLI::write(' * batch #' . ++$i . ' (' . count($objects) . ')', CLI::LOG_BLANK, true, true); foreach ($objects as $object) - DB::Aowow()->query('INSERT INTO ?_objects VALUES (?a)', array_values($object)); + DB::Aowow()->qry('INSERT INTO ::objects VALUES %l', $object); } // apply typeCat and reqSkill depending on locks - DB::Aowow()->query( - 'UPDATE ?_objects o + DB::Aowow()->qry( + 'UPDATE ::objects o LEFT JOIN dbc_lock l ON l.id = IF(o.`type` = 3, lockId, null) SET typeCat = IF(`type` = 3 AND (l.properties1 = 1 OR l.properties2 = 1), -5, -- footlocker IF(`type` = 3 AND (l.properties1 = 2), -3, -- herb IF(`type` = 3 AND (l.properties1 = 3), -4, typeCat))), -- ore reqSkill = IF(`type` = 3 AND l.properties1 IN (1, 2, 3), IF(l.reqSkill1 > 1, l.reqSkill1, 1), - IF(`type` = 3 AND l.properties2 = 1, IF(l.reqSkill2 > 1, l.reqSkill2, 1), 0)) - { WHERE o.id IN (?a) }', - $ids ?: DBSIMPLE_SKIP + IF(`type` = 3 AND l.properties2 = 1, IF(l.reqSkill2 > 1, l.reqSkill2, 1), 0))', ); $this->reapplyCCFlags('objects', Type::OBJECT); diff --git a/setup/tools/sqlgen/pet.ss.php b/setup/tools/sqlgen/pet.ss.php index 20b08a99..d7aa96a0 100644 --- a/setup/tools/sqlgen/pet.ss.php +++ b/setup/tools/sqlgen/pet.ss.php @@ -11,6 +11,8 @@ if (!CLI) CLISetup::registerSetup("sql", new class extends SetupScript { + use TrCustomData; + protected $info = array( 'pet' => [[], CLISetup::ARGV_PARAM, 'Compiles data for type: Pet from dbc and world db.'] ); @@ -19,75 +21,70 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $worldDependency = ['creature_template', 'creature']; protected $setupAfter = [['icons'], []]; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_pet'); + DB::Aowow()->qry('TRUNCATE ::pet'); // basic copy from creaturefamily.dbc - DB::Aowow()->query( - 'INSERT INTO ?_pet - SELECT f.id, - categoryEnumId, - 0, -- cuFlags - 0, -- minLevel - 0, -- maxLevel - petFoodMask, - petTalentType, - 0, -- exotic - 0, -- expansion - name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8, - ic.id, - skillLine1, - 0, 0, 0, 0, -- spell[1-4] - 0, 0, 0 -- armor, damage, health - FROM dbc_creaturefamily f - LEFT JOIN ?_icons ic ON ic.name = LOWER(SUBSTRING_INDEX(f.iconString, "\\\\", -1)) - WHERE petTalentType <> -1' + DB::Aowow()->qry( + 'INSERT INTO ::pet + SELECT f.`id`, + `categoryEnumId`, + 0, -- cuFlags + 0, -- minLevel + 0, -- maxLevel + `petFoodMask`, + `petTalentType`, + 0, -- exotic + 0, -- expansion + `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, + ic.`id`, + `skillLine1`, + 0, 0, 0, 0, -- spell[1-4] + 0, 0, 0 -- armor, damage, health + FROM dbc_creaturefamily f + LEFT JOIN ::icons ic ON ic.`name_source` = LOWER(SUBSTRING_INDEX(f.`iconString`, "\\", -1)) + WHERE `petTalentType` <> -1' ); // stats from craeture_template - $spawnInfo = DB::World()->query( - 'SELECT ct.family AS ARRAY_KEY, - MIN(ct.minlevel) AS minLevel, - MAX(ct.maxlevel) AS maxLevel, - IF(ct.type_flags & 0x10000, 1, 0) AS exotic + $spawnInfo = DB::World()->selectAssoc( + 'SELECT ct.`family` AS ARRAY_KEY, + MIN(ct.`minlevel`) AS "minLevel", + MAX(ct.`maxlevel`) AS "maxLevel", + IF(ct.`type_flags` & %i, 1, 0) AS "exotic" FROM creature_template ct - JOIN creature c ON ct.entry = c.id - WHERE ct.type_flags & 0x1 - GROUP BY ct.family' + JOIN creature c ON ct.`entry` = c.`id` + WHERE ct.`type_flags` & %i + GROUP BY ct.`family`', + NPC_TYPEFLAG_EXOTIC_PET, NPC_TYPEFLAG_TAMEABLE ); foreach ($spawnInfo as $id => $info) - DB::Aowow()->query('UPDATE ?_pet SET ?a WHERE id = ?d', $info, $id); + DB::Aowow()->qry('UPDATE ::pet SET %a WHERE id = %i', $info, $id); // add petFamilyModifier to health, mana, dmg - DB::Aowow()->query( - 'UPDATE ?_pet p, - dbc_skilllineability sla, - dbc_spell s - SET armor = s.effect2BasePoints + s.effect2DieSides, - damage = s.effect1BasePoints + s.effect1DieSides, - health = s.effect3BasePoints + s.effect3DieSides - WHERE p.skillLineId = sla.skillLineId AND - sla.spellId = s.id AND - s.name_loc0 = "Tamed Pet Passive (DND)"' + DB::Aowow()->qry( + 'UPDATE ::pet p, + dbc_skilllineability sla, + dbc_spell s + SET `armor` = s.`effect2BasePoints` + s.`effect2DieSides`, + `damage` = s.`effect1BasePoints` + s.`effect1DieSides`, + `health` = s.`effect3BasePoints` + s.`effect3DieSides` + WHERE p.`skillLineId` = sla.`skillLineId` AND + sla.`spellId` = s.`id` AND + s.`name_loc0` = "Tamed Pet Passive (DND)"' ); - // add expansion manually - /********************/ - /* TODO: MOVE TO DB */ - /********************/ - DB::Aowow()->query('UPDATE ?_pet SET expansion = 1 WHERE id IN (30, 31, 32, 33, 34)'); - DB::Aowow()->query('UPDATE ?_pet SET expansion = 2 WHERE id IN (37, 38, 39, 41, 42, 43, 44, 45, 46)'); - // assign pet spells - $pets = DB::Aowow()->select( - 'SELECT p.id, MAX(s.id) AS spell - FROM dbc_skilllineability sla - JOIN ?_pet p ON p.skillLineId = sla.skillLineId - JOIN dbc_spell s ON sla.spellId = s.id - LEFT OUTER JOIN dbc_talent t ON s.id = t.rank1 - WHERE (s.attributes0 & 0x40) = 0 AND t.id IS NULL - GROUP BY s.name_loc0, p.id' + $pets = DB::Aowow()->selectAssoc( + 'SELECT p.`id`, MAX(s.`id`) AS "spell" + FROM dbc_skilllineability sla + JOIN ::pet p ON p.`skillLineId` = sla.`skillLineId` + JOIN dbc_spell s ON sla.`spellId` = s.`id` + LEFT OUTER JOIN dbc_talent t ON s.`id` = t.`rank1` + WHERE (s.`attributes0` & %i) = 0 AND t.`id` IS NULL + GROUP BY s.`name_loc0`, p.`id`', + SPELL_ATTR0_PASSIVE ); $petSpells = []; @@ -100,7 +97,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript } foreach ($petSpells as $petId => $row) - DB::Aowow()->query('UPDATE ?_pet SET ?a WHERE id = ?d', $row, $petId); + DB::Aowow()->qry('UPDATE ::pet SET %a WHERE `id` = %i', $row, $petId); $this->reapplyCCFlags('pet', Type::PET); diff --git a/setup/tools/sqlgen/quests.ss.php b/setup/tools/sqlgen/quests.ss.php index d2d8a6da..12558f38 100644 --- a/setup/tools/sqlgen/quests.ss.php +++ b/setup/tools/sqlgen/quests.ss.php @@ -20,7 +20,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['questxp', 'questfactionreward']; protected $worldDependency = ['quest_template', 'quest_template_addon', 'quest_template_locale', 'game_event', 'game_event_seasonal_questrelation', 'quest_request_items', 'quest_request_items_locale', 'quest_offer_reward', 'quest_offer_reward_locale', 'disables']; - public function generate(array $ids = []) : bool + public function generate() : bool { $baseQuery = 'SELECT q.ID, @@ -29,8 +29,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript MinLevel, IFNULL(qa.MaxLevel, 0), QuestSortID, - QuestSortID AS zoneOrSortBak, -- ZoneOrSortBak - QuestInfoID, -- QuestType + QuestSortID AS questSortIdBak, + QuestInfoID, SuggestedGroupNum, TimeAllowed, IFNULL(gesqr.eventEntry, 0) AS eventId, @@ -45,8 +45,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript IF(d.entry IS NULL, 0, 134217728) + -- disabled IF(q.Flags & 16384, 536870912, 0) -- unavailable ) AS cuFlags, - IFNULL(qa.AllowableClasses, 0), - AllowableRaces, + IFNULL(IF((qa.`AllowableClasses` & 1535) = 1535, 0, qa.`AllowableClasses` & 1535), 0) AS "reqClassMask", + IF((`AllowableRaces` & 1791) = 1791, 0, `AllowableRaces` & 1791) AS "reqRaceMask", IFNULL(qa.RequiredSkillId, 0), IFNULL(qa.RequiredSkillPoints, 0), RequiredFactionId1, RequiredFactionId2, RequiredFactionValue1, RequiredFactionValue2, @@ -60,7 +60,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript RewardMoney, RewardBonusMoney, RewardDisplaySpell, RewardSpell, - RewardHonor * 124 * RewardKillHonor, -- alt calculation in QuestDef.cpp -> Quest::CalculateHonorGain(playerLevel) + RewardHonor + (124 * RewardKillHonor), -- 124 = 1HK @ L80 (real value per level in TeamContributionPoints.dbc) IFNULL(qa.RewardMailTemplateId, 0), IFNULL(qa.RewardMailDelay, 0), RewardTitle, RewardTalents, @@ -113,17 +113,17 @@ CLISetup::registerSetup("sql", new class extends SetupScript LEFT JOIN quest_template_addon qa ON q.ID = qa.ID LEFT JOIN game_event_seasonal_questrelation gesqr ON gesqr.questId = q.ID LEFT JOIN disables d ON d.entry = q.ID AND d.sourceType = 1 - { WHERE q.Id IN (?a) } - LIMIT ?d, ?d'; + LIMIT %i, %i'; + + DB::Aowow()->qry('TRUNCATE ::quests'); $i = 0; - DB::Aowow()->query('TRUNCATE ?_quests'); - while ($quests = DB::World()->select($baseQuery, $ids ?: DBSIMPLE_SKIP, CLISetup::SQL_BATCH * $i, CLISetup::SQL_BATCH)) + while ($quests = DB::World()->selectAssoc($baseQuery, CLISetup::SQL_BATCH * $i, CLISetup::SQL_BATCH)) { CLI::write(' * batch #' . ++$i . ' (' . count($quests) . ')', CLI::LOG_BLANK, true, true); foreach ($quests as $quest) - DB::Aowow()->query('INSERT INTO ?_quests VALUES (?a)', array_values($quest)); + DB::Aowow()->qry('INSERT INTO ::quests VALUES %l', $quest); } @@ -139,14 +139,13 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write(' * unpacking XP rewards (1/4)', CLI::LOG_BLANK, true, true); // unpack XP-reward - DB::Aowow()->query( - 'UPDATE ?_quests q, dbc_questxp xp + DB::Aowow()->qry( + 'UPDATE ::quests q, dbc_questxp xp SET `rewardXP` = (CASE `rewardXP` WHEN 0 THEN xp.`Field1` WHEN 1 THEN xp.`Field2` WHEN 2 THEN xp.`Field3` WHEN 3 THEN xp.`Field4` WHEN 4 THEN xp.`Field5` WHEN 5 THEN xp.`Field6` WHEN 6 THEN xp.`Field7` WHEN 7 THEN xp.`Field8` WHEN 8 THEN xp.`Field9` WHEN 9 THEN xp.`Field10` ELSE 0 END) - WHERE xp.`id` = q.`level` { AND q.`id` IN (?a) }', - $ids ?: DBSIMPLE_SKIP + WHERE xp.`id` = q.`level`', ); @@ -154,46 +153,45 @@ CLISetup::registerSetup("sql", new class extends SetupScript // unpack Rep-rewards for ($i = 1; $i < 6; $i++) - DB::Aowow()->query( - 'UPDATE ?_quests q - LEFT JOIN dbc_questfactionreward rep ON rep.`id` = IF(rewardFactionValue?d > 0, 1, 2) - SET rewardFactionValue?d = (CASE ABS(rewardFactionValue?d) + DB::Aowow()->qry( + 'UPDATE ::quests q + LEFT JOIN dbc_questfactionreward rep ON rep.`id` = IF(rewardFactionValue%i > 0, 1, 2) + SET rewardFactionValue%i = (CASE ABS(rewardFactionValue%i) WHEN 0 THEN rep.`Field1` WHEN 1 THEN rep.`Field2` WHEN 2 THEN rep.`Field3` WHEN 3 THEN rep.`Field4` WHEN 4 THEN rep.`Field5` WHEN 5 THEN rep.`Field6` WHEN 6 THEN rep.`Field7` WHEN 7 THEN rep.`Field8` WHEN 8 THEN rep.`Field9` WHEN 9 THEN rep.`Field10` ELSE 0 END) - WHERE ABS(rewardFactionValue?d) BETWEEN 1 AND 10 { AND q.`id` IN (?a) }', + WHERE ABS(rewardFactionValue%i) BETWEEN 1 AND 10', $i, $i, $i, $i, - $ids ?: DBSIMPLE_SKIP ); CLI::write(' * flagging quests series (3/4)', CLI::LOG_BLANK, true, true); // set series flags - DB::Aowow()->query( - 'UPDATE ?_quests q - LEFT JOIN ?_quests q2 ON q2.`NextQuestIdChain` = q.id - SET q.`cuFlags` = q.`cuFlags` | ?d + DB::Aowow()->qry( + 'UPDATE ::quests q + LEFT JOIN ::quests q2 ON q2.`NextQuestIdChain` = q.id + SET q.`cuFlags` = q.`cuFlags` | %i WHERE q.`NextQuestIdChain` > 0 AND q2.`id` IS NULL', QUEST_CU_FIRST_SERIES | QUEST_CU_PART_OF_SERIES ); - DB::Aowow()->query( - 'UPDATE ?_quests q - JOIN ?_quests q2 ON q2.`NextQuestIdChain` = q.id - SET q.`cuFlags` = q.`cuFlags` | ?d + DB::Aowow()->qry( + 'UPDATE ::quests q + JOIN ::quests q2 ON q2.`NextQuestIdChain` = q.id + SET q.`cuFlags` = q.`cuFlags` | %i WHERE q.`NextQuestIdChain` = 0', QUEST_CU_LAST_SERIES | QUEST_CU_PART_OF_SERIES ); - DB::Aowow()->query('UPDATE ?_quests SET `cuFlags` = `cuFlags` | ?d WHERE `NextQuestIdChain` > 0', QUEST_CU_PART_OF_SERIES); + DB::Aowow()->qry('UPDATE ::quests SET `cuFlags` = `cuFlags` | %i WHERE `NextQuestIdChain` > 0', QUEST_CU_PART_OF_SERIES); CLI::write(' * applying fixes (4/4)', CLI::LOG_BLANK, true, true); // fix questSorts for instance quests foreach (Game::$questSortFix as $child => $parent) - DB::Aowow()->query('UPDATE ?_quests SET `zoneOrSort` = ?d WHERE `zoneOrSortBak` = ?d', $parent, $child); + DB::Aowow()->qry('UPDATE ::quests SET `questSortId` = %i WHERE `questSortIdBak` = %i', $parent, $child); // move quests linked to holidays into appropirate quests-sorts. create dummy sorts as needed @@ -207,20 +205,20 @@ CLISetup::registerSetup("sql", new class extends SetupScript foreach ($holidaySorts as $hId => $sort) if (!empty($eventSet[$hId])) - DB::Aowow()->query('UPDATE ?_quests SET `zoneOrSort` = ?d WHERE `eventId` = ?d{ AND `id` IN (?a)}', $sort, $eventSet[$hId], $ids ?: DBSIMPLE_SKIP); + DB::Aowow()->qry('UPDATE ::quests SET `questSortId` = %i WHERE `eventId` = %i', $sort, $eventSet[$hId]); // 'special' special cases // fishing quests to stranglethorn extravaganza - DB::Aowow()->query('UPDATE ?_quests SET `zoneOrSort` = ?d WHERE `id` IN (?a){ AND `id` IN (?a)}', -101, [8228, 8229], $ids ?: DBSIMPLE_SKIP); + DB::Aowow()->qry('UPDATE ::quests SET `questSortId` = %i WHERE `id` IN %in', -101, [8228, 8229]); // dungeon quests to Misc/Dungeon Finder - DB::Aowow()->query('UPDATE ?_quests SET `zoneOrSort` = ?d WHERE (`specialFlags` & ?d OR `id` IN (?a)){ AND `id` IN (?a)}', -1010, QUEST_FLAG_SPECIAL_DUNGEON_FINDER, [24789, 24791, 24923], $ids ?: DBSIMPLE_SKIP); + DB::Aowow()->qry('UPDATE ::quests SET `questSortId` = %i WHERE (`specialFlags` & %i OR `id` IN %in)', -1010, QUEST_FLAG_SPECIAL_DUNGEON_FINDER, [24789, 24791, 24923]); // flag internal/unsued quests as unsearchable - DB::Aowow()->query( - 'UPDATE ?_quests - SET `cuFlags` = `cuFlags` | ?d + DB::Aowow()->qry( + 'UPDATE ::quests + SET `cuFlags` = `cuFlags` | %i WHERE `name_loc0` LIKE "%<%" OR `name_loc0` LIKE "%[%" OR `name_loc0` LIKE "%deprecated%" OR `name_loc0` LIKE "%unused%" OR diff --git a/setup/tools/sqlgen/questsstartend.ss.php b/setup/tools/sqlgen/questsstartend.ss.php index bdee3959..da847eef 100644 --- a/setup/tools/sqlgen/questsstartend.ss.php +++ b/setup/tools/sqlgen/questsstartend.ss.php @@ -17,7 +17,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $worldDependency = ['creature_queststarter', 'creature_questender', 'game_event_creature_quest', 'gameobject_queststarter', 'gameobject_questender', 'game_event_gameobject_quest', 'item_template']; - public function generate(array $ids = []) : bool + public function generate() : bool { $query['NPC'] = 'SELECT 1 AS `type`, `id` AS `typeId`, `quest` AS `questId`, 1 AS `method`, 0 AS `eventId` FROM creature_queststarter UNION @@ -31,19 +31,19 @@ CLISetup::registerSetup("sql", new class extends SetupScript $query['Item'] = 'SELECT 3 AS `type`, `entry` AS `typeId`, `startquest` AS `questId`, 1 AS `method`, 0 AS `eventId` FROM item_template WHERE `startquest` <> 0'; - DB::Aowow()->query('TRUNCATE ?_quests_startend'); + DB::Aowow()->qry('TRUNCATE ::quests_startend'); foreach ($query as $n => $q) { CLI::write(' - ' . $n . ' start/end-points', CLI::LOG_BLANK, true, true); - $data = DB::World()->select($q); + $data = DB::World()->selectAssoc($q); foreach ($data as $d) - DB::Aowow()->query('INSERT INTO ?_quests_startend (?#) VALUES (?a) ON DUPLICATE KEY UPDATE `method` = `method` | ?d, `eventId` = IF(`eventId` = 0, ?d, `eventId`)', array_keys($d), array_values($d), $d['method'], $d['eventId']); + DB::Aowow()->qry('INSERT INTO ::quests_startend %v ON DUPLICATE KEY UPDATE `method` = `method` | %i, `eventId` = IF(`eventId` = 0, %i, `eventId`)', $d, $d['method'], $d['eventId']); } // update quests without start as unavailable - Db::Aowow()->query('UPDATE ?_quests q LEFT JOIN ?_quests_startend qse ON qse.`questId` = q.`id` AND qse.`method` & 1 SET q.`cuFlags` = q.`cuFlags` | ?d WHERE qse.`questId` IS NULL', CUSTOM_UNAVAILABLE); + DB::Aowow()->qry('UPDATE ::quests q LEFT JOIN ::quests_startend qse ON qse.`questId` = q.`id` AND qse.`method` & 1 SET q.`cuFlags` = q.`cuFlags` | %i WHERE qse.`questId` IS NULL', CUSTOM_UNAVAILABLE); return true; } diff --git a/setup/tools/sqlgen/races.ss.php b/setup/tools/sqlgen/races.ss.php index fd517778..92370294 100644 --- a/setup/tools/sqlgen/races.ss.php +++ b/setup/tools/sqlgen/races.ss.php @@ -17,22 +17,26 @@ CLISetup::registerSetup("sql", new class extends SetupScript 'races' => [[], CLISetup::ARGV_PARAM, 'Compiles data for type: PlayerRace from dbc.'] ); + protected $setupAfter = [['icons'], []]; protected $dbcSourceFiles = ['chrraces', 'charbaseinfo']; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_races'); - DB::Aowow()->query( - 'INSERT INTO ?_races - SELECT `id`, 0, `flags`, 0, `factionId`, 0, 0, `baseLanguage`, IF(`side` = 2, 0, `side` + 1), `fileString`, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `expansion` + DB::Aowow()->qry('TRUNCATE ::races'); + DB::Aowow()->qry( + 'INSERT INTO ::races + SELECT `id`, 0, `flags`, 0, `factionId`, 0, 0, `baseLanguage`, IF(`side` = 2, 0, `side` + 1), `fileString`, 0, 0, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc4`, `name_loc6`, `name_loc8`, `expansion` FROM dbc_chrraces' ); + // collect iconIds + DB::Aowow()->qry('UPDATE ::races r, ::icons i0, ::icons i1 SET r.`iconId0` = i0.`id`, r.`iconId1` = i1.`id` WHERE i0.`name_source` = CONCAT("race_", LOWER(r.`fileString`), "_male") AND i1.`name_source` = CONCAT("race_", LOWER(r.`fileString`), "_female")'); + // add classMask - DB::Aowow()->query('UPDATE ?_races r JOIN (SELECT BIT_OR(1 << (`classId` - 1)) AS "classMask", `raceId` FROM dbc_charbaseinfo GROUP BY `raceId`) cbi ON cbi.`raceId` = r.id SET r.`classMask` = cbi.`classMask`'); + DB::Aowow()->qry('UPDATE ::races r JOIN (SELECT BIT_OR(1 << (`classId` - 1)) AS "classMask", `raceId` FROM dbc_charbaseinfo GROUP BY `raceId`) cbi ON cbi.`raceId` = r.id SET r.`classMask` = cbi.`classMask`'); // add cuFlags - DB::Aowow()->query('UPDATE ?_races SET `cuFlags` = ?d WHERE `flags` & ?d', CUSTOM_EXCLUDE_FOR_LISTVIEW, 0x1); + DB::Aowow()->qry('UPDATE ::races SET `cuFlags` = %i WHERE `flags` & %i', CUSTOM_EXCLUDE_FOR_LISTVIEW, 0x1); $this->reapplyCCFlags('races', Type::CHR_RACE); diff --git a/setup/tools/sqlgen/search.ss.php b/setup/tools/sqlgen/search.ss.php new file mode 100644 index 00000000..6c60bee4 --- /dev/null +++ b/setup/tools/sqlgen/search.ss.php @@ -0,0 +1,364 @@ +<?php + +namespace Aowow; + +if (!defined('AOWOW_REVISION')) + die('illegal access'); + +if (!CLI) + die('not in cli mode'); + + +CLISetup::registerSetup("sql", new class extends SetupScript +{ + use TrCustomData; + + protected $info = array( + 'search' => [[ ], CLISetup::ARGV_PARAM, 'Normalize strings from creatures, items, objects, quests & spells for fulltext search.'], +/* 1 */ 'creature' => [['1'], CLISetup::ARGV_OPTIONAL, '...only for creatures.'], +/* 2 */ 'item' => [['2'], CLISetup::ARGV_OPTIONAL, '...only for items.'], +/* 4 */ 'object' => [['3'], CLISetup::ARGV_OPTIONAL, '...only for objects.'], +/* 8 */ 'spell' => [['4'], CLISetup::ARGV_OPTIONAL, '...only for spells.'], +/*16 */ 'quest' => [['5'], CLISetup::ARGV_OPTIONAL, '...only for quests.'] + ); + + protected $setupAfter = [['creature', 'items', 'objects', 'spell', 'quests'], []]; + + private const /* int */ OPT_NPCS = (1 << 0); + private const /* int */ OPT_ITEMS = (1 << 1); + private const /* int */ OPT_OBJECTS = (1 << 2); + private const /* int */ OPT_SPELLS = (1 << 3); + private const /* int */ OPT_QUESTS = (1 << 4); + + private array $spells = []; + private array $locales = []; + + public function generate() : bool + { + // find out what to do + $opts = array_slice(array_keys($this->info), 1); + $getO = CLISetup::getOpt(...$opts); + $mask = null; + + // todo: have an extra search table with ngram fulltext indices + $this->locales = array_filter(CLISetup::$locales, fn($x) => !$x->isLogographic()); + + if ($getO['creature']) + $mask |= self::OPT_NPCS; + if ($getO['item']) + $mask |= self::OPT_ITEMS; + if ($getO['object']) + $mask |= self::OPT_OBJECTS; + if ($getO['spell']) + $mask |= self::OPT_SPELLS; + if ($getO['quest']) + $mask |= self::OPT_QUESTS; + + $mask ??= (self::OPT_NPCS | self::OPT_ITEMS | self::OPT_OBJECTS | self::OPT_SPELLS | self::OPT_QUESTS); + + // do what needs doing + DB::Aowow()->qry('SET SESSION innodb_ft_enable_stopword = OFF'); + + if ($mask & self::OPT_NPCS) + $this->normalizeCreatures(); + if ($mask & self::OPT_ITEMS) + $this->normalizeItems(); + if ($mask & self::OPT_OBJECTS) + $this->normalizeObjects(); + if ($mask & self::OPT_SPELLS) + $this->normalizeSpells(); + if ($mask & self::OPT_QUESTS) + $this->normalizeQuests(); + + return true; + } + + private function normalizeQuests() : void + { + CLI::write(' - creating indices for quest names, descriptions & details...'); + + DB::Aowow()->qry('TRUNCATE ::quests_search'); + + CLI::write(' * fetching', tmpRow: true); + + $rows = DB::Aowow()->selectAssoc( + 'SELECT `id`, 0 AS "locale", `name_loc0` AS "name", `objectives_loc0` AS "objectives", `details_loc0` AS "details" FROM ::quests UNION + SELECT `id`, 2 AS "locale", `name_loc2` AS "name", `objectives_loc2` AS "objectives", `details_loc2` AS "details" FROM ::quests UNION + SELECT `id`, 3 AS "locale", `name_loc3` AS "name", `objectives_loc3` AS "objectives", `details_loc3` AS "details" FROM ::quests UNION + SELECT `id`, 6 AS "locale", `name_loc6` AS "name", `objectives_loc6` AS "objectives", `details_loc6` AS "details" FROM ::quests UNION + SELECT `id`, 8 AS "locale", `name_loc8` AS "name", `objectives_loc8` AS "objectives", `details_loc8` AS "details" FROM ::quests' + ); + + CLI::write(' * normalizing', tmpRow: true); + + array_walk($rows, self::normalizeRow(...), ['name', 'objectives', 'details']); + + $rows = array_filter($rows, fn($x) => $x['name'] !== null); + + $n = ceil(count($rows) / CLISetup::SQL_BATCH); + for ($i = 0; $i < $n; $i++) + { + $sub = array_slice($rows, $i * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH); + + CLI::write(' * inserting batch '.($i + 1).' / '.$n, tmpRow: true); + DB::Aowow()->qry('INSERT INTO ::quests_search %m', array( + 'id' => array_column($sub, 'id'), + 'locale' => array_column($sub, 'locale'), + 'nName' => array_column($sub, 'name'), + 'nObjectives' => array_column($sub, 'objectives'), + 'nDetails' => array_column($sub, 'details') + )); + } + } + + private function normalizeObjects() : void + { + CLI::write(' - creating indices for object names...'); + + DB::Aowow()->qry('TRUNCATE ::objects_search'); + + CLI::write(' * fetching', tmpRow: true); + + $rows = DB::Aowow()->selectAssoc( + 'SELECT `id`, 0 AS "locale", `name_loc0` AS "name" FROM ::objects UNION + SELECT `id`, 2 AS "locale", `name_loc2` AS "name" FROM ::objects UNION + SELECT `id`, 3 AS "locale", `name_loc3` AS "name" FROM ::objects UNION + SELECT `id`, 6 AS "locale", `name_loc6` AS "name" FROM ::objects UNION + SELECT `id`, 8 AS "locale", `name_loc8` AS "name" FROM ::objects' + ); + + CLI::write(' * normalizing', tmpRow: true); + + array_walk($rows, self::normalizeRow(...), ['name']); + + $rows = array_filter($rows, fn($x) => $x['name'] !== null); + + $result = array( + 'id' => array_column($rows, 'id'), + 'locale' => array_column($rows, 'locale'), + 'nName' => array_column($rows, 'name') + ); + + $n = ceil(count(current($result)) / CLISetup::SQL_BATCH); + for ($i = 0; $i < $n; $i++) + { + CLI::write(' * inserting batch '.($i + 1).' / '.$n, tmpRow: true); + DB::Aowow()->qry('INSERT INTO ::objects_search %m', array_map(fn($x) => array_slice($x, $i * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH), $result)); + } + } + + private function normalizeCreatures() : void + { + CLI::write(' - creating indices for creature names & subnames...'); + + DB::Aowow()->qry('TRUNCATE ::creature_search'); + + CLI::write(' * fetching', tmpRow: true); + + $rows = DB::Aowow()->selectAssoc( + 'SELECT `id`, 0 AS "locale", `name_loc0` AS "name", `subname_loc0` AS "subname" FROM ::creature UNION + SELECT `id`, 2 AS "locale", `name_loc2` AS "name", `subname_loc2` AS "subname" FROM ::creature UNION + SELECT `id`, 3 AS "locale", `name_loc3` AS "name", `subname_loc3` AS "subname" FROM ::creature UNION + SELECT `id`, 6 AS "locale", `name_loc6` AS "name", `subname_loc6` AS "subname" FROM ::creature UNION + SELECT `id`, 8 AS "locale", `name_loc8` AS "name", `subname_loc8` AS "subname" FROM ::creature' + ); + + CLI::write(' * normalizing', tmpRow: true); + + array_walk($rows, self::normalizeRow(...), ['name', 'subname']); + + $rows = array_filter($rows, fn($x) => $x['name'] !== null || $x['subname'] !== null); + + $result = array( + 'id' => array_column($rows, 'id'), + 'locale' => array_column($rows, 'locale'), + 'nName' => array_column($rows, 'name'), + 'nSubname' => array_column($rows, 'subname') + ); + + $n = ceil(count(current($result)) / CLISetup::SQL_BATCH); + for ($i = 0; $i < $n; $i++) + { + CLI::write(' * inserting batch '.($i + 1).' / '.$n, tmpRow: true); + DB::Aowow()->qry('INSERT INTO ::creature_search %m', array_map(fn($x) => array_slice($x, $i * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH), $result)); + } + } + + private function normalizeItems() : void + { + CLI::write(' - creating indices for item names, descriptions & spells...'); + + DB::Aowow()->qry('TRUNCATE ::items_search'); + + CLI::write(' * fetching', tmpRow: true); + + $result = []; + $items = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc6`, `name_loc8`, `description_loc0`, `description_loc2`, `description_loc3`, `description_loc6`, `description_loc8`, `spellId1`, `spellId2`, `spellId3`, `spellId4`, `spellId5` FROM ::items'); + $spells = new SpellList(array(['id', + array_filter(array_column($items, 'spellId1')), + array_filter(array_column($items, 'spellId2')), + array_filter(array_column($items, 'spellId3')), + array_filter(array_column($items, 'spellId4')), + array_filter(array_column($items, 'spellId5')) + ]), ['interactive' => SpellList::INTERACTIVE_NONE]); + + + $n = count($items) * count($this->locales); + $j = 0; + + foreach ($this->locales as $locId => $loc) + { + Lang::load($loc); + + foreach ($items as $id => $item) + { + CLI::write(' * normalizing '.++$j.' / '.$n.' ('.sprintf('%.2f%%', $j * 100 / $n).')', tmpRow: true); + + $name = $desc = $effects = null; + + // ui escape sequences not in default 335a, but undestood by client and may be custom + if ($_ = Util::localizedString($item, 'name', true)) + $name = self::normalize(Lang::unescapeUISequences($_, Lang::FMT_RAW)); + if ($_ = Util::localizedString($item, 'description', true)) + $desc = self::normalize($_); + + for ($i = 1; $i < 6; $i++) + { + $sId = $item['spellId'.$i]; + if (!$sId) + continue; + + if ($spells->getEntry($sId)) + if ($_ = $spells->parseText('description')[0]) + $effects .= str_replace('<br />', ' ', $_); + } + + if (($effects = self::normalize($effects)) || $name || $desc) + { + $result['id'][] = $id; + $result['locale'][] = $locId; + $result['nName'][] = $name; + $result['nDescription'][] = $desc; + $result['nEffects'][] = $effects; + } + } + } + + $n = ceil(count(current($result)) / CLISetup::SQL_BATCH); + for ($i = 0; $i < $n; $i++) + { + CLI::write(' * inserting batch '.($i + 1).' / '.$n, tmpRow: true); + DB::Aowow()->qry('INSERT INTO ::items_search %m', array_map(fn($x) => array_slice($x, $i * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH), $result)); + } + } + + private function normalizeSpells() : void + { + CLI::write(' - creating indices for spell names, descriptions & buffs...'); + + DB::Aowow()->qry('TRUNCATE ::spell_search'); + + CLI::write(' * fetching', tmpRow: true); + + $splBuf = []; + $result = []; + $spells = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `name_loc0`, `name_loc2`, `name_loc3`, `name_loc6`, `name_loc8`, `buff_loc0`, `buff_loc2`, `buff_loc3`, `buff_loc6`, `buff_loc8`, `description_loc0`, `description_loc2`, `description_loc3`, `description_loc6`, `description_loc8` FROM ::spell'); + + $n = count($spells) * count($this->locales); + $j = 0; + + foreach ($this->locales as $locId => $loc) + { + Lang::load($loc); + + foreach ($spells as $id => $spell) + { + CLI::write(' * normalizing '.++$j.' / '.$n.' ('.sprintf('%.2f%%', $j * 100 / $n).')', tmpRow: true); + + $name = $desc = $buff = null; + + // initializing a Spell Object and parsing the tooltip is a lot of effort. + // so don't do that unless we really really have to + if (strpos($spell['description_loc'.$locId], '$') || strpos($spell['buff_loc'.$locId], '$')) + { + $splBuf[$id] ??= new SpellList(array(['id', $id]), ['interactive' => SpellList::INTERACTIVE_NONE]); + + if ($_ = $splBuf[$id]->parseText('description')[0]) + $desc = self::normalize(str_replace('<br />', ' ', $_)); + if ($_ = $splBuf[$id]->parseText('buff')[0]) + $buff = self::normalize(str_replace('<br />', ' ', $_)); + } + + if (!$desc && ($_ = Util::localizedString($spell, 'description', true))) + $desc = self::normalize($_); + if (!$buff && ($_ = Util::localizedString($spell, 'buff', true))) + $buff = self::normalize($_); + if ($_ = Util::localizedString($spell, 'name', true)) + $name = self::normalize($_); + + if ($buff || $name || $desc) + { + $result['id'][] = $id; + $result['locale'][] = $locId; + $result['nName'][] = $name; + $result['nDescription'][] = $desc; + $result['nBuff'][] = $buff; + } + } + } + + $n = ceil(count(current($result)) / CLISetup::SQL_BATCH); + for ($i = 0; $i < $n; $i++) + { + CLI::write(' * inserting batch '.($i + 1).' / '.$n, tmpRow: true); + DB::Aowow()->qry('INSERT INTO ::spell_search %m', array_map(fn($x) => array_slice($x, $i * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH), $result)); + } + } + + private static function normalizeRow(array &$row, int $idx, array $keys) : void + { + foreach ($keys as $key) + $row[$key] = self::normalize($row[$key] ?? ''); + } + + // e.g. "Zul'Aman O'Reilly" => "Zul Aman ZulAman OReilly Reilly luZ namA luZnamA yllieRO yllieR" + private static function normalize(?string $words) : ?string + { + if (!$words) + return null; + + $words = array_filter(explode(' ', $words), fn($x) => mb_strlen($x) > 2); + $result = []; + + foreach ($words as $word) + { + if (($new = trim(preg_replace(Filter::PATTERN_FT, ' ', $word, count: $n))) && $n) + { + if (!strpos($new, ' ')) // caught trailing dots or something + { + $result[] = $new; + continue; + } + + if ($splitWords = array_filter(explode(' ', $new), fn($x) => mb_strlen($x) > 2)) + $result = array_merge($result, $splitWords); + + $result[] = str_replace(' ', '', $new); + + continue; + } + + $result[] = $word; + } + + if (!$result) + return null; + + $result = array_unique($result); + $reversed = array_map(Util::strrev(...), $result); + + return implode(' ', array_merge($result, $reversed)); + } +}); + +?> diff --git a/setup/tools/sqlgen/shapeshiftforms.ss.php b/setup/tools/sqlgen/shapeshiftforms.ss.php index eeb53455..0c31afe7 100644 --- a/setup/tools/sqlgen/shapeshiftforms.ss.php +++ b/setup/tools/sqlgen/shapeshiftforms.ss.php @@ -19,11 +19,11 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['spellshapeshiftform']; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_shapeshiftforms'); - DB::Aowow()->query( - 'INSERT INTO ?_shapeshiftforms + DB::Aowow()->qry('TRUNCATE ::shapeshiftforms'); + DB::Aowow()->qry( + 'INSERT INTO ::shapeshiftforms SELECT id, flags, creatureType, displayIdA, displayIdH, spellId1, spellId2, spellId3, spellId4, spellId5, spellId6, spellId7, spellId8, IF(name_loc0 = "", IF(name_loc2 = "", IF(name_loc3 = "", IF(name_loc4 = "", IF(name_loc6 = "", IF(name_loc8 = "", "???", name_loc8), name_loc6), name_loc4), name_loc3), name_loc2), name_loc0) diff --git a/setup/tools/sqlgen/skillline.ss.php b/setup/tools/sqlgen/skillline.ss.php index 9478b8b0..d0433cce 100644 --- a/setup/tools/sqlgen/skillline.ss.php +++ b/setup/tools/sqlgen/skillline.ss.php @@ -20,41 +20,41 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['skillline', 'spell', 'skilllineability']; protected $setupAfter = [['icons'], []]; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_skillline'); - DB::Aowow()->query( - 'INSERT INTO ?_skillline + DB::Aowow()->qry('TRUNCATE ::skillline'); + DB::Aowow()->qry( + 'INSERT INTO ::skillline SELECT id, categoryId, 0, categoryId, name_loc0, name_loc2, name_loc3, name_loc4, name_loc6, name_loc8, description_loc0, description_loc2, description_loc3, description_loc4, description_loc6, description_loc8, 0, iconId, 0, 0, "" FROM dbc_skillline' ); // categorization - DB::Aowow()->query('UPDATE ?_skillline SET typeCat = -5 WHERE id = 777 OR (categoryId = 9 AND id NOT IN (356, 129, 185, 142, 155))'); - DB::Aowow()->query('UPDATE ?_skillline SET typeCat = -4 WHERE categoryId = 9 AND name_loc0 LIKE "%racial%"'); - DB::Aowow()->query('UPDATE ?_skillline SET typeCat = -6 WHERE id IN (778, 788, 758) OR (categoryId = 7 AND name_loc0 LIKE "%pet%")'); + DB::Aowow()->qry('UPDATE ::skillline SET typeCat = -5 WHERE id = 777 OR (categoryId = 9 AND id NOT IN (356, 129, 185, 142, 155))'); + DB::Aowow()->qry('UPDATE ::skillline SET typeCat = -4 WHERE categoryId = 9 AND name_loc0 LIKE "%racial%"'); + DB::Aowow()->qry('UPDATE ::skillline SET typeCat = -6 WHERE id IN (778, 788, 758) OR (categoryId = 7 AND name_loc0 LIKE "%pet%")'); // more complex fixups - DB::Aowow()->query('UPDATE ?_skillline SET name_loc8 = REPLACE(name_loc8, " - ", ": ") WHERE categoryId = 7 OR id IN (758, 788)'); - DB::Aowow()->query('UPDATE ?_skillline SET cuFlags = ?d WHERE id IN (?a)', CUSTOM_EXCLUDE_FOR_LISTVIEW, [142, 148, 149, 150, 152, 155, 183, 533, 553, 554, 713, 769]); + DB::Aowow()->qry('UPDATE ::skillline SET name_loc8 = REPLACE(name_loc8, " - ", ": ") WHERE categoryId = 7 OR id IN (758, 788)'); + DB::Aowow()->qry('UPDATE ::skillline SET cuFlags = %i WHERE id IN %in', CUSTOM_EXCLUDE_FOR_LISTVIEW, [142, 148, 149, 150, 152, 155, 183, 533, 553, 554, 713, 769]); // apply icons - DB::Aowow()->query('UPDATE ?_skillline sl, ?_icons ic, dbc_spellicon si SET sl.iconId = ic.id WHERE sl.iconIdBak = si.id AND ic.name = LOWER(SUBSTRING_INDEX(si.iconPath, "\\\\", -1))'); - DB::Aowow()->query( - 'UPDATE ?_skillline sl, + DB::Aowow()->qry('UPDATE ::skillline sl, ::icons ic, dbc_spellicon si SET sl.iconId = ic.id WHERE sl.iconIdBak = si.id AND ic.name = LOWER(SUBSTRING_INDEX(si.iconPath, "\\", -1))'); + DB::Aowow()->qry( + 'UPDATE ::skillline sl, dbc_spell s, dbc_skilllineability sla, - ?_icons ic, + ::icons ic, dbc_spellicon si SET sl.iconId = ic.id WHERE (s.effect1Id IN (25, 26, 40) OR s.effect2Id = 60) AND - ic.name = LOWER(SUBSTRING_INDEX(si.iconPath, "\\\\", -1)) AND + ic.name_source = LOWER(SUBSTRING_INDEX(si.iconPath, "\\", -1)) AND s.iconId = si.id AND sla.spellId = s.id AND sl.id = sla.skillLineId' ); - DB::Aowow()->query('UPDATE ?_skillline sl, ?_icons ic SET sl.iconId = ic.id WHERE ic.name = ? AND sl.id = ?d', 'inv_misc_pelt_wolf_01', 393); - DB::Aowow()->query('UPDATE ?_skillline sl, ?_icons ic SET sl.iconId = ic.id WHERE ic.name = ? AND sl.id = ?d', 'inv_misc_key_03', 633); + DB::Aowow()->qry('UPDATE ::skillline sl, ::icons ic SET sl.iconId = ic.id WHERE ic.name = %s AND sl.id = %i', 'inv_misc_pelt_wolf_01', 393); + DB::Aowow()->qry('UPDATE ::skillline sl, ::icons ic SET sl.iconId = ic.id WHERE ic.name = %s AND sl.id = %i', 'inv_misc_key_03', 633); $this->reapplyCCFlags('skillline', Type::SKILL); diff --git a/setup/tools/sqlgen/sounds.ss.php b/setup/tools/sqlgen/sounds.ss.php index 84cb2b7b..8b201393 100644 --- a/setup/tools/sqlgen/sounds.ss.php +++ b/setup/tools/sqlgen/sounds.ss.php @@ -30,7 +30,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript 'material', 'itemgroupsounds', 'itemdisplayinfo', 'weaponimpactsounds', 'itemsubclass', 'weaponswingsounds2' /*, 'sheathesoundlookups' data is redundant with material..? */ ); - public function generate(array $ids = []) : bool + public function generate() : bool { /* okay, here's the thing. WMOAreaTable.dbc references WMO-files to get its position in the world (AreTable) and has sparse information on the related AreaTables themself. @@ -62,20 +62,20 @@ CLISetup::registerSetup("sql", new class extends SetupScript // .mp3 => audio/mpeg $query = - 'SELECT `id` AS `id`, `type` AS `cat`, `name`, 0 AS `cuFlags`, - `file1` AS `soundFile1`, `file2` AS `soundFile2`, `file3` AS `soundFile3`, `file4` AS `soundFile4`, `file5` AS `soundFile5`, - `file6` AS `soundFile6`, `file7` AS `soundFile7`, `file8` AS `soundFile8`, `file9` AS `soundFile9`, `file10` AS `soundFile10`, - `path`, `flags` + 'SELECT `id` AS "id", `type` AS "cat", `name`, 0 AS "cuFlags", + `file1` AS "soundFile1", `file2` AS "soundFile2", `file3` AS "soundFile3", `file4` AS "soundFile4", `file5` AS "soundFile5", + `file6` AS "soundFile6", `file7` AS "soundFile7", `file8` AS "soundFile8", `file9` AS "soundFile9", `file10` AS "soundFile10", + IFNULL(`path`, "") AS "path", `flags` FROM dbc_soundentries - LIMIT ?d, ?d'; + LIMIT %i, %i'; - DB::Aowow()->query('TRUNCATE ?_sounds'); - DB::Aowow()->query('TRUNCATE ?_sounds_files'); + DB::Aowow()->qry('TRUNCATE ::sounds'); + DB::Aowow()->qry('TRUNCATE ::sounds_files'); $soundFileIdx = 0; $soundIndex = []; $j = 0; - while ($sounds = DB::Aowow()->select($query, $j * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH)) + while ($sounds = DB::Aowow()->selectAssoc($query, $j * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH)) { CLI::write('[sound] * batch #' . ++$j . ' (' . count($sounds) . ')', CLI::LOG_BLANK, true, true); @@ -95,6 +95,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript $hasDupes = false; for ($i = 1; $i < 11; $i++) { + if (!$s['soundFile'.$i]) + continue; + $nicePath = CLI::nicePath($s['soundFile'.$i], $s['path']); if ($s['soundFile'.$i] && array_key_exists($nicePath, $soundIndex)) { @@ -108,7 +111,11 @@ CLISetup::registerSetup("sql", new class extends SetupScript { $soundIndex[$nicePath] = ++$soundFileIdx; - $fileSets[] = [$soundFileIdx, $s['soundFile'.$i], $s['path'], SOUND_TYPE_OGG]; + $fileSets['id'][] = $soundFileIdx; + $fileSets['file'][] = $s['soundFile'.$i]; + $fileSets['path'][] = $s['path']; + $fileSets['type'][] = SOUND_TYPE_OGG; + $s['soundFile'.$i] = $soundFileIdx; } // mp3 .. keep as is @@ -116,7 +123,11 @@ CLISetup::registerSetup("sql", new class extends SetupScript { $soundIndex[$nicePath] = ++$soundFileIdx; - $fileSets[] = [$soundFileIdx, $s['soundFile'.$i], $s['path'], SOUND_TYPE_MP3]; + $fileSets['id'][] = $soundFileIdx; + $fileSets['file'][] = $s['soundFile'.$i]; + $fileSets['path'][] = $s['path']; + $fileSets['type'][] = SOUND_TYPE_MP3; + $s['soundFile'.$i] = $soundFileIdx; } // i call bullshit @@ -126,9 +137,6 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[sound] Group '.str_pad('['.$s['id'].']', 7).' '.CLI::bold($s['name']).' has invalid sound file '.CLI::bold($s['soundFile'.$i]).' on index '.$i.'! Skipping...', CLI::LOG_WARN); $s['soundFile'.$i] = null; } - // empty case - else - $s['soundFile'.$i] = null; } if (!$fileSets && !$hasDupes) @@ -137,14 +145,15 @@ CLISetup::registerSetup("sql", new class extends SetupScript continue; } else if ($fileSets) - DB::Aowow()->query('INSERT INTO ?_sounds_files VALUES (?a)', array_values($fileSets)); + DB::Aowow()->qry('INSERT INTO ::sounds_files %m', $fileSets); unset($s['path']); $groupSets[] = array_values($s); } - DB::Aowow()->query('INSERT INTO ?_sounds VALUES (?a)', array_values($groupSets)); + foreach ($groupSets as $gs) + DB::Aowow()->qry('INSERT INTO ::sounds VALUES %l', $gs); } @@ -154,9 +163,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[sound] - linking to race'); - DB::Aowow()->query('TRUNCATE ?_races_sounds'); - DB::Aowow()->query( - 'INSERT IGNORE INTO ?_races_sounds + DB::Aowow()->qry('TRUNCATE ::races_sounds'); + DB::Aowow()->qry( + 'INSERT IGNORE INTO ::races_sounds SELECT `raceId`, `soundIdMale`, 1 FROM dbc_vocaluisounds WHERE `soundIdMale` <> `soundIdFemale` AND `soundIdMale` > 0 UNION SELECT `raceId`, `soundIdFemale`, 2 FROM dbc_vocaluisounds WHERE `soundIdMale` <> `soundIdFemale` AND `soundIdFemale` > 0' ); @@ -168,8 +177,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[sound] - linking to emotes'); - DB::Aowow()->query('TRUNCATE ?_emotes_sounds'); - DB::Aowow()->query('INSERT IGNORE INTO ?_emotes_sounds SELECT `emotesTextId`, `raceId`, `gender` + 1, `soundId` FROM dbc_emotestextsound'); + DB::Aowow()->qry('TRUNCATE ::emotes_sounds'); + DB::Aowow()->qry('INSERT IGNORE INTO ::emotes_sounds SELECT `emotesTextId`, `raceId`, `gender` + 1, `soundId` FROM dbc_emotestextsound'); /*******************/ @@ -184,9 +193,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript // * customattack2 through 3 // in case of conflicting data CreatureDisplayInfo overrides CreatureModelData (seems to be more specialized (Thral > MaleOrc / Maiden > FemaleTitan)) - DB::Aowow()->query('TRUNCATE ?_creature_sounds'); - DB::Aowow()->query( - 'INSERT INTO ?_creature_sounds + DB::Aowow()->qry('TRUNCATE ::creature_sounds'); + DB::Aowow()->qry( + 'INSERT INTO ::creature_sounds SELECT cdi.`id`, GREATEST(IFNULL(ns.`greetSoundId`, 0), 0), GREATEST(IFNULL(ns.`byeSoundId`, 0), 0), @@ -236,9 +245,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript // * missile and impactarea not in js // * ready, castertargeting, casterstate and targetstate not in dbc - DB::Aowow()->query('TRUNCATE ?_spell_sounds'); - DB::Aowow()->query( - 'INSERT INTO ?_spell_sounds + DB::Aowow()->qry('TRUNCATE ::spell_sounds'); + DB::Aowow()->qry( + 'INSERT INTO ::spell_sounds SELECT sv.`id`, GREATEST(`animationSoundId`, 0), 0, -- ready @@ -282,9 +291,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript // omiting data from WMOAreaTable, as its at the moment impossible to link to actual zones - DB::Aowow()->query('TRUNCATE ?_zones_sounds'); - DB::Aowow()->query( - 'INSERT INTO ?_zones_sounds (`id`, `ambienceDay`, `ambienceNight`, `musicDay`, `musicNight`, `intro`, `worldStateId`, `worldStateValue`) + DB::Aowow()->qry('TRUNCATE ::zones_sounds'); + DB::Aowow()->qry( + 'INSERT INTO ::zones_sounds (`id`, `ambienceDay`, `ambienceNight`, `musicDay`, `musicNight`, `intro`, `worldStateId`, `worldStateValue`) SELECT a.`id`, IFNULL(sa1.`soundIdDay`, 0), IFNULL(sa1.`soundIdNight`, 0), @@ -311,13 +320,13 @@ CLISetup::registerSetup("sql", new class extends SetupScript LEFT JOIN dbc_soundambience sa2 ON sa2.`id` = wszs.`soundAmbienceId` LEFT JOIN dbc_zonemusic zm2 ON zm2.`id` = wszs.`zoneMusicId` LEFT JOIN dbc_zoneintromusictable zimt2 ON zimt2.`id` = wszs.`zoneIntroMusicId` - WHERE wszs.`zoneMusicId` > 0 AND (wszs.`areaId` OR wszs.`wmoAreaId` IN (?a))', + WHERE wszs.`zoneMusicId` > 0 AND (wszs.`areaId` OR wszs.`wmoAreaId` IN %in)', array_keys($worldStateZoneSoundFix) ); // apply post-fix foreach ($worldStateZoneSoundFix as $old => $new) - DB::Aowow()->query('UPDATE ?_zones_sounds SET `id` = ?d WHERE `id` = ?d', $new, $old); + DB::Aowow()->qry('UPDATE ::zones_sounds SET `id` = %i WHERE `id` = %i', $new, $old); /***************/ @@ -326,8 +335,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[sound] - linking to items'); - DB::Aowow()->query( - 'UPDATE ?_items i + DB::Aowow()->qry( + 'UPDATE ::items i LEFT JOIN dbc_itemdisplayinfo idi ON idi.`id` = i.`displayId` LEFT JOIN dbc_itemgroupsounds igs ON igs.`id` = idi.`groupSoundId` LEFT JOIN dbc_material m ON m.`id` = i.`material` @@ -338,22 +347,22 @@ CLISetup::registerSetup("sql", new class extends SetupScript i.`unsheatheSoundId` = IFNULL(m.`unsheatheSoundId`, 0)' ); - DB::Aowow()->query('TRUNCATE ?_items_sounds'); + DB::Aowow()->qry('TRUNCATE ::items_sounds'); $fields = ['hit', 'crit']; foreach ($fields as $f) for ($i = 1; $i <= 10; $i++) - DB::Aowow()->query( - 'INSERT INTO ?_items_sounds - SELECT ?#, (1 << wis.`subClass`) + DB::Aowow()->qry( + 'INSERT INTO ::items_sounds + SELECT %n, (1 << wis.`subClass`) FROM dbc_weaponimpactsounds wis - WHERE ?# > 0 + WHERE %n > 0 ON DUPLICATE KEY UPDATE `subClassMask` = `subClassMask` | (1 << wis.`subClass`)', $f.$i, $f.$i ); - DB::Aowow()->query( - 'INSERT INTO ?_items_sounds + DB::Aowow()->qry( + 'INSERT INTO ::items_sounds SELECT wss.`soundId`, (1 << isc.`subClass`) FROM dbc_itemsubclass isc JOIN dbc_weaponswingsounds2 wss ON wss.`weaponSize` = isc.`weaponSize` @@ -368,9 +377,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[sound] - linking to screen effects'); - DB::Aowow()->query('TRUNCATE ?_screeneffect_sounds'); - DB::Aowow()->query( - 'INSERT INTO ?_screeneffect_sounds + DB::Aowow()->qry('TRUNCATE ::screeneffect_sounds'); + DB::Aowow()->qry( + 'INSERT INTO ::screeneffect_sounds SELECT se.`id`, se.`name`, IFNULL(sa.`soundIdDay`, 0), IFNULL(sa.`soundIdNight`, 0), IFNULL(zm.`soundIdDay`, 0), IFNULL(zm.`soundIdNight`, 0) FROM dbc_screeneffect se LEFT JOIN dbc_soundambience sa ON se.`soundAmbienceId` = sa.`id` diff --git a/setup/tools/sqlgen/source.ss.php b/setup/tools/sqlgen/source.ss.php index 7aece181..be0ccd0c 100644 --- a/setup/tools/sqlgen/source.ss.php +++ b/setup/tools/sqlgen/source.ss.php @@ -19,31 +19,59 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $worldDependency = ['playercreateinfo_skills', 'playercreateinfo_item', 'skill_discovery_template', 'achievement_reward', 'skill_perfect_item_template', 'item_template', 'gameobject_template', 'quest_template', 'quest_template_addon', 'creature_template', 'creature', 'creature_default_trainer', 'trainer_spell', 'npc_vendor', 'game_event_npc_vendor', 'reference_loot_template', 'item_loot_template', 'creature_loot_template', 'gameobject_loot_template', 'mail_loot_template', 'disenchant_loot_template', 'fishing_loot_template', 'skinning_loot_template', 'milling_loot_template', 'prospecting_loot_template', 'pickpocketing_loot_template']; protected $setupAfter = [['spell', 'achievement', 'items', 'itemset', 'spawns', 'creature', 'zones', 'titles'], []]; - private $srcBuffer = []; - private $refLoot = []; - private $dummyNPCs = []; + private array $srcBuffer = []; + private array $refLoot = []; + private array $dummyNPCs = []; + private array $dummyGOs = []; + private array $disables = []; - private const PVP_MONEY = [26045, 24581, 24579, 43589, 37836]; // Nagrand, Hellfire Pen. H, Hellfire Pen. A, Wintergrasp, Grizzly Hills - private const COMMON_THRESHOLD = 100; + private const /* array */ PVP_MONEY = [26045, 24581, 24579, 43589, 37836]; // Nagrand, Hellfire Pen. H, Hellfire Pen. A, Wintergrasp, Grizzly Hills + private const /* int */ COMMON_THRESHOLD = 30; // if an item has more than X sources it gets filtered by default in loot listviews; ancient WH versions have chance < 1% instead of checking for commonloot property. + // but that would include the super rare vanity pet drops etc, so.. idk? Make it depend of item class and/or quality? That sounds like pain. :< + private const /* array */ FAKE_CHESTS = array( // special cases where multiple chests share the same loot and are spawned for the same encounter + // icc - gunship armory // if we process it like normal the contained items show up as zone drop because technically they have multiple sources. So lets avoid that! + 201873 => 202178, // A -> H + 201874 => 202180, + 201872 => 202177, + 201875 => 202179, + // ulduar freya's gift - point +1 and +2 hardmode chests to max chest + 194329 => 194331, // 25 + 194330 => 194331, + 194325 => 194327, // 10 + 194326 => 194327 + ); - public function generate(array $ids = []) : bool + public function generate() : bool { /*********************************/ /* resolve difficulty dummy NPCS */ /*********************************/ - $this->dummyNPCs = DB::Aowow()->select( - 'SELECT difficultyEntry1 AS ARRAY_KEY, 2 AS "0", `id` AS "1" FROM ?_creature WHERE difficultyEntry1 > 0 UNION - SELECT difficultyEntry2 AS ARRAY_KEY, 4 AS "0", `id` AS "1" FROM ?_creature WHERE difficultyEntry2 > 0 UNION - SELECT difficultyEntry3 AS ARRAY_KEY, 8 AS "0", `id` AS "1" FROM ?_creature WHERE difficultyEntry3 > 0' + $this->dummyNPCs = DB::Aowow()->selectAssoc( + 'SELECT `difficultyEntry1` AS ARRAY_KEY, 2 AS "0", `id` AS "1" FROM ::creature WHERE `difficultyEntry1` > 0 UNION + SELECT `difficultyEntry2` AS ARRAY_KEY, 4 AS "0", `id` AS "1" FROM ::creature WHERE `difficultyEntry2` > 0 UNION + SELECT `difficultyEntry3` AS ARRAY_KEY, 8 AS "0", `id` AS "1" FROM ::creature WHERE `difficultyEntry3` > 0' + ); + + $this->dummyGOs = DB::Aowow()->selectAssoc( + 'SELECT `normal10` AS ARRAY_KEY, 1 AS "0", `normal10` AS "1", `mapType` AS "2" FROM ::objectdifficulty WHERE `normal10` > 0 UNION + SELECT `normal25` AS ARRAY_KEY, 2 AS "0", `normal10` AS "1", `mapType` AS "2" FROM ::objectdifficulty WHERE `normal25` > 0 UNION + SELECT `heroic10` AS ARRAY_KEY, 4 AS "0", `normal10` AS "1", `mapType` AS "2" FROM ::objectdifficulty WHERE `heroic10` > 0 UNION + SELECT `heroic25` AS ARRAY_KEY, 8 AS "0", `normal10` AS "1", `mapType` AS "2" FROM ::objectdifficulty WHERE `heroic25` > 0' + ); + + $this->disables = DB::World()->selectCol( + 'SELECT IF(`sourceType`, %i, %i) AS ARRAY_KEY, `entry` AS ARRAY_KEY2, `entry` + FROM disables + WHERE (`sourceType` = 0 AND (`flags` & 0xF)) OR `sourceType` = 1', + Type::QUEST, Type::SPELL ); - // todo: do the same for GOs CLI::write('[source] - resolving ref-loot tree', CLI::LOG_BLANK, true, true); - $this->refLoot = DB::World()->select( - 'SELECT rlt.Entry AS ARRAY_KEY, IF(Reference, -Reference, Item) AS ARRAY_KEY2, it.entry, it.class, it.subclass, it.spellid_1, it.spelltrigger_1, it.spellid_2, it.spelltrigger_2 + $this->refLoot = DB::World()->selectAssoc( + 'SELECT rlt.`Entry` AS ARRAY_KEY, IF(`Reference`, -`Reference`, `Item`) AS ARRAY_KEY2, it.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2` FROM reference_loot_template rlt - LEFT JOIN item_template it ON rlt.Reference = 0 AND rlt.Item = it.entry + LEFT JOIN item_template it ON rlt.`Reference` = 0 AND rlt.`Item` = it.`entry` GROUP BY ARRAY_KEY, ARRAY_KEY2' ); @@ -70,13 +98,13 @@ CLISetup::registerSetup("sql", new class extends SetupScript } } - DB::Aowow()->query('TRUNCATE ?_source'); + DB::Aowow()->qry('TRUNCATE ::source'); /***************************/ /* Item & inherited Spells */ /***************************/ - CLI::write('[source] - Items & Spells [inherited]'); + CLI::write('[source] - Items & inherited Spells'); # also everything from items that teach spells, is src of spell $this->itemCrafted(); # 1: Crafted # @@ -100,7 +128,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript /* Spell */ /*********/ - CLI::write('[source] - Spells [original]'); + CLI::write('[source] - Spells'); $this->spellQuest(); # 4: Quest # $this->spellTrainer(); # 6: Trainer # @@ -127,23 +155,27 @@ CLISetup::registerSetup("sql", new class extends SetupScript $this->itemset(); # Meta category .. inherit from items # - $t = new Timer(500); + $d = new Timer(500); foreach ($this->srcBuffer as $type => $data) { $j = 0; $rows = []; $sum = count($data); - foreach ($data as $d) + foreach ($data as [$t, $ti, $mt, $mti, $mz, $mFlags, $modes, $sumSources]) { - $rows[++$j] = array_slice($d, 0, 6); - for ($i = 1; $i < 25; $i++) - $rows[$j][] = $d[6][$i] ?? 0; + // can only ever be either/or .. unset if both + if (($mFlags & (SRC_FLAG_RAID_DROP | SRC_FLAG_DUNGEON_DROP)) == (SRC_FLAG_RAID_DROP | SRC_FLAG_DUNGEON_DROP)) + $mFlags &= ~(SRC_FLAG_RAID_DROP | SRC_FLAG_DUNGEON_DROP); - if ($d[7] > self::COMMON_THRESHOLD) + $rows[++$j] = [$t, $ti, $mt, $mti, $mz, $mFlags]; + for ($i = 1; $i < 25; $i++) + $rows[$j][] = $modes[$i] ?? 0; + + if ($sumSources > self::COMMON_THRESHOLD) $rows[$j][5] |= SRC_FLAG_COMMON; - if ($t->update()) - CLI::write('[source] - Inserting... (['.$type.'] '.$j.' / '.$sum.')', CLI::LOG_BLANK, true, true); + if ($d->update()) + CLI::write('[source] - Inserting... (['.Type::getFileString($type).'] '.$j.' / '.$sum.')', CLI::LOG_BLANK, true, true); if (!($j % 300)) $this->insert($rows); @@ -155,14 +187,33 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[source] - Inserting... (done)'); - // flagging aowow_items for source (note: this is not exact! creatures dropping items may not be spawnd, quests granting items may be disabled) - DB::Aowow()->query('UPDATE ?_items SET `cuFlags` = `cuFlags` & ?d', ~CUSTOM_UNAVAILABLE); - DB::Aowow()->query('UPDATE ?_items i LEFT JOIN ?_source s ON s.`typeId` = i.`id` AND s.`type` = ?d SET i.`cuFlags` = i.`cuFlags` | ?d WHERE s.`typeId` IS NULL', Type::ITEM, CUSTOM_UNAVAILABLE); + // generally treat all items generated by spell as being available. The spells may be triggered in convoluted ways but usually _are_ in use. + $itemSpellSource = DB::Aowow()->selectCol( + 'SELECT x.`itemId` FROM ( + SELECT `effect1CreateItemId` AS "itemId", s.`id` FROM dbc_spell s WHERE `effect1CreateItemId` > 0 AND (`effect1Id` IN %in OR `effect1AuraId` IN %in) UNION ALL + SELECT `effect2CreateItemId` AS "itemId", s.`id` FROM dbc_spell s WHERE `effect2CreateItemId` > 0 AND (`effect2Id` IN %in OR `effect2AuraId` IN %in) UNION ALL + SELECT `effect3CreateItemId` AS "itemId", s.`id` FROM dbc_spell s WHERE `effect3CreateItemId` > 0 AND (`effect3Id` IN %in OR `effect3AuraId` IN %in) ) AS x + WHERE x.`id` NOT IN %in', + SpellList::EFFECTS_ITEM_CREATE, SpellList::AURAS_ITEM_CREATE, + SpellList::EFFECTS_ITEM_CREATE, SpellList::AURAS_ITEM_CREATE, + SpellList::EFFECTS_ITEM_CREATE, SpellList::AURAS_ITEM_CREATE, + $this->disables[Type::SPELL] ?? [0] + ); + + // flagging aowow_items for source (note: this is not exact! creatures dropping items may not be spawned, etc.) + DB::Aowow()->qry('UPDATE ::items SET `cuFlags` = `cuFlags` & %i', ~CUSTOM_UNAVAILABLE); + DB::Aowow()->qry( + 'UPDATE ::items i + LEFT JOIN ::source s ON s.`typeId` = i.`id` AND s.`type` = %i + SET i.`cuFlags` = i.`cuFlags` | %i + WHERE (s.`typeId` IS NULL AND i.`id` NOT IN %in) OR i.`quality` = %i', + Type::ITEM, CUSTOM_UNAVAILABLE, $itemSpellSource, ITEM_QUALITY_ARTIFACT + ); return true; } - private function pushBuffer(int $type, int $typeId, int $srcId, int $srcBit = 1, int $mType = 0, int $mTypeId = 0, ?int $mZoneId = null, int $mMask = 0x0, int $qty = 1) : void + private function pushBuffer(int $type, int $typeId, int $srcId, int $srcBit = 1, int $mType = 0, int $mTypeId = 0, ?int $mZoneId = null, ?int $mMask = null, int $qty = 1) : void { if (!isset($this->srcBuffer[$type])) $this->srcBuffer[$type] = []; @@ -173,19 +224,26 @@ CLISetup::registerSetup("sql", new class extends SetupScript return; } - $b = &$this->srcBuffer[$type][$typeId]; + [, , &$bType, &$bTypeId, &$bZone, &$bFlags, &$bSrc, &$bQty] = $this->srcBuffer[$type][$typeId]; - if ($mType != $b[2] || $mTypeId != $b[3]) - $b[2] = $b[3] = null; + if ($mType != $bType || $mTypeId != $bTypeId) + $bType = $bTypeId = null; - if ($mZoneId && $b[4] === null) - $b[4] = $mZoneId; - else if ($mZoneId && $b[4] && $mZoneId != $b[4]) - $b[4] = 0; + if ($bZone === null) + $bZone = $mZoneId; + else if ($mZoneId !== null && $mZoneId != $bZone) + $bZone = 0; - $b[5] = ($b[5] ?? 0) & $mMask; // only bossdrop for now .. remove flag if regular source is available - $b[6][$srcId] = ($b[6][$srcId] ?? 0) | $srcBit; // SIDE_X for quests, modeMask for drops, subSrc for pvp, else: 1 - $b[7] += $qty; + if ($bFlags === null) // bossdrop, raid drop, dungeon drop .. remove flag if regular source is available + $bFlags = $mMask; + else if ($mMask !== null) + { + $bFlags &= ($mMask & SRC_FLAG_BOSSDROP); + $bFlags |= ($mMask & (SRC_FLAG_DUNGEON_DROP | SRC_FLAG_RAID_DROP)); + } + + $bSrc[$srcId] = ($bSrc[$srcId] ?? 0) | $srcBit; // SIDE_X for quests, modeMask for drops, subSrc for pvp, else: 1 + $bQty += $qty; } private function insert(array &$rows) : void @@ -201,13 +259,11 @@ CLISetup::registerSetup("sql", new class extends SetupScript } $rows = []; - DB::Aowow()->query('INSERT INTO ?_source VALUES '. substr($str, 0, -1)); + DB::Aowow()->qry('INSERT INTO ::source VALUES '. substr($str, 0, -1)); } private function taughtSpell(array $item) : int { - # spelltrigger_X (0: onUse; 6: onLearnSpell) - // should not be able to teach spells (recipe || mount || vanityPet) if ($item['class'] != ITEM_CLASS_RECIPE && ($item['class'] != ITEM_CLASS_MISC || ($item['subclass'] != 2 && $item['subclass'] != 5))) return 0; @@ -231,16 +287,27 @@ CLISetup::registerSetup("sql", new class extends SetupScript 'SELECT `effect1CreateItemId` AS ARRAY_KEY, s.`id` FROM dbc_spell s JOIN dbc_skilllineability sla ON s.`id` = sla.`spellId` - WHERE `effect1CreateItemId` > 0 AND sla.`skillLineId` IN (?a) + WHERE `effect1CreateItemId` > 0 AND sla.`skillLineId` IN %in GROUP BY ARRAY_KEY', array_merge(SKILLS_TRADE_PRIMARY, [SKILL_FIRST_AID, SKILL_COOKING, SKILL_FISHING]) ); + // assume unique craft spells per item $perfectItems = DB::World()->selectCol('SELECT `perfectItemType` AS ARRAY_KEY, `spellId` AS "spell" FROM skill_perfect_item_template'); foreach ($perfectItems AS $item => $spell) $itemSpells[$item] = $spell; - $spellItems = DB::World()->select('SELECT `entry` AS ARRAY_KEY, `class`, `subclass`, `spellid_1`, `spelltrigger_1`, `spellid_2`, `spelltrigger_2` FROM item_template WHERE `entry` IN (?a)', array_keys($itemSpells)); + $itemSpells = array_filter($itemSpells, fn($x) => empty($this->disables[Type::SPELL][$x])); + + $spellLoot = DB::World()->selectCol('SELECT IF(`Reference` > 0, -`Reference`, `Item`) AS ARRAY_KEY, `entry` FROM spell_loot_template WHERE `entry` IN %in', $itemSpells); + if ($_ = array_filter($spellLoot, fn($x) => $x > 0, ARRAY_FILTER_USE_KEY)) + $itemSpells = array_replace($itemSpells, $_); + + foreach (array_filter($spellLoot, fn($x) => $x < 0, ARRAY_FILTER_USE_KEY) as $r => $spellId) + if (isset($this->refLoot[-$r])) + $itemSpells = array_replace(array_fill_keys(array_keys($this->refLoot[-$r]), $spellId)); + + $spellItems = DB::World()->selectAssoc('SELECT `entry` AS ARRAY_KEY, `class`, `subclass`, `spellid_1`, `spelltrigger_1`, `spellid_2`, `spelltrigger_2` FROM item_template WHERE `entry` IN %in', array_keys($itemSpells)); foreach ($spellItems as $iId => $si) { if ($_ = $this->taughtSpell($si)) @@ -257,8 +324,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[source] * #2 Drop [NPC]', CLI::LOG_BLANK, true, true); - $creatureLoot = DB::World()->select( - 'SELECT IF(clt.`Reference` > 0, -clt.`Reference`, clt.`Item`) AS "refOrItem", ct.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty" + $creatureLoot = DB::World()->selectAssoc( + 'SELECT IF(clt.`Reference` > 0, -clt.`Reference`, clt.`Item`) AS "refOrItem", ct.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT clt.`Reference`) AS "qty" FROM creature_loot_template clt JOIN creature_template ct ON clt.`entry` = ct.`lootid` LEFT JOIN item_template it ON it.`entry` = clt.`Item` AND clt.`Reference` <= 0 @@ -266,25 +333,26 @@ CLISetup::registerSetup("sql", new class extends SetupScript GROUP BY `refOrItem`, ct.`entry`' ); - $spawns = DB::Aowow()->select('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT s.`areaId`) > 1, 0, s.`areaId`) AS "areaId", z.`type` FROM ?_spawns s JOIN ?_zones z ON z.`id` = s.`areaId` WHERE s.`type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::NPC, array_filter(array_column($creatureLoot, 'entry'))); - $bosses = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, IF(`cuFlags` & ?d, 1, IF(`typeFlags` & 0x4 AND `rank` > 0, 1, 0)) FROM ?_creature WHERE `id` IN (?a)', NPC_CU_INSTANCE_BOSS, array_filter(array_column($creatureLoot, 'entry'))); + $linkedNpcs = DB::Aowow()->selectCol('SELECT l1.`objectId` AS ARRAY_KEY, IFNULL(l2.`npcId`, l1.`npcId`) FROM ::loot_link l1 LEFT JOIN ::loot_link l2 ON l1.`objectId` = l2.`objectId` AND l2.`priority` = 1 GROUP BY l1.`objectid`'); + $npcSpawns = DB::Aowow()->selectAssoc('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT s.`areaId`) > 1, 0, s.`areaId`) AS "areaId", z.`type` FROM ::spawns s JOIN ::zones z ON z.`id` = s.`areaId` WHERE s.`type` = %i AND `typeId` IN %in GROUP BY `typeId`', Type::NPC, array_merge($linkedNpcs, array_filter(array_column($creatureLoot, 'entry')))); + $bosses = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, IF(`cuFlags` & %i, 1, IF(`typeFlags` & 0x4 AND `rank` > 0, 1, 0)) FROM ::creature WHERE `id` IN %in', NPC_CU_INSTANCE_BOSS, array_filter(array_column($creatureLoot, 'entry'))); foreach ($creatureLoot as $l) { $roi = $l['refOrItem']; $entry = $l['entry']; $mode = 1; - $zoneId = 0; + $zoneId = null; $mMask = 0x0; if (isset($this->dummyNPCs[$l['entry']])) [$mode, $entry] = $this->dummyNPCs[$l['entry']]; - if (isset($bosses[$entry]) && $bosses[$entry]) // can be empty...? + if (!empty($bosses[$entry])) $mMask |= SRC_FLAG_BOSSDROP; - if (isset($spawns[$entry])) + if (isset($npcSpawns[$entry])) { - switch ($spawns[$entry]['type']) + switch ($npcSpawns[$entry]['type']) { case MAP_TYPE_DUNGEON_HC: $mMask |= SRC_FLAG_DUNGEON_DROP; break; @@ -293,7 +361,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript $mMask |= SRC_FLAG_RAID_DROP; break; } - $zoneId = $spawns[$entry]['areaId']; + $zoneId = $npcSpawns[$entry]['areaId']; } if ($roi < 0 && !empty($this->refLoot[-$roi])) @@ -320,86 +388,139 @@ CLISetup::registerSetup("sql", new class extends SetupScript $this->pushBuffer(Type::ITEM, $roi, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::NPC, $entry, $zoneId, $mMask, $l['qty']); } + CLI::write('[source] * #2 Drop [Object]', CLI::LOG_BLANK, true, true); - $objectLoot = DB::World()->select( - 'SELECT IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) AS "refOrItem", gt.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty" + $objectLoot = DB::World()->selectAssoc( + 'SELECT IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) AS "refOrItem", gt.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT glt.`Reference`) AS "qty" FROM gameobject_loot_template glt JOIN gameobject_template gt ON glt.`entry` = gt.`data1` LEFT JOIN item_template it ON it.`entry` = glt.`Item` AND glt.`Reference` <= 0 - WHERE `type` = ?d AND gt.`data1` > 0 AND gt.`data0` NOT IN (?a) + WHERE `type` = %i AND gt.`data1` > 0 AND gt.`data0` NOT IN %in GROUP BY `refOrItem`, gt.`entry`', OBJECT_CHEST, - DB::Aowow()->selectCol('SELECT `id` FROM dbc_lock WHERE `properties1` IN (?a)', [LOCK_PROPERTY_HERBALISM, LOCK_PROPERTY_MINING]) + DB::Aowow()->selectCol('SELECT `id` FROM dbc_lock WHERE `properties1` IN %in', [LOCK_PROPERTY_HERBALISM, LOCK_PROPERTY_MINING]) ); - $spawns = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::OBJECT, array_column($objectLoot, 'entry')); + $goSpawns = DB::Aowow()->selectAssoc('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT s.`areaId`) > 1, 0, s.`areaId`) AS "areaId", z.`type` FROM ::spawns s JOIN ::zones z ON z.`id` = s.`areaId` WHERE s.`type` = %i AND `typeId`IN %in GROUP BY `typeId`', Type::OBJECT, array_filter(array_column($objectLoot, 'entry'))); - // todo: difficulty entrys for boss chests foreach ($objectLoot as $l) { $roi = $l['refOrItem']; - $zoneId = $spawns[$l['entry']] ?? 0; + $entry = self::FAKE_CHESTS[$l['entry']] ?? $l['entry']; + $mode = 1; + $zoneId = null; $mMask = 0x0; + if ([$modeBit, $baseEntry, $mapType] = ($this->dummyGOs[$entry] ?? null)) + { + $mMask |= SRC_FLAG_BOSSDROP; // we know these are all boss drops + $mode = $modeBit; + $entry = $baseEntry ?: $entry; + + if ($mapType == 1) + $mMask |= SRC_FLAG_DUNGEON_DROP; + if ($mapType == 2) + $mMask |= SRC_FLAG_RAID_DROP; + } + else if (isset($goSpawns[$entry])) + { + switch ($goSpawns[$entry]['type']) + { + case MAP_TYPE_DUNGEON_HC: + $mMask |= SRC_FLAG_DUNGEON_DROP; break; + case MAP_TYPE_MMODE_RAID: + case MAP_TYPE_MMODE_RAID_HC: + $mMask |= SRC_FLAG_RAID_DROP; break; + } + } + + if (isset($goSpawns[$entry])) + $zoneId = $goSpawns[$entry]['areaId']; + else if (isset($linkedNpcs[$entry])) + { + if (!empty($bosses[$linkedNpcs[$entry]])) + $mMask |= SRC_FLAG_BOSSDROP; + if (isset($npcSpawns[$linkedNpcs[$entry]])) + $zoneId = $npcSpawns[$linkedNpcs[$entry]]['areaId']; + } + if ($roi < 0 && !empty($this->refLoot[-$roi])) { foreach ($this->refLoot[-$roi] as $iId => $r) { if ($_ = $this->taughtSpell($r)) - $this->pushBuffer(Type::SPELL, $_, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']); + $this->pushBuffer(Type::SPELL, $_, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $entry, $zoneId, $mMask, $l['qty']); $objectOT[] = $iId; - $this->pushBuffer(Type::ITEM, $iId, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']); + $this->pushBuffer(Type::ITEM, $iId, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $entry, $zoneId, $mMask, $l['qty']); } continue; } if ($_ = $this->taughtSpell($l)) - $this->pushBuffer(Type::SPELL, $_, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']); + $this->pushBuffer(Type::SPELL, $_, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $entry, $zoneId, $mMask, $l['qty']); $objectOT[] = $roi; - $this->pushBuffer(Type::ITEM, $roi, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::OBJECT, $l['entry'], $zoneId, $mMask, $l['qty']); + $this->pushBuffer(Type::ITEM, $roi, SRC_DROP, $mode, $l['qty'] > 1 ? 0 : Type::OBJECT, $entry, $zoneId, $mMask, $l['qty']); } + CLI::write('[source] * #2 Drop [Item]', CLI::LOG_BLANK, true, true); - $itemLoot = DB::World()->select( - 'SELECT IF(ilt.`Reference` > 0, -ilt.`Reference`, ilt.`Item`) AS ARRAY_KEY, itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(1) AS "qty" + $itemLoot = DB::World()->selectAssoc( + 'SELECT IF(ilt.`Reference` > 0, -ilt.`Reference`, ilt.`Item`) AS ARRAY_KEY, itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(DISTINCT ilt.`Reference`) AS "qty" FROM item_loot_template ilt JOIN item_template itA ON ilt.`entry` = itA.`entry` LEFT JOIN item_template itB ON itB.`entry` = ilt.`Item` AND ilt.`Reference` <= 0 - WHERE itA.`flags` & ?d + WHERE itA.`flags` & %i GROUP BY ARRAY_KEY', ITEM_FLAG_OPENABLE ); - foreach ($itemLoot as $roi => $l) - { - if ($roi < 0 && !empty($this->refLoot[-$roi])) - { - foreach ($this->refLoot[-$roi] as $iId => $r) - { - if ($_ = $this->taughtSpell($r)) - $this->pushBuffer(Type::SPELL, $_, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], 0, $l['qty']); + // Clams are not item containers but have SpellLoot + $lootSpells = DB::Aowow()->selectCol('SELECT s.`id` FROM ::spell s JOIN ::items i ON i.`spellId1` = s.`id` AND i.`spellTrigger1` = %i WHERE s.`effect1Id` = %i', SPELL_TRIGGER_USE, SPELL_EFFECT_CREATE_RANDOM_ITEM); + $spellLoot = DB::World()->selectAssoc( + 'SELECT IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) AS ARRAY_KEY, itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(DISTINCT slt.`Reference`) AS "qty" + FROM spell_loot_template slt + JOIN item_template itA ON slt.`entry` = itA.`spellid_1` + LEFT JOIN item_template itB ON itB.`entry` = slt.`Item` AND slt.`Reference` <= 0 + WHERE itA.`spellid_1` IN %in AND itA.`spelltrigger_1` = %i + GROUP BY ARRAY_KEY', + $lootSpells, SPELL_TRIGGER_USE + ); - $itemOT[] = $iId; - $this->pushBuffer(Type::ITEM, $iId, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], 0, $l['qty']); + foreach ([$itemLoot, $spellLoot] as $container) + { + foreach ($container as $roi => $l) + { + if ($roi < 0 && !empty($this->refLoot[-$roi])) + { + foreach ($this->refLoot[-$roi] as $iId => $r) + { + if ($_ = $this->taughtSpell($r)) + $this->pushBuffer(Type::SPELL, $_, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], qty: $l['qty']); + + $itemOT[] = $iId; + $this->pushBuffer(Type::ITEM, $iId, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], qty: $l['qty']); + } + + continue; } - continue; + if ($_ = $this->taughtSpell($l)) + $this->pushBuffer(Type::SPELL, $_, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], qty: $l['qty']); + + $itemOT[] = $roi; + $this->pushBuffer(Type::ITEM, $roi, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], qty: $l['qty']); } - - if ($_ = $this->taughtSpell($l)) - $this->pushBuffer(Type::SPELL, $_, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], 0, $l['qty']); - - $itemOT[] = $roi; - $this->pushBuffer(Type::ITEM, $roi, SRC_DROP, 1, $l['qty'] > 1 ? 0 : Type::ITEM, $l['entry'], 0, $l['qty']); } - DB::Aowow()->query('UPDATE ?_items SET `cuFLags` = `cuFlags` | ?d WHERE `id` IN (?a)', ITEM_CU_OT_ITEMLOOT, $itemOT); - DB::Aowow()->query('UPDATE ?_items SET `cuFLags` = `cuFlags` | ?d WHERE `id` IN (?a)', ITEM_CU_OT_OBJECTLOOT, $objectOT); + if ($itemOT) + DB::Aowow()->qry('UPDATE ::items SET `cuFLags` = `cuFlags` | %i WHERE `id` IN %in', ITEM_CU_OT_ITEMLOOT, $itemOT); + if ($objectOT) + DB::Aowow()->qry('UPDATE ::items SET `cuFLags` = `cuFlags` | %i WHERE `id` IN %in', ITEM_CU_OT_OBJECTLOOT, $objectOT); } private function itemPvP() : void @@ -409,19 +530,19 @@ CLISetup::registerSetup("sql", new class extends SetupScript $subSrcByXCost = array( SRC_SUB_PVP_BG => DB::Aowow()->selectCol('SELECT `id` FROM dbc_itemextendedcost WHERE `reqArenaPoints` = 0 AND `reqHonorPoints` > 0'), SRC_SUB_PVP_ARENA => DB::Aowow()->selectCol('SELECT `id` FROM dbc_itemextendedcost WHERE `reqArenaPoints` > 0'), - SRC_SUB_PVP_WORLD => DB::Aowow()->selectCol('SELECT `id` FROM dbc_itemextendedcost WHERE `reqItemId1` IN (?a) OR `reqItemId2` IN (?a) OR `reqItemId3` IN (?a) OR `reqItemId4` IN (?a) OR `reqItemId5` IN (?a)', self::PVP_MONEY, self::PVP_MONEY, self::PVP_MONEY, self::PVP_MONEY, self::PVP_MONEY) + SRC_SUB_PVP_WORLD => DB::Aowow()->selectCol('SELECT `id` FROM dbc_itemextendedcost WHERE `reqItemId1` IN %in OR `reqItemId2` IN %in OR `reqItemId3` IN %in OR `reqItemId4` IN %in OR `reqItemId5` IN %in', self::PVP_MONEY, self::PVP_MONEY, self::PVP_MONEY, self::PVP_MONEY, self::PVP_MONEY) ); $vendorQuery = 'SELECT n.`item`, SUM(n.`qty`) AS "qty", it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2` - FROM (SELECT `item`, COUNT(1) AS "qty" FROM npc_vendor WHERE `ExtendedCost` IN (?a) AND `item` > 0 GROUP BY `item` UNION - SELECT nv2.`item`, COUNT(1) AS "qty" FROM npc_vendor nv1 JOIN npc_vendor nv2 ON nv2.`entry` = -nv1.`item` AND nv1.`item` < 0 WHERE nv1.`ExtendedCost` IN (?a) AND `nv1`.`item` < 0 GROUP BY `item` UNION - SELECT `item`, COUNT(1) AS "qty" FROM game_event_npc_vendor genv JOIN creature c ON c.`guid` = genv.`guid` WHERE `ExtendedCost` IN (?a) GROUP BY `item`) n + FROM (SELECT `item`, COUNT(1) AS "qty" FROM npc_vendor WHERE `ExtendedCost` IN %in AND `item` > 0 GROUP BY `item` UNION + SELECT nv2.`item`, COUNT(1) AS "qty" FROM npc_vendor nv1 JOIN npc_vendor nv2 ON nv2.`entry` = -nv1.`item` AND nv1.`item` < 0 WHERE nv1.`ExtendedCost` IN %in AND `nv1`.`item` < 0 GROUP BY `item` UNION + SELECT `item`, COUNT(1) AS "qty" FROM game_event_npc_vendor genv JOIN creature c ON c.`guid` = genv.`guid` WHERE `ExtendedCost` IN %in GROUP BY `item`) n JOIN item_template it ON it.`entry` = n.`item` GROUP BY `item`'; foreach ($subSrcByXCost as $subSrc => $xCost) { - foreach (DB::World()->select($vendorQuery, $xCost, $xCost, $xCost) as $v) + foreach (DB::World()->selectAssoc($vendorQuery, $xCost, $xCost, $xCost) as $v) { if ($_ = $this->taughtSpell($v)) $this->pushBuffer(Type::SPELL, $_, SRC_PVP, $subSrc); @@ -435,20 +556,21 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #4 Quest', CLI::LOG_BLANK, true, true); - $quests = DB::World()->select( - 'SELECT n.`item` AS ARRAY_KEY, n.`Id` AS "quest", SUM(n.`qty`) AS "qty", BIT_OR(n.`side`) AS "side", IF(COUNT(DISTINCT `zone`) > 1, 0, `zone`) AS "zone", it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2` - FROM (SELECT `RewardChoiceItemID1` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardChoiceItemID1` > 0 GROUP BY `item` UNION - SELECT `RewardChoiceItemID2` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardChoiceItemID2` > 0 GROUP BY `item` UNION - SELECT `RewardChoiceItemID3` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardChoiceItemID3` > 0 GROUP BY `item` UNION - SELECT `RewardChoiceItemID4` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardChoiceItemID4` > 0 GROUP BY `item` UNION - SELECT `RewardChoiceItemID5` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardChoiceItemID5` > 0 GROUP BY `item` UNION - SELECT `RewardChoiceItemID6` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardChoiceItemID6` > 0 GROUP BY `item` UNION - SELECT `RewardItem1` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardItem1` > 0 GROUP BY `item` UNION - SELECT `RewardItem2` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardItem2` > 0 GROUP BY `item` UNION - SELECT `RewardItem3` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardItem3` > 0 GROUP BY `item` UNION - SELECT `RewardItem4` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `RewardItem4` > 0 GROUP BY `item` UNION - SELECT `StartItem` AS "item", `ID`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone" FROM quest_template WHERE `StartItem` > 0 GROUP BY `item`) n + $quests = DB::World()->selectAssoc( + 'SELECT n.`item` AS ARRAY_KEY, n.`ID` AS "quest", SUM(n.`qty`) AS "qty", BIT_OR(n.`side`) AS "side", IF(COUNT(DISTINCT `zone`) > 1, 0, `zone`) AS "zone", it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2` + FROM (SELECT `RewardChoiceItemID1` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardChoiceItemID1` > 0 UNION ALL + SELECT `RewardChoiceItemID2` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardChoiceItemID2` > 0 UNION ALL + SELECT `RewardChoiceItemID3` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardChoiceItemID3` > 0 UNION ALL + SELECT `RewardChoiceItemID4` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardChoiceItemID4` > 0 UNION ALL + SELECT `RewardChoiceItemID5` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardChoiceItemID5` > 0 UNION ALL + SELECT `RewardChoiceItemID6` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardChoiceItemID6` > 0 UNION ALL + SELECT `RewardItem1` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardItem1` > 0 UNION ALL + SELECT `RewardItem2` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardItem2` > 0 UNION ALL + SELECT `RewardItem3` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardItem3` > 0 UNION ALL + SELECT `RewardItem4` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardItem4` > 0 UNION ALL + SELECT `StartItem` AS "item", `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `StartItem` > 0) n JOIN item_template it ON it.`entry` = n.`item` + WHERE n.`ID` NOT IN %in GROUP BY `item`', ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, @@ -460,9 +582,10 @@ CLISetup::registerSetup("sql", new class extends SetupScript ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, - ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH + ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, + $this->disables[Type::QUEST] ?? [0] ); - $areaParent = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `parentArea` FROM ?_zones WHERE `id` IN (?a) AND `parentArea` > 0', array_filter(array_column($quests, 'zone'))); + $areaParent = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `parentArea` FROM ::zones WHERE `id` IN %in AND `parentArea` > 0', array_filter(array_column($quests, 'zone'))); foreach ($quests as $iId => $q) { @@ -475,8 +598,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript $this->pushBuffer(Type::ITEM, $iId, SRC_QUEST, $q['side'], $q['qty'] > 1 ? 0 : Type::QUEST, $q['quest'], $areaParent[$q['zone']] ?? Game::$questSortFix[$q['zone']] ?? $q['zone']); } - $mailLoot = DB::World()->select( - 'SELECT IF(mlt.`Reference` > 0, -mlt.`Reference`, mlt.`Item`) AS ARRAY_KEY, qt.`Id` AS "entry", it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone", BIT_OR(IF(qt.`AllowableRaces` & ?d AND NOT (qt.`AllowableRaces` & ?d), ?d, IF(qt.`AllowableRaces` & ?d AND NOT (qt.`AllowableRaces` & ?d), ?d, ?d))) AS "side" + $mailLoot = DB::World()->selectAssoc( + 'SELECT IF(mlt.`Reference` > 0, -mlt.`Reference`, mlt.`Item`) AS ARRAY_KEY, qt.`Id` AS "entry", it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT mlt.`Reference`) AS "qty", IF(COUNT(DISTINCT `QuestSortID`) > 1, 0, GREATEST(`QuestSortID`, 0)) AS "zone", BIT_OR(IF(qt.`AllowableRaces` & %i AND NOT (qt.`AllowableRaces` & %i), %i, IF(qt.`AllowableRaces` & %i AND NOT (qt.`AllowableRaces` & %i), %i, %i))) AS "side" FROM mail_loot_template mlt JOIN quest_template_addon qta ON qta.`RewardMailTemplateId` = mlt.`entry` JOIN quest_template qt ON qt.`ID` = qta.`ID` @@ -486,7 +609,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH ); - $areaParent = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `parentArea` FROM ?_zones WHERE `id` IN (?a) AND `parentArea` > 0', array_filter(array_column($mailLoot, 'zone'))); + $areaParent = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `parentArea` FROM ::zones WHERE `id` IN %in AND `parentArea` > 0', array_filter(array_column($mailLoot, 'zone'))); foreach ($mailLoot as $roi => $l) { @@ -519,18 +642,18 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #5 Vendor', CLI::LOG_BLANK, true, true); - $xCostIds = DB::Aowow()->selectCol('SELECT `id` FROM dbc_itemextendedcost WHERE `reqHonorPoints` <> 0 OR `reqArenaPoints` <> 0 OR `reqItemId1` IN (?a) OR `reqItemId2` IN (?a) OR `reqItemId3` IN (?a) OR `reqItemId4` IN (?a) OR `reqItemId5` IN (?a)', self::PVP_MONEY, self::PVP_MONEY, self::PVP_MONEY, self::PVP_MONEY, self::PVP_MONEY); - $vendors = DB::World()->select( + $xCostIds = DB::Aowow()->selectCol('SELECT `id` FROM dbc_itemextendedcost WHERE `reqHonorPoints` <> 0 OR `reqArenaPoints` <> 0 OR `reqItemId1` IN %in OR `reqItemId2` IN %in OR `reqItemId3` IN %in OR `reqItemId4` IN %in OR `reqItemId5` IN %in', self::PVP_MONEY, self::PVP_MONEY, self::PVP_MONEY, self::PVP_MONEY, self::PVP_MONEY); + $vendors = DB::World()->selectAssoc( 'SELECT n.`item`, n.`npc`, SUM(n.`qty`) AS "qty", it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2` - FROM (SELECT `item`, `entry` AS "npc", COUNT(1) AS "qty" FROM npc_vendor WHERE `ExtendedCost` NOT IN (?a) AND `item` > 0 GROUP BY `item`, `npc` UNION - SELECT nv2.`item`, nv1.`entry` AS "npc", COUNT(1) AS "qty" FROM npc_vendor nv1 JOIN npc_vendor nv2 ON nv2.`entry` = -nv1.`item` AND nv1.`item` < 0 WHERE nv1.`ExtendedCost` NOT IN (?a) AND `nv1`.`item` < 0 GROUP BY `item`, `npc` UNION - SELECT `item`, c.`id` AS "npc", COUNT(1) AS "qty" FROM game_event_npc_vendor genv JOIN creature c ON c.`guid` = genv.`guid` WHERE `ExtendedCost` NOT IN (?a) GROUP BY `item`, `npc`) n + FROM (SELECT `item`, `entry` AS "npc", COUNT(1) AS "qty" FROM npc_vendor WHERE `ExtendedCost` NOT IN %in AND `item` > 0 GROUP BY `item`, `npc` UNION + SELECT nv2.`item`, nv1.`entry` AS "npc", COUNT(1) AS "qty" FROM npc_vendor nv1 JOIN npc_vendor nv2 ON nv2.`entry` = -nv1.`item` AND nv1.`item` < 0 WHERE nv1.`ExtendedCost` NOT IN %in AND `nv1`.`item` < 0 GROUP BY `item`, `npc` UNION + SELECT `item`, c.`id` AS "npc", COUNT(1) AS "qty" FROM game_event_npc_vendor genv JOIN creature c ON c.`guid` = genv.`guid` WHERE `ExtendedCost` NOT IN %in GROUP BY `item`, `npc`) n JOIN item_template it ON it.`entry` = n.`item` GROUP BY `item`, `npc`', $xCostIds, $xCostIds, $xCostIds ); - $spawns = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::NPC, array_column($vendors, 'npc')); + $spawns = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ::spawns WHERE `type` = %i AND `typeId`IN %in GROUP BY `typeId`', Type::NPC, array_column($vendors, 'npc')); foreach ($vendors as $v) { @@ -550,7 +673,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript $this->pushBuffer(Type::ITEM, $item, SRC_STARTER); for ($i = 1; $i < 21; $i++) - if ($cso = DB::Aowow()->selectCol('SELECT ?# FROM dbc_charstartoutfit WHERE ?# > 0', 'item'.$i, 'item'.$i)) + if ($cso = DB::Aowow()->selectCol('SELECT %n FROM dbc_charstartoutfit WHERE %n > 0', 'item'.$i, 'item'.$i)) foreach ($cso as $item) $this->pushBuffer(Type::ITEM, $item, SRC_STARTER); } @@ -559,8 +682,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #12 Achievement', CLI::LOG_BLANK, true, true); - $xItems = DB::Aowow()->select('SELECT `id` AS "entry", `itemExtra` AS ARRAY_KEY, COUNT(1) AS "qty" FROM ?_achievement WHERE `itemExtra` > 0 GROUP BY `itemExtra`'); - $rewItems = DB::World()->select( + $xItems = DB::Aowow()->selectAssoc('SELECT `id` AS "entry", `itemExtra` AS ARRAY_KEY, COUNT(1) AS "qty" FROM ::achievement WHERE `itemExtra` > 0 GROUP BY `itemExtra`'); + $rewItems = DB::World()->selectAssoc( 'SELECT src.`item` AS ARRAY_KEY, src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty" FROM (SELECT IFNULL(IF(mlt.`Reference` > 0, -mlt.`Reference`, mlt.`Item`), ar.`ItemID`) AS "item", ar.`ID` AS "entry" FROM achievement_reward ar @@ -574,7 +697,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[source] itemAchievement() - Reward items are unexpectedly empty.', CLI::LOG_WARN); else { - $extraItems = DB::World()->select('SELECT `entry` AS ARRAY_KEY, `class`, `subclass`, `spellid_1`, `spelltrigger_1`, `spellid_2`, `spelltrigger_2` FROM item_template WHERE `entry` IN (?a)', array_keys($xItems)); + $extraItems = DB::World()->selectAssoc('SELECT `entry` AS ARRAY_KEY, `class`, `subclass`, `spellid_1`, `spelltrigger_1`, `spellid_2`, `spelltrigger_2` FROM item_template WHERE `entry` IN %in', array_keys($xItems)); foreach ($extraItems as $iId => $l) { if ($_ = $this->taughtSpell($l)) @@ -597,8 +720,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #15 Disenchanted', CLI::LOG_BLANK, true, true); - $deLoot = DB::World()->select( - 'SELECT IF(dlt.`Reference` > 0, -dlt.`Reference`, dlt.`Item`) AS "refOrItem", itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(1) AS "qty" + $deLoot = DB::World()->selectAssoc( + 'SELECT IF(dlt.`Reference` > 0, -dlt.`Reference`, dlt.`Item`) AS "refOrItem", itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(DISTINCT dlt.`Reference`) AS "qty" FROM disenchant_loot_template dlt JOIN item_template itA ON dlt.`entry` = itA.`DisenchantId` LEFT JOIN item_template itB ON itB.`entry` = dlt.`Item` AND dlt.`Reference` <= 0 @@ -634,10 +757,10 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #16 Fished', CLI::LOG_BLANK, true, true); - $fishLoot = DB::World()->select( - 'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty", IF(COUNT(DISTINCT `zone`) > 2, 0, MAX(`zone`)) AS "zone" + $fishLoot = DB::World()->selectAssoc( + 'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT src.`itemOrRef`) AS "qty", IF(COUNT(DISTINCT `zone`) > 2, 0, MAX(`zone`)) AS "zone" FROM (SELECT 0 AS "entry", IF(flt.`Reference` > 0, -flt.`Reference`, flt.`Item`) AS "itemOrRef", `entry` AS "zone" FROM fishing_loot_template flt UNION - SELECT gt.`entry`, IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) AS "itemOrRef", 0 AS "zone" FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.`entry` = gt.`data1` WHERE `type` = ?d AND gt.`data1` > 0) src + SELECT gt.`entry`, IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) AS "itemOrRef", 0 AS "zone" FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.`entry` = gt.`data1` WHERE `type` = %i AND gt.`data1` > 0) src LEFT JOIN item_template it ON src.`itemOrRef` > 0 AND src.`itemOrRef` = it.`entry` GROUP BY `refOrItem`, src.`entry`', OBJECT_FISHINGHOLE @@ -649,14 +772,14 @@ CLISetup::registerSetup("sql", new class extends SetupScript return; } - $goSpawns = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::OBJECT, array_filter(array_column($fishLoot, 'entry'))); - $areaParent = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `parentArea` FROM ?_zones WHERE `id` IN (?a) AND `parentArea` > 0', array_filter(array_column($fishLoot, 'zone'))); + $goSpawns = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ::spawns WHERE `type` = %i AND `typeId`IN %in GROUP BY `typeId`', Type::OBJECT, array_filter(array_column($fishLoot, 'entry'))); + $areaParent = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `parentArea` FROM ::zones WHERE `id` IN %in AND `parentArea` > 0', array_filter(array_column($fishLoot, 'zone'))); foreach ($fishLoot as $l) { $roi = $l['refOrItem']; $l['zone'] = $areaParent[$l['zone']] ?? $l['zone']; - $zoneId = $goSpawns[$l['entry']] ?? 0; + $zoneId = $goSpawns[$l['entry']] ?? null; if ($l['zone'] != $zoneId) $zoneId = 0; @@ -684,14 +807,14 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #17 Gathered', CLI::LOG_BLANK, true, true); - $herbLoot = DB::World()->select( - 'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty", src.`srcType` - FROM (SELECT ct.`entry`, IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) `itemOrRef`, ?d AS "srcType" FROM creature_template ct JOIN skinning_loot_template slt ON slt.`entry` = ct.`skinloot` WHERE (`type_flags` & ?d) AND ct.`skinloot` > 0 UNION - SELECT gt.`entry`, IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) `itemOrRef`, ?d AS "srcType" FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.`entry` = gt.`data1` WHERE gt.`type` = ?d AND gt.`data1` > 0 AND gt.`data0` IN (?a)) src + $herbLoot = DB::World()->selectAssoc( + 'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT src.`itemOrRef`) AS "qty", src.`srcType` + FROM (SELECT ct.`entry`, IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) `itemOrRef`, %i AS "srcType" FROM creature_template ct JOIN skinning_loot_template slt ON slt.`entry` = ct.`skinloot` WHERE (`type_flags` & %i) AND ct.`skinloot` > 0 UNION + SELECT gt.`entry`, IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) `itemOrRef`, %i AS "srcType" FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.`entry` = gt.`data1` WHERE gt.`type` = %i AND gt.`data1` > 0 AND gt.`data0` IN %in) src LEFT JOIN item_template it ON src.itemOrRef > 0 AND src.`itemOrRef` = it.`entry` GROUP BY `refOrItem`, src.`entry`', Type::NPC, NPC_TYPEFLAG_SKIN_WITH_HERBALISM, - Type::OBJECT, OBJECT_CHEST, DB::Aowow()->selectCol('SELECT `id` FROM dbc_lock WHERE `properties1` = ?d', LOCK_PROPERTY_HERBALISM) + Type::OBJECT, OBJECT_CHEST, DB::Aowow()->selectCol('SELECT `id` FROM dbc_lock WHERE `properties1` = %i', LOCK_PROPERTY_HERBALISM) ); if (!$herbLoot) @@ -700,8 +823,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript return; } - $spawns[Type::OBJECT] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId`', Type::OBJECT, array_column(array_filter($herbLoot, function($x) { return $x['srcType'] == Type::OBJECT; }), 'entry')); - $spawns[Type::NPC] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId` IN (?a) GROUP BY `typeId`', Type::NPC, array_column(array_filter($herbLoot, function($x) { return $x['srcType'] == Type::NPC; }), 'entry')); + $spawns[Type::OBJECT] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ::spawns WHERE `type` = %i AND `typeId` IN %in GROUP BY `typeId`', Type::OBJECT, array_column(array_filter($herbLoot, fn($x) => $x['srcType'] == Type::OBJECT), 'entry')); + $spawns[Type::NPC] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ::spawns WHERE `type` = %i AND `typeId` IN %in GROUP BY `typeId`', Type::NPC, array_column(array_filter($herbLoot, fn($x) => $x['srcType'] == Type::NPC), 'entry')); foreach ($herbLoot as $l) { @@ -711,7 +834,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript if (isset($this->dummyNPCs[$l['entry']]) && $l['srcType'] == Type::NPC) [$mode, $entry] = $this->dummyNPCs[$l['entry']]; - $zoneId = $spawns[$l['srcType']][$l['entry']] ?? 0; + $zoneId = $spawns[$l['srcType']][$l['entry']] ?? null; if ($roi < 0 && !empty($this->refLoot[-$roi])) { @@ -737,8 +860,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #18 Milled', CLI::LOG_BLANK, true, true); - $millLoot = DB::World()->select( - 'SELECT IF(mlt.`Reference` > 0, -mlt.`Reference`, mlt.`Item`) AS "refOrItem", itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(1) AS "qty" + $millLoot = DB::World()->selectAssoc( + 'SELECT IF(mlt.`Reference` > 0, -mlt.`Reference`, mlt.`Item`) AS "refOrItem", itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(DISTINCT mlt.`Reference`) AS "qty" FROM milling_loot_template mlt JOIN item_template itA ON mlt.`entry` = itA.`entry` LEFT JOIN item_template itB ON itB.`entry` = mlt.`Item` AND mlt.`Reference` <= 0 @@ -773,14 +896,14 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #19 Mined', CLI::LOG_BLANK, true, true); - $mineLoot = DB::World()->select( - 'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty", src.`srcType` - FROM (SELECT ct.`entry`, IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) `itemOrRef`, ?d AS "srcType" FROM creature_template ct JOIN skinning_loot_template slt ON slt.`entry` = ct.`skinloot` WHERE (`type_flags` & ?d) AND ct.`skinloot` > 0 UNION - SELECT gt.`entry`, IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) `itemOrRef`, ?d AS "srcType" FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.`entry` = gt.`data1` WHERE gt.`type` = ?d AND gt.`data1` > 0 AND gt.`data0` IN (?a)) src + $mineLoot = DB::World()->selectAssoc( + 'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT src.`itemOrRef`) AS "qty", src.`srcType` + FROM (SELECT ct.`entry`, IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) `itemOrRef`, %i AS "srcType" FROM creature_template ct JOIN skinning_loot_template slt ON slt.`entry` = ct.`skinloot` WHERE (`type_flags` & %i) AND ct.`skinloot` > 0 UNION + SELECT gt.`entry`, IF(glt.`Reference` > 0, -glt.`Reference`, glt.`Item`) `itemOrRef`, %i AS "srcType" FROM gameobject_template gt JOIN gameobject_loot_template glt ON glt.`entry` = gt.`data1` WHERE gt.`type` = %i AND gt.`data1` > 0 AND gt.`data0` IN %in) src LEFT JOIN item_template it ON src.itemOrRef > 0 AND src.`itemOrRef` = it.`entry` GROUP BY `refOrItem`, src.`entry`', Type::NPC, NPC_TYPEFLAG_SKIN_WITH_MINING, - Type::OBJECT, OBJECT_CHEST, DB::Aowow()->selectCol('SELECT `id` FROM dbc_lock WHERE `properties1` = ?d', LOCK_PROPERTY_MINING) + Type::OBJECT, OBJECT_CHEST, DB::Aowow()->selectCol('SELECT `id` FROM dbc_lock WHERE `properties1` = %i', LOCK_PROPERTY_MINING) ); if (!$mineLoot) @@ -789,8 +912,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript return; } - $spawns[Type::OBJECT] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::OBJECT, array_column(array_filter($mineLoot, function($x) { return $x['srcType'] == Type::OBJECT; }), 'entry')); - $spawns[Type::NPC] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::NPC, array_column(array_filter($mineLoot, function($x) { return $x['srcType'] == Type::NPC; }), 'entry')); + $spawns[Type::OBJECT] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ::spawns WHERE `type` = %i AND `typeId`IN %in GROUP BY `typeId`', Type::OBJECT, array_column(array_filter($mineLoot, fn($x) => $x['srcType'] == Type::OBJECT), 'entry')); + $spawns[Type::NPC] = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ::spawns WHERE `type` = %i AND `typeId`IN %in GROUP BY `typeId`', Type::NPC, array_column(array_filter($mineLoot, fn($x) => $x['srcType'] == Type::NPC), 'entry')); foreach ($mineLoot as $l) { @@ -800,7 +923,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript if (isset($this->dummyNPCs[$l['entry']]) && $l['srcType'] == Type::NPC) [$mode, $entry] = $this->dummyNPCs[$l['entry']]; - $zoneId = $spawns[$l['srcType']][$l['entry']] ?? 0; + $zoneId = $spawns[$l['srcType']][$l['entry']] ?? null; if ($roi < 0 && !empty($this->refLoot[-$roi])) { @@ -826,8 +949,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #20 Prospected', CLI::LOG_BLANK, true, true); - $prospectLoot = DB::World()->select( - 'SELECT IF(plt.`Reference` > 0, -plt.`Reference`, plt.`Item`) AS "refOrItem", itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(1) AS "qty" + $prospectLoot = DB::World()->selectAssoc( + 'SELECT IF(plt.`Reference` > 0, -plt.`Reference`, plt.`Item`) AS "refOrItem", itA.`entry`, itB.`class`, itB.`subclass`, itB.`spellid_1`, itB.`spelltrigger_1`, itB.`spellid_2`, itB.`spelltrigger_2`, COUNT(DISTINCT plt.`Reference`) AS "qty" FROM prospecting_loot_template plt JOIN item_template itA ON plt.`entry` = itA.`entry` LEFT JOIN item_template itB ON itB.`entry` = plt.`Item` AND plt.`Reference` <= 0 @@ -862,8 +985,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #21 Pickpocket', CLI::LOG_BLANK, true, true); - $theftLoot = DB::World()->select( - 'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty" + $theftLoot = DB::World()->selectAssoc( + 'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT src.`itemOrRef`) AS "qty" FROM (SELECT ct.`entry`, IF(plt.`Reference` > 0, -plt.`Reference`, plt.`Item`) `itemOrRef` FROM creature_template ct JOIN pickpocketing_loot_template plt ON plt.`entry` = ct.`pickpocketloot` WHERE ct.`pickpocketloot` > 0) src LEFT JOIN item_template it ON src.`itemOrRef` > 0 AND src.`itemOrRef` = it.`entry` GROUP BY `refOrItem`, src.`entry`' @@ -875,7 +998,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript return; } - $spawns = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::NPC, array_filter(array_column($theftLoot, 'entry'))); + $spawns = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ::spawns WHERE `type` = %i AND `typeId`IN %in GROUP BY `typeId`', Type::NPC, array_filter(array_column($theftLoot, 'entry'))); foreach ($theftLoot as $l) { @@ -885,7 +1008,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript if (isset($this->dummyNPCs[$l['entry']])) [$mode, $entry] = $this->dummyNPCs[$l['entry']]; - $zoneId = $spawns[$l['entry']] ?? 0; + $zoneId = $spawns[$l['entry']] ?? null; if ($roi < 0 && !empty($this->refLoot[-$roi])) { @@ -911,9 +1034,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #22 Salvaged', CLI::LOG_BLANK, true, true); - $salvageLoot = DB::World()->select( - 'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty" - FROM (SELECT ct.`entry`, IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) `itemOrRef` FROM creature_template ct JOIN skinning_loot_template slt ON slt.`entry` = ct.`skinloot` WHERE (`type_flags` & ?d) AND ct.`skinloot` > 0) src + $salvageLoot = DB::World()->selectAssoc( + 'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT src.`itemOrRef`) AS "qty" + FROM (SELECT ct.`entry`, IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) `itemOrRef` FROM creature_template ct JOIN skinning_loot_template slt ON slt.`entry` = ct.`skinloot` WHERE (`type_flags` & %i) AND ct.`skinloot` > 0) src LEFT JOIN item_template it ON src.`itemOrRef` > 0 AND src.`itemOrRef` = it.`entry` GROUP BY `refOrItem`, src.`entry`', NPC_TYPEFLAG_SKIN_WITH_ENGINEERING @@ -925,7 +1048,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript return; } - $spawns = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::NPC, array_filter(array_column($salvageLoot, 'entry'))); + $spawns = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ::spawns WHERE `type` = %i AND `typeId`IN %in GROUP BY `typeId`', Type::NPC, array_filter(array_column($salvageLoot, 'entry'))); foreach ($salvageLoot as $l) { @@ -935,7 +1058,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript if (isset($this->dummyNPCs[$l['entry']])) [$mode, $entry] = $this->dummyNPCs[$l['entry']]; - $zoneId = $spawns[$l['entry']] ?? 0; + $zoneId = $spawns[$l['entry']] ?? null; if ($roi < 0 && !empty($this->refLoot[-$roi])) { @@ -961,9 +1084,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #23 Skinned', CLI::LOG_BLANK, true, true); - $skinLoot = DB::World()->select( - 'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(1) AS "qty" - FROM (SELECT ct.`entry`, IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) `itemOrRef` FROM creature_template ct JOIN skinning_loot_template slt ON slt.`entry` = ct.`skinloot` WHERE (`type_flags` & ?d) = 0 AND ct.`skinloot` > 0 AND ct.`type` <> 13) src + $skinLoot = DB::World()->selectAssoc( + 'SELECT src.`itemOrRef` AS "refOrItem", src.`entry`, it.`class`, it.`subclass`, it.`spellid_1`, it.`spelltrigger_1`, it.`spellid_2`, it.`spelltrigger_2`, COUNT(DISTINCT src.`itemOrRef`) AS "qty" + FROM (SELECT ct.`entry`, IF(slt.`Reference` > 0, -slt.`Reference`, slt.`Item`) `itemOrRef` FROM creature_template ct JOIN skinning_loot_template slt ON slt.`entry` = ct.`skinloot` WHERE (`type_flags` & %i) = 0 AND ct.`skinloot` > 0 AND ct.`type` <> 13) src LEFT JOIN item_template it ON src.`itemOrRef` > 0 AND src.`itemOrRef` = it.`entry` GROUP BY `refOrItem`, src.`entry`', NPC_TYPEFLAG_SPECIALLOOT @@ -975,7 +1098,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript return; } - $spawns = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ?_spawns WHERE `type` = ?d AND `typeId`IN (?a) GROUP BY `typeId`', Type::NPC, array_filter(array_column($skinLoot, 'entry'))); + $spawns = DB::Aowow()->selectCol('SELECT `typeId` AS ARRAY_KEY, IF(COUNT(DISTINCT `areaId`) > 1, 0, `areaId`) FROM ::spawns WHERE `type` = %i AND `typeId`IN %in GROUP BY `typeId`', Type::NPC, array_filter(array_column($skinLoot, 'entry'))); foreach ($skinLoot as $l) { @@ -985,7 +1108,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript if (isset($this->dummyNPCs[$l['entry']])) [$mode, $entry] = $this->dummyNPCs[$l['entry']]; - $zoneId = $spawns[$l['entry']] ?? 0; + $zoneId = $spawns[$l['entry']] ?? null; if ($roi < 0 && !empty($this->refLoot[-$roi])) { @@ -1011,13 +1134,15 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #4 Quest', CLI::LOG_BLANK, true, true); - $quests = DB::World()->select( - 'SELECT `spell` AS ARRAY_KEY, `id`, SUM(`qty`) AS "qty", BIT_OR(`side`) AS "side", IF(COUNT(DISTINCT `zone`) > 1, 0, `zone`) AS "zone" - FROM (SELECT IF(`RewardSpell` = 0, `RewardDisplaySpell`, `RewardSpell`) AS "spell", `Id`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE IF(`RewardSpell` = 0, `RewardDisplaySpell`, `RewardSpell`) > 0 GROUP BY `spell` UNION - SELECT qta.`SourceSpellId` AS "spell", qt.`Id`, COUNT(1) AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template qt JOIN quest_template_addon qta ON qta.ID = qt.ID WHERE qta.`SourceSpellId` > 0 GROUP BY `spell`) t + $quests = DB::World()->selectAssoc( + 'SELECT `spell` AS ARRAY_KEY, `ID` AS "id", SUM(`qty`) AS "qty", BIT_OR(`side`) AS "side", IF(COUNT(DISTINCT `zone`) > 1, 0, `zone`) AS "zone" + FROM (SELECT IF(`RewardSpell` = 0, `RewardDisplaySpell`, `RewardSpell`) AS "spell", `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE IF(`RewardSpell` = 0, `RewardDisplaySpell`, `RewardSpell`) > 0 UNION ALL + SELECT qta.`SourceSpellId` AS "spell", qt.`ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template qt JOIN quest_template_addon qta ON qta.ID = qt.ID WHERE qta.`SourceSpellId` > 0 ) t + WHERE t.`ID` NOT IN %in GROUP BY `spell`', ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, - ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH + ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, + $this->disables[Type::QUEST] ?? [0] ); if (!$quests) @@ -1026,8 +1151,8 @@ CLISetup::registerSetup("sql", new class extends SetupScript return; } - $areaParent = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `parentArea` FROM ?_zones WHERE `id` IN (?a) AND `parentArea` > 0', array_filter(array_column($quests, 'zone'))); - $qSpells = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `effect1Id`, `effect2Id`, `effect3Id`, `effect1TriggerSpell`, `effect2TriggerSpell`, `effect3TriggerSpell` FROM dbc_spell WHERE `id` IN (?a) AND (`effect1Id` = ?d OR `effect2Id` = ?d OR `effect3Id` = ?d)', + $areaParent = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `parentArea` FROM ::zones WHERE `id` IN %in AND `parentArea` > 0', array_filter(array_column($quests, 'zone'))); + $qSpells = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `effect1Id`, `effect2Id`, `effect3Id`, `effect1TriggerSpell`, `effect2TriggerSpell`, `effect3TriggerSpell` FROM dbc_spell WHERE `id` IN %in AND (`effect1Id` = %i OR `effect2Id` = %i OR `effect3Id` = %i)', array_keys($quests), SPELL_EFFECT_LEARN_SPELL, SPELL_EFFECT_LEARN_SPELL, SPELL_EFFECT_LEARN_SPELL ); @@ -1041,7 +1166,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #6 Trainer', CLI::LOG_BLANK, true, true); - $tNpcs = DB::World()->select('SELECT `SpellID` AS ARRAY_KEY, cdt.`CreatureId` AS "entry", COUNT(1) AS "qty" FROM trainer_spell ts JOIN creature_default_trainer cdt ON cdt.`TrainerId` = ts.`TrainerId` GROUP BY ARRAY_KEY'); + $tNpcs = DB::World()->selectAssoc('SELECT `SpellID` AS ARRAY_KEY, cdt.`CreatureId` AS "entry", COUNT(1) AS "qty" FROM trainer_spell ts JOIN creature_default_trainer cdt ON cdt.`TrainerId` = ts.`TrainerId` GROUP BY ARRAY_KEY'); if (!$tNpcs) { CLI::write('[source] spelltrainer() - trainer_spell contained no spell or creature_default_trainer no trainer.', CLI::LOG_WARN); @@ -1050,7 +1175,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript // note: for consistency you could check for boss dummys and get the zone where the trainer resides, but seriously. Whats wrong with you‽ - $tSpells = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `effect1Id`, `effect2Id`, `effect3Id`, `effect1TriggerSpell`, `effect2TriggerSpell`, `effect3TriggerSpell` FROM dbc_spell WHERE `id` IN (?a)', array_keys($tNpcs)); + $tSpells = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `effect1Id`, `effect2Id`, `effect3Id`, `effect1TriggerSpell`, `effect2TriggerSpell`, `effect3TriggerSpell` FROM dbc_spell WHERE `id` IN %in', array_keys($tNpcs)); // todo (med): this skips some spells (e.g. riding) foreach ($tNpcs as $spellId => $npc) @@ -1081,7 +1206,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[source] * #7 Discovery', CLI::LOG_BLANK, true, true); // 61756: Northrend Inscription Research (FAST QA VERSION); - if ($disco = DB::World()->selectCol('SELECT `spellId` FROM skill_discovery_template WHERE `reqSpell` <> ?d', 61756)) + if ($disco = DB::World()->selectCol('SELECT `spellId` FROM skill_discovery_template WHERE `reqSpell` <> %i', 61756)) foreach ($disco as $d) $this->pushBuffer(Type::SPELL, $d, SRC_DISCOVERY); } @@ -1090,11 +1215,11 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #9 Talent', CLI::LOG_BLANK, true, true); - $tSpells = DB::Aowow()->select( + $tSpells = DB::Aowow()->selectAssoc( 'SELECT s.`id` AS ARRAY_KEY, s.`effect1Id`, s.`effect2Id`, s.`effect3Id`, s.`effect1TriggerSpell`, s.`effect2TriggerSpell`, s.`effect3TriggerSpell` FROM dbc_talent t JOIN dbc_spell s ON s.`id` = t.`rank1` - WHERE t.`rank2` < 1 AND (t.`talentSpell` = 1 OR (s.`effect1Id` = ?d OR s.`effect2Id` = ?d OR s.`effect3Id` = ?d))', + WHERE t.`rank2` < 1 AND (t.`talentSpell` = 1 OR (s.`effect1Id` = %i OR s.`effect2Id` = %i OR s.`effect3Id` = %i))', SPELL_EFFECT_LEARN_SPELL, SPELL_EFFECT_LEARN_SPELL, SPELL_EFFECT_LEARN_SPELL ); @@ -1120,7 +1245,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript if (!$recurse) break; - $tSpells = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `effect1Id`, `effect2Id`, `effect3Id`, `effect1TriggerSpell`, `effect2TriggerSpell`, `effect3TriggerSpell` FROM dbc_spell WHERE `id` IN (?a)', array_keys($recurse)); + $tSpells = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `effect1Id`, `effect2Id`, `effect3Id`, `effect1TriggerSpell`, `effect2TriggerSpell`, `effect3TriggerSpell` FROM dbc_spell WHERE `id` IN %in', array_keys($recurse)); } } @@ -1133,7 +1258,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[source] * #10 Starter', CLI::LOG_BLANK, true, true); $pcis = DB::World()->selectCol('SELECT DISTINCT `skill` FROM playercreateinfo_skills'); - $subSkills = DB::Aowow()->selectCol('SELECT `spellId` FROM dbc_skilllineability WHERE {(`skillLineId` IN (?a) AND `acquireMethod` = 2) OR} (`acquireMethod` = 1 AND (`reqSkillLevel` = 1 OR `skillLineId` = ?d)) GROUP BY `spellId`', $pcis ?: DBSIMPLE_SKIP, SKILL_FIRST_AID); + $subSkills = DB::Aowow()->selectCol('SELECT `spellId` FROM dbc_skilllineability WHERE %if', $pcis, '(`skillLineId` IN %in AND `acquireMethod` = 2) OR', $pcis, '%end (`acquireMethod` = 1 AND (`reqSkillLevel` = 1 OR `skillLineId` = %i)) GROUP BY `spellId`', SKILL_FIRST_AID); foreach ($subSkills as $s) $this->pushBuffer(Type::SPELL, $s, SRC_STARTER); } @@ -1142,11 +1267,13 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #4 Quest', CLI::LOG_BLANK, true, true); - $quests = DB::World()->select( + $quests = DB::World()->selectAssoc( 'SELECT `RewardTitle` AS ARRAY_KEY, `ID` AS "id", SUM(`qty`) AS "qty", BIT_OR(`side`) AS "side", IF(COUNT(DISTINCT `zone`) > 1, 0, `zone`) AS "zone" - FROM (SELECT `RewardTitle`, `ID`, 1 AS "qty", IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, IF(`AllowableRaces` & ?d AND NOT (`AllowableRaces` & ?d), ?d, ?d)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardTitle` > 0) q + FROM (SELECT `RewardTitle`, `ID`, 1 AS "qty", IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, IF(`AllowableRaces` & %i AND NOT (`AllowableRaces` & %i), %i, %i)) AS "side", GREATEST(`QuestSortID`, 0) AS "zone" FROM quest_template WHERE `RewardTitle` > 0) q + WHERE q.`Id` NOT IN %in GROUP BY `RewardTitle`', - ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH + ChrRace::MASK_HORDE, ChrRace::MASK_ALLIANCE, SIDE_HORDE, ChrRace::MASK_ALLIANCE, ChrRace::MASK_HORDE, SIDE_ALLIANCE, SIDE_BOTH, + $this->disables[Type::QUEST] ?? [0] ); if (!$quests) @@ -1155,7 +1282,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript return; } - $areaParent = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `parentArea` FROM ?_zones WHERE `id` IN (?a) AND parentArea > 0', array_filter(array_column($quests, 'zone'))); + $areaParent = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, `parentArea` FROM ::zones WHERE `id` IN %in AND parentArea > 0', array_filter(array_column($quests, 'zone'))); foreach ($quests as $titleId => $q) $this->pushBuffer(Type::TITLE, $titleId, SRC_QUEST, $q['side'], $q['qty'] > 1 ? 0 : Type::QUEST, $q['id'], $areaParent[$q['zone']] ?? Game::$questSortFix[$q['zone']] ?? $q['zone']); @@ -1165,7 +1292,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript { CLI::write('[source] * #12 Achievement', CLI::LOG_BLANK, true, true); - $sets = DB::World()->select( + $sets = DB::World()->selectAssoc( 'SELECT `titleId` AS ARRAY_KEY, MIN(`ID`) AS "srcId", NULLIF(MAX(`ID`), MIN(`ID`)) AS "altSrcId" FROM (SELECT `TitleA` AS "titleId", `ID` FROM achievement_reward WHERE `TitleA` <> 0 UNION SELECT `TitleH` AS "titleId", `ID` FROM achievement_reward WHERE `TitleH` <> 0) AS x @@ -1177,7 +1304,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript $this->pushBuffer(Type::TITLE, $tId, SRC_ACHIEVEMENT, 1, Type::ACHIEVEMENT, $set['srcId']); if ($set['altSrcId']) - DB::Aowow()->query('UPDATE ?_titles SET src12Ext = ?d WHERE id = ?d', $set['altSrcId'], $tId); + DB::Aowow()->qry('UPDATE ::titles SET src12Ext = %i WHERE id = %i', $set['altSrcId'], $tId); } } @@ -1191,9 +1318,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript private function itemset() : void { - // every item in ?_itemset needs a source. if so merge fields. if not it's not available. + // every item in ::itemset needs a source. if so merge fields. if not it's not available. - $sets = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `contentGroup`, `item1`, `item2`, `item3`, `item4`, `item5`, `item6`, `item7`, `item8`, `item9`, `item10` FROM ?_itemset'); + $sets = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `contentGroup`, `item1`, `item2`, `item3`, `item4`, `item5`, `item6`, `item7`, `item8`, `item9`, `item10` FROM ::itemset'); $metaSrc = []; foreach ($sets as $id => $set) diff --git a/setup/tools/sqlgen/spawns.ss.php b/setup/tools/sqlgen/spawns.ss.php index f139a424..e6ccd4a2 100644 --- a/setup/tools/sqlgen/spawns.ss.php +++ b/setup/tools/sqlgen/spawns.ss.php @@ -24,23 +24,25 @@ CLISetup::registerSetup("sql", new class extends SetupScript ); protected $dbcSourceFiles = ['worldmaparea', 'map', 'taxipathnode', 'soundemitters', 'areatrigger', 'areatable']; - protected $worldDependency = ['creature', 'creature_addon', 'gameobject', 'gameobject_template', 'vehicle_accessory', 'vehicle_accessory_template', 'script_waypoint', 'waypoints', 'waypoint_data', 'smart_scripts', 'areatrigger_teleport']; + protected $worldDependency = ['creature', 'creature_addon', 'creature_template_addon', 'gameobject', 'gameobject_template', 'vehicle_accessory', 'vehicle_accessory_template', 'waypoint_data', 'smart_scripts', 'areatrigger_teleport']; protected $setupAfter = [['dungeonmap', 'worldmaparea', 'zones'], ['img-maps']]; - private $transports = []; - private $overrideData = []; + private array $transports = []; + private array $overrideData = []; + private array $mapToArea = []; + private array $areaParents = []; private $steps = array( - 0x01 => ['creature', Type::NPC, false, '`creature` spawns', ], - 0x02 => ['gameobject', Type::OBJECT, false, '`gameobject` spawns', ], - 0x04 => ['soundemitter', Type::SOUND, false, 'SoundEmitters.dbc positions', ], - 0x08 => ['areatrigger', Type::AREATRIGGER, false, 'AreaTrigger.dbc positions and teleporter endpoints' ], - 0x10 => ['instances', Type::ZONE, false, 'Map.dbc instance portals positions' ], - 0x20 => ['waypoints', Type::NPC, true, 'NPC waypoints from `script_waypoint`, `waypoints` & `waypoint_data`'], + 0x01 => ['creature', Type::NPC, false, '`creature` spawns', ], + 0x02 => ['gameobject', Type::OBJECT, false, '`gameobject` spawns', ], + 0x04 => ['soundemitter', Type::SOUND, false, 'SoundEmitters.dbc positions', ], + 0x08 => ['areatrigger', Type::AREATRIGGER, false, 'AreaTrigger.dbc positions and teleporter endpoints'], + 0x10 => ['instances', Type::ZONE, false, 'Map.dbc instance portals positions' ], + 0x20 => ['waypoints', Type::NPC, true, 'NPC waypoints from `waypoint_data`' ] ); - public function generate(array $ids = []) : bool + public function generate() : bool { /*****************************/ /* find out what to generate */ @@ -74,30 +76,32 @@ CLISetup::registerSetup("sql", new class extends SetupScript /*********************************/ if (!$todoMask || ($todoMask & 0x1F) == 0x1F) - DB::Aowow()->query('TRUNCATE TABLE ?_spawns'); + DB::Aowow()->qry('TRUNCATE TABLE ::spawns'); else foreach ($this->steps as $idx => [, $type, $isWP, ]) if (($idx & $todoMask) && !$isWP) - DB::Aowow()->query('DELETE FROM ?_spawns WHERE `type` = ?d', $type); + DB::Aowow()->qry('DELETE FROM ::spawns WHERE `type` = %i', $type); if (!$todoMask || ($todoMask & 0x20)) - DB::Aowow()->query('TRUNCATE TABLE ?_creature_waypoints'); + DB::Aowow()->qry('TRUNCATE TABLE ::creature_waypoints'); /**************************/ /* offsets for transports */ /**************************/ - $this->transports = DB::World()->selectCol('SELECT `data0` AS `pathId`, `data6` AS ARRAY_KEY FROM gameobject_template WHERE `type` = ?d AND `data6` <> 0', OBJECT_MO_TRANSPORT); + $this->transports = DB::World()->selectCol('SELECT `data0` AS `pathId`, `data6` AS ARRAY_KEY FROM gameobject_template WHERE `type` = %i AND `data6` <> 0', OBJECT_MO_TRANSPORT); foreach ($this->transports as &$t) - $t = DB::Aowow()->selectRow('SELECT `posX`, `posY`, `mapId` FROM dbc_taxipathnode tpn WHERE tpn.`pathId` = ?d AND `nodeIdx` = 0', $t); + $t = DB::Aowow()->selectRow('SELECT `posX`, `posY`, `mapId` FROM dbc_taxipathnode tpn WHERE tpn.`pathId` = %i AND `nodeIdx` = 0', $t); /*********************/ /* get override data */ /*********************/ - $this->overrideData = DB::Aowow()->select('SELECT `type` AS ARRAY_KEY, `typeGuid` AS ARRAY_KEY2, `areaId`, `floor` FROM ?_spawns_override'); + $this->overrideData = DB::Aowow()->selectAssoc('SELECT `type` AS ARRAY_KEY, `typeGuid` AS ARRAY_KEY2, `areaId`, `floor` FROM ::spawns_override'); + $this->mapToArea = DB::Aowow()->selectCol('SELECT `mapId` AS ARRAY_KEY, `id` FROM ::zones WHERE `parentArea` = 0 AND (`cuFlags` & %i) = 0', CUSTOM_EXCLUDE_FOR_LISTVIEW); + $this->areaParents = DB::Aowow()->selectCol('SELECT `id` AS ARRAY_KEY, IF(`parentArea`, `parentArea`, `id`) FROM ::zones'); /**************/ @@ -109,6 +113,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript $time = new Timer(500); $sum = 0; $lastOverride = 0; + $insertData = []; $nSteps = count($this->steps); $queryResult = $this->$generator(); $queryTotal = count($queryResult); @@ -139,16 +144,28 @@ CLISetup::registerSetup("sql", new class extends SetupScript continue; } - $set = array_merge($spawn, $point); - if (!$isWP) // REPLACE: because there is bogus data where one path may be assigned to multiple npcs + [ + $insertData['areaId'][], + $insertData['posX'][], + $insertData['posY'][], + $insertData['floor'][] + ] = $point; // [areaId, posX, posY, floor] + + unset($spawn['map'], $spawn['posX'], $spawn['posY'], $spawn['areaId']); + foreach ($spawn as $k => $v) + $insertData[$k][] = $v; + + if (!($sum % 1000) || $sum == $queryTotal) { - unset($set['map']); - DB::Aowow()->query('REPLACE INTO ?_spawns (?#) VALUES (?a)', array_keys($set), array_values($set)); - } - else - { - unset($set['map'], $set['guid']); - DB::Aowow()->query('REPLACE INTO ?_creature_waypoints (?#) VALUES (?a)', array_keys($set), array_values($set)); + if (!$isWP) // REPLACE: because there is bogus data where one path may be assigned to multiple npcs + DB::Aowow()->qry('REPLACE INTO ::spawns %m', $insertData); + else + { + unset($insertData['guid']); + DB::Aowow()->qry('REPLACE INTO ::creature_waypoints %m', $insertData); + } + + $insertData = []; } } } @@ -161,7 +178,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript if ($todoMask & 0x01) // only when creature is set { // get vehicle template accessories - $accessories = DB::World()->select( + $accessories = DB::World()->selectAssoc( 'SELECT vta.`accessory_entry` AS `typeId`, c.`guid`, vta.`entry`, COUNT(1) AS `nSeats` FROM vehicle_template_accessory vta LEFT JOIN creature c ON c.`id` = vta.`entry` GROUP BY `accessory_entry`, c.`guid` UNION SELECT va.`accessory_entry` AS `typeId`, va.`guid`, 0 AS `entry`, COUNT(1) AS `nSeats` FROM vehicle_accessory va GROUP BY `accessory_entry`, va.`guid`' ); @@ -178,16 +195,16 @@ CLISetup::registerSetup("sql", new class extends SetupScript { $vehicles = []; if ($data['guid']) // vehicle already spawned - $vehicles = DB::Aowow()->select('SELECT s.`areaId`, s.`posX`, s.`posY`, s.`floor` FROM ?_spawns s WHERE s.`guid` = ?d AND s.`type` = ?d', $data['guid'], Type::NPC); + $vehicles = DB::Aowow()->selectAssoc('SELECT s.`areaId`, s.`posX`, s.`posY`, s.`floor` FROM ::spawns s WHERE s.`guid` = %i AND s.`type` = %i', $data['guid'], Type::NPC); else if ($data['entry']) // vehicle on unspawned vehicle action - $vehicles = DB::Aowow()->select('SELECT s.`areaId`, s.`posX`, s.`posY`, s.`floor` FROM ?_spawns s WHERE s.`typeId` = ?d AND s.`type` = ?d', $data['entry'], Type::NPC); + $vehicles = DB::Aowow()->selectAssoc('SELECT s.`areaId`, s.`posX`, s.`posY`, s.`floor` FROM ::spawns s WHERE s.`typeId` = %i AND s.`type` = %i', $data['entry'], Type::NPC); if ($vehicles) { $matches++; foreach ($vehicles as $v) // if there is more than one vehicle, its probably due to overlapping zones for ($i = 0; $i < $data['nSeats']; $i++) - DB::Aowow()->query('INSERT INTO ?_spawns (`guid`, `type`, `typeId`, `respawn`, `spawnMask`, `phaseMask`, `areaId`, `floor`, `posX`, `posY`, `pathId`) VALUES (?d, ?d, ?d, 0, 0, 1, ?d, ?d, ?f, ?f, 0)', + DB::Aowow()->qry('INSERT INTO ::spawns (`guid`, `type`, `typeId`, `respawn`, `spawnMask`, `phaseMask`, `areaId`, `floor`, `posX`, `posY`, `pathId`) VALUES (%i, %i, %i, 0, 0, 1, %i, %i, %f, %f, 0)', --$vGuid, Type::NPC, $data['typeId'], $v['areaId'], $v['floor'], $v['posX'], $v['posY']); unset($accessories[$idx]); @@ -205,27 +222,28 @@ CLISetup::registerSetup("sql", new class extends SetupScript /* restrict difficulty displays */ /********************************/ - DB::Aowow()->query('UPDATE ?_spawns s, dbc_worldmaparea wma, dbc_map m SET s.`spawnMask` = 0 WHERE s.`areaId` = wma.`areaId` AND wma.`mapId` = m.`id` AND m.`areaType` IN (0, 3, 4)'); + DB::Aowow()->qry('UPDATE ::spawns s, dbc_worldmaparea wma, dbc_map m SET s.`spawnMask` = 0 WHERE s.`areaId` = wma.`areaId` AND wma.`mapId` = m.`id` AND m.`areaType` IN (0, 3, 4)'); return true; } private function creature() : array { - // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId]] - return DB::World()->select( - 'SELECT c.`guid`, ?d AS `type`, c.`id` AS `typeId`, c.`map`, c.`position_x` AS `posX`, c.`position_y` AS `posY`, c.`spawntimesecs` AS `respawn`, c.`spawnMask`, c.`phaseMask`, c.`zoneId` AS `areaId`, IFNULL(ca.`path_id`, 0) AS `pathId` + // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId, ScriptName, StringId]] + return DB::World()->selectAssoc( + 'SELECT c.`guid`, %i AS `type`, c.`id` AS `typeId`, c.`map`, c.`position_x` AS `posX`, c.`position_y` AS `posY`, c.`spawntimesecs` AS `respawn`, c.`spawnMask`, c.`phaseMask`, c.`zoneId` AS `areaId`, IFNULL(ca.`path_id`, IFNULL(cta.`path_id`, 0)) AS `pathId`, NULLIF(`ScriptName`, "") AS "ScriptName", NULLIF(`StringId`, "") AS "StringId" FROM creature c - LEFT JOIN creature_addon ca ON ca.guid = c.guid', + LEFT JOIN creature_addon ca ON ca.guid = c.guid + LEFT JOIN creature_template_addon cta ON cta.entry = c.id', Type::NPC ); } private function gameobject() : array { - // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId]] - return DB::World()->select( - 'SELECT `guid`, ?d AS `type`, `id` AS `typeId`, `map`, `position_x` AS `posX`, `position_y` AS `posY`, `spawntimesecs` AS `respawn`, `spawnMask`, `phaseMask`, `zoneId` AS `areaId` + // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId, ScriptName, StringId]] + return DB::World()->selectAssoc( + 'SELECT `guid`, %i AS `type`, `id` AS `typeId`, `map`, `position_x` AS `posX`, `position_y` AS `posY`, `spawntimesecs` AS `respawn`, `spawnMask`, `phaseMask`, `zoneId` AS `areaId`, NULLIF(`ScriptName`, "") AS "ScriptName", NULLIF(`StringId`, "") AS "StringId" FROM gameobject', Type::OBJECT ); @@ -233,9 +251,9 @@ CLISetup::registerSetup("sql", new class extends SetupScript private function soundemitter() : array { - // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId]] - return DB::Aowow()->select( - 'SELECT `id` AS `guid`, ?d AS `type`, `soundId` AS `typeId`, `mapId` AS `map`, `posX`, `posY` + // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId, ScriptName, StringId]] + return DB::Aowow()->selectAssoc( + 'SELECT `id` AS `guid`, %i AS `type`, `soundId` AS `typeId`, `mapId` AS `map`, `posX`, `posY` FROM dbc_soundemitters', Type::SOUND ); @@ -243,19 +261,19 @@ CLISetup::registerSetup("sql", new class extends SetupScript private function areatrigger() : array { - // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId]] - $base = DB::Aowow()->select( - 'SELECT `id` AS `guid`, ?d AS `type`, `id` AS `typeId`, `mapId` AS `map`, `posX`, `posY` + // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId, ScriptName, StringId]] + $base = DB::Aowow()->selectAssoc( + 'SELECT `id` AS `guid`, %i AS `type`, `id` AS `typeId`, `mapId` AS `map`, `posX`, `posY` FROM dbc_areatrigger', Type::AREATRIGGER ); - $addData = DB::World()->select( - 'SELECT -`ID` AS `guid`, ?d AS `type`, ID AS `typeId`, `target_map` AS `map`, `target_position_x` AS `posX`, `target_position_y` AS `posY` + $addData = DB::World()->selectAssoc( + 'SELECT -`ID` AS `guid`, %i AS `type`, ID AS `typeId`, `target_map` AS `map`, `target_position_x` AS `posX`, `target_position_y` AS `posY` FROM areatrigger_teleport UNION - SELECT -`entryorguid` AS `guid`, ?d AS `type`, entryorguid AS `typeId`, `action_param1` AS `map`, `target_x` AS `posX`, `target_y` AS `posY` + SELECT -`entryorguid` AS `guid`, %i AS `type`, entryorguid AS `typeId`, `action_param1` AS `map`, `target_x` AS `posX`, `target_y` AS `posY` FROM smart_scripts - WHERE `source_type` = ?d AND `action_type` = ?d', + WHERE `source_type` = %i AND `action_type` = %i', Type::AREATRIGGER, Type::AREATRIGGER, SmartAI::SRC_TYPE_AREATRIGGER, SmartAction::ACTION_TELEPORT ); @@ -265,34 +283,35 @@ CLISetup::registerSetup("sql", new class extends SetupScript private function instances() : array { // maps with set graveyard - return DB::Aowow()->select( - 'SELECT -`id` AS `guid`, ?d AS `type`, `id` AS `typeId`, `parentMapId` AS `map`, `parentX` AS `posX`, `parentY` AS `posY` - FROM ?_zones - WHERE `parentX` <> 0 AND `parentY` <> 0 AND `parentArea` = 0 AND (`cuFlags` & ?d) = 0', + return DB::Aowow()->selectAssoc( + 'SELECT -`id` AS `guid`, %i AS `type`, `id` AS `typeId`, `parentMapId` AS `map`, `parentX` AS `posX`, `parentY` AS `posY` + FROM ::zones + WHERE `parentX` <> 0 AND `parentY` <> 0 AND `parentArea` = 0 AND (`cuFlags` & %i) = 0', Type::ZONE, CUSTOM_EXCLUDE_FOR_LISTVIEW ); } private function waypoints() : array { - // todo (med): at least `waypoints` can contain paths that do not belong to a creature but get assigned by SmartAI (or script) during runtime + // todo (med): `waypoint_data` can contain paths that do not belong to a creature but get assigned by SmartAI (or script) during runtime // in the future guid should be optional and additional parameters substituting guid should be passed down from NpcPage after SmartAI has been evaluated - return DB::World()->select( - 'SELECT c.`guid`, w.`entry` AS `creatureOrPath`, w.`pointId` AS `point`, c.`zoneId` AS `areaId`, c.`map`, w.`waittime` AS `wait`, w.`location_x` AS `posX`, w.`location_y` AS `posY` - FROM creature c - JOIN script_waypoint w ON c.`id` = w.`entry` UNION - SELECT c.`guid`, w.`entry` AS `creatureOrPath`, w.`pointId` AS `point`, c.`zoneId` AS `areaId`, c.`map`, 0 AS `wait`, w.`position_x` AS `posX`, w.`position_y` AS `posY` - FROM creature c - JOIN waypoints w ON c.`id` = w.`entry` UNION - SELECT c.`guid`, -w.`id` AS `creatureOrPath`, w.`point`, c.`zoneId` AS `areaId`, c.`map`, w.`delay` AS `wait`, w.`position_x` AS `posX`, w.`position_y` AS `posY` - FROM creature c - JOIN creature_addon ca ON ca.`guid` = c.`guid` - JOIN waypoint_data w ON w.`id` = ca.`path_id` - WHERE ca.`path_id` <> 0' + + // assume that creature_template_addon data isn't stupid and only creatures with a single spawn are referenced here + return DB::World()->selectAssoc( + 'SELECT c.`guid`, -w.`id` AS `creatureOrPath`, w.`point`, c.`zoneId` AS `areaId`, c.`map`, w.`delay` AS `wait`, w.`position_x` AS `posX`, w.`position_y` AS `posY` + FROM creature c + JOIN creature_addon ca ON ca.`guid` = c.`guid` + JOIN waypoint_data w ON w.`id` = ca.`path_id` + WHERE ca.`path_id` <> 0 UNION + SELECT c.`guid`, -w.`id` AS `creatureOrPath`, w.`point`, c.`zoneId` AS `areaId`, c.`map`, w.`delay` AS `wait`, w.`position_x` AS `posX`, w.`position_y` AS `posY` + FROM creature c + JOIN creature_template_addon cta ON cta.`entry` = c.`id` + JOIN waypoint_data w ON w.`id` = cta.`path_id` + WHERE cta.`path_id` <> 0' ); } - private function transformPoint(array $point, int $type, ?string &$notice = '') : array + private function transformPoint(array $point, int $type, ?string &$notice = '') : ?array { // npc/object is on a transport -> apply offsets to path of transport // note, that transport DO spawn outside of displayable area maps .. another todo i guess.. @@ -316,22 +335,21 @@ CLISetup::registerSetup("sql", new class extends SetupScript { // if areaId is set and we match it .. we're fine .. mostly if (count($points) == 1 && $area == $points[0]['areaId']) - return ['areaId' => $points[0]['areaId'], 'posX' => $points[0]['posX'], 'posY' => $points[0]['posY'], 'floor' => $points[0]['floor']]; + return [$points[0]['areaId'], $points[0]['posX'], $points[0]['posY'], $points[0]['floor']]; $point = WorldPosition::checkZonePos($points); // try to determine best found point by alphamap - return ['areaId' => $point['areaId'], 'posX' => $point['posX'], 'posY' => $point['posY'], 'floor' => $point['floor']]; + return [$point['areaId'], $point['posX'], $point['posY'], $point['floor']]; } // cannot be placed on a map, try to reuse TC assigned areaId (note: area has been invalid in the past) - if ($area && ($selfOrParent = DB::Aowow()->selectCell('SELECT IF(`parentArea`, `parentArea`, `id`) FROM ?_zones WHERE `id` = ?d', $area))) - return ['areaId' => $selfOrParent, 'posX' => 0, 'posY' => 0, 'floor' => 0]; + if ($area && isset($this->areaParents[$area])) + return [$this->areaParents[$area], 0, 0, 0]; // we know the instanced map; try to assign a zone this way - if (!in_array($point['map'], [0, 1, 530, 571])) - if ($area = DB::Aowow()->selectCell('SELECT `id` FROM ?_zones WHERE `mapId` = ?d AND `parentArea` = 0 AND (`cuFlags` & ?d) = 0', $point['map'], CUSTOM_EXCLUDE_FOR_LISTVIEW)) - return ['areaId' => $area, 'posX' => 0, 'posY' => 0, 'floor' => 0]; + if (!in_array($point['map'], [0, 1, 530, 571]) && isset($this->mapToArea[$point['map']])) + return [$this->mapToArea[$point['map']], 0, 0, 0]; - return []; + return null; } }); diff --git a/setup/tools/sqlgen/spell.ss.php b/setup/tools/sqlgen/spell.ss.php index ee874270..7547c1ad 100644 --- a/setup/tools/sqlgen/spell.ss.php +++ b/setup/tools/sqlgen/spell.ss.php @@ -21,14 +21,14 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $worldDependency = ['item_template', 'creature_template', 'creature_template_addon', 'creature_template_spell', 'smart_scripts', 'trainer_spell', 'disables', 'spell_ranks', 'spell_dbc', 'skill_discovery_template']; protected $setupAfter = [['icons', 'spellrange'], []]; // spellrange required to use SpellList - public function generate(array $ids = []) : bool + public function generate() : bool { $ssQuery = 'SELECT id, 0 AS category, Dispel, Mechanic, Attributes, AttributesEx, AttributesEx2, AttributesEx3, AttributesEx4, AttributesEx5, AttributesEx6, AttributesEx7, - ?d AS cuFlags, + %i AS cuFlags, 0 AS typeCat, Stances, StancesNot, Targets, @@ -105,7 +105,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript 0 AS spellDescriptionVariable, 0 AS trainingCost FROM spell_dbc - LIMIT ?d,?d'; + LIMIT %i,%i'; $baseQry = 'SELECT s.id, category, @@ -195,16 +195,16 @@ CLISetup::registerSetup("sql", new class extends SetupScript LEFT JOIN dbc_spellradius sr1 ON s.effect1RadiusId = sr1.id LEFT JOIN dbc_spellradius sr2 ON s.effect2RadiusId = sr2.id LEFT JOIN dbc_spellradius sr3 ON s.effect3RadiusId = sr3.id - LIMIT ?d,?d'; + LIMIT %i,%i'; - DB::Aowow()->query('TRUNCATE ?_spell'); + DB::Aowow()->qry('TRUNCATE ::spell'); // merge serverside spells into aowow_spell $lastMax = 0; $n = 0; CLI::write('[spell] - copying serverside spells into aowow_spell'); - while ($spells = DB::World()->select($ssQuery, CUSTOM_SERVERSIDE, $n++ * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH)) + while ($spells = DB::World()->selectAssoc($ssQuery, CUSTOM_SERVERSIDE, $n++ * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH)) { $newMax = max(array_column($spells, 'id')); @@ -213,21 +213,21 @@ CLISetup::registerSetup("sql", new class extends SetupScript $lastMax = $newMax; foreach ($spells as $spell) - DB::Aowow()->query('INSERT INTO ?_spell VALUES (?a)', array_values($spell)); + DB::Aowow()->qry('INSERT INTO ::spell VALUES %l', $spell); } // apply spell radii, duration & casting time - DB::Aowow()->query('UPDATE ?_spell s LEFT JOIN dbc_spellradius sr ON s.`effect1RadiusMin` = sr.`id` SET s.`effect1RadiusMin` = IFNULL(sr.`radiusMin`, 0), s.`effect1RadiusMax` = IFNULL(sr.`radiusMax`, 0)'); - DB::Aowow()->query('UPDATE ?_spell s LEFT JOIN dbc_spellradius sr ON s.`effect2RadiusMin` = sr.`id` SET s.`effect2RadiusMin` = IFNULL(sr.`radiusMin`, 0), s.`effect2RadiusMax` = IFNULL(sr.`radiusMax`, 0)'); - DB::Aowow()->query('UPDATE ?_spell s LEFT JOIN dbc_spellradius sr ON s.`effect3RadiusMin` = sr.`id` SET s.`effect3RadiusMin` = IFNULL(sr.`radiusMin`, 0), s.`effect3RadiusMax` = IFNULL(sr.`radiusMax`, 0)'); - DB::Aowow()->query('UPDATE ?_spell s LEFT JOIN dbc_spellduration sd ON s.`duration` = sd.`id` SET s.`duration` = IF(sd.`baseTime` iS NULL, -1, IF(sd.`baseTime` <> -1, ABS(sd.`baseTime`), -1))'); - DB::Aowow()->query('UPDATE ?_spell s LEFT JOIN dbc_spellcasttimes sct ON s.`castTime` = sct.`id` SET s.`castTime` = GREATEST(IFNULL(sct.`baseTime`, 0), 0) / 1000'); + DB::Aowow()->qry('UPDATE ::spell s LEFT JOIN dbc_spellradius sr ON s.`effect1RadiusMin` = sr.`id` SET s.`effect1RadiusMin` = IFNULL(sr.`radiusMin`, 0), s.`effect1RadiusMax` = IFNULL(sr.`radiusMax`, 0)'); + DB::Aowow()->qry('UPDATE ::spell s LEFT JOIN dbc_spellradius sr ON s.`effect2RadiusMin` = sr.`id` SET s.`effect2RadiusMin` = IFNULL(sr.`radiusMin`, 0), s.`effect2RadiusMax` = IFNULL(sr.`radiusMax`, 0)'); + DB::Aowow()->qry('UPDATE ::spell s LEFT JOIN dbc_spellradius sr ON s.`effect3RadiusMin` = sr.`id` SET s.`effect3RadiusMin` = IFNULL(sr.`radiusMin`, 0), s.`effect3RadiusMax` = IFNULL(sr.`radiusMax`, 0)'); + DB::Aowow()->qry('UPDATE ::spell s LEFT JOIN dbc_spellduration sd ON s.`duration` = sd.`id` SET s.`duration` = IF(sd.`baseTime` iS NULL, -1, IF(sd.`baseTime` <> -1, ABS(sd.`baseTime`), -1))'); + DB::Aowow()->qry('UPDATE ::spell s LEFT JOIN dbc_spellcasttimes sct ON s.`castTime` = sct.`id` SET s.`castTime` = GREATEST(IFNULL(sct.`baseTime`, 0), 0) / 1000'); // merge spell.dbc into aowow_spell $lastMax = 0; $n = 0; CLI::write('[spell] - merging spell.dbc into aowow_spell'); - while ($spells = DB::Aowow()->select($baseQry, $n++ * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH)) + while ($spells = DB::Aowow()->selectAssoc($baseQry, $n++ * CLISetup::SQL_BATCH, CLISetup::SQL_BATCH)) { $newMax = max(array_column($spells, 'id')); @@ -236,26 +236,26 @@ CLISetup::registerSetup("sql", new class extends SetupScript $lastMax = $newMax; foreach ($spells as $spell) - DB::Aowow()->query('INSERT INTO ?_spell VALUES (?a)', array_values($spell)); + DB::Aowow()->qry('INSERT INTO ::spell VALUES %l', $spell); } // apply flag: CUSTOM_DISABLED [0xD: players (0x1), pets (0x4), general (0x8); only generally disabled spells] if ($disables = DB::World()->selectCol('SELECT `entry` FROM disables WHERE `sourceType` = 0 AND `params_0` = "" AND `params_1` = "" AND `flags` & 0xD')) - DB::Aowow()->query('UPDATE ?_spell SET `cuFlags` = `cuFlags` | ?d WHERE `id` IN (?a)', CUSTOM_DISABLED, $disables); + DB::Aowow()->qry('UPDATE ::spell SET `cuFlags` = `cuFlags` | %i WHERE `id` IN %in', CUSTOM_DISABLED, $disables); // apply spell ranks (can't use skilllineability.dbc, as it does not contain ranks for non-player/pet spells) $ranks = DB::World()->selectCol('SELECT `first_spell_id` AS ARRAY_KEY, `spell_id` AS ARRAY_KEY2, `rank` FROM spell_ranks'); foreach ($ranks as $firstSpell => $sets) { // apply flag: SPELL_CU_FIRST_RANK - DB::Aowow()->query('UPDATE ?_spell SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', SPELL_CU_FIRST_RANK, $firstSpell); + DB::Aowow()->qry('UPDATE ::spell SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', SPELL_CU_FIRST_RANK, $firstSpell); foreach ($sets as $spell => $rank) - DB::Aowow()->query('UPDATE ?_spell SET `rankNo` = ?d WHERE `id` = ?d', $rank, $spell); + DB::Aowow()->qry('UPDATE ::spell SET `rankNo` = %i WHERE `id` = %i', $rank, $spell); // apply flag: SPELL_CU_LAST_RANK end($sets); - DB::Aowow()->query('UPDATE ?_spell SET `cuFlags` = `cuFlags` | ?d WHERE `id` = ?d', SPELL_CU_LAST_RANK, key($sets)); + DB::Aowow()->qry('UPDATE ::spell SET `cuFlags` = `cuFlags` | %i WHERE `id` = %i', SPELL_CU_LAST_RANK, key($sets)); } @@ -270,7 +270,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[spell] - linking with skilllineability'); - $results = DB::Aowow()->select('SELECT `spellId` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `skillLineId`, `reqRaceMask`, `reqClassMask`, `reqSkillLevel`, `acquireMethod`, `skillLevelGrey`, `skillLevelYellow` FROM dbc_skilllineability sla'); + $results = DB::Aowow()->selectAssoc('SELECT `spellId` AS ARRAY_KEY, `id` AS ARRAY_KEY2, `skillLineId`, `reqRaceMask`, `reqClassMask`, `reqSkillLevel`, `acquireMethod`, `skillLevelGrey`, `skillLevelYellow` FROM dbc_skilllineability sla'); foreach ($results as $spellId => $sets) { $names = array_keys(current($sets)); @@ -319,7 +319,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript } if ($trainer) - DB::Aowow()->query('UPDATE ?_spell SET `learnedAt` = 1 WHERE `id` = ?d', $spellId); + DB::Aowow()->qry('UPDATE ::spell SET `learnedAt` = 1 WHERE `id` = %i', $spellId); // check skillLineId against mask switch (count($lines)) @@ -343,13 +343,13 @@ CLISetup::registerSetup("sql", new class extends SetupScript } } - DB::Aowow()->query('UPDATE ?_spell SET ?a WHERE `id` = ?d', $update, $spellId); + DB::Aowow()->qry('UPDATE ::spell SET %a WHERE `id` = %i', $update, $spellId); } // fill learnedAt, trainingCost from trainer - if ($trainer = DB::World()->select('SELECT `spellID` AS ARRAY_KEY, MIN(`ReqSkillRank`) AS `reqSkill`, MIN(`MoneyCost`) AS `cost`, `ReqAbility1` AS `reqSpellId`, COUNT(*) AS `count` FROM trainer_spell GROUP BY `SpellID`')) + if ($trainer = DB::World()->selectAssoc('SELECT `spellID` AS ARRAY_KEY, MIN(`ReqSkillRank`) AS `reqSkill`, MIN(`MoneyCost`) AS `cost`, `ReqAbility1` AS `reqSpellId`, COUNT(*) AS `count` FROM trainer_spell GROUP BY `SpellID`')) { - $spells = DB::Aowow()->select('SELECT `id` AS ARRAY_KEY, `effect1Id`, `effect2Id`, `effect3Id`, `effect1TriggerSpell`, `effect2TriggerSpell`, `effect3TriggerSpell` FROM dbc_spell WHERE `id` IN (?a)', array_keys($trainer)); + $spells = DB::Aowow()->selectAssoc('SELECT `id` AS ARRAY_KEY, `effect1Id`, `effect2Id`, `effect3Id`, `effect1TriggerSpell`, `effect2TriggerSpell`, `effect3TriggerSpell` FROM dbc_spell WHERE `id` IN %in', array_keys($trainer)); $links = []; // todo (med): this skips some spells (e.g. riding) @@ -396,14 +396,14 @@ CLISetup::registerSetup("sql", new class extends SetupScript } foreach ($links as $spell => $link) - DB::Aowow()->query("UPDATE ?_spell s SET s.`learnedAt` = ?d, s.`trainingCost` = ?d WHERE s.`id` = ?d", $link[0], $link[1], $spell); + DB::Aowow()->qry("UPDATE ::spell s SET s.`learnedAt` = %i, s.`trainingCost` = %i WHERE s.`id` = %i", $link[0], $link[1], $spell); } // fill learnedAt from recipe-items - $recipes = DB::World()->selectCol('SELECT IF(`spelltrigger_2` = ?d, `spellid_2`, `spellid_1`) AS ARRAY_KEY, MIN(`RequiredSkillRank`) FROM item_template WHERE `class` = ?d AND `spelltrigger_1` <> ?d AND `RequiredSkillRank` > 0 GROUP BY ARRAY_KEY', + $recipes = DB::World()->selectCol('SELECT IF(`spelltrigger_2` = %i, `spellid_2`, `spellid_1`) AS ARRAY_KEY, MIN(`RequiredSkillRank`) FROM item_template WHERE `class` = %i AND `spelltrigger_1` <> %i AND `RequiredSkillRank` > 0 GROUP BY ARRAY_KEY', SPELL_TRIGGER_LEARN, ITEM_CLASS_RECIPE, SPELL_TRIGGER_EQUIP); foreach ($recipes as $spell => $reqSkill) - DB::Aowow()->query('UPDATE ?_spell SET `learnedAt` = IF(`learnedAt` = 0 OR `learnedAt` > ?d, ?d, `learnedAt`) WHERE `id` = ?d', $reqSkill, $reqSkill, $spell); + DB::Aowow()->qry('UPDATE ::spell SET `learnedAt` = IF(`learnedAt` = 0 OR `learnedAt` > %i, %i, `learnedAt`) WHERE `id` = %i', $reqSkill, $reqSkill, $spell); // fill learnedAt from Discovery // 61756: Northrend Inscription Research (FAST QA VERSION); @@ -411,20 +411,20 @@ CLISetup::registerSetup("sql", new class extends SetupScript // 28571 - 28576: $element Protection Potion (todo: get reqSkill from teaching spell [360]) $discovery = DB::World()->selectCol( 'SELECT `spellId` AS ARRAY_KEY, - IF(`reqSpell` = ?d, ?d, - IF(`reqSpell` BETWEEN ?d AND ?d, ?d, + IF(`reqSpell` = %i, %i, + IF(`reqSpell` BETWEEN %i AND %i, %i, IF(`reqSkillValue`, `reqSkillValue`, 1))) FROM skill_discovery_template - WHERE `reqSpell` NOT IN (?a)', + WHERE `reqSpell` NOT IN %in', 64323, 425, 28571, 28576, 360, [61756] ); foreach ($discovery as $spell => $reqSkill) - DB::Aowow()->query('UPDATE ?_spell SET `learnedAt` = ?d WHERE `id` = ?d', $reqSkill, $spell); + DB::Aowow()->qry('UPDATE ::spell SET `learnedAt` = %i WHERE `id` = %i', $reqSkill, $spell); // calc reqSkill for gathering-passives (herbing, mining, skinning) (on second thought .. it is set in skilllineability >.<) - $sets = DB::World()->selectCol('SELECT `spell_id` AS ARRAY_KEY, `rank` * 75 AS `reqSkill` FROM spell_ranks WHERE `first_spell_id` IN (?a)', [55428, 53120, 53125]); + $sets = DB::World()->selectCol('SELECT `spell_id` AS ARRAY_KEY, `rank` * 75 AS `reqSkill` FROM spell_ranks WHERE `first_spell_id` IN %in', [55428, 53120, 53125]); foreach ($sets as $spell => $reqSkill) - DB::Aowow()->query('UPDATE ?_spell SET `learnedAt` = ?d WHERE `id` = ?d', $reqSkill, $spell); + DB::Aowow()->qry('UPDATE ::spell SET `learnedAt` = %i WHERE `id` = %i', $reqSkill, $spell); /******************/ @@ -436,16 +436,16 @@ CLISetup::registerSetup("sql", new class extends SetupScript for ($i = 1; $i < 6; $i++) { // classMask - DB::Aowow()->query('UPDATE ?_spell s, dbc_talent t, dbc_talenttab tt SET s.`reqClassMask` = tt.`classMask` WHERE tt.`creatureFamilyMask` = 0 AND tt.`id` = t.`tabId` AND t.?# = s.`id`', 'rank'.$i); + DB::Aowow()->qry('UPDATE ::spell s, dbc_talent t, dbc_talenttab tt SET s.`reqClassMask` = tt.`classMask` WHERE tt.`creatureFamilyMask` = 0 AND tt.`id` = t.`tabId` AND t.%n = s.`id`', 'rank'.$i); // talentLevel - DB::Aowow()->query('UPDATE ?_spell s, dbc_talent t, dbc_talenttab tt SET s.`talentLevel` = (t.`row` * 5) + 10 + (?d * 1) WHERE tt.`id` = t.`tabId` AND tt.`creatureFamilyMask` = 0 AND t.?# = s.`id`', $i - 1, 'rank'.$i); - DB::Aowow()->query('UPDATE ?_spell s, dbc_talent t, dbc_talenttab tt SET s.`talentLevel` = (t.`row` * 12) + 20 + (?d * 4) WHERE tt.`id` = t.`tabId` AND tt.`creatureFamilyMask` <> 0 AND t.?# = s.`id`', $i - 1, 'rank'.$i); + DB::Aowow()->qry('UPDATE ::spell s, dbc_talent t, dbc_talenttab tt SET s.`talentLevel` = (t.`row` * 5) + 10 + (%i * 1) WHERE tt.`id` = t.`tabId` AND tt.`creatureFamilyMask` = 0 AND t.%n = s.`id`', $i - 1, 'rank'.$i); + DB::Aowow()->qry('UPDATE ::spell s, dbc_talent t, dbc_talenttab tt SET s.`talentLevel` = (t.`row` * 12) + 20 + (%i * 4) WHERE tt.`id` = t.`tabId` AND tt.`creatureFamilyMask` <> 0 AND t.%n = s.`id`', $i - 1, 'rank'.$i); } // passive talent - DB::Aowow()->query('UPDATE ?_spell s, dbc_talent t SET s.`cuFlags` = s.`cuFlags` | ?d WHERE t.`talentSpell` = 0 AND (s.`id` = t.`rank1` OR s.`id` = t.`rank2` OR s.`id` = t.`rank3` OR s.`id` = t.`rank4` OR s.`id` = t.`rank5`)', SPELL_CU_TALENT); + DB::Aowow()->qry('UPDATE ::spell s, dbc_talent t SET s.`cuFlags` = s.`cuFlags` | %i WHERE t.`talentSpell` = 0 AND (s.`id` = t.`rank1` OR s.`id` = t.`rank2` OR s.`id` = t.`rank3` OR s.`id` = t.`rank4` OR s.`id` = t.`rank5`)', SPELL_CU_TALENT); // spell taught by talent - DB::Aowow()->query('UPDATE ?_spell s, dbc_talent t SET s.`cuFlags` = s.`cuFlags` | ?d WHERE t.`talentSpell` = 1 AND (s.`id` = t.`rank1` OR s.`id` = t.`rank2` OR s.`id` = t.`rank3` OR s.`id` = t.`rank4` OR s.`id` = t.`rank5`)', SPELL_CU_TALENTSPELL); + DB::Aowow()->qry('UPDATE ::spell s, dbc_talent t SET s.`cuFlags` = s.`cuFlags` | %i WHERE t.`talentSpell` = 1 AND (s.`id` = t.`rank1` OR s.`id` = t.`rank2` OR s.`id` = t.`rank3` OR s.`id` = t.`rank4` OR s.`id` = t.`rank5`)', SPELL_CU_TALENTSPELL); /*********/ @@ -455,17 +455,17 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[spell] - misc fixups & icons'); // FU [FixUps] - DB::Aowow()->query('UPDATE ?_spell SET `reqRaceMask` = ?d WHERE `skillLine1` = ?d', ChrRace::DRAENEI->toMask(), 760); // Draenei Racials - DB::Aowow()->query('UPDATE ?_spell SET `reqRaceMask` = ?d WHERE `skillLine1` = ?d', ChrRace::BLOODELF->toMask(), 756); // Bloodelf Racials - DB::Aowow()->query('UPDATE ?_spell SET `reqClassMask` = ?d WHERE `id` = ?d', ChrClass::MAGE->toMask(), 30449); // Mage - Spellsteal + DB::Aowow()->qry('UPDATE ::spell SET `reqRaceMask` = %i WHERE `skillLine1` = %i', ChrRace::DRAENEI->toMask(), 760); // Draenei Racials + DB::Aowow()->qry('UPDATE ::spell SET `reqRaceMask` = %i WHERE `skillLine1` = %i', ChrRace::BLOODELF->toMask(), 756); // Bloodelf Racials + DB::Aowow()->qry('UPDATE ::spell SET `reqClassMask` = %i WHERE `id` = %i', ChrClass::MAGE->toMask(), 30449); // Mage - Spellsteal // triggered by spell - DB::Aowow()->query( - 'UPDATE ?_spell a - JOIN ( SELECT effect1TriggerSpell as id FROM ?_spell WHERE effect1Id NOT IN (36, 57, 133) AND effect1TriggerSpell <> 0 UNION - SELECT effect2TriggerSpell as id FROM ?_spell WHERE effect2Id NOT IN (36, 57, 133) AND effect2TriggerSpell <> 0 UNION - SELECT effect3TriggerSpell as id FROM ?_spell WHERE effect3Id NOT IN (36, 57, 133) AND effect3TriggerSpell <> 0 ) as b - SET cuFlags = cuFlags | ?d + DB::Aowow()->qry( + 'UPDATE ::spell a + JOIN ( SELECT effect1TriggerSpell as id FROM ::spell WHERE effect1Id NOT IN (36, 57, 133) AND effect1TriggerSpell <> 0 UNION + SELECT effect2TriggerSpell as id FROM ::spell WHERE effect2Id NOT IN (36, 57, 133) AND effect2TriggerSpell <> 0 UNION + SELECT effect3TriggerSpell as id FROM ::spell WHERE effect3Id NOT IN (36, 57, 133) AND effect3TriggerSpell <> 0 ) as b + SET cuFlags = cuFlags | %i WHERE a.id = b.id', SPELL_CU_TRIGGERED); @@ -476,41 +476,41 @@ CLISetup::registerSetup("sql", new class extends SetupScript LEFT JOIN dbc_talent t1 ON t1.rank1 = s.id LEFT JOIN dbc_talent t2 ON t2.rank2 = s.id LEFT JOIN dbc_talent t3 ON t3.rank3 = s.id - WHERE effect1CreateItemId > 0 AND (effect1Id in (?a) OR effect1AuraId in (?a)) AND t1.id IS NULL AND t2.id IS NULL AND t3.id IS NULL + WHERE effect1CreateItemId > 0 AND (effect1Id IN %in OR effect1AuraId IN %in) AND t1.id IS NULL AND t2.id IS NULL AND t3.id IS NULL UNION SELECT s.id AS ARRAY_KEY, effect2CreateItemId FROM dbc_spell s LEFT JOIN dbc_talent t1 ON t1.rank1 = s.id LEFT JOIN dbc_talent t2 ON t2.rank2 = s.id LEFT JOIN dbc_talent t3 ON t3.rank3 = s.id - WHERE effect2CreateItemId > 0 AND (effect2Id in (?a) OR effect2AuraId in (?a)) AND t1.id IS NULL AND t2.id IS NULL AND t3.id IS NULL + WHERE effect2CreateItemId > 0 AND (effect2Id IN %in OR effect2AuraId IN %in) AND t1.id IS NULL AND t2.id IS NULL AND t3.id IS NULL UNION SELECT s.id AS ARRAY_KEY, effect3CreateItemId FROM dbc_spell s LEFT JOIN dbc_talent t1 ON t1.rank1 = s.id LEFT JOIN dbc_talent t2 ON t2.rank2 = s.id LEFT JOIN dbc_talent t3 ON t3.rank3 = s.id - WHERE effect3CreateItemId > 0 AND (effect3Id in (?a) OR effect3AuraId in (?a)) AND t1.id IS NULL AND t2.id IS NULL AND t3.id IS NULL', + WHERE effect3CreateItemId > 0 AND (effect3Id IN %in OR effect3AuraId IN %in) AND t1.id IS NULL AND t2.id IS NULL AND t3.id IS NULL', SpellList::EFFECTS_ITEM_CREATE, SpellList::AURAS_ITEM_CREATE, SpellList::EFFECTS_ITEM_CREATE, SpellList::AURAS_ITEM_CREATE, SpellList::EFFECTS_ITEM_CREATE, SpellList::AURAS_ITEM_CREATE); - $itemInfo = DB::World()->select('SELECT entry AS ARRAY_KEY, displayId AS d, Quality AS q FROM item_template WHERE entry IN (?a)', $itemSpells); + $itemInfo = DB::World()->selectAssoc('SELECT entry AS ARRAY_KEY, displayId AS d, Quality AS q FROM item_template WHERE entry IN %in', $itemSpells); foreach ($itemSpells as $sId => $itemId) if (isset($itemInfo[$itemId])) - DB::Aowow()->query('UPDATE ?_spell s, ?_icons ic, dbc_itemdisplayinfo idi SET s.iconIdAlt = ic.id, s.cuFlags = s.cuFlags | ?d WHERE ic.name = LOWER(idi.inventoryIcon1) AND idi.id = ?d AND s.id = ?d', ((7 - $itemInfo[$itemId]['q']) << 8), $itemInfo[$itemId]['d'], $sId); + DB::Aowow()->qry('UPDATE ::spell s, ::icons ic, dbc_itemdisplayinfo idi SET s.iconIdAlt = ic.id, s.cuFlags = s.cuFlags | %i WHERE ic.name_source = LOWER(idi.inventoryIcon1) AND idi.id = %i AND s.id = %i', ((7 - $itemInfo[$itemId]['q']) << 8), $itemInfo[$itemId]['d'], $sId); - $itemReqs = DB::World()->selectCol('SELECT entry AS ARRAY_KEY, requiredSpell FROM item_template WHERE requiredSpell NOT IN (?a)', [0, 34090, 34091]); // not riding + $itemReqs = DB::World()->selectCol('SELECT entry AS ARRAY_KEY, requiredSpell FROM item_template WHERE requiredSpell NOT IN %in', [0, 34090, 34091]); // not riding foreach ($itemReqs AS $itemId => $req) - DB::Aowow()->query('UPDATE ?_spell SET reqSpellId = ?d WHERE skillLine1 IN (?a) AND effect1CreateItemId = ?d', $req, [SKILL_BLACKSMITHING, SKILL_LEATHERWORKING, SKILL_TAILORING, SKILL_ENGINEERING], $itemId); + DB::Aowow()->qry('UPDATE ::spell SET reqSpellId = %i WHERE skillLine1 IN %in AND effect1CreateItemId = %i', $req, [SKILL_BLACKSMITHING, SKILL_LEATHERWORKING, SKILL_TAILORING, SKILL_ENGINEERING], $itemId); // setting icons - DB::Aowow()->query('UPDATE ?_spell s, ?_icons ic, dbc_spellicon si SET s.iconId = ic.id WHERE s.iconIdBak = si.id AND ic.name = LOWER(SUBSTRING_INDEX(si.iconPath, "\\\\", -1))'); + DB::Aowow()->qry('UPDATE ::spell s, ::icons ic, dbc_spellicon si SET s.`iconId` = ic.`id` WHERE s.`iconIdBak` = si.`id` AND ic.`name_source` = LOWER(SUBSTRING_INDEX(si.`iconPath`, "\\", -1))'); // hide internal stuff from listviews // QA*; *DND*; square brackets anything; *(NYI)*; *(TEST)* // cant catch raw: NYI (uNYIelding); PH (PHasing) - DB::Aowow()->query('UPDATE ?_spell SET cuFlags = cuFlags | ?d WHERE name_loc0 LIKE "QA%" OR name_loc0 LIKE "%DND%" OR name_loc0 LIKE "%[%" OR name_loc0 LIKE "%(NYI)%" OR name_loc0 LIKE "%(TEST)%"', CUSTOM_EXCLUDE_FOR_LISTVIEW); + DB::Aowow()->qry('UPDATE ::spell SET cuFlags = cuFlags | %i WHERE name_loc0 LIKE "QA%" OR name_loc0 LIKE "%DND%" OR name_loc0 LIKE "%[%" OR name_loc0 LIKE "%(NYI)%" OR name_loc0 LIKE "%(TEST)%"', CUSTOM_EXCLUDE_FOR_LISTVIEW); /**************/ @@ -520,10 +520,10 @@ CLISetup::registerSetup("sql", new class extends SetupScript CLI::write('[spell] - applying categories'); // player talents (-2) - DB::Aowow()->query('UPDATE ?_spell s, dbc_talent t SET s.typeCat = -2 WHERE t.tabId NOT IN (409, 410, 411) AND (s.id = t.rank1 OR s.id = t.rank2 OR s.id = t.rank3 OR s.id = t.rank4 OR s.id = t.rank5)'); + DB::Aowow()->qry('UPDATE ::spell s, dbc_talent t SET s.typeCat = -2 WHERE t.tabId NOT IN (409, 410, 411) AND (s.id = t.rank1 OR s.id = t.rank2 OR s.id = t.rank3 OR s.id = t.rank4 OR s.id = t.rank5)'); // pet spells (-3) - DB::Aowow()->query('UPDATE ?_spell s SET s.typeCat = -3 WHERE (s.cuFlags & 0x3) = 0 AND s.skillline1 IN (?a)', + DB::Aowow()->qry('UPDATE ::spell s SET s.typeCat = -3 WHERE (s.cuFlags & 0x3) = 0 AND s.skillline1 IN %in', array_merge( array_column(Game::$skillLineMask[-1], 1), // hunter pets array_column(Game::$skillLineMask[-2], 1), // warlock pets @@ -533,22 +533,22 @@ CLISetup::registerSetup("sql", new class extends SetupScript ); // racials (-4) - DB::Aowow()->query('UPDATE ?_spell s SET s.typeCat = -4 WHERE s.skillLine1 IN (101, 124, 125, 126, 220, 733, 753, 754, 756, 760)'); + DB::Aowow()->qry('UPDATE ::spell s SET s.typeCat = -4 WHERE s.skillLine1 IN (101, 124, 125, 126, 220, 733, 753, 754, 756, 760)'); // mounts (-5) - DB::Aowow()->query('UPDATE ?_spell s SET s.typeCat = -5 WHERE s.effect1AuraId = 78 AND (s.skillLine1 IN (354, 594, 772, 777) OR (s.skillLine1 > 0 AND s.skillLine2OrMask = 777))'); + DB::Aowow()->qry('UPDATE ::spell s SET s.typeCat = -5 WHERE s.effect1AuraId = 78 AND (s.skillLine1 IN (354, 594, 772, 777) OR (s.skillLine1 > 0 AND s.skillLine2OrMask = 777))'); // companions (-6) - DB::Aowow()->query('UPDATE ?_spell s SET s.typeCat = -6 WHERE s.skillLine1 = 778'); + DB::Aowow()->qry('UPDATE ::spell s SET s.typeCat = -6 WHERE s.skillLine1 = 778'); // pet talents (-7) - DB::Aowow()->query('UPDATE ?_spell s, dbc_talent t SET s.typeCat = -7, s.cuFlags = s.cuFlags | 0x10 WHERE t.tabId = 409 AND (s.id = t.rank1 OR s.id = t.rank2 OR s.id = t.rank3)'); - DB::Aowow()->query('UPDATE ?_spell s, dbc_talent t SET s.typeCat = -7, s.cuFlags = s.cuFlags | 0x08 WHERE t.tabId = 410 AND (s.id = t.rank1 OR s.id = t.rank2 OR s.id = t.rank3)'); - DB::Aowow()->query('UPDATE ?_spell s, dbc_talent t SET s.typeCat = -7, s.cuFlags = s.cuFlags | 0x20 WHERE t.tabId = 411 AND (s.id = t.rank1 OR s.id = t.rank2 OR s.id = t.rank3)'); + DB::Aowow()->qry('UPDATE ::spell s, dbc_talent t SET s.typeCat = -7, s.cuFlags = s.cuFlags | 0x10 WHERE t.tabId = 409 AND (s.id = t.rank1 OR s.id = t.rank2 OR s.id = t.rank3)'); + DB::Aowow()->qry('UPDATE ::spell s, dbc_talent t SET s.typeCat = -7, s.cuFlags = s.cuFlags | 0x08 WHERE t.tabId = 410 AND (s.id = t.rank1 OR s.id = t.rank2 OR s.id = t.rank3)'); + DB::Aowow()->qry('UPDATE ::spell s, dbc_talent t SET s.typeCat = -7, s.cuFlags = s.cuFlags | 0x20 WHERE t.tabId = 411 AND (s.id = t.rank1 OR s.id = t.rank2 OR s.id = t.rank3)'); // internal (-9) by faaaaaar not complete - DB::Aowow()->query('UPDATE ?_spell s SET s.typeCat = -9 WHERE s.skillLine1 = 769'); - DB::Aowow()->query('UPDATE ?_spell s SET s.typeCat = -9 WHERE s.typeCat = 0 AND s.cuFlags = 0 AND ( + DB::Aowow()->qry('UPDATE ::spell s SET s.typeCat = -9 WHERE s.skillLine1 = 769'); + DB::Aowow()->qry('UPDATE ::spell s SET s.typeCat = -9 WHERE s.typeCat = 0 AND s.cuFlags = 0 AND ( s.name_loc0 LIKE "%qa%" OR s.name_loc0 LIKE "%debug%" OR s.name_loc0 LIKE "%internal%" OR @@ -558,49 +558,50 @@ CLISetup::registerSetup("sql", new class extends SetupScript ); // proficiencies (-11) - DB::Aowow()->query('UPDATE ?_spell s, dbc_skillline sl SET s.typeCat = -11 WHERE s.skillLine1 = sl.id AND sl.categoryId IN (6, 8, 10)'); + DB::Aowow()->qry('UPDATE ::spell s, dbc_skillline sl SET s.typeCat = -11 WHERE s.skillLine1 = sl.id AND sl.categoryId IN (6, 8, 10)'); // glyphs (-13) - DB::Aowow()->query('UPDATE ?_spell s, dbc_glyphproperties gp SET s.cuFlags = s.cuFlags | IF(gp.typeFlags, ?d, ?d), s.typeCat = -13 WHERE gp.typeFlags IN (0, 1) AND gp.id = s.effect1MiscValue AND s.effect1Id = 74', SPELL_CU_GLYPH_MINOR, SPELL_CU_GLYPH_MAJOR); + DB::Aowow()->qry('UPDATE ::spell s, dbc_glyphproperties gp SET s.cuFlags = s.cuFlags | IF(gp.typeFlags, %i, %i), s.typeCat = -13 WHERE gp.typeFlags IN (0, 1) AND gp.id = s.effect1MiscValue AND s.effect1Id = 74', SPELL_CU_GLYPH_MINOR, SPELL_CU_GLYPH_MAJOR); $glyphs = DB::World()->selectCol('SELECT it.spellid_1 AS ARRAY_KEY, it.AllowableClass FROM item_template it WHERE it.class = 16'); foreach ($glyphs as $spell => $classMask) - DB::Aowow()->query('UPDATE ?_spell s, dbc_glyphproperties gp SET s.reqClassMask = ?d WHERE gp.typeFlags IN (0, 1) AND gp.id = s.effect1MiscValue AND s.effect1Id = 74 AND s.id = ?d', $classMask, $spell); + DB::Aowow()->qry('UPDATE ::spell s, dbc_glyphproperties gp SET s.reqClassMask = %i WHERE gp.typeFlags IN (0, 1) AND gp.id = s.effect1MiscValue AND s.effect1Id = 74 AND s.id = %i', $classMask, $spell); // class Spells (7) - DB::Aowow()->query('UPDATE ?_spell s, dbc_skillline sl SET s.typeCat = 7 WHERE s.typeCat = 0 AND s.skillLine1 = sl.id AND sl.categoryId = 7'); + DB::Aowow()->qry('UPDATE ::spell s, dbc_skillline sl SET s.typeCat = 7 WHERE s.typeCat = 0 AND s.skillLine1 = sl.id AND sl.categoryId = 7'); // hide some internal/unused stuffs - DB::Aowow()->query('UPDATE ?_spell s SET s.cuFlags = ?d WHERE s.typeCat = 7 AND ( + DB::Aowow()->qry('UPDATE ::spell s SET s.cuFlags = %i WHERE s.typeCat = 7 AND ( s.name_loc0 LIKE "%passive%" OR s.name_loc0 LIKE "%effect%" OR s.name_loc0 LIKE "%improved%" OR s.name_loc0 LIKE "%prototype%" OR -- can probably be extended (s.id NOT IN (47241, 59879, 59671) AND s.baseLevel <= 1 AND s.reqclassMask = 0) OR -- can probably still be extended (s.SpellFamilyId = 15 AND s.SpellFamilyFlags1 & 0x2000 AND s.SpellDescriptionVariableId <> 84) OR -- DK: Skill Coil (s.SpellFamilyId = 10 AND s.SpellFamilyFlags2 & 0x1000000 AND s.attributes1 = 0) OR -- Paladin: Bacon of Light hmm.. Bacon.... :] (s.SpellFamilyId = 6 AND s.SpellFamilyFlags3 & 0x4000) OR -- Priest: Lolwell Renew (s.SpellFamilyId = 6 AND s.SpellFamilyFlags1 & 0x8000000 AND s.rank_loc0 <> "") OR -- Priest: Bling Bling - (s.SpellFamilyId = 8 AND s.attributes0 = 0x50 AND s.attributes1 & 0x400) OR -- Rogue: Intuition (dropped Talent..? looks nice though) + (s.SpellFamilyId = 8 AND s.attributes0 = 0x50 AND s.attributes1 & 0x400) OR -- Rogue: Intuition (s.SpellfamilyId = 11 AND s.SpellFamilyFlags1 & 3 AND s.attributes1 = 1024) OR -- Shaman: Lightning Overload procs (s.attributes0 = 0x20000000 AND s.attributes3 = 0x10000000) -- Master Demonologist (FamilyId = 0) )', CUSTOM_EXCLUDE_FOR_LISTVIEW); foreach (ChrClass::cases() as $cl) - DB::Aowow()->query( - 'UPDATE ?_spell s, dbc_skillline sl, dbc_skillraceclassinfo srci + DB::Aowow()->qry( + 'UPDATE ::spell s, dbc_skillline sl, dbc_skillraceclassinfo srci SET s.`reqClassMask` = srci.`classMask` WHERE s.`typeCat` IN (-2, 7) AND (s.`attributes0` & 0x80) = 0 AND s.`skillLine1` = srci.`skillLine` AND sl.`categoryId` = 7 AND - srci.`skillline` <> 769 AND srci.`skillline` = sl.`id` AND srci.`flags` & 0x90 AND srci.`classMask` & ?d', + srci.`skillline` <> 769 AND srci.`skillline` = sl.`id` AND srci.`flags` & 0x90 AND srci.`classMask` & %i', $cl->toMask() ); // secondary Skills (9) - DB::Aowow()->query('UPDATE ?_spell s SET s.typeCat = 9 WHERE s.typeCat = 0 AND (s.skillLine1 IN (?a) OR (s.skillLine1 > 0 AND s.skillLine2OrMask IN (?a)))', SKILLS_TRADE_SECONDARY, SKILLS_TRADE_SECONDARY); + DB::Aowow()->qry('UPDATE ::spell s SET s.typeCat = 9 WHERE s.typeCat = 0 AND (s.skillLine1 IN %in OR (s.skillLine1 > 0 AND s.skillLine2OrMask IN %in))', SKILLS_TRADE_SECONDARY, SKILLS_TRADE_SECONDARY); // primary Skills (11) - DB::Aowow()->query('UPDATE ?_spell s SET s.typeCat = 11 WHERE s.typeCat = 0 AND s.skillLine1 IN (?a)', SKILLS_TRADE_PRIMARY); + DB::Aowow()->qry('UPDATE ::spell s SET s.typeCat = 11 WHERE s.typeCat = 0 AND s.skillLine1 IN %in', SKILLS_TRADE_PRIMARY); // npc spells (-8) (run as last! .. missing from npc_scripts? "enum Spells { \s+(\w\d_)+\s+=\s(\d+) }" and "#define SPELL_(\d\w_)+\s+(\d+)") // RAID_MODE(1, 2[, 3, 4]) - macro still not considered $world = DB::World()->selectCol( - 'SELECT ss.`action_param1` FROM smart_scripts ss WHERE ss.`action_type` IN (?a) UNION - SELECT cts.`Spell` FROM creature_template_spell cts', + 'SELECT ss.`action_param1` FROM smart_scripts ss WHERE ss.`action_type` IN %in UNION + SELECT cts.`Spell` FROM creature_template_spell cts UNION + SELECT nscs.`spell_id` FROM npc_spellclick_spells nscs', [SmartAction::ACTION_CAST, SmartAction::ACTION_ADD_AURA, SmartAction::ACTION_SELF_CAST, SmartAction::ACTION_CROSS_CAST] ); @@ -615,7 +616,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript $world = array_merge($world, array_filter(explode(' ', $a))); } - DB::Aowow()->query('UPDATE ?_spell s SET s.typeCat = -8 WHERE s.typeCat = 0 AND s.id IN (?a)', $world); + DB::Aowow()->qry('UPDATE ::spell s SET s.typeCat = -8 WHERE s.typeCat = 0 AND s.id IN %in', $world); /**********/ @@ -656,14 +657,14 @@ CLISetup::registerSetup("sql", new class extends SetupScript ); $queryIcons = - 'SELECT s.id, s.name_loc0, s.skillLine1 as skill, ic.id as icon, s.typeCat * s.typeCat AS prio - FROM ?_spell s - LEFT JOIN dbc_spellicon si ON s.iconIdBak = si.id - LEFT JOIN ?_icons ic ON ic.name = LOWER(SUBSTRING_INDEX(si.iconPath, "\\\\", -1)) - WHERE [WHERE] AND (s.cuFlags & ?d) = 0 AND s.typeCat IN (0, 7, -2) -- not triggered; class spells first, talents second, unk last + 'SELECT s.`id`, s.`name_loc0`, s.`skillLine1` AS "skill", ic.`id` AS "icon", s.`typeCat` * s.`typeCat` AS "prio" + FROM ::spell s + LEFT JOIN dbc_spellicon si ON s.`iconIdBak` = si.`id` + LEFT JOIN ::icons ic ON ic.`name_source` = LOWER(SUBSTRING_INDEX(si.`iconPath`, "\\", -1)) + WHERE %and ORDER BY prio DESC'; - $effects = DB::Aowow()->select( + $effects = DB::Aowow()->selectAssoc( 'SELECT s2.id AS ARRAY_KEY, s1.id, s1.name_loc0, @@ -674,9 +675,10 @@ CLISetup::registerSetup("sql", new class extends SetupScript s1.effect1SpellClassMaskB, s1.effect2SpellClassMaskB, s1.effect3SpellClassMaskB, s1.effect1SpellClassMaskC, s1.effect2SpellClassMaskC, s1.effect3SpellClassMaskC FROM dbc_glyphproperties gp - JOIN ?_spell s1 ON s1.id = gp.spellId - JOIN ?_spell s2 ON s2.effect1MiscValue = gp.id AND s2.effect1Id = 74 - WHERE gp.typeFlags IN (0, 1)' // AND s2.id In (58271, 56297, 56289, 63941, 58275) + JOIN ::spell s1 ON s1.id = gp.spellId + JOIN ::spell s2 ON s2.effect1MiscValue = gp.id AND s2.effect1Id = %i + WHERE gp.typeFlags IN (0, 1)', // AND s2.id In (58271, 56297, 56289, 63941, 58275) + SPELL_EFFECT_APPLY_GLYPH ); foreach ($effects as $applyId => $glyphEffect) @@ -688,41 +690,54 @@ CLISetup::registerSetup("sql", new class extends SetupScript // first: manuall replace if ($applyId == 57144) // has no skillLine.. :/ { - DB::Aowow()->query('UPDATE ?_spell s, ?_icons ic SET s.skillLine1 = ?d, s.iconIdAlt = ic.id WHERE s.id = ?d AND ic.name = ?', 253, 57144, 'ability_poisonsting'); + DB::Aowow()->qry('UPDATE ::spell s, ::icons ic SET s.skillLine1 = %i, s.iconIdAlt = ic.id WHERE s.id = %i AND ic.name = %s', 253, 57144, 'ability_poisonsting'); continue; } // second: search by name and family equality if (!$icons) { + $where = array( + ['(s.cuFlags & %i) = 0', SPELL_CU_TRIGGERED], + ['s.typeCat IN (0, 7, -2)'] // not triggered; class spells first, talents second, unk last + ); + $search = !empty($glyphAffects[$applyId]) ? $glyphAffects[$applyId] : str_replace('Glyph of ', '', $glyphEffect['name_loc0']); if (is_int($search)) - $where = "?d AND s.id = ?d"; + $where[] = ['s.`id` = %i', $search]; else - $where = "s.SpellFamilyId = ?d AND s.name_loc0 LIKE ?"; + $where[] = ['s.`SpellFamilyId` = %i AND s.`name_loc0` = %s', $fam, $search]; - $qry = str_replace('[WHERE]', $where, $queryIcons); - $icons = DB::Aowow()->selectRow($qry, $fam ?: 1, $search, SPELL_CU_TRIGGERED); + $icons = DB::Aowow()->selectRow($queryIcons, $where); } // third: match by SpellFamily affect mask while (empty($icons) && $i < 3) { $i++; - $m1 = $glyphEffect['effect'.$i.'SpellClassMaskA']; - $m2 = $glyphEffect['effect'.$i.'SpellClassMaskB']; - $m3 = $glyphEffect['effect'.$i.'SpellClassMaskC']; + $m1 = $glyphEffect['effect'.$i.'SpellClassMaskA']; + $m2 = $glyphEffect['effect'.$i.'SpellClassMaskB']; + $m3 = $glyphEffect['effect'.$i.'SpellClassMaskC']; - if ($glyphEffect['effect'.$i.'Id'] != 6 || (!$m1 && !$m2 && !$m3)) + if ($glyphEffect['effect'.$i.'Id'] != SPELL_EFFECT_APPLY_AURA || (!$m1 && !$m2 && !$m3)) continue; - $where = "s.SpellFamilyId = ?d AND (s.SpellFamilyFlags1 & ?d OR s.SpellFamilyFlags2 & ?d OR s.SpellFamilyFlags3 & ?d)"; + $where = array( + ['(s.`cuFlags` & %i) = 0', SPELL_CU_TRIGGERED], + ['s.`typeCat` IN (0, 7, -2)'], // not triggered; class spells first, talents second, unk last + ['s.`SpellFamilyId` = %i', $fam], + [DB::OR, [ + ['s.`SpellFamilyFlags1` & %i', $m1], + ['s.`SpellFamilyFlags2` & %i', $m2], + ['s.`SpellFamilyFlags3` & %i', $m3] + ]] + ); - $icons = DB::Aowow()->selectRow(str_replace('[WHERE]', $where, $queryIcons), $fam, $m1, $m2, $m3, SPELL_CU_TRIGGERED); + $icons = DB::Aowow()->selectRow($queryIcons, $where); } if ($icons) - DB::Aowow()->query('UPDATE ?_spell s SET s.skillLine1 = ?d, s.iconIdAlt = ?d WHERE s.id = ?d', $icons['skill'], $icons['icon'], $applyId); + DB::Aowow()->qry('UPDATE ::spell s SET s.`skillLine1` = %i, s.`iconIdAlt` = %i WHERE s.`id` = %i', $icons['skill'], $icons['icon'], $applyId); else CLI::write('[spell] '.str_pad('['.$glyphEffect['id'].']', 8).'could not match '.CLI::bold($glyphEffect['name_loc0']).' with affected spells', CLI::LOG_WARN); } diff --git a/setup/tools/sqlgen/spelldifficulty.ss.php b/setup/tools/sqlgen/spelldifficulty.ss.php index 3adfd037..987e839e 100644 --- a/setup/tools/sqlgen/spelldifficulty.ss.php +++ b/setup/tools/sqlgen/spelldifficulty.ss.php @@ -17,16 +17,63 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['spelldifficulty']; protected $worldDependency = ['spelldifficulty_dbc']; + protected $setupAfter = [['creature', 'spawns'], []]; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE TABLE ?_spelldifficulty'); + DB::Aowow()->qry('TRUNCATE TABLE ::spelldifficulty'); - DB::Aowow()->query('INSERT INTO ?_spelldifficulty SELECT GREATEST(`normal10`, 0), GREATEST(`normal25`, 0), GREATEST(`heroic10`, 0), GREATEST(`heroic25`, 0) FROM dbc_spelldifficulty'); + DB::Aowow()->qry('INSERT INTO ::spelldifficulty SELECT GREATEST(`normal10`, 0), GREATEST(`normal25`, 0), GREATEST(`heroic10`, 0), GREATEST(`heroic25`, 0), IF(`heroic10` > 0, 2, 0) FROM dbc_spelldifficulty'); - $rows = DB::World()->select('SELECT `spellid0`, `spellid1`, `spellid2`, `spellid3` FROM spelldifficulty_dbc'); + $rows = DB::World()->selectAssoc('SELECT `spellid0`, `spellid1`, `spellid2`, `spellid3`, IF(`spellid2` > 0, 2, 0) FROM spelldifficulty_dbc'); foreach ($rows as $r) - DB::Aowow()->query('INSERT INTO ?_spelldifficulty VALUES (?a)', array_values($r)); + DB::Aowow()->qry('INSERT INTO ::spelldifficulty VALUES %l', $r); + + + CLI::write('[spelldifficulty] - trying to assign map type by traversing creature spells > spawns'); + + // try to update mode of ambiguous entries + $baseSpells = DB::Aowow()->selectCol('SELECT `normal10` FROM ::spelldifficulty WHERE `heroic10` = 0 AND `heroic25` = 0'); + + for ($i = 1; $i < 9; $i++) + DB::Aowow()->qry( + 'UPDATE ::spelldifficulty sd, + (SELECT c.%n AS "spell", BIT_OR(CASE WHEN z.`type` = %i THEN 1 WHEN z.`type` = %i THEN 2 WHEN z.`type` = %i THEN 2 ELSE 0 END) AS "mapType" + FROM ::creature c + JOIN ::spawns s ON c.id = s.typeId AND s.type = %i + JOIN ::zones z ON z.id = s.areaId + WHERE c.%n IN %in + GROUP BY c.%n + HAVING c.%n <> 0) x + SET sd.`mapType` = x.`mapType` + WHERE sd.`normal10` = x.`spell`', + 'spell'.$i, MAP_TYPE_DUNGEON_HC, MAP_TYPE_MMODE_RAID, MAP_TYPE_MMODE_RAID_HC, + Type::NPC, 'spell'.$i, $baseSpells, 'spell'.$i, 'spell'.$i + ); + + + CLI::write('[spelldifficulty] - trying to assign map type by traversing smart_scripts > spawns'); + + $smartCaster = []; + foreach ($baseSpells as $bs) + if ($owner = SmartAI::getOwnerOfSpellCast($bs)) + foreach ($owner as $type => $caster) + $smartCaster[$type][$bs] = $caster; + + foreach ($smartCaster as $type => $spells) + foreach ($spells as $spellId => $casterEntries) + DB::Aowow()->qry( + 'UPDATE ::spelldifficulty sd, + (SELECT BIT_OR(CASE WHEN z.`type` = %i THEN 1 WHEN z.`type` = %i THEN 2 WHEN z.`type` = %i THEN 2 ELSE 0 END) AS "mapType" + FROM ::spawns s + JOIN ::zones z ON z.id = s.areaId + WHERE s.type = %i AND s.typeId IN %in ) sp + SET sd.`mapType` = IF(sp.`mapType` > 2, 0, sp.`mapType`) + WHERE sd.`normal10` = %i', + MAP_TYPE_DUNGEON_HC, MAP_TYPE_MMODE_RAID, MAP_TYPE_MMODE_RAID_HC, + $type, $casterEntries, + $spellId + ); return true; } diff --git a/setup/tools/sqlgen/talents.ss.php b/setup/tools/sqlgen/talents.ss.php index 6b8cdd23..d325e8fd 100644 --- a/setup/tools/sqlgen/talents.ss.php +++ b/setup/tools/sqlgen/talents.ss.php @@ -17,25 +17,25 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['talent', 'talenttab']; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_talents'); + DB::Aowow()->qry('TRUNCATE ::talents'); // class: 0 => hunter pets for ($i = 1; $i < 6; $i++) - DB::Aowow()->query( - 'INSERT INTO ?_talents + DB::Aowow()->qry( + 'INSERT INTO ::talents SELECT t.id, IF(tt.classMask <> 0, LOG(2, tt.classMask) + 1, 0), tt.creatureFamilyMask, IF(tt.creaturefamilyMask <> 0, LOG(2, tt.creaturefamilyMask), tt.tabNumber), t.row, t.column, - t.rank?d, - ?d + t.rank%i, + %i FROM dbc_talenttab tt JOIN dbc_talent t ON tt.id = t.tabId - WHERE t.rank?d <> 0', + WHERE t.rank%i <> 0', $i, $i, $i ); diff --git a/setup/tools/sqlgen/taxi.ss.php b/setup/tools/sqlgen/taxi.ss.php index 15652be5..29558a86 100644 --- a/setup/tools/sqlgen/taxi.ss.php +++ b/setup/tools/sqlgen/taxi.ss.php @@ -17,20 +17,21 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $dbcSourceFiles = ['taxipath', 'taxinodes', 'worldmaparea', 'worldmaptransforms', 'factiontemplate']; protected $worldDependency = ['creature', 'creature_template']; + protected $setupAfter = [['dungeonmap', 'worldmaparea'], []]; // accessed by WorldPosition::toZonePos - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_taxipath'); - DB::Aowow()->query('TRUNCATE ?_taxinodes'); + DB::Aowow()->qry('TRUNCATE ::taxipath'); + DB::Aowow()->qry('TRUNCATE ::taxinodes'); /*********/ /* paths */ /*********/ - DB::Aowow()->query('INSERT INTO ?_taxipath SELECT tp.id, tp.startNodeId, tp.endNodeId FROM dbc_taxipath tp WHERE tp.startNodeId > 0 AND tp.EndNodeId > 0'); + DB::Aowow()->qry('INSERT INTO ::taxipath SELECT tp.id, tp.startNodeId, tp.endNodeId FROM dbc_taxipath tp WHERE tp.startNodeId > 0 AND tp.EndNodeId > 0'); // paths are monodirectional and thus exist twice for regular flight travel (which is bidirectional) - $paths = DB::Aowow()->select('SELECT id AS ARRAY_KEY, tp.* FROM ?_taxipath tp'); + $paths = DB::Aowow()->selectAssoc('SELECT id AS ARRAY_KEY, tp.* FROM ::taxipath tp'); foreach ($paths as $i => $p) { foreach ($paths as $j => $_) @@ -38,7 +39,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript if ($_['startNodeId'] != $p['endNodeId'] || $_['endNodeId'] != $p['startNodeId']) continue; - DB::Aowow()->query('DELETE FROM ?_taxipath WHERE id = ?d', $j); + DB::Aowow()->qry('DELETE FROM ::taxipath WHERE id = %i', $j); unset($paths[$j]); unset($paths[$i]); break; @@ -51,97 +52,99 @@ CLISetup::registerSetup("sql", new class extends SetupScript /*********/ // all sensible nodes - $fNodes = DB::Aowow()->select( - 'SELECT tn.id, tn.mapId, - 100 - ROUND((tn.posY - wma.right) * 100 / (wma.left - wma.right), 1) AS posX, - 100 - ROUND((tn.posX - wma.bottom) * 100 / (wma.top - wma.bottom), 1) AS poxY, - 1 AS `type`, 0 AS `typeId`, 1 AS reactA, 1 AS reactH, - tn.name_loc0, tn.name_loc2, tn.name_loc3, tn.name_loc4, tn.name_loc6, tn.name_loc8, - tn.mapId AS origMap, tn.posX AS origPosX, tn.posY AS origPosY, - IF (tn.id NOT IN (15, 148, 225, 235) AND ( - tn.id IN (64, 250) OR ( - tn.name_loc0 NOT LIKE "%Transport%" AND tn.name_loc0 NOT LIKE "%Quest%" AND - tn.name_loc0 NOT LIKE "%Start%" AND tn.name_loc0 NOT LIKE "%End%" + $fNodes = DB::Aowow()->selectAssoc( + 'SELECT tn.`id`, + tn.`mapId`, + 100 - ROUND((tn.`posY` - wma.`right`) * 100 / (wma.`left` - wma.`right`), 1) AS "mapX", + 100 - ROUND((tn.`posX` - wma.`bottom`) * 100 / (wma.`top` - wma.`bottom`), 1) AS "mapY", + 0 AS "areaId", + 0 AS "areaX", + 0 AS "areaY", + 1 AS `type`, 0 AS `typeId`, 1 AS "reactA", 1 AS "reactH", + tn.`name_loc0`, tn.`name_loc2`, tn.`name_loc3`, tn.`name_loc4`, tn.`name_loc6`, tn.`name_loc8`, + tn.`mapId` AS "_mapId", tn.`posX` AS "_posX", tn.`posY` AS "_posY", + IF (tn.`id` NOT IN (15, 148, 225, 235) AND ( + tn.`id` IN (64, 250) OR ( + tn.`name_loc0` NOT LIKE "%Transport%" AND tn.`name_loc0` NOT LIKE "%Quest%" AND + tn.`name_loc0` NOT LIKE "%Start%" AND tn.`name_loc0` NOT LIKE "%End%" ) - ), 0, 1) AS scripted + ), 0, 1) AS "_scripted" FROM dbc_taxinodes tn - JOIN dbc_worldmaparea wma ON ( tn.mapId = wma.mapId AND tn.posX BETWEEN wma.bottom AND wma.top AND tn.posY BETWEEN wma.right AND wma.left) - WHERE wma.areaId = 0 AND wma.mapId = tn.mapId + JOIN dbc_worldmaparea wma ON ( tn.`mapId` = wma.`mapId` AND tn.`posX` BETWEEN wma.`bottom` AND wma.`top` AND tn.`posY` BETWEEN wma.`right` AND wma.`left`) + WHERE wma.`areaId` = 0 AND wma.`mapId` = tn.`mapId` UNION - SELECT tn.id, wmt.targetMapId, - 100 - ROUND((tn.posY + wmt.offsetY - wma.right) * 100 / (wma.left - wma.right), 1) AS posX, - 100 - ROUND((tn.posX + wmt.offsetX - wma.bottom) * 100 / (wma.top - wma.bottom), 1) AS poxY, - 1 AS `type`, 0 AS `typeId`, 1 AS reactA, 1 AS reactH, - tn.name_loc0, tn.name_loc2, tn.name_loc3, tn.name_loc4, tn.name_loc6, tn.name_loc8, - tn.mapId AS origMap, tn.posX AS origPosX, tn.posY AS origPosY, + SELECT tn.`id`, + wmt.`targetMapId`, + 100 - ROUND((tn.`posY` + wmt.`offsetY` - wma.`right`) * 100 / (wma.`left` - wma.`right`), 1) AS "mapX", + 100 - ROUND((tn.`posX` + wmt.`offsetX` - wma.`bottom`) * 100 / (wma.`top` - wma.`bottom`), 1) AS "mapY", + 0 AS "areaId", + 0 AS "areaX", + 0 AS "areaY", + 1 AS `type`, 0 AS `typeId`, 1 AS "reactA", 1 AS "reactH", + tn.`name_loc0`, tn.`name_loc2`, tn.`name_loc3`, tn.`name_loc4`, tn.`name_loc6`, tn.`name_loc8`, + tn.`mapId` AS "_mapId", tn.`posX` AS "_posX", tn.`posY` AS "_posY", IF (tn.name_loc0 NOT LIKE "%Transport%" AND tn.name_loc0 NOT LIKE "%Quest%" AND tn.name_loc0 NOT LIKE "%Start%" AND tn.name_loc0 NOT LIKE "%End%", - 0, 1 ) AS scripted + 0, 1 ) AS "_scripted" FROM dbc_taxinodes tn - JOIN dbc_worldmaptransforms wmt ON ( tn.mapId = wmt.sourceMapId AND tn.posX BETWEEN wmt.minX AND wmt.maxX AND tn.posY BETWEEN wmt.minY AND wmt.maxY) - JOIN dbc_worldmaparea wma ON ( wmt.targetMapId = wma.mapId AND tn.posX + wmt.offsetX BETWEEN wma.bottom AND wma.top AND tn.posY + wmt.offsetY BETWEEN wma.right AND wma.left) - WHERE wma.areaId = 0 AND wmt.sourcemapId = tn.mapId' + JOIN dbc_worldmaptransforms wmt ON ( tn.`mapId` = wmt.`sourceMapId` AND tn.`posX` BETWEEN wmt.`minX` AND wmt.`maxX` AND tn.`posY` BETWEEN wmt.`minY` AND wmt.`maxY`) + JOIN dbc_worldmaparea wma ON ( wmt.`targetMapId` = wma.`mapId` AND tn.`posX` + wmt.`offsetX` BETWEEN wma.`bottom` AND wma.`top` AND tn.`posY` + wmt.`offsetY` BETWEEN wma.`right` AND wma.`left`) + WHERE wma.`areaId` = 0 AND wmt.`sourcemapId` = tn.`mapId`' ); // all available flightmaster - $fMaster = DB::World()->select( - 'SELECT ct.entry, ct.faction, c.map, c.position_x AS posX, c.position_y AS posY FROM creature_template ct JOIN creature c ON c.id = ct.entry WHERE ct.npcflag & ?d OR c.npcflag & ?d', + $fMaster = DB::World()->selectAssoc( + 'SELECT ct.`entry`, ct.`faction`, c.`map`, c.`position_x` AS "posX", c.`position_y` AS "posY" FROM creature_template ct JOIN creature c ON c.`id` = ct.`entry` WHERE ct.`npcflag` & %i OR c.`npcflag` & %i', NPC_FLAG_FLIGHT_MASTER, NPC_FLAG_FLIGHT_MASTER ); - // assign nearest flightmaster to node - foreach ($fNodes as &$n) - { - foreach ($fMaster as &$c) - { - if ($c['map'] != $n['origMap']) - continue; - - $dist = pow($c['posX'] - $n['origPosX'], 2) + pow($c['posY'] - $n['origPosY'], 2); - if ($dist > 1000) - continue; - - if (!isset($n['dist']) || $n['dist'] < $dist) - { - $n['dist'] = $dist; - $n['typeId'] = $c['entry']; - $n['faction'] = $c['faction']; - } - } - } - - unset($n); - // fetch reactions per faction - $factions = DB::Aowow()->query( - 'SELECT id AS ARRAY_KEY, - IF(enemyFactionId1 = 1 OR enemyFactionId2 = 1 OR enemyFactionId3 = 1 OR enemyFactionId4 = 1 OR hostileMask & 0x3, -1, 1) AS reactA, - IF(enemyFactionId1 = 2 OR enemyFactionId2 = 2 OR enemyFactionId3 = 2 OR enemyFactionId4 = 2 OR hostileMask & 0x5, -1, 1) AS reactH + $factions = DB::Aowow()->selectAssoc( + 'SELECT `id` AS ARRAY_KEY, + IF(`enemyFactionId1` = 1 OR `enemyFactionId2` = 1 OR `enemyFactionId3` = 1 OR `enemyFactionId4` = 1 OR `hostileMask` & 0x3, -1, 1) AS "reactA", + IF(`enemyFactionId1` = 2 OR `enemyFactionId2` = 2 OR `enemyFactionId3` = 2 OR `enemyFactionId4` = 2 OR `hostileMask` & 0x5, -1, 1) AS "reactH" FROM dbc_factiontemplate - WHERE id IN (?a)', - array_column($fNodes, 'faction')); + WHERE `id` IN %in', + array_column($fMaster, 'faction')); foreach ($fNodes as $n) { - // if (empty($n['faction'])) - // { - // CLI::write(' - ['.$n['id'].'] "'.$n['name_loc0'].'" has no NPC assigned ... skipping', CLI::LOG_WARN); - // continue; - // } - - if ($n['scripted'] || empty($n['faction'])) + // assign nearest flightmaster and its reaction to node + if ($n['_scripted']) $n['type'] = $n['typeId'] = 0; - else if (isset($factions[$n['faction']])) + else { - $n['reactA'] = $factions[$n['faction']]['reactA']; - $n['reactH'] = $factions[$n['faction']]['reactH']; + foreach ($fMaster as &$c) + { + if ($c['map'] != $n['_mapId']) + continue; + + $dist = pow($c['posX'] - $n['_posX'], 2) + pow($c['posY'] - $n['_posY'], 2); + if ($dist > 1000) + continue; + + if (!isset($n['_dist']) || $n['_dist'] < $dist) + { + $n['_dist'] = $dist; + $n['typeId'] = $c['entry']; + $n['reactA'] = $factions[$c['faction']]['reactA'] ?? 0; + $n['reactH'] = $factions[$c['faction']]['reactH'] ?? 0; + } + } } - unset($n['faction'], $n['origMap'], $n['origPosX'], $n['origPosY'], $n['dist'], $n['scripted']); + // calculate zone pos + if ($points = WorldPosition::toZonePos($n['_mapId'], $n['_posX'], $n['_posY'])) + { + $n['areaId'] = $points[0]['areaId']; + $n['areaX'] = $points[0]['posX']; + $n['areaY'] = $points[0]['posY']; + } - DB::Aowow()->query('INSERT INTO ?_taxinodes VALUES (?a)', array_values($n)); + unset($n['_mapId'], $n['_posX'], $n['_posY'], $n['_dist'], $n['_scripted']); + + DB::Aowow()->qry('INSERT INTO ::taxinodes VALUES %l', $n); } - return true; } }); diff --git a/setup/tools/sqlgen/titles.ss.php b/setup/tools/sqlgen/titles.ss.php index 76b68ae0..94d7dfb1 100644 --- a/setup/tools/sqlgen/titles.ss.php +++ b/setup/tools/sqlgen/titles.ss.php @@ -34,7 +34,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript 168 => 404 ); - public function generate(array $ids = []) : bool + public function generate() : bool { $questQuery = 'SELECT qt.`RewardTitle` AS ARRAY_KEY, qt.`AllowableRaces`, IFNULL(ge.`eventEntry`, 0) AS `eventEntry` @@ -43,38 +43,38 @@ CLISetup::registerSetup("sql", new class extends SetupScript LEFT JOIN game_event ge ON ge.`eventEntry` = sq.`eventEntry` WHERE qt.`RewardTitle` <> 0'; - DB::Aowow()->query('TRUNCATE ?_titles'); - DB::Aowow()->query('INSERT INTO ?_titles SELECT `id`, 0, 0, 0, 0, 0, 0, 0, `bitIdx`, `male_loc0`, `male_loc2`, `male_loc3`, `male_loc4`, `male_loc6`, `male_loc8`, `female_loc0`, `female_loc2`, `female_loc3`, `female_loc4`, `female_loc6`, `female_loc8` FROM dbc_chartitles'); + DB::Aowow()->qry('TRUNCATE ::titles'); + DB::Aowow()->qry('INSERT INTO ::titles SELECT `id`, 0, 0, 0, 0, 0, 0, 0, `bitIdx`, `male_loc0`, `male_loc2`, `male_loc3`, `male_loc4`, `male_loc6`, `male_loc8`, `female_loc0`, `female_loc2`, `female_loc3`, `female_loc4`, `female_loc6`, `female_loc8` FROM dbc_chartitles'); // hide unused titles - DB::Aowow()->query('UPDATE ?_titles SET `cuFlags` = ?d WHERE `id` BETWEEN 85 AND 123 AND `id` NOT IN (113, 120, 121, 122)', CUSTOM_EXCLUDE_FOR_LISTVIEW); + DB::Aowow()->qry('UPDATE ::titles SET `cuFlags` = %i WHERE `id` BETWEEN 85 AND 123 AND `id` NOT IN (113, 120, 121, 122)', CUSTOM_EXCLUDE_FOR_LISTVIEW); // set expansion - DB::Aowow()->query('UPDATE ?_titles SET `expansion` = 2 WHERE `id` >= 72 AND `id` <> 80'); - DB::Aowow()->query('UPDATE ?_titles SET `expansion` = 1 WHERE `id` >= 42 AND `id` <> 46 AND `expansion` = 0'); + DB::Aowow()->qry('UPDATE ::titles SET `expansion` = 2 WHERE `id` >= 72 AND `id` <> 80'); + DB::Aowow()->qry('UPDATE ::titles SET `expansion` = 1 WHERE `id` >= 42 AND `id` <> 46 AND `expansion` = 0'); // set category - DB::Aowow()->query('UPDATE ?_titles SET `category` = 1 WHERE `id` <= 28 OR `id` IN (42, 43, 44, 45, 47, 48, 62, 71, 72, 80, 82, 126, 127, 128, 157, 163, 167, 169, 177)'); - DB::Aowow()->query('UPDATE ?_titles SET `category` = 5 WHERE `id` BETWEEN 96 AND 109 OR `id` IN (83, 84)'); - DB::Aowow()->query('UPDATE ?_titles SET `category` = 2 WHERE `id` BETWEEN 144 AND 156 OR `id` IN (63, 77, 79, 113, 123, 130, 131, 132, 176)'); - DB::Aowow()->query('UPDATE ?_titles SET `category` = 6 WHERE `id` IN (46, 74, 75, 76, 124, 133, 134, 135, 137, 138, 155, 168)'); - DB::Aowow()->query('UPDATE ?_titles SET `category` = 4 WHERE `id` IN (81, 125)'); - DB::Aowow()->query('UPDATE ?_titles SET `category` = 3 WHERE `id` IN (53, 64, 120, 121, 122, 129, 139, 140, 141, 142) OR (`id` >= 158 AND `category` = 0)'); + DB::Aowow()->qry('UPDATE ::titles SET `category` = 1 WHERE `id` <= 28 OR `id` IN (42, 43, 44, 45, 47, 48, 62, 71, 72, 80, 82, 126, 127, 128, 157, 163, 167, 169, 177)'); + DB::Aowow()->qry('UPDATE ::titles SET `category` = 5 WHERE `id` BETWEEN 96 AND 109 OR `id` IN (83, 84)'); + DB::Aowow()->qry('UPDATE ::titles SET `category` = 2 WHERE `id` BETWEEN 144 AND 156 OR `id` IN (63, 77, 79, 113, 123, 130, 131, 132, 176)'); + DB::Aowow()->qry('UPDATE ::titles SET `category` = 6 WHERE `id` IN (46, 74, 75, 76, 124, 133, 134, 135, 137, 138, 155, 168)'); + DB::Aowow()->qry('UPDATE ::titles SET `category` = 4 WHERE `id` IN (81, 125)'); + DB::Aowow()->qry('UPDATE ::titles SET `category` = 3 WHERE `id` IN (53, 64, 120, 121, 122, 129, 139, 140, 141, 142) OR (`id` >= 158 AND `category` = 0)'); // update event - if ($assoc = DB::World()->selectCol('SELECT `holiday` AS ARRAY_KEY, `eventEntry` FROM game_event WHERE `holiday` IN (?a)', array_values($this->titleHoliday))) + if ($assoc = DB::World()->selectCol('SELECT `holiday` AS ARRAY_KEY, `eventEntry` FROM game_event WHERE `holiday` IN %in', array_values($this->titleHoliday))) foreach ($this->titleHoliday as $tId => $hId) if (!empty($assoc[$hId])) - DB::Aowow()->query('UPDATE ?_titles SET `eventId` = ?d WHERE `id` = ?d', $assoc[$hId], $tId); + DB::Aowow()->qry('UPDATE ::titles SET `eventId` = %i WHERE `id` = %i', $assoc[$hId], $tId); // update side - $questInfo = DB::World()->select($questQuery); - $sideUpd = DB::World()->selectCol('SELECT IF(`TitleA`, `TitleA`, `TitleH`) AS ARRAY_KEY, BIT_OR(IF(`TitleA`, ?d, ?d)) AS `side` FROM achievement_reward WHERE (`TitleA` <> 0 AND `TitleH` = 0) OR (`TitleH` <> 0 AND `TitleA` = 0) GROUP BY ARRAY_KEY HAVING `side` <> ?d', + $questInfo = DB::World()->selectAssoc($questQuery); + $sideUpd = DB::World()->selectCol('SELECT IF(`TitleA`, `TitleA`, `TitleH`) AS ARRAY_KEY, BIT_OR(IF(`TitleA`, %i, %i)) AS `side` FROM achievement_reward WHERE (`TitleA` <> 0 AND `TitleH` = 0) OR (`TitleH` <> 0 AND `TitleA` = 0) GROUP BY ARRAY_KEY HAVING `side` <> %i', SIDE_ALLIANCE, SIDE_HORDE, SIDE_BOTH); foreach ($questInfo as $tId => $data) { if ($data['eventEntry']) - DB::Aowow()->query('UPDATE ?_titles SET `eventId` = ?d WHERE `id` = ?d', $data['eventEntry'], $tId); + DB::Aowow()->qry('UPDATE ::titles SET `eventId` = %i WHERE `id` = %i', $data['eventEntry'], $tId); $side = ChrRace::sideFromMask($data['AllowableRaces']); if ($side == SIDE_BOTH) @@ -87,11 +87,11 @@ CLISetup::registerSetup("sql", new class extends SetupScript } foreach ($sideUpd as $tId => $side) if ($side != SIDE_BOTH) - DB::Aowow()->query("UPDATE ?_titles SET `side` = ?d WHERE `id` = ?d", $side, $tId); + DB::Aowow()->qry("UPDATE ::titles SET `side` = %i WHERE `id` = %i", $side, $tId); // update side - sourceless titles (maintain query order) - DB::Aowow()->query('UPDATE ?_titles SET `side` = ?d WHERE `id` <= 28 OR `id` IN (118, 119, 116, 117, 110, 127)', SIDE_HORDE); - DB::Aowow()->query('UPDATE ?_titles SET `side` = ?d WHERE `id` <= 14 OR `id` IN (111, 115, 112, 114, 126)', SIDE_ALLIANCE); + DB::Aowow()->qry('UPDATE ::titles SET `side` = %i WHERE `id` <= 28 OR `id` IN (118, 119, 116, 117, 110, 127)', SIDE_HORDE); + DB::Aowow()->qry('UPDATE ::titles SET `side` = %i WHERE `id` <= 14 OR `id` IN (111, 115, 112, 114, 126)', SIDE_ALLIANCE); $this->reapplyCCFlags('titles', Type::TITLE); diff --git a/setup/tools/sqlgen/zones.ss.php b/setup/tools/sqlgen/zones.ss.php index 08cff871..326c31fe 100644 --- a/setup/tools/sqlgen/zones.ss.php +++ b/setup/tools/sqlgen/zones.ss.php @@ -26,18 +26,18 @@ CLISetup::registerSetup("sql", new class extends SetupScript protected $worldDependency = ['access_requirement', 'areatrigger_teleport']; protected $setupAfter = [['dungeonmap', 'worldmaparea'], []]; - public function generate(array $ids = []) : bool + public function generate() : bool { - DB::Aowow()->query('TRUNCATE ?_zones'); + DB::Aowow()->qry('TRUNCATE ::zones'); - $baseData = DB::Aowow()->query( + $baseData = DB::Aowow()->selectAssoc( 'SELECT a.id, IFNULL(wmt.targetMapId, m.id) AS map, m.id AS mapBak, a.areaTable AS parentArea, IFNULL(wmt.targetMapId, IF(m.areaType = 1, 2, IF(m.areaType = 2, 3, IF(m.areaType = 4, 9, IF(m.isBG = 1, 6, IF(m.id = 571, 10, IF(m.id = 530, 8, m.id))))))) AS category, a.flags, - IF(a.mapId IN (13, 25, 37, 42, 169) OR (a.mapId IN (0, 1, 530, 571) AND wma.id IS NULL) OR a.areaTable <> 0 OR (a.soundAmbience = 0 AND a.mapId IN (0, 1, 530, 571)), ?d, 0) AS cuFlags, + IF(a.mapId IN (13, 25, 37, 42, 169) OR (a.mapId IN (0, 1, 530, 571) AND wma.id IS NULL) OR a.areaTable <> 0 OR (a.soundAmbience = 0 AND a.mapId IN (0, 1, 530, 571)), %i, 0) AS cuFlags, IF(a.flags & 0x01000000, 5, IF(m.isBG = 1, 4, IF(m.areaType = 4, 4, IF(a.flags & 0x00000800, 3, IF(a.factionGroupMask = 6, 2, IF(a.factionGroupMask > 0, LOG2(a.factionGroupMask) - 1, 2)))))) AS faction, -- g_zone_territories m.expansion, IF(m.areaType = 0, 0, IF(m.isBG = 1, 4, IF(m.areaType = 4, 6, IF(md.modeMask & 0xC, 8, IF(md.minPl = 10 AND md.maxPL = 25, 7, IF(m.areaType = 2, 3, IF(m.areaType = 1 AND md.modeMask & 0x2, 5, 2))))))) AS `type`, -- g_zone_instancetypes @@ -48,7 +48,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript IFNULL(lfgIni.levelLFG, 0) AS `levelReqLFG`, 0 AS `levelHeroic`, IF(a.flags & 0x8, 1, IFNULL(bm.minLevel, IFNULL(lfgIni.levelMin, IFNULL(lfgOpen.levelMin, 0)))) AS `levelMin`, - IF(a.flags & 0x8, ?d, IFNULL(bm.maxLevel, IFNULL(lfgIni.levelMax, IFNULL(lfgOpen.levelMax, 0)))) AS `levelMax`, + IF(a.flags & 0x8, %i, IFNULL(bm.maxLevel, IFNULL(lfgIni.levelMax, IFNULL(lfgOpen.levelMax, 0)))) AS `levelMax`, "" AS `attunementsN`, "" AS `attunementsH`, GREATEST(m.parentMapId, 0), @@ -79,20 +79,21 @@ CLISetup::registerSetup("sql", new class extends SetupScript CUSTOM_EXCLUDE_FOR_LISTVIEW, MAX_LEVEL ); - DB::Aowow()->query('INSERT INTO ?_zones VALUES (?a)', $baseData); + foreach ($baseData as $bd) + DB::Aowow()->qry('INSERT INTO ::zones VALUES %l', $bd); // set missing graveyards from areatrigger data (auto-resurrect map or just plain errors) // grouped because naxxramas _just has_ to be special with 4 entrances... if ($missingMaps = DB::Aowow()->selectCol('SELECT `id` FROM dbc_map WHERE `parentX` = 0 AND `parentY` = 0 AND `parentMapId` > -1 AND `areaType` NOT IN (0, 3, 4)')) - if ($triggerIds = DB::World()->selectCol('SELECT `target_map`, `id` AS ARRAY_KEY FROM areatrigger_teleport WHERE `target_map` IN (?a) GROUP BY `target_map`', $missingMaps)) - if ($positions = DB::Aowow()->select('SELECT `id` AS `ARRAY_KEY`, `mapId` AS "parentMapId", `posX` AS "parentX", `posY` AS "parentY" FROM dbc_areatrigger WHERE `id` IN (?a)', array_keys($triggerIds))) + if ($triggerIds = DB::World()->selectCol('SELECT `target_map`, `id` AS ARRAY_KEY FROM areatrigger_teleport WHERE `target_map` IN %in GROUP BY `target_map`', $missingMaps)) + if ($positions = DB::Aowow()->selectAssoc('SELECT `id` AS `ARRAY_KEY`, `mapId` AS "parentMapId", `posX` AS "parentX", `posY` AS "parentY" FROM dbc_areatrigger WHERE `id` IN %in', array_keys($triggerIds))) foreach ($positions as $atId => $parentPos) - DB::Aowow()->query('UPDATE ?_zones SET ?a WHERE `mapId` = ?d', $parentPos, $triggerIds[$atId]); + DB::Aowow()->qry('UPDATE ::zones SET %a WHERE `mapId` = %i', $parentPos, $triggerIds[$atId]); // get requirements from world.access_requirement - $zoneReq = DB::World()->select( + $zoneReq = DB::World()->selectAssoc( 'SELECT mapId AS ARRAY_KEY, MIN(level_min) AS reqLevel, MAX(IF(difficulty > 0, level_min, 0)) AS heroicLevel, @@ -108,7 +109,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript GROUP BY mapId' ); - $heroics = DB::Aowow()->selectCol('SELECT DISTINCT mapId FROM ?_zones WHERE type IN (5, 8)'); + $heroics = DB::Aowow()->selectCol('SELECT DISTINCT mapId FROM ::zones WHERE type IN (5, 8)'); foreach ($zoneReq as $mapId => $req) { @@ -167,7 +168,7 @@ CLISetup::registerSetup("sql", new class extends SetupScript $update['attunementsH'] = implode(' ', $aH); } - DB::Aowow()->query('UPDATE ?_zones SET ?a WHERE mapId = ?d', $update, $mapId); + DB::Aowow()->qry('UPDATE ::zones SET %a WHERE mapId = %i', $update, $mapId); } $this->reapplyCCFlags('zones', Type::ZONE); diff --git a/static/css/Profiler.css b/static/css/Profiler.css index 2f74155d..576bb5a8 100644 --- a/static/css/Profiler.css +++ b/static/css/Profiler.css @@ -780,6 +780,11 @@ a.profiler-achievements-total-right { border-color: #101010; } +/* aowow - spell schools table in stats tooltip had no cell spacing */ +.wowhead-tooltip td td:nth-child(2) { + padding-left: 8px; +} + /********************/ /* PROFILER BUTTONS */ /********************/ diff --git a/static/css/aowow.css b/static/css/aowow.css index 7692cdd9..4fe19b47 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -1651,7 +1651,7 @@ span.icon-instance8 { .listview-note { line-height: 16px; - clear: left; + /* clear: left; */ } .listview table { @@ -2552,7 +2552,7 @@ a.modelviewer-close { .layout { min-width: 980px; - max-width: 1340px; + max-width: 1662px; margin: 0 auto; } @@ -4126,6 +4126,14 @@ div.captcha-center { font-size: 11px; } +audio { + background: rgba(0, 0, 0, 0.21); + border-radius: 99px; + margin-bottom: 0 !important; + margin-top: 0 !important; + max-height: 32px; +} + .audio-controls td { text-align:center } @@ -4232,6 +4240,17 @@ input.button-copy:hover { background-color: #444; } +/* clicktocopy fa-replacement on redbutton custom */ +a.button-red.fa-check > em > span { + padding-left: 19px; + background: url(../images/icons/tick.png) no-repeat left center / 12px; +} + +a.button-red.fa-clipboard > em > span { + padding-left: 19px; + background: url(../images/icons/pages.gif) no-repeat left center / 12px; +} + /* favicon fa-replacement custom */ .fav-star { cursor:pointer; @@ -4249,3 +4268,33 @@ input.button-copy:hover { .fav-star-1 { background-position: -65px center; } + +/* imported from lists */ +.compact-completion-display a { + margin-left: 2px; + vertical-align: text-top; /* middle; */ +} + +.progress-icon { + /* background: url(/images/ListManager/completion.png?4) no-repeat; */ + background: url(../images/ui/check.png) no-repeat -1px -15px; + border-radius: 99px; + display: inline-block; + height: 13px; /* 16px; */ + line-height: 16px; + vertical-align: sub; /* bottom; */ + width: 13px; /* 16px; */ +} + +.progress-icon:focus { + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); +} + +/* newer atlas has 9 icons */ +.progress-icon.progress-8 { + background-position: -1px 0px; +} + +.progress-icon.with-text { + padding-left: 16px; +} diff --git a/static/css/global.css b/static/css/global.css index b80fe0f7..231c7068 100644 --- a/static/css/global.css +++ b/static/css/global.css @@ -422,10 +422,12 @@ padding-left: 21px; } +/* aowow - clips in the list marker .infobox .guide-sticky { position: relative; left: -6px; } +*/ #guiderating { display: block; diff --git a/static/js/Profiler.js b/static/js/Profiler.js index ca60a8bf..774d7a3f 100644 --- a/static/js/Profiler.js +++ b/static/js/Profiler.js @@ -6362,7 +6362,7 @@ function ProfilerInventory(_parent) { if (ls != null) { buff += '; ' + ls.text; - if (sm.dd) { + if (sm.dd && sm.dd != 99) { if (sm.dd < 0) { // Dungeon buff += ' ' + (sm.dd < -1 ? LANG.pr_print_heroic : LANG.pr_print_normal); } @@ -8538,10 +8538,15 @@ function ProfilerCompletion(_parent) { _tabsListview.show((_subtotal[_category].complete[_subcategory] ? 2 : 3)); if (_opt.subname) { - _listview.createIndicator($WH.sprintf(LANG['lvnote_' + _mode + 'ind'], _category, _subcategory, _opt.subname(_subcategory)), Listview.headerFilter.bind(_listview, _listview.columns[_listview.columns.length - 2], '')); - _excluded.createIndicator($WH.sprintf(LANG['lvnote_' + _mode + 'ind'], _category, _subcategory, _opt.subname(_subcategory)), Listview.headerFilter.bind(_excluded, _listview.columns[_listview.columns.length - 2], '')); - setTimeout(Listview.headerFilter.bind(_listview, _listview.columns[_listview.columns.length - 2], _opt.subname(_subcategory)), 1); - setTimeout(Listview.headerFilter.bind(_excluded, _excluded.columns[_excluded.columns.length - 2], _opt.subname(_subcategory)), 1); + // aowow - adressing col by offset breaks everytime we add/remove cols in a listview template + // _listview.createIndicator($WH.sprintf(LANG['lvnote_' + _mode + 'ind'], _category, _subcategory, _opt.subname(_subcategory)), Listview.headerFilter.bind(_listview, _listview.columns[_listview.columns.length - 2], '')); + // _excluded.createIndicator($WH.sprintf(LANG['lvnote_' + _mode + 'ind'], _category, _subcategory, _opt.subname(_subcategory)), Listview.headerFilter.bind(_excluded, _listview.columns[_listview.columns.length - 2], '')); + // setTimeout(Listview.headerFilter.bind(_listview, _listview.columns[_listview.columns.length - 2], _opt.subname(_subcategory)), 1); + // setTimeout(Listview.headerFilter.bind(_excluded, _excluded.columns[_excluded.columns.length - 2], _opt.subname(_subcategory)), 1); + _listview.createIndicator($WH.sprintf(LANG['lvnote_' + _mode + 'ind'], _category, _subcategory, _opt.subname(_subcategory)), Listview.headerFilter.bind(_listview, _listview.columns.find((x) => x.id == (_opt.catgcol || 'category')), '')); + _excluded.createIndicator($WH.sprintf(LANG['lvnote_' + _mode + 'ind'], _category, _subcategory, _opt.subname(_subcategory)), Listview.headerFilter.bind(_excluded, _listview.columns.find((x) => x.id == (_opt.catgcol || 'category')), '')); + setTimeout(Listview.headerFilter.bind(_listview, _listview.columns.find((x) => x.id == (_opt.catgcol || 'category')), _opt.subname(_subcategory)), 1); + setTimeout(Listview.headerFilter.bind(_excluded, _excluded.columns.find((x) => x.id == (_opt.catgcol || 'category')), _opt.subname(_subcategory)), 1); } } } @@ -8710,7 +8715,7 @@ Listview.templates.inventory = { $WH.ae(td, $WH.ct(Listview.funcBox.getUpperSource(item.source[0], sm))); } - if (sm.dd) { + if (sm.dd && sm.dd != 99) { if (sm.dd < 0) { // Dungeon $WH.ae(td, $WH.ct($WH.sprintf(LANG.lvitem_dd, '', (sm.dd < -1 ? LANG.lvitem_heroic : LANG.lvitem_normal)))); } @@ -8958,7 +8963,7 @@ Listview.templates.gallery = { $WH.ae(s, $WH.ct(Listview.funcBox.getUpperSource(item.source[0], sm))); } - if (sm.dd) { + if (sm.dd && sm.dd != 99) { if (sm.dd < 0) { // Dungeon $WH.ae(s, $WH.ct($WH.sprintf(LANG.lvitem_dd, '', (sm.dd < -1 ? LANG.lvitem_heroic : LANG.lvitem_normal)))); } @@ -9006,7 +9011,7 @@ Listview.templates.gallery = { buff += ' ' + Listview.funcBox.getUpperSource(item.source[0], sm); - if (sm.dd) { + if (sm.dd && sm.dd != 99) { buff += ' ' + (sm.dd < -1 || sm.dd > 2 ? LANG.pr_print_heroic : LANG.pr_print_normal); } } diff --git a/static/js/ShowOnMap.js b/static/js/ShowOnMap.js index c586a8e6..3e1bae12 100644 --- a/static/js/ShowOnMap.js +++ b/static/js/ShowOnMap.js @@ -107,10 +107,8 @@ ShowOnMap.prototype.construct = function() { } if (nCoords > 0) { var url = (g_types[group[p][0].type] && group[p][0].id ? '?' + g_types[group[p][0].type] + '=' + group[p][0].id : ''); - // legend[submenu.length+1] = [p, url]; aowow - switch to numeric groupIdx to support mail - // entry.push(p + $WH.sprintf(LANG.qty, nPins)); - legend[submenu.length+1] = [group[p][0].name, url]; - entry.push(group[p][0].name + $WH.sprintf(LANG.qty, nPins)); + legend[submenu.length+1] = [p, url]; + entry.push(p + $WH.sprintf(LANG.qty, nPins)); entry.push(this.showStuff.bind(this, coords, [i, i2], legend)); submenu.push(entry); for (var l in coords) { diff --git a/static/js/TalentCalc.js b/static/js/TalentCalc.js index 50a2a5e8..66750bf7 100644 --- a/static/js/TalentCalc.js +++ b/static/js/TalentCalc.js @@ -2289,7 +2289,7 @@ function TalentCalc() { _setPoints(basePoints, -1); _setGlyphSlots(lvl); _refreshGlyphs(); - }; + } function _setLock(locked) { if (_locked != locked) { diff --git a/static/js/admin-article.js b/static/js/admin-article.js index eb0215dd..b65655c8 100644 --- a/static/js/admin-article.js +++ b/static/js/admin-article.js @@ -100,4 +100,4 @@ function updateSnippet() { $("#snippet").val(a) } }) -}; \ No newline at end of file +} \ No newline at end of file diff --git a/static/js/admin.js b/static/js/admin.js index 36899e33..b119e1e3 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -16,4 +16,4 @@ function ar_ValidateUrl(a) { else { return "You used invalid characters in your URL.\n\nYou can only use the following:\n a to z\n 0 to 9\n = _ & . / -" } -}; +} diff --git a/static/js/article-description.js b/static/js/article-description.js index 9dd67556..7d07af4f 100644 --- a/static/js/article-description.js +++ b/static/js/article-description.js @@ -81,4 +81,4 @@ function updatePlaceholder() { else $("#description").text(data); }) -}; \ No newline at end of file +} \ No newline at end of file diff --git a/static/js/basic.js b/static/js/basic.js index 05d30daf..251f1aaf 100644 --- a/static/js/basic.js +++ b/static/js/basic.js @@ -473,12 +473,16 @@ $WH.sp = function(z) { // Set cookie $WH.sc = function(z, y, x, w, v) { var a = new Date(); - var b = z + "=" + escape(x) + "; "; + var b = z + "=" + encodeURIComponent(x) + "; "; a.setDate(a.getDate() + y); b += "expires=" + a.toUTCString() + "; "; - b += "SameSite=strict;"; + b += "samesite=lax; "; + + if (location.protocol === 'https:') { + b += "secure; "; + } if (w) { b += "path=" + w + "; "; @@ -502,7 +506,7 @@ $WH.dc = function(z) { // Get all cookies (return value is cached) $WH.gc = function(z) { if ($WH.gc.I == null) { // Initialize cookie table - var words = unescape(document.cookie).split("; "); + var words = decodeURIComponent(document.cookie).split("; "); $WH.gc.C = {}; for (var i = 0, len = words.length; i < len; ++i) { @@ -549,7 +553,7 @@ $WH.eO = function(z) { // Duplicate object $WH.dO = function(s) { - function f(){}; + function f(){} f.prototype = s; return new f; } @@ -981,7 +985,11 @@ $WH.g_getQueryString = function() { }; $WH.g_parseQueryString = function(str) { - str = decodeURIComponent(str); + // aowow - set to catch invalid unicode escapes (%ff) + // str = decodeURIComponent(str); + try { str = decodeURIComponent(str); } + catch (e) { return {}; } + var words = str.split('&'); var params = {}; @@ -1220,6 +1228,24 @@ if (!$WH.wowheadRemote) { $WH.g_ajaxIshRequest('?data=item-scaling'); } +// aowow - custom..ish +$WH.g_convertScalingSpell = function(base, spellLevel, level) { + let scaler = $WH.g_convertScalingSpell.SV; + + if (!scaler[level] || !scaler[spellLevel]) { + if (g_user.roles & U_GROUP_ADMIN) { + alert('There are no spell scaling values for level ' + level); + } + + return base; + } + + return base *= scaler[level] / scaler[spellLevel]; +} + +if(!$WH.wowheadRemote) + $WH.g_ajaxIshRequest('?data=spell-scaling'); + $WH.g_getDataSource = function() { if ($WH.isset('g_pageInfo')) { switch (g_pageInfo.type) { @@ -1326,6 +1352,14 @@ $WH.g_setJsonItemLevel = function (json, level) { } }; +$WH.g_setJsonSpellLevel = function(json, level) +{ + if (!json.scadist) + return; + + $WH.cO(json, $WH.g_convertScalingSpell(level, json.scadist)); +} + $WH.g_setTooltipLevel = function(tooltip, level) { var _ = typeof tooltip; @@ -1355,7 +1389,8 @@ $WH.g_setTooltipLevel = function(tooltip, level) { // Update the tooltip if (scaDist) { - if (!tooltip.match(/<!--pts[0-9](:[0-9])?-->/g)) { // Not a spell + // if (!tooltip.match(/<!--pts[0-9](:[0-9])?-->/g)) { // Not a spell + if (!tooltip.match(/<!--pts[0-9]+(:[0-9]+)?-->/g)) { // aowow - appropriated for 335a var scaFlags = parseInt(_[5]) || 0, speed = tooltip.match(/<!--spd-->(\d\.\d+)/); @@ -1454,6 +1489,23 @@ $WH.g_setTooltipLevel = function(tooltip, level) { return '<span class="q2"' + style + '>' + prefix + '<!--rtg' + ratingId + '-->' + value + suffix + '</span>' + br; }); } + else + { + var json = { + scadist: scaDist + }; + + // $WH.g_setJsonSpellLevel(json, level); + + // Cast time + // tooltip = tooltip.replace(/<!--cast-->\d+\.\d+/, '<!--cast-->' + json.cast); + + // Spell effects + // aowow - custom: spell level damage calculation + tooltip = tooltip.replace(/<!--pts(\d+):(\d+)-->\s*\d+/gi, function(_all, spellLvl, base) { + return '<!--pts' + spellLvl + ':' + base + '-->' + Math.round($WH.g_convertScalingSpell(base, parseInt(spellLvl), parseInt(level))); + }); + } } // Points per level diff --git a/static/js/consent.js b/static/js/consent.js index 1142d60d..fb3842c1 100644 --- a/static/js/consent.js +++ b/static/js/consent.js @@ -1,10 +1,10 @@ $(document).ready(function() { $WH.qs('#consent-overlay #accept-btn').onclick = function () { - $WH.sc('consent', 1000, 1); + $WH.sc('consent', 1000, 1, '/', location.hostname); $WH.ge('consent-overlay').style.display = 'none'; }; $WH.qs('#consent-overlay #reject-all').onclick = function () { - $WH.sc('consent', 1000, 0); + $WH.sc('consent', 1000, 0, '/', location.hostname); $WH.ge('consent-overlay').style.display = 'none'; }; }); diff --git a/static/js/fileuploader.js b/static/js/fileuploader.js index 025cdf33..f8eed06f 100644 --- a/static/js/fileuploader.js +++ b/static/js/fileuploader.js @@ -1367,7 +1367,7 @@ qq.extend(qq.UploadHandlerXhr.prototype, { } for (key in this._options.customHeaders){ xhr.setRequestHeader(key, this._options.customHeaders[key]); - }; + } xhr.send(file); }, _onComplete: function(id, xhr){ diff --git a/static/js/filters.js b/static/js/filters.js index a09d5e6e..4c6a346a 100644 --- a/static/js/filters.js +++ b/static/js/filters.js @@ -280,6 +280,7 @@ var fi_filters = { { id: 29, name: 'repeatable', type: 'yn' }, { id: 30, name: 'id', type: 'num', before: 'name' }, { id: 44, name: 'countsforloremaster_stc', type: 'yn' }, + { id: 47, name: 'setspvpflag', type: 'yn' }, { id: 9, name: 'objectiveearnrepwith', type: 'faction-any+none' }, { id: 33, name: 'relatedevent', type: 'event-any+none' }, { id: 5, name: 'sharable', type: 'yn' }, @@ -1088,15 +1089,20 @@ function fi_setCriteria(cr, crs, crv) { var i, - c = _.childNodes[0].childNodes[0]; + c = _.childNodes[0].childNodes[0], + s; _ = c.getElementsByTagName('option'); for (i = 0; i < _.length; ++i) { if (_[i].value == cr[0]) { _[i].selected = true; - if (fi_Lookup(cr[0])) { - g_trackEvent('Filters', fi_type, fi_Lookup(cr[0]).name); + if (s = fi_Lookup(cr[0])) { + $WH.Track.nonInteractiveEvent({ + category: "Filters", + action: fi_type, // vars.page, + label: s.name + }); } break; @@ -1108,8 +1114,12 @@ function fi_setCriteria(cr, crs, crv) { for (i = 1; i < cr.length && i < 5; ++i) { fi_criterionChange(fi_addCriterion(a, cr[i]), crs[i], crv[i]); - if (fi_Lookup(cr[i])) { - g_trackEvent('Filters', fi_type, fi_Lookup(cr[i]).name); + if (s = fi_Lookup(cr[i])) { + $WH.Track.nonInteractiveEvent({ + category: "Filters", + action: fi_type, // vars.page, + label: s.name + }); } } } diff --git a/static/js/home.js b/static/js/home.js index ae6969c3..6de5865f 100644 --- a/static/js/home.js +++ b/static/js/home.js @@ -16,7 +16,7 @@ $(document).ready(function () { $('.home-featuredbox-links a').hover( function () { $(this).next('var').addClass('active') }, function () { $(this).next('var').removeClass('active') } - ).click(function () { g_trackEvent('Featured Box', 'Click', this.title) } - ).each( function () { g_trackEvent('Featured Box', 'Impression', this.title) } - ) + ).click(function () { $WH.Track.interactiveEvent({category: 'Featured Box', action: 'Click', label: this.title}) } + ).each( function () { $WH.Track.nonInteractiveEvent({category: 'Featured Box', action: 'Show', label: this.title}) } + ); }); diff --git a/static/js/locale_dede.js b/static/js/locale_dede.js index 055c8497..45dff883 100644 --- a/static/js/locale_dede.js +++ b/static/js/locale_dede.js @@ -1531,6 +1531,7 @@ var g_quest_sorts = { "-374": 'Nobelgarten', "-375": 'Pilgerfreuden', "-376": 'Liebe liegt in der Luft', + 0: 'Nicht kategorisiert', 1: 'Dun Morogh', 3: 'Ödland', 4: 'Verwüstete Lande', @@ -2520,7 +2521,8 @@ var g_conditions = { 47: 'Der Spieler hat [quest=$1]$N: nicht; $2', 48: 'Der Questfortschritt für Ziel #$2 von [quest=$1] ist$N: nicht; $3', 49: 'Der aktuelle Schwierigkeitsgrad für diese Instanz ist #$1', - 50: 'Der Spieler $C$1kann$N: nicht; Gamemaster sein:ist $Nein:kein; Gamemaster;' + 50: 'Der Spieler $C$1kann$N: nicht; Gamemaster sein:ist $Nein:kein; Gamemaster;', + 58: 'Die StringID dieser Entität ist$N: nicht; $4' }; /* end aowow custom */ @@ -2552,7 +2554,7 @@ var LANG = { date_simple: "$1.$2.$3", unknowndate_stc: "Unbekanntes Datum", date_months: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], - date_days: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"], + date_days: ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"], amount: "Menge", abilities: "Fähigkeiten", @@ -3097,6 +3099,8 @@ var LANG = { tooltip_uprate: "Hilfreich/lustig", tooltip_zonelink: "Ein Klick auf diesen Link<br />bringt Euch zur Gebietsseite.", + tooltip_immune: "Immun", // aowow - custom + reputationhistory: "Rufgeschichte", reputationaction: "Aktion", @@ -3612,59 +3616,76 @@ var LANG = { num: [ [1, ">"], [2, ">="], [3, "="], [4, "<="], [5, "<"], [6, "!="]], side: [ [1, "Ja"], [2, "Allianz"], [3, "Horde"], [4, "Beide"], [5, "Nein"] ], faction: [ - [469, "Allianz"], [529, "Argentumdämmerung"], [1106, "Argentumkreuzzug"], [21, "Beutebucht"], [87, "Blutsegelbukaniere"], - [910, "Brut Nozdormus"], [169, "Dampfdruckkartell"], [69, "Darnassus"], [933, "Das Konsortium"], [967, "Das Violette Auge"], - [1156, "Das Äscherne Verdikt"], [509, "Der Bund von Arathor"], [1094, "Der Silberbund"], [1091, "Der Wyrmruhpakt"], [932, "Die Aldor"], - [510, "Die Entweihten"], [930, "Die Exodar"], [1126, "Die Frosterben"], [1067, "Die Hand der Rache"], [1073, "Die Kalu'ak"], - [941, "Die Mag'har"], [1105, "Die Orakel"], [934, "Die Seher"], [935, "Die Sha'tar"], [1124, "Die Sonnenhäscher"], - [1119, "Die Söhne Hodirs"], [1064, "Die Taunka"], [1012, "Die Todeshörigen"], [990, "Die Wächter der Sande"], [81, "Donnerfels"], - [909, "Dunkelmond-Jahrmarkt"], [530, "Dunkelspeertrolle"], [946, "Ehrenfeste"], [47, "Eisenschmiede"], [577, "Ewige Warte"], - [1052, "Expedition der Horde"], [942, "Expedition des Cenarius"], [1050, "Expedition Valianz"], [1068, "Forscherliga"], [729, "Frostwolfklan"], - [369, "Gadgetzan"], [92, "Gelkisklan"], [54, "Gnomeregangnome"], [1031, "Himmelswache der Sha'tari"], [576, "Holzschlundfeste"], - [67, "Horde"], [749, "Hydraxianer"], [989, "Hüter der Zeit"], [1090, "Kirin Tor"], [889, "Kriegshymnenklan"], - [1085, "Kriegshymnenoffensive"], [978, "Kurenai"], [93, "Magramklan"], [1015, "Netherschwingen"], [1077, "Offensive der Zerschmetterten Sonne"], - [1038, "Ogri'la"], [76, "Orgrimmar"], [349, "Rabenholdt"], [470, "Ratschet"], [1098, "Ritter der Schwarzen Klinge"], - [809, "Shen'dralar"], [911, "Silbermond"], [890, "Silberschwingen"], [970, "Sporeggar"], [1104, "Stamm der Wildherzen"], - [270, "Stamm der Zandalari"], [730, "Sturmlanzengarde"], [72, "Sturmwind"], [70, "Syndikat"], [59, "Thoriumbruderschaft"], - [947, "Thrallmar"], [922, "Tristessa"], [1011, "Unteres Viertel"], [68, "Unterstadt"], [1037, "Vorposten der Allianz"], - [589, "Wintersäblerausbilder"], [609, "Zirkel des Cenarius"] + [null, "Classic"], + [ 469, "Allianz"], [ 529, "Argentumdämmerung"], [ 21, "Beutebucht"], [ 87, "Blutsegelbukaniere"], [ 910, "Brut Nozdormus"], + [ 169, "Dampfdruckkartell"], [ 69, "Darnassus"], [ 509, "Der Bund von Arathor"], [ 510, "Die Entweihten"], [ 930, "Die Exodar"], + [ 81, "Donnerfels"], [ 909, "Dunkelmond-Jahrmarkt"], [ 530, "Dunkelspeertrolle"], [ 47, "Eisenschmiede"], [ 577, "Ewige Warte"], + [ 729, "Frostwolfklan"], [ 369, "Gadgetzan"], [ 92, "Gelkisklan"], [ 54, "Gnomeregangnome"], [ 576, "Holzschlundfeste"], + [ 67, "Horde"], [ 749, "Hydraxianer"], [ 889, "Kriegshymnenklan"], [ 93, "Magramklan"], [ 76, "Orgrimmar"], + [ 349, "Rabenholdt"], [ 470, "Ratschet"], [ 809, "Shen'dralar"], [ 911, "Silbermond"], [ 890, "Silberschwingen"], + [ 270, "Stamm der Zandalari"], [ 730, "Sturmlanzengarde"], [ 72, "Sturmwind"], [ 59, "Thoriumbruderschaft"], [ 922, "Tristessa"], + [ 68, "Unterstadt"], [ 609, "Zirkel des Cenarius"], + [null, "Burning Crusade"], + [ 933, "Das Konsortium"], [ 967, "Das Violette Auge"], [ 932, "Die Aldor"], [ 941, "Die Mag'har"], [ 934, "Die Seher"], + [ 935, "Die Sha'tar"], [1012, "Die Todeshörigen"], [ 990, "Die Wächter der Sande"], [ 946, "Ehrenfeste"], [ 942, "Expedition des Cenarius"], + [1031, "Himmelswache der Sha'tari"], [ 989, "Hüter der Zeit"], [ 978, "Kurenai"], [1015, "Netherschwingen"], [1077, "Offensive der Zerschmetterten Sonne"], + [1038, "Ogri'la"], [ 970, "Sporeggar"], [ 947, "Thrallmar"], [1011, "Unteres Viertel"], + [null, "Wrath of the Lich King"], + [1106, "Argentumkreuzzug"], [1156, "Das Äscherne Verdikt"], [1094, "Der Silberbund"], [1091, "Der Wyrmruhpakt"], [1126, "Die Frosterben"], + [1067, "Die Hand der Rache"], [1073, "Die Kalu'ak"], [1105, "Die Orakel"], [1119, "Die Söhne Hodirs"], [1124, "Die Sonnenhäscher"], + [1064, "Die Taunka"], [1052, "Expedition der Horde"], [1050, "Expedition Valianz"], [1068, "Forscherliga"], [1090, "Kirin Tor"], + [1085, "Kriegshymnenoffensive"], [1098, "Ritter der Schwarzen Klinge"], [1104, "Stamm der Wildherzen"], [1037, "Vorposten der Allianz"], + [null, "Verschiedenes"], + [ 70, "Syndikat"], [ 589, "Wintersäblerausbilder"] ], zone: [ - [4494, "Ahn'kahet: Das Alte Königreich"],[3428, "Tempel von Ahn'Qiraj"], [36, "Alteracgebirge"], [2597, "Alteractal"], [3358, "Arathibecken"], - [45, "Arathihochland"], [4603, "Archavons Kammer"], [3702, "Arena des Schergrats"], [4378, "Arena von Dalaran"], [3698, "Arena von Nagrand"], - [3790, "Auchenaikrypta"], [3820, "Auge des Sturms"], [4277, "Azjol-Nerub"], [16, "Azshara"], [3524, "Azurmythosinsel"], - [3525, "Blutmythosinsel"], [3537, "Boreanische Tundra"], [17, "Brachland"], [46, "Brennende Steppe"], [209, "Burg Schattenfang"], - [206, "Burg Utgarde"], [4395, "Dalaran"], [1657, "Darnassus"], [4500, "Das Auge der Ewigkeit"], [4100, "Das Ausmerzen von Stratholme"], - [4493, "Das Obsidiansanktum"], [4228, "Das Oculus"], [796, "Das Scharlachrote Kloster"], [457, "Das verhüllte Meer"], [717, "Das Verlies"], - [3713, "Der Blutkessel"], [3905, "Der Echsenkessel"], [2437, "Der Flammenschlund"], [495, "Der heulende Fjord"], [4265, "Der Nexus"], - [4406, "Der Ring der Ehre"], [3638, "Der Ring der Prüfung"], [2366, "Der schwarze Morast"], [3959, "Der Schwarze Tempel"], [3840, "Der Schwarze Tempel"], - [25, "Der Schwarzfels"], [1477, "Der Tempel von Atal'Hakkar"], [3716, "Der Tiefensumpf"], [3775, "Der Zirkel des Blutes"], [405, "Desolace"], - [3848, "Die Arkatraz"], [3847, "Die Botanika"], [3715, "Die Dampfkammer"], [3557, "Die Exodar"], [4272, "Die Hallen der Blitze"], - [4264, "Die Hallen des Steins"], [718, "Die Höhlen des Wehklagens"], [3849, "Die Mechanar"], [4809, "Die Seelenschmiede"], [3717, "Die Sklavenunterkünfte"], - [67, "Die Sturmgipfel"], [2257, "Die Tiefenbahn"], [1581, "Die Todesminen"], [4415, "Die Violette Festung"], [3714, "Die zerschmetterten Hallen"], - [1638, "Donnerfels"], [65, "Drachenöde"], [148, "Dunkelküste"], [1, "Dun Morogh"], [14, "Durotar"], - [10, "Dämmerwald"], [2557, "Düsterbruch"], [15, "Düstermarschen"], [1537, "Eisenschmiede"], [210, "Eiskrone"], - [4812, "Eiskronenzitadelle"], [331, "Eschental"], [357, "Feralas"], [4196, "Feste Drak'Tharon"], [3845, "Festung der Stürme"], - [41, "Gebirgspass der Totenwinde"], [3433, "Geisterlande"], [2717, "Geschmolzener Kern"], [721, "Gnomeregan"], [394, "Grizzlyhügel"], - [4813, "Grube von Saron"], [3923, "Gruuls Unterschlupf"], [4416, "Gundrak"], [2918, "Halle der Champions"], [2917, "Halle der Legenden"], - [4820, "Hallen der Reflexion"], [47, "Hinterland"], [1216, "Holzschlundfeste"], [4742, "Hrothgars Landestelle"], [3606, "Hyjalgipfel"], - [3607, "Höhle des Schlangenschreins"], [1941, "Höhlen der Zeit"], [3562, "Höllenfeuerbollwerk"], [3483, "Höllenfeuerhalbinsel"], [722, "Hügel der Klingenhauer"], - [3430, "Immersangwald"], [4710, "Insel der Eroberung"], [4080, "Insel von Quel'Danas"], [4024, "Kaltarra"], [3457, "Karazhan"], - [491, "Kral der Klingenhauer"], [490, "Krater von Un'Goro"], [3277, "Kriegshymnenschlucht"], [2817, "Kristallsangwald"], [38, "Loch Modan"], - [3836, "Magtheridons Kammer"], [3792, "Managruft"], [2100, "Maraudon"], [493, "Mondlichtung"], [215, "Mulgore"], - [3518, "Nagrand"], [3456, "Naxxramas"], [3523, "Nethersturm"], [2159, "Onyxias Hort"], [1637, "Orgrimmar"], - [2677, "Pechschwingenhort"], [4298, "Die Scharlachrote Enklave"], [4723, "Prüfung des Champions"], [4722, "Prüfung des Kreuzfahrers"], [44, "Rotkammgebirge"], - [3429, "Ruinen von Ahn'Qiraj"], [3968, "Ruinen von Lordaeron"], [3789, "Schattenlabyrinth"], [3520, "Schattenmondtal"], [3522, "Schergrat"], - [33, "Schlingendorntal"], [2057, "Scholomance"], [1583, "Schwarzfelsspitze"], [1584, "Schwarzfelstiefen"], [51, "Sengende Schlucht"], - [3791, "Sethekkhallen"], [3703, "Shattrath"], [3711, "Sholazarbecken"], [3487, "Silbermond"], [130, "Silberwald"], - [1377, "Silithus"], [3679, "Skettis"], [4075, "Sonnenbrunnenplateau"], [406, "Steinkrallengebirge"], [393, "Strand der Dunkelspeere"], - [4384, "Strand der Uralten"], [2017, "Stratholme"], [1519, "Sturmwind"], [11, "Sumpfland"], [8, "Sümpfe des Elends"], - [440, "Tanaris"], [400, "Tausend Nadeln"], [4197, "Tausendwintersee"], [141, "Teldrassil"], [4131, "Terrasse der Magister"], - [361, "Teufelswald"], [719, "Tiefschwarze Grotte"], [85, "Tirisfal"], [1196, "Turm Utgarde"], [1337, "Uldaman"], - [4273, "Ulduar"], [1497, "Unterstadt"], [4, "Verwüstete Lande"], [2367, "Vorgebirge des Alten Hügellands"], - [267, "Vorgebirge des Hügellands"], [12, "Wald von Elwynn"], [40, "Westfall"], [28, "Westliche Pestländer"], [618, "Winterquell"], - [3519, "Wälder von Terokkar"], [3521, "Zangarmarschen"], [3805, "Zul'Aman"], [66, "Zul'Drak"], [1176, "Zul'Farrak"], - [1977, "Zul'Gurub"], [3, "Ödland"], [139, "Östliche Pestländer"] + [null, "Östliche Königreiche"], + [ 36, "Alteracgebirge"], [ 45, "Arathihochland"], [ 46, "Brennende Steppe"], [ 10, "Dämmerwald"], [2257, "Die Tiefenbahn"], + [ 1, "Dun Morogh"], [1537, "Eisenschmiede"], [ 41, "Gebirgspass der Totenwinde"], [3433, "Geisterlande"], [ 47, "Hinterland"], + [3430, "Immersangwald"], [4080, "Insel von Quel'Danas"], [ 38, "Loch Modan"], [ 3, "Ödland"], [ 139, "Östliche Pestländer"], + [4298, "Pestländer: Die Scharlachrote Enklave"], [ 44, "Rotkammgebirge"], [ 33, "Schlingendorntal"], [ 51, "Sengende Schlucht"], + [3487, "Silbermond"], [ 130, "Silberwald"], [1519, "Sturmwind"], [ 8, "Sümpfe des Elends"], [ 11, "Sumpfland"], + [ 85, "Tirisfal"], [1497, "Unterstadt"], [ 4, "Verwüstete Lande"], [ 267, "Vorgebirge des Hügellands"], [ 12, "Wald von Elwynn"], + [ 40, "Westfall"], [ 28, "Westliche Pestländer"], + [null, "Kalimdor"], + [ 16, "Azshara"], [3524, "Azurmythosinsel"], [3525, "Blutmythosinsel"], [ 17, "Brachland"], [1657, "Darnassus"], + [ 405, "Desolace"], [3557, "Die Exodar"], [1638, "Donnerfels"], [ 148, "Dunkelküste"], [ 14, "Durotar"], + [ 15, "Düstermarschen"], [ 331, "Eschental"], [ 357, "Feralas"], [ 490, "Krater von Un'Goro"], [ 493, "Mondlichtung"], + [ 215, "Mulgore"], [1637, "Orgrimmar"], [1377, "Silithus"], [ 406, "Steinkrallengebirge"], [ 440, "Tanaris"], + [ 400, "Tausend Nadeln"], [ 141, "Teldrassil"], [ 361, "Teufelswald"], [ 618, "Winterquell"], + [null, "Scherbenwelt"], + [3522, "Blade's Edge Mountains"], [3483, "Hellfire Peninsula"], [3518, "Nagrand"], [3523, "Netherstorm"], [3520, "Shadowmoon Valley"], + [3703, "Shattrath City"], [3519, "Terokkar Forest"], [3521, "Zangarmarsh"], + [null, "Nordend"], + [3537, "Boreanische Tundra"], [4395, "Dalaran"], [ 495, "Der heulende Fjord"], [ 67, "Die Sturmgipfel"], [ 65, "Drachenöde"], + [ 210, "Eiskrone"], [ 394, "Grizzlyhügel"], [4742, "Hrothgars Landestelle"], [2817, "Kristallsangwald"], [3711, "Sholazarbecken"], + [4197, "Tausendwintersee"], [ 66, "Zul'Drak"], + [null, "Dungeons"], + [4494, "Ahn'kahet: Das Alte Königreich"],[3790, "Auchenaikrypta"], [4277, "Azjol-Nerub"], [3713, "Blutkessel"], [ 209, "Burg Schattenfang"], + [ 206, "Burg Utgarde"], [3715, "Dampfkammer"], [4100, "Das Ausmerzen von Stratholme"], [4228, "Das Oculus"], [2437, "Der Flammenschlund"], + [4265, "Der Nexus"], [2366, "Der schwarze Morast"], [3848, "Die Arkatraz"], [3847, "Die Botanika"], [3849, "Die Mechanar"], + [4809, "Die Seelenschmiede"], [3714, "Die zerschmetterten Hallen"], [2557, "Düsterbruch"], [4196, "Feste von Drak'Tharon"], [3717, "Frostfürst Ahune"], + [ 721, "Gnomeregan"], [4813, "Grube von Saron"], [4416, "Gundrak"], [4272, "Hallen der Blitze"], [4820, "Hallen der Reflexion"], + [4264, "Hallen des Steins"], [ 718, "Höhlen des Wehklagens"], [3562, "Höllenfeuerbollwerk"], [ 722, "Hügel der Klingenhauer"], [ 491, "Kral der Klingenhauer"], + [3792, "Managruft"], [2100, "Maraudon"], [4723, "Prüfung des Champions"], [ 796, "Scharlachrotes Kloster"], [3789, "Schattenlabyrinth"], + [2057, "Scholomance"], [1583, "Schwarzfelsspitze"], [1584, "Schwarzfelstiefen"], [3791, "Sethekkhallen"], [3717, "Sklavenunterkünfte"], + [2017, "Stratholme"], [4131, "Terrasse der Magister"], [3716, "Tiefensumpf"], [ 719, "Tiefschwarze Grotte"], [1581, "Todesminen"], + [1196, "Turm Utgarde"], [1337, "Uldaman"], [ 717, "Verlies von Sturmwind"], [1477, "Versunkener Tempel"], [4415, "Violette Festung"], + [2367, "Vorgebirge des Alten Hügellands"],[1176, "Zul'Farrak"], + [null, "Schlachtzüge"], + [4603, "Archavons Kammer"], [4500, "Das Auge der Ewigkeit"], [4493, "Das Obsidiansanktum"], [4987, "Das Rubinsanktum"], [3959, "Der Schwarze Tempel"], + [4075, "Der Sonnenbrunnen"], [3606, "Die Schlacht um den Berg Hyjal"],[4812, "Eiskronenzitadelle"], [3845, "Festung der Stürme"], [2717, "Geschmolzener Kern"], + [3923, "Gruuls Unterschlupf"], [3607, "Höhle des Schlangenschreins"], [3457, "Karazhan"], [3836, "Magtheridons Kammer"], [3456, "Naxxramas"], + [2159, "Onyxias Hort"], [2677, "Pechschwingenhort"], [4722, "Prüfung des Kreuzfahrers"], [3429, "Ruinen von Ahn'Qiraj"], [3428, "Tempel von Ahn'Qiraj"], + [4273, "Ulduar"], [3805, "Zul'Aman"], [1977, "Zul'Gurub"], [2597, "Alteractal"], [3358, "Arathibecken"], + [3820, "Auge des Sturms"], [4710, "Insel der Eroberung"], [3277, "Kriegshymnenschlucht"], [4384, "Strand der Uralten"], [3483, "Höllenfeuerhalbinsel"], + [3518, "Nagrand"], [3523, "Nethersturm"], [3520, "Schattenmondtal"], [3522, "Schergrat"], [3703, "Shattrath"], + [3519, "Wälder von Terokkar"], [3521, "Zangarmarschen"], + [null, "Schlachtfelder"], + [2597, "Alteractal"], [3358, "Arathibecken"], [3820, "Auge des Sturms"], [4710, "Insel der Eroberung"], [3277, "Kriegshymnenschlucht"], + [4384, "Strand der Uralten"], + [null, "Arenen"], + [3702, "Arena des Schergrats"], [4378, "Arena von Dalaran"], [3698, "Arena von Nagrand"], [4406, "Der Ring der Ehre"], [3968, "Ruinen von Lordaeron"] ], resistance: [ [6, "Arkan"], [2, "Feuer"], [3, "Natur"], [4, "Frost"], [5, "Schatten"], @@ -3711,67 +3732,84 @@ var LANG = { ], pvp: [ [1, "Ja"], [3, "Arena"], [4, "Schlachtfeld"], [5, "Welt"], [2, "Nein"] ], currency: [ - [24579, "Abzeichen der Ehrenfeste"], [29434, "Abzeichen der Gerechtigkeit"], [24581, "Abzeichen von Thrallmar"], [32572, "Apexiskristall"], [32569, "Apexissplitter"], - [29736, "Arkane Rune"], [44128, "Arktischer Pelz"], [23247, "Brandblüte"], [37829, "Braufestmarke"], [20558, "Ehrenabzeichen der Kriegshymnenschlucht"], - [20560, "Ehrenabzeichen des Alteractals"], [20559, "Ehrenabzeichen des Arathibeckens"], [29024, "Ehrenabzeichen vom Auge des Sturms"], - [43589, "Ehrenabzeichen von Tausendwinter"], [40753, "Emblem der Ehre"], [45624, "Emblem der Eroberung"], [49426, "Emblem des Frosts"], - [40752, "Emblem des Heldentums"], [47241, "Emblem des Triumphs"], [26044, "Forschermarke von Halaa"], [28558, "Geistsplitter"], [24245, "Glühkappe"], - [29735, "Heiliger Staub"], [41596, "Juwelierssymbol von Dalaran"], [26045, "Kampfmarke von Halaa"], [43016, "Kochpreis von Dalaran"], [32897, "Mal der Illidari"], - [37836, "Münze der Venture Co."], [22484, "Nekrotische Rune"], [38425, "Schweres boreanisches Leder"], [4291, "Seidenfaden"], [44990, "Siegel des Champions"], - [34664, "Sonnenpartikel"], [43228, "Splitter eines Steinbewahrers"],[34052, "Traumsplitter"], [47242, "Trophäe des Kreuzzugs"], [24368, "Waffen des Echsenkessels"], - [52026, "Weiheabzeichen des Beschützers"], [52029, "Weiheabzeichen des Beschützers (Heroisch)"], [52025, "Weiheabzeichen des Bezwingers"], - [52028, "Weiheabzeichen des Bezwingers (Heroisch)"], [52027, "Weiheabzeichen des Eroberers"],[52030, "Weiheabzeichen des Eroberers (Heroisch)"], - [34597, "Winterflossenmuschel"] + [null, "Classic"], + [37829, "Braufestmarke"], [23247, "Brandblüte"], [4291, "Seidenfaden"], + [null, "Burning Crusade"], + [32572, "Apexiskristall"], [32569, "Apexissplitter"], [29736, "Arkane Rune"], [24368, "Waffen des Echsenkessels"], [24245, "Glühkappe"], + [29735, "Heiliger Staub"], [32897, "Mal der Illidari"], [22484, "Nekrotische Rune"], [34664, "Sonnenpartikel"], + [null, "Wrath of the Lich King"], + [44128, "Arktischer Pelz"], [44990, "Siegel des Champions"], [43016, "Kochpreis von Dalaran"], [41596, "Juwelierssymbol von Dalaran"], [34052, "Traumsplitter"], + [38425, "Schweres boreanisches Leder"], [34597, "Winterflossenmuschel"], + [null, "Spieler gegen Spieler"], + [24579, "Abzeichen der Ehrenfeste"], [24581, "Abzeichen von Thrallmar"], [20560, "Ehrenabzeichen des Alteractals"], [20559, "Ehrenabzeichen des Arathibeckens"], + [20558, "Ehrenabzeichen der Kriegshymnenschlucht"], [29024, "Ehrenabzeichen vom Auge des Sturms"], [43589, "Ehrenabzeichen von Tausendwinter"], + [26044, "Forschermarke von Halaa"], [28558, "Geistsplitter"], [26045, "Kampfmarke von Halaa"], [37836, "Münze der Venture Co."], [43228, "Splitter eines Steinbewahrers"], + [null, "Dungeon und Schlachtzug"], + [29434, "Abzeichen der Gerechtigkeit"], [45624, "Emblem der Eroberung"], [49426, "Emblem des Frosts"], [40752, "Emblem des Heldentums"], [47241, "Emblem des Triumphs"], + [40753, "Emblem der Ehre"] ], itemcurrency: [ - [34851, "Armschienen des vergessenen Beschützers"], [34852, "Armschienen des vergessenen Bezwingers"], [34848, "Armschienen des vergessenen Eroberers"], - [34208, "Ausgewuchtete Schulterklappen"], [34245, "Bedeckung von Ursol dem Weisen"], [45654, "Beinplatten des abtrünnigen Beschützers"], - [45655, "Beinplatten des abtrünnigen Bezwingers"], [45653, "Beinplatten des abtrünnigen Eroberers"], [34167, "Beinplatten des heiligen Molochs"], - [40635, "Beinplatten des verlorenen Beschützers"], [40636, "Beinplatten des verlorenen Bezwingers"], [40634, "Beinplatten des verlorenen Eroberers"], - [34211, "Brustharnisch der Fleischeslust"], [45633, "Brustplatte des abtrünnigen Beschützers"], [45634, "Brustplatte des abtrünnigen Bezwingers"], - [45632, "Brustplatte des abtrünnigen Eroberers"], [40626, "Brustplatte des verlorenen Beschützers"], [40627, "Brustplatte des verlorenen Bezwingers"], - [40625, "Brustplatte des verlorenen Eroberers"], [45636, "Brustschutz des abtrünnigen Beschützers"], [45637, "Brustschutz des abtrünnigen Bezwingers"], - [45635, "Brustschutz des abtrünnigen Eroberers"], [30236, "Brustschutz des bezwungenen Champions"], [30238, "Brustschutz des bezwungenen Helden"], - [30237, "Brustschutz des bezwungenen Verteidigers"], [29754, "Brustschutz des gefallenen Champions"], [29755, "Brustschutz des gefallenen Helden"], - [29753, "Brustschutz des gefallenen Verteidigers"], [34216, "Brustschutz des heroischen Richters"], [31091, "Brustschutz des vergessenen Beschützers"], - [31090, "Brustschutz des vergessenen Bezwingers"], [31089, "Brustschutz des vergessenen Eroberers"], [40611, "Brustschutz des verlorenen Beschützers"], - [40612, "Brustschutz des verlorenen Bezwingers"], [40610, "Brustschutz des verlorenen Eroberers"], [34169, "Bundhosen der natürlichen Aggression"], - [45651, "Gamaschen des abtrünnigen Beschützers"], [45652, "Gamaschen des abtrünnigen Bezwingers"], [45650, "Gamaschen des abtrünnigen Eroberers"], - [30245, "Gamaschen des bezwungenen Champions"], [30247, "Gamaschen des bezwungenen Helden"], [30246, "Gamaschen des bezwungenen Verteidigers"], - [29766, "Gamaschen des gefallenen Champions"], [29765, "Gamaschen des gefallenen Helden"], [29767, "Gamaschen des gefallenen Verteidigers"], - [31100, "Gamaschen des vergessenen Beschützers"], [31099, "Gamaschen des vergessenen Bezwingers"], [31098, "Gamaschen des vergessenen Eroberers"], - [40620, "Gamaschen des verlorenen Beschützers"], [40621, "Gamaschen des verlorenen Bezwingers"], [40619, "Gamaschen des verlorenen Eroberers"], - [34229, "Gewänder der stillen Küsten"], [34339, "Gugel des reinen Lichts"], [34332, "Gugel von Gul'dan"], - [34854, "Gürtel des vergessenen Beschützers"], [34855, "Gürtel des vergessenen Bezwingers"], [34853, "Gürtel des vergessenen Eroberers"], - [45645, "Handschuhe des abtrünnigen Beschützers"], [45646, "Handschuhe des abtrünnigen Bezwingers"], [45644, "Handschuhe des abtrünnigen Eroberers"], - [30239, "Handschuhe des bezwungenen Champions"], [30241, "Handschuhe des bezwungenen Helden"], [30240, "Handschuhe des bezwungenen Verteidigers"], - [29757, "Handschuhe des gefallenen Champions"], [29756, "Handschuhe des gefallenen Helden"], [29758, "Handschuhe des gefallenen Verteidigers"], - [31094, "Handschuhe des vergessenen Beschützers"], [31093, "Handschuhe des vergessenen Bezwingers"], [31092, "Handschuhe des vergessenen Eroberers"], - [40614, "Handschuhe des verlorenen Beschützers"], [40615, "Handschuhe des verlorenen Bezwingers"], [40613, "Handschuhe des verlorenen Eroberers"], - [34342, "Handschützer des Morgens"], [34244, "Haube des Doppelzüngigen"], [34243, "Helm der lodernden Rechtschaffenheit"], - [45648, "Helm des abtrünnigen Beschützers"], [45649, "Helm des abtrünnigen Bezwingers"], [45647, "Helm des abtrünnigen Eroberers"], - [30242, "Helm des bezwungenen Champions"], [30244, "Helm des bezwungenen Helden"], [30243, "Helm des bezwungenen Verteidigers"], - [29760, "Helm des gefallenen Champions"], [29759, "Helm des gefallenen Helden"], [29761, "Helm des gefallenen Verteidigers"], - [31095, "Helm des vergessenen Beschützers"], [31096, "Helm des vergessenen Bezwingers"], [31097, "Helm des vergessenen Eroberers"], - [40617, "Helm des verlorenen Beschützers"], [40618, "Helm des verlorenen Bezwingers"], [40616, "Helm des verlorenen Eroberers"], - [34186, "Kettenverbund des tobenden Sturmes"], [34215, "Kriegsharnisch des tollkühnen Furors"], [45639, "Krone des abtrünnigen Beschützers"], - [45640, "Krone des abtrünnigen Bezwingers"], [45638, "Krone des abtrünnigen Eroberers"], [40632, "Krone des verlorenen Beschützers"], - [40633, "Krone des verlorenen Bezwingers"], [40631, "Krone des verlorenen Eroberers"], [34345, "Krone von Anasterian"], - [40638, "Mantel des verlorenen Beschützers"], [40639, "Mantel des verlorenen Bezwingers"], [40637, "Mantel des verlorenen Eroberers"], - [45657, "Mantelung des abtrünnigen Beschützers"], [45658, "Mantelung des abtrünnigen Bezwingers"], [45656, "Mantelung des abtrünnigen Eroberers"], + [null, "Tier 10 Raid-Set"], // tag: 29 + [52027, "Weiheabzeichen des Eroberers"], [52026, "Weiheabzeichen des Beschützers"], [52025, "Weiheabzeichen des Bezwingers"], + [52030, "Weiheabzeichen des Eroberers (Heroisch)"], [52029, "Weiheabzeichen des Beschützers (Heroisch)"], [52028, "Weiheabzeichen des Bezwingers (Heroisch)"], + [null, "Tier 9 Raid-Set"], // tag: 27 + [47242, "Trophäe des Kreuzzugs"], [47558, "Ornat des großen Beschützers"], [47559, "Ornat des großen Bezwingers"], [47557, "Ornat des großen Eroberers"], - [34170, "Pantalons des schwindenden Zwists"], [34233, "Roben des schwindenden Lichts"], [34202, "Schal der Verwunderung"], - [34234, "Schattige Stulpen des Paroxysmus"], [34209, "Schiftung der Wiedergewinnung"], [45660, "Schiftung des abtrünnigen Beschützers"], - [45661, "Schiftung des abtrünnigen Bezwingers"], [45659, "Schiftung des abtrünnigen Eroberers"], [34193, "Schiftung des thalassischen Retters"], + [null, "Tier 8 Raid-Set"], // tag: 25 + [45654, "Beinplatten des abtrünnigen Beschützers"], [45655, "Beinplatten des abtrünnigen Bezwingers"], [45653, "Beinplatten des abtrünnigen Eroberers"], + [45633, "Brustplatte des abtrünnigen Beschützers"], [45634, "Brustplatte des abtrünnigen Bezwingers"], [45632, "Brustplatte des abtrünnigen Eroberers"], + [45636, "Brustschutz des abtrünnigen Beschützers"], [45637, "Brustschutz des abtrünnigen Bezwingers"], [45635, "Brustschutz des abtrünnigen Eroberers"], + [45651, "Gamaschen des abtrünnigen Beschützers"], [45652, "Gamaschen des abtrünnigen Bezwingers"], [45650, "Gamaschen des abtrünnigen Eroberers"], + [45645, "Handschuhe des abtrünnigen Beschützers"], [45646, "Handschuhe des abtrünnigen Bezwingers"], [45644, "Handschuhe des abtrünnigen Eroberers"], + [45648, "Helm des abtrünnigen Beschützers"], [45649, "Helm des abtrünnigen Bezwingers"], [45647, "Helm des abtrünnigen Eroberers"], + [45639, "Krone des abtrünnigen Beschützers"], [45640, "Krone des abtrünnigen Bezwingers"], [45638, "Krone des abtrünnigen Eroberers"], + [45657, "Mantelung des abtrünnigen Beschützers"], [45658, "Mantelung des abtrünnigen Bezwingers"], [45656, "Mantelung des abtrünnigen Eroberers"], + [45660, "Schiftung des abtrünnigen Beschützers"], [45661, "Schiftung des abtrünnigen Bezwingers"], [45659, "Schiftung des abtrünnigen Eroberers"], + [45642, "Stulpen des abtrünnigen Beschützers"], [45643, "Stulpen des abtrünnigen Bezwingers"], [45641, "Stulpen des abtrünnigen Eroberers"], + [null, "Tier 7 Raid-Set"], // tag: 23 + [40625, "Breastplate of the Lost Conqueror"], [40626, "Breastplate of the Lost Protector"], [40627, "Breastplate of the Lost Vanquisher"], + [40610, "Chestguard of the Lost Conqueror"], [40611, "Chestguard of the Lost Protector"], [40612, "Chestguard of the Lost Vanquisher"], + [40631, "Crown of the Lost Conqueror"], [40632, "Crown of the Lost Protector"], [40633, "Crown of the Lost Vanquisher"], + [40628, "Gauntlets of the Lost Conqueror"], [40629, "Gauntlets of the Lost Protector"], [40630, "Gauntlets of the Lost Vanquisher"], + [40613, "Gloves of the Lost Conqueror"], [40614, "Gloves of the Lost Protector"], [40615, "Gloves of the Lost Vanquisher"], + [40616, "Helm of the Lost Conqueror"], [40617, "Helm of the Lost Protector"], [40618, "Helm of the Lost Vanquisher"], + [40619, "Leggings of the Lost Conqueror"], [40620, "Leggings of the Lost Protector"], [40621, "Leggings of the Lost Vanquisher"], + [40634, "Legplates of the Lost Conqueror"], [40635, "Legplates of the Lost Protector"], [40636, "Legplates of the Lost Vanquisher"], + [40637, "Mantle of the Lost Conqueror"], [40638, "Mantle of the Lost Protector"], [40639, "Mantle of the Lost Vanquisher"], + [40622, "Spaulders of the Lost Conqueror"], [40623, "Spaulders of the Lost Protector"], [40624, "Spaulders of the Lost Vanquisher"], + [null, "Tier 6 Raid-Set"], // tag: 18 + [40635, "Beinplatten des verlorenen Beschützers"], [40636, "Beinplatten des verlorenen Bezwingers"], [40634, "Beinplatten des verlorenen Eroberers"], + [40626, "Brustplatte des verlorenen Beschützers"], [40627, "Brustplatte des verlorenen Bezwingers"], [40625, "Brustplatte des verlorenen Eroberers"], + [40611, "Brustschutz des verlorenen Beschützers"], [40612, "Brustschutz des verlorenen Bezwingers"], [40610, "Brustschutz des verlorenen Eroberers"], + [40620, "Gamaschen des verlorenen Beschützers"], [40621, "Gamaschen des verlorenen Bezwingers"], [40619, "Gamaschen des verlorenen Eroberers"], + [40614, "Handschuhe des verlorenen Beschützers"], [40615, "Handschuhe des verlorenen Bezwingers"], [40613, "Handschuhe des verlorenen Eroberers"], + [40617, "Helm des verlorenen Beschützers"], [40618, "Helm des verlorenen Bezwingers"], [40616, "Helm des verlorenen Eroberers"], + [40632, "Krone des verlorenen Beschützers"], [40633, "Krone des verlorenen Bezwingers"], [40631, "Krone des verlorenen Eroberers"], + [40638, "Mantel des verlorenen Beschützers"], [40639, "Mantel des verlorenen Bezwingers"], [40637, "Mantel des verlorenen Eroberers"], [40623, "Schiftung des verlorenen Beschützers"], [40624, "Schiftung des verlorenen Bezwingers"], [40622, "Schiftung des verlorenen Eroberers"], - [34195, "Schulterpolster der Vehemenz"], [34192, "Schulterstücke der Beharrlichkeit"], [30248, "Schulterstücke des bezwungenen Champions"], - [30250, "Schulterstücke des bezwungenen Helden"], [30249, "Schulterstücke des bezwungenen Verteidigers"], [29763, "Schulterstücke des gefallenen Champions"], - [29762, "Schulterstücke des gefallenen Helden"], [29764, "Schulterstücke des gefallenen Verteidigers"], [31103, "Schulterstücke des vergessenen Beschützers"], - [31102, "Schulterstücke des vergessenen Bezwingers"], [31101, "Schulterstücke des vergessenen Eroberers"], [34212, "Sonnenglühende Weste"], - [34857, "Stiefel des vergessenen Beschützers"], [34858, "Stiefel des vergessenen Bezwingers"], [34856, "Stiefel des vergessenen Eroberers"], - [34350, "Stulpen der alten Schattenmond"], [45642, "Stulpen des abtrünnigen Beschützers"], [45643, "Stulpen des abtrünnigen Bezwingers"], - [45641, "Stulpen des abtrünnigen Eroberers"], [40629, "Stulpen des verlorenen Beschützers"], [40630, "Stulpen des verlorenen Bezwingers"], - [40628, "Stulpen des verlorenen Eroberers"], [34180, "Teufelszornbeinplatten"], [34351, "Wickeltücher der ruhigen Majestät"], + [40629, "Stulpen des verlorenen Beschützers"], [40630, "Stulpen des verlorenen Bezwingers"], [40628, "Stulpen des verlorenen Eroberers"], + [null, "Tier 5 Raid-Set"], // tag: 13 + [30236, "Brustschutz des bezwungenen Champions"], [30238, "Brustschutz des bezwungenen Helden"], [30237, "Brustschutz des bezwungenen Verteidigers"], + [30245, "Gamaschen des bezwungenen Champions"], [30247, "Gamaschen des bezwungenen Helden"], [30246, "Gamaschen des bezwungenen Verteidigers"], + [30239, "Handschuhe des bezwungenen Champions"], [30241, "Handschuhe des bezwungenen Helden"], [30240, "Handschuhe des bezwungenen Verteidigers"], + [30242, "Helm des bezwungenen Champions"], [30244, "Helm des bezwungenen Helden"], [30243, "Helm des bezwungenen Verteidigers"], + [30248, "Schulterstücke des bezwungenen Champions"], [30250, "Schulterstücke des bezwungenen Helden"], [30249, "Schulterstücke des bezwungenen Verteidigers"], + [null, "Tier 4 Raid-Set"], // tag: 12 + [29754, "Brustschutz des gefallenen Champions"], [29755, "Brustschutz des gefallenen Helden"], [29753, "Brustschutz des gefallenen Verteidigers"], + [29766, "Gamaschen des gefallenen Champions"], [29765, "Gamaschen des gefallenen Helden"], [29767, "Gamaschen des gefallenen Verteidigers"], + [29757, "Handschuhe des gefallenen Champions"], [29756, "Handschuhe des gefallenen Helden"], [29758, "Handschuhe des gefallenen Verteidigers"], + [29760, "Helm des gefallenen Champions"], [29759, "Helm des gefallenen Helden"], [29761, "Helm des gefallenen Verteidigers"], + [29763, "Schulterstücke des gefallenen Champions"], [29762, "Schulterstücke des gefallenen Helden"], [29764, "Schulterstücke des gefallenen Verteidigers"], + [null, "Verschiedenes"], // sepmisc + [34208, "Ausgewuchtete Schulterklappen"], [34245, "Bedeckung von Ursol dem Weisen"], [34167, "Beinplatten des heiligen Molochs"], + [34211, "Brustharnisch der Fleischeslust"], [34216, "Brustschutz des heroischen Richters"], [34169, "Bundhosen der natürlichen Aggression"], + [34229, "Gewänder der stillen Küsten"], [34339, "Gugel des reinen Lichts"], [34332, "Gugel von Gul'dan"], + [34342, "Handschützer des Morgens"], [34244, "Haube des Doppelzüngigen"], [34243, "Helm der lodernden Rechtschaffenheit"], + [34186, "Kettenverbund des tobenden Sturmes"], [34215, "Kriegsharnisch des tollkühnen Furors"], [34345, "Krone von Anasterian"], + [34170, "Pantalons des schwindenden Zwists"], [34233, "Roben des schwindenden Lichts"], [34202, "Schal der Verwunderung"], + [34234, "Schattige Stulpen des Paroxysmus"], [34209, "Schiftung der Wiedergewinnung"], [34193, "Schiftung des thalassischen Retters"], + [34195, "Schulterpolster der Vehemenz"], [34192, "Schulterstücke der Beharrlichkeit"], [34212, "Sonnenglühende Weste"], + [34350, "Stulpen der alten Schattenmond"], [34180, "Teufelszornbeinplatten"], [34351, "Wickeltücher der ruhigen Majestät"] ], queststart: [ [3, "Gegenstand"], [1, "NPC"], [2, "Objekt"] ], questend: [ [1, "NPC"], [2, "Objekt"] ], @@ -3818,7 +3856,7 @@ var LANG = { [31, "Increase Run Speed %"], [32, "Mod Mounted Speed %"], [33, "Decrease Run Speed %"], [34, "Mod Maximum Health - Flat"], [35, "Mod Maximum Power - Flat"], [36, "Shapeshift"], [37, "Spell Effect Immunity"], [38, "Spell Aura Immunity"], [39, "Spell School Immunity"], [40, "Damage Immunity"], [41, "Dispel Type Immunity"], [42, "Proc Trigger Spell"], [43, "Proc Trigger Damage"], [44, "Track Creatures"], [45, "Track Resources"], - [46, "Ignore All Gear"], [47, "Mod Parry %"], /* [48, null] */ [49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], + [46, "Ignore All Gear"], [47, "Mod Parry %"], [48, "Periodic Trigger Spell from Client"],[49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], [51, "Mod Block %"], [52, "Mod Physical Crit Chance"], [53, "Periodically Drain Health"], [54, "Mod Physical Hit Chance"], [55, "Mod Spell Hit Chance"], [56, "Transform"], [57, "Mod Spell Crit Chance"], [58, "Increase Swim Speed %"], [59, "Mod Damage Done Versus Creature"],[60, "Pacify & Silence"], [61, "Mod Size %"], [62, "Periodically Transfer Health"], [63, "Periodically Transfer Power"], [64, "Periodically Drain Power"], [65, "Mod Spell Haste % (not stacking)"], @@ -3900,11 +3938,10 @@ var LANG = { [131, "Play Sound"], [132, "Play Music"], [133, "Unlearn Specialization"], [134, "Kill Credit2"], [135, "Call Pet"], [136, "Heal for % of Total Health"], [137, "Give % of Total Power"], [138, "Leap Back"], [139, "Abandon Quest"], [140, "Force Spell Cast"], [141, "Force Spell Cast with Value"], [142, "Trigger Spell with Value"], [143, "Apply Area Aura - Pet Owner"], [144, "Knockback to Dest."], [145, "Pull Towards Dest."], - [146, "Activate Rune"], [147, "Fail Quest"], [149, "Charge to Dest."], [150, "Start Quest"], + [146, "Activate Rune"], [147, "Fail Quest"], [148, "Trigger Missile with Value"], [149, "Charge to Dest."], [150, "Start Quest"], [151, "Trigger Spell 2"], [152, "Summon - Refer-A-Friend"], [153, "Create Tamed Pet"], [154, "Discover Flight Path"], [155, "Dual Wield 2H Weapons"], - [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], - [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"], - [167, "Update Player Phase"] + [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], [160, "Force Spell Cast 2"], + [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"] ], damagetype: [ [1, "Keins"], [2, "Magie"], [3, "Nahkampf"], [4, "Distanz"] @@ -4010,10 +4047,10 @@ var LANG = { otpvp: "Erworben durch PvP", otredemption: "Erworben durch Einlösung", otskinning: "Erworben durch Kürschnerei", - purchasablewithcurrency: "Mit einer Währung erwerbbar", - purchasablewith: "Mit einer Währung erwerbbar...", + purchasablewithitem: "Mit einem Gegenstand erwerbbar...", purchasablewithhonor: "Erworben mit Ehrenpunkten", purchasablewitharena: "Erworben mit Arenapunkten", + purchasablewithcurrency: "Mit einer Währung erwerbbar...", rewardedbyachievement: "Belohnung eines Erfolgs", rewardedbyfactionquest: "Belohnung einer Quest", rewardedbyquestin: "Belohnung einer Quest in...", @@ -4130,6 +4167,7 @@ var LANG = { id: "ID", classspecific: "Klassenspezifisch", racespecific: "Volksspezifisch", + setspvpflag: "Hält Euch im PvP", sepgainsrewards: "Belohnungen/Steigerungen", experiencegained: "Erhaltene Erfahrung", @@ -4900,10 +4938,18 @@ var LANG = { /* AoWoW: start custom */ + // Profiler completions import + completion: 'Vervollständigung', // WH.TERMS.completion + complete: 'Vollständig', // WH.TERMS.complete + incomplete: 'Unvollständig', // WH.TERMS.incomplete + parens_format: '$1 ($2)', // WH.TERMS.parens_format + // click to copy fn copied: 'Kopiert', clickToCopy: 'Klicke zum Kopieren', nothingToCopy_tip: 'Nichts zu kopieren!', + copy_clipboard: 'Kopieren', + copy_format: 'Kopiere %s', // TC conditions display tab_conditions: 'Konditionen', diff --git a/static/js/locale_enus.js b/static/js/locale_enus.js index 417ee9ab..6dc726c0 100644 --- a/static/js/locale_enus.js +++ b/static/js/locale_enus.js @@ -1578,6 +1578,7 @@ var g_quest_sorts = { "-374": 'Noblegarden', "-375": 'Pilgrim\'s Bounty', "-376": 'Love is in the Air', + 0: 'Uncategorized', 1: 'Dun Morogh', 3: 'Badlands', 4: 'Blasted Lands', @@ -2568,7 +2569,8 @@ var g_conditions = { 47: 'The Player has$N: not; $2 [quest=$1]', 48: 'The Player has$N: not; collected $3 towards objective #$2 of [quest=$1]', 49: 'The current map difficulty is #$1', - 50: 'The Player $C$1$Ncan:can\'t; be:is$N: not;; a Gamemaster' + 50: 'The Player $C$1$Ncan:can\'t; be:is$N: not;; a Gamemaster', + 58: 'The StringID of this entity is$N: not; $4' }; /* end aowow custom */ @@ -3146,6 +3148,8 @@ var LANG = { tooltip_uprate: "Insightful/funny", tooltip_zonelink: "Clicking on this link will<br />take you to the zone page.", + tooltip_immune: "Immune", // aowow - custom + reputationhistory: "Reputation History", reputationaction: "Reputation Action", @@ -3661,59 +3665,73 @@ var LANG = { num: [ [1, ">"], [2, ">="], [3, "="], [4, "<="], [5, "<"], [6, "!="]], side: [ [1, "Yes"], [2, "Alliance"], [3, "Horde"], [4, "Both"], [5, "No"] ], faction: [ - [469, "Alliance"], [1037, "Alliance Vanguard"], [1106, "Argent Crusade"], [529, "Argent Dawn"], [1012, "Ashtongue Deathsworn"], - [87, "Bloodsail Buccaneers"], [21, "Booty Bay"], [910, "Brood of Nozdormu"], [609, "Cenarion Circle"], [942, "Cenarion Expedition"], - [909, "Darkmoon Faire"], [530, "Darkspear Trolls"], [69, "Darnassus"], [577, "Everlook"], [930, "Exodar"], - [1068, "Explorers' League"], [1104, "Frenzyheart Tribe"], [729, "Frostwolf Clan"], [369, "Gadgetzan"], [92, "Gelkis Clan Centaur"], - [54, "Gnomeregan Exiles"], [946, "Honor Hold"], [67, "Horde"], [1052, "Horde Expedition"], [749, "Hydraxian Waterlords"], - [47, "Ironforge"], [989, "Keepers of Time"], [1090, "Kirin Tor"], [1098, "Knights of the Ebon Blade"], [978, "Kurenai"], - [1011, "Lower City"], [93, "Magram Clan Centaur"], [1015, "Netherwing"], [1038, "Ogri'la"], [76, "Orgrimmar"], - [470, "Ratchet"], [349, "Ravenholdt"], [1031, "Sha'tari Skyguard"], [1077, "Shattered Sun Offensive"], [809, "Shen'dralar"], - [911, "Silvermoon City"], [890, "Silverwing Sentinels"], [970, "Sporeggar"], [169, "Steamwheedle Cartel"], [730, "Stormpike Guard"], - [72, "Stormwind"], [70, "Syndicate"], [932, "The Aldor"], [1156, "The Ashen Verdict"], [933, "The Consortium"], - [510, "The Defilers"], [1126, "The Frostborn"], [1067, "The Hand of Vengeance"], [1073, "The Kalu'ak"], [509, "The League of Arathor"], - [941, "The Mag'har"], [1105, "The Oracles"], [990, "The Scale of the Sands"], [934, "The Scryers"], [935, "The Sha'tar"], - [1094, "The Silver Covenant"], [1119, "The Sons of Hodir"], [1124, "The Sunreavers"], [1064, "The Taunka"], [967, "The Violet Eye"], - [1091, "The Wyrmrest Accord"], [59, "Thorium Brotherhood"], [947, "Thrallmar"], [81, "Thunder Bluff"], [576, "Timbermaw Hold"], - [922, "Tranquillien"], [68, "Undercity"], [1050, "Valiance Expedition"], [1085, "Warsong Offensive"], [889, "Warsong Outriders"], - [589, "Wintersaber Trainers"], [270, "Zandalar Tribe"] + [null, "Classic"], + [ 469, "Alliance"], [ 529, "Argent Dawn"], [ 87, "Bloodsail Buccaneers"], [ 21, "Booty Bay"], [ 910, "Brood of Nozdormu"], + [ 609, "Cenarion Circle"], [ 909, "Darkmoon Faire"], [ 530, "Darkspear Trolls"], [ 69, "Darnassus"], [ 577, "Everlook"], + [ 930, "Exodar"], [ 729, "Frostwolf Clan"], [ 369, "Gadgetzan"], [ 92, "Gelkis Clan Centaur"], [ 54, "Gnomeregan Exiles"], + [ 67, "Horde"], [ 749, "Hydraxian Waterlords"], [ 47, "Ironforge"], [ 93, "Magram Clan Centaur"], [ 76, "Orgrimmar"], + [ 470, "Ratchet"], [ 349, "Ravenholdt"], [ 809, "Shen'dralar"], [ 911, "Silvermoon City"], [ 890, "Silverwing Sentinels"], + [ 169, "Steamwheedle Cartel"], [ 730, "Stormpike Guard"], [ 72, "Stormwind"], [ 510, "The Defilers"], [ 509, "The League of Arathor"], + [ 59, "Thorium Brotherhood"], [ 81, "Thunder Bluff"], [ 576, "Timbermaw Hold"], [ 922, "Tranquillien"], [ 68, "Undercity"], + [ 889, "Warsong Outriders"], [ 270, "Zandalar Tribe"], + [null, "Burning Crusade"], + [1012, "Ashtongue Deathsworn"], [ 942, "Cenarion Expedition"], [ 946, "Honor Hold"], [ 989, "Keepers of Time"], [ 978, "Kurenai"], + [1011, "Lower City"], [1015, "Netherwing"], [1038, "Ogri'la"], [1031, "Sha'tari Skyguard"], [1077, "Shattered Sun Offensive"], + [ 970, "Sporeggar"], [ 932, "The Aldor"], [ 933, "The Consortium"], [ 941, "The Mag'har"], [ 990, "The Scale of the Sands"], + [ 934, "The Scryers"], [ 935, "The Sha'tar"], [ 967, "The Violet Eye"], [ 947, "Thrallmar"], + [null, "Wrath of the Lich King"], + [1037, "Alliance Vanguard"], [1106, "Argent Crusade"], [1068, "Explorers' League"], [1104, "Frenzyheart Tribe"], [1052, "Horde Expedition"], + [1090, "Kirin Tor"], [1098, "Knights of the Ebon Blade"], [1156, "The Ashen Verdict"], [1126, "The Frostborn"], [1067, "The Hand of Vengeance"], + [1073, "The Kalu'ak"], [1105, "The Oracles"], [1094, "The Silver Covenant"], [1119, "The Sons of Hodir"], [1124, "The Sunreavers"], + [1064, "The Taunka"], [1091, "The Wyrmrest Accord"], [1050, "Valiance Expedition"], [1085, "Warsong Offensive"], + [null, "Miscellaneous"], + [ 70, "Syndicate"], [ 589, "Wintersaber Trainers"] ], zone: [ - [4494, "Ahn'kahet: The Old Kingdom"], [36, "Alterac Mountains"], [2597, "Alterac Valley"], [3358, "Arathi Basin"], [45, "Arathi Highlands"], - [331, "Ashenvale"], [3790, "Auchenai Crypts"], [4277, "Azjol-Nerub"], [16, "Azshara"], [3524, "Azuremyst Isle"], - [3, "Badlands"], [3959, "Black Temple"], [719, "Blackfathom Deeps"], [1584, "Blackrock Depths"], [25, "Blackrock Mountain"], - [1583, "Blackrock Spire"], [2677, "Blackwing Lair"], [3702, "Blade's Edge Arena"], [3522, "Blade's Edge Mountains"], [4, "Blasted Lands"], - [3525, "Bloodmyst Isle"], [3537, "Borean Tundra"], [46, "Burning Steppes"], [1941, "Caverns of Time"], [2918, "Champions' Hall"], - [3905, "Coilfang Reservoir"], [4024, "Coldarra"], [2817, "Crystalsong Forest"], [4395, "Dalaran"], [4378, "Dalaran Sewers"], - [148, "Darkshore"], [393, "Darkspear Strand"], [1657, "Darnassus"], [41, "Deadwind Pass"], [2257, "Deeprun Tram"], - [405, "Desolace"], [2557, "Dire Maul"], [65, "Dragonblight"], [4196, "Drak'Tharon Keep"], [1, "Dun Morogh"], - [14, "Durotar"], [10, "Duskwood"], [15, "Dustwallow Marsh"], [139, "Eastern Plaguelands"], [12, "Elwynn Forest"], - [3430, "Eversong Woods"], [3820, "Eye of the Storm"], [361, "Felwood"], [357, "Feralas"], [3433, "Ghostlands"], - [721, "Gnomeregan"], [394, "Grizzly Hills"], [3923, "Gruul's Lair"], [4416, "Gundrak"], [2917, "Hall of Legends"], - [4272, "Halls of Lightning"], [4820, "Halls of Reflection"], [4264, "Halls of Stone"], [3483, "Hellfire Peninsula"], [3562, "Hellfire Ramparts"], - [267, "Hillsbrad Foothills"], [495, "Howling Fjord"], [4742, "Hrothgar's Landing"], [3606, "Hyjal Summit"], [210, "Icecrown"], - [4812, "Icecrown Citadel"], [1537, "Ironforge"], [4710, "Isle of Conquest"], [4080, "Isle of Quel'Danas"], [3457, "Karazhan"], - [38, "Loch Modan"], [4131, "Magisters' Terrace"], [3836, "Magtheridon's Lair"], [3792, "Mana-Tombs"], [2100, "Maraudon"], - [2717, "Molten Core"], [493, "Moonglade"], [215, "Mulgore"], [3518, "Nagrand"], [3698, "Nagrand Arena"], - [3456, "Naxxramas"], [3523, "Netherstorm"], [2367, "Old Hillsbrad Foothills"], [2159, "Onyxia's Lair"], [1637, "Orgrimmar"], - [4813, "Pit of Saron"], [4298, "The Scarlet Enclave"], [2437, "Ragefire Chasm"], [722, "Razorfen Downs"], - [491, "Razorfen Kraul"], [44, "Redridge Mountains"], [3429, "Ruins of Ahn'Qiraj"], [3968, "Ruins of Lordaeron"], [796, "Scarlet Monastery"], - [2057, "Scholomance"], [51, "Searing Gorge"], [3607, "Serpentshrine Cavern"], [3791, "Sethekk Halls"], [3789, "Shadow Labyrinth"], - [209, "Shadowfang Keep"], [3520, "Shadowmoon Valley"], [3703, "Shattrath City"], [3711, "Sholazar Basin"], [1377, "Silithus"], - [3487, "Silvermoon City"], [130, "Silverpine Forest"], [3679, "Skettis"], [406, "Stonetalon Mountains"], [1519, "Stormwind City"], - [4384, "Strand of the Ancients"], [33, "Stranglethorn Vale"], [2017, "Stratholme"], [1477, "Sunken Temple"], [4075, "Sunwell Plateau"], - [8, "Swamp of Sorrows"], [440, "Tanaris"], [141, "Teldrassil"], [3428, "Temple of Ahn'Qiraj"], [3519, "Terokkar Forest"], - [3848, "The Arcatraz"], [17, "The Barrens"], [2366, "The Black Morass"], [3840, "The Black Temple"], [3713, "The Blood Furnace"], - [3847, "The Botanica"], [3775, "The Circle of Blood"], [4100, "The Culling of Stratholme"], [1581, "The Deadmines"], [3557, "The Exodar"], - [3845, "The Eye"], [4500, "The Eye of Eternity"], [4809, "The Forge of Souls"], [47, "The Hinterlands"], [3849, "The Mechanar"], - [4265, "The Nexus"], [4493, "The Obsidian Sanctum"], [4228, "The Oculus"], [3698, "The Ring of Trials"], [4406, "The Ring of Valor"], - [3714, "The Shattered Halls"], [3717, "The Slave Pens"], [3715, "The Steamvault"], [717, "The Stockade"], [67, "The Storm Peaks"], - [3716, "The Underbog"], [457, "The Veiled Sea"], [4415, "The Violet Hold"], [400, "Thousand Needles"], [1638, "Thunder Bluff"], - [1216, "Timbermaw Hold"], [85, "Tirisfal Glades"], [4723, "Trial of the Champion"], [4722, "Trial of the Crusader"], [1337, "Uldaman"], - [4273, "Ulduar"], [490, "Un'Goro Crater"], [1497, "Undercity"], [206, "Utgarde Keep"], [1196, "Utgarde Pinnacle"], - [4603, "Vault of Archavon"], [718, "Wailing Caverns"], [3277, "Warsong Gulch"], [28, "Western Plaguelands"], [40, "Westfall"], - [11, "Wetlands"], [4197, "Wintergrasp"], [618, "Winterspring"], [3521, "Zangarmarsh"], [3805, "Zul'Aman"], - [66, "Zul'Drak"], [1176, "Zul'Farrak"], [1977, "Zul'Gurub"] + [null, "Eastern Kingdoms"], + [ 36, "Alterac Mountains"], [ 45, "Arathi Highlands"], [ 3, "Badlands"], [ 4, "Blasted Lands"], [ 46, "Burning Steppes"], + [ 41, "Deadwind Pass"], [2257, "Deeprun Tram"], [ 1, "Dun Morogh"], [ 10, "Duskwood"], [ 139, "Eastern Plaguelands"], + [ 12, "Elwynn Forest"], [3430, "Eversong Woods"], [3433, "Ghostlands"], [ 267, "Hillsbrad Foothills"], [1537, "Ironforge"], + [4080, "Isle of Quel'Danas"], [ 38, "Loch Modan"], [4298, "Plaguelands: The Scarlet Enclave"], [ 44, "Redridge Mountains"], + [ 51, "Searing Gorge"], [3487, "Silvermoon City"], [ 130, "Silverpine Forest"], [1519, "Stormwind City"], [ 33, "Stranglethorn Vale"], + [ 8, "Swamp of Sorrows"], [ 47, "The Hinterlands"], [ 85, "Tirisfal Glades"], [1497, "Undercity"], [ 28, "Western Plaguelands"], + [ 40, "Westfall"], [ 11, "Wetlands"], + [null, "Kalimdor"], + [ 331, "Ashenvale"], [ 16, "Azshara"], [3524, "Azuremyst Isle"], [3525, "Bloodmyst Isle"], [ 148, "Darkshore"], + [1657, "Darnassus"], [ 405, "Desolace"], [ 14, "Durotar"], [ 15, "Dustwallow Marsh"], [ 361, "Felwood"], + [ 357, "Feralas"], [ 493, "Moonglade"], [ 215, "Mulgore"], [1637, "Orgrimmar"], [1377, "Silithus"], + [ 406, "Stonetalon Mountains"], [ 440, "Tanaris"], [ 141, "Teldrassil"], [ 17, "The Barrens"], [3557, "The Exodar"], + [ 400, "Thousand Needles"], [1638, "Thunder Bluff"], [ 490, "Un'Goro Crater"], [ 618, "Winterspring"], + [null, "Outland"], + [3522, "Blade's Edge Mountains"], [3483, "Hellfire Peninsula"], [3518, "Nagrand"], [3523, "Netherstorm"], [3520, "Shadowmoon Valley"], + [3703, "Shattrath City"], [3519, "Terokkar Forest"], [3521, "Zangarmarsh"], + [null, "Northrend"], + [3537, "Borean Tundra"], [2817, "Crystalsong Forest"], [4395, "Dalaran"], [ 65, "Dragonblight"], [ 394, "Grizzly Hills"], + [ 495, "Howling Fjord"], [4742, "Hrothgar's Landing"], [ 210, "Icecrown"], [3711, "Sholazar Basin"], [ 67, "The Storm Peaks"], + [4197, "Wintergrasp"], [ 66, "Zul'Drak"], + [null, "Dungeons"], + [4494, "Ahn'kahet: The Old Kingdom"], [3790, "Auchenai Crypts"], [4277, "Azjol-Nerub"], [ 719, "Blackfathom Deeps"], [1584, "Blackrock Depths"], + [1583, "Blackrock Spire"], [3713, "Blood Furnace"], [1581, "Deadmines"], [2557, "Dire Maul"], [4196, "Drak'Tharon Keep"], + [ 721, "Gnomeregan"], [4416, "Gundrak"], [4272, "Halls of Lightning"], [4820, "Halls of Reflection"], [4264, "Halls of Stone"], + [3562, "Hellfire Ramparts"], [4131, "Magisters' Terrace"], [3792, "Mana-Tombs"], [2100, "Maraudon"], [2367, "Old Hillsbrad Foothills"], + [4813, "Pit of Saron"], [2437, "Ragefire Chasm"], [ 722, "Razorfen Downs"], [ 491, "Razorfen Kraul"], [ 796, "Scarlet Monastery"], + [2057, "Scholomance"], [3791, "Sethekk Halls"], [3789, "Shadow Labyrinth"], [ 209, "Shadowfang Keep"], [3714, "Shattered Halls"], + [3717, "Slave Pens"], [ 717, "Stormwind Stockade"], [2017, "Stratholme"], [1477, "Sunken Temple"], [3848, "The Arcatraz"], + [2366, "The Black Morass"], [3847, "The Botanica"], [4100, "The Culling of Stratholme"], [4809, "The Forge of Souls"], [3717, "The Frost Lord Ahune"], + [3849, "The Mechanar"], [4265, "The Nexus"], [4228, "The Oculus"], [3715, "The Steamvault"], [4723, "Trial of the Champion"], + [1337, "Uldaman"], [3716, "Underbog"], [ 206, "Utgarde Keep"], [1196, "Utgarde Pinnacle"], [4415, "Violet Hold"], + [ 718, "Wailing Caverns"], [1176, "Zul'Farrak"], + [null, "Raids"], + [3428, "Ahn'Qiraj Temple"], [3959, "Black Temple"], [2677, "Blackwing Lair"], [3923, "Gruul's Lair"], [4812, "Icecrown Citadel"], + [3457, "Karazhan"], [3836, "Magtheridon's Lair"], [2717, "Molten Core"], [3456, "Naxxramas"], [2159, "Onyxia's Lair"], + [3429, "Ruins of Ahn'Qiraj"], [3607, "Serpentshrine Cavern"], [3845, "Tempest Keep"], [3606, "The Battle for Mount Hyjal"], [4500, "The Eye of Eternity"], + [4493, "The Obsidian Sanctum"], [4987, "The Ruby Sanctum"], [4075, "The Sunwell"], [4722, "Trial of the Crusader"], [4273, "Ulduar"], + [4603, "Vault of Archavon"], [3805, "Zul'Aman"], [1977, "Zul'Gurub"], + [null, "Battlegrounds"], + [2597, "Alterac Valley"], [3358, "Arathi Basin"], [3820, "Eye of the Storm"], [4710, "Isle of Conquest"], [4384, "Strand of the Ancients"], + [3277, "Warsong Gulch"], + [null, "Arenas"], + [3702, "Blade's Edge Arena"], [4378, "Dalaran Arena"], [3698, "Nagrand Arena"], [3968, "Ruins of Lordaeron"], [4406, "The Ring of Valor"] ], resistance: [ [6, "Arcane"], [2, "Fire"], [3, "Nature"], [4, "Frost"], [5, "Shadow"], @@ -3760,65 +3778,81 @@ var LANG = { ], pvp: [ [1, "Yes"], [3, "Arena"], [4, "Battleground"], [5, "World"], [2, "No"] ], currency: [ - [32572, "Apexis Crystal"], [32569, "Apexis Shard"], [29736, "Arcane Rune"], [44128, "Arctic Fur"], [20560, "Alterac Valley Mark of Honor"], - [20559, "Arathi Basin Mark of Honor"], [29434, "Badge of Justice"], [37829, "Brewfest Prize Token"], [23247, "Burning Blossom"], [44990, "Champion's Seal"], - [24368, "Coilfang Armaments"], [52027, "Conqueror's Mark of Sanctification"], [52030, "Conqueror's Mark of Sanctification (Heroic)"], - [43016, "Dalaran Cooking Award"], [41596, "Dalaran Jewelcrafter's Token"],[34052, "Dream Shard"], [45624, "Emblem of Conquest"], [49426, "Emblem of Frost"], - [40752, "Emblem of Heroism"], [47241, "Emblem of Triumph"], [40753, "Emblem of Valor"], [29024, "Eye of the Storm Mark of Honor"], - [24245, "Glowcap"], [26045, "Halaa Battle Token"], [26044, "Halaa Research Token"], [38425, "Heavy Borean Leather"], [29735, "Holy Dust"], - [24579, "Mark of Honor Hold"], [24581, "Mark of Thrallmar"], [32897, "Mark of the Illidari"], [22484, "Necrotic Rune"], [52026, "Protector's Mark of Sanctification"], - [52029, "Protector's Mark of Sanctification (Heroic)"], [4291, "Silken Thread"], [28558, "Spirit Shard"], [43228, "Stone Keeper's Shard"], - [34664, "Sunmote"], [47242, "Trophy of the Crusade"], [52025, "Vanquisher's Mark of Sanctification"], [52028, "Vanquisher's Mark of Sanctification (Heroic)"], - [37836, "Venture Coin"], [20558, "Warsong Gulch Mark of Honor"], [34597, "Winterfin Clam"], [43589, "Wintergrasp Mark of Honor"] + [null, "Classic"], + [37829, "Brewfest Prize Token"], [23247, "Burning Blossom"], [4291, "Silken Thread"], + [null, "Burning Crusade"], + [32572, "Apexis Crystal"], [32569, "Apexis Shard"], [29736, "Arcane Rune"], [24368, "Coilfang Armaments"], [24245, "Glowcap"], + [29735, "Holy Dust"], [32897, "Mark of the Illidari"], [22484, "Necrotic Rune"], [34664, "Sunmote"], + [null, "Wrath of the Lich King"], + [44128, "Arctic Fur"], [44990, "Champion's Seal"], [43016, "Dalaran Cooking Award"], [41596, "Dalaran Jewelcrafter's Token"],[34052, "Dream Shard"], + [38425, "Heavy Borean Leather"], [34597, "Winterfin Clam"], + [null, "Player vs. Player"], + [20560, "Alterac Valley Mark of Honor"],[20559, "Arathi Basin Mark of Honor"], [29024, "Eye of the Storm Mark of Honor"], [26045, "Halaa Battle Token"], + [26044, "Halaa Research Token"], [24579, "Mark of Honor Hold"], [24581, "Mark of Thrallmar"], [28558, "Spirit Shard"], [43228, "Stone Keeper's Shard"], + [37836, "Venture Coin"], [20558, "Warsong Gulch Mark of Honor"], [43589, "Wintergrasp Mark of Honor"], + [null, "Dungeon and Raid"], + [29434, "Badge of Justice"], [45624, "Emblem of Conquest"], [49426, "Emblem of Frost"], [40752, "Emblem of Heroism"], [47241, "Emblem of Triumph"], + [40753, "Emblem of Valor"] ], itemcurrency: [ + [null, "Tier 10 Raid Set"], + [52027, "Conqueror's Mark of Sanctification"], [52026, "Protextor's Mark of Sanctification"], [52025, "Vanquisher's Mark of Sanctification"], + [52030, "Conqueror's Mark of Sanctification (Heroic)"], [52029, "Protector's Mark of Sanctification (Heroic)"], [52028, "Vanquisher's Mark of Sanctification (Heroic)"], + [null, "Tier 9 Raid Set"], + [47242, "Trophy of the Crusade"], + [47557, "Regalia of the Grand Conqueror"], [47558, "Regalia of the Grand Protector"], [47559, "Regalia of the Grand Vanquisher"], + [null, "Tier 8 Raid Set"], + [45632, "Breastplate of the Wayward Conqueror"], [45633, "Breastplate of the Wayward Protector"], [45634, "Breastplate of the Wayward Vanquisher"], + [45635, "Chestguard of the Wayward Conqueror"], [45636, "Chestguard of the Wayward Protector"], [45637, "Chestguard of the Wayward Vanquisher"], + [45638, "Crown of the Wayward Conqueror"], [45639, "Crown of the Wayward Protector"], [45640, "Crown of the Wayward Vanquisher"], + [45641, "Gauntlets of the Wayward Conqueror"], [45642, "Gauntlets of the Wayward Protector"], [45643, "Gauntlets of the Wayward Vanquisher"], + [45644, "Gloves of the Wayward Conqueror"], [45645, "Gloves of the Wayward Protector"], [45646, "Gloves of the Wayward Vanquisher"], + [45647, "Helm of the Wayward Conqueror"], [45648, "Helm of the Wayward Protector"], [45649, "Helm of the Wayward Vanquisher"], + [45650, "Leggings of the Wayward Conqueror"], [45651, "Leggings of the Wayward Protector"], [45652, "Leggings of the Wayward Vanquisher"], + [45653, "Legplates of the Wayward Conqueror"], [45654, "Legplates of the Wayward Protector"], [45655, "Legplates of the Wayward Vanquisher"], + [45656, "Mantle of the Wayward Conqueror"], [45657, "Mantle of the Wayward Protector"], [45658, "Mantle of the Wayward Vanquisher"], + [45659, "Spaulders of the Wayward Conqueror"], [45660, "Spaulders of the Wayward Protector"], [45661, "Spaulders of the Wayward Vanquisher"], + [null, "Tier 7 Raid Set"], + [40625, "Breastplate of the Lost Conqueror"], [40626, "Breastplate of the Lost Protector"], [40627, "Breastplate of the Lost Vanquisher"], + [40610, "Chestguard of the Lost Conqueror"], [40611, "Chestguard of the Lost Protector"], [40612, "Chestguard of the Lost Vanquisher"], + [40631, "Crown of the Lost Conqueror"], [40632, "Crown of the Lost Protector"], [40633, "Crown of the Lost Vanquisher"], + [40628, "Gauntlets of the Lost Conqueror"], [40629, "Gauntlets of the Lost Protector"], [40630, "Gauntlets of the Lost Vanquisher"], + [40613, "Gloves of the Lost Conqueror"], [40614, "Gloves of the Lost Protector"], [40615, "Gloves of the Lost Vanquisher"], + [40616, "Helm of the Lost Conqueror"], [40617, "Helm of the Lost Protector"], [40618, "Helm of the Lost Vanquisher"], + [40619, "Leggings of the Lost Conqueror"], [40620, "Leggings of the Lost Protector"], [40621, "Leggings of the Lost Vanquisher"], + [40634, "Legplates of the Lost Conqueror"], [40635, "Legplates of the Lost Protector"], [40636, "Legplates of the Lost Vanquisher"], + [40637, "Mantle of the Lost Conqueror"], [40638, "Mantle of the Lost Protector"], [40639, "Mantle of the Lost Vanquisher"], + [40622, "Spaulders of the Lost Conqueror"], [40623, "Spaulders of the Lost Protector"], [40624, "Spaulders of the Lost Vanquisher"], + [null, "Tier 6 Raid Set"], [34853, "Belt of the Forgotten Conqueror"], [34854, "Belt of the Forgotten Protector"], [34855, "Belt of the Forgotten Vanquisher"], [34856, "Boots of the Forgotten Conqueror"], [34857, "Boots of the Forgotten Protector"], [34858, "Boots of the Forgotten Vanquisher"], [34848, "Bracers of the Forgotten Conqueror"], [34851, "Bracers of the Forgotten Protector"], [34852, "Bracers of the Forgotten Vanquisher"], - [40625, "Breastplate of the Lost Conqueror"], [40626, "Breastplate of the Lost Protector"], [40627, "Breastplate of the Lost Vanquisher"], - [45632, "Breastplate of the Wayward Conqueror"], [45633, "Breastplate of the Wayward Protector"], [45634, "Breastplate of the Wayward Vanquisher"], - [34169, "Breeches of Natural Aggression"], [34186, "Chain Links of the Tumultuous Storm"], [29754, "Chestguard of the Fallen Champion"], - [29753, "Chestguard of the Fallen Defender"], [29755, "Chestguard of the Fallen Hero"], [31089, "Chestguard of the Forgotten Conqueror"], - [31091, "Chestguard of the Forgotten Protector"], [31090, "Chestguard of the Forgotten Vanquisher"], [40610, "Chestguard of the Lost Conqueror"], - [40611, "Chestguard of the Lost Protector"], [40612, "Chestguard of the Lost Vanquisher"], [30236, "Chestguard of the Vanquished Champion"], - [30237, "Chestguard of the Vanquished Defender"], [30238, "Chestguard of the Vanquished Hero"], [45635, "Chestguard of the Wayward Conqueror"], - [45636, "Chestguard of the Wayward Protector"], [45637, "Chestguard of the Wayward Vanquisher"], [34245, "Cover of Ursol the Wise"], - [34332, "Cowl of Gul'dan"], [34339, "Cowl of Light's Purity"], [34345, "Crown of Anasterian"], - [40631, "Crown of the Lost Conqueror"], [40632, "Crown of the Lost Protector"], [40633, "Crown of the Lost Vanquisher"], - [45638, "Crown of the Wayward Conqueror"], [45639, "Crown of the Wayward Protector"], [45640, "Crown of the Wayward Vanquisher"], - [34244, "Duplicitous Guise"], [34208, "Equilibrium Epaulets"], [34180, "Felfury Legplates"], - [34229, "Garments of Serene Shores"], [34350, "Gauntlets of the Ancient Shadowmoon"], [40628, "Gauntlets of the Lost Conqueror"], - [40629, "Gauntlets of the Lost Protector"], [40630, "Gauntlets of the Lost Vanquisher"], [45641, "Gauntlets of the Wayward Conqueror"], - [45642, "Gauntlets of the Wayward Protector"], [45643, "Gauntlets of the Wayward Vanquisher"], [29757, "Gloves of the Fallen Champion"], - [29758, "Gloves of the Fallen Defender"], [29756, "Gloves of the Fallen Hero"], [31092, "Gloves of the Forgotten Conqueror"], - [31094, "Gloves of the Forgotten Protector"], [31093, "Gloves of the Forgotten Vanquisher"], [40613, "Gloves of the Lost Conqueror"], - [40614, "Gloves of the Lost Protector"], [40615, "Gloves of the Lost Vanquisher"], [30239, "Gloves of the Vanquished Champion"], - [30240, "Gloves of the Vanquished Defender"], [30241, "Gloves of the Vanquished Hero"], [45644, "Gloves of the Wayward Conqueror"], - [45645, "Gloves of the Wayward Protector"], [45646, "Gloves of the Wayward Vanquisher"], [34342, "Handguards of the Dawn"], - [34211, "Harness of Carnal Instinct"], [34243, "Helm of Burning Righteousness"], [29760, "Helm of the Fallen Champion"], - [29761, "Helm of the Fallen Defender"], [29759, "Helm of the Fallen Hero"], [31097, "Helm of the Forgotten Conqueror"], - [31095, "Helm of the Forgotten Protector"], [31096, "Helm of the Forgotten Vanquisher"], [40616, "Helm of the Lost Conqueror"], - [40617, "Helm of the Lost Protector"], [40618, "Helm of the Lost Vanquisher"], [30242, "Helm of the Vanquished Champion"], - [30243, "Helm of the Vanquished Defender"], [30244, "Helm of the Vanquished Hero"], [45647, "Helm of the Wayward Conqueror"], - [45648, "Helm of the Wayward Protector"], [45649, "Helm of the Wayward Vanquisher"], [34216, "Heroic Judicator's Chestguard"], - [29766, "Leggings of the Fallen Champion"], [29767, "Leggings of the Fallen Defender"], [29765, "Leggings of the Fallen Hero"], + [31089, "Chestguard of the Forgotten Conqueror"], [31091, "Chestguard of the Forgotten Protector"], [31090, "Chestguard of the Forgotten Vanquisher"], + [31092, "Gloves of the Forgotten Conqueror"], [31094, "Gloves of the Forgotten Protector"], [31093, "Gloves of the Forgotten Vanquisher"], + [31097, "Helm of the Forgotten Conqueror"], [31095, "Helm of the Forgotten Protector"], [31096, "Helm of the Forgotten Vanquisher"], [31098, "Leggings of the Forgotten Conqueror"], [31100, "Leggings of the Forgotten Protector"], [31099, "Leggings of the Forgotten Vanquisher"], - [40619, "Leggings of the Lost Conqueror"], [40620, "Leggings of the Lost Protector"], [40621, "Leggings of the Lost Vanquisher"], - [30245, "Leggings of the Vanquished Champion"], [30246, "Leggings of the Vanquished Defender"], [30247, "Leggings of the Vanquished Hero"], - [45650, "Leggings of the Wayward Conqueror"], [45651, "Leggings of the Wayward Protector"], [45652, "Leggings of the Wayward Vanquisher"], - [34167, "Legplates of the Holy Juggernaut"], [40634, "Legplates of the Lost Conqueror"], [40635, "Legplates of the Lost Protector"], - [40636, "Legplates of the Lost Vanquisher"], [45653, "Legplates of the Wayward Conqueror"], [45654, "Legplates of the Wayward Protector"], - [45655, "Legplates of the Wayward Vanquisher"], [40637, "Mantle of the Lost Conqueror"], [40638, "Mantle of the Lost Protector"], - [40639, "Mantle of the Lost Vanquisher"], [45656, "Mantle of the Wayward Conqueror"], [45657, "Mantle of the Wayward Protector"], - [45658, "Mantle of the Wayward Vanquisher"], [34170, "Pantaloons of Calming Strife"], [34192, "Pauldrons of Perseverance"], - [29763, "Pauldrons of the Fallen Champion"], [29764, "Pauldrons of the Fallen Defender"], [29762, "Pauldrons of the Fallen Hero"], [31101, "Pauldrons of the Forgotten Conqueror"], [31103, "Pauldrons of the Forgotten Protector"], [31102, "Pauldrons of the Forgotten Vanquisher"], + [null, "Tier 5 Raid Set"], + [30236, "Chestguard of the Vanquished Champion"], [30237, "Chestguard of the Vanquished Defender"], [30238, "Chestguard of the Vanquished Hero"], + [30239, "Gloves of the Vanquished Champion"], [30240, "Gloves of the Vanquished Defender"], [30241, "Gloves of the Vanquished Hero"], + [30242, "Helm of the Vanquished Champion"], [30243, "Helm of the Vanquished Defender"], [30244, "Helm of the Vanquished Hero"], + [30245, "Leggings of the Vanquished Champion"], [30246, "Leggings of the Vanquished Defender"], [30247, "Leggings of the Vanquished Hero"], [30248, "Pauldrons of the Vanquished Champion"], [30249, "Pauldrons of the Vanquished Defender"], [30250, "Pauldrons of the Vanquished Hero"], - [47557, "Regalia of the Grand Conqueror"], [47558, "Regalia of the Grand Protector"], [47559, "Regalia of the Grand Vanquisher"], + [null, "Tier 4 Raid Set"], + [29754, "Chestguard of the Fallen Champion"], [29753, "Chestguard of the Fallen Defender"], [29755, "Chestguard of the Fallen Hero"], + [29757, "Gloves of the Fallen Champion"], [29758, "Gloves of the Fallen Defender"], [29756, "Gloves of the Fallen Hero"], + [29760, "Helm of the Fallen Champion"], [29761, "Helm of the Fallen Defender"], [29759, "Helm of the Fallen Hero"], + [29766, "Leggings of the Fallen Champion"], [29767, "Leggings of the Fallen Defender"], [29765, "Leggings of the Fallen Hero"], + [29763, "Pauldrons of the Fallen Champion"], [29764, "Pauldrons of the Fallen Defender"], [29762, "Pauldrons of the Fallen Hero"], + [null, "Miscellaneous"], + [34169, "Breeches of Natural Aggression"], [34186, "Chain Links of the Tumultuous Storm"], [34245, "Cover of Ursol the Wise"], + [34332, "Cowl of Gul'dan"], [34339, "Cowl of Light's Purity"], [34345, "Crown of Anasterian"], + [34244, "Duplicitous Guise"], [34208, "Equilibrium Epaulets"], [34180, "Felfury Legplates"], + [34229, "Garments of Serene Shores"], [34350, "Gauntlets of the Ancient Shadowmoon"], [34342, "Handguards of the Dawn"], + [34211, "Harness of Carnal Instinct"], [34243, "Helm of Burning Righteousness"], [34216, "Heroic Judicator's Chestguard"], + [34167, "Legplates of the Holy Juggernaut"], [34170, "Pantaloons of Calming Strife"], [34192, "Pauldrons of Perseverance"], [34233, "Robes of Faltered Light"], [34234, "Shadowed Gauntlets of Paroxysm"], [34202, "Shawl of Wonderment"], - [34195, "Shoulderpads of Vehemence"], [34209, "Spaulders of Reclamation"], [40622, "Spaulders of the Lost Conqueror"], - [40623, "Spaulders of the Lost Protector"], [40624, "Spaulders of the Lost Vanquisher"], [34193, "Spaulders of the Thalassian Savior"], - [45659, "Spaulders of the Wayward Conqueror"], [45660, "Spaulders of the Wayward Protector"], [45661, "Spaulders of the Wayward Vanquisher"], + [34195, "Shoulderpads of Vehemence"], [34209, "Spaulders of Reclamation"], [34193, "Spaulders of the Thalassian Savior"], [34212, "Sunglow Vest"], [34351, "Tranquil Majesty Wraps"], [34215, "Warharness of Reckless Fury"] ], queststart: [ [3, "Item"], [1, "NPC"], [2, "Object"] ], @@ -3866,7 +3900,7 @@ var LANG = { [31, "Increase Run Speed %"], [32, "Mod Mounted Speed %"], [33, "Decrease Run Speed %"], [34, "Mod Maximum Health - Flat"], [35, "Mod Maximum Power - Flat"], [36, "Shapeshift"], [37, "Spell Effect Immunity"], [38, "Spell Aura Immunity"], [39, "Spell School Immunity"], [40, "Damage Immunity"], [41, "Dispel Type Immunity"], [42, "Proc Trigger Spell"], [43, "Proc Trigger Damage"], [44, "Track Creatures"], [45, "Track Resources"], - [46, "Ignore All Gear"], [47, "Mod Parry %"], /* [48, null] */ [49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], + [46, "Ignore All Gear"], [47, "Mod Parry %"], [48, "Periodic Trigger Spell from Client"],[49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], [51, "Mod Block %"], [52, "Mod Physical Crit Chance"], [53, "Periodically Drain Health"], [54, "Mod Physical Hit Chance"], [55, "Mod Spell Hit Chance"], [56, "Transform"], [57, "Mod Spell Crit Chance"], [58, "Increase Swim Speed %"], [59, "Mod Damage Done Versus Creature"],[60, "Pacify & Silence"], [61, "Mod Size %"], [62, "Periodically Transfer Health"], [63, "Periodically Transfer Power"], [64, "Periodically Drain Power"], [65, "Mod Spell Haste % (not stacking)"], @@ -3948,11 +3982,10 @@ var LANG = { [131, "Play Sound"], [132, "Play Music"], [133, "Unlearn Specialization"], [134, "Kill Credit2"], [135, "Call Pet"], [136, "Heal for % of Total Health"], [137, "Give % of Total Power"], [138, "Leap Back"], [139, "Abandon Quest"], [140, "Force Spell Cast"], [141, "Force Spell Cast with Value"], [142, "Trigger Spell with Value"], [143, "Apply Area Aura - Pet Owner"], [144, "Knockback to Dest."], [145, "Pull Towards Dest."], - [146, "Activate Rune"], [147, "Fail Quest"], [149, "Charge to Dest."], [150, "Start Quest"], + [146, "Activate Rune"], [147, "Fail Quest"], [148, "Trigger Missile with Value"], [149, "Charge to Dest."], [150, "Start Quest"], [151, "Trigger Spell 2"], [152, "Summon - Refer-A-Friend"], [153, "Create Tamed Pet"], [154, "Discover Flight Path"], [155, "Dual Wield 2H Weapons"], - [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], - [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"], - [167, "Update Player Phase"] + [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], [160, "Force Spell Cast 2"], + [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"] ], damagetype: [ [1, "None"], [2, "Magic"], [3, "Melee"], [4, "Ranged"] @@ -4178,6 +4211,7 @@ var LANG = { id: "ID", classspecific: "Class-specific", racespecific: "Race-specific", + setspvpflag: "Keeps you PvP flagged", sepgainsrewards: "Gains/rewards", experiencegained: "Experience gained", @@ -4948,10 +4982,18 @@ var LANG = { /* AoWoW: start custom */ + // Profiler completions import + completion: 'Completion', // WH.TERMS.completion + complete: 'Complete', // WH.TERMS.complete + incomplete: 'Incomplete', // WH.TERMS.incomplete + parens_format: '$1 ($2)', // WH.TERMS.parens_format + // click to copy fn copied: 'Copied', clickToCopy: 'Click to Copy', nothingToCopy_tip: 'Nothing to copy!', + copy_clipboard: 'Copy', + copy_format: 'Copy %s', // TC conditions display tab_conditions: 'Conditions', diff --git a/static/js/locale_eses.js b/static/js/locale_eses.js index ca03d740..723a7651 100644 --- a/static/js/locale_eses.js +++ b/static/js/locale_eses.js @@ -1531,6 +1531,7 @@ var g_quest_sorts = { "-374": 'Jardín Noble', "-375": 'Generosidad del Peregrino', "-376": 'Amor en el aire', + 0: 'Sin categoría', 1: 'Dun Morogh', 3: 'Tierras Inhóspitas', 4: 'Las Tierras Devastadas', @@ -2520,7 +2521,8 @@ var g_conditions = { 47: 'El Jugador $Nha: no ha; $2 [quest=$1]', 48: 'El Jugador $Nha: no ha; recolectado $3 para el objetivo #$2 de [quest=$1]', 49: 'La dificultad actual del mapa es #$1', - 50: 'El Jugador $C$1$Npuede:no puede; ser:es$N: no es;; un Maestro de Juego' + 50: 'El Jugador $C$1$Npuede:no puede; ser:es$N: no es;; un Maestro de Juego', + 58: 'El StringID de esta entidad es$N: no es; $4' }; /* end aowow custom */ @@ -2552,7 +2554,7 @@ var LANG = { date_simple: "$1/$2/$3", unknowndate_stc: "Fecha desconocida", date_months: ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"], - date_days: ["Domingo", "Lunes", "Martes", "Jueves", "Jueves", "Viernes", "Sábado"], + date_days: ["Lunes", "Martes", "Jueves", "Jueves", "Viernes", "Sábado", "Domingo"], amount: "Cantidad", abilities: "Habilidades", @@ -3097,6 +3099,8 @@ var LANG = { tooltip_uprate: "Útil/gracioso", tooltip_zonelink: "Hacer click en este enlace, te<br />llevará a la página de la zona.", + tooltip_immune: "Inmune", // aowow - custom + reputationhistory: "Historial de reputación", reputationaction: "Acción de reputación", @@ -3612,59 +3616,74 @@ var LANG = { num: [ [1, ">"], [2, ">="], [3, "="], [4, "<="], [5, "<"], [6, "!="]], side: [ [1, "Sí"], [2, "Alianza"], [3, "Horda"], [4, "Ambos"], [5, "No"] ], faction: [ - [1015, "Ala Abisal"], [469, "Alianza"], [21, "Bahía del Botín"], [1011, "Bajo Arrabal"], [946, "Bastión del Honor"], - [576, "Bastión Fauces de Madera"], [87, "Bucaneros Velasangre"], [1098, "Caballeros de la Espada de Ébano"], [92, "Centauros del clan Gelkis"], - [93, "Centauros del clan Magram"], [890, "Centinelas Ala de Plata"], [81, "Cima del Trueno"], [911, "Ciudad de Lunargenta"], [729, "Clan Lobo Gélido"], - [1106, "Cruzada Argenta"], [169, "Cártel Bonvapor"], [609, "Círculo Cenarion"], [69, "Darnassus"], [1091, "El Acuerdo del Reposo del Dragón"], - [529, "El Alba Argenta"], [933, "El Consorcio"], [930, "El Exodar"], [967, "El Ojo Violeta"], [1094, "El Pacto de Plata"], - [1156, "El Veredicto Cinéreo"], [68, "Entrañas"], [889, "Escoltas Grito de Guerra"], [970, "Esporaggar"], [54, "Exiliados de Gnomeregan"], - [942, "Expedición Cenarion"], [1050, "Expedición de Denuedo"], [1052, "Expedición de la Horda"], [909, "Feria de la Luna Negra"], [47, "Forjaz"], - [369, "Gadgetzan"], [1031, "Guardia del cielo Sha'tari"], [730, "Guardia Pico Tormenta"], [59, "Hermandad del Torio"], [67, "Horda"], - [589, "Instructores de Sableinvernales"],[1012, "Juramorte Lengua de ceniza"], [1090, "Kirin Tor"], [978, "Kurenai"], [990, "La Escama de las Arenas"], - [70, "La Hermandad"], [1067, "La Mano de la Venganza"], [509, "Liga de Arathor"], [1068, "Liga de Expedicionarios"], [910, "Linaje de Nozdormu"], - [932, "Los Aldor"], [934, "Los Arúspices"], [1124, "Los Atracasol"], [1119, "Los Hijos de Hodir"], [1073, "Los Kalu'ak"], - [941, "Los Mag'har"], [1126, "Los Natoescarcha"], [1105, "Los Oráculos"], [510, "Los Rapiñadores"], [935, "Los Sha'tar"], - [1064, "Los taunka"], [1085, "Ofensiva Grito de Guerra"], [1077, "Ofensiva Sol Devastado"], [1038, "Ogri'la"], [76, "Orgrimmar"], - [349, "Ravenholdt"], [809, "Shen'dralar"], [749, "Srs. del Agua de Hydraxis"], [947, "Thrallmar"], [922, "Tranquillien"], - [1104, "Tribu Corazón Frenético"], [270, "Tribu Zandalar"], [470, "Trinquete"], [530, "Trols Lanza Negra"], [1037, "Vanguardia de la Alianza"], - [72, "Ventormenta"], [989, "Vigilantes del Tiempo"], [577, "Vista Eterna"] + [null, "World of Warcraft"], + [ 469, "Alianza"], [ 21, "Bahía del Botín"], [ 576, "Bastión Fauces de Madera"], [ 87, "Bucaneros Velasangre"], [ 169, "Cártel Bonvapor"], + [ 92, "Centauros del clan Gelkis"], [ 93, "Centauros del clan Magram"], [ 890, "Centinelas Ala de Plata"], [ 81, "Cima del Trueno"], [ 609, "Círculo Cenarion"], + [ 911, "Ciudad de Lunargenta"], [ 729, "Clan Lobo Gélido"], [ 69, "Darnassus"], [ 529, "El Alba Argenta"], [ 930, "El Exodar"], + [ 68, "Entrañas"], [ 889, "Escoltas Grito de Guerra"], [ 54, "Exiliados de Gnomeregan"], [ 909, "Feria de la Luna Negra"], [ 47, "Forjaz"], + [ 369, "Gadgetzan"], [ 730, "Guardia Pico Tormenta"], [ 59, "Hermandad del Torio"], [ 67, "Horda"], [ 509, "Liga de Arathor"], + [ 910, "Linaje de Nozdormu"], [ 510, "Los Rapiñadores"], [ 76, "Orgrimmar"], [ 349, "Ravenholdt"], [ 809, "Shen'dralar"], + [ 749, "Srs. del Agua de Hydraxis"], [ 922, "Tranquillien"], [ 270, "Tribu Zandalar"], [ 470, "Trinquete"], [ 530, "Trols Lanza Negra"], + [ 72, "Ventormenta"], [ 577, "Vista Eterna"], + [null, "Burning Crusade"], + [1015, "Ala Abisal"], [1011, "Bajo Arrabal"], [ 946, "Bastión del Honor"], [ 933, "El Consorcio"], [ 967, "El Ojo Violeta"], + [ 970, "Esporaggar"], [ 942, "Expedición Cenarion"], [1031, "Guardia del cielo Sha'tari"], [1012, "Juramorte Lengua de ceniza"], [ 978, "Kurenai"], + [ 990, "La Escama de las Arenas"], [ 932, "Los Aldor"], [ 934, "Los Arúspices"], [ 941, "Los Mag'har"], [ 935, "Los Sha'tar"], + [1077, "Ofensiva Sol Devastado"], [1038, "Ogri'la"], [ 947, "Thrallmar"], [ 989, "Vigilantes del Tiempo"], + [null, "Wrath of the Lich King"], + [1098, "Caballeros de la Espada de Ébano"], [1106, "Cruzada Argenta"], [1091, "El Acuerdo del Reposo del Dragón"], + [1094, "El Pacto de Plata"], [1156, "El Veredicto Cinéreo"], [1050, "Expedición de Denuedo"], [1052, "Expedición de la Horda"], [1090, "Kirin Tor"], + [1067, "La Mano de la Venganza"], [1068, "Liga de Expedicionarios"], [1124, "Los Atracasol"], [1119, "Los Hijos de Hodir"], [1073, "Los Kalu'ak"], + [1126, "Los Natoescarcha"], [1105, "Los Oráculos"], [1064, "Los taunka"], [1085, "Ofensiva Grito de Guerra"], [1104, "Tribu Corazón Frenético"], + [1037, "Vanguardia de la Alianza"], + [null, "Miscelánea"], + [ 589, "Instructores de Sableinvernales"], [ 70, "La Hermandad"] ], zone: [ - [4494, "Ahn'kahet: El Antiguo Reino"], [3428, "Ahn'Qiraj"], [3775, "Anillo de Sangre"], [2367, "Antiguas Laderas de Trabalomas"],[4378, "Arena de Dalaran"], - [3698, "Arena de Nagrand"], [3702, "Arena Filospada"], [4277, "Azjol-Nerub"], [16, "Azshara"], [4131, "Bancal del Magister"], - [1216, "Bastión Fauces de Madera"], [3430, "Bosque Canción Eterna"], [2817, "Bosque Canto de Cristal"], [130, "Bosque de Argénteos"], [12, "Bosque de Elwynn"], - [10, "Bosque del Ocaso"], [3519, "Bosque de Terokkar"], [209, "Castillo de Colmillo Oscuro"], [3607, "Caverna Santuario Serpiente"], [719, "Cavernas de Brazanegra"], - [1941, "Cavernas del Tiempo"], [65, "Cementerio de Dragones"], [1638, "Cima del Trueno"], [3487, "Ciudad de Lunargenta"], [3703, "Ciudad de Shattrath"], - [1519, "Ciudad de Ventormenta"], [4812, "Ciudadela de la Corona de Hielo"],[493, "Claro de la Luna"], [85, "Claros de Tirisfal"], [394, "Colinas Pardas"], - [4197, "Conquista del Invierno"], [210, "Corona de Hielo"], [148, "Costa Oscura"], [3790, "Criptas Auchenai"], [490, "Cráter de Un'Goro"], - [3358, "Cuenca de Arathi"], [3711, "Cuenca de Sholazar"], [718, "Cuevas de los Lamentos"], [1583, "Cumbre de Roca Negra"], [618, "Cuna del Invierno"], - [4264, "Cámaras de Piedra"], [4820, "Cámaras de Reflexión"], [4272, "Cámaras de Relámpagos"], [4395, "Dalaran"], [1657, "Darnassus"], - [4742, "Desembarco de Hrothgar"], [405, "Desolace"], [1, "Dun Morogh"], [14, "Durotar"], [3848, "El Arcatraz"], - [4415, "El Bastión Violeta"], [3845, "El Castillo de la Tempestad"], [3638, "El Círculo de los Retos"], [4406, "El Círculo del Valor"], [3557, "El Exodar"], - [3713, "El Horno de Sangre"], [3847, "El Invernáculo"], [3849, "El Mechanar"], [4265, "El Nexo"], [4228, "El Oculus"], - [4500, "El Ojo de la Eternidad"], [4493, "El Sagrario Obsidiana"], [1477, "El Templo de Atal'Hakkar"], [3840, "El Templo Oscuro"], [1497, "Entrañas"], - [357, "Feralas"], [495, "Fiordo Aquilonal"], [1537, "Forjaz"], [4196, "Fortaleza de Drak'Tharon"], [206, "Fortaleza de Utgarde"], - [4813, "Foso de Saron"], [361, "Frondavil"], [3277, "Garganta Grito de Guerra"], [4024, "Gelidar"], [721, "Gnomeregan"], - [2677, "Guarida de Alanegra"], [3923, "Guarida de Gruul"], [3836, "Guarida de Magtheridon"], [2159, "Guarida de Onyxia"], [4416, "Gundrak"], - [491, "Horado Rajacieno"], [3524, "Isla Bruma Azur"], [3525, "Isla Bruma de Sangre"], [4710, "Isla de la Conquista"], [4080, "Isla de Quel'Danas"], - [3457, "Karazhan"], [3789, "Laberinto de las Sombras"], [3606, "La Cima Hyjal"], [2366, "La Ciénaga Negra"], [4603, "La Cámara de Archavon"], - [3715, "La Cámara de Vapor"], [267, "Laderas de Trabalomas"], [4809, "La Forja de Almas"], [51, "La Garganta de Fuego"], [2557, "La Masacre"], - [4100, "La Matanza de Stratholme"], [67, "Las Cumbres Tormentosas"], [46, "Las Estepas Ardientes"], [717, "Las Mazmorras"], [400, "Las Mil Agujas"], - [1581, "Las Minas de la Muerte"], [3716, "La Sotiénaga"], [3714, "Las Salas Arrasadas"], [4, "Las Tierras Devastadas"], [38, "Loch Modan"], - [17, "Los Baldíos"], [11, "Los Humedales"], [2100, "Maraudon"], [457, "Mar de la Bruma"], [3521, "Marisma de Zangar"], - [15, "Marjal Revolcafango"], [4075, "Meseta de La Fuente del Sol"], [796, "Monasterio Escarlata"], [25, "Montaña Roca Negra"], [44, "Montañas Crestagrana"], - [36, "Montañas de Alterac"], [3522, "Montañas Filospada"], [215, "Mulgore"], [3562, "Murallas del Fuego Infernal"], [3518, "Nagrand"], - [3456, "Naxxramas"], [2717, "Núcleo de Magma"], [3820, "Ojo de la Tormenta"], [1637, "Orgrimmar"], [8, "Pantano de las Penas"], - [41, "Paso de la Muerte"], [3483, "Península del Fuego Infernal"], [1196, "Pináculo de Utgarde"], [4384, "Playa de los Ancestros"], [393, "Playa Lanza Negra"], - [1584, "Profundidades de Roca Negra"], [4723, "Prueba del Campeón"], [4722, "Prueba del Cruzado"], [40, "Páramos de Poniente"], [3717, "Recinto de los Esclavos"], - [3905, "Reserva Colmillo Torcido"], [3429, "Ruinas de Ahn'Qiraj"], [3968, "Ruinas de Lordaeron"], [2917, "Sala de las Leyendas"], [2918, "Sala de los Campeones"], - [3791, "Salas Sethekk"], [2057, "Scholomance"], [406, "Sierra Espolón"], [1377, "Silithus"], [2437, "Sima Ígnea"], - [3679, "Skettis"], [2017, "Stratholme"], [440, "Tanaris"], [141, "Teldrassil"], [3959, "Templo Oscuro"], - [45, "Tierras Altas de Arathi"], [4298, "El Enclave Escarlata"], [139, "Tierras de la Peste del Este"], [28, "Tierras de la Peste del Oeste"], [47, "Tierras del Interior"], - [3433, "Tierras Fantasma"], [3, "Tierras Inhóspitas"], [3523, "Tormenta Abisal"], [2257, "Tranvía Subterráneo"], [3792, "Tumbas de Maná"], - [3537, "Tundra Boreal"], [1337, "Uldaman"], [4273, "Ulduar"], [2597, "Valle de Alterac"], [331, "Vallefresno"], - [3520, "Valle Sombraluna"], [33, "Vega de Tuercespina"], [722, "Zahúrda Rajacieno"], [3805, "Zul'Aman"], [66, "Zul'Drak"], - [1176, "Zul'Farrak"], [1977, "Zul'Gurub"] + [null, "Reinos del Este"], + [3430, "Bosque Canción Eterna"], [ 130, "Bosque de Argénteos"], [ 12, "Bosque de Elwynn"], [ 10, "Bosque del Ocaso"], [3487, "Ciudad de Lunargenta"], + [1519, "Ciudad de Ventormenta"], [ 85, "Claros de Tirisfal"], [ 1, "Dun Morogh"], [1497, "Entrañas"], [1537, "Forjaz"], + [4080, "Isla de Quel'Danas"], [ 51, "La Garganta de Fuego"], [ 267, "Laderas de Trabalomas"], [ 46, "Las Estepas Ardientes"], [ 4, "Las Tierras Devastadas"], + [ 38, "Loch Modan"], [ 11, "Los Humedales"], [ 44, "Montañas Crestagrana"], [ 36, "Montañas de Alterac"], [ 8, "Pantano de las Penas"], + [ 40, "Páramos de Poniente"], [ 41, "Paso de la Muerte"], [ 45, "Tierras Altas de Arathi"], [ 139, "Tierras de la Peste del Este"], [ 28, "Tierras de la Peste del Oeste"], + [4298, "Tierras de la Peste: El Enclave Escarlata"], [ 47, "Tierras del Interior"], [3433, "Tierras Fantasma"], [ 3, "Tierras Inhóspitas"], + [2257, "Tranvía Subterráneo"], [ 33, "Vega de Tuercespina"], + [null, "Kalimdor"], + [ 16, "Azshara"], [1638, "Cima del Trueno"], [ 493, "Claro de la Luna"], [ 148, "Costa Oscura"], [ 490, "Cráter de Un'Goro"], + [ 618, "Cuna del Invierno"], [1657, "Darnassus"], [ 405, "Desolace"], [ 14, "Durotar"], [3557, "El Exodar"], + [ 357, "Feralas"], [ 361, "Frondavil"], [3524, "Isla Bruma Azur"], [3525, "Isla Bruma de Sangre"], [ 400, "Las Mil Agujas"], + [ 17, "Los Baldíos"], [ 15, "Marjal Revolcafango"], [ 215, "Mulgore"], [1637, "Orgrimmar"], [ 406, "Sierra Espolón"], + [1377, "Silithus"], [ 440, "Tanaris"], [ 141, "Teldrassil"], [ 331, "Vallefresno"], + [null, "Terrallende"], + [3519, "Bosque de Terokkar"], [3703, "Ciudad de Shattrath"], [3521, "Marisma de Zangar"], [3522, "Montañas Filospada"], [3518, "Nagrand"], + [3483, "Península del Fuego Infernal"], [3523, "Tormenta Abisal"], [3520, "Valle Sombraluna"], + [null, "Rasganorte"], + [2817, "Bosque Canto de Cristal"], [ 65, "Cementerio de Dragones"], [ 394, "Colinas Pardas"], [4197, "Conquista del Invierno"], [ 210, "Corona de Hielo"], + [3711, "Cuenca de Sholazar"], [4395, "Dalaran"], [4742, "Desembarco de Hrothgar"], [ 495, "Fiordo Aquilonal"], [ 67, "Las Cumbres Tormentosas"], + [3537, "Tundra Boreal"], [ 66, "Zul'Drak"], + [null, "Mazmorras"], + [4494, "Ahn'kahet: El Antiguo Reino"], [2367, "Antiguas Laderas de Trabalomas"],[4277, "Azjol-Nerub"], [4131, "Bancal del Magister"], [4415, "Bastión Violeta"], + [4264, "Cámaras de Piedra"], [4820, "Cámaras de Reflexión"], [4272, "Cámaras de Relámpagos"], [ 209, "Castillo de Colmillo Oscuro"], [ 719, "Cavernas de Brazanegra"], + [3790, "Criptas Auchenai"], [ 718, "Cuevas de los Lamentos"], [1583, "Cumbre de Roca Negra"], [3848, "El Arcatraz"], [3713, "El Horno de Sangre"], + [3847, "El Invernáculo"], [3849, "El Mechanar"], [4265, "El Nexo"], [4228, "El Oculus"], [3717, "El Señor de la Escarcha Ahune"], + [4196, "Fortaleza de Drak'Tharon"], [ 206, "Fortaleza de Utgarde"], [4813, "Foso de Saron"], [ 721, "Gnomeregan"], [4416, "Gundrak"], + [ 491, "Horado Rajacieno"], [3715, "La Cámara de Vapor"], [2366, "La Ciénaga Negra"], [4809, "La Forja de Almas"], [2557, "La Masacre"], + [4100, "La Matanza de Stratholme"], [3716, "La Sotiénaga"], [3789, "Laberinto de las Sombras"], [2100, "Maraudon"], [ 717, "Mazmorras de Ventormenta"], + [1581, "Minas de la Muerte"], [ 796, "Monasterio Escarlata"], [3562, "Murallas del Fuego Infernal"], [1196, "Pináculo de Utgarde"], [1584, "Profundidades de Roca Negra"], + [4723, "Prueba del Campeón"], [3717, "Recinto de los Esclavos"], [3714, "Salas Arrasadas"], [3791, "Salas Sethekk"], [2057, "Scholomance"], + [2437, "Sima Ígnea"], [2017, "Stratholme"], [1477, "Templo Sumergido"], [3792, "Tumbas de Maná"], [1337, "Uldaman"], + [ 722, "Zahúrda Rajacieno"], [1176, "Zul'Farrak"], + [null, "Bandas"], + [3606, "Batalla del Monte Hyjal"], [4603, "Cámara de Archavon"], [3607, "Caverna Santuario Serpiente"], [4812, "Ciudadela de la Corona de Hielo"],[3845, "El Castillo de la Tempestad"], + [4500, "El Ojo de la Eternidad"], [4493, "El Sagrario Obsidiana"], [4987, "El Sagrario Rubí"], [2677, "Guarida de Alanegra"], [3923, "Guarida de Gruul"], + [3836, "Guarida de Magtheridon"], [2159, "Guarida de Onyxia"], [3457, "Karazhan"], [4075, "La Fuente del Sol"], [3456, "Naxxramas"], + [2717, "Núcleo de Magma"], [4722, "Prueba del Cruzado"], [3429, "Ruinas de Ahn'Qiraj"], [3428, "Templo de Ahn'Qiraj"], [3959, "Templo Oscuro"], + [4273, "Ulduar"], [3805, "Zul'Aman"], [1977, "Zul'Gurub"], + [null, "Campos de batalla"], + [3358, "Cuenca de Arathi"], [3277, "Garganta Grito de Guerra"], [4710, "Isla de la Conquista"], [3820, "Ojo de la Tormenta"], [4384, "Playa de los Ancestros"], + [2597, "Valle de Alterac"], + [null, "Arenas"], + [4378, "Arena de Dalaran"], [3698, "Arena de Nagrand"], [3702, "Arena Filospada"], [4406, "El Círculo del Valor"], [3968, "Ruinas de Lordaeron"] ], resistance: [ [6, "Arcano"], [2, "Fuego"], [3, "Naturaleza"], [4, "Escarcha"], [5, "Sombras"], @@ -3712,67 +3731,84 @@ var LANG = { ], pvp: [ [1, "Sí"], [3, "Arena"], [4, "Campo de batalla"], [5, "Mundo"], [2, "No"] ], currency: [ - [34597, "Almeja Aleta Invernal"], [24368, "Armamentos de Colmillo Torcido"],[32572, "Cristal de ápices"], [38425, "Cuero boreal pesado"], [29434, "Distintivo de justicia"], - [45624, "Emblema de conquista"], [49426, "Emblema de escarcha"], [40752, "Emblema de heroísmo"], [47241, "Emblema de triunfo"], [40753, "Emblema de valor"], - [37829, "Ficha para la Fiesta de la cerveza"], [23247, "Flores ardientes"], [24245, "Fluochampiñón"], [28558, "Fragmento de espíritu"], - [43228, "Fragmento de vigilante de piedra"], [32569, "Fragmento de ápices"], [34052, "Fragmento onírico"], [4291, "Hilo de seda"], - [24579, "Marca de Bastión del Honor"], [43589, "Marca de Honor de Conquista del Invierno"], [20559, "Marca de Honor de la Cuenca de Arathi"], - [20558, "Marca de Honor de la Garganta Grito de Guerra"], [29024, "Marca de Honor del Ojo de la Tormenta"], [20560, "Marca de Honor del Valle de Alterac"], - [32897, "Marca de los Illidari"], [52030, "Marca de santificación de conquistador (Heroico)"], [52027, "Marca de santificación de conquistador"], - [52026, "Marca de santificación de protector"], [52029, "Marca de santificación de protector (Heroico)"], [52025, "Marca de santificación de vencedor"], - [52028, "Marca de santificación de vencedor (Heroico)"], [24581, "Marca de Thrallmar"], [37836, "Moneda de Ventura"], [34664, "Mota de sol"], - [41596, "Muestra de joyero de Dalaran"],[44128, "Pelaje ártico"], [26045, "Pieza Halaa de batalla"], [26044, "Pieza Halaa de investigación"],[29735, "Polvo sagrado"], - [43016, "Premio de cocina de Dalaran"], [29736, "Runa Arcana"], [22484, "Runa necrótica"], [44990, "Sello de Campeón"], [47242, "Trofeo de la Cruzada"] + [null, "World of Warcraft"], + [37829, "Ficha para la Fiesta de la cerveza"], [23247, "Flores ardientes"], [4291, "Hilo de seda"], + [null, "Burning Crusade"], + [24368, "Armamentos de Colmillo Torcido"], [32572, "Cristal de ápices"], [24245, "Fluochampiñón"], + [32569, "Fragmento de ápices"], [32897, "Marca de los Illidari"], [34664, "Mota de sol"], + [29735, "Polvo sagrado"], [29736, "Runa Arcana"], [22484, "Runa necrótica"], + [null, "Wrath of the Lich King"], + [34597, "Almeja Aleta Invernal"], [38425, "Cuero boreal pesado"], [34052, "Fragmento onírico"], + [41596, "Muestra de joyero de Dalaran"], [44128, "Pelaje ártico"], [43016, "Premio de cocina de Dalaran"], + [44990, "Sello de Campeón"], + [null, "Jugador contra Jugador"], + [28558, "Fragmento de espíritu"], [43228, "Fragmento de vigilante de piedra"], [24579, "Marca de Bastión del Honor"], + [20559, "Marca de Honor de la Cuenca de Arathi"], [29024, "Marca de Honor del Ojo de la Tormenta"], [20560, "Marca de Honor del Valle de Alterac"], + [24581, "Marca de Thrallmar"], [26045, "Pieza Halaa de batalla"], [26044, "Pieza Halaa de investigación"], + [null, "Mazmorra y banda"], + [29434, "Distintivo de justicia"], [45624, "Emblema de conquista"], [49426, "Emblema de escarcha"], + [40752, "Emblema de heroísmo"], [47241, "Emblema de triunfo"], [40753, "Emblema de valor"] ], itemcurrency: [ - [34215, "Arnés de guerra de furia temeraria"], [34211, "Arnés del instinto carnal"], [47557, "Atavío del Gran conquistador"], - [47558, "Atavío del Gran protector"], [47559, "Atavío del Gran vencedor"], [34170, "Bombachos de menores conflictos"], + [null, "Set de banda tier 10"], + [52027, "Marca de santificación de conquistador"], [52026, "Marca de santificación de protector"], [52025, "Marca de santificación de vencedor"], + [52030, "Marca de santificación de conquistador (Heroico)"], [52029, "Marca de santificación de protector (Heroico)"], [52028, "Marca de santificación de vencedor (Heroico)"], + [null, "Set de banda tier 9"], + [47242, "Trofeo de la Cruzada"], + [47557, "Atavío del Gran conquistador"], [47558, "Atavío del Gran protector"], [47559, "Atavío del Gran vencedor"], + [null, "Set de banda tier 8"], + [45659, "Bufas del conquistador díscolo"], [45660, "Bufas del protector díscolo"], [45661, "Bufas del vencedor díscolo"], + [45632, "Coraza del conquistador díscolo"], [45633, "Coraza del protector díscolo"], [45634, "Coraza del vencedor díscolo"], + [45638, "Corona del conquistador díscolo"], [45639, "Corona del protector díscolo"], [45640, "Corona del vencedor díscolo"], + [45635, "Coselete del conquistador díscolo"], [45636, "Coselete del protector díscolo"], [45637, "Coselete del vencedor díscolo"], + [45641, "Guanteletes del conquistador díscolo"], [45642, "Guanteletes del protector díscolo"], [45643, "Guanteletes del vencedor díscolo"], + [45644, "Guantes del conquistador díscolo"], [45645, "Guantes del protector díscolo"], [45646, "Guantes del vencedor díscolo"], + [45650, "Leotardos del conquistador díscolo"], [45651, "Leotardos del protector díscolo"], [45652, "Leotardos del vencedor díscolo"], + [45656, "Manto del conquistador díscolo"], [45657, "Manto del protector díscolo"], [45658, "Manto del vencedor díscolo"], + [45653, "Quijotes del conquistador díscolo"], [45654, "Quijotes del protector díscolo"], [45655, "Quijotes del vencedor díscolo"], + [45647, "Yelmo del conquistador díscolo"], [45648, "Yelmo del protector díscolo"], [45649, "Yelmo del vencedor díscolo"], + [null, "Set de banda tier 7"], + [40622, "Bufas del conquistador perdido"], [40623, "Bufas del protector perdido"], [40624, "Bufas del vencedor perdido"], + [40625, "Coraza del conquistador perdido"], [40626, "Coraza del protector perdido"], [40627, "Coraza del vencedor perdido"], + [40631, "Corona del conquistador perdido"], [40632, "Corona del protector perdido"], [40633, "Corona del vencedor perdido"], + [40610, "Coselete del conquistador perdido"], [40611, "Coselete del protector perdido"], [40612, "Coselete del vencedor perdido"], + [40628, "Guanteletes del conquistador perdido"], [40629, "Guanteletes del protector perdido"], [40630, "Guanteletes del vencedor perdido"], + [40613, "Guantes del conquistador perdido"], [40614, "Guantes del protector perdido"], [40615, "Guantes del vencedor perdido"], + [40619, "Leotardos del conquistador perdido"], [40620, "Leotardos del protector perdido"], [40621, "Leotardos del vencedor perdido"], + [40637, "Manto del conquistador perdido"], [40638, "Manto del protector perdido"], [40639, "Manto del vencedor perdido"], + [40634, "Quijotes del conquistador perdido"], [40635, "Quijotes del protector perdido"], [40636, "Quijotes del vencedor perdido"], + [40616, "Yelmo del conquistador perdido"], [40617, "Yelmo del protector perdido"], [40618, "Yelmo del vencedor perdido"], + [null, "Set de banda tier 6"], [34856, "Botas del conquistador olvidado"], [34857, "Botas del protector olvidado"], [34858, "Botas del vencedor olvidado"], [34848, "Brazales del conquistador olvidado"], [34851, "Brazales del protector olvidado"], [34852, "Brazales del vencedor olvidado"], - [45659, "Bufas del conquistador díscolo"], [40622, "Bufas del conquistador perdido"], [45660, "Bufas del protector díscolo"], - [40623, "Bufas del protector perdido"], [34193, "Bufas del salvador thalassiano"], [45661, "Bufas del vencedor díscolo"], - [40624, "Bufas del vencedor perdido"], [34209, "Bufas de reclamo"], [34169, "Calzones de agresividad natural"], + [34853, "Cinturón del conquistador olvidado"], [34854, "Cinturón del protector olvidado"], [34855, "Cinturón del vencedor olvidado"], + [31089, "Coselete del conquistador olvidado"], [31091, "Coselete del protector olvidado"], [31090, "Coselete del vencedor olvidado"], + [31101, "Espaldares del conquistador olvidado"], [31103, "Espaldares del protector olvidado"], [31102, "Espaldares del vencedor olvidado"], + [31092, "Guantes del conquistador olvidado"], [31094, "Guantes del protector olvidado"], [31093, "Guantes del vencedor olvidado"], + [31098, "Leotardos del conquistador olvidado"], [31100, "Leotardos del protector olvidado"], [31099, "Leotardos del vencedor olvidado"], + [31097, "Yelmo del conquistador olvidado"], [31095, "Yelmo del protector olvidado"], [31096, "Yelmo del vencedor olvidado"], + [null, "Set de banda tier 5"], + [30236, "Coselete del campeón vencido"], [30237, "Coselete del defensor vencido"], [30238, "Coselete del héroe vencido"], + [30248, "Espaldares del campeón vencido"], [30249, "Espaldares del defensor vencido"], [30250, "Espaldares del héroe vencido"], + [30239, "Guantes del campeón vencido"], [30240, "Guantes del defensor vencido"], [30241, "Guantes del héroe vencido"], + [30245, "Leotardos del campeón vencido"], [30246, "Leotardos del defensor vencido"], [30247, "Leotardos del héroe vencido"], + [30242, "Yelmo del campeón vencido"], [30243, "Yelmo del defensor vencido"], [30244, "Yelmo del héroe vencido"], + [null, "Set de banda tier 4"], + [29754, "Coselete del campeón caído"], [29753, "Coselete del defensor caído"], [29755, "Coselete del héroe caído"], + [29763, "Espaldares del campeón caído"], [29764, "Espaldares del defensor caído"], [29762, "Espaldares del héroe caído"], + [29757, "Guantes del campeón caído"], [29758, "Guantes del defensor caído"], [29756, "Guantes del héroe caído"], + [29766, "Leotardos del campeón caído"], [29767, "Leotardos del defensor caído"], [29765, "Leotardos del héroe caído"], + [29760, "Yelmo del campeón caído"], [29761, "Yelmo del defensor caído"], [29759, "Yelmo del héroe caído"], + [null, "Miscelánea"], + [34215, "Arnés de guerra de furia temeraria"], [34211, "Arnés del instinto carnal"], [34170, "Bombachos de menores conflictos"], + [34209, "Bufas de reclamo"], [34193, "Bufas del salvador thalassiano"], [34169, "Calzones de agresividad natural"], [34332, "Capucha de Gul'dan"], [34339, "Capucha de pureza de luz"], [34245, "Casquete de Ursoc el Sabio"], - [34202, "Chal de asombro"], [34853, "Cinturón del conquistador olvidado"], [34854, "Cinturón del protector olvidado"], - [34855, "Cinturón del vencedor olvidado"], [45632, "Coraza del conquistador díscolo"], [40625, "Coraza del conquistador perdido"], - [45633, "Coraza del protector díscolo"], [40626, "Coraza del protector perdido"], [45634, "Coraza del vencedor díscolo"], - [40627, "Coraza del vencedor perdido"], [34345, "Corona de Anasterian"], [45638, "Corona del conquistador díscolo"], - [40631, "Corona del conquistador perdido"], [45639, "Corona del protector díscolo"], [40632, "Corona del protector perdido"], - [45640, "Corona del vencedor díscolo"], [40633, "Corona del vencedor perdido"], [34216, "Coselete de enjuiciador heroico"], - [29754, "Coselete del campeón caído"], [30236, "Coselete del campeón vencido"], [45635, "Coselete del conquistador díscolo"], - [31089, "Coselete del conquistador olvidado"], [40610, "Coselete del conquistador perdido"], [29753, "Coselete del defensor caído"], - [30237, "Coselete del defensor vencido"], [29755, "Coselete del héroe caído"], [30238, "Coselete del héroe vencido"], - [45636, "Coselete del protector díscolo"], [31091, "Coselete del protector olvidado"], [40611, "Coselete del protector perdido"], - [45637, "Coselete del vencedor díscolo"], [31090, "Coselete del vencedor olvidado"], [40612, "Coselete del vencedor perdido"], - [34208, "Cubrehombros de equilibrio"], [34186, "Eslabones de la tormenta tumultuosa"], [29763, "Espaldares del campeón caído"], - [30248, "Espaldares del campeón vencido"], [31101, "Espaldares del conquistador olvidado"], [29764, "Espaldares del defensor caído"], - [30249, "Espaldares del defensor vencido"], [29762, "Espaldares del héroe caído"], [30250, "Espaldares del héroe vencido"], - [31103, "Espaldares del protector olvidado"], [31102, "Espaldares del vencedor olvidado"], [34192, "Espaldares de perseverancia"], - [34350, "Guanteletes del ancestro Sombraluna"], [45641, "Guanteletes del conquistador díscolo"], [40628, "Guanteletes del conquistador perdido"], - [45642, "Guanteletes del protector díscolo"], [40629, "Guanteletes del protector perdido"], [45643, "Guanteletes del vencedor díscolo"], - [40630, "Guanteletes del vencedor perdido"], [34234, "Guanteletes de paroxismo ensombrecidos"], [29757, "Guantes del campeón caído"], - [30239, "Guantes del campeón vencido"], [45644, "Guantes del conquistador díscolo"], [31092, "Guantes del conquistador olvidado"], - [40613, "Guantes del conquistador perdido"], [29758, "Guantes del defensor caído"], [30240, "Guantes del defensor vencido"], - [29756, "Guantes del héroe caído"], [30241, "Guantes del héroe vencido"], [45645, "Guantes del protector díscolo"], - [31094, "Guantes del protector olvidado"], [40614, "Guantes del protector perdido"], [45646, "Guantes del vencedor díscolo"], - [31093, "Guantes del vencedor olvidado"], [40615, "Guantes del vencedor perdido"], [34244, "Guisa engañosa"], - [34195, "Hombreras de vehemencia"], [34212, "Jubón brillo del sol"], [29766, "Leotardos del campeón caído"], - [30245, "Leotardos del campeón vencido"], [45650, "Leotardos del conquistador díscolo"], [31098, "Leotardos del conquistador olvidado"], - [40619, "Leotardos del conquistador perdido"], [29767, "Leotardos del defensor caído"], [30246, "Leotardos del defensor vencido"], - [29765, "Leotardos del héroe caído"], [30247, "Leotardos del héroe vencido"], [45651, "Leotardos del protector díscolo"], - [31100, "Leotardos del protector olvidado"], [40620, "Leotardos del protector perdido"], [45652, "Leotardos del vencedor díscolo"], - [31099, "Leotardos del vencedor olvidado"], [40621, "Leotardos del vencedor perdido"], [34351, "Manijas de majestad tranquila"], - [34342, "Manoplas del alba"], [45656, "Manto del conquistador díscolo"], [40637, "Manto del conquistador perdido"], - [45657, "Manto del protector díscolo"], [40638, "Manto del protector perdido"], [45658, "Manto del vencedor díscolo"], - [40639, "Manto del vencedor perdido"], [34180, "Quijotes de furia vil"], [45653, "Quijotes del conquistador díscolo"], - [40634, "Quijotes del conquistador perdido"], [34167, "Quijotes del gigante sagrado"], [45654, "Quijotes del protector díscolo"], - [40635, "Quijotes del protector perdido"], [45655, "Quijotes del vencedor díscolo"], [40636, "Quijotes del vencedor perdido"], - [34229, "Ropas de costas tranquilas"], [34233, "Togas de Luz titilante"], [29760, "Yelmo del campeón caído"], - [30242, "Yelmo del campeón vencido"], [45647, "Yelmo del conquistador díscolo"], [31097, "Yelmo del conquistador olvidado"], - [40616, "Yelmo del conquistador perdido"], [29761, "Yelmo del defensor caído"], [30243, "Yelmo del defensor vencido"], - [29759, "Yelmo del héroe caído"], [30244, "Yelmo del héroe vencido"], [45648, "Yelmo del protector díscolo"], - [31095, "Yelmo del protector olvidado"], [40617, "Yelmo del protector perdido"], [45649, "Yelmo del vencedor díscolo"], - [31096, "Yelmo del vencedor olvidado"], [40618, "Yelmo del vencedor perdido"], [34243, "Yelmo de rectitud ardiente"] + [34202, "Chal de asombro"], [34345, "Corona de Anasterian"], [34216, "Coselete de enjuiciador heroico"], + [34208, "Cubrehombros de equilibrio"], [34186, "Eslabones de la tormenta tumultuosa"], [34192, "Espaldares de perseverancia"], + [34234, "Guanteletes de paroxismo ensombrecidos"], [34350, "Guanteletes del ancestro Sombraluna"], [34244, "Guisa engañosa"], + [34195, "Hombreras de vehemencia"], [34212, "Jubón brillo del sol"], [34351, "Manijas de majestad tranquila"], + [34342, "Manoplas del alba"], [34180, "Quijotes de furia vil"], [34167, "Quijotes del gigante sagrado"], + [34229, "Ropas de costas tranquilas"], [34233, "Togas de Luz titilante"], [34243, "Yelmo de rectitud ardiente"] ], queststart: [ [3, "Objeto"], [1, "PNJ"], [2, "Entidad"] ], questend: [ [1, "PNJ"], [2, "Entidad"] ], @@ -3820,7 +3856,7 @@ var LANG = { [31, "Aumentar velocidad de carrera %"], [32, "Modificar velocidad montado %"], [33, "Reducir velocidad de carrera %"], [34, "Modificar salud máxima - Fijo"], [35, "Modificar poder máximo - Fijo"], [36, "Cambio de forma"], [37, "Inmunidad a efecto de hechizo"], [38, "Inmunidad a aura de hechizo"], [39, "Inmunidad a escuela de hechizo"], [40, "Inmunidad a daño"], [41, "Inmunidad a tipo de disipación"], [42, "Activar hechizo al recibir golpe"], [43, "Activar daño al recibir golpe"], [44, "Rastrear criaturas"], [45, "Rastrear recursos"], - [46, "Ignorar todo el equipo"], [47, "Modificar parada %"], [49, "Modificar esquiva %"], [50, "Modificar sanación crítica %"], + [46, "Ignorar todo el equipo"], [47, "Modificar parada %"], [48, "Activar hechizo periódicamente desde el cliente"] [49, "Modificar esquiva %"], [50, "Modificar sanación crítica %"], [51, "Modificar bloqueo %"], [52, "Modificar probabilidad de crítico físico"], [53, "Drenar salud periódicamente"], [54, "Modificar probabilidad de golpe físico"], [55, "Modificar probabilidad de golpe de hechizo"], [56, "Transformar"], [57, "Modificar probabilidad de crítico de hechizo"], [58, "Aumentar velocidad de nado %"], [59, "Modificar daño hecho vs criatura"], [60, "Pacificar y silenciar"], [61, "Modificar tamaño %"], [62, "Transferir salud periódicamente"], [63, "Transferir poder periódicamente"], [64, "Drenar poder periódicamente"], [65, "Modificar celeridad de hechizo % (no acumulable)"], @@ -3902,11 +3938,10 @@ var LANG = { [131, "Reproducir sonido"], [132, "Reproducir música"], [133, "Olvidar especialización"], [134, "Crédito de muerte2"], [135, "Llamar mascota"], [136, "Sanar por % de salud total"], [137, "Dar % de poder total"], [138, "Saltar hacia atrás"], [139, "Abandonar misión"], [140, "Forzar lanzamiento de hechizo"], [141, "Forzar lanzamiento de hechizo con valor"], [142, "Activar hechizo con valor"], [143, "Aplicar aura de área - Dueño de mascota"], [144, "Empujar hacia destino."], [145, "Atraer hacia destino."], - [146, "Activar runa"], [147, "Fallar misión"], [149, "Cargar hacia destino."], [150, "Iniciar misión"], + [146, "Activar runa"], [147, "Fallar misión"], [148, "Disparar misil con valor"], [149, "Cargar hacia destino."], [150, "Iniciar misión"], [151, "Activar hechizo 2"], [152, "Invocar - Referido por un amigo"], [153, "Crear mascota domesticada"], [154, "Descubrir ruta de vuelo"], [155, "Doble empuñadura de armas de 2 manos"], - [156, "Añadir socket a objeto"], [157, "Crear objeto de oficio"], [158, "Molienda"], [159, "Renombrar mascota"], - [161, "Cambiar conteo de espe. de talento"], [162, "Activar espec. de talento."], [164, "Eliminar aura"], - [167, "Actualizar fase de jugador"] + [156, "Añadir socket a objeto"], [157, "Crear objeto de oficio"], [158, "Molienda"], [159, "Renombrar mascota"], [160, "Forzar lanzamiento de hechizo 2"], + [161, "Cambiar conteo de espe. de talento"], [162, "Activar espec. de talento."], [164, "Eliminar aura"] ], damagetype: [ [1, "Ninguno"], [2, "Magia"], [3, "Cuerpo a cuerpo"], [4, "A distancia"] @@ -4132,6 +4167,7 @@ var LANG = { id: "ID", classspecific: "Específico de clase", racespecific: "Específico de raza", + setspvpflag: "Mantiene el JcJ activado", sepgainsrewards: "Ganancias/recompensas", experiencegained: "Experiencia ganada", @@ -4902,10 +4938,18 @@ var LANG = { /* AoWoW: start custom */ + // Profiler completions import + completion: 'Terminación', // WH.TERMS.completion + complete: 'Completo', // WH.TERMS.complete + incomplete: 'Incompleto', // WH.TERMS.incomplete + parens_format: '$1 ($2)', // WH.TERMS.parens_format + // click to copy fn copied: 'Copiado', clickToCopy: 'Click para copiar', nothingToCopy_tip: '[Nothing to copy!]', + copy_clipboard: 'Copiar', + copy_format: 'Copiar %s', // TC conditions display tab_conditions: 'Condiciones', diff --git a/static/js/locale_frfr.js b/static/js/locale_frfr.js index 04be890c..b46cdd3f 100644 --- a/static/js/locale_frfr.js +++ b/static/js/locale_frfr.js @@ -1531,6 +1531,7 @@ var g_quest_sorts = { "-374": 'Jardin des nobles', "-375": 'Bienfaits du pèlerin', "-376": 'De l\'amour dans l\'air', + 0: 'Non classés', 1: 'Dun Morogh', 3: 'Terres ingrates', 4: 'Terres foudroyées', @@ -2520,7 +2521,8 @@ var g_conditions = { 47: 'The Player has$N: not; $2 [quest=$1]', 48: 'The Player has$N: not; collected $3 towards objective #$2 of [quest=$1]', 49: 'The current map difficulty is #$1', - 50: 'The Player $C$1$Ncan:can\'t; be:is$N: not;; a Gamemaster' + 50: 'The Player $C$1$Ncan:can\'t; be:is$N: not;; a Gamemaster', + 58: 'The StringID of this entity is$N: not; $4' }; /* end aowow custom */ @@ -2552,7 +2554,7 @@ var LANG = { date_simple: "$1-$2-$3", unknowndate_stc: "Date inconnue", date_months: ["janvier", "février", "mars", "Avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"], - date_days: ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"], + date_days: ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"], amount: "Montant", abilities: "Techniques", @@ -3098,6 +3100,8 @@ var LANG = { tooltip_uprate: "Intéressant/drôle", tooltip_zonelink: "Cliquer sur ce lien vous amènera<br />à la page de la zone correspondante.", + tooltip_immune: "Immunisé", // aowow - custom + reputationhistory: "Historique de réputation", reputationaction: "Action de réputation", @@ -3613,59 +3617,73 @@ var LANG = { num: [ [1, ">"], [2, ">="], [3, "="], [4, "<="], [5, "<"], [6, "!="]], side: [ [1, "Oui"], [2, "Alliance"], [3, "Horde"], [4, "Les deux"], [5, "Non"] ], faction: [ - [1015, "Aile-du-Néant"], [469, "Alliance"], [529, "Aube d'argent"], [1037, "Avant-garde de l'Alliance"], [21, "Baie-du-Butin"], - [946, "Bastion de l'Honneur"], [470, "Cabestan"], [169, "Cartel Gentepression"], [92, "Centaures (Gelkis)"], [93, "Centaures (Magram)"], - [609, "Cercle cénarien"], [1098, "Chevaliers de la Lame d'ébène"],[729, "Clan Loup-de-givre"], [59, "Confrérie du thorium"], [69, "Darnassus"], - [54, "Exilés de Gnomeregan"], [930, "Exodar"], [942, "Expédition cénarienne"], [1050, "Expédition de la Bravoure"], [1052, "Expédition de la Horde"], - [909, "Foire de Sombrelune"], [47, "Forgefer"], [68, "Fossoyeuse"], [369, "Gadgetzan"], [1031, "Garde-ciel sha'tari"], - [730, "Garde Foudrepique"], [989, "Gardiens du Temps"], [67, "Horde"], [72, "Hurlevent"], [1090, "Kirin Tor"], - [978, "Kurenaï"], [1091, "L'Accord du Repos du ver"], [932, "L'Aldor"], [967, "L'Œil pourpre"], [990, "La Balance des sables"], - [1106, "La Croisade d'argent"], [509, "La Ligue d'Arathor"], [1067, "La Main de la vengeance"], [87, "La Voile sanglante"], [1094, "Le Concordat argenté"], - [933, "Le Consortium"], [934, "Les Clairvoyants"], [1119, "Les Fils de Hodir"], [1126, "Les Givre-nés"], [576, "Les Grumegueules"], - [749, "Les Hydraxiens"], [1073, "Les Kalu'aks"], [1105, "Les Oracles"], [81, "Les Pitons du Tonnerre"], [510, "Les Profanateurs"], - [1124, "Les Saccage-soleil"], [935, "Les Sha'tar"], [1064, "Les Taunkas"], [1156, "Le Verdict des cendres"], [1012, "Ligemort cendrelangue"], - [1068, "Ligue des explorateurs"], [577, "Long-guet"], [911, "Lune-d'argent"], [941, "Mag'har"], [1085, "Offensive chanteguerre"], - [1038, "Ogri'la"], [1077, "Opération Soleil brisé"], [76, "Orgrimmar"], [910, "Progéniture de Nozdormu"], [349, "Ravenholdt"], - [890, "Sentinelles d'Aile-argent"], [809, "Shen'dralar"], [970, "Sporeggar"], [70, "Syndicat"], [947, "Thrallmar"], - [922, "Tranquillien"], [1104, "Tribu Frénécœur"], [270, "Tribu Zandalar"], [530, "Trolls Sombrelance"], [1011, "Ville basse"], - [889, "Voltigeurs Chanteguerre"], [589, "Éleveurs de sabres-d'hiver"] + [null, "Classique"], + [ 469, "Alliance"], [ 529, "Aube d'argent"], [ 21, "Baie-du-Butin"], [ 470, "Cabestan"], [ 169, "Cartel Gentepression"], + [ 92, "Centaures (Gelkis)"], [ 93, "Centaures (Magram)"], [ 609, "Cercle cénarien"], [ 729, "Clan Loup-de-givre"], [ 59, "Confrérie du thorium"], + [ 69, "Darnassus"], [ 54, "Exilés de Gnomeregan"], [ 930, "Exodar"], [ 909, "Foire de Sombrelune"], [ 47, "Forgefer"], + [ 68, "Fossoyeuse"], [ 369, "Gadgetzan"], [ 730, "Garde Foudrepique"], [ 67, "Horde"], [ 72, "Hurlevent"], + [ 509, "La Ligue d'Arathor"], [ 87, "La Voile sanglante"], [ 576, "Les Grumegueules"], [ 749, "Les Hydraxiens"], [ 81, "Les Pitons du Tonnerre"], + [ 510, "Les Profanateurs"], [ 577, "Long-guet"], [ 911, "Lune-d'argent"], [ 76, "Orgrimmar"], [ 910, "Progéniture de Nozdormu"], + [ 349, "Ravenholdt"], [ 890, "Sentinelles d'Aile-argent"], [ 809, "Shen'dralar"], [ 922, "Tranquillien"], [ 270, "Tribu Zandalar"], + [ 530, "Trolls Sombrelance"], [ 889, "Voltigeurs Chanteguerre"], + [null, "Burning Crusade"], + [1015, "Aile-du-Néant"], [ 946, "Bastion de l'Honneur"], [ 942, "Expédition cénarienne"], [1031, "Garde-ciel sha'tari"], [ 989, "Gardiens du Temps"], + [ 978, "Kurenaï"], [ 932, "L'Aldor"], [ 967, "L'Œil pourpre"], [ 990, "La Balance des sables"], [ 933, "Le Consortium"], + [ 934, "Les Clairvoyants"], [ 935, "Les Sha'tar"], [1012, "Ligemort cendrelangue"], [ 941, "Mag'har"], [1038, "Ogri'la"], + [1077, "Opération Soleil brisé"], [ 970, "Sporeggar"], [ 947, "Thrallmar"], [1011, "Ville basse"], + [null, "Wrath of the Lich King"], + [1037, "Avant-garde de l'Alliance"], [1098, "Chevaliers de la Lame d'ébène"],[1050, "Expédition de la Bravoure"], [1052, "Expédition de la Horde"], [1090, "Kirin Tor"], + [1091, "L'Accord du Repos du ver"], [1106, "La Croisade d'argent"], [1067, "La Main de la vengeance"], [1094, "Le Concordat argenté"], [1156, "Le Verdict des cendres"], + [1119, "Les Fils de Hodir"], [1126, "Les Givre-nés"], [1073, "Les Kalu'aks"], [1105, "Les Oracles"], [1124, "Les Saccage-soleil"], + [1064, "Les Taunkas"], [1068, "Ligue des explorateurs"], [1085, "Offensive chanteguerre"], [1104, "Tribu Frénécœur"], + [null, "Divers"], + [ 589, "Éleveurs de sabres-d'hiver"], [ 70, "Syndicat"] ], zone: [ - [4742, "Accostage de Hrothgar"], [4494, "Ahn'kahet : l'Ancien royaume"], [3428, "Ahn'Qiraj"], [4378, "Arène de Dalaran"], [3698, "Arène de Nagrand"], - [3702, "Arène des Tranchantes"], [4277, "Azjol-Nérub"], [16, "Azshara"], [3358, "Bassin d'Arathi"], [3711, "Bassin de Sholazar"], - [618, "Berceau-de-l'Hiver"], [10, "Bois de la Pénombre"], [3430, "Bois des Chants éternels"], [4603, "Caveau d'Archavon"], [3607, "Caverne du sanctuaire du Serpent"], - [718, "Cavernes des lamentations"], [3775, "Cercle de sang"], [1196, "Cime d'Utgarde"], [4812, "Citadelle de la Couronne de glace"], - [85, "Clairières de Tirisfal"], [267, "Contreforts de Hautebrande"], [2367, "Contreforts de Hautebrande d'antan"], [490, "Cratère d'Un'Goro"], - [3790, "Cryptes Auchenaï"], [2717, "Cœur du Magma"], [4395, "Dalaran"], [1657, "Darnassus"], [209, "Donjon d'Ombrecroc"], - [206, "Donjon d'Utgarde"], [4196, "Donjon de Drak'Tharon"], [3845, "Donjon de la Tempête"], [1, "Dun Morogh"], [14, "Durotar"], - [41, "Défilé de Deuillevent"], [405, "Désolace"], [65, "Désolation des dragons"], [495, "Fjord Hurlant"], [1537, "Forgefer"], - [12, "Forêt d'Elwynn"], [130, "Forêt des Pins argentés"], [3519, "Forêt de Terokkar"], [2817, "Forêt du Chant de cristal"], [4813, "Fosse de Saron"], - [1497, "Fossoyeuse"], [4024, "Frimarra"], [357, "Féralas"], [361, "Gangrebois"], [721, "Gnomeregan"], - [51, "Gorge des Vents brûlants"], [2437, "Gouffre de Ragefeu"], [3277, "Goulet des Chanteguerres"], [1941, "Grottes du temps"], [393, "Grève des Sombrelances"], - [4416, "Gundrak"], [2557, "Hache-tripes"], [2918, "Hall des Champions"], [2917, "Hall des Légendes"], [45, "Hautes-terres d'Arathi"], - [1519, "Hurlevent"], [3524, "Île de Brume-azur"], [3525, "Île de Brume-sang"], [4080, "Île de Quel'Danas"], [4710, "Île des Conquérants"], - [4197, "Joug-d'hiver"], [3457, "Karazhan"], [491, "Kraal de Tranchebauge"], [3848, "L'Arcatraz"], [4406, "L'Arène des valeureux"], - [3638, "L'arène des épreuves"], [3557, "L'Exodar"], [4228, "L'Oculus"], [4100, "L'Épuration de Stratholme"], [4723, "L'épreuve du champion"], - [4722, "L'épreuve du croisé"], [4500, "L'Œil de l'éternité"], [3820, "L'Œil du cyclone"], [3716, "La Basse-tourbière"], [3847, "La Botanica"], - [3789, "Labyrinthe des ombres"], [210, "La Couronne de glace"], [4809, "La Forge des âmes"], [3713, "La Fournaise du sang"], [457, "La Mer voilée"], - [717, "La Prison"], [3715, "Le Caveau de la vapeur"], [4415, "Le fort Pourpre"], [3849, "Le Méchanar"], [4265, "Le Nexus"], - [2366, "Le Noir Marécage"], [3836, "Le repaire de Magtheridon"], [4493, "Le sanctum Obsidien"], [44, "Les Carmines"], [3717, "Les enclos aux esclaves"], - [394, "Les Grisonnes"], [47, "Les Hinterlands"], [1581, "Les Mortemines"], [11, "Les Paluns"], [67, "Les pics Foudroyés"], - [1638, "Les Pitons du Tonnerre"], [3714, "Les Salles brisées"], [4272, "Les salles de Foudre"], [4264, "Les salles de Pierre"], [3791, "Les salles des Sethekk"], - [406, "Les Serres-Rocheuses"], [17, "Les Tarides"], [3433, "Les Terres fantômes"], [3522, "Les Tranchantes"], [1477, "Le temple d'Atal'Hakkar"], - [3840, "Le Temple noir"], [38, "Loch Modan"], [3487, "Lune-d'argent"], [4298, "L'enclave Écarlate"], [139, "Maleterres de l'est"], - [28, "Maleterres de l'ouest"], [8, "Marais des Chagrins"], [2100, "Maraudon"], [40, "Marche de l'Ouest"], [15, "Marécage d'Âprefange"], - [3521, "Marécage de Zangar"], [400, "Mille pointes"], [796, "Monastère écarlate"], [36, "Montagnes d'Alterac"], [25, "Mont Rochenoire"], - [215, "Mulgore"], [3518, "Nagrand"], [3456, "Naxxramas"], [1637, "Orgrimmar"], [331, "Orneval"], - [1583, "Pic Rochenoire"], [4075, "Plateau du Puits de soleil"], [719, "Profondeurs de Brassenoire"], [1584, "Profondeurs de Rochenoire"], [3483, "Péninsule des Flammes infernales"], - [3523, "Raz-de-Néant"], [493, "Reflet-de-Lune"], [3562, "Remparts des Flammes infernales"],[2159, "Repaire d'Onyxia"], [3923, "Repaire de Gruul"], - [2677, "Repaire de l'Aile noire"], [1216, "Repaire des Grumegueules"], [4384, "Rivage des Anciens"], [3429, "Ruines d'Ahn'Qiraj"], [3968, "Ruines de Lordaeron"], - [3905, "Réservoir de Glissecroc"], [4820, "Salles des Reflets"], [2057, "Scholomance"], [3703, "Shattrath"], [1377, "Silithus"], - [3679, "Skettis"], [148, "Sombrivage"], [3606, "Sommet d'Hyjal"], [722, "Souilles de Tranchebauge"], [46, "Steppes ardentes"], - [2017, "Stratholme"], [440, "Tanaris"], [141, "Teldrassil"], [3959, "Temple noir"], [4131, "Terrasse des Magistères"], - [4, "Terres foudroyées"], [3, "Terres ingrates"], [3792, "Tombes-mana"], [3537, "Toundra Boréenne"], [2257, "Tram des profondeurs"], - [1337, "Uldaman"], [4273, "Ulduar"], [2597, "Vallée d'Alterac"], [3520, "Vallée d'Ombrelune"], [33, "Vallée de Strangleronce"], - [3805, "Zul'Aman"], [66, "Zul'Drak"], [1176, "Zul'Farrak"], [1977, "Zul'Gurub"] + [null, "Royaumes de l'est"], + [ 10, "Bois de la Pénombre"], [3430, "Bois des Chants éternels"], [ 85, "Clairières de Tirisfal"], [ 267, "Contreforts de Hautebrande"], [ 41, "Défilé de Deuillevent"], + [ 1, "Dun Morogh"], [ 12, "Forêt d'Elwynn"], [ 130, "Forêt des Pins argentés"], [1537, "Forgefer"], [1497, "Fossoyeuse"], + [ 51, "Gorge des Vents brûlants"], [ 45, "Hautes-terres d'Arathi"], [1519, "Hurlevent"], [4080, "Île de Quel'Danas"], [ 44, "Les Carmines"], + [ 47, "Les Hinterlands"], [ 11, "Les Paluns"], [3433, "Les Terres fantômes"], [ 38, "Loch Modan"], [3487, "Lune-d'argent"], + [4298, "Maleterres : l'enclave Écarlate"],[ 139, "Maleterres de l'est"], [ 28, "Maleterres de l'ouest"], [ 8, "Marais des Chagrins"], [ 40, "Marche de l'Ouest"], + [ 36, "Montagnes d'Alterac"], [ 46, "Steppes ardentes"], [ 4, "Terres foudroyées"], [ 3, "Terres ingrates"], [2257, "Tram des profondeurs"], + [ 33, "Vallée de Strangleronce"], + [null, "Kalimdor"], + [ 16, "Azshara"], [ 618, "Berceau-de-l'Hiver"], [ 490, "Cratère d'Un'Goro"], [1657, "Darnassus"], [ 405, "Désolace"], + [ 14, "Durotar"], [ 357, "Féralas"], [ 361, "Gangrebois"], [3524, "Île de Brume-azur"], [3525, "Île de Brume-sang"], + [3557, "L'Exodar"], [1638, "Les Pitons du Tonnerre"], [ 406, "Les Serres-Rocheuses"], [ 17, "Les Tarides"], [ 15, "Marécage d'Âprefange"], + [ 400, "Mille pointes"], [ 215, "Mulgore"], [1637, "Orgrimmar"], [ 331, "Orneval"], [ 493, "Reflet-de-Lune"], + [1377, "Silithus"], [ 148, "Sombrivage"], [ 440, "Tanaris"], [ 141, "Teldrassil"], + [null, "Outreterre"], + [3519, "Forêt de Terokkar"], [3522, "Les Tranchantes"], [3521, "Marécage de Zangar"], [3518, "Nagrand"], [3483, "Péninsule des Flammes infernales"], + [3523, "Raz-de-Néant"], [3703, "Shattrath"], [3520, "Vallée d'Ombrelune"], + [null, "Norfendre"], + [4742, "Accostage de Hrothgar"], [3711, "Bassin de Sholazar"], [4395, "Dalaran"], [ 65, "Désolation des dragons"], [ 495, "Fjord Hurlant"], + [2817, "Forêt du Chant de cristal"], [4197, "Joug-d'hiver"], [ 210, "La Couronne de glace"], [ 394, "Les Grisonnes"], [ 67, "Les pics Foudroyés"], + [3537, "Toundra Boréenne"], [ 66, "Zul'Drak"], + [null, "Donjons"], + [4494, "Ahn'kahet : l'Ancien royaume"], [4277, "Azjol -Nérub"], [3716, "Basse-tourbière"], [3715, "Caveau de la vapeur"], [ 718, "Cavernes des Lamentations"], + [1196, "Cime d'Utgarde"], [2367, "Contreforts de Hautebrande d'antan"], [3790, "Cryptes Auchenaï"], [ 209, "Donjon d'Ombrecroc"], + [ 206, "Donjon d'Utgarde"], [4196, "Donjon de Drak'Tharon"], [3717, "Le seigneur du Givre Ahune"], [4813, "Fosse de Saron"], [3713, "Fournaise du sang"], + [ 721, "Gnomeregan"], [2437, "Gouffre de Ragefeu"], [4416, "Gundrak"], [2557, "Hache-tripes"], [ 491, "Kraal de Tranchebauge"], + [4723, "L'épreuve du champion"], [4100, "L'Épuration de Stratholme"], [4228, "L'Oculus"], [3848, "L’Arcatraz"], [3847, "La Botanica"], + [4809, "La Forge des âmes"], [3789, "Labyrinthe des ombres"], [4415, "Le fort Pourpre"], [3849, "Le Méchanar"], [4265, "Le Nexus"], + [2366, "Le Noir Marécage"], [4272, "Les salles de Foudre"], [4264, "Les salles de Pierre"], [2100, "Maraudon"], [ 796, "Monastère écarlate"], + [1581, "Mortemines"], [1583, "Pic Rochenoire"], [ 717, "Prison de Hurlevent"], [ 719, "Profondeurs de Brassenoire"], [1584, "Profondeurs de Rochenoire"], + [3562, "Remparts des Flammes infernales"],[3714, "Salles brisées"], [4820, "Salles des Reflets"], [3791, "Salles des Sethekk"], [2057, "Scholomance"], + [ 722, "Souilles de Tranchebauge"], [2017, "Stratholme"], [1477, "Temple englouti"], [4131, "Terrasse des Magistères"], [3792, "Tombes-mana"], + [1337, "Uldaman"], [1176, "Zul'Farrak"], + [null, "Raids"], + [4603, "Caveau d'Archavon"], [3607, "Caverne du sanctuaire du Serpent"], [4812, "Citadelle de la Couronne de glace"], + [2717, "Cœur du Magma"], [3845, "Donjon de la Tempête"], [3457, "Karazhan"], [4722, "L'épreuve du croisé"], [4500, "L'Œil de l'éternité"], + [3606, "La bataille du mont Hyjal"], [4075, "Le Puits de soleil"], [3923, "Le repaire de Gruul"], [3836, "Le repaire de Magtheridon"], [4493, "Le sanctum Obsidien"], + [4987, "Le sanctum Rubis"], [3456, "Naxxramas"], [2159, "Repaire d'Onyxia"], [2677, "Repaire de l'Aile noire"], [3429, "Ruines d'Ahn'Qiraj"], + [3428, "Temple d'Ahn'Qiraj"], [3959, "Temple noir"], [4273, "Ulduar"], [3805, "Zul'Aman"], [1977, "Zul'Gurub"], + [null, "Champs de bataille"], + [3358, "Bassin d'Arathi"], [3277, "Goulet des Chanteguerres"], [4710, "Île des Conquérants"], [3820, "L'Œil du cyclone"], [4384, "Rivage des Anciens"], + [2597, "Vallée d'Alterac"], + [null, "Arènes"], + [4378, "Arène de Dalaran"], [3698, "Arène de Nagrand"], [3702, "Arène des Tranchantes"], [4406, "L'Arène des valeureux"], [3968, "Ruines de Lordaeron"] ], resistance: [ [6, "Arcane"], [2, "Feu"], [3, "Nature"], [4, "Givre"], [5, "Ombre"], @@ -3713,67 +3731,85 @@ var LANG = { ], pvp: [ [1, "Oui"], [3, "Arène"], [4, "Champ de bataille"], [5, "Monde"], [2, "Non"] ], currency: [ - [24368, "Armes de Glissecroc"], [4291, "Bobine de soie"], [24245, "Chapeluisant"], [32572, "Cristal apogide"], [38425, "Cuir boréen lourd"], - [32569, "Eclat apogide"], [28558, "Eclat d'esprit"], [34052, "Eclat de rêve"], [43228, "Eclat du gardien des pierres"],[40752, "Emblème d'héroïsme"], - [45624, "Emblème de conquête"], [49426, "Emblème de givre"], [47241, "Emblème de triomphe"], [40753, "Emblème de vaillance"], [23247, "Fleur ardente"], - [44128, "Fourrure arctique"], [26045, "Gage de bataille de Halaa"], [26044, "Gage de recherche de Halaa"], [34664, "Granule de soleil"], [29434, "Insigne de justice"], - [37829, "Jeton de la fête des Brasseurs"], [43589, "Marque d'honneur de Joug-d'hiver"], [29024, "Marque d'honneur de l'Oeil du cyclone"], - [20560, "Marque d'honneur de la vallée d'Alterac"], [20559, "Marque d'honneur du bassin d'Arathi"], [20558, "Marque d'honneur du goulet des Chanteguerres"], - [41596, "Marque de joaillerie de Dalaran"], [32897, "Marque des Illidari"], [24581, "Marque de Thrallmar"], [24579, "Marque du bastion de l'Honneur"], - [34597, "Palourde aileron-d'hiver"], [37836, "Pièce de la KapitalRisk"], [29735, "Poussière sacrée"], [43016, "Prix de cuisine de Dalaran"], [29736, "Rune des arcanes"], - [22484, "Rune nécrotique"], [44990, "Sceau de champion"], [47242, "Trophée de la croisade"], [52027, "Marque de sanctification de conquérant"], - [52030, "Marque de sanctification de conquérant (Héroïque)"], [52026, "Marque de sanctification de protecteur"], [52029, "Marque de sanctification de protecteur (Héroïque)"], - [52025, "Marque de sanctification de vainqueur"], [52028, "Marque de sanctification de vainqueur (Héroïque)"] + [null, "Classique"], + [4291, "Bobine de soie"], [23247, "Fleur ardente"], [37829, "Jeton de la fête des Brasseurs"], + [null, "Burning Crusade"], + [24368, "Armes de Glissecroc"], [24245, "Chapeluisant"], [32572, "Cristal apogide"], + [32569, "Eclat apogide"], [34664, "Granule de soleil"], [32897, "Marque des Illidari"], + [29735, "Poussière sacrée"], [29736, "Rune des Arcanes"], [22484, "Rune nécrotique"], + [null, "Wrath of the Lich King"], + [38425, "Cuir boréen lourd"], [34052, "Eclat de rêve"], [44128, "Fourrure arctique"], + [41596, "Marque de joaillerie de Dalaran"], [34597, "Palourde aileron-d'hiver"], [43016, "Prix de cuisine de Dalaran"], + [44990, "Sceau de champion"], + [null, "JcJ"], + [28558, "Eclat d'esprit"], [43228, "Eclat du gardien des pierres"], [26045, "Gage de bataille de Halaa"], + [26044, "Gage de recherche de Halaa"], [43589, "Marque d'honneur de Joug-d'hiver"], [29024, "Marque d'honneur de l'Oeil du cyclone"], + [20560, "Marque d'honneur de la vallée d'Alterac"], [20559, "Marque d'honneur du bassin Arathi"], [20558, "Marque d'honneur du goulet des Chanteguerres"], + [24581, "Marque de Thrallmar"], [24579, "Marque du bastion de l'Honneur"], [37836, "Pièce de la KapitalRisk"], + [null, "Donjons & Raids"], + [29434, "Ecusson de justice"], [40752, "Emblème d'héroïsme"], [45624, "Emblème de conquête"], + [49426, "Emblème de givre"], [47241, "Emblème de triomphe"], [40753, "Emblème de vaillance"] ], itemcurrency: [ + [null, "Ensemble de raid palier 10"], + [52027, "Marque de sanctification de conquérant"], [52026, "Marque de sanctification de protecteur"], [52025, "Marque de sanctification de vainqueur"], + [52030, "Marque de sanctification de conquérant (Héroïque)"], [52029, "Marque de sanctification de protecteur (Héroïque)"], [52028, "Marque de sanctification de vainqueur (Héroïque)"], + [null, "Ensemble de raid palier 9"], + [47242, "Trophée de la croisade"], + [47557, "Tenue de parade du grand conquérant"], [47558, "Tenue de parade du grand protecteur"], [47559, "Tenue de parade du grand vainqueur"], + [null, "Ensemble de raid palier 8"], + [45635, "Corselet du conquérant indiscipliné"], [45636, "Corselet du protecteur indiscipliné"], [45637, "Corselet du vainqueur indiscipliné"], + [45638, "Couronne du conquérant indiscipliné"], [45639, "Couronne du protecteur indiscipliné"], [45640, "Couronne du vainqueur indiscipliné"], + [45632, "Cuirasse du conquérant indiscipliné"], [45633, "Cuirasse du protecteur indiscipliné"], [45634, "Cuirasse du vainqueur indiscipliné"], + [45653, "Cuissards du conquérant indiscipliné"], [45654, "Cuissards du protecteur indiscipliné"], [45655, "Cuissards du vainqueur indiscipliné"], + [45641, "Gantelets du conquérant indiscipliné"], [45642, "Gantelets du protecteur indiscipliné"], [45643, "Gantelets du vainqueur indiscipliné"], + [45644, "Gants du conquérant indiscipliné"], [45645, "Gants du protecteur indiscipliné"], [45646, "Gants du vainqueur indiscipliné"], + [45647, "Heaume du conquérant indiscipliné"], [45648, "Heaume du protecteur indiscipliné"], [45649, "Heaume du vainqueur indiscipliné"], + [45650, "Jambières du conquérant indiscipliné"], [45651, "Jambières du protecteur indiscipliné"], [45652, "Jambières du vainqueur indiscipliné"], + [45656, "Mantelet du conquérant indiscipliné"], [45657, "Mantelet du protecteur indiscipliné"], [45658, "Mantelet du vainqueur indiscipliné"], + [45659, "Spallières du conquérant indiscipliné"], [45660, "Spallières du protecteur indiscipliné"], [45661, "Spallières du vainqueur indiscipliné"], + [null, "Ensemble de raid palier 7"], + [40610, "Corselet du conquérant perdu"], [40611, "Corselet du protecteur perdu"], [40612, "Corselet du vainqueur perdu"], + [40631, "Couronne du conquérant perdu"], [40632, "Couronne du protecteur perdu"], [40633, "Couronne du vainqueur perdu"], + [40625, "Cuirasse du conquérant perdu"], [40626, "Cuirasse du protecteur perdu"], [40627, "Cuirasse du vainqueur perdu"], + [40634, "Cuissards du conquérant perdu"], [40635, "Cuissards du protecteur perdu"], [40636, "Cuissards du vainqueur perdu"], + [40628, "Gantelets du conquérant perdu"], [40629, "Gantelets du protecteur perdu"], [40630, "Gantelets du vainqueur perdu"], + [40613, "Gants du conquérant perdu"], [40614, "Gants du protecteur perdu"], [40615, "Gants du vainqueur perdu"], + [40616, "Heaume du conquérant perdu"], [40617, "Heaume du protecteur perdu"], [40618, "Heaume du vainqueur perdu"], + [40619, "Jambières du conquérant perdu"], [40620, "Jambières du protecteur perdu"], [40621, "Jambières du vainqueur perdu"], + [40637, "Mantelet du conquérant perdu"], [40638, "Mantelet du protecteur perdu"], [40639, "Mantelet du vainqueur perdu"], + [40622, "Spallières du conquérant perdu"], [40623, "Spallières du protecteur perdu"], [40624, "Spallières du vainqueur perdu"], + [null, "Ensemble de raid palier 6"], [34856, "Bottes du conquérant oublié"], [34857, "Bottes du protecteur oublié"], [34858, "Bottes du vainqueur oublié"], - [34169, "Braies de l'agression naturelle"], [34848, "Brassards du conquérant oublié"], [34851, "Brassards du protecteur oublié"], - [34852, "Brassards du vainqueur oublié"], [34332, "Capuche de Gul'dan"], [34339, "Capuche de la pureté de la Lumière"], + [34848, "Brassards du conquérant oublié"], [34851, "Brassards du protecteur oublié"], [34852, "Brassards du vainqueur oublié"], [34853, "Ceinture du conquérant oublié"], [34854, "Ceinture du protecteur oublié"], [34855, "Ceinture du vainqueur oublié"], - [34202, "Châle d'émerveillement"], [29754, "Corselet du champion déchu"], [30236, "Corselet du champion vaincu"], - [45635, "Corselet du conquérant indiscipliné"], [31089, "Corselet du conquérant oublié"], [40610, "Corselet du conquérant perdu"], - [29753, "Corselet du défenseur déchu"], [30237, "Corselet du défenseur vaincu"], [29755, "Corselet du héros déchu"], - [30238, "Corselet du héros vaincu"], [34216, "Corselet du judicateur héroïque"], [45636, "Corselet du protecteur indiscipliné"], - [31091, "Corselet du protecteur oublié"], [40611, "Corselet du protecteur perdu"], [45637, "Corselet du vainqueur indiscipliné"], - [31090, "Corselet du vainqueur oublié"], [40612, "Corselet du vainqueur perdu"], [34345, "Couronne d'Anasterian"], - [45638, "Couronne du conquérant indiscipliné"], [40631, "Couronne du conquérant perdu"], [45639, "Couronne du protecteur indiscipliné"], - [40632, "Couronne du protecteur perdu"], [45640, "Couronne du vainqueur indiscipliné"], [40633, "Couronne du vainqueur perdu"], - [34245, "Couvre-chef d'Ursoc le Sage"], [45632, "Cuirasse du conquérant indiscipliné"], [40625, "Cuirasse du conquérant perdu"], - [45633, "Cuirasse du protecteur indiscipliné"], [40626, "Cuirasse du protecteur perdu"], [45634, "Cuirasse du vainqueur indiscipliné"], - [40627, "Cuirasse du vainqueur perdu"], [45653, "Cuissards du conquérant indiscipliné"], [40634, "Cuissards du conquérant perdu"], - [34167, "Cuissards du mastodonte sacré"], [45654, "Cuissards du protecteur indiscipliné"], [40635, "Cuissards du protecteur perdu"], - [45655, "Cuissards du vainqueur indiscipliné"], [40636, "Cuissards du vainqueur perdu"], [34180, "Cuissards gangrefurie"], + [31089, "Corselet du conquérant oublié"], [31091, "Corselet du protecteur oublié"], [31090, "Corselet du vainqueur oublié"], + [31101, "Espauliers du conquérant oublié"], [31103, "Espauliers du protecteur oublié"], [31102, "Espauliers du vainqueur oublié"], + [31092, "Gants du conquérant oublié"], [31094, "Gants du protecteur oublié"], [31093, "Gants du vainqueur oublié"], + [31097, "Heaume du conquérant oublié"], [31095, "Heaume du protecteur oublié"], [31096, "Heaume du vainqueur oublié"], + [31098, "Jambières du conquérant oublié"], [31100, "Jambières du protecteur oublié"], [31099, "Jambières du vainqueur oublié"], + [null, "Ensemble de raid palier 5"], + [30236, "Corselet du champion vaincu"], [30237, "Corselet du défenseur vaincu"], [30238, "Corselet du héros vaincu"], + [30248, "Espauliers du champion vaincu"], [30249, "Espauliers du défenseur vaincu"], [30250, "Espauliers du héros vaincu"], + [30239, "Gants du champion vaincu"], [30240, "Gants du défenseur vaincu"], [30241, "Gants du héros vaincu"], + [30242, "Heaume du champion vaincu"], [30243, "Heaume du défenseur vaincu"], [30244, "Heaume du héros vaincu"], + [30245, "Jambières du champion vaincu"], [30246, "Jambières du défenseur vaincu"], [30247, "Jambières du héros vaincu"], + [null, "Ensemble de raid palier 4"], + [29754, "Corselet du champion déchu"], [29753, "Corselet du défenseur déchu"], [29755, "Corselet du héros déchu"], + [29763, "Espauliers du champion déchu"], [29764, "Espauliers du défenseur déchu"], [29762, "Espauliers du héros déchu"], + [29757, "Gants du champion déchu"], [29758, "Gants du défenseur déchu"], [29756, "Gants du héros déchu"], + [29760, "Heaume du champion déchu"], [29761, "Heaume du défenseur déchu"], [29759, "Heaume du héros déchu"], + [29766, "Jambières du champion déchu"], [29767, "Jambières du défenseur déchu"], [29765, "Jambières du héros déchu"], + [null, "Divers"], + [34169, "Braies de l'agression naturelle"], [34332, "Capuche de Gul'dan"], [34339, "Capuche de la pureté de la Lumière"], + [34202, "Châle d'émerveillement"], [34216, "Corselet du judicateur héroïque"], [34345, "Couronne d'Anasterian"], + [34245, "Couvre-chef d'Ursoc le Sage"], [34167, "Cuissards du mastodonte sacré"], [34180, "Cuissards gangrefurie"], [34170, "Culotte de la lutte calmante"], [34208, "Epaulettes d'équilibre"], [34192, "Espauliers de persévérance"], - [29763, "Espauliers du champion déchu"], [30248, "Espauliers du champion vaincu"], [31101, "Espauliers du conquérant oublié"], - [29764, "Espauliers du défenseur déchu"], [30249, "Espauliers du défenseur vaincu"], [29762, "Espauliers du héros déchu"], - [30250, "Espauliers du héros vaincu"], [31103, "Espauliers du protecteur oublié"], [31102, "Espauliers du vainqueur oublié"], - [34350, "Gantelets de l'ancienne Ombrelune"], [45641, "Gantelets du conquérant indiscipliné"], [40628, "Gantelets du conquérant perdu"], - [45642, "Gantelets du protecteur indiscipliné"], [40629, "Gantelets du protecteur perdu"], [45643, "Gantelets du vainqueur indiscipliné"], - [40630, "Gantelets du vainqueur perdu"], [34234, "Gantelets ombreux du paroxysme"], [29757, "Gants du champion déchu"], - [30239, "Gants du champion vaincu"], [45644, "Gants du conquérant indiscipliné"], [31092, "Gants du conquérant oublié"], - [40613, "Gants du conquérant perdu"], [29758, "Gants du défenseur déchu"], [30240, "Gants du défenseur vaincu"], - [29756, "Gants du héros déchu"], [30241, "Gants du héros vaincu"], [45645, "Gants du protecteur indiscipliné"], - [31094, "Gants du protecteur oublié"], [40614, "Gants du protecteur perdu"], [45646, "Gants du vainqueur indiscipliné"], - [31093, "Gants du vainqueur oublié"], [40615, "Gants du vainqueur perdu"], [34342, "Garde-mains de l'aube"], + [34350, "Gantelets de l'ancienne Ombrelune"], [34234, "Gantelets ombreux du paroxysme"], [34342, "Garde-mains de l'aube"], [34212, "Gilet du halo solaire"], [34215, "Harnais de guerre de la fureur téméraire"], [34211, "Harnais des instincts charnels"], - [34243, "Heaume de la piété ardente"], [29760, "Heaume du champion déchu"], [30242, "Heaume du champion vaincu"], - [45647, "Heaume du conquérant indiscipliné"], [31097, "Heaume du conquérant oublié"], [40616, "Heaume du conquérant perdu"], - [29761, "Heaume du défenseur déchu"], [30243, "Heaume du défenseur vaincu"], [29759, "Heaume du héros déchu"], - [30244, "Heaume du héros vaincu"], [45648, "Heaume du protecteur indiscipliné"], [31095, "Heaume du protecteur oublié"], - [40617, "Heaume du protecteur perdu"], [45649, "Heaume du vainqueur indiscipliné"], [31096, "Heaume du vainqueur oublié"], - [40618, "Heaume du vainqueur perdu"], [29766, "Jambières du champion déchu"], [30245, "Jambières du champion vaincu"], - [45650, "Jambières du conquérant indiscipliné"], [31098, "Jambières du conquérant oublié"], [40619, "Jambières du conquérant perdu"], - [29767, "Jambières du défenseur déchu"], [30246, "Jambières du défenseur vaincu"], [29765, "Jambières du héros déchu"], - [30247, "Jambières du héros vaincu"], [45651, "Jambières du protecteur indiscipliné"], [31100, "Jambières du protecteur oublié"], - [40620, "Jambières du protecteur perdu"], [45652, "Jambières du vainqueur indiscipliné"], [31099, "Jambières du vainqueur oublié"], - [40621, "Jambières du vainqueur perdu"], [34186, "Mailles de l'orage tumultueux"], [45656, "Mantelet du conquérant indiscipliné"], - [40637, "Mantelet du conquérant perdu"], [45657, "Mantelet du protecteur indiscipliné"], [40638, "Mantelet du protecteur perdu"], - [45658, "Mantelet du vainqueur indiscipliné"], [40639, "Mantelet du vainqueur perdu"], [34351, "Protège-mains de majesté paisible"], - [34195, "Protège-épaules de véhémence"], [34233, "Robe de la Lumière chancelante"], [34244, "Semblance fourbe"], - [34209, "Spallières de reconquête"], [45659, "Spallières du conquérant indiscipliné"], [40622, "Spallières du conquérant perdu"], - [45660, "Spallières du protecteur indiscipliné"], [40623, "Spallières du protecteur perdu"], [34193, "Spallières du sauveur thalassien"], - [45661, "Spallières du vainqueur indiscipliné"], [40624, "Spallières du vainqueur perdu"], [47557, "Tenue de parade du grand conquérant"], - [47558, "Tenue de parade du grand protecteur"], [47559, "Tenue de parade du grand vainqueur"] + [34243, "Heaume de la piété ardente"], [34186, "Mailles de l'orage tumultueux"], [34195, "Protège-épaules de véhémence"], + [34351, "Protège-mains de majesté paisible"], [34233, "Robe de la Lumière chancelante"], [34244, "Semblance fourbe"], + [34209, "Spallières de reconquête"], [34193, "Spallières du sauveur thalassien"], [34229, "Vêtements des rivages sereins"] ], queststart: [ [3, "Objet"], [1, "PNJ"], [2, "Entité"] ], questend: [ [1, "PNJ"], [2, "Entité"] ], @@ -3820,7 +3856,7 @@ var LANG = { [31, "Increase Run Speed %"], [32, "Mod Mounted Speed %"], [33, "Decrease Run Speed %"], [34, "Mod Maximum Health - Flat"], [35, "Mod Maximum Power - Flat"], [36, "Shapeshift"], [37, "Spell Effect Immunity"], [38, "Spell Aura Immunity"], [39, "Spell School Immunity"], [40, "Damage Immunity"], [41, "Dispel Type Immunity"], [42, "Proc Trigger Spell"], [43, "Proc Trigger Damage"], [44, "Track Creatures"], [45, "Track Resources"], - [46, "Ignore All Gear"], [47, "Mod Parry %"], /* [48, null] */ [49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], + [46, "Ignore All Gear"], [47, "Mod Parry %"], [48, "Periodic Trigger Spell from Client"],[49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], [51, "Mod Block %"], [52, "Mod Physical Crit Chance"], [53, "Periodically Drain Health"], [54, "Mod Physical Hit Chance"], [55, "Mod Spell Hit Chance"], [56, "Transform"], [57, "Mod Spell Crit Chance"], [58, "Increase Swim Speed %"], [59, "Mod Damage Done Versus Creature"],[60, "Pacify & Silence"], [61, "Mod Size %"], [62, "Periodically Transfer Health"], [63, "Periodically Transfer Power"], [64, "Periodically Drain Power"], [65, "Mod Spell Haste % (not stacking)"], @@ -3902,11 +3938,10 @@ var LANG = { [131, "Play Sound"], [132, "Play Music"], [133, "Unlearn Specialization"], [134, "Kill Credit2"], [135, "Call Pet"], [136, "Heal for % of Total Health"], [137, "Give % of Total Power"], [138, "Leap Back"], [139, "Abandon Quest"], [140, "Force Spell Cast"], [141, "Force Spell Cast with Value"], [142, "Trigger Spell with Value"], [143, "Apply Area Aura - Pet Owner"], [144, "Knockback to Dest."], [145, "Pull Towards Dest."], - [146, "Activate Rune"], [147, "Fail Quest"], [149, "Charge to Dest."], [150, "Start Quest"], + [146, "Activate Rune"], [147, "Fail Quest"], [148, "Trigger Missile with Value"], [149, "Charge to Dest."], [150, "Start Quest"], [151, "Trigger Spell 2"], [152, "Summon - Refer-A-Friend"], [153, "Create Tamed Pet"], [154, "Discover Flight Path"], [155, "Dual Wield 2H Weapons"], - [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], - [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"], - [167, "Update Player Phase"] + [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], [160, "Force Cast 2"], + [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"] ], damagetype: [ [1, "Aucun"], [2, "Magie"], [3, "En mêlée"], [4, "À distance"] @@ -4132,6 +4167,7 @@ var LANG = { id: "ID", classspecific: "Classe-spécifique", racespecific: "Spécifique à la race", + setspvpflag: "Vous garde en mode JvJ", sepgainsrewards: "Gains/récompenses", experiencegained: "Expérience gagnée", @@ -4902,10 +4938,18 @@ var LANG = { /* AoWoW: start custom */ + // Profiler completions import + completion: 'Achèvement', // WH.TERMS.completion + complete: 'Complète', // WH.TERMS.complete + incomplete: 'Incomplet', // WH.TERMS.incomplete + parens_format: '$1 ($2)', // WH.TERMS.parens_format + // click to copy fn copied: 'Copié', clickToCopy: 'Cliquer pour Copier', nothingToCopy_tip: 'Rien à copier !', + copy_clipboard: 'Copier', + copy_format: 'Copier %s', // TC conditions display tab_conditions: '[Conditions]', diff --git a/static/js/locale_ruru.js b/static/js/locale_ruru.js index 117fd9cc..1a2b065f 100644 --- a/static/js/locale_ruru.js +++ b/static/js/locale_ruru.js @@ -1531,6 +1531,7 @@ var g_quest_sorts = { "-374": 'Сад чудес', "-375": 'Пиршество странников', "-376": 'Любовная лихорадка', + 0: 'Разное', 1: 'Дун Морог', 3: 'Бесплодные земли', 4: 'Выжженные земли', @@ -2520,7 +2521,8 @@ var g_conditions = { 47: 'The Player has$N: not; $2 [quest=$1]', 48: 'The Player has$N: not; collected $3 towards objective #$2 of [quest=$1]', 49: 'The current map difficulty is #$1', - 50: 'The Player $C$1$Ncan:can\'t; be:is$N: not;; a Gamemaster' + 50: 'The Player $C$1$Ncan:can\'t; be:is$N: not;; a Gamemaster', + 58: 'The StringID of this entity is$N: not; $4' }; /* end aowow custom */ @@ -2552,7 +2554,7 @@ var LANG = { date_simple: "$1.$2.$3", unknowndate_stc: "Неизвестная дата", date_months: ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"], - date_days: ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"], + date_days: ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"], amount: "Количество", abilities: "Способности", @@ -3098,6 +3100,8 @@ var LANG = { tooltip_uprate: "Высокий", tooltip_zonelink: "Щелкнув по этой ссылке вы<br />попадете на страницу местности.", + tooltip_immune: "Иммунный", // aowow - custom + reputationhistory: "История репутации", reputationaction: "Действие Репутации", @@ -3613,59 +3617,73 @@ var LANG = { num: [ [1, ">"], [2, ">="], [3, "="], [4, "<="], [5, "<"], [6, "!="]], side: [ [1, "Да"], [2, "Альянс"], [3, "Орда"], [4, "Обе"], [5, "Нет"] ], faction: [ - [1037, "Авангард Альянса"], [932, "Алдоры"], [469, "Альянс"], [967, "Аметистовое Око"], [1085, "Армия Песни Войны"], - [1077, "Армия Расколотого Солнца"], [59, "Братство Тория"], [889, "Всадники Песни Войны"], [749, "Гидраксианские Повелители Вод"], [81, "Громовой Утес"], - [69, "Дарнас"], [1091, "Драконий союз"], [576, "Древобрюхи"], [1126, "Зиморожденные"], [54, "Изгнанники Гномрегана"], - [470, "Кабестан"], [1073, "Калу'ак"], [1067, "Карающая Длань"], [169, "Картель Хитрая Шестеренка"], [942, "Кенарийская экспедиция"], - [92, "Кентавры из племени Гелкис"], [93, "Кентавры из племени Маграм"], [1090, "Кирин-Тор"], [729, "Клан Северного Волка"], [933, "Консорциум"], - [609, "Круг Кенария"], [577, "Круговзор"], [1015, "Крылья Пустоты"], [978, "Куренай"], [509, "Лига Аратора"], - [1068, "Лига исследователей"], [911, "Луносвет"], [941, "Маг'хары"], [1011, "Нижний Город"], [1038, "Огри'ла"], - [946, "Оплот Чести"], [1105, "Оракулы"], [76, "Оргриммар"], [67, "Орда"], [510, "Осквернители"], - [1156, "Пепельный союз"], [1012, "Пеплоусты-служители"], [990, "Песчаная Чешуя"], [21, "Пиратская Бухта"], [87, "Пираты Кровавого Паруса"], - [1104, "Племя Бешеного Сердца"], [270, "Племя Зандалар"], [68, "Подгород"], [1124, "Похитители Солнца"], [369, "Прибамбасск"], - [934, "Провидцы"], [910, "Род Ноздорму"], [1098, "Рыцари Черного Клинка"], [1106, "Серебряный Авангард"], [529, "Серебряный Рассвет"], - [1094, "Серебряный союз"], [70, "Синдикат"], [970, "Спореггар"], [890, "Среброкрылые Часовые"], [47, "Стальгорн"], - [730, "Стража Грозовой Вершины"], [1031, "Стражи Небес Ша'тар"], [1119, "Сыны Ходира"], [1064, "Таунка"], [947, "Траллмар"], - [922, "Транквиллион"], [530, "Тролли Черного Копья"], [589, "Укротители ледопардов"], [989, "Хранители Времени"], [349, "Черный Ворон"], - [935, "Ша'тар"], [809, "Шен'дралар"], [72, "Штормград"], [930, "Экзодар"], [1052, "Экспедиция Орды"], - [1050, "Экспедиция Отважных"], [909, "Ярмарка Новолуния"] + [null, "World of Warcraft"], + [ 469, "Альянс"], [ 59, "Братство Тория"], [ 889, "Всадники Песни Войны"], [ 749, "Гидраксианские Повелители Вод"],[ 81, "Громовой Утес"], + [ 69, "Дарнас"], [ 576, "Древобрюхи"], [ 54, "Изгнанники Гномрегана"], [ 470, "Кабестан"], [ 169, "Картель Хитрая Шестеренка"], + [ 92, "Кентавры из племени Гелкис"], [ 93, "Кентавры из племени Маграм"], [ 729, "Клан Северного Волка"], [ 609, "Круг Кенария"], [ 577, "Круговзор"], + [ 509, "Лига Аратора"], [ 911, "Луносвет"], [ 76, "Оргриммар"], [ 67, "Орда"], [ 510, "Осквернители"], + [ 21, "Пиратская Бухта"], [ 87, "Пираты Кровавого Паруса"], [ 270, "Племя Зандалар"], [ 68, "Подгород"], [ 369, "Прибамбасск"], + [ 910, "Род Ноздорму"], [ 529, "Серебряный Рассвет"], [ 890, "Среброкрылые Часовые"], [ 47, "Стальгорн"], [ 730, "Стража Грозовой Вершины"], + [ 922, "Транквиллион"], [ 530, "Тролли Черного Копья"], [ 349, "Черный Ворон"], [ 809, "Шен'дралар"], [ 72, "Штормград"], + [ 930, "Экзодар"], [ 909, "Ярмарка Новолуния"], + [null, "The Burning Crusade"], + [ 932, "Алдоры"], [ 967, "Аметистовое Око"], [1077, "Армия Расколотого Солнца"], [ 942, "Кенарийская экспедиция"], [ 933, "Консорциум"], + [1015, "Крылья Пустоты"], [ 978, "Куренай"], [ 941, "Маг'хары"], [1011, "Нижний Город"], [1038, "Огри'ла"], + [ 946, "Оплот Чести"], [1012, "Пеплоусты-служители"], [ 990, "Песчаная Чешуя"], [ 934, "Провидцы"], [ 970, "Спореггар"], + [1031, "Стражи Небес Ша'тар"], [ 947, "Траллмар"], [ 989, "Хранители Времени"], [ 935, "Ша'тар"], + [null, "Wrath of the Lich King"], + [1037, "Авангард Альянса"], [1085, "Армия Песни Войны"], [1091, "Драконий союз"], [1126, "Зиморожденные"], [1073, "Калу'ак"], + [1067, "Карающая Длань"], [1090, "Кирин-Тор"], [1068, "Лига исследователей"], [1105, "Оракулы"], [1156, "Пепельный союз"], + [1104, "Племя Бешеного Сердца"], [1124, "Похитители Солнца"], [1098, "Рыцари Черного Клинка"], [1106, "Серебряный Авангард"], [1094, "Серебряный союз"], + [1119, "Сыны Ходира"], [1064, "Таунка"], [1052, "Экспедиция Орды"], [1050, "Экспедиция Отважных"], + [null, "Разное"], + [ 70, "Синдикат"], [ 589, "Укротители ледопардов"] ], zone: [ - [4277, "Азжол-Неруб"], [16, "Азшара"], [2597, "Альтеракская долина"], [36, "Альтеракские горы"], [4415, "Аметистовая крепость"], - [3428, "Ан'Кираж"], [4494, "Ан'кахет: Старое Королевство"], [4378, "Арена Даларана"], [4406, "Арена Доблести"], [3698, "Арена Награнда"], - [3702, "Арена Острогорья"], [3848, "Аркатрац"], [3790, "Аукенайские гробницы"], [3562, "Бастионы Адского Пламени"], [4384, "Берег Древних"], - [3, "Бесплодные земли"], [11, "Болотина"], [8, "Болото Печали"], [3537, "Борейская тундра"], [3847, "Ботаника"], - [1196, "Вершина Утгард"], [3606, "Вершина Хиджала"], [47, "Внутренние земли"], [139, "Восточные Чумные земли"], [4, "Выжженные земли"], - [1584, "Глубины Черной горы"], [721, "Гномреган"], [3792, "Гробницы Маны"], [67, "Грозовая Гряда"], [1638, "Громовой Утес"], - [4416, "Гундрак"], [4395, "Даларан"], [1657, "Дарнас"], [3520, "Долина Призрачной Луны"], [65, "Драконий Погост"], - [1, "Дун Морог"], [14, "Дуротар"], [2557, "Забытый Город"], [2918, "Зал Защитника"], [2917, "Зал Легенд"], - [4820, "Залы Отражений"], [3521, "Зангартопь"], [28, "Западные Чумные земли"], [40, "Западный Край"], [618, "Зимние Ключи"], - [3607, "Змеиное святилище"], [3805, "Зул'Аман"], [1977, "Зул'Гуруб"], [66, "Зул'Драк"], [1176, "Зул'Фаррак"], - [4722, "Испытание крестоносца"], [4723, "Испытание чемпиона"], [3457, "Каражан"], [406, "Когтистые горы"], [44, "Красногорье"], - [490, "Кратер Ун'Горо"], [3845, "Крепость Бурь"], [4196, "Крепость Драк'Тарон"], [1216, "Крепость Древобрюхов"], [209, "Крепость Темного Клыка"], - [206, "Крепость Утгард"], [3638, "Круг Испытаний"], [3775, "Круг Крови"], [4809, "Кузня Душ"], [3713, "Кузня Крови"], - [722, "Курганы Иглошкурых"], [491, "Лабиринты Иглошкурых"], [4742, "Лагерь Хротгара"], [210, "Ледяная Корона"], [3519, "Лес Тероккар"], - [2817, "Лес Хрустальной Песни"], [3430, "Леса Вечной Песни"], [3923, "Логово Груула"], [2677, "Логово Крыла Тьмы"], [3836, "Логово Магтеридона"], - [2159, "Логово Ониксии"], [38, "Лок Модан"], [493, "Лунная поляна"], [3487, "Луносвет"], [2100, "Мародон"], - [1581, "Мертвые копи"], [3849, "Механар"], [796, "Монастырь Алого ордена"], [215, "Мулгор"], [45, "Нагорье Арати"], - [3518, "Награнд"], [3456, "Наксрамас"], [2057, "Некроситет"], [4265, "Нексус"], [719, "Непроглядная Пучина"], - [3716, "Нижетопь"], [3358, "Низина Арати"], [3711, "Низина Шолазар"], [4493, "Обсидиановое святилище"], [2437, "Огненная пропасть"], - [2717, "Огненные Недра"], [4197, "Озеро Ледяных Оков"], [3820, "Око Бури"], [4500, "Око Вечности"], [4228, "Окулус"], - [1637, "Оргриммар"], [361, "Оскверненный лес"], [4710, "Остров Завоеваний"], [4080, "Остров Кель'Данас"], [3525, "Остров Кровавой Дымки"], - [3524, "Остров Лазурной Дымки"], [3522, "Острогорье"], [4100, "Очищение Стратхольма"], [3715, "Паровое подземелье"], [41, "Перевал Мертвого Ветра"], - [1941, "Пещеры Времени"], [718, "Пещеры Стенаний"], [1583, "Пик Черной горы"], [4075, "Плато Солнечного Колодца"], [393, "Побережье Черного Копья"], - [1497, "Подгород"], [2257, "Подземный поезд"], [3483, "Полуостров Адского Пламени"], [267, "Предгорья Хилсбрада"], [3433, "Призрачные земли"], - [3523, "Пустоверть"], [405, "Пустоши"], [46, "Пылающие степи"], [15, "Пылевые топи"], [3714, "Разрушенные залы"], - [495, "Ревущий фьорд"], [3905, "Резервуар Кривого Клыка"], [3429, "Руины Ан'Киража"], [3968, "Руины Лордерона"], [394, "Седые холмы"], - [130, "Серебряный бор"], [3791, "Сетеккские залы"], [1377, "Силитус"], [3679, "Скеттис"], [4603, "Склеп Аркавона"], - [457, "Сокрытое море"], [1537, "Стальгорн"], [2367, "Старые предгорья Хилсбрада"], [17, "Степи"], [2017, "Стратхольм"], - [10, "Сумеречный лес"], [440, "Танарис"], [141, "Тельдрассил"], [148, "Темные берега"], [3789, "Темный лабиринт"], - [33, "Тернистая долина"], [4131, "Терраса Магистров"], [85, "Тирисфальские леса"], [51, "Тлеющее ущелье"], [400, "Тысяча Игл"], - [717, "Тюрьма"], [3717, "Узилище"], [1337, "Ульдаман"], [4273, "Ульдуар"], [3277, "Ущелье Песни Войны"], - [357, "Фералас"], [4024, "Хладарра"], [1477, "Храм Атал'Хаккара"], [4812, "Цитадель Ледяной Короны"], [25, "Черная гора"], - [2366, "Черные топи"], [3840, "Черный храм"], [3959, "Черный храм"], [4264, "Чертоги Камня"], [4272, "Чертоги Молний"], - [4298, "Анклав Алого ордена"], [3703, "Шаттрат"], [1519, "Штормград"], [3557, "Экзодар"], [12, "Элвиннский лес"], - [4813, "Яма Сарона"], [331, "Ясеневый лес"] + [null, "Восточные королевства"], + [ 36, "Альтеракские горы"], [ 3, "Бесплодные земли"], [ 11, "Болотина"], [ 8, "Болото Печали"], [ 47, "Внутренние земли"], + [ 139, "Восточные Чумные земли"], [ 4, "Выжженные земли"], [ 1, "Дун Морог"], [ 28, "Западные Чумные земли"], [ 40, "Западный Край"], + [ 44, "Красногорье"], [3430, "Леса Вечной Песни"], [ 38, "Лок Модан"], [3487, "Луносвет"], [ 45, "Нагорье Арати"], + [4080, "Остров Кель'Данас"], [ 41, "Перевал Мертвого Ветра"], [1497, "Подгород"], [2257, "Подземный поезд"], [ 267, "Предгорья Хилсбрада"], + [3433, "Призрачные земли"], [ 46, "Пылающие степи"], [ 130, "Серебряный бор"], [1537, "Стальгорн"], [ 10, "Сумеречный лес"], + [ 33, "Тернистая долина"], [ 85, "Тирисфальские леса"], [ 51, "Тлеющее ущелье"], [4298, "Чумные земли: Анклав Алого ордена"], + [1519, "Штормград"], [ 12, "Элвиннский лес"], + [null, "Калимдор"], + [ 16, "Азшара"], [1638, "Громовой Утес"], [1657, "Дарнас"], [ 14, "Дуротар"], [ 618, "Зимние Ключи"], + [ 406, "Когтистые горы"], [ 490, "Кратер Ун'Горо"], [ 493, "Лунная поляна"], [ 215, "Мулгор"], [1637, "Оргриммар"], + [ 361, "Оскверненный лес"], [3525, "Остров Кровавой Дымки"], [3524, "Остров Лазурной Дымки"], [ 405, "Пустоши"], [ 15, "Пылевые топи"], + [1377, "Силитус"], [ 17, "Степи"], [ 440, "Танарис"], [ 141, "Тельдрассил"], [ 148, "Темные берега"], + [ 400, "Тысяча Игл"], [ 357, "Фералас"], [3557, "Экзодар"], [ 331, "Ясеневый лес"], + [null, "Запределье"], + [3520, "Долина Призрачной Луны"], [3521, "Зангартопь"], [3519, "Лес Тероккар"], [3518, "Награнд"], [3522, "Острогорье"], + [3483, "Полуостров Адского Пламени"], [3523, "Пустоверть"], [3703, "Шаттрат"], + [null, "Нордскол"], + [3537, "Борейская тундра"], [ 67, "Грозовая Гряда"], [4395, "Даларан"], [ 65, "Драконий Погост"], [ 66, "Зул'Драк"], + [4742, "Лагерь Хротгара"], [ 210, "Ледяная Корона"], [2817, "Лес Хрустальной Песни"], [3711, "Низина Шолазар"], [4197, "Озеро Ледяных Оков"], + [ 495, "Ревущий фьорд"], [ 394, "Седые холмы"], + [null, "Подземелья"], + [4277, "Азжол-Неруб"], [4415, "Аметистовая крепость"], [4494, "Ан'кахет: Старое Королевство"], [3848, "Аркатрац"], [3790, "Аукенайские гробницы"], + [3562, "Бастионы Адского Пламени"], [3847, "Ботаника"], [1196, "Вершина Утгард"], [1584, "Глубины Черной горы"], [ 721, "Гномреган"], + [3792, "Гробницы маны"], [4416, "Гундрак"], [2557, "Забытый Город"], [4820, "Залы Отражений"], [1477, "Затонувший храм"], + [1176, "Зул'Фаррак"], [4723, "Испытание чемпиона"], [4196, "Крепость Драк'Тарон"], [ 209, "Крепость Темного Клыка"], [ 206, "Крепость Утгард"], + [4809, "Кузня Душ"], [3713, "Кузня Крови"], [ 722, "Курганы Иглошкурых"], [ 491, "Лабиринты Иглошкурых"], [2100, "Мародон"], + [1581, "Мертвые копи"], [3849, "Механар"], [ 796, "Монастырь Алого ордена"], [2057, "Некроситет"], [4265, "Нексус"], + [ 719, "Непроглядная Пучина"], [3716, "Нижетопь"], [2437, "Огненная пропасть"], [4228, "Окулус"], [4100, "Очищение Стратхольма"], + [3715, "Паровое Подземелье"], [ 718, "Пещеры Стенаний"], [1583, "Пик Черной горы"], [3717, "Узилище"], [3714, "Разрушенные залы"], + [3791, "Сетеккские залы"], [2367, "Старые предгорья Хилсбрада"], [2017, "Стратхольм"], [3789, "Темный лабиринт"], [4131, "Терраса Магистров"], + [ 717, "Тюрьма Штормграда"], [1337, "Ульдаман"], [2366, "Черные топи"], [4264, "Чертоги Камня"], [4272, "Чертоги Молний"], + [4813, "Яма Сарона"], + [null, "Рейды"], + [3606, "Битва за гору Хиджал"], [3607, "Змеиное святилище"], [3805, "Зул'Аман"], [1977, "Зул'Гуруб"], [4722, "Испытание крестоносца"], + [3457, "Каражан"], [3845, "Крепость Бурь"], [3923, "Логово Груула"], [2677, "Логово Крыла Тьмы"], [3836, "Логово Магтеридона"], + [2159, "Логово Ониксии"], [3456, "Наксрамас"], [4493, "Обсидиановое святилище"], [2717, "Огненные Недра"], [4500, "Око вечности"], + [4987, "Рубиновое святилище"], [3429, "Руины Ан'Киража"], [4603, "Склеп Аркавона"], [4075, "Солнечный Колодец"], [4273, "Ульдуар"], + [3428, "Храм Ан'Киража"], [4812, "Цитадель Ледяной Короны"], [3959, "Черный храм"], + [null, "Поля боя"], + [2597, "Альтеракская долина"], [4384, "Берег Древних"], [3358, "Низина Арати"], [3820, "Око Бури"], [4710, "Остров Завоеваний"], + [3277, "Ущелье Песни Войны"], + [null, "Арены"], + [4378, "Арена Даларана"], [4406, "Арена Доблести"], [3698, "Арена Награнда"], [3702, "Арена Острогорья"], [3968, "Руины Лордерона"] ], resistance: [ [6, "Тайная магия"], [2, "Огонь"], [3, "природа"], [4, "Лед"], [5, "Тьма"], @@ -3712,68 +3730,85 @@ var LANG = { ], pvp: [ [1, "Да"], [3, "Арена"], [4, "Поле боя"], [5, "Игровой мир"], [2, "Нет"] ], currency: [ - [32572, "Апекситовый кристалл"], [32569, "Апекситовый осколок"], [44128, "Арктический мех"], [26045, "Боевой знак Халаа"], [43016, "Даларанская кулинарная награда"], - [41596, "Даларанский знак ювелира"], [32897, "Знак Иллидари"], [52027, "Знак освящения завоевателя"], [52030, "Знак освящения завоевателя (Героический)"], - [52026, "Знак освящения защитника"], [52029, "Знак освящения защитника (Героический)"], [52025, "Знак освящения покорителя"], [52028, "Знак освящения покорителя (Героический)"], - [29434, "Знак справедливости"], [26044, "Исследовательский знак Халаа"],[34597, "Моллюск Зимних Плавников"], [37836, "Монета Торговой Компании"], [22484, "Некротическая руна"], - [24245, "Огнешляпка"], [24368, "Оружие Змеиного Зуба"], [34052, "Осколок грез"], [28558, "Осколок духа"], [43228, "Осколок каменного хранителя"], - [24581, "Печать Траллмара"], [44990, "Печать чемпиона"], [20560, "Почетный знак Альтеракской долины"], [20559, "Почетный знак Низины Арати"], - [43589, "Почетный знак Озера Ледяных Оков"], [29024, "Почетный знак Ока Бури"], [24579, "Почетный знак Оплота Чести"], [20558, "Почетный знак Ущелья Песни Войны"], - [37829, "Призовой жетон Хмельного фестиваля"], [23247, "Пылающий цвет"], [29735, "Святая пыль"], [38425, "Толстая борейская кожа"], - [47242, "Трофей Авангарда"], [29736, "Чародейские руны"], [34664, "Частица солнца"], [4291, "Шелковая нить"], [40752, "Эмблема героизма"], - [40753, "Эмблема доблести"], [45624, "Эмблема завоевания"], [49426, "Эмблема льда"], [47241, "Эмблема триумфа"] - - + [null, "World of Warcraft"], + [37829, "Призовой жетон Хмельного фестиваля"], [23247, "Пылающий цвет"], [4291, "Шелковая нить"], + [null, "Burning Crusade"], + [32572, "Апекситовый кристалл"], [32569, "Апекситовый осколок"], [32897, "Знак Иллидари"], + [22484, "Некротическая руна"], [24245, "Огнешляпка"], [24368, "Оружие Кривого Клыка"], + [29735, "Святая пыль"], [29736, "Чародейская руна"], [34664, "Частица солнца"], + [null, "Wrath of the Lich King"], + [44128, "Арктический мех"], [43016, "Даларанская кулинарная награда"], [41596, "Даларанский знак ювелира"], + [34597, "Моллюск Зимних Плавников"], [34052, "Осколок грез"], [44990, "Печать чемпиона"], + [38425, "Толстая борейская кожа"], + [null, "PvP"], + [26045, "Боевой знак Халаа"], [26044, "Исследовательский знак Халаа"], [37836, "Монета Торговой Компании"], + [28558, "Осколок духа"], [43228, "Осколок каменного хранителя"], [24581, "Печать Траллмара"], + [20560, "Почетный знак Альтеракской долины"], [20559, "Почетный знак Низины Арати"], [43589, "Почетный знак Озера Ледяных Оков"], + [29024, "Почетный знак Ока Бури"], [24579, "Почетный знак Оплота Чести"], [20558, "Почетный знак Ущелья Песни Войны"], + [null, "Подземелья и рейды"], + [29434, "Знак справедливости"], [40752, "Эмблема героизма"], [40753, "Эмблема доблести"], + [45624, "Эмблема завоевания"], [49426, "Эмблема льда"], [47241, "Эмблема триумфа"] ], itemcurrency: [ - [34342, "Боевые рукавицы рассвета"], [34169, "Брюки Природной ярости"], [34212, "Жилет Солнечного сияния"], - [34234, "Затемненные перчатки пароксизма"], [40625, "Кираса пропавшего завоевателя"], [40626, "Кираса пропавшего защитника"], - [40627, "Кираса пропавшего покорителя"], [45632, "Кираса своенравного завоевателя"], [45633, "Кираса своенравного защитника"], - [45634, "Кираса своенравного покорителя"], [34186, "Клепаные кольчужные поножи Яростной Бури"], [34332, "Клобук Гул'дана"], - [34339, "Клобук Чистейшего света"], [34345, "Корона Анастериана"], [40631, "Корона пропавшего завоевателя"], - [40632, "Корона пропавшего защитника"], [40633, "Корона пропавшего покорителя"], [45638, "Корона своенравного завоевателя"], - [45639, "Корона своенравного защитника"], [45640, "Корона своенравного покорителя"], [34211, "Куртка Основного инстинкта"], - [34170, "Кюлоты Затишья в бою"], [45656, "Мантия своенравного завоевателя"], [45657, "Мантия своенравного защитника"], - [45658, "Мантия своенравного покорителя"], [34244, "Маска Двуличия"], [34216, "Нагрудный доспех Героического ревнителя"], - [34215, "Нагрудный доспех безрассудной ярости"], [31089, "Нагрудный доспех забытого завоевателя"], [31091, "Нагрудный доспех забытого защитника"], - [31090, "Нагрудный доспех забытого покорителя"], [29754, "Нагрудный доспех павшего воителя"], [29755, "Нагрудный доспех павшего героя"], - [29753, "Нагрудный доспех павшего заступника"], [30238, "Нагрудный доспех поверженного героя"], [30237, "Нагрудный доспех поверженного заступника"], - [30236, "Нагрудный доспех поверженного защитника"], [40610, "Нагрудный доспех пропавшего завоевателя"], [40611, "Нагрудный доспех пропавшего защитника"], - [40612, "Нагрудный доспех пропавшего покорителя"], [45635, "Нагрудный доспех своенравного завоевателя"], [45636, "Нагрудный доспех своенравного защитника"], - [45637, "Нагрудный доспех своенравного покорителя"], [34209, "Наплеч Освоения"], [40623, "Наплеч Пропавшего защитника"], - [34193, "Наплеч Талассийского Спасителя"], [34202, "Наплечная накидка Изумления"], [34195, "Наплечные пластины горячности"], - [40622, "Наплеч пропавшего завоевателя"], [40624, "Наплеч пропавшего покорителя"], [45659, "Наплеч своенравного завоевателя"], - [45660, "Наплеч своенравного защитника"], [45661, "Наплеч своенравного покорителя"], [34192, "Наплечье Настойчивости"], - [31101, "Наплечье забытого завоевателя"], [31103, "Наплечье забытого защитника"], [31102, "Наплечье забытого покорителя"], - [29763, "Наплечье павшего воителя"], [29762, "Наплечье павшего героя"], [29764, "Наплечье павшего заступника"], - [30248, "Наплечье поверженного Защитника"], [30250, "Наплечье поверженного героя"], [30249, "Наплечье поверженного заступника"], - [34848, "Наручи забытого завоевателя"], [34851, "Наручи забытого защитника"], [34852, "Наручи забытого покорителя"], - [40634, "Ножные латы пропавшего завоевателя"], [40635, "Ножные латы пропавшего защитника"], [40636, "Ножные латы пропавшего покорителя"], + [null, "Рейдовый комплект Tier 10"], + [52027, "Знак освящения завоевателя"], [52026, "Знак освящения защитника"], [52025, "Знак освящения покорителя"], + [52030, "Знак освящения завоевателя (Героический)"], [52029, "Знак освящения защитника (Героический)"], [52028, "Знак освящения покорителя (Героический)"], + [null, "Рейдовый комплект Tier 9"], + [47242, "Трофей Авангарда"], + [47557, "Регалии великого завоевателя"], [47558, "Регалии великого защитника"], [47559, "Регалии великого покорителя"], + [null, "Рейдовый комплект Tier 8"], + [45632, "Кираса своенравного завоевателя"], [45633, "Кираса своенравного защитника"], [45634, "Кираса своенравного покорителя"], + [45638, "Корона своенравного завоевателя"], [45639, "Корона своенравного защитника"], [45640, "Корона своенравного покорителя"], + [45656, "Мантия своенравного завоевателя"], [45657, "Мантия своенравного защитника"], [45658, "Мантия своенравного покорителя"], + [45635, "Нагрудный доспех своенравного завоевателя"], [45636, "Нагрудный доспех своенравного защитника"], [45637, "Нагрудный доспех своенравного покорителя"], + [45659, "Наплеч своенравного завоевателя"], [45660, "Наплеч своенравного защитника"], [45661, "Наплеч своенравного покорителя"], [45653, "Ножные латы своенравного завоевателя"], [45654, "Ножные латы своенравного защитника"], [45655, "Ножные латы своенравного покорителя"], - [34180, "Ножные латы ярости Скверны"], [34229, "Облачение Безмятежных берегов"], [34233, "Одеяния неверного света"], - [40637, "Оплечье пропавшего завоевателя"], [40638, "Оплечье пропавшего защитника"], [40639, "Оплечье пропавшего покорителя"], - [31092, "Перчатки забытого завоевателя"], [31094, "Перчатки забытого защитника"], [31093, "Перчатки забытого покорителя"], - [29757, "Перчатки павшего воителя"], [29756, "Перчатки павшего героя"], [29758, "Перчатки павшего заступника"], - [30239, "Перчатки поверженного Защитника"], [30241, "Перчатки поверженного героя"], [30240, "Перчатки поверженного заступника"], - [40613, "Перчатки пропавшего завоевателя"], [40614, "Перчатки пропавшего защитника"], [40615, "Перчатки пропавшего покорителя"], [45644, "Перчатки своенравного завоевателя"], [45645, "Перчатки своенравного защитника"], [45646, "Перчатки своенравного покорителя"], - [34351, "Повязки Спокойного величия"], [34245, "Покров Урсола Мудрого"], [40620, "Поножи Пропавшего защитника"], - [31098, "Поножи забытого завоевателя"], [31100, "Поножи забытого защитника"], [31099, "Поножи забытого покорителя"], - [29766, "Поножи павшего воителя"], [29765, "Поножи павшего героя"], [29767, "Поножи павшего заступника"], - [30247, "Поножи поверженного героя"], [30246, "Поножи поверженного заступника"], [30245, "Поножи поверженного защитника"], - [40619, "Поножи пропавшего завоевателя"], [40621, "Поножи пропавшего покорителя"], [45650, "Поножи своенравного завоевателя"], - [45651, "Поножи своенравного защитника"], [45652, "Поножи своенравного покорителя"], [34853, "Пояс забытого завоевателя"], - [34854, "Пояс забытого защитника"], [34855, "Пояс забытого покорителя"], [47557, "Регалии великого завоевателя"], - [47558, "Регалии великого защитника"], [47559, "Регалии великого покорителя"], [34350, "Рукавицы предков Призрачной Луны"], - [40628, "Рукавицы пропавшего завоевателя"], [40629, "Рукавицы пропавшего защитника"], [40630, "Рукавицы пропавшего покорителя"], + [45650, "Поножи своенравного завоевателя"], [45651, "Поножи своенравного защитника"], [45652, "Поножи своенравного покорителя"], [45641, "Рукавицы своенравного завоевателя"], [45642, "Рукавицы своенравного защитника"], [45643, "Рукавицы своенравного покорителя"], + [45647, "Шлем своенравного завоевателя"], [45648, "Шлем своенравного защитника"], [45649, "Шлем своенравного покорителя"], + [null, "Рейдовый комплект Tier 7"], + [40625, "Кираса пропавшего завоевателя"], [40626, "Кираса пропавшего защитника"], [40627, "Кираса пропавшего покорителя"], + [40631, "Корона пропавшего завоевателя"], [40632, "Корона пропавшего защитника"], [40633, "Корона пропавшего покорителя"], + [40610, "Нагрудный доспех пропавшего завоевателя"], [40611, "Нагрудный доспех пропавшего защитника"], [40612, "Нагрудный доспех пропавшего покорителя"], + [40622, "Наплеч пропавшего завоевателя"], [40623, "Наплеч Пропавшего защитника"], [40624, "Наплеч пропавшего покорителя"], + [40634, "Ножные латы пропавшего завоевателя"], [40635, "Ножные латы пропавшего защитника"], [40636, "Ножные латы пропавшего покорителя"], + [40637, "Оплечье пропавшего завоевателя"], [40638, "Оплечье пропавшего защитника"], [40639, "Оплечье пропавшего покорителя"], + [40613, "Перчатки пропавшего завоевателя"], [40614, "Перчатки пропавшего защитника"], [40615, "Перчатки пропавшего покорителя"], + [40619, "Поножи пропавшего завоевателя"], [40620, "Поножи Пропавшего защитника"], [40621, "Поножи пропавшего покорителя"], + [40628, "Рукавицы пропавшего завоевателя"], [40629, "Рукавицы пропавшего защитника"], [40630, "Рукавицы пропавшего покорителя"], + [40616, "Шлем пропавшего завоевателя"], [40617, "Шлем пропавшего защитника"], [40618, "Шлем пропавшего покорителя"], + [null, "Рейдовый комплект Tier 6"], + [31089, "Нагрудный доспех забытого завоевателя"], [31091, "Нагрудный доспех забытого защитника"], [31090, "Нагрудный доспех забытого покорителя"], + [31101, "Наплечье забытого завоевателя"], [31103, "Наплечье забытого защитника"], [31102, "Наплечье забытого покорителя"], + [34848, "Наручи забытого завоевателя"], [34851, "Наручи забытого защитника"], [34852, "Наручи забытого покорителя"], + [31092, "Перчатки забытого завоевателя"], [31094, "Перчатки забытого защитника"], [31093, "Перчатки забытого покорителя"], + [31098, "Поножи забытого завоевателя"], [31100, "Поножи забытого защитника"], [31099, "Поножи забытого покорителя"], + [34853, "Пояс забытого завоевателя"], [34854, "Пояс забытого защитника"], [34855, "Пояс забытого покорителя"], [34856, "Сапоги забытого завоевателя"], [34857, "Сапоги забытого защитника"], [34858, "Сапоги забытого покорителя"], - [34167, "Священные ножные латы владыки мира"], [31097, "Шлем забытого завоевателя"], [31095, "Шлем забытого защитника"], - [31096, "Шлем забытого покорителя"], [34243, "Шлем обжигающей праведности"], [29760, "Шлем павшего воителя"], - [29759, "Шлем павшего героя"], [29761, "Шлем павшего заступника"], [30244, "Шлем поверженного героя"], - [30243, "Шлем поверженного заступника"], [30242, "Шлем поверженного защитника"], [40616, "Шлем пропавшего завоевателя"], - [40617, "Шлем пропавшего защитника"], [40618, "Шлем пропавшего покорителя"], [45647, "Шлем своенравного завоевателя"], - [45648, "Шлем своенравного защитника"], [45649, "Шлем своенравного покорителя"], [34208, "Эполеты равновесия"], + [31097, "Шлем забытого завоевателя"], [31095, "Шлем забытого защитника"], [31096, "Шлем забытого покорителя"], + [null, "Рейдовый комплект Tier 5"], + [30238, "Нагрудный доспех поверженного героя"], [30237, "Нагрудный доспех поверженного заступника"], [30236, "Нагрудный доспех поверженного защитника"], + [30250, "Наплечье поверженного героя"], [30249, "Наплечье поверженного заступника"], [30248, "Наплечье поверженного Защитника"], + [30241, "Перчатки поверженного героя"], [30240, "Перчатки поверженного заступника"], [30239, "Перчатки поверженного Защитника"], + [30247, "Поножи поверженного героя"], [30246, "Поножи поверженного заступника"], [30245, "Поножи поверженного защитника"], + [30244, "Шлем поверженного героя"], [30243, "Шлем поверженного заступника"], [30242, "Шлем поверженного защитника"], + [null, "Рейдовый комплект Tier 4"], + [29754, "Нагрудный доспех павшего воителя"], [29755, "Нагрудный доспех павшего героя"], [29753, "Нагрудный доспех павшего заступника"], + [29763, "Наплечье павшего воителя"], [29762, "Наплечье павшего героя"], [29764, "Наплечье павшего заступника"], + [29757, "Перчатки павшего воителя"], [29756, "Перчатки павшего героя"], [29758, "Перчатки павшего заступника"], + [29766, "Поножи павшего воителя"], [29765, "Поножи павшего героя"], [29767, "Поножи павшего заступника"], + [29760, "Шлем павшего воителя"], [29759, "Шлем павшего героя"], [29761, "Шлем павшего заступника"], + [null, "Разное"], + [34342, "Боевые рукавицы рассвета"], [34169, "Брюки природной ярости"], [34212, "Жилет солнечного сияния"], + [34234, "Затемненные перчатки пароксизма"], [34186, "Клепаные кольчужные поножи яростной бури"], [34332, "Клобук Гул'дана"], + [34339, "Клобук чистейшего света"], [34345, "Корона Анастериана"], [34211, "Куртка основного инстинкта"], + [34170, "Кюлоты затишья в бою"], [34244, "Маска Двуличия"], [34215, "Нагрудный доспех безрассудной ярости"], + [34216, "Нагрудный доспех героического ревнителя"], [34209, "Наплеч освоения"], [34193, "Наплеч Талассийского Спасителя"], + [34202, "Наплечная накидка изумления"], [34195, "Наплечные пластины горячности"], [34192, "Наплечье настойчивости"], + [34180, "Ножные латы ярости Скверны"], [34229, "Облачение безмятежных берегов"], [34233, "Одеяния неверного света"], + [34351, "Повязки спокойного величия"], [34245, "Покров Урсола Мудрого"], [34350, "Рукавицы предков Призрачной Луны"], + [34167, "Священные ножные латы слепого поклонения"], [34243, "Шлем обжигающей праведности"], [34208, "Эполеты равновесия"] ], queststart: [ [3, "Предмет"], [1, "НИП"], [2, "Объект"] ], questend: [ [1, "НИП"], [2, "Объект"] ], @@ -3820,7 +3855,7 @@ var LANG = { [31, "Increase Run Speed %"], [32, "Mod Mounted Speed %"], [33, "Decrease Run Speed %"], [34, "Mod Maximum Health - Flat"], [35, "Mod Maximum Power - Flat"], [36, "Shapeshift"], [37, "Spell Effect Immunity"], [38, "Spell Aura Immunity"], [39, "Spell School Immunity"], [40, "Damage Immunity"], [41, "Dispel Type Immunity"], [42, "Proc Trigger Spell"], [43, "Proc Trigger Damage"], [44, "Track Creatures"], [45, "Track Resources"], - [46, "Ignore All Gear"], [47, "Mod Parry %"], /* [48, null] */ [49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], + [46, "Ignore All Gear"], [47, "Mod Parry %"], [48, "Periodic Trigger Spell from Client"],[49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], [51, "Mod Block %"], [52, "Mod Physical Crit Chance"], [53, "Periodically Drain Health"], [54, "Mod Physical Hit Chance"], [55, "Mod Spell Hit Chance"], [56, "Transform"], [57, "Mod Spell Crit Chance"], [58, "Increase Swim Speed %"], [59, "Mod Damage Done Versus Creature"],[60, "Pacify & Silence"], [61, "Mod Size %"], [62, "Periodically Transfer Health"], [63, "Periodically Transfer Power"], [64, "Periodically Drain Power"], [65, "Mod Spell Haste % (not stacking)"], @@ -3902,11 +3937,10 @@ var LANG = { [131, "Play Sound"], [132, "Play Music"], [133, "Unlearn Specialization"], [134, "Kill Credit2"], [135, "Call Pet"], [136, "Heal for % of Total Health"], [137, "Give % of Total Power"], [138, "Leap Back"], [139, "Abandon Quest"], [140, "Force Spell Cast"], [141, "Force Spell Cast with Value"], [142, "Trigger Spell with Value"], [143, "Apply Area Aura - Pet Owner"], [144, "Knockback to Dest."], [145, "Pull Towards Dest."], - [146, "Activate Rune"], [147, "Fail Quest"], [149, "Charge to Dest."], [150, "Start Quest"], + [146, "Activate Rune"], [147, "Fail Quest"], [148, "Trigger Missile with Value"], [149, "Charge to Dest."], [150, "Start Quest"], [151, "Trigger Spell 2"], [152, "Summon - Refer-A-Friend"], [153, "Create Tamed Pet"], [154, "Discover Flight Path"], [155, "Dual Wield 2H Weapons"], - [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], - [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"], - [167, "Update Player Phase"] + [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], [160, "Force Cast 2"], + [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"] ], damagetype: [ [1, "Нет"], [2, "Магический"], [3, "Ближний бой"], [4, "Дальний бой"] @@ -4132,6 +4166,7 @@ var LANG = { id: "Номер", classspecific: "Только для класса", racespecific: "Расовый", + setspvpflag: "Включает доступность PvP", sepgainsrewards: "Награда за выполнение", experiencegained: "Получаемый опыт", @@ -4904,10 +4939,18 @@ var LANG = { /* AoWoW: start custom */ + // Profiler completions import + completion: 'Завершено', // WH.TERMS.completion + complete: 'Завершено', // WH.TERMS.complete + incomplete: 'Не завершено', // WH.TERMS.incomplete + parens_format: '$1 ($2)', // WH.TERMS.parens_format + // click to copy fn copied: 'Скопировано', clickToCopy: 'Нажмите, чтобы скопировать', nothingToCopy_tip: 'Нет данных для копирования!', + copy_clipboard: 'Скопировать', + copy_format: 'Скопировать %s', // TC conditions display tab_conditions: '[Conditions]', diff --git a/static/js/locale_zhcn.js b/static/js/locale_zhcn.js index 7399cae5..09016afa 100644 --- a/static/js/locale_zhcn.js +++ b/static/js/locale_zhcn.js @@ -1577,6 +1577,7 @@ var g_quest_sorts = { "-374": '复活节', "-375": '感恩节', "-376": '情人节', + 0: '未分类', 1: '丹莫罗', 3: '荒芜之地', 4: '诅咒之地', @@ -2567,7 +2568,8 @@ var g_conditions = { 47: 'The Player has$N: not; $2 [quest=$1]', 48: 'The Player has$N: not; collected $3 towards objective #$2 of [quest=$1]', 49: 'The current map difficulty is #$1', - 50: 'The Player $C$1$Ncan:can\'t; be:is$N: not;; a Gamemaster' + 50: 'The Player $C$1$Ncan:can\'t; be:is$N: not;; a Gamemaster', + 58: 'The StringID of this entity is$N: not; $4' }; /* end aowow custom */ @@ -2863,7 +2865,7 @@ var LANG = { lvnote_sort: "分类:", - lvnote_tryfiltering: "试试筛选您的结果", + lvnote_tryfiltering: "试试<a>筛选</a>您的结果", lvnote_trynarrowing: "请尝试缩小搜索范围", lvnote_upgradesfor: '寻找升级 <a href="?item=$1" class="q$2"><b>$3</b></a>.', lvnote_witherrors: "您搜索中的部分筛选词无效,已被忽略。", @@ -3145,6 +3147,8 @@ var LANG = { tooltip_uprate: "犀利/有趣", tooltip_zonelink: "点击此链接就能够<br />跳转到地区页。", + tooltip_immune: "免疫", // aowow - custom + reputationhistory: "声望历史", reputationaction: "声望行动", @@ -3661,59 +3665,73 @@ var LANG = { num: [ [1, ">"], [2, ">="], [3, "="], [4, "<="], [5, "<"], [6, "!="] ], side: [ [1, "是"], [2, "联盟"], [3, "部落"], [4, "双方"], [5, "无"] ], faction: [ - [21, "藏宝海湾"], [47, "铁炉堡"], [54, "诺莫瑞根流亡者"], [59, "瑟银兄弟会"], [67, "部落"], - [68, "幽暗城"], [69, "达纳苏斯"], [70, "辛迪加"], [72, "暴风城"], [76, "奥格瑞玛"], - [81, "雷霆崖"], [87, "血帆海盗"], [92, "吉尔吉斯半人马"], [93, "玛格拉姆半人马"], [169, "热砂港"], - [270, "赞达拉部族"], [349, "拉文霍德"], [369, "加基森"], [469, "联盟"], [470, "棘齿城"], - [509, "阿拉索联军"], [510, "污染者"], [529, "银色黎明"], [530, "暗矛巨魔"], [576, "木喉要塞"], - [577, "永望镇"], [589, "冬刃豹训练师"], [609, "塞纳里奥议会"], [729, "霜狼氏族"], [730, "雷矛卫队"], - [749, "海达希亚水元素"], [809, "辛德拉"], [889, "战歌氏族"], [890, "银翼要塞"], [909, "暗月马戏团"], - [910, "诺兹多姆的子嗣"], [911, "银月城"], [922, "塔奎林"], [930, "埃索达"], [932, "奥尔多"], - [933, "星界财团"], [934, "占星者"], [935, "沙塔尔"], [941, "玛格汉"], [942, "塞纳里奥远征队"], - [946, "荣耀堡"], [947, "萨尔玛"], [967, "紫罗兰之眼"], [970, "孢子村"], [978, "库雷尼"], - [989, "时光守护者"], [990, "流沙之鳞"], [1011, "贫民窟"], [1012, "灰舌死誓者"], [1015, "灵翼之龙"], - [1031, "沙塔尔天空卫队"], [1037, "联盟先遣军"], [1038, "奥格瑞拉"], [1050, "无畏远征军"], [1052, "部落先遣军"], - [1064, "牦牛人"], [1067, "复仇之手"], [1068, "探险者协会"], [1073, "卡鲁亚克"], [1077, "破碎残阳"], - [1085, "战歌远征军"], [1090, "肯瑞托"], [1091, "龙眠联军"], [1094, "银色盟约"], [1098, "黑锋骑士团"], - [1104, "狂心氏族"], [1105, "神谕者"], [1106, "银色北伐军"], [1119, "霍迪尔之子"], [1124, "夺日者"], - [1126, "霜脉矮人"], [1156, "灰烬审判军"] + [null, "经典旧世"], + [ 369, "加基森"], [ 92, "吉尔吉斯半人马"], [ 930, "埃索达"], [ 922, "塔奎林"], [ 609, "塞纳里奥议会"], + [ 76, "奥格瑞玛"], [ 68, "幽暗城"], [ 889, "战歌氏族"], [ 349, "拉文霍德"], [ 909, "暗月马戏团"], + [ 530, "暗矛巨魔"], [ 72, "暴风城"], [ 576, "木喉要塞"], [ 470, "棘齿城"], [ 577, "永望镇"], + [ 510, "污染者"], [ 749, "海达希亚水元素"], [ 169, "热砂港"], [ 93, "玛格拉姆半人马"], [ 59, "瑟银兄弟会"], + [ 469, "联盟"], [ 21, "藏宝海湾"], [ 87, "血帆海盗"], [ 910, "诺兹多姆的子嗣"], [ 54, "诺莫瑞根流亡者"], + [ 270, "赞达拉部族"], [ 809, "辛德拉"], [ 69, "达纳苏斯"], [ 67, "部落"], [ 47, "铁炉堡"], + [ 911, "银月城"], [ 890, "银翼要塞"], [ 529, "银色黎明"], [ 509, "阿拉索联军"], [ 730, "雷矛卫队"], + [ 81, "雷霆崖"], [ 729, "霜狼氏族"], + [null, "燃烧的远征"], + [ 934, "占星者"], [ 942, "塞纳里奥远征队"], [ 932, "奥尔多"], [1038, "奥格瑞拉"], [ 970, "孢子村"], + [ 978, "库雷尼"], [ 989, "时光守护者"], [ 933, "星界财团"], [ 935, "沙塔尔"], [1031, "沙塔尔天空卫队"], + [ 990, "流沙之鳞"], [1012, "灰舌死誓者"], [1015, "灵翼之龙"], [ 941, "玛格汉"], [1077, "破碎残阳"], + [ 967, "紫罗兰之眼"], [ 946, "荣耀堡"], [ 947, "萨尔玛"], [1011, "贫民窟"], + [null, "巫妖王之怒"], + [1073, "卡鲁亚克"], [1067, "复仇之手"], [1124, "夺日者"], [1085, "战歌远征军"], [1068, "探险者协会"], + [1050, "无畏远征军"], [1156, "灰烬审判军"], [1064, "牦牛人"], [1104, "狂心氏族"], [1105, "神谕者"], + [1037, "联盟先遣军"], [1090, "肯瑞托"], [1052, "部落先遣军"], [1106, "银色北伐军"], [1094, "银色盟约"], + [1119, "霍迪尔之子"], [1126, "霜脉矮人"], [1098, "黑锋骑士团"], [1091, "龙眠联军"], + [null, "其他"], + [ 589, "冬刃豹训练师"], [ 70, "辛迪加"] ], zone: [ - [1, "丹莫罗"], [3, "荒芜之地"], [4, "诅咒之地"], [8, "悲伤沼泽"], [10, "暮色森林"], - [11, "湿地"], [12, "艾尔文森林"], [14, "杜隆塔尔"], [15, "尘泥沼泽"], [16, "艾萨拉"], - [17, "贫瘠之地"], [25, "黑石山"], [28, "西瘟疫之地"], [33, "荆棘谷"], [36, "奥特兰克山脉"], - [38, "洛克莫丹"], [40, "西部荒野"], [41, "逆风小径"], [44, "赤脊山"], [45, "阿拉希高地"], - [46, "燃烧平原"], [47, "辛特兰"], [51, "灼热峡谷"], [65, "龙骨荒野"], [66, "祖达克"], - [67, "风暴峭壁"], [85, "提瑞斯法林地"], [130, "银松森林"], [139, "东瘟疫之地"], [141, "泰达希尔"], - [148, "黑海岸"], [206, "乌特加德城堡"], [209, "影牙城堡"], [210, "冰冠冰川"], [215, "莫高雷"], - [267, "希尔斯布莱德丘陵"], [331, "灰谷"], [357, "菲拉斯"], [361, "费伍德森林"], [393, "暗矛海滩"], - [394, "灰熊丘陵"], [400, "千针石林"], [405, "凄凉之地"], [406, "石爪山脉"], [440, "塔纳利斯"], - [457, "迷雾之海"], [490, "安戈洛环形山"], [491, "剃刀沼泽"], [493, "月光林地"], [495, "嚎风峡湾"], - [618, "冬泉谷"], [717, "监狱"], [718, "哀嚎洞穴"], [719, "黑暗深渊"], [721, "诺莫瑞根"], - [722, "剃刀高地"], [796, "血色修道院"], [1176, "祖尔法拉克"], [1196, "乌特加德之巅"], [1216, "木喉要塞"], - [1337, "奥达曼"], [1377, "希利苏斯"], [1477, "阿塔哈卡神庙"], [1497, "幽暗城"], [1519, "暴风城"], - [1537, "铁炉堡"], [1581, "死亡矿井"], [1583, "黑石塔"], [1584, "黑石深渊"], [1637, "奥格瑞玛"], - [1638, "雷霆崖"], [1657, "达纳苏斯"], [1941, "时光之穴"], [1977, "祖尔格拉布"], [2017, "斯坦索姆"], - [2057, "通灵学院"], [2100, "玛拉顿"], [2159, "奥妮克希亚的巢穴"], [2257, "矿道地铁"], [2366, "黑色沼泽"], - [2367, "旧希尔斯布莱德丘陵"], [2437, "怒焰裂谷"], [2557, "厄运之槌"], [2597, "奥特兰克山谷"], [2677, "黑翼之巢"], - [2717, "熔火之心"], [2817, "晶歌森林"], [2917, "传说大厅"], [2918, "勇士大厅"], [3277, "战歌峡谷"], - [3358, "阿拉希盆地"], [3428, "安其拉"], [3429, "安其拉废墟"], [3430, "永歌森林"], [3433, "幽魂之地"], - [3456, "纳克萨玛斯"], [3457, "卡拉赞"], [3483, "地狱火半岛"], [3487, "银月城"], [3518, "纳格兰"], - [3519, "泰罗卡森林"], [3520, "影月谷"], [3521, "赞加沼泽"], [3522, "刀锋山"], [3523, "虚空风暴"], - [3524, "秘蓝岛"], [3525, "秘血岛"], [3537, "北风苔原"], [3557, "埃索达"], [3562, "地狱火城墙"], - [3606, "海加尔峰"], [3607, "毒蛇神殿"], [3679, "斯克提斯"], [3698, "纳格兰竞技场"], [3702, "刀锋山竞技场"], - [3703, "沙塔斯城"], [3711, "索拉查盆地"], [3713, "鲜血熔炉"], [3714, "破碎大厅"], [3715, "蒸汽地窟"], - [3716, "幽暗沼泽"], [3717, "奴隶围栏"], [3775, "鲜血之环"], [3789, "暗影迷宫"], [3790, "奥金尼地穴"], - [3791, "塞泰克大厅"], [3792, "法力陵墓"], [3805, "祖阿曼"], [3820, "风暴之眼"], [3836, "玛瑟里顿的巢穴"], - [3840, "黑暗神殿"], [3845, "风暴要塞"], [3847, "生态船"], [3848, "禁魔监狱"], [3849, "能源舰"], - [3905, "盘牙水库"], [3923, "格鲁尔的巢穴"], [3959, "黑暗神殿"], [3968, "洛丹伦废墟"], [4024, "考达拉"], - [4075, "太阳之井高地"], [4080, "奎尔丹纳斯岛"], [4100, "净化斯坦索姆"], [4131, "魔导师平台"], [4196, "达克萨隆要塞"], - [4197, "冬拥湖"], [4228, "魔环"], [4264, "岩石大厅"], [4265, "魔枢"], [4272, "闪电大厅"], - [4273, "奥杜尔"], [4277, "艾卓-尼鲁布"], [4298, "血色领地"], [4378, "达拉然竞技场"], [4384, "远古海滩"], - [4395, "达拉然"], [4406, "勇气竞技场"], [4415, "紫罗兰监狱"], [4416, "古达克"], [4493, "黑曜石圣殿"], - [4494, "安卡赫特:古代王国"], [4500, "永恒之眼"], [4603, "阿尔卡冯的宝库"], [4710, "征服之岛"], [4722, "十字军的试炼"], - [4723, "冠军的试炼"], [4742, "洛斯加尔登陆点"], [4809, "灵魂洪炉"], [4812, "冰冠堡垒"], [4813, "萨隆矿坑"], - [4820, "映像大厅"] + [null, "东部王国"], + [ 139, "东瘟疫之地"], [4298, "东瘟疫之地:血色领地"], [ 1, "丹莫罗"], [4080, "奎尔丹纳斯岛"], [ 36, "奥特兰克山脉"], + [ 267, "希尔斯布莱德丘陵"], [1497, "幽暗城"], [3433, "幽魂之地"], [ 8, "悲伤沼泽"], [ 85, "提瑞斯法林地"], + [ 10, "暮色森林"], [1519, "暴风城"], [3430, "永歌森林"], [ 38, "洛克莫丹"], [ 11, "湿地"], + [ 51, "灼热峡谷"], [ 46, "燃烧平原"], [2257, "矿道地铁"], [ 12, "艾尔文森林"], [ 33, "荆棘谷"], + [ 3, "荒芜之地"], [ 28, "西瘟疫之地"], [ 40, "西部荒野"], [ 4, "诅咒之地"], [ 44, "赤脊山"], + [ 47, "辛特兰"], [ 41, "逆风小径"], [1537, "铁炉堡"], [3487, "银月城"], [ 130, "银松森林"], + [ 45, "阿拉希高地"], + [null, "卡利姆多"], + [ 618, "冬泉谷"], [ 405, "凄凉之地"], [ 400, "千针石林"], [3557, "埃索达"], [ 440, "塔纳利斯"], + [1637, "奥格瑞玛"], [ 490, "安戈洛环形山"], [ 15, "尘泥沼泽"], [1377, "希利苏斯"], [ 493, "月光林地"], + [ 14, "杜隆塔尔"], [ 141, "泰达希尔"], [ 331, "灰谷"], [ 406, "石爪山脉"], [3524, "秘蓝岛"], + [3525, "秘血岛"], [ 16, "艾萨拉"], [ 215, "莫高雷"], [ 357, "菲拉斯"], [ 17, "贫瘠之地"], + [ 361, "费伍德森林"], [1657, "达纳苏斯"], [1638, "雷霆崖"], [ 148, "黑海岸"], + [null, "外域"], + [3522, "刀锋山"], [3483, "地狱火半岛"], [3520, "影月谷"], [3703, "沙塔斯城"], [3519, "泰罗卡森林"], + [3518, "纳格兰"], [3523, "虚空风暴"], [3521, "赞加沼泽"], + [null, "诺森德"], + [4197, "冬拥湖"], [ 210, "冰冠冰川"], [3537, "北风苔原"], [ 495, "嚎风峡湾"], [2817, "晶歌森林"], + [4742, "洛斯加尔登陆点"], [ 394, "灰熊丘陵"], [ 66, "祖达克"], [3711, "索拉查盆地"], [4395, "达拉然"], + [ 67, "风暴峭壁"], [ 65, "龙骨荒野"], + [null, "地下城"], + [1196, "乌特加德之巅"], [ 206, "乌特加德城堡"], [4723, "冠军的试炼"], [3717, "奴隶围栏"], [4100, "净化斯坦索姆"], + [ 491, "剃刀沼泽"], [ 722, "剃刀高地"], [2557, "厄运之槌"], [4416, "古达克"], [ 718, "哀嚎洞穴"], + [3562, "地狱火城墙"], [3791, "塞泰克大厅"], [1337, "奥达曼"], [3790, "奥金尼地穴"], [4494, "安卡赫特:古代王国"], + [4264, "岩石大厅"], [3716, "幽暗沼泽"], [ 209, "影牙城堡"], [2437, "怒焰裂谷"], [2017, "斯坦索姆"], + [2367, "旧希尔斯布莱德丘陵"], [4820, "映像大厅"], [3789, "暗影迷宫"], [ 717, "暴风城监狱"], [1581, "死亡矿井"], + [1477, "沉没的神庙"], [3792, "法力陵墓"], [4809, "灵魂洪炉"], [2100, "玛拉顿"], [3847, "生态船"], + [3714, "破碎大厅"], [1176, "祖尔法拉克"], [3848, "禁魔监狱"], [4415, "紫罗兰监狱"], [3849, "能源舰"], + [4277, "艾卓-尼鲁布"], [4813, "萨隆深渊"], [3715, "蒸汽地窟"], [ 796, "血色修道院"], [ 721, "诺莫瑞根"], + [4196, "达克萨隆要塞"], [2057, "通灵学院"], [4272, "闪电大厅"], [4131, "魔导师平台"], [4265, "魔枢"], + [4228, "魔环"], [3713, "鲜血熔炉"], [ 719, "黑暗深渊"], [1583, "黑石塔"], [1584, "黑石深渊"], + [2366, "黑色沼泽"], + [null, "团队副本"], + [4812, "冰冠堡垒"], [4722, "十字军的试炼"], [3457, "卡拉赞"], [4075, "太阳之井"], [2159, "奥妮克希亚的巢穴"], + [4273, "奥杜尔"], [3429, "安其拉废墟"], [3428, "安其拉神殿"], [3923, "格鲁尔的巢穴"], [3607, "毒蛇神殿"], + [4500, "永恒之眼"], [3606, "海加尔山之战"], [2717, "熔火之心"], [3836, "玛瑟里顿的巢穴"], [1977, "祖尔格拉布"], + [3805, "祖阿曼"], [4987, "红玉圣殿"], [3456, "纳克萨玛斯"], [4603, "阿尔卡冯的宝库"], [3845, "风暴要塞"], + [3959, "黑暗神殿"], [4493, "黑曜石圣殿"], [2677, "黑翼之巢"], + [null, "战场"], + [2597, "奥特兰克山谷"], [4710, "征服之岛"], [3277, "战歌峡谷"], [4384, "远古海滩"], [3358, "阿拉希盆地"], + [3820, "风暴之眼"], + [null, "竞技场"], + [3702, "刀锋山竞技场"], [4406, "勇气竞技场"], [3968, "洛丹伦废墟"], [3698, "纳格兰竞技场"], [4378, "达拉然竞技场"] ], resistance: [ [6, "奥术"], [2, "火焰"], [3, "自然"], [4, "冰霜"], [5, "暗影"], @@ -3760,46 +3778,79 @@ var LANG = { ], pvp: [ [1, "是"], [3, "竞技场"], [4, "战场"], [5, "世界"], [2, "无"] ], currency: [ - [4291, "丝线"], [20558, "战歌峡谷荣誉奖章"], [20559, "阿拉希盆地荣誉奖章"], [20560, "奥特兰克山谷荣誉奖章"], [22484, "死灵符文"], - [23247, "燃烧之花"], [24245, "亮顶蘑菇"], [24368, "盘牙武器"], [24579, "荣耀堡印记"], [24581, "萨尔玛印记"], - [26044, "哈兰研究勋章"], [26045, "哈兰作战勋章"], [28558, "幽魂碎片"], [29024, "风暴之眼荣誉奖章"], [29434, "公正徽章"], - [29735, "神圣之尘"], [29736, "奥术符文"], [32569, "埃匹希斯碎片"], [32572, "埃匹希斯水晶"], [32897, "伊利达雷印记"], - [34052, "梦境碎片"], [34597, "冬鳞蚌壳"], [34664, "太阳之尘"], [37829, "美酒节奖币"], [37836, "风险硬币"], - [38425, "厚北地皮"], [40752, "英雄纹章"], [40753, "勇气纹章"], [41596, "达拉然珠宝匠硬币"], [43016, "达拉然烹饪奖牌"], - [43228, "岩石守卫者的碎片"], [43589, "冬拥湖荣誉奖章"], [44128, "极地毛皮"], [44990, "冠军的徽记"], [45624, "征服纹章"], - [47241, "凯旋纹章"], [47242, "北伐奖章"], [49426, "寒冰纹章"], [52025, "胜利者的圣洁印记"], [52026, "保护者的圣洁徽记"], - [52027, "征服者的圣洁徽记"], [52028, "胜利者的圣洁印记"], [52029, "保护者的圣洁徽记"], [52030, "征服者的圣洁徽记"] + [null, "经典旧世"], + [4291, "丝线"], [23247, "燃烧之花"], [37829, "美酒节奖币"], + [null, "燃烧的远征"], + [24245, "亮顶蘑菇"], [32897, "伊利达雷印记"], [32572, "埃匹希斯水晶"], [32569, "埃匹希斯碎片"], [34664, "太阳之尘"], + [29736, "奥术符文"], [22484, "死灵符文"], [24368, "盘牙武器"], [29735, "神圣之尘"], + [null, "巫妖王之怒"], + [44990, "冠军的徽记"], [34597, "冬鳞蚌壳"], [38425, "厚北地皮"], [44128, "极地毛皮"], [34052, "梦境碎片"], + [43016, "达拉然烹饪奖牌"], [41596, "达拉然珠宝匠硬币"], + [null, "PvP"], + [43589, "冬拥湖荣誉奖章"], [26045, "哈兰作战勋章"], [26044, "哈兰研究勋章"], [20560, "奥特兰克山谷荣誉奖章"], [43228, "岩石守卫者的碎片"], + [28558, "幽魂碎片"], [20558, "战歌峡谷荣誉奖章"], [24579, "荣耀堡印记"], [24581, "萨尔玛印记"], [20559, "阿拉希盆地荣誉奖章"], + [29024, "风暴之眼荣誉奖章"], [37836, "风险硬币"], + [null, "地下城与团队"], + [29434, "公正徽章"], [47241, "凯旋纹章"], [40753, "勇气纹章"], [49426, "寒冰纹章"], [45624, "征服纹章"], + [40752, "英雄纹章"] ], itemcurrency: [ - [29753, "阵亡卫士的胸甲"], [29754, "阵亡勇士的胸甲"], [29755, "阵亡英雄的胸甲"], [29756, "阵亡英雄的手套"], [29757, "阵亡勇士的手套"], - [29758, "阵亡卫士的手套"], [29759, "阵亡英雄的头盔"], [29760, "阵亡勇士的头盔"], [29761, "阵亡卫士的头盔"], [29762, "阵亡英雄的护肩"], - [29763, "阵亡勇士的护肩"], [29764, "阵亡卫士的护肩"], [29765, "阵亡英雄的护腿"], [29766, "阵亡勇士的护腿"], [29767, "阵亡卫士的护腿"], - [30236, "战败勇士的胸甲"], [30237, "战败卫士的胸甲"], [30238, "战败英雄的胸甲"], [30239, "战败勇士的手套"], [30240, "战败卫士的手套"], - [30241, "战败英雄的手套"], [30242, "战败勇士的头盔"], [30243, "战败卫士的头盔"], [30244, "战败英雄的头盔"], [30245, "战败勇士的护腿"], - [30246, "战败卫士的护腿"], [30247, "战败英雄的护腿"], [30248, "战败勇士的护肩"], [30249, "战败卫士的护肩"], [30250, "战败英雄的护肩"], - [31089, "遗忘征服者的胸甲"], [31090, "遗忘胜利者的胸甲"], [31091, "遗忘保卫者的胸甲"], [31092, "遗忘征服者的手套"], [31093, "遗忘胜利者的手套"], - [31094, "遗忘保卫者的手套"], [31095, "遗忘保卫者的头盔"], [31096, "遗忘胜利者的头盔"], [31097, "遗忘征服者的头盔"], [31098, "遗忘征服者的护腿"], - [31099, "遗忘胜利者的护腿"], [31100, "遗忘保卫者的护腿"], [31101, "遗忘征服者的护肩"], [31102, "遗忘胜利者的护肩"], [31103, "遗忘保卫者的护肩"], - [34167, "神圣信仰腿铠"], [34169, "自然侵攻护腿"], [34170, "止战马裤"], [34180, "魔怒腿铠"], [34186, "雷暴锁链护腿"], - [34192, "坚定肩铠"], [34193, "萨拉斯救世主护肩"], [34195, "盛怒护肩"], [34202, "奇迹披肩"], [34208, "均衡肩饰"], - [34209, "拓荒护肩"], [34211, "野兽本能外套"], [34212, "阳光外衣"], [34215, "鲁莽怒火战甲"], [34216, "英勇仲裁者胸甲"], - [34229, "静谧海岸外衣"], [34233, "幻化光芒长袍"], [34234, "暗影爆发护手"], [34243, "燃烧的正义"], [34244, "狡诈之盔"], - [34245, "智者乌索尔之帽"], [34332, "古尔丹的兜帽"], [34339, "纯净圣光兜帽"], [34342, "黎明护手"], [34345, "安纳斯特里亚的王冠"], - [34350, "上古影月护手"], [34351, "稳固王权护手"], [34848, "遗忘征服者的护腕"], [34851, "遗忘保卫者的护腕"], [34852, "遗忘胜利者的护腕"], - [34853, "遗忘征服者的腰带"], [34854, "遗忘保卫者的腰带"], [34855, "遗忘胜利者的腰带"], [34856, "遗忘征服者的长靴"], [34857, "遗忘保卫者的长靴"], - [34858, "遗忘胜利者的长靴"], [40610, "失落征服者的护胸"], [40611, "失落保卫者的护胸"], [40612, "失落胜利者的护胸"], [40613, "失落征服者的手套"], - [40614, "失落保卫者的手套"], [40615, "失落胜利者的手套"], [40616, "失落征服者的头盔"], [40617, "失落保卫者的头盔"], [40618, "失落胜利者的头盔"], - [40619, "失落征服者的护腿"], [40620, "失落保卫者的护腿"], [40621, "失落胜利者的护腿"], [40622, "失落征服者的护肩"], [40623, "失落保卫者的护肩"], - [40624, "失落胜利者的护肩"], [40625, "失落征服者的胸甲"], [40626, "失落保卫者的胸甲"], [40627, "失落胜利者的胸甲"], [40628, "失落征服者的护手"], - [40629, "失落保卫者的护手"], [40630, "失落胜利者的护手"], [40631, "失落征服者的头冠"], [40632, "失落保卫者的头冠"], [40633, "失落胜利者的头冠"], - [40634, "失落征服者的腿甲"], [40635, "失落保卫者的腿甲"], [40636, "失落胜利者的腿甲"], [40637, "失落征服者的肩甲"], [40638, "失落保卫者的肩甲"], - [40639, "失落胜利者的肩甲"], [45632, "固执征服者的胸甲"], [45633, "固执保卫者的胸甲"], [45634, "固执胜利者的胸甲"], [45635, "固执征服者的护胸"], - [45636, "固执保卫者的护胸"], [45637, "固执胜利者的护胸"], [45638, "固执征服者的头冠"], [45639, "固执保卫者的头冠"], [45640, "固执胜利者的头冠"], - [45641, "固执征服者的护手"], [45642, "固执保卫者的护手"], [45643, "固执胜利者的护手"], [45644, "固执征服者的手套"], [45645, "固执保卫者的手套"], - [45646, "固执胜利者的手套"], [45647, "固执征服者的头盔"], [45648, "固执保卫者的头盔"], [45649, "固执胜利者的头盔"], [45650, "固执征服者的护腿"], - [45651, "固执保卫者的护腿"], [45652, "固执胜利者的护腿"], [45653, "固执征服者的腿甲"], [45654, "固执保卫者的腿甲"], [45655, "固执胜利者的腿甲"], - [45656, "固执征服者的衬肩"], [45657, "固执保卫者的衬肩"], [45658, "固执胜利者的衬肩"], [45659, "固执征服者的护肩"], [45660, "固执保卫者的护肩"], - [45661, "固执胜利者的护肩"], [47557, "无上征服者的圣装"], [47558, "无上保卫者的圣装"], [47559, "无上胜利者的圣装"] + [null, "T10副本套装"], + [52026, "保护者的圣洁徽记"], [52027, "征服者的圣洁徽记"], [52025, "胜利者的圣洁印记"], + [52029, "保护者的圣洁徽记(英雄)"], [52030, "征服者的圣洁徽记(英雄)"], [52028, "胜利者的圣洁印记(英雄)"], + [null, "T9副本套装"], + [47242, "北伐奖章"], + [47558, "无上保卫者的圣装"], [47557, "无上征服者的圣装"], [47559, "无上胜利者的圣装"], + [null, "T8副本套装"], + [45639, "固执保卫者的头冠"], [45648, "固执保卫者的头盔"], [45645, "固执保卫者的手套"], + [45642, "固执保卫者的护手"], [45660, "固执保卫者的护肩"], [45636, "固执保卫者的护胸"], + [45651, "固执保卫者的护腿"], [45633, "固执保卫者的胸甲"], [45654, "固执保卫者的腿甲"], + [45657, "固执保卫者的衬肩"], [45638, "固执征服者的头冠"], [45647, "固执征服者的头盔"], + [45644, "固执征服者的手套"], [45641, "固执征服者的护手"], [45659, "固执征服者的护肩"], + [45635, "固执征服者的护胸"], [45650, "固执征服者的护腿"], [45632, "固执征服者的胸甲"], + [45653, "固执征服者的腿甲"], [45656, "固执征服者的衬肩"], [45640, "固执胜利者的头冠"], + [45649, "固执胜利者的头盔"], [45646, "固执胜利者的手套"], [45643, "固执胜利者的护手"], + [45661, "固执胜利者的护肩"], [45637, "固执胜利者的护胸"], [45652, "固执胜利者的护腿"], + [45634, "固执胜利者的胸甲"], [45655, "固执胜利者的腿甲"], [45658, "固执胜利者的衬肩"], + [null, "T7副本套装"], + [40632, "失落保卫者的头冠"], [40617, "失落保卫者的头盔"], [40614, "失落保卫者的手套"], + [40629, "失落保卫者的护手"], [40623, "失落保卫者的护肩"], [40611, "失落保卫者的护胸"], + [40620, "失落保卫者的护腿"], [40638, "失落保卫者的肩甲"], [40626, "失落保卫者的胸甲"], + [40635, "失落保卫者的腿甲"], [40631, "失落征服者的头冠"], [40616, "失落征服者的头盔"], + [40613, "失落征服者的手套"], [40628, "失落征服者的护手"], [40622, "失落征服者的护肩"], + [40610, "失落征服者的护胸"], [40619, "失落征服者的护腿"], [40637, "失落征服者的肩甲"], + [40625, "失落征服者的胸甲"], [40634, "失落征服者的腿甲"], [40633, "失落胜利者的头冠"], + [40618, "失落胜利者的头盔"], [40615, "失落胜利者的手套"], [40630, "失落胜利者的护手"], + [40624, "失落胜利者的护肩"], [40612, "失落胜利者的护胸"], [40621, "失落胜利者的护腿"], + [40639, "失落胜利者的肩甲"], [40627, "失落胜利者的胸甲"], [40636, "失落胜利者的腿甲"], + [null, "T6副本套装"], + [31095, "遗忘保卫者的头盔"], [31094, "遗忘保卫者的手套"], [31103, "遗忘保卫者的护肩"], + [34851, "遗忘保卫者的护腕"], [31100, "遗忘保卫者的护腿"], [31091, "遗忘保卫者的胸甲"], + [34854, "遗忘保卫者的腰带"], [34857, "遗忘保卫者的长靴"], [31097, "遗忘征服者的头盔"], + [31092, "遗忘征服者的手套"], [31101, "遗忘征服者的护肩"], [34848, "遗忘征服者的护腕"], + [31098, "遗忘征服者的护腿"], [31089, "遗忘征服者的胸甲"], [34853, "遗忘征服者的腰带"], + [34856, "遗忘征服者的长靴"], [31096, "遗忘胜利者的头盔"], [31093, "遗忘胜利者的手套"], + [31102, "遗忘胜利者的护肩"], [34852, "遗忘胜利者的护腕"], [31099, "遗忘胜利者的护腿"], + [31090, "遗忘胜利者的胸甲"], [34855, "遗忘胜利者的腰带"], [34858, "遗忘胜利者的长靴"], + [null, "T5副本套装"], + [30242, "战败勇士的头盔"], [30239, "战败勇士的手套"], [30248, "战败勇士的护肩"], + [30245, "战败勇士的护腿"], [30236, "战败勇士的胸甲"], [30243, "战败卫士的头盔"], + [30240, "战败卫士的手套"], [30249, "战败卫士的护肩"], [30246, "战败卫士的护腿"], + [30237, "战败卫士的胸甲"], [30244, "战败英雄的头盔"], [30241, "战败英雄的手套"], + [30250, "战败英雄的护肩"], [30247, "战败英雄的护腿"], [30238, "战败英雄的胸甲"], + [null, "T4副本套装"], + [29760, "阵亡勇士的头盔"], [29757, "阵亡勇士的手套"], [29763, "阵亡勇士的护肩"], + [29766, "阵亡勇士的护腿"], [29754, "阵亡勇士的胸甲"], [29761, "阵亡卫士的头盔"], + [29758, "阵亡卫士的手套"], [29764, "阵亡卫士的护肩"], [29767, "阵亡卫士的护腿"], + [29753, "阵亡卫士的胸甲"], [29759, "阵亡英雄的头盔"], [29756, "阵亡英雄的手套"], + [29762, "阵亡英雄的护肩"], [29765, "阵亡英雄的护腿"], [29755, "阵亡英雄的胸甲"], + [null, "其他"], + [34350, "上古影月护手"], [34332, "古尔丹的兜帽"], [34208, "均衡肩饰"], [34192, "坚定肩铠"], [34202, "奇迹披肩"], + [34345, "安纳斯特里亚的王冠"], [34233, "幻化光芒长袍"], [34209, "拓荒护肩"], [34245, "智者乌索尔之帽"], [34234, "暗影爆发护手"], + [34170, "止战马裤"], [34243, "燃烧的正义"], [34244, "狡诈之盔"], [34195, "盛怒护肩"], [34167, "神圣信仰腿铠"], + [34351, "稳固王权护手"], [34339, "纯净圣光兜帽"], [34169, "自然侵攻护腿"], [34216, "英勇仲裁者胸甲"], [34193, "萨拉斯救世主护肩"], + [34211, "野兽本能外套"], [34212, "阳光外衣"], [34186, "雷暴锁链护腿"], [34229, "静谧海岸外衣"], [34180, "魔怒腿铠"], + [34215, "鲁莽怒火战甲"], [34342, "黎明护手"] ], queststart: [ [3, "物品"], [1, "NPC"], [2, "物件"] ], questend: [ [1, "NPC"], [2, "物件"] ], @@ -3846,7 +3897,7 @@ var LANG = { [31, "Increase Run Speed %"], [32, "Mod Mounted Speed %"], [33, "Decrease Run Speed %"], [34, "Mod Maximum Health - Flat"], [35, "Mod Maximum Power - Flat"], [36, "Shapeshift"], [37, "Spell Effect Immunity"], [38, "Spell Aura Immunity"], [39, "Spell School Immunity"], [40, "Damage Immunity"], [41, "Dispel Type Immunity"], [42, "Proc Trigger Spell"], [43, "Proc Trigger Damage"], [44, "Track Creatures"], [45, "Track Resources"], - [46, "Ignore All Gear"], [47, "Mod Parry %"], /* [48, null] */ [49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], + [46, "Ignore All Gear"], [47, "Mod Parry %"], [48, "Periodic Trigger Spell from Client"],[49, "Mod Dodge %"], [50, "Mod Critical Healing Amount %"], [51, "Mod Block %"], [52, "Mod Physical Crit Chance"], [53, "Periodically Drain Health"], [54, "Mod Physical Hit Chance"], [55, "Mod Spell Hit Chance"], [56, "Transform"], [57, "Mod Spell Crit Chance"], [58, "Increase Swim Speed %"], [59, "Mod Damage Done Versus Creature"],[60, "Pacify & Silence"], [61, "Mod Size %"], [62, "Periodically Transfer Health"], [63, "Periodically Transfer Power"], [64, "Periodically Drain Power"], [65, "Mod Spell Haste % (not stacking)"], @@ -3928,11 +3979,10 @@ var LANG = { [131, "Play Sound"], [132, "Play Music"], [133, "Unlearn Specialization"], [134, "Kill Credit2"], [135, "Call Pet"], [136, "Heal for % of Total Health"], [137, "Give % of Total Power"], [138, "Leap Back"], [139, "Abandon Quest"], [140, "Force Spell Cast"], [141, "Force Spell Cast with Value"], [142, "Trigger Spell with Value"], [143, "Apply Area Aura - Pet Owner"], [144, "Knockback to Dest."], [145, "Pull Towards Dest."], - [146, "Activate Rune"], [147, "Fail Quest"], [149, "Charge to Dest."], [150, "Start Quest"], + [146, "Activate Rune"], [147, "Fail Quest"], [148, "Trigger Missile with Value"], [149, "Charge to Dest."], [150, "Start Quest"], [151, "Trigger Spell 2"], [152, "Summon - Refer-A-Friend"], [153, "Create Tamed Pet"], [154, "Discover Flight Path"], [155, "Dual Wield 2H Weapons"], - [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], - [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"], - [167, "Update Player Phase"] + [156, "Add Socket to Item"], [157, "Create Tradeskill Item"], [158, "Milling"], [159, "Rename Pet"], [160, "Force Cast 2"], + [161, "Change Talent Spec. Count"], [162, "Activate Talent Spec."], [164, "Remove Aura"] ], damagetype: [ [1, "无"], [2, "魔法"], [3, "近战"], [4, "远程"] @@ -4158,6 +4208,7 @@ var LANG = { id: "ID", classspecific: "职业专属", racespecific: "种族特定", + setspvpflag: "保持PvP旗帜标记", sepgainsrewards: "收获/奖励", experiencegained: "获得的经验", @@ -4928,10 +4979,18 @@ var LANG = { /* AoWoW: start custom */ + // Profiler completions import + completion: '达成', // WH.TERMS.completion + complete: '完成', // WH.TERMS.complete + incomplete: '未完成', // WH.TERMS.incomplete + parens_format: '$1($2)', // WH.TERMS.parens_format + // click to copy fn copied: '已复制', clickToCopy: '点击复制', nothingToCopy_tip: '[Nothing to copy!]', + copy_clipboard: '复制', + copy_format: '[Copy %s]', // TC conditions display tab_conditions: '[Conditions]', diff --git a/static/js/maps.js b/static/js/maps.js index 03224dd8..368ae8e7 100644 --- a/static/js/maps.js +++ b/static/js/maps.js @@ -88,4 +88,4 @@ function ma_UpdateLink(_) { } $WH.ge('link-to-this-map').href = b; -}; +} diff --git a/static/js/petcalc.js b/static/js/petcalc.js index 824e6baf..2d4c85ad 100644 --- a/static/js/petcalc.js +++ b/static/js/petcalc.js @@ -119,4 +119,4 @@ function pc_readPound() { pc_object.setWhBuild(pc_build); } } -}; +} diff --git a/static/js/profile.js b/static/js/profile.js index 36a7369f..bf750c55 100644 --- a/static/js/profile.js +++ b/static/js/profile.js @@ -588,7 +588,7 @@ function pr_addEquipButton(id, itemId) $('#' + id).append(button); - return; + } }); } diff --git a/static/js/talent.js b/static/js/talent.js index 61cf8c8b..cc1ebdf3 100644 --- a/static/js/talent.js +++ b/static/js/talent.js @@ -143,4 +143,4 @@ function tc_readPound() { } } } -}; +} diff --git a/static/js/user.js b/static/js/user.js index 40b34959..6cdf2450 100644 --- a/static/js/user.js +++ b/static/js/user.js @@ -165,7 +165,7 @@ Listview.funcBox.beforeUserComments = function() }).bind(this); $WH.ae(d, i); } - }; + } this.customFilter = function (comment, i) { diff --git a/static/js/video.js b/static/js/video.js index dc00cb8a..58059ace 100644 --- a/static/js/video.js +++ b/static/js/video.js @@ -801,7 +801,7 @@ function () { if (!resizing) { // aowow - /uploads/videos/ not seen on server // aOriginal.href = g_staticUrl + '/uploads/videos/' + (video.pending ? 'pending' : 'normal') + '/' + video.id + '.jpg'; - aOriginal.href = $WH.sprintf(vi_siteurls[video.videoType], video.videoId);; + aOriginal.href = $WH.sprintf(vi_siteurls[video.videoType], video.videoId); var hasFrom = video.date && video.user; if (hasFrom) { var diff --git a/template/bricks/announcement.tpl.php b/template/bricks/announcement.tpl.php index 4f674a06..4b5313ee 100644 --- a/template/bricks/announcement.tpl.php +++ b/template/bricks/announcement.tpl.php @@ -1,9 +1,13 @@ <?php namespace Aowow\Template; + /** @var PageTemplate $this */ + foreach ($this->announcements as $a): ?> + <div id="announcement-<?=$a->id;?>"></div> <script type="text/javascript"> <?=$a;?> </script> + <?php endforeach; ?> diff --git a/template/bricks/book.tpl.php b/template/bricks/book.tpl.php index 2ef56dc4..e7b3922c 100644 --- a/template/bricks/book.tpl.php +++ b/template/bricks/book.tpl.php @@ -3,7 +3,10 @@ use \Aowow\Lang; + /** @var PageTemplate $this */ + if ($this->book): ?> + <div class="clear"></div> <h3><?=Lang::item('content'); ?></h3> @@ -11,4 +14,5 @@ if ($this->book): ?> <script>//<![CDATA[ <?=$this->book; ?> //]]></script> + <?php endif; ?> diff --git a/template/bricks/contribute.tpl.php b/template/bricks/contribute.tpl.php index 20d9834a..6f33b293 100644 --- a/template/bricks/contribute.tpl.php +++ b/template/bricks/contribute.tpl.php @@ -1,8 +1,15 @@ -<?php namespace Aowow; ?> +<?php + namespace Aowow\Template; + + use \Aowow\Lang; + + /** @var \PageTemplate $this */ +?> <?php if ($this->contribute): ?> + <div class="clear"></div> <div class="text"> <h2><?=Lang::main('contribute'); ?></h2> @@ -10,25 +17,30 @@ if ($this->contribute): <div id="tabs-contribute-generic" style="width: 50%"></div> <div class="text" style="margin-right: 310px"> <div class="tabbed-contents" style="clear: none"> + <?php $this->localizedBrick('contrib', ['coError' => $this->community['coError'], 'ssError' => $this->community['ssError'], 'viError' => $this->community['viError']]); ?> + </div> </div> <script type="text/javascript"> var tabsContribute = new Tabs({parent: $WH.ge('tabs-contribute-generic')}); + <?php if ($this->contribute & CONTRIBUTE_CO): - echo " tabsContribute.add(LANG.tab_addyourcomment, {id: 'add-your-comment'});\n"; + echo " tabsContribute.add(LANG.tab_addyourcomment, {id: 'add-your-comment'});".PHP_EOL; endif; if ($this->contribute & CONTRIBUTE_SS): - echo " tabsContribute.add(LANG.tab_submitascreenshot, {id: 'submit-a-screenshot'});\n"; + echo " tabsContribute.add(LANG.tab_submitascreenshot, {id: 'submit-a-screenshot'});".PHP_EOL; endif; if ($this->contribute & CONTRIBUTE_VI): - echo " if (g_user && g_user.roles & (U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO))\n"; - echo " tabsContribute.add(LANG.tab_suggestavideo, {id: 'suggest-a-video'});\n"; + echo " if (g_user && g_user.roles & (U_GROUP_ADMIN | U_GROUP_BUREAU | U_GROUP_VIDEO))".PHP_EOL; + echo " tabsContribute.add(LANG.tab_suggestavideo, {id: 'suggest-a-video'});".PHP_EOL; endif; ?> + tabsContribute.flush(); </script> + <?php endif; ?> diff --git a/template/bricks/footer.tpl.php b/template/bricks/footer.tpl.php index 73eb9586..d42b2964 100644 --- a/template/bricks/footer.tpl.php +++ b/template/bricks/footer.tpl.php @@ -2,31 +2,35 @@ namespace Aowow\Template; use \Aowow\Lang; + + /** @var PageTemplate $this */ ?> <div class="footer"> + <?php if ($this->pageStats): - echo " <table style=\"margin:auto;\">\n"; + echo ' <table style="margin:auto;">'.PHP_EOL; if ($x = $this->pageStats['sql']): - echo ' <tr><td style="text-align:left;">'.Lang::main('numSQL') .'</td><td>'.$x['count']."</td></tr>\n"; - echo ' <tr><td style="text-align:left;">'.Lang::main('timeSQL').'</td><td>'.$x['time']."</td></tr>\n"; + echo ' <tr><td style="text-align:left;">'.Lang::main('numSQL') .'</td><td>'.$x['count']."</td></tr>".PHP_EOL; + echo ' <tr><td style="text-align:left;">'.Lang::main('timeSQL').'</td><td>'.$x['time']."</td></tr>".PHP_EOL; endif; if ($x = $this->pageStats['time']): - echo ' <tr><td style="text-align:left;">Page generated in</td><td>'.$x."</td></tr>\n"; + echo ' <tr><td style="text-align:left;">Page generated in</td><td>'.$x."</td></tr>".PHP_EOL; endif; if ($this->pageStats['cache'] && $this->pageStats['cache'][0] == CACHE_MODE_FILECACHE): - echo " <tr><td style=\"text-align:left;\">Stored in filecache</td><td>".$this->pageStats['cache'][1]."</td></tr>\n"; + echo ' <tr><td style="text-align:left;">Stored in filecache</td><td>'.$this->pageStats['cache'][1].'</td></tr>'.PHP_EOL; elseif ($this->pageStats['cache'] && $this->pageStats['cache'][0] == CACHE_MODE_MEMCACHED): - echo " <tr><td style=\"text-align:left;\">Stored in Memcached</td><td>".$this->pageStats['cache'][1]."</td></tr>\n"; + echo ' <tr><td style="text-align:left;">Stored in Memcached</td><td>'.$this->pageStats['cache'][1].'</td></tr>'.PHP_EOL; endif; - echo " </table>\n"; + echo ' </table>'.PHP_EOL; endif; ?> + </div> </div><!-- #wrapper .nosidebar --> </div><!-- #layout-inner --> @@ -44,6 +48,8 @@ endif; <script type="text/javascript"> window.open("/", "SqlLog", "width=1800,height=200,top=100,left=100,status=no,location=no,toolbar=no,menubar=no")?.document?.write('<?=$this->dbProfiles;?>'); </script> + <?php endif; ?> + </body> </html> diff --git a/template/bricks/head.tpl.php b/template/bricks/head.tpl.php index 0306305b..e279d8ff 100644 --- a/template/bricks/head.tpl.php +++ b/template/bricks/head.tpl.php @@ -2,6 +2,8 @@ namespace Aowow\Template; use \Aowow\Lang; + + /** @var PageTemplate $this */ ?> <title><?=$this->concat('title', ' - '); ?> @@ -14,36 +16,39 @@ var g_serverTime = gServerTime; ?>; var g_staticUrl = "gStaticUrl; ?>"; var g_host = "gHost; ?>"; + gDataKey): - echo " var g_dataKey = '".$_SESSION['dataKey']."'\n"; + echo " var g_dataKey = '".$_SESSION['dataKey']."'".PHP_EOL; endif; ?> + renderArray('js', 4); ?> -analyticsTag): ?> - +hasAnalytics): ?> + rss): ?> + + diff --git a/template/bricks/headIcons.tpl.php b/template/bricks/headIcons.tpl.php index d6fdb62d..5d1225c1 100644 --- a/template/bricks/headIcons.tpl.php +++ b/template/bricks/headIcons.tpl.php @@ -1,18 +1,24 @@ headIcons): foreach ($this->headIcons as $k => $v): - echo '
    \n"; + echo '
    '.PHP_EOL; endforeach; ?> + + diff --git a/template/bricks/header.tpl.php b/template/bricks/header.tpl.php index ae143fab..6910b755 100644 --- a/template/bricks/header.tpl.php +++ b/template/bricks/header.tpl.php @@ -1,14 +1,19 @@ + + brick('head'); ?> user::isPremium() ? ' class="premium-logo"' : ''); ?>>
    + headerLogo): ?> + +